[
  {
    "path": ".coderabbit.yaml",
    "content": "# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json\n\nlanguage: 'en-US'\nearly_access: false\ntone_instructions: 'You are an expert code reviewer in TypeScript, JavaScript, NodeJS, and ElectronJS. You work in an enterprise software developer team, providing concise and clear code review advice. You only elaborate or provide detailed explanations when requested.'\n\nknowledge_base:\n  opt_out: false\n  code_guidelines:\n    enabled: true\n    filePatterns:\n      - '**/CODING_STANDARDS.md'\n\nreviews:\n  profile: 'chill'\n  request_changes_workflow: false\n  high_level_summary: true\n  poem: true\n  review_status: true\n  collapse_walkthrough: false\n  auto_review:\n    enabled: true\n    drafts: false\n    base_branches: ['main', 'release/*']\n  path_instructions:\n    - path: '**/*'\n      instructions: |\n        Bruno is a cross-platform Electron desktop app that runs on macOS, Windows, and Linux. Ensure that all code is OS-agnostic:\n        - File paths must use `path.join()` or `path.resolve()` instead of hardcoded `/` or `\\\\` separators\n        - Never assume case-sensitive or case-insensitive filesystems\n        - Use `os.homedir()`, `app.getPath()`, or environment-appropriate APIs instead of hardcoded paths like `/home/`, `C:\\\\Users\\\\`, or `~/`\n        - Line endings should be handled consistently (be aware of CRLF vs LF issues)\n        - Use `path.sep` or `path.posix`/`path.win32` when platform-specific separators are needed\n        - Shell commands or child_process calls must account for platform differences (e.g., `which` vs `where`, `/bin/sh` vs `cmd.exe`)\n        - File permissions (e.g., `fs.chmod`, `fs.access`) should account for Windows not supporting Unix-style permission bits\n        - Avoid relying on Unix-only signals (e.g., `SIGKILL`) without Windows fallbacks\n        - Use `os.tmpdir()` instead of hardcoding `/tmp`\n        - Environment variable access should handle platform differences (e.g., `HOME` vs `USERPROFILE`)\n    - path: 'tests/**/**.*'\n      instructions: |\n        Review the following e2e test code written using the Playwright test library. Ensure that:\n        - Follow best practices for Playwright code and e2e automation\n        - Try to reduce usage of `page.waitForTimeout();` in code unless absolutely necessary and the locator cannot be found using existing `expect()` playwright calls\n        - Avoid using `page.pause()` in code\n        - Use locator variables for locators\n        - Avoid using test.only\n        - Use multiple assertions\n        - Promote the use of `test.step` as much as possible so the generated reports are easier to read\n        - Ensure that the `fixtures` like the collections are nested inside the `fixtures` folder \n          \n\n\n          **Fixture Example***: Here's an example of possible fixture and test pair\n          ```\n          .\n          ├── fixtures\n          │   └── collection\n          │       ├── base.bru\n          │       ├── bruno.json\n          │       ├── collection.bru\n          │       ├── ws-test-request-with-headers.bru\n          │       ├── ws-test-request-with-subproto.bru\n          │       └── ws-test-request.bru\n          ├── connection.spec.ts # <- Depends on the collection in ./fixtures/collection\n          ├── headers.spec.ts\n          ├── persistence.spec.ts\n          ├── variable-interpolation\n          │   ├── fixtures\n          │   │   └── collection\n          │   │       ├── environments\n          │   │       ├── bruno.json\n          │   │       └── ws-interpolation-test.bru\n          │   ├── init-user-data\n          │   └── variable-interpolation.spec.ts # <- Depends on the collection in ./variable-interpolation/fixtures/collection\n          └── subproto.spec.ts\n          ```\n\nchat:\n  auto_reply: true\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "* @helloanoop @maintainer-bruno @bijin-bruno @lohit-bruno @naman-bruno\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/BugReport.yaml",
    "content": "name: Bug Report\ndescription: File a bug report\nlabels: ['bug']\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this bug report!\n\n        Before submitting, please make sure you've searched existing issues:\n        👉 [Search existing issues](https://github.com/usebruno/bruno/issues?q=is%3Aissue)\n\n  - type: checkboxes\n    attributes:\n      label: 'I have checked the following:'\n      options:\n        - label: \"I have searched existing issues and found nothing related to my issue.\"\n          required: true\n\n  - type: checkboxes\n    attributes:\n      label: 'This bug is:'\n      options:\n        - label: making Bruno unusable for me\n          required: false\n        - label: slowing me down but I'm able to continue working\n          required: false\n        - label: annoying\n          required: false\n        - label: this feature was working in a previous version but is broken in the current release.\n          required: false\n\n  - type: input\n    attributes:\n      label: Bruno version\n      description: Please specify the version of Bruno you are using in which the issue occurs.\n      placeholder: 1.38.1\n    validations:\n      required: true\n\n  - type: input\n    attributes:\n      label: Operating System\n      description: Information about the operating system the issue occurs on.\n      placeholder: Windows 11 26100.3037 / macOS 15.1 (24B83) / Linux 6.13.1\n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: Describe the bug\n      description: A clear and concise description of the bug and how it's effecting your work along with steps to reproduce. \n    validations:\n      required: true\n\n  - type: textarea\n    attributes:\n      label: .bru file to reproduce the bug\n      description: Attach your .bru file here that can reproduce the problem.\n    validations:\n      required: false\n\n  - type: textarea\n    attributes:\n      label: Screenshots/Live demo link\n      description: Add some screenshots to help explain the problem.\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/FeatureRequest.yaml",
    "content": "name: Feature Request\ndescription: Suggest an idea for this project.\nlabels: ['enhancement']\nbody:\n  - type: checkboxes\n    attributes:\n      label: 'I have checked the following:'\n      options:\n        - label: I've searched existing issues and found nothing related to my issue.\n          required: true\n  - type: checkboxes\n    attributes:\n      label: 'This feature'\n      options:\n        - label: blocks me from using Bruno \n          required: false\n        - label: would improve my quality of life in Bruno \n          required: false\n        - label: is something I've never seen an API client do before\n          required: false\n  - type: markdown\n    attributes:\n      value: |\n        Suggest an idea for this project.\n  - type: textarea\n    attributes:\n      label: Describe the feature you want to add, and how it would change your usage of Bruno \n      description: A clear and concise description of the feature you want to be added.\n    validations:\n      required: true\n  - type: textarea\n    attributes:\n      label: Mockups or Images of the feature\n      description: Add some images to support your feature.\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yaml",
    "content": "blank_issues_enabled: true\ncontact_links:\n  - name: Discussions\n    url: https://github.com/usebruno/bruno/discussions\n    about: You can ask general questions or give feedback here.\n  - name: Discord Server\n    url: https://discord.com/invite/KgcZUncpjq\n    about: Join our Discord community to chat about Bruno.\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "### Description\n\n<!-- Explain here the changes your PR introduces and text to help us understand the context of this change. -->\n\n#### Contribution Checklist:\n\n- [ ] **I've used AI significantly to create this pull request**\n- [ ] **The pull request only addresses one issue or adds one feature.**\n- [ ] **The pull request does not introduce any breaking changes**\n- [ ] **I have added screenshots or gifs to help explain the change if applicable.**\n- [ ] **I have read the [contribution guidelines](https://github.com/usebruno/bruno/blob/main/contributing.md).**\n- [ ] **Create an issue and link to the pull request.**\n\nNote: Keeping the PR small and focused helps make it easier to review and merge. If you have multiple changes you want to make, please consider submitting them as separate pull requests.\n\n#### Publishing to New Package Managers\n\nPlease see [here](../publishing.md) for more information.\n"
  },
  {
    "path": ".github/actions/common/setup-node-deps/action.yml",
    "content": "name: 'Setup Node Dependencies'\ndescription: 'Install Node.js and npm dependencies'\ninputs:\n  skip-build:\n    description: 'Skip building libraries'\n    required: false\n    default: 'false'\nruns:\n  using: 'composite'\n  steps:\n    - name: Setup Node.js\n      uses: actions/setup-node@v4\n      with:\n        node-version: v22.17.0\n        cache: 'npm'\n        cache-dependency-path: './package-lock.json'\n\n    - name: Install node dependencies\n      shell: bash\n      run: npm ci --legacy-peer-deps\n\n    - name: Build libraries\n      if: inputs.skip-build != 'true'\n      shell: bash\n      run: |\n        npm run build:graphql-docs\n        npm run build:bruno-query\n        npm run build:bruno-common\n        npm run sandbox:bundle-libraries --workspace=packages/bruno-js\n        npm run build:bruno-converters\n        npm run build:bruno-requests\n        npm run build:schema-types\n        npm run build:bruno-filestore\n"
  },
  {
    "path": ".github/actions/ssl/linux/run-basic-ssl-cli-tests/action.yml",
    "content": "name: 'Run Basic SSL CLI Tests - Linux'\ndescription: 'Run basic SSL CLI tests on Linux'\nruns:\n  using: 'composite'\n  steps:\n    - name: Run CLI tests\n      shell: bash\n      run: |\n        set -euo pipefail\n        \n        # navigate to basic SSL test collection directory\n        cd tests/ssl/basic-ssl/collections/badssl\n\n        echo \"basic ssl success\"\n        # should pass\n        node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --insecure --format junit\n        xmllint --xpath 'count(//testsuite[@errors=\"0\"])' junit1.xml | grep -q \"^1$\" || exit 1\n\n        echo \"with default/system ca certs\"\n        # should pass\n        node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --format junit\n        xmllint --xpath 'count(//testsuite[@errors=\"0\"])' junit2.xml | grep -q \"^1$\" || exit 1\n\n        # navigate to self-signed SSL test collection directory\n        cd ../self-signed-badssl\n\n        echo \"self-signed ssl with validation disabled\"\n        # should pass\n        node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit3.xml --insecure --format junit\n        xmllint --xpath 'count(//testsuite[@errors=\"0\"])' junit3.xml | grep -q \"^1$\" || exit 1\n\n        echo \"self-signed ssl with default/system ca certs\"\n        echo \"request will error\"\n        # should fail\n        node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --format junit 2>/dev/null || true\n        xmllint --xpath 'count(//testsuite[@errors=\"1\"])' junit4.xml | grep -q \"^1$\" || exit 1\n"
  },
  {
    "path": ".github/actions/ssl/linux/run-custom-ca-certs-cli-tests/action.yml",
    "content": "name: 'Run Custom CA Certs CLI Tests - Linux'\ndescription: 'Run custom CA certs CLI tests on Linux'\nruns:\n  using: 'composite'\n  steps:\n    - name: Run CLI tests\n      shell: bash\n      run: |\n        set -euo pipefail\n        \n        # navigate to CA certificates test collection directory\n        cd tests/ssl/custom-ca-certs/collection\n\n        echo \"custom valid ca cert\"\n        # should pass\n        node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --cacert ../server/certs/ca-cert.pem --ignore-truststore --format junit\n        xmllint --xpath 'count(//testsuite[@errors=\"0\"])' junit1.xml | grep -q \"^1$\" || exit 1\n\n        echo \"custom valid ca cert with defaults\"\n        # should pass\n        node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --cacert ../server/certs/ca-cert.pem --format junit\n        xmllint --xpath 'count(//testsuite[@errors=\"0\"])' junit2.xml | grep -q \"^1$\" || exit 1\n\n        echo \"custom invalid ca cert\"\n        echo \"request will error\"\n        # should fail\n        node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit3.xml --cacert ../server/certs/ca-key.pem --ignore-truststore --format junit 2>/dev/null || true\n        xmllint --xpath 'count(//testsuite[@errors=\"1\"])' junit3.xml | grep -q \"^1$\" || exit 1\n\n        echo \"custom invalid ca cert with defaults\"\n        # should pass\n        node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --cacert ../server/certs/ca-key.pem --format junit\n        xmllint --xpath 'count(//testsuite[@errors=\"0\"])' junit4.xml | grep -q \"^1$\" || exit 1\n"
  },
  {
    "path": ".github/actions/ssl/linux/run-ssl-e2e-tests/action.yml",
    "content": "name: 'Run SSL E2E Tests - Linux'\ndescription: 'Run SSL E2E tests on Linux'\nruns:\n  using: 'composite'\n  steps:\n    - name: Run E2E tests\n      shell: bash\n      run: |\n        set -euo pipefail\n        \n        xvfb-run npm run test:e2e:ssl\n\n    - name: Upload Playwright Report\n      if: ${{ !cancelled() }}\n      uses: actions/upload-artifact@v4\n      with:\n        name: playwright-report-linux\n        path: playwright-report/\n        retention-days: 30\n"
  },
  {
    "path": ".github/actions/ssl/linux/setup-ca-certs/action.yml",
    "content": "name: 'Setup CA Certificates - Linux'\ndescription: 'Setup CA certificates and start test server for custom CA certs tests on Linux'\nruns:\n  using: 'composite'\n  steps:\n    - name: Setup CA certificates\n      shell: bash\n      run: |\n        set -euo pipefail\n        \n        cd tests/ssl/custom-ca-certs/server\n        \n        echo \"running certificate setup\"\n        node scripts/generate-certs.js\n\n    - name: Start test server\n      shell: bash\n      run: |\n        set -euo pipefail\n        \n        cd tests/ssl/custom-ca-certs/server\n        \n        echo \"starting server in background\"\n        node index.js &\n        \n        echo \"server started with PID: $!\"\n"
  },
  {
    "path": ".github/actions/ssl/linux/setup-feature-specific-deps/action.yml",
    "content": "name: 'Setup Custom CA Certs Feature Dependencies - Linux'\ndescription: 'Setup feature-specific dependencies for custom CA certs tests on Linux'\nruns:\n  using: 'composite'\n  steps:\n    - name: Install additional OS dependencies for custom CA certs\n      shell: bash\n      run: |\n        sudo apt-get update\n        sudo apt-get --no-install-recommends install -y \\\n          libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \\\n          xvfb libxml2-utils\n\n        sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox\n        sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox\n"
  },
  {
    "path": ".github/actions/ssl/macos/run-basic-ssl-cli-tests/action.yml",
    "content": "name: 'Run Basic SSL CLI Tests - macOS'\ndescription: 'Run basic SSL CLI tests on macOS'\nruns:\n  using: 'composite'\n  steps:\n    - name: Run CLI tests\n      shell: bash\n      run: |\n        set -euo pipefail\n        \n        # navigate to basic SSL test collection directory\n        cd tests/ssl/basic-ssl/collections/badssl\n\n        echo \"basic ssl success\"\n        # should pass\n        node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --insecure --format junit\n        xmllint --xpath 'count(//testsuite[@errors=\"0\"])' junit1.xml | grep -q \"^1$\" || exit 1\n\n        echo \"with default/system ca certs\"\n        # should pass\n        node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --format junit\n        xmllint --xpath 'count(//testsuite[@errors=\"0\"])' junit2.xml | grep -q \"^1$\" || exit 1\n\n        # navigate to self-signed SSL test collection directory\n        cd ../self-signed-badssl\n\n        echo \"self-signed ssl with validation disabled\"\n        # should pass\n        node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit3.xml --insecure --format junit\n        xmllint --xpath 'count(//testsuite[@errors=\"0\"])' junit3.xml | grep -q \"^1$\" || exit 1\n\n        echo \"self-signed ssl with default/system ca certs\"\n        echo \"request will error\"\n        # should fail\n        node ../../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --format junit 2>/dev/null || true\n        xmllint --xpath 'count(//testsuite[@errors=\"1\"])' junit4.xml | grep -q \"^1$\" || exit 1\n"
  },
  {
    "path": ".github/actions/ssl/macos/run-custom-ca-certs-cli-tests/action.yml",
    "content": "name: 'Run Custom CA Certs CLI Tests - macOS'\ndescription: 'Run custom CA certs CLI tests on macOS'\nruns:\n  using: 'composite'\n  steps:\n    - name: Run CLI tests\n      shell: bash\n      run: |\n        set -euo pipefail\n        \n        # navigate to CA certificates test collection directory\n        cd tests/ssl/custom-ca-certs/collection\n\n        echo \"custom valid ca cert\"\n        # should pass\n        node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit1.xml --cacert ../server/certs/ca-cert.pem --ignore-truststore --format junit\n        xmllint --xpath 'count(//testsuite[@errors=\"0\"])' junit1.xml | grep -q \"^1$\" || exit 1\n\n        echo \"custom valid ca cert with defaults\"\n        # should pass\n        node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit2.xml --cacert ../server/certs/ca-cert.pem --format junit\n        xmllint --xpath 'count(//testsuite[@errors=\"0\"])' junit2.xml | grep -q \"^1$\" || exit 1\n\n        echo \"custom invalid ca cert\"\n        echo \"request will error\"\n        # should fail\n        node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit3.xml --cacert ../server/certs/ca-key.pem --ignore-truststore --format junit 2>/dev/null || true\n        xmllint --xpath 'count(//testsuite[@errors=\"1\"])' junit3.xml | grep -q \"^1$\" || exit 1\n\n        echo \"custom invalid ca cert with defaults\"\n        # should pass\n        node ../../../../packages/bruno-cli/bin/bru.js run ./request.bru --output junit4.xml --cacert ../server/certs/ca-key.pem --format junit\n        xmllint --xpath 'count(//testsuite[@errors=\"0\"])' junit4.xml | grep -q \"^1$\" || exit 1\n"
  },
  {
    "path": ".github/actions/ssl/macos/run-ssl-e2e-tests/action.yml",
    "content": "name: 'Run SSL E2E Tests - macOS'\ndescription: 'Run SSL E2E tests on macOS'\nruns:\n  using: 'composite'\n  steps:\n    - name: Run E2E tests\n      shell: bash\n      run: |\n        npm run test:e2e:ssl\n\n    - name: Upload Playwright Report\n      if: ${{ !cancelled() }}\n      uses: actions/upload-artifact@v4\n      with:\n        name: playwright-report-macos\n        path: playwright-report/\n        retention-days: 30\n"
  },
  {
    "path": ".github/actions/ssl/macos/setup-ca-certs/action.yml",
    "content": "name: 'Setup CA Certificates - macOS'\ndescription: 'Setup CA certificates and start test server for custom CA certs tests on macOS'\nruns:\n  using: 'composite'\n  steps:\n    - name: Setup CA certificates\n      shell: bash\n      run: |\n        set -euo pipefail\n        \n        cd tests/ssl/custom-ca-certs/server\n        \n        echo \"running certificate setup\"\n        node scripts/generate-certs.js\n\n    - name: Start test server\n      shell: bash\n      run: |\n        set -euo pipefail\n        \n        cd tests/ssl/custom-ca-certs/server\n        \n        echo \"starting server in background\"\n        node index.js &\n        \n        echo \"server started with PID: $!\"\n"
  },
  {
    "path": ".github/actions/ssl/macos/setup-feature-specific-deps/action.yml",
    "content": "name: 'Setup Custom CA Certs Feature Dependencies - macOS'\ndescription: 'Setup feature-specific dependencies for custom CA certs tests on macOS'\nruns:\n  using: 'composite'\n  steps:\n    - name: Install additional OS dependencies for custom CA certs\n      shell: bash\n      run: |\n        brew install libxml2\n"
  },
  {
    "path": ".github/actions/ssl/windows/run-basic-ssl-cli-tests/action.yml",
    "content": "name: 'Run Basic SSL CLI Tests - Windows'\ndescription: 'Run basic SSL CLI tests on Windows'\nruns:\n  using: 'composite'\n  steps:\n    - name: Run CLI tests\n      shell: pwsh\n      run: |\n        Set-StrictMode -Version Latest\n        $ErrorActionPreference = \"Stop\"\n        \n        # navigate to basic SSL test collection directory\n        Set-Location tests\\ssl\\basic-ssl\\collections\\badssl\n\n        Write-Host \"basic ssl success\"\n        # should pass\n        $process = Start-Process -FilePath \"node\" -ArgumentList \"..\\..\\..\\..\\..\\packages\\bruno-cli\\bin\\bru.js run .\\request.bru --output junit1.xml --insecure --format junit\" -NoNewWindow -Wait -PassThru -RedirectStandardError \"nul\"\n        [xml]$xml1 = Get-Content junit1.xml\n        $testsuites1 = if ($xml1.testsuites) { $xml1.testsuites.testsuite } else { $xml1.testsuite }\n        $errorCount1 = ($testsuites1 | Where-Object { $_.errors -eq \"0\" } | Measure-Object).Count\n        if ($errorCount1 -ne 1) { exit 1 }\n\n        Write-Host \"with default/system ca certs\"\n        # should pass\n        $process = Start-Process -FilePath \"node\" -ArgumentList \"..\\..\\..\\..\\..\\packages\\bruno-cli\\bin\\bru.js run .\\request.bru --output junit2.xml --format junit\" -NoNewWindow -Wait -PassThru -RedirectStandardError \"nul\"\n        [xml]$xml2 = Get-Content junit2.xml\n        $testsuites2 = if ($xml2.testsuites) { $xml2.testsuites.testsuite } else { $xml2.testsuite }\n        $errorCount2 = ($testsuites2 | Where-Object { $_.errors -eq \"0\" } | Measure-Object).Count\n        if ($errorCount2 -ne 1) { exit 1 }\n\n        # navigate to self-signed SSL test collection directory\n        Set-Location ..\\self-signed-badssl\n\n        Write-Host \"self-signed ssl with validation disabled\"\n        # should pass\n        $process = Start-Process -FilePath \"node\" -ArgumentList \"..\\..\\..\\..\\..\\packages\\bruno-cli\\bin\\bru.js run .\\request.bru --output junit3.xml --insecure --format junit\" -NoNewWindow -Wait -PassThru -RedirectStandardError \"nul\"\n        [xml]$xml3 = Get-Content junit3.xml\n        $testsuites3 = if ($xml3.testsuites) { $xml3.testsuites.testsuite } else { $xml3.testsuite }\n        $errorCount3 = ($testsuites3 | Where-Object { $_.errors -eq \"0\" } | Measure-Object).Count\n        if ($errorCount3 -ne 1) { exit 1 }\n\n        Write-Host \"self-signed ssl with default/system ca certs\"\n        Write-Host \"request will error\"\n        # should fail\n        $process = Start-Process -FilePath \"node\" -ArgumentList \"..\\..\\..\\..\\..\\packages\\bruno-cli\\bin\\bru.js run .\\request.bru --output junit4.xml --format junit\" -NoNewWindow -Wait -PassThru -RedirectStandardError \"nul\"\n        # Ignore the exit code - we expect this to fail\n        [xml]$xml4 = Get-Content junit4.xml\n        $testsuites4 = if ($xml4.testsuites) { $xml4.testsuites.testsuite } else { $xml4.testsuite }\n        $errorCount4 = ($testsuites4 | Where-Object { $_.errors -eq \"1\" } | Measure-Object).Count\n        if ($errorCount4 -ne 1) { exit 1 }\n"
  },
  {
    "path": ".github/actions/ssl/windows/run-custom-ca-certs-cli-tests/action.yml",
    "content": "name: 'Run Custom CA Certs CLI Tests - Windows'\ndescription: 'Run custom CA certs CLI tests on Windows'\nruns:\n  using: 'composite'\n  steps:\n    - name: Run CLI tests\n      shell: pwsh\n      run: |\n        Set-StrictMode -Version Latest\n        $ErrorActionPreference = \"Stop\"\n        \n        # navigate to CA certificates test collection directory\n        Set-Location tests\\ssl\\custom-ca-certs\\collection\n\n        Write-Host \"custom valid ca cert\"\n        # should pass\n        $process = Start-Process -FilePath \"node\" -ArgumentList \"..\\..\\..\\..\\packages\\bruno-cli\\bin\\bru.js run .\\request.bru --output junit1.xml --cacert ..\\server\\certs\\ca-cert.pem --ignore-truststore --format junit\" -NoNewWindow -Wait -PassThru -RedirectStandardError \"nul\"\n        [xml]$xml1 = Get-Content junit1.xml\n        $testsuites1 = if ($xml1.testsuites) { $xml1.testsuites.testsuite } else { $xml1.testsuite }\n        $errorCount1 = ($testsuites1 | Where-Object { $_.errors -eq \"0\" } | Measure-Object).Count\n        if ($errorCount1 -ne 1) { exit 1 }\n\n        Write-Host \"custom valid ca cert with defaults\"\n        # should pass\n        $process = Start-Process -FilePath \"node\" -ArgumentList \"..\\..\\..\\..\\packages\\bruno-cli\\bin\\bru.js run .\\request.bru --output junit2.xml --cacert ..\\server\\certs\\ca-cert.pem --format junit\" -NoNewWindow -Wait -PassThru -RedirectStandardError \"nul\"\n        [xml]$xml2 = Get-Content junit2.xml\n        $testsuites2 = if ($xml2.testsuites) { $xml2.testsuites.testsuite } else { $xml2.testsuite }\n        $errorCount2 = ($testsuites2 | Where-Object { $_.errors -eq \"0\" } | Measure-Object).Count\n        if ($errorCount2 -ne 1) { exit 1 }\n\n        Write-Host \"custom invalid ca cert\"\n        Write-Host \"request will error\"\n        # should fail\n        $process = Start-Process -FilePath \"node\" -ArgumentList \"..\\..\\..\\..\\packages\\bruno-cli\\bin\\bru.js run .\\request.bru --output junit3.xml --cacert ..\\server\\certs\\ca-key.pem --ignore-truststore --format junit\" -NoNewWindow -Wait -PassThru -RedirectStandardError \"nul\"\n        # Ignore the exit code - we expect this to fail\n        [xml]$xml3 = Get-Content junit3.xml\n        $testsuites3 = if ($xml3.testsuites) { $xml3.testsuites.testsuite } else { $xml3.testsuite }\n        $errorCount3 = ($testsuites3 | Where-Object { $_.errors -eq \"1\" } | Measure-Object).Count\n        if ($errorCount3 -ne 1) { exit 1 }\n\n        Write-Host \"custom invalid ca cert with defaults\"\n        # should pass\n        $process = Start-Process -FilePath \"node\" -ArgumentList \"..\\..\\..\\..\\packages\\bruno-cli\\bin\\bru.js run .\\request.bru --output junit4.xml --cacert ..\\server\\certs\\ca-key.pem --format junit\" -NoNewWindow -Wait -PassThru -RedirectStandardError \"nul\"\n        [xml]$xml4 = Get-Content junit4.xml\n        $testsuites4 = if ($xml4.testsuites) { $xml4.testsuites.testsuite } else { $xml4.testsuite }\n        $errorCount4 = ($testsuites4 | Where-Object { $_.errors -eq \"0\" } | Measure-Object).Count\n        if ($errorCount4 -ne 1) { exit 1 }\n"
  },
  {
    "path": ".github/actions/ssl/windows/run-ssl-e2e-tests/action.yml",
    "content": "name: 'Run SSL E2E Tests - Windows'\ndescription: 'Run SSL E2E tests on Windows'\nruns:\n  using: 'composite'\n  steps:\n    - name: Run E2E tests\n      shell: pwsh\n      run: |\n        npm run test:e2e:ssl\n\n    - name: Upload Playwright Report\n      if: ${{ !cancelled() }}\n      uses: actions/upload-artifact@v4\n      with:\n        name: playwright-report-windows\n        path: playwright-report/\n        retention-days: 30\n"
  },
  {
    "path": ".github/actions/ssl/windows/setup-ca-certs/action.yml",
    "content": "name: 'Setup CA Certificates - Windows'\ndescription: 'Setup CA certificates and start test server for custom CA certs tests on Windows'\nruns:\n  using: 'composite'\n  steps:\n    - name: Setup CA certificates\n      shell: pwsh\n      run: |\n        Set-StrictMode -Version Latest\n        $ErrorActionPreference = \"Stop\"\n        \n        Set-Location tests\\ssl\\custom-ca-certs\\server\n        \n        Write-Host \"running certificate setup\"\n        node scripts/generate-certs.js\n\n    - name: Start test server\n      shell: pwsh\n      run: |\n        Set-StrictMode -Version Latest\n        \n        Set-Location tests\\ssl\\custom-ca-certs\\server\n        \n        Write-Host \"starting server in background\"\n        Start-Process -FilePath \"node\" -ArgumentList \"index.js\" -PassThru -WindowStyle Hidden\n"
  },
  {
    "path": ".github/actions/tests/run-cli-tests/action.yml",
    "content": "name: 'Run CLI Tests'\ndescription: 'Setup dependencies, start local testbench and run CLI tests'\nruns:\n  using: 'composite'\n  steps:\n    - name: Run Local Testbench\n      shell: bash\n      run: |\n        npm start --workspace=packages/bruno-tests &\n        sleep 5\n\n    - name: Install Test Collection Dependencies\n      shell: bash\n      run: npm ci --prefix packages/bruno-tests/collection\n\n    - name: Run CLI Tests\n      shell: bash\n      run: |\n        cd packages/bruno-tests/collection\n        node ../../bruno-cli/bin/bru.js run --env Prod --output junit.xml --format junit --sandbox developer\n"
  },
  {
    "path": ".github/actions/tests/run-e2e-tests/action.yml",
    "content": "name: 'Run E2E Tests'\ndescription: 'Setup dependencies, configure environment, and run Playwright E2E tests'\ninputs:\n  os:\n    description: 'Operating system (ubuntu, macos, windows)'\n    default: 'ubuntu'\nruns:\n  using: 'composite'\n  steps:\n    - name: Install Test Collection Dependencies\n      shell: bash\n      run: npm ci --prefix packages/bruno-tests/collection\n\n    - name: Run Playwright Tests (Ubuntu)\n      if: inputs.os == 'ubuntu'\n      shell: bash\n      run: xvfb-run npm run test:e2e\n\n    - name: Run Playwright Tests\n      if: inputs.os != 'ubuntu'\n      shell: bash\n      run: npm run test:e2e\n"
  },
  {
    "path": ".github/actions/tests/run-unit-tests/action.yml",
    "content": "name: 'Run Unit Tests'\ndescription: 'Setup dependencies and run unit tests for all packages'\nruns:\n  using: 'composite'\n  steps:\n    - name: Test Package bruno-js\n      shell: bash\n      run: npm run test --workspace=packages/bruno-js\n\n    - name: Test Package bruno-cli\n      shell: bash\n      run: npm run test --workspace=packages/bruno-cli\n\n    - name: Test Package bruno-query\n      shell: bash\n      run: npm run test --workspace=packages/bruno-query\n\n    - name: Test Package bruno-lang\n      shell: bash\n      run: npm run test --workspace=packages/bruno-lang\n\n    - name: Test Package bruno-schema\n      shell: bash\n      run: npm run test --workspace=packages/bruno-schema\n\n    - name: Test Package bruno-app\n      shell: bash\n      run: npm run test --workspace=packages/bruno-app\n\n    - name: Test Package bruno-common\n      shell: bash\n      run: npm run test --workspace=packages/bruno-common\n\n    - name: Test Package bruno-converters\n      shell: bash\n      run: npm run test --workspace=packages/bruno-converters\n\n    - name: Test Package bruno-electron\n      shell: bash\n      run: npm run test --workspace=packages/bruno-electron\n\n    - name: Test Package bruno-requests\n      shell: bash\n      run: npm run test --workspace=packages/bruno-requests\n\n    - name: Test Package bruno-filestore\n      shell: bash\n      run: npm run test --workspace=packages/bruno-filestore\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\n\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: weekly\n  \n  - package-ecosystem: npm\n    directory: \"/\"\n    schedule:\n      interval: weekly\n    groups:\n      bruno-dependencies:\n        patterns:\n          - \"*usebruno*\"\n      babel-dependencies:\n        patterns:\n          - \"*babel*\"\n      fortawesome-dependencies:\n        patterns:\n          - \"*fortawesome*\"\n      electron-dependencies:\n        patterns:\n          - \"*electron*\"\n      rollup-dependencies:\n        patterns:\n          - \"*rollup*\"\n      jest-dependencies:\n        patterns:\n          - \"*jest*\"\n"
  },
  {
    "path": ".github/scripts/comment-on-flaky-tests.js",
    "content": "const fs = require('fs');\nconst { execSync } = require('child_process');\n\n// Check if flaky-tests.json exists\nif (!fs.existsSync('flaky-tests.json')) {\n  console.log('No flaky-tests.json found');\n  process.exit(0);\n}\n\n// Get changed files in PR\nlet changedFiles = [];\ntry {\n  changedFiles = execSync('git diff --name-only origin/main...HEAD')\n    .toString()\n    .split('\\n')\n    .filter(f => f.endsWith('.spec.ts'));\n} catch (error) {\n  console.log('Could not determine changed files:', error.message);\n  process.exit(0);\n}\n\nif (changedFiles.length === 0) {\n  console.log('No test files were modified in this PR');\n  process.exit(0);\n}\n\n// Read flaky tests\nconst flakyTests = JSON.parse(fs.readFileSync('flaky-tests.json', 'utf8'));\n\nif (flakyTests.length === 0) {\n  console.log('No flaky/failed tests found');\n  process.exit(0);\n}\n\n// Find modified flaky tests\nconst modifiedFlakyTests = flakyTests.filter(test =>\n  changedFiles.some(file => test.file.includes(file))\n);\n\nif (modifiedFlakyTests.length === 0) {\n  console.log('No modified test files are flaky');\n  process.exit(0);\n}\n\n// Generate comment markdown\nlet comment = '## ⚠️ Warning: You modified flaky/failed test files\\n\\n';\ncomment += 'The following test files you modified have reliability issues:\\n\\n';\n\nmodifiedFlakyTests.forEach(test => {\n  const testType = test.status === 'failed' ? '❌ Failed' : '⚠️ Flaky';\n  comment += `### ${testType}: \\`${test.file}\\`\\n`;\n  comment += `**Test:** ${test.testTitle}\\n`;\n  comment += `**Status:** ${test.status}\\n`;\n  if (test.retryAttempt > 0) {\n    comment += `**Retry Attempt:** ${test.retryAttempt}\\n`;\n  }\n  comment += '\\n**To debug locally, run:**\\n';\n  comment += '```bash\\n';\n  comment += `npx playwright test ${test.file} --repeat-each=5 --workers=1\\n`;\n  comment += '```\\n\\n';\n});\n\ncomment += '---\\n';\ncomment += '**Note:** Flaky tests passed after retrying, failed tests did not pass. ';\ncomment += 'Please investigate and fix the root cause before merging.\\n';\n\n// Save comment to file for GitHub Action to post\nfs.writeFileSync('pr-comment.md', comment);\n\nconsole.log(`Found ${modifiedFlakyTests.length} modified flaky tests`);\n"
  },
  {
    "path": ".github/scripts/detect-flaky-tests.js",
    "content": "const fs = require('fs');\n\n\n// Read Playwright JSON report\nconst resultsPath = 'playwright-report/results.json';\n\nif (!fs.existsSync(resultsPath)) {\n  console.log('No Playwright results found at', resultsPath);\n  process.exit(0);\n}\n\nconst results = JSON.parse(fs.readFileSync(resultsPath, 'utf8'));\n\n// Extract flaky tests\n// A test is flaky if: status === \"passed\" AND retry > 0\n// A test is failed if: status === \"failed\"\n// This means it failed initially but passed on retry OR failed completely\nconst flakyTests = [];\n\nfunction traverseSuites(suites) {\n  for (const suite of suites) {\n    // Process specs in this suite\n    for (const spec of suite.specs || []) {\n      for (const test of spec.tests || []) {\n        // Check each test result\n        for (const result of test.results || []) {\n          // Track two types of problematic tests:\n          // 1. Flaky: passed on a retry attempt (retry > 0)\n          // 2. Failed: failed on all attempts\n          if ((result.status === 'passed' && result.retry > 0) || result.status === 'failed') {\n            flakyTests.push({\n              file: spec.file,\n              title: spec.title,\n              testTitle: spec.title,\n              line: spec.line,\n              status: result.status,\n              retryAttempt: result.retry\n            });\n            break; // Only record once per test\n          }\n        }\n      }\n    }\n\n    // Recursively process nested suites\n    if (suite.suites && suite.suites.length > 0) {\n      traverseSuites(suite.suites);\n    }\n  }\n}\n\ntraverseSuites(results.suites || []);\n\n// Save flaky tests to JSON\nfs.writeFileSync('flaky-tests.json', JSON.stringify(flakyTests, null, 2));\n\n// Generate markdown report\nlet markdown = '## ⚠️ Flaky/Failed Tests Detected\\n\\n';\nmarkdown += 'The following tests are problematic:\\n\\n';\n\nflakyTests.forEach(test => {\n  const testType = test.status === 'failed' ? '❌ Failed' : '⚠️ Flaky';\n  markdown += `### ${testType}: \\`${test.file}\\`\\n`;\n  markdown += `- **Test:** ${test.testTitle}\\n`;\n  markdown += `- **Status:** ${test.status}\\n`;\n  if (test.retryAttempt > 0) {\n    markdown += `- **Retry Attempt:** ${test.retryAttempt}\\n`;\n  }\n  markdown += `- **Debug command:**\\n`;\n  markdown += '```bash\\n';\n  markdown += `npx playwright test ${test.file} --repeat-each=5 --workers=1\\n`;\n  markdown += '```\\n\\n';\n});\n\nfs.writeFileSync('flaky-report.md', markdown);\n\nconsole.log(`Found ${flakyTests.length} flaky/failed tests`);\nprocess.exit(flakyTests.length > 0 ? 1 : 0);\n"
  },
  {
    "path": ".github/workflows/flaky-test-detector.yml",
    "content": "name: Flaky Test Detector\n\non:\n  pull_request:\n    branches: [main]\n    paths:\n      - 'tests/**/*.spec.ts'\n\npermissions:\n  contents: read\n  pull-requests: write\n  issues: write\n  checks: write\n\njobs:\n  detect-flaky-tests:\n    name: Detect Flaky Tests\n    runs-on: ubuntu-24.04\n    timeout-minutes: 60\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0  # Need full history to compare with main\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v5\n        with:\n          node-version-file: '.nvmrc'\n          cache: 'npm'\n\n      - name: Install system dependencies\n        run: |\n          sudo apt-get update\n          sudo apt-get --no-install-recommends install -y \\\n            libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 \\\n            libcups2 libgtk-3-0 libasound2t64 xvfb\n\n      - name: Install npm dependencies\n        run: |\n          npm ci --legacy-peer-deps\n          sudo chown root /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox\n          sudo chmod 4755 /home/runner/work/bruno/bruno/node_modules/electron/dist/chrome-sandbox\n\n      - name: Install test collection dependencies\n        run: npm ci --prefix packages/bruno-tests/collection\n\n      - name: Build libraries\n        run: |\n          npm run build:graphql-docs\n          npm run build:bruno-query\n          npm run build:bruno-common\n          npm run sandbox:bundle-libraries --workspace=packages/bruno-js\n          npm run build:bruno-converters\n          npm run build:bruno-requests\n          npm run build:schema-types\n          npm run build:bruno-filestore\n\n      - name: Run Playwright tests\n        run: xvfb-run npm run test:e2e\n        continue-on-error: true  # Continue even if tests fail\n\n      - name: Detect flaky tests\n        id: detect\n        run: node .github/scripts/detect-flaky-tests.js\n        continue-on-error: true  # Don't fail workflow if flaky tests found\n\n      - name: Check modified flaky tests\n        id: check-modified\n        run: node .github/scripts/comment-on-flaky-tests.js\n        continue-on-error: true\n\n      - name: Post PR comment\n        if: hashFiles('pr-comment.md') != ''\n        uses: actions/github-script@v7\n        with:\n          script: |\n            const fs = require('fs');\n            const comment = fs.readFileSync('pr-comment.md', 'utf8');\n\n            // Check if we already commented\n            const { data: comments } = await github.rest.issues.listComments({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              issue_number: context.issue.number\n            });\n\n            const botComment = comments.find(c =>\n              c.user.type === 'Bot' && c.body.includes('Warning: You modified flaky/failed test files')\n            );\n\n            if (botComment) {\n              // Update existing comment\n              await github.rest.issues.updateComment({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                comment_id: botComment.id,\n                body: comment\n              });\n            } else {\n              // Create new comment\n              await github.rest.issues.createComment({\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                issue_number: context.issue.number,\n                body: comment\n              });\n            }\n\n      - name: Upload flaky test artifacts\n        if: always()\n        uses: actions/upload-artifact@v6\n        with:\n          name: flaky-test-results\n          path: |\n            flaky-tests.json\n            flaky-report.md\n            playwright-report/\n          retention-days: 30\n"
  },
  {
    "path": ".github/workflows/lint-checks.yml",
    "content": "name: Lint Checks\non:\n  workflow_dispatch:\n  push:\n    branches: [main, 'release/v*']\n  pull_request:\n    branches: [main, 'release/v*']\n\njobs:\n  lint:\n    name: Lint Check\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Setup Node Dependencies\n        uses: ./.github/actions/common/setup-node-deps\n        with:\n          skip-build: 'true'\n\n      - name: Lint Check\n        run: npm run lint\n        env:\n          ESLINT_PLUGIN_DIFF_COMMIT: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || 'main' }}"
  },
  {
    "path": ".github/workflows/npm-bru-cli.yml",
    "content": "name: Bru CLI Tests (npm)\n\non:\n  workflow_dispatch:\n    inputs:\n      build:\n        description: 'Test Bru CLI (npm)'\n        required: true\n        default: 'true'\n\n# Assign permissions for unit tests to be reported.\n# See https://github.com/dorny/test-reporter/issues/168\npermissions:\n  statuses: write\n  checks: write\n  contents: write\n  pull-requests: write\n  actions: write\n\njobs:\n  test:\n    name: CLI Tests\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macos-latest]\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-node@v5\n        with:\n          node-version-file: '.nvmrc'\n\n      - name: Install Bru CLI from NPM\n        run: npm install -g @usebruno/cli\n\n      - name: Display Bru CLI Version\n        run: bru --version\n\n      - name: Run tests\n        run: |\n          cd packages/bruno-tests/collection\n          npm install\n          bru run --env Prod --output junit.xml --format junit --sandbox developer\n\n      - name: Publish Test Report\n        uses: dorny/test-reporter@v2\n        if: success() || failure()\n        with:\n          name: Test Report\n          path: packages/bruno-tests/collection/junit.xml\n          reporter: java-junit\n"
  },
  {
    "path": ".github/workflows/ssl-tests.yml",
    "content": "name: SSL Tests\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  tests-for-linux:\n    name: SSL Tests - Linux\n    timeout-minutes: 60\n    runs-on: ubuntu-latest\n    permissions:\n      checks: write\n      pull-requests: write\n      contents: read\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Setup Node Dependencies\n        uses: ./.github/actions/common/setup-node-deps\n\n      - name: Setup Feature Dependencies\n        uses: ./.github/actions/ssl/linux/setup-feature-specific-deps\n\n      - name: Setup CA Certificates\n        uses: ./.github/actions/ssl/linux/setup-ca-certs\n\n      - name: Run Basic SSL CLI Tests\n        uses: ./.github/actions/ssl/linux/run-basic-ssl-cli-tests\n\n      - name: Run Custom CA Certs CLI Tests\n        uses: ./.github/actions/ssl/linux/run-custom-ca-certs-cli-tests\n\n      - name: Run Custom CA Certs E2E Tests\n        uses: ./.github/actions/ssl/linux/run-ssl-e2e-tests\n\n  tests-for-macos:\n    name: SSL Tests - macOS\n    timeout-minutes: 60\n    runs-on: macos-latest\n    permissions:\n      checks: write\n      pull-requests: write\n      contents: read\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Setup Node Dependencies\n        uses: ./.github/actions/common/setup-node-deps\n\n      - name: Setup Feature Dependencies\n        uses: ./.github/actions/ssl/macos/setup-feature-specific-deps\n\n      - name: Setup CA Certificates\n        uses: ./.github/actions/ssl/macos/setup-ca-certs\n\n      - name: Run Basic SSL CLI Tests\n        uses: ./.github/actions/ssl/macos/run-basic-ssl-cli-tests\n\n      - name: Run Custom CA Certs CLI Tests\n        uses: ./.github/actions/ssl/macos/run-custom-ca-certs-cli-tests\n\n      - name: Run Custom CA Certs E2E Tests\n        uses: ./.github/actions/ssl/macos/run-ssl-e2e-tests\n\n  tests-for-windows:\n    name: SSL Tests - Windows\n    timeout-minutes: 60\n    runs-on: windows-latest\n    permissions:\n      checks: write\n      pull-requests: write\n      contents: read\n    steps:\n      - uses: actions/checkout@v6\n      \n      - name: Setup Node Dependencies\n        uses: ./.github/actions/common/setup-node-deps\n\n      - name: Setup CA Certificates\n        uses: ./.github/actions/ssl/windows/setup-ca-certs\n\n      - name: Run Basic SSL CLI Tests\n        uses: ./.github/actions/ssl/windows/run-basic-ssl-cli-tests\n\n      - name: Run Custom CA Certs CLI Tests\n        uses: ./.github/actions/ssl/windows/run-custom-ca-certs-cli-tests\n\n      - name: Run Custom CA Certs E2E Tests\n        uses: ./.github/actions/ssl/windows/run-ssl-e2e-tests\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: Tests\non:\n  workflow_dispatch:\n  push:\n    branches: [main, 'release/v*']\n  pull_request:\n    branches: [main, 'release/v*']\n\njobs:\n  unit-test:\n    name: Unit Tests\n    timeout-minutes: 60\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Setup Node Dependencies\n        uses: ./.github/actions/common/setup-node-deps\n\n      - name: Run Unit Tests\n        uses: ./.github/actions/tests/run-unit-tests\n\n  cli-test:\n    name: CLI Tests\n    runs-on: ubuntu-latest\n    permissions:\n      checks: write\n      pull-requests: write\n      contents: read\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Setup Node Dependencies\n        uses: ./.github/actions/common/setup-node-deps\n\n      - name: Run CLI Tests\n        uses: ./.github/actions/tests/run-cli-tests\n\n      - name: Publish Test Report\n        uses: EnricoMi/publish-unit-test-result-action@v2\n        if: always()\n        with:\n          check_name: CLI Test Results\n          files: packages/bruno-tests/collection/junit.xml\n          comment_mode: always\n\n  e2e-test:\n    name: Playwright E2E Tests\n    timeout-minutes: 60\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Install System Dependencies (Ubuntu)\n        run: |\n          sudo apt-get update\n          sudo apt-get --no-install-recommends install -y \\\n            libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \\\n            xvfb\n\n      - name: Setup Node Dependencies\n        uses: ./.github/actions/common/setup-node-deps\n\n      - name: Configure Chrome Sandbox\n        run: |\n          sudo chown root node_modules/electron/dist/chrome-sandbox\n          sudo chmod 4755 node_modules/electron/dist/chrome-sandbox\n\n      - name: Run playwright Tests\n        uses: ./.github/actions/tests/run-e2e-tests\n        with:\n          os: ubuntu\n\n      - name: Upload Playwright Report\n        uses: actions/upload-artifact@v6\n        if: ${{ !cancelled() }}\n        with:\n          name: playwright-report\n          path: playwright-report/\n          retention-days: 30\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\nbun.lockb\nnode_modules\nyarn.lock\npnpm-lock.yaml\n.pnp\n.pnp.js\nbun.lockb\nbun.lock\n\n# testing\ncoverage\n\n# production\nbuild\nchrome-extension\nchrome-extension.pem\nchrome-extension.crx\nbruno.zip\n*.zip\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\n# next.js\n/renderer\n/renderer/.next/\n/renderer/out/\n/test-results/\n/playwright-report/\n/playwright/.cache/\n\n#dev editor\nbruno.iml\n.idea\n.vscode\n.cursor\n.claude\n.codex\n.agents\n.agent\nskills-lock.json\n\n# Playwright\n/blob-report/\n\n# Development plan files\nCLAUDE.md\nAGENTS.md\n*.plan.md\n\n# packages dist\npackages/bruno-filestore/dist\npackages/bruno-requests/dist\npackages/bruno-schema-types/dist\npackages/bruno-converters/dist\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "npx nano-staged\n"
  },
  {
    "path": ".nvmrc",
    "content": "v22.12.0\n"
  },
  {
    "path": "CODING_STANDARDS.md",
    "content": "# Bruno Coding Standards\n\n- No diffs unless an actual change is made, the code changes need to be as minimal as possible, avoid making un-necessary whitespace diffs. This is already handled by eslint but make sure you check your code changes before commiting and raising a PR.\n\n## General Style Rules\n\n- Use 2 spaces for indentation. No tabs, just spaces – keeps everything neat and uniform.\n\n- Stick to single quotes for strings. For JSX/TSX attributes, use double quotes (e.g., <svg xmlns=\"...\" viewBox=\"...\">) to follow React conventions.\n\n- Always add semicolons at the end of statements. It's like putting a period at the end of a sentence – clarity matters.\n\n- JSX is enabled, so feel free to use it where it makes sense.\n\n## Punctuation and Spacing\n\n- No trailing commas. Keep it clean, no extra commas hanging around.\n\n- Always use parentheses around parameters in arrow functions. Even for single params – consistency is key.\n\n- For multiline constructs, put opening braces on the same line, and ensure consistency. Minimum 2 elements for multiline.\n\n- No newlines inside function parentheses. Keep 'em tight.\n\n- Space before and after the arrow in arrow functions. `() => {}` is good.\n\n- No space between function name and parentheses. `func()` not `func ()`.\n\n- Semicolons go at the end of the line, not on a new line.\n\n- No strict max length – write readable code, not cramped lines.\n\n- Multiple expressions per line in JSX are fine – flexibility is nice.\n\nRemember, these rules are here to make our codebase harmonious. If something doesn't fit perfectly, let's chat about it. Happy coding! 🚀\n\n\n## Tests \n\n- Add tests for any new functionality or meaningful changes. If code is added, removed, or significantly modified, corresponding tests should be updated or created.\n \n- Prioritise high-value tests over maximum coverage. Focus on testing behaviour that is critical, complex, or likely to break—don’t chase coverage numbers for their own sake.\n\n- Write behaviour-driven tests, not implementation-driven ones. Tests should validate real expected output and observable behaviour, not internal details or mocked-out logic unless absolutely necessary.\n\n- Minimise mocking unless it meaningfully increases clarity or isolates external dependencies. Prefer real flows where practical; only mock external services, slow systems, or non-deterministic behaviour.\n\n- Keep tests readable and maintainable. Optimise for clarity over cleverness. Name tests descriptively, keep setup minimal, and avoid unnecessary abstraction.\n\n- Aim for tests that fail usefully. When a test fails, it should clearly indicate what behaviour broke and why.\n\n- Cover both the “happy path” and the realistically problematic paths. Validate expected success behaviour, but also validate error handling, edge cases, and degraded-mode behaviour when appropriate.\n\n- Ensure tests are deterministic and reproducible. No randomness, timing dependencies, or environment-specific assumptions without explicit control.\n\n- Avoid overfitting tests to current behaviour if future flexibility matters. Only assert what needs to be true, not incidental details.\n\n- Use consistent patterns and helper utilities where they improve clarity. Prefer shared test utilities over copy-pasted setup code, but only when it actually reduces complexity.\n\n- Tests should be fast enough to run continuously. Avoid long-running operations unless absolutely necessary; prefer lightweight fixtures and isolated units.\n\n\n## UI Specific instructions \n\n### React\n\n- Use styled component's theme prop to manage CSS colors and not CSS variables when in the context of a styled component or any react component using the styled component \n- Styled Components are used as wrappers to define both self and children components style, tailwind classes are used specifically for layout based styles. \n- Styled Component CSS might also change layout but tailwind classes shouldn't define colors.\n- MUST: Prefer custom hooks for business logic, data fetching, and side-effects.\n- MUST: Avoid `useEffect` unless absolutely needed. Prefer derived state, event handlers.\n- SHOULD: Memoize only when necessary (`useMemo`/`useCallback`), and prefer moving logic into hooks first.\n- MUST: Do not use namespace access for hooks in app code (e.g., `React.useCallback`, `React.useMemo`, `React.useState`). Import hooks directly.\n  - Correct: `import { useCallback, useMemo, useState } from \"react\";`\n  - Avoid: `import * as React from \"react\";` then `React.useCallback(...)`\n- Add `data-testid` to testable elements for Playwright\n- Co-locate utilities that are truly component-specific next to the component, otherwise place shared items under a common folder\n\n\n## Readability and Abstractions\n\n- Avoid abstractions unless the exact same code is being used in more than 3 places.\n- Names for functions need to be concise and descriptive.\n- Add in JSDoc comments to add more details to the abstractions if needed.\n- Follow functional programming but just enough to be readable, we don't need to go as deep as ADTs and Monads, we want to keep the code pipeline obvious and easy for everyone to read and contribute to.\n- Avoid single line abstractions where all that's being done is increasing the call stack with one additional function.\n- Add in meaningful comments instead of obvious ones where complex code flow is explained properly.\n"
  },
  {
    "path": "contributing.md",
    "content": "**English**\n| [Українська](docs/contributing/contributing_ua.md)\n| [Русский](docs/contributing/contributing_ru.md)\n| [Türkçe](docs/contributing/contributing_tr.md)\n| [Deutsch](docs/contributing/contributing_de.md)\n| [Français](docs/contributing/contributing_fr.md)\n| [Português (BR)](docs/contributing/contributing_pt_br.md)\n| [한국어](docs/contributing/contributing_kr.md)\n| [বাংলা](docs/contributing/contributing_bn.md)\n| [Español](docs/contributing/contributing_es.md)\n| [Italiano](docs/contributing/contributing_it.md)\n| [Română](docs/contributing/contributing_ro.md)\n| [Polski](docs/contributing/contributing_pl.md)\n| [简体中文](docs/contributing/contributing_cn.md)\n| [正體中文](docs/contributing/contributing_zhtw.md)\n| [日本語](docs/contributing/contributing_ja.md)\n| [हिंदी](docs/contributing/contributing_hi.md)\n| [Dutch](docs/contributing/contributing_nl.md)\n| [فارسی](docs/contributing/contributing_fa.md)\n\n## Let's make Bruno better, together!!\n\nWe are happy that you are looking to improve Bruno. Below are the guidelines to run Bruno on your computer.\n\n### Technology Stack\n\nBruno is built using React and Electron.\n\nLibraries we use\n\n- CSS - Tailwind\n- Code Editors - Codemirror\n- State Management - Redux\n- Icons - Tabler Icons\n- Forms - formik\n- Schema Validation - Yup\n- Request Client - axios\n- Filesystem Watcher - chokidar\n- i18n - i18next\n\n> [!IMPORTANT]\n> You would need [Node v22.x or the latest LTS version](https://nodejs.org/en/). We use npm workspaces in the project\n\n## Development\n\nBruno is a desktop app. Below are the instructions to run Bruno.\n\n> Note: We use React for the frontend and rsbuild for build and dev server.\n\n## Install Dependencies\n\n```bash\n# use nodejs 22 version\nnvm use\n\n# install deps\nnpm i --legacy-peer-deps\n```\n\n### Local Development\n\n#### Build packages\n\n##### Option 1\n\n```bash\n# build packages\nnpm run build:graphql-docs\nnpm run build:bruno-query\nnpm run build:bruno-common\nnpm run build:bruno-converters\nnpm run build:bruno-requests\nnpm run build:schema-types\nnpm run build:bruno-filestore\n\n# bundle js sandbox libraries\nnpm run sandbox:bundle-libraries --workspace=packages/bruno-js\n```\n\n##### Option 2\n\n```bash\n# install dependencies and setup\nnpm run setup\n```\n\n#### Run the app\n\n##### Option 1\n\n```bash\n# run react app (terminal 1)\nnpm run dev:web\n\n# run electron app (terminal 2)\nnpm run dev:electron\n```\n\n##### Option 2\n\n```bash\n# run electron and react app concurrently\nnpm run dev\n```\n\n#### Customize Electron `userData` path\n\nIf `ELECTRON_USER_DATA_PATH` env-variable is present and its development mode, then `userData` path is modified accordingly.\n\ne.g.\n\n```sh\nELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron\n```\n\nThis will create a `bruno-test` folder on your Desktop and use it as the `userData` path.\n\n### Troubleshooting\n\nYou might encounter a `Unsupported platform` error when you run `npm install`. To fix this, you will need to delete `node_modules` and `package-lock.json` and run `npm install`. This should install all the necessary packages needed to run the app.\n\n```shell\n# Delete node_modules in sub-directories\nfind ./ -type d -name \"node_modules\" -print0 | while read -d $'\\0' dir; do\n  rm -rf \"$dir\"\ndone\n\n# Delete package-lock in sub-directories\nfind . -type f -name \"package-lock.json\" -delete\n```\n\n### Testing\n\n```bash\n# run bruno-schema tests\nnpm run test --workspace=packages/bruno-schema\n\n# run bruno-query tests\nnpm run test --workspace=packages/bruno-query\n\n# run bruno-common tests\nnpm run test --workspace=packages/bruno-common\n\n# run bruno-converters tests\nnpm run test --workspace=packages/bruno-converters\n\n# run bruno-app tests\nnpm run test --workspace=packages/bruno-app\n\n# run bruno-electron tests\nnpm run test --workspace=packages/bruno-electron\n\n# run bruno-lang tests\nnpm run test --workspace=packages/bruno-lang\n\n# run bruno-toml tests\nnpm run test --workspace=packages/bruno-toml\n\n# run tests over all workspaces\nnpm test --workspaces --if-present\n```\n\n### Raising Pull Requests\n\n- Please keep the PR's small and focused on one thing\n- Please follow the format of creating branches\n  - feature/[feature name]: This branch should contain changes for a specific feature\n    - Example: feature/dark-mode\n  - bugfix/[bug name]: This branch should contain only bug fixes for a specific bug\n    - Example bugfix/bug-1\n"
  },
  {
    "path": "docs/contributing/contributing_bn.md",
    "content": "[English](../../contributing.md)\n\n## আসুন ব্রুনোকে আরও ভালো করি, একসাথে!!\n\nআমরা খুশি যে আপনি ব্রুনোর উন্নতি করতে চাইছেন। নীচে আপনার কম্পিউটারে ব্রুনো ইনষ্টল করার নির্দেশিকা রয়েছে৷।\n\n### Technology Stack (প্রযুক্তি স্ট্যাক)\n\nব্রুনো Next.js এবং React ব্যবহার করে নির্মিত। এছাড়াও আমরা একটি ডেস্কটপ সংস্করণ পাঠাতে ইলেক্ট্রন ব্যবহার করি (যা স্থানীয় সংগ্রহ সমর্থন করে)\n\nনিম্ন লিখিত লাইব্রেরি আমরা ব্যবহার করি -\n\n- CSS - Tailwind\n- Code Editors - Codemirror\n- State Management - Redux\n- Icons - Tabler Icons\n- Forms - formik\n- Schema Validation - Yup\n- Request Client - axios\n- Filesystem Watcher - chokidar\n\n### Dependencies (নির্ভরতা)\n\nআপনার প্রয়োজন হবে [নোড v18.x বা সর্বশেষ LTS সংস্করণ](https://nodejs.org/en/) এবং npm 8.x। আমরা প্রকল্পে npm ওয়ার্কস্পেস ব্যবহার করি ।\n\n## Development\n\nব্রুনো একটি ডেস্কটপ অ্যাপ হিসেবে তৈরি করা হচ্ছে। আপনাকে একটি টার্মিনালে Next.js অ্যাপটি চালিয়ে অ্যাপটি লোড করতে হবে এবং তারপরে অন্য টার্মিনালে ইলেক্ট্রন অ্যাপটি চালাতে হবে।\n\n### Dependencies (নির্ভরতা)\n\n- NodeJS v18\n\n### Local Development\n\n```bash\n# nodejs 18 সংস্করণ ব্যবহার করুন\nnvm use\n\n# নির্ভরতা ইনস্টল করুন\nnpm i --legacy-peer-deps\n\n# গ্রাফকিউএল ডক্স তৈরি করুন\nnpm run build:graphql-docs\n\n# ব্রুনো কোয়েরি তৈরি করুন\nnpm run build:bruno-query\n\n# NextJs অ্যাপ চালান (টার্মিনাল 1)\nnpm run dev:web\n\n# ইলেক্ট্রন অ্যাপ চালান (টার্মিনাল 2)\nnpm run dev:electron\n```\n\n### Troubleshooting (সমস্যা সমাধান)\n\nআপনি যখন 'npm install' চালান তখন আপনি একটি 'অসমর্থিত প্ল্যাটফর্ম' ত্রুটির সম্মুখীন হতে পারেন৷ এটি ঠিক করতে, আপনাকে `node_modules` এবং `package-lock.json` মুছে ফেলতে হবে এবং `npm install` চালাতে হবে। এটি অ্যাপটি চালানোর জন্য প্রয়োজনীয় সমস্ত প্যাকেজ ইনস্টল করবে যাতে এই ত্রুটি ঠিক হয়ে যেতে পারে ।\n\n```shell\n# সাব-ডিরেক্টরিতে নোড_মডিউল মুছুন\nfind ./ -type d -name \"node_modules\" -print0 | while read -d $'\\0' dir; do\n  rm -rf \"$dir\"\ndone\n\n# সাব-ডিরেক্টরিতে প্যাকেজ-লক মুছুন\nfind . -type f -name \"package-lock.json\" -delete\n```\n\n### Testing (পরীক্ষা)\n\n```bash\n# ব্রুনো-স্কিমা পরীক্ষা চালান\nnpm test --workspace=packages/bruno-schema\n\n# সমস্ত কর্মক্ষেত্রে পরীক্ষা চালান\nnpm test --workspaces --if-present\n```\n\n### Raising Pull Request (পুল অনুরোধ উত্থাপন)\n\n- অনুগ্রহ করে PR এর আকার ছোট রাখুন এবং একটি বিষয়ে ফোকাস করুন।\n- অনুগ্রহ করে শাখা তৈরির বিন্যাস অনুসরণ করুন।\n  - বৈশিষ্ট্য/[ফিচারের নাম]: এই শাখায় একটি নির্দিষ্ট বৈশিষ্ট্যের জন্য পরিবর্তন থাকতে হবে।\n    - উদাহরণ: বৈশিষ্ট্য/ডার্ক-মোড।\n  - বাগফিক্স/[বাগ নাম]: এই শাখায় একটি নির্দিষ্ট বাগ-এর জন্য শুধুমাত্র বাগ ফিক্স থাকা উচিত।\n    - উদাহরণ বাগফিক্স/বাগ-1।\n"
  },
  {
    "path": "docs/contributing/contributing_cn.md",
    "content": "[English](../../contributing.md)\n\n## 让我们一起改进 Bruno！\n\n很高兴看到您考虑改进 Bruno。以下是获取 Bruno 并在您的电脑上运行它的规则和指南。\n\n### 使用的技术\n\nBruno 基于 NextJs 和 React 构建。我们使用 Electron 来封装桌面版本。\n\n我们使用的库包括：\n\n- CSS - Tailwind\n- 代码编辑器 - Codemirror\n- 状态管理 - Redux\n- 图标 - Tabler Icons\n- 表单 - formik\n- 模式验证 - Yup\n- 请求客户端 - axios\n- 文件系统监视器 - chokidar\n\n### 依赖项\n\n您需要 [Node v20.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我们在这个项目中也使用 npm 工作区（_npm workspaces_）。\n\n## 开发\n\nBruno 是作为一个 _client lourd（重客户端）_ 应用程序开发的。您需要在一个终端中启动 nextjs 来加载应用程序，然后在另一个终端中启动 Electron 应用程序。\n\n### 依赖项\n\n- NodeJS v18\n\n### 本地开发\n\n```bash\n# 使用 node 版本 18\nnvm use\n\n# 安装依赖项\nnpm i --legacy-peer-deps\n\n# 构建 graphql 文档\nnpm run build:graphql-docs\n\n# 构建 bruno 查询\nnpm run build:bruno-query\n\n# 启动 next（终端 1）\nnpm run dev:web\n\n# 启动重客户端（终端 2）\nnpm run dev:electron\n```\n\n### 故障排除\n\n在运行 npm install 时，您可能会遇到 Unsupported platform 错误。为了解决这个问题，请删除 node_modules 目录和 package-lock.json 文件，然后再次运行 npm install。这应该会安装运行应用程序所需的所有包。\n\n```shell\n# 删除子目录中的 node_modules 目录\nfind ./ -type d -name \"node_modules\" -print0 | while read -d $'\\0' dir; do\n  rm -rf \"$dir\"\ndone\n\n# 删除子目录中的 package-lock.json 文件\nfind . -type f -name \"package-lock.json\" -delete\n```\n\n### 测试\n\n```bash\n# 运行 bruno-schema 测试\nnpm test --workspace=packages/bruno-schema\n\n# 在所有工作区上运行测试\nnpm test --workspaces --if-present\n```\n\n### 提交 Pull Request\n\n- 请保持 PR 精简并专注于单一目标\n- 请遵循分支命名格式：\n  - feature/[feature name]：该分支应包含特定功能\n    - 例如：feature/dark-mode\n  - bugfix/[bug name]：该分支应仅包含特定 bug 的修复\n    - 例如：bugfix/bug-1\n"
  },
  {
    "path": "docs/contributing/contributing_de.md",
    "content": "[English](../../contributing.md)\n\n## Lass uns Bruno noch besser machen, gemeinsam!!\n\nIch freue mich, dass Du Bruno verbessern möchtest. Hier findest Du eine Anleitung, mit der Du Bruno auf Deinem Computer einrichten kannst.\n\n### Technologie Stack\n\nBruno ist mit Next.js und React erstellt. Außerdem benötigen wir electron für die Desktop Version (die lokale Sammlungen unterstützt).\n\nBibliotheken die wir benutzen\n\n- CSS - Tailwind\n- Code Editoren - Codemirror\n- State Management - Redux\n- Icons - Tabler Icons\n- Formulare - formik\n- Schema Validierung - Yup\n- Request Client - axios\n- Dateisystem Watcher - chokidar\n\n### Abhängigkeiten\n\nDu benötigst [Node v22.x oder die neuste LTS Version](https://nodejs.org/en/) und npm 8.x. Wir benutzen npm workspaces in dem Projekt.\n\n### Lass uns coden\n\nEine Anleitung zum Ausführen einer lokalen Entwicklungsumgebung findest Du in [development.md](docs/development_de.md).\n\n### Pull Request erstellen\n\n- Bitte halte die PRs klein und begrenzt auf eine Sache\n- Bitte halte Dich beim Erstellen eines Branches an das folgende Format\n  - feature/[feature name]: Dieser Branch soll Änderungen für ein bestimmtes Feature enthalten\n    - Beispiel: feature/dark-mode\n  - bugfix/[bug name]: Dieser Branch soll ausschließlich Bugfixes für einen bestimmten Bug enthalten\n    - Beispiel: bugfix/bug-1\n\n## Entwicklung\n\nBruno wird als Desktop-Anwendung entwickelt. Um die App zu starten, musst Du zuerst die Next.js App in einem Terminal ausführen und anschließend in einem anderen Terminal die Electron-App.\n\n### Abhängigkeiten\n\n- NodeJS v22\n\n### Lokales Entwickeln\n\n```bash\n# use nodejs 22 version\nnvm use\n\n# install deps\nnpm i --legacy-peer-deps\n\n# build graphql docs\nnpm run build:graphql-docs\n\n# build bruno query\nnpm run build:bruno-query\n\n# run next app (terminal 1)\nnpm run dev:web\n\n# run electron app (terminal 2)\nnpm run dev:electron\n```\n\n### Troubleshooting\n\nEs kann sein, dass Du einen `Unsupported platform`-Fehler bekommst, wenn Du `npm install` ausführst. Um dies zu beheben, musst Du `node_modules` und `package-lock.json` löschen und `npm install` erneut ausführen. Dies sollte alle notwendigen Pakete installieren, die zum Ausführen der Anwendung benötigt werden.\n\n```shell\n# Delete node_modules in sub-directories\nfind ./ -type d -name \"node_modules\" -print0 | while read -d $'\\0' dir; do\n  rm -rf \"$dir\"\ndone\n\n# Delete package-lock in sub-directories\nfind . -type f -name \"package-lock.json\" -delete\n```\n\n### Testen\n\n```bash\n# Führen Sie Bruno-Schema-Tests aus\nnpm test --workspace=packages/bruno-schema\n\n# Führen Sie Tests für alle Arbeitsbereiche durch\nnpm test --workspaces --if-present\n```\n"
  },
  {
    "path": "docs/contributing/contributing_es.md",
    "content": "[Inglés](../../contributing.md)\n\n## ¡Juntos, hagamos a Bruno mejor!\n\nEstamos encantados de que quieras ayudar a mejorar Bruno. A continuación encontrarás las instrucciones para empezar a trabajar con Bruno en tu computadora.\n\n### Tecnologías utilizadas\n\nBruno está construido con React y Electron\n\nLibrerías que utilizamos:\n\n- CSS - Tailwind CSS\n- Editores de código - CodeMirror\n- Manejo del estado - Redux\n- Íconos - Tabler Icons\n- Formularios - formik\n- Validación de esquemas - Yup\n- Cliente de peticiones - axios\n- Monitor del sistema de archivos - chokidar\n- i18n (internacionalización) - i18next\n\n### Dependencias\n\n> [!IMPORTANT]\n> Necesitarás [Node v22.x o la última versión LTS](https://nodejs.org/es/). Ten en cuenta que Bruno usa los espacios de trabajo de npm\n\n## Desarrollo\n\nBruno es una aplicación de escritorio. A continuación se detallan las instrucciones paso a paso para ejecutar Bruno.\n\n> Nota: Utilizamos React para el frontend y rsbuild para el servidor de desarrollo.\n\n### Instalar dependencias\n\n```bash\n# Use la versión 22.x o LTS (Soporte a Largo Plazo) de Node.js\nnvm use 22.11.0\n\n# instalar las dependencias\nnpm i --legacy-peer-deps\n```\n\n> ¿Por qué `--legacy-peer-deps`?: Fuerza la instalación ignorando conflictos en dependencias “peer”, evitando errores de árbol de dependencias.\n\n### Desarrollo local\n\n#### Construir paquetes\n\n##### Opción 1\n\n```bash\n# construir paquetes\nnpm run build:graphql-docs\nnpm run build:bruno-query\nnpm run build:bruno-common\nnpm run build:bruno-converters\nnpm run build:bruno-requests\n\n# empaquetar bibliotecas JavaScript del entorno de pruebas aislado\nnpm run sandbox:bundle-libraries --workspace=packages/bruno-js\n```\n\n##### Opción 2\n\n```bash\n# instalar dependencias y configurar el entorno\nnpm run setup\n```\n\n#### Ejecutar la aplicación\n\n```bash\n# ejecutar aplicación react (terminal 1)\nnpm run dev:web\n\n# ejecutar aplicación electron (terminal 2)\nnpm run dev:electron\n```\n\n##### Opción 1\n\n```bash\n# ejecutar aplicación react (terminal 1)\nnpm run dev:web\n\n# ejecutar aplicación electron (terminal 2)\nnpm run dev:electron\n```\n\n##### Opción 2\n\n```bash\n# ejecutar aplicación electron y react de forma concurrente\nnpm run dev\n```\n\n#### Personalizar la ruta `userData` de Electron\n\nSi la variable de entorno `ELECTRON_USER_DATA_PATH` está presente y se encuentra en modo de desarrollo, entonces la ruta `userData` se modifica en consecuencia.\nejemplo:\n\n```sh\nELECTRON_USER_DATA_PATH=$(realpath ~/Desktop/bruno-test) npm run dev:electron\n```\n\nEsto creará una carpeta llamada `bruno-test` en tu escritorio y la usará como la ruta userData.\n\n### Solución de problemas\n\nEs posible que te encuentres con un error `Unsupported platform` cuando ejecutes `npm install`. Para solucionarlo, tendrás que eliminar las carpetas `node_modules` y el archivo `package-lock.json`, y luego volver a ejecutar `npm install`. Esto debería instalar todos los paquetes necesarios para que la aplicación funcione.\n\n```sh\n# Elimina la carpeta node_modules en los subdirectorios\nfind ./ -type d -name \"node_modules\" -print0 | while read -d $'\\0' dir; do\n  rm -rf \"$dir\"\ndone\n\n# Elimina el archivo package-lock en los subdirectorios\nfind . -type f -name \"package-lock.json\" -delete\n```\n\n### Pruebas\n\n#### Pruebas individuales\n\n```bash\n# ejecutar pruebas de bruno-app\nnpm run test --workspace=packages/bruno-app\n\n# ejecutar pruebas de bruno-electron\nnpm run test --workspace=packages/bruno-electron\n\n# ejecutar pruebas de bruno-cli\nnpm run test --workspace=packages/bruno-cli\n\n# ejecutar pruebas de bruno-common\nnpm run test --workspace=packages/bruno-common\n\n# ejecutar pruebas de bruno-converters\nnpm run test --workspace=packages/bruno-converters\n\n# ejecutar pruebas de bruno-schema\nnpm run test --workspace=packages/bruno-schema\n\n# ejecutar pruebas de bruno-query\nnpm run test --workspace=packages/bruno-query\n\n# ejecutar pruebas de bruno-js\nnpm run test --workspace=packages/bruno-js\n\n# ejecutar pruebas de bruno-lang\nnpm run test --workspace=packages/bruno-lang\n\n# ejecutar pruebas de bruno-toml\nnpm run test --workspace=packages/bruno-toml\n```\n#### Pruebas en conjunto\n\n```bash\n# ejecutar pruebas en todos los espacios de trabajo\nnpm test --workspaces --if-present\n```\n\n### Crea un Pull Request\n\n- Por favor, mantén los Pull Request pequeños y enfocados en una sola cosa.\n- Por favor, sigue el siguiente formato para la creación de ramas:\n  - feature/[nombre de la funcionalidad]: Esta rama debe contener los cambios para una funcionalidad específica.\n    - Ejemplo: feature/dark-mode\n  - bugfix/[nombre del error]: Esta rama debe contener solo correcciones de errores para un error específico.\n    - Ejemplo: bugfix/bug-1\n"
  },
  {
    "path": "docs/contributing/contributing_fa.md",
    "content": "[English](../../contributing.md)\n\n## با هم، Bruno را بهتر می‌کنیم!\n\nخوشحالم که قصد دارید Bruno را بهبود ببخشید. در ادامه قوانین و راهنماها برای راه‌اندازی Bruno روی سیستم شما آورده شده است.\n\n### فناوری‌های استفاده‌شده\n\nبه فارسی برونو Bruno با استفاده از Next.js و React ساخته شده است. همچنین از Electron برای بسته‌بندی نسخه دسکتاپ (که امکان مجموعه‌های محلی را فراهم می‌کند) استفاده می‌کنیم.\n\nکتابخانه‌هایی که استفاده می‌کنیم:\n\n- CSS - Tailwind استایل\n- Codemirror - ویرایشگر کد\n- Redux - مدیریت وضعیت\n- Tabler Icons - آیکون‌ها\n- formik - فرم‌ها\n- Yup اعتبارسنجی اسکیمـا\n- axios - کلاینت درخواست\n- chokidar - پایش‌گر سیستم فایل\n\n### پیش‌نیازها\n\nشما به [نود v20.x یا اخرین نسخه پایدار](https://nodejs.org/en/) و npm 8.x نیاز دارید. در این پروژه از فضای کاری npm (npm workspaces) استفاده می‌کنیم.\n\n### شروع به کدنویسی\n\nبرای راه‌اندازی محیط توسعه محلی به فایل [مستندات توسعه](docs/development_fa.md) مراجعه کنید:\n\n### ارسال Pull Request\n\n1 - لطفاً Pull Requestها (PR) را کوتاه و متمرکز نگه دارید و تنها یک هدف مشخص را دنبال کنند. </br>\n2 - لطفاً از فرمت نام‌گذاری شاخه‌ها استفاده کنید:\n\n- feature/[name]: این شاخه باید شامل یک قابلیت مشخص باشد.\n  - feature/dark-mode : مثال\n- bugfix/[name]: این شاخه باید تنها شامل رفع یک باگ مشخص باشد.\n  - bugfix/bug-1 : مثال\n\n## توسعه\n\nبه فارسی برونو یا Bruno به‌صورت یک اپلیکیشن «سنگین» توسعه داده می‌شود. برای اجرا باید ابتدا Next.js را در یک پنجره ترمینال اجرا کنید و سپس اپلیکیشن Electron را در پنجره ترمینال دیگری راه‌اندازی نمایید.\n\n### نیازمندی توسعه\n\n- NodeJS v18\n\n### اجرای محلی\n\n```bash\n# از ورژن NodeJS 18 استفاده کنید\nnvm use\n\n# نصب وابستگی‌ها\nnpm i --legacy-peer-deps\n\n# ساخت مستندات GraphQL\nnpm run build:graphql-docs\n\n# ساخت bruno-query\nnpm run build:bruno-query\n\n# اجرای اپ Next (ترمینال 1)\nnpm run dev:web\n\n# اجرای اپ Electron (ترمینال 2)\nnpm run dev:electron\n```\n\n### عیب‌یابی\n\nممکن است هنگام اجرای `npm install` خطای `Unsupported platform` ببینید. برای رفع این مشکل، پوشه `node_modules` و فایل `package-lock.json` را حذف کرده و سپس دوباره `npm install` را اجرا کنید. این کار معمولاً همه پکیج‌های لازم را نصب می‌کند.\n\n```shell\n# حذف پوشه node_modules در زیردایرکتوری‌ها\nfind ./ -type d -name \"node_modules\" -print0 | while read -d $'\\0' dir; do\n  rm -rf \"$dir\"\ndone\n\n# حذف فایل package-lock.json در زیردایرکتوری‌ها\nfind . -type f -name \"package-lock.json\" -delete\n```\n\n### تست‌ها\n\n```bash\n# اجرای تست‌های schema مربوط به bruno\nnpm test --workspace=packages/bruno-schema\n\n# اجرای تست‌ها در همه فضاهای کاری (در صورت وجود)\nnpm test --workspaces --if-present\n```\n"
  },
  {
    "path": "docs/contributing/contributing_fr.md",
    "content": "[English](../../contributing.md)\n\n## Ensemble, améliorons Bruno !\n\nJe suis content de voir que vous envisagez d'améliorer Bruno. Vous trouverez ci-dessous les règles et guides pour récupérer Bruno sur votre ordinateur.\n\n### Technologies utilisées\n\nBruno est basé sur NextJs et React. Nous utilisons aussi Electron pour embarquer la version ordinateur (ce qui permet les collections locales).\n\nLes librairies que nous utilisons :\n\n- CSS - Tailwind\n- Code Editors - Codemirror\n- State Management - Redux\n- Icons - Tabler Icons\n- Forms - formik\n- Schema Validation - Yup\n- Request Client - axios\n- Filesystem Watcher - chokidar\n\n### Dépendances\n\nVous aurez besoin de [Node v20.x ou la dernière version LTS](https://nodejs.org/en/) et npm 8.x. Nous utilisons aussi les espaces de travail npm (_npm workspaces_) dans ce projet.\n\n## Développement\n\nBruno est développé comme une application _client lourd_. Vous devrez charger l'application en démarrant nextjs dans un premier terminal, puis démarre l'application Electron dans un second.\n\n### Dépendances\n\n- NodeJS v18\n\n### Développement local\n\n```bash\n# utiliser node en version 18\nnvm use\n\n# installation des dépendances\nnpm i --legacy-peer-deps\n\n# construction des docs graphql\nnpm run build:graphql-docs\n\n# construction de bruno query\nnpm run build:bruno-query\n\n# construction de bruno common\nnpm run build:bruno-common\n\n# démarrage de next (terminal 1)\nnpm run dev:web\n\n# démarrage du client lourd (terminal 2)\nnpm run dev:electron\n```\n\n### Dépannage\n\nVous pourriez rencontrer une erreur `Unsupported platform` durant le lancement de `npm install`. Pour résoudre cela, veuillez supprimer le répertoire `node_modules` ainsi que le fichier `package-lock.json` et lancez à nouveau `npm install`. Cela devrait installer tous les paquets nécessaires pour lancer l'application.\n\n```shell\n# Efface les répertoires node_modules dans les sous-répertoires\nfind ./ -type d -name \"node_modules\" -print0 | while read -d $'\\0' dir; do\n  rm -rf \"$dir\"\ndone\n\n# Efface les fichiers package-lock.json dans les sous-répertoires\nfind . -type f -name \"package-lock.json\" -delete\n```\n\n### Tests\n\n```bash\n# exécuter des tests de schéma bruno\nnpm test --workspace=packages/bruno-schema\n\n# exécuter des tests sur tous les espaces de travail\nnpm test --workspaces --if-present\n```\n\n### Ouvrir une Pull Request\n\n- Merci de conserver les PR minimes et focalisées sur un seul objectif\n- Merci de suivre le format de nom des branches :\n  - feature/[feature name]: Cette branche doit contenir une fonctionnalité spécifique\n    - Exemple : feature/dark-mode\n  - bugfix/[bug name]: Cette branche doit contenir seulement une solution pour un bug spécifique\n    - Exemple : bugfix/bug-1\n"
  },
  {
    "path": "docs/contributing/contributing_hi.md",
    "content": "[English](../../contributing.md)\n\n## आइए मिलकर Bruno को बेहतर बनाएं !!\n\nहमें खुशी है कि आप Bruno को बेहतर बनाना चाहते हैं। Bruno को अपने कंप्यूटर पर लाना शुरू करने के लिए दिशानिर्देश नीचे दिए गए हैं।\n\n### टेक्नोलॉजी स्टैक\n\nBruno को Next.js और React का उपयोग करके बनाया गया है। हम डेस्कटॉप संस्करण को शिप करने के लिए इलेक्ट्रॉन का भी उपयोग करते हैं (जो स्थानीय संग्रह का समर्थन करता है)\n\nLibraries जिनका हम उपयोग करते हैं\n\n- CSS - Tailwind\n- कोड संपादक - Codemirror\n- State Management - Redux\n- Icons - Tabler Icons\n- Forms - formik\n- Schema Validation - Yup\n- Request Client - axios\n- Filesystem Watcher - chokidar\n\n### निर्भरताएँ\n\nआपको [Node v20.x या नवीनतम LTS संस्करण](https://nodejs.org/en/) और npm 8.x की आवश्यकता होगी। हम प्रोजेक्ट में npm वर्कस्पेस का उपयोग करते हैं\n\n## डेवलपमेंट\n\nBruno को एक डेस्कटॉप ऐप के रूप में बनाया किया जा रहा है। आपको Next.js ऐप को एक टर्मिनल में चलाकर ऐप को लोड करना होगा और फिर इलेक्ट्रॉन ऐप को दूसरे टर्मिनल में चलाना होगा।\n\n### लोकल डेवलपमेंट\n\n```bash\n# nodejs 18 संस्करण का उपयोग करें\nnvm use\n\n# डिपेंडेंसी इनस्टॉल करे\nnpm i --legacy-peer-deps\n\n# पैकेज बिल्ड करें\nnpm run build:graphql-docs\nnpm run build:bruno-query\nnpm run build:bruno-common\nnpm run build:bruno-converters\nnpm run build:bruno-requests\n\n# Next.js ऐप चलाएँ (टर्मिनल 1 पर)\nnpm run dev:web\n\n# इलेक्ट्रॉन ऐप चलाएँ (टर्मिनल 2 पर)\nnpm run dev:electron\n```\n\n### समस्या निवारण\n\nजब आप `npm इंस्टॉल` चलाते हैं तो आपको `असमर्थित प्लेटफ़ॉर्म` त्रुटि का सामना करना पड़ सकता है। इसे ठीक करने के लिए, आपको `node_modules` और `package-lock.json` को हटाना होगा और `npm install` चलाना होगा। इसमें ऐप चलाने के लिए आवश्यक सभी आवश्यक पैकेज इंस्टॉल होने चाहिए।\n\n```shell\n# सब-डायरेक्टरी में node_modules डिलीट करे\nfind ./ -type d -name \"node_modules\" -print0 | while read -d $'\\0' dir; do\n  rm -rf \"$dir\"\ndone\n\n# सब-डायरेक्टरी में package-lock डिलीट करे\nfind . -type f -name \"package-lock.json\" -delete\n```\n\n### परिक्षण\n\n```bash\n# ब्रूनो-स्कीमा परीक्षण चलाएँ\nnpm test --workspace=packages/bruno-schema\n\n# सभी कार्यस्थानों पर परीक्षण चलाएँ\nnpm test --workspaces --if-present\n```\n\n### पुल अनुरोध प्रक्रिया\n\n- कृपया PR को छोटा रखें और एक चीज़ पर केंद्रित रखें\n- कृपया शाखाएँ बनाने के प्रारूप का पालन करें\n  - feature/[feature name]: इस शाखा में किसी विशिष्ट सुविधा के लिए परिवर्तन होने चाहिए\n    - उदाहरण: feature/dark-mode\n  - bugfix/[bug name]: इस शाखा में केवल विशिष्ट बग के लिए बग फिक्स शामिल होने चाहिए\n    - उदाहरण bugfix/bug-1\n"
  },
  {
    "path": "docs/contributing/contributing_it.md",
    "content": "[English](../../contributing.md)\n\n## Insieme, miglioriamo Bruno!\n\nSono felice di vedere che hai intenzione di migliorare Bruno. Di seguito, troverai le regole e le guide per ripristinare Bruno sul tuo computer.\n\n### Tecnologie utilizzate\n\nBruno è costruito utilizzando Next.js e React. Utilizziamo anche Electron per incorporare la versione desktop (che consente raccolte locali).\n\nLe librerie che utilizziamo sono:\n\n- CSS - Tailwind\n- Code Editors - Codemirror\n- State Management - Redux\n- Icons - Tabler Icons\n- Forms - formik\n- Schema Validation - Yup\n- Request Client - axios\n- Filesystem Watcher - chokidar\n\n### Dependences\n\nHai bisogno di [Node v20.x o dell'ultima versione LTS](https://nodejs.org/en/) di npm 8.x. Utilizziamo gli spazi di lavoro npm (_npm workspaces_) in questo progetto.\n\n### Iniziamo a codificare\n\nSi prega di fare riferimento alla [documentazione di sviluppo](docs/development_it.md) per le istruzioni su come avviare l'ambiente di sviluppo locale.\n\n### Aprire una richiesta di pull (Pull Request)\n\n- Si prega di mantenere le Pull Request (PR) brevi e concentrate su un singolo obiettivo.\n- Si prega di seguire il formato di denominazione dei rami.\n  - feature/[feature name]: Questo ramo dovrebbe contenere una specifica funzionalità.\n    - Esempio: feature/dark-mode\n  - bugfix/[bug name]: Questo ramo dovrebbe contenere solo una soluzione per un bug specifico.\n    - Esempio: bugfix/bug-1\n\n## Sviluppo\n\nBruno è sviluppato come un'applicazione \"heavy\". È necessario caricare l'applicazione avviando Next.js in una finestra del terminale e quindi avviare l'applicazione Electron in un altro terminale.\n\n### Sviluppo\n\n- NodeJS v18\n\n### Sviluppo locale\n\n```bash\n# use nodejs 18 version\nnvm use\n\n# install deps\nnpm i --legacy-peer-deps\n\n# build graphql docs\nnpm run build:graphql-docs\n\n# build bruno query\nnpm run build:bruno-query\n\n# run next app (terminal 1)\nnpm run dev:web\n\n# run electron app (terminal 2)\nnpm run dev:electron\n```\n\n### Risoluzione dei problemi\n\nPotresti trovare un errore `Unsupported platform` durante l'esecuzione di `npm install`. Per risolvere questo problema, ti preghiamo di eliminare la cartella `node_modules`, il file `package-lock.json` e di seguito nuovamente `npm install`. Qeusto dovrebbe installare tutti i pacchetti necessari per avviare l'applicazione.\n\n```shell\n# delete node_modules in sub-directories\nfind ./ -type d -name \"node_modules\" -print0 | while read -d $'\\0' dir; do\n  rm -rf \"$dir\"\ndone\n\n# delete package-lock in sub-directories\nfind . -type f -name \"package-lock.json\" -delete\n```\n\n### Tests\n\n```bash\n# esegui i test dello schema bruno\nnpm test --workspace=packages/bruno-schema\n\n# esegui test su tutti gli spazi di lavoro\nnpm test --workspaces --if-present\n```\n"
  },
  {
    "path": "docs/contributing/contributing_ja.md",
    "content": "[English](../../contributing.md)\n\n## 一緒に Bruno をよりよいものにしていきましょう！！\n\nBruno を改善していただけるのは歓迎です。以下はあなたの環境で Bruno を起動するためのガイドラインです。\n\n### 技術スタック\n\nBruno は Next.js と React で作られています。デスクトップアプリ(ローカルのコレクションに対応しています)には electron も使用しています。\n\n使用ライブラリ\n\n- CSS - Tailwind\n- Code Editors - Codemirror\n- State Management - Redux\n- Icons - Tabler Icons\n- Forms - formik\n- Schema Validation - Yup\n- Request Client - axios\n- Filesystem Watcher - chokidar\n\n### 依存関係\n\n[Node v20.x もしくは最新の LTS バージョン](https://nodejs.org/en/)と npm 8.x が必要です。プロジェクトに npm ワークスペースを使用しています。\n\n## 開発\n\nBruno はデスクトップアプリとして開発されています。一つのターミナルで Next.js アプリを立ち上げ、もう一つのターミナルで electron アプリを立ち上げてアプリを読み込む必要があります。\n\n### ローカル環境での開発\n\n```bash\n# use nodejs 18 version\nnvm use\n\n# install deps\nnpm i --legacy-peer-deps\n\n# build packages\nnpm run build:graphql-docs\nnpm run build:bruno-query\nnpm run build:bruno-common\nnpm run build:bruno-converters\nnpm run build:bruno-requests\n\n# run next app (terminal 1)\nnpm run dev:web\n\n# run electron app (terminal 2)\nnpm run dev:electron\n```\n\n### トラブルシューティング\n\n`npm install`を実行すると、`Unsupported platform`エラーに遭遇することがあります。これを直すためには、`node_modules`と`package-lock.json`を削除し、`npm install`を実行しなおす必要があります。これにより、アプリを動かすのに必要なパッケージがすべてインストールされます。\n\n```shell\n# Delete node_modules in sub-directories\nfind ./ -type d -name \"node_modules\" -print0 | while read -d $'\\0' dir; do\n  rm -rf \"$dir\"\ndone\n\n# Delete package-lock in sub-directories\nfind . -type f -name \"package-lock.json\" -delete\n```\n\n### テストを動かすには\n\n```bash\n# ブルーノスキーマのテストを実行します\nnpm test --workspace=packages/bruno-schema\n\n# すべてのワークスペースでテストを実行します\nnpm test --workspaces --if-present\n```\n\n### プルリクエストの手順\n\n- プルリクエストは小規模で、一つのことにフォーカスしたものにしてください。\n- 以下のフォーマットに従ってブランチを作ってください。\n  - feature/[feature name]: このブランチには特定の機能に対する変更を含んでください。\n    - 例: feature/dark-mode\n  - bugfix/[bug name]: このブランチには特定のバグに対する修正のみを含むようにしてください。\n    - 例: bugfix/bug-1\n"
  },
  {
    "path": "docs/contributing/contributing_kr.md",
    "content": "[English](../../contributing.md)\n\n## 함께 Bruno를 더 좋게 만들어요!!\n\n우리는 여러분이 Bruno를 발전시키기 위해 노력해주셔서 기쁩니다. 다음은 여러분의 컴퓨터에서 Bruno를 불러오는 가이드라인입니다.\n\n### 기술 스택\n\nBruno는 Next.js와 React로 구축되었습니다. 또한, (로컬 컬렉션을 지원하는) 데스크톱 버전을 제공하기 위해 electron을 사용합니다.\n\n우리가 사용하는 라이브러리\n\n- CSS - Tailwind\n- Code Editors - Codemirror\n- State Management - Redux\n- Icons - Tabler Icons\n- Forms - formik\n- Schema Validation - Yup\n- Request Client - axios\n- Filesystem Watcher - chokidar\n\n### 의존성\n\n[Node v20.x 혹은 최신 LTS version](https://nodejs.org/en/)과 npm 8.x 버전이 필요합니다. 우리는 이 프로젝트에서 npm workspaces를 사용합니다.\n\n## 개발\n\nBruno는 데스크톱 앱으로 개발되고 있습니다. 한 터미널에서 Next.js를 실행하여 앱을 로드한 다음 다른 터미널에서 electron 앱을 실행해야합니다.\n\n### 로컬 개발\n\n```bash\n# nodejs 18 버전 사용\nnvm use\n\n# 의존성 설치\nnpm i --legacy-peer-deps\n\n# packages 빌드\nnpm run build:graphql-docs\nnpm run build:bruno-query\nnpm run build:bruno-common\nnpm run build:bruno-converters\nnpm run build:bruno-requests\n\n# next 앱 실행 (1번 터미널)\nnpm run dev:web\n\n# electron 앱 실행 (2번 터미널)\nnpm run dev:electron\n```\n\n### 트러블 슈팅\n\n`npm install`을 실행할 때, `Unsupported platform` 에러를 마주칠 수 있습니다. 이것을 고치기 위해서는 `node_modules`와 `package-lock.json`을 삭제하고 `npm install`을 실행해야 합니다.\n그러면 앱을 실행하기 위해 필요한 패키지들이 모두 설치됩니다.\n\n```shell\n# 하위 디렉토리에 있는 node_modules 삭제\nfind ./ -type d -name \"node_modules\" -print0 | while read -d $'\\0' dir; do\n  rm -rf \"$dir\"\ndone\n\n# 하위 디렉토리에 있는 package-lock 삭제\nfind . -type f -name \"package-lock.json\" -delete\n```\n\n### 테스팅\n\n```bash\n# bruno-schema 테스트 실행\nnpm test --workspace=packages/bruno-schema\n\n# 모든 작업 공간에서 테스트 실행\nnpm test --workspaces --if-present\n```\n\n### Pull Requests 요청\n\n- PR을 작게 유지하고 한가지에 집중해주세요.\n- 브랜치를 생성하는 형식을 따라주세요.\n  - feature/[feature name]: 이 브랜치는 특정 기능에 대한 변경사항이 포함되어야합니다.\n    - 예시: feature/dark-mode\n  - bugfix/[bug name]: 이 브랜치는 특정 버그에 대한 버그 수정만 포함되어야합니다.\n    - 예시: bugfix/bug-1\n"
  },
  {
    "path": "docs/contributing/contributing_nl.md",
    "content": "[English](../../contributing.md)\n\n## Laten we Bruno samen beter maken !!\n\nWe zijn blij dat je Bruno wilt verbeteren. Hieronder staan de richtlijnen om Bruno op je computer op te zetten.\n\n### Technologiestack\n\nBruno is gebouwd met Next.js en React. We gebruiken ook Electron om een desktopversie te leveren (die lokale collecties ondersteunt).\n\nBibliotheken die we gebruiken:\n\n- CSS - Tailwind\n- Code Editors - Codemirror\n- State Management - Redux\n- Iconen - Tabler Icons\n- Formulieren - formik\n- Schema Validatie - Yup\n- Request Client - axios\n- Bestandsysteem Watcher - chokidar\n\n### Afhankelijkheden\n\nJe hebt [Node v18.x of de nieuwste LTS-versie](https://nodejs.org/en/) en npm 8.x nodig. We gebruiken npm workspaces in het project.\n\n## Ontwikkeling\n\nBruno wordt ontwikkeld als een desktop-app. Je moet de app laden door de Next.js app in één terminal te draaien en daarna de Electron app in een andere terminal te draaien.\n\n### Lokale Ontwikkeling\n\n```bash\n# gebruik voorgeschreven node versie\nnvm use\n\n# installeer afhankelijkheden\nnpm i --legacy-peer-deps\n\n# build pakketten\nnpm run build:graphql-docs\nnpm run build:bruno-query\nnpm run build:bruno-common\nnpm run build:bruno-converters\nnpm run build:bruno-requests\n\n# draai next app (terminal 1)\nnpm run dev:web\n\n# draai electron app (terminal 2)\nnpm run dev:electron\n```\n\n### Problemen oplossen\n\nJe kunt een `Unsupported platform`-fout tegenkomen wanneer je `npm install` uitvoert. Om dit te verhelpen, moet je `node_modules` en `package-lock.json` verwijderen en `npm install` uitvoeren. Dit zou alle benodigde afhankelijkheden moeten installeren om de app te draaien.\n\n```shell\n# Verwijder node_modules in subdirectories\nfind ./ -type d -name \"node_modules\" -print0 | while read -d $'\\0' dir; do\n  rm -rf \"$dir\"\ndone\n\n# Verwijder package-lock in subdirectories\nfind . -type f -name \"package-lock.json\" -delete\n```\n\n### Testen\n\n```bash\n# voer bruno-schema tests uit\nnpm test --workspace=packages/bruno-schema\n\n# voer tests uit over alle werkruimten\nnpm test --workspaces --if-present\n```\n\n### Pull Requests indienen\n\n- Houd de PR's klein en gefocust op één ding\n- Volg het formaat voor het aanmaken van branches\n  - feature/[feature naam]: Deze branch moet wijzigingen voor een specifieke functie bevatten\n    - Voorbeeld: feature/dark-mode\n  - bugfix/[bug naam]: Deze branch moet alleen bugfixes voor een specifieke bug bevatten\n    - Voorbeeld: bugfix/bug-1"
  },
  {
    "path": "docs/contributing/contributing_pl.md",
    "content": "[English](../../contributing.md)\n\n## Wspólnie uczynijmy Bruno lepszym !!\n\nCieszymy się, że chcesz udoskonalić Bruno. Poniżej znajdziesz wskazówki, jak rozpocząć pracę z Bruno na Twoim komputerze.\n\n### Stos Technologiczny\n\nBruno jest zbudowane przy użyciu Next.js i React. Używamy również electron do stworzenia wersji desktopowej (która obsługuje lokalne kolekcje)\n\nBiblioteki, których używamy\n\n- CSS - Tailwind\n- Edytory Kodu - Codemirror\n- Zarządzanie Stanem - Redux\n- Ikony - Tabler Icons\n- Formularze - formik\n- Walidacja Schematu - Yup\n- Klient Zapytań - axios\n- Obserwator Systemu Plików - chokidar\n\n### Zależności\n\nBędziesz potrzebować [Node v20.x lub najnowszej wersji LTS](https://nodejs.org/en/) oraz npm 8.x. W projekcie używamy npm workspaces\n\n## Rozwój\n\nBruno jest rozwijane jako aplikacja desktopowa. Musisz załadować aplikację, uruchamiając aplikację Next.js w jednym terminalu, a następnie uruchomić aplikację electron w innym terminalu.\n\n### Zależności\n\n- NodeJS v18\n\n### Lokalny Rozwój\n\n```bash\n# użyj wersji nodejs 18\nnvm use\n\n# zainstaluj zależności\nnpm i --legacy-peer-deps\n\n# zbuduj dokumentację graphql\nnpm run build:graphql-docs\n\n# zbuduj zapytanie bruno\nnpm run build:bruno-query\n\n# uruchom aplikację next (terminal 1)\nnpm run dev:web\n\n# uruchom aplikację electron (terminal 2)\nnpm run dev:electron\n```\n\n### Rozwiązywanie Problemów\n\nMożesz napotkać błąd `Unsupported platform` podczas uruchamiania `npm install`. Aby to naprawić, będziesz musiał usunąć `node_modules` i `package-lock.json`, a następnie uruchomić `npm install`. Powinno to zainstalować wszystkie niezbędne pakiety potrzebne do uruchomienia aplikacji.\n\n```shell\n# Usuń node_modules w podkatalogach\nfind ./ -type d -name \"node_modules\" -print0 | while read -d $'\\0' dir; do\n  rm -rf \"$dir\"\ndone\n\n# Usuń package-lock w podkatalogach\nfind . -type f -name \"package-lock.json\" -delete\n\n```\n\n### Testowanie\n\n```bash\n# uruchom testy bruno-schema\nnpm test --workspace=packages/bruno-schema\n\n# uruchom testy we wszystkich przestrzeniach roboczych\nnpm test --workspaces --if-present\n```\n\n### Tworzenie Pull Request\n\n- Prosimy, aby PR były małe i skoncentrowane na jednej rzeczy\n- Prosimy przestrzegać formatu tworzenia gałęzi\n  - feature/[nazwa funkcji]: Ta gałąź powinna zawierać zmiany dotyczące konkretnej funkcji\n    - Przykład: feature/dark-mode\n  - bugfix/[nazwa błędu]: Ta gałąź powinna zawierać tylko poprawki dla konkretnego błędu\n    - Przykład bugfix/bug-1\n"
  },
  {
    "path": "docs/contributing/contributing_pt_br.md",
    "content": "[English](../../contributing.md)\n\n## Vamos tornar o Bruno melhor, juntos!!\n\nEstamos felizes que você queira ajudar a melhorar o Bruno. Abaixo estão as diretrizes e orientações para começar a executar o Bruno no seu computador.\n\n### Stack de Tecnologias\n\nO Bruno é construído usando Next.js e React. Também usamos o Electron para disponibilizar uma versão para desktop (que suporta coleções locais).\n\nBibliotecas que utilizamos:\n\n- CSS - Tailwind\n- Editor de Código - Codemirror\n- Gerenciador de Estado - Redux\n- Ícones - Tabler Icons\n- Formulários - formik\n- Validador de Schema - Yup\n- Cliente de Requisições - axios\n- Monitor de Arquivos - chokidar\n\n### Dependências\n\nVocê precisará do [Node v20.x (ou da versão LTS mais recente)](https://nodejs.org/en/) e do npm na versão 8.x. Nós utilizamos npm workspaces no projeto.\n\n## Desenvolvimento\n\nBruno está sendo desenvolvido como um aplicativo de desktop. Você precisa carregar o programa executando o aplicativo Next.js em um terminal e, em seguida, executar o aplicativo Electron em outro terminal.\n\n### Dependências\n\n- NodeJS v18\n\n### Desenvolvimento Local\n\n```bash\n# use nodejs 18 version\nnvm use\n\n# install deps\nnpm i --legacy-peer-deps\n\n# build graphql docs\nnpm run build:graphql-docs\n\n# build bruno query\nnpm run build:bruno-query\n\n# run next app (terminal 1)\nnpm run dev:web\n\n# run electron app (terminal 2)\nnpm run dev:electron\n```\n\n### Troubleshooting\n\nVocê pode se deparar com o erro `Unsupported platform` ao executar o comando `npm install`. Para corrigir isso, você precisará excluir a pasta `node_modules` e o arquivo `package-lock.json` e, em seguida, executar o comando `npm install` novamente. Isso deve instalar todos os pacotes necessários para executar o aplicativo.\n\n```shell\n# delete node_modules in sub-directories\nfind ./ -type d -name \"node_modules\" -print0 | while read -d $'\\0' dir; do\n  rm -rf \"$dir\"\ndone\n\n# delete package-lock in sub-directories\nfind . -type f -name \"package-lock.json\" -delete\n```\n\n### Testando\n\n```bash\n# executar testes do bruno-schema\nnpm test --workspace=packages/bruno-schema\n\n# executar testes em todos os ambientes de trabalho\nnpm test --workspaces --if-present\n```\n\n### Envio de Pull Request\n\n- Por favor, mantenha os PRs pequenos e focados em uma única coisa.\n- Siga o formato de criação de branches.\n  - feature/[nome da funcionalidade]: Esta branch deve conter alterações para uma funcionalidade específica.\n    - Exemplo: feature/dark-mode\n  - bugfix/[nome do bug]: Esta branch deve conter apenas correções para um bug específico.\n    - Exemplo: bugfix/bug-1\n"
  },
  {
    "path": "docs/contributing/contributing_ro.md",
    "content": "[English](../../contributing.md)\n\n## Haideţi să îmbunătățim Bruno, împreună!!\n\nNe bucurăm că doriți să îmbunătățiți bruno. Mai jos sunt instrucțiunile pentru ca să porniți bruno pe calculatorul dvs.\n\n### Stack-ul tehnologic\n\nBruno este construit cu Next.js și React. De asemenea, folosim electron pentru a livra o versiune desktop (care poate folosi colecții locale)\n\nBibliotecile pe care le folosim\n\n- CSS - Tailwind\n- Editori de cod - Codemirror\n- Management de condiție - Redux\n- Icoane - Tabler Icons\n- Formulare - formik\n- Validarea schemelor - Yup\n- Cererile client - axios\n- Observatorul sistemului de fișiere - chokidar\n\n### Dependențele\n\nVeți avea nevoie de [Node v20.x sau cea mai recentă versiune LTS](https://nodejs.org/en/) și npm 8.x. Noi folosim spații de lucru npm în proiect\n\n## Dezvoltarea\n\nBruno este dezvoltat ca o aplicație desktop. Ca să porniți aplicatia trebuie să rulați aplicația Next.js într-un terminal și apoi să rulați aplicația electron într-un alt terminal.\n\n```shell\n# folosiți nodejs versiunea 18\nnvm use\n\n# instalați dependențele\nnpm i --legacy-peer-deps\n\n# construiți documente graphql\nnpm run build:graphql-docs\n\n# construiți bruno query\nnpm run build:bruno-query\n\n# rulați aplicația next (terminal 1)\nnpm run dev:web\n\n# rulați aplicația electron (terminal 2)\nnpm run dev:electron\n```\n\n### Depanare\n\nEste posibil să întâmpinați o eroare `Unsupported platform` când rulați „npm install”. Pentru a remedia acest lucru, va trebui să ștergeți `node_modules` și `package-lock.json` și să rulați `npm install`. Aceasta ar trebui să instaleze toate pachetele necesare pentru a rula aplicația.\n\n```shell\n# Ștergeți node_modules din subdirectoare\nfind ./ -type d -name \"node_modules\" -print0 | while read -d $'\\0' dir; do\n  rm -rf \"$dir\"\ndone\n\n# Ștergeți package-lock din subdirectoare\nfind . -type f -name \"package-lock.json\" -delete\n```\n\n### Testarea\n\n```shell\n# executați teste bruno-schema\nnpm test --workspace=packages/bruno-schema\n\n# executați teste peste toate spațiile de lucru\nnpm test --workspaces --if-present\n```\n\n### Crearea unui Pull Request\n\n- Vă rugăm să păstrați PR-urile mici și concentrate pe un singur lucru\n- Vă rugăm să urmați formatul de creare a branchurilor\n  - feature/[Numele funcției]: Acest branch ar trebui să conțină modificări pentru o funcție anumită\n    - Exemplu: feature/dark-mode\n  - bugfix/[Numele eroarei]: Acest branch ar trebui să conţină numai remedieri pentru o eroare anumită\n    - Exemplu bugfix/bug-1\n"
  },
  {
    "path": "docs/contributing/contributing_ru.md",
    "content": "[English](../../contributing.md)\n\n## Давайте вместе сделаем Бруно лучше!!!\n\nЯ рад, что Вы хотите усовершенствовать bruno. Ниже приведены рекомендации по запуску bruno на вашем компьютере.\n\n### Стек\n\nBruno построен с использованием Next.js и React. Мы также используем electron для поставки десктопной версии ( которая поддерживает локальные коллекции )\n\nБиблиотеки, которые мы используем\n\n- CSS - Tailwind\n- Редакторы кода - Codemirror\n- Управление состоянием - Redux\n- Иконки - Tabler Icons\n- Формы - formik\n- Валидация схем - Yup\n- Запросы клиента - axios\n- Наблюдатель за файловой системой - chokidar\n\n### Зависимости\n\nВам потребуется [Node v20.x или последняя версия LTS](https://nodejs.org/en/) и npm 8.x. В проекте мы используем рабочие пространства npm\n\n### Приступим к коду\n\nПожалуйста, обратитесь к [development_ru.md](docs/development_ru.md) для получения инструкций по запуску локальной среды разработки.\n\n### Создание Pull Request\n\n- Пожалуйста, пусть PR будет небольшим и сфокусированным на одной вещи\n- Пожалуйста, соблюдайте формат создания веток\n  - feature/[название функции]: Эта ветка должна содержать изменения для конкретной функции\n    - Пример: feature/dark-mode\n  - bugfix/[название ошибки]: Эта ветка должна содержать только исправления для конкретной ошибки\n    - Пример bugfix/bug-1\n\n## Разработка\n\nBruno разрабатывается как десктопное приложение. Необходимо загрузить приложение, запустив приложение Next.js в одном терминале, а затем запустить приложение electron в другом терминале.\n\n### Зависимости\n\n- NodeJS v18\n\n### Локальная разработка\n\n```bash\n# используйте nodejs 18 версии\nnvm use\n\n# установите зависимости\nnpm i --legacy-peer-deps\n\n# билд документации по graphql\nnpm run build:graphql-docs\n\n# билд bruno query\nnpm run build:bruno-query\n\n# запустить next приложение ( терминал 1 )\nnpm run dev:web\n\n# запустить приложение electron ( терминал 2 )\nnpm run dev:electron\n```\n\n### Устранение неисправностей\n\nПри запуске `npm install` может возникнуть ошибка `Unsupported platform`. Чтобы исправить это, необходимо удалить `node_modules` и `package-lock.json` и запустить `npm install`. В результате будут установлены все пакеты, необходимые для работы приложения.\n\n```shell\n# Удаление node_modules в подкаталогах\nfind ./ -type d -name \"node_modules\" -print0 | while read -d $'\\0' dir; do\n  rm -rf \"$dir\"\ndone\n\n# Удаление package-lock в подкаталогах\nfind . -type f -name \"package-lock.json\" -delete\n```\n\n### Тестирование\n\n```bash\n# запустите тесты bruno-schema\nnpm test --workspace=packages/bruno-schema\n\n# запустите тесты во всех рабочих пространствах\nnpm test --workspaces --if-present\n```\n"
  },
  {
    "path": "docs/contributing/contributing_sk.md",
    "content": "## Urobme bruno lepším, spoločne !!\r\n\r\nSme radi, že chcete zlepšiť bruno. Nižšie sú uvedené pokyny, ako začať s výchovou bruno na vašom počítači.\r\n\r\n### Technologický zásobník\r\n\r\nBruno je vytvorené pomocou Next.js a React. Na dodávanie desktopovej verzie (ktorá podporuje lokálne kolekcie) používame aj electron.\r\n\r\nBalíčky, ktoré používame:\r\n\r\n- CSS - Tailwind\r\n- Editory kódu - Codemirror\r\n- Správa stavu - Redux\r\n- Ikony - Tabler Icons\r\n- Formuláre - formik\r\n- Overovanie schém - Yup\r\n- Klient požiadaviek - axios\r\n- Sledovač súborového systému - chokidar\r\n\r\n### Závislosti\r\n\r\nBudete potrebovať [NodeJS v18.x alebo najnovšiu verziu LTS](https://nodejs.org/en/) a npm versiu 8.x. V projekte používame pracovné priestory npm\r\n\r\n## Vývoj\r\n\r\nBruno sa vyvíja ako desktopová aplikácia. Aplikáciu je potrebné načítať spustením aplikácie Next.js v jednom termináli a potom spustiť aplikáciu electron v inom termináli.\r\n\r\n### Závislosti\r\n\r\n- NodeJS v18\r\n\r\n### Miestny vývoj\r\n\r\n```bash\r\n# použite verziu nodejs 18\r\nnvm use\r\n\r\n# nainštalovať balíčky\r\nnpm i --legacy-peer-deps\r\n\r\n# zostaviť balíčky\r\nnpm run build:graphql-docs\r\nnpm run build:bruno-query\r\nnpm run build:bruno-common\r\nnpm run build:bruno-converters\r\nnpm run build:bruno-requests\r\n\r\n# spustite ďalšiu aplikáciu (terminál 1)\r\nnpm run dev:web\r\n\r\n# spustite aplikáciu electron (terminál 2)\r\nnpm run dev:electron\r\n```\r\n\r\n### Riešenie problémov\r\n\r\nPri spustení `npm install` sa môžete stretnúť s chybou `Unsupported platform`. Ak chcete túto chybu odstrániť, musíte odstrániť súbory `node_modules`, `package-lock.json` a spustiť `npm install`. Tým by sa mali nainštalovať všetky potrebné balíky potrebné na spustenie aplikácie.\r\n\r\n```shell\r\n# Odstrániť node_modules v podadresároch\r\nfind ./ -type d -name \"node_modules\" -print0 | while read -d $'\\0' dir; do\r\n  rm -rf \"$dir\"\r\ndone\r\n\r\n# Odstráňte package-lock v podadresároch\r\nfind . -type f -name \"package-lock.json\" -delete\r\n```\r\n\r\n### Testovanie\r\n\r\n````bash\r\n# spustiť bruno-schema testy\r\nnpm test --workspace=packages/bruno-schema\r\n\r\n# spustiť testy vo všetkých pracovných priestoroch\r\nnpm test --workspaces --if-present\r\n```\r\n\r\n### Vyrobenie Pull Request\r\n\r\n- Prosím, aby PR boli malé a zamerané na jednu vec\r\n- Prosím, dodržujte formát vytvárania vetiev\r\n  - feature/[názov funkcie]: Táto vetva by mala obsahovať zmeny pre konkrétnu funkciu\r\n    - Príklad: feature/dark-mode\r\n  - bugfix/[názov chyby]: Táto vetva by mala obsahovať iba opravy konkrétnej chyby\r\n    - Príklad: bugfix/bug-1\r\n"
  },
  {
    "path": "docs/contributing/contributing_tr.md",
    "content": "[English](../../contributing.md)\n\n## Bruno'yu birlikte daha iyi hale getirelim!!!\n\nbruno'yu geliştirmek istemenizden mutluluk duyuyoruz. Aşağıda, bruno'yu bilgisayarınıza getirmeye başlamak için yönergeler bulunmaktadır.\n\n### Kullanılan Teknolojiler\n\nBruno, Next.js ve React kullanılarak oluşturulmuştur. Ayrıca bir masaüstü sürümü (yerel koleksiyonları destekleyen) göndermek için electron kullanıyoruz\n\nKullandığımız kütüphaneler\n\n- CSS - Tailwind\n- Kod Düzenleyiciler - Codemirror\n- Durum Yönetimi - Redux\n- Iconlar - Tabler Icons\n- Formlar - formik\n- Şema Doğrulama - Yup\n- İstek İstemcisi - axios\n- Dosya Sistemi İzleyicisi - chokidar\n\n### Bağımlılıklar\n\n[Node v20.x veya en son LTS sürümüne](https://nodejs.org/en/) ve npm 8.x'e ihtiyacınız olacaktır. Projede npm çalışma alanlarını kullanıyoruz\n\n## Gelişim\n\nBruno bir masaüstü uygulaması olarak geliştirilmektedir. Next.js uygulamasını bir terminalde çalıştırarak uygulamayı yüklemeniz ve ardından electron uygulamasını başka bir terminalde çalıştırmanız gerekir.\n\n### Bağımlılıklar\n\n- NodeJS v18\n\n### Yerel Geliştirme\n\n```bash\n# nodejs 18 sürümünü kullan\nnvm use\n\n# deps yükleyin\nnpm i --legacy-peer-deps\n\n# graphql dokümanlarını oluştur\nnpm run build:graphql-docs\n\n# bruno sorgusu oluştur\nnpm run build:bruno-query\n\n# sonraki uygulamayı çalıştır (terminal 1)\nnpm run dev:web\n\n# electron uygulamasını çalıştır (terminal 2)\nnpm run dev:electron\n```\n\n### Sorun Giderme\n\n`npm install`'ı çalıştırdığınızda `Unsupported platform` hatası ile karşılaşabilirsiniz. Bunu düzeltmek için `node_modules` ve `package-lock.json` dosyalarını silmeniz ve `npm install` dosyasını çalıştırmanız gerekecektir. Bu, uygulamayı çalıştırmak için gereken tüm gerekli paketleri yüklemelidir.\n\n```shell\n#  Alt dizinlerdeki node_modules öğelerini silme\nfind ./ -type d -name \"node_modules\" -print0 | while read -d $'\\0' dir; do\n  rm -rf \"$dir\"\ndone\n\n# Alt dizinlerdeki paket kilidini silme\nfind . -type f -name \"package-lock.json\" -delete\n```\n\n### Test\n\n```bash\n# bruno-schema testlerini çalıştır\nnpm test --workspace=packages/bruno-schema\n\n# tüm çalışma alanlarında testleri çalıştır\nnpm test --workspaces --if-present\n```\n\n### Pull Request Oluşturma\n\n- Lütfen PR'ları küçük tutun ve tek bir şeye odaklanın\n- Lütfen şube oluşturma formatını takip edin\n  - feature/[özellik adı]: Bu dal belirli bir özellik için değişiklikler içermelidir\n    - Örnek: feature/dark-mode\n  - bugfix/[hata adı]: Bu dal yalnızca belirli bir hata için hata düzeltmeleri içermelidir\n    - Örnek bugfix/bug-1\n"
  },
  {
    "path": "docs/contributing/contributing_ua.md",
    "content": "[English](../../contributing.md)\n\n## Давайте зробимо Bruno краще, разом !!\n\nЯ дуже радий що Ви бажаєте покращити Bruno. Нижче наведені вказівки як розпочати розробку Bruno на Вашому комп'ютері.\n\n### Стек технологій\n\nBruno побудований на Next.js та React. Також для десктопної версії (яка підтримує локальні колекції) використовується Electron\n\nБібліотеки, які ми використовуємо\n\n- CSS - Tailwind\n- Редактори коду - Codemirror\n- Керування станом - Redux\n- Іконки - Tabler Icons\n- Форми - formik\n- Валідація по схемі - Yup\n- Клієнт запитів - axios\n- Спостерігач за файловою системою - chokidar\n\n### Залежності\n\nВам знадобиться [Node v20.x або остання LTS версія](https://nodejs.org/en/) та npm 8.x. Ми використовуєм npm workspaces в цьому проекті\n\n### Починаєм писати код\n\nБудь ласка, зверніться до [development_ua.md](docs/development_ua.md) за інструкціями щодо запуску локального середовища розробки.\n\n### Створення Pull Request-ів\n\n- Будь ласка, робіть PR-и маленькими і сфокусованими на одній речі\n- Будь ласка, слідуйте формату назв гілок\n  - feature/[назва feature]: Така гілка має містити зміни лише щодо конкретної feature\n    - Приклад: feature/dark-mode\n  - bugfix/[назва баґу]: Така гілка має містити лише виправлення конкретного багу\n    - Приклад: bugfix/bug-1\n\n## Розробка\n\nBruno розробляється як декстопний застосунок. Вам потрібно запустити Next.js в одній сесії терміналу, та запустити застосунок Electron в іншій сесії терміналу.\n\n### Залежності\n\n- NodeJS v18\n\n### Локальна розробка\n\n```bash\n# Використовуйте nodejs 18-ї версії\nnvm use\n\n# встановіть залежності\nnpm i --legacy-peer-deps\n\n# зберіть документацію graphql\nnpm run build:graphql-docs\n\n# зберіть bruno query\nnpm run build:bruno-query\n\n# запустіть додаток next (термінал 1)\nnpm run dev:web\n\n# запустіть додаток електрон (термінал 2)\nnpm run dev:electron\n```\n\n### Усунення несправностей\n\nВи можете зтикнутись із помилкою `Unsupported platform` коли запускаєте `npm install`. Щоб усунути цю проблему, вам потрібно видалити `node_modules` та `package-lock.json`, і тоді запустити `npm install`. Це має встановити всі потрібні для запуску додатку пекеджі.\n\n```shell\n# Видаліть node_modules в піддиректоріях\nfind ./ -type d -name \"node_modules\" -print0 | while read -d $'\\0' dir; do\n  rm -rf \"$dir\"\ndone\n\n# Видаліть package-lock в піддиректоріях\nfind . -type f -name \"package-lock.json\" -delete\n```\n\n### Тестування\n\n```bash\n# запустити тести bruno-schema\nnpm test --workspace=packages/bruno-schema\n\n# запустити тести у всіх робочих просторах\nnpm test --workspaces --if-present\n```\n"
  },
  {
    "path": "docs/contributing/contributing_zhtw.md",
    "content": "[English](../../contributing.md)\n\n## 讓我們一起來讓 Bruno 變得更好！\n\n我們很高興您希望一同改善 Bruno。以下是在您的電腦上開始運行 Bruno 的規則及指南。\n\n### 技術細節\n\nBruno 使用 Next.js 和 React 構建。我們使用 Electron 來封裝及發佈桌面版本。\n\n我們使用的函式庫：\n\n- CSS - Tailwind\n- 程式碼編輯器 - Codemirror\n- 狀態管理 - Redux\n- Icons - Tabler Icons\n- 表單 - formik\n- 結構驗證- Yup\n- 請求用戶端 - axios\n- 檔案系統監測 - chokidar\n\n### 依賴關係\n\n您需要使用 [Node v20.x 或最新的 LTS 版本](https://nodejs.org/en/) 和 npm 8.x。我們在這個專案中使用 npm 工作區（_npm workspaces_）。\n\n## 開發\n\nBruno 正以桌面應用程式的形式開發。您需要在一個終端機中執行 Next.js 來載入應用程式，然後在另一個終端機中執行 electron 應用程式。\n\n### 開發依賴\n\n- NodeJS v18\n\n### 本地開發\n\n```bash\n# 使用 nodejs 第 18 版\nnvm use\n\n# 安裝相依套件（使用--legacy-peer-deps 解決套件相依性問題）\nnpm i --legacy-peer-deps\n\n# 建立 graphql 文件\nnpm run build:graphql-docs\n\n# 建立 bruno 查詢\nnpm run build:bruno-query\n\n# 執行 next 應用程式（終端機 1）\nnpm run dev:web\n\n# 執行 electron 應用程式（終端機 2）\nnpm run dev:electron\n```\n\n### 故障排除\n\n在執行 `npm install` 時，您可能會遇到 `Unsupported platform` 的錯誤訊息。爲了解決這個問題，您需要刪除 `node_modules` 資料夾和 `package-lock.json` 檔案，然後再執行一次 `npm install`。這應該能重新安裝應用程式所需的套件。\n\n```shell\n# 刪除子資料夾中的 node_modules 資料夾\nfind ./ -type d -name \"node_modules\" -print0 | while read -d $'\\0' dir; do\n  rm -rf \"$dir\"\ndone\n\n# 刪除子資料夾中的 package-lock.json 檔案\nfind . -type f -name \"package-lock.json\" -delete\n```\n\n### 測試\n\n```bash\n# 執行布魯諾架構測試\nnpm test --workspace=packages/bruno-schema\n\n# 對所有工作區執行測試\nnpm test --workspaces --if-present\n```\n\n### 發送 Pull Request\n\n- 請保持 PR 精簡並專注於一個目標\n- 請遵循建立分支的格式：\n  - feature/[feature name]：該分支應包含特定功能的更改\n    - 範例：feature/dark-mode\n  - bugfix/[bug name]：該分支應僅包含特定 bug 的修復\n    - 範例：bugfix/bug-1\n"
  },
  {
    "path": "docs/playwright-testing-guide.md",
    "content": "# Playwright Testing Guide for Bruno\n\nThis guide explains how to create and run Playwright test cases for the Bruno application using the UI.\n\n## Table of Contents\n\n- [Overview](#overview)\n- [Prerequisites](#prerequisites)\n- [Creating Tests Using Codegen](#creating-tests-using-codegen)\n- [Manual Test Creation](#manual-test-creation)\n- [Test Structure and Organization](#test-structure-and-organization)\n- [Available Test Fixtures](#available-test-fixtures)\n- [Running Tests](#running-tests)\n- [Best Practices](#best-practices)\n- [Examples](#examples)\n- [Troubleshooting](#troubleshooting)\n\n## Overview\n\nBruno uses Playwright for end-to-end testing of its Electron application. The testing setup includes custom fixtures for Electron app testing and utilities for managing test data.\n\n## Prerequisites\n\n- Node.js installed\n- All dependencies installed (`npm install`)\n- Electron app can be built and run\n\n## Creating Tests Using Codegen\n\nThe easiest way to create tests is using Playwright's codegen feature, which records your UI interactions and generates test code.\n\n### Using the Built-in Codegen Script\n\n```bash\n# Generate a test with a specific name\nnpm run test:codegen my-new-test\n\n# Generate a test without specifying a name (will prompt for input)\nnpm run test:codegen\n```\n\n### What Happens During Codegen\n\n1. The Electron app launches automatically\n2. Playwright Inspector opens in a separate window\n3. You interact with the Bruno UI\n4. Actions are recorded and converted to test code\n5. The generated test file is saved in `e2e-tests/`\n\n### Codegen Workflow\n\n1. **Start Recording**: Run the codegen command\n2. **Interact with UI**: Perform the actions you want to test\n3. **Add Assertions**: Use the inspector to add assertions\n4. **Save Test**: The test file is automatically generated\n5. **Review and Refine**: Edit the generated test as needed\n\n## Manual Test Creation\n\nYou can also create tests manually by following the established patterns.\n\n### Basic Test Structure\n\n```typescript\nimport { test, expect } from '../../playwright';\n\ntest('Test description', async ({ page }) => {\n  // Test steps here\n  await page.getByLabel('Some Label').click();\n\n  // Assertions\n  await expect(page.getByText('Expected Text')).toBeVisible();\n});\n```\n\n### Test with Temporary Data\n\n```typescript\nimport { test, expect } from '../../playwright';\n\ntest('Test with temporary data', async ({ page, createTmpDir }) => {\n  // Create temporary directory for test data\n  const testDir = await createTmpDir('test-collection');\n\n  // Test steps\n  await page.getByLabel('Create Collection').click();\n  await page.getByLabel('Name').fill('test-collection');\n  await page.getByLabel('Location').fill(testDir);\n\n  // Assertions\n  await expect(page.getByText('test-collection')).toBeVisible();\n});\n```\n\n## Test Structure and Organization\n\n### Directory Structure\n\n```\ne2e-tests/\n├── 001-sanity-tests/          # Basic functionality tests\n│   ├── 001-home-screen.spec.ts\n│   └── 002-create-new-collection-and-new-request.spec.ts\n├── 002-feature-tests/         # Specific feature tests\n├── 003-integration-tests/     # Complex workflow tests\n└── bruno-testbench/           # Test utilities and helpers\n```\n\n### Naming Conventions\n\n- **Files**: Use descriptive names with `.spec.ts` extension\n- **Tests**: Use clear, descriptive test names\n- **Folders**: Use numbered prefixes for ordering\n\n### Test File Template\n\n```typescript\nimport { test, expect } from '../../playwright';\n\ntest.describe('Feature Name', () => {\n  test('should perform specific action', async ({ page }) => {\n    // Arrange\n    // Act\n    // Assert\n  });\n\n  test('should handle error case', async ({ page }) => {\n    // Test error scenarios\n  });\n});\n```\n\n## Available Test Fixtures\n\nThe Bruno Playwright setup provides several custom fixtures:\n\n### Core Fixtures\n\n- `page`: Main page for testing\n- `context`: Browser context\n- `electronApp`: Electron application instance\n\n### Utility Fixtures\n\n- `createTmpDir`: Creates temporary directories for test data\n- `newPage`: Creates a new page instance\n- `pageWithUserData`: Page with custom user data\n- `launchElectronApp`: Launches a new Electron app instance\n- `reuseOrLaunchElectronApp`: Reuses existing app or launches new one\n\n### Using Fixtures\n\n```typescript\ntest('Test with multiple fixtures', async ({ page, createTmpDir, electronApp }) => {\n  const testDir = await createTmpDir('test-data');\n\n  // Your test logic here\n});\n```\n\n## Running Tests\n\n### Basic Commands\n\n```bash\n# Run all tests\nnpm run test:e2e\n\n# Run specific test file\nnpx playwright test e2e-tests/001-sanity-tests/001-home-screen.spec.ts\n\n# Run tests in a specific folder\nnpx playwright test e2e-tests/001-sanity-tests/\n```\n\n### Advanced Options\n\n```bash\n# Run with UI mode (for debugging)\nnpx playwright test --ui\n\n# Run in headed mode (see browser)\nnpx playwright test --headed\n\n# Run with specific browser\nnpx playwright test --project=\"Bruno Electron App\"\n\n# Run with debugging\nnpx playwright test --debug\n\n# Run with trace recording\nnpx playwright test --trace on\n```\n\n### CI/CD Integration\n\n```bash\n# Install browsers for CI\nnpx playwright install\n\n# Run tests in CI mode\nnpm run test:e2e\n```\n\n## Best Practices\n\n### 1. Use Semantic Selectors\n\n**Preferred:**\n\n```typescript\nawait page.getByRole('button', { name: 'Create' }).click();\nawait page.getByLabel('Collection Name').fill('test');\nawait page.getByText('Success message').toBeVisible();\n```\n\n**Avoid:**\n\n```typescript\nawait page.locator('.btn-primary').click();\nawait page.locator('#collection-name').fill('test');\n```\n\n### 2. Create Isolated Tests\n\nEach test should be independent and not rely on other tests:\n\n```typescript\ntest('should create collection', async ({ page, createTmpDir }) => {\n  const testDir = await createTmpDir('collection-test');\n\n  // Test creates its own data\n  await page.getByLabel('Create Collection').click();\n  await page.getByLabel('Name').fill('test-collection');\n  await page.getByLabel('Location').fill(testDir);\n\n  // Clean up happens automatically via createTmpDir\n});\n```\n\n### 3. Add Meaningful Assertions\n\nAlways verify the expected outcomes:\n\n```typescript\ntest('should save request successfully', async ({ page }) => {\n  // Arrange\n  await page.getByLabel('Create Collection').click();\n\n  // Act\n  await page.getByRole('button', { name: 'Save' }).click();\n\n  // Assert\n  await expect(page.getByText('Request saved successfully')).toBeVisible();\n  await expect(page.getByRole('tab', { name: 'GET request' })).toBeVisible();\n});\n```\n\n### 4. Handle Async Operations\n\n```typescript\ntest('should wait for network requests', async ({ page }) => {\n  // Wait for specific network request\n  await page.waitForResponse((response) => response.url().includes('/api/endpoint'));\n\n  // Or wait for element to be stable\n  await page.waitForSelector('[data-testid=\"loading\"]', { state: 'hidden' });\n});\n```\n\n### 5. Use Test Data Management\n\n```typescript\ntest('should work with test data', async ({ page, createTmpDir }) => {\n  const testDir = await createTmpDir('test-data');\n\n  // Create test files\n  await fs.writeFile(path.join(testDir, 'test.bru'), testContent);\n\n  // Use in test\n  await page.getByLabel('Open Collection').click();\n  await page.getByText(testDir).click();\n});\n```\n\n## Examples\n\n### Example 1: Basic Collection Creation\n\n```typescript\nimport { test, expect } from '../../playwright';\n\ntest('should create a new collection', async ({ page, createTmpDir }) => {\n  const testDir = await createTmpDir('new-collection');\n\n  await page.getByLabel('Create Collection').click();\n  await page.getByLabel('Name').fill('My Test Collection');\n  await page.getByLabel('Location').fill(testDir);\n  await page.getByRole('button', { name: 'Create' }).click();\n\n  await expect(page.getByText('My Test Collection')).toBeVisible();\n});\n```\n\n### Example 2: Request Creation and Execution\n\n```typescript\nimport { test, expect } from '../../playwright';\n\ntest('should create and execute HTTP request', async ({ page, createTmpDir }) => {\n  const testDir = await createTmpDir('request-test');\n\n  // Create collection\n  await page.getByLabel('Create Collection').click();\n  await page.getByLabel('Name').fill('Request Test');\n  await page.getByLabel('Location').fill(testDir);\n  await page.getByRole('button', { name: 'Create' }).click();\n\n  // Create request\n  await page.locator('#create-new-tab').getByRole('img').click();\n  await page.getByPlaceholder('Request Name').fill('Test Request');\n  await page.locator('#new-request-url .CodeMirror').click();\n  await page.locator('textarea').fill('http://localhost:8081/ping');\n  await page.getByRole('button', { name: 'Create' }).click();\n\n  // Execute request\n  await page.locator('#send-request').getByRole('img').nth(2).click();\n\n  // Verify response\n  await expect(page.getByRole('main')).toContainText('200 OK');\n});\n```\n\n### Example 3: Environment Management\n\n```typescript\nimport { test, expect } from '../../playwright';\n\ntest('should create and use environment variables', async ({ page, createTmpDir }) => {\n  const testDir = await createTmpDir('env-test');\n\n  // Setup collection\n  await page.getByLabel('Create Collection').click();\n  await page.getByLabel('Name').fill('Environment Test');\n  await page.getByLabel('Location').fill(testDir);\n  await page.getByRole('button', { name: 'Create' }).click();\n\n  // Create environment\n  await page.getByRole('button', { name: 'Environments' }).click();\n  await page.getByRole('button', { name: 'Add Environment' }).click();\n  await page.getByLabel('Environment Name').fill('Development');\n  await page.getByRole('button', { name: 'Create' }).click();\n\n  // Add variable\n  await page.getByRole('button', { name: 'Add Variable' }).click();\n  await page.getByLabel('Variable Name').fill('API_URL');\n  await page.getByLabel('Variable Value').fill('http://localhost:3000');\n  await page.getByRole('button', { name: 'Save' }).click();\n\n  await expect(page.getByText('API_URL')).toBeVisible();\n});\n```\n\n## Troubleshooting\n\n### Common Issues\n\n1. **Electron App Not Starting**\n\n   ```bash\n   # Ensure dependencies are installed\n   npm install\n\n   # Try running the app manually first\n   npm run dev:electron\n   ```\n\n2. **Tests Timing Out**\n\n   ```typescript\n   // Increase timeout for specific test\n   test('slow test', async ({ page }) => {\n     test.setTimeout(60000); // 60 seconds\n     // Test steps\n   });\n   ```\n\n3. **Element Not Found**\n\n   ```typescript\n   // Wait for element to be present\n   await page.waitForSelector('[data-testid=\"element\"]');\n\n   // Or use more specific selectors\n   await page.getByRole('button', { name: 'Exact Button Text' }).click();\n   ```\n\n4. **Flaky Tests**\n\n   ```typescript\n   // Use stable selectors\n   await page.getByTestId('stable-id').click();\n\n   // Wait for state changes\n   await page.waitForLoadState('networkidle');\n   ```\n\n### Debug Mode\n\n```bash\n# Run with debug mode\nnpx playwright test --debug\n\n# Run specific test in debug mode\nnpx playwright test --debug e2e-tests/001-sanity-tests/001-home-screen.spec.ts\n```\n\n### Trace Analysis\n\n```bash\n# Run with trace recording\nnpx playwright test --trace on\n\n# View trace in browser\nnpx playwright show-trace test-results/trace-*.zip\n```\n\n## Configuration\n\nThe Playwright configuration is in `playwright.config.ts`:\n\n```typescript\nexport default defineConfig({\n  testDir: './e2e-tests',\n  fullyParallel: false,\n  forbidOnly: !!process.env.CI,\n  retries: process.env.CI ? 1 : 0,\n  workers: process.env.CI ? undefined : 1,\n\n  projects: [\n    {\n      name: 'Bruno Electron App'\n    }\n  ],\n\n  webServer: [\n    {\n      command: 'npm run dev:web',\n      url: 'http://localhost:3000',\n      reuseExistingServer: !process.env.CI\n    },\n    {\n      command: 'npm start --workspace=packages/bruno-tests',\n      url: 'http://localhost:8081/ping',\n      reuseExistingServer: !process.env.CI\n    }\n  ]\n});\n```\n\n## Additional Resources\n\n- [Playwright Documentation](https://playwright.dev/)\n- [Playwright Test API](https://playwright.dev/docs/api/class-test)\n- [Electron Testing with Playwright](https://playwright.dev/docs/api/class-electronapplication)\n- [Bruno Project Structure](../readme.md)\n\n---\n\nFor questions or issues with testing, please refer to the project's contributing guidelines or create an issue in the repository.\n"
  },
  {
    "path": "docs/publishing/publishing_bn.md",
    "content": "[English](../../publishing.md)\n\n### ব্রুনোকে নতুন প্যাকেজ ম্যানেজারে প্রকাশ করা\n\nযদিও আমাদের কোড ওপেন সোর্স এবং সবার ব্যবহারের জন্য উপলব্ধ, তবে আমরা নতুন প্যাকেজ ম্যানেজারে প্রকাশনা বিবেচনা করার আগে আমাদের সাথে যোগাযোগ করার জন্য অনুরোধ করি। ব্রুনোর স্রষ্টা হিসাবে, আমি এই প্রকল্পের জন্য `Bruno` ট্রেডমার্ক ধারণ করি এবং এর বিতরণ পরিচালনা করতে চাই। যদি আপনি একটি নতুন প্যাকেজ ম্যানেজারে ব্রুনো দেখতে চান, দয়া করে একটি GitHub ইস্যু তুলুন।\n\nযদিও আমাদের বেশিরভাগ বৈশিষ্ট্য বিনামূল্যে এবং ওপেন সোর্স (যা REST এবং GraphQL API গুলিকে কভার করে), আমরা ওপেন-সোর্স নীতি এবং স্থায়িত্বের মধ্যে একটি সুসঙ্গত ভারসাম্য বজায় রাখার জন্য চেষ্টা করি - https://github.com/usebruno/bruno/discussions/269\n"
  },
  {
    "path": "docs/publishing/publishing_cn.md",
    "content": "[English](../../publishing.md)\n\n### 将 Bruno 发布到新的包管理器\n\n虽然我们的代码是开源的，每个人都可以使用，但我们恳请您在考虑在新的包管理器上发布之前与我们联系。作为 Bruno 的创建者，我拥有这个项目的 Bruno 商标并希望管理其发行。如果您希望看到它使用新的包管理器，请提交一个 GitHub issue。\n\n虽然我们的大部分功能都是免费与开源的 (涵盖 REST 和 GraphQL APIs) ，但我们努力在开源原则和可持续性之间取得和谐的平衡 - https://github.com/usebruno/bruno/discussions/269\n"
  },
  {
    "path": "docs/publishing/publishing_de.md",
    "content": "[English](../../publishing.md)\n\n### Veröffentlichung von Bruno über neue Paket-Manager\n\nObwohl Bruno Open Source und für alle frei zugänglich ist, bitten wir dich Kontakt zu uns aufzunehmen, bevor du Bruno über weitere Paket-Manager veröffentlichst.\nAls Schöpfer von Bruno liegen alle Marktrechte von `Bruno` bei mir und ich möchte die volle Kontrolle über alle Verbreitungswege behalten.\nFalls Bruno über einen weiteren Paketmanager veröffentlicht werden soll, eröffne bitte ein GitHub-Issue.\n\nWährend ein Großteil der Features kostenlos und Open Source ist (beinhaltet REST und GraphQL APIs),\nbemühen wir uns um ein harmonisches Gleichgewicht zwischen Open-Source-Prinzipien und Nachhaltigkeit - https://github.com/usebruno/bruno/discussions/269\n"
  },
  {
    "path": "docs/publishing/publishing_fa.md",
    "content": "[English](../../publishing.md)\n\n### انتشار Bruno در یک پکیج منیجر جدید\n\nاگرچه کد ما متن‌باز است و همه می‌توانند از آن استفاده کنند، لطفاً قبل از انتشار Bruno در مدیر بسته‌های جدید با ما تماس بگیرید. به عنوان سازنده Bruno، علامت تجاری `Bruno` را برای این پروژه دارم و مایلم توزیع آن را مدیریت کنم. اگر دوست دارید Bruno را در یک مدیر بسته جدید ببینید، لطفاً یک issue در گیت‌هاب ثبت کنید.\n\nاگرچه بیشتر قابلیت‌های ما رایگان و متن‌باز هستند (شامل REST و GraphQL Apis)،\nما تلاش می‌کنیم بین اصول متن‌باز و توسعه پایدار تعادل مناسبی برقرار کنیم - https://github.com/usebruno/bruno/discussions/269\n"
  },
  {
    "path": "docs/publishing/publishing_fr.md",
    "content": "[English](../../publishing.md)\n\n### Publier Bruno dans un nouveau gestionnaire de paquets\n\nBien que notre code soit open source et disponible pour tout le monde, nous vous remercions de nous contacter avant de considérer sa publication sur un nouveau gestionnaire de paquets. En tant que créateur de Bruno, je détiens la marque `Bruno` pour ce projet et j'aimerais gérer moi-même sa distribution. Si vous voyez Bruno sur un nouveau gestionnaire de paquets, merci de créer une _issue_ GitHub.\n\nBien que la majorité de nos fonctionnalités soient gratuites et open source (ce qui couvre les APIs REST et GraphQL), nous nous efforçons de trouver un équilibre harmonieux entre les principes de l'open source et la pérennité - https://github.com/usebruno/bruno/discussions/269\n"
  },
  {
    "path": "docs/publishing/publishing_ja.md",
    "content": "[English](../../publishing.md)\n\n### Bruno を新しいパッケージマネージャに公開する場合の注意\n\n私たちのソースコードはオープンソースで誰でも使用できますが、新しいパッケージマネージャで公開を検討する前に、私たちにご連絡ください。私は Bruno の製作者として、このプロジェクト「Bruno」の商標を保有しており、その配布を管理したいと考えています。もし新しいパッケージマネージャで Bruno を使いたい場合は、GitHub の issue を立ててください。\n\n私たちの機能の大部分が無料でオープンソース(REST や GraphQL の API も含む)ですが、\n私たちはオープンソースの原則と長期的な維持の間でよいバランスをとれるように努力しています- https://github.com/usebruno/bruno/discussions/269\n"
  },
  {
    "path": "docs/publishing/publishing_nl.md",
    "content": "[English](../../publishing.md)\n\n### Bruno publiceren naar een nieuwe pakketbeheerder\n\nHoewel onze code open source is en beschikbaar voor iedereen, verzoeken we je vriendelijk om contact met ons op te nemen voordat je publicatie overweegt op nieuwe pakketbeheerders. Als de maker van Bruno houd ik het handelsmerk `Bruno` voor dit project en wil ik het distributieproces beheren. Als je Bruno op een nieuwe pakketbeheerder wilt zien, dien dan een GitHub-issue in.\n\nHoewel de meerderheid van onze functies gratis en open source zijn (die REST en GraphQL API's dekken), streven we ernaar een harmonieuze balans te vinden tussen open-source principes en duurzaamheid - https://github.com/usebruno/bruno/discussions/269"
  },
  {
    "path": "docs/publishing/publishing_pl.md",
    "content": "[English](../../publishing.md)\n\n### Publikowanie Bruno w nowym menedżerze pakietów\n\nChociaż nasz kod jest otwartoźródłowy i dostępny dla każdego do użytku, uprzejmie prosimy o kontakt z nami przed rozważeniem publikacji w nowych menedżerach pakietów. Jako twórca Bruno, posiadam znak towarowy `Bruno` dla tego projektu i chciałbym zarządzać jego dystrybucją. Jeśli chcesz zobaczyć Bruno w nowym menedżerze pakietów, proszę zgłoś problem na GitHubie.\n\nChociaż większość naszych funkcji jest darmowa i otwartoźródłowa (co obejmuje REST i GraphQL Apis),\nstaramy się osiągnąć harmonijny balans między zasadami open-source a zrównoważonym rozwojem - https://github.com/usebruno/bruno/discussions/269\n"
  },
  {
    "path": "docs/publishing/publishing_pt_br.md",
    "content": "[English](../../publishing.md)\n\n### Publicando Bruno em um novo gerenciador de pacotes\n\nEmbora nosso código seja de código aberto e esteja disponível para todos usarem, pedimos gentilmente que entre em contato conosco antes de considerar a publicação em novos gerenciadores de pacotes. Como o criador da ferramenta, mantenho a marca registrada `Bruno` para este projeto e gostaria de gerenciar sua distribuição. Se deseja ver o Bruno em um novo gerenciador de pacotes, por favor, solicite através de uma issue no GitHub.\n\nEmbora a maioria de nossas funcionalidades seja gratuita e de código aberto (o que abrange API's REST e GraphQL), buscamos alcançar um equilíbrio harmonioso entre os princípios de código aberto e sustentabilidade. - https://github.com/usebruno/bruno/discussions/269\n"
  },
  {
    "path": "docs/publishing/publishing_ro.md",
    "content": "[English](../../publishing.md)\n\n### Publicarea lui Bruno la un gestionar de pachete nou\n\nDeși codul nostru este cu sursă deschisă și disponibil pentru utilizare pentru toată lumea, vă rugăm să ne contactați înainte de a considera publicarea pe gestionari de pachete noi. În calitate de creator al lui Bruno, dețin marca comercială `Bruno` pentru acest proiect și aș dori să gestionez distribuția acestuia. Dacă doriți să-l vedeți pe Bruno pe un gestionar de pachete nou, vă rugăm să creați un issue pe GitHub.\n\nÎn timp ce majoritatea funcțiilor noastre sunt gratuite și cu sursă deschisă (ceea ce acoperă API-uri REST și GraphQL),\nne străduim să găsim un echilibru armonios între principiile de sursă deschisă și sustenabilitate - https://github.com/usebruno/bruno/discussions/269\n"
  },
  {
    "path": "docs/publishing/publishing_tr.md",
    "content": "[English](../../publishing.md)\n\n### Bruno'yu yeni bir paket yöneticisine yayınlama\n\nKodumuz açık kaynak kodlu ve herkesin kullanımına açık olsa da, yeni paket yöneticilerinde yayınlamayı düşünmeden önce bize ulaşmanızı rica ediyoruz. Bruno'nun yaratıcısı olarak, bu proje için `Bruno` ticari markasına sahibim ve dağıtımını yönetmek istiyorum. Bruno'yu yeni bir paket yöneticisinde görmek istiyorsanız, lütfen bir GitHub sorunu oluşturun.\n\nÖzelliklerimizin çoğu ücretsiz ve açık kaynak olsa da (REST ve GraphQL Apis'i kapsar),\naçık kaynak ilkeleri ile sürdürülebilirlik arasında uyumlu bir denge kurmaya çalışıyoruz - https://github.com/usebruno/bruno/discussions/269\n"
  },
  {
    "path": "docs/publishing/publishing_zhtw.md",
    "content": "[English](../../publishing.md)\n\n### 將 Bruno 發佈到新的套件管理器\n\n雖然我們的程式碼是開源的並且可供所有人使用，但我們懇請您在考慮在新的套件管理器上發布之前與我們聯繫。作為 Bruno 的創建者，我擁有這個專案的 Bruno 商標並希望管理其發行。如果您希望看到 Bruno 使用新的套件管理器，請提出一個 GitHub issue。\n\n雖然我們的大部分功能都是免費和開源（涵蓋 REST 和 GraphQL APIs），但我們努力在開源的原則和永續性之間，取得和諧的平衡 - https://github.com/usebruno/bruno/discussions/269\n"
  },
  {
    "path": "docs/readme/readme_ar.md",
    "content": "<br />\n<img src=\"../../assets/images/logo-transparent.png\" width=\"80\"/>\n\n### برونو - بيئة تطوير مفتوحة المصدر لاستكشاف واختبار واجهات برمجة التطبيقات (APIs).\n\n[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)\n[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)\n[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)\n[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)\n[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)\n[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)\n\n[English](../../readme.md)\n| [Українська](./readme_ua.md)\n| [Русский](./readme_ru.md)\n| [Türkçe](./readme_tr.md)\n| [Deutsch](./readme_de.md)\n| [Français](./readme_fr.md)\n| [Português (BR)](./readme_pt_br.md)\n| [한국어](./readme_kr.md)\n| [বাংলা](./readme_bn.md)\n| [Español](./readme_es.md)\n| [Italiano](./readme_it.md)\n| [Română](./readme_ro.md)\n| [Polski](./readme_pl.md)\n| [简体中文](./readme_cn.md)\n| [正體中文](./readme_zhtw.md)\n| **العربية**\n| [日本語](./readme_ja.md)\n| [ქართული](./readme_ka.md)\n\nبرونو هو عميل API جديد ومبتكر، يهدف إلى ثورة الحالة الحالية التي يمثلها برنامج Postman وأدوات مماثلة هناك.\n\nيقوم برونو بتخزين مجموعاتك مباشرة في مجلد على نظام الملفات الخاص بك. نحن نستخدم لغة ترميز النص العادية، Bru، لحفظ معلومات حول طلبات واجهة برمجة التطبيقات (API).\n\nيمكنك استخدام Git أو أي نظام تحكم في الإصدار الذي تفضله للتعاون على مجموعات API الخاصة بك.\n\nبرونو هو خاص بالاستخدام دون اتصال بالإنترنت. ليس هناك خطط لإضافة مزامنة السحابة إلى برونو أبدًا. نحن نقدر خصوصية بياناتك ونعتقد أنه يجب أن تظل على جهازك. اقرأ رؤيتنا على المدى الطويل [هنا](https://github.com/usebruno/bruno/discussions/269)\n\n📢 شاهد حديثنا الأخير في مؤتمر India FOSS 3.0 [هنا](https://www.youtube.com/watch?v=7bSMFpbcPiY)\n\n![bruno](/assets/images/landing-2.png) <br /><br />\n\n\n### التثبيت\n\nبرونو متاح كتنزيل ثنائي [على موقعنا على الويب](https://www.usebruno.com/downloads) لأنظمة التشغيل Mac و Windows و Linux.\n\nيمكنك أيضًا تثبيت برونو عبر مديري الحزم مثل Homebrew و Chocolatey و Scoop و Snap و Flatpak و Apt.\n\n```sh\n# على نظام Mac عبر Homebrew\nbrew install bruno\n\n# على نظام Windows عبر Chocolatey\nchoco install bruno\n\n# على نظام Windows عبر Scoop\nscoop bucket add extras\nscoop install bruno\n\n# على نظام Linux عبر Snap\nsnap install bruno\n\n# على نظام Linux عبر Flatpak\nflatpak install com.usebruno.Bruno\n\n# على نظام Linux عبر Apt\nsudo mkdir -p /etc/apt/keyrings\nsudo apt update && sudo apt install gpg curl\ncurl -fsSL \"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266\" \\\n  | gpg --dearmor \\\n  | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null\nsudo chmod 644 /etc/apt/keyrings/bruno.gpg\necho \"deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable\" \\\n  | sudo tee /etc/apt/sources.list.d/bruno.list\nsudo apt update && sudo apt install bruno\n```\n\n### التشغيل عبر منصات متعددة 🖥️\n\n![bruno](/assets/images/run-anywhere.png) <br /><br />\n\n### التعاون عبر Git 👩‍💻🧑‍💻\n\nأو أي نظام تحكم في الإصدار الذي تفضله\n\n![bruno](/assets/images/version-control.png) <br /><br />\n\n### الروابط المهمة 📌\n\n- [رؤيتنا على المدى الطويل](https://github.com/usebruno/bruno/discussions/269)\n- [خارطة الطريق](https://github.com/usebruno/bruno/discussions/384)\n- [التوثيق](https://docs.usebruno.com)\n- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)\n- [الموقع الإلكتروني](https://www.usebruno.com)\n- [التسعير](https://www.usebruno.com/pricing)\n- [التنزيل](https://www.usebruno.com/downloads)\n- [Github Sponsors](https://github.com/sponsors/helloanoop).\n\n### عروض 🎥\n\n- [الشهادات](https://github.com/usebruno/bruno/discussions/343)\n- [مركز المعرفة](https://github.com/usebruno/bruno/discussions/386)\n- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)\n\n### الدعم ❤️\n\nإذا كنت تحب برونو وترغب في دعم عملنا مفتوح المصدر، فكر في رعايتنا عبر [Github Sponsors](https://github.com/sponsors/helloanoop).\n\n### شارك الشهادات 📣\n\nإذا كان برونو قد ساعدك في العمل وفرقك، فلا تنسى مشاركة [شهاداتك في مناقشتنا على GitHub](https://github.com/usebruno/bruno/discussions/343)\n\n### نشر إلى مديري الحزم الجديدة\n\nيرجى الرجوع [هنا](../../publishing.md) لمزيد من المعلومات.\n\n### تواصل معنا 🌐\n\n[𝕏 (تويتر)](https://twitter.com/use_bruno) <br />\n[الموقع الإلكتروني](https://www.usebruno.com) <br />\n[ديسكورد](https://discord.com/invite/KgcZUncpjq) <br />\n[لينكدإن](https://www.linkedin.com/company/usebruno)\n\n### علامة تجارية\n\n**الاسم**\n\n`برونو` هو علامة تجارية تمتلكها [أنوب إم دي](https://www.helloanoop.com/)\n\n**الشعار**\n\nالشعار من [OpenMoji](https://openmoji.org/library/emoji-1F436/). الترخيص: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)\n\n### المساهمة 👩‍💻🧑‍💻\n\nيسعدني أنك تتطلع لتحسين برونو. يرجى الاطلاع على [دليل المساهمة](../../contributing.md)\n\nحتى إذا لم تكن قادرًا على التساهم بشكل مباشر من خلال الشيفرة، فلا تتردد في الإبلاغ عن الأخطاء وطلب الميزات التي يجب تنفيذها لحل حالتك.\n\n### الكتّاب\n\n<div align=\"center\">\n    <a href=\"https://github.com/usebruno/bruno/graphs/contributors\">\n        <img src=\"https://contrib.rocks/image?repo=usebruno/bruno\" />\n    </a>\n</div>\n\n### الرخصة 📄\n\n[MIT](../../license.md)\n"
  },
  {
    "path": "docs/readme/readme_bn.md",
    "content": "<br />\n<img src=\"../../assets/images/logo-transparent.png\" width=\"80\"/>\n\n### ব্রুনো - API অন্বেষণ এবং পরীক্ষা করার জন্য ওপেনসোর্স IDE।\n\n[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)\n[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)\n[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)\n[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)\n[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)\n[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)\n\n[English](../../readme.md)\n| [Українська](./readme_ua.md)\n| [Русский](./readme_ru.md)\n| [Türkçe](./readme_tr.md)\n| [Deutsch](./readme_de.md)\n| [Français](./readme_fr.md)\n| [Português (BR)](./readme_pt_br.md)\n| [한국어](./readme_kr.md)\n| **বাংলা**\n| [Español](./readme_es.md)\n| [Italiano](./readme_it.md)\n| [Română](./readme_ro.md)\n| [Polski](./readme_pl.md)\n| [简体中文](./readme_cn.md)\n| [正體中文](./readme_zhtw.md)\n| [العربية](./readme_ar.md)\n| [日本語](./readme_ja.md)\n| [ქართული](./readme_ka.md)\n\nব্রুনো হল একটি নতুন এবং উদ্ভাবনী API ক্লায়েন্ট, যার লক্ষ্য পোস্টম্যান এবং অনুরূপ সরঞ্জাম দ্বারা প্রতিনিধিত্ব করা স্থিতাবস্থায় বিপ্লব ঘটানো।\n\nব্রুনো আপনার সংগ্রহগুলি সরাসরি আপনার ফাইল সিস্টেমের একটি ফোল্ডারে সঞ্চয় করে। আমরা API অনুরোধ সম্পর্কে তথ্য সংরক্ষণ করতে একটি প্লেইন টেক্সট মার্কআপ ভাষা, ব্রু ব্যবহার করি।\n\nআপনি আপনার API সংগ্রহে সহযোগিতা করতে গিট বা আপনার পছন্দের যেকোনো সংস্করণ নিয়ন্ত্রণ ব্যবহার করতে পারেন।\n\nব্রুনো শুধুমাত্র অফলাইন। ব্রুনোতে ক্লাউড-সিঙ্ক যোগ করার কোন পরিকল্পনা নেই, কখনও। আমরা আপনার ডেটা গোপনীয়তার মূল্য দিই এবং বিশ্বাস করি এটি আপনার ডিভাইসে থাকা উচিত। আমাদের দীর্ঘমেয়াদী দৃষ্টি পড়ুন। [এখানে ](https://github.com/usebruno/bruno/discussions/269)\n\n📢 ইন্ডিয়া FOSS 3.0 সম্মেলনে আমাদের সাম্প্রতিক আলোচনা দেখুন [এখানে](https://www.youtube.com/watch?v=7bSMFpbcPiY)\n\n![bruno](/assets/images/landing-2.png) <br /><br />\n\n### স্থাপন\n\nব্রুনো বাইনারি ডাউনলোড হিসাবে উপলব্ধ [আমাদের ওয়েবসাইটে](https://www.usebruno.com/downloads) ম্যাক, উইন্ডোজ এবং লিনাক্সের জন্য।\n\nআপনি Homebrew, Chocolatey, Snap এবং Apt এর মত প্যাকেজ ম্যানেজারদের মাধ্যমে ব্রুনো ইনস্টল করতে পারেন।\n\n```sh\n# Homebrew এর মাধ্যমে Mac-এ\nbrew install bruno\n\n# চকোলেটির মাধ্যমে উইন্ডোজে\nchoco install bruno\n\n# স্ন্যাপ এর মাধ্যমে লিনাক্সে\nsnap install bruno\n\n# Apt এর মাধ্যমে লিনাক্সে\nsudo mkdir -p /etc/apt/keyrings\nsudo apt update && sudo apt install gpg curl\ncurl -fsSL \"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266\" \\\n  | gpg --dearmor \\\n  | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null\nsudo chmod 644 /etc/apt/keyrings/bruno.gpg\necho \"deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable\" \\\n  | sudo tee /etc/apt/sources.list.d/bruno.list\nsudo apt update && sudo apt install bruno\n```\n\n### একাধিক প্ল্যাটফর্মে চালান 🖥️\n\n![bruno](/assets/images/run-anywhere.png) <br /><br />\n\n### Git এর মাধ্যমে সহযোগিতা করুন 👩‍💻🧑‍💻\n\nঅথবা আপনার পছন্দের যেকোনো সংস্করণ নিয়ন্ত্রণ ব্যবস্থা\n\n![bruno](/assets/images/version-control.png) <br /><br />\n\n### গুরুত্বপূর্ণ লিংক 📌\n\n- [আমাদের দীর্ঘমেয়াদী দৃষ্টি](https://github.com/usebruno/bruno/discussions/269)\n- [রোডম্যাপ](https://github.com/usebruno/bruno/discussions/384)\n- [ডকুমেন্টেশন](https://docs.usebruno.com)\n- [ওয়েবসাইট](https://www.usebruno.com)\n- [মূল্য](https://www.usebruno.com/pricing)\n- [ডাউনলোড করুন](https://www.usebruno.com/downloads)\n\n### শোকেস 🎥\n\n- [প্রশংসাপত্র](https://github.com/usebruno/bruno/discussions/343)\n- [নলেজ হাব](https://github.com/usebruno/bruno/discussions/386)\n- [স্ক্রিপ্টম্যানিয়া](https://github.com/usebruno/bruno/discussions/385)\n\n### সমর্থন ❤️\n\nউফ ! আপনি যদি প্রকল্পটি পছন্দ করেন তবে ⭐ বোতামটি টিপুন !!\n\n### প্রশংসাপত্র শেয়ার করুন 📣\n\nযদি ব্রুনো আপনাকে কর্মক্ষেত্রে এবং আপনার দলগুলিতে সাহায্য করে থাকে, অনুগ্রহ করে আপনার [আমাদের গিটহাব আলোচনায় প্রশংসাপত্রগুলি](https://github.com/usebruno/bruno/discussions/343) শেয়ার করতে ভুলবেন না\n\n### নতুন প্যাকেজ পরিচালকদের কাছে প্রকাশ করা হচ্ছে\n\nআরও তথ্যের জন্য অনুগ্রহ করে [এখানে](../publishing/publishing_bn.md) দেখুন।\n\n### অবদান 👩‍💻🧑‍💻\n\nআমি খুশি যে আপনি ব্রুনোর উন্নতি করতে চাইছেন। অনুগ্রহ করে [অবদানকারী নির্দেশিকা](../contributing/contributing_bn.md) দেখুন\n\nআপনি কোডের মাধ্যমে অবদান রাখতে না পারলেও, অনুগ্রহ করে বাগ এবং বৈশিষ্ট্যের অনুরোধ ফাইল করতে দ্বিধা করবেন না যা আপনার ব্যবহারের ক্ষেত্রে সমাধান করার জন্য প্রয়োগ করা প্রয়োজন।\n\n### লেখক\n\n<div align=\"center\">\n    <a href=\"https://github.com/usebruno/bruno/graphs/contributors\">\n        <img src=\"https://contrib.rocks/image?repo=usebruno/bruno\" />\n    </a>\n</div>\n\n### সাথে থাকুন 🌐\n\n[𝕏 (টুইটার)](https://twitter.com/use_bruno) <br />\n[ওয়েবসাইট](https://www.usebruno.com) <br />\n[ডিসকর্ড](https://discord.com/invite/KgcZUncpjq) <br />\n[লিঙ্কডইন](https://www.linkedin.com/company/usebruno)\n\n### ট্রেডমার্ক\n\n**নাম**\n\n`Bruno` হল একটি ট্রেডমার্ক [Anoop M D](https://www.helloanoop.com/)\n\n**লোগো**\n\nলোগোটি [OpenMoji](https://openmoji.org/library/emoji-1F436/) থেকে নেওয়া হয়েছে। লাইসেন্স: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)\n\n### লাইসেন্স 📄\n\n[MIT](../../license.md)\n"
  },
  {
    "path": "docs/readme/readme_cn.md",
    "content": "<br />\n<img src=\"../../assets/images/logo-transparent.png\" width=\"80\"/>\n\n### Bruno - 开源 IDE，用于探索和测试 API。\n\n[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)\n[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)\n[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)\n[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)\n[![网站](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)\n[![下载](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)\n\n[English](../../readme.md)\n| [Українська](./readme_ua.md)\n| [Русский](./readme_ru.md)\n| [Türkçe](./readme_tr.md)\n| [Deutsch](./readme_de.md)\n| [Français](./readme_fr.md)\n| [Português (BR)](./readme_pt_br.md)\n| [한국어](./readme_kr.md)\n| [বাংলা](./readme_bn.md)\n| [Español](./readme_es.md)\n| [Italiano](./readme_it.md)\n| [Română](./readme_ro.md)\n| [Polski](./readme_pl.md)\n| **简体中文**\n| [正體中文](./readme_zhtw.md)\n| [العربية](./readme_ar.md)\n| [日本語](./readme_ja.md)\n| [ქართული](./readme_ka.md)\n\nBruno 是一款全新且创新的 API 客户端，旨在颠覆 Postman 和其他类似工具。\n\nBruno 直接在您的电脑文件夹中存储您的 API 信息。我们使用纯文本标记语言 Bru 来保存有关 API 的信息。\n\n您可以使用 Git 或您选择的任何版本控制系统来对您的 API 信息进行版本控制和协作。\n\nBruno 仅限离线使用。我们计划永不向 Bruno 添加云同步功能。我们重视您的数据隐私，并认为它应该留在您的设备上。阅读我们的长期愿景 [点击查看](https://github.com/usebruno/bruno/discussions/269)\n\n[下载 Bruno](https://www.usebruno.com/downloads)\n\n📢 观看我们在印度 FOSS 3.0 会议上的最新演讲 [点击查看](https://www.youtube.com/watch?v=7bSMFpbcPiY)\n\n![bruno](../../assets/images/landing-2.png) <br /><br />\n\n## 商业版本 ✨\n\n我们的大多数功能都是免费且开源的。\n我们致力于在 [开源与可持续性发展](https://github.com/usebruno/bruno/discussions/269) 之间取得和谐的平衡\n\n欢迎使用我们的 [付费版本](https://www.usebruno.com/pricing) ，看看附加的功能是否对您或团队有所帮助！ <br/>\n\n## 目录\n- [安装](#安装)\n- [特性](#特性)\n    - [跨平台使用 🖥️](#跨平台使用-)\n    - [通过Git协作 👩‍💻🧑‍💻](#通过git协作-)\n- [重要链接 📌](#重要链接-)\n- [展示 🎥](#展示-)\n- [分享评价 📣](#分享评价-)\n- [发布到新的包管理器](#发布到新的包管理器)\n- [联系方式 🌐](#联系方式-)\n- [商标](#商标)\n- [贡献 👩‍💻🧑‍💻](#贡献-)\n- [作者](#作者)\n- [许可证 📄](#许可证-)\n\n## 安装\n\nBruno 可以在我们的 [网站上下载](https://www.usebruno.com/downloads) 适用于Mac、Windows 和 Linux 的可执行文件。\n\n您也可以通过包管理器如 Homebrew、Chocolatey、Scoop、Snap 和 Apt 安装 Bruno。\n\n```sh\n# 在 Mac 电脑上用 Homebrew 安装\nbrew install bruno\n\n# 在 Windows 上用 Chocolatey 安装\nchoco install bruno\n\n# 在 Windows 上用 Scoop 安装\nscoop bucket add extras\nscoop install bruno\n\n# 在 Windows 上用 winget 安装\nwinget install Bruno.Bruno\n\n# 在 Linux 上用 Snap 安装\nsnap install bruno\n\n# 在 Linux 上用 Flatpak 安装\nflatpak install com.usebruno.Bruno\n\n# 在 Linux 上用 Apt 安装\nsudo mkdir -p /etc/apt/keyrings\nsudo apt update && sudo apt install gpg curl\ncurl -fsSL \"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266\" \\\n  | gpg --dearmor \\\n  | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null\nsudo chmod 644 /etc/apt/keyrings/bruno.gpg\necho \"deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable\" \\\n  | sudo tee /etc/apt/sources.list.d/bruno.list\nsudo apt update && sudo apt install bruno\n```\n\n## 特性\n\n### 跨平台使用 🖥️\n\n![bruno](../../assets/images/run-anywhere.png) <br /><br />\n\n### 通过Git协作 👩‍💻🧑‍💻\n\n或者任何您选择的版本控制系统\n\n![bruno](../../assets/images/version-control.png) <br /><br />\n\n## 重要链接 📌\n\n- [我们的愿景](https://github.com/usebruno/bruno/discussions/269)\n- [路线图](https://www.usebruno.com/roadmap)\n- [文档](https://docs.usebruno.com)\n- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)\n- [网站](https://www.usebruno.com)\n- [价格](https://www.usebruno.com/pricing)\n- [下载](https://www.usebruno.com/downloads)\n\n## 展示 🎥\n\n- [Testimonials](https://github.com/usebruno/bruno/discussions/343)\n- [Knowledge Hub](https://github.com/usebruno/bruno/discussions/386)\n- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)\n\n## 分享评价 📣\n\n如果 Bruno 在您的工作和团队中帮助了您，请不要忘记在我们的 GitHub 讨论上分享您的 [评价](https://github.com/usebruno/bruno/discussions/343)\n\n## 发布到新的包管理器\n\n如需了解更多信息，请参见 [此处](../publishing/publishing_cn.md) 。\n\n## 联系方式 🌐\n\n[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />\n[Website](https://www.usebruno.com) <br />\n[Discord](https://discord.com/invite/KgcZUncpjq) <br />\n[LinkedIn](https://www.linkedin.com/company/usebruno)\n\n## 商标\n\n**名称**\n\n`Bruno` 是由 [Anoop M D](https://www.helloanoop.com/) 持有的商标。\n\n**Logo**\n\nLogo 源自 [OpenMoji](https://openmoji.org/library/emoji-1F436/). License: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)\n\n## 贡献 👩‍💻🧑‍💻\n\n很高兴您希望改进 bruno。请查看 [贡献指南](../contributing/contributing_cn.md)。\n\n即使您无法通过代码做出贡献，我们仍然欢迎您提出 BUG 和新的功能需求。\n\n## 作者\n\n<div align=\"center\">\n    <a href=\"https://github.com/usebruno/bruno/graphs/contributors\">\n        <img src=\"https://contrib.rocks/image?repo=usebruno/bruno\" />\n    </a>\n</div>\n\n## 许可证 📄\n\n[MIT](../../license.md)\n"
  },
  {
    "path": "docs/readme/readme_de.md",
    "content": "<br />\n<img src=\"/assets/images/logo-transparent.png\" width=\"80\"/>\n\n### Bruno - Opensource IDE zum Erkunden und Testen von APIs.\n\n[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)\n[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)\n[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)\n[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)\n[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)\n[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)\n\n[English](../../readme.md)\n| [Українська](./readme_ua.md)\n| [Русский](./readme_ru.md)\n| [Türkçe](./readme_tr.md)\n| **Deutsch**\n| [Français](./readme_fr.md)\n| [Português (BR)](./readme_pt_br.md)\n| [한국어](./readme_kr.md)\n| [বাংলা](./readme_bn.md)\n| [Español](./readme_es.md)\n| [Italiano](./readme_it.md)\n| [Română](./readme_ro.md)\n| [Polski](./readme_pl.md)\n| [简体中文](./readme_cn.md)\n| [正體中文](./readme_zhtw.md)\n| [العربية](./readme_ar.md)\n| [日本語](./readme_ja.md)\n| [ქართული](./readme_ka.md)\n\nBruno ist ein neuer und innovativer API-Client, der den Status Quo von Postman und ähnlichen Tools revolutionieren soll.\n\nBruno speichert deine Sammlungen direkt in einem Ordner in deinem Dateisystem. Wir verwenden eine einfache Textauszeichnungssprache - Bru - um Informationen über API-Anfragen zu speichern.\n\nDu kannst Git oder eine andere Versionskontrolle deiner Wahl verwenden, um gemeinsam mit anderen an deinen API-Sammlungen zu arbeiten.\n\nBruno ist ein reines Offline-Tool. Es gibt keine Pläne, Bruno um eine Cloud-Synchronisation zu erweitern. Wir schätzen den Schutz deiner Daten und glauben, dass sie auf deinem Gerät bleiben sollten. Lies unsere Langzeit-Vision [hier](https://github.com/usebruno/bruno/discussions/269).\n\n[Download Bruno](https://www.usebruno.com/downloads)\n\n📢 Sieh Dir unseren Vortrag auf der India FOSS 3.0 Conference [hier](https://www.youtube.com/watch?v=7bSMFpbcPiY) an.\n\n![bruno](/assets/images/landing-2.png) <br /><br />\n\n### Installation\n\nBruno ist als Download [auf unserer Website](https://www.usebruno.com/downloads) für Mac, Windows und Linux verfügbar.\n\nDu kannst Bruno auch über Paketmanager wie Homebrew, Chocolatey, Scoop, Snap, Flatpak und Apt installieren.\n\n```sh\n# Auf Mac via Homebrew\nbrew install bruno\n\n# Auf Windows via Chocolatey\nchoco install bruno\n\n# Auf Windows via Scoop\nscoop bucket add extras\nscoop install bruno\n\n# Auf Windows via winget\nwinget install Bruno.Bruno\n\n# Auf Linux via Snap\nsnap install bruno\n\n# Auf Linux via Flatpak\nflatpak install com.usebruno.Bruno\n\n# Auf Linux via Apt\nsudo mkdir -p /etc/apt/keyrings\nsudo apt update && sudo apt install gpg curl\ncurl -fsSL \"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266\" \\\n  | gpg --dearmor \\\n  | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null\nsudo chmod 644 /etc/apt/keyrings/bruno.gpg\necho \"deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable\" \\\n  | sudo tee /etc/apt/sources.list.d/bruno.list\nsudo apt update && sudo apt install bruno\n```\n\n### Einsatz auf verschiedensten Plattformen 🖥️\n\n![bruno](/assets/images/run-anywhere.png) <br /><br />\n\n### Zusammenarbeit mit Git 👩‍💻🧑‍💻\n\nOder einer Versionskontrolle deiner Wahl\n\n![bruno](/assets/images/version-control.png) <br /><br />\n\n### Sponsoren\n\n#### Gold Sponsoren\n\n<img src=\"/assets/images/sponsors/samagata.png\" width=\"150\"/>\n\n#### Silber Sponsoren\n\n<img src=\"/assets/images/sponsors/commit-company.png\" width=\"70\"/>\n\n### Wichtige Links 📌\n\n- [Unsere Langzeit-Vision](https://github.com/usebruno/bruno/discussions/269)\n- [Roadmap](https://github.com/usebruno/bruno/discussions/384)\n- [Dokumentation](https://docs.usebruno.com)\n- [Webseite](https://www.usebruno.com)\n- [Preise](https://www.usebruno.com/pricing)\n- [Download](https://www.usebruno.com/downloads)\n\n### Showcase 🎥\n\n- [Erfahrungsberichte](https://github.com/usebruno/bruno/discussions/343)\n- [Wissenswertes](https://github.com/usebruno/bruno/discussions/386)\n- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)\n\n### Unterstützung ❤️\n\nWuff! Wenn du dieses Projekt magst, klick auf den ⭐ Button !!\n\n### Teile Erfahrungsberichte 📣\n\nWenn Bruno dir und in deinem Team bei der Arbeit geholfen hat, vergiss bitte nicht, deine [Erfahrungsberichte in unserer GitHub-Diskussion](https://github.com/usebruno/bruno/discussions/343) zu teilen.\n\n### Bereitstellung in neuen Paket-Managern\n\nMehr Informationen findest du [hier](../publishing/publishing_de.md).\n\n### Mitmachen 👩‍💻🧑‍💻\n\nIch freue mich, dass du Bruno verbessern willst. Bitte schau dir den [Leitfaden zum Mitmachen](../contributing/contributing_de.md) an.\n\nAuch wenn du nicht in der Lage bist, einen Beitrag in Form von Code zu leisten, zögere bitte nicht, uns Fehler und Funktionswünsche mitzuteilen, die implementiert werden müssen, um deinen Anwendungsfall zu unterstützen.\n\n### Autoren\n\n<div align=\"center\">\n    <a href=\"https://github.com/usebruno/bruno/graphs/contributors\">\n        <img src=\"https://contrib.rocks/image?repo=usebruno/bruno\" />\n    </a>\n</div>\n\n### In Verbindung bleiben 🌐\n\n[Twitter](https://twitter.com/use_bruno) <br />\n[Webseite](https://www.usebruno.com) <br />\n[Discord](https://discord.com/invite/KgcZUncpjq) <br />\n[LinkedIn](https://www.linkedin.com/company/usebruno)\n\n### Markenzeichen\n\n**Name**\n\n`Bruno` ist ein Markenzeichen von [Anoop M D](https://www.helloanoop.com/)\n\n**Logo**\n\nDas Logo stammt von [OpenMoji](https://openmoji.org/library/emoji-1F436/). Lizenz: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)\n\n### Lizenz 📄\n\n[MIT](../../license.md)\n"
  },
  {
    "path": "docs/readme/readme_es.md",
    "content": "<br />\n<img src=\"../../assets/images/logo-transparent.png\" width=\"80\"/>\n\n### Bruno - IDE de código abierto para explorar y probar APIs.\n\n[![Versión en Github](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)\n[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)\n[![Actividad de Commits](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)\n[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)\n[![Sitio Web](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)\n[![Descargas](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)\n\n[English](../../readme.md)\n| [Українська](./readme_ua.md)\n| [Русский](./readme_ru.md)\n| [Türkçe](./readme_tr.md)\n| [Deutsch](./readme_de.md)\n| [Français](./readme_fr.md)\n| [Português (BR)](./readme_pt_br.md)\n| [한국어](./readme_kr.md)\n| [বাংলা](./readme_bn.md)\n| **Español**\n| [Italiano](./readme_it.md)\n| [Română](./readme_ro.md)\n| [Polski](./readme_pl.md)\n| [简体中文](./readme_cn.md)\n| [正體中文](./readme_zhtw.md)\n| [العربية](./readme_ar.md)\n| [日本語](./readme_ja.md)\n| [ქართული](./readme_ka.md)\n\nBruno es un cliente de APIs nuevo e innovador, creado con el objetivo de revolucionar el panorama actual representado por Postman y otras herramientas similares.\n\nBruno almacena tus colecciones directamente en una carpeta de tu sistema de archivos. Usamos un lenguaje de marcado de texto plano, llamado Bru, para guardar información sobre las peticiones a tus APIs.\n\nPuedes usar git o cualquier otro sistema de control de versiones que prefieras para colaborar en tus colecciones.\n\nBruno funciona sin conexión a internet. No tenemos intenciones de añadir sincronización en la nube a Bruno, en ningún momento. Valoramos tu privacidad y creemos que tus datos deben permanecer en tu dispositivo. Puedes leer nuestra visión a largo plazo [aquí](https://github.com/usebruno/bruno/discussions/269).\n\n[Descarga Bruno](https://www.usebruno.com/downloads).\n\n📢 Mira nuestra charla en la conferencia India FOSS 3.0 [aquí](https://www.youtube.com/watch?v=7bSMFpbcPiY).\n\n![bruno](/assets/images/landing-2.png) <br /><br />\n\n### Instalación\n\nBruno está disponible para su descarga [en nuestro sitio web](https://www.usebruno.com/downloads) para Mac, Windows y Linux.\n\nTambién puedes instalar Bruno mediante package managers como Homebrew, Chocolatey, Scoop, Flatpak y Apt.\n\n```sh\n# En Mac con Homebrew\nbrew install bruno\n\n# En Windows con Chocolatey\nchoco install bruno\n\n# En Windows con Scoop\nscoop bucket add extras\nscoop install bruno\n\n# En Linux con Snap\nsnap install bruno\n\n# En Linux con Flatpak\nflatpak install com.usebruno.Bruno\n\n# En Linux con Apt\nsudo mkdir -p /etc/apt/keyrings\nsudo apt update && sudo apt install gpg curl\ncurl -fsSL \"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266\" \\\n  | gpg --dearmor \\\n  | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null\nsudo chmod 644 /etc/apt/keyrings/bruno.gpg\necho \"deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable\" \\\n  | sudo tee /etc/apt/sources.list.d/bruno.list\nsudo apt update && sudo apt install bruno\n```\n\n### Ejecútalo en múltiples plataformas 🖥️\n\n![bruno](/assets/images/run-anywhere.png) <br /><br />\n\n### Colabora vía Git 👩‍💻🧑‍💻\n\nO cualquier otro sistema de control de versiones que prefieras\n\n![bruno](/assets/images/version-control.png) <br /><br />\n\n### Enlaces importantes 📌\n\n- [Nuestra Visión a Largo Plazo](https://github.com/usebruno/bruno/discussions/269)\n- [Hoja de Ruta](https://github.com/usebruno/bruno/discussions/384)\n- [Documentación](https://docs.usebruno.com)\n- [Sitio Web](https://www.usebruno.com)\n- [Precios](https://www.usebruno.com/pricing)\n- [Descargas](https://www.usebruno.com/downloads)\n\n### Casos de uso 🎥\n\n- [Testimonios](https://github.com/usebruno/bruno/discussions/343)\n- [Centro de Conocimiento](https://github.com/usebruno/bruno/discussions/386)\n- [Scripts de la Comunidad](https://github.com/usebruno/bruno/discussions/385)\n\n### Apoya el proyecto ❤️\n\n¡Guau! Si te gusta el proyecto, ¡dale al botón de ⭐!\n\n### Comparte tus testimonios 📣\n\nSi Bruno te ha ayudado en tu trabajo y con tus equipos, por favor, no olvides compartir tus testimonios en [nuestras discusiones de GitHub](https://github.com/usebruno/bruno/discussions/343)\n\n### Publicar en nuevos gestores de paquetes\n\nPor favor, consulta [aquí](../../publishing.md) para más información.\n\n### Contribuye 👩‍💻🧑‍💻\n\nEstamos encantados de que quieras ayudar a mejorar Bruno. Por favor, consulta la [guía de contribución](../contributing/contributing_es.md) para más información.\n\nIncluso si no puedes contribuir con código, no dudes en reportar errores y solicitar nuevas funcionalidades que necesites para resolver tu caso de uso.\n\n### Colaboradores\n\n<div align=\"center\">\n    <a href=\"https://github.com/usebruno/bruno/graphs/contributors\">\n        <img src=\"https://contrib.rocks/image?repo=usebruno/bruno\" />\n    </a>\n</div>\n\n### Mantente en contacto 🌐\n\n[X](https://twitter.com/use_bruno) <br />\n[Sitio Web](https://www.usebruno.com) <br />\n[Discord](https://discord.com/invite/KgcZUncpjq) <br />\n[LinkedIn](https://www.linkedin.com/company/usebruno)\n\n### Marca\n\n**Nombre**\n\n`Bruno` es una marca propiedad de [Anoop M D](https://www.helloanoop.com/).\n\n**Logo**\n\nEl logo fue obtenido de [OpenMoji](https://openmoji.org/library/emoji-1F436/). Licencia: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)\n\n### Licencia 📄\n\n[MIT](../../license.md)\n"
  },
  {
    "path": "docs/readme/readme_fa.md",
    "content": "<br />\n<img src=\"../../assets/images/logo-transparent.png\" width=\"80\"/>\n\n### برونو یا Bruno - محیط توسعه متن باز برای تست و توسعه API ها\n\n[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)\n[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)\n[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)\n[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)\n[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)\n[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)\n\n[English](../../readme.md)\n| [Українська](./readme_ua.md)\n| [Русский](./readme_ru.md)\n| [Türkçe](./readme_tr.md)\n| [Deutsch](./readme_de.md)\n| [Français](./readme_fr.md)\n| [Português (BR)](./readme_pt_br.md)\n| [한국어](./readme_kr.md)\n| [বাংলা](./readme_bn.md)\n| [Español](./readme_es.md)\n| **فارسی**\n| [Română](./readme_ro.md)\n| [Polski](./readme_pl.md)\n| [简体中文](./readme_cn.md)\n| [正體中文](./readme_zhtw.md)\n| [العربية](./readme_ar.md)\n| [日本語](./readme_ja.md)\n| [ქართული](./readme_ka.md)\n\nبرونو یک کلاینت API جدید و نوآورانه است که هدفش تغییر وضعیت فعلی ابزارهایی مانند Postman و سایر ابزارهای مشابه است.\n\nبرونو مجموعه‌های شما را مستقیماً در یک پوشه روی فایل‌سیستم شما ذخیره می‌کند. ما از یک زبان نشانه‌گذاری ساده به نام Bru برای ذخیره اطلاعات درخواست‌های API استفاده می‌کنیم.\n\nشما می‌توانید برای همکاری روی مجموعه‌های API خود، از Git یا هر سیستم کنترل نسخه دلخواهتان استفاده کنید.\n\nبرونو فقط به صورت آفلاین کار می‌کند. هیچ برنامه‌ای برای اضافه کردن همگام‌سازی ابری به برونو در آینده وجود ندارد. ما به حریم خصوصی داده‌های شما اهمیت می‌دهیم و معتقدیم که باید روی دستگاه خودتان باقی بمانند. می‌توانید چشم‌انداز بلندمدت ما را مطالعه کنید. [اینجا (به انگلیسی)](https://github.com/usebruno/bruno/discussions/269)\n\n📢 جدیدترین ارائه ما را در کنفرانس India FOSS 3.0 تماشا کنید.\n[اینجا](https://www.youtube.com/watch?v=7bSMFpbcPiY)\n\n![bruno](/assets/images/landing-2.png) <br /><br />\n\n### نصب\n\nبرونو به صورت یک فایل باینری برای دانلود در دسترس است. [بر روی وبسایت ما](https://www.usebruno.com/downloads) برای مک لینکوس و ویندوز.\n\nهمچنین می‌توانید برونو را از طریق مدیر بسته‌هایی مانند Homebrew، Chocolatey، Snap و Apt نصب کنید.\n\n```sh\n# بر روی مک از طریق brew\nbrew install bruno\n\n# بر روی ویندوز از طریق Chocolatey\nchoco install bruno\n\n# بر روی لینوکس از طریق Snap\nsnap install bruno\n\n# بر روی لینوکس از طریق Apt\nsudo mkdir -p /etc/apt/keyrings\nsudo apt update && sudo apt install gpg curl\ncurl -fsSL \"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266\" \\\n  | gpg --dearmor \\\n  | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null\nsudo chmod 644 /etc/apt/keyrings/bruno.gpg\necho \"deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable\" \\\n  | sudo tee /etc/apt/sources.list.d/bruno.list\nsudo apt update && sudo apt install bruno\n```\n\n### روی پلتفرم‌های مختلف کار می‌کند 🖥️\n\n![bruno](/assets/images/run-anywhere.png) <br /><br />\n\n### همکاری از طریق گیت 👩‍💻🧑‍💻\n\nیا هر سیستم کنترل نسخه‌ای که ترجیح می‌دهید\n\n![bruno](/assets/images/version-control.png) <br /><br />\n\n### لینک‌های مهم 📌\n\n- [آخرین نسخه پایدار ما](https://github.com/usebruno/bruno/discussions/269)\n- [نقشه راه](https://github.com/usebruno/bruno/discussions/384)\n- [مستندات](https://docs.usebruno.com)\n- [وبسایت](https://www.usebruno.com)\n- [اشتراک ها](https://www.usebruno.com/pricing)\n- [دانلود](https://www.usebruno.com/downloads)\n\n### ویدیوها 🎥\n\n- [تجربه ها](https://github.com/usebruno/bruno/discussions/343)\n- [مرکز دانش](https://github.com/usebruno/bruno/discussions/386)\n- [اسکریپ مانیا](https://github.com/usebruno/bruno/discussions/385)\n\n### حمایت ❤️\n\nجوون! اگر این پروژه را دوست دارید، روی دکمه ⭐ کلیک کنید!\n\n### تجربه‌های به اشتراک گذاشته‌شده 📣\n\nاگر برونو به شما یا تیمتان کمک کرده است، لطفاً فراموش نکنید تجربه‌های خود را به اشتراک بگذارید. [تجربه‌های خود را در بحث گیت‌هاب ما به اشتراک بگذارید](https://github.com/usebruno/bruno/discussions/343).\n\n### انتشار برونو در یک پکیچ منیجر جدید\n\nلطفا چک بکنید [اینجارو](../../publishing.md) برای اطلاعات بیشتر.\n\n### مشارکت 👩‍💻🧑‍💻\n\nخوشحالم که می‌خواهید برونو را بهتر کنید. لطفا [راهنمای مشارکت را بررسی کنید](../contributing/contributing_fa.md).\n\nحتی اگر نمی‌توانید از طریق کدنویسی مشارکت کنید، در گزارش باگ‌ها و درخواست قابلیت‌های جدید که به حل نیازهای شما کمک می‌کند تردید نکنید.\n\n### نویسنده ها\n\n<div align=\"center\">\n    <a href=\"https://github.com/usebruno/bruno/graphs/contributors\">\n        <img src=\"https://contrib.rocks/image?repo=usebruno/bruno\" />\n    </a>\n</div>\n\n### در ارتباط باشید 🌐\n\n[𝕏 (تویتر)](https://twitter.com/use_bruno) <br />\n[وبسایت](https://www.usebruno.com) <br />\n[دیسکورد](https://discord.com/invite/KgcZUncpjq) <br />\n[لینکدین](https://www.linkedin.com/company/usebruno)\n\n### برند\n\n**نام**\n\nبه فارسی برونو - `Bruno` یک علامت تجاری ثبت‌شده متعلق به [Anoop M D](https://www.helloanoop.com/)\n\n**لوگو**\n\nلوگو توسط [OpenMoji](https://openmoji.org/library/emoji-1F436/) ساخته شده است. مجوز: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)\n\n### مجوز 📄\n\n[MIT](../../license.md)\n"
  },
  {
    "path": "docs/readme/readme_fr.md",
    "content": "<br />\n<img src=\"../../assets/images/logo-transparent.png\" width=\"80\"/>\n\n### Bruno - IDE Opensource pour explorer et tester des APIs.\n\n[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)\n[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)\n[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)\n[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)\n[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)\n[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)\n\n[English](../../readme.md)\n| [Українська](./readme_ua.md)\n| [Русский](./readme_ru.md)\n| [Türkçe](./readme_tr.md)\n| [Deutsch](./readme_de.md)\n| **Français**\n| [Português (BR)](./readme_pt_br.md)\n| [한국어](./readme_kr.md)\n| [বাংলা](./readme_bn.md)\n| [Español](./readme_es.md)\n| [Italiano](./readme_it.md)\n| [Română](./readme_ro.md)\n| [Polski](./readme_pl.md)\n| [简体中文](./readme_cn.md)\n| [正體中文](./readme_zhtw.md)\n| [العربية](./readme_ar.md)\n| [日本語](./readme_ja.md)\n| [ქართული](./readme_ka.md)\n\nBruno est un nouveau client API, innovant, qui a pour but de révolutionner le _statu quo_ que représentent Postman et les autres outils.\n\nBruno sauvegarde vos collections directement sur votre système de fichiers. Nous utilisons un langage de balise de type texte pour décrire les requêtes API.\n\nVous pouvez utiliser git ou tout autre gestionnaire de version pour travailler de manière collaborative sur vos collections d'APIs.\n\nBruno ne fonctionne qu'en mode déconnecté. Il n'y a pas d'abonnement ou de synchronisation avec le cloud Bruno, il n'y en aura jamais. Nous sommes conscients de la confidentialité de vos données et nous sommes convaincus qu'elles doivent rester sur vos appareils. Vous pouvez lire notre vision à long terme [ici (en anglais)](https://github.com/usebruno/bruno/discussions/269).\n\n📢 Regardez notre présentation récente lors de la conférence India FOSS 3.0 (en anglais) [ici](https://www.youtube.com/watch?v=7bSMFpbcPiY)\n\n![bruno](/assets/images/landing-2.png) <br /><br />\n\n### Installation\n\nBruno est disponible au téléchargement [sur notre site web](https://www.usebruno.com/downloads), pour Mac, Windows et Linux.\n\nVous pouvez aussi installer Bruno via un gestionnaire de paquets, comme Homebrew, Chocolatey, Scoop, Snap et Apt.\n\n```sh\n# Mac via Homebrew\nbrew install bruno\n\n# Windows via Chocolatey\nchoco install bruno\n\n# Windows via Scoop\nscoop bucket add extras\nscoop install bruno\n\n# Linux via Snap\nsnap install bruno\n\n# Linux via Apt\nsudo mkdir -p /etc/apt/keyrings\nsudo apt update && sudo apt install gpg curl\ncurl -fsSL \"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266\" \\\n  | gpg --dearmor \\\n  | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null\nsudo chmod 644 /etc/apt/keyrings/bruno.gpg\necho \"deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable\" \\\n  | sudo tee /etc/apt/sources.list.d/bruno.list\nsudo apt update && sudo apt install bruno\n```\n\n### Fonctionne sur de multiples plateformes 🖥️\n\n![bruno](/assets/images/run-anywhere.png) <br /><br />\n\n### Collaborer via Git 👩‍💻🧑‍💻\n\nOu n'importe quel système de gestion de sources\n\n![bruno](/assets/images/version-control.png) <br /><br />\n\n### Liens importants 📌\n\n- [Notre vision à long terme (en anglais)](https://github.com/usebruno/bruno/discussions/269)\n- [Roadmap](https://github.com/usebruno/bruno/discussions/384)\n- [Documentation](https://docs.usebruno.com)\n- [Site web](https://www.usebruno.com)\n- [Prix](https://www.usebruno.com/pricing)\n- [Téléchargement](https://www.usebruno.com/downloads)\n- [Sponsors GitHub](https://github.com/sponsors/helloanoop)\n\n### Showcase 🎥\n\n- [Témoignages](https://github.com/usebruno/bruno/discussions/343)\n- [Centre de connaissance](https://github.com/usebruno/bruno/discussions/386)\n- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)\n\n### Soutien ❤️\n\nSi vous aimez Bruno et que vous souhaitez soutenir le travail _opensource_, pensez à devenir un sponsor via la page [Github Sponsors](https://github.com/sponsors/helloanoop).\n\n### Partage de témoignages 📣\n\nSi Bruno vous a aidé dans votre travail, au sein de votre équipe, merci de penser à partager votre témoignage sur la [page discussion GitHub dédiée](https://github.com/usebruno/bruno/discussions/343)\n\n### Publier Bruno sur un nouveau gestionnaire de paquets\n\nVeuillez regarder [ici](../publishing/publishing_fr.md) pour plus d'information.\n\n### Contribuer 👩‍💻🧑‍💻\n\nJe suis heureux de voir que vous cherchez à améliorer Bruno. Merci de consulter le [guide de contribution](../contributing/contributing_fr.md)\n\nMême si vous n'êtes pas en mesure de contribuer directement via du code, n'hésitez pas à consigner les bogues et les demandes de nouvelles fonctionnalités pour résoudre vos cas d'usage !\n\n### Auteurs\n\n<div align=\"center\">\n    <a href=\"https://github.com/usebruno/bruno/graphs/contributors\">\n        <img src=\"https://contrib.rocks/image?repo=usebruno/bruno\" />\n    </a>\n</div>\n\n### Restons en contact 🌐\n\n[Twitter](https://twitter.com/use_bruno) <br />\n[Website](https://www.usebruno.com) <br />\n[Discord](https://discord.com/invite/KgcZUncpjq) <br />\n[LinkedIn](https://www.linkedin.com/company/usebruno)\n\n### Marque\n\n**Nom**\n\n`Bruno` est une marque appartenant à [Anoop M D](https://www.helloanoop.com/)\n\n**Logo**\n\nLe logo est issu de [OpenMoji](https://openmoji.org/library/emoji-1F436/).\nLicence : CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)\n\n### Licence 📄\n\n[MIT](../../license.md)\n"
  },
  {
    "path": "docs/readme/readme_hi.md",
    "content": "<br />\n<img src=\"../../assets/images/logo-transparent.png\" width=\"80\"/>\n\n### ब्रूनो - API इंटरफेस (API) का अन्वेषण और परीक्षण करने के लिए एक ओपन-सोर्स विकास वातावरण।\n\n[![GitHub संस्करण](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)\n[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)\n[![कमिट गतिविधि](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)\n[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)\n[![वेबसाइट](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)\n[![डाउनलोड](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)\n\n[English](../../readme.md)\n| [Українська](./readme_ua.md)\n| [Русский](./readme_ru.md)\n| [Türkçe](./readme_tr.md)\n| [Deutsch](./readme_de.md)\n| [Français](./readme_fr.md)\n| [Português (BR)](./readme_pt_br.md)\n| [한국어](./readme_kr.md)\n| [বাংলা](./readme_bn.md)\n| [Español](./readme_es.md)\n| [Italiano](./readme_it.md)\n| [Română](./readme_ro.md)\n| [Polski](./readme_pl.md)\n| [简体中文](./readme_cn.md)\n| [正體中文](./readme_zhtw.md)\n| [العربية](./readme_ar.md)\n| [日本語](./readme_ja.md)\n| [ქართული](./readme_ka.md)\n| **हिन्दी**\n\nब्रूनो एक नया और अभिनव API क्लाइंट है, जिसका उद्देश्य Postman और अन्य समान उपकरणों द्वारा प्रस्तुत स्थिति को बदलना है।\n\nब्रूनो आपकी कलेक्शनों को सीधे आपकी फाइल सिस्टम के एक फ़ोल्डर में संग्रहीत करता है। हम API अनुरोधों के बारे में जानकारी सहेजने के लिए एक सामान्य टेक्स्ट मार्कअप भाषा, Bru, का उपयोग करते हैं।\n\nआप अपनी API कलेक्शनों पर सहयोग करने के लिए Git या अपनी पसंद के किसी भी संस्करण नियंत्रण प्रणाली का उपयोग कर सकते हैं।\n\nब्रूनो केवल ऑफ़लाइन उपयोग के लिए है। ब्रूनो में कभी भी क्लाउड-सिंक जोड़ने की कोई योजना नहीं है। हम आपके डेटा की गोपनीयता को महत्व देते हैं और मानते हैं कि इसे आपके डिवाइस पर ही रहना चाहिए। हमारी दीर्घकालिक दृष्टि [यहाँ](https://github.com/usebruno/bruno/discussions/269) पढ़ें।\n\n📢 हमारे हालिया India FOSS 3.0 सम्मेलन में हमारे वार्तालाप को [यहाँ](https://www.youtube.com/watch?v=7bSMFpbcPiY) देखें।\n\n![bruno](/assets/images/landing-2.png) <br /><br />\n\n### गोल्डन संस्करण ✨\n\nहमारी अधिकांश सुविधाएँ मुफ्त और ओपन-सोर्स हैं।\nहम [पारदर्शिता और स्थिरता के सिद्धांतों](https://github.com/usebruno/bruno/discussions/269) के बीच एक सामंजस्यपूर्ण संतुलन प्राप्त करने का प्रयास करते हैं।\n\n[गोल्डन संस्करण](https://www.usebruno.com/pricing) के लिए खरीदारी जल्द ही $9 की कीमत पर उपलब्ध होगी! <br/>\n[यहाँ सदस्यता लें](https://usebruno.ck.page/4c65576bd4) ताकि आपको लॉन्च पर सूचनाएं मिलें।\n\n### स्थापना\n\nब्रूनो Mac, Windows और Linux के लिए हमारे [वेबसाइट](https://www.usebruno.com/downloads) पर एक बाइनरी डाउनलोड के रूप में उपलब्ध है।\n\nआप ब्रूनो को Homebrew, Chocolatey, Scoop, Snap, Flatpak और Apt जैसे पैकेज प्रबंधकों के माध्यम से भी स्थापित कर सकते हैं।\n\n```sh\n# Mac पर Homebrew के माध्यम से\nbrew install bruno\n\n# Windows पर Chocolatey के माध्यम से\nchoco install bruno\n\n# Windows पर Scoop के माध्यम से\nscoop bucket add extras\nscoop install bruno\n\n# Linux पर Snap के माध्यम से\nsnap install bruno\n\n# Linux पर Flatpak के माध्यम से\nflatpak install com.usebruno.Bruno\n\n# Linux पर Apt के माध्यम से\nsudo mkdir -p /etc/apt/keyrings\nsudo apt update && sudo apt install gpg curl\ncurl -fsSL \"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266\" \\\n  | gpg --dearmor \\\n  | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null\nsudo chmod 644 /etc/apt/keyrings/bruno.gpg\necho \"deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable\" \\\n  | sudo tee /etc/apt/sources.list.d/bruno.list\nsudo apt update && sudo apt install bruno\n\nकई प्लेटफार्मों पर चलाएं 🖥️\n<br /><br />\n\nGit के माध्यम से सहयोग करें 👩‍💻🧑‍💻\nया अपनी पसंद के किसी भी संस्करण नियंत्रण प्रणाली का उपयोग करें\n\n<br /><br />\n\nमहत्वपूर्ण लिंक 📌\nहमारी दीर्घकालिक दृष्टि\n\nरोडमैप\n\nप्रलेखन\n\nStack Overflow\n\nवेबसाइट\n\nमूल्य निर्धारण\n\nडाउनलोड\n\nGitHub प्रायोजक\n\nप्रस्तुतियाँ 🎥\nप्रशंसापत्र\n\nज्ञान केंद्र\n\nScriptmania\n\nसमर्थन ❤️\nयदि आप ब्रूनो को पसंद करते हैं और हमारे ओपन-सोर्स कार्य का समर्थन करना चाहते हैं, तो कृपया GitHub प्रायोजक के माध्यम से हमें प्रायोजित करने पर विचार करें।\n\nप्रशंसापत्र साझा करें 📣\nयदि ब्रूनो ने आपके और आपकी टीमों के लिए काम में मदद की है, तो कृपया हमारे GitHub चर्चा में अपने प्रशंसापत्र साझा करना न भूलें\n\nनए पैकेज प्रबंधकों में प्रकाशित करना\nअधिक जानकारी के लिए कृपया यहाँ देखें।\n\nहमसे संपर्क करें 🌐\n𝕏 (ट्विटर) <br />\nवेबसाइट <br />\nडिस्कॉर्ड <br />\nलिंक्डइन\n\nट्रेडमार्क\nनाम\n\nब्रूनो एक ट्रेडमार्क है जो अनूप एम डी के स्वामित्व में है।\n\nलोगो\n\nलोगो OpenMoji से लिया गया है। लाइसेंस: CC BY-SA 4.0\n\nयोगदान 👩‍💻🧑‍💻\nहमें खुशी है कि आप ब्रूनो को बेहतर बनाने में रुचि रखते हैं। कृपया योगदान गाइड देखें।\n\nयदि आप सीधे कोड के माध्यम से योगदान नहीं कर सकते, तो भी कृपया बग्स की रिपोर्ट करने और उन सुविधाओं का अनुरोध करने में संकोच न करें जिन्हें आपकी स्थिति को हल करने के लिए लागू किया जाना चाहिए।\n\nलेखक\n<div align=\"center\"> <a href=\"https://github.com/usebruno/bruno/graphs/contributors\"> <img src=\"https://contrib.rocks/image?repo=usebruno/bruno\" /> </a> </div>\n\nलाइसेंस 📄\nMIT\n"
  },
  {
    "path": "docs/readme/readme_it.md",
    "content": "<br />\n<img src=\"../../assets/images/logo-transparent.png\" width=\"80\"/>\n\n### Bruno - Opensource IDE per esplorare e testare gli APIs.\n\n[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)\n[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)\n[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)\n[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)\n[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)\n[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)\n\n[English](../../readme.md)\n| [Українська](./readme_ua.md)\n| [Русский](./readme_ru.md)\n| [Türkçe](./readme_tr.md)\n| [Deutsch](./readme_de.md)\n| [Français](./readme_fr.md)\n| [Português (BR)](./readme_pt_br.md)\n| [한국어](./readme_kr.md)\n| [বাংলা](./readme_bn.md)\n| [Español](./readme_es.md)\n| **Italiano**\n| [Română](./readme_ro.md)\n| [Polski](./readme_pl.md)\n| [简体中文](./readme_cn.md)\n| [正體中文](./readme_zhtw.md)\n| [العربية](./readme_ar.md)\n| [日本語](./readme_ja.md)\n| [ქართული](./readme_ka.md)\n\nBruno è un nuovo ed innovativo API client, mirato a rivoluzionare lo status quo rappresentato da Postman e strumenti simili disponibili.\n\nBruno memorizza le tue raccolte direttamente in una cartella del tuo filesystem. Utilizziamo un linguaggio di markup in testo semplice chiamato Bru per salvare informazioni sulle richeste API.\n\nPuoi utilizzare Git o qualsiasi sistema di controllo che preferisci per collaborare sulle tue raccolte di API.\n\nBruno funziona solo in modalità offline. Non ci sono piani per aggiungere la sincronizzazione su cloud a Bruno in futuro. Valorizziamo la privacy dei tuoi dati e crediamo che dovrebbero rimanere sul tuo dispositivo. Puoi leggere la nostra visione a lungo termine [qui (in inglese)](https://github.com/usebruno/bruno/discussions/269)\n\n📢 Guarda la nostra presentazione più recente alla conferenza India FOSS 3.0 [qui](https://www.youtube.com/watch?v=7bSMFpbcPiY)\n\n![bruno](/assets/images/landing-2.png) <br /><br />\n\n### Installazione\n\nBruno è disponibile come download binario [sul nostro sito](https://www.usebruno.com/downloads) per Mac, Windows e Linux.\n\nPuoi installare Bruno anche tramite package manger come Homebrew, Chocolatey, Snap e Apt.\n\n```sh\n# Su Mac come Homebrew\nbrew install bruno\n\n# Su Windows come Chocolatey\nchoco install bruno\n\n# Su Linux tramite Snap\nsnap install bruno\n\n# Su Linux tramite Apt\nsudo mkdir -p /etc/apt/keyrings\nsudo apt update && sudo apt install gpg curl\ncurl -fsSL \"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266\" \\\n  | gpg --dearmor \\\n  | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null\nsudo chmod 644 /etc/apt/keyrings/bruno.gpg\necho \"deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable\" \\\n  | sudo tee /etc/apt/sources.list.d/bruno.list\nsudo apt update && sudo apt install bruno\n```\n\n### Funziona su diverse piattaforme 🖥️\n\n![bruno](/assets/images/run-anywhere.png) <br /><br />\n\n### Collabora tramite Git 👩‍💻🧑‍💻\n\nO con qualsiasi sistema di controllo di versioni a tua scelta\n\n![bruno](/assets/images/version-control.png) <br /><br />\n\n### Collegamenti importanti 📌\n\n- [La nostra visione a lungo termine](https://github.com/usebruno/bruno/discussions/269)\n- [Roadmap](https://github.com/usebruno/bruno/discussions/384)\n- [Documentazione](https://docs.usebruno.com)\n- [Sito internet](https://www.usebruno.com)\n- [Prezzo](https://www.usebruno.com/pricing)\n- [Download](https://www.usebruno.com/downloads)\n\n### Showcase 🎥\n\n- [Testimonianze](https://github.com/usebruno/bruno/discussions/343)\n- [Centro di conoscenza](https://github.com/usebruno/bruno/discussions/386)\n- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)\n\n### Supporto ❤️\n\nWoof! se ti piace il progetto, premi quel ⭐ pulsante !!\n\n### Testimonianze condivise 📣\n\nSe Bruno ti ha aiutato con il tuo lavoro ed il tuo team, per favore non dimenticare di condividere le tue [testimonianze nella nostra discussione su GitHub](https://github.com/usebruno/bruno/discussions/343)\n\n### Pubblica Bruno su un nuovo gestore di pacchetti\n\nPer favore vedi [qui](../../publishing.md) per accedere a più informazioni.\n\n### Contribuire 👩‍💻🧑‍💻\n\nSono felice che vuoi migliorare Bruno. Per favore controlla la [guida per la partecipazione](../contributing/contributing_it.md)\n\nAnche se non sei in grado di contribuire tramite il codice, non esitare a segnalare bug e richieste di funzionalità che devono essere implementati per risolvere il tuo caso d'uso.\n\n### Autori\n\n<div align=\"center\">\n    <a href=\"https://github.com/usebruno/bruno/graphs/contributors\">\n        <img src=\"https://contrib.rocks/image?repo=usebruno/bruno\" />\n    </a>\n</div>\n\n### Resta in contatto 🌐\n\n[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />\n[Sito internet](https://www.usebruno.com) <br />\n[Discord](https://discord.com/invite/KgcZUncpjq) <br />\n[LinkedIn](https://www.linkedin.com/company/usebruno)\n\n### Marchio\n\n**Nome**\n\n`Bruno` è un marchio registrato appartenente a [Anoop M D](https://www.helloanoop.com/)\n\n**Logo**\n\nIl logo è stato creato da [OpenMoji](https://openmoji.org/library/emoji-1F436/). Licenza: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)\n\n### Licenza 📄\n\n[MIT](../../license.md)\n"
  },
  {
    "path": "docs/readme/readme_ja.md",
    "content": "<br />\n<img src=\"../../assets/images/logo-transparent.png\" width=\"80\"/>\n\n### Bruno - API の検証・動作テストのためのオープンソース IDE.\n\n[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)\n[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)\n[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)\n[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)\n[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)\n[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)\n\n[English](../../readme.md)\n| [Українська](./readme_ua.md)\n| [Русский](./readme_ru.md)\n| [Türkçe](./readme_tr.md)\n| [Deutsch](./readme_de.md)\n| [Français](./readme_fr.md)\n| [Português (BR)](./readme_pt_br.md)\n| [한국어](./readme_kr.md)\n| [বাংলা](./readme_bn.md)\n| [Español](./readme_es.md)\n| [Italiano](./readme_it.md)\n| [Română](./readme_ro.md)\n| [Polski](./readme_pl.md)\n| [简体中文](./readme_cn.md)\n| [正體中文](./readme_zhtw.md)\n| [العربية](./readme_ar.md)\n| **日本語**\n| [ქართული](./readme_ka.md)\n\nBruno は革新的な API クライアントです。Postman を代表する API クライアントツールの現状に一石を投じることを目指しています。\n\nBruno はローカルフォルダに直接コレクションを保存します。API リクエストの情報を保存するために Bru というプレーンテキストのマークアップ言語を採用しています。\n\nGit や任意のバージョン管理システムを使って API コレクションを共同開発することもできます。\n\nBruno はオフラインのみで利用できます。Bruno にクラウド同期機能を追加する予定はありません。私たちはデータプライバシーを尊重しており、データはローカルに保存されるべきだと考えています。私たちの長期的なビジョンは[こちら](https://github.com/usebruno/bruno/discussions/269)をご覧ください。\n\n[Bruno をダウンロード](https://www.usebruno.com/downloads)\n\n📢 India FOSS 3.0 Conference での発表の様子は[こちら](https://www.youtube.com/watch?v=7bSMFpbcPiY)から\n\n![bruno](/assets/images/landing-2.png) <br /><br />\n\n### インストール方法\n\nBruno は[私たちのウェブサイト](https://www.usebruno.com/downloads)からバイナリをダウンロードできます。Mac, Windows, Linux に対応しています。\n\nHomebrew, Chocolatey, Scoop, Snap, Flatpak, Apt などのパッケージマネージャからもインストール可能です。\n\n```sh\n# MacでHomebrewを使ってインストール\nbrew install bruno\n\n# WindowsでChocolateyを使ってインストール\nchoco install bruno\n\n# WindowsでScoopを使ってインストール\nscoop bucket add extras\nscoop install bruno\n\n# Windowsでwingetを使ってインストール\nwinget install Bruno.Bruno\n\n# LinuxでSnapを使ってインストール\nsnap install bruno\n\n# LinuxでFlatpakを使ってインストール\nflatpak install com.usebruno.Bruno\n\n# LinuxでAptを使ってインストール\nsudo mkdir -p /etc/apt/keyrings\nsudo apt update && sudo apt install gpg curl\ncurl -fsSL \"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266\" \\\n  | gpg --dearmor \\\n  | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null\nsudo chmod 644 /etc/apt/keyrings/bruno.gpg\necho \"deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable\" \\\n  | sudo tee /etc/apt/sources.list.d/bruno.list\nsudo apt update && sudo apt install bruno\n```\n\n### マルチプラットフォームでの実行に対応 🖥️\n\n![bruno](/assets/images/run-anywhere.png) <br /><br />\n\n### Git との連携が可能 👩‍💻🧑‍💻\n\nまたは任意のバージョン管理システムにも対応しています。\n\n![bruno](/assets/images/version-control.png) <br /><br />\n\n### スポンサー\n\n#### ゴールドスポンサー\n\n<img src=\"../../assets/images/sponsors/samagata.png\" width=\"150\"/>\n\n#### シルバースポンサー\n\n<img src=\"../../assets/images/sponsors/commit-company.png\" width=\"70\"/>\n\n#### ブロンズスポンサー\n\n<a href=\"https://zuplo.link/bruno\">\n    <img src=\"../../assets/images/sponsors/zuplo.png\" width=\"120\"/>\n</a>\n\n### 主要リンク 📌\n\n- [私たちの長期ビジョン](https://github.com/usebruno/bruno/discussions/269)\n- [ロードマップ](https://github.com/usebruno/bruno/discussions/384)\n- [ドキュメント](https://docs.usebruno.com)\n- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)\n- [ウェブサイト](https://www.usebruno.com)\n- [料金設定](https://www.usebruno.com/pricing)\n- [ダウンロード](https://www.usebruno.com/downloads)\n- [Github スポンサー](https://github.com/sponsors/helloanoop).\n\n### Showcase 🎥\n\n- [体験談](https://github.com/usebruno/bruno/discussions/343)\n- [ナレッジベース](https://github.com/usebruno/bruno/discussions/386)\n- [スクリプト集](https://github.com/usebruno/bruno/discussions/385)\n\n### サポート ❤️\n\nもし Bruno を気に入っていただいて、オープンソースの活動を支援していただけるなら、[Github Sponsors](https://github.com/sponsors/helloanoop)でスポンサーになることを考えてみてください。\n\n### 体験談のシェア 📣\n\nBruno が職場やチームで役立っているのであれば、[GitHub discussion 上であなたの体験談](https://github.com/usebruno/bruno/discussions/343)をシェアしていただくようお願いします。\n\n### 新しいパッケージマネージャへの公開\n\n詳しくは[こちら](../publishing/publishing_ja.md)をご覧ください。\n\n### 連絡先 🌐\n\n[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />\n[Website](https://www.usebruno.com) <br />\n[Discord](https://discord.com/invite/KgcZUncpjq) <br />\n[LinkedIn](https://www.linkedin.com/company/usebruno)\n\n### 商標について\n\n**名前**\n\n`Bruno`は[Anoop M D](https://www.helloanoop.com/)は取得している商標です。\n\n**ロゴ**\n\nロゴの出典は[OpenMoji](https://openmoji.org/library/emoji-1F436/)です。CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)でライセンスされています。\n\n### 貢献するには 👩‍💻🧑‍💻\n\nBruno を改善していただけるのは歓迎です。[コントリビュートガイド](../contributing/contributing_ja.md)をご覧ください。\n\nもしコードによる貢献ができない場合でも、あなたのユースケースを解決するために遠慮なくバグ報告や機能リクエストを出してください。\n\n### 開発者\n\n<div align=\"center\">\n    <a href=\"https://github.com/usebruno/bruno/graphs/contributors\">\n        <img src=\"https://contrib.rocks/image?repo=usebruno/bruno\" />\n    </a>\n</div>\n\n### ライセンス 📄\n\n[MIT](../../license.md)\n"
  },
  {
    "path": "docs/readme/readme_ka.md",
    "content": "<br />\n<img src=\"../../assets/images/logo-transparent.png\" width=\"80\"/>\n\n### ბრუნო - ღია წყაროების IDE API-ების შესწავლისა და ტესტირებისათვის.\n\n[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)\n[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)\n[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)\n[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)\n[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)\n[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)\n\n[English](../../readme.md)\n| [Українська](./readme_ua.md)\n| [Русский](./readme_ru.md)\n| [Türkçe](./readme_tr.md)\n| [Deutsch](./readme_de.md)\n| [Français](./readme_fr.md)\n| [Português (BR)](./readme_pt_br.md)\n| [한국어](./readme_kr.md)\n| [বাংলা](./readme_bn.md)\n| [Español](./readme_es.md)\n| [Italiano](./readme_it.md)\n| [Română](./readme_ro.md)\n| [Polski](./readme_pl.md)\n| [简体中文](./readme_cn.md)\n| [正體中文](./readme_zhtw.md)\n| [العربية](./readme_ar.md)\n| [日本語](./readme_ja.md)\n| **ქართული**\n\nბრუნო არის ახალი და ინოვაციური API კლიენტი, რომელიც მიზნად ისახავს პოსტმანისა და მსგავსი ინსტრუმენტების არსებული მდგომარეობის რევოლუციას.\n\nბრუნო თქვენი კოლექციების შენახვას უშუალოდ თქვენს ფაილური სისტემის ერთ-ერთ საქაღალოში ახდენს. ჩვენ ვხმარობთ უბრალო ტექსტურ მარკაპ ენის, Bru-ს, API მოთხოვნების შესახებ ინფორმაციის შენახვისთვის.\n\nთქვენ შეგიძლიათ გამოიყენოთ Git ან ნებისმიერი ვერსიის კონტროლის სისტემა თქვენი API კოლექციების გასაზიარებლად.\n\nბრუნო მხოლოდ ოფლაინ რეჟიმში მუშაობს. ბრუნოში ღრუბლური სინქრონიზაციის დამატების გეგმები არ არის. ჩვენ ვაფასებთ თქვენი მონაცემების პრივატობას და creemos, რომ ისინი თქვენს მოწყობილობაში უნდა დარჩეს. წაიკითხეთ ჩვენი გრძელვადიანი ხედვა [აქ](https://github.com/usebruno/bruno/discussions/269)\n\n[დამატებით ბრუნო](https://www.usebruno.com/downloads)\n\n📢 შეიტყვეთ ჩვენი უახლესი საუბრის შესახებ India FOSS 3.0 კონფერენციაზე [აქ](https://www.youtube.com/watch?v=7bSMFpbcPiY)\n\n![bruno](../../assets/images/landing-2.png) <br /><br />\n\n### ინსტალაცია\n\nბრუნო ხელმისაწვდომია როგორც ბინარული ჩამოტვირთვა [ჩვენ的网站上](https://www.usebruno.com/downloads) Mac-ის, Windows-ისა და Linux-ისთვის.\n\nთქვენ ასევე შეგიძლიათ დააინსტალიროთ ბრუნო პაკეტის მენეჯერების საშუალებით, როგორიცაა Homebrew, Chocolatey, Scoop, Snap, Flatpak და Apt.\n\n```sh\n# Mac-ზე Homebrew-ს საშუალებით\nbrew install bruno\n\n# Windows-ზე Chocolatey-ს საშუალებით\nchoco install bruno\n\n# Windows-ზე Scoop-ის საშუალებით\nscoop bucket add extras\nscoop install bruno\n\n# Windows-ზე winget-ის საშუალებით\nwinget install Bruno.Bruno\n\n# Linux-ზე Snap-ის საშუალებით\nsnap install bruno\n\n# Linux-ზე Flatpak-ის საშუალებით\nflatpak install com.usebruno.Bruno\n\n# Linux-ზე Apt-ის საშუალებით\nsudo mkdir -p /etc/apt/keyrings\nsudo apt update && sudo apt install gpg curl\ncurl -fsSL \"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266\" \\\n  | gpg --dearmor \\\n  | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null\nsudo chmod 644 /etc/apt/keyrings/bruno.gpg\necho \"deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable\" \\\n  | sudo tee /etc/apt/sources.list.d/bruno.list\nsudo apt update && sudo apt install bruno\n```\n\n### პლატფორმებს შორის მუშაობა 🖥️\n\n![bruno](../../assets/images/run-anywhere.png) <br /><br />\n\n### თანამშრომლობა Git-ის საშუალებით 👩‍💻🧑‍💻\n\nან ნებისმიერი ვერსიის კონტროლის სისტემის საშუალებით\n\n![bruno](../../assets/images/version-control.png) <br /><br />\n\n### სპონსორები\n\n#### ოქროს სპონსორები\n\n<img src=\"../../assets/images/sponsors/samagata.png\" width=\"150\"/>\n\n#### ვერცხლის სპონსორები\n\n<img src=\"../../assets/images/sponsors/commit-company.png\" width=\"70\"/>\n\n#### ბრინჯის სპონსორები\n\n<a href=\"https://zuplo.link/bruno\">\n    <img src=\"../../assets/images/sponsors/zuplo.png\" width=\"120\"/>\n</a>\n\n### მნიშვნელოვანი ბმულები 📌\n\n- [ჩვენი გრძელვადიანი ხედვა](https://github.com/usebruno/bruno/discussions/269)\n- [გეგმა](https://github.com/usebruno/bruno/discussions/384)\n- [დოკუმენტაცია](https://docs.usebruno.com)\n- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)\n- [ვებსაიტი](https://www.usebruno.com)\n- [ფასები](https://www.usebruno.com/pricing)\n- [დამატება](https://www.usebruno.com/downloads)\n- [GitHub სპონსორები](https://github.com/sponsors/helloanoop).\n\n### ვიტრინა 🎥\n\n- [მოწონებები](https://github.com/usebruno/bruno/discussions/343)\n- [მეცნიერების ჰაბი](https://github.com/usebruno/bruno/discussions/386)\n- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)\n\n### მხარდაჭერა ❤️\n\nთუ გიყვართ ბრუნო და გინდათ მხარი დაუჭიროთ ჩვენს ღია წყაროების მუშაობას, გაითვალისწინეთ ჩვენი დახმარება [GitHub სპონსორების საშუალებით](https://github.com/sponsors/helloanoop).\n\n### გააზიარეთ მოწმობები 📣\n\nთუ ბრუნო დაგეხმარათ თქვენს სამუშაოში და გუნდებში, გთხოვთ, არ დაგავიწყდეთ ჩვენი [მოწონებების გაზიარება ჩვენს GitHub განხილვაში](https://github.com/usebruno/bruno/discussions/343)\n\n### ახალი პაკეტის მენეჯერებში გამოქვეყნება\n\nიხილეთ [აქ](../../publishing.md) მეტი ინფორმაციისათვის.\n\n### დაინტერესდით 🌐\n\n[𝕎 (Twitter)](https://twitter.com/use_bruno) <br />\n[ვებსაიტი](https://www.usebruno.com) <br />\n[Discord](https://discord.com/invite/KgcZUncpjq) <br />\n[LinkedIn](https://www.linkedin.com/company/usebruno)\n\n### სავაჭრო ნიშანი\n\n**სახელი**\n\n`ბრუნო` არის სავაჭრო ნიშანი, რომელსაც ფლობს [ანუპ მ. დ.](https://www.helloanoop.com/)\n\n**ლოგო**\n\nლოგო არის [OpenMoji](https://openmoji.org/library/emoji-1F436/) სურათებიდან. ლიცენზია: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)\n\n### თანამშრომლობა 👩‍💻🧑‍💻\n\nმიხარია, რომ დაინტერესებული ხართ ბრუნოს გაუმჯობესებით. გთხოვთ, გადახედეთ [თანამშრომლობის სახელმძღვანელოს](../../contributing.md)\n\nთუნდაც ვერ მოახერხოთ კოდის საშუალებით კონტრიბუცია, ნუ ინანებთ პრობლემების და ფუნქციის მოთხოვნების ჩაწერას, რომლებიც უნდა განხორციელდეს თქვენი შემთხვევის გადასაჭრელად.\n\n### ავტორები\n\n<div align=\"center\">\n    <a href=\"https://github.com/usebruno/bruno/graphs/contributors\">\n        <img src=\"https://contrib.rocks/image?repo=usebruno/bruno\" />\n    </a>\n</div>\n\n### ლიცენზია 📄\n\n[MIT](../../license.md)"
  },
  {
    "path": "docs/readme/readme_kr.md",
    "content": "<br />\n<img src=\"../../assets/images/logo-transparent.png\" width=\"80\"/>\n\n### Bruno - API 탐색 및 테스트를 위한 오픈소스 IDE.\n\n[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)\n[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)\n[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)\n[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)\n[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)\n[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)\n\n[English](../../readme.md)\n| [Українська](./readme_ua.md)\n| [Русский](./readme_ru.md)\n| [Türkçe](./readme_tr.md)\n| [Deutsch](./readme_de.md)\n| [Français](./readme_fr.md)\n| [Português (BR)](./readme_pt_br.md)\n| **한국어**\n| [বাংলা](./readme_bn.md)\n| [Español](./readme_es.md)\n| [Italiano](./readme_it.md)\n| [Română](./readme_ro.md)\n| [Polski](./readme_pl.md)\n| [简体中文](./readme_cn.md)\n| [正體中文](./readme_zhtw.md)\n| [العربية](./readme_ar.md)\n| [日本語](./readme_ja.md)\n| [ქართული](./readme_ka.md)\n\nBruno는 새롭고 혁신적인 API 클라이언트로, Postman과 유사한 툴들을 혁신하는 것을 목표로 합니다.\n\nBruno는 사용자의 컬렉션을 파일 시스템의 폴더에 직접 저장합니다. 일반 텍스트 마크업 언어인 Bru를 사용해 API 요청에 대한 정보를 저장합니다.\n\nGit 또는 원하는 버전 관리 도구를 사용하여 API 컬렉션을 연동할 수 있습니다.\n\n브루는 오프라인 전용입니다. 브루노에 클라우드 동기화 기능을 추가할 계획은 없습니다. 저희는 사용자의 데이터 프라이버시를 소중히 여기며, 데이터는 사용자의 기기에 남아 있어야 한다고 믿습니다. 장기 비전 읽기 [링크](https://github.com/usebruno/bruno/discussions/269)\n\n📢 Watch our recent talk at India FOSS 3.0 Conference [here](https://www.youtube.com/watch?v=7bSMFpbcPiY)\n\n![bruno](/assets/images/landing-2.png) <br /><br />\n\n### 설치\n\nBruno 는 여기에서 다운로드 받을 수 있습니다.[링크](https://www.usebruno.com/downloads) (맥, 윈도우, 리눅스)\n\nHomebrew, Chocolatey, Snap, Apt 같은 패키지 관리자를 통해서도 Bruno를 설치할 수 있습니다.\n\n```sh\n# On Mac via Homebrew\nbrew install bruno\n\n# On Windows via Chocolatey\nchoco install bruno\n\n# On Linux via Snap\nsnap install bruno\n\n# On Linux via Apt\nsudo mkdir -p /etc/apt/keyrings\nsudo apt update && sudo apt install gpg curl\ncurl -fsSL \"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266\" \\\n  | gpg --dearmor \\\n  | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null\nsudo chmod 644 /etc/apt/keyrings/bruno.gpg\necho \"deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable\" \\\n  | sudo tee /etc/apt/sources.list.d/bruno.list\nsudo apt update && sudo apt install bruno\n```\n\n### 여러 플랫폼에서 실행하세요. 🖥️\n\n![bruno](/assets/images/run-anywhere.png) <br /><br />\n\n### Git과 연동하세요. 👩‍💻🧑‍💻\n\n또는 원하는 버전 관리 시스템을 선택하세요.\n\n![bruno](/assets/images/version-control.png) <br /><br />\n\n### 중요 링크 📌\n\n- [Our Long Term Vision](https://github.com/usebruno/bruno/discussions/269)\n- [Roadmap](https://github.com/usebruno/bruno/discussions/384)\n- [Documentation](https://docs.usebruno.com)\n- [Website](https://www.usebruno.com)\n- [Pricing](https://www.usebruno.com/pricing)\n- [Download](https://www.usebruno.com/downloads)\n\n### 쇼케이스 🎥\n\n- [Testimonials](https://github.com/usebruno/bruno/discussions/343)\n- [Knowledge Hub](https://github.com/usebruno/bruno/discussions/386)\n- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)\n\n### 지원 ❤️\n\n프로젝트가 마음에 들면 ⭐ 버튼을 눌러 주세요.\n\n### 후기 공유 📣\n\nBruno가 여러분과 여러분의 팀에 도움이 되었다면, 잊지 말고 공유해 주세요. [GitHub discussion 공유 링크](https://github.com/usebruno/bruno/discussions/343)\n\n### 새 패키지 관리자에게 게시\n\n더 많은 정보를 확인하시려면 링크를 클릭해 주세요. [배포 가이드](../../publishing.md)\n\n### 컨트리뷰트 👩‍💻🧑‍💻\n\n컨트리뷰트에 관심이 있으시면 링크를 참고해 주세요. [컨트리뷰트 가이드](../contributing/contributing_kr.md)\n\n코드를 통해 기여할 수 없더라도 사용 사례를 해결하기 위해 구현이 필요한 버그나 기능 요청을 주저하지 마시고 제출해 주세요.\n\n### Authors\n\n<div align=\"center\">\n    <a href=\"https://github.com/usebruno/bruno/graphs/contributors\">\n        <img src=\"https://contrib.rocks/image?repo=usebruno/bruno\" />\n    </a>\n</div>\n\n### Stay in touch 🌐\n\n[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />\n[Website](https://www.usebruno.com) <br />\n[Discord](https://discord.com/invite/KgcZUncpjq) <br />\n[LinkedIn](https://www.linkedin.com/company/usebruno)\n\n### Trademark\n\n**Name**\n\n`Bruno` is a trademark held by [Anoop M D](https://www.helloanoop.com/)\n\n**Logo**\n\nThe logo is sourced from [OpenMoji](https://openmoji.org/library/emoji-1F436/). License: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)\n\n### License 📄\n\n[MIT](../../license.md)\n"
  },
  {
    "path": "docs/readme/readme_nl.md",
    "content": "<br />\n<img src=\"../../assets/images/logo-transparent.png\" width=\"80\"/>\n\n### Bruno - Open source IDE voor het verkennen en testen van API's.\n\n[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)\n[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)\n[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)\n[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)\n[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)\n[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)\n\n[English](../../readme.md) | [Українська](docs/readme/readme_ua.md) | [Русский](docs/readme/readme_ru.md) | [Türkçe](docs/readme/readme_tr.md) | [Deutsch](docs/readme/readme_de.md) | ** Nederlands ** | [Français](docs/readme/readme_fr.md) | [Português (BR)](docs/readme/readme_pt_br.md) | [한국어](docs/readme/readme_kr.md) | [বাংলা](docs/readme/readme_bn.md) | [Español](docs/readme/readme_es.md) | [Italiano](docs/readme/readme_it.md) | [Română](docs/readme/readme_ro.md) | [Polski](docs/readme/readme_pl.md) | [简体中文](docs/readme/readme_cn.md) | [正體中文](docs/readme/readme_zhtw.md) | [العربية](docs/readme/readme_ar.md) | [日本語](docs/readme/readme_ja.md)\n\nBruno is een nieuwe en innovatieve API-client, gericht op het revolutioneren van de status quo die wordt vertegenwoordigd door Postman en vergelijkbare tools.\n\nBruno slaat je collecties direct op in een map op je bestandssysteem. We gebruiken een platte tekst opmaaktaal, Bru, om informatie over API-verzoeken op te slaan.\n\nJe kunt Git of elke versiebeheertool naar keuze gebruiken om samen te werken aan je API-collecties.\n\nBruno is uitsluitend offline. Er zijn geen plannen om ooit cloud-synchronisatie aan Bruno toe te voegen. We waarderen je gegevensprivacy en geloven dat deze op je apparaat moet blijven. Lees onze langetermijnvisie [hier](https://github.com/usebruno/bruno/discussions/269)\n\n[Download Bruno](https://www.usebruno.com/downloads)\n\n📢 Bekijk onze recente presentatie op de India FOSS 3.0 Conference [hier](https://www.youtube.com/watch?v=7bSMFpbcPiY)\n\n![bruno](/assets/images/landing-2.png) <br /><br />\n\n### Installatie\n\nBruno is beschikbaar als binaire download [op onze website](https://www.usebruno.com/downloads) voor Mac, Windows en Linux.\n\nJe kunt Bruno ook installeren via pakketbeheerders zoals Homebrew, Chocolatey, Scoop, Snap, Flatpak en Apt.\n\n```sh\n# Op Mac via Homebrew\nbrew install bruno\n\n# Op Windows via Chocolatey\nchoco install bruno\n\n# Op Windows via Scoop\nscoop bucket add extras\nscoop install bruno\n\n# Op Windows via winget\nwinget install Bruno.Bruno\n\n# Op Linux via Snap\nsnap install bruno\n\n# Op Linux via Flatpak\nflatpak install com.usebruno.Bruno\n\n# Op Linux via Apt\nsudo mkdir -p /etc/apt/keyrings\nsudo apt update && sudo apt install gpg curl\ncurl -fsSL \"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266\" \\\n  | gpg --dearmor \\\n  | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null\nsudo chmod 644 /etc/apt/keyrings/bruno.gpg\necho \"deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable\" \\\n  | sudo tee /etc/apt/sources.list.d/bruno.list\nsudo apt update && sudo apt install bruno\n```\n\n### Draai op meerdere platformen 🖥️\n\n![bruno](/assets/images/run-anywhere.png) <br /><br />\n\n### Samenwerken via Git 👩‍💻🧑‍💻\n\nOf elk versiebeheersysteem naar keuze\n\n![bruno](/assets/images/version-control.png) <br /><br />\n\n### Sponsors\n\n#### Gouden Sponsors\n\n<img src=\"../../assets/images/sponsors/samagata.png\" width=\"150\"/>\n\n#### Zilveren Sponsors\n\n<img src=\"../../assets/images/sponsors/commit-company.png\" width=\"70\"/>\n\n#### Bronzen Sponsors\n\n<a href=\"https://zuplo.link/bruno\">\n    <img src=\"../../assets/images/sponsors/zuplo.png\" width=\"120\"/>\n</a>\n\n### Belangrijke Links 📌\n\n- [Onze Langetermijnvisie](https://github.com/usebruno/bruno/discussions/269)\n- [Roadmap](https://github.com/usebruno/bruno/discussions/384)\n- [Documentatie](https://docs.usebruno.com)\n- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)\n- [Website](https://www.usebruno.com)\n- [Prijzen](https://www.usebruno.com/pricing)\n- [Download](https://www.usebruno.com/downloads)\n- [GitHub Sponsors](https://github.com/sponsors/helloanoop)\n\n### Showcase 🎥\n\n- [Getuigenissen](https://github.com/usebruno/bruno/discussions/343)\n- [Kenniscentrum](https://github.com/usebruno/bruno/discussions/386)\n- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)\n\n### Ondersteuning ❤️\n\nAls je Bruno leuk vindt en ons open-source werk wilt ondersteunen, overweeg dan om ons te sponsoren via [GitHub Sponsors](https://github.com/sponsors/helloanoop).\n\n### Deel Getuigenissen 📣\n\nAls Bruno je heeft geholpen op je werk en in je teams, deel dan je [getuigenissen op onze GitHub-discussie](https://github.com/usebruno/bruno/discussions/343).\n\n\n### Blijf in contact 🌐\n\n[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />\n[Website](https://www.usebruno.com) <br />\n[Discord](https://discord.com/invite/KgcZUncpjq) <br />\n[LinkedIn](https://www.linkedin.com/company/usebruno)\n\n### Handelsmerk\n\n**Naam**\n\n`Bruno` is een handelsmerk in bezit van [Anoop M D](https://www.helloanoop.com/).\n\n**Logo**\n\nHet logo is afkomstig van [OpenMoji](https://openmoji.org/library/emoji-1F436/). Licentie: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)\n\n### Bijdragen 👩‍💻🧑‍💻\n\nIk ben blij dat je Bruno wilt verbeteren. Bekijk de [bijdragegids](contributing.md).\n\nZelfs als je geen bijdragen via code kunt leveren, aarzel dan niet om bugs en functieverzoeken in te dienen die moeten worden geïmplementeerd om jouw gebruiksscenario op te lossen.\n\n### Auteurs\n\n<div align=\"center\">\n    <a href=\"https://github.com/usebruno/bruno/graphs/contributors\">\n        <img src=\"https://contrib.rocks/image?repo=usebruno/bruno\" />\n    </a>\n</div>\n\n### Licentie 📄\n\n[MIT](../../license.md)"
  },
  {
    "path": "docs/readme/readme_pl.md",
    "content": "<br />\n<img src=\"../../assets/images/logo-transparent.png\" width=\"80\"/>\n\n### Bruno - Otwartoźródłowe IDE do eksploracji i testów APIs.\n\n[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)\n[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)\n[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)\n[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)\n[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)\n[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)\n\n[English](../../readme.md)\n| [Українська](./readme_ua.md)\n| [Русский](./readme_ru.md)\n| [Türkçe](./readme_tr.md)\n| [Deutsch](./readme_de.md)\n| [Français](./readme_fr.md)\n| [Português (BR)](./readme_pt_br.md)\n| [한국어](./readme_kr.md)\n| [বাংলা](./readme_bn.md)\n| [Español](./readme_es.md)\n| [Italiano](./readme_it.md)\n| [Română](./readme_ro.md)\n| **Polski**\n| [简体中文](./readme_cn.md)\n| [正體中文](./readme_zhtw.md)\n| [العربية](./readme_ar.md)\n| [日本語](./readme_ja.md)\n| [ქართული](./readme_ka.md)\n\nBruno to nowy i innowacyjny klient API, którego celem jest zrewolucjonizowanie status quo reprezentowanego przez narzędzia takie jak Postman.\n\nBruno przechowuje twoje kolekcje bezpośrednio w folderze na twoim systemie plików. Używamy prostego języka znaczników, Bru, do zapisywania informacji o żądaniach API.\n\nMożesz użyć Git lub dowolnego systemu kontroli wersji do współpracy nad swoimi kolekcjami API.\n\nBruno działa tylko w trybie offline. Nie planujemy nigdy dodawać synchronizacji w chmurze do Bruno. Cenimy prywatność Twoich danych i wierzymy, że powinny one pozostać na Twoim urządzeniu. Przeczytaj naszą długoterminową wizję [tutaj](https://github.com/usebruno/bruno/discussions/269)\n\n📢 Obejrzyj naszą ostatnią rozmowę na konferencji India FOSS 3.0 [tutaj](https://www.youtube.com/watch?v=7bSMFpbcPiY)\n\n![bruno](/assets/images/landing-2.png) <br /><br />\n\n### Instalacja\n\nBruno jest dostępny jako plik binarny do pobrania [na naszej stronie internetowej](https://www.usebruno.com/downloads) dla Mac, Windows i Linux.\n\nMożesz również zainstalować Bruno za pomocą menedżerów pakietów, takich jak Homebrew, Chocolatey, Scoop, Snap i Apt.\n\n```sh\n# On Mac via Homebrew\nbrew install bruno\n\n# On Windows via Chocolatey\nchoco install bruno\n\n# On Windows via Scoop\nscoop bucket add extras\nscoop install bruno\n\n# On Windows via winget\nwinget install Bruno.Bruno\n\n# On Linux via Snap\nsnap install bruno\n\n# On Linux via Flatpak\nflatpak install com.usebruno.Bruno\n\n# On Linux via Apt\nsudo mkdir -p /etc/apt/keyrings\nsudo apt update && sudo apt install gpg curl\ncurl -fsSL \"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266\" \\\n  | gpg --dearmor \\\n  | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null\nsudo chmod 644 /etc/apt/keyrings/bruno.gpg\necho \"deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable\" \\\n  | sudo tee /etc/apt/sources.list.d/bruno.list\nsudo apt update && sudo apt install bruno\n```\n\n### Uruchom na wielu platformach 🖥️\n\n![bruno](/assets/images/run-anywhere.png) <br /><br />\n\n### Współpracuj przez Git 👩‍💻🧑‍💻\n\nLub dowolny inny system kontroli wersji, który wybierzesz\n\n![bruno](/assets/images/version-control.png) <br /><br />\n\n### Ważne Linki 📌\n\n- [Nasza Długoterminowa Wizja](https://github.com/usebruno/bruno/discussions/269)\n- [Mapa Drogi](https://github.com/usebruno/bruno/discussions/384)\n- [Dokumentacja](https://docs.usebruno.com)\n- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)\n- [Strona Internetowa](https://www.usebruno.com)\n- [Cennik](https://www.usebruno.com/pricing)\n- [Pobieranie](https://www.usebruno.com/downloads)\n- [Sponsorzy GitHub](https://github.com/sponsors/helloanoop).\n\n### Zobacz 🎥\n\n- [Opinie](https://github.com/usebruno/bruno/discussions/343)\n- [Centrum Wiedzy](https://github.com/usebruno/bruno/discussions/386)\n- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)\n\n### Wsparcie ❤️\n\nJeśli podoba Ci się Bruno i chcesz wspierać naszą pracę opensource, rozważ sponsorowanie nas przez [Sponsorzy GitHub](https://github.com/sponsors/helloanoop).\n\n### Udostępnij Opinie 📣\n\nJeśli Bruno pomógł w pracy Tobie i Twoim zespołom, nie zapomnij podzielić się swoimi [opiniami na naszej dyskusji GitHub](https://github.com/usebruno/bruno/discussions/343)\n\n### Publikowanie w Nowych Menedżerach Pakietów\n\nWięcej informacji znajdziesz [tutaj](../publishing/publishing_pl.md).\n\n### Współpraca 👩‍💻🧑‍💻\n\nCieszymy się, że chcesz udoskonalić bruno. Proszę sprawdź [przewodnik współpracy](../contributing/contributing_pl.md)\n\nNawet jeśli nie jesteś w stanie przyczynić się poprzez kod, nie wahaj się zgłaszać błędów i wniosków o funkcje, które muszą zostać zaimplementowane, aby rozwiązać Twój przypadek użycia.\n\n### Autorzy\n\n<div align=\"center\">\n    <a href=\"https://github.com/usebruno/bruno/graphs/contributors\">\n        <img src=\"https://contrib.rocks/image?repo=usebruno/bruno\" />\n    </a>\n</div>\n\n### Pozostań w kontakcie 🌐\n\n[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />\n[Strona Internetowa](https://www.usebruno.com) <br />\n[Discord](https://discord.com/invite/KgcZUncpjq) <br />\n[LinkedIn](https://www.linkedin.com/company/usebruno)\n\n### Znak Towarowy\n\n**Nazwa**\n\n`Bruno` jest znakiem towarowym należącym do [Anoop M D](https://www.helloanoop.com/)\n\n**Logo**\n\nLogo pochodzi z [OpenMoji](https://openmoji.org/library/emoji-1F436/). Licencja: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)\n\n### Licencja 📄\n\n[MIT](../../license.md)\n"
  },
  {
    "path": "docs/readme/readme_pt_br.md",
    "content": "<br />\n<img src=\"../../assets/images/logo-transparent.png\" width=\"80\"/>\n\n### Bruno - IDE de código aberto para explorar e testar APIs.\n\n[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)\n[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)\n[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)\n[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)\n[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)\n[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)\n\n[English](../../readme.md)\n| [Українська](./readme_ua.md)\n| [Русский](./readme_ru.md)\n| [Türkçe](./readme_tr.md)\n| [Deutsch](./readme_de.md)\n| [Français](./readme_fr.md)\n| **Português (BR)**\n| [한국어](./readme_kr.md)\n| [বাংলা](./readme_bn.md)\n| [Español](./readme_es.md)\n| [Italiano](./readme_it.md)\n| [Română](./readme_ro.md)\n| [Polski](./readme_pl.md)\n| [简体中文](./readme_cn.md)\n| [正體中文](./readme_zhtw.md)\n| [العربية](./readme_ar.md)\n| [日本語](./readme_ja.md)\n| [ქართული](./readme_ka.md)\n\nBruno é um novo e inovador cliente de API, com o objetivo de revolucionar o status quo representado por ferramentas como o Postman e outras semelhantes.\n\nBruno armazena suas coleções diretamente em uma pasta no seu sistema de arquivos. Utilizamos uma linguagem de marcação de texto simples, chamada Bru, para salvar informações sobre requisições de API.\n\nVocê pode usar o Git ou qualquer sistema de controle de versão de sua escolha para colaborar em suas coleções de API.\n\nBruno é totalmente offline. Não há planos de adicionar sincronização em nuvem ao Bruno, nunca. Valorizamos a privacidade de seus dados e acreditamos que eles devem permanecer em seu dispositivo. Saiba mais sobre nossa visão a longo prazo [aqui](https://github.com/usebruno/bruno/discussions/269).\n\n📢 Assista à nossa palestra recente na India FOSS 3.0 Conference [aqui](https://www.youtube.com/watch?v=7bSMFpbcPiY).\n\n![bruno](../../assets/images/landing-2.png) <br /><br />\n\n### Instalação\n\nBruno está disponível para download como binário [em nosso site](https://www.usebruno.com/downloads) para Mac, Windows e Linux.\n\nVocê também pode instalar o Bruno via gerenciadores de pacotes como Homebrew, Chocolatey, Snap e Apt.\n\n```sh\n# No Mac via Homebrew\nbrew install bruno\n\n# No Windows via Chocolatey\nchoco install bruno\n\n# No Windows via Scoop\nscoop bucket add extras\nscoop install bruno\n\n# No Windows via winget\nwinget install Bruno.Bruno\n\n# No Linux via Snap\nsnap install bruno\n\n# No Linux via Flatpak\nflatpak install com.usebruno.Bruno\n\n# No Linux via Apt\nsudo mkdir -p /etc/apt/keyrings\nsudo apt update && sudo apt install gpg curl\ncurl -fsSL \"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266\" \\\n  | gpg --dearmor \\\n  | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null\nsudo chmod 644 /etc/apt/keyrings/bruno.gpg\necho \"deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable\" \\\n  | sudo tee /etc/apt/sources.list.d/bruno.list\nsudo apt update && sudo apt install bruno\n```\n\n### Execute em várias plataformas 🖥️\n\n![bruno](../../assets/images/run-anywhere.png) <br /><br />\n\n### Colaboração via Git 👩‍💻🧑‍💻\n\nOu qualquer sistema de controle de versão de sua escolha.\n\n![bruno](../../assets/images/version-control.png) <br /><br />\n\n### Apoiadores\n\n#### Apoiadores Gold\n\n<img src=\"../../assets/images/sponsors/samagata.png\" width=\"150\"/>\n\n#### Apoiadores Silver\n\n<img src=\"../../assets/images/sponsors/commit-company.png\" width=\"70\"/>\n\n#### Apoiadores Bronze\n\n<a href=\"https://zuplo.link/bruno\">\n    <img src=\"../../assets/images/sponsors/zuplo.png\" width=\"120\"/>\n</a>\n\n### Links Importantes 📌\n\n- [Nossa Visão de Longo Prazo](https://github.com/usebruno/bruno/discussions/269)\n- [Roadmap](https://github.com/usebruno/bruno/discussions/384)\n- [Documentação](https://docs.usebruno.com)\n- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)\n- [Website](https://www.usebruno.com)\n- [Preços](https://www.usebruno.com/pricing)\n- [Download](https://www.usebruno.com/downloads)\n- [GitHub Sponsors](https://github.com/sponsors/helloanoop)\n\n### Showcase 🎥\n\n- [Depoimentos](https://github.com/usebruno/bruno/discussions/343)\n- [Hub de Conhecimento](https://github.com/usebruno/bruno/discussions/386)\n- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)\n\n### Apoie ❤️\n\nAu-au! Se você gosta do projeto e deseja apoiar nosso trabalho, considere nos ajudando via [GitHub Sponsors](https://github.com/sponsors/helloanoop).\n\n### Compartilhe sua experiência 📣\n\nSe o Bruno ajudou no seu trabalho e/ou no trabalho de sua equipe, por favor, não se esqueça de compartilhar seu [depoimento em nossas discussões no GitHub](https://github.com/usebruno/bruno/discussions/343).\n\n### Publicando em Novos Gerenciadores de Pacotes\n\nPor favor, verifique [aqui](../publishing/publishing_pt_br.md) mais informações.\n\n### Mantenha Contato 🌐\n\n[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />\n[Website](https://www.usebruno.com) <br />\n[Discord](https://discord.com/invite/KgcZUncpjq) <br />\n[LinkedIn](https://www.linkedin.com/company/usebruno)\n\n### Trademark\n\n**Nome**\n\n`Bruno` é uma marca registrada de [Anoop M D](https://www.helloanoop.com/).\n\n**Logo**\n\nA logo é original do [OpenMoji](https://openmoji.org/library/emoji-1F436/). Licença: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/).\n\n### Colabore 👩‍💻🧑‍💻\n\nFico feliz que você queira melhorar o Bruno. Por favor, confira o [guia de colaboração](../contributing/contributing_pt_br.md).\n\nMesmo que você não possa contribuir codificando, não deixe de relatar problemas e solicitar recursos que precisam ser implementados para atender ao contexto de seu dia a dia.\n\n### Contribuidores\n\n<div align=\"center\">\n    <a href=\"https://github.com/usebruno/bruno/graphs/contributors\">\n        <img src=\"https://contrib.rocks/image?repo=usebruno/bruno\" />\n    </a>\n</div>\n\n### Licença 📄\n\n[MIT](../../license.md)\n"
  },
  {
    "path": "docs/readme/readme_ro.md",
    "content": "<br />\n<img src=\"../../assets/images/logo-transparent.png\" width=\"80\"/>\n\n### Bruno - Mediu integrat de dezvoltare cu sursă deschisă pentru explorarea și testarea API-urilor.\n\n[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)\n[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)\n[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)\n[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)\n[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)\n[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)\n\n[English](../../readme.md)\n| [Українська](./readme_ua.md)\n| [Русский](./readme_ru.md)\n| [Türkçe](./readme_tr.md)\n| [Deutsch](./readme_de.md)\n| [Français](./readme_fr.md)\n| [Português (BR)](./readme_pt_br.md)\n| [한국어](./readme_kr.md)\n| [বাংলা](./readme_bn.md)\n| [Español](./readme_es.md)\n| [Italiano](./readme_it.md)\n| **Română**\n| [Polski](./readme_pl.md)\n| [简体中文](./readme_cn.md)\n| [正體中文](./readme_zhtw.md)\n| [العربية](./readme_ar.md)\n| [日本語](./readme_ja.md)\n| [ქართული](./readme_ka.md)\n\nBruno este un client API nou și inovativ, care vizează să revoluționeze status quo-ul reprezentat de Postman și alte instrumente similare.\n\nBruno salvează colecțiile voastre direct într-o mapă din sistemul dvs. de fișiere. Folosim un limbaj de marcare cu text simplu, Bru, pentru a salva informații despre cererile API.\n\nPuteți folosi Git sau orice altă unealtă de control al versiunii la alegere pentru a colabora la colecțiile API voastre.\n\nBruno este numai offline. Nu va exista niciodată vreun plan pentru a adăuga sincronizarea cloud la Bruno. Noi valorăm confidențialitatea datelor voastre și credem că ar trebui să rămână pe dispozitivul vostru. Citiți viziunea noastră pe termen lung [aici](https://github.com/usebruno/bruno/discussions/269)\n\n📢 Priviți prezentarea noastră recentă de la India FOSS 3.0 Conference [aici](https://www.youtube.com/watch?v=7bSMFpbcPiY)\n\n![bruno](/assets/images/landing-2.png) <br /><br />\n\n### Instalarea\n\nBruno este disponibil ca descărcare binară [pe website-ul nostru](https://www.usebruno.com/downloads) pentru Mac, Windows și Linux.\n\nDe asemenea, puteţi instala Bruno cu un gestionar de pachete precum Homebrew, Chocolatey, Snap şi Apt.\n\n```sh\n# Pe Mac cu Homebrew\nbrew install bruno\n\n# Pe Windows cu Chocolatey\nchoco install bruno\n\n# Pe Linux cu Snap\nsnap install bruno\n\n# Pe Linux cu Apt\nsudo mkdir -p /etc/apt/keyrings\nsudo apt update && sudo apt install gpg curl\ncurl -fsSL \"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266\" \\\n  | gpg --dearmor \\\n  | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null\nsudo chmod 644 /etc/apt/keyrings/bruno.gpg\necho \"deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable\" \\\n  | sudo tee /etc/apt/sources.list.d/bruno.list\nsudo apt update && sudo apt install bruno\n```\n\n### Utilizați pe mai multe platforme 🖥️\n\n![bruno](/assets/images/run-anywhere.png) <br /><br />\n\n### Colaborați cu Git 👩‍💻🧑‍💻\n\nSau orice unealtă de control al versiunii la alegere\n\n![bruno](/assets/images/version-control.png) <br /><br />\n\n### Linkuri importante 📌\n\n- [Viziunea noastră pe termen lung](https://github.com/usebruno/bruno/discussions/269)\n- [Roadmap](https://github.com/usebruno/bruno/discussions/384)\n- [Documentație](https://docs.usebruno.com)\n- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)\n- [Website](https://www.usebruno.com)\n- [Prețuri](https://www.usebruno.com/pricing)\n- [Descărcări](https://www.usebruno.com/downloads)\n- [Sponsori GitHub](https://github.com/sponsors/helloanoop).\n\n### Vitrina 🎥\n\n- [Recenzii](https://github.com/usebruno/bruno/discussions/343)\n- [Centrul de cunoștințe](https://github.com/usebruno/bruno/discussions/386)\n- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)\n\n### Sprijiniți ❤️\n\nDacă vă place Bruno și doriți să sprijiniți munca noastră de sursă deschisă, puteți considera să ne sponsorizați [pe GitHub](https://github.com/sponsors/helloanoop).\n\n### Distribuiți recenziile 📣\n\nDacă Bruno va ajutat la locul de muncă și la echipele dvs., vă rugăm să nu uitați să distribuiți [recenziile în discuția noastră GitHub](https://github.com/usebruno/bruno/discussions/343)\n\n### Publicarea la gestionari de pachete noi\n\nVă rugăm să citiţi [aici](../publishing/publishing_ro.md) pentru mai multă informaţie.\n\n### Contribuiți 👩‍💻🧑‍💻\n\nMă bucur că doriți să îmbunătățiți Bruno. Vă rugăm să consultați [ghidul pentru contribuire](../contributing/contributing_ro.md)\n\nChiar dacă nu puteți face contribuții prin cod, vă rugăm să nu ezitați să raportați erori și să solicitați funcții care trebuie implementate pentru a rezolva cazul dvs. de utilizare.\n\n### Autori\n\n<div align=\"center\">\n    <a href=\"https://github.com/usebruno/bruno/graphs/contributors\">\n        <img src=\"https://contrib.rocks/image?repo=usebruno/bruno\" />\n    </a>\n</div>\n\n### Păstrați legătura 🌐\n\n[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />\n[Website](https://www.usebruno.com) <br />\n[Discord](https://discord.com/invite/KgcZUncpjq) <br />\n[LinkedIn](https://www.linkedin.com/company/usebruno)\n\n### Marcă comercială\n\n**Nume**\n\n`Bruno` este o marcă deținută de [Anoop M D](https://www.helloanoop.com/)\n\n**Logo**\n\nLogo-ul provine de la [OpenMoji](https://openmoji.org/library/emoji-1F436/). Licența: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)\n\n### Licența 📄\n\n[MIT](../../license.md)\n"
  },
  {
    "path": "docs/readme/readme_ru.md",
    "content": "<br />\n<img src=\"../../assets/images/logo-transparent.png\" width=\"80\"/>\n\n### Bruno - IDE с открытым исходным кодом для изучения и тестирования API.\n\n[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)\n[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)\n[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)\n[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)\n[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)\n[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)\n\n[English](../../readme.md)\n| [Українська](./readme_ua.md)\n| **Русский**\n| [Türkçe](./readme_tr.md)\n| [Deutsch](./readme_de.md)\n| [Français](./readme_fr.md)\n| [Português (BR)](./readme_pt_br.md)\n| [한국어](./readme_kr.md)\n| [বাংলা](./readme_bn.md)\n| [Español](./readme_es.md)\n| [Italiano](./readme_it.md)\n| [Română](./readme_ro.md)\n| [Polski](./readme_pl.md)\n| [简体中文](./readme_cn.md)\n| [正體中文](./readme_zhtw.md)\n| [العربية](./readme_ar.md)\n| [日本語](./readme_ja.md)\n| [ქართული](./readme_ka.md)\n\nBruno - новый и инновационный клиент API, направленный на революцию в установившейся ситуации, представленной Postman и подобными инструментами.\n\nBruno хранит ваши коллекции непосредственно в папке в вашей файловой системе. Для сохранения информации об API-запросах мы используем язык Bru.\n\nДля совместной работы над коллекциями API можно использовать git или любой другой контроль версий по вашему выбору.\n\nBruno работает только в автономном режиме. Добавление облачной синхронизации в Bruno не планируется. Мы ценим конфиденциальность ваших данных и считаем, что они должны оставаться на вашем устройстве. Ознакомьтесь с нашим долгосрочным видением [здесь](https://github.com/usebruno/bruno/discussions/269)\n\n![bruno](/assets/images/landing-2.png) <br /><br />\n\n### Работа на нескольких платформах 🖥️\n\n![bruno](/assets/images/run-anywhere.png) <br /><br />\n\n### Совместная работа через Git 👩‍💻🧑‍💻\n\nИли другая система контроля версий по вашему выбору\n\n![bruno](/assets/images/version-control.png) <br /><br />\n\n### Важные ссылки 📌\n\n- [Наше долгосрочное видение](https://github.com/usebruno/bruno/discussions/269)\n- [Roadmap](https://github.com/usebruno/bruno/discussions/384)\n- [Документация](https://docs.usebruno.com)\n- [Сайт](https://www.usebruno.com)\n- [Скачать Bruno](https://www.usebruno.com/downloads)\n\n### Витрина 🎥\n\n- [Отзывы](https://github.com/usebruno/bruno/discussions/343)\n- [Центр знаний](https://github.com/usebruno/bruno/discussions/386)\n- [Скриптомания](https://github.com/usebruno/bruno/discussions/385)\n\n### Поддержка ❤️\n\nГав! Если вам нравится проект, нажмите на звездочку ⭐ !!!\n\n### Поделись отзывами 📣\n\nЕсли Бруно помог вам в работе и в ваших командах, пожалуйста, не забудьте поделиться своим [отзывом на нашем обсуждении в github](https://github.com/usebruno/bruno/discussions/343)\n\n### Внести вклад 👩‍💻🧑‍💻\n\nЯ рад, что Вы хотите улучшить Бруно. Пожалуйста, ознакомьтесь с [этим гайдом](../contributing/contributing_ru.md)\n\nДаже если вы не можете внести свой вклад с помощью кода, пожалуйста, не стесняйтесь сообщать об ошибках и пожеланиях к функциям, которые необходимо реализовать для решения вашей задачи.\n\n### Авторы\n\n<div align=\"center\">\n    <a href=\"https://github.com/usebruno/bruno/graphs/contributors\">\n        <img src=\"https://contrib.rocks/image?repo=usebruno/bruno\" />\n    </a>\n</div>\n\n### Оставайтесь на связи 🌐\n\n[X ( Twitter )](https://twitter.com/use_bruno) <br />\n[Наш сайт](https://www.usebruno.com) <br />\n[Discord](https://discord.com/invite/KgcZUncpjq)\n\n### Лицензия 📄\n\n[MIT](../../license.md)\n"
  },
  {
    "path": "docs/readme/readme_tr.md",
    "content": "<br />\n<img src=\"../../assets/images/logo-transparent.png\" width=\"80\"/>\n\n### Bruno - API'leri keşfetmek ve test etmek için açık kaynaklı IDE.\n\n[![GitHub sürümü](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)\n[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)\n[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)\n[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)\n[![Web Sitesi](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)\n[![İndir](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)\n\n[English](../../readme.md)\n| [Українська](./readme_ua.md)\n| [Русский](./readme_ru.md)\n| **Türkçe**\n| [Deutsch](./readme_de.md)\n| [Français](./readme_fr.md)\n| [Português (BR)](./readme_pt_br.md)\n| [한국어](./readme_kr.md)\n| [বাংলা](./readme_bn.md)\n| [Español](./readme_es.md)\n| [Italiano](./readme_it.md)\n| [Română](./readme_ro.md)\n| [Polski](./readme_pl.md)\n| [简体中文](./readme_cn.md)\n| [正體中文](./readme_zhtw.md)\n| [العربية](./readme_ar.md)\n| [日本語](./readme_ja.md)\n| [ქართული](./readme_ka.md)\n\nBruno, Postman ve benzeri araçlar tarafından temsil edilen statükoda devrim yaratmayı amaçlayan yeni ve yenilikçi bir API istemcisidir.\n\nBruno koleksiyonlarınızı doğrudan dosya sisteminizdeki bir klasörde saklar. API istekleri hakkındaki bilgileri kaydetmek için düz bir metin biçimlendirme dili olan Bru kullanıyoruz.\n\nAPI koleksiyonlarınız üzerinde işbirliği yapmak için Git veya seçtiğiniz herhangi bir sürüm kontrolünü kullanabilirsiniz.\n\nBruno yalnızca çevrimdışıdır. Bruno'ya bulut senkronizasyonu eklemek gibi bir planımız yok. Veri gizliliğinize değer veriyoruz ve cihazınızda kalması gerektiğine inanıyoruz. Uzun vadeli vizyonumuzu okuyun [burada](https://github.com/usebruno/bruno/discussions/269)\n\n📢 Hindistan FOSS 3.0 Konferansındaki son konuşmamızı izleyin [burada](https://www.youtube.com/watch?v=7bSMFpbcPiY)\n\n![bruno](/assets/images/landing-2.png) <br /><br />\n\n### Kurulum\n\nBruno Mac, Windows ve Linux için ikili indirme olarak [web sitemizde](https://www.usebruno.com/downloads) mevcuttur.\n\nBruno'yu Homebrew, Chocolatey, Scoop, Snap ve Apt gibi paket yöneticileri aracılığıyla da yükleyebilirsiniz.\n\n```sh\n# Homebrew aracılığıyla Mac'te\nbrew install bruno\n\n# Chocolatey aracılığıyla Windows'ta\nchoco install bruno\n\n# Scoop aracılığıyla Windows'ta\nscoop bucket add extras\nscoop install bruno\n\n# Snap aracılığıyla Linux'ta\nsnap install bruno\n\n# Apt aracılığıyla Linux'ta\nsudo mkdir -p /etc/apt/keyrings\nsudo apt update && sudo apt install gpg curl\ncurl -fsSL \"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266\" \\\n  | gpg --dearmor \\\n  | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null\nsudo chmod 644 /etc/apt/keyrings/bruno.gpg\necho \"deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable\" \\\n  | sudo tee /etc/apt/sources.list.d/bruno.list\nsudo apt update && sudo apt install bruno\n```\n\n### Birden fazla platformda çalıştırın 🖥️\n\n![bruno](/assets/images/run-anywhere.png) <br /><br />\n\n### Git üzerinden katkıda bulunun 👩‍💻🧑‍💻\n\nVeya seçtiğiniz herhangi bir sürüm kontrol sistemi\n\n![bruno](/assets/images/version-control.png) <br /><br />\n\n### Önemli Bağlantılar 📌\n\n- [Uzun Vadeli Vizyonumuz](https://github.com/usebruno/bruno/discussions/269)\n- [Yol Haritası](https://github.com/usebruno/bruno/discussions/384)\n- [Dokümantasyon](https://docs.usebruno.com)\n- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)\n- [Web sitesi](https://www.usebruno.com)\n- [Fiyatlandırma](https://www.usebruno.com/pricing)\n- [İndir](https://www.usebruno.com/downloads)\n- [GitHub Sponsorları](https://github.com/sponsors/helloanoop).\n\n### Vitrin 🎥\n\n- [Görüşler](https://github.com/usebruno/bruno/discussions/343)\n- [Bilgi Merkezi](https://github.com/usebruno/bruno/discussions/386)\n- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)\n\n### Destek ❤️\n\nBruno'yu seviyorsanız ve açık kaynak çalışmalarımızı desteklemek istiyorsanız, [GitHub Sponsorları](https://github.com/sponsors/helloanoop) aracılığıyla bize sponsor olmayı düşünün.\n\n### Referansları Paylaşın 📣\n\nBruno işinizde ve ekiplerinizde size yardımcı olduysa, lütfen [github tartışmamızdaki referanslarınızı](https://github.com/usebruno/bruno/discussions/343) paylaşmayı unutmayın.\n\n### Yeni Paket Yöneticilerine Yayınlama\n\nDaha fazla bilgi için lütfen [buraya](../publishing/publishing_tr.md) bakın.\n\n### Katkıda Bulunun 👩‍💻🧑‍💻\n\nBruno'yu geliştirmek istemenize sevindim. Lütfen [katkıda bulunma kılavuzuna](../contributing/contributing_tr.md) göz atın\n\nKod yoluyla katkıda bulunamasanız bile, lütfen kullanım durumunuzu çözmek için uygulanması gereken hataları ve özellik isteklerini bildirmekten çekinmeyin.\n\n### Katkıda Bulunanlar\n\n<div align=\"center\">\n    <a href=\"https://github.com/usebruno/bruno/graphs/contributors\">\n        <img src=\"https://contrib.rocks/image?repo=usebruno/bruno\" />\n    </a>\n</div>\n\n### İletişimde Kalın 🌐\n\n[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />\n[Website](https://www.usebruno.com) <br />\n[Discord](https://discord.com/invite/KgcZUncpjq) <br />\n[LinkedIn](https://www.linkedin.com/company/usebruno)\n\n### Ticari Marka\n\n**İsim**\n\n`Bruno` [Anoop M D](https://www.helloanoop.com/) tarafından sahip olunan bir ticari markadır.\n\n**Logo**\n\nLogo [OpenMoji](https://openmoji.org/library/emoji-1F436/) adresinden alınmıştır. Lisans: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)\n\n### Lisans 📄\n\n[MIT](../../license.md)\n"
  },
  {
    "path": "docs/readme/readme_ua.md",
    "content": "<br />\n<img src=\"../../assets/images/logo-transparent.png\" width=\"80\"/>\n\n### Bruno - IDE із відкритим кодом для тестування та дослідження API\n\n[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)\n[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)\n[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)\n[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)\n[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)\n[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)\n\n[English](../../readme.md)\n| **Українська**\n| [Русский](./readme_ru.md)\n| [Türkçe](./readme_tr.md)\n| [Deutsch](./readme_de.md)\n| [Français](./readme_fr.md)\n| [Português (BR)](./readme_pt_br.md)\n| [한국어](./readme_kr.md)\n| [বাংলা](./readme_bn.md)\n| [Español](./readme_es.md)\n| [Italiano](./readme_it.md)\n| [Română](./readme_ro.md)\n| [Polski](./readme_pl.md)\n| [简体中文](./readme_cn.md)\n| [正體中文](./readme_zhtw.md)\n| [العربية](./readme_ar.md)\n| [日本語](./readme_ja.md)\n| [ქართული](./readme_ka.md)\n\nBruno це новий та іноваційний API клієнт, націлений на революційну зміну статусy кво, запровадженого інструментами на кшталт Postman.\n\nBruno зберігає ваші колекції напряму у теці на вашому диску. Він використовує текстову мову розмітки Bru для збереження інформації про ваші API запити.\n\nВи можете використовувати git або будь-яку іншу систему контролю версій щоб спільно працювати над вашими колекціями API запитів.\n\nBruno є повністю автономним. Немає жодних планів додавати будь-які синхронізації через хмару, ніколи. Ми цінуємо приватність ваших даних, і вважаєм, що вони мають залишитись лише на вашому комп'ютері. Дізнатись більше про наше бачення у довготривалій перспективі можна [тут](https://github.com/usebruno/bruno/discussions/269)\n\n![bruno](/assets/images/landing-2.png) <br /><br />\n\n### Кросплатформенність 🖥️\n\n![bruno](/assets/images/run-anywhere.png) <br /><br />\n\n### Спільна робота через Git 👩‍💻🧑‍💻\n\nАбо будь-яку іншу систему контролю версій на ваш вибір\n\n![bruno](/assets/images/version-control.png) <br /><br />\n\n### Важливі посилання 📌\n\n- [Наше бачення довготривалої перспективи проекту](https://github.com/usebruno/bruno/discussions/269)\n- [Дорожня карта проекту](https://github.com/usebruno/bruno/discussions/384)\n- [Документація](https://docs.usebruno.com)\n- [Сайт](https://www.usebruno.com)\n- [Завантаження](https://www.usebruno.com/downloads)\n\n### Вітрина 🎥\n\n- [Відгуки](https://github.com/usebruno/bruno/discussions/343)\n- [Хаб знань](https://github.com/usebruno/bruno/discussions/386)\n- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)\n\n### Підтримка ❤️\n\nГав! Якщо вам сподобався проект, тисніть на ⭐ !!\n\n### Поділитись відгуками 📣\n\nЯкщо Bruno допоміг у роботі вам або вашій команді, будь ласка не забудьте поділитись вашими [відгуками у github дискусії](https://github.com/usebruno/bruno/discussions/343)\n\n### Зробити свій внесок 👩‍💻🧑‍💻\n\nЯ радий що ви бажаєте покращити Bruno. Будь ласка переглянте [інструкцію по контрибуції](../contributing/contributing_ua.md)\n\nНавіть якщо ви не можете зробити свій внесок пишучи код, будь ласка не соромтесь рапортувати про помилки і писати запити на новий функціонал, який потрібен вам у вашій роботі.\n\n### Автори\n\n<div align=\"center\">\n    <a href=\"https://github.com/usebruno/bruno/graphs/contributors\">\n        <img src=\"https://contrib.rocks/image?repo=usebruno/bruno\" />\n    </a>\n</div>\n\n### Залишайтесь на зв'язку 🌐\n\n[Twitter](https://twitter.com/use_bruno) <br />\n[Сайт](https://www.usebruno.com) <br />\n[Discord](https://discord.com/invite/KgcZUncpjq) <br />\n[LinkedIn](https://www.linkedin.com/company/usebruno)\n\n### Ліцензія 📄\n\n[MIT](../../license.md)\n"
  },
  {
    "path": "docs/readme/readme_zhtw.md",
    "content": "<br />\n<img src=\"../../assets/images/logo-transparent.png\" width=\"80\"/>\n\n### Bruno - 探索和測試 API 的開源 IDE 工具\n\n[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)\n[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)\n[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)\n[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)\n[![网站](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)\n[![下载](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)\n\n[English](../../readme.md)\n| [Українська](./readme_ua.md)\n| [Русский](./readme_ru.md)\n| [Türkçe](./readme_tr.md)\n| [Deutsch](./readme_de.md)\n| [Français](./readme_fr.md)\n| [Português (BR)](./readme_pt_br.md)\n| [한국어](./readme_kr.md)\n| [বাংলা](./readme_bn.md)\n| [Español](./readme_es.md)\n| [Italiano](./readme_it.md)\n| [Română](./readme_ro.md)\n| [Polski](./readme_pl.md)\n| [简体中文](./readme_cn.md)\n| **正體中文**\n| [العربية](./readme_ar.md)\n| [日本語](./readme_ja.md)\n| [ქართული](./readme_ka.md)\n\nBruno 是一個全新且有創新性的 API 用戶端，目的在徹底改變以 Postman 和其他類似工具的現況。\n\nBruno 將您的 API 集合直接儲存在檔案系統上的資料夾中。我們以純文本標記語言- Bru，來儲存和 API 有關的資訊。\n\n您可以使用 Git 或您選擇的任何版本管理軟體，來管理及協作 API 集合。\n\nBruno 僅能夠離線使用，永遠不會計劃為 Bruno 增加雲端同步的功能。我們重視您的資料隱私，並相信它應該保留在您的裝置上。瞭解我們的長期願景 [連結](https://github.com/usebruno/bruno/discussions/269)\n\n📢 觀看我們最近在 India FOSS 3.0 研討會上的演講 [連結](https://www.youtube.com/watch?v=7bSMFpbcPiY)\n\n![bruno](../../assets/images/landing-2.png) <br /><br />\n\n### 安装\n\n可以在我們的 [網站上下載](https://www.usebruno.com/downloads) 跨平臺（Mac、Windows 和 Linux）的 Bruno 程式檔。\n\n您也可以透過套件管理程式來安裝 Bruno，如：Homebrew、Chocolatey、Scoop、Snap 和 Apt。\n\n```shell\n# 在 Mac 上使用 Homebrew 安裝\nbrew install bruno\n\n# 在 Windows 上使用 Chocolatey 安裝\nchoco install bruno\n\n# 在 Windows 上使用 Scoop 安裝\nscoop bucket add extras\nscoop install bruno\n\n# 在 Linux 上使用 Snap 安裝\nsnap install bruno\n\n# 在 Linux 上使用 Apt 安裝\nsudo mkdir -p /etc/apt/keyrings\nsudo apt update && sudo apt install gpg curl\ncurl -fsSL \"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266\" \\\n  | gpg --dearmor \\\n  | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null\nsudo chmod 644 /etc/apt/keyrings/bruno.gpg\necho \"deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable\" \\\n  | sudo tee /etc/apt/sources.list.d/bruno.list\nsudo apt update && sudo apt install bruno\n```\n\n### 跨多個平台運行 🖥️\n\n![bruno](../../assets/images/run-anywhere.png) <br /><br />\n\n### 透過 Git 進行協作 👩‍💻🧑‍💻\n\n您選擇的任何版本管理軟體\n\n![bruno](../../assets/images/version-control.png) <br /><br />\n\n### 重要連結 📌\n\n- [我們的長期願景](https://github.com/usebruno/bruno/discussions/269)\n- [藍圖](https://github.com/usebruno/bruno/discussions/384)\n- [說明文件](https://docs.usebruno.com)\n- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)\n- [網站](https://www.usebruno.com)\n- [定價](https://www.usebruno.com/pricing)\n- [下載](https://www.usebruno.com/downloads)\n- [GitHub 贊助](https://github.com/sponsors/helloanoop).\n\n### 展示 🎥\n\n- [Testimonials](https://github.com/usebruno/bruno/discussions/343)\n- [Knowledge Hub](https://github.com/usebruno/bruno/discussions/386)\n- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)\n\n### 贊助支持 ❤️\n\n如果您喜歡 Bruno 和希望支持我們在開源上的工作，請考慮使用 [GitHub Sponsors](https://github.com/sponsors/helloanoop) 來贊助我們。\n\n### 分享感想 📣\n\n如果 Bruno 在工作和您的團隊中為您提供了幫助，請不要忘記在我們的 [GitHub 討論區](https://github.com/usebruno/bruno/discussions/343) 中分享您的感想。\n\n### 發佈到新的套件管理器\n\n更多資訊，請參考這個 [連結](../publishing/publishing_zhtw.md) 。\n\n### 持續關注 🌐\n\n[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />\n[Website](https://www.usebruno.com) <br />\n[Discord](https://discord.com/invite/KgcZUncpjq) <br />\n[LinkedIn](https://www.linkedin.com/company/usebruno)\n\n### 商標\n\n**名稱**\n\n`Bruno` 是 [Anoop M D](https://www.helloanoop.com/) 持有的商標。\n\n**Logo**\n\nLogo 源自於 [OpenMoji](https://openmoji.org/library/emoji-1F436/)。授權: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)\n\n### 提供貢獻 👩‍💻🧑‍💻\n\n我很高興您希望一同改善 Bruno。請參考 [貢獻指南](../contributing/contributing_zhtw.md)。\n\n即使您無法透過程式碼做出貢獻，我們仍然歡迎您提出 Bug 及新的實作需求。\n\n### 作者們\n\n<div align=\"center\">\n    <a href=\"https://github.com/usebruno/bruno/graphs/contributors\">\n        <img src=\"https://contrib.rocks/image?repo=usebruno/bruno\" />\n    </a>\n</div>\n\n### 授權許可 📄\n\n[MIT](../../license.md)\n"
  },
  {
    "path": "eslint.config.js",
    "content": "// eslint.config.js\nconst { defineConfig } = require('eslint/config');\nconst globals = require('globals');\nconst { fixupPluginRules } = require('@eslint/compat');\nconst eslintPluginDiff = require('eslint-plugin-diff');\n\nlet stylistic;\n\nconst runESMImports = async () => {\n  stylistic = await import('@stylistic/eslint-plugin').then((d) => d.default);\n};\n\nmodule.exports = runESMImports().then(() => defineConfig([\n  // Global ignores - must be a standalone object with ONLY ignores\n  {\n    ignores: [\n      '**/node_modules/**/*',\n      '**/dist/**/*',\n      '**/*.bru',\n      'packages/bruno-js/src/sandbox/bundle-browser-rollup.js',\n      'packages/bruno-app/public/static/**/*',\n      'packages/bruno-app/.next/**/*',\n      'packages/bruno-electron/web/**/*'\n    ]\n  },\n  {\n    plugins: {\n      'diff': fixupPluginRules(eslintPluginDiff),\n      '@stylistic': stylistic\n    },\n    languageOptions: {\n      parser: require('@typescript-eslint/parser'),\n      parserOptions: {\n        ecmaVersion: 'latest',\n        sourceType: 'module'\n      }\n    },\n    files: [\n      './eslint.config.js',\n      'tests/**/*.{ts,js}',\n      'playwright/**/*.{js,ts}',\n      'packages/bruno-app/**/*.{js,jsx,ts}',\n      'packages/bruno-app/src/test-utils/mocks/codemirror.js',\n      'packages/bruno-cli/**/*.js',\n      'packages/bruno-common/**/*.ts',\n      'packages/bruno-converters/**/*.js',\n      'packages/bruno-electron/**/*.js',\n      'packages/bruno-filestore/**/*.ts',\n      'packages/bruno-schema-types/**/*.ts',\n      'packages/bruno-js/**/*.js',\n      'packages/bruno-lang/**/*.js',\n      'packages/bruno-requests/**/*.ts',\n      'packages/bruno-requests/**/*.js',\n      'packages/bruno-tests/**/*.{js,ts}'\n    ],\n    rules: {\n      ...stylistic.configs.customize({\n        indent: 2,\n        quotes: 'single',\n        semi: true,\n        jsx: true\n      }).rules,\n      '@stylistic/comma-dangle': ['error', 'never'],\n      '@stylistic/brace-style': ['error', '1tbs', { allowSingleLine: true }],\n      '@stylistic/arrow-parens': ['error', 'always'],\n      '@stylistic/curly-newline': ['error', {\n        multiline: true,\n        minElements: 2,\n        consistent: true\n      }],\n      '@stylistic/function-paren-newline': ['off'],\n      '@stylistic/array-bracket-spacing': ['error', 'never'],\n      '@stylistic/arrow-spacing': ['error', { before: true, after: true }],\n      '@stylistic/function-call-spacing': ['error', 'never'],\n      '@stylistic/multiline-ternary': ['off'],\n      '@stylistic/padding-line-between-statements': ['off'],\n      '@stylistic/semi-style': ['error', 'last'],\n      '@stylistic/max-len': ['off'],\n      '@stylistic/jsx-one-expression-per-line': ['off'],\n      '@stylistic/max-statements-per-line': ['off'],\n      '@stylistic/no-mixed-operators': ['off']\n    }\n  },\n  {\n    files: ['packages/bruno-app/**/*.{js,jsx,ts}'],\n    ignores: ['**/*.config.js', '**/public/**/*'],\n    languageOptions: {\n      globals: {\n        ...globals.browser,\n        ...globals.jest,\n        global: false,\n        require: false,\n        Buffer: false,\n        process: false,\n        ipcRenderer: false\n      },\n      parserOptions: {\n        ecmaFeatures: {\n          jsx: true\n        }\n      }\n    },\n    rules: {\n      'no-undef': 'error'\n    }\n  },\n  {\n    // It prevents lint errors when using CommonJS exports (module.exports) in Jest mocks.\n    files: ['packages/bruno-app/src/test-utils/mocks/codemirror.js'],\n    languageOptions: {\n      globals: {\n        ...globals.node,\n        ...globals.jest\n      }\n    },\n    rules: {\n      'no-undef': 'error'\n    }\n  },\n  {\n    // Storybook config files use CommonJS with __dirname and module.exports\n    files: ['packages/bruno-app/storybook/**/*.js'],\n    languageOptions: {\n      globals: {\n        ...globals.node\n      }\n    },\n    rules: {\n      'no-undef': 'error'\n    }\n  },\n  {\n    files: ['packages/bruno-cli/**/*.js'],\n    ignores: ['**/*.config.js'],\n    languageOptions: {\n      globals: {\n        ...globals.node,\n        ...globals.jest\n      },\n      parserOptions: {\n        ecmaVersion: 'latest'\n      }\n    },\n    rules: {\n      'no-undef': 'error'\n    }\n  },\n  {\n    files: ['packages/bruno-common/**/*.ts'],\n    ignores: ['**/*.config.js', '**/dist/**/*'],\n    languageOptions: {\n      globals: {\n        ...globals.node,\n        ...globals.jest\n      },\n      parser: require('@typescript-eslint/parser'),\n      parserOptions: {\n        ecmaVersion: 'latest',\n        sourceType: 'module',\n        project: './packages/bruno-common/tsconfig.json'\n      }\n    },\n    rules: {\n      'no-undef': 'error'\n    }\n  },\n  {\n    files: ['packages/bruno-converters/**/*.js'],\n    ignores: ['**/*.config.js', '**/dist/**/*'],\n    languageOptions: {\n      globals: {\n        ...globals.node,\n        ...globals.jest\n      },\n      parserOptions: {\n        ecmaVersion: 'latest',\n        sourceType: 'module'\n      }\n    },\n    rules: {\n      'no-undef': 'error'\n    }\n  },\n  {\n    files: ['packages/bruno-electron/**/*.js'],\n    ignores: ['**/*.config.js', '**/web/**/*'],\n    languageOptions: {\n      globals: {\n        ...globals.node,\n        ...globals.jest\n      }\n    },\n    rules: {\n      'no-undef': 'error'\n    }\n  },\n  {\n    files: ['packages/bruno-filestore/**/*.ts'],\n    ignores: ['**/*.config.js', '**/dist/**/*'],\n    languageOptions: {\n      globals: {\n        ...globals.node,\n        ...globals.jest\n      },\n      parser: require('@typescript-eslint/parser'),\n      parserOptions: {\n        ecmaVersion: 'latest',\n        sourceType: 'module',\n        project: './packages/bruno-filestore/tsconfig.json'\n      }\n    },\n    rules: {\n      'no-undef': 'error'\n    }\n  },\n  {\n    files: ['packages/bruno-js/**/*.js'],\n    ignores: ['**/*.config.js', '**/dist/**/*'],\n    languageOptions: {\n      globals: {\n        ...globals.node,\n        ...globals.jest,\n        window: false,\n        self: false,\n        HTMLElement: false,\n        typeDetectGlobalObject: false\n      },\n      parserOptions: {\n        ecmaVersion: 'latest',\n        sourceType: 'module'\n      }\n    },\n    rules: {\n      'no-undef': 'error'\n    }\n  },\n  {\n    files: ['packages/bruno-lang/**/*.js'],\n    ignores: ['**/*.config.js', '**/dist/**/*'],\n    languageOptions: {\n      globals: {\n        ...globals.node,\n        ...globals.jest\n      },\n      parserOptions: {\n        ecmaVersion: 'latest',\n        sourceType: 'module'\n      }\n    },\n    rules: {\n      'no-undef': 'error'\n    }\n  },\n  {\n    files: ['packages/bruno-requests/**/*.ts'],\n    ignores: ['**/*.config.js', '**/dist/**/*'],\n    languageOptions: {\n      globals: {\n        ...globals.node,\n        ...globals.jest\n      },\n      parser: require('@typescript-eslint/parser'),\n      parserOptions: {\n        ecmaVersion: 'latest',\n        sourceType: 'module',\n        project: './packages/bruno-requests/tsconfig.json'\n      }\n    },\n    rules: {\n      'no-undef': 'error'\n    }\n  },\n  {\n    files: ['packages/bruno-requests/**/*.js'],\n    ignores: ['**/*.config.js', '**/dist/**/*'],\n    languageOptions: {\n      globals: {\n        ...globals.node,\n        ...globals.jest\n      },\n      parserOptions: {\n        ecmaVersion: 'latest',\n        sourceType: 'module'\n      }\n    },\n    rules: {\n      'no-undef': 'error'\n    }\n  }\n]));\n"
  },
  {
    "path": "license.md",
    "content": "\nMIT License\n\nCopyright (c) 2022 Anoop M D, Anusree P S and Contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"usebruno\",\n  \"private\": true,\n  \"workspaces\": [\n    \"packages/bruno-app\",\n    \"packages/bruno-electron\",\n    \"packages/bruno-cli\",\n    \"packages/bruno-common\",\n    \"packages/bruno-converters\",\n    \"packages/bruno-schema\",\n    \"packages/bruno-schema-types\",\n    \"packages/bruno-query\",\n    \"packages/bruno-js\",\n    \"packages/bruno-lang\",\n    \"packages/bruno-tests\",\n    \"packages/bruno-toml\",\n    \"packages/bruno-graphql-docs\",\n    \"packages/bruno-requests\",\n    \"packages/bruno-filestore\"\n  ],\n  \"homepage\": \"https://usebruno.com\",\n  \"devDependencies\": {\n    \"@eslint/compat\": \"^1.3.2\",\n    \"@faker-js/faker\": \"^7.6.0\",\n    \"@jest/globals\": \"^29.2.0\",\n    \"@opencollection/types\": \"~0.8.0\",\n    \"@playwright/test\": \"^1.51.1\",\n    \"@rollup/plugin-json\": \"^6.1.0\",\n    \"@storybook/addon-webpack5-compiler-babel\": \"^4.0.0\",\n    \"@storybook/builder-webpack5\": \"^10.1.10\",\n    \"@storybook/react\": \"^10.1.10\",\n    \"@storybook/react-webpack5\": \"^10.1.10\",\n    \"@stylistic/eslint-plugin\": \"^5.3.1\",\n    \"@types/jest\": \"^29.5.11\",\n    \"@types/lodash-es\": \"^4.17.12\",\n    \"@types/node\": \"^22.14.1\",\n    \"@typescript-eslint/parser\": \"^8.39.0\",\n    \"concurrently\": \"^8.2.2\",\n    \"cross-env\": \"10.1.0\",\n    \"eslint\": \"^9.26.0\",\n    \"eslint-plugin-diff\": \"^2.0.3\",\n    \"fs-extra\": \"^11.1.1\",\n    \"globals\": \"^16.1.0\",\n    \"husky\": \"^9.1.7\",\n    \"jest\": \"^29.2.0\",\n    \"lodash-es\": \"^4.17.21\",\n    \"nano-staged\": \"^0.8.0\",\n    \"playwright\": \"^1.51.1\",\n    \"pretty-quick\": \"^3.1.3\",\n    \"randomstring\": \"^1.2.2\",\n    \"rimraf\": \"^6.0.1\",\n    \"storybook\": \"^10.1.10\",\n    \"ts-jest\": \"^29.2.6\"\n  },\n  \"scripts\": {\n    \"setup\": \"node ./scripts/setup.js\",\n    \"watch:converters\": \"npm run watch --workspace=packages/bruno-converters\",\n    \"dev\": \"node ./scripts/dev.js\",\n    \"watch\": \"npm run dev:watch\",\n    \"dev:watch\": \"node ./scripts/dev-hot-reload.js\",\n    \"dev:web\": \"npm run dev --workspace=packages/bruno-app\",\n    \"build:web\": \"npm run build --workspace=packages/bruno-app\",\n    \"dev:electron\": \"npm run dev --workspace=packages/bruno-electron\",\n    \"dev:electron:debug\": \"npm run debug --workspace=packages/bruno-electron\",\n    \"storybook\": \"npm run storybook --workspace=packages/bruno-app\",\n    \"build:bruno-common\": \"npm run build --workspace=packages/bruno-common\",\n    \"build:bruno-requests\": \"npm run build --workspace=packages/bruno-requests\",\n    \"build:bruno-filestore\": \"npm run build --workspace=packages/bruno-filestore\",\n    \"build:bruno-converters\": \"npm run build --workspace=packages/bruno-converters\",\n    \"build:bruno-query\": \"npm run build --workspace=packages/bruno-query\",\n    \"build:graphql-docs\": \"npm run build --workspace=packages/bruno-graphql-docs\",\n    \"build:schema-types\": \"npm run build --workspace=packages/bruno-schema-types\",\n    \"build:electron\": \"node ./scripts/build-electron.js\",\n    \"build:electron:mac\": \"./scripts/build-electron.sh mac\",\n    \"build:electron:win\": \"./scripts/build-electron.sh win\",\n    \"build:electron:linux\": \"./scripts/build-electron.sh linux\",\n    \"build:electron:deb\": \"./scripts/build-electron.sh deb\",\n    \"build:electron:rpm\": \"./scripts/build-electron.sh rpm\",\n    \"build:electron:snap\": \"./scripts/build-electron.sh snap\",\n    \"watch:common\": \"npm run watch --workspace=packages/bruno-common\",\n    \"watch:requests\": \"npm run watch --workspace=packages/bruno-requests\",\n    \"test:codegen\": \"node playwright/codegen.ts\",\n    \"test:e2e\": \"playwright test --project=default\",\n    \"test:e2e:ssl\": \"playwright test --project=ssl\",\n    \"lint\": \"cross-env NODE_OPTIONS=\\\"--max_old_space_size=4096\\\" npx eslint\",\n    \"lint:fix\": \"cross-env NODE_OPTIONS=\\\"--max_old_space_size=4096\\\" npx eslint --fix\",\n    \"prepare\": \"husky\"\n  },\n  \"nano-staged\": {\n    \"*.{js,ts,jsx}\": [\n      \"npm run lint:fix\"\n    ]\n  },\n  \"overrides\": {\n    \"rollup\": \"3.29.5\",\n    \"electron-store\": {\n      \"conf\": {\n        \"json-schema-typed\": \"8.0.1\"\n      }\n    }\n  },\n  \"dependencies\": {\n    \"ajv\": \"^8.17.1\",\n    \"git-url-parse\": \"^14.1.0\"\n  }\n}"
  },
  {
    "path": "packages/bruno-app/.babelrc",
    "content": "{\n  \"presets\": [\"@babel/preset-env\", \"@babel/preset-react\"],\n  \"plugins\": [[\"styled-components\", { \"ssr\": true }]]\n}"
  },
  {
    "path": "packages/bruno-app/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\nnode_modules\n.pnp\n.pnp.js\npnpm-lock.yaml\npackage-lock.json\nyarn.lock\n\n# testing\ncoverage\n\n# production\nbuild\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n*.log\n\n# local env files\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\n# next.js\n.next/\ndist/\n\n.env\nstorybook-static/"
  },
  {
    "path": "packages/bruno-app/babel.config.js",
    "content": "module.exports = {\n  presets: [\n    '@babel/preset-env',\n    ['@babel/preset-react', {\n      runtime: 'automatic'\n    }]\n  ],\n  plugins: ['babel-plugin-styled-components']\n};\n"
  },
  {
    "path": "packages/bruno-app/jest.config.js",
    "content": "module.exports = {\n  rootDir: '.',\n  transform: {\n    '^.+\\\\.[jt]sx?$': 'babel-jest'\n  },\n  transformIgnorePatterns: [\n    '/node_modules/(?!strip-json-comments|nanoid|xml-formatter)/'\n  ],\n  moduleNameMapper: {\n    '^assets/(.*)$': '<rootDir>/src/assets/$1',\n    '^components/(.*)$': '<rootDir>/src/components/$1',\n    '^hooks/(.*)$': '<rootDir>/src/hooks/$1',\n    '^themes/(.*)$': '<rootDir>/src/themes/$1',\n    '^api/(.*)$': '<rootDir>/src/api/$1',\n    '^pageComponents/(.*)$': '<rootDir>/src/pageComponents/$1',\n    '^providers/(.*)$': '<rootDir>/src/providers/$1',\n    '^utils/(.*)$': '<rootDir>/src/utils/$1',\n    '^test-utils/(.*)$': '<rootDir>/src/test-utils/$1'\n  },\n  clearMocks: true,\n  moduleDirectories: ['node_modules', 'src'],\n  testEnvironment: 'jsdom',\n  setupFilesAfterEnv: ['@testing-library/jest-dom'],\n  setupFiles: [\n    '<rootDir>/jest.setup.js'\n  ],\n  testMatch: [\n    '<rootDir>/src/**/*.spec.[jt]s?(x)'\n  ]\n};\n"
  },
  {
    "path": "packages/bruno-app/jest.setup.js",
    "content": "jest.mock('nanoid', () => {\n  return {\n    nanoid: () => {}\n  };\n});\n\njest.mock('strip-json-comments', () => {\n  return {\n    stripJsonComments: (str) => str\n  };\n});\n"
  },
  {
    "path": "packages/bruno-app/jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"jsx\": \"react\",\n    \"target\": \"es2017\",\n    \"allowSyntheticDefaultImports\": false,\n    \"baseUrl\": \"./\",\n    \"paths\": {\n      \"assets/*\": [\"src/assets/*\"],\n      \"ui/*\": [\"src/ui/*\"],\n      \"components/*\": [\"src/components/*\"],\n      \"hooks/*\": [\"src/hooks/*\"],\n      \"themes/*\": [\"src/themes/*\"],\n      \"api/*\": [\"src/api/*\"],\n      \"pageComponents/*\": [\"src/pageComponents/*\"],\n      \"providers/*\": [\"src/providers/*\"],\n      \"utils/*\": [\"src/utils/*\"]\n    }\n  },\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/bruno-app/package.json",
    "content": "{\n  \"name\": \"@usebruno/app\",\n  \"version\": \"2.0.0\",\n  \"license\": \"MIT\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"rsbuild dev\",\n    \"build\": \"rsbuild build -m production\",\n    \"preview\": \"rsbuild preview\",\n    \"test\": \"jest\",\n    \"storybook\": \"storybook dev -p 6006 --config-dir storybook\",\n    \"build-storybook\": \"storybook build --config-dir storybook\"\n  },\n  \"dependencies\": {\n    \"@fontsource/inter\": \"^5.0.15\",\n    \"@prantlf/jsonlint\": \"^16.0.0\",\n    \"@reduxjs/toolkit\": \"^1.8.0\",\n    \"@tabler/icons\": \"^1.46.0\",\n    \"@testing-library/user-event\": \"^14.6.1\",\n    \"@tippyjs/react\": \"^4.2.6\",\n    \"@usebruno/common\": \"0.1.0\",\n    \"@usebruno/graphql-docs\": \"0.1.0\",\n    \"@usebruno/schema\": \"0.7.0\",\n    \"@xterm/addon-fit\": \"^0.10.0\",\n    \"@xterm/xterm\": \"^5.5.0\",\n    \"classnames\": \"^2.3.1\",\n    \"codemirror\": \"5.65.2\",\n    \"codemirror-graphql\": \"2.1.1\",\n    \"cookie\": \"0.7.1\",\n    \"diff2html\": \"^3.4.47\",\n    \"dompurify\": \"^3.2.4\",\n    \"escape-html\": \"^1.0.3\",\n    \"fast-fuzzy\": \"^1.12.0\",\n    \"fast-json-format\": \"~0.4.0\",\n    \"file\": \"^0.2.2\",\n    \"file-dialog\": \"^0.0.8\",\n    \"file-saver\": \"^2.0.5\",\n    \"formik\": \"^2.2.9\",\n    \"github-markdown-css\": \"^5.2.0\",\n    \"graphiql\": \"3.7.1\",\n    \"graphql\": \"^16.6.0\",\n    \"graphql-request\": \"^3.7.0\",\n    \"hexy\": \"^0.3.5\",\n    \"httpsnippet\": \"^3.0.9\",\n    \"i18next\": \"24.1.2\",\n    \"idb\": \"^7.0.0\",\n    \"immer\": \"^9.0.15\",\n    \"js-yaml\": \"^4.1.0\",\n    \"jsesc\": \"^3.0.2\",\n    \"jshint\": \"^2.13.6\",\n    \"json5\": \"^2.2.3\",\n    \"jsonc-parser\": \"^3.2.1\",\n    \"jsonpath-plus\": \"^10.3.0\",\n    \"jsonschema\": \"^1.5.0\",\n    \"know-your-http-well\": \"^0.5.0\",\n    \"linkify-it\": \"^5.0.0\",\n    \"lodash\": \"^4.17.21\",\n    \"markdown-it\": \"^13.0.2\",\n    \"markdown-it-replace-link\": \"^1.2.0\",\n    \"mime-types\": \"^3.0.2\",\n    \"moment\": \"^2.30.1\",\n    \"moment-timezone\": \"^0.5.47\",\n    \"mousetrap\": \"^1.6.5\",\n    \"nanoid\": \"3.3.8\",\n    \"path\": \"^0.12.7\",\n    \"pdfjs-dist\": \"4.4.168\",\n    \"platform\": \"^1.3.6\",\n    \"polished\": \"^4.3.1\",\n    \"posthog-node\": \"4.2.1\",\n    \"prettier\": \"^2.7.1\",\n    \"qs\": \"^6.14.1\",\n    \"query-string\": \"^7.0.1\",\n    \"react\": \"19.0.0\",\n    \"react-copy-to-clipboard\": \"^5.1.0\",\n    \"react-dnd\": \"^16.0.1\",\n    \"react-dnd-html5-backend\": \"^16.0.1\",\n    \"react-dom\": \"19.0.0\",\n    \"react-hot-toast\": \"^2.4.0\",\n    \"react-i18next\": \"^15.0.1\",\n    \"react-inspector\": \"^6.0.2\",\n    \"react-json-view\": \"^1.21.3\",\n    \"react-pdf\": \"9.1.1\",\n    \"react-player\": \"^2.16.0\",\n    \"react-redux\": \"^7.2.9\",\n    \"react-tooltip\": \"^5.5.2\",\n    \"react-virtuoso\": \"^4.18.1\",\n    \"sass\": \"^1.46.0\",\n    \"semver\": \"^7.7.1\",\n    \"shell-quote\": \"^1.8.3\",\n    \"strip-json-comments\": \"^5.0.1\",\n    \"styled-components\": \"^5.3.3\",\n    \"swagger-ui-react\": \"^5.31.0\",\n    \"system\": \"^2.0.1\",\n    \"url\": \"^0.11.3\",\n    \"xml-formatter\": \"^3.5.0\",\n    \"xml2js\": \"^0.6.2\",\n    \"yup\": \"^0.32.11\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.27.1\",\n    \"@babel/preset-env\": \"^7.27.2\",\n    \"@babel/preset-react\": \"^7.27.1\",\n    \"@rsbuild/core\": \"^1.1.2\",\n    \"@rsbuild/plugin-babel\": \"^1.0.3\",\n    \"@rsbuild/plugin-node-polyfill\": \"^1.2.0\",\n    \"@rsbuild/plugin-react\": \"^1.0.7\",\n    \"@rsbuild/plugin-sass\": \"^1.1.0\",\n    \"@rsbuild/plugin-styled-components\": \"1.1.0\",\n    \"@testing-library/dom\": \"^10.4.1\",\n    \"@testing-library/jest-dom\": \"^6.6.3\",\n    \"@testing-library/react\": \"^16.3.0\",\n    \"autoprefixer\": \"10.4.20\",\n    \"babel-jest\": \"^29.7.0\",\n    \"babel-plugin-react-compiler\": \"19.0.0-beta-a7bf2bd-20241110\",\n    \"babel-plugin-styled-components\": \"^2.1.4\",\n    \"cross-env\": \"^7.0.3\",\n    \"css-loader\": \"7.1.2\",\n    \"file-loader\": \"^6.2.0\",\n    \"html-loader\": \"^3.0.1\",\n    \"html-webpack-plugin\": \"^5.5.0\",\n    \"jest-environment-jsdom\": \"^29.7.0\",\n    \"mini-css-extract-plugin\": \"^2.4.5\",\n    \"postcss\": \"8.4.47\",\n    \"style-loader\": \"^3.3.1\",\n    \"tailwindcss\": \"^3.4.1\",\n    \"webpack\": \"^5.64.4\",\n    \"webpack-cli\": \"^4.9.1\"\n  },\n  \"overrides\": {\n    \"httpsnippet\": {\n      \"form-data\": \"4.0.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bruno-app/postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {}\n  }\n};\n"
  },
  {
    "path": "packages/bruno-app/public/static/diff2Html.js",
    "content": "!(function (e, t) {\n  'object' == typeof exports && 'object' == typeof module\n    ? (module.exports = t())\n    : 'function' == typeof define && define.amd\n    ? define('Diff2Html', [], t)\n    : 'object' == typeof exports\n    ? (exports.Diff2Html = t())\n    : (e.Diff2Html = t());\n})(this, () => {\n  return (\n    (e = {\n      696: (e, t) => {\n        'use strict';\n        Object.defineProperty(t, '__esModule', { value: !0 }),\n          (t.convertChangesToDMP = function (e) {\n            for (var t, n, i = [], r = 0; r < e.length; r++)\n              (n = (t = e[r]).added ? 1 : t.removed ? -1 : 0), i.push([n, t.value]);\n            return i;\n          });\n      },\n      826: (e, t) => {\n        'use strict';\n        Object.defineProperty(t, '__esModule', { value: !0 }),\n          (t.convertChangesToXML = function (e) {\n            for (var t = [], n = 0; n < e.length; n++) {\n              var i = e[n];\n              i.added ? t.push('<ins>') : i.removed && t.push('<del>'),\n                t.push(\n                  i.value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;')\n                ),\n                i.added ? t.push('</ins>') : i.removed && t.push('</del>');\n            }\n            return t.join('');\n          });\n      },\n      976: (e, t, n) => {\n        'use strict';\n        var i;\n        Object.defineProperty(t, '__esModule', { value: !0 }),\n          (t.diffArrays = function (e, t, n) {\n            return r.diff(e, t, n);\n          }),\n          (t.arrayDiff = void 0);\n        var r = new ((i = n(913)) && i.__esModule ? i : { default: i }).default();\n        (t.arrayDiff = r),\n          (r.tokenize = function (e) {\n            return e.slice();\n          }),\n          (r.join = r.removeEmpty =\n            function (e) {\n              return e;\n            });\n      },\n      913: (e, t) => {\n        'use strict';\n        function n() {}\n        function i(e, t, n, i, r) {\n          for (var s = 0, o = t.length, a = 0, l = 0; s < o; s++) {\n            var c = t[s];\n            if (c.removed) {\n              if (((c.value = e.join(i.slice(l, l + c.count))), (l += c.count), s && t[s - 1].added)) {\n                var d = t[s - 1];\n                (t[s - 1] = t[s]), (t[s] = d);\n              }\n            } else {\n              if (!c.added && r) {\n                var f = n.slice(a, a + c.count);\n                (f = f.map(function (e, t) {\n                  var n = i[l + t];\n                  return n.length > e.length ? n : e;\n                })),\n                  (c.value = e.join(f));\n              } else c.value = e.join(n.slice(a, a + c.count));\n              (a += c.count), c.added || (l += c.count);\n            }\n          }\n          var u = t[o - 1];\n          return (\n            o > 1 &&\n              'string' == typeof u.value &&\n              (u.added || u.removed) &&\n              e.equals('', u.value) &&\n              ((t[o - 2].value += u.value), t.pop()),\n            t\n          );\n        }\n        Object.defineProperty(t, '__esModule', { value: !0 }),\n          (t.default = n),\n          (n.prototype = {\n            diff: function (e, t) {\n              var n = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {},\n                r = n.callback;\n              'function' == typeof n && ((r = n), (n = {})), (this.options = n);\n              var s = this;\n              function o(e) {\n                return r\n                  ? (setTimeout(function () {\n                      r(void 0, e);\n                    }, 0),\n                    !0)\n                  : e;\n              }\n              (e = this.castInput(e)), (t = this.castInput(t)), (e = this.removeEmpty(this.tokenize(e)));\n              var a = (t = this.removeEmpty(this.tokenize(t))).length,\n                l = e.length,\n                c = 1,\n                d = a + l;\n              n.maxEditLength && (d = Math.min(d, n.maxEditLength));\n              var f = [{ newPos: -1, components: [] }],\n                u = this.extractCommon(f[0], t, e, 0);\n              if (f[0].newPos + 1 >= a && u + 1 >= l) return o([{ value: this.join(t), count: t.length }]);\n              function h() {\n                for (var n = -1 * c; n <= c; n += 2) {\n                  var r = void 0,\n                    d = f[n - 1],\n                    u = f[n + 1],\n                    h = (u ? u.newPos : 0) - n;\n                  d && (f[n - 1] = void 0);\n                  var p = d && d.newPos + 1 < a,\n                    b = u && 0 <= h && h < l;\n                  if (p || b) {\n                    if (\n                      (!p || (b && d.newPos < u.newPos)\n                        ? ((r = { newPos: (g = u).newPos, components: g.components.slice(0) }),\n                          s.pushComponent(r.components, void 0, !0))\n                        : ((r = d).newPos++, s.pushComponent(r.components, !0, void 0)),\n                      (h = s.extractCommon(r, t, e, n)),\n                      r.newPos + 1 >= a && h + 1 >= l)\n                    )\n                      return o(i(s, r.components, t, e, s.useLongestToken));\n                    f[n] = r;\n                  } else f[n] = void 0;\n                }\n                var g;\n                c++;\n              }\n              if (r)\n                !(function e() {\n                  setTimeout(function () {\n                    if (c > d) return r();\n                    h() || e();\n                  }, 0);\n                })();\n              else\n                for (; c <= d; ) {\n                  var p = h();\n                  if (p) return p;\n                }\n            },\n            pushComponent: function (e, t, n) {\n              var i = e[e.length - 1];\n              i && i.added === t && i.removed === n\n                ? (e[e.length - 1] = { count: i.count + 1, added: t, removed: n })\n                : e.push({ count: 1, added: t, removed: n });\n            },\n            extractCommon: function (e, t, n, i) {\n              for (\n                var r = t.length, s = n.length, o = e.newPos, a = o - i, l = 0;\n                o + 1 < r && a + 1 < s && this.equals(t[o + 1], n[a + 1]);\n\n              )\n                o++, a++, l++;\n              return l && e.components.push({ count: l }), (e.newPos = o), a;\n            },\n            equals: function (e, t) {\n              return this.options.comparator\n                ? this.options.comparator(e, t)\n                : e === t || (this.options.ignoreCase && e.toLowerCase() === t.toLowerCase());\n            },\n            removeEmpty: function (e) {\n              for (var t = [], n = 0; n < e.length; n++) e[n] && t.push(e[n]);\n              return t;\n            },\n            castInput: function (e) {\n              return e;\n            },\n            tokenize: function (e) {\n              return e.split('');\n            },\n            join: function (e) {\n              return e.join('');\n            }\n          });\n      },\n      630: (e, t, n) => {\n        'use strict';\n        var i;\n        Object.defineProperty(t, '__esModule', { value: !0 }),\n          (t.diffChars = function (e, t, n) {\n            return r.diff(e, t, n);\n          }),\n          (t.characterDiff = void 0);\n        var r = new ((i = n(913)) && i.__esModule ? i : { default: i }).default();\n        t.characterDiff = r;\n      },\n      852: (e, t, n) => {\n        'use strict';\n        var i;\n        Object.defineProperty(t, '__esModule', { value: !0 }),\n          (t.diffCss = function (e, t, n) {\n            return r.diff(e, t, n);\n          }),\n          (t.cssDiff = void 0);\n        var r = new ((i = n(913)) && i.__esModule ? i : { default: i }).default();\n        (t.cssDiff = r),\n          (r.tokenize = function (e) {\n            return e.split(/([{}:;,]|\\s+)/);\n          });\n      },\n      276: (e, t, n) => {\n        'use strict';\n        Object.defineProperty(t, '__esModule', { value: !0 }),\n          (t.diffJson = function (e, t, n) {\n            return l.diff(e, t, n);\n          }),\n          (t.canonicalize = c),\n          (t.jsonDiff = void 0);\n        var i,\n          r = (i = n(913)) && i.__esModule ? i : { default: i },\n          s = n(187);\n        function o(e) {\n          return (\n            (o =\n              'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator\n                ? function (e) {\n                    return typeof e;\n                  }\n                : function (e) {\n                    return e && 'function' == typeof Symbol && e.constructor === Symbol && e !== Symbol.prototype\n                      ? 'symbol'\n                      : typeof e;\n                  }),\n            o(e)\n          );\n        }\n        var a = Object.prototype.toString,\n          l = new r.default();\n        function c(e, t, n, i, r) {\n          var s, l;\n          for (t = t || [], n = n || [], i && (e = i(r, e)), s = 0; s < t.length; s += 1) if (t[s] === e) return n[s];\n          if ('[object Array]' === a.call(e)) {\n            for (t.push(e), l = new Array(e.length), n.push(l), s = 0; s < e.length; s += 1) l[s] = c(e[s], t, n, i, r);\n            return t.pop(), n.pop(), l;\n          }\n          if ((e && e.toJSON && (e = e.toJSON()), 'object' === o(e) && null !== e)) {\n            t.push(e), (l = {}), n.push(l);\n            var d,\n              f = [];\n            for (d in e) e.hasOwnProperty(d) && f.push(d);\n            for (f.sort(), s = 0; s < f.length; s += 1) l[(d = f[s])] = c(e[d], t, n, i, d);\n            t.pop(), n.pop();\n          } else l = e;\n          return l;\n        }\n        (t.jsonDiff = l),\n          (l.useLongestToken = !0),\n          (l.tokenize = s.lineDiff.tokenize),\n          (l.castInput = function (e) {\n            var t = this.options,\n              n = t.undefinedReplacement,\n              i = t.stringifyReplacer,\n              r =\n                void 0 === i\n                  ? function (e, t) {\n                      return void 0 === t ? n : t;\n                    }\n                  : i;\n            return 'string' == typeof e ? e : JSON.stringify(c(e, null, null, r), r, '  ');\n          }),\n          (l.equals = function (e, t) {\n            return r.default.prototype.equals.call(l, e.replace(/,([\\r\\n])/g, '$1'), t.replace(/,([\\r\\n])/g, '$1'));\n          });\n      },\n      187: (e, t, n) => {\n        'use strict';\n        Object.defineProperty(t, '__esModule', { value: !0 }),\n          (t.diffLines = function (e, t, n) {\n            return o.diff(e, t, n);\n          }),\n          (t.diffTrimmedLines = function (e, t, n) {\n            var i = (0, s.generateOptions)(n, { ignoreWhitespace: !0 });\n            return o.diff(e, t, i);\n          }),\n          (t.lineDiff = void 0);\n        var i,\n          r = (i = n(913)) && i.__esModule ? i : { default: i },\n          s = n(9),\n          o = new r.default();\n        (t.lineDiff = o),\n          (o.tokenize = function (e) {\n            var t = [],\n              n = e.split(/(\\n|\\r\\n)/);\n            n[n.length - 1] || n.pop();\n            for (var i = 0; i < n.length; i++) {\n              var r = n[i];\n              i % 2 && !this.options.newlineIsToken\n                ? (t[t.length - 1] += r)\n                : (this.options.ignoreWhitespace && (r = r.trim()), t.push(r));\n            }\n            return t;\n          });\n      },\n      146: (e, t, n) => {\n        'use strict';\n        var i;\n        Object.defineProperty(t, '__esModule', { value: !0 }),\n          (t.diffSentences = function (e, t, n) {\n            return r.diff(e, t, n);\n          }),\n          (t.sentenceDiff = void 0);\n        var r = new ((i = n(913)) && i.__esModule ? i : { default: i }).default();\n        (t.sentenceDiff = r),\n          (r.tokenize = function (e) {\n            return e.split(/(\\S.+?[.!?])(?=\\s+|$)/);\n          });\n      },\n      303: (e, t, n) => {\n        'use strict';\n        Object.defineProperty(t, '__esModule', { value: !0 }),\n          (t.diffWords = function (e, t, n) {\n            return (n = (0, s.generateOptions)(n, { ignoreWhitespace: !0 })), l.diff(e, t, n);\n          }),\n          (t.diffWordsWithSpace = function (e, t, n) {\n            return l.diff(e, t, n);\n          }),\n          (t.wordDiff = void 0);\n        var i,\n          r = (i = n(913)) && i.__esModule ? i : { default: i },\n          s = n(9),\n          o = /^[A-Za-z\\xC0-\\u02C6\\u02C8-\\u02D7\\u02DE-\\u02FF\\u1E00-\\u1EFF]+$/,\n          a = /\\S/,\n          l = new r.default();\n        (t.wordDiff = l),\n          (l.equals = function (e, t) {\n            return (\n              this.options.ignoreCase && ((e = e.toLowerCase()), (t = t.toLowerCase())),\n              e === t || (this.options.ignoreWhitespace && !a.test(e) && !a.test(t))\n            );\n          }),\n          (l.tokenize = function (e) {\n            for (var t = e.split(/([^\\S\\r\\n]+|[()[\\]{}'\"\\r\\n]|\\b)/), n = 0; n < t.length - 1; n++)\n              !t[n + 1] &&\n                t[n + 2] &&\n                o.test(t[n]) &&\n                o.test(t[n + 2]) &&\n                ((t[n] += t[n + 2]), t.splice(n + 1, 2), n--);\n            return t;\n          });\n      },\n      785: (e, t, n) => {\n        'use strict';\n        Object.defineProperty(t, '__esModule', { value: !0 }),\n          Object.defineProperty(t, 'Diff', {\n            enumerable: !0,\n            get: function () {\n              return r.default;\n            }\n          }),\n          Object.defineProperty(t, 'diffChars', {\n            enumerable: !0,\n            get: function () {\n              return s.diffChars;\n            }\n          }),\n          Object.defineProperty(t, 'diffWords', {\n            enumerable: !0,\n            get: function () {\n              return o.diffWords;\n            }\n          }),\n          Object.defineProperty(t, 'diffWordsWithSpace', {\n            enumerable: !0,\n            get: function () {\n              return o.diffWordsWithSpace;\n            }\n          }),\n          Object.defineProperty(t, 'diffLines', {\n            enumerable: !0,\n            get: function () {\n              return a.diffLines;\n            }\n          }),\n          Object.defineProperty(t, 'diffTrimmedLines', {\n            enumerable: !0,\n            get: function () {\n              return a.diffTrimmedLines;\n            }\n          }),\n          Object.defineProperty(t, 'diffSentences', {\n            enumerable: !0,\n            get: function () {\n              return l.diffSentences;\n            }\n          }),\n          Object.defineProperty(t, 'diffCss', {\n            enumerable: !0,\n            get: function () {\n              return c.diffCss;\n            }\n          }),\n          Object.defineProperty(t, 'diffJson', {\n            enumerable: !0,\n            get: function () {\n              return d.diffJson;\n            }\n          }),\n          Object.defineProperty(t, 'canonicalize', {\n            enumerable: !0,\n            get: function () {\n              return d.canonicalize;\n            }\n          }),\n          Object.defineProperty(t, 'diffArrays', {\n            enumerable: !0,\n            get: function () {\n              return f.diffArrays;\n            }\n          }),\n          Object.defineProperty(t, 'applyPatch', {\n            enumerable: !0,\n            get: function () {\n              return u.applyPatch;\n            }\n          }),\n          Object.defineProperty(t, 'applyPatches', {\n            enumerable: !0,\n            get: function () {\n              return u.applyPatches;\n            }\n          }),\n          Object.defineProperty(t, 'parsePatch', {\n            enumerable: !0,\n            get: function () {\n              return h.parsePatch;\n            }\n          }),\n          Object.defineProperty(t, 'merge', {\n            enumerable: !0,\n            get: function () {\n              return p.merge;\n            }\n          }),\n          Object.defineProperty(t, 'structuredPatch', {\n            enumerable: !0,\n            get: function () {\n              return b.structuredPatch;\n            }\n          }),\n          Object.defineProperty(t, 'createTwoFilesPatch', {\n            enumerable: !0,\n            get: function () {\n              return b.createTwoFilesPatch;\n            }\n          }),\n          Object.defineProperty(t, 'createPatch', {\n            enumerable: !0,\n            get: function () {\n              return b.createPatch;\n            }\n          }),\n          Object.defineProperty(t, 'convertChangesToDMP', {\n            enumerable: !0,\n            get: function () {\n              return g.convertChangesToDMP;\n            }\n          }),\n          Object.defineProperty(t, 'convertChangesToXML', {\n            enumerable: !0,\n            get: function () {\n              return m.convertChangesToXML;\n            }\n          });\n        var i,\n          r = (i = n(913)) && i.__esModule ? i : { default: i },\n          s = n(630),\n          o = n(303),\n          a = n(187),\n          l = n(146),\n          c = n(852),\n          d = n(276),\n          f = n(976),\n          u = n(690),\n          h = n(719),\n          p = n(51),\n          b = n(286),\n          g = n(696),\n          m = n(826);\n      },\n      690: (e, t, n) => {\n        'use strict';\n        Object.defineProperty(t, '__esModule', { value: !0 }),\n          (t.applyPatch = o),\n          (t.applyPatches = function (e, t) {\n            'string' == typeof e && (e = (0, r.parsePatch)(e));\n            var n = 0;\n            !(function i() {\n              var r = e[n++];\n              if (!r) return t.complete();\n              t.loadFile(r, function (e, n) {\n                if (e) return t.complete(e);\n                var s = o(n, r, t);\n                t.patched(r, s, function (e) {\n                  if (e) return t.complete(e);\n                  i();\n                });\n              });\n            })();\n          });\n        var i,\n          r = n(719),\n          s = (i = n(169)) && i.__esModule ? i : { default: i };\n        function o(e, t) {\n          var n = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {};\n          if (('string' == typeof t && (t = (0, r.parsePatch)(t)), Array.isArray(t))) {\n            if (t.length > 1) throw new Error('applyPatch only works with a single input.');\n            t = t[0];\n          }\n          var i,\n            o,\n            a = e.split(/\\r\\n|[\\n\\v\\f\\r\\x85]/),\n            l = e.match(/\\r\\n|[\\n\\v\\f\\r\\x85]/g) || [],\n            c = t.hunks,\n            d =\n              n.compareLine ||\n              function (e, t, n, i) {\n                return t === i;\n              },\n            f = 0,\n            u = n.fuzzFactor || 0,\n            h = 0,\n            p = 0;\n          function b(e, t) {\n            for (var n = 0; n < e.lines.length; n++) {\n              var i = e.lines[n],\n                r = i.length > 0 ? i[0] : ' ',\n                s = i.length > 0 ? i.substr(1) : i;\n              if (' ' === r || '-' === r) {\n                if (!d(t + 1, a[t], r, s) && ++f > u) return !1;\n                t++;\n              }\n            }\n            return !0;\n          }\n          for (var g = 0; g < c.length; g++) {\n            for (\n              var m = c[g], v = a.length - m.oldLines, y = 0, w = p + m.oldStart - 1, S = (0, s.default)(w, h, v);\n              void 0 !== y;\n              y = S()\n            )\n              if (b(m, w + y)) {\n                m.offset = p += y;\n                break;\n              }\n            if (void 0 === y) return !1;\n            h = m.offset + m.oldStart + m.oldLines;\n          }\n          for (var L = 0, C = 0; C < c.length; C++) {\n            var x = c[C],\n              O = x.oldStart + x.offset + L - 1;\n            L += x.newLines - x.oldLines;\n            for (var T = 0; T < x.lines.length; T++) {\n              var j = x.lines[T],\n                _ = j.length > 0 ? j[0] : ' ',\n                N = j.length > 0 ? j.substr(1) : j,\n                P = x.linedelimiters[T];\n              if (' ' === _) O++;\n              else if ('-' === _) a.splice(O, 1), l.splice(O, 1);\n              else if ('+' === _) a.splice(O, 0, N), l.splice(O, 0, P), O++;\n              else if ('\\\\' === _) {\n                var E = x.lines[T - 1] ? x.lines[T - 1][0] : null;\n                '+' === E ? (i = !0) : '-' === E && (o = !0);\n              }\n            }\n          }\n          if (i) for (; !a[a.length - 1]; ) a.pop(), l.pop();\n          else o && (a.push(''), l.push('\\n'));\n          for (var M = 0; M < a.length - 1; M++) a[M] = a[M] + l[M];\n          return a.join('');\n        }\n      },\n      286: (e, t, n) => {\n        'use strict';\n        Object.defineProperty(t, '__esModule', { value: !0 }),\n          (t.structuredPatch = o),\n          (t.formatPatch = a),\n          (t.createTwoFilesPatch = l),\n          (t.createPatch = function (e, t, n, i, r, s) {\n            return l(e, e, t, n, i, r, s);\n          });\n        var i = n(187);\n        function r(e) {\n          return (\n            (function (e) {\n              if (Array.isArray(e)) return s(e);\n            })(e) ||\n            (function (e) {\n              if ('undefined' != typeof Symbol && Symbol.iterator in Object(e)) return Array.from(e);\n            })(e) ||\n            (function (e, t) {\n              if (e) {\n                if ('string' == typeof e) return s(e, t);\n                var n = Object.prototype.toString.call(e).slice(8, -1);\n                return (\n                  'Object' === n && e.constructor && (n = e.constructor.name),\n                  'Map' === n || 'Set' === n\n                    ? Array.from(e)\n                    : 'Arguments' === n || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)\n                    ? s(e, t)\n                    : void 0\n                );\n              }\n            })(e) ||\n            (function () {\n              throw new TypeError(\n                'Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.'\n              );\n            })()\n          );\n        }\n        function s(e, t) {\n          (null == t || t > e.length) && (t = e.length);\n          for (var n = 0, i = new Array(t); n < t; n++) i[n] = e[n];\n          return i;\n        }\n        function o(e, t, n, s, o, a, l) {\n          l || (l = {}), void 0 === l.context && (l.context = 4);\n          var c = (0, i.diffLines)(n, s, l);\n          if (c) {\n            c.push({ value: '', lines: [] });\n            for (\n              var d = [],\n                f = 0,\n                u = 0,\n                h = [],\n                p = 1,\n                b = 1,\n                g = function (e) {\n                  var t = c[e],\n                    i = t.lines || t.value.replace(/\\n$/, '').split('\\n');\n                  if (((t.lines = i), t.added || t.removed)) {\n                    var o;\n                    if (!f) {\n                      var a = c[e - 1];\n                      (f = p),\n                        (u = b),\n                        a &&\n                          ((h = l.context > 0 ? v(a.lines.slice(-l.context)) : []), (f -= h.length), (u -= h.length));\n                    }\n                    (o = h).push.apply(\n                      o,\n                      r(\n                        i.map(function (e) {\n                          return (t.added ? '+' : '-') + e;\n                        })\n                      )\n                    ),\n                      t.added ? (b += i.length) : (p += i.length);\n                  } else {\n                    if (f)\n                      if (i.length <= 2 * l.context && e < c.length - 2) {\n                        var g;\n                        (g = h).push.apply(g, r(v(i)));\n                      } else {\n                        var m,\n                          y = Math.min(i.length, l.context);\n                        (m = h).push.apply(m, r(v(i.slice(0, y))));\n                        var w = { oldStart: f, oldLines: p - f + y, newStart: u, newLines: b - u + y, lines: h };\n                        if (e >= c.length - 2 && i.length <= l.context) {\n                          var S = /\\n$/.test(n),\n                            L = /\\n$/.test(s),\n                            C = 0 == i.length && h.length > w.oldLines;\n                          !S && C && n.length > 0 && h.splice(w.oldLines, 0, '\\\\ No newline at end of file'),\n                            ((S || C) && L) || h.push('\\\\ No newline at end of file');\n                        }\n                        d.push(w), (f = 0), (u = 0), (h = []);\n                      }\n                    (p += i.length), (b += i.length);\n                  }\n                },\n                m = 0;\n              m < c.length;\n              m++\n            )\n              g(m);\n            return { oldFileName: e, newFileName: t, oldHeader: o, newHeader: a, hunks: d };\n          }\n          function v(e) {\n            return e.map(function (e) {\n              return ' ' + e;\n            });\n          }\n        }\n        function a(e) {\n          var t = [];\n          e.oldFileName == e.newFileName && t.push('Index: ' + e.oldFileName),\n            t.push('==================================================================='),\n            t.push('--- ' + e.oldFileName + (void 0 === e.oldHeader ? '' : '\\t' + e.oldHeader)),\n            t.push('+++ ' + e.newFileName + (void 0 === e.newHeader ? '' : '\\t' + e.newHeader));\n          for (var n = 0; n < e.hunks.length; n++) {\n            var i = e.hunks[n];\n            0 === i.oldLines && (i.oldStart -= 1),\n              0 === i.newLines && (i.newStart -= 1),\n              t.push('@@ -' + i.oldStart + ',' + i.oldLines + ' +' + i.newStart + ',' + i.newLines + ' @@'),\n              t.push.apply(t, i.lines);\n          }\n          return t.join('\\n') + '\\n';\n        }\n        function l(e, t, n, i, r, s, l) {\n          return a(o(e, t, n, i, r, s, l));\n        }\n      },\n      51: (e, t, n) => {\n        'use strict';\n        Object.defineProperty(t, '__esModule', { value: !0 }),\n          (t.calcLineCount = l),\n          (t.merge = function (e, t, n) {\n            (e = c(e, n)), (t = c(t, n));\n            var i = {};\n            (e.index || t.index) && (i.index = e.index || t.index),\n              (e.newFileName || t.newFileName) &&\n                (d(e)\n                  ? d(t)\n                    ? ((i.oldFileName = f(i, e.oldFileName, t.oldFileName)),\n                      (i.newFileName = f(i, e.newFileName, t.newFileName)),\n                      (i.oldHeader = f(i, e.oldHeader, t.oldHeader)),\n                      (i.newHeader = f(i, e.newHeader, t.newHeader)))\n                    : ((i.oldFileName = e.oldFileName),\n                      (i.newFileName = e.newFileName),\n                      (i.oldHeader = e.oldHeader),\n                      (i.newHeader = e.newHeader))\n                  : ((i.oldFileName = t.oldFileName || e.oldFileName),\n                    (i.newFileName = t.newFileName || e.newFileName),\n                    (i.oldHeader = t.oldHeader || e.oldHeader),\n                    (i.newHeader = t.newHeader || e.newHeader))),\n              (i.hunks = []);\n            for (var r = 0, s = 0, o = 0, a = 0; r < e.hunks.length || s < t.hunks.length; ) {\n              var l = e.hunks[r] || { oldStart: 1 / 0 },\n                b = t.hunks[s] || { oldStart: 1 / 0 };\n              if (u(l, b)) i.hunks.push(h(l, o)), r++, (a += l.newLines - l.oldLines);\n              else if (u(b, l)) i.hunks.push(h(b, a)), s++, (o += b.newLines - b.oldLines);\n              else {\n                var g = {\n                  oldStart: Math.min(l.oldStart, b.oldStart),\n                  oldLines: 0,\n                  newStart: Math.min(l.newStart + o, b.oldStart + a),\n                  newLines: 0,\n                  lines: []\n                };\n                p(g, l.oldStart, l.lines, b.oldStart, b.lines), s++, r++, i.hunks.push(g);\n              }\n            }\n            return i;\n          });\n        var i = n(286),\n          r = n(719),\n          s = n(780);\n        function o(e) {\n          return (\n            (function (e) {\n              if (Array.isArray(e)) return a(e);\n            })(e) ||\n            (function (e) {\n              if ('undefined' != typeof Symbol && Symbol.iterator in Object(e)) return Array.from(e);\n            })(e) ||\n            (function (e, t) {\n              if (e) {\n                if ('string' == typeof e) return a(e, t);\n                var n = Object.prototype.toString.call(e).slice(8, -1);\n                return (\n                  'Object' === n && e.constructor && (n = e.constructor.name),\n                  'Map' === n || 'Set' === n\n                    ? Array.from(e)\n                    : 'Arguments' === n || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)\n                    ? a(e, t)\n                    : void 0\n                );\n              }\n            })(e) ||\n            (function () {\n              throw new TypeError(\n                'Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.'\n              );\n            })()\n          );\n        }\n        function a(e, t) {\n          (null == t || t > e.length) && (t = e.length);\n          for (var n = 0, i = new Array(t); n < t; n++) i[n] = e[n];\n          return i;\n        }\n        function l(e) {\n          var t = C(e.lines),\n            n = t.oldLines,\n            i = t.newLines;\n          void 0 !== n ? (e.oldLines = n) : delete e.oldLines, void 0 !== i ? (e.newLines = i) : delete e.newLines;\n        }\n        function c(e, t) {\n          if ('string' == typeof e) {\n            if (/^@@/m.test(e) || /^Index:/m.test(e)) return (0, r.parsePatch)(e)[0];\n            if (!t) throw new Error('Must provide a base reference or pass in a patch');\n            return (0, i.structuredPatch)(void 0, void 0, t, e);\n          }\n          return e;\n        }\n        function d(e) {\n          return e.newFileName && e.newFileName !== e.oldFileName;\n        }\n        function f(e, t, n) {\n          return t === n ? t : ((e.conflict = !0), { mine: t, theirs: n });\n        }\n        function u(e, t) {\n          return e.oldStart < t.oldStart && e.oldStart + e.oldLines < t.oldStart;\n        }\n        function h(e, t) {\n          return {\n            oldStart: e.oldStart,\n            oldLines: e.oldLines,\n            newStart: e.newStart + t,\n            newLines: e.newLines,\n            lines: e.lines\n          };\n        }\n        function p(e, t, n, i, r) {\n          var s = { offset: t, lines: n, index: 0 },\n            a = { offset: i, lines: r, index: 0 };\n          for (v(e, s, a), v(e, a, s); s.index < s.lines.length && a.index < a.lines.length; ) {\n            var c = s.lines[s.index],\n              d = a.lines[a.index];\n            if (('-' !== c[0] && '+' !== c[0]) || ('-' !== d[0] && '+' !== d[0]))\n              if ('+' === c[0] && ' ' === d[0]) {\n                var f;\n                (f = e.lines).push.apply(f, o(w(s)));\n              } else if ('+' === d[0] && ' ' === c[0]) {\n                var u;\n                (u = e.lines).push.apply(u, o(w(a)));\n              } else\n                '-' === c[0] && ' ' === d[0]\n                  ? g(e, s, a)\n                  : '-' === d[0] && ' ' === c[0]\n                  ? g(e, a, s, !0)\n                  : c === d\n                  ? (e.lines.push(c), s.index++, a.index++)\n                  : m(e, w(s), w(a));\n            else b(e, s, a);\n          }\n          y(e, s), y(e, a), l(e);\n        }\n        function b(e, t, n) {\n          var i = w(t),\n            r = w(n);\n          if (S(i) && S(r)) {\n            var a, l;\n            if ((0, s.arrayStartsWith)(i, r) && L(n, i, i.length - r.length))\n              return void (a = e.lines).push.apply(a, o(i));\n            if ((0, s.arrayStartsWith)(r, i) && L(t, r, r.length - i.length))\n              return void (l = e.lines).push.apply(l, o(r));\n          } else if ((0, s.arrayEqual)(i, r)) {\n            var c;\n            return void (c = e.lines).push.apply(c, o(i));\n          }\n          m(e, i, r);\n        }\n        function g(e, t, n, i) {\n          var r,\n            s = w(t),\n            a = (function (e, t) {\n              for (var n = [], i = [], r = 0, s = !1, o = !1; r < t.length && e.index < e.lines.length; ) {\n                var a = e.lines[e.index],\n                  l = t[r];\n                if ('+' === l[0]) break;\n                if (((s = s || ' ' !== a[0]), i.push(l), r++, '+' === a[0]))\n                  for (o = !0; '+' === a[0]; ) n.push(a), (a = e.lines[++e.index]);\n                l.substr(1) === a.substr(1) ? (n.push(a), e.index++) : (o = !0);\n              }\n              if (('+' === (t[r] || '')[0] && s && (o = !0), o)) return n;\n              for (; r < t.length; ) i.push(t[r++]);\n              return { merged: i, changes: n };\n            })(n, s);\n          a.merged ? (r = e.lines).push.apply(r, o(a.merged)) : m(e, i ? a : s, i ? s : a);\n        }\n        function m(e, t, n) {\n          (e.conflict = !0), e.lines.push({ conflict: !0, mine: t, theirs: n });\n        }\n        function v(e, t, n) {\n          for (; t.offset < n.offset && t.index < t.lines.length; ) {\n            var i = t.lines[t.index++];\n            e.lines.push(i), t.offset++;\n          }\n        }\n        function y(e, t) {\n          for (; t.index < t.lines.length; ) {\n            var n = t.lines[t.index++];\n            e.lines.push(n);\n          }\n        }\n        function w(e) {\n          for (var t = [], n = e.lines[e.index][0]; e.index < e.lines.length; ) {\n            var i = e.lines[e.index];\n            if (('-' === n && '+' === i[0] && (n = '+'), n !== i[0])) break;\n            t.push(i), e.index++;\n          }\n          return t;\n        }\n        function S(e) {\n          return e.reduce(function (e, t) {\n            return e && '-' === t[0];\n          }, !0);\n        }\n        function L(e, t, n) {\n          for (var i = 0; i < n; i++) {\n            var r = t[t.length - n + i].substr(1);\n            if (e.lines[e.index + i] !== ' ' + r) return !1;\n          }\n          return (e.index += n), !0;\n        }\n        function C(e) {\n          var t = 0,\n            n = 0;\n          return (\n            e.forEach(function (e) {\n              if ('string' != typeof e) {\n                var i = C(e.mine),\n                  r = C(e.theirs);\n                void 0 !== t && (i.oldLines === r.oldLines ? (t += i.oldLines) : (t = void 0)),\n                  void 0 !== n && (i.newLines === r.newLines ? (n += i.newLines) : (n = void 0));\n              } else void 0 === n || ('+' !== e[0] && ' ' !== e[0]) || n++, void 0 === t || ('-' !== e[0] && ' ' !== e[0]) || t++;\n            }),\n            { oldLines: t, newLines: n }\n          );\n        }\n      },\n      719: (e, t) => {\n        'use strict';\n        Object.defineProperty(t, '__esModule', { value: !0 }),\n          (t.parsePatch = function (e) {\n            var t = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {},\n              n = e.split(/\\r\\n|[\\n\\v\\f\\r\\x85]/),\n              i = e.match(/\\r\\n|[\\n\\v\\f\\r\\x85]/g) || [],\n              r = [],\n              s = 0;\n            function o() {\n              var e = {};\n              for (r.push(e); s < n.length; ) {\n                var i = n[s];\n                if (/^(\\-\\-\\-|\\+\\+\\+|@@)\\s/.test(i)) break;\n                var o = /^(?:Index:|diff(?: -r \\w+)+)\\s+(.+?)\\s*$/.exec(i);\n                o && (e.index = o[1]), s++;\n              }\n              for (a(e), a(e), e.hunks = []; s < n.length; ) {\n                var c = n[s];\n                if (/^(Index:|diff|\\-\\-\\-|\\+\\+\\+)\\s/.test(c)) break;\n                if (/^@@/.test(c)) e.hunks.push(l());\n                else {\n                  if (c && t.strict) throw new Error('Unknown line ' + (s + 1) + ' ' + JSON.stringify(c));\n                  s++;\n                }\n              }\n            }\n            function a(e) {\n              var t = /^(---|\\+\\+\\+)\\s+(.*)$/.exec(n[s]);\n              if (t) {\n                var i = '---' === t[1] ? 'old' : 'new',\n                  r = t[2].split('\\t', 2),\n                  o = r[0].replace(/\\\\\\\\/g, '\\\\');\n                /^\".*\"$/.test(o) && (o = o.substr(1, o.length - 2)),\n                  (e[i + 'FileName'] = o),\n                  (e[i + 'Header'] = (r[1] || '').trim()),\n                  s++;\n              }\n            }\n            function l() {\n              var e = s,\n                r = n[s++].split(/@@ -(\\d+)(?:,(\\d+))? \\+(\\d+)(?:,(\\d+))? @@/),\n                o = {\n                  oldStart: +r[1],\n                  oldLines: void 0 === r[2] ? 1 : +r[2],\n                  newStart: +r[3],\n                  newLines: void 0 === r[4] ? 1 : +r[4],\n                  lines: [],\n                  linedelimiters: []\n                };\n              0 === o.oldLines && (o.oldStart += 1), 0 === o.newLines && (o.newStart += 1);\n              for (\n                var a = 0, l = 0;\n                s < n.length &&\n                !(\n                  0 === n[s].indexOf('--- ') &&\n                  s + 2 < n.length &&\n                  0 === n[s + 1].indexOf('+++ ') &&\n                  0 === n[s + 2].indexOf('@@')\n                );\n                s++\n              ) {\n                var c = 0 == n[s].length && s != n.length - 1 ? ' ' : n[s][0];\n                if ('+' !== c && '-' !== c && ' ' !== c && '\\\\' !== c) break;\n                o.lines.push(n[s]),\n                  o.linedelimiters.push(i[s] || '\\n'),\n                  '+' === c ? a++ : '-' === c ? l++ : ' ' === c && (a++, l++);\n              }\n              if ((a || 1 !== o.newLines || (o.newLines = 0), l || 1 !== o.oldLines || (o.oldLines = 0), t.strict)) {\n                if (a !== o.newLines) throw new Error('Added line count did not match for hunk at line ' + (e + 1));\n                if (l !== o.oldLines) throw new Error('Removed line count did not match for hunk at line ' + (e + 1));\n              }\n              return o;\n            }\n            for (; s < n.length; ) o();\n            return r;\n          });\n      },\n      780: (e, t) => {\n        'use strict';\n        function n(e, t) {\n          if (t.length > e.length) return !1;\n          for (var n = 0; n < t.length; n++) if (t[n] !== e[n]) return !1;\n          return !0;\n        }\n        Object.defineProperty(t, '__esModule', { value: !0 }),\n          (t.arrayEqual = function (e, t) {\n            return e.length === t.length && n(e, t);\n          }),\n          (t.arrayStartsWith = n);\n      },\n      169: (e, t) => {\n        'use strict';\n        Object.defineProperty(t, '__esModule', { value: !0 }),\n          (t.default = function (e, t, n) {\n            var i = !0,\n              r = !1,\n              s = !1,\n              o = 1;\n            return function a() {\n              if (i && !s) {\n                if ((r ? o++ : (i = !1), e + o <= n)) return o;\n                s = !0;\n              }\n              if (!r) return s || (i = !0), t <= e - o ? -o++ : ((r = !0), a());\n            };\n          });\n      },\n      9: (e, t) => {\n        'use strict';\n        Object.defineProperty(t, '__esModule', { value: !0 }),\n          (t.generateOptions = function (e, t) {\n            if ('function' == typeof e) t.callback = e;\n            else if (e) for (var n in e) e.hasOwnProperty(n) && (t[n] = e[n]);\n            return t;\n          });\n      },\n      397: (e, t) => {\n        !(function (e) {\n          var t = /\\S/,\n            n = /\\\"/g,\n            i = /\\n/g,\n            r = /\\r/g,\n            s = /\\\\/g,\n            o = /\\u2028/,\n            a = /\\u2029/;\n          function l(e) {\n            return e.trim ? e.trim() : e.replace(/^\\s*|\\s*$/g, '');\n          }\n          function c(e, t, n) {\n            if (t.charAt(n) != e.charAt(0)) return !1;\n            for (var i = 1, r = e.length; i < r; i++) if (t.charAt(n + i) != e.charAt(i)) return !1;\n            return !0;\n          }\n          (e.tags = { '#': 1, '^': 2, '<': 3, $: 4, '/': 5, '!': 6, '>': 7, '=': 8, _v: 9, '{': 10, '&': 11, _t: 12 }),\n            (e.scan = function (n, i) {\n              var r,\n                s = n.length,\n                o = 0,\n                a = null,\n                d = null,\n                f = '',\n                u = [],\n                h = !1,\n                p = 0,\n                b = 0,\n                g = '{{',\n                m = '}}';\n              function v() {\n                f.length > 0 && (u.push({ tag: '_t', text: new String(f) }), (f = ''));\n              }\n              function y(n, i) {\n                if (\n                  (v(),\n                  n &&\n                    (function () {\n                      for (var n = !0, i = b; i < u.length; i++)\n                        if (!(n = e.tags[u[i].tag] < e.tags._v || ('_t' == u[i].tag && null === u[i].text.match(t))))\n                          return !1;\n                      return n;\n                    })())\n                )\n                  for (var r, s = b; s < u.length; s++)\n                    u[s].text && ((r = u[s + 1]) && '>' == r.tag && (r.indent = u[s].text.toString()), u.splice(s, 1));\n                else i || u.push({ tag: '\\n' });\n                (h = !1), (b = u.length);\n              }\n              function w(e, t) {\n                var n = '=' + m,\n                  i = e.indexOf(n, t),\n                  r = l(e.substring(e.indexOf('=', t) + 1, i)).split(' ');\n                return (g = r[0]), (m = r[r.length - 1]), i + n.length - 1;\n              }\n              for (i && ((i = i.split(' ')), (g = i[0]), (m = i[1])), p = 0; p < s; p++)\n                0 == o\n                  ? c(g, n, p)\n                    ? (--p, v(), (o = 1))\n                    : '\\n' == n.charAt(p)\n                    ? y(h)\n                    : (f += n.charAt(p))\n                  : 1 == o\n                  ? ((p += g.length - 1),\n                    '=' == (a = (d = e.tags[n.charAt(p + 1)]) ? n.charAt(p + 1) : '_v')\n                      ? ((p = w(n, p)), (o = 0))\n                      : (d && p++, (o = 2)),\n                    (h = p))\n                  : c(m, n, p)\n                  ? (u.push({ tag: a, n: l(f), otag: g, ctag: m, i: '/' == a ? h - g.length : p + m.length }),\n                    (f = ''),\n                    (p += m.length - 1),\n                    (o = 0),\n                    '{' == a &&\n                      ('}}' == m\n                        ? p++\n                        : '}' === (r = u[u.length - 1]).n.substr(r.n.length - 1) &&\n                          (r.n = r.n.substring(0, r.n.length - 1))))\n                  : (f += n.charAt(p));\n              return y(h, !0), u;\n            });\n          var d = { _t: !0, '\\n': !0, $: !0, '/': !0 };\n          function f(t, n, i, r) {\n            var s,\n              o = [],\n              a = null,\n              l = null;\n            for (s = i[i.length - 1]; t.length > 0; ) {\n              if (((l = t.shift()), s && '<' == s.tag && !(l.tag in d)))\n                throw new Error('Illegal content in < super tag.');\n              if (e.tags[l.tag] <= e.tags.$ || u(l, r)) i.push(l), (l.nodes = f(t, l.tag, i, r));\n              else {\n                if ('/' == l.tag) {\n                  if (0 === i.length) throw new Error('Closing tag without opener: /' + l.n);\n                  if (((a = i.pop()), l.n != a.n && !h(l.n, a.n, r)))\n                    throw new Error('Nesting error: ' + a.n + ' vs. ' + l.n);\n                  return (a.end = l.i), o;\n                }\n                '\\n' == l.tag && (l.last = 0 == t.length || '\\n' == t[0].tag);\n              }\n              o.push(l);\n            }\n            if (i.length > 0) throw new Error('missing closing tag: ' + i.pop().n);\n            return o;\n          }\n          function u(e, t) {\n            for (var n = 0, i = t.length; n < i; n++) if (t[n].o == e.n) return (e.tag = '#'), !0;\n          }\n          function h(e, t, n) {\n            for (var i = 0, r = n.length; i < r; i++) if (n[i].c == e && n[i].o == t) return !0;\n          }\n          function p(e) {\n            var t = [];\n            for (var n in e.partials)\n              t.push('\"' + g(n) + '\":{name:\"' + g(e.partials[n].name) + '\", ' + p(e.partials[n]) + '}');\n            return (\n              'partials: {' +\n              t.join(',') +\n              '}, subs: ' +\n              (function (e) {\n                var t = [];\n                for (var n in e) t.push('\"' + g(n) + '\": function(c,p,t,i) {' + e[n] + '}');\n                return '{ ' + t.join(',') + ' }';\n              })(e.subs)\n            );\n          }\n          e.stringify = function (t, n, i) {\n            return '{code: function (c,p,i) { ' + e.wrapMain(t.code) + ' },' + p(t) + '}';\n          };\n          var b = 0;\n          function g(e) {\n            return e\n              .replace(s, '\\\\\\\\')\n              .replace(n, '\\\\\"')\n              .replace(i, '\\\\n')\n              .replace(r, '\\\\r')\n              .replace(o, '\\\\u2028')\n              .replace(a, '\\\\u2029');\n          }\n          function m(e) {\n            return ~e.indexOf('.') ? 'd' : 'f';\n          }\n          function v(e, t) {\n            var n = '<' + (t.prefix || '') + e.n + b++;\n            return (\n              (t.partials[n] = { name: e.n, partials: {} }),\n              (t.code += 't.b(t.rp(\"' + g(n) + '\",c,p,\"' + (e.indent || '') + '\"));'),\n              n\n            );\n          }\n          function y(e, t) {\n            t.code += 't.b(t.t(t.' + m(e.n) + '(\"' + g(e.n) + '\",c,p,0)));';\n          }\n          function w(e) {\n            return 't.b(' + e + ');';\n          }\n          (e.generate = function (t, n, i) {\n            b = 0;\n            var r = { code: '', subs: {}, partials: {} };\n            return e.walk(t, r), i.asString ? this.stringify(r, n, i) : this.makeTemplate(r, n, i);\n          }),\n            (e.wrapMain = function (e) {\n              return 'var t=this;t.b(i=i||\"\");' + e + 'return t.fl();';\n            }),\n            (e.template = e.Template),\n            (e.makeTemplate = function (e, t, n) {\n              var i = this.makePartials(e);\n              return (i.code = new Function('c', 'p', 'i', this.wrapMain(e.code))), new this.template(i, t, this, n);\n            }),\n            (e.makePartials = function (e) {\n              var t,\n                n = { subs: {}, partials: e.partials, name: e.name };\n              for (t in n.partials) n.partials[t] = this.makePartials(n.partials[t]);\n              for (t in e.subs) n.subs[t] = new Function('c', 'p', 't', 'i', e.subs[t]);\n              return n;\n            }),\n            (e.codegen = {\n              '#': function (t, n) {\n                (n.code +=\n                  'if(t.s(t.' +\n                  m(t.n) +\n                  '(\"' +\n                  g(t.n) +\n                  '\",c,p,1),c,p,0,' +\n                  t.i +\n                  ',' +\n                  t.end +\n                  ',\"' +\n                  t.otag +\n                  ' ' +\n                  t.ctag +\n                  '\")){t.rs(c,p,function(c,p,t){'),\n                  e.walk(t.nodes, n),\n                  (n.code += '});c.pop();}');\n              },\n              '^': function (t, n) {\n                (n.code += 'if(!t.s(t.' + m(t.n) + '(\"' + g(t.n) + '\",c,p,1),c,p,1,0,0,\"\")){'),\n                  e.walk(t.nodes, n),\n                  (n.code += '};');\n              },\n              '>': v,\n              '<': function (t, n) {\n                var i = { partials: {}, code: '', subs: {}, inPartial: !0 };\n                e.walk(t.nodes, i);\n                var r = n.partials[v(t, n)];\n                (r.subs = i.subs), (r.partials = i.partials);\n              },\n              $: function (t, n) {\n                var i = { subs: {}, code: '', partials: n.partials, prefix: t.n };\n                e.walk(t.nodes, i), (n.subs[t.n] = i.code), n.inPartial || (n.code += 't.sub(\"' + g(t.n) + '\",c,p,i);');\n              },\n              '\\n': function (e, t) {\n                t.code += w('\"\\\\n\"' + (e.last ? '' : ' + i'));\n              },\n              _v: function (e, t) {\n                t.code += 't.b(t.v(t.' + m(e.n) + '(\"' + g(e.n) + '\",c,p,0)));';\n              },\n              _t: function (e, t) {\n                t.code += w('\"' + g(e.text) + '\"');\n              },\n              '{': y,\n              '&': y\n            }),\n            (e.walk = function (t, n) {\n              for (var i, r = 0, s = t.length; r < s; r++) (i = e.codegen[t[r].tag]) && i(t[r], n);\n              return n;\n            }),\n            (e.parse = function (e, t, n) {\n              return f(e, 0, [], (n = n || {}).sectionTags || []);\n            }),\n            (e.cache = {}),\n            (e.cacheKey = function (e, t) {\n              return [e, !!t.asString, !!t.disableLambda, t.delimiters, !!t.modelGet].join('||');\n            }),\n            (e.compile = function (t, n) {\n              n = n || {};\n              var i = e.cacheKey(t, n),\n                r = this.cache[i];\n              if (r) {\n                var s = r.partials;\n                for (var o in s) delete s[o].instance;\n                return r;\n              }\n              return (r = this.generate(this.parse(this.scan(t, n.delimiters), t, n), t, n)), (this.cache[i] = r);\n            });\n        })(t);\n      },\n      485: (e, t, n) => {\n        var i = n(397);\n        (i.Template = n(882).Template), (i.template = i.Template), (e.exports = i);\n      },\n      882: (e, t) => {\n        !(function (e) {\n          function t(e, t, n) {\n            var i;\n            return (\n              t &&\n                'object' == typeof t &&\n                (void 0 !== t[e] ? (i = t[e]) : n && t.get && 'function' == typeof t.get && (i = t.get(e))),\n              i\n            );\n          }\n          (e.Template = function (e, t, n, i) {\n            (e = e || {}),\n              (this.r = e.code || this.r),\n              (this.c = n),\n              (this.options = i || {}),\n              (this.text = t || ''),\n              (this.partials = e.partials || {}),\n              (this.subs = e.subs || {}),\n              (this.buf = '');\n          }),\n            (e.Template.prototype = {\n              r: function (e, t, n) {\n                return '';\n              },\n              v: function (e) {\n                return (\n                  (e = l(e)),\n                  a.test(e)\n                    ? e\n                        .replace(n, '&amp;')\n                        .replace(i, '&lt;')\n                        .replace(r, '&gt;')\n                        .replace(s, '&#39;')\n                        .replace(o, '&quot;')\n                    : e\n                );\n              },\n              t: l,\n              render: function (e, t, n) {\n                return this.ri([e], t || {}, n);\n              },\n              ri: function (e, t, n) {\n                return this.r(e, t, n);\n              },\n              ep: function (e, t) {\n                var n = this.partials[e],\n                  i = t[n.name];\n                if (n.instance && n.base == i) return n.instance;\n                if ('string' == typeof i) {\n                  if (!this.c) throw new Error('No compiler available.');\n                  i = this.c.compile(i, this.options);\n                }\n                if (!i) return null;\n                if (((this.partials[e].base = i), n.subs)) {\n                  for (key in (t.stackText || (t.stackText = {}), n.subs))\n                    t.stackText[key] ||\n                      (t.stackText[key] =\n                        void 0 !== this.activeSub && t.stackText[this.activeSub]\n                          ? t.stackText[this.activeSub]\n                          : this.text);\n                  i = (function (e, t, n, i, r, s) {\n                    function o() {}\n                    function a() {}\n                    var l;\n                    (o.prototype = e), (a.prototype = e.subs);\n                    var c = new o();\n                    for (l in ((c.subs = new a()),\n                    (c.subsText = {}),\n                    (c.buf = ''),\n                    (i = i || {}),\n                    (c.stackSubs = i),\n                    (c.subsText = s),\n                    t))\n                      i[l] || (i[l] = t[l]);\n                    for (l in i) c.subs[l] = i[l];\n                    for (l in ((r = r || {}), (c.stackPartials = r), n)) r[l] || (r[l] = n[l]);\n                    for (l in r) c.partials[l] = r[l];\n                    return c;\n                  })(i, n.subs, n.partials, this.stackSubs, this.stackPartials, t.stackText);\n                }\n                return (this.partials[e].instance = i), i;\n              },\n              rp: function (e, t, n, i) {\n                var r = this.ep(e, n);\n                return r ? r.ri(t, n, i) : '';\n              },\n              rs: function (e, t, n) {\n                var i = e[e.length - 1];\n                if (c(i)) for (var r = 0; r < i.length; r++) e.push(i[r]), n(e, t, this), e.pop();\n                else n(e, t, this);\n              },\n              s: function (e, t, n, i, r, s, o) {\n                var a;\n                return (\n                  (!c(e) || 0 !== e.length) &&\n                  ('function' == typeof e && (e = this.ms(e, t, n, i, r, s, o)),\n                  (a = !!e),\n                  !i && a && t && t.push('object' == typeof e ? e : t[t.length - 1]),\n                  a)\n                );\n              },\n              d: function (e, n, i, r) {\n                var s,\n                  o = e.split('.'),\n                  a = this.f(o[0], n, i, r),\n                  l = this.options.modelGet,\n                  d = null;\n                if ('.' === e && c(n[n.length - 2])) a = n[n.length - 1];\n                else for (var f = 1; f < o.length; f++) void 0 !== (s = t(o[f], a, l)) ? ((d = a), (a = s)) : (a = '');\n                return !(r && !a) && (r || 'function' != typeof a || (n.push(d), (a = this.mv(a, n, i)), n.pop()), a);\n              },\n              f: function (e, n, i, r) {\n                for (var s = !1, o = !1, a = this.options.modelGet, l = n.length - 1; l >= 0; l--)\n                  if (void 0 !== (s = t(e, n[l], a))) {\n                    o = !0;\n                    break;\n                  }\n                return o ? (r || 'function' != typeof s || (s = this.mv(s, n, i)), s) : !r && '';\n              },\n              ls: function (e, t, n, i, r) {\n                var s = this.options.delimiters;\n                return (\n                  (this.options.delimiters = r),\n                  this.b(this.ct(l(e.call(t, i)), t, n)),\n                  (this.options.delimiters = s),\n                  !1\n                );\n              },\n              ct: function (e, t, n) {\n                if (this.options.disableLambda) throw new Error('Lambda features disabled.');\n                return this.c.compile(e, this.options).render(t, n);\n              },\n              b: function (e) {\n                this.buf += e;\n              },\n              fl: function () {\n                var e = this.buf;\n                return (this.buf = ''), e;\n              },\n              ms: function (e, t, n, i, r, s, o) {\n                var a,\n                  l = t[t.length - 1],\n                  c = e.call(l);\n                return 'function' == typeof c\n                  ? !!i ||\n                      ((a =\n                        this.activeSub && this.subsText && this.subsText[this.activeSub]\n                          ? this.subsText[this.activeSub]\n                          : this.text),\n                      this.ls(c, l, n, a.substring(r, s), o))\n                  : c;\n              },\n              mv: function (e, t, n) {\n                var i = t[t.length - 1],\n                  r = e.call(i);\n                return 'function' == typeof r ? this.ct(l(r.call(i)), i, n) : r;\n              },\n              sub: function (e, t, n, i) {\n                var r = this.subs[e];\n                r && ((this.activeSub = e), r(t, n, this, i), (this.activeSub = !1));\n              }\n            });\n          var n = /&/g,\n            i = /</g,\n            r = />/g,\n            s = /\\'/g,\n            o = /\\\"/g,\n            a = /[&<>\\\"\\']/;\n          function l(e) {\n            return String(null == e ? '' : e);\n          }\n          var c =\n            Array.isArray ||\n            function (e) {\n              return '[object Array]' === Object.prototype.toString.call(e);\n            };\n        })(t);\n      },\n      468: (e, t, n) => {\n        'use strict';\n        Object.defineProperty(t, '__esModule', { value: !0 }), (t.parse = void 0);\n        const i = n(699),\n          r = n(593);\n        function s(e, t) {\n          const n = e.split('.');\n          return n.length > 1 ? n[n.length - 1] : t;\n        }\n        function o(e, t) {\n          return t.reduce((t, n) => t || e.startsWith(n), !1);\n        }\n        const a = ['a/', 'b/', 'i/', 'w/', 'c/', 'o/'];\n        function l(e, t, n) {\n          const i = void 0 !== n ? [...a, n] : a,\n            s = t ? new RegExp(`^${(0, r.escapeForRegExp)(t)} \"?(.+?)\"?$`) : new RegExp('^\"?(.+?)\"?$'),\n            [, o = ''] = s.exec(e) || [],\n            l = i.find((e) => 0 === o.indexOf(e));\n          return (l ? o.slice(l.length) : o).replace(\n            /\\s+\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)? [+-]\\d{4}.*$/,\n            ''\n          );\n        }\n        t.parse = function (e, t = {}) {\n          const n = [];\n          let r = null,\n            a = null,\n            c = null,\n            d = null,\n            f = null,\n            u = null,\n            h = null;\n          const p = '--- ',\n            b = '+++ ',\n            g = '@@',\n            m = /^old mode (\\d{6})/,\n            v = /^new mode (\\d{6})/,\n            y = /^deleted file mode (\\d{6})/,\n            w = /^new file mode (\\d{6})/,\n            S = /^copy from \"?(.+)\"?/,\n            L = /^copy to \"?(.+)\"?/,\n            C = /^rename from \"?(.+)\"?/,\n            x = /^rename to \"?(.+)\"?/,\n            O = /^similarity index (\\d+)%/,\n            T = /^dissimilarity index (\\d+)%/,\n            j = /^index ([\\da-z]+)\\.\\.([\\da-z]+)\\s*(\\d{6})?/,\n            _ = /^Binary files (.*) and (.*) differ/,\n            N = /^GIT binary patch/,\n            P = /^index ([\\da-z]+),([\\da-z]+)\\.\\.([\\da-z]+)/,\n            E = /^mode (\\d{6}),(\\d{6})\\.\\.(\\d{6})/,\n            M = /^new file mode (\\d{6})/,\n            H = /^deleted file mode (\\d{6}),(\\d{6})/,\n            k = e\n              .replace(/\\\\ No newline at end of file/g, '')\n              .replace(/\\r\\n?/g, '\\n')\n              .split('\\n');\n          function D() {\n            null !== a && null !== r && (r.blocks.push(a), (a = null));\n          }\n          function F() {\n            null !== r &&\n              (r.oldName || null === u || (r.oldName = u),\n              r.newName || null === h || (r.newName = h),\n              r.newName && (n.push(r), (r = null))),\n              (u = null),\n              (h = null);\n          }\n          function I() {\n            D(), F(), (r = { blocks: [], deletedLines: 0, addedLines: 0 });\n          }\n          function A(e) {\n            let t;\n            D(),\n              null !== r &&\n                ((t = /^@@ -(\\d+)(?:,\\d+)? \\+(\\d+)(?:,\\d+)? @@.*/.exec(e))\n                  ? ((r.isCombined = !1), (c = parseInt(t[1], 10)), (f = parseInt(t[2], 10)))\n                  : (t = /^@@@ -(\\d+)(?:,\\d+)? -(\\d+)(?:,\\d+)? \\+(\\d+)(?:,\\d+)? @@@.*/.exec(e))\n                  ? ((r.isCombined = !0), (c = parseInt(t[1], 10)), (d = parseInt(t[2], 10)), (f = parseInt(t[3], 10)))\n                  : (e.startsWith(g) && console.error('Failed to parse lines, starting in 0!'),\n                    (c = 0),\n                    (f = 0),\n                    (r.isCombined = !1))),\n              (a = { lines: [], oldStartLine: c, oldStartLine2: d, newStartLine: f, header: e });\n          }\n          return (\n            k.forEach((e, d) => {\n              if (!e || e.startsWith('*')) return;\n              let D;\n              const F = k[d - 1],\n                R = k[d + 1],\n                W = k[d + 2];\n              if (e.startsWith('diff --git') || e.startsWith('diff --combined')) {\n                if (\n                  (I(),\n                  (D = /^diff --git \"?([a-ciow]\\/.+)\"? \"?([a-ciow]\\/.+)\"?/.exec(e)) &&\n                    ((u = l(D[1], void 0, t.dstPrefix)), (h = l(D[2], void 0, t.srcPrefix))),\n                  null === r)\n                )\n                  throw new Error('Where is my file !!!');\n                return void (r.isGitDiff = !0);\n              }\n              if (e.startsWith('Binary files') && !(null == r ? void 0 : r.isGitDiff)) {\n                if (\n                  (I(),\n                  (D = /^Binary files \"?([a-ciow]\\/.+)\"? and \"?([a-ciow]\\/.+)\"? differ/.exec(e)) &&\n                    ((u = l(D[1], void 0, t.dstPrefix)), (h = l(D[2], void 0, t.srcPrefix))),\n                  null === r)\n                )\n                  throw new Error('Where is my file !!!');\n                return void (r.isBinary = !0);\n              }\n              if (\n                ((!r || (!r.isGitDiff && r && e.startsWith(p) && R.startsWith(b) && W.startsWith(g))) && I(),\n                null == r ? void 0 : r.isTooBig)\n              )\n                return;\n              if (\n                r &&\n                (('number' == typeof t.diffMaxChanges && r.addedLines + r.deletedLines > t.diffMaxChanges) ||\n                  ('number' == typeof t.diffMaxLineLength && e.length > t.diffMaxLineLength))\n              )\n                return (\n                  (r.isTooBig = !0),\n                  (r.addedLines = 0),\n                  (r.deletedLines = 0),\n                  (r.blocks = []),\n                  (a = null),\n                  void A(\n                    'function' == typeof t.diffTooBigMessage\n                      ? t.diffTooBigMessage(n.length)\n                      : 'Diff too big to be displayed'\n                  )\n                );\n              if ((e.startsWith(p) && R.startsWith(b)) || (e.startsWith(b) && F.startsWith(p))) {\n                if (\n                  r &&\n                  !r.oldName &&\n                  e.startsWith('--- ') &&\n                  (D = (function (e, t) {\n                    return l(e, '---', t);\n                  })(e, t.srcPrefix))\n                )\n                  return (r.oldName = D), void (r.language = s(r.oldName, r.language));\n                if (\n                  r &&\n                  !r.newName &&\n                  e.startsWith('+++ ') &&\n                  (D = (function (e, t) {\n                    return l(e, '+++', t);\n                  })(e, t.dstPrefix))\n                )\n                  return (r.newName = D), void (r.language = s(r.newName, r.language));\n              }\n              if (r && (e.startsWith(g) || (r.isGitDiff && r.oldName && r.newName && !a))) return void A(e);\n              if (a && (e.startsWith('+') || e.startsWith('-') || e.startsWith(' ')))\n                return void (function (e) {\n                  if (null === r || null === a || null === c || null === f) return;\n                  const t = { content: e },\n                    n = r.isCombined ? ['+ ', ' +', '++'] : ['+'],\n                    s = r.isCombined ? ['- ', ' -', '--'] : ['-'];\n                  o(e, n)\n                    ? (r.addedLines++, (t.type = i.LineType.INSERT), (t.oldNumber = void 0), (t.newNumber = f++))\n                    : o(e, s)\n                    ? (r.deletedLines++, (t.type = i.LineType.DELETE), (t.oldNumber = c++), (t.newNumber = void 0))\n                    : ((t.type = i.LineType.CONTEXT), (t.oldNumber = c++), (t.newNumber = f++)),\n                    a.lines.push(t);\n                })(e);\n              const B = !(function (e, t) {\n                let n = t;\n                for (; n < k.length - 3; ) {\n                  if (e.startsWith('diff')) return !1;\n                  if (k[n].startsWith(p) && k[n + 1].startsWith(b) && k[n + 2].startsWith(g)) return !0;\n                  n++;\n                }\n                return !1;\n              })(e, d);\n              if (null === r) throw new Error('Where is my file !!!');\n              (D = m.exec(e))\n                ? (r.oldMode = D[1])\n                : (D = v.exec(e))\n                ? (r.newMode = D[1])\n                : (D = y.exec(e))\n                ? ((r.deletedFileMode = D[1]), (r.isDeleted = !0))\n                : (D = w.exec(e))\n                ? ((r.newFileMode = D[1]), (r.isNew = !0))\n                : (D = S.exec(e))\n                ? (B && (r.oldName = D[1]), (r.isCopy = !0))\n                : (D = L.exec(e))\n                ? (B && (r.newName = D[1]), (r.isCopy = !0))\n                : (D = C.exec(e))\n                ? (B && (r.oldName = D[1]), (r.isRename = !0))\n                : (D = x.exec(e))\n                ? (B && (r.newName = D[1]), (r.isRename = !0))\n                : (D = _.exec(e))\n                ? ((r.isBinary = !0),\n                  (r.oldName = l(D[1], void 0, t.srcPrefix)),\n                  (r.newName = l(D[2], void 0, t.dstPrefix)),\n                  A('Binary file'))\n                : N.test(e)\n                ? ((r.isBinary = !0), A(e))\n                : (D = O.exec(e))\n                ? (r.unchangedPercentage = parseInt(D[1], 10))\n                : (D = T.exec(e))\n                ? (r.changedPercentage = parseInt(D[1], 10))\n                : (D = j.exec(e))\n                ? ((r.checksumBefore = D[1]), (r.checksumAfter = D[2]), D[3] && (r.mode = D[3]))\n                : (D = P.exec(e))\n                ? ((r.checksumBefore = [D[2], D[3]]), (r.checksumAfter = D[1]))\n                : (D = E.exec(e))\n                ? ((r.oldMode = [D[2], D[3]]), (r.newMode = D[1]))\n                : (D = M.exec(e))\n                ? ((r.newFileMode = D[1]), (r.isNew = !0))\n                : (D = H.exec(e)) && ((r.deletedFileMode = D[1]), (r.isDeleted = !0));\n            }),\n            D(),\n            F(),\n            n\n          );\n        };\n      },\n      979: function (e, t, n) {\n        'use strict';\n        var i =\n            (this && this.__createBinding) ||\n            (Object.create\n              ? function (e, t, n, i) {\n                  void 0 === i && (i = n);\n                  var r = Object.getOwnPropertyDescriptor(t, n);\n                  (r && !('get' in r ? !t.__esModule : r.writable || r.configurable)) ||\n                    (r = {\n                      enumerable: !0,\n                      get: function () {\n                        return t[n];\n                      }\n                    }),\n                    Object.defineProperty(e, i, r);\n                }\n              : function (e, t, n, i) {\n                  void 0 === i && (i = n), (e[i] = t[n]);\n                }),\n          r =\n            (this && this.__setModuleDefault) ||\n            (Object.create\n              ? function (e, t) {\n                  Object.defineProperty(e, 'default', { enumerable: !0, value: t });\n                }\n              : function (e, t) {\n                  e.default = t;\n                }),\n          s =\n            (this && this.__importStar) ||\n            function (e) {\n              if (e && e.__esModule) return e;\n              var t = {};\n              if (null != e)\n                for (var n in e) 'default' !== n && Object.prototype.hasOwnProperty.call(e, n) && i(t, e, n);\n              return r(t, e), t;\n            };\n        Object.defineProperty(t, '__esModule', { value: !0 }), (t.defaultTemplates = void 0);\n        const o = s(n(485));\n        (t.defaultTemplates = {}),\n          (t.defaultTemplates['file-summary-line'] = new o.Template({\n            code: function (e, t, n) {\n              var i = this;\n              return (\n                i.b((n = n || '')),\n                i.b('<li class=\"d2h-file-list-line\">'),\n                i.b('\\n' + n),\n                i.b('    <span class=\"d2h-file-name-wrapper\">'),\n                i.b('\\n' + n),\n                i.b(i.rp('<fileIcon0', e, t, '      ')),\n                i.b('      <a href=\"#'),\n                i.b(i.v(i.f('fileHtmlId', e, t, 0))),\n                i.b('\" class=\"d2h-file-name\">'),\n                i.b(i.v(i.f('fileName', e, t, 0))),\n                i.b('</a>'),\n                i.b('\\n' + n),\n                i.b('      <span class=\"d2h-file-stats\">'),\n                i.b('\\n' + n),\n                i.b('          <span class=\"d2h-lines-added\">'),\n                i.b(i.v(i.f('addedLines', e, t, 0))),\n                i.b('</span>'),\n                i.b('\\n' + n),\n                i.b('          <span class=\"d2h-lines-deleted\">'),\n                i.b(i.v(i.f('deletedLines', e, t, 0))),\n                i.b('</span>'),\n                i.b('\\n' + n),\n                i.b('      </span>'),\n                i.b('\\n' + n),\n                i.b('    </span>'),\n                i.b('\\n' + n),\n                i.b('</li>'),\n                i.fl()\n              );\n            },\n            partials: { '<fileIcon0': { name: 'fileIcon', partials: {}, subs: {} } },\n            subs: {}\n          })),\n          (t.defaultTemplates['file-summary-wrapper'] = new o.Template({\n            code: function (e, t, n) {\n              var i = this;\n              return (\n                i.b((n = n || '')),\n                i.b('<div class=\"d2h-file-list-wrapper '),\n                i.b(i.v(i.f('colorScheme', e, t, 0))),\n                i.b('\">'),\n                i.b('\\n' + n),\n                i.b('    <div class=\"d2h-file-list-header\">'),\n                i.b('\\n' + n),\n                i.b('        <span class=\"d2h-file-list-title\">Files changed ('),\n                i.b(i.v(i.f('filesNumber', e, t, 0))),\n                i.b(')</span>'),\n                i.b('\\n' + n),\n                i.b('        <a class=\"d2h-file-switch d2h-hide\">hide</a>'),\n                i.b('\\n' + n),\n                i.b('        <a class=\"d2h-file-switch d2h-show\">show</a>'),\n                i.b('\\n' + n),\n                i.b('    </div>'),\n                i.b('\\n' + n),\n                i.b('    <ol class=\"d2h-file-list\">'),\n                i.b('\\n' + n),\n                i.b('    '),\n                i.b(i.t(i.f('files', e, t, 0))),\n                i.b('\\n' + n),\n                i.b('    </ol>'),\n                i.b('\\n' + n),\n                i.b('</div>'),\n                i.fl()\n              );\n            },\n            partials: {},\n            subs: {}\n          })),\n          (t.defaultTemplates['generic-block-header'] = new o.Template({\n            code: function (e, t, n) {\n              var i = this;\n              return (\n                i.b((n = n || '')),\n                i.b('<tr>'),\n                i.b('\\n' + n),\n                i.b('    <td class=\"'),\n                i.b(i.v(i.f('lineClass', e, t, 0))),\n                i.b(' '),\n                i.b(i.v(i.d('CSSLineClass.INFO', e, t, 0))),\n                i.b('\"></td>'),\n                i.b('\\n' + n),\n                i.b('    <td class=\"'),\n                i.b(i.v(i.d('CSSLineClass.INFO', e, t, 0))),\n                i.b('\">'),\n                i.b('\\n' + n),\n                i.b('        <div class=\"'),\n                i.b(i.v(i.f('contentClass', e, t, 0))),\n                i.b('\">'),\n                i.s(i.f('blockHeader', e, t, 1), e, t, 0, 156, 173, '{{ }}') &&\n                  (i.rs(e, t, function (e, t, n) {\n                    n.b(n.t(n.f('blockHeader', e, t, 0)));\n                  }),\n                  e.pop()),\n                i.s(i.f('blockHeader', e, t, 1), e, t, 1, 0, 0, '') || i.b('&nbsp;'),\n                i.b('</div>'),\n                i.b('\\n' + n),\n                i.b('    </td>'),\n                i.b('\\n' + n),\n                i.b('</tr>'),\n                i.fl()\n              );\n            },\n            partials: {},\n            subs: {}\n          })),\n          (t.defaultTemplates['generic-empty-diff'] = new o.Template({\n            code: function (e, t, n) {\n              var i = this;\n              return (\n                i.b((n = n || '')),\n                i.b('<tr>'),\n                i.b('\\n' + n),\n                i.b('    <td class=\"'),\n                i.b(i.v(i.d('CSSLineClass.INFO', e, t, 0))),\n                i.b('\">'),\n                i.b('\\n' + n),\n                i.b('        <div class=\"'),\n                i.b(i.v(i.f('contentClass', e, t, 0))),\n                i.b('\">'),\n                i.b('\\n' + n),\n                i.b('            File without changes'),\n                i.b('\\n' + n),\n                i.b('        </div>'),\n                i.b('\\n' + n),\n                i.b('    </td>'),\n                i.b('\\n' + n),\n                i.b('</tr>'),\n                i.fl()\n              );\n            },\n            partials: {},\n            subs: {}\n          })),\n          (t.defaultTemplates['generic-file-path'] = new o.Template({\n            code: function (e, t, n) {\n              var i = this;\n              return (\n                i.b((n = n || '')),\n                i.b('<span class=\"d2h-file-name-wrapper\">'),\n                i.b('\\n' + n),\n                i.b(i.rp('<fileIcon0', e, t, '    ')),\n                i.b('    <span class=\"d2h-file-name\">'),\n                i.b(i.v(i.f('fileDiffName', e, t, 0))),\n                i.b('</span>'),\n                i.b('\\n' + n),\n                i.b(i.rp('<fileTag1', e, t, '    ')),\n                i.b('</span>'),\n                i.b('\\n' + n),\n                i.b('<label class=\"d2h-file-collapse\">'),\n                i.b('\\n' + n),\n                i.b('    <input class=\"d2h-file-collapse-input\" type=\"checkbox\" name=\"viewed\" value=\"viewed\">'),\n                i.b('\\n' + n),\n                i.b('    Viewed'),\n                i.b('\\n' + n),\n                i.b('</label>'),\n                i.fl()\n              );\n            },\n            partials: {\n              '<fileIcon0': { name: 'fileIcon', partials: {}, subs: {} },\n              '<fileTag1': { name: 'fileTag', partials: {}, subs: {} }\n            },\n            subs: {}\n          })),\n          (t.defaultTemplates['generic-line'] = new o.Template({\n            code: function (e, t, n) {\n              var i = this;\n              return (\n                i.b((n = n || '')),\n                i.b('<tr>'),\n                i.b('\\n' + n),\n                i.b('    <td class=\"'),\n                i.b(i.v(i.f('lineClass', e, t, 0))),\n                i.b(' '),\n                i.b(i.v(i.f('type', e, t, 0))),\n                i.b('\">'),\n                i.b('\\n' + n),\n                i.b('      '),\n                i.b(i.t(i.f('lineNumber', e, t, 0))),\n                i.b('\\n' + n),\n                i.b('    </td>'),\n                i.b('\\n' + n),\n                i.b('    <td class=\"'),\n                i.b(i.v(i.f('type', e, t, 0))),\n                i.b('\">'),\n                i.b('\\n' + n),\n                i.b('        <div class=\"'),\n                i.b(i.v(i.f('contentClass', e, t, 0))),\n                i.b('\">'),\n                i.b('\\n' + n),\n                i.s(i.f('prefix', e, t, 1), e, t, 0, 162, 238, '{{ }}') &&\n                  (i.rs(e, t, function (e, t, i) {\n                    i.b('            <span class=\"d2h-code-line-prefix\">'),\n                      i.b(i.t(i.f('prefix', e, t, 0))),\n                      i.b('</span>'),\n                      i.b('\\n' + n);\n                  }),\n                  e.pop()),\n                i.s(i.f('prefix', e, t, 1), e, t, 1, 0, 0, '') ||\n                  (i.b('            <span class=\"d2h-code-line-prefix\">&nbsp;</span>'), i.b('\\n' + n)),\n                i.s(i.f('content', e, t, 1), e, t, 0, 371, 445, '{{ }}') &&\n                  (i.rs(e, t, function (e, t, i) {\n                    i.b('            <span class=\"d2h-code-line-ctn\">'),\n                      i.b(i.t(i.f('content', e, t, 0))),\n                      i.b('</span>'),\n                      i.b('\\n' + n);\n                  }),\n                  e.pop()),\n                i.s(i.f('content', e, t, 1), e, t, 1, 0, 0, '') ||\n                  (i.b('            <span class=\"d2h-code-line-ctn\"><br></span>'), i.b('\\n' + n)),\n                i.b('        </div>'),\n                i.b('\\n' + n),\n                i.b('    </td>'),\n                i.b('\\n' + n),\n                i.b('</tr>'),\n                i.fl()\n              );\n            },\n            partials: {},\n            subs: {}\n          })),\n          (t.defaultTemplates['generic-wrapper'] = new o.Template({\n            code: function (e, t, n) {\n              var i = this;\n              return (\n                i.b((n = n || '')),\n                i.b('<div class=\"d2h-wrapper '),\n                i.b(i.v(i.f('colorScheme', e, t, 0))),\n                i.b('\">'),\n                i.b('\\n' + n),\n                i.b('    '),\n                i.b(i.t(i.f('content', e, t, 0))),\n                i.b('\\n' + n),\n                i.b('</div>'),\n                i.fl()\n              );\n            },\n            partials: {},\n            subs: {}\n          })),\n          (t.defaultTemplates['icon-file-added'] = new o.Template({\n            code: function (e, t, n) {\n              var i = this;\n              return (\n                i.b((n = n || '')),\n                i.b(\n                  '<svg aria-hidden=\"true\" class=\"d2h-icon d2h-added\" height=\"16\" title=\"added\" version=\"1.1\" viewBox=\"0 0 14 16\"'\n                ),\n                i.b('\\n' + n),\n                i.b('     width=\"14\">'),\n                i.b('\\n' + n),\n                i.b(\n                  '    <path d=\"M13 1H1C0.45 1 0 1.45 0 2v12c0 0.55 0.45 1 1 1h12c0.55 0 1-0.45 1-1V2c0-0.55-0.45-1-1-1z m0 13H1V2h12v12zM6 9H3V7h3V4h2v3h3v2H8v3H6V9z\"></path>'\n                ),\n                i.b('\\n' + n),\n                i.b('</svg>'),\n                i.fl()\n              );\n            },\n            partials: {},\n            subs: {}\n          })),\n          (t.defaultTemplates['icon-file-changed'] = new o.Template({\n            code: function (e, t, n) {\n              var i = this;\n              return (\n                i.b((n = n || '')),\n                i.b('<svg aria-hidden=\"true\" class=\"d2h-icon d2h-changed\" height=\"16\" title=\"modified\" version=\"1.1\"'),\n                i.b('\\n' + n),\n                i.b('     viewBox=\"0 0 14 16\" width=\"14\">'),\n                i.b('\\n' + n),\n                i.b(\n                  '    <path d=\"M13 1H1C0.45 1 0 1.45 0 2v12c0 0.55 0.45 1 1 1h12c0.55 0 1-0.45 1-1V2c0-0.55-0.45-1-1-1z m0 13H1V2h12v12zM4 8c0-1.66 1.34-3 3-3s3 1.34 3 3-1.34 3-3 3-3-1.34-3-3z\"></path>'\n                ),\n                i.b('\\n' + n),\n                i.b('</svg>'),\n                i.fl()\n              );\n            },\n            partials: {},\n            subs: {}\n          })),\n          (t.defaultTemplates['icon-file-deleted'] = new o.Template({\n            code: function (e, t, n) {\n              var i = this;\n              return (\n                i.b((n = n || '')),\n                i.b('<svg aria-hidden=\"true\" class=\"d2h-icon d2h-deleted\" height=\"16\" title=\"removed\" version=\"1.1\"'),\n                i.b('\\n' + n),\n                i.b('     viewBox=\"0 0 14 16\" width=\"14\">'),\n                i.b('\\n' + n),\n                i.b(\n                  '    <path d=\"M13 1H1C0.45 1 0 1.45 0 2v12c0 0.55 0.45 1 1 1h12c0.55 0 1-0.45 1-1V2c0-0.55-0.45-1-1-1z m0 13H1V2h12v12zM11 9H3V7h8v2z\"></path>'\n                ),\n                i.b('\\n' + n),\n                i.b('</svg>'),\n                i.fl()\n              );\n            },\n            partials: {},\n            subs: {}\n          })),\n          (t.defaultTemplates['icon-file-renamed'] = new o.Template({\n            code: function (e, t, n) {\n              var i = this;\n              return (\n                i.b((n = n || '')),\n                i.b('<svg aria-hidden=\"true\" class=\"d2h-icon d2h-moved\" height=\"16\" title=\"renamed\" version=\"1.1\"'),\n                i.b('\\n' + n),\n                i.b('     viewBox=\"0 0 14 16\" width=\"14\">'),\n                i.b('\\n' + n),\n                i.b(\n                  '    <path d=\"M6 9H3V7h3V4l5 4-5 4V9z m8-7v12c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h12c0.55 0 1 0.45 1 1z m-1 0H1v12h12V2z\"></path>'\n                ),\n                i.b('\\n' + n),\n                i.b('</svg>'),\n                i.fl()\n              );\n            },\n            partials: {},\n            subs: {}\n          })),\n          (t.defaultTemplates['icon-file'] = new o.Template({\n            code: function (e, t, n) {\n              var i = this;\n              return (\n                i.b((n = n || '')),\n                i.b(\n                  '<svg aria-hidden=\"true\" class=\"d2h-icon\" height=\"16\" version=\"1.1\" viewBox=\"0 0 12 16\" width=\"12\">'\n                ),\n                i.b('\\n' + n),\n                i.b(\n                  '    <path d=\"M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z\"></path>'\n                ),\n                i.b('\\n' + n),\n                i.b('</svg>'),\n                i.fl()\n              );\n            },\n            partials: {},\n            subs: {}\n          })),\n          (t.defaultTemplates['line-by-line-file-diff'] = new o.Template({\n            code: function (e, t, n) {\n              var i = this;\n              return (\n                i.b((n = n || '')),\n                i.b('<div id=\"'),\n                i.b(i.v(i.f('fileHtmlId', e, t, 0))),\n                i.b('\" class=\"d2h-file-wrapper\" data-lang=\"'),\n                i.b(i.v(i.d('file.language', e, t, 0))),\n                i.b('\">'),\n                i.b('\\n' + n),\n                i.b('    <div class=\"d2h-file-header\">'),\n                i.b('\\n' + n),\n                i.b('    '),\n                i.b(i.t(i.f('filePath', e, t, 0))),\n                i.b('\\n' + n),\n                i.b('    </div>'),\n                i.b('\\n' + n),\n                i.b('    <div class=\"d2h-file-diff\">'),\n                i.b('\\n' + n),\n                i.b('        <div class=\"d2h-code-wrapper\">'),\n                i.b('\\n' + n),\n                i.b('            <table class=\"d2h-diff-table\">'),\n                i.b('\\n' + n),\n                i.b('                <tbody class=\"d2h-diff-tbody\">'),\n                i.b('\\n' + n),\n                i.b('                '),\n                i.b(i.t(i.f('diffs', e, t, 0))),\n                i.b('\\n' + n),\n                i.b('                </tbody>'),\n                i.b('\\n' + n),\n                i.b('            </table>'),\n                i.b('\\n' + n),\n                i.b('        </div>'),\n                i.b('\\n' + n),\n                i.b('    </div>'),\n                i.b('\\n' + n),\n                i.b('</div>'),\n                i.fl()\n              );\n            },\n            partials: {},\n            subs: {}\n          })),\n          (t.defaultTemplates['line-by-line-numbers'] = new o.Template({\n            code: function (e, t, n) {\n              var i = this;\n              return (\n                i.b((n = n || '')),\n                i.b('<div class=\"line-num1\">'),\n                i.b(i.v(i.f('oldNumber', e, t, 0))),\n                i.b('</div>'),\n                i.b('\\n' + n),\n                i.b('<div class=\"line-num2\">'),\n                i.b(i.v(i.f('newNumber', e, t, 0))),\n                i.b('</div>'),\n                i.fl()\n              );\n            },\n            partials: {},\n            subs: {}\n          })),\n          (t.defaultTemplates['side-by-side-file-diff'] = new o.Template({\n            code: function (e, t, n) {\n              var i = this;\n              return (\n                i.b((n = n || '')),\n                i.b('<div id=\"'),\n                i.b(i.v(i.f('fileHtmlId', e, t, 0))),\n                i.b('\" class=\"d2h-file-wrapper\" data-lang=\"'),\n                i.b(i.v(i.d('file.language', e, t, 0))),\n                i.b('\">'),\n                i.b('\\n' + n),\n                i.b('    <div class=\"d2h-file-header\">'),\n                i.b('\\n' + n),\n                i.b('      '),\n                i.b(i.t(i.f('filePath', e, t, 0))),\n                i.b('\\n' + n),\n                i.b('    </div>'),\n                i.b('\\n' + n),\n                i.b('    <div class=\"d2h-files-diff\">'),\n                i.b('\\n' + n),\n                i.b('        <div class=\"d2h-file-side-diff\">'),\n                i.b('\\n' + n),\n                i.b('            <div class=\"d2h-code-wrapper\">'),\n                i.b('\\n' + n),\n                i.b('                <table class=\"d2h-diff-table\">'),\n                i.b('\\n' + n),\n                i.b('                    <tbody class=\"d2h-diff-tbody\">'),\n                i.b('\\n' + n),\n                i.b('                    '),\n                i.b(i.t(i.d('diffs.left', e, t, 0))),\n                i.b('\\n' + n),\n                i.b('                    </tbody>'),\n                i.b('\\n' + n),\n                i.b('                </table>'),\n                i.b('\\n' + n),\n                i.b('            </div>'),\n                i.b('\\n' + n),\n                i.b('        </div>'),\n                i.b('\\n' + n),\n                i.b('        <div class=\"d2h-file-side-diff\">'),\n                i.b('\\n' + n),\n                i.b('            <div class=\"d2h-code-wrapper\">'),\n                i.b('\\n' + n),\n                i.b('                <table class=\"d2h-diff-table\">'),\n                i.b('\\n' + n),\n                i.b('                    <tbody class=\"d2h-diff-tbody\">'),\n                i.b('\\n' + n),\n                i.b('                    '),\n                i.b(i.t(i.d('diffs.right', e, t, 0))),\n                i.b('\\n' + n),\n                i.b('                    </tbody>'),\n                i.b('\\n' + n),\n                i.b('                </table>'),\n                i.b('\\n' + n),\n                i.b('            </div>'),\n                i.b('\\n' + n),\n                i.b('        </div>'),\n                i.b('\\n' + n),\n                i.b('    </div>'),\n                i.b('\\n' + n),\n                i.b('</div>'),\n                i.fl()\n              );\n            },\n            partials: {},\n            subs: {}\n          })),\n          (t.defaultTemplates['tag-file-added'] = new o.Template({\n            code: function (e, t, n) {\n              var i = this;\n              return i.b((n = n || '')), i.b('<span class=\"d2h-tag d2h-added d2h-added-tag\">ADDED</span>'), i.fl();\n            },\n            partials: {},\n            subs: {}\n          })),\n          (t.defaultTemplates['tag-file-changed'] = new o.Template({\n            code: function (e, t, n) {\n              var i = this;\n              return (\n                i.b((n = n || '')), i.b('<span class=\"d2h-tag d2h-changed d2h-changed-tag\">CHANGED</span>'), i.fl()\n              );\n            },\n            partials: {},\n            subs: {}\n          })),\n          (t.defaultTemplates['tag-file-deleted'] = new o.Template({\n            code: function (e, t, n) {\n              var i = this;\n              return (\n                i.b((n = n || '')), i.b('<span class=\"d2h-tag d2h-deleted d2h-deleted-tag\">DELETED</span>'), i.fl()\n              );\n            },\n            partials: {},\n            subs: {}\n          })),\n          (t.defaultTemplates['tag-file-renamed'] = new o.Template({\n            code: function (e, t, n) {\n              var i = this;\n              return i.b((n = n || '')), i.b('<span class=\"d2h-tag d2h-moved d2h-moved-tag\">RENAMED</span>'), i.fl();\n            },\n            partials: {},\n            subs: {}\n          }));\n      },\n      834: function (e, t, n) {\n        'use strict';\n        var i =\n            (this && this.__createBinding) ||\n            (Object.create\n              ? function (e, t, n, i) {\n                  void 0 === i && (i = n);\n                  var r = Object.getOwnPropertyDescriptor(t, n);\n                  (r && !('get' in r ? !t.__esModule : r.writable || r.configurable)) ||\n                    (r = {\n                      enumerable: !0,\n                      get: function () {\n                        return t[n];\n                      }\n                    }),\n                    Object.defineProperty(e, i, r);\n                }\n              : function (e, t, n, i) {\n                  void 0 === i && (i = n), (e[i] = t[n]);\n                }),\n          r =\n            (this && this.__setModuleDefault) ||\n            (Object.create\n              ? function (e, t) {\n                  Object.defineProperty(e, 'default', { enumerable: !0, value: t });\n                }\n              : function (e, t) {\n                  e.default = t;\n                }),\n          s =\n            (this && this.__importStar) ||\n            function (e) {\n              if (e && e.__esModule) return e;\n              var t = {};\n              if (null != e)\n                for (var n in e) 'default' !== n && Object.prototype.hasOwnProperty.call(e, n) && i(t, e, n);\n              return r(t, e), t;\n            },\n          o =\n            (this && this.__importDefault) ||\n            function (e) {\n              return e && e.__esModule ? e : { default: e };\n            };\n        Object.defineProperty(t, '__esModule', { value: !0 }), (t.html = t.parse = t.defaultDiff2HtmlConfig = void 0);\n        const a = s(n(468)),\n          l = n(479),\n          c = s(n(378)),\n          d = s(n(170)),\n          f = n(699),\n          u = o(n(63));\n        (t.defaultDiff2HtmlConfig = Object.assign(\n          Object.assign(Object.assign({}, c.defaultLineByLineRendererConfig), d.defaultSideBySideRendererConfig),\n          { outputFormat: f.OutputFormatType.LINE_BY_LINE, drawFileList: !0 }\n        )),\n          (t.parse = function (e, n = {}) {\n            return a.parse(e, Object.assign(Object.assign({}, t.defaultDiff2HtmlConfig), n));\n          }),\n          (t.html = function (e, n = {}) {\n            const i = Object.assign(Object.assign({}, t.defaultDiff2HtmlConfig), n),\n              r = 'string' == typeof e ? a.parse(e, i) : e,\n              s = new u.default(i),\n              { colorScheme: o } = i,\n              f = { colorScheme: o };\n            return (\n              (i.drawFileList ? new l.FileListRenderer(s, f).render(r) : '') +\n              ('side-by-side' === i.outputFormat ? new d.default(s, i).render(r) : new c.default(s, i).render(r))\n            );\n          });\n      },\n      479: function (e, t, n) {\n        'use strict';\n        var i =\n            (this && this.__createBinding) ||\n            (Object.create\n              ? function (e, t, n, i) {\n                  void 0 === i && (i = n);\n                  var r = Object.getOwnPropertyDescriptor(t, n);\n                  (r && !('get' in r ? !t.__esModule : r.writable || r.configurable)) ||\n                    (r = {\n                      enumerable: !0,\n                      get: function () {\n                        return t[n];\n                      }\n                    }),\n                    Object.defineProperty(e, i, r);\n                }\n              : function (e, t, n, i) {\n                  void 0 === i && (i = n), (e[i] = t[n]);\n                }),\n          r =\n            (this && this.__setModuleDefault) ||\n            (Object.create\n              ? function (e, t) {\n                  Object.defineProperty(e, 'default', { enumerable: !0, value: t });\n                }\n              : function (e, t) {\n                  e.default = t;\n                }),\n          s =\n            (this && this.__importStar) ||\n            function (e) {\n              if (e && e.__esModule) return e;\n              var t = {};\n              if (null != e)\n                for (var n in e) 'default' !== n && Object.prototype.hasOwnProperty.call(e, n) && i(t, e, n);\n              return r(t, e), t;\n            };\n        Object.defineProperty(t, '__esModule', { value: !0 }),\n          (t.FileListRenderer = t.defaultFileListRendererConfig = void 0);\n        const o = s(n(741)),\n          a = 'file-summary';\n        (t.defaultFileListRendererConfig = { colorScheme: o.defaultRenderConfig.colorScheme }),\n          (t.FileListRenderer = class {\n            constructor(e, n = {}) {\n              (this.hoganUtils = e),\n                (this.config = Object.assign(Object.assign({}, t.defaultFileListRendererConfig), n));\n            }\n            render(e) {\n              const t = e\n                .map((e) =>\n                  this.hoganUtils.render(\n                    a,\n                    'line',\n                    {\n                      fileHtmlId: o.getHtmlId(e),\n                      oldName: e.oldName,\n                      newName: e.newName,\n                      fileName: o.filenameDiff(e),\n                      deletedLines: '-' + e.deletedLines,\n                      addedLines: '+' + e.addedLines\n                    },\n                    { fileIcon: this.hoganUtils.template('icon', o.getFileIcon(e)) }\n                  )\n                )\n                .join('\\n');\n              return this.hoganUtils.render(a, 'wrapper', {\n                colorScheme: o.colorSchemeToCss(this.config.colorScheme),\n                filesNumber: e.length,\n                files: t\n              });\n            }\n          });\n      },\n      63: function (e, t, n) {\n        'use strict';\n        var i =\n            (this && this.__createBinding) ||\n            (Object.create\n              ? function (e, t, n, i) {\n                  void 0 === i && (i = n);\n                  var r = Object.getOwnPropertyDescriptor(t, n);\n                  (r && !('get' in r ? !t.__esModule : r.writable || r.configurable)) ||\n                    (r = {\n                      enumerable: !0,\n                      get: function () {\n                        return t[n];\n                      }\n                    }),\n                    Object.defineProperty(e, i, r);\n                }\n              : function (e, t, n, i) {\n                  void 0 === i && (i = n), (e[i] = t[n]);\n                }),\n          r =\n            (this && this.__setModuleDefault) ||\n            (Object.create\n              ? function (e, t) {\n                  Object.defineProperty(e, 'default', { enumerable: !0, value: t });\n                }\n              : function (e, t) {\n                  e.default = t;\n                }),\n          s =\n            (this && this.__importStar) ||\n            function (e) {\n              if (e && e.__esModule) return e;\n              var t = {};\n              if (null != e)\n                for (var n in e) 'default' !== n && Object.prototype.hasOwnProperty.call(e, n) && i(t, e, n);\n              return r(t, e), t;\n            };\n        Object.defineProperty(t, '__esModule', { value: !0 });\n        const o = s(n(485)),\n          a = n(979);\n        t.default = class {\n          constructor({ compiledTemplates: e = {}, rawTemplates: t = {} }) {\n            const n = Object.entries(t).reduce((e, [t, n]) => {\n              const i = o.compile(n, { asString: !1 });\n              return Object.assign(Object.assign({}, e), { [t]: i });\n            }, {});\n            this.preCompiledTemplates = Object.assign(Object.assign(Object.assign({}, a.defaultTemplates), e), n);\n          }\n          static compile(e) {\n            return o.compile(e, { asString: !1 });\n          }\n          render(e, t, n, i, r) {\n            const s = this.templateKey(e, t);\n            try {\n              return this.preCompiledTemplates[s].render(n, i, r);\n            } catch (e) {\n              throw new Error(`Could not find template to render '${s}'`);\n            }\n          }\n          template(e, t) {\n            return this.preCompiledTemplates[this.templateKey(e, t)];\n          }\n          templateKey(e, t) {\n            return `${e}-${t}`;\n          }\n        };\n      },\n      378: function (e, t, n) {\n        'use strict';\n        var i =\n            (this && this.__createBinding) ||\n            (Object.create\n              ? function (e, t, n, i) {\n                  void 0 === i && (i = n);\n                  var r = Object.getOwnPropertyDescriptor(t, n);\n                  (r && !('get' in r ? !t.__esModule : r.writable || r.configurable)) ||\n                    (r = {\n                      enumerable: !0,\n                      get: function () {\n                        return t[n];\n                      }\n                    }),\n                    Object.defineProperty(e, i, r);\n                }\n              : function (e, t, n, i) {\n                  void 0 === i && (i = n), (e[i] = t[n]);\n                }),\n          r =\n            (this && this.__setModuleDefault) ||\n            (Object.create\n              ? function (e, t) {\n                  Object.defineProperty(e, 'default', { enumerable: !0, value: t });\n                }\n              : function (e, t) {\n                  e.default = t;\n                }),\n          s =\n            (this && this.__importStar) ||\n            function (e) {\n              if (e && e.__esModule) return e;\n              var t = {};\n              if (null != e)\n                for (var n in e) 'default' !== n && Object.prototype.hasOwnProperty.call(e, n) && i(t, e, n);\n              return r(t, e), t;\n            };\n        Object.defineProperty(t, '__esModule', { value: !0 }), (t.defaultLineByLineRendererConfig = void 0);\n        const o = s(n(483)),\n          a = s(n(741)),\n          l = n(699);\n        t.defaultLineByLineRendererConfig = Object.assign(Object.assign({}, a.defaultRenderConfig), {\n          renderNothingWhenEmpty: !1,\n          matchingMaxComparisons: 2500,\n          maxLineSizeInBlockForComparison: 200\n        });\n        const c = 'generic',\n          d = 'line-by-line';\n        t.default = class {\n          constructor(e, n = {}) {\n            (this.hoganUtils = e),\n              (this.config = Object.assign(Object.assign({}, t.defaultLineByLineRendererConfig), n));\n          }\n          render(e) {\n            const t = e\n              .map((e) => {\n                let t;\n                return (\n                  (t = e.blocks.length ? this.generateFileHtml(e) : this.generateEmptyDiff()),\n                  this.makeFileDiffHtml(e, t)\n                );\n              })\n              .join('\\n');\n            return this.hoganUtils.render(c, 'wrapper', {\n              colorScheme: a.colorSchemeToCss(this.config.colorScheme),\n              content: t\n            });\n          }\n          makeFileDiffHtml(e, t) {\n            if (this.config.renderNothingWhenEmpty && Array.isArray(e.blocks) && 0 === e.blocks.length) return '';\n            const n = this.hoganUtils.template(d, 'file-diff'),\n              i = this.hoganUtils.template(c, 'file-path'),\n              r = this.hoganUtils.template('icon', 'file'),\n              s = this.hoganUtils.template('tag', a.getFileIcon(e));\n            return n.render({\n              file: e,\n              fileHtmlId: a.getHtmlId(e),\n              diffs: t,\n              filePath: i.render({ fileDiffName: a.filenameDiff(e) }, { fileIcon: r, fileTag: s })\n            });\n          }\n          generateEmptyDiff() {\n            return this.hoganUtils.render(c, 'empty-diff', {\n              contentClass: 'd2h-code-line',\n              CSSLineClass: a.CSSLineClass\n            });\n          }\n          generateFileHtml(e) {\n            const t = o.newMatcherFn(o.newDistanceFn((t) => a.deconstructLine(t.content, e.isCombined).content));\n            return e.blocks\n              .map((n) => {\n                let i = this.hoganUtils.render(c, 'block-header', {\n                  CSSLineClass: a.CSSLineClass,\n                  blockHeader: e.isTooBig ? n.header : a.escapeForHtml(n.header),\n                  lineClass: 'd2h-code-linenumber',\n                  contentClass: 'd2h-code-line'\n                });\n                return (\n                  this.applyLineGroupping(n).forEach(([n, r, s]) => {\n                    if (r.length && s.length && !n.length)\n                      this.applyRematchMatching(r, s, t).map(([t, n]) => {\n                        const { left: r, right: s } = this.processChangedLines(e, e.isCombined, t, n);\n                        (i += r), (i += s);\n                      });\n                    else if (n.length)\n                      n.forEach((t) => {\n                        const { prefix: n, content: r } = a.deconstructLine(t.content, e.isCombined);\n                        i += this.generateSingleLineHtml(e, {\n                          type: a.CSSLineClass.CONTEXT,\n                          prefix: n,\n                          content: r,\n                          oldNumber: t.oldNumber,\n                          newNumber: t.newNumber\n                        });\n                      });\n                    else if (r.length || s.length) {\n                      const { left: t, right: n } = this.processChangedLines(e, e.isCombined, r, s);\n                      (i += t), (i += n);\n                    } else console.error('Unknown state reached while processing groups of lines', n, r, s);\n                  }),\n                  i\n                );\n              })\n              .join('\\n');\n          }\n          applyLineGroupping(e) {\n            const t = [];\n            let n = [],\n              i = [];\n            for (let r = 0; r < e.lines.length; r++) {\n              const s = e.lines[r];\n              ((s.type !== l.LineType.INSERT && i.length) || (s.type === l.LineType.CONTEXT && n.length > 0)) &&\n                (t.push([[], n, i]), (n = []), (i = [])),\n                s.type === l.LineType.CONTEXT\n                  ? t.push([[s], [], []])\n                  : s.type === l.LineType.INSERT && 0 === n.length\n                  ? t.push([[], [], [s]])\n                  : s.type === l.LineType.INSERT && n.length > 0\n                  ? i.push(s)\n                  : s.type === l.LineType.DELETE && n.push(s);\n            }\n            return (n.length || i.length) && (t.push([[], n, i]), (n = []), (i = [])), t;\n          }\n          applyRematchMatching(e, t, n) {\n            const i = e.length * t.length,\n              r = Math.max.apply(null, [0].concat(e.concat(t).map((e) => e.content.length)));\n            return i < this.config.matchingMaxComparisons &&\n              r < this.config.maxLineSizeInBlockForComparison &&\n              ('lines' === this.config.matching || 'words' === this.config.matching)\n              ? n(e, t)\n              : [[e, t]];\n          }\n          processChangedLines(e, t, n, i) {\n            const r = { right: '', left: '' },\n              s = Math.max(n.length, i.length);\n            for (let o = 0; o < s; o++) {\n              const s = n[o],\n                l = i[o],\n                c = void 0 !== s && void 0 !== l ? a.diffHighlight(s.content, l.content, t, this.config) : void 0,\n                d =\n                  void 0 !== s && void 0 !== s.oldNumber\n                    ? Object.assign(\n                        Object.assign(\n                          {},\n                          void 0 !== c\n                            ? {\n                                prefix: c.oldLine.prefix,\n                                content: c.oldLine.content,\n                                type: a.CSSLineClass.DELETE_CHANGES\n                              }\n                            : Object.assign(Object.assign({}, a.deconstructLine(s.content, t)), {\n                                type: a.toCSSClass(s.type)\n                              })\n                        ),\n                        { oldNumber: s.oldNumber, newNumber: s.newNumber }\n                      )\n                    : void 0,\n                f =\n                  void 0 !== l && void 0 !== l.newNumber\n                    ? Object.assign(\n                        Object.assign(\n                          {},\n                          void 0 !== c\n                            ? {\n                                prefix: c.newLine.prefix,\n                                content: c.newLine.content,\n                                type: a.CSSLineClass.INSERT_CHANGES\n                              }\n                            : Object.assign(Object.assign({}, a.deconstructLine(l.content, t)), {\n                                type: a.toCSSClass(l.type)\n                              })\n                        ),\n                        { oldNumber: l.oldNumber, newNumber: l.newNumber }\n                      )\n                    : void 0,\n                { left: u, right: h } = this.generateLineHtml(e, d, f);\n              (r.left += u), (r.right += h);\n            }\n            return r;\n          }\n          generateLineHtml(e, t, n) {\n            return { left: this.generateSingleLineHtml(e, t), right: this.generateSingleLineHtml(e, n) };\n          }\n          generateSingleLineHtml(e, t) {\n            if (void 0 === t) return '';\n            const n = this.hoganUtils.render(d, 'numbers', {\n              oldNumber: t.oldNumber || '',\n              newNumber: t.newNumber || ''\n            });\n            return this.hoganUtils.render(c, 'line', {\n              type: t.type,\n              lineClass: 'd2h-code-linenumber',\n              contentClass: 'd2h-code-line',\n              prefix: ' ' === t.prefix ? '&nbsp;' : t.prefix,\n              content: t.content,\n              lineNumber: n,\n              line: t,\n              file: e\n            });\n          }\n        };\n      },\n      483: (e, t) => {\n        'use strict';\n        function n(e, t) {\n          if (0 === e.length) return t.length;\n          if (0 === t.length) return e.length;\n          const n = [];\n          let i, r;\n          for (i = 0; i <= t.length; i++) n[i] = [i];\n          for (r = 0; r <= e.length; r++) n[0][r] = r;\n          for (i = 1; i <= t.length; i++)\n            for (r = 1; r <= e.length; r++)\n              t.charAt(i - 1) === e.charAt(r - 1)\n                ? (n[i][r] = n[i - 1][r - 1])\n                : (n[i][r] = Math.min(n[i - 1][r - 1] + 1, Math.min(n[i][r - 1] + 1, n[i - 1][r] + 1)));\n          return n[t.length][e.length];\n        }\n        Object.defineProperty(t, '__esModule', { value: !0 }),\n          (t.newMatcherFn = t.newDistanceFn = t.levenshtein = void 0),\n          (t.levenshtein = n),\n          (t.newDistanceFn = function (e) {\n            return (t, i) => {\n              const r = e(t).trim(),\n                s = e(i).trim();\n              return n(r, s) / (r.length + s.length);\n            };\n          }),\n          (t.newMatcherFn = function (e) {\n            return function t(n, i, r = 0, s = new Map()) {\n              const o = (function (t, n, i = new Map()) {\n                let r,\n                  s = 1 / 0;\n                for (let o = 0; o < t.length; ++o)\n                  for (let a = 0; a < n.length; ++a) {\n                    const l = JSON.stringify([t[o], n[a]]);\n                    let c;\n                    (i.has(l) && (c = i.get(l))) || ((c = e(t[o], n[a])), i.set(l, c)),\n                      c < s && ((s = c), (r = { indexA: o, indexB: a, score: s }));\n                  }\n                return r;\n              })(n, i, s);\n              if (!o || n.length + i.length < 3) return [[n, i]];\n              const a = n.slice(0, o.indexA),\n                l = i.slice(0, o.indexB),\n                c = [n[o.indexA]],\n                d = [i[o.indexB]],\n                f = o.indexA + 1,\n                u = o.indexB + 1,\n                h = n.slice(f),\n                p = i.slice(u),\n                b = t(a, l, r + 1, s),\n                g = t(c, d, r + 1, s),\n                m = t(h, p, r + 1, s);\n              let v = g;\n              return (\n                (o.indexA > 0 || o.indexB > 0) && (v = b.concat(v)),\n                (n.length > f || i.length > u) && (v = v.concat(m)),\n                v\n              );\n            };\n          });\n      },\n      741: function (e, t, n) {\n        'use strict';\n        var i =\n            (this && this.__createBinding) ||\n            (Object.create\n              ? function (e, t, n, i) {\n                  void 0 === i && (i = n);\n                  var r = Object.getOwnPropertyDescriptor(t, n);\n                  (r && !('get' in r ? !t.__esModule : r.writable || r.configurable)) ||\n                    (r = {\n                      enumerable: !0,\n                      get: function () {\n                        return t[n];\n                      }\n                    }),\n                    Object.defineProperty(e, i, r);\n                }\n              : function (e, t, n, i) {\n                  void 0 === i && (i = n), (e[i] = t[n]);\n                }),\n          r =\n            (this && this.__setModuleDefault) ||\n            (Object.create\n              ? function (e, t) {\n                  Object.defineProperty(e, 'default', { enumerable: !0, value: t });\n                }\n              : function (e, t) {\n                  e.default = t;\n                }),\n          s =\n            (this && this.__importStar) ||\n            function (e) {\n              if (e && e.__esModule) return e;\n              var t = {};\n              if (null != e)\n                for (var n in e) 'default' !== n && Object.prototype.hasOwnProperty.call(e, n) && i(t, e, n);\n              return r(t, e), t;\n            };\n        Object.defineProperty(t, '__esModule', { value: !0 }),\n          (t.diffHighlight =\n            t.getFileIcon =\n            t.getHtmlId =\n            t.filenameDiff =\n            t.deconstructLine =\n            t.escapeForHtml =\n            t.colorSchemeToCss =\n            t.toCSSClass =\n            t.defaultRenderConfig =\n            t.CSSLineClass =\n              void 0);\n        const o = s(n(785)),\n          a = n(593),\n          l = s(n(483)),\n          c = n(699);\n        (t.CSSLineClass = {\n          INSERTS: 'd2h-ins',\n          DELETES: 'd2h-del',\n          CONTEXT: 'd2h-cntx',\n          INFO: 'd2h-info',\n          INSERT_CHANGES: 'd2h-ins d2h-change',\n          DELETE_CHANGES: 'd2h-del d2h-change'\n        }),\n          (t.defaultRenderConfig = {\n            matching: c.LineMatchingType.NONE,\n            matchWordsThreshold: 0.25,\n            maxLineLengthHighlight: 1e4,\n            diffStyle: c.DiffStyleType.WORD,\n            colorScheme: c.ColorSchemeType.LIGHT\n          });\n        const d = '/',\n          f = l.newDistanceFn((e) => e.value),\n          u = l.newMatcherFn(f);\n        function h(e) {\n          return -1 !== e.indexOf('dev/null');\n        }\n        function p(e) {\n          return e.replace(/(<del[^>]*>((.|\\n)*?)<\\/del>)/g, '');\n        }\n        function b(e) {\n          return e\n            .slice(0)\n            .replace(/&/g, '&amp;')\n            .replace(/</g, '&lt;')\n            .replace(/>/g, '&gt;')\n            .replace(/\"/g, '&quot;')\n            .replace(/'/g, '&#x27;')\n            .replace(/\\//g, '&#x2F;');\n        }\n        function g(e, t, n = !0) {\n          const i = (function (e) {\n            return e ? 2 : 1;\n          })(t);\n          return { prefix: e.substring(0, i), content: n ? b(e.substring(i)) : e.substring(i) };\n        }\n        function m(e) {\n          const t = (0, a.unifyPath)(e.oldName),\n            n = (0, a.unifyPath)(e.newName);\n          if (t === n || h(t) || h(n)) return h(n) ? t : n;\n          {\n            const e = [],\n              i = [],\n              r = t.split(d),\n              s = n.split(d);\n            let o = 0,\n              a = r.length - 1,\n              l = s.length - 1;\n            for (; o < a && o < l && r[o] === s[o]; ) e.push(s[o]), (o += 1);\n            for (; a > o && l > o && r[a] === s[l]; ) i.unshift(s[l]), (a -= 1), (l -= 1);\n            const c = e.join(d),\n              f = i.join(d),\n              u = r.slice(o, a + 1).join(d),\n              h = s.slice(o, l + 1).join(d);\n            return c.length && f.length\n              ? c + d + '{' + u + ' → ' + h + '}' + d + f\n              : c.length\n              ? c + d + '{' + u + ' → ' + h + '}'\n              : f.length\n              ? '{' + u + ' → ' + h + '}' + d + f\n              : t + ' → ' + n;\n          }\n        }\n        (t.toCSSClass = function (e) {\n          switch (e) {\n            case c.LineType.CONTEXT:\n              return t.CSSLineClass.CONTEXT;\n            case c.LineType.INSERT:\n              return t.CSSLineClass.INSERTS;\n            case c.LineType.DELETE:\n              return t.CSSLineClass.DELETES;\n          }\n        }),\n          (t.colorSchemeToCss = function (e) {\n            switch (e) {\n              case c.ColorSchemeType.DARK:\n                return 'd2h-dark-color-scheme';\n              case c.ColorSchemeType.AUTO:\n                return 'd2h-auto-color-scheme';\n              case c.ColorSchemeType.LIGHT:\n              default:\n                return 'd2h-light-color-scheme';\n            }\n          }),\n          (t.escapeForHtml = b),\n          (t.deconstructLine = g),\n          (t.filenameDiff = m),\n          (t.getHtmlId = function (e) {\n            return `d2h-${(0, a.hashCode)(m(e)).toString().slice(-6)}`;\n          }),\n          (t.getFileIcon = function (e) {\n            let t = 'file-changed';\n            return (\n              e.isRename || e.isCopy\n                ? (t = 'file-renamed')\n                : e.isNew\n                ? (t = 'file-added')\n                : e.isDeleted\n                ? (t = 'file-deleted')\n                : e.newName !== e.oldName && (t = 'file-renamed'),\n              t\n            );\n          }),\n          (t.diffHighlight = function (e, n, i, r = {}) {\n            const {\n                matching: s,\n                maxLineLengthHighlight: a,\n                matchWordsThreshold: l,\n                diffStyle: c\n              } = Object.assign(Object.assign({}, t.defaultRenderConfig), r),\n              d = g(e, i, !1),\n              h = g(n, i, !1);\n            if (d.content.length > a || h.content.length > a)\n              return {\n                oldLine: { prefix: d.prefix, content: b(d.content) },\n                newLine: { prefix: h.prefix, content: b(h.content) }\n              };\n            const m = 'char' === c ? o.diffChars(d.content, h.content) : o.diffWordsWithSpace(d.content, h.content),\n              v = [];\n            if ('word' === c && 'words' === s) {\n              const e = m.filter((e) => e.removed),\n                t = m.filter((e) => e.added);\n              u(t, e).forEach((e) => {\n                1 === e[0].length && 1 === e[1].length && f(e[0][0], e[1][0]) < l && (v.push(e[0][0]), v.push(e[1][0]));\n              });\n            }\n            const y = m.reduce((e, t) => {\n              const n = t.added ? 'ins' : t.removed ? 'del' : null,\n                i = v.indexOf(t) > -1 ? ' class=\"d2h-change\"' : '',\n                r = b(t.value);\n              return null !== n ? `${e}<${n}${i}>${r}</${n}>` : `${e}${r}`;\n            }, '');\n            return {\n              oldLine: { prefix: d.prefix, content: ((w = y), w.replace(/(<ins[^>]*>((.|\\n)*?)<\\/ins>)/g, '')) },\n              newLine: { prefix: h.prefix, content: p(y) }\n            };\n            var w;\n          });\n      },\n      170: function (e, t, n) {\n        'use strict';\n        var i =\n            (this && this.__createBinding) ||\n            (Object.create\n              ? function (e, t, n, i) {\n                  void 0 === i && (i = n);\n                  var r = Object.getOwnPropertyDescriptor(t, n);\n                  (r && !('get' in r ? !t.__esModule : r.writable || r.configurable)) ||\n                    (r = {\n                      enumerable: !0,\n                      get: function () {\n                        return t[n];\n                      }\n                    }),\n                    Object.defineProperty(e, i, r);\n                }\n              : function (e, t, n, i) {\n                  void 0 === i && (i = n), (e[i] = t[n]);\n                }),\n          r =\n            (this && this.__setModuleDefault) ||\n            (Object.create\n              ? function (e, t) {\n                  Object.defineProperty(e, 'default', { enumerable: !0, value: t });\n                }\n              : function (e, t) {\n                  e.default = t;\n                }),\n          s =\n            (this && this.__importStar) ||\n            function (e) {\n              if (e && e.__esModule) return e;\n              var t = {};\n              if (null != e)\n                for (var n in e) 'default' !== n && Object.prototype.hasOwnProperty.call(e, n) && i(t, e, n);\n              return r(t, e), t;\n            };\n        Object.defineProperty(t, '__esModule', { value: !0 }), (t.defaultSideBySideRendererConfig = void 0);\n        const o = s(n(483)),\n          a = s(n(741)),\n          l = n(699);\n        t.defaultSideBySideRendererConfig = Object.assign(Object.assign({}, a.defaultRenderConfig), {\n          renderNothingWhenEmpty: !1,\n          matchingMaxComparisons: 2500,\n          maxLineSizeInBlockForComparison: 200\n        });\n        const c = 'generic';\n        t.default = class {\n          constructor(e, n = {}) {\n            (this.hoganUtils = e),\n              (this.config = Object.assign(Object.assign({}, t.defaultSideBySideRendererConfig), n));\n          }\n          render(e) {\n            const t = e\n              .map((e) => {\n                let t;\n                return (\n                  (t = e.blocks.length ? this.generateFileHtml(e) : this.generateEmptyDiff()),\n                  this.makeFileDiffHtml(e, t)\n                );\n              })\n              .join('\\n');\n            return this.hoganUtils.render(c, 'wrapper', {\n              colorScheme: a.colorSchemeToCss(this.config.colorScheme),\n              content: t\n            });\n          }\n          makeFileDiffHtml(e, t) {\n            if (this.config.renderNothingWhenEmpty && Array.isArray(e.blocks) && 0 === e.blocks.length) return '';\n            const n = this.hoganUtils.template('side-by-side', 'file-diff'),\n              i = this.hoganUtils.template(c, 'file-path'),\n              r = this.hoganUtils.template('icon', 'file'),\n              s = this.hoganUtils.template('tag', a.getFileIcon(e));\n            return n.render({\n              file: e,\n              fileHtmlId: a.getHtmlId(e),\n              diffs: t,\n              filePath: i.render({ fileDiffName: a.filenameDiff(e) }, { fileIcon: r, fileTag: s })\n            });\n          }\n          generateEmptyDiff() {\n            return {\n              right: '',\n              left: this.hoganUtils.render(c, 'empty-diff', {\n                contentClass: 'd2h-code-side-line',\n                CSSLineClass: a.CSSLineClass\n              })\n            };\n          }\n          generateFileHtml(e) {\n            const t = o.newMatcherFn(o.newDistanceFn((t) => a.deconstructLine(t.content, e.isCombined).content));\n            return e.blocks\n              .map((n) => {\n                const i = { left: this.makeHeaderHtml(n.header, e), right: this.makeHeaderHtml('') };\n                return (\n                  this.applyLineGroupping(n).forEach(([n, r, s]) => {\n                    if (r.length && s.length && !n.length)\n                      this.applyRematchMatching(r, s, t).map(([t, n]) => {\n                        const { left: r, right: s } = this.processChangedLines(e.isCombined, t, n);\n                        (i.left += r), (i.right += s);\n                      });\n                    else if (n.length)\n                      n.forEach((t) => {\n                        const { prefix: n, content: r } = a.deconstructLine(t.content, e.isCombined),\n                          { left: s, right: o } = this.generateLineHtml(\n                            { type: a.CSSLineClass.CONTEXT, prefix: n, content: r, number: t.oldNumber },\n                            { type: a.CSSLineClass.CONTEXT, prefix: n, content: r, number: t.newNumber }\n                          );\n                        (i.left += s), (i.right += o);\n                      });\n                    else if (r.length || s.length) {\n                      const { left: t, right: n } = this.processChangedLines(e.isCombined, r, s);\n                      (i.left += t), (i.right += n);\n                    } else console.error('Unknown state reached while processing groups of lines', n, r, s);\n                  }),\n                  i\n                );\n              })\n              .reduce((e, t) => ({ left: e.left + t.left, right: e.right + t.right }), { left: '', right: '' });\n          }\n          applyLineGroupping(e) {\n            const t = [];\n            let n = [],\n              i = [];\n            for (let r = 0; r < e.lines.length; r++) {\n              const s = e.lines[r];\n              ((s.type !== l.LineType.INSERT && i.length) || (s.type === l.LineType.CONTEXT && n.length > 0)) &&\n                (t.push([[], n, i]), (n = []), (i = [])),\n                s.type === l.LineType.CONTEXT\n                  ? t.push([[s], [], []])\n                  : s.type === l.LineType.INSERT && 0 === n.length\n                  ? t.push([[], [], [s]])\n                  : s.type === l.LineType.INSERT && n.length > 0\n                  ? i.push(s)\n                  : s.type === l.LineType.DELETE && n.push(s);\n            }\n            return (n.length || i.length) && (t.push([[], n, i]), (n = []), (i = [])), t;\n          }\n          applyRematchMatching(e, t, n) {\n            const i = e.length * t.length,\n              r = Math.max.apply(null, [0].concat(e.concat(t).map((e) => e.content.length)));\n            return i < this.config.matchingMaxComparisons &&\n              r < this.config.maxLineSizeInBlockForComparison &&\n              ('lines' === this.config.matching || 'words' === this.config.matching)\n              ? n(e, t)\n              : [[e, t]];\n          }\n          makeHeaderHtml(e, t) {\n            return this.hoganUtils.render(c, 'block-header', {\n              CSSLineClass: a.CSSLineClass,\n              blockHeader: (null == t ? void 0 : t.isTooBig) ? e : a.escapeForHtml(e),\n              lineClass: 'd2h-code-side-linenumber',\n              contentClass: 'd2h-code-side-line'\n            });\n          }\n          processChangedLines(e, t, n) {\n            const i = { right: '', left: '' },\n              r = Math.max(t.length, n.length);\n            for (let s = 0; s < r; s++) {\n              const r = t[s],\n                o = n[s],\n                l = void 0 !== r && void 0 !== o ? a.diffHighlight(r.content, o.content, e, this.config) : void 0,\n                c =\n                  void 0 !== r && void 0 !== r.oldNumber\n                    ? Object.assign(\n                        Object.assign(\n                          {},\n                          void 0 !== l\n                            ? {\n                                prefix: l.oldLine.prefix,\n                                content: l.oldLine.content,\n                                type: a.CSSLineClass.DELETE_CHANGES\n                              }\n                            : Object.assign(Object.assign({}, a.deconstructLine(r.content, e)), {\n                                type: a.toCSSClass(r.type)\n                              })\n                        ),\n                        { number: r.oldNumber }\n                      )\n                    : void 0,\n                d =\n                  void 0 !== o && void 0 !== o.newNumber\n                    ? Object.assign(\n                        Object.assign(\n                          {},\n                          void 0 !== l\n                            ? {\n                                prefix: l.newLine.prefix,\n                                content: l.newLine.content,\n                                type: a.CSSLineClass.INSERT_CHANGES\n                              }\n                            : Object.assign(Object.assign({}, a.deconstructLine(o.content, e)), {\n                                type: a.toCSSClass(o.type)\n                              })\n                        ),\n                        { number: o.newNumber }\n                      )\n                    : void 0,\n                { left: f, right: u } = this.generateLineHtml(c, d);\n              (i.left += f), (i.right += u);\n            }\n            return i;\n          }\n          generateLineHtml(e, t) {\n            return { left: this.generateSingleHtml(e), right: this.generateSingleHtml(t) };\n          }\n          generateSingleHtml(e) {\n            const t = 'd2h-code-side-linenumber',\n              n = 'd2h-code-side-line';\n            return this.hoganUtils.render(c, 'line', {\n              type: (null == e ? void 0 : e.type) || `${a.CSSLineClass.CONTEXT} d2h-emptyplaceholder`,\n              lineClass: void 0 !== e ? t : `${t} d2h-code-side-emptyplaceholder`,\n              contentClass: void 0 !== e ? n : `${n} d2h-code-side-emptyplaceholder`,\n              prefix: ' ' === (null == e ? void 0 : e.prefix) ? '&nbsp;' : null == e ? void 0 : e.prefix,\n              content: null == e ? void 0 : e.content,\n              lineNumber: null == e ? void 0 : e.number\n            });\n          }\n        };\n      },\n      699: (e, t) => {\n        'use strict';\n        var n, i;\n        Object.defineProperty(t, '__esModule', { value: !0 }),\n          (t.ColorSchemeType = t.DiffStyleType = t.LineMatchingType = t.OutputFormatType = t.LineType = void 0),\n          (function (e) {\n            (e.INSERT = 'insert'), (e.DELETE = 'delete'), (e.CONTEXT = 'context');\n          })(n || (t.LineType = n = {})),\n          (t.OutputFormatType = { LINE_BY_LINE: 'line-by-line', SIDE_BY_SIDE: 'side-by-side' }),\n          (t.LineMatchingType = { LINES: 'lines', WORDS: 'words', NONE: 'none' }),\n          (t.DiffStyleType = { WORD: 'word', CHAR: 'char' }),\n          (function (e) {\n            (e.AUTO = 'auto'), (e.DARK = 'dark'), (e.LIGHT = 'light');\n          })(i || (t.ColorSchemeType = i = {}));\n      },\n      593: (e, t) => {\n        'use strict';\n        Object.defineProperty(t, '__esModule', { value: !0 }), (t.hashCode = t.unifyPath = t.escapeForRegExp = void 0);\n        const n = RegExp(\n          '[' + ['-', '[', ']', '/', '{', '}', '(', ')', '*', '+', '?', '.', '\\\\', '^', '$', '|'].join('\\\\') + ']',\n          'g'\n        );\n        (t.escapeForRegExp = function (e) {\n          return e.replace(n, '\\\\$&');\n        }),\n          (t.unifyPath = function (e) {\n            return e ? e.replace(/\\\\/g, '/') : e;\n          }),\n          (t.hashCode = function (e) {\n            let t,\n              n,\n              i,\n              r = 0;\n            for (t = 0, i = e.length; t < i; t++) (n = e.charCodeAt(t)), (r = (r << 5) - r + n), (r |= 0);\n            return r;\n          });\n      }\n    }),\n    (t = {}),\n    (function n(i) {\n      var r = t[i];\n      if (void 0 !== r) return r.exports;\n      var s = (t[i] = { exports: {} });\n      return e[i].call(s.exports, s, s.exports, n), s.exports;\n    })(834)\n  );\n  var e, t;\n});\n"
  },
  {
    "path": "packages/bruno-app/public/theme/dark.js",
    "content": "const darkTheme = {\n  'brand': '#546de5',\n  'text': 'rgb(52 52 52)',\n  'primary-text': '#ffffff',\n  'primary-theme': '#1e1e1e',\n  'secondary-text': '#929292',\n  'sidebar-collection-item-active-indent-border': '#d0d0d0',\n  'sidebar-collection-item-active-background': '#e1e1e1',\n  'sidebar-background': '#252526',\n  'sidebar-bottom-bg': '#68217a',\n  'request-dragbar-background': '#efefef',\n  'request-dragbar-background-active': 'rgb(200, 200, 200)',\n  'tab-inactive': 'rgb(155 155 155)',\n  'tab-active-border': '#546de5',\n  'layout-border': '#dedede',\n  'codemirror-border': '#efefef',\n  'codemirror-background': 'rgb(243, 243, 243)',\n  'text-link': '#1663bb',\n  'text-danger': 'rgb(185, 28, 28)',\n  'background-danger': '#dc3545',\n  'method-get': 'rgb(5, 150, 105)',\n  'method-post': '#8e44ad',\n  'method-delete': 'rgb(185, 28, 28)',\n  'method-patch': 'rgb(52 52 52)',\n  'method-options': 'rgb(52 52 52)',\n  'method-head': 'rgb(52 52 52)',\n  'table-stripe': '#f3f3f3'\n};\n\nexport default darkTheme;\n"
  },
  {
    "path": "packages/bruno-app/public/theme/index.js",
    "content": "import darkTheme from './dark';\nimport lightTheme from './light';\n\nexport default {\n  Light: lightTheme,\n  Dark: darkTheme\n};\n"
  },
  {
    "path": "packages/bruno-app/public/theme/light.js",
    "content": "const lightTheme = {\n  'brand': '#546de5',\n  'text': 'rgb(52 52 52)',\n  'primary-text': 'rgb(52 52 52)',\n  'primary-theme': '#ffffff',\n  'secondary-text': '#929292',\n  'sidebar-collection-item-active-indent-border': '#d0d0d0',\n  'sidebar-collection-item-active-background': '#e1e1e1',\n  'sidebar-background': '#f3f3f3',\n  'sidebar-bottom-bg': '#f3f3f3',\n  'request-dragbar-background': '#efefef',\n  'request-dragbar-background-active': 'rgb(200, 200, 200)',\n  'tab-inactive': 'rgb(155 155 155)',\n  'tab-active-border': '#546de5',\n  'layout-border': '#dedede',\n  'codemirror-border': '#efefef',\n  'codemirror-background': 'rgb(243, 243, 243)',\n  'text-link': '#1663bb',\n  'text-danger': 'rgb(185, 28, 28)',\n  'background-danger': '#dc3545',\n  'method-get': 'rgb(5, 150, 105)',\n  'method-post': '#8e44ad',\n  'method-delete': 'rgb(185, 28, 28)',\n  'method-patch': 'rgb(52 52 52)',\n  'method-options': 'rgb(52 52 52)',\n  'method-head': 'rgb(52 52 52)',\n  'table-stripe': '#f3f3f3'\n};\n\nexport default lightTheme;\n"
  },
  {
    "path": "packages/bruno-app/rsbuild.config.mjs",
    "content": "import { defineConfig } from '@rsbuild/core';\nimport { pluginReact } from '@rsbuild/plugin-react';\nimport { pluginBabel } from '@rsbuild/plugin-babel';\nimport { pluginStyledComponents } from '@rsbuild/plugin-styled-components';\nimport { pluginSass } from '@rsbuild/plugin-sass';\nimport { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill'\n\nexport default defineConfig({\n  plugins: [\n    pluginNodePolyfill(),\n    pluginReact(),\n    pluginStyledComponents(),\n    pluginSass(),\n    pluginBabel({\n      include: /\\.(?:js|jsx|tsx)$/,\n      babelLoaderOptions(opts) {\n        opts.plugins?.unshift('babel-plugin-react-compiler');\n      }\n    })\n  ],\n  source: {\n    tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file,\n    exclude: [\n      '**/test-utils/**',\n      '**/*.test.*',\n      '**/*.spec.*'\n    ]\n  },\n  html: {\n    title: 'Bruno'\n  },\n  tools: {\n    rspack: {\n      module: {\n        parser: {\n          javascript: {\n            // This loads the JavaScript contents from a library along with the main JavaScript bundle.\n            dynamicImportMode: \"eager\",\n          },\n        },\n      },\n      ignoreWarnings: [\n        (warning) =>  warning.message.includes('Critical dependency: the request of a dependency is an expression') && warning?.moduleDescriptor?.name?.includes('flow-parser')\n      ],\n      // Add externals configuration to exclude Node.js libraries\n      externals: {\n        // List specific Node.js modules you want to exclude\n        // Format: 'module-name': 'commonjs module-name'\n        'worker_threads': 'commonjs worker_threads',\n        // 'path': 'commonjs path'\n      }\n    },\n  }\n});\n"
  },
  {
    "path": "packages/bruno-app/src/components/Accordion/index.js",
    "content": "import React, { createContext, useContext, useState } from 'react';\nimport { IconChevronDown } from '@tabler/icons';\nimport { AccordionItem, AccordionHeader, AccordionContent } from './styledWrapper';\n\nconst AccordionContext = createContext();\n\nconst Accordion = ({ children, defaultIndex, dataTestId }) => {\n  const [openIndex, setOpenIndex] = useState(defaultIndex);\n\n  const toggleItem = (index) => {\n    setOpenIndex(openIndex === index ? null : index);\n  };\n\n  return (\n    <AccordionContext.Provider value={{ openIndex, toggleItem }}>\n      <div data-testid={dataTestId}>{children}</div>\n    </AccordionContext.Provider>\n  );\n};\n\nconst Item = ({ index, children, ...props }) => {\n  return (\n    <AccordionItem {...props}>\n      {React.Children.map(children, (child) => React.cloneElement(child, { index }))}\n    </AccordionItem>\n  );\n};\n\nexport const Header = ({ index, children, ...props }) => {\n  const { openIndex, toggleItem } = useContext(AccordionContext);\n  const isOpen = openIndex === index;\n\n  return (\n    <AccordionHeader onClick={() => toggleItem(index)} {...props} className={isOpen ? 'open' : ''}>\n      <div className=\"w-full\">{children}</div>\n\n      <IconChevronDown\n        className=\"w-5 h-5 ml-auto\"\n        style={{\n          transform: `rotate(${isOpen ? '180deg' : '0deg'})`,\n          transition: 'transform 0.3s ease-in-out'\n        }}\n      />\n    </AccordionHeader>\n  );\n};\n\nconst Content = ({ index, children, ...props }) => {\n  const { openIndex } = useContext(AccordionContext);\n  const isOpen = openIndex === index;\n\n  return (\n    <AccordionContent isOpen={isOpen} {...props}>\n      {children}\n    </AccordionContent>\n  );\n};\n\nAccordion.Item = Item;\nAccordion.Header = Header;\nAccordion.Content = Content;\nexport default Accordion;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Accordion/styledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst AccordionItem = styled.div`\n  border: 1px solid ${(props) => props.theme.input.border};\n  border-radius: 4px;\n  overflow: hidden;\n  margin-bottom: 1rem;\n`;\n\nconst AccordionHeader = styled.button`\n  width: 100%;\n  display: flex;\n  padding: 0.75rem 1rem;\n  background: transparent;\n  cursor: pointer;\n  font-weight: 500;\n\n  &.open, &:hover {\n    background-color: ${(props) => props.theme.plainGrid.hoverBg};\n  }\n`;\n\nconst AccordionContent = styled.div`\n  padding: ${(props) => (props.isOpen ? '1rem' : '0')};\n  max-height: ${(props) => (props.isOpen ? 'auto' : '0')};\n`;\n\nexport { AccordionItem, AccordionHeader, AccordionContent };\n"
  },
  {
    "path": "packages/bruno-app/src/components/ApiSpecPanel/FileEditor/CodeEditor/Plugins/Yaml/index.js",
    "content": "const yamlPlugin = (cm) => {\n  cm.defineMode('yaml', function () {\n    var cons = ['true', 'false', 'on', 'off', 'yes', 'no'];\n    var keywordRegex = new RegExp('\\\\b((' + cons.join(')|(') + '))$', 'i');\n\n    return {\n      token: function (stream, state) {\n        var ch = stream.peek();\n        var esc = state.escaped;\n        state.escaped = false;\n        /* comments */\n        if (ch == '#' && (stream.pos == 0 || /\\s/.test(stream.string.charAt(stream.pos - 1)))) {\n          stream.skipToEnd();\n          return 'comment';\n        }\n\n        if (stream.match(/^('([^']|\\\\.)*'?|\"([^\"]|\\\\.)*\"?)/)) return 'string';\n\n        if (state.literal && stream.indentation() > state.keyCol) {\n          stream.skipToEnd();\n          return 'string';\n        } else if (state.literal) {\n          state.literal = false;\n        }\n        if (stream.sol()) {\n          state.keyCol = 0;\n          state.pair = false;\n          state.pairStart = false;\n          /* document start */\n          if (stream.match('---')) {\n            return 'def';\n          }\n          /* document end */\n          if (stream.match('...')) {\n            return 'def';\n          }\n          /* array list item */\n          if (stream.match(/\\s*-\\s+/)) {\n            return 'meta';\n          }\n        }\n        /* inline pairs/lists */\n        if (stream.match(/^(\\{|\\}|\\[|\\])/)) {\n          if (ch == '{') state.inlinePairs++;\n          else if (ch == '}') state.inlinePairs--;\n          else if (ch == '[') state.inlineList++;\n          else state.inlineList--;\n          return 'meta';\n        }\n\n        /* list separator */\n        if (state.inlineList > 0 && !esc && ch == ',') {\n          stream.next();\n          return 'meta';\n        }\n        /* pairs separator */\n        if (state.inlinePairs > 0 && !esc && ch == ',') {\n          state.keyCol = 0;\n          state.pair = false;\n          state.pairStart = false;\n          stream.next();\n          return 'meta';\n        }\n\n        /* start of value of a pair */\n        if (state.pairStart) {\n          /* block literals */\n          if (stream.match(/^\\s*(\\||\\>)\\s*/)) {\n            state.literal = true;\n            return 'meta';\n          }\n          /* references */\n          if (stream.match(/^\\s*(\\&|\\*)[a-z0-9\\._-]+\\b/i)) {\n            return 'variable-2';\n          }\n          /* numbers */\n          if (state.inlinePairs == 0 && stream.match(/^\\s*-?[0-9\\.\\,]+\\s?$/)) {\n            return 'number';\n          }\n          if (state.inlinePairs > 0 && stream.match(/^\\s*-?[0-9\\.\\,]+\\s?(?=(,|}))/)) {\n            return 'number';\n          }\n          /* keywords */\n          if (stream.match(keywordRegex)) {\n            return 'keyword';\n          }\n        }\n\n        /* pairs (associative arrays) -> key */\n        if (\n          !state.pair\n          && stream.match(/^\\s*(?:[,\\[\\]{}&*!|>'\"%@`][^\\s'\":]|[^\\s,\\[\\]{}#&*!|>'\"%@`])[^#:]*(?=:($|\\s))/)\n        ) {\n          state.pair = true;\n          state.keyCol = stream.indentation();\n          return 'atom';\n        }\n        if (state.pair && stream.match(/^:\\s*/)) {\n          state.pairStart = true;\n          return 'meta';\n        }\n\n        /* nothing found, continue */\n        state.pairStart = false;\n        state.escaped = ch == '\\\\';\n        stream.next();\n        return null;\n      },\n      startState: function () {\n        return {\n          pair: false,\n          pairStart: false,\n          keyCol: 0,\n          inlinePairs: 0,\n          inlineList: 0,\n          literal: false,\n          escaped: false\n        };\n      },\n      lineComment: '#',\n      fold: 'indent'\n    };\n  });\n\n  cm.defineMIME('text/x-yaml', 'yaml');\n  cm.defineMIME('text/yaml', 'yaml');\n};\n\nexport default yamlPlugin;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ApiSpecPanel/FileEditor/CodeEditor/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  div.CodeMirror {\n    height: calc(100vh - 9rem);\n    background: ${(props) => props.theme.codemirror.bg};\n    border: solid 1px ${(props) => props.theme.codemirror.border};\n    font-family: ${(props) => (props.font ? props.font : 'default')};\n    font-size: ${(props) => props.theme.font.size.base};\n    line-break: anywhere;\n  }\n\n  .CodeMirror-dialog {\n    overflow: visible;\n    input {\n      background: transparent;\n      border: 1px solid #d3d6db;\n      outline: none;\n      border-radius: 0px;\n    }\n  }\n\n  .CodeMirror-overlayscroll-horizontal div,\n  .CodeMirror-overlayscroll-vertical div {\n    background: #d2d7db;\n  }\n\n  textarea.cm-editor {\n    position: relative;\n  }\n\n  // Todo: dark mode temporary fix\n  // Clean this\n  .CodeMirror.cm-s-monokai {\n    .CodeMirror-overlayscroll-horizontal div,\n    .CodeMirror-overlayscroll-vertical div {\n      background: #444444;\n    }\n  }\n\n  .cm-s-monokai span.cm-property,\n  .cm-s-monokai span.cm-attribute {\n    color: #9cdcfe !important;\n  }\n\n  .cm-s-monokai span.cm-string {\n    color: #ce9178 !important;\n  }\n\n  .cm-s-monokai span.cm-number {\n    color: #b5cea8 !important;\n  }\n\n  .cm-s-monokai span.cm-atom {\n    color: #569cd6 !important;\n  }\n\n  .cm-variable-valid {\n    color: ${(props) => props.theme.codemirror.variable.valid};\n  }\n  .cm-variable-invalid {\n    color: ${(props) => props.theme.codemirror.variable.invalid};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ApiSpecPanel/FileEditor/CodeEditor/index.js",
    "content": "/**\n *  Copyright (c) 2021 GraphQL Contributors.\n *\n *  This source code is licensed under the MIT license found in the\n *  LICENSE file in the root directory of this source tree.\n */\n\nimport React from 'react';\nimport StyledWrapper from './StyledWrapper';\nimport yamlPlugin from './Plugins/Yaml/index';\n\nlet CodeMirror;\nconst SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;\n\nif (!SERVER_RENDERED) {\n  CodeMirror = require('codemirror');\n}\n\nexport default class CodeEditor extends React.Component {\n  constructor(props) {\n    super(props);\n    this.cachedValue = props.value || '';\n    this.variables = {};\n    this.lintOptions = {\n      esversion: 11,\n      expr: true,\n      asi: true\n    };\n  }\n\n  componentWillMount() {\n    switch (this.props.mode) {\n      case 'yaml':\n        // YAML linting and hightlighting plugin\n        yamlPlugin(CodeMirror);\n        break;\n      default:\n        break;\n    }\n  }\n\n  componentDidMount() {\n    const editor = (this.editor = CodeMirror(this._node, {\n      value: this.props.value || '',\n      lineNumbers: true,\n      lineWrapping: true,\n      tabSize: 2,\n      mode: this.props.mode || 'application/text',\n      keyMap: 'sublime',\n      autoCloseBrackets: true,\n      matchBrackets: true,\n      showCursorWhenSelecting: true,\n      foldGutter: true,\n      gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],\n      lint: this.lintOptions,\n      readOnly: this.props.readOnly,\n      scrollbarStyle: 'overlay',\n      theme: this.props.theme === 'dark' ? 'monokai' : 'default',\n      extraKeys: {\n        'Cmd-S': () => {\n          if (this.props.onSave) {\n            this.props.onSave();\n          }\n        },\n        'Ctrl-S': () => {\n          if (this.props.onSave) {\n            this.props.onSave();\n          }\n        },\n        'Cmd-F': 'findPersistent',\n        'Ctrl-F': 'findPersistent',\n        'Cmd-H': 'replace',\n        'Ctrl-H': 'replace',\n        'Tab': function (cm) {\n          cm.getSelection().includes('\\n') || editor.getLine(cm.getCursor().line) == cm.getSelection()\n            ? cm.execCommand('indentMore')\n            : cm.replaceSelection('  ', 'end');\n        },\n        'Shift-Tab': 'indentLess',\n        'Ctrl-Space': 'autocomplete',\n        'Cmd-Space': 'autocomplete',\n        'Ctrl-Y': 'foldAll',\n        'Cmd-Y': 'foldAll',\n        'Ctrl-I': 'unfoldAll',\n        'Cmd-I': 'unfoldAll'\n      }\n    }));\n    if (editor) {\n      editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);\n      editor.on('change', this._onEdit);\n    }\n  }\n\n  componentDidUpdate(prevProps) {\n    this.ignoreChangeEvent = true;\n    if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {\n      this.cachedValue = this.props.value;\n      this.editor.setValue(this.props.value);\n    }\n    if (this.props.theme !== prevProps.theme && this.editor) {\n      this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');\n    }\n    this.ignoreChangeEvent = false;\n  }\n\n  componentWillUnmount() {\n    if (this.editor) {\n      this.editor.off('change', this._onEdit);\n      this.editor = null;\n    }\n  }\n\n  render() {\n    if (this.editor) {\n      this.editor.refresh();\n    }\n    return (\n      <StyledWrapper\n        className=\"h-full w-full graphiql-container\"\n        aria-label=\"Code Editor\"\n        font={this.props.font}\n        ref={(node) => {\n          this._node = node;\n        }}\n      />\n    );\n  }\n\n  _onEdit = () => {\n    if (!this.ignoreChangeEvent && this.editor) {\n      this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? this.lintOptions : false);\n      this.cachedValue = this.editor.getValue();\n      if (this.props.onEdit) {\n        this.props.onEdit(this.cachedValue);\n      }\n    }\n  };\n}\n"
  },
  {
    "path": "packages/bruno-app/src/components/ApiSpecPanel/Renderers/Swagger/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .swagger-root {\n    height: calc(100vh - 7rem);\n    border-left: solid 1px ${(props) => props.theme.border.border1};\n    overflow-y: auto;\n    background: ${(props) => props.theme.bg};\n    padding-bottom: 20px;\n\n    /* ── Global reset ── */\n    .swagger-ui {\n      font-family: inherit;\n      font-size: ${(props) => props.theme.font.size.base};\n      color: ${(props) => props.theme.text};\n\n      * {\n        border-color: ${(props) => props.theme.border.border1};\n      }\n\n      .auth-container {\n        padding: 0;\n      }\n\n      select {\n        box-shadow: none !important;\n      }\n\n      .wrapper {\n        padding: 0 20px;\n        max-width: none;\n      }\n\n      /* ── Info section ── */\n      .info {\n        margin: 16px 0 12px;\n\n        hgroup.main {\n          margin: 0;\n        }\n\n        .title {\n          font-size: 16px;\n          font-weight: 600;\n          color: ${(props) => props.theme.text};\n\n          small {\n            padding: 2px 6px !important;\n            font-size: 10px;\n            vertical-align: middle;\n            border-radius: 3px;\n\n            pre {\n              color: ${(props) => props.theme.text} !important;\n              font-size: 10px;\n            }\n          }\n        }\n\n        .base-url {\n          font-size: ${(props) => props.theme.font.size.xs};\n          color: ${(props) => props.theme.colors.text.muted};\n        }\n\n        .description {\n          font-size: ${(props) => props.theme.font.size.sm};\n          color: ${(props) => props.theme.colors.text.muted};\n\n          p, li {\n            font-size: ${(props) => props.theme.font.size.sm};\n            color: ${(props) => props.theme.colors.text.muted};\n            margin: 3px 0;\n            line-height: 1.5;\n          }\n\n          h1, h2, h3, h4, h5, h6 {\n            color: ${(props) => props.theme.text};\n          }\n\n          a {\n            color: ${(props) => props.theme.textLink};\n          }\n        }\n      }\n\n      /* Version / OAS badges */\n      .version-stamp span.version {\n        background: ${(props) => props.theme.border.border1} !important;\n        border: 1px solid ${(props) => props.theme.colors.text.muted} !important;\n        color: ${(props) => props.theme.text} !important;\n        font-size: 9px;\n        padding: 2px 6px;\n        border-radius: 3px;\n      }\n\n      .version-pragma {\n        font-size: ${(props) => props.theme.font.size.xs};\n        color: ${(props) => props.theme.colors.text.muted};\n      }\n\n      /* ── Tag section headings ── */\n      .opblock-tag-section {\n        .opblock-tag {\n          font-size: ${(props) => props.theme.font.size.md};\n          color: ${(props) => props.theme.text};\n          border-bottom: none;\n          padding: 0;\n\n          &:hover {\n            background: ${(props) => props.theme.background.mantle};\n          }\n\n          a {\n            color: ${(props) => props.theme.text} !important;\n          }\n\n          small {\n            font-size: ${(props) => props.theme.font.size.xs};\n            color: ${(props) => props.theme.colors.text.muted};\n            padding: 0 10px;\n          }\n        }\n      }\n\n      /* ── Operation blocks (GET, POST, PUT, DELETE, PATCH) ── */\n      .opblock {\n        margin: 0 0 8px;\n        border-radius: 4px;\n        border: 1px solid ${(props) => props.theme.border.border1} !important;\n        background: ${(props) => props.theme.bg} !important;\n        box-shadow: none !important;\n\n        .opblock-summary {\n          padding: 6px 10px;\n          border: none !important;\n          background: transparent !important;\n\n          .opblock-summary-method {\n            font-size: 10px;\n            font-weight: 700;\n            padding: 3px 8px;\n            min-width: 50px;\n            text-align: center;\n            border-radius: 3px;\n          }\n\n          .opblock-summary-path {\n            font-size: ${(props) => props.theme.font.size.sm};\n\n            a, span {\n              color: ${(props) => props.theme.text} !important;\n            }\n          }\n\n          .opblock-summary-description {\n            font-size: ${(props) => props.theme.font.size.xs};\n            color: ${(props) => props.theme.colors.text.muted};\n          }\n\n          .opblock-summary-control {\n            svg {\n              fill: ${(props) => props.theme.colors.text.muted};\n              width: 14px;\n              height: 14px;\n            }\n          }\n        }\n\n        .opblock-body {\n          font-size: ${(props) => props.theme.font.size.sm};\n          color: ${(props) => props.theme.text};\n          background: ${(props) => props.theme.bg};\n          border-top: 1px solid ${(props) => props.theme.border.border1};\n\n          .opblock-description-wrapper,\n          .opblock-section {\n            p {\n              color: ${(props) => props.theme.colors.text.muted};\n              font-size: ${(props) => props.theme.font.size.sm};\n            }\n          }\n\n          .tab-header .tab-item {\n            color: ${(props) => props.theme.colors.text.muted};\n\n            &.active {\n              color: ${(props) => props.theme.text};\n            }\n          }\n\n          select {\n            color: ${(props) => props.theme.text};\n            background: ${(props) => props.theme.bg};\n            border: 1px solid ${(props) => props.theme.border.border1};\n            border-radius: 3px;\n            font-size: ${(props) => props.theme.font.size.xs};\n            padding: 2px 6px;\n          }\n\n          input[type=\"text\"] {\n            color: ${(props) => props.theme.text};\n            background: ${(props) => props.theme.bg};\n            border: 1px solid ${(props) => props.theme.border.border1};\n            border-radius: 3px;\n            font-size: ${(props) => props.theme.font.size.sm};\n          }\n        }\n      }\n\n      /* Method badge colors — keep them but tone down */\n      .opblock.opblock-get .opblock-summary-method { background: #61affe; color: #fff; }\n      .opblock.opblock-post .opblock-summary-method { background: #49cc90; color: #fff; }\n      .opblock.opblock-put .opblock-summary-method { background: #fca130; color: #fff; }\n      .opblock.opblock-delete .opblock-summary-method { background: #f93e3e; color: #fff; }\n      .opblock.opblock-patch .opblock-summary-method { background: #50e3c2; color: #000; }\n\n      /* Lock / authorization icons */\n      .authorization__btn {\n\n        svg {\n          fill: ${(props) => props.theme.colors.text.muted};\n          width: 14px;\n          height: 14px;\n        }\n      }\n\n      /* ── Tables ── */\n      table {\n        font-size: ${(props) => props.theme.font.size.sm};\n\n        thead {\n          tr {\n            th {\n              font-size: ${(props) => props.theme.font.size.xs} !important;\n              color: ${(props) => props.theme.colors.text.muted} !important;\n              border-bottom: 1px solid ${(props) => props.theme.border.border1} !important;\n              padding: 6px 0;\n            }\n          }\n        }\n\n        td {\n          padding: 6px 0;\n          border-bottom: 1px solid ${(props) => props.theme.border.border1};\n          color: ${(props) => props.theme.text};\n        }\n      }\n\n      .parameter__name {\n        font-size: ${(props) => props.theme.font.size.sm};\n        color: ${(props) => props.theme.text};\n\n        &.required::after {\n          color: ${(props) => props.theme.colors.text.danger || '#c0392b'};\n          font-size: ${(props) => props.theme.font.size.xs};\n        }\n      }\n\n      .parameter__type {\n        font-size: ${(props) => props.theme.font.size.xs};\n        color: ${(props) => props.theme.colors.text.muted};\n      }\n\n      .parameter__in {\n        font-size: ${(props) => props.theme.font.size.xs};\n        color: ${(props) => props.theme.colors.text.muted};\n      }\n\n      /* ── Models / Schemas ── */\n      section.models {\n        border: 1px solid ${(props) => props.theme.border.border1};\n        border-radius: 4px;\n        background: ${(props) => props.theme.bg};\n        padding-bottom: 0px;\n        margin-bottom: 40px;\n        margin-top: 8px;\n\n        h4 {\n          font-size: ${(props) => props.theme.font.size.sm};\n          color: ${(props) => props.theme.text};\n          border-bottom: none;\n          padding: 6px 10px;\n          margin: 0;\n\n          svg {\n            fill: ${(props) => props.theme.colors.text.muted};\n            width: 16px;\n            height: 16px;\n          }\n        }\n\n        .model-container {\n          background: ${(props) => props.theme.bg} !important;\n          margin: 0;\n          padding: 4px 8px;\n          border-bottom: 1px solid ${(props) => props.theme.border.border1};\n\n          &:last-child {\n            border-bottom: none;\n          }\n\n          .model-box {\n            background: ${(props) => props.theme.bg} !important;\n            padding: 2px 0;\n          }\n        }\n      }\n\n      .model {\n        font-size: 11px;\n        color: ${(props) => props.theme.text};\n        line-height: 1.4;\n\n        .prop-type {\n          color: ${(props) => props.theme.textLink};\n          font-size: 11px;\n        }\n\n        .prop-format {\n          color: ${(props) => props.theme.colors.text.muted};\n          font-size: 10px;\n        }\n\n        span.prop-enum {\n          display: block;\n          color: ${(props) => props.theme.colors.text.muted};\n          font-size: 10px;\n        }\n      }\n\n      .model-example {\n\n        .tab li {\n          color: ${(props) => props.theme.colors.text.muted} !important;\n        }\n      }\n\n      /* Model expand/collapse toggle */\n      .model-toggle {\n        cursor: pointer;\n        font-size: 10px;\n        color: ${(props) => props.theme.colors.text.muted};\n\n        &::after {\n          color: ${(props) => props.theme.colors.text.muted};\n        }\n      }\n\n      /* Model box inner styling */\n      .model-box {\n        background: ${(props) => props.theme.bg} !important;\n        color: ${(props) => props.theme.text};\n      }\n\n      /* Inner model details */\n      .inner-object {\n        color: ${(props) => props.theme.text};\n      }\n\n      /* Model title (schema name) */\n      .model-title {\n        color: ${(props) => props.theme.text};\n        font-size: 12px;\n        font-weight: 600;\n      }\n\n      /* ── JSON Schema 2020-12 (OpenAPI 3.1) schema overrides ── */\n      .json-schema-2020-12-accordion,\n      .json-schema-2020-12-expand-deep-button,\n      section.models h4 button,\n      .model-box button,\n      .models-control,\n      .opblock-summary,\n      .opblock-summary-control,\n      .opblock-tag {\n        outline: none !important;\n        box-shadow: none !important;\n      }\n\n      button:focus-visible,\n      .opblock-summary:focus-visible,\n      .opblock-tag:focus-visible,\n      .models-control:focus-visible {\n        outline: 2px solid ${(props) => props.theme.textLink} !important;\n        outline-offset: 2px;\n      }\n\n      .json-schema-2020-12__title {\n        font-size: 12px !important;\n        font-weight: 600;\n        color: ${(props) => props.theme.text} !important;\n      }\n\n      .json-schema-2020-12-head {\n        padding: 4px 8px !important;\n        background: ${(props) => props.theme.bg} !important;\n\n        .json-schema-2020-12-accordion {\n          padding: 0 !important;\n          color: ${(props) => props.theme.text} !important;\n          background: transparent !important;\n        }\n\n        /* chevron / arrow icon */\n        .json-schema-2020-12-accordion__icon {\n          fill: ${(props) => props.theme.colors.text.muted} !important;\n        }\n\n        button.json-schema-2020-12-expand-deep-button {\n          font-size: 10px !important;\n          color: ${(props) => props.theme.colors.text.muted} !important;\n          background: transparent !important;\n          padding: 0 4px !important;\n        }\n\n        strong.json-schema-2020-12__attribute--primary {\n          font-size: 11px !important;\n          color: ${(props) => props.theme.textLink} !important;\n          font-weight: normal;\n        }\n      }\n\n      .json-schema-2020-12-body {\n        font-size: 11px !important;\n        margin-left: 16px;\n        color: ${(props) => props.theme.text} !important;\n\n        .json-schema-2020-12-property {\n          margin-left: 8px;\n          color: ${(props) => props.theme.text} !important;\n          border-color: ${(props) => props.theme.border.border1} !important;\n        }\n\n        /* property names */\n        .json-schema-2020-12__title {\n          font-size: 11px !important;\n          font-weight: normal;\n          color: ${(props) => props.theme.text} !important;\n        }\n\n        /* type badges inside expanded schema */\n        strong.json-schema-2020-12__attribute--primary {\n          font-size: 10px !important;\n          color: ${(props) => props.theme.textLink} !important;\n          font-weight: normal;\n        }\n\n        strong.json-schema-2020-12__attribute {\n          font-size: 10px !important;\n          color: ${(props) => props.theme.colors.text.muted} !important;\n          font-weight: normal;\n        }\n      }\n\n      .json-schema-2020-12 {\n        font-size: 11px !important;\n        margin: 0 !important;\n        width: 100%;\n        height: 100%;\n        color: ${(props) => props.theme.text} !important;\n        background: ${(props) => props.theme.bg} !important;\n      }\n\n      /* JSON viewer (Examples section inside schema properties) */\n      .json-schema-2020-12-json-viewer {\n        background: transparent !important;\n        color: ${(props) => props.theme.text} !important;\n      }\n\n      .json-schema-2020-12-json-viewer__name {\n        color: ${(props) => props.theme.text} !important;\n      }\n\n      .json-schema-2020-12-json-viewer__name--secondary {\n        color: ${(props) => props.theme.colors.text.muted} !important;\n        font-weight: normal !important;\n      }\n\n      .json-schema-2020-12-json-viewer__value {\n        color: ${(props) => props.theme.text} !important;\n      }\n\n      .json-schema-2020-12-json-viewer__value--secondary {\n        color: ${(props) => props.theme.colors.text.subtext0} !important;\n      }\n\n      .json-schema-2020-12-json-viewer__value--string,\n      .json-schema-2020-12-json-viewer__value--string.json-schema-2020-12-json-viewer__value--secondary {\n        color: ${(props) => props.theme.colors.text.green} !important;\n      }\n\n      .json-schema-2020-12-json-viewer__value--number,\n      .json-schema-2020-12-json-viewer__value--bigint,\n      .json-schema-2020-12-json-viewer__value--number.json-schema-2020-12-json-viewer__value--secondary,\n      .json-schema-2020-12-json-viewer__value--bigint.json-schema-2020-12-json-viewer__value--secondary {\n        color: ${(props) => props.theme.textLink} !important;\n      }\n\n      .json-schema-2020-12-json-viewer__value--boolean,\n      .json-schema-2020-12-json-viewer__value--boolean.json-schema-2020-12-json-viewer__value--secondary {\n        color: ${(props) => props.theme.colors.text.warning} !important;\n      }\n\n      .json-schema-2020-12-json-viewer__value--null,\n      .json-schema-2020-12-json-viewer__value--undefined {\n        color: ${(props) => props.theme.colors.text.muted} !important;\n      }\n\n      /* enum/keyword example values container */\n      .json-schema-2020-12-keyword--examples,\n      [data-json-schema-keyword=\"examples\"] {\n        color: ${(props) => props.theme.text} !important;\n      }\n\n      /* Model collapse/expand all link */\n      span.model-toggle {\n        color: ${(props) => props.theme.colors.text.muted};\n        font-size: 10px;\n      }\n\n      /* Brace styling in models */\n      .brace-open, .brace-close {\n        color: ${(props) => props.theme.colors.text.muted};\n        font-size: 11px;\n      }\n\n      /* ── Code / Response blocks ── */\n      .microlight {\n        background: ${(props) => props.theme.codemirror.bg} !important;\n        color: ${(props) => props.theme.text} !important;\n        font-size: ${(props) => props.theme.font.size.xs};\n        border-radius: 4px;\n        padding: 8px;\n        border: 1px solid ${(props) => props.theme.border.border1};\n      }\n\n      .highlight-code {\n        background: ${(props) => props.theme.codemirror.bg} !important;\n\n        > .microlight {\n          border: none;\n        }\n      }\n\n      pre {\n        color: ${(props) => props.theme.text};\n        font-size: ${(props) => props.theme.font.size.xs};\n        border-radius: 4px;\n      }\n\n      .response-col_status {\n        font-size: ${(props) => props.theme.font.size.sm};\n        color: ${(props) => props.theme.text};\n      }\n\n      .response-col_description {\n        font-size: ${(props) => props.theme.font.size.sm};\n        color: ${(props) => props.theme.colors.text.muted};\n      }\n\n      .responses-inner {\n        h4, h5 {\n          font-size: ${(props) => props.theme.font.size.sm};\n          color: ${(props) => props.theme.text};\n        }\n      }\n\n      /* ── Buttons ── */\n      .btn {\n        font-size: ${(props) => props.theme.font.size.xs};\n        border-radius: 4px;\n        box-shadow: none !important;\n        color: ${(props) => props.theme.text};\n        border-color: ${(props) => props.theme.border.border1};\n        background: transparent;\n      }\n\n      .btn.authorize {\n        color: ${(props) => props.theme.text};\n        border-color: ${(props) => props.theme.border.border1};\n        background: transparent;\n\n        svg {\n          fill: ${(props) => props.theme.text};\n        }\n\n        span {\n          color: ${(props) => props.theme.text};\n        }\n      }\n\n      .btn.execute {\n        background: ${(props) => props.theme.primary?.solid || props.theme.textLink};\n        color: #fff;\n        border-color: transparent;\n      }\n\n      .btn-group {\n        .btn {\n          background: ${(props) => props.theme.bg};\n          color: ${(props) => props.theme.text};\n        }\n      }\n\n      /* ── Links ── */\n      a {\n        color: ${(props) => props.theme.textLink};\n      }\n\n      /* ── Servers / Scheme container ── */\n      .scheme-container {\n        background: ${(props) => props.theme.background.mantle} !important;\n        border-top: 1px solid ${(props) => props.theme.border.border1};\n        border-bottom: 1px solid ${(props) => props.theme.border.border1};\n        padding: 10px;\n        box-shadow: none !important;\n\n        .schemes-title {\n          font-size: ${(props) => props.theme.font.size.sm};\n          color: ${(props) => props.theme.colors.text.muted};\n        }\n\n        label {\n          font-size: ${(props) => props.theme.font.size.sm};\n          color: ${(props) => props.theme.colors.text.muted};\n        }\n\n        select {\n          font-size: ${(props) => props.theme.font.size.sm};\n          color: ${(props) => props.theme.text};\n          background: ${(props) => props.theme.bg};\n          border: 1px solid ${(props) => props.theme.border.border1};\n          border-radius: 4px;\n          padding: 4px 8px;\n        }\n      }\n\n      /* ── SVGs / icons ── */\n      svg {\n        fill: ${(props) => props.theme.colors.text.muted};\n      }\n\n      svg.arrow {\n        fill: ${(props) => props.theme.text};\n        width: 12px;\n        height: 12px;\n        margin-left: 4px;\n      }\n\n      .expand-operation svg {\n        fill: ${(props) => props.theme.colors.text.muted};\n        width: 14px;\n        height: 14px;\n      }\n\n      /* ── Misc / catch-all ── */\n      .loading-container .loading::after {\n        color: ${(props) => props.theme.colors.text.muted};\n        font-size: ${(props) => props.theme.font.size.sm};\n      }\n\n      .renderedMarkdown p {\n        color: ${(props) => props.theme.colors.text.muted};\n        font-size: ${(props) => props.theme.font.size.sm};\n      }\n\n      .opblock-section-header {\n        background: ${(props) => props.theme.background.mantle} !important;\n        box-shadow: none !important;\n        border-bottom: 1px solid ${(props) => props.theme.border.border1};\n        padding: 6px 10px;\n\n        h4 {\n          font-size: ${(props) => props.theme.font.size.sm};\n          color: ${(props) => props.theme.text};\n        }\n\n        label {\n          font-size: ${(props) => props.theme.font.size.xs};\n          color: ${(props) => props.theme.colors.text.muted};\n        }\n      }\n\n      .copy-to-clipboard {\n        button {\n          background: ${(props) => props.theme.background.mantle};\n          border: 1px solid ${(props) => props.theme.border.border1};\n          border-radius: 3px;\n        }\n      }\n\n      /* Dialog / modal overrides */\n      .dialog-ux {\n        .modal-ux {\n          background: ${(props) => props.theme.bg};\n          border: 1px solid ${(props) => props.theme.border.border1};\n          border-radius: 6px;\n          color: ${(props) => props.theme.text};\n          box-shadow: 0 8px 32px rgba(0,0,0,0.4);\n\n          .modal-ux-header {\n            border-bottom: 1px solid ${(props) => props.theme.border.border1};\n            padding: 12px 0px;\n\n            h3 {\n              font-size: ${(props) => props.theme.font.size.md};\n              font-weight: 600;\n              color: ${(props) => props.theme.text};\n            }\n\n            .close-modal {\n              opacity: 0.6;\n              &:hover { opacity: 1; }\n              svg { fill: ${(props) => props.theme.text}; }\n            }\n          }\n\n          .modal-ux-content {\n            color: ${(props) => props.theme.text};\n            padding: 12px 16px;\n\n            p {\n              font-size: ${(props) => props.theme.font.size.sm};\n              color: ${(props) => props.theme.colors.text.muted};\n            }\n\n            /* Section headings like \"api_key (apiKey)\" */\n            h4, h5, h6 {\n              font-size: ${(props) => props.theme.font.size.sm};\n              font-weight: 600;\n              color: ${(props) => props.theme.textLink};\n              margin: 12px 0 6px;\n            }\n\n            /* Labels: \"Name:\", \"In:\", \"Flow:\", \"Value:\", etc. */\n            label {\n              font-size: ${(props) => props.theme.font.size.sm};\n              color: ${(props) => props.theme.text};\n\n              > span {\n                font-size: ${(props) => props.theme.font.size.sm};\n                color: ${(props) => props.theme.colors.text.muted};\n              }\n            }\n\n            /* \"Scopes:\" heading */\n            .scopes h2 {\n              font-size: ${(props) => props.theme.font.size.sm} !important;\n              font-weight: 500;\n              color: ${(props) => props.theme.text} !important;\n            }\n\n            /* Scope item name + description */\n            .scopes .checkbox {\n              p.name {\n                font-size: ${(props) => props.theme.font.size.sm} !important;\n                color: ${(props) => props.theme.text} !important;\n                font-weight: 500;\n                margin: 0;\n              }\n\n              p.description {\n                font-size: ${(props) => props.theme.font.size.xs} !important;\n                color: ${(props) => props.theme.colors.text.muted} !important;\n                margin: 0;\n              }\n            }\n\n            /* Text inputs */\n            input[type=\"text\"],\n            input[type=\"password\"],\n            input[type=\"email\"] {\n              background: ${(props) => props.theme.background.mantle} !important;\n              color: ${(props) => props.theme.text} !important;\n              border: 1px solid ${(props) => props.theme.border.border1} !important;\n              border-radius: 4px !important;\n              font-size: ${(props) => props.theme.font.size.sm} !important;\n              padding: 6px 10px !important;\n              outline: none !important;\n              box-shadow: none !important;\n\n              &:focus {\n                border-color: ${(props) => props.theme.textLink} !important;\n                outline: none !important;\n                box-shadow: none !important;\n              }\n            }\n\n            /* Checkboxes — custom styled to match theme */\n            input[type=\"checkbox\"] {\n              appearance: none !important;\n              -webkit-appearance: none !important;\n              width: 14px !important;\n              height: 14px !important;\n              min-width: 14px;\n              border: 1px solid ${(props) => props.theme.border.border1} !important;\n              border-radius: 3px !important;\n              background: ${(props) => props.theme.background.mantle} !important;\n              cursor: pointer;\n              position: relative;\n              vertical-align: middle;\n\n              &:checked {\n                background: ${(props) => props.theme.textLink} !important;\n                border-color: ${(props) => props.theme.textLink} !important;\n\n                &::after {\n                  content: '';\n                  position: absolute;\n                  left: 3px;\n                  top: 1px;\n                  width: 5px;\n                  height: 8px;\n                  border: 2px solid #fff;\n                  border-top: none;\n                  border-left: none;\n                  transform: rotate(45deg);\n                }\n              }\n            }\n\n            /* \"select all / select none\" links */\n            a {\n              font-size: ${(props) => props.theme.font.size.xs};\n              color: ${(props) => props.theme.textLink};\n            }\n\n            /* Dividers between auth sections */\n            hr {\n              border-color: ${(props) => props.theme.border.border1};\n              margin: 12px 0;\n            }\n\n            /* Authorize / Close buttons */\n            .btn-done,\n            .auth-btn-wrapper .btn {\n              font-size: ${(props) => props.theme.font.size.sm};\n              border-radius: 4px;\n              padding: 6px 16px;\n              border: 1px solid ${(props) => props.theme.border.border1};\n              background: transparent;\n              color: ${(props) => props.theme.text};\n              cursor: pointer;\n              outline: none !important;\n              box-shadow: none !important;\n\n              &:hover {\n                background: ${(props) => props.theme.background.mantle};\n              }\n\n              &.modal-btn-operation {\n                background: ${(props) => props.theme.textLink};\n                color: #fff;\n                border-color: transparent;\n\n                &:hover {\n                  opacity: 0.9;\n                }\n              }\n            }\n          }\n        }\n\n        .backdrop-ux {\n          background: rgba(0, 0, 0, 0.5);\n        }\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ApiSpecPanel/Renderers/Swagger/index.js",
    "content": "import SwaggerUI from 'swagger-ui-react';\nimport StyledWrapper from './StyledWrapper';\n\nconst Swagger = ({ spec }) => {\n  return (\n    <StyledWrapper>\n      <div className=\"swagger-root w-full\">\n        <SwaggerUI spec={spec} />\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Swagger;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ApiSpecPanel/SpecViewer.js",
    "content": "import React, { useState, useEffect, Suspense } from 'react';\nimport get from 'lodash/get';\nimport { useTheme } from 'providers/Theme';\nimport { useSelector } from 'react-redux';\nimport { IconDeviceFloppy } from '@tabler/icons';\nimport CodeEditor from './FileEditor/CodeEditor/index';\nimport Swagger from './Renderers/Swagger';\n\n/**\n * Shared split-pane spec viewer: CodeEditor (left) + Swagger preview (right).\n *\n * Props:\n *  - content    (string)   The spec content (YAML/JSON string)\n *  - readOnly   (boolean)  If true, editor is not editable and save icon is hidden\n *  - onSave     (function) Called with current editor content on save (editable mode only)\n */\nconst SpecViewer = ({ content, readOnly, onSave }) => {\n  const { displayedTheme, theme } = useTheme();\n  const preferences = useSelector((state) => state.app.preferences);\n\n  const [editorContent, setEditorContent] = useState(content);\n\n  // Sync editor when saved content changes from outside (e.g. after save completes)\n  useEffect(() => {\n    setEditorContent(content);\n  }, [content]);\n\n  const hasChanges = !readOnly && editorContent !== content;\n\n  const handleSave = () => {\n    if (onSave) onSave(editorContent);\n  };\n\n  return (\n    <section className=\"main flex flex-grow pl-4 relative\">\n      <div className=\"w-full grid grid-cols-2\">\n        <div className=\"col-span-1\">\n          <div className=\"flex flex-grow relative\">\n            <CodeEditor\n              theme={displayedTheme}\n              value={readOnly ? content : editorContent}\n              readOnly={readOnly ? 'nocursor' : false}\n              onEdit={readOnly ? undefined : (val) => setEditorContent(val)}\n              onSave={readOnly ? undefined : handleSave}\n              mode=\"yaml\"\n              font={get(preferences, 'font.codeFont', 'default')}\n            />\n            {!readOnly && onSave && (\n              <IconDeviceFloppy\n                onClick={handleSave}\n                color={hasChanges ? theme.draftColor : theme.requestTabs.icon.color}\n                strokeWidth={1.5}\n                size={22}\n                className={`absolute right-0 top-0 m-4 ${\n                  hasChanges ? 'cursor-pointer opacity-100' : 'cursor-default opacity-50'\n                }`}\n              />\n            )}\n          </div>\n        </div>\n        <div className=\"col-span-1\">\n          <Suspense fallback=\"\">\n            <Swagger spec={content} />\n          </Suspense>\n        </div>\n      </div>\n    </section>\n  );\n};\n\nexport default SpecViewer;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ApiSpecPanel/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .menu-icon {\n    cursor: pointer;\n    color: ${(props) => props.theme.sidebar.dropdownIcon.color};\n  }\n\n  div.dropdown-item.menu-item {\n    color: ${(props) => props.theme.colors.text.danger};\n    &:hover {\n      background-color: ${(props) => props.theme.colors.bg.danger};\n      color: white;\n    }\n  }\n\n  .react-tooltip {\n    z-index: 10;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ApiSpecPanel/index.js",
    "content": "import React, { forwardRef, useRef } from 'react';\nimport find from 'lodash/find';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { IconFileCode, IconDots } from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\nimport SpecViewer from './SpecViewer';\nimport Dropdown from 'components/Dropdown';\nimport { openApiSpec, saveApiSpecToFile } from 'providers/ReduxStore/slices/apiSpec';\nimport { useState } from 'react';\nimport CreateApiSpec from 'components/Sidebar/ApiSpecs/CreateApiSpec';\nimport toast from 'react-hot-toast';\n\nconst ApiSpecPanel = () => {\n  const dispatch = useDispatch();\n\n  const [createApiSpecModalOpen, setCreateApiSpecModalOpen] = useState(false);\n\n  const { apiSpecs, activeApiSpecUid } = useSelector((state) => state.apiSpec);\n\n  const dropdownTippyRef = useRef();\n  const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);\n\n  let apiSpec = find(apiSpecs, (c) => c.uid === activeApiSpecUid);\n  const { filename, pathname, raw, uid } = apiSpec || {};\n  if (!uid) {\n    return <div className=\"p-4 opacity-50\">API Spec not found!</div>;\n  }\n\n  const MenuIcon = forwardRef((props, ref) => {\n    return (\n      <div ref={ref}>\n        <IconDots size={22} />\n      </div>\n    );\n  });\n\n  const handleOpenApiSpec = () => {\n    dispatch(openApiSpec()).catch(\n      (err) => console.log(err) && toast.error('An error occurred while opening the API spec')\n    );\n  };\n\n  return (\n    <StyledWrapper className=\"flex flex-col flex-grow relative\">\n      {createApiSpecModalOpen ? <CreateApiSpec onClose={() => setCreateApiSpecModalOpen(false)} /> : null}\n      <div className=\"p-3 mb-2 w-full flex flex-row justify-between grid grid-cols-3\">\n        <div className=\"flex flex-row justify-start gap-x-4 col-span-1\">\n          <div className=\"flex w-fit items-center cursor-pointer\">\n            <IconFileCode size={18} strokeWidth={1.5} />\n            <span className=\"ml-2 mr-4 font-semibold\">API Designer</span>\n          </div>\n        </div>\n        <div className=\"w-full col-span-1 flex justify-center\" title={pathname}>\n          {filename}\n        </div>\n        <div className=\"menu-icon pr-2 col-span-1 flex justify-end\">\n          <Dropdown onCreate={onDropdownCreate} icon={<MenuIcon />} placement=\"bottom-start\">\n            <div\n              className=\"dropdown-item\"\n              onClick={(e) => {\n                dropdownTippyRef.current.hide();\n                setCreateApiSpecModalOpen(true);\n              }}\n            >\n              Create API Spec\n            </div>\n            <div\n              className=\"dropdown-item\"\n              onClick={(e) => {\n                dropdownTippyRef.current.hide();\n                handleOpenApiSpec();\n              }}\n            >\n              Open API Spec\n            </div>\n          </Dropdown>\n        </div>\n      </div>\n      <SpecViewer\n        content={raw}\n        onSave={(content) => dispatch(saveApiSpecToFile({ uid, content }))}\n      />\n    </StyledWrapper>\n  );\n};\n\nexport default ApiSpecPanel;\n"
  },
  {
    "path": "packages/bruno-app/src/components/AppTitleBar/AppMenu/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  align-items: center;\n  height: 100%;\n  -webkit-app-region: no-drag;\n\n  .shortcut {\n    font-size: 11px;\n    color: ${(props) => props.theme.dropdown.mutedText};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/AppTitleBar/AppMenu/index.js",
    "content": "import React, { useState } from 'react';\nimport { IconMenu2 } from '@tabler/icons';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport ActionIcon from 'ui/ActionIcon';\nimport StyledWrapper from './StyledWrapper';\n\nconst AppMenu = () => {\n  const [isOpen, setIsOpen] = useState(false);\n  const { ipcRenderer } = window;\n\n  const menuItems = [\n    {\n      id: 'file',\n      label: 'File',\n      submenu: [\n        {\n          id: 'open-collection',\n          label: 'Open Collection',\n          onClick: () => ipcRenderer?.invoke('renderer:open-collection')\n        },\n        { type: 'divider', id: 'file-div-1' },\n        {\n          id: 'preferences',\n          label: 'Preferences',\n          rightSection: <span className=\"shortcut\">Ctrl+,</span>,\n          onClick: () => ipcRenderer?.invoke('renderer:open-preferences')\n        },\n        { type: 'divider', id: 'file-div-2' },\n        {\n          id: 'quit',\n          label: 'Quit',\n          rightSection: <span className=\"shortcut\">Alt+F4</span>,\n          onClick: () => ipcRenderer?.send('renderer:window-close')\n        }\n      ]\n    },\n    {\n      id: 'edit',\n      label: 'Edit',\n      submenu: [\n        {\n          id: 'undo',\n          label: 'Undo',\n          rightSection: <span className=\"shortcut\">Ctrl+Z</span>,\n          onClick: () => document.execCommand('undo')\n        },\n        {\n          id: 'redo',\n          label: 'Redo',\n          rightSection: <span className=\"shortcut\">Ctrl+Y</span>,\n          onClick: () => document.execCommand('redo')\n        },\n        { type: 'divider', id: 'edit-div-1' },\n        {\n          id: 'cut',\n          label: 'Cut',\n          rightSection: <span className=\"shortcut\">Ctrl+X</span>,\n          onClick: () => document.execCommand('cut')\n        },\n        {\n          id: 'copy',\n          label: 'Copy',\n          rightSection: <span className=\"shortcut\">Ctrl+C</span>,\n          onClick: () => document.execCommand('copy')\n        },\n        {\n          id: 'paste',\n          label: 'Paste',\n          rightSection: <span className=\"shortcut\">Ctrl+V</span>,\n          onClick: () => document.execCommand('paste')\n        },\n        { type: 'divider', id: 'edit-div-2' },\n        {\n          id: 'select-all',\n          label: 'Select All',\n          rightSection: <span className=\"shortcut\">Ctrl+A</span>,\n          onClick: () => document.execCommand('selectAll')\n        }\n      ]\n    },\n    {\n      id: 'view',\n      label: 'View',\n      submenu: [\n        {\n          id: 'toggle-devtools',\n          label: 'Developer Tools',\n          rightSection: <span className=\"shortcut\">Ctrl+Shift+I</span>,\n          onClick: () => ipcRenderer?.invoke('renderer:toggle-devtools')\n        },\n        { type: 'divider', id: 'view-div-1' },\n        {\n          id: 'reset-zoom',\n          label: 'Reset Zoom',\n          rightSection: <span className=\"shortcut\">Ctrl+0</span>,\n          onClick: () => ipcRenderer?.invoke('renderer:reset-zoom')\n        },\n        {\n          id: 'zoom-in',\n          label: 'Zoom In',\n          rightSection: <span className=\"shortcut\">Ctrl++</span>,\n          onClick: () => ipcRenderer?.invoke('renderer:zoom-in')\n        },\n        {\n          id: 'zoom-out',\n          label: 'Zoom Out',\n          rightSection: <span className=\"shortcut\">Ctrl+-</span>,\n          onClick: () => ipcRenderer?.invoke('renderer:zoom-out')\n        },\n        { type: 'divider', id: 'view-div-2' },\n        {\n          id: 'toggle-fullscreen',\n          label: 'Full Screen',\n          rightSection: <span className=\"shortcut\">F11</span>,\n          onClick: () => ipcRenderer?.invoke('renderer:toggle-fullscreen')\n        }\n      ]\n    },\n    {\n      id: 'help',\n      label: 'Help',\n      submenu: [\n        {\n          id: 'about',\n          label: 'About Bruno',\n          onClick: () => ipcRenderer?.invoke('renderer:open-about')\n        },\n        {\n          id: 'documentation',\n          label: 'Documentation',\n          onClick: () => ipcRenderer?.invoke('renderer:open-docs')\n        }\n      ]\n    }\n  ];\n\n  return (\n    <StyledWrapper>\n      <MenuDropdown\n        opened={isOpen}\n        onChange={setIsOpen}\n        placement=\"bottom-start\"\n        showTickMark={false}\n        items={menuItems}\n      >\n        <ActionIcon label=\"Menu\" size=\"lg\">\n          <IconMenu2 size={16} stroke={1.5} />\n        </ActionIcon>\n      </MenuDropdown>\n    </StyledWrapper>\n  );\n};\n\nexport default AppMenu;\n"
  },
  {
    "path": "packages/bruno-app/src/components/AppTitleBar/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  height: 36px;\n  display: flex;\n  align-items: center;\n  background: ${(props) => props.theme.sidebar.bg};\n  -webkit-app-region: drag;\n  user-select: none;\n\n  .titlebar-content {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    width: 100%;\n    height: 100%;\n    padding: 0 12px;\n    padding-left: 70px; /* Space for macOS window controls */\n    transition: padding-left 0.15s ease;\n  }\n\n  /* When in full screen, no traffic lights so reduce padding */\n  &.fullscreen .titlebar-content {\n    padding-left: 6px;\n  }\n\n  /* Remove drag region from interactive elements */\n  .workspace-name-container,\n  .dropdown-item,\n  .home-button,\n  .dropdown,\n  button {\n    -webkit-app-region: no-drag;\n  }\n\n  /* Left section */\n  .titlebar-left {\n    display: flex;\n    align-items: center;\n    flex-shrink: 0;\n    margin-left: 10px;\n    -webkit-app-region: no-drag;\n  }\n\n  /* When in full screen, no traffic lights so remove margin-left */\n  &.fullscreen .titlebar-left {\n    margin-left: 0px;\n  }\n\n  /* Workspace Name Dropdown Trigger */\n  .workspace-name-container {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    padding: 5px 10px;\n    border-radius: 6px;\n    cursor: pointer;\n    transition: all 0.15s ease;\n\n    &:hover {\n      background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n    }\n\n    .workspace-name {\n      font-size: 13px;\n      font-weight: 500;\n      color: ${(props) => props.theme.sidebar.color};\n      white-space: nowrap;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      max-width: 180px;\n    }\n\n    .chevron-icon {\n      flex-shrink: 0;\n      color: ${(props) => props.theme.sidebar.muted};\n      transition: transform 0.2s ease;\n    }\n  }\n\n  /* Center section - Bruno branding */\n  .titlebar-center {\n    position: absolute;\n    left: 50%;\n    transform: translateX(-50%);\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    pointer-events: none;\n\n    .bruno-text {\n      font-size: 13px;\n      font-weight: 600;\n      color: ${(props) => props.theme.text};\n      letter-spacing: 0.5px;\n    }\n  }\n\n  /* Right section */\n  .titlebar-right {\n    display: flex;\n    align-items: center;\n    justify-content: flex-end;\n    flex-shrink: 0;\n    -webkit-app-region: no-drag;\n  }\n\n  /* App action buttons container */\n  .titlebar-actions {\n    display: flex;\n    align-items: center;\n  }\n\n  /* Workspace Dropdown Styles */\n  .workspace-item {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 4px 10px !important;\n    margin: 0 !important;\n\n    &.active {\n      .check-icon {\n        opacity: 1;\n      }\n    }\n\n    &:hover {\n      .pin-btn:not(.pinned) {\n        opacity: 1;\n      }\n    }\n\n    .workspace-name {\n      flex: 1;\n      min-width: 0;\n      font-size: 13px;\n      font-weight: 400;\n      color: ${(props) => props.theme.dropdown.color};\n      white-space: nowrap;\n      overflow: hidden;\n      text-overflow: ellipsis;\n    }\n\n    .workspace-actions {\n      display: flex;\n      align-items: center;\n      gap: 4px;\n      margin-left: 8px;\n      flex-shrink: 0;\n      pointer-events: none;\n\n      > * {\n        pointer-events: auto;\n      }\n    }\n\n    .check-icon {\n      color: ${(props) => props.theme.workspace?.accent || props.theme.colors?.text?.yellow};\n      flex-shrink: 0;\n    }\n\n    .pin-btn {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      width: 22px;\n      height: 22px;\n      padding: 0;\n      border: none;\n      background: transparent;\n      border-radius: 4px;\n      cursor: pointer;\n      color: ${(props) => props.theme.dropdown.mutedText};\n      transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease;\n      opacity: 0;\n\n      &.pinned {\n        opacity: 1;\n      }\n\n      &:hover {\n        background: ${(props) => props.theme.dropdown.hoverBg};\n        color: ${(props) => props.theme.dropdown.mutedText};\n      }\n    }\n  }\n\n  /* Adjust for non-macOS platforms */\n  &:not(.os-mac) .titlebar-content {\n    padding-left: 12px;\n  }\n\n  /* Windows-specific styles */\n  &.os-windows .titlebar-content {\n    padding-right: 0px;\n    padding-left: 0px;\n  }\n\n  &.os-windows .titlebar-left {\n    margin-left: 6px;\n  }\n\n  &.os-linux .titlebar-content {\n    padding-right: 0px;\n    padding-left: 0px;\n  }\n\n  &.os-linux .titlebar-left {\n    margin-left: 6px;\n  }\n\n  .app-menu {\n    margin-left: 8px;\n  }\n\n  /* Custom window control buttons for Windows - always interactive, above modal overlay */\n  .window-controls {\n    display: flex;\n    align-items: stretch;\n    height: 36px;\n    margin-left: 8px;\n    position: relative;\n    z-index: 1000;\n  }\n\n  .window-control-btn {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 46px;\n    height: 100%;\n    border: none;\n    background: transparent;\n    color: ${(props) => props.theme.text};\n    cursor: pointer;\n    transition: background-color 0.1s ease;\n    -webkit-app-region: no-drag;\n\n    &:hover {\n      background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n    }\n\n    &:active {\n      background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n    }\n\n    &.close:hover {\n      background: #e81123;\n      color: white;\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/AppTitleBar/index.js",
    "content": "import React from 'react';\nimport { IconCheck, IconChevronDown, IconFolder, IconHome, IconPin, IconPinned, IconPlus, IconDownload, IconSettings, IconMinus, IconSquare, IconX, IconCopy } from '@tabler/icons';\nimport { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';\nimport toast from 'react-hot-toast';\nimport { useDispatch, useSelector } from 'react-redux';\n\nimport { savePreferences, showManageWorkspacePage, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';\nimport { closeConsole, openConsole } from 'providers/ReduxStore/slices/logs';\nimport { createWorkspaceWithUniqueName, openWorkspaceDialog, switchWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';\nimport { sortWorkspaces, toggleWorkspacePin } from 'utils/workspaces';\nimport { focusTab } from 'providers/ReduxStore/slices/tabs';\nimport get from 'lodash/get';\n\nimport Bruno from 'components/Bruno';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport ActionIcon from 'ui/ActionIcon';\nimport IconSidebarToggle from 'components/Icons/IconSidebarToggle';\nimport CreateWorkspace from 'components/WorkspaceSidebar/CreateWorkspace';\nimport ImportWorkspace from 'components/WorkspaceSidebar/ImportWorkspace';\n\nimport IconBottombarToggle from 'components/Icons/IconBottombarToggle/index';\nimport AppMenu from './AppMenu';\nimport StyledWrapper from './StyledWrapper';\nimport ResponseLayoutToggle from 'components/ResponsePane/ResponseLayoutToggle';\nimport { isMacOS, isWindowsOS, isLinuxOS } from 'utils/common/platform';\nimport classNames from 'classnames';\n\nconst getOsClass = () => {\n  if (isMacOS()) return 'os-mac';\n  if (isWindowsOS()) return 'os-windows';\n  if (isLinuxOS()) return 'os-linux';\n  return 'os-other';\n};\n\n// Helper to get display name for workspace\nexport const getWorkspaceDisplayName = (name) => {\n  if (!name) return 'Untitled Workspace';\n  return name;\n};\n\nconst AppTitleBar = () => {\n  const dispatch = useDispatch();\n  const [isFullScreen, setIsFullScreen] = useState(false);\n  const [isMaximized, setIsMaximized] = useState(false);\n  const osClass = getOsClass();\n  const isWindows = osClass === 'os-windows';\n  const isLinux = osClass === 'os-linux';\n  const showWindowControls = isWindows || isLinux;\n\n  // Listen for fullscreen changes\n  useEffect(() => {\n    const { ipcRenderer } = window;\n    if (!ipcRenderer) return;\n\n    const removeEnterFullScreenListener = ipcRenderer.on('main:enter-full-screen', () => {\n      setIsFullScreen(true);\n    });\n\n    const removeLeaveFullScreenListener = ipcRenderer.on('main:leave-full-screen', () => {\n      setIsFullScreen(false);\n    });\n\n    return () => {\n      removeEnterFullScreenListener();\n      removeLeaveFullScreenListener();\n    };\n  }, []);\n\n  useEffect(() => {\n    if (!showWindowControls) return;\n    const { ipcRenderer } = window;\n    if (!ipcRenderer) return;\n\n    ipcRenderer.invoke('renderer:window-is-maximized')\n      .then((maximized) => {\n        setIsMaximized(maximized);\n      })\n      .catch((error) => {\n        console.error('Error getting initial maximized state:', error);\n      });\n\n    const removeMaximizedListener = ipcRenderer.on('main:window-maximized', () => {\n      setIsMaximized(true);\n    });\n\n    const removeUnmaximizedListener = ipcRenderer.on('main:window-unmaximized', () => {\n      setIsMaximized(false);\n    });\n\n    return () => {\n      removeMaximizedListener();\n      removeUnmaximizedListener();\n    };\n  }, [showWindowControls]);\n\n  const handleMinimize = useCallback(() => {\n    window.ipcRenderer?.send('renderer:window-minimize');\n  }, []);\n\n  const handleMaximize = useCallback(() => {\n    window.ipcRenderer?.send('renderer:window-maximize');\n    // State will be updated via IPC events from main process (main:window-maximized/main:window-unmaximized)\n  }, []);\n\n  const handleClose = useCallback(() => {\n    window.ipcRenderer?.send('renderer:window-close');\n  }, []);\n\n  // Get workspace info\n  const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);\n  const preferences = useSelector((state) => state.app.preferences);\n  const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);\n  const isConsoleOpen = useSelector((state) => state.logs.isConsoleOpen);\n  const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);\n\n  // Sort workspaces according to preferences\n  const sortedWorkspaces = useMemo(() => {\n    return sortWorkspaces(workspaces, preferences);\n  }, [workspaces, preferences]);\n\n  const [createWorkspaceModalOpen, setCreateWorkspaceModalOpen] = useState(false);\n  const [importWorkspaceModalOpen, setImportWorkspaceModalOpen] = useState(false);\n\n  const WorkspaceName = forwardRef((props, ref) => {\n    return (\n      <div ref={ref} className=\"workspace-name-container\" {...props}>\n        <span data-testid=\"workspace-name\" className={classNames('workspace-name', { 'italic text-muted': !activeWorkspace?.name })}>{getWorkspaceDisplayName(activeWorkspace?.name)}</span>\n        <IconChevronDown size={14} stroke={1.5} className=\"chevron-icon\" />\n      </div>\n    );\n  });\n\n  const handleHomeClick = () => {\n    const scratchCollectionUid = activeWorkspace?.scratchCollectionUid;\n    if (scratchCollectionUid) {\n      dispatch(focusTab({ uid: `${scratchCollectionUid}-overview` }));\n    }\n  };\n\n  const handleWorkspaceSwitch = (workspaceUid) => {\n    dispatch(switchWorkspace(workspaceUid));\n    toast.success(`Switched to ${getWorkspaceDisplayName(workspaces.find((w) => w.uid === workspaceUid)?.name)}`);\n  };\n\n  const handleOpenWorkspace = async () => {\n    try {\n      await dispatch(openWorkspaceDialog());\n      toast.success('Workspace opened successfully');\n    } catch (error) {\n      toast.error(error.message || 'Failed to open workspace');\n    }\n  };\n\n  const handleCreateWorkspace = useCallback(async () => {\n    const defaultLocation = get(preferences, 'general.defaultLocation', '');\n    if (!defaultLocation) {\n      setCreateWorkspaceModalOpen(true);\n      return;\n    }\n\n    try {\n      await dispatch(createWorkspaceWithUniqueName(defaultLocation));\n    } catch (error) {\n      toast.error(error?.message || 'Failed to create workspace');\n    }\n  }, [preferences, dispatch]);\n\n  const handleManageWorkspaces = () => {\n    dispatch(showManageWorkspacePage());\n  };\n\n  const handleImportWorkspace = () => {\n    setImportWorkspaceModalOpen(true);\n  };\n\n  const handlePinWorkspace = useCallback((workspaceUid, e) => {\n    e.preventDefault();\n    e.stopPropagation();\n    const newPreferences = toggleWorkspacePin(workspaceUid, preferences);\n    dispatch(savePreferences(newPreferences));\n  }, [dispatch, preferences]);\n\n  const handleToggleSidebar = () => {\n    dispatch(toggleSidebarCollapse());\n  };\n\n  const handleToggleDevtools = () => {\n    if (isConsoleOpen) {\n      dispatch(closeConsole());\n    } else {\n      dispatch(openConsole());\n    }\n  };\n\n  // Build workspace menu items\n  const workspaceMenuItems = useMemo(() => {\n    const items = sortedWorkspaces.map((workspace) => {\n      const isActive = workspace.uid === activeWorkspaceUid;\n      const isPinned = preferences?.workspaces?.pinnedWorkspaceUids?.includes(workspace.uid);\n\n      return {\n        id: workspace.uid,\n        label: getWorkspaceDisplayName(workspace.name),\n        onClick: () => handleWorkspaceSwitch(workspace.uid),\n        className: `workspace-item ${isActive ? 'active' : ''}`,\n        rightSection: (\n          <div className=\"workspace-actions\">\n            {workspace.type !== 'default' && (\n              <ActionIcon\n                className={`pin-btn ${isPinned ? 'pinned' : ''}`}\n                onClick={(e) => handlePinWorkspace(workspace.uid, e)}\n                label={isPinned ? 'Unpin workspace' : 'Pin workspace'}\n                size=\"sm\"\n              >\n                {isPinned ? <IconPinned size={14} stroke={1.5} /> : <IconPin size={14} stroke={1.5} />}\n              </ActionIcon>\n            )}\n            {isActive && <IconCheck size={16} stroke={1.5} className=\"check-icon\" />}\n          </div>\n        )\n      };\n    });\n\n    // Add label and action items\n    items.push(\n      { type: 'label', label: 'Workspaces' },\n      {\n        id: 'create-workspace',\n        leftSection: IconPlus,\n        label: 'Create workspace',\n        onClick: handleCreateWorkspace\n      },\n      {\n        id: 'open-workspace',\n        leftSection: IconFolder,\n        label: 'Open workspace',\n        onClick: handleOpenWorkspace\n      },\n      {\n        id: 'import-workspace',\n        leftSection: IconDownload,\n        label: 'Import workspace',\n        onClick: handleImportWorkspace\n      },\n      {\n        id: 'manage-workspaces',\n        leftSection: IconSettings,\n        label: 'Manage workspaces',\n        onClick: handleManageWorkspaces\n      }\n    );\n\n    return items;\n  }, [sortedWorkspaces, activeWorkspaceUid, preferences, handlePinWorkspace, handleCreateWorkspace]);\n\n  return (\n    <StyledWrapper className={`app-titlebar ${osClass} ${isFullScreen ? 'fullscreen' : ''}`}>\n      {createWorkspaceModalOpen && (\n        <CreateWorkspace onClose={() => setCreateWorkspaceModalOpen(false)} />\n      )}\n      {importWorkspaceModalOpen && (\n        <ImportWorkspace onClose={() => setImportWorkspaceModalOpen(false)} />\n      )}\n\n      <div className=\"titlebar-content\">\n        <div className=\"titlebar-left\">\n          {showWindowControls && <AppMenu />}\n\n          <ActionIcon onClick={handleHomeClick} label=\"Home\" size=\"lg\" className=\"home-button\">\n            <IconHome size={16} stroke={1.5} />\n          </ActionIcon>\n\n          {/* Workspace Dropdown */}\n          <MenuDropdown\n            data-testid=\"workspace-menu\"\n            items={workspaceMenuItems}\n            placement=\"bottom-start\"\n            selectedItemId={activeWorkspaceUid}\n          >\n            <WorkspaceName />\n          </MenuDropdown>\n        </div>\n\n        {/* Center section: Bruno logo + text */}\n        <div className=\"titlebar-center\">\n          <Bruno width={18} />\n          <span className=\"bruno-text\">Bruno</span>\n        </div>\n\n        {/* Right section: Action buttons */}\n        <div className=\"titlebar-right\">\n          <div className=\"titlebar-actions\">\n            {/* Toggle sidebar */}\n            <ActionIcon\n              onClick={handleToggleSidebar}\n              label={sidebarCollapsed ? 'Show sidebar' : 'Hide sidebar'}\n              size=\"lg\"\n              data-testid=\"toggle-sidebar-button\"\n            >\n              <IconSidebarToggle collapsed={sidebarCollapsed} size={16} strokeWidth={1.5} />\n            </ActionIcon>\n\n            {/* Toggle devtools */}\n            <ActionIcon\n              onClick={handleToggleDevtools}\n              label={isConsoleOpen ? 'Hide devtools' : 'Show devtools'}\n              size=\"lg\"\n              data-testid=\"toggle-devtools-button\"\n            >\n              <IconBottombarToggle collapsed={!isConsoleOpen} size={16} strokeWidth={1.5} />\n            </ActionIcon>\n\n            <ResponseLayoutToggle />\n          </div>\n\n          {showWindowControls && (\n            <div className=\"window-controls\">\n              <button\n                className=\"window-control-btn minimize\"\n                onClick={handleMinimize}\n                aria-label=\"Minimize\"\n              >\n                <IconMinus size={16} stroke={1} />\n              </button>\n              <button\n                className=\"window-control-btn maximize\"\n                onClick={handleMaximize}\n                aria-label={isMaximized ? 'Restore' : 'Maximize'}\n              >\n                {isMaximized ? <IconCopy size={14} stroke={1} /> : <IconSquare size={14} stroke={1} />}\n              </button>\n              <button\n                className=\"window-control-btn close\"\n                onClick={handleClose}\n                aria-label=\"Close\"\n              >\n                <IconX size={16} stroke={1} />\n              </button>\n            </div>\n          )}\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default AppTitleBar;\n"
  },
  {
    "path": "packages/bruno-app/src/components/BodyModeSelector/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  font-size: ${(props) => props.theme.font.size.base};\n\n  .body-mode-selector {\n    background: transparent;\n    border-radius: 3px;\n\n    .dropdown-item {\n      padding: 0.2rem 0.6rem !important;\n      padding-left: 1.5rem !important;\n      display: flex;\n      align-items: center;\n    }\n\n    .label-item {\n      padding: 0.2rem 0.6rem !important;\n    }\n\n    .selected-body-mode {\n      color: ${(props) => props.theme.primary.text};\n    }\n\n    .dropdown-icon {\n      display: flex;\n      align-items: center;\n      margin-right: 0.5rem;\n    }\n  }\n\n  .caret {\n    color: ${(props) => props.theme.colors.text.muted};\n    fill: ${(props) => props.theme.colors.text.muted};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/BodyModeSelector/index.js",
    "content": "import React, { useMemo } from 'react';\nimport { IconCaretDown, IconForms, IconBraces, IconCode, IconFileText, IconDatabase, IconFile, IconX } from '@tabler/icons';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport { humanizeRequestBodyMode } from 'utils/collections';\nimport StyledWrapper from './StyledWrapper';\n\nconst DEFAULT_MODES = [\n  {\n    name: 'Form',\n    options: [\n      { id: 'multipartForm', label: 'Multipart Form', leftSection: IconForms },\n      { id: 'formUrlEncoded', label: 'Form URL Encoded', leftSection: IconForms }\n    ]\n  },\n  {\n    name: 'Raw',\n    options: [\n      { id: 'json', label: 'JSON', leftSection: IconBraces },\n      { id: 'xml', label: 'XML', leftSection: IconCode },\n      { id: 'text', label: 'TEXT', leftSection: IconFileText },\n      { id: 'sparql', label: 'SPARQL', leftSection: IconDatabase }\n    ]\n  },\n  {\n    name: 'Other',\n    options: [\n      { id: 'file', label: 'File / Binary', leftSection: IconFile },\n      { id: 'none', label: 'No Body', leftSection: IconX }\n    ]\n  }\n];\n\nconst BodyModeSelector = ({\n  currentMode,\n  onModeChange,\n  modes = DEFAULT_MODES,\n  disabled = false,\n  className = '',\n  wrapperClassName = '',\n  placement = 'bottom-end'\n}) => {\n  // Add onClick handlers to mode options\n  const menuItems = useMemo(() => {\n    return modes.map((group) => ({\n      ...group,\n      options: group.options.map((option) => ({\n        ...option,\n        onClick: () => onModeChange(option.id)\n      }))\n    }));\n  }, [modes, onModeChange]);\n\n  return (\n    <StyledWrapper className={wrapperClassName}>\n      <div className={`inline-flex items-center body-mode-selector ${disabled ? 'cursor-default' : 'cursor-pointer'}`}>\n        <MenuDropdown\n          items={menuItems}\n          placement={placement}\n          disabled={disabled}\n          className={className}\n          selectedItemId={currentMode}\n          showGroupDividers={false}\n          groupStyle=\"select\"\n        >\n          <div className=\"flex items-center justify-center pl-3 py-1 select-none selected-body-mode\">\n            {humanizeRequestBodyMode(currentMode)}\n            {' '}\n            <IconCaretDown className=\"caret ml-2\" size={14} strokeWidth={2} />\n          </div>\n        </MenuDropdown>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default BodyModeSelector;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Bruno/index.js",
    "content": "import React from 'react';\n\nconst Bruno = ({ width }) => {\n  return (\n    <svg id=\"emoji\" width={width} viewBox=\"0 0 72 72\" xmlns=\"http://www.w3.org/2000/svg\">\n      <g id=\"color\">\n        <path\n          fill=\"#F4AA41\"\n          stroke=\"none\"\n          d=\"M23.5,14.5855l-4.5,1.75l-7.25,8.5l-4.5,10.75l2,5.25c1.2554,3.7911,3.5231,7.1832,7.25,10l2.5-3.3333 c0,0,3.8218,7.7098,10.7384,8.9598c0,0,10.2616,1.936,15.5949-0.8765c3.4203-1.8037,4.4167-4.4167,4.4167-4.4167l3.4167-3.4167 l1.5833,2.3333l2.0833-0.0833l5.4167-7.25L64,37.3355l-0.1667-4.5l-2.3333-5.5l-4.8333-7.4167c0,0-2.6667-4.9167-8.1667-3.9167 c0,0-6.5-4.8333-11.8333-4.0833S32.0833,10.6688,23.5,14.5855z\"\n        />\n        <polygon\n          fill=\"#EA5A47\"\n          stroke=\"none\"\n          points=\"36,47.2521 32.9167,49.6688 30.4167,49.6688 30.3333,53.5021 31.0833,57.0021 32.1667,58.9188 35,60.4188 39.5833,59.8355 41.1667,58.0855 42.1667,53.8355 41.9167,49.8355 39.9167,50.0855\"\n        />\n        <polygon\n          fill=\"#3F3F3F\"\n          stroke=\"none\"\n          points=\"32.5,36.9188 30.9167,40.6688 33.0833,41.9188 34.3333,42.4188 38.6667,42.5855 41.5833,40.3355 39.8333,37.0855\"\n        />\n      </g>\n      <g id=\"hair\" />\n      <g id=\"skin\" />\n      <g id=\"skin-shadow\" />\n      <g id=\"line\">\n        <path\n          fill=\"#000000\"\n          stroke=\"none\"\n          d=\"M29.5059,30.1088c0,0-1.8051,1.2424-2.7484,0.6679c-0.9434-0.5745-1.2424-1.8051-0.6679-2.7484 s1.805-1.2424,2.7484-0.6679S29.5059,30.1088,29.5059,30.1088z\"\n        />\n        <path\n          fill=\"none\"\n          stroke=\"#000000\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeMiterlimit=\"10\"\n          strokeWidth=\"2\"\n          d=\"M33.1089,37.006h6.1457c0.4011,0,0.7634,0.2397,0.9203,0.6089l1.1579,2.7245l-2.1792,1.1456 c-0.6156,0.3236-1.3654-0.0645-1.4567-0.754\"\n        />\n        <path\n          fill=\"none\"\n          stroke=\"#000000\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeMiterlimit=\"10\"\n          strokeWidth=\"2\"\n          d=\"M34.7606,40.763c-0.1132,0.6268-0.7757,0.9895-1.3647,0.7471l-2.3132-0.952l1.0899-2.9035 c0.1465-0.3901,0.5195-0.6486,0.9362-0.6486\"\n        />\n        <path\n          fill=\"none\"\n          stroke=\"#000000\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeMiterlimit=\"10\"\n          strokeWidth=\"2\"\n          d=\"M30.4364,50.0268c0,0-0.7187,8.7934,3.0072,9.9375c2.6459,0.8125,5.1497,0.5324,6.0625-0.25 c0.875-0.75,2.6323-4.4741,1.8267-9.6875\"\n        />\n        <path\n          fill=\"#000000\"\n          stroke=\"none\"\n          d=\"M44.2636,30.1088c0,0,1.805,1.2424,2.7484,0.6679c0.9434-0.5745,1.2424-1.8051,0.6679-2.7484 c-0.5745-0.9434-1.805-1.2424-2.7484-0.6679C43.9881,27.9349,44.2636,30.1088,44.2636,30.1088z\"\n        />\n        <path\n          fill=\"none\"\n          stroke=\"#000000\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeMiterlimit=\"10\"\n          strokeWidth=\"2\"\n          d=\"M25.6245,42.8393c-0.475,3.6024,2.2343,5.7505,4.2847,6.8414c1.1968,0.6367,2.6508,0.5182,3.7176-0.3181l2.581-2.0233l2.581,2.0233 c1.0669,0.8363,2.5209,0.9548,3.7176,0.3181c2.0504-1.0909,4.7597-3.239,4.2847-6.8414\"\n        />\n        <path\n          fill=\"none\"\n          stroke=\"#000000\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeMiterlimit=\"10\"\n          strokeWidth=\"2\"\n          d=\"M19.9509,28.3572c-2.3166,5.1597-0.5084,13.0249,0.119,15.3759c0.122,0.4571,0.0755,0.9355-0.1271,1.3631l-1.9874,4.1937 c-0.623,1.3146-2.3934,1.5533-3.331,0.4409c-3.1921-3.7871-8.5584-11.3899-6.5486-16.686 c7.0625-18.6104,15.8677-18.1429,15.8677-18.1429c2.8453-1.9336,13.1042-6.9375,24.8125,0.875c0,0,8.6323-1.7175,14.9375,16.9375 c1.8036,5.3362-3.4297,12.8668-6.5506,16.6442c-0.9312,1.127-2.7162,0.8939-3.3423-0.4272l-1.9741-4.1656 c-0.2026-0.4275-0.2491-0.906-0.1271-1.3631c0.6275-2.3509,2.4356-10.2161,0.119-15.3759\"\n        />\n        <path\n          fill=\"none\"\n          stroke=\"#000000\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeMiterlimit=\"10\"\n          strokeWidth=\"2\"\n          d=\"M52.6309,46.4628c0,0-3.0781,6.7216-7.8049,8.2712\"\n        />\n        <path\n          fill=\"none\"\n          stroke=\"#000000\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeMiterlimit=\"10\"\n          strokeWidth=\"2\"\n          d=\"M19.437,46.969c0,0,3.0781,6.0823,7.8049,7.632\"\n        />\n        <line\n          x1=\"36.2078\"\n          x2=\"36.2078\"\n          y1=\"47.3393\"\n          y2=\"44.3093\"\n          fill=\"none\"\n          stroke=\"#000000\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeMiterlimit=\"10\"\n          strokeWidth=\"2\"\n        />\n      </g>\n    </svg>\n  );\n};\n\nexport default Bruno;\n"
  },
  {
    "path": "packages/bruno-app/src/components/BrunoSupport/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  color: ${(props) => props.theme.text};\n  .collection-options {\n    svg {\n      position: relative;\n      top: -1px;\n    }\n\n    .label {\n      cursor: pointer;\n      &:hover {\n        text-decoration: underline;\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/BrunoSupport/index.js",
    "content": "import React from 'react';\nimport Modal from 'components/Modal/index';\nimport { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord, IconBook } from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\n\nconst BrunoSupport = ({ onClose }) => {\n  return (\n    <StyledWrapper>\n      <Modal size=\"sm\" title=\"Support\" handleCancel={onClose} hideFooter={true}>\n        <div className=\"collection-options\">\n          <div className=\"mt-2\">\n            <a href=\"https://docs.usebruno.com\" target=\"_blank\" className=\"flex items-end\">\n              <IconBook size={18} strokeWidth={2} />\n              <span className=\"label ml-2\">Documentation</span>\n            </a>\n          </div>\n          <div className=\"mt-2\">\n            <a href=\"https://github.com/usebruno/bruno/issues\" target=\"_blank\" className=\"flex items-end\">\n              <IconSpeakerphone size={18} strokeWidth={2} />\n              <span className=\"label ml-2\">Report Issues</span>\n            </a>\n          </div>\n          <div className=\"mt-2\">\n            <a href=\"https://discord.com/invite/KgcZUncpjq\" target=\"_blank\" className=\"flex items-end\">\n              <IconBrandDiscord size={18} strokeWidth={2} />\n              <span className=\"label ml-2\">Discord</span>\n            </a>\n          </div>\n          <div className=\"mt-2\">\n            <a href=\"https://github.com/usebruno/bruno\" target=\"_blank\" className=\"flex items-end\">\n              <IconBrandGithub size={18} strokeWidth={2} />\n              <span className=\"label ml-2\">GitHub</span>\n            </a>\n          </div>\n          <div className=\"mt-2\">\n            <a href=\"https://twitter.com/use_bruno\" target=\"_blank\" className=\"flex items-end\">\n              <IconBrandTwitter size={18} strokeWidth={2} />\n              <span className=\"label ml-2\">Twitter</span>\n            </a>\n          </div>\n        </div>\n      </Modal>\n    </StyledWrapper>\n  );\n};\n\nexport default BrunoSupport;\n"
  },
  {
    "path": "packages/bruno-app/src/components/BulkEditor/index.js",
    "content": "import React, { useMemo } from 'react';\nimport get from 'lodash/get';\nimport CodeEditor from 'components/CodeEditor';\nimport { useTheme } from 'providers/Theme';\nimport { useSelector } from 'react-redux';\nimport { parseBulkKeyValue, serializeBulkKeyValue } from 'utils/common/bulkKeyValueUtils';\n\nconst BulkEditor = ({ params, onChange, onToggle, onSave, onRun }) => {\n  const preferences = useSelector((state) => state.app.preferences);\n  const { displayedTheme } = useTheme();\n\n  const parsedParams = useMemo(() => serializeBulkKeyValue(params), [params]);\n\n  const handleEdit = (value) => {\n    const parsed = parseBulkKeyValue(value);\n    onChange(parsed);\n  };\n\n  return (\n    <>\n      <div className=\"h-[200px]\">\n        <CodeEditor\n          mode=\"text/plain\"\n          theme={displayedTheme}\n          font={get(preferences, 'font.codeFont', 'default')}\n          fontSize={get(preferences, 'font.codeFontSize')}\n          value={parsedParams}\n          onEdit={handleEdit}\n          onSave={onSave}\n          onRun={onRun}\n        />\n      </div>\n      <div className=\"flex btn-action justify-between items-center mt-3\">\n        <button className=\"text-link select-none ml-auto\" onClick={onToggle}>\n          Key/Value Edit\n        </button>\n      </div>\n    </>\n  );\n};\n\nexport default BulkEditor;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Checkbox/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .checkbox-container {\n    width: 1rem;\n    height: 1rem;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    position: relative;\n    cursor: pointer;\n    \n    &:disabled {\n      cursor: not-allowed;\n      opacity: 0.5;\n    }\n  }\n\n  .checkbox-checkmark {\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    transform: translate(-50%, -50%);\n    visibility: ${(props) => props.checked ? 'visible' : 'hidden'};\n    pointer-events: none;\n  }\n\n  .checkbox-input {\n    appearance: none;\n    -webkit-appearance: none;\n    -moz-appearance: none;\n    width: 1rem;\n    height: 1rem;\n    border: 2px solid ${(props) => {\n      if (props.checked && props.disabled) {\n        return props.theme.colors.text.muted;\n      }\n\n      if (props.checked && !props.disabled) {\n        return props.theme.colors.text.yellow;\n      }\n\n      return props.theme.colors.text.muted;\n    }};\n    border-radius: 4px;\n    background-color: ${(props) => {\n      if (props.checked && !props.disabled) {\n        return props.theme.colors.text.yellow;\n      }\n\n      if (props.checked && props.disabled) {\n        return props.theme.colors.text.muted;\n      }\n\n      return 'transparent';\n    }};\n    cursor: pointer;\n    position: relative;\n    transition: all 0.2s ease;\n    outline: none;\n    box-shadow: none;\n    \n    &:hover:not(:disabled) {\n      opacity: 0.8;\n    }\n    \n    &:disabled {\n      cursor: not-allowed;\n      opacity: 0.5;\n    }\n    \n    &:focus {\n      outline: none;\n      box-shadow: 0 0 0 2px ${(props) => props.theme.colors.text.yellow}40;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Checkbox/index.js",
    "content": "import React from 'react';\nimport StyledWrapper from './StyledWrapper';\nimport IconCheckMark from 'components/Icons/IconCheckMark';\nimport { useTheme } from 'providers/Theme';\n\nconst Checkbox = ({\n  checked = false,\n  disabled = false,\n  onChange,\n  className = '',\n  id,\n  name,\n  value,\n  dataTestId = 'checkbox'\n}) => {\n  const { theme } = useTheme();\n\n  const handleChange = (e) => {\n    if (!disabled && onChange) {\n      onChange(e);\n    }\n  };\n\n  return (\n    <StyledWrapper checked={checked} disabled={disabled} className={className}>\n      <div className=\"checkbox-container\">\n        <input\n          type=\"checkbox\"\n          id={id}\n          name={name}\n          value={value}\n          checked={checked}\n          disabled={disabled}\n          onChange={handleChange}\n          className=\"checkbox-input\"\n          data-testid={dataTestId}\n        />\n        <IconCheckMark className=\"checkbox-checkmark\" color={theme.examples.checkbox.color} size={14} />\n      </div>\n\n    </StyledWrapper>\n  );\n};\n\nexport default Checkbox;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CodeEditor/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  &.read-only {\n    div.CodeMirror .CodeMirror-cursor {\n      display: none !important;\n    }\n  }\n\n  div.CodeMirror {\n    background: ${(props) => props.theme.codemirror.bg};\n    border: solid 1px ${(props) => props.theme.codemirror.border};\n    font-family: ${(props) => (props.font ? props.font : 'default')};\n    font-size: ${(props) => (props.fontSize ? `${props.fontSize}px` : 'inherit')};\n    line-break: anywhere;\n    flex: 1 1 0;\n    display: flex;\n    flex-direction: column-reverse;\n  }\n\n  .CodeMirror-placeholder {\n    color: ${(props) => props.theme.text} !important;\n    opacity: 0.5 !important;\n  }\n\n  .CodeMirror-linenumber {\n    text-align: left !important;\n    padding-left: 3px !important;\n  }\n\n  /* Override default lint highlight background when emphasizing the gutter */\n  .CodeMirror-lint-line-error,\n  .CodeMirror-lint-line-warning {\n    background: none !important;\n  }\n\n  /* Style line numbers when there's a lint issue */\n  .CodeMirror-lint-line-error .CodeMirror-linenumber {\n    color: ${(props) => props.theme.colors.text.danger} !important;\n    text-decoration: underline;\n  }\n\n  .CodeMirror-lint-line-warning .CodeMirror-linenumber {\n    color: ${(props) => props.theme.colors.text.warning} !important;\n    text-decoration: underline;\n  }\n\n  /* Removes the glow outline around the folded json */\n  .CodeMirror-foldmarker {\n    text-shadow: none;\n    color: ${(props) => props.theme.textLink};\n    background: none;\n    padding: 0;\n    margin: 0;\n  }\n\n  .CodeMirror-overlayscroll-horizontal div,\n  .CodeMirror-overlayscroll-vertical div {\n    background: #d2d7db;\n  }\n\n  .CodeMirror-dialog {\n    overflow: visible;\n    position: relative;\n    top: unset;\n    left: unset;\n\n    input {\n      background: transparent;\n      border: 1px solid #d3d6db;\n      outline: none;\n      border-radius: 0px;\n    }\n  }\n\n  #search-results-count {\n    display: inline-block;\n    position: absolute;\n    top: calc(100% + 1px);\n    right: 0;\n    border-width: 0 0 1px 1px;\n    border-style: solid;\n    border-color: ${(props) => props.theme.codemirror.border};\n    padding: 0.1em 0.8em;\n    background-color: ${(props) => props.theme.codemirror.bg};\n    color: rgb(102, 102, 102);\n    white-space: nowrap;\n  }\n\n  textarea.cm-editor {\n    position: relative;\n  }\n\n  // Todo: dark mode temporary fix\n  // Clean this\n  .CodeMirror.cm-s-monokai {\n    .CodeMirror-overlayscroll-horizontal div,\n    .CodeMirror-overlayscroll-vertical div {\n      background: #444444;\n    }\n  }\n\n  .cm-s-default, .cm-s-monokai {\n    span.cm-def {\n      color: ${(props) => props.theme.codemirror.tokens.definition} !important;\n    }\n    span.cm-property {\n      color: ${(props) => props.theme.codemirror.tokens.property} !important;\n    }\n    span.cm-string {\n      color: ${(props) => props.theme.codemirror.tokens.string} !important;\n    }\n    span.cm-number {\n      color: ${(props) => props.theme.codemirror.tokens.number} !important;\n    }\n    span.cm-atom {\n      color: ${(props) => props.theme.codemirror.tokens.atom} !important;\n    }\n    span.cm-variable, span.cm-variable-2 {\n      color: ${(props) => props.theme.codemirror.tokens.variable} !important;\n    }\n    span.cm-keyword {\n      color: ${(props) => props.theme.codemirror.tokens.keyword} !important;\n    }\n    span.cm-comment {\n      color: ${(props) => props.theme.codemirror.tokens.comment} !important;\n    }\n    span.cm-operator {\n      color: ${(props) => props.theme.codemirror.tokens.operator} !important;\n    }\n    span.cm-tag {\n      color: ${(props) => props.theme.codemirror.tokens.tag} !important;\n    }\n    span.cm-tag.cm-bracket {\n      color: ${(props) => props.theme.codemirror.tokens.tagBracket} !important;\n    }\n  }\n\n  /* Variable validation colors */\n  .cm-variable-valid {\n    color: ${(props) => props.theme.codemirror.variable.valid} !important;\n  }\n  .cm-variable-invalid {\n    color: ${(props) => props.theme.codemirror.variable.invalid} !important;\n  }\n\n  .CodeMirror-search-hint {\n    display: inline;\n  }\n  \n  \n  //matching bracket fix\n  .CodeMirror-matchingbracket {\n    background: #5cc0b48c !important;\n    text-decoration:unset;\n  }\n\n  .cm-search-line-highlight {\n    background: ${(props) => props.theme.codemirror.searchLineHighlightCurrent};\n  }\n\n  .cm-search-match {\n    background: rgba(255, 193, 7, 0.25);\n  }\n\n  .cm-search-current {\n    background: rgba(255, 193, 7, 0.4);\n  }\n\n  .lint-error-tooltip {\n    position: fixed;\n    z-index: 10000;\n    background: ${(props) => props.theme.codemirror.bg};\n    border-radius: ${(props) => props.theme.border.radius.base};\n    padding: 8px 12px;\n    max-width: 400px;\n    box-shadow: ${(props) => props.theme.shadow.sm};\n    font-size: ${(props) => props.theme.font.size.xs};\n    line-height: 1.5;\n    pointer-events: none;\n\n    .lint-tooltip-message {\n      padding: 2px 0;\n    }\n\n    .lint-tooltip-message.error {\n      color: ${(props) => props.theme.colors.text.danger};\n    }\n\n    .lint-tooltip-message.warning {\n      color: ${(props) => props.theme.colors.text.warning};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CodeEditor/index.js",
    "content": "/**\n *  Copyright (c) 2021 GraphQL Contributors.\n *\n *  This source code is licensed under the MIT license found in the\n *  LICENSE file in the root directory of this source tree.\n */\n\nimport React, { createRef } from 'react';\nimport { isEqual, escapeRegExp } from 'lodash';\nimport { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';\nimport { setupAutoComplete, showRootHints } from 'utils/codemirror/autocomplete';\nimport StyledWrapper from './StyledWrapper';\nimport * as jsonlint from '@prantlf/jsonlint';\nimport { JSHINT } from 'jshint';\nimport stripJsonComments from 'strip-json-comments';\nimport { getAllVariables } from 'utils/collections';\nimport { setupLinkAware } from 'utils/codemirror/linkAware';\nimport { setupLintErrorTooltip } from 'utils/codemirror/lint-errors';\nimport CodeMirrorSearch from 'components/CodeMirrorSearch/index';\n\nconst CodeMirror = require('codemirror');\nwindow.jsonlint = jsonlint;\nwindow.JSHINT = JSHINT;\n\nconst TAB_SIZE = 2;\n\nexport default class CodeEditor extends React.Component {\n  constructor(props) {\n    super(props);\n\n    // Keep a cached version of the value, this cache will be updated when the\n    // editor is updated, which can later be used to protect the editor from\n    // unnecessary updates during the update lifecycle.\n    this.cachedValue = props.value || '';\n    this.variables = {};\n    this.searchResultsCountElementId = 'search-results-count';\n    this.searchBarRef = createRef();\n\n    this.lintOptions = {\n      esversion: 11,\n      expr: true,\n      asi: true,\n      highlightLines: true\n    };\n\n    this.state = {\n      searchBarVisible: false\n    };\n  }\n\n  componentDidMount() {\n    const variables = getAllVariables(this.props.collection, this.props.item);\n\n    const editor = (this.editor = CodeMirror(this._node, {\n      value: this.props.value || '',\n      placeholder: '...',\n      lineNumbers: true,\n      lineWrapping: this.props.enableLineWrapping ?? true,\n      tabSize: TAB_SIZE,\n      mode: this.props.mode || 'application/ld+json',\n      brunoVarInfo: this.props.enableBrunoVarInfo !== false ? {\n        variables,\n        collection: this.props.collection,\n        item: this.props.item\n      } : false,\n      keyMap: 'sublime',\n      autoCloseBrackets: true,\n      matchBrackets: true,\n      showCursorWhenSelecting: true,\n      foldGutter: true,\n      gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],\n      lint: this.lintOptions,\n      readOnly: this.props.readOnly,\n      scrollbarStyle: 'overlay',\n      theme: this.props.theme === 'dark' ? 'monokai' : 'default',\n      extraKeys: {\n        'Cmd-Enter': () => {\n          if (this.props.onRun) {\n            this.props.onRun();\n          }\n        },\n        'Ctrl-Enter': () => {\n          if (this.props.onRun) {\n            this.props.onRun();\n          }\n        },\n        'Cmd-S': () => {\n          if (this.props.onSave) {\n            this.props.onSave();\n          }\n        },\n        'Ctrl-S': () => {\n          if (this.props.onSave) {\n            this.props.onSave();\n          }\n        },\n        'Cmd-F': (cm) => {\n          this.setState({ searchBarVisible: true }, () => {\n            this.searchBarRef.current?.focus();\n          });\n        },\n        'Ctrl-F': (cm) => {\n          this.setState({ searchBarVisible: true }, () => {\n            this.searchBarRef.current?.focus();\n          });\n        },\n        'Cmd-H': 'replace',\n        'Ctrl-H': 'replace',\n        'Tab': function (cm) {\n          cm.getSelection().includes('\\n') || editor.getLine(cm.getCursor().line) == cm.getSelection()\n            ? cm.execCommand('indentMore')\n            : cm.replaceSelection('  ', 'end');\n        },\n        'Shift-Tab': 'indentLess',\n        'Ctrl-Space': (cm) => {\n          showRootHints(cm, this.props.showHintsFor);\n        },\n        'Cmd-Space': (cm) => {\n          showRootHints(cm, this.props.showHintsFor);\n        },\n        'Ctrl-Y': 'foldAll',\n        'Cmd-Y': 'foldAll',\n        'Ctrl-I': 'unfoldAll',\n        'Cmd-I': 'unfoldAll',\n        'Ctrl-/': () => {\n          if (['application/ld+json', 'application/json'].includes(this.props.mode)) {\n            this.editor.toggleComment({ lineComment: '//', blockComment: '/*' });\n          } else {\n            this.editor.toggleComment();\n          }\n        },\n        'Cmd-/': () => {\n          if (['application/ld+json', 'application/json'].includes(this.props.mode)) {\n            this.editor.toggleComment({ lineComment: '//', blockComment: '/*' });\n          } else {\n            this.editor.toggleComment();\n          }\n        },\n        'Esc': () => {\n          if (this.state.searchBarVisible) {\n            this.setState({ searchBarVisible: false });\n          }\n        }\n      },\n      foldOptions: {\n        widget: (from, to) => {\n          var count = undefined;\n          var internal = this.editor.getRange(from, to);\n          if (this.props.mode == 'application/ld+json') {\n            if (this.editor.getLine(from.line).endsWith('[')) {\n              var toParse = '[' + internal + ']';\n            } else var toParse = '{' + internal + '}';\n            try {\n              count = Object.keys(JSON.parse(toParse)).length;\n            } catch (e) {}\n          } else if (this.props.mode == 'application/xml') {\n            var doc = new DOMParser();\n            try {\n              // add header element and remove prefix namespaces for DOMParser\n              var dcm = doc.parseFromString(\n                '<a> ' + internal.replace(/(?<=\\<|<\\/)\\w+:/g, '') + '</a>',\n                'application/xml'\n              );\n              count = dcm.documentElement.children.length;\n            } catch (e) {}\n          }\n          return count ? `\\u21A4${count}\\u21A6` : '\\u2194';\n        }\n      }\n    }));\n    CodeMirror.registerHelper('lint', 'json', function (text) {\n      let found = [];\n      if (!window.jsonlint) {\n        if (window.console) {\n          window.console.error('Error: window.jsonlint not defined, CodeMirror JSON linting cannot run.');\n        }\n        return found;\n      }\n      let jsonlint = window.jsonlint.parser || window.jsonlint;\n      try {\n        jsonlint.parse(stripJsonComments(text.replace(/(?<!\"[^\":{]*){{[^}]*}}(?![^\"},]*\")/g, '1')));\n      } catch (error) {\n        const { message, location } = error;\n        const line = location?.start?.line;\n        const column = location?.start?.column;\n        if (line && column) {\n          found.push({\n            from: CodeMirror.Pos(line - 1, column),\n            to: CodeMirror.Pos(line - 1, column),\n            message\n          });\n        }\n      }\n      return found;\n    });\n\n    if (editor) {\n      editor.setOption('lint', this.props.mode && editor.getValue().trim().length > 0 ? this.lintOptions : false);\n      editor.on('change', this._onEdit);\n      editor.scrollTo(null, this.props.initialScroll);\n      this.addOverlay();\n\n      const getAllVariablesHandler = () => getAllVariables(this.props.collection, this.props.item);\n\n      // Setup AutoComplete Helper for all modes\n      const autoCompleteOptions = {\n        showHintsFor: this.props.showHintsFor,\n        getAllVariables: getAllVariablesHandler\n      };\n\n      this.brunoAutoCompleteCleanup = setupAutoComplete(\n        editor,\n        autoCompleteOptions\n      );\n\n      setupLinkAware(editor);\n\n      // Setup lint error tooltip on line number hover\n      this.cleanupLintErrorTooltip = setupLintErrorTooltip(editor);\n    }\n  }\n\n  componentDidUpdate(prevProps) {\n    // Ensure the changes caused by this update are not interpreted as\n    // user-input changes which could otherwise result in an infinite\n    // event loop.\n    this.ignoreChangeEvent = true;\n    if (this.props.schema !== prevProps.schema && this.editor) {\n      this.editor.options.lint.schema = this.props.schema;\n      this.editor.options.hintOptions.schema = this.props.schema;\n      this.editor.options.info.schema = this.props.schema;\n      this.editor.options.jump.schema = this.props.schema;\n      CodeMirror.signal(this.editor, 'change', this.editor);\n    }\n    if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {\n      // TODO: temporary fix for keeping cursor state when auto save and new line insertion collide PR#7098\n      const nextValue = this.props.value ?? '';\n      const currentValue = this.editor.getValue();\n      // Skip updating only when focused and editable; read-only editors (e.g. response viewer) must always show new value\n      if (this.editor.hasFocus?.() && currentValue !== nextValue && !this.props.readOnly) {\n        this.cachedValue = currentValue;\n      } else {\n        const cursor = this.editor.getCursor();\n        this.cachedValue = nextValue;\n        this.editor.setValue(nextValue);\n        this.editor.setCursor(cursor);\n      }\n    }\n\n    if (this.editor) {\n      let variables = getAllVariables(this.props.collection, this.props.item);\n      if (!isEqual(variables, this.variables)) {\n        this.addOverlay();\n      }\n\n      // Update collection and item when they change\n      if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) {\n        if (!isEqual(this.props.collection, this.editor.options.brunoVarInfo.collection)) {\n          this.editor.options.brunoVarInfo.collection = this.props.collection;\n        }\n        if (!isEqual(this.props.item, this.editor.options.brunoVarInfo.item)) {\n          this.editor.options.brunoVarInfo.item = this.props.item;\n        }\n      }\n    }\n\n    if (this.props.theme !== prevProps.theme && this.editor) {\n      this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');\n    }\n\n    if (this.props.initialScroll !== prevProps.initialScroll) {\n      this.editor.scrollTo(null, this.props.initialScroll);\n    }\n\n    if (this.props.enableLineWrapping !== prevProps.enableLineWrapping) {\n      this.editor.setOption('lineWrapping', this.props.enableLineWrapping);\n    }\n\n    if (this.props.mode !== prevProps.mode) {\n      this.editor.setOption('mode', this.props.mode);\n    }\n\n    if (this.props.readOnly !== prevProps.readOnly && this.editor) {\n      this.editor.setOption('readOnly', this.props.readOnly);\n    }\n\n    this.ignoreChangeEvent = false;\n  }\n\n  componentWillUnmount() {\n    if (this.editor) {\n      if (this.props.onScroll) {\n        this.props.onScroll(this.editor);\n      }\n\n      this.editor?._destroyLinkAware?.();\n      this.editor.off('change', this._onEdit);\n\n      // Clean up lint error tooltip\n      this.cleanupLintErrorTooltip?.();\n\n      const wrapper = this.editor.getWrapperElement();\n      wrapper?.parentNode?.removeChild(wrapper);\n\n      this.editor = null;\n    }\n  }\n\n  render() {\n    if (this.editor) {\n      this.editor.refresh();\n    }\n    return (\n      <StyledWrapper\n        className={`h-full w-full flex flex-col relative graphiql-container ${this.props.readOnly ? 'read-only' : ''}`}\n        aria-label=\"Code Editor\"\n        font={this.props.font}\n        fontSize={this.props.fontSize}\n      >\n        <CodeMirrorSearch\n          ref={(node) => {\n            if (!node) return;\n            this.searchBarRef.current = node;\n          }}\n          visible={this.state.searchBarVisible}\n          editor={this.editor}\n          onClose={() => this.setState({ searchBarVisible: false })}\n        />\n        <div\n          className={`editor-container${this.state.searchBarVisible ? ' search-bar-visible' : ''}`}\n          ref={(node) => { this._node = node; }}\n          style={{ height: '100%', width: '100%' }}\n        />\n      </StyledWrapper>\n    );\n  }\n\n  addOverlay = () => {\n    const mode = this.props.mode || 'application/ld+json';\n    let variables = getAllVariables(this.props.collection, this.props.item);\n    this.variables = variables;\n\n    // Update brunoVarInfo with latest variables\n    if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) {\n      this.editor.options.brunoVarInfo.variables = variables;\n    }\n\n    defineCodeMirrorBrunoVariablesMode(variables, mode, false, this.props.enableVariableHighlighting);\n    this.editor.setOption('mode', 'brunovariables');\n  };\n\n  _onEdit = () => {\n    if (!this.ignoreChangeEvent && this.editor) {\n      this.editor.setOption('lint', this.editor.getValue().trim().length > 0 ? this.lintOptions : false);\n      this.cachedValue = this.editor.getValue();\n      if (this.props.onEdit) {\n        this.props.onEdit(this.cachedValue);\n      }\n    }\n  };\n}\n"
  },
  {
    "path": "packages/bruno-app/src/components/CodeEditor/index.spec.js",
    "content": "import React from 'react';\nimport { render, act } from '@testing-library/react';\nimport CodeEditor from './index';\nimport { ThemeProvider } from 'styled-components';\n\njest.mock('codemirror', () => {\n  const codemirror = require('test-utils/mocks/codemirror');\n  return codemirror;\n});\n\nconst MOCK_THEME = {\n  codemirror: {\n    bg: '#1e1e1e',\n    border: '#333'\n  },\n  textLink: '#007acc'\n};\n\nconst setupEditorState = (editor, { value, cursorPosition }) => {\n  editor._currentValue = value;\n  editor.getCursor.mockReturnValue({ line: 0, ch: cursorPosition });\n  editor.getRange.mockImplementation((from, to) => {\n    if (from.line === 0 && from.ch === 0 && to.line === 0 && to.ch === cursorPosition) {\n      return value;\n    }\n    return editor._currentValue.slice(from.ch, to.ch);\n  });\n\n  editor.state = {\n    completionActive: null\n  };\n};\n\nconst setupEditorWithRef = () => {\n  const ref = React.createRef();\n  const { rerender } = render(\n    <ThemeProvider theme={MOCK_THEME}>\n      <CodeEditor ref={ref} />\n    </ThemeProvider>\n  );\n  return { ref, rerender };\n};\n\ndescribe('CodeEditor', () => {\n  beforeEach(() => {\n    jest.clearAllMocks();\n    jest.resetModules();\n  });\n\n  it('add CodeEditor related tests here', () => {});\n});\n"
  },
  {
    "path": "packages/bruno-app/src/components/CodeMirrorSearch/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst StyledWrapper = styled.div`\n  .bruno-search-bar {\n    position: absolute;\n    top: 8px;\n    right: 8px;\n    z-index: 20;\n    display: flex;\n    align-items: center;\n    flex-wrap: nowrap;\n    gap: 0;\n    padding: 1px 3px;\n    width: auto;\n    max-width: 320px;\n    min-height: 22px;\n    background: ${(props) => props.theme.background.base};\n    color: ${(props) => props.theme.text.base};\n    border: solid 1px ${(props) => props.theme.border.border2};\n    border-radius: ${(props) => props.theme.border.radius.sm};\n  }\n\n  .bruno-search-bar input {\n    min-width: 80px;\n    background: transparent;\n    color: inherit;\n    border: none;\n    outline: none;\n    padding: 1px 2px;\n    font-size: ${(props) => props.theme.font.size.base};\n    margin: 0 1px;\n    height: 28px;\n  }\n\n  .searchbar-icon-btn {\n    background: none;\n    border: none;\n    padding: 0 1px;\n    margin: 0 1px;\n    cursor: pointer;\n    color: ${(props) => props.theme.colors.text.subtext1};\n    border-radius: 3px;\n    height: 18px;\n    width: 18px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n  }\n\n  .searchbar-result-count {\n    min-width: 28px;\n    text-align: center;\n    font-size: ${(props) => props.theme.font.size.xs};\n    color: ${(props) => props.theme.colors.text.subtext1};\n    margin: 0 8px 0 1px; \n    white-space: nowrap;\n  }\n\n  .bruno-search-bar input {\n    background: transparent;\n    color: ${(props) => props.theme.colors.text.subtext2};\n    border: none;\n    outline: none;\n    font-size: ${(props) => props.theme.font.size.base};\n    padding: 1px 2px;\n    min-width: 80px;\n  }\n\n  .searchbar-icon-btn:focus {\n    outline: 1px solid ${(props) => props.theme.codemirror.border};\n  }\n\n  .bruno-search-bar, .bruno-search-bar input {\n    font-family: Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif !important;\n  }\n\n  .cm-search-line-highlight {\n    background: ${(props) => props.theme.codemirror.searchLineHighlightCurrent};\n  }\n\n  .searchbar-icon-btn.active {\n    color: ${(props) => props.theme.brand};\n    background-color: ${(props) => rgba(props.theme.brand, 0.1)};\n    font-weight: 500;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CodeMirrorSearch/index.js",
    "content": "import React, { useState, useEffect, useRef, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react';\nimport { IconRegex, IconArrowUp, IconArrowDown, IconX, IconLetterCase, IconLetterW } from '@tabler/icons';\nimport ToolHint from 'components/ToolHint';\nimport StyledWrapper from './StyledWrapper';\nimport useDebounce from 'hooks/useDebounce';\n\nfunction escapeRegExp(string) {\n  return string.replace(/[.*+?^${}()|[\\\\]\\\\\\\\]/g, '\\\\\\\\$&');\n}\n\nconst MAX_MATCHES = 99_999;\nfunction findSearchMatches(editor, searchText, regex, caseSensitive, wholeWord) {\n  try {\n    let query, options = {};\n    if (regex) {\n      try {\n        query = new RegExp(searchText, caseSensitive ? 'g' : 'gi');\n      } catch (error) {\n        console.warn('Invalid regex provided in search!', error);\n        return [];\n      }\n    } else if (wholeWord) {\n      const escaped = escapeRegExp(searchText);\n      query = new RegExp(`\\\\b${escaped}\\\\b`, caseSensitive ? 'g' : 'gi');\n    } else {\n      query = searchText;\n      options = { caseFold: !caseSensitive };\n    }\n\n    const cursor = editor.getSearchCursor(query, { line: 0, ch: 0 }, options);\n    const out = [];\n    while (cursor.findNext()) {\n      out.push({ from: cursor.from(), to: cursor.to() });\n      if (out.length >= MAX_MATCHES) {\n        break;\n      }\n    }\n    return out;\n  } catch (e) {\n    console.error('Search error:', e);\n    return [];\n  }\n}\n\nfunction createCacheKey(editor, searchText, regex, caseSensitive, wholeWord) {\n  return `${editor.getValue().length}⇴${searchText}⇴${regex}⇴${caseSensitive}⇴${wholeWord}`;\n}\n\nconst CodeMirrorSearch = forwardRef(({ visible, editor, onClose }, ref) => {\n  const [searchText, setSearchText] = useState('');\n  const [regex, setRegex] = useState(false);\n  const [caseSensitive, setCaseSensitive] = useState(false);\n  const [wholeWord, setWholeWord] = useState(false);\n  const [matchIndex, setMatchIndex] = useState(0);\n  const [matchCount, setMatchCount] = useState(0);\n\n  const searchMarks = useRef([]);\n  const searchLineHighlight = useRef(null);\n  const searchMatches = useRef([]);\n  const searchCacheKey = useRef('');\n  const inputRef = useRef(null);\n\n  const debouncedSearchText = useDebounce(searchText, 250);\n  const doSearch = useCallback((newIndex = 0) => {\n    if (!editor || !visible) {\n      return;\n    }\n\n    if (searchLineHighlight.current !== null) {\n      editor.removeLineClass(searchLineHighlight.current, 'wrap', 'cm-search-line-highlight');\n      searchLineHighlight.current = null;\n    }\n\n    if (!debouncedSearchText) {\n      setMatchCount(0);\n      setMatchIndex(0);\n      searchMatches.current = [];\n      searchMarks.current.forEach((mark) => mark.clear());\n      searchMarks.current = [];\n      return;\n    }\n\n    try {\n      const newCacheKey = createCacheKey(editor, debouncedSearchText, regex, caseSensitive, wholeWord);\n      const isCacheHit = newCacheKey === searchCacheKey.current;\n\n      let matches = searchMatches.current;\n      if (!isCacheHit) {\n        matches = findSearchMatches(editor, debouncedSearchText, regex, caseSensitive, wholeWord);\n        searchMatches.current = matches;\n        searchCacheKey.current = newCacheKey;\n        setMatchCount(matches.length);\n      }\n\n      if (!matches.length) {\n        setMatchIndex(0);\n        // Clear previous marks\n        searchMarks.current.forEach((mark) => mark.clear());\n        searchMarks.current = [];\n        return;\n      }\n\n      const matchIndex = Math.max(0, Math.min(newIndex, matches.length - 1));\n      setMatchIndex(matchIndex);\n\n      if (isCacheHit) {\n        // Clear only old current mark\n        const oldIndex = searchMarks.current.findIndex((mark) => mark.className?.includes('cm-search-current'));\n\n        if (oldIndex !== -1) {\n          searchMarks.current[oldIndex].clear();\n          searchMarks.current.splice(oldIndex, 1);\n        }\n\n        // Add mark to the new current and remark the previous and next\n        const toMark = [\n          // Previous\n          matchIndex > 0 ? matchIndex - 1 : null,\n          // Current\n          matchIndex,\n          // Next\n          matchIndex < matches.length - 1 ? matchIndex + 1 : null\n        ].filter((i) => i !== null);\n\n        toMark.forEach((i) => {\n          const mark = editor.markText(matches[i].from, matches[i].to, {\n            className: i === matchIndex ? 'cm-search-current' : 'cm-search-match',\n            clearOnEnter: true\n          });\n          searchMarks.current.push(mark);\n        });\n      } else {\n        // Clear previous marks\n        searchMarks.current.forEach((mark) => mark.clear());\n        searchMarks.current = [];\n\n        // Mark all on new search\n        matches.forEach((m, i) => {\n          const mark = editor.markText(m.from, m.to, {\n            className: i === matchIndex ? 'cm-search-current' : 'cm-search-match',\n            clearOnEnter: true\n          });\n          searchMarks.current.push(mark);\n        });\n      }\n\n      const currentLine = matches[matchIndex].from.line;\n      editor.addLineClass(currentLine, 'wrap', 'cm-search-line-highlight');\n      searchLineHighlight.current = currentLine;\n\n      editor.scrollIntoView(matches[matchIndex].from, 100);\n      editor.setSelection(matches[matchIndex].from, matches[matchIndex].to);\n    } catch (e) {\n      console.error('Search error:', e);\n      setMatchCount(0);\n      setMatchIndex(0);\n      searchMatches.current = [];\n      searchCacheKey.current = '';\n    }\n  }, [debouncedSearchText, regex, caseSensitive, wholeWord, editor, visible]);\n\n  useImperativeHandle(ref, () => ({\n    focus: () => {\n      if (inputRef.current) {\n        inputRef.current.focus();\n      }\n    }\n  }));\n\n  useEffect(() => {\n    doSearch(0);\n  }, [debouncedSearchText, doSearch]);\n\n  const handleSearchBarClose = useCallback(() => {\n    searchMarks.current.forEach((mark) => mark.clear());\n    searchMarks.current = [];\n    if (searchLineHighlight.current !== null && editor) {\n      editor.removeLineClass(searchLineHighlight.current, 'wrap', 'cm-search-line-highlight');\n      searchLineHighlight.current = null;\n    }\n    searchMatches.current = [];\n    searchCacheKey.current = '';\n    if (onClose) onClose();\n    // Focus the editor after closing the search bar\n    if (editor) {\n      setTimeout(() => editor.focus(), 0);\n    }\n  }, [editor, onClose]);\n\n  const handleSearchTextChange = (text) => {\n    setSearchText(text);\n    setMatchIndex(0);\n  };\n\n  const handleToggleRegex = () => {\n    setRegex((prev) => !prev);\n    setMatchIndex(0);\n  };\n\n  const handleToggleCase = () => {\n    setCaseSensitive((prev) => !prev);\n    setMatchIndex(0);\n  };\n\n  const handleToggleWholeWord = () => {\n    setWholeWord((prev) => !prev);\n    setMatchIndex(0);\n  };\n\n  const handleNext = () => {\n    if (!searchMatches.current || !searchMatches.current.length) return;\n    const next = (matchIndex + 1) % searchMatches.current.length;\n    doSearch(next);\n  };\n\n  const handlePrev = () => {\n    if (!searchMatches.current || !searchMatches.current.length) return;\n    const prev = (matchIndex - 1 + searchMatches.current.length) % searchMatches.current.length;\n    doSearch(prev);\n  };\n\n  if (!visible) return null;\n\n  return (\n    <StyledWrapper>\n      <div className=\"bruno-search-bar\">\n        <input\n          ref={inputRef}\n          autoFocus\n          type=\"text\"\n          value={searchText}\n          onChange={(e) => handleSearchTextChange(e.target.value)}\n          placeholder=\"Search...\"\n          spellCheck={false}\n          onKeyDown={(e) => {\n            if (e.key === 'Enter' && !e.shiftKey) handleNext();\n            if (e.key === 'Enter' && e.shiftKey) handlePrev();\n            if (e.key === 'Escape') handleSearchBarClose();\n          }}\n        />\n        <span className=\"searchbar-result-count\">{matchCount > 0 ? `${matchIndex + 1} / ${matchCount}` : '0 results'}</span>\n        <ToolHint text=\"Regex search\" toolhintId=\"searchbar-regex-toolhint\" place=\"top\">\n          <button className={`searchbar-icon-btn ${regex ? 'active' : ''}`} onClick={handleToggleRegex}><IconRegex size={16} /></button>\n        </ToolHint>\n        <ToolHint text=\"Case sensitive\" toolhintId=\"searchbar-case-toolhint\" place=\"top\">\n          <button className={`searchbar-icon-btn ${caseSensitive ? 'active' : ''}`} onClick={handleToggleCase}><IconLetterCase size={14} /></button>\n        </ToolHint>\n        <ToolHint text=\"Whole word\" toolhintId=\"searchbar-wholeword-toolhint\" place=\"top\">\n          <button className={`searchbar-icon-btn ${wholeWord ? 'active' : ''}`} onClick={handleToggleWholeWord}><IconLetterW size={14} /></button>\n        </ToolHint>\n        <button className=\"searchbar-icon-btn\" title=\"Previous\" onClick={handlePrev}><IconArrowUp size={14} /></button>\n        <button className=\"searchbar-icon-btn\" title=\"Next\" onClick={handleNext}><IconArrowDown size={14} /></button>\n        <button className=\"searchbar-icon-btn\" title=\"Close\" onClick={handleSearchBarClose}><IconX size={14} /></button>\n      </div>\n    </StyledWrapper>\n  );\n});\n\nexport default CodeMirrorSearch;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Auth/ApiKeyAuth/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  label {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.subtext1};\n  }\n\n  .single-line-editor-wrapper {\n    padding: 0.15rem 0.4rem;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n  }\n\n  .auth-placement-selector {\n    font-size: ${(props) => props.theme.font.size.sm};\n    padding: 0.2rem 0px;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n\n    .dropdown {\n      width: fit-content;\n\n      div[data-tippy-root] {\n        width: fit-content;\n      }\n      .tippy-box {\n        width: fit-content;\n        max-width: none !important;\n\n        .tippy-content: {\n          width: fit-content;\n          max-width: none !important;\n        }\n      }\n    }\n\n    .auth-type-label {\n      width: fit-content;\n      justify-content: space-between;\n      padding: 0 0.5rem;\n    }\n\n    .dropdown-item {\n      padding: 0.2rem 0.6rem !important;\n    }\n  }\n\n  .caret {\n    color: rgb(140, 140, 140);\n    fill: rgb(140 140 140);\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Auth/ApiKeyAuth/index.js",
    "content": "import React, { useRef, forwardRef, useEffect } from 'react';\nimport { useDispatch } from 'react-redux';\nimport get from 'lodash/get';\nimport { IconCaretDown } from '@tabler/icons';\nimport Dropdown from 'components/Dropdown';\nimport { useTheme } from 'providers/Theme';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';\nimport { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';\nimport StyledWrapper from './StyledWrapper';\nimport { humanizeRequestAPIKeyPlacement } from 'utils/collections';\n\nconst ApiKeyAuth = ({ collection }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n  const dropdownTippyRef = useRef();\n  const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);\n\n  const apikeyAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.apikey', {}) : get(collection, 'root.request.auth.apikey', {});\n\n  const handleSave = () => dispatch(saveCollectionSettings(collection.uid));\n\n  const Icon = forwardRef((props, ref) => {\n    return (\n      <div ref={ref} className=\"flex items-center justify-end auth-type-label select-none\">\n        {humanizeRequestAPIKeyPlacement(apikeyAuth?.placement)}\n        <IconCaretDown className=\"caret ml-1 mr-1\" size={14} strokeWidth={2} />\n      </div>\n    );\n  });\n\n  const handleAuthChange = (property, value) => {\n    dispatch(\n      updateCollectionAuth({\n        mode: 'apikey',\n        collectionUid: collection.uid,\n        content: {\n          ...apikeyAuth,\n          [property]: value\n        }\n      })\n    );\n  };\n\n  useEffect(() => {\n    !apikeyAuth?.placement\n    && dispatch(\n      updateCollectionAuth({\n        mode: 'apikey',\n        collectionUid: collection.uid,\n        content: {\n          placement: 'header'\n        }\n      })\n    );\n  }, [apikeyAuth]);\n\n  return (\n    <StyledWrapper className=\"mt-2 w-full\">\n      <label className=\"block mb-1\">Key</label>\n      <div className=\"single-line-editor-wrapper mb-3\">\n        <SingleLineEditor\n          value={apikeyAuth.key || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleAuthChange('key', val)}\n          collection={collection}\n          isCompact\n        />\n      </div>\n\n      <label className=\"block mb-1\">Value</label>\n      <div className=\"single-line-editor-wrapper mb-3\">\n        <SingleLineEditor\n          value={apikeyAuth.value || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleAuthChange('value', val)}\n          collection={collection}\n          isCompact\n        />\n      </div>\n\n      <label className=\"block mb-1\">Add To</label>\n      <div className=\"inline-flex items-center cursor-pointer auth-placement-selector w-fit\">\n        <Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement=\"bottom-end\">\n          <div\n            className=\"dropdown-item\"\n            onClick={() => {\n              dropdownTippyRef.current.hide();\n              handleAuthChange('placement', 'header');\n            }}\n          >\n            Header\n          </div>\n          <div\n            className=\"dropdown-item\"\n            onClick={() => {\n              dropdownTippyRef.current.hide();\n              handleAuthChange('placement', 'queryparams');\n            }}\n          >\n            Query Params\n          </div>\n        </Dropdown>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default ApiKeyAuth;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  font-size: ${(props) => props.theme.font.size.base};\n\n  .auth-mode-selector {\n    background: transparent;\n\n    .auth-mode-label {\n      color: ${(props) => props.theme.primary.text};\n\n      .caret {\n        color: rgb(140, 140, 140);\n        fill: rgb(140, 140, 140);\n      }\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js",
    "content": "import React, { useMemo, useCallback } from 'react';\nimport get from 'lodash/get';\nimport { IconCaretDown } from '@tabler/icons';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport { useDispatch } from 'react-redux';\nimport { updateCollectionAuthMode } from 'providers/ReduxStore/slices/collections';\nimport { humanizeRequestAuthMode } from 'utils/collections';\nimport StyledWrapper from './StyledWrapper';\n\nconst AuthMode = ({ collection }) => {\n  const dispatch = useDispatch();\n  const authMode = collection.draft?.root ? get(collection, 'draft.root.request.auth.mode') : get(collection, 'root.request.auth.mode');\n\n  const onModeChange = useCallback((value) => {\n    dispatch(\n      updateCollectionAuthMode({\n        collectionUid: collection.uid,\n        mode: value\n      })\n    );\n  }, [dispatch, collection.uid]);\n\n  const menuItems = useMemo(() => [\n    {\n      id: 'awsv4',\n      label: 'AWS Sig v4',\n      onClick: () => onModeChange('awsv4')\n    },\n    {\n      id: 'basic',\n      label: 'Basic Auth',\n      onClick: () => onModeChange('basic')\n    },\n    {\n      id: 'wsse',\n      label: 'WSSE Auth',\n      onClick: () => onModeChange('wsse')\n    },\n    {\n      id: 'bearer',\n      label: 'Bearer Token',\n      onClick: () => onModeChange('bearer')\n    },\n    {\n      id: 'digest',\n      label: 'Digest Auth',\n      onClick: () => onModeChange('digest')\n    },\n    {\n      id: 'ntlm',\n      label: 'NTLM Auth',\n      onClick: () => onModeChange('ntlm')\n    },\n    {\n      id: 'oauth2',\n      label: 'OAuth 2.0',\n      onClick: () => onModeChange('oauth2')\n    },\n    {\n      id: 'apikey',\n      label: 'API Key',\n      onClick: () => onModeChange('apikey')\n    },\n    {\n      id: 'none',\n      label: 'No Auth',\n      onClick: () => onModeChange('none')\n    }\n  ], [onModeChange]);\n\n  return (\n    <StyledWrapper>\n      <div className=\"inline-flex items-center cursor-pointer auth-mode-selector\">\n        <MenuDropdown\n          items={menuItems}\n          placement=\"bottom-end\"\n          selectedItemId={authMode}\n        >\n          <div className=\"flex items-center justify-center auth-mode-label select-none\">\n            {humanizeRequestAuthMode(authMode)} <IconCaretDown className=\"caret ml-1\" size={14} strokeWidth={2} />\n          </div>\n        </MenuDropdown>\n      </div>\n    </StyledWrapper>\n  );\n};\nexport default AuthMode;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  label {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.subtext1};\n  }\n\n  .single-line-editor-wrapper {\n    padding: 0.15rem 0.4rem;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/index.js",
    "content": "import React from 'react';\nimport SensitiveFieldWarning from 'components/SensitiveFieldWarning';\nimport { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';\nimport get from 'lodash/get';\nimport { useTheme } from 'providers/Theme';\nimport { useDispatch } from 'react-redux';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';\nimport { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';\nimport StyledWrapper from './StyledWrapper';\n\nconst AwsV4Auth = ({ collection }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n\n  const awsv4Auth = collection.draft?.root ? get(collection, 'draft.root.request.auth.awsv4', {}) : get(collection, 'root.request.auth.awsv4', {});\n  const { isSensitive } = useDetectSensitiveField(collection);\n  const { showWarning, warningMessage } = isSensitive(awsv4Auth?.secretAccessKey);\n\n  const handleSave = () => dispatch(saveCollectionSettings(collection.uid));\n\n  const handleAccessKeyIdChange = (accessKeyId) => {\n    dispatch(\n      updateCollectionAuth({\n        mode: 'awsv4',\n        collectionUid: collection.uid,\n        content: {\n          accessKeyId: accessKeyId || '',\n          secretAccessKey: awsv4Auth.secretAccessKey || '',\n          sessionToken: awsv4Auth.sessionToken || '',\n          service: awsv4Auth.service || '',\n          region: awsv4Auth.region || '',\n          profileName: awsv4Auth.profileName || ''\n        }\n      })\n    );\n  };\n\n  const handleSecretAccessKeyChange = (secretAccessKey) => {\n    dispatch(\n      updateCollectionAuth({\n        mode: 'awsv4',\n        collectionUid: collection.uid,\n        content: {\n          accessKeyId: awsv4Auth.accessKeyId || '',\n          secretAccessKey: secretAccessKey || '',\n          sessionToken: awsv4Auth.sessionToken || '',\n          service: awsv4Auth.service || '',\n          region: awsv4Auth.region || '',\n          profileName: awsv4Auth.profileName || ''\n        }\n      })\n    );\n  };\n\n  const handleSessionTokenChange = (sessionToken) => {\n    dispatch(\n      updateCollectionAuth({\n        mode: 'awsv4',\n        collectionUid: collection.uid,\n        content: {\n          accessKeyId: awsv4Auth.accessKeyId || '',\n          secretAccessKey: awsv4Auth.secretAccessKey || '',\n          sessionToken: sessionToken || '',\n          service: awsv4Auth.service || '',\n          region: awsv4Auth.region || '',\n          profileName: awsv4Auth.profileName || ''\n        }\n      })\n    );\n  };\n\n  const handleServiceChange = (service) => {\n    dispatch(\n      updateCollectionAuth({\n        mode: 'awsv4',\n        collectionUid: collection.uid,\n        content: {\n          accessKeyId: awsv4Auth.accessKeyId || '',\n          secretAccessKey: awsv4Auth.secretAccessKey || '',\n          sessionToken: awsv4Auth.sessionToken || '',\n          service: service || '',\n          region: awsv4Auth.region || '',\n          profileName: awsv4Auth.profileName || ''\n        }\n      })\n    );\n  };\n\n  const handleRegionChange = (region) => {\n    dispatch(\n      updateCollectionAuth({\n        mode: 'awsv4',\n        collectionUid: collection.uid,\n        content: {\n          accessKeyId: awsv4Auth.accessKeyId || '',\n          secretAccessKey: awsv4Auth.secretAccessKey || '',\n          sessionToken: awsv4Auth.sessionToken || '',\n          service: awsv4Auth.service || '',\n          region: region || '',\n          profileName: awsv4Auth.profileName || ''\n        }\n      })\n    );\n  };\n\n  const handleProfileNameChange = (profileName) => {\n    dispatch(\n      updateCollectionAuth({\n        mode: 'awsv4',\n        collectionUid: collection.uid,\n        content: {\n          accessKeyId: awsv4Auth.accessKeyId || '',\n          secretAccessKey: awsv4Auth.secretAccessKey || '',\n          sessionToken: awsv4Auth.sessionToken || '',\n          service: awsv4Auth.service || '',\n          region: awsv4Auth.region || '',\n          profileName: profileName || ''\n        }\n      })\n    );\n  };\n\n  return (\n    <StyledWrapper className=\"mt-2 w-full\">\n      <label className=\"block mb-1\">Access Key ID</label>\n      <div className=\"single-line-editor-wrapper mb-3\">\n        <SingleLineEditor\n          value={awsv4Auth.accessKeyId || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleAccessKeyIdChange(val)}\n          collection={collection}\n          isCompact\n        />\n      </div>\n\n      <label className=\"block mb-1\">Secret Access Key</label>\n      <div className=\"single-line-editor-wrapper mb-3 flex items-center\">\n        <SingleLineEditor\n          value={awsv4Auth.secretAccessKey || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleSecretAccessKeyChange(val)}\n          collection={collection}\n          isSecret={true}\n          isCompact\n        />\n        {showWarning && <SensitiveFieldWarning fieldName=\"awsv4-secret-access-key\" warningMessage={warningMessage} />}\n      </div>\n\n      <label className=\"block mb-1\">Session Token</label>\n      <div className=\"single-line-editor-wrapper mb-3\">\n        <SingleLineEditor\n          value={awsv4Auth.sessionToken || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleSessionTokenChange(val)}\n          collection={collection}\n          isCompact\n        />\n      </div>\n\n      <label className=\"block mb-1\">Service</label>\n      <div className=\"single-line-editor-wrapper mb-3\">\n        <SingleLineEditor\n          value={awsv4Auth.service || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleServiceChange(val)}\n          collection={collection}\n          isCompact\n        />\n      </div>\n\n      <label className=\"block mb-1\">Region</label>\n      <div className=\"single-line-editor-wrapper mb-3\">\n        <SingleLineEditor\n          value={awsv4Auth.region || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleRegionChange(val)}\n          collection={collection}\n          isCompact\n        />\n      </div>\n\n      <label className=\"block mb-1\">Profile Name</label>\n      <div className=\"single-line-editor-wrapper\">\n        <SingleLineEditor\n          value={awsv4Auth.profileName || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleProfileNameChange(val)}\n          collection={collection}\n          isCompact\n        />\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default AwsV4Auth;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Auth/BasicAuth/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  label {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.subtext1};\n  }\n\n  .single-line-editor-wrapper {\n    padding: 0.15rem 0.4rem;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Auth/BasicAuth/index.js",
    "content": "import React from 'react';\nimport SensitiveFieldWarning from 'components/SensitiveFieldWarning';\nimport { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';\nimport get from 'lodash/get';\nimport { useTheme } from 'providers/Theme';\nimport { useDispatch } from 'react-redux';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';\nimport { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';\nimport StyledWrapper from './StyledWrapper';\n\nconst BasicAuth = ({ collection }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n\n  const basicAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.basic', {}) : get(collection, 'root.request.auth.basic', {});\n  const { isSensitive } = useDetectSensitiveField(collection);\n  const { showWarning, warningMessage } = isSensitive(basicAuth?.password);\n\n  const handleSave = () => dispatch(saveCollectionSettings(collection.uid));\n\n  const handleUsernameChange = (username) => {\n    dispatch(\n      updateCollectionAuth({\n        mode: 'basic',\n        collectionUid: collection.uid,\n        content: {\n          username: username || '',\n          password: basicAuth.password || ''\n        }\n      })\n    );\n  };\n\n  const handlePasswordChange = (password) => {\n    dispatch(\n      updateCollectionAuth({\n        mode: 'basic',\n        collectionUid: collection.uid,\n        content: {\n          username: basicAuth.username || '',\n          password: password || ''\n        }\n      })\n    );\n  };\n\n  return (\n    <StyledWrapper className=\"mt-2 w-full\">\n      <label className=\"block mb-1\">Username</label>\n      <div className=\"single-line-editor-wrapper mb-3\">\n        <SingleLineEditor\n          value={basicAuth.username || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleUsernameChange(val)}\n          collection={collection}\n          isCompact\n        />\n      </div>\n\n      <label className=\"block mb-1\">Password</label>\n      <div className=\"single-line-editor-wrapper flex items-center\">\n        <SingleLineEditor\n          value={basicAuth.password || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handlePasswordChange(val)}\n          collection={collection}\n          isSecret={true}\n          isCompact\n        />\n        {showWarning && <SensitiveFieldWarning fieldName=\"basic-password\" warningMessage={warningMessage} />}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default BasicAuth;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Auth/BearerAuth/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  label {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.subtext1};\n  }\n\n  .single-line-editor-wrapper {\n    padding: 0.15rem 0.4rem;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Auth/BearerAuth/index.js",
    "content": "import React from 'react';\nimport SensitiveFieldWarning from 'components/SensitiveFieldWarning';\nimport { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';\nimport get from 'lodash/get';\nimport { useTheme } from 'providers/Theme';\nimport { useDispatch } from 'react-redux';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';\nimport { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';\nimport StyledWrapper from './StyledWrapper';\n\nconst BearerAuth = ({ collection }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n\n  const bearerToken = collection.draft?.root ? get(collection, 'draft.root.request.auth.bearer.token', '') : get(collection, 'root.request.auth.bearer.token', '');\n  const { isSensitive } = useDetectSensitiveField(collection);\n  const { showWarning, warningMessage } = isSensitive(bearerToken);\n\n  const handleSave = () => dispatch(saveCollectionSettings(collection.uid));\n\n  const handleTokenChange = (token) => {\n    dispatch(\n      updateCollectionAuth({\n        mode: 'bearer',\n        collectionUid: collection.uid,\n        content: {\n          token: token\n        }\n      })\n    );\n  };\n\n  return (\n    <StyledWrapper className=\"mt-2 w-full\">\n      <label className=\"block mb-1\">Token</label>\n      <div className=\"single-line-editor-wrapper flex items-center\">\n        <SingleLineEditor\n          value={bearerToken}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleTokenChange(val)}\n          collection={collection}\n          isSecret={true}\n          isCompact\n        />\n        {showWarning && <SensitiveFieldWarning fieldName=\"bearer-token\" warningMessage={warningMessage} />}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default BearerAuth;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  label {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.subtext1};\n  }\n\n  .single-line-editor-wrapper {\n    padding: 0.15rem 0.4rem;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Auth/DigestAuth/index.js",
    "content": "import React from 'react';\nimport SensitiveFieldWarning from 'components/SensitiveFieldWarning';\nimport { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';\nimport get from 'lodash/get';\nimport { useTheme } from 'providers/Theme';\nimport { useDispatch } from 'react-redux';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';\nimport { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';\nimport StyledWrapper from './StyledWrapper';\n\nconst DigestAuth = ({ collection }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n\n  const digestAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.digest', {}) : get(collection, 'root.request.auth.digest', {});\n  const { isSensitive } = useDetectSensitiveField(collection);\n  const { showWarning, warningMessage } = isSensitive(digestAuth?.password);\n\n  const handleSave = () => dispatch(saveCollectionSettings(collection.uid));\n\n  const handleUsernameChange = (username) => {\n    dispatch(\n      updateCollectionAuth({\n        mode: 'digest',\n        collectionUid: collection.uid,\n        content: {\n          username: username || '',\n          password: digestAuth.password || ''\n        }\n      })\n    );\n  };\n\n  const handlePasswordChange = (password) => {\n    dispatch(\n      updateCollectionAuth({\n        mode: 'digest',\n        collectionUid: collection.uid,\n        content: {\n          username: digestAuth.username || '',\n          password: password || ''\n        }\n      })\n    );\n  };\n\n  return (\n    <StyledWrapper className=\"mt-2 w-full\">\n      <label className=\"block mb-1\">Username</label>\n      <div className=\"single-line-editor-wrapper mb-3\">\n        <SingleLineEditor\n          value={digestAuth.username || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleUsernameChange(val)}\n          collection={collection}\n          isCompact\n        />\n      </div>\n\n      <label className=\"block mb-1\">Password</label>\n      <div className=\"single-line-editor-wrapper flex items-center\">\n        <SingleLineEditor\n          value={digestAuth.password || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handlePasswordChange(val)}\n          collection={collection}\n          isSecret={true}\n          isCompact\n        />\n        {showWarning && <SensitiveFieldWarning fieldName=\"digest-password\" warningMessage={warningMessage} />}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default DigestAuth;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Auth/NTLMAuth/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  label {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.subtext1};\n  }\n\n  .single-line-editor-wrapper {\n    max-width: 400px;\n    padding: 0.15rem 0.4rem;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Auth/NTLMAuth/index.js",
    "content": "import React from 'react';\nimport SensitiveFieldWarning from 'components/SensitiveFieldWarning';\nimport { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';\nimport get from 'lodash/get';\nimport { useTheme } from 'providers/Theme';\nimport { useDispatch } from 'react-redux';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';\nimport { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';\nimport StyledWrapper from './StyledWrapper';\n\nconst NTLMAuth = ({ collection }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n\n  const ntlmAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.ntlm', {}) : get(collection, 'root.request.auth.ntlm', {});\n  const { isSensitive } = useDetectSensitiveField(collection);\n  const { showWarning, warningMessage } = isSensitive(ntlmAuth?.password);\n\n  const handleSave = () => dispatch(saveCollectionSettings(collection.uid));\n\n  const handleUsernameChange = (username) => {\n    dispatch(\n      updateCollectionAuth({\n        mode: 'ntlm',\n        collectionUid: collection.uid,\n        content: {\n          username: username || '',\n          password: ntlmAuth.password || '',\n          domain: ntlmAuth.domain || ''\n\n        }\n      })\n    );\n  };\n\n  const handlePasswordChange = (password) => {\n    dispatch(\n      updateCollectionAuth({\n        mode: 'ntlm',\n        collectionUid: collection.uid,\n        content: {\n          username: ntlmAuth.username || '',\n          password: password || '',\n          domain: ntlmAuth.domain || ''\n        }\n      })\n    );\n  };\n\n  const handleDomainChange = (domain) => {\n    dispatch(\n      updateCollectionAuth({\n        mode: 'ntlm',\n        collectionUid: collection.uid,\n        content: {\n          username: ntlmAuth.username || '',\n          password: ntlmAuth.password || '',\n          domain: domain || ''\n        }\n      })\n    );\n  };\n\n  return (\n    <StyledWrapper className=\"mt-2 w-full\">\n      <label className=\"block mb-1\">Username</label>\n      <div className=\"single-line-editor-wrapper mb-3\">\n        <SingleLineEditor\n          value={ntlmAuth.username || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleUsernameChange(val)}\n          collection={collection}\n          isCompact\n        />\n      </div>\n\n      <label className=\"block mb-1\">Password</label>\n      <div className=\"single-line-editor-wrapper mb-3 flex items-center\">\n        <SingleLineEditor\n          value={ntlmAuth.password || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handlePasswordChange(val)}\n          collection={collection}\n          isSecret={true}\n          isCompact\n        />\n        {showWarning && <SensitiveFieldWarning fieldName=\"ntlm-password\" warningMessage={warningMessage} />}\n      </div>\n\n      <label className=\"block mb-1\">Domain</label>\n      <div className=\"single-line-editor-wrapper\">\n        <SingleLineEditor\n          value={ntlmAuth.domain || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleDomainChange(val)}\n          collection={collection}\n          isCompact\n        />\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default NTLMAuth;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  label {\n    font-size: ${(props) => props.theme.font.size.base};\n  }\n  .single-line-editor-wrapper {\n    max-width: 400px;\n    padding: 0.15rem 0.4rem;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Auth/OAuth2/index.js",
    "content": "import React from 'react';\nimport get from 'lodash/get';\nimport StyledWrapper from './StyledWrapper';\nimport { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';\nimport OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index';\nimport { updateCollectionAuth } from 'providers/ReduxStore/slices/collections/index';\nimport { useDispatch } from 'react-redux';\nimport OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index';\nimport OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index';\nimport OAuth2Implicit from 'components/RequestPane/Auth/OAuth2/Implicit/index';\nimport GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';\n\nconst GrantTypeComponentMap = ({ collection }) => {\n  const dispatch = useDispatch();\n\n  const save = () => {\n    dispatch(saveCollectionSettings(collection.uid));\n  };\n\n  let request = collection.draft?.root ? get(collection, 'draft.root.request', {}) : get(collection, 'root.request', {});\n  const grantType = get(request, 'auth.oauth2.grantType', {});\n\n  switch (grantType) {\n    case 'password':\n      return <OAuth2PasswordCredentials save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;\n      break;\n    case 'authorization_code':\n      return <OAuth2AuthorizationCode save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;\n      break;\n    case 'client_credentials':\n      return <OAuth2ClientCredentials save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;\n      break;\n    case 'implicit':\n      return <OAuth2Implicit save={save} request={request} updateAuth={updateCollectionAuth} collection={collection} />;\n      break;\n    default:\n      return <div>TBD</div>;\n      break;\n  }\n};\n\nconst OAuth2 = ({ collection }) => {\n  let request = collection.draft?.root ? get(collection, 'draft.root.request', {}) : get(collection, 'root.request', {});\n\n  return (\n    <StyledWrapper className=\"mt-2 w-full\">\n      <GrantTypeSelector request={request} updateAuth={updateCollectionAuth} collection={collection} />\n      <GrantTypeComponentMap collection={collection} />\n    </StyledWrapper>\n  );\n};\n\nexport default OAuth2;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Auth/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  max-width: 800px;\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  label {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.subtext1};\n  }\n\n  .single-line-editor-wrapper {\n    padding: 0.15rem 0.4rem;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Auth/WsseAuth/index.js",
    "content": "import React from 'react';\nimport SensitiveFieldWarning from 'components/SensitiveFieldWarning';\nimport { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';\nimport get from 'lodash/get';\nimport { useTheme } from 'providers/Theme';\nimport { useDispatch } from 'react-redux';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport { updateCollectionAuth } from 'providers/ReduxStore/slices/collections';\nimport { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';\nimport StyledWrapper from './StyledWrapper';\n\nconst WsseAuth = ({ collection }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n\n  const wsseAuth = collection.draft?.root ? get(collection, 'draft.root.request.auth.wsse', {}) : get(collection, 'root.request.auth.wsse', {});\n  const { isSensitive } = useDetectSensitiveField(collection);\n  const { showWarning, warningMessage } = isSensitive(wsseAuth?.password);\n\n  const handleSave = () => dispatch(saveCollectionSettings(collection.uid));\n\n  const handleUserChange = (username) => {\n    dispatch(\n      updateCollectionAuth({\n        mode: 'wsse',\n        collectionUid: collection.uid,\n        content: {\n          username: username || '',\n          password: wsseAuth.password || ''\n        }\n      })\n    );\n  };\n\n  const handlePasswordChange = (password) => {\n    dispatch(\n      updateCollectionAuth({\n        mode: 'wsse',\n        collectionUid: collection.uid,\n        content: {\n          username: wsseAuth.username || '',\n          password: password || ''\n        }\n      })\n    );\n  };\n\n  return (\n    <StyledWrapper className=\"mt-2 w-full\">\n      <label className=\"block mb-1\">Username</label>\n      <div className=\"single-line-editor-wrapper mb-3\">\n        <SingleLineEditor\n          value={wsseAuth.username || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleUserChange(val)}\n          collection={collection}\n          isCompact\n        />\n      </div>\n\n      <label className=\"block mb-1\">Password</label>\n      <div className=\"single-line-editor-wrapper flex items-center\">\n        <SingleLineEditor\n          value={wsseAuth.password || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handlePasswordChange(val)}\n          collection={collection}\n          isSecret={true}\n          isCompact\n        />\n        {showWarning && <SensitiveFieldWarning fieldName=\"wsse-password\" warningMessage={warningMessage} />}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default WsseAuth;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Auth/index.js",
    "content": "import React from 'react';\nimport get from 'lodash/get';\nimport { useDispatch } from 'react-redux';\nimport AuthMode from './AuthMode';\nimport AwsV4Auth from './AwsV4Auth';\nimport BearerAuth from './BearerAuth';\nimport BasicAuth from './BasicAuth';\nimport DigestAuth from './DigestAuth';\nimport WsseAuth from './WsseAuth';\nimport ApiKeyAuth from './ApiKeyAuth/';\nimport { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';\nimport StyledWrapper from './StyledWrapper';\nimport OAuth2 from './OAuth2';\nimport NTLMAuth from './NTLMAuth';\nimport Button from 'ui/Button';\n\nconst Auth = ({ collection }) => {\n  const authMode = collection.draft?.root ? get(collection, 'draft.root.request.auth.mode') : get(collection, 'root.request.auth.mode');\n  const dispatch = useDispatch();\n\n  const handleSave = () => dispatch(saveCollectionSettings(collection.uid));\n\n  const getAuthView = () => {\n    switch (authMode) {\n      case 'awsv4': {\n        return <AwsV4Auth collection={collection} />;\n      }\n      case 'basic': {\n        return <BasicAuth collection={collection} />;\n      }\n      case 'bearer': {\n        return <BearerAuth collection={collection} />;\n      }\n      case 'digest': {\n        return <DigestAuth collection={collection} />;\n      }\n      case 'ntlm': {\n        return <NTLMAuth collection={collection} />;\n      }\n      case 'oauth2': {\n        return <OAuth2 collection={collection} />;\n      }\n      case 'wsse': {\n        return <WsseAuth collection={collection} />;\n      }\n      case 'apikey': {\n        return <ApiKeyAuth collection={collection} />;\n      }\n    }\n  };\n\n  return (\n    <StyledWrapper className=\"w-full h-full\">\n      <div className=\"text-xs mb-4 text-muted\">\n        Configures authentication for the entire collection. This applies to all requests using the{' '}\n        <span className=\"font-medium\">Inherit</span> option in the <span className=\"font-medium\">Auth</span> tab.\n      </div>\n      <div className=\"flex flex-grow justify-start items-center\">\n        <AuthMode collection={collection} />\n      </div>\n      {getAuthView()}\n      <div className=\"mt-6\">\n        <Button type=\"submit\" size=\"sm\" onClick={handleSave}>\n          Save\n        </Button>\n      </div>\n    </StyledWrapper>\n  );\n};\nexport default Auth;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .settings-label {\n    width: 90px;\n  }\n\n  .certificate-icon {\n    color: ${(props) => props.theme.colors.text.yellow};\n  }\n\n  .non-passphrase-input {\n    width: 300px;\n  }\n\n  .available-certificates {\n    background-color: ${(props) => props.theme.requestTabPanel.url.bg};\n\n    button.remove-certificate {\n      color: ${(props) => props.theme.colors.text.danger};\n    }\n  }\n\n  .textbox {\n    border: 1px solid #ccc;\n    padding: 0.15rem 0.45rem;\n    box-shadow: none;\n    border-radius: 0px;\n    outline: none;\n    box-shadow: none;\n    transition: border-color ease-in-out 0.1s;\n    border-radius: 3px;\n    background-color: ${(props) => props.theme.input.bg};\n    border: 1px solid ${(props) => props.theme.input.border};\n\n    &:focus {\n      border: solid 1px ${(props) => props.theme.input.focusBorder} !important;\n      outline: none !important;\n    }\n  }\n\n  .protocol-placeholder {\n    height: 100%;\n    position: relative;\n    display: inline-block;\n    width: 60px;\n    overflow: hidden;\n  }\n\n  .protocol-https,\n  .protocol-grpcs,\n  .protocol-wss {\n    position: absolute;\n    right: 8px;\n    top: 0;\n    bottom: 0;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n  }\n\n  .protocol-https {\n    animation: slideUpDown 9s infinite;\n    transform: translateY(0);\n  }\n\n  .protocol-grpcs {\n    animation: slideUpDown 9s infinite 3s;\n    transform: translateY(100%);\n  }\n\n  .protocol-wss {\n    animation: slideUpDown 9s infinite 6s;\n    transform: translateY(100%);\n  }\n\n  @keyframes slideUpDown {\n    0%, 30% {\n      transform: translateY(0);\n    }\n    33.33%, 97% {\n      transform: translateY(100%);\n    }\n    100% {\n      transform: translateY(0);\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/ClientCertSettings/index.js",
    "content": "import React from 'react';\nimport { IconCertificate, IconTrash, IconWorld } from '@tabler/icons';\nimport { useFormik } from 'formik';\nimport * as Yup from 'yup';\nimport StyledWrapper from './StyledWrapper';\nimport { useRef } from 'react';\nimport path from 'utils/common/path';\nimport SensitiveFieldWarning from 'components/SensitiveFieldWarning/index';\nimport SingleLineEditor from 'components/SingleLineEditor/index';\nimport { useDetectSensitiveField } from 'hooks/useDetectSensitiveField/index';\nimport { useTheme } from 'styled-components';\nimport { useDispatch } from 'react-redux';\nimport { updateCollectionClientCertificates } from 'providers/ReduxStore/slices/collections';\nimport { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';\nimport get from 'lodash/get';\nimport Button from 'ui/Button';\n\nconst ClientCertSettings = ({ collection }) => {\n  const dispatch = useDispatch();\n\n  // Get client certs from draft if exists, otherwise from brunoConfig\n  const clientCertConfig = collection.draft?.brunoConfig\n    ? get(collection, 'draft.brunoConfig.clientCertificates.certs', [])\n    : get(collection, 'brunoConfig.clientCertificates.certs', []);\n  const certFilePathInputRef = useRef();\n  const keyFilePathInputRef = useRef();\n  const pfxFilePathInputRef = useRef();\n  const { storedTheme } = useTheme();\n\n  const formik = useFormik({\n    initialValues: {\n      domain: '',\n      type: 'cert',\n      certFilePath: '',\n      keyFilePath: '',\n      pfxFilePath: '',\n      passphrase: ''\n    },\n    validationSchema: Yup.object({\n      domain: Yup.string()\n        .required()\n        .trim()\n        .test('not-empty-after-trim', 'Domain is required', (value) => value && value.trim().length > 0),\n      type: Yup.string().required().oneOf(['cert', 'pfx']),\n      certFilePath: Yup.string().when('type', {\n        is: (type) => type == 'cert',\n        then: Yup.string().min(1, 'certFilePath is a required field').required()\n      }),\n      keyFilePath: Yup.string().when('type', {\n        is: (type) => type == 'cert',\n        then: Yup.string().min(1, 'keyFilePath is a required field').required()\n      }),\n      pfxFilePath: Yup.string().when('type', {\n        is: (type) => type == 'pfx',\n        then: Yup.string().min(1, 'pfxFilePath is a required field').required()\n      }),\n      passphrase: Yup.string()\n    }),\n    onSubmit: (values) => {\n      let relevantValues = {};\n      if (values.type === 'cert') {\n        relevantValues = {\n          domain: values.domain?.trim(),\n          type: values.type,\n          certFilePath: values.certFilePath,\n          keyFilePath: values.keyFilePath,\n          passphrase: values.passphrase\n        };\n      } else {\n        relevantValues = {\n          domain: values.domain?.trim(),\n          type: values.type,\n          pfxFilePath: values.pfxFilePath,\n          passphrase: values.passphrase\n        };\n      }\n\n      // Add the new cert to the existing certs in draft\n      const updatedCerts = [...clientCertConfig, relevantValues];\n      const clientCertificates = {\n        enabled: true,\n        certs: updatedCerts\n      };\n\n      dispatch(updateCollectionClientCertificates({\n        collectionUid: collection.uid,\n        clientCertificates\n      }));\n\n      formik.resetForm();\n      resetFileInputFields();\n    }\n  });\n\n  const { isSensitive } = useDetectSensitiveField(collection);\n  const { showWarning, warningMessage } = isSensitive(formik.values.passphrase);\n\n  const getFile = (e) => {\n    const filePath = window?.ipcRenderer?.getFilePath(e?.files?.[0]);\n    if (filePath) {\n      let relativePath = path.relative(collection.pathname, filePath);\n      formik.setFieldValue(e.name, relativePath);\n    }\n  };\n\n  const resetFileInputFields = () => {\n    if (certFilePathInputRef.current) {\n      certFilePathInputRef.current.value = '';\n    }\n    if (keyFilePathInputRef.current) {\n      keyFilePathInputRef.current.value = '';\n    }\n    if (pfxFilePathInputRef.current) {\n      pfxFilePathInputRef.current.value = '';\n    }\n  };\n\n  const handleTypeChange = (e) => {\n    formik.setFieldValue('type', e.target.value);\n    if (e.target.value === 'cert') {\n      formik.setFieldValue('pfxFilePath', '');\n      pfxFilePathInputRef.current.value = '';\n    } else {\n      formik.setFieldValue('certFilePath', '');\n      certFilePathInputRef.current.value = '';\n      formik.setFieldValue('keyFilePath', '');\n      keyFilePathInputRef.current.value = '';\n    }\n  };\n\n  const handleRemove = (indexToRemove) => {\n    const updatedCerts = clientCertConfig.filter((cert, index) => index !== indexToRemove);\n    const clientCertificates = {\n      enabled: true,\n      certs: updatedCerts\n    };\n\n    dispatch(updateCollectionClientCertificates({\n      collectionUid: collection.uid,\n      clientCertificates\n    }));\n  };\n\n  const handleSave = () => dispatch(saveCollectionSettings(collection.uid));\n\n  return (\n    <StyledWrapper className=\"w-full h-full\">\n      <div className=\"text-xs mb-4 text-muted\">Add client certificates to be used for specific domains.</div>\n\n      <h1 className=\"font-medium\">Client Certificates</h1>\n      <ul className=\"mt-4\">\n        {!clientCertConfig.length\n          ? 'No client certificates added'\n          : clientCertConfig.map((clientCert, index) => (\n              <li key={`client-cert-${index}`} className=\"flex items-center available-certificates p-2 rounded-lg mb-2\">\n                <div className=\"flex items-center w-full justify-between\">\n                  <div className=\"flex w-full items-center\">\n                    <IconWorld className=\"mr-2\" size={18} strokeWidth={1.5} />\n                    {clientCert.domain}\n                  </div>\n                  <div className=\"flex w-full items-center\">\n                    <IconCertificate className=\"mr-2 flex-shrink-0\" size={18} strokeWidth={1.5} />\n                    {clientCert.type === 'cert' ? clientCert.certFilePath : clientCert.pfxFilePath}\n                  </div>\n                  <button onClick={() => handleRemove(index)} className=\"remove-certificate ml-2\">\n                    <IconTrash size={18} strokeWidth={1.5} />\n                  </button>\n                </div>\n              </li>\n            ))}\n      </ul>\n\n      <h1 className=\"font-medium mt-8 mb-2\">Add Client Certificate</h1>\n      <form className=\"bruno-form\" onSubmit={formik.handleSubmit}>\n        <div className=\"mb-3 flex items-center\">\n          <label className=\"settings-label\" htmlFor=\"domain\">\n            Domain\n          </label>\n          <div className=\"relative flex items-center\">\n            <div className=\"absolute left-0 pl-2 text-gray-400 pointer-events-none flex items-center h-full\">\n              <span className=\"protocol-placeholder\">\n                <span className=\"protocol-https\">https://</span>\n                <span className=\"protocol-grpcs\">grpcs://</span>\n                <span className=\"protocol-wss\">wss://</span>\n              </span>\n            </div>\n            <input\n              id=\"domain\"\n              type=\"text\"\n              name=\"domain\"\n              placeholder=\"example.org\"\n              className=\"block textbox non-passphrase-input !pl-[60px]\"\n              onChange={formik.handleChange}\n              value={formik.values.domain || ''}\n            />\n          </div>\n          {formik.touched.domain && formik.errors.domain ? (\n            <div className=\"ml-1 text-red-500\">{formik.errors.domain}</div>\n          ) : null}\n        </div>\n        <div className=\"mb-3 flex items-center\">\n          <label id=\"type-label\" className=\"settings-label\">\n            Type\n          </label>\n          <div className=\"flex items-center\" aria-labelledby=\"type-label\">\n            <label className=\"flex items-center cursor-pointer\" htmlFor=\"cert\">\n              <input\n                id=\"cert\"\n                type=\"radio\"\n                name=\"type\"\n                value=\"cert\"\n                checked={formik.values.type === 'cert'}\n                onChange={handleTypeChange}\n                className=\"mr-1\"\n              />\n              Cert\n            </label>\n            <label className=\"flex items-center ml-4 cursor-pointer\" htmlFor=\"pfx\">\n              <input\n                id=\"pfx\"\n                type=\"radio\"\n                name=\"type\"\n                value=\"pfx\"\n                checked={formik.values.type === 'pfx'}\n                onChange={handleTypeChange}\n                className=\"mr-1\"\n              />\n              PFX\n            </label>\n          </div>\n        </div>\n        {formik.values.type === 'cert' ? (\n          <>\n            <div className=\"mb-3 flex items-center\">\n              <label className=\"settings-label\" htmlFor=\"certFilePath\">\n                Cert file\n              </label>\n              <div className=\"flex flex-row gap-2 justify-start\">\n                <input\n                  key=\"certFilePath\"\n                  id=\"certFilePath\"\n                  type=\"file\"\n                  name=\"certFilePath\"\n                  className={`non-passphrase-input ${formik.values.certFilePath?.length ? 'hidden' : 'block'}`}\n                  onChange={(e) => getFile(e.target)}\n                  ref={certFilePathInputRef}\n                />\n                {formik.values.certFilePath ? (\n                  <div className=\"flex flex-row gap-2 items-center\">\n                    <div\n                      className=\"my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]\"\n                      title={path.basename(formik.values.certFilePath)}\n                    >\n                      {path.basename(formik.values.certFilePath)}\n                    </div>\n                    <IconTrash\n                      size={18}\n                      strokeWidth={1.5}\n                      className=\"ml-2 cursor-pointer\"\n                      onClick={() => {\n                        formik.setFieldValue('certFilePath', '');\n                        certFilePathInputRef.current.value = '';\n                      }}\n                    />\n                  </div>\n                ) : (\n                  <></>\n                )}\n              </div>\n              {formik.touched.certFilePath && formik.errors.certFilePath ? (\n                <div className=\"ml-1 text-red-500\">{formik.errors.certFilePath}</div>\n              ) : null}\n            </div>\n            <div className=\"mb-3 flex items-center\">\n              <label className=\"settings-label\" htmlFor=\"keyFilePath\">\n                Key file\n              </label>\n              <div className=\"flex flex-row gap-2\">\n                <input\n                  key=\"keyFilePath\"\n                  id=\"keyFilePath\"\n                  type=\"file\"\n                  name=\"keyFilePath\"\n                  className={`non-passphrase-input ${formik.values.keyFilePath?.length ? 'hidden' : 'block'}`}\n                  onChange={(e) => getFile(e.target)}\n                  ref={keyFilePathInputRef}\n                />\n                {formik.values.keyFilePath ? (\n                  <div className=\"flex flex-row gap-2 items-center\">\n                    <div\n                      className=\"my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]\"\n                      title={path.basename(formik.values.keyFilePath)}\n                    >\n                      {path.basename(formik.values.keyFilePath)}\n                    </div>\n                    <IconTrash\n                      size={18}\n                      strokeWidth={1.5}\n                      className=\"ml-2 cursor-pointer\"\n                      onClick={() => {\n                        formik.setFieldValue('keyFilePath', '');\n                        keyFilePathInputRef.current.value = '';\n                      }}\n                    />\n                  </div>\n                ) : (\n                  <></>\n                )}\n              </div>\n              {formik.touched.keyFilePath && formik.errors.keyFilePath ? (\n                <div className=\"ml-1 text-red-500\">{formik.errors.keyFilePath}</div>\n              ) : null}\n            </div>\n          </>\n        ) : (\n          <>\n            <div className=\"mb-3 flex items-center\">\n              <label className=\"settings-label\" htmlFor=\"pfxFilePath\">\n                PFX file\n              </label>\n              <div className=\"flex flex-row gap-2\">\n                <input\n                  key=\"pfxFilePath\"\n                  id=\"pfxFilePath\"\n                  type=\"file\"\n                  name=\"pfxFilePath\"\n                  className={`non-passphrase-input ${formik.values.pfxFilePath?.length ? 'hidden' : 'block'}`}\n                  onChange={(e) => getFile(e.target)}\n                  ref={pfxFilePathInputRef}\n                />\n                {formik.values.pfxFilePath ? (\n                  <div className=\"flex flex-row gap-2 items-center\">\n                    <div\n                      className=\"my-[3px] overflow-hidden text-ellipsis whitespace-nowrap max-w-[300px]\"\n                      title={path.basename(formik.values.pfxFilePath)}\n                    >\n                      {path.basename(formik.values.pfxFilePath)}\n                    </div>\n                    <IconTrash\n                      size={18}\n                      strokeWidth={1.5}\n                      className=\"ml-2 cursor-pointer\"\n                      onClick={() => {\n                        formik.setFieldValue('pfxFilePath', '');\n                        pfxFilePathInputRef.current.value = '';\n                      }}\n                    />\n                  </div>\n                ) : (\n                  <></>\n                )}\n              </div>\n              {formik.touched.pfxFilePath && formik.errors.pfxFilePath ? (\n                <div className=\"ml-1 text-red-500\">{formik.errors.pfxFilePath}</div>\n              ) : null}\n            </div>\n          </>\n        )}\n        <div className=\"mb-3 flex items-center\">\n          <label className=\"settings-label\" htmlFor=\"passphrase\">\n            Passphrase\n          </label>\n          <div className=\"textbox flex flex-row items-center w-[300px] h-[1.70rem] relative\">\n            <SingleLineEditor\n              value={formik.values.passphrase || ''}\n              theme={storedTheme}\n              onChange={(val) => formik.setFieldValue('passphrase', val)}\n              collection={collection}\n              isSecret={true}\n            />\n            {showWarning && <SensitiveFieldWarning fieldName=\"basic-password\" warningMessage={warningMessage} />}\n          </div>\n          {formik.touched.passphrase && formik.errors.passphrase ? (\n            <div className=\"ml-1 text-red-500\">{formik.errors.passphrase}</div>\n          ) : null}\n        </div>\n        <div className=\"mt-6 flex flex-row gap-2 items-center\">\n          <Button type=\"submit\" size=\"sm\" data-testid=\"add-client-cert\">\n            Add\n          </Button>\n          <div className=\"h-4 border-l border-gray-600\"></div>\n          <Button type=\"button\" size=\"sm\" onClick={handleSave}>\n            Save\n          </Button>\n        </div>\n      </form>\n    </StyledWrapper>\n  );\n};\n\nexport default ClientCertSettings;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Docs/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n\n  .editing-mode {\n    cursor: pointer;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Docs/index.js",
    "content": "import 'github-markdown-css/github-markdown.css';\nimport get from 'lodash/get';\nimport { updateCollectionDocs, deleteCollectionDraft } from 'providers/ReduxStore/slices/collections';\nimport { useTheme } from 'providers/Theme';\nimport { useState } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';\nimport Markdown from 'components/MarkDown';\nimport CodeEditor from 'components/CodeEditor';\nimport StyledWrapper from './StyledWrapper';\nimport { IconEdit, IconX, IconFileText } from '@tabler/icons';\nimport Button from 'ui/Button/index';\nimport ActionIcon from 'ui/ActionIcon/index';\n\nconst Docs = ({ collection }) => {\n  const dispatch = useDispatch();\n  const { displayedTheme } = useTheme();\n  const [isEditing, setIsEditing] = useState(false);\n  const docs = collection.draft?.root ? get(collection, 'draft.root.docs', '') : get(collection, 'root.docs', '');\n  const preferences = useSelector((state) => state.app.preferences);\n\n  const toggleViewMode = () => {\n    setIsEditing((prev) => !prev);\n  };\n\n  const onEdit = (value) => {\n    dispatch(\n      updateCollectionDocs({\n        collectionUid: collection.uid,\n        docs: value\n      })\n    );\n  };\n\n  const handleDiscardChanges = () => {\n    dispatch((\n      updateCollectionDocs({\n        collectionUid: collection.uid,\n        docs: docs\n      }))\n    );\n    toggleViewMode();\n  };\n\n  const onSave = () => {\n    dispatch(saveCollectionSettings(collection.uid));\n    toggleViewMode();\n  };\n\n  return (\n    <StyledWrapper className=\"h-full w-full relative flex flex-col\">\n      <div className=\"flex flex-row w-full justify-between items-center mb-4\">\n        <div className=\"text-lg font-medium flex items-center gap-2\">\n          <IconFileText size={20} strokeWidth={1.5} />\n          Documentation\n        </div>\n        <div className=\"flex flex-row gap-2 items-center justify-center\">\n          {isEditing ? (\n            <>\n              <Button type=\"button\" color=\"secondary\" onClick={handleDiscardChanges}>\n                Cancel\n              </Button>\n              <Button type=\"button\" onClick={onSave}>\n                Save\n              </Button>\n            </>\n          ) : (\n            <ActionIcon className=\"editing-mode\" onClick={toggleViewMode}>\n              <IconEdit className=\"cursor-pointer\" size={16} strokeWidth={1.5} />\n            </ActionIcon>\n          )}\n        </div>\n      </div>\n      {isEditing ? (\n        <CodeEditor\n          collection={collection}\n          theme={displayedTheme}\n          value={docs}\n          onEdit={onEdit}\n          onSave={onSave}\n          mode=\"application/text\"\n          font={get(preferences, 'font.codeFont', 'default')}\n          fontSize={get(preferences, 'font.codeFontSize')}\n        />\n      ) : (\n        <div className=\"h-full overflow-auto pl-1\">\n          <div className=\"h-[1px] min-h-[500px]\">\n            {\n              docs?.length > 0\n                ? <Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />\n                : <Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={documentationPlaceholder} />\n            }\n          </div>\n        </div>\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default Docs;\n\nconst documentationPlaceholder = `\nWelcome to your collection documentation! This space is designed to help you document your API collection effectively.\n\n## Overview\nUse this section to provide a high-level overview of your collection. You can describe:\n- The purpose of these API endpoints\n- Key features and functionalities\n- Target audience or users\n\n## Best Practices\n- Keep documentation up to date\n- Include request/response examples\n- Document error scenarios\n- Add relevant links and references\n\n## Markdown Support\nThis documentation supports Markdown formatting! You can use:\n- **Bold** and *italic* text\n- \\`code blocks\\` and syntax highlighting\n- Tables and lists\n- [Links](https://usebruno.com)\n- And more!\n`;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Headers/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  max-width: 800px;\n\n  table {\n    width: 100%;\n    border-collapse: collapse;\n    font-weight: 500;\n    table-layout: fixed;\n\n    thead,\n    td {\n      border: 1px solid ${(props) => props.theme.table.border};\n    }\n\n    thead {\n      color: ${(props) => props.theme.table.thead.color};\n      font-size: ${(props) => props.theme.font.size.base};\n      user-select: none;\n    }\n    td {\n      padding: 6px 10px;\n\n      &:nth-child(1) {\n        width: 30%;\n      }\n\n      &:nth-child(3) {\n        width: 70px;\n      }\n    }\n  }\n\n  .btn-add-header {\n    font-size: ${(props) => props.theme.font.size.base};\n  }\n\n  input[type='text'] {\n    width: 100%;\n    border: solid 1px transparent;\n    outline: none !important;\n    background-color: inherit;\n\n    &:focus {\n      outline: none !important;\n      border: solid 1px transparent;\n    }\n  }\n\n  input[type='checkbox'] {\n    cursor: pointer;\n    position: relative;\n    top: 1px;\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Headers/index.js",
    "content": "import React, { useState, useCallback } from 'react';\nimport get from 'lodash/get';\nimport { useDispatch } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport { setCollectionHeaders } from 'providers/ReduxStore/slices/collections';\nimport { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport EditableTable from 'components/EditableTable';\nimport StyledWrapper from './StyledWrapper';\nimport { headers as StandardHTTPHeaders } from 'know-your-http-well';\nimport { MimeTypes } from 'utils/codemirror/autocompleteConstants';\nimport BulkEditor from 'components/BulkEditor/index';\nimport Button from 'ui/Button';\nimport { headerNameRegex, headerValueRegex } from 'utils/common/regex';\n\nconst headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);\n\nconst Headers = ({ collection }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n  const headers = collection.draft?.root\n    ? get(collection, 'draft.root.request.headers', [])\n    : get(collection, 'root.request.headers', []);\n  const [isBulkEditMode, setIsBulkEditMode] = useState(false);\n\n  const toggleBulkEditMode = () => {\n    setIsBulkEditMode(!isBulkEditMode);\n  };\n\n  const handleHeadersChange = useCallback((updatedHeaders) => {\n    dispatch(setCollectionHeaders({ collectionUid: collection.uid, headers: updatedHeaders }));\n  }, [dispatch, collection.uid]);\n\n  const handleSave = () => dispatch(saveCollectionSettings(collection.uid));\n\n  const getRowError = useCallback((row, index, key) => {\n    if (key === 'name') {\n      if (!row.name || row.name.trim() === '') return null;\n      if (!headerNameRegex.test(row.name)) {\n        return 'Header name cannot contain spaces or newlines';\n      }\n    }\n    if (key === 'value') {\n      if (!row.value) return null;\n      if (!headerValueRegex.test(row.value)) {\n        return 'Header value cannot contain newlines';\n      }\n    }\n    return null;\n  }, []);\n\n  const columns = [\n    {\n      key: 'name',\n      name: 'Name',\n      isKeyField: true,\n      placeholder: 'Name',\n      width: '30%',\n      render: ({ value, onChange }) => (\n        <SingleLineEditor\n          value={value || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(newValue) => onChange(newValue.replace(/[\\r\\n]/g, ''))}\n          autocomplete={headerAutoCompleteList}\n          collection={collection}\n          placeholder={!value ? 'Name' : ''}\n        />\n      )\n    },\n    {\n      key: 'value',\n      name: 'Value',\n      placeholder: 'Value',\n      render: ({ value, onChange }) => (\n        <SingleLineEditor\n          value={value || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={onChange}\n          collection={collection}\n          autocomplete={MimeTypes}\n          placeholder={!value ? 'Value' : ''}\n        />\n      )\n    }\n  ];\n\n  const defaultRow = {\n    name: '',\n    value: '',\n    description: ''\n  };\n\n  if (isBulkEditMode) {\n    return (\n      <StyledWrapper className=\"h-full w-full\">\n        <div className=\"text-xs mb-4 text-muted\">\n          Add request headers that will be sent with every request in this collection.\n        </div>\n        <BulkEditor\n          params={headers}\n          onChange={handleHeadersChange}\n          onToggle={toggleBulkEditMode}\n          onSave={handleSave}\n        />\n      </StyledWrapper>\n    );\n  }\n\n  return (\n    <StyledWrapper className=\"h-full w-full\">\n      <div className=\"text-xs mb-4 text-muted\">\n        Add request headers that will be sent with every request in this collection.\n      </div>\n      <EditableTable\n        columns={columns}\n        rows={headers}\n        onChange={handleHeadersChange}\n        defaultRow={defaultRow}\n        getRowError={getRowError}\n      />\n      <div className=\"flex justify-end mt-2\">\n        <button className=\"text-link select-none\" onClick={toggleBulkEditMode}>\n          Bulk Edit\n        </button>\n      </div>\n      <div className=\"mt-6\">\n        <Button type=\"submit\" size=\"sm\" onClick={handleSave}>\n          Save\n        </Button>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Headers;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Overview/Info/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst StyledWrapper = styled.div`\n  .icon-box {\n    &.location {\n      background-color: ${(props) => rgba(props.theme.textLink, 0.08)};\n      border: 1px solid ${(props) => rgba(props.theme.textLink, 0.09)};\n\n      svg {\n        color: ${(props) => props.theme.textLink};\n      }\n    }\n\n    &.environments {\n      background-color: ${(props) => rgba(props.theme.colors.text.green, 0.08)};\n      border: 1px solid ${(props) => rgba(props.theme.colors.text.green, 0.09)};\n\n      svg {\n        color: ${(props) => props.theme.colors.text.green};\n      }\n    }\n\n    &.requests {\n      background-color: ${(props) => rgba(props.theme.colors.text.purple, 0.08)};\n      border: 1px solid ${(props) => rgba(props.theme.colors.text.purple, 0.09)};\n\n      svg {\n        color: ${(props) => props.theme.colors.text.purple};\n      }\n    }\n\n    &.share {\n      background-color: ${(props) => rgba(props.theme.textLink, 0.08)};\n      border: 1px solid ${(props) => rgba(props.theme.textLink, 0.09)};\n\n      svg {\n        color: ${(props) => props.theme.textLink};\n      }\n    }\n\n    &.generate-docs {\n      background-color: ${(props) => rgba(props.theme.accents.primary, 0.08)};\n      border: 1px solid ${(props) => rgba(props.theme.accents.primary, 0.09)};\n\n      svg {\n        color: ${(props) => props.theme.accents.primary};\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Overview/Info/index.js",
    "content": "import React from 'react';\nimport { getTotalRequestCountInCollection } from 'utils/collections/';\nimport { IconFolder, IconWorld, IconApi, IconShare, IconBook } from '@tabler/icons';\nimport { areItemsLoading, getItemsLoadStats } from 'utils/collections/index';\nimport { useState } from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport ShareCollection from 'components/ShareCollection/index';\nimport GenerateDocumentation from 'components/Sidebar/Collections/Collection/GenerateDocumentation';\nimport { addTab } from 'providers/ReduxStore/slices/tabs';\nimport StyledWrapper from './StyledWrapper';\n\nconst Info = ({ collection }) => {\n  const dispatch = useDispatch();\n  const totalRequestsInCollection = getTotalRequestCountInCollection(collection);\n\n  const isCollectionLoading = areItemsLoading(collection);\n  const { loading: itemsLoadingCount, total: totalItems } = getItemsLoadStats(collection);\n  const [showShareCollectionModal, toggleShowShareCollectionModal] = useState(false);\n  const [showGenerateDocumentationModal, setShowGenerateDocumentationModal] = useState(false);\n\n  const globalEnvironments = useSelector((state) => state.globalEnvironments.globalEnvironments);\n\n  const collectionEnvironmentCount = collection.environments?.length || 0;\n  const globalEnvironmentCount = globalEnvironments?.length || 0;\n\n  const handleToggleShowShareCollectionModal = (value) => (e) => {\n    toggleShowShareCollectionModal(value);\n  };\n\n  return (\n    <StyledWrapper className=\"w-full flex flex-col h-fit\">\n      <div className=\"rounded-lg py-6\">\n        <div className=\"grid gap-5\">\n          {/* Location Row */}\n          <div className=\"flex items-start\">\n            <div className=\"icon-box location flex-shrink-0 p-3 rounded-lg\">\n              <IconFolder className=\"w-5 h-5\" stroke={1.5} />\n            </div>\n            <div className=\"ml-4\">\n              <div className=\"font-medium\">Location</div>\n              <div className=\"mt-1 text-muted break-all\">\n                {collection.pathname}\n              </div>\n            </div>\n          </div>\n\n          {/* Environments Row */}\n          <div className=\"flex items-start\">\n            <div className=\"icon-box environments flex-shrink-0 p-3 rounded-lg\">\n              <IconWorld className=\"w-5 h-5\" stroke={1.5} />\n            </div>\n            <div className=\"ml-4\">\n              <div className=\"font-medium\">Environments</div>\n              <div className=\"mt-1 flex flex-col gap-1\">\n                <button\n                  type=\"button\"\n                  className=\"text-link cursor-pointer hover:underline text-left bg-transparent\"\n                  onClick={() => {\n                    dispatch(\n                      addTab({\n                        uid: `${collection.uid}-environment-settings`,\n                        collectionUid: collection.uid,\n                        type: 'environment-settings'\n                      })\n                    );\n                  }}\n                >\n                  {collectionEnvironmentCount} collection environment{collectionEnvironmentCount !== 1 ? 's' : ''}\n                </button>\n                <button\n                  type=\"button\"\n                  className=\"text-link cursor-pointer hover:underline text-left bg-transparent\"\n                  onClick={() => {\n                    dispatch(\n                      addTab({\n                        uid: `${collection.uid}-global-environment-settings`,\n                        collectionUid: collection.uid,\n                        type: 'global-environment-settings'\n                      })\n                    );\n                  }}\n                >\n                  {globalEnvironmentCount} global environment{globalEnvironmentCount !== 1 ? 's' : ''}\n                </button>\n              </div>\n            </div>\n          </div>\n\n          {/* Requests Row */}\n          <div className=\"flex items-start\">\n            <div className=\"icon-box requests flex-shrink-0 p-3 rounded-lg\">\n              <IconApi className=\"w-5 h-5\" stroke={1.5} />\n            </div>\n            <div className=\"ml-4\">\n              <div className=\"font-medium\">Requests</div>\n              <div className=\"mt-1 text-muted\">\n                {\n                  isCollectionLoading ? `${totalItems - itemsLoadingCount} out of ${totalItems} requests in the collection loaded` : `${totalRequestsInCollection} request${totalRequestsInCollection !== 1 ? 's' : ''} in collection`\n                }\n              </div>\n            </div>\n          </div>\n\n          <div className=\"flex items-start group cursor-pointer\" onClick={handleToggleShowShareCollectionModal(true)}>\n            <div className=\"icon-box share flex-shrink-0 p-3 rounded-lg\">\n              <IconShare className=\"w-5 h-5\" stroke={1.5} />\n            </div>\n            <div className=\"ml-4 h-full flex flex-col justify-start\">\n              <div className=\"font-medium h-fit my-auto\">Share</div>\n              <div className=\"group-hover:underline text-link\">\n                Share Collection\n              </div>\n            </div>\n          </div>\n          {showShareCollectionModal && <ShareCollection collectionUid={collection.uid} onClose={handleToggleShowShareCollectionModal(false)} />}\n\n          <div className=\"flex items-start group cursor-pointer\" onClick={() => setShowGenerateDocumentationModal(true)}>\n            <div className=\"icon-box generate-docs flex-shrink-0 p-3 rounded-lg\">\n              <IconBook className=\"w-5 h-5\" stroke={1.5} />\n            </div>\n            <div className=\"ml-4 h-full flex flex-col justify-start\">\n              <div className=\"font-medium h-fit my-auto\">Documentation</div>\n              <div className=\"group-hover:underline text-link\">\n                Generate Docs\n              </div>\n            </div>\n          </div>\n          {showGenerateDocumentationModal && <GenerateDocumentation collectionUid={collection.uid} onClose={() => setShowGenerateDocumentationModal(false)} />}\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Info;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst StyledWrapper = styled.div`\n  &.card {\n    background-color: ${(props) => props.theme.requestTabPanel.card.bg};\n\n    .title {\n      border-top: 1px solid ${(props) => props.theme.table.border};\n      border-left: 1px solid ${(props) => props.theme.table.border};\n      border-right: 1px solid ${(props) => props.theme.table.border};\n\n      border-top-left-radius: 3px;\n      border-top-right-radius: 3px;\n\n      background-color: ${(props) => props.theme.status.warning.background};\n    }\n\n    .warning-icon {\n      color: ${(props) => props.theme.status.warning.text};\n    }\n\n    .table {\n      thead {\n        color: ${(props) => props.theme.table.thead.color} !important;\n        background: ${(props) => props.theme.sidebar.bg};\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Overview/RequestsNotLoaded/index.js",
    "content": "import React from 'react';\nimport { flattenItems } from 'utils/collections';\nimport { IconAlertTriangle } from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { isItemARequest, itemIsOpenedInTabs } from 'utils/tabs/index';\nimport { getDefaultRequestPaneTab } from 'utils/collections/index';\nimport { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';\n\nconst RequestsNotLoaded = ({ collection }) => {\n  const dispatch = useDispatch();\n  const tabs = useSelector((state) => state.tabs.tabs);\n  const flattenedItems = flattenItems(collection.items);\n  const itemsFailedLoading = flattenedItems?.filter((item) => item?.partial && !item?.loading);\n\n  if (!itemsFailedLoading?.length) {\n    return null;\n  }\n\n  const handleRequestClick = (item) => (e) => {\n    e.preventDefault();\n    if (isItemARequest(item)) {\n      if (itemIsOpenedInTabs(item, tabs)) {\n        dispatch(\n          focusTab({\n            uid: item.uid\n          })\n        );\n        return;\n      }\n      dispatch(\n        addTab({\n          uid: item.uid,\n          collectionUid: collection.uid,\n          requestPaneTab: getDefaultRequestPaneTab(item)\n        })\n      );\n      return;\n    }\n  };\n\n  return (\n    <StyledWrapper className=\"w-full card my-2\">\n      <div className=\"flex items-center gap-2 px-3 py-2 title\">\n        <IconAlertTriangle size={16} className=\"warning-icon\" />\n        <span className=\"font-medium\">Following requests were not loaded</span>\n      </div>\n      <table className=\"w-full border-collapse\">\n        <thead>\n          <tr>\n            <th className=\"py-2 px-3 text-left font-medium\">\n              Pathname\n            </th>\n            <th className=\"py-2 px-3 text-left font-medium\">\n              Size\n            </th>\n          </tr>\n        </thead>\n        <tbody>\n          {flattenedItems?.map((item, index) => (\n            item?.partial && !item?.loading ? (\n              <tr key={index} className=\"cursor-pointer\" onClick={handleRequestClick(item)}>\n                <td className=\"py-1.5 px-3\">\n                  {item?.pathname?.split(`${collection?.pathname}/`)?.[1]}\n                </td>\n                <td className=\"py-1.5 px-3\">\n                  {item?.size?.toFixed?.(2)}&nbsp;MB\n                </td>\n              </tr>\n            ) : null\n          ))}\n        </tbody>\n      </table>\n    </StyledWrapper>\n  );\n};\n\nexport default RequestsNotLoaded;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Overview/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .partial {\n    color: ${(props) => props.theme.colors.text.yellow};\n    opacity: 0.8;\n  }\n\n  .loading {\n    color: ${(props) => props.theme.colors.text.muted};\n    opacity: 0.8;\n  }\n\n  .completed {\n    color: ${(props) => props.theme.colors.text.green};\n    opacity: 0.8;\n  }\n\n  .failed {\n    color: ${(props) => props.theme.colors.text.danger};\n    opacity: 0.8;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Overview/index.js",
    "content": "import StyledWrapper from './StyledWrapper';\nimport Docs from '../Docs';\nimport Info from './Info';\nimport { IconBox } from '@tabler/icons';\nimport RequestsNotLoaded from './RequestsNotLoaded';\n\nconst Overview = ({ collection }) => {\n  return (\n    <div className=\"h-full\">\n      <div className=\"grid grid-cols-5 gap-5 h-full\">\n        <div className=\"col-span-2\">\n          <div className=\"text-lg font-medium flex items-center gap-2\">\n            <IconBox size={20} stroke={1.5} />\n            {collection?.name}\n          </div>\n          <Info collection={collection} />\n          <RequestsNotLoaded collection={collection} />\n        </div>\n        <div className=\"col-span-3\">\n          <Docs collection={collection} />\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default Overview;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Presets/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  max-width: 800px;\n\n  .settings-label {\n    width: 110px;\n  }\n\n  .textbox {\n    border: 1px solid #ccc;\n    padding: 0.15rem 0.45rem;\n    box-shadow: none;\n    border-radius: 0px;\n    outline: none;\n    box-shadow: none;\n    transition: border-color ease-in-out 0.1s;\n    border-radius: 3px;\n    background-color: ${(props) => props.theme.input.bg};\n    border: 1px solid ${(props) => props.theme.input.border};\n\n    &:focus {\n      border: solid 1px ${(props) => props.theme.input.focusBorder} !important;\n      outline: none !important;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Presets/index.js",
    "content": "import React from 'react';\nimport { useDispatch } from 'react-redux';\nimport StyledWrapper from './StyledWrapper';\nimport { updateCollectionPresets } from 'providers/ReduxStore/slices/collections';\nimport { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';\nimport { get } from 'lodash';\nimport Button from 'ui/Button';\n\nconst PresetsSettings = ({ collection }) => {\n  const dispatch = useDispatch();\n  const initialPresets = { requestType: 'http', requestUrl: '' };\n\n  // Get presets from draft.brunoConfig if it exists, otherwise from brunoConfig\n  const currentPresets = collection.draft?.brunoConfig\n    ? get(collection, 'draft.brunoConfig.presets', initialPresets)\n    : get(collection, 'brunoConfig.presets', initialPresets);\n\n  // Helper to update presets config\n  const updatePresets = (updates) => {\n    const updatedPresets = { ...currentPresets, ...updates };\n    dispatch(updateCollectionPresets({\n      collectionUid: collection.uid,\n      presets: updatedPresets\n    }));\n  };\n\n  const handleSave = () => dispatch(saveCollectionSettings(collection.uid));\n\n  const handleRequestTypeChange = (e) => {\n    updatePresets({ requestType: e.target.value });\n  };\n\n  const handleRequestUrlChange = (e) => {\n    updatePresets({ requestUrl: e.target.value });\n  };\n\n  return (\n    <StyledWrapper className=\"h-full w-full\">\n      <div className=\"text-xs mb-4 text-muted\">\n        These presets will be used as the default values for new requests in this collection.\n      </div>\n      <div className=\"bruno-form\">\n        <div className=\"mb-3 flex items-center\">\n          <label className=\"settings-label flex items-center\" htmlFor=\"http\">\n            Request Type\n          </label>\n          <div className=\"flex items-center\">\n            <input\n              id=\"http\"\n              className=\"cursor-pointer\"\n              type=\"radio\"\n              name=\"requestType\"\n              onChange={handleRequestTypeChange}\n              value=\"http\"\n              checked={(currentPresets.requestType || 'http') === 'http'}\n            />\n            <label htmlFor=\"http\" className=\"ml-1 cursor-pointer select-none\">\n              HTTP\n            </label>\n\n            <input\n              id=\"graphql\"\n              className=\"ml-4 cursor-pointer\"\n              type=\"radio\"\n              name=\"requestType\"\n              onChange={handleRequestTypeChange}\n              value=\"graphql\"\n              checked={(currentPresets.requestType || 'http') === 'graphql'}\n            />\n            <label htmlFor=\"graphql\" className=\"ml-1 cursor-pointer select-none\">\n              GraphQL\n            </label>\n\n            <input\n              id=\"grpc\"\n              className=\"ml-4 cursor-pointer\"\n              type=\"radio\"\n              name=\"requestType\"\n              onChange={handleRequestTypeChange}\n              value=\"grpc\"\n              checked={(currentPresets.requestType || 'http') === 'grpc'}\n            />\n            <label htmlFor=\"grpc\" className=\"ml-1 cursor-pointer select-none\">\n              gRPC\n            </label>\n\n            <input\n              id=\"ws\"\n              className=\"ml-4 cursor-pointer\"\n              type=\"radio\"\n              name=\"requestType\"\n              onChange={handleRequestTypeChange}\n              value=\"ws\"\n              checked={(currentPresets.requestType || 'http') === 'ws'}\n            />\n            <label htmlFor=\"ws\" className=\"ml-1 cursor-pointer select-none\">\n              WebSocket\n            </label>\n          </div>\n        </div>\n        <div className=\"mb-3 flex items-center\">\n          <label className=\"settings-label\" htmlFor=\"request-url\">\n            Base URL\n          </label>\n          <div className=\"flex items-center w-full\">\n            <div className=\"flex items-center flex-grow input-container h-full\">\n              <input\n                id=\"request-url\"\n                type=\"text\"\n                name=\"requestUrl\"\n                placeholder=\"Request URL\"\n                className=\"block textbox\"\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n                onChange={handleRequestUrlChange}\n                value={currentPresets.requestUrl || ''}\n                style={{ width: '100%' }}\n              />\n            </div>\n          </div>\n        </div>\n\n        <div className=\"mt-6\">\n          <Button type=\"button\" size=\"sm\" onClick={handleSave}>\n            Save\n          </Button>\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default PresetsSettings;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Protobuf/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .available-certificates {\n    background-color: ${(props) => props.theme.requestTabPanel.url.bg};\n\n    button.remove-certificate {\n      color: ${(props) => props.theme.colors.text.danger};\n    }\n  }\n\n  /* Section labels */\n  label {\n    color: ${(props) => props.theme.text};\n  }\n\n  /* Tooltip icon */\n  .tooltip-icon {\n    color: ${(props) => props.theme.colors.text.muted};\n    cursor: pointer;\n  }\n\n  /* Error messages */\n  .error-message {\n    color: ${(props) => props.theme.colors.text.danger};\n    background-color: ${(props) => props.theme.bg};\n    border-radius: ${(props) => props.theme.border.radius.base};\n  }\n\n  /* Tables */\n  table {\n    width: 100%;\n    border-collapse: collapse;\n\n    thead {\n      th {\n        text-align: left;\n        font-size: ${(props) => props.theme.font.size.xs};\n        font-weight: 500;\n        text-transform: uppercase;\n        letter-spacing: 0.05em;\n        color: ${(props) => props.theme.table.thead.color};\n        border: 1px solid ${(props) => props.theme.table.border};\n        padding: 0.5rem 0.75rem;\n\n        &.text-right {\n          text-align: right;\n        }\n      }\n    }\n\n    tbody {\n      td {\n        border: 1px solid ${(props) => props.theme.table.border};\n        padding: 0.5rem 0.75rem;\n\n        &.text-center {\n          text-align: center;\n        }\n\n        &.text-right {\n          text-align: right;\n        }\n      }\n    }\n  }\n\n  /* File/Directory icons */\n  .file-icon,\n  .folder-icon {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  /* File/Directory names */\n  .file-name,\n  .directory-name {\n    font-weight: 500;\n    color: ${(props) => props.theme.text};\n  }\n\n  /* Path text */\n  .path-text {\n    font-size: ${(props) => props.theme.font.size.xs};\n    color: ${(props) => props.theme.colors.text.muted};\n    font-family: monospace;\n  }\n\n  /* Empty state */\n  .empty-state {\n    .empty-icon {\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n\n    .empty-text {\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n  }\n\n  /* Invalid file indicator */\n  .invalid-indicator {\n    color: ${(props) => props.theme.colors.text.danger};\n  }\n\n  /* Action buttons */\n  .action-button {\n    padding: 0.25rem;\n    border-radius: ${(props) => props.theme.border.radius.base};\n    transition: all 0.2s;\n\n    &.replace-button {\n      color: ${(props) => props.theme.colors.text.danger};\n\n      &:hover {\n        color: ${(props) => props.theme.colors.text.danger};\n        background-color: ${(props) => props.theme.colors.bg.danger}20;\n      }\n    }\n\n    &.remove-button {\n      color: ${(props) => props.theme.colors.text.muted};\n\n      &:hover {\n        color: ${(props) => props.theme.text};\n        background-color: ${(props) => props.theme.dropdown.hoverBg};\n      }\n    }\n  }\n\n  /* Checkbox */\n  input[type='checkbox'] {\n    cursor: pointer;\n    accent-color: ${(props) => props.theme.colors.accent};\n    border-color: ${(props) => props.theme.table.border};\n\n    &:focus {\n      outline: none;\n      border-color: ${(props) => props.theme.primary.solid};\n    }\n  }\n\n  /* Add button */\n  .btn-add-param {\n    color: ${(props) => props.theme.textLink};\n    padding-right: 0.5rem;\n    padding-top: 0.75rem;\n    padding-bottom: 0.75rem;\n    margin-top: 0.5rem;\n    user-select: none;\n    cursor: pointer;\n    transition: color 0.2s;\n\n    &:hover {\n      color: ${(props) => props.theme.primary.solid};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Protobuf/index.js",
    "content": "import React, { useRef } from 'react';\nimport { useDispatch } from 'react-redux';\nimport StyledWrapper from './StyledWrapper';\nimport {\n  IconTrash,\n  IconFile,\n  IconFileImport,\n  IconAlertCircle,\n  IconFolder\n} from '@tabler/icons';\nimport { getBasename } from 'utils/common/path';\nimport { Tooltip } from 'react-tooltip';\nimport useProtoFileManagement from '../../../hooks/useProtoFileManagement';\nimport { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';\nimport Button from 'ui/Button';\n\nconst ProtobufSettings = ({ collection }) => {\n  const dispatch = useDispatch();\n  const {\n    protoFiles,\n    importPaths,\n    addProtoFileToCollection,\n    addImportPathToCollection,\n    toggleImportPath,\n    browseForProtoFile,\n    browseForImportDirectory,\n    removeProtoFileFromCollection,\n    removeImportPathFromCollection,\n    replaceImportPathInCollection,\n    replaceProtoFileInCollection\n  } = useProtoFileManagement(collection);\n  const fileInputRef = useRef(null);\n\n  const handleSave = () => dispatch(saveCollectionSettings(collection.uid));\n\n  // Get file path using the ipcRenderer\n  const getProtoFile = async (event) => {\n    const files = event?.files;\n    if (files && files.length > 0) {\n      for (let i = 0; i < files.length; i++) {\n        const filePath = window?.ipcRenderer?.getFilePath(files[i]);\n        if (filePath) {\n          await addProtoFileToCollection(filePath);\n        }\n      }\n      // Reset the file input\n      if (fileInputRef.current) {\n        fileInputRef.current.value = '';\n      }\n    }\n  };\n\n  const handleRemoveProtoFile = async (index) => {\n    await removeProtoFileFromCollection(index);\n  };\n\n  const handleBrowseClick = () => {\n    if (fileInputRef.current) {\n      fileInputRef.current.click();\n    }\n  };\n\n  const handleReplaceProtoFile = async (index) => {\n    const result = await browseForProtoFile();\n    if (result.success) {\n      await replaceProtoFileInCollection(index, result.filePath);\n    }\n  };\n\n  const handleReplaceImportPath = async (index) => {\n    const result = await browseForImportDirectory();\n    if (result.success) {\n      await replaceImportPathInCollection(index, result.directoryPath);\n    }\n  };\n\n  const handleFileInputChange = (e) => {\n    getProtoFile(e.target);\n  };\n\n  const getImportPath = async () => {\n    const result = await browseForImportDirectory();\n    if (result.success) {\n      await addImportPathToCollection(result.directoryPath);\n    }\n  };\n\n  const handleRemoveImportPath = async (index) => {\n    await removeImportPathFromCollection(index);\n  };\n\n  const handleToggleImportPath = async (index) => {\n    await toggleImportPath(index);\n  };\n\n  const handleBrowseImportPathClick = () => {\n    getImportPath();\n  };\n\n  return (\n    <StyledWrapper className=\"h-full w-full\">\n      {/* Hidden file input for file selection */}\n      <input\n        type=\"file\"\n        ref={fileInputRef}\n        style={{ display: 'none' }}\n        accept=\".proto\"\n        multiple\n        onChange={handleFileInputChange}\n      />\n\n      {/* Proto Files Section */}\n      <div className=\"mb-6\" data-testid=\"protobuf-proto-files-section\">\n        <div className=\"flex items-center justify-between mb-3\">\n          <div className=\"flex items-center\">\n            <label className=\"flex items-center\" htmlFor=\"protoFiles\">\n              Proto Files (\n              {protoFiles.length}\n              )\n              <span id=\"proto-files-tooltip\" className=\"ml-2\">\n                <IconAlertCircle size={16} className=\"tooltip-icon\" />\n              </span>\n              <Tooltip\n                anchorId=\"proto-files-tooltip\"\n                className=\"tooltip-mod font-normal\"\n                html=\"Keep your proto files within the collection folder or the corresponding git repository to ensure paths remain valid when sharing the collection.\"\n              />\n            </label>\n          </div>\n        </div>\n\n        <div>\n          {protoFiles.some((file) => !file.exists) && (\n            <div className=\"error-message text-xs mb-2 flex items-center p-2\" data-testid=\"protobuf-invalid-files-message\">\n              <IconAlertCircle size={14} className=\"mr-1\" />\n              Some proto files cannot be found. Use the replace option to update their locations.\n            </div>\n          )}\n\n          <table className=\"w-full border-collapse\" data-testid=\"protobuf-proto-files-table\">\n            <thead>\n              <tr>\n                <th>\n                  File\n                </th>\n                <th>\n                  Path\n                </th>\n                <th className=\"text-right\">\n                  Actions\n                </th>\n              </tr>\n            </thead>\n            <tbody>\n              {protoFiles.length === 0 ? (\n                <tr>\n                  <td colSpan=\"3\" className=\"text-center\">\n                    <div className=\"empty-state flex flex-col items-center\">\n                      <IconFile size={24} className=\"empty-icon mb-2\" />\n                      <span className=\"empty-text\">No proto files added</span>\n                    </div>\n                  </td>\n                </tr>\n              ) : (\n                protoFiles.map((file, index) => {\n                  const isValid = file.exists;\n\n                  return (\n                    <tr key={index}>\n                      <td>\n                        <div className=\"flex items-center\">\n                          <IconFile size={16} className=\"file-icon mr-2\" />\n                          <span className=\"file-name\" data-testid=\"protobuf-proto-file-name\">\n                            {getBasename(collection.pathname, file.path)}\n                          </span>\n                          {!isValid && <IconAlertCircle size={12} className=\"invalid-indicator ml-2\" />}\n                        </div>\n                      </td>\n                      <td>\n                        <div className=\"path-text\">\n                          {file.path}\n                        </div>\n                      </td>\n                      <td className=\"text-right\">\n                        <div className=\"flex items-center justify-end space-x-1\">\n                          {!isValid && (\n                            <button\n                              type=\"button\"\n                              onClick={() => handleReplaceProtoFile(index)}\n                              className=\"action-button replace-button\"\n                              title=\"Replace file\"\n                            >\n                              <IconFileImport size={14} />\n                            </button>\n                          )}\n                          <button\n                            type=\"button\"\n                            onClick={() => handleRemoveProtoFile(index)}\n                            className=\"action-button remove-button\"\n                            title=\"Remove file\"\n                            data-testid=\"protobuf-remove-file-button\"\n                          >\n                            <IconTrash size={14} />\n                          </button>\n                        </div>\n                      </td>\n                    </tr>\n                  );\n                })\n              )}\n            </tbody>\n          </table>\n          <button type=\"button\" className=\"btn-add-param text-link pr-2 py-3 mt-2 select-none\" onClick={handleBrowseClick} data-testid=\"protobuf-add-file-button\">\n            + Add Proto File\n          </button>\n        </div>\n      </div>\n\n      {/* Import Paths Section */}\n      <div className=\"mb-6\" data-testid=\"protobuf-import-paths-section\">\n        <div className=\"flex items-center justify-between mb-3\">\n          <div className=\"flex items-center\">\n            <label className=\"flex items-center\" htmlFor=\"importPaths\">\n              Import Paths (\n              {importPaths.length}\n              )\n              <span id=\"import-paths-tooltip\" className=\"ml-2\">\n                <IconAlertCircle size={16} className=\"tooltip-icon\" />\n              </span>\n              <Tooltip\n                anchorId=\"import-paths-tooltip\"\n                className=\"tooltip-mod font-normal\"\n                html=\"Add directories that contain proto files to be imported. These paths help resolve import statements in your proto files.\"\n              />\n            </label>\n          </div>\n        </div>\n\n        <div>\n          {importPaths.some((path) => !path.exists) && (\n            <div className=\"error-message text-xs mb-2 flex items-center p-2\" data-testid=\"protobuf-invalid-import-paths-message\">\n              <IconAlertCircle size={14} className=\"mr-1\" />\n              Some import paths cannot be found at their specified locations.\n            </div>\n          )}\n\n          <table className=\"w-full border-collapse\" data-testid=\"protobuf-import-paths-table\">\n            <thead>\n              <tr>\n                <th>\n                </th>\n                <th>\n                  Directory\n                </th>\n                <th>\n                  Path\n                </th>\n                <th className=\"text-right\">\n                  Actions\n                </th>\n              </tr>\n            </thead>\n            <tbody>\n              {importPaths.length === 0 ? (\n                <tr>\n                  <td colSpan=\"4\" className=\"text-center\">\n                    <div className=\"empty-state flex flex-col items-center\">\n                      <IconFolder size={24} className=\"empty-icon mb-2\" />\n                      <span className=\"empty-text\">No import paths added</span>\n                    </div>\n                  </td>\n                </tr>\n              ) : (\n                importPaths.map((importPath, index) => {\n                  const isValid = importPath.exists;\n\n                  return (\n                    <tr key={index}>\n                      <td>\n                        <input\n                          type=\"checkbox\"\n                          checked={importPath.enabled}\n                          onChange={() => handleToggleImportPath(index)}\n                          className=\"h-4 w-4\"\n                          title={importPath.enabled ? 'Disable this import path' : 'Enable this import path'}\n                          data-testid=\"protobuf-import-path-checkbox\"\n                        />\n                      </td>\n                      <td>\n                        <div className=\"flex items-center\">\n                          <IconFolder size={16} className=\"folder-icon mr-2\" />\n                          <span className=\"directory-name\">\n                            {getBasename(collection.pathname, importPath.path)}\n                          </span>\n                          {!isValid && <IconAlertCircle size={12} className=\"invalid-indicator ml-2\" />}\n                        </div>\n                      </td>\n                      <td>\n                        <div className=\"path-text\">\n                          {importPath.path}\n                        </div>\n                      </td>\n                      <td className=\"text-right\">\n                        <div className=\"flex items-center justify-end space-x-1\">\n                          {!isValid && (\n                            <button\n                              type=\"button\"\n                              onClick={() => handleReplaceImportPath(index)}\n                              className=\"action-button replace-button\"\n                              title=\"Replace directory\"\n                            >\n                              <IconFileImport size={14} />\n                            </button>\n                          )}\n                          <button\n                            type=\"button\"\n                            onClick={() => handleRemoveImportPath(index)}\n                            className=\"action-button remove-button\"\n                            title=\"Remove import path\"\n                            data-testid=\"protobuf-remove-import-path-button\"\n                          >\n                            <IconTrash size={14} />\n                          </button>\n                        </div>\n                      </td>\n                    </tr>\n                  );\n                })\n              )}\n            </tbody>\n          </table>\n          <button type=\"button\" className=\"btn-add-param text-link pr-2 py-3 mt-2 select-none\" onClick={handleBrowseImportPathClick} data-testid=\"protobuf-add-import-path-button\">\n            + Add Import Path\n          </button>\n        </div>\n      </div>\n\n      <div className=\"mt-6\">\n        <Button type=\"button\" size=\"sm\" onClick={handleSave}>\n          Save\n        </Button>\n      </div>\n\n    </StyledWrapper>\n  );\n};\n\nexport default ProtobufSettings;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/ProxySettings/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .settings-label {\n    width: 80px;\n  }\n\n  .textbox {\n    border: 1px solid #ccc;\n    padding: 0.15rem 0.45rem;\n    box-shadow: none;\n    border-radius: 0px;\n    outline: none;\n    box-shadow: none;\n    transition: border-color ease-in-out 0.1s;\n    border-radius: 3px;\n    background-color: ${(props) => props.theme.input.bg};\n    border: 1px solid ${(props) => props.theme.input.border};\n\n    &:focus {\n      border: solid 1px ${(props) => props.theme.input.focusBorder} !important;\n      outline: none !important;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/ProxySettings/index.js",
    "content": "import React from 'react';\nimport InfoTip from 'components/InfoTip';\nimport StyledWrapper from './StyledWrapper';\nimport { IconEye, IconEyeOff } from '@tabler/icons';\nimport { useState } from 'react';\nimport { useDispatch } from 'react-redux';\nimport { updateCollectionProxy } from 'providers/ReduxStore/slices/collections';\nimport { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';\nimport { get } from 'lodash';\nimport toast from 'react-hot-toast';\nimport Button from 'ui/Button';\n\nconst ProxySettings = ({ collection }) => {\n  const dispatch = useDispatch();\n  const initialProxyConfig = {\n    inherit: true,\n    config: {\n      protocol: 'http',\n      hostname: '',\n      port: '',\n      auth: {\n        username: '',\n        password: ''\n      },\n      bypassProxy: ''\n    }\n  };\n\n  // Get proxy from draft.brunoConfig if it exists, otherwise from brunoConfig\n  const currentProxyConfig = collection.draft?.brunoConfig\n    ? get(collection, 'draft.brunoConfig.proxy', initialProxyConfig)\n    : get(collection, 'brunoConfig.proxy', initialProxyConfig);\n\n  const [passwordVisible, setPasswordVisible] = useState(false);\n\n  const validateHostnameOnChange = (hostname) => {\n    if (hostname && hostname.length > 1024) {\n      toast.error('Hostname must be less than 1024 characters');\n      return false;\n    }\n    return true;\n  };\n\n  const validatePortOnChange = (port) => {\n    if (!port || port === '') {\n      return true; // Allow empty port during typing\n    }\n    const portNum = Number(port);\n    if (isNaN(portNum)) {\n      toast.error('Port must be a valid number');\n      return false;\n    }\n    if (portNum < 1 || portNum > 65535) {\n      toast.error('Port must be between 1 and 65535');\n      return false;\n    }\n    return true;\n  };\n\n  const validateAuthUsernameOnChange = (username) => {\n    if (username && username.length > 1024) {\n      toast.error('Username must be less than 1024 characters');\n      return false;\n    }\n    return true;\n  };\n\n  const validateAuthPasswordOnChange = (password) => {\n    if (password && password.length > 1024) {\n      toast.error('Password must be less than 1024 characters');\n      return false;\n    }\n    return true;\n  };\n\n  const validateBypassProxyOnChange = (bypassProxy) => {\n    if (bypassProxy && bypassProxy.length > 1024) {\n      toast.error('Bypass proxy must be less than 1024 characters');\n      return false;\n    }\n    return true;\n  };\n\n  // Helper to update proxy config\n  const updateProxy = (updates) => {\n    const updatedProxy = { ...currentProxyConfig, ...updates };\n    dispatch(updateCollectionProxy({\n      collectionUid: collection.uid,\n      proxy: updatedProxy\n    }));\n  };\n\n  const handleSave = () => dispatch(saveCollectionSettings(collection.uid));\n\n  const handleEnabledChange = (e) => {\n    const value = e.target.value;\n    // Map UI values to new format\n    if (value === 'inherit') {\n      updateProxy({ disabled: false, inherit: true });\n    } else if (value === 'true') {\n      updateProxy({ disabled: false, inherit: false });\n    } else {\n      updateProxy({ disabled: true, inherit: false });\n    }\n  };\n\n  const handleProtocolChange = (e) => {\n    updateProxy({\n      config: {\n        ...currentProxyConfig.config,\n        protocol: e.target.value\n      }\n    });\n  };\n\n  const handleHostnameChange = (e) => {\n    const hostname = e.target.value;\n    if (validateHostnameOnChange(hostname)) {\n      updateProxy({\n        config: {\n          ...currentProxyConfig.config,\n          hostname\n        }\n      });\n    }\n  };\n\n  const handlePortChange = (e) => {\n    const port = e.target.value ? Number(e.target.value) : '';\n    if (validatePortOnChange(port)) {\n      updateProxy({\n        config: {\n          ...currentProxyConfig.config,\n          port\n        }\n      });\n    }\n  };\n\n  const handleAuthEnabledChange = (e) => {\n    updateProxy({\n      config: {\n        ...currentProxyConfig.config,\n        auth: {\n          ...currentProxyConfig.config.auth,\n          disabled: !e.target.checked\n        }\n      }\n    });\n  };\n\n  const handleAuthUsernameChange = (e) => {\n    const username = e.target.value;\n    if (validateAuthUsernameOnChange(username)) {\n      updateProxy({\n        config: {\n          ...currentProxyConfig.config,\n          auth: {\n            ...currentProxyConfig.config.auth,\n            username\n          }\n        }\n      });\n    }\n  };\n\n  const handleAuthPasswordChange = (e) => {\n    const password = e.target.value;\n    if (validateAuthPasswordOnChange(password)) {\n      updateProxy({\n        config: {\n          ...currentProxyConfig.config,\n          auth: {\n            ...currentProxyConfig.config.auth,\n            password\n          }\n        }\n      });\n    }\n  };\n\n  const handleBypassProxyChange = (e) => {\n    const bypassProxy = e.target.value;\n    if (validateBypassProxyOnChange(bypassProxy)) {\n      updateProxy({\n        config: {\n          ...currentProxyConfig.config,\n          bypassProxy\n        }\n      });\n    }\n  };\n\n  // Map new format to UI values\n  const disabled = currentProxyConfig.disabled || false;\n  const inherit = currentProxyConfig.inherit !== undefined ? currentProxyConfig.inherit : true;\n  const enabledValue = disabled ? 'false' : (inherit ? 'inherit' : 'true');\n\n  return (\n    <StyledWrapper className=\"h-full w-full\">\n      <div className=\"text-xs mb-4 text-muted\">Configure proxy settings for this collection.</div>\n      <div className=\"bruno-form\">\n        <div className=\"mb-3 flex items-center\">\n          <label className=\"settings-label flex items-center\" htmlFor=\"enabled\">\n            Config\n            <InfoTip infotipId=\"request-var\">\n              <div>\n                <ul>\n                  <li><span style={{ width: '50px', display: 'inline-block' }}>inherit</span> - inherit from global preferences</li>\n                  <li><span style={{ width: '50px', display: 'inline-block' }}>enabled</span> - use collection-specific proxy config</li>\n                  <li><span style={{ width: '50px', display: 'inline-block' }}>disabled</span> - disable proxy for this collection</li>\n                </ul>\n              </div>\n            </InfoTip>\n          </label>\n          <div className=\"flex items-center\">\n            <label className=\"flex items-center\">\n              <input\n                type=\"radio\"\n                name=\"enabled\"\n                value=\"inherit\"\n                checked={enabledValue === 'inherit'}\n                onChange={handleEnabledChange}\n                className=\"mr-1\"\n              />\n              inherit\n            </label>\n            <label className=\"flex items-center ml-4\">\n              <input\n                type=\"radio\"\n                name=\"enabled\"\n                value=\"true\"\n                checked={enabledValue === 'true'}\n                onChange={handleEnabledChange}\n                className=\"mr-1\"\n              />\n              enabled\n            </label>\n            <label className=\"flex items-center ml-4\">\n              <input\n                type=\"radio\"\n                name=\"enabled\"\n                value=\"false\"\n                checked={enabledValue === 'false'}\n                onChange={handleEnabledChange}\n                className=\"mr-1\"\n              />\n              disabled\n            </label>\n          </div>\n        </div>\n        {enabledValue === 'true' && (\n          <>\n            <div className=\"mb-3 flex items-center\">\n              <label className=\"settings-label\" htmlFor=\"protocol\">\n                Protocol\n              </label>\n              <div className=\"flex items-center\">\n                <label className=\"flex items-center\">\n                  <input\n                    type=\"radio\"\n                    name=\"protocol\"\n                    value=\"http\"\n                    checked={(currentProxyConfig.config?.protocol || 'http') === 'http'}\n                    onChange={handleProtocolChange}\n                    className=\"mr-1\"\n                  />\n                  HTTP\n                </label>\n                <label className=\"flex items-center ml-4\">\n                  <input\n                    type=\"radio\"\n                    name=\"protocol\"\n                    value=\"https\"\n                    checked={(currentProxyConfig.config?.protocol || 'http') === 'https'}\n                    onChange={handleProtocolChange}\n                    className=\"mr-1\"\n                  />\n                  HTTPS\n                </label>\n                <label className=\"flex items-center ml-4\">\n                  <input\n                    type=\"radio\"\n                    name=\"protocol\"\n                    value=\"socks4\"\n                    checked={(currentProxyConfig.config?.protocol || 'http') === 'socks4'}\n                    onChange={handleProtocolChange}\n                    className=\"mr-1\"\n                  />\n                  SOCKS4\n                </label>\n                <label className=\"flex items-center ml-4\">\n                  <input\n                    type=\"radio\"\n                    name=\"protocol\"\n                    value=\"socks5\"\n                    checked={(currentProxyConfig.config?.protocol || 'http') === 'socks5'}\n                    onChange={handleProtocolChange}\n                    className=\"mr-1\"\n                  />\n                  SOCKS5\n                </label>\n              </div>\n            </div>\n            <div className=\"mb-3 flex items-center\">\n              <label className=\"settings-label\" htmlFor=\"hostname\">\n                Hostname\n              </label>\n              <input\n                id=\"hostname\"\n                type=\"text\"\n                name=\"hostname\"\n                className=\"block textbox\"\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n                onChange={handleHostnameChange}\n                value={currentProxyConfig.config?.hostname || ''}\n              />\n            </div>\n            <div className=\"mb-3 flex items-center\">\n              <label className=\"settings-label\" htmlFor=\"port\">\n                Port\n              </label>\n              <input\n                id=\"port\"\n                type=\"number\"\n                name=\"port\"\n                className=\"block textbox\"\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n                onChange={handlePortChange}\n                value={currentProxyConfig.config?.port || ''}\n              />\n            </div>\n            <div className=\"mb-3 flex items-center\">\n              <label className=\"settings-label\" htmlFor=\"auth.disabled\">\n                Auth\n              </label>\n              <input\n                type=\"checkbox\"\n                name=\"auth.disabled\"\n                checked={!currentProxyConfig.config?.auth?.disabled}\n                onChange={handleAuthEnabledChange}\n              />\n            </div>\n            <div>\n              <div className=\"mb-3 flex items-center\">\n                <label className=\"settings-label\" htmlFor=\"auth.username\">\n                  Username\n                </label>\n                <input\n                  id=\"auth.username\"\n                  type=\"text\"\n                  name=\"auth.username\"\n                  className=\"block textbox\"\n                  autoComplete=\"off\"\n                  autoCorrect=\"off\"\n                  autoCapitalize=\"off\"\n                  spellCheck=\"false\"\n                  value={currentProxyConfig.config?.auth?.username || ''}\n                  onChange={handleAuthUsernameChange}\n                />\n              </div>\n              <div className=\"mb-3 flex items-center\">\n                <label className=\"settings-label\" htmlFor=\"auth.password\">\n                  Password\n                </label>\n                <div className=\"textbox flex flex-row items-center w-[13.2rem] h-[1.70rem] relative\">\n                  <input\n                    id=\"auth.password\"\n                    type={passwordVisible ? 'text' : 'password'}\n                    name=\"auth.password\"\n                    className=\"outline-none bg-transparent w-[10.5rem]\"\n                    autoComplete=\"off\"\n                    autoCorrect=\"off\"\n                    autoCapitalize=\"off\"\n                    spellCheck=\"false\"\n                    value={currentProxyConfig.config?.auth?.password || ''}\n                    onChange={handleAuthPasswordChange}\n                  />\n                  <button\n                    type=\"button\"\n                    className=\"btn btn-sm absolute right-0\"\n                    onClick={() => setPasswordVisible(!passwordVisible)}\n                  >\n                    {passwordVisible ? <IconEyeOff size={18} strokeWidth={1.5} /> : <IconEye size={18} strokeWidth={1.5} />}\n                  </button>\n                </div>\n              </div>\n            </div>\n            <div className=\"mb-3 flex items-center\">\n              <label className=\"settings-label\" htmlFor=\"bypassProxy\">\n                Proxy Bypass\n              </label>\n              <input\n                id=\"bypassProxy\"\n                type=\"text\"\n                name=\"bypassProxy\"\n                className=\"block textbox\"\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n                onChange={handleBypassProxyChange}\n                value={currentProxyConfig.config?.bypassProxy || ''}\n              />\n            </div>\n          </>\n        )}\n        <div className=\"mt-6\">\n          <Button type=\"submit\" size=\"sm\" onClick={handleSave}>\n            Save\n          </Button>\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default ProxySettings;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Script/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  max-width: 800px;\n\n  div.CodeMirror {\n    height: inherit;\n  }\n\n  div.title {\n    color: ${(props) => props.theme.colors.text.subtext0};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Script/index.js",
    "content": "import React, { useState, useEffect, useRef } from 'react';\nimport get from 'lodash/get';\nimport { useDispatch, useSelector } from 'react-redux';\nimport CodeEditor from 'components/CodeEditor';\nimport { updateCollectionRequestScript, updateCollectionResponseScript } from 'providers/ReduxStore/slices/collections';\nimport { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';\nimport { useTheme } from 'providers/Theme';\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs';\nimport StatusDot from 'components/StatusDot';\nimport { flattenItems, isItemARequest } from 'utils/collections';\nimport StyledWrapper from './StyledWrapper';\nimport Button from 'ui/Button';\n\nconst Script = ({ collection }) => {\n  const dispatch = useDispatch();\n  const preRequestEditorRef = useRef(null);\n  const postResponseEditorRef = useRef(null);\n  const requestScript = collection.draft?.root ? get(collection, 'draft.root.request.script.req', '') : get(collection, 'root.request.script.req', '');\n  const responseScript = collection.draft?.root ? get(collection, 'draft.root.request.script.res', '') : get(collection, 'root.request.script.res', '');\n\n  // Default to post-response if pre-request script is empty\n  const getInitialTab = () => {\n    const hasPreRequestScript = requestScript && requestScript.trim().length > 0;\n    return hasPreRequestScript ? 'pre-request' : 'post-response';\n  };\n\n  const [activeTab, setActiveTab] = useState(getInitialTab);\n  const prevCollectionUidRef = useRef(collection.uid);\n\n  const { displayedTheme } = useTheme();\n  const preferences = useSelector((state) => state.app.preferences);\n\n  // Update active tab only when switching to a different collection\n  useEffect(() => {\n    if (prevCollectionUidRef.current !== collection.uid) {\n      prevCollectionUidRef.current = collection.uid;\n      const hasPreRequestScript = requestScript && requestScript.trim().length > 0;\n      setActiveTab(hasPreRequestScript ? 'pre-request' : 'post-response');\n    }\n  }, [collection.uid, requestScript]);\n\n  // Refresh CodeMirror when tab becomes visible\n  useEffect(() => {\n    const timer = setTimeout(() => {\n      if (activeTab === 'pre-request' && preRequestEditorRef.current?.editor) {\n        preRequestEditorRef.current.editor.refresh();\n      } else if (activeTab === 'post-response' && postResponseEditorRef.current?.editor) {\n        postResponseEditorRef.current.editor.refresh();\n      }\n    }, 0);\n\n    return () => clearTimeout(timer);\n  }, [activeTab]);\n\n  const onRequestScriptEdit = (value) => {\n    dispatch(\n      updateCollectionRequestScript({\n        script: value,\n        collectionUid: collection.uid\n      })\n    );\n  };\n\n  const onResponseScriptEdit = (value) => {\n    dispatch(\n      updateCollectionResponseScript({\n        script: value,\n        collectionUid: collection.uid\n      })\n    );\n  };\n\n  const handleSave = () => {\n    dispatch(saveCollectionSettings(collection.uid));\n  };\n\n  const items = flattenItems(collection.items || []);\n  const hasPreRequestScriptError = items.some((i) => isItemARequest(i) && i.preRequestScriptErrorMessage);\n  const hasPostResponseScriptError = items.some((i) => isItemARequest(i) && i.postResponseScriptErrorMessage);\n\n  return (\n    <StyledWrapper className=\"w-full flex flex-col h-full\">\n      <div className=\"text-xs mb-4 text-muted\">\n        Write pre and post-request scripts that will run before and after any request in this collection is sent.\n      </div>\n\n      <Tabs value={activeTab} onValueChange={setActiveTab}>\n        <TabsList>\n          <TabsTrigger value=\"pre-request\">\n            Pre Request\n            {requestScript && requestScript.trim().length > 0 && (\n              <StatusDot type={hasPreRequestScriptError ? 'error' : 'default'} />\n            )}\n          </TabsTrigger>\n          <TabsTrigger value=\"post-response\">\n            Post Response\n            {responseScript && responseScript.trim().length > 0 && (\n              <StatusDot type={hasPostResponseScriptError ? 'error' : 'default'} />\n            )}\n          </TabsTrigger>\n        </TabsList>\n\n        <TabsContent value=\"pre-request\" className=\"mt-2\">\n          <CodeEditor\n            ref={preRequestEditorRef}\n            collection={collection}\n            value={requestScript || ''}\n            theme={displayedTheme}\n            onEdit={onRequestScriptEdit}\n            mode=\"javascript\"\n            onSave={handleSave}\n            font={get(preferences, 'font.codeFont', 'default')}\n            fontSize={get(preferences, 'font.codeFontSize')}\n            showHintsFor={['req', 'bru']}\n          />\n        </TabsContent>\n\n        <TabsContent value=\"post-response\" className=\"mt-2\">\n          <CodeEditor\n            ref={postResponseEditorRef}\n            collection={collection}\n            value={responseScript || ''}\n            theme={displayedTheme}\n            onEdit={onResponseScriptEdit}\n            mode=\"javascript\"\n            onSave={handleSave}\n            font={get(preferences, 'font.codeFont', 'default')}\n            fontSize={get(preferences, 'font.codeFontSize')}\n            showHintsFor={['req', 'res', 'bru']}\n          />\n        </TabsContent>\n      </Tabs>\n\n      <div className=\"mt-12\">\n        <Button type=\"submit\" size=\"sm\" onClick={handleSave}>\n          Save\n        </Button>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Script;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  div.tabs {\n    div.tab {\n      padding: 6px 0px;\n      border: none;\n      border-bottom: solid 2px transparent;\n      margin-right: ${(props) => props.theme.tabs.marginRight};\n      color: ${(props) => props.theme.colors.text.subtext0};\n      cursor: pointer;\n\n      &:focus,\n      &:active,\n      &:focus-within,\n      &:focus-visible,\n      &:target {\n        outline: none !important;\n        box-shadow: none !important;\n      }\n\n      &:hover {\n        color: ${(props) => props.theme.tabs.active.color} !important;\n      }\n\n      &.active {\n        font-weight: ${(props) => props.theme.tabs.active.fontWeight} !important;\n        color: ${(props) => props.theme.tabs.active.color} !important;\n        border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;\n      }\n    }\n  }\n  table {\n    thead,\n    td {\n      border: 1px solid ${(props) => props.theme.table.border};\n\n      li {\n        background-color: ${(props) => props.theme.bg} !important;\n      }\n    }\n  }\n\n  .muted {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  input[type='radio'] {\n    cursor: pointer;\n    accent-color: ${(props) => props.theme.primary.solid};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Tests/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  max-width: 800px;\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Tests/index.js",
    "content": "import React from 'react';\nimport get from 'lodash/get';\nimport { useDispatch, useSelector } from 'react-redux';\nimport CodeEditor from 'components/CodeEditor';\nimport { updateCollectionTests } from 'providers/ReduxStore/slices/collections';\nimport { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';\nimport { useTheme } from 'providers/Theme';\nimport StyledWrapper from './StyledWrapper';\nimport Button from 'ui/Button';\n\nconst Tests = ({ collection }) => {\n  const dispatch = useDispatch();\n  const tests = collection.draft?.root ? get(collection, 'draft.root.request.tests', '') : get(collection, 'root.request.tests', '');\n\n  const { displayedTheme } = useTheme();\n  const preferences = useSelector((state) => state.app.preferences);\n\n  const onEdit = (value) => {\n    dispatch(\n      updateCollectionTests({\n        tests: value,\n        collectionUid: collection.uid\n      })\n    );\n  };\n\n  const handleSave = () => dispatch(saveCollectionSettings(collection.uid));\n\n  return (\n    <StyledWrapper className=\"w-full flex flex-col h-full\">\n      <div className=\"text-xs mb-4 text-muted\">These tests will run any time a request in this collection is sent.</div>\n      <CodeEditor\n        collection={collection}\n        value={tests || ''}\n        theme={displayedTheme}\n        onEdit={onEdit}\n        mode=\"javascript\"\n        onSave={handleSave}\n        font={get(preferences, 'font.codeFont', 'default')}\n        fontSize={get(preferences, 'font.codeFontSize')}\n        showHintsFor={['req', 'res', 'bru']}\n      />\n\n      <div className=\"mt-6\">\n        <Button type=\"submit\" size=\"sm\" onClick={handleSave}>\n          Save\n        </Button>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Tests;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Vars/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  max-width: 800px;\n\n  div.title {\n    color: ${(props) => props.theme.colors.text.subtext0};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  table {\n    width: 100%;\n    border-collapse: collapse;\n    font-weight: 500;\n    table-layout: fixed;\n\n    thead,\n    td {\n      border: 1px solid ${(props) => props.theme.table.border};\n    }\n\n    thead {\n      color: ${(props) => props.theme.table.thead.color};\n      font-size: ${(props) => props.theme.font.size.base};\n      user-select: none;\n    }\n    td {\n      padding: 6px 10px;\n\n      &:nth-child(1) {\n        width: 30%;\n      }\n\n      &:nth-child(3) {\n        width: 70px;\n      }\n    }\n  }\n\n  .btn-add-var {\n    font-size: ${(props) => props.theme.font.size.base};\n  }\n\n  input[type='text'] {\n    width: 100%;\n    border: solid 1px transparent;\n    outline: none !important;\n    background-color: inherit;\n\n    &:focus {\n      outline: none !important;\n      border: solid 1px transparent;\n    }\n  }\n\n  input[type='checkbox'] {\n    cursor: pointer;\n    position: relative;\n    top: 1px;\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Vars/VarsTable/index.js",
    "content": "import React, { useCallback } from 'react';\nimport { useDispatch } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';\nimport MultiLineEditor from 'components/MultiLineEditor';\nimport InfoTip from 'components/InfoTip';\nimport EditableTable from 'components/EditableTable';\nimport StyledWrapper from './StyledWrapper';\nimport toast from 'react-hot-toast';\nimport { variableNameRegex } from 'utils/common/regex';\nimport { setCollectionVars } from 'providers/ReduxStore/slices/collections/index';\n\nconst VarsTable = ({ collection, vars, varType }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n\n  const onSave = () => dispatch(saveCollectionSettings(collection.uid));\n\n  const handleVarsChange = useCallback((updatedVars) => {\n    dispatch(setCollectionVars({ collectionUid: collection.uid, vars: updatedVars, type: varType }));\n  }, [dispatch, collection.uid, varType]);\n\n  const getRowError = useCallback((row, index, key) => {\n    if (key !== 'name') return null;\n    if (!row.name || row.name.trim() === '') return null;\n    if (!variableNameRegex.test(row.name)) {\n      return 'Variable contains invalid characters. Must only contain alphanumeric characters, \"-\", \"_\", \".\"';\n    }\n    return null;\n  }, []);\n\n  const columns = [\n    {\n      key: 'name',\n      name: 'Name',\n      isKeyField: true,\n      placeholder: 'Name',\n      width: '40%'\n    },\n    {\n      key: 'value',\n      name: varType === 'request' ? 'Value' : (\n        <div className=\"flex items-center\">\n          <span>Expr</span>\n          <InfoTip content=\"You can write any valid JS Template Literal here\" infotipId={`collection-${varType}-var`} />\n        </div>\n      ),\n      placeholder: varType === 'request' ? 'Value' : 'Expr',\n      render: ({ value, onChange }) => (\n        <MultiLineEditor\n          value={value || ''}\n          theme={storedTheme}\n          onSave={onSave}\n          onChange={onChange}\n          collection={collection}\n          placeholder={!value ? (varType === 'request' ? 'Value' : 'Expr') : ''}\n        />\n      )\n    }\n  ];\n\n  const defaultRow = {\n    name: '',\n    value: '',\n    ...(varType === 'response' ? { local: false } : {})\n  };\n\n  return (\n    <StyledWrapper className=\"w-full\">\n      <EditableTable\n        columns={columns}\n        rows={vars}\n        onChange={handleVarsChange}\n        defaultRow={defaultRow}\n        getRowError={getRowError}\n      />\n    </StyledWrapper>\n  );\n};\n\nexport default VarsTable;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/Vars/index.js",
    "content": "import React from 'react';\nimport get from 'lodash/get';\nimport VarsTable from './VarsTable';\nimport StyledWrapper from './StyledWrapper';\nimport { saveCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';\nimport { useDispatch } from 'react-redux';\nimport Button from 'ui/Button';\n\nconst Vars = ({ collection }) => {\n  const dispatch = useDispatch();\n  const requestVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.req', []) : get(collection, 'root.request.vars.req', []);\n  const responseVars = collection.draft?.root ? get(collection, 'draft.root.request.vars.res', []) : get(collection, 'root.request.vars.res', []);\n  const handleSave = () => dispatch(saveCollectionSettings(collection.uid));\n\n  return (\n    <StyledWrapper className=\"w-full flex flex-col\">\n      <div className=\"flex-1\">\n        <div className=\"mb-3 title text-xs\">Pre Request</div>\n        <VarsTable collection={collection} vars={requestVars} varType=\"request\" />\n      </div>\n      <div className=\"flex-1\">\n        <div className=\"mt-3 mb-3 title text-xs\">Post Response</div>\n        <VarsTable collection={collection} vars={responseVars} varType=\"response\" />\n      </div>\n      <div className=\"mt-6\">\n        <Button type=\"submit\" size=\"sm\" onClick={handleSave}>\n          Save\n        </Button>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Vars;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CollectionSettings/index.js",
    "content": "import React from 'react';\nimport classnames from 'classnames';\nimport get from 'lodash/get';\nimport { updateSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';\nimport { useDispatch } from 'react-redux';\nimport ProxySettings from './ProxySettings';\nimport ClientCertSettings from './ClientCertSettings';\nimport Headers from './Headers';\nimport Auth from './Auth';\nimport Script from './Script';\nimport Test from './Tests';\nimport Presets from './Presets';\nimport Protobuf from './Protobuf';\nimport StyledWrapper from './StyledWrapper';\nimport Vars from './Vars/index';\nimport StatusDot from 'components/StatusDot';\nimport Overview from './Overview/index';\n\nconst CollectionSettings = ({ collection }) => {\n  const dispatch = useDispatch();\n  const tab = collection.settingsSelectedTab;\n  const setTab = (tab) => {\n    dispatch(\n      updateSettingsSelectedTab({\n        collectionUid: collection.uid,\n        tab\n      })\n    );\n  };\n\n  const root = collection?.draft?.root || collection?.root;\n  const hasScripts = root?.request?.script?.res || root?.request?.script?.req;\n  const hasTests = root?.request?.tests;\n  const hasDocs = root?.docs;\n\n  const headers = collection.draft?.root\n    ? get(collection, 'draft.root.request.headers', [])\n    : get(collection, 'root.request.headers', []);\n  const activeHeadersCount = headers.filter((header) => header.enabled).length;\n\n  const requestVars = collection.draft?.root\n    ? get(collection, 'draft.root.request.vars.req', [])\n    : get(collection, 'root.request.vars.req', []);\n  const responseVars = collection.draft?.root\n    ? get(collection, 'draft.root.request.vars.res', [])\n    : get(collection, 'root.request.vars.res', []);\n  const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;\n  const authMode\n    = (collection.draft?.root ? get(collection, 'draft.root.request.auth', {}) : get(collection, 'root.request.auth', {}))\n      .mode || 'none';\n\n  const proxyConfig = collection.draft?.brunoConfig\n    ? get(collection, 'draft.brunoConfig.proxy', {})\n    : get(collection, 'brunoConfig.proxy', {});\n  const proxyEnabled = proxyConfig.hostname ? true : false;\n  const clientCertConfig = collection.draft?.brunoConfig\n    ? get(collection, 'draft.brunoConfig.clientCertificates.certs', [])\n    : get(collection, 'brunoConfig.clientCertificates.certs', []);\n  const protobufConfig = collection.draft?.brunoConfig\n    ? get(collection, 'draft.brunoConfig.protobuf', {})\n    : get(collection, 'brunoConfig.protobuf', {});\n  const presets = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.presets', {}) : get(collection, 'brunoConfig.presets', {});\n  const hasPresets = presets && presets.requestUrl !== '';\n\n  const getTabPanel = (tab) => {\n    switch (tab) {\n      case 'overview': {\n        return <Overview collection={collection} />;\n      }\n      case 'headers': {\n        return <Headers collection={collection} />;\n      }\n      case 'vars': {\n        return <Vars collection={collection} />;\n      }\n      case 'auth': {\n        return <Auth collection={collection} />;\n      }\n      case 'script': {\n        return <Script collection={collection} />;\n      }\n      case 'tests': {\n        return <Test collection={collection} />;\n      }\n      case 'presets': {\n        return <Presets collection={collection} />;\n      }\n      case 'proxy': {\n        return <ProxySettings collection={collection} />;\n      }\n      case 'clientCert': {\n        return <ClientCertSettings collection={collection} />;\n      }\n      case 'protobuf': {\n        return <Protobuf collection={collection} />;\n      }\n    }\n  };\n\n  const getTabClassname = (tabName) => {\n    return classnames(`tab select-none ${tabName}`, {\n      active: tabName === tab\n    });\n  };\n\n  return (\n    <StyledWrapper className=\"flex flex-col h-full relative px-4 py-4 overflow-hidden\">\n      <div className=\"flex flex-wrap items-center tabs\" role=\"tablist\">\n        <div className={getTabClassname('overview')} role=\"tab\" onClick={() => setTab('overview')}>\n          Overview\n        </div>\n        <div className={getTabClassname('headers')} role=\"tab\" onClick={() => setTab('headers')}>\n          Headers\n          {activeHeadersCount > 0 && <sup className=\"ml-1 font-medium\">{activeHeadersCount}</sup>}\n        </div>\n        <div className={getTabClassname('vars')} role=\"tab\" onClick={() => setTab('vars')}>\n          Vars\n          {activeVarsCount > 0 && <sup className=\"ml-1 font-medium\">{activeVarsCount}</sup>}\n        </div>\n        <div className={getTabClassname('auth')} role=\"tab\" onClick={() => setTab('auth')}>\n          Auth\n          {authMode !== 'none' && <StatusDot />}\n        </div>\n        <div className={getTabClassname('script')} role=\"tab\" onClick={() => setTab('script')}>\n          Script\n          {hasScripts && <StatusDot />}\n        </div>\n        <div className={getTabClassname('tests')} role=\"tab\" onClick={() => setTab('tests')}>\n          Tests\n          {hasTests && <StatusDot />}\n        </div>\n        <div className={getTabClassname('presets')} role=\"tab\" onClick={() => setTab('presets')}>\n          Presets\n          {hasPresets && <StatusDot />}\n        </div>\n        <div className={getTabClassname('proxy')} role=\"tab\" onClick={() => setTab('proxy')}>\n          Proxy\n          {Object.keys(proxyConfig).length > 0 && proxyEnabled && <StatusDot />}\n        </div>\n        <div className={getTabClassname('clientCert')} role=\"tab\" onClick={() => setTab('clientCert')}>\n          Client Certificates\n          {clientCertConfig.length > 0 && <StatusDot />}\n        </div>\n        <div className={getTabClassname('protobuf')} role=\"tab\" onClick={() => setTab('protobuf')}>\n          Protobuf\n          {protobufConfig.protoFiles && protobufConfig.protoFiles.length > 0 && <StatusDot />}\n        </div>\n      </div>\n      <section className=\"mt-4 h-full overflow-auto\">{getTabPanel(tab)}</section>\n    </StyledWrapper>\n  );\n};\n\nexport default CollectionSettings;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ColorBadge/index.js",
    "content": "import React from 'react';\nimport { useTheme } from 'providers/Theme';\n\nconst ColorBadge = ({ color, size = 10 }) => {\n  const sizeValue = typeof size === 'string' ? size : `${size}px`;\n  const { theme } = useTheme();\n\n  return (\n    <div\n      className=\"flex-shrink-0 rounded-full\"\n      style={{\n        width: sizeValue,\n        height: sizeValue,\n        backgroundColor: color || 'transparent'\n      }}\n    />\n  );\n};\n\nexport default ColorBadge;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ColorPicker/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  \n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ColorPicker/index.js",
    "content": "import React, { useState, useEffect, useRef } from 'react';\nimport { IconBan, IconBrush } from '@tabler/icons';\nimport Dropdown from 'components/Dropdown';\nimport ColorBadge from 'components/ColorBadge';\nimport StyledWrapper from './StyledWrapper';\nimport { parseToRgb, toColorString } from 'polished';\nimport ColorRangePicker from 'components/ColorRange/index';\n\nconst PRESET_COLORS = [\n  '#CE4F3B',\n  '#2E8A54',\n  '#346AB2',\n  '#C77A0F',\n  '#B83D7F',\n  '#8D44B2'\n];\n\nconst COLOR_RANGE_SEQUENCE = ['#D85D43', '#F4BB74', '#61DCB1', '#7EBDF2', '#D48ADE', '#B491E5'];\n\n/**\n * @param {string} hex\n * @returns {red:string,green:string,blue:string}\n */\nconst hexToRgb = (hex) => {\n  try {\n    return parseToRgb(hex);\n  } catch (err) {\n    return { red: 0, green: 0, blue: 0 };\n  }\n};\n\nconst rgbToHex = (r, g, b) => {\n  return toColorString({ red: Math.round(r), green: Math.round(g), blue: Math.round(b) });\n};\n\nconst interpolateColor = (position) => {\n  const numColors = COLOR_RANGE_SEQUENCE.length;\n  const scaledPos = (position / 100) * (numColors - 1);\n  const index = Math.floor(scaledPos);\n  const fraction = scaledPos - index;\n\n  if (index >= numColors - 1) {\n    return COLOR_RANGE_SEQUENCE[numColors - 1];\n  }\n\n  const color1 = hexToRgb(COLOR_RANGE_SEQUENCE[index]);\n  const color2 = hexToRgb(COLOR_RANGE_SEQUENCE[index + 1]);\n\n  const r = color1.red + (color2.red - color1.red) * fraction;\n  const g = color1.green + (color2.green - color1.green) * fraction;\n  const b = color1.blue + (color2.blue - color1.blue) * fraction;\n\n  return rgbToHex(r, g, b);\n};\n\nconst findClosestPosition = (hex) => {\n  if (!hex) return 0;\n  const target = hexToRgb(hex);\n  let closestPos = 0;\n  let minDistance = Infinity;\n\n  for (let pos = 0; pos <= 100; pos++) {\n    const color = hexToRgb(interpolateColor(pos));\n    const distance = Math.sqrt(\n      Math.pow(target.red - color.red, 2) + Math.pow(target.green - color.green, 2) + Math.pow(target.blue - color.blue, 2)\n    );\n    if (distance < minDistance) {\n      minDistance = distance;\n      closestPos = pos;\n    }\n  }\n  return closestPos;\n};\n\nconst ColorPickerIcon = ({ color }) => {\n  if (color) {\n    return <ColorBadge color={color} size={8} />;\n  }\n  return <IconBrush size={14} strokeWidth={1.5} className=\"opacity-70\" />;\n};\n\nconst ColorPicker = ({ color, onChange, icon }) => {\n  const [sliderPosition, setSliderPosition] = useState(() =>\n    color && !PRESET_COLORS.includes(color) ? findClosestPosition(color) : 0\n  );\n  const [customColor, setCustomColor] = useState(() =>\n    color && !PRESET_COLORS.includes(color) ? color : COLOR_RANGE_SEQUENCE[0]\n  );\n  const pendingColorRef = useRef(customColor);\n\n  const handleColorSelect = (selectedColor) => {\n    onChange(selectedColor);\n  };\n\n  const handleSliderChange = (e) => {\n    const newPosition = parseInt(e.target.value, 10);\n    setSliderPosition(newPosition);\n    const newColor = interpolateColor(newPosition);\n    setCustomColor(newColor);\n    pendingColorRef.current = newColor;\n  };\n\n  const handleSliderEnd = () => {\n    onChange(pendingColorRef.current);\n  };\n\n  const defaultIcon = (\n    <div className=\"cursor-pointer flex items-center\" title=\"Change color\">\n      <ColorPickerIcon color={color} />\n    </div>\n  );\n\n  const colorPickerContent = (\n    <StyledWrapper>\n      <div className=\"p-2\">\n        <div className=\"flex flex-wrap gap-1.5 justify-between items-center\">\n          <div\n            className=\"w-5 h-5 cursor-pointer flex items-center justify-center transition-transform duration-100 hover:scale-110\"\n            onClick={() => handleColorSelect(null)}\n            title=\"No color\"\n          >\n            <IconBan size={20} strokeWidth={1.5} />\n          </div>\n          {PRESET_COLORS.map((presetColor, index) => (\n            <div\n              key={index}\n              className={`w-5 h-5 rounded cursor-pointer flex items-center justify-center transition-transform duration-100 hover:scale-110 border-2 border-transparent\n                ${color === presetColor ? 'border-solid !border-current' : ''}\n              `}\n              style={{ backgroundColor: presetColor }}\n              onClick={() => handleColorSelect(presetColor)}\n              title={presetColor}\n            />\n          ))}\n        </div>\n\n        <div className=\"flex items-center gap-2 mt-2 pt-0.5\">\n          <div\n            className=\"w-5 h-5 rounded-full flex-shrink-0 cursor-pointer\"\n            style={{ backgroundColor: customColor }}\n            onClick={() => handleColorSelect(customColor)}\n            title=\"Custom color\"\n          />\n          <ColorRangePicker\n            className=\"flex-1 flex\"\n            value={sliderPosition}\n            onChange={handleSliderChange}\n            onMouseUp={handleSliderEnd}\n            selectedColor={customColor}\n            colorRange={COLOR_RANGE_SEQUENCE}\n          />\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n\n  return (\n    <Dropdown icon={icon || defaultIcon} placement=\"bottom-start\">\n      {colorPickerContent}\n    </Dropdown>\n  );\n};\n\nexport default ColorPicker;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ColorRange/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .hue-slider {\n    -webkit-appearance: none;\n    appearance: none;\n    width: 100%;\n    height: 4px;\n    border-radius: 2px;\n    outline: none;\n  }\n\n  .hue-slider::-webkit-slider-thumb {\n    -webkit-appearance: none;\n    appearance: none;\n    width: 14px;\n    height: 14px;\n    border-radius: 50%;\n    background: ${(props) => props.color ?? props.theme.bg};\n    border: none;\n    cursor: pointer;\n    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);\n    transition: transform 0.1s ease;\n  }\n\n  .hue-slider::-webkit-slider-thumb:hover {\n    transform: scale(1.1);\n  }\n\n  .hue-slider::-moz-range-thumb {\n    width: 14px;\n    height: 14px;\n    border-radius: 50%;\n    background: ${(props) => props.color ?? props.theme.bg};\n    border: none;\n    cursor: pointer;\n    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);\n    transition: transform 0.1s ease;\n  }\n\n  .hue-slider::-moz-range-thumb:hover {\n    transform: scale(1.1);\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ColorRange/index.js",
    "content": "import StyledWrapper from './StyledWrapper';\n\nconst ColorRangePicker = ({ selectedColor, className, value, onChange, colorRange, ...props }) => {\n  return (\n    <StyledWrapper color={selectedColor} className={className}>\n      <input\n        type=\"range\"\n        min=\"0\"\n        max=\"100\"\n        value={value}\n        onChange={onChange}\n        className=\"hue-slider\"\n        style={{\n          background: `linear-gradient(to right, ${colorRange.join(',')})`\n        }}\n        title=\"Adjust color\"\n        {...props}\n      />\n    </StyledWrapper>\n  );\n};\n\nexport default ColorRangePicker;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Cookies/ModifyCookieModal/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  /* Info icon */\n  .info-icon {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  /* Required field asterisk */\n  .required-asterisk {\n    color: ${(props) => props.theme.colors.text.danger};\n  }\n\n  /* Error messages */\n  .error-message {\n    color: ${(props) => props.theme.colors.text.danger};\n  }\n\n  /* Checkbox */\n  input[type='checkbox'] {\n    cursor: pointer;\n    accent-color: ${(props) => props.theme.primary.solid};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Cookies/ModifyCookieModal/index.js",
    "content": "import React, { useState, useRef, useEffect } from 'react';\nimport { useFormik } from 'formik';\nimport * as Yup from 'yup';\nimport Modal from 'components/Modal/index';\nimport { modifyCookie, addCookie, getParsedCookie, createCookieString } from 'providers/ReduxStore/slices/app';\nimport { useDispatch } from 'react-redux';\nimport toast from 'react-hot-toast';\nimport ToggleSwitch from 'components/ToggleSwitch/index';\nimport { IconInfoCircle } from '@tabler/icons';\nimport moment from 'moment';\nimport 'moment-timezone';\nimport { Tooltip } from 'react-tooltip';\nimport { isEmpty } from 'lodash';\nimport StyledWrapper from './StyledWrapper';\n\nconst removeEmptyValues = (obj) => {\n  return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== null && value !== undefined));\n};\n\nconst ModifyCookieModal = ({ onClose, domain, cookie }) => {\n  const dispatch = useDispatch();\n  const [isRawMode, setIsRawMode] = useState(false);\n  const [cookieString, setCookieString] = useState('');\n  const initialParseRef = useRef(false);\n\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: {\n      ...(cookie ? cookie : {}),\n      key: cookie?.key || '',\n      value: cookie?.value || '',\n      path: cookie?.path || '/',\n      domain: cookie?.domain || domain || '',\n      expires: cookie?.expires ? moment(cookie.expires).format(moment.HTML5_FMT.DATETIME_LOCAL) : '',\n      secure: cookie?.secure || false,\n      httpOnly: cookie?.httpOnly || false\n    },\n    validationSchema: Yup.object({\n      key: Yup.string().required('Key is required'),\n      value: Yup.string().required('Value is required'),\n      domain: Yup.string().required('Domain is required'),\n      secure: Yup.boolean(),\n      httpOnly: Yup.boolean(),\n      expires: Yup.mixed()\n        .nullable()\n        .transform((value) => {\n          if (!value || value === '') return null;\n          return moment(value).isValid() ? moment(value).toDate() : null;\n        })\n        .test('future-date', 'Expiration date must be in the future', (value) => {\n          if (!value) return true;\n          return moment(value).isAfter(moment());\n        })\n    }),\n    onSubmit: (values) => {\n      const modValues = removeEmptyValues({\n        ...(cookie ? cookie : {}),\n        ...values,\n        expires: values.expires\n          ? moment(values.expires).isValid()\n            ? moment(values.expires).toDate()\n            : Infinity\n          : Infinity\n      });\n\n      handleCookieDispatch(cookie, domain, modValues, onClose);\n    }\n  });\n\n  const title = cookie ? 'Modify Cookie' : 'Add Cookie';\n\n  const handleCookieDispatch = (cookie, domain, modValues, onClose) => {\n    if (cookie) {\n      dispatch(modifyCookie(domain, cookie, modValues))\n        .then(() => {\n          toast.success('Cookie modified successfully');\n          onClose();\n        })\n        .catch((err) => {\n          toast.error('An error occurred while modifying cookie');\n          console.error(err);\n        });\n    } else {\n      dispatch(addCookie(domain, modValues))\n        .then(() => {\n          toast.success('Cookie added successfully');\n          onClose();\n        })\n        .catch((err) => {\n          toast.error('An error occurred while adding cookie');\n          console.error(err);\n        });\n    }\n  };\n\n  const onSubmit = async () => {\n    try {\n      if (isRawMode) {\n        const cookieObj = await dispatch(getParsedCookie(cookieString));\n\n        const modifiedCookie = removeEmptyValues({\n          ...formik.values,\n          ...cookieObj,\n          expires: cookieObj?.expires\n            ? moment(cookieObj.expires).isValid()\n              ? moment(cookieObj.expires).toDate()\n              : Infinity\n            : Infinity\n        });\n\n        if (!cookieObj) {\n          toast.error('Please enter a valid cookie string');\n          return;\n        }\n\n        const validationErrors = await formik.setValues(\n          (values) => ({\n            ...values,\n            ...modifiedCookie,\n            expires:\n              modifiedCookie?.expires && moment(modifiedCookie.expires).isValid()\n                ? moment(new Date(modifiedCookie.expires)).format(moment.HTML5_FMT.DATETIME_LOCAL)\n                : ''\n          }),\n          true\n        );\n\n        if (!isEmpty(validationErrors)) {\n          toast.error(Object.values(validationErrors).join('\\n'));\n          return;\n        }\n\n        handleCookieDispatch(cookie, domain, modifiedCookie, onClose);\n      } else {\n        formik.handleSubmit();\n      }\n    } catch (error) {\n      const errMsg = error.message || 'An error occurred while parsing cookie string';\n      toast.error(errMsg);\n    }\n  };\n\n  useEffect(() => {\n    if (!isRawMode) return;\n    const loadCookieString = async () => {\n      if (cookie) {\n        const str = await dispatch(createCookieString(cookie));\n        setCookieString(str);\n      }\n      return '';\n    };\n\n    loadCookieString();\n  }, [cookie, isRawMode]);\n\n  // create the cookieString when raw mode is enabled\n  useEffect(() => {\n    if (isRawMode) {\n      const createCookieStr = async () => {\n        const str = await dispatch(createCookieString(formik.values));\n        setCookieString(str);\n      };\n\n      createCookieStr();\n    }\n  }, [isRawMode, formik.values]);\n\n  useEffect(() => {\n    // Reset the ref when raw mode changes\n    if (isRawMode) {\n      initialParseRef.current = false;\n      return;\n    }\n\n    const setParsedCookie = async () => {\n      if (!isRawMode && cookieString && !initialParseRef.current) {\n        initialParseRef.current = true;\n\n        try {\n          const cookieObj = await dispatch(getParsedCookie(cookieString));\n\n          if (!cookieObj) return;\n\n          formik.setValues(\n            (values) => ({\n              ...values,\n              ...removeEmptyValues(cookieObj),\n              expires:\n                cookieObj?.expires && moment(cookieObj.expires).isValid()\n                  ? moment(new Date(cookieObj.expires)).format(moment.HTML5_FMT.DATETIME_LOCAL)\n                  : ''\n            }),\n            true\n          );\n        } catch (error) {\n          const errMsg = error.message || 'An error occurred while parsing cookie string';\n          toast.error(errMsg);\n        }\n      }\n    };\n\n    setParsedCookie();\n  }, [isRawMode, cookieString, dispatch, formik]);\n\n  return (\n    <Modal\n      size=\"lg\"\n      title={title}\n      onClose={onClose}\n      handleCancel={onClose}\n      handleConfirm={onSubmit}\n      customHeader={(\n        <div className=\"flex items-center justify-between w-full\">\n          <h2 className=\"font-bold\">{title}</h2>\n          <div className=\"ml-auto flex items-center \">\n            <ToggleSwitch\n              className=\"mr-2\"\n              isOn={isRawMode}\n              size=\"2xs\"\n              handleToggle={(e) => {\n                setIsRawMode(e.target.checked);\n              }}\n            />\n            <label className=\"font-normal mr-4 normal-case\">Edit Raw</label>\n          </div>\n        </div>\n      )}\n    >\n      <StyledWrapper>\n        <form onSubmit={(e) => e.preventDefault()} className=\"px-2\">\n          {isRawMode ? (\n            <div>\n              <div className=\"flex items-center gap-2 mb-1\">\n                <label className=\"block\">Set-Cookie String</label>\n                <IconInfoCircle id=\"cookie-raw-info\" size={16} strokeWidth={1.5} className=\"info-icon\" />\n                <Tooltip\n                  anchorId=\"cookie-raw-info\"\n                  className=\"tooltip-mod\"\n                  html=\"Key, Path, and Domain are immutable properties and cannot be modified for existing cookies\"\n                />\n              </div>\n              <textarea\n                value={cookieString}\n                onChange={(e) => setCookieString(e.target.value)}\n                className=\"block textbox w-full h-24\"\n                placeholder=\"key=value; key2=value2\"\n              />\n            </div>\n          ) : (\n            <div className=\"space-y-4\">\n              <div className=\"grid grid-cols-2 gap-4\">\n                <div>\n                  <label className=\"block mb-1\">\n                    Domain<span className=\"required-asterisk\">*</span>{' '}\n                  </label>\n                  <input\n                    type=\"text\"\n                    name=\"domain\"\n                    // Auto-focus if its add-new i.e. when domain prop is empty\n                    autoFocus={!domain && !formik.values.domain}\n                    value={formik.values.domain}\n                    onChange={formik.handleChange}\n                    className=\"block textbox non-passphrase-input w-full disabled:opacity-50\"\n                    disabled={!!cookie}\n                  />\n                  {formik.touched.domain && formik.errors.domain && (\n                    <div className=\"error-message mt-1\">{formik.errors.domain}</div>\n                  )}\n                </div>\n                <div>\n                  <label className=\"block mb-1\">Path</label>\n                  <input\n                    type=\"text\"\n                    name=\"path\"\n                    value={formik.values.path}\n                    onChange={formik.handleChange}\n                    className=\"block textbox non-passphrase-input w-full disabled:opacity-50\"\n                    disabled={!!cookie}\n                  />\n                  {formik.touched.path && formik.errors.path && (\n                    <div className=\"error-message mt-1\">{formik.errors.path}</div>\n                  )}\n                </div>\n                <div>\n                  <label className=\"block mb-1\">\n                    Key<span className=\"required-asterisk\">*</span>{' '}\n                  </label>\n                  <input\n                    type=\"text\"\n                    name=\"key\"\n                    // Auto focus when add-for-domain i.e. if domain is already prefilled\n                    autoFocus={!!domain && !formik.values.key}\n                    value={formik.values.key}\n                    onChange={formik.handleChange}\n                    className=\"block textbox non-passphrase-input w-full disabled:opacity-50\"\n                    disabled={!!cookie}\n                  />\n                  {formik.touched.key && formik.errors.key && (\n                    <div className=\"error-message mt-1\">{formik.errors.key}</div>\n                  )}\n                </div>\n\n                <div>\n                  <label className=\"block mb-1\">\n                    Value<span className=\"required-asterisk\">*</span>{' '}\n                  </label>\n                  <input\n                    type=\"text\"\n                    name=\"value\"\n                    // Auto-focus when its in edit mode i.e. cookie prop is present\n                    autoFocus={!!cookie}\n                    value={formik.values.value}\n                    onChange={formik.handleChange}\n                    className=\"block textbox non-passphrase-input w-full\"\n                  />\n                  {formik.touched.value && formik.errors.value && (\n                    <div className=\"error-message mt-1\">{formik.errors.value}</div>\n                  )}\n                </div>\n              </div>\n\n              {/* Date Picker */}\n              <div className=\"w-full flex items-end\">\n                <div>\n                  <label className=\"block mb-1\">Expiration ({moment.tz.guess()})</label>\n                  <input\n                    type=\"datetime-local\"\n                    name=\"expires\"\n                    value={formik.values.expires}\n                    onChange={(e) => {\n                      formik.handleChange(e);\n                    }}\n                    className=\"block textbox non-passphrase-input w-full\"\n                    min={moment().format(moment.HTML5_FMT.DATETIME_LOCAL)}\n                  />\n                  {formik.touched.expires && formik.errors.expires && (\n                    <div className=\"error-message mt-1\">{formik.errors.expires}</div>\n                  )}\n                </div>\n\n                {/* Checkboxes */}\n                <div className=\"flex space-x-4 ml-auto\">\n                  <label className=\"flex items-center\">\n                    <input\n                      type=\"checkbox\"\n                      name=\"secure\"\n                      checked={formik.values.secure}\n                      onChange={formik.handleChange}\n                      className=\"mr-2\"\n                    />\n                    <span>Secure</span>\n                  </label>\n\n                  <label className=\"flex items-center\">\n                    <input\n                      type=\"checkbox\"\n                      name=\"httpOnly\"\n                      checked={formik.values.httpOnly}\n                      onChange={formik.handleChange}\n                      className=\"mr-2\"\n                    />\n                    <span>HTTP Only</span>\n                  </label>\n                </div>\n              </div>\n            </div>\n          )}\n        </form>\n      </StyledWrapper>\n    </Modal>\n  );\n};\n\nexport default ModifyCookieModal;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Cookies/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  table {\n    width: 100%;\n    table-layout: fixed;\n\n    thead {\n      color: ${(props) => props.theme.table.thead.color};\n      font-size: ${(props) => props.theme.font.size.base};\n      user-select: none;\n    }\n  }\n\n  &.header {\n    input {\n      padding: 0.3rem 0.5rem;\n    }\n  }\n\n  .textbox {\n    line-height: 1.42857143;\n    border: 1px solid #ccc;\n    padding: 0.45rem;\n    box-shadow: none;\n    border-radius: 0px;\n    outline: none;\n    box-shadow: none;\n    transition: border-color ease-in-out 0.1s;\n    border-radius: 3px;\n    background-color: ${(props) => props.theme.input.bg};\n    border: 1px solid ${(props) => props.theme.input.border};\n\n    &:focus {\n      border: solid 1px ${(props) => props.theme.input.focusBorder} !important;\n      outline: none !important;\n    }\n  }\n\n  .scroll-box {\n    max-height: 500px;\n    overflow-y: auto;\n\n    background:\n    /* Shadow Cover TOP */\n    linear-gradient(\n      ${(props) => props.theme.modal.body.bg} 20%,\n      rgba(255, 255, 255, 0)\n    ) center top,\n    \n    /* Shadow Cover BOTTOM */\n    linear-gradient(\n      rgba(255, 255, 255, 0),\n      ${(props) => props.theme.modal.body.bg} 80%\n    ) center bottom,\n    \n    /* Shadow TOP */\n    linear-gradient(\n      rgba(0, 0, 0, 0.1) 0%,\n      rgba(0, 0, 0, 0) 100%\n    ) center top,\n    \n    /* Shadow BOTTOM */\n    linear-gradient(\n      rgba(0, 0, 0, 0) 0%,\n      rgba(0, 0, 0, 0.1) 100%\n    ) center bottom;\n\n    background-repeat: no-repeat;\n    background-size: 100% 30px, 100% 30px, 100% 10px, 100% 10px;\n    background-attachment: local, local, scroll, scroll;\n  }\n\n  /* Warning icon */\n  .warning-icon {\n    color: ${(props) => props.theme.colors.text.warning};\n  }\n\n  /* Empty state */\n  .empty-icon {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .empty-text {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  /* Domain count text */\n  .domain-count {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  /* Action buttons */\n  .action-button {\n    color: ${(props) => props.theme.colors.text.muted};\n    transition: color 0.2s;\n    cursor: pointer;\n\n    &:hover {\n      color: ${(props) => props.theme.text};\n    }\n  }\n\n  .action-button-danger {\n    color: ${(props) => props.theme.text};\n    transition: color 0.2s;\n    cursor: pointer;\n\n    &:hover {\n      color: ${(props) => props.theme.colors.text.danger};\n    }\n  }\n\n  /* Table styles */\n  table {\n    thead {\n      tr {\n        border-bottom: 1px solid ${(props) => props.theme.table.border};\n        color: ${(props) => props.theme.table.thead.color};\n\n        th {\n          color: ${(props) => props.theme.table.thead.color};\n        }\n      }\n    }\n\n    tbody {\n      tr {\n        border-bottom: 1px solid ${(props) => props.theme.table.border};\n\n        &:last-child {\n          border-bottom: none;\n        }\n      }\n    }\n  }\n\n  /* Edit button */\n  .edit-button {\n    color: ${(props) => props.theme.text};\n    transition: color 0.2s;\n    cursor: pointer;\n\n    &:hover {\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n  }\n\n  /* Delete button */\n  .delete-button {\n    color: ${(props) => props.theme.text};\n    transition: color 0.2s;\n    cursor: pointer;\n\n    &:hover {\n      color: ${(props) => props.theme.colors.text.danger};\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Cookies/index.js",
    "content": "import React, { useState, useRef, useEffect, useMemo } from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport Modal from 'components/Modal';\nimport Accordion from 'components/Accordion/index';\nimport { IconTrash, IconEdit, IconCirclePlus, IconCookieOff, IconAlertTriangle, IconSearch } from '@tabler/icons';\nimport { deleteCookiesForDomain, deleteCookie } from 'providers/ReduxStore/slices/app';\nimport toast from 'react-hot-toast';\nimport ModifyCookieModal from 'components/Cookies/ModifyCookieModal/index';\nimport StyledWrapper from './StyledWrapper';\nimport moment from 'moment';\nimport { Tooltip } from 'react-tooltip';\nimport Button from 'ui/Button';\n\nconst ClearDomainCookiesModal = ({ onClose, domain, onClear }) => (\n  <Modal onClose={onClose} handleCancel={onClose} title=\"Clear Domain Cookies\" hideFooter={true}>\n    <div className=\"flex items-center font-normal\">\n      <IconAlertTriangle size={32} strokeWidth={1.5} className=\"warning-icon\" />\n      <h1 className=\"ml-2 text-lg font-medium\">Hold on..</h1>\n    </div>\n    <div className=\"font-normal mt-4\">\n      Are you sure you want to clear all cookies for the domain {domain}?\n    </div>\n\n    <div className=\"flex justify-between mt-6\">\n      <div>\n        <Button color=\"secondary\" variant=\"ghost\" onClick={onClose}>\n          Close\n        </Button>\n      </div>\n      <div>\n        <Button color=\"danger\" onClick={onClear}>\n          Clear All\n        </Button>\n      </div>\n    </div>\n  </Modal>\n);\n\nconst DeleteCookieModal = ({ onClose, cookieName, onDelete }) => (\n  <Modal onClose={onClose} handleCancel={onClose} title=\"Delete Cookie\" hideFooter={true}>\n    <div className=\"flex items-center font-normal\">\n      <IconAlertTriangle size={32} strokeWidth={1.5} className=\"warning-icon\" />\n      <h1 className=\"ml-2 text-lg font-medium\">Hold on..</h1>\n    </div>\n    <div className=\"font-normal mt-4\">\n      Are you sure you want to delete the cookie {cookieName}?\n    </div>\n\n    <div className=\"flex justify-between mt-6\">\n      <div>\n        <Button color=\"secondary\" variant=\"ghost\" onClick={onClose}>\n          Close\n        </Button>\n      </div>\n      <div>\n        <Button color=\"danger\" onClick={onDelete}>\n          Delete\n        </Button>\n      </div>\n    </div>\n  </Modal>\n);\n\nconst CollectionProperties = ({ onClose }) => {\n  const dispatch = useDispatch();\n  const cookies = useSelector((state) => state.app.cookies) || [];\n  const [isModifyCookieModalOpen, setIsModifyCookieModalOpen] = useState(false);\n  const [currentDomain, setCurrentDomain] = useState(null);\n  const [cookieToEdit, setCookieToEdit] = useState(null);\n\n  const [domainToClear, setDomainToClear] = useState(null);\n  const [cookieToDelete, setCookieToDelete] = useState(null);\n  const [searchText, setSearchText] = useState(null);\n\n  const handleAddCookie = (domain) => {\n    if (domain) setCurrentDomain(domain);\n    setIsModifyCookieModalOpen(true);\n  };\n\n  const handleEditCookie = (domain, cookie) => {\n    setCurrentDomain(domain);\n    setCookieToEdit(cookie);\n    setIsModifyCookieModalOpen(true);\n  };\n\n  const handleClearDomainCookies = (domain) => {\n    setDomainToClear(domain);\n  };\n\n  const clearDomainCookiesAction = () => {\n    dispatch(deleteCookiesForDomain(domainToClear))\n      .then(() => {\n        toast.success('Domain cookies cleared successfully');\n      })\n      .catch((err) => console.log(err) && toast.error('Failed to clear domain cookies'));\n    setDomainToClear(null);\n  };\n\n  const handleDeleteCookie = (domain, path, key) => {\n    setCookieToDelete({ key, domain, path });\n  };\n\n  const deleteCookieAction = () => {\n    if (cookieToDelete) {\n      const { domain, path, key } = cookieToDelete;\n      dispatch(deleteCookie(domain, path, key))\n        .then(() => {\n          toast.success('Cookie deleted successfully');\n        })\n        .catch((err) => console.log(err) && toast.error('Failed to delete cookie'));\n    }\n    setCookieToDelete(null);\n  };\n\n  const filteredCookies = useMemo(() => {\n    if (!searchText) return cookies;\n\n    return cookies.filter((cookie) =>\n      cookie.domain.toLowerCase().includes(searchText.toLowerCase())\n    );\n  }, [cookies, searchText]);\n\n  const shouldShowHeader = cookies && cookies.length > 0;\n\n  return (\n    <>\n      <Modal\n        size=\"xl\"\n        title=\"Cookies\"\n        hideFooter={true}\n        handleCancel={onClose}\n        customHeader={shouldShowHeader ? (\n          <StyledWrapper className=\"header flex items-center justify-between w-full\">\n            <h2 className=\"text-xs font-medium\">Cookies</h2>\n            <input\n              type=\"search\"\n              placeholder=\"Search by domain\"\n              value={searchText || ''}\n              onChange={(e) => setSearchText(e.target.value)}\n              className=\"block textbox non-passphrase-input ml-auto font-normal\"\n              autoFocus\n            />\n            <Button\n              type=\"submit\"\n              size=\"sm\"\n              className=\"mx-4\"\n              icon={<IconCirclePlus strokeWidth={1.5} size={16} />}\n              onClick={(e) => {\n                e.stopPropagation();\n                handleAddCookie();\n              }}\n            >\n              <span>Add Cookie</span>\n            </Button>\n          </StyledWrapper>\n        ) : null}\n      >\n        <StyledWrapper>\n          {!cookies || !cookies.length ? (\n            // No cookies found\n            <div className=\"flex items-center justify-center flex-col\">\n              <IconCookieOff size={48} strokeWidth={1.5} className=\"empty-icon\" />\n              <h2 className=\"text-lg font-medium mt-4\">No cookies found</h2>\n              <p className=\"empty-text mt-2\">Add cookies to get started</p>\n              <Button\n                type=\"submit\"\n                size=\"sm\"\n                className=\"mt-8\"\n                icon={<IconCirclePlus strokeWidth={1.5} size={16} />}\n                onClick={(e) => {\n                  e.stopPropagation();\n                  handleAddCookie();\n                }}\n              >\n                Add Cookie\n              </Button>\n            </div>\n          ) : cookies.length && !filteredCookies.length ? (\n            // No search results\n            <div className=\"flex items-center justify-center flex-col\">\n              <IconSearch size={48} />\n              <h2 className=\"text-lg font-medium mt-4\">No search results</h2>\n              <p className=\"empty-text mt-2\">Try a different search term</p>\n            </div>\n          ) : (\n            // Show cookies list\n            <div className=\"scroll-box\">\n              <Accordion defaultIndex={0}>\n                {filteredCookies.map((domainWithCookies, i) => (\n                  <Accordion.Item key={i} index={i}>\n                    <Accordion.Header index={i} className=\"flex items-center\">\n                      <div className=\"flex items-center\">\n                        <span>{domainWithCookies.domain}</span>\n                        <span className=\"domain-count ml-2 text-xs\">\n                          ({domainWithCookies.cookies.length}{' '}\n                          {domainWithCookies.cookies.length === 1 ? 'cookie' : 'cookies'})\n                        </span>\n                        <div className=\"ml-auto flex items-center gap-2\">\n                          <button\n                            type=\"submit\"\n                            className=\"action-button flex items-center gap-1\"\n                            onClick={(e) => {\n                              e.stopPropagation();\n                              handleAddCookie(domainWithCookies.domain);\n                            }}\n                          >\n                            <IconCirclePlus strokeWidth={1.5} size={16} />\n                          </button>\n                          <button\n                            onClick={(e) => {\n                              e.stopPropagation();\n                              handleClearDomainCookies(domainWithCookies.domain);\n                            }}\n                            className=\"action-button-danger mr-2\"\n                          >\n                            <IconTrash strokeWidth={1.5} size={16} />\n                          </button>\n                        </div>\n                      </div>\n                    </Accordion.Header>\n                    <Accordion.Content index={i}>\n                      <div className=\"flex items-center justify-between\">\n                        <table className=\"w-full\">\n                          <thead>\n                            <tr className=\"text-left\">\n                              <th className=\"py-2 px-4 font-medium w-32\">Name</th>\n                              <th className=\"py-2 px-4 font-medium w-52\">Value</th>\n                              <th className=\"py-2 px-4 font-medium\">Path</th>\n                              <th className=\"py-2 px-4 font-medium\">Expires</th>\n                              <th className=\"py-2 px-4 font-medium text-center\">Secure</th>\n                              <th className=\"py-2 px-4 font-medium text-center\">HTTP Only</th>\n                              <th className=\"py-2 px-4 font-medium text-right w-24\">Actions</th>\n                            </tr>\n                          </thead>\n                          <tbody>\n                            {domainWithCookies.cookies.map((cookie) => (\n                              <tr key={cookie.key}>\n                                <td className=\"py-2 px-4 truncate\">\n                                  <span id={`cookie-key-${cookie.key}`}>{cookie.key}</span>\n                                  <Tooltip\n                                    anchorId={`cookie-key-${cookie.key}`}\n                                    className=\"tooltip-mod\"\n                                    html={cookie.key}\n                                  />\n                                </td>\n                                <td className=\"py-2 px-4 truncate\">\n                                  <span id={`cookie-value-${cookie.key}`}>{cookie.value}</span>\n                                  <Tooltip\n                                    anchorId={`cookie-value-${cookie.key}`}\n                                    className=\"tooltip-mod\"\n                                    html={cookie.value}\n                                  />\n                                </td>\n                                <td className=\"py-2 px-4 truncate\">{cookie.path || '/'}</td>\n                                <td className=\"py-2 px-4 truncate\">\n                                  <span id={`cookie-expires-${cookie.key}`}>\n                                    {cookie.expires && moment(cookie.expires).isValid()\n                                      ? new Date(cookie.expires).toLocaleString()\n                                      : 'Session'}\n                                  </span>\n                                  {cookie.expires && moment(cookie.expires).isValid() && (\n                                    <Tooltip\n                                      anchorId={`cookie-expires-${cookie.key}`}\n                                      className=\"tooltip-mod\"\n                                      html={new Date(cookie.expires).toLocaleString()}\n                                    />\n                                  )}\n                                </td>\n                                <td className=\"py-2 px-4 text-center\">{cookie.secure ? '✓' : ''}</td>\n                                <td className=\"py-2 px-4 text-center\">{cookie.httpOnly ? '✓' : ''}</td>\n                                <td className=\"py-2 px-4\">\n                                  <div className=\"flex items-center justify-end gap-2\">\n                                    <button\n                                      onClick={(e) => {\n                                        e.stopPropagation();\n                                        handleEditCookie(domainWithCookies.domain, cookie);\n                                      }}\n                                      className=\"edit-button\"\n                                    >\n                                      <IconEdit strokeWidth={1.5} size={16} />\n                                    </button>\n                                    <button\n                                      onClick={(e) => {\n                                        e.stopPropagation();\n                                        handleDeleteCookie(domainWithCookies.domain, cookie.path, cookie.key);\n                                      }}\n                                      className=\"delete-button\"\n                                    >\n                                      <IconTrash strokeWidth={1.5} size={16} />\n                                    </button>\n                                  </div>\n                                </td>\n                              </tr>\n                            ))}\n                          </tbody>\n                        </table>\n                      </div>\n                    </Accordion.Content>\n                  </Accordion.Item>\n                ))}\n              </Accordion>\n            </div>\n          )}\n        </StyledWrapper>\n      </Modal>\n      {isModifyCookieModalOpen && (\n        <ModifyCookieModal\n          onClose={() => {\n            setCookieToEdit(null);\n            setCurrentDomain(null);\n            setIsModifyCookieModalOpen(false);\n          }}\n          domain={currentDomain}\n          cookie={cookieToEdit}\n        />\n      )}\n      {domainToClear ? (\n        <ClearDomainCookiesModal\n          onClose={() => setDomainToClear(null)}\n          domain={domainToClear}\n          onClear={clearDomainCookiesAction}\n        />\n      ) : null}\n      {cookieToDelete ? (\n        <DeleteCookieModal\n          onClose={() => setCookieToDelete(null)}\n          cookieName={cookieToDelete.key}\n          onDelete={deleteCookieAction}\n        />\n      ) : null}\n    </>\n  );\n};\n\nexport default CollectionProperties;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CreateTransientRequest/index.js",
    "content": "import React, { useState, useRef, useCallback, useMemo } from 'react';\nimport { IconPlus, IconApi, IconBrandGraphql, IconPlugConnected, IconCode } from '@tabler/icons';\nimport ActionIcon from 'ui/ActionIcon/index';\nimport Dropdown from 'components/Dropdown';\nimport { newHttpRequest, newGrpcRequest, newWsRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport { sanitizeName } from 'utils/common/regex';\nimport toast from 'react-hot-toast';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { flattenItems, isItemARequest, isItemTransientRequest } from 'utils/collections';\nimport filter from 'lodash/filter';\nimport { get } from 'lodash';\nimport { formatIpcError } from 'utils/common/error';\n\nconst REQUEST_TYPE = {\n  HTTP: 'http',\n  GRAPHQL: 'graphql',\n  GRPC: 'grpc',\n  WEBSOCKET: 'websocket'\n};\n\n/**\n * Generate a request name for transient requests in the pattern \"Untitled {Count}\"\n * @param {Object} collection - The collection object\n * @returns {string} A request name like \"Untitled 1\", \"Untitled 2\", etc.\n */\nconst generateTransientRequestName = (collection) => {\n  if (!collection || !collection.items) {\n    return 'Untitled 1';\n  }\n  const allItems = flattenItems(collection.items);\n  const transientRequests = filter(allItems, (item) => {\n    return isItemTransientRequest(item);\n  });\n\n  // Find the highest \"Untitled X\" number among transient requests\n  let maxNumber = 0;\n  transientRequests.forEach((item) => {\n    const match = item.name?.match(/^Untitled (\\d+)$/);\n    if (match) {\n      const number = parseInt(match[1], 10);\n      if (number > maxNumber) {\n        maxNumber = number;\n      }\n    }\n  });\n\n  // Increment from the highest number found, or start at 1 if none found\n  const count = maxNumber + 1;\n\n  return `Untitled ${count}`;\n};\n\nconst CreateTransientRequest = ({ collectionUid }) => {\n  const [dropdownVisible, setDropdownVisible] = useState(false);\n  const dropdownTippyRef = useRef();\n  const dispatch = useDispatch();\n  const collections = useSelector((state) => state.collections.collections);\n\n  const collection = useMemo(() => {\n    return collections?.find((c) => c.uid === collectionUid);\n  }, [collections, collectionUid]);\n\n  const collectionPresets = useMemo(() => {\n    return get(collection, collection?.draft?.brunoConfig ? 'draft.brunoConfig.presets' : 'brunoConfig.presets', {\n      requestType: 'http',\n      requestUrl: ''\n    });\n  }, [collection]);\n\n  const onDropdownCreate = (ref) => {\n    dropdownTippyRef.current = ref;\n    if (ref) {\n      ref.setProps({\n        onHide: () => {\n          setDropdownVisible(false);\n        }\n      });\n    }\n  };\n\n  const handleLeftClick = () => {\n    handleItemClick(collectionPresets.requestType);\n  };\n\n  const handleRightClick = (e) => {\n    e.preventDefault();\n    setDropdownVisible(true);\n  };\n\n  const handleCreateHttpRequest = useCallback(() => {\n    if (!collection) return;\n\n    const uniqueName = generateTransientRequestName(collection);\n    const filename = sanitizeName(uniqueName);\n\n    dispatch(\n      newHttpRequest({\n        requestName: uniqueName,\n        filename: filename,\n        requestType: 'http-request',\n        requestUrl: collectionPresets.requestUrl,\n        requestMethod: 'GET',\n        collectionUid: collection.uid,\n        itemUid: null,\n        isTransient: true\n      })\n    ).catch((err) => toast.error(formatIpcError(err) || 'An error occurred while adding the request'));\n  }, [dispatch, collection, collectionPresets.requestUrl]);\n\n  const handleCreateGraphQLRequest = useCallback(() => {\n    if (!collection) return;\n\n    const uniqueName = generateTransientRequestName(collection);\n    const filename = sanitizeName(uniqueName);\n\n    dispatch(\n      newHttpRequest({\n        requestName: uniqueName,\n        filename: filename,\n        requestType: 'graphql-request',\n        requestUrl: collectionPresets.requestUrl,\n        requestMethod: 'POST',\n        collectionUid: collection.uid,\n        itemUid: null,\n        isTransient: true,\n        body: {\n          mode: 'graphql',\n          graphql: {\n            query: '',\n            variables: ''\n          }\n        }\n      })\n    ).catch((err) => toast.error(formatIpcError(err) || 'An error occurred while adding the request'));\n  }, [dispatch, collection, collectionPresets.requestUrl]);\n\n  const handleCreateWebSocketRequest = useCallback(() => {\n    if (!collection) return;\n\n    const uniqueName = generateTransientRequestName(collection);\n    const filename = sanitizeName(uniqueName);\n\n    dispatch(\n      newWsRequest({\n        requestName: uniqueName,\n        filename: filename,\n        requestUrl: collectionPresets.requestUrl,\n        requestMethod: 'ws',\n        collectionUid: collection.uid,\n        itemUid: null,\n        isTransient: true\n      })\n    ).catch((err) => toast.error(formatIpcError(err) || 'An error occurred while adding the request'));\n  }, [dispatch, collection, collectionPresets.requestUrl]);\n\n  const handleCreateGrpcRequest = useCallback(() => {\n    if (!collection) return;\n\n    const uniqueName = generateTransientRequestName(collection);\n    const filename = sanitizeName(uniqueName);\n\n    dispatch(\n      newGrpcRequest({\n        requestName: uniqueName,\n        filename: filename,\n        requestUrl: collectionPresets.requestUrl,\n        collectionUid: collection.uid,\n        itemUid: null,\n        isTransient: true\n      })\n    ).catch((err) => toast.error(formatIpcError(err) || 'An error occurred while adding the request'));\n  }, [dispatch, collection, collectionPresets.requestUrl]);\n\n  const handleItemClick = (type) => {\n    if (dropdownTippyRef.current) {\n      dropdownTippyRef.current.hide();\n    }\n    switch (type) {\n      case REQUEST_TYPE.HTTP:\n        handleCreateHttpRequest();\n        break;\n      case REQUEST_TYPE.GRAPHQL:\n        handleCreateGraphQLRequest();\n        break;\n      case REQUEST_TYPE.GRPC:\n        handleCreateGrpcRequest();\n        break;\n      case REQUEST_TYPE.WEBSOCKET:\n        handleCreateWebSocketRequest();\n        break;\n    }\n  };\n\n  if (!collection) {\n    return null;\n  }\n\n  const IconButton = (\n    <ActionIcon\n      onClick={handleLeftClick}\n      onContextMenu={handleRightClick}\n      aria-label=\"New Transient Request\"\n      size=\"lg\"\n      style={{ marginBottom: '3px' }}\n    >\n      <IconPlus size={18} strokeWidth={1.5} />\n    </ActionIcon>\n  );\n\n  return (\n    <Dropdown\n      icon={IconButton}\n      visible={dropdownVisible}\n      onCreate={onDropdownCreate}\n      onClickOutside={() => setDropdownVisible(false)}\n      placement=\"bottom-end\"\n    >\n      <div className=\"dropdown-item\" onClick={() => handleItemClick(REQUEST_TYPE.HTTP)}>\n        <div className=\"dropdown-icon\">\n          <IconApi size={16} strokeWidth={2} />\n        </div>\n        <div className=\"dropdown-label\">HTTP</div>\n      </div>\n      <div className=\"dropdown-item\" onClick={() => handleItemClick(REQUEST_TYPE.GRAPHQL)}>\n        <div className=\"dropdown-icon\">\n          <IconBrandGraphql size={16} strokeWidth={2} />\n        </div>\n        <div className=\"dropdown-label\">GraphQL</div>\n      </div>\n      <div className=\"dropdown-item\" onClick={() => handleItemClick(REQUEST_TYPE.GRPC)}>\n        <div className=\"dropdown-icon\">\n          <IconCode size={16} strokeWidth={2} />\n        </div>\n        <div className=\"dropdown-label\">gRPC</div>\n      </div>\n      <div className=\"dropdown-item\" onClick={() => handleItemClick(REQUEST_TYPE.WEBSOCKET)}>\n        <div className=\"dropdown-icon\">\n          <IconPlugConnected size={16} strokeWidth={2} />\n        </div>\n        <div className=\"dropdown-label\">WebSocket</div>\n      </div>\n    </Dropdown>\n  );\n};\n\nexport default CreateTransientRequest;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CreateUntitledRequest/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  position: relative;\n  display: inline-block;\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/CreateUntitledRequest/index.js",
    "content": "import React, { useMemo, useCallback } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport { newHttpRequest, newGrpcRequest, newWsRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport { generateUniqueRequestName } from 'utils/collections';\nimport { sanitizeName } from 'utils/common/regex';\nimport toast from 'react-hot-toast';\nimport { IconApi, IconBrandGraphql, IconPlugConnected, IconCode, IconPlus } from '@tabler/icons';\nimport ActionIcon from 'ui/ActionIcon';\n\nconst CreateUntitledRequest = ({ collectionUid, itemUid = null, onRequestCreated, placement = 'bottom' }) => {\n  const dispatch = useDispatch();\n  const collections = useSelector((state) => state.collections.collections);\n  const collection = collections?.find((c) => c.uid === collectionUid);\n\n  const handleCreateHttpRequest = useCallback(async () => {\n    const uniqueName = await generateUniqueRequestName(collection, 'Untitled', itemUid);\n    const filename = sanitizeName(uniqueName);\n\n    dispatch(\n      newHttpRequest({\n        requestName: uniqueName,\n        filename: filename,\n        requestType: 'http-request',\n        requestUrl: '',\n        requestMethod: 'GET',\n        collectionUid: collection.uid,\n        itemUid: itemUid\n      })\n    )\n      .then(() => {\n        toast.success('New request created!');\n        onRequestCreated?.();\n      })\n      .catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));\n  }, [dispatch, collection, itemUid, onRequestCreated]);\n\n  const handleCreateGraphQLRequest = useCallback(async () => {\n    const uniqueName = await generateUniqueRequestName(collection, 'Untitled', itemUid);\n    const filename = sanitizeName(uniqueName);\n\n    dispatch(\n      newHttpRequest({\n        requestName: uniqueName,\n        filename: filename,\n        requestType: 'graphql-request',\n        requestUrl: '',\n        requestMethod: 'POST',\n        collectionUid: collection.uid,\n        itemUid: itemUid,\n        body: {\n          mode: 'graphql',\n          graphql: {\n            query: '',\n            variables: ''\n          }\n        }\n      })\n    )\n      .then(() => {\n        toast.success('New request created!');\n        onRequestCreated?.();\n      })\n      .catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));\n  }, [dispatch, collection, itemUid, onRequestCreated]);\n\n  const handleCreateWebSocketRequest = useCallback(async () => {\n    const uniqueName = await generateUniqueRequestName(collection, 'Untitled', itemUid);\n    const filename = sanitizeName(uniqueName);\n\n    dispatch(\n      newWsRequest({\n        requestName: uniqueName,\n        filename: filename,\n        requestUrl: '',\n        requestMethod: 'ws',\n        collectionUid: collection.uid,\n        itemUid: itemUid\n      })\n    )\n      .then(() => {\n        toast.success('New request created!');\n        onRequestCreated?.();\n      })\n      .catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));\n  }, [dispatch, collection, itemUid, onRequestCreated]);\n\n  const handleCreateGrpcRequest = useCallback(async () => {\n    const uniqueName = await generateUniqueRequestName(collection, 'Untitled', itemUid);\n    const filename = sanitizeName(uniqueName);\n\n    dispatch(\n      newGrpcRequest({\n        requestName: uniqueName,\n        filename: filename,\n        requestUrl: '',\n        collectionUid: collection.uid,\n        itemUid: itemUid\n      })\n    )\n      .then(() => {\n        toast.success('New request created!');\n        onRequestCreated?.();\n      })\n      .catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));\n  }, [dispatch, collection, itemUid, onRequestCreated]);\n\n  const menuItems = useMemo(() => [\n    {\n      id: 'http',\n      label: 'HTTP',\n      leftSection: <IconApi size={16} strokeWidth={2} />,\n      onClick: handleCreateHttpRequest\n    },\n    {\n      id: 'graphql',\n      label: 'GraphQL',\n      leftSection: <IconBrandGraphql size={16} strokeWidth={2} />,\n      onClick: handleCreateGraphQLRequest\n    },\n    {\n      id: 'websocket',\n      label: 'WebSocket',\n      leftSection: <IconPlugConnected size={16} strokeWidth={2} />,\n      onClick: handleCreateWebSocketRequest\n    },\n    {\n      id: 'grpc',\n      label: 'gRPC',\n      leftSection: <IconCode size={16} strokeWidth={2} />,\n      onClick: handleCreateGrpcRequest\n    }\n  ], [handleCreateHttpRequest, handleCreateGraphQLRequest, handleCreateWebSocketRequest, handleCreateGrpcRequest]);\n\n  if (!collection) {\n    return null;\n  }\n\n  return (\n    <MenuDropdown\n      items={menuItems}\n      placement={placement}\n      autoFocusFirstOption={true}\n    >\n      <ActionIcon size=\"sm\">\n        <IconPlus size={16} strokeWidth={2} />\n      </ActionIcon>\n    </MenuDropdown>\n  );\n};\n\nexport default CreateUntitledRequest;\n"
  },
  {
    "path": "packages/bruno-app/src/components/DeprecationWarning/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .deprecation-warning {\n    box-sizing: border-box;\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    padding: 8px;\n    gap: 4px;\n    margin-bottom: 8px;\n    background: ${(props) => props.theme.deprecationWarning.bg};\n    border: 1px solid ${(props) => props.theme.deprecationWarning.border};\n    border-radius: 6px;\n\n    .warning-icon {\n      color: ${(props) => props.theme.deprecationWarning.icon};\n      flex-shrink: 0;\n      width: 16px;\n      height: 16px;\n    }\n\n    .warning-text {\n      font-family: 'Inter', sans-serif;\n      font-style: normal;\n      font-size: 14px;\n      line-height: 17px;\n      color: ${(props) => props.theme.deprecationWarning.text};\n\n      a {\n        color: ${(props) => props.theme.textLink};\n        text-decoration: underline;\n\n        &:hover {\n          text-decoration: none;\n        }\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/DeprecationWarning/index.js",
    "content": "import React from 'react';\nimport IconAlertTriangleFilled from '../Icons/IconAlertTriangleFilled';\nimport StyledWrapper from './StyledWrapper';\n\nconst DeprecationWarning = ({ featureName, learnMoreUrl }) => {\n  return (\n    <StyledWrapper>\n      <div className=\"deprecation-warning\">\n        <IconAlertTriangleFilled className=\"warning-icon\" size={16} />\n        <span className=\"warning-text\">\n          {featureName} will be removed in <strong>v3.0.0</strong>. They are deprecated and will no longer be supported. Learn more in{' '}\n          <a href={learnMoreUrl} target=\"_blank\" rel=\"noreferrer\">this post</a> or contact us at{' '}\n          <a href=\"mailto:support@usebruno.com\">support@usebruno.com</a> with questions.\n        </span>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default DeprecationWarning;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Devtools/Console/DebugTab/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n  background: ${(props) => props.theme.console.contentBg};\n  overflow: hidden;\n\n  .debug-header {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 8px 16px;\n    background: ${(props) => props.theme.console.headerBg};\n    border-bottom: 1px solid ${(props) => props.theme.console.border};\n    flex-shrink: 0;\n  }\n\n  .debug-title {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    color: ${(props) => props.theme.console.titleColor};\n    font-size: ${(props) => props.theme.font.size.base};\n    font-weight: 500;\n\n    .error-count {\n      color: ${(props) => props.theme.console.countColor};\n      font-size: ${(props) => props.theme.font.size.sm};\n      font-weight: 400;\n    }\n  }\n\n  .debug-controls {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n  }\n\n  .debug-content {\n    flex: 1;\n    overflow: hidden;\n    display: flex;\n    flex-direction: column;\n    min-height: 0;\n  }\n\n  .debug-empty {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    height: 100%;\n    color: ${(props) => props.theme.console.emptyColor};\n    text-align: center;\n    gap: 8px;\n    padding: 40px 20px;\n\n    p {\n      margin: 0;\n      font-size: ${(props) => props.theme.font.size.base};\n      font-weight: 500;\n    }\n\n    span {\n      font-size: ${(props) => props.theme.font.size.sm};\n      opacity: 0.7;\n    }\n  }\n\n  .errors-container {\n    display: flex;\n    flex-direction: column;\n    height: 100%;\n    overflow: hidden;\n    min-height: 0;\n  }\n\n  .errors-header {\n    display: grid;\n    grid-template-columns: 1fr 200px 120px;\n    gap: 12px;\n    padding: 8px 16px;\n    background: ${(props) => props.theme.console.headerBg};\n    border-bottom: 1px solid ${(props) => props.theme.console.border};\n    font-size: ${(props) => props.theme.font.size.xs};\n    font-weight: 500;\n    color: ${(props) => props.theme.console.titleColor};\n    text-transform: uppercase;\n    letter-spacing: 0.5px;\n    flex-shrink: 0;\n  }\n\n  .errors-list {\n    flex: 1;\n    overflow-y: auto;\n    overflow-x: hidden;\n    min-height: 0;\n  }\n\n  .error-row {\n    display: grid;\n    grid-template-columns: 1fr 200px 120px;\n    gap: 12px;\n    padding: 8px 16px;\n    border-bottom: 1px solid ${(props) => props.theme.console.border};\n    cursor: pointer;\n    transition: background-color 0.1s ease;\n    font-size: ${(props) => props.theme.font.size.sm};\n    align-items: center;\n\n    &:hover {\n      background: ${(props) => props.theme.console.logHoverBg};\n    }\n\n    &.selected {\n      background: ${(props) => props.theme.console.buttonHoverBg};\n      border-left: 3px solid ${(props) => props.theme.console.checkboxColor};\n    }\n  }\n\n  .error-message {\n    color: ${(props) => props.theme.console.messageColor};\n    font-weight: 500;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n  }\n\n  .error-location {\n    color: ${(props) => props.theme.console.messageColor};\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n    font-size: ${(props) => props.theme.font.size.xs};\n  }\n\n  .error-time {\n    color: ${(props) => props.theme.console.timestampColor};\n    font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n    font-size: ${(props) => props.theme.font.size.xs};\n    text-align: right;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Devtools/Console/DebugTab/index.js",
    "content": "import React from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { IconBug } from '@tabler/icons';\nimport {\n  setSelectedError,\n  clearDebugErrors\n} from 'providers/ReduxStore/slices/logs';\nimport StyledWrapper from './StyledWrapper';\n\nconst ErrorRow = ({ error, isSelected, onClick }) => {\n  const formatTime = (timestamp) => {\n    const date = new Date(timestamp);\n    return date.toLocaleTimeString('en-US', {\n      hour12: false,\n      hour: '2-digit',\n      minute: '2-digit',\n      second: '2-digit',\n      fractionalSecondDigits: 3\n    });\n  };\n\n  const getShortMessage = (message, maxLength = 80) => {\n    if (!message) return 'Unknown error';\n    return message.length > maxLength ? message.substring(0, maxLength) + '...' : message;\n  };\n\n  const getLocation = (error) => {\n    if (error.filename) {\n      const filename = error.filename.split('/').pop(); // Get just the filename\n      if (error.lineno && error.colno) {\n        return `${filename}:${error.lineno}:${error.colno}`;\n      } else if (error.lineno) {\n        return `${filename}:${error.lineno}`;\n      }\n      return filename;\n    }\n    return '-';\n  };\n\n  return (\n    <div\n      className={`error-row ${isSelected ? 'selected' : ''}`}\n      onClick={onClick}\n    >\n      <div className=\"error-message\" title={error.message}>\n        {getShortMessage(error.message)}\n      </div>\n\n      <div className=\"error-location\" title={error.filename}>\n        {getLocation(error)}\n      </div>\n\n      <div className=\"error-time\">\n        {formatTime(error.timestamp)}\n      </div>\n    </div>\n  );\n};\n\nconst DebugTab = () => {\n  const dispatch = useDispatch();\n  const { debugErrors, selectedError } = useSelector((state) => state.logs);\n\n  const handleErrorClick = (error) => {\n    dispatch(setSelectedError(error));\n  };\n\n  const handleClearErrors = () => {\n    dispatch(clearDebugErrors());\n  };\n\n  return (\n    <StyledWrapper>\n      <div className=\"debug-content\">\n        {debugErrors.length === 0 ? (\n          <div className=\"debug-empty\">\n            <IconBug size={48} strokeWidth={1} />\n            <p>No errors</p>\n            <span>console.error() calls will appear here</span>\n          </div>\n        ) : (\n          <div className=\"errors-container\">\n            <div className=\"errors-header\">\n              <div>Message</div>\n              <div>Location</div>\n              <div className=\"text-right\">Time</div>\n            </div>\n\n            <div className=\"errors-list\">\n              {debugErrors.map((error, index) => (\n                <ErrorRow\n                  key={error.id}\n                  error={error}\n                  isSelected={selectedError?.id === error.id}\n                  onClick={() => handleErrorClick(error)}\n                />\n              ))}\n            </div>\n          </div>\n        )}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default DebugTab;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Devtools/Console/ErrorDetailsPanel/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n  background: ${(props) => props.theme.console.contentBg};\n  border-left: 1px solid ${(props) => props.theme.console.border};\n  min-width: 400px;\n  max-width: 600px;\n  width: 40%;\n  overflow: hidden;\n\n  .panel-header {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 8px 16px;\n    background: ${(props) => props.theme.console.headerBg};\n    border-bottom: 1px solid ${(props) => props.theme.console.border};\n    flex-shrink: 0;\n  }\n\n  .panel-title {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    color: ${(props) => props.theme.console.titleColor};\n    font-size: ${(props) => props.theme.font.size.base};\n    font-weight: 500;\n\n    .error-time {\n      color: ${(props) => props.theme.console.countColor};\n      font-size: ${(props) => props.theme.font.size.xs};\n      font-weight: 400;\n    }\n  }\n\n  .close-button {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 24px;\n    height: 24px;\n    background: transparent;\n    border: none;\n    border-radius: 4px;\n    color: ${(props) => props.theme.console.buttonColor};\n    cursor: pointer;\n    transition: all 0.2s ease;\n\n    &:hover {\n      background: ${(props) => props.theme.console.buttonHoverBg};\n      color: ${(props) => props.theme.console.buttonHoverColor};\n    }\n  }\n\n  .panel-tabs {\n    display: flex;\n    background: ${(props) => props.theme.console.headerBg};\n    border-bottom: 1px solid ${(props) => props.theme.console.border};\n    flex-shrink: 0;\n  }\n\n  .tab-button {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    padding: 8px 16px;\n    background: transparent;\n    border: none;\n    border-bottom: 2px solid transparent;\n    color: ${(props) => props.theme.console.buttonColor};\n    cursor: pointer;\n    transition: all 0.2s ease;\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-weight: 500;\n\n    &:hover {\n      background: ${(props) => props.theme.console.buttonHoverBg};\n      color: ${(props) => props.theme.console.buttonHoverColor};\n    }\n\n    &.active {\n      color: ${(props) => props.theme.console.checkboxColor};\n      border-bottom-color: ${(props) => props.theme.console.checkboxColor};\n      background: ${(props) => props.theme.console.contentBg};\n    }\n  }\n\n  .panel-content {\n    flex: 1;\n    overflow-y: auto;\n    overflow-x: hidden;\n    background: ${(props) => props.theme.console.contentBg};\n    min-height: 0;\n  }\n\n  .tab-content {\n    padding: 16px;\n    height: 100%;\n    overflow-y: auto;\n  }\n\n  .section {\n    margin-bottom: 24px;\n\n    &:last-child {\n      margin-bottom: 0;\n    }\n\n    h4 {\n      margin: 0 0 12px 0;\n      font-size: ${(props) => props.theme.font.size.base};\n      font-weight: 500;\n      color: ${(props) => props.theme.console.titleColor};\n      text-transform: uppercase;\n      letter-spacing: 0.5px;\n    }\n  }\n\n  .info-grid {\n    display: flex;\n    flex-direction: column;\n    gap: 12px;\n  }\n\n  .info-item {\n    display: flex;\n    flex-direction: column;\n    gap: 4px;\n\n    label {\n      font-size: ${(props) => props.theme.font.size.xs};\n      font-weight: 500;\n      color: ${(props) => props.theme.console.titleColor};\n      text-transform: uppercase;\n      letter-spacing: 0.5px;\n    }\n\n    span {\n      font-size: ${(props) => props.theme.font.size.sm};\n      color: ${(props) => props.theme.console.messageColor};\n      font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n      word-break: break-all;\n      line-height: 1.4;\n    }\n  }\n\n  .error-message-full {\n    color: ${(props) => props.theme.console.messageColor} !important;\n    background: ${(props) => props.theme.console.headerBg};\n    padding: 8px 12px;\n    border-radius: 4px;\n    border: 1px solid ${(props) => props.theme.console.border};\n  }\n\n  .file-path {\n    color: ${(props) => props.theme.console.checkboxColor} !important;\n    font-weight: 500 !important;\n  }\n\n  .report-section {\n    display: flex;\n    flex-direction: column;\n    gap: 12px;\n\n    p {\n      margin: 0;\n      font-size: ${(props) => props.theme.font.size.sm};\n      color: ${(props) => props.theme.console.messageColor};\n      line-height: 1.4;\n    }\n  }\n\n  .report-button {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    padding: 8px 16px;\n    background: ${(props) => props.theme.console.buttonHoverBg};\n    border: 1px solid ${(props) => props.theme.console.border};\n    border-radius: 6px;\n    color: ${(props) => props.theme.console.buttonColor};\n    cursor: pointer;\n    transition: all 0.2s ease;\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-weight: 500;\n    text-decoration: none;\n    align-self: flex-start;\n\n    &:hover {\n      background: ${(props) => props.theme.console.checkboxColor};\n      color: white;\n      border-color: ${(props) => props.theme.console.checkboxColor};\n    }\n\n    span {\n      font-family: inherit;\n    }\n  }\n\n  .stack-trace-container,\n  .arguments-container {\n    background: ${(props) => props.theme.console.headerBg};\n    border: 1px solid ${(props) => props.theme.console.border};\n    border-radius: 6px;\n    overflow: hidden;\n  }\n\n  .stack-trace,\n  .arguments {\n    margin: 0;\n    padding: 16px;\n    font-size: ${(props) => props.theme.font.size.xs};\n    line-height: 1.5;\n    color: ${(props) => props.theme.console.messageColor};\n    background: transparent;\n    font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n    white-space: pre-wrap;\n    word-break: break-word;\n    overflow-x: auto;\n    max-height: 400px;\n    overflow-y: auto;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Devtools/Console/ErrorDetailsPanel/index.js",
    "content": "import React, { useState } from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport {\n  IconX,\n  IconBug,\n  IconFileText,\n  IconCode,\n  IconStack,\n  IconBrandGithub\n} from '@tabler/icons';\nimport { clearSelectedError } from 'providers/ReduxStore/slices/logs';\nimport { useApp } from 'providers/App';\nimport platformLib from 'platform';\nimport StyledWrapper from './StyledWrapper';\n\nconst ErrorInfoTab = ({ error }) => {\n  const { version } = useApp();\n\n  const formatTimestamp = (timestamp) => {\n    const date = new Date(timestamp);\n    return date.toLocaleString();\n  };\n\n  const generateGitHubIssueUrl = () => {\n    const title = `Bug: ${error.message.substring(0, 50)}${error.message.length > 50 ? '...' : ''}`;\n\n    const body = `## Bug Report\n\n### Error Details\n- **Message**: ${error.message}\n- **File**: ${error.filename || 'Unknown'}\n- **Line**: ${error.lineno || 'Unknown'}:${error.colno || 'Unknown'}\n- **Timestamp**: ${formatTimestamp(error.timestamp)}\n\n### Environment\n- **Bruno Version**: ${version}\n- **OS**: ${platformLib.os.family} ${platformLib.os.version || ''}\n- **Browser**: ${platformLib.name} ${platformLib.version || ''}\n\n### Stack Trace\n\\`\\`\\`\n${error.stack || 'No stack trace available'}\n\\`\\`\\`\n\n### Arguments\n\\`\\`\\`\n${error.args ? error.args.map((arg, index) => {\n  if (arg && typeof arg === 'object' && arg.__type === 'Error') {\n    return `[${index}]: Error: ${arg.message}`;\n  }\n  return `[${index}]: ${typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)}`;\n}).join('\\n') : 'No arguments'}\n\\`\\`\\`\n\n### Steps to Reproduce\n1. \n2. \n3. \n\n### Expected Behavior\n\n\n### Additional Context\n\n`;\n\n    const encodedTitle = encodeURIComponent(title);\n    const encodedBody = encodeURIComponent(body);\n\n    return `https://github.com/usebruno/bruno/issues/new?template=BLANK_ISSUE&title=${encodedTitle}&body=${encodedBody}`;\n  };\n\n  const handleReportIssue = () => {\n    const url = generateGitHubIssueUrl();\n    window.open(url, '_blank');\n  };\n\n  return (\n    <div className=\"tab-content\">\n      <div className=\"section\">\n        <h4>Error Information</h4>\n        <div className=\"info-grid\">\n          <div className=\"info-item\">\n            <label>Message:</label>\n            <span className=\"error-message-full\">{error.message || 'No message available'}</span>\n          </div>\n\n          {error.filename && (\n            <div className=\"info-item\">\n              <label>File:</label>\n              <span className=\"file-path\">{error.filename}</span>\n            </div>\n          )}\n\n          {error.lineno && (\n            <div className=\"info-item\">\n              <label>Line:</label>\n              <span>{error.lineno}{error.colno ? `:${error.colno}` : ''}</span>\n            </div>\n          )}\n\n          <div className=\"info-item\">\n            <label>Timestamp:</label>\n            <span>{formatTimestamp(error.timestamp)}</span>\n          </div>\n        </div>\n      </div>\n\n      <div className=\"section\">\n        <h4>Report Issue</h4>\n        <div className=\"report-section\">\n          <p>Found a bug? Help us improve Bruno by reporting this error on GitHub.</p>\n          <button\n            className=\"report-button\"\n            onClick={handleReportIssue}\n            title=\"Report this error on GitHub\"\n          >\n            <IconBrandGithub size={16} strokeWidth={1.5} />\n            <span>Report Issue on GitHub</span>\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nconst StackTraceTab = ({ error }) => {\n  const formatStackTrace = (stack) => {\n    if (!stack) return 'Stack trace not available';\n\n    return stack\n      .split('\\n')\n      .map((line) => line.trim())\n      .filter((line) => line.length > 0)\n      .join('\\n');\n  };\n\n  return (\n    <div className=\"tab-content\">\n      <div className=\"section\">\n        <h4>Stack Trace</h4>\n        <div className=\"stack-trace-container\">\n          <pre className=\"stack-trace\">\n            {formatStackTrace(error.stack)}\n          </pre>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nconst ArgumentsTab = ({ error }) => {\n  const formatArguments = (args) => {\n    if (!args || args.length === 0) return 'No arguments available';\n\n    try {\n      return args.map((arg, index) => {\n        // Handle special Error object format\n        if (arg && typeof arg === 'object' && arg.__type === 'Error') {\n          return `[${index}]: Error: ${arg.message}\\n  Name: ${arg.name}\\n  Stack: ${arg.stack || 'No stack trace'}`;\n        }\n\n        if (typeof arg === 'object' && arg !== null) {\n          return `[${index}]: ${JSON.stringify(arg, null, 2)}`;\n        }\n\n        return `[${index}]: ${String(arg)}`;\n      }).join('\\n\\n');\n    } catch (e) {\n      return 'Arguments could not be formatted';\n    }\n  };\n\n  return (\n    <div className=\"tab-content\">\n      <div className=\"section\">\n        <h4>Arguments</h4>\n        <div className=\"arguments-container\">\n          <pre className=\"arguments\">\n            {formatArguments(error.args)}\n          </pre>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nconst ErrorDetailsPanel = () => {\n  const dispatch = useDispatch();\n  const { selectedError } = useSelector((state) => state.logs);\n  const [activeTab, setActiveTab] = useState('info');\n\n  if (!selectedError) return null;\n\n  const handleClose = () => {\n    dispatch(clearSelectedError());\n  };\n\n  const formatTime = (timestamp) => {\n    const date = new Date(timestamp);\n    return date.toLocaleString();\n  };\n\n  const getTabContent = () => {\n    switch (activeTab) {\n      case 'info':\n        return <ErrorInfoTab error={selectedError} />;\n      case 'stack':\n        return <StackTraceTab error={selectedError} />;\n      case 'args':\n        return <ArgumentsTab error={selectedError} />;\n      default:\n        return <ErrorInfoTab error={selectedError} />;\n    }\n  };\n\n  return (\n    <StyledWrapper>\n      <div className=\"panel-header\">\n        <div className=\"panel-title\">\n          <IconBug size={16} strokeWidth={1.5} />\n          <span>Error Details</span>\n          <span className=\"error-time\">({formatTime(selectedError.timestamp)})</span>\n        </div>\n\n        <button\n          className=\"close-button\"\n          onClick={handleClose}\n          title=\"Close details panel\"\n        >\n          <IconX size={16} strokeWidth={1.5} />\n        </button>\n      </div>\n\n      <div className=\"panel-tabs\">\n        <button\n          className={`tab-button ${activeTab === 'info' ? 'active' : ''}`}\n          onClick={() => setActiveTab('info')}\n        >\n          <IconFileText size={14} strokeWidth={1.5} />\n          Info\n        </button>\n\n        <button\n          className={`tab-button ${activeTab === 'stack' ? 'active' : ''}`}\n          onClick={() => setActiveTab('stack')}\n        >\n          <IconStack size={14} strokeWidth={1.5} />\n          Stack\n        </button>\n\n        <button\n          className={`tab-button ${activeTab === 'args' ? 'active' : ''}`}\n          onClick={() => setActiveTab('args')}\n        >\n          <IconCode size={14} strokeWidth={1.5} />\n          Args\n        </button>\n      </div>\n\n      <div className=\"panel-content\">\n        {getTabContent()}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default ErrorDetailsPanel;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Devtools/Console/NetworkTab/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n  background: ${(props) => props.theme.console.contentBg};\n  overflow: hidden;\n\n  .network-header {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 8px 16px;\n    background: ${(props) => props.theme.console.headerBg};\n    border-bottom: 1px solid ${(props) => props.theme.console.border};\n    flex-shrink: 0;\n  }\n\n  .network-title {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    color: ${(props) => props.theme.console.titleColor};\n    font-size: ${(props) => props.theme.font.size.base};\n    font-weight: 500;\n\n    .request-count {\n      color: ${(props) => props.theme.console.countColor};\n      font-size: ${(props) => props.theme.font.size.sm};\n      font-weight: 400;\n    }\n  }\n\n  .network-content {\n    flex: 1;\n    overflow: hidden;\n    display: flex;\n    flex-direction: column;\n    min-height: 0; /* Important for proper flex behavior */\n  }\n\n  .network-empty {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    height: 100%;\n    color: ${(props) => props.theme.console.emptyColor};\n    text-align: center;\n    gap: 8px;\n    padding: 40px 20px;\n\n    p {\n      margin: 0;\n      font-size: ${(props) => props.theme.font.size.base};\n      font-weight: 500;\n    }\n\n    span {\n      font-size: ${(props) => props.theme.font.size.sm};\n      opacity: 0.7;\n    }\n  }\n\n  .requests-container {\n    display: flex;\n    flex-direction: column;\n    height: 100%;\n    overflow: hidden;\n    min-height: 0; /* Important for proper flex behavior */\n  }\n\n  .requests-header {\n    display: grid;\n    grid-template-columns: 80px 80px 150px 1fr 100px 80px 80px;\n    gap: 12px;\n    padding: 4px 16px;\n    background: ${(props) => props.theme.console.headerBg};\n    border-bottom: 1px solid ${(props) => props.theme.console.border};\n    font-size: 10px;\n    color: ${(props) => props.theme.console.titleColor};\n    text-transform: uppercase;\n    letter-spacing: 0.5px;\n    flex-shrink: 0;\n  }\n\n  .requests-list {\n    flex: 1;\n    overflow-y: auto;\n    overflow-x: hidden;\n    min-height: 0; /* Important for proper scrolling */\n  }\n\n  .request-row {\n    display: grid;\n    grid-template-columns: 80px 80px 150px 1fr 100px 80px 80px;\n    gap: 12px;\n    padding: 2px 16px;\n    cursor: pointer;\n    transition: background-color 0.1s ease;\n    font-size: ${(props) => props.theme.font.size.sm};\n    align-items: center;\n\n    &:hover {\n      background: ${(props) => props.theme.console.logHoverBg};\n    }\n\n    &.selected {\n      padding-left: 13px;\n      background: ${(props) => props.theme.console.logHoverBg};\n      border-left: 3px solid ${(props) => props.theme.console.checkboxColor};\n    }\n  }\n\n  .method-badge {\n    display: inline-flex;\n    align-items: center;\n    justify-content: start;\n    font-size: 10px;\n    text-transform: uppercase;\n    letter-spacing: 0.5px;\n    min-width: 45px;\n  }\n\n  .status-badge {\n    font-size: ${(props) => props.theme.font.size.sm};\n  }\n\n  .request-domain {\n    color: ${(props) => props.theme.console.messageColor};\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n\n  .request-path {\n    color: ${(props) => props.theme.console.messageColor};\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n  }\n\n  .request-time {\n    color: ${(props) => props.theme.console.timestampColor};\n    font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n    font-size: ${(props) => props.theme.font.size.xs};\n  }\n\n  .request-duration {\n    color: ${(props) => props.theme.console.messageColor};\n    font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n    font-size: ${(props) => props.theme.font.size.xs};\n    text-align: right;\n  }\n\n  .request-size {\n    color: ${(props) => props.theme.console.messageColor};\n    font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n    font-size: ${(props) => props.theme.font.size.xs};\n    text-align: right;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Devtools/Console/NetworkTab/index.js",
    "content": "import React, { useMemo } from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport {\n  IconNetwork\n} from '@tabler/icons';\nimport {\n  setSelectedRequest\n} from 'providers/ReduxStore/slices/logs';\nimport StyledWrapper from './StyledWrapper';\n\nconst MethodBadge = ({ method }) => {\n  const methodLower = method?.toLowerCase() || 'get';\n\n  return (\n    <span className={`method-badge ${methodLower}`}>\n      {method?.toUpperCase() || 'GET'}\n    </span>\n  );\n};\n\nconst StatusBadge = ({ status, statusCode }) => {\n  const displayStatus = statusCode || status;\n\n  return (\n    <span className=\"status-badge\">\n      {displayStatus}\n    </span>\n  );\n};\n\nconst RequestRow = ({ request, isSelected, onClick }) => {\n  const { data } = request;\n  const { request: req, response: res, timestamp } = data;\n\n  const formatTime = (timestamp) => {\n    const date = new Date(timestamp);\n    return date.toLocaleTimeString('en-US', {\n      hour12: false,\n      hour: '2-digit',\n      minute: '2-digit',\n      second: '2-digit',\n      fractionalSecondDigits: 3\n    });\n  };\n\n  const formatDuration = (duration) => {\n    if (!duration) return '-';\n    if (duration < 1000) return `${Math.round(duration)}ms`;\n    return `${(duration / 1000).toFixed(2)}s`;\n  };\n\n  const formatSize = (size) => {\n    if (!size) return '-';\n    if (size < 1024) return `${size}B`;\n    if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)}KB`;\n    return `${(size / (1024 * 1024)).toFixed(1)}MB`;\n  };\n\n  const getUrl = () => {\n    return req?.url || 'Unknown URL';\n  };\n\n  const getDomain = () => {\n    try {\n      const url = new URL(getUrl());\n      return url.hostname;\n    } catch {\n      return getUrl();\n    }\n  };\n\n  const getPath = () => {\n    try {\n      const url = new URL(getUrl());\n      return url.pathname + url.search;\n    } catch {\n      return getUrl();\n    }\n  };\n\n  return (\n    <div\n      className={`request-row ${isSelected ? 'selected' : ''}`}\n      onClick={onClick}\n    >\n      <div className=\"request-method\">\n        <MethodBadge method={req?.method} />\n      </div>\n\n      <div className=\"request-status\">\n        <StatusBadge status={res?.status} statusCode={res?.statusCode} />\n      </div>\n\n      <div className=\"request-domain\" title={getDomain()}>\n        {getDomain()}\n      </div>\n\n      <div className=\"request-path\" title={getPath()}>\n        {getPath()}\n      </div>\n\n      <div className=\"request-time\">\n        {formatTime(timestamp)}\n      </div>\n\n      <div className=\"request-duration\">\n        {formatDuration(res?.duration)}\n      </div>\n\n      <div className=\"request-size\">\n        {formatSize(res?.size)}\n      </div>\n    </div>\n  );\n};\n\nconst NetworkTab = () => {\n  const dispatch = useDispatch();\n  const { networkFilters, selectedRequest } = useSelector((state) => state.logs);\n  const collections = useSelector((state) => state.collections.collections);\n\n  const allRequests = useMemo(() => {\n    const requests = [];\n\n    collections.forEach((collection) => {\n      if (collection.timeline) {\n        collection.timeline\n          .filter((entry) => entry.type === 'request')\n          .forEach((entry) => {\n            requests.push({\n              ...entry,\n              collectionName: collection.name,\n              collectionUid: collection.uid\n            });\n          });\n      }\n    });\n\n    return requests.sort((a, b) => a.timestamp - b.timestamp);\n  }, [collections]);\n\n  const filteredRequests = useMemo(() => {\n    return allRequests.filter((request) => {\n      const method = request.data?.request?.method?.toUpperCase() || 'GET';\n      return networkFilters[method];\n    });\n  }, [allRequests, networkFilters]);\n\n  const handleRequestClick = (request) => {\n    dispatch(setSelectedRequest(request));\n  };\n\n  return (\n    <StyledWrapper>\n      <div className=\"network-content\">\n        {filteredRequests.length === 0 ? (\n          <div className=\"network-empty\">\n            <IconNetwork size={48} strokeWidth={1} />\n            <p>No network requests</p>\n            <span>Requests will appear here as you make API calls</span>\n          </div>\n        ) : (\n          <div className=\"requests-container\">\n            <div className=\"requests-header\">\n              <div>Method</div>\n              <div>Status</div>\n              <div>Domain</div>\n              <div>Path</div>\n              <div>Time</div>\n              <div className=\"text-right\">Duration</div>\n              <div className=\"text-right\">Size</div>\n            </div>\n\n            <div className=\"requests-list\">\n              {filteredRequests.map((request, index) => (\n                <RequestRow\n                  key={`${request.collectionUid}-${request.itemUid}-${request.timestamp}-${index}`}\n                  request={request}\n                  isSelected={selectedRequest?.timestamp === request.timestamp && selectedRequest?.itemUid === request.itemUid}\n                  onClick={() => handleRequestClick(request)}\n                />\n              ))}\n            </div>\n          </div>\n        )}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default NetworkTab;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n  background: ${(props) => props.theme.console.contentBg};\n  border-left: 1px solid ${(props) => props.theme.console.border};\n  min-width: 400px;\n  max-width: 600px;\n  width: 40%;\n  overflow: hidden;\n\n  .panel-header {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 2px 8px;\n    background: ${(props) => props.theme.console.headerBg};\n    border-bottom: 1px solid ${(props) => props.theme.console.border};\n    flex-shrink: 0;\n  }\n\n  .panel-title {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    color: ${(props) => props.theme.console.titleColor};\n    font-size: ${(props) => props.theme.font.size.base};\n\n    .request-time {\n      color: ${(props) => props.theme.console.countColor};\n      font-size: ${(props) => props.theme.font.size.xs};\n      font-weight: 400;\n    }\n  }\n\n  .close-button {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 24px;\n    height: 24px;\n    background: transparent;\n    border: none;\n    border-radius: 4px;\n    color: ${(props) => props.theme.console.buttonColor};\n    cursor: pointer;\n    transition: all 0.2s ease;\n\n    &:hover {\n      background: ${(props) => props.theme.console.buttonHoverBg};\n      color: ${(props) => props.theme.console.buttonHoverColor};\n    }\n  }\n\n  .panel-tabs {\n    display: flex;\n    background: ${(props) => props.theme.console.headerBg};\n    border-bottom: 1px solid ${(props) => props.theme.console.border};\n    flex-shrink: 0;\n  }\n\n  .tab-button {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    padding: 4px 8px;\n    background: transparent;\n    border: none;\n    border-bottom: 2px solid transparent;\n    color: ${(props) => props.theme.console.buttonColor};\n    cursor: pointer;\n    transition: all 0.2s ease;\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-weight: 500;\n\n    &:hover {\n      background: ${(props) => props.theme.console.buttonHoverBg};\n      color: ${(props) => props.theme.console.buttonHoverColor};\n    }\n\n    &.active {\n      color: ${(props) => props.theme.console.checkboxColor};\n      border-bottom-color: ${(props) => props.theme.console.checkboxColor};\n      background: ${(props) => props.theme.console.contentBg};\n    }\n  }\n\n  .panel-content {\n    flex: 1;\n    overflow-y: auto;\n    overflow-x: hidden;\n    padding: 8px;\n    min-height: 0;\n    height: 0;\n  }\n\n  .tab-content {\n    display: flex;\n    flex-direction: column;\n    gap: 20px;\n    min-height: min-content;\n  }\n\n  .section {\n    display: flex;\n    flex-direction: column;\n    gap: 8px;\n\n    h4 {\n      margin: 0;\n      font-size: ${(props) => props.theme.font.size.base};\n      font-weight: 500;\n      color: ${(props) => props.theme.console.titleColor};\n      padding-bottom: 4px;\n      border-bottom: 1px solid ${(props) => props.theme.console.border};\n    }\n  }\n\n  .info-grid {\n    display: flex;\n    flex-direction: column;\n    gap: 8px;\n  }\n\n  .info-item {\n    display: flex;\n    flex-direction: column;\n    gap: 2px;\n\n    .label {\n      font-size: ${(props) => props.theme.font.size.xs};\n      font-weight: 500;\n      color: ${(props) => props.theme.console.countColor};\n      text-transform: uppercase;\n      letter-spacing: 0.5px;\n    }\n\n    .value {\n      font-size: ${(props) => props.theme.font.size.sm};\n      color: ${(props) => props.theme.console.messageColor};\n      font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n      word-break: break-all;\n      padding: 4px 8px;\n      background: ${(props) => props.theme.console.headerBg};\n      border-radius: 4px;\n      border: 1px solid ${(props) => props.theme.console.border};\n    }\n  }\n\n  .headers-table,\n  .timeline-table {\n    overflow: auto;\n    border-radius: 4px;\n    border: 1px solid ${(props) => props.theme.console.border};\n    max-height: 300px;\n\n    table {\n      width: 100%;\n      border-collapse: collapse;\n      font-size: ${(props) => props.theme.font.size.sm};\n      background: ${(props) => props.theme.console.headerBg};\n\n      thead {\n        background: ${(props) => props.theme.console.dropdownHeaderBg};\n        position: sticky;\n        top: 0;\n        z-index: 10;\n\n        td {\n          padding: 4px 8px;\n          color: ${(props) => props.theme.console.titleColor};\n          text-transform: uppercase;\n          font-size: ${(props) => props.theme.font.size.xs};\n          letter-spacing: 0.5px;\n          border-bottom: 1px solid ${(props) => props.theme.console.border};\n        }\n      }\n\n      tbody {\n        tr {\n          border-bottom: 1px solid ${(props) => props.theme.console.border};\n\n          &:last-child {\n            border-bottom: none;\n          }\n\n          &:nth-child(odd) {\n            background: ${(props) => props.theme.console.contentBg};\n          }\n\n          &:hover {\n            background: ${(props) => props.theme.console.logHoverBg};\n          }\n        }\n\n        td {\n          padding: 2px 8px;\n          vertical-align: top;\n          word-break: break-word;\n        }\n      }\n    }\n  }\n\n  .header-name,\n  .timeline-phase {\n    color: ${(props) => props.theme.console.countColor};\n    font-weight: 500;\n    font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n    min-width: 120px;\n  }\n\n  .header-value,\n  .timeline-message {\n    color: ${(props) => props.theme.console.messageColor};\n    font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n    word-break: break-all;\n  }\n\n  .timeline-duration {\n    color: ${(props) => props.theme.console.timestampColor};\n    font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n    text-align: right;\n    min-width: 80px;\n  }\n\n  .code-block {\n    background: ${(props) => props.theme.console.headerBg};\n    border: 1px solid ${(props) => props.theme.console.border};\n    border-radius: 4px;\n    padding: 12px;\n    font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n    font-size: ${(props) => props.theme.font.size.xs};\n    line-height: 1.4;\n    color: ${(props) => props.theme.console.messageColor};\n    overflow: auto;\n    white-space: pre-wrap;\n    word-break: break-word;\n    max-height: 400px;\n    margin: 0;\n  }\n\n  .empty-state {\n    padding: 12px;\n    text-align: center;\n    color: ${(props) => props.theme.console.emptyColor};\n    font-style: italic;\n    font-size: ${(props) => props.theme.font.size.sm};\n    background: ${(props) => props.theme.console.headerBg};\n    border: 1px solid ${(props) => props.theme.console.border};\n    border-radius: 4px;\n  }\n\n  .response-body-container {\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    overflow: hidden;\n    height: 400px;\n    display: flex;\n    flex-direction: column;\n\n    .w-full.h-full.relative.flex {\n      height: 100% !important;\n      width: 100% !important;\n      display: flex !important;\n      flex-direction: column !important;\n    }\n\n    div[role=\"tablist\"] {\n      padding: 4px 8px;\n      border-bottom: 1px solid ${(props) => props.theme.console.border};\n      display: flex !important;\n      gap: 8px !important;\n      flex-wrap: wrap !important;\n      align-items: center !important;\n      min-height: 40px !important;\n      flex-shrink: 0 !important;\n\n      > div {\n        color: ${(props) => props.theme.console.buttonColor};\n        font-size: ${(props) => props.theme.font.size.sm} !important;\n        cursor: pointer;\n        white-space: nowrap !important;\n        min-width: auto !important;\n        height: auto !important;\n        line-height: 1.2 !important;\n        font-weight: 500 !important;\n\n        &.active {\n          background: ${(props) => props.theme.console.checkboxColor};\n          color: white;\n          border-color: ${(props) => props.theme.console.checkboxColor};\n        }\n      }\n    }\n    .response-filter {\n      position: absolute !important;\n      bottom: 8px !important;\n      right: 8px !important;\n      left: 8px !important;\n      z-index: 10 !important;\n    }\n  }\n\n  .network-logs-wrapper {\n    border: 1px solid ${(props) => props.theme.console.border};\n    border-radius: 4px;\n    overflow: hidden;\n    background: ${(props) => props.theme.console.headerBg};\n    min-height: 200px;\n    max-height: 400px;\n\n    .network-logs-container {\n      background: ${(props) => props.theme.console.contentBg} !important;\n      color: ${(props) => props.theme.console.messageColor} !important;\n      height: 100% !important;\n      max-height: 400px !important;\n      padding: 0.5rem !important;\n\n      .network-logs-pre {\n        color: ${(props) => props.theme.console.messageColor} !important;\n        font-size: ${(props) => props.theme.font.size.xs} !important;\n        line-height: 1.4 !important;\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Devtools/Console/RequestDetailsPanel/index.js",
    "content": "import React, { useState } from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport {\n  IconX,\n  IconFileText,\n  IconArrowRight,\n  IconNetwork\n} from '@tabler/icons';\nimport { clearSelectedRequest } from 'providers/ReduxStore/slices/logs';\nimport QueryResponse from 'components/ResponsePane/QueryResponse/index';\nimport Network from 'components/ResponsePane/Timeline/TimelineItem/Network';\nimport StyledWrapper from './StyledWrapper';\nimport { uuid } from 'utils/common/index';\n\nconst RequestTab = ({ request, response }) => {\n  const formatHeaders = (headers) => {\n    if (!headers) return [];\n    if (Array.isArray(headers)) return headers;\n    return Object.entries(headers).map(([key, value]) => ({ name: key, value }));\n  };\n\n  const formatBody = (body) => {\n    if (!body) return 'No body';\n    if (typeof body === 'string') return body;\n    return JSON.stringify(body, null, 2);\n  };\n\n  return (\n    <div className=\"tab-content\">\n      <div className=\"section\">\n        <h4>General</h4>\n        <div className=\"info-grid\">\n          <div className=\"info-item\">\n            <span className=\"label\">Request URL:</span>\n            <span className=\"value\">{request?.url || 'N/A'}</span>\n          </div>\n          <div className=\"info-item\">\n            <span className=\"label\">Request Method:</span>\n            <span className=\"value\">{request?.method || 'GET'}</span>\n          </div>\n        </div>\n      </div>\n\n      <div className=\"section\">\n        <h4>Request Headers</h4>\n        {formatHeaders(request?.headers).length > 0 ? (\n          <div className=\"headers-table\">\n            <table>\n              <thead>\n                <tr>\n                  <td>Name</td>\n                  <td>Value</td>\n                </tr>\n              </thead>\n              <tbody>\n                {formatHeaders(request.headers).map((header, index) => (\n                  <tr key={index}>\n                    <td className=\"header-name\">{header.name}</td>\n                    <td className=\"header-value\">{header.value}</td>\n                  </tr>\n                ))}\n              </tbody>\n            </table>\n          </div>\n        ) : (\n          <div className=\"empty-state\">No headers</div>\n        )}\n      </div>\n\n      {request?.data && (\n        <div className=\"section\">\n          <h4>Request Body</h4>\n          <pre className=\"code-block\">{formatBody(request.data)}</pre>\n        </div>\n      )}\n    </div>\n  );\n};\n\nconst ResponseTab = ({ response, request, collection }) => {\n  const formatHeaders = (headers) => {\n    if (!headers) return [];\n    if (Array.isArray(headers)) return headers;\n    return Object.entries(headers).map(([key, value]) => ({ name: key, value }));\n  };\n\n  return (\n    <div className=\"tab-content\">\n      <div className=\"section\">\n        <h4>Response Headers</h4>\n        {formatHeaders(response?.headers).length > 0 ? (\n          <div className=\"headers-table\">\n            <table>\n              <thead>\n                <tr>\n                  <td>Name</td>\n                  <td>Value</td>\n                </tr>\n              </thead>\n              <tbody>\n                {formatHeaders(response.headers).map((header, index) => (\n                  <tr key={index}>\n                    <td className=\"header-name\">{header.name}</td>\n                    <td className=\"header-value\">{header.value}</td>\n                  </tr>\n                ))}\n              </tbody>\n            </table>\n          </div>\n        ) : (\n          <div className=\"empty-state\">No headers</div>\n        )}\n      </div>\n\n      <div className=\"section\">\n        <h4>Response Body</h4>\n        <div className=\"response-body-container\">\n          {response?.data || response?.dataBuffer ? (\n            <QueryResponse\n              item={{ uid: uuid() }}\n              collection={collection}\n              data={response.data}\n              dataBuffer={response.dataBuffer}\n              headers={response.headers}\n              error={response.error}\n              disableRunEventListener={true}\n            />\n          ) : (\n            <div className=\"empty-state\">No response data</div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nconst NetworkTab = ({ response }) => {\n  const timeline = response?.timeline || [];\n\n  return (\n    <div className=\"tab-content\">\n      <div className=\"section\">\n        <h4>Network Logs</h4>\n        <div className=\"network-logs-wrapper\">\n          {timeline.length > 0 ? (\n            <Network logs={timeline} />\n          ) : (\n            <div className=\"empty-state\">No network logs available</div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nconst RequestDetailsPanel = () => {\n  const dispatch = useDispatch();\n  const { selectedRequest } = useSelector((state) => state.logs);\n  const collections = useSelector((state) => state.collections.collections);\n  const [activeTab, setActiveTab] = useState('request');\n\n  if (!selectedRequest) return null;\n\n  const { data } = selectedRequest;\n  const { request, response } = data;\n\n  const collection = collections.find((c) => c.uid === selectedRequest.collectionUid);\n\n  const handleClose = () => {\n    dispatch(clearSelectedRequest());\n  };\n\n  const formatTime = (timestamp) => {\n    const date = new Date(timestamp);\n    return date.toLocaleString();\n  };\n\n  const getTabContent = () => {\n    switch (activeTab) {\n      case 'request':\n        return <RequestTab request={request} response={response} />;\n      case 'response':\n        return <ResponseTab response={response} request={request} collection={collection} />;\n      case 'network':\n        return <NetworkTab response={response} />;\n      default:\n        return <RequestTab request={request} response={response} />;\n    }\n  };\n\n  return (\n    <StyledWrapper>\n      <div className=\"panel-header\">\n        <div className=\"panel-title\">\n          <IconFileText size={16} strokeWidth={1.5} />\n          <span>Request Details</span>\n          <span className=\"request-time\">({formatTime(selectedRequest.timestamp)})</span>\n        </div>\n\n        <button\n          className=\"close-button\"\n          onClick={handleClose}\n          title=\"Close details panel\"\n        >\n          <IconX size={16} strokeWidth={1.5} />\n        </button>\n      </div>\n\n      <div className=\"panel-tabs\">\n        <button\n          className={`tab-button ${activeTab === 'request' ? 'active' : ''}`}\n          onClick={() => setActiveTab('request')}\n        >\n          <IconArrowRight size={14} strokeWidth={1.5} />\n          Request\n        </button>\n\n        <button\n          className={`tab-button ${activeTab === 'response' ? 'active' : ''}`}\n          onClick={() => setActiveTab('response')}\n        >\n          <IconFileText size={14} strokeWidth={1.5} />\n          Response\n        </button>\n\n        <button\n          className={`tab-button ${activeTab === 'network' ? 'active' : ''}`}\n          onClick={() => setActiveTab('network')}\n        >\n          <IconNetwork size={14} strokeWidth={1.5} />\n          Network\n        </button>\n      </div>\n\n      <div className=\"panel-content\">\n        {getTabContent()}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default RequestDetailsPanel;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Devtools/Console/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  width: 100%;\n  height: 100%;\n  background: ${(props) => props.theme.console.bg};\n  border-top: 1px solid ${(props) => props.theme.console.border};\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n\n  .console-header {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 0 8px;\n    background: ${(props) => props.theme.console.headerBg};\n    border-bottom: 1px solid ${(props) => props.theme.console.border};\n    flex-shrink: 0;\n    position: relative;\n  }\n\n  .console-tabs {\n    display: flex;\n    align-items: center;\n    gap: 2px;\n  }\n\n  .console-tab {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    padding: 4px 8px;\n    background: transparent;\n    border: none;\n    border-bottom: 2px solid transparent;\n    color: ${(props) => props.theme.console.buttonColor};\n    cursor: pointer;\n    transition: all 0.2s ease;\n    font-size: ${(props) => props.theme.font.size.sm};\n\n    &:hover {\n      background: ${(props) => props.theme.console.buttonHoverBg};\n      color: ${(props) => props.theme.console.buttonHoverColor};\n    }\n\n    &.active {\n      color: ${(props) => props.theme.primary.strong};\n      border-bottom-color: ${(props) => props.theme.primary.strong};\n      background: ${(props) => props.theme.background.mantle};\n    }\n  }\n\n  .console-controls {\n    display: flex;\n    align-items: center;\n    gap: 4px;\n  }\n\n  .console-content {\n    flex: 1;\n    overflow: hidden;\n    background: ${(props) => props.theme.console.contentBg};\n    min-height: 0;\n    display: flex;\n    flex-direction: column;\n  }\n\n  .tab-content {\n    display: flex;\n    flex-direction: column;\n    height: 100%;\n  }\n\n  .tab-header {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 8px 16px;\n    background: ${(props) => props.theme.console.headerBg};\n    border-bottom: 1px solid ${(props) => props.theme.console.border};\n    flex-shrink: 0;\n  }\n\n  .tab-title {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    color: ${(props) => props.theme.console.titleColor};\n    font-size: ${(props) => props.theme.font.size.base};\n    font-weight: 500;\n\n    .log-count {\n      color: ${(props) => props.theme.console.countColor};\n      font-size: ${(props) => props.theme.font.size.sm};\n      font-weight: 400;\n    }\n  }\n\n  .tab-controls {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n  }\n\n  .tab-content-area {\n    flex: 1;\n    overflow-y: auto;\n    background: ${(props) => props.theme.console.contentBg};\n    min-height: 0;\n  }\n\n  .network-with-details {\n    display: flex;\n    height: 100%;\n    overflow: hidden;\n  }\n\n  .network-main {\n    flex: 1;\n    overflow: hidden;\n    display: flex;\n    flex-direction: column;\n    min-width: 0;\n  }\n\n  .debug-with-details {\n    display: flex;\n    height: 100%;\n    overflow: hidden;\n  }\n\n  .debug-main {\n    flex: 1;\n    overflow: hidden;\n    display: flex;\n    flex-direction: column;\n    min-width: 0;\n  }\n\n  .filter-controls {\n    display: flex;\n    align-items: center;\n    gap: 4px;\n  }\n\n  .action-controls {\n    display: flex;\n    align-items: center;\n    gap: 4px;\n  }\n\n  .control-button {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 24px;\n    height: 24px;\n    background: transparent;\n    border: none;\n    border-radius: 4px;\n    color: ${(props) => props.theme.text};\n    cursor: pointer;\n    transition: all 0.2s ease;\n\n    &:hover {\n      background: ${(props) => props.theme.background.surface0};\n    }\n\n    &.close-button:hover {\n      background: ${(props) => props.theme.background.surface0};\n    }\n  }\n\n  .filter-dropdown {\n    position: relative;\n  }\n\n  .filter-dropdown-trigger {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    padding: 2px 8px;\n    background: transparent;\n    border: 1px solid ${(props) => props.theme.border.border0};\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    color: ${(props) => props.theme.text};\n    cursor: pointer;\n    transition: all 0.2s ease;\n    font-size: ${(props) => props.theme.font.size.sm};\n\n    &:hover {\n      background: ${(props) => props.theme.background.surface0};\n    }\n\n    .filter-summary {\n      font-weight: 500;\n      min-width: 24px;\n      text-align: center;\n    }\n  }\n\n  .filter-dropdown-menu {\n    position: absolute;\n    top: calc(100% + 4px);\n    left: 0;\n    min-width: 200px;\n    max-width: 250px;\n    background: ${(props) => props.theme.console.dropdownBg};\n    border: 1px solid ${(props) => props.theme.console.border};\n    border-radius: 6px;\n    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);\n    z-index: 1000;\n    overflow: hidden;\n    \n    &.right {\n      left: auto;\n      right: 0;\n    }\n  }\n\n  .filter-dropdown-header {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 4px 12px;\n    background: ${(props) => props.theme.console.dropdownHeaderBg};\n    border-bottom: 1px solid ${(props) => props.theme.console.border};\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-weight: 500;\n    color: ${(props) => props.theme.console.titleColor};\n  }\n\n  .filter-toggle-all {\n    background: transparent;\n    border: none;\n    color: ${(props) => props.theme.console.buttonColor};\n    cursor: pointer;\n    font-size: ${(props) => props.theme.font.size.xs};\n    font-weight: 500;\n    padding: 2px 4px;\n    border-radius: 2px;\n    transition: all 0.2s ease;\n\n    &:hover {\n      background: ${(props) => props.theme.console.buttonHoverBg};\n    }\n  }\n\n  .filter-dropdown-options {\n    padding: 4px 0;\n  }\n\n  .filter-option {\n    display: flex;\n    align-items: center;\n    padding: 4px 12px;\n    cursor: pointer;\n    transition: background-color 0.2s ease;\n\n    &:hover {\n      background: ${(props) => props.theme.console.optionHoverBg};\n    }\n\n    input[type=\"checkbox\"] {\n      margin: 0 8px 0 0;\n      width: 14px;\n      height: 14px;\n      accent-color: ${(props) => props.theme.console.checkboxColor};\n    }\n  }\n\n  .filter-option-content {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    flex: 1;\n  }\n\n  .filter-option-label {\n    color: ${(props) => props.theme.console.optionLabelColor};\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-weight: 400;\n  }\n\n  .filter-option-count {\n    color: ${(props) => props.theme.console.optionCountColor};\n    font-size: ${(props) => props.theme.font.size.xs};\n    font-weight: 400;\n    margin-left: auto;\n  }\n\n  .console-empty {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    height: 100%;\n    color: ${(props) => props.theme.console.emptyColor};\n    text-align: center;\n    gap: 8px;\n    padding: 40px 20px;\n\n    p {\n      margin: 0;\n      font-size: ${(props) => props.theme.font.size.base};\n      font-weight: 500;\n    }\n\n    span {\n      font-size: ${(props) => props.theme.font.size.sm};\n      opacity: 0.7;\n    }\n  }\n\n  .logs-container {\n    padding: 8px 0;\n  }\n\n  .log-entry {\n    display: flex;\n    align-items: flex-start;\n    gap: 12px;\n    padding: 4px 16px;\n    font-family: ui-monospace, 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n    font-size: ${(props) => props.theme.font.size.sm};\n    line-height: 1.4;\n    border-left: 2px solid transparent;\n    transition: background-color 0.1s ease;\n\n    &:hover {\n      background: ${(props) => props.theme.console.logHoverBg};\n    }\n\n    &.error {\n      border-left-color: #f14c4c;\n      \n      .log-level {\n        background: #f14c4c;\n        color: white;\n      }\n      \n      .log-icon {\n        color: #f14c4c;\n      }\n    }\n\n    &.warn {\n      border-left-color: #ffcc02;\n      \n      .log-level {\n        background: #ffcc02;\n        color: #000;\n      }\n      \n      .log-icon {\n        color: #ffcc02;\n      }\n    }\n\n    &.info {\n      border-left-color: #0078d4;\n      \n      .log-level {\n        background: #0078d4;\n        color: white;\n      }\n      \n      .log-icon {\n        color: #0078d4;\n      }\n    }\n\n    &.debug {\n      border-left-color: #9b59b6;\n      \n      .log-level {\n        background: #9b59b6;\n        color: white;\n      }\n      \n      .log-icon {\n        color: #9b59b6;\n      }\n    }\n\n    &.log {\n      border-left-color: #6a6a6a;\n      \n      .log-level {\n        background: #6a6a6a;\n        color: white;\n      }\n      \n      .log-icon {\n        color: #6a6a6a;\n      }\n    }\n  }\n\n  .log-meta {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    flex-shrink: 0;\n    min-width: 120px;\n  }\n\n  .log-timestamp {\n    color: ${(props) => props.theme.console.timestampColor};\n    font-size: ${(props) => props.theme.font.size.xs};\n    font-weight: 400;\n  }\n\n  .log-level {\n    font-size: 9px;\n    font-weight: 500;\n    padding: 2px 4px;\n    border-radius: 2px;\n    text-transform: uppercase;\n    letter-spacing: 0.5px;\n  }\n\n  .log-icon {\n    flex-shrink: 0;\n  }\n\n  .log-message {\n    color: ${(props) => props.theme.console.messageColor};\n    white-space: pre-wrap;\n    word-break: break-word;\n    flex: 1;\n    \n    .log-object {\n      margin: 4px 0;\n      padding: 8px;\n      background: ${(props) => props.theme.console.headerBg};\n      border-radius: 4px;\n      border: 1px solid ${(props) => props.theme.console.border};\n      \n      .react-json-view {\n        background: transparent !important;\n        \n        .object-key-val {\n          font-size: ${(props) => props.theme.font.size.sm} !important;\n        }\n        \n        .object-key {\n          color: ${(props) => props.theme.console.messageColor} !important;\n          font-weight: 500 !important;\n        }\n        \n        .object-value {\n          color: ${(props) => props.theme.console.messageColor} !important;\n        }\n        \n        .string-value {\n          color: ${(props) => props.theme.colors?.text?.green || (props.theme.console.messageColor)} !important;\n        }\n        \n        .number-value {\n          color: ${(props) => props.theme.colors?.text?.purple || (props.theme.console.messageColor)} !important;\n        }\n        \n        .boolean-value {\n          color: ${(props) => props.theme.colors?.text?.yellow || (props.theme.console.messageColor)} !important;\n        }\n        \n        .null-value {\n          color: ${(props) => props.theme.colors?.text?.danger || (props.theme.console.messageColor)} !important;\n        }\n        \n        .object-size {\n          color: ${(props) => props.theme.console.timestampColor} !important;\n        }\n        \n        .brace, .bracket {\n          color: ${(props) => props.theme.console.messageColor} !important;\n        }\n        \n        .collapsed-icon, .expanded-icon {\n          color: ${(props) => props.theme.console.checkboxColor} !important;\n        }\n        \n        .icon-container {\n          color: ${(props) => props.theme.console.checkboxColor} !important;\n        }\n        \n        .click-to-expand, .click-to-collapse {\n          color: ${(props) => props.theme.console.checkboxColor} !important;\n        }\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Devtools/Console/TerminalTab/SessionList.js",
    "content": "import React from 'react';\nimport { IconTerminal, IconX } from '@tabler/icons';\nimport styled from 'styled-components';\nimport ToolHint from 'components/ToolHint/index';\n\nconst StyledSessionList = styled.div`\n  .session-list-item {\n    padding: 2px 6px;\n    cursor: pointer;\n    border-bottom: 1px solid ${(props) => props.theme.border || 'rgba(255, 255, 255, 0.05)'};\n    display: flex;\n    flex-direction: column;\n    gap: 4px;\n    position: relative;\n\n    &:hover {\n      background: ${(props) => props.theme.sidebarHover || 'rgba(255, 255, 255, 0.05)'};\n      \n      .session-close-btn {\n        opacity: 1;\n      }\n    }\n\n    &.active {\n      background: ${(props) => props.theme.sidebarActive || 'rgba(59, 142, 234, 0.12)'};\n      border-left: 2px solid ${(props) => props.theme.primary.subtle};\n      padding-left: 4px;\n    }\n\n    &:last-child {\n      border-bottom: none;\n    }\n  }\n\n  .session-close-btn {\n    position: absolute;\n    right: 8px;\n    top: 50%;\n    transform: translateY(-50%);\n    opacity: 0;\n    transition: opacity 0.2s;\n    padding: 4px;\n    cursor: pointer;\n    color: ${(props) => props.theme.textSecondary || '#888'};\n    \n    &:hover {\n      color: ${(props) => props.theme.text};\n      background: ${(props) => props.theme.sidebarHover || 'rgba(255, 255, 255, 0.1)'};\n      border-radius: 4px;\n    }\n  }\n\n  .session-name {\n    font-size: 13px;\n    font-weight: 500;\n    color: ${(props) => props.theme.text};\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    padding-right: 24px;\n    display: flex;\n    align-items: center;\n    gap: 6px;\n  }\n\n  .session-icon {\n    flex-shrink: 0;\n    opacity: 0.7;\n  }\n\n  .session-path {\n    font-size: 11px;\n    color: ${(props) => props.theme.textSecondary || '#888'};\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n`;\n\nconst SessionList = ({ sessions, activeSessionId, onSelectSession, onCloseSession }) => {\n  const getSessionDisplayInfo = (session) => {\n    if (session.name) {\n      return { name: session.name };\n    }\n\n    if (session.cwd) {\n      // Normalize path and get the last directory name\n      const normalizedPath = session.cwd.replace(/\\\\/g, '/').replace(/\\/$/, '');\n      const pathParts = normalizedPath.split('/').filter((p) => p);\n\n      if (pathParts.length > 0) {\n        const folderName = pathParts[pathParts.length - 1];\n        return { name: folderName };\n      }\n\n      // If it's root or home directory\n      if (normalizedPath === '' || normalizedPath === '/' || normalizedPath.match(/^[A-Z]:\\/?$/)) {\n        return { name: 'Root' };\n      }\n    }\n\n    // Fallback: use a cool name based on session ID\n    const shortId = session.sessionId.split('_')[1]?.slice(-6) || session.sessionId.slice(-6);\n    return { name: `Terminal ${shortId}` };\n  };\n\n  const getFullPath = (session) => {\n    if (session.cwd) {\n      return session.cwd;\n    }\n    return '~ (Home Directory)';\n  };\n\n  return (\n    <StyledSessionList>\n      {sessions.map((session) => {\n        const { name } = getSessionDisplayInfo(session);\n        return (\n          <ToolHint\n            key={session.sessionId}\n            text={getFullPath(session)}\n            toolhintId={`session-path-${session.sessionId}`}\n            place=\"bottom-start\"\n            delayShow={100}\n          >\n            <div\n              className={`session-list-item ${activeSessionId === session.sessionId ? 'active' : ''}`}\n              onClick={() => onSelectSession(session.sessionId)}\n            >\n              <div className=\"session-name\">\n                <IconTerminal className=\"session-icon\" size={14} />\n                <span>{name}</span>\n              </div>\n              <div\n                className=\"session-close-btn\"\n                onClick={(e) => {\n                  e.stopPropagation();\n                  onCloseSession(session.sessionId);\n                }}\n              >\n                <IconX size={14} />\n              </div>\n            </div>\n          </ToolHint>\n        );\n      })}\n    </StyledSessionList>\n  );\n};\n\nexport default SessionList;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Devtools/Console/TerminalTab/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  height: 100%;\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n  color: ${(props) => props.theme.text};\n\n  .xterm-rows {\n    color: ${(props) => props.theme.text} !important;\n  }\n\n  .terminal-content {\n    height: 100%;\n    width: 100%;\n    position: relative;\n    display: flex;\n    flex-direction: row;\n  }\n\n  .terminal-sessions-sidebar {\n    width: 200px;\n    min-width: 200px;\n    border-right: 1px solid ${(props) => props.theme.border || 'rgba(255, 255, 255, 0.08)'};\n    background: ${(props) => props.theme.sidebarBackground || props.theme.background};\n    display: flex;\n    flex-direction: column;\n    overflow-y: auto;\n  }\n\n  .terminal-sessions-header {\n    padding: 6px 8px;\n    font-weight: 600;\n    font-size: 13px;\n    color: ${(props) => props.theme.text};\n    border-bottom: 1px solid ${(props) => props.theme.border || 'rgba(255, 255, 255, 0.08)'};\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n  }\n\n  .terminal-sessions-list {\n    flex: 1;\n    overflow-y: auto;\n\n    /* Custom scrollbar styling - subtle */\n    &::-webkit-scrollbar {\n      width: 6px;\n    }\n\n    &::-webkit-scrollbar-track {\n      background: transparent;\n    }\n\n    &::-webkit-scrollbar-thumb {\n      background: rgba(255, 255, 255, 0.1);\n      border-radius: 3px;\n    }\n\n    &::-webkit-scrollbar-thumb:hover {\n      background: rgba(255, 255, 255, 0.15);\n    }\n  }\n\n  .terminal-session-item {\n    padding: 10px 12px;\n    cursor: pointer;\n    border-bottom: 1px solid ${(props) => props.theme.border};\n    transition: background 0.2s;\n    display: flex;\n    flex-direction: column;\n    gap: 4px;\n\n    &:hover {\n      background: ${(props) => props.theme.sidebarHover || 'rgba(255, 255, 255, 0.05)'};\n    }\n\n    &.active {\n      background: ${(props) => props.theme.sidebarActive || 'rgba(59, 142, 234, 0.15)'};\n      border-left: 3px solid ${(props) => props.theme.brandColor || '#3b8eea'};\n    }\n  }\n\n  .terminal-session-name {\n    font-size: 13px;\n    font-weight: 500;\n    color: ${(props) => props.theme.text};\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n\n  .terminal-session-path {\n    font-size: 11px;\n    color: ${(props) => props.theme.textSecondary || '#888'};\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n\n  .terminal-display-container {\n    flex: 1;\n    display: flex;\n    flex-direction: column;\n    position: relative;\n  }\n\n  .terminal-loading {\n    position: absolute;\n    top: 50%;\n    left: 50%;\n    transform: translate(-50%, -50%);\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    gap: 12px;\n    color: #888;\n    font-size: 14px;\n    z-index: 10;\n\n    svg {\n      opacity: 0.7;\n    }\n\n    span {\n      font-weight: 500;\n    }\n  }\n\n  .terminal-container {\n    flex: 1;\n    position: relative;\n    \n    .xterm {\n      height: 100% !important;\n      width: 100% !important;\n      padding: 8px;\n    }\n\n    .xterm-viewport {\n      background: transparent !important;\n    }\n\n    .xterm-screen {\n      background: transparent !important;\n    }\n\n    .xterm-decoration-overview-ruler {\n      display: none;\n    }\n\n    /* Custom scrollbar for terminal */\n    .xterm-viewport::-webkit-scrollbar {\n      width: 8px;\n    }\n\n    .xterm-viewport::-webkit-scrollbar-track {\n      background: rgba(255, 255, 255, 0.05);\n      border-radius: 4px;\n    }\n\n    .xterm-viewport::-webkit-scrollbar-thumb {\n      background: rgba(255, 255, 255, 0.2);\n      border-radius: 4px;\n    }\n\n    .xterm-viewport::-webkit-scrollbar-thumb:hover {\n      background: rgba(255, 255, 255, 0.3);\n    }\n  }\n\n  /* Dark theme adjustments */\n  .xterm-helper-textarea {\n    position: absolute !important;\n    left: -9999px !important;\n    top: -9999px !important;\n  }\n\n  /* Selection styling */\n  .xterm .xterm-selection div {\n    background-color: rgba(255, 255, 255, 0.3) !important;\n  }\n\n  /* Cursor styling */\n  .xterm .xterm-cursor-layer .xterm-cursor {\n    background-color: #d4d4d4 !important;\n  }\n\n  /* Link styling */\n  .xterm .xterm-decoration-link {\n    text-decoration: underline;\n    color: #3b8eea;\n  }\n\n  .xterm .xterm-decoration-link:hover {\n    color: #5ba7f7;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Devtools/Console/TerminalTab/index.js",
    "content": "import React, { useRef, useEffect, useState, useCallback } from 'react';\nimport { Terminal } from '@xterm/xterm';\nimport { FitAddon } from '@xterm/addon-fit';\nimport { IconTerminal2, IconPlus } from '@tabler/icons';\nimport { useTheme } from 'providers/Theme';\nimport StyledWrapper from './StyledWrapper';\nimport SessionList from './SessionList';\nimport '@xterm/xterm/css/xterm.css';\n\n// Build xterm.js theme from app theme\nconst getTerminalTheme = (theme) => {\n  return {\n    background: theme.console.bg,\n    foreground: theme.console.messageColor,\n    cursor: theme.console.messageColor,\n    selectionBackground: theme.status.info.background,\n    black: theme.background.base,\n    red: theme.status.danger.text,\n    green: theme.status.success.text,\n    yellow: theme.status.warning.text,\n    blue: theme.status.info.text,\n    magenta: theme.colors.text.purple,\n    cyan: theme.codemirror.variable.prompt,\n    white: theme.text,\n    brightBlack: theme.colors.text.muted,\n    brightRed: theme.status.danger.text,\n    brightGreen: theme.status.success.text,\n    brightYellow: theme.status.warning.text,\n    brightBlue: theme.status.info.text,\n    brightMagenta: theme.colors.text.purple,\n    brightCyan: theme.codemirror.variable.prompt,\n    brightWhite: theme.text\n  };\n};\n\n// Terminal instances per session - Map<sessionId, { terminal, fitAddon, inputDisposable, resizeDisposable }>\nconst terminalInstances = new Map();\n\n// Data listeners per session - Map<sessionId, { onData, onExit }>\nconst sessionListeners = new Map();\n\n// Parking host for terminal DOM when view unmounts\nlet parkingHost = null;\n\n// Export function to get current session ID (for backward compatibility)\nexport const getSessionId = () => {\n  // Return the first active session ID if any\n  if (terminalInstances.size > 0) {\n    return Array.from(terminalInstances.keys())[0];\n  }\n  return null;\n};\n\nconst ensureParkingHost = () => {\n  if (parkingHost && document.body.contains(parkingHost)) return parkingHost;\n  parkingHost = document.createElement('div');\n  parkingHost.style.display = 'none';\n  parkingHost.setAttribute('data-terminal-parking-host', 'true');\n  document.body.appendChild(parkingHost);\n  return parkingHost;\n};\n\nconst createTerminalForSession = (sessionId, terminalTheme) => {\n  if (terminalInstances.has(sessionId)) {\n    return terminalInstances.get(sessionId);\n  }\n\n  const terminal = new Terminal({\n    cursorBlink: true,\n    fontSize: 14,\n    fontFamily: 'Menlo, Monaco, \"Courier New\", monospace',\n    theme: terminalTheme,\n    allowProposedApi: true\n  });\n\n  const fitAddon = new FitAddon();\n  terminal.loadAddon(fitAddon);\n\n  const inputDisposable = terminal.onData((data) => {\n    if (data && sessionId && window.ipcRenderer) {\n      window.ipcRenderer.send('terminal:input', sessionId, data);\n    }\n  });\n\n  const resizeDisposable = terminal.onResize(({ cols, rows }) => {\n    if (sessionId && window.ipcRenderer) {\n      window.ipcRenderer.send('terminal:resize', sessionId, { cols, rows });\n    }\n  });\n\n  const instance = {\n    terminal,\n    fitAddon,\n    inputDisposable,\n    resizeDisposable\n  };\n\n  terminalInstances.set(sessionId, instance);\n\n  // Setup IPC listeners for this session\n  if (window.ipcRenderer && !sessionListeners.has(sessionId)) {\n    const onData = (data) => {\n      if (!data) return;\n      const instance = terminalInstances.get(sessionId);\n      if (instance && instance.terminal) {\n        try {\n          instance.terminal.write(data);\n        } catch (err) {\n          console.warn('Failed to write terminal data:', err);\n        }\n      }\n    };\n\n    const onExit = ({ exitCode, signal } = {}) => {\n      const msg = `\\r\\n[Process exited with code ${exitCode ?? ''} ${signal ? `(signal ${signal})` : ''}]\\r\\n`;\n      const instance = terminalInstances.get(sessionId);\n      if (instance && instance.terminal) {\n        try {\n          instance.terminal.write(msg);\n        } catch (err) {\n          console.warn('Failed to write terminal exit message:', err);\n        }\n      }\n      // Cleanup on exit\n      cleanupTerminalInstance(sessionId);\n    };\n\n    window.ipcRenderer.on(`terminal:data:${sessionId}`, onData);\n    window.ipcRenderer.on(`terminal:exit:${sessionId}`, onExit);\n\n    sessionListeners.set(sessionId, { onData, onExit });\n  }\n\n  return instance;\n};\n\nconst cleanupTerminalInstance = (sessionId) => {\n  const instance = terminalInstances.get(sessionId);\n  if (instance) {\n    try {\n      if (instance.inputDisposable) instance.inputDisposable.dispose();\n      if (instance.resizeDisposable) instance.resizeDisposable.dispose();\n      if (instance.terminal) {\n        instance.terminal.dispose();\n      }\n    } catch (err) {\n      console.warn('Error disposing terminal instance:', err);\n    }\n    terminalInstances.delete(sessionId);\n  }\n\n  // Remove IPC listeners\n  const listeners = sessionListeners.get(sessionId);\n  if (listeners && window.ipcRenderer) {\n    try {\n      window.ipcRenderer.removeAllListeners(`terminal:data:${sessionId}`);\n      window.ipcRenderer.removeAllListeners(`terminal:exit:${sessionId}`);\n    } catch (err) {\n      console.warn('Error removing IPC listeners:', err);\n    }\n    sessionListeners.delete(sessionId);\n  }\n};\n\nconst openTerminalIntoContainer = async (container, sessionId, terminalTheme) => {\n  if (!container || !sessionId) return;\n\n  const instance = createTerminalForSession(sessionId, terminalTheme);\n  const { terminal, fitAddon } = instance;\n\n  if (!terminal.element) {\n    terminal.open(container);\n  } else {\n    // Move terminal element to new container\n    if (terminal.element.parentElement !== container) {\n      container.appendChild(terminal.element);\n    }\n  }\n\n  await new Promise((resolve) => setTimeout(resolve, 50));\n  try {\n    fitAddon.fit();\n    terminal.focus();\n    const { cols, rows } = terminal;\n    if (cols && rows && window.ipcRenderer) {\n      window.ipcRenderer.send('terminal:resize', sessionId, { cols, rows });\n    }\n  } catch (e) {\n    console.warn('Error fitting terminal:', e);\n  }\n};\n\nlet fitFrameRef;\nconst fitTerminal = (activeSessionId, container) => {\n  if (!container) return;\n\n  const instance = terminalInstances.get(activeSessionId);\n  if (!instance?.fitAddon) return;\n\n  if (fitFrameRef) {\n    cancelAnimationFrame(fitFrameRef);\n  }\n\n  fitFrameRef = requestAnimationFrame(() => {\n    fitFrameRef = null;\n\n    // Avoid fitting when hidden/0-sized (common during tab switches/layout transitions)\n    if (container.offsetWidth === 0 || container.offsetHeight === 0) return;\n\n    try {\n      instance.fitAddon.fit();\n    } catch (e) {}\n  });\n};\n\nconst TerminalTab = () => {\n  const terminalRef = useRef(null);\n  const [sessions, setSessions] = useState([]);\n  const [activeSessionId, setActiveSessionId] = useState(null);\n  const [isLoading, setIsLoading] = useState(true);\n  const { theme } = useTheme();\n  const terminalTheme = getTerminalTheme(theme);\n\n  // Load sessions list\n  const loadSessions = useCallback(async (currentActiveSessionId = null) => {\n    if (!window.ipcRenderer) return [];\n\n    try {\n      const sessionList = await window.ipcRenderer.invoke('terminal:list-sessions');\n      setSessions(sessionList);\n\n      // Use functional state updates to get the current activeSessionId\n      setActiveSessionId((prevActiveSessionId) => {\n        const activeId = currentActiveSessionId !== null ? currentActiveSessionId : prevActiveSessionId;\n\n        // Auto-select first session if none selected\n        if (!activeId && sessionList.length > 0) {\n          return sessionList[0].sessionId;\n        }\n\n        // If active session no longer exists, select first available\n        if (activeId && !sessionList.find((s) => s.sessionId === activeId)) {\n          return sessionList.length > 0 ? sessionList[0].sessionId : null;\n        }\n\n        // Keep current selection if it still exists\n        return activeId;\n      });\n\n      return sessionList;\n    } catch (err) {\n      console.error('Failed to load sessions:', err);\n      return [];\n    }\n  }, []);\n\n  // Create new terminal session\n  const createNewSession = useCallback(\n    async (cwd = null) => {\n      if (!window.ipcRenderer) return null;\n\n      try {\n        const options = cwd ? { cwd } : {};\n        const newSessionId = await window.ipcRenderer.invoke('terminal:create', options);\n        if (newSessionId) {\n          await loadSessions(newSessionId);\n          setActiveSessionId(newSessionId);\n          return newSessionId;\n        }\n      } catch (err) {\n        console.error('Failed to create terminal session:', err);\n      }\n      return null;\n    },\n    [loadSessions]\n  );\n\n  // Listen for requests to open terminal at specific CWD\n  useEffect(() => {\n    const normalizePath = (path) => {\n      if (!path) return '';\n      // Normalize path separators and remove trailing separators for comparison\n      return path.replace(/\\\\/g, '/').replace(/\\/$/, '') || '/';\n    };\n\n    const handleOpenTerminalAtCwd = async (event) => {\n      const { cwd } = event.detail;\n      if (!cwd) return;\n\n      const normalizedCwd = normalizePath(cwd);\n\n      // Check if session already exists at this CWD\n      const sessionList = await window.ipcRenderer.invoke('terminal:list-sessions');\n      const existingSession = sessionList.find((s) => normalizePath(s.cwd) === normalizedCwd);\n\n      if (existingSession) {\n        // Switch to existing session\n        await loadSessions(existingSession.sessionId);\n        setActiveSessionId(existingSession.sessionId);\n      } else {\n        // Create new session at this CWD\n        await createNewSession(cwd);\n      }\n    };\n\n    window.addEventListener('terminal:open-at-cwd', handleOpenTerminalAtCwd);\n\n    return () => {\n      window.removeEventListener('terminal:open-at-cwd', handleOpenTerminalAtCwd);\n    };\n  }, [loadSessions, createNewSession]);\n\n  // Close terminal session\n  const closeSession = async (sessionId) => {\n    if (!window.ipcRenderer) return;\n\n    try {\n      window.ipcRenderer.send('terminal:kill', sessionId);\n      cleanupTerminalInstance(sessionId);\n\n      // Load updated sessions (this will also handle active session switching)\n      const updatedSessions = await loadSessions();\n\n      // If we closed the active session and there are no sessions left, clear selection\n      if (activeSessionId === sessionId && updatedSessions.length === 0) {\n        setActiveSessionId(null);\n      }\n    } catch (err) {\n      console.error('Failed to close terminal session:', err);\n    }\n  };\n\n  // Load sessions on mount and set up polling\n  useEffect(() => {\n    if (!window.ipcRenderer) {\n      setIsLoading(false);\n      return;\n    }\n\n    let mounted = true;\n\n    const initialLoad = async () => {\n      const sessionList = await loadSessions();\n      if (mounted) {\n        setIsLoading(false);\n      }\n    };\n\n    initialLoad();\n\n    // Poll for session updates every 2 seconds\n    // Note: We don't pass currentActiveSessionId here to avoid stale closures\n    // The functional update inside loadSessions will use the current state\n    const pollInterval = setInterval(() => {\n      if (mounted) {\n        loadSessions();\n      }\n    }, 2000);\n\n    return () => {\n      mounted = false;\n      clearInterval(pollInterval);\n    };\n  }, []);\n\n  // Update all terminal themes when app theme changes\n  useEffect(() => {\n    terminalInstances.forEach((instance) => {\n      if (instance.terminal) {\n        instance.terminal.options.theme = terminalTheme;\n      }\n    });\n  }, [theme.mode]);\n\n  // Handle terminal display for active session\n  useEffect(() => {\n    if (!activeSessionId || !terminalRef.current) return;\n\n    let mounted = true;\n\n    const setupTerminal = async () => {\n      await openTerminalIntoContainer(terminalRef.current, activeSessionId, terminalTheme);\n\n      if (mounted) {\n        const instance = terminalInstances.get(activeSessionId);\n        if (instance) {\n          try {\n            const { cols, rows } = instance.terminal;\n            if (cols && rows && window.ipcRenderer) {\n              window.ipcRenderer.send('terminal:resize', activeSessionId, { cols, rows });\n            }\n          } catch (err) {\n            console.warn('Failed to perform initial resize:', err);\n          }\n\n          return () => {\n            // Park terminal element when switching sessions\n            if (instance.terminal && instance.terminal.element) {\n              const host = ensureParkingHost();\n              if (instance.terminal.element.parentElement !== host) {\n                host.appendChild(instance.terminal.element);\n              }\n            }\n          };\n        }\n      }\n    };\n\n    const cleanup = setupTerminal();\n\n    return () => {\n      mounted = false;\n      Promise.resolve(cleanup).then((fn) => {\n        if (typeof fn === 'function') fn();\n      });\n    };\n  }, [activeSessionId]);\n\n  const onSessionMount = useCallback(\n    (node) => {\n      if (!node) return;\n      terminalRef.current = node;\n      fitTerminal(activeSessionId, node);\n      const ro = new ResizeObserver(() => fitTerminal(activeSessionId, node));\n      ro.observe(node.parentNode);\n      return () => ro.disconnect();\n    },\n    [activeSessionId]\n  );\n\n  return (\n    <StyledWrapper>\n      <div className=\"terminal-content\">\n        {/* Left Sidebar */}\n        <div className=\"terminal-sessions-sidebar\">\n          <div className=\"terminal-sessions-header\">\n            <span>Sessions</span>\n            <IconPlus\n              size={16}\n              style={{ cursor: 'pointer', color: '#888' }}\n              onClick={(e) => {\n                e.stopPropagation();\n                createNewSession();\n              }}\n              title=\"New Terminal Session\"\n            />\n          </div>\n          <div className=\"terminal-sessions-list\">\n            {isLoading ? (\n              <div style={{ padding: '12px', color: '#888', fontSize: '13px' }}>Loading sessions...</div>\n            ) : sessions.length === 0 ? (\n              <div style={{ padding: '12px', color: '#888', fontSize: '13px' }}>No active sessions</div>\n            ) : (\n              <SessionList\n                sessions={sessions}\n                activeSessionId={activeSessionId}\n                onSelectSession={setActiveSessionId}\n                onCloseSession={closeSession}\n              />\n            )}\n          </div>\n        </div>\n\n        {/* Right Terminal Display */}\n        <div className=\"terminal-display-container\">\n          {!activeSessionId && window.ipcRenderer && (\n            <div className=\"terminal-loading\">\n              <IconTerminal2 size={24} strokeWidth={1.5} />\n              <span>No terminal session selected</span>\n            </div>\n          )}\n          <div\n            ref={onSessionMount}\n            className=\"terminal-container\"\n            style={{\n              height: '100%',\n              width: '100%',\n              display: activeSessionId ? 'block' : 'none'\n            }}\n          />\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default TerminalTab;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Devtools/Console/index.js",
    "content": "import React, { useEffect, useRef, useState } from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport ReactJson from 'react-json-view';\nimport { useTheme } from 'providers/Theme';\nimport {\n  IconX,\n  IconTrash,\n  IconFilter,\n  IconAlertTriangle,\n  IconAlertCircle,\n  IconBug,\n  IconCode,\n  IconChevronDown,\n  IconTerminal2,\n  IconNetwork,\n  IconDashboard\n} from '@tabler/icons';\nimport {\n  closeConsole,\n  clearLogs,\n  updateFilter,\n  toggleAllFilters,\n  setActiveTab,\n  clearDebugErrors,\n  updateNetworkFilter,\n  toggleAllNetworkFilters\n} from 'providers/ReduxStore/slices/logs';\n\nimport NetworkTab from './NetworkTab';\nimport TerminalTab from './TerminalTab';\nimport RequestDetailsPanel from './RequestDetailsPanel';\n// import DebugTab from './DebugTab';\nimport ErrorDetailsPanel from './ErrorDetailsPanel';\nimport Performance from '../Performance';\nimport StyledWrapper from './StyledWrapper';\n\nconst LogIcon = ({ type }) => {\n  const iconProps = { size: 16, strokeWidth: 1.5 };\n\n  switch (type) {\n    case 'error':\n      return <IconAlertCircle className=\"log-icon error\" {...iconProps} />;\n    case 'warn':\n      return <IconAlertTriangle className=\"log-icon warn\" {...iconProps} />;\n    case 'info':\n      return <IconAlertTriangle className=\"log-icon info\" {...iconProps} />;\n    // case 'debug':\n    //   return <IconBug className=\"log-icon debug\" {...iconProps} />;\n    default:\n      return <IconCode className=\"log-icon log\" {...iconProps} />;\n  }\n};\n\nconst LogTimestamp = ({ timestamp }) => {\n  const date = new Date(timestamp);\n  const time = date.toLocaleTimeString('en-US', {\n    hour12: false,\n    hour: '2-digit',\n    minute: '2-digit',\n    second: '2-digit',\n    fractionalSecondDigits: 3\n  });\n\n  return <span className=\"log-timestamp\">{time}</span>;\n};\n\n// Helper function to check if an object is a plain object (not a class instance)\nconst isPlainObject = (obj) => {\n  if (typeof obj !== 'object' || obj === null) return false;\n  const proto = Object.getPrototypeOf(obj);\n  return proto === null || proto === Object.prototype;\n};\n\n// Helper function to transform Bruno special types back to readable format\n// Extracted outside component to avoid recreation on every render\nconst transformBrunoTypes = (obj, seen = new WeakSet()) => {\n  if (typeof obj !== 'object' || obj === null) {\n    return obj;\n  }\n\n  // Guard against circular references\n  if (seen.has(obj)) {\n    return '[Circular]';\n  }\n  seen.add(obj);\n\n  // Handle Bruno special types\n  if (obj.__brunoType) {\n    switch (obj.__brunoType) {\n      case 'Set':\n        // Transform Set to display values at top level with numeric indices\n        if (Array.isArray(obj.__brunoValue)) {\n          return Object.fromEntries(\n            obj.__brunoValue.map((value, index) => [index, transformBrunoTypes(value, seen)])\n          );\n        }\n        return {};\n      case 'Map':\n        // Transform Map to display entries at top level with => notation\n        if (Array.isArray(obj.__brunoValue)) {\n          const mapEntries = {};\n          for (const entry of obj.__brunoValue) {\n            // Defensive check: ensure entry is a valid [key, value] pair\n            if (Array.isArray(entry) && entry.length >= 2) {\n              const [key, value] = entry;\n              mapEntries[`${String(key)} =>`] = transformBrunoTypes(value, seen);\n            }\n          }\n          return mapEntries;\n        }\n        return {};\n      case 'Function':\n        return `[Function: ${obj.__brunoValue?.split?.('\\n')?.[0]?.substring(0, 50) ?? 'anonymous'}...]`;\n      case 'undefined':\n        return 'undefined';\n      default:\n        return obj;\n    }\n  }\n\n  // Handle arrays - recurse into elements\n  if (Array.isArray(obj)) {\n    return obj.map((item) => transformBrunoTypes(item, seen));\n  }\n\n  // Preserve non-plain objects (Date, Error, RegExp, class instances, etc.)\n  if (!isPlainObject(obj)) {\n    return obj;\n  }\n\n  // Only deep-clone plain objects\n  const transformed = {};\n  for (const [key, value] of Object.entries(obj)) {\n    transformed[key] = transformBrunoTypes(value, seen);\n  }\n  return transformed;\n};\n\n// Helper to get metadata about Bruno types for display purposes\nconst getBrunoTypeMetadata = (obj) => {\n  if (typeof obj !== 'object' || obj === null) {\n    return {};\n  }\n  if (obj.__brunoType === 'Set' || obj.__brunoType === 'Map') {\n    return { type: obj.__brunoType };\n  }\n  return {};\n};\n\nconst LogMessage = ({ message, args }) => {\n  const { displayedTheme } = useTheme();\n\n  const formatMessage = (msg, originalArgs) => {\n    if (originalArgs && originalArgs.length > 0) {\n      return originalArgs.map((arg, index) => {\n        if (typeof arg === 'object' && arg !== null) {\n          const metadata = getBrunoTypeMetadata(arg);\n          const transformedArg = transformBrunoTypes(arg);\n\n          // Determine the name to display based on the type\n          let displayName = false;\n          let shouldCollapse = 1; // Default: collapse at depth 1 for regular objects\n\n          if (metadata.type === 'Map' || metadata.type === 'Set') {\n            displayName = metadata.type;\n            shouldCollapse = true; // Fully collapse Maps/Sets by default\n          }\n\n          return (\n            <div key={index} className=\"log-object\">\n              <ReactJson\n                src={transformedArg}\n                theme={displayedTheme === 'light' ? 'rjv-default' : 'monokai'}\n                iconStyle=\"triangle\"\n                indentWidth={2}\n                collapsed={shouldCollapse}\n                displayDataTypes={false}\n                displayObjectSize={false}\n                enableClipboard={false}\n                name={displayName}\n                style={{\n                  backgroundColor: 'transparent',\n                  fontSize: '${(props) => props.theme.font.size.sm}',\n                  fontFamily: 'ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace'\n                }}\n              />\n            </div>\n          );\n        }\n        return String(arg);\n      });\n    }\n    return msg;\n  };\n\n  const formattedMessage = formatMessage(message, args);\n\n  return (\n    <span className=\"log-message\">\n      {Array.isArray(formattedMessage) ? formattedMessage.map((item, index) => (\n        <span key={index}>{item} </span>\n      )) : formattedMessage}\n    </span>\n  );\n};\n\nconst FilterDropdown = ({ filters, logCounts, onFilterToggle, onToggleAll }) => {\n  const [isOpen, setIsOpen] = useState(false);\n  const dropdownRef = useRef(null);\n\n  const allFiltersEnabled = Object.values(filters).every((f) => f);\n  const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled);\n\n  useEffect(() => {\n    const handleClickOutside = (event) => {\n      if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {\n        setIsOpen(false);\n      }\n    };\n\n    document.addEventListener('mousedown', handleClickOutside);\n    return () => document.removeEventListener('mousedown', handleClickOutside);\n  }, []);\n\n  return (\n    <div className=\"filter-dropdown\" ref={dropdownRef}>\n      <button\n        className=\"filter-dropdown-trigger\"\n        onClick={() => setIsOpen(!isOpen)}\n        title=\"Filter logs by type\"\n      >\n        <IconFilter size={16} strokeWidth={1.5} />\n        <span className=\"filter-summary\">\n          {activeFilters.length === Object.keys(filters).length ? 'All' : `${activeFilters.length}/${Object.keys(filters).length}`}\n        </span>\n        <IconChevronDown size={14} strokeWidth={1.5} />\n      </button>\n\n      {isOpen && (\n        <div className=\"filter-dropdown-menu right\">\n          <div className=\"filter-dropdown-header\">\n            <span>Filter by Type</span>\n            <button\n              className=\"filter-toggle-all\"\n              onClick={() => onToggleAll(!allFiltersEnabled)}\n            >\n              {allFiltersEnabled ? 'Hide All' : 'Show All'}\n            </button>\n          </div>\n\n          <div className=\"filter-dropdown-options\">\n            {Object.entries(filters).map(([filterType, enabled]) => (\n              <label key={filterType} className=\"filter-option\">\n                <input\n                  type=\"checkbox\"\n                  checked={enabled}\n                  onChange={(e) => onFilterToggle(filterType, e.target.checked)}\n                />\n                <div className=\"filter-option-content\">\n                  <LogIcon type={filterType} />\n                  <span className=\"filter-option-label\">{filterType}</span>\n                  <span className=\"filter-option-count\">({logCounts[filterType] || 0})</span>\n                </div>\n              </label>\n            ))}\n          </div>\n        </div>\n      )}\n    </div>\n  );\n};\n\nconst NetworkFilterDropdown = ({ filters, requestCounts, onFilterToggle, onToggleAll }) => {\n  const [isOpen, setIsOpen] = useState(false);\n  const dropdownRef = useRef(null);\n\n  const allFiltersEnabled = Object.values(filters).every((f) => f);\n  const activeFilters = Object.entries(filters).filter(([_, enabled]) => enabled);\n\n  useEffect(() => {\n    const handleClickOutside = (event) => {\n      if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {\n        setIsOpen(false);\n      }\n    };\n\n    document.addEventListener('mousedown', handleClickOutside);\n    return () => document.removeEventListener('mousedown', handleClickOutside);\n  }, []);\n\n  return (\n    <div className=\"filter-dropdown\" ref={dropdownRef}>\n      <button\n        className=\"filter-dropdown-trigger\"\n        onClick={() => setIsOpen(!isOpen)}\n        title=\"Filter requests by method\"\n      >\n        <IconFilter size={16} strokeWidth={1.5} />\n        <span className=\"filter-summary\">\n          {activeFilters.length === Object.keys(filters).length ? 'All' : `${activeFilters.length}/${Object.keys(filters).length}`}\n        </span>\n        <IconChevronDown size={14} strokeWidth={1.5} />\n      </button>\n\n      {isOpen && (\n        <div className=\"filter-dropdown-menu right\">\n          <div className=\"filter-dropdown-header\">\n            <span>Filter by Method</span>\n            <button\n              className=\"filter-toggle-all\"\n              onClick={() => onToggleAll(!allFiltersEnabled)}\n            >\n              {allFiltersEnabled ? 'Hide All' : 'Show All'}\n            </button>\n          </div>\n\n          <div className=\"filter-dropdown-options\">\n            {Object.entries(filters).map(([method, enabled]) => (\n              <label key={method} className=\"filter-option\">\n                <input\n                  type=\"checkbox\"\n                  checked={enabled}\n                  onChange={(e) => onFilterToggle(method, e.target.checked)}\n                />\n                <div className=\"filter-option-content\">\n                  <span className=\"filter-option-label\">{method}</span>\n                  <span className=\"filter-option-count\">({requestCounts[method] || 0})</span>\n                </div>\n              </label>\n            ))}\n          </div>\n        </div>\n      )}\n    </div>\n  );\n};\n\nconst ConsoleTab = ({ logs, filters, logCounts, onFilterToggle, onToggleAll, onClearLogs }) => {\n  const logsEndRef = useRef(null);\n  const prevLogsCountRef = useRef(0);\n\n  useEffect(() => {\n    // Only scroll when new logs are added, not when switching tabs\n    if (logsEndRef.current && logs.length > prevLogsCountRef.current) {\n      logsEndRef.current.scrollIntoView({ behavior: 'auto' });\n    }\n    prevLogsCountRef.current = logs.length;\n  }, [logs]);\n\n  const filteredLogs = logs.filter((log) => filters[log.type]);\n\n  return (\n    <div className=\"tab-content\">\n      <div className=\"tab-content-area\">\n        {filteredLogs.length === 0 ? (\n          <div className=\"console-empty\">\n            <IconTerminal2 size={48} strokeWidth={1} />\n            <p>No logs to display</p>\n            <span>Logs will appear here as your application runs</span>\n          </div>\n        ) : (\n          <div className=\"logs-container\">\n            {filteredLogs.map((log) => (\n              <div key={log.id} className={`log-entry ${log.type}`}>\n                <div className=\"log-meta\">\n                  <LogTimestamp timestamp={log.timestamp} />\n                  <LogIcon type={log.type} />\n                </div>\n                <LogMessage message={log.message} args={log.args} />\n              </div>\n            ))}\n            <div ref={logsEndRef} />\n          </div>\n        )}\n      </div>\n    </div>\n  );\n};\n\nconst Console = () => {\n  const dispatch = useDispatch();\n  const { logs, filters, activeTab, selectedRequest, selectedError, networkFilters, debugErrors } = useSelector((state) => state.logs);\n  const collections = useSelector((state) => state.collections.collections);\n  const consoleRef = useRef(null);\n\n  const logCounts = logs.reduce((counts, log) => {\n    counts[log.type] = (counts[log.type] || 0) + 1;\n    return counts;\n  }, {});\n\n  const allRequests = React.useMemo(() => {\n    const requests = [];\n\n    collections.forEach((collection) => {\n      if (collection.timeline) {\n        collection.timeline\n          .filter((entry) => entry.type === 'request')\n          .forEach((entry) => {\n            requests.push({\n              ...entry,\n              collectionName: collection.name,\n              collectionUid: collection.uid\n            });\n          });\n      }\n    });\n\n    return requests.sort((a, b) => a.timestamp - b.timestamp);\n  }, [collections]);\n\n  const filteredLogs = logs.filter((log) => filters[log.type]);\n  const filteredRequests = allRequests.filter((request) => {\n    const method = request.data?.request?.method?.toUpperCase() || 'GET';\n    return networkFilters[method];\n  });\n\n  const requestCounts = allRequests.reduce((counts, request) => {\n    const method = request.data?.request?.method?.toUpperCase() || 'GET';\n    counts[method] = (counts[method] || 0) + 1;\n    return counts;\n  }, {});\n\n  const handleFilterToggle = (filterType, enabled) => {\n    dispatch(updateFilter({ filterType, enabled }));\n  };\n\n  const handleNetworkFilterToggle = (method, enabled) => {\n    dispatch(updateNetworkFilter({ method, enabled }));\n  };\n\n  const handleClearLogs = () => {\n    dispatch(clearLogs());\n  };\n\n  const handleClearDebugErrors = () => {\n    dispatch(clearDebugErrors());\n  };\n\n  const handlecloseConsole = () => {\n    dispatch(closeConsole());\n  };\n\n  const handleToggleAllFilters = (enabled) => {\n    dispatch(toggleAllFilters(enabled));\n  };\n\n  const handleToggleAllNetworkFilters = (enabled) => {\n    dispatch(toggleAllNetworkFilters(enabled));\n  };\n\n  const handleTabChange = (tab) => {\n    dispatch(setActiveTab(tab));\n  };\n\n  const renderTabContent = () => {\n    switch (activeTab) {\n      case 'console':\n        return (\n          <ConsoleTab\n            logs={logs}\n            filters={filters}\n            logCounts={logCounts}\n            onFilterToggle={handleFilterToggle}\n            onToggleAll={handleToggleAllFilters}\n            onClearLogs={handleClearLogs}\n          />\n        );\n      case 'network':\n        return <NetworkTab />;\n      case 'performance':\n        return <Performance />;\n      case 'terminal':\n        return <TerminalTab />;\n      // case 'debug':\n      //   return <DebugTab />;\n      default:\n        return (\n          <ConsoleTab\n            logs={logs}\n            filters={filters}\n            logCounts={logCounts}\n            onFilterToggle={handleFilterToggle}\n            onToggleAll={handleToggleAllFilters}\n            onClearLogs={handleClearLogs}\n          />\n        );\n    }\n  };\n\n  const renderTabControls = () => {\n    switch (activeTab) {\n      case 'console':\n        return (\n          <div className=\"tab-controls\">\n            <div className=\"filter-controls\">\n              <FilterDropdown\n                filters={filters}\n                logCounts={logCounts}\n                onFilterToggle={handleFilterToggle}\n                onToggleAll={handleToggleAllFilters}\n              />\n            </div>\n            <div className=\"action-controls\">\n              <button\n                className=\"control-button\"\n                onClick={handleClearLogs}\n                title=\"Clear all logs\"\n              >\n                <IconTrash size={16} strokeWidth={1.5} />\n              </button>\n            </div>\n          </div>\n        );\n      case 'network':\n        return (\n          <div className=\"tab-controls\">\n            <div className=\"filter-controls\">\n              <NetworkFilterDropdown\n                filters={networkFilters}\n                requestCounts={requestCounts}\n                onFilterToggle={handleNetworkFilterToggle}\n                onToggleAll={handleToggleAllNetworkFilters}\n              />\n            </div>\n          </div>\n        );\n      case 'terminal':\n        return null; // No controls needed for terminal\n      // case 'debug':\n      //   return (\n      //     <div className=\"tab-controls\">\n      //       <div className=\"action-controls\">\n      //         {debugErrors.length > 0 && (\n      //           <button\n      //             className=\"control-button\"\n      //             onClick={handleClearDebugErrors}\n      //             title=\"Clear all errors\"\n      //           >\n      //             <IconTrash size={16} strokeWidth={1.5} />\n      //           </button>\n      //         )}\n      //       </div>\n      //     </div>\n      //   );\n      default:\n        return null;\n    }\n  };\n\n  return (\n    <StyledWrapper ref={consoleRef}>\n      <div\n        className=\"console-resize-handle\"\n      />\n\n      <div className=\"console-header\">\n        <div className=\"console-tabs\">\n          <button\n            className={`console-tab ${activeTab === 'console' ? 'active' : ''}`}\n            onClick={() => handleTabChange('console')}\n          >\n            <IconTerminal2 size={16} strokeWidth={1.5} />\n            <span>Console</span>\n          </button>\n\n          <button\n            className={`console-tab ${activeTab === 'network' ? 'active' : ''}`}\n            onClick={() => handleTabChange('network')}\n          >\n            <IconNetwork size={16} strokeWidth={1.5} />\n            <span>Network</span>\n          </button>\n\n          <button\n            className={`console-tab ${activeTab === 'performance' ? 'active' : ''}`}\n            onClick={() => handleTabChange('performance')}\n          >\n            <IconDashboard size={16} strokeWidth={1.5} />\n            <span>Performance</span>\n          </button>\n\n          <button\n            className={`console-tab ${activeTab === 'terminal' ? 'active' : ''}`}\n            onClick={() => handleTabChange('terminal')}\n          >\n            <IconTerminal2 size={16} strokeWidth={1.5} />\n            <span>Terminal</span>\n          </button>\n\n          {/* <button\n            className={`console-tab ${activeTab === 'debug' ? 'active' : ''}`}\n            onClick={() => handleTabChange('debug')}\n          >\n            <IconBug size={16} strokeWidth={1.5} />\n            <span>Debug</span>\n          </button> */}\n        </div>\n\n        <div className=\"console-controls\">\n          {renderTabControls()}\n          <button\n            className=\"control-button close-button\"\n            onClick={handlecloseConsole}\n            title=\"Close console\"\n          >\n            <IconX size={16} strokeWidth={1.5} />\n          </button>\n        </div>\n      </div>\n\n      <div className=\"console-content\">\n        {activeTab === 'network' && selectedRequest ? (\n          <div className=\"network-with-details\">\n            <div className=\"network-main\">\n              {renderTabContent()}\n            </div>\n            <RequestDetailsPanel />\n          </div>\n        ) : activeTab === 'debug' && selectedError ? (\n          <div className=\"debug-with-details\">\n            <div className=\"debug-main\">\n              {renderTabContent()}\n            </div>\n            <ErrorDetailsPanel />\n          </div>\n        ) : (\n          renderTabContent()\n        )}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Console;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Devtools/Performance/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .tab-content {\n    height: 100%;\n    display: flex;\n    flex-direction: column;\n    background: ${(props) => props.theme.console.bg};\n  }\n\n  .tab-content-area {\n    flex: 1;\n    overflow-y: auto;\n    padding: 16px;\n  }\n\n  .overview-container {\n    max-width: 1200px;\n    margin: 0 auto;\n  }\n\n  .overview-section {\n    margin-bottom: 32px;\n\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n\n  .section-header {\n    margin-bottom: 20px;\n    padding-bottom: 12px;\n    border-bottom: 1px solid ${(props) => props.theme.console.border};\n\n    h3 {\n      margin: 0 0 4px 0;\n      font-size: 16px;\n      font-weight: 500;\n      color: ${(props) => props.theme.console.titleColor};\n    }\n\n    p {\n      margin: 0;\n      font-size: ${(props) => props.theme.font.size.base};\n      color: ${(props) => props.theme.console.textMuted};\n    }\n  }\n\n    .system-resources {\n    margin-bottom: 16px;\n\n    h2 {\n      margin: 0 0 8px 0;\n      font-size: ${(props) => props.theme.font.size.base};\n      font-weight: 500;\n      color: ${(props) => props.theme.console.titleColor};\n    }\n  }\n\n  .resource-cards {\n    display: grid;\n    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));\n    gap: 8px;\n    margin-bottom: 16px;\n  }\n\n  .resource-card {\n    background: ${(props) => props.theme.console.headerBg};\n    border: 1px solid ${(props) => props.theme.console.border};\n    border-radius: 4px;\n    padding: 8px;\n  }\n\n  .resource-header {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    margin-bottom: 6px;\n    color: ${(props) => props.theme.console.titleColor};\n  }\n\n  .resource-title {\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-weight: 500;\n  }\n\n  .resource-value {\n    font-size: 18px;\n    font-weight: 500;\n    color: ${(props) => props.theme.console.titleColor};\n    margin-bottom: 2px;\n  }\n\n  .resource-subtitle {\n    font-size: ${(props) => props.theme.font.size.xs};\n    color: ${(props) => props.theme.console.buttonColor};\n  }\n\n  .resource-trend {\n    display: flex;\n    align-items: center;\n    gap: 4px;\n    font-size: ${(props) => props.theme.font.size.xs};\n    margin-top: 8px;\n\n    &.up {\n      color: #10b981;\n    }\n\n    &.down {\n      color: #e81123;\n    }\n\n    &.stable {\n      color: ${(props) => props.theme.console.buttonColor};\n    }\n  }\n\n  .performance-header {\n    display: flex;\n    align-items: center;\n    border-bottom: 1px solid ${(props) => props.theme.console.border};\n    padding: 12px 16px;\n    background: ${(props) => props.theme.console.headerBg};\n  }\n\n  .performance-selector-wrapper {\n    display: flex;\n    align-items: center;\n    gap: 12px;\n  }\n\n  .performance-selector-label {\n    font-size: 13px;\n    font-weight: 500;\n    color: ${(props) => props.theme.console.titleColor};\n    user-select: none;\n  }\n\n  .performance-selector {\n    position: relative;\n    display: inline-flex;\n    align-items: center;\n  }\n\n  .performance-select {\n    appearance: none;\n    background: ${(props) => props.theme.console.bg};\n    border: 1px solid ${(props) => props.theme.console.border};\n    border-radius: 4px;\n    padding: 6px 32px 6px 12px;\n    font-size: 13px;\n    font-weight: 500;\n    color: ${(props) => props.theme.console.titleColor};\n    cursor: pointer;\n    outline: none;\n    transition: all 0.2s ease;\n    min-width: 250px;\n    max-width: 400px;\n\n    &:hover {\n      border-color: ${(props) => props.theme.colors.primary};\n    }\n\n    &:focus {\n      border-color: ${(props) => props.theme.colors.primary};\n      box-shadow: 0 0 0 2px ${(props) => props.theme.colors.primary}33;\n    }\n\n    option {\n      background: ${(props) => props.theme.console.bg};\n      color: ${(props) => props.theme.console.titleColor};\n      padding: 8px;\n    }\n  }\n\n  .performance-select-icon {\n    position: absolute;\n    right: 10px;\n    pointer-events: none;\n    color: ${(props) => props.theme.console.buttonColor};\n  }\n\n  .processes-table-container {\n    display: flex;\n    flex-direction: column;\n    height: 100%;\n    min-height: 0;\n\n    h2 {\n      margin: 0 0 16px 0;\n      font-size: 14px;\n      font-weight: 600;\n      color: ${(props) => props.theme.console.titleColor};\n      flex-shrink: 0;\n    }\n  }\n\n  .no-processes {\n    padding: 32px;\n    text-align: center;\n    color: ${(props) => props.theme.console.buttonColor};\n    font-size: 13px;\n  }\n\n  .processes-table-wrapper {\n    flex: 1;\n    min-height: 0;\n    overflow: auto;\n  }\n\n  .processes-table {\n    width: 100%;\n    border-collapse: collapse;\n    background: ${(props) => props.theme.console.headerBg};\n    border: 1px solid ${(props) => props.theme.console.border};\n    border-radius: 4px;\n    overflow: hidden;\n\n    thead {\n      background: ${(props) => props.theme.console.bg};\n      border-bottom: 1px solid ${(props) => props.theme.console.border};\n\n      th {\n        padding: 10px 12px;\n        text-align: left;\n        font-size: 12px;\n        font-weight: 600;\n        color: ${(props) => props.theme.console.titleColor};\n        text-transform: uppercase;\n        letter-spacing: 0.5px;\n\n        &:first-child {\n          padding-left: 16px;\n        }\n\n        &:last-child {\n          padding-right: 16px;\n        }\n      }\n    }\n\n    tbody {\n      tr {\n        border-bottom: 1px solid ${(props) => props.theme.console.border};\n        transition: background 0.15s ease;\n\n        &:hover {\n          background: ${(props) => props.theme.console.bg};\n        }\n\n        &:last-child {\n          border-bottom: none;\n        }\n      }\n\n      td {\n        padding: 10px 12px;\n        font-size: 13px;\n        color: ${(props) => props.theme.console.textColor};\n\n        &:first-child {\n          padding-left: 16px;\n        }\n\n        &:last-child {\n          padding-right: 16px;\n        }\n      }\n    }\n\n    .pid-cell {\n      font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;\n      font-size: 12px;\n      color: ${(props) => props.theme.console.buttonColor};\n    }\n\n    .type-cell {\n      .process-type-badge {\n        display: inline-block;\n        padding: 2px 8px;\n        border-radius: 3px;\n        font-size: 11px;\n        font-weight: 500;\n        text-transform: lowercase;\n        background: ${(props) => props.theme.console.border};\n        color: ${(props) => props.theme.console.buttonColor};\n\n        &.Browser {\n          background: rgba(59, 130, 246, 0.2);\n          color: #3b82f6;\n        }\n\n        &.Renderer {\n          background: rgba(16, 185, 129, 0.2);\n          color: #10b981;\n        }\n\n        &.Utility {\n          background: rgba(139, 92, 246, 0.2);\n          color: #8b5cf6;\n        }\n\n        &.Zygote {\n          background: rgba(245, 158, 11, 0.2);\n          color: #f59e0b;\n        }\n\n        &.Sandbox {\n          background: rgba(239, 68, 68, 0.2);\n          color: #ef4444;\n        }\n      }\n    }\n\n    .title-cell {\n      max-width: 300px;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      white-space: nowrap;\n    }\n\n    .cpu-cell {\n      font-weight: 500;\n\n      .high-cpu {\n        color: #ef4444;\n      }\n\n      .medium-cpu {\n        color: #f59e0b;\n      }\n\n      .low-cpu {\n        color: ${(props) => props.theme.console.buttonColor};\n      }\n    }\n\n    .memory-cell {\n      font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;\n      font-size: 12px;\n    }\n\n    .created-cell {\n      font-size: 12px;\n      color: ${(props) => props.theme.console.buttonColor};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Devtools/Performance/index.js",
    "content": "import React, { useEffect, useState, useMemo } from 'react';\nimport { useSelector } from 'react-redux';\nimport StyledWrapper from './StyledWrapper';\nimport {\n  IconCpu,\n  IconDatabase,\n  IconClock,\n  IconServer,\n  IconChevronDown,\n  IconChartLine\n} from '@tabler/icons';\n\nconst getProcessOptions = (processes) => {\n  return [\n    { value: 'cumulative', label: 'Cumulative (All Processes)' },\n    ...(processes ?? []).map((process) => ({\n      value: String(process.pid),\n      label: `PID ${process.pid}${process.title ? ` - ${process.title}` : ''}${process.type ? ` (${process.type})` : ''}`\n    }))\n  ];\n};\n\nconst Performance = () => {\n  const { systemResources } = useSelector((state) => state.performance);\n  const [selectedPid, setSelectedPid] = useState('cumulative');\n\n  useEffect(() => {\n    const { ipcRenderer } = window;\n\n    if (!ipcRenderer) {\n      console.warn('IPC Renderer not available');\n      return;\n    }\n\n    const startMonitoring = async () => {\n      try {\n        await ipcRenderer.invoke('renderer:start-system-monitoring', 2000);\n      } catch (error) {\n        console.error('Failed to start system monitoring:', error);\n      }\n    };\n\n    const stopMonitoring = async () => {\n      try {\n        await ipcRenderer.invoke('renderer:stop-system-monitoring');\n      } catch (error) {\n        console.error('Failed to stop system monitoring:', error);\n      }\n    };\n\n    startMonitoring();\n\n    return () => {\n      stopMonitoring();\n    };\n  }, []);\n\n  const formatBytes = (bytes) => {\n    if (bytes === 0) return '0 Bytes';\n    const k = 1024;\n    const sizes = ['Bytes', 'KB', 'MB', 'GB'];\n    const i = Math.floor(Math.log(bytes) / Math.log(k));\n    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];\n  };\n\n  const formatUptime = (seconds) => {\n    const hours = Math.floor(seconds / 3600);\n    const minutes = Math.floor((seconds % 3600) / 60);\n    const secs = Math.floor(seconds % 60);\n\n    if (hours > 0) return `${hours}h ${minutes}m ${secs}s`;\n    if (minutes > 0) return `${minutes}m ${secs}s`;\n    return `${secs}s`;\n  };\n\n  const SystemResourceCard = ({ icon: Icon, title, value, subtitle, color = 'default', trend }) => (\n    <div className={`resource-card ${color}`}>\n      <div className=\"resource-header\">\n        <Icon size={20} strokeWidth={1.5} />\n        <span className=\"resource-title\">{title}</span>\n      </div>\n      <div className=\"resource-value\">{value}</div>\n      {subtitle && <div className=\"resource-subtitle\">{subtitle}</div>}\n      {trend && (\n        <div className={`resource-trend ${trend > 0 ? 'up' : trend < 0 ? 'down' : 'stable'}`}>\n          <IconChartLine size={12} strokeWidth={1.5} />\n          <span>\n            {trend > 0 ? '+' : ''}\n            {trend.toFixed(1)}\n            %\n          </span>\n        </div>\n      )}\n    </div>\n  );\n\n  // Get process options for dropdown\n  const processOptions = useMemo(() => getProcessOptions(systemResources.processes), [systemResources.processes]);\n\n  // Get selected process data\n  const selectedProcess = useMemo(() => {\n    if (selectedPid === 'cumulative') {\n      return null; // Show cumulative view\n    }\n    const processes = systemResources.processes || [];\n    return processes.find((p) => String(p.pid) === selectedPid) || null;\n  }, [selectedPid, systemResources.processes]);\n\n  // Reset to cumulative if selected PID no longer exists\n  useEffect(() => {\n    if (selectedPid !== 'cumulative' && !selectedProcess) {\n      setSelectedPid('cumulative');\n    }\n  }, [selectedPid, selectedProcess]);\n\n  const renderCumulativeView = () => (\n    <div className=\"system-resources\">\n      <h2>System Resources</h2>\n      <div className=\"resource-cards\">\n        <SystemResourceCard\n          icon={IconCpu}\n          title=\"CPU Usage\"\n          value={`${systemResources.cpu.toFixed(1)}%`}\n          subtitle=\"Total CPU usage\"\n          color={systemResources.cpu > 80 ? 'danger' : systemResources.cpu > 60 ? 'warning' : 'success'}\n        />\n\n        <SystemResourceCard\n          icon={IconDatabase}\n          title=\"Memory Usage\"\n          value={formatBytes(systemResources.memory)}\n          subtitle=\"Total memory usage\"\n          color={systemResources.memory > (500 * 1024 * 1024) ? 'danger' : 'default'}\n        />\n\n        <SystemResourceCard\n          icon={IconClock}\n          title=\"Uptime\"\n          value={formatUptime(systemResources.uptime)}\n          subtitle=\"Process runtime\"\n          color=\"info\"\n        />\n\n        <SystemResourceCard\n          icon={IconServer}\n          title=\"Process ID\"\n          value={systemResources.pid || 'N/A'}\n          subtitle=\"Main process PID\"\n          color=\"default\"\n        />\n      </div>\n    </div>\n  );\n\n  const renderProcessView = (process) => {\n    if (!process) return null;\n\n    // Calculate uptime for individual process\n    const processUptime = process.creationTime\n      ? (new Date() - new Date(process.creationTime)) / 1000\n      : 0;\n\n    return (\n      <div className=\"system-resources\">\n        <h2>System Resources</h2>\n        <div className=\"resource-cards\">\n          <SystemResourceCard\n            icon={IconCpu}\n            title=\"CPU Usage\"\n            value={`${process.cpu.toFixed(1)}%`}\n            subtitle=\"Current CPU usage\"\n            color={process.cpu > 80 ? 'danger' : process.cpu > 60 ? 'warning' : 'success'}\n          />\n\n          <SystemResourceCard\n            icon={IconDatabase}\n            title=\"Memory Usage\"\n            value={formatBytes(process.memory)}\n            subtitle=\"Current memory usage\"\n            color={process.memory > (500 * 1024 * 1024) ? 'danger' : 'default'}\n          />\n\n          <SystemResourceCard\n            icon={IconClock}\n            title=\"Uptime\"\n            value={formatUptime(processUptime)}\n            subtitle=\"Process runtime\"\n            color=\"info\"\n          />\n\n          <SystemResourceCard\n            icon={IconServer}\n            title=\"Process ID\"\n            value={process.pid}\n            subtitle=\"Process PID\"\n            color=\"default\"\n          />\n        </div>\n      </div>\n    );\n  };\n\n  return (\n    <StyledWrapper>\n      <div className=\"tab-content\">\n        <div className=\"performance-header\">\n          <div className=\"performance-selector-wrapper\">\n            <label htmlFor=\"process-selector\" className=\"performance-selector-label\">\n              View:\n            </label>\n            <div className=\"performance-selector\">\n              <select\n                id=\"process-selector\"\n                value={selectedPid}\n                onChange={(e) => setSelectedPid(e.target.value)}\n                className=\"performance-select\"\n              >\n                {processOptions.map((option) => (\n                  <option key={option.value} value={option.value}>\n                    {option.label}\n                  </option>\n                ))}\n              </select>\n              <IconChevronDown size={16} className=\"performance-select-icon\" />\n            </div>\n          </div>\n        </div>\n        <div className=\"tab-content-area\">\n          {selectedPid === 'cumulative' ? renderCumulativeView() : renderProcessView(selectedProcess)}\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Performance;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Devtools/index.js",
    "content": "import React, { useCallback, useEffect, useState, useMemo } from 'react';\nimport { useSelector } from 'react-redux';\nimport { darken } from 'polished';\nimport Console from './Console';\nimport { useTheme } from 'providers/Theme';\n\nconst MIN_DEVTOOLS_HEIGHT = 150;\nconst MAX_DEVTOOLS_HEIGHT = window.innerHeight * 0.7;\nconst DEFAULT_DEVTOOLS_HEIGHT = 300;\n\nconst Devtools = ({ mainSectionRef }) => {\n  const isDevtoolsOpen = useSelector((state) => state.logs.isConsoleOpen);\n  const [devtoolsHeight, setDevtoolsHeight] = useState(DEFAULT_DEVTOOLS_HEIGHT);\n  const [isResizingDevtools, setIsResizingDevtools] = useState(false);\n  const { theme } = useTheme();\n\n  const dragHandleColor = useMemo(() => darken(0.1, theme.primary.subtle), [theme.primary.subtle]);\n\n  const handleDevtoolsResizeStart = useCallback((e) => {\n    e.preventDefault();\n    setIsResizingDevtools(true);\n  }, []);\n\n  const handleDevtoolsResize = useCallback((e) => {\n    if (!isResizingDevtools || !mainSectionRef.current) return;\n\n    const windowHeight = window.innerHeight;\n    const statusBarHeight = 22;\n    const mouseY = e.clientY;\n\n    // Calculate new devtools height - expanding upward from bottom\n    const newHeight = windowHeight - mouseY - statusBarHeight;\n    const clampedHeight = Math.min(MAX_DEVTOOLS_HEIGHT, Math.max(MIN_DEVTOOLS_HEIGHT, newHeight));\n    setDevtoolsHeight(clampedHeight);\n\n    // Update main section height\n    if (mainSectionRef.current) {\n      mainSectionRef.current.style.height = `calc(100vh - 22px - ${clampedHeight}px)`;\n    }\n  }, [isResizingDevtools, mainSectionRef]);\n\n  const handleDevtoolsResizeEnd = useCallback(() => {\n    setIsResizingDevtools(false);\n  }, []);\n\n  useEffect(() => {\n    if (isResizingDevtools) {\n      document.addEventListener('mousemove', handleDevtoolsResize);\n      document.addEventListener('mouseup', handleDevtoolsResizeEnd);\n      document.body.style.userSelect = 'none';\n\n      return () => {\n        document.removeEventListener('mousemove', handleDevtoolsResize);\n        document.removeEventListener('mouseup', handleDevtoolsResizeEnd);\n        document.body.style.userSelect = '';\n      };\n    }\n  }, [isResizingDevtools, handleDevtoolsResize, handleDevtoolsResizeEnd]);\n\n  // Set initial height\n  useEffect(() => {\n    if (mainSectionRef.current && isDevtoolsOpen) {\n      mainSectionRef.current.style.height = `calc(100vh - 22px - ${devtoolsHeight}px)`;\n    }\n  }, [isDevtoolsOpen, devtoolsHeight, mainSectionRef]);\n\n  if (!isDevtoolsOpen) {\n    return null;\n  }\n\n  return (\n    <>\n      <div\n        onMouseDown={handleDevtoolsResizeStart}\n        style={{\n          height: '2px',\n          cursor: 'row-resize',\n          backgroundColor: isResizingDevtools ? dragHandleColor : 'transparent',\n          transition: 'background-color 0.2s ease',\n          zIndex: 20,\n          position: 'relative'\n        }}\n        onMouseEnter={(e) => e.target.style.backgroundColor = dragHandleColor}\n        onMouseLeave={(e) => e.target.style.backgroundColor = isResizingDevtools ? dragHandleColor : 'transparent'}\n      />\n      <div style={{ height: `${devtoolsHeight}px`, overflow: 'hidden', position: 'relative' }}>\n        <Console />\n      </div>\n    </>\n  );\n};\n\nexport default Devtools;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Documentation/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .editing-mode {\n    cursor: pointer;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Documentation/index.js",
    "content": "import 'github-markdown-css/github-markdown.css';\nimport get from 'lodash/get';\nimport { updateRequestDocs } from 'providers/ReduxStore/slices/collections';\nimport { useTheme } from 'providers/Theme';\nimport { useState } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport Markdown from 'components/MarkDown';\nimport CodeEditor from 'components/CodeEditor';\nimport StyledWrapper from './StyledWrapper';\n\nconst Documentation = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  const { displayedTheme } = useTheme();\n  const [isEditing, setIsEditing] = useState(false);\n  const docs = item.draft ? get(item, 'draft.request.docs') : get(item, 'request.docs');\n  const preferences = useSelector((state) => state.app.preferences);\n\n  const toggleViewMode = () => {\n    setIsEditing((prev) => !prev);\n  };\n\n  const onEdit = (value) => {\n    dispatch(\n      updateRequestDocs({\n        itemUid: item.uid,\n        collectionUid: collection.uid,\n        docs: value\n      })\n    );\n  };\n\n  const onSave = () => dispatch(saveRequest(item.uid, collection.uid));\n\n  if (!item) {\n    return null;\n  }\n\n  return (\n    <StyledWrapper className=\"flex flex-col gap-y-1 h-full w-full relative\">\n      <div className=\"editing-mode\" role=\"tab\" onClick={toggleViewMode}>\n        {isEditing ? 'Preview' : 'Edit'}\n      </div>\n\n      {isEditing ? (\n        <CodeEditor\n          collection={collection}\n          theme={displayedTheme}\n          font={get(preferences, 'font.codeFont', 'default')}\n          fontSize={get(preferences, 'font.codeFontSize')}\n          value={docs || ''}\n          onEdit={onEdit}\n          onSave={onSave}\n          mode=\"application/text\"\n        />\n      ) : (\n        <Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default Documentation;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Dropdown/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst Wrapper = styled.div`\n  min-width: 160px;\n  font-size: ${(props) => props.theme.font.size.sm};\n  color: ${(props) => props.theme.dropdown.color};\n  background-color: ${(props) => props.theme.dropdown.bg};\n  ${(props) =>\n    props.theme.dropdown.shadow && props.theme.dropdown.shadow !== 'none'\n      ? `box-shadow: ${props.theme.dropdown.shadow};`\n      : ''}\n  border-radius: ${(props) => props.theme.border.radius.base};\n  ${(props) =>\n    props.theme.dropdown.border && props.theme.dropdown.border !== 'none'\n      ? `border: 1px solid ${props.theme.dropdown.border};`\n      : ''}\n  max-height: 90vh;\n  overflow-y: auto;\n  max-width: unset !important;\n  padding: 0.25rem;\n\n  [role=\"menu\"] {\n    outline: none;\n    &:focus {\n      outline: none;\n    }\n    &:focus-visible {\n      outline: none;\n    }\n  }\n\n  .label-item {\n    display: flex;\n    align-items: center;\n    padding: 0.375rem 0.625rem 0.25rem 0.625rem;\n    font-size: 0.6875rem;\n    font-weight: 600;\n    letter-spacing: 0.025em;\n    color: ${(props) => props.theme.dropdown.color};\n    opacity: 0.6;\n    margin-top: 0.25rem;\n    &:first-child {\n      margin-top: 0;\n    }\n  }\n\n  .dropdown-item {\n    display: flex;\n    align-items: center;\n    gap: 0.5rem;\n    padding: 0.275rem 0.625rem;\n    cursor: pointer;\n    border-radius: 6px;\n    margin: 0.0625rem 0;\n    font-size: ${(props) => props.theme.font.size.sm};\n\n    &.active {\n      color: ${(props) => props.theme.colors.text.yellow} !important;\n      .dropdown-icon {\n        color: ${(props) => props.theme.colors.text.yellow} !important;\n      }\n    }\n\n    .dropdown-label {\n      flex: 1;\n    }\n\n    .dropdown-icon {\n      flex-shrink: 0;\n      width: 16px;\n      height: 16px;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      color: ${(props) => props.theme.dropdown.iconColor};\n      opacity: 0.8;\n    }\n\n    .dropdown-right-section {\n      margin-left: auto;\n      flex-shrink: 0;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n    }\n\n    .dropdown-tab-count {\n      margin-left: auto;\n      font-size: 11px;\n      font-weight: 500;\n      padding: 1px 6px;\n      border-radius: 10px;\n      background: ${(props) => props.theme.dropdown.hoverBg};\n      min-width: 18px;\n      text-align: center;\n    }\n\n    &:hover:not(:disabled):not(.disabled) {\n      background-color: ${(props) => props.theme.dropdown.hoverBg};\n    }\n\n    &.selected-focused:not(:disabled):not(.disabled) {\n      background-color: ${(props) => props.theme.dropdown.hoverBg};\n    }\n\n    &:focus-visible:not(:disabled):not(.disabled) {\n      outline: none;\n      background-color: ${(props) => props.theme.dropdown.hoverBg};\n    }\n\n    &:focus:not(:focus-visible) {\n      outline: none;\n    }\n\n    &:disabled,\n    &.disabled {\n      cursor: not-allowed;\n      opacity: 0.5;\n    }\n\n    &.delete-item {\n      color: ${(props) => props.theme.colors.text.danger};\n      .dropdown-icon {\n        color: ${(props) => props.theme.colors.text.danger};\n      }\n      &:hover {\n        background-color: ${({ theme }) => {\n          const hex = theme.colors.text.danger.replace('#', '');\n          const r = parseInt(hex.substring(0, 2), 16);\n          const g = parseInt(hex.substring(2, 4), 16);\n          const b = parseInt(hex.substring(4, 6), 16);\n          return `rgba(${r}, ${g}, ${b}, 0.04)`; // 4% opacity\n        }} !important;\n\n        color: ${(props) => props.theme.colors.text.danger} !important;\n      }\n    }\n\n    &.border-top {\n      border-top: solid 1px ${(props) => props.theme.dropdown.separator};\n      margin-top: 0.25rem;\n      padding-top: 0.375rem;\n    }\n\n    &.dropdown-item-select {\n      padding-left: 1.5rem;\n    }\n\n    /* Focused state - applied during keyboard navigation */\n    &.dropdown-item-focused {\n      background-color: ${({ theme }) => theme.dropdown.hoverBg};\n      outline: none;\n    }\n\n    /* Active/selected state - applied to the currently selected item */\n    &.dropdown-item-active {\n      color: ${({ theme }) => theme.dropdown.selectedColor} !important;\n      background-color: ${({ theme }) => rgba(theme.dropdown.selectedColor, 0.07)} !important;\n      .dropdown-icon {\n        color: ${({ theme }) => theme.dropdown.selectedColor} !important;\n      }\n\n      &:hover {\n        color: ${({ theme }) => theme.dropdown.selectedColor} !important;\n        background-color: ${({ theme }) => rgba(theme.dropdown.selectedColor, 0.07)} !important;\n      }\n    }\n\n    /* Combined state - when active item is also focused */\n    &.dropdown-item-active.dropdown-item-focused {\n      background-color: ${({ theme }) => rgba(theme.dropdown.selectedColor, 0.07)} !important;\n    }\n\n    /* Focus visible for accessibility */\n    &:focus-visible {\n      outline: 2px solid ${({ theme }) => theme.dropdown.focusRing};\n      outline-offset: -2px;\n    }\n  }\n\n  .dropdown-separator {\n    height: 1px;\n    background-color: ${(props) => props.theme.dropdown.separator};\n    margin: 0.25rem 0;\n  }\n\n  .submenu-trigger {\n    position: relative;\n  }\n\n  .submenu-arrow {\n    color: ${(props) => props.theme.dropdown.mutedText};\n    flex-shrink: 0;\n    display: flex;\n    align-items: center;\n    margin-left: auto;\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Dropdown/index.js",
    "content": "import React from 'react';\nimport Tippy from '@tippyjs/react';\nimport StyledWrapper from './StyledWrapper';\n\nconst Dropdown = ({ icon, children, onCreate, placement, transparent, visible, appendTo, onMouseEnter, onMouseLeave, ...props }) => {\n  // When in controlled mode (visible prop is provided), don't use trigger prop\n  const tippyProps = visible !== undefined\n    ? { ...props, visible, interactive: true, appendTo: appendTo || 'parent' }\n    : { ...props, trigger: 'click', interactive: true, appendTo: appendTo || 'parent' };\n\n  return (\n    <Tippy\n      render={(attrs) => (\n        <StyledWrapper\n          className=\"tippy-box dropdown\"\n          transparent={transparent}\n          tabIndex={-1}\n          onMouseEnter={onMouseEnter}\n          onMouseLeave={onMouseLeave}\n          {...attrs}\n        >\n          {children}\n        </StyledWrapper>\n      )}\n      placement={placement || 'bottom-end'}\n      animation={false}\n      arrow={false}\n      onCreate={onCreate}\n      {...tippyProps}\n    >\n      {icon}\n    </Tippy>\n  );\n};\n\nexport default Dropdown;\n"
  },
  {
    "path": "packages/bruno-app/src/components/EditableTable/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  flex: 1;\n  overflow: hidden;\n\n  &.is-resizing {\n    cursor: col-resize !important;\n    user-select: none;\n  }\n\n  .table-container {\n    overflow: auto;\n    border-radius: ${(props) => props.theme.border.radius.base};\n    border: solid 1px ${(props) => props.theme.border.border0};\n  }\n\n  table {\n    width: 100%;\n    border-collapse: collapse;\n    table-layout: fixed;\n    font-size: ${(props) => props.theme.font.size.base};\n    font-weight: normal !important;\n  }\n\n  thead {\n    color: ${(props) => props.theme.table.thead.color} !important;\n    background: ${(props) => props.theme.sidebar.bg};\n    user-select: none;\n    overflow: visible;\n\n    border: none !important;\n\n    td {\n      padding: 5px 10px !important;\n      border-top: none !important;\n      border-left: none !important;\n      border-bottom: solid 1px ${(props) => props.theme.border.border0};\n      border-right: solid 1px ${(props) => props.theme.border.border0};\n      vertical-align: middle;\n      position: relative;\n      overflow: visible;\n\n      &:last-child {\n        border-right: none;\n      }\n\n      .column-name {\n        display: block;\n        overflow: hidden;\n        text-overflow: ellipsis;\n        white-space: nowrap;\n        padding-right: 4px;\n      }\n\n      .resize-handle {\n        position: absolute;\n        right: 0;\n        top: 0;\n        width: 4px;\n        height: 100%;\n        cursor: col-resize;\n        background: transparent;\n        z-index: 100;\n\n        &:hover,\n        &.resizing {\n          background: ${(props) => props.theme.colors.accent};\n        }\n      }\n    }\n  }\n\n  &.has-checkbox thead td:nth-child(1) {\n    width: 25px !important;\n    border-right: none;\n  }\n\n  tbody {\n    tr {\n      transition: background 0.1s ease;\n\n      &:last-child td {\n        border-bottom: none;\n      }\n\n      td {\n        padding: 1px 10px !important;\n        border-top: none !important;\n        border-left: none !important;\n        border-bottom: solid 1px ${(props) => props.theme.border.border0};\n        border-right: solid 1px ${(props) => props.theme.border.border0};\n        vertical-align: middle;\n        overflow: hidden;\n        text-overflow: ellipsis;\n        white-space: nowrap;\n\n        &:last-child {\n          border-right: none;\n        }\n\n        /* Handle CodeMirror editors overflow */\n        .cm-editor {\n          max-width: 100%;\n\n          .cm-scroller {\n            overflow: hidden !important;\n          }\n\n          .cm-content {\n            max-width: 100%;\n          }\n\n          .cm-line {\n            overflow: hidden;\n            text-overflow: ellipsis;\n            white-space: nowrap;\n          }\n        }\n      }\n    }\n  }\n\n  &.has-checkbox tbody td:nth-child(1) {\n    width: 25px;\n    border-right: none;\n    text-align: center;\n    vertical-align: middle;\n    line-height: 1;\n    text-overflow: clip;\n\n    input[type='checkbox'] {\n      vertical-align: baseline;\n      display: inline-block;\n    }\n  }\n\n  .tooltip-mod {\n    max-width: 200px !important;\n    word-wrap: break-word !important;\n    overflow-wrap: break-word !important;\n    white-space: normal !important;\n  }\n\n  input[type='text'] {\n    width: 100%;\n    outline: none !important;\n    background-color: transparent;\n    color: ${(props) => props.theme.text};\n    padding: 0;\n    border-radius: 4px;\n    transition: all 0.15s ease;\n\n    &:focus {\n      outline: none !important;\n    }\n  }\n\n  input[type='checkbox'] {\n    cursor: pointer;\n    width: 14px;\n    height: 14px;\n    accent-color: ${(props) => props.theme.colors.accent};\n    vertical-align: middle;\n    margin: 0;\n  }\n\n  button {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    padding: 4px;\n    color: ${(props) => props.theme.colors.text.muted};\n    background: transparent;\n    border: none;\n    cursor: pointer;\n    border-radius: 4px;\n    transition: color 0.15s ease, background 0.15s ease;\n\n    &:hover {\n      color: ${(props) => props.theme.colors.text.danger};\n    }\n  }\n\n  .drag-handle {\n    .icon-grip,\n    .icon-minus {\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n  }\n\n  select {\n    background-color: transparent;\n    color: ${(props) => props.theme.text};\n    border: none;\n    outline: none;\n    padding: 2px 8px;\n    font-size: 12px;\n    cursor: pointer;\n\n    option {\n      background-color: ${(props) => props.theme.bg};\n      color: ${(props) => props.theme.text};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/EditableTable/index.js",
    "content": "import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react';\nimport { IconTrash, IconAlertCircle, IconGripVertical, IconMinusVertical } from '@tabler/icons';\nimport { Tooltip } from 'react-tooltip';\nimport { uuid } from 'utils/common';\nimport StyledWrapper from './StyledWrapper';\n\nconst MIN_COLUMN_WIDTH = 80;\n\nconst EditableTable = ({\n  columns,\n  rows,\n  onChange,\n  defaultRow,\n  getRowError,\n  showCheckbox = true,\n  showDelete = true,\n  disableCheckbox = false,\n  checkboxLabel = '',\n  checkboxKey = 'enabled',\n  reorderable = false,\n  onReorder,\n  showAddRow = true,\n  testId = 'editable-table'\n}) => {\n  const tableRef = useRef(null);\n  const emptyRowUidRef = useRef(null);\n  const [hoveredRow, setHoveredRow] = useState(null);\n  const [resizing, setResizing] = useState(null);\n  const [tableHeight, setTableHeight] = useState(0);\n  const [columnWidths, setColumnWidths] = useState(() => {\n    const initialWidths = {};\n    columns.forEach((col) => {\n      initialWidths[col.key] = col.width || 'auto';\n    });\n    return initialWidths;\n  });\n\n  const handleResizeStart = useCallback((e, columnKey) => {\n    e.preventDefault();\n    e.stopPropagation();\n\n    const currentCell = e.target.closest('td');\n    const nextCell = currentCell?.nextElementSibling;\n    if (!currentCell || !nextCell) return;\n\n    const columnIndex = columns.findIndex((col) => col.key === columnKey);\n    if (columnIndex >= columns.length - 1) return;\n\n    const startX = e.clientX;\n    const startWidth = currentCell.offsetWidth;\n    const nextColumnKey = columns[columnIndex + 1].key;\n    const nextColumnStartWidth = nextCell.offsetWidth;\n\n    setResizing(columnKey);\n\n    const handleMouseMove = (moveEvent) => {\n      const diff = moveEvent.clientX - startX;\n      const maxGrow = nextColumnStartWidth - MIN_COLUMN_WIDTH;\n      const maxShrink = startWidth - MIN_COLUMN_WIDTH;\n      const clampedDiff = Math.max(-maxShrink, Math.min(maxGrow, diff));\n\n      setColumnWidths((prev) => ({\n        ...prev,\n        [columnKey]: `${startWidth + clampedDiff}px`,\n        [nextColumnKey]: `${nextColumnStartWidth - clampedDiff}px`\n      }));\n    };\n\n    const handleMouseUp = () => {\n      // Convert pixel widths to percentages for responsive scaling\n      const table = tableRef.current?.querySelector('table');\n      if (table) {\n        const tableWidth = table.offsetWidth;\n        const headerCells = table.querySelectorAll('thead td');\n        const newWidths = {};\n\n        headerCells.forEach((cell, cellIndex) => {\n          const checkboxOffset = showCheckbox ? 1 : 0;\n          const colIndex = cellIndex - checkboxOffset;\n\n          if (colIndex >= 0 && colIndex < columns.length) {\n            const colKey = columns[colIndex]?.key;\n            if (colKey) {\n              const percentage = (cell.offsetWidth / tableWidth) * 100;\n              newWidths[colKey] = `${percentage}%`;\n            }\n          }\n        });\n\n        if (Object.keys(newWidths).length > 0) {\n          setColumnWidths((prev) => ({ ...prev, ...newWidths }));\n        }\n      }\n      setResizing(null);\n      document.removeEventListener('mousemove', handleMouseMove);\n      document.removeEventListener('mouseup', handleMouseUp);\n    };\n\n    document.addEventListener('mousemove', handleMouseMove);\n    document.addEventListener('mouseup', handleMouseUp);\n  }, [columns, showCheckbox]);\n\n  // Track table height for resize handles\n  useEffect(() => {\n    const table = tableRef.current?.querySelector('table');\n    if (!table) return;\n\n    const updateHeight = () => {\n      setTableHeight(table.offsetHeight);\n    };\n\n    updateHeight();\n\n    const resizeObserver = new ResizeObserver(updateHeight);\n    resizeObserver.observe(table);\n\n    return () => resizeObserver.disconnect();\n  }, [rows.length]);\n\n  const getColumnWidth = useCallback((column) => {\n    return columnWidths[column.key] || column.width || 'auto';\n  }, [columnWidths]);\n\n  const createEmptyRow = useCallback(() => {\n    const newUid = uuid();\n    emptyRowUidRef.current = newUid;\n    return {\n      uid: newUid,\n      [checkboxKey]: true,\n      ...defaultRow\n    };\n  }, [defaultRow, checkboxKey]);\n\n  const rowsWithEmpty = useMemo(() => {\n    if (!showAddRow) {\n      return rows;\n    }\n\n    if (rows.length === 0) {\n      return [createEmptyRow()];\n    }\n\n    const lastRow = rows[rows.length - 1];\n    const keyColumn = columns.find((col) => col.isKeyField);\n\n    if (keyColumn) {\n      const lastRowKeyValue = keyColumn.getValue ? keyColumn.getValue(lastRow) : lastRow[keyColumn.key];\n      const isLastRowEmpty = !lastRowKeyValue || (typeof lastRowKeyValue === 'string' && lastRowKeyValue.trim() === '');\n\n      if (isLastRowEmpty) {\n        return rows;\n      }\n    }\n\n    if (!emptyRowUidRef.current || rows.some((r) => r.uid === emptyRowUidRef.current)) {\n      emptyRowUidRef.current = uuid();\n    }\n\n    return [...rows, {\n      uid: emptyRowUidRef.current,\n      [checkboxKey]: true,\n      ...defaultRow\n    }];\n  }, [rows, columns, defaultRow, checkboxKey, createEmptyRow, showAddRow]);\n\n  const isEmptyRow = useCallback((row) => {\n    const keyColumn = columns.find((col) => col.isKeyField);\n    if (!keyColumn) return false;\n\n    const value = keyColumn.getValue ? keyColumn.getValue(row) : row[keyColumn.key];\n    return !value || (typeof value === 'string' && value.trim() === '');\n  }, [columns]);\n\n  const isLastEmptyRow = useCallback((row, index) => {\n    if (!showAddRow) return false;\n    return index === rowsWithEmpty.length - 1 && isEmptyRow(row);\n  }, [rowsWithEmpty.length, isEmptyRow, showAddRow]);\n\n  const handleValueChange = useCallback((rowUid, key, value) => {\n    const rowIndex = rowsWithEmpty.findIndex((r) => r.uid === rowUid);\n    if (rowIndex === -1) return;\n\n    const currentRow = rowsWithEmpty[rowIndex];\n    const isLast = rowIndex === rowsWithEmpty.length - 1;\n    const wasEmpty = isEmptyRow(currentRow);\n\n    const keyColumn = columns.find((col) => col.isKeyField);\n    const isKeyFieldChange = keyColumn && keyColumn.key === key;\n\n    let updatedRows = rowsWithEmpty.map((row) => {\n      if (row.uid === rowUid) {\n        return { ...row, [key]: value };\n      }\n      return row;\n    });\n\n    // Only add a new empty row when the key field is filled\n    if (showAddRow && isLast && wasEmpty && isKeyFieldChange && value && value.trim() !== '') {\n      emptyRowUidRef.current = uuid();\n      updatedRows.push({\n        uid: emptyRowUidRef.current,\n        [checkboxKey]: true,\n        ...defaultRow\n      });\n    }\n\n    const hasAnyValue = (row) => {\n      for (const col of columns) {\n        const val = col.getValue ? col.getValue(row) : row[col.key];\n        const defaultVal = defaultRow[col.key];\n        if (val && val !== defaultVal && (typeof val !== 'string' || val.trim() !== '')) {\n          return true;\n        }\n      }\n      return false;\n    };\n\n    const result = updatedRows.filter((row, i) => {\n      if (showAddRow && i === updatedRows.length - 1) {\n        return hasAnyValue(row);\n      }\n      return true;\n    });\n\n    onChange(result);\n  }, [rowsWithEmpty, columns, onChange, checkboxKey, defaultRow, isEmptyRow, showAddRow]);\n\n  const handleCheckboxChange = useCallback((rowUid, checked) => {\n    handleValueChange(rowUid, checkboxKey, checked);\n  }, [handleValueChange, checkboxKey]);\n\n  const handleRemoveRow = useCallback((rowUid) => {\n    const filteredRows = rows.filter((row) => row.uid !== rowUid);\n    onChange(filteredRows);\n  }, [rows, onChange]);\n\n  const handleDragStart = useCallback((e, index) => {\n    e.dataTransfer.effectAllowed = 'move';\n    e.dataTransfer.setData('text/plain', index);\n  }, []);\n\n  const handleDragOver = useCallback((e, index) => {\n    e.preventDefault();\n    e.dataTransfer.dropEffect = 'move';\n    setHoveredRow(index);\n  }, []);\n\n  const handleDrop = useCallback((e, toIndex) => {\n    e.preventDefault();\n    const fromIndex = parseInt(e.dataTransfer.getData('text/plain'), 10);\n    if (fromIndex !== toIndex && onReorder) {\n      const reorderableRows = showAddRow ? rowsWithEmpty.slice(0, -1) : rowsWithEmpty;\n      const updatedOrder = [...reorderableRows];\n      const [movedRow] = updatedOrder.splice(fromIndex, 1);\n      if (!movedRow) {\n        setHoveredRow(null);\n        return;\n      }\n      updatedOrder.splice(toIndex, 0, movedRow);\n      onReorder({ updateReorderedItem: updatedOrder.map((row) => row.uid) });\n    }\n    setHoveredRow(null);\n  }, [onReorder, rowsWithEmpty, showAddRow]);\n\n  const handleDragEnd = useCallback(() => {\n    setHoveredRow(null);\n  }, []);\n\n  const renderCell = useCallback((column, row, rowIndex) => {\n    const isEmpty = isLastEmptyRow(row, rowIndex);\n    const value = column.getValue ? column.getValue(row) : row[column.key];\n    const error = getRowError?.(row, rowIndex, column.key);\n\n    const errorIcon = error && !isEmpty ? (\n      <span>\n        <IconAlertCircle\n          data-tooltip-id={`error-${row.uid}-${column.key}`}\n          className=\"text-red-600 cursor-pointer ml-1\"\n          size={20}\n        />\n        <Tooltip\n          className=\"tooltip-mod\"\n          id={`error-${row.uid}-${column.key}`}\n          html={error}\n        />\n      </span>\n    ) : null;\n\n    if (column.render) {\n      return (\n        <div className=\"flex items-center\">\n          {column.render({\n            row,\n            value,\n            rowIndex,\n            isLastEmptyRow: isEmpty,\n            onChange: (newValue) => handleValueChange(row.uid, column.key, newValue)\n          })}\n          {errorIcon}\n        </div>\n      );\n    }\n\n    return (\n      <div className=\"flex items-center\">\n        <input\n          type=\"text\"\n          autoComplete=\"off\"\n          autoCorrect=\"off\"\n          autoCapitalize=\"off\"\n          spellCheck=\"false\"\n          className=\"mousetrap\"\n          value={value || ''}\n          readOnly={column.readOnly}\n          placeholder={!value ? column.placeholder || column.name : ''}\n          onChange={(e) => handleValueChange(row.uid, column.key, e.target.value)}\n        />\n        {errorIcon}\n      </div>\n    );\n  }, [isLastEmptyRow, getRowError, handleValueChange]);\n\n  const reorderableRowCount = showAddRow ? rowsWithEmpty.length - 1 : rowsWithEmpty.length;\n\n  return (\n    <StyledWrapper className={`${showCheckbox ? 'has-checkbox' : 'no-checkbox'} ${resizing ? 'is-resizing' : ''}`}>\n      <div className=\"table-container\" ref={tableRef} data-testid={testId}>\n        <table>\n          <thead>\n            <tr>\n              {showCheckbox && (\n                <td className=\"text-center\">{checkboxLabel}</td>\n              )}\n              {columns.map((column, colIndex) => (\n                <td\n                  key={column.key}\n                  style={{ width: getColumnWidth(column) }}\n                >\n                  <span className=\"column-name\">{column.name}</span>\n                  {colIndex < columns.length - 1 && (\n                    <div\n                      className={`resize-handle ${resizing === column.key ? 'resizing' : ''}`}\n                      style={{ height: tableHeight > 0 ? `${tableHeight}px` : undefined }}\n                      onMouseDown={(e) => handleResizeStart(e, column.key)}\n                    />\n                  )}\n                </td>\n              ))}\n              {showDelete && (\n                <td style={{ width: '60px' }}></td>\n              )}\n            </tr>\n          </thead>\n          <tbody>\n            {rowsWithEmpty.map((row, rowIndex) => {\n              const isEmpty = isLastEmptyRow(row, rowIndex);\n              const canDrag = reorderable && !isEmpty && rowIndex < reorderableRowCount;\n\n              return (\n                <tr\n                  key={row.uid}\n                  draggable={canDrag}\n                  onDragStart={canDrag ? (e) => handleDragStart(e, rowIndex) : undefined}\n                  onDragOver={canDrag ? (e) => handleDragOver(e, rowIndex) : undefined}\n                  onDrop={canDrag ? (e) => handleDrop(e, rowIndex) : undefined}\n                  onDragEnd={canDrag ? handleDragEnd : undefined}\n                  onMouseEnter={() => setHoveredRow(rowIndex)}\n                  onMouseLeave={() => setHoveredRow(null)}\n                >\n                  {showCheckbox && (\n                    <td className=\"text-center relative\">\n                      {reorderable && canDrag && (\n                        <div\n                          draggable\n                          className=\"drag-handle group absolute z-10 left-[-8px] top-1/2 -translate-y-1/2 p-1 cursor-grab\"\n                        >\n                          {hoveredRow === rowIndex && (\n                            <>\n                              <IconGripVertical\n                                size={14}\n                                className=\"icon-grip hidden group-hover:block\"\n                              />\n                              <IconMinusVertical\n                                size={14}\n                                className=\"icon-minus block group-hover:hidden\"\n                              />\n                            </>\n                          )}\n                        </div>\n                      )}\n                      {!isEmpty && (\n                        <input\n                          type=\"checkbox\"\n                          className=\"mousetrap\"\n                          data-testid=\"column-checkbox\"\n                          checked={row[checkboxKey] ?? true}\n                          disabled={disableCheckbox}\n                          onChange={(e) => handleCheckboxChange(row.uid, e.target.checked)}\n                        />\n                      )}\n                    </td>\n                  )}\n                  {columns.map((column) => (\n                    <td key={column.key} data-testid={`column-${column.key}`}>\n                      {renderCell(column, row, rowIndex)}\n                    </td>\n                  ))}\n                  {showDelete && (\n                    <td>\n                      {!isEmpty && (\n                        <button\n                          data-testid=\"column-delete\"\n                          onClick={() => handleRemoveRow(row.uid)}\n                        >\n                          <IconTrash strokeWidth={1.5} size={18} />\n                        </button>\n                      )}\n                    </td>\n                  )}\n                </tr>\n              );\n            })}\n          </tbody>\n        </table>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default EditableTable;\n"
  },
  {
    "path": "packages/bruno-app/src/components/EnvironmentVariablesTable/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  flex: 1;\n  overflow: hidden;\n\n  &.is-resizing {\n    cursor: col-resize !important;\n    user-select: none;\n  }\n\n  .table-container {\n    overflow-y: auto;\n    border-radius: 8px;\n    border: solid 1px ${(props) => props.theme.border.border0};\n  }\n\n  table {\n    width: 100%;\n    border-collapse: collapse;\n    table-layout: fixed;\n    font-size: 12px;\n\n    td {\n      vertical-align: middle;\n      padding: 2px 10px;\n\n      &:nth-child(1) {\n        width: 25px;\n        border-right: none;\n      }\n      &:nth-child(4) {\n        width: 80px;\n      }\n      &:nth-child(5) {\n        width: 60px;\n      }\n    }\n\n    thead {\n      color: ${(props) => props.theme.table.thead.color} !important;\n      background: ${(props) => props.theme.sidebar.bg};\n      font-size: ${(props) => props.theme.font.size.base};\n      user-select: none;\n\n      td {\n        padding: 5px 10px !important;\n        border-bottom: solid 1px ${(props) => props.theme.border.border0};\n        border-right: solid 1px ${(props) => props.theme.border.border0};\n        position: relative;\n\n        &:last-child {\n          border-right: none;\n        }\n\n        .resize-handle {\n          position: absolute;\n          right: 0;\n          top: 0;\n          width: 4px;\n          cursor: col-resize;\n          background: transparent;\n          z-index: 100;\n\n          &:hover,\n          &.resizing {\n            background: ${(props) => props.theme.colors.accent};\n          }\n        }\n      }\n    }\n\n    tbody {\n      tr {\n        transition: background 0.1s ease;\n\n        &:last-child td {\n          border-bottom: none;\n        }\n\n        td {\n          border-bottom: solid 1px ${(props) => props.theme.border.border0};\n          border-right: solid 1px ${(props) => props.theme.border.border0};\n\n          &:last-child {\n            border-right: none;\n          }\n        }\n      }\n    }\n  }\n\n  .tooltip-mod {\n    max-width: 200px !important;\n  }\n\n  .name-cell-wrapper {\n    position: relative;\n    width: 100%;\n  }\n\n  .no-results {\n    padding: 24px;\n    text-align: center;\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  input[type='text'] {\n    width: 100%;\n    border: 1px solid transparent;\n    outline: none !important;\n    background-color: transparent;\n    color: ${(props) => props.theme.text};\n    padding: 0;\n    border-radius: 4px;\n    transition: all 0.15s ease;\n\n    &:focus {\n      outline: none !important;\n    }\n  }\n\n  input[type='checkbox'] {\n    cursor: pointer;\n    width: 14px;\n    height: 14px;\n    accent-color: ${(props) => props.theme.colors.accent};\n    vertical-align: middle;\n    margin: 0;\n  }\n\n  button {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    padding: 4px;\n    color: ${(props) => props.theme.colors.text.muted};\n    background: transparent;\n    border: none;\n    cursor: pointer;\n    border-radius: 4px;\n    transition: color 0.15s ease, background 0.15s ease;\n  }\n\n  .button-container {\n    padding: 12px 2px;\n    background: ${(props) => props.theme.bg};\n    flex-shrink: 0;\n    display: flex;\n    gap: 8px;\n  }\n\n  .submit {\n    padding: 6px 16px;\n    font-size: ${(props) => props.theme.font.size.sm};\n    border-radius: ${(props) => props.theme.border.radius.base};\n    border: none;\n    background: ${(props) => props.theme.brand};\n    color: ${(props) => props.theme.bg};\n    cursor: pointer;\n    transition: opacity 0.15s ease;\n\n    &:hover {\n      opacity: 0.9;\n    }\n  }\n\n  .reset {\n    background: transparent;\n    padding: 6px 16px;\n    color: ${(props) => props.theme.brand};\n    &:hover {\n      opacity: 0.9;\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/EnvironmentVariablesTable/index.js",
    "content": "import React, { useCallback, useRef, useState, useEffect, useMemo } from 'react';\nimport { TableVirtuoso } from 'react-virtuoso';\nimport cloneDeep from 'lodash/cloneDeep';\nimport { IconTrash, IconAlertCircle, IconInfoCircle } from '@tabler/icons';\nimport { useTheme } from 'providers/Theme';\nimport { useSelector } from 'react-redux';\nimport MultiLineEditor from 'components/MultiLineEditor/index';\nimport StyledWrapper from './StyledWrapper';\nimport { uuid } from 'utils/common';\nimport { useFormik } from 'formik';\nimport * as Yup from 'yup';\nimport { variableNameRegex } from 'utils/common/regex';\nimport toast from 'react-hot-toast';\nimport { Tooltip } from 'react-tooltip';\nimport { getGlobalEnvironmentVariables } from 'utils/collections';\nimport { stripEnvVarUid } from 'utils/environments';\n\nconst MIN_H = 35 * 2;\nconst MIN_COLUMN_WIDTH = 80;\n\nconst TableRow = React.memo(\n  ({ children, item }) => (\n    <tr key={item.uid} data-testid={`env-var-row-${item.name}`}>\n      {children}\n    </tr>\n  ),\n  (prevProps, nextProps) => {\n    const prevUid = prevProps?.item?.uid;\n    const nextUid = nextProps?.item?.uid;\n    return prevUid === nextUid && prevProps.children === nextProps.children;\n  }\n);\n\nconst EnvironmentVariablesTable = ({\n  environment,\n  collection,\n  onSave,\n  draft,\n  onDraftChange,\n  onDraftClear,\n  setIsModified,\n  renderExtraValueContent,\n  searchQuery = ''\n}) => {\n  const { storedTheme } = useTheme();\n  const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);\n\n  const hasDraftForThisEnv = draft?.environmentUid === environment.uid;\n\n  const [tableHeight, setTableHeight] = useState(MIN_H);\n  const [columnWidths, setColumnWidths] = useState({ name: '30%', value: 'auto' });\n  const [resizing, setResizing] = useState(null);\n  const [pinnedData, setPinnedData] = useState({ query: '', uids: new Set() });\n\n  const handleResizeStart = useCallback((e, columnKey) => {\n    e.preventDefault();\n    e.stopPropagation();\n\n    const currentCell = e.target.closest('td');\n    const nextCell = currentCell?.nextElementSibling;\n    if (!currentCell || !nextCell) return;\n\n    const startX = e.clientX;\n    const startWidth = currentCell.offsetWidth;\n    const nextColumnKey = 'value';\n    const nextColumnStartWidth = nextCell.offsetWidth;\n\n    setResizing(columnKey);\n\n    const handleMouseMove = (moveEvent) => {\n      const diff = moveEvent.clientX - startX;\n      const maxGrow = nextColumnStartWidth - MIN_COLUMN_WIDTH;\n      const maxShrink = startWidth - MIN_COLUMN_WIDTH;\n      const clampedDiff = Math.max(-maxShrink, Math.min(maxGrow, diff));\n\n      setColumnWidths({\n        [columnKey]: `${startWidth + clampedDiff}px`,\n        [nextColumnKey]: `${nextColumnStartWidth - clampedDiff}px`\n      });\n    };\n\n    const handleMouseUp = () => {\n      setResizing(null);\n      document.removeEventListener('mousemove', handleMouseMove);\n      document.removeEventListener('mouseup', handleMouseUp);\n    };\n\n    document.addEventListener('mousemove', handleMouseMove);\n    document.addEventListener('mouseup', handleMouseUp);\n  }, []);\n\n  const handleTotalHeightChanged = useCallback((h) => {\n    setTableHeight(h);\n  }, []);\n\n  const handleRowFocus = useCallback((uid) => {\n    setPinnedData((prev) => ({\n      query: searchQuery,\n      uids: prev.query === searchQuery ? new Set([...prev.uids, uid]) : new Set([uid])\n    }));\n  }, [searchQuery]);\n\n  const prevEnvUidRef = useRef(null);\n  const prevEnvVariablesRef = useRef(environment.variables);\n  const mountedRef = useRef(false);\n\n  let _collection = collection ? cloneDeep(collection) : {};\n  const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });\n  if (_collection) {\n    _collection.globalEnvironmentVariables = globalEnvironmentVariables;\n  }\n\n  const initialValues = useMemo(() => {\n    const vars = environment.variables || [];\n    return [\n      ...vars,\n      {\n        uid: uuid(),\n        name: '',\n        value: '',\n        type: 'text',\n        secret: false,\n        enabled: true\n      }\n    ];\n  }, [environment.uid, environment.variables]);\n\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: initialValues,\n    validationSchema: Yup.array().of(\n      Yup.object({\n        enabled: Yup.boolean(),\n        name: Yup.string().when('$isLastRow', {\n          is: true,\n          then: (schema) => schema.optional(),\n          otherwise: (schema) =>\n            schema\n              .required('Name cannot be empty')\n              .matches(\n                variableNameRegex,\n                'Name contains invalid characters. Must only contain alphanumeric characters, \"-\", \"_\", \".\" and cannot start with a digit.'\n              )\n              .trim()\n        }),\n        secret: Yup.boolean(),\n        type: Yup.string(),\n        uid: Yup.string(),\n        value: Yup.mixed().nullable()\n      })\n    ),\n    validate: (values) => {\n      const errors = {};\n      values.forEach((variable, index) => {\n        const isLastRow = index === values.length - 1;\n        const isEmptyRow = !variable.name || variable.name.trim() === '';\n\n        if (isLastRow && isEmptyRow) {\n          return;\n        }\n\n        if (!variable.name || variable.name.trim() === '') {\n          if (!errors[index]) errors[index] = {};\n          errors[index].name = 'Name cannot be empty';\n        } else if (!variableNameRegex.test(variable.name)) {\n          if (!errors[index]) errors[index] = {};\n          errors[index].name\n            = 'Name contains invalid characters. Must only contain alphanumeric characters, \"-\", \"_\", \".\" and cannot start with a digit.';\n        }\n      });\n      return Object.keys(errors).length > 0 ? errors : {};\n    },\n    onSubmit: () => {}\n  });\n\n  // Restore draft values on mount or environment switch\n  useEffect(() => {\n    const isMount = !mountedRef.current;\n    const envChanged = prevEnvUidRef.current !== null && prevEnvUidRef.current !== environment.uid;\n    const variablesReloaded = !isMount && !envChanged && prevEnvVariablesRef.current !== environment.variables;\n\n    prevEnvUidRef.current = environment.uid;\n    prevEnvVariablesRef.current = environment.variables;\n    mountedRef.current = true;\n\n    if ((isMount || envChanged || variablesReloaded) && hasDraftForThisEnv && draft?.variables) {\n      formik.setValues([\n        ...draft.variables,\n        {\n          uid: uuid(),\n          name: '',\n          value: '',\n          type: 'text',\n          secret: false,\n          enabled: true\n        }\n      ]);\n    }\n  }, [environment.uid, environment.variables, hasDraftForThisEnv, draft?.variables]);\n\n  const savedValuesJson = useMemo(() => {\n    return JSON.stringify((environment.variables || []).map(stripEnvVarUid));\n  }, [environment.variables]);\n\n  useEffect(() => {\n    setPinnedData({ query: '', uids: new Set() });\n  }, [savedValuesJson]);\n\n  // Sync modified state\n  useEffect(() => {\n    const currentValues = formik.values.filter((variable) => variable.name && variable.name.trim() !== '');\n    const currentValuesJson = JSON.stringify(currentValues.map(stripEnvVarUid));\n    const hasActualChanges = currentValuesJson !== savedValuesJson;\n    setIsModified(hasActualChanges);\n  }, [formik.values, savedValuesJson, setIsModified]);\n\n  // Sync draft state\n  useEffect(() => {\n    const timeoutId = setTimeout(() => {\n      const currentValues = formik.values.filter((variable) => variable.name && variable.name.trim() !== '');\n      const currentValuesJson = JSON.stringify(currentValues.map(stripEnvVarUid));\n      const hasActualChanges = currentValuesJson !== savedValuesJson;\n\n      const existingDraftVariables = hasDraftForThisEnv ? draft?.variables : null;\n      const existingDraftJson = existingDraftVariables ? JSON.stringify(existingDraftVariables.map(stripEnvVarUid)) : null;\n\n      if (hasActualChanges) {\n        if (currentValuesJson !== existingDraftJson) {\n          onDraftChange(currentValues);\n        }\n      } else if (hasDraftForThisEnv) {\n        onDraftClear();\n      }\n    }, 300);\n\n    return () => clearTimeout(timeoutId);\n  }, [formik.values, savedValuesJson, environment.uid, hasDraftForThisEnv, draft?.variables, onDraftChange, onDraftClear]);\n\n  const ErrorMessage = ({ name, index }) => {\n    const meta = formik.getFieldMeta(name);\n    const id = `error-${name}-${index}`;\n\n    const isLastRow = index === formik.values.length - 1;\n    const variable = formik.values[index];\n    const isEmptyRow = !variable?.name || variable.name.trim() === '';\n\n    if (isLastRow && isEmptyRow) {\n      return null;\n    }\n\n    if (!meta.error || !meta.touched) {\n      return null;\n    }\n    return (\n      <span>\n        <IconAlertCircle id={id} className=\"text-red-600 cursor-pointer\" size={20} />\n        <Tooltip className=\"tooltip-mod\" anchorId={id} html={meta.error || ''} />\n      </span>\n    );\n  };\n\n  const handleRemoveVar = useCallback(\n    (id) => {\n      const currentValues = formik.values;\n\n      if (!currentValues || currentValues.length === 0) {\n        return;\n      }\n\n      const lastRow = currentValues[currentValues.length - 1];\n      const isLastEmptyRow = lastRow?.uid === id && (!lastRow.name || lastRow.name.trim() === '');\n\n      if (isLastEmptyRow) {\n        return;\n      }\n\n      const filteredValues = currentValues.filter((variable) => variable.uid !== id);\n\n      const hasEmptyLastRow\n        = filteredValues.length > 0\n          && (!filteredValues[filteredValues.length - 1].name\n            || filteredValues[filteredValues.length - 1].name.trim() === '');\n\n      const newValues = hasEmptyLastRow\n        ? filteredValues\n        : [\n            ...filteredValues,\n            {\n              uid: uuid(),\n              name: '',\n              value: '',\n              type: 'text',\n              secret: false,\n              enabled: true\n            }\n          ];\n\n      formik.setValues(newValues);\n    },\n    [formik.values]\n  );\n\n  const handleNameChange = (index, e) => {\n    formik.handleChange(e);\n    const isLastRow = index === formik.values.length - 1;\n\n    if (isLastRow) {\n      const newVariable = {\n        uid: uuid(),\n        name: '',\n        value: '',\n        type: 'text',\n        secret: false,\n        enabled: true\n      };\n      setTimeout(() => {\n        formik.setFieldValue(formik.values.length, newVariable, false);\n      }, 0);\n    }\n  };\n\n  const handleNameBlur = (index) => {\n    formik.setFieldTouched(`${index}.name`, true, true);\n  };\n\n  const handleNameKeyDown = (index, e) => {\n    if (e.key === 'Enter') {\n      e.preventDefault();\n      formik.setFieldTouched(`${index}.name`, true, true);\n    }\n  };\n\n  const handleSave = useCallback(() => {\n    const variablesToSave = formik.values.filter((variable) => variable.name && variable.name.trim() !== '');\n    const savedValues = environment.variables || [];\n\n    // Compare without UIDs since they can be different but the actual data is the same\n    const hasChanges = JSON.stringify(variablesToSave.map(stripEnvVarUid)) !== JSON.stringify(savedValues.map(stripEnvVarUid));\n    if (!hasChanges) {\n      toast.error('No changes to save');\n      return;\n    }\n\n    const hasValidationErrors = variablesToSave.some((variable) => {\n      if (!variable.name || variable.name.trim() === '') {\n        return true;\n      }\n      if (!variableNameRegex.test(variable.name)) {\n        return true;\n      }\n      return false;\n    });\n\n    if (hasValidationErrors) {\n      toast.error('Please fix validation errors before saving');\n      return;\n    }\n\n    onSave(cloneDeep(variablesToSave))\n      .then(() => {\n        toast.success('Changes saved successfully');\n        onDraftClear();\n        const newValues = [\n          ...variablesToSave,\n          {\n            uid: uuid(),\n            name: '',\n            value: '',\n            type: 'text',\n            secret: false,\n            enabled: true\n          }\n        ];\n        formik.resetForm({ values: newValues });\n        setIsModified(false);\n      })\n      .catch((error) => {\n        console.error(error);\n        toast.error('An error occurred while saving the changes');\n      });\n  }, [formik.values, environment.variables, onSave, onDraftClear, setIsModified]);\n\n  const handleReset = useCallback(() => {\n    const originalVars = environment.variables || [];\n    const resetValues = [\n      ...originalVars,\n      {\n        uid: uuid(),\n        name: '',\n        value: '',\n        type: 'text',\n        secret: false,\n        enabled: true\n      }\n    ];\n    formik.resetForm({ values: resetValues });\n    setIsModified(false);\n  }, [environment.variables, setIsModified]);\n\n  const handleSaveRef = useRef(handleSave);\n  handleSaveRef.current = handleSave;\n\n  useEffect(() => {\n    const handleSaveEvent = () => {\n      handleSaveRef.current();\n    };\n\n    window.addEventListener('environment-save', handleSaveEvent);\n\n    return () => {\n      window.removeEventListener('environment-save', handleSaveEvent);\n    };\n  }, []);\n\n  const filteredVariables = useMemo(() => {\n    const allVariables = formik.values.map((variable, index) => ({ variable, index }));\n    if (!searchQuery?.trim()) {\n      return allVariables;\n    }\n\n    const query = searchQuery.toLowerCase().trim();\n\n    const effectivePins = pinnedData.query === searchQuery ? pinnedData.uids : new Set();\n    return allVariables.filter(({ variable }) => {\n      if (effectivePins.has(variable.uid)) return true;\n      const nameMatch = variable.name ? variable.name.toLowerCase().includes(query) : false;\n      const valueText\n        = typeof variable.value === 'string'\n          ? variable.value\n          : typeof variable.value === 'number' || typeof variable.value === 'boolean'\n            ? String(variable.value)\n            : '';\n      const valueMatch = valueText.toLowerCase().includes(query);\n      return !!(nameMatch || valueMatch);\n    });\n  }, [formik.values, searchQuery, pinnedData]);\n\n  const isSearchActive = !!searchQuery?.trim();\n\n  return (\n    <StyledWrapper className={resizing ? 'is-resizing' : ''}>\n      {isSearchActive && filteredVariables.length === 0 ? (\n        <div className=\"no-results\">No results found for &ldquo;{searchQuery.trim()}&rdquo;</div>\n      ) : (\n        <TableVirtuoso\n          className=\"table-container\"\n          style={{ height: tableHeight }}\n          components={{ TableRow }}\n          data={filteredVariables}\n          totalListHeightChanged={handleTotalHeightChanged}\n          fixedHeaderContent={() => (\n            <tr>\n              <td className=\"text-center\"></td>\n              <td style={{ width: columnWidths.name }}>\n                Name\n                <div\n                  className={`resize-handle ${resizing === 'name' ? 'resizing' : ''}`}\n                  style={{ height: tableHeight > 0 ? `${tableHeight}px` : undefined }}\n                  onMouseDown={(e) => handleResizeStart(e, 'name')}\n                />\n              </td>\n              <td style={{ width: columnWidths.value }}>Value</td>\n              <td className=\"text-center\">Secret</td>\n              <td></td>\n            </tr>\n          )}\n          fixedItemHeight={35}\n          computeItemKey={(virtualIndex, item) => `${environment.uid}-${item.index}`}\n          itemContent={(virtualIndex, { variable, index: actualIndex }) => {\n            const isLastRow = actualIndex === formik.values.length - 1;\n            const isEmptyRow = !variable.name || variable.name.trim() === '';\n            const isLastEmptyRow = isLastRow && isEmptyRow;\n\n            return (\n              <>\n                <td className=\"text-center\">\n                  {!isLastEmptyRow && (\n                    <input\n                      type=\"checkbox\"\n                      className=\"mousetrap\"\n                      name={`${actualIndex}.enabled`}\n                      checked={variable.enabled}\n                      onChange={formik.handleChange}\n                    />\n                  )}\n                </td>\n                <td style={{ width: columnWidths.name }}>\n                  <div className=\"flex items-center\">\n                    <div className=\"name-cell-wrapper\">\n                      <input\n                        type=\"text\"\n                        autoComplete=\"off\"\n                        autoCorrect=\"off\"\n                        autoCapitalize=\"off\"\n                        spellCheck=\"false\"\n                        className=\"mousetrap\"\n                        id={`${actualIndex}.name`}\n                        name={`${actualIndex}.name`}\n                        value={variable.name}\n                        placeholder={!variable.value || (typeof variable.value === 'string' && variable.value.trim() === '') ? 'Name' : ''}\n                        onChange={(e) => handleNameChange(actualIndex, e)}\n                        onFocus={() => handleRowFocus(variable.uid)}\n                        onBlur={() => {\n                          handleNameBlur(actualIndex);\n                        }}\n                        onKeyDown={(e) => handleNameKeyDown(actualIndex, e)}\n                      />\n                    </div>\n                    <ErrorMessage name={`${actualIndex}.name`} index={actualIndex} />\n                  </div>\n                </td>\n                <td\n                  className=\"flex flex-row flex-nowrap items-center\"\n                  style={{ width: columnWidths.value }}\n                >\n                  <div\n                    className=\"overflow-hidden grow w-full relative\"\n                    onFocus={() => handleRowFocus(variable.uid)}\n                  >\n                    <MultiLineEditor\n                      theme={storedTheme}\n                      collection={_collection}\n                      name={`${actualIndex}.value`}\n                      value={variable.value}\n                      placeholder={isLastEmptyRow ? 'Value' : ''}\n                      isSecret={variable.secret}\n                      readOnly={typeof variable.value !== 'string'}\n                      onChange={(newValue) => {\n                        formik.setFieldValue(`${actualIndex}.value`, newValue, true);\n                        // Clear ephemeral metadata when user manually edits the value\n                        if (variable.ephemeral) {\n                          formik.setFieldValue(`${actualIndex}.ephemeral`, undefined, false);\n                          formik.setFieldValue(`${actualIndex}.persistedValue`, undefined, false);\n                        }\n                      }}\n                      onSave={handleSave}\n                    />\n                  </div>\n                  {typeof variable.value !== 'string' && (\n                    <span className=\"ml-2 flex items-center\">\n                      <IconInfoCircle id={`${variable.uid}-disabled-info-icon`} className=\"text-muted\" size={16} />\n                      <Tooltip\n                        anchorId={`${variable.uid}-disabled-info-icon`}\n                        content=\"Non-string values set via scripts are read-only and can only be updated through scripts.\"\n                        place=\"top\"\n                      />\n                    </span>\n                  )}\n                  {renderExtraValueContent && renderExtraValueContent(variable)}\n                </td>\n                <td className=\"text-center\">\n                  {!isLastEmptyRow && (\n                    <input\n                      type=\"checkbox\"\n                      className=\"mousetrap\"\n                      name={`${actualIndex}.secret`}\n                      checked={variable.secret}\n                      onChange={formik.handleChange}\n                    />\n                  )}\n                </td>\n                <td>\n                  {!isLastEmptyRow && (\n                    <button onClick={() => handleRemoveVar(variable.uid)}>\n                      <IconTrash strokeWidth={1.5} size={18} />\n                    </button>\n                  )}\n                </td>\n              </>\n            );\n          }}\n        />\n      )}\n\n      <div className=\"button-container\">\n        <div className=\"flex items-center\">\n          <button type=\"button\" className=\"submit\" onClick={handleSave} data-testid=\"save-env\">\n            Save\n          </button>\n          <button type=\"button\" className=\"submit reset ml-2\" onClick={handleReset} data-testid=\"reset-env\">\n            Reset\n          </button>\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default EnvironmentVariablesTable;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/CollapsibleSection/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n\n  &.collapsed {\n    flex-shrink: 0;\n\n    .section-content {\n      display: none;\n    }\n  }\n\n  &.expanded {\n    flex: 1;\n    min-height: 0;\n    overflow: hidden;\n\n    .section-content {\n      flex: 1;\n      overflow-y: auto;\n      overflow-x: hidden;\n    }\n  }\n\n  .section-header {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 8px 12px;\n    cursor: pointer;\n    user-select: none;\n    border-radius: 4px;\n    transition: background 0.15s ease;\n    flex-shrink: 0;\n\n    &:hover {\n      background: ${(props) => props.theme.workspace.button.bg};\n    }\n\n    .section-title-wrapper {\n      display: flex;\n      align-items: center;\n      gap: 6px;\n    }\n\n    .section-icon {\n      color: ${(props) => props.theme.colors.text.muted};\n      transition: transform 0.2s ease;\n\n      &.expanded {\n        transform: rotate(90deg);\n      }\n    }\n\n    .section-title {\n      padding-right: 4px;\n      font-size: 11px;\n      font-weight: 600;\n      text-transform: uppercase;\n      letter-spacing: 0.5px;\n      color: ${(props) => props.theme.sidebar.color};\n    }\n\n    .section-badge {\n      font-size: 10px;\n      padding: 1px 6px;\n      background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n      border-radius: 10px;\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n\n    .section-actions {\n      display: flex;\n      align-items: center;\n      gap: 2px;\n\n      .btn-action {\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        width: 22px;\n        height: 22px;\n        padding: 0;\n        background: transparent;\n        border: none;\n        border-radius: 4px;\n        color: ${(props) => props.theme.colors.text.muted};\n        cursor: pointer;\n        transition: all 0.15s ease;\n\n        &:hover {\n          background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n          color: ${(props) => props.theme.text};\n        }\n      }\n    }\n  }\n\n  .section-content {\n    padding: 4px 0;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/CollapsibleSection/index.js",
    "content": "import React from 'react';\nimport { IconChevronRight } from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\n\nconst CollapsibleSection = ({\n  title,\n  expanded,\n  onToggle,\n  badge,\n  actions,\n  children\n}) => {\n  return (\n    <StyledWrapper className={expanded ? 'expanded' : 'collapsed'}>\n      <div className=\"section-header\" onClick={onToggle}>\n        <div className=\"section-title-wrapper\">\n          <IconChevronRight\n            size={14}\n            strokeWidth={2}\n            className={`section-icon ${expanded ? 'expanded' : ''}`}\n          />\n          <span className=\"section-title\">{title}</span>\n          {badge !== undefined && badge !== null && (\n            <span className=\"section-badge\">{badge}</span>\n          )}\n        </div>\n        {actions && (\n          <div className=\"section-actions\" onClick={(e) => e.stopPropagation()}>\n            {actions}\n          </div>\n        )}\n      </div>\n      <div className=\"section-content\">\n        {children}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default CollapsibleSection;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/Common/ExportEnvironmentModal/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  /* Environment item styling */\n  .environment-item {\n    display: flex;\n    align-items: center;\n    gap: 0.5rem;\n    cursor: pointer;\n    padding: 0.375rem 0.5rem;\n    border-radius: 0.25rem;\n    transition: background-color 0.15s ease;\n\n    .environment-name {\n      color: ${(props) => props.theme.text};\n      overflow: hidden;\n      text-overflow: ellipsis;\n      white-space: nowrap;\n      flex: 1;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/Common/ExportEnvironmentModal/index.js",
    "content": "import React, { useState, useEffect, useMemo } from 'react';\nimport Portal from 'components/Portal/index';\nimport Modal from 'components/Modal';\nimport { exportBrunoEnvironment } from 'utils/exporters/bruno-environment';\nimport { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';\nimport { useDispatch } from 'react-redux';\nimport toast from 'react-hot-toast';\nimport StyledWrapper from './StyledWrapper';\nimport Button from 'ui/Button';\n\nconst ExportEnvironmentModal = ({ onClose, environments = [], environmentType }) => {\n  const dispatch = useDispatch();\n\n  // Helper function to truncate environment names\n  const truncateEnvName = (name) => {\n    if (name.length > 40) {\n      return name.substring(0, 40) + '...';\n    }\n    return name;\n  };\n\n  const [isExporting, setIsExporting] = useState(false);\n  const [filePath, setFilePath] = useState('');\n  const [selectedEnvironments, setSelectedEnvironments] = useState({});\n  const [exportFormat, setExportFormat] = useState(environments.length > 1 ? 'single-file' : 'single-object');\n\n  // Initialize selected environments\n  useEffect(() => {\n    const initialSelection = {};\n\n    // Add all environments and select them by default\n    environments.forEach((env) => {\n      initialSelection[env.uid] = true;\n    });\n\n    setSelectedEnvironments(initialSelection);\n  }, [environments]);\n\n  useEffect(() => {\n    const selectedCount = Object.values(selectedEnvironments).filter(Boolean).length;\n    if (selectedCount <= 1) {\n      setExportFormat('single-object');\n    }\n    if (exportFormat === 'single-object' && selectedCount > 1) {\n      setExportFormat('single-file');\n    }\n  }, [selectedEnvironments]);\n\n  const browse = () => {\n    dispatch(browseDirectory())\n      .then((dirPath) => {\n        if (typeof dirPath === 'string') {\n          setFilePath(dirPath);\n        }\n      })\n      .catch((error) => {\n        setFilePath('');\n        console.error(error);\n      });\n  };\n\n  const handleEnvironmentToggle = (envUid) => {\n    setSelectedEnvironments((prev) => {\n      const newSelection = {\n        ...prev,\n        [envUid]: !prev[envUid]\n      };\n      return newSelection;\n    });\n  };\n\n  const handleSelectAll = () => {\n    const allSelected = environments.every((env) => selectedEnvironments[env.uid]) || false;\n\n    const newSelection = environments.reduce((acc, env) => ({\n      ...acc,\n      [env.uid]: !allSelected\n    }), {}) || {};\n\n    setSelectedEnvironments(newSelection);\n  };\n\n  // Memoized selected environments and count\n  const selectedEnvs = useMemo(() => {\n    return environments.filter((env) => selectedEnvironments[env.uid]) || [];\n  }, [environments, selectedEnvironments]);\n\n  const selectedCount = selectedEnvs.length;\n\n  const exportFormatOptions = useMemo(() => {\n    const isMultiple = selectedCount > 1;\n\n    if (isMultiple) {\n      return [\n        { value: 'single-file', label: 'Single JSON file', description: 'All environments in one JSON array' },\n        { value: 'folder', label: 'Separate files in folder', description: 'Each environment as a separate JSON file', disabled: false }\n      ];\n    }\n\n    return [\n      { value: 'single-object', label: 'Single JSON file', description: 'Export as a single environment JSON object' },\n      { value: 'folder', label: 'Separate files in folder', description: 'Each environment as a separate JSON file', disabled: true }\n    ];\n  }, [selectedCount, exportFormat]);\n\n  const handleExport = async () => {\n    try {\n      setIsExporting(true);\n\n      if (!filePath) {\n        toast.error('Please select a location to save the files');\n        return;\n      }\n\n      if (selectedCount === 0) {\n        toast.error('Please select at least one environment to export');\n        return;\n      }\n\n      await exportBrunoEnvironment({ environments: selectedEnvs, environmentType, filePath, exportFormat });\n\n      const successMessage = exportFormat === 'folder'\n        ? `Environments exported successfully to bruno-${environmentType}-environments folder`\n        : 'Environment(s) exported successfully';\n      toast.success(successMessage);\n      onClose();\n    } catch (error) {\n      console.error('Export error:', error);\n      toast.error(error.message || 'Failed to export environments');\n    } finally {\n      setIsExporting(false);\n    }\n  };\n\n  return (\n    <Portal>\n      <StyledWrapper>\n        <Modal\n          size=\"md\"\n          title=\"Export Environments\"\n          hideFooter={true}\n          handleCancel={onClose}\n        >\n          <div className=\"py-2\">\n            {/* Environments Section */}\n            <div className=\"mb-4\">\n              {environments && environments.length > 0 ? (\n                <div className=\"flex flex-col h-full\">\n                  <div className=\"flex justify-between items-center mb-2 pb-1\">\n                    <h3 className=\"font-medium text-theme\">\n                      {environmentType === 'global' ? 'Global Environments' : 'Collection Environments'}\n                    </h3>\n                    <button\n                      type=\"button\"\n                      onClick={handleSelectAll}\n                      className=\"text-xs text-link px-1 py-0.5 rounded transition-colors\"\n                    >\n                      {environments.every((env) => selectedEnvironments[env.uid]) ? 'Deselect All' : 'Select All'}\n                    </button>\n                  </div>\n                  <div className=\"flex flex-col gap-1 flex-1 overflow-y-auto\">\n                    {environments.map((env) => (\n                      <label key={env.uid} className=\"environment-item\">\n                        <input\n                          type=\"checkbox\"\n                          checked={selectedEnvironments[env.uid] || false}\n                          onChange={() => handleEnvironmentToggle(env.uid)}\n                          disabled={isExporting}\n                          className=\"w-3.5 h-3.5 flex-shrink-0\"\n                        />\n                        <span className=\"environment-name\">{truncateEnvName(env.name)}</span>\n                      </label>\n                    ))}\n                  </div>\n                </div>\n              ) : (\n                <div className=\"flex flex-col h-full\">\n                  <div className=\"flex justify-between items-center mb-2 pb-1\">\n                    <h3 className=\"font-medium text-theme\">\n                      {environmentType === 'global' ? 'Global Environments' : 'Collection Environments'}\n                    </h3>\n                  </div>\n                  <div className=\"flex items-center justify-center flex-1 p-4 text-center\">\n                    <span className=\"text-xs text-muted\">\n                      No {environmentType === 'global' ? 'global' : 'collection'} environments\n                    </span>\n                  </div>\n                </div>\n              )}\n            </div>\n\n            {/* Export Format Section */}\n            {selectedCount > 0 && (\n              <div className=\"mb-4\">\n                <label className=\"block font-medium mb-2 text-theme\">\n                  Export Format\n                </label>\n                <div className=\"space-y-2\">\n                  {exportFormatOptions.map((option) => (\n                    <label key={option.value} className={`flex items-start p-2 rounded transition-colors ${option.disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'}`}>\n                      <input\n                        type=\"radio\"\n                        name=\"exportFormat\"\n                        value={option.value}\n                        checked={exportFormat === option.value}\n                        onChange={(e) => setExportFormat(e.target.value)}\n                        disabled={isExporting || option.disabled}\n                        className={`mt-0.5 mr-3 w-4 h-4 ${option.disabled ? 'cursor-not-allowed' : 'cursor-pointer'}`}\n                      />\n                      <div>\n                        <div className={`font-medium ${option.disabled ? 'text-muted' : 'text-theme'}`}>{option.label}</div>\n                        <div className=\"text-xs text-muted\">{option.description}</div>\n                      </div>\n                    </label>\n                  ))}\n                </div>\n              </div>\n            )}\n\n            {/* Location Input Section */}\n            <div className=\"mb-4\">\n              <label htmlFor=\"export-location\" className=\"block font-medium mb-2 text-theme\">\n                Location\n              </label>\n              <div className=\"flex flex-col relative items-center\">\n                <input\n                  id=\"export-location\"\n                  type=\"text\"\n                  className={`flex-1 textbox w-full ${isExporting || selectedCount <= 0 ? '' : 'cursor-pointer'}`}\n                  title={filePath}\n                  value={filePath}\n                  onClick={browse}\n                  onChange={(e) => setFilePath(e.target.value)}\n                  disabled={isExporting || selectedCount <= 0}\n                  placeholder=\"Select a target location\"\n                  autoComplete=\"off\"\n                  autoCorrect=\"off\"\n                  autoCapitalize=\"off\"\n                  spellCheck=\"false\"\n                />\n              </div>\n            </div>\n\n            {/* Export Actions */}\n            <div className=\"flex justify-end gap-2 mt-4 pt-3 border-t border-gray-200 dark:border-gray-700\">\n              <Button\n                type=\"button\"\n                size=\"sm\"\n                color=\"secondary\"\n                variant=\"ghost\"\n                onClick={onClose}\n                disabled={isExporting}\n                className=\"mt-2 mr-2\"\n              >\n                Cancel\n              </Button>\n              <Button\n                type=\"button\"\n                size=\"sm\"\n                onClick={handleExport}\n                disabled={isExporting || selectedCount === 0}\n                className=\"mt-2\"\n              >\n                {isExporting ? 'Exporting...' : `Export ${selectedCount || ''} Environment${selectedCount !== 1 ? 's' : ''}`}\n              </Button>\n            </div>\n          </div>\n        </Modal>\n      </StyledWrapper>\n    </Portal>\n  );\n};\n\nexport default ExportEnvironmentModal;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/Common/ImportEnvironmentModal/index.js",
    "content": "import React, { useState } from 'react';\nimport Portal from 'components/Portal';\nimport Modal from 'components/Modal';\nimport toast from 'react-hot-toast';\nimport { useDispatch } from 'react-redux';\nimport importPostmanEnvironment from 'utils/importers/postman-environment';\nimport importBrunoEnvironment from 'utils/importers/bruno-environment';\nimport { readMultipleFiles } from 'utils/importers/file-reader';\nimport { importEnvironment } from 'providers/ReduxStore/slices/collections/actions';\nimport { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';\nimport { toastError } from 'utils/common/error';\nimport { IconFileImport } from '@tabler/icons';\n\nconst ImportEnvironmentModal = ({ type = 'collection', collection, onClose, onEnvironmentCreated }) => {\n  const dispatch = useDispatch();\n  const [isDragOver, setIsDragOver] = useState(false);\n\n  const isGlobal = type === 'global';\n\n  // Validate required props\n  if (!isGlobal && !collection) {\n    console.error('ImportEnvironmentModal: collection prop is required when type is \"collection\"');\n    return null;\n  }\n  const modalTitle = isGlobal ? 'Import Global Environment' : 'Import Environment';\n  const modalTestId = isGlobal ? 'import-global-environment-modal' : 'import-environment-modal';\n  const importTestId = isGlobal ? 'import-global-environment' : 'import-environment';\n\n  const processEnvironments = async (environments, successMessage) => {\n    const validEnvironments = environments.filter((env) => {\n      if (env.name && env.name !== 'undefined') {\n        return true;\n      } else {\n        toast.error('Failed to import environment: env has no name');\n        return false;\n      }\n    });\n\n    if (validEnvironments.length === 0) {\n      toast.error('No valid environments found to import');\n      return;\n    }\n\n    try {\n      // Process environments sequentially to ensure unique name checking considers previously imported environments\n      let importedCount = 0;\n      for (const environment of validEnvironments) {\n        const action = isGlobal\n          ? addGlobalEnvironment({ name: environment.name, variables: environment.variables, color: environment.color })\n          : importEnvironment({ name: environment.name, variables: environment.variables, color: environment.color, collectionUid: collection?.uid });\n\n        await dispatch(action);\n        importedCount++;\n      }\n\n      toast.success(`${importedCount > 1 ? `${importedCount} environments` : 'Environment'} imported successfully`);\n    } catch (error) {\n      toast.error('An error occurred while importing the environment(s)');\n      console.error(error);\n      throw error;\n    }\n  };\n\n  const detectEnvironmentFormat = (data) => {\n    // bruno environment `single-object` export type\n    if (data.info && data.info.type === 'bruno-environment') {\n      return 'bruno';\n    } else if (Array.isArray(data)) {\n      // bruno environment`single-file` export type\n      return data.some((env) => env.info && env.info.type === 'bruno-environment') ? 'bruno' : 'postman';\n    } else if (data.id && data.values) {\n      // postman environment\n      return 'postman';\n    }\n    return 'bruno';\n  };\n\n  const handleImportEnvironment = async (files) => {\n    try {\n      // Read and parse all files\n      const parsedFiles = await readMultipleFiles(Array.from(files));\n\n      // Detect format from first file's content\n      const format = detectEnvironmentFormat(parsedFiles[0].content);\n      let environments;\n\n      if (format === 'postman') {\n        environments = await importPostmanEnvironment(parsedFiles);\n      } else {\n        environments = await importBrunoEnvironment(parsedFiles);\n      }\n\n      await processEnvironments(environments);\n      onClose();\n      if (onEnvironmentCreated) {\n        onEnvironmentCreated();\n      }\n    } catch (err) {\n      toastError(err, 'Import environment failed');\n    }\n  };\n\n  const handleFileSelect = () => {\n    const input = document.createElement('input');\n    input.type = 'file';\n    input.multiple = true;\n    input.accept = '.json';\n    input.onchange = (e) => {\n      if (e.target.files && e.target.files.length > 0) {\n        handleImportEnvironment(e.target.files);\n      }\n    };\n    input.click();\n  };\n\n  const handleDragOver = (e) => {\n    e.preventDefault();\n    setIsDragOver(true);\n  };\n\n  const handleDragLeave = (e) => {\n    e.preventDefault();\n    setIsDragOver(false);\n  };\n\n  const handleDrop = (e) => {\n    e.preventDefault();\n    setIsDragOver(false);\n\n    const files = Array.from(e.dataTransfer.files);\n    if (files.length > 0) {\n      handleImportEnvironment(files);\n    }\n  };\n\n  return (\n    <Portal>\n      <Modal size=\"md\" title={modalTitle} hideFooter={true} handleConfirm={onClose} handleCancel={onClose} dataTestId={modalTestId}>\n        <div className=\"py-2\">\n          <div\n            className={`flex justify-center flex-col items-center w-full dark:bg-zinc-700 rounded-lg border-2 border-dashed p-12 text-center cursor-pointer transition-colors focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-2 ${\n              isDragOver\n                ? 'border-amber-400 bg-amber-50 dark:bg-amber-900/20'\n                : 'border-zinc-300 dark:border-zinc-400 hover:border-zinc-400'\n            }`}\n            onClick={handleFileSelect}\n            onDragOver={handleDragOver}\n            onDragLeave={handleDragLeave}\n            onDrop={handleDrop}\n            data-testid={importTestId}\n          >\n            <IconFileImport size={64} />\n            <span className=\"mt-2 block font-medium\">\n              {isDragOver ? 'Drop your environment files here' : 'Import your environments'}\n            </span>\n            <span className=\"mt-1 block text-xs text-muted\">\n              Drag & drop JSON files/folders or click to browse. Supports both Bruno and Postman formats.\n            </span>\n          </div>\n        </div>\n      </Modal>\n    </Portal>\n  );\n};\n\nexport default ImportEnvironmentModal;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/ConfirmCloseEnvironment/index.js",
    "content": "import React from 'react';\nimport { IconAlertTriangle } from '@tabler/icons';\nimport Modal from 'components/Modal';\nimport Portal from 'components/Portal';\nimport Button from 'ui/Button';\n\nconst ConfirmCloseEnvironment = ({ onCancel, onCloseWithoutSave, onSaveAndClose, isGlobal, isDotEnv }) => {\n  let settingsLabel = 'collection environment settings';\n  if (isDotEnv) {\n    settingsLabel = '.env file';\n  } else if (isGlobal) {\n    settingsLabel = 'global environment settings';\n  }\n\n  return (\n    <Portal>\n      <Modal\n        size=\"md\"\n        title=\"Unsaved changes\"\n        disableEscapeKey={true}\n        disableCloseOnOutsideClick={true}\n        closeModalFadeTimeout={150}\n        handleCancel={onCancel}\n        hideFooter={true}\n      >\n        <div className=\"flex items-center font-normal\">\n          <IconAlertTriangle size={32} strokeWidth={1.5} className=\"text-yellow-600\" />\n          <h1 className=\"ml-2 text-lg font-medium\">Hold on...</h1>\n        </div>\n        <div className=\"font-normal mt-4\">\n          You have unsaved changes in {settingsLabel}.\n        </div>\n\n        <div className=\"flex justify-between mt-6\">\n          <div>\n            <Button color=\"danger\" onClick={onCloseWithoutSave}>\n              Don't Save\n            </Button>\n          </div>\n          <div className=\"flex gap-2\">\n            <Button size=\"sm\" color=\"secondary\" variant=\"ghost\" onClick={onCancel}>\n              Cancel\n            </Button>\n            <Button onClick={onSaveAndClose}>\n              Save\n            </Button>\n          </div>\n        </div>\n      </Modal>\n    </Portal>\n  );\n};\n\nexport default ConfirmCloseEnvironment;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/DotEnvFileDetails/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n  background: ${(props) => props.theme.bg};\n\n  .header {\n    position: relative;\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 16px 20px 8px 20px;\n    flex-shrink: 0;\n\n    .title {\n      font-size: ${(props) => props.theme.font.size.base};\n      font-weight: 500;\n      color: ${(props) => props.theme.text};\n      margin: 0;\n    }\n\n    .actions {\n      display: flex;\n      align-items: center;\n      gap: 12px;\n\n      .view-toggle {\n        display: flex;\n        border: 1px solid ${(props) => props.theme.border.border0};\n        border-radius: 4px;\n        overflow: hidden;\n\n        .toggle-btn {\n          padding: 4px 12px;\n          font-size: 12px;\n          border: none;\n          background: transparent;\n          color: ${(props) => props.theme.colors.text.muted};\n          cursor: pointer;\n          transition: all 0.15s ease;\n\n          &:first-child {\n            border-right: 1px solid ${(props) => props.theme.border.border0};\n          }\n\n          &:hover {\n            background: ${(props) => props.theme.sidebar.bg};\n          }\n\n          &.active {\n            background: ${(props) => props.theme.brand};\n            color: ${(props) => props.theme.bg};\n          }\n        }\n      }\n\n      .action-btn {\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        padding: 6px;\n        border: none;\n        background: transparent;\n        color: ${(props) => props.theme.colors.text.muted};\n        cursor: pointer;\n        border-radius: 4px;\n        transition: all 0.15s ease;\n\n        &:hover {\n          background: ${(props) => props.theme.sidebar.bg};\n          color: ${(props) => props.theme.text};\n        }\n\n        &.delete-btn:hover {\n          color: ${(props) => props.theme.colors.text.danger};\n        }\n      }\n    }\n  }\n\n  .content {\n    flex: 1;\n    overflow: hidden;\n    display: flex;\n    flex-direction: column;\n    padding: 0 20px 20px 20px;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/DotEnvFileDetails/index.js",
    "content": "import React, { useState } from 'react';\nimport { IconTrash } from '@tabler/icons';\nimport DeleteDotEnvFile from 'components/Environments/EnvironmentSettings/DeleteDotEnvFile';\nimport StyledWrapper from './StyledWrapper';\n\nconst DotEnvFileDetails = ({\n  title,\n  children,\n  onDelete,\n  dotEnvExists,\n  viewMode,\n  onViewModeChange\n}) => {\n  const [showDeleteModal, setShowDeleteModal] = useState(false);\n\n  const handleDeleteClick = () => {\n    setShowDeleteModal(true);\n  };\n\n  const handleConfirmDelete = () => {\n    if (onDelete) {\n      onDelete();\n    }\n  };\n\n  return (\n    <StyledWrapper>\n      <div className=\"header\">\n        <h3 className=\"title\">{title}</h3>\n        <div className=\"actions\">\n          {dotEnvExists && (\n            <>\n              <div className=\"view-toggle\" role=\"group\" aria-label=\"View mode\">\n                <button\n                  type=\"button\"\n                  className={`toggle-btn ${viewMode === 'table' ? 'active' : ''}`}\n                  onClick={() => onViewModeChange?.('table')}\n                  aria-pressed={viewMode === 'table'}\n                >\n                  Table\n                </button>\n                <button\n                  type=\"button\"\n                  className={`toggle-btn ${viewMode === 'raw' ? 'active' : ''}`}\n                  onClick={() => onViewModeChange?.('raw')}\n                  aria-pressed={viewMode === 'raw'}\n                >\n                  Raw\n                </button>\n              </div>\n              <button type=\"button\" onClick={handleDeleteClick} title=\"Delete .env file\" className=\"action-btn delete-btn\">\n                <IconTrash size={15} strokeWidth={1.5} />\n              </button>\n            </>\n          )}\n        </div>\n      </div>\n\n      {showDeleteModal && (\n        <DeleteDotEnvFile\n          onClose={() => setShowDeleteModal(false)}\n          onConfirm={handleConfirmDelete}\n          filename={title}\n        />\n      )}\n\n      <div className=\"content\">\n        {children}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default DotEnvFileDetails;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/DotEnvFileEditor/DotEnvEmptyState.js",
    "content": "import React from 'react';\nimport { IconFileOff } from '@tabler/icons';\n\nconst DotEnvEmptyState = () => {\n  return (\n    <div className=\"empty-state\">\n      <IconFileOff size={48} strokeWidth={1.5} />\n      <div className=\"title\">No .env File</div>\n      <div className=\"description\">\n        Add a variable below to create a .env file in this location.\n      </div>\n    </div>\n  );\n};\n\nexport default DotEnvEmptyState;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/DotEnvFileEditor/DotEnvErrorMessage.js",
    "content": "import React from 'react';\nimport { IconAlertCircle } from '@tabler/icons';\nimport { Tooltip } from 'react-tooltip';\n\nconst DotEnvErrorMessage = React.memo(({ formik, name, index }) => {\n  const meta = formik.getFieldMeta(name);\n  const id = `error-${name}-${index}`;\n\n  const isLastRow = index === formik.values.length - 1;\n  const variable = formik.values[index];\n  const isEmptyRow = !variable?.name || variable.name.trim() === '';\n\n  if ((isLastRow && isEmptyRow) || !meta.error || !meta.touched) {\n    return null;\n  }\n\n  return (\n    <span>\n      <IconAlertCircle id={id} className=\"text-red-600 cursor-pointer\" size={20} />\n      <Tooltip className=\"tooltip-mod\" anchorId={id} html={meta.error || ''} />\n    </span>\n  );\n});\n\nexport default DotEnvErrorMessage;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/DotEnvFileEditor/DotEnvRawView.js",
    "content": "import React from 'react';\nimport CodeEditor from 'components/CodeEditor';\n\nconst DotEnvRawView = ({\n  collection,\n  item,\n  theme,\n  value,\n  onChange,\n  onSave,\n  onReset,\n  isSaving\n}) => {\n  return (\n    <>\n      <div className=\"raw-editor-container\">\n        <CodeEditor\n          collection={collection}\n          item={item}\n          theme={theme}\n          value={value}\n          onEdit={onChange}\n          onSave={onSave}\n          mode=\"text/plain\"\n          enableVariableHighlighting={false}\n          enableBrunoVarInfo={false}\n        />\n      </div>\n      <div className=\"button-container\">\n        <div className=\"flex items-center\">\n          <button type=\"button\" className=\"submit\" onClick={onSave} disabled={isSaving} data-testid=\"save-dotenv-raw\">\n            {isSaving ? 'Saving...' : 'Save'}\n          </button>\n          <button type=\"button\" className=\"submit reset ml-2\" onClick={onReset} disabled={isSaving} data-testid=\"reset-dotenv-raw\">\n            Reset\n          </button>\n        </div>\n      </div>\n    </>\n  );\n};\n\nexport default DotEnvRawView;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/DotEnvFileEditor/DotEnvTableView.js",
    "content": "import React, { useCallback, useRef } from 'react';\nimport { TableVirtuoso } from 'react-virtuoso';\nimport { IconTrash } from '@tabler/icons';\nimport MultiLineEditor from 'components/MultiLineEditor/index';\nimport DotEnvErrorMessage from './DotEnvErrorMessage';\nimport { MIN_TABLE_HEIGHT } from './utils';\n\nconst TableRow = React.memo(({ children, item }) => (\n  <tr key={item.uid} data-testid={`dotenv-var-row-${item.name}`}>{children}</tr>\n), (prevProps, nextProps) => {\n  const prevUid = prevProps?.item?.uid;\n  const nextUid = nextProps?.item?.uid;\n  return prevUid === nextUid && prevProps.children === nextProps.children;\n});\n\nconst DotEnvTableView = ({\n  formik,\n  theme,\n  showValueColumn,\n  tableHeight,\n  onHeightChange,\n  onNameChange,\n  onNameBlur,\n  onNameKeyDown,\n  onRemoveVar,\n  onSave,\n  onReset,\n  isSaving\n}) => {\n  const handleTotalHeightChanged = useCallback((h) => {\n    onHeightChange(h);\n  }, [onHeightChange]);\n\n  // Use refs for stable access to formik values in callbacks\n  const formikRef = useRef(formik);\n  formikRef.current = formik;\n\n  // Don't memoize itemContent - TableVirtuoso handles this internally\n  // and we need fresh access to formik values\n  const itemContent = (index, variable) => {\n    const currentFormik = formikRef.current;\n    const isLastRow = index === currentFormik.values.length - 1;\n    const isEmptyRow = !variable.name || variable.name.trim() === '';\n    const isLastEmptyRow = isLastRow && isEmptyRow;\n\n    return (\n      <>\n        <td>\n          <div className=\"flex items-center\">\n            <input\n              type=\"text\"\n              autoComplete=\"off\"\n              autoCorrect=\"off\"\n              autoCapitalize=\"off\"\n              spellCheck=\"false\"\n              className=\"mousetrap\"\n              id={`${index}.name`}\n              name={`${index}.name`}\n              value={variable.name}\n              placeholder={isLastEmptyRow ? 'Name' : ''}\n              onChange={(e) => onNameChange(index, e)}\n              onBlur={() => onNameBlur(index)}\n              onKeyDown={(e) => onNameKeyDown(index, e)}\n            />\n            <DotEnvErrorMessage formik={currentFormik} name={`${index}.name`} index={index} />\n          </div>\n        </td>\n        {showValueColumn && (\n          <td className=\"flex flex-row flex-nowrap items-center\">\n            <div className=\"overflow-hidden grow w-full relative\">\n              <MultiLineEditor\n                theme={theme}\n                name={`${index}.value`}\n                value={variable.value}\n                placeholder={isLastEmptyRow ? 'Value' : ''}\n                onChange={(newValue) => currentFormik.setFieldValue(`${index}.value`, newValue, true)}\n                onSave={onSave}\n              />\n            </div>\n          </td>\n        )}\n        <td className=\"delete-col\">\n          {!isLastEmptyRow && (\n            <button\n              type=\"button\"\n              aria-label=\"Delete variable\"\n              onClick={() => onRemoveVar(variable.uid)}\n            >\n              <IconTrash strokeWidth={1.5} size={18} />\n            </button>\n          )}\n        </td>\n      </>\n    );\n  };\n\n  return (\n    <>\n      <TableVirtuoso\n        className=\"table-container\"\n        style={{ height: tableHeight || MIN_TABLE_HEIGHT }}\n        components={{ TableRow }}\n        data={formik.values}\n        totalListHeightChanged={handleTotalHeightChanged}\n        fixedHeaderContent={() => (\n          <tr>\n            <td>Name</td>\n            {showValueColumn && <td>Value</td>}\n            <td className=\"delete-col\"></td>\n          </tr>\n        )}\n        fixedItemHeight={35}\n        computeItemKey={(index, variable) => variable.uid}\n        itemContent={itemContent}\n      />\n      <div className=\"button-container\">\n        <div className=\"flex items-center\">\n          <button type=\"button\" className=\"submit\" onClick={onSave} disabled={isSaving} data-testid=\"save-dotenv\">\n            {isSaving ? 'Saving...' : 'Save'}\n          </button>\n          <button type=\"button\" className=\"submit reset ml-2\" onClick={onReset} disabled={isSaving} data-testid=\"reset-dotenv\">\n            Reset\n          </button>\n        </div>\n      </div>\n    </>\n  );\n};\n\nexport default DotEnvTableView;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/DotEnvFileEditor/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  flex: 1;\n  overflow: hidden;\n\n  .raw-editor-container {\n    flex: 1;\n    overflow: hidden;\n    border-radius: 8px;\n    border: solid 1px ${(props) => props.theme.border.border0};\n\n    .CodeMirror {\n      font-size: ${(props) => props.theme.font.size.base};\n    }\n  }\n\n  .table-container {\n    overflow-y: auto;\n    border-radius: 8px;\n    border: solid 1px ${(props) => props.theme.border.border0};\n  }\n\n  table {\n    width: 100%;\n    border-collapse: collapse;\n    table-layout: fixed;\n    font-size: 12px;\n\n    td {\n      vertical-align: middle;\n      padding: 2px 10px;\n\n      &:first-child {\n        width: 35%;\n      }\n\n      &.delete-col {\n        width: 40px;\n        text-align: center;\n        padding: 2px 4px;\n      }\n    }\n\n    thead {\n      color: ${(props) => props.theme.table.thead.color} !important;\n      background: ${(props) => props.theme.sidebar.bg};\n      font-size: ${(props) => props.theme.font.size.base};\n      user-select: none;\n\n      td {\n        padding: 5px 10px !important;\n        border-bottom: solid 1px ${(props) => props.theme.border.border0};\n        border-right: solid 1px ${(props) => props.theme.border.border0};\n\n        &:last-child {\n          border-right: none;\n        }\n      }\n    }\n\n    tbody {\n      tr {\n        transition: background 0.1s ease;\n\n        &:last-child td {\n          border-bottom: none;\n        }\n\n        td {\n          border-bottom: solid 1px ${(props) => props.theme.border.border0};\n          border-right: solid 1px ${(props) => props.theme.border.border0};\n\n          &:last-child {\n            border-right: none;\n          }\n        }\n      }\n    }\n  }\n\n  .tooltip-mod {\n    max-width: 200px !important;\n  }\n\n  input[type='text'] {\n    width: 100%;\n    border: 1px solid transparent;\n    outline: none !important;\n    background-color: transparent;\n    color: ${(props) => props.theme.text};\n    padding: 0;\n    border-radius: 4px;\n    transition: all 0.15s ease;\n\n    &:focus {\n      outline: none !important;\n    }\n  }\n\n  input[type='checkbox'] {\n    cursor: pointer;\n    width: 14px;\n    height: 14px;\n    accent-color: ${(props) => props.theme.colors.accent};\n    vertical-align: middle;\n    margin: 0;\n  }\n\n  button {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    padding: 4px;\n    color: ${(props) => props.theme.colors.text.muted};\n    background: transparent;\n    border: none;\n    cursor: pointer;\n    border-radius: 4px;\n    transition: color 0.15s ease, background 0.15s ease;\n  }\n\n  .button-container {\n    padding: 12px 2px;\n    background: ${(props) => props.theme.bg};\n    flex-shrink: 0;\n    display: flex;\n    gap: 8px;\n  }\n\n  .submit {\n    padding: 6px 16px;\n    font-size: ${(props) => props.theme.font.size.sm};\n    border-radius: ${(props) => props.theme.border.radius.base};\n    border: none;\n    background: ${(props) => props.theme.brand};\n    color: ${(props) => props.theme.bg};\n    cursor: pointer;\n    transition: opacity 0.15s ease;\n\n    &:hover {\n      opacity: 0.9;\n    }\n  }\n\n  .reset {\n    background: transparent;\n    padding: 6px 16px;\n    color: ${(props) => props.theme.brand};\n    &:hover {\n      opacity: 0.9;\n    }\n  }\n\n  .empty-state {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    padding: 40px 20px;\n    color: ${(props) => props.theme.colors.text.muted};\n\n    svg {\n      opacity: 0.4;\n      margin-bottom: 12px;\n    }\n\n    .title {\n      font-size: 13px;\n      font-weight: 500;\n      margin-bottom: 8px;\n    }\n\n    .description {\n      font-size: 12px;\n      text-align: center;\n      max-width: 300px;\n      line-height: 1.5;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/DotEnvFileEditor/index.js",
    "content": "import React, { useCallback, useRef, useMemo, useEffect, useState } from 'react';\nimport { useTheme } from 'providers/Theme';\nimport { uuid } from 'utils/common';\nimport { useFormik } from 'formik';\nimport { variableNameRegex } from 'utils/common/regex';\nimport toast from 'react-hot-toast';\nimport useDeferredLoading from 'hooks/useDeferredLoading';\n\nimport StyledWrapper from './StyledWrapper';\nimport DotEnvTableView from './DotEnvTableView';\nimport DotEnvRawView from './DotEnvRawView';\nimport DotEnvEmptyState from './DotEnvEmptyState';\nimport { variablesToRaw, rawToVariables, MIN_TABLE_HEIGHT } from './utils';\n\nconst DotEnvFileEditor = ({\n  variables,\n  onSave,\n  onSaveRaw,\n  isModified,\n  setIsModified,\n  dotEnvExists,\n  rawContent,\n  viewMode = 'table',\n  collection,\n  item\n}) => {\n  const { displayedTheme } = useTheme();\n  const [tableHeight, setTableHeight] = useState(MIN_TABLE_HEIGHT);\n  // Derive a single baseline raw value for consistent dirty-tracking\n  const baselineRaw = rawContent ?? variablesToRaw(variables || []);\n  const initialRawValue = baselineRaw;\n  const [rawValue, setRawValue] = useState(initialRawValue);\n  const [prevViewMode, setPrevViewMode] = useState(viewMode);\n  const [isSaving, setIsSaving] = useState(false);\n  const showSaving = useDeferredLoading(isSaving, 200);\n\n  const formikRef = useRef(null);\n\n  const initialValues = useMemo(() => {\n    const vars = (variables || []).map((v) => ({\n      ...v,\n      uid: v.uid || uuid()\n    }));\n    return [\n      ...vars,\n      {\n        uid: uuid(),\n        name: '',\n        value: ''\n      }\n    ];\n  }, [variables]);\n\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: initialValues,\n    validate: (values) => {\n      const errors = {};\n      values.forEach((variable, index) => {\n        const isLastRow = index === values.length - 1;\n        const isEmptyRow = !variable.name || variable.name.trim() === '';\n\n        if (isLastRow && isEmptyRow) {\n          return;\n        }\n\n        if (!variable.name || variable.name.trim() === '') {\n          if (!errors[index]) errors[index] = {};\n          errors[index].name = 'Name cannot be empty';\n        } else if (!variableNameRegex.test(variable.name)) {\n          if (!errors[index]) errors[index] = {};\n          errors[index].name\n            = 'Name contains invalid characters. Must only contain alphanumeric characters, \"-\", \"_\", \".\" and cannot start with a digit.';\n        }\n      });\n      return Object.keys(errors).length > 0 ? errors : {};\n    },\n    onSubmit: () => {}\n  });\n\n  formikRef.current = formik;\n\n  // Sync raw value with external changes\n  useEffect(() => {\n    setRawValue(baselineRaw);\n  }, [baselineRaw]);\n\n  // Handle view mode switching\n  useEffect(() => {\n    if (viewMode !== prevViewMode) {\n      if (viewMode === 'raw' && prevViewMode === 'table') {\n        const currentVars = formikRef.current.values.filter((v) => v.name && v.name.trim() !== '');\n        const newRawValue = variablesToRaw(currentVars);\n        setRawValue(newRawValue);\n      } else if (viewMode === 'table' && prevViewMode === 'raw') {\n        const parsedVars = rawToVariables(rawValue);\n        const newValues = [\n          ...parsedVars,\n          { uid: uuid(), name: '', value: '' }\n        ];\n        formikRef.current.setValues(newValues);\n      }\n      setPrevViewMode(viewMode);\n    }\n  }, [viewMode, prevViewMode, rawValue]);\n\n  const normalizeForComparison = (vars) => {\n    return vars\n      .filter((v) => v.name && v.name.trim() !== '')\n      .map(({ name, value }) => ({ name, value: value || '' }));\n  };\n\n  const savedValuesJson = useMemo(() => {\n    return JSON.stringify(normalizeForComparison(variables || []));\n  }, [variables]);\n\n  useEffect(() => {\n    if (viewMode === 'raw') {\n      const hasRawChanges = rawValue !== baselineRaw;\n      setIsModified(hasRawChanges);\n    } else {\n      const currentValuesJson = JSON.stringify(normalizeForComparison(formik.values));\n      const hasActualChanges = currentValuesJson !== savedValuesJson;\n      setIsModified(hasActualChanges);\n    }\n  }, [formik.values, savedValuesJson, setIsModified, viewMode, rawValue, baselineRaw]);\n\n  // Ref for stable formik.values access\n  const valuesRef = useRef(formik.values);\n  valuesRef.current = formik.values;\n\n  const handleRemoveVar = useCallback((id) => {\n    const currentValues = valuesRef.current;\n\n    if (!currentValues || currentValues.length === 0) {\n      return;\n    }\n\n    const lastRow = currentValues[currentValues.length - 1];\n    const isLastEmptyRow = lastRow?.uid === id && (!lastRow.name || lastRow.name.trim() === '');\n\n    if (isLastEmptyRow) {\n      return;\n    }\n\n    const filteredValues = currentValues.filter((variable) => variable.uid !== id);\n\n    const hasEmptyLastRow\n      = filteredValues.length > 0\n        && (!filteredValues[filteredValues.length - 1].name\n          || filteredValues[filteredValues.length - 1].name.trim() === '');\n\n    const newValues = hasEmptyLastRow\n      ? filteredValues\n      : [\n          ...filteredValues,\n          { uid: uuid(), name: '', value: '' }\n        ];\n\n    formikRef.current.setValues(newValues);\n  }, []);\n\n  const handleNameChange = useCallback((index, e) => {\n    formik.handleChange(e);\n    const isLastRow = index === valuesRef.current.length - 1;\n\n    if (isLastRow) {\n      const newVariable = { uid: uuid(), name: '', value: '' };\n      setTimeout(() => {\n        formik.setValues((prev) => {\n          const lastRow = prev[prev.length - 1];\n          if (lastRow?.name?.trim()) {\n            return [...prev, newVariable];\n          }\n          return prev;\n        });\n      }, 0);\n    }\n  }, []);\n\n  const handleNameBlur = useCallback((index) => {\n    formik.setFieldTouched(`${index}.name`, true, true);\n  }, []);\n\n  const handleNameKeyDown = useCallback((index, e) => {\n    if (e.key === 'Enter') {\n      e.preventDefault();\n      formik.setFieldTouched(`${index}.name`, true, true);\n    }\n  }, []);\n\n  const handleSave = useCallback(() => {\n    if (isSaving) return;\n\n    const variablesToSave = formik.values.filter((variable) => variable.name && variable.name.trim() !== '');\n\n    const hasValidationErrors = variablesToSave.some((variable) => {\n      if (!variable.name || variable.name.trim() === '') {\n        return true;\n      }\n      if (!variableNameRegex.test(variable.name)) {\n        return true;\n      }\n      return false;\n    });\n\n    if (hasValidationErrors) {\n      toast.error('Please fix validation errors before saving');\n      return;\n    }\n\n    setIsSaving(true);\n    onSave(variablesToSave)\n      .then(() => {\n        toast.success('Changes saved successfully');\n        const newValues = [\n          ...variablesToSave,\n          { uid: uuid(), name: '', value: '' }\n        ];\n        formik.resetForm({ values: newValues });\n        setIsModified(false);\n        window.dispatchEvent(new Event('dotenv-save-complete'));\n      })\n      .catch((error) => {\n        console.error(error);\n        toast.error('An error occurred while saving the changes');\n        window.dispatchEvent(new Event('dotenv-save-failed'));\n      })\n      .finally(() => {\n        setIsSaving(false);\n      });\n  }, [isSaving, formik.values, onSave, setIsModified]);\n\n  const handleSaveRaw = useCallback(() => {\n    if (isSaving) return;\n\n    if (!onSaveRaw) {\n      toast.error('Raw save is not supported');\n      return;\n    }\n\n    setIsSaving(true);\n    onSaveRaw(rawValue)\n      .then(() => {\n        toast.success('Changes saved successfully');\n        setIsModified(false);\n        window.dispatchEvent(new Event('dotenv-save-complete'));\n      })\n      .catch((error) => {\n        console.error(error);\n        toast.error('An error occurred while saving the changes');\n        window.dispatchEvent(new Event('dotenv-save-failed'));\n      })\n      .finally(() => {\n        setIsSaving(false);\n      });\n  }, [isSaving, rawValue, onSaveRaw, setIsModified]);\n\n  const handleReset = useCallback(() => {\n    if (viewMode === 'raw') {\n      setRawValue(baselineRaw);\n      setIsModified(false);\n    } else {\n      const originalVars = (variables || []).map((v) => ({\n        ...v,\n        uid: v.uid || uuid()\n      }));\n      const resetValues = [\n        ...originalVars,\n        { uid: uuid(), name: '', value: '' }\n      ];\n      formik.resetForm({ values: resetValues });\n      setIsModified(false);\n    }\n  }, [viewMode, baselineRaw, variables, setIsModified]);\n\n  const handleRawChange = useCallback((newValue) => {\n    setRawValue(newValue);\n  }, []);\n\n  // Global save event listener\n  const handleSaveRef = useRef(handleSave);\n  handleSaveRef.current = handleSave;\n\n  const handleSaveRawRef = useRef(handleSaveRaw);\n  handleSaveRawRef.current = handleSaveRaw;\n\n  useEffect(() => {\n    const handleSaveEvent = () => {\n      if (viewMode === 'raw') {\n        handleSaveRawRef.current();\n      } else {\n        handleSaveRef.current();\n      }\n    };\n\n    window.addEventListener('dotenv-save', handleSaveEvent);\n\n    return () => {\n      window.removeEventListener('dotenv-save', handleSaveEvent);\n    };\n  }, [viewMode]);\n\n  // Raw view mode\n  if (viewMode === 'raw') {\n    return (\n      <StyledWrapper>\n        <DotEnvRawView\n          collection={collection}\n          item={item}\n          theme={displayedTheme}\n          value={rawValue}\n          onChange={handleRawChange}\n          onSave={handleSaveRaw}\n          onReset={handleReset}\n          isSaving={showSaving}\n        />\n      </StyledWrapper>\n    );\n  }\n\n  // Empty state (no .env file exists yet)\n  const showEmptyState = !dotEnvExists && (!variables || variables.length === 0);\n\n  return (\n    <StyledWrapper>\n      {showEmptyState && <DotEnvEmptyState />}\n      <DotEnvTableView\n        formik={formik}\n        theme={displayedTheme}\n        showValueColumn={!showEmptyState}\n        tableHeight={showEmptyState ? MIN_TABLE_HEIGHT : tableHeight}\n        onHeightChange={setTableHeight}\n        onNameChange={handleNameChange}\n        onNameBlur={handleNameBlur}\n        onNameKeyDown={handleNameKeyDown}\n        onRemoveVar={handleRemoveVar}\n        onSave={handleSave}\n        onReset={handleReset}\n        isSaving={showSaving}\n      />\n    </StyledWrapper>\n  );\n};\n\nexport default DotEnvFileEditor;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/DotEnvFileEditor/utils.js",
    "content": "import { uuid } from 'utils/common';\n\nexport const variablesToRaw = (variables) => {\n  return variables\n    .filter((v) => v.name && v.name.trim() !== '')\n    .map((v) => {\n      const value = v.value || '';\n      if (value.includes('\\n') || value.includes('\"') || value.includes('\\'')) {\n        const escapedValue = value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"').replace(/\\n/g, '\\\\n');\n        return `${v.name}=\"${escapedValue}\"`;\n      }\n      return `${v.name}=${value}`;\n    })\n    .join('\\n');\n};\n\nexport const rawToVariables = (rawContent) => {\n  if (!rawContent || rawContent.trim() === '') {\n    return [];\n  }\n\n  const variables = [];\n  const lines = rawContent.split('\\n');\n\n  for (const line of lines) {\n    const trimmedLine = line.trim();\n\n    if (!trimmedLine || trimmedLine.startsWith('#')) {\n      continue;\n    }\n\n    const equalIndex = trimmedLine.indexOf('=');\n    if (equalIndex === -1) {\n      continue;\n    }\n\n    const name = trimmedLine.substring(0, equalIndex).trim();\n    let value = trimmedLine.substring(equalIndex + 1);\n\n    if ((value.startsWith('\"') && value.endsWith('\"')) || (value.startsWith('\\'') && value.endsWith('\\''))) {\n      value = value.slice(1, -1);\n      value = value.replace(/\\\\n/g, '\\n').replace(/\\\\\"/g, '\"').replace(/\\\\\\\\/g, '\\\\');\n    }\n\n    if (name) {\n      variables.push({\n        uid: uuid(),\n        name,\n        value,\n        enabled: true,\n        secret: false\n      });\n    }\n  }\n\n  return variables;\n};\n\nexport const MIN_TABLE_HEIGHT = 35 * 2;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/EnvironmentSelector/EnvironmentListContent/index.js",
    "content": "import React from 'react';\nimport { IconPlus, IconDownload, IconSettings } from '@tabler/icons';\nimport ToolHint from 'components/ToolHint';\nimport ColorBadge from 'components/ColorBadge';\n\nconst EnvironmentListContent = ({\n  environments,\n  activeEnvironmentUid,\n  description,\n  onEnvironmentSelect,\n  onSettingsClick,\n  onCreateClick,\n  onImportClick\n}) => {\n  return (\n    <div>\n      {environments && environments.length > 0 ? (\n        <>\n          <div className=\"environment-list\">\n            <div className=\"dropdown-item no-environment\" onClick={() => onEnvironmentSelect(null)}>\n              <span>No Environment</span>\n            </div>\n            <ToolHint\n              anchorSelect=\"[data-tooltip-content]\"\n              place=\"right\"\n              positionStrategy=\"fixed\"\n              tooltipStyle={{\n                maxWidth: '200px',\n                wordWrap: 'break-word'\n              }}\n              delayShow={1000}\n            >\n              <div>\n                {environments.map((env) => (\n                  <div\n                    key={env.uid}\n                    className={`dropdown-item ${env.uid === activeEnvironmentUid ? 'dropdown-item-active' : ''}`}\n                    onClick={() => onEnvironmentSelect(env)}\n                    data-tooltip-content={env.name}\n                    data-tooltip-hidden={env.name?.length < 90}\n                  >\n                    <ColorBadge color={env.color} size={8} />\n                    <span className=\"max-w-100% truncate no-wrap\">{env.name}</span>\n                  </div>\n                ))}\n              </div>\n            </ToolHint>\n            <div className=\"dropdown-item configure-button\">\n              <button onClick={onSettingsClick} id=\"configure-env\">\n                <IconSettings size={16} strokeWidth={1.5} />\n                <span>Configure</span>\n              </button>\n            </div>\n          </div>\n        </>\n      ) : (\n        <div className=\"empty-state\">\n          <h3>Ready to get started?</h3>\n          <p>{description}</p>\n          <div className=\"space-y-2\">\n            <button onClick={onCreateClick} id=\"create-env\">\n              <IconPlus size={16} strokeWidth={1.5} />\n              Create\n            </button>\n            <button onClick={onImportClick} id=\"import-env\">\n              <IconDownload size={16} strokeWidth={1.5} />\n              Import\n            </button>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default EnvironmentListContent;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/EnvironmentSelector/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  .current-environment {\n    border-radius: ${(props) => props.theme.border.radius.base};\n    padding: 0.25rem 0.3rem 0.25rem 0.5rem;\n    user-select: none;\n    background-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.bg};\n    border: 1px solid ${(props) => props.theme.app.collection.toolbar.environmentSelector.border};\n    line-height: 1rem;\n    transition: all 0.15s ease;\n    height: 24px;\n\n    &:hover {\n      border-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.hoverBorder};\n      background-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.hoverBg};\n    }\n\n    .caret {\n      margin-left: 0.25rem;\n      color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.caret};\n      fill: ${(props) => props.theme.app.collection.toolbar.environmentSelector.caret};\n      align-self: center;\n    }\n\n    .env-icon {\n      margin-right: 0.25rem;\n      color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.icon};\n    }\n\n    .env-text {\n      color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.text};\n      display: block;\n    }\n\n    .env-separator {\n      background-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.separator};\n    }\n\n    .env-text-inactive {\n      color: ${(props) => props.theme.colors.text.muted};\n      font-size: ${(props) => props.theme.font.size.sm};\n    }\n\n    &.no-environments {\n      color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.noEnvironment.text};\n      background-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.noEnvironment.bg};\n      border: 1px dashed ${(props) => props.theme.app.collection.toolbar.environmentSelector.noEnvironment.border};\n\n      &:hover {\n        border-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.noEnvironment.hoverBorder};\n        background-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.noEnvironment.hoverBg};\n      }\n    }\n  }\n\n  .tippy-box {\n    width: ${(props) => props.width}px;\n    min-width: 12rem;\n    max-width: 650px !important;\n    min-height: 15.5rem;\n    max-height: 75vh;\n    font-size: ${(props) => props.theme.font.size.base};\n    position: relative;\n    overflow: hidden;\n  }\n\n  .configure-button {\n    position: absolute;\n    bottom: 0;\n    left: 0;\n    right: 0;\n    background-color: ${(props) => props.theme.dropdown.bg};\n    border-top: 0.0625rem solid ${(props) => props.theme.dropdown.separator};\n    z-index: 10;\n    margin: 0;\n\n    &:hover {\n      background-color: ${(props) => props.theme.dropdown.bg + ' !important'};\n    }\n\n    button {\n      color: ${(props) => props.theme.text};\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      width: 100%;\n      gap: 0.5rem;\n    }\n  }\n\n  .tab-button {\n    color: ${(props) => props.theme.colors.text.subtext0};\n    font-size: ${(props) => props.theme.font.size.sm};\n\n    .tab-content-wrapper {\n      position: relative;\n      display: flex;\n      align-items: center;\n      gap: 0.125rem;\n    }\n\n    &.active {\n      color: ${(props) => props.theme.tabs.active.color};\n      border-bottom-color: ${(props) => props.theme.tabs.active.border};\n    }\n\n    &.inactive {\n      border-bottom-color: transparent;\n    }\n  }\n\n  .tab-content {\n    flex: 1;\n    display: flex;\n    flex-direction: column;\n    overflow: hidden;\n  }\n\n  .environment-list {\n    flex: 1;\n    overflow-y: auto;\n    max-height: calc(75vh - 8rem);\n    padding-bottom: 2.625rem;\n  }\n\n  .dropdown-item-list {\n    max-height: 75vh;\n    overflow-y: scroll;\n  }\n\n  .empty-state {\n    max-width: 20rem;\n    margin: 0 auto;\n    padding: 0.35rem 0.6rem;\n    text-align: center;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    min-height: 12.5rem;\n\n    h3 {\n      color: ${(props) => props.theme.text};\n      font-size: 1rem;\n      font-weight: 500;\n      margin-bottom: 0.5rem;\n      line-height: 1.4;\n    }\n\n    p {\n      color: ${(props) => props.theme.text};\n      opacity: 0.75;\n      font-size: ${(props) => props.theme.font.size.xs};\n      line-height: 1.5;\n      margin-bottom: 1rem;\n      max-width: 11.875rem;\n      margin: 0 auto;\n      margin-bottom: 1rem;\n    }\n\n    .space-y-2 {\n      width: 100%;\n      align-self: stretch;\n    }\n\n    .space-y-2 > button {\n      border: 0.0625rem solid ${(props) => props.theme.text};\n      background: transparent;\n      color: ${(props) => props.theme.text};\n      padding: 0.5rem 1rem;\n      border-radius: 0.375rem;\n      width: 100%;\n      margin-bottom: 0.5rem;\n      font-size: ${(props) => props.theme.font.size.sm};\n      font-weight: 500;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      gap: 0.5rem;\n\n      &:hover {\n        background-color: ${(props) => props.theme.dropdown.hoverBg};\n      }\n\n      &:last-child {\n        margin-bottom: 0;\n      }\n    }\n  }\n\n  .no-collection-message {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    padding: 2rem 1rem;\n    color: ${(props) => props.theme.text};\n    font-size: ${(props) => props.theme.font.size.base};\n    line-height: 1.5;\n    text-align: center;\n    opacity: 0.75;\n\n    svg {\n      margin: 0 auto 1rem auto;\n      color: ${(props) => props.theme.text};\n      opacity: 0.5;\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/EnvironmentSelector/index.js",
    "content": "import React, { useMemo, useState, useRef, forwardRef } from 'react';\nimport find from 'lodash/find';\nimport Dropdown from 'components/Dropdown';\nimport { IconWorld, IconDatabase, IconCaretDown } from '@tabler/icons';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { addTab } from 'providers/ReduxStore/slices/tabs';\nimport { selectEnvironment } from 'providers/ReduxStore/slices/collections/actions';\nimport { selectGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';\nimport toast from 'react-hot-toast';\nimport EnvironmentListContent from './EnvironmentListContent/index';\nimport CreateEnvironment from '../EnvironmentSettings/CreateEnvironment';\nimport ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';\nimport CreateGlobalEnvironment from 'components/WorkspaceHome/WorkspaceEnvironments/CreateEnvironment';\nimport ToolHint from 'components/ToolHint';\nimport StyledWrapper from './StyledWrapper';\nimport { transparentize, toColorString, parseToRgb } from 'polished';\n\nconst TABS = [\n  { id: 'collection', label: 'Collection', icon: <IconDatabase size={16} strokeWidth={1.5} /> },\n  { id: 'global', label: 'Global', icon: <IconWorld size={16} strokeWidth={1.5} /> }\n];\n\nconst EMPTY_STATE_DESCRIPTIONS = {\n  collection: 'Create your first environment to begin working with your collection.',\n  global: 'Create your first global environment to begin working across collections.'\n};\n\n/**\n * Generates background color with transparency for environment badges\n */\nconst getEnvBackgroundColor = (color) => (color ? transparentize(1 - 0.12, color) : 'transparent');\n\n/**\n * Calculates the style for an environment badge section\n */\nconst getEnvBadgeStyle = (environment, position, hasOtherEnv) => {\n  const color = environment?.color;\n  const isLeft = position === 'left';\n\n  // Determine border radius based on position and whether other env exists\n  let borderRadius = '0.3rem';\n  if (hasOtherEnv) {\n    borderRadius = isLeft ? '0.3rem 0 0 0.3rem' : '0 0.3rem 0.3rem 0';\n  }\n\n  // Determine padding based on position\n  const padding = isLeft\n    ? hasOtherEnv\n      ? '0.25rem 0.5rem 0.25rem 0.5rem'\n      : '0.25rem 0.3rem 0.25rem 0.5rem'\n    : '0.25rem 0.3rem 0.25rem 0.5rem';\n\n  return {\n    backgroundColor: getEnvBackgroundColor(color),\n    padding,\n    borderRadius\n  };\n};\n\n/**\n * Calculates dropdown width based on longest environment name\n */\nconst calculateDropdownWidth = (environments, globalEnvironments) => {\n  const allEnvironments = [...environments, ...globalEnvironments];\n  if (allEnvironments.length === 0) return 0;\n\n  const maxCharLength = Math.max(...allEnvironments.map((env) => env.name?.length || 0));\n  // 8 pixels per character (rough estimate for average character width)\n  return maxCharLength * 8;\n};\n\n/**\n * Displays a single environment with icon, name, and optional color styling\n */\nconst EnvironmentBadge = ({ environment, icon: Icon }) => {\n  if (!environment) return null;\n\n  const colorStyle = environment.color ? { color: environment.color } : {};\n\n  return (\n    <>\n      <Icon size={14} strokeWidth={1.5} className=\"env-icon\" style={colorStyle} />\n      <ToolHint\n        text={environment.name}\n        toolhintId={`env-${environment.uid}`}\n        place=\"bottom-start\"\n        delayShow={1000}\n        hidden={environment.name?.length < 7}\n      >\n        <span className=\"env-text max-w-24 truncate overflow-hidden\" style={colorStyle}>\n          {environment.name}\n        </span>\n      </ToolHint>\n    </>\n  );\n};\n\n/**\n * Dropdown trigger component showing active environments\n */\nconst DropdownTrigger = forwardRef(({ collectionEnv, globalEnv }, ref) => {\n  const hasAnyEnv = collectionEnv || globalEnv;\n\n  // Empty state - no environments selected\n  if (!hasAnyEnv) {\n    return (\n      <div\n        ref={ref}\n        className=\"current-environment flex align-center justify-center cursor-pointer bg-transparent no-environments\"\n        data-testid=\"environment-selector-trigger\"\n      >\n        <span className=\"env-text-inactive max-w-36 truncate no-wrap\">No Environment</span>\n        <IconCaretDown className=\"caret flex items-center justify-center\" size={12} strokeWidth={2} />\n      </div>\n    );\n  }\n\n  // Only collection env selected - caret goes with collection env\n  if (collectionEnv && !globalEnv) {\n    return (\n      <div\n        ref={ref}\n        className=\"current-environment flex align-center justify-center cursor-pointer bg-transparent\"\n        style={{ padding: 0 }}\n        data-testid=\"environment-selector-trigger\"\n      >\n        <div className=\"flex items-center\" style={getEnvBadgeStyle(collectionEnv, 'left', false)}>\n          <EnvironmentBadge environment={collectionEnv} icon={IconDatabase} />\n          <IconCaretDown className=\"caret flex items-center justify-center\" size={12} strokeWidth={2} />\n        </div>\n      </div>\n    );\n  }\n\n  // Only global env selected - caret goes with global env\n  if (!collectionEnv && globalEnv) {\n    return (\n      <div\n        ref={ref}\n        className=\"current-environment flex align-center justify-center cursor-pointer bg-transparent\"\n        style={{ padding: 0 }}\n        data-testid=\"environment-selector-trigger\"\n      >\n        <div className=\"flex items-center\" style={getEnvBadgeStyle(globalEnv, 'right', false)}>\n          <EnvironmentBadge environment={globalEnv} icon={IconWorld} />\n          <IconCaretDown className=\"caret flex items-center justify-center\" size={12} strokeWidth={2} />\n        </div>\n      </div>\n    );\n  }\n\n  // Both environments selected\n  return (\n    <div\n      ref={ref}\n      className=\"current-environment flex align-center justify-center cursor-pointer bg-transparent\"\n      style={{ padding: 0 }}\n      data-testid=\"environment-selector-trigger\"\n    >\n      {/* Collection Environment Section */}\n      <div className=\"flex items-center\" style={getEnvBadgeStyle(collectionEnv, 'left', true)}>\n        <EnvironmentBadge environment={collectionEnv} icon={IconDatabase} />\n      </div>\n\n      {/* Separator */}\n      <div className=\"env-separator\" style={{ width: '1px', alignSelf: 'stretch' }} />\n\n      {/* Global Environment Section + Caret */}\n      <div className=\"flex items-center\" style={getEnvBadgeStyle(globalEnv, 'right', true)}>\n        <EnvironmentBadge environment={globalEnv} icon={IconWorld} />\n        <IconCaretDown className=\"caret flex items-center justify-center\" size={12} strokeWidth={2} />\n      </div>\n    </div>\n  );\n});\n\nconst EnvironmentSelector = ({ collection }) => {\n  const dispatch = useDispatch();\n  const dropdownTippyRef = useRef();\n  const [activeTab, setActiveTab] = useState('collection');\n  const [showCreateGlobalModal, setShowCreateGlobalModal] = useState(false);\n  const [showImportGlobalModal, setShowImportGlobalModal] = useState(false);\n  const [showCreateCollectionModal, setShowCreateCollectionModal] = useState(false);\n  const [showImportCollectionModal, setShowImportCollectionModal] = useState(false);\n\n  const globalEnvironments = useSelector((state) => state.globalEnvironments.globalEnvironments);\n  const activeGlobalEnvironmentUid = useSelector((state) => state.globalEnvironments.activeGlobalEnvironmentUid);\n  const activeGlobalEnvironment = activeGlobalEnvironmentUid\n    ? find(globalEnvironments, (e) => e.uid === activeGlobalEnvironmentUid)\n    : null;\n\n  const environments = collection?.environments || [];\n  const activeEnvironmentUid = collection?.activeEnvironmentUid;\n  const activeCollectionEnvironment = activeEnvironmentUid\n    ? find(environments, (e) => e.uid === activeEnvironmentUid)\n    : null;\n\n  const dropdownWidth = useMemo(\n    () => calculateDropdownWidth(environments, globalEnvironments),\n    [environments, globalEnvironments]\n  );\n\n  const description = EMPTY_STATE_DESCRIPTIONS[activeTab];\n\n  const hideDropdown = () => dropdownTippyRef.current?.hide();\n\n  const handleEnvironmentSelect = (environment) => {\n    const action\n      = activeTab === 'collection'\n        ? selectEnvironment(environment?.uid || null, collection.uid)\n        : selectGlobalEnvironment({ environmentUid: environment?.uid || null });\n\n    dispatch(action)\n      .then(() => {\n        toast.success(environment ? `Environment changed to ${environment.name}` : 'No Environments are active now');\n        hideDropdown();\n      })\n      .catch(() => {\n        toast.error('An error occurred while selecting the environment');\n      });\n  };\n\n  const handleSettingsClick = () => {\n    const isCollection = activeTab === 'collection';\n    dispatch(\n      addTab({\n        uid: `${collection.uid}-${isCollection ? 'environment' : 'global-environment'}-settings`,\n        collectionUid: collection.uid,\n        type: isCollection ? 'environment-settings' : 'global-environment-settings'\n      })\n    );\n    hideDropdown();\n  };\n\n  const handleCreateClick = () => {\n    if (activeTab === 'collection') {\n      setShowCreateCollectionModal(true);\n    } else {\n      setShowCreateGlobalModal(true);\n    }\n    hideDropdown();\n  };\n\n  const handleImportClick = () => {\n    if (activeTab === 'collection') {\n      setShowImportCollectionModal(true);\n    } else {\n      setShowImportGlobalModal(true);\n    }\n    hideDropdown();\n  };\n\n  const openEnvironmentSettingsTab = (type) => {\n    dispatch(\n      addTab({\n        uid: `${collection.uid}-${type}-settings`,\n        collectionUid: collection.uid,\n        type: `${type}-settings`\n      })\n    );\n  };\n\n  return (\n    <StyledWrapper width={dropdownWidth}>\n      <div className=\"environment-selector flex align-center cursor-pointer\">\n        <Dropdown\n          onCreate={(ref) => (dropdownTippyRef.current = ref)}\n          icon={<DropdownTrigger collectionEnv={activeCollectionEnvironment} globalEnv={activeGlobalEnvironment} />}\n          placement=\"bottom-end\"\n        >\n          {/* Tab Headers */}\n          <div className=\"tab-header flex pt-3 pb-2 px-3\">\n            {TABS.map((tab) => (\n              <button\n                key={tab.id}\n                className={`tab-button whitespace-nowrap pb-[0.375rem] border-b-[0.125rem] bg-transparent flex align-center cursor-pointer transition-all duration-200 mr-[1.25rem] ${\n                  activeTab === tab.id ? 'active' : 'inactive'\n                }`}\n                onClick={() => setActiveTab(tab.id)}\n                data-testid={`env-tab-${tab.id}`}\n              >\n                <span className=\"tab-content-wrapper\">\n                  {tab.icon}\n                  {tab.label}\n                </span>\n              </button>\n            ))}\n          </div>\n\n          {/* Tab Content */}\n          <div className=\"tab-content\">\n            <EnvironmentListContent\n              environments={activeTab === 'collection' ? environments : globalEnvironments}\n              activeEnvironmentUid={activeTab === 'collection' ? activeEnvironmentUid : activeGlobalEnvironmentUid}\n              description={description}\n              onEnvironmentSelect={handleEnvironmentSelect}\n              onSettingsClick={handleSettingsClick}\n              onCreateClick={handleCreateClick}\n              onImportClick={handleImportClick}\n            />\n          </div>\n        </Dropdown>\n      </div>\n\n      {showCreateGlobalModal && (\n        <CreateGlobalEnvironment\n          onClose={() => setShowCreateGlobalModal(false)}\n          onEnvironmentCreated={() => openEnvironmentSettingsTab('global-environment')}\n        />\n      )}\n\n      {showImportGlobalModal && (\n        <ImportEnvironmentModal\n          type=\"global\"\n          onClose={() => setShowImportGlobalModal(false)}\n          onEnvironmentCreated={() => openEnvironmentSettingsTab('global-environment')}\n        />\n      )}\n\n      {showCreateCollectionModal && (\n        <CreateEnvironment\n          collection={collection}\n          onClose={() => setShowCreateCollectionModal(false)}\n          onEnvironmentCreated={() => openEnvironmentSettingsTab('environment')}\n        />\n      )}\n\n      {showImportCollectionModal && (\n        <ImportEnvironmentModal\n          type=\"collection\"\n          collection={collection}\n          onClose={() => setShowImportCollectionModal(false)}\n          onEnvironmentCreated={() => openEnvironmentSettingsTab('environment')}\n        />\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default EnvironmentSelector;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/EnvironmentSettings/CopyEnvironment/index.js",
    "content": "import Modal from 'components/Modal/index';\nimport Portal from 'components/Portal/index';\nimport { useFormik } from 'formik';\nimport { copyEnvironment } from 'providers/ReduxStore/slices/collections/actions';\nimport { useEffect, useRef } from 'react';\nimport toast from 'react-hot-toast';\nimport { useDispatch } from 'react-redux';\nimport * as Yup from 'yup';\n\nconst CopyEnvironment = ({ collection, environment, onClose }) => {\n  const dispatch = useDispatch();\n  const inputRef = useRef();\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: {\n      name: environment.name + ' - Copy'\n    },\n    validationSchema: Yup.object({\n      name: Yup.string()\n        .min(1, 'must be at least 1 character')\n        .max(50, 'must be 50 characters or less')\n        .required('name is required')\n    }),\n    onSubmit: (values) => {\n      dispatch(copyEnvironment(values.name, environment.uid, collection.uid))\n        .then(() => {\n          toast.success('Environment created in collection');\n          onClose();\n        })\n        .catch(() => toast.error('An error occurred while created the environment'));\n    }\n  });\n\n  useEffect(() => {\n    if (inputRef && inputRef.current) {\n      inputRef.current.focus();\n    }\n  }, [inputRef]);\n\n  const onSubmit = () => {\n    formik.handleSubmit();\n  };\n\n  return (\n    <Portal>\n      <Modal size=\"sm\" title=\"Copy Environment\" confirmText=\"Copy\" handleConfirm={onSubmit} handleCancel={onClose}>\n        <form className=\"bruno-form\" onSubmit={(e) => e.preventDefault()}>\n          <div>\n            <label htmlFor=\"name\" className=\"block font-medium\">\n              New Environment Name\n            </label>\n            <input\n              id=\"environment-name\"\n              type=\"text\"\n              name=\"name\"\n              ref={inputRef}\n              className=\"block textbox mt-2 w-full\"\n              autoComplete=\"off\"\n              autoCorrect=\"off\"\n              autoCapitalize=\"off\"\n              spellCheck=\"false\"\n              onChange={formik.handleChange}\n              value={formik.values.name || ''}\n            />\n            {formik.touched.name && formik.errors.name ? (\n              <div className=\"text-red-500\">{formik.errors.name}</div>\n            ) : null}\n          </div>\n        </form>\n      </Modal>\n    </Portal>\n  );\n};\n\nexport default CopyEnvironment;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/EnvironmentSettings/CreateEnvironment/index.js",
    "content": "import React, { useEffect, useRef } from 'react';\nimport toast from 'react-hot-toast';\nimport { useFormik } from 'formik';\nimport { addEnvironment } from 'providers/ReduxStore/slices/collections/actions';\nimport * as Yup from 'yup';\nimport { useDispatch } from 'react-redux';\nimport Portal from 'components/Portal';\nimport Modal from 'components/Modal';\nimport { validateName, validateNameError } from 'utils/common/regex';\n\nconst CreateEnvironment = ({ collection, onClose, onEnvironmentCreated }) => {\n  const dispatch = useDispatch();\n  const inputRef = useRef();\n\n  const validateEnvironmentName = (name) => {\n    return !collection?.environments?.some((env) => env?.name?.toLowerCase().trim() === name?.toLowerCase().trim());\n  };\n\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: {\n      name: ''\n    },\n    validationSchema: Yup.object({\n      name: Yup.string()\n        .min(1, 'Must be at least 1 character')\n        .max(255, 'Must be 255 characters or less')\n        .test('is-valid-filename', function (value) {\n          const isValid = validateName(value);\n          return isValid ? true : this.createError({ message: validateNameError(value) });\n        })\n        .required('Name is required')\n        .test('duplicate-name', 'Environment already exists', validateEnvironmentName)\n    }),\n    onSubmit: (values) => {\n      dispatch(addEnvironment(values.name, collection.uid))\n        .then(() => {\n          toast.success('Environment created in collection');\n          onClose();\n          // Call the callback if provided\n          if (onEnvironmentCreated) {\n            onEnvironmentCreated();\n          }\n        })\n        .catch(() => toast.error('An error occurred while creating the environment'));\n    }\n  });\n\n  useEffect(() => {\n    if (inputRef && inputRef.current) {\n      inputRef.current.focus();\n    }\n  }, [inputRef]);\n\n  const onSubmit = () => {\n    formik.handleSubmit();\n  };\n\n  return (\n    <Portal>\n      <Modal\n        size=\"md\"\n        title=\"Create Environment\"\n        confirmText=\"Create\"\n        handleConfirm={onSubmit}\n        handleCancel={onClose}\n      >\n        <form className=\"bruno-form\" onSubmit={(e) => e.preventDefault()}>\n          <div>\n            <label htmlFor=\"name\" className=\"block font-medium\">\n              Environment Name\n            </label>\n            <div className=\"flex items-center mt-2\">\n              <input\n                id=\"environment-name\"\n                type=\"text\"\n                name=\"name\"\n                ref={inputRef}\n                className=\"block textbox w-full\"\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n                onChange={formik.handleChange}\n                value={formik.values.name || ''}\n              />\n            </div>\n            {formik.touched.name && formik.errors.name ? (\n              <div className=\"text-red-500\">{formik.errors.name}</div>\n            ) : null}\n          </div>\n        </form>\n      </Modal>\n    </Portal>\n  );\n};\n\nexport default CreateEnvironment;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/EnvironmentSettings/DeleteDotEnvFile/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  button.submit {\n    color: white;\n    background-color: var(--color-background-danger) !important;\n    border: inherit !important;\n\n    &:hover {\n      border: inherit !important;\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/EnvironmentSettings/DeleteDotEnvFile/index.js",
    "content": "import React from 'react';\nimport Portal from 'components/Portal/index';\nimport Modal from 'components/Modal/index';\nimport StyledWrapper from './StyledWrapper';\n\nconst DeleteDotEnvFile = ({ onClose, onConfirm, filename = '.env' }) => {\n  const handleConfirm = () => {\n    onConfirm();\n    onClose();\n  };\n\n  return (\n    <Portal>\n      <StyledWrapper>\n        <Modal\n          size=\"sm\"\n          title={`Delete ${filename} File`}\n          confirmText=\"Delete\"\n          handleConfirm={handleConfirm}\n          handleCancel={onClose}\n          confirmButtonColor=\"danger\"\n        >\n          Are you sure you want to delete <span className=\"font-medium\">{filename}</span> file?\n        </Modal>\n      </StyledWrapper>\n    </Portal>\n  );\n};\n\nexport default DeleteDotEnvFile;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/EnvironmentSettings/DeleteEnvironment/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  button.submit {\n    color: white;\n    background-color: var(--color-background-danger) !important;\n    border: inherit !important;\n\n    &:hover {\n      border: inherit !important;\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/EnvironmentSettings/DeleteEnvironment/index.js",
    "content": "import React from 'react';\nimport Portal from 'components/Portal/index';\nimport toast from 'react-hot-toast';\nimport Modal from 'components/Modal/index';\nimport { deleteEnvironment } from 'providers/ReduxStore/slices/collections/actions';\nimport { useDispatch } from 'react-redux';\nimport StyledWrapper from './StyledWrapper';\n\nconst DeleteEnvironment = ({ onClose, environment, collection }) => {\n  const dispatch = useDispatch();\n  const onConfirm = () => {\n    dispatch(deleteEnvironment(environment.uid, collection.uid))\n      .then(() => {\n        toast.success('Environment deleted successfully');\n        onClose();\n      })\n      .catch(() => toast.error('An error occurred while deleting the environment'));\n  };\n\n  return (\n    <Portal>\n      <StyledWrapper>\n        <Modal\n          size=\"sm\"\n          title=\"Delete Environment\"\n          confirmText=\"Delete\"\n          handleConfirm={onConfirm}\n          handleCancel={onClose}\n          confirmButtonColor=\"danger\"\n        >\n          Are you sure you want to delete <span className=\"font-medium\">{environment.name}</span> ?\n        </Modal>\n      </StyledWrapper>\n    </Portal>\n  );\n};\n\nexport default DeleteEnvironment;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/constants.js",
    "content": "const sensitiveFields = [\n  'request.auth.oauth2.clientSecret',\n  'request.auth.basic.password',\n  'request.auth.digest.password',\n  'request.auth.wsse.password',\n  'request.auth.ntlm.password',\n  'request.auth.awsv4.secretAccessKey',\n  'request.auth.bearer.token'\n];\n\nexport { sensitiveFields };\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js",
    "content": "import React, { useMemo, useCallback } from 'react';\nimport cloneDeep from 'lodash/cloneDeep';\nimport { get } from 'lodash';\nimport { useDispatch } from 'react-redux';\nimport { saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';\nimport { setEnvironmentsDraft, clearEnvironmentsDraft } from 'providers/ReduxStore/slices/collections';\nimport { flattenItems, isItemARequest } from 'utils/collections';\nimport SensitiveFieldWarning from 'components/SensitiveFieldWarning';\nimport EnvironmentVariablesTable from 'components/EnvironmentVariablesTable';\nimport { sensitiveFields } from './constants';\n\nconst EnvironmentVariables = ({ environment, setIsModified, collection, searchQuery = '' }) => {\n  const dispatch = useDispatch();\n\n  const environmentsDraft = collection?.environmentsDraft;\n  const hasDraftForThisEnv = environmentsDraft?.environmentUid === environment.uid;\n\n  // Check for non-secret variables used in sensitive fields\n  const nonSecretSensitiveVarUsageMap = useMemo(() => {\n    const result = {};\n    if (!collection || !environment?.variables) {\n      return result;\n    }\n    const nonSecretVars = environment.variables.filter((v) => v.enabled && !v.secret && v.name);\n    if (!nonSecretVars.length) {\n      return result;\n    }\n    const varNames = new Set(nonSecretVars.map((v) => v.name));\n\n    const checkSensitiveField = (obj, fieldPath) => {\n      const value = get(obj, fieldPath);\n      if (typeof value === 'string') {\n        varNames.forEach((varName) => {\n          if (new RegExp(`\\\\{\\\\{\\\\s*${varName}\\\\s*\\\\}\\\\}`).test(value)) {\n            result[varName] = true;\n          }\n        });\n      }\n    };\n\n    const getObjectToProcess = (item) => {\n      if (isItemARequest(item)) {\n        return item.draft || item;\n      }\n      return item.root;\n    };\n\n    const collectionObj = getObjectToProcess(collection);\n    sensitiveFields.forEach((fieldPath) => {\n      checkSensitiveField(collectionObj, fieldPath);\n    });\n\n    const items = flattenItems(collection.items || []);\n    items.forEach((item) => {\n      const objToProcess = getObjectToProcess(item);\n      sensitiveFields.forEach((fieldPath) => {\n        checkSensitiveField(objToProcess, fieldPath);\n      });\n    });\n    return result;\n  }, [collection, environment]);\n\n  const hasSensitiveUsage = useCallback((name) => !!nonSecretSensitiveVarUsageMap[name], [nonSecretSensitiveVarUsageMap]);\n\n  const handleSave = useCallback(\n    (variables) => {\n      return dispatch(saveEnvironment(cloneDeep(variables), environment.uid, collection.uid));\n    },\n    [dispatch, environment.uid, collection.uid]\n  );\n\n  const handleDraftChange = useCallback(\n    (variables) => {\n      dispatch(\n        setEnvironmentsDraft({\n          collectionUid: collection.uid,\n          environmentUid: environment.uid,\n          variables\n        })\n      );\n    },\n    [dispatch, collection.uid, environment.uid]\n  );\n\n  const handleDraftClear = useCallback(() => {\n    dispatch(clearEnvironmentsDraft({ collectionUid: collection.uid }));\n  }, [dispatch, collection.uid]);\n\n  const renderExtraValueContent = useCallback(\n    (variable) => {\n      if (!variable.secret && hasSensitiveUsage(variable.name)) {\n        return (\n          <SensitiveFieldWarning\n            fieldName={variable.name}\n            warningMessage=\"This variable is used in sensitive fields. Mark it as a secret for security\"\n          />\n        );\n      }\n      return null;\n    },\n    [hasSensitiveUsage]\n  );\n\n  return (\n    <EnvironmentVariablesTable\n      environment={environment}\n      collection={collection}\n      onSave={handleSave}\n      draft={hasDraftForThisEnv ? environmentsDraft : null}\n      onDraftChange={handleDraftChange}\n      onDraftClear={handleDraftClear}\n      setIsModified={setIsModified}\n      renderExtraValueContent={renderExtraValueContent}\n      searchQuery={searchQuery}\n    />\n  );\n};\n\nexport default EnvironmentVariables;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n  background: ${(props) => props.theme.bg};\n\n  .header {\n    position: relative;\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 9px 20px 8px 20px;\n    flex-shrink: 0;\n\n    .title {\n      font-size: ${(props) => props.theme.font.size.base};\n      font-weight: 500;\n      color: ${(props) => props.theme.text};\n      margin: 0;\n    }\n\n    .title-container {\n      display: flex;\n      align-items: center;\n      gap: 8px;\n      flex: 1;\n\n      &.renaming {\n        .title-input {\n          flex: 1;\n          background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n          outline: none;\n          color: ${(props) => props.theme.text};\n          font-size: 15px;\n          font-weight: 500;\n          padding: 4px 8px;\n          border-radius: 5px;\n        }\n\n        .inline-actions {\n          display: flex;\n          gap: 2px;\n        }\n\n        .inline-action-btn {\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          width: 26px;\n          height: 26px;\n          padding: 0;\n          background: transparent;\n          border: none;\n          border-radius: 4px;\n          cursor: pointer;\n          transition: all 0.15s ease;\n\n          &.save {\n            color: ${(props) => props.theme.textLink};\n\n            &:hover {\n              background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n            }\n          }\n\n          &.cancel {\n            color: ${(props) => props.theme.colors.text.muted};\n\n            &:hover {\n              background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n              color: ${(props) => props.theme.text};\n            }\n          }\n        }\n      }\n    }\n\n    .title-error {\n      position: absolute;\n      top: 100%;\n      left: 20px;\n      margin-top: 4px;\n      padding: 4px 8px;\n      font-size: 11px;\n      color: ${(props) => props.theme.colors.text.danger};\n      background: ${(props) => props.theme.bg};\n      border: 1px solid ${(props) => props.theme.colors.text.danger};\n      border-radius: 4px;\n      white-space: nowrap;\n    }\n\n    .actions {\n      display: flex;\n      align-items: center;\n      gap: 2px;\n\n      .search-input-wrapper {\n        position: relative;\n        display: flex;\n        align-items: center;\n\n        .search-icon {\n          position: absolute;\n          left: 8px;\n          color: ${(props) => props.theme.colors.text.muted};\n          pointer-events: none;\n        }\n\n        .search-input {\n          width: 200px;\n          padding: 5px 32px 5px 32px;\n          border: 1px solid ${(props) => props.theme.input.border};\n          border-radius: ${(props) => props.theme.border.radius.sm};\n          background: ${(props) => props.theme.input.bg};\n          color: ${(props) => props.theme.text};\n          font-size: ${(props) => props.theme.font.size.base};\n          outline: none;\n          transition: border-color 0.15s ease;\n\n          &:focus {\n            border-color: ${(props) => props.theme.input.focusBorder};\n          }\n\n          &::placeholder {\n            color: ${(props) => props.theme.input.placeholder.color};\n            opacity: ${(props) => props.theme.input.placeholder.opacity};\n          }\n        }\n\n        .clear-search {\n          position: absolute;\n          right: 1px;\n          padding: 4px;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          color: ${(props) => props.theme.colors.text.muted};\n          background: transparent;\n          border: none;\n          cursor: pointer;\n          border-radius: ${(props) => props.theme.border.radius.sm};\n          transition: all 0.15s ease;\n\n          &:hover {\n            color: ${(props) => props.theme.text};\n            background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n          }\n        }\n      }\n\n      button {\n        display: inline-flex;\n        align-items: center;\n        justify-content: center;\n        width: 28px;\n        height: 28px;\n        padding: 0;\n        color: ${(props) => props.theme.colors.text.muted};\n        background: transparent;\n        border: none;\n        border-radius: 5px;\n        cursor: pointer;\n        transition: all 0.15s ease;\n\n        &:hover {\n          background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n          color: ${(props) => props.theme.text};\n        }\n\n        &:last-child:hover {\n          color: ${(props) => props.theme.colors.text.danger};\n        }\n      }\n    }\n  }\n\n  .content {\n    flex: 1;\n    overflow: hidden;\n    display: flex;\n    flex-direction: column;\n    padding: 0 20px 20px 20px;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/EnvironmentDetails/index.js",
    "content": "import { IconCopy, IconEdit, IconTrash, IconCheck, IconX, IconSearch } from '@tabler/icons';\nimport { useState, useRef } from 'react';\nimport { useDispatch } from 'react-redux';\nimport { renameEnvironment, updateEnvironmentColor } from 'providers/ReduxStore/slices/collections/actions';\nimport { validateName, validateNameError } from 'utils/common/regex';\nimport toast from 'react-hot-toast';\nimport CopyEnvironment from 'components/Environments/EnvironmentSettings/CopyEnvironment';\nimport DeleteEnvironment from 'components/Environments/EnvironmentSettings/DeleteEnvironment';\nimport EnvironmentVariables from './EnvironmentVariables';\nimport ColorPicker from 'components/ColorPicker';\nimport StyledWrapper from './StyledWrapper';\n\nconst EnvironmentDetails = ({ environment, setIsModified, collection, searchQuery, setSearchQuery, isSearchExpanded, setIsSearchExpanded, debouncedSearchQuery, searchInputRef }) => {\n  const dispatch = useDispatch();\n  const environments = collection?.environments || [];\n\n  const [openDeleteModal, setOpenDeleteModal] = useState(false);\n  const [openCopyModal, setOpenCopyModal] = useState(false);\n  const [isRenaming, setIsRenaming] = useState(false);\n  const [newName, setNewName] = useState('');\n  const [nameError, setNameError] = useState('');\n  const inputRef = useRef(null);\n\n  const validateEnvironmentName = (name) => {\n    if (!name || name.trim() === '') {\n      return 'Name is required';\n    }\n\n    if (name.length < 1) {\n      return 'Must be at least 1 character';\n    }\n\n    if (name.length > 255) {\n      return 'Must be 255 characters or less';\n    }\n\n    if (!validateName(name)) {\n      return validateNameError(name);\n    }\n\n    const trimmedName = name.toLowerCase().trim();\n    const isDuplicate = (environments || []).some(\n      (env) => env?.uid !== environment.uid && env?.name?.toLowerCase().trim() === trimmedName\n    );\n    if (isDuplicate) {\n      return 'Environment already exists';\n    }\n\n    return null;\n  };\n\n  const handleRenameClick = () => {\n    setIsRenaming(true);\n    setNewName(environment.name);\n    setNameError('');\n    setTimeout(() => {\n      inputRef.current?.focus();\n      inputRef.current?.select();\n    }, 50);\n  };\n\n  const handleSaveRename = () => {\n    const error = validateEnvironmentName(newName);\n    if (error) {\n      setNameError(error);\n      return;\n    }\n\n    dispatch(renameEnvironment(newName, environment.uid, collection.uid))\n      .then(() => {\n        toast.success('Environment renamed!');\n        setIsRenaming(false);\n        setNewName('');\n        setNameError('');\n      })\n      .catch(() => {\n        toast.error('An error occurred while renaming the environment');\n      });\n  };\n\n  const handleCancelRename = () => {\n    setIsRenaming(false);\n    setNewName('');\n    setNameError('');\n  };\n\n  const handleNameChange = (e) => {\n    setNewName(e.target.value);\n    if (nameError) {\n      setNameError('');\n    }\n  };\n\n  const handleNameBlur = () => {\n    if (newName.trim() === '') {\n      handleCancelRename();\n    } else {\n      const error = validateEnvironmentName(newName);\n      if (error) {\n        setNameError(error);\n      }\n    }\n  };\n\n  const handleNameKeyDown = (e) => {\n    if (e.key === 'Enter') {\n      e.preventDefault();\n      handleSaveRename();\n    } else if (e.key === 'Escape') {\n      e.preventDefault();\n      handleCancelRename();\n    }\n  };\n\n  const handleSearchIconClick = () => {\n    setIsSearchExpanded(true);\n    setTimeout(() => {\n      searchInputRef.current?.focus();\n    }, 50);\n  };\n\n  const handleClearSearch = () => {\n    setSearchQuery('');\n  };\n\n  const handleSearchBlur = () => {\n    if (searchQuery === '') {\n      setIsSearchExpanded(false);\n    }\n  };\n\n  const handleColorChange = (color) => {\n    dispatch(updateEnvironmentColor(environment.uid, color, collection.uid));\n  };\n\n  return (\n    <StyledWrapper>\n      {openDeleteModal && (\n        <DeleteEnvironment onClose={() => setOpenDeleteModal(false)} environment={environment} collection={collection} />\n      )}\n      {openCopyModal && (\n        <CopyEnvironment onClose={() => setOpenCopyModal(false)} environment={environment} collection={collection} />\n      )}\n\n      <div className=\"header\">\n        <div className={`title-container ${isRenaming ? 'renaming' : ''}`}>\n          {isRenaming ? (\n            <>\n              <input\n                ref={inputRef}\n                type=\"text\"\n                className=\"title-input\"\n                value={newName}\n                onChange={handleNameChange}\n                onBlur={handleNameBlur}\n                onKeyDown={handleNameKeyDown}\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n              />\n              <div className=\"inline-actions\">\n                <button\n                  className=\"inline-action-btn save\"\n                  onClick={handleSaveRename}\n                  onMouseDown={(e) => e.preventDefault()}\n                  title=\"Save\"\n                >\n                  <IconCheck size={14} strokeWidth={2} />\n                </button>\n                <button\n                  className=\"inline-action-btn cancel\"\n                  onClick={handleCancelRename}\n                  onMouseDown={(e) => e.preventDefault()}\n                  title=\"Cancel\"\n                >\n                  <IconX size={14} strokeWidth={2} />\n                </button>\n              </div>\n            </>\n          ) : (\n            <div className=\"flex items-center gap-2\">\n              <h2 className=\"title\">{environment.name}</h2>\n              <ColorPicker color={environment.color} onChange={handleColorChange} />\n            </div>\n          )}\n        </div>\n        {nameError && isRenaming && <div className=\"title-error\">{nameError}</div>}\n        <div className=\"actions\">\n          {isSearchExpanded ? (\n            <div className=\"search-input-wrapper\">\n              <IconSearch size={14} strokeWidth={1.5} className=\"search-icon\" />\n              <input\n                ref={searchInputRef}\n                type=\"text\"\n                placeholder=\"Search variables...\"\n                value={searchQuery}\n                onChange={(e) => setSearchQuery(e.target.value)}\n                onBlur={handleSearchBlur}\n                className=\"search-input\"\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n              />\n              {searchQuery && (\n                <button\n                  className=\"clear-search\"\n                  onClick={handleClearSearch}\n                  onMouseDown={(e) => e.preventDefault()}\n                  title=\"Clear search\"\n                >\n                  <IconX size={14} strokeWidth={1.5} />\n                </button>\n              )}\n            </div>\n          ) : (\n            <button onClick={handleSearchIconClick} title=\"Search variables\">\n              <IconSearch size={15} strokeWidth={1.5} />\n            </button>\n          )}\n          <button onClick={handleRenameClick} title=\"Rename\">\n            <IconEdit size={15} strokeWidth={1.5} />\n          </button>\n          <button onClick={() => setOpenCopyModal(true)} title=\"Copy\">\n            <IconCopy size={15} strokeWidth={1.5} />\n          </button>\n          <button onClick={() => setOpenDeleteModal(true)} title=\"Delete\">\n            <IconTrash size={15} strokeWidth={1.5} />\n          </button>\n        </div>\n      </div>\n\n      <div className=\"content\">\n        <EnvironmentVariables\n          environment={environment}\n          setIsModified={setIsModified}\n          collection={collection}\n          searchQuery={debouncedSearchQuery}\n        />\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default EnvironmentDetails;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  height: 100%;\n  overflow: hidden;\n  position: relative;\n\n  .environments-container {\n    display: flex;\n    height: 100%;\n    width: 100%;\n    overflow: hidden;\n  }\n\n  .confirm-switch-overlay {\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    z-index: 10;\n    background: ${(props) => props.theme.bg};\n    padding: 12px;\n  }\n\n  /* Left Sidebar */\n  .sidebar {\n    width: 240px;\n    min-width: 240px;\n    display: flex;\n    flex-direction: column;\n  }\n\n    .btn-action {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      width: 24px;\n      height: 24px;\n      padding: 0;\n      background: transparent;\n      border: none;\n      border-radius: 4px;\n      color: ${(props) => props.theme.colors.text.muted};\n      cursor: pointer;\n      transition: all 0.15s ease;\n      \n      &:hover {\n        background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n        color: ${(props) => props.theme.text};\n      }\n    }\n  }\n\n  .env-list-search {\n    position: relative;\n    display: flex;\n    align-items: center;\n    margin: 0 4px 6px 4px;\n\n    .env-list-search-icon {\n      position: absolute;\n      left: 8px;\n      color: ${(props) => props.theme.colors.text.muted};\n      pointer-events: none;\n    }\n\n    .env-list-search-input {\n      width: 100%;\n      padding: 5px 24px 5px 26px;\n      font-size: 12px;\n      background: transparent;\n      border: 1px solid ${(props) => props.theme.border.border1};\n      border-radius: 6px;\n      color: ${(props) => props.theme.text};\n      transition: border-color 0.15s ease;\n\n      &::placeholder {\n        color: ${(props) => props.theme.colors.text.muted};\n      }\n      \n      &:focus {\n        outline: none;\n        border-color: ${(props) => props.theme.colors.accent};\n      }\n    }\n\n    .env-list-search-clear {\n      position: absolute;\n      right: 4px;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      padding: 2px;\n      background: transparent;\n      border: none;\n      cursor: pointer;\n      color: ${(props) => props.theme.colors.text.muted};\n      border-radius: 3px;\n\n      &:hover {\n        color: ${(props) => props.theme.text};\n      }\n    }\n  }\n\n  .sections-container {\n    flex: 1;\n    display: flex;\n    flex-direction: column;\n    overflow: hidden;\n    padding: 8px;\n  }\n\n  .section-header {\n    margin-inline: 4px !important;\n    padding-left: 6px !important;\n    border-radius: 6px ;\n    padding-right: 3px !important;\n    padding-block: 4px !important;\n  }\n\n  .environments-list {\n    overflow-y: auto;\n    padding: 0 4px;\n  }\n\n  .btn-action {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 22px;\n    height: 22px;\n    padding: 0;\n    background: transparent;\n    border: none;\n    border-radius: 4px;\n    color: ${(props) => props.theme.colors.text.muted};\n    cursor: pointer;\n    transition: all 0.15s ease;\n\n    &:hover {\n      background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n      color: ${(props) => props.theme.text};\n    }\n\n    &.active {\n      color: ${(props) => props.theme.colors.accent};\n    }\n  }\n\n  .environment-item {\n    position: relative;\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    gap: 8px;\n    padding: 4px 8px;\n    margin-bottom: 1px;\n    font-size: 13px;\n    color: ${(props) => props.theme.text};\n    cursor: pointer;\n    border-radius: 6px;\n    transition: background 0.15s ease;\n    \n    .environment-name {\n      flex: 1;\n      white-space: nowrap;\n      overflow: hidden;\n      text-overflow: ellipsis;\n    }\n\n    .environment-actions {\n      display: flex;\n      align-items: center;\n      opacity: 0;\n      transition: opacity 0.15s ease;\n\n      .activate-btn {\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        padding: 4px;\n        border: none;\n        background: transparent;\n        cursor: pointer;\n        color: ${(props) => props.theme.text.muted};\n        border-radius: 3px;\n        transition: all 0.15s ease;\n\n        &:hover {\n          background: ${(props) => props.theme.workspace.button.bg};\n          color: ${(props) => props.theme.colors.text.green};\n        }\n      }\n\n      .activated-checkmark {\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        padding: 4px;\n        color: ${(props) => props.theme.colors.text.green};\n        opacity: 1;\n      }\n    }\n\n    &:hover .environment-actions {\n      opacity: 1;\n    }\n\n    &.activated .environment-actions {\n      opacity: 1;\n    }\n\n    &:hover {\n      background: ${(props) => props.theme.workspace.button.bg};\n    }\n    \n    &.active {\n      background: ${(props) => props.theme.background.surface0};\n      color: ${(props) => props.theme.text};\n    }\n    \n    &.renaming,\n    &.creating {\n      cursor: default;\n      padding: 4px 4px 4px 8px;\n      background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n      \n      &:hover {\n        background: ${(props) => props.theme.workspace.button.bg};\n      }\n    }\n\n    .rename-container {\n      display: flex;\n      align-items: center;\n      flex: 1;\n      min-width: 0;\n      overflow: hidden;\n      \n      .environment-name-input {\n        flex: 1;\n        min-width: 0;\n        background: transparent;\n        border: none;\n        outline: none;\n        color: ${(props) => props.theme.text};\n        font-size: 13px;\n        padding: 2px 4px;\n        \n        &::placeholder {\n          color: ${(props) => props.theme.colors.text.muted};\n        }\n      }\n      \n      .inline-actions {\n        display: flex;\n        gap: 2px;\n        margin-left: 4px;\n        flex-shrink: 0;\n      }\n    }\n\n    &.creating {\n      .environment-name-input {\n        flex: 1;\n        min-width: 0;\n        background: transparent;\n        border: none;\n        outline: none;\n        color: ${(props) => props.theme.text};\n        font-size: 13px;\n        padding: 2px 4px;\n        \n        &::placeholder {\n          color: ${(props) => props.theme.colors.text.muted};\n        }\n      }\n      \n      .inline-actions {\n        display: flex;\n        gap: 2px;\n        margin-left: 4px;\n        flex-shrink: 0;\n      }\n    }\n\n    .inline-action-btn {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      width: 22px;\n      height: 22px;\n      padding: 0;\n      background: transparent;\n      border: none;\n      border-radius: 4px;\n      cursor: pointer;\n      transition: all 0.15s ease;\n      \n      &.save {\n        color: ${(props) => props.theme.colors.text.green};\n        \n        &:hover {\n          background: ${(props) => rgba(props.theme.colors.text.green, 0.1)};\n        }\n      }\n      \n      &.cancel {\n        color: ${(props) => props.theme.colors.text.danger};\n        \n        &:hover {\n          background: ${(props) => rgba(props.theme.colors.text.danger, 0.1)};\n        }\n      }\n    }\n  }\n  \n  .env-error {\n    padding: 4px 12px;\n    margin-top: 4px;\n    font-size: 11px;\n    color: ${(props) => props.theme.colors.text.danger};\n    background: ${(props) => `${props.theme.colors.text.danger}15`};\n    border-radius: 4px;\n  }\n\n  .no-env-file {\n    padding: 8px 12px;\n    font-size: 12px;\n    color: ${(props) => props.theme.colors.text.muted};\n    font-style: italic;\n  }\n\n  .empty-state {\n    flex: 1;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    padding-top: 10%;\n    color: ${(props) => props.theme.colors.text.muted};\n\n    svg {\n      opacity: 0.3;\n      margin-bottom: 8px;\n    }\n\n    .title {\n      font-size: 13px;\n      font-weight: 500;\n      margin-bottom: 12px;\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n\n    .actions {\n      display: flex;\n      gap: 8px;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/EnvironmentSettings/EnvironmentList/index.js",
    "content": "import React, { useEffect, useState, useRef, useCallback } from 'react';\nimport usePrevious from 'hooks/usePrevious';\nimport useOnClickOutside from 'hooks/useOnClickOutside';\nimport useDebounce from 'hooks/useDebounce';\nimport EnvironmentDetails from './EnvironmentDetails';\nimport { IconDownload, IconUpload, IconSearch, IconPlus, IconCheck, IconX, IconFileAlert } from '@tabler/icons';\nimport Button from 'ui/Button';\nimport StyledWrapper from './StyledWrapper';\nimport ConfirmSwitchEnv from 'components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/ConfirmSwitchEnv';\nimport ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';\nimport CollapsibleSection from 'components/Environments/CollapsibleSection';\nimport DotEnvFileEditor from 'components/Environments/DotEnvFileEditor';\nimport DotEnvFileDetails from 'components/Environments/DotEnvFileDetails';\nimport ColorBadge from 'components/ColorBadge';\nimport { isEqual } from 'lodash';\nimport { useDispatch, useSelector } from 'react-redux';\nimport {\n  addEnvironment,\n  renameEnvironment,\n  selectEnvironment,\n  saveDotEnvVariables,\n  saveDotEnvRaw,\n  createDotEnvFile,\n  deleteDotEnvFile\n} from 'providers/ReduxStore/slices/collections/actions';\nimport { setEnvironmentsDraft, clearEnvironmentsDraft } from 'providers/ReduxStore/slices/collections';\nimport { setEnvVarSearchQuery, setEnvVarSearchExpanded } from 'providers/ReduxStore/slices/app';\nimport { validateName, validateNameError } from 'utils/common/regex';\nimport toast from 'react-hot-toast';\nimport classnames from 'classnames';\n\nconst EMPTY_ARRAY = [];\n\nconst EnvironmentList = ({\n  environments,\n  activeEnvironmentUid,\n  selectedEnvironment,\n  setSelectedEnvironment,\n  isModified,\n  setIsModified,\n  collection,\n  setShowExportModal\n}) => {\n  const dispatch = useDispatch();\n  const envSearchQuery = useSelector((state) => state.app.envVarSearch?.collection?.query ?? '');\n  const isEnvSearchExpanded = useSelector((state) => state.app.envVarSearch?.collection?.expanded ?? false);\n  const setEnvSearchQuery = (q) => dispatch(setEnvVarSearchQuery({ context: 'collection', query: q }));\n  const setIsEnvSearchExpanded = (v) => dispatch(setEnvVarSearchExpanded({ context: 'collection', expanded: v }));\n\n  const [openImportModal, setOpenImportModal] = useState(false);\n  const [searchText, setSearchText] = useState('');\n  const envListSearchInputRef = useRef(null);\n  const [isCreatingInline, setIsCreatingInline] = useState(false);\n  const [renamingEnvUid, setRenamingEnvUid] = useState(null);\n  const [newEnvName, setNewEnvName] = useState('');\n  const [envNameError, setEnvNameError] = useState('');\n  const inputRef = useRef(null);\n  const renameContainerRef = useRef(null);\n  const createContainerRef = useRef(null);\n\n  const [switchEnvConfirmClose, setSwitchEnvConfirmClose] = useState(false);\n  const [originalEnvironmentVariables, setOriginalEnvironmentVariables] = useState([]);\n  const [environmentsExpanded, setEnvironmentsExpanded] = useState(true);\n  const [dotEnvExpanded, setDotEnvExpanded] = useState(false);\n  const [activeView, setActiveView] = useState('environment');\n  const [isDotEnvModified, setIsDotEnvModified] = useState(false);\n  const [dotEnvViewMode, setDotEnvViewMode] = useState('table');\n  const [selectedDotEnvFile, setSelectedDotEnvFile] = useState(null);\n  const [isCreatingDotEnvInline, setIsCreatingDotEnvInline] = useState(false);\n  const [newDotEnvName, setNewDotEnvName] = useState('.env');\n  const [dotEnvNameError, setDotEnvNameError] = useState('');\n  const dotEnvInputRef = useRef(null);\n  const dotEnvCreateContainerRef = useRef(null);\n\n  const debouncedEnvSearchQuery = useDebounce(envSearchQuery, 300);\n  const envSearchInputRef = useRef(null);\n\n  const dotEnvFiles = useSelector((state) => {\n    const coll = state.collections.collections.find((c) => c.uid === collection?.uid);\n    return coll?.dotEnvFiles || EMPTY_ARRAY;\n  });\n\n  const envUids = environments ? environments.map((env) => env.uid) : [];\n  const prevEnvUids = usePrevious(envUids);\n\n  const environmentsDraftUid = collection?.environmentsDraft?.environmentUid;\n\n  const handleDotEnvModifiedChange = useCallback((modified) => {\n    setIsDotEnvModified(modified);\n    if (modified) {\n      dispatch(setEnvironmentsDraft({\n        collectionUid: collection.uid,\n        environmentUid: `dotenv:${selectedDotEnvFile}`,\n        variables: []\n      }));\n    } else if (environmentsDraftUid?.startsWith('dotenv:')) {\n      dispatch(clearEnvironmentsDraft({ collectionUid: collection.uid }));\n    }\n  }, [dispatch, collection.uid, selectedDotEnvFile, environmentsDraftUid]);\n\n  useEffect(() => {\n    if (dotEnvFiles.length === 0) {\n      setSelectedDotEnvFile(null);\n      setActiveView('environment');\n      handleDotEnvModifiedChange(false);\n      return;\n    }\n\n    const fileExists = dotEnvFiles.some((f) => f.filename === selectedDotEnvFile);\n    if (!selectedDotEnvFile || !fileExists) {\n      setSelectedDotEnvFile(dotEnvFiles[0].filename);\n    }\n  }, [dotEnvFiles]);\n\n  useEffect(() => {\n    if (!environments?.length) {\n      setSelectedEnvironment(null);\n      setOriginalEnvironmentVariables([]);\n      return;\n    }\n\n    if (selectedEnvironment) {\n      let _selectedEnvironment = environments?.find((env) => env?.uid === selectedEnvironment?.uid);\n\n      if (!_selectedEnvironment) {\n        _selectedEnvironment = environments?.find((env) => env?.name === selectedEnvironment?.name);\n      }\n\n      if (!_selectedEnvironment) {\n        _selectedEnvironment = environments?.find((env) => env.uid === activeEnvironmentUid) || environments?.[0];\n      }\n\n      const hasSelectedEnvironmentChanged = !isEqual(selectedEnvironment, _selectedEnvironment);\n      if (hasSelectedEnvironmentChanged || selectedEnvironment.uid !== _selectedEnvironment?.uid) {\n        setSelectedEnvironment(_selectedEnvironment);\n      }\n      setOriginalEnvironmentVariables(_selectedEnvironment?.variables || []);\n      return;\n    }\n\n    const environment = environments?.find((env) => env.uid === activeEnvironmentUid) || environments?.[0];\n\n    setSelectedEnvironment(environment);\n    setOriginalEnvironmentVariables(environment?.variables || []);\n  }, [environments, activeEnvironmentUid, selectedEnvironment]);\n\n  useEffect(() => {\n    if (prevEnvUids && prevEnvUids.length && envUids.length > prevEnvUids.length) {\n      const newEnv = environments.find((env) => !prevEnvUids.includes(env.uid));\n      if (newEnv) {\n        setSelectedEnvironment(newEnv);\n      }\n    }\n\n    if (prevEnvUids && prevEnvUids.length && envUids.length < prevEnvUids.length) {\n      setSelectedEnvironment(environments && environments.length ? environments[0] : null);\n    }\n  }, [envUids, environments, prevEnvUids]);\n\n  const handleEnvironmentClick = (env) => {\n    if (activeView === 'dotenv' && isDotEnvModified) {\n      setSwitchEnvConfirmClose(true);\n      return;\n    }\n    if (!isModified) {\n      setSelectedEnvironment(env);\n      setActiveView('environment');\n      setEnvironmentsExpanded(true);\n    } else {\n      setSwitchEnvConfirmClose(true);\n    }\n  };\n\n  const handleDotEnvClick = (filename) => {\n    if (isModified) {\n      setSwitchEnvConfirmClose(true);\n      return;\n    }\n    if (activeView === 'dotenv' && isDotEnvModified && selectedDotEnvFile !== filename) {\n      setSwitchEnvConfirmClose(true);\n      return;\n    }\n    setSelectedDotEnvFile(filename);\n    setActiveView('dotenv');\n    setDotEnvExpanded(true);\n  };\n\n  const handleEnvironmentDoubleClick = (env) => {\n    setRenamingEnvUid(env.uid);\n    setNewEnvName(env.name);\n    setEnvNameError('');\n    setTimeout(() => {\n      inputRef.current?.focus();\n      inputRef.current?.select();\n    }, 50);\n  };\n\n  const handleActivateEnvironment = useCallback((e, env) => {\n    e.stopPropagation();\n    dispatch(selectEnvironment(env.uid, collection.uid))\n      .then(() => {\n        toast.success(`Environment \"${env.name}\" activated`);\n      })\n      .catch(() => {\n        toast.error('Failed to activate environment');\n      });\n  }, [dispatch, collection.uid]);\n\n  const validateEnvironmentName = (name, excludeUid = null) => {\n    if (!name || name.trim() === '') {\n      return 'Name is required';\n    }\n\n    if (!validateName(name)) {\n      return validateNameError(name);\n    }\n\n    const trimmedName = name.toLowerCase().trim();\n    const isDuplicate = environments.some(\n      (env) => env?.uid !== excludeUid && env?.name?.toLowerCase().trim() === trimmedName\n    );\n    if (isDuplicate) {\n      return 'Environment already exists';\n    }\n\n    return null;\n  };\n\n  const handleCreateEnvClick = () => {\n    if (!isModified && !isDotEnvModified) {\n      setIsCreatingInline(true);\n      setNewEnvName('');\n      setEnvNameError('');\n      setTimeout(() => {\n        inputRef.current?.focus();\n      }, 50);\n    } else {\n      setSwitchEnvConfirmClose(true);\n    }\n  };\n\n  const handleCancelCreate = useCallback(() => {\n    setIsCreatingInline(false);\n    setNewEnvName('');\n    setEnvNameError('');\n  }, []);\n\n  useOnClickOutside(createContainerRef, handleCancelCreate, isCreatingInline);\n\n  const handleSaveNewEnv = () => {\n    const error = validateEnvironmentName(newEnvName);\n    if (error) {\n      setEnvNameError(error);\n      return;\n    }\n\n    dispatch(addEnvironment(newEnvName, collection.uid))\n      .then(() => {\n        toast.success('Environment created!');\n        setIsCreatingInline(false);\n        setNewEnvName('');\n        setEnvNameError('');\n      })\n      .catch(() => {\n        toast.error('An error occurred while creating the environment');\n      });\n  };\n\n  const handleEnvNameChange = (e) => {\n    const value = e.target.value;\n    setNewEnvName(value);\n\n    if (envNameError) {\n      setEnvNameError('');\n    }\n  };\n\n  const handleEnvNameKeyDown = (e) => {\n    if (e.key === 'Enter') {\n      e.preventDefault();\n      if (renamingEnvUid) {\n        handleSaveRename();\n      } else {\n        handleSaveNewEnv();\n      }\n    } else if (e.key === 'Escape') {\n      e.preventDefault();\n      if (renamingEnvUid) {\n        handleCancelRename();\n      } else {\n        handleCancelCreate();\n      }\n    }\n  };\n\n  const handleSaveRename = () => {\n    const error = validateEnvironmentName(newEnvName, renamingEnvUid);\n    if (error) {\n      setEnvNameError(error);\n      return;\n    }\n\n    dispatch(renameEnvironment(newEnvName, renamingEnvUid, collection.uid))\n      .then(() => {\n        toast.success('Environment renamed!');\n        setRenamingEnvUid(null);\n        setNewEnvName('');\n        setEnvNameError('');\n      })\n      .catch(() => {\n        toast.error('An error occurred while renaming the environment');\n      });\n  };\n\n  const handleCancelRename = useCallback(() => {\n    setRenamingEnvUid(null);\n    setNewEnvName('');\n    setEnvNameError('');\n  }, []);\n\n  useOnClickOutside(renameContainerRef, handleCancelRename, !!renamingEnvUid);\n\n  const handleImportClick = () => {\n    if (!isModified && !isDotEnvModified) {\n      setOpenImportModal(true);\n    } else {\n      setSwitchEnvConfirmClose(true);\n    }\n  };\n\n  const handleExportClick = () => {\n    if (setShowExportModal) {\n      setShowExportModal(true);\n    }\n  };\n\n  const handleConfirmSwitch = (saveChanges) => {\n    if (!saveChanges) {\n      setSwitchEnvConfirmClose(false);\n    }\n  };\n\n  const handleSaveDotEnv = (variables) => {\n    if (!selectedDotEnvFile) return Promise.reject(new Error('No file selected'));\n    return dispatch(saveDotEnvVariables(collection.uid, variables, selectedDotEnvFile));\n  };\n\n  const handleSaveDotEnvRaw = (content) => {\n    if (!selectedDotEnvFile) return Promise.reject(new Error('No file selected'));\n    return dispatch(saveDotEnvRaw(collection.uid, content, selectedDotEnvFile));\n  };\n\n  const handleCreateDotEnvInlineClick = () => {\n    if (isModified || isDotEnvModified) {\n      setSwitchEnvConfirmClose(true);\n      return;\n    }\n    setIsCreatingDotEnvInline(true);\n    setNewDotEnvName('.env');\n    setDotEnvNameError('');\n    setTimeout(() => {\n      dotEnvInputRef.current?.focus();\n      const input = dotEnvInputRef.current;\n      if (input) {\n        input.setSelectionRange(input.value.length, input.value.length);\n      }\n    }, 50);\n  };\n\n  const handleCancelDotEnvCreate = useCallback(() => {\n    setIsCreatingDotEnvInline(false);\n    setNewDotEnvName('.env');\n    setDotEnvNameError('');\n  }, []);\n\n  useOnClickOutside(dotEnvCreateContainerRef, handleCancelDotEnvCreate, isCreatingDotEnvInline);\n\n  const validateDotEnvName = (name) => {\n    if (!name || name.trim() === '') {\n      return 'Name is required';\n    }\n\n    if (!name.startsWith('.env')) {\n      return 'File name must start with .env';\n    }\n\n    const validPattern = /^\\.env[a-zA-Z0-9._-]*$/;\n    if (!validPattern.test(name)) {\n      return 'Invalid file name';\n    }\n\n    const exists = dotEnvFiles.some((f) => f.filename === name);\n    if (exists) {\n      return 'File already exists';\n    }\n\n    return null;\n  };\n\n  const handleSaveNewDotEnv = () => {\n    const error = validateDotEnvName(newDotEnvName);\n    if (error) {\n      setDotEnvNameError(error);\n      return;\n    }\n\n    dispatch(createDotEnvFile(collection.uid, newDotEnvName))\n      .then(() => {\n        toast.success(`${newDotEnvName} file created!`);\n        setIsCreatingDotEnvInline(false);\n        setNewDotEnvName('.env');\n        setDotEnvNameError('');\n        setSelectedDotEnvFile(newDotEnvName);\n        setActiveView('dotenv');\n        setDotEnvExpanded(true);\n      })\n      .catch((error) => {\n        toast.error(error.message || 'Failed to create .env file');\n      });\n  };\n\n  const handleDotEnvNameChange = (e) => {\n    const value = e.target.value;\n    if (!value.startsWith('.env')) {\n      setNewDotEnvName('.env');\n    } else {\n      setNewDotEnvName(value);\n    }\n    if (dotEnvNameError) {\n      setDotEnvNameError('');\n    }\n  };\n\n  const handleDotEnvNameKeyDown = (e) => {\n    if (e.key === 'Enter') {\n      e.preventDefault();\n      handleSaveNewDotEnv();\n    } else if (e.key === 'Escape') {\n      e.preventDefault();\n      handleCancelDotEnvCreate();\n    } else if (e.key === 'Backspace') {\n      const input = e.target;\n      if (input.selectionStart <= 4 && input.selectionEnd <= 4) {\n        e.preventDefault();\n      }\n    }\n  };\n\n  const handleDeleteDotEnvFile = (filename) => {\n    dispatch(deleteDotEnvFile(collection.uid, filename))\n      .then(() => {\n        toast.success(`${filename} file deleted!`);\n        handleDotEnvModifiedChange(false);\n        if (selectedDotEnvFile === filename) {\n          const remainingFiles = dotEnvFiles.filter((f) => f.filename !== filename);\n          if (remainingFiles.length > 0) {\n            setSelectedDotEnvFile(remainingFiles[0].filename);\n          } else {\n            setActiveView('environment');\n            if (environments?.length) {\n              const env = environments.find((e) => e.uid === activeEnvironmentUid) || environments[0];\n              setSelectedEnvironment(env);\n            }\n          }\n        }\n      })\n      .catch((error) => {\n        toast.error(error.message || 'Failed to delete .env file');\n      });\n  };\n\n  const handleDotEnvViewModeChange = (mode) => {\n    setDotEnvViewMode(mode);\n  };\n\n  const filteredEnvironments\n    = environments?.filter((env) => env.name.toLowerCase().includes(searchText.toLowerCase())) || [];\n\n  const selectedDotEnvData = dotEnvFiles.find((f) => f.filename === selectedDotEnvFile);\n\n  const renderContent = () => {\n    if (activeView === 'dotenv' && selectedDotEnvFile && selectedDotEnvData) {\n      return (\n        <DotEnvFileDetails\n          title={selectedDotEnvFile}\n          onDelete={() => handleDeleteDotEnvFile(selectedDotEnvFile)}\n          dotEnvExists={selectedDotEnvData?.exists}\n          viewMode={dotEnvViewMode}\n          onViewModeChange={handleDotEnvViewModeChange}\n        >\n          <DotEnvFileEditor\n            variables={selectedDotEnvData?.variables || []}\n            onSave={handleSaveDotEnv}\n            onSaveRaw={handleSaveDotEnvRaw}\n            isModified={isDotEnvModified}\n            setIsModified={handleDotEnvModifiedChange}\n            dotEnvExists={selectedDotEnvData?.exists}\n            viewMode={dotEnvViewMode}\n            collection={collection}\n          />\n        </DotEnvFileDetails>\n      );\n    }\n\n    if (selectedEnvironment) {\n      return (\n        <EnvironmentDetails\n          environment={selectedEnvironment}\n          setIsModified={setIsModified}\n          originalEnvironmentVariables={originalEnvironmentVariables}\n          collection={collection}\n          searchQuery={envSearchQuery}\n          setSearchQuery={setEnvSearchQuery}\n          isSearchExpanded={isEnvSearchExpanded}\n          setIsSearchExpanded={setIsEnvSearchExpanded}\n          debouncedSearchQuery={debouncedEnvSearchQuery}\n          searchInputRef={envSearchInputRef}\n        />\n      );\n    }\n\n    return (\n      <div className=\"empty-state\">\n        <IconFileAlert size={48} strokeWidth={1.5} />\n        <div className=\"title\">No Environments</div>\n        <div className=\"actions\">\n          <Button size=\"sm\" color=\"secondary\" onClick={() => handleCreateEnvClick()}>\n            Create Environment\n          </Button>\n          <Button size=\"sm\" color=\"secondary\" onClick={() => handleImportClick()}>\n            Import Environment\n          </Button>\n        </div>\n      </div>\n    );\n  };\n\n  return (\n    <StyledWrapper>\n      {openImportModal && (\n        <ImportEnvironmentModal type=\"collection\" collection={collection} onClose={() => setOpenImportModal(false)} />\n      )}\n\n      <div className=\"environments-container\">\n        {switchEnvConfirmClose && (\n          <div className=\"confirm-switch-overlay\">\n            <ConfirmSwitchEnv onCancel={() => handleConfirmSwitch(false)} />\n          </div>\n        )}\n\n        <div className=\"sidebar\">\n\n          <div className=\"sections-container\">\n            <CollapsibleSection\n              title=\"Environments\"\n              expanded={environmentsExpanded}\n              onToggle={() => setEnvironmentsExpanded(!environmentsExpanded)}\n              actions={(\n                <>\n                  <button\n                    type=\"button\"\n                    className=\"btn-action\"\n                    onClick={() => {\n                      if (!environmentsExpanded) setEnvironmentsExpanded(true);\n                      handleCreateEnvClick();\n                    }}\n                    title=\"Create environment\"\n                  >\n                    <IconPlus size={14} strokeWidth={1.5} />\n                  </button>\n                  <button\n                    type=\"button\"\n                    className=\"btn-action\"\n                    onClick={() => {\n                      if (!environmentsExpanded) setEnvironmentsExpanded(true);\n                      handleImportClick();\n                    }}\n                    title=\"Import environment\"\n                  >\n                    <IconDownload size={14} strokeWidth={1.5} />\n                  </button>\n                  <button\n                    type=\"button\"\n                    className=\"btn-action\"\n                    onClick={() => {\n                      if (!environmentsExpanded) setEnvironmentsExpanded(true);\n                      handleExportClick();\n                    }}\n                    title=\"Export environment\"\n                  >\n                    <IconUpload size={14} strokeWidth={1.5} />\n                  </button>\n                </>\n              )}\n            >\n              <div className=\"env-list-search\">\n                <IconSearch size={13} strokeWidth={1.5} className=\"env-list-search-icon\" />\n                <input\n                  ref={envListSearchInputRef}\n                  type=\"text\"\n                  placeholder=\"Search environments...\"\n                  value={searchText}\n                  onChange={(e) => setSearchText(e.target.value)}\n                  className=\"env-list-search-input\"\n                  autoComplete=\"off\"\n                  autoCorrect=\"off\"\n                  autoCapitalize=\"off\"\n                  spellCheck=\"false\"\n                />\n                {searchText && (\n                  <button\n                    className=\"env-list-search-clear\"\n                    title=\"Clear search\"\n                    onClick={() => setSearchText('')}\n                    onMouseDown={(e) => e.preventDefault()}\n                  >\n                    <IconX size={12} strokeWidth={1.5} />\n                  </button>\n                )}\n              </div>\n              <div className=\"environments-list\">\n                {filteredEnvironments.map((env) => (\n                  <div\n                    key={env.uid}\n                    id={env.uid}\n                    className={classnames('environment-item', {\n                      active: activeView === 'environment' && selectedEnvironment?.uid === env.uid,\n                      renaming: renamingEnvUid === env.uid,\n                      activated: activeEnvironmentUid === env.uid\n                    })}\n                    onClick={() => renamingEnvUid !== env.uid && handleEnvironmentClick(env)}\n                    onDoubleClick={() => handleEnvironmentDoubleClick(env)}\n                  >\n                    {renamingEnvUid === env.uid ? (\n                      <div className=\"rename-container\" ref={renameContainerRef}>\n                        <input\n                          ref={inputRef}\n                          type=\"text\"\n                          className=\"environment-name-input\"\n                          value={newEnvName}\n                          onChange={handleEnvNameChange}\n                          onKeyDown={handleEnvNameKeyDown}\n                          autoComplete=\"off\"\n                          autoCorrect=\"off\"\n                          autoCapitalize=\"off\"\n                          spellCheck=\"false\"\n                        />\n                        <div className=\"inline-actions\">\n                          <button\n                            className=\"inline-action-btn save\"\n                            onClick={handleSaveRename}\n                            onMouseDown={(e) => e.preventDefault()}\n                            title=\"Save\"\n                          >\n                            <IconCheck size={14} strokeWidth={2} />\n                          </button>\n                          <button\n                            className=\"inline-action-btn cancel\"\n                            onClick={handleCancelRename}\n                            onMouseDown={(e) => e.preventDefault()}\n                            title=\"Cancel\"\n                          >\n                            <IconX size={14} strokeWidth={2} />\n                          </button>\n                        </div>\n                      </div>\n                    ) : (\n                      <>\n                        <ColorBadge color={env.color} size={8} />\n                        <span className=\"environment-name\">{env.name}</span>\n                        <div className=\"environment-actions\">\n                          {activeEnvironmentUid === env.uid ? (\n                            <div className=\"activated-checkmark\" title=\"Active environment\">\n                              <IconCheck size={16} strokeWidth={2} />\n                            </div>\n                          ) : (\n                            <button\n                              className=\"activate-btn\"\n                              onClick={(e) => handleActivateEnvironment(e, env)}\n                              title=\"Activate environment\"\n                            >\n                              <IconCheck size={16} strokeWidth={2} />\n                            </button>\n                          )}\n                        </div>\n                      </>\n                    )}\n                  </div>\n                ))}\n\n                {isCreatingInline && (\n                  <div className=\"environment-item creating\" ref={createContainerRef}>\n                    <input\n                      ref={inputRef}\n                      type=\"text\"\n                      className=\"environment-name-input\"\n                      value={newEnvName}\n                      onChange={handleEnvNameChange}\n                      onKeyDown={handleEnvNameKeyDown}\n                      placeholder=\"Environment name...\"\n                      autoComplete=\"off\"\n                      autoCorrect=\"off\"\n                      autoCapitalize=\"off\"\n                      spellCheck=\"false\"\n                    />\n                    <div className=\"inline-actions\">\n                      <button\n                        className=\"inline-action-btn save\"\n                        onClick={handleSaveNewEnv}\n                        onMouseDown={(e) => e.preventDefault()}\n                        title=\"Save\"\n                      >\n                        <IconCheck size={14} strokeWidth={2} />\n                      </button>\n                      <button\n                        className=\"inline-action-btn cancel\"\n                        onClick={handleCancelCreate}\n                        onMouseDown={(e) => e.preventDefault()}\n                        title=\"Cancel\"\n                      >\n                        <IconX size={14} strokeWidth={2} />\n                      </button>\n                    </div>\n                  </div>\n                )}\n\n                {envNameError && (isCreatingInline || renamingEnvUid) && <div className=\"env-error\">{envNameError}</div>}\n\n                {filteredEnvironments.length === 0 && !isCreatingInline && (\n                  <div className=\"no-env-file\">\n                    <span>No environments</span>\n                  </div>\n                )}\n              </div>\n            </CollapsibleSection>\n\n            <CollapsibleSection\n              title=\".env Files\"\n              expanded={dotEnvExpanded}\n              onToggle={() => setDotEnvExpanded(!dotEnvExpanded)}\n              badge={dotEnvFiles.length}\n              actions={(\n                <button\n                  className=\"btn-action\"\n                  onClick={handleCreateDotEnvInlineClick}\n                  title=\"Create .env file\"\n                >\n                  <IconPlus size={14} strokeWidth={1.5} />\n                </button>\n              )}\n            >\n              <div className=\"environments-list\">\n                {dotEnvFiles.map((file) => (\n                  <div\n                    key={file.filename}\n                    className={classnames('environment-item', {\n                      active: activeView === 'dotenv' && selectedDotEnvFile === file.filename\n                    })}\n                    onClick={() => handleDotEnvClick(file.filename)}\n                  >\n                    <span className=\"environment-name\">{file.filename}</span>\n                  </div>\n                ))}\n\n                {isCreatingDotEnvInline && (\n                  <div className=\"environment-item creating\" ref={dotEnvCreateContainerRef}>\n                    <input\n                      ref={dotEnvInputRef}\n                      type=\"text\"\n                      className=\"environment-name-input\"\n                      value={newDotEnvName}\n                      onChange={handleDotEnvNameChange}\n                      onKeyDown={handleDotEnvNameKeyDown}\n                      autoComplete=\"off\"\n                      autoCorrect=\"off\"\n                      autoCapitalize=\"off\"\n                      spellCheck=\"false\"\n                    />\n                    <div className=\"inline-actions\">\n                      <button\n                        className=\"inline-action-btn save\"\n                        onClick={handleSaveNewDotEnv}\n                        onMouseDown={(e) => e.preventDefault()}\n                        title=\"Create\"\n                      >\n                        <IconCheck size={14} strokeWidth={2} />\n                      </button>\n                      <button\n                        className=\"inline-action-btn cancel\"\n                        onClick={handleCancelDotEnvCreate}\n                        onMouseDown={(e) => e.preventDefault()}\n                        title=\"Cancel\"\n                      >\n                        <IconX size={14} strokeWidth={2} />\n                      </button>\n                    </div>\n                  </div>\n                )}\n\n                {dotEnvNameError && isCreatingDotEnvInline && <div className=\"env-error\">{dotEnvNameError}</div>}\n\n                {dotEnvFiles.length === 0 && !isCreatingDotEnvInline && (\n                  <div className=\"no-env-file\">\n                    <span>No .env files</span>\n                  </div>\n                )}\n              </div>\n            </CollapsibleSection>\n          </div>\n        </div>\n\n        {renderContent()}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default EnvironmentList;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/EnvironmentSettings/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n  background-color: ${(props) => props.theme.bg};\n\n  .empty-state {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    padding-top: 10%;\n    flex: 1;\n    color: ${(props) => props.theme.colors.text.muted};\n\n    svg {\n      opacity: 0.3;\n      margin-bottom: 8px;\n    }\n\n    .title {\n      font-size: 13px;\n      font-weight: 500;\n      margin-bottom: 12px;\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n\n    .actions {\n      display: flex;\n      gap: 8px;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/EnvironmentSettings/index.js",
    "content": "import React, { useState } from 'react';\nimport EnvironmentList from './EnvironmentList';\nimport StyledWrapper from './StyledWrapper';\nimport ExportEnvironmentModal from 'components/Environments/Common/ExportEnvironmentModal';\n\nconst EnvironmentSettings = ({ collection }) => {\n  const [isModified, setIsModified] = useState(false);\n  const environments = collection?.environments || [];\n\n  const [selectedEnvironment, setSelectedEnvironment] = useState(() => {\n    if (!environments.length) return null;\n    return environments.find((env) => env.uid === collection?.activeEnvironmentUid) || environments[0];\n  });\n  const [showExportModal, setShowExportModal] = useState(false);\n\n  return (\n    <StyledWrapper>\n      <EnvironmentList\n        environments={environments}\n        activeEnvironmentUid={collection?.activeEnvironmentUid}\n        selectedEnvironment={selectedEnvironment}\n        setSelectedEnvironment={setSelectedEnvironment}\n        isModified={isModified}\n        setIsModified={setIsModified}\n        collection={collection}\n        setShowExportModal={setShowExportModal}\n      />\n      {showExportModal && (\n        <ExportEnvironmentModal\n          onClose={() => setShowExportModal(false)}\n          environments={environments}\n          environmentType=\"collection\"\n        />\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default EnvironmentSettings;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Environments/GlobalEnvironmentSettings/index.js",
    "content": "import React from 'react';\nimport { useSelector } from 'react-redux';\nimport WorkspaceEnvironments from 'components/WorkspaceHome/WorkspaceEnvironments';\n\nconst GlobalEnvironmentSettings = () => {\n  const activeWorkspaceUid = useSelector((state) => state.workspaces.activeWorkspaceUid);\n  const workspace = useSelector((state) =>\n    state.workspaces.workspaces.find((w) => w.uid === activeWorkspaceUid)\n  );\n\n  return <WorkspaceEnvironments workspace={workspace} />;\n};\n\nexport default GlobalEnvironmentSettings;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ErrorCapture/index.js",
    "content": "import React, { Component, useEffect } from 'react';\nimport { useDispatch } from 'react-redux';\nimport { addDebugError } from 'providers/ReduxStore/slices/logs';\n\nclass ErrorBoundary extends Component {\n  constructor(props) {\n    super(props);\n    this.state = { hasError: false };\n  }\n\n  static getDerivedStateFromError(error) {\n    return { hasError: true };\n  }\n\n  componentDidCatch(error, errorInfo) {\n    if (this.props.onError) {\n      this.props.onError({\n        message: error.message,\n        stack: error.stack,\n        error: error,\n        timestamp: new Date().toISOString()\n      });\n    }\n\n    setTimeout(() => {\n      this.setState({ hasError: false });\n    }, 100);\n  }\n\n  render() {\n    return this.props.children;\n  }\n}\n\nconst serializeArgs = (args) => {\n  return args.map((arg) => {\n    const seen = new WeakSet();\n\n    const replacer = (key, value) => {\n      if (typeof value === 'object' && value !== null) {\n        if (seen.has(value)) {\n          return '[Circular Reference]';\n        }\n        seen.add(value);\n\n        if (value instanceof Error || Object.prototype.toString.call(value) === '[object Error]' || (typeof value.message === 'string' && typeof value.stack === 'string')) {\n          const error = {};\n          Object.getOwnPropertyNames(value).forEach((prop) => {\n            error[prop] = value[prop];\n          });\n          return error;\n        }\n      }\n      return value;\n    };\n\n    try {\n      if (arg === null) return 'null';\n      if (arg === undefined) return 'undefined';\n      if (typeof arg === 'string' || typeof arg === 'number' || typeof arg === 'boolean') {\n        return arg;\n      }\n\n      if (typeof arg === 'object') {\n        try {\n          return JSON.parse(JSON.stringify(arg, replacer));\n        } catch {\n          return String(arg);\n        }\n      }\n      return String(arg);\n    } catch (e) {\n      return '[Unserializable]';\n    }\n  });\n};\n\n// Helper function to extract file and line info from stack trace\nconst extractFileInfo = (stack) => {\n  if (!stack) return { filename: null, lineno: null, colno: null };\n\n  try {\n    const lines = stack.split('\\n');\n    for (let line of lines) {\n      if (line.includes('ErrorCapture') || line.trim() === 'Error') continue;\n\n      const match = line.match(/(?:at\\s+.*?\\s+)?\\(?([^)]+):(\\d+):(\\d+)\\)?/);\n      if (match) {\n        return {\n          filename: match[1],\n          lineno: parseInt(match[2]),\n          colno: parseInt(match[3])\n        };\n      }\n    }\n  } catch (e) {\n    // Ignore parsing errors\n  }\n\n  return { filename: null, lineno: null, colno: null };\n};\n\nconst useGlobalErrorCapture = () => {\n  const dispatch = useDispatch();\n\n  useEffect(() => {\n    const originalConsoleError = console.error;\n\n    console.error = (...args) => {\n      const currentStack = new Error().stack;\n\n      originalConsoleError.apply(console, args);\n\n      if (currentStack && currentStack.includes('useIpcEvents.js')) {\n        return;\n      }\n\n      const errorMessage = args.join(' ');\n      if (errorMessage.includes('removeConsoleLogListener')) {\n        return;\n      }\n\n      const { filename, lineno, colno } = extractFileInfo(currentStack);\n\n      const serializedArgs = serializeArgs(args);\n\n      dispatch(addDebugError({\n        message: errorMessage,\n        stack: currentStack,\n        filename: filename,\n        lineno: lineno,\n        colno: colno,\n        args: serializedArgs,\n        timestamp: new Date().toISOString()\n      }));\n    };\n\n    return () => {\n      console.error = originalConsoleError;\n    };\n  }, [dispatch]);\n};\n\nconst ErrorCapture = ({ children }) => {\n  const dispatch = useDispatch();\n\n  useGlobalErrorCapture();\n\n  const handleReactError = (errorData) => {\n    dispatch(addDebugError(errorData));\n  };\n\n  return (\n    <ErrorBoundary onError={handleReactError}>\n      {children}\n    </ErrorBoundary>\n  );\n};\n\nexport default ErrorCapture;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Errors/IpcErrorModal/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  color: ${(props) => props.theme.colors.danger};\n  pre {\n    color: ${(props) => props.theme.colors.danger};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Errors/IpcErrorModal/index.js",
    "content": "import React from 'react';\nimport Portal from 'components/Portal';\nimport Modal from 'components/Modal';\nimport { useState } from 'react';\nimport StyledWrapper from './StyledWrapper';\n\nconst IpcErrorModal = ({ error }) => {\n  const [showModal, setShowModal] = useState(true);\n  return (\n    <>\n      {showModal ? (\n        <StyledWrapper>\n          <Portal>\n            <Modal\n              size=\"sm\"\n              title=\"Error\"\n              hideFooter={true}\n              hideCancel={true}\n              handleCancel={() => {\n                setShowModal(false);\n              }}\n              disableCloseOnOutsideClick={true}\n              disableEscapeKey={true}\n            >\n              <pre className=\"w-full flex flex-wrap whitespace-pre-wrap\">{error}</pre>\n            </Modal>\n          </Portal>\n        </StyledWrapper>\n      ) : null}\n    </>\n  );\n};\n\nexport default IpcErrorModal;\n"
  },
  {
    "path": "packages/bruno-app/src/components/FilePickerEditor/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  align-items: center;\n  width: 100%;\n  overflow: hidden;\n  min-width: 0;\n\n  .file-picker-btn {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    padding: 4px 8px;\n    color: ${(props) => props.theme.colors.text.muted};\n    background: transparent;\n    border: none;\n    cursor: pointer;\n    border-radius: 4px;\n    transition: color 0.15s ease;\n    font-size: 12px;\n    white-space: nowrap;\n    overflow: hidden;\n    min-width: 0;\n    max-width: 100%;\n\n    &:hover {\n      color: ${(props) => props.theme.text} !important;\n    }\n\n    &.read-only {\n      cursor: default;\n      opacity: 0.6;\n    }\n\n    &.icon-only {\n      padding: 4px;\n      flex-shrink: 0;\n    }\n\n    &.icon-right {\n      width: 100%;\n      justify-content: space-between;\n    }\n\n    span {\n      line-height: 1;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      min-width: 0;\n    }\n\n    .label {\n      font-style: italic;\n    }\n\n    svg {\n      flex-shrink: 0;\n    }\n  }\n\n  .file-picker-selected {\n    display: flex;\n    align-items: center;\n    padding: 4px 0;\n    width: 100%;\n    cursor: pointer;\n\n    &.read-only {\n      cursor: default;\n    }\n\n    .file-icon {\n      flex-shrink: 0;\n      color: ${(props) => props.theme.colors.text.muted};\n      margin-right: 4px;\n    }\n\n    .file-name {\n      flex: 1;\n      font-size: 12px;\n      color: ${(props) => props.theme.text};\n      overflow: hidden;\n      text-overflow: ellipsis;\n      white-space: nowrap;\n      min-width: 0;\n    }\n\n    .clear-btn {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      padding: 4px;\n      margin-left: 4px;\n      color: ${(props) => props.theme.colors.text.muted};\n      background: transparent;\n      border: none;\n      cursor: pointer;\n      border-radius: 4px;\n      transition: color 0.15s ease;\n      flex-shrink: 0;\n\n      &:hover {\n        color: ${(props) => props.theme.text};\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/FilePickerEditor/index.js",
    "content": "import React from 'react';\nimport path from 'utils/common/path';\nimport { useDispatch } from 'react-redux';\nimport { browseFiles } from 'providers/ReduxStore/slices/collections/actions';\nimport { IconX, IconUpload, IconFile } from '@tabler/icons';\nimport { isWindowsOS } from 'utils/common/platform';\nimport StyledWrapper from './StyledWrapper';\n\n/**\n * FilePickerEditor component for selecting files\n *\n * @param {Object} props\n * @param {string|string[]} props.value - Selected file path(s)\n * @param {Function} props.onChange - Callback when file selection changes\n * @param {Object} props.collection - Collection object with pathname\n * @param {boolean} props.isSingleFilePicker - If true, only allows single file selection\n * @param {boolean} props.readOnly - If true, disables file selection\n * @param {string} props.displayMode - Display mode: 'label', 'icon', or 'labelAndIcon' (default: 'label')\n * @param {string} props.label - Custom label text (defaults to \"Select File\" or \"Select Files\")\n * @param {React.ComponentType} props.icon - Custom icon component (defaults to IconUpload)\n */\nconst FilePickerEditor = ({\n  value,\n  onChange,\n  collection,\n  isSingleFilePicker = false,\n  readOnly = false,\n  displayMode = 'label',\n  label,\n  icon: CustomIcon\n}) => {\n  const dispatch = useDispatch();\n  const filenames = (isSingleFilePicker ? [value] : value || [])\n    .filter((v) => v != null && v != '')\n    .map((v) => {\n      const separator = isWindowsOS() ? '\\\\' : '/';\n      return v.split(separator).pop();\n    });\n\n  // title is shown when hovering over the button\n  const title = filenames.map((v) => `- ${v}`).join('\\n');\n\n  const browse = () => {\n    if (readOnly) return;\n\n    dispatch(browseFiles([], [!isSingleFilePicker ? 'multiSelections' : '']))\n      .then((filePaths) => {\n        // If file is in the collection's directory, then we use relative path\n        // Otherwise, we use the absolute path\n        filePaths = filePaths.map((filePath) => {\n          const collectionDir = collection.pathname;\n\n          if (filePath.startsWith(collectionDir)) {\n            return path.relative(collectionDir, filePath);\n          }\n\n          return filePath;\n        });\n\n        onChange(isSingleFilePicker ? filePaths[0] : filePaths);\n      })\n      .catch((error) => {\n        console.error(error);\n      });\n  };\n\n  const clear = (e) => {\n    e.stopPropagation();\n    onChange(isSingleFilePicker ? '' : []);\n  };\n\n  const renderButtonText = (filenames) => {\n    if (filenames.length == 1) {\n      return filenames[0];\n    }\n    return filenames.length + ' file(s) selected';\n  };\n\n  const defaultLabel = isSingleFilePicker ? 'Select File' : 'Select Files';\n  const displayLabel = label || defaultLabel;\n  const IconComponent = CustomIcon || IconUpload;\n\n  // Render the button content based on displayMode\n  const renderButtonContent = () => {\n    switch (displayMode) {\n      case 'icon':\n        return <IconComponent size={16} />;\n      case 'labelAndIcon':\n        return (\n          <>\n            <span className=\"label\">{displayLabel}</span>\n            <IconComponent size={16} />\n          </>\n        );\n      case 'label':\n      default:\n        return <span>{displayLabel}</span>;\n    }\n  };\n\n  // When files are selected, show file info with clear button\n  if (filenames.length > 0) {\n    return (\n      <StyledWrapper>\n        <div\n          className={`file-picker-selected ${readOnly ? 'read-only' : ''}`}\n          title={title}\n          onClick={!readOnly ? browse : undefined}\n        >\n          <IconFile size={16} className=\"file-icon\" />\n          <span className=\"file-name\">\n            {renderButtonText(filenames)}\n          </span>\n          {!readOnly && (\n            <button\n              className=\"clear-btn\"\n              onClick={clear}\n              title=\"Remove file\"\n              type=\"button\"\n            >\n              <IconX size={16} />\n            </button>\n          )}\n        </div>\n      </StyledWrapper>\n    );\n  }\n\n  // When no files selected, show the picker button\n  return (\n    <StyledWrapper>\n      <button\n        className={`file-picker-btn ${readOnly ? 'read-only' : ''} ${displayMode === 'icon' ? 'icon-only' : ''} ${displayMode === 'labelAndIcon' ? 'icon-right' : ''}`}\n        onClick={browse}\n        disabled={readOnly}\n        type=\"button\"\n        title={displayLabel}\n      >\n        {renderButtonContent()}\n      </button>\n    </StyledWrapper>\n  );\n};\n\nexport default FilePickerEditor;\n"
  },
  {
    "path": "packages/bruno-app/src/components/FolderSettings/Auth/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  label {\n    font-size: ${(props) => props.theme.font.size.base};\n  }\n  .single-line-editor-wrapper {\n    max-width: 400px;\n    padding: 0.15rem 0.4rem;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n  }\n  .inherit-mode-text {\n    color: ${(props) => props.theme.primary.text};\n  }\n  .auth-mode-label {\n    color: ${(props) => props.theme.colors.text.yellow};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/FolderSettings/Auth/index.js",
    "content": "import React from 'react';\nimport get from 'lodash/get';\nimport StyledWrapper from './StyledWrapper';\nimport { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';\nimport OAuth2AuthorizationCode from 'components/RequestPane/Auth/OAuth2/AuthorizationCode/index';\nimport { updateFolderAuth as _updateFolderAuth } from 'providers/ReduxStore/slices/collections';\nimport { useDispatch } from 'react-redux';\nimport OAuth2PasswordCredentials from 'components/RequestPane/Auth/OAuth2/PasswordCredentials/index';\nimport OAuth2ClientCredentials from 'components/RequestPane/Auth/OAuth2/ClientCredentials/index';\nimport OAuth2Implicit from 'components/RequestPane/Auth/OAuth2/Implicit/index';\nimport GrantTypeSelector from 'components/RequestPane/Auth/OAuth2/GrantTypeSelector/index';\nimport AuthMode from '../AuthMode';\nimport BasicAuth from 'components/RequestPane/Auth/BasicAuth';\nimport BearerAuth from 'components/RequestPane/Auth/BearerAuth';\nimport DigestAuth from 'components/RequestPane/Auth/DigestAuth';\nimport NTLMAuth from 'components/RequestPane/Auth/NTLMAuth';\nimport WsseAuth from 'components/RequestPane/Auth/WsseAuth';\nimport ApiKeyAuth from 'components/RequestPane/Auth/ApiKeyAuth';\nimport AwsV4Auth from 'components/RequestPane/Auth/AwsV4Auth';\nimport { humanizeRequestAuthMode, getTreePathFromCollectionToItem } from 'utils/collections/index';\nimport Button from 'ui/Button';\n\nconst GrantTypeComponentMap = ({ collection, folder, updateFolderAuth }) => {\n  const dispatch = useDispatch();\n\n  const save = () => {\n    dispatch(saveFolderRoot(collection.uid, folder.uid));\n  };\n\n  const folderRoot = folder?.draft || folder?.root;\n  let request = get(folderRoot, 'request', {});\n  const grantType = get(request, 'auth.oauth2.grantType', 'authorization_code');\n\n  switch (grantType) {\n    case 'password':\n      return <OAuth2PasswordCredentials save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;\n    case 'authorization_code':\n      return <OAuth2AuthorizationCode save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;\n    case 'client_credentials':\n      return <OAuth2ClientCredentials save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;\n    case 'implicit':\n      return <OAuth2Implicit save={save} item={folder} request={request} updateAuth={updateFolderAuth} collection={collection} folder={folder} />;\n    default:\n      return <div>TBD</div>;\n  }\n};\n\nconst Auth = ({ collection, folder }) => {\n  const dispatch = useDispatch();\n  const folderRoot = folder?.draft || folder?.root;\n  let request = get(folderRoot, 'request', {});\n  const authMode = get(folderRoot, 'request.auth.mode');\n\n  const getEffectiveAuthSource = () => {\n    if (authMode !== 'inherit') return null;\n\n    const collectionRoot = collection?.draft?.root || collection?.root || {};\n    const collectionAuth = get(collectionRoot, 'request.auth');\n    let effectiveSource = {\n      type: 'collection',\n      name: 'Collection',\n      auth: collectionAuth\n    };\n\n    // Get path from collection to current folder\n    const folderTreePath = getTreePathFromCollectionToItem(collection, folder);\n\n    // Check parent folders to find closest auth configuration\n    // Skip the last item which is the current folder\n    for (let i = 0; i < folderTreePath.length - 1; i++) {\n      const parentFolder = folderTreePath[i];\n      if (parentFolder.type === 'folder') {\n        const parentFolderRoot = parentFolder?.draft || parentFolder?.root;\n        const folderAuth = get(parentFolderRoot, 'request.auth');\n        if (folderAuth && folderAuth.mode && folderAuth.mode !== 'inherit') {\n          effectiveSource = {\n            type: 'folder',\n            name: parentFolder.name,\n            auth: folderAuth\n          };\n          break;\n        }\n      }\n    }\n\n    return effectiveSource;\n  };\n\n  const handleSave = () => {\n    dispatch(saveFolderRoot(collection.uid, folder.uid));\n  };\n\n  const updateFolderAuth = ({ itemUid, ...rest }) => {\n    return _updateFolderAuth({\n      ...rest,\n      folderUid: folder.uid\n    });\n  };\n\n  const getAuthView = () => {\n    switch (authMode) {\n      case 'basic': {\n        return (\n          <BasicAuth\n            collection={collection}\n            item={folder}\n            updateAuth={updateFolderAuth}\n            request={request}\n            save={() => handleSave()}\n          />\n        );\n      }\n      case 'bearer': {\n        return (\n          <BearerAuth\n            collection={collection}\n            item={folder}\n            updateAuth={updateFolderAuth}\n            request={request}\n            save={() => handleSave()}\n          />\n        );\n      }\n      case 'digest': {\n        return (\n          <DigestAuth\n            collection={collection}\n            item={folder}\n            updateAuth={updateFolderAuth}\n            request={request}\n            save={() => handleSave()}\n          />\n        );\n      }\n      case 'ntlm': {\n        return (\n          <NTLMAuth\n            collection={collection}\n            item={folder}\n            updateAuth={updateFolderAuth}\n            request={request}\n            save={() => handleSave()}\n          />\n        );\n      }\n      case 'wsse': {\n        return (\n          <WsseAuth\n            collection={collection}\n            item={folder}\n            updateAuth={updateFolderAuth}\n            request={request}\n            save={() => handleSave()}\n          />\n        );\n      }\n      case 'apikey': {\n        return (\n          <ApiKeyAuth\n            collection={collection}\n            item={folder}\n            updateAuth={updateFolderAuth}\n            request={request}\n            save={() => handleSave()}\n          />\n        );\n      }\n      case 'awsv4': {\n        return (\n          <AwsV4Auth\n            collection={collection}\n            item={folder}\n            updateAuth={updateFolderAuth}\n            request={request}\n            save={() => handleSave()}\n          />\n        );\n      }\n      case 'oauth2': {\n        return (\n          <>\n            <GrantTypeSelector\n              request={request}\n              updateAuth={updateFolderAuth}\n              collection={collection}\n              item={folder}\n            />\n            <GrantTypeComponentMap collection={collection} folder={folder} updateFolderAuth={updateFolderAuth} />\n          </>\n        );\n      }\n      case 'inherit': {\n        const source = getEffectiveAuthSource();\n        return (\n          <>\n            <div className=\"flex flex-row w-full mt-2 gap-2\">\n              <div>Auth inherited from {source.name}: </div>\n              <div className=\"inherit-mode-text\">{humanizeRequestAuthMode(source.auth?.mode)}</div>\n            </div>\n          </>\n        );\n      }\n      case 'none': {\n        return null;\n      }\n      default:\n        return null;\n    }\n  };\n\n  return (\n    <StyledWrapper className=\"w-full\">\n      <div className=\"text-xs mb-4 text-muted\">\n        Configures authentication for the entire folder. This applies to all requests using the{' '}\n        <span className=\"font-medium\">Inherit</span> option in the <span className=\"font-medium\">Auth</span> tab.\n      </div>\n      <div className=\"flex flex-grow justify-start items-center\">\n        <AuthMode collection={collection} folder={folder} />\n      </div>\n      {getAuthView()}\n      <div className=\"mt-6\">\n        <Button type=\"submit\" size=\"sm\" onClick={handleSave}>\n          Save\n        </Button>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Auth;\n"
  },
  {
    "path": "packages/bruno-app/src/components/FolderSettings/AuthMode/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  font-size: ${(props) => props.theme.font.size.base};\n\n  .auth-mode-selector {\n    background: transparent;\n\n    .auth-mode-label {\n      color: ${(props) => props.theme.primary.text};\n\n    .caret {\n      color: rgb(140, 140, 140);\n      fill: rgb(140, 140, 140);\n    }\n  }\n}\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/FolderSettings/AuthMode/index.js",
    "content": "import React, { useMemo, useCallback } from 'react';\nimport get from 'lodash/get';\nimport { IconCaretDown } from '@tabler/icons';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport { useDispatch } from 'react-redux';\nimport { updateFolderAuthMode } from 'providers/ReduxStore/slices/collections';\nimport { humanizeRequestAuthMode } from 'utils/collections';\nimport StyledWrapper from './StyledWrapper';\n\nconst AuthMode = ({ collection, folder }) => {\n  const dispatch = useDispatch();\n  const authMode = folder.draft ? get(folder, 'draft.request.auth.mode') : get(folder, 'root.request.auth.mode');\n\n  const onModeChange = useCallback((value) => {\n    dispatch(\n      updateFolderAuthMode({\n        mode: value,\n        collectionUid: collection.uid,\n        folderUid: folder.uid\n      })\n    );\n  }, [dispatch, collection.uid, folder.uid]);\n\n  const menuItems = useMemo(() => [\n    {\n      id: 'awsv4',\n      label: 'AWS Sig v4',\n      onClick: () => onModeChange('awsv4')\n    },\n    {\n      id: 'basic',\n      label: 'Basic Auth',\n      onClick: () => onModeChange('basic')\n    },\n    {\n      id: 'bearer',\n      label: 'Bearer Token',\n      onClick: () => onModeChange('bearer')\n    },\n    {\n      id: 'digest',\n      label: 'Digest Auth',\n      onClick: () => onModeChange('digest')\n    },\n    {\n      id: 'ntlm',\n      label: 'NTLM Auth',\n      onClick: () => onModeChange('ntlm')\n    },\n    {\n      id: 'oauth2',\n      label: 'OAuth 2.0',\n      onClick: () => onModeChange('oauth2')\n    },\n    {\n      id: 'wsse',\n      label: 'WSSE Auth',\n      onClick: () => onModeChange('wsse')\n    },\n    {\n      id: 'apikey',\n      label: 'API Key',\n      onClick: () => onModeChange('apikey')\n    },\n    {\n      id: 'inherit',\n      label: 'Inherit',\n      onClick: () => onModeChange('inherit')\n    },\n    {\n      id: 'none',\n      label: 'No Auth',\n      onClick: () => onModeChange('none')\n    }\n  ], [onModeChange]);\n\n  return (\n    <StyledWrapper>\n      <div className=\"inline-flex items-center cursor-pointer auth-mode-selector\">\n        <MenuDropdown\n          items={menuItems}\n          placement=\"bottom-end\"\n          selectedItemId={authMode}\n          showTickMark={true}\n        >\n          <div className=\"flex items-center justify-center auth-mode-label select-none\">\n            {humanizeRequestAuthMode(authMode)} <IconCaretDown className=\"caret ml-1 mr-1\" size={14} strokeWidth={2} />\n          </div>\n        </MenuDropdown>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default AuthMode;\n"
  },
  {
    "path": "packages/bruno-app/src/components/FolderSettings/Documentation/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .editing-mode {\n    cursor: pointer;\n    color: ${(props) => props.theme.colors.text.yellow};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/FolderSettings/Documentation/index.js",
    "content": "import 'github-markdown-css/github-markdown.css';\nimport get from 'lodash/get';\nimport { updateFolderDocs } from 'providers/ReduxStore/slices/collections';\nimport { useTheme } from 'providers/Theme';\nimport { useState } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';\nimport Markdown from 'components/MarkDown';\nimport CodeEditor from 'components/CodeEditor';\nimport Button from 'ui/Button';\nimport StyledWrapper from './StyledWrapper';\n\nconst Documentation = ({ collection, folder }) => {\n  const dispatch = useDispatch();\n  const { displayedTheme } = useTheme();\n  const preferences = useSelector((state) => state.app.preferences);\n  const [isEditing, setIsEditing] = useState(false);\n  const docs = folder.draft ? get(folder, 'draft.docs', '') : get(folder, 'root.docs', '');\n\n  const toggleViewMode = () => {\n    setIsEditing((prev) => !prev);\n  };\n\n  const onEdit = (value) => {\n    dispatch(\n      updateFolderDocs({\n        folderUid: folder.uid,\n        collectionUid: collection.uid,\n        docs: value\n      })\n    );\n  };\n\n  const onSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));\n\n  if (!folder) {\n    return null;\n  }\n\n  return (\n    <StyledWrapper className=\"w-full relative flex flex-col\">\n      <div className=\"editing-mode flex justify-between items-center flex-shrink-0\" role=\"tab\" onClick={toggleViewMode}>\n        {isEditing ? 'Preview' : 'Edit'}\n      </div>\n\n      {isEditing ? (\n        <div className=\"flex flex-col flex-1 min-h-0\">\n          <div className=\"mt-2 flex-1 overflow-auto min-h-0\">\n            <CodeEditor\n              collection={collection}\n              theme={displayedTheme}\n              value={docs || ''}\n              onEdit={onEdit}\n              onSave={onSave}\n              font={get(preferences, 'font.codeFont', 'default')}\n              fontSize={get(preferences, 'font.codeFontSize')}\n              mode=\"application/text\"\n            />\n          </div>\n          <div className=\"mt-6 flex-shrink-0\">\n            <Button type=\"submit\" size=\"sm\" onClick={onSave}>\n              Save\n            </Button>\n          </div>\n        </div>\n      ) : (\n        <div className=\"h-full\">\n          <Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />\n        </div>\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default Documentation;\n"
  },
  {
    "path": "packages/bruno-app/src/components/FolderSettings/Headers/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  table {\n    width: 100%;\n    border-collapse: collapse;\n    font-weight: 500;\n    table-layout: fixed;\n\n    thead,\n    td {\n      border: 1px solid ${(props) => props.theme.table.border};\n    }\n\n    thead {\n      color: ${(props) => props.theme.table.thead.color};\n      font-size: ${(props) => props.theme.font.size.base};\n      user-select: none;\n    }\n    td {\n      padding: 6px 10px;\n\n      &:nth-child(1) {\n        width: 30%;\n      }\n\n      &:nth-child(3) {\n        width: 70px;\n      }\n    }\n  }\n\n  .btn-add-header {\n    font-size: ${(props) => props.theme.font.size.base};\n  }\n\n  input[type='text'] {\n    width: 100%;\n    border: solid 1px transparent;\n    outline: none !important;\n    background-color: inherit;\n\n    &:focus {\n      outline: none !important;\n      border: solid 1px transparent;\n    }\n  }\n\n  input[type='checkbox'] {\n    cursor: pointer;\n    position: relative;\n    top: 1px;\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/FolderSettings/Headers/index.js",
    "content": "import React, { useState, useCallback } from 'react';\nimport get from 'lodash/get';\nimport { useDispatch } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport { setFolderHeaders } from 'providers/ReduxStore/slices/collections';\nimport { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport EditableTable from 'components/EditableTable';\nimport StyledWrapper from './StyledWrapper';\nimport { headers as StandardHTTPHeaders } from 'know-your-http-well';\nimport { MimeTypes } from 'utils/codemirror/autocompleteConstants';\nimport BulkEditor from 'components/BulkEditor/index';\nimport Button from 'ui/Button';\nimport { headerNameRegex, headerValueRegex } from 'utils/common/regex';\n\nconst headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);\n\nconst Headers = ({ collection, folder }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n  const headers = folder.draft\n    ? get(folder, 'draft.request.headers', [])\n    : get(folder, 'root.request.headers', []);\n  const [isBulkEditMode, setIsBulkEditMode] = useState(false);\n\n  const toggleBulkEditMode = () => {\n    setIsBulkEditMode(!isBulkEditMode);\n  };\n\n  const handleHeadersChange = useCallback((updatedHeaders) => {\n    dispatch(setFolderHeaders({\n      collectionUid: collection.uid,\n      folderUid: folder.uid,\n      headers: updatedHeaders\n    }));\n  }, [dispatch, collection.uid, folder.uid]);\n\n  const handleSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));\n\n  const getRowError = useCallback((row, index, key) => {\n    if (key === 'name') {\n      if (!row.name || row.name.trim() === '') return null;\n      if (!headerNameRegex.test(row.name)) {\n        return 'Header name cannot contain spaces or newlines';\n      }\n    }\n    if (key === 'value') {\n      if (!row.value) return null;\n      if (!headerValueRegex.test(row.value)) {\n        return 'Header value cannot contain newlines';\n      }\n    }\n    return null;\n  }, []);\n\n  const columns = [\n    {\n      key: 'name',\n      name: 'Name',\n      isKeyField: true,\n      placeholder: 'Name',\n      width: '30%',\n      render: ({ value, onChange }) => (\n        <SingleLineEditor\n          value={value || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(newValue) => onChange(newValue.replace(/[\\r\\n]/g, ''))}\n          autocomplete={headerAutoCompleteList}\n          collection={collection}\n          placeholder={!value ? 'Name' : ''}\n        />\n      )\n    },\n    {\n      key: 'value',\n      name: 'Value',\n      placeholder: 'Value',\n      render: ({ value, onChange }) => (\n        <SingleLineEditor\n          value={value || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={onChange}\n          collection={collection}\n          item={folder}\n          autocomplete={MimeTypes}\n          placeholder={!value ? 'Value' : ''}\n        />\n      )\n    }\n  ];\n\n  const defaultRow = {\n    name: '',\n    value: '',\n    description: ''\n  };\n\n  if (isBulkEditMode) {\n    return (\n      <StyledWrapper className=\"w-full\">\n        <div className=\"text-xs mb-4 text-muted\">\n          Request headers that will be sent with every request inside this folder.\n        </div>\n        <BulkEditor\n          params={headers}\n          onChange={handleHeadersChange}\n          onToggle={toggleBulkEditMode}\n          onSave={handleSave}\n        />\n      </StyledWrapper>\n    );\n  }\n\n  return (\n    <StyledWrapper className=\"w-full\">\n      <div className=\"text-xs mb-4 text-muted\">\n        Request headers that will be sent with every request inside this folder.\n      </div>\n      <EditableTable\n        columns={columns}\n        rows={headers}\n        onChange={handleHeadersChange}\n        defaultRow={defaultRow}\n        getRowError={getRowError}\n      />\n      <div className=\"flex justify-end mt-2\">\n        <button className=\"text-link select-none\" onClick={toggleBulkEditMode}>\n          Bulk Edit\n        </button>\n      </div>\n      <div className=\"mt-6\">\n        <Button type=\"submit\" size=\"sm\" onClick={handleSave}>\n          Save\n        </Button>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Headers;\n"
  },
  {
    "path": "packages/bruno-app/src/components/FolderSettings/Script/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  div.CodeMirror {\n    height: inherit;\n  }\n\n  div.title {\n    color: ${(props) => props.theme.colors.text.subtext0};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/FolderSettings/Script/index.js",
    "content": "import React, { useState, useEffect, useRef } from 'react';\nimport get from 'lodash/get';\nimport { useDispatch, useSelector } from 'react-redux';\nimport CodeEditor from 'components/CodeEditor';\nimport { updateFolderRequestScript, updateFolderResponseScript } from 'providers/ReduxStore/slices/collections';\nimport { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';\nimport { useTheme } from 'providers/Theme';\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs';\nimport StatusDot from 'components/StatusDot';\nimport { flattenItems, isItemARequest } from 'utils/collections';\nimport StyledWrapper from './StyledWrapper';\nimport Button from 'ui/Button';\n\nconst Script = ({ collection, folder }) => {\n  const dispatch = useDispatch();\n  const preRequestEditorRef = useRef(null);\n  const postResponseEditorRef = useRef(null);\n  const requestScript = folder.draft ? get(folder, 'draft.request.script.req', '') : get(folder, 'root.request.script.req', '');\n  const responseScript = folder.draft ? get(folder, 'draft.request.script.res', '') : get(folder, 'root.request.script.res', '');\n\n  // Default to post-response if pre-request script is empty\n  const getInitialTab = () => {\n    const hasPreRequestScript = requestScript && requestScript.trim().length > 0;\n    return hasPreRequestScript ? 'pre-request' : 'post-response';\n  };\n\n  const [activeTab, setActiveTab] = useState(getInitialTab);\n  const prevFolderUidRef = useRef(folder.uid);\n\n  const { displayedTheme } = useTheme();\n  const preferences = useSelector((state) => state.app.preferences);\n\n  // Update active tab only when switching to a different folder\n  useEffect(() => {\n    if (prevFolderUidRef.current !== folder.uid) {\n      prevFolderUidRef.current = folder.uid;\n      const hasPreRequestScript = requestScript && requestScript.trim().length > 0;\n      setActiveTab(hasPreRequestScript ? 'pre-request' : 'post-response');\n    }\n  }, [folder.uid, requestScript]);\n\n  // Refresh CodeMirror when tab becomes visible\n  useEffect(() => {\n    const timer = setTimeout(() => {\n      if (activeTab === 'pre-request' && preRequestEditorRef.current?.editor) {\n        preRequestEditorRef.current.editor.refresh();\n      } else if (activeTab === 'post-response' && postResponseEditorRef.current?.editor) {\n        postResponseEditorRef.current.editor.refresh();\n      }\n    }, 0);\n\n    return () => clearTimeout(timer);\n  }, [activeTab]);\n\n  const onRequestScriptEdit = (value) => {\n    dispatch(\n      updateFolderRequestScript({\n        script: value,\n        collectionUid: collection.uid,\n        folderUid: folder.uid\n      })\n    );\n  };\n\n  const onResponseScriptEdit = (value) => {\n    dispatch(\n      updateFolderResponseScript({\n        script: value,\n        collectionUid: collection.uid,\n        folderUid: folder.uid\n      })\n    );\n  };\n\n  const handleSave = () => {\n    dispatch(saveFolderRoot(collection.uid, folder.uid));\n  };\n\n  const items = flattenItems(folder.items || []);\n  const hasPreRequestScriptError = items.some((i) => isItemARequest(i) && i.preRequestScriptErrorMessage);\n  const hasPostResponseScriptError = items.some((i) => isItemARequest(i) && i.postResponseScriptErrorMessage);\n\n  return (\n    <StyledWrapper className=\"w-full flex flex-col h-full\">\n      <div className=\"text-xs mb-4 text-muted\">\n        Pre and post-request scripts that will run before and after any request inside this folder is sent.\n      </div>\n\n      <Tabs value={activeTab} onValueChange={setActiveTab}>\n        <TabsList>\n          <TabsTrigger value=\"pre-request\">\n            Pre Request\n            {requestScript && requestScript.trim().length > 0 && (\n              <StatusDot type={hasPreRequestScriptError ? 'error' : 'default'} />\n            )}\n          </TabsTrigger>\n          <TabsTrigger value=\"post-response\">\n            Post Response\n            {responseScript && responseScript.trim().length > 0 && (\n              <StatusDot type={hasPostResponseScriptError ? 'error' : 'default'} />\n            )}\n          </TabsTrigger>\n        </TabsList>\n\n        <TabsContent value=\"pre-request\" className=\"mt-2\">\n          <CodeEditor\n            ref={preRequestEditorRef}\n            collection={collection}\n            value={requestScript || ''}\n            theme={displayedTheme}\n            onEdit={onRequestScriptEdit}\n            mode=\"javascript\"\n            onSave={handleSave}\n            font={get(preferences, 'font.codeFont', 'default')}\n            fontSize={get(preferences, 'font.codeFontSize')}\n            showHintsFor={['req', 'bru']}\n          />\n        </TabsContent>\n\n        <TabsContent value=\"post-response\" className=\"mt-2\">\n          <CodeEditor\n            ref={postResponseEditorRef}\n            collection={collection}\n            value={responseScript || ''}\n            theme={displayedTheme}\n            onEdit={onResponseScriptEdit}\n            mode=\"javascript\"\n            onSave={handleSave}\n            font={get(preferences, 'font.codeFont', 'default')}\n            fontSize={get(preferences, 'font.codeFontSize')}\n            showHintsFor={['req', 'res', 'bru']}\n          />\n        </TabsContent>\n      </Tabs>\n\n      <div className=\"mt-12\">\n        <Button type=\"submit\" size=\"sm\" onClick={handleSave}>\n          Save\n        </Button>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Script;\n"
  },
  {
    "path": "packages/bruno-app/src/components/FolderSettings/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  max-width: 800px;\n\n  div.tabs {\n    div.tab {\n      padding: 6px 0px;\n      border: none;\n      border-bottom: solid 2px transparent;\n      margin-right: ${(props) => props.theme.tabs.marginRight};\n      color: ${(props) => props.theme.colors.text.subtext0};\n      cursor: pointer;\n\n      &:focus,\n      &:active,\n      &:focus-within,\n      &:focus-visible,\n      &:target {\n        outline: none !important;\n        box-shadow: none !important;\n      }\n\n      &:hover {\n        color: ${(props) => props.theme.tabs.active.color} !important;\n      }\n\n      &.active {\n        font-weight: ${(props) => props.theme.tabs.active.fontWeight} !important;\n        color: ${(props) => props.theme.tabs.active.color} !important;\n        border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;\n      }\n    }\n  }\n  table {\n    thead,\n    td {\n      border: 1px solid ${(props) => props.theme.table.border};\n\n      li {\n        background-color: ${(props) => props.theme.bg} !important;\n      }\n    }\n  }\n\n  .muted {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/FolderSettings/Tests/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div``;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/FolderSettings/Tests/index.js",
    "content": "import React from 'react';\nimport get from 'lodash/get';\nimport { useDispatch, useSelector } from 'react-redux';\nimport CodeEditor from 'components/CodeEditor';\nimport { updateFolderTests } from 'providers/ReduxStore/slices/collections';\nimport { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';\nimport { useTheme } from 'providers/Theme';\nimport StyledWrapper from './StyledWrapper';\nimport Button from 'ui/Button';\n\nconst Tests = ({ collection, folder }) => {\n  const dispatch = useDispatch();\n  const tests = folder.draft ? get(folder, 'draft.request.tests', '') : get(folder, 'root.request.tests', '');\n\n  const { displayedTheme } = useTheme();\n  const preferences = useSelector((state) => state.app.preferences);\n\n  const onEdit = (value) => {\n    dispatch(\n      updateFolderTests({\n        tests: value,\n        collectionUid: collection.uid,\n        folderUid: folder.uid\n      })\n    );\n  };\n\n  const handleSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));\n\n  return (\n    <StyledWrapper className=\"w-full flex flex-col h-full\">\n      <div className=\"text-xs mb-4 text-muted\">These tests will run any time a request in this collection is sent.</div>\n      <CodeEditor\n        collection={collection}\n        value={tests || ''}\n        theme={displayedTheme}\n        onEdit={onEdit}\n        mode=\"javascript\"\n        onSave={handleSave}\n        font={get(preferences, 'font.codeFont', 'default')}\n        fontSize={get(preferences, 'font.codeFontSize')}\n        showHintsFor={['req', 'res', 'bru']}\n      />\n\n      <div className=\"mt-6\">\n        <Button type=\"submit\" size=\"sm\" onClick={handleSave}>\n          Save\n        </Button>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Tests;\n"
  },
  {
    "path": "packages/bruno-app/src/components/FolderSettings/Vars/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  div.title {\n    color: ${(props) => props.theme.colors.text.subtext0};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  table {\n    width: 100%;\n    border-collapse: collapse;\n    font-weight: 500;\n    table-layout: fixed;\n\n    thead,\n    td {\n      border: 1px solid ${(props) => props.theme.table.border};\n    }\n\n    thead {\n      color: ${(props) => props.theme.table.thead.color};\n      font-size: ${(props) => props.theme.font.size.base};\n      user-select: none;\n    }\n    td {\n      padding: 6px 10px;\n\n      &:nth-child(1) {\n        width: 30%;\n      }\n\n      &:nth-child(3) {\n        width: 70px;\n      }\n    }\n  }\n\n  .btn-add-var {\n    font-size: ${(props) => props.theme.font.size.base};\n  }\n\n  input[type='text'] {\n    width: 100%;\n    border: solid 1px transparent;\n    outline: none !important;\n    background-color: inherit;\n\n    &:focus {\n      outline: none !important;\n      border: solid 1px transparent;\n    }\n  }\n\n  input[type='checkbox'] {\n    cursor: pointer;\n    position: relative;\n    top: 1px;\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/FolderSettings/Vars/VarsTable/index.js",
    "content": "import React, { useCallback } from 'react';\nimport { useDispatch } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';\nimport MultiLineEditor from 'components/MultiLineEditor';\nimport InfoTip from 'components/InfoTip';\nimport EditableTable from 'components/EditableTable';\nimport StyledWrapper from './StyledWrapper';\nimport toast from 'react-hot-toast';\nimport { variableNameRegex } from 'utils/common/regex';\nimport { setFolderVars } from 'providers/ReduxStore/slices/collections/index';\n\nconst VarsTable = ({ folder, collection, vars, varType }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n\n  const onSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));\n\n  const handleVarsChange = useCallback((updatedVars) => {\n    dispatch(setFolderVars({\n      collectionUid: collection.uid,\n      folderUid: folder.uid,\n      vars: updatedVars,\n      type: varType\n    }));\n  }, [dispatch, collection.uid, folder.uid, varType]);\n\n  const getRowError = useCallback((row, index, key) => {\n    if (key !== 'name') return null;\n    if (!row.name || row.name.trim() === '') return null;\n    if (!variableNameRegex.test(row.name)) {\n      return 'Variable contains invalid characters. Must only contain alphanumeric characters, \"-\", \"_\", \".\"';\n    }\n    return null;\n  }, []);\n\n  const columns = [\n    {\n      key: 'name',\n      name: 'Name',\n      isKeyField: true,\n      placeholder: 'Name',\n      width: '40%'\n    },\n    {\n      key: 'value',\n      name: varType === 'request' ? 'Value' : (\n        <div className=\"flex items-center\">\n          <span>Expr</span>\n          <InfoTip content=\"You can write any valid JS expression here\" infotipId={`folder-${varType}-var`} />\n        </div>\n      ),\n      placeholder: varType === 'request' ? 'Value' : 'Expr',\n      render: ({ value, onChange }) => (\n        <MultiLineEditor\n          value={value || ''}\n          theme={storedTheme}\n          onSave={onSave}\n          onChange={onChange}\n          collection={collection}\n          item={folder}\n          placeholder={!value ? (varType === 'request' ? 'Value' : 'Expr') : ''}\n        />\n      )\n    }\n  ];\n\n  const defaultRow = {\n    name: '',\n    value: '',\n    ...(varType === 'response' ? { local: false } : {})\n  };\n\n  return (\n    <StyledWrapper className=\"w-full\">\n      <EditableTable\n        columns={columns}\n        rows={vars}\n        onChange={handleVarsChange}\n        defaultRow={defaultRow}\n        getRowError={getRowError}\n      />\n    </StyledWrapper>\n  );\n};\n\nexport default VarsTable;\n"
  },
  {
    "path": "packages/bruno-app/src/components/FolderSettings/Vars/index.js",
    "content": "import React from 'react';\nimport get from 'lodash/get';\nimport VarsTable from './VarsTable';\nimport StyledWrapper from './StyledWrapper';\nimport { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';\nimport { useDispatch } from 'react-redux';\nimport Button from 'ui/Button';\n\nconst Vars = ({ collection, folder }) => {\n  const dispatch = useDispatch();\n  const requestVars = folder.draft ? get(folder, 'draft.request.vars.req', []) : get(folder, 'root.request.vars.req', []);\n  const responseVars = folder.draft ? get(folder, 'draft.request.vars.res', []) : get(folder, 'root.request.vars.res', []);\n  const handleSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));\n\n  return (\n    <StyledWrapper className=\"w-full flex flex-col\">\n      <div>\n        <div className=\"mb-3 title text-xs\">Pre Request</div>\n        <VarsTable folder={folder} collection={collection} vars={requestVars} varType=\"request\" />\n      </div>\n      <div>\n        <div className=\"mt-3 mb-3 title text-xs\">Post Response</div>\n        <VarsTable folder={folder} collection={collection} vars={responseVars} varType=\"response\" />\n      </div>\n      <div className=\"mt-6\">\n        <Button type=\"submit\" size=\"sm\" onClick={handleSave}>\n          Save\n        </Button>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Vars;\n"
  },
  {
    "path": "packages/bruno-app/src/components/FolderSettings/index.js",
    "content": "import React from 'react';\nimport classnames from 'classnames';\nimport { updatedFolderSettingsSelectedTab } from 'providers/ReduxStore/slices/collections';\nimport { useDispatch } from 'react-redux';\nimport Headers from './Headers';\nimport Script from './Script';\nimport Tests from './Tests';\nimport StyledWrapper from './StyledWrapper';\nimport Vars from './Vars';\nimport Documentation from './Documentation';\nimport Auth from './Auth';\nimport StatusDot from 'components/StatusDot';\nimport get from 'lodash/get';\n\nconst FolderSettings = ({ collection, folder }) => {\n  const dispatch = useDispatch();\n  let tab = 'headers';\n  const { folderLevelSettingsSelectedTab } = collection;\n  if (folderLevelSettingsSelectedTab?.[folder?.uid]) {\n    tab = folderLevelSettingsSelectedTab[folder?.uid];\n  }\n\n  const folderRoot = folder?.draft || folder?.root;\n  const hasScripts = folderRoot?.request?.script?.res || folderRoot?.request?.script?.req;\n  const hasTests = folderRoot?.request?.tests;\n\n  const headers = folderRoot?.request?.headers || [];\n  const activeHeadersCount = headers.filter((header) => header.enabled).length;\n\n  const requestVars = folderRoot?.request?.vars?.req || [];\n  const responseVars = folderRoot?.request?.vars?.res || [];\n  const activeVarsCount = requestVars.filter((v) => v.enabled).length + responseVars.filter((v) => v.enabled).length;\n\n  const auth = get(folderRoot, 'request.auth.mode');\n  const hasAuth = auth && auth !== 'none';\n\n  const setTab = (tab) => {\n    dispatch(\n      updatedFolderSettingsSelectedTab({\n        collectionUid: collection?.uid,\n        folderUid: folder?.uid,\n        tab\n      })\n    );\n  };\n\n  const getTabPanel = (tab) => {\n    switch (tab) {\n      case 'headers': {\n        return <Headers collection={collection} folder={folder} />;\n      }\n      case 'script': {\n        return <Script collection={collection} folder={folder} />;\n      }\n      case 'test': {\n        return <Tests collection={collection} folder={folder} />;\n      }\n      case 'vars': {\n        return <Vars collection={collection} folder={folder} />;\n      }\n      case 'auth': {\n        return <Auth collection={collection} folder={folder} />;\n      }\n      case 'docs': {\n        return <Documentation collection={collection} folder={folder} />;\n      }\n    }\n  };\n\n  const getTabClassname = (tabName) => {\n    return classnames(`tab select-none ${tabName}`, {\n      active: tabName === tab\n    });\n  };\n\n  return (\n    <StyledWrapper className=\"flex flex-col h-full overflow-auto\">\n      <div className=\"flex flex-col h-full relative px-4 py-4\">\n        <div className=\"flex flex-wrap items-center tabs\" role=\"tablist\">\n          <div className={getTabClassname('headers')} role=\"tab\" onClick={() => setTab('headers')}>\n            Headers\n            {activeHeadersCount > 0 && <sup className=\"ml-1 font-medium\">{activeHeadersCount}</sup>}\n          </div>\n          <div className={getTabClassname('script')} role=\"tab\" onClick={() => setTab('script')}>\n            Script\n            {hasScripts && <StatusDot />}\n          </div>\n          <div className={getTabClassname('test')} role=\"tab\" onClick={() => setTab('test')}>\n            Test\n            {hasTests && <StatusDot />}\n          </div>\n          <div className={getTabClassname('vars')} role=\"tab\" onClick={() => setTab('vars')}>\n            Vars\n            {activeVarsCount > 0 && <sup className=\"ml-1 font-medium\">{activeVarsCount}</sup>}\n          </div>\n          <div className={getTabClassname('auth')} role=\"tab\" onClick={() => setTab('auth')}>\n            Auth\n            {hasAuth && <StatusDot />}\n          </div>\n          <div className={getTabClassname('docs')} role=\"tab\" onClick={() => setTab('docs')}>\n            Docs\n          </div>\n        </div>\n        <section className=\"flex mt-4 h-full overflow-auto\">{getTabPanel(tab)}</section>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default FolderSettings;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Git/GitNotFoundModal/index.js",
    "content": "import React from 'react';\nimport Modal from 'components/Modal/index';\nimport Portal from 'components/Portal/index';\n\nconst getOSName = () => {\n  const platform = window.navigator.userAgentData?.platform || '';\n  if (platform.startsWith('Win')) {\n    return 'Windows';\n  } else if (platform.startsWith('Mac')) {\n    return 'macOS';\n  } else if (platform.startsWith('Linux')) {\n    return 'Linux';\n  } else {\n    return 'your OS';\n  }\n};\n\nconst getDownloadUrl = (os) => {\n  switch (os) {\n    case 'Windows':\n      return 'https://git-scm.com/download/win';\n    case 'macOS':\n      return 'https://git-scm.com/download/mac';\n    case 'Linux':\n      return 'https://git-scm.com/download/linux';\n    default:\n      return 'https://git-scm.com/download';\n  }\n};\n\nconst GitNotFoundModal = ({ onClose }) => {\n  const osName = getOSName();\n  const downloadUrl = getDownloadUrl(osName);\n\n  return (\n    <Portal>\n      <Modal\n        size=\"sm\"\n        title=\"Git Not Found\"\n        handleCancel={onClose}\n        hideFooter={true}\n      >\n        <div>\n          <p>Git was not detected on your system. You need to install Git to proceed.</p>\n          <p className=\"mt-2\">\n            You can download Git for <strong>{osName}</strong> here:\n          </p>\n          <p>\n            <span\n              className=\"text-blue-600 cursor-pointer border-b border-blue-600\"\n              onClick={() => window.open(downloadUrl, '_blank')}\n            >\n              Download Git for {osName}\n            </span>\n          </p>\n        </div>\n      </Modal>\n    </Portal>\n  );\n};\n\nexport default GitNotFoundModal;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Git/VisualDiffViewer/CollapsibleDiffRow.js",
    "content": "import React from 'react';\nimport { IconChevronDown, IconChevronRight } from '@tabler/icons';\n\nconst CollapsibleDiffRow = ({ title, isCollapsed, onToggle, oldContent, newContent, hasOldContent, hasNewContent }) => {\n  if (!hasOldContent && !hasNewContent) {\n    return null;\n  }\n\n  return (\n    <div className=\"diff-row\">\n      <div className=\"diff-row-header\" onClick={onToggle}>\n        <span className=\"collapse-toggle\">\n          {isCollapsed ? (\n            <IconChevronRight size={14} strokeWidth={2} />\n          ) : (\n            <IconChevronDown size={14} strokeWidth={2} />\n          )}\n        </span>\n        <span className=\"diff-row-title\">{title}</span>\n      </div>\n      {!isCollapsed && (\n        <div className=\"diff-row-content\">\n          <div className=\"diff-row-pane old\">\n            {hasOldContent ? oldContent : <div className=\"empty-placeholder\" />}\n          </div>\n          <div className=\"diff-row-pane new\">\n            {hasNewContent ? newContent : <div className=\"empty-placeholder\" />}\n          </div>\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default CollapsibleDiffRow;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Git/VisualDiffViewer/VisualDiffAuth.js",
    "content": "import React, { useMemo } from 'react';\nimport get from 'lodash/get';\nimport isEqual from 'lodash/isEqual';\n\nconst AUTH_TYPE_LABELS = {\n  awsv4: 'AWS Signature v4',\n  basic: 'Basic Auth',\n  bearer: 'Bearer Token',\n  digest: 'Digest Auth',\n  ntlm: 'NTLM',\n  oauth2: 'OAuth 2.0',\n  wsse: 'WSSE',\n  apikey: 'API Key'\n};\n\nconst AUTH_FIELD_LABELS = {\n  // AWS v4\n  accessKeyId: 'Access Key ID',\n  secretAccessKey: 'Secret Access Key',\n  sessionToken: 'Session Token',\n  service: 'Service',\n  region: 'Region',\n  profileName: 'Profile Name',\n  // Basic/Digest/NTLM/WSSE\n  username: 'Username',\n  password: 'Password',\n  domain: 'Domain',\n  // Bearer\n  token: 'Token',\n  // API Key\n  key: 'Key',\n  value: 'Value',\n  placement: 'Placement',\n  // OAuth2\n  grantType: 'Grant Type',\n  callbackUrl: 'Callback URL',\n  authorizationUrl: 'Authorization URL',\n  accessTokenUrl: 'Access Token URL',\n  refreshTokenUrl: 'Refresh Token URL',\n  clientId: 'Client ID',\n  clientSecret: 'Client Secret',\n  scope: 'Scope',\n  state: 'State',\n  pkce: 'PKCE',\n  credentialsPlacement: 'Credentials Placement',\n  credentialsId: 'Credentials ID',\n  tokenPlacement: 'Token Placement',\n  tokenHeaderPrefix: 'Token Header Prefix',\n  tokenQueryKey: 'Token Query Key',\n  autoFetchToken: 'Auto Fetch Token',\n  autoRefreshToken: 'Auto Refresh Token'\n};\n\nconst VisualDiffAuth = ({ oldData, newData, showSide }) => {\n  const oldAuth = get(oldData, 'request.auth', {});\n  const newAuth = get(newData, 'request.auth', {});\n\n  const currentAuth = showSide === 'old' ? oldAuth : newAuth;\n  const otherAuth = showSide === 'old' ? newAuth : oldAuth;\n\n  const authTypes = useMemo(() => {\n    const types = new Set([...Object.keys(currentAuth), ...Object.keys(otherAuth)]);\n    types.delete('mode');\n    return Array.from(types);\n  }, [currentAuth, otherAuth]);\n\n  const authSections = useMemo(() => {\n    return authTypes.map((authType) => {\n      const rawCurrentConfig = currentAuth[authType];\n      const rawOtherConfig = otherAuth[authType];\n      const currentConfig = (typeof rawCurrentConfig === 'object' && rawCurrentConfig !== null) ? rawCurrentConfig : {};\n      const otherConfig = (typeof rawOtherConfig === 'object' && rawOtherConfig !== null) ? rawOtherConfig : {};\n\n      if (Object.keys(currentConfig).length === 0 && showSide === 'old') {\n        return null;\n      }\n      if (Object.keys(currentConfig).length === 0 && showSide === 'new') {\n        return null;\n      }\n\n      let sectionStatus = 'unchanged';\n      if (Object.keys(otherConfig).length === 0) {\n        sectionStatus = showSide === 'old' ? 'deleted' : 'added';\n      } else if (!isEqual(currentConfig, otherConfig)) {\n        sectionStatus = 'modified';\n      }\n\n      const allFields = new Set([...Object.keys(currentConfig), ...Object.keys(otherConfig)]);\n      const fields = Array.from(allFields).map((field) => {\n        const currentValue = currentConfig[field];\n        const otherValue = otherConfig[field];\n\n        let status = 'unchanged';\n        if (otherValue === undefined) {\n          status = showSide === 'old' ? 'deleted' : 'added';\n        } else if (currentValue !== otherValue) {\n          status = 'modified';\n        }\n\n        let displayValue = currentValue;\n        if (typeof displayValue === 'boolean') {\n          displayValue = displayValue ? 'true' : 'false';\n        } else if (displayValue === undefined || displayValue === null) {\n          displayValue = '';\n        }\n\n        return {\n          key: AUTH_FIELD_LABELS[field] || field,\n          value: String(displayValue),\n          status\n        };\n      });\n\n      return {\n        type: authType,\n        label: AUTH_TYPE_LABELS[authType] || authType,\n        status: sectionStatus,\n        fields\n      };\n    }).filter(Boolean);\n  }, [authTypes, currentAuth, otherAuth, showSide]);\n\n  const currentMode = currentAuth.mode;\n  const otherMode = otherAuth.mode;\n  const modeStatus = currentMode !== otherMode ? (otherMode === undefined ? (showSide === 'old' ? 'deleted' : 'added') : 'modified') : 'unchanged';\n\n  if (authSections.length === 0 && !currentMode) {\n    return null;\n  }\n\n  return (\n    <>\n      {currentMode && (\n        <div className=\"diff-section\">\n          <table className=\"diff-table\">\n            <thead>\n              <tr>\n                <th style={{ width: '30px' }}></th>\n                <th style={{ width: '40%' }}>Field</th>\n                <th>Value</th>\n              </tr>\n            </thead>\n            <tbody>\n              <tr className={modeStatus}>\n                <td>\n                  {modeStatus !== 'unchanged' && (\n                    <span className={`status-badge ${modeStatus}`}>\n                      {modeStatus === 'added' ? 'A' : modeStatus === 'deleted' ? 'D' : 'M'}\n                    </span>\n                  )}\n                </td>\n                <td className=\"key-cell\">Auth Mode</td>\n                <td className=\"value-cell\">{AUTH_TYPE_LABELS[currentMode] || currentMode}</td>\n              </tr>\n            </tbody>\n          </table>\n        </div>\n      )}\n      {authSections.map((section) => (\n        <div key={section.type} className=\"diff-section\">\n          <div className=\"diff-section-header\">\n            <span>{section.label}</span>\n            {section.status !== 'unchanged' && (\n              <span className={`status-badge ${section.status}`}>\n                {section.status === 'added' ? 'A' : section.status === 'deleted' ? 'D' : 'M'}\n              </span>\n            )}\n          </div>\n          <table className=\"diff-table\">\n            <thead>\n              <tr>\n                <th style={{ width: '30px' }}></th>\n                <th style={{ width: '40%' }}>Field</th>\n                <th>Value</th>\n              </tr>\n            </thead>\n            <tbody>\n              {section.fields.map((field, index) => (\n                <tr key={index} className={field.status}>\n                  <td>\n                    {field.status !== 'unchanged' && (\n                      <span className={`status-badge ${field.status}`}>\n                        {field.status === 'added' ? 'A' : field.status === 'deleted' ? 'D' : 'M'}\n                      </span>\n                    )}\n                  </td>\n                  <td className=\"key-cell\">{field.key}</td>\n                  <td className=\"value-cell\">{field.value}</td>\n                </tr>\n              ))}\n            </tbody>\n          </table>\n        </div>\n      ))}\n    </>\n  );\n};\n\nexport default VisualDiffAuth;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Git/VisualDiffViewer/VisualDiffBody.js",
    "content": "import React, { useMemo } from 'react';\nimport get from 'lodash/get';\nimport isEqual from 'lodash/isEqual';\nimport { computeLineDiffForOld, computeLineDiffForNew } from './utils/diffUtils';\n\nconst BODY_TYPE_LABELS = {\n  json: 'JSON',\n  text: 'Text',\n  xml: 'XML',\n  sparql: 'SPARQL',\n  graphql: 'GraphQL',\n  formUrlEncoded: 'Form URL Encoded',\n  multipartForm: 'Multipart Form',\n  file: 'File',\n  grpc: 'gRPC',\n  ws: 'WebSocket'\n};\n\nconst TEXT_BODY_TYPES = ['json', 'text', 'xml', 'sparql'];\nconst FORM_BODY_TYPES = ['formUrlEncoded', 'multipartForm'];\nconst ALL_BODY_TYPES = Object.keys(BODY_TYPE_LABELS);\n\nconst VisualDiffBody = ({ oldData, newData, showSide }) => {\n  const oldBody = get(oldData, 'request.body', {});\n  const newBody = get(newData, 'request.body', {});\n\n  const currentBody = showSide === 'old' ? oldBody : newBody;\n  const otherBody = showSide === 'old' ? newBody : oldBody;\n\n  const bodyTypes = useMemo(() => {\n    const currentMode = currentBody.mode;\n    const otherMode = otherBody.mode;\n\n    // Collect body types that match either side's active mode\n    const relevantTypes = new Set();\n    if (currentMode && currentMode !== 'none') {\n      relevantTypes.add(currentMode);\n    }\n    if (otherMode && otherMode !== 'none') {\n      relevantTypes.add(otherMode);\n    }\n\n    // If neither side has a mode (legacy data), fall back to showing all defined types\n    if (relevantTypes.size === 0) {\n      return ALL_BODY_TYPES.filter((type) => {\n        const currentVal = currentBody[type];\n        const otherVal = otherBody[type];\n        return currentVal !== undefined || otherVal !== undefined;\n      });\n    }\n\n    // Only show body types that match the active mode on either side\n    return ALL_BODY_TYPES.filter((type) => {\n      if (!relevantTypes.has(type)) return false;\n      const currentVal = currentBody[type];\n      const otherVal = otherBody[type];\n      return currentVal !== undefined || otherVal !== undefined;\n    });\n  }, [currentBody, otherBody]);\n\n  const renderLineDiff = (segments) => {\n    return segments.map((segment, index) => (\n      <div key={index} className={`diff-line ${segment.status}`}>\n        {segment.text || '\\u00A0'}\n      </div>\n    ));\n  };\n\n  const renderFormData = (items, otherItems) => {\n    if (!items || items.length === 0) return null;\n\n    const otherItemMap = new Map();\n    (otherItems || []).forEach((item) => {\n      otherItemMap.set(item.name, item);\n    });\n\n    return (\n      <table className=\"diff-table\">\n        <thead>\n          <tr>\n            <th style={{ width: '30px' }}></th>\n            <th className=\"checkbox-cell\"></th>\n            <th style={{ width: '40%' }}>Key</th>\n            <th>Value</th>\n          </tr>\n        </thead>\n        <tbody>\n          {items.map((item, index) => {\n            const otherItem = otherItemMap.get(item.name);\n            let status = 'unchanged';\n            if (!otherItem) {\n              status = showSide === 'old' ? 'deleted' : 'added';\n            } else if (item.value !== otherItem.value || item.enabled !== otherItem.enabled) {\n              status = 'modified';\n            }\n\n            return (\n              <tr key={`${item.name}-${index}`} className={status}>\n                <td>\n                  {status !== 'unchanged' && (\n                    <span className={`status-badge ${status}`}>\n                      {status === 'added' ? 'A' : status === 'deleted' ? 'D' : 'M'}\n                    </span>\n                  )}\n                </td>\n                <td className=\"checkbox-cell\">\n                  <input\n                    type=\"checkbox\"\n                    checked={item.enabled !== false}\n                    readOnly\n                    disabled\n                  />\n                </td>\n                <td className=\"key-cell\">{item.name}</td>\n                <td className=\"value-cell\">{item.value}</td>\n              </tr>\n            );\n          })}\n        </tbody>\n      </table>\n    );\n  };\n\n  const renderFileBody = (files, otherFiles) => {\n    if (!files || files.length === 0) return null;\n\n    const otherFileMap = new Map();\n    (otherFiles || []).forEach((f, idx) => {\n      otherFileMap.set(f.filePath || idx, f);\n    });\n\n    return (\n      <table className=\"diff-table\">\n        <thead>\n          <tr>\n            <th style={{ width: '30px' }}></th>\n            <th className=\"checkbox-cell\"></th>\n            <th>File Path</th>\n            <th style={{ width: '100px' }}>Content Type</th>\n          </tr>\n        </thead>\n        <tbody>\n          {files.map((file, index) => {\n            const otherFile = otherFileMap.get(file.filePath || index);\n            let status = 'unchanged';\n            if (!otherFile) {\n              status = showSide === 'old' ? 'deleted' : 'added';\n            } else if (file.filePath !== otherFile.filePath || file.contentType !== otherFile.contentType) {\n              status = 'modified';\n            }\n\n            return (\n              <tr key={index} className={status}>\n                <td>\n                  {status !== 'unchanged' && (\n                    <span className={`status-badge ${status}`}>\n                      {status === 'added' ? 'A' : status === 'deleted' ? 'D' : 'M'}\n                    </span>\n                  )}\n                </td>\n                <td className=\"checkbox-cell\">\n                  <input type=\"checkbox\" checked={file.selected !== false} readOnly disabled />\n                </td>\n                <td className=\"value-cell\">{file.filePath}</td>\n                <td className=\"value-cell\">{file.contentType || '-'}</td>\n              </tr>\n            );\n          })}\n        </tbody>\n      </table>\n    );\n  };\n\n  const renderMessageBody = (messages, otherMessages, typeLabel) => {\n    if (!messages || messages.length === 0) return null;\n\n    return messages.map((msg, index) => {\n      const otherMsg = (otherMessages || [])[index];\n      const contentDiff = showSide === 'old'\n        ? computeLineDiffForOld(msg.content || '', otherMsg?.content || '')\n        : computeLineDiffForNew(otherMsg?.content || '', msg.content || '');\n\n      let msgStatus = 'unchanged';\n      if (!otherMsg) {\n        msgStatus = showSide === 'old' ? 'deleted' : 'added';\n      } else if (msg.name !== otherMsg.name || msg.type !== otherMsg.type) {\n        msgStatus = 'modified';\n      }\n\n      return (\n        <div key={index}>\n          <div className=\"diff-section-header\">\n            <span>{typeLabel}: {msg.name || `Message ${index + 1}`}{msg.type ? ` (${msg.type})` : ''}</span>\n            {msgStatus !== 'unchanged' && (\n              <span className={`status-badge ${msgStatus}`}>\n                {msgStatus === 'added' ? 'A' : msgStatus === 'deleted' ? 'D' : 'M'}\n              </span>\n            )}\n          </div>\n          <div className=\"code-diff-content\">{renderLineDiff(contentDiff)}</div>\n        </div>\n      );\n    });\n  };\n\n  const renderGraphqlBody = (graphql, otherGraphql) => {\n    const currentQuery = graphql?.query || '';\n    const otherQuery = otherGraphql?.query || '';\n    const currentVariables = graphql?.variables || '';\n    const otherVariables = otherGraphql?.variables || '';\n\n    const queryDiff = showSide === 'old'\n      ? computeLineDiffForOld(currentQuery, otherQuery)\n      : computeLineDiffForNew(otherQuery, currentQuery);\n\n    const variablesDiff = showSide === 'old'\n      ? computeLineDiffForOld(currentVariables, otherVariables)\n      : computeLineDiffForNew(otherVariables, currentVariables);\n\n    return (\n      <>\n        {(currentQuery || otherQuery) && (\n          <div>\n            <div className=\"diff-section-header\">Query</div>\n            <div className=\"code-diff-content\">{renderLineDiff(queryDiff)}</div>\n          </div>\n        )}\n        {(currentVariables || otherVariables) && (\n          <div>\n            <div className=\"diff-section-header\">Variables</div>\n            <div className=\"code-diff-content\">{renderLineDiff(variablesDiff)}</div>\n          </div>\n        )}\n      </>\n    );\n  };\n\n  const renderTextBody = (currentContent, otherContent) => {\n    const diffSegments = showSide === 'old'\n      ? computeLineDiffForOld(currentContent || '', otherContent || '')\n      : computeLineDiffForNew(otherContent || '', currentContent || '');\n\n    return (\n      <div className=\"code-diff-content\">\n        {renderLineDiff(diffSegments)}\n      </div>\n    );\n  };\n\n  const renderBodyType = (type) => {\n    const currentVal = currentBody[type];\n    const otherVal = otherBody[type];\n\n    if (currentVal === undefined && otherVal === undefined) return null;\n\n    // For text-based body types\n    if (TEXT_BODY_TYPES.includes(type)) {\n      if (!currentVal) return null;\n      return renderTextBody(currentVal, otherVal);\n    }\n\n    // For form data types\n    if (FORM_BODY_TYPES.includes(type)) {\n      return renderFormData(currentVal, otherVal);\n    }\n\n    // GraphQL\n    if (type === 'graphql') {\n      return renderGraphqlBody(currentVal, otherVal);\n    }\n\n    // File\n    if (type === 'file') {\n      return renderFileBody(currentVal, otherVal);\n    }\n\n    // gRPC\n    if (type === 'grpc') {\n      return renderMessageBody(currentVal, otherVal, 'gRPC');\n    }\n\n    // WebSocket\n    if (type === 'ws') {\n      return renderMessageBody(currentVal, otherVal, 'WebSocket');\n    }\n\n    return null;\n  };\n\n  // Show body mode if present\n  const currentMode = currentBody.mode;\n  const otherMode = otherBody.mode;\n  const modeStatus = currentMode !== otherMode ? (otherMode === undefined ? (showSide === 'old' ? 'deleted' : 'added') : 'modified') : 'unchanged';\n\n  if (bodyTypes.length === 0 && !currentMode) {\n    return null;\n  }\n\n  return (\n    <>\n      {currentMode && (\n        <div className=\"diff-section\">\n          <table className=\"diff-table\">\n            <thead>\n              <tr>\n                <th style={{ width: '30px' }}></th>\n                <th style={{ width: '40%' }}>Field</th>\n                <th>Value</th>\n              </tr>\n            </thead>\n            <tbody>\n              <tr className={modeStatus}>\n                <td>\n                  {modeStatus !== 'unchanged' && (\n                    <span className={`status-badge ${modeStatus}`}>\n                      {modeStatus === 'added' ? 'A' : modeStatus === 'deleted' ? 'D' : 'M'}\n                    </span>\n                  )}\n                </td>\n                <td className=\"key-cell\">Body Mode</td>\n                <td className=\"value-cell\">{BODY_TYPE_LABELS[currentMode] || currentMode}</td>\n              </tr>\n            </tbody>\n          </table>\n        </div>\n      )}\n      {bodyTypes.map((type) => {\n        const content = renderBodyType(type);\n        if (!content) return null;\n\n        const currentVal = currentBody[type];\n        const otherVal = otherBody[type];\n        const hasChanges = !isEqual(currentVal, otherVal);\n\n        return (\n          <div key={type} className=\"diff-section\">\n            <div className=\"diff-section-header\">\n              <span>{BODY_TYPE_LABELS[type] || type}</span>\n              {hasChanges && (\n                <span className={`status-badge ${otherVal === undefined ? (showSide === 'old' ? 'deleted' : 'added') : 'modified'}`}>\n                  {otherVal === undefined ? (showSide === 'old' ? 'D' : 'A') : 'M'}\n                </span>\n              )}\n            </div>\n            {content}\n          </div>\n        );\n      })}\n    </>\n  );\n};\n\nexport default VisualDiffBody;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Git/VisualDiffViewer/VisualDiffContent/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n\n  .visual-diff-content {\n    flex: 1;\n    overflow: auto;\n  }\n\n  .diff-header-row {\n    display: flex;\n    border: 1px solid ${(props) => props.theme.border.border1};\n    border-radius: ${(props) => props.theme.border.radius.base};\n    margin-bottom: 1rem;\n  }\n\n  .diff-header-pane {\n     flex: 1;\n      padding: 0.5rem 0.75rem;\n      font-size: ${(props) => props.theme.font.size.xs};\n      font-weight: 600;\n      color: ${(props) => props.theme.colors.text.muted};\n      text-transform: uppercase;\n\n      &.old {\n        border-right: 1px solid ${(props) => props.theme.border.border1};\n      }\n  }\n\n  .diff-sections {\n    display: flex;\n    flex-direction: column;\n    gap: 0.5rem;\n  }\n\n  .diff-row {\n    border: 1px solid ${(props) => props.theme.border.border1};\n    border-radius: ${(props) => props.theme.border.radius.base};\n    overflow: hidden;\n  }\n\n  .diff-row-header {\n    display: flex;\n    align-items: center;\n    gap: 0.5rem;\n    padding: 0.5rem 0.75rem;\n    background: ${(props) => props.theme.sidebar.bg};\n    cursor: pointer;\n    user-select: none;\n    border-bottom: 1px solid ${(props) => props.theme.border.border1};\n\n    &:hover {\n      background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n    }\n  }\n\n  .collapse-toggle {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .diff-row-title {\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-weight: 500;\n    color: ${(props) => props.theme.text};\n  }\n\n  .diff-row-content {\n    display: flex;\n    gap: 1rem;\n    padding: 0.75rem;\n    background: ${(props) => props.theme.background.base};\n  }\n\n  .diff-row-pane {\n    flex: 1;\n    min-width: 0;\n    display: flex;\n    flex-direction: column;\n    gap: 0.5rem;\n\n    &.old {\n      border-left: 2px solid ${(props) => props.theme.colors.text.danger}20;\n      padding-left: 0.5rem;\n    }\n\n    &.new {\n      border-left: 2px solid ${(props) => props.theme.colors.text.green}20;\n      padding-left: 0.5rem;\n    }\n  }\n\n  .empty-placeholder {\n    flex: 1;\n    min-height: 40px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    background: ${(props) => props.theme.sidebar.bg};\n    border: 1px dashed ${(props) => props.theme.border.border1};\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    color: ${(props) => props.theme.colors.text.muted};\n    font-size: ${(props) => props.theme.font.size.xs};\n  }\n\n  .empty-placeholder::after {\n    content: 'No content';\n  }\n\n  .diff-section {\n    border: 1px solid ${(props) => props.theme.border.border1};\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    overflow: hidden;\n\n    &.added {\n      border-color: ${(props) => props.theme.colors.text.green};\n    }\n\n    &.deleted {\n      border-color: ${(props) => props.theme.colors.text.danger};\n    }\n  }\n\n  .diff-section-header {\n    padding: 0.375rem 0.5rem;\n    font-size: ${(props) => props.theme.font.size.xs};\n    font-weight: 500;\n    background: ${(props) => props.theme.sidebar.bg};\n    border-bottom: 1px solid ${(props) => props.theme.border.border1};\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n  }\n\n  .diff-section-content {\n    padding: 0.5rem;\n  }\n\n  .url-bar {\n    display: flex;\n    align-items: center;\n    gap: 0.5rem;\n    padding: 0.375rem 0.5rem;\n\n    .method {\n      font-weight: 600;\n      font-size: ${(props) => props.theme.font.size.xs};\n      text-transform: uppercase;\n      padding: 0.125rem 0.375rem;\n      border-radius: ${(props) => props.theme.border.radius.sm};\n      background: ${(props) => props.theme.brand}15;\n      color: ${(props) => props.theme.brand};\n    }\n\n    .url {\n      flex: 1;\n      font-family: 'Fira Code', monospace;\n      font-size: ${(props) => props.theme.font.size.xs};\n      color: ${(props) => props.theme.text};\n      word-break: break-all;\n\n      &.changed {\n        background: color-mix(in srgb, ${(props) => props.theme.colors.text.warning} 20%, transparent);\n        padding: 0.125rem 0.25rem;\n        border-radius: ${(props) => props.theme.border.radius.sm};\n      }\n    }\n\n    .method.changed {\n      background: color-mix(in srgb, ${(props) => props.theme.colors.text.warning} 30%, transparent);\n      color: ${(props) => props.theme.colors.text.warning};\n    }\n  }\n\n  .diff-inline {\n    padding: 0.125rem 0.25rem;\n    border-radius: 2px;\n\n    &.added {\n      background: color-mix(in srgb, ${(props) => props.theme.colors.text.green} 25%, transparent);\n      color: ${(props) => props.theme.colors.text.green};\n    }\n\n    &.deleted {\n      background: color-mix(in srgb, ${(props) => props.theme.colors.text.danger} 25%, transparent);\n      color: ${(props) => props.theme.colors.text.danger};\n      text-decoration: line-through;\n    }\n\n    &.modified {\n      background: color-mix(in srgb, ${(props) => props.theme.colors.text.warning} 25%, transparent);\n      color: ${(props) => props.theme.colors.text.warning};\n    }\n  }\n\n\n\n  .diff-table {\n    width: 100%;\n    border-collapse: collapse;\n    font-size: ${(props) => props.theme.font.size.xs};\n\n    th, td {\n      padding: 0.375rem 0.5rem;\n      text-align: left;\n      border-bottom: 1px solid ${(props) => props.theme.border.border1};\n    }\n\n    th {\n      font-weight: 500;\n      color: ${(props) => props.theme.colors.text.muted};\n      background: ${(props) => props.theme.sidebar.bg};\n    }\n\n    tr.added {\n      background: color-mix(in srgb, ${(props) => props.theme.colors.text.green} 10%, transparent);\n    }\n\n    tr.deleted {\n      background: color-mix(in srgb, ${(props) => props.theme.colors.text.danger} 10%, transparent);\n    }\n\n    tr.modified {\n      background: color-mix(in srgb, ${(props) => props.theme.colors.text.warning} 10%, transparent);\n    }\n\n    .checkbox-cell {\n      width: 24px;\n      text-align: center;\n\n      input[type='checkbox'] {\n        cursor: default;\n        width: 12px;\n        height: 12px;\n        accent-color: ${(props) => props.theme.colors.accent};\n        vertical-align: middle;\n        margin: 0;\n      }\n    }\n\n    .key-cell {\n      font-family: 'Fira Code', monospace;\n      color: ${(props) => props.theme.text};\n    }\n\n    .value-cell {\n      font-family: 'Fira Code', monospace;\n      color: ${(props) => props.theme.colors.text.muted};\n      word-break: break-all;\n    }\n\n    .status-badge {\n      display: inline-flex;\n      align-items: center;\n      justify-content: center;\n      width: 0.875rem;\n      height: 0.875rem;\n      border-radius: 2px;\n      font-size: 8px;\n      font-weight: 600;\n\n      &.added {\n        background: color-mix(in srgb, ${(props) => props.theme.colors.text.green} 13%, transparent);\n        color: ${(props) => props.theme.colors.text.green};\n      }\n\n      &.deleted {\n        background: color-mix(in srgb, ${(props) => props.theme.colors.text.danger} 13%, transparent);\n        color: ${(props) => props.theme.colors.text.danger};\n      }\n\n      &.modified {\n        background: color-mix(in srgb, ${(props) => props.theme.colors.text.warning} 13%, transparent);\n        color: ${(props) => props.theme.colors.text.warning};\n      }\n    }\n  }\n\n  .code-diff-content {\n    max-height: 250px;\n    overflow: auto;\n    font-family: 'Fira Code', monospace;\n    font-size: ${(props) => props.theme.font.size.xs};\n    line-height: 1.5;\n\n    .diff-line {\n      padding: 0 0.5rem;\n      white-space: pre-wrap;\n      word-break: break-word;\n\n      &.unchanged {\n        color: ${(props) => props.theme.text};\n      }\n\n      &.added {\n        background: color-mix(in srgb, ${(props) => props.theme.colors.text.green} 15%, transparent);\n        color: ${(props) => props.theme.colors.text.green};\n      }\n\n      &.deleted {\n        background: color-mix(in srgb, ${(props) => props.theme.colors.text.danger} 15%, transparent);\n        color: ${(props) => props.theme.colors.text.danger};\n        text-decoration: line-through;\n      }\n    }\n  }\n\n  .example-content {\n    padding: 0.5rem;\n  }\n\n  .example-block {\n    margin-bottom: 0.5rem;\n\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n\n  .example-block-header {\n    font-size: ${(props) => props.theme.font.size.xs};\n    font-weight: 600;\n    color: ${(props) => props.theme.colors.text.muted};\n    text-transform: uppercase;\n    letter-spacing: 0.05em;\n    padding: 0.25rem 0.5rem;\n    background: ${(props) => props.theme.sidebar.bg};\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    margin-bottom: 0.375rem;\n  }\n\n  .example-subsection {\n    margin-bottom: 0.375rem;\n\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n\n  .example-subsection-title {\n    font-size: ${(props) => props.theme.font.size.xs};\n    font-weight: 500;\n    color: ${(props) => props.theme.colors.text.muted};\n    padding: 0.25rem 0.5rem;\n    margin-bottom: 0.25rem;\n  }\n\n  .example-description {\n    font-weight: 400;\n    color: ${(props) => props.theme.colors.text.muted};\n    font-style: italic;\n  }\n\n  .status-display {\n    display: flex;\n    align-items: center;\n    gap: 0.5rem;\n    padding: 0.25rem 0.5rem;\n    font-family: 'Fira Code', monospace;\n    font-size: ${(props) => props.theme.font.size.xs};\n\n    .status-code {\n      font-weight: 600;\n      padding: 0.125rem 0.375rem;\n      border-radius: ${(props) => props.theme.border.radius.sm};\n      background: ${(props) => props.theme.sidebar.bg};\n\n      &.changed {\n        background: color-mix(in srgb, ${(props) => props.theme.colors.text.warning} 20%, transparent);\n        color: ${(props) => props.theme.colors.text.warning};\n      }\n    }\n\n    .status-text {\n      color: ${(props) => props.theme.colors.text.muted};\n\n      &.changed {\n        color: ${(props) => props.theme.colors.text.warning};\n      }\n    }\n  }\n\n  .example-subsection .diff-table {\n    margin: 0;\n  }\n\n  .example-subsection .code-diff-content {\n    max-height: 150px;\n    border: 1px solid ${(props) => props.theme.border.border1};\n    border-radius: ${(props) => props.theme.border.radius.sm};\n  }\n\n  .empty-state {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    padding: 2rem;\n    color: ${(props) => props.theme.colors.text.muted};\n    font-size: ${(props) => props.theme.font.size.sm};\n  }\n\n  .tags-container {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 0.25rem;\n  }\n\n  .tag-badge {\n    display: inline-block;\n    padding: 0.125rem 0.375rem;\n    font-size: ${(props) => props.theme.font.size.xs};\n    font-family: 'Fira Code', monospace;\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    background: ${(props) => props.theme.sidebar.bg};\n    border: 1px solid ${(props) => props.theme.border.border1};\n\n    &.added {\n      background: color-mix(in srgb, ${(props) => props.theme.colors.text.green} 15%, transparent);\n      border-color: ${(props) => props.theme.colors.text.green};\n      color: ${(props) => props.theme.colors.text.green};\n    }\n\n    &.deleted {\n      background: color-mix(in srgb, ${(props) => props.theme.colors.text.danger} 15%, transparent);\n      border-color: ${(props) => props.theme.colors.text.danger};\n      color: ${(props) => props.theme.colors.text.danger};\n      text-decoration: line-through;\n    }\n\n    &.modified {\n      background: color-mix(in srgb, ${(props) => props.theme.colors.text.warning} 15%, transparent);\n      border-color: ${(props) => props.theme.colors.text.warning};\n      color: ${(props) => props.theme.colors.text.warning};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Git/VisualDiffViewer/VisualDiffContent/index.js",
    "content": "import React, { useState, useEffect } from 'react';\nimport CollapsibleDiffRow from '../CollapsibleDiffRow';\nimport StyledWrapper from './StyledWrapper';\n\n/**\n * VisualDiffContent - Presentational component for rendering visual diffs\n *\n * This is a reusable component that renders the visual diff UI.\n * It can be used by:\n * - Git VisualDiffViewer (for git diffs)\n * - OpenAPI ChangeSection (for spec diffs)\n *\n * Props:\n * - oldData: The \"before\" data\n * - newData: The \"after\" data\n * - sections: Array of section configs { key, title, Component, hasContent }\n * - sectionHasChanges: Function (sectionKey, oldData, newData) => boolean\n * - oldLabel: Label for the left/old pane (default: \"Before\")\n * - newLabel: Label for the right/new pane (default: \"After\")\n * - hideUnchanged: Hide sections without changes entirely (default: false)\n */\nconst VisualDiffContent = ({\n  oldData,\n  newData,\n  sections,\n  sectionHasChanges,\n  oldLabel = 'Before',\n  newLabel = 'After',\n  hideUnchanged = false\n}) => {\n  const [collapsedSections, setCollapsedSections] = useState({});\n\n  const toggleSection = (sectionKey) => {\n    setCollapsedSections((prev) => ({\n      ...prev,\n      [sectionKey]: !prev[sectionKey]\n    }));\n  };\n\n  // Auto-collapse unchanged sections (collapsed but still visible)\n  useEffect(() => {\n    if (!sectionHasChanges || (!oldData && !newData)) return;\n\n    const initialCollapsed = {};\n    sections.forEach(({ key }) => {\n      const hasChanges = sectionHasChanges(key, oldData, newData);\n      initialCollapsed[key] = !hasChanges;\n    });\n\n    setCollapsedSections(initialCollapsed);\n  }, [oldData, newData, sections, sectionHasChanges]);\n\n  if (!oldData && !newData) {\n    return (\n      <StyledWrapper>\n        <div className=\"empty-state\">\n          No content to display\n        </div>\n      </StyledWrapper>\n    );\n  }\n\n  return (\n    <StyledWrapper>\n\n      <div className=\"visual-diff-content\">\n        <div className=\"diff-header-row\">\n          <div className=\"diff-header-pane old\">{oldLabel}</div>\n          <div className=\"diff-header-pane new\">{newLabel}</div>\n        </div>\n\n        <div className=\"diff-sections\">\n          {sections.map(({ key, title, Component, hasContent: checkContent }) => {\n            const hasOld = oldData && checkContent(oldData);\n            const hasNew = newData && checkContent(newData);\n\n            if (!hasOld && !hasNew) {\n              return null;\n            }\n\n            // Hide sections without changes entirely when hideUnchanged is enabled\n            if (hideUnchanged && sectionHasChanges && !sectionHasChanges(key, oldData, newData)) {\n              return null;\n            }\n\n            return (\n              <CollapsibleDiffRow\n                key={key}\n                title={title}\n                isCollapsed={collapsedSections[key] || false}\n                onToggle={() => toggleSection(key)}\n                hasOldContent={hasOld}\n                hasNewContent={hasNew}\n                oldContent={\n                  <Component oldData={oldData} newData={newData} showSide=\"old\" />\n                }\n                newContent={\n                  <Component oldData={oldData} newData={newData} showSide=\"new\" />\n                }\n              />\n            );\n          })}\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default VisualDiffContent;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Git/VisualDiffViewer/VisualDiffHeaders.js",
    "content": "import React, { useMemo } from 'react';\nimport get from 'lodash/get';\n\nconst VisualDiffHeaders = ({ oldData, newData, showSide }) => {\n  const oldHeaders = get(oldData, 'request.headers', []);\n  const newHeaders = get(newData, 'request.headers', []);\n\n  const currentHeaders = showSide === 'old' ? oldHeaders : newHeaders;\n  const otherHeaders = showSide === 'old' ? newHeaders : oldHeaders;\n\n  const headersWithStatus = useMemo(() => {\n    const otherHeaderMap = new Map();\n    otherHeaders.forEach((h) => {\n      otherHeaderMap.set(h.name, h);\n    });\n\n    return currentHeaders.map((header) => {\n      const otherHeader = otherHeaderMap.get(header.name);\n\n      let status = 'unchanged';\n      if (!otherHeader) {\n        status = showSide === 'old' ? 'deleted' : 'added';\n      } else if (header.value !== otherHeader.value || header.enabled !== otherHeader.enabled) {\n        status = 'modified';\n      }\n\n      return { ...header, status };\n    });\n  }, [currentHeaders, otherHeaders, showSide]);\n\n  if (headersWithStatus.length === 0) {\n    return null;\n  }\n\n  return (\n    <div className=\"diff-section\">\n      <table className=\"diff-table\">\n        <thead>\n          <tr>\n            <th style={{ width: '30px' }}></th>\n            <th className=\"checkbox-cell\"></th>\n            <th style={{ width: '40%' }}>Key</th>\n            <th>Value</th>\n          </tr>\n        </thead>\n        <tbody>\n          {headersWithStatus.map((header, index) => (\n            <tr key={`${header.name}-${index}`} className={header.status}>\n              <td>\n                {header.status !== 'unchanged' && (\n                  <span className={`status-badge ${header.status}`}>\n                    {header.status === 'added' ? 'A' : header.status === 'deleted' ? 'D' : 'M'}\n                  </span>\n                )}\n              </td>\n              <td className=\"checkbox-cell\">\n                <input\n                  type=\"checkbox\"\n                  checked={header.enabled !== false}\n                  readOnly\n                  disabled\n                />\n              </td>\n              <td className=\"key-cell\">{header.name}</td>\n              <td className=\"value-cell\">{header.value}</td>\n            </tr>\n          ))}\n        </tbody>\n      </table>\n    </div>\n  );\n};\n\nexport default VisualDiffHeaders;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Git/VisualDiffViewer/VisualDiffParams.js",
    "content": "import React, { useMemo } from 'react';\nimport get from 'lodash/get';\n\nconst VisualDiffParams = ({ oldData, newData, showSide }) => {\n  const oldParams = get(oldData, 'request.params', []);\n  const newParams = get(newData, 'request.params', []);\n\n  const currentParams = showSide === 'old' ? oldParams : newParams;\n  const otherParams = showSide === 'old' ? newParams : oldParams;\n\n  const paramsWithStatus = useMemo(() => {\n    const otherParamMap = new Map();\n    otherParams.forEach((p) => {\n      otherParamMap.set(p.name, p);\n    });\n\n    return currentParams.map((param) => {\n      const otherParam = otherParamMap.get(param.name);\n\n      let status = 'unchanged';\n      if (!otherParam) {\n        status = showSide === 'old' ? 'deleted' : 'added';\n      } else if (param.value !== otherParam.value || param.enabled !== otherParam.enabled) {\n        status = 'modified';\n      }\n\n      return { ...param, status };\n    });\n  }, [currentParams, otherParams, showSide]);\n\n  const queryParams = paramsWithStatus.filter((p) => p.type === 'query');\n  const pathParams = paramsWithStatus.filter((p) => p.type === 'path');\n\n  if (queryParams.length === 0 && pathParams.length === 0) {\n    return null;\n  }\n\n  const renderTable = (params, title) => {\n    if (params.length === 0) return null;\n\n    return (\n      <div className=\"diff-section\">\n        <div className=\"diff-section-header\">{title}</div>\n        <table className=\"diff-table\">\n          <thead>\n            <tr>\n              <th style={{ width: '30px' }}></th>\n              <th className=\"checkbox-cell\"></th>\n              <th style={{ width: '40%' }}>Key</th>\n              <th>Value</th>\n            </tr>\n          </thead>\n          <tbody>\n            {params.map((param, index) => (\n              <tr key={`${param.name}-${index}`} className={param.status}>\n                <td>\n                  {param.status !== 'unchanged' && (\n                    <span className={`status-badge ${param.status}`}>\n                      {param.status === 'added' ? 'A' : param.status === 'deleted' ? 'D' : 'M'}\n                    </span>\n                  )}\n                </td>\n                <td className=\"checkbox-cell\">\n                  <input\n                    type=\"checkbox\"\n                    checked={param.enabled !== false}\n                    readOnly\n                    disabled\n                  />\n                </td>\n                <td className=\"key-cell\">{param.name}</td>\n                <td className=\"value-cell\">{param.value}</td>\n              </tr>\n            ))}\n          </tbody>\n        </table>\n      </div>\n    );\n  };\n\n  return (\n    <>\n      {renderTable(queryParams, 'Query Parameters')}\n      {renderTable(pathParams, 'Path Parameters')}\n    </>\n  );\n};\n\nexport default VisualDiffParams;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Git/VisualDiffViewer/VisualDiffUrlBar.js",
    "content": "import React, { useMemo } from 'react';\nimport { computeWordDiffForOld, computeWordDiffForNew } from './utils/diffUtils';\nimport { getMethod, getUrl } from './utils/bruUtils';\n\nconst VisualDiffUrlBar = ({ oldData, newData, showSide }) => {\n  const oldMethod = getMethod(oldData);\n  const newMethod = getMethod(newData);\n  const oldUrl = getUrl(oldData);\n  const newUrl = getUrl(newData);\n\n  const currentMethod = showSide === 'old' ? oldMethod : newMethod;\n\n  const urlDiffSegments = useMemo(() => {\n    if (showSide === 'old') {\n      return computeWordDiffForOld(oldUrl, newUrl);\n    } else {\n      return computeWordDiffForNew(oldUrl, newUrl);\n    }\n  }, [oldUrl, newUrl, showSide]);\n\n  const methodChanged = oldMethod !== newMethod;\n  const methodStatus = useMemo(() => {\n    if (!methodChanged) return 'unchanged';\n    if (showSide === 'old') return 'deleted';\n    return 'added';\n  }, [methodChanged, showSide]);\n\n  const renderDiffSegments = (segments) => {\n    return segments.map((segment, index) => {\n      if (segment.status === 'unchanged') {\n        return <span key={index}>{segment.text}</span>;\n      }\n      return (\n        <span key={index} className={`diff-inline ${segment.status}`}>\n          {segment.text}\n        </span>\n      );\n    });\n  };\n\n  return (\n    <div className=\"diff-section\">\n      <div className=\"url-bar\">\n        <span className={`method ${methodStatus !== 'unchanged' ? `diff-inline ${methodStatus}` : ''}`}>\n          {currentMethod?.toUpperCase() || 'GET'}\n        </span>\n        <span className=\"url\">\n          {renderDiffSegments(urlDiffSegments)}\n        </span>\n      </div>\n    </div>\n  );\n};\n\nexport default VisualDiffUrlBar;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Git/VisualDiffViewer/utils/bruUtils.js",
    "content": "import get from 'lodash/get';\n\nexport const DIFF_STATUS = Object.freeze({\n  ADDED: 'added',\n  DELETED: 'deleted',\n  MODIFIED: 'modified',\n  UNCHANGED: 'unchanged'\n});\n\nexport const getBodyContent = (body) => {\n  if (!body) return '';\n  if (body.json) return body.json;\n  if (body.text) return body.text;\n  if (body.xml) return body.xml;\n  if (body.sparql) return body.sparql;\n  if (body.graphql?.query) return body.graphql.query;\n  if (body.content) return body.content;\n  return '';\n};\n\nexport const getBodyMode = (body) => {\n  if (!body) return 'none';\n  if (body.json !== undefined) return 'json';\n  if (body.text !== undefined) return 'text';\n  if (body.xml !== undefined) return 'xml';\n  if (body.sparql !== undefined) return 'sparql';\n  if (body.graphql) return 'graphql';\n  if (body.formUrlEncoded) return 'formUrlEncoded';\n  if (body.multipartForm) return 'multipartForm';\n  if (body.file) return 'file';\n  if (body.grpc) return 'grpc';\n  if (body.ws) return 'ws';\n  if (body.mode === 'none') return 'none';\n  return 'none';\n};\n\nexport const getMethod = (data) => {\n  return get(data, 'request.method', 'GET');\n};\n\nexport const getUrl = (data) => {\n  return get(data, 'request.url', '');\n};\n\nexport const computeItemDiffStatus = (currentItem, otherItem, showSide) => {\n  if (!otherItem) {\n    return showSide === 'old' ? DIFF_STATUS.DELETED : DIFF_STATUS.ADDED;\n  }\n  if (currentItem.value !== otherItem.value || currentItem.enabled !== otherItem.enabled) {\n    return DIFF_STATUS.MODIFIED;\n  }\n  return DIFF_STATUS.UNCHANGED;\n};\n"
  },
  {
    "path": "packages/bruno-app/src/components/Git/VisualDiffViewer/utils/bruUtils.spec.js",
    "content": "const { describe, it, expect } = require('@jest/globals');\n\nimport {\n  getBodyContent,\n  getBodyMode,\n  getMethod,\n  getUrl,\n  computeItemDiffStatus\n} from './bruUtils';\n\ndescribe('bruUtils', () => {\n  describe('getBodyContent', () => {\n    it('should return empty string for null or undefined body', () => {\n      expect(getBodyContent(null)).toBe('');\n      expect(getBodyContent(undefined)).toBe('');\n    });\n\n    it('should return empty string for empty body', () => {\n      expect(getBodyContent({})).toBe('');\n    });\n\n    it('should return json content', () => {\n      expect(getBodyContent({ json: '{\"key\": \"value\"}' })).toBe('{\"key\": \"value\"}');\n    });\n\n    it('should return text content', () => {\n      expect(getBodyContent({ text: 'plain text content' })).toBe('plain text content');\n    });\n\n    it('should return xml content', () => {\n      expect(getBodyContent({ xml: '<root><item>value</item></root>' })).toBe('<root><item>value</item></root>');\n    });\n\n    it('should return sparql content', () => {\n      expect(getBodyContent({ sparql: 'SELECT * WHERE { ?s ?p ?o }' })).toBe('SELECT * WHERE { ?s ?p ?o }');\n    });\n\n    it('should return graphql query content', () => {\n      expect(getBodyContent({ graphql: { query: 'query { users { id } }' } })).toBe('query { users { id } }');\n    });\n\n    it('should return generic content', () => {\n      expect(getBodyContent({ content: 'generic content' })).toBe('generic content');\n    });\n\n    it('should return empty string for graphql without query', () => {\n      expect(getBodyContent({ graphql: {} })).toBe('');\n      expect(getBodyContent({ graphql: { variables: '{}' } })).toBe('');\n    });\n\n    it('should prioritize json over other types', () => {\n      expect(getBodyContent({ json: '{\"a\":1}', text: 'text' })).toBe('{\"a\":1}');\n    });\n  });\n\n  describe('getBodyMode', () => {\n    it('should return none for null or undefined body', () => {\n      expect(getBodyMode(null)).toBe('none');\n      expect(getBodyMode(undefined)).toBe('none');\n    });\n\n    it('should return none for empty body', () => {\n      expect(getBodyMode({})).toBe('none');\n    });\n\n    it('should return json mode', () => {\n      expect(getBodyMode({ json: '{}' })).toBe('json');\n      expect(getBodyMode({ json: '' })).toBe('json');\n    });\n\n    it('should return text mode', () => {\n      expect(getBodyMode({ text: 'content' })).toBe('text');\n      expect(getBodyMode({ text: '' })).toBe('text');\n    });\n\n    it('should return xml mode', () => {\n      expect(getBodyMode({ xml: '<root/>' })).toBe('xml');\n    });\n\n    it('should return sparql mode', () => {\n      expect(getBodyMode({ sparql: 'SELECT *' })).toBe('sparql');\n    });\n\n    it('should return graphql mode', () => {\n      expect(getBodyMode({ graphql: { query: '' } })).toBe('graphql');\n    });\n\n    it('should return formUrlEncoded mode', () => {\n      expect(getBodyMode({ formUrlEncoded: [] })).toBe('formUrlEncoded');\n      expect(getBodyMode({ formUrlEncoded: [{ name: 'key', value: 'val' }] })).toBe('formUrlEncoded');\n    });\n\n    it('should return multipartForm mode', () => {\n      expect(getBodyMode({ multipartForm: [] })).toBe('multipartForm');\n    });\n\n    it('should return file mode', () => {\n      expect(getBodyMode({ file: [] })).toBe('file');\n    });\n\n    it('should return grpc mode', () => {\n      expect(getBodyMode({ grpc: [] })).toBe('grpc');\n    });\n\n    it('should return ws mode', () => {\n      expect(getBodyMode({ ws: [] })).toBe('ws');\n    });\n\n    it('should return none for explicit none mode', () => {\n      expect(getBodyMode({ mode: 'none' })).toBe('none');\n    });\n\n    it('should prioritize json over other modes', () => {\n      expect(getBodyMode({ json: '{}', text: 'text' })).toBe('json');\n    });\n  });\n\n  describe('getMethod', () => {\n    it('should return GET as default', () => {\n      expect(getMethod(null)).toBe('GET');\n      expect(getMethod(undefined)).toBe('GET');\n      expect(getMethod({})).toBe('GET');\n    });\n\n    it('should return request method', () => {\n      expect(getMethod({ request: { method: 'POST' } })).toBe('POST');\n      expect(getMethod({ request: { method: 'PUT' } })).toBe('PUT');\n      expect(getMethod({ request: { method: 'DELETE' } })).toBe('DELETE');\n    });\n\n    it('should return GET when request exists but method is missing', () => {\n      expect(getMethod({ request: {} })).toBe('GET');\n    });\n  });\n\n  describe('getUrl', () => {\n    it('should return empty string as default', () => {\n      expect(getUrl(null)).toBe('');\n      expect(getUrl(undefined)).toBe('');\n      expect(getUrl({})).toBe('');\n    });\n\n    it('should return request url', () => {\n      expect(getUrl({ request: { url: 'https://api.example.com/users' } })).toBe('https://api.example.com/users');\n    });\n\n    it('should return empty string when request exists but url is missing', () => {\n      expect(getUrl({ request: {} })).toBe('');\n    });\n\n    it('should return url with different protocols', () => {\n      expect(getUrl({ request: { url: 'http://localhost:3000' } })).toBe('http://localhost:3000');\n      expect(getUrl({ request: { url: 'ws://localhost:8080' } })).toBe('ws://localhost:8080');\n      expect(getUrl({ request: { url: 'grpc://localhost:50051' } })).toBe('grpc://localhost:50051');\n    });\n  });\n\n  describe('computeItemDiffStatus', () => {\n    it('should return deleted when other item is missing and showing old side', () => {\n      expect(computeItemDiffStatus({ name: 'key', value: 'val' }, null, 'old')).toBe('deleted');\n      expect(computeItemDiffStatus({ name: 'key', value: 'val' }, undefined, 'old')).toBe('deleted');\n    });\n\n    it('should return added when other item is missing and showing new side', () => {\n      expect(computeItemDiffStatus({ name: 'key', value: 'val' }, null, 'new')).toBe('added');\n      expect(computeItemDiffStatus({ name: 'key', value: 'val' }, undefined, 'new')).toBe('added');\n    });\n\n    it('should return unchanged when items are equal', () => {\n      const item = { name: 'key', value: 'val', enabled: true };\n      const otherItem = { name: 'key', value: 'val', enabled: true };\n      expect(computeItemDiffStatus(item, otherItem, 'old')).toBe('unchanged');\n      expect(computeItemDiffStatus(item, otherItem, 'new')).toBe('unchanged');\n    });\n\n    it('should return modified when values differ', () => {\n      const item = { name: 'key', value: 'val1', enabled: true };\n      const otherItem = { name: 'key', value: 'val2', enabled: true };\n      expect(computeItemDiffStatus(item, otherItem, 'old')).toBe('modified');\n    });\n\n    it('should return modified when enabled status differs', () => {\n      const item = { name: 'key', value: 'val', enabled: true };\n      const otherItem = { name: 'key', value: 'val', enabled: false };\n      expect(computeItemDiffStatus(item, otherItem, 'old')).toBe('modified');\n    });\n\n    it('should handle undefined enabled as different from explicit false', () => {\n      const item = { name: 'key', value: 'val' };\n      const otherItem = { name: 'key', value: 'val', enabled: false };\n      expect(computeItemDiffStatus(item, otherItem, 'old')).toBe('modified');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/components/Git/VisualDiffViewer/utils/diffUtils.js",
    "content": "// Matches word-boundary separators: whitespace, slashes, query/path delimiters (?&=), dots, hyphens, underscores, colons, @\nconst WORD_SEPARATOR = /[\\s\\/\\?\\&\\=\\.\\-\\_\\:\\@]/;\n\nconst splitWithSeparators = (str) => {\n  const result = [];\n  let current = '';\n  for (const char of str) {\n    if (WORD_SEPARATOR.test(char)) {\n      if (current) {\n        result.push(current);\n        current = '';\n      }\n      result.push(char);\n    } else {\n      current += char;\n    }\n  }\n  if (current) {\n    result.push(current);\n  }\n  return result;\n};\n\nexport const computeWordDiffForOld = (oldStr, newStr) => {\n  if (oldStr === newStr) {\n    return [{ text: oldStr, status: 'unchanged' }];\n  }\n\n  if (!oldStr) {\n    return [];\n  }\n\n  if (!newStr) {\n    return [{ text: oldStr, status: 'deleted' }];\n  }\n\n  const oldWords = splitWithSeparators(oldStr);\n  const newWords = splitWithSeparators(newStr);\n  const lcs = computeLCS(oldWords, newWords);\n\n  const segments = [];\n  let oldIdx = 0;\n  let lcsIdx = 0;\n\n  while (oldIdx < oldWords.length) {\n    if (lcsIdx < lcs.length && oldIdx === lcs[lcsIdx].oldIndex) {\n      segments.push({ text: oldWords[oldIdx], status: 'unchanged' });\n      lcsIdx++;\n    } else {\n      segments.push({ text: oldWords[oldIdx], status: 'deleted' });\n    }\n    oldIdx++;\n  }\n\n  return mergeSegments(segments);\n};\n\nexport const computeWordDiffForNew = (oldStr, newStr) => {\n  if (oldStr === newStr) {\n    return [{ text: newStr, status: 'unchanged' }];\n  }\n\n  if (!newStr) {\n    return [];\n  }\n\n  if (!oldStr) {\n    return [{ text: newStr, status: 'added' }];\n  }\n\n  const oldWords = splitWithSeparators(oldStr);\n  const newWords = splitWithSeparators(newStr);\n  const lcs = computeLCS(oldWords, newWords);\n\n  const segments = [];\n  let newIdx = 0;\n  let lcsIdx = 0;\n\n  while (newIdx < newWords.length) {\n    if (lcsIdx < lcs.length && newIdx === lcs[lcsIdx].newIndex) {\n      segments.push({ text: newWords[newIdx], status: 'unchanged' });\n      lcsIdx++;\n    } else {\n      segments.push({ text: newWords[newIdx], status: 'added' });\n    }\n    newIdx++;\n  }\n\n  return mergeSegments(segments);\n};\n\nconst mergeSegments = (segments) => {\n  const merged = [];\n  for (const segment of segments) {\n    if (merged.length > 0 && merged[merged.length - 1].status === segment.status) {\n      merged[merged.length - 1].text += segment.text;\n    } else {\n      merged.push({ ...segment });\n    }\n  }\n  return merged;\n};\n\nconst computeLCS = (arr1, arr2) => {\n  const m = arr1.length;\n  const n = arr2.length;\n  const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));\n\n  for (let i = 1; i <= m; i++) {\n    for (let j = 1; j <= n; j++) {\n      if (arr1[i - 1] === arr2[j - 1]) {\n        dp[i][j] = dp[i - 1][j - 1] + 1;\n      } else {\n        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);\n      }\n    }\n  }\n\n  const lcs = [];\n  let i = m, j = n;\n  while (i > 0 && j > 0) {\n    if (arr1[i - 1] === arr2[j - 1]) {\n      lcs.unshift({ value: arr1[i - 1], oldIndex: i - 1, newIndex: j - 1 });\n      i--;\n      j--;\n    } else if (dp[i - 1][j] > dp[i][j - 1]) {\n      i--;\n    } else {\n      j--;\n    }\n  }\n\n  return lcs;\n};\n\nexport const computeLineDiffForOld = (oldStr, newStr) => {\n  if (oldStr === newStr) {\n    return (oldStr || '').split('\\n').map((line) => ({ text: line, status: 'unchanged' }));\n  }\n\n  if (!oldStr) {\n    return [];\n  }\n\n  if (!newStr) {\n    return oldStr.split('\\n').map((line) => ({ text: line, status: 'deleted' }));\n  }\n\n  const oldLines = oldStr.split('\\n');\n  const newLines = newStr.split('\\n');\n  const lcs = computeLCS(oldLines, newLines);\n\n  const segments = [];\n  let oldIdx = 0;\n  let lcsIdx = 0;\n\n  while (oldIdx < oldLines.length) {\n    if (lcsIdx < lcs.length && oldIdx === lcs[lcsIdx].oldIndex) {\n      segments.push({ text: oldLines[oldIdx], status: 'unchanged' });\n      lcsIdx++;\n    } else {\n      segments.push({ text: oldLines[oldIdx], status: 'deleted' });\n    }\n    oldIdx++;\n  }\n\n  return segments;\n};\n\nexport const computeLineDiffForNew = (oldStr, newStr) => {\n  if (oldStr === newStr) {\n    return (newStr || '').split('\\n').map((line) => ({ text: line, status: 'unchanged' }));\n  }\n\n  if (!newStr) {\n    return [];\n  }\n\n  if (!oldStr) {\n    return newStr.split('\\n').map((line) => ({ text: line, status: 'added' }));\n  }\n\n  const oldLines = oldStr.split('\\n');\n  const newLines = newStr.split('\\n');\n  const lcs = computeLCS(oldLines, newLines);\n\n  const segments = [];\n  let newIdx = 0;\n  let lcsIdx = 0;\n\n  while (newIdx < newLines.length) {\n    if (lcsIdx < lcs.length && newIdx === lcs[lcsIdx].newIndex) {\n      segments.push({ text: newLines[newIdx], status: 'unchanged' });\n      lcsIdx++;\n    } else {\n      segments.push({ text: newLines[newIdx], status: 'added' });\n    }\n    newIdx++;\n  }\n\n  return segments;\n};\n"
  },
  {
    "path": "packages/bruno-app/src/components/Git/VisualDiffViewer/utils/diffUtils.spec.js",
    "content": "const { describe, it, expect } = require('@jest/globals');\n\nimport {\n  computeWordDiffForOld,\n  computeWordDiffForNew,\n  computeLineDiffForOld,\n  computeLineDiffForNew\n} from './diffUtils';\n\ndescribe('diffUtils', () => {\n  describe('computeWordDiffForOld', () => {\n    it('should return unchanged for identical strings', () => {\n      expect(computeWordDiffForOld('hello world', 'hello world')).toEqual([\n        { text: 'hello world', status: 'unchanged' }\n      ]);\n    });\n\n    it('should return empty array for empty old string', () => {\n      expect(computeWordDiffForOld('', 'new text')).toEqual([]);\n      expect(computeWordDiffForOld(null, 'new text')).toEqual([]);\n      expect(computeWordDiffForOld(undefined, 'new text')).toEqual([]);\n    });\n\n    it('should return deleted for entire old string when new is empty', () => {\n      expect(computeWordDiffForOld('old text', '')).toEqual([\n        { text: 'old text', status: 'deleted' }\n      ]);\n      expect(computeWordDiffForOld('old text', null)).toEqual([\n        { text: 'old text', status: 'deleted' }\n      ]);\n    });\n\n    it('should detect deleted words', () => {\n      const result = computeWordDiffForOld('hello world', 'hello');\n      expect(result).toContainEqual({ text: 'hello', status: 'unchanged' });\n      expect(result.some((s) => s.status === 'deleted' && s.text.includes('world'))).toBe(true);\n    });\n\n    it('should handle URL paths', () => {\n      const result = computeWordDiffForOld(\n        'https://api.example.com/users/123',\n        'https://api.example.com/users/456'\n      );\n      expect(result.some((s) => s.status === 'unchanged')).toBe(true);\n      expect(result.some((s) => s.status === 'deleted')).toBe(true);\n    });\n\n    it('should preserve separators', () => {\n      const result = computeWordDiffForOld('a/b/c', 'a/b/c');\n      expect(result).toEqual([{ text: 'a/b/c', status: 'unchanged' }]);\n    });\n  });\n\n  describe('computeWordDiffForNew', () => {\n    it('should return unchanged for identical strings', () => {\n      expect(computeWordDiffForNew('hello world', 'hello world')).toEqual([\n        { text: 'hello world', status: 'unchanged' }\n      ]);\n    });\n\n    it('should return empty array for empty new string', () => {\n      expect(computeWordDiffForNew('old text', '')).toEqual([]);\n      expect(computeWordDiffForNew('old text', null)).toEqual([]);\n      expect(computeWordDiffForNew('old text', undefined)).toEqual([]);\n    });\n\n    it('should return added for entire new string when old is empty', () => {\n      expect(computeWordDiffForNew('', 'new text')).toEqual([\n        { text: 'new text', status: 'added' }\n      ]);\n      expect(computeWordDiffForNew(null, 'new text')).toEqual([\n        { text: 'new text', status: 'added' }\n      ]);\n    });\n\n    it('should detect added words', () => {\n      const result = computeWordDiffForNew('hello', 'hello world');\n      expect(result).toContainEqual({ text: 'hello', status: 'unchanged' });\n      expect(result.some((s) => s.status === 'added' && s.text.includes('world'))).toBe(true);\n    });\n\n    it('should handle URL paths', () => {\n      const result = computeWordDiffForNew(\n        'https://api.example.com/users/123',\n        'https://api.example.com/users/456'\n      );\n      expect(result.some((s) => s.status === 'unchanged')).toBe(true);\n      expect(result.some((s) => s.status === 'added')).toBe(true);\n    });\n  });\n\n  describe('computeLineDiffForOld', () => {\n    it('should return unchanged for identical multiline strings', () => {\n      const text = 'line1\\nline2\\nline3';\n      expect(computeLineDiffForOld(text, text)).toEqual([\n        { text: 'line1', status: 'unchanged' },\n        { text: 'line2', status: 'unchanged' },\n        { text: 'line3', status: 'unchanged' }\n      ]);\n    });\n\n    it('should return empty array for empty old string', () => {\n      expect(computeLineDiffForOld('', 'new\\ntext')).toEqual([]);\n      expect(computeLineDiffForOld(null, 'new\\ntext')).toEqual([]);\n    });\n\n    it('should return deleted for all lines when new is empty', () => {\n      expect(computeLineDiffForOld('line1\\nline2', '')).toEqual([\n        { text: 'line1', status: 'deleted' },\n        { text: 'line2', status: 'deleted' }\n      ]);\n    });\n\n    it('should detect deleted lines', () => {\n      const result = computeLineDiffForOld('line1\\nline2\\nline3', 'line1\\nline3');\n      expect(result).toContainEqual({ text: 'line1', status: 'unchanged' });\n      expect(result).toContainEqual({ text: 'line2', status: 'deleted' });\n      expect(result).toContainEqual({ text: 'line3', status: 'unchanged' });\n    });\n\n    it('should handle single line strings', () => {\n      expect(computeLineDiffForOld('single line', 'single line')).toEqual([\n        { text: 'single line', status: 'unchanged' }\n      ]);\n    });\n\n    it('should handle code blocks', () => {\n      const oldCode = 'function foo() {\\n  return 1;\\n}';\n      const newCode = 'function foo() {\\n  return 2;\\n}';\n      const result = computeLineDiffForOld(oldCode, newCode);\n      expect(result).toContainEqual({ text: 'function foo() {', status: 'unchanged' });\n      expect(result).toContainEqual({ text: '  return 1;', status: 'deleted' });\n      expect(result).toContainEqual({ text: '}', status: 'unchanged' });\n    });\n  });\n\n  describe('computeLineDiffForNew', () => {\n    it('should return unchanged for identical multiline strings', () => {\n      const text = 'line1\\nline2\\nline3';\n      expect(computeLineDiffForNew(text, text)).toEqual([\n        { text: 'line1', status: 'unchanged' },\n        { text: 'line2', status: 'unchanged' },\n        { text: 'line3', status: 'unchanged' }\n      ]);\n    });\n\n    it('should return empty array for empty new string', () => {\n      expect(computeLineDiffForNew('old\\ntext', '')).toEqual([]);\n      expect(computeLineDiffForNew('old\\ntext', null)).toEqual([]);\n    });\n\n    it('should return added for all lines when old is empty', () => {\n      expect(computeLineDiffForNew('', 'line1\\nline2')).toEqual([\n        { text: 'line1', status: 'added' },\n        { text: 'line2', status: 'added' }\n      ]);\n    });\n\n    it('should detect added lines', () => {\n      const result = computeLineDiffForNew('line1\\nline3', 'line1\\nline2\\nline3');\n      expect(result).toContainEqual({ text: 'line1', status: 'unchanged' });\n      expect(result).toContainEqual({ text: 'line2', status: 'added' });\n      expect(result).toContainEqual({ text: 'line3', status: 'unchanged' });\n    });\n\n    it('should handle code blocks', () => {\n      const oldCode = 'function foo() {\\n  return 1;\\n}';\n      const newCode = 'function foo() {\\n  return 2;\\n}';\n      const result = computeLineDiffForNew(oldCode, newCode);\n      expect(result).toContainEqual({ text: 'function foo() {', status: 'unchanged' });\n      expect(result).toContainEqual({ text: '  return 2;', status: 'added' });\n      expect(result).toContainEqual({ text: '}', status: 'unchanged' });\n    });\n  });\n\n  describe('edge cases', () => {\n    it('should handle empty strings', () => {\n      expect(computeWordDiffForOld('', '')).toEqual([{ text: '', status: 'unchanged' }]);\n      expect(computeWordDiffForNew('', '')).toEqual([{ text: '', status: 'unchanged' }]);\n    });\n\n    it('should handle strings with only whitespace', () => {\n      const result = computeWordDiffForOld('   ', '   ');\n      expect(result).toEqual([{ text: '   ', status: 'unchanged' }]);\n    });\n\n    it('should handle special characters in URLs', () => {\n      const url = 'https://api.example.com/users?id=123&name=test';\n      expect(computeWordDiffForOld(url, url)).toEqual([{ text: url, status: 'unchanged' }]);\n    });\n\n    it('should handle JSON-like content', () => {\n      const json = '{\"key\": \"value\", \"number\": 123}';\n      const result = computeLineDiffForOld(json, json);\n      expect(result).toEqual([{ text: json, status: 'unchanged' }]);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/components/GlobalSearchModal/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst StyledWrapper = styled.div`\n  /* Screen reader only content */\n  .sr-only {\n    position: absolute;\n    width: 1px;\n    height: 1px;\n    padding: 0;\n    margin: -1px;\n    overflow: hidden;\n    clip: rect(0, 0, 0, 0);\n    white-space: nowrap;\n    border: 0;\n  }\n\n  .command-k-overlay {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100vw;\n    height: 100vh;\n    display: flex;\n    align-items: flex-start;\n    justify-content: center;\n    overflow-y: auto;\n    z-index: 20;\n    background-color: transparent;\n    &:before {\n      content: '';\n      height: 100%;\n      width: 100%;\n      left: 0;\n      opacity: ${(props) => props.theme.modal.backdrop.opacity};\n      top: 0;\n      background: black;\n      position: fixed;\n    }\n    animation: fade-in 0.1s forwards cubic-bezier(0.19, 1, 0.22, 1);\n  }\n  .command-k-modal {\n    background: ${(props) => props.theme.modal.body.bg};\n    border: 1px solid ${(props) => props.theme.border.border1};\n    border-radius: 8px;\n    box-shadow: ${(props) => props.theme.shadow.md};\n    width: 90%;\n    max-width: 600px;\n    max-height: 70vh;\n    display: flex;\n    flex-direction: column;\n    overflow: hidden;\n    margin: 80px auto;\n    animation: fade-and-slide-in-from-top 0.3s forwards cubic-bezier(0.19, 1, 0.22, 1);\n    will-change: opacity, transform;\n  }\n  .command-k-header {\n    padding: 12px;\n    border-bottom: 1px solid ${(props) => props.theme.border.border1};\n  }\n  .search-input-container {\n    position: relative;\n    display: flex;\n    align-items: center;\n    width: 100%;\n    padding: 8px 12px;\n    border: 1px solid ${(props) => props.theme.input.border};\n    border-radius: 6px;\n    transition: all 0.2s ease;\n    &:focus-within {\n      border: 1px solid ${(props) => props.theme.input.focusBorder};\n    }\n    .search-icon {\n      color: ${(props) => props.theme.colors.text.muted};\n      opacity: 0.8;\n      margin-right: 8px;\n      flex-shrink: 0;\n    }\n    .clear-button {\n      background: transparent;\n      border: none;\n      padding: 4px;\n      cursor: pointer;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      color: ${(props) => props.theme.colors.text.muted};\n      opacity: 0.8;\n      margin-left: 8px;\n      border-radius: 4px;\n      flex-shrink: 0;\n      &:hover {\n        background: ${(props) => rgba(props.theme.text, 0.1)};\n      }\n    }\n  }\n  .search-input {\n    flex: 1;\n    background: transparent;\n    border: none;\n    outline: none;\n    color: ${(props) => props.theme.text};\n    font-size: ${(props) => props.theme.font.size.base};\n    width: 100%;\n    padding: 0;\n    &::placeholder {\n      color: ${(props) => props.theme.colors.text.muted};\n      opacity: 0.7;\n    }\n  }\n  .command-k-results {\n    flex: 1;\n    overflow-y: auto;\n    max-height: 400px;\n    scrollbar-width: thin;\n    padding: 6px 0;\n    scroll-behavior: smooth;\n    /* Webkit scrollbar styling */\n    &::-webkit-scrollbar {\n      width: 8px;\n      height: 8px;\n    }\n    &::-webkit-scrollbar-track {\n      background: transparent;\n    }\n    &::-webkit-scrollbar-thumb {\n      background: ${(props) => rgba(props.theme.text, 0.2)};\n      border-radius: 4px;\n      &:hover {\n        background: ${(props) => rgba(props.theme.text, 0.3)};\n      }\n    }\n  }\n  .result-item {\n    display: flex;\n    align-items: center;\n    padding: 10px 12px;\n    margin: 2px 8px;\n    gap: 10px;\n    cursor: pointer;\n    border-radius: ${(props) => props.theme.border.radius.base};\n    transition: background 0.1s ease;\n    &:hover:not(.selected) {\n      background: ${(props) => rgba(props.theme.text, 0.05)};\n    }\n    &.selected {\n      background: ${(props) => props.theme.dropdown.hoverBg};\n    }\n  }\n  .result-icon {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 24px;\n    height: 24px;\n    flex-shrink: 0;\n    color: ${(props) => props.theme.colors.text.muted};\n    opacity: 0.8;\n  }\n  .result-content {\n    flex: 1;\n    min-width: 0;\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    gap: 12px;\n  }\n  .result-info {\n    flex: 1;\n    min-width: 0;\n    margin-right: 8px;\n  }\n  .result-badges {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    flex-shrink: 0;\n  }\n  .result-name {\n    font-size: ${(props) => props.theme.font.size.base};\n    margin-bottom: 3px;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    color: ${(props) => props.theme.text};\n    letter-spacing: 0.2px;\n  }\n  .result-path {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.muted};\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    letter-spacing: 0.1px;\n  }\n  .method-badge {\n    font-size: 0.625rem;\n    font-weight: 500;\n    padding: 2px 6px;\n    border-radius: 4px;\n    text-transform: uppercase;\n    letter-spacing: 0.3px;\n    flex-shrink: 0;\n    min-width: 48px;\n    text-align: center;\n    &.get {\n      color: ${(props) => props.theme.request.methods.get};\n      background: ${(props) => rgba(props.theme.request.methods.get, 0.1)};\n      border: 1px solid ${(props) => rgba(props.theme.request.methods.get, 0.2)};\n    }\n    &.post {\n      color: ${(props) => props.theme.request.methods.post};\n      background: ${(props) => rgba(props.theme.request.methods.post, 0.1)};\n      border: 1px solid ${(props) => rgba(props.theme.request.methods.post, 0.2)};\n    }\n    &.put {\n      color: ${(props) => props.theme.request.methods.put};\n      background: ${(props) => rgba(props.theme.request.methods.put, 0.1)};\n      border: 1px solid ${(props) => rgba(props.theme.request.methods.put, 0.2)};\n    }\n    &.delete {\n      color: ${(props) => props.theme.request.methods.delete};\n      background: ${(props) => rgba(props.theme.request.methods.delete, 0.1)};\n      border: 1px solid ${(props) => rgba(props.theme.request.methods.delete, 0.2)};\n    }\n    &.patch {\n      color: ${(props) => props.theme.request.methods.patch};\n      background: ${(props) => rgba(props.theme.request.methods.patch, 0.1)};\n      border: 1px solid ${(props) => rgba(props.theme.request.methods.patch, 0.2)};\n    }\n    &.head {\n      color: ${(props) => props.theme.request.methods.head};\n      background: ${(props) => rgba(props.theme.request.methods.head, 0.1)};\n      border: 1px solid ${(props) => rgba(props.theme.request.methods.head, 0.2)};\n    }\n    &.options {\n      color: ${(props) => props.theme.request.methods.options};\n      background: ${(props) => rgba(props.theme.request.methods.options, 0.1)};\n      border: 1px solid ${(props) => rgba(props.theme.request.methods.options, 0.2)};\n    }\n    &.unary {\n      color: ${(props) => props.theme.request.methods.get};\n      background: ${(props) => rgba(props.theme.request.methods.get, 0.12)};\n      border: 1px solid ${(props) => rgba(props.theme.request.methods.get, 0.2)};\n    }\n    &.client-streaming {\n      color: ${(props) => props.theme.request.methods.post};\n      background: ${(props) => rgba(props.theme.request.methods.post, 0.12)};\n      border: 1px solid ${(props) => rgba(props.theme.request.methods.post, 0.2)};\n    }\n    &.server-streaming {\n      color: ${(props) => props.theme.request.methods.put};\n      background: ${(props) => rgba(props.theme.request.methods.put, 0.12)};\n      border: 1px solid ${(props) => rgba(props.theme.request.methods.put, 0.2)};\n    }\n    &.bidirectional-streaming,\n    &.bidi-streaming {\n      color: ${(props) => props.theme.colors.text.purple};\n      background: ${(props) => rgba(props.theme.colors.text.purple, 0.12)};\n      border: 1px solid ${(props) => rgba(props.theme.colors.text.purple, 0.2)};\n    }\n  }\n  .result-type {\n    font-size: 0.625rem;\n    color: ${(props) => props.theme.textLink};\n    padding: 2px 6px;\n    border-radius: 4px;\n    text-transform: uppercase;\n    letter-spacing: 0.3px;\n    font-weight: 500;\n    background: ${(props) => rgba(props.theme.textLink, 0.1)};\n    border: 1px solid ${(props) => rgba(props.theme.textLink, 0.2)};\n    flex-shrink: 0;\n  }\n  .result-item[data-type=\"documentation\"] {\n    .result-icon {\n      color: ${(props) => props.theme.colors.text.muted};\n      opacity: 0.8;\n    }\n    .result-path {\n      font-size: ${(props) => props.theme.font.size.sm};\n      color: ${(props) => props.theme.colors.text.muted};\n      white-space: nowrap;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      letter-spacing: 0.1px;\n      opacity: 0.8;\n    }\n  }\n  .no-results,\n  .empty-state {\n    padding: 24px 16px;\n    text-align: center;\n    color: ${(props) => props.theme.colors.text.muted};\n    font-size: ${(props) => props.theme.font.size.base};\n  }\n  .command-k-footer {\n    padding: 8px 12px;\n    border-top: 1px solid ${(props) => props.theme.border.border1};\n    background: ${(props) => props.theme.colors.surface};\n  }\n  .keyboard-hints {\n    display: flex;\n    justify-content: center;\n    gap: 24px;\n    color: ${(props) => props.theme.colors.text.muted};\n    font-size: ${(props) => props.theme.font.size.sm};\n    letter-spacing: 0.2px;\n    span {\n      display: flex;\n      align-items: center;\n      gap: 6px;\n      .hint-icon {\n        color: ${(props) => props.theme.colors.text.muted};\n        opacity: 0.8;\n      }\n      .hint-icon + .hint-icon {\n        margin-left: -8px;\n      }\n      .keycap {\n        display: inline-flex;\n        align-items: center;\n        justify-content: center;\n        padding: 2px 6px;\n        border: 1px solid ${(props) => props.theme.border.border2};\n        border-radius: 4px;\n        background: ${(props) => rgba(props.theme.text, 0.08)};\n        font-size: ${(props) => props.theme.font.size.xs};\n        font-weight: 500;\n        font-family: inherit;\n        line-height: 1;\n        color: ${(props) => props.theme.text};\n      }\n    }\n  }\n  .highlight {\n    color: ${(props) => props.theme.brand};\n    border-radius: 2px;\n    padding: 1px 2px;\n    margin: 0 -1px;\n  }\n  @keyframes fade-in {\n    from {\n      opacity: 0;\n    }\n    to {\n      opacity: 1;\n    }\n  }\n  @keyframes fade-and-slide-in-from-top {\n    from {\n      opacity: 0;\n      transform: translateY(-20px);\n    }\n    to {\n      opacity: 1;\n      transform: translateY(0);\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/GlobalSearchModal/constants/index.js",
    "content": "export const SEARCH_TYPES = {\n  DOCUMENTATION: 'documentation',\n  COLLECTION: 'collection',\n  FOLDER: 'folder',\n  REQUEST: 'request'\n};\n\nexport const MATCH_TYPES = {\n  COLLECTION: 'collection',\n  FOLDER: 'folder',\n  REQUEST: 'request',\n  URL: 'url',\n  PATH: 'path',\n  DOCUMENTATION: 'documentation'\n};\n\nexport const SEARCH_CONFIG = {\n  MAX_DEPTH: 20,\n  FOCUS_DELAY: 100,\n  SCROLL_BEHAVIOR: 'smooth',\n  SCROLL_BLOCK: 'nearest',\n  DEBOUNCE_DELAY: 300\n};\n\nexport const DOCUMENTATION_RESULT = {\n  type: SEARCH_TYPES.DOCUMENTATION,\n  item: { id: 'docs', name: 'Bruno Documentation' },\n  name: 'Bruno Documentation',\n  path: '/',\n  description: 'Browse the official Bruno documentation',\n  matchType: MATCH_TYPES.DOCUMENTATION\n};\n"
  },
  {
    "path": "packages/bruno-app/src/components/GlobalSearchModal/index.js",
    "content": "import React, { useState, useEffect, useRef, useCallback } from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport {\n  IconSearch,\n  IconX,\n  IconFolder,\n  IconBox,\n  IconFileText,\n  IconBook\n} from '@tabler/icons';\nimport { flattenItems, isItemARequest, isItemAFolder, findParentItemInCollection } from 'utils/collections';\nimport { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';\nimport { toggleCollectionItem, toggleCollection } from 'providers/ReduxStore/slices/collections';\nimport { mountCollection } from 'providers/ReduxStore/slices/collections/actions';\nimport { getDefaultRequestPaneTab } from 'utils/collections';\nimport { normalizeQuery, isValidQuery, highlightText, sortResults, getTypeLabel, getItemPath } from './utils/searchUtils';\nimport { SEARCH_TYPES, MATCH_TYPES, SEARCH_CONFIG, DOCUMENTATION_RESULT } from './constants';\nimport StyledWrapper from './StyledWrapper';\n\nconst GlobalSearchModal = ({ isOpen, onClose }) => {\n  const [query, setQuery] = useState('');\n  const [selectedIndex, setSelectedIndex] = useState(0);\n  const [results, setResults] = useState([]);\n  const inputRef = useRef(null);\n  const resultsRef = useRef(null);\n  const debounceTimeoutRef = useRef(null);\n  const dispatch = useDispatch();\n\n  const collections = useSelector((state) => state.collections.collections);\n  const tabs = useSelector((state) => state.tabs.tabs);\n\n  const createCollectionResults = () => {\n    const collectionResults = collections.map((collection) => ({\n      type: SEARCH_TYPES.COLLECTION,\n      item: collection,\n      name: collection.name,\n      path: collection.name,\n      matchType: MATCH_TYPES.COLLECTION,\n      collectionUid: collection.uid\n    }));\n\n    collectionResults.sort((a, b) => a.name.localeCompare(b.name));\n    return [DOCUMENTATION_RESULT, ...collectionResults];\n  };\n\n  const searchInCollections = (searchTerms, enablePathMatch) => {\n    const results = [];\n\n    // Check for documentation match\n    const queryLower = searchTerms.join(' ');\n    if (['documentation', 'docs', 'bruno docs'].some((term) => term.includes(queryLower))) {\n      results.push(DOCUMENTATION_RESULT);\n    }\n\n    collections.forEach((collection) => {\n      // Search collection name\n      if (searchTerms.every((term) => collection.name.toLowerCase().includes(term))) {\n        results.push({\n          type: SEARCH_TYPES.COLLECTION,\n          item: collection,\n          name: collection.name,\n          path: collection.name,\n          matchType: MATCH_TYPES.COLLECTION,\n          collectionUid: collection.uid\n        });\n      }\n\n      // Search collection items\n      const flattenedItems = flattenItems(collection.items);\n      flattenedItems.forEach((item) => {\n        const itemPath = getItemPath(item, collection, findParentItemInCollection);\n        const itemPathLower = itemPath.toLowerCase();\n\n        if (isItemARequest(item)) {\n          // add an optional check for the item name to prevent a crash if it doesn’t exist.\n          const nameMatch = searchTerms.every((term) => (item.name || '').toLowerCase().includes(term));\n          const urlMatch = searchTerms.every((term) => (item.request?.url || '').toLowerCase().includes(term));\n          const pathMatch = enablePathMatch && searchTerms.every((term) => itemPathLower.includes(term));\n\n          if (nameMatch || urlMatch || pathMatch) {\n            // Check if this is a gRPC request and get the method type\n            const isGrpcRequest = item.request?.type === 'grpc';\n\n            let method = item.request?.method || '';\n\n            if (isGrpcRequest) {\n              // For gRPC requests, use the methodType\n              const methodType = item.request?.methodType || 'UNARY';\n              method = methodType.toLowerCase().replace(/[_]/g, '-');\n            }\n\n            results.push({\n              type: SEARCH_TYPES.REQUEST,\n              item,\n              name: item.name,\n              path: itemPath,\n              matchType: nameMatch ? MATCH_TYPES.REQUEST : urlMatch ? MATCH_TYPES.URL : MATCH_TYPES.PATH,\n              method,\n              collectionUid: collection.uid\n            });\n          }\n        } else if (isItemAFolder(item)) {\n          const nameMatch = searchTerms.every((term) => item.name.toLowerCase().includes(term));\n          const pathMatch = enablePathMatch && searchTerms.every((term) => itemPathLower.includes(term));\n\n          if (nameMatch || pathMatch) {\n            results.push({\n              type: SEARCH_TYPES.FOLDER,\n              item,\n              name: item.name,\n              path: itemPath,\n              matchType: nameMatch ? MATCH_TYPES.FOLDER : MATCH_TYPES.PATH,\n              collectionUid: collection.uid\n            });\n          }\n        }\n      });\n    });\n\n    return results;\n  };\n\n  const performSearch = (searchQuery) => {\n    const normalizedQuery = normalizeQuery(searchQuery);\n\n    if (!normalizedQuery) {\n      setResults(createCollectionResults());\n      return;\n    }\n\n    if (!isValidQuery(normalizedQuery)) {\n      setResults([]);\n      return;\n    }\n\n    const searchTerms = normalizedQuery.toLowerCase().split(/[\\s\\/]+/).filter(Boolean);\n    if (!searchTerms.length) {\n      setResults([]);\n      return;\n    }\n\n    const enablePathMatch = normalizedQuery.includes('/');\n    const searchResults = searchInCollections(searchTerms, enablePathMatch);\n    const sortedResults = sortResults(searchResults);\n\n    setResults(sortedResults);\n    setSelectedIndex(0);\n  };\n\n  const debouncedSearch = useCallback((searchQuery) => {\n    // Clear existing timeout\n    if (debounceTimeoutRef.current) {\n      clearTimeout(debounceTimeoutRef.current);\n    }\n\n    // Set new timeout\n    debounceTimeoutRef.current = setTimeout(() => {\n      performSearch(searchQuery);\n    }, SEARCH_CONFIG.DEBOUNCE_DELAY);\n  }, [collections]); // Depend on collections to recreate when they change\n\n  const expandItemPath = (result) => {\n    const collection = collections.find((c) => c.uid === result.collectionUid);\n    if (!collection) return;\n\n    ensureCollectionIsMounted(collection);\n\n    if (collection.collapsed) {\n      dispatch(toggleCollection(collection.uid));\n    }\n\n    let currentItem = result.type === SEARCH_TYPES.FOLDER\n      ? result.item\n      : findParentItemInCollection(collection, result.item.uid);\n\n    while (currentItem?.type === 'folder') {\n      if (currentItem.collapsed) {\n        dispatch(toggleCollectionItem({ collectionUid: collection.uid, itemUid: currentItem.uid }));\n      }\n      currentItem = findParentItemInCollection(collection, currentItem.uid);\n    }\n  };\n\n  const ensureCollectionIsMounted = (collection) => {\n    if (!collection || collection.mountStatus === 'mounted') return;\n    dispatch(mountCollection({\n      collectionUid: collection.uid,\n      collectionPathname: collection.pathname,\n      brunoConfig: collection.brunoConfig\n    }));\n  };\n\n  const handleKeyNavigation = (e) => {\n    const handlers = {\n      ArrowDown: () => {\n        e.preventDefault();\n        setSelectedIndex((prev) => prev < results.length - 1 ? prev + 1 : 0);\n      },\n      ArrowUp: () => {\n        e.preventDefault();\n        setSelectedIndex((prev) => prev > 0 ? prev - 1 : results.length - 1);\n      },\n      Enter: () => {\n        e.preventDefault();\n        if (results[selectedIndex]) {\n          handleResultSelection(results[selectedIndex]);\n        }\n      },\n      Escape: () => {\n        e.preventDefault();\n        onClose();\n      },\n      PageDown: () => {\n        e.preventDefault();\n        setSelectedIndex((prev) => Math.min(prev + 5, results.length - 1));\n      },\n      PageUp: () => {\n        e.preventDefault();\n        setSelectedIndex((prev) => Math.max(prev - 5, 0));\n      },\n      Home: () => {\n        e.preventDefault();\n        setSelectedIndex(0);\n      },\n      End: () => {\n        e.preventDefault();\n        setSelectedIndex(results.length - 1);\n      }\n    };\n\n    const handler = handlers[e.key];\n    if (handler) handler();\n  };\n\n  const handleResultSelection = (result) => {\n    const targetCollection = collections.find((c) => c.uid === result.collectionUid);\n    ensureCollectionIsMounted(targetCollection);\n\n    if (result.type === SEARCH_TYPES.DOCUMENTATION) {\n      window.open('https://docs.usebruno.com/', '_blank');\n      onClose();\n      return;\n    }\n\n    expandItemPath(result);\n\n    if (result.type === SEARCH_TYPES.REQUEST) {\n      const existingTab = tabs.find((tab) => tab.uid === result.item.uid);\n\n      if (existingTab) {\n        dispatch(focusTab({ uid: result.item.uid }));\n      } else {\n        dispatch(addTab({\n          uid: result.item.uid,\n          collectionUid: result.collectionUid,\n          requestPaneTab: getDefaultRequestPaneTab(result.item),\n          type: 'request'\n        }));\n      }\n    } else if (result.type === SEARCH_TYPES.FOLDER) {\n      dispatch(addTab({\n        uid: result.item.uid,\n        collectionUid: result.collectionUid,\n        type: 'folder-settings'\n      }));\n    } else if (result.type === SEARCH_TYPES.COLLECTION) {\n      dispatch(addTab({\n        uid: result.item.uid,\n        collectionUid: result.collectionUid,\n        type: 'collection-settings'\n      }));\n    }\n\n    onClose();\n  };\n\n  const handleQueryChange = (e) => {\n    const newQuery = e.target.value;\n    setQuery(newQuery);\n\n    if (newQuery.trim()) {\n      debouncedSearch(newQuery);\n    } else {\n      // For empty queries, search immediately to show collections\n      performSearch(newQuery);\n    }\n  };\n\n  const clearSearch = () => {\n    // Clear any pending debounced search\n    if (debounceTimeoutRef.current) {\n      clearTimeout(debounceTimeoutRef.current);\n    }\n\n    setQuery('');\n    setResults([]);\n  };\n\n  // Initialize modal when opened\n  useEffect(() => {\n    if (isOpen) {\n      const timeoutId = setTimeout(() => inputRef.current?.focus(), SEARCH_CONFIG.FOCUS_DELAY);\n      setQuery('');\n      performSearch('');\n      setSelectedIndex(0);\n\n      return () => clearTimeout(timeoutId);\n    } else {\n      // Clear any pending debounced search when modal closes\n      if (debounceTimeoutRef.current) {\n        clearTimeout(debounceTimeoutRef.current);\n      }\n    }\n  }, [isOpen]);\n\n  // Auto-scroll selected item into view\n  useEffect(() => {\n    if (resultsRef.current && results.length > 0) {\n      const selectedElement = resultsRef.current.children[selectedIndex];\n      selectedElement?.scrollIntoView({\n        behavior: SEARCH_CONFIG.SCROLL_BEHAVIOR,\n        block: SEARCH_CONFIG.SCROLL_BLOCK\n      });\n    }\n  }, [selectedIndex, results]);\n\n  // Cleanup debounce timeout on unmount or modal close\n  useEffect(() => {\n    return () => {\n      if (debounceTimeoutRef.current) {\n        clearTimeout(debounceTimeoutRef.current);\n      }\n    };\n  }, []);\n\n  const getResultIcon = (type) => {\n    const iconMap = {\n      [SEARCH_TYPES.DOCUMENTATION]: IconBook,\n      [SEARCH_TYPES.COLLECTION]: IconBox,\n      [SEARCH_TYPES.FOLDER]: IconFolder,\n      [SEARCH_TYPES.REQUEST]: IconFileText\n    };\n    const IconComponent = iconMap[type] || IconFileText;\n    return <IconComponent size={18} stroke={1.5} />;\n  };\n\n  if (!isOpen) return null;\n\n  return (\n    <StyledWrapper>\n      <div\n        className=\"command-k-overlay\"\n        onClick={onClose}\n        role=\"dialog\"\n        aria-modal=\"true\"\n        aria-labelledby=\"search-modal-title\"\n        aria-describedby=\"search-modal-description\"\n      >\n        <div className=\"command-k-modal\" onClick={(e) => e.stopPropagation()}>\n          <h1 id=\"search-modal-title\" className=\"sr-only\">Global Search</h1>\n          <p id=\"search-modal-description\" className=\"sr-only\">\n            Search through collections, requests, folders, and documentation. Use arrow keys to navigate results and Enter to select.\n          </p>\n          <div aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\">\n            {results.length > 0 && query\n              ? `${results.length} result${results.length === 1 ? '' : 's'} found`\n              : query && results.length === 0\n                ? 'No results found'\n                : ''}\n          </div>\n          <div className=\"command-k-header\">\n            <div className=\"search-input-container\">\n              <IconSearch size={20} className=\"search-icon\" aria-hidden=\"true\" />\n              <input\n                ref={inputRef}\n                type=\"text\"\n                placeholder=\"Search collections, requests, or documentation...\"\n                value={query}\n                onChange={handleQueryChange}\n                onKeyDown={handleKeyNavigation}\n                className=\"search-input\"\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n                aria-label=\"Search collections, requests, or documentation\"\n                aria-expanded={results.length > 0}\n                aria-controls=\"search-results\"\n                aria-activedescendant={results.length > 0 ? `search-result-${selectedIndex}` : undefined}\n                role=\"combobox\"\n                aria-autocomplete=\"list\"\n              />\n              {query && (\n                <button\n                  onClick={clearSearch}\n                  className=\"clear-button\"\n                  aria-label=\"Clear search query\"\n                  type=\"button\"\n                >\n                  <IconX size={16} aria-hidden=\"true\" />\n                </button>\n              )}\n            </div>\n          </div>\n\n          <div\n            className=\"command-k-results\"\n            ref={resultsRef}\n            id=\"search-results\"\n            role=\"listbox\"\n            aria-label=\"Search results\"\n          >\n            {results.length === 0 && query ? (\n              <div className=\"no-results\">\n                <p>\n                  No results found for \"{query}\".\n                  <br />\n                  <span className=\"block mt-2\">\n                    The item might not exist yet, or its collection isn’t mounted. Press <strong>Enter</strong> here (or open it from the sidebar) to mount the collection automatically.\n                  </span>\n                </p>\n              </div>\n            ) : results.length === 0 ? (\n              <div className=\"empty-state\">\n                <p>\n                  No collections are currently mounted or visible.\n                  <br />\n                  <span className=\"block mt-2\">\n                    Mount a collection via the sidebar or this search modal, then try again.\n                  </span>\n                </p>\n              </div>\n            ) : (\n              results.map((result, index) => {\n                const isSelected = index === selectedIndex;\n                const typeLabel = getTypeLabel(result.type);\n\n                return (\n                  <div\n                    key={`${result.type}-${result.item.id || result.item.uid}-${index}`}\n                    id={`search-result-${index}`}\n                    className={`result-item ${isSelected ? 'selected' : ''}`}\n                    onClick={() => handleResultSelection(result)}\n                    data-selected={isSelected}\n                    data-type={result.type}\n                    role=\"option\"\n                    aria-selected={isSelected}\n                    aria-label={`${result.name}, ${typeLabel || result.type}${result.method ? `, ${result.method}` : ''}`}\n                    tabIndex={-1}\n                  >\n                    <div className=\"result-icon\">\n                      {getResultIcon(result.type)}\n                    </div>\n                    <div className=\"result-content\">\n                      <div className=\"result-info\">\n                        <div className=\"result-name\">\n                          {highlightText(result.name, query)}\n                        </div>\n                        <div className=\"result-path\">\n                          {result.type === SEARCH_TYPES.DOCUMENTATION\n                            ? result.description\n                            : result.type === SEARCH_TYPES.REQUEST\n                              ? highlightText(result.item.request?.url || '', query)\n                              : highlightText(result.path, query)}\n                        </div>\n                      </div>\n                      <div className=\"result-badges\">\n                        {result.type === SEARCH_TYPES.REQUEST && result.method && (\n                          <span\n                            className={`method-badge ${result.method.toLowerCase()}`}\n                            aria-label={`HTTP method ${result.method.toUpperCase().replace(/-/g, ' ')}`}\n                          >\n                            {result.method.toUpperCase().replace(/-/g, ' ')}\n                          </span>\n                        )}\n                        {typeLabel && (\n                          <div className=\"result-type\" aria-label={`Item type ${typeLabel}`}>\n                            {typeLabel}\n                          </div>\n                        )}\n                      </div>\n                    </div>\n                  </div>\n                );\n              })\n            )}\n          </div>\n\n          <div className=\"command-k-footer\">\n            <div className=\"keyboard-hints\" role=\"region\" aria-label=\"Keyboard shortcuts\">\n              <span aria-label=\"Use up and down arrows to navigate\">\n                <span className=\"keycap\" aria-hidden=\"true\">↑</span>\n                <span className=\"keycap\" aria-hidden=\"true\">↓</span>\n                <span className=\"hint-label\">to navigate</span>\n              </span>\n              <span aria-label=\"Press Enter to select\">\n                <span className=\"keycap\" aria-hidden=\"true\">↵</span>\n                <span className=\"hint-label\">to select</span>\n              </span>\n              <span aria-label=\"Press Escape to close\">\n                <span className=\"keycap\" aria-hidden=\"true\">esc</span>\n                <span className=\"hint-label\">to close</span>\n              </span>\n            </div>\n          </div>\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default GlobalSearchModal;\n"
  },
  {
    "path": "packages/bruno-app/src/components/GlobalSearchModal/utils/searchUtils.js",
    "content": "import React from 'react';\nimport { SEARCH_TYPES, MATCH_TYPES, SEARCH_CONFIG } from '../constants';\n\nexport const normalizeQuery = (searchQuery) => {\n  return searchQuery.trim().replace(/\\/+/g, '/');\n};\n\nexport const isValidQuery = (normalizedQuery) => {\n  return normalizedQuery\n    && normalizedQuery !== '/'\n    && !(normalizedQuery.length === 1 && !normalizedQuery.match(/[a-zA-Z0-9]/));\n};\n\nexport const highlightText = (text, searchQuery) => {\n  if (!searchQuery) return text;\n\n  try {\n    const escapedQuery = searchQuery.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n    const regex = new RegExp(`(${escapedQuery})`, 'gi');\n    return text.split(regex).map((part, i) =>\n      regex.test(part) ? (\n        <span key={i} className=\"highlight\">{part}</span>\n      ) : part\n    );\n  } catch {\n    return text;\n  }\n};\n\nexport const sortResults = (results) => {\n  return results.sort((a, b) => {\n    // Documentation always first\n    if (a.type === SEARCH_TYPES.DOCUMENTATION) return -1;\n    if (b.type === SEARCH_TYPES.DOCUMENTATION) return 1;\n\n    // Sort by match type priority\n    const matchTypeOrder = {\n      [MATCH_TYPES.COLLECTION]: 0,\n      [MATCH_TYPES.FOLDER]: 1,\n      [MATCH_TYPES.REQUEST]: 2,\n      [MATCH_TYPES.URL]: 3,\n      [MATCH_TYPES.PATH]: 4\n    };\n    const aMatchType = matchTypeOrder[a.matchType] ?? 5;\n    const bMatchType = matchTypeOrder[b.matchType] ?? 5;\n\n    if (aMatchType !== bMatchType) return aMatchType - bMatchType;\n\n    // Sort by type priority\n    const typeOrder = {\n      [SEARCH_TYPES.COLLECTION]: 0,\n      [SEARCH_TYPES.FOLDER]: 1,\n      [SEARCH_TYPES.REQUEST]: 2\n    };\n    const aType = typeOrder[a.type] ?? 3;\n    const bType = typeOrder[b.type] ?? 3;\n\n    if (aType !== bType) return aType - bType;\n\n    // Finally sort alphabetically\n    return a.name.toLowerCase().localeCompare(b.name.toLowerCase());\n  });\n};\n\nexport const getTypeLabel = (type) => {\n  const baseLabels = {\n    [SEARCH_TYPES.DOCUMENTATION]: 'Documentation',\n    [SEARCH_TYPES.COLLECTION]: 'Collection',\n    [SEARCH_TYPES.FOLDER]: 'Folder'\n  };\n\n  return baseLabels[type] || '';\n};\n\nexport const getItemPath = (item, collection, findParentItemInCollection) => {\n  const pathParts = [];\n  let currentItem = item;\n  let depth = 0;\n  const maxDepth = SEARCH_CONFIG.MAX_DEPTH;\n\n  while (currentItem && depth < maxDepth) {\n    pathParts.unshift(currentItem.name);\n    const parent = findParentItemInCollection(collection, currentItem.uid);\n    if (parent) {\n      currentItem = parent;\n      depth++;\n    } else {\n      break;\n    }\n  }\n\n  pathParts.unshift(collection.name);\n  return pathParts.join('/');\n};\n"
  },
  {
    "path": "packages/bruno-app/src/components/Help/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  font-weight: 400;\n  font-size: ${(props) => props.theme.font.size.sm};\n  color: ${(props) => props.theme.text};\n  white-space: normal;\n  background-color: ${(props) => props.theme.infoTip.bg};\n  border: 1px solid ${(props) => props.theme.infoTip.border};\n  box-shadow: ${(props) => props.theme.infoTip.boxShadow};\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Help/index.js",
    "content": "/**\n * The InfoTip components needs to be nuked\n * This component will be the future replacement\n * We should allow icon and placement props to be passed in\n */\n\nimport React, { useState, useRef, useCallback } from 'react';\nimport { createPortal } from 'react-dom';\nimport QuestionCircle from 'components/Icons/QuestionCircle';\nimport InfoCircle from 'components/Icons/InfoCircle';\nimport StyledWrapper from './StyledWrapper';\n\nconst GAP = 8;\n\nconst getPortalPosition = (rect, placement, width) => {\n  switch (placement) {\n    case 'top':\n      return {\n        top: rect.top - GAP,\n        left: rect.left + rect.width / 2 - width / 2,\n        transform: 'translateY(-100%)'\n      };\n    case 'bottom':\n      return {\n        top: rect.bottom + GAP,\n        left: rect.left + rect.width / 2 - width / 2\n      };\n    case 'left':\n      return {\n        top: rect.top + rect.height / 2,\n        left: rect.left - GAP - width,\n        transform: 'translateY(-50%)'\n      };\n    case 'right':\n    default:\n      return {\n        top: rect.top + rect.height / 2,\n        left: rect.right + GAP,\n        transform: 'translateY(-50%)'\n      };\n  }\n};\n\nconst iconMap = {\n  question: QuestionCircle,\n  info: InfoCircle\n};\n\nconst Help = ({ children, width = 200, placement = 'right', icon = 'question', iconComponent: IconComponent, size = 14 }) => {\n  const [showTooltip, setShowTooltip] = useState(false);\n  const [position, setPosition] = useState(null);\n  const iconRef = useRef(null);\n  const ResolvedIcon = IconComponent || iconMap[icon] || QuestionCircle;\n\n  const handleMouseEnter = useCallback(() => {\n    if (iconRef.current) {\n      const rect = iconRef.current.getBoundingClientRect();\n      setPosition(getPortalPosition(rect, placement, width));\n    }\n    setShowTooltip(true);\n  }, [placement, width]);\n\n  return (\n    <div className=\"flex items-center\">\n      <span\n        ref={iconRef}\n        className=\"flex items-center\"\n        onMouseEnter={handleMouseEnter}\n        onMouseLeave={() => setShowTooltip(false)}\n      >\n        <ResolvedIcon size={size} />\n      </span>\n      {showTooltip && position && createPortal(\n        <StyledWrapper\n          className=\"z-50 rounded-md p-3\"\n          style={{\n            position: 'fixed',\n            ...position,\n            width: `${width}px`\n          }}\n        >\n          {children}\n        </StyledWrapper>,\n        document.body\n      )}\n    </div>\n  );\n};\n\nexport default Help;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Icons/CloseAll/index.js",
    "content": "import React from 'react';\n\nconst CloseAllIcon = ({ size = 18, strokeWidth = 1.5, className = '', ...props }) => {\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" {...props}>\n      <path\n        d=\"M7 7L7 5C7 4.46957 7.21072 3.96086 7.58579 3.58579C7.96086 3.21071 8.46957 3 9 3L19 3C19.5304 3 20.0391 3.21071 20.4142 3.58579C20.7893 3.96086 21 4.46957 21 5L21 15C21 15.5304 20.7893 16.0391 20.4142 16.4142C20.0391 16.7893 19.5304 17 19 17L17 17M17 19C17 19.5304 16.7893 20.0391 16.4142 20.4142C16.0391 20.7893 15.5304 21 15 21L5 21C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19L3 9C3 8.46957 3.21072 7.96086 3.58579 7.58579C3.96086 7.21071 4.46957 7 5 7L15 7C15.5304 7 16.0391 7.21071 16.4142 7.58579C16.7893 7.96086 17 8.46957 17 9L17 19Z\"\n        stroke=\"currentColor\"\n        strokeWidth={strokeWidth}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n      <path\n        d=\"M13 11L7 17M7 11L13 17\"\n        stroke=\"currentColor\"\n        strokeWidth={strokeWidth}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n};\n\nexport default CloseAllIcon;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Icons/Dot/index.js",
    "content": "import React from 'react';\n\nconst DotIcon = ({ width }) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={width}\n      height={width}\n      viewBox=\"0 0 24 24\"\n      strokeWidth=\"1.5\"\n      stroke=\"currentColor\"\n      fill=\"none\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className=\"inline-block\"\n    >\n      <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\" />\n      <path d=\"M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z\" strokeWidth=\"0\" fill=\"currentColor\" />\n    </svg>\n  );\n};\n\nexport default DotIcon;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Icons/ExampleIcon/index.js",
    "content": "import React from 'react';\n\nconst ExampleIcon = ({ color = 'currentColor', size = 16, ...props }) => {\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" {...props}>\n      <g clipPath=\"url(#clip0_486_1191)\">\n        <path d=\"M2.66699 3.33329C2.66699 3.15648 2.73723 2.98691 2.86225 2.86189C2.98728 2.73686 3.15685 2.66663 3.33366 2.66663H12.667C12.8438 2.66663 13.0134 2.73686 13.1384 2.86189C13.2634 2.98691 13.3337 3.15648 13.3337 3.33329V12.6666C13.3337 12.8434 13.2634 13.013 13.1384 13.138C13.0134 13.2631 12.8438 13.3333 12.667 13.3333H3.33366C3.15685 13.3333 2.98728 13.2631 2.86225 13.138C2.73723 13.013 2.66699 12.8434 2.66699 12.6666V3.33329Z\" stroke={color} strokeWidth=\"1\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n        <path d=\"M9.33366 5.33337H6.66699V10.6667H9.33366\" stroke={color} strokeWidth=\"1\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n        <path d=\"M9.33366 8H6.66699\" stroke={color} strokeWidth=\"1\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n      </g>\n      <defs>\n        <clipPath id=\"clip0_486_1191\">\n          <rect width={size} height={size} fill={color} />\n        </clipPath>\n      </defs>\n    </svg>\n\n  );\n};\n\nexport default ExampleIcon;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Icons/Grpc/index.js",
    "content": "import React from 'react';\n\n// UNARY - Single request, single response (Blue)\nexport const IconGrpcUnary = ({ size = 18, strokeWidth = 1.5, className = '', color = '#3B82F6' }) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={size}\n    height={size}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    className={className}\n  >\n    <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\" />\n    {/* Request arrow (top) - right */}\n    <path d=\"M3 8h18\" stroke={color} strokeWidth={strokeWidth} />\n    <path d=\"M18 5l3 3l-3 3\" stroke={color} strokeWidth={strokeWidth} />\n    {/* Response arrow (bottom) - left */}\n    <path d=\"M21 16h-18\" stroke={color} strokeWidth={strokeWidth} />\n    <path d=\"M6 13l-3 3l3 3\" stroke={color} strokeWidth={strokeWidth} />\n  </svg>\n);\n\n// CLIENT_STREAMING - Streaming request, single response (Purple)\nexport const IconGrpcClientStreaming = ({ size = 18, strokeWidth = 1.5, className = '', color = '#8B5CF6' }) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={size}\n    height={size}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    className={className}\n  >\n    <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\" />\n    {/* Request arrow (top) - right with double heads */}\n    <path d=\"M3 8h18\" stroke={color} strokeWidth={strokeWidth} />\n    <path d=\"M18 5l3 3l-3 3\" stroke={color} strokeWidth={strokeWidth} />\n    <path d=\"M14 5l3 3l-3 3\" stroke={color} strokeWidth={strokeWidth} />\n    {/* Response arrow (bottom) - left */}\n    <path d=\"M21 16h-18\" stroke={color} strokeWidth={strokeWidth} />\n    <path d=\"M6 13l-3 3l3 3\" stroke={color} strokeWidth={strokeWidth} />\n  </svg>\n);\n\n// SERVER_STREAMING - Single request, streaming response (Green)\nexport const IconGrpcServerStreaming = ({ size = 18, strokeWidth = 1.5, className = '', color = '#10B981' }) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={size}\n    height={size}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    className={className}\n  >\n    <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\" />\n    {/* Request arrow (top) - right */}\n    <path d=\"M3 8h18\" stroke={color} strokeWidth={strokeWidth} />\n    <path d=\"M18 5l3 3l-3 3\" stroke={color} strokeWidth={strokeWidth} />\n    {/* Response arrow (bottom) - left with double heads */}\n    <path d=\"M21 16h-18\" stroke={color} strokeWidth={strokeWidth} />\n    <path d=\"M6 13l-3 3l3 3\" stroke={color} strokeWidth={strokeWidth} />\n    <path d=\"M10 13l-3 3l3 3\" stroke={color} strokeWidth={strokeWidth} />\n  </svg>\n);\n\n// BIDI_STREAMING - Streaming request, streaming response (Orange)\nexport const IconGrpcBidiStreaming = ({ size = 18, strokeWidth = 1.5, className = '', color = '#F97316' }) => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width={size}\n    height={size}\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n    className={className}\n  >\n    <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\" />\n    {/* Request arrow (top) - right with double heads */}\n    <path d=\"M3 8h18\" stroke={color} strokeWidth={strokeWidth} />\n    <path d=\"M18 5l3 3l-3 3\" stroke={color} strokeWidth={strokeWidth} />\n    <path d=\"M14 5l3 3l-3 3\" stroke={color} strokeWidth={strokeWidth} />\n    {/* Response arrow (bottom) - left with double heads */}\n    <path d=\"M21 16h-18\" stroke={color} strokeWidth={strokeWidth} />\n    <path d=\"M6 13l-3 3l3 3\" stroke={color} strokeWidth={strokeWidth} />\n    <path d=\"M10 13l-3 3l3 3\" stroke={color} strokeWidth={strokeWidth} />\n  </svg>\n);\n"
  },
  {
    "path": "packages/bruno-app/src/components/Icons/IconAlertTriangleFilled/index.js",
    "content": "import React from 'react';\n\nconst IconAlertTriangleFilled = ({ size = 16, ...props }) => {\n  return (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" {...props}>\n      <path\n        d=\"M12 1.67c.955 0 1.845 .467 2.39 1.247l.105 .16l8.114 13.548a2.914 2.914 0 0 1 -2.307 4.363l-.195 .008h-16.225a2.914 2.914 0 0 1 -2.582 -4.2l.099 -.185l8.11 -13.538a2.914 2.914 0 0 1 2.491 -1.403zm.01 13.33l-.127 .007a1 1 0 0 0 0 1.986l.117 .007l.127 -.007a1 1 0 0 0 0 -1.986l-.117 -.007zm-.01 -7a1 1 0 0 0 -.993 .883l-.007 .117v4l.007 .117a1 1 0 0 0 1.986 0l.007 -.117v-4l-.007 -.117a1 1 0 0 0 -.993 -.883z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n};\n\nexport default IconAlertTriangleFilled;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Icons/IconBottombarToggle/index.js",
    "content": "import React from 'react';\n\nconst IconBottombarToggle = ({ collapsed = false, size = 16, strokeWidth = 1.5, className = '', ...rest }) => {\n  return (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={strokeWidth} strokeLinecap=\"round\" strokeLinejoin=\"round\" className={`icon icon-tabler icons-tabler-outline icon-tabler-layout-bottombar ${className}`} {...rest}>\n      <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\" />\n      <path d=\"M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z\" />\n      <path d=\"M4 15l16 0\" />\n      {!collapsed && (\n        <rect x=\"4.6\" y=\"15.6\" width=\"14.8\" height=\"2.8\" rx=\"0.8\" fill=\"currentColor\" />\n      )}\n    </svg>\n  );\n};\n\nexport default IconBottombarToggle;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Icons/IconCaretDown/index.js",
    "content": "import React from 'react';\n\nconst IconCaretDown = ({ color = '#8C8C8C', ...props }) => {\n  return (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\">\n      <g clipPath=\"url(#clip0_464_9256)\">\n        <path d=\"M10.5444 5.75H4.46004C4.26888 5.7509 4.08142 5.78521 3.91637 5.84952C3.75132 5.91383 3.61447 6.00587 3.51947 6.11647C3.42448 6.22706 3.37466 6.35234 3.375 6.47978C3.37534 6.60723 3.42583 6.73238 3.52142 6.84275L6.56492 10.23C6.66228 10.3372 6.79942 10.4258 6.96311 10.4874C7.1268 10.5489 7.31151 10.5813 7.49945 10.5814C7.68739 10.5816 7.8722 10.5494 8.03608 10.4881C8.19995 10.4267 8.33735 10.3383 8.43504 10.2312L11.4763 6.8465C11.573 6.73635 11.6246 6.61118 11.626 6.48355C11.6273 6.35591 11.5783 6.23028 11.4839 6.11924C11.3895 6.0082 11.253 5.91564 11.088 5.85084C10.9231 5.78603 10.7359 5.75126 10.5444 5.75Z\" fill=\"#8C8C8C\" />\n      </g>\n      <defs>\n        <clipPath id=\"clip0_464_9256\">\n          <rect width=\"9\" height=\"6\" fill=\"white\" transform=\"translate(3 5)\" />\n        </clipPath>\n      </defs>\n    </svg>\n  );\n};\n\nexport default IconCaretDown;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Icons/IconCheckMark/index.js",
    "content": "import React from 'react';\n\nconst IconCheckMark = ({ color = '#cccccc', size = 16, ...props }) => {\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 16 17\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" {...props}>\n      <path d=\"M3.3335 8.49996L6.66683 11.8333L13.3335 5.16663\" stroke={color} strokeWidth=\"1.33333\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n    </svg>\n  );\n};\n\nexport default IconCheckMark;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Icons/IconEdit/index.js",
    "content": "import React from 'react';\n\nconst IconEdit = ({ color = '#F39D0E', size = 16, ...props }) => {\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" {...props}>\n      <g clipPath=\"url(#clip0_464_9527)\">\n        <path d=\"M12.6665 13.3332H5.66654L2.85988 10.4665C2.73571 10.3416 2.66602 10.1727 2.66602 9.99654C2.66602 9.82042 2.73571 9.65145 2.85988 9.52654L9.52654 2.85988C9.65145 2.73571 9.82042 2.66602 9.99654 2.66602C10.1727 2.66602 10.3416 2.73571 10.4665 2.85988L13.7999 6.19321C13.924 6.31812 13.9937 6.48709 13.9937 6.66321C13.9937 6.83933 13.924 7.0083 13.7999 7.13321L7.66654 13.3332\" stroke={color} strokeWidth=\"1.33333\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n        <path d=\"M11.9998 8.86663L7.7998 4.66663\" stroke={color} strokeWidth=\"1.33333\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n      </g>\n      <defs>\n        <clipPath id=\"clip0_464_9527\">\n          <rect width={size} height={size} fill=\"white\" />\n        </clipPath>\n      </defs>\n    </svg>\n  );\n};\n\nexport default IconEdit;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Icons/IconSidebarToggle/index.js",
    "content": "import React from 'react';\n\nconst IconSidebarToggle = ({ collapsed = false, size = 16, strokeWidth = 1.5, className = '', ...rest }) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={size}\n      height={size}\n      viewBox=\"0 0 24 24\"\n      strokeWidth={strokeWidth}\n      stroke=\"currentColor\"\n      fill=\"none\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className={`icon icon-tabler icons-tabler-outline icon-tabler-layout-sidebar ${className}`}\n      {...rest}\n    >\n      <path stroke=\"none\" d=\"M0 0h24v24H0z\" fill=\"none\" />\n      <path d=\"M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z\" />\n      <path d=\"M9 4l0 16\" />\n      {!collapsed && (\n        <rect x=\"4.6\" y=\"4.6\" width=\"4.8\" height=\"14.8\" rx=\"0.8\" fill=\"currentColor\" />\n      )}\n    </svg>\n  );\n};\n\nexport default IconSidebarToggle;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Icons/InfoCircle/index.js",
    "content": "import React from 'react';\n\nconst InfoCircle = ({ size = 14 }) => {\n  return (\n    <svg\n      tabIndex=\"-1\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={size}\n      height={size}\n      fill=\"currentColor\"\n      className=\"inline-block ml-2 cursor-pointer\"\n      viewBox=\"0 0 16 16\"\n    >\n      <path d=\"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z\" />\n      <path d=\"m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z\" />\n    </svg>\n  );\n};\n\nexport default InfoCircle;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Icons/OpenAPILogo/index.js",
    "content": "const OpenApiLogo = () => {\n  return (\n    <svg width=\"28\" height=\"28\" viewBox=\"0 0 128 128\" xmlns=\"http://www.w3.org/2000/svg\">\n      <path\n        style={{\n          fill: '#91d400',\n          fillOpacity: 1,\n          fillRule: 'nonzero',\n          stroke: 'none'\n        }}\n        d=\"M43.125 51.148H20.781l.012.325c.012.21.027.418.039.625.004.09.008.18.016.27a41.442 41.442 0 0 0 .164 1.687c0 .027.004.05.008.078.035.285.07.574.113.86 0 .003 0 .007.004.01a36.98 36.98 0 0 0 1.152 5.255c.004.008.008.012.008.02a27.978 27.978 0 0 0 .265.859c.004.015.012.031.016.047.078.242.164.484.246.73.024.059.043.121.067.184.074.207.148.418.226.629.04.093.074.187.11.285.07.172.136.343.203.52.054.128.11.257.164.39.054.133.113.27.168.406.074.164.148.328.218.492.047.102.09.2.133.297.09.195.184.395.278.59l.093.191c.11.227.223.45.336.672.02.035.035.07.051.102a36.344 36.344 0 0 0 .41.773c.028.051.059.102.086.149L44.45 56.156l.07-.043a15.031 15.031 0 0 1-1.394-4.965Zm0 0\"\n        transform=\"translate(-26.793 -.606) scale(1.44332)\"\n      />\n      <path\n        style={{\n          fill: '#91d400',\n          fillOpacity: 1,\n          fillRule: 'nonzero',\n          stroke: 'none'\n        }}\n        d=\"m22.563 61.137-.727.207.008.02zm0 0\"\n        transform=\"translate(-26.793 -.606) scale(1.44332)\"\n      />\n      <path\n        style={{\n          fill: '#4c5930',\n          fillOpacity: 1,\n          fillRule: 'nonzero',\n          stroke: 'none'\n        }}\n        d=\"m48.613 61.355-.05.055-15.739 15.664c.082.074.16.149.242.223.149.133.297.266.446.394.078.067.152.137.23.204.18.152.36.3.54.449.046.043.097.082.144.12a21.669 21.669 0 0 0 .691.556c.227.175.45.343.676.515.012.008.02.012.027.02.95.707 1.93 1.367 2.946 1.98.035.024.07.043.105.067l.578.34c.117.066.239.132.356.203.113.062.222.125.336.187.207.11.41.223.617.328a35.567 35.567 0 0 0 1.824.887l.559-1.348 7.918-19.133.027-.07a15.337 15.337 0 0 1-2.473-1.64Zm0 0\"\n        transform=\"translate(-26.793 -.606) scale(1.44332)\"\n      />\n      <path\n        style={{\n          fill: '#68a338',\n          fillOpacity: 1,\n          fillRule: 'nonzero',\n          stroke: 'none'\n        }}\n        d=\"M46.977 59.797a16.778 16.778 0 0 1-.899-1.102c-.152-.203-.3-.406-.437-.617a15.93 15.93 0 0 1-.41-.633L26.124 68.902c.297.485.602.957.914 1.422.012.016.02.035.031.051l.012.016c.008.015.02.03.027.046.004.004.004.004.004.008.028.035.051.07.078.11 0 .004 0 .004.004.007v.004a35.121 35.121 0 0 0 1.051 1.465c.008.012.016.02.02.031.156.2.308.403.464.602.024.027.043.05.063.078.164.203.328.406.496.61.04.046.078.093.117.144.153.18.305.36.457.535.067.078.137.153.203.227.13.148.262.297.395.445.074.082.152.16.226.242.032.035.067.075.102.11l.293.316.121.121c.176.184.352.363.531.54l15.758-15.684a22.18 22.18 0 0 1-.515-.551Zm0 0\"\n        transform=\"translate(-26.793 -.606) scale(1.44332)\"\n      />\n      <path\n        style={{\n          fill: '#4c5930',\n          fillOpacity: 1,\n          fillRule: 'nonzero',\n          stroke: 'none'\n        }}\n        d=\"M67.867 61.348c-.176.136-.347.273-.527.406l.039.066L78.87 80.805a38.54 38.54 0 0 0 1.57-1.078 38.099 38.099 0 0 0 3.227-2.653L67.93 61.41Zm0 0\"\n        transform=\"translate(-26.793 -.606) scale(1.44332)\"\n      />\n      <path\n        style={{\n          fill: '#91d400',\n          fillOpacity: 1,\n          fillRule: 'nonzero',\n          stroke: 'none'\n        }}\n        d=\"m77.418 81.707.023-.012-.023.012zm.023-.012c.051-.027.102-.054.153-.086l-.004-.004c-.05.032-.098.06-.149.09zm-.023.012-.008.004zm-.039-.039.027.047zm.039.039.023-.012-.023.012zm-.016.012.004-.004zm.008-.009-.004.005c.004-.004.008-.004.012-.008-.004.004-.004.004-.008.004zm0 0\"\n        transform=\"translate(-26.793 -.606) scale(1.44332)\"\n      />\n      <path\n        style={{\n          fill: '#91d400',\n          fillOpacity: 1,\n          fillRule: 'nonzero',\n          stroke: 'none'\n        }}\n        d=\"M77.441 81.695c.051-.03.102-.054.153-.086-.051.032-.102.059-.153.086zm.149-.09.004.004zm-.2.118.005-.004zm-.19-.763-.391-.644-10.727-17.722c-.215.133-.437.25-.66.367-.223.121-.45.23-.676.34a15.303 15.303 0 0 1-6.523 1.469c-1.465 0-2.93-.211-4.344-.63-.238-.074-.473-.167-.711-.25-.238-.085-.48-.156-.715-.253l-7.91 19.117-.309.75-.265.644h-.004l.062.024c.024.012.043.016.067.027h.004c.004 0 .007.004.011.004.188.078.375.145.563.215.238.094.473.184.707.27.121.042.238.097.36.136a38.13 38.13 0 0 0 7.648 1.824c.105.012.207.028.308.04l.32.035c.2.023.4.047.602.066l.149.012c.25.023.496.043.742.062.082.004.168.008.25.016.219.016.433.027.648.039.133.004.266.008.399.016.172.004.343.011.515.015.246.008.496.008.746.012h.176c2.082 0 4.164-.172 6.223-.516l.105-.015c.215-.04.434-.078.653-.117.125-.024.246-.047.37-.075.126-.023.255-.05.384-.078.21-.043.421-.09.632-.14l.118-.024a37.8 37.8 0 0 0 8.992-3.34c.187-.097.367-.207.554-.308.22-.121.438-.246.657-.371.152-.086.308-.164.457-.254l.004-.004h.004l.003-.004c.004 0 .004 0 .004-.004l-.027-.047.027.047h.004c.004-.004.008-.004.008-.004.008-.008.016-.012.023-.016.051-.03.102-.058.149-.09zM48.625 37.938c.172-.141.348-.274.523-.407l-.039-.066L37.621 18.48c-.535.348-1.062.704-1.578 1.082a37.213 37.213 0 0 0-3.219 2.649l15.739 15.664Zm0 0\"\n        transform=\"translate(-26.793 -.606) scale(1.44332)\"\n      />\n      <path\n        style={{\n          fill: '#4c5930',\n          fillOpacity: 1,\n          fillRule: 'nonzero',\n          stroke: 'none'\n        }}\n        d=\"M31.73 23.254c-.18.18-.347.363-.523.543-.172.18-.352.36-.523.543a38.163 38.163 0 0 0-3.32 4.125c-.106.156-.216.312-.321.469-.11.164-.219.328-.324.496-.04.058-.078.12-.117.18a37.099 37.099 0 0 0-5.82 18.527c-.009.25-.016.504-.02.754s-.012.5-.012.75h22.29c0-.25.023-.5.034-.75.012-.254.016-.504.043-.754a15.002 15.002 0 0 1 3.36-8.078c.16-.192.34-.375.507-.559.172-.188.329-.379.508-.559zm45.993-5.508a46.43 46.43 0 0 0-.684-.402c-.117-.067-.23-.133-.348-.196-.117-.066-.23-.128-.347-.195-.2-.11-.403-.219-.606-.324-.031-.016-.062-.031-.093-.05a38.014 38.014 0 0 0-4.02-1.798c-.035-.015-.074-.027-.11-.039a37.527 37.527 0 0 0-8.41-2.102c-.101-.015-.207-.027-.312-.042l-.313-.036a31.754 31.754 0 0 0-.777-.078c-.238-.023-.48-.043-.719-.062-.093-.004-.187-.012-.28-.016-.204-.015-.415-.027-.618-.039l-.328-.012v22.239c1.144.117 2.281.363 3.383.734l16.445-16.367a41.117 41.117 0 0 0-1.863-1.215zm0 0\"\n        transform=\"translate(-26.793 -.606) scale(1.44332)\"\n      />\n      <path\n        style={{\n          fill: '#68a338',\n          fillOpacity: 1,\n          fillRule: 'nonzero',\n          stroke: 'none'\n        }}\n        d=\"m38.898 17.68.391.644zm18.59-5.34c-.25.004-.504.004-.754.015a38.117 38.117 0 0 0-4.71.48c-.036.009-.07.013-.106.02-.219.036-.434.079-.652.118l-.371.07c-.13.027-.258.05-.383.082a36.99 36.99 0 0 0-.75.164 37.925 37.925 0 0 0-8.996 3.336c-.184.098-.368.21-.551.312-.219.118-.438.243-.66.368-.16.093-.325.18-.489.277h-.003a.162.162 0 0 1-.036.02c-.043.027-.086.046-.129.074l.004.004.387.644 11.117 18.367c.215-.132.438-.25.66-.367a15.15 15.15 0 0 1 5.668-1.73c.25-.028.5-.047.754-.063.25-.011.504-.023.758-.023V12.324c-.254 0-.504.008-.758.016zm0 0\"\n        transform=\"translate(-26.793 -.606) scale(1.44332)\"\n      />\n      <path\n        style={{\n          fill: '#4c5930',\n          fillOpacity: 1,\n          fillRule: 'nonzero',\n          stroke: 'none'\n        }}\n        d=\"m95.695 47.809-.035-.598c-.008-.102-.012-.2-.02-.3l-.058-.704c-.008-.059-.012-.121-.016-.18a53.501 53.501 0 0 0-.086-.785c-.003-.023-.003-.043-.007-.062 0-.012 0-.02-.004-.032-.035-.28-.07-.562-.114-.843 0-.012 0-.02-.003-.028a36.772 36.772 0 0 0-1.153-5.246 47.929 47.929 0 0 0-.258-.832c-.011-.035-.023-.07-.03-.105-.083-.242-.161-.48-.247-.719-.023-.063-.043-.129-.066-.195a39.302 39.302 0 0 0-.34-.91c-.067-.172-.133-.344-.2-.512-.054-.137-.109-.27-.163-.403-.055-.132-.114-.261-.168-.394l-.223-.504-.129-.285c-.09-.2-.188-.402-.281-.602-.032-.058-.059-.12-.086-.18-.113-.23-.227-.456-.34-.683-.016-.031-.031-.062-.05-.094-.13-.25-.259-.5-.395-.746l-.012-.023a36.958 36.958 0 0 0-2.133-3.446L72.628 44.77c.376 1.097.618 2.23.735 3.37h22.344l-.012-.331zm0 0\"\n        transform=\"translate(-26.793 -.606) scale(1.44332)\"\n      />\n      <path\n        style={{\n          fill: '#68a338',\n          fillOpacity: 1,\n          fillRule: 'nonzero',\n          stroke: 'none'\n        }}\n        d=\"M73.45 49.64c0 .255-.024.505-.036.755-.012.25-.016.503-.043.753a15.002 15.002 0 0 1-3.36 8.079c-.16.191-.335.375-.507.558-.168.188-.328.38-.508.559L84.758 76.03c.18-.18.347-.363.523-.543.172-.183.352-.363.52-.547a36.965 36.965 0 0 0 3.195-3.937c.04-.055.074-.11.11-.16.117-.168.234-.34.347-.508.102-.152.203-.3.3-.453.048-.074.095-.153.142-.223a37.055 37.055 0 0 0 5.808-18.515c.012-.25.016-.5.024-.75.003-.25.011-.5.011-.754zm0 0\"\n        transform=\"translate(-26.793 -.606) scale(1.44332)\"\n      />\n      <path\n        style={{\n          fill: '#3f3f42',\n          fillOpacity: 1,\n          fillRule: 'nonzero',\n          stroke: 'none'\n        }}\n        d=\"M102.36 5.734c-4.079-4.058-10.688-4.058-14.766 0-3.254 3.235-3.903 8.075-1.965 11.961l-22.746 22.64c-3.906-1.925-8.77-1.28-12.024 1.958-4.078 4.059-4.074 10.64 0 14.7 4.082 4.058 10.692 4.054 14.77 0a10.356 10.356 0 0 0 1.965-11.966l22.746-22.64c3.906 1.93 8.765 1.281 12.02-1.957a10.355 10.355 0 0 0 0-14.696zm0 0\"\n        transform=\"translate(-26.793 -.606) scale(1.44332)\"\n      />\n    </svg>\n  );\n};\n\nexport default OpenApiLogo;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Icons/OpenAPISync/index.js",
    "content": "import React from 'react';\n\nconst OpenAPISyncIcon = ({ size = 16, color = 'currentColor', ...props }) => {\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 46 46\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" {...props}>\n      <path d=\"M39.1499 0.0455742C36.8449 0.458965 35.0171 2.12048 34.4267 4.37029C34.2407 5.06987 34.2245 6.31799 34.3944 7.02553C34.4591 7.30377 34.4914 7.56612 34.4672 7.61382C34.4348 7.66152 34.4429 7.67742 34.4914 7.65357C34.5319 7.62972 34.6208 7.76486 34.6936 7.94771L34.823 8.28955L32.9305 10.1498C31.8872 11.1753 30.9976 12.0021 30.9491 11.9862C30.9006 11.9783 30.8844 12.0021 30.9087 12.0419C30.9329 12.0816 29.6227 13.4172 27.9971 15.0072C26.3716 16.5892 24.6085 18.3222 24.0828 18.8469L23.1284 19.793L22.3278 19.4193C21.1794 18.8787 20.4515 18.7118 19.2707 18.7118C17.726 18.7038 16.715 18.99 15.4776 19.793C14.2079 20.6118 13.3506 21.5499 12.7198 22.8059C12.1779 23.8792 12.0081 24.6503 12 25.962C12 27.0671 12.0809 27.5202 12.4448 28.506C13.205 30.5411 15.138 32.1788 17.5319 32.8068C18.5509 33.0771 20.1765 33.0612 21.2683 32.775C25.5224 31.6621 27.803 27.393 26.2583 23.426C26.1208 23.0683 26.0157 22.7582 26.0319 22.7423C26.048 22.7264 27.0994 21.6771 28.3692 20.421C29.6389 19.1649 31.1108 17.6863 31.6446 17.1377C34.2488 14.4666 37.8397 10.8573 37.8882 10.8573C37.9044 10.8573 38.058 10.9289 38.2198 11.0084C39.1984 11.5013 40.8402 11.5887 41.9967 11.223C42.8782 10.9448 43.5737 10.5235 44.245 9.87157C45.4581 8.67909 45.9919 7.44687 46 5.80126C46 4.37824 45.539 3.16191 44.5442 1.96148C44.2774 1.63554 44.059 1.39705 44.059 1.42885C44.059 1.46065 43.9134 1.36525 43.7274 1.2142C43.2664 0.824657 42.4253 0.403316 41.7136 0.212521C41.01 0.0217247 39.7645 -0.0577736 39.1499 0.0455742Z\" fill={color} />\n      <circle cx=\"20\" cy=\"26\" r=\"18.5\" stroke={color} strokeWidth=\"3\" />\n      <path d=\"M26 15.4988C22 13.4999 17.793 13.752 15.2009 14.6172C12.6088 15.4823 10.3896 17.2063 8.91029 19.504C7.431 21.8016 6.78025 24.5354 7.06564 27.2531C7.35103 29.9709 8.55548 32.5098 10.4798 34.4501C12.4041 36.3904 14.933 37.6157 17.6483 37.9235C20.3636 38.2314 23.1027 37.6032 25.4125 36.1429C27.7223 34.6826 29.6135 32.5849 30.5 30C31.3865 27.4151 31.5 25 31 20L28 22.5C28.5 24.5 28.0118 26.9632 27.359 28.8667C26.7061 30.7702 25.4231 32.3939 23.7222 33.4693C22.0212 34.5446 20.0042 35.0072 18.0046 34.7805C16.0051 34.5539 14.1427 33.6515 12.7257 32.2227C11.3086 30.7939 10.4216 28.9242 10.2115 26.9228C10.0013 24.9214 10.4805 22.9083 11.5699 21.2163C12.6592 19.5242 14.2935 18.2547 16.2023 17.6176C18.1112 16.9805 20 16.9999 23.5 17.9999L26 15.4988Z\" fill={color} />\n    </svg>\n  );\n};\n\nexport default OpenAPISyncIcon;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Icons/OpenCollectionIcon/index.js",
    "content": "import React, { useId } from 'react';\nimport styled from 'styled-components';\n\nconst StyledSvg = styled.svg`\n  .icon-stroke {\n    stroke: ${(props) => props.theme.text};\n  }\n  .icon-fill {\n    fill: ${(props) => props.theme.text};\n  }\n`;\n\nconst OpenCollectionIcon = ({ size = 28 }) => {\n  const clipId = useId();\n\n  return (\n    <StyledSvg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={size}\n      height={size}\n      viewBox=\"0 0 567 567\"\n      preserveAspectRatio=\"xMidYMid meet\"\n    >\n      <g transform=\"matrix(1, 0, 0, 1, 93, 56)\">\n        <clipPath id={clipId}>\n          <rect x=\"0\" width=\"418\" y=\"0\" height=\"455\" />\n        </clipPath>\n        <g clipPath={`url(#${clipId})`}>\n          <path\n            className=\"icon-fill\"\n            d=\"M 249.175781 222.132812 C 249.175781 224.011719 249.085938 225.890625 248.90625 227.761719 C 248.722656 229.632812 248.453125 231.492188 248.09375 233.335938 C 247.738281 235.179688 247.289062 237 246.753906 238.800781 C 246.222656 240.601562 245.601562 242.367188 244.898438 244.105469 C 244.195312 245.84375 243.40625 247.542969 242.539062 249.199219 C 241.671875 250.859375 240.726562 252.46875 239.707031 254.035156 C 238.683594 255.597656 237.589844 257.105469 236.421875 258.558594 C 235.253906 260.011719 234.019531 261.40625 232.71875 262.734375 C 231.417969 264.0625 230.054688 265.324219 228.632812 266.519531 C 227.210938 267.710938 225.734375 268.832031 224.203125 269.875 C 222.671875 270.921875 221.097656 271.886719 219.472656 272.773438 C 217.851562 273.660156 216.1875 274.460938 214.488281 275.179688 C 212.789062 275.902344 211.058594 276.535156 209.296875 277.078125 C 207.535156 277.625 205.753906 278.082031 203.949219 278.449219 C 202.144531 278.816406 200.324219 279.089844 198.492188 279.277344 C 196.664062 279.460938 194.828125 279.550781 192.988281 279.550781 C 191.144531 279.550781 189.308594 279.460938 187.480469 279.277344 C 185.648438 279.089844 183.828125 278.816406 182.023438 278.449219 C 180.21875 278.082031 178.4375 277.625 176.675781 277.078125 C 174.914062 276.535156 173.183594 275.902344 171.484375 275.179688 C 169.785156 274.460938 168.121094 273.660156 166.5 272.773438 C 164.875 271.886719 163.300781 270.921875 161.769531 269.875 C 160.238281 268.832031 158.761719 267.710938 157.339844 266.519531 C 155.917969 265.324219 154.554688 264.0625 153.253906 262.734375 C 151.953125 261.40625 150.71875 260.011719 149.550781 258.558594 C 148.386719 257.105469 147.289062 255.597656 146.265625 254.035156 C 145.246094 252.46875 144.300781 250.859375 143.433594 249.199219 C 142.566406 247.542969 141.78125 245.84375 141.074219 244.105469 C 140.371094 242.367188 139.75 240.601562 139.21875 238.800781 C 138.683594 237 138.238281 235.179688 137.878906 233.335938 C 137.519531 231.492188 137.25 229.632812 137.070312 227.761719 C 136.886719 225.890625 136.796875 224.011719 136.796875 222.132812 C 136.796875 220.253906 136.886719 218.375 137.070312 216.503906 C 137.25 214.632812 137.519531 212.773438 137.878906 210.929688 C 138.238281 209.085938 138.683594 207.265625 139.21875 205.464844 C 139.75 203.664062 140.371094 201.898438 141.074219 200.160156 C 141.78125 198.421875 142.566406 196.722656 143.433594 195.066406 C 144.300781 193.40625 145.246094 191.796875 146.265625 190.230469 C 147.289062 188.667969 148.386719 187.160156 149.550781 185.707031 C 150.71875 184.253906 151.953125 182.859375 153.253906 181.53125 C 154.554688 180.203125 155.917969 178.941406 157.339844 177.746094 C 158.761719 176.554688 160.238281 175.433594 161.769531 174.390625 C 163.300781 173.34375 164.875 172.378906 166.5 171.492188 C 168.121094 170.605469 169.785156 169.804688 171.484375 169.085938 C 173.183594 168.363281 174.914062 167.730469 176.675781 167.1875 C 178.4375 166.640625 180.21875 166.183594 182.023438 165.816406 C 183.828125 165.449219 185.648438 165.175781 187.480469 164.988281 C 189.308594 164.804688 191.144531 164.714844 192.988281 164.714844 C 194.828125 164.714844 196.664062 164.804688 198.492188 164.988281 C 200.324219 165.175781 202.144531 165.449219 203.949219 165.816406 C 205.753906 166.183594 207.535156 166.640625 209.296875 167.1875 C 211.058594 167.730469 212.789062 168.363281 214.488281 169.085938 C 216.1875 169.804688 217.851562 170.605469 219.472656 171.492188 C 221.097656 172.378906 222.671875 173.34375 224.203125 174.390625 C 225.734375 175.433594 227.210938 176.554688 228.632812 177.746094 C 230.054688 178.941406 231.417969 180.203125 232.71875 181.53125 C 234.019531 182.859375 235.253906 184.253906 236.421875 185.707031 C 237.589844 187.160156 238.683594 188.667969 239.707031 190.230469 C 240.726562 191.796875 241.671875 193.40625 242.539062 195.066406 C 243.40625 196.722656 244.195312 198.421875 244.898438 200.160156 C 245.601562 201.898438 246.222656 203.664062 246.753906 205.464844 C 247.289062 207.265625 247.738281 209.085938 248.09375 210.929688 C 248.453125 212.773438 248.722656 214.632812 248.90625 216.503906 C 249.085938 218.375 249.175781 220.253906 249.175781 222.132812 Z M 249.175781 222.132812\"\n            fillOpacity=\"1\"\n            fillRule=\"nonzero\"\n          />\n          <path\n            className=\"icon-fill\"\n            d=\"M 331.925781 84.105469 C 304.367188 55.941406 269.136719 36.925781 230.835938 29.546875 C 192.535156 22.164062 152.945312 26.757812 117.242188 42.726562 C 81.535156 58.699219 51.375 85.304688 30.695312 119.066406 C 10.015625 152.828125 -0.214844 192.179688 1.332031 231.980469 C 2.882812 271.78125 16.140625 310.175781 39.375 342.15625 C 62.613281 374.132812 94.746094 398.207031 131.582031 411.230469 C 168.414062 424.25 208.234375 425.621094 245.839844 415.152344 C 283.445312 404.683594 317.089844 382.871094 342.375 352.558594 L 265.257812 285.382812 C 253.199219 299.839844 237.152344 310.246094 219.214844 315.238281 C 201.273438 320.230469 182.28125 319.578125 164.710938 313.367188 C 147.140625 307.15625 131.8125 295.671875 120.730469 280.417969 C 109.644531 265.164062 103.320312 246.851562 102.582031 227.867188 C 101.84375 208.882812 106.722656 190.109375 116.589844 174.007812 C 126.453125 157.898438 140.839844 145.210938 157.871094 137.59375 C 174.902344 129.972656 193.785156 127.78125 212.054688 131.304688 C 230.324219 134.824219 247.128906 143.894531 260.277344 157.328125 Z M 331.925781 84.105469\"\n            fillOpacity=\"1\"\n            fillRule=\"nonzero\"\n          />\n        </g>\n      </g>\n    </StyledSvg>\n  );\n};\n\nexport default OpenCollectionIcon;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Icons/QuestionCircle/index.js",
    "content": "import React from 'react';\n\nconst QuestionCircle = ({ size = 14 }) => {\n  return (\n    <svg\n      tabIndex=\"-1\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={size}\n      height={size}\n      fill=\"currentColor\"\n      className=\"inline-block ml-2 cursor-pointer\"\n      viewBox=\"0 0 16 16\"\n    >\n      <path d=\"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z\" />\n      <path d=\"M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z\" />\n    </svg>\n  );\n};\n\nexport default QuestionCircle;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Icons/Send/index.js",
    "content": "import React from 'react';\n\nconst SendIcon = ({ color, width }) => {\n  return (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" width={width} height={width} viewBox=\"0 0 48 48\">\n      <path fill={color} d=\"M4.02 42l41.98-18-41.98-18-.02 14 30 4-30 4z\" />\n      <path d=\"M0 0h48v48h-48z\" fill=\"none\" />\n    </svg>\n  );\n};\n\nexport default SendIcon;\n"
  },
  {
    "path": "packages/bruno-app/src/components/InfoTip/index.js",
    "content": "import React from 'react';\nimport { Tooltip as ReactInfoTip } from 'react-tooltip';\n\nconst InfoTip = ({ html: _ignored, infotipId, ...props }) => {\n  return (\n    <>\n      <svg\n        tabIndex=\"-1\"\n        id={infotipId}\n        xmlns=\"http://www.w3.org/2000/svg\"\n        width=\"14\"\n        height=\"14\"\n        fill=\"currentColor\"\n        className=\"inline-block ml-2 cursor-pointer\"\n        viewBox=\"0 0 16 16\"\n      >\n        <path d=\"M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z\" />\n        <path d=\"M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z\" />\n      </svg>\n      <ReactInfoTip anchorId={infotipId} {...props} />\n    </>\n  );\n};\n\nexport default InfoTip;\n"
  },
  {
    "path": "packages/bruno-app/src/components/InheritableSettingsInput/index.js",
    "content": "import React from 'react';\nimport { IconChevronDown, IconX } from '@tabler/icons';\nimport { useTheme } from 'providers/Theme';\nimport Dropdown from 'components/Dropdown';\n\nconst InheritableSettingsInput = ({\n  id,\n  label,\n  value,\n  description,\n  onKeyDown,\n  isInherited,\n  onDropdownSelect,\n  onValueChange,\n  onCustomValueReset\n}) => {\n  const { theme } = useTheme();\n\n  return (\n    <div className=\"flex items-center justify-between\">\n      <div className=\"flex flex-col\">\n        <label className=\"text-xs font-medium text-gray-900 dark:text-gray-100\" htmlFor={id}>\n          {label}\n        </label>\n        {description && (\n          <p className=\"text-xs text-gray-700 dark:text-gray-400\">\n            {description}\n          </p>\n        )}\n      </div>\n      <div className=\"flex items-center justify-end\">\n        {isInherited ? (\n          <Dropdown\n            icon={(\n              <button\n                type=\"button\"\n                className=\"px-2 py-1 text-xs rounded-sm outline-none transition-colors duration-100 w-24 h-8 flex items-center justify-between\"\n                style={{\n                  backgroundColor: theme.input.bg,\n                  border: `1px solid ${theme.input.border}`,\n                  color: theme.text\n                }}\n              >\n                <span>Inherit</span>\n                <IconChevronDown size={12} />\n              </button>\n            )}\n          >\n            <div className=\"dropdown-item\" onClick={() => onDropdownSelect('inherit')}>\n              Inherit\n            </div>\n            <div className=\"dropdown-item\" onClick={() => onDropdownSelect('custom')}>\n              Custom\n            </div>\n          </Dropdown>\n        ) : (\n          <div className=\"relative\">\n            <input\n              id={id}\n              type=\"text\"\n              className=\"block px-2 py-1 pr-6 rounded-sm outline-none transition-colors duration-100 w-24 h-8\"\n              style={{\n                backgroundColor: theme.input.bg,\n                border: `1px solid ${theme.input.border}`,\n                color: theme.text\n              }}\n              autoComplete=\"off\"\n              autoCorrect=\"off\"\n              autoCapitalize=\"off\"\n              spellCheck=\"false\"\n              value={value}\n              onChange={onValueChange}\n              onKeyDown={onKeyDown}\n            />\n            <button\n              type=\"button\"\n              onClick={onCustomValueReset}\n              className=\"absolute right-1 top-1/2 transform -translate-y-1/2 p-1 text-gray-400 hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300 transition-colors\"\n              title=\"Reset to inherit\"\n            >\n              <IconX size={14} />\n            </button>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n};\n\nexport default InheritableSettingsInput;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ManageWorkspace/DeleteWorkspace/index.js",
    "content": "import React, { useState } from 'react';\nimport Portal from 'components/Portal/index';\nimport Modal from 'components/Modal/index';\nimport toast from 'react-hot-toast';\nimport { useDispatch } from 'react-redux';\nimport { IconFolder } from '@tabler/icons';\nimport { closeWorkspaceAction } from 'providers/ReduxStore/slices/workspaces/actions';\n\nconst DeleteWorkspace = ({ onClose, workspace }) => {\n  const dispatch = useDispatch();\n  const [isDeleting, setIsDeleting] = useState(false);\n\n  const onConfirm = async () => {\n    if (isDeleting) return;\n\n    try {\n      setIsDeleting(true);\n      await dispatch(closeWorkspaceAction(workspace.uid));\n      onClose();\n    } catch (error) {\n      toast.error(error?.message || 'An error occurred while removing the workspace');\n      setIsDeleting(false);\n    }\n  };\n\n  return (\n    <Portal>\n      <Modal\n        size=\"sm\"\n        title=\"Remove Workspace\"\n        confirmText={isDeleting ? 'Removing...' : 'Remove'}\n        handleConfirm={onConfirm}\n        handleCancel={onClose}\n        confirmDisabled={isDeleting}\n        confirmButtonColor=\"danger\"\n      >\n        <div className=\"flex items-center\">\n          <IconFolder size={18} strokeWidth={1.5} />\n          <span className=\"ml-2 mr-4 font-semibold\">{workspace?.name}</span>\n        </div>\n        {workspace?.pathname && (\n          <div className=\"break-words text-xs mt-1\">{workspace.pathname}</div>\n        )}\n        <div className=\"mt-4\">\n          Are you sure you want to remove workspace <span className=\"font-semibold\">{workspace?.name}</span>?\n        </div>\n        <div className=\"mt-4\">\n          The workspace will still be available in the file system and can be re-opened later.\n        </div>\n      </Modal>\n    </Portal>\n  );\n};\n\nexport default DeleteWorkspace;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ManageWorkspace/RenameWorkspace/index.js",
    "content": "import React, { useEffect, useRef } from 'react';\nimport Portal from 'components/Portal/index';\nimport Modal from 'components/Modal/index';\nimport toast from 'react-hot-toast';\nimport { useFormik } from 'formik';\nimport * as Yup from 'yup';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { renameWorkspaceAction } from 'providers/ReduxStore/slices/workspaces/actions';\n\nconst RenameWorkspace = ({ onClose, workspace }) => {\n  const dispatch = useDispatch();\n  const { workspaces } = useSelector((state) => state.workspaces);\n  const inputRef = useRef();\n\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: {\n      name: workspace.name\n    },\n    validationSchema: Yup.object({\n      name: Yup.string()\n        .min(1, 'must be at least 1 character')\n        .max(255, 'must be 255 characters or less')\n        .required('name is required')\n        .test('unique-name', 'A workspace with this name already exists', function (value) {\n          if (!value) return true;\n          return !workspaces.some((w) =>\n            w.uid !== workspace.uid && w.name && w.name.toLowerCase() === value.toLowerCase()\n          );\n        })\n    }),\n    onSubmit: (values) => {\n      if (values.name === workspace.name) {\n        onClose();\n        return;\n      }\n      dispatch(renameWorkspaceAction(workspace.uid, values.name))\n        .then(() => {\n          onClose();\n        })\n        .catch((error) => {\n          toast.error(error?.message || 'An error occurred while renaming the workspace');\n        });\n    }\n  });\n\n  useEffect(() => {\n    if (inputRef && inputRef.current) {\n      inputRef.current.focus();\n      inputRef.current.select();\n    }\n  }, [inputRef]);\n\n  const onSubmit = () => {\n    formik.handleSubmit();\n  };\n\n  return (\n    <Portal>\n      <Modal\n        size=\"md\"\n        title=\"Rename Workspace\"\n        confirmText=\"Rename\"\n        handleConfirm={onSubmit}\n        handleCancel={onClose}\n      >\n        <form className=\"bruno-form\" onSubmit={(e) => e.preventDefault()}>\n          <div>\n            <label htmlFor=\"workspace-name\" className=\"block font-semibold\">\n              Workspace Name\n            </label>\n            <input\n              id=\"workspace-name\"\n              type=\"text\"\n              name=\"name\"\n              ref={inputRef}\n              className=\"block textbox mt-2 w-full\"\n              autoComplete=\"off\"\n              autoCorrect=\"off\"\n              autoCapitalize=\"off\"\n              spellCheck=\"false\"\n              onChange={formik.handleChange}\n              value={formik.values.name || ''}\n            />\n            {formik.touched.name && formik.errors.name ? (\n              <div className=\"text-red-500\">{formik.errors.name}</div>\n            ) : null}\n          </div>\n        </form>\n      </Modal>\n    </Portal>\n  );\n};\n\nexport default RenameWorkspace;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ManageWorkspace/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n\n  .manage-workspace-header {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 12px 16px;\n    border-bottom: 1px solid ${(props) => props.theme.workspace.border};\n  }\n\n  .header-left {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n  }\n\n  .back-button {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    padding: 4px;\n    cursor: pointer;\n    color: ${(props) => props.theme.text};\n  }\n\n  .header-title {\n    font-size: 15px;\n    font-weight: 600;\n    color: ${(props) => props.theme.text};\n  }\n\n  .create-workspace-btn {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    padding: 6px 12px;\n    border-radius: ${(props) => props.theme.border.radius.base};\n    background: ${(props) => props.theme.brand};\n    color: white;\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-weight: 500;\n    cursor: pointer;\n    border: none;\n  }\n\n  .workspace-list {\n    flex: 1;\n    overflow-y: auto;\n    padding: 0 16px;\n  }\n\n  .workspace-item {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 12px 0;\n    border-bottom: 1px solid ${(props) => props.theme.workspace.border};\n  }\n\n  .workspace-info {\n    display: flex;\n    flex-direction: column;\n    gap: 2px;\n    flex: 1;\n    min-width: 0;\n  }\n\n  .workspace-name-row {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n  }\n\n  .workspace-icon {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    flex-shrink: 0;\n\n    &.default {\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n\n    &.regular {\n      color: ${(props) => props.theme.brand};\n    }\n  }\n\n  .workspace-name {\n    font-size: ${(props) => props.theme.font.size.md};\n    font-weight: 500;\n    color: ${(props) => props.theme.text};\n  }\n\n  .default-badge {\n    padding: 1px 6px;\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    background: ${(props) => props.theme.background.surface1};\n    color: ${(props) => props.theme.text};\n    font-size: ${(props) => props.theme.font.size.xs};\n  }\n\n  .workspace-path {\n    font-size: ${(props) => props.theme.font.size.xs};\n    color: ${(props) => props.theme.text.muted};\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n\n  .workspace-actions {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n  }\n\n  .action-btn {\n    display: flex;\n    align-items: center;\n    gap: 4px;\n    padding: 4px 8px;\n    background: transparent;\n    border: none;\n    color: ${(props) => props.theme.text};\n    font-size: ${(props) => props.theme.font.size.xs};\n    cursor: pointer;\n  }\n\n  .more-actions-btn {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    padding: 4px;\n    background: transparent;\n    border: none;\n    color: ${(props) => props.theme.text};\n    cursor: pointer;\n  }\n\n  .dropdown-menu {\n    min-width: 120px;\n  }\n\n  .dropdown-item {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    padding: 8px 12px;\n    cursor: pointer;\n    color: ${(props) => props.theme.text};\n    font-size: ${(props) => props.theme.font.size.sm};\n\n    &.danger {\n      color: ${(props) => props.theme.colors.text.danger};\n    }\n  }\n\n  .empty-state {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    height: 200px;\n    color: ${(props) => props.theme.colors.text.muted};\n    font-size: ${(props) => props.theme.font.size.sm};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ManageWorkspace/index.js",
    "content": "import React, { useState, useMemo } from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { IconArrowLeft, IconPlus, IconFolder, IconLock, IconDots, IconCategory, IconLogin } from '@tabler/icons';\nimport toast from 'react-hot-toast';\n\nimport get from 'lodash/get';\nimport { showHomePage } from 'providers/ReduxStore/slices/app';\nimport { createWorkspaceWithUniqueName, switchWorkspace } from 'providers/ReduxStore/slices/workspaces/actions';\nimport { showInFolder } from 'providers/ReduxStore/slices/collections/actions';\nimport { sortWorkspaces } from 'utils/workspaces';\n\nimport CreateWorkspace from 'components/WorkspaceSidebar/CreateWorkspace';\nimport RenameWorkspace from './RenameWorkspace';\nimport DeleteWorkspace from './DeleteWorkspace';\nimport StyledWrapper from './StyledWrapper';\nimport MenuDropdown from 'ui/MenuDropdown/index';\nimport Button from 'ui/Button';\nimport { getRevealInFolderLabel } from 'utils/common/platform';\n\nconst ManageWorkspace = () => {\n  const dispatch = useDispatch();\n  const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);\n  const preferences = useSelector((state) => state.app.preferences);\n\n  const [createWorkspaceModalOpen, setCreateWorkspaceModalOpen] = useState(false);\n  const [renameWorkspaceModal, setRenameWorkspaceModal] = useState({ open: false, workspace: null });\n  const [deleteWorkspaceModal, setDeleteWorkspaceModal] = useState({ open: false, workspace: null });\n\n  const sortedWorkspaces = useMemo(() => {\n    const persistedWorkspaces = workspaces.filter((w) => !w.isCreating);\n    return sortWorkspaces(persistedWorkspaces, preferences);\n  }, [workspaces, preferences]);\n\n  const handleBack = () => {\n    dispatch(showHomePage());\n  };\n\n  const handleOpenWorkspace = (workspace) => {\n    dispatch(switchWorkspace(workspace.uid));\n    dispatch(showHomePage());\n    toast.success(`Switched to ${workspace.name}`);\n  };\n\n  const handleShowInFolder = (workspace) => {\n    if (workspace.pathname) {\n      dispatch(showInFolder(workspace.pathname)).catch(() => {\n        toast.error('Error opening the folder');\n      });\n    }\n  };\n\n  const handleRenameClick = (workspace) => {\n    setRenameWorkspaceModal({ open: true, workspace });\n  };\n\n  const handleCloseClick = (workspace) => {\n    if (workspace.type === 'default') {\n      toast.error('Cannot remove the default workspace');\n      return;\n    }\n    setDeleteWorkspaceModal({ open: true, workspace });\n  };\n\n  const handleCreateWorkspace = async () => {\n    const defaultLocation = get(preferences, 'general.defaultLocation', '');\n    if (!defaultLocation) {\n      setCreateWorkspaceModalOpen(true);\n      return;\n    }\n\n    try {\n      await dispatch(createWorkspaceWithUniqueName(defaultLocation));\n    } catch (error) {\n      toast.error(error?.message || 'Failed to create workspace');\n    }\n  };\n\n  return (\n    <StyledWrapper>\n      {createWorkspaceModalOpen && (\n        <CreateWorkspace onClose={() => setCreateWorkspaceModalOpen(false)} />\n      )}\n\n      {renameWorkspaceModal.open && renameWorkspaceModal.workspace && (\n        <RenameWorkspace\n          workspace={renameWorkspaceModal.workspace}\n          onClose={() => setRenameWorkspaceModal({ open: false, workspace: null })}\n        />\n      )}\n\n      {deleteWorkspaceModal.open && deleteWorkspaceModal.workspace && (\n        <DeleteWorkspace\n          workspace={deleteWorkspaceModal.workspace}\n          onClose={() => setDeleteWorkspaceModal({ open: false, workspace: null })}\n        />\n      )}\n\n      <div className=\"manage-workspace-header\">\n        <div className=\"header-left\">\n          <div className=\"back-button\" onClick={handleBack}>\n            <IconArrowLeft size={18} strokeWidth={1.5} />\n          </div>\n          <span className=\"header-title\">Manage Workspace</span>\n        </div>\n        <Button size=\"sm\" onClick={handleCreateWorkspace} icon={<IconPlus size={14} strokeWidth={2} />}>\n          Create Workspace\n        </Button>\n      </div>\n\n      <div className=\"workspace-list\">\n        {sortedWorkspaces.length === 0 ? (\n          <div className=\"empty-state\">\n            <span>No workspaces found</span>\n          </div>\n        ) : (\n          sortedWorkspaces.map((workspace) => {\n            const isDefault = workspace.type === 'default';\n            const isActive = workspace.uid === activeWorkspaceUid;\n\n            return (\n              <div key={workspace.uid} className=\"workspace-item\">\n                <div className=\"workspace-info\">\n                  <div className=\"workspace-name-row\">\n                    <span className={`workspace-icon ${isDefault ? 'default' : 'regular'}`}>\n                      {isDefault ? (\n                        <IconLock size={14} strokeWidth={1.5} />\n                      ) : (\n                        <IconCategory size={14} strokeWidth={1.5} />\n                      )}\n                    </span>\n                    <span className=\"workspace-name\">{workspace.name}</span>\n                    {isDefault && <span className=\"default-badge\">Default</span>}\n                  </div>\n                  {workspace.pathname && (\n                    <div className=\"workspace-path\">{workspace.pathname}</div>\n                  )}\n                </div>\n\n                <div className=\"workspace-actions\">\n                  <button\n                    className=\"action-btn\"\n                    onClick={() => handleOpenWorkspace(workspace)}\n                  >\n                    <IconLogin size={14} strokeWidth={1.5} />\n                    <span>Open</span>\n                  </button>\n                  {workspace.pathname && workspace.type !== 'default' && (\n                    <button\n                      className=\"action-btn\"\n                      onClick={() => handleShowInFolder(workspace)}\n                    >\n                      <IconFolder size={14} strokeWidth={1.5} />\n                      <span>{getRevealInFolderLabel()}</span>\n                    </button>\n                  )}\n                  {!isDefault && (\n                    <MenuDropdown\n                      placement=\"bottom-end\"\n                      items={[\n                        { id: 'rename', label: 'Rename', onClick: () => handleRenameClick(workspace) },\n                        { id: 'remove', label: 'Remove', onClick: () => handleCloseClick(workspace) }\n                      ]}\n                    >\n                      <button className=\"more-actions-btn\">\n                        <IconDots size={14} strokeWidth={1.5} />\n                      </button>\n                    </MenuDropdown>\n                  )}\n                </div>\n              </div>\n            );\n          })\n        )}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default ManageWorkspace;\n"
  },
  {
    "path": "packages/bruno-app/src/components/MarkDown/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledMarkdownBodyWrapper = styled.div`\n  background: transparent;\n  .markdown-body {\n    background: transparent;\n    overflow-y: auto;\n    color: ${(props) => props.theme.text};\n    box-sizing: border-box;\n    height: 100%;\n    margin: 0 auto;\n    font-size: ${(props) => props.theme.font.size.base};\n\n    h1 {\n      margin: 0.67em 0;\n      font-weight: var(--base-text-weight-semibold, 600);\n      padding-bottom: 0.3em;\n      font-size: 2.2em;\n      border-bottom: 1px solid var(--color-border-muted);\n    }\n\n    h2 {\n      font-weight: var(--base-text-weight-semibold, 600);\n      padding-bottom: 0.3em;\n      font-size: 1.7em;\n      border-bottom: 1px solid var(--color-border-muted);\n    }\n\n    h3 {\n      font-weight: var(--base-text-weight-semibold, 600);\n      font-size: 1.45em;\n    }\n\n    h4 {\n      font-weight: var(--base-text-weight-semibold, 600);\n      font-size: 1.1em;\n    }\n\n    h5 {\n      font-weight: var(--base-text-weight-semibold, 600);\n      font-size: 0.975em;\n    }\n\n    h6 {\n      font-weight: var(--base-text-weight-semibold, 600);\n      font-size: 0.85em;\n      color: var(--color-fg-muted);\n    }\n\n    hr {\n      box-sizing: content-box;\n      overflow: hidden;\n      border-bottom: 1px solid var(--color-border-muted);\n      height: 1px;\n      padding: 0;\n      margin: 24px 0;\n      background-color: var(--color-sidebar-collection-item-active-indent-border);\n      border: 0;\n    }\n\n    ul {\n      list-style-type: disc;\n    }\n\n    ol {\n      list-style-type: decimal;\n    }\n\n    pre {\n      background: ${(props) => props.theme.sidebar.bg};\n      color: ${(props) => props.theme.text};\n    }\n\n    table {\n      th,\n      td {\n        border: 1px solid ${(props) => props.theme.table.border};\n        background-color: ${(props) => props.theme.bg};\n      }\n    }\n\n    p {\n      white-space: pre-wrap;\n    }\n\n    div {\n      white-space: pre-wrap;\n    }\n  }\n`;\n\nexport default StyledMarkdownBodyWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/MarkDown/index.jsx",
    "content": "import MarkdownIt from 'markdown-it';\nimport * as MarkdownItReplaceLink from 'markdown-it-replace-link';\nimport StyledWrapper from './StyledWrapper';\nimport React from 'react';\nimport { isValidUrl } from 'utils/url/index';\n\nconst Markdown = ({ collectionPath, onDoubleClick, content }) => {\n  const markdownItOptions = {\n    html: true,\n    breaks: true,\n    linkify: true,\n    replaceLink: function (link, env) {\n      return link.replace(/^\\./, collectionPath);\n    }\n  };\n\n  const handleOnClick = (event) => {\n    const target = event.target;\n    if (target.tagName === 'A') {\n      event.preventDefault();\n      const href = target.getAttribute('href');\n      if (href && isValidUrl(href)) {\n        window.open(href, '_blank');\n        return;\n      }\n    }\n  };\n\n  const handleOnDoubleClick = (event) => {\n    if (event.detail === 2) {\n      onDoubleClick();\n    }\n  };\n\n  const md = new MarkdownIt(markdownItOptions).use(MarkdownItReplaceLink);\n\n  const htmlFromMarkdown = md.render(content || '');\n\n  return (\n    <StyledWrapper>\n      <div\n        className=\"markdown-body\"\n        dangerouslySetInnerHTML={{ __html: htmlFromMarkdown }}\n        onClick={handleOnClick}\n        onDoubleClick={handleOnDoubleClick}\n      />\n    </StyledWrapper>\n  );\n};\n\nexport default Markdown;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Modal/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst Wrapper = styled.div`\n  color: ${(props) => props.theme.text};\n\n  &.modal--animate-out {\n    animation: fade-out 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1);\n\n    .bruno-modal-card {\n      animation: fade-and-slide-out-from-top 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1);\n    }\n  }\n\n  &.bruno-modal {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100vw;\n    height: 100vh;\n    display: flex;\n    align-items: flex-start;\n    justify-content: center;\n    overflow-y: auto;\n    z-index: 20;\n    background-color: rgba(0, 0, 0, 0.5);\n  }\n\n  .bruno-modal-card {\n    animation-duration: 0.85s;\n    animation-delay: 0.1s;\n    background: ${(props) => props.theme.modal.body.bg};\n    border-radius: ${(props) => props.theme.border.radius.base};\n    position: relative;\n    z-index: 11;\n    max-width: calc(100% - var(--spacing-base-unit));\n    box-shadow: var(--box-shadow-base);\n    display: flex;\n    flex-direction: column;\n    will-change: opacity, transform;\n    flex-grow: 0;\n    margin: 3vh 10vw;\n    margin-top: 50px;\n    border: 1px solid ${(props) => props.theme.border.border0};\n\n    &.modal-sm {\n      min-width: 300px;\n      max-width: 500px;\n    }\n\n    &.modal-md {\n      min-width: 500px;\n      max-width: 800px;\n    }\n\n    &.modal-lg {\n      min-width: 800px;\n      max-width: 1140px;\n    }\n\n    &.modal-xl {\n      min-width: 1140px;\n      max-width: calc(100% - 30px);\n    }\n\n    animation: fade-and-slide-in-from-top 0.5s forwards cubic-bezier(0.19, 1, 0.22, 1);\n  }\n\n  .bruno-modal-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    color: ${(props) => props.theme.modal.title.color};\n    background-color: ${(props) => props.theme.modal.title.bg};\n    font-size: ${(props) => props.theme.font.size.md};\n    padding: 0.5rem 1rem;\n    border-top-left-radius: ${(props) => props.theme.border.radius.base};\n    border-top-right-radius: ${(props) => props.theme.border.radius.base};\n\n    .bruno-modal-header-title {\n      display: flex;\n      align-items: center;\n      gap: 8px;\n    }\n\n    .close {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      width: 24px;\n      height: 24px;\n      margin-right: -0.5rem;\n      font-size: 1.125rem;\n      line-height: 1;\n      color: ${(props) => props.theme.modal.title.color};\n      border-radius: ${(props) => props.theme.border.radius.sm};\n      opacity: 0.7;\n      transition: opacity 0.2s ease, background-color 0.2s ease;\n\n      &:hover {\n        opacity: 1;\n        background-color: ${(props) => rgba(props.theme.modal.title.color, 0.1)};\n      }\n    }\n  }\n\n  .bruno-modal-content {\n    flex-grow: 1;\n    background-color: ${(props) => props.theme.modal.body.bg};\n\n    .textbox {\n      line-height: 1.42857143;\n      border: 1px solid #ccc;\n      padding: 0.45rem;\n      box-shadow: none;\n      border-radius: 0px;\n      outline: none;\n      box-shadow: none;\n      transition: border-color ease-in-out 0.1s;\n      border-radius: ${(props) => props.theme.border.radius.sm};\n      background-color: ${(props) => props.theme.input.bg};\n      border: 1px solid ${(props) => props.theme.input.border};\n\n      &:focus {\n        border: solid 1px ${(props) => props.theme.input.focusBorder} !important;\n        outline: none !important;\n      }\n    }\n\n    select.textbox {\n      appearance: none;\n      padding-right: 30px;\n      background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E\");\n      background-repeat: no-repeat;\n      background-position: right 0.5rem center;\n      cursor: pointer;\n    }\n\n    .bruno-form {\n      color: ${(props) => props.theme.modal.body.color};\n    }\n  }\n\n  .bruno-modal-backdrop {\n    height: 100%;\n    width: 100%;\n    left: 0;\n    top: 0;\n    position: fixed;\n    will-change: opacity;\n    background: transparent;\n\n    &:before {\n      content: '';\n      height: 100%;\n      width: 100%;\n      left: 0;\n      opacity: ${(props) => props.theme.modal.backdrop.opacity};\n      top: 0;\n      background: black;\n      position: fixed;\n    }\n\n    animation: fade-in 0.1s forwards cubic-bezier(0.19, 1, 0.22, 1);\n  }\n\n  .bruno-modal-footer {\n    background-color: ${(props) => props.theme.modal.body.bg};\n    border-bottom-left-radius: ${(props) => props.theme.border.radius.base};\n    border-bottom-right-radius: ${(props) => props.theme.border.radius.base};\n  }\n\n  &.modal-footer-none {\n    .bruno-modal-content {\n      border-bottom-left-radius: ${(props) => props.theme.border.radius.base};\n      border-bottom-right-radius: ${(props) => props.theme.border.radius.base};\n    }\n  }\n\n  input[type='radio'] {\n    cursor: pointer;\n    accent-color: ${(props) => props.theme.primary.solid};\n  }\n\n  input[type='checkbox'] {\n    cursor: pointer;\n    accent-color: ${(props) => props.theme.primary.solid};\n\n  }\n\n  .checkbox {\n    appearance: none;\n    -webkit-appearance: none;\n    width: 1rem;\n    height: 1rem;\n    border: 1px solid ${(props) => props.theme.border.border2};\n    border-radius: 3px;\n    background: transparent;\n    position: relative;\n    flex-shrink: 0;\n\n    &:hover {\n      border-color: ${(props) => props.theme.primary.solid};\n    }\n\n    &:focus-visible {\n      outline: 2px solid ${(props) => props.theme.textLink};\n      outline-offset: 2px;\n    }\n\n    &:checked {\n      background: ${(props) => props.theme.button2.color.primary.bg};\n      border-color: ${(props) => props.theme.button2.color.primary.border};\n\n      &::after {\n        content: '';\n        position: absolute;\n        left: 4px;\n        top: 1px;\n        width: 5px;\n        height: 9px;\n        border: solid ${(props) => props.theme.button2.color.primary.text};\n        border-width: 0 2px 2px 0;\n        transform: rotate(45deg);\n      }\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Modal/index.js",
    "content": "import React, { useEffect, useState, useRef } from 'react';\nimport StyledWrapper from './StyledWrapper';\nimport useFocusTrap from 'hooks/useFocusTrap';\nimport Button from 'ui/Button';\n\nconst ESC_KEY_CODE = 27;\nconst ENTER_KEY_CODE = 13;\n\nconst ModalHeader = ({ title, handleCancel, customHeader, hideClose }) => (\n  <div className=\"bruno-modal-header\">\n    {customHeader ? customHeader : <>{title ? <div className=\"bruno-modal-header-title\">{title}</div> : null}</>}\n    {handleCancel && !hideClose ? (\n      // TODO: Remove data-test-id and use data-testid instead across the codebase.\n      <div className=\"close cursor-pointer\" onClick={handleCancel ? () => handleCancel() : null} data-testid=\"modal-close-button\">\n        ×\n      </div>\n    ) : null}\n  </div>\n);\n\nconst ModalContent = ({ children }) => <div className=\"bruno-modal-content px-4 py-4\">{children}</div>;\n\nconst ModalFooter = ({\n  confirmText,\n  cancelText,\n  handleSubmit,\n  handleCancel,\n  confirmDisabled,\n  hideCancel,\n  hideFooter,\n  confirmButtonColor = 'primary'\n}) => {\n  confirmText = confirmText || 'Save';\n  cancelText = cancelText || 'Cancel';\n\n  if (hideFooter) {\n    return null;\n  }\n\n  return (\n    <div className=\"flex justify-end p-4 bruno-modal-footer\">\n      <span className={hideCancel ? 'hidden' : 'mr-2'}>\n        <Button type=\"button\" color=\"secondary\" variant=\"ghost\" onClick={handleCancel}>\n          {cancelText}\n        </Button>\n      </span>\n      <span>\n        <Button\n          type=\"submit\"\n          color={confirmButtonColor}\n          disabled={confirmDisabled}\n          onClick={handleSubmit}\n          className=\"submit\"\n        >\n          {confirmText}\n        </Button>\n      </span>\n    </div>\n  );\n};\n\nconst Modal = ({\n  size,\n  title,\n  customHeader,\n  confirmText,\n  cancelText,\n  handleCancel,\n  handleConfirm = () => {},\n  children,\n  confirmDisabled,\n  hideCancel,\n  hideFooter,\n  hideClose,\n  disableCloseOnOutsideClick,\n  disableEscapeKey,\n  onClick,\n  closeModalFadeTimeout = 500,\n  dataTestId,\n  confirmButtonColor = 'primary'\n}) => {\n  const modalRef = useRef(null);\n  const [isClosing, setIsClosing] = useState(false);\n\n  const handleKeydown = (event) => {\n    const { keyCode, shiftKey, ctrlKey, altKey, metaKey } = event;\n\n    // Only handle events from elements inside this modal\n    if (keyCode !== ESC_KEY_CODE && (!modalRef.current || !modalRef.current.contains(event.target))) {\n      return;\n    }\n\n    switch (keyCode) {\n      case ESC_KEY_CODE: {\n        if (disableEscapeKey) return;\n        return closeModal({ type: 'esc' });\n      }\n      case ENTER_KEY_CODE: {\n        const isSubmitButton = event.target?.type === 'submit';\n        if (!shiftKey && !ctrlKey && !altKey && !metaKey && handleConfirm && !isSubmitButton && !confirmDisabled) {\n          return handleConfirm();\n        }\n      }\n    }\n  };\n\n  useFocusTrap(modalRef);\n\n  const closeModal = (args) => {\n    setIsClosing(true);\n    setTimeout(() => handleCancel(args), closeModalFadeTimeout);\n  };\n\n  useEffect(() => {\n    document.addEventListener('keydown', handleKeydown, false);\n    return () => {\n      document.removeEventListener('keydown', handleKeydown);\n    };\n  }, [disableEscapeKey, document, handleConfirm, confirmDisabled]);\n\n  let classes = 'bruno-modal';\n  if (isClosing) {\n    classes += ' modal--animate-out';\n  }\n  if (hideFooter) {\n    classes += ' modal-footer-none';\n  }\n  return (\n    <StyledWrapper className={classes} onClick={onClick ? (e) => onClick(e) : null}>\n      <div\n        className={`bruno-modal-card modal-${size}`}\n        ref={modalRef}\n        role=\"dialog\"\n        aria-labelledby=\"modal-title\"\n        aria-describedby=\"modal-description\"\n        data-testid={dataTestId}\n      >\n        <ModalHeader\n          title={title}\n          hideClose={hideClose}\n          handleCancel={() => closeModal({ type: 'icon' })}\n          customHeader={customHeader}\n        />\n        <ModalContent>{children}</ModalContent>\n        <ModalFooter\n          confirmText={confirmText}\n          cancelText={cancelText}\n          handleCancel={() => closeModal({ type: 'button' })}\n          handleSubmit={handleConfirm}\n          confirmDisabled={confirmDisabled}\n          hideCancel={hideCancel}\n          hideFooter={hideFooter}\n          confirmButtonColor={confirmButtonColor}\n        />\n      </div>\n\n      {/* Clicking on backdrop closes the modal */}\n      <div\n        className=\"bruno-modal-backdrop\"\n        onClick={\n          disableCloseOnOutsideClick\n            ? null\n            : () => {\n                closeModal({ type: 'backdrop' });\n              }\n        }\n      />\n    </StyledWrapper>\n  );\n};\n\nexport default Modal;\n"
  },
  {
    "path": "packages/bruno-app/src/components/MultiLineEditor/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  width: 100%;\n  height: fit-content;\n  max-height: 200px;\n  overflow: auto;\n\n  &.read-only {\n    .CodeMirror .CodeMirror-lines {\n      cursor: not-allowed !important;\n    }\n\n    .CodeMirror-line {\n      color: ${(props) => props.theme.colors.text.muted} !important;\n    }\n\n    .CodeMirror-cursor {\n      display: none !important;\n    }\n  }\n\n  .CodeMirror {\n    background: transparent;\n    height: fit-content;\n    font-size: ${(props) => props.theme.font.size.base};\n    line-height: 30px;\n    display: flex;\n    flex-direction: column;\n    max-height: 200px;\n\n    pre.CodeMirror-placeholder {\n      color: ${(props) => props.theme.text};\n      padding-left: 0;\n      opacity: 0.5;\n    }\n\n    .CodeMirror-vscrollbar,\n    .CodeMirror-hscrollbar,\n    .CodeMirror-scrollbar-filler {\n      display: none !important;\n    }\n\n    .CodeMirror-lines {\n      padding: 0;\n    }\n\n    .CodeMirror-cursor {\n      height: 20px !important;\n      margin-top: 5px !important;\n      border-left: 1px solid ${(props) => props.theme.text} !important;\n    }\n\n    pre {\n      font-family: Inter, sans-serif !important;\n      font-weight: 400;\n    }\n\n    .CodeMirror-line {\n      color: ${(props) => props.theme.text};\n      padding: 0;\n    }\n\n    .CodeMirror-selected {\n      background-color: rgba(212, 125, 59, 0.3);\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/MultiLineEditor/index.js",
    "content": "import React, { Component } from 'react';\nimport isEqual from 'lodash/isEqual';\nimport { getAllVariables } from 'utils/collections';\nimport { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';\nimport { setupAutoComplete } from 'utils/codemirror/autocomplete';\nimport { MaskedEditor } from 'utils/common/masked-editor';\nimport StyledWrapper from './StyledWrapper';\nimport { setupLinkAware } from 'utils/codemirror/linkAware';\nimport { IconEye, IconEyeOff } from '@tabler/icons';\n\nconst CodeMirror = require('codemirror');\n\nclass MultiLineEditor extends Component {\n  constructor(props) {\n    super(props);\n    // Keep a cached version of the value, this cache will be updated when the\n    // editor is updated, which can later be used to protect the editor from\n    // unnecessary updates during the update lifecycle.\n    this.cachedValue = props.value || '';\n    this.editorRef = React.createRef();\n    this.variables = {};\n    this.readOnly = props.readOnly || false;\n\n    this.state = {\n      maskInput: props.isSecret || false // Always mask the input by default (if it's a secret)\n    };\n  }\n\n  componentDidMount() {\n    // Initialize CodeMirror as a single line editor\n    /** @type {import(\"codemirror\").Editor} */\n    const variables = getAllVariables(this.props.collection, this.props.item);\n\n    this.editor = CodeMirror(this.editorRef.current, {\n      lineWrapping: false,\n      lineNumbers: false,\n      theme: this.props.theme === 'dark' ? 'monokai' : 'default',\n      placeholder: this.props.placeholder,\n      mode: 'brunovariables',\n      brunoVarInfo: this.props.enableBrunoVarInfo !== false ? {\n        variables,\n        collection: this.props.collection,\n        item: this.props.item\n      } : false,\n      readOnly: this.props.readOnly,\n      tabindex: 0,\n      extraKeys: {\n        'Ctrl-Enter': () => {\n          if (this.props.onRun) {\n            this.props.onRun();\n          }\n        },\n        'Cmd-Enter': () => {\n          if (this.props.onRun) {\n            this.props.onRun();\n          }\n        },\n        'Cmd-S': () => {\n          if (this.props.onSave) {\n            this.props.onSave();\n          }\n        },\n        'Ctrl-S': () => {\n          if (this.props.onSave) {\n            this.props.onSave();\n          }\n        },\n        'Cmd-F': () => {},\n        'Ctrl-F': () => {},\n        // Tabbing disabled to make tabindex work\n        'Tab': false,\n        'Shift-Tab': false\n      }\n    });\n\n    const getAllVariablesHandler = () => getAllVariables(this.props.collection, this.props.item);\n    const getAnywordAutocompleteHints = () => this.props.autocomplete || [];\n\n    // Setup AutoComplete Helper\n    const autoCompleteOptions = {\n      showHintsFor: ['variables'],\n      getAllVariables: getAllVariablesHandler,\n      getAnywordAutocompleteHints\n    };\n\n    this.brunoAutoCompleteCleanup = setupAutoComplete(\n      this.editor,\n      autoCompleteOptions\n    );\n\n    setupLinkAware(this.editor);\n\n    this.editor.setValue(String(this.props.value) || '');\n    this.editor.on('change', this._onEdit);\n    this.addOverlay(variables);\n\n    // Initialize masking if this is a secret field\n    this.setState({ maskInput: this.props.isSecret });\n    this._enableMaskedEditor(this.props.isSecret);\n  }\n\n  _onEdit = () => {\n    if (!this.ignoreChangeEvent && this.editor) {\n      this.cachedValue = this.editor.getValue();\n      if (this.props.onChange) {\n        this.props.onChange(this.cachedValue);\n      }\n    }\n  };\n\n  /** Enable or disable masking the rendered content of the editor */\n  _enableMaskedEditor = (enabled) => {\n    if (typeof enabled !== 'boolean') return;\n\n    if (enabled == true) {\n      if (!this.maskedEditor) this.maskedEditor = new MaskedEditor(this.editor, '*');\n      this.maskedEditor.enable();\n    } else {\n      if (this.maskedEditor) {\n        this.maskedEditor.disable();\n        this.maskedEditor.destroy();\n        this.maskedEditor = null;\n      }\n    }\n  };\n\n  componentDidUpdate(prevProps) {\n    // Ensure the changes caused by this update are not interpreted as\n    // user-input changes which could otherwise result in an infinite\n    // event loop.\n    this.ignoreChangeEvent = true;\n\n    let variables = getAllVariables(this.props.collection, this.props.item);\n    if (!isEqual(variables, this.variables)) {\n      if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) {\n        this.editor.options.brunoVarInfo.variables = variables;\n      }\n      this.addOverlay(variables);\n    }\n\n    // Update collection and item when they change\n    if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) {\n      if (!isEqual(this.props.collection, this.editor.options.brunoVarInfo.collection)) {\n        this.editor.options.brunoVarInfo.collection = this.props.collection;\n      }\n      if (!isEqual(this.props.item, this.editor.options.brunoVarInfo.item)) {\n        this.editor.options.brunoVarInfo.item = this.props.item;\n      }\n    }\n    if (this.props.theme !== prevProps.theme && this.editor) {\n      this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');\n    }\n    if (this.props.readOnly !== prevProps.readOnly && this.editor) {\n      this.editor.setOption('readOnly', this.props.readOnly);\n    }\n    if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {\n      // TODO: temporary fix for keeping cursor state when auto save and new line insertion collide PR#7098\n      const nextValue = String(this.props.value ?? '');\n      const currentValue = this.editor.getValue();\n      if (this.editor.hasFocus?.() && currentValue !== nextValue) {\n        this.cachedValue = currentValue;\n      } else {\n        const cursor = this.editor.getCursor();\n        this.cachedValue = nextValue;\n        this.editor.setValue(nextValue);\n        this.editor.setCursor(cursor);\n      }\n    }\n    if (!isEqual(this.props.isSecret, prevProps.isSecret)) {\n      // If the secret flag has changed, update the editor to reflect the change\n      this._enableMaskedEditor(this.props.isSecret);\n      // also set the maskInput flag to the new value\n      this.setState({ maskInput: this.props.isSecret });\n    }\n    if (this.props.readOnly !== prevProps.readOnly && this.editor) {\n      this.editor.setOption('readOnly', this.props.readOnly || false);\n    }\n    this.ignoreChangeEvent = false;\n  }\n\n  componentWillUnmount() {\n    if (this.brunoAutoCompleteCleanup) {\n      this.brunoAutoCompleteCleanup();\n    }\n    if (this.editor?._destroyLinkAware) {\n      this.editor._destroyLinkAware();\n    }\n    if (this.maskedEditor) {\n      this.maskedEditor.destroy();\n      this.maskedEditor = null;\n    }\n    this.editor.getWrapperElement().remove();\n  }\n\n  addOverlay = (variables) => {\n    this.variables = variables;\n    defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', false, true);\n    this.editor.setOption('mode', 'brunovariables');\n  };\n\n  /**\n   * @brief Toggle the visibility of the secret value\n   */\n  toggleVisibleSecret = () => {\n    const isVisible = !this.state.maskInput;\n    this.setState({ maskInput: isVisible });\n    this._enableMaskedEditor(isVisible);\n  };\n\n  /**\n   * @brief Eye icon to show/hide the secret value\n   * @returns ReactComponent The eye icon\n   */\n  secretEye = (isSecret) => {\n    return isSecret === true ? (\n      <button className=\"mx-2\" onClick={() => this.toggleVisibleSecret()}>\n        {this.state.maskInput === true ? (\n          <IconEyeOff size={18} strokeWidth={2} />\n        ) : (\n          <IconEye size={18} strokeWidth={2} />\n        )}\n      </button>\n    ) : null;\n  };\n\n  render() {\n    const wrapperClass = `multi-line-editor grow ${this.props.readOnly ? 'read-only' : ''}`;\n    return (\n      <div className={`flex flex-row justify-between w-full overflow-x-auto ${this.props.className}`}>\n        <StyledWrapper ref={this.editorRef} className={wrapperClass} />\n        {this.secretEye(this.props.isSecret)}\n      </div>\n    );\n  }\n}\nexport default MultiLineEditor;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Notifications/StyleWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .notifications-modal {\n    margin-inline: -1rem;\n    margin-block: -1.5rem;\n    background-color: ${(props) => props.theme.notifications.bg};\n  }\n\n  .notification-count {\n    display: flex;\n    color: white;\n    position: absolute;\n    top: -0.625rem;\n    right: -0.5rem;\n    margin-right: 0.5rem;\n    justify-content: center;\n    font-size: 0.625rem;\n    border-radius: 50%;\n    background-color: ${(props) => props.theme.colors.text.yellow};\n    border: solid 2px ${(props) => props.theme.sidebar.bg};\n    min-width: 1.25rem;\n  }\n\n  button.mark-as-read {\n    font-weight: 400 !important;\n  }\n\n  ul.notifications {\n    background-color: ${(props) => props.theme.notifications.list.bg};\n    border-right: solid 1px ${(props) => props.theme.notifications.list.borderRight};\n    min-height: 400px;\n    height: 100%;\n    max-height: 85vh;\n    overflow-y: auto;\n\n    li {\n      min-width: 150px;\n      cursor: pointer;\n      padding: 0.5rem 0.625rem;\n      border-left: solid 2px transparent;\n      color: ${(props) => props.theme.textLink};\n      border-bottom: solid 1px ${(props) => props.theme.notifications.list.borderBottom};\n      &:hover {\n        background-color: ${(props) => props.theme.notifications.list.hoverBg};\n      }\n\n      &.active {\n        color: ${(props) => props.theme.text} !important;\n        background-color: ${(props) => props.theme.notifications.list.active.bg} !important;\n        border-left: solid 2px ${(props) => props.theme.notifications.list.active.border};\n        &:hover {\n          background-color: ${(props) => props.theme.notifications.list.active.hoverBg} !important;\n        }\n      }\n\n      &.read {\n        color: ${(props) => props.theme.text} !important;\n      }\n\n      .notification-date {\n        font-size: ${(props) => props.theme.font.size.xs};\n      }\n    }\n  }\n\n  .notification-title {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    display: -webkit-box;\n    -webkit-line-clamp: 2;\n    -webkit-box-orient: vertical;\n  }\n\n  .notification-date {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .pagination {\n    background-color: ${(props) => props.theme.notifications.list.bg};\n    border-right: solid 1px ${(props) => props.theme.notifications.list.borderRight};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Notifications/index.js",
    "content": "import { IconBell } from '@tabler/icons';\nimport { useState } from 'react';\nimport StyledWrapper from './StyleWrapper';\nimport Modal from 'components/Modal/index';\nimport Portal from 'components/Portal';\nimport { useEffect } from 'react';\nimport { useApp } from 'providers/App';\nimport {\n  fetchNotifications,\n  markAllNotificationsAsRead,\n  markNotificationAsRead\n} from 'providers/ReduxStore/slices/notifications';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { humanizeDate, relativeDate } from 'utils/common';\nimport ToolHint from 'components/ToolHint';\nimport DOMPurify from 'dompurify';\n\nconst PAGE_SIZE = 5;\n\nconst Notifications = () => {\n  const dispatch = useDispatch();\n  const { version } = useApp();\n  const notifications = useSelector((state) => state.notifications.notifications);\n\n  const [showNotificationsModal, setShowNotificationsModal] = useState(false);\n  const [selectedNotification, setSelectedNotification] = useState(null);\n  const [pageNumber, setPageNumber] = useState(1);\n\n  const notificationsStartIndex = (pageNumber - 1) * PAGE_SIZE;\n  const notificationsEndIndex = pageNumber * PAGE_SIZE;\n  const totalPages = Math.ceil(notifications.length / PAGE_SIZE);\n  const unreadNotifications = notifications.filter((notification) => !notification.read);\n\n  useEffect(() => {\n    dispatch(fetchNotifications({\n      currentVersion: version\n    }));\n  }, []);\n\n  useEffect(() => {\n    reset();\n  }, [showNotificationsModal]);\n\n  useEffect(() => {\n    if (!selectedNotification && notifications?.length > 0 && showNotificationsModal) {\n      let firstNotification = notifications[0];\n      setSelectedNotification(firstNotification);\n      dispatch(markNotificationAsRead({ notificationId: firstNotification?.id }));\n    }\n  }, [notifications, selectedNotification, showNotificationsModal]);\n\n  const reset = () => {\n    setSelectedNotification(null);\n    setPageNumber(1);\n  };\n\n  const handlePrev = (e) => {\n    if (pageNumber - 1 < 1) return;\n    setPageNumber(pageNumber - 1);\n  };\n\n  const handleNext = (e) => {\n    if (pageNumber + 1 > totalPages) return;\n    setPageNumber(pageNumber + 1);\n  };\n\n  const handleNotificationItemClick = (notification) => (e) => {\n    e.preventDefault();\n    setSelectedNotification(notification);\n    dispatch(markNotificationAsRead({ notificationId: notification?.id }));\n  };\n\n  const getSanitizedDescription = (description) => {\n    return DOMPurify.sanitize(encodeURIComponent(description), {\n      ALLOWED_TAGS: ['a', 'ul', 'img', 'li', 'div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],\n      ALLOWED_ATTR: ['href', 'style', 'target', 'src', 'alt']\n    });\n  };\n\n  const modalCustomHeader = (\n    <div className=\"flex flex-row gap-8\">\n      <div className=\"bruno-modal-header-title\">NOTIFICATIONS</div>\n      {unreadNotifications.length > 0 && (\n        <>\n          <div className=\"normal-case font-normal\">\n            {unreadNotifications.length} <span>unread notifications</span>\n          </div>\n          <button\n            className={`select-none ${1 == 2 ? 'opacity-50' : 'text-link mark-as-read cursor-pointer hover:underline'}`}\n            onClick={() => dispatch(markAllNotificationsAsRead())}\n          >\n            Mark all as read\n          </button>\n        </>\n      )}\n    </div>\n  );\n\n  return (\n    <StyledWrapper>\n      <a\n        className=\"relative cursor-pointer\"\n        onClick={() => {\n          dispatch(fetchNotifications({\n            currentVersion: version\n          }));\n          setShowNotificationsModal(true);\n        }}\n        aria-label=\"Check all Notifications\"\n      >\n        <ToolHint text=\"Notifications\" toolhintId=\"Notifications\" offset={8}>\n          <IconBell\n            size={16}\n            aria-hidden\n            strokeWidth={1.5}\n            className={`${unreadNotifications?.length > 0 ? 'bell' : ''}`}\n          />\n          {unreadNotifications.length > 0 && (\n            <span className=\"notification-count text-xs\">{unreadNotifications.length}</span>\n          )}\n        </ToolHint>\n      </a>\n\n      {showNotificationsModal && (\n        <Portal>\n          <Modal\n            size=\"lg\"\n            title=\"Notifications\"\n            confirmText=\"Close\"\n            handleConfirm={() => {\n              setShowNotificationsModal(false);\n            }}\n            handleCancel={() => {\n              setShowNotificationsModal(false);\n            }}\n            hideFooter={true}\n            customHeader={modalCustomHeader}\n            disableCloseOnOutsideClick={true}\n            disableEscapeKey={true}\n          >\n            <div className=\"notifications-modal\">\n              {notifications?.length > 0 ? (\n                <div className=\"grid grid-cols-4 flex flex-row\">\n                  <div className=\"col-span-1 flex flex-col\">\n                    <ul\n                      className=\"notifications w-full flex flex-col h-[50vh] max-h-[50vh] overflow-y-auto\"\n                      style={{ maxHeight: '50vh', height: '46vh' }}\n                    >\n                      {notifications?.slice(notificationsStartIndex, notificationsEndIndex)?.map((notification) => (\n                        <li\n                          key={notification.id}\n                          className={`p-4 flex flex-col justify-center ${\n                            selectedNotification?.id == notification.id ? 'active' : notification.read ? 'read' : ''\n                          }`}\n                          onClick={handleNotificationItemClick(notification)}\n                        >\n                          <div className=\"notification-title w-full\">{notification?.title}</div>\n                          <div className=\"notification-date text-xs py-2\">{relativeDate(notification?.date)}</div>\n                        </li>\n                      ))}\n                    </ul>\n                    <div className=\"w-full pagination flex flex-row gap-4 justify-center p-2 items-center text-xs\">\n                      <button\n                        className={`pl-2 pr-2 py-3 select-none ${\n                          pageNumber <= 1 ? 'opacity-50' : 'text-link cursor-pointer hover:underline'\n                        }`}\n                        onClick={handlePrev}\n                      >\n                        Prev\n                      </button>\n                      <div className=\"flex flex-row items-center justify-center gap-1\">\n                        Page\n                        <div className=\"w-[20px] flex justify-center\" style={{ width: '20px' }}>\n                          {pageNumber}\n                        </div>\n                        of\n                        <div className=\"w-[20px] flex justify-center\" style={{ width: '20px' }}>\n                          {totalPages}\n                        </div>\n                      </div>\n                      <button\n                        className={`pl-2 pr-2 py-3 select-none ${\n                          pageNumber == totalPages ? 'opacity-50' : 'text-link cursor-pointer hover:underline'\n                        }`}\n                        onClick={handleNext}\n                      >\n                        Next\n                      </button>\n                    </div>\n                  </div>\n                  <div className=\"flex w-full col-span-3 p-4 flex-col\">\n                    <div className=\"w-full text-lg flex flex-wrap h-fit mb-1\">{selectedNotification?.title}</div>\n                    <div className=\"w-full notification-date text-xs mb-4\">\n                      {humanizeDate(selectedNotification?.date)}\n                    </div>\n                    <iframe\n                      src={`data:text/html,${getSanitizedDescription(selectedNotification?.description)}`}\n                      sandbox=\"allow-popups\"\n                      style={{ width: '100%', height: '100%' }}\n                    >\n                    </iframe>\n                  </div>\n                </div>\n              ) : (\n                <div className=\"opacity-50 italic text-xs p-12 flex justify-center\">You are all caught up!</div>\n              )}\n            </div>\n          </Modal>\n        </Portal>\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default Notifications;\n"
  },
  {
    "path": "packages/bruno-app/src/components/OpenAPISpecTab/index.js",
    "content": "import React, { useState, useEffect, useCallback } from 'react';\nimport { IconLoader2, IconCloud } from '@tabler/icons';\nimport fastJsonFormat from 'fast-json-format';\nimport SpecViewer from 'components/ApiSpecPanel/SpecViewer';\nimport StyledWrapper from 'components/ApiSpecPanel/StyledWrapper';\n\n/**\n * Pretty-print JSON content for readable display. YAML content is returned as-is.\n */\nconst prettyPrintSpec = (content) => {\n  if (!content) return content;\n  if (content.trimStart()[0] !== '{') return content;\n  try {\n    return fastJsonFormat(content);\n  } catch {\n    return content;\n  }\n};\n\nconst OpenAPISpecTab = ({ collection }) => {\n  const [specContent, setSpecContent] = useState(null);\n  const [isLoading, setIsLoading] = useState(true);\n  const [error, setError] = useState(null);\n  const [isRemote, setIsRemote] = useState(false);\n\n  const openApiSyncConfig = collection?.brunoConfig?.openapi?.[0];\n  const sourceUrl = openApiSyncConfig?.sourceUrl;\n\n  const loadSpec = useCallback(async () => {\n    setIsLoading(true);\n    setError(null);\n    setIsRemote(false);\n    try {\n      const { ipcRenderer } = window;\n      const result = await ipcRenderer.invoke('renderer:read-openapi-spec', {\n        collectionPath: collection.pathname\n      });\n      if (result.error) {\n        // Local file not found — fall back to fetching from remote URL\n        if (sourceUrl) {\n          const fetchResult = await ipcRenderer.invoke('renderer:fetch-openapi-spec', {\n            collectionUid: collection.uid,\n            collectionPath: collection.pathname,\n            sourceUrl,\n            environmentContext: {\n              activeEnvironmentUid: collection.activeEnvironmentUid,\n              environments: collection.environments,\n              runtimeVariables: collection.runtimeVariables,\n              globalEnvironmentVariables: collection.globalEnvironmentVariables\n            }\n          });\n          if (fetchResult.content) {\n            setSpecContent(prettyPrintSpec(fetchResult.content));\n            setIsRemote(true);\n            return;\n          }\n        }\n        setError(result.error);\n      } else {\n        setSpecContent(prettyPrintSpec(result.content));\n      }\n    } catch (err) {\n      setError(err.message || 'Failed to read spec file');\n    } finally {\n      setIsLoading(false);\n    }\n  }, [collection?.pathname, collection?.uid, collection?.activeEnvironmentUid, collection?.environments, collection?.runtimeVariables, collection?.globalEnvironmentVariables, sourceUrl]);\n\n  useEffect(() => {\n    if (collection?.pathname) {\n      loadSpec();\n    }\n  }, [loadSpec]);\n\n  if (isLoading) {\n    return (\n      <div className=\"flex items-center justify-center h-full gap-2 opacity-50\">\n        <IconLoader2 size={20} className=\"animate-spin\" />\n        <span>Loading spec...</span>\n      </div>\n    );\n  }\n\n  if (error || !specContent) {\n    return (\n      <div className=\"flex items-center justify-center h-full opacity-50\">\n        <span>{error || 'No spec file found. Sync your collection first.'}</span>\n      </div>\n    );\n  }\n\n  return (\n    <StyledWrapper className=\"flex flex-col flex-grow relative\">\n      {isRemote && (\n        <div className=\"flex items-center gap-1.5 px-3 py-1.5 text-xs opacity-60\" style={{ borderBottom: '1px solid var(--color-border)' }}>\n          <IconCloud size={14} />\n          <span>Showing spec file from {sourceUrl}.</span>\n        </div>\n      )}\n      <SpecViewer content={specContent} readOnly />\n    </StyledWrapper>\n  );\n};\n\nexport default OpenAPISpecTab;\n"
  },
  {
    "path": "packages/bruno-app/src/components/OpenAPISyncTab/CollectionStatusSection/index.js",
    "content": "import { useMemo } from 'react';\nimport {\n  IconCheck,\n  IconPlus,\n  IconTrash,\n  IconArrowBackUp,\n  IconExternalLink,\n  IconAlertTriangle,\n  IconInfoCircle,\n  IconLoader2\n} from '@tabler/icons';\nimport moment from 'moment';\nimport Button from 'ui/Button';\nimport StatusBadge from 'ui/StatusBadge';\nimport Modal from 'components/Modal';\nimport EndpointChangeSection from '../EndpointChangeSection';\nimport ExpandableEndpointRow from '../EndpointChangeSection/ExpandableEndpointRow';\nimport useEndpointActions from '../hooks/useEndpointActions';\n\nconst CollectionStatusSection = ({\n  collection,\n  collectionDrift,\n  reloadDrift,\n  specDrift,\n  storedSpec,\n  lastSyncDate,\n  onOpenEndpoint,\n  isLoading,\n  onTabSelect\n}) => {\n  const {\n    pendingAction, setPendingAction,\n    confirmPendingAction,\n    handleResetEndpoint,\n    handleResetAllModified,\n    handleDeleteEndpoint,\n    handleDeleteAllLocalOnly,\n    handleRevertAllChanges,\n    handleAddMissingEndpoint,\n    handleAddAllMissing\n  } = useEndpointActions(collection, collectionDrift, reloadDrift);\n\n  const spec = storedSpec || specDrift?.newSpec;\n  const hasStoredSpec = collectionDrift && !collectionDrift.noStoredSpec;\n  const hasDrift = hasStoredSpec && (collectionDrift.modified?.length > 0\n    || collectionDrift.missing?.length > 0\n    || collectionDrift.localOnly?.length > 0);\n\n  const renderDriftRow = (endpoint, idx, actions) => (\n    <ExpandableEndpointRow\n      key={endpoint.id}\n      endpoint={endpoint}\n      collectionPath={collection.pathname}\n      newSpec={spec}\n      showDecisions={false}\n      diffLeftLabel=\"Last Synced Spec\"\n      diffRightLabel=\"Current (in collection)\"\n      swapDiffSides\n      collectionUid={collection.uid}\n      actions={actions}\n    />\n  );\n\n  const modifiedCount = collectionDrift?.modified?.length || 0;\n  const missingCount = collectionDrift?.missing?.length || 0;\n  const localOnlyCount = collectionDrift?.localOnly?.length || 0;\n  const version = specDrift?.storedVersion || storedSpec?.info?.version;\n\n  const bannerState = useMemo(() => {\n    if (hasDrift) {\n      return {\n        variant: 'muted',\n        message: 'Collection has changes since last sync',\n        badges: { modifiedCount, missingCount, localOnlyCount },\n        actions: ['revert-all']\n      };\n    }\n    return null;\n  }, [hasDrift, modifiedCount, missingCount, localOnlyCount, version, lastSyncDate]);\n\n  return (\n    <div className=\"collection-status-section\">\n      {bannerState && (\n        <div className={`spec-update-banner ${bannerState.variant}`}>\n          <div className=\"banner-left\">\n            {bannerState.variant === 'success'\n              ? <IconCheck size={16} className=\"status-check-icon\" />\n              : <div className={`status-dot ${bannerState.variant}`} />}\n            <span className=\"banner-title\">\n              {bannerState.message}\n            </span>\n            {bannerState.badges && (\n              <span className=\"banner-details\">\n                {bannerState.badges.modifiedCount > 0 && <StatusBadge status=\"warning\" radius=\"full\">{bannerState.badges.modifiedCount} modified</StatusBadge>}\n                {bannerState.badges.missingCount > 0 && <StatusBadge status=\"danger\" radius=\"full\">{bannerState.badges.missingCount} deleted</StatusBadge>}\n                {bannerState.badges.localOnlyCount > 0 && <StatusBadge status=\"muted\" radius=\"full\">{bannerState.badges.localOnlyCount} added</StatusBadge>}\n              </span>\n            )}\n          </div>\n          {bannerState.actions.includes('revert-all') && (\n            <div className=\"banner-actions\">\n              <Button size=\"sm\" variant=\"ghost\" color=\"danger\" onClick={handleRevertAllChanges}>\n                Revert All to Spec\n              </Button>\n            </div>\n          )}\n        </div>\n      )}\n\n      {hasDrift && (\n        <div className=\"sync-info-notice mt-4\">\n          <IconInfoCircle size={14} className=\"sync-info-icon\" />\n          <span><span className=\"whats-updated-title\">What's tracked:</span> Changes to parameters, headers, body and auth compared to the synced spec. Your variables, scripts, tests, assertions, settings etc. are not tracked here.</span>\n        </div>\n      )}\n\n      {hasDrift ? (\n        <div className=\"mt-5\">\n          {/* Modified in Collection */}\n          <EndpointChangeSection\n            title=\"Modified in Collection\"\n            type=\"modified\"\n            endpoints={collectionDrift.modified || []}\n            expandableLayout\n            collectionUid={collection.uid}\n            sectionKey=\"drift-modified\"\n            renderItem={(endpoint, idx) =>\n              renderDriftRow(endpoint, idx, (\n                <>\n                  <Button size=\"xs\" variant=\"ghost\" onClick={() => onOpenEndpoint(endpoint.id)} title=\"Open in tab\" icon={<IconExternalLink size={14} />}>\n                    Open\n                  </Button>\n                  <Button size=\"xs\" variant=\"ghost\" onClick={() => handleResetEndpoint(endpoint)} title=\"Reset to spec\" icon={<IconArrowBackUp size={14} />}>\n                    Reset\n                  </Button>\n                </>\n              ))}\n            actions={(\n              <Button\n                size=\"xs\"\n                variant=\"outline\"\n                onClick={handleResetAllModified}\n                title=\"Reset all modified endpoints to match the spec\"\n                icon={<IconArrowBackUp size={14} />}\n              >\n                Reset All\n              </Button>\n            )}\n          />\n\n          {/* Deleted from Collection */}\n          <EndpointChangeSection\n            title=\"Deleted from Collection\"\n            type=\"missing\"\n            endpoints={collectionDrift.missing || []}\n            expandableLayout\n            collectionUid={collection.uid}\n            sectionKey=\"drift-missing\"\n            renderItem={(endpoint, idx) =>\n              renderDriftRow(endpoint, idx, (\n                <Button size=\"xs\" variant=\"ghost\" onClick={() => handleAddMissingEndpoint(endpoint)} title=\"Restore to collection\" icon={<IconPlus size={14} />}>\n                  Restore\n                </Button>\n              ))}\n            actions={(\n              <Button\n                size=\"xs\"\n                variant=\"outline\"\n                onClick={handleAddAllMissing}\n                title=\"Add all deleted endpoints back to collection\"\n                icon={<IconPlus size={14} />}\n              >\n                Restore All\n              </Button>\n            )}\n          />\n\n          {/* Added to Collection */}\n          <EndpointChangeSection\n            title=\"Added to Collection\"\n            type=\"local-only\"\n            endpoints={collectionDrift.localOnly || []}\n            expandableLayout\n            collectionUid={collection.uid}\n            sectionKey=\"drift-local-only\"\n            renderItem={(endpoint, idx) =>\n              renderDriftRow(endpoint, idx, (\n                <>\n                  <Button size=\"xs\" variant=\"ghost\" onClick={() => onOpenEndpoint(endpoint.id)} title=\"Open in tab\" icon={<IconExternalLink size={14} />}>\n                    Open\n                  </Button>\n                  <Button size=\"xs\" variant=\"ghost\" color=\"danger\" onClick={() => handleDeleteEndpoint(endpoint)} title=\"Delete endpoint\" icon={<IconTrash size={14} />}>\n                    Delete\n                  </Button>\n                </>\n              ))}\n            actions={(\n              <Button\n                size=\"xs\"\n                variant=\"outline\"\n                color=\"danger\"\n                onClick={handleDeleteAllLocalOnly}\n                title=\"Delete all locally added endpoints\"\n                icon={<IconTrash size={14} />}\n              >\n                Delete All\n              </Button>\n            )}\n          />\n        </div>\n      ) : isLoading ? (\n        <div className=\"sync-review-empty-state mt-5\">\n          <IconLoader2 size={40} className=\"empty-state-icon animate-spin\" />\n          <h4>Checking for updates</h4>\n          <p>Comparing your collection with the last synced spec...</p>\n        </div>\n      ) : !hasStoredSpec ? (\n        <div className=\"sync-review-empty-state mt-5\">\n          <IconAlertTriangle size={40} className=\"empty-state-icon\" />\n          <h4>{lastSyncDate ? 'Cannot track collection changes' : 'Waiting for initial sync'}</h4>\n          <p>{lastSyncDate\n            ? 'The last synced spec is missing. Go to the \\'Spec Updates\\' tab to restore it, or sync the collection if updates are available to track future changes.'\n            : 'Once you sync your collection with the spec, local changes will appear here.'}\n          </p>\n          <Button variant=\"outline\" size=\"sm\" className=\"mt-4\" onClick={() => onTabSelect('spec-updates')}>Go to Spec Updates</Button>\n        </div>\n      ) : (\n        <div className=\"sync-review-empty-state mt-5\">\n          <IconCheck size={40} className=\"empty-state-icon\" />\n          <h4>No changes in collection</h4>\n          <p>The collection endpoints match the last synced spec. Nothing to review.</p>\n        </div>\n      )}\n      {/* Action confirmation modal */}\n      {pendingAction && (\n        <Modal size=\"sm\" title={pendingAction.title} hideFooter={true} handleCancel={() => setPendingAction(null)}>\n          <div className=\"action-confirm-modal\">\n            <p className=\"confirm-message\">{pendingAction.message}</p>\n            <div className=\"confirm-actions\">\n              <Button variant=\"ghost\" onClick={() => setPendingAction(null)}>\n                Cancel\n              </Button>\n              <Button\n                color={pendingAction.type.includes('delete') ? 'danger' : 'primary'}\n                onClick={confirmPendingAction}\n              >\n                Confirm\n              </Button>\n            </div>\n          </div>\n        </Modal>\n      )}\n    </div>\n  );\n};\n\nexport default CollectionStatusSection;\n"
  },
  {
    "path": "packages/bruno-app/src/components/OpenAPISyncTab/ConfirmSyncModal/index.js",
    "content": "import React, { useState } from 'react';\nimport { IconChevronRight } from '@tabler/icons';\nimport Modal from 'components/Modal';\nimport Button from 'ui/Button';\nimport MethodBadge from 'ui/MethodBadge';\n\nconst handleKeyDown = (toggle) => (e) => {\n  if (e.key === 'Enter' || e.key === ' ') {\n    e.preventDefault();\n    toggle();\n  }\n};\n\nconst ConfirmGroup = ({ group }) => {\n  const [expanded, setExpanded] = useState(false);\n  const toggle = () => setExpanded((prev) => !prev);\n  return (\n    <div className={`confirm-group type-${group.type}`}>\n      <div\n        className=\"confirm-group-header\"\n        role=\"button\"\n        tabIndex={0}\n        onClick={toggle}\n        onKeyDown={handleKeyDown(toggle)}\n      >\n        <IconChevronRight size={14} className={`chevron ${expanded ? 'expanded' : ''}`} />\n        <span className=\"confirm-group-label\">{group.label}</span>\n        <span className=\"confirm-group-count\">{group.endpoints.length}</span>\n      </div>\n      {expanded && (\n        <div className=\"endpoints-list\">\n          {group.endpoints.map((ep, i) => (\n            <div key={ep.id || i} className=\"endpoint-row\">\n              <MethodBadge method={ep.method} />\n              <span className=\"endpoint-path\">{ep.path}</span>\n              {(ep.summary || ep.name) && (\n                <span className=\"endpoint-summary\">{ep.summary || ep.name}</span>\n              )}\n            </div>\n          ))}\n        </div>\n      )}\n    </div>\n  );\n};\n\nconst ConfirmSyncModal = ({ groups, onCancel, onSync, isSyncing }) => {\n  const hasNoChanges = groups.length === 0;\n\n  return (\n    <Modal\n      size=\"md\"\n      title=\"Confirm Sync\"\n      handleCancel={onCancel}\n      hideFooter={true}\n    >\n      <div className=\"sync-confirm-modal\">\n        {hasNoChanges ? (\n          <p className=\"sync-confirm-description\">\n            Your collection is already in sync with the remote spec. Syncing will update the local spec file to match the latest remote version.\n          </p>\n        ) : (\n          <>\n            <p className=\"sync-confirm-description\">\n              The following changes will be applied to your collection. This action cannot be undone. Are you sure you want to proceed?\n            </p>\n\n            <div className=\"sync-confirm-groups\">\n              {groups.map((group, idx) => (\n                <ConfirmGroup key={idx} group={group} />\n              ))}\n            </div>\n          </>\n        )}\n\n        <div className=\"sync-confirm-actions\">\n          <Button variant=\"ghost\" color=\"secondary\" onClick={onCancel}>\n            Cancel\n          </Button>\n          <Button onClick={onSync} loading={isSyncing} disabled={isSyncing}>\n            {hasNoChanges ? 'Restore Spec File' : 'Confirm & Sync Collection'}\n          </Button>\n        </div>\n      </div>\n    </Modal>\n  );\n};\n\nexport default ConfirmSyncModal;\n"
  },
  {
    "path": "packages/bruno-app/src/components/OpenAPISyncTab/ConnectSpecForm/index.js",
    "content": "import { useState, useRef } from 'react';\nimport { IconCheck } from '@tabler/icons';\nimport Button from 'ui/Button';\nimport { isHttpUrl } from 'utils/url/index';\nimport { isOpenApiSpec } from 'utils/importers/openapi-collection';\nimport { parseFileAsJsonOrYaml } from 'utils/importers/file-reader';\n\nconst FEATURES = [\n  'Detect new, modified, and removed endpoints',\n  'Track local changes against the spec',\n  'Sync collection with a single click',\n  'Your tests, assertions, and scripts are preserved during sync'\n];\n\nconst ConnectSpecForm = ({ sourceUrl, setSourceUrl, isLoading, error, setError, onConnect }) => {\n  const [mode, setMode] = useState('url');\n  const fileInputRef = useRef(null);\n\n  return (\n    <div className=\"setup-section\">\n      <div className=\"setup-header\">\n        <h2 className=\"setup-title\">Connect to OpenAPI Spec</h2>\n        <p className=\"setup-description\">\n          Keep your collection synchronized with an OpenAPI specification. Changes in the spec will be detected automatically.\n        </p>\n      </div>\n\n      <form\n        className=\"setup-form\"\n        onSubmit={(e) => {\n          e.preventDefault(); onConnect();\n        }}\n      >\n        <label className=\"url-label\">OpenAPI Specification</label>\n        <div className=\"url-row\">\n          <div className=\"setup-mode-toggle\">\n            <button\n              type=\"button\"\n              className={`setup-mode-btn ${mode === 'url' ? 'active' : ''}`}\n              onClick={() => {\n                setMode('url'); setSourceUrl('');\n              }}\n            >\n              URL\n            </button>\n            <button\n              type=\"button\"\n              className={`setup-mode-btn ${mode === 'file' ? 'active' : ''}`}\n              onClick={() => {\n                setMode('file'); setSourceUrl('');\n              }}\n            >\n              File\n            </button>\n          </div>\n\n          {mode === 'url' ? (\n            <input\n              type=\"text\"\n              className=\"url-input\"\n              value={sourceUrl}\n              onChange={(e) => setSourceUrl(e.target.value)}\n              placeholder=\"https://api.example.com/openapi.json\"\n            />\n          ) : (\n            <>\n              <input\n                ref={fileInputRef}\n                type=\"file\"\n                accept=\".json,.yaml,.yml\"\n                style={{ display: 'none' }}\n                onChange={async (e) => {\n                  const file = e.target.files?.[0];\n                  if (!file) return;\n                  setError(null);\n                  setSourceUrl('');\n                  try {\n                    const data = await parseFileAsJsonOrYaml(file);\n                    if (!isOpenApiSpec(data)) {\n                      setError('The selected file is not a valid OpenAPI 3.x specification');\n                      return;\n                    }\n                    const filePath = window.ipcRenderer.getFilePath(file);\n                    if (filePath) setSourceUrl(filePath);\n                  } catch (err) {\n                    setError(err.message || 'Failed to read the selected file');\n                  }\n                }}\n              />\n              <button\n                type=\"button\"\n                className=\"url-input file-pick-btn\"\n                onClick={() => fileInputRef.current?.click()}\n              >\n                {sourceUrl ? sourceUrl.split(/[\\\\/]/).pop() : 'Choose file...'}\n              </button>\n            </>\n          )}\n\n          <Button\n            type=\"submit\"\n            size=\"sm\"\n            disabled={mode === 'url' ? !isHttpUrl(sourceUrl.trim()) : !sourceUrl.trim()}\n            loading={isLoading}\n          >\n            Connect\n          </Button>\n        </div>\n        <p className=\"setup-hint\">\n          {mode === 'url'\n            ? 'Supports OpenAPI 3.x specifications in JSON or YAML format'\n            : 'Select a local OpenAPI/Swagger JSON or YAML file'}\n        </p>\n        {error && (\n          <p className=\"setup-error\">{error}</p>\n        )}\n      </form>\n\n      <div className=\"setup-features\">\n        {FEATURES.map((text) => (\n          <div className=\"setup-feature\" key={text}>\n            <IconCheck size={16} />\n            <span>{text}</span>\n          </div>\n        ))}\n      </div>\n\n      <p className=\"beta-feedback-inline\">\n        OpenAPI Sync is in Beta — we'd love to hear your feedback and suggestions.{' '}\n        <button\n          type=\"button\"\n          className=\"beta-feedback-link\"\n          onClick={() => window?.ipcRenderer?.openExternal('https://github.com/usebruno/bruno/discussions/7401')}\n        >\n          Share feedback\n        </button>\n      </p>\n    </div>\n  );\n};\n\nexport default ConnectSpecForm;\n"
  },
  {
    "path": "packages/bruno-app/src/components/OpenAPISyncTab/ConnectionSettingsModal/index.js",
    "content": "import { useState, useRef } from 'react';\nimport toast from 'react-hot-toast';\nimport Button from 'ui/Button';\nimport Modal from 'components/Modal';\nimport { isHttpUrl } from 'utils/url/index';\nimport { isOpenApiSpec } from 'utils/importers/openapi-collection';\nimport { parseFileAsJsonOrYaml } from 'utils/importers/file-reader';\n\nconst ConnectionSettingsModal = ({ collection, sourceUrl, onSave, onDisconnect, onClose }) => {\n  const openApiSyncConfig = collection?.brunoConfig?.openapi?.[0];\n  const normalizedSourceUrl = (sourceUrl || '').trim();\n  const isUrl = isHttpUrl(normalizedSourceUrl);\n  const initialMode = isUrl ? 'url' : 'file';\n  const [mode, setMode] = useState(initialMode);\n  const [url, setUrl] = useState(isUrl ? normalizedSourceUrl : '');\n  const [filePath, setFilePath] = useState(isUrl ? '' : normalizedSourceUrl);\n  const [autoCheck, setAutoCheck] = useState(openApiSyncConfig?.autoCheck !== false);\n  const [checkInterval, setCheckInterval] = useState(openApiSyncConfig?.autoCheckInterval || 5);\n  const [isSaving, setIsSaving] = useState(false);\n  const fileInputRef = useRef(null);\n\n  const intervals = [5, 15, 30, 60];\n\n  const effectiveSource = mode === 'file' ? filePath : url.trim();\n  const canSave = mode === 'file' ? !!effectiveSource : isHttpUrl(effectiveSource.trim());\n\n  const handleSave = async () => {\n    setIsSaving(true);\n    try {\n      await onSave({ sourceUrl: effectiveSource, autoCheck, autoCheckInterval: checkInterval });\n      onClose();\n    } catch (_) {\n      // caller (handleSaveSettings) already shows a toast on failure\n    } finally {\n      setIsSaving(false);\n    }\n  };\n\n  return (\n    <Modal\n      size=\"md\"\n      title=\"Connection Settings\"\n      hideFooter={true}\n      handleCancel={onClose}\n    >\n      <div className=\"settings-modal\">\n        <div className=\"settings-body\">\n          <div className=\"settings-field\">\n            <label className=\"settings-label\">Spec Source</label>\n            <div className=\"setup-mode-toggle\" style={{ marginBottom: '8px' }}>\n              <button\n                type=\"button\"\n                className={`setup-mode-btn ${mode === 'url' ? 'active' : ''}`}\n                onClick={() => setMode('url')}\n              >\n                URL\n              </button>\n              <button\n                type=\"button\"\n                className={`setup-mode-btn ${mode === 'file' ? 'active' : ''}`}\n                onClick={() => setMode('file')}\n              >\n                File\n              </button>\n            </div>\n\n            {mode === 'url' ? (\n              <input\n                className=\"settings-input\"\n                type=\"text\"\n                value={url}\n                onChange={(e) => setUrl(e.target.value)}\n                placeholder=\"https://api.example.com/openapi.json\"\n              />\n            ) : (\n              <>\n                <input\n                  ref={fileInputRef}\n                  type=\"file\"\n                  accept=\".json,.yaml,.yml\"\n                  style={{ display: 'none' }}\n                  onChange={async (e) => {\n                    const file = e.target.files?.[0];\n                    if (file) {\n                      try {\n                        const data = await parseFileAsJsonOrYaml(file);\n                        if (!isOpenApiSpec(data)) {\n                          toast.error('The selected file is not a valid OpenAPI 3.x specification');\n                          return;\n                        }\n                        const path = window.ipcRenderer.getFilePath(file);\n                        if (path) setFilePath(path);\n                      } catch (err) {\n                        toast.error(err.message || 'Failed to read the selected file');\n                      }\n                    }\n                  }}\n                />\n                <button\n                  type=\"button\"\n                  className=\"settings-input file-pick-btn\"\n                  onClick={() => fileInputRef.current?.click()}\n                >\n                  {filePath ? filePath.split(/[\\\\/]/).pop() : 'Choose file...'}\n                </button>\n              </>\n            )}\n          </div>\n\n          <div className=\"settings-field\">\n            <label className=\"settings-label\">Auto-check for updates</label>\n            <div className=\"settings-toggle-row\">\n              <div className=\"toggle-info\">\n                <div className=\"toggle-description\">\n                  Automatically check for spec changes at a regular interval\n                </div>\n              </div>\n              <button\n                className={`toggle-switch ${autoCheck ? 'active' : ''}`}\n                onClick={() => setAutoCheck(!autoCheck)}\n                type=\"button\"\n              >\n                <span className=\"toggle-knob\" />\n              </button>\n            </div>\n          </div>\n\n          {autoCheck && (\n            <div className=\"settings-field\">\n              <label className=\"settings-label\">Check interval</label>\n              <div className=\"interval-buttons\">\n                {intervals.map((mins) => (\n                  <button\n                    key={mins}\n                    type=\"button\"\n                    className={checkInterval === mins ? 'active' : ''}\n                    onClick={() => setCheckInterval(mins)}\n                  >\n                    {mins} min\n                  </button>\n                ))}\n              </div>\n            </div>\n          )}\n        </div>\n\n        <div className=\"settings-footer\">\n          <button className=\"disconnect-link\" onClick={onDisconnect} type=\"button\">\n            Disconnect sync\n          </button>\n          <div className=\"settings-actions\">\n            <Button variant=\"ghost\" color=\"secondary\" size=\"sm\" onClick={onClose}>Cancel</Button>\n            <Button size=\"sm\" onClick={handleSave} loading={isSaving} disabled={!canSave || isSaving}>Save</Button>\n          </div>\n        </div>\n      </div>\n    </Modal>\n  );\n};\n\nexport default ConnectionSettingsModal;\n"
  },
  {
    "path": "packages/bruno-app/src/components/OpenAPISyncTab/DisconnectSyncModal/index.js",
    "content": "import Button from 'ui/Button';\nimport Modal from 'components/Modal';\n\nconst DisconnectSyncModal = ({ onConfirm, onClose }) => {\n  return (\n    <Modal\n      size=\"sm\"\n      title=\"Disconnect Sync\"\n      hideFooter={true}\n      handleCancel={onClose}\n    >\n      <div className=\"disconnect-modal\">\n        <p className=\"disconnect-message\">\n          <>Are you sure you want to disconnect OpenAPI sync? </> <br /> <br />\n          <>This will only disconnect the sync configuration. Your collection will remain intact.</>\n        </p>\n        <div className=\"disconnect-actions\">\n          <Button variant=\"ghost\" color=\"secondary\" onClick={onClose}>\n            Cancel\n          </Button>\n          <Button color=\"danger\" onClick={onConfirm}>\n            Disconnect\n          </Button>\n        </div>\n      </div>\n    </Modal>\n  );\n};\n\nexport default DisconnectSyncModal;\n"
  },
  {
    "path": "packages/bruno-app/src/components/OpenAPISyncTab/EndpointChangeSection/EndpointItem.js",
    "content": "import React from 'react';\nimport MethodBadge from 'ui/MethodBadge';\n\n// Simple endpoint item for non-review mode\nconst EndpointItem = ({ endpoint, type, actions }) => {\n  return (\n    <div className={`endpoint-item type-${type}`}>\n      <div className=\"endpoint-row\">\n        <MethodBadge method={endpoint.method} />\n        <span className=\"endpoint-path\">{endpoint.path}</span>\n        {endpoint.summary && <span className=\"endpoint-summary\">{endpoint.summary}</span>}\n        {endpoint.name && !endpoint.summary && <span className=\"endpoint-summary\">{endpoint.name}</span>}\n        {endpoint.deprecated && <span className=\"deprecated-tag\">deprecated</span>}\n        {actions && <div className=\"endpoint-actions\">{actions}</div>}\n      </div>\n    </div>\n  );\n};\n\nexport default EndpointItem;\n"
  },
  {
    "path": "packages/bruno-app/src/components/OpenAPISyncTab/EndpointChangeSection/EndpointVisualDiff.js",
    "content": "import React from 'react';\nimport isEqual from 'lodash/isEqual';\nimport get from 'lodash/get';\nimport VisualDiffUrlBar from 'components/Git/VisualDiffViewer/VisualDiffUrlBar';\nimport VisualDiffParams from 'components/Git/VisualDiffViewer/VisualDiffParams';\nimport VisualDiffHeaders from 'components/Git/VisualDiffViewer/VisualDiffHeaders';\nimport VisualDiffAuth from 'components/Git/VisualDiffViewer/VisualDiffAuth';\nimport VisualDiffBody from 'components/Git/VisualDiffViewer/VisualDiffBody';\nimport VisualDiffContent from 'components/Git/VisualDiffViewer/VisualDiffContent/index';\n\n// OpenAPI sync diff section configs (HTTP request sections only)\n// Data format matches Git diff format: data.request.url, data.request.params, etc.\nconst openAPIDiffSectionDataPaths = {\n  url: ['request.url', 'request.method'],\n  params: 'request.params',\n  headers: 'request.headers',\n  auth: 'request.auth',\n  body: 'request.body'\n};\n\nconst openAPISectionHasChanges = (sectionKey, oldData, newData) => {\n  // For body, only compare the mode and the content for the active mode(s)\n  // The full request.body object can have extra empty properties that cause false positives\n  if (sectionKey === 'body') {\n    const oldBody = get(oldData, 'request.body', {});\n    const newBody = get(newData, 'request.body', {});\n    if (oldBody.mode !== newBody.mode) return true;\n    const mode = oldBody.mode || newBody.mode;\n    if (!mode || mode === 'none') return false;\n    return !isEqual(oldBody[mode], newBody[mode]);\n  }\n\n  // For auth, only compare the mode and spec-derived fields for the active auth mode\n  // Bruno adds extra fields (pkce, credentialsId, tokenQueryKey, etc.) that don't\n  // come from the OpenAPI spec. Also, the converter generates ALL oauth2 fields\n  // regardless of grant type, but the collection only stores relevant ones per flow.\n  if (sectionKey === 'auth') {\n    const oldAuth = get(oldData, 'request.auth', {});\n    const newAuth = get(newData, 'request.auth', {});\n    if (oldAuth.mode !== newAuth.mode) return true;\n    const mode = oldAuth.mode || newAuth.mode;\n    if (!mode || mode === 'none') return false;\n    const oldConfig = oldAuth[mode] || {};\n    const newConfig = newAuth[mode] || {};\n\n    if (mode === 'oauth2') {\n      // Compare only fields relevant to the specific grant type\n      const grantType = oldConfig.grantType || newConfig.grantType;\n      const commonFields = ['grantType', 'scope', 'state'];\n      const grantTypeFields = {\n        authorization_code: [...commonFields, 'authorizationUrl', 'accessTokenUrl', 'refreshTokenUrl', 'callbackUrl', 'clientId', 'clientSecret'],\n        implicit: [...commonFields, 'authorizationUrl', 'callbackUrl'],\n        password: [...commonFields, 'accessTokenUrl', 'refreshTokenUrl', 'clientId', 'clientSecret'],\n        client_credentials: [...commonFields, 'accessTokenUrl', 'clientId', 'clientSecret']\n      };\n      const fields = grantTypeFields[grantType] || commonFields;\n      return fields.some((field) => !isEqual(oldConfig[field], newConfig[field]));\n    }\n\n    // Other auth modes: compare only spec-relevant fields\n    const specFields = {\n      basic: ['username', 'password'],\n      bearer: ['token'],\n      apikey: ['key', 'value', 'placement'],\n      digest: ['username', 'password']\n    };\n    const fields = specFields[mode];\n    if (fields) {\n      return fields.some((field) => !isEqual(oldConfig[field], newConfig[field]));\n    }\n    return !isEqual(oldConfig, newConfig);\n  }\n\n  const paths = openAPIDiffSectionDataPaths[sectionKey];\n\n  if (Array.isArray(paths)) {\n    return paths.some((path) => !isEqual(get(oldData, path), get(newData, path)));\n  }\n\n  return !isEqual(get(oldData, paths), get(newData, paths));\n};\n\nconst openAPIDiffHasContent = {\n  url: (data) => data?.request?.url || data?.request?.method,\n  params: (data) => data?.request?.params && data.request.params.length > 0,\n  headers: (data) => data?.request?.headers && data.request.headers.length > 0,\n  auth: (data) => data?.request?.auth && data.request.auth.mode && data.request.auth.mode !== 'none',\n  body: (data) => {\n    if (!data?.request?.body) return false;\n    const mode = data.request.body.mode;\n    if (!mode || mode === 'none') return false;\n    return data.request.body.json || data.request.body.text || data.request.body.xml\n      || data.request.body.graphql || data.request.body.formUrlEncoded?.length > 0\n      || data.request.body.multipartForm?.length > 0;\n  }\n};\n\nconst openAPIDiffSections = [\n  { key: 'url', title: 'URL', Component: VisualDiffUrlBar, hasContent: openAPIDiffHasContent.url },\n  { key: 'params', title: 'Parameters', Component: VisualDiffParams, hasContent: openAPIDiffHasContent.params },\n  { key: 'headers', title: 'Headers', Component: VisualDiffHeaders, hasContent: openAPIDiffHasContent.headers },\n  { key: 'auth', title: 'Authentication', Component: VisualDiffAuth, hasContent: openAPIDiffHasContent.auth },\n  { key: 'body', title: 'Body', Component: VisualDiffBody, hasContent: openAPIDiffHasContent.body }\n];\n\n/**\n * EndpointVisualDiff - Wrapper around VisualDiffContent for OpenAPI sync\n *\n * Props:\n * - oldData: data from collection (actual current state)\n * - newData: data from spec (expected state)\n * - leftLabel/rightLabel: custom labels for diff panes\n * - swapSides: if true, show spec on left and collection on right\n */\nconst EndpointVisualDiff = ({\n  oldData,\n  newData,\n  leftLabel = 'Current (in collection)',\n  rightLabel = 'Expected (from spec)',\n  swapSides = false\n}) => {\n  const sections = openAPIDiffSections;\n\n  // Determine which data goes on which side based on swapSides\n  const displayOldData = swapSides ? newData : oldData;\n  const displayNewData = swapSides ? oldData : newData;\n\n  return (\n    <VisualDiffContent\n      oldData={displayOldData}\n      newData={displayNewData}\n      sections={sections}\n      sectionHasChanges={openAPISectionHasChanges}\n      oldLabel={leftLabel}\n      newLabel={rightLabel}\n      hideUnchanged={true}\n    />\n  );\n};\n\nexport default EndpointVisualDiff;\n"
  },
  {
    "path": "packages/bruno-app/src/components/OpenAPISyncTab/EndpointChangeSection/ExpandableEndpointRow.js",
    "content": "import React, { useCallback, useEffect, useState } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport {\n  IconChevronRight,\n  IconChevronDown,\n  IconCheck,\n  IconX,\n  IconLoader2\n} from '@tabler/icons';\nimport { toggleRowExpanded } from 'providers/ReduxStore/slices/openapi-sync';\nimport MethodBadge from 'ui/MethodBadge';\nimport { formatIpcError } from 'utils/common/error';\nimport StatusBadge from 'ui/StatusBadge';\nimport Help from 'components/Help';\nimport EndpointVisualDiff from './EndpointVisualDiff';\n\n// Expandable row - can be used with or without decision buttons\nconst ExpandableEndpointRow = ({ endpoint, decision, onDecisionChange, collectionPath, newSpec, showDecisions = true, decisionLabels, diffLeftLabel, diffRightLabel, swapDiffSides, collectionUid, actions }) => {\n  const dispatch = useDispatch();\n  const rowKey = endpoint.id || `${endpoint.method}-${endpoint.path}`;\n  const isExpanded = useSelector((state) => {\n    return state.openapiSync?.tabUiState?.[collectionUid]?.expandedRows?.[rowKey] || false;\n  });\n  const [isLoading, setIsLoading] = useState(false);\n  const [diffData, setDiffData] = useState(null);\n  const [error, setError] = useState(null);\n\n  const loadDiffData = useCallback(async () => {\n    if (diffData) return;\n\n    setIsLoading(true);\n    setError(null);\n\n    try {\n      const { ipcRenderer } = window;\n      const result = await ipcRenderer.invoke('renderer:get-endpoint-diff-data', {\n        collectionPath,\n        endpointId: endpoint.id,\n        newSpec\n      });\n\n      if (result.error) {\n        setError(result.error);\n      } else {\n        setDiffData(result);\n      }\n    } catch (err) {\n      setError(formatIpcError(err) || 'Failed to load diff data');\n    } finally {\n      setIsLoading(false);\n    }\n  }, [collectionPath, endpoint.id, newSpec]);\n\n  // Load diff data when expanded (e.g. restored from Redux state)\n  useEffect(() => {\n    if (isExpanded && !diffData && !isLoading && !error) {\n      loadDiffData();\n    }\n  }, [isExpanded, diffData, isLoading, loadDiffData, error]);\n\n  const handleToggle = () => {\n    const willExpand = !isExpanded;\n    if (collectionUid) {\n      dispatch(toggleRowExpanded({ collectionUid, rowKey }));\n    }\n    if (willExpand && !diffData && !isLoading) {\n      loadDiffData();\n    }\n  };\n\n  return (\n    <div className={`endpoint-review-row ${showDecisions ? `decision-${decision}` : ''}`}>\n      <div\n        className=\"review-row-header\"\n        role=\"button\"\n        tabIndex={0}\n        aria-expanded={isExpanded}\n        onClick={handleToggle}\n        onKeyDown={(e) => {\n          if (e.key === 'Enter' || e.key === ' ') {\n            e.preventDefault(); handleToggle();\n          }\n        }}\n      >\n        <span className=\"expand-toggle\">\n          {isExpanded ? <IconChevronDown size={14} /> : <IconChevronRight size={14} />}\n        </span>\n        <MethodBadge method={endpoint.method} />\n        <span className=\"endpoint-path\">{endpoint.path}</span>\n        {endpoint.summary && <span className=\"endpoint-name\">{endpoint.summary}</span>}\n        {endpoint.name && !endpoint.summary && <span className=\"endpoint-name\">{endpoint.name}</span>}\n        {endpoint.conflict && (\n          <StatusBadge\n            status=\"danger\"\n            rightSection={(\n              <Help icon=\"info\" size={11} placement=\"top\" width={250}>\n                This endpoint was modified in both the spec and your collection. Choose which version to keep.\n              </Help>\n            )}\n          >\n            Conflict\n          </StatusBadge>\n        )}\n\n        {actions && <div className=\"endpoint-actions\" onClick={(e) => e.stopPropagation()}>{actions}</div>}\n\n        {showDecisions && onDecisionChange && (\n          <div className=\"decision-buttons\" onClick={(e) => e.stopPropagation()}>\n            <button\n              className={`decision-btn keep ${decision === 'keep-mine' ? 'selected' : ''}`}\n              onClick={() => onDecisionChange('keep-mine')}\n              title=\"Keep your local version\"\n            >\n              <IconX size={12} /> {decisionLabels?.keep || 'Keep Mine'}\n            </button>\n            <button\n              className={`decision-btn accept ${decision === 'accept-incoming' ? 'selected' : ''}`}\n              onClick={() => onDecisionChange('accept-incoming')}\n              title=\"Accept the spec version\"\n            >\n              <IconCheck size={12} /> {decisionLabels?.accept || 'Accept Spec'}\n            </button>\n          </div>\n        )}\n      </div>\n\n      {isExpanded && (\n        <div className=\"review-row-diff\">\n          {isLoading && (\n            <div className=\"diff-loading\">\n              <IconLoader2 size={16} className=\"spinning\" />\n              <span>Loading diff...</span>\n            </div>\n          )}\n          {error && (\n            <div className=\"diff-error\">\n              Error: {error}\n            </div>\n          )}\n          {diffData && !isLoading && !error && (\n            <EndpointVisualDiff\n              oldData={diffData.oldData}\n              newData={diffData.newData}\n              leftLabel={diffLeftLabel || 'Current (in collection)'}\n              rightLabel={diffRightLabel || 'Expected (from spec)'}\n              swapSides={swapDiffSides}\n            />\n          )}\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default ExpandableEndpointRow;\n"
  },
  {
    "path": "packages/bruno-app/src/components/OpenAPISyncTab/EndpointChangeSection/index.js",
    "content": "import React from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { IconChevronRight } from '@tabler/icons';\nimport { toggleSectionExpanded } from 'providers/ReduxStore/slices/openapi-sync';\n\n/**\n * Collapsible section container for endpoint lists.\n * Renders a clickable header (with chevron, dot, title, count) and a body of items.\n * Expand/collapse state is persisted in Redux via collectionUid + sectionKey.\n *\n * @param {string} title - Section heading\n * @param {string} type - CSS modifier for color theming (e.g. 'modified', 'missing', 'in-sync')\n * @param {Array} endpoints - Items to render; section is hidden when empty\n * @param {Function} renderItem - (endpoint, idx) => ReactNode\n * @param {boolean} [defaultExpanded=false] - Fallback when no Redux state exists\n * @param {boolean} [expandableLayout=false] - Removes max-height scroll constraint on body\n * @param {ReactNode} [actions] - Header-right buttons (wrapped in a stopPropagation container)\n * @param {string} [subtitle] - Secondary text after the count\n * @param {ReactNode} [headerExtra] - Extra content shown in header only when collapsed\n * @param {string} collectionUid - Redux key for persisting expand/collapse state\n * @param {string} sectionKey - Redux key for persisting expand/collapse state\n */\nconst EndpointChangeSection = ({\n  title,\n  type,\n  endpoints,\n  defaultExpanded = false,\n  actions,\n  subtitle,\n  renderItem,\n  expandableLayout = false,\n  headerExtra,\n  collectionUid,\n  sectionKey\n}) => {\n  const dispatch = useDispatch();\n  const reduxExpanded = useSelector((state) => {\n    if (!collectionUid || !sectionKey) return undefined;\n    return state.openapiSync?.tabUiState?.[collectionUid]?.expandedSections?.[sectionKey];\n  });\n  const isExpanded = reduxExpanded !== undefined ? reduxExpanded : defaultExpanded;\n\n  if (endpoints.length === 0) return null;\n\n  return (\n    <div className={`change-section type-${type}${isExpanded ? ' expanded' : ''}`}>\n      <div\n        className=\"section-header\"\n        role=\"button\"\n        tabIndex={0}\n        onClick={() => {\n          if (collectionUid && sectionKey) {\n            dispatch(toggleSectionExpanded({ collectionUid, sectionKey }));\n          }\n        }}\n        onKeyDown={(e) => {\n          if (e.key === 'Enter' || e.key === ' ') {\n            e.preventDefault();\n            if (collectionUid && sectionKey) {\n              dispatch(toggleSectionExpanded({ collectionUid, sectionKey }));\n            }\n          }\n        }}\n      >\n        <IconChevronRight size={16} className={`chevron ${isExpanded ? 'expanded' : ''}`} />\n        <span className={`section-dot type-${type}`} />\n        <span className=\"section-title\">{title}</span>\n        <span className=\"section-count\">{endpoints.length}</span>\n        {subtitle && <span className=\"section-subtitle\">{subtitle}</span>}\n        {!isExpanded && headerExtra}\n        {actions && <div className=\"section-actions\" onClick={(e) => e.stopPropagation()}>{actions}</div>}\n      </div>\n      {isExpanded && (\n        <div className={`section-body${expandableLayout ? ' expandable-mode' : ''}`}>\n          {endpoints.map((endpoint, idx) => renderItem(endpoint, idx))}\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default EndpointChangeSection;\n"
  },
  {
    "path": "packages/bruno-app/src/components/OpenAPISyncTab/OpenAPISyncHeader/index.js",
    "content": "import { useState, useEffect } from 'react';\nimport { useSelector } from 'react-redux';\nimport { selectStoredSpecMeta } from 'providers/ReduxStore/slices/openapi-sync';\nimport {\n  IconCopy,\n  IconDotsVertical,\n  IconUnlink,\n  IconSettings,\n  IconRefresh,\n  IconCircleCheck,\n  IconAlertTriangle\n} from '@tabler/icons';\nimport toast from 'react-hot-toast';\nimport Button from 'ui/Button';\nimport ActionIcon from 'ui/ActionIcon/index';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport Help from 'components/Help';\nimport { isHttpUrl } from 'utils/url/index';\n\nconst OpenAPISyncHeader = ({\n  collection, spec, sourceUrl, syncStatus, onViewSpec,\n  onOpenSettings, onOpenDisconnect,\n  onCheck, isLoading\n}) => {\n  const sourceIsLocal = !isHttpUrl(sourceUrl);\n  const canCheck = !!sourceUrl?.trim();\n\n  // Resolve relative file paths to absolute for display\n  const [displayPath, setDisplayPath] = useState(sourceUrl);\n  useEffect(() => {\n    if (sourceIsLocal && sourceUrl) {\n      window.ipcRenderer.invoke('renderer:resolve-path', sourceUrl, collection.pathname)\n        .then((resolved) => setDisplayPath(resolved))\n        .catch(() => setDisplayPath(sourceUrl));\n    } else {\n      setDisplayPath(sourceUrl);\n    }\n  }, [sourceUrl, sourceIsLocal, collection.pathname]);\n\n  const specMeta = useSelector(selectStoredSpecMeta(collection.uid));\n  const title = specMeta?.title || spec?.info?.title || 'Unknown API';\n\n  const copyUrl = async () => {\n    if (!sourceUrl) return;\n    try {\n      if (sourceIsLocal) {\n        const absolutePath = await window.ipcRenderer.invoke('renderer:resolve-path', sourceUrl, collection.pathname);\n        await navigator.clipboard.writeText(absolutePath);\n      } else {\n        await navigator.clipboard.writeText(sourceUrl);\n      }\n      toast.success(sourceIsLocal ? 'Path copied to clipboard' : 'URL copied to clipboard');\n    } catch (err) {\n      console.error('Error copying to clipboard:', err);\n      toast.error('Failed to copy to clipboard');\n    }\n  };\n\n  const revealInFolder = async () => {\n    if (!sourceUrl) return;\n    try {\n      const absolutePath = await window.ipcRenderer.invoke('renderer:resolve-path', sourceUrl, collection.pathname);\n      await window.ipcRenderer.invoke('renderer:show-in-folder', absolutePath);\n    } catch (err) {\n      console.error('Error revealing in folder:', err);\n      toast.error('Failed to open in file manager');\n    }\n  };\n\n  const menuItems = [\n    {\n      id: 'settings',\n      label: 'Edit connection settings',\n      leftSection: IconSettings,\n      onClick: onOpenSettings\n    },\n    {\n      id: 'disconnect',\n      label: 'Disconnect Sync',\n      leftSection: IconUnlink,\n      className: 'delete-item',\n      onClick: onOpenDisconnect\n    }\n  ];\n\n  return (\n    <div className=\"spec-info-card\">\n      <div className=\"spec-info-header\">\n        <div className=\"spec-title-section\">\n          <div className=\"spec-title-row\">\n            <span className=\"spec-title\">{title}</span>\n          </div>\n        </div>\n        <div className=\"spec-header-actions\">\n          <Button\n            color=\"secondary\"\n            size=\"sm\"\n            onClick={onCheck}\n            disabled={!canCheck}\n            loading={isLoading}\n            icon={<IconRefresh size={14} />}\n          >\n            Check for updates\n          </Button>\n          <Button\n            color=\"secondary\"\n            size=\"sm\"\n            onClick={onViewSpec}\n          >\n            View spec\n          </Button>\n          <MenuDropdown items={menuItems} placement=\"bottom-end\">\n            <ActionIcon label=\"More options\">\n              <IconDotsVertical size={16} strokeWidth={2} />\n            </ActionIcon>\n          </MenuDropdown>\n        </div>\n      </div>\n      <div className=\"spec-url-row\">\n        <span className=\"spec-url-label\">{sourceIsLocal ? 'Source File:' : 'Source URL:'}</span>\n        {sourceIsLocal ? (\n          <button\n            className=\"spec-url-value spec-file-reveal\"\n            title=\"Reveal in file manager\"\n            type=\"button\"\n            onClick={revealInFolder}\n          >\n            {displayPath}\n          </button>\n        ) : (\n          <a\n            className=\"spec-url-value\"\n            href={sourceUrl}\n            title={sourceUrl}\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n          >\n            {sourceUrl}\n          </a>\n        )}\n        <button className=\"copy-btn\" onClick={copyUrl} title={sourceIsLocal ? 'Copy path' : 'Copy URL'} type=\"button\">\n          <IconCopy size={12} />\n        </button>\n      </div>\n      <div className=\"linked-collection-row mt-1\">\n        <span className=\"spec-url-label\">Linked Collection:</span>\n        <span className=\"linked-collection-name\">{collection.name}</span>\n        {syncStatus === 'in-sync' && (\n          <Help\n            placement=\"bottom\"\n            width={240}\n            iconComponent={() => <IconCircleCheck size={14} className=\"sync-status-icon in-sync\" />}\n          >\n            Collection is up to date with the spec\n          </Help>\n        )}\n        {syncStatus === 'not-in-sync' && (\n          <Help\n            placement=\"bottom\"\n            width={260}\n            iconComponent={() => <IconAlertTriangle size={14} className=\"sync-status-icon not-in-sync\" />}\n          >\n            Collection is not up to date with the spec\n          </Help>\n        )}\n      </div>\n    </div>\n  );\n};\n\nexport default OpenAPISyncHeader;\n"
  },
  {
    "path": "packages/bruno-app/src/components/OpenAPISyncTab/OverviewSection/index.js",
    "content": "import { useMemo } from 'react';\nimport { useSelector } from 'react-redux';\nimport { selectStoredSpecMeta } from 'providers/ReduxStore/slices/openapi-sync';\nimport { getTotalRequestCountInCollection } from 'utils/collections/';\nimport { countEndpoints } from '../utils';\nimport moment from 'moment';\nimport { IconCheck } from '@tabler/icons';\nimport Button from 'ui/Button';\nimport Help from 'components/Help';\n\nconst capitalize = (str) => str ? str.charAt(0).toUpperCase() + str.slice(1) : str;\n\nconst SUMMARY_CARDS = [\n  {\n    key: 'total',\n    label: 'Total in Collection',\n    color: 'blue',\n    tooltip: 'Total endpoints in your collection'\n  },\n  {\n    key: 'inSync',\n    label: 'In Sync with Spec',\n    color: 'green',\n    tooltip: 'Endpoints that currently match the latest spec from the source'\n  },\n  {\n    key: 'changed',\n    label: 'Changed in Collection',\n    color: 'muted',\n    tooltip: 'Endpoints modified, deleted, or added locally since last sync',\n    tab: 'collection-changes'\n  },\n  {\n    key: 'pending',\n    label: 'Spec Updates Pending',\n    color: 'amber',\n    tooltip: 'Spec changes available to sync to your collection',\n    tab: 'spec-updates'\n  }\n];\n\nconst OverviewSection = ({ collection, storedSpec, collectionDrift, specDrift, remoteDrift, onTabSelect, error, onOpenSettings }) => {\n  const openApiSyncConfig = collection?.brunoConfig?.openapi?.[0];\n\n  const reduxError = useSelector((state) => state.openapiSync?.collectionUpdates?.[collection.uid]?.error);\n  const specMeta = useSelector(selectStoredSpecMeta(collection.uid));\n  const activeError = error || reduxError;\n\n  const version = specMeta?.version;\n  const endpointCount = specMeta?.endpointCount ?? null;\n  const lastSyncDate = openApiSyncConfig?.lastSyncDate;\n  const groupBy = openApiSyncConfig?.groupBy || 'tags';\n  const autoCheckEnabled = openApiSyncConfig?.autoCheck !== false;\n  const autoCheckInterval = openApiSyncConfig?.autoCheckInterval || 5;\n\n  // Endpoint Summary counts\n  // Total: from collection items in Redux; In Sync: from remote spec comparison\n  // Changed/Conflicts: compare against stored spec in AppData (0 on initial sync)\n  const hasDriftData = collectionDrift && !collectionDrift.noStoredSpec;\n\n  const totalInCollection = getTotalRequestCountInCollection(collection);\n\n  const inSyncCount = remoteDrift\n    ? (remoteDrift.inSync?.length || 0)\n    : null;\n\n  const changedInCollection = hasDriftData\n    ? (collectionDrift.modified?.length || 0) + (collectionDrift.missing?.length || 0) + (collectionDrift.localOnly?.length || 0)\n    : 0;\n\n  const specUpdatesPending = hasDriftData\n    ? (specDrift?.added?.length || 0) + (specDrift?.modified?.length || 0) + (specDrift?.removed?.length || 0)\n    : (remoteDrift?.modified?.length || 0) + (remoteDrift?.missing?.length || 0);\n\n  // Conflict count: endpoints modified in both spec and collection\n  const conflictCount = hasDriftData && specDrift?.modified\n    ? (() => {\n        const localModifiedIds = new Set((collectionDrift.modified || []).map((ep) => ep.id));\n        return specDrift.modified.filter((ep) => localModifiedIds.has(ep.id)).length;\n      })()\n    : 0;\n\n  const summaryValues = {\n    total: totalInCollection,\n    inSync: inSyncCount,\n    changed: changedInCollection,\n    pending: activeError ? null : specDrift ? specUpdatesPending : null\n  };\n\n  const details = [\n    { label: 'Spec Version', value: version ? `v${version}` : '–' },\n    { label: 'Endpoints in Spec', value: endpointCount != null ? endpointCount : '–' },\n    { label: 'Last Synced At', value: lastSyncDate ? moment(lastSyncDate).fromNow() : '–', tooltip: lastSyncDate ? moment(lastSyncDate).format('MMMM D, YYYY [at] h:mm A') : undefined },\n    { label: 'Folder Grouping', value: capitalize(groupBy) },\n    { label: 'Auto Check for Updates', value: autoCheckEnabled ? `Every ${autoCheckInterval} min` : 'Disabled' }\n  ];\n\n  const hasCollectionChanges = changedInCollection > 0;\n  const hasSpecUpdates = specUpdatesPending > 0;\n\n  const bannerState = useMemo(() => {\n    const versionInfo = (specDrift?.storedVersion && specDrift?.newVersion && specDrift.storedVersion !== specDrift.newVersion)\n      ? ` (v${specDrift.storedVersion} → v${specDrift.newVersion})`\n      : '';\n\n    if (activeError) {\n      return {\n        variant: 'danger',\n        title: 'Failed to check for spec updates',\n        subtitle: activeError,\n        buttons: ['open-settings']\n      };\n    }\n    if (specDrift?.storedSpecMissing && !lastSyncDate) {\n      return {\n        variant: 'warning',\n        title: 'Initial sync required — your collection differs from the spec',\n        subtitle: 'Review the changes and sync to bring your collection up to date.',\n        buttons: ['review']\n      };\n    }\n    if (hasSpecUpdates && hasCollectionChanges) {\n      return {\n        variant: 'warning',\n        title: `OpenAPI spec has new updates${versionInfo} and the collection has changes`,\n        subtitle: 'New or changed requests are available. Some collection changes may be overwritten.',\n        buttons: ['sync', 'changes']\n      };\n    }\n    if (hasSpecUpdates) {\n      return {\n        variant: 'warning',\n        title: `OpenAPI spec has new updates${versionInfo}`,\n        subtitle: 'New or changed requests are available.',\n        buttons: ['sync']\n      };\n    }\n    if (specDrift?.storedSpecMissing && lastSyncDate) {\n      return {\n        variant: 'warning',\n        title: 'Last synced spec not found',\n        subtitle: 'The last synced spec is missing in the storage. Restore the latest spec from the source to track collection changes.',\n        buttons: ['spec-details']\n      };\n    }\n    if (!hasDriftData) return null;\n    if (hasCollectionChanges) {\n      return {\n        variant: 'muted',\n        title: 'Collection has changes not in the spec',\n        subtitle: 'Some requests have been modified or removed and no longer match the spec.',\n        buttons: ['changes']\n      };\n    }\n    return null;\n  }, [activeError, hasDriftData, hasSpecUpdates, hasCollectionChanges, specDrift?.storedSpecMissing, specDrift?.storedVersion, specDrift?.newVersion, lastSyncDate]);\n\n  return (\n    <div className=\"overview-section\">\n      {bannerState && (\n        <div className={`overview-status-banner ${bannerState.variant}`}>\n          <div className=\"banner-text\">\n            <div className=\"banner-title-row\">\n              {bannerState.variant === 'success'\n                ? <IconCheck size={16} className=\"status-check-icon\" />\n                : <div className={`status-dot ${bannerState.variant}`} />}\n              <span className=\"banner-title\">{bannerState.title}</span>\n            </div>\n            {bannerState.subtitle && (\n              <p className=\"banner-subtitle\">{bannerState.subtitle}</p>\n            )}\n          </div>\n          {bannerState.buttons.length > 0 && (\n            <div className=\"banner-button-row\">\n              {bannerState.buttons.includes('changes') && (\n                <Button\n                  size=\"sm\"\n                  variant={bannerState.buttons.includes('sync') ? 'outline' : 'filled'}\n                  color={bannerState.buttons.includes('sync') ? 'secondary' : 'primary'}\n                  onClick={() => onTabSelect('collection-changes')}\n                >\n                  View Collection Changes\n                </Button>\n              )}\n              {(bannerState.buttons.includes('sync') || bannerState.buttons.includes('review')) && (\n                <Button size=\"sm\" onClick={() => onTabSelect('spec-updates')}>\n                  Review and Sync Collection\n                </Button>\n              )}\n              {bannerState.buttons.includes('spec-details') && (\n                <Button variant=\"outline\" size=\"sm\" onClick={() => onTabSelect('spec-updates')}>\n                  Go to Spec Updates\n                </Button>\n              )}\n              {bannerState.buttons.includes('open-settings') && (\n                <Button variant=\"outline\" size=\"sm\" onClick={onOpenSettings}>\n                  Update connection settings\n                </Button>\n              )}\n            </div>\n          )}\n        </div>\n      )}\n\n      <h4 className=\"overview-section-title mt-5\">Endpoint Summary</h4>\n      <div className=\"sync-summary-cards\">\n        {SUMMARY_CARDS.map(({ key, label, tooltip, tab, color }) => {\n          const count = summaryValues[key];\n          const resolvedColor = count > 0 ? color : 'muted';\n          const isClickable = tab && count > 0;\n          return (\n            <div\n              className={`summary-card${isClickable ? ' clickable' : ''}`}\n              key={key}\n              onClick={isClickable ? () => onTabSelect(tab) : undefined}\n            >\n              <span className=\"card-info-icon\">\n                <Help icon=\"info\" size={12} placement=\"top\" width={220}>{tooltip}</Help>\n              </span>\n              <div className=\"summary-count-row\">\n                <span className={`summary-count ${resolvedColor}`}>{count != null ? count : '–'}</span>\n                {key === 'pending' && conflictCount > 0 && (\n                  <span className=\"conflict-annotation\">({conflictCount} {conflictCount === 1 ? 'conflict' : 'conflicts'})</span>\n                )}\n              </div>\n              <div className=\"summary-label\">\n                {label}\n              </div>\n            </div>\n          );\n        })}\n      </div>\n\n      <h4 className=\"overview-section-title mt-7\">Last Synced Spec Details</h4>\n      <div className=\"spec-details-grid\">\n        {details.map(({ label, value, tooltip }) => (\n          <div className=\"spec-detail-item\" key={label}>\n            <div className=\"spec-detail-label\">{label}</div>\n            <div className=\"spec-detail-value\">\n              {value}\n              {tooltip && (\n                <Help icon=\"info\" size={11} placement=\"top\" width={200}>{tooltip}</Help>\n              )}\n            </div>\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n};\n\nexport default OverviewSection;\n"
  },
  {
    "path": "packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/index.js",
    "content": "import { useRef, useEffect, useState } from 'react';\nimport { useTheme } from 'providers/Theme/index';\nimport { IconLoader2 } from '@tabler/icons';\nimport Modal from 'components/Modal';\nimport StatusBadge from 'ui/StatusBadge';\n\nconst SpecDiffModal = ({ specDrift, onClose }) => {\n  const diffRef = useRef(null);\n  const { displayedTheme } = useTheme();\n  const [isRendering, setIsRendering] = useState(true);\n\n  const addedCount = specDrift?.added?.length || 0;\n  const modifiedCount = specDrift?.modified?.length || 0;\n  const removedCount = specDrift?.removed?.length || 0;\n\n  const versionLabel = specDrift?.versionChanged\n    ? `v${specDrift.storedVersion || '?'} → v${specDrift.newVersion}`\n    : null;\n\n  useEffect(() => {\n    const { Diff2Html } = window;\n    if (!diffRef?.current || !Diff2Html || !specDrift?.unifiedDiff) {\n      setIsRendering(false);\n      return;\n    }\n    setIsRendering(true);\n    const diffHtml = Diff2Html.html(specDrift.unifiedDiff, {\n      drawFileList: false,\n      matching: 'lines',\n      outputFormat: 'side-by-side',\n      synchronisedScroll: true,\n      highlight: true,\n      renderNothingWhenEmpty: false,\n      colorScheme: displayedTheme\n    });\n    // Safe: Diff2Html is loaded from a local static bundle (public/static/diff2Html.js)\n    diffRef.current.innerHTML = diffHtml;\n    setIsRendering(false);\n  }, [displayedTheme, specDrift?.unifiedDiff]);\n\n  return (\n    <Modal\n      size=\"xl\"\n      title=\"Spec Diff\"\n      hideFooter\n      handleCancel={onClose}\n    >\n      <div className=\"spec-diff-modal\">\n        <div className=\"spec-diff-badges\">\n          {modifiedCount > 0 && <StatusBadge status=\"warning\">Updated: {modifiedCount}</StatusBadge>}\n          {addedCount > 0 && <StatusBadge status=\"success\">Added: {addedCount}</StatusBadge>}\n          {removedCount > 0 && <StatusBadge status=\"danger\">Removed: {removedCount}</StatusBadge>}\n          {versionLabel && <StatusBadge>{versionLabel}</StatusBadge>}\n        </div>\n\n        <p className=\"spec-diff-subtitle\">\n          {specDrift?.storedSpecMissing\n            ? 'The current spec file is missing. The full remote spec is shown below.'\n            : 'Side-by-side diff of your current spec vs the updated spec from the spec URL.'}\n        </p>\n\n        <div className=\"spec-diff-body\">\n          <div className=\"text-diff-container\">\n            {specDrift?.unifiedDiff ? (\n              <>\n                <div className=\"diff-column-headers\">\n                  <span className=\"diff-column-label\">{specDrift?.storedSpecMissing ? 'Current Spec (missing)' : 'Current Spec'}</span>\n                  <span className=\"diff-column-label\">Updated Spec</span>\n                </div>\n                {isRendering && (\n                  <div className=\"text-diff-loading\">\n                    <IconLoader2 className=\"animate-spin\" size={20} strokeWidth={1.5} />\n                    <span>Loading diff...</span>\n                  </div>\n                )}\n                <div ref={diffRef} style={{ display: isRendering ? 'none' : 'block' }}></div>\n              </>\n            ) : (\n              <div className=\"text-diff-empty\">No text diff available.</div>\n            )}\n          </div>\n        </div>\n      </div>\n    </Modal>\n  );\n};\n\nexport default SpecDiffModal;\n"
  },
  {
    "path": "packages/bruno-app/src/components/OpenAPISyncTab/SpecStatusSection/index.js",
    "content": "import { useMemo } from 'react';\nimport { useSelector } from 'react-redux';\nimport {\n  IconCheck,\n  IconRefresh,\n  IconAlertTriangle,\n  IconClock\n} from '@tabler/icons';\nimport Button from 'ui/Button';\nimport StatusBadge from 'ui/StatusBadge';\nimport ConfirmSyncModal from '../ConfirmSyncModal';\nimport SyncReviewPage from '../SyncReviewPage';\nimport useSyncFlow from '../hooks/useSyncFlow';\n\nconst SpecStatusSection = ({\n  collection, sourceUrl,\n  isLoading, error, setError, fileNotFound,\n  specDrift, storedSpec,\n  collectionDrift, remoteDrift,\n  onCheck, onOpenSettings\n}) => {\n  const openApiSyncConfig = collection?.brunoConfig?.openapi?.[0];\n  const lastCheckedAt = useSelector((state) => state.openapiSync?.collectionUpdates?.[collection.uid]?.lastChecked);\n\n  const {\n    isSyncing, showConfirmModal, confirmGroups,\n    handleRestoreSpec, handleApplySync, cancelConfirmModal, handleConfirmModalSync\n  } = useSyncFlow({\n    collection, specDrift, remoteDrift, collectionDrift,\n    setError, checkForUpdates: onCheck\n  });\n\n  const lastSyncedAt = openApiSyncConfig?.lastSyncDate;\n\n  const hasRemoteUpdates = remoteDrift && (\n    (remoteDrift.missing?.length || 0)\n    + (remoteDrift.modified?.length || 0)\n    + (remoteDrift.localOnly?.length || 0)\n  ) > 0;\n\n  const bannerState = useMemo(() => {\n    if (fileNotFound) {\n      return { variant: 'danger', message: `Source file not found at ${sourceUrl}`, actions: ['open-settings'] };\n    }\n    if (error || specDrift?.isValid === false) {\n      return { variant: 'danger', message: error || specDrift?.error || 'Invalid OpenAPI specification', actions: ['open-settings'] };\n    }\n    if (!specDrift) {\n      return null;\n    }\n    if (specDrift.storedSpecMissing && !hasRemoteUpdates) {\n      return null;\n    }\n    const hasEndpointUpdates = specDrift.storedSpecMissing\n      ? hasRemoteUpdates\n      : (specDrift.added?.length || 0) + (specDrift.modified?.length || 0) + (specDrift.removed?.length || 0) > 0;\n    if (hasEndpointUpdates) {\n      const versionInfo = (specDrift.storedVersion && specDrift.newVersion && specDrift.storedVersion !== specDrift.newVersion)\n        ? ` (v${specDrift.storedVersion} → v${specDrift.newVersion})`\n        : '';\n      return {\n        variant: 'warning', message: `OpenAPI spec has been updated${versionInfo}`, actions: [],\n        changes: { added: specDrift.added?.length || 0, modified: specDrift.modified?.length || 0, removed: specDrift.removed?.length || 0 }\n      };\n    }\n    return null;\n  }, [fileNotFound, error, sourceUrl, specDrift, lastSyncedAt, storedSpec, lastCheckedAt, hasRemoteUpdates]);\n  return (\n    <>\n      {bannerState && (\n        <div className=\"spec-status-section\">\n\n          <div className={`spec-update-banner ${bannerState.variant}`}>\n            <div className=\"banner-left\">\n              {bannerState.variant === 'success'\n                ? <IconCheck size={16} className=\"status-check-icon\" />\n                : <div className={`status-dot ${bannerState.variant}`} />}\n              <span className=\"banner-title\">\n                {bannerState.message}\n                {bannerState.version && (\n                  <> &middot; <code style={{ fontStyle: 'normal' }} className=\"checked-text\">v{bannerState.version}</code></>\n                )}\n                {bannerState.lastChecked && (\n                  <span className=\"checked-text\"> &middot; Checked {bannerState.lastChecked}</span>\n                )}\n              </span>\n              {bannerState.changes && (\n                <span className=\"banner-details\">\n                  {bannerState.changes.modified > 0 && <StatusBadge key=\"modified\" status=\"warning\" radius=\"full\">{bannerState.changes.modified} {bannerState.changes.modified > 1 ? 'endpoints' : 'endpoint'} updated</StatusBadge>}\n                  {bannerState.changes.added > 0 && <StatusBadge key=\"added\" status=\"success\" radius=\"full\">{bannerState.changes.added} {bannerState.changes.added > 1 ? 'endpoints' : 'endpoint'} added</StatusBadge>}\n                  {bannerState.changes.removed > 0 && <StatusBadge key=\"removed\" status=\"danger\" radius=\"full\">{bannerState.changes.removed} {bannerState.changes.removed > 1 ? 'endpoints' : 'endpoint'} removed</StatusBadge>}\n                </span>\n              )}\n            </div>\n            <div className=\"banner-actions\">\n              {bannerState.actions.includes('open-settings') && (\n                <Button variant=\"ghost\" size=\"sm\" onClick={onOpenSettings}>\n                  Update connection settings\n                </Button>\n              )}\n            </div>\n          </div>\n        </div>\n      )}\n\n      {(error || fileNotFound || specDrift?.isValid === false) ? (\n        <div className=\"sync-review-empty-state mt-5\">\n          <IconAlertTriangle size={40} className=\"empty-state-icon\" />\n          <h4>Unable to check for updates</h4>\n          <p>Fix the connection issue above and check again.</p>\n        </div>\n      ) : specDrift?.storedSpecMissing && openApiSyncConfig?.lastSyncDate && !hasRemoteUpdates ? (\n        <div className=\"sync-review-empty-state mt-5\">\n          <IconCheck size={40} className=\"empty-state-icon\" />\n          <h4>No updates from the spec</h4>\n          <p>The spec endpoints have not been updated since the last sync. You can restore the spec file to track local collection changes.</p>\n          <Button className=\"mt-4\" color=\"warning\" onClick={handleRestoreSpec} loading={isSyncing}>\n            Restore Spec File\n          </Button>\n        </div>\n      ) : (\n        <div className=\"mt-5\">\n          <SyncReviewPage\n            specDrift={specDrift}\n            remoteDrift={remoteDrift}\n            collectionDrift={collectionDrift}\n            collectionPath={collection.pathname}\n            collectionUid={collection.uid}\n            newSpec={specDrift?.newSpec}\n            isSyncing={isSyncing}\n            isLoading={isLoading}\n            onApplySync={handleApplySync}\n          />\n        </div>\n      )}\n\n      {showConfirmModal && (\n        <ConfirmSyncModal\n          groups={confirmGroups}\n          isSyncing={isSyncing}\n          onCancel={cancelConfirmModal}\n          onSync={handleConfirmModalSync}\n        />\n      )}\n    </>\n  );\n};\n\nexport default SpecStatusSection;\n"
  },
  {
    "path": "packages/bruno-app/src/components/OpenAPISyncTab/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba, darken } from 'polished';\n\nconst StyledWrapper = styled.div`\n\n  .setup-header {\n    margin-bottom: 1.5rem;\n  }\n\n  .setup-title {\n    font-size: ${(props) => props.theme.font.size.base};\n    font-weight: 600;\n    color: ${(props) => props.theme.text};\n    margin: 0 0 0.375rem 0;\n  }\n\n  .setup-description {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.muted};\n    line-height: 1.5;\n    margin: 0;\n  }\n\n  .setup-form {\n    border: 1px solid ${(props) => props.theme.border.border1};\n    border-radius: ${(props) => props.theme.border.radius.md};\n    padding: 1rem;\n    margin-bottom: 1.25rem;\n\n    .url-label {\n      display: block;\n      font-size: ${(props) => props.theme.font.size.sm};\n      font-weight: 500;\n      color: ${(props) => props.theme.text};\n      margin-bottom: 0.5rem;\n    }\n\n    .url-row {\n      display: flex;\n      gap: 0.5rem;\n      align-items: center;\n      margin-top: 0.25rem;\n    }\n\n    .url-input {\n      flex: 1;\n      padding: 0.25rem 0.75rem;\n      font-size: ${(props) => props.theme.font.size.sm};\n      color: ${(props) => props.theme.text};\n      background: ${(props) => props.theme.input.bg};\n      border: 1px solid ${(props) => props.theme.input.border};\n      border-radius: ${(props) => props.theme.border.radius.md};\n      outline: none;\n\n      &:focus {\n        border-color: ${(props) => props.theme.input.focusBorder};\n      }\n\n      &::placeholder {\n        color: ${(props) => props.theme.colors.text.muted};\n      }\n    }\n  }\n\n  .setup-hint {\n    font-size: ${(props) => props.theme.font.size.xs};\n    color: ${(props) => props.theme.colors.text.muted};\n    margin: 0.5rem 0 0 0;\n  }\n\n  .setup-error {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.danger};\n    margin: 0.5rem 0 0 0;\n  }\n\n  .setup-features {\n    display: flex;\n    flex-direction: column;\n    gap: 0.375rem;\n  }\n\n  .setup-feature {\n    display: flex;\n    align-items: center;\n    gap: 0.5rem;\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.muted};\n\n    svg {\n      color: ${(props) => props.theme.colors.text.green};\n      flex-shrink: 0;\n    }\n  }\n\n  /* Spec Info Card — borderless header */\n  .spec-info-card {\n    margin-bottom: 14px;\n\n    .spec-info-header {\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n    }\n\n    .spec-title-section {\n      display: flex;\n      align-items: center;\n      gap: 8px;\n      min-width: 0;\n    }\n\n    .spec-title-row {\n      display: flex;\n      align-items: center;\n      gap: 8px;\n    }\n\n    .spec-title {\n      font-weight: 600;\n      font-size: 13px;\n      color: ${(props) => props.theme.text};\n    }\n\n    .spec-version {\n      font-family: monospace;\n    }\n\n    .spec-header-actions {\n      display: flex;\n      align-items: center;\n      gap: 6px;\n      flex-shrink: 0;\n    }\n\n    .spec-url-label {\n      color: ${(props) => props.theme.colors.text.muted};\n      flex-shrink: 0;\n    }\n\n    .spec-url-row {\n      display: flex;\n      align-items: center;\n      gap: 6px;\n      font-size: 11px;\n      margin-top: 0.35rem;\n\n      .spec-url-value {\n        font-family: monospace;\n        color: ${(props) => props.theme.colors.text.subtext0};\n        overflow: hidden;\n        text-overflow: ellipsis;\n        white-space: nowrap;\n        min-width: 0;\n        text-decoration: none;\n\n        &:hover {\n          text-decoration: underline;\n          color: ${(props) => props.theme.status.info.text};\n        }\n      }\n\n      .spec-file-reveal {\n        background: none;\n        border: none;\n        padding: 0;\n        text-align: left;\n        cursor: pointer;\n\n        &:hover {\n          text-decoration: underline;\n          color: ${(props) => props.theme.status.info.text};\n        }\n      }\n\n    }\n\n    .linked-collection-row {\n      display: flex;\n      align-items: center;\n      gap: 6px;\n      font-size: 11px;\n\n      .linked-collection-name {\n        color: ${(props) => props.theme.colors.text.subtext0};\n      }\n\n      .sync-status-icon {\n        &.in-sync {\n          color: ${(props) => props.theme.colors.text.green};\n        }\n\n        &.not-in-sync {\n          color: ${(props) => props.theme.colors.text.warning};\n        }\n      }\n    }\n\n    .copy-btn {\n      flex-shrink: 0;\n      padding: 0 4px;\n      background: none;\n      border: none;\n      color: ${(props) => props.theme.colors.text.muted};\n      cursor: pointer;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n\n      &:hover {\n        color: ${(props) => props.theme.text};\n      }\n    }\n\n  }\n\n  /* Overview Status Banner */\n  .overview-status-banner {\n    display: flex;\n    flex-wrap: wrap;\n    align-items: center;\n    justify-content: space-between;\n    gap: 8px;\n    padding: 10px 16px;\n    border-radius: 10px;\n    border: 1px solid transparent;\n    margin-top: 20px;\n\n    &.success {\n      background: ${(props) => rgba(props.theme.colors.text.green, 0.07)};\n      border-color: ${(props) => rgba(props.theme.colors.text.green, 0.22)};\n\n      .banner-title { color: ${(props) => props.theme.colors.text.green}; }\n    }\n\n    &.warning {\n      background: ${(props) => rgba(props.theme.colors.text.warning, 0.07)};\n      border-color: ${(props) => rgba(props.theme.colors.text.warning, 0.22)};\n\n      .banner-title { color: ${(props) => props.theme.colors.text.warning}; }\n    }\n\n    &.muted {\n      background: ${(props) => rgba(props.theme.colors.text.muted, 0.07)};\n      border-color: ${(props) => props.theme.border.border1};\n\n      .banner-title { color: ${(props) => props.theme.text}; }\n    }\n\n    &.danger {\n      background: ${(props) => rgba(props.theme.colors.text.danger || '#c0392b', 0.07)};\n      border-color: ${(props) => rgba(props.theme.colors.text.danger || '#c0392b', 0.22)};\n\n      .banner-title { color: ${(props) => props.theme.colors.text.danger}; }\n    }\n\n    &.info {\n      background: ${(props) => rgba(props.theme.status.info.text, 0.07)};\n      border-color: ${(props) => rgba(props.theme.status.info.text, 0.22)};\n\n      .banner-title { color: ${(props) => props.theme.status.info.text}; }\n    }\n\n    .banner-text {\n      flex: 1 1 0%;\n      min-width: 60%;\n    }\n\n    .banner-title-row {\n      display: flex;\n      align-items: center;\n      gap: 8px;\n    }\n\n    .status-dot.success::before {\n      animation: none;\n    }\n\n    .banner-subtitle {\n      font-size: 12px;\n      color: ${(props) => props.theme.colors.text.muted};\n      margin: 6px 0 0 16px;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      white-space: nowrap;\n    }\n\n    .banner-button-row {\n      display: flex;\n      gap: 10px;\n      flex-shrink: 0;\n      margin-left: 16px;\n    }\n  }\n\n  /* Overview Section */\n  .overview-section {\n    margin-top: 0;\n\n    .overview-section-title {\n      font-size: 12px;\n      font-weight: 600;\n      color: ${(props) => props.theme.text};\n      margin-bottom: 6px;\n    }\n\n    .spec-details-grid {\n      display: grid;\n      grid-template-columns: 1fr 1fr 1fr;\n      border: 1px solid ${(props) => props.theme.border.border1};\n      border-radius: 8px;\n    }\n\n    .spec-detail-item {\n      padding: 12px 16px;\n      border-right: 1px solid ${(props) => props.theme.border.border1};\n      border-bottom: 1px solid ${(props) => props.theme.border.border1};\n\n      &:nth-child(3n) {\n        border-right: none;\n      }\n\n      &:nth-child(n+4) {\n        border-bottom: none;\n      }\n\n      &:first-child { border-top-left-radius: 8px; }\n      &:nth-child(3) { border-top-right-radius: 8px; }\n      &:nth-child(4) { border-bottom-left-radius: 8px; }\n      &:last-child { border-bottom-right-radius: 8px; }\n    }\n\n    .spec-detail-label {\n      font-size: 10px;\n      text-transform: uppercase;\n      letter-spacing: 0.5px;\n      color: ${(props) => props.theme.colors.text.muted};\n      margin-bottom: 6px;\n      font-weight: 600;\n    }\n\n    .spec-detail-value {\n      display: inline-flex;\n      align-items: center;\n      gap: 0px;\n      font-size: 13px;\n      font-weight: 500;\n      color: ${(props) => props.theme.text};\n\n      svg {\n        opacity: 0.3;\n      }\n\n      &:hover svg {\n        opacity: 0.6;\n      }\n    }\n  }\n\n  /* Update Banner */\n  .spec-update-banner {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    gap: 8px;\n    padding: 10px 16px;\n    margin-top: 20px;\n    border-radius: 8px;\n    background: ${(props) => rgba(props.theme.dropdown.selectedColor, 0.08)};\n    border: 1px solid transparent;\n    overflow: hidden;\n\n    &.danger {\n      background: ${(props) => rgba(props.theme.colors.text.danger || '#c0392b', 0.07)};\n      border-color: ${(props) => rgba(props.theme.colors.text.danger || '#c0392b', 0.22)};\n    }\n\n    &.info {\n      background: ${(props) => rgba(props.theme.status.info.text, 0.07)};\n      border-color: ${(props) => rgba(props.theme.status.info.text, 0.22)};\n    }\n\n    &.warning {\n      background: ${(props) => rgba(props.theme.colors.text.warning, 0.07)};\n      border-color: ${(props) => rgba(props.theme.colors.text.warning, 0.22)};\n    }\n\n    &.success {\n      background: ${(props) => rgba(props.theme.colors.text.green, 0.07)};\n      border-color: ${(props) => rgba(props.theme.colors.text.green, 0.22)};\n\n      .status-dot::before {\n        animation: none;\n      }\n    }\n\n    &.muted {\n      background: ${(props) => rgba(props.theme.colors.text.muted, 0.07)};\n      border: 1px solid ${(props) => props.theme.border.border1};\n    }\n\n    .banner-left {\n      display: flex;\n      align-items: center;\n      gap: 8px;\n      min-width: 0;\n    }\n\n    .banner-title {\n      color: ${(props) => props.theme.text};\n\n      .version-code {\n        font-family: monospace;\n        font-size: 11px;\n        padding: 1px 5px;\n        border-radius: 3px;\n        background: ${(props) => props.theme.background.surface1};\n        border: 1px solid ${(props) => props.theme.border.border1};\n      }\n\n      .checked-text {\n        font-weight: 400;\n        font-size: 11px;\n        font-style: italic;\n        color: ${(props) => props.theme.colors.text.muted};\n      }\n    }\n\n    .banner-details {\n      display: inline-flex;\n      align-items: center;\n      gap: 4px;\n      flex-shrink: 0;\n    }\n\n    .banner-actions {\n      display: flex;\n      align-items: center;\n      gap: 6px;\n      flex-shrink: 0;\n    }\n\n  }\n\n  @keyframes radiate {\n    0%   { transform: scale(1); opacity: 0.6; }\n    100% { transform: scale(2.8); opacity: 0; }\n  }\n\n  .status-dot {\n    position: relative;\n    width: 12px;\n    height: 12px;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    flex-shrink: 0;\n\n    &::before {\n      content: '';\n      position: absolute;\n      width: 7px;\n      height: 7px;\n      border-radius: 50%;\n      opacity: 0.35;\n      animation: radiate 1.6s ease-out infinite;\n    }\n\n    &::after {\n      content: '';\n      position: relative;\n      width: 7px;\n      height: 7px;\n      border-radius: 50%;\n    }\n\n    &.success {\n      &::before, &::after { background: ${(props) => props.theme.colors.text.green}; }\n    }\n    &.warning {\n      &::before, &::after { background: ${(props) => props.theme.colors.text.warning}; }\n    }\n    &.muted {\n      &::before, &::after { background: ${(props) => props.theme.colors.text.muted}; }\n    }\n    &.danger {\n      &::before, &::after { background: ${(props) => props.theme.colors.text.danger}; }\n    }\n    &.info {\n      &::before, &::after { background: ${(props) => props.theme.status.info.text}; }\n    }\n  }\n\n  .status-check-icon {\n    flex-shrink: 0;\n    color: ${(props) => props.theme.colors.text.green};\n  }\n\n  .banner-title {\n    font-size: 12px;\n    font-weight: 500;\n  }\n\n  /* Summary Cards */\n\n  .sync-summary-title-row {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    margin-bottom: 12px;\n\n    .sync-summary-title {\n      margin-bottom: 0;\n    }\n\n    .last-synced-pill {\n      strong {\n        color: ${(props) => props.theme.text};\n        font-weight: 600;\n      }\n    }\n  }\n\n  .sync-summary-title {\n    font-size: 13px;\n    font-weight: 600;\n    color: ${(props) => props.theme.text};\n    margin-bottom: 10px;\n  }\n\n  .sync-summary-subtitle {\n    font-size: 11px;\n    color: ${(props) => props.theme.colors.text.muted};\n    font-weight: 400;\n    margin-top: 2px;\n  }\n\n  .sync-summary-cards {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 10px;\n  }\n\n  .summary-card {\n    width: 180px;\n    flex-shrink: 0;\n    border: 1px solid ${(props) => props.theme.border.border1};\n    border-radius: 8px;\n    padding: 14px 16px;\n    background: ${(props) => props.theme.background.default};\n    position: relative;\n\n    &.clickable {\n      cursor: pointer;\n    }\n  }\n\n  .summary-count-row {\n    display: flex;\n    align-items: baseline;\n    gap: 4px;\n    margin-bottom: 8px;\n  }\n\n  .summary-count {\n    font-size: 28px;\n    font-weight: 700;\n    font-variant-numeric: tabular-nums;\n    line-height: 1;\n\n    &.green  { color: ${(props) => props.theme.colors.text.green}; }\n    &.amber  { color: ${(props) => props.theme.colors.text.warning}; }\n    &.blue   { color: ${(props) => props.theme.status.info.text}; }\n    &.red    { color: ${(props) => props.theme.colors.text.danger || '#c0392b'}; }\n    &.purple { color: #7c3aed; }\n    &.default { color: ${(props) => props.theme.text}; }\n    &.muted  { color: ${(props) => props.theme.colors.text.muted}; }\n  }\n\n  .summary-count-unit {\n    font-size: 10px;\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .summary-label {\n    font-size: 12px;\n    font-weight: 500;\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .conflict-annotation {\n    font-size: 11px;\n    font-weight: 600;\n    color: ${(props) => props.theme.colors.text.danger || '#c0392b'};\n  }\n\n  .card-info-icon {\n    position: absolute;\n    top: 8px;\n    right: 8px;\n\n    svg {\n      margin: 0;\n      width: 12px;\n      height: 12px;\n      opacity: 0.3;\n    }\n\n    &:hover svg {\n      opacity: 0.6;\n    }\n  }\n\n  /* Connection Settings Modal */\n  .settings-modal {\n\n    .settings-field {\n      margin-bottom: 16px;\n\n      &:last-child {\n        margin-bottom: 0;\n      }\n    }\n\n    .settings-label {\n      font-size: 11px;\n      font-weight: 600;\n      color: ${(props) => props.theme.text};\n      display: block;\n      margin-bottom: 5px;\n    }\n\n    .settings-input {\n      width: 100%;\n      padding: 7px 10px;\n      font-size: 12px;\n      font-family: monospace;\n      color: ${(props) => props.theme.text};\n      border: 1px solid ${(props) => props.theme.border.border1};\n      border-radius: 5px;\n      background: ${(props) => props.theme.input.bg};\n      outline: none;\n      box-sizing: border-box;\n      text-align: left;\n\n      &:focus {\n        border-color: ${(props) => props.theme.input.focusBorder};\n      }\n\n      &.file-pick-btn {\n        cursor: pointer;\n        color: ${(props) => props.theme.colors.text.muted};\n        overflow: hidden;\n        text-overflow: ellipsis;\n        white-space: nowrap;\n      }\n    }\n\n    .settings-toggle-row {\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      gap: 12px;\n    }\n\n    .toggle-info {\n      flex: 1;\n      min-width: 0;\n    }\n\n    .toggle-description {\n      font-size: 11px;\n      color: ${(props) => props.theme.text};\n      margin-top: 2px;\n    }\n\n    .toggle-switch {\n      width: 34px;\n      height: 20px;\n      border-radius: 10px;\n      border: none;\n      cursor: pointer;\n      padding: 0;\n      flex-shrink: 0;\n      position: relative;\n      transition: background 0.2s;\n      background: ${(props) => props.theme.colors.text.muted};\n\n      &.active {\n        background: ${(props) => props.theme.colors.text.green};\n      }\n\n      .toggle-knob {\n        width: 14px;\n        height: 14px;\n        border-radius: 50%;\n        background: #fff;\n        position: absolute;\n        top: 3px;\n        left: 3px;\n        transition: left 0.2s;\n        box-shadow: 0 1px 2px rgba(0,0,0,0.2);\n      }\n\n      &.active .toggle-knob {\n        left: 17px;\n      }\n    }\n\n    .interval-buttons {\n      display: flex;\n      gap: 6px;\n      margin-top: 8px;\n\n      button {\n        padding: 5px 12px;\n        font-size: 12px;\n        border-radius: 5px;\n        cursor: pointer;\n        font-weight: 500;\n        border: 1px solid ${(props) => props.theme.border.border1};\n        background: ${(props) => props.theme.background.default};\n        color: ${(props) => props.theme.colors.text.subtext0};\n        transition: all 0.15s;\n\n        &.active {\n          border-color: ${(props) => props.theme.button2.color.primary.border};\n          background: ${(props) => props.theme.button2.color.primary.bg};\n          color: ${(props) => props.theme.button2.color.primary.text};\n        }\n      }\n    }\n\n    .settings-footer {\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      padding-top: 14px;\n    }\n\n    .disconnect-link {\n      font-size: 12px;\n      color: ${(props) => props.theme.colors.text.danger};\n      background: none;\n      border: none;\n      cursor: pointer;\n      padding: 0;\n\n      &:hover {\n        text-decoration: underline;\n      }\n    }\n\n    .settings-actions {\n      display: flex;\n      gap: 8px;\n    }\n  }\n\n  /* State Messages */\n  .state-message {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    padding: 1.5rem;\n    gap: 0.5rem;\n    color: ${(props) => props.theme.colors.text.muted};\n\n    &.success {\n      color: ${(props) => props.theme.colors.text.green};\n    }\n\n    .spinning {\n      animation: spin 1s linear infinite;\n    }\n  }\n\n  @keyframes spin {\n    from { transform: rotate(0deg); }\n    to { transform: rotate(360deg); }\n  }\n\n  .spec-status-section {\n    margin-top: 20px;\n\n    .spec-update-banner {\n      margin-top: 0;\n    }\n  }\n\n  .sync-info-notice {\n    display: flex;\n    align-items: flex-start;\n    gap: 0.5rem;\n    padding: 8px 12px;\n    border-radius: 8px;\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.subtext0};\n    background: ${(props) => props.theme.background.mantle};\n\n    .sync-info-icon {\n      flex-shrink: 0;\n      margin-top: 1px;\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n\n    .whats-updated-title {\n      color: ${(props) => props.theme.text};\n      font-weight: 500;\n    }\n  }\n\n  .sync-review-empty-state {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    padding: 4rem 2rem;\n    text-align: center;\n\n    .empty-state-icon {\n      color: var(--color-text-muted, #9ca3af);\n      margin-bottom: 1rem;\n    }\n\n    h4 {\n      font-size: ${(props) => props.theme.font.size.base};\n      font-weight: 500;\n      color: ${(props) => props.theme.text};\n      margin: 0 0 0.375rem 0;\n    }\n\n    p {\n      font-size: ${(props) => props.theme.font.size.xs};\n      line-height: 1.5;\n      max-width: 400px;\n      margin: 0;\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n  }\n\n  .collection-status-section {\n    margin-top: 20px;\n\n    .change-section {\n      margin-top: 0.75rem;\n\n      .section-body.expandable-mode {\n        border-radius: 0 0 8px 8px;\n        max-height: none; /* Override default max-height so all items remain visible */\n      }\n    }\n\n  }\n\n  /* Expandable endpoint rows — shared base styles */\n  .endpoint-review-row {\n    border-bottom: 1px solid ${(props) => props.theme.border.border1};\n\n    &:last-child {\n      border-bottom: none;\n    }\n\n    .review-row-header {\n      display: flex;\n      align-items: center;\n      gap: 0.5rem;\n      padding: 0.5rem 0.75rem;\n      cursor: pointer;\n\n      &:hover {\n        background: ${(props) => props.theme.background.mantle};\n      }\n\n      .expand-toggle {\n        color: ${(props) => props.theme.colors.text.muted};\n      }\n\n      .endpoint-path {\n        font-family: monospace;\n        font-size: ${(props) => props.theme.font.size.sm};\n        color: ${(props) => props.theme.text};\n      }\n\n      .endpoint-name {\n        color: ${(props) => props.theme.colors.text.muted};\n        font-size: ${(props) => props.theme.font.size.xs};\n        overflow: hidden;\n        text-overflow: ellipsis;\n        white-space: nowrap;\n      }\n\n      .changes-tag {\n        font-size: ${(props) => props.theme.font.size.xs};\n        padding: 0.125rem 0.375rem;\n        background: ${(props) => props.theme.background.surface1};\n        color: ${(props) => props.theme.colors.text.muted};\n        border-radius: ${(props) => props.theme.border.radius.sm};\n      }\n\n      .endpoint-actions {\n        display: flex;\n        gap: 0.25rem;\n        margin-left: auto;\n        opacity: 0;\n        transition: opacity 0.15s;\n      }\n\n      &:hover .endpoint-actions {\n        opacity: 1;\n      }\n    }\n\n    .review-row-diff {\n      border-top: 1px solid ${(props) => props.theme.border.border1};\n      background: ${(props) => props.theme.background.mantle};\n    }\n\n    .diff-loading {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      gap: 0.5rem;\n      padding: 2rem;\n      color: ${(props) => props.theme.colors.text.muted};\n      font-size: ${(props) => props.theme.font.size.sm};\n\n      .spinning {\n        animation: spin 1s linear infinite;\n      }\n    }\n\n    .diff-error {\n      padding: 1rem;\n      color: ${(props) => props.theme.colors.text.danger};\n      font-size: ${(props) => props.theme.font.size.sm};\n    }\n  }\n\n  .change-section {\n    border: 1px solid ${(props) => props.theme.border.border1};\n    border-radius: 8px;\n\n    .section-header {\n      display: flex;\n      align-items: center;\n      gap: 0.5rem;\n      padding: 0.5rem 0.75rem;\n      background: transparent;\n      border-radius: 8px;\n      cursor: pointer;\n      user-select: none;\n\n      &:hover {\n        background: ${(props) => props.theme.background.mantle};\n      }\n\n      .section-dot {\n        width: 7px;\n        height: 7px;\n        border-radius: 50%;\n        flex-shrink: 0;\n        background: ${(props) => props.theme.colors.text.muted};\n\n        &.type-added { background: ${(props) => props.theme.colors.text.green}; }\n        &.type-modified { background: ${(props) => props.theme.colors.text.warning}; }\n        &.type-removed { background: ${(props) => props.theme.colors.text.danger}; }\n        &.type-missing { background: ${(props) => props.theme.colors.text.danger}; }\n        &.type-local-only { background: ${(props) => props.theme.colors.text.muted}; }\n        &.type-in-sync { background: ${(props) => props.theme.colors.text.green}; }\n        &.type-conflict { background: ${(props) => props.theme.colors.text.danger}; }\n        &.type-spec-modified { background: ${(props) => props.theme.colors.text.warning}; }\n        &.type-collection-drift { background: ${(props) => props.theme.colors.text.warning}; }\n      }\n\n      .section-title {\n        font-size: ${(props) => props.theme.font.size.xs};\n        font-weight: 600;\n        color: ${(props) => props.theme.text};\n      }\n\n      .section-count {\n        display: inline-flex;\n        align-items: center;\n        justify-content: center;\n        min-width: 1.25rem;\n        height: 1.25rem;\n        padding: 0 0.3rem;\n        font-size: ${(props) => props.theme.font.size.xs};\n        color: ${(props) => props.theme.colors.text.subtext1};\n        background: ${(props) => props.theme.background.surface1};\n        border-radius: 999px;\n      }\n\n      .section-subtitle {\n        font-size: 10px;\n        color: ${(props) => props.theme.colors.text.muted};\n      }\n\n      .section-actions {\n        margin-left: auto;\n      }\n    }\n\n    /* When section body is visible, show background and flatten header's bottom radius */\n    &.expanded .section-header {\n      background: ${(props) => props.theme.background.mantle};\n      border-bottom-left-radius: 0;\n      border-bottom-right-radius: 0;\n    }\n\n    .section-body {\n      border-top: 1px solid ${(props) => props.theme.border.border1};\n      border-bottom-left-radius: 8px;\n      border-bottom-right-radius: 8px;\n      max-height: 300px;\n      overflow-y: auto;\n\n      &.expandable-mode {\n        max-height: none;\n        overflow-y: visible;\n      }\n    }\n  }\n\n  /* Chevron */\n  .chevron {\n    color: ${(props) => props.theme.colors.text.muted};\n    transition: transform 0.15s ease;\n    flex-shrink: 0;\n\n    &.expanded {\n      transform: rotate(90deg);\n    }\n  }\n\n  /* Endpoint Items */\n  .endpoint-item {\n    border-bottom: 1px solid ${(props) => props.theme.border.border1};\n\n    &:last-child {\n      border-bottom: none;\n    }\n\n    .endpoint-row {\n      display: flex;\n      align-items: center;\n      gap: 0.5rem;\n      padding: 0.5rem 0.75rem;\n      font-size: ${(props) => props.theme.font.size.sm};\n\n      &.clickable {\n        cursor: pointer;\n\n        &:hover {\n          background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n        }\n      }\n\n      .endpoint-path {\n        font-family: monospace;\n        color: ${(props) => props.theme.text};\n      }\n\n      .endpoint-summary {\n        flex: 1;\n        color: ${(props) => props.theme.colors.text.muted};\n        white-space: nowrap;\n        overflow: hidden;\n        text-overflow: ellipsis;\n      }\n\n      .deprecated-tag {\n        font-size: ${(props) => props.theme.font.size.xs};\n        padding: 0.125rem 0.375rem;\n        background: ${(props) => props.theme.status.warning.background};\n        color: ${(props) => props.theme.status.warning.text};\n        border-radius: ${(props) => props.theme.border.radius.sm};\n      }\n\n      .changes-tag {\n        font-size: ${(props) => props.theme.font.size.xs};\n        padding: 0.125rem 0.5rem;\n        background: ${(props) => props.theme.status.warning.background};\n        color: ${(props) => props.theme.status.warning.text};\n        border-radius: ${(props) => props.theme.border.radius.sm};\n        font-weight: 500;\n      }\n\n      .endpoint-actions {\n        display: flex;\n        gap: 0.25rem;\n        margin-left: auto;\n        opacity: 0;\n        transition: opacity 0.15s;\n      }\n\n      &:hover .endpoint-actions {\n        opacity: 1;\n      }\n    }\n  }\n\n  /* Endpoint Details */\n  .endpoint-details {\n    padding: 0.75rem;\n    background: ${(props) => props.theme.background.surface0};\n    border-top: 1px solid ${(props) => props.theme.border.border1};\n\n    .detail-group {\n      margin-bottom: 0.75rem;\n\n      &:last-child {\n        margin-bottom: 0;\n      }\n\n      .detail-title {\n        font-size: ${(props) => props.theme.font.size.xs};\n        font-weight: 600;\n        text-transform: uppercase;\n        color: ${(props) => props.theme.colors.text.muted};\n        margin-bottom: 0.375rem;\n        display: flex;\n        align-items: center;\n        gap: 0.5rem;\n      }\n\n      .description-text {\n        font-size: ${(props) => props.theme.font.size.sm};\n        color: ${(props) => props.theme.text};\n        line-height: 1.5;\n        margin: 0;\n      }\n    }\n\n    .params-table {\n      width: 100%;\n      font-size: ${(props) => props.theme.font.size.xs};\n      border-collapse: collapse;\n\n      td {\n        padding: 0.25rem 0.5rem;\n        border-bottom: 1px solid ${(props) => props.theme.border.border1};\n        vertical-align: top;\n\n        &:first-child {\n          padding-left: 0;\n        }\n\n        &:last-child {\n          padding-right: 0;\n        }\n      }\n\n      tr:last-child td {\n        border-bottom: none;\n      }\n\n      .param-name {\n        font-family: monospace;\n        font-weight: 500;\n        color: ${(props) => props.theme.text};\n        white-space: nowrap;\n      }\n\n      .param-type {\n        color: ${(props) => props.theme.colors.text.subtext0};\n        white-space: nowrap;\n      }\n\n      .param-desc {\n        color: ${(props) => props.theme.colors.text.muted};\n      }\n    }\n\n    .required-badge {\n      font-size: 10px;\n      padding: 0.125rem 0.25rem;\n      background: ${(props) => props.theme.status.danger.background};\n      color: ${(props) => props.theme.status.danger.text};\n      border-radius: ${(props) => props.theme.border.radius.sm};\n    }\n\n    .content-type-badge {\n      display: inline-block;\n      font-size: ${(props) => props.theme.font.size.xs};\n      padding: 0.125rem 0.375rem;\n      background: ${(props) => props.theme.background.surface1};\n      color: ${(props) => props.theme.colors.text.muted};\n      border-radius: ${(props) => props.theme.border.radius.sm};\n      margin-bottom: 0.5rem;\n    }\n\n    .schema-block {\n      font-family: monospace;\n      font-size: 11px;\n      background: ${(props) => props.theme.background.surface1};\n      border: 1px solid ${(props) => props.theme.border.border1};\n      border-radius: ${(props) => props.theme.border.radius.sm};\n      padding: 0.5rem;\n      margin: 0;\n      overflow-x: auto;\n      max-height: 120px;\n      color: ${(props) => props.theme.text};\n    }\n\n    .responses-list {\n      display: flex;\n      flex-direction: column;\n      gap: 0.25rem;\n    }\n\n    .response-row {\n      display: flex;\n      align-items: center;\n      gap: 0.5rem;\n      font-size: ${(props) => props.theme.font.size.xs};\n\n      .status-code {\n        font-family: monospace;\n        font-weight: 500;\n        padding: 0.125rem 0.375rem;\n        border-radius: ${(props) => props.theme.border.radius.sm};\n\n        &.status-2xx {\n          background: ${(props) => rgba(props.theme.colors.text.green, 0.07)};\n          color: ${(props) => props.theme.status.success.text};\n        }\n        &.status-3xx {\n          background: ${(props) => props.theme.status.info.background};\n          color: ${(props) => props.theme.status.info.text};\n        }\n        &.status-4xx {\n          background: ${(props) => props.theme.status.warning.background};\n          color: ${(props) => props.theme.status.warning.text};\n        }\n        &.status-5xx {\n          background: ${(props) => props.theme.status.danger.background};\n          color: ${(props) => props.theme.status.danger.text};\n        }\n      }\n\n      .response-desc {\n        color: ${(props) => props.theme.colors.text.muted};\n      }\n    }\n  }\n\n  /* Disconnect Modal */\n  .disconnect-modal {\n    .disconnect-message {\n      font-size: ${(props) => props.theme.font.size.sm};\n      line-height: 1.5;\n      margin-bottom: 1.5rem;\n    }\n\n    .disconnect-checkbox {\n      display: flex;\n      align-items: center;\n      gap: 0.5rem;\n      font-size: ${(props) => props.theme.font.size.sm};\n      color: ${(props) => props.theme.colors.text.muted};\n      cursor: pointer;\n      margin-bottom: 1.5rem;\n\n      input[type=\"checkbox\"] {\n        cursor: pointer;\n      }\n    }\n\n    .disconnect-actions {\n      display: flex;\n      justify-content: flex-end;\n      gap: 0.5rem;\n    }\n  }\n\n  /* Action Confirm Modal */\n  .action-confirm-modal {\n    .confirm-message {\n      font-size: ${(props) => props.theme.font.size.sm};\n      color: ${(props) => props.theme.text};\n      line-height: 1.5;\n      margin-bottom: 1.5rem;\n    }\n\n    .confirm-actions {\n      display: flex;\n      justify-content: flex-end;\n      gap: 0.5rem;\n    }\n  }\n\n  /* Endpoints Modal List */\n  .endpoints-list {\n    max-height: 12rem;\n    overflow-y: auto;\n    border: 1px solid ${(props) => props.theme.border.border1};\n    border-radius: ${(props) => props.theme.border.radius.base};\n    background: ${(props) => props.theme.background.default};\n\n    .endpoint-row {\n      display: flex;\n      align-items: center;\n      gap: 0.5rem;\n      padding: 0.5rem 0.75rem;\n      border-bottom: 1px solid ${(props) => props.theme.border.border1};\n\n      &:last-child {\n        border-bottom: none;\n      }\n\n      &.selectable {\n        cursor: pointer;\n\n        &:hover {\n          background: ${(props) => props.theme.background.surface1};\n        }\n      }\n    }\n\n    .endpoint-path {\n      font-family: monospace;\n      flex: 1;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      white-space: nowrap;\n    }\n\n    .endpoint-summary {\n      color: ${(props) => props.theme.colors.text.muted};\n      font-size: ${(props) => props.theme.font.size.xs};\n      overflow: hidden;\n      text-overflow: ellipsis;\n      white-space: nowrap;\n      max-width: 40%;\n    }\n\n    input[type=\"checkbox\"] {\n      accent-color: ${(props) => props.theme.colors.primary};\n      cursor: pointer;\n    }\n  }\n\n  .removal-controls {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    margin-bottom: 0.25rem;\n    padding: 0 0.25rem;\n  }\n\n  .removal-count {\n    font-size: ${(props) => props.theme.font.size.xs};\n    color: ${(props) => props.theme.colors.text.muted};\n    margin-left: 1.25rem;\n  }\n\n  .removal-actions {\n    display: flex;\n    gap: 0.5rem;\n    align-items: center;\n  }\n\n  .removal-separator {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .text-link {\n    color: ${(props) => props.theme.colors.primary};\n    background: none;\n    border: none;\n    padding: 0;\n    cursor: pointer;\n    font-size: ${(props) => props.theme.font.size.xs};\n\n    &:hover {\n      text-decoration: underline;\n    }\n  }\n\n  /* Sync Review Modal */\n  .sync-review-page {\n    position: relative;\n    display: flex;\n    flex-direction: column;\n    height: 100%;\n\n    .sync-review-header {\n      flex-shrink: 0;\n      padding-bottom: 0.75rem;\n\n      .back-link-row {\n        display: flex;\n        align-items: center;\n        justify-content: space-between;\n        margin-bottom: 0.75rem;\n      }\n\n      .back-link {\n        display: inline-flex;\n        align-items: center;\n        gap: 0.25rem;\n        font-size: ${(props) => props.theme.font.size.sm};\n        color: ${(props) => props.theme.colors.text.muted};\n        cursor: pointer;\n        background: none;\n        border: none;\n        padding: 0;\n        font-family: inherit;\n\n        &:hover {\n          color: ${(props) => props.theme.text};\n        }\n      }\n\n      .title-row {\n        display: flex;\n        align-items: flex-start;\n        justify-content: space-between;\n        gap: 1rem;\n        flex-wrap: wrap;\n        margin-bottom: 0.25rem;\n      }\n\n      .title-left {\n        display: flex;\n        flex-direction: column;\n        gap: 0.25rem;\n      }\n\n      .description-row {\n        display: flex;\n        align-items: center;\n        justify-content: space-between;\n        gap: 1rem;\n        flex-wrap: wrap;\n        margin-bottom: 0.25rem;\n      }\n\n      .review-title {\n        font-size: ${(props) => props.theme.font.size.base};\n        font-weight: 600;\n        color: ${(props) => props.theme.text};\n        margin: 0;\n      }\n\n      .review-badges {\n        display: flex;\n        flex-direction: column;\n        align-items: flex-end;\n        gap: 0.375rem;\n\n        .badge-row {\n          display: flex;\n          flex-wrap: wrap;\n          gap: 0.5rem;\n        }\n      }\n\n      .review-subtitle {\n        font-size: ${(props) => props.theme.font.size.sm};\n        color: ${(props) => props.theme.colors.text.muted};\n      }\n    }\n\n    .context-pill {\n      font-size: ${(props) => props.theme.font.size.xs};\n      padding: 0.125rem 0.5rem;\n      border-radius: ${(props) => props.theme.border.radius.sm};\n      background: ${(props) => props.theme.background.surface1};\n      color: ${(props) => props.theme.colors.text.muted};\n      white-space: nowrap;\n      font-weight: 500;\n\n      &.spec {\n        background: ${(props) => props.theme.status.info.background};\n        color: ${(props) => props.theme.status.info.text};\n      }\n\n      &.drift {\n        background: ${(props) => props.theme.status.warning.background};\n        color: ${(props) => props.theme.status.warning.text};\n      }\n\n      &.conflict {\n        background: ${(props) => props.theme.status.danger.background};\n        color: ${(props) => props.theme.status.danger.text};\n      }\n\n      &.added {\n        background: ${(props) => rgba(props.theme.colors.text.green, 0.07)};\n        color: ${(props) => props.theme.colors.text.green};\n      }\n\n      &.removed {\n        background: ${(props) => props.theme.status.danger.background};\n        color: ${(props) => props.theme.colors.text.danger};\n      }\n    }\n\n    .text-diff-container {\n      border-radius: ${(props) => props.theme.border.radius.sm};\n      border: 1px solid ${(props) => props.theme.border.border1};\n      overflow: auto;\n\n      .diff-column-headers {\n        display: flex;\n        border-bottom: 1px solid ${(props) => props.theme.border.border1};\n        position: sticky;\n        top: 0;\n        z-index: 2;\n        background: ${(props) => props.theme.bg};\n\n        .diff-column-label {\n          flex: 1;\n          padding: 6px 12px;\n          font-size: 12px;\n          font-weight: 600;\n          color: ${(props) => props.theme.colors.text.muted};\n\n          &:first-child {\n            border-right: 1px solid ${(props) => props.theme.border.border1};\n          }\n        }\n      }\n\n      .d2h-wrapper {\n        background-color: ${(props) => props.theme.bg} !important;\n        font-family: 'Fira Code', monospace;\n        font-size: 12px;\n      }\n\n      .d2h-file-wrapper {\n        border: none;\n        border-radius: 0;\n        margin-bottom: 0;\n      }\n\n      .d2h-file-header {\n        display: none;\n      }\n\n      .d2h-files-diff {\n        width: 100%;\n\n        .d2h-file-side-diff:first-child {\n          border-right: 1px solid ${(props) => props.theme.border.border1};\n        }\n      }\n\n      .d2h-code-side-linenumber {\n        background: transparent !important;\n        position: static !important;\n      }\n\n      .d2h-diff-tbody {\n        tr td { border: none !important; }\n      }\n\n      .d2h-ins {\n        background-color: color-mix(in srgb, ${(props) => props.theme.colors.text.green} 15%, transparent) !important;\n        border-color: color-mix(in srgb, ${(props) => props.theme.colors.text.green} 40%, transparent) !important;\n      }\n\n      .d2h-del {\n        background-color: color-mix(in srgb, ${(props) => props.theme.colors.text.danger} 15%, transparent) !important;\n        border-color: color-mix(in srgb, ${(props) => props.theme.colors.text.danger} 40%, transparent) !important;\n      }\n\n      .d2h-file-diff .d2h-ins.d2h-change {\n        background-color: color-mix(in srgb, ${(props) => props.theme.colors.text.green} 25%, transparent) !important;\n      }\n\n      .d2h-file-diff .d2h-del.d2h-change {\n        background-color: color-mix(in srgb, ${(props) => props.theme.colors.text.warning} 20%, transparent) !important;\n      }\n\n      .d2h-code-line ins,\n      .d2h-code-side-line ins {\n        background-color: color-mix(in srgb, ${(props) => props.theme.colors.text.green} 40%, transparent) !important;\n        text-decoration: none;\n      }\n\n      .d2h-code-line del,\n      .d2h-code-side-line del {\n        background-color: color-mix(in srgb, ${(props) => props.theme.colors.text.danger} 40%, transparent) !important;\n        text-decoration: none;\n      }\n\n      .d2h-code-line,\n      .d2h-code-side-line {\n        color: ${(props) => props.theme.text} !important;\n        word-break: break-all;\n      }\n\n      .d2h-code-line-ctn {\n        word-break: break-all;\n      }\n\n      .d2h-tag {\n        font-size: 9px;\n        font-weight: 500;\n        padding: 1px 5px;\n        border-radius: ${(props) => props.theme.border.radius.sm};\n        text-transform: uppercase;\n        letter-spacing: 0.02em;\n        border: none;\n      }\n\n      .d2h-changed-tag {\n        background-color: color-mix(in srgb, ${(props) => props.theme.colors.text.warning} 15%, transparent);\n        color: ${(props) => props.theme.colors.text.warning};\n      }\n\n      .d2h-added-tag {\n        background-color: color-mix(in srgb, ${(props) => props.theme.colors.text.green} 13%, transparent);\n        color: ${(props) => props.theme.colors.text.green};\n      }\n\n      .d2h-deleted-tag {\n        background-color: color-mix(in srgb, ${(props) => props.theme.colors.text.danger} 13%, transparent);\n        color: ${(props) => props.theme.colors.text.danger};\n      }\n\n      .d2h-renamed-tag,\n      .d2h-moved-tag {\n        display: none;\n      }\n\n      .d2h-file-wrapper,\n      .d2h-file-diff,\n      .d2h-code-wrapper,\n      .d2h-diff-table,\n      .d2h-code-line,\n      .d2h-code-side-line,\n      .d2h-code-line-ctn,\n      .d2h-code-linenumber,\n      .d2h-code-side-linenumber {\n        font-family: 'Fira Code', monospace !important;\n        font-size: 12px !important;\n      }\n    }\n\n    .text-diff-loading {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      gap: 0.5rem;\n      padding: 2rem;\n      color: ${(props) => props.theme.colors.text.muted};\n      font-size: ${(props) => props.theme.font.size.sm};\n    }\n\n    .text-diff-empty {\n      padding: 2rem;\n      text-align: center;\n      color: ${(props) => props.theme.colors.text.muted};\n      font-size: ${(props) => props.theme.font.size.sm};\n    }\n\n    .spec-diff-modal {\n      .spec-diff-badges {\n        display: flex;\n        gap: 0.5rem;\n        flex-wrap: wrap;\n        margin-bottom: 0.5rem;\n      }\n\n      .spec-diff-subtitle {\n        font-size: ${(props) => props.theme.font.size.sm};\n        color: ${(props) => props.theme.colors.text.muted};\n        margin: 0 0 0.75rem 0;\n      }\n\n      .spec-diff-body {\n        .text-diff-container {\n          max-height: calc(80vh - 140px);\n        }\n      }\n    }\n\n    .review-actions-bar {\n      display: flex;\n      justify-content: space-between;\n      align-items: center;\n      padding: 0.75rem;\n      background: ${(props) => props.theme.background.surface0};\n      border: 1px solid ${(props) => props.theme.border.border1};\n      border-radius: ${(props) => props.theme.border.radius.md};\n      margin-bottom: 0.75rem;\n    }\n\n    .review-stats {\n      display: flex;\n      gap: 1rem;\n      font-size: ${(props) => props.theme.font.size.sm};\n\n      .stat {\n        display: inline-flex;\n        align-items: center;\n        gap: 0.25rem;\n\n        &.add { color: ${(props) => props.theme.colors.text.green}; }\n        &.update { color: ${(props) => props.theme.status.info.text}; }\n        &.remove { color: ${(props) => props.theme.colors.text.danger}; }\n        &.keep { color: ${(props) => props.theme.colors.text.muted}; }\n      }\n    }\n\n    .bulk-actions {\n      display: flex;\n      gap: 0.5rem;\n    }\n\n    .bulk-btn {\n      display: inline-flex;\n      align-items: center;\n      gap: 0.25rem;\n      padding: 0.25rem 0.5rem;\n      font-size: ${(props) => props.theme.font.size.xs};\n      background: none;\n      border: 1px solid ${(props) => props.theme.border.border1};\n      border-radius: ${(props) => props.theme.border.radius.sm};\n      color: ${(props) => props.theme.text};\n      cursor: pointer;\n\n      &:hover {\n        background: ${(props) => props.theme.background.surface1};\n      }\n\n      &.active {\n        border-color: ${(props) => props.theme.status.info.text};\n        color: ${(props) => props.theme.status.info.text};\n        background: ${(props) => props.theme.status.info.background};\n      }\n\n      &:disabled {\n        opacity: 0.7;\n        cursor: not-allowed;\n      }\n\n      .spinner-icon {\n        animation: spin 1s linear infinite;\n      }\n    }\n\n    .sync-review-body {\n      flex: 1;\n      overflow-y: auto;\n    }\n\n    &.sync-mode .sync-review-body {\n      margin-top: 0;\n    }\n\n    .endpoints-review-sections {\n      display: flex;\n      flex-direction: column;\n      gap: 1.25rem;\n\n      .review-group {\n        display: flex;\n        flex-direction: column;\n        gap: 0.75rem;\n      }\n\n      .review-group-header {\n        display: flex;\n        align-items: center;\n        justify-content: flex-end;\n      }\n\n      .review-group-title {\n        font-size: ${(props) => props.theme.font.size.sm};\n        font-weight: 600;\n        color: ${(props) => props.theme.colors.text.muted};\n        text-transform: uppercase;\n        letter-spacing: 0.05em;\n        margin: 0;\n      }\n\n      .change-section {\n        .section-subtitle {\n          font-size: ${(props) => props.theme.font.size.xs};\n          color: ${(props) => props.theme.colors.text.muted};\n          margin-left: 0.25rem;\n        }\n\n        .section-body {\n          max-height: none;\n\n          &.expandable-mode {\n            border-top: none;\n            border-radius: 0 0 ${(props) => props.theme.border.radius.sm} ${(props) => props.theme.border.radius.sm};\n          }\n        }\n      }\n    }\n\n    .endpoint-review-row {\n      .review-row-header {\n        .source-tag {\n          font-size: 10px;\n          padding: 0.125rem 0.375rem;\n          border-radius: ${(props) => props.theme.border.radius.sm};\n          font-weight: 500;\n\n          &.spec {\n            background: ${(props) => props.theme.status.info.background};\n            color: ${(props) => props.theme.status.info.text};\n          }\n\n          &.drift {\n            background: ${(props) => props.theme.status.warning.background};\n            color: ${(props) => props.theme.status.warning.text};\n          }\n\n          &.conflict {\n            background: ${(props) => props.theme.status.danger.background};\n            color: ${(props) => props.theme.status.danger.text};\n          }\n\n          &.local-modified, &.local-deleted, &.local-added {\n            background: ${(props) => props.theme.status.warning.background};\n            color: ${(props) => props.theme.status.warning.text};\n          }\n\n          &.spec-modified, &.spec-added {\n            background: ${(props) => props.theme.status.info.background};\n            color: ${(props) => props.theme.status.info.text};\n          }\n\n          &.spec-removed {\n            background: ${(props) => props.theme.status.danger.background};\n            color: ${(props) => props.theme.status.danger.text};\n          }\n        }\n\n      }\n\n      .decision-buttons {\n        display: flex;\n        gap: 0.25rem;\n        margin-left: auto;\n      }\n\n      .decision-btn {\n        display: inline-flex;\n        align-items: center;\n        gap: 0.25rem;\n        padding: 0.25rem 0.5rem;\n        font-size: ${(props) => props.theme.font.size.xs};\n        background: none;\n        border: 1px solid ${(props) => props.theme.border.border1};\n        border-radius: ${(props) => props.theme.border.radius.sm};\n        color: ${(props) => props.theme.colors.text.muted};\n        cursor: pointer;\n\n        &:hover {\n          background: ${(props) => props.theme.background.surface1};\n        }\n\n        &.keep.selected {\n          background: ${(props) => props.theme.background.surface1};\n          border-color: ${(props) => props.theme.colors.text.muted};\n          color: ${(props) => props.theme.text};\n        }\n\n        &.accept.selected {\n          background: ${(props) => rgba(props.theme.colors.text.green, 0.07)};\n          border-color: ${(props) => props.theme.colors.text.green};\n          color: ${(props) => props.theme.colors.text.green};\n        }\n      }\n\n      .endpoint-diff-view {\n        .diff-section {\n          margin-bottom: 0.5rem;\n\n          &:last-child {\n            margin-bottom: 0;\n          }\n        }\n\n        .url-bar {\n          display: flex;\n          align-items: center;\n          gap: 0.5rem;\n          padding: 0.375rem;\n          background: ${(props) => props.theme.background.surface0};\n          border-radius: ${(props) => props.theme.border.radius.sm};\n\n          .url {\n            font-family: monospace;\n            font-size: ${(props) => props.theme.font.size.xs};\n            color: ${(props) => props.theme.text};\n            word-break: break-all;\n          }\n        }\n\n        .diff-section-title {\n          font-size: ${(props) => props.theme.font.size.xs};\n          font-weight: 600;\n          color: ${(props) => props.theme.colors.text.muted};\n          margin-bottom: 0.25rem;\n        }\n\n        .diff-table {\n          width: 100%;\n          font-size: ${(props) => props.theme.font.size.xs};\n          border-collapse: collapse;\n          border: 1px solid ${(props) => props.theme.border.border1};\n          border-radius: ${(props) => props.theme.border.radius.sm};\n\n          th, td {\n            padding: 0.25rem 0.5rem;\n            text-align: left;\n            border-bottom: 1px solid ${(props) => props.theme.border.border1};\n          }\n\n          th {\n            background: ${(props) => props.theme.background.surface1};\n            color: ${(props) => props.theme.colors.text.muted};\n            font-weight: 500;\n          }\n\n          tr:last-child td {\n            border-bottom: none;\n          }\n\n          .row-added {\n            background: ${(props) => rgba(props.theme.colors.text.green, 0.07)};\n          }\n\n          .row-deleted {\n            background: ${(props) => props.theme.status.danger.background};\n          }\n\n          .row-modified {\n            background: ${(props) => props.theme.status.warning.background};\n          }\n\n          .status-badge {\n            display: inline-block;\n            width: 14px;\n            height: 14px;\n            text-align: center;\n            line-height: 14px;\n            font-size: 10px;\n            font-weight: 700;\n            border-radius: 2px;\n\n            &.added {\n              background: ${(props) => rgba(props.theme.colors.text.green, 0.07)};\n              color: ${(props) => props.theme.colors.text.green};\n            }\n\n            &.deleted {\n              background: ${(props) => props.theme.status.danger.background};\n              color: ${(props) => props.theme.colors.text.danger};\n            }\n\n            &.modified {\n              background: ${(props) => props.theme.status.warning.background};\n              color: ${(props) => props.theme.colors.text.warning};\n            }\n          }\n\n          .key-cell {\n            font-family: monospace;\n            font-weight: 500;\n          }\n\n          .value-cell {\n            font-family: monospace;\n            color: ${(props) => props.theme.colors.text.muted};\n          }\n        }\n\n        .body-mode-badge {\n          display: inline-block;\n          padding: 0.25rem 0.5rem;\n          font-size: ${(props) => props.theme.font.size.xs};\n          background: ${(props) => props.theme.background.surface1};\n          color: ${(props) => props.theme.colors.text.muted};\n          border-radius: ${(props) => props.theme.border.radius.sm};\n          font-family: monospace;\n        }\n\n        .empty-diff {\n          font-size: ${(props) => props.theme.font.size.sm};\n          color: ${(props) => props.theme.colors.text.muted};\n          font-style: italic;\n        }\n      }\n    }\n\n    .sync-review-bottom-bar {\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      position: sticky;\n      bottom: 0rem;\n      background: ${(props) => props.theme.background.base};\n      margin-top: 1rem;\n      z-index: 10;\n      padding-top: 0.75rem;\n      padding-bottom: 0.75rem;\n\n      .bar-stats {\n        display: flex;\n        align-items: center;\n        gap: 0.75rem;\n        font-size: ${(props) => props.theme.font.size.sm};\n\n        .stats-prefix {\n          color: ${(props) => props.theme.colors.text.muted};\n        }\n\n        .stat {\n          display: inline-flex;\n          align-items: center;\n          gap: 0.25rem;\n          position: relative;\n          cursor: default;\n\n          &.add { color: ${(props) => props.theme.colors.text.green}; }\n          &.update { color: ${(props) => props.theme.status.info.text}; }\n          &.remove { color: ${(props) => props.theme.colors.text.danger}; }\n          &.keep { color: ${(props) => props.theme.colors.text.muted}; }\n\n          .stat-hover-card {\n            transform: translateX(-50%);\n            background: ${(props) => props.theme.background.base};\n            border: 1px solid ${(props) => props.theme.border.border1};\n            border-radius: ${(props) => props.theme.border.radius.md};\n            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n            padding: 0.5rem;\n            min-width: 200px;\n            max-width: 320px;\n            max-height: 200px;\n            overflow-y: auto;\n            z-index: 100;\n          }\n\n          .stat-hover-list {\n            display: flex;\n            flex-direction: column;\n            gap: 0.25rem;\n          }\n\n          .stat-hover-item {\n            display: flex;\n            align-items: center;\n            gap: 0.375rem;\n            padding: 0.2rem 0.25rem;\n            border-radius: ${(props) => props.theme.border.radius.sm};\n\n            &:hover {\n              background: ${(props) => props.theme.background.hover};\n            }\n          }\n\n          .stat-hover-path {\n            font-size: ${(props) => props.theme.font.size.xs};\n            font-family: monospace;\n            color: ${(props) => props.theme.text};\n            white-space: nowrap;\n            overflow: hidden;\n            text-overflow: ellipsis;\n          }\n        }\n      }\n\n      .bar-actions {\n        display: flex;\n        gap: 0.5rem;\n      }\n    }\n\n  }\n\n  .sync-confirm-modal {\n    display: flex;\n    flex-direction: column;\n    max-height: 60vh;\n\n    .sync-confirm-description {\n      font-size: ${(props) => props.theme.font.size.sm};\n      color: ${(props) => props.theme.colors.text.muted};\n      margin: 0 0 0.75rem 0;\n      flex-shrink: 0;\n    }\n\n    .sync-confirm-groups {\n      display: flex;\n      flex-direction: column;\n      gap: 0.5rem;\n      margin-bottom: 1rem;\n      flex: 1;\n      min-height: 0;\n      overflow-y: auto;\n    }\n\n    .confirm-group {\n      .confirm-group-header {\n        display: flex;\n        align-items: center;\n        gap: 0.375rem;\n        padding: 0.25rem 0;\n        cursor: pointer;\n        user-select: none;\n\n      }\n\n      .confirm-group-label {\n        font-size: ${(props) => props.theme.font.size.sm};\n        font-weight: 500;\n      }\n\n      .confirm-group-count {\n        font-size: ${(props) => props.theme.font.size.xs};\n        color: ${(props) => props.theme.colors.text.muted};\n      }\n\n      .confirm-group-subtitle {\n        font-size: ${(props) => props.theme.font.size.xs};\n        color: ${(props) => props.theme.colors.text.muted};\n        font-weight: 400;\n      }\n\n      &.type-add .confirm-group-label { color: ${(props) => props.theme.colors.text.green}; }\n      &.type-update .confirm-group-label { color: ${(props) => props.theme.status.info.text}; }\n      &.type-remove .confirm-group-label { color: ${(props) => props.theme.colors.text.danger}; }\n      &.type-keep .confirm-group-label { color: ${(props) => props.theme.colors.text.muted}; }\n\n      .endpoints-list {\n        margin-top: 0.5rem;\n        margin-left: 1.25rem;\n      }\n\n      .confirm-group-body {\n        margin-top: 0.5rem;\n      }\n\n      .confirm-group-endpoints {\n        display: flex;\n        flex-direction: column;\n        gap: 0.125rem;\n        padding-left: 1.25rem;\n      }\n\n      .confirm-endpoint {\n        display: flex;\n        align-items: center;\n        gap: 0.375rem;\n        padding: 0.125rem 0.25rem;\n      }\n\n      .confirm-endpoint-path {\n        font-family: monospace;\n        font-size: ${(props) => props.theme.font.size.xs};\n        color: ${(props) => props.theme.text};\n      }\n    }\n\n    .sync-confirm-actions {\n      display: flex;\n      justify-content: flex-end;\n      gap: 0.5rem;\n      flex-shrink: 0;\n    }\n  }\n\n  /* Visual Diff Content Overrides */\n  .visual-diff-content {\n    margin: 0.75rem;\n    border: 1px solid ${(props) => props.theme.border.border1};\n    border-radius: ${(props) => props.theme.border.radius.sm};\n\n    .diff-header-row {\n      border-top: none;\n      border-left: none;\n      border-right: none;\n      border-radius: 0;\n      margin-bottom: 0;\n      background: ${(props) => props.theme.background.surface0};\n    }\n\n    .diff-sections {\n      gap: 0;\n    }\n\n    .diff-row {\n      border-top: none;\n      border-left: none;\n      border-right: none;\n      border-radius: 0;\n      margin-bottom: 0;\n      &:last-child {\n        border-bottom: none;\n      }\n    }\n  }\n\n  /* URL/File mode toggle in setup form and settings modal */\n  .setup-mode-toggle {\n    display: inline-flex;\n    flex-shrink: 0;\n    align-items: stretch;\n    align-self: stretch;\n    gap: 2px;\n    padding: 2px;\n    background: ${(props) => props.theme.background.surface1};\n    border-radius: ${(props) => props.theme.border.radius.md};\n  }\n\n  .setup-mode-btn {\n    padding: 0 0.65rem;\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-weight: 500;\n    color: ${(props) => props.theme.text};\n    background: transparent;\n    border: none;\n    border-radius: calc(${(props) => props.theme.border.radius.md} - 3px);\n    cursor: pointer;\n    transition: background 0.12s, color 0.12s;\n    white-space: nowrap;\n\n    &.active {\n      background: ${(props) => darken(0.03, props.theme.background.base)};\n      color: ${(props) => props.theme.button2.color.secondary.text};\n      box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);\n    }\n\n    &:hover:not(.active) {\n      color: ${(props) => props.theme.text};\n    }\n  }\n\n  .file-pick-btn {\n    text-align: left;\n    cursor: pointer;\n    font-family: monospace;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  /* File not found banner */\n  .file-not-found-banner {\n    display: flex;\n    align-items: flex-start;\n    justify-content: space-between;\n    gap: 1rem;\n    padding: 0.75rem 1rem;\n    margin-bottom: 0.75rem;\n    background: ${(props) => rgba(props.theme.colors.text.yellow || '#f59e0b', 0.08)};\n    border: 1px solid ${(props) => rgba(props.theme.colors.text.yellow || '#f59e0b', 0.3)};\n    border-radius: ${(props) => props.theme.border.radius.md};\n  }\n\n  .file-not-found-content {\n    display: flex;\n    align-items: flex-start;\n    gap: 0.625rem;\n    flex: 1;\n    min-width: 0;\n  }\n\n  .file-not-found-icon {\n    flex-shrink: 0;\n    margin-top: 1px;\n    color: ${(props) => props.theme.colors.text.yellow || '#f59e0b'};\n  }\n\n  .file-not-found-title {\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-weight: 600;\n    color: ${(props) => props.theme.text};\n    margin-bottom: 0.125rem;\n  }\n\n  .file-not-found-desc {\n    font-size: ${(props) => props.theme.font.size.xs};\n    color: ${(props) => props.theme.colors.text.muted};\n    word-break: break-all;\n\n    code {\n      font-family: monospace;\n      font-size: ${(props) => props.theme.font.size.xs};\n    }\n  }\n\n  .file-not-found-actions {\n    display: flex;\n    align-items: center;\n    gap: 0.5rem;\n    flex-shrink: 0;\n  }\n\n  .beta-feedback-inline {\n    margin-top: 2rem;\n    font-size: ${(props) => props.theme.font.size.xs};\n    color: ${(props) => props.theme.colors.text.muted};\n\n    .beta-feedback-link {\n      background: none;\n      border: none;\n      padding: 0;\n      color: ${(props) => props.theme.status.info.text};\n      cursor: pointer;\n      font-size: inherit;\n      text-decoration: underline;\n\n      &:hover {\n        opacity: 0.8;\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/OpenAPISyncTab/SyncReviewPage/index.js",
    "content": "import React, { useState, useMemo, useEffect } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport {\n  IconCheck,\n  IconX,\n  IconArrowRight,\n  IconArrowsDiff,\n  IconInfoCircle,\n  IconLoader2\n} from '@tabler/icons';\nimport Button from 'ui/Button';\nimport StatusBadge from 'ui/StatusBadge';\nimport EndpointChangeSection from '../EndpointChangeSection';\nimport ExpandableEndpointRow from '../EndpointChangeSection/ExpandableEndpointRow';\nimport ConfirmSyncModal from '../ConfirmSyncModal';\nimport SpecDiffModal from '../SpecDiffModal';\nimport Help from 'components/Help';\nimport { setReviewDecision, setReviewDecisions, selectTabUiState } from 'providers/ReduxStore/slices/openapi-sync';\n\n/**\n * Categorize remoteDrift endpoints using three-way merge.\n * Uses specDrift and collectionDrift to determine who changed each modified endpoint.\n *\n * Returns:\n *  - specAddedEndpoints: new in spec, not yet in collection\n *  - specUpdatedEndpoints: modified in spec (includes conflicts where both sides changed)\n *  - localUpdatedEndpoints: modified only in the collection (spec didn't change)\n *  - specRemovedEndpoints: removed from spec, still in collection\n */\nconst categorizeEndpoints = (remoteDrift, specDrift, collectionDrift) => {\n  // Only show endpoints as \"New in Spec\" if they were actually added to the spec\n  // (i.e., they appear in specDrift.added). Endpoints the user deleted locally that\n  // still exist in both stored and remote spec should not appear here — they belong\n  // in \"Collection Changes\" only.\n  const specAddedIds = new Set((specDrift?.added || []).map((ep) => ep.id));\n  const specAddedEndpoints = (remoteDrift.missing || []).filter((ep) => specAddedIds.has(ep.id));\n\n  // Only show endpoints as \"Removed from Spec\" if they were actually in the stored spec\n  // (i.e., they appear in specDrift.removed). Locally-added endpoints that were never in\n  // the spec should not appear here — they belong in \"Collection Changes\" only.\n  const specRemovedIds = new Set((specDrift?.removed || []).map((ep) => ep.id));\n  const specRemovedEndpoints = (remoteDrift.localOnly || []).filter((ep) => specRemovedIds.has(ep.id));\n\n  // Build lookup sets to determine who changed each modified endpoint\n  const specModifiedIds = new Set((specDrift?.modified || []).map((ep) => ep.id));\n  const localModifiedIds = new Set((collectionDrift?.modified || []).map((ep) => ep.id));\n  const noMergeBase = collectionDrift?.noStoredSpec;\n\n  const specUpdatedEndpoints = [];\n  const localUpdatedEndpoints = [];\n\n  (remoteDrift.modified || []).forEach((ep) => {\n    // When there's no merge base (noStoredSpec), we can't tell who changed what — treat as spec update\n    const specChanged = !noMergeBase && specModifiedIds.has(ep.id);\n    const localChanged = !noMergeBase && localModifiedIds.has(ep.id);\n\n    if (!specChanged && localChanged) {\n      // Only local changed — user modification, spec didn't change\n      localUpdatedEndpoints.push({\n        ...ep,\n        source: 'collection-drift',\n        localAction: 'modified'\n      });\n    } else {\n      // Spec changed, both changed (conflict), no merge base, or sensitivity mismatch\n      specUpdatedEndpoints.push({\n        ...ep,\n        source: 'spec-modified',\n        specAction: 'modified',\n        ...(specChanged && localChanged && { conflict: true, localAction: 'modified' })\n      });\n    }\n  });\n\n  return { specAddedEndpoints, specUpdatedEndpoints, localUpdatedEndpoints, specRemovedEndpoints };\n};\n\nconst SyncReviewPage = ({\n  specDrift,\n  remoteDrift,\n  collectionDrift,\n  collectionPath,\n  collectionUid,\n  newSpec,\n  isSyncing,\n  isLoading,\n  onApplySync\n}) => {\n  const dispatch = useDispatch();\n  const tabUiState = useSelector(selectTabUiState(collectionUid));\n  const [showConfirmation, setShowConfirmation] = useState(false);\n  const [showSpecDiffModal, setShowSpecDiffModal] = useState(false);\n\n  const { specAddedEndpoints, specUpdatedEndpoints, localUpdatedEndpoints, specRemovedEndpoints } = useMemo(() => {\n    if (!remoteDrift) {\n      return { specAddedEndpoints: [], specUpdatedEndpoints: [], localUpdatedEndpoints: [], specRemovedEndpoints: [] };\n    }\n    return categorizeEndpoints(remoteDrift, specDrift, collectionDrift);\n  }, [specDrift, remoteDrift, collectionDrift]);\n\n  const conflictCount = specUpdatedEndpoints.filter((ep) => ep.conflict).length;\n  const hasConflicts = conflictCount > 0;\n\n  // Track decisions in Redux (persisted across navigations)\n  const savedDecisions = tabUiState.reviewDecisions || {};\n\n  // Compute defaults for any endpoints not yet in Redux\n  const decisions = useMemo(() => {\n    const merged = { ...savedDecisions };\n    // Spec changes: accept-incoming by default, null for conflicts (must resolve manually)\n    specUpdatedEndpoints.forEach((ep) => {\n      if (!(ep.id in merged)) merged[ep.id] = ep.conflict ? null : 'accept-incoming';\n    });\n    // Local changes: keep-mine (preserved silently, not shown in review)\n    localUpdatedEndpoints.forEach((ep) => {\n      if (!(ep.id in merged)) merged[ep.id] = 'keep-mine';\n    });\n    // Added + removed endpoints: accept-incoming\n    [...specAddedEndpoints, ...specRemovedEndpoints].forEach((ep) => {\n      if (!(ep.id in merged)) merged[ep.id] = 'accept-incoming';\n    });\n    return merged;\n  }, [savedDecisions, specUpdatedEndpoints, localUpdatedEndpoints, specRemovedEndpoints, specAddedEndpoints]);\n\n  // Sync computed defaults back to Redux when they differ from saved state\n  useEffect(() => {\n    const hasNewDefaults = Object.keys(decisions).some((id) => !(id in savedDecisions));\n    if (hasNewDefaults) {\n      dispatch(setReviewDecisions({ collectionUid, decisions }));\n    }\n  }, [decisions, savedDecisions, collectionUid, dispatch]);\n\n  const handleDecisionChange = (endpointId, decision) => {\n    dispatch(setReviewDecision({ collectionUid, endpointId, decision }));\n  };\n\n  // Bulk actions — all spec-driven sections\n  const decidableEndpoints = useMemo(() => {\n    return [...specUpdatedEndpoints, ...specAddedEndpoints, ...specRemovedEndpoints];\n  }, [specUpdatedEndpoints, specAddedEndpoints, specRemovedEndpoints]);\n\n  const setBulkDecision = (decision) => {\n    const newDecisions = {};\n    decidableEndpoints.forEach((ep) => { newDecisions[ep.id] = decision; });\n    dispatch(setReviewDecisions({ collectionUid, decisions: newDecisions }));\n  };\n\n  const allAccepted = decidableEndpoints.length > 0\n    && decidableEndpoints.every((ep) => decisions[ep.id] === 'accept-incoming');\n  const allSkipped = decidableEndpoints.length > 0\n    && decidableEndpoints.every((ep) => decisions[ep.id] === 'keep-mine');\n\n  const unresolvedConflicts = specUpdatedEndpoints.filter((ep) => ep.conflict && !decisions[ep.id]).length;\n\n  // Confirmation summary — grouped endpoint lists\n  const confirmGroups = useMemo(() => {\n    const groups = [];\n    const addGroup = (label, type, endpoints) => {\n      if (endpoints.length > 0) groups.push({ label, type, endpoints });\n    };\n\n    const isAccepted = (ep) => decisions[ep.id] === 'accept-incoming';\n    const isSkipped = (ep) => decisions[ep.id] === 'keep-mine';\n\n    // Accepted — changes that will be applied\n    addGroup('New endpoints to add', 'add', specAddedEndpoints.filter(isAccepted));\n    addGroup('Endpoints to update', 'update', specUpdatedEndpoints.filter(isAccepted));\n    addGroup('Endpoints to delete', 'remove', specRemovedEndpoints.filter(isAccepted));\n\n    // Skipped — changes that will be preserved as-is\n    addGroup('Keeping local version', 'keep', specUpdatedEndpoints.filter((ep) => ep.conflict && isSkipped(ep)));\n    addGroup('Retaining removed endpoints', 'keep', specRemovedEndpoints.filter(isSkipped));\n    addGroup('Skipped new endpoints', 'keep', specAddedEndpoints.filter(isSkipped));\n    addGroup('Keeping current version (skipped updates)', 'keep', specUpdatedEndpoints.filter((ep) => !ep.conflict && isSkipped(ep)));\n\n    return groups;\n  }, [specAddedEndpoints, specUpdatedEndpoints, specRemovedEndpoints, decisions]);\n\n  const handleConfirmApply = () => {\n    setShowConfirmation(false);\n\n    // Filter based on decisions\n    const filteredAddedEndpoints = specAddedEndpoints.filter(\n      (ep) => decisions[ep.id] === 'accept-incoming'\n    );\n    const filteredSpecChanges = specUpdatedEndpoints.filter(\n      (ep) => !ep.conflict && decisions[ep.id] === 'accept-incoming'\n    );\n\n    // Collect \"Not in Spec\" endpoints where user chose to remove\n    const localOnlyIds = specRemovedEndpoints\n      .filter((ep) => decisions[ep.id] === 'accept-incoming')\n      .map((ep) => ep.id);\n\n    onApplySync({\n      endpointDecisions: decisions,\n      localOnlyIds,\n      // Pass filtered categorized endpoints for performSync to construct the right backend diff\n      newToCollection: filteredAddedEndpoints,\n      specUpdates: filteredSpecChanges,\n      resolvedConflicts: specUpdatedEndpoints.filter((ep) => ep.conflict && decisions[ep.id] === 'accept-incoming'),\n      localChangesToReset: localUpdatedEndpoints.filter((ep) => decisions[ep.id] === 'accept-incoming')\n    });\n  };\n\n  const totalChanges = specAddedEndpoints.length + specUpdatedEndpoints.length + localUpdatedEndpoints.length + specRemovedEndpoints.length;\n  const hasRemoteUpdates = specAddedEndpoints.length + specUpdatedEndpoints.length + specRemovedEndpoints.length > 0;\n\n  const buttonLabel = unresolvedConflicts > 0\n    ? `Resolve ${unresolvedConflicts} conflict${unresolvedConflicts !== 1 ? 's and sync' : ' and sync'}`\n    : !hasRemoteUpdates && specDrift?.storedSpecMissing\n        ? 'Restore Spec File'\n        : 'Sync Collection';\n\n  return (\n    <div className=\"sync-review-page sync-mode\">\n      {hasRemoteUpdates && (\n        <div className=\"sync-review-header\">\n          <div className=\"title-row\">\n            <div className=\"title-left\">\n              <h3 className=\"review-title\">Review Changes</h3>\n              {totalChanges > 0 && (\n                <p className=\"review-subtitle\">\n                  Choose to keep the current version or accept the updated one.\n                </p>\n              )}\n            </div>\n            {(specDrift?.unifiedDiff || decidableEndpoints.length > 0) && (\n              <div className=\"bulk-actions\">\n                {specDrift?.unifiedDiff && (\n                  <button className=\"bulk-btn\" onClick={() => setShowSpecDiffModal(true)}>\n                    <IconArrowsDiff size={12} /> View Spec Diff\n                  </button>\n                )}\n                {decidableEndpoints.length > 0 && (\n                  <>\n                    <button\n                      className={`bulk-btn ${allSkipped ? 'active' : ''}`}\n                      onClick={() => setBulkDecision('keep-mine')}\n                    >\n                      <IconX size={12} /> Skip All\n                    </button>\n                    <button\n                      className={`bulk-btn ${allAccepted ? 'active' : ''}`}\n                      onClick={() => setBulkDecision('accept-incoming')}\n                    >\n                      <IconCheck size={12} /> Accept All\n                    </button>\n                  </>\n                )}\n              </div>\n            )}\n          </div>\n        </div>\n      )}\n\n      <div className=\"sync-review-body\">\n        {!hasRemoteUpdates ? (\n          <div className=\"sync-review-empty-state\">\n            {isLoading ? (\n              <>\n                <IconLoader2 size={40} className=\"empty-state-icon animate-spin\" />\n                <h4>Checking for updates</h4>\n                <p>Comparing your last synced spec with the latest spec...</p>\n              </>\n            ) : (\n              <>\n                <IconCheck size={40} className=\"empty-state-icon\" />\n                <h4>No updates from the spec</h4>\n                <p>The spec endpoints have not been updated since the last sync.</p>\n              </>\n            )}\n          </div>\n        ) : (\n          <div className=\"endpoints-review-sections\">\n            {/* === Updates from Spec === */}\n            {decidableEndpoints.length > 0 && (\n              <div className=\"review-group\">\n\n                <EndpointChangeSection\n                  title=\"Updated in Spec\"\n                  type=\"spec-modified\"\n                  endpoints={specUpdatedEndpoints}\n                  defaultExpanded={true}\n                  expandableLayout\n                  subtitle=\"The spec has updates for these endpoints\"\n                  headerExtra={conflictCount > 0 ? (\n                    <StatusBadge\n                      status=\"danger\"\n                      rightSection={(\n                        <Help icon=\"info\" size={11} placement=\"top\" width={250}>\n                          {`This section has ${conflictCount} endpoint${conflictCount === 1 ? '' : 's'} modified in both the spec and your collection. Expand to review and resolve.`}\n                        </Help>\n                      )}\n                    >\n                      {conflictCount} {conflictCount === 1 ? 'Conflict' : 'Conflicts'}\n                    </StatusBadge>\n                  ) : null}\n                  collectionUid={collectionUid}\n                  sectionKey=\"review-spec-modified\"\n                  renderItem={(endpoint, idx) => (\n                    <ExpandableEndpointRow\n                      key={endpoint.id || idx}\n                      endpoint={endpoint}\n                      decision={decisions?.[endpoint.id]}\n                      onDecisionChange={(decision) => handleDecisionChange(endpoint.id, decision)}\n                      collectionPath={collectionPath}\n                      newSpec={newSpec}\n                      showDecisions={true}\n                      decisionLabels={{ keep: 'Keep Current', accept: 'Update' }}\n                      collectionUid={collectionUid}\n                    />\n                  )}\n                />\n\n                <EndpointChangeSection\n                  title=\"New in Spec\"\n                  type=\"added\"\n                  endpoints={specAddedEndpoints}\n                  defaultExpanded={true}\n                  expandableLayout\n                  subtitle=\"New endpoints from the spec\"\n                  collectionUid={collectionUid}\n                  sectionKey=\"review-added\"\n                  renderItem={(endpoint, idx) => (\n                    <ExpandableEndpointRow\n                      key={endpoint.id || idx}\n                      endpoint={endpoint}\n                      decision={decisions?.[endpoint.id]}\n                      onDecisionChange={(decision) => handleDecisionChange(endpoint.id, decision)}\n                      collectionPath={collectionPath}\n                      newSpec={newSpec}\n                      showDecisions={true}\n                      decisionLabels={{ keep: 'Skip', accept: 'Add' }}\n                      collectionUid={collectionUid}\n                    />\n                  )}\n                />\n\n                <EndpointChangeSection\n                  title=\"Removed from Spec\"\n                  type=\"removed\"\n                  endpoints={specRemovedEndpoints}\n                  defaultExpanded={true}\n                  expandableLayout\n                  subtitle=\"These endpoints are in your collection but not in the spec\"\n                  collectionUid={collectionUid}\n                  sectionKey=\"review-removed\"\n                  renderItem={(endpoint, idx) => (\n                    <ExpandableEndpointRow\n                      key={endpoint.id || idx}\n                      endpoint={endpoint}\n                      decision={decisions?.[endpoint.id]}\n                      onDecisionChange={(decision) => handleDecisionChange(endpoint.id, decision)}\n                      collectionPath={collectionPath}\n                      newSpec={newSpec}\n                      showDecisions={true}\n                      decisionLabels={{ keep: 'Keep', accept: 'Delete' }}\n                      collectionUid={collectionUid}\n                    />\n                  )}\n                />\n              </div>\n            )}\n\n          </div>\n        )}\n      </div>\n\n      {hasRemoteUpdates && (\n        <div className=\"sync-info-notice mt-4\">\n          <IconInfoCircle size={14} className=\"sync-info-icon\" />\n          <span><span className=\"whats-updated-title\">What gets updated:</span> Parameters, headers, body and auth will be updated. Tests, scripts, and assertions are always preserved.</span>\n        </div>\n      )}\n\n      {hasRemoteUpdates && (\n        <div className=\"sync-review-bottom-bar mt-4\">\n          <div className=\"bar-stats\">\n            {totalChanges === 0 && (\n              <span className=\"stats-prefix\">\n                {specDrift?.storedSpecMissing ? 'Sync will update the spec file' : 'No endpoint changes to apply'}\n              </span>\n            )}\n          </div>\n          <div className=\"bar-actions\">\n            <Button\n              onClick={totalChanges === 0 ? handleConfirmApply : () => setShowConfirmation(true)}\n              disabled={unresolvedConflicts > 0 || isSyncing}\n              loading={isSyncing}\n            >\n              {buttonLabel}\n              {unresolvedConflicts === 0 && <IconArrowRight size={14} style={{ marginLeft: 4 }} />}\n            </Button>\n          </div>\n        </div>\n      )}\n\n      {showConfirmation && (\n        <ConfirmSyncModal\n          groups={confirmGroups}\n          onCancel={() => setShowConfirmation(false)}\n          onSync={handleConfirmApply}\n          isSyncing={isSyncing}\n        />\n      )}\n\n      {showSpecDiffModal && (\n        <SpecDiffModal\n          specDrift={specDrift}\n          onClose={() => setShowSpecDiffModal(false)}\n        />\n      )}\n    </div>\n  );\n};\n\nexport default SyncReviewPage;\n"
  },
  {
    "path": "packages/bruno-app/src/components/OpenAPISyncTab/hooks/useEndpointActions.js",
    "content": "import { useState } from 'react';\nimport toast from 'react-hot-toast';\n\nconst useEndpointActions = (collection, collectionDrift, reloadDrift) => {\n  const [pendingAction, setPendingAction] = useState(null);\n\n  // Action execution helper — runs IPC call(s), shows toast, reloads drift\n  const executeEndpointAction = async (ipcCalls, successMsg, errorMsg) => {\n    try {\n      const { ipcRenderer } = window;\n      if (Array.isArray(ipcCalls[0])) {\n        await Promise.all(ipcCalls.map(([channel, params]) => ipcRenderer.invoke(channel, params)));\n      } else {\n        const [channel, params] = ipcCalls;\n        await ipcRenderer.invoke(channel, params);\n      }\n      toast.success(successMsg);\n      await reloadDrift();\n    } catch (err) {\n      console.error(`Error: ${errorMsg}`, err);\n      toast.error(errorMsg);\n    }\n  };\n\n  // Confirmation handlers — show modal before executing\n  const handleResetEndpoint = (endpoint) => {\n    setPendingAction({\n      type: 'reset-endpoint',\n      title: 'Reset Endpoint',\n      message: `Are you sure you want to reset \"${endpoint.method} ${endpoint.path}\" to match the spec? Your local changes will be lost.`,\n      endpoint\n    });\n  };\n\n  const handleResetAllModified = () => {\n    if (!collectionDrift?.modified?.length) return;\n    setPendingAction({\n      type: 'reset-all-modified',\n      title: 'Reset All Modified',\n      message: `Are you sure you want to reset ${collectionDrift.modified.length} modified endpoint(s) to match the spec? Your local changes will be lost.`\n    });\n  };\n\n  const handleDeleteEndpoint = (endpoint) => {\n    setPendingAction({\n      type: 'delete-endpoint',\n      title: 'Delete Endpoint',\n      message: `Are you sure you want to delete \"${endpoint.method} ${endpoint.path}\"? This action cannot be undone.`,\n      endpoint\n    });\n  };\n\n  const handleDeleteAllLocalOnly = () => {\n    if (!collectionDrift?.localOnly?.length) return;\n    setPendingAction({\n      type: 'delete-all-local',\n      title: 'Delete All Local Endpoints',\n      message: `Are you sure you want to delete ${collectionDrift.localOnly.length} local-only endpoint(s)? This action cannot be undone.`\n    });\n  };\n\n  const handleRevertAllChanges = () => {\n    const modifiedCount = collectionDrift?.modified?.length || 0;\n    const missingCount = collectionDrift?.missing?.length || 0;\n    const localOnlyCount = collectionDrift?.localOnly?.length || 0;\n\n    setPendingAction({\n      type: 'revert-all',\n      title: 'Revert All Changes',\n      message: `Are you sure you want to revert all changes? This will reset ${modifiedCount} modified, restore ${missingCount} missing, and delete ${localOnlyCount} local-only endpoint(s).`\n    });\n  };\n\n  const handleAddMissingEndpoint = (endpoint) => {\n    setPendingAction({\n      type: 'restore-endpoint',\n      title: 'Restore Endpoint',\n      message: `Are you sure you want to restore \"${endpoint.method} ${endpoint.path}\" to your collection?`,\n      endpoint\n    });\n  };\n\n  const handleAddAllMissing = () => {\n    if (!collectionDrift?.missing?.length) return;\n    setPendingAction({\n      type: 'restore-all-missing',\n      title: 'Restore All Missing',\n      message: `Are you sure you want to restore ${collectionDrift.missing.length} missing endpoint(s) to your collection?`\n    });\n  };\n\n  // Execute confirmed action\n  const confirmPendingAction = async () => {\n    if (!pendingAction) return;\n\n    const { type, endpoint } = pendingAction;\n    setPendingAction(null);\n\n    switch (type) {\n      case 'reset-endpoint':\n        return executeEndpointAction(\n          ['renderer:reset-endpoints-to-spec', { collectionPath: collection.pathname, endpoints: [endpoint] }],\n          `Reset ${endpoint.method} ${endpoint.path} to spec`,\n          'Failed to reset endpoint'\n        );\n      case 'reset-all-modified':\n        return executeEndpointAction(\n          ['renderer:reset-endpoints-to-spec', { collectionPath: collection.pathname, endpoints: collectionDrift.modified }],\n          `Reset ${collectionDrift.modified.length} endpoints to spec`,\n          'Failed to reset endpoints'\n        );\n      case 'delete-endpoint':\n        return executeEndpointAction(\n          ['renderer:delete-endpoints', { collectionPath: collection.pathname, collectionUid: collection.uid, endpoints: [endpoint] }],\n          `Deleted ${endpoint.method} ${endpoint.path}`,\n          'Failed to delete endpoint'\n        );\n      case 'delete-all-local':\n        return executeEndpointAction(\n          ['renderer:delete-endpoints', { collectionPath: collection.pathname, collectionUid: collection.uid, endpoints: collectionDrift.localOnly }],\n          `Deleted ${collectionDrift.localOnly.length} local-only endpoints`,\n          'Failed to delete endpoints'\n        );\n      case 'revert-all': {\n        const calls = [];\n        if (collectionDrift?.modified?.length > 0) {\n          calls.push(['renderer:reset-endpoints-to-spec', { collectionPath: collection.pathname, endpoints: collectionDrift.modified }]);\n        }\n        if (collectionDrift?.missing?.length > 0) {\n          calls.push(['renderer:add-missing-endpoints', { collectionPath: collection.pathname, endpoints: collectionDrift.missing }]);\n        }\n        if (collectionDrift?.localOnly?.length > 0) {\n          calls.push(['renderer:delete-endpoints', { collectionPath: collection.pathname, collectionUid: collection.uid, endpoints: collectionDrift.localOnly }]);\n        }\n        return executeEndpointAction(calls, 'All changes discarded successfully', 'Failed to discard changes');\n      }\n      case 'restore-endpoint':\n        return executeEndpointAction(\n          ['renderer:add-missing-endpoints', { collectionPath: collection.pathname, endpoints: [endpoint] }],\n          `Added ${endpoint.method} ${endpoint.path} to collection`,\n          'Failed to add endpoint'\n        );\n      case 'restore-all-missing':\n        return executeEndpointAction(\n          ['renderer:add-missing-endpoints', { collectionPath: collection.pathname, endpoints: collectionDrift.missing }],\n          `Added ${collectionDrift.missing.length} endpoints to collection`,\n          'Failed to add endpoints'\n        );\n    }\n  };\n\n  return {\n    pendingAction, setPendingAction,\n    confirmPendingAction,\n    handleResetEndpoint,\n    handleResetAllModified,\n    handleDeleteEndpoint,\n    handleDeleteAllLocalOnly,\n    handleRevertAllChanges,\n    handleAddMissingEndpoint,\n    handleAddAllMissing\n  };\n};\n\nexport default useEndpointActions;\n"
  },
  {
    "path": "packages/bruno-app/src/components/OpenAPISyncTab/hooks/useOpenAPISync.js",
    "content": "import { useState, useEffect, useMemo, useRef } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport toast from 'react-hot-toast';\nimport { addTab, focusTab, closeTabs } from 'providers/ReduxStore/slices/tabs';\nimport { getDefaultRequestPaneTab } from 'utils/collections';\nimport { clearCollectionState, setCollectionUpdate, setStoredSpecMeta } from 'providers/ReduxStore/slices/openapi-sync';\nimport { fetchAndValidateApiSpecFromUrl } from 'utils/importers/common';\nimport { isHttpUrl } from 'utils/url/index';\nimport { flattenItems } from 'utils/collections/index';\nimport { formatIpcError } from 'utils/common/error';\nimport { countEndpoints } from '../utils';\n\nconst useOpenAPISync = (collection) => {\n  const dispatch = useDispatch();\n  const openApiSyncConfig = collection?.brunoConfig?.openapi?.[0];\n\n  // Core state\n  const [sourceUrl, setSourceUrl] = useState(openApiSyncConfig?.sourceUrl || '');\n  const [isLoading, setIsLoading] = useState(false);\n  const [error, setError] = useState(null);\n  const [fileNotFound, setFileNotFound] = useState(false);\n  const [specDrift, setSpecDrift] = useState(null);\n  // Collection drift state\n  const [collectionDrift, setCollectionDrift] = useState(null);\n  const [remoteDrift, setRemoteDrift] = useState(null);\n  const [isDriftLoading, setIsDriftLoading] = useState(false);\n  const [storedSpec, setStoredSpec] = useState(null);\n\n  const tabs = useSelector((state) => state.tabs.tabs);\n\n  const isConfigured = !!openApiSyncConfig?.sourceUrl;\n\n  const updateStoredSpec = (spec) => {\n    setStoredSpec(spec);\n    dispatch(setStoredSpecMeta({\n      collectionUid: collection.uid,\n      title: spec?.info?.title || null,\n      version: spec?.info?.version || null,\n      endpointCount: spec ? countEndpoints(spec) : null\n    }));\n  };\n\n  // Flatten collection items including nested items in folders\n  const allHttpItems = useMemo(() => {\n    return flattenItems(collection?.items || []).filter((item) => item.type === 'http-request');\n  }, [collection?.items]);\n\n  const httpItemCount = useMemo(() => {\n    return String(allHttpItems.filter((item) => !item.partial && !item.loading).length);\n  }, [allHttpItems]);\n\n  // Map endpoint drift id (METHOD:path) → collection item uid\n  const endpointUidMap = useMemo(() => {\n    const normalize = (url) => (url || '')\n      .replace(/\\{\\{[^}]+\\}\\}/g, '')\n      .replace(/^https?:\\/\\/[^/]+/, '')\n      .replace(/\\?.*$/, '')\n      .replace(/{([^}]+)}/g, ':$1')\n      .replace(/\\/+/g, '/')\n      .replace(/\\/$/, '');\n    const map = {};\n    allHttpItems.forEach((item) => {\n      if (item.request?.method && item.request?.url) {\n        const key = `${item.request.method.toUpperCase()}:${normalize(item.request.url)}`;\n        map[key] = item.uid;\n      }\n    });\n    return map;\n  }, [allHttpItems]);\n\n  // Open an endpoint in a tab (focus existing or add new), same as sidebar click\n  const openEndpointInTab = (endpointId) => {\n    const itemUid = endpointUidMap[endpointId];\n    if (!itemUid) return;\n    const existingTab = tabs.find((t) => t.uid === itemUid);\n    if (existingTab) {\n      dispatch(focusTab({ uid: itemUid }));\n    } else {\n      const item = allHttpItems.find((i) => i.uid === itemUid);\n      dispatch(addTab({\n        uid: itemUid,\n        collectionUid: collection.uid,\n        requestPaneTab: item ? getDefaultRequestPaneTab(item) : undefined,\n        type: 'request'\n      }));\n    }\n  };\n\n  const prevItemCountRef = useRef(httpItemCount);\n  const isDriftLoadingRef = useRef(false);\n  const specDriftRef = useRef(specDrift);\n\n  const loadCollectionDrift = async ({ clear = false } = {}) => {\n    if (isDriftLoadingRef.current && !clear) return;\n    isDriftLoadingRef.current = true;\n    if (clear) setCollectionDrift(null);\n    setIsDriftLoading(true);\n    try {\n      const { ipcRenderer } = window;\n      const result = await ipcRenderer.invoke('renderer:get-collection-drift', {\n        collectionPath: collection.pathname\n      });\n\n      if (!result.error) {\n        setCollectionDrift(result);\n      }\n    } catch (err) {\n      console.error('Error loading collection drift:', err);\n    } finally {\n      isDriftLoadingRef.current = false;\n      setIsDriftLoading(false);\n    }\n  };\n\n  const checkForUpdates = async ({ sourceUrlOverride } = {}) => {\n    const effectiveUrl = (sourceUrlOverride ?? sourceUrl).trim();\n    if (!effectiveUrl) {\n      setError('Please enter a URL or select a file');\n      return;\n    }\n\n    setIsLoading(true);\n    setError(null);\n    setFileNotFound(false);\n    setSpecDrift(null);\n    setRemoteDrift(null);\n    setCollectionDrift(null);\n\n    try {\n      const { ipcRenderer } = window;\n      const result = await ipcRenderer.invoke('renderer:compare-openapi-specs', {\n        collectionUid: collection.uid,\n        collectionPath: collection.pathname,\n        sourceUrl: effectiveUrl,\n        environmentContext: {\n          activeEnvironmentUid: collection.activeEnvironmentUid,\n          environments: collection.environments,\n          runtimeVariables: collection.runtimeVariables,\n          globalEnvironmentVariables: collection.globalEnvironmentVariables\n        }\n      });\n\n      if (result.errorCode === 'SOURCE_FILE_NOT_FOUND') {\n        setFileNotFound(true);\n        setError(result.error);\n        return;\n      }\n\n      setSpecDrift(result);\n      updateStoredSpec(result.storedSpec || null);\n\n      // Update Redux store so toolbar status stays in sync\n      dispatch(setCollectionUpdate({\n        collectionUid: collection.uid,\n        hasUpdates: result.isValid !== false && result.hasChanges,\n        diff: result,\n        error: result.isValid === false ? result.error : null\n      }));\n\n      // Fetch remote drift (remote spec vs collection) for collection-centric categorization\n      if (result.newSpec) {\n        const remoteComparison = await ipcRenderer.invoke('renderer:get-collection-drift', {\n          collectionPath: collection.pathname,\n          compareSpec: result.newSpec\n        });\n        if (remoteComparison.error) {\n          console.error('Error computing remote drift:', remoteComparison.error);\n          setError(remoteComparison.error);\n        } else {\n          setRemoteDrift(remoteComparison);\n        }\n      }\n\n      // Refresh collection drift (stored spec vs collection) — skip if no stored spec\n      if (!result.storedSpecMissing) {\n        await loadCollectionDrift({ clear: true });\n      }\n    } catch (err) {\n      console.error('Error checking for updates:', err);\n      setError(formatIpcError(err) || 'Failed to check for updates');\n      dispatch(setCollectionUpdate({\n        collectionUid: collection.uid,\n        hasUpdates: false,\n        diff: null,\n        error: formatIpcError(err) || 'Failed to check for updates'\n      }));\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  useEffect(() => {\n    if (isConfigured) {\n      checkForUpdates();\n    }\n  }, [isConfigured]);\n\n  // Reload drift when collection items change (e.g., endpoint deleted from sidebar)\n  useEffect(() => {\n    if (prevItemCountRef.current !== httpItemCount && isConfigured) {\n      prevItemCountRef.current = httpItemCount;\n      loadCollectionDrift();\n    }\n  }, [httpItemCount, isConfigured]);\n\n  const handleConnect = async () => {\n    const trimmedUrl = sourceUrl.trim();\n    if (!trimmedUrl) {\n      setError('Please enter a URL or select a file');\n      return;\n    }\n\n    setIsLoading(true);\n    setError(null);\n    setFileNotFound(false);\n\n    try {\n      // Validate it's a valid OpenAPI spec before proceeding (URL only; files are validated at picker)\n      if (isHttpUrl(trimmedUrl)) {\n        try {\n          const { specType } = await fetchAndValidateApiSpecFromUrl({ url: trimmedUrl });\n          if (specType !== 'openapi') {\n            setError('The URL does not point to a valid OpenAPI 3.x specification');\n            return;\n          }\n        } catch {\n          setError('The URL does not point to a valid OpenAPI 3.x specification');\n          return;\n        }\n      }\n\n      const { ipcRenderer } = window;\n\n      // Validate the spec first\n      const result = await ipcRenderer.invoke('renderer:compare-openapi-specs', {\n        collectionUid: collection.uid,\n        collectionPath: collection.pathname,\n        sourceUrl: trimmedUrl,\n        environmentContext: {\n          activeEnvironmentUid: collection.activeEnvironmentUid,\n          environments: collection.environments,\n          runtimeVariables: collection.runtimeVariables,\n          globalEnvironmentVariables: collection.globalEnvironmentVariables\n        }\n      });\n\n      if (result.isValid === false) {\n        setSpecDrift(result);\n        setError(result.error);\n        return;\n      }\n\n      // Save sync config (no spec file yet — deferred to first sync unless collection already matches)\n      await ipcRenderer.invoke('renderer:update-openapi-sync-config', {\n        collectionPath: collection.pathname,\n        config: {\n          sourceUrl: trimmedUrl,\n          groupBy: 'tags',\n          autoCheck: true,\n          autoCheckInterval: 5\n        }\n      });\n\n      // Check if collection already matches the spec\n      if (result.newSpec) {\n        const drift = await ipcRenderer.invoke('renderer:get-collection-drift', {\n          collectionPath: collection.pathname,\n          compareSpec: result.newSpec\n        });\n\n        const isInSync = !drift.error\n          && (!drift.missing || drift.missing.length === 0)\n          && (!drift.modified || drift.modified.length === 0)\n          && (!drift.localOnly || drift.localOnly.length === 0);\n\n        if (isInSync) {\n          // Collection matches — save spec file silently to complete setup\n          await ipcRenderer.invoke('renderer:save-openapi-spec', {\n            collectionPath: collection.pathname,\n            specContent: result.newSpecContent || JSON.stringify(result.newSpec, null, 2)\n          });\n        }\n      }\n\n      toast.success('OpenAPI sync connected');\n    } catch (err) {\n      console.error('Error connecting OpenAPI sync:', err);\n      setError(formatIpcError(err) || 'Failed to connect');\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const handleDisconnect = async () => {\n    try {\n      const { ipcRenderer } = window;\n      await ipcRenderer.invoke('renderer:remove-openapi-sync-config', {\n        collectionPath: collection.pathname,\n        deleteSpecFile: true\n      });\n      setSourceUrl('');\n      setSpecDrift(null);\n      setCollectionDrift(null);\n      setRemoteDrift(null);\n      setStoredSpec(null);\n\n      // Clear Redux state for this collection\n      dispatch(clearCollectionState({ collectionUid: collection.uid }));\n\n      // Close the openapi-spec tab if open (spec file no longer exists)\n      const specTab = tabs.find((t) => t.collectionUid === collection.uid && t.type === 'openapi-spec');\n      if (specTab) {\n        dispatch(closeTabs({ tabUids: [specTab.uid] }));\n      }\n\n      toast.success('OpenAPI sync disconnected');\n    } catch (err) {\n      console.error('Error disconnecting sync:', err);\n      toast.error('Failed to disconnect sync');\n    }\n  };\n\n  // Keep ref in sync so reloadDrift always reads the latest specDrift\n  specDriftRef.current = specDrift;\n\n  // Reload both drifts — passed to useEndpointActions so it can refresh after actions.\n  // Uses specDriftRef to avoid stale closure over specDrift state.\n  const reloadDrift = async () => {\n    await loadCollectionDrift({ clear: true });\n    // Refresh remoteDrift if we have a remote spec cached from the last check\n    const currentSpecDrift = specDriftRef.current;\n    if (currentSpecDrift?.newSpec) {\n      try {\n        const { ipcRenderer } = window;\n        const remoteComparison = await ipcRenderer.invoke('renderer:get-collection-drift', {\n          collectionPath: collection.pathname,\n          compareSpec: currentSpecDrift.newSpec\n        });\n        if (!remoteComparison.error) {\n          setRemoteDrift(remoteComparison);\n        }\n      } catch (err) {\n        console.error('Error reloading remote drift:', err);\n      }\n    }\n  };\n\n  // Save connection settings from the modal\n  const handleSaveSettings = async ({ sourceUrl: newUrl, autoCheck, autoCheckInterval }) => {\n    const sourceUrlChanged = newUrl !== openApiSyncConfig?.sourceUrl;\n\n    // Validate the spec before saving if source URL changed (URL only; files are validated at picker)\n    // Kept outside try-catch so validation errors propagate to the caller and the modal stays open\n    if (sourceUrlChanged && isHttpUrl(newUrl)) {\n      let specType;\n      try {\n        ({ specType } = await fetchAndValidateApiSpecFromUrl({ url: newUrl }));\n      } catch {\n        toast.error('The URL does not point to a valid OpenAPI 3.x specification');\n        throw new Error('Invalid OpenAPI specification');\n      }\n      if (specType !== 'openapi') {\n        toast.error('The URL does not point to a valid OpenAPI 3.x specification');\n        throw new Error('Invalid OpenAPI specification');\n      }\n    }\n\n    try {\n      const { ipcRenderer } = window;\n\n      await ipcRenderer.invoke('renderer:update-openapi-sync-config', {\n        collectionPath: collection.pathname,\n        config: {\n          sourceUrl: newUrl,\n          autoCheck,\n          autoCheckInterval\n        }\n      });\n      setSourceUrl(newUrl);\n      setFileNotFound(false);\n      toast.success('Settings saved');\n      // Re-check with new settings — pass newUrl directly to avoid stale closure\n      await checkForUpdates({ sourceUrlOverride: newUrl });\n    } catch (err) {\n      console.error('Error saving settings:', err);\n      toast.error('Failed to save settings');\n    }\n  };\n\n  return {\n    // State\n    sourceUrl, setSourceUrl,\n    isLoading,\n    error, setError,\n    fileNotFound,\n    specDrift,\n    collectionDrift,\n    remoteDrift,\n    isDriftLoading,\n    storedSpec,\n\n    // Handlers\n    checkForUpdates,\n    handleConnect,\n    handleDisconnect,\n    handleSaveSettings,\n    openEndpointInTab,\n    reloadDrift\n  };\n};\n\nexport default useOpenAPISync;\n"
  },
  {
    "path": "packages/bruno-app/src/components/OpenAPISyncTab/hooks/useSyncFlow.js",
    "content": "import { useState, useMemo } from 'react';\nimport { useDispatch } from 'react-redux';\nimport toast from 'react-hot-toast';\nimport { clearCollectionUpdate } from 'providers/ReduxStore/slices/openapi-sync';\nimport { formatIpcError } from 'utils/common/error';\n\nconst useSyncFlow = ({\n  collection, specDrift, remoteDrift, collectionDrift,\n  setError, checkForUpdates\n}) => {\n  const dispatch = useDispatch();\n\n  const [pendingSyncMode, setPendingSyncMode] = useState(null);\n  const [showConfirmModal, setShowConfirmModal] = useState(false);\n  const [isSyncing, setIsSyncing] = useState(false);\n\n  const performSync = async (selections = { localOnlyIds: [], endpointDecisions: {} }, mode = 'sync') => {\n    setShowConfirmModal(false);\n    setIsSyncing(true);\n    setError(null);\n\n    const {\n      localOnlyIds = [], endpointDecisions: decisions = {},\n      newToCollection, specUpdates, resolvedConflicts, localChangesToReset\n    } = selections;\n\n    try {\n      const { ipcRenderer } = window;\n\n      let filteredDiff;\n      let localOnlyToRemove;\n      let driftedToReset;\n\n      if (newToCollection) {\n        // Called from SyncReviewPage with categorized remoteDrift data\n        filteredDiff = {\n          ...specDrift,\n          added: newToCollection,\n          modified: [...(specUpdates || []), ...(resolvedConflicts || [])],\n          removed: [] // Removals handled via localOnlyToRemove\n        };\n\n        localOnlyToRemove = localOnlyIds.length > 0\n          ? (remoteDrift?.localOnly || []).filter((ep) => localOnlyIds.includes(ep.id))\n          : [];\n\n        driftedToReset = localChangesToReset || [];\n      } else {\n        // Called from \"Sync Now\" (skip review) or ConfirmSyncModal — use specDrift directly\n        filteredDiff = {\n          ...specDrift,\n          removed: [] // Removals handled via localOnlyToRemove\n        };\n\n        localOnlyToRemove = localOnlyIds.length > 0\n          ? (remoteDrift?.localOnly || collectionDrift?.localOnly || []).filter((ep) => localOnlyIds.includes(ep.id))\n          : [];\n\n        driftedToReset = collectionDrift?.modified?.filter((ep) => {\n          const decision = decisions[ep.id];\n          return decision === 'accept-incoming';\n        }) || [];\n      }\n\n      await ipcRenderer.invoke('renderer:apply-openapi-sync', {\n        collectionUid: collection.uid,\n        collectionPath: collection.pathname,\n        addNewRequests: mode !== 'spec-only',\n        removeDeletedRequests: localOnlyIds.length > 0,\n        diff: filteredDiff,\n        localOnlyToRemove,\n        driftedToReset,\n        mode,\n        endpointDecisions: decisions\n      });\n\n      setPendingSyncMode(null);\n\n      dispatch(clearCollectionUpdate({ collectionUid: collection.uid }));\n      toast.success(\n        mode === 'spec-only' ? 'Spec updated successfully'\n          : mode === 'reset' ? 'Collection reset to spec successfully'\n            : 'Collection synced successfully'\n      );\n\n      // Re-check to show \"up to date\" state\n      await checkForUpdates();\n    } catch (err) {\n      console.error('Error syncing collection:', err);\n      setError(formatIpcError(err) || 'Failed to sync collection');\n    } finally {\n      setIsSyncing(false);\n    }\n  };\n\n  const handleSyncNow = () => {\n    if (!remoteDrift) return;\n    setPendingSyncMode('sync');\n    setShowConfirmModal(true);\n  };\n\n  const handleApplySync = (selections) => {\n    const mode = pendingSyncMode || 'sync';\n    setPendingSyncMode(null);\n    performSync(selections, mode);\n  };\n\n  const cancelConfirmModal = () => {\n    setShowConfirmModal(false);\n    setPendingSyncMode(null);\n  };\n\n  // Only treat endpoints as spec changes if they actually changed in the spec\n  // (not locally-added/deleted endpoints that were never in or removed from the spec)\n  const specAddedIds = useMemo(() => {\n    return new Set((specDrift?.added || []).map((ep) => ep.id));\n  }, [specDrift]);\n\n  const specRemovedIds = useMemo(() => {\n    return new Set((specDrift?.removed || []).map((ep) => ep.id));\n  }, [specDrift]);\n\n  const handleRestoreSpec = () => {\n    const localOnlyIds = (remoteDrift?.localOnly || [])\n      .filter((ep) => specRemovedIds.has(ep.id))\n      .map((ep) => ep.id);\n    performSync({ localOnlyIds, endpointDecisions: {} }, 'sync');\n  };\n\n  const handleConfirmModalSync = () => {\n    const localOnlyIds = (remoteDrift?.localOnly || [])\n      .filter((ep) => specRemovedIds.has(ep.id))\n      .map((ep) => ep.id);\n    performSync({\n      localOnlyIds,\n      endpointDecisions: {}\n    }, pendingSyncMode || 'sync');\n  };\n\n  const confirmGroups = useMemo(() => {\n    if (!remoteDrift) return [];\n    const groups = [];\n    const actuallyAdded = (remoteDrift.missing || []).filter((ep) => specAddedIds.has(ep.id));\n    if (actuallyAdded.length > 0) {\n      groups.push({ label: 'New endpoints to add', type: 'add', endpoints: actuallyAdded });\n    }\n    if (remoteDrift.modified?.length > 0) {\n      groups.push({ label: 'Endpoints to update', type: 'update', endpoints: remoteDrift.modified });\n    }\n    const actuallyRemoved = (remoteDrift.localOnly || []).filter((ep) => specRemovedIds.has(ep.id));\n    if (actuallyRemoved.length > 0) {\n      groups.push({ label: 'Endpoints to delete', type: 'remove', endpoints: actuallyRemoved });\n    }\n    return groups;\n  }, [remoteDrift, specAddedIds, specRemovedIds]);\n\n  return {\n    isSyncing, showConfirmModal, confirmGroups,\n    handleSyncNow, handleRestoreSpec,\n    handleApplySync, cancelConfirmModal, handleConfirmModalSync\n  };\n};\n\nexport default useSyncFlow;\n"
  },
  {
    "path": "packages/bruno-app/src/components/OpenAPISyncTab/index.js",
    "content": "import { useState, useMemo, useCallback } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { v4 as uuid } from 'uuid';\nimport { addTab } from 'providers/ReduxStore/slices/tabs';\nimport { setTabUiState } from 'providers/ReduxStore/slices/openapi-sync';\nimport ResponsiveTabs from 'ui/ResponsiveTabs';\nimport StyledWrapper from './StyledWrapper';\nimport OpenAPISyncHeader from './OpenAPISyncHeader';\nimport ConnectSpecForm from './ConnectSpecForm';\nimport SpecStatusSection from './SpecStatusSection';\nimport CollectionStatusSection from './CollectionStatusSection';\nimport ConnectionSettingsModal from './ConnectionSettingsModal';\nimport DisconnectSyncModal from './DisconnectSyncModal';\nimport OverviewSection from './OverviewSection';\nimport useOpenAPISync from './hooks/useOpenAPISync';\n\nconst OpenAPISyncTab = ({ collection }) => {\n  const {\n    sourceUrl, setSourceUrl,\n    isLoading,\n    error, setError,\n    fileNotFound,\n    specDrift,\n    collectionDrift,\n    remoteDrift,\n    isDriftLoading,\n    storedSpec,\n    checkForUpdates,\n    handleConnect,\n    handleDisconnect,\n    handleSaveSettings,\n    openEndpointInTab,\n    reloadDrift\n  } = useOpenAPISync(collection);\n\n  const dispatch = useDispatch();\n  const openApiSyncConfig = collection?.brunoConfig?.openapi?.[0];\n  const isConfigured = !!openApiSyncConfig?.sourceUrl;\n\n  const handleViewSpec = () => {\n    dispatch(addTab({\n      uid: uuid(),\n      collectionUid: collection.uid,\n      type: 'openapi-spec'\n    }));\n  };\n\n  const [showSettingsModal, setShowSettingsModal] = useState(false);\n  const [showDisconnectModal, setShowDisconnectModal] = useState(false);\n  const activeTab = useSelector((state) => state.openapiSync?.tabUiState?.[collection.uid]?.activeTab) || 'overview';\n  const setActiveTab = useCallback((tab) => {\n    dispatch(setTabUiState({ collectionUid: collection.uid, activeTab: tab }));\n  }, [dispatch, collection.uid]);\n\n  const hasDriftData = collectionDrift && !collectionDrift.noStoredSpec;\n  const collectionChangesCount = hasDriftData\n    ? (collectionDrift.modified?.length || 0) + (collectionDrift.missing?.length || 0) + (collectionDrift.localOnly?.length || 0)\n    : 0;\n  const specUpdatesCount = hasDriftData\n    ? (specDrift?.added?.length || 0) + (specDrift?.modified?.length || 0) + (specDrift?.removed?.length || 0)\n    : (remoteDrift?.modified?.length || 0) + (remoteDrift?.missing?.length || 0);\n\n  const syncStatus = (() => {\n    if (isLoading) return 'loading';\n    if (error) return 'not-in-sync';\n    if (!hasDriftData) return null;\n    if (collectionChangesCount > 0 || specUpdatesCount > 0) return 'not-in-sync';\n    return 'in-sync';\n  })();\n\n  const syncTabs = useMemo(() => [\n    { key: 'overview', label: 'Overview' },\n    {\n      key: 'collection-changes',\n      label: 'Collection Changes',\n      indicator: collectionChangesCount > 0 ? <span className=\"tab-count\">{collectionChangesCount}</span> : null\n    },\n    {\n      key: 'spec-updates',\n      label: 'Spec Updates',\n      indicator: specUpdatesCount > 0 ? <span className=\"tab-count\">{specUpdatesCount}</span> : null\n    }\n  ], [collectionChangesCount, specUpdatesCount]);\n\n  return (\n    <StyledWrapper className=\"flex flex-col h-full relative px-4 pt-4 overflow-auto\">\n      <div className=\"sync-page w-full\">\n\n        {/* Setup form when not configured */}\n        {!isConfigured && (\n          <ConnectSpecForm\n            sourceUrl={sourceUrl}\n            setSourceUrl={setSourceUrl}\n            isLoading={isLoading}\n            error={error}\n            setError={setError}\n            onConnect={handleConnect}\n          />\n        )}\n\n        {/* Configured: spec header + tabs */}\n        {isConfigured && (\n          <>\n            <OpenAPISyncHeader\n              collection={collection}\n              spec={storedSpec || specDrift?.newSpec}\n              sourceUrl={sourceUrl}\n              syncStatus={syncStatus}\n              onViewSpec={handleViewSpec}\n              onOpenSettings={() => setShowSettingsModal(true)}\n              onOpenDisconnect={() => setShowDisconnectModal(true)}\n              onCheck={checkForUpdates}\n              isLoading={isLoading}\n            />\n\n            <ResponsiveTabs\n              tabs={syncTabs}\n              activeTab={activeTab}\n              onTabSelect={setActiveTab}\n            />\n\n            {activeTab === 'overview' && (\n              <div className=\"sync-tab-content\">\n                <OverviewSection\n                  collection={collection}\n                  storedSpec={storedSpec}\n                  collectionDrift={collectionDrift}\n                  specDrift={specDrift}\n                  remoteDrift={remoteDrift}\n                  onTabSelect={setActiveTab}\n                  error={error}\n                  onOpenSettings={() => setShowSettingsModal(true)}\n                />\n                <p className=\"beta-feedback-inline\">\n                  OpenAPI Sync is in Beta — we'd love to hear your feedback and suggestions.{' '}\n                  <button\n                    type=\"button\"\n                    className=\"beta-feedback-link\"\n                    onClick={() => window?.ipcRenderer?.openExternal('https://github.com/usebruno/bruno/discussions/7401')}\n                  >\n                    Share feedback\n                  </button>\n                </p>\n              </div>\n            )}\n\n            {activeTab === 'collection-changes' && (\n              <div className=\"sync-tab-content\">\n\n                <CollectionStatusSection\n                  collection={collection}\n                  collectionDrift={collectionDrift}\n                  reloadDrift={reloadDrift}\n                  specDrift={specDrift}\n                  storedSpec={storedSpec}\n                  lastSyncDate={openApiSyncConfig?.lastSyncDate}\n                  onOpenEndpoint={openEndpointInTab}\n                  isLoading={isDriftLoading || isLoading}\n                  onTabSelect={setActiveTab}\n                />\n              </div>\n            )}\n\n            {activeTab === 'spec-updates' && (\n              <div className=\"sync-tab-content\">\n                <SpecStatusSection\n                  collection={collection}\n                  sourceUrl={sourceUrl}\n                  isLoading={isLoading}\n                  error={error}\n                  setError={setError}\n                  fileNotFound={fileNotFound}\n                  specDrift={specDrift}\n                  storedSpec={storedSpec}\n                  collectionDrift={collectionDrift}\n                  remoteDrift={remoteDrift}\n                  onCheck={checkForUpdates}\n                  onOpenSettings={() => setShowSettingsModal(true)}\n                />\n              </div>\n            )}\n          </>\n        )}\n\n      </div>\n\n      {showSettingsModal && (\n        <ConnectionSettingsModal\n          collection={collection}\n          sourceUrl={sourceUrl}\n          onSave={handleSaveSettings}\n          onDisconnect={() => {\n            setShowSettingsModal(false);\n            setShowDisconnectModal(true);\n          }}\n          onClose={() => setShowSettingsModal(false)}\n        />\n      )}\n\n      {showDisconnectModal && (\n        <DisconnectSyncModal\n          onConfirm={() => {\n            setShowDisconnectModal(false);\n            handleDisconnect();\n          }}\n          onClose={() => setShowDisconnectModal(false)}\n        />\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default OpenAPISyncTab;\n"
  },
  {
    "path": "packages/bruno-app/src/components/OpenAPISyncTab/utils.js",
    "content": "const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace'];\n\n/**\n * Count the number of HTTP endpoints in an OpenAPI spec.\n * Returns null if the spec has no paths (e.g. spec is null/undefined).\n */\nexport const countEndpoints = (spec) => {\n  if (!spec?.paths) return null;\n  let count = 0;\n  for (const path of Object.values(spec.paths)) {\n    for (const key of Object.keys(path)) {\n      if (HTTP_METHODS.includes(key.toLowerCase())) count++;\n    }\n  }\n  return count;\n};\n"
  },
  {
    "path": "packages/bruno-app/src/components/PathDisplay/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  width: 100%;\n  .path-display {\n    background: ${(props) => props.theme.requestTabPanel.url.bg};\n    border-radius: 4px;\n    padding: 8px 12px;\n    font-size: ${(props) => props.theme.font.size.base};\n    border: 1px solid rgba(0, 0, 0, 0.08);\n    \n    .icon-column {\n      padding-right: 8px;\n      align-items: flex-start;\n      padding-top: 2px;\n    }\n\n    .path-container {\n      flex-wrap: wrap;\n    }\n\n    .path-segment {\n      white-space: nowrap;\n    }\n\n    \n    .name-container, .file-extension {\n      color: ${(props) => props.theme.colors.text.yellow};\n    }\n\n    .separator {\n      color: ${(props) => props.theme.text};\n      opacity: 0.6;\n      margin: 0 2px;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/PathDisplay/index.js",
    "content": "import React from 'react';\nimport { IconFolder, IconFile } from '@tabler/icons';\nimport path from 'utils/common/path';\nimport StyledWrapper from './StyledWrapper';\n\nconst PathDisplay = ({\n  baseName = '',\n  iconType = 'file'\n}) => {\n  return (\n    <StyledWrapper>\n      <div className=\"path-display mt-2\">\n        <div className=\"path-layout flex font-mono\">\n          <div className=\"icon-column flex\">\n            {iconType === 'file' ? <IconFile size={16} /> : <IconFolder size={16} />}\n          </div>\n          <span className=\"name-container\">\n            {baseName}\n          </span>\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default PathDisplay;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Portal/index.js",
    "content": "import { createPortal } from 'react-dom';\n\nfunction Portal({ children }) {\n  return createPortal(children, document.body);\n}\nexport default Portal;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/Beta/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n  width: 100%;\n\n  .submit {\n    margin-top: 1rem;\n  }\n\n  .beta-feature-item {\n    border-radius: 0.5rem;\n    border: 1px solid var(--color-gray-200);\n    background-color: var(--color-gray-50);\n    margin-bottom: 1rem;\n  }\n\n  .beta-feature-item:hover {\n    background-color: var(--color-gray-100);\n  }\n\n  .beta-feature-description {\n    margin-top: 0.25rem;\n  }\n\n  .no-features-message {\n    color: var(--color-gray-500);\n    font-style: italic;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/Beta/index.js",
    "content": "import React, { useEffect, useCallback, useRef } from 'react';\nimport { useFormik } from 'formik';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { savePreferences } from 'providers/ReduxStore/slices/app';\nimport StyledWrapper from './StyledWrapper';\nimport * as Yup from 'yup';\nimport debounce from 'lodash/debounce';\nimport toast from 'react-hot-toast';\nimport { IconFlask } from '@tabler/icons';\nimport get from 'lodash/get';\nimport { BETA_FEATURES as BETA_FEATURE_IDS } from 'utils/beta-features';\n\n/**\n * UI metadata for beta features rendered in Preferences.\n * IDs must match keys from utils/beta-features.js BETA_FEATURES.\n */\nconst BETA_FEATURES = [\n  {\n    id: BETA_FEATURE_IDS.OPENAPI_SYNC,\n    label: 'OpenAPI Sync',\n    description: 'Synchronize your Bruno collection with an OpenAPI specification. Detect drift, review changes, and sync with a single click.'\n  }\n];\n\nconst Beta = ({ close }) => {\n  const preferences = useSelector((state) => state.app.preferences);\n  const dispatch = useDispatch();\n\n  // Generate validation schema dynamically from beta features\n  const generateValidationSchema = () => {\n    const schemaShape = {};\n    BETA_FEATURES.forEach((feature) => {\n      schemaShape[feature.id] = Yup.boolean();\n    });\n    return Yup.object().shape(schemaShape);\n  };\n\n  // Generate initial values dynamically from beta features\n  const generateInitialValues = () => {\n    const initialValues = {};\n    BETA_FEATURES.forEach((feature) => {\n      initialValues[feature.id] = get(preferences, `beta.${feature.id}`, false);\n    });\n    return initialValues;\n  };\n\n  const betaSchema = generateValidationSchema();\n\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: generateInitialValues(),\n    validationSchema: betaSchema,\n    onSubmit: async (values) => {\n      try {\n        const newPreferences = await betaSchema.validate(values, { abortEarly: true });\n        handleSave(newPreferences);\n      } catch (error) {\n        console.error('Beta preferences validation error:', error.message);\n      }\n    }\n  });\n\n  const handleSave = useCallback((newBetaPreferences) => {\n    dispatch(\n      savePreferences({\n        ...preferences,\n        beta: {\n          ...preferences.beta,\n          ...newBetaPreferences\n        }\n      })\n    )\n      .catch((err) => console.log(err) && toast.error('Failed to update beta preferences'));\n  }, [dispatch, preferences]);\n\n  const handleSaveRef = useRef(handleSave);\n  handleSaveRef.current = handleSave;\n\n  const debouncedSave = useCallback(\n    debounce((values) => {\n      betaSchema.validate(values, { abortEarly: true })\n        .then((validatedValues) => {\n          handleSaveRef.current(validatedValues);\n        })\n        .catch((error) => {\n        });\n    }, 500),\n    [betaSchema]\n  );\n\n  // Auto-save when form values change\n  useEffect(() => {\n    if (formik.dirty && formik.isValid) {\n      debouncedSave(formik.values);\n    }\n    return () => {\n      debouncedSave.flush();\n    };\n  }, [formik.values, formik.dirty, formik.isValid, debouncedSave]);\n\n  const hasAnyBetaFeatures = BETA_FEATURES.length > 0;\n\n  return (\n    <StyledWrapper>\n      <div className=\"section-header\">Beta Features</div>\n      <form onSubmit={formik.handleSubmit}>\n        <div className=\"mb-6\">\n          <p className=\"text-gray-500 dark:text-gray-400 mb-4 text-wrap\">\n            Beta features are experimental previews that may change before full release. Try them and share feedback.\n          </p>\n        </div>\n\n        <div className=\"space-y-4\">\n          {BETA_FEATURES.map((feature) => (\n            <div key={feature.id} className=\"beta-feature-item\">\n              <div className=\"flex items-center\">\n                <input\n                  id={feature.id}\n                  type=\"checkbox\"\n                  name={feature.id}\n                  checked={formik.values[feature.id]}\n                  onChange={formik.handleChange}\n                  className=\"mousetrap mr-0\"\n                />\n                <label className=\"block ml-2 select-none font-medium\" htmlFor={feature.id}>\n                  {feature.label}\n                </label>\n              </div>\n              <div className=\"beta-feature-description ml-6 text-xs text-gray-500 dark:text-gray-400\">\n                {feature.description}\n              </div>\n            </div>\n          ))}\n        </div>\n\n        {!hasAnyBetaFeatures && (\n          <div className=\"no-features-message\">\n            <p>No beta features are currently available</p>\n          </div>\n        )}\n      </form>\n    </StyledWrapper>\n  );\n};\n\nexport default Beta;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/Cache/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  color: ${(props) => props.theme.text};\n  \n  form.bruno-form {\n    label {\n      font-size: 0.8125rem;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/Cache/index.js",
    "content": "import React, { useEffect, useCallback, useRef } from 'react';\nimport { useFormik } from 'formik';\nimport { useSelector, useDispatch } from 'react-redux';\nimport {\n  savePreferences,\n  clearHttpHttpsAgentCache\n} from 'providers/ReduxStore/slices/app';\nimport toast from 'react-hot-toast';\nimport StyledWrapper from './StyledWrapper';\nimport * as Yup from 'yup';\nimport debounce from 'lodash/debounce';\nimport get from 'lodash/get';\n\nconst cacheSchema = Yup.object().shape({\n  sslSession: Yup.object({\n    enabled: Yup.boolean()\n  })\n});\n\nconst Cache = () => {\n  const preferences = useSelector((state) => state.app.preferences);\n  const dispatch = useDispatch();\n\n  const handleSave = useCallback(\n    (newCachePreferences) => {\n      dispatch(\n        savePreferences({\n          ...preferences,\n          cache: newCachePreferences\n        })\n      ).catch(() => toast.error('Failed to update cache preferences'));\n    },\n    [dispatch, preferences]\n  );\n\n  const handleSaveRef = useRef(handleSave);\n  handleSaveRef.current = handleSave;\n\n  const formik = useFormik({\n    initialValues: {\n      sslSession: {\n        enabled: get(preferences, 'cache.sslSession.enabled', false)\n      }\n    },\n    validationSchema: cacheSchema,\n    onSubmit: async (values) => {\n      try {\n        const newPreferences = await cacheSchema.validate(values, { abortEarly: true });\n        handleSave(newPreferences);\n      } catch (error) {\n        console.error('Cache preferences validation error:', error.message);\n      }\n    }\n  });\n\n  const debouncedSave = useCallback(\n    debounce((values) => {\n      cacheSchema\n        .validate(values, { abortEarly: true })\n        .then((validatedValues) => handleSaveRef.current(validatedValues))\n        .catch(() => {});\n    }, 500),\n    []\n  );\n\n  useEffect(() => {\n    if (formik.dirty && formik.isValid) {\n      debouncedSave(formik.values);\n    }\n    return () => {\n      debouncedSave.flush();\n    };\n  }, [formik.values, formik.dirty, formik.isValid, debouncedSave]);\n\n  const handleAgentCachingChange = (e) => {\n    formik.handleChange(e);\n    // Immediately evict all cached agents when caching is disabled\n    if (!e.target.checked) {\n      dispatch(clearHttpHttpsAgentCache()).catch(() => {});\n    }\n  };\n\n  const handleResetCache = () => {\n    dispatch(clearHttpHttpsAgentCache())\n      .then(() => toast.success('ssl session cache cleared'))\n      .catch(() => toast.error('Failed to clear ssl session cache'));\n  };\n\n  return (\n    <StyledWrapper className=\"w-full\">\n      <form className=\"bruno-form\" onSubmit={formik.handleSubmit}>\n        <div className=\"section-title mt-6 mb-3\">Cache SSL Session</div>\n\n        <div className=\"flex items-center my-2\">\n          <input\n            id=\"sslSession.enabled\"\n            type=\"checkbox\"\n            name=\"sslSession.enabled\"\n            checked={formik.values.sslSession.enabled}\n            onChange={handleAgentCachingChange}\n            className=\"mousetrap mr-0\"\n          />\n          <label className=\"block ml-2 select-none\" htmlFor=\"sslSession.enabled\">\n            Enable SSL session caching\n          </label>\n        </div>\n        <div className=\"text-xs mt-1 ml-6 opacity-70\">\n          Reuses TLS sessions and connections across requests for faster handshakes. Disable to create a fresh connection for every\n          request.\n        </div>\n\n        <div className=\"mt-6\">\n          <button type=\"button\" className=\"text-link cursor-pointer hover:underline\" onClick={handleResetCache}>\n            Clear\n          </button>\n        </div>\n      </form>\n    </StyledWrapper>\n  );\n};\n\nexport default Cache;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/Display/Font/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  color: ${(props) => props.theme.text};\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/Display/Font/index.js",
    "content": "import React, { useState, useEffect, useCallback, useRef } from 'react';\nimport get from 'lodash/get';\nimport debounce from 'lodash/debounce';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { savePreferences } from 'providers/ReduxStore/slices/app';\nimport StyledWrapper from './StyledWrapper';\nimport toast from 'react-hot-toast';\n\nconst Font = () => {\n  const dispatch = useDispatch();\n  const preferences = useSelector((state) => state.app.preferences);\n  const isInitialMount = useRef(true);\n\n  const [codeFont, setCodeFont] = useState(get(preferences, 'font.codeFont', 'default'));\n  const [codeFontSize, setCodeFontSize] = useState(get(preferences, 'font.codeFontSize', '13'));\n\n  const handleCodeFontChange = (event) => {\n    setCodeFont(event.target.value);\n  };\n\n  const handleCodeFontSizeChange = (event) => {\n    // Restrict to min/max value\n    const clampedSize = Math.max(1, Math.min(event.target.value, 32));\n    setCodeFontSize(clampedSize);\n  };\n\n  const handleSave = useCallback((font, fontSize) => {\n    dispatch(\n      savePreferences({\n        ...preferences,\n        font: {\n          codeFont: font,\n          codeFontSize: fontSize\n        }\n      })\n    ).catch(() => {\n      toast.error('Failed to save preferences');\n    });\n  }, [dispatch, preferences]);\n\n  const handleSaveRef = useRef(handleSave);\n  handleSaveRef.current = handleSave;\n\n  const debouncedSave = useCallback(\n    debounce((font, fontSize) => {\n      handleSaveRef.current(font, fontSize);\n    }, 500),\n    []\n  );\n\n  useEffect(() => {\n    if (isInitialMount.current) {\n      isInitialMount.current = false;\n      return;\n    }\n    debouncedSave(codeFont, codeFontSize);\n    return () => {\n      debouncedSave.flush();\n    };\n  }, [codeFont, codeFontSize, debouncedSave]);\n\n  return (\n    <StyledWrapper>\n      <div className=\"flex flex-row gap-2 w-full\">\n        <div className=\"w-4/5\">\n          <label className=\"block\">Code Editor Font</label>\n          <input\n            type=\"text\"\n            className=\"block textbox mt-2 w-full\"\n            autoComplete=\"off\"\n            autoCorrect=\"off\"\n            autoCapitalize=\"off\"\n            spellCheck=\"false\"\n            onChange={handleCodeFontChange}\n            defaultValue={codeFont}\n          />\n        </div>\n        <div className=\"w-1/5\">\n          <label className=\"block\">Font Size</label>\n          <input\n            type=\"number\"\n            className=\"block textbox mt-2 w-full\"\n            autoComplete=\"off\"\n            autoCorrect=\"off\"\n            inputMode=\"numeric\"\n            onChange={handleCodeFontSizeChange}\n            defaultValue={codeFontSize}\n          />\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Font;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/Display/Theme/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  color: var(--color-text);\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/Display/Theme/index.js",
    "content": "import React from 'react';\nimport { useFormik } from 'formik';\nimport * as Yup from 'yup';\nimport StyledWrapper from './StyledWrapper';\nimport { useTheme } from 'providers/Theme';\n\nconst Theme = () => {\n  const { storedTheme, setStoredTheme } = useTheme();\n\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: {\n      theme: storedTheme\n    },\n    validationSchema: Yup.object({\n      theme: Yup.string().oneOf(['light', 'dark', 'system']).required('theme is required')\n    }),\n    onSubmit: (values) => {\n      setStoredTheme(values.theme);\n    }\n  });\n\n  return (\n    <StyledWrapper>\n      <div className=\"bruno-form\">\n        <div className=\"flex items-center mt-2\">\n          <input\n            id=\"light-theme\"\n            className=\"cursor-pointer\"\n            type=\"radio\"\n            name=\"theme\"\n            onChange={(e) => {\n              formik.handleChange(e);\n              formik.handleSubmit();\n            }}\n            value=\"light\"\n            checked={formik.values.theme === 'light'}\n          />\n          <label htmlFor=\"light-theme\" className=\"ml-1 cursor-pointer select-none\">\n            Light\n          </label>\n\n          <input\n            id=\"dark-theme\"\n            className=\"ml-4 cursor-pointer\"\n            type=\"radio\"\n            name=\"theme\"\n            onChange={(e) => {\n              formik.handleChange(e);\n              formik.handleSubmit();\n            }}\n            value=\"dark\"\n            checked={formik.values.theme === 'dark'}\n          />\n          <label htmlFor=\"dark-theme\" className=\"ml-1 cursor-pointer select-none\">\n            Dark\n          </label>\n\n          <input\n            id=\"system-theme\"\n            className=\"ml-4 cursor-pointer\"\n            type=\"radio\"\n            name=\"theme\"\n            onChange={(e) => {\n              formik.handleChange(e);\n              formik.handleSubmit();\n            }}\n            value=\"system\"\n            checked={formik.values.theme === 'system'}\n          />\n          <label htmlFor=\"system-theme\" className=\"ml-1 cursor-pointer select-none\">\n            System\n          </label>\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Theme;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/Display/Zoom/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  color: ${(props) => props.theme.text};\n\n  .zoom-field {\n    position: relative;\n  }\n\n  .zoom-field label {\n    font-size: 0.875rem;\n    font-weight: 500;\n    margin-bottom: 0.5rem;\n    display: block;\n  }\n\n  .custom-select {\n    width: fit-content;\n    height: 35.89px;\n    padding: 0 0.5rem;\n    cursor: pointer;\n    appearance: none;\n    -webkit-appearance: none;\n    -moz-appearance: none;\n    background-color: ${(props) => props.theme.input.background};\n    border: 1px solid ${(props) => props.theme.input.border};\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    color: ${(props) => props.theme.text};\n    font-size: 0.875rem;\n    line-height: 1.5;\n    transition: all 0.15s ease;\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    gap: 0.5rem;\n  }\n\n  .custom-select:hover {\n    border-color: ${(props) => props.theme.input.hoverBorder || props.theme.input.border};\n  }\n\n  .custom-select .selected-value {\n    flex: 1;\n  }\n\n  .custom-select .chevron-icon {\n    color: ${(props) => props.theme.input.border};\n    flex-shrink: 0;\n    transition: transform 0.15s ease;\n    margin-left: auto;\n  }\n\n  .dropdown-menu {\n    width: 80px;\n    position: absolute;\n    top: 100%;\n    left: 0;\n    right: 0;\n    margin-top: 0.25rem;\n    background-color: ${(props) => props.theme.input.background};\n    border: 1px solid ${(props) => props.theme.input.border};\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);\n    z-index: 50;\n    max-height: 200px;\n    overflow-y: auto;\n    scrollbar-width: none;\n    -ms-overflow-style: none;\n  }\n\n  .dropdown-menu::-webkit-scrollbar {\n    display: none;\n  }\n\n  .dropdown-option {\n    padding: 0.5rem 0.75rem;\n    font-size: 0.875rem;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    transition: background-color 0.1s ease;\n  }\n\n  .dropdown-option:hover {\n    background-color: ${(props) => props.theme.input.border};\n  }\n\n  .dropdown-option.selected {\n    background-color: ${(props) => props.theme.input.focusBorder || props.theme.input.border}22;\n  }\n\n  .dropdown-option .option-label {\n    flex: 1;\n  }\n\n  .dropdown-option .check-icon {\n    color: ${(props) => props.theme.textLink};\n    flex-shrink: 0;\n  }\n\n  .reset-btn {\n    padding: 0.45rem 1rem;\n    background: transparent;\n    border: 1px solid ${(props) => props.theme.input.border};\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    color: ${(props) => props.theme.textLink};\n    font-size: 0.875rem;\n    line-height: 1.5;\n    cursor: pointer;\n    transition: all 0.15s ease;\n    white-space: nowrap;\n\n    &:hover {\n      background: ${(props) => props.theme.input.border};\n    }\n\n    &:focus {\n      outline: none;\n      border-color: ${(props) => props.theme.input.focusBorder};\n      box-shadow: 0 0 0 2px ${(props) => props.theme.input.focusBorder}33;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/Display/Zoom/index.js",
    "content": "import React, { useState, useRef, useEffect } from 'react';\nimport get from 'lodash/get';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { savePreferences } from 'providers/ReduxStore/slices/app';\nimport StyledWrapper from './StyledWrapper';\nimport { IconReload } from '@tabler/icons';\nimport { IconChevronDown, IconCheck } from '@tabler/icons';\nimport Button from 'ui/Button/index';\nconst { percentageToZoomLevel } = require('@usebruno/common');\n\n// Zoom options for dropdown (50% to 150%)\nconst ZOOM_OPTIONS = [\n  { label: '50%', value: 50 },\n  { label: '60%', value: 60 },\n  { label: '70%', value: 70 },\n  { label: '80%', value: 80 },\n  { label: '90%', value: 90 },\n  { label: '100%', value: 100 },\n  { label: '110%', value: 110 },\n  { label: '120%', value: 120 },\n  { label: '130%', value: 130 },\n  { label: '140%', value: 140 },\n  { label: '150%', value: 150 }\n];\n\nconst DEFAULT_ZOOM = 100;\n\nconst Zoom = () => {\n  const dispatch = useDispatch();\n  const preferences = useSelector((state) => state.app.preferences);\n  const dropdownRef = useRef(null);\n  const dropdownMenuRef = useRef(null);\n  const { ipcRenderer } = window;\n\n  // Get saved zoom percentage from Redux preferences (single source of truth)\n  const savedZoom = get(preferences, 'display.zoomPercentage', DEFAULT_ZOOM);\n  const [isOpen, setIsOpen] = useState(false);\n\n  // Close dropdown when clicking outside\n  useEffect(() => {\n    const handleClickOutside = (event) => {\n      if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {\n        setIsOpen(false);\n      }\n    };\n\n    document.addEventListener('mousedown', handleClickOutside);\n    return () => document.removeEventListener('mousedown', handleClickOutside);\n  }, []);\n\n  // Callback ref to scroll to selected option when dropdown renders\n  const setDropdownMenuRef = (node) => {\n    dropdownMenuRef.current = node;\n    if (node) {\n      const selectedOption = node.querySelector('.dropdown-option.selected');\n      if (selectedOption) {\n        selectedOption.scrollIntoView({ block: 'nearest', behavior: 'smooth' });\n      }\n    }\n  };\n\n  const handleSelect = (zoom) => {\n    // Apply zoom level to Electron window immediately\n    if (ipcRenderer) {\n      const zoomLevel = percentageToZoomLevel(zoom);\n      ipcRenderer.invoke('renderer:set-zoom-level', zoomLevel);\n    }\n\n    // Save to preferences via Redux (same pattern as layout)\n    const updatedPreferences = {\n      ...preferences,\n      display: {\n        ...get(preferences, 'display', {}),\n        zoomPercentage: zoom\n      }\n    };\n    dispatch(savePreferences(updatedPreferences));\n    setIsOpen(false);\n  };\n\n  const handleResetToDefault = () => {\n    handleSelect(DEFAULT_ZOOM);\n  };\n\n  const selectedOption = ZOOM_OPTIONS.find((opt) => opt.value === savedZoom);\n  const isDefault = savedZoom === DEFAULT_ZOOM;\n\n  return (\n    <StyledWrapper>\n      <div>\n        <label className=\"block\">Interface Zoom</label>\n      </div>\n      <div className=\"flex flex-row gap-1 items-center mt-2\">\n        <div className=\"zoom-field\" ref={dropdownRef}>\n          <div className=\"custom-select\" onClick={() => setIsOpen(!isOpen)}>\n            <span className=\"selected-value\">{selectedOption?.label}</span>\n            <IconChevronDown size={14} className=\"chevron-icon\" />\n          </div>\n          {isOpen && (\n            <div className=\"dropdown-menu\" ref={setDropdownMenuRef}>\n              {ZOOM_OPTIONS.map((option) => (\n                <div\n                  key={option.value}\n                  className={`dropdown-option ${option.value === savedZoom ? 'selected' : ''}`}\n                  onClick={() => handleSelect(option.value)}\n                >\n                  <span className=\"option-label\">{option.label}</span>\n                  {option.value === savedZoom && <IconCheck size={12} className=\"check-icon\" />}\n                </div>\n              ))}\n            </div>\n          )}\n        </div>\n        {!isDefault && (\n          <Button\n            size=\"sm\"\n            icon={<IconReload />}\n            color=\"secondary\"\n            variant=\"ghost\"\n            onClick={handleResetToDefault}\n          />\n        )}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Zoom;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/Display/index.js",
    "content": "import React from 'react';\nimport Font from './Font/index';\nimport Zoom from './Zoom/index';\n\nconst Display = ({ close }) => {\n  return (\n    <div className=\"flex flex-col gap-4 w-full\">\n      <div className=\"section-header\">Display</div>\n      <div className=\"flex flex-col mb-2 gap-10 w-full\">\n        <div className=\"w-fit flex flex-col gap-2\">\n          <Font close={close} />\n        </div>\n        <div className=\"w-full flex flex-col gap-2\">\n          <Zoom />\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default Display;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/General/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n  width: 100%;\n  \n  color: ${(props) => props.theme.text};\n\n  .text-link {\n    color: ${(props) => props.theme.colors.text.link};\n    text-decoration: none;\n    font-size: 0.8125rem;\n\n    &:hover {\n      text-decoration: underline;\n    }\n  }\n\n  form.bruno-form {\n    label {\n      font-size: 0.8125rem;\n    }\n  }\n\n  .default-location-input {\n    max-width: 28rem;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/General/index.js",
    "content": "import React, { useRef, useEffect, useCallback } from 'react';\nimport get from 'lodash/get';\nimport debounce from 'lodash/debounce';\nimport { useFormik } from 'formik';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { savePreferences } from 'providers/ReduxStore/slices/app';\nimport { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';\nimport StyledWrapper from './StyledWrapper';\nimport * as Yup from 'yup';\nimport toast from 'react-hot-toast';\nimport path from 'utils/common/path';\nimport { IconTrash } from '@tabler/icons';\n\nconst General = () => {\n  const preferences = useSelector((state) => state.app.preferences);\n  const dispatch = useDispatch();\n  const inputFileCaCertificateRef = useRef();\n\n  const preferencesSchema = Yup.object().shape({\n    sslVerification: Yup.boolean(),\n    customCaCertificate: Yup.object({\n      enabled: Yup.boolean(),\n      filePath: Yup.string().nullable()\n    }),\n    keepDefaultCaCertificates: Yup.object({\n      enabled: Yup.boolean()\n    }),\n    storeCookies: Yup.boolean(),\n    sendCookies: Yup.boolean(),\n    timeout: Yup.mixed()\n      .transform((value, originalValue) => {\n        return originalValue === '' ? undefined : value;\n      })\n      .nullable()\n      .test('isNumber', 'Request Timeout must be a number', (value) => {\n        return value === undefined || !isNaN(value);\n      })\n      .test('isValidTimeout', 'Request Timeout must be equal or greater than 0', (value) => {\n        return value === undefined || Number(value) >= 0;\n      }),\n    autoSave: Yup.object({\n      enabled: Yup.boolean(),\n      interval: Yup.mixed()\n        .transform((value, originalValue) => {\n          return originalValue === '' ? undefined : value;\n        })\n        .test('isNumber', 'Save Delay must be a number', (value) => {\n          return value === undefined || !isNaN(value);\n        })\n        .test('isValidInterval', 'Save Delay must be at least 500ms', (value) => {\n          return value === undefined || Number(value) >= 500;\n        })\n    }).test('intervalRequired', 'Save Delay is required when Auto Save is enabled', (value) => {\n      // If autosave is enabled, interval must be provided\n      if (value.enabled && (value.interval === undefined || value.interval === '')) {\n        return false;\n      }\n      return true;\n    }),\n    oauth2: Yup.object({\n      useSystemBrowser: Yup.boolean()\n    }),\n    defaultLocation: Yup.string().max(1024)\n  });\n\n  const formik = useFormik({\n    initialValues: {\n      sslVerification: preferences.request.sslVerification,\n      customCaCertificate: {\n        enabled: get(preferences, 'request.customCaCertificate.enabled', false),\n        filePath: get(preferences, 'request.customCaCertificate.filePath', null)\n      },\n      keepDefaultCaCertificates: {\n        enabled: get(preferences, 'request.keepDefaultCaCertificates.enabled', true)\n      },\n      timeout: preferences.request.timeout,\n      storeCookies: get(preferences, 'request.storeCookies', true),\n      sendCookies: get(preferences, 'request.sendCookies', true),\n      autoSave: {\n        enabled: get(preferences, 'autoSave.enabled', false),\n        interval: get(preferences, 'autoSave.interval', 1000)\n      },\n      oauth2: {\n        useSystemBrowser: get(preferences, 'request.oauth2.useSystemBrowser', false)\n      },\n      defaultLocation: get(preferences, 'general.defaultLocation', '')\n    },\n    validationSchema: preferencesSchema,\n    onSubmit: async (values) => {\n      try {\n        const newPreferences = await preferencesSchema.validate(values, { abortEarly: true });\n        handleSave(newPreferences);\n      } catch (error) {\n        console.error('Preferences validation error:', error.message);\n      }\n    }\n  });\n\n  const handleSave = useCallback((newPreferences) => {\n    dispatch(\n      savePreferences({\n        ...preferences,\n        request: {\n          sslVerification: newPreferences.sslVerification,\n          customCaCertificate: {\n            enabled: newPreferences.customCaCertificate.enabled,\n            filePath: newPreferences.customCaCertificate.filePath\n          },\n          keepDefaultCaCertificates: {\n            enabled: newPreferences.keepDefaultCaCertificates.enabled\n          },\n          timeout: newPreferences.timeout,\n          storeCookies: newPreferences.storeCookies,\n          sendCookies: newPreferences.sendCookies,\n          oauth2: {\n            useSystemBrowser: newPreferences.oauth2.useSystemBrowser\n          }\n        },\n        autoSave: {\n          enabled: newPreferences.autoSave.enabled,\n          interval: newPreferences.autoSave.interval\n        },\n        general: {\n          defaultLocation: newPreferences.defaultLocation\n        }\n      }))\n      .catch((err) => console.log(err) && toast.error('Failed to update preferences'));\n  }, [dispatch, preferences]);\n\n  const handleSaveRef = useRef(handleSave);\n  handleSaveRef.current = handleSave;\n\n  const debouncedSave = useCallback(\n    debounce((values) => {\n      preferencesSchema.validate(values, { abortEarly: true })\n        .then((validatedValues) => {\n          handleSaveRef.current(validatedValues);\n        })\n        .catch((error) => {\n        });\n    }, 500),\n    []\n  );\n\n  useEffect(() => {\n    if (formik.dirty && formik.isValid) {\n      debouncedSave(formik.values);\n    }\n    return () => {\n      debouncedSave.flush();\n    };\n  }, [formik.values, formik.dirty, formik.isValid, debouncedSave]);\n\n  const addCaCertificate = (e) => {\n    const filePath = window?.ipcRenderer?.getFilePath(e?.target?.files?.[0]);\n    if (filePath) {\n      formik.setFieldValue('customCaCertificate.filePath', filePath);\n    }\n  };\n\n  const deleteCaCertificate = () => {\n    formik.setFieldValue('customCaCertificate.filePath', null);\n  };\n\n  const browseDefaultLocation = () => {\n    dispatch(browseDirectory())\n      .then((dirPath) => {\n        if (typeof dirPath === 'string') {\n          formik.setFieldValue('defaultLocation', dirPath);\n        }\n      })\n      .catch((error) => {\n        formik.setFieldValue('defaultLocation', '');\n        console.error(error);\n      });\n  };\n\n  return (\n    <StyledWrapper className=\"w-full\">\n      <div className=\"section-header\">General Settings</div>\n      <form className=\"bruno-form\" onSubmit={formik.handleSubmit}>\n        <div className=\"flex items-center mb-2\">\n          <input\n            id=\"sslVerification\"\n            type=\"checkbox\"\n            name=\"sslVerification\"\n            checked={formik.values.sslVerification}\n            onChange={formik.handleChange}\n            className=\"mousetrap mr-0\"\n          />\n          <label className=\"block ml-2 select-none\" htmlFor=\"sslVerification\">\n            SSL/TLS Certificate Verification\n          </label>\n        </div>\n        <div className=\"flex items-center mt-2\">\n          <input\n            id=\"customCaCertificateEnabled\"\n            type=\"checkbox\"\n            name=\"customCaCertificate.enabled\"\n            checked={formik.values.customCaCertificate.enabled}\n            onChange={formik.handleChange}\n            className=\"mousetrap mr-0\"\n          />\n          <label className=\"block ml-2 select-none\" htmlFor=\"customCaCertificateEnabled\">\n            Use Custom CA Certificate\n          </label>\n        </div>\n        {formik.values.customCaCertificate.filePath ? (\n          <div\n            className={`flex items-center mt-2 pl-6 ${formik.values.customCaCertificate.enabled ? '' : 'opacity-25'}`}\n          >\n            <span className=\"flex items-center border px-2 rounded-md\">\n              {path.basename(formik.values.customCaCertificate.filePath)}\n              <button\n                type=\"button\"\n                tabIndex=\"-1\"\n                className=\"pl-1\"\n                disabled={formik.values.customCaCertificate.enabled ? false : true}\n                onClick={deleteCaCertificate}\n              >\n                <IconTrash strokeWidth={1.5} size={14} />\n              </button>\n            </span>\n          </div>\n        ) : (\n          <div\n            className={`flex items-center mt-2 pl-6 ${formik.values.customCaCertificate.enabled ? '' : 'opacity-25'}`}\n          >\n            <button\n              type=\"button\"\n              tabIndex=\"-1\"\n              className=\"flex items-center border px-2 rounded-md\"\n              disabled={formik.values.customCaCertificate.enabled ? false : true}\n              onClick={() => inputFileCaCertificateRef.current.click()}\n            >\n              select file\n              <input\n                id=\"caCertFilePath\"\n                type=\"file\"\n                name=\"customCaCertificate.filePath\"\n                className=\"hidden\"\n                ref={inputFileCaCertificateRef}\n                disabled={formik.values.customCaCertificate.enabled ? false : true}\n                onChange={addCaCertificate}\n              />\n            </button>\n          </div>\n        )}\n        <div className=\"flex items-center mt-2\">\n          <input\n            id=\"keepDefaultCaCertificatesEnabled\"\n            type=\"checkbox\"\n            name=\"keepDefaultCaCertificates.enabled\"\n            checked={formik.values.keepDefaultCaCertificates.enabled}\n            onChange={formik.handleChange}\n            className={`mousetrap mr-0 ${formik.values.customCaCertificate.enabled && formik.values.customCaCertificate.filePath ? '' : 'opacity-25'}`}\n            disabled={formik.values.customCaCertificate.enabled && formik.values.customCaCertificate.filePath ? false : true}\n          />\n          <label\n            className={`block ml-2 select-none ${formik.values.customCaCertificate.enabled && formik.values.customCaCertificate.filePath ? '' : 'opacity-25'}`}\n            htmlFor=\"keepDefaultCaCertificatesEnabled\"\n          >\n            Keep Default CA Certificates\n          </label>\n        </div>\n        <div className=\"flex items-center mt-2\">\n          <input\n            id=\"storeCookies\"\n            type=\"checkbox\"\n            name=\"storeCookies\"\n            checked={formik.values.storeCookies}\n            onChange={formik.handleChange}\n            className=\"mousetrap mr-0\"\n          />\n          <label className=\"block ml-2 select-none\" htmlFor=\"storeCookies\">\n            Store Cookies automatically\n          </label>\n        </div>\n        <div className=\"flex items-center mt-2\">\n          <input\n            id=\"sendCookies\"\n            type=\"checkbox\"\n            name=\"sendCookies\"\n            checked={formik.values.sendCookies}\n            onChange={formik.handleChange}\n            className=\"mousetrap mr-0\"\n          />\n          <label className=\"block ml-2 select-none\" htmlFor=\"sendCookies\">\n            Send Cookies automatically\n          </label>\n        </div>\n        <div className=\"flex items-center mt-2\">\n          <input\n            id=\"oauth2.useSystemBrowser\"\n            type=\"checkbox\"\n            name=\"oauth2.useSystemBrowser\"\n            checked={formik.values.oauth2.useSystemBrowser}\n            onChange={formik.handleChange}\n            className=\"mousetrap mr-0\"\n          />\n          <label className=\"block ml-2 select-none\" htmlFor=\"oauth2.useSystemBrowser\">\n            Use System Browser for OAuth2 Authorization\n          </label>\n        </div>\n        <div className=\"flex flex-col mt-6\">\n          <label className=\"block select-none\" htmlFor=\"timeout\">\n            Request Timeout (in ms)\n          </label>\n          <input\n            type=\"text\"\n            name=\"timeout\"\n            className=\"block textbox mt-2 w-16\"\n            autoComplete=\"off\"\n            autoCorrect=\"off\"\n            autoCapitalize=\"off\"\n            spellCheck=\"false\"\n            onChange={formik.handleChange}\n            value={formik.values.timeout}\n          />\n        </div>\n        {formik.touched.timeout && formik.errors.timeout ? (\n          <div className=\"text-red-500\">{formik.errors.timeout}</div>\n        ) : null}\n        <div className=\"flex items-center mt-6\">\n          <input\n            id=\"autoSaveEnabled\"\n            type=\"checkbox\"\n            name=\"autoSave.enabled\"\n            checked={formik.values.autoSave.enabled}\n            onChange={formik.handleChange}\n            className=\"mousetrap mr-0\"\n          />\n          <label className=\"block ml-2 select-none\" htmlFor=\"autoSaveEnabled\">\n            Enable Auto Save\n          </label>\n        </div>\n        <div className={`flex flex-col mt-2 ${!formik.values.autoSave.enabled ? 'opacity-50' : ''}`}>\n          <label className=\"block select-none\" htmlFor=\"autoSaveInterval\">\n            Save Delay (in ms)\n          </label>\n          <input\n            type=\"text\"\n            name=\"autoSave.interval\"\n            id=\"autoSaveInterval\"\n            className=\"block textbox mt-2 w-24\"\n            autoComplete=\"off\"\n            autoCorrect=\"off\"\n            autoCapitalize=\"off\"\n            spellCheck=\"false\"\n            onChange={formik.handleChange}\n            value={formik.values.autoSave.interval}\n            disabled={!formik.values.autoSave.enabled}\n          />\n        </div>\n        {formik.touched.autoSave && formik.errors.autoSave && typeof formik.errors.autoSave === 'string' && (\n          <div className=\"text-red-500\">{formik.errors.autoSave}</div>\n        )}\n        {formik.touched.autoSave?.interval && formik.errors.autoSave?.interval && (\n          <div className=\"text-red-500\">{formik.errors.autoSave.interval}</div>\n        )}\n        <div className=\"flex flex-col mt-6\">\n          <label className=\"block select-none default-location-label\" htmlFor=\"defaultLocation\">\n            Default Location\n          </label>\n          <p className=\"text-muted mt-1 text-xs\">\n            Used as the default location for new workspaces and collections\n          </p>\n          <input\n            type=\"text\"\n            name=\"defaultLocation\"\n            id=\"defaultLocation\"\n            className=\"block textbox mt-2 w-full cursor-pointer default-location-input\"\n            autoComplete=\"off\"\n            autoCorrect=\"off\"\n            autoCapitalize=\"off\"\n            spellCheck=\"false\"\n            readOnly={true}\n            onChange={formik.handleChange}\n            value={formik.values.defaultLocation || ''}\n            onClick={browseDefaultLocation}\n            placeholder=\"Click to browse for default location\"\n          />\n          <div className=\"mt-1\">\n            <span\n              className=\"text-link cursor-pointer hover:underline default-location-browse\"\n              onClick={browseDefaultLocation}\n            >\n              Browse\n            </span>\n          </div>\n        </div>\n        {formik.touched.defaultLocation && formik.errors.defaultLocation ? (\n          <div className=\"text-red-500\">{formik.errors.defaultLocation}</div>\n        ) : null}\n      </form>\n    </StyledWrapper>\n  );\n};\n\nexport default General;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/Keybindings/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n  width: 100%;\n  \n  table {\n    width: 80%;\n    border-collapse: collapse;\n\n    thead,\n    td {\n      border: 1px solid ${(props) => props.theme.table.border};\n    }\n\n    thead {\n      color: ${(props) => props.theme.table.thead.color};\n      font-size: ${(props) => props.theme.font.size.base};\n      user-select: none;\n    }\n\n    td {\n      padding: 6px 10px;\n      font-size: ${(props) => props.theme.font.size.sm};\n    }\n\n    thead th {\n      font-weight: 500;\n      padding: 10px;\n      text-align: left;\n      border: 1px solid ${(props) => props.theme.table.border};\n    }\n  }\n\n  .table-container {\n    overflow-y: auto;\n  }\n\n  .key-button {\n    display: inline-block;\n    color: ${(props) => props.theme.table.input.color};\n    opacity: 0.7;\n    border-radius: 4px;\n    padding: 1px 5px;\n    font-family: monospace;\n    margin-right: 8px;\n    border: 1px solid #ccc;\n    border-bottom: 1.44px solid ${(props) => props.theme.table.input.border};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/Keybindings/index.js",
    "content": "import StyledWrapper from './StyledWrapper';\nimport React from 'react';\nimport { getKeyBindingsForOS } from 'providers/Hotkeys/keyMappings';\nimport { isMacOS } from 'utils/common/platform';\n\nconst Keybindings = ({ close }) => {\n  const keyMapping = getKeyBindingsForOS(isMacOS() ? 'mac' : 'windows');\n\n  return (\n    <StyledWrapper className=\"w-full\">\n      <div className=\"section-header\">Keybindings</div>\n      <div className=\"table-container\">\n        <table>\n          <thead>\n            <tr>\n              <th>Command</th>\n              <th>Keybinding</th>\n            </tr>\n          </thead>\n          <tbody>\n            {keyMapping ? (\n              Object.entries(keyMapping).map(([action, { name, keys }], index) => (\n                <tr key={index}>\n                  <td>{name}</td>\n                  <td>\n                    {keys.split('+').map((key, i) => (\n                      <div className=\"key-button\" key={i}>\n                        {key}\n                      </div>\n                    ))}\n                  </td>\n                </tr>\n              ))\n            ) : (\n              <tr>\n                <td colSpan=\"2\">No key bindings available</td>\n              </tr>\n            )}\n          </tbody>\n        </table>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Keybindings;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/ProxySettings/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n  width: 100%;\n  \n  .settings-label {\n    width: 100px;\n  }\n\n  .textbox {\n    border: 1px solid #ccc;\n    padding: 0.15rem 0.45rem;\n    box-shadow: none;\n    outline: none;\n    transition: border-color ease-in-out 0.1s;\n    border-radius: 3px;\n    background-color: ${(props) => props.theme.input.bg};\n    border: 1px solid ${(props) => props.theme.input.border};\n\n    &:focus {\n      border: solid 1px ${(props) => props.theme.input.focusBorder} !important;\n      outline: none !important;\n    }\n  }\n\n  .system-proxy-settings {\n    label {\n      color: ${(props) => props.theme.colors.text.yellow};\n    }\n\n    .system-proxy-title {\n      color: ${(props) => props.theme.text};\n    }\n\n    .system-proxy-description {\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n\n    .system-proxy-error-container {\n      background: ${(props) => props.theme.status.danger.background};\n      border: 1px solid ${(props) => props.theme.status.danger.border};\n      width: fit-content;\n    }\n\n    .system-proxy-error-text {\n      color: ${(props) => props.theme.status.danger.text};\n    }\n\n    .system-proxy-source-label {\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n\n    .system-proxy-source-value {\n      color: ${(props) => props.theme.text};\n    }\n\n    .system-proxy-info-text {\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n\n    .system-proxy-value {\n      color: ${(props) => props.theme.colors.text.purple};\n      opacity: 0.8;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/ProxySettings/SystemProxy/index.js",
    "content": "import { useEffect, useState } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { IconLoader2, IconRefresh } from '@tabler/icons';\nimport { getSystemProxyVariables, refreshSystemProxy } from 'providers/ReduxStore/slices/app';\nimport StyledWrapper from '../StyledWrapper';\n\nconst SystemProxy = () => {\n  const dispatch = useDispatch();\n  const systemProxyVariables = useSelector((state) => state.app.systemProxyVariables);\n  const { source, http_proxy, https_proxy, no_proxy } = systemProxyVariables || {};\n  const [isFetching, setIsFetching] = useState(true);\n  const [error, setError] = useState(null);\n\n  const fetchProxy = (forceRefresh = false) => {\n    setIsFetching(true);\n    setError(null);\n    const action = forceRefresh ? refreshSystemProxy : getSystemProxyVariables;\n    dispatch(action())\n      .then(() => setError(null))\n      .catch((err) => setError(err.message || String(err)))\n      .finally(() => setIsFetching(false));\n  };\n\n  useEffect(() => {\n    fetchProxy(false);\n  }, [dispatch]);\n\n  const handleRefresh = () => {\n    fetchProxy(true);\n  };\n\n  return (\n    <StyledWrapper>\n      <div className=\"mb-3 text-muted system-proxy-settings space-y-4\">\n        <div className=\"flex items-start justify-start flex-col gap-2 mt-2\">\n          <div className=\"flex flex-row items-center gap-2\">\n            <div>\n              <h2 className=\"text-xs system-proxy-title flex flex-row\">\n                System Proxy {isFetching ? <IconLoader2 className=\"animate-spin ml-1\" size={16} strokeWidth={1.5} /> : null}\n              </h2>\n              <small className=\"system-proxy-description\">\n                Below values are sourced from your system proxy settings.\n              </small>\n            </div>\n          </div>\n        </div>\n        {error && (\n          <div className=\"mb-2 p-3 system-proxy-error-container rounded\">\n            <small className=\"system-proxy-error-text\">\n              Error loading system proxy settings: {error}\n            </small>\n          </div>\n        )}\n        {source && (\n          <div className=\"mb-2\">\n            <small className=\"font-medium flex flex-row gap-2\">\n              <div className=\"system-proxy-source-label text-xs\">\n                Proxy source:\n              </div>\n              <div className=\"system-proxy-source-value\">\n                {source}\n              </div>\n            </small>\n          </div>\n        )}\n        <small className=\"system-proxy-info-text\">\n          These values cannot be directly updated in Bruno. Please refer to your OS documentation to update these.\n        </small>\n        <div className=\"flex flex-col justify-start items-start pt-2\">\n          <div className=\"mb-1 flex items-center\">\n            <label className=\"settings-label\">\n              http_proxy\n            </label>\n            <div className=\"system-proxy-value\">{http_proxy || '-'}</div>\n          </div>\n          <div className=\"mb-1 flex items-center\">\n            <label className=\"settings-label\">\n              https_proxy\n            </label>\n            <div className=\"system-proxy-value\">{https_proxy || '-'}</div>\n          </div>\n          <div className=\"mb-1 flex items-center\">\n            <label className=\"settings-label\">\n              no_proxy\n            </label>\n            <div className=\"system-proxy-value\">{no_proxy || '-'}</div>\n          </div>\n        </div>\n        <span\n          className=\"text-link cursor-pointer hover:underline default-collection-location-browse flex flex-row items-center\"\n          onClick={handleRefresh}\n        >\n          <IconRefresh size={14} strokeWidth={1.5} className=\"mr-1\" />\n          Refresh\n        </span>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default SystemProxy;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/ProxySettings/index.js",
    "content": "import React, { useEffect, useCallback, useRef } from 'react';\nimport { useFormik } from 'formik';\nimport * as Yup from 'yup';\nimport debounce from 'lodash/debounce';\nimport toast from 'react-hot-toast';\nimport { savePreferences } from 'providers/ReduxStore/slices/app';\n\nimport StyledWrapper from './StyledWrapper';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { IconEye, IconEyeOff } from '@tabler/icons';\nimport { useState } from 'react';\nimport SystemProxy from './SystemProxy';\n\nconst ProxySettings = ({ close }) => {\n  const preferences = useSelector((state) => state.app.preferences);\n  const dispatch = useDispatch();\n\n  const proxySchema = Yup.object({\n    disabled: Yup.boolean().optional(),\n    inherit: Yup.boolean().required(),\n    config: Yup.object({\n      protocol: Yup.string().required().oneOf(['http', 'https', 'socks4', 'socks5']),\n      hostname: Yup.string().max(1024),\n      port: Yup.number()\n        .min(1)\n        .max(65535)\n        .typeError('Specify port between 1 and 65535')\n        .nullable()\n        .transform((_, val) => (val ? Number(val) : null)),\n      auth: Yup.object({\n        disabled: Yup.boolean().optional(),\n        username: Yup.string().max(1024),\n        password: Yup.string().max(1024)\n      }).optional(),\n      bypassProxy: Yup.string().optional().max(1024)\n    }).required()\n  });\n\n  const formik = useFormik({\n    initialValues: {\n      disabled: preferences.proxy.disabled || false,\n      inherit: preferences.proxy.inherit || false,\n      config: {\n        protocol: preferences.proxy.config?.protocol || 'http',\n        hostname: preferences.proxy.config?.hostname || '',\n        port: preferences.proxy.config?.port || 0,\n        auth: {\n          disabled: preferences.proxy.config?.auth?.disabled || false,\n          username: preferences.proxy.config?.auth?.username || '',\n          password: preferences.proxy.config?.auth?.password || ''\n        },\n        bypassProxy: preferences.proxy.config?.bypassProxy || ''\n      }\n    },\n    validationSchema: proxySchema,\n    onSubmit: (values) => {\n      onUpdate(values);\n    }\n  });\n\n  const onUpdate = useCallback((values) => {\n    proxySchema\n      .validate(values, { abortEarly: true })\n      .then((validatedProxy) => {\n        dispatch(\n          savePreferences({\n            ...preferences,\n            proxy: validatedProxy\n          })\n        ).catch(() => {\n          toast.error('Failed to save preferences');\n        });\n      })\n      .catch((error) => {\n      });\n  }, [dispatch, preferences, proxySchema]);\n\n  const onUpdateRef = useRef(onUpdate);\n  onUpdateRef.current = onUpdate;\n\n  const debouncedSave = useCallback(\n    debounce((values) => {\n      onUpdateRef.current(values);\n    }, 500),\n    []\n  );\n\n  const [passwordVisible, setPasswordVisible] = useState(false);\n\n  useEffect(() => {\n    if (formik.dirty && formik.isValid) {\n      debouncedSave(formik.values);\n    }\n    return () => {\n      debouncedSave.flush();\n    };\n  }, [formik.values, formik.dirty, formik.isValid, debouncedSave]);\n\n  return (\n    <StyledWrapper>\n      <div className=\"section-header\">Proxy Settings</div>\n      <form className=\"bruno-form\" onSubmit={formik.handleSubmit}>\n        <div className=\"mb-3 flex items-center mt-2\">\n          <label className=\"settings-label\" htmlFor=\"protocol\">\n            Mode\n          </label>\n          <div className=\"flex items-center\">\n            <label className=\"flex items-center cursor-pointer\">\n              <input\n                type=\"radio\"\n                name=\"mode\"\n                value=\"off\"\n                checked={formik.values.disabled === true}\n                onChange={(e) => {\n                  formik.setFieldValue('disabled', true);\n                  formik.setFieldValue('inherit', false);\n                }}\n                className=\"mr-1 cursor-pointer\"\n              />\n              Off\n            </label>\n            <label className=\"flex items-center ml-4 cursor-pointer\">\n              <input\n                type=\"radio\"\n                name=\"mode\"\n                value=\"on\"\n                checked={formik.values.disabled === false && formik.values.inherit === false}\n                onChange={(e) => {\n                  formik.setFieldValue('disabled', false);\n                  formik.setFieldValue('inherit', false);\n                }}\n                className=\"mr-1 cursor-pointer\"\n              />\n              On\n            </label>\n            <label className=\"flex items-center ml-4 cursor-pointer\">\n              <input\n                type=\"radio\"\n                name=\"mode\"\n                value=\"system\"\n                checked={formik.values.disabled === false && formik.values.inherit === true}\n                onChange={(e) => {\n                  formik.setFieldValue('disabled', false);\n                  formik.setFieldValue('inherit', true);\n                }}\n                className=\"mr-1 cursor-pointer\"\n              />\n              System Proxy\n            </label>\n          </div>\n        </div>\n        {formik.values.disabled === false && formik.values.inherit === true ? (\n          <div className=\"mb-3 pt-1 text-muted system-proxy-settings\">\n            <SystemProxy />\n          </div>\n        ) : null}\n        {formik.values.disabled === false && formik.values.inherit === false ? (\n          <>\n            <div className=\"mb-3 flex items-center\">\n              <label className=\"settings-label\" htmlFor=\"protocol\">\n                Protocol\n              </label>\n              <div className=\"flex items-center\">\n                <label className=\"flex items-center\">\n                  <input\n                    type=\"radio\"\n                    name=\"config.protocol\"\n                    value=\"http\"\n                    checked={formik.values.config.protocol === 'http'}\n                    onChange={formik.handleChange}\n                    className=\"mr-1\"\n                  />\n                  HTTP\n                </label>\n                <label className=\"flex items-center ml-4\">\n                  <input\n                    type=\"radio\"\n                    name=\"config.protocol\"\n                    value=\"https\"\n                    checked={formik.values.config.protocol === 'https'}\n                    onChange={formik.handleChange}\n                    className=\"mr-1\"\n                  />\n                  HTTPS\n                </label>\n                <label className=\"flex items-center ml-4\">\n                  <input\n                    type=\"radio\"\n                    name=\"config.protocol\"\n                    value=\"socks4\"\n                    checked={formik.values.config.protocol === 'socks4'}\n                    onChange={formik.handleChange}\n                    className=\"mr-1\"\n                  />\n                  SOCKS4\n                </label>\n                <label className=\"flex items-center ml-4\">\n                  <input\n                    type=\"radio\"\n                    name=\"config.protocol\"\n                    value=\"socks5\"\n                    checked={formik.values.config.protocol === 'socks5'}\n                    onChange={formik.handleChange}\n                    className=\"mr-1\"\n                  />\n                  SOCKS5\n                </label>\n              </div>\n            </div>\n            <div className=\"mb-3 flex items-center\">\n              <label className=\"settings-label\" htmlFor=\"config.hostname\">\n                Hostname\n              </label>\n              <input\n                id=\"config.hostname\"\n                type=\"text\"\n                name=\"config.hostname\"\n                className=\"block textbox\"\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n                onChange={formik.handleChange}\n                value={formik.values.config.hostname || ''}\n              />\n              {formik.touched.config?.hostname && formik.errors.config?.hostname ? (\n                <div className=\"ml-3 text-red-500\">{formik.errors.config.hostname}</div>\n              ) : null}\n            </div>\n            <div className=\"mb-3 flex items-center\">\n              <label className=\"settings-label\" htmlFor=\"config.port\">\n                Port\n              </label>\n              <input\n                id=\"config.port\"\n                type=\"number\"\n                name=\"config.port\"\n                className=\"block textbox\"\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n                onChange={formik.handleChange}\n                value={formik.values.config.port}\n              />\n              {formik.touched.config?.port && formik.errors.config?.port ? (\n                <div className=\"ml-3 text-red-500\">{formik.errors.config.port}</div>\n              ) : null}\n            </div>\n            <div className=\"mb-3 flex items-center\">\n              <label className=\"settings-label\" htmlFor=\"config.auth.disabled\">\n                Auth\n              </label>\n              <input\n                id=\"config.auth.disabled\"\n                type=\"checkbox\"\n                name=\"config.auth.disabled\"\n                checked={!formik.values.config.auth.disabled}\n                onChange={(e) => {\n                  formik.setFieldValue('config.auth.disabled', !e.target.checked);\n                }}\n                className=\"mousetrap mr-0\"\n              />\n            </div>\n            <div>\n              <div className=\"mb-3 flex items-center\">\n                <label className=\"settings-label\" htmlFor=\"config.auth.username\">\n                  Username\n                </label>\n                <input\n                  id=\"config.auth.username\"\n                  type=\"text\"\n                  name=\"config.auth.username\"\n                  className=\"block textbox\"\n                  autoComplete=\"off\"\n                  autoCorrect=\"off\"\n                  autoCapitalize=\"off\"\n                  spellCheck=\"false\"\n                  value={formik.values.config.auth.username}\n                  onChange={formik.handleChange}\n                />\n                {formik.touched.config?.auth?.username && formik.errors.config?.auth?.username ? (\n                  <div className=\"ml-3 text-red-500\">{formik.errors.config.auth.username}</div>\n                ) : null}\n              </div>\n              <div className=\"mb-3 flex items-center\">\n                <label className=\"settings-label\" htmlFor=\"config.auth.password\">\n                  Password\n                </label>\n                <div className=\"textbox flex flex-row items-center w-[13.2rem] h-[2.25rem] relative\">\n                  <input\n                    id=\"config.auth.password\"\n                    type={passwordVisible ? `text` : 'password'}\n                    name=\"config.auth.password\"\n                    className=\"outline-none w-[10.5rem] bg-transparent\"\n                    autoComplete=\"off\"\n                    autoCorrect=\"off\"\n                    autoCapitalize=\"off\"\n                    spellCheck=\"false\"\n                    value={formik.values.config.auth.password}\n                    onChange={formik.handleChange}\n                  />\n                  <button\n                    type=\"button\"\n                    className=\"btn btn-sm absolute right-0\"\n                    onClick={() => setPasswordVisible(!passwordVisible)}\n                  >\n                    {passwordVisible ? <IconEyeOff size={18} strokeWidth={2} /> : <IconEye size={18} strokeWidth={2} />}\n                  </button>\n                </div>\n                {formik.touched.config?.auth?.password && formik.errors.config?.auth?.password ? (\n                  <div className=\"ml-3 text-red-500\">{formik.errors.config.auth.password}</div>\n                ) : null}\n              </div>\n            </div>\n            <div className=\"mb-3 flex items-center\">\n              <label className=\"settings-label\" htmlFor=\"config.bypassProxy\">\n                Proxy Bypass\n              </label>\n              <input\n                id=\"config.bypassProxy\"\n                type=\"text\"\n                name=\"config.bypassProxy\"\n                className=\"block textbox\"\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n                onChange={formik.handleChange}\n                value={formik.values.config.bypassProxy || ''}\n              />\n              {formik.touched.config?.bypassProxy && formik.errors.config?.bypassProxy ? (\n                <div className=\"ml-3 text-red-500\">{formik.errors.config.bypassProxy}</div>\n              ) : null}\n            </div>\n          </>\n        ) : null}\n      </form>\n    </StyledWrapper>\n  );\n};\n\nexport default ProxySettings;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  div.tabs {\n    padding: 12px;\n    min-width: 160px;\n\n    div.tab {\n      display: flex;\n      align-items: center;\n      gap: 8px;\n      width: 100%;\n      padding: 6px 10px;\n      border: none;\n      border-radius: ${(props) => props.theme.border.radius.sm};\n      color: ${(props) => props.theme.colors.text.muted};\n      cursor: pointer;\n      transition: background-color 0.15s ease;\n\n      &:focus,\n      &:active,\n      &:focus-within,\n      &:focus-visible,\n      &:target {\n        outline: none !important;\n        box-shadow: none !important;\n      }\n\n      &.active {\n        color: ${(props) => props.theme.text} !important;\n        background: ${(props) => props.theme.tabs.secondary.active.bg};\n\n        &:hover {\n          background: ${(props) => props.theme.tabs.secondary.active.bg} !important;\n        }\n      }\n    }\n  }\n\n  section.tab-panel {\n    min-height: 70vh;\n    overflow-y: auto;\n    flex-grow: 1;\n    padding: 12px;\n  }\n\n  input[type=\"checkbox\"],\n  input[type=\"radio\"] {\n    accent-color: ${(props) => props.theme.workspace.accent};\n    cursor: pointer;\n  }\n\n  .textbox {\n    line-height: 1.5;\n    padding: 0.45rem;\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    background-color: ${(props) => props.theme.input.bg};\n    border: 1px solid ${(props) => props.theme.input.border};\n    color: ${(props) => props.theme.text};\n\n    &:focus {\n      border: solid 1px ${(props) => props.theme.input.focusBorder} !important;\n      outline: none !important;\n    }\n\n    &:disabled {\n      opacity: 0.5;\n      cursor: not-allowed;\n    }\n  }\n  .section-header {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.muted};\n    font-weight: 500;\n    margin: 6px 0 8px 0;\n    text-transform: uppercase;\n    letter-spacing: 0.5px;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/Support/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n  width: 100%;\n\n  color: ${(props) => props.theme.text};\n  .rows {\n    svg {\n      position: relative;\n      top: -1px;\n    }\n\n    .label {\n      cursor: pointer;\n      &:hover {\n        text-decoration: underline;\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/Support/index.js",
    "content": "import React from 'react';\nimport { IconSpeakerphone, IconBrandTwitter, IconBrandGithub, IconBrandDiscord, IconBook } from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\nimport { useTranslation } from 'react-i18next';\n\nconst Support = () => {\n  const { t } = useTranslation();\n\n  return (\n    <StyledWrapper>\n      <div className=\"section-header\">Support</div>\n      <div className=\"rows\">\n        <div className=\"mb-2\">\n          <a href=\"https://docs.usebruno.com\" target=\"_blank\" className=\"flex items-end\">\n            <IconBook size={18} strokeWidth={2} />\n            <span className=\"label ml-2\">{t('COMMON.DOCUMENTATION')}</span>\n          </a>\n        </div>\n        <div className=\"mt-2\">\n          <a href=\"https://github.com/usebruno/bruno/issues\" target=\"_blank\" className=\"flex items-end\">\n            <IconSpeakerphone size={18} strokeWidth={2} />\n            <span className=\"label ml-2\">{t('COMMON.REPORT_ISSUES')}</span>\n          </a>\n        </div>\n        <div className=\"mt-2\">\n          <a href=\"https://discord.com/invite/KgcZUncpjq\" target=\"_blank\" className=\"flex items-end\">\n            <IconBrandDiscord size={18} strokeWidth={2} />\n            <span className=\"label ml-2\">{t('COMMON.DISCORD')}</span>\n          </a>\n        </div>\n        <div className=\"mt-2\">\n          <a href=\"https://github.com/usebruno/bruno\" target=\"_blank\" className=\"flex items-end\">\n            <IconBrandGithub size={18} strokeWidth={2} />\n            <span className=\"label ml-2\">{t('COMMON.GITHUB')}</span>\n          </a>\n        </div>\n        <div className=\"mt-2\">\n          <a href=\"https://twitter.com/use_bruno\" target=\"_blank\" className=\"flex items-end\">\n            <IconBrandTwitter size={18} strokeWidth={2} />\n            <span className=\"label ml-2\">{t('COMMON.TWITTER')}</span>\n          </a>\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Support;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/Themes/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst StyledWrapper = styled.div`\n  .appearance-container {\n    padding-bottom: 16px;\n  }\n        \n  .theme-mode-option {\n    border: 1px solid ${(props) => props.theme.input.border};\n    border-radius: ${(props) => props.theme.border.radius.md};\n    box-shadow: none;\n    padding: 6px 8px;\n    width: auto;\n\n    &.selected {\n      border: 1px solid ${(props) => props.theme.accents.primary};\n      background: ${(props) => rgba(props.theme.accents.primary, 0.07)};\n      cursor: default;\n    }\n\n    &:hover {\n      border: 1px solid ${(props) => props.theme.accents.primary};\n    }\n  }\n\n  .theme-variant-label {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.muted};\n    margin-bottom: 12px;\n  }\n\n  .theme-variants {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 12px;\n  }\n\n  .theme-variant-card {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    padding: 12px 8px;\n    border: 2px solid ${(props) => props.theme.input.border};\n    border-radius: ${(props) => props.theme.border.radius.md};\n    cursor: pointer;\n    transition: all 0.15s ease;\n    min-width: 155px;\n\n    &:hover {\n      border-color: ${(props) => props.theme.input.focusBorder};\n    }\n\n    &.selected {\n      border-color: ${(props) => props.theme.accents.primary};\n      background: ${(props) => rgba(props.theme.accents.primary, 0.07)};\n      cursor: default;\n    }\n  }\n\n  .theme-preview {\n    width: 60px;\n    height: 40px;\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    margin-bottom: 8px;\n    display: flex;\n    overflow: hidden;\n  }\n\n  .theme-preview-sidebar {\n    width: 15px;\n    height: 100%;\n  }\n\n  .theme-preview-main {\n    flex: 1;\n    height: 100%;\n    display: flex;\n    flex-direction: column;\n    padding: 4px;\n    gap: 3px;\n  }\n\n  .theme-preview-line {\n    height: 4px;\n    border-radius: 2px;\n    width: 80%;\n  }\n\n  .theme-variant-name {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.text};\n  }\n\n  .section-divider {\n    height: 1px;\n    background: ${(props) => props.theme.input.border};\n    margin: 15px 0;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/Themes/index.js",
    "content": "import React from 'react';\nimport { rgba } from 'polished';\nimport { useTheme } from 'providers/Theme';\nimport themes, { getLightThemes, getDarkThemes } from 'themes/index';\nimport { IconBrightnessUp, IconMoon, IconDeviceDesktop } from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\n\nconst ThemePreview = ({ themeId, isDark }) => {\n  const theme = themes[themeId] || themes[isDark ? 'dark' : 'light'];\n\n  const bgColor = theme.background.base;\n  const sidebarColor = theme.sidebar.bg;\n  const lineColor = rgba(theme.brand, 0.5);\n\n  return (\n    <div className=\"theme-preview\" style={{ background: bgColor, border: `1px solid ${lineColor}` }}>\n      <div className=\"theme-preview-sidebar\" style={{ background: sidebarColor }} />\n      <div className=\"theme-preview-main\">\n        <div className=\"theme-preview-line\" style={{ background: lineColor }} />\n        <div className=\"theme-preview-line\" style={{ background: lineColor, width: '60%' }} />\n        <div className=\"theme-preview-line\" style={{ background: lineColor, width: '70%' }} />\n      </div>\n    </div>\n  );\n};\n\nconst ThemeVariantCard = ({ theme, isSelected, onClick }) => {\n  const isDark = theme.mode === 'dark';\n\n  return (\n    <div className={`theme-variant-card ${isSelected ? 'selected' : ''}`} onClick={onClick}>\n      <ThemePreview themeId={theme.id} isDark={isDark} />\n      <span className=\"theme-variant-name\">{theme.name}</span>\n    </div>\n  );\n};\n\nconst Themes = () => {\n  const {\n    storedTheme,\n    setStoredTheme,\n    themeVariantLight,\n    setThemeVariantLight,\n    themeVariantDark,\n    setThemeVariantDark\n  } = useTheme();\n\n  const lightThemes = getLightThemes();\n  const darkThemes = getDarkThemes();\n\n  const themeModes = [\n    { key: 'light', label: 'Light', icon: IconBrightnessUp },\n    { key: 'dark', label: 'Dark', icon: IconMoon },\n    { key: 'system', label: 'System', icon: IconDeviceDesktop }\n  ];\n\n  const handleModeChange = (mode) => {\n    setStoredTheme(mode);\n  };\n\n  const renderThemeVariants = (themes, selectedVariant, onSelect, label) => (\n    <div className=\"theme-variant-section\">\n      <div className=\"theme-variant-label\">{label}</div>\n      <div className=\"theme-variants\">\n        {themes.map((theme) => (\n          <ThemeVariantCard\n            key={theme.id}\n            theme={theme}\n            isSelected={selectedVariant === theme.id}\n            onClick={() => onSelect(theme.id)}\n          />\n        ))}\n      </div>\n    </div>\n  );\n\n  return (\n    <StyledWrapper>\n      <div className=\"flex flex-col gap-4 w-full appearance-container\">\n        <div>\n          <div className=\"section-header\">Appearance</div>\n        </div>\n\n        <div className=\"flex gap-3 theme-mode-selector justify-start\">\n          {themeModes.map((mode) => {\n            const Icon = mode.icon;\n            const isSelected = storedTheme === mode.key;\n\n            return (\n              <button\n                key={mode.key}\n                onClick={() => handleModeChange(mode.key)}\n                className={`theme-mode-option relative ${isSelected ? 'selected' : ''}`}\n              >\n                <div className=\"flex items-center justify-start gap-2\">\n                  <Icon size={16} strokeWidth={1.5} />\n                  <span>{mode.label}</span>\n                </div>\n              </button>\n            );\n          })}\n        </div>\n\n        <div className=\"section-divider\" />\n\n        {storedTheme === 'light' && (\n          <>\n            {renderThemeVariants(lightThemes, themeVariantLight, setThemeVariantLight, 'Light Theme')}\n          </>\n        )}\n\n        {storedTheme === 'dark' && (\n          <>\n            {renderThemeVariants(darkThemes, themeVariantDark, setThemeVariantDark, 'Dark Theme')}\n          </>\n        )}\n\n        {storedTheme === 'system' && (\n          <>\n            {renderThemeVariants(lightThemes, themeVariantLight, setThemeVariantLight, 'Light Theme')}\n            <div className=\"section-divider\" />\n            {renderThemeVariants(darkThemes, themeVariantDark, setThemeVariantDark, 'Dark Theme')}\n          </>\n        )}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Themes;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Preferences/index.js",
    "content": "import classnames from 'classnames';\nimport React from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { updateActivePreferencesTab } from 'providers/ReduxStore/slices/app';\nimport {\n  IconSettings,\n  IconPalette,\n  IconBrowser,\n  IconUserCircle,\n  IconKeyboard,\n  IconZoomQuestion,\n  IconSquareLetterB,\n  IconDatabase\n} from '@tabler/icons';\n\nimport Support from './Support';\nimport General from './General';\nimport Themes from './Themes';\nimport Proxy from './ProxySettings';\nimport Display from './Display';\nimport Keybindings from './Keybindings';\nimport Beta from './Beta';\n\nimport StyledWrapper from './StyledWrapper';\nimport Cache from './Cache/index';\n\nconst Preferences = () => {\n  const dispatch = useDispatch();\n  const tab = useSelector((state) => state.app.activePreferencesTab);\n\n  const setTab = (tab) => {\n    dispatch(updateActivePreferencesTab({ tab }));\n  };\n\n  const getTabClassname = (tabName) => {\n    return classnames(`tab select-none ${tabName}`, {\n      active: tabName === tab\n    });\n  };\n\n  const getTabPanel = (tab) => {\n    switch (tab) {\n      case 'general': {\n        return <General />;\n      }\n\n      case 'themes': {\n        return <Themes />;\n      }\n\n      case 'proxy': {\n        return <Proxy />;\n      }\n\n      case 'display': {\n        return <Display />;\n      }\n\n      case 'keybindings': {\n        return <Keybindings />;\n      }\n\n      case 'beta': {\n        return <Beta />;\n      }\n\n      case 'support': {\n        return <Support />;\n      }\n\n      case 'cache': {\n        return <Cache />;\n      }\n    }\n  };\n\n  return (\n    <StyledWrapper className=\"h-full\">\n      <div className=\"flex flex-row gap-2 h-full\">\n        <div className=\"flex flex-col items-center tabs tablist\" role=\"tablist\">\n          <div className={getTabClassname('general')} role=\"tab\" onClick={() => setTab('general')}>\n            <IconSettings size={16} strokeWidth={1.5} />\n            General\n          </div>\n          <div className={getTabClassname('themes')} role=\"tab\" onClick={() => setTab('themes')}>\n            <IconPalette size={16} strokeWidth={1.5} />\n            Themes\n          </div>\n          <div className={getTabClassname('display')} role=\"tab\" onClick={() => setTab('display')}>\n            <IconBrowser size={16} strokeWidth={1.5} />\n            Display\n          </div>\n          <div className={getTabClassname('proxy')} role=\"tab\" onClick={() => setTab('proxy')}>\n            <IconUserCircle size={16} strokeWidth={1.5} />\n            Proxy\n          </div>\n          <div className={getTabClassname('keybindings')} role=\"tab\" onClick={() => setTab('keybindings')}>\n            <IconKeyboard size={16} strokeWidth={1.5} />\n            Keybindings\n          </div>\n          <div className={getTabClassname('cache')} role=\"tab\" onClick={() => setTab('cache')}>\n            <IconDatabase size={16} strokeWidth={1.5} />\n            Cache\n          </div>\n          <div className={getTabClassname('support')} role=\"tab\" onClick={() => setTab('support')}>\n            <IconZoomQuestion size={16} strokeWidth={1.5} />\n            Support\n          </div>\n          <div className={getTabClassname('beta')} role=\"tab\" onClick={() => setTab('beta')}>\n            <IconSquareLetterB size={16} strokeWidth={1.5} />\n            Beta\n          </div>\n        </div>\n        <section\n          className=\"flex flex-grow ps-2 pe-4 pt-2 pb-6 p-[12px] tab-panel\"\n          role=\"tabpanel\"\n          id={`${tab}-panel`}\n          aria-labelledby={`${tab}-tab`}\n        >\n          {getTabPanel(tab)}\n        </section>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Preferences;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RadioButton/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .radio-container {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    position: relative;\n  }\n\n  .radio-input {\n    appearance: none;\n    -webkit-appearance: none;\n    -moz-appearance: none;\n    width: 16px;\n    height: 16px;\n    border: 2px solid ${(props) => props.theme.colors.text.muted};\n    border-radius: 50%;\n    background-color: transparent;\n    cursor: pointer;\n    position: relative;\n    outline: none;\n    box-shadow: none;\n    margin: 0;\n    \n    &:checked {\n      border-color: ${(props) => props.theme.colors.text.yellow};\n      background-color: transparent;\n      \n      &::after {\n        content: '';\n        position: absolute;\n        top: 50%;\n        left: 50%;\n        transform: translate(-50%, -50%);\n        width: 0.5rem;\n        height: 0.5rem;\n        border-radius: 50%;\n        background-color: ${(props) => props.theme.colors.text.yellow};\n      }\n    }\n    \n    &:disabled {\n      cursor: not-allowed;\n      opacity: 0.5;\n      border-color: ${(props) => props.theme.colors.text.muted};\n      background-color: transparent;\n      \n      &:checked {\n        border-color: ${(props) => props.theme.colors.text.muted};\n        \n        &::after {\n          background-color: ${(props) => props.theme.colors.text.muted};\n        }\n      }\n    }\n    \n    &:hover:not(:disabled) {\n      opacity: 0.8;\n    }\n  }\n\n  .radio-label {\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 16px;\n    height: 16px;\n    cursor: pointer;\n    pointer-events: none;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RadioButton/index.js",
    "content": "import React from 'react';\nimport StyledWrapper from './StyledWrapper';\n\nconst RadioButton = ({\n  checked,\n  disabled = false,\n  onChange,\n  name,\n  value,\n  id,\n  className = '',\n  dataTestId = 'radio-button'\n}) => {\n  const handleChange = (e) => {\n    if (!disabled && onChange) {\n      onChange(e);\n    }\n  };\n\n  return (\n    <StyledWrapper>\n      <div className={`radio-container ${className}`}>\n        <input\n          type=\"radio\"\n          id={id}\n          name={name}\n          value={value}\n          checked={checked}\n          disabled={disabled}\n          onChange={handleChange}\n          className=\"radio-input\"\n          data-testid={dataTestId}\n        />\n        <label htmlFor={id} className=\"radio-label\" />\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default RadioButton;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ReorderTable/index.js",
    "content": "import React, { useEffect, useRef, useState, useMemo } from 'react';\nimport { IconGripVertical, IconMinusVertical } from '@tabler/icons';\n\n/**\n * ReorderTable Component\n *\n * A table component that allows rows to be reordered via drag-and-drop.\n *\n * @param {Object} props - The component props\n * @param {React.ReactNode[]} props.children - The table rows as children\n * @param {function} props.updateReorderedItem - Callback function to handle reordered rows\n */\n\nconst ReorderTable = ({ children, updateReorderedItem }) => {\n  const tbodyRef = useRef();\n  const [hoveredRow, setHoveredRow] = useState(null);\n  const [dragStart, setDragStart] = useState(null);\n\n  const rowsOrder = useMemo(() => React.Children.toArray(children), [children]);\n\n  /**\n   * useEffect hook to handle row hover states\n   */\n  useEffect(() => {\n    handleRowHover(null, false);\n  }, [children]);\n\n  const handleRowHover = (index, hoverstatus = true) => {\n    setHoveredRow(hoverstatus ? index : null);\n  };\n\n  const handleDragStart = (e, index) => {\n    e.dataTransfer.effectAllowed = 'move';\n    e.dataTransfer.setData('text/plain', index);\n    setDragStart(index);\n  };\n\n  const handleDragOver = (e, index) => {\n    e.preventDefault();\n    e.dataTransfer.dropEffect = 'move';\n    handleRowHover(index);\n  };\n\n  const handleDrop = (e, toIndex) => {\n    e.preventDefault();\n    const fromIndex = parseInt(e.dataTransfer.getData('text/plain'), 10);\n    if (fromIndex !== toIndex) {\n      const updatedRowsOrder = [...rowsOrder];\n      const [movedRow] = updatedRowsOrder.splice(fromIndex, 1);\n      updatedRowsOrder.splice(toIndex, 0, movedRow);\n\n      updateReorderedItem({\n        updateReorderedItem: updatedRowsOrder.map((row) => row.props['data-uid'])\n      });\n\n      setTimeout(() => {\n        handleRowHover(toIndex);\n      }, 0);\n    }\n  };\n\n  return (\n    <tbody ref={tbodyRef}>\n      {rowsOrder.map((row, index) => (\n        <tr\n          key={row.props['data-uid']}\n          data-uid={row.props['data-uid']}\n          draggable\n          onDragStart={(e) => handleDragStart(e, index)}\n          onDragOver={(e) => handleDragOver(e, index)}\n          onDrop={(e) => handleDrop(e, index)}\n          onMouseEnter={() => handleRowHover(index)}\n          onMouseLeave={() => handleRowHover(index, false)}\n        >\n          {React.Children.map(row.props.children, (child, childIndex) => {\n            if (childIndex === 0) {\n              return React.cloneElement(child, {\n                children: (\n                  <>\n                    <div\n                      draggable\n                      className=\"group drag-handle absolute z-10 left-[-17px] top-1/2 -translate-y-[80%] p-2.5 cursor-grab\"\n                    >\n                      {hoveredRow === index && (\n                        <>\n                          <IconGripVertical\n                            size={14}\n                            className=\"z-10 icon-grip rounded-md absolute hidden group-hover:block\"\n                          />\n                          <IconMinusVertical\n                            size={14}\n                            className=\"z-10 icon-minus rounded-md absolute block group-hover:hidden\"\n                          />\n                        </>\n                      )}\n                    </div>\n                    {child.props.children}\n                  </>\n                )\n              });\n            } else {\n              return child;\n            }\n          })}\n        </tr>\n      ))}\n    </tbody>\n  );\n};\n\nexport default ReorderTable;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Assertions/AssertionOperator/index.js",
    "content": "import React from 'react';\n\n/**\n * Assertion operators\n *\n * eq          : equal to\n * neq         : not equal to\n * gt          : greater than\n * gte         : greater than or equal to\n * lt          : less than\n * lte         : less than or equal to\n * in          : in\n * notIn       : not in\n * contains    : contains\n * notContains : not contains\n * length      : length\n * matches     : matches\n * notMatches  : not matches\n * startsWith  : starts with\n * endsWith    : ends with\n * between     : between\n * isEmpty     : is empty\n * isNotEmpty  : is not empty\n * isNull      : is null\n * isUndefined : is undefined\n * isDefined   : is defined\n * isTruthy    : is truthy\n * isFalsy     : is falsy\n * isJson      : is json\n * isNumber    : is number\n * isString    : is string\n * isBoolean   : is boolean\n * isArray     : is array\n */\n\nconst AssertionOperator = ({ operator, onChange }) => {\n  const operators = [\n    'eq',\n    'neq',\n    'gt',\n    'gte',\n    'lt',\n    'lte',\n    'in',\n    'notIn',\n    'contains',\n    'notContains',\n    'length',\n    'matches',\n    'notMatches',\n    'startsWith',\n    'endsWith',\n    'between',\n    'isEmpty',\n    'isNotEmpty',\n    'isNull',\n    'isUndefined',\n    'isDefined',\n    'isTruthy',\n    'isFalsy',\n    'isJson',\n    'isNumber',\n    'isString',\n    'isBoolean',\n    'isArray'\n  ];\n\n  const handleChange = (e) => {\n    onChange(e.target.value);\n  };\n\n  const getLabel = (operator) => {\n    switch (operator) {\n      case 'eq':\n        return 'equals';\n      case 'neq':\n        return 'notEquals';\n      default:\n        return operator;\n    }\n  };\n\n  return (\n    <select value={operator} onChange={handleChange} className=\"mousetrap\" data-testid=\"assertion-operator-select\">\n      {operators.map((operator) => (\n        <option key={operator} value={operator}>\n          {getLabel(operator)}\n        </option>\n      ))}\n    </select>\n  );\n};\n\nexport default AssertionOperator;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Assertions/AssertionRow/index.js",
    "content": "import React from 'react';\nimport { IconTrash } from '@tabler/icons';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport AssertionOperator from '../AssertionOperator';\nimport { useTheme } from 'providers/Theme';\n\n/**\n * Assertion operators\n *\n * eq          : equal to\n * neq         : not equal to\n * gt          : greater than\n * gte         : greater than or equal to\n * lt          : less than\n * lte         : less than or equal to\n * in          : in\n * notIn       : not in\n * contains    : contains\n * notContains : not contains\n * length      : length\n * matches     : matches\n * notMatches  : not matches\n * startsWith  : starts with\n * endsWith    : ends with\n * between     : between\n * isEmpty     : is empty\n * isNotEmpty  : is not empty\n * isNull      : is null\n * isUndefined : is undefined\n * isDefined   : is defined\n * isTruthy    : is truthy\n * isFalsy     : is falsy\n * isJson      : is json\n * isNumber    : is number\n * isString    : is string\n * isBoolean   : is boolean\n * isArray     : is array\n */\nconst parseAssertionOperator = (str = '') => {\n  if (!str || typeof str !== 'string' || !str.length) {\n    return {\n      operator: 'eq',\n      value: str\n    };\n  }\n\n  const operators = [\n    'eq',\n    'neq',\n    'gt',\n    'gte',\n    'lt',\n    'lte',\n    'in',\n    'notIn',\n    'contains',\n    'notContains',\n    'length',\n    'matches',\n    'notMatches',\n    'startsWith',\n    'endsWith',\n    'between',\n    'isEmpty',\n    'isNotEmpty',\n    'isNull',\n    'isUndefined',\n    'isDefined',\n    'isTruthy',\n    'isFalsy',\n    'isJson',\n    'isNumber',\n    'isString',\n    'isBoolean',\n    'isArray'\n  ];\n\n  const unaryOperators = [\n    'isEmpty',\n    'isNotEmpty',\n    'isNull',\n    'isUndefined',\n    'isDefined',\n    'isTruthy',\n    'isFalsy',\n    'isJson',\n    'isNumber',\n    'isString',\n    'isBoolean',\n    'isArray'\n  ];\n\n  const [operator, ...rest] = str.split(' ');\n  const value = rest.join(' ');\n\n  if (unaryOperators.includes(operator)) {\n    return {\n      operator,\n      value: ''\n    };\n  }\n\n  if (operators.includes(operator)) {\n    return {\n      operator,\n      value\n    };\n  }\n\n  return {\n    operator: 'eq',\n    value: str\n  };\n};\n\nconst isUnaryOperator = (operator) => {\n  const unaryOperators = [\n    'isEmpty',\n    'isNotEmpty',\n    'isNull',\n    'isUndefined',\n    'isDefined',\n    'isTruthy',\n    'isFalsy',\n    'isJson',\n    'isNumber',\n    'isString',\n    'isBoolean',\n    'isArray'\n  ];\n\n  return unaryOperators.includes(operator);\n};\n\nconst AssertionRow = ({\n  item,\n  collection,\n  assertion,\n  handleAssertionChange,\n  handleRemoveAssertion,\n  onSave,\n  handleRun\n}) => {\n  const { storedTheme } = useTheme();\n\n  const { operator, value } = parseAssertionOperator(assertion.value);\n\n  return (\n    <>\n\n      <td>\n        <AssertionOperator\n          operator={operator}\n          onChange={(op) =>\n            handleAssertionChange(\n              {\n                target: {\n                  value: isUnaryOperator(op) ? op : `${op} ${value}`\n                }\n              },\n              assertion,\n              'value'\n            )}\n        />\n      </td>\n      <td>\n        {!isUnaryOperator(operator) ? (\n          <SingleLineEditor\n            value={value}\n            theme={storedTheme}\n            onSave={onSave}\n            onChange={(newValue) => {\n              handleAssertionChange(\n                {\n                  target: {\n                    value: `${operator} ${newValue}`\n                  }\n                },\n                assertion,\n                'value'\n              );\n            }}\n            onRun={handleRun}\n            collection={collection}\n            item={item}\n          />\n        ) : (\n          <input type=\"text\" className=\"cursor-default\" disabled />\n        )}\n      </td>\n      <td>\n        <div className=\"flex items-center\">\n          <input\n            type=\"checkbox\"\n            checked={assertion.enabled}\n            tabIndex=\"-1\"\n            className=\"mr-3 mousetrap\"\n            onChange={(e) => handleAssertionChange(e, assertion, 'enabled')}\n          />\n          <button tabIndex=\"-1\" onClick={() => handleRemoveAssertion(assertion)}>\n            <IconTrash strokeWidth={1.5} size={20} />\n          </button>\n        </div>\n      </td>\n    </>\n  );\n};\n\nexport default AssertionRow;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Assertions/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  table {\n    width: 100%;\n    border-collapse: collapse;\n    font-weight: 500;\n    table-layout: fixed;\n\n    thead,\n    td {\n      border: 1px solid ${(props) => props.theme.table.border};\n    }\n\n    thead {\n      color: ${(props) => props.theme.table.thead.color};\n      font-size: ${(props) => props.theme.font.size.base};\n      user-select: none;\n    }\n    td {\n      padding: 6px 10px;\n      }\n\n    select {\n        background-color: transparent;\n      }\n    }\n\n  .btn-add-assertion {\n    font-size: ${(props) => props.theme.font.size.base};\n  }\n\n  input[type='text'] {\n    width: 100%;\n    border: solid 1px transparent;\n    outline: none !important;\n    color: ${(props) => props.theme.table.input.color};\n    background: transparent;\n\n    &:focus {\n      outline: none !important;\n      border: solid 1px transparent;\n    }\n  }\n\n  input[type='checkbox'] {\n    cursor: pointer;\n    position: relative;\n    top: 1px;\n  }\n  option {\n    background-color: ${(props) => props.theme.bg};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Assertions/index.js",
    "content": "import React, { useCallback } from 'react';\nimport get from 'lodash/get';\nimport { useDispatch } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport { moveAssertion, setRequestAssertions } from 'providers/ReduxStore/slices/collections';\nimport { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport AssertionOperator from './AssertionOperator';\nimport EditableTable from 'components/EditableTable';\nimport StyledWrapper from './StyledWrapper';\n\nconst unaryOperators = [\n  'isEmpty',\n  'isNotEmpty',\n  'isNull',\n  'isUndefined',\n  'isDefined',\n  'isTruthy',\n  'isFalsy',\n  'isJson',\n  'isNumber',\n  'isString',\n  'isBoolean',\n  'isArray'\n];\n\nconst parseAssertionOperator = (str = '') => {\n  if (!str || typeof str !== 'string' || !str.length) {\n    return { operator: 'eq', value: str };\n  }\n\n  const operators = [\n    'eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'in', 'notIn',\n    'contains', 'notContains', 'length', 'matches', 'notMatches',\n    'startsWith', 'endsWith', 'between', ...unaryOperators\n  ];\n\n  const [operator, ...rest] = str.split(' ');\n  const value = rest.join(' ');\n\n  if (unaryOperators.includes(operator)) {\n    return { operator, value: '' };\n  }\n\n  if (operators.includes(operator)) {\n    return { operator, value };\n  }\n\n  return { operator: 'eq', value: str };\n};\n\nconst isUnaryOperator = (operator) => unaryOperators.includes(operator);\n\nconst Assertions = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n  const assertions = item.draft ? get(item, 'draft.request.assertions') : get(item, 'request.assertions');\n\n  const onSave = () => dispatch(saveRequest(item.uid, collection.uid));\n  const handleRun = () => dispatch(sendRequest(item, collection.uid));\n\n  const handleAssertionsChange = useCallback((updatedAssertions) => {\n    dispatch(setRequestAssertions({\n      collectionUid: collection.uid,\n      itemUid: item.uid,\n      assertions: updatedAssertions\n    }));\n  }, [dispatch, collection.uid, item.uid]);\n\n  const handleAssertionDrag = useCallback(({ updateReorderedItem }) => {\n    dispatch(moveAssertion({\n      collectionUid: collection.uid,\n      itemUid: item.uid,\n      updateReorderedItem\n    }));\n  }, [dispatch, collection.uid, item.uid]);\n\n  const columns = [\n    {\n      key: 'name',\n      name: 'Expr',\n      isKeyField: true,\n      placeholder: 'Expr',\n      width: '30%'\n    },\n    {\n      key: 'operator',\n      name: 'Operator',\n      width: '120px',\n      getValue: (row) => parseAssertionOperator(row.value).operator,\n      render: ({ row, rowIndex, isLastEmptyRow }) => {\n        const { operator } = parseAssertionOperator(row.value);\n        const assertionValue = parseAssertionOperator(row.value).value;\n\n        const handleOperatorChange = (newOperator) => {\n          const currentAssertions = assertions || [];\n          const existingAssertion = currentAssertions.find((a) => a.uid === row.uid);\n          const newValue = isUnaryOperator(newOperator) ? newOperator : `${newOperator} ${assertionValue}`;\n\n          if (existingAssertion) {\n            const updatedAssertions = currentAssertions.map((assertion) => {\n              if (assertion.uid === row.uid) {\n                return {\n                  ...assertion,\n                  value: newValue\n                };\n              }\n              return assertion;\n            });\n            handleAssertionsChange(updatedAssertions);\n          } else {\n            handleAssertionsChange([...currentAssertions, { ...row, value: newValue }]);\n          }\n        };\n\n        return (\n          <AssertionOperator\n            operator={operator}\n            onChange={handleOperatorChange}\n          />\n        );\n      }\n    },\n    {\n      key: 'value',\n      name: 'Value',\n      width: '30%',\n      render: ({ row, value, onChange }) => {\n        const { operator, value: assertionValue } = parseAssertionOperator(value);\n\n        if (isUnaryOperator(operator)) {\n          return <input type=\"text\" className=\"cursor-default\" disabled />;\n        }\n\n        return (\n          <SingleLineEditor\n            value={assertionValue}\n            theme={storedTheme}\n            onSave={onSave}\n            onChange={(newValue) => onChange(`${operator} ${newValue}`)}\n            onRun={handleRun}\n            collection={collection}\n            item={item}\n            placeholder={!value ? 'Value' : ''}\n          />\n        );\n      }\n    }\n  ];\n\n  const defaultRow = {\n    name: '',\n    value: 'eq ',\n    operator: 'eq'\n  };\n\n  return (\n    <StyledWrapper className=\"w-full\">\n      <EditableTable\n        columns={columns}\n        rows={assertions || []}\n        onChange={handleAssertionsChange}\n        defaultRow={defaultRow}\n        reorderable={true}\n        onReorder={handleAssertionDrag}\n        testId=\"assertions-table\"\n      />\n    </StyledWrapper>\n  );\n};\n\nexport default Assertions;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  label {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.subtext1};\n  }\n\n  .single-line-editor-wrapper {\n    max-width: 400px;\n    padding: 0.15rem 0.4rem;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n  }\n\n  .auth-placement-selector {\n    font-size: ${(props) => props.theme.font.size.sm};\n    padding: 0.2rem 0px;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n\n    .dropdown {\n      width: fit-content;\n\n      div[data-tippy-root] {\n        width: fit-content;\n      }\n      .tippy-box {\n        width: fit-content;\n        max-width: none !important;\n\n        .tippy-content: {\n          width: fit-content;\n          max-width: none !important;\n        }\n      }\n    }\n\n    .auth-type-label {\n      width: fit-content;\n      justify-content: space-between;\n      padding: 0 0.5rem;\n    }\n\n    .dropdown-item {\n      padding: 0.2rem 0.6rem !important;\n    }\n  }\n\n  .caret {\n    color: rgb(140, 140, 140);\n    fill: rgb(140 140 140);\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/ApiKeyAuth/index.js",
    "content": "import React, { useRef, forwardRef, useEffect } from 'react';\nimport { useDispatch } from 'react-redux';\nimport get from 'lodash/get';\nimport { IconCaretDown } from '@tabler/icons';\nimport Dropdown from 'components/Dropdown';\nimport { useTheme } from 'providers/Theme';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport { sendRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport StyledWrapper from './StyledWrapper';\nimport { humanizeRequestAPIKeyPlacement } from 'utils/collections';\n\nconst ApiKeyAuth = ({ item, collection, updateAuth, request, save }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n  const dropdownTippyRef = useRef();\n  const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);\n\n  const apikeyAuth = get(request, 'auth.apikey', {});\n\n  const handleRun = () => dispatch(sendRequest(item, collection.uid));\n\n  const handleSave = () => {\n    save();\n  };\n\n  const Icon = forwardRef((props, ref) => {\n    return (\n      <div ref={ref} className=\"flex items-center justify-end auth-type-label select-none\">\n        {humanizeRequestAPIKeyPlacement(apikeyAuth?.placement)}\n        <IconCaretDown className=\"caret ml-1 mr-1\" size={14} strokeWidth={2} />\n      </div>\n    );\n  });\n\n  const handleAuthChange = (property, value) => {\n    dispatch(\n      updateAuth({\n        mode: 'apikey',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          ...apikeyAuth,\n          [property]: value\n        }\n      })\n    );\n  };\n\n  useEffect(() => {\n    !apikeyAuth?.placement\n    && dispatch(\n      updateAuth({\n        mode: 'apikey',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          placement: 'header'\n        }\n      })\n    );\n  }, [apikeyAuth]);\n\n  return (\n    <StyledWrapper className=\"w-full\">\n      <label className=\"block mb-1\">Key</label>\n      <div className=\"single-line-editor-wrapper mb-3\">\n        <SingleLineEditor\n          value={apikeyAuth.key || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleAuthChange('key', val)}\n          onRun={handleRun}\n          collection={collection}\n          isCompact\n        />\n      </div>\n\n      <label className=\"block mb-1\">Value</label>\n      <div className=\"single-line-editor-wrapper mb-3\">\n        <SingleLineEditor\n          value={apikeyAuth.value || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleAuthChange('value', val)}\n          onRun={handleRun}\n          collection={collection}\n          isCompact\n        />\n      </div>\n\n      <label className=\"block mb-1\">Add To</label>\n      <div className=\"inline-flex items-center cursor-pointer auth-placement-selector w-fit\">\n        <Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement=\"bottom-end\">\n          <div\n            className=\"dropdown-item\"\n            onClick={() => {\n              dropdownTippyRef?.current?.hide();\n              handleAuthChange('placement', 'header');\n            }}\n          >\n            Header\n          </div>\n          <div\n            className=\"dropdown-item\"\n            onClick={() => {\n              dropdownTippyRef?.current?.hide();\n              handleAuthChange('placement', 'queryparams');\n            }}\n          >\n            Query Param\n          </div>\n        </Dropdown>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default ApiKeyAuth;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/AuthMode/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  font-size: ${(props) => props.theme.font.size.base};\n\n  .auth-mode-selector {\n    background: transparent;\n\n    .auth-mode-label {\n      color: ${(props) => props.theme.primary.text};\n\n      .caret {\n        color: rgb(140, 140, 140);\n        fill: rgb(140, 140, 140);\n      }\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js",
    "content": "import React, { useMemo, useCallback } from 'react';\nimport get from 'lodash/get';\nimport { IconCaretDown } from '@tabler/icons';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport { useDispatch } from 'react-redux';\nimport { updateRequestAuthMode } from 'providers/ReduxStore/slices/collections';\nimport { humanizeRequestAuthMode } from 'utils/collections';\nimport StyledWrapper from './StyledWrapper';\n\nconst AuthMode = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');\n\n  const onModeChange = useCallback((value) => {\n    dispatch(\n      updateRequestAuthMode({\n        itemUid: item.uid,\n        collectionUid: collection.uid,\n        mode: value\n      })\n    );\n  }, [dispatch, item.uid, collection.uid]);\n\n  const menuItems = useMemo(() => [\n    {\n      id: 'awsv4',\n      label: 'AWS Sig v4',\n      onClick: () => onModeChange('awsv4')\n    },\n    {\n      id: 'basic',\n      label: 'Basic Auth',\n      onClick: () => onModeChange('basic')\n    },\n    {\n      id: 'bearer',\n      label: 'Bearer Token',\n      onClick: () => onModeChange('bearer')\n    },\n    {\n      id: 'digest',\n      label: 'Digest Auth',\n      onClick: () => onModeChange('digest')\n    },\n    {\n      id: 'ntlm',\n      label: 'NTLM Auth',\n      onClick: () => onModeChange('ntlm')\n    },\n    {\n      id: 'oauth2',\n      label: 'OAuth 2.0',\n      onClick: () => onModeChange('oauth2')\n    },\n    {\n      id: 'wsse',\n      label: 'WSSE Auth',\n      onClick: () => onModeChange('wsse')\n    },\n    {\n      id: 'apikey',\n      label: 'API Key',\n      onClick: () => onModeChange('apikey')\n    },\n    {\n      id: 'inherit',\n      label: 'Inherit',\n      onClick: () => onModeChange('inherit')\n    },\n    {\n      id: 'none',\n      label: 'No Auth',\n      onClick: () => onModeChange('none')\n    }\n  ], [onModeChange]);\n\n  return (\n    <StyledWrapper>\n      <div className=\"inline-flex items-center cursor-pointer auth-mode-selector\">\n        <MenuDropdown\n          items={menuItems}\n          placement=\"bottom-end\"\n          selectedItemId={authMode}\n          showTickMark={true}\n        >\n          <div className=\"flex items-center justify-center auth-mode-label select-none\">\n            {humanizeRequestAuthMode(authMode)} <IconCaretDown className=\"caret ml-1\" size={14} strokeWidth={2} />\n          </div>\n        </MenuDropdown>\n      </div>\n    </StyledWrapper>\n  );\n};\nexport default AuthMode;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/AwsV4Auth/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  label {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.subtext1};\n  }\n\n  .single-line-editor-wrapper {\n    max-width: 400px;\n    padding: 0.15rem 0.4rem;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/AwsV4Auth/index.js",
    "content": "import React, { useState } from 'react';\nimport get from 'lodash/get';\nimport { useTheme } from 'providers/Theme';\nimport { useDispatch } from 'react-redux';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport { updateAuth } from 'providers/ReduxStore/slices/collections';\nimport { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport StyledWrapper from './StyledWrapper';\nimport SensitiveFieldWarning from 'components/SensitiveFieldWarning';\nimport { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';\n\nconst AwsV4Auth = ({ item, collection, updateAuth, request, save }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n\n  const awsv4Auth = get(request, 'auth.awsv4', {});\n  const { isSensitive } = useDetectSensitiveField(collection);\n  const { showWarning, warningMessage } = isSensitive(awsv4Auth?.secretAccessKey);\n\n  const handleRun = () => dispatch(sendRequest(item, collection.uid));\n\n  const handleSave = () => {\n    save();\n  };\n\n  const handleAccessKeyIdChange = (accessKeyId) => {\n    dispatch(\n      updateAuth({\n        mode: 'awsv4',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          accessKeyId: accessKeyId,\n          secretAccessKey: awsv4Auth.secretAccessKey || '',\n          sessionToken: awsv4Auth.sessionToken || '',\n          service: awsv4Auth.service || '',\n          region: awsv4Auth.region || '',\n          profileName: awsv4Auth.profileName || ''\n        }\n      })\n    );\n  };\n\n  const handleSecretAccessKeyChange = (secretAccessKey) => {\n    dispatch(\n      updateAuth({\n        mode: 'awsv4',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          accessKeyId: awsv4Auth.accessKeyId || '',\n          secretAccessKey: secretAccessKey || '',\n          sessionToken: awsv4Auth.sessionToken || '',\n          service: awsv4Auth.service || '',\n          region: awsv4Auth.region || '',\n          profileName: awsv4Auth.profileName || ''\n        }\n      })\n    );\n  };\n\n  const handleSessionTokenChange = (sessionToken) => {\n    dispatch(\n      updateAuth({\n        mode: 'awsv4',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          accessKeyId: awsv4Auth.accessKeyId || '',\n          secretAccessKey: awsv4Auth.secretAccessKey || '',\n          sessionToken: sessionToken || '',\n          service: awsv4Auth.service || '',\n          region: awsv4Auth.region || '',\n          profileName: awsv4Auth.profileName || ''\n        }\n      })\n    );\n  };\n\n  const handleServiceChange = (service) => {\n    dispatch(\n      updateAuth({\n        mode: 'awsv4',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          accessKeyId: awsv4Auth.accessKeyId || '',\n          secretAccessKey: awsv4Auth.secretAccessKey || '',\n          sessionToken: awsv4Auth.sessionToken || '',\n          service: service || '',\n          region: awsv4Auth.region || '',\n          profileName: awsv4Auth.profileName || ''\n        }\n      })\n    );\n  };\n\n  const handleRegionChange = (region) => {\n    dispatch(\n      updateAuth({\n        mode: 'awsv4',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          accessKeyId: awsv4Auth.accessKeyId || '',\n          secretAccessKey: awsv4Auth.secretAccessKey || '',\n          sessionToken: awsv4Auth.sessionToken || '',\n          service: awsv4Auth.service || '',\n          region: region || '',\n          profileName: awsv4Auth.profileName || ''\n        }\n      })\n    );\n  };\n\n  const handleProfileNameChange = (profileName) => {\n    dispatch(\n      updateAuth({\n        mode: 'awsv4',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          accessKeyId: awsv4Auth.accessKeyId || '',\n          secretAccessKey: awsv4Auth.secretAccessKey || '',\n          sessionToken: awsv4Auth.sessionToken || '',\n          service: awsv4Auth.service || '',\n          region: awsv4Auth.region || '',\n          profileName: profileName || ''\n        }\n      })\n    );\n  };\n\n  return (\n    <StyledWrapper className=\"mt-2 w-full\">\n      <label className=\"block mb-1\">Access Key ID</label>\n      <div className=\"single-line-editor-wrapper mb-3\">\n        <SingleLineEditor\n          value={awsv4Auth.accessKeyId || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleAccessKeyIdChange(val)}\n          onRun={handleRun}\n          collection={collection}\n          item={item}\n          isCompact\n        />\n      </div>\n\n      <label className=\"block mb-1\">Secret Access Key</label>\n      <div className=\"single-line-editor-wrapper mb-3 flex items-center\">\n        <SingleLineEditor\n          value={awsv4Auth.secretAccessKey || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleSecretAccessKeyChange(val)}\n          onRun={handleRun}\n          collection={collection}\n          item={item}\n          isSecret={true}\n          isCompact\n        />\n\n        {showWarning && <SensitiveFieldWarning fieldName=\"awsv4-secret-access-key\" warningMessage={warningMessage} />}\n      </div>\n\n      <label className=\"block mb-1\">Session Token</label>\n      <div className=\"single-line-editor-wrapper mb-3\">\n        <SingleLineEditor\n          value={awsv4Auth.sessionToken || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleSessionTokenChange(val)}\n          onRun={handleRun}\n          collection={collection}\n          item={item}\n          isCompact\n        />\n      </div>\n\n      <label className=\"block mb-1\">Service</label>\n      <div className=\"single-line-editor-wrapper mb-3\">\n        <SingleLineEditor\n          value={awsv4Auth.service || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleServiceChange(val)}\n          onRun={handleRun}\n          collection={collection}\n          item={item}\n          isCompact\n        />\n      </div>\n\n      <label className=\"block mb-1\">Region</label>\n      <div className=\"single-line-editor-wrapper mb-3\">\n        <SingleLineEditor\n          value={awsv4Auth.region || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleRegionChange(val)}\n          onRun={handleRun}\n          collection={collection}\n          item={item}\n          isCompact\n        />\n      </div>\n\n      <label className=\"block mb-1\">Profile Name</label>\n      <div className=\"single-line-editor-wrapper\">\n        <SingleLineEditor\n          value={awsv4Auth.profileName || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleProfileNameChange(val)}\n          onRun={handleRun}\n          collection={collection}\n          item={item}\n          isCompact\n        />\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default AwsV4Auth;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  label {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.subtext1};\n  }\n\n  .single-line-editor-wrapper {\n    max-width: 400px;\n    padding: 0.15rem 0.4rem;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/BasicAuth/index.js",
    "content": "import React from 'react';\nimport SensitiveFieldWarning from 'components/SensitiveFieldWarning';\nimport { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';\nimport get from 'lodash/get';\nimport { useTheme } from 'providers/Theme';\nimport { useDispatch } from 'react-redux';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport { updateAuth } from 'providers/ReduxStore/slices/collections';\nimport { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport StyledWrapper from './StyledWrapper';\n\nconst BasicAuth = ({ item, collection, updateAuth, request, save }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n\n  const basicAuth = get(request, 'auth.basic', {});\n  const { isSensitive } = useDetectSensitiveField(collection);\n  const { showWarning, warningMessage } = isSensitive(basicAuth?.password);\n\n  const handleRun = () => dispatch(sendRequest(item, collection.uid));\n\n  const handleSave = () => {\n    save();\n  };\n\n  const handleUsernameChange = (username) => {\n    dispatch(\n      updateAuth({\n        mode: 'basic',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          username: username || '',\n          password: basicAuth.password || ''\n        }\n      })\n    );\n  };\n\n  const handlePasswordChange = (password) => {\n    dispatch(\n      updateAuth({\n        mode: 'basic',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          username: basicAuth.username || '',\n          password: password || ''\n        }\n      })\n    );\n  };\n\n  return (\n    <StyledWrapper className=\"w-full\">\n      <label className=\"block mb-1\">Username</label>\n      <div className=\"single-line-editor-wrapper mb-3\">\n        <SingleLineEditor\n          value={basicAuth.username || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleUsernameChange(val)}\n          onRun={handleRun}\n          collection={collection}\n          item={item}\n          isCompact\n        />\n      </div>\n\n      <label className=\"block mb-1\">Password</label>\n      <div className=\"single-line-editor-wrapper flex items-center\">\n        <SingleLineEditor\n          value={basicAuth.password || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handlePasswordChange(val)}\n          onRun={handleRun}\n          collection={collection}\n          item={item}\n          isSecret={true}\n          isCompact\n        />\n        {showWarning && <SensitiveFieldWarning fieldName=\"basic-password\" warningMessage={warningMessage} />}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default BasicAuth;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  label {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.subtext1};\n  }\n\n  .single-line-editor-wrapper {\n    max-width: 400px;\n    padding: 0.15rem 0.4rem;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/BearerAuth/index.js",
    "content": "import React from 'react';\nimport SensitiveFieldWarning from 'components/SensitiveFieldWarning';\nimport { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';\nimport get from 'lodash/get';\nimport { useTheme } from 'providers/Theme';\nimport { useDispatch } from 'react-redux';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport { updateAuth } from 'providers/ReduxStore/slices/collections';\nimport { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport StyledWrapper from './StyledWrapper';\n\nconst BearerAuth = ({ item, collection, updateAuth, request, save }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n\n  // Use the request prop directly like OAuth2ClientCredentials does\n  const bearerToken = get(request, 'auth.bearer.token', '');\n  const { isSensitive } = useDetectSensitiveField(collection);\n  const { showWarning, warningMessage } = isSensitive(bearerToken);\n\n  const handleRun = () => dispatch(sendRequest(item, collection.uid));\n\n  const handleSave = () => {\n    save();\n  };\n\n  const handleTokenChange = (token) => {\n    dispatch(\n      updateAuth({\n        mode: 'bearer',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          token: token\n        }\n      })\n    );\n  };\n\n  return (\n    <StyledWrapper className=\"w-full\">\n      <label className=\"block mb-1\">Token</label>\n      <div className=\"single-line-editor-wrapper flex items-center\">\n        <SingleLineEditor\n          value={bearerToken}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleTokenChange(val)}\n          onRun={handleRun}\n          collection={collection}\n          item={item}\n          isSecret={true}\n          isCompact\n        />\n        {showWarning && <SensitiveFieldWarning fieldName=\"bearer-token\" warningMessage={warningMessage} />}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default BearerAuth;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  label {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.subtext1};\n  }\n\n  .single-line-editor-wrapper {\n    max-width: 400px;\n    padding: 0.15rem 0.4rem;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/DigestAuth/index.js",
    "content": "import React from 'react';\nimport SensitiveFieldWarning from 'components/SensitiveFieldWarning';\nimport { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';\nimport get from 'lodash/get';\nimport { useTheme } from 'providers/Theme';\nimport { useDispatch } from 'react-redux';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport StyledWrapper from './StyledWrapper';\n\nconst DigestAuth = ({ item, collection, updateAuth, request, save }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n\n  const digestAuth = get(request, 'auth.digest', {});\n  const { isSensitive } = useDetectSensitiveField(collection);\n  const { showWarning, warningMessage } = isSensitive(digestAuth?.password);\n\n  const handleRun = () => dispatch(sendRequest(item, collection.uid));\n\n  const handleSave = () => {\n    save();\n  };\n\n  const handleUsernameChange = (username) => {\n    dispatch(\n      updateAuth({\n        mode: 'digest',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          username: username || '',\n          password: digestAuth.password || ''\n        }\n      })\n    );\n  };\n\n  const handlePasswordChange = (password) => {\n    dispatch(\n      updateAuth({\n        mode: 'digest',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          username: digestAuth.username || '',\n          password: password || ''\n        }\n      })\n    );\n  };\n\n  return (\n    <StyledWrapper className=\"mt-2 w-full\">\n      <label className=\"block mb-1\">Username</label>\n      <div className=\"single-line-editor-wrapper mb-3\">\n        <SingleLineEditor\n          value={digestAuth.username || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleUsernameChange(val)}\n          onRun={handleRun}\n          collection={collection}\n          item={item}\n          isCompact\n        />\n      </div>\n\n      <label className=\"block mb-1\">Password</label>\n      <div className=\"single-line-editor-wrapper flex items-center\">\n        <SingleLineEditor\n          value={digestAuth.password || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handlePasswordChange(val)}\n          onRun={handleRun}\n          collection={collection}\n          item={item}\n          isSecret={true}\n          isCompact\n        />\n        {showWarning && <SensitiveFieldWarning fieldName=\"digest-password\" warningMessage={warningMessage} />}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default DigestAuth;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/NTLMAuth/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  label {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.subtext1};\n  }\n\n  .single-line-editor-wrapper {\n    max-width: 400px;\n    padding: 0.15rem 0.4rem;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/NTLMAuth/index.js",
    "content": "import React from 'react';\nimport SensitiveFieldWarning from 'components/SensitiveFieldWarning';\nimport { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';\nimport get from 'lodash/get';\nimport { useTheme } from 'providers/Theme';\nimport { useDispatch } from 'react-redux';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport { updateAuth } from 'providers/ReduxStore/slices/collections';\nimport { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport StyledWrapper from './StyledWrapper';\n\nconst NTLMAuth = ({ item, collection, request, save, updateAuth }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n\n  const ntlmAuth = get(request, 'auth.ntlm', {});\n  const { isSensitive } = useDetectSensitiveField(collection);\n  const { showWarning, warningMessage } = isSensitive(ntlmAuth?.password);\n\n  const handleRun = () => dispatch(sendRequest(item, collection.uid));\n\n  const handleSave = () => {\n    save();\n  };\n\n  const handleUsernameChange = (username) => {\n    dispatch(\n      updateAuth({\n        mode: 'ntlm',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          username: username || '',\n          password: ntlmAuth.password || '',\n          domain: ntlmAuth.domain || ''\n        }\n      })\n    );\n  };\n\n  const handlePasswordChange = (password) => {\n    dispatch(\n      updateAuth({\n        mode: 'ntlm',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          username: ntlmAuth.username || '',\n          password: password || '',\n          domain: ntlmAuth.domain || ''\n        }\n      })\n    );\n  };\n\n  const handleDomainChange = (domain) => {\n    dispatch(\n      updateAuth({\n        mode: 'ntlm',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          username: ntlmAuth.username || '',\n          password: ntlmAuth.password || '',\n          domain: domain || ''\n        }\n      })\n    );\n  };\n\n  return (\n    <StyledWrapper className=\"mt-2 w-full\">\n      <label className=\"block mb-1\">Username</label>\n      <div className=\"single-line-editor-wrapper mb-3\">\n        <SingleLineEditor\n          value={ntlmAuth.username || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleUsernameChange(val)}\n          onRun={handleRun}\n          collection={collection}\n          item={item}\n          isCompact\n        />\n      </div>\n\n      <label className=\"block mb-1\">Password</label>\n      <div className=\"single-line-editor-wrapper mb-3 flex items-center\">\n        <SingleLineEditor\n          value={ntlmAuth.password || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handlePasswordChange(val)}\n          onRun={handleRun}\n          collection={collection}\n          item={item}\n          isSecret={true}\n          isCompact\n        />\n        {showWarning && <SensitiveFieldWarning fieldName=\"ntlm-password\" warningMessage={warningMessage} />}\n      </div>\n\n      <label className=\"block mb-1\">Domain</label>\n      <div className=\"single-line-editor-wrapper\">\n        <SingleLineEditor\n          value={ntlmAuth.domain || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleDomainChange(val)}\n          onRun={handleRun}\n          collection={collection}\n          item={item}\n          isCompact\n        />\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default NTLMAuth;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AdditionalParams/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst StyledWrapper = styled.div`\n  .oauth2-icon-container {\n    background-color: ${(props) => rgba(props.theme.primary.solid, 0.1)};\n  }\n\n  .oauth2-icon {\n    color: ${(props) => props.theme.primary.solid};\n  }\n\n  &.oauth2-additional-params-wrapper div.tabs {\n    div.tab {\n      cursor: pointer;\n      padding: 4px 8px !important;\n      font-size: ${(props) => props.theme.font.size.sm};\n      border-radius: 4px;\n      border: none !important;\n      border-bottom: none !important;\n      margin-right: 0;\n\n      &:hover {\n        background-color: ${(props) => rgba(props.theme.primary.solid, 0.1)};\n      }\n\n      &.active {\n        background-color: ${(props) => {\n          return props.theme.mode === 'dark'\n            ? rgba(props.theme.primary.solid, 0.2)\n            : rgba(props.theme.primary.solid, 0.1);\n        }};\n        color: ${(props) => props.theme.primary.text} !important;\n        border-bottom: none !important;\n        font-weight: normal !important;\n      }\n    }\n  }\n\n  table {\n    width: 100%;\n    border-collapse: collapse;\n    font-weight: 500;\n    table-layout: fixed;\n\n    thead,\n    td {\n      border: 1px solid ${(props) => props.theme.table.border};\n    }\n\n    thead {\n      color: ${(props) => props.theme.table.thead.color};\n      font-size: ${(props) => props.theme.font.size.base};\n      user-select: none;\n    }\n    td {\n      padding: 6px 10px;\n    }\n  }\n\n  .additional-parameter-sends-in-selector {\n    select {\n      height: 32px;\n      width: 100%;\n      border: 1px solid ${(props) => props.theme.input.border};\n      border-radius: 4px;\n      padding: 0 8px;\n\n      &:focus {\n        outline: none;\n        border-color: ${(props) => props.theme.primary.solid};\n      }\n    }\n  }\n\n  .add-additional-param-actions {\n    &:hover {\n      color: ${(props) => props.theme.primary.solid};\n    }\n  }\n\n  input[type='checkbox'] {\n    cursor: pointer;\n    accent-color: ${(props) => props.theme.primary.solid};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AdditionalParams/index.js",
    "content": "import { useDispatch } from 'react-redux';\nimport React, { useState } from 'react';\nimport get from 'lodash/get';\nimport { useTheme } from 'providers/Theme';\nimport { IconPlus, IconTrash, IconAdjustmentsHorizontal } from '@tabler/icons';\nimport { cloneDeep } from 'lodash';\nimport SingleLineEditor from 'components/SingleLineEditor/index';\nimport MultiLineEditor from 'components/MultiLineEditor/index';\nimport StyledWrapper from './StyledWrapper';\nimport Table from 'components/Table/index';\n\nconst AdditionalParams = ({ item = {}, request, updateAuth, collection, handleSave }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n\n  const oAuth = get(request, 'auth.oauth2', {});\n  const {\n    grantType,\n    additionalParameters = {}\n  } = oAuth;\n\n  const [activeTab, setActiveTab] = useState(\n    (grantType == 'authorization_code' || grantType == 'implicit') ? 'authorization' : 'token'\n  );\n\n  const isEmptyParam = (param) => {\n    return !param.name.trim() && !param.value.trim();\n  };\n\n  const hasEmptyRow = () => {\n    const tabParams = additionalParameters[activeTab] || [];\n    return tabParams.some(isEmptyParam);\n  };\n\n  const updateAdditionalParameters = ({ updatedAdditionalParameters }) => {\n    const filteredParams = cloneDeep(updatedAdditionalParameters);\n\n    Object.keys(filteredParams).forEach((paramType) => {\n      if (filteredParams[paramType]?.length) {\n        filteredParams[paramType] = filteredParams[paramType].filter((param) =>\n          param.name.trim() || param.value.trim()\n        );\n\n        if (filteredParams[paramType].length === 0) {\n          delete filteredParams[paramType];\n        }\n      } else if (Array.isArray(filteredParams[paramType]) && filteredParams[paramType].length === 0) {\n        // Remove empty arrays\n        delete filteredParams[paramType];\n      }\n    });\n\n    dispatch(\n      updateAuth({\n        mode: 'oauth2',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          ...oAuth,\n          additionalParameters: Object.keys(filteredParams).length > 0 ? filteredParams : undefined\n        }\n      })\n    );\n  };\n\n  const handleUpdateAdditionalParam = ({ paramType, key, paramIndex, value }) => {\n    const updatedAdditionalParameters = cloneDeep(additionalParameters);\n\n    if (!updatedAdditionalParameters[paramType]) {\n      updatedAdditionalParameters[paramType] = [];\n    }\n\n    if (!updatedAdditionalParameters[paramType][paramIndex]) {\n      updatedAdditionalParameters[paramType][paramIndex] = {\n        name: '',\n        value: '',\n        sendIn: 'headers',\n        enabled: true\n      };\n    }\n\n    updatedAdditionalParameters[paramType][paramIndex][key] = value;\n\n    // Only filter when updating a parameter\n    updateAdditionalParameters({ updatedAdditionalParameters });\n  };\n\n  const handleDeleteAdditionalParam = ({ paramType, paramIndex }) => {\n    const updatedAdditionalParameters = cloneDeep(additionalParameters);\n\n    if (updatedAdditionalParameters[paramType]?.length) {\n      updatedAdditionalParameters[paramType] = updatedAdditionalParameters[paramType].filter((_, index) => index !== paramIndex);\n\n      // If the array is now empty, ensure we're not sending empty arrays\n      if (updatedAdditionalParameters[paramType].length === 0) {\n        delete updatedAdditionalParameters[paramType];\n      }\n    }\n\n    updateAdditionalParameters({ updatedAdditionalParameters });\n  };\n\n  const handleAddNewAdditionalParam = () => {\n    // Prevent adding multiple empty rows\n    if (hasEmptyRow()) {\n      return;\n    }\n\n    const paramType = activeTab;\n    const localAdditionalParameters = cloneDeep(additionalParameters);\n\n    if (!localAdditionalParameters[paramType]) {\n      localAdditionalParameters[paramType] = [];\n    }\n\n    localAdditionalParameters[paramType] = [\n      ...localAdditionalParameters[paramType],\n      {\n        name: '',\n        value: '',\n        sendIn: 'headers',\n        enabled: true\n      }\n    ];\n\n    // Don't filter here to allow the empty row to display in UI\n    // But don't permanently store it in state until it has values\n    dispatch(\n      updateAuth({\n        mode: 'oauth2',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          ...oAuth,\n          additionalParameters: localAdditionalParameters\n        }\n      })\n    );\n  };\n\n  // Add a class to the Add Parameter button if it's disabled\n  const addButtonDisabled = hasEmptyRow();\n\n  // Define available tabs for each grant type\n  const getAvailableTabs = (grantType) => {\n    const tabConfig = {\n      authorization_code: ['authorization', 'token', 'refresh'],\n      implicit: ['authorization'],\n      password: ['token', 'refresh'],\n      client_credentials: ['token', 'refresh']\n    };\n    return tabConfig[grantType] || ['token', 'refresh'];\n  };\n\n  const availableTabs = getAvailableTabs(grantType);\n\n  const renderTab = (tabKey, tabLabel) => (\n    <div\n      key={tabKey}\n      className={`tab ${activeTab === tabKey ? 'active' : ''}`}\n      onClick={() => setActiveTab(tabKey)}\n    >\n      {tabLabel}\n    </div>\n  );\n\n  return (\n    <StyledWrapper className=\"mt-4 oauth2-additional-params-wrapper\">\n      <div className=\"flex items-center gap-2.5 mb-3\">\n        <div className=\"flex items-center px-2.5 py-1.5 oauth2-icon-container rounded-md\">\n          <IconAdjustmentsHorizontal size={14} className=\"oauth2-icon\" />\n        </div>\n        <span className=\"oauth2-section-label\">\n          Additional Parameters\n        </span>\n      </div>\n\n      <div className=\"tabs flex w-full gap-2 my-2\">\n        {availableTabs.includes('authorization') && renderTab('authorization', 'Authorization')}\n        {availableTabs.includes('token') && renderTab('token', 'Token')}\n        {availableTabs.includes('refresh') && renderTab('refresh', 'Refresh')}\n      </div>\n      <Table\n        headers={[\n          { name: 'Key', accessor: 'name', width: '30%' },\n          { name: 'Value', accessor: 'value', width: '30%' },\n          { name: 'Send In', accessor: 'sendIn', width: '150px' },\n          { name: '', accessor: '', width: '15%' }\n        ]}\n      >\n        <tbody>\n          {(additionalParameters?.[activeTab] || []).map((param, index) => (\n            <tr key={index}>\n              <td className=\"flex relative\">\n                <SingleLineEditor\n                  value={param?.name || ''}\n                  theme={storedTheme}\n                  onChange={(value) => handleUpdateAdditionalParam({\n                    paramType: activeTab,\n                    key: 'name',\n                    paramIndex: index,\n                    value\n                  })}\n                  collection={collection}\n                  onSave={handleSave}\n                  isCompact\n                />\n              </td>\n              <td>\n                <MultiLineEditor\n                  value={param?.value || ''}\n                  theme={storedTheme}\n                  onChange={(value) => handleUpdateAdditionalParam({\n                    paramType: activeTab,\n                    key: 'value',\n                    paramIndex: index,\n                    value\n                  })}\n                  collection={collection}\n                  onSave={handleSave}\n                />\n              </td>\n              <td>\n                <div className=\"w-full additional-parameter-sends-in-selector\">\n                  <select\n                    value={param?.sendIn || 'headers'}\n                    onChange={(e) => {\n                      handleUpdateAdditionalParam({\n                        paramType: activeTab,\n                        key: 'sendIn',\n                        paramIndex: index,\n                        value: e.target.value\n                      });\n                    }}\n                    className=\"mousetrap bg-transparent\"\n                  >\n                    {sendInOptionsMap[grantType || 'authorization_code'][activeTab].map((optionValue) => (\n                      <option key={optionValue} value={optionValue}>\n                        {optionValue}\n                      </option>\n                    ))}\n                  </select>\n                </div>\n              </td>\n              <td>\n                <div className=\"flex items-center\">\n                  <input\n                    type=\"checkbox\"\n                    checked={param?.enabled ?? true}\n                    tabIndex=\"-1\"\n                    className=\"mr-3 mousetrap\"\n                    onChange={(e) => {\n                      handleUpdateAdditionalParam({\n                        paramType: activeTab,\n                        key: 'enabled',\n                        paramIndex: index,\n                        value: e.target.checked\n                      });\n                    }}\n                  />\n                  <button\n                    tabIndex=\"-1\"\n                    onClick={() => {\n                      handleDeleteAdditionalParam({\n                        paramType: activeTab,\n                        paramIndex: index\n                      });\n                    }}\n                  >\n                    <IconTrash strokeWidth={1.5} size={20} />\n                  </button>\n                </div>\n              </td>\n            </tr>\n          )\n          )}\n        </tbody>\n      </Table>\n      <div\n        className={`add-additional-param-actions w-fit flex items-center mt-2 ${addButtonDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}\n        onClick={addButtonDisabled ? null : handleAddNewAdditionalParam}\n      >\n        <IconPlus size={16} strokeWidth={1.5} style={{ marginLeft: '2px' }} />\n        <span className=\"ml-1 text-gray-500\">Add Parameter</span>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default AdditionalParams;\n\nconst sendInOptionsMap = {\n  authorization_code: {\n    authorization: ['headers', 'queryparams'],\n    token: ['headers', 'queryparams', 'body'],\n    refresh: ['headers', 'queryparams', 'body']\n  },\n  password: {\n    token: ['headers', 'queryparams', 'body'],\n    refresh: ['headers', 'queryparams', 'body']\n  },\n  client_credentials: {\n    token: ['headers', 'queryparams', 'body'],\n    refresh: ['headers', 'queryparams', 'body']\n  },\n  implicit: {\n    authorization: ['headers', 'queryparams']\n  }\n};\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst Wrapper = styled.div`\n  .oauth2-icon-container {\n    background-color: ${(props) => rgba(props.theme.primary.solid, 0.1)};\n  }\n\n  .oauth2-icon {\n    color: ${(props) => props.theme.primary.text};\n  }\n\n  label {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.subtext1};\n  }\n  .single-line-editor-wrapper {\n    max-width: 400px;\n    padding: 0.15rem 0.4rem;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n  }\n\n  .token-placement-selector {\n    font-size: ${(props) => props.theme.font.size.sm};\n    padding: 0.2rem 0px;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n    min-width: 100px;\n\n    .dropdown {\n      width: fit-content;\n      min-width: 100px;\n\n      div[data-tippy-root] {\n        width: fit-content;\n        min-width: 100px;\n      }\n      .tippy-box {\n        width: fit-content;\n        max-width: none !important;\n        min-width: 100px;\n\n        .tippy-content: {\n          width: fit-content;\n          max-width: none !important;\n          min-width: 100px;\n        }\n      }\n    }\n\n    .token-placement-label {\n      width: fit-content;\n      // color: ${(props) => props.theme.colors.text.yellow};\n      justify-content: space-between;\n      padding: 0 0.5rem;\n      min-width: 100px;\n    }\n\n    .dropdown-item {\n      padding: 0.2rem 0.6rem !important;\n    }\n  }\n\n  input[type='checkbox'] {\n    cursor: pointer;\n    accent-color: ${(props) => props.theme.primary.solid};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/index.js",
    "content": "import React from 'react';\nimport { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';\nimport get from 'lodash/get';\nimport { useTheme } from 'providers/Theme';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { IconCaretDown, IconSettings, IconKey, IconHelp, IconAdjustmentsHorizontal } from '@tabler/icons';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport StyledWrapper from './StyledWrapper';\nimport { inputsConfig } from './inputsConfig';\nimport Oauth2TokenViewer from '../Oauth2TokenViewer/index';\nimport Oauth2ActionButtons from '../Oauth2ActionButtons/index';\nimport AdditionalParams from '../AdditionalParams/index';\nimport SensitiveFieldWarning from 'components/SensitiveFieldWarning';\nimport { savePreferences } from 'providers/ReduxStore/slices/app';\nimport toast from 'react-hot-toast';\n\nconst OAuth2AuthorizationCode = ({ save, item = {}, request, handleRun, updateAuth, collection, folder }) => {\n  const dispatch = useDispatch();\n  const preferences = useSelector((state) => state.app.preferences);\n  const { storedTheme } = useTheme();\n  const useSystemBrowser = get(preferences, 'request.oauth2.useSystemBrowser', false);\n  const { isSensitive } = useDetectSensitiveField(collection);\n  const oAuth = get(request, 'auth.oauth2', {});\n  const {\n    callbackUrl,\n    authorizationUrl,\n    accessTokenUrl,\n    clientId,\n    clientSecret,\n    scope,\n    credentialsPlacement,\n    state,\n    pkce,\n    credentialsId,\n    tokenPlacement,\n    tokenHeaderPrefix,\n    tokenQueryKey,\n    refreshTokenUrl,\n    autoRefreshToken,\n    autoFetchToken,\n    tokenSource,\n    additionalParameters\n  } = oAuth;\n\n  const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';\n  const isAutoRefreshDisabled = !refreshTokenUrlAvailable;\n\n  const handleSave = () => { save(); };\n\n  const handleChange = (key, value) => {\n    dispatch(\n      updateAuth({\n        mode: 'oauth2',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          grantType: 'authorization_code',\n          callbackUrl,\n          authorizationUrl,\n          accessTokenUrl,\n          clientId,\n          clientSecret,\n          state,\n          scope,\n          pkce,\n          credentialsPlacement,\n          credentialsId,\n          tokenPlacement,\n          tokenHeaderPrefix,\n          tokenQueryKey,\n          refreshTokenUrl,\n          autoRefreshToken,\n          autoFetchToken,\n          tokenSource,\n          additionalParameters,\n          [key]: value\n        }\n      })\n    );\n  };\n\n  const handlePKCEToggle = (e) => {\n    dispatch(\n      updateAuth({\n        mode: 'oauth2',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          grantType: 'authorization_code',\n          callbackUrl,\n          authorizationUrl,\n          accessTokenUrl,\n          clientId,\n          clientSecret,\n          state,\n          scope,\n          credentialsPlacement,\n          credentialsId,\n          tokenPlacement,\n          tokenHeaderPrefix,\n          tokenQueryKey,\n          autoFetchToken,\n          tokenSource,\n          additionalParameters,\n          pkce: !Boolean(oAuth?.['pkce'])\n        }\n      })\n    );\n  };\n\n  const handleUseSystemBrowserToggle = (e) => {\n    const newValue = e.target.checked;\n    dispatch(\n      savePreferences({\n        ...preferences,\n        request: {\n          ...preferences.request,\n          oauth2: {\n            ...preferences.request.oauth2,\n            useSystemBrowser: newValue\n          }\n        }\n      })\n    )\n      .then(() => {\n        toast.success('Preference updated successfully');\n      })\n      .catch((err) => {\n        console.error(err);\n        toast.error('Failed to update preference');\n      });\n  };\n\n  return (\n    <StyledWrapper className=\"mt-2 flex w-full gap-4 flex-col\">\n      <Oauth2TokenViewer handleRun={handleRun} collection={collection} item={item} url={accessTokenUrl} credentialsId={credentialsId} />\n      <div className=\"flex items-center gap-2.5 mt-2\">\n        <div className=\"flex items-center px-2.5 py-1.5 oauth2-icon-container rounded-md\">\n          <IconSettings size={14} className=\"oauth2-icon\" />\n        </div>\n        <span className=\"oauth2-section-label\">\n          Configuration\n        </span>\n      </div>\n      <div className=\"flex items-center gap-4 w-full\" key=\"input-callbackUrl\">\n        <label className=\"block min-w-[140px]\">Callback URL</label>\n        <div className=\"flex flex-col gap-1 w-full\">\n          <div className=\"single-line-editor-wrapper flex-1 flex items-center\">\n            <SingleLineEditor\n              value={callbackUrl}\n              theme={storedTheme}\n              onSave={handleSave}\n              onChange={(val) => handleChange('callbackUrl', val)}\n              onRun={handleRun}\n              collection={collection}\n              item={item}\n              placeholder={useSystemBrowser ? 'https://oauth.usebruno.com/callback' : undefined}\n              isCompact\n            />\n          </div>\n        </div>\n      </div>\n      <div className=\"flex items-center gap-4 w-full\" key=\"input-use-system-browser\">\n        <label className=\"block min-w-[140px]\"></label>\n        <div className=\"flex items-center gap-2\">\n          <input\n            type=\"checkbox\"\n            checked={Boolean(useSystemBrowser)}\n            onChange={handleUseSystemBrowserToggle}\n            className=\"cursor-pointer\"\n          />\n          <label\n            className=\"block cursor-pointer\"\n            onClick={(e) => {\n              e.preventDefault();\n              handleUseSystemBrowserToggle({ target: { checked: !useSystemBrowser } });\n            }}\n          >\n            Use system browser for OAuth\n          </label>\n        </div>\n      </div>\n      {inputsConfig.map((input) => {\n        const { key, label, isSecret } = input;\n        const value = oAuth[key] || '';\n        const { showWarning, warningMessage } = isSensitive(value);\n\n        return (\n          <div className=\"flex items-center gap-4 w-full\" key={`input-${key}`}>\n            <label className=\"block min-w-[140px]\">{label}</label>\n            <div className=\"single-line-editor-wrapper flex-1 flex items-center\">\n              <SingleLineEditor\n                value={value}\n                theme={storedTheme}\n                onSave={handleSave}\n                onChange={(val) => handleChange(key, val)}\n                onRun={handleRun}\n                collection={collection}\n                item={item}\n                isSecret={isSecret}\n                isCompact\n              />\n              {isSecret && showWarning && <SensitiveFieldWarning fieldName={key} warningMessage={warningMessage} />}\n            </div>\n          </div>\n        );\n      })}\n      <div className=\"flex items-center gap-4 w-full\" key=\"input-credentials-placement\">\n        <label className=\"block min-w-[140px]\">Add Credentials to</label>\n        <div className=\"inline-flex items-center cursor-pointer token-placement-selector\">\n          <MenuDropdown\n            items={[\n              { id: 'body', label: 'Request Body', onClick: () => handleChange('credentialsPlacement', 'body') },\n              { id: 'basic_auth_header', label: 'Basic Auth Header', onClick: () => handleChange('credentialsPlacement', 'basic_auth_header') }\n            ]}\n            selectedItemId={credentialsPlacement}\n            placement=\"bottom-end\"\n          >\n            <div className=\"flex items-center justify-end token-placement-label select-none\">\n              {credentialsPlacement == 'body' ? 'Request Body' : 'Basic Auth Header'}\n              <IconCaretDown className=\"caret ml-1 mr-1\" size={14} strokeWidth={2} />\n            </div>\n          </MenuDropdown>\n        </div>\n      </div>\n      <div className=\"flex flex-row w-full gap-4\" key=\"pkce\">\n        <label className=\"block\">Use PKCE</label>\n        <input\n          className=\"cursor-pointer\"\n          type=\"checkbox\"\n          checked={Boolean(oAuth?.['pkce'])}\n          onChange={handlePKCEToggle}\n        />\n      </div>\n      <div className=\"flex items-center gap-2.5 mt-2\">\n        <div className=\"flex items-center px-2.5 py-1.5 oauth2-icon-container rounded-md\">\n          <IconKey size={14} className=\"oauth2-icon\" />\n        </div>\n        <span className=\"oauth2-section-label\">\n          Token\n        </span>\n      </div>\n      <div className=\"flex items-center gap-4 w-full\" key=\"input-token-type\">\n        <label className=\"block min-w-[140px]\">Token Source</label>\n        <div className=\"inline-flex items-center cursor-pointer token-placement-selector\">\n          <MenuDropdown\n            items={[\n              { id: 'access_token', label: 'Access Token', onClick: () => handleChange('tokenSource', 'access_token') },\n              { id: 'id_token', label: 'ID Token', onClick: () => handleChange('tokenSource', 'id_token') }\n            ]}\n            selectedItemId={tokenSource}\n            placement=\"bottom-end\"\n          >\n            <div className=\"flex items-center justify-end token-placement-label select-none\">\n              {tokenSource === 'id_token' ? 'ID Token' : 'Access Token'}\n              <IconCaretDown className=\"caret ml-1 mr-1\" size={14} strokeWidth={2} />\n            </div>\n          </MenuDropdown>\n        </div>\n      </div>\n      <div className=\"flex items-center gap-4 w-full\" key=\"input-token-name\">\n        <label className=\"block min-w-[140px]\">Token ID</label>\n        <div className=\"single-line-editor-wrapper flex-1\">\n          <SingleLineEditor\n            value={oAuth['credentialsId'] || ''}\n            theme={storedTheme}\n            onSave={handleSave}\n            onChange={(val) => handleChange('credentialsId', val)}\n            onRun={handleRun}\n            collection={collection}\n            item={item}\n            isCompact\n          />\n        </div>\n      </div>\n      <div className=\"flex items-center gap-4 w-full\" key=\"input-token-placement\">\n        <label className=\"block min-w-[140px]\">Add token to</label>\n        <div className=\"inline-flex items-center cursor-pointer token-placement-selector\">\n          <MenuDropdown\n            items={[\n              { id: 'header', label: 'Header', onClick: () => handleChange('tokenPlacement', 'header') },\n              { id: 'url', label: 'URL', onClick: () => handleChange('tokenPlacement', 'url') }\n            ]}\n            selectedItemId={tokenPlacement}\n            placement=\"bottom-end\"\n          >\n            <div className=\"flex items-center justify-end token-placement-label select-none\">\n              {tokenPlacement == 'url' ? 'URL' : 'Headers'}\n              <IconCaretDown className=\"caret ml-1 mr-1\" size={14} strokeWidth={2} />\n            </div>\n          </MenuDropdown>\n        </div>\n      </div>\n      {\n        tokenPlacement === 'header'\n          ? (\n              <div className=\"flex items-center gap-4 w-full\" key=\"input-token-prefix\">\n                <label className=\"block min-w-[140px]\">Header Prefix</label>\n                <div className=\"single-line-editor-wrapper flex-1\">\n                  <SingleLineEditor\n                    value={oAuth['tokenHeaderPrefix'] || ''}\n                    theme={storedTheme}\n                    onSave={handleSave}\n                    onChange={(val) => handleChange('tokenHeaderPrefix', val)}\n                    onRun={handleRun}\n                    collection={collection}\n                    isCompact\n                  />\n                </div>\n              </div>\n            )\n          : (\n              <div className=\"flex items-center gap-4 w-full\" key=\"input-token-query-param-key\">\n                <label className=\"block min-w-[140px]\">Query Param Key</label>\n                <div className=\"single-line-editor-wrapper flex-1\">\n                  <SingleLineEditor\n                    value={oAuth['tokenQueryKey'] || ''}\n                    theme={storedTheme}\n                    onSave={handleSave}\n                    onChange={(val) => handleChange('tokenQueryKey', val)}\n                    onRun={handleRun}\n                    collection={collection}\n                    isCompact\n                  />\n                </div>\n              </div>\n            )\n      }\n      <div className=\"flex items-center gap-2.5 mt-4 mb-2\">\n        <div className=\"flex items-center px-2.5 py-1.5 oauth2-icon-container rounded-md\">\n          <IconAdjustmentsHorizontal size={14} className=\"oauth2-icon\" />\n        </div>\n        <span className=\"oauth2-section-label\">\n          Advanced Settings\n        </span>\n      </div>\n\n      <div className=\"flex items-center gap-4 w-full mb-4\">\n        <label className=\"block min-w-[140px]\">Refresh Token URL</label>\n        <div className=\"single-line-editor-wrapper flex-1\">\n          <SingleLineEditor\n            value={get(request, 'auth.oauth2.refreshTokenUrl', '')}\n            theme={storedTheme}\n            onSave={handleSave}\n            onChange={(val) => handleChange('refreshTokenUrl', val)}\n            collection={collection}\n            item={item}\n            isCompact\n          />\n        </div>\n      </div>\n\n      <div className=\"flex items-center gap-2.5 mt-4\">\n        <div className=\"flex items-center px-2.5 py-1.5 oauth2-icon-container rounded-md\">\n          <IconSettings size={14} className=\"oauth2-icon\" />\n        </div>\n        <span className=\"font-medium\">Settings</span>\n      </div>\n\n      {/* Automatically Fetch Token */}\n      <div className=\"flex items-center gap-4 w-full\">\n        <input\n          type=\"checkbox\"\n          checked={Boolean(autoFetchToken)}\n          onChange={(e) => handleChange('autoFetchToken', e.target.checked)}\n          className=\"cursor-pointer ml-1\"\n        />\n        <label className=\"block min-w-[140px]\">Automatically fetch token if not found</label>\n        <div className=\"flex items-center gap-2\">\n          <div className=\"relative group cursor-pointer\">\n            <IconHelp size={16} className=\"text-gray-500\" />\n            <span className=\"group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200\">\n              Automatically fetch a new token when you try to access a resource and don't have one.\n            </span>\n          </div>\n        </div>\n      </div>\n\n      {/* Auto Refresh Token (With Refresh URL) */}\n      <div className=\"flex items-center gap-4 w-full\">\n        <input\n          type=\"checkbox\"\n          checked={Boolean(autoRefreshToken)}\n          onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}\n          className={`cursor-pointer ml-1 ${isAutoRefreshDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}\n          disabled={isAutoRefreshDisabled}\n        />\n        <label className={`block min-w-[140px] ${isAutoRefreshDisabled ? 'text-gray-500' : ''}`}>Auto refresh token (with refresh URL)</label>\n        <div className=\"flex items-center gap-2\">\n          <div className=\"relative group cursor-pointer\">\n            <IconHelp size={16} className=\"text-gray-500\" />\n            <span className=\"group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200\">\n              Automatically refresh your token using the refresh URL when it expires.\n            </span>\n          </div>\n        </div>\n      </div>\n      <AdditionalParams\n        item={item}\n        request={request}\n        collection={collection}\n        updateAuth={updateAuth}\n        handleSave={handleSave}\n      />\n      <Oauth2ActionButtons item={item} request={request} collection={collection} url={accessTokenUrl} credentialsId={credentialsId} />\n    </StyledWrapper>\n  );\n};\n\nexport default OAuth2AuthorizationCode;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/OAuth2/AuthorizationCode/inputsConfig.js",
    "content": "const inputsConfig = [\n  {\n    key: 'authorizationUrl',\n    label: 'Authorization URL'\n  },\n  {\n    key: 'accessTokenUrl',\n    label: 'Access Token URL'\n  },\n  {\n    key: 'clientId',\n    label: 'Client ID'\n  },\n  {\n    key: 'clientSecret',\n    label: 'Client Secret',\n    isSecret: true\n  },\n  {\n    key: 'scope',\n    label: 'Scope'\n  },\n  {\n    key: 'state',\n    label: 'State'\n  }\n];\n\nexport { inputsConfig };\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst Wrapper = styled.div`\n  .oauth2-icon-container {\n    background-color: ${(props) => rgba(props.theme.primary.solid, 0.1)};\n  }\n\n  .oauth2-icon {\n    color: ${(props) => props.theme.primary.text};\n  }\n\n  label {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.subtext1};\n  }\n  .single-line-editor-wrapper {\n    max-width: 400px;\n    padding: 0.15rem 0.4rem;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n  }\n\n  .token-placement-selector {\n    font-size: ${(props) => props.theme.font.size.sm};\n    padding: 0.2rem 0px;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n    min-width: 100px;\n\n    .dropdown {\n      width: fit-content;\n      min-width: 100px;\n\n      div[data-tippy-root] {\n        width: fit-content;\n        min-width: 100px;\n      }\n      .tippy-box {\n        width: fit-content;\n        max-width: none !important;\n        min-width: 100px;\n\n        .tippy-content {\n          width: fit-content;\n          max-width: none !important;\n          min-width: 100px;\n        }\n      }\n    }\n\n    .token-placement-label {\n      width: fit-content;\n      // color: ${(props) => props.theme.colors.text.yellow};\n      justify-content: space-between;\n      padding: 0 0.5rem;\n      min-width: 100px;\n    }\n\n    .dropdown-item {\n      padding: 0.2rem 0.6rem !important;\n    }\n  }\n\n  input[type='checkbox'] {\n    cursor: pointer;\n    accent-color: ${(props) => props.theme.primary.solid};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/index.js",
    "content": "import React from 'react';\nimport { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';\nimport get from 'lodash/get';\nimport { useTheme } from 'providers/Theme';\nimport { useDispatch } from 'react-redux';\nimport { IconCaretDown, IconSettings, IconKey, IconAdjustmentsHorizontal, IconHelp } from '@tabler/icons';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport StyledWrapper from './StyledWrapper';\nimport { inputsConfig } from './inputsConfig';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport Oauth2TokenViewer from '../Oauth2TokenViewer/index';\nimport Oauth2ActionButtons from '../Oauth2ActionButtons/index';\nimport AdditionalParams from '../AdditionalParams/index';\nimport SensitiveFieldWarning from 'components/SensitiveFieldWarning';\n\nconst OAuth2ClientCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n  const { isSensitive } = useDetectSensitiveField(collection);\n  const oAuth = get(request, 'auth.oauth2', {});\n\n  const {\n    accessTokenUrl,\n    clientId,\n    clientSecret,\n    scope,\n    credentialsPlacement,\n    credentialsId,\n    tokenPlacement,\n    tokenHeaderPrefix,\n    tokenQueryKey,\n    refreshTokenUrl,\n    autoRefreshToken,\n    autoFetchToken,\n    tokenSource,\n    additionalParameters\n  } = oAuth;\n\n  const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';\n  const isAutoRefreshDisabled = !refreshTokenUrlAvailable;\n\n  const handleSave = () => { save(); };\n\n  const handleChange = (key, value) => {\n    dispatch(\n      updateAuth({\n        mode: 'oauth2',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          grantType: 'client_credentials',\n          accessTokenUrl,\n          clientId,\n          clientSecret,\n          scope,\n          credentialsPlacement,\n          credentialsId,\n          tokenPlacement,\n          tokenHeaderPrefix,\n          tokenQueryKey,\n          refreshTokenUrl,\n          autoRefreshToken,\n          autoFetchToken,\n          tokenSource,\n          additionalParameters,\n          [key]: value\n        }\n      })\n    );\n  };\n\n  return (\n    <StyledWrapper className=\"mt-2 flex w-full gap-4 flex-col\">\n      <Oauth2TokenViewer handleRun={handleRun} collection={collection} item={item} url={accessTokenUrl} credentialsId={credentialsId} />\n      <div className=\"flex items-center gap-2.5 mt-2\">\n        <div className=\"flex items-center px-2.5 py-1.5 oauth2-icon-container rounded-md\">\n          <IconSettings size={14} className=\"oauth2-icon\" />\n        </div>\n        <span className=\"oauth2-section-label\">\n          Configuration\n        </span>\n      </div>\n      {inputsConfig.map((input) => {\n        const { key, label, isSecret } = input;\n        const value = oAuth[key] || '';\n        const { showWarning, warningMessage } = isSensitive(value);\n\n        return (\n          <div className=\"flex items-center gap-4 w-full\" key={`input-${key}`}>\n            <label className=\"block min-w-[140px]\">{label}</label>\n            <div className=\"single-line-editor-wrapper flex-1 flex items-center\">\n              <SingleLineEditor\n                value={value}\n                theme={storedTheme}\n                onSave={handleSave}\n                onChange={(val) => handleChange(key, val)}\n                onRun={handleRun}\n                collection={collection}\n                item={item}\n                isSecret={isSecret}\n                isCompact\n              />\n              {isSecret && showWarning && <SensitiveFieldWarning fieldName={key} warningMessage={warningMessage} />}\n            </div>\n          </div>\n        );\n      })}\n      <div className=\"flex items-center gap-4 w-full\" key=\"input-credentials-placement\">\n        <label className=\"block min-w-[140px]\">Add Credentials to</label>\n        <div className=\"inline-flex items-center cursor-pointer token-placement-selector\">\n          <MenuDropdown\n            items={[\n              { id: 'body', label: 'Request Body', onClick: () => handleChange('credentialsPlacement', 'body') },\n              { id: 'basic_auth_header', label: 'Basic Auth Header', onClick: () => handleChange('credentialsPlacement', 'basic_auth_header') }\n            ]}\n            selectedItemId={credentialsPlacement}\n            placement=\"bottom-end\"\n          >\n            <div className=\"flex items-center justify-end token-placement-label select-none\">\n              {credentialsPlacement == 'body' ? 'Request Body' : 'Basic Auth Header'}\n              <IconCaretDown className=\"caret ml-1 mr-1\" size={14} strokeWidth={2} />\n            </div>\n          </MenuDropdown>\n        </div>\n      </div>\n      <div className=\"flex items-center gap-2.5 mt-2\">\n        <div className=\"flex items-center px-2.5 py-1.5 oauth2-icon-container rounded-md\">\n          <IconKey size={14} className=\"oauth2-icon\" />\n        </div>\n        <span className=\"oauth2-section-label\">\n          Token\n        </span>\n      </div>\n      <div className=\"flex items-center gap-4 w-full\" key=\"input-token-type\">\n        <label className=\"block min-w-[140px]\">Token Source</label>\n        <div className=\"inline-flex items-center cursor-pointer token-placement-selector\">\n          <MenuDropdown\n            items={[\n              { id: 'access_token', label: 'Access Token', onClick: () => handleChange('tokenSource', 'access_token') },\n              { id: 'id_token', label: 'ID Token', onClick: () => handleChange('tokenSource', 'id_token') }\n            ]}\n            selectedItemId={tokenSource}\n            placement=\"bottom-end\"\n          >\n            <div className=\"flex items-center justify-end token-placement-label select-none\">\n              {tokenSource === 'id_token' ? 'ID Token' : 'Access Token'}\n              <IconCaretDown className=\"caret ml-1 mr-1\" size={14} strokeWidth={2} />\n            </div>\n          </MenuDropdown>\n        </div>\n      </div>\n      <div className=\"flex items-center gap-4 w-full\" key=\"input-token-name\">\n        <label className=\"block min-w-[140px]\">Token ID</label>\n        <div className=\"single-line-editor-wrapper flex-1\">\n          <SingleLineEditor\n            value={oAuth['credentialsId'] || ''}\n            theme={storedTheme}\n            onSave={handleSave}\n            onChange={(val) => handleChange('credentialsId', val)}\n            onRun={handleRun}\n            collection={collection}\n            item={item}\n            isCompact\n          />\n        </div>\n      </div>\n      <div className=\"flex items-center gap-4 w-full\" key=\"input-token-placement\">\n        <label className=\"block min-w-[140px]\">Add token to</label>\n        <div className=\"inline-flex items-center cursor-pointer token-placement-selector w-fit\">\n          <MenuDropdown\n            items={[\n              { id: 'header', label: 'Header', onClick: () => handleChange('tokenPlacement', 'header') },\n              { id: 'url', label: 'URL', onClick: () => handleChange('tokenPlacement', 'url') }\n            ]}\n            selectedItemId={tokenPlacement}\n            placement=\"bottom-end\"\n          >\n            <div className=\"flex items-center justify-end token-placement-label select-none\">\n              {tokenPlacement == 'url' ? 'URL' : 'Headers'}\n              <IconCaretDown className=\"caret ml-1 mr-1\" size={14} strokeWidth={2} />\n            </div>\n          </MenuDropdown>\n        </div>\n      </div>\n      {\n        tokenPlacement === 'header'\n          ? (\n              <div className=\"flex items-center gap-4 w-full\" key=\"input-token-prefix\">\n                <label className=\"block min-w-[140px]\">Header Prefix</label>\n                <div className=\"single-line-editor-wrapper flex-1\">\n                  <SingleLineEditor\n                    value={oAuth['tokenHeaderPrefix'] || ''}\n                    theme={storedTheme}\n                    onSave={handleSave}\n                    onChange={(val) => handleChange('tokenHeaderPrefix', val)}\n                    onRun={handleRun}\n                    collection={collection}\n                    isCompact\n                  />\n                </div>\n              </div>\n            )\n          : (\n              <div className=\"flex items-center gap-4 w-full\" key=\"input-token-query-param-key\">\n                <label className=\"block min-w-[140px]\">Query Param Key</label>\n                <div className=\"single-line-editor-wrapper flex-1\">\n                  <SingleLineEditor\n                    value={oAuth['tokenQueryKey'] || ''}\n                    theme={storedTheme}\n                    onSave={handleSave}\n                    onChange={(val) => handleChange('tokenQueryKey', val)}\n                    onRun={handleRun}\n                    collection={collection}\n                    isCompact\n                  />\n                </div>\n              </div>\n            )\n      }\n      <div className=\"flex items-center gap-2.5 mt-4 mb-2\">\n        <div className=\"flex items-center px-2.5 py-1.5 oauth2-icon-container rounded-md\">\n          <IconAdjustmentsHorizontal size={14} className=\"oauth2-icon\" />\n        </div>\n        <span className=\"oauth2-section-label\">\n          Advanced Settings\n        </span>\n      </div>\n\n      <div className=\"flex items-center gap-4 w-full mb-4\">\n        <label className=\"block min-w-[140px]\">Refresh Token URL</label>\n        <div className=\"single-line-editor-wrapper flex-1\">\n          <SingleLineEditor\n            value={get(request, 'auth.oauth2.refreshTokenUrl', '')}\n            theme={storedTheme}\n            onSave={handleSave}\n            onChange={(val) => handleChange('refreshTokenUrl', val)}\n            collection={collection}\n            item={item}\n            isCompact\n          />\n        </div>\n      </div>\n\n      <div className=\"flex items-center gap-2.5 mt-4\">\n        <div className=\"flex items-center px-2.5 py-1.5 oauth2-icon-container rounded-md\">\n          <IconSettings size={14} className=\"oauth2-icon\" />\n        </div>\n        <span className=\"oauth2-section-label\">Settings</span>\n      </div>\n\n      {/* Automatically Fetch Token */}\n      <div className=\"flex items-center gap-4 w-full\">\n        <input\n          type=\"checkbox\"\n          checked={Boolean(autoFetchToken)}\n          onChange={(e) => handleChange('autoFetchToken', e.target.checked)}\n          className=\"cursor-pointer ml-1\"\n        />\n        <label className=\"block min-w-[140px]\">Automatically fetch token if not found</label>\n        <div className=\"flex items-center gap-2\">\n          <div className=\"relative group cursor-pointer\">\n            <IconHelp size={16} className=\"text-gray-500\" />\n            <span className=\"group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200\">\n              Automatically fetch a new token when you try to access a resource and don't have one.\n            </span>\n          </div>\n        </div>\n      </div>\n\n      {/* Auto Refresh Token (With Refresh URL) */}\n      <div className=\"flex items-center gap-4 w-full\">\n        <input\n          type=\"checkbox\"\n          checked={Boolean(autoRefreshToken)}\n          onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}\n          className={`cursor-pointer ml-1 ${isAutoRefreshDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}\n          disabled={isAutoRefreshDisabled}\n        />\n        <label className={`block min-w-[140px] ${isAutoRefreshDisabled ? 'text-gray-500' : ''}`}>Auto refresh token (with refresh URL)</label>\n        <div className=\"flex items-center gap-2\">\n          <div className=\"relative group cursor-pointer\">\n            <IconHelp size={16} className=\"text-gray-500\" />\n            <span className=\"group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200\">\n              Automatically refresh your token using the refresh URL when it expires.\n            </span>\n          </div>\n        </div>\n      </div>\n      <AdditionalParams\n        item={item}\n        request={request}\n        collection={collection}\n        updateAuth={updateAuth}\n        handleSave={handleSave}\n      />\n      <Oauth2ActionButtons item={item} request={request} collection={collection} url={accessTokenUrl} credentialsId={credentialsId} />\n\n    </StyledWrapper>\n  );\n};\n\nexport default OAuth2ClientCredentials;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/OAuth2/ClientCredentials/inputsConfig.js",
    "content": "const inputsConfig = [\n  {\n    key: 'accessTokenUrl',\n    label: 'Access Token URL'\n  },\n  {\n    key: 'clientId',\n    label: 'Client ID'\n  },\n  {\n    key: 'clientSecret',\n    label: 'Client Secret',\n    isSecret: true\n  },\n  {\n    key: 'scope',\n    label: 'Scope'\n  }\n];\n\nexport { inputsConfig };\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/OAuth2/GrantTypeSelector/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst Wrapper = styled.div`\n  .oauth2-icon-container {\n    background-color: ${(props) => rgba(props.theme.primary.solid, 0.1)};\n  }\n\n  .oauth2-icon {\n    color: ${(props) => props.theme.primary.text};\n  }\n\n  font-size: ${(props) => props.theme.font.size.base};\n\n  .grant-type-mode-selector {\n    font-size: ${(props) => props.theme.font.size.sm};\n    padding: 0.2rem 0px;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n\n    .dropdown {\n      width: fit-content;\n\n      div[data-tippy-root] {\n        width: fit-content;\n      }\n      .tippy-box {\n        width: fit-content;\n        max-width: none !important;\n\n        .tippy-content: {\n          width: fit-content;\n          max-width: none !important;\n        }\n      }\n    }\n\n    .grant-type-label {\n      width: fit-content;\n      color: ${(props) => props.theme.primary.text};\n      justify-content: space-between;\n      padding: 0 0.5rem;\n    }\n\n    .dropdown-item {\n      padding: 0.2rem 0.6rem !important;\n    }\n\n    .label-item {\n      padding: 0.2rem 0.6rem !important;\n    }\n  }\n\n  .caret {\n    color: rgb(140, 140, 140);\n    fill: rgb(140 140 140);\n  }\n  label {\n    font-size: ${(props) => props.theme.font.size.base};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/OAuth2/GrantTypeSelector/index.js",
    "content": "import React from 'react';\nimport get from 'lodash/get';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport { useDispatch } from 'react-redux';\nimport StyledWrapper from './StyledWrapper';\nimport { IconCaretDown, IconKey } from '@tabler/icons';\nimport { humanizeGrantType } from 'utils/collections';\nimport { useEffect } from 'react';\nimport { useState } from 'react';\n\nconst GrantTypeSelector = ({ item = {}, request, updateAuth, collection }) => {\n  const dispatch = useDispatch();\n  const oAuth = get(request, 'auth.oauth2', {});\n  const [valuesCache, setValuesCache] = useState({\n    ...oAuth\n  });\n\n  const onGrantTypeChange = (grantType) => {\n    let updatedValues = {\n      ...valuesCache,\n      ...oAuth,\n      grantType\n    };\n    setValuesCache(updatedValues);\n    dispatch(\n      updateAuth({\n        mode: 'oauth2',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          ...updatedValues\n        }\n      })\n    );\n  };\n\n  useEffect(() => {\n    // initialize redux state with a default oauth2 grant type\n    // authorization_code - default option\n    !oAuth?.grantType\n    && dispatch(\n      updateAuth({\n        mode: 'oauth2',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          grantType: 'authorization_code',\n          accessTokenUrl: '',\n          username: '',\n          password: '',\n          clientId: '',\n          clientSecret: '',\n          scope: '',\n          credentialsPlacement: 'body',\n          credentialsId: 'credentials',\n          tokenPlacement: 'header',\n          tokenHeaderPrefix: 'Bearer',\n          tokenQueryKey: 'access_token',\n          tokenSource: 'access_token'\n        }\n      })\n    );\n  }, [oAuth]);\n\n  return (\n    <StyledWrapper>\n      <div className=\"flex items-center gap-2.5 mb-4\">\n        <div className=\"flex items-center px-2.5 py-1.5 oauth2-icon-container rounded-md\">\n          <IconKey size={14} className=\"oauth2-icon\" />\n        </div>\n        <span className=\"oauth2-section-label\">\n          Grant Type\n        </span>\n      </div>\n      <div className=\"inline-flex items-center cursor-pointer grant-type-mode-selector w-fit\">\n        <MenuDropdown\n          items={[\n            { id: 'password', label: 'Password Credentials', onClick: () => onGrantTypeChange('password') },\n            { id: 'authorization_code', label: 'Authorization Code', onClick: () => onGrantTypeChange('authorization_code') },\n            { id: 'implicit', label: 'Implicit', onClick: () => onGrantTypeChange('implicit') },\n            { id: 'client_credentials', label: 'Client Credentials', onClick: () => onGrantTypeChange('client_credentials') }\n          ]}\n          selectedItemId={oAuth?.grantType}\n          placement=\"bottom-end\"\n        >\n          <div className=\"flex items-center justify-end grant-type-label select-none\">\n            {humanizeGrantType(oAuth?.grantType)} <IconCaretDown className=\"caret ml-1 mr-1\" size={14} strokeWidth={2} />\n          </div>\n        </MenuDropdown>\n      </div>\n    </StyledWrapper>\n  );\n};\nexport default GrantTypeSelector;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Implicit/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst Wrapper = styled.div`\n  .oauth2-icon-container {\n    background-color: ${(props) => rgba(props.theme.primary.solid, 0.1)};\n  }\n\n  .oauth2-icon {\n    color: ${(props) => props.theme.primary.text};\n  }\n\n  label {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.subtext1};\n  }\n  .oauth2-input-wrapper {\n    max-width: 400px;\n    padding: 0.15rem 0.4rem;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n  }\n\n  .token-placement-selector {\n    font-size: ${(props) => props.theme.font.size.sm};\n    padding: 0.2rem 0px;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n    min-width: 100px;\n\n    .dropdown {\n      width: fit-content;\n      min-width: 100px;\n\n      div[data-tippy-root] {\n        width: fit-content;\n        min-width: 100px;\n      }\n      .tippy-box {\n        width: fit-content;\n        max-width: none !important;\n        min-width: 100px;\n\n        .tippy-content {\n          width: fit-content;\n          max-width: none !important;\n          min-width: 100px;\n        }\n      }\n    }\n\n    .token-placement-label {\n      width: fit-content;\n      justify-content: space-between;\n      padding: 0 0.5rem;\n      min-width: 100px;\n    }\n\n    .dropdown-item {\n      padding: 0.2rem 0.6rem !important;\n    }\n  }\n\n  .checkbox-label {\n    color: ${(props) => props.theme.colors.text.primary};\n    user-select: none;\n  }\n\n  input[type='checkbox'] {\n    cursor: pointer;\n    accent-color: ${(props) => props.theme.primary.solid};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Implicit/index.js",
    "content": "import React, { useMemo } from 'react';\nimport get from 'lodash/get';\nimport { useTheme } from 'providers/Theme';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { IconCaretDown, IconSettings, IconKey, IconHelp, IconAdjustmentsHorizontal } from '@tabler/icons';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport Wrapper from './StyledWrapper';\nimport { inputsConfig } from './inputsConfig';\nimport Oauth2TokenViewer from '../Oauth2TokenViewer/index';\nimport Oauth2ActionButtons from '../Oauth2ActionButtons/index';\nimport AdditionalParams from '../AdditionalParams/index';\nimport { getAllVariables } from 'utils/collections/index';\nimport { interpolate } from '@usebruno/common';\nimport { savePreferences } from 'providers/ReduxStore/slices/app';\nimport toast from 'react-hot-toast';\n\nconst OAuth2Implicit = ({ save, item = {}, request, handleRun, updateAuth, collection, folder }) => {\n  const dispatch = useDispatch();\n  const preferences = useSelector((state) => state.app.preferences);\n  const useSystemBrowser = get(preferences, 'request.oauth2.useSystemBrowser', false);\n  const { storedTheme } = useTheme();\n  const oAuth = get(request, 'auth.oauth2', {});\n  const {\n    callbackUrl,\n    authorizationUrl,\n    clientId,\n    scope,\n    state,\n    credentialsId,\n    tokenPlacement,\n    tokenHeaderPrefix,\n    tokenQueryKey,\n    autoFetchToken,\n    tokenSource\n  } = oAuth;\n\n  const interpolatedAuthUrl = useMemo(() => {\n    const variables = getAllVariables(collection, item);\n    return interpolate(authorizationUrl, variables);\n  }, [collection, item, authorizationUrl]);\n\n  const handleSave = () => { save(); };\n\n  const handleChange = (key, value) => {\n    dispatch(\n      updateAuth({\n        mode: 'oauth2',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          grantType: 'implicit',\n          callbackUrl,\n          authorizationUrl,\n          clientId,\n          state,\n          scope,\n          credentialsId,\n          tokenPlacement,\n          tokenHeaderPrefix,\n          tokenQueryKey,\n          autoFetchToken,\n          tokenSource,\n          [key]: value\n        }\n      })\n    );\n  };\n\n  const handleAutoFetchTokenToggle = (e) => {\n    handleChange('autoFetchToken', e.target.checked);\n  };\n\n  const handleUseSystemBrowserToggle = (e) => {\n    const newValue = e.target.checked;\n    dispatch(\n      savePreferences({\n        ...preferences,\n        request: {\n          ...preferences.request,\n          oauth2: {\n            ...preferences.request.oauth2,\n            useSystemBrowser: newValue\n          }\n        }\n      })\n    )\n      .then(() => {\n        toast.success('Preference updated successfully');\n      })\n      .catch((err) => {\n        console.error(err);\n        toast.error('Failed to update preference');\n      });\n  };\n\n  return (\n    <Wrapper className=\"mt-2 flex w-full gap-4 flex-col\">\n      <Oauth2TokenViewer handleRun={handleRun} collection={collection} item={item} url={authorizationUrl} credentialsId={credentialsId} />\n      <div className=\"flex items-center gap-2.5 mt-2\">\n        <div className=\"flex items-center px-2.5 py-1.5 oauth2-icon-container rounded-md\">\n          <IconSettings size={14} className=\"oauth2-icon\" />\n        </div>\n        <span className=\"oauth2-section-label\">\n          Configuration\n        </span>\n      </div>\n      <div className=\"flex items-center gap-4 w-full\" key=\"input-callbackUrl\">\n        <label className=\"block min-w-[140px]\">Callback URL</label>\n        <div className=\"flex flex-col gap-1 w-full\">\n          <div className=\"oauth2-input-wrapper flex-1 flex items-center\">\n            <SingleLineEditor\n              value={callbackUrl}\n              theme={storedTheme}\n              onSave={handleSave}\n              onChange={(val) => handleChange('callbackUrl', val)}\n              onRun={handleRun}\n              collection={collection}\n              item={item}\n              placeholder={useSystemBrowser ? 'https://oauth.usebruno.com/callback' : undefined}\n              isCompact\n            />\n          </div>\n        </div>\n      </div>\n      <div className=\"flex items-center gap-4 w-full\" key=\"input-use-system-browser\">\n        <label className=\"block min-w-[140px]\"></label>\n        <div className=\"flex items-center gap-2\">\n          <input\n            type=\"checkbox\"\n            checked={Boolean(useSystemBrowser)}\n            onChange={handleUseSystemBrowserToggle}\n            className=\"cursor-pointer\"\n          />\n          <label\n            className=\"block cursor-pointer\"\n            onClick={(e) => {\n              e.preventDefault();\n              handleUseSystemBrowserToggle({ target: { checked: !useSystemBrowser } });\n            }}\n          >\n            Use system browser for OAuth\n          </label>\n        </div>\n      </div>\n      {inputsConfig.map((input) => {\n        const { key, label, isSecret } = input;\n        return (\n          <div className=\"flex items-center gap-4 w-full\" key={`input-${key}`}>\n            <label className=\"block min-w-[140px]\">{label}</label>\n            <div className=\"oauth2-input-wrapper flex-1\">\n              <SingleLineEditor\n                value={oAuth[key] || ''}\n                theme={storedTheme}\n                onSave={handleSave}\n                onChange={(val) => handleChange(key, val)}\n                onRun={handleRun}\n                collection={collection}\n                item={item}\n                isSecret={isSecret}\n                isCompact\n              />\n            </div>\n          </div>\n        );\n      })}\n\n      <div className=\"flex items-center gap-2.5 mt-2\">\n        <div className=\"flex items-center px-2.5 py-1.5 oauth2-icon-container rounded-md\">\n          <IconKey size={14} className=\"oauth2-icon\" />\n        </div>\n        <span className=\"oauth2-section-label\">\n          Token\n        </span>\n      </div>\n\n      <div className=\"flex items-center gap-4 w-full\" key=\"input-token-type\">\n        <label className=\"block min-w-[140px]\">Token Source</label>\n        <div className=\"inline-flex items-center cursor-pointer token-placement-selector\">\n          <MenuDropdown\n            items={[\n              { id: 'access_token', label: 'Access Token', onClick: () => handleChange('tokenSource', 'access_token') },\n              { id: 'id_token', label: 'ID Token', onClick: () => handleChange('tokenSource', 'id_token') }\n            ]}\n            selectedItemId={tokenSource}\n            placement=\"bottom-end\"\n          >\n            <div className=\"flex items-center justify-end token-placement-label select-none\">\n              {tokenSource === 'id_token' ? 'ID Token' : 'Access Token'}\n              <IconCaretDown className=\"caret ml-1 mr-1\" size={14} strokeWidth={2} />\n            </div>\n          </MenuDropdown>\n        </div>\n      </div>\n\n      <div className=\"flex items-center gap-4 w-full\" key=\"input-token-name\">\n        <label className=\"block min-w-[140px]\">Token ID</label>\n        <div className=\"oauth2-input-wrapper flex-1\">\n          <SingleLineEditor\n            value={oAuth['credentialsId'] || 'credentials'}\n            theme={storedTheme}\n            onSave={handleSave}\n            onChange={(val) => handleChange('credentialsId', val)}\n            onRun={handleRun}\n            collection={collection}\n            item={item}\n            isCompact\n          />\n        </div>\n      </div>\n\n      <div className=\"flex items-center gap-4 w-full\" key=\"input-token-placement\">\n        <label className=\"block min-w-[140px]\">Add Token to</label>\n        <div className=\"inline-flex items-center cursor-pointer token-placement-selector\">\n          <MenuDropdown\n            items={[\n              { id: 'header', label: 'Headers', onClick: () => handleChange('tokenPlacement', 'header') },\n              { id: 'url', label: 'URL', onClick: () => handleChange('tokenPlacement', 'url') }\n            ]}\n            selectedItemId={tokenPlacement}\n            placement=\"bottom-end\"\n          >\n            <div className=\"flex items-center justify-end token-placement-label select-none\">\n              {tokenPlacement == 'url' ? 'URL' : 'Headers'}\n              <IconCaretDown className=\"caret ml-1 mr-1\" size={14} strokeWidth={2} />\n            </div>\n          </MenuDropdown>\n        </div>\n      </div>\n\n      {tokenPlacement == 'header' ? (\n        <div className=\"flex items-center gap-4 w-full\" key=\"input-token-header-prefix\">\n          <label className=\"block min-w-[140px]\">Header Prefix</label>\n          <div className=\"oauth2-input-wrapper flex-1\">\n            <SingleLineEditor\n              value={oAuth.tokenHeaderPrefix || 'Bearer'}\n              theme={storedTheme}\n              onSave={handleSave}\n              onChange={(val) => handleChange('tokenHeaderPrefix', val)}\n              onRun={handleRun}\n              collection={collection}\n              item={item}\n              isCompact\n            />\n          </div>\n        </div>\n      ) : (\n        <div className=\"flex items-center gap-4 w-full\" key=\"input-token-query-key\">\n          <label className=\"block min-w-[140px]\">URL Query Key</label>\n          <div className=\"oauth2-input-wrapper flex-1\">\n            <SingleLineEditor\n              value={oAuth.tokenQueryKey || 'access_token'}\n              theme={storedTheme}\n              onSave={handleSave}\n              onChange={(val) => handleChange('tokenQueryKey', val)}\n              onRun={handleRun}\n              collection={collection}\n              item={item}\n              isCompact\n            />\n          </div>\n        </div>\n      )}\n\n      <div className=\"flex items-center gap-2.5 mt-2\">\n        <div className=\"flex items-center px-2.5 py-1.5 oauth2-icon-container rounded-md\">\n          <IconAdjustmentsHorizontal size={14} className=\"oauth2-icon\" />\n        </div>\n        <span className=\"oauth2-section-label\">\n          Advanced Options\n        </span>\n      </div>\n\n      <div className=\"flex items-center gap-4 w-full\">\n        <input\n          type=\"checkbox\"\n          checked={oAuth.autoFetchToken !== false}\n          onChange={handleAutoFetchTokenToggle}\n          className=\"cursor-pointer ml-1\"\n        />\n        <label className=\"block min-w-[140px]\">Auto fetch token</label>\n        <div className=\"flex items-center gap-2\">\n          <div className=\"relative group cursor-pointer\">\n            <IconHelp size={16} className=\"text-gray-500\" />\n            <span className=\"group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200\">\n              Automatically fetch a new token when the current one expires.\n            </span>\n          </div>\n        </div>\n      </div>\n\n      <AdditionalParams\n        item={item}\n        request={request}\n        collection={collection}\n        updateAuth={updateAuth}\n        handleSave={handleSave}\n      />\n      <Oauth2ActionButtons item={item} request={request} collection={collection} url={interpolatedAuthUrl} credentialsId={credentialsId} />\n    </Wrapper>\n  );\n};\n\nexport default OAuth2Implicit;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Implicit/inputsConfig.js",
    "content": "const inputsConfig = [\n  {\n    key: 'authorizationUrl',\n    label: 'Authorization URL'\n  },\n  {\n    key: 'clientId',\n    label: 'Client ID'\n  },\n  {\n    key: 'scope',\n    label: 'Scope'\n  },\n  {\n    key: 'state',\n    label: 'State'\n  }\n];\n\nexport { inputsConfig };\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2ActionButtons/index.js",
    "content": "import { useMemo, useState, useEffect } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport toast from 'react-hot-toast';\nimport { cloneDeep, find, get } from 'lodash';\nimport { IconLoader2, IconX } from '@tabler/icons';\nimport { interpolate } from '@usebruno/common';\nimport { fetchOauth2Credentials, clearOauth2Cache, refreshOauth2Credentials, cancelOauth2AuthorizationRequest, isOauth2AuthorizationRequestInProgress } from 'providers/ReduxStore/slices/collections/actions';\nimport { getAllVariables } from 'utils/collections/index';\nimport Button from 'ui/Button';\n\nconst Oauth2ActionButtons = ({ item, request, collection, url: accessTokenUrl, credentialsId }) => {\n  const { uid: collectionUid } = collection;\n\n  const dispatch = useDispatch();\n  const preferences = useSelector((state) => state.app.preferences);\n  const [fetchingToken, toggleFetchingToken] = useState(false);\n  const [refreshingToken, toggleRefreshingToken] = useState(false);\n  const [fetchingAuthorizationCode, toggleFetchingAuthorizationCode] = useState(false);\n\n  const useSystemBrowser = get(preferences, 'request.oauth2.useSystemBrowser', false);\n\n  // Check for pending authorization when component mounts or when fetching starts\n  useEffect(() => {\n    if (useSystemBrowser && fetchingToken) {\n      const getRequestStatus = async () => {\n        try {\n          toggleFetchingAuthorizationCode(await dispatch(isOauth2AuthorizationRequestInProgress()));\n        } catch (err) {\n          console.error('Error checking pending authorization:', err);\n        }\n      };\n      getRequestStatus();\n    }\n  }, [useSystemBrowser, fetchingToken, dispatch]);\n\n  const interpolatedAccessTokenUrl = useMemo(() => {\n    const variables = getAllVariables(collection, item);\n    return interpolate(accessTokenUrl, variables);\n  }, [collection, item, accessTokenUrl]);\n\n  const credentialsData = find(collection?.oauth2Credentials, (creds) => creds?.url == interpolatedAccessTokenUrl && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId);\n  const creds = credentialsData?.credentials || {};\n\n  const handleFetchOauth2Credentials = async () => {\n    let requestCopy = cloneDeep(request);\n    requestCopy.oauth2 = requestCopy?.auth.oauth2;\n    requestCopy.headers = {};\n    toggleFetchingToken(true);\n    try {\n      const result = await dispatch(fetchOauth2Credentials({\n        itemUid: item.uid,\n        request: requestCopy,\n        collection,\n        forceGetToken: true\n      }));\n\n      // Check if the result contains error or if access_token is missing\n      if (!result || !result.access_token) {\n        const errorMessage = result?.error || 'No access token received from authorization server';\n        console.error(errorMessage);\n        toast.error(errorMessage);\n        return;\n      }\n\n      toast.success('Token fetched successfully!');\n    } catch (error) {\n      console.error('could not fetch the token!');\n      console.error(error);\n      // Don't show error toast for user cancellation\n      if (error?.message && error.message.includes('cancelled by user')) {\n        return;\n      }\n      toast.error(error?.message || 'An error occurred while fetching token!');\n    } finally {\n      toggleFetchingToken(false);\n      toggleFetchingAuthorizationCode(false);\n    }\n  };\n\n  const handleRefreshAccessToken = async () => {\n    let requestCopy = cloneDeep(request);\n    requestCopy.oauth2 = requestCopy?.auth.oauth2;\n    requestCopy.headers = {};\n    toggleRefreshingToken(true);\n    try {\n      const result = await dispatch(refreshOauth2Credentials({\n        itemUid: item.uid,\n        request: requestCopy,\n        collection,\n        forceGetToken: true\n      }));\n\n      toggleRefreshingToken(false);\n\n      // Check if the result contains error or if access_token is missing\n      if (!result || !result.access_token) {\n        const errorMessage = result?.error || 'No access token received from authorization server';\n        console.error(errorMessage);\n        toast.error(errorMessage);\n        return;\n      }\n\n      toast.success('Token refreshed successfully!');\n    } catch (error) {\n      console.error(error);\n      toggleRefreshingToken(false);\n      toast.error(error?.message || 'An error occurred while refreshing token!');\n    }\n  };\n\n  const handleClearCache = (e) => {\n    dispatch(clearOauth2Cache({ collectionUid: collection?.uid, url: interpolatedAccessTokenUrl, credentialsId }))\n      .then(() => {\n        toast.success('Cleared cache successfully');\n      })\n      .catch((err) => {\n        toast.error(err.message);\n      });\n  };\n\n  const handleCancelAuthorization = async () => {\n    try {\n      const result = await dispatch(cancelOauth2AuthorizationRequest());\n      if (result.success && result.cancelled) {\n        toast.error('Authorization cancelled');\n        toggleFetchingToken(false);\n        toggleFetchingAuthorizationCode(false);\n      }\n    } catch (err) {\n      console.error('Error cancelling authorization:', err);\n      toast.error('Failed to cancel authorization');\n    }\n  };\n\n  return (\n    <div className=\"flex flex-row gap-2 mt-4\">\n      <Button\n        size=\"sm\"\n        color=\"secondary\"\n        onClick={handleFetchOauth2Credentials}\n        disabled={fetchingToken || refreshingToken}\n        loading={fetchingToken}\n      >\n        Get Access Token\n      </Button>\n      {creds?.refresh_token\n        ? (\n            <Button\n              size=\"sm\"\n              color=\"secondary\"\n              onClick={handleRefreshAccessToken}\n              disabled={fetchingToken || refreshingToken}\n              loading={refreshingToken}\n            >\n              Refresh Token\n            </Button>\n          )\n        : null}\n      {useSystemBrowser && fetchingAuthorizationCode\n        ? (\n            <Button\n              size=\"sm\"\n              color=\"secondary\"\n              onClick={handleCancelAuthorization}\n              icon={<IconX size={16} />}\n              iconPosition=\"left\"\n            >\n              Cancel Authorization\n            </Button>\n          ) : null}\n      <Button\n        size=\"sm\"\n        color=\"secondary\"\n        variant=\"ghost\"\n        onClick={handleClearCache}\n      >\n        Clear Cache\n      </Button>\n    </div>\n  );\n};\n\nexport default Oauth2ActionButtons;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2TokenViewer/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst Wrapper = styled.div`\n  .oauth2-copy-button {\n    background-color: ${(props) => rgba(props.theme.primary.solid, 0.1)};\n    \n    &:hover {\n      background-color: ${(props) => rgba(props.theme.primary.solid, 0.2)};\n    }\n  }\n\n  ol[role=\"tree\"] {\n    overflow: hidden;\n  }\n  ol[role=\"group\"] span {\n    line-break: anywhere;\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/OAuth2/Oauth2TokenViewer/index.js",
    "content": "import { useState, useEffect, useMemo } from 'react';\nimport { find } from 'lodash';\nimport StyledWrapper from './StyledWrapper';\nimport { IconChevronDown, IconChevronRight, IconCopy, IconCheck } from '@tabler/icons';\nimport { getAllVariables } from 'utils/collections/index';\nimport { interpolate } from '@usebruno/common';\n\nconst TokenSection = ({ title, token }) => {\n  if (!token) return null;\n\n  const [isExpanded, setIsExpanded] = useState(false);\n  const [decodedToken, setDecodedToken] = useState(null);\n  const [copied, setCopied] = useState(false);\n\n  useEffect(() => {\n    if (token) {\n      try {\n        const parts = token.split('.');\n        if (parts.length === 3) {\n          const payload = JSON.parse(atob(parts[1]));\n          setDecodedToken(payload);\n        }\n      } catch (err) {\n        console.error('Error decoding token:', err);\n      }\n    }\n  }, [token]);\n\n  const handleCopy = async (text) => {\n    await navigator.clipboard.writeText(text);\n    setCopied(true);\n    setTimeout(() => setCopied(false), 2000);\n  };\n\n  return (\n    <div className=\"mb-2 border dark:border-gray-700 rounded-lg overflow-hidden\">\n      <div\n        className=\"flex items-center justify-between px-3 py-2 bg-gray-50 dark:bg-gray-800 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-750 transition-colors\"\n        onClick={() => setIsExpanded(!isExpanded)}\n      >\n        <div className=\"flex items-center space-x-2 w-full\">\n          {isExpanded\n            ? <IconChevronDown size={18} className=\"text-gray-500\" />\n            : <IconChevronRight size={18} className=\"text-gray-500\" />}\n          <div className=\"flex flex-row justify-between w-full\">\n            <h3 className=\"font-medium\">{title}</h3>\n            {decodedToken?.exp && <ExpiryTimer expiresIn={decodedToken?.exp} />}\n          </div>\n        </div>\n      </div>\n      {isExpanded && (\n        <div className=\"p-3\">\n          <div className=\"relative group\">\n            <div className=\"absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity\">\n              <button\n                onClick={() => handleCopy(token)}\n                className=\"p-1 oauth2-copy-button rounded\"\n                title=\"Copy token\"\n              >\n                {copied\n                  ? <IconCheck size={16} className=\"text-green-700\" />\n                  : <IconCopy size={16} className=\"text-gray-500\" />}\n              </button>\n            </div>\n            <div className=\"font-mono text-xs bg-gray-50 dark:bg-gray-800 p-2 rounded break-all\">\n              {token}\n            </div>\n          </div>\n          {decodedToken && (\n            <div className=\"mt-3\">\n              <div className=\"text-xs font-medium text-gray-500 dark:text-gray-400 mb-2\">Decoded Payload</div>\n              <div className=\"grid grid-cols-2 gap-x-4 gap-y-1\">\n                {Object.entries(decodedToken).map(([key, value]) => (\n                  <div key={key} className=\"overflow-hidden text-ellipsis\">\n                    <span className=\"font-medium text-xs\">{key}: </span>\n                    <span className=\"text-xs text-gray-600 dark:text-gray-300\">\n                      {typeof value === 'object' ? JSON.stringify(value) : value.toString()}\n                    </span>\n                  </div>\n                ))}\n              </div>\n            </div>\n          )}\n        </div>\n      )}\n    </div>\n  );\n};\n\nconst formatExpiryTime = (seconds) => {\n  if (seconds < 60) return `${seconds}s`;\n  if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;\n  return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;\n};\n\nconst ExpiryTimer = ({ expiresIn }) => {\n  if (!expiresIn) return null;\n\n  const calculateTimeLeft = () => Math.max(0, Math.floor(expiresIn - Date.now() / 1000));\n\n  const [timeLeft, setTimeLeft] = useState(calculateTimeLeft);\n\n  useEffect(() => {\n    setTimeLeft(calculateTimeLeft());\n\n    const timer = setInterval(() => {\n      setTimeLeft((prev) => (prev > 0 ? prev - 1 : 0));\n    }, 1000);\n\n    return () => clearInterval(timer);\n  }, [expiresIn]);\n\n  return (\n    <div\n      className={`text-xs px-2 py-1 rounded-full min-w-[120px] text-center ${timeLeft <= 30\n        ? 'bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400'\n        : 'bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400'\n      }`}\n    >\n      {timeLeft > 0 ? `Expires in ${formatExpiryTime(timeLeft)}` : `Expired`}\n    </div>\n  );\n};\n\nconst Oauth2TokenViewer = ({ collection, item, url, credentialsId, handleRun }) => {\n  const { uid: collectionUid } = collection;\n\n  const interpolatedUrl = useMemo(() => {\n    const variables = getAllVariables(collection, item);\n    return interpolate(url, variables);\n  }, [collection, item, url]);\n\n  const credentialsData = find(collection?.oauth2Credentials, (creds) => creds?.url == interpolatedUrl && creds?.collectionUid == collectionUid && creds?.credentialsId == credentialsId);\n  const creds = credentialsData?.credentials || {};\n\n  return (\n    <StyledWrapper className=\"relative w-auto h-fit mt-2\">\n      {Object.keys(creds)?.length ? (\n        creds?.error ? (\n          <pre className=\"text-red-600 dark:text-red-400\">Error fetching token. Check network logs for more details.</pre>\n        ) : (\n          <div className=\"border border-gray-200 dark:border-gray-700 rounded-lg p-4 shadow-sm\">\n            <TokenSection title=\"Access Token\" token={creds.access_token} />\n            <TokenSection title=\"Refresh Token\" token={creds.refresh_token} />\n            <TokenSection title=\"ID Token\" token={creds.id_token} />\n            {(creds.token_type || creds.scope) ? (\n              <div className=\"mt-3 p-2 bg-gray-50 dark:bg-gray-800 rounded-lg text-xs\">\n                <div className=\"grid grid-cols-2 gap-2\">\n                  {creds.token_type ? (\n                    <div className=\"flex items-center space-x-1\">\n                      <span className=\"font-medium\">Token Type:</span>\n                      <span className=\"text-gray-600 dark:text-gray-300\">{creds.token_type}</span>\n                    </div>\n                  ) : null}\n                  {creds?.scope ? (\n                    <div className=\"flex items-center space-x-1 min-w-0\">\n                      <span className=\"font-medium flex-shrink-0\">Scope:</span>\n                      <span className=\"text-gray-600 dark:text-gray-300 truncate\" title={creds.scope}>\n                        {creds.scope}\n                      </span>\n                    </div>\n                  ) : null}\n                </div>\n              </div>\n            ) : null}\n          </div>\n        )\n      ) : (\n        <div className=\"text-gray-500 dark:text-gray-400\">No token found</div>\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default Oauth2TokenViewer;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst Wrapper = styled.div`\n  .oauth2-icon-container {\n    background-color: ${(props) => rgba(props.theme.primary.solid, 0.1)};\n  }\n\n  .oauth2-icon {\n    color: ${(props) => props.theme.primary.text};\n  }\n\n  label {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.subtext1};\n  }\n  .single-line-editor-wrapper {\n    max-width: 400px;\n    padding: 0.15rem 0.4rem;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n  }\n\n  .token-placement-selector {\n    font-size: ${(props) => props.theme.font.size.sm};\n    padding: 0.2rem 0px;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n    min-width: 100px;\n\n    .dropdown {\n      width: fit-content;\n      min-width: 100px;\n\n      div[data-tippy-root] {\n        width: fit-content;\n        min-width: 100px;\n      }\n      .tippy-box {\n        width: fit-content;\n        max-width: none !important;\n        min-width: 100px;\n\n        .tippy-content: {\n          width: fit-content;\n          max-width: none !important;\n          min-width: 100px;\n        }\n      }\n    }\n\n    .token-placement-label {\n      width: fit-content;\n      // color: ${(props) => props.theme.colors.text.yellow};\n      justify-content: space-between;\n      padding: 0 0.5rem;\n      min-width: 100px;\n    }\n\n    .dropdown-item {\n      padding: 0.2rem 0.6rem !important;\n    }\n  }\n\n  input[type='checkbox'] {\n    cursor: pointer;\n    accent-color: ${(props) => props.theme.primary.solid};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/index.js",
    "content": "import React from 'react';\nimport { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';\nimport get from 'lodash/get';\nimport { useTheme } from 'providers/Theme';\nimport { useDispatch } from 'react-redux';\nimport { IconCaretDown, IconSettings, IconKey, IconAdjustmentsHorizontal, IconHelp } from '@tabler/icons';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport StyledWrapper from './StyledWrapper';\nimport { inputsConfig } from './inputsConfig';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport Oauth2TokenViewer from '../Oauth2TokenViewer/index';\nimport Oauth2ActionButtons from '../Oauth2ActionButtons/index';\nimport AdditionalParams from '../AdditionalParams/index';\nimport SensitiveFieldWarning from 'components/SensitiveFieldWarning/index';\n\nconst OAuth2PasswordCredentials = ({ save, item = {}, request, handleRun, updateAuth, collection }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n  const oAuth = get(request, 'auth.oauth2', {});\n  const { isSensitive } = useDetectSensitiveField(collection);\n\n  const {\n    accessTokenUrl,\n    username,\n    password,\n    clientId,\n    clientSecret,\n    scope,\n    credentialsPlacement,\n    credentialsId,\n    tokenPlacement,\n    tokenHeaderPrefix,\n    tokenQueryKey,\n    refreshTokenUrl,\n    autoRefreshToken,\n    autoFetchToken,\n    tokenSource,\n    additionalParameters\n  } = oAuth;\n\n  const refreshTokenUrlAvailable = refreshTokenUrl?.trim() !== '';\n  const isAutoRefreshDisabled = !refreshTokenUrlAvailable;\n\n  const handleSave = () => { save(); };\n\n  const handleChange = (key, value) => {\n    dispatch(\n      updateAuth({\n        mode: 'oauth2',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          grantType: 'password',\n          accessTokenUrl,\n          username,\n          password,\n          clientId,\n          clientSecret,\n          scope,\n          credentialsPlacement,\n          credentialsId,\n          tokenPlacement,\n          tokenHeaderPrefix,\n          tokenQueryKey,\n          refreshTokenUrl,\n          autoRefreshToken,\n          autoFetchToken,\n          tokenSource,\n          additionalParameters,\n          [key]: value\n        }\n      })\n    );\n  };\n\n  return (\n    <StyledWrapper className=\"mt-2 flex w-full gap-4 flex-col\">\n      <Oauth2TokenViewer handleRun={handleRun} collection={collection} item={item} url={accessTokenUrl} credentialsId={credentialsId} />\n      <div className=\"flex items-center gap-2.5 mt-2\">\n        <div className=\"flex items-center px-2.5 py-1.5 oauth2-icon-container rounded-md\">\n          <IconSettings size={14} className=\"oauth2-icon\" />\n        </div>\n        <span className=\"oauth2-section-label\">\n          Configuration\n        </span>\n      </div>\n      {inputsConfig.map((input) => {\n        const { key, label, isSecret } = input;\n        const value = oAuth[key] || '';\n        const { showWarning, warningMessage } = isSensitive(value);\n\n        return (\n          <div className=\"flex items-center gap-4 w-full\" key={`input-${key}`}>\n            <label className=\"block min-w-[140px]\">{label}</label>\n            <div className=\"single-line-editor-wrapper flex-1 flex items-center\">\n              <SingleLineEditor\n                value={value}\n                theme={storedTheme}\n                onSave={handleSave}\n                onChange={(val) => handleChange(key, val)}\n                onRun={handleRun}\n                collection={collection}\n                item={item}\n                isSecret={isSecret}\n                isCompact\n              />\n              {isSecret && showWarning && <SensitiveFieldWarning fieldName={key} warningMessage={warningMessage} />}\n            </div>\n          </div>\n        );\n      })}\n      <div className=\"flex items-center gap-4 w-full\" key=\"input-credentials-placement\">\n        <label className=\"block min-w-[140px]\">Add Credentials to</label>\n        <div className=\"inline-flex items-center cursor-pointer token-placement-selector\">\n          <MenuDropdown\n            items={[\n              { id: 'body', label: 'Request Body', onClick: () => handleChange('credentialsPlacement', 'body') },\n              { id: 'basic_auth_header', label: 'Basic Auth Header', onClick: () => handleChange('credentialsPlacement', 'basic_auth_header') }\n            ]}\n            selectedItemId={credentialsPlacement}\n            placement=\"bottom-end\"\n          >\n            <div className=\"flex items-center justify-end token-placement-label select-none\">\n              {credentialsPlacement == 'body' ? 'Request Body' : 'Basic Auth Header'}\n              <IconCaretDown className=\"caret ml-1 mr-1\" size={14} strokeWidth={2} />\n            </div>\n          </MenuDropdown>\n        </div>\n      </div>\n      <div className=\"flex items-center gap-2.5 mt-2\">\n        <div className=\"flex items-center px-2.5 py-1.5 oauth2-icon-container rounded-md\">\n          <IconKey size={14} className=\"oauth2-icon\" />\n        </div>\n        <span className=\"oauth2-section-label\">\n          Token\n        </span>\n      </div>\n      <div className=\"flex items-center gap-4 w-full\" key=\"input-token-type\">\n        <label className=\"block min-w-[140px]\">Token Source</label>\n        <div className=\"inline-flex items-center cursor-pointer token-placement-selector\">\n          <MenuDropdown\n            items={[\n              { id: 'access_token', label: 'Access Token', onClick: () => handleChange('tokenSource', 'access_token') },\n              { id: 'id_token', label: 'ID Token', onClick: () => handleChange('tokenSource', 'id_token') }\n            ]}\n            selectedItemId={tokenSource}\n            placement=\"bottom-end\"\n          >\n            <div className=\"flex items-center justify-end token-placement-label select-none\">\n              {tokenSource === 'id_token' ? 'ID Token' : 'Access Token'}\n              <IconCaretDown className=\"caret ml-1 mr-1\" size={14} strokeWidth={2} />\n            </div>\n          </MenuDropdown>\n        </div>\n      </div>\n      <div className=\"flex items-center gap-4 w-full\" key=\"input-token-name\">\n        <label className=\"block min-w-[140px]\">Token ID</label>\n        <div className=\"single-line-editor-wrapper flex-1\">\n          <SingleLineEditor\n            value={oAuth['credentialsId'] || ''}\n            theme={storedTheme}\n            onSave={handleSave}\n            onChange={(val) => handleChange('credentialsId', val)}\n            onRun={handleRun}\n            collection={collection}\n            item={item}\n            isCompact\n          />\n        </div>\n      </div>\n      <div className=\"flex items-center gap-4 w-full\" key=\"input-token-placement\">\n        <label className=\"block min-w-[140px]\">Add token to</label>\n        <div className=\"inline-flex items-center cursor-pointer token-placement-selector\">\n          <MenuDropdown\n            items={[\n              { id: 'header', label: 'Header', onClick: () => handleChange('tokenPlacement', 'header') },\n              { id: 'url', label: 'URL', onClick: () => handleChange('tokenPlacement', 'url') }\n            ]}\n            selectedItemId={tokenPlacement}\n            placement=\"bottom-end\"\n          >\n            <div className=\"flex items-center justify-end token-placement-label select-none\">\n              {tokenPlacement == 'url' ? 'URL' : 'Headers'}\n              <IconCaretDown className=\"caret ml-1 mr-1\" size={14} strokeWidth={2} />\n            </div>\n          </MenuDropdown>\n        </div>\n      </div>\n      {\n        tokenPlacement === 'header'\n          ? (\n              <div className=\"flex items-center gap-4 w-full\" key=\"input-token-prefix\">\n                <label className=\"block min-w-[140px]\">Header Prefix</label>\n                <div className=\"single-line-editor-wrapper flex-1\">\n                  <SingleLineEditor\n                    value={oAuth['tokenHeaderPrefix'] || ''}\n                    theme={storedTheme}\n                    onSave={handleSave}\n                    onChange={(val) => handleChange('tokenHeaderPrefix', val)}\n                    onRun={handleRun}\n                    collection={collection}\n                    isCompact\n                  />\n                </div>\n              </div>\n            )\n          : (\n              <div className=\"flex items-center gap-4 w-full\" key=\"input-token-query-param-key\">\n                <label className=\"block min-w-[140px]\">Query Param Key</label>\n                <div className=\"single-line-editor-wrapper flex-1\">\n                  <SingleLineEditor\n                    value={oAuth['tokenQueryKey'] || ''}\n                    theme={storedTheme}\n                    onSave={handleSave}\n                    onChange={(val) => handleChange('tokenQueryKey', val)}\n                    onRun={handleRun}\n                    collection={collection}\n                    isCompact\n                  />\n                </div>\n              </div>\n            )\n      }\n      <div className=\"flex items-center gap-2.5 mt-4 mb-2\">\n        <div className=\"flex items-center px-2.5 py-1.5 oauth2-icon-container rounded-md\">\n          <IconAdjustmentsHorizontal size={14} className=\"oauth2-icon\" />\n        </div>\n        <span className=\"oauth2-section-label\">\n          Advanced Settings\n        </span>\n      </div>\n\n      <div className=\"flex items-center gap-4 w-full mb-4\">\n        <label className=\"block min-w-[140px]\">Refresh Token URL</label>\n        <div className=\"single-line-editor-wrapper flex-1\">\n          <SingleLineEditor\n            value={get(request, 'auth.oauth2.refreshTokenUrl', '')}\n            theme={storedTheme}\n            onSave={handleSave}\n            onChange={(val) => handleChange('refreshTokenUrl', val)}\n            collection={collection}\n            item={item}\n            isCompact\n          />\n        </div>\n      </div>\n\n      <div className=\"flex items-center gap-2.5 mt-4\">\n        <div className=\"flex items-center px-2.5 py-1.5 oauth2-icon-container rounded-md\">\n          <IconSettings size={14} className=\"oauth2-icon\" />\n        </div>\n        <span className=\"oauth2-section-label\">Settings</span>\n      </div>\n\n      {/* Automatically Fetch Token */}\n      <div className=\"flex items-center gap-4 w-full\">\n        <input\n          type=\"checkbox\"\n          checked={Boolean(autoFetchToken)}\n          onChange={(e) => handleChange('autoFetchToken', e.target.checked)}\n          className=\"cursor-pointer ml-1\"\n        />\n        <label className=\"block min-w-[140px]\">Automatically fetch token if not found</label>\n        <div className=\"flex items-center gap-2\">\n          <div className=\"relative group cursor-pointer\">\n            <IconHelp size={16} className=\"text-gray-500\" />\n            <span className=\"group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200\">\n              Automatically fetch a new token when you try to access a resource and don't have one.\n            </span>\n          </div>\n        </div>\n      </div>\n\n      {/* Auto Refresh Token (With Refresh URL) */}\n      <div className=\"flex items-center gap-4 w-full\">\n        <input\n          type=\"checkbox\"\n          checked={Boolean(autoRefreshToken)}\n          onChange={(e) => handleChange('autoRefreshToken', e.target.checked)}\n          className={`cursor-pointer ml-1 ${isAutoRefreshDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}\n          disabled={isAutoRefreshDisabled}\n        />\n        <label className={`block min-w-[140px] ${isAutoRefreshDisabled ? 'text-gray-500' : ''}`}>Auto refresh token (with refresh URL)</label>\n        <div className=\"flex items-center gap-2\">\n          <div className=\"relative group cursor-pointer\">\n            <IconHelp size={16} className=\"text-gray-500\" />\n            <span className=\"group-hover:opacity-100 pointer-events-none opacity-0 max-w-60 absolute left-0 bottom-full mb-1 w-max p-2 bg-gray-700 text-white text-xs rounded-md transition-opacity duration-200\">\n              Automatically refresh your token using the refresh URL when it expires.\n            </span>\n          </div>\n        </div>\n      </div>\n      <AdditionalParams\n        item={item}\n        request={request}\n        collection={collection}\n        updateAuth={updateAuth}\n        handleSave={handleSave}\n      />\n      <Oauth2ActionButtons item={item} request={request} collection={collection} url={accessTokenUrl} credentialsId={credentialsId} />\n    </StyledWrapper>\n  );\n};\n\nexport default OAuth2PasswordCredentials;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/OAuth2/PasswordCredentials/inputsConfig.js",
    "content": "const inputsConfig = [\n  {\n    key: 'accessTokenUrl',\n    label: 'Access Token URL'\n  },\n  {\n    key: 'username',\n    label: 'Username'\n  },\n  {\n    key: 'password',\n    label: 'Password',\n    isSecret: true\n  },\n  {\n    key: 'clientId',\n    label: 'Client ID'\n  },\n  {\n    key: 'clientSecret',\n    label: 'Client Secret',\n    isSecret: true\n  },\n  {\n    key: 'scope',\n    label: 'Scope'\n  }\n];\n\nexport { inputsConfig };\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/OAuth2/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  label {\n    font-size: ${(props) => props.theme.font.size.base};\n  }\n  .single-line-editor-wrapper {\n    max-width: 400px;\n    padding: 0.15rem 0.4rem;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n  }\n\n  input[type='checkbox'] {\n    cursor: pointer;\n    accent-color: ${(props) => props.theme.primary.solid};\n  }\n\n  .oauth2-section-label {\n    color: ${(props) => props.theme.text};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/OAuth2/index.js",
    "content": "import React from 'react';\nimport get from 'lodash/get';\nimport StyledWrapper from './StyledWrapper';\nimport GrantTypeSelector from './GrantTypeSelector/index';\nimport OAuth2PasswordCredentials from './PasswordCredentials/index';\nimport OAuth2AuthorizationCode from './AuthorizationCode/index';\nimport OAuth2Implicit from './Implicit/index';\nimport OAuth2ClientCredentials from './ClientCredentials/index';\nimport { updateAuth } from 'providers/ReduxStore/slices/collections';\nimport { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport { useDispatch } from 'react-redux';\n\nconst GrantTypeComponentMap = ({ item, collection }) => {\n  const dispatch = useDispatch();\n\n  const save = () => {\n    dispatch(saveRequest(item.uid, collection.uid));\n  };\n\n  let request = item.draft ? get(item, 'draft.request', {}) : get(item, 'request', {});\n  const grantType = get(request, 'auth.oauth2.grantType', {});\n\n  const handleRun = async () => {\n    dispatch(sendRequest(item, collection.uid));\n  };\n\n  switch (grantType) {\n    case 'password':\n      return <OAuth2PasswordCredentials item={item} save={save} request={request} handleRun={handleRun} updateAuth={updateAuth} collection={collection} />;\n      break;\n    case 'authorization_code':\n      return <OAuth2AuthorizationCode item={item} save={save} request={request} handleRun={handleRun} updateAuth={updateAuth} collection={collection} />;\n      break;\n    case 'implicit':\n      return <OAuth2Implicit item={item} save={save} request={request} handleRun={handleRun} updateAuth={updateAuth} collection={collection} />;\n      break;\n    case 'client_credentials':\n      return <OAuth2ClientCredentials item={item} save={save} request={request} handleRun={handleRun} updateAuth={updateAuth} collection={collection} />;\n      break;\n    default:\n      return <div>TBD</div>;\n      break;\n  }\n};\n\nconst OAuth2 = ({ item, collection }) => {\n  let request = item.draft ? get(item, 'draft.request', {}) : get(item, 'request', {});\n\n  return (\n    <StyledWrapper className=\"w-full\">\n      <GrantTypeSelector item={item} request={request} updateAuth={updateAuth} collection={collection} />\n      <GrantTypeComponentMap item={item} collection={collection} />\n    </StyledWrapper>\n  );\n};\n\nexport default OAuth2;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  .inherit-mode-text {\n    color: ${(props) => props.theme.primary.text};\n  }\n  .inherit-mode-label {\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  label {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.subtext1};\n  }\n\n  .single-line-editor-wrapper {\n    max-width: 400px;\n    padding: 0.15rem 0.4rem;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/WsseAuth/index.js",
    "content": "import React from 'react';\nimport SensitiveFieldWarning from 'components/SensitiveFieldWarning';\nimport { useDetectSensitiveField } from 'hooks/useDetectSensitiveField';\nimport get from 'lodash/get';\nimport { useTheme } from 'providers/Theme';\nimport { useDispatch } from 'react-redux';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport { updateAuth } from 'providers/ReduxStore/slices/collections';\nimport { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport StyledWrapper from './StyledWrapper';\n\nconst WsseAuth = ({ item, collection, updateAuth, request, save }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n\n  const wsseAuth = get(request, 'auth.wsse', {});\n  const { isSensitive } = useDetectSensitiveField(collection);\n  const { showWarning, warningMessage } = isSensitive(wsseAuth?.password);\n\n  const handleRun = () => dispatch(sendRequest(item, collection.uid));\n\n  const handleSave = () => {\n    save();\n  };\n\n  const handleUserChange = (username) => {\n    dispatch(\n      updateAuth({\n        mode: 'wsse',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          username: username || '',\n          password: wsseAuth.password || ''\n        }\n      })\n    );\n  };\n\n  const handlePasswordChange = (password) => {\n    dispatch(\n      updateAuth({\n        mode: 'wsse',\n        collectionUid: collection.uid,\n        itemUid: item.uid,\n        content: {\n          username: wsseAuth.username || '',\n          password: password || ''\n        }\n      })\n    );\n  };\n\n  return (\n    <StyledWrapper className=\"w-full\">\n      <label className=\"block mb-1\">Username</label>\n      <div className=\"single-line-editor-wrapper mb-3\">\n        <SingleLineEditor\n          value={wsseAuth.username || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handleUserChange(val)}\n          onRun={handleRun}\n          collection={collection}\n          item={item}\n          isCompact\n        />\n      </div>\n\n      <label className=\"block mb-1\">Password</label>\n      <div className=\"single-line-editor-wrapper flex items-center\">\n        <SingleLineEditor\n          value={wsseAuth.password || ''}\n          theme={storedTheme}\n          onSave={handleSave}\n          onChange={(val) => handlePasswordChange(val)}\n          onRun={handleRun}\n          collection={collection}\n          item={item}\n          isSecret={true}\n          isCompact\n        />\n        {showWarning && <SensitiveFieldWarning fieldName=\"wsse-password\" warningMessage={warningMessage} />}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default WsseAuth;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Auth/index.js",
    "content": "import React from 'react';\nimport get from 'lodash/get';\nimport AwsV4Auth from './AwsV4Auth';\nimport BearerAuth from './BearerAuth';\nimport BasicAuth from './BasicAuth';\nimport DigestAuth from './DigestAuth';\nimport WsseAuth from './WsseAuth';\nimport NTLMAuth from './NTLMAuth';\nimport { updateAuth } from 'providers/ReduxStore/slices/collections';\nimport { saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport { useDispatch } from 'react-redux';\n\nimport ApiKeyAuth from './ApiKeyAuth';\nimport StyledWrapper from './StyledWrapper';\nimport { humanizeRequestAuthMode } from 'utils/collections';\nimport OAuth2 from './OAuth2/index';\nimport { findItemInCollection, findParentItemInCollection } from 'utils/collections/index';\n\nconst getTreePathFromCollectionToItem = (collection, _item) => {\n  let path = [];\n  let item = findItemInCollection(collection, _item?.uid);\n  while (item) {\n    path.unshift(item);\n    item = findParentItemInCollection(collection, item?.uid);\n  }\n  return path;\n};\n\nconst Auth = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');\n  const requestTreePath = getTreePathFromCollectionToItem(collection, item);\n\n  // Create a request object to pass to the auth components\n  const request = item.draft\n    ? get(item, 'draft.request', {})\n    : get(item, 'request', {});\n\n  // Save function for request level\n  const save = () => {\n    return dispatch(saveRequest(item.uid, collection.uid));\n  };\n\n  const getEffectiveAuthSource = () => {\n    if (authMode !== 'inherit') return null;\n\n    const collectionRoot = collection?.draft?.root || collection?.root || {};\n    const collectionAuth = get(collectionRoot, 'request.auth');\n    let effectiveSource = {\n      type: 'collection',\n      name: 'Collection',\n      auth: collectionAuth\n    };\n\n    // Check folders in reverse to find the closest auth configuration\n    for (let i of [...requestTreePath].reverse()) {\n      if (i.type === 'folder') {\n        const folderAuth = get(i, 'root.request.auth');\n        if (folderAuth && folderAuth.mode && folderAuth.mode !== 'inherit') {\n          effectiveSource = {\n            type: 'folder',\n            name: i.name,\n            auth: folderAuth\n          };\n          break;\n        }\n      }\n    }\n\n    return effectiveSource;\n  };\n\n  const getAuthView = () => {\n    switch (authMode) {\n      case 'none': {\n        return <div className=\"mt-2\">No Auth</div>;\n      }\n      case 'awsv4': {\n        return <AwsV4Auth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;\n      }\n      case 'basic': {\n        return <BasicAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;\n      }\n      case 'bearer': {\n        return <BearerAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;\n      }\n      case 'digest': {\n        return <DigestAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;\n      }\n      case 'ntlm': {\n        return <NTLMAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;\n      }\n      case 'oauth2': {\n        return <OAuth2 collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;\n      }\n      case 'wsse': {\n        return <WsseAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;\n      }\n      case 'apikey': {\n        return <ApiKeyAuth collection={collection} item={item} request={request} save={save} updateAuth={updateAuth} />;\n      }\n      case 'inherit': {\n        const source = getEffectiveAuthSource();\n        return (\n          <>\n            <div className=\"flex flex-row w-full gap-2\">\n              <div>Auth inherited from {source.name}: </div>\n              <div className=\"inherit-mode-text\">{humanizeRequestAuthMode(source.auth?.mode)}</div>\n            </div>\n          </>\n        );\n      }\n    }\n  };\n\n  return (\n    <StyledWrapper className=\"w-full overflow-auto\">\n      {getAuthView()}\n    </StyledWrapper>\n  );\n};\n\nexport default Auth;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/FileBody/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  table {\n    width: 100%;\n    border-collapse: collapse;\n    font-weight: 500;\n    table-layout: fixed;\n\n    thead,\n    td {\n      border: 1px solid ${(props) => props.theme.table.border};\n    }\n\n    thead {\n      color: ${(props) => props.theme.table.thead.color};\n      font-size: ${(props) => props.theme.font.size.base};\n      user-select: none;\n    }\n    td {\n      padding: 6px 10px;\n\n      &:nth-child(1) {\n        width: 30%;\n      }\n\n      &:nth-child(2) {\n        width: 45%;\n      }\n\n      &:nth-child(3) {\n        width: 25%;\n      }\n\n      &:nth-child(4) {\n        width: 70px;\n      }\n    }\n  }\n\n  .btn-add-param {\n    font-size: ${(props) => props.theme.font.size.base};\n  }\n\n  input[type='text'] {\n    width: 100%;\n    border: solid 1px transparent;\n    outline: none !important;\n    color: ${(props) => props.theme.table.input.color};\n    background: transparent;\n\n    &:focus {\n      outline: none !important;\n      border: solid 1px transparent;\n    }\n  }\n\n  input[type='radio'] {\n    cursor: pointer;\n    position: relative;\n    top: 1px;\n    accent-color: ${(props) => props.theme.primary.solid};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/FileBody/index.js",
    "content": "import React, { useState, useEffect } from 'react';\nimport { get, cloneDeep, isArray } from 'lodash';\nimport { IconTrash } from '@tabler/icons';\nimport { useDispatch } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport { addFile as _addFile, updateFile, deleteFile } from 'providers/ReduxStore/slices/collections/index';\nimport { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport StyledWrapper from './StyledWrapper';\nimport FilePickerEditor from 'components/FilePickerEditor/index';\nimport SingleLineEditor from 'components/SingleLineEditor/index';\n\nconst FileBody = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n  const params = item.draft ? get(item, 'draft.request.body.file') : get(item, 'request.body.file');\n\n  const [enabledFileUid, setEnableFileUid] = useState(params && params.length ? params[0].uid : '');\n\n  const addFile = () => {\n    dispatch(\n      _addFile({\n        itemUid: item.uid,\n        collectionUid: collection.uid\n      })\n    );\n  };\n\n  const onSave = () => dispatch(saveRequest(item.uid, collection.uid));\n  const handleRun = () => dispatch(sendRequest(item, collection.uid));\n\n  const handleParamChange = (e, _param, type) => {\n    const param = cloneDeep(_param);\n    switch (type) {\n      case 'filePath': {\n        param.filePath = e.target.filePath;\n        param.contentType = '';\n        break;\n      }\n      case 'contentType': {\n        param.contentType = e.target.contentType;\n        break;\n      }\n      case 'selected': {\n        param.selected = e.target.selected;\n        setEnableFileUid(param.uid);\n        break;\n      }\n    }\n    dispatch(\n      updateFile({\n        param: param,\n        itemUid: item.uid,\n        collectionUid: collection.uid\n      })\n    );\n  };\n\n  const handleRemoveParams = (param) => {\n    dispatch(\n      deleteFile({\n        paramUid: param.uid,\n        itemUid: item.uid,\n        collectionUid: collection.uid\n      })\n    );\n  };\n\n  return (\n    <StyledWrapper className=\"w-full\">\n      <table>\n        <thead>\n          <tr>\n            <td>\n              <div className=\"flex items-center justify-center\">File</div>\n            </td>\n            <td>\n              <div className=\"flex items-center justify-center\">Content-Type</div>\n            </td>\n            <td>\n              <div className=\"flex items-center justify-center\">Selected</div>\n            </td>\n            <td></td>\n          </tr>\n        </thead>\n        <tbody>\n          {params && params.length\n            ? params.map((param, index) => {\n                return (\n                  <tr key={param.uid}>\n                    <td>\n                      <FilePickerEditor\n                        isSingleFilePicker={true}\n                        value={param.filePath}\n                        onChange={(path) =>\n                          handleParamChange(\n                            {\n                              target: {\n                                filePath: path\n                              }\n                            },\n                            param,\n                            'filePath'\n                          )}\n                        collection={collection}\n                        displayMode=\"labelAndIcon\"\n                      />\n                    </td>\n                    <td>\n                      <SingleLineEditor\n                        className=\"flex items-center justify-center\"\n                        onSave={onSave}\n                        theme={storedTheme}\n                        placeholder=\"Auto\"\n                        value={param.contentType}\n                        onChange={(newValue) =>\n                          handleParamChange(\n                            {\n                              target: {\n                                contentType: newValue\n                              }\n                            },\n                            param,\n                            'contentType'\n                          )}\n                        onRun={handleRun}\n                        collection={collection}\n                      />\n                    </td>\n                    <td>\n                      <div className=\"flex items-center justify-center\">\n                        <input\n                          key={param.uid}\n                          type=\"radio\"\n                          name=\"selected\"\n                          checked={enabledFileUid === param.uid || param.selected}\n                          tabIndex=\"-1\"\n                          className=\"mr-1 mousetrap\"\n                          onChange={(e) => handleParamChange(e, param, 'selected')}\n                        />\n                      </div>\n                    </td>\n                    <td>\n                      <div className=\"flex items-center justify-center\">\n                        <button tabIndex=\"-1\" onClick={() => handleRemoveParams(param)}>\n                          <IconTrash strokeWidth={1.5} size={20} />\n                        </button>\n                      </div>\n                    </td>\n                  </tr>\n                );\n              })\n            : null}\n        </tbody>\n      </table>\n      <div>\n        <button className=\"btn-add-param text-link pr-2 pt-3 select-none\" onClick={addFile}>\n          + Add File\n        </button>\n      </div>\n    </StyledWrapper>\n  );\n};\nexport default FileBody;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/FormUrlEncodedParams/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  table {\n    width: 100%;\n    border-collapse: collapse;\n    font-weight: 500;\n    table-layout: fixed;\n\n    thead,\n    td {\n      border: 1px solid ${(props) => props.theme.table.border};\n    }\n\n    thead {\n      color: ${(props) => props.theme.table.thead.color};\n      font-size: ${(props) => props.theme.font.size.base};\n      user-select: none;\n    }\n    td {\n      padding: 6px 10px;\n      }\n    }\n\n  .btn-add-param {\n    font-size: ${(props) => props.theme.font.size.base};\n  }\n\n  input[type='text'] {\n    width: 100%;\n    border: solid 1px transparent;\n    outline: none !important;\n    color: ${(props) => props.theme.table.input.color};\n    background: transparent;\n\n    &:focus {\n      outline: none !important;\n      border: solid 1px transparent;\n    }\n  }\n\n  input[type='checkbox'] {\n    cursor: pointer;\n    position: relative;\n    top: 1px;\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/FormUrlEncodedParams/index.js",
    "content": "import React, { useCallback } from 'react';\nimport get from 'lodash/get';\nimport { useDispatch } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport {\n  moveFormUrlEncodedParam,\n  setFormUrlEncodedParams\n} from 'providers/ReduxStore/slices/collections';\nimport MultiLineEditor from 'components/MultiLineEditor';\nimport { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport EditableTable from 'components/EditableTable';\nimport StyledWrapper from './StyledWrapper';\n\nconst FormUrlEncodedParams = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n  const params = item.draft ? get(item, 'draft.request.body.formUrlEncoded') : get(item, 'request.body.formUrlEncoded');\n\n  const onSave = () => dispatch(saveRequest(item.uid, collection.uid));\n  const handleRun = () => dispatch(sendRequest(item, collection.uid));\n\n  const handleParamsChange = useCallback((updatedParams) => {\n    dispatch(setFormUrlEncodedParams({\n      collectionUid: collection.uid,\n      itemUid: item.uid,\n      params: updatedParams\n    }));\n  }, [dispatch, collection.uid, item.uid]);\n\n  const handleParamDrag = useCallback(({ updateReorderedItem }) => {\n    dispatch(moveFormUrlEncodedParam({\n      collectionUid: collection.uid,\n      itemUid: item.uid,\n      updateReorderedItem\n    }));\n  }, [dispatch, collection.uid, item.uid]);\n\n  const columns = [\n    {\n      key: 'name',\n      name: 'Key',\n      isKeyField: true,\n      placeholder: 'Key',\n      width: '30%'\n    },\n    {\n      key: 'value',\n      name: 'Value',\n      placeholder: 'Value',\n      render: ({ value, onChange }) => (\n        <MultiLineEditor\n          value={value || ''}\n          theme={storedTheme}\n          onSave={onSave}\n          onChange={onChange}\n          allowNewlines={true}\n          onRun={handleRun}\n          collection={collection}\n          item={item}\n          placeholder={!value ? 'Value' : ''}\n        />\n      )\n    }\n  ];\n\n  const defaultRow = {\n    name: '',\n    value: '',\n    description: ''\n  };\n\n  return (\n    <StyledWrapper className=\"w-full\">\n      <EditableTable\n        columns={columns}\n        rows={params || []}\n        onChange={handleParamsChange}\n        defaultRow={defaultRow}\n        reorderable={true}\n        onReorder={handleParamDrag}\n      />\n    </StyledWrapper>\n  );\n};\n\nexport default FormUrlEncodedParams;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GraphQLRequestPane/index.js",
    "content": "import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react';\nimport find from 'lodash/find';\nimport get from 'lodash/get';\nimport classnames from 'classnames';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';\nimport QueryEditor from 'components/RequestPane/QueryEditor';\nimport Auth from 'components/RequestPane/Auth';\nimport GraphQLVariables from 'components/RequestPane/GraphQLVariables';\nimport RequestHeaders from 'components/RequestPane/RequestHeaders';\nimport Vars from 'components/RequestPane/Vars';\nimport Assertions from 'components/RequestPane/Assertions';\nimport Script from 'components/RequestPane/Script';\nimport Tests from 'components/RequestPane/Tests';\nimport { useTheme } from 'providers/Theme';\nimport { updateRequestGraphqlQuery } from 'providers/ReduxStore/slices/collections';\nimport { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport Documentation from 'components/Documentation/index';\nimport GraphQLSchemaActions from '../GraphQLSchemaActions/index';\nimport HeightBoundContainer from 'ui/HeightBoundContainer';\nimport Settings from 'components/RequestPane/Settings';\nimport ResponsiveTabs from 'ui/ResponsiveTabs';\nimport AuthMode from '../Auth/AuthMode/index';\n\nconst TAB_CONFIG = [\n  { key: 'query', label: 'Query' },\n  { key: 'variables', label: 'Variables' },\n  { key: 'headers', label: 'Headers' },\n  { key: 'auth', label: 'Auth' },\n  { key: 'vars', label: 'Vars' },\n  { key: 'script', label: 'Script' },\n  { key: 'assert', label: 'Assert' },\n  { key: 'tests', label: 'Tests' },\n  { key: 'docs', label: 'Docs' },\n  { key: 'settings', label: 'Settings' }\n];\n\nconst GraphQLRequestPane = ({ item, collection, onSchemaLoad, toggleDocs, handleGqlClickReference }) => {\n  const dispatch = useDispatch();\n  const tabs = useSelector((state) => state.tabs.tabs);\n  const activeTabUid = useSelector((state) => state.tabs.activeTabUid);\n  const preferences = useSelector((state) => state.app.preferences);\n\n  const query = item.draft\n    ? get(item, 'draft.request.body.graphql.query', '')\n    : get(item, 'request.body.graphql.query', '');\n  const variables = item.draft\n    ? get(item, 'draft.request.body.graphql.variables', '')\n    : get(item, 'request.body.graphql.variables', '');\n\n  const { displayedTheme } = useTheme();\n  const [schema, setSchema] = useState(null);\n  const schemaActionsRef = useRef(null);\n\n  const focusedTab = find(tabs, (t) => t.uid === activeTabUid);\n  const requestPaneTab = focusedTab?.requestPaneTab;\n\n  useEffect(() => {\n    onSchemaLoad(schema);\n  }, [schema, onSchemaLoad]);\n\n  const onQueryChange = useCallback(\n    (value) => {\n      dispatch(\n        updateRequestGraphqlQuery({\n          query: value,\n          itemUid: item.uid,\n          collectionUid: collection.uid\n        })\n      );\n    },\n    [dispatch, item.uid, collection.uid]\n  );\n\n  const onRun = useCallback(\n    () => dispatch(sendRequest(item, collection.uid)),\n    [dispatch, item, collection.uid]\n  );\n\n  const onSave = useCallback(\n    () => dispatch(saveRequest(item.uid, collection.uid)),\n    [dispatch, item.uid, collection.uid]\n  );\n\n  const selectTab = useCallback(\n    (tabKey) => {\n      dispatch(updateRequestPaneTab({ uid: item.uid, requestPaneTab: tabKey }));\n    },\n    [dispatch, item.uid]\n  );\n\n  const allTabs = useMemo(() => TAB_CONFIG.map(({ key, label }) => ({ key, label })), []);\n\n  const tabPanel = useMemo(() => {\n    switch (requestPaneTab) {\n      case 'query':\n        return (\n          <QueryEditor\n            collection={collection}\n            theme={displayedTheme}\n            schema={schema}\n            onSave={onSave}\n            value={query}\n            onRun={onRun}\n            onEdit={onQueryChange}\n            onClickReference={handleGqlClickReference}\n            font={get(preferences, 'font.codeFont', 'default')}\n            fontSize={get(preferences, 'font.codeFontSize')}\n          />\n        );\n      case 'variables':\n        return <GraphQLVariables item={item} variables={variables} collection={collection} />;\n      case 'headers':\n        return <RequestHeaders item={item} collection={collection} />;\n      case 'auth':\n        return <Auth item={item} collection={collection} />;\n      case 'vars':\n        return <Vars item={item} collection={collection} />;\n      case 'assert':\n        return <Assertions item={item} collection={collection} />;\n      case 'script':\n        return <Script item={item} collection={collection} />;\n      case 'tests':\n        return <Tests item={item} collection={collection} />;\n      case 'docs':\n        return <Documentation item={item} collection={collection} />;\n      case 'settings':\n        return <Settings item={item} collection={collection} />;\n      default:\n        return <div className=\"mt-4\">404 | Not found</div>;\n    }\n  }, [requestPaneTab, item, collection, displayedTheme, schema, onSave, query, onRun, onQueryChange, handleGqlClickReference, preferences, variables]);\n\n  if (!activeTabUid || !focusedTab?.uid || !requestPaneTab) {\n    return <div className=\"pb-4 px-4\">An error occurred!</div>;\n  }\n\n  const rightContent = requestPaneTab === 'auth' ? (\n    <div ref={schemaActionsRef} className=\"flex flex-grow justify-start items-center\">\n      <AuthMode item={item} collection={collection} />\n    </div>\n  ) : requestPaneTab === 'query' ? (\n    <div ref={schemaActionsRef}>\n      <GraphQLSchemaActions item={item} collection={collection} onSchemaLoad={setSchema} toggleDocs={toggleDocs} />\n    </div>\n  ) : null;\n\n  return (\n    <div className=\"flex flex-col h-full relative\">\n      <ResponsiveTabs\n        tabs={allTabs}\n        activeTab={requestPaneTab}\n        onTabSelect={selectTab}\n        rightContent={rightContent}\n        rightContentRef={rightContent ? schemaActionsRef : null}\n      />\n\n      <section className={classnames('flex w-full flex-1 mt-4')}>\n        <HeightBoundContainer>{tabPanel}</HeightBoundContainer>\n      </section>\n    </div>\n  );\n};\n\nexport default GraphQLRequestPane;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/index.js",
    "content": "import React, { useEffect, useRef, forwardRef } from 'react';\nimport useGraphqlSchema from './useGraphqlSchema';\nimport { IconBook, IconDownload, IconLoader2, IconRefresh } from '@tabler/icons';\nimport get from 'lodash/get';\nimport { findEnvironmentInCollection } from 'utils/collections';\nimport Dropdown from '../../Dropdown';\n\nconst GraphQLSchemaActions = ({ item, collection, onSchemaLoad, toggleDocs }) => {\n  const url = item.draft ? get(item, 'draft.request.url', '') : get(item, 'request.url', '');\n  const pathname = item.draft ? get(item, 'draft.pathname', '') : get(item, 'pathname', '');\n  const uid = item.draft ? get(item, 'draft.uid', '') : get(item, 'uid', '');\n  const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);\n  const request = item.draft ? { ...item.draft.request, pathname, uid } : { ...item.request, pathname, uid };\n\n  let {\n    schema,\n    schemaSource,\n    loadSchema,\n    isLoading: isSchemaLoading\n  } = useGraphqlSchema(url, environment, request, collection);\n\n  useEffect(() => {\n    if (onSchemaLoad) {\n      onSchemaLoad(schema);\n    }\n  }, [schema]);\n\n  const schemaDropdownTippyRef = useRef();\n  const onSchemaDropdownCreate = (ref) => (schemaDropdownTippyRef.current = ref);\n\n  const MenuIcon = forwardRef((props, ref) => {\n    return (\n      <div ref={ref} className=\"dropdown-icon cursor-pointer flex hover:underline ml-2\">\n        {isSchemaLoading && <IconLoader2 className=\"animate-spin\" size={18} strokeWidth={1.5} />}\n        {!isSchemaLoading && schema && <IconRefresh size={18} strokeWidth={1.5} />}\n        {!isSchemaLoading && !schema && <IconDownload size={18} strokeWidth={1.5} />}\n        <span className=\"ml-1\">Schema</span>\n      </div>\n    );\n  });\n\n  return (\n    <div className=\"flex flex-grow justify-end items-center\">\n      <div className=\"flex items-center cursor-pointer hover:underline\" onClick={toggleDocs}>\n        <IconBook size={18} strokeWidth={1.5} />\n        <span className=\"ml-1\">Docs</span>\n      </div>\n      <Dropdown onCreate={onSchemaDropdownCreate} icon={<MenuIcon />} placement=\"bottom-start\">\n        <div\n          className=\"dropdown-item\"\n          onClick={(e) => {\n            schemaDropdownTippyRef.current.hide();\n            loadSchema('introspection');\n          }}\n        >\n          {schema && schemaSource === 'introspection' ? 'Refresh from Introspection' : 'Load from Introspection'}\n        </div>\n        <div\n          className=\"dropdown-item\"\n          onClick={(e) => {\n            schemaDropdownTippyRef.current.hide();\n            loadSchema('file');\n          }}\n        >\n          Load from File\n        </div>\n      </Dropdown>\n    </div>\n  );\n};\n\nexport default GraphQLSchemaActions;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GraphQLSchemaActions/useGraphqlSchema.js",
    "content": "import { useState } from 'react';\nimport toast from 'react-hot-toast';\nimport { buildClientSchema, buildSchema, validateSchema } from 'graphql';\nimport { fetchGqlSchema } from 'utils/network';\nimport { simpleHash, safeParseJSON } from 'utils/common';\n\nconst buildAndValidateSchema = (data) => {\n  let schema;\n  if (typeof data === 'object') {\n    schema = buildClientSchema(data);\n  } else {\n    schema = buildSchema(data);\n  }\n\n  // Validate the schema to catch issues like empty object types\n  // The GraphQL spec requires object types to have at least one field\n  const validationErrors = validateSchema(schema);\n  if (validationErrors.length > 0) {\n    const errorMessages = validationErrors.map((e) => e.message).join('; ');\n    console.warn('GraphQL schema has validation issues:', errorMessages);\n  }\n\n  return { schema, validationErrors };\n};\n\nconst schemaHashPrefix = 'bruno.graphqlSchema';\n\nconst useGraphqlSchema = (endpoint, environment, request, collection) => {\n  const { ipcRenderer } = window;\n  const localStorageKey = `${schemaHashPrefix}.${simpleHash(endpoint)}`;\n  const [error, setError] = useState(null);\n  const [isLoading, setIsLoading] = useState(false);\n  const [schemaSource, setSchemaSource] = useState('');\n  const [schema, setSchema] = useState(() => {\n    try {\n      const saved = localStorage.getItem(localStorageKey);\n      if (!saved) {\n        return null;\n      }\n      let parsedData = safeParseJSON(saved);\n      const { schema } = buildAndValidateSchema(parsedData);\n      return schema;\n    } catch (err) {\n      localStorage.removeItem(localStorageKey);\n      console.warn('Failed to load cached GraphQL schema:', err.message);\n      return null;\n    }\n  });\n\n  const loadSchemaFromIntrospection = async () => {\n    const response = await fetchGqlSchema(endpoint, environment, request, collection);\n    if (!response) {\n      throw new Error('Introspection query failed');\n    }\n    if (response.status !== 200) {\n      throw new Error(response.statusText);\n    }\n    const data = response.data?.data;\n    if (!data) {\n      throw new Error('No data returned from introspection query');\n    }\n    setSchemaSource('introspection');\n    return data;\n  };\n\n  const loadSchemaFromFile = async () => {\n    const schemaContent = await ipcRenderer.invoke('renderer:load-gql-schema-file');\n    if (!schemaContent) {\n      setIsLoading(false);\n      return;\n    }\n    setSchemaSource('file');\n    return schemaContent?.data || schemaContent;\n  };\n\n  const loadSchema = async (schemaSource) => {\n    if (isLoading) {\n      return;\n    }\n\n    setIsLoading(true);\n\n    try {\n      let data;\n      if (schemaSource === 'file') {\n        data = await loadSchemaFromFile();\n      } else {\n        // fallback to introspection if source is unknown\n        data = await loadSchemaFromIntrospection();\n      }\n      if (data) {\n        const { schema, validationErrors } = buildAndValidateSchema(data);\n        setSchema(schema);\n        localStorage.setItem(localStorageKey, JSON.stringify(data));\n\n        if (validationErrors.length > 0) {\n          const errorMessages = validationErrors.map((e) => e.message).join('; ');\n          toast(`Schema validation issues: ${errorMessages}`, {\n            icon: '⚠️',\n            duration: 5000\n          });\n        } else {\n          toast.success('GraphQL Schema loaded successfully');\n        }\n      }\n    } catch (err) {\n      setError(err);\n      console.error(err);\n      toast.error(`Error occurred while loading GraphQL Schema: ${err.message}`);\n    }\n\n    setIsLoading(false);\n  };\n\n  return {\n    isLoading,\n    schema,\n    schemaSource,\n    loadSchema,\n    error\n  };\n};\n\nexport default useGraphqlSchema;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GraphQLVariables/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  div.CodeMirror {\n    /* todo: find a better way */\n    height: calc(100vh - 220px);\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GraphQLVariables/index.js",
    "content": "import React from 'react';\nimport get from 'lodash/get';\nimport { useDispatch, useSelector } from 'react-redux';\nimport CodeEditor from 'components/CodeEditor';\nimport { updateRequestGraphqlVariables } from 'providers/ReduxStore/slices/collections';\nimport { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport { useTheme } from 'providers/Theme';\nimport StyledWrapper from './StyledWrapper';\nimport { IconWand } from '@tabler/icons';\nimport toast from 'react-hot-toast';\nimport { prettifyJsonString } from 'utils/common/index';\n\nconst GraphQLVariables = ({ variables, item, collection }) => {\n  const dispatch = useDispatch();\n\n  const { displayedTheme } = useTheme();\n  const preferences = useSelector((state) => state.app.preferences);\n\n  const onPrettify = () => {\n    if (!variables) return;\n    try {\n      const prettyVariables = prettifyJsonString(variables);\n      dispatch(\n        updateRequestGraphqlVariables({\n          variables: prettyVariables,\n          itemUid: item.uid,\n          collectionUid: collection.uid\n        })\n      );\n      toast.success('Variables prettified');\n    } catch (error) {\n      console.error(error);\n      toast.error('Error occurred while prettifying GraphQL variables');\n    }\n  };\n\n  const onEdit = (value) => {\n    dispatch(\n      updateRequestGraphqlVariables({\n        variables: value,\n        itemUid: item.uid,\n        collectionUid: collection.uid\n      })\n    );\n  };\n\n  const onRun = () => dispatch(sendRequest(item, collection.uid));\n  const onSave = () => dispatch(saveRequest(item.uid, collection.uid));\n\n  return (\n    <>\n      <button\n        className=\"btn-add-param text-link px-4 py-4 select-none absolute right-0 z-10\"\n        onClick={onPrettify}\n        title=\"Prettify\"\n      >\n        <IconWand size={20} strokeWidth={1.5} />\n      </button>\n      <CodeEditor\n        collection={collection}\n        value={variables || ''}\n        theme={displayedTheme}\n        font={get(preferences, 'font.codeFont', 'default')}\n        fontSize={get(preferences, 'font.codeFontSize')}\n        onEdit={onEdit}\n        mode=\"application/json\"\n        onRun={onRun}\n        onSave={onSave}\n        enableVariableHighlighting={true}\n        showHintsFor={['variables']}\n      />\n    </>\n  );\n};\n\nexport default GraphQLVariables;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GrpcBody/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  height: 100%;\n  position: relative;\n\n  .messages-container {\n    flex: 1;\n    display: flex;\n    flex-direction: column;\n\n    &.single {\n      height: 100%;\n    }\n\n    &.multi {\n      overflow-y: auto;\n      padding-bottom: 48px;\n    }\n  }\n\n  .message-container {\n    display: flex;\n    flex-direction: column;\n\n    &.single {\n      height: 100%;\n\n      .editor-container {\n        height: calc(100% - 32px);\n      }\n    }\n\n    &:not(.single) {\n      min-height: 240px;\n      margin-bottom: 8px;\n\n      &.last {\n        margin-bottom: 0;\n      }\n    }\n  }\n\n  .message-toolbar {\n    display: flex;\n    align-items: center;\n    justify-content: flex-end;\n    gap: 4px;\n    padding: 4px 0px;\n    padding-top: 0px;\n    height: 32px;\n    flex-shrink: 0;\n\n    .message-label {\n      font-size: ${(props) => props.theme.font.size.sm};\n      color: ${(props) => props.theme.colors.text.subtext1};\n      margin-right: auto;\n    }\n\n    .toolbar-actions {\n      display: flex;\n      align-items: center;\n      gap: 2px;\n    }\n\n    .toolbar-btn {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      width: 28px;\n      height: 28px;\n      border-radius: 4px;\n      color: ${(props) => props.theme.colors.text.muted};\n      transition: all 0.15s ease;\n\n      &:hover {\n        background-color: ${(props) => props.theme.dropdown.hoverBg};\n        color: ${(props) => props.theme.text};\n      }\n\n      &.disabled {\n        opacity: 0.4;\n        cursor: not-allowed;\n\n        &:hover {\n          background-color: transparent;\n          color: ${(props) => props.theme.colors.text.muted};\n        }\n      }\n\n      &.delete:hover {\n        color: ${(props) => props.theme.colors.text.danger};\n      }\n    }\n  }\n\n  .editor-container {\n    flex: 1;\n    min-height: 0;\n  }\n\n  .empty-state {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    height: 100%;\n    gap: 12px;\n\n    p {\n      color: ${(props) => props.theme.colors.text.muted};\n      font-size: 13px;\n    }\n  }\n\n  .add-message-footer {\n    position: absolute;\n    bottom: 0;\n    left: 0;\n    right: 0;\n    padding: 8px;\n    background: ${(props) => props.theme.bg};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GrpcBody/index.js",
    "content": "import React, { useEffect, useRef } from 'react';\nimport { get } from 'lodash';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport { updateRequestBody } from 'providers/ReduxStore/slices/collections';\nimport { saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport { sendGrpcMessage, generateGrpcSampleMessage } from 'utils/network/index';\nimport useLocalStorage from 'hooks/useLocalStorage';\n\nimport CodeEditor from 'components/CodeEditor/index';\nimport Button from 'ui/Button';\nimport StyledWrapper from './StyledWrapper';\nimport { IconSend, IconRefresh, IconWand, IconPlus, IconTrash } from '@tabler/icons';\nimport ToolHint from 'components/ToolHint/index';\nimport { toastError } from 'utils/common/error';\nimport toast from 'react-hot-toast';\nimport { getAbsoluteFilePath } from 'utils/common/path';\nimport { prettifyJsonString } from 'utils/common/index';\n\nconst MessageToolbar = ({\n  index,\n  canClientStream,\n  isConnectionActive,\n  onSend,\n  onRegenerateMessage,\n  onPrettify,\n  onDeleteMessage,\n  showDelete\n}) => {\n  return (\n    <div className=\"message-toolbar\">\n      <span className=\"message-label\">Message {index + 1}</span>\n      <div className=\"toolbar-actions mr-2\">\n        <ToolHint text=\"Format JSON\" toolhintId={`prettify-msg-${index}`}>\n          <button onClick={onPrettify} className=\"toolbar-btn\">\n            <IconWand size={16} strokeWidth={1.5} />\n          </button>\n        </ToolHint>\n\n        <ToolHint text=\"Generate sample\" toolhintId={`regenerate-msg-${index}`}>\n          <button onClick={onRegenerateMessage} className=\"toolbar-btn\">\n            <IconRefresh size={16} strokeWidth={1.5} />\n          </button>\n        </ToolHint>\n\n        {canClientStream && (\n          <ToolHint text={isConnectionActive ? 'Send message' : 'Connection not active'} toolhintId={`send-msg-${index}`}>\n            <button\n              onClick={onSend}\n              disabled={!isConnectionActive}\n              className={`toolbar-btn ${!isConnectionActive ? 'disabled' : ''}`}\n              data-testid={`grpc-send-message-${index}`}\n            >\n              <IconSend size={16} strokeWidth={1.5} />\n            </button>\n          </ToolHint>\n        )}\n\n        {showDelete && (\n          <ToolHint text=\"Delete message\" toolhintId={`delete-msg-${index}`}>\n            <button onClick={onDeleteMessage} className=\"toolbar-btn delete\">\n              <IconTrash size={16} strokeWidth={1.5} />\n            </button>\n          </ToolHint>\n        )}\n      </div>\n    </div>\n  );\n};\n\nconst SingleGrpcMessage = ({ message, item, collection, index, methodType, handleRun, canClientSendMultipleMessages, isLast }) => {\n  const dispatch = useDispatch();\n  const { displayedTheme } = useTheme();\n  const preferences = useSelector((state) => state.app.preferences);\n  const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');\n  const isConnectionActive = useSelector((state) => state.collections.activeConnections.includes(item.uid));\n\n  const [reflectionCache] = useLocalStorage('bruno.grpc.reflectionCache', {});\n  const [protofileCache] = useLocalStorage('bruno.grpc.protofileCache', {});\n\n  const canClientStream = methodType === 'client-streaming' || methodType === 'bidi-streaming';\n  const { name, content } = message;\n\n  const onEdit = (value) => {\n    const currentMessages = [...(body.grpc || [])];\n    currentMessages[index] = {\n      name: name ? name : `message ${index + 1}`,\n      content: value\n    };\n    dispatch(updateRequestBody({\n      content: currentMessages,\n      itemUid: item.uid,\n      collectionUid: collection.uid\n    }));\n  };\n\n  const onSend = async () => {\n    try {\n      await sendGrpcMessage(item, collection.uid, content);\n    } catch (error) {\n      console.error('Error sending message:', error);\n    }\n  };\n\n  const onSave = () => dispatch(saveRequest(item.uid, collection.uid));\n\n  const onRegenerateMessage = async () => {\n    try {\n      const methodPath = item.draft?.request?.method || item.request?.method;\n      if (!methodPath) {\n        toastError(new Error('Method path not found in request'));\n        return;\n      }\n\n      const url = item.draft?.request?.url || item.request?.url;\n      const protoPath = item.draft?.request?.protoPath || item.request?.protoPath;\n\n      let methodMetadata = null;\n      if (protoPath) {\n        const absolutePath = getAbsoluteFilePath(collection.pathname, protoPath);\n        const cachedMethods = protofileCache[absolutePath];\n        if (cachedMethods) {\n          methodMetadata = cachedMethods.find((method) => method.path === methodPath);\n        }\n      } else if (url) {\n        const cachedMethods = reflectionCache[url];\n        if (cachedMethods) {\n          methodMetadata = cachedMethods.find((method) => method.path === methodPath);\n        }\n      }\n\n      const result = await generateGrpcSampleMessage(methodPath, content, {\n        arraySize: 2,\n        methodMetadata\n      });\n\n      if (result.success) {\n        const currentMessages = [...(body.grpc || [])];\n        currentMessages[index] = {\n          name: name ? name : `message ${index + 1}`,\n          content: result.message\n        };\n        dispatch(updateRequestBody({\n          content: currentMessages,\n          itemUid: item.uid,\n          collectionUid: collection.uid\n        }));\n        toast.success('Sample message generated');\n      } else {\n        toastError(new Error(result.error || 'Failed to generate sample message'));\n      }\n    } catch (error) {\n      console.error('Error generating sample message:', error);\n      toastError(error);\n    }\n  };\n\n  const onDeleteMessage = () => {\n    const currentMessages = [...(body.grpc || [])];\n    currentMessages.splice(index, 1);\n    dispatch(updateRequestBody({\n      content: currentMessages,\n      itemUid: item.uid,\n      collectionUid: collection.uid\n    }));\n  };\n\n  const onPrettify = () => {\n    try {\n      const prettyBodyJson = prettifyJsonString(content);\n      const currentMessages = [...(body.grpc || [])];\n      currentMessages[index] = {\n        name: name ? name : `message ${index + 1}`,\n        content: prettyBodyJson\n      };\n      dispatch(updateRequestBody({\n        content: currentMessages,\n        itemUid: item.uid,\n        collectionUid: collection.uid\n      }));\n    } catch (e) {\n      toastError(new Error('Unable to prettify. Invalid JSON format.'));\n    }\n  };\n\n  const isSingleMessage = !canClientSendMultipleMessages || body.grpc.length === 1;\n\n  return (\n    <div className={`message-container ${isSingleMessage ? 'single' : ''} ${isLast ? 'last' : ''}`}>\n      <MessageToolbar\n        index={index}\n        canClientStream={canClientStream}\n        isConnectionActive={isConnectionActive}\n        onSend={onSend}\n        onRegenerateMessage={onRegenerateMessage}\n        onPrettify={onPrettify}\n        onDeleteMessage={onDeleteMessage}\n        showDelete={index > 0}\n      />\n      <div className=\"editor-container\">\n        <CodeEditor\n          collection={collection}\n          theme={displayedTheme}\n          font={get(preferences, 'font.codeFont', 'default')}\n          fontSize={get(preferences, 'font.codeFontSize')}\n          value={content}\n          onEdit={onEdit}\n          onRun={handleRun}\n          onSave={onSave}\n          mode=\"application/ld+json\"\n          enableVariableHighlighting={true}\n        />\n      </div>\n    </div>\n  );\n};\n\nconst GrpcBody = ({ item, collection, handleRun }) => {\n  const dispatch = useDispatch();\n  const messagesContainerRef = useRef(null);\n  const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');\n  const methodType = item.draft ? get(item, 'draft.request.methodType') : get(item, 'request.methodType');\n  const canClientSendMultipleMessages = methodType === 'client-streaming' || methodType === 'bidi-streaming';\n\n  useEffect(() => {\n    if (messagesContainerRef.current && body?.grpc?.length > 0) {\n      const container = messagesContainerRef.current;\n      container.scrollTop = container.scrollHeight;\n    }\n  }, [body?.grpc?.length]);\n\n  const addNewMessage = () => {\n    const currentMessages = Array.isArray(body.grpc) ? [...body.grpc] : [];\n    currentMessages.push({\n      name: `message ${currentMessages.length + 1}`,\n      content: '{}'\n    });\n    dispatch(updateRequestBody({\n      content: currentMessages,\n      itemUid: item.uid,\n      collectionUid: collection.uid\n    }));\n  };\n\n  if (!body?.grpc || !Array.isArray(body.grpc)) {\n    return (\n      <StyledWrapper>\n        <div className=\"empty-state\">\n          <p>No gRPC messages available</p>\n          <Button\n            onClick={addNewMessage}\n            variant=\"filled\"\n            color=\"secondary\"\n            size=\"sm\"\n            icon={<IconPlus size={14} strokeWidth={1.5} />}\n          >\n            Add Message\n          </Button>\n        </div>\n      </StyledWrapper>\n    );\n  }\n\n  const messagesToShow = body.grpc.filter((_, index) => canClientSendMultipleMessages || index === 0);\n\n  return (\n    <StyledWrapper>\n      <div\n        ref={messagesContainerRef}\n        data-testid=\"grpc-messages-container\"\n        className={`messages-container ${canClientSendMultipleMessages && messagesToShow.length > 1 ? 'multi' : 'single'}`}\n      >\n        {messagesToShow.map((message, index) => (\n          <SingleGrpcMessage\n            key={index}\n            message={message}\n            item={item}\n            collection={collection}\n            index={index}\n            methodType={methodType}\n            handleRun={handleRun}\n            canClientSendMultipleMessages={canClientSendMultipleMessages}\n            isLast={index === messagesToShow.length - 1}\n          />\n        ))}\n      </div>\n\n      {canClientSendMultipleMessages && (\n        <div className=\"add-message-footer\">\n          <Button\n            onClick={addNewMessage}\n            variant=\"filled\"\n            color=\"secondary\"\n            size=\"sm\"\n            fullWidth\n            icon={<IconPlus size={14} strokeWidth={1.5} />}\n            data-testid=\"grpc-add-message-button\"\n          >\n            Add Message\n          </Button>\n        </div>\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default GrpcBody;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/GrpcurlModal/index.js",
    "content": "import React, { useState } from 'react';\nimport { useSelector } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport { IconCheck, IconCopy } from '@tabler/icons';\nimport toast from 'react-hot-toast';\nimport get from 'lodash/get';\nimport Modal from 'components/Modal/index';\nimport CodeEditor from 'components/CodeEditor';\nimport Button from 'ui/Button';\n\nconst GrpcurlModal = ({ isOpen, onClose, command }) => {\n  const { displayedTheme } = useTheme();\n  const [copied, setCopied] = useState(false);\n  const preferences = useSelector((state) => state.app.preferences);\n\n  const handleCopy = async () => {\n    try {\n      await navigator.clipboard.writeText(command);\n      setCopied(true);\n      toast.success('Command copied to clipboard');\n      setTimeout(() => setCopied(false), 2000);\n    } catch (error) {\n      toast.error('Failed to copy command');\n    }\n  };\n\n  return (\n    <Modal\n      isOpen={isOpen}\n      handleCancel={onClose}\n      title={(\n        <div className=\"flex items-center gap-2\">\n          <span>Generate gRPCurl Command</span>\n        </div>\n      )}\n      size=\"lg\"\n      hideFooter={true}\n    >\n      <div>\n        <div className=\"flex w-full min-h-[400px]\">\n          <div className=\"flex-grow relative\">\n            <div className=\"absolute top-2 right-2 z-10\">\n              <Button\n                size=\"sm\"\n                variant=\"ghost\"\n                onClick={handleCopy}\n                icon={copied ? <IconCheck size={20} /> : <IconCopy size={20} />}\n              />\n            </div>\n            <CodeEditor\n              value={command}\n              theme={displayedTheme}\n              readOnly={true}\n              mode=\"shell\"\n              font={get(preferences, 'font.codeFont', 'default')}\n              fontSize={get(preferences, 'font.codeFontSize')}\n            />\n          </div>\n        </div>\n      </div>\n    </Modal>\n  );\n};\n\nexport default GrpcurlModal;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/MethodDropdown/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst StyledWrapper = styled.div`\n  .method-dropdown-container {\n    display: flex;\n    align-items: center;\n    height: 100%;\n  }\n\n  .method-dropdown-trigger {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    margin-left: 0.5rem;\n    cursor: pointer;\n    user-select: none;\n  }\n\n  .method-dropdown-trigger-icon {\n    margin-right: 0.5rem;\n  }\n\n  .method-dropdown-trigger-text {\n    font-size: ${(props) => props.theme.font.size.xs};\n    white-space: nowrap;\n    color: ${(props) => props.theme.dropdown.color};\n  }\n\n  .method-dropdown-caret {\n    margin-left: 0.25rem;\n    color: ${(props) => props.theme.colors.text.muted};\n    fill: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .method-dropdown-list {\n    max-height: 24rem;\n    overflow-y: auto;\n    width: 24rem;\n    min-width: 15rem;\n  }\n\n  input#search-input {\n    border: 1px solid ${(props) => props.theme.input.border};\n    color: ${(props) => props.theme.text};\n\n    &:focus {\n      outline: none;\n      border-color: ${(props) => props.theme.input.focusBorder};\n    }\n  }\n\n  .method-dropdown-service-group {\n    margin-bottom: 0.5rem;\n  }\n\n  .method-dropdown-service-header {\n    padding: 0.25rem 0.75rem;\n    font-weight: 500;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    position: sticky;\n    top: 0;\n    z-index: 10;\n    background-color: ${(props) => props.theme.dropdown.separator};\n    color: ${(props) => props.theme.dropdown.color};\n  }\n\n  .method-dropdown-method-item {\n    padding: 0.5rem 0.75rem;\n    width: 100%;\n    border-left-width: 2px;\n    border-left-style: solid;\n    border-left-color: transparent;\n    transition: all 200ms;\n    position: relative;\n    cursor: pointer;\n\n    &:hover {\n      background-color: ${(props) => props.theme.dropdown.hoverBg};\n    }\n\n    &--selected {\n      border-left-color: ${(props) => props.theme.dropdown.selectedColor};\n      background-color: ${(props) => rgba(props.theme.dropdown.selectedColor, 0.2)};\n    }\n\n    &--focused {\n      background-color: ${(props) => props.theme.dropdown.hoverBg};\n    }\n  }\n\n  .method-dropdown-method-content {\n    display: flex;\n    align-items: center;\n  }\n\n  .method-dropdown-method-icon {\n    font-size: ${(props) => props.theme.font.size.xs};\n    margin-right: 0.75rem;\n    color: ${(props) => props.theme.dropdown.iconColor};\n  }\n\n  .method-dropdown-method-details {\n    display: flex;\n    flex-direction: column;\n    flex: 1;\n  }\n\n  .method-dropdown-method-name {\n    font-weight: 500;\n    color: ${(props) => props.theme.dropdown.color};\n  }\n\n  .method-dropdown-method-type {\n    font-size: ${(props) => props.theme.font.size.xs};\n    color: ${(props) => props.theme.dropdown.mutedText};\n  }\n\n  .method-dropdown-empty-state {\n    padding: 0.5rem 0.75rem;\n    width: 100%;\n    transition: all 200ms;\n    position: relative;\n  }\n\n  .method-dropdown-empty-state-text {\n    display: flex;\n    align-items: center;\n    font-size: ${(props) => props.theme.font.size.xs};\n    margin-right: 0.75rem;\n    color: ${(props) => props.theme.dropdown.mutedText};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/MethodDropdown/index.js",
    "content": "import { IconChevronDown } from '@tabler/icons';\nimport Dropdown from 'components/Dropdown/index';\nimport {\n  IconGrpcBidiStreaming,\n  IconGrpcClientStreaming,\n  IconGrpcServerStreaming,\n  IconGrpcUnary\n} from 'components/Icons/Grpc';\nimport SearchInput from 'components/SearchInput/index';\nimport { search } from 'fast-fuzzy';\nimport React, { forwardRef, useEffect, useRef, useState } from 'react';\nimport { useTheme } from 'providers/Theme';\nimport StyledWrapper from './StyledWrapper';\n\nconst MethodDropdown = ({\n  grpcMethods,\n  selectedGrpcMethod,\n  onMethodSelect,\n  onMethodDropdownCreate\n}) => {\n  const { theme } = useTheme();\n  const [searchText, setSearchText] = useState('');\n  const [focusedIndex, setFocusedIndex] = useState(-1);\n  const searchInputRef = useRef();\n  const listRef = useRef();\n\n  useEffect(() => {\n    const activeItem = listRef.current?.querySelector(`[data-index=\"${focusedIndex}\"]`);\n    if (activeItem) {\n      activeItem.scrollIntoView({ block: 'nearest' });\n    }\n  }, [focusedIndex]);\n\n  const groupMethodsByService = (methods) => {\n    if (!methods || !methods.length) return {};\n\n    const groupedMethods = {};\n\n    methods.forEach((method) => {\n      const pathWithoutLeadingSlash = method.path.startsWith('/') ? method.path.slice(1) : method.path;\n      const parts = pathWithoutLeadingSlash.split('/');\n      const serviceName = parts[0] || 'Default';\n      const methodName = parts[1] || method.path;\n\n      const enhancedMethod = {\n        ...method,\n        serviceName,\n        methodName\n      };\n\n      if (!groupedMethods[serviceName]) {\n        groupedMethods[serviceName] = [];\n      }\n\n      groupedMethods[serviceName].push(enhancedMethod);\n    });\n\n    return groupedMethods;\n  };\n\n  const getIconForMethodType = (type) => {\n    switch (type) {\n      case 'unary':\n        return <IconGrpcUnary size={20} strokeWidth={2} color={theme.request.methods.get} />;\n      case 'client-streaming':\n        return <IconGrpcClientStreaming size={20} strokeWidth={2} color={theme.request.methods.post} />;\n      case 'server-streaming':\n        return <IconGrpcServerStreaming size={20} strokeWidth={2} color={theme.request.methods.put} />;\n      case 'bidi-streaming':\n        return <IconGrpcBidiStreaming size={20} strokeWidth={2} color={theme.colors.text.purple} />;\n      default:\n        return <IconGrpcUnary size={20} strokeWidth={2} color={theme.request.methods.get} />;\n    }\n  };\n\n  const MethodsDropdownIcon = forwardRef((props, ref) => {\n    return (\n      <div ref={ref} className=\"method-dropdown-trigger\" data-testid=\"grpc-method-dropdown-trigger\">\n        {selectedGrpcMethod && <div className=\"method-dropdown-trigger-icon\">{getIconForMethodType(selectedGrpcMethod.type)}</div>}\n        <span className=\"method-dropdown-trigger-text\" data-testid=\"selected-grpc-method-name\">\n          {selectedGrpcMethod ? (selectedGrpcMethod.path.split('.').at(-1) || selectedGrpcMethod.path) : 'Select Method'}\n        </span>\n        <IconChevronDown className=\"method-dropdown-caret\" size={14} strokeWidth={2} />\n      </div>\n    );\n  });\n\n  const handleGrpcMethodSelect = (method) => {\n    const methodType = method.type;\n    onMethodSelect({ path: method.path, type: methodType });\n  };\n\n  const filteredMethods = searchText ? search(String(searchText), grpcMethods, { keySelector: (obj) => obj.path }) : grpcMethods;\n\n  const groupedMethods = groupMethodsByService(filteredMethods);\n\n  // Flatten grouped methods for keyboard navigation\n  const flatMethodList = Object.values(groupedMethods).flat();\n\n  const handleKeyDown = (e) => {\n    if (!flatMethodList.length) return;\n\n    if (e.key === 'ArrowDown') {\n      e.preventDefault();\n      setFocusedIndex((prev) =>\n        prev < flatMethodList.length - 1 ? prev + 1 : flatMethodList.length - 1);\n    } else if (e.key === 'ArrowUp') {\n      e.preventDefault();\n      setFocusedIndex((prev) =>\n        prev >= 0 ? prev - 1 : -1);\n    } else if (e.key === 'Enter' && focusedIndex >= 0) {\n      e.preventDefault();\n      handleGrpcMethodSelect(flatMethodList[focusedIndex]);\n    }\n  };\n\n  const focusSearchInput = () => {\n    setTimeout(() => {\n      if (searchInputRef.current) {\n        searchInputRef.current.focus();\n      }\n    }, 0); // 0ms to ensure the dropdown is fully rendered and focused\n  };\n\n  const handleDropdownShow = () => {\n    focusSearchInput();\n    setSearchText('');\n    setFocusedIndex(-1);\n  };\n\n  const handleSearchChange = (e) => {\n    // auto focus the first method when the search input is not empty\n    if (e.target.value.trim().length > 0) {\n      setFocusedIndex(0);\n    } else {\n      setFocusedIndex(-1);\n    }\n  };\n\n  if (!grpcMethods || grpcMethods.length === 0) {\n    return null;\n  }\n\n  return (\n    <StyledWrapper>\n      <div className=\"method-dropdown-container\" data-testid=\"grpc-methods-dropdown\">\n        <Dropdown onCreate={onMethodDropdownCreate} icon={<MethodsDropdownIcon />} placement=\"bottom-end\" style={{ maxWidth: 'unset' }} onShow={handleDropdownShow}>\n          <SearchInput\n            searchText={searchText}\n            setSearchText={setSearchText}\n            placeholder=\"Search\"\n            ref={searchInputRef}\n            onKeyDown={handleKeyDown}\n            onBlur={focusSearchInput}\n            onChange={handleSearchChange}\n            className=\"mt-2 mb-3\"\n            data-testid=\"grpc-methods-search-input\"\n          />\n          <div ref={listRef} className=\"method-dropdown-list\" data-testid=\"grpc-methods-list\">\n            {Object.entries(groupedMethods).map(([serviceName, methods], serviceIndex) => (\n              <div key={serviceIndex} className=\"method-dropdown-service-group\" onKeyDown={handleKeyDown} tabIndex={0}>\n                <div className=\"method-dropdown-service-header\">\n                  {serviceName || 'Default Service'}\n                </div>\n                <div>\n                  {methods.map((method, methodIndex) => {\n                    const globalMethodIndex\n                      = Object.values(groupedMethods)\n                        .slice(0, serviceIndex)\n                        .reduce((acc, group) => acc + group.length, 0) + methodIndex;\n                    const isSelected = selectedGrpcMethod && selectedGrpcMethod.path === method.path;\n                    const isFocused = focusedIndex === globalMethodIndex;\n                    return (\n                      <div\n                        key={`${serviceIndex}-${methodIndex}`}\n                        className={`method-dropdown-method-item ${\n                          isSelected ? 'method-dropdown-method-item--selected' : ''\n                        } ${isFocused ? 'method-dropdown-method-item--focused' : ''}`}\n                        onClick={() => handleGrpcMethodSelect(method)}\n                        data-index={globalMethodIndex}\n                        data-testid=\"grpc-method-item\"\n                      >\n                        <div className=\"method-dropdown-method-content\">\n                          <div className=\"method-dropdown-method-icon\">\n                            {getIconForMethodType(method.type)}\n                          </div>\n                          <div className=\"method-dropdown-method-details\">\n                            <div className=\"method-dropdown-method-name\">\n                              {method.methodName}\n                            </div>\n                            <div className=\"method-dropdown-method-type\">\n                              {method.type}\n                            </div>\n                          </div>\n                        </div>\n                      </div>\n                    );\n                  })}\n                </div>\n              </div>\n            ))}\n\n            {filteredMethods.length === 0 && (\n              <div className=\"method-dropdown-empty-state\">\n                <div className=\"method-dropdown-empty-state-text\">\n                  No methods found for the search term\n                </div>\n              </div>\n            )}\n          </div>\n        </Dropdown>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default MethodDropdown;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/ProtoFileDropdown/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .proto-file-dropdown-container {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    cursor: pointer;\n    user-select: none;\n  }\n\n  .proto-file-dropdown-icon {\n    margin-right: 0.25rem;\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .proto-file-dropdown-text {\n    font-size: ${(props) => props.theme.font.size.xs};\n    white-space: nowrap;\n    color: ${(props) => props.theme.dropdown.color};\n  }\n\n  .proto-file-dropdown-caret {\n    margin-left: 0.25rem;\n    color: ${(props) => props.theme.colors.text.muted};\n    fill: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .proto-file-dropdown-content {\n    max-height: fit-content;\n    overflow-y: auto;\n    width: 30rem;\n  }\n\n  .proto-file-dropdown-mode-section {\n    padding: 0.5rem 0.75rem;\n    border-bottom: 1px solid ${(props) => props.theme.border.border1};\n  }\n\n  .proto-file-dropdown-mode-controls {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n  }\n\n  .proto-file-dropdown-mode-options {\n    display: flex;\n    align-items: center;\n    gap: 0.5rem;\n  }\n\n  .proto-file-dropdown-mode-option {\n    font-size: ${(props) => props.theme.font.size.xs};\n    color: ${(props) => props.theme.colors.text.muted};\n\n    &--active {\n      font-weight: 500;\n    }\n  }\n\n  .proto-file-dropdown-reflection-message {\n    padding: 0.5rem 0.75rem;\n    color: ${(props) => props.theme.colors.text.muted};\n    margin-bottom: 0.5rem;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/ProtoFileDropdown/index.js",
    "content": "import React, { forwardRef, useState } from 'react';\nimport { IconFile, IconChevronDown } from '@tabler/icons';\nimport { getBasename } from 'utils/common/path';\nimport { useTheme } from 'providers/Theme';\nimport { useDispatch } from 'react-redux';\nimport { updateRequestProtoPath } from 'providers/ReduxStore/slices/collections';\nimport { openCollectionSettings } from 'providers/ReduxStore/slices/collections/actions';\nimport toast from 'react-hot-toast';\nimport Dropdown from 'components/Dropdown/index';\nimport ToggleSwitch from 'components/ToggleSwitch/index';\nimport { TabNavigation, ProtoFilesTab, ImportPathsTab } from '../Tabs';\nimport useProtoFileManagement from 'hooks/useProtoFileManagement/index';\nimport StyledWrapper from './StyledWrapper';\n\nconst ProtoFileDropdown = ({\n  collection,\n  item,\n  isReflectionMode,\n  protoFilePath,\n  showProtoDropdown,\n  setShowProtoDropdown,\n  onProtoDropdownCreate,\n  onReflectionModeToggle,\n  onProtoFileLoad\n}) => {\n  const { theme } = useTheme();\n  const dispatch = useDispatch();\n  const [activeTab, setActiveTab] = useState('protofiles'); // 'protofiles' or 'importpaths'\n  const protoFileManagement = useProtoFileManagement(collection, protoFilePath);\n  const invalidProtoFiles = protoFileManagement.protoFiles.filter((file) => !file.exists);\n  const invalidImportPaths = protoFileManagement.importPaths.filter((path) => !path.exists);\n\n  const handleSelectProtoFile = async (e) => {\n    e.stopPropagation();\n    const { success, filePath, error } = await protoFileManagement.browseForProtoFile();\n    if (!success) {\n      if (error) {\n        toast.error(`Failed to browse for proto file: ${error.message}`);\n      }\n      return;\n    }\n\n    const { success: addSuccess, relativePath, alreadyExists, error: addError } = await protoFileManagement.addProtoFileFromRequest(filePath);\n    if (!addSuccess) {\n      if (addError) {\n        toast.error(`Failed to add proto file: ${addError.message}`);\n      }\n      return;\n    }\n\n    if (alreadyExists) {\n      toast.error('Proto file already exists in collection settings');\n    } else {\n      toast.success('Added proto file to collection');\n    }\n\n    dispatch(updateRequestProtoPath({\n      protoPath: relativePath,\n      itemUid: item.uid,\n      collectionUid: collection.uid\n    }));\n\n    setShowProtoDropdown(false);\n\n    onProtoFileLoad(relativePath);\n  };\n\n  const handleSelectCollectionProtoFile = (protoFile) => {\n    if (!protoFile || !protoFile.exists) {\n      toast.error('Proto file not found');\n      return;\n    }\n\n    setShowProtoDropdown(false);\n\n    dispatch(updateRequestProtoPath({\n      protoPath: protoFile.path,\n      itemUid: item.uid,\n      collectionUid: collection.uid\n    }));\n\n    onProtoFileLoad(protoFile.path);\n  };\n\n  const handleBrowseImportPath = async (e) => {\n    e.stopPropagation();\n    const { success, directoryPath, error } = await protoFileManagement.browseForImportDirectory();\n    if (!success) {\n      if (error) {\n        toast.error(`Failed to browse for import directory: ${error.message}`);\n      }\n      return;\n    }\n\n    const { success: addSuccess, error: addError } = await protoFileManagement.addImportPathFromRequest(directoryPath);\n    if (!addSuccess) {\n      if (addError) {\n        toast.error(`Failed to add import path: ${addError.message}`);\n      }\n      return;\n    }\n\n    toast.success('Added import path to collection');\n  };\n\n  const handleToggleImportPath = async (index) => {\n    const { success, enabled, error } = await protoFileManagement.toggleImportPathFromRequest(index);\n    if (!success) {\n      if (error) {\n        toast.error(`Failed to toggle import path: ${error.message}`);\n      }\n      return;\n    }\n\n    toast.success(`Import path ${enabled ? 'enabled' : 'disabled'}`);\n  };\n\n  const handleOpenCollectionProtobufSettings = (e) => {\n    e.stopPropagation();\n    dispatch(openCollectionSettings(collection.uid, 'protobuf'));\n  };\n\n  const ProtoFileDropdownIcon = forwardRef((props, ref) => {\n    return (\n      <div ref={ref} className=\"proto-file-dropdown-container\" onClick={() => setShowProtoDropdown((prev) => !prev)} data-testid=\"grpc-proto-file-dropdown-icon\">\n        {!isReflectionMode && (\n          <IconFile size={20} strokeWidth={1.5} className=\"proto-file-dropdown-icon\" />\n        )}\n        <span className=\"proto-file-dropdown-text\">\n          {isReflectionMode ? 'Using Reflection' : (protoFilePath ? getBasename(collection.pathname, protoFilePath) : 'Select Proto File')}\n        </span>\n        <IconChevronDown className=\"proto-file-dropdown-caret\" size={14} strokeWidth={2} />\n      </div>\n    );\n  });\n\n  return (\n    <StyledWrapper>\n      <div className=\"proto-file-dropdown\">\n        <Dropdown\n          onCreate={onProtoDropdownCreate}\n          icon={<ProtoFileDropdownIcon />}\n          placement=\"bottom-end\"\n          visible={showProtoDropdown}\n          onClickOutside={() => setShowProtoDropdown(false)}\n          data-testid=\"grpc-proto-file-dropdown\"\n        >\n          <div className=\"proto-file-dropdown-content\">\n            <div className=\"proto-file-dropdown-mode-section\" data-testid=\"grpc-mode-toggle\">\n              <div className=\"proto-file-dropdown-mode-controls\">\n                <span>Mode</span>\n                <div className=\"proto-file-dropdown-mode-options\">\n                  <span className={`proto-file-dropdown-mode-option ${!isReflectionMode ? 'proto-file-dropdown-mode-option--active' : ''}`} style={{ color: !isReflectionMode ? theme.primary.text : undefined }}>\n                    Proto File\n                  </span>\n                  <ToggleSwitch\n                    isOn={isReflectionMode}\n                    handleToggle={onReflectionModeToggle}\n                    size=\"2xs\"\n                    activeColor={theme.primary.solid}\n                  />\n                  <span className={`proto-file-dropdown-mode-option ${isReflectionMode ? 'proto-file-dropdown-mode-option--active' : ''}`} style={{ color: isReflectionMode ? theme.primary.text : undefined }}>\n                    Reflection\n                  </span>\n                </div>\n              </div>\n            </div>\n\n            {!isReflectionMode && (\n              <TabNavigation\n                activeTab={activeTab}\n                onTabChange={setActiveTab}\n                collectionProtoFiles={protoFileManagement.protoFiles}\n                collectionImportPaths={protoFileManagement.importPaths}\n              />\n            )}\n\n            {!isReflectionMode && (\n              <>\n                {activeTab === 'protofiles' && (\n                  <ProtoFilesTab\n                    collectionProtoFiles={protoFileManagement.protoFiles}\n                    invalidProtoFiles={invalidProtoFiles}\n                    protoFilePath={protoFilePath}\n                    collection={collection}\n                    onSelectCollectionProtoFile={handleSelectCollectionProtoFile}\n                    onOpenCollectionProtobufSettings={handleOpenCollectionProtobufSettings}\n                    onSelectProtoFile={handleSelectProtoFile}\n                    setShowProtoDropdown={setShowProtoDropdown}\n                  />\n                )}\n\n                {activeTab === 'importpaths' && (\n                  <ImportPathsTab\n                    collectionImportPaths={protoFileManagement.importPaths}\n                    invalidImportPaths={invalidImportPaths}\n                    onOpenCollectionProtobufSettings={handleOpenCollectionProtobufSettings}\n                    onBrowseImportPath={handleBrowseImportPath}\n                    onToggleImportPath={handleToggleImportPath}\n                  />\n                )}\n              </>\n            )}\n\n            {isReflectionMode && (\n              <div className=\"proto-file-dropdown-reflection-message\">\n                Using server reflection to discover gRPC methods.\n              </div>\n            )}\n          </div>\n        </Dropdown>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default ProtoFileDropdown;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  height: 2.1rem;\n  border: ${(props) => props.theme.requestTabPanel.url.border};\n  border-radius: ${(props) => props.theme.border.radius.base};\n\n  .method-selector-container {\n    background-color: ${(props) => props.theme.requestTabPanel.url.bg};\n    border-top-left-radius: ${(props) => props.theme.border.radius.base};\n    border-bottom-left-radius: ${(props) => props.theme.border.radius.base};\n  }\n\n  .input-container {\n    background-color: ${(props) => props.theme.requestTabPanel.url.bg};\n    border-top-right-radius: ${(props) => props.theme.border.radius.base};\n    border-bottom-right-radius: ${(props) => props.theme.border.radius.base};\n\n    input {\n      background-color: ${(props) => props.theme.requestTabPanel.url.bg};\n      outline: none;\n      box-shadow: none;\n\n      &:focus {\n        outline: none !important;\n        box-shadow: none !important;\n      }\n    }\n  }\n\n  .caret {\n    color: rgb(140, 140, 140);\n    fill: rgb(140 140 140);\n    position: relative;\n    top: 1px;\n  }\n\n  .infotip {\n    position: relative;\n    display: inline-block;\n    cursor: pointer;\n  }\n\n  .infotip:hover .infotip-text {\n    visibility: visible;\n    opacity: 1;\n  }\n\n  .infotip-text {\n    visibility: hidden;\n    width: auto;\n    background-color: ${(props) => props.theme.background.surface2};\n    color: ${(props) => props.theme.text};\n    text-align: center;\n    border-radius: 4px;\n    padding: 4px 8px;\n    position: absolute;\n    z-index: 1;\n    bottom: 34px;\n    left: 50%;\n    transform: translateX(-50%);\n    opacity: 0;\n    transition: opacity 0.3s;\n    white-space: nowrap;\n  }\n\n  .infotip-text::after {\n    content: '';\n    position: absolute;\n    top: 100%;\n    left: 50%;\n    margin-left: -4px;\n    border-width: 4px;\n    border-style: solid;\n    border-color: ${(props) => props.theme.background.surface2} transparent transparent transparent;\n  }\n\n  .shortcut {\n    font-size: 0.625rem;\n  }\n\n  @keyframes pulse {\n    0% {\n      opacity: 0.4;\n    }\n    50% {\n      opacity: 1;\n    }\n    100% {\n      opacity: 0.4;\n    }\n  }\n\n  .connection-status-strip {\n    animation: pulse 1.5s ease-in-out infinite;\n    background-color: ${(props) => props.theme.colors.text.green};\n    position: absolute;\n    bottom: 0;\n    left: 0;\n    right: 0;\n    height: 2px;\n  }\n\n  /* Method dropdown styling */\n  .method-dropdown {\n    margin-right: 8px;\n    position: relative;\n    z-index: 10;\n  }\n\n  .dropdown-item {\n    padding: 8px 12px;\n    cursor: pointer;\n\n    &:hover {\n      background-color: ${(props) => props.theme.dropdown.hoverBg};\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/Tabs/ImportPathsTab/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  .content-wrapper {\n    padding: 0.5rem 0.75rem;\n  }\n\n  .header-wrapper {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    margin-bottom: 0.25rem;\n  }\n\n  .header-text {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.grpc.importPaths.header.text};\n  }\n\n  .settings-button {\n    color: ${(props) => props.theme.grpc.importPaths.header.button.color};\n    background: transparent;\n    border: none;\n    cursor: pointer;\n    padding: 0.25rem;\n    border-radius: 0.25rem;\n    transition: color 0.2s ease;\n\n    &:hover {\n      color: ${(props) => props.theme.grpc.importPaths.header.button.hoverColor};\n    }\n  }\n\n  .error-wrapper {\n    margin-bottom: 0.5rem;\n    padding: 0.5rem;\n    background-color: ${(props) => props.theme.grpc.importPaths.error.bg};\n    border-radius: 0.25rem;\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.grpc.importPaths.error.text};\n  }\n\n  .error-text {\n    display: flex;\n    align-items: center;\n    margin: 0;\n  }\n\n  .error-link {\n    color: ${(props) => props.theme.grpc.importPaths.error.link.color};\n    background: transparent;\n    border: none;\n    cursor: pointer;\n    text-decoration: underline;\n    margin-left: 0.25rem;\n    font-size: inherit;\n\n    &:hover {\n      color: ${(props) => props.theme.grpc.importPaths.error.link.hoverColor};\n    }\n  }\n\n  .items-container {\n    display: flex;\n    flex-direction: column;\n    gap: 0.25rem;\n    max-height: 15rem;\n    overflow: auto;\n    max-width: 30rem;\n  }\n\n  .item-wrapper {\n    padding: 0.5rem 0.75rem;\n    opacity: ${(props) => props.theme.grpc.importPaths.item.invalid.opacity};\n\n    &.valid {\n      opacity: 1;\n    }\n  }\n\n  .item-content {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n  }\n\n  .item-left {\n    display: flex;\n    align-items: center;\n  }\n\n  .checkbox-wrapper {\n    display: flex;\n    align-items: center;\n    margin-right: 0.75rem;\n  }\n\n  .checkbox {\n    margin-right: 0.5rem;\n    cursor: pointer;\n    color: ${(props) => props.theme.grpc.importPaths.item.checkbox.color};\n    accent-color: ${(props) => props.theme.colors.accent};\n  }\n\n  .item-text {\n    display: flex;\n    align-items: center;\n    font-size: ${(props) => props.theme.font.size.sm};\n    white-space: nowrap;\n  }\n\n  .invalid-icon {\n    color: ${(props) => props.theme.grpc.importPaths.item.invalid.text};\n    font-size: ${(props) => props.theme.font.size.sm};\n    display: flex;\n    align-items: center;\n  }\n\n  .empty-wrapper {\n    padding: 0.5rem 0.75rem;\n  }\n\n  .empty-text {\n    color: ${(props) => props.theme.grpc.importPaths.empty.text};\n    font-size: ${(props) => props.theme.font.size.base};\n    font-style: italic;\n    text-align: center;\n    padding: 0.5rem 0;\n  }\n\n  .button-wrapper {\n    padding: 0.5rem 0.75rem;\n  }\n\n  .browse-button {\n    width: 100%;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    background-color: ${(props) => props.theme.grpc.importPaths.button.bg};\n    color: ${(props) => props.theme.grpc.importPaths.button.color};\n    border: 1px solid ${(props) => props.theme.grpc.importPaths.button.border};\n    padding: 0.5rem 1rem;\n    border-radius: 0.25rem;\n    font-size: ${(props) => props.theme.font.size.base};\n    cursor: pointer;\n    transition: border-color 0.2s ease;\n\n    &:hover {\n      border-color: ${(props) => props.theme.grpc.importPaths.button.hoverBorder};\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/Tabs/ImportPathsTab/index.js",
    "content": "import React from 'react';\nimport { IconFolder, IconSettings, IconAlertCircle, IconFileImport } from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\n\nconst ImportPathsTab = ({\n  collectionImportPaths,\n  invalidImportPaths,\n  onOpenCollectionProtobufSettings,\n  onBrowseImportPath,\n  onToggleImportPath\n}) => {\n  return (\n    <StyledWrapper>\n      {collectionImportPaths && collectionImportPaths.length > 0 && (\n        <div className=\"content-wrapper\">\n          <div className=\"header-wrapper\">\n            <div className=\"header-text\">From Collection Settings</div>\n            <button\n              onClick={onOpenCollectionProtobufSettings}\n              className=\"settings-button\"\n            >\n              <IconSettings size={16} strokeWidth={1.5} />\n            </button>\n          </div>\n\n          {invalidImportPaths.length > 0 && (\n            <div className=\"error-wrapper\">\n              <p className=\"error-text\">\n                <IconAlertCircle size={16} strokeWidth={1.5} style={{ marginRight: '0.25rem' }} />\n                Some import paths could not be found.\n                {' '}\n                <button\n                  onClick={onOpenCollectionProtobufSettings}\n                  className=\"error-link\"\n                >\n                  Manage import paths\n                </button>\n              </p>\n            </div>\n          )}\n\n          <div className=\"items-container\">\n            {collectionImportPaths.map((importPath, index) => {\n              const isInvalid = !importPath.exists;\n\n              return (\n                <div\n                  key={`collection-import-${index}`}\n                  className={`item-wrapper ${!isInvalid ? 'valid' : ''}`}\n                >\n                  <div className=\"item-content\">\n                    <div className=\"item-left\">\n                      <div className=\"checkbox-wrapper\">\n                        <input\n                          type=\"checkbox\"\n                          checked={importPath.enabled}\n                          disabled={isInvalid}\n                          onChange={() => onToggleImportPath(index)}\n                          className=\"checkbox\"\n                          title={importPath.enabled ? 'Import path enabled' : 'Import path disabled'}\n                        />\n                      </div>\n                      <IconFolder size={20} strokeWidth={1.5} style={{ marginRight: '0.5rem', color: 'inherit' }} />\n                      <div className=\"item-text\">\n                        {importPath.path}\n                        {isInvalid && (\n                          <span className=\"invalid-icon\">\n                            <IconAlertCircle size={16} strokeWidth={1.5} style={{ margin: '0 0.25rem' }} />\n                          </span>\n                        )}\n                      </div>\n                    </div>\n                  </div>\n                </div>\n              );\n            })}\n          </div>\n        </div>\n      )}\n\n      {(!collectionImportPaths || collectionImportPaths.length === 0) && (\n        <div className=\"empty-wrapper\">\n          <div className=\"empty-text\">\n            No import paths configured in collection settings\n          </div>\n        </div>\n      )}\n\n      <div className=\"button-wrapper\">\n        <button\n          className=\"browse-button\"\n          onClick={onBrowseImportPath}\n        >\n          <IconFileImport size={16} strokeWidth={1.5} style={{ marginRight: '0.25rem' }} />\n          Browse for Import Path\n        </button>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default ImportPathsTab;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/Tabs/ProtoFilesTab/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  .content-wrapper {\n    padding: 0.5rem 0.75rem;\n  }\n\n  .header-wrapper {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    margin-bottom: 0.25rem;\n  }\n\n  .header-text {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.grpc.protoFiles.header.text};\n  }\n\n  .settings-button {\n    color: ${(props) => props.theme.grpc.protoFiles.header.button.color};\n    background: transparent;\n    border: none;\n    cursor: pointer;\n    padding: 0.25rem;\n    border-radius: 0.25rem;\n    transition: color 0.2s ease;\n\n    &:hover {\n      color: ${(props) => props.theme.grpc.protoFiles.header.button.hoverColor};\n    }\n  }\n\n  .error-wrapper {\n    margin-bottom: 0.5rem;\n    padding: 0.5rem;\n    background-color: ${(props) => props.theme.grpc.protoFiles.error.bg};\n    border-radius: 0.25rem;\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.grpc.protoFiles.error.text};\n  }\n\n  .error-text {\n    display: flex;\n    align-items: center;\n    margin: 0;\n  }\n\n  .error-link {\n    color: ${(props) => props.theme.grpc.protoFiles.error.link.color};\n    background: transparent;\n    border: none;\n    cursor: pointer;\n    text-decoration: underline;\n    margin-left: 0.25rem;\n    font-size: inherit;\n\n    &:hover {\n      color: ${(props) => props.theme.grpc.protoFiles.error.link.hoverColor};\n    }\n  }\n\n  .items-container {\n    display: flex;\n    flex-direction: column;\n    gap: 0.25rem;\n    max-height: 15rem;\n    overflow-y: auto;\n  }\n\n  .item-wrapper {\n    padding: 0.5rem 0.75rem;\n    cursor: pointer;\n    border-left: 2px solid transparent;\n    background-color: ${(props) => props.theme.grpc.protoFiles.item.bg};\n    transition: all 0.2s ease;\n    opacity: ${(props) => props.theme.grpc.protoFiles.item.invalid.opacity};\n\n    &.valid {\n      opacity: 1;\n    }\n\n    &.selected {\n      border-left-color: ${(props) => props.theme.grpc.protoFiles.item.selected.border};\n      background-color: ${(props) => props.theme.grpc.protoFiles.item.selected.bg};\n    }\n\n    &:hover {\n      background-color: ${(props) => props.theme.grpc.protoFiles.item.hoverBg};\n\n      &.selected {\n        background-color: ${(props) => props.theme.grpc.protoFiles.item.selected.bg};\n      }\n    }\n  }\n\n  .item-content {\n    display: flex;\n    align-items: center;\n  }\n\n  .item-icon {\n    margin-right: 0.75rem;\n    color: ${(props) => props.theme.grpc.protoFiles.item.icon};\n  }\n\n  .item-details {\n    display: flex;\n    flex-direction: column;\n  }\n\n  .item-title {\n    font-size: ${(props) => props.theme.font.size.base};\n    display: flex;\n    align-items: center;\n    color: ${(props) => props.theme.grpc.protoFiles.item.text};\n  }\n\n  .item-path {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.grpc.protoFiles.item.secondaryText};\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    max-width: 12.5rem;\n  }\n\n  .invalid-icon {\n    color: ${(props) => props.theme.grpc.protoFiles.item.invalid.text};\n    font-size: ${(props) => props.theme.font.size.sm};\n    display: flex;\n    align-items: center;\n    margin-left: 0.5rem;\n  }\n\n  .empty-wrapper {\n    padding: 0.5rem 0.75rem;\n  }\n\n  .empty-text {\n    color: ${(props) => props.theme.grpc.protoFiles.empty.text};\n    font-size: ${(props) => props.theme.font.size.base};\n    font-style: italic;\n    text-align: center;\n    padding: 0.5rem 0;\n  }\n\n  .button-wrapper {\n    padding: 0.5rem 0.75rem;\n  }\n\n  .browse-button {\n    width: 100%;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    background-color: ${(props) => props.theme.grpc.protoFiles.button.bg};\n    color: ${(props) => props.theme.grpc.protoFiles.button.color};\n    border: 1px solid ${(props) => props.theme.grpc.protoFiles.button.border};\n    padding: 0.5rem 1rem;\n    border-radius: 0.25rem;\n    font-size: ${(props) => props.theme.font.size.base};\n    cursor: pointer;\n    transition: border-color 0.2s ease;\n\n    &:hover {\n      border-color: ${(props) => props.theme.grpc.protoFiles.button.hoverBorder};\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/Tabs/ProtoFilesTab/index.js",
    "content": "import React from 'react';\nimport { IconFile, IconSettings, IconAlertCircle } from '@tabler/icons';\nimport { getBasename } from 'utils/common/path';\nimport StyledWrapper from './StyledWrapper';\n\nconst ProtoFilesTab = ({\n  collectionProtoFiles,\n  invalidProtoFiles,\n  protoFilePath,\n  collection,\n  onSelectCollectionProtoFile,\n  onOpenCollectionProtobufSettings,\n  onSelectProtoFile\n}) => {\n  return (\n    <StyledWrapper>\n      {collectionProtoFiles && collectionProtoFiles.length > 0 && (\n        <div className=\"content-wrapper\">\n          <div className=\"header-wrapper\">\n            <div className=\"header-text\">From Collection Settings</div>\n            <button\n              onClick={onOpenCollectionProtobufSettings}\n              className=\"settings-button\"\n            >\n              <IconSettings size={16} strokeWidth={1.5} />\n            </button>\n          </div>\n\n          {invalidProtoFiles.length > 0 && (\n            <div className=\"error-wrapper\">\n              <p className=\"error-text\">\n                <IconAlertCircle size={16} strokeWidth={1.5} style={{ marginRight: '0.25rem' }} />\n                Some proto files could not be found.\n                {' '}\n                <button\n                  onClick={onOpenCollectionProtobufSettings}\n                  className=\"error-link\"\n                >\n                  Manage proto files\n                </button>\n              </p>\n            </div>\n          )}\n\n          <div className=\"items-container\">\n            {collectionProtoFiles.map((protoFile, index) => {\n              const isSelected = protoFilePath === protoFile.path;\n              const isInvalid = !protoFile.exists;\n\n              return (\n                <div\n                  key={`collection-proto-${index}`}\n                  className={`item-wrapper ${!isInvalid ? 'valid' : ''} ${isSelected ? 'selected' : ''}`}\n                  onClick={() => {\n                    if (!isInvalid) {\n                      onSelectCollectionProtoFile(protoFile);\n                    }\n                  }}\n                >\n                  <div className=\"item-content\">\n                    <div className=\"item-icon\">\n                      <IconFile size={20} strokeWidth={1.5} />\n                    </div>\n                    <div className=\"item-details\">\n                      <div className=\"item-title\">\n                        {getBasename(collection.pathname, protoFile.path)}\n                        {isInvalid && (\n                          <span className=\"invalid-icon\">\n                            <IconAlertCircle size={14} strokeWidth={1.5} />\n                          </span>\n                        )}\n                      </div>\n                      <div className=\"item-path\">\n                        {protoFile.path}\n                      </div>\n                    </div>\n                  </div>\n                </div>\n              );\n            })}\n          </div>\n        </div>\n      )}\n\n      {(!collectionProtoFiles || collectionProtoFiles.length === 0) && (\n        <div className=\"empty-wrapper\">\n          <div className=\"empty-text\">\n            No proto files configured in collection settings\n          </div>\n        </div>\n      )}\n\n      <div className=\"button-wrapper\">\n        <button\n          className=\"browse-button\"\n          onClick={onSelectProtoFile}\n        >\n          <IconFile size={16} strokeWidth={1.5} style={{ marginRight: '0.25rem' }} />\n          Browse for Proto File\n        </button>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default ProtoFilesTab;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/Tabs/TabNavigation/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n.tab-container {\n  background-color: ${(props) => props.theme.dropdown.separator};\n}\n.tab-button {\n  background-color: ${(props) => props.theme.grpc.tabNav.button.inactive.bg};\n  color: ${(props) => props.theme.grpc.tabNav.button.inactive.color};\n  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);\n  \n  &:hover {\n    background-color: ${(props) => props.theme.grpc.tabNav.button.inactive.hover};\n  }\n  \n  &.active {\n    background-color: ${(props) => props.theme.grpc.tabNav.button.active.bg};\n    color: ${(props) => props.theme.grpc.tabNav.button.active.color};\n  }\n}\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/Tabs/TabNavigation/index.js",
    "content": "import React from 'react';\nimport StyledWrapper from './StyledWrapper';\n\nconst TabNavigation = ({ activeTab, onTabChange, collectionProtoFiles, collectionImportPaths }) => {\n  return (\n    <StyledWrapper className=\"px-3 py-2 border-b border-neutral-200 dark:border-neutral-700\">\n      <div className=\"tab-container flex space-x-1 rounded-lg p-1\">\n        <button\n          className={`flex-1 px-3 py-1.5 text-xs font-medium rounded-md transition-colors tab-button ${activeTab === 'protofiles' ? 'active' : ''}`}\n          onClick={(e) => {\n            e.stopPropagation();\n            onTabChange('protofiles');\n          }}\n        >\n          Proto Files (\n          {collectionProtoFiles?.length || 0}\n          )\n        </button>\n        <button\n          className={`flex-1 px-3 py-1.5 text-xs font-medium rounded-md transition-colors tab-button ${activeTab === 'importpaths' ? 'active' : ''}`}\n          onClick={(e) => {\n            e.stopPropagation();\n            onTabChange('importpaths');\n          }}\n        >\n          Import Paths (\n          {collectionImportPaths?.length || 0}\n          )\n        </button>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default TabNavigation;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/Tabs/index.js",
    "content": "export { default as TabNavigation } from './TabNavigation';\nexport { default as ProtoFilesTab } from './ProtoFilesTab';\nexport { default as ImportPathsTab } from './ImportPathsTab';\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GrpcQueryUrl/index.js",
    "content": "import React, { useState, useEffect, useRef } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { requestUrlChanged, updateRequestMethod, updateRequestProtoPath } from 'providers/ReduxStore/slices/collections';\nimport { saveRequest, generateGrpcurlCommand } from 'providers/ReduxStore/slices/collections/actions';\nimport { useTheme } from 'providers/Theme';\nimport SingleLineEditor from 'components/SingleLineEditor/index';\nimport { isMacOS } from 'utils/common/platform';\nimport StyledWrapper from './StyledWrapper';\nimport {\n  IconX,\n  IconCheck,\n  IconRefresh,\n  IconDeviceFloppy,\n  IconArrowRight,\n  IconCode\n} from '@tabler/icons';\nimport toast from 'react-hot-toast';\nimport {\n  cancelGrpcConnection,\n  endGrpcConnection\n} from 'utils/network/index';\nimport GrpcurlModal from './GrpcurlModal';\nimport { debounce } from 'lodash';\nimport { getPropertyFromDraftOrRequest } from 'utils/collections';\nimport useReflectionManagement from 'hooks/useReflectionManagement/index';\nimport useProtoFileManagement from 'hooks/useProtoFileManagement/index';\nimport MethodDropdown from './MethodDropdown';\nimport ProtoFileDropdown from './ProtoFileDropdown';\n\nconst STREAMING_METHOD_TYPES = ['client-streaming', 'server-streaming', 'bidi-streaming'];\nconst CLIENT_STREAMING_METHOD_TYPES = ['client-streaming', 'bidi-streaming'];\n\nconst GrpcQueryUrl = ({ item, collection, handleRun }) => {\n  const { theme, storedTheme } = useTheme();\n  const dispatch = useDispatch();\n  const method = getPropertyFromDraftOrRequest(item, 'request.method');\n  const type = getPropertyFromDraftOrRequest(item, 'request.type');\n  const url = getPropertyFromDraftOrRequest(item, 'request.url', '');\n  const isMac = isMacOS();\n  const saveShortcut = isMac ? 'Cmd + S' : 'Ctrl + S';\n  const editorRef = useRef(null);\n  const isConnectionActive = useSelector((state) => state.collections.activeConnections.includes(item.uid));\n\n  const [grpcMethods, setGrpcMethods] = useState([]);\n  const [selectedGrpcMethod, setSelectedGrpcMethod] = useState({\n    path: method,\n    type: type\n  });\n  const [isReflectionMode, setIsReflectionMode] = useState(false);\n  const [protoFilePath, setProtoFilePath] = useState(item?.request?.protoPath || '');\n  const [showGrpcurlModal, setShowGrpcurlModal] = useState(false);\n  const [grpcurlCommand, setGrpcurlCommand] = useState('');\n  const [showProtoDropdown, setShowProtoDropdown] = useState(false);\n\n  const methodDropdownRef = useRef(null);\n  const protoDropdownRef = useRef(null);\n  const haveFetchedMethodsRef = useRef(false);\n\n  const protoFileManagement = useProtoFileManagement(collection, protoFilePath);\n  const reflectionManagement = useReflectionManagement(item, collection.uid);\n\n  const onMethodSelect = ({ path, type }) => {\n    if (isConnectionActive) {\n      cancelGrpcConnection(item.uid)\n        .then(() => {\n          toast.success('gRPC connection cancelled');\n        })\n        .catch((err) => {\n          console.error('Failed to cancel gRPC connection:', err);\n        });\n    }\n\n    dispatch(updateRequestMethod({\n      method: path,\n      methodType: type,\n      itemUid: item.uid,\n      collectionUid: collection.uid\n    }));\n  };\n\n  const onMethodDropdownCreate = (ref) => (methodDropdownRef.current = ref);\n  const onProtoDropdownCreate = (ref) => (protoDropdownRef.current = ref);\n\n  const isStreamingMethod = selectedGrpcMethod && selectedGrpcMethod.type && STREAMING_METHOD_TYPES.includes(selectedGrpcMethod.type);\n  const isClientStreamingMethod = selectedGrpcMethod && selectedGrpcMethod.type && CLIENT_STREAMING_METHOD_TYPES.includes(selectedGrpcMethod.type);\n\n  const onSave = () => {\n    dispatch(saveRequest(item.uid, collection.uid));\n  };\n\n  const onUrlChange = (value) => {\n    if (!editorRef.current?.editor) return;\n    const editor = editorRef.current.editor;\n    const cursor = editor.getCursor();\n\n    const finalUrl = value?.trim() || value;\n\n    dispatch(\n      requestUrlChanged({\n        itemUid: item.uid,\n        collectionUid: collection.uid,\n        url: finalUrl\n      })\n    );\n\n    if (finalUrl !== value) {\n      setTimeout(() => {\n        if (editor) {\n          editor.setCursor(cursor);\n        }\n      }, 0);\n    }\n\n    if (!protoFilePath && value) {\n      setIsReflectionMode(true);\n      handleReflection(finalUrl);\n    }\n  };\n\n  const handleReflection = async (url, isManualRefresh = false) => {\n    const { methods, error, fromCache } = await reflectionManagement.loadMethodsFromReflection(url, isManualRefresh);\n\n    if (error) {\n      toast.error(`Failed to load gRPC methods: ${error.message || 'Unknown error'}`);\n      return;\n    }\n\n    setGrpcMethods(methods);\n    setProtoFilePath('');\n    setIsReflectionMode(true);\n\n    // Only update protoPath if it was previously set (to avoid creating unnecessary draft state)\n    const currentProtoPath = getPropertyFromDraftOrRequest(item, 'request.protoPath', '');\n    if (currentProtoPath) {\n      dispatch(updateRequestProtoPath({\n        protoPath: '',\n        itemUid: item.uid,\n        collectionUid: collection.uid\n      }));\n    }\n\n    if (!fromCache && methods && methods.length > 0) {\n      toast.success(`Loaded ${methods.length} gRPC methods from reflection`);\n    }\n\n    if (methods && methods.length > 0) {\n      const haveSelectedMethod = selectedGrpcMethod && methods.some((method) => method.path === selectedGrpcMethod.path);\n      if (!haveSelectedMethod) {\n        setSelectedGrpcMethod(null);\n        onMethodSelect({ path: '', type: '' });\n      } else if (selectedGrpcMethod) {\n        const currentMethod = methods.find((method) => method.path === selectedGrpcMethod.path);\n        if (currentMethod) {\n          setSelectedGrpcMethod({\n            path: selectedGrpcMethod.path,\n            type: currentMethod.type\n          });\n        }\n      }\n    }\n  };\n\n  const handleProtoFileLoad = async (filePath, isManualRefresh = false) => {\n    const { methods, error, fromCache } = await protoFileManagement.loadMethodsFromProtoFile(filePath, isManualRefresh);\n\n    if (error) {\n      console.error('Failed to load gRPC methods:', error);\n      toast.error('Failed to load gRPC methods');\n      setGrpcMethods([]);\n      return;\n    }\n\n    setProtoFilePath(filePath);\n    setGrpcMethods(methods);\n    setIsReflectionMode(false);\n\n    if (!fromCache) {\n      toast.success(`Loaded ${methods.length} gRPC methods from proto file`);\n    }\n\n    if (methods && methods.length > 0) {\n      const haveSelectedMethod = selectedGrpcMethod && methods.some((method) => method.path === selectedGrpcMethod.path);\n      if (!haveSelectedMethod) {\n        setSelectedGrpcMethod(null);\n        onMethodSelect({ path: '', type: '' });\n      } else if (selectedGrpcMethod) {\n        const currentMethod = methods.find((method) => method.path === selectedGrpcMethod.path);\n        if (currentMethod) {\n          setSelectedGrpcMethod({\n            path: selectedGrpcMethod.path,\n            type: currentMethod.type\n          });\n        }\n      }\n    }\n  };\n\n  const handleGrpcurl = async (url) => {\n    if (!url) {\n      toast.error('Please enter a valid gRPC server URL');\n      return;\n    }\n\n    if (!selectedGrpcMethod?.path) {\n      toast.error('Please select a gRPC method');\n      return;\n    }\n\n    try {\n      const result = await dispatch(generateGrpcurlCommand(item, collection.uid));\n\n      if (result.success) {\n        setGrpcurlCommand(result.command);\n        setShowGrpcurlModal(true);\n      } else {\n        toast.error(result.error || 'Failed to generate grpcurl command');\n      }\n    } catch (error) {\n      console.error('Error generating grpcurl command:', error);\n      toast.error('Failed to generate grpcurl command');\n    }\n  };\n\n  const handleGrpcMethodSelect = (method) => {\n    const methodType = method.type;\n    setSelectedGrpcMethod({\n      path: method.path,\n      type: methodType\n    });\n    onMethodSelect({ path: method.path, type: methodType });\n  };\n\n  const handleCancelConnection = (e) => {\n    e.stopPropagation();\n\n    cancelGrpcConnection(item.uid)\n      .then(() => {\n        toast.success('gRPC connection cancelled');\n      })\n      .catch((err) => {\n        console.error('Failed to cancel gRPC connection:', err);\n        toast.error('Failed to cancel gRPC connection');\n      });\n  };\n\n  const handleEndConnection = (e) => {\n    e.stopPropagation();\n\n    endGrpcConnection(item.uid)\n      .then(() => {\n        toast.success('gRPC stream ended');\n      })\n      .catch((err) => {\n        console.error('Failed to end gRPC stream:', err);\n        toast.error('Failed to end gRPC stream');\n      });\n  };\n\n  const handleReflectionModeToggle = (e) => {\n    e.stopPropagation();\n    e.preventDefault();\n    setIsReflectionMode(!isReflectionMode);\n    if (!isReflectionMode) {\n      setProtoFilePath('');\n      dispatch(updateRequestProtoPath({\n        protoPath: '',\n        itemUid: item.uid,\n        collectionUid: collection.uid\n      }));\n      if (url) {\n        handleReflection(url);\n      }\n    } else {\n      setGrpcMethods([]);\n      setSelectedGrpcMethod(null);\n      onMethodSelect({ path: '', type: '' });\n    }\n  };\n\n  const debouncedOnUrlChange = debounce(onUrlChange, 1000);\n\n  useEffect(() => {\n    if (haveFetchedMethodsRef.current) {\n      return;\n    }\n    haveFetchedMethodsRef.current = true;\n\n    if (protoFilePath) {\n      setIsReflectionMode(false);\n      handleProtoFileLoad(protoFilePath);\n      return;\n    }\n    if (!url) return;\n    setIsReflectionMode(true);\n    handleReflection(url);\n  }, []);\n\n  return (\n    <StyledWrapper className=\"flex items-center relative\" data-testid=\"grpc-query-url-container\">\n      <div className=\"flex items-center h-full method-selector-container\">\n        <div className=\"flex items-center justify-center h-full px-[10px]\" data-testid=\"grpc-method-indicator\">\n          <span className=\"text-xs font-medium\" style={{ color: theme.request.grpc }}>gRPC</span>\n        </div>\n      </div>\n      <div className=\"flex items-center w-full input-container h-full relative overflow-auto\">\n        <SingleLineEditor\n          ref={editorRef}\n          value={url}\n          onSave={(finalValue) => onSave(finalValue)}\n          theme={storedTheme}\n          onChange={(newValue) => debouncedOnUrlChange(newValue)}\n          onRun={handleRun}\n          collection={collection}\n          highlightPathParams={true}\n          item={item}\n        />\n\n      </div>\n\n      <div className=\"flex items-center h-full mx-2 gap-3\" id=\"send-request\">\n        <MethodDropdown\n          grpcMethods={grpcMethods}\n          selectedGrpcMethod={selectedGrpcMethod}\n          onMethodSelect={handleGrpcMethodSelect}\n          onMethodDropdownCreate={onMethodDropdownCreate}\n        />\n        <ProtoFileDropdown\n          collection={collection}\n          item={item}\n          isReflectionMode={isReflectionMode}\n          protoFilePath={protoFilePath}\n          showProtoDropdown={showProtoDropdown}\n          setShowProtoDropdown={setShowProtoDropdown}\n          onProtoDropdownCreate={onProtoDropdownCreate}\n          onReflectionModeToggle={handleReflectionModeToggle}\n          onProtoFileLoad={handleProtoFileLoad}\n        />\n\n        <div\n          className=\"infotip\"\n          onClick={(e) => {\n            e.stopPropagation();\n            if (isReflectionMode) {\n              handleReflection(url, true);\n            } else if (protoFilePath) {\n              handleProtoFileLoad(protoFilePath, true);\n            } else {\n              toast.error('No proto file selected');\n            }\n          }}\n        >\n          <IconRefresh\n            color={theme.requestTabs.icon.color}\n            strokeWidth={1.5}\n            size={20}\n            className={`${(isReflectionMode ? reflectionManagement.isLoadingMethods : protoFileManagement.isLoadingMethods) ? 'animate-spin' : 'cursor-pointer'}`}\n            data-testid=\"refresh-methods-icon\"\n          />\n          <span className=\"infotip-text text-xs\">\n            {isReflectionMode ? 'Refresh server reflection' : 'Refresh proto file methods'}\n          </span>\n        </div>\n\n        <div\n          className=\"infotip\"\n          onClick={(e) => {\n            e.stopPropagation();\n            handleGrpcurl(url);\n          }}\n        >\n          <IconCode\n            color={theme.requestTabs.icon.color}\n            strokeWidth={1.5}\n            size={20}\n          />\n          <span className=\"infotip-text text-xs\">Generate grpcurl command</span>\n        </div>\n\n        <div\n          className=\"infotip\"\n          onClick={(e) => {\n            e.stopPropagation();\n            if (!item.draft) return;\n            onSave();\n          }}\n        >\n          <IconDeviceFloppy\n            color={item.draft ? theme.draftColor : theme.requestTabs.icon.color}\n            strokeWidth={1.5}\n            size={20}\n            className={`${item.draft ? 'cursor-pointer' : 'cursor-default'}`}\n          />\n          <span className=\"infotip-text text-xs\">\n            Save <span className=\"shortcut\">({saveShortcut})</span>\n          </span>\n        </div>\n\n        {isConnectionActive && isStreamingMethod && (\n          <div className=\"connection-controls relative flex items-center h-full gap-3\">\n            <div className=\"infotip\" onClick={handleCancelConnection} data-testid=\"grpc-cancel-connection-button\">\n              <IconX color={theme.requestTabs.icon.color} strokeWidth={1.5} size={20} className=\"cursor-pointer\" />\n              <span className=\"infotip-text text-xs\">Cancel</span>\n            </div>\n\n            {isClientStreamingMethod && (\n              <div onClick={handleEndConnection} data-testid=\"grpc-end-connection-button\">\n                <IconCheck\n                  color={theme.colors.text.green}\n                  strokeWidth={2}\n                  size={20}\n                  className=\"cursor-pointer\"\n                />\n              </div>\n            )}\n          </div>\n        )}\n\n        {(!isConnectionActive || !isStreamingMethod) && (\n          <div\n            className=\"cursor-pointer\"\n            data-testid=\"grpc-send-request-button\"\n            onClick={(e) => {\n              e.stopPropagation();\n              handleRun(e);\n            }}\n          >\n            <IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={20} />\n          </div>\n        )}\n      </div>\n      {isConnectionActive && isStreamingMethod && (\n        <div className=\"connection-status-strip\"></div>\n      )}\n\n      {showGrpcurlModal && (\n        <GrpcurlModal\n          isOpen={showGrpcurlModal}\n          onClose={() => setShowGrpcurlModal(false)}\n          command={grpcurlCommand}\n        />\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default GrpcQueryUrl;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GrpcRequestPane/GrpcAuth/GrpcAuthMode/index.js",
    "content": "import React, { useMemo, useCallback } from 'react';\nimport get from 'lodash/get';\nimport { IconCaretDown } from '@tabler/icons';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport { useDispatch } from 'react-redux';\nimport { updateRequestAuthMode } from 'providers/ReduxStore/slices/collections';\nimport { humanizeRequestAuthMode } from 'utils/collections';\nimport StyledWrapper from '../../../Auth/AuthMode/StyledWrapper';\n\nconst GrpcAuthMode = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');\n\n  const onModeChange = useCallback((value) => {\n    dispatch(\n      updateRequestAuthMode({\n        itemUid: item.uid,\n        collectionUid: collection.uid,\n        mode: value\n      })\n    );\n  }, [dispatch, item.uid, collection.uid]);\n\n  const menuItems = useMemo(() => [\n    {\n      id: 'basic',\n      label: 'Basic Auth',\n      onClick: () => onModeChange('basic')\n    },\n    {\n      id: 'bearer',\n      label: 'Bearer Token',\n      onClick: () => onModeChange('bearer')\n    },\n    {\n      id: 'apikey',\n      label: 'API Key',\n      onClick: () => onModeChange('apikey')\n    },\n    {\n      id: 'oauth2',\n      label: 'OAuth 2.0',\n      onClick: () => onModeChange('oauth2')\n    },\n    {\n      id: 'wsse',\n      label: 'WSSE Auth',\n      onClick: () => onModeChange('wsse')\n    },\n    {\n      id: 'inherit',\n      label: 'Inherit',\n      onClick: () => onModeChange('inherit')\n    },\n    {\n      id: 'none',\n      label: 'No Auth',\n      onClick: () => onModeChange('none')\n    }\n  ], [onModeChange]);\n\n  return (\n    <StyledWrapper>\n      <div className=\"inline-flex items-center cursor-pointer auth-mode-selector\">\n        <MenuDropdown\n          items={menuItems}\n          placement=\"bottom-end\"\n          selectedItemId={authMode}\n          showTickMark={true}\n        >\n          <div className=\"flex items-center justify-center auth-mode-label select-none\">\n            {humanizeRequestAuthMode(authMode)} <IconCaretDown className=\"caret ml-1\" size={14} strokeWidth={2} />\n          </div>\n        </MenuDropdown>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default GrpcAuthMode;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GrpcRequestPane/GrpcAuth/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  .inherit-mode-text {\n    color: ${(props) => props.theme.primary.text};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GrpcRequestPane/GrpcAuth/index.js",
    "content": "import React, { useEffect } from 'react';\nimport get from 'lodash/get';\nimport { useDispatch } from 'react-redux';\nimport GrpcAuthMode from './GrpcAuthMode';\nimport BearerAuth from '../../Auth/BearerAuth';\nimport BasicAuth from '../../Auth/BasicAuth';\nimport ApiKeyAuth from '../../Auth/ApiKeyAuth';\nimport OAuth2 from '../../Auth/OAuth2/index';\nimport WsseAuth from '../../Auth/WsseAuth';\nimport StyledWrapper from './StyledWrapper';\nimport { humanizeRequestAuthMode } from 'utils/collections';\nimport { getTreePathFromCollectionToItem } from 'utils/collections/index';\nimport { updateRequestAuthMode, updateAuth } from 'providers/ReduxStore/slices/collections';\nimport { saveRequest } from 'providers/ReduxStore/slices/collections/actions';\n\n// List of auth modes supported by gRPC\n// Note: Only header-based auth modes work with gRPC\n// Complex auth modes like AWS Sig v4, Digest, and NTLM require axios interceptors\n// and cannot be supported in gRPC requests as of now\nconst supportedGrpcAuthModes = ['basic', 'bearer', 'apikey', 'oauth2', 'wsse', 'none', 'inherit'];\n\nconst GrpcAuth = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');\n  const requestTreePath = getTreePathFromCollectionToItem(collection, item);\n\n  const request = item.draft\n    ? get(item, 'draft.request', {})\n    : get(item, 'request', {});\n\n  const save = () => {\n    return saveRequest(item.uid, collection.uid);\n  };\n\n  // Reset to 'none' if current auth mode is not supported by gRPC\n  useEffect(() => {\n    if (authMode && !supportedGrpcAuthModes.includes(authMode)) {\n      dispatch(\n        updateRequestAuthMode({\n          itemUid: item.uid,\n          collectionUid: collection.uid,\n          mode: 'none'\n        })\n      );\n    }\n  }, [authMode, collection.uid, dispatch, item.uid]);\n\n  const getEffectiveAuthSource = () => {\n    if (authMode !== 'inherit') return null;\n\n    const collectionRoot = collection?.draft?.root || collection?.root || {};\n    const collectionAuth = get(collectionRoot, 'request.auth');\n    let effectiveSource = {\n      type: 'collection',\n      name: 'Collection',\n      auth: collectionAuth\n    };\n\n    // Check folders in reverse to find the closest auth configuration\n    for (let i of [...requestTreePath].reverse()) {\n      if (i.type === 'folder') {\n        const folderAuth = get(i, 'root.request.auth');\n        if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {\n          effectiveSource = {\n            type: 'folder',\n            name: i.name,\n            auth: folderAuth\n          };\n          break;\n        }\n      }\n    }\n\n    return effectiveSource;\n  };\n\n  const getAuthView = () => {\n    switch (authMode) {\n      case 'none': {\n        return <div>No Auth</div>;\n      }\n      case 'basic': {\n        return <BasicAuth collection={collection} item={item} updateAuth={updateAuth} request={request} save={save} />;\n      }\n      case 'bearer': {\n        return <BearerAuth collection={collection} item={item} updateAuth={updateAuth} request={request} save={save} />;\n      }\n      case 'apikey': {\n        return <ApiKeyAuth collection={collection} item={item} updateAuth={updateAuth} request={request} save={save} />;\n      }\n      case 'oauth2': {\n        return <OAuth2 collection={collection} item={item} updateAuth={updateAuth} request={request} save={save} />;\n      }\n      case 'wsse': {\n        return <WsseAuth collection={collection} item={item} updateAuth={updateAuth} request={request} save={save} />;\n      }\n      case 'inherit': {\n        const source = getEffectiveAuthSource();\n\n        // Only show inherited auth if it's one of the supported types\n        if (source && supportedGrpcAuthModes.includes(source.auth?.mode)) {\n          return (\n            <>\n              <div className=\"flex flex-row w-full gap-2\">\n                <div>Auth inherited from {source.name}: </div>\n                <div className=\"inherit-mode-text\">{humanizeRequestAuthMode(source.auth?.mode)}</div>\n              </div>\n            </>\n          );\n        } else {\n          return (\n            <>\n              <div className=\"flex flex-row w-full gap-2\">\n                <div>Inherited auth not supported by gRPC. Using no auth instead.</div>\n              </div>\n            </>\n          );\n        }\n      }\n      default: {\n        return null;\n      }\n    }\n  };\n\n  return (\n    <StyledWrapper className=\"w-full overflow-y-scroll\">\n      {getAuthView()}\n    </StyledWrapper>\n  );\n};\n\nexport default GrpcAuth;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GrpcRequestPane/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/GrpcRequestPane/index.js",
    "content": "import React, { useMemo, useCallback, useEffect, useRef } from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';\nimport RequestHeaders from 'components/RequestPane/RequestHeaders';\nimport GrpcBody from 'components/RequestPane/GrpcBody';\nimport GrpcAuth from './GrpcAuth/index';\nimport GrpcAuthMode from './GrpcAuth/GrpcAuthMode/index';\nimport StatusDot from 'components/StatusDot/index';\nimport HeightBoundContainer from 'ui/HeightBoundContainer';\nimport find from 'lodash/find';\nimport Documentation from 'components/Documentation/index';\nimport { getPropertyFromDraftOrRequest } from 'utils/collections/index';\nimport ResponsiveTabs from 'ui/ResponsiveTabs';\nimport StyledWrapper from './StyledWrapper';\n\nconst GrpcRequestPane = ({ item, collection, handleRun }) => {\n  const dispatch = useDispatch();\n  const tabs = useSelector((state) => state.tabs.tabs);\n  const activeTabUid = useSelector((state) => state.tabs.activeTabUid);\n  const rightContentRef = useRef(null);\n  const focusedTab = find(tabs, (t) => t.uid === activeTabUid);\n  const requestPaneTab = focusedTab?.requestPaneTab;\n\n  const selectTab = useCallback((tab) => {\n    dispatch(\n      updateRequestPaneTab({\n        uid: item.uid,\n        requestPaneTab: tab\n      })\n    );\n  }, [dispatch, item.uid]);\n\n  const tabPanel = useMemo(() => {\n    switch (requestPaneTab) {\n      case 'body': {\n        return <GrpcBody item={item} collection={collection} hideModeSelector={true} hidePrettifyButton={true} handleRun={handleRun} />;\n      }\n      case 'headers': {\n        return <RequestHeaders item={item} collection={collection} addHeaderText=\"Add Metadata\" />;\n      }\n      case 'auth': {\n        return <GrpcAuth item={item} collection={collection} />;\n      }\n      case 'docs': {\n        return <Documentation item={item} collection={collection} />;\n      }\n      default: {\n        return <div className=\"mt-4\">404 | Not found</div>;\n      }\n    }\n  }, [requestPaneTab, item, collection, handleRun]);\n\n  const body = getPropertyFromDraftOrRequest(item, 'request.body');\n  const headers = getPropertyFromDraftOrRequest(item, 'request.headers');\n  const docs = getPropertyFromDraftOrRequest(item, 'request.docs');\n  const auth = getPropertyFromDraftOrRequest(item, 'request.auth');\n\n  const activeHeadersLength = headers.filter((header) => header.enabled).length;\n  const grpcMessagesCount = body?.grpc?.length || 0;\n\n  // Determine if this is a client streaming request\n  const request = item.draft ? item.draft.request : item.request;\n  const isClientStreaming = request.methodType === 'client-streaming' || request.methodType === 'bidi-streaming';\n\n  const allTabs = useMemo(() => {\n    const getMessageIndicator = () => {\n      if (grpcMessagesCount > 0) {\n        return isClientStreaming ? (\n          <sup className=\"ml-[.125rem] font-medium\">{grpcMessagesCount}</sup>\n        ) : (\n          <StatusDot />\n        );\n      }\n      return null;\n    };\n\n    return [\n      {\n        key: 'body',\n        label: 'Message',\n        indicator: getMessageIndicator()\n      },\n      {\n        key: 'headers',\n        label: 'Metadata',\n        indicator: activeHeadersLength > 0 ? <sup className=\"ml-[.125rem] font-medium\">{activeHeadersLength}</sup> : null\n      },\n      {\n        key: 'auth',\n        label: 'Auth',\n        indicator: auth?.mode && auth.mode !== 'none' ? <StatusDot type=\"default\" /> : null\n      },\n      {\n        key: 'docs',\n        label: 'Docs',\n        indicator: docs && docs.length > 0 ? <StatusDot type=\"default\" /> : null\n      }\n    ];\n  }, [grpcMessagesCount, isClientStreaming, activeHeadersLength, auth?.mode, docs]);\n\n  // Initialize tab to 'body' if no tab is currently set\n  useEffect(() => {\n    if (activeTabUid && focusedTab?.uid && !requestPaneTab) {\n      selectTab('body');\n    }\n  }, [activeTabUid, focusedTab?.uid, requestPaneTab, selectTab]);\n\n  // Return error for truly missing active/focused tabs\n  if (!activeTabUid || !focusedTab?.uid) {\n    return <div className=\"pb-4 px-4\">An error occurred!</div>;\n  }\n\n  // Return null during initialization while requestPaneTab is being set by useEffect\n  if (!requestPaneTab) {\n    return null;\n  }\n\n  const rightContent = requestPaneTab === 'auth' ? (\n    <div ref={rightContentRef} className=\"flex flex-grow justify-start items-center\">\n      <GrpcAuthMode item={item} collection={collection} />\n    </div>\n  ) : null;\n\n  return (\n    <StyledWrapper className=\"flex flex-col h-full relative\">\n      <ResponsiveTabs\n        tabs={allTabs}\n        activeTab={requestPaneTab}\n        onTabSelect={selectTab}\n        rightContent={rightContent}\n        rightContentRef={rightContent ? rightContentRef : null}\n      />\n\n      <section\n        className=\"flex w-full flex-1 h-full mt-4\"\n      >\n        <HeightBoundContainer>\n          {tabPanel}\n        </HeightBoundContainer>\n      </section>\n    </StyledWrapper>\n  );\n};\n\nexport default GrpcRequestPane;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js",
    "content": "import React, { useRef, useCallback, useMemo } from 'react';\nimport classnames from 'classnames';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { find, get } from 'lodash';\nimport { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';\nimport QueryParams from 'components/RequestPane/QueryParams';\nimport RequestHeaders from 'components/RequestPane/RequestHeaders';\nimport RequestBody from 'components/RequestPane/RequestBody';\nimport RequestBodyMode from 'components/RequestPane/RequestBody/RequestBodyMode';\nimport Auth from 'components/RequestPane/Auth';\nimport Vars from 'components/RequestPane/Vars';\nimport Assertions from 'components/RequestPane/Assertions';\nimport Script from 'components/RequestPane/Script';\nimport Tests from 'components/RequestPane/Tests';\nimport Settings from 'components/RequestPane/Settings';\nimport Documentation from 'components/Documentation/index';\nimport StatusDot from 'components/StatusDot';\nimport ResponsiveTabs from 'ui/ResponsiveTabs';\nimport HeightBoundContainer from 'ui/HeightBoundContainer';\nimport AuthMode from '../Auth/AuthMode/index';\n\nconst TAB_CONFIG = [\n  { key: 'params', label: 'Params' },\n  { key: 'body', label: 'Body' },\n  { key: 'headers', label: 'Headers' },\n  { key: 'auth', label: 'Auth' },\n  { key: 'vars', label: 'Vars' },\n  { key: 'script', label: 'Script' },\n  { key: 'assert', label: 'Assert' },\n  { key: 'tests', label: 'Tests' },\n  { key: 'docs', label: 'Docs' },\n  { key: 'settings', label: 'Settings' }\n];\n\nconst TAB_PANELS = {\n  params: QueryParams,\n  body: RequestBody,\n  headers: RequestHeaders,\n  auth: Auth,\n  vars: Vars,\n  assert: Assertions,\n  script: Script,\n  tests: Tests,\n  docs: Documentation,\n  settings: Settings\n};\n\nconst HttpRequestPane = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  const tabs = useSelector((state) => state.tabs.tabs);\n  const activeTabUid = useSelector((state) => state.tabs.activeTabUid);\n\n  const rightContentRef = useRef(null);\n\n  const focusedTab = find(tabs, (t) => t.uid === activeTabUid);\n  const requestPaneTab = focusedTab?.requestPaneTab;\n\n  const getProperty = useCallback(\n    (key) => (item.draft ? get(item, `draft.${key}`, []) : get(item, key, [])),\n    [item.draft, item]\n  );\n\n  const params = getProperty('request.params');\n  const body = getProperty('request.body');\n  const headers = getProperty('request.headers');\n  const script = getProperty('request.script');\n  const assertions = getProperty('request.assertions');\n  const tests = getProperty('request.tests');\n  const docs = getProperty('request.docs');\n  const requestVars = getProperty('request.vars.req');\n  const responseVars = getProperty('request.vars.res');\n  const auth = getProperty('request.auth');\n  const tags = getProperty('tags');\n\n  const activeCounts = useMemo(() => ({\n    params: params.filter((p) => p.enabled).length,\n    headers: headers.filter((h) => h.enabled).length,\n    assertions: assertions.filter((a) => a.enabled).length,\n    vars: requestVars.filter((r) => r.enabled).length + responseVars.filter((r) => r.enabled).length\n  }), [params, headers, assertions, requestVars, responseVars]);\n\n  const selectTab = useCallback(\n    (tabKey) => {\n      dispatch(updateRequestPaneTab({ uid: item.uid, requestPaneTab: tabKey }));\n    },\n    [dispatch, item.uid]\n  );\n\n  const indicators = useMemo(() => {\n    const hasScriptError = item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage;\n    const hasTestError = item.testScriptErrorMessage;\n\n    return {\n      params: activeCounts.params > 0 ? <sup className=\"font-medium\">{activeCounts.params}</sup> : null,\n      body: body.mode !== 'none' ? <StatusDot /> : null,\n      headers: activeCounts.headers > 0 ? <sup className=\"font-medium\">{activeCounts.headers}</sup> : null,\n      auth: auth.mode !== 'none' ? <StatusDot /> : null,\n      vars: activeCounts.vars > 0 ? <sup className=\"font-medium\">{activeCounts.vars}</sup> : null,\n      script: (script.req || script.res) ? (hasScriptError ? <StatusDot type=\"error\" /> : <StatusDot />) : null,\n      assert: activeCounts.assertions > 0 ? <sup className=\"font-medium\">{activeCounts.assertions}</sup> : null,\n      tests: tests?.length > 0 ? (hasTestError ? <StatusDot type=\"error\" /> : <StatusDot />) : null,\n      docs: docs?.length > 0 ? <StatusDot /> : null,\n      settings: tags?.length > 0 ? <StatusDot /> : null\n    };\n  }, [activeCounts, body.mode, auth.mode, script, item.preRequestScriptErrorMessage, item.postResponseScriptErrorMessage, item.testScriptErrorMessage, tests, docs, tags]);\n\n  const allTabs = useMemo(\n    () => TAB_CONFIG.map(({ key, label }) => ({ key, label, indicator: indicators[key] })),\n    [indicators]\n  );\n\n  const tabPanel = useMemo(() => {\n    const Component = TAB_PANELS[requestPaneTab];\n    return Component ? <Component item={item} collection={collection} /> : <div className=\"mt-4\">404 | Not found</div>;\n  }, [requestPaneTab, item, collection]);\n\n  if (!activeTabUid || !focusedTab?.uid || !requestPaneTab) {\n    return <div className=\"pb-4 px-4\">An error occurred!</div>;\n  }\n\n  const rightContent = requestPaneTab === 'body' ? (\n    <div ref={rightContentRef}>\n      <RequestBodyMode item={item} collection={collection} />\n    </div>\n  ) : requestPaneTab === 'auth' ? (\n    <div ref={rightContentRef} className=\"flex flex-grow justify-start items-center\">\n      <AuthMode item={item} collection={collection} />\n    </div>\n  ) : null;\n\n  return (\n    <div className=\"flex flex-col h-full relative\">\n      <ResponsiveTabs\n        tabs={allTabs}\n        activeTab={requestPaneTab}\n        onTabSelect={selectTab}\n        rightContent={rightContent}\n        rightContentRef={rightContent ? rightContentRef : null}\n        delayedTabs={['body']}\n      />\n\n      <section className={classnames('flex w-full flex-1 mt-4')}>\n        <HeightBoundContainer>{tabPanel}</HeightBoundContainer>\n      </section>\n    </div>\n  );\n};\n\nexport default HttpRequestPane;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/MultipartFormParams/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  .upload-btn,\n  .clear-file-btn {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    padding: 4px;\n    color: ${(props) => props.theme.colors.text.muted};\n    background: transparent;\n    border: none;\n    cursor: pointer;\n    border-radius: 4px;\n    transition: color 0.15s ease;\n\n    &:hover {\n      color: ${(props) => props.theme.colors.text.link};\n    }\n  }\n\n  .clear-file-btn:hover {\n    color: ${(props) => props.theme.colors.text.danger};\n  }\n\n  .file-value-cell {\n    padding: 4px 0;\n\n    .file-name {\n      font-size: 12px;\n      color: ${(props) => props.theme.text};\n    }\n  }\n\n  .value-cell {\n    .flex-1 {\n      min-width: 0;\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/MultipartFormParams/index.js",
    "content": "import React, { useCallback } from 'react';\nimport get from 'lodash/get';\nimport { useDispatch } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport { IconUpload, IconX, IconFile } from '@tabler/icons';\nimport {\n  moveMultipartFormParam,\n  setMultipartFormParams\n} from 'providers/ReduxStore/slices/collections';\nimport { browseFiles } from 'providers/ReduxStore/slices/collections/actions';\nimport MultiLineEditor from 'components/MultiLineEditor';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport EditableTable from 'components/EditableTable';\nimport StyledWrapper from './StyledWrapper';\nimport path from 'utils/common/path';\nimport { isWindowsOS } from 'utils/common/platform';\n\nconst MultipartFormParams = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n  const params = item.draft ? get(item, 'draft.request.body.multipartForm') : get(item, 'request.body.multipartForm');\n\n  const onSave = () => dispatch(saveRequest(item.uid, collection.uid));\n  const handleRun = () => dispatch(sendRequest(item, collection.uid));\n\n  const handleParamsChange = useCallback((updatedParams) => {\n    dispatch(setMultipartFormParams({\n      collectionUid: collection.uid,\n      itemUid: item.uid,\n      params: updatedParams\n    }));\n  }, [dispatch, collection.uid, item.uid]);\n\n  const handleParamDrag = useCallback(({ updateReorderedItem }) => {\n    dispatch(moveMultipartFormParam({\n      collectionUid: collection.uid,\n      itemUid: item.uid,\n      updateReorderedItem\n    }));\n  }, [dispatch, collection.uid, item.uid]);\n\n  const handleBrowseFiles = useCallback((row, onChange) => {\n    dispatch(browseFiles())\n      .then((filePaths) => {\n        const processedPaths = filePaths.map((filePath) => {\n          const collectionDir = collection.pathname;\n          if (filePath.startsWith(collectionDir)) {\n            return path.relative(collectionDir, filePath);\n          }\n          return filePath;\n        });\n\n        const currentParams = item.draft\n          ? get(item, 'draft.request.body.multipartForm')\n          : get(item, 'request.body.multipartForm');\n        const updatedParams = (currentParams || []).map((p) => {\n          if (p.uid === row.uid) {\n            return { ...p, type: 'file', value: processedPaths };\n          }\n          return p;\n        });\n        handleParamsChange(updatedParams);\n      })\n      .catch((error) => {\n        console.error(error);\n      });\n  }, [dispatch, collection.pathname, item, handleParamsChange]);\n\n  const handleClearFile = useCallback((row) => {\n    const currentParams = params || [];\n    const updatedParams = currentParams.map((p) => {\n      if (p.uid === row.uid) {\n        return { ...p, type: 'text', value: '' };\n      }\n      return p;\n    });\n    handleParamsChange(updatedParams);\n  }, [params, handleParamsChange]);\n\n  const handleValueChange = useCallback((row, newValue, onChange) => {\n    const currentParams = params || [];\n    const existingParam = currentParams.find((p) => p.uid === row.uid);\n    if (existingParam) {\n      const updatedParams = currentParams.map((p) => {\n        if (p.uid === row.uid) {\n          return { ...p, type: 'text', value: newValue };\n        }\n        return p;\n      });\n      handleParamsChange(updatedParams);\n    } else {\n      onChange(newValue);\n    }\n  }, [params, handleParamsChange]);\n\n  const getFileName = (filePaths) => {\n    if (!filePaths || (Array.isArray(filePaths) && filePaths.length === 0)) {\n      return null;\n    }\n    const paths = Array.isArray(filePaths) ? filePaths : [filePaths];\n    const validPaths = paths.filter((v) => v != null && v !== '');\n    if (validPaths.length === 0) return null;\n\n    const separator = isWindowsOS() ? '\\\\' : '/';\n    if (validPaths.length === 1) {\n      return validPaths[0].split(separator).pop();\n    }\n    return `${validPaths.length} file(s)`;\n  };\n\n  const columns = [\n    {\n      key: 'name',\n      name: 'Key',\n      isKeyField: true,\n      placeholder: 'Key',\n      width: '30%'\n    },\n    {\n      key: 'value',\n      name: 'Value',\n      placeholder: 'Value',\n      width: '35%',\n      render: ({ row, value, onChange, isLastEmptyRow }) => {\n        const isFile = row.type === 'file';\n        const fileName = isFile ? getFileName(value) : null;\n        const hasTextValue = !isFile && value && value.length > 0;\n\n        if (fileName) {\n          return (\n            <div className=\"flex items-center file-value-cell\">\n              <button\n                className=\"clear-file-btn ml-1\"\n                onClick={() => handleClearFile(row)}\n                title=\"Remove file\"\n              >\n                <IconX size={16} />\n              </button>\n              <IconFile size={16} className=\"text-muted mr-1\" />\n              <span className=\"file-name flex-1 truncate\" title={Array.isArray(value) ? value.join(', ') : value}>\n                {fileName}\n              </span>\n            </div>\n          );\n        }\n\n        return (\n          <div className=\"flex items-center value-cell\">\n            <div className=\"flex-1\">\n              <MultiLineEditor\n                onSave={onSave}\n                theme={storedTheme}\n                value={value || ''}\n                onChange={(newValue) => handleValueChange(row, newValue, onChange)}\n                onRun={handleRun}\n                allowNewlines={true}\n                collection={collection}\n                item={item}\n                placeholder={!value ? 'Value' : ''}\n              />\n            </div>\n            {!hasTextValue && !isLastEmptyRow && (\n              <button\n                className=\"upload-btn ml-1\"\n                onClick={() => handleBrowseFiles(row, onChange)}\n                title=\"Select file\"\n              >\n                <IconUpload size={16} />\n              </button>\n            )}\n          </div>\n        );\n      }\n    },\n    {\n      key: 'contentType',\n      name: 'Content-Type',\n      placeholder: 'Auto',\n      width: '20%',\n      render: ({ value, onChange }) => (\n        <SingleLineEditor\n          onSave={onSave}\n          theme={storedTheme}\n          placeholder={!value ? 'Auto' : ''}\n          value={value || ''}\n          onChange={onChange}\n          onRun={handleRun}\n          collection={collection}\n        />\n      )\n    }\n  ];\n\n  const defaultRow = {\n    name: '',\n    value: '',\n    contentType: '',\n    type: 'text'\n  };\n\n  return (\n    <StyledWrapper className=\"w-full\">\n      <EditableTable\n        columns={columns}\n        rows={params || []}\n        onChange={handleParamsChange}\n        defaultRow={defaultRow}\n        reorderable={true}\n        onReorder={handleParamDrag}\n      />\n    </StyledWrapper>\n  );\n};\n\nexport default MultipartFormParams;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/PromptVariables/PromptVariablesModal/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  max-height: 60vh;\n  overflow-y: auto;\n  padding: 0 0.2rem;\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/PromptVariables/PromptVariablesModal/index.js",
    "content": "import React, { useState } from 'react';\nimport Portal from 'components/Portal';\nimport Modal from 'components/Modal';\nimport StyledWrapper from './StyledWrapper';\nimport { IconAlertTriangle } from '@tabler/icons';\n\nexport default function PromptVariablesModal({ title = 'Input Required', prompts, onSubmit, onCancel }) {\n  const [values, setValues] = useState({});\n\n  const handleChange = (prompt, value) => {\n    setValues((prev) => ({ ...prev, [prompt]: value }));\n  };\n\n  if (!prompts?.length) {\n    return null;\n  }\n\n  return (\n    <Portal>\n      <Modal\n        size=\"lg\"\n        title={title}\n        confirmText=\"Continue\"\n        cancelText=\"Cancel\"\n        handleConfirm={() => onSubmit(values)}\n        handleCancel={onCancel}\n      >\n        <StyledWrapper data-testid=\"prompt-variables-modal-content\">\n          <div className=\"space-y-5 mt-2\">\n            {prompts.map((prompt, index) => (\n              <div key={prompt} data-testid=\"prompt-variable-input-container\">\n                <label htmlFor={`prompt-${index}`} className=\"block font-medium\">\n                  {prompt}\n                </label>\n                <input\n                  id={`prompt-${index}`}\n                  type=\"text\"\n                  data-testid={`prompt-variable-input-${index}`}\n                  className=\"textbox mt-2 w-full\"\n                  placeholder=\"Enter value\"\n                  value={values[prompt] || ''}\n                  onChange={(e) => handleChange(prompt, e.target.value)}\n                  autoFocus={index === 0}\n                  autoComplete=\"off\"\n                  autoCorrect=\"off\"\n                  autoCapitalize=\"off\"\n                  spellCheck=\"false\"\n                />\n              </div>\n            ))}\n          </div>\n        </StyledWrapper>\n      </Modal>\n    </Portal>\n  );\n}\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/QueryEditor/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  div.CodeMirror {\n    background: ${(props) => props.theme.codemirror.bg};\n    border: solid 1px ${(props) => props.theme.codemirror.border};\n    font-family: ${(props) => (props.font ? props.font : 'default')};\n    font-size: ${(props) => (props.fontSize ? `${props.fontSize}px` : 'inherit')};\n    flex: 1 1 0;\n  }\n\n  textarea.cm-editor {\n    position: relative;\n  }\n\n  // Todo: dark mode temporary fix\n  // Clean this\n  .CodeMirror.cm-s-monokai {\n    .CodeMirror-overlayscroll-horizontal div,\n    .CodeMirror-overlayscroll-vertical div {\n      background: #444444;\n    }\n  }\n\n  .cm-s-default, .cm-s-monokai {\n    span.cm-def {\n      color: ${(props) => props.theme.codemirror.tokens.definition} !important;\n    }\n    span.cm-property {\n      color: ${(props) => props.theme.codemirror.tokens.property} !important;\n    }\n    span.cm-string {\n      color: ${(props) => props.theme.codemirror.tokens.string} !important;\n    }\n    span.cm-number {\n      color: ${(props) => props.theme.codemirror.tokens.number} !important;\n    }\n    span.cm-atom {\n      color: ${(props) => props.theme.codemirror.tokens.atom} !important;\n    }\n    span.cm-variable, span.cm-variable-2 {\n      color: ${(props) => props.theme.codemirror.tokens.variable} !important;\n    }\n    span.cm-keyword {\n      color: ${(props) => props.theme.codemirror.tokens.keyword} !important;\n    }\n    span.cm-comment {\n      color: ${(props) => props.theme.codemirror.tokens.comment} !important;\n    }\n    span.cm-operator {\n      color: ${(props) => props.theme.codemirror.tokens.operator} !important;\n    }\n    span.cm-tag {\n      color: ${(props) => props.theme.codemirror.tokens.tag} !important;\n    }\n    span.cm-tag.cm-bracket {\n      color: ${(props) => props.theme.codemirror.tokens.tagBracket} !important;\n    }\n  }\n\n  /* Variable validation colors */\n  .cm-variable-valid {\n    color: ${(props) => props.theme.codemirror.variable.valid};\n  }\n  .cm-variable-invalid {\n    color: ${(props) => props.theme.codemirror.variable.invalid};\n  }\n\n\n  .CodeMirror-search-hint {\n    display: inline;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/QueryEditor/index.js",
    "content": "/**\n *  Copyright (c) 2021 GraphQL Contributors.\n *\n *  This source code is licensed under the MIT license found in the\n *  LICENSE file in the root directory of this source tree.\n */\n\nimport React from 'react';\nimport isEqual from 'lodash/isEqual';\nimport MD from 'markdown-it';\nimport { format } from 'prettier/standalone';\nimport prettierPluginGraphql from 'prettier/parser-graphql';\nimport { getAllVariables } from 'utils/collections';\nimport { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';\nimport toast from 'react-hot-toast';\nimport StyledWrapper from './StyledWrapper';\nimport { IconWand } from '@tabler/icons';\n\nimport onHasCompletion from './onHasCompletion';\nimport { setupLinkAware } from 'utils/codemirror/linkAware';\n\nconst CodeMirror = require('codemirror');\n\nconst md = new MD();\nconst AUTO_COMPLETE_AFTER_KEY = /^[a-zA-Z0-9_@(]$/;\n\nconst createSafeGraphQLLinter = () => {\n  // Get the original GraphQL lint helper registered by codemirror-graphql\n  const originalLinter = CodeMirror.helpers?.lint?.graphql?.[0];\n\n  return (text, options) => {\n    try {\n      if (originalLinter) {\n        return originalLinter(text, options);\n      }\n      return [];\n    } catch (error) {\n      // Log the error but don't crash - return empty lint results\n      // This can happen if the schema has validation issues\n      console.warn('GraphQL lint error (schema may be invalid):', error.message);\n      return [];\n    }\n  };\n};\n\nexport default class QueryEditor extends React.Component {\n  constructor(props) {\n    super(props);\n\n    // Keep a cached version of the value, this cache will be updated when the\n    // editor is updated, which can later be used to protect the editor from\n    // unnecessary updates during the update lifecycle.\n    this.cachedValue = props.value || '';\n    this.variables = {};\n  }\n\n  componentDidMount() {\n    const editor = (this.editor = CodeMirror(this._node, {\n      value: this.props.value || '',\n      lineNumbers: true,\n      tabSize: 2,\n      mode: 'graphql',\n      // mode: 'brunovariables',\n      brunoVarInfo: {\n        variables: getAllVariables(this.props.collection)\n      },\n      theme: this.props.editorTheme || 'graphiql',\n      theme: this.props.theme === 'dark' ? 'monokai' : 'default',\n      keyMap: 'sublime',\n      autoCloseBrackets: true,\n      matchBrackets: true,\n      showCursorWhenSelecting: true,\n      scrollbarStyle: 'overlay',\n      readOnly: this.props.readOnly ? 'nocursor' : false,\n      foldGutter: {\n        minFoldSize: 4\n      },\n      lint: {\n        getAnnotations: createSafeGraphQLLinter(),\n        schema: this.props.schema,\n        validationRules: this.props.validationRules ?? null,\n        // linting accepts string or FragmentDefinitionNode[]\n        externalFragments: this.props?.externalFragments\n      },\n      hintOptions: {\n        schema: this.props.schema,\n        closeOnUnfocus: false,\n        completeSingle: false,\n        container: this._node,\n        externalFragments: this.props?.externalFragments\n      },\n      info: {\n        schema: this.props.schema,\n        renderDescription: (text) => md.render(text),\n        onClick: (reference) => this.props.onClickReference && this.props.onClickReference(reference)\n      },\n      jump: {\n        schema: this.props.schema,\n        onClick: (reference) => this.props.onClickReference && this.props.onClickReference(reference)\n      },\n      gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],\n      extraKeys: {\n        'Cmd-Space': () => editor.showHint({ completeSingle: true, container: this._node }),\n        'Ctrl-Space': () => editor.showHint({ completeSingle: true, container: this._node }),\n        'Alt-Space': () => editor.showHint({ completeSingle: true, container: this._node }),\n        'Shift-Space': () => editor.showHint({ completeSingle: true, container: this._node }),\n        'Shift-Alt-Space': () => editor.showHint({ completeSingle: true, container: this._node }),\n        'Cmd-Enter': () => {\n          if (this.props.onRun) {\n            this.props.onRun();\n          }\n        },\n        'Ctrl-Enter': () => {\n          if (this.props.onRun) {\n            this.props.onRun();\n          }\n        },\n        'Shift-Ctrl-C': () => {\n          if (this.props.onCopyQuery) {\n            this.props.onCopyQuery();\n          }\n        },\n        'Shift-Ctrl-P': () => {\n          if (this.props.onPrettifyQuery) {\n            this.props.onPrettifyQuery();\n          }\n        },\n        /* Shift-Ctrl-P is hard coded in Firefox for private browsing so adding an alternative to Prettify */\n        'Shift-Ctrl-F': () => {\n          if (this.props.onPrettifyQuery) {\n            this.props.onPrettifyQuery();\n          }\n        },\n        'Shift-Ctrl-M': () => {\n          if (this.props.onMergeQuery) {\n            this.props.onMergeQuery();\n          }\n        },\n        'Cmd-S': () => {\n          if (this.props.onSave) {\n            this.props.onSave();\n            return false;\n          }\n        },\n        'Ctrl-S': () => {\n          if (this.props.onSave) {\n            this.props.onSave();\n            return false;\n          }\n        },\n        'Cmd-F': 'findPersistent',\n        'Ctrl-F': 'findPersistent'\n      }\n    }));\n    if (editor) {\n      editor.on('change', this._onEdit);\n      editor.on('keyup', this._onKeyUp);\n      editor.on('hasCompletion', this._onHasCompletion);\n      editor.on('beforeChange', this._onBeforeChange);\n    }\n    this.addOverlay();\n\n    setupLinkAware(editor);\n  }\n\n  componentDidUpdate(prevProps) {\n    // Ensure the changes caused by this update are not interpreted as\n    // user-input changes which could otherwise result in an infinite\n    // event loop.\n    this.ignoreChangeEvent = true;\n    if (this.props.schema !== prevProps.schema && this.editor) {\n      this.editor.options.lint.schema = this.props.schema;\n      this.editor.options.hintOptions.schema = this.props.schema;\n      this.editor.options.info.schema = this.props.schema;\n      this.editor.options.jump.schema = this.props.schema;\n      CodeMirror.signal(this.editor, 'change', this.editor);\n    }\n    if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {\n      // TODO: temporary fix for keeping cursor state when auto save and new line insertion collide PR#7098\n      const nextValue = this.props.value ?? '';\n      const currentValue = this.editor.getValue();\n      if (this.editor.hasFocus?.() && currentValue !== nextValue) {\n        this.cachedValue = currentValue;\n      } else {\n        this.cachedValue = nextValue;\n        this.editor.setValue(nextValue);\n      }\n    }\n\n    if (this.props.theme !== prevProps.theme && this.editor) {\n      this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');\n    }\n    let variables = getAllVariables(this.props.collection);\n    if (!isEqual(variables, this.variables)) {\n      this.editor.options.brunoVarInfo.variables = variables;\n      this.addOverlay();\n    }\n    this.ignoreChangeEvent = false;\n  }\n\n  componentWillUnmount() {\n    if (this.editor) {\n      if (this.editor?._destroyLinkAware) {\n        this.editor._destroyLinkAware();\n      }\n      this.editor.off('change', this._onEdit);\n      this.editor.off('keyup', this._onKeyUp);\n      this.editor.off('hasCompletion', this._onHasCompletion);\n      this.editor = null;\n    }\n  }\n\n  beautifyRequestBody = () => {\n    try {\n      const prettyQuery = format(this.props.value, {\n        parser: 'graphql',\n        plugins: [prettierPluginGraphql]\n      });\n\n      this.editor.setValue(prettyQuery);\n      toast.success('Query prettified');\n    } catch (e) {\n      toast.error('Error occurred while prettifying GraphQL query');\n    }\n  };\n\n  // Todo: Overlay is messing up with schema hint\n  // Fix this\n  addOverlay = () => {\n    // let variables = getAllVariables(this.props.collection);\n    // this.variables = variables;\n    // defineCodeMirrorBrunoVariablesMode(variables, 'graphql');\n    // this.editor.setOption('mode', 'brunovariables');\n  };\n\n  render() {\n    return (\n      <>\n        <StyledWrapper\n          className=\"h-full w-full  flex flex-col relative graphiql-container\"\n          aria-label=\"Query Editor\"\n          font={this.props.font}\n          fontSize={this.props.fontSize}\n          ref={(node) => {\n            this._node = node;\n          }}\n        >\n          <button\n            className=\"btn-add-param text-link px-4 py-4 select-none absolute top-0 right-0 z-10\"\n            onClick={this.beautifyRequestBody}\n            title=\"prettify\"\n          >\n            <IconWand size={20} strokeWidth={1.5} />\n          </button>\n        </StyledWrapper>\n      </>\n    );\n  }\n\n  _onKeyUp = (_cm, e) => {\n    if (e.metaKey || e.ctrlKey || e.altKey) {\n      return;\n    }\n    if (AUTO_COMPLETE_AFTER_KEY.test(e.key) && this.editor) {\n      this.editor.execCommand('autocomplete');\n    }\n  };\n\n  _onEdit = () => {\n    if (!this.ignoreChangeEvent && this.editor) {\n      this.cachedValue = this.editor.getValue();\n      if (this.props.onEdit) {\n        this.props.onEdit(this.cachedValue);\n      }\n    }\n  };\n\n  /**\n   * Render a custom UI for CodeMirror's hint which includes additional info\n   * about the type and description for the selected context.\n   */\n  _onHasCompletion = (cm, data) => {\n    onHasCompletion(cm, data, this.props.onHintInformationRender);\n  };\n\n  _onBeforeChange(_instance, change) {\n    const normalizeWhitespace = (line) => {\n      // Unicode whitespace characters that break the interface.\n      const invalidCharacters = Array.from({ length: 11 }, (_, i) => {\n        // \\u2000 -> \\u200a\n        return String.fromCharCode(0x2000 + i);\n      }).concat(['\\u2028', '\\u2029', '\\u202f', '\\u00a0']);\n\n      const sanitizeRegex = new RegExp('[' + invalidCharacters.join('') + ']', 'g');\n      return line.replace(sanitizeRegex, ' ');\n    };\n\n    // The update function is only present on non-redo, non-undo events.\n    if (change.origin === 'paste') {\n      const text = change.text.map(normalizeWhitespace);\n      change.update(change.from, change.to, text);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/QueryEditor/onHasCompletion.js",
    "content": "/**\n *  Copyright (c) 2021 GraphQL Contributors.\n *\n *  This source code is licensed under the MIT license found in the\n *  LICENSE file in the root directory of this source tree.\n */\nimport escapeHTML from 'escape-html';\nimport MD from 'markdown-it';\n\nimport { GraphQLNonNull, GraphQLList } from 'graphql';\n\nconst md = new MD();\n\n/**\n * Render a custom UI for CodeMirror's hint which includes additional info\n * about the type and description for the selected context.\n */\nexport default function onHasCompletion(_cm, data, onHintInformationRender) {\n  const CodeMirror = require('codemirror');\n\n  let information;\n  let deprecation;\n\n  // When a hint result is selected, we augment the UI with information.\n  CodeMirror.on(data, 'select', (ctx, el) => {\n    // Only the first time (usually when the hint UI is first displayed)\n    // do we create the information nodes.\n    if (!information) {\n      const hintsUl = el.parentNode;\n\n      // This \"information\" node will contain the additional info about the\n      // highlighted typeahead option.\n      information = document.createElement('div');\n      information.className = 'CodeMirror-hint-information';\n      hintsUl.appendChild(information);\n\n      // This \"deprecation\" node will contain info about deprecated usage.\n      deprecation = document.createElement('div');\n      deprecation.className = 'CodeMirror-hint-deprecation';\n      hintsUl.appendChild(deprecation);\n\n      // When CodeMirror attempts to remove the hint UI, we detect that it was\n      // removed and in turn remove the information nodes.\n      let onRemoveFn;\n      hintsUl.addEventListener(\n        'DOMNodeRemoved',\n        (onRemoveFn = (event) => {\n          if (event.target === hintsUl) {\n            hintsUl.removeEventListener('DOMNodeRemoved', onRemoveFn);\n            information = null;\n            deprecation = null;\n            onRemoveFn = null;\n          }\n        })\n      );\n    }\n\n    // Now that the UI has been set up, add info to information.\n    const description = ctx.description ? md.render(ctx.description) : 'Self descriptive.';\n    const type = ctx.type ? '<span className=\"infoType\">' + renderType(ctx.type) + '</span>' : '';\n\n    information.innerHTML\n      = '<div className=\"content\">'\n        + (description.slice(0, 3) === '<p>' ? '<p>' + type + description.slice(3) : type + description)\n        + '</div>';\n\n    if (ctx && deprecation && ctx.deprecationReason) {\n      const reason = ctx.deprecationReason ? md.render(ctx.deprecationReason) : '';\n      deprecation.innerHTML = '<span className=\"deprecation-label\">Deprecated</span>' + reason;\n      deprecation.style.display = 'block';\n    } else if (deprecation) {\n      deprecation.style.display = 'none';\n    }\n\n    // Additional rendering?\n    if (onHintInformationRender) {\n      onHintInformationRender(information);\n    }\n  });\n}\n\nfunction renderType(type) {\n  if (type instanceof GraphQLNonNull) {\n    return `${renderType(type.ofType)}!`;\n  }\n  if (type instanceof GraphQLList) {\n    return `[${renderType(type.ofType)}]`;\n  }\n  return `<a className=\"typeName\">${escapeHTML(type.name)}</a>`;\n}\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  div.title {\n    color: ${(props) => props.theme.colors.text.subtext0};\n  }\n  table {\n    width: 100%;\n    border-collapse: collapse;\n    font-weight: 500;\n    table-layout: fixed;\n\n    thead,\n    td {\n      border: 1px solid ${(props) => props.theme.table.border};\n    }\n\n    thead {\n      color: ${(props) => props.theme.table.thead.color};\n      font-size: ${(props) => props.theme.font.size.base};\n      user-select: none;\n    }\n    td {\n      padding: 6px 10px;\n    }\n  }\n\n  td {\n    &:nth-child(1) {\n      padding: 0 0 0 8px;\n    }\n  }\n\n  .btn-action {\n    font-size: ${(props) => props.theme.font.size.base};\n    &:hover span {\n      text-decoration: underline;\n    }\n  }\n\n  input[type='text'] {\n    width: 100%;\n    border: solid 1px transparent;\n    outline: none !important;\n    background-color: inherit;\n\n    &:focus {\n      outline: none !important;\n      border: solid 1px transparent;\n    }\n  }\n\n  input[type='checkbox'] {\n    cursor: pointer;\n    position: relative;\n    top: 1px;\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/QueryParams/index.js",
    "content": "import React, { useState, useCallback } from 'react';\nimport get from 'lodash/get';\nimport InfoTip from 'components/InfoTip';\nimport { useDispatch } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport {\n  moveQueryParam,\n  updatePathParam,\n  setQueryParams\n} from 'providers/ReduxStore/slices/collections';\nimport MultiLineEditor from 'components/MultiLineEditor';\nimport { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport EditableTable from 'components/EditableTable';\nimport StyledWrapper from './StyledWrapper';\nimport BulkEditor from '../../BulkEditor';\n\nconst QueryParams = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n  const params = item.draft ? get(item, 'draft.request.params') : get(item, 'request.params');\n  const queryParams = params.filter((param) => param.type === 'query');\n  const pathParams = params.filter((param) => param.type === 'path');\n\n  const [isBulkEditMode, setIsBulkEditMode] = useState(false);\n\n  const onSave = () => dispatch(saveRequest(item.uid, collection.uid));\n  const handleRun = () => dispatch(sendRequest(item, collection.uid));\n\n  const handleQueryParamsChange = useCallback((updatedParams) => {\n    const paramsWithType = updatedParams.map((p) => ({ ...p, type: 'query' }));\n    dispatch(setQueryParams({\n      collectionUid: collection.uid,\n      itemUid: item.uid,\n      params: paramsWithType\n    }));\n  }, [dispatch, collection.uid, item.uid]);\n\n  const handlePathParamChange = useCallback((rowUid, key, value) => {\n    const pathParam = pathParams.find((p) => p.uid === rowUid);\n    if (pathParam) {\n      dispatch(updatePathParam({\n        pathParam: { ...pathParam, [key]: value },\n        itemUid: item.uid,\n        collectionUid: collection.uid\n      }));\n    }\n  }, [dispatch, pathParams, item.uid, collection.uid]);\n\n  const handleQueryParamDrag = useCallback(({ updateReorderedItem }) => {\n    dispatch(moveQueryParam({\n      collectionUid: collection.uid,\n      itemUid: item.uid,\n      updateReorderedItem\n    }));\n  }, [dispatch, collection.uid, item.uid]);\n\n  const toggleBulkEditMode = () => {\n    setIsBulkEditMode(!isBulkEditMode);\n  };\n\n  const queryColumns = [\n    {\n      key: 'name',\n      name: 'Name',\n      isKeyField: true,\n      placeholder: 'Name',\n      width: '30%'\n    },\n    {\n      key: 'value',\n      name: 'Value',\n      placeholder: 'Value',\n      render: ({ value, onChange }) => (\n        <MultiLineEditor\n          value={value || ''}\n          theme={storedTheme}\n          onSave={onSave}\n          onChange={onChange}\n          onRun={handleRun}\n          collection={collection}\n          item={item}\n          variablesAutocomplete={true}\n          placeholder={!value ? 'Value' : ''}\n        />\n      )\n    }\n  ];\n\n  const pathColumns = [\n    {\n      key: 'name',\n      name: 'Name',\n      isKeyField: true,\n      width: '30%',\n      readOnly: true\n    },\n    {\n      key: 'value',\n      name: 'Value',\n      placeholder: 'Value',\n      render: ({ row, value, onChange }) => (\n        <MultiLineEditor\n          value={value || ''}\n          theme={storedTheme}\n          onSave={onSave}\n          onChange={(newValue) => handlePathParamChange(row.uid, 'value', newValue)}\n          onRun={handleRun}\n          collection={collection}\n          item={item}\n        />\n      )\n    }\n  ];\n\n  const defaultQueryRow = {\n    name: '',\n    value: '',\n    description: '',\n    type: 'query'\n  };\n\n  if (isBulkEditMode) {\n    return (\n      <StyledWrapper className=\"w-full mt-3\">\n        <BulkEditor\n          params={queryParams}\n          onChange={handleQueryParamsChange}\n          onToggle={toggleBulkEditMode}\n          onSave={onSave}\n          onRun={handleRun}\n        />\n      </StyledWrapper>\n    );\n  }\n\n  return (\n    <StyledWrapper className=\"w-full flex flex-col\">\n      <div className=\"flex-1\">\n        <div className=\"mb-3 title text-xs\">Query</div>\n        <EditableTable\n          columns={queryColumns}\n          rows={queryParams || []}\n          onChange={handleQueryParamsChange}\n          defaultRow={defaultQueryRow}\n          reorderable={true}\n          onReorder={handleQueryParamDrag}\n        />\n        <div className=\"flex justify-end mt-2\">\n          <button className=\"btn-action text-link select-none\" onClick={toggleBulkEditMode}>\n            Bulk Edit\n          </button>\n        </div>\n\n        <div className=\"mb-3 title text-xs flex items-stretch\">\n          <span>Path</span>\n          <InfoTip infotipId=\"path-param-InfoTip\">\n            <div>\n              Path variables are automatically added whenever the\n              <code className=\"font-mono mx-2\">:name</code>\n              template is used in the URL. <br /> For example:\n              <code className=\"font-mono mx-2\">\n                https://example.com/v1/users/<span>:id</span>\n              </code>\n            </div>\n          </InfoTip>\n        </div>\n        {pathParams && pathParams.length > 0 ? (\n          <EditableTable\n            columns={pathColumns}\n            rows={pathParams}\n            onChange={() => {}}\n            defaultRow={{}}\n            showCheckbox={false}\n            showDelete={false}\n            showAddRow={false}\n          />\n        ) : (\n          <div className=\"title pr-2 py-3 mt-2 text-xs\"></div>\n        )}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default QueryParams;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/QueryUrl/HttpMethodSelector/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  font-size: ${(props) => props.theme.font.size.base};\n  height: 100%;\n  display: flex;\n  align-items: stretch;\n  border-radius: 4px;\n  transition: background-color 0.15s ease;\n\n  .dropdown {\n    width: 100%;\n    display: flex;\n    align-items: stretch;\n  }\n\n  .method-selector {\n    display: flex;\n    align-items: center;\n    margin: 2px;\n    border-radius: ${(props) => props.theme.border.radius.sm};\n\n    &:not(.custom-input-mode):hover,\n    &:has(button[aria-expanded=\"true\"]) {\n      background-color: color-mix(in srgb, currentColor 15%, transparent);\n    }\n\n\n    .tippy-box {\n      max-width: 150px !important;\n      min-width: 110px !important;\n    }\n\n    .dropdown-item {\n      padding: 0.25rem 0.6rem !important;\n    }\n\n    .text-link {\n      color: ${(props) => props.theme.textLink};\n    }\n  }\n\n  input {\n    background-color: ${(props) => props.theme.requestTabPanel.url.bg};\n    outline: none;\n    box-shadow: none;\n    text-align: left;\n\n    &:focus {\n      outline: none !important;\n      box-shadow: none !important;\n    }\n  }\n\n  .method-span {\n    display: block;\n    max-width: 15ch;\n    text-overflow: ellipsis;\n    overflow: hidden;\n    white-space: nowrap;\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-weight: 500;\n    line-height: 1.5;\n  }\n\n  .caret {\n    color: currentColor;\n    fill: currentColor;\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/QueryUrl/HttpMethodSelector/index.js",
    "content": "import React, { useState, useRef, useMemo, useCallback } from 'react';\nimport { IconCaretDown } from '@tabler/icons';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport StyledWrapper from './StyledWrapper';\nimport { useTheme } from 'providers/Theme';\n\nconst STANDARD_METHODS = Object.freeze(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'TRACE', 'CONNECT']);\n\nconst KEY = Object.freeze({ ENTER: 'Enter', ESCAPE: 'Escape' });\n\nconst DEFAULT_METHOD = 'GET';\n\nconst TriggerButton = ({ method, methodSpanRef, showCaret, ...props }) => {\n  return (\n    <button\n      type=\"button\"\n      className=\"cursor-pointer flex items-center gap-2 text-left w-full select-none px-2\"\n      {...props}\n    >\n      <span\n        ref={methodSpanRef}\n        className=\"truncate method-span\"\n        id=\"create-new-request-method\"\n        title={method}\n      >\n        {method}\n      </span>\n      {showCaret && <IconCaretDown className=\"caret\" size={14} strokeWidth={2} />}\n    </button>\n  );\n};\n\nconst HttpMethodSelector = ({ method = DEFAULT_METHOD, onMethodSelect, showCaret = false }) => {\n  const [isCustomMode, setIsCustomMode] = useState(false);\n  const inputRef = useRef();\n  const selectedMethodRef = useRef(method);\n  const methodSpanRef = useRef();\n  const [previousMethodWidth, setPreviousMethodWidth] = useState(null);\n\n  const { theme } = useTheme();\n  const methodColor = useMemo(() => {\n    const colorMap = {\n      ...theme.request.methods,\n      ...theme.request\n    };\n    return colorMap[method.toLocaleLowerCase()];\n  }, [method, theme]);\n\n  const blurInput = () => inputRef.current?.blur();\n\n  const handleInputChange = (e) => {\n    const val = e.target.value.toUpperCase();\n    onMethodSelect(val);\n  };\n\n  const handleMethodSelect = useCallback((verb) => {\n    onMethodSelect(verb);\n    selectedMethodRef.current = verb;\n    setIsCustomMode(false);\n    blurInput();\n  }, [onMethodSelect]);\n\n  const handleBlur = (e) => {\n    // Keep the current value when blurring\n    let currentValue = '';\n    if (e.target.value && e.target.value.length > 0) {\n      currentValue = e.target.value.toUpperCase();\n      selectedMethodRef.current = currentValue;\n    } else {\n      currentValue = selectedMethodRef.current;\n    }\n    onMethodSelect(currentValue);\n    setIsCustomMode(false);\n  };\n\n  const handleAddCustomMethod = useCallback(() => {\n    // Capture the width of the current method span before switching to custom mode\n    if (methodSpanRef.current) {\n      setPreviousMethodWidth(methodSpanRef.current.offsetWidth);\n    }\n    setIsCustomMode(true);\n    onMethodSelect('');\n\n    setTimeout(() => {\n      inputRef.current?.focus();\n      inputRef.current?.select();\n    }, 0);\n  }, [onMethodSelect]);\n\n  const handleKeyDown = (e) => {\n    switch (e.key) {\n      case KEY.ESCAPE: {\n        setIsCustomMode(false);\n        blurInput();\n        e.preventDefault();\n        e.stopPropagation();\n        return;\n      }\n      case KEY.ENTER: {\n        onMethodSelect(e.target.value ? e.target.value.toUpperCase() : DEFAULT_METHOD);\n        setIsCustomMode(false);\n        blurInput();\n        return;\n      }\n      default: {\n        return;\n      }\n    }\n  };\n\n  // Convert STANDARD_METHODS to MenuDropdown items format\n  const menuItems = useMemo(() => {\n    const items = STANDARD_METHODS.map((verb) => ({\n      id: verb.toLowerCase(),\n      label: verb,\n      onClick: () => handleMethodSelect(verb)\n    }));\n\n    // Add \"Add Custom\" item\n    items.push({\n      id: 'add-custom',\n      label: '+ Add Custom',\n      onClick: handleAddCustomMethod,\n      className: 'font-normal mt-1 text-link'\n    });\n\n    return items;\n  }, [handleMethodSelect, handleAddCustomMethod]);\n\n  // Determine selected item ID (only if method is a standard method)\n  const selectedItemId = useMemo(() => {\n    if (isCustomMode || !STANDARD_METHODS.includes(method)) {\n      return null;\n    }\n    return method.toLowerCase();\n  }, [method, isCustomMode]);\n\n  // If in custom mode, render input field instead of dropdown\n  if (isCustomMode) {\n    // Calculate width based on content length (in ch units), clamped to max 16ch\n    // Add 1ch for cursor space\n    const contentWidth = Math.min(method.length + 1, 16);\n    // Use previous method width as minimum, content-based width as current\n    const minWidthPx = previousMethodWidth ? `${previousMethodWidth}px` : '5ch';\n    // Use calc to add padding space (px-2 = 0.5rem per side = 1rem total) to the ch width\n    const currentWidth = `calc(${Math.max(contentWidth, 1)}ch + 1rem)`;\n\n    return (\n      <StyledWrapper>\n        <div className=\"flex method-selector custom-input-mode\" style={{ color: methodColor }}>\n          <div className=\"flex flex-col w-full\">\n            <input\n              ref={inputRef}\n              type=\"text\"\n              className=\"px-2 focus:bg-transparent\"\n              style={{\n                minWidth: minWidthPx,\n                width: currentWidth,\n                maxWidth: 'calc(16ch + 1rem)',\n                fontSize: '12px'\n              }}\n              value={method}\n              onChange={handleInputChange}\n              onBlur={handleBlur}\n              onKeyDown={handleKeyDown}\n              title={method}\n              autoFocus\n            />\n          </div>\n        </div>\n      </StyledWrapper>\n    );\n  }\n\n  return (\n    <StyledWrapper>\n      <div className=\"flex method-selector\" style={{ color: methodColor }}>\n        <MenuDropdown\n          items={menuItems}\n          placement=\"bottom-start\"\n          selectedItemId={selectedItemId}\n          data-testid=\"method-selector\"\n        >\n          <TriggerButton method={method} showCaret={showCaret} methodSpanRef={methodSpanRef} />\n        </MenuDropdown>\n      </div>\n    </StyledWrapper>\n  );\n};\nexport default HttpMethodSelector;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/QueryUrl/HttpMethodSelector/index.spec.js",
    "content": "import '@testing-library/jest-dom';\nimport React from 'react';\nimport { render, screen, fireEvent, waitFor } from '@testing-library/react';\nimport { ThemeProvider } from 'styled-components';\nimport HttpMethodSelector from './index';\nimport themes from 'themes/index';\nimport userEvent from '@testing-library/user-event';\nimport { ThemeContext } from 'providers/Theme';\n\nconst renderWithTheme = (component) => {\n  return render(\n    <ThemeContext.Provider value={{ theme: themes.dark }}>\n      <ThemeProvider theme={themes.dark}>\n        {component}\n      </ThemeProvider>\n    </ThemeContext.Provider>\n  );\n};\n\ndescribe('HttpMethodSelector', () => {\n  const mockOnMethodSelect = jest.fn();\n\n  beforeEach(() => {\n    mockOnMethodSelect.mockClear();\n  });\n\n  describe('Initial Render', () => {\n    it('should render with default GET method when no method prop is provided', () => {\n      renderWithTheme(<HttpMethodSelector onMethodSelect={mockOnMethodSelect} />);\n\n      const methodSpan = screen.getByText('GET');\n      expect(methodSpan).toBeInTheDocument();\n      expect(methodSpan).toHaveClass('method-span');\n      expect(methodSpan).toHaveAttribute('title', 'GET');\n    });\n\n    it('should render with a standard method when method prop is provided', () => {\n      renderWithTheme(<HttpMethodSelector method=\"POST\" onMethodSelect={mockOnMethodSelect} />);\n\n      const methodSpan = screen.getByText('POST');\n      expect(methodSpan).toBeInTheDocument();\n      expect(methodSpan).toHaveAttribute('title', 'POST');\n    });\n\n    it('should render with a custom method when method prop is provided', () => {\n      renderWithTheme(<HttpMethodSelector method=\"CUSTOM\" onMethodSelect={mockOnMethodSelect} />);\n\n      const methodSpan = screen.getByText('CUSTOM');\n      expect(methodSpan).toBeInTheDocument();\n      expect(methodSpan).toHaveAttribute('title', 'CUSTOM');\n    });\n  });\n\n  describe('Dropdown Interaction', () => {\n    it('should display all standard HTTP methods in dropdown when clicked', async () => {\n      renderWithTheme(<HttpMethodSelector onMethodSelect={mockOnMethodSelect} />);\n\n      const button = screen.getByRole('button');\n      fireEvent.click(button);\n\n      await waitFor(() => {\n        const standardMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'TRACE', 'CONNECT'];\n        const dropdownItems = screen.getAllByRole('menuitem');\n        const renderedMethods = dropdownItems.map((item) => item.textContent.trim());\n\n        standardMethods.forEach((method, index) => {\n          // GET should have a checkmark (✓) since it's the default selected method\n          const expectedText = index === 0 ? method + '✓' : method;\n          expect(renderedMethods).toContain(expectedText);\n        });\n      });\n    });\n\n    it('should display \"Add Custom\" option in dropdown', async () => {\n      renderWithTheme(<HttpMethodSelector onMethodSelect={mockOnMethodSelect} />);\n\n      const button = screen.getByRole('button');\n      fireEvent.click(button);\n\n      await waitFor(() => {\n        const addCustomSpan = screen.getByText('+ Add Custom');\n        expect(addCustomSpan).toBeInTheDocument();\n        // The className is applied to the parent dropdown-item div, not the label span\n        expect(addCustomSpan.closest('.dropdown-item')).toHaveClass('text-link');\n      });\n    });\n\n    it('should call onMethodSelect when a standard method is clicked', async () => {\n      renderWithTheme(<HttpMethodSelector onMethodSelect={mockOnMethodSelect} />);\n\n      const button = screen.getByRole('button');\n      fireEvent.click(button);\n\n      await waitFor(() => {\n        const postMethod = screen.getByRole('menuitem', { name: /^POST/ });\n        expect(postMethod).toBeInTheDocument();\n      });\n\n      const postMethod = screen.getByRole('menuitem', { name: /^POST/ });\n      fireEvent.click(postMethod);\n\n      expect(mockOnMethodSelect).toHaveBeenCalledWith('POST');\n    });\n  });\n\n  describe('Custom Method Mode', () => {\n    it('should enter custom mode when \"Add Custom\" is clicked', async () => {\n      renderWithTheme(<HttpMethodSelector onMethodSelect={mockOnMethodSelect} />);\n\n      const button = screen.getByRole('button');\n      fireEvent.click(button);\n\n      await waitFor(() => {\n        const addCustom = screen.getByText('+ Add Custom');\n        fireEvent.click(addCustom);\n      });\n\n      expect(mockOnMethodSelect).toHaveBeenCalledWith('');\n\n      // Should show input field\n      await waitFor(() => {\n        const input = screen.getByRole('textbox');\n        expect(input).toBeInTheDocument();\n        expect(input).toHaveFocus();\n      });\n    });\n\n    it('should call onMethodSelect with uppercase value when typing in custom input', async () => {\n      const user = userEvent.setup();\n\n      // Create a wrapper component that manages the method state\n      const TestWrapper = () => {\n        const [method, setMethod] = React.useState('GET');\n\n        const handleMethodSelect = (newMethod) => {\n          mockOnMethodSelect(newMethod);\n          setMethod(newMethod);\n        };\n\n        return (\n          <HttpMethodSelector\n            method={method}\n            onMethodSelect={handleMethodSelect}\n          />\n        );\n      };\n\n      renderWithTheme(<TestWrapper />);\n\n      const button = screen.getByRole('button');\n      fireEvent.click(button);\n\n      await waitFor(() => {\n        const addCustom = screen.getByText('+ Add Custom');\n        fireEvent.click(addCustom);\n      });\n\n      const input = await screen.findByRole('textbox');\n      await user.type(input, 'custom');\n\n      expect(mockOnMethodSelect).toHaveBeenCalledWith('');\n      expect(mockOnMethodSelect).toHaveBeenCalledWith('C');\n      expect(mockOnMethodSelect).toHaveBeenCalledWith('CU');\n      expect(mockOnMethodSelect).toHaveBeenCalledWith('CUS');\n      expect(mockOnMethodSelect).toHaveBeenCalledWith('CUST');\n      expect(mockOnMethodSelect).toHaveBeenCalledWith('CUSTO');\n      expect(mockOnMethodSelect).toHaveBeenCalledWith('CUSTOM');\n      expect(mockOnMethodSelect).toHaveBeenCalledTimes(7);\n    });\n\n    it('should exit custom mode and set method on Enter key', async () => {\n      renderWithTheme(<HttpMethodSelector onMethodSelect={mockOnMethodSelect} />);\n\n      const button = screen.getByRole('button');\n      fireEvent.click(button);\n\n      await waitFor(() => {\n        const addCustom = screen.getByText('+ Add Custom');\n        fireEvent.click(addCustom);\n      });\n\n      const input = await screen.findByRole('textbox');\n      fireEvent.change(input, { target: { value: 'CUSTOM' } });\n      fireEvent.keyDown(input, { key: 'Enter' });\n\n      expect(mockOnMethodSelect).toHaveBeenCalledWith('CUSTOM');\n\n      // Should exit custom mode\n      await waitFor(() => {\n        expect(screen.queryByRole('textbox')).not.toBeInTheDocument();\n      });\n    });\n\n    it('should set default method on Enter key with empty input', async () => {\n      renderWithTheme(<HttpMethodSelector onMethodSelect={mockOnMethodSelect} />);\n\n      const button = screen.getByRole('button');\n      fireEvent.click(button);\n\n      await waitFor(() => {\n        const addCustom = screen.getByText('+ Add Custom');\n        fireEvent.click(addCustom);\n      });\n\n      const input = await screen.findByRole('textbox');\n      fireEvent.keyDown(input, { key: 'Enter' });\n\n      expect(mockOnMethodSelect).toHaveBeenCalledWith('GET');\n    });\n\n    it('should exit custom mode on Escape key and keep the custom method', async () => {\n      renderWithTheme(<HttpMethodSelector method=\"POST\" onMethodSelect={mockOnMethodSelect} />);\n\n      const button = screen.getByRole('button');\n      fireEvent.click(button);\n\n      await waitFor(() => {\n        const addCustom = screen.getByText('+ Add Custom');\n        fireEvent.click(addCustom);\n      });\n\n      const input = await screen.findByRole('textbox');\n      fireEvent.change(input, { target: { value: 'CUSTOM' } });\n      fireEvent.keyDown(input, { key: 'Escape' });\n\n      // Should exit custom mode and onMethodSelect should be called with custom method\n      await waitFor(() => {\n        expect(screen.queryByRole('textbox')).not.toBeInTheDocument();\n        expect(mockOnMethodSelect).toHaveBeenCalledWith('CUSTOM');\n      });\n    });\n\n    it('should exit custom mode on blur and keep the custom method', async () => {\n      renderWithTheme(<HttpMethodSelector onMethodSelect={mockOnMethodSelect} />);\n\n      const button = screen.getByRole('button');\n      fireEvent.click(button);\n\n      await waitFor(() => {\n        const addCustom = screen.getByText('+ Add Custom');\n        fireEvent.click(addCustom);\n      });\n\n      const input = await screen.findByRole('textbox');\n      fireEvent.change(input, { target: { value: 'CUSTOM' } });\n      fireEvent.blur(input);\n\n      // Should exit custom mode and onMethodSelect should be called with custom method\n      await waitFor(() => {\n        expect(screen.queryByRole('textbox')).not.toBeInTheDocument();\n        expect(mockOnMethodSelect).toHaveBeenCalledWith('CUSTOM');\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/QueryUrl/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  height: 2.1rem;\n  border: ${(props) => props.theme.requestTabPanel.url.border};\n  border-radius: ${(props) => props.theme.border.radius.base};\n\n\n  .infotip {\n    position: relative;\n    display: inline-block;\n    cursor: pointer;\n  }\n\n  .infotip:hover .infotiptext {\n    visibility: visible;\n    opacity: 1;\n  }\n\n  .infotiptext {\n    visibility: hidden;\n    width: auto;\n    background-color: ${(props) => props.theme.background.surface2};\n    color: ${(props) => props.theme.text};\n    text-align: center;\n    border-radius: 4px;\n    padding: 4px 8px;\n    position: absolute;\n    z-index: 1;\n    bottom: 34px;\n    left: 50%;\n    transform: translateX(-50%);\n    opacity: 0;\n    transition: opacity 0.3s;\n    white-space: nowrap;\n  }\n\n  .infotiptext::after {\n    content: '';\n    position: absolute;\n    top: 100%;\n    left: 50%;\n    margin-left: -4px;\n    border-width: 4px;\n    border-style: solid;\n    border-color: ${(props) => props.theme.background.surface2} transparent transparent transparent;\n  }\n\n  .shortcut {\n    font-size: 0.625rem;\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/QueryUrl/index.js",
    "content": "import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';\nimport get from 'lodash/get';\nimport { useDispatch } from 'react-redux';\nimport {\n  requestUrlChanged,\n  updateRequestMethod,\n  setRequestHeaders,\n  updateRequestBodyMode,\n  updateRequestBody,\n  updateRequestGraphqlQuery,\n  updateRequestGraphqlVariables,\n  updateRequestAuthMode,\n  updateAuth\n} from 'providers/ReduxStore/slices/collections';\nimport { saveRequest, cancelRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport { getRequestFromCurlCommand } from 'utils/curl';\nimport HttpMethodSelector from './HttpMethodSelector';\nimport { useTheme } from 'providers/Theme';\nimport { IconDeviceFloppy, IconArrowRight, IconCode, IconSquareRoundedX } from '@tabler/icons';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport { isMacOS } from 'utils/common/platform';\nimport { hasRequestChanges } from 'utils/collections';\nimport StyledWrapper from './StyledWrapper';\nimport GenerateCodeItem from 'components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index';\nimport toast from 'react-hot-toast';\n\nconst QueryUrl = ({ item, collection, handleRun }) => {\n  const { theme, storedTheme } = useTheme();\n  const dispatch = useDispatch();\n  const method = item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');\n  const url = item.draft ? get(item, 'draft.request.url', '') : get(item, 'request.url', '');\n  const isMac = isMacOS();\n  const saveShortcut = isMac ? 'Cmd + S' : 'Ctrl + S';\n  const editorRef = useRef(null);\n  const isLoading = ['queued', 'sending'].includes(item.requestState);\n\n  const [generateCodeItemModalOpen, setGenerateCodeItemModalOpen] = useState(false);\n  const hasChanges = useMemo(() => hasRequestChanges(item), [item]);\n\n  const onSave = () => {\n    dispatch(saveRequest(item.uid, collection.uid));\n  };\n\n  const onUrlChange = (value) => {\n    if (!editorRef.current?.editor) return;\n    const editor = editorRef.current.editor;\n    const cursor = editor.getCursor();\n\n    const finalUrl = value?.trim() ?? value;\n\n    dispatch(\n      requestUrlChanged({\n        itemUid: item.uid,\n        collectionUid: collection.uid,\n        url: finalUrl\n      })\n    );\n\n    // Restore cursor position only if URL was trimmed\n    if (finalUrl !== value) {\n      setTimeout(() => {\n        if (editor) {\n          editor.setCursor(cursor);\n        }\n      }, 0);\n    }\n  };\n\n  const onMethodSelect = (verb) => {\n    dispatch(\n      updateRequestMethod({\n        method: verb,\n        itemUid: item.uid,\n        collectionUid: collection.uid\n      })\n    );\n  };\n\n  const handleGenerateCode = (e) => {\n    e.stopPropagation();\n    if (item?.request?.url !== '' || (item.draft?.request?.url !== undefined && item.draft?.request?.url !== '')) {\n      setGenerateCodeItemModalOpen(true);\n    } else {\n      toast.error('URL is required');\n    }\n  };\n\n  const handleGraphqlPaste = useCallback((event) => {\n    if (item.type !== 'graphql-request') {\n      return;\n    }\n\n    const clipboardData = event.clipboardData || window.clipboardData;\n    const pastedData = clipboardData.getData('Text');\n\n    const curlCommandRegex = /^\\s*curl\\s/i;\n    if (!curlCommandRegex.test(pastedData)) {\n      // Not a curl command, allow normal paste behavior\n      return;\n    }\n    event.preventDefault();\n    try {\n      const request = getRequestFromCurlCommand(pastedData, 'graphql-request');\n      if (!request || !request.url) {\n        toast.error('Invalid cURL command');\n        return;\n      }\n      // Update URL\n      dispatch(requestUrlChanged({\n        itemUid: item.uid,\n        collectionUid: collection.uid,\n        url: request.url\n      }));\n\n      // Update method\n      dispatch(updateRequestMethod({\n        method: request.method.toUpperCase(), // Convert to uppercase\n        itemUid: item.uid,\n        collectionUid: collection.uid\n      }));\n\n      // Update headers\n      if (request.headers && request.headers.length > 0) {\n        dispatch(setRequestHeaders({\n          collectionUid: collection.uid,\n          itemUid: item.uid,\n          headers: request.headers\n        }));\n      }\n\n      // Update body\n      if (request.body) {\n        const bodyMode = request.body.mode;\n        if (bodyMode === 'graphql') {\n          dispatch(updateRequestGraphqlQuery({\n            itemUid: item.uid,\n            collectionUid: collection.uid,\n            query: request.body.graphql.query\n          }));\n          let variables = request.body.graphql.variables;\n          try {\n            variables = JSON.parse(variables);\n          } catch (error) {\n            // Keep variables as-is if JSON parsing fails\n          }\n          dispatch(updateRequestGraphqlVariables({\n            itemUid: item.uid,\n            collectionUid: collection.uid,\n            variables: variables\n          }));\n        }\n\n        toast.success('GraphQL query imported successfully');\n      }\n    } catch (error) {\n      console.error('Error parsing cURL command:', error);\n      toast.error('Failed to parse GraphQL query');\n    }\n  }, [dispatch, item.uid, collection.uid]);\n\n  const handleHttpPaste = useCallback((event) => {\n    // Only enable curl paste detection for HTTP requests\n    if (item.type !== 'http-request') {\n      return;\n    }\n\n    const clipboardData = event.clipboardData || window.clipboardData;\n    const pastedData = clipboardData.getData('Text');\n\n    // Check if pasted data looks like a cURL command\n    const curlCommandRegex = /^\\s*curl\\s/i;\n    if (!curlCommandRegex.test(pastedData)) {\n      // Not a curl command, allow normal paste behavior\n      return;\n    }\n\n    // Prevent the default paste behavior\n    event.preventDefault();\n\n    try {\n      // Parse the curl command\n      const request = getRequestFromCurlCommand(pastedData);\n      if (!request || !request.url) {\n        toast.error('Invalid cURL command');\n        return;\n      }\n\n      // Update URL\n      dispatch(\n        requestUrlChanged({\n          itemUid: item.uid,\n          collectionUid: collection.uid,\n          url: request.url\n        })\n      );\n\n      // Update method\n      if (request.method) {\n        dispatch(\n          updateRequestMethod({\n            method: request.method.toUpperCase(), // Convert to uppercase\n            itemUid: item.uid,\n            collectionUid: collection.uid\n          })\n        );\n      }\n\n      // Update headers\n      if (request.headers && request.headers.length > 0) {\n        dispatch(\n          setRequestHeaders({\n            collectionUid: collection.uid,\n            itemUid: item.uid,\n            headers: request.headers\n          })\n        );\n      }\n\n      // Update body\n      if (request.body) {\n        const bodyMode = request.body.mode;\n\n        // Set body mode first\n        dispatch(\n          updateRequestBodyMode({\n            itemUid: item.uid,\n            collectionUid: collection.uid,\n            mode: bodyMode\n          })\n        );\n\n        // Set body content based on mode\n        if (bodyMode === 'json' && request.body.json) {\n          dispatch(\n            updateRequestBody({\n              itemUid: item.uid,\n              collectionUid: collection.uid,\n              content: request.body.json\n            })\n          );\n        } else if (bodyMode === 'text' && request.body.text) {\n          dispatch(\n            updateRequestBody({\n              itemUid: item.uid,\n              collectionUid: collection.uid,\n              content: request.body.text\n            })\n          );\n        } else if (bodyMode === 'xml' && request.body.xml) {\n          dispatch(\n            updateRequestBody({\n              itemUid: item.uid,\n              collectionUid: collection.uid,\n              content: request.body.xml\n            })\n          );\n        } else if (bodyMode === 'graphql' && request.body.graphql) {\n          if (request.body.graphql.query) {\n            dispatch(\n              updateRequestGraphqlQuery({\n                itemUid: item.uid,\n                collectionUid: collection.uid,\n                query: request.body.graphql.query\n              })\n            );\n          }\n          if (request.body.graphql.variables) {\n            dispatch(\n              updateRequestGraphqlVariables({\n                itemUid: item.uid,\n                collectionUid: collection.uid,\n                variables: request.body.graphql.variables\n              })\n            );\n          }\n        } else if (bodyMode === 'formUrlEncoded' && request.body.formUrlEncoded) {\n          // For formUrlEncoded, we need to set each param individually\n          // This is a limitation - we'd need to clear existing params first\n          // For now, we'll set the body mode and the user can manually adjust\n          // TODO: Implement proper formUrlEncoded param setting\n        } else if (bodyMode === 'multipartForm' && request.body.multipartForm) {\n          // For multipartForm, similar limitation\n          // TODO: Implement proper multipartForm param setting\n        }\n      }\n\n      // Update auth\n      if (request.auth) {\n        const authMode = request.auth.mode;\n        if (authMode) {\n          dispatch(\n            updateRequestAuthMode({\n              itemUid: item.uid,\n              collectionUid: collection.uid,\n              mode: authMode\n            })\n          );\n\n          // Set auth content based on mode\n          if (request.auth.basic) {\n            dispatch(\n              updateAuth({\n                mode: 'basic',\n                collectionUid: collection.uid,\n                itemUid: item.uid,\n                content: request.auth.basic\n              })\n            );\n          } else if (request.auth.bearer) {\n            dispatch(\n              updateAuth({\n                mode: 'bearer',\n                collectionUid: collection.uid,\n                itemUid: item.uid,\n                content: request.auth.bearer\n              })\n            );\n          } else if (request.auth.digest) {\n            dispatch(\n              updateAuth({\n                mode: 'digest',\n                collectionUid: collection.uid,\n                itemUid: item.uid,\n                content: request.auth.digest\n              })\n            );\n          } else if (request.auth.ntlm) {\n            dispatch(\n              updateAuth({\n                mode: 'ntlm',\n                collectionUid: collection.uid,\n                itemUid: item.uid,\n                content: request.auth.ntlm\n              })\n            );\n          } else if (request.auth.awsv4) {\n            dispatch(\n              updateAuth({\n                mode: 'awsv4',\n                collectionUid: collection.uid,\n                itemUid: item.uid,\n                content: request.auth.awsv4\n              })\n            );\n          } else if (request.auth.apikey) {\n            dispatch(\n              updateAuth({\n                mode: 'apikey',\n                collectionUid: collection.uid,\n                itemUid: item.uid,\n                content: request.auth.apikey\n              })\n            );\n          }\n        }\n      }\n\n      toast.success('cURL command imported successfully');\n    } catch (error) {\n      console.error('Error parsing cURL command:', error);\n      toast.error('Failed to parse cURL command');\n    }\n  },\n  [dispatch, item.uid, item.type, collection.uid]\n  );\n  const handleCancelRequest = (e) => {\n    e.preventDefault();\n    e.stopPropagation();\n    dispatch(cancelRequest(item.cancelTokenUid, item, collection));\n  };\n  return (\n    <StyledWrapper className=\"flex items-center w-full\">\n      <div className=\"flex items-center h-full min-w-fit\">\n        <HttpMethodSelector method={method} onMethodSelect={onMethodSelect} />\n      </div>\n      <div\n        id=\"request-url\"\n        className=\"h-full w-full flex flex-row input-container overflow-auto\"\n      >\n        <SingleLineEditor\n          ref={editorRef}\n          value={url}\n          placeholder=\"Enter URL or paste a cURL request\"\n          onSave={(finalValue) => onSave(finalValue)}\n          theme={storedTheme}\n          onChange={(newValue) => onUrlChange(newValue)}\n          onRun={handleRun}\n          onPaste={item.type === 'http-request' ? handleHttpPaste : item.type === 'graphql-request' ? handleGraphqlPaste : null}\n          collection={collection}\n          highlightPathParams={true}\n          item={item}\n          showNewlineArrow={true}\n        />\n\n      </div>\n      <div className=\"flex items-center h-full mx-2 gap-3 cursor-pointer\" id=\"send-request\" onClick={handleRun}>\n        <div\n          title=\"Generate Code\"\n          className=\"infotip\"\n          onClick={(e) => {\n            handleGenerateCode(e);\n          }}\n        >\n          <IconCode color={theme.requestTabs.icon.color} strokeWidth={1.5} size={20} className=\"cursor-pointer\" />\n          <span className=\"infotiptext text-xs\">Generate Code</span>\n        </div>\n        <div\n          title=\"Save Request\"\n          className=\"infotip\"\n          onClick={(e) => {\n            e.stopPropagation();\n            if (!hasChanges) return;\n            onSave();\n          }}\n        >\n          <IconDeviceFloppy\n            color={hasChanges ? theme.draftColor : theme.requestTabs.icon.color}\n            strokeWidth={1.5}\n            size={20}\n            className={`${hasChanges ? 'cursor-pointer' : 'cursor-default'}`}\n          />\n          <span className=\"infotiptext text-xs\">\n            Save <span className=\"shortcut\">({saveShortcut})</span>\n          </span>\n        </div>\n        {isLoading || item.response?.stream?.running ? (\n          <IconSquareRoundedX\n            color={theme.requestTabPanel.url.iconDanger}\n            strokeWidth={1.5}\n            size={20}\n            data-testid=\"cancel-request-icon\"\n            onClick={handleCancelRequest}\n          />\n        ) : (\n          <IconArrowRight\n            color={theme.requestTabPanel.url.icon}\n            strokeWidth={1.5}\n            size={20}\n            data-testid=\"send-arrow-icon\"\n          />\n        )}\n      </div>\n      {generateCodeItemModalOpen && (\n        <GenerateCodeItem\n          collectionUid={collection.uid}\n          item={item}\n          onClose={() => setGenerateCodeItemModalOpen(false)}\n        />\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default QueryUrl;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  font-size: ${(props) => props.theme.font.size.base};\n  white-space: nowrap;\n\n  .body-mode-selector {\n    background: transparent;\n    border-radius: 3px;\n\n    .selected-body-mode {\n      color: ${(props) => props.theme.primary.text};\n    }\n  }\n\n  .caret {\n    color: rgb(140, 140, 140);\n    fill: rgb(140, 140, 140);\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/RequestBody/RequestBodyMode/index.js",
    "content": "import React, { useMemo, useCallback } from 'react';\nimport get from 'lodash/get';\nimport {\n  IconCaretDown,\n  IconForms,\n  IconBraces,\n  IconCode,\n  IconFileText,\n  IconDatabase,\n  IconFile,\n  IconX\n} from '@tabler/icons';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport { useDispatch } from 'react-redux';\nimport { updateRequestBodyMode } from 'providers/ReduxStore/slices/collections';\nimport { humanizeRequestBodyMode } from 'utils/collections';\nimport StyledWrapper from './StyledWrapper';\nimport { updateRequestBody } from 'providers/ReduxStore/slices/collections/index';\nimport { toastError } from 'utils/common/error';\nimport { prettifyJsonString } from 'utils/common/index';\nimport xmlFormat from 'xml-formatter';\n\nconst DEFAULT_MODES = [\n  {\n    name: 'Form',\n    options: [\n      { id: 'multipartForm', label: 'Multipart Form', leftSection: IconForms },\n      { id: 'formUrlEncoded', label: 'Form URL Encoded', leftSection: IconForms }\n    ]\n  },\n  {\n    name: 'Raw',\n    options: [\n      { id: 'json', label: 'JSON', leftSection: IconBraces },\n      { id: 'xml', label: 'XML', leftSection: IconCode },\n      { id: 'text', label: 'TEXT', leftSection: IconFileText },\n      { id: 'sparql', label: 'SPARQL', leftSection: IconDatabase }\n    ]\n  },\n  {\n    name: 'Other',\n    options: [\n      { id: 'file', label: 'File / Binary', leftSection: IconFile },\n      { id: 'none', label: 'No Body', leftSection: IconX }\n    ]\n  }\n];\n\nconst RequestBodyMode = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');\n  const bodyMode = body?.mode;\n\n  const onModeChange = useCallback((value) => {\n    dispatch(\n      updateRequestBodyMode({\n        itemUid: item.uid,\n        collectionUid: collection.uid,\n        mode: value\n      })\n    );\n  }, [dispatch, item.uid, collection.uid]);\n\n  const onPrettify = () => {\n    if (body?.json && bodyMode === 'json') {\n      try {\n        const prettyBodyJson = prettifyJsonString(body.json);\n        dispatch(\n          updateRequestBody({\n            content: prettyBodyJson,\n            itemUid: item.uid,\n            collectionUid: collection.uid\n          })\n        );\n      } catch (e) {\n        toastError(new Error('Unable to prettify. Invalid JSON format.'));\n      }\n    } else if (body?.xml && bodyMode === 'xml') {\n      try {\n        const prettyBodyXML = xmlFormat(body.xml, { collapseContent: true });\n        dispatch(\n          updateRequestBody({\n            content: prettyBodyXML,\n            itemUid: item.uid,\n            collectionUid: collection.uid\n          })\n        );\n      } catch (e) {\n        toastError(new Error('Unable to prettify. Invalid XML format.'));\n      }\n    }\n  };\n\n  const menuItems = useMemo(() => {\n    return DEFAULT_MODES.map((group) => ({\n      ...group,\n      options: group.options.map((option) => ({\n        ...option,\n        onClick: () => onModeChange(option.id)\n      }))\n    }));\n  }, [onModeChange]);\n\n  return (\n    <StyledWrapper>\n      <div className=\"inline-flex items-center cursor-pointer body-mode-selector\" data-testid=\"request-body-mode-selector\">\n        <MenuDropdown\n          items={menuItems}\n          placement=\"bottom-end\"\n          selectedItemId={bodyMode}\n          showGroupDividers={false}\n          groupStyle=\"select\"\n        >\n          <div className=\"flex items-center justify-center pl-3 py-1 select-none selected-body-mode\">\n            {humanizeRequestBodyMode(bodyMode)} <IconCaretDown className=\"caret ml-1\" size={14} strokeWidth={2} />\n          </div>\n        </MenuDropdown>\n      </div>\n      {(bodyMode === 'json' || bodyMode === 'xml') && (\n        <button className=\"ml-2\" onClick={onPrettify}>\n          Prettify\n        </button>\n      )}\n    </StyledWrapper>\n  );\n};\nexport default RequestBodyMode;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/RequestBody/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/RequestBody/index.js",
    "content": "import React from 'react';\nimport get from 'lodash/get';\nimport find from 'lodash/find';\nimport CodeEditor from 'components/CodeEditor';\nimport FormUrlEncodedParams from 'components/RequestPane/FormUrlEncodedParams';\nimport MultipartFormParams from 'components/RequestPane/MultipartFormParams';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport { updateRequestBody } from 'providers/ReduxStore/slices/collections';\nimport { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport { updateRequestBodyScrollPosition } from 'providers/ReduxStore/slices/tabs';\nimport StyledWrapper from './StyledWrapper';\nimport FileBody from '../FileBody/index';\n\nconst RequestBody = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');\n  const bodyMode = item.draft ? get(item, 'draft.request.body.mode') : get(item, 'request.body.mode');\n  const { displayedTheme } = useTheme();\n  const preferences = useSelector((state) => state.app.preferences);\n  const tabs = useSelector((state) => state.tabs.tabs);\n  const activeTabUid = useSelector((state) => state.tabs.activeTabUid);\n  const focusedTab = find(tabs, (t) => t.uid === activeTabUid);\n\n  const onEdit = (value) => {\n    dispatch(\n      updateRequestBody({\n        content: value,\n        itemUid: item.uid,\n        collectionUid: collection.uid\n      })\n    );\n  };\n\n  const onRun = () => dispatch(sendRequest(item, collection.uid));\n  const onSave = () => dispatch(saveRequest(item.uid, collection.uid));\n\n  const onScroll = (editor) => {\n    dispatch(\n      updateRequestBodyScrollPosition({\n        uid: focusedTab.uid,\n        scrollY: editor.doc.scrollTop\n      })\n    );\n  };\n\n  if (['json', 'xml', 'text', 'sparql'].includes(bodyMode)) {\n    let codeMirrorMode = {\n      json: 'application/ld+json',\n      text: 'application/text',\n      xml: 'application/xml',\n      sparql: 'application/sparql-query'\n    };\n\n    let bodyContent = {\n      json: body.json,\n      text: body.text,\n      xml: body.xml,\n      sparql: body.sparql\n    };\n\n    return (\n      <StyledWrapper className=\"w-full\" data-testid=\"request-body-editor\">\n        <CodeEditor\n          collection={collection}\n          item={item}\n          theme={displayedTheme}\n          font={get(preferences, 'font.codeFont', 'default')}\n          fontSize={get(preferences, 'font.codeFontSize')}\n          value={bodyContent[bodyMode] || ''}\n          onEdit={onEdit}\n          onRun={onRun}\n          onSave={onSave}\n          onScroll={onScroll}\n          initialScroll={focusedTab?.requestBodyScrollPosition || 0}\n          mode={codeMirrorMode[bodyMode]}\n          enableVariableHighlighting={true}\n          showHintsFor={['variables']}\n        />\n      </StyledWrapper>\n    );\n  }\n\n  if (bodyMode === 'file') {\n    return <FileBody item={item} collection={collection} />;\n  }\n\n  if (bodyMode === 'formUrlEncoded') {\n    return <FormUrlEncodedParams item={item} collection={collection} />;\n  }\n\n  if (bodyMode === 'multipartForm') {\n    return <MultipartFormParams item={item} collection={collection} />;\n  }\n\n  return <StyledWrapper className=\"w-full\">No Body</StyledWrapper>;\n};\nexport default RequestBody;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/RequestHeaders/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  table {\n    width: 100%;\n    border-collapse: collapse;\n    font-weight: 500;\n    table-layout: fixed;\n\n    thead,\n    td {\n      border: 1px solid ${(props) => props.theme.table.border};\n    }\n\n    thead {\n      color: ${(props) => props.theme.table.thead.color};\n      font-size: ${(props) => props.theme.font.size.base};\n      user-select: none;\n    }\n    td {\n      padding: 6px 10px;\n      }\n  }\n\n  .btn-action {\n    font-size: ${(props) => props.theme.font.size.base};\n    &:hover span {\n      text-decoration: underline;\n    }\n  }\n\n  input[type='text'] {\n    width: 100%;\n    border: solid 1px transparent;\n    outline: none !important;\n    background-color: inherit;\n\n    &:focus {\n      outline: none !important;\n      border: solid 1px transparent;\n    }\n  }\n\n  input[type='checkbox'] {\n    cursor: pointer;\n    position: relative;\n    top: 1px;\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/RequestHeaders/index.js",
    "content": "import React, { useState, useCallback } from 'react';\nimport get from 'lodash/get';\nimport { useDispatch } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport { moveRequestHeader, setRequestHeaders } from 'providers/ReduxStore/slices/collections';\nimport { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport EditableTable from 'components/EditableTable';\nimport StyledWrapper from './StyledWrapper';\nimport { headers as StandardHTTPHeaders } from 'know-your-http-well';\nimport { MimeTypes } from 'utils/codemirror/autocompleteConstants';\nimport BulkEditor from '../../BulkEditor';\nimport { headerNameRegex, headerValueRegex } from 'utils/common/regex';\n\nconst headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);\n\nconst RequestHeaders = ({ item, collection, addHeaderText }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n  const headers = item.draft ? get(item, 'draft.request.headers') : get(item, 'request.headers');\n  const [isBulkEditMode, setIsBulkEditMode] = useState(false);\n\n  const onSave = () => dispatch(saveRequest(item.uid, collection.uid));\n  const handleRun = () => dispatch(sendRequest(item, collection.uid));\n\n  const handleHeadersChange = useCallback((updatedHeaders) => {\n    dispatch(setRequestHeaders({\n      collectionUid: collection.uid,\n      itemUid: item.uid,\n      headers: updatedHeaders\n    }));\n  }, [dispatch, collection.uid, item.uid]);\n\n  const handleHeaderDrag = useCallback(({ updateReorderedItem }) => {\n    dispatch(moveRequestHeader({\n      collectionUid: collection.uid,\n      itemUid: item.uid,\n      updateReorderedItem\n    }));\n  }, [dispatch, collection.uid, item.uid]);\n\n  const getRowError = useCallback((row, index, key) => {\n    if (key === 'name') {\n      if (!row.name || row.name.trim() === '') return null;\n      if (!headerNameRegex.test(row.name)) {\n        return 'Header name cannot contain spaces or newlines';\n      }\n    }\n    if (key === 'value') {\n      if (!row.value) return null;\n      if (!headerValueRegex.test(row.value)) {\n        return 'Header value cannot contain newlines';\n      }\n    }\n    return null;\n  }, []);\n\n  const toggleBulkEditMode = () => {\n    setIsBulkEditMode(!isBulkEditMode);\n  };\n\n  const columns = [\n    {\n      key: 'name',\n      name: 'Name',\n      isKeyField: true,\n      placeholder: 'Name',\n      width: '30%',\n      render: ({ value, onChange }) => (\n        <SingleLineEditor\n          value={value || ''}\n          theme={storedTheme}\n          onSave={onSave}\n          onChange={(newValue) => onChange(newValue.replace(/[\\r\\n]/g, ''))}\n          autocomplete={headerAutoCompleteList}\n          onRun={handleRun}\n          collection={collection}\n          item={item}\n          placeholder={!value ? 'Name' : ''}\n        />\n      )\n    },\n    {\n      key: 'value',\n      name: 'Value',\n      placeholder: 'Value',\n      render: ({ value, onChange }) => (\n        <SingleLineEditor\n          value={value || ''}\n          theme={storedTheme}\n          onSave={onSave}\n          onChange={onChange}\n          onRun={handleRun}\n          autocomplete={MimeTypes}\n          collection={collection}\n          item={item}\n          placeholder={!value ? 'Value' : ''}\n        />\n      )\n    }\n  ];\n\n  const defaultRow = {\n    name: '',\n    value: '',\n    description: ''\n  };\n\n  if (isBulkEditMode) {\n    return (\n      <StyledWrapper className=\"w-full mt-3\">\n        <BulkEditor\n          params={headers}\n          onChange={handleHeadersChange}\n          onToggle={toggleBulkEditMode}\n          onSave={onSave}\n          onRun={handleRun}\n        />\n      </StyledWrapper>\n    );\n  }\n\n  return (\n    <StyledWrapper className=\"w-full\">\n      <EditableTable\n        columns={columns}\n        rows={headers || []}\n        onChange={handleHeadersChange}\n        defaultRow={defaultRow}\n        getRowError={getRowError}\n        reorderable={true}\n        onReorder={handleHeaderDrag}\n      />\n      <div className=\"flex justify-end mt-2\">\n        <button className=\"btn-action text-link select-none\" onClick={toggleBulkEditMode}>\n          Bulk Edit\n        </button>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default RequestHeaders;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Script/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  div.CodeMirror {\n    height: inherit;\n  }\n\n  div.title {\n    color: ${(props) => props.theme.colors.text.subtext0};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Script/index.js",
    "content": "import React, { useState, useEffect, useRef } from 'react';\nimport get from 'lodash/get';\nimport find from 'lodash/find';\nimport { useDispatch, useSelector } from 'react-redux';\nimport CodeEditor from 'components/CodeEditor';\nimport { updateRequestScript, updateResponseScript } from 'providers/ReduxStore/slices/collections';\nimport { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport { updateScriptPaneTab } from 'providers/ReduxStore/slices/tabs';\nimport { useTheme } from 'providers/Theme';\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from 'components/Tabs';\nimport StatusDot from 'components/StatusDot';\n\nconst Script = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  const preRequestEditorRef = useRef(null);\n  const postResponseEditorRef = useRef(null);\n  const requestScript = item.draft ? get(item, 'draft.request.script.req') : get(item, 'request.script.req');\n  const responseScript = item.draft ? get(item, 'draft.request.script.res') : get(item, 'request.script.res');\n\n  const tabs = useSelector((state) => state.tabs.tabs);\n  const activeTabUid = useSelector((state) => state.tabs.activeTabUid);\n  const focusedTab = find(tabs, (t) => t.uid === activeTabUid);\n  const scriptPaneTab = focusedTab?.scriptPaneTab;\n\n  // Default to post-response if pre-request script is empty (only when scriptPaneTab is null/undefined)\n  const getDefaultTab = () => {\n    const hasPreRequestScript = requestScript && requestScript.trim().length > 0;\n    return hasPreRequestScript ? 'pre-request' : 'post-response';\n  };\n\n  const activeTab = scriptPaneTab || getDefaultTab();\n\n  const { displayedTheme } = useTheme();\n  const preferences = useSelector((state) => state.app.preferences);\n\n  // Refresh CodeMirror when tab becomes visible\n  useEffect(() => {\n    // Small delay to ensure DOM is updated\n    const timer = setTimeout(() => {\n      if (activeTab === 'pre-request' && preRequestEditorRef.current?.editor) {\n        preRequestEditorRef.current.editor.refresh();\n      } else if (activeTab === 'post-response' && postResponseEditorRef.current?.editor) {\n        postResponseEditorRef.current.editor.refresh();\n      }\n    }, 0);\n\n    return () => clearTimeout(timer);\n  }, [activeTab]);\n\n  const onRequestScriptEdit = (value) => {\n    dispatch(\n      updateRequestScript({\n        script: value,\n        itemUid: item.uid,\n        collectionUid: collection.uid\n      })\n    );\n  };\n\n  const onResponseScriptEdit = (value) => {\n    dispatch(\n      updateResponseScript({\n        script: value,\n        itemUid: item.uid,\n        collectionUid: collection.uid\n      })\n    );\n  };\n\n  const onRun = () => dispatch(sendRequest(item, collection.uid));\n  const onSave = () => dispatch(saveRequest(item.uid, collection.uid));\n\n  const hasPreRequestScript = requestScript && requestScript.trim().length > 0;\n  const hasPostResponseScript = responseScript && responseScript.trim().length > 0;\n\n  const onScriptTabChange = (tab) => {\n    dispatch(updateScriptPaneTab({ uid: item.uid, scriptPaneTab: tab }));\n  };\n\n  return (\n    <div className=\"w-full h-full flex flex-col\">\n      <Tabs value={activeTab} onValueChange={onScriptTabChange}>\n        <TabsList>\n          <TabsTrigger value=\"pre-request\">\n            Pre Request\n            {hasPreRequestScript && (\n              <StatusDot type={item.preRequestScriptErrorMessage ? 'error' : 'default'} />\n            )}\n          </TabsTrigger>\n          <TabsTrigger value=\"post-response\">\n            Post Response\n            {hasPostResponseScript && (\n              <StatusDot type={item.postResponseScriptErrorMessage ? 'error' : 'default'} />\n            )}\n          </TabsTrigger>\n        </TabsList>\n\n        <TabsContent value=\"pre-request\" className=\"mt-2\" dataTestId=\"pre-request-script-editor\">\n          <CodeEditor\n            ref={preRequestEditorRef}\n            collection={collection}\n            value={requestScript || ''}\n            theme={displayedTheme}\n            font={get(preferences, 'font.codeFont', 'default')}\n            fontSize={get(preferences, 'font.codeFontSize')}\n            onEdit={onRequestScriptEdit}\n            mode=\"javascript\"\n            onRun={onRun}\n            onSave={onSave}\n            showHintsFor={['req', 'bru']}\n          />\n        </TabsContent>\n\n        <TabsContent value=\"post-response\" className=\"mt-2\" dataTestId=\"post-response-script-editor\">\n          <CodeEditor\n            ref={postResponseEditorRef}\n            collection={collection}\n            value={responseScript || ''}\n            theme={displayedTheme}\n            font={get(preferences, 'font.codeFont', 'default')}\n            fontSize={get(preferences, 'font.codeFontSize')}\n            onEdit={onResponseScriptEdit}\n            mode=\"javascript\"\n            onRun={onRun}\n            onSave={onSave}\n            showHintsFor={['req', 'res', 'bru']}\n          />\n        </TabsContent>\n      </Tabs>\n    </div>\n  );\n};\n\nexport default Script;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Settings/Tags/index.js",
    "content": "import React, { useCallback, useEffect } from 'react';\nimport get from 'lodash/get';\nimport { useDispatch } from 'react-redux';\nimport { addRequestTag, deleteRequestTag, updateCollectionTagsList } from 'providers/ReduxStore/slices/collections';\nimport { makeTabPermanent } from 'providers/ReduxStore/slices/tabs';\nimport TagList from 'components/TagList/index';\nimport { saveRequest } from 'providers/ReduxStore/slices/collections/actions';\n\nconst Tags = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  // all tags in the collection\n  const collectionTags = collection.allTags || [];\n\n  // tags for the current request\n  const tags = item.draft ? get(item, 'draft.tags', []) : get(item, 'tags', []);\n\n  // Filter out tags that are already associated with the current request\n  const collectionTagsWithoutCurrentRequestTags = collectionTags?.filter((tag) => !tags.includes(tag)) || [];\n\n  const handleAdd = useCallback((tag) => {\n    const trimmedTag = tag.trim();\n    if (trimmedTag && !tags.includes(trimmedTag)) {\n      dispatch(\n        addRequestTag({\n          tag: trimmedTag,\n          itemUid: item.uid,\n          collectionUid: collection.uid\n        })\n      );\n      dispatch(makeTabPermanent({ uid: item.uid }));\n    }\n  }, [dispatch, tags, item.uid, collection.uid]);\n\n  const handleRemove = useCallback((tag) => {\n    dispatch(\n      deleteRequestTag({\n        tag,\n        itemUid: item.uid,\n        collectionUid: collection.uid\n      })\n    );\n    dispatch(makeTabPermanent({ uid: item.uid }));\n  }, [dispatch, item.uid, collection.uid]);\n\n  const handleRequestSave = () => {\n    dispatch(saveRequest(item.uid, collection.uid));\n  };\n\n  useEffect(() => {\n    dispatch(updateCollectionTagsList({ collectionUid: collection.uid }));\n  }, [collection.uid, dispatch]);\n\n  return (\n    <div className=\"flex flex-col\">\n      <TagList\n        tagsHintList={collectionTagsWithoutCurrentRequestTags}\n        handleAddTag={handleAdd}\n        handleRemoveTag={handleRemove}\n        tags={tags}\n        onSave={handleRequestSave}\n        collectionFormat={collection.format}\n      />\n    </div>\n  );\n};\n\nexport default Tags;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Settings/ToggleSelector/index.js",
    "content": "import React from 'react';\nimport { useTheme } from 'providers/Theme';\n\nconst ToggleSelector = ({\n  checked,\n  onChange,\n  label,\n  description,\n  disabled = false,\n  size = 'small', // 'small', 'medium', 'large'\n  'data-testid': dataTestId\n}) => {\n  const { theme } = useTheme();\n\n  const sizeClasses = {\n    small: {\n      container: 'h-4 w-8',\n      thumb: 'h-3 w-3',\n      translate: checked ? 'translate-x-4' : 'translate-x-1'\n    },\n    medium: {\n      container: 'h-5 w-9',\n      thumb: 'h-3 w-3',\n      translate: checked ? 'translate-x-5' : 'translate-x-1'\n    },\n    large: {\n      container: 'h-6 w-11',\n      thumb: 'h-4 w-4',\n      translate: checked ? 'translate-x-6' : 'translate-x-1'\n    }\n  };\n\n  const currentSize = sizeClasses[size];\n\n  return (\n    <div className=\"flex items-center justify-between\">\n      <div className=\"flex flex-col\">\n        <label className=\"text-xs font-medium text-gray-900 dark:text-gray-100\">\n          {label}\n        </label>\n        {description && (\n          <p className=\"text-xs text-gray-700 dark:text-gray-400\">\n            {description}\n          </p>\n        )}\n      </div>\n      <button\n        type=\"button\"\n        onClick={onChange}\n        disabled={disabled}\n        data-testid={dataTestId}\n        style={{\n          backgroundColor: checked ? theme.primary.solid : theme.background.surface2\n        }}\n        className={`\n          relative inline-flex ${currentSize.container} flex-shrink-0 items-center rounded-full transition-colors\n          focus:outline-none focus:ring-1 focus:ring-offset-1\n          ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}\n        `}\n        role=\"switch\"\n        aria-checked={checked}\n        aria-disabled={disabled}\n      >\n        <span\n          className={`\n            inline-block ${currentSize.thumb} transform rounded-full bg-white transition-transform\n            ${currentSize.translate}\n          `}\n        />\n      </button>\n    </div>\n  );\n};\n\nexport default ToggleSelector;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Settings/index.js",
    "content": "import React, { useCallback } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport get from 'lodash/get';\nimport { IconTag } from '@tabler/icons';\nimport ToggleSelector from 'components/RequestPane/Settings/ToggleSelector';\nimport SettingsInput from 'components/SettingsInput';\nimport InheritableSettingsInput from 'components/InheritableSettingsInput';\nimport { updateItemSettings } from 'providers/ReduxStore/slices/collections';\nimport { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport Tags from './Tags/index';\n\n// Default settings configuration\nconst DEFAULT_SETTINGS = {\n  encodeUrl: false,\n  followRedirects: true,\n  maxRedirects: 5,\n  timeout: 'inherit'\n};\n\nconst Settings = ({ item, collection }) => {\n  const dispatch = useDispatch();\n\n  // Get current settings with defaults applied\n  const getPropertyFromDraftOrRequest = (propertyKey) =>\n    item.draft ? get(item, `draft.${propertyKey}`, {}) : get(item, propertyKey, {});\n\n  const rawSettings = getPropertyFromDraftOrRequest('settings');\n  const settings = { ...DEFAULT_SETTINGS, ...rawSettings };\n  const { encodeUrl, followRedirects, maxRedirects, timeout } = settings;\n\n  // Reusable function to update settings\n  const updateSetting = useCallback((settingUpdate) => {\n    const updatedSettings = { ...settings, ...settingUpdate };\n    dispatch(updateItemSettings({\n      collectionUid: collection.uid,\n      itemUid: item.uid,\n      settings: updatedSettings\n    }));\n  }, [dispatch, collection.uid, item.uid, settings]);\n\n  // Setting change handlers\n  const onToggleUrlEncoding = useCallback(() =>\n    updateSetting({ encodeUrl: !encodeUrl }), [encodeUrl, updateSetting]);\n\n  const onToggleFollowRedirects = useCallback(() =>\n    updateSetting({ followRedirects: !followRedirects }), [followRedirects, updateSetting]);\n\n  const onMaxRedirectsChange = useCallback((e) => {\n    const value = e.target.value;\n    // Only allow empty string or digits\n    if (value === '' || /^\\d+$/.test(value)) {\n      const numericValue = value === '' ? 0 : parseInt(value, 10);\n      updateSetting({ maxRedirects: numericValue });\n    }\n  }, [updateSetting]);\n\n  const onTimeoutChange = useCallback((e) => {\n    const value = e.target.value;\n    // Only allow empty string or digits\n    if (value === '' || /^\\d+$/.test(value)) {\n      const numericValue = value === '' ? 0 : parseInt(value, 10);\n      updateSetting({ timeout: numericValue });\n    }\n  }, [updateSetting]);\n\n  // Check if timeout is inherited\n  const isTimeoutInherited = timeout === 'inherit' || timeout === undefined || timeout === null;\n\n  const handleTimeoutDropdownSelect = useCallback((option) => {\n    if (option === 'inherit') {\n      updateSetting({ timeout: 'inherit' });\n    } else if (option === 'custom') {\n      // Switch to custom value - start with 0\n      updateSetting({ timeout: 0 });\n    }\n  }, [updateSetting]);\n\n  // Keyboard shortcut handlers\n  const onSave = useCallback(() => {\n    dispatch(saveRequest(item.uid, collection.uid));\n  }, [dispatch, item.uid, collection.uid]);\n\n  const onRun = useCallback(() => {\n    dispatch(sendRequest(item, collection.uid));\n  }, [dispatch, item, collection.uid]);\n\n  // Keyboard shortcut handler for input fields\n  const handleKeyDown = useCallback((e) => {\n    if ((e.ctrlKey || e.metaKey) && e.key === 's') {\n      e.preventDefault();\n      onSave();\n    } else if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {\n      e.preventDefault();\n      onRun();\n    }\n  }, [onSave, onRun]);\n\n  return (\n    <div className=\"h-full w-full\">\n      <div className=\"text-xs mb-4 text-muted\">Configure request settings for this item.</div>\n      <div className=\"bruno-form\">\n        <div className=\"mb-6\">\n          <h3 className=\"text-xs font-medium text-gray-900 dark:text-gray-100 flex items-center gap-1 mb-4\">\n            <IconTag size={16} />\n            Tags\n          </h3>\n          <Tags item={item} collection={collection} />\n        </div>\n\n        <div className=\"flex flex-col gap-4\">\n\n          <div className=\"flex flex-col gap-4\">\n            <ToggleSelector\n              checked={encodeUrl}\n              onChange={onToggleUrlEncoding}\n              label=\"URL Encoding\"\n              description=\"Automatically encode query parameters in the URL\"\n              size=\"medium\"\n              data-testid=\"encode-url-toggle\"\n            />\n          </div>\n\n          <div className=\"flex flex-col gap-4\">\n            <ToggleSelector\n              checked={followRedirects}\n              onChange={onToggleFollowRedirects}\n              label=\"Automatically Follow Redirects\"\n              description=\"Follow HTTP redirects automatically\"\n              size=\"medium\"\n              data-testid=\"follow-redirects-toggle\"\n            />\n          </div>\n\n          <SettingsInput\n            id=\"maxRedirects\"\n            label=\"Max Redirects\"\n            value={maxRedirects}\n            onChange={onMaxRedirectsChange}\n            description=\"Set a limit for the number of redirects to follow\"\n            onKeyDown={handleKeyDown}\n          />\n\n          <InheritableSettingsInput\n            id=\"timeout\"\n            label=\"Timeout (ms)\"\n            value={timeout}\n            description=\"Set maximum time to wait before aborting the request\"\n            onKeyDown={handleKeyDown}\n            isInherited={isTimeoutInherited}\n            onDropdownSelect={handleTimeoutDropdownSelect}\n            onValueChange={(e) => !isTimeoutInherited && onTimeoutChange(e)}\n            onCustomValueReset={() => updateSetting({ timeout: 'inherit' })}\n          />\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default Settings;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Tests/index.js",
    "content": "import React from 'react';\nimport get from 'lodash/get';\nimport { useDispatch, useSelector } from 'react-redux';\nimport CodeEditor from 'components/CodeEditor';\nimport { updateRequestTests } from 'providers/ReduxStore/slices/collections';\nimport { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport { useTheme } from 'providers/Theme';\n\nconst Tests = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  const tests = item.draft ? get(item, 'draft.request.tests') : get(item, 'request.tests');\n\n  const { displayedTheme } = useTheme();\n  const preferences = useSelector((state) => state.app.preferences);\n\n  const onEdit = (value) => {\n    dispatch(\n      updateRequestTests({\n        tests: value,\n        itemUid: item.uid,\n        collectionUid: collection.uid\n      })\n    );\n  };\n\n  const onRun = () => dispatch(sendRequest(item, collection.uid));\n  const onSave = () => dispatch(saveRequest(item.uid, collection.uid));\n\n  return (\n    <CodeEditor\n      collection={collection}\n      value={tests || ''}\n      theme={displayedTheme}\n      font={get(preferences, 'font.codeFont', 'default')}\n      fontSize={get(preferences, 'font.codeFontSize')}\n      onEdit={onEdit}\n      mode=\"javascript\"\n      onRun={onRun}\n      onSave={onSave}\n      showHintsFor={['req', 'res', 'bru']}\n    />\n  );\n};\n\nexport default Tests;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Vars/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  div.title {\n    color: ${(props) => props.theme.colors.text.subtext0};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Vars/VarsTable/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  table {\n    width: 100%;\n    border-collapse: collapse;\n    font-weight: 500;\n    table-layout: fixed;\n\n    thead,\n    td {\n      border: 1px solid ${(props) => props.theme.table.border};\n    }\n\n    thead {\n      color: ${(props) => props.theme.table.thead.color};\n      font-size: ${(props) => props.theme.font.size.base};\n      user-select: none;\n    }\n    td {\n      padding: 6px 10px;\n      }\n    }\n\n  .btn-add-var {\n    font-size: ${(props) => props.theme.font.size.base};\n  }\n\n  input[type='text'] {\n    width: 100%;\n    border: solid 1px transparent;\n    outline: none !important;\n    color: ${(props) => props.theme.table.input.color};\n    background: transparent;\n\n    &:focus {\n      outline: none !important;\n      border: solid 1px transparent;\n    }\n  }\n\n  input[type='checkbox'] {\n    cursor: pointer;\n    position: relative;\n    top: 1px;\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Vars/VarsTable/index.js",
    "content": "import React, { useCallback } from 'react';\nimport { useDispatch } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport { moveVar, setRequestVars } from 'providers/ReduxStore/slices/collections';\nimport { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport MultiLineEditor from 'components/MultiLineEditor';\nimport InfoTip from 'components/InfoTip';\nimport EditableTable from 'components/EditableTable';\nimport StyledWrapper from './StyledWrapper';\nimport toast from 'react-hot-toast';\nimport { variableNameRegex } from 'utils/common/regex';\n\nconst VarsTable = ({ item, collection, vars, varType }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n\n  const onSave = () => dispatch(saveRequest(item.uid, collection.uid));\n  const handleRun = () => dispatch(sendRequest(item, collection.uid));\n\n  const handleVarsChange = useCallback((updatedVars) => {\n    dispatch(setRequestVars({\n      collectionUid: collection.uid,\n      itemUid: item.uid,\n      vars: updatedVars,\n      type: varType\n    }));\n  }, [dispatch, collection.uid, item.uid, varType]);\n\n  const handleVarDrag = useCallback(({ updateReorderedItem }) => {\n    dispatch(moveVar({\n      type: varType,\n      collectionUid: collection.uid,\n      itemUid: item.uid,\n      updateReorderedItem\n    }));\n  }, [dispatch, varType, collection.uid, item.uid]);\n\n  const getRowError = useCallback((row, index, key) => {\n    if (key !== 'name') return null;\n    if (!row.name || row.name.trim() === '') return null;\n    if (!variableNameRegex.test(row.name)) {\n      return 'Variable contains invalid characters. Must only contain alphanumeric characters, \"-\", \"_\", \".\"';\n    }\n    return null;\n  }, []);\n\n  const columns = [\n    {\n      key: 'name',\n      name: 'Name',\n      isKeyField: true,\n      placeholder: 'Name',\n      width: '35%'\n    },\n    {\n      key: 'value',\n      name: varType === 'request' ? 'Value' : (\n        <div className=\"flex items-center\">\n          <span>Expr</span>\n          <InfoTip content=\"You can write any valid JS expression here\" infotipId={`request-${varType}-var`} />\n        </div>\n      ),\n      placeholder: varType === 'request' ? 'Value' : 'Expr',\n      render: ({ value, onChange }) => (\n        <MultiLineEditor\n          value={value || ''}\n          theme={storedTheme}\n          onSave={onSave}\n          onChange={onChange}\n          onRun={handleRun}\n          collection={collection}\n          item={item}\n          placeholder={!value ? (varType === 'request' ? 'Value' : 'Expr') : ''}\n        />\n      )\n    }\n  ];\n\n  const defaultRow = {\n    name: '',\n    value: '',\n    ...(varType === 'response' ? { local: false } : {})\n  };\n\n  return (\n    <StyledWrapper className=\"w-full\">\n      <EditableTable\n        columns={columns}\n        rows={vars || []}\n        onChange={handleVarsChange}\n        defaultRow={defaultRow}\n        getRowError={getRowError}\n        reorderable={true}\n        onReorder={handleVarDrag}\n      />\n    </StyledWrapper>\n  );\n};\n\nexport default VarsTable;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/Vars/index.js",
    "content": "import React from 'react';\nimport get from 'lodash/get';\nimport VarsTable from './VarsTable';\nimport StyledWrapper from './StyledWrapper';\n\nconst Vars = ({ item, collection }) => {\n  const requestVars = item.draft ? get(item, 'draft.request.vars.req') : get(item, 'request.vars.req');\n  const responseVars = item.draft ? get(item, 'draft.request.vars.res') : get(item, 'request.vars.res');\n\n  return (\n    <StyledWrapper className=\"w-full flex flex-col\">\n      <div>\n        <div className=\"mb-3 title text-xs\">Pre Request</div>\n        <VarsTable item={item} collection={collection} vars={requestVars} varType=\"request\" />\n      </div>\n      <div>\n        <div className=\"mt-3 mb-3 title text-xs\">Post Response</div>\n        <VarsTable item={item} collection={collection} vars={responseVars} varType=\"response\" />\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Vars;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/WSRequestPane/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/WSRequestPane/WSAuth/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  .inherit-mode-text {\n    color: ${(props) => props.theme.primary.text};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/WSRequestPane/WSAuth/WSAuthMode/index.js",
    "content": "import React, { useMemo, useCallback } from 'react';\nimport get from 'lodash/get';\nimport { IconCaretDown } from '@tabler/icons';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport { useDispatch } from 'react-redux';\nimport { updateRequestAuthMode } from 'providers/ReduxStore/slices/collections';\nimport { humanizeRequestAuthMode } from 'utils/collections';\nimport StyledWrapper from '../../../Auth/AuthMode/StyledWrapper';\n\nconst WSAuthMode = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');\n\n  const onModeChange = useCallback((value) => {\n    dispatch(updateRequestAuthMode({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      mode: value\n    }));\n  }, [dispatch, item.uid, collection.uid]);\n\n  const menuItems = useMemo(() => [\n    {\n      id: 'basic',\n      label: 'Basic Auth',\n      onClick: () => onModeChange('basic')\n    },\n    {\n      id: 'bearer',\n      label: 'Bearer Token',\n      onClick: () => onModeChange('bearer')\n    },\n    {\n      id: 'apikey',\n      label: 'API Key',\n      onClick: () => onModeChange('apikey')\n    },\n    {\n      id: 'oauth2',\n      label: 'OAuth 2.0',\n      onClick: () => onModeChange('oauth2')\n    },\n    {\n      id: 'inherit',\n      label: 'Inherit',\n      onClick: () => onModeChange('inherit')\n    },\n    {\n      id: 'none',\n      label: 'No Auth',\n      onClick: () => onModeChange('none')\n    }\n  ], [onModeChange]);\n\n  return (\n    <StyledWrapper>\n      <div className=\"inline-flex items-center cursor-pointer auth-mode-selector\">\n        <MenuDropdown\n          items={menuItems}\n          placement=\"bottom-end\"\n          selectedItemId={authMode}\n          showTickMark={true}\n        >\n          <div className=\"flex items-center justify-center auth-mode-label select-none\">\n            {humanizeRequestAuthMode(authMode)} <IconCaretDown className=\"caret ml-1\" size={14} strokeWidth={2} />\n          </div>\n        </MenuDropdown>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default WSAuthMode;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/WSRequestPane/WSAuth/index.js",
    "content": "import React, { useEffect } from 'react';\nimport get from 'lodash/get';\nimport { useDispatch } from 'react-redux';\nimport BearerAuth from '../../Auth/BearerAuth';\nimport BasicAuth from '../../Auth/BasicAuth';\nimport ApiKeyAuth from '../../Auth/ApiKeyAuth';\nimport StyledWrapper from './StyledWrapper';\nimport { humanizeRequestAuthMode } from 'utils/collections';\nimport { getTreePathFromCollectionToItem } from 'utils/collections/index';\nimport { updateRequestAuthMode, updateAuth } from 'providers/ReduxStore/slices/collections';\nimport { saveRequest } from 'providers/ReduxStore/slices/collections/actions';\n\nconst supportedAuthModes = ['basic', 'bearer', 'apikey', 'oauth2', 'none', 'inherit'];\n\nconst WSAuth = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');\n  const requestTreePath = getTreePathFromCollectionToItem(collection, item);\n\n  const request = item.draft\n    ? get(item, 'draft.request', {})\n    : get(item, 'request', {});\n\n  const save = () => {\n    return saveRequest(item.uid, collection.uid);\n  };\n\n  // Reset to 'none' if current auth mode is not supported\n  useEffect(() => {\n    if (authMode && !supportedAuthModes.includes(authMode)) {\n      dispatch(updateRequestAuthMode({\n        itemUid: item.uid,\n        collectionUid: collection.uid,\n        mode: 'none'\n      }));\n    }\n  }, [authMode, collection.uid, dispatch, item.uid]);\n\n  const getEffectiveAuthSource = () => {\n    if (authMode !== 'inherit') return null;\n\n    const collectionRoot = collection?.draft?.root || collection?.root || {};\n    const collectionAuth = get(collectionRoot, 'request.auth');\n    let effectiveSource = {\n      type: 'collection',\n      name: 'Collection',\n      auth: collectionAuth\n    };\n\n    // Check folders in reverse to find the closest auth configuration\n    for (let i of [...requestTreePath].reverse()) {\n      if (i.type === 'folder') {\n        const folderAuth = get(i, 'root.request.auth');\n        if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {\n          effectiveSource = {\n            type: 'folder',\n            name: i.name,\n            auth: folderAuth\n          };\n          break;\n        }\n      }\n    }\n\n    return effectiveSource;\n  };\n\n  const getAuthView = () => {\n    switch (authMode) {\n      case 'none': {\n        return <div>No Auth</div>;\n      }\n      case 'basic': {\n        return <BasicAuth collection={collection} item={item} updateAuth={updateAuth} request={request} save={save} />;\n      }\n      case 'bearer': {\n        return <BearerAuth collection={collection} item={item} updateAuth={updateAuth} request={request} save={save} />;\n      }\n      case 'apikey': {\n        return <ApiKeyAuth collection={collection} item={item} updateAuth={updateAuth} request={request} save={save} />;\n      }\n      case 'oauth2': {\n        return (\n          <>\n            <div className=\"flex flex-row w-full gap-2\">\n              <div>\n                OAuth 2 not <strong>yet</strong> supported by WebSockets. Using no auth instead.\n              </div>\n            </div>\n          </>\n        );\n      }\n      case 'inherit': {\n        const source = getEffectiveAuthSource();\n\n        // Check if inherited auth is OAuth2 - not supported for WebSockets\n        if (source?.auth?.mode === 'oauth2') {\n          return (\n            <>\n              <div className=\"flex flex-row w-full mt-2 gap-2\">\n                OAuth 2 not <strong>yet</strong> supported by WebSockets. Using no auth instead.\n              </div>\n            </>\n          );\n        }\n\n        // Only show inherited auth if it's one of the supported types\n        if (source && supportedAuthModes.includes(source.auth?.mode)) {\n          return (\n            <>\n              <div className=\"flex flex-row w-full gap-2\">\n                <div> Auth inherited from {source.name}: </div>\n                <div className=\"inherit-mode-text\">{humanizeRequestAuthMode(source.auth?.mode)}</div>\n              </div>\n            </>\n          );\n        } else {\n          return (\n            <>\n              <div className=\"flex flex-row w-full gap-2\">\n                <div>Inherited auth not supported by WebSockets. Using no auth instead.</div>\n              </div>\n            </>\n          );\n        }\n      }\n      default: {\n        return null;\n      }\n    }\n  };\n\n  return (\n    <StyledWrapper className=\"w-full overflow-y-scroll\">\n      {getAuthView()}\n    </StyledWrapper>\n  );\n};\n\nexport default WSAuth;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/WSRequestPane/index.js",
    "content": "import React, { useMemo, useCallback, useRef } from 'react';\nimport Documentation from 'components/Documentation/index';\nimport RequestHeaders from 'components/RequestPane/RequestHeaders';\nimport StatusDot from 'components/StatusDot/index';\nimport { find } from 'lodash';\nimport { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';\nimport { useDispatch, useSelector } from 'react-redux';\nimport HeightBoundContainer from 'ui/HeightBoundContainer';\nimport ResponsiveTabs from 'ui/ResponsiveTabs';\nimport { getPropertyFromDraftOrRequest } from 'utils/collections/index';\nimport WsBody from '../WsBody/index';\nimport StyledWrapper from './StyledWrapper';\nimport WSAuth from './WSAuth';\nimport WSAuthMode from './WSAuth/WSAuthMode';\nimport WSSettingsPane from '../WSSettingsPane/index';\n\nconst WSRequestPane = ({ item, collection, handleRun }) => {\n  const dispatch = useDispatch();\n  const tabs = useSelector((state) => state.tabs.tabs);\n  const activeTabUid = useSelector((state) => state.tabs.activeTabUid);\n\n  const rightContentRef = useRef(null);\n\n  const focusedTab = find(tabs, (t) => t.uid === activeTabUid);\n  const requestPaneTab = focusedTab?.requestPaneTab;\n\n  const selectTab = useCallback(\n    (tab) => {\n      dispatch(updateRequestPaneTab({\n        uid: item.uid,\n        requestPaneTab: tab\n      }));\n    },\n    [dispatch, item.uid]\n  );\n\n  const headers = getPropertyFromDraftOrRequest(item, 'request.headers');\n  const docs = getPropertyFromDraftOrRequest(item, 'request.docs');\n  const auth = getPropertyFromDraftOrRequest(item, 'request.auth');\n\n  const activeHeadersLength = headers.filter((header) => header.enabled).length;\n\n  const allTabs = useMemo(() => {\n    return [\n      {\n        key: 'body',\n        label: 'Message',\n        indicator: null\n      },\n      {\n        key: 'headers',\n        label: 'Headers',\n        indicator: activeHeadersLength > 0 ? <sup className=\"ml-[.125rem] font-medium\">{activeHeadersLength}</sup> : null\n      },\n      {\n        key: 'auth',\n        label: 'Auth',\n        indicator: auth.mode !== 'none' ? <StatusDot type=\"default\" /> : null\n      },\n      {\n        key: 'settings',\n        label: 'Settings',\n        indicator: null\n      },\n      {\n        key: 'docs',\n        label: 'Docs',\n        indicator: docs && docs.length > 0 ? <StatusDot type=\"default\" /> : null\n      }\n    ];\n  }, [activeHeadersLength, auth.mode, docs]);\n\n  const tabPanel = useMemo(() => {\n    switch (requestPaneTab) {\n      case 'body': {\n        return (\n          <WsBody\n            item={item}\n            collection={collection}\n            hideModeSelector={true}\n            hidePrettifyButton={true}\n            handleRun={handleRun}\n          />\n        );\n      }\n      case 'headers': {\n        return <RequestHeaders item={item} collection={collection} addHeaderText=\"Add Headers\" />;\n      }\n      case 'settings': {\n        return <WSSettingsPane item={item} collection={collection} />;\n      }\n      case 'auth': {\n        return <WSAuth item={item} collection={collection} />;\n      }\n      case 'docs': {\n        return <Documentation item={item} collection={collection} />;\n      }\n      default: {\n        return <div className=\"mt-4\">404 | Not found</div>;\n      }\n    }\n  }, [requestPaneTab, item, collection, handleRun]);\n\n  if (!activeTabUid || !focusedTab?.uid || !requestPaneTab) {\n    return <div className=\"pb-4 px-4\">An error occurred!</div>;\n  }\n\n  const rightContent = requestPaneTab === 'auth' ? (\n    <div ref={rightContentRef} className=\"flex flex-grow justify-start items-center\">\n      <WSAuthMode item={item} collection={collection} />\n    </div>\n  ) : null;\n\n  return (\n    <StyledWrapper className=\"flex flex-col h-full relative\">\n      <ResponsiveTabs\n        tabs={allTabs}\n        activeTab={requestPaneTab}\n        onTabSelect={selectTab}\n        rightContent={rightContent}\n        rightContentRef={rightContent ? rightContentRef : null}\n      />\n\n      <section className=\"flex w-full flex-1 h-full mt-4\">\n        <HeightBoundContainer>{tabPanel}</HeightBoundContainer>\n      </section>\n    </StyledWrapper>\n  );\n};\n\nexport default WSRequestPane;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/WSSettingsPane/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .single-line-editor-wrapper {\n    padding: 0.15rem 0.4rem;\n    border-radius: 3px;\n    border: solid 1px ${(props) => props.theme.input.border};\n    background-color: ${(props) => props.theme.input.bg};\n\n    &.error{\n      border-color: ${(props) => props.theme.colors.text.danger};\n    }\n  }\n\n  .tooltip-mod {\n    width: 150px !important;\n\n    & ul {\n      padding-left: 4px;\n    }\n\n    & ul > li {\n      list-style: circle;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/WSSettingsPane/index.js",
    "content": "import cn from 'classnames';\nimport InfoTip from 'components/InfoTip/index';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport ToolHint from 'components/ToolHint/index';\nimport { useFormik } from 'formik';\nimport get from 'lodash/get';\nimport { updateItemSettings } from 'providers/ReduxStore/slices/collections';\nimport { useTheme } from 'providers/Theme';\nimport React, { useEffect } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport * as Yup from 'yup';\nimport StyledWrapper from './StyledWrapper';\n\n/**\n * @param {string} propertyKey\n * @param {{draft?:Record<string,unknown>}} item\n * @returns\n */\nconst getPropertyFromDraftOrRequest = (propertyKey, item) =>\n  item.draft ? get(item, `draft.${propertyKey}`, {}) : get(item, propertyKey, {});\n\nconst ERRORS = {\n  timeout: {\n    invalid: `Timeout needs to be a valid number`\n  },\n  keepAliveInterval: {\n    invalid: `Timeout needs to be a valid number`\n  }\n};\n\nconst WSSettingsPane = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n  const requestPreferences = useSelector((state) => state.app.preferences.request);\n\n  const { timeout: _connectionTimeout, keepAliveInterval = 0 } = getPropertyFromDraftOrRequest('settings', item);\n\n  const connectionTimeout = _connectionTimeout ?? requestPreferences.timeout;\n\n  const updateSetting = (key, value) => {\n    dispatch(updateItemSettings({\n      collectionUid: collection.uid,\n      itemUid: item.uid,\n      settings: {\n        [key]: value\n      }\n    }));\n  };\n\n  const formErrors = {\n    timeout: isNaN(Number(connectionTimeout)) && ERRORS.timeout.invalid,\n    keepAliveInterval: isNaN(Number(keepAliveInterval)) && ERRORS.keepAliveInterval.invalid\n  };\n\n  return (\n    <StyledWrapper className=\"flex flex-col gap-4 w-full\">\n      <section className=\"grid gap-4 items-center grid-cols-2\">\n        <div>\n          <label className=\"font-medium mb-2\">Timeout</label>\n          <InfoTip\n            infotipId=\"setting-connection-timeout\"\n            className=\"tooltip-mod max-w-lg\"\n            content={(\n              <div>\n                <p>\n                  <span>Timeout in milliseconds</span>\n                </p>\n              </div>\n            )}\n          />\n        </div>\n        <div>\n          <div className={cn('single-line-editor-wrapper', {\n            error: formErrors.timeout\n          })}\n          >\n            <ToolHint\n              key=\"timeout\"\n              toolhintId=\"ws-settings-timeout\"\n              place=\"top\"\n              text={formErrors.timeout ? formErrors.timeout : ''}\n            >\n              <SingleLineEditor\n                value={connectionTimeout}\n                theme={storedTheme}\n                onChange={(newValue) => updateSetting('timeout', newValue)}\n                collection={collection}\n              />\n            </ToolHint>\n          </div>\n        </div>\n\n        <div>\n          <label className=\"font-medium mb-2\">Keep Alive Interval</label>\n          <InfoTip\n            infotipId=\"setting-keep-alive\"\n            className=\"tooltip-mod max-w-lg\"\n            content={(\n              <div>\n                <p>\n                  <span>\n                    Keep the websocket alive by sending ping requests to the server at every interval (in millseconds)\n                  </span>\n                </p>\n                <p className=\"mt-2\">0 (zero) = off</p>\n              </div>\n            )}\n          />\n        </div>\n        <div>\n          <div className={cn('single-line-editor-wrapper', {\n            error: formErrors.keepAliveInterval\n          })}\n          >\n            <ToolHint\n              key=\"timeout\"\n              toolhintId=\"ws-settings-keepAliveInterval\"\n              place=\"top\"\n              text={formErrors.keepAliveInterval ? formErrors.keepAliveInterval : ''}\n            >\n              <SingleLineEditor\n                value={keepAliveInterval}\n                theme={storedTheme}\n                onChange={(newValue) => updateSetting('keepAliveInterval', newValue)}\n                collection={collection}\n              />\n            </ToolHint>\n          </div>\n        </div>\n      </section>\n    </StyledWrapper>\n  );\n};\n\nexport default WSSettingsPane;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/WsBody/BodyMode/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  font-size: ${(props) => props.theme.font.size.base};\n\n  .body-mode-selector {\n    background: transparent;\n    border-radius: 3px;\n\n    .dropdown-item {\n      padding: 0.2rem 0.6rem !important;\n      padding-left: 1.5rem !important;\n    }\n\n    .label-item {\n      padding: 0.2rem 0.6rem !important;\n    }\n\n    .selected-body-mode {\n      color: ${(props) => props.theme.primary.text};\n    }\n  }\n\n  .caret {\n    color: ${(props) => props.theme.colors.text.muted};\n    fill: ${(props) => props.theme.colors.text.muted};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/WsBody/BodyMode/index.js",
    "content": "import React, { useRef, forwardRef } from 'react';\nimport { IconCaretDown } from '@tabler/icons';\nimport Dropdown from 'components/Dropdown';\nimport { humanizeRequestBodyMode } from 'utils/collections';\nimport StyledWrapper from './StyledWrapper';\n\nconst RAW_MODES = [\n  {\n    label: 'JSON',\n    key: 'json'\n  },\n  {\n    label: 'XML',\n    key: 'xml'\n  },\n  {\n    label: 'TEXT',\n    key: 'text'\n  }\n];\n\nconst WSRequestBodyMode = ({ mode, onModeChange }) => {\n  const dropdownTippyRef = useRef();\n  const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);\n\n  const Icon = forwardRef((props, ref) => {\n    return (\n      <div ref={ref} className=\"flex items-center justify-center pl-3 py-1 select-none selected-body-mode\">\n        {humanizeRequestBodyMode(mode)}\n        {' '}\n        <IconCaretDown className=\"caret ml-2\" size={14} strokeWidth={2} />\n      </div>\n    );\n  });\n\n  return (\n    <StyledWrapper>\n      <div className=\"inline-flex items-center cursor-pointer body-mode-selector\">\n        <Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement=\"bottom-end\">\n          <div className=\"label-item font-medium\">Raw</div>\n          {RAW_MODES.map((d) => (\n            <div\n              className=\"dropdown-item\"\n              key={d.key}\n              onClick={() => {\n                dropdownTippyRef.current.hide();\n                onModeChange(d.key);\n              }}\n            >\n              {d.label}\n            </div>\n          ))}\n        </Dropdown>\n      </div>\n    </StyledWrapper>\n  );\n};\nexport default WSRequestBodyMode;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/WsBody/SingleWSMessage/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n\n  &.single {\n    height: 100%;\n\n    .editor-container {\n      height: calc(100% - 32px);\n    }\n  }\n\n  &:not(.single) {\n    min-height: 240px;\n    margin-bottom: 8px;\n\n    &.last {\n      margin-bottom: 0;\n    }\n  }\n\n  .message-toolbar {\n    display: flex;\n    align-items: center;\n    justify-content: flex-end;\n    gap: 4px;\n    padding: 4px 0px;\n    padding-top: 0px;\n    height: 32px;\n    flex-shrink: 0;\n\n    .message-label {\n      font-size: ${(props) => props.theme.font.size.sm};\n      color: ${(props) => props.theme.colors.text.subtext1};\n      margin-right: auto;\n    }\n\n    .toolbar-actions {\n      display: flex;\n      align-items: center;\n      gap: 2px;\n    }\n\n    .toolbar-btn {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      width: 28px;\n      height: 28px;\n      border-radius: 4px;\n      color: ${(props) => props.theme.colors.text.muted};\n      transition: all 0.15s ease;\n\n      &:hover {\n        background-color: ${(props) => props.theme.dropdown.hoverBg};\n        color: ${(props) => props.theme.text};\n      }\n\n      &.delete:hover {\n        color: ${(props) => props.theme.colors.text.danger};\n      }\n    }\n  }\n\n  .editor-container {\n    flex: 1;\n    min-height: 0;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/WsBody/SingleWSMessage/index.js",
    "content": "import { IconTrash, IconWand } from '@tabler/icons';\nimport CodeEditor from 'components/CodeEditor/index';\nimport ToolHint from 'components/ToolHint/index';\nimport { get } from 'lodash';\nimport invert from 'lodash/invert';\nimport { updateRequestBody } from 'providers/ReduxStore/slices/collections';\nimport { saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport { useTheme } from 'providers/Theme';\nimport React, { useState } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { autoDetectLang } from 'utils/codemirror/lang-detect';\nimport { toastError } from 'utils/common/error';\nimport { prettifyJsonString } from 'utils/common/index';\nimport xmlFormat from 'xml-formatter';\nimport WSRequestBodyMode from '../BodyMode/index';\nimport StyledWrapper from './StyledWrapper';\n\nexport const TYPE_BY_DECODER = {\n  base64: 'binary',\n  json: 'json',\n  xml: 'xml'\n};\n\nexport const DECODER_BY_TYPE = invert(TYPE_BY_DECODER);\n\nexport const SingleWSMessage = ({\n  message,\n  item,\n  collection,\n  index,\n  methodType,\n  handleRun,\n  canClientSendMultipleMessages,\n  isLast\n}) => {\n  const dispatch = useDispatch();\n  const { displayedTheme } = useTheme();\n  const preferences = useSelector((state) => state.app.preferences);\n  const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');\n\n  const { name, content, type } = message;\n  const [messageFormat, setMessageFormat] = useState(autoDetectLang(content));\n\n  const onUpdateMessageType = (type) => {\n    setMessageFormat(type);\n\n    const currentMessages = [...(body.ws || [])];\n\n    currentMessages[index] = {\n      ...currentMessages[index],\n      type: DECODER_BY_TYPE[type]\n    };\n\n    dispatch(updateRequestBody({\n      content: currentMessages,\n      itemUid: item.uid,\n      collectionUid: collection.uid\n    }));\n  };\n\n  const onEdit = (value) => {\n    const currentMessages = [...(body.ws || [])];\n\n    currentMessages[index] = {\n      name: name ? name : `message ${index + 1}`,\n      type: DECODER_BY_TYPE[messageFormat],\n      content: value\n    };\n\n    dispatch(updateRequestBody({\n      content: currentMessages,\n      itemUid: item.uid,\n      collectionUid: collection.uid\n    }));\n  };\n\n  const onSave = () => dispatch(saveRequest(item.uid, collection.uid));\n\n  const onDeleteMessage = () => {\n    const currentMessages = [...(body.ws || [])];\n\n    currentMessages.splice(index, 1);\n\n    dispatch(updateRequestBody({\n      content: currentMessages,\n      itemUid: item.uid,\n      collectionUid: collection.uid\n    }));\n  };\n\n  let codeType = messageFormat;\n  if (TYPE_BY_DECODER[type]) {\n    codeType = TYPE_BY_DECODER[type];\n  }\n\n  const codemirrorMode = {\n    text: 'application/text',\n    xml: 'application/xml',\n    json: 'application/ld+json'\n  };\n\n  const onPrettify = () => {\n    if (codeType === 'json') {\n      try {\n        const prettyBodyJson = prettifyJsonString(content);\n        const currentMessages = [...(body.ws || [])];\n        currentMessages[index] = {\n          ...currentMessages[index],\n          name: name ? name : `message ${index + 1}`,\n          content: prettyBodyJson\n        };\n        dispatch(updateRequestBody({\n          content: currentMessages,\n          itemUid: item.uid,\n          collectionUid: collection.uid\n        }));\n      } catch (e) {\n        toastError(new Error('Unable to prettify. Invalid JSON format.'));\n      }\n    }\n\n    if (codeType === 'xml') {\n      try {\n        const prettyBodyXML = xmlFormat(content, { collapseContent: true });\n\n        const currentMessages = [...(body.ws || [])];\n        currentMessages[index] = {\n          ...currentMessages[index],\n          name: name ? name : `message ${index + 1}`,\n          content: prettyBodyXML\n        };\n\n        dispatch(updateRequestBody({\n          content: currentMessages,\n          itemUid: item.uid,\n          collectionUid: collection.uid\n        }));\n      } catch (e) {\n        toastError(new Error('Unable to prettify. Invalid XML format.'));\n      }\n    }\n  };\n\n  const isSingleMessage = !canClientSendMultipleMessages || body.ws.length === 1;\n\n  return (\n    <StyledWrapper className={`message-container ${isSingleMessage ? 'single' : ''} ${isLast ? 'last' : ''}`}>\n      <div className=\"message-toolbar\">\n        <span className=\"message-label\">Message {index + 1}</span>\n        <div className=\"toolbar-actions\">\n          <WSRequestBodyMode mode={messageFormat} onModeChange={onUpdateMessageType} />\n\n          <ToolHint text=\"Format\" toolhintId={`prettify-msg-${index}`}>\n            <button onClick={onPrettify} className=\"toolbar-btn\">\n              <IconWand size={16} strokeWidth={1.5} />\n            </button>\n          </ToolHint>\n\n          {index > 0 && (\n            <ToolHint text=\"Delete message\" toolhintId={`delete-msg-${index}`}>\n              <button onClick={onDeleteMessage} className=\"toolbar-btn delete\">\n                <IconTrash size={16} strokeWidth={1.5} />\n              </button>\n            </ToolHint>\n          )}\n        </div>\n      </div>\n      <div className=\"editor-container\">\n        <CodeEditor\n          collection={collection}\n          theme={displayedTheme}\n          font={get(preferences, 'font.codeFont', 'default')}\n          fontSize={get(preferences, 'font.codeFontSize')}\n          value={content}\n          onEdit={onEdit}\n          onRun={handleRun}\n          onSave={onSave}\n          mode={codemirrorMode[codeType] ?? 'text/plain'}\n          enableVariableHighlighting={true}\n        />\n      </div>\n    </StyledWrapper>\n  );\n};\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/WsBody/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  height: 100%;\n  position: relative;\n\n  .messages-container {\n    flex: 1;\n    display: flex;\n    flex-direction: column;\n\n    &.single {\n      height: 100%;\n    }\n\n    &.multi {\n      overflow-y: auto;\n      padding-bottom: 48px;\n    }\n  }\n\n  .empty-state {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    height: 100%;\n    gap: 12px;\n\n    p {\n      color: ${(props) => props.theme.colors.text.muted};\n      font-size: 13px;\n    }\n  }\n\n  .add-message-footer {\n    position: absolute;\n    bottom: 0;\n    left: 0;\n    right: 0;\n    padding: 8px;\n    background: ${(props) => props.theme.bg};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/WsBody/index.js",
    "content": "import { get } from 'lodash';\nimport { updateRequestBody } from 'providers/ReduxStore/slices/collections';\nimport { IconPlus } from '@tabler/icons';\nimport React, { useEffect, useRef } from 'react';\nimport { useDispatch } from 'react-redux';\nimport Button from 'ui/Button';\nimport StyledWrapper from './StyledWrapper';\nimport { SingleWSMessage } from './SingleWSMessage/index';\n\nconst WSBody = ({ item, collection, handleRun }) => {\n  const dispatch = useDispatch();\n  const messagesContainerRef = useRef(null);\n  const body = item.draft ? get(item, 'draft.request.body') : get(item, 'request.body');\n\n  const methodType = item.draft ? get(item, 'draft.request.methodType') : get(item, 'request.methodType');\n  const canClientSendMultipleMessages = false;\n\n  // Auto-scroll to the latest message when messages are added\n  useEffect(() => {\n    if (messagesContainerRef.current && body?.ws?.length > 0) {\n      const container = messagesContainerRef.current;\n      container.scrollTop = container.scrollHeight;\n    }\n  }, [body?.ws?.length]);\n\n  const addNewMessage = () => {\n    const currentMessages = Array.isArray(body.ws) ? [...body.ws] : [];\n\n    currentMessages.push({\n      name: `message ${currentMessages.length + 1}`,\n      content: '{}'\n    });\n\n    dispatch(updateRequestBody({\n      content: currentMessages,\n      itemUid: item.uid,\n      collectionUid: collection.uid\n    }));\n  };\n\n  if (!body?.ws || !Array.isArray(body.ws)) {\n    return (\n      <StyledWrapper>\n        <div className=\"empty-state\">\n          <p>No WebSocket messages available</p>\n          <Button\n            onClick={addNewMessage}\n            variant=\"filled\"\n            color=\"secondary\"\n            size=\"sm\"\n            icon={<IconPlus size={14} strokeWidth={1.5} />}\n          >\n            Add Message\n          </Button>\n        </div>\n      </StyledWrapper>\n    );\n  }\n\n  const messagesToShow = body.ws.filter((_, index) => canClientSendMultipleMessages || index === 0);\n\n  return (\n    <StyledWrapper>\n      <div\n        ref={messagesContainerRef}\n        className={`messages-container ${canClientSendMultipleMessages && messagesToShow.length > 1 ? 'multi' : 'single'}`}\n      >\n        {messagesToShow.map((message, index) => (\n          <SingleWSMessage\n            key={index}\n            message={message}\n            item={item}\n            collection={collection}\n            index={index}\n            methodType={methodType}\n            handleRun={handleRun}\n            canClientSendMultipleMessages={canClientSendMultipleMessages}\n            isLast={index === messagesToShow.length - 1}\n          />\n        ))}\n      </div>\n\n      {canClientSendMultipleMessages && (\n        <div className=\"add-message-footer\">\n          <Button\n            onClick={addNewMessage}\n            variant=\"filled\"\n            color=\"secondary\"\n            size=\"sm\"\n            fullWidth\n            icon={<IconPlus size={14} strokeWidth={1.5} />}\n          >\n            Add Message\n          </Button>\n        </div>\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default WSBody;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/WsQueryUrl/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  height: 2.1rem;\n  position: relative;\n  border: ${(props) => props.theme.requestTabPanel.url.border};\n  border-radius: ${(props) => props.theme.border.radius.base};\n\n  .input-container {\n    background-color: ${(props) => props.theme.requestTabPanel.url.bg};\n    border-radius: ${(props) => props.theme.border.radius.base};\n\n    input {\n      background-color: ${(props) => props.theme.requestTabPanel.url.bg};\n      outline: none;\n      box-shadow: none;\n\n      &:focus {\n        outline: none !important;\n        box-shadow: none !important;\n      }\n    }\n  }\n\n  .method-ws {\n    color: ${(props) => props.theme.request.ws};\n  }\n\n  .connection-status-strip {\n    animation: pulse 1.5s ease-in-out infinite;\n    background-color: ${(props) => props.theme.colors.text.green};\n    position: absolute;\n    bottom: 0;\n    left: 0;\n    right: 0;\n    height: 2px;\n  }\n\n  @keyframes pulse {\n    0% {\n      opacity: 0.4;\n    }\n    50% {\n      opacity: 1;\n    }\n    100% {\n      opacity: 0.4;\n    }\n  }\n\n  .infotip {\n    position: relative;\n    display: inline-block;\n    cursor: pointer;\n  }\n\n  .infotip:hover .infotip-text {\n    visibility: visible;\n    opacity: 1;\n  }\n\n  .infotip-text {\n    visibility: hidden;\n    width: auto;\n    background-color: ${(props) => props.theme.background.surface2};\n    color: ${(props) => props.theme.text};\n    text-align: center;\n    border-radius: 4px;\n    padding: 4px 8px;\n    position: absolute;\n    z-index: 1;\n    bottom: 34px;\n    left: 50%;\n    transform: translateX(-50%);\n    opacity: 0;\n    transition: opacity 0.3s;\n    white-space: nowrap;\n  }\n\n  .infotip-text::after {\n    content: '';\n    position: absolute;\n    top: 100%;\n    left: 50%;\n    margin-left: -4px;\n    border-width: 4px;\n    border-style: solid;\n    border-color: ${(props) => props.theme.background.surface2} transparent transparent transparent;\n  }\n\n  .shortcut {\n    font-size: 0.625rem;\n  }\n\n  .connection-controls {\n    .infotip {\n      &:hover {\n        background-color: color-mix(in srgb, ${(props) => props.theme.colors.text.danger} 6%, transparent);\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestPane/WsQueryUrl/index.js",
    "content": "import { IconArrowRight, IconDeviceFloppy, IconPlugConnected, IconPlugConnectedX } from '@tabler/icons';\nimport classnames from 'classnames';\nimport SingleLineEditor from 'components/SingleLineEditor/index';\nimport { requestUrlChanged } from 'providers/ReduxStore/slices/collections';\nimport { wsConnectOnly, saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport { useTheme } from 'providers/Theme';\nimport React, { useEffect, useState, useMemo, useRef } from 'react';\nimport toast from 'react-hot-toast';\nimport { useDispatch } from 'react-redux';\nimport { isMacOS } from 'utils/common/platform';\nimport { hasRequestChanges } from 'utils/collections';\nimport { closeWsConnection, getWsConnectionStatus } from 'utils/network/index';\nimport StyledWrapper from './StyledWrapper';\nimport { interpolateUrl } from 'utils/url';\nimport { getAllVariables } from 'utils/collections';\nimport useDebounce from 'hooks/useDebounce';\nimport get from 'lodash/get';\n\nconst CONNECTION_STATUS = {\n  CONNECTING: 'connecting',\n  CONNECTED: 'connected',\n  DISCONNECTED: 'disconnected'\n};\n\nconst useWsConnectionStatus = (requestId) => {\n  const [connectionStatus, setConnectionStatus] = useState(CONNECTION_STATUS.DISCONNECTED);\n  useEffect(() => {\n    const checkConnectionStatus = async () => {\n      const result = await getWsConnectionStatus(requestId);\n      setConnectionStatus(result?.status ?? CONNECTION_STATUS.DISCONNECTED);\n    };\n    checkConnectionStatus();\n    const interval = setInterval(checkConnectionStatus, 2000);\n    return () => clearInterval(interval);\n  }, [requestId]);\n  return [connectionStatus, setConnectionStatus];\n};\n\nconst WsQueryUrl = ({ item, collection, handleRun }) => {\n  const dispatch = useDispatch();\n  const { theme, displayedTheme } = useTheme();\n  // TODO: reaper, better state for connecting\n  const saveShortcut = isMacOS() ? '⌘S' : 'Ctrl+S';\n  const hasChanges = useMemo(() => hasRequestChanges(item), [item]);\n\n  const [connectionStatus, setConnectionStatus] = useWsConnectionStatus(item.uid);\n  const url = item.draft ? get(item, 'draft.request.url', '') : get(item, 'request.url', '');\n\n  const allVariables = useMemo(() => {\n    return getAllVariables(collection, item);\n  }, [collection, item]);\n\n  const interpolatedURL = useMemo(() => {\n    if (!url) return '';\n    return interpolateUrl({ url, variables: allVariables }) || '';\n  }, [url, allVariables]);\n\n  // Debounce interpolated URL to avoid excessive reconnections\n  const debouncedInterpolatedURL = useDebounce(interpolatedURL, 400);\n  const previousDeboundedInterpolatedURL = useRef(debouncedInterpolatedURL);\n\n  const handleConnect = async () => {\n    dispatch(wsConnectOnly(item, collection.uid));\n    previousDeboundedInterpolatedURL.current = debouncedInterpolatedURL;\n  };\n\n  const handleDisconnect = async (e, notify) => {\n    e && e.stopPropagation();\n    closeWsConnection(item.uid)\n      .then(() => {\n        notify && toast.success('WebSocket connection closed');\n        setConnectionStatus('disconnected');\n      })\n      .catch((err) => {\n        console.error('Failed to close WebSocket connection:', err);\n        notify && toast.error('Failed to close WebSocket connection');\n      });\n  };\n\n  const handleReconnect = async (e) => {\n    e && e.stopPropagation();\n    try {\n      handleDisconnect(e, false);\n      setTimeout(() => {\n        handleConnect(e, false);\n      }, 2000);\n    } catch (err) {\n      console.error('Failed to re-connect WebSocket connection', err);\n    }\n  };\n\n  const handleRunClick = async (e) => {\n    e.stopPropagation();\n    if (!url) {\n      toast.error('Please enter a valid WebSocket URL');\n      return;\n    }\n    handleRun(e);\n  };\n\n  const onSave = (finalValue) => {\n    dispatch(saveRequest(item.uid, collection.uid));\n  };\n\n  const handleUrlChange = (value) => {\n    const finalUrl = value?.trim() ?? value;\n    console.log('finalUrl: ', finalUrl);\n    dispatch(requestUrlChanged({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      url: finalUrl\n    }));\n  };\n\n  // Detect interpolated URL changes and reconnect if connection is active\n  useEffect(() => {\n    if (connectionStatus !== 'connected') return;\n    if (previousDeboundedInterpolatedURL.current === debouncedInterpolatedURL) return;\n    if (debouncedInterpolatedURL === '') return;\n    handleReconnect();\n  }, [debouncedInterpolatedURL, connectionStatus]);\n\n  return (\n    <StyledWrapper>\n      <div className=\"flex items-center h-full\">\n        <div className=\"flex items-center input-container flex-1 w-full h-full relative\">\n          <div className=\"flex items-center justify-center px-[10px]\">\n            <span className=\"text-xs font-medium method-ws\">WS</span>\n          </div>\n          <SingleLineEditor\n            value={url}\n            onSave={(finalValue) => onSave(finalValue)}\n            onChange={handleUrlChange}\n            placeholder=\"ws://localhost:8080 or wss://example.com\"\n            className=\"w-full\"\n            theme={displayedTheme}\n            onRun={handleRun}\n            collection={collection}\n            item={item}\n          />\n          <div className=\"flex items-center h-full cursor-pointer gap-3 mx-3\">\n            <div\n              className=\"infotip\"\n              onClick={(e) => {\n                e.stopPropagation();\n                if (!hasChanges) return;\n                onSave();\n              }}\n            >\n              <IconDeviceFloppy\n                color={hasChanges ? theme.draftColor : theme.requestTabs.icon.color}\n                strokeWidth={1.5}\n                size={20}\n                className={`${hasChanges ? 'cursor-pointer' : 'cursor-default'}`}\n              />\n              <span className=\"infotip-text text-xs\">\n                Save <span className=\"shortcut\">({saveShortcut})</span>\n              </span>\n            </div>\n\n            {connectionStatus === 'connected' && (\n              <div className=\"connection-controls relative flex items-center h-full\">\n                <div className=\"infotip\" onClick={(e) => handleDisconnect(e, true)}>\n                  <IconPlugConnectedX\n                    color={theme.colors.text.danger}\n                    strokeWidth={1.5}\n                    size={20}\n                    className=\"cursor-pointer\"\n                  />\n                  <span className=\"infotip-text text-xs\">Close Connection</span>\n                </div>\n              </div>\n            )}\n\n            {connectionStatus !== 'connected' && (\n              <div className=\"connection-controls relative flex items-center h-full\">\n                <div className=\"infotip\" onClick={handleConnect}>\n                  <IconPlugConnected\n                    className={classnames('cursor-pointer', {\n                      'animate-pulse': connectionStatus === CONNECTION_STATUS.CONNECTING\n                    })}\n                    color={theme.colors.text.green}\n                    strokeWidth={1.5}\n                    size={20}\n                  />\n                  <span className=\"infotip-text text-xs\">Connect</span>\n                </div>\n              </div>\n            )}\n\n            <div data-testid=\"run-button\" className=\"cursor-pointer\" onClick={handleRunClick}>\n              <IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={20} />\n            </div>\n          </div>\n        </div>\n      </div>\n\n      {connectionStatus === CONNECTION_STATUS.CONNECTED && <div className=\"connection-status-strip\"></div>}\n    </StyledWrapper>\n  );\n};\n\nexport default WsQueryUrl;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabPanel/ExampleNotFound/index.js",
    "content": "import React, { useEffect, useState } from 'react';\nimport { closeTabs } from 'providers/ReduxStore/slices/collections/actions';\nimport { useDispatch } from 'react-redux';\nimport ErrorBanner from 'ui/ErrorBanner';\nimport Button from 'ui/Button';\n\nconst ExampleNotFound = ({ exampleUid }) => {\n  const dispatch = useDispatch();\n  const [showErrorMessage, setShowErrorMessage] = useState(false);\n\n  const closeTab = () => {\n    dispatch(closeTabs({\n      tabUids: [exampleUid]\n    }));\n  };\n\n  useEffect(() => {\n    setTimeout(() => {\n      setShowErrorMessage(true);\n    }, 300);\n  }, []);\n\n  if (!showErrorMessage) {\n    return null;\n  }\n\n  const errors = [\n    {\n      title: 'Response example no longer exists',\n      message: 'This can occur when the example definition in your local file has been deleted or updated.'\n    }\n  ];\n\n  return (\n    <div className=\"mt-6 px-6\">\n      <ErrorBanner errors={errors} className=\"mb-4\" />\n      <Button size=\"md\" color=\"secondary\" variant=\"ghost\" onClick={closeTab}>\n        Close Tab\n      </Button>\n    </div>\n  );\n};\n\nexport default ExampleNotFound;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabPanel/FolderNotFound/index.js",
    "content": "import React, { useEffect, useState, useCallback } from 'react';\nimport { closeTabs } from 'providers/ReduxStore/slices/collections/actions';\nimport { useDispatch } from 'react-redux';\nimport ErrorBanner from 'ui/ErrorBanner';\nimport Button from 'ui/Button';\n\nconst FolderNotFound = ({ folderUid }) => {\n  const dispatch = useDispatch();\n  const [showErrorMessage, setShowErrorMessage] = useState(false);\n\n  const closeTab = useCallback(() => {\n    dispatch(\n      closeTabs({\n        tabUids: [folderUid]\n      })\n    );\n  }, [dispatch, folderUid]);\n\n  useEffect(() => {\n    setTimeout(() => {\n      setShowErrorMessage(true);\n    }, 300);\n  }, []);\n\n  if (!showErrorMessage) {\n    return null;\n  }\n\n  const errors = [\n    {\n      title: 'Folder no longer exists',\n      message: 'This can happen when the folder was renamed or deleted on your filesystem.'\n    }\n  ];\n\n  return (\n    <div className=\"mt-6 px-6\">\n      <ErrorBanner errors={errors} className=\"mb-4\" />\n      <Button size=\"md\" color=\"secondary\" variant=\"ghost\" onClick={closeTab}>\n        Close Tab\n      </Button>\n    </div>\n  );\n};\n\nexport default FolderNotFound;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabPanel/RequestIsLoading/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  div.card {\n    background: ${(props) => props.theme.requestTabPanel.card.bg};\n    border: 1px solid ${(props) => props.theme.requestTabPanel.card.border};\n\n    div.hr {\n      border-bottom: 1px solid ${(props) => props.theme.requestTabPanel.card.hr};\n      height: 1px;\n    }\n\n    div.border-top {\n      border-top: 1px solid ${(props) => props.theme.requestTabPanel.card.border};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabPanel/RequestIsLoading/index.js",
    "content": "import { IconLoader2, IconFile } from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\n\nconst RequestIsLoading = ({ item }) => {\n  return (\n    <StyledWrapper>\n      <div className=\"flex flex-col p-4\">\n        <div className=\"card shadow-sm rounded-md p-4 w-[600px]\">\n          <div>\n            <div className=\"font-medium flex items-center gap-2 pb-4\">\n              <IconFile size={16} strokeWidth={1.5} className=\"text-gray-400\" />\n              File Info\n            </div>\n            <div className=\"hr\" />\n\n            <div className=\"flex items-center mt-2\">\n              <span className=\"w-12 mr-2 text-muted\">Name:</span>\n              <div>\n                {item?.name}\n              </div>\n            </div>\n\n            <div className=\"flex items-center mt-1\">\n              <span className=\"w-12 mr-2 text-muted\">Path:</span>\n              <div className=\"break-all\">\n                {item?.pathname}\n              </div>\n            </div>\n\n            <div className=\"flex items-center mt-1 pb-4\">\n              <span className=\"w-12 mr-2 text-muted\">Size:</span>\n              <div>\n                {item?.size?.toFixed?.(2)} MB\n              </div>\n            </div>\n\n            <div className=\"hr\" />\n            <div className=\"flex items-center gap-2 mt-4\">\n              <IconLoader2 className=\"animate-spin\" size={16} strokeWidth={2} />\n              <span>Loading...</span>\n            </div>\n          </div>\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default RequestIsLoading;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabPanel/RequestNotFound/index.js",
    "content": "import React, { useEffect, useState } from 'react';\nimport { closeTabs } from 'providers/ReduxStore/slices/collections/actions';\nimport { useDispatch } from 'react-redux';\nimport ErrorBanner from 'ui/ErrorBanner';\nimport Button from 'ui/Button';\n\nconst RequestNotFound = ({ itemUid }) => {\n  const dispatch = useDispatch();\n  const [showErrorMessage, setShowErrorMessage] = useState(false);\n\n  const closeTab = () => {\n    dispatch(\n      closeTabs({\n        tabUids: [itemUid]\n      })\n    );\n  };\n\n  useEffect(() => {\n    setTimeout(() => {\n      setShowErrorMessage(true);\n    }, 300);\n  }, []);\n\n  // add a delay component in react that shows a loading spinner\n  // and then shows the error message after a delay\n  // this will prevent the error message from flashing on the screen\n\n  if (!showErrorMessage) {\n    return null;\n  }\n\n  const errors = [\n    {\n      title: 'Request no longer exists',\n      message: 'This can happen when the file associated with this request was deleted on your filesystem.'\n    }\n  ];\n\n  return (\n    <div className=\"mt-6 px-6\">\n      <ErrorBanner errors={errors} className=\"mb-4\" />\n      <Button size=\"md\" color=\"secondary\" variant=\"ghost\" onClick={closeTab}>\n        Close Tab\n      </Button>\n    </div>\n  );\n};\n\nexport default RequestNotFound;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  div.card {\n    background: ${(props) => props.theme.requestTabPanel.card.bg};\n    border: 1px solid ${(props) => props.theme.requestTabPanel.card.border};\n\n    div.hr {\n      border-bottom: 1px solid ${(props) => props.theme.requestTabPanel.card.hr};\n      height: 1px;\n    }\n\n    div.border-top {\n      border-top: 1px solid ${(props) => props.theme.requestTabPanel.card.border};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabPanel/RequestNotLoaded/index.js",
    "content": "import { IconLoader2, IconFile, IconAlertTriangle } from '@tabler/icons';\nimport { loadLargeRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport { useDispatch } from 'react-redux';\nimport StyledWrapper from './StyledWrapper';\n\nconst RequestNotLoaded = ({ collection, item }) => {\n  const dispatch = useDispatch();\n\n  const handleLoadLargeRequest = () => {\n    !item?.loading && dispatch(loadLargeRequest({ collectionUid: collection?.uid, pathname: item?.pathname }));\n  };\n\n  return (\n    <StyledWrapper>\n      <div className=\"flex flex-col p-4\">\n        <div className=\"card shadow-sm rounded-md p-4 w-[685px]\">\n          <div>\n            <div className=\"font-medium flex items-center gap-2 pb-4\">\n              <IconFile size={16} strokeWidth={1.5} className=\"text-gray-400\" />\n              File Info\n            </div>\n            <div className=\"hr\" />\n\n            <div className=\"flex items-center mt-2\">\n              <span className=\"w-12 mr-2 text-muted\">Name:</span>\n              <div>{item?.name}</div>\n            </div>\n\n            <div className=\"flex items-center mt-1\">\n              <span className=\"w-12 mr-2 text-muted\">Path:</span>\n              <div className=\"break-all\">{item?.pathname}</div>\n            </div>\n\n            <div className=\"flex items-center mt-1 pb-4\">\n              <span className=\"w-12 mr-2 text-muted\">Size:</span>\n              <div>{item?.size?.toFixed?.(2)} MB</div>\n            </div>\n\n            {!item?.error && (\n              <div className=\"flex flex-col\">\n                <div className=\"flex items-center gap-2 px-3 py-2 title bg-yellow-50 dark:bg-yellow-900/20\">\n                  <IconAlertTriangle size={16} className=\"text-yellow-500\" />\n                  <span>The request wasn't loaded due to its large size. Please try again with the following options:</span>\n                </div>\n                <div className=\"flex flex-row mt-6 items-center gap-2 w-full\">\n                  <button\n                    className={`submit btn btn-sm btn-secondary w-fit h-fit flex flex-row gap-2 ${item?.loading ? 'opacity-50 cursor-blocked' : ''}`}\n                    onClick={handleLoadLargeRequest}\n                  >\n                    Load Request\n                  </button>\n                  <p>(Uses a regex based parsing approach)</p>\n                </div>\n              </div>\n            )}\n\n            {item?.loading && (\n              <>\n                <div className=\"hr mt-4\" />\n                <div className=\"flex items-center gap-2 mt-4\">\n                  <IconLoader2 className=\"animate-spin\" size={16} strokeWidth={2} />\n                  <span>Loading...</span>\n                </div>\n              </>\n            )}\n          </div>\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default RequestNotLoaded;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabPanel/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  &.dragging {\n    cursor: col-resize;\n\n    &.vertical-layout {\n      cursor: row-resize;\n    }\n  }\n\n  .request-pane {\n    flex-shrink: 0;\n  }\n\n  .response-pane {\n    min-width: 0;\n  }\n\n  div.dragbar-wrapper {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 10px;\n    min-width: 10px;\n    padding: 0;\n    cursor: col-resize;\n    background: transparent;\n    position: relative;\n\n    div.dragbar-handle {\n      display: flex;\n      height: 100%;\n      width: 1px;\n      border-left: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.border};\n    }\n\n    &:hover div.dragbar-handle {\n      border-left: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.activeBorder};\n    }\n  }\n\n  &.vertical-layout {\n    .request-pane {\n      padding-bottom: 0.5rem;\n    }\n\n    .response-pane {\n      padding-top: 0.5rem;\n    }\n\n    div.dragbar-wrapper {\n      width: 100%;\n      height: 10px;\n      cursor: row-resize;\n      padding: 0 1rem;\n      position: relative;\n\n      div.dragbar-handle {\n        width: 100%;\n        height: 1px;\n        border-left: none;\n        border-top: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.border};\n      }\n\n      &:hover div.dragbar-handle {\n        border-left: none;\n        border-top: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.activeBorder};\n      }\n    }\n  }\n\n  div.graphql-docs-explorer-container {\n    background: ${(props) => props.theme.requestTabPanel.graphqlDocsExplorer.bg};\n    color: ${(props) => props.theme.requestTabPanel.graphqlDocsExplorer.color};\n    outline: none;\n    box-shadow: rgb(0 0 0 / 15%) 0px 0px 8px;\n    position: absolute;\n    right: 0px;\n    z-index: 2000;\n    width: 350px;\n    height: 100%;\n\n    .doc-explorer-contents,\n    .doc-explorer,\n    .search-box > input,\n    .search-box-clear {\n      background-color: ${(props) => props.theme.requestTabPanel.graphqlDocsExplorer.bg};\n      color: ${(props) => props.theme.requestTabPanel.graphqlDocsExplorer.color};\n    }\n\n    div.doc-explorer-title {\n      text-align: left;\n    }\n\n    div.doc-explorer-rhs {\n      display: flex;\n    }\n\n    // GraphQL docs color overrides\n    .doc-explorer-back {\n      color: ${(props) => props.theme.textLink};\n\n      &:before {\n        border-left-color: ${(props) => props.theme.textLink};\n        border-top-color: ${(props) => props.theme.textLink};\n      }\n    }\n\n    .doc-explorer-contents {\n      border-top-color: ${(props) => props.theme.border.border2};\n    }\n\n    .doc-type-description code,\n    .doc-category code {\n      color: ${(props) => props.theme.codemirror.tokens.keyword};\n      background-color: ${(props) => props.theme.background.surface0};\n      border-color: ${(props) => props.theme.border.border1};\n    }\n\n    .doc-category-title {\n      border-bottom-color: ${(props) => props.theme.border.border1};\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n\n    .doc-category-item {\n      color: ${(props) => props.theme.colors.text.subtext2};\n    }\n\n    .keyword {\n      color: ${(props) => props.theme.codemirror.tokens.property};\n    }\n\n    .type-name {\n      color: ${(props) => props.theme.codemirror.tokens.atom};\n    }\n\n    .field-name {\n      color: ${(props) => props.theme.codemirror.tokens.property};\n    }\n\n    .field-short-description {\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n\n    .enum-value {\n      color: ${(props) => props.theme.textLink};\n    }\n\n    .arg-name {\n      color: ${(props) => props.theme.colors.text.purple};\n    }\n\n    .arg-default-value {\n      color: ${(props) => props.theme.colors.text.green};\n    }\n\n    .doc-deprecation {\n      background: ${(props) => props.theme.status.warning.background};\n      box-shadow: inset 0 0 1px ${(props) => props.theme.status.warning.border};\n      color: ${(props) => props.theme.colors.text.muted};\n\n      &:before {\n        color: ${(props) => props.theme.status.warning.text};\n      }\n    }\n\n    .show-btn {\n      border-color: ${(props) => props.theme.border.border2};\n      background: ${(props) => props.theme.background.surface0};\n      color: ${(props) => props.theme.text};\n    }\n\n    .search-box {\n      border-bottom-color: ${(props) => props.theme.border.border1};\n    }\n\n    .search-box-clear {\n      background-color: ${(props) => props.theme.overlay.overlay1};\n      color: ${(props) => props.theme.colors.text.white};\n\n      &:hover {\n        background-color: ${(props) => props.theme.overlay.overlay2};\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabPanel/index.js",
    "content": "import React, { useState, useEffect, useRef, useCallback } from 'react';\nimport find from 'lodash/find';\nimport toast from 'react-hot-toast';\nimport { useSelector, useDispatch } from 'react-redux';\nimport GraphQLRequestPane from 'components/RequestPane/GraphQLRequestPane';\nimport HttpRequestPane from 'components/RequestPane/HttpRequestPane';\nimport GrpcRequestPane from 'components/RequestPane/GrpcRequestPane/index';\nimport ResponsePane from 'components/ResponsePane';\nimport GrpcResponsePane from 'components/ResponsePane/GrpcResponsePane';\nimport { findItemInCollection } from 'utils/collections';\nimport { cancelRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport RequestNotFound from './RequestNotFound';\nimport QueryUrl from 'components/RequestPane/QueryUrl/index';\nimport GrpcQueryUrl from 'components/RequestPane/GrpcQueryUrl/index';\nimport NetworkError from 'components/ResponsePane/NetworkError';\nimport RunnerResults from 'components/RunnerResults';\nimport VariablesEditor from 'components/VariablesEditor';\nimport CollectionSettings from 'components/CollectionSettings';\nimport { DocExplorer } from '@usebruno/graphql-docs';\n\nimport StyledWrapper from './StyledWrapper';\nimport FolderSettings from 'components/FolderSettings';\nimport { getGlobalEnvironmentVariables, getGlobalEnvironmentVariablesMasked } from 'utils/collections/index';\nimport { produce } from 'immer';\nimport CollectionOverview from 'components/CollectionSettings/Overview';\nimport RequestNotLoaded from './RequestNotLoaded';\nimport RequestIsLoading from './RequestIsLoading';\nimport FolderNotFound from './FolderNotFound';\nimport ExampleNotFound from './ExampleNotFound';\nimport WsQueryUrl from 'components/RequestPane/WsQueryUrl';\nimport WSRequestPane from 'components/RequestPane/WSRequestPane';\nimport WSResponsePane from 'components/ResponsePane/WsResponsePane';\nimport { useTabPaneBoundaries } from 'hooks/useTabPaneBoundaries/index';\nimport ResponseExample from 'components/ResponseExample';\nimport WorkspaceOverview from 'components/WorkspaceHome/WorkspaceOverview';\nimport Preferences from 'components/Preferences';\nimport EnvironmentSettings from 'components/Environments/EnvironmentSettings';\nimport GlobalEnvironmentSettings from 'components/Environments/GlobalEnvironmentSettings';\nimport OpenAPISyncTab from 'components/OpenAPISyncTab';\nimport OpenAPISpecTab from 'components/OpenAPISpecTab';\n\nconst MIN_LEFT_PANE_WIDTH = 300;\nconst MIN_RIGHT_PANE_WIDTH = 490;\nconst MIN_TOP_PANE_HEIGHT = 150;\nconst MIN_BOTTOM_PANE_HEIGHT = 150;\n\nconst RequestTabPanel = () => {\n  const dispatch = useDispatch();\n  const tabs = useSelector((state) => state.tabs.tabs);\n  const activeTabUid = useSelector((state) => state.tabs.activeTabUid);\n  const focusedTab = find(tabs, (t) => t.uid === activeTabUid);\n  const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);\n  const _collections = useSelector((state) => state.collections.collections);\n  const preferences = useSelector((state) => state.app.preferences);\n  const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);\n  const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);\n  const isVerticalLayout = preferences?.layout?.responsePaneOrientation === 'vertical';\n  const isConsoleOpen = useSelector((state) => state.logs.isConsoleOpen);\n\n  // Use ref to avoid stale closure in event handlers\n  const isVerticalLayoutRef = useRef(isVerticalLayout);\n  useEffect(() => {\n    isVerticalLayoutRef.current = isVerticalLayout;\n  }, [isVerticalLayout]);\n\n  // merge `globalEnvironmentVariables` into the active collection and rebuild `collections` immer proxy object\n  const collections = produce(_collections, (draft) => {\n    const collection = find(draft, (c) => c.uid === focusedTab?.collectionUid);\n\n    if (collection) {\n      // add selected global env variables to the collection object\n      const globalEnvironmentVariables = getGlobalEnvironmentVariables({\n        globalEnvironments,\n        activeGlobalEnvironmentUid\n      });\n      const globalEnvSecrets = getGlobalEnvironmentVariablesMasked({ globalEnvironments, activeGlobalEnvironmentUid });\n      collection.globalEnvironmentVariables = globalEnvironmentVariables;\n      collection.globalEnvSecrets = globalEnvSecrets;\n    }\n  });\n\n  const collection = find(collections, (c) => c.uid === focusedTab?.collectionUid);\n  const [dragging, setDragging] = useState(false);\n  const draggingRef = useRef(false);\n\n  const { left: leftPaneWidth, top: topPaneHeight, reset: resetPaneBoundaries, setTop: setTopPaneHeight, setLeft: setLeftPaneWidth } = useTabPaneBoundaries(activeTabUid);\n  const previousTopPaneHeight = useRef(null); // Store height before devtools opens\n\n  // Not a recommended pattern here to have the child component\n  // make a callback to set state, but treating this as an exception\n  const docExplorerRef = useRef(null);\n  const mainSectionRef = useRef(null);\n\n  const [schema, setSchema] = useState(null);\n  const [showGqlDocs, setShowGqlDocs] = useState(false);\n  const onSchemaLoad = useCallback((schema) => setSchema(schema), []);\n  const toggleDocs = useCallback(() => setShowGqlDocs((prev) => !prev), []);\n\n  const handleGqlClickReference = useCallback((reference) => {\n    if (docExplorerRef.current) {\n      docExplorerRef.current.showDocForReference(reference);\n    }\n    if (!showGqlDocs) {\n      setShowGqlDocs(true);\n    }\n  }, []);\n\n  const handleMouseMove = useCallback((e) => {\n    if (!draggingRef.current || !mainSectionRef.current) return;\n\n    e.preventDefault();\n    const mainRect = mainSectionRef.current.getBoundingClientRect();\n\n    if (isVerticalLayoutRef.current) {\n      const newHeight = e.clientY - mainRect.top;\n      const maxHeight = mainRect.height - MIN_BOTTOM_PANE_HEIGHT;\n      // Clamp to bounds instead of returning early\n      const clampedHeight = Math.max(MIN_TOP_PANE_HEIGHT, Math.min(newHeight, maxHeight));\n      setTopPaneHeight(clampedHeight);\n    } else {\n      const newWidth = e.clientX - mainRect.left;\n      const maxWidth = mainRect.width - MIN_RIGHT_PANE_WIDTH;\n      // Clamp to bounds instead of returning early\n      const clampedWidth = Math.max(MIN_LEFT_PANE_WIDTH, Math.min(newWidth, maxWidth));\n      setLeftPaneWidth(clampedWidth);\n    }\n  }, [setTopPaneHeight, setLeftPaneWidth]);\n\n  const handleMouseUp = useCallback((e) => {\n    if (draggingRef.current) {\n      e.preventDefault();\n      draggingRef.current = false;\n      setDragging(false);\n    }\n  }, []);\n\n  const handleDragbarMouseDown = useCallback((e) => {\n    e.preventDefault();\n    draggingRef.current = true;\n    setDragging(true);\n  }, []);\n\n  useEffect(() => {\n    document.addEventListener('mouseup', handleMouseUp);\n    document.addEventListener('mousemove', handleMouseMove);\n\n    return () => {\n      document.removeEventListener('mouseup', handleMouseUp);\n      document.removeEventListener('mousemove', handleMouseMove);\n    };\n  }, [handleMouseUp, handleMouseMove]);\n\n  useEffect(() => {\n    if (!isVerticalLayout) return;\n\n    if (isConsoleOpen) {\n      // Store current height before reducing\n      if (previousTopPaneHeight.current === null) {\n        previousTopPaneHeight.current = topPaneHeight;\n      }\n      // Reduce request pane height to make room for response pane when devtools is open\n      const maxHeight = 200;\n      if (topPaneHeight > maxHeight) {\n        setTopPaneHeight(maxHeight);\n      }\n    } else {\n      // Restore previous height when devtools closes\n      if (previousTopPaneHeight.current !== null) {\n        setTopPaneHeight(previousTopPaneHeight.current);\n        previousTopPaneHeight.current = null;\n      }\n    }\n  }, [isConsoleOpen, isVerticalLayout]);\n\n  if (typeof window == 'undefined') {\n    return <div></div>;\n  }\n\n  if (!activeTabUid || !focusedTab) {\n    return <div className=\"pb-4 px-4\">Loading...</div>;\n  }\n\n  if (focusedTab.type === 'global-environment-settings') {\n    return <GlobalEnvironmentSettings />;\n  }\n\n  if (focusedTab.type === 'preferences') {\n    return <Preferences />;\n  }\n\n  if (focusedTab.type === 'workspaceOverview') {\n    return activeWorkspace ? <WorkspaceOverview workspace={activeWorkspace} /> : null;\n  }\n\n  if (focusedTab.type === 'workspaceEnvironments') {\n    return <GlobalEnvironmentSettings />;\n  }\n\n  if (!focusedTab.uid || !focusedTab.collectionUid) {\n    return <div className=\"pb-4 px-4\">An error occurred!</div>;\n  }\n\n  if (!collection || !collection.uid) {\n    return <div className=\"pb-4 px-4\">Collection not found!</div>;\n  }\n\n  if (focusedTab.type === 'response-example') {\n    const item = findItemInCollection(collection, focusedTab.itemUid);\n    const example = item?.examples?.find((ex) => ex.uid === focusedTab.uid);\n\n    if (!example) {\n      return <ExampleNotFound itemUid={focusedTab.itemUid} exampleUid={focusedTab.uid} />;\n    }\n    return <ResponseExample item={item} collection={collection} example={example} />;\n  }\n\n  const item = findItemInCollection(collection, activeTabUid);\n  const isGrpcRequest = item?.type === 'grpc-request';\n  const isWsRequest = item?.type === 'ws-request';\n\n  if (focusedTab.type === 'collection-runner') {\n    return <RunnerResults collection={collection} />;\n  }\n\n  if (focusedTab.type === 'variables') {\n    return <VariablesEditor collection={collection} />;\n  }\n\n  if (focusedTab.type === 'collection-settings') {\n    return <CollectionSettings collection={collection} />;\n  }\n\n  if (focusedTab.type === 'collection-overview') {\n    return <CollectionOverview collection={collection} />;\n  }\n\n  if (focusedTab.type === 'folder-settings') {\n    const folder = findItemInCollection(collection, focusedTab.folderUid);\n    if (!folder) {\n      return <FolderNotFound folderUid={focusedTab.folderUid} />;\n    }\n\n    return <FolderSettings collection={collection} folder={folder} />;\n  }\n\n  if (focusedTab.type === 'environment-settings') {\n    return <EnvironmentSettings collection={collection} />;\n  }\n\n  if (focusedTab.type === 'openapi-sync') {\n    return <OpenAPISyncTab collection={collection} />;\n  }\n\n  if (focusedTab.type === 'openapi-spec') {\n    return <OpenAPISpecTab collection={collection} />;\n  }\n\n  if (!item || !item.uid) {\n    return <RequestNotFound itemUid={activeTabUid} />;\n  }\n\n  if (item?.partial) {\n    return <RequestNotLoaded item={item} collection={collection} />;\n  }\n\n  if (item?.loading) {\n    return <RequestIsLoading item={item} />;\n  }\n\n  const handleRun = async () => {\n    const request = item.draft ? item.draft.request : item.request;\n\n    if (isGrpcRequest && !request.url) {\n      toast.error('Please enter a valid gRPC server URL');\n      return;\n    }\n\n    if (isGrpcRequest && !request.method) {\n      toast.error('Please select a gRPC method');\n      return;\n    }\n\n    if (isWsRequest && !request.url) {\n      toast.error('Please enter a valid WebSocket URL');\n      return;\n    }\n\n    if (item.response?.stream?.running) {\n      dispatch(cancelRequest(item.cancelTokenUid, item, collection)).catch((err) =>\n        toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {\n          duration: 5000\n        }));\n    } else if (item.requestState !== 'sending' && item.requestState !== 'queued') {\n      dispatch(sendRequest(item, collection.uid)).catch((err) =>\n        toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {\n          duration: 5000\n        }));\n    }\n  };\n\n  const renderQueryUrl = () => {\n    if (isGrpcRequest) {\n      return <GrpcQueryUrl item={item} collection={collection} handleRun={handleRun} />;\n    }\n    if (isWsRequest) {\n      return <WsQueryUrl item={item} collection={collection} handleRun={handleRun} />;\n    }\n    return <QueryUrl item={item} collection={collection} handleRun={handleRun} />;\n  };\n\n  const renderRequestPane = () => {\n    switch (item.type) {\n      case 'graphql-request':\n        return (\n          <GraphQLRequestPane\n            item={item}\n            collection={collection}\n            onSchemaLoad={onSchemaLoad}\n            toggleDocs={toggleDocs}\n            handleGqlClickReference={handleGqlClickReference}\n          />\n        );\n      case 'http-request':\n        return <HttpRequestPane item={item} collection={collection} />;\n      case 'grpc-request':\n        return <GrpcRequestPane item={item} collection={collection} handleRun={handleRun} />;\n      case 'ws-request':\n        return <WSRequestPane item={item} collection={collection} handleRun={handleRun} />;\n      default:\n        return null;\n    }\n  };\n\n  const renderResponsePane = () => {\n    switch (item.type) {\n      case 'grpc-request':\n        return <GrpcResponsePane item={item} collection={collection} response={item.response} />;\n      case 'ws-request':\n        return <WSResponsePane item={item} collection={collection} response={item.response} />;\n      default:\n        return <ResponsePane item={item} collection={collection} response={item.response} />;\n    }\n  };\n\n  const requestPaneStyle = isVerticalLayout\n    ? {\n        height: `${Math.max(topPaneHeight, MIN_TOP_PANE_HEIGHT)}px`,\n        minHeight: `${MIN_TOP_PANE_HEIGHT}px`,\n        width: '100%'\n      }\n    : {\n        width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`\n      };\n\n  return (\n    <StyledWrapper\n      className={`flex flex-col flex-grow relative ${dragging ? 'dragging' : ''} ${\n        isVerticalLayout ? 'vertical-layout' : ''\n      }`}\n    >\n      <div className=\"pt-3 pb-3 px-4\">\n        {renderQueryUrl()}\n      </div>\n      <section ref={mainSectionRef} className={`main flex ${isVerticalLayout ? 'flex-col' : ''} flex-grow pb-4 relative overflow-auto`}>\n        <section className=\"request-pane\">\n          <div\n            className=\"px-4 h-full\"\n            style={requestPaneStyle}\n          >\n            {renderRequestPane()}\n          </div>\n        </section>\n\n        <div\n          className=\"dragbar-wrapper\"\n          onDoubleClick={(e) => {\n            e.preventDefault();\n            resetPaneBoundaries();\n          }}\n          onMouseDown={handleDragbarMouseDown}\n        >\n          <div className=\"dragbar-handle\" />\n        </div>\n\n        <section className=\"response-pane flex-grow overflow-x-auto\">\n          {renderResponsePane()}\n        </section>\n      </section>\n\n      {item.type === 'graphql-request' ? (\n        <div className={`graphql-docs-explorer-container ${showGqlDocs ? '' : 'hidden'}`}>\n          <DocExplorer schema={schema} ref={(r) => (docExplorerRef.current = r)}>\n            <button className=\"mr-2\" onClick={toggleDocs} aria-label=\"Close Documentation Explorer\">\n              {'\\u2715'}\n            </button>\n          </DocExplorer>\n        </div>\n      ) : null}\n    </StyledWrapper>\n  );\n};\n\nexport default RequestTabPanel;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabs/CollectionHeader/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .collection-switcher {\n    display: flex;\n    align-items: center;\n    gap: 4px;\n  }\n\n  .switcher-trigger {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    padding: 4px 8px;\n    border: none;\n    border-radius: 4px;\n    background: transparent;\n    color: ${(props) => props.theme.text};\n    cursor: pointer;\n    font-weight: 500;\n    transition: background-color 0.15s ease;\n\n    &:hover {\n      background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n    }\n\n    .switcher-name {\n      max-width: 300px;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      white-space: nowrap;\n\n      &.scratch-collection {\n        font-weight: 600;\n        font-size: 15px;\n      }\n    }\n\n    .tab-count {\n      font-size: 11px;\n      font-weight: 500;\n      padding: 1px 6px;\n      border-radius: 10px;\n      background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n      min-width: 18px;\n      text-align: center;\n    }\n\n    .chevron {\n      opacity: 0.6;\n      flex-shrink: 0;\n    }\n  }\n\n  .workspace-actions-trigger {\n    cursor: pointer;\n    opacity: 0.6;\n    padding: 4px;\n    border-radius: 4px;\n    transition: opacity 0.15s ease, background-color 0.15s ease;\n\n    &:hover {\n      opacity: 1;\n      background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n    }\n  }\n\n  .workspace-rename-container {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    padding: 4px 8px;\n  }\n\n  .workspace-input-wrapper {\n    display: flex;\n    align-items: center;\n    border: 1px solid ${(props) => props.theme.input.border};\n    border-radius: 3px;\n    background: ${(props) => props.theme.input.bg};\n    min-width: 150px;\n\n    &:focus-within {\n      border-color: ${(props) => props.theme.input.focusBorder};\n    }\n  }\n\n  .workspace-name-input {\n    font-size: 14px;\n    font-weight: 500;\n    padding: 2px 6px;\n    border: none;\n    background: transparent;\n    color: ${(props) => props.theme.text};\n    outline: none;\n    flex: 1;\n    min-width: 0;\n  }\n\n  .cog-btn {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    flex-shrink: 0;\n    width: 22px;\n    height: 100%;\n    border: none;\n    cursor: pointer;\n    background: transparent;\n    color: ${(props) => props.theme.text};\n    opacity: 0.5;\n\n    &:hover {\n      opacity: 1;\n    }\n  }\n\n  .inline-actions {\n    display: flex;\n    align-items: center;\n    gap: 2px;\n  }\n\n  .inline-action-btn {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 22px;\n    height: 22px;\n    border: none;\n    border-radius: 3px;\n    cursor: pointer;\n    background: transparent;\n    color: ${(props) => props.theme.text};\n\n    &:hover {\n      background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n    }\n\n    &.save {\n      color: ${(props) => props.theme.colors.text.green};\n    }\n\n    &.cancel {\n      color: ${(props) => props.theme.colors.text.danger};\n    }\n  }\n\n  .workspace-error {\n    font-size: 12px;\n    color: ${(props) => props.theme.colors.text.danger};\n    margin-left: 8px;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabs/CollectionHeader/index.js",
    "content": "import { useState, useRef, useEffect, useCallback } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport {\n  IconCategory,\n  IconBox,\n  IconChevronDown,\n  IconRun,\n  IconEye,\n  IconSettings,\n  IconDots,\n  IconEdit,\n  IconX,\n  IconCheck,\n  IconFolder,\n  IconUpload\n} from '@tabler/icons';\nimport OpenAPISyncIcon from 'components/Icons/OpenAPISync';\nimport { switchWorkspace, renameWorkspaceAction, exportWorkspaceAction, confirmWorkspaceCreation, cancelWorkspaceCreation } from 'providers/ReduxStore/slices/workspaces/actions';\nimport { updateWorkspace } from 'providers/ReduxStore/slices/workspaces';\nimport { showInFolder } from 'providers/ReduxStore/slices/collections/actions';\nimport { addTab, focusTab } from 'providers/ReduxStore/slices/tabs';\nimport { uuid } from 'utils/common';\nimport toast from 'react-hot-toast';\nimport Dropdown from 'components/Dropdown';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport CloseWorkspace from 'components/Sidebar/CloseWorkspace';\nimport CreateWorkspace from 'components/WorkspaceSidebar/CreateWorkspace';\nimport EnvironmentSelector from 'components/Environments/EnvironmentSelector';\nimport ToolHint from 'components/ToolHint';\nimport JsSandboxMode from 'components/SecuritySettings/JsSandboxMode';\nimport ActionIcon from 'ui/ActionIcon';\nimport { getRevealInFolderLabel } from 'utils/common/platform';\nimport { normalizePath } from 'utils/common/path';\nimport classNames from 'classnames';\nimport StyledWrapper from './StyledWrapper';\nimport { useTheme } from 'providers/Theme';\nimport { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';\nimport StatusBadge from 'ui/StatusBadge/index';\n\nconst CollectionHeader = ({ collection, isScratchCollection }) => {\n  const dispatch = useDispatch();\n  const workspaces = useSelector((state) => state.workspaces.workspaces);\n  const activeWorkspaceUid = useSelector((state) => state.workspaces.activeWorkspaceUid);\n  const collections = useSelector((state) => state.collections.collections);\n  const tabs = useSelector((state) => state.tabs.tabs);\n\n  // Get the current active workspace\n  const currentWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);\n  const gitRootPath = collection?.git?.gitRootPath;\n  const isOpenAPISyncEnabled = useBetaFeature(BETA_FEATURES.OPENAPI_SYNC);\n\n  // Workspace rename state\n  const [isRenamingWorkspace, setIsRenamingWorkspace] = useState(false);\n  const [workspaceNameInput, setWorkspaceNameInput] = useState('');\n  const [workspaceNameError, setWorkspaceNameError] = useState('');\n  const [closeWorkspaceModalOpen, setCloseWorkspaceModalOpen] = useState(false);\n  const [createWorkspaceModalOpen, setCreateWorkspaceModalOpen] = useState(false);\n\n  const switcherRef = useRef();\n  const workspaceActionsRef = useRef();\n  const workspaceNameInputRef = useRef(null);\n  const workspaceRenameContainerRef = useRef(null);\n  const openingAdvancedRef = useRef(false);\n  const clickedOutsideRef = useRef(false);\n  const handleSaveRef = useRef(null);\n  const tempWorkspaceUidRef = useRef(null);\n  const isSavingRef = useRef(false);\n\n  const onSwitcherCreate = (ref) => (switcherRef.current = ref);\n  const onWorkspaceActionsCreate = (ref) => (workspaceActionsRef.current = ref);\n\n  // Auto-enter rename mode when workspace is newly created\n  useEffect(() => {\n    if (isScratchCollection && currentWorkspace?.isNewlyCreated) {\n      dispatch(updateWorkspace({ uid: currentWorkspace.uid, isNewlyCreated: false }));\n      setIsRenamingWorkspace(true);\n      setWorkspaceNameInput(currentWorkspace.name || '');\n      setWorkspaceNameError('');\n    }\n  }, [isScratchCollection, currentWorkspace?.isNewlyCreated, currentWorkspace?.uid, currentWorkspace?.name, dispatch]);\n\n  const handleCancelWorkspaceRename = useCallback(() => {\n    if (openingAdvancedRef.current) return;\n    if (currentWorkspace?.isCreating) {\n      dispatch(cancelWorkspaceCreation(currentWorkspace.uid));\n      return;\n    }\n    setIsRenamingWorkspace(false);\n    setWorkspaceNameInput('');\n    setWorkspaceNameError('');\n  }, [currentWorkspace?.isCreating, currentWorkspace?.uid, dispatch]);\n\n  useEffect(() => {\n    if (!isRenamingWorkspace) return;\n\n    const handleClickOutside = (event) => {\n      if (workspaceRenameContainerRef.current && !workspaceRenameContainerRef.current.contains(event.target)) {\n        if (currentWorkspace?.isCreating) {\n          clickedOutsideRef.current = true;\n          handleSaveRef.current?.();\n        } else {\n          handleCancelWorkspaceRename();\n        }\n      }\n    };\n\n    document.addEventListener('mousedown', handleClickOutside);\n    const timer = setTimeout(() => {\n      workspaceNameInputRef.current?.focus();\n      workspaceNameInputRef.current?.select();\n    }, 50);\n    return () => {\n      document.removeEventListener('mousedown', handleClickOutside);\n      clearTimeout(timer);\n    };\n  }, [isRenamingWorkspace, handleCancelWorkspaceRename, currentWorkspace?.isCreating]);\n\n  const collectionUpdates = useSelector((state) => state.openapiSync?.collectionUpdates || {});\n  const { theme } = useTheme();\n\n  if (!collection) {\n    return null;\n  }\n\n  const hasOpenApiSyncConfigured = collection?.brunoConfig?.openapi?.[0]?.sourceUrl;\n  const hasOpenApiUpdates = hasOpenApiSyncConfigured && collectionUpdates[collection.uid]?.hasUpdates;\n  const hasOpenApiError = hasOpenApiSyncConfigured && collectionUpdates[collection.uid]?.error;\n\n  // Get mounted collections for the current workspace (excluding scratch collections)\n  const mountedCollections = collections.filter((c) => {\n    if (c.mountStatus !== 'mounted') return false;\n\n    const isScratch = workspaces.some((w) => w.scratchCollectionUid === c.uid);\n    if (isScratch) return false;\n\n    const workspaceCollectionPaths = currentWorkspace?.collections?.map((wc) => wc.path) || [];\n    return workspaceCollectionPaths.some((wcPath) => normalizePath(c.pathname) === normalizePath(wcPath));\n  });\n\n  // Count tabs for the current collection\n  const tabCount = tabs.filter((t) => t.collectionUid === collection.uid).length;\n\n  // Get tab count for a given collection uid\n  const getTabCount = (collectionUid) => tabs.filter((t) => t.collectionUid === collectionUid).length;\n\n  // Get tab count for workspace (scratch collection)\n  const workspaceTabCount = currentWorkspace?.scratchCollectionUid\n    ? getTabCount(currentWorkspace.scratchCollectionUid)\n    : 0;\n\n  // Display name and icon based on context\n  const displayName = isScratchCollection\n    ? (currentWorkspace?.name || 'Untitled Workspace')\n    : (collection.name || 'Untitled Collection');\n\n  const DisplayIcon = isScratchCollection ? IconCategory : IconBox;\n\n  // Switcher handlers\n  const handleSwitchToWorkspace = (workspaceUid) => {\n    switcherRef.current?.hide();\n    if (workspaceUid) {\n      dispatch(switchWorkspace(workspaceUid));\n    }\n  };\n\n  const handleSwitchToCollection = (targetCollection) => {\n    switcherRef.current?.hide();\n    if (!targetCollection?.uid) return;\n\n    const existingTab = tabs.find((t) => t.collectionUid === targetCollection.uid);\n    if (existingTab) {\n      dispatch(focusTab({ uid: existingTab.uid }));\n    } else {\n      dispatch(\n        addTab({\n          uid: targetCollection.uid,\n          collectionUid: targetCollection.uid,\n          type: 'collection-settings'\n        })\n      );\n    }\n  };\n\n  // Collection action handlers\n  const handleRun = () => {\n    dispatch(\n      addTab({\n        uid: uuid(),\n        collectionUid: collection.uid,\n        type: 'collection-runner'\n      })\n    );\n  };\n\n  const viewVariables = () => {\n    dispatch(\n      addTab({\n        uid: uuid(),\n        collectionUid: collection.uid,\n        type: 'variables'\n      })\n    );\n  };\n\n  const viewCollectionSettings = () => {\n    dispatch(\n      addTab({\n        uid: collection.uid,\n        collectionUid: collection.uid,\n        type: 'collection-settings'\n      })\n    );\n  };\n\n  const viewOpenApiSync = () => {\n    dispatch(addTab({\n      uid: uuid(),\n      collectionUid: collection.uid,\n      type: 'openapi-sync'\n    }));\n  };\n\n  // Build overflow menu items for the \"...\" dropdown\n  const overflowMenuItems = [\n    { id: 'variables', label: 'Variables', leftSection: IconEye, onClick: viewVariables },\n    ...(isOpenAPISyncEnabled && !hasOpenApiSyncConfigured\n      ? [{ id: 'openapi-sync', label: 'OpenAPI', leftSection: OpenAPISyncIcon, rightSection: <StatusBadge status=\"info\" size=\"xs\">Beta</StatusBadge>, onClick: viewOpenApiSync }]\n      : []),\n    { id: 'collection-settings', label: 'Collection Settings', leftSection: IconSettings, onClick: viewCollectionSettings }\n  ];\n\n  // Workspace action handlers (only used when isScratchCollection is true)\n  const handleRenameWorkspaceClick = () => {\n    workspaceActionsRef.current?.hide();\n    setIsRenamingWorkspace(true);\n    setWorkspaceNameInput(currentWorkspace?.name || '');\n    setWorkspaceNameError('');\n  };\n\n  const handleCloseWorkspaceClick = () => {\n    workspaceActionsRef.current?.hide();\n    if (currentWorkspace?.type === 'default') {\n      toast.error('Cannot close the default workspace');\n      return;\n    }\n    setCloseWorkspaceModalOpen(true);\n  };\n\n  const handleShowInFolder = () => {\n    workspaceActionsRef.current?.hide();\n    const pathname = currentWorkspace?.pathname;\n    if (pathname) {\n      dispatch(showInFolder(pathname)).catch(() => {\n        toast.error('Error opening the folder');\n      });\n    }\n  };\n\n  const handleExportWorkspace = () => {\n    workspaceActionsRef.current?.hide();\n    const uid = currentWorkspace?.uid;\n    if (!uid) return;\n\n    dispatch(exportWorkspaceAction(uid))\n      .then((result) => {\n        if (!result?.canceled) {\n          toast.success('Workspace exported successfully');\n        }\n      })\n      .catch((error) => {\n        toast.error(error?.message || 'Error exporting workspace');\n      });\n  };\n\n  const validateWorkspaceName = (name) => {\n    const trimmed = name?.trim();\n    if (!trimmed) {\n      return 'Name is required';\n    }\n    if (trimmed.length > 255) {\n      return 'Must be 255 characters or less';\n    }\n    return null;\n  };\n\n  const handleSaveWorkspaceRename = () => {\n    const fromOutside = clickedOutsideRef.current;\n    clickedOutsideRef.current = false;\n\n    if (openingAdvancedRef.current) return;\n    if (isSavingRef.current) return;\n\n    const trimmedName = workspaceNameInput?.trim();\n    if (!trimmedName) {\n      if (fromOutside && currentWorkspace?.isCreating) {\n        dispatch(cancelWorkspaceCreation(currentWorkspace.uid));\n        return;\n      }\n      setWorkspaceNameError('Name is required');\n      return;\n    }\n\n    const error = validateWorkspaceName(workspaceNameInput);\n    if (error) {\n      setWorkspaceNameError(error);\n      if (fromOutside && currentWorkspace?.isCreating) {\n        dispatch(cancelWorkspaceCreation(currentWorkspace.uid));\n      }\n      return;\n    }\n\n    const uid = currentWorkspace?.uid;\n    if (!uid) return;\n\n    isSavingRef.current = true;\n\n    if (currentWorkspace?.isCreating) {\n      dispatch(confirmWorkspaceCreation(uid, trimmedName))\n        .then(() => {\n          setIsRenamingWorkspace(false);\n          setWorkspaceNameInput('');\n          setWorkspaceNameError('');\n          toast.success('Workspace created!');\n        })\n        .catch((err) => {\n          toast.error(err?.message || 'An error occurred while creating the workspace');\n        })\n        .finally(() => {\n          isSavingRef.current = false;\n        });\n    } else {\n      dispatch(renameWorkspaceAction(uid, workspaceNameInput))\n        .then(() => {\n          toast.success('Workspace renamed!');\n          setIsRenamingWorkspace(false);\n          setWorkspaceNameInput('');\n          setWorkspaceNameError('');\n        })\n        .catch((err) => {\n          toast.error(err?.message || 'An error occurred while renaming the workspace');\n          setWorkspaceNameError(err?.message || 'Failed to rename workspace');\n        })\n        .finally(() => {\n          isSavingRef.current = false;\n        });\n    }\n  };\n\n  // Keep ref in sync so click-outside handler always has the latest save logic\n  handleSaveRef.current = handleSaveWorkspaceRename;\n\n  const handleWorkspaceNameChange = (e) => {\n    setWorkspaceNameInput(e.target.value);\n    if (workspaceNameError) {\n      setWorkspaceNameError('');\n    }\n  };\n\n  const handleWorkspaceNameKeyDown = (e) => {\n    if (e.key === 'Enter') {\n      e.preventDefault();\n      handleSaveWorkspaceRename();\n    } else if (e.key === 'Escape') {\n      e.preventDefault();\n      handleCancelWorkspaceRename();\n    }\n  };\n\n  const handleOpenAdvancedCreate = () => {\n    openingAdvancedRef.current = true;\n    tempWorkspaceUidRef.current = currentWorkspace?.isCreating ? currentWorkspace.uid : null;\n    setCreateWorkspaceModalOpen(true);\n  };\n\n  const handleAdvancedCreateClose = () => {\n    openingAdvancedRef.current = false;\n    setCreateWorkspaceModalOpen(false);\n    setIsRenamingWorkspace(false);\n    setWorkspaceNameInput('');\n    setWorkspaceNameError('');\n    const tempUid = tempWorkspaceUidRef.current;\n    tempWorkspaceUidRef.current = null;\n    // Clean up the temp workspace (cancelWorkspaceCreation only switches to default\n    // if the temp workspace was still active, so this is safe after modal success too)\n    if (tempUid) {\n      dispatch(cancelWorkspaceCreation(tempUid));\n    }\n  };\n\n  // Check if workspace actions should be shown\n  const showWorkspaceActions = isScratchCollection\n    && currentWorkspace\n    && currentWorkspace.type !== 'default'\n    && !isRenamingWorkspace;\n\n  return (\n    <StyledWrapper>\n      {closeWorkspaceModalOpen && currentWorkspace?.uid && (\n        <CloseWorkspace\n          workspaceUid={currentWorkspace.uid}\n          onClose={() => setCloseWorkspaceModalOpen(false)}\n        />\n      )}\n\n      {createWorkspaceModalOpen && (\n        <CreateWorkspace onClose={handleAdvancedCreateClose} />\n      )}\n\n      <div className=\"flex items-center justify-between gap-2 py-2 px-4\">\n        {/* Left side: Switcher dropdown or rename input */}\n        <div className=\"collection-switcher\">\n          {isRenamingWorkspace ? (\n            <div className=\"workspace-rename-container\" ref={workspaceRenameContainerRef}>\n              <DisplayIcon size={18} strokeWidth={1.5} />\n              <div className=\"workspace-input-wrapper\">\n                <input\n                  ref={workspaceNameInputRef}\n                  type=\"text\"\n                  className=\"workspace-name-input\"\n                  value={workspaceNameInput}\n                  onChange={handleWorkspaceNameChange}\n                  onKeyDown={handleWorkspaceNameKeyDown}\n                  autoComplete=\"off\"\n                  autoCorrect=\"off\"\n                  autoCapitalize=\"off\"\n                  spellCheck=\"false\"\n                />\n                {currentWorkspace?.isCreating && (\n                  <button\n                    className=\"cog-btn\"\n                    onMouseDown={(e) => e.preventDefault()}\n                    onClick={handleOpenAdvancedCreate}\n                    title=\"Advanced options\"\n                  >\n                    <IconSettings size={13} strokeWidth={1.5} />\n                  </button>\n                )}\n              </div>\n              <div className=\"inline-actions\">\n                <button\n                  className=\"inline-action-btn save\"\n                  onClick={handleSaveWorkspaceRename}\n                  onMouseDown={(e) => e.preventDefault()}\n                  title={currentWorkspace?.isCreating ? 'Create' : 'Save'}\n                >\n                  <IconCheck size={14} strokeWidth={2} />\n                </button>\n                <button\n                  className=\"inline-action-btn cancel\"\n                  onClick={handleCancelWorkspaceRename}\n                  onMouseDown={(e) => e.preventDefault()}\n                  title=\"Cancel\"\n                >\n                  <IconX size={14} strokeWidth={2} />\n                </button>\n              </div>\n              {workspaceNameError && (\n                <span className=\"workspace-error\">{workspaceNameError}</span>\n              )}\n            </div>\n          ) : (\n            <Dropdown\n              placement=\"bottom-start\"\n              onCreate={onSwitcherCreate}\n              appendTo={() => document.body}\n              icon={(\n                <button className=\"switcher-trigger\">\n                  <DisplayIcon size={18} strokeWidth={1.5} />\n                  <span className={classNames('switcher-name', { 'scratch-collection': isScratchCollection })}>{displayName}</span>\n                  <IconChevronDown size={14} strokeWidth={1.5} className=\"chevron\" />\n                </button>\n              )}\n            >\n              {/* Workspace section */}\n              {currentWorkspace && (\n                <>\n                  <div className=\"label-item\">Workspace</div>\n                  <div\n                    className={classNames('dropdown-item', {\n                      'dropdown-item-active': isScratchCollection\n                    })}\n                    onClick={() => handleSwitchToWorkspace(currentWorkspace.uid)}\n                  >\n                    <div className=\"dropdown-icon\">\n                      <IconCategory size={16} strokeWidth={1.5} />\n                    </div>\n                    <span className=\"dropdown-label\">\n                      {currentWorkspace.name || 'Untitled Workspace'}\n                    </span>\n                    {workspaceTabCount > 0 && (\n                      <span className=\"dropdown-tab-count\">{workspaceTabCount}</span>\n                    )}\n                  </div>\n                </>\n              )}\n\n              {/* Collections section */}\n              {mountedCollections.length > 0 && (\n                <>\n                  <div className=\"dropdown-separator\" />\n                  <div className=\"label-item\">Collections</div>\n                  {mountedCollections.map((col) => {\n                    const colTabCount = getTabCount(col.uid);\n                    return (\n                      <div\n                        key={col.uid}\n                        className={classNames('dropdown-item', {\n                          'dropdown-item-active': !isScratchCollection && collection.uid === col.uid\n                        })}\n                        onClick={() => handleSwitchToCollection(col)}\n                      >\n                        <div className=\"dropdown-icon\">\n                          <IconBox size={16} strokeWidth={1.5} />\n                        </div>\n                        <span className=\"dropdown-label\">{col.name || 'Untitled Collection'}</span>\n                        {colTabCount > 0 && (\n                          <span className=\"dropdown-tab-count\">{colTabCount}</span>\n                        )}\n                      </div>\n                    );\n                  })}\n                </>\n              )}\n            </Dropdown>\n          )}\n\n          {/* Workspace actions dropdown */}\n          {showWorkspaceActions && (\n            <Dropdown\n              placement=\"bottom-start\"\n              onCreate={onWorkspaceActionsCreate}\n              appendTo={() => document.body}\n              icon={<IconDots size={18} strokeWidth={1.5} className=\"workspace-actions-trigger\" />}\n            >\n              <div className=\"dropdown-item\" onClick={handleRenameWorkspaceClick}>\n                <div className=\"dropdown-icon\">\n                  <IconEdit size={16} strokeWidth={1.5} />\n                </div>\n                <span>Rename</span>\n              </div>\n              <div className=\"dropdown-item\" onClick={handleShowInFolder}>\n                <div className=\"dropdown-icon\">\n                  <IconFolder size={16} strokeWidth={1.5} />\n                </div>\n                <span>{getRevealInFolderLabel()}</span>\n              </div>\n              <div className=\"dropdown-item\" onClick={handleExportWorkspace}>\n                <div className=\"dropdown-icon\">\n                  <IconUpload size={16} strokeWidth={1.5} />\n                </div>\n                <span>Export</span>\n              </div>\n              <div className=\"dropdown-item\" onClick={handleCloseWorkspaceClick}>\n                <div className=\"dropdown-icon\">\n                  <IconX size={16} strokeWidth={1.5} />\n                </div>\n                <span>Close</span>\n              </div>\n            </Dropdown>\n          )}\n        </div>\n\n        {/* Right side: Actions (only for regular collections) */}\n        {!isScratchCollection && (\n          <div className=\"flex flex-grow gap-1.5 items-center justify-end\">\n            {/* OpenAPI Sync - standalone only when configured and beta enabled */}\n            {isOpenAPISyncEnabled && hasOpenApiSyncConfigured && (\n              <ToolHint\n                text={hasOpenApiError ? 'OpenAPI Error' : hasOpenApiUpdates ? 'OpenAPI Updates Available' : 'OpenAPI'}\n                toolhintId=\"OpenApiSyncToolhintId\"\n                place=\"bottom\"\n              >\n                <ActionIcon onClick={viewOpenApiSync} aria-label=\"OpenAPI\" size=\"sm\" className=\"relative\">\n                  <OpenAPISyncIcon size={15} />\n                  {(hasOpenApiUpdates || hasOpenApiError) && (\n                    <span className=\"absolute top-0 right-0 w-1.5 h-1.5 rounded-full\" style={{ backgroundColor: hasOpenApiError ? theme.status.danger.text : theme.status.warning.text }} />\n                  )}\n                </ActionIcon>\n              </ToolHint>\n            )}\n            {/* Runner - always visible */}\n            <ToolHint text=\"Runner\" toolhintId=\"RunnerToolhintId\" place=\"bottom\">\n              <ActionIcon onClick={handleRun} aria-label=\"Runner\" size=\"sm\">\n                <IconRun size={16} strokeWidth={1.5} />\n              </ActionIcon>\n            </ToolHint>\n            {/* JS Sandbox Mode - always visible */}\n            <JsSandboxMode collection={collection} />\n            {/* Overflow menu */}\n            <MenuDropdown items={overflowMenuItems} placement=\"bottom-end\">\n              <ActionIcon label=\"More actions\" size=\"sm\" style={{ border: `1px solid ${theme.border.border1}`, borderRadius: theme.border.radius.base, width: 24, marginRight: 4, marginLeft: 4 }}>\n                <IconDots size={16} strokeWidth={1.5} />\n              </ActionIcon>\n            </MenuDropdown>\n            {/* Environment Selector - always visible */}\n            <span>\n              <EnvironmentSelector collection={collection} />\n            </span>\n          </div>\n        )}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default CollectionHeader;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabs/DraggableTab.js",
    "content": "import React from 'react';\nimport { useDrag, useDrop } from 'react-dnd';\n\nconst DraggableTab = ({ id, onMoveTab, index, children, className, onClick }) => {\n  const ref = React.useRef(null);\n\n  const [{ handlerId, isOver }, drop] = useDrop({\n    accept: 'tab',\n    hover(item, monitor) {\n      onMoveTab(item.id, id);\n    },\n    collect: (monitor) => ({\n      handlerId: monitor.getHandlerId(),\n      isOver: monitor.isOver()\n    })\n  });\n\n  const [{ isDragging }, drag] = useDrag({\n    type: 'tab',\n    item: () => {\n      return { id, index };\n    },\n    collect: (monitor) => ({\n      isDragging: monitor.isDragging()\n    }),\n    options: {\n      dropEffect: 'move'\n    }\n  });\n\n  drag(drop(ref));\n\n  return (\n    <li\n      className={className}\n      ref={ref}\n      role=\"tab\"\n      style={{ opacity: isDragging || isOver ? 0 : 1 }}\n      onClick={onClick}\n      data-handler-id={handlerId}\n    >\n      {children}\n    </li>\n  );\n};\n\nexport default DraggableTab;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabs/ExampleTab/index.js",
    "content": "import React, { useState, useRef, useMemo } from 'react';\nimport { useDispatch } from 'react-redux';\nimport { makeTabPermanent } from 'providers/ReduxStore/slices/tabs';\nimport { deleteRequestDraft } from 'providers/ReduxStore/slices/collections';\nimport { saveRequest, closeTabs } from 'providers/ReduxStore/slices/collections/actions';\nimport { hasExampleChanges, findItemInCollection } from 'utils/collections';\nimport ExampleIcon from 'components/Icons/ExampleIcon';\nimport ConfirmRequestClose from '../RequestTab/ConfirmRequestClose';\nimport RequestTabNotFound from '../RequestTab/RequestTabNotFound';\nimport StyledWrapper from '../RequestTab/StyledWrapper';\nimport GradientCloseButton from '../RequestTab/GradientCloseButton';\n\nconst ExampleTab = ({ tab, collection }) => {\n  const dispatch = useDispatch();\n  const [showConfirmClose, setShowConfirmClose] = useState(false);\n\n  const dropdownTippyRef = useRef();\n\n  // Get item and example data\n  const item = findItemInCollection(collection, tab.itemUid);\n  const example = useMemo(() => item?.examples?.find((ex) => ex.uid === tab.uid), [item?.examples, tab.uid]);\n\n  const hasChanges = useMemo(() => hasExampleChanges(item, tab.uid), [item, tab.uid]);\n\n  const handleCloseClick = (event) => {\n    event.stopPropagation();\n    event.preventDefault();\n    dispatch(closeTabs({\n      tabUids: [tab.uid]\n    }));\n  };\n\n  const handleRightClick = (_event) => {\n    const menuDropdown = dropdownTippyRef.current;\n    if (!menuDropdown) {\n      return;\n    }\n\n    if (menuDropdown.state.isShown) {\n      menuDropdown.hide();\n    } else {\n      menuDropdown.show();\n    }\n  };\n\n  const handleMouseUp = (e) => {\n    if (e.button === 1) {\n      e.preventDefault();\n      e.stopPropagation();\n\n      // Close the tab\n      dispatch(closeTabs({\n        tabUids: [tab.uid]\n      }));\n    }\n  };\n\n  if (!item || !example) {\n    return (\n      <StyledWrapper\n        className=\"flex items-center justify-between tab-container\"\n        onMouseUp={(e) => {\n          if (e.button === 1) {\n            e.preventDefault();\n            e.stopPropagation();\n\n            dispatch(closeTabs({ tabUids: [tab.uid] }));\n          }\n        }}\n      >\n        <RequestTabNotFound handleCloseClick={handleCloseClick} />\n      </StyledWrapper>\n    );\n  }\n\n  return (\n    <StyledWrapper className=\"flex items-center justify-between tab-container px-2\">\n      {showConfirmClose && (\n        <ConfirmRequestClose\n          item={item}\n          example={example}\n          onCancel={() => setShowConfirmClose(false)}\n          onCloseWithoutSave={() => {\n            dispatch(deleteRequestDraft({\n              itemUid: item.uid,\n              collectionUid: collection.uid\n            }));\n            dispatch(closeTabs({\n              tabUids: [tab.uid]\n            }));\n            setShowConfirmClose(false);\n          }}\n          onSaveAndClose={() => {\n            // For examples, we don't have a separate save action\n            // The changes are saved automatically when the request is saved\n            dispatch(saveRequest(item.uid, collection.uid, true));\n            dispatch(closeTabs({\n              tabUids: [tab.uid]\n            }));\n            setShowConfirmClose(false);\n          }}\n        />\n      )}\n      <div\n        className={`flex items-center tab-label ${tab.preview ? 'italic' : ''}`}\n        onContextMenu={handleRightClick}\n        onDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))}\n        onMouseUp={(e) => {\n          if (!hasChanges) return handleMouseUp(e);\n\n          if (e.button === 1) {\n            e.stopPropagation();\n            e.preventDefault();\n            setShowConfirmClose(true);\n          }\n        }}\n      >\n        <ExampleIcon size={14} color=\"currentColor\" className=\"example-icon flex-shrink-0\" />\n        <span className=\"tab-name ml-1\" title={example.name}>\n          {example.name}\n        </span>\n      </div>\n      <GradientCloseButton\n        hasChanges={hasChanges}\n        onClick={(e) => {\n          if (!hasChanges) {\n            return handleCloseClick(e);\n          }\n\n          e.stopPropagation();\n          e.preventDefault();\n          setShowConfirmClose(true);\n        }}\n      />\n    </StyledWrapper>\n  );\n};\n\nexport default ExampleTab;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabs/RequestTab/CloseTabIcon.js",
    "content": "const CloseTabIcon = () => (\n  <svg focusable=\"false\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 320 512\" className=\"close-icon\">\n    <path\n      fill=\"currentColor\"\n      d=\"M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z\"\n    >\n    </path>\n  </svg>\n);\n\nexport default CloseTabIcon;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabs/RequestTab/ConfirmCollectionClose/index.js",
    "content": "import React from 'react';\nimport { IconAlertTriangle } from '@tabler/icons';\nimport Modal from 'components/Modal';\nimport Button from 'ui/Button';\n\nconst ConfirmCollectionClose = ({ collection, onCancel, onCloseWithoutSave, onSaveAndClose }) => {\n  return (\n    <Modal\n      size=\"md\"\n      title=\"Unsaved changes\"\n      confirmText=\"Save and Close\"\n      cancelText=\"Close without saving\"\n      disableEscapeKey={true}\n      disableCloseOnOutsideClick={true}\n      closeModalFadeTimeout={150}\n      handleCancel={onCancel}\n      onClick={(e) => {\n        e.stopPropagation();\n        e.preventDefault();\n      }}\n      hideFooter={true}\n    >\n      <div className=\"flex items-center font-normal\">\n        <IconAlertTriangle size={32} strokeWidth={1.5} className=\"text-yellow-600\" />\n        <h1 className=\"ml-2 text-lg font-medium\">Hold on..</h1>\n      </div>\n      <div className=\"font-normal mt-4\">\n        You have unsaved changes in <span className=\"font-medium\">{collection.name}</span> collection settings.\n      </div>\n\n      <div className=\"flex justify-between mt-6\">\n        <div>\n          <Button color=\"danger\" onClick={onCloseWithoutSave}>\n            Don't Save\n          </Button>\n        </div>\n        <div className=\"flex gap-2\">\n          <Button size=\"sm\" color=\"secondary\" variant=\"ghost\" onClick={onCancel}>\n            Cancel\n          </Button>\n          <Button onClick={onSaveAndClose}>\n            Save\n          </Button>\n        </div>\n      </div>\n    </Modal>\n  );\n};\n\nexport default ConfirmCollectionClose;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabs/RequestTab/ConfirmFolderClose/index.js",
    "content": "import React from 'react';\nimport { IconAlertTriangle } from '@tabler/icons';\nimport Modal from 'components/Modal';\nimport Button from 'ui/Button';\n\nconst ConfirmFolderClose = ({ folder, onCancel, onCloseWithoutSave, onSaveAndClose }) => {\n  return (\n    <Modal\n      size=\"md\"\n      title=\"Unsaved changes\"\n      confirmText=\"Save and Close\"\n      cancelText=\"Close without saving\"\n      disableEscapeKey={true}\n      disableCloseOnOutsideClick={true}\n      closeModalFadeTimeout={150}\n      handleCancel={onCancel}\n      onClick={(e) => {\n        e.stopPropagation();\n        e.preventDefault();\n      }}\n      hideFooter={true}\n    >\n      <div className=\"flex items-center font-normal\">\n        <IconAlertTriangle size={32} strokeWidth={1.5} className=\"text-yellow-600\" />\n        <h1 className=\"ml-2 text-lg font-medium\">Hold on..</h1>\n      </div>\n      <div className=\"font-normal mt-4\">\n        You have unsaved changes in <span className=\"font-medium\">{folder.name}</span> folder settings.\n      </div>\n\n      <div className=\"flex justify-between mt-6\">\n        <div>\n          <Button color=\"danger\" onClick={onCloseWithoutSave}>\n            Don't Save\n          </Button>\n        </div>\n        <div className=\"flex gap-2\">\n          <Button size=\"sm\" color=\"secondary\" variant=\"ghost\" onClick={onCancel}>\n            Cancel\n          </Button>\n          <Button onClick={onSaveAndClose}>\n            Save\n          </Button>\n        </div>\n      </div>\n    </Modal>\n  );\n};\n\nexport default ConfirmFolderClose;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabs/RequestTab/ConfirmRequestClose/index.js",
    "content": "import React from 'react';\nimport { IconAlertTriangle } from '@tabler/icons';\nimport Modal from 'components/Modal';\nimport Button from 'ui/Button';\n\nconst ConfirmRequestClose = ({ item, example, onCancel, onCloseWithoutSave, onSaveAndClose }) => {\n  const isExample = !!example;\n  const itemName = isExample ? example.name : item.name;\n  const itemType = isExample ? 'example' : 'request';\n\n  return (\n    <Modal\n      size=\"md\"\n      title=\"Unsaved changes\"\n      confirmText=\"Save and Close\"\n      cancelText=\"Close without saving\"\n      disableEscapeKey={true}\n      disableCloseOnOutsideClick={true}\n      closeModalFadeTimeout={150}\n      handleCancel={onCancel}\n      onClick={(e) => {\n        e.stopPropagation();\n        e.preventDefault();\n      }}\n      hideFooter={true}\n    >\n      <div className=\"flex items-center font-normal\">\n        <IconAlertTriangle size={32} strokeWidth={1.5} className=\"text-yellow-600\" />\n        <h1 className=\"ml-2 text-lg font-medium\">Hold on..</h1>\n      </div>\n      <div className=\"font-normal mt-4\">\n        You have unsaved changes in {itemType} <span className=\"font-medium\">{itemName}</span>.\n      </div>\n\n      <div className=\"flex justify-between mt-6\">\n        <div>\n          <Button color=\"danger\" onClick={onCloseWithoutSave}>\n            Don't Save\n          </Button>\n        </div>\n        <div className=\"flex gap-2\">\n          <Button size=\"sm\" color=\"secondary\" variant=\"ghost\" onClick={onCancel}>\n            Cancel\n          </Button>\n          <Button onClick={onSaveAndClose}>Save</Button>\n        </div>\n      </div>\n    </Modal>\n  );\n};\n\nexport default ConfirmRequestClose;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabs/RequestTab/DraftTabIcon.js",
    "content": "import { useTheme } from 'providers/Theme';\n\nconst DraftTabIcon = () => {\n  const { theme } = useTheme();\n\n  return (\n    <svg\n      focusable=\"false\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"8\"\n      height=\"16\"\n      fill={theme.draftColor}\n      className=\"has-changes-icon\"\n      viewBox=\"0 0 8 8\"\n    >\n      <circle cx=\"4\" cy=\"4\" r=\"3\" />\n    </svg>\n  );\n};\n\nexport default DraftTabIcon;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabs/RequestTab/GradientCloseButton/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div.attrs((props) => ({\n  style: {\n    '--gradient-color': props.theme.requestTabs.bg,\n    '--gradient-color-active': props.theme.bg\n  }\n}))`\n  display: flex;\n  align-items: center;\n  justify-content: flex-end;\n  position: absolute;\n  width: 44px;\n  height: 100%;\n  right: 0;\n  top: 0;\n  padding-right: 4px;\n  \n  background-image: linear-gradient(\n    90deg,\n    transparent 0%,\n    var(--gradient-color) 40%\n  );\n  \n  opacity: 0;\n  pointer-events: none;\n  transition: opacity 0.15s ease;\n\n  li.active & {\n    background-image: linear-gradient(\n      90deg,\n      transparent 0%,\n      var(--gradient-color-active) 40%\n    );\n  }\n\n  li:hover &,\n  &.has-changes {\n    opacity: 1;\n    pointer-events: auto;\n  }\n\n  .close-icon-container {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    width: 22px;\n    height: 22px;\n    border-radius: ${(props) => props.theme.border.radius.base};\n    cursor: pointer;\n    transition: background-color 0.12s ease;\n\n    &:hover {\n      background-color: ${(props) => props.theme.requestTabs.icon.hoverBg};\n\n      .close-icon {\n        color: ${(props) => props.theme.requestTabs.icon.hoverColor};\n      }\n    }\n  }\n\n  .close-icon {\n    color: ${(props) => props.theme.requestTabs.icon.color};\n    width: 12px;\n    height: 12px;\n    transition: color 0.12s ease;\n  }\n\n  .has-changes-icon {\n    width: 8px;\n    height: 8px;\n  }\n\n  .draft-icon-wrapper { \n    display: none; \n  }\n  \n  .close-icon-wrapper { \n    display: flex;\n    align-items: center;\n    justify-content: center;\n  }\n\n  &.has-changes:not(li:hover &) {\n    .draft-icon-wrapper { \n      display: flex;\n      align-items: center;\n      justify-content: center;\n    }\n    .close-icon-wrapper { \n      display: none; \n    }\n  }\n\n  li:hover &.has-changes {\n    .draft-icon-wrapper { \n      display: none; \n    }\n    .close-icon-wrapper { \n      display: flex; \n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabs/RequestTab/GradientCloseButton/index.js",
    "content": "import React from 'react';\nimport CloseTabIcon from '../CloseTabIcon';\nimport DraftTabIcon from '../DraftTabIcon';\nimport StyledWrapper from './StyledWrapper';\n\nconst GradientCloseButton = ({ onClick, hasChanges = false }) => {\n  return (\n    <StyledWrapper className={`close-gradient ${hasChanges ? 'has-changes' : ''}`}>\n      <div className=\"close-icon-container\" onClick={onClick} data-testid=\"request-tab-close-icon\">\n        <span className=\"draft-icon-wrapper\">\n          <DraftTabIcon />\n        </span>\n        <span className=\"close-icon-wrapper\">\n          <CloseTabIcon />\n        </span>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default GradientCloseButton;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabs/RequestTab/RequestTabNotFound.js",
    "content": "import React, { useState, useEffect } from 'react';\nimport { IconAlertTriangle } from '@tabler/icons';\nimport GradientCloseButton from './GradientCloseButton';\n\nconst RequestTabNotFound = ({ handleCloseClick }) => {\n  const [showErrorMessage, setShowErrorMessage] = useState(false);\n\n  // add a delay component in react that shows a loading spinner\n  // and then shows the error message after a delay\n  // this will prevent the error message from flashing on the screen\n  useEffect(() => {\n    setTimeout(() => {\n      setShowErrorMessage(true);\n    }, 300);\n  }, []);\n\n  if (!showErrorMessage) {\n    return null;\n  }\n\n  return (\n    <>\n      <div className=\"flex items-center tab-label px-3\">\n        {showErrorMessage ? (\n          <>\n            <IconAlertTriangle size={18} strokeWidth={1.5} className=\"text-yellow-600\" />\n            <span className=\"ml-1\">Not Found</span>\n          </>\n        ) : null}\n      </div>\n      <GradientCloseButton onClick={handleCloseClick} hasChanges={true} />\n    </>\n  );\n};\n\nexport default RequestTabNotFound;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabs/RequestTab/SpecialTab.js",
    "content": "import React from 'react';\nimport GradientCloseButton from './GradientCloseButton';\nimport { IconVariable, IconSettings, IconRun, IconFolder, IconDatabase, IconWorld, IconHome, IconFileCode } from '@tabler/icons';\nimport OpenAPISyncIcon from 'components/Icons/OpenAPISync';\nimport StatusBadge from 'ui/StatusBadge/index';\n\nconst SpecialTab = ({ handleCloseClick, type, tabName, handleDoubleClick, hasDraft }) => {\n  const getTabInfo = (type, tabName) => {\n    switch (type) {\n      case 'collection-settings': {\n        return (\n          <>\n            <IconSettings size={14} strokeWidth={1.5} className=\"special-tab-icon flex-shrink-0\" />\n            <span className=\"ml-1 tab-name\">Collection</span>\n          </>\n        );\n      }\n      case 'collection-overview': {\n        return (\n          <>\n            <IconSettings size={14} strokeWidth={1.5} className=\"special-tab-icon flex-shrink-0\" />\n            <span className=\"ml-1 tab-name\">Overview</span>\n          </>\n        );\n      }\n      case 'folder-settings': {\n        return (\n          <>\n            <IconFolder size={14} strokeWidth={1.5} className=\"special-tab-icon flex-shrink-0\" />\n            <span className=\"ml-1 tab-name\">{tabName || 'Folder'}</span>\n          </>\n        );\n      }\n      case 'variables': {\n        return (\n          <>\n            <IconVariable size={14} strokeWidth={1.5} className=\"special-tab-icon flex-shrink-0\" />\n            <span className=\"ml-1 tab-name\">Variables</span>\n          </>\n        );\n      }\n      case 'collection-runner': {\n        return (\n          <>\n            <IconRun size={14} strokeWidth={1.5} className=\"special-tab-icon flex-shrink-0\" />\n            <span className=\"ml-1 tab-name\">Runner</span>\n          </>\n        );\n      }\n      case 'environment-settings': {\n        return (\n          <>\n            <IconDatabase size={14} strokeWidth={1.5} className=\"special-tab-icon flex-shrink-0\" />\n            <span className=\"ml-1 tab-name\">Environments</span>\n          </>\n        );\n      }\n      case 'global-environment-settings': {\n        return (\n          <>\n            <IconWorld size={14} strokeWidth={1.5} className=\"special-tab-icon flex-shrink-0\" />\n            <span className=\"ml-1 tab-name\">Global Environments</span>\n          </>\n        );\n      }\n      case 'preferences': {\n        return (\n          <>\n            <IconSettings size={14} strokeWidth={1.5} className=\"special-tab-icon flex-shrink-0\" />\n            <span className=\"ml-1 tab-name\">Preferences</span>\n          </>\n        );\n      }\n      case 'workspaceOverview': {\n        return (\n          <>\n            <IconHome size={14} strokeWidth={1.5} className=\"special-tab-icon flex-shrink-0\" />\n            <span className=\"ml-1 tab-name\">Overview</span>\n          </>\n        );\n      }\n      case 'workspaceEnvironments': {\n        return (\n          <>\n            <IconWorld size={14} strokeWidth={1.5} className=\"special-tab-icon flex-shrink-0\" />\n            <span className=\"ml-1 tab-name\">Environments</span>\n          </>\n        );\n      }\n      case 'openapi-sync': {\n        return (\n          <>\n            <OpenAPISyncIcon size={14} className=\"special-tab-icon flex-shrink-0\" />\n            <span className=\"ml-1 tab-name mr-1\">OpenAPI</span>\n            <StatusBadge status=\"info\" size=\"xs\">Beta</StatusBadge>\n          </>\n        );\n      }\n      case 'openapi-spec': {\n        return (\n          <>\n            <IconFileCode size={14} strokeWidth={1.5} className=\"special-tab-icon flex-shrink-0\" />\n            <span className=\"ml-1 tab-name\">API Spec</span>\n          </>\n        );\n      }\n    }\n  };\n\n  return (\n    <>\n      <div\n        className=\"flex items-center tab-label\"\n        onDoubleClick={handleDoubleClick}\n      >\n        {getTabInfo(type, tabName)}\n      </div>\n      {handleCloseClick && <GradientCloseButton hasChanges={hasDraft} onClick={(e) => handleCloseClick(e)} />}\n    </>\n  );\n};\n\nexport default SpecialTab;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabs/RequestTab/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  position: relative;\n  width: 100%;\n  height: 100%;\n\n  .tab-label {\n    overflow: hidden;\n    align-items: center;\n    position: relative;\n    flex: 1;\n    min-width: 0;\n  }\n\n  .tab-method {\n    font-size: 0.6875rem;\n    letter-spacing: 0.02em;\n    flex-shrink: 0;\n  }\n\n  .tab-name {\n    position: relative;\n    overflow: hidden;\n    white-space: nowrap;\n    font-size: 0.8125rem;\n\n    // so that the name does not cutoff when italicized\n    padding-right: 2px;\n  }\n\n  .example-icon {\n    color: ${(props) => props.theme.requestTabs.example.iconColor};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabs/RequestTab/index.js",
    "content": "import React, { useCallback, useState, useRef, Fragment, useMemo, useEffect } from 'react';\nimport get from 'lodash/get';\nimport { makeTabPermanent } from 'providers/ReduxStore/slices/tabs';\nimport { saveRequest, saveCollectionRoot, saveFolderRoot, saveEnvironment, closeTabs } from 'providers/ReduxStore/slices/collections/actions';\nimport { deleteRequestDraft, deleteCollectionDraft, deleteFolderDraft, clearEnvironmentsDraft } from 'providers/ReduxStore/slices/collections';\nimport { clearGlobalEnvironmentDraft } from 'providers/ReduxStore/slices/global-environments';\nimport { saveGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';\nimport { useTheme } from 'providers/Theme';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { findItemInCollection, hasRequestChanges } from 'utils/collections';\nimport ConfirmRequestClose from './ConfirmRequestClose';\nimport ConfirmCollectionClose from './ConfirmCollectionClose';\nimport ConfirmFolderClose from './ConfirmFolderClose';\nimport ConfirmCloseEnvironment from 'components/Environments/ConfirmCloseEnvironment';\nimport RequestTabNotFound from './RequestTabNotFound';\nimport SpecialTab from './SpecialTab';\nimport StyledWrapper from './StyledWrapper';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport CloneCollectionItem from 'components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index';\nimport NewRequest from 'components/Sidebar/NewRequest/index';\nimport GradientCloseButton from './GradientCloseButton';\nimport { flattenItems } from 'utils/collections/index';\nimport { closeWsConnection } from 'utils/network/index';\nimport ExampleTab from '../ExampleTab';\nimport toast from 'react-hot-toast';\n\nconst RequestTab = ({ tab, collection, tabIndex, collectionRequestTabs, folderUid, hasOverflow, setHasOverflow, dropdownContainerRef }) => {\n  const dispatch = useDispatch();\n  const { theme } = useTheme();\n  const tabNameRef = useRef(null);\n  const tabLabelRef = useRef(null);\n  const lastOverflowStateRef = useRef(null);\n  const [showConfirmClose, setShowConfirmClose] = useState(false);\n  const [showConfirmCollectionClose, setShowConfirmCollectionClose] = useState(false);\n  const [showConfirmFolderClose, setShowConfirmFolderClose] = useState(false);\n  const [showConfirmEnvironmentClose, setShowConfirmEnvironmentClose] = useState(false);\n  const [showConfirmGlobalEnvironmentClose, setShowConfirmGlobalEnvironmentClose] = useState(false);\n\n  const menuDropdownRef = useRef();\n\n  const item = findItemInCollection(collection, tab.uid);\n\n  const method = useMemo(() => {\n    if (!item) return;\n    switch (item.type) {\n      case 'grpc-request':\n        return 'gRPC';\n      case 'ws-request':\n        return 'WS';\n      case 'graphql-request':\n        return 'GQL';\n      default:\n        return item.draft ? get(item, 'draft.request.method') : get(item, 'request.method');\n    }\n  }, [item]);\n\n  const hasChanges = useMemo(() => hasRequestChanges(item), [item]);\n\n  const isWS = item?.type === 'ws-request';\n\n  useEffect(() => {\n    if (!item || !tabNameRef.current || !setHasOverflow) return;\n\n    const checkOverflow = () => {\n      if (tabNameRef.current && setHasOverflow) {\n        const hasOverflow = tabNameRef.current.scrollWidth > tabNameRef.current.clientWidth;\n        if (lastOverflowStateRef.current !== hasOverflow) {\n          lastOverflowStateRef.current = hasOverflow;\n          setHasOverflow(hasOverflow);\n        }\n      }\n    };\n\n    const timeoutId = setTimeout(checkOverflow, 0);\n    const resizeObserver = new ResizeObserver(() => {\n      checkOverflow();\n    });\n\n    if (tabNameRef.current) {\n      resizeObserver.observe(tabNameRef.current);\n    }\n\n    return () => {\n      clearTimeout(timeoutId);\n      resizeObserver.disconnect();\n    };\n  }, [item, item?.name, method, setHasOverflow]);\n\n  const handleCloseClick = (event) => {\n    event.stopPropagation();\n    event.preventDefault();\n    dispatch(\n      closeTabs({\n        tabUids: [tab.uid]\n      })\n    );\n  };\n\n  const handleRightClick = (event) => {\n    event.preventDefault();\n    event.stopPropagation();\n    menuDropdownRef.current?.show();\n  };\n\n  const handleMouseUp = (e) => {\n    if (e.button === 1) {\n      e.preventDefault();\n      e.stopPropagation();\n\n      // Close the tab\n      dispatch(\n        closeTabs({\n          tabUids: [tab.uid]\n        })\n      );\n    }\n  };\n\n  const getMethodColor = (method = '') => {\n    const colorMap = {\n      ...theme.request.methods,\n      ...theme.request\n    };\n    return colorMap[method.toLocaleLowerCase()];\n  };\n\n  const handleCloseCollectionSettings = (event) => {\n    if (!collection.draft) {\n      return handleCloseClick(event);\n    }\n\n    event.stopPropagation();\n    event.preventDefault();\n    setShowConfirmCollectionClose(true);\n  };\n\n  const folder = folderUid ? findItemInCollection(collection, folderUid) : null;\n\n  const handleCloseFolderSettings = (event) => {\n    if (!folder?.draft) {\n      return handleCloseClick(event);\n    }\n\n    event.stopPropagation();\n    event.preventDefault();\n    setShowConfirmFolderClose(true);\n  };\n\n  const specialTabs = [\n    'collection-overview',\n    'collection-settings',\n    'folder-settings',\n    'variables',\n    'collection-runner',\n    'environment-settings',\n    'global-environment-settings',\n    'preferences',\n    'workspaceOverview',\n    'workspaceEnvironments',\n    'openapi-sync',\n    'openapi-spec'\n  ];\n\n  const hasDraft = tab.type === 'collection-settings' && collection?.draft;\n  const hasFolderDraft = tab.type === 'folder-settings' && folder?.draft;\n  const hasEnvironmentDraft = tab.type === 'environment-settings' && collection?.environmentsDraft;\n  const globalEnvironmentDraft = useSelector((state) => state.globalEnvironments.globalEnvironmentDraft);\n  const hasGlobalEnvironmentDraft = tab.type === 'global-environment-settings' && globalEnvironmentDraft;\n\n  const handleCloseEnvironmentSettings = (event) => {\n    if (!collection?.environmentsDraft) {\n      return handleCloseClick(event);\n    }\n\n    event.stopPropagation();\n    event.preventDefault();\n    setShowConfirmEnvironmentClose(true);\n  };\n\n  const handleCloseGlobalEnvironmentSettings = (event) => {\n    if (!globalEnvironmentDraft) {\n      return handleCloseClick(event);\n    }\n\n    event.stopPropagation();\n    event.preventDefault();\n    setShowConfirmGlobalEnvironmentClose(true);\n  };\n\n  if (specialTabs.includes(tab.type)) {\n    return (\n      <StyledWrapper\n        className={`flex items-center justify-between tab-container px-2 ${tab.preview ? 'italic' : ''}`}\n        onMouseUp={handleMouseUp}\n      >\n        {showConfirmCollectionClose && tab.type === 'collection-settings' && (\n          <ConfirmCollectionClose\n            collection={collection}\n            onCancel={() => setShowConfirmCollectionClose(false)}\n            onCloseWithoutSave={() => {\n              dispatch(deleteCollectionDraft({\n                collectionUid: collection.uid\n              }));\n              dispatch(closeTabs({\n                tabUids: [tab.uid]\n              }));\n              setShowConfirmCollectionClose(false);\n            }}\n            onSaveAndClose={() => {\n              dispatch(saveCollectionRoot(collection.uid))\n                .then(() => {\n                  dispatch(closeTabs({\n                    tabUids: [tab.uid]\n                  }));\n                  setShowConfirmCollectionClose(false);\n                })\n                .catch((err) => {\n                  console.log('err', err);\n                });\n            }}\n          />\n        )}\n        {showConfirmFolderClose && tab.type === 'folder-settings' && (\n          <ConfirmFolderClose\n            folder={folder}\n            onCancel={() => setShowConfirmFolderClose(false)}\n            onCloseWithoutSave={() => {\n              dispatch(deleteFolderDraft({\n                collectionUid: collection.uid,\n                folderUid: folder.uid\n              }));\n              dispatch(closeTabs({\n                tabUids: [tab.uid]\n              }));\n              setShowConfirmFolderClose(false);\n            }}\n            onSaveAndClose={() => {\n              dispatch(saveFolderRoot(collection.uid, folder.uid))\n                .then(() => {\n                  dispatch(closeTabs({\n                    tabUids: [tab.uid]\n                  }));\n                  setShowConfirmFolderClose(false);\n                })\n                .catch((err) => {\n                  console.log('err', err);\n                });\n            }}\n          />\n        )}\n        {showConfirmEnvironmentClose && tab.type === 'environment-settings' && (\n          <ConfirmCloseEnvironment\n            isGlobal={false}\n            isDotEnv={collection.environmentsDraft?.environmentUid?.startsWith('dotenv:')}\n            onCancel={() => setShowConfirmEnvironmentClose(false)}\n            onCloseWithoutSave={() => {\n              dispatch(clearEnvironmentsDraft({ collectionUid: collection.uid }));\n              dispatch(closeTabs({ tabUids: [tab.uid] }));\n              setShowConfirmEnvironmentClose(false);\n            }}\n            onSaveAndClose={() => {\n              const draft = collection.environmentsDraft;\n              if (draft?.environmentUid?.startsWith('dotenv:')) {\n                const onSuccess = () => {\n                  cleanup();\n                  dispatch(clearEnvironmentsDraft({ collectionUid: collection.uid }));\n                  dispatch(closeTabs({ tabUids: [tab.uid] }));\n                  setShowConfirmEnvironmentClose(false);\n                };\n                const onFailed = () => {\n                  cleanup();\n                  setShowConfirmEnvironmentClose(false);\n                };\n                const cleanup = () => {\n                  window.removeEventListener('dotenv-save-complete', onSuccess);\n                  window.removeEventListener('dotenv-save-failed', onFailed);\n                };\n                window.addEventListener('dotenv-save-complete', onSuccess, { once: true });\n                window.addEventListener('dotenv-save-failed', onFailed, { once: true });\n                window.dispatchEvent(new Event('dotenv-save'));\n              } else if (draft?.environmentUid && draft?.variables) {\n                dispatch(saveEnvironment(draft.variables, draft.environmentUid, collection.uid))\n                  .then(() => {\n                    dispatch(clearEnvironmentsDraft({ collectionUid: collection.uid }));\n                    dispatch(closeTabs({ tabUids: [tab.uid] }));\n                    setShowConfirmEnvironmentClose(false);\n                    toast.success('Environment saved');\n                  })\n                  .catch((err) => {\n                    console.log('err', err);\n                    toast.error('Failed to save environment');\n                  });\n              }\n            }}\n          />\n        )}\n        {showConfirmGlobalEnvironmentClose && tab.type === 'global-environment-settings' && (\n          <ConfirmCloseEnvironment\n            isGlobal={true}\n            isDotEnv={globalEnvironmentDraft?.environmentUid?.startsWith('dotenv:')}\n            onCancel={() => setShowConfirmGlobalEnvironmentClose(false)}\n            onCloseWithoutSave={() => {\n              dispatch(clearGlobalEnvironmentDraft());\n              dispatch(closeTabs({ tabUids: [tab.uid] }));\n              setShowConfirmGlobalEnvironmentClose(false);\n            }}\n            onSaveAndClose={() => {\n              const draft = globalEnvironmentDraft;\n              if (draft?.environmentUid?.startsWith('dotenv:')) {\n                const onSuccess = () => {\n                  cleanup();\n                  dispatch(clearGlobalEnvironmentDraft());\n                  dispatch(closeTabs({ tabUids: [tab.uid] }));\n                  setShowConfirmGlobalEnvironmentClose(false);\n                };\n                const onFailed = () => {\n                  cleanup();\n                  setShowConfirmGlobalEnvironmentClose(false);\n                };\n                const cleanup = () => {\n                  window.removeEventListener('dotenv-save-complete', onSuccess);\n                  window.removeEventListener('dotenv-save-failed', onFailed);\n                };\n                window.addEventListener('dotenv-save-complete', onSuccess, { once: true });\n                window.addEventListener('dotenv-save-failed', onFailed, { once: true });\n                window.dispatchEvent(new Event('dotenv-save'));\n              } else if (draft?.environmentUid && draft?.variables) {\n                dispatch(saveGlobalEnvironment({ variables: draft.variables, environmentUid: draft.environmentUid }))\n                  .then(() => {\n                    dispatch(clearGlobalEnvironmentDraft());\n                    dispatch(closeTabs({ tabUids: [tab.uid] }));\n                    setShowConfirmGlobalEnvironmentClose(false);\n                    toast.success('Global environment saved');\n                  })\n                  .catch((err) => {\n                    console.log('err', err);\n                    toast.error('Failed to save global environment');\n                  });\n              }\n            }}\n          />\n        )}\n        {tab.type === 'folder-settings' && !folder ? (\n          <RequestTabNotFound handleCloseClick={handleCloseClick} />\n        ) : tab.type === 'folder-settings' ? (\n          <SpecialTab handleCloseClick={handleCloseFolderSettings} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={folder?.name} hasDraft={hasFolderDraft} />\n        ) : tab.type === 'collection-settings' ? (\n          <SpecialTab handleCloseClick={handleCloseCollectionSettings} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} tabName={collection?.name} hasDraft={hasDraft} />\n        ) : tab.type === 'environment-settings' ? (\n          <SpecialTab handleCloseClick={handleCloseEnvironmentSettings} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} hasDraft={hasEnvironmentDraft} />\n        ) : tab.type === 'global-environment-settings' ? (\n          <SpecialTab handleCloseClick={handleCloseGlobalEnvironmentSettings} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} hasDraft={hasGlobalEnvironmentDraft} />\n        ) : tab.type === 'workspaceOverview' ? (\n          <SpecialTab handleCloseClick={null} type={tab.type} />\n        ) : tab.type === 'workspaceEnvironments' ? (\n          <SpecialTab handleCloseClick={null} type={tab.type} />\n        ) : (\n          <SpecialTab handleCloseClick={handleCloseClick} handleDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))} type={tab.type} />\n        )}\n      </StyledWrapper>\n    );\n  }\n\n  // Handle response-example tabs specially\n  if (tab.type === 'response-example') {\n    return (\n      <ExampleTab\n        tab={tab}\n        collection={collection}\n        tabIndex={tabIndex}\n        collectionRequestTabs={collectionRequestTabs}\n        folderUid={folderUid}\n      />\n    );\n  }\n\n  if (!item) {\n    return (\n      <StyledWrapper\n        className=\"flex items-center justify-between tab-container\"\n        onMouseUp={(e) => {\n          if (e.button === 1) {\n            e.preventDefault();\n            e.stopPropagation();\n\n            dispatch(closeTabs({ tabUids: [tab.uid] }));\n          }\n        }}\n      >\n        <RequestTabNotFound handleCloseClick={handleCloseClick} />\n      </StyledWrapper>\n    );\n  }\n\n  return (\n    <StyledWrapper className=\"flex items-center justify-between tab-container px-2\">\n      {showConfirmClose && (\n        <ConfirmRequestClose\n          item={item}\n          onCancel={() => setShowConfirmClose(false)}\n          onCloseWithoutSave={() => {\n            isWS && closeWsConnection(item.uid);\n            dispatch(\n              deleteRequestDraft({\n                itemUid: item.uid,\n                collectionUid: collection.uid\n              })\n            );\n            dispatch(\n              closeTabs({\n                tabUids: [tab.uid]\n              })\n            );\n            setShowConfirmClose(false);\n          }}\n          onSaveAndClose={() => {\n            dispatch(saveRequest(item.uid, collection.uid))\n              .then(() => {\n                dispatch(\n                  closeTabs({\n                    tabUids: [tab.uid]\n                  })\n                );\n                setShowConfirmClose(false);\n              })\n              .catch((err) => {\n                console.log('err', err);\n              });\n          }}\n        />\n      )}\n      <div\n        ref={tabLabelRef}\n        className={`flex items-baseline tab-label ${tab.preview ? 'italic' : ''}`}\n        onContextMenu={handleRightClick}\n        onDoubleClick={() => dispatch(makeTabPermanent({ uid: tab.uid }))}\n        onMouseUp={(e) => {\n          if (!hasChanges) return handleMouseUp(e);\n\n          if (e.button === 1) {\n            e.stopPropagation();\n            e.preventDefault();\n            setShowConfirmClose(true);\n          }\n        }}\n      >\n        <span className=\"tab-method uppercase\" style={{ color: getMethodColor(method) }}>\n          {method}\n        </span>\n        <span ref={tabNameRef} className=\"ml-1 tab-name\" title={item.name}>\n          {item.name}\n        </span>\n        <RequestTabMenu\n          menuDropdownRef={menuDropdownRef}\n          tabLabelRef={tabLabelRef}\n          tabIndex={tabIndex}\n          collectionRequestTabs={collectionRequestTabs}\n          collection={collection}\n          dispatch={dispatch}\n          dropdownContainerRef={dropdownContainerRef}\n        />\n      </div>\n      <GradientCloseButton\n        hasChanges={hasChanges}\n        onClick={(e) => {\n          if (!hasChanges) {\n            isWS && closeWsConnection(item.uid);\n            return handleCloseClick(e);\n          }\n\n          e.stopPropagation();\n          e.preventDefault();\n          setShowConfirmClose(true);\n        }}\n      />\n    </StyledWrapper>\n  );\n};\n\nfunction RequestTabMenu({ menuDropdownRef, tabLabelRef, collectionRequestTabs, tabIndex, collection, dispatch, dropdownContainerRef }) {\n  const [showCloneRequestModal, setShowCloneRequestModal] = useState(false);\n  const [showAddNewRequestModal, setShowAddNewRequestModal] = useState(false);\n\n  // Returns the tab-label's position for dropdown positioning.\n  // Returns zero-sized rect if element isn't mounted yet (prevents Tippy errors).\n  const getTabLabelRect = () => {\n    if (!tabLabelRef.current) {\n      return { width: 0, height: 0, top: 0, bottom: 0, left: 0, right: 0 };\n    }\n    return tabLabelRef.current.getBoundingClientRect();\n  };\n\n  const totalTabs = collectionRequestTabs.length || 0;\n  const currentTabUid = collectionRequestTabs[tabIndex]?.uid;\n  const currentTabItem = findItemInCollection(collection, currentTabUid);\n  const currentTabHasChanges = useMemo(() => hasRequestChanges(currentTabItem), [currentTabItem]);\n\n  const hasLeftTabs = tabIndex !== 0;\n  const hasRightTabs = totalTabs > tabIndex + 1;\n  const hasOtherTabs = totalTabs > 1;\n\n  async function handleCloseTab(tabUid) {\n    if (!tabUid) {\n      return;\n    }\n\n    try {\n      const item = findItemInCollection(collection, tabUid);\n      // silently save unsaved changes before closing the tab\n      if (hasRequestChanges(item)) {\n        await dispatch(saveRequest(item.uid, collection.uid, true));\n      }\n\n      dispatch(closeTabs({ tabUids: [tabUid] }));\n    } catch (err) { }\n  }\n\n  function handleRevertChanges() {\n    if (!currentTabUid) {\n      return;\n    }\n\n    try {\n      const item = findItemInCollection(collection, currentTabUid);\n      if (item.draft) {\n        dispatch(deleteRequestDraft({\n          itemUid: item.uid,\n          collectionUid: collection.uid\n        }));\n      }\n    } catch (err) { }\n  }\n\n  async function handleCloseMultipleTabs(tabs) {\n    const tabUidsToClose = [];\n\n    for (const tab of tabs) {\n      const item = findItemInCollection(collection, tab.uid);\n      if (item && hasRequestChanges(item)) {\n        try {\n          await dispatch(saveRequest(item.uid, collection.uid, true));\n        } catch (err) {\n          continue;\n        }\n      }\n\n      if (tab?.uid) {\n        tabUidsToClose.push(tab.uid);\n      }\n    }\n\n    if (tabUidsToClose.length > 0) {\n      dispatch(closeTabs({ tabUids: tabUidsToClose }));\n    }\n  }\n\n  async function handleCloseOtherTabs() {\n    const otherTabs = collectionRequestTabs.filter((_, index) => index !== tabIndex);\n    await handleCloseMultipleTabs(otherTabs);\n  }\n\n  async function handleCloseTabsToTheLeft() {\n    const leftTabs = collectionRequestTabs.filter((_, index) => index < tabIndex);\n    await handleCloseMultipleTabs(leftTabs);\n  }\n\n  async function handleCloseTabsToTheRight() {\n    const rightTabs = collectionRequestTabs.filter((_, index) => index > tabIndex);\n    await handleCloseMultipleTabs(rightTabs);\n  }\n\n  function handleCloseSavedTabs() {\n    const items = flattenItems(collection?.items);\n    const savedTabs = items?.filter?.((item) => !hasRequestChanges(item));\n    const savedTabIds = savedTabs?.map((item) => item.uid) || [];\n    dispatch(closeTabs({ tabUids: savedTabIds }));\n  }\n\n  async function handleCloseAllTabs() {\n    await handleCloseMultipleTabs(collectionRequestTabs);\n  }\n\n  const menuItems = useMemo(() => [\n    {\n      id: 'new-request',\n      label: 'New Request',\n      onClick: () => setShowAddNewRequestModal(true)\n    },\n    {\n      id: 'clone-request',\n      label: 'Clone Request',\n      onClick: () => setShowCloneRequestModal(true)\n    },\n    {\n      id: 'revert-changes',\n      label: 'Revert Changes',\n      onClick: handleRevertChanges,\n      disabled: !currentTabItem?.draft\n    },\n    {\n      id: 'close',\n      label: 'Close',\n      onClick: () => handleCloseTab(currentTabUid)\n    },\n    {\n      id: 'close-others',\n      label: 'Close Others',\n      onClick: handleCloseOtherTabs,\n      disabled: !hasOtherTabs\n    },\n    {\n      id: 'close-left',\n      label: 'Close to the Left',\n      onClick: handleCloseTabsToTheLeft,\n      disabled: !hasLeftTabs\n    },\n    {\n      id: 'close-right',\n      label: 'Close to the Right',\n      onClick: handleCloseTabsToTheRight,\n      disabled: !hasRightTabs\n    },\n    {\n      id: 'close-saved',\n      label: 'Close Saved',\n      onClick: handleCloseSavedTabs\n    },\n    {\n      id: 'close-all',\n      label: 'Close All',\n      onClick: handleCloseAllTabs\n    }\n  ], [currentTabUid, currentTabItem, hasOtherTabs, hasLeftTabs, hasRightTabs, collection, collectionRequestTabs, tabIndex, dispatch]);\n\n  const menuDropdown = (\n    <MenuDropdown\n      ref={menuDropdownRef}\n      items={menuItems}\n      placement=\"bottom-start\"\n      appendTo={dropdownContainerRef?.current || document.body}\n      getReferenceClientRect={getTabLabelRect}\n    >\n      <span></span>\n    </MenuDropdown>\n  );\n\n  return (\n    <Fragment>\n      {showAddNewRequestModal && (\n        <NewRequest collectionUid={collection.uid} onClose={() => setShowAddNewRequestModal(false)} />\n      )}\n\n      {showCloneRequestModal && (\n        <CloneCollectionItem\n          item={currentTabItem}\n          collectionUid={collection.uid}\n          onClose={() => setShowCloneRequestModal(false)}\n        />\n      )}\n\n      {menuDropdown}\n    </Fragment>\n  );\n}\n\nexport default RequestTab;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabs/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  position: relative;\n\n  &::after {\n    content: '';\n    position: absolute;\n    bottom: 0;\n    left: 0;\n    right: 0;\n    height: 1px;\n    background: ${(props) => props.theme.requestTabs.bottomBorder};\n    z-index: 0;\n  }\n\n  .scroll-chevrons.hidden {\n    display: none;\n  }\n\n  .tabs-scroll-container {\n    overflow-x: auto;\n    overflow-y: clip;\n\n    &::-webkit-scrollbar {\n      display: none;\n    }\n\n    scrollbar-width: none;\n\n    ul {\n      margin-bottom: 0;\n      overflow: visible;\n    }\n  }\n\n  ul {\n    padding: 0 3px;\n    margin: 0;\n    display: flex;\n    align-items: flex-end;\n    position: relative;\n\n    &::-webkit-scrollbar {\n      display: none;\n    }\n\n    scrollbar-width: none;\n\n    li {\n      display: inline-flex;\n      max-width: 180px;\n      min-width: 80px;\n      list-style: none;\n      cursor: pointer;\n      font-size: 0.8125rem;\n      position: relative;\n      margin-right: 3px;\n      color: ${(props) => props.theme.requestTabs.color};\n      background: transparent;\n      border: 1px solid transparent;\n      padding: 6px 0;\n      flex-shrink: 0;\n      margin-bottom: 3px;\n\n      .tab-container {\n        width: 100%;\n        position: relative;\n        overflow: hidden;\n      }\n\n      &:not(.active) {\n        background: ${(props) => props.theme.requestTabs.bg};\n        border-color: transparent;\n        border-radius: ${(props) => props.theme.border.radius.base};\n\n      }\n\n      &:nth-last-child(1) {\n        margin-right: 4px;\n      }\n\n      &.has-overflow:not(:hover) .tab-name {\n        mask-image: linear-gradient(\n          to right,\n          ${(props) => props.theme.requestTabs.color} 0%,\n          ${(props) => props.theme.requestTabs.color} calc(100% - 12px),\n          transparent 100%\n        );\n        -webkit-mask-image: linear-gradient(\n          to right,\n          ${(props) => props.theme.requestTabs.color} 0%,\n          ${(props) => props.theme.requestTabs.color} calc(100% - 12px),\n          transparent 100%\n        );\n      }\n\n      &.has-overflow:hover .tab-name {\n        mask-image: linear-gradient(\n          to right,\n          ${(props) => props.theme.requestTabs.color} 0%,\n          ${(props) => props.theme.requestTabs.color} calc(100% - 8px),\n          transparent 100%\n        );\n        -webkit-mask-image: linear-gradient(\n          to right,\n          ${(props) => props.theme.requestTabs.color} 0%,\n          ${(props) => props.theme.requestTabs.color} calc(100% - 8px),\n          transparent 100%\n        );\n      }\n\n      &.active {\n        background: ${(props) => props.theme.bg || '#ffffff'};\n        border: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};\n        border-bottom-color: ${(props) => props.theme.bg || '#ffffff'};\n        border-radius: 8px 8px 0 0;\n        z-index: 1;\n        margin-bottom: -2px;\n        padding-bottom: 12px;\n\n        &::before {\n          content: '';\n          position: absolute;\n          bottom: 1px;\n          left: -8px;\n          width: 8px;\n          height: 8px;\n          background: transparent;\n          border-bottom-right-radius: 6px;\n          box-shadow: 3px 3px 0 0 ${(props) => props.theme.bg || '#ffffff'};\n          border-right: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};\n          border-bottom: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};\n        }\n\n        &::after {\n          content: '';\n          position: absolute;\n          bottom: 1px;\n          right: -8px;\n          width: 8px;\n          height: 8px;\n          background: transparent;\n          border-bottom-left-radius: 6px;\n          box-shadow: -3px 3px 0 0 ${(props) => props.theme.bg || '#ffffff'};\n          border-left: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};\n          border-bottom: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};\n        }\n      }\n\n      &.short-tab {\n        width: 32px;\n        min-width: 32px;\n        max-width: 32px;\n        padding: 5px 0;\n        display: inline-flex;\n        justify-content: center;\n        align-items: center;\n        color: ${(props) => props.theme.text};\n        background-color: transparent;\n        border: 1px solid transparent;\n        border-radius: ${(props) => props.theme.border.radius.base};\n        flex-shrink: 0;\n\n        > div {\n          padding: 3px;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          border-radius: ${(props) => props.theme.border.radius.sm};\n          transition: background-color 0.12s ease, color 0.12s ease;\n        }\n\n        > div.home-icon-container {\n          padding: 3px 7px;\n        }\n\n        &.choose-request {\n          > div {\n            padding: 3px 5px;\n          }\n        }\n\n        svg {\n          height: 20px;\n          width: 20px;\n        }\n\n        &:hover {\n          > div {\n            background-color: ${(props) => props.theme.background.surface0};\n            color: ${(props) => props.theme.text};\n          }\n        }\n      }\n    }\n  }\n\n  .special-tab-icon {\n    color: ${(props) => props.theme.primary.text};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RequestTabs/index.js",
    "content": "import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';\nimport find from 'lodash/find';\nimport filter from 'lodash/filter';\nimport classnames from 'classnames';\nimport { IconChevronRight, IconChevronLeft } from '@tabler/icons';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { focusTab, reorderTabs } from 'providers/ReduxStore/slices/tabs';\nimport NewRequest from 'components/Sidebar/NewRequest';\nimport CollectionHeader from './CollectionHeader';\nimport RequestTab from './RequestTab';\nimport StyledWrapper from './StyledWrapper';\nimport DraggableTab from './DraggableTab';\nimport CreateTransientRequest from 'components/CreateTransientRequest';\nimport ActionIcon from 'ui/ActionIcon/index';\n\nconst RequestTabs = () => {\n  const dispatch = useDispatch();\n  const tabsRef = useRef();\n  const scrollContainerRef = useRef();\n  const collectionTabsRef = useRef();\n  const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);\n  const [tabOverflowStates, setTabOverflowStates] = useState({});\n  const [showChevrons, setShowChevrons] = useState(false);\n  const tabs = useSelector((state) => state.tabs.tabs);\n  const activeTabUid = useSelector((state) => state.tabs.activeTabUid);\n  const collections = useSelector((state) => state.collections.collections);\n  const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth);\n  const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);\n  const screenWidth = useSelector((state) => state.app.screenWidth);\n  const workspaces = useSelector((state) => state.workspaces.workspaces);\n\n  const createSetHasOverflow = useCallback((tabUid) => {\n    return (hasOverflow) => {\n      setTabOverflowStates((prev) => {\n        if (prev[tabUid] === hasOverflow) {\n          return prev;\n        }\n        return {\n          ...prev,\n          [tabUid]: hasOverflow\n        };\n      });\n    };\n  }, []);\n\n  const activeTab = find(tabs, (t) => t.uid === activeTabUid);\n  const activeCollection = find(collections, (c) => c?.uid === activeTab?.collectionUid);\n  const collectionRequestTabs = filter(tabs, (t) => t.collectionUid === activeTab?.collectionUid);\n\n  const isScratchCollection = useMemo(() => {\n    return activeCollection ? workspaces.some((w) => w.scratchCollectionUid === activeCollection.uid) : false;\n  }, [workspaces, activeCollection]);\n\n  useEffect(() => {\n    if (!activeTabUid || !activeTab) return;\n\n    const checkOverflow = () => {\n      if (tabsRef.current && scrollContainerRef.current) {\n        const hasOverflow = tabsRef.current.scrollWidth > scrollContainerRef.current.clientWidth + 1;\n        setShowChevrons(hasOverflow);\n      }\n    };\n\n    checkOverflow();\n    const resizeObserver = new ResizeObserver(checkOverflow);\n    if (scrollContainerRef.current) {\n      resizeObserver.observe(scrollContainerRef.current);\n    }\n\n    return () => resizeObserver.disconnect();\n  }, [activeTabUid, activeTab, collectionRequestTabs.length, screenWidth, leftSidebarWidth, sidebarCollapsed]);\n\n  const getTabClassname = (tab, index) => {\n    return classnames('request-tab select-none', {\n      'active': tab.uid === activeTabUid,\n      'last-tab': tabs && tabs.length && index === tabs.length - 1,\n      'has-overflow': tabOverflowStates[tab.uid]\n    });\n  };\n\n  const handleClick = (tab) => {\n    dispatch(\n      focusTab({\n        uid: tab.uid\n      })\n    );\n  };\n\n  if (!activeTabUid) {\n    return null;\n  }\n\n  const effectiveSidebarWidth = sidebarCollapsed ? 0 : leftSidebarWidth;\n  const maxTablistWidth = screenWidth - effectiveSidebarWidth - 150;\n\n  const leftSlide = () => {\n    scrollContainerRef.current?.scrollBy({\n      left: -120,\n      behavior: 'smooth'\n    });\n  };\n\n  const rightSlide = () => {\n    scrollContainerRef.current?.scrollBy({\n      left: 120,\n      behavior: 'smooth'\n    });\n  };\n\n  // Todo: Must support ephemeral requests\n  return (\n    <StyledWrapper>\n      {newRequestModalOpen && (\n        <NewRequest collectionUid={activeCollection?.uid} onClose={() => setNewRequestModalOpen(false)} />\n      )}\n      {collectionRequestTabs && collectionRequestTabs.length ? (\n        <>\n          {activeCollection && (\n            <CollectionHeader\n              collection={activeCollection}\n              isScratchCollection={isScratchCollection}\n            />\n          )}\n          <div className=\"flex items-center gap-2 pl-2\" ref={collectionTabsRef}>\n            <div className={classnames('scroll-chevrons', { hidden: !showChevrons })}>\n              <ActionIcon size=\"lg\" onClick={leftSlide} aria-label=\"Left Chevron\" style={{ marginBottom: '3px' }}>\n                <IconChevronLeft size={18} strokeWidth={1.5} />\n              </ActionIcon>\n            </div>\n            {/* Moved to post mvp */}\n            {/* <li className=\"select-none new-tab mr-1\" onClick={createNewTab}>\n              <div className=\"flex items-center home-icon-container\">\n                <IconHome2 size={18} strokeWidth={1.5}/>\n              </div>\n            </li> */}\n            <div className=\"tabs-scroll-container\" style={{ maxWidth: maxTablistWidth }} ref={scrollContainerRef}>\n              <ul role=\"tablist\" ref={tabsRef}>\n                {collectionRequestTabs && collectionRequestTabs.length\n                  ? collectionRequestTabs.map((tab, index) => {\n                      return (\n                        <DraggableTab\n                          key={tab.uid}\n                          id={tab.uid}\n                          index={index}\n                          onMoveTab={(source, target) => {\n                            dispatch(reorderTabs({\n                              sourceUid: source,\n                              targetUid: target\n                            }));\n                          }}\n                          className={getTabClassname(tab, index)}\n                          onClick={() => handleClick(tab)}\n                        >\n                          <RequestTab\n                            collectionRequestTabs={collectionRequestTabs}\n                            tabIndex={index}\n                            key={tab.uid}\n                            tab={tab}\n                            collection={activeCollection}\n                            folderUid={tab.folderUid}\n                            hasOverflow={tabOverflowStates[tab.uid]}\n                            setHasOverflow={createSetHasOverflow(tab.uid)}\n                            dropdownContainerRef={collectionTabsRef}\n                          />\n                        </DraggableTab>\n                      );\n                    })\n                  : null}\n              </ul>\n            </div>\n\n            {activeCollection && (\n              <CreateTransientRequest collectionUid={activeCollection.uid} />\n            )}\n\n            <div className={classnames('scroll-chevrons', { hidden: !showChevrons })}>\n              <ActionIcon size=\"lg\" onClick={rightSlide} aria-label=\"Right Chevron\" style={{ marginBottom: '3px' }}>\n                <IconChevronRight size={18} strokeWidth={1.5} />\n              </ActionIcon>\n            </div>\n            {/* Moved to post mvp */}\n            {/* <li className=\"select-none new-tab choose-request\">\n                <div className=\"flex items-center\">\n                  <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" fill=\"currentColor\" viewBox=\"0 0 16 16\">\n                    <path d=\"M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z\"/>\n                  </svg>\n                </div>\n              </li> */}\n          </div>\n        </>\n      ) : null}\n    </StyledWrapper>\n  );\n};\n\nexport default RequestTabs;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/CreateExampleModal/index.js",
    "content": "import { useState, useEffect } from 'react';\nimport Modal from 'components/Modal';\nimport Portal from 'components/Portal';\n\nconst CreateExampleModal = ({ isOpen, onClose, onSave, title = 'Create Response Example', initialName = '' }) => {\n  const [name, setName] = useState('');\n  const [description, setDescription] = useState('');\n  const [nameError, setNameError] = useState('');\n\n  const handleNameChange = (e) => {\n    setName(e.target.value);\n    // Clear error when user starts typing\n    if (nameError) {\n      setNameError('');\n    }\n  };\n\n  const handleConfirm = () => {\n    if (name.trim()) {\n      onSave(name.trim(), description.trim());\n      // Reset form\n      setName('');\n      setDescription('');\n      setNameError('');\n    } else {\n      setNameError('Example name is required');\n    }\n  };\n\n  const handleClose = () => {\n    // Reset form when closing\n    setName('');\n    setDescription('');\n    setNameError('');\n    onClose();\n  };\n\n  useEffect(() => {\n    if (isOpen) {\n      setName(initialName);\n      setDescription('');\n      setNameError('');\n    }\n  }, [isOpen, initialName]);\n\n  if (!isOpen) {\n    return null;\n  }\n\n  return (\n    <Portal>\n      <Modal\n        size=\"md\"\n        title={title}\n        handleCancel={handleClose}\n        handleConfirm={handleConfirm}\n        confirmText=\"Create Example\"\n        cancelText=\"Cancel\"\n        isOpen={isOpen}\n      >\n        <div className=\"space-y-4\">\n          <div>\n            <label htmlFor=\"exampleName\" className=\"block font-medium\">\n              Example Name<span className=\"text-red-600\">*</span>\n            </label>\n            <input\n              id=\"exampleName\"\n              type=\"text\"\n              className=\"textbox mt-2 w-full\"\n              value={name}\n              onChange={handleNameChange}\n              autoFocus\n              required\n              data-testid=\"create-example-name-input\"\n            />\n            {nameError && (\n              <div className=\"text-red-500 mt-1\" data-testid=\"name-error\">\n                {nameError}\n              </div>\n            )}\n          </div>\n\n          <div>\n            <label htmlFor=\"exampleDescription\" className=\"block font-medium\">\n              Description\n            </label>\n            <textarea\n              id=\"exampleDescription\"\n              className=\"textbox mt-2 w-full\"\n              value={description}\n              onChange={(e) => setDescription(e.target.value)}\n              rows={3}\n              data-testid=\"create-example-description-input\"\n            />\n          </div>\n        </div>\n      </Modal>\n    </Portal>\n  );\n};\n\nexport default CreateExampleModal;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleBody/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n\n  .title {\n    font-weight: 700;\n    color: ${(props) => props.theme.text};\n  }\n\n  font-size: ${(props) => props.theme.font.size.base};\n\n  .body-mode-selector {\n    background: transparent;\n    border-radius: 3px;\n\n    .dropdown-item {\n      padding: 0.2rem 0.6rem !important;\n      padding-left: 1.5rem !important;\n    }\n\n    .label-item {\n      padding: 0.2rem 0.6rem !important;\n    }\n\n    .selected-body-mode {\n      color: ${(props) => props.theme.primary.text};\n    }\n\n    &.cursor-default {\n      opacity: 0.6;\n\n      .selected-body-mode {\n        color: ${(props) => props.theme.colors.text.muted};\n      }\n    }\n  }\n\n  .caret {\n    color: rgb(140, 140, 140);\n    fill: rgb(140 140 140);\n  }\n\n  .btn-action {\n    border-radius: 3px;\n    border: 1px solid transparent;\n    cursor: pointer;\n    transition: all 0.2s ease;\n    white-space: nowrap;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: ${(props) => props.theme.colors.text.muted};\n\n    &:hover {\n      opacity: 0.9;\n    }\n\n    &:disabled {\n      opacity: 0.5;\n      cursor: not-allowed;\n    }\n  }\n\n  .no-body-text {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  /* CodeEditor container */\n  .code-editor-container {\n    flex: 1;\n    min-height: 200px;\n    height: 200px;\n    border-top: none;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleBody/index.js",
    "content": "import React, { useMemo } from 'react';\nimport { useDispatch } from 'react-redux';\nimport get from 'lodash/get';\nimport { updateResponseExampleRequest } from 'providers/ReduxStore/slices/collections';\nimport ResponseExampleBodyMode from '../ResponseExampleBodyMode';\nimport ResponseExampleBodyRenderer from '../ResponseExampleBodyRenderer';\nimport StyledWrapper from './StyledWrapper';\n\nconst ResponseExampleBody = ({ editMode, item, collection, exampleUid, onSave }) => {\n  const dispatch = useDispatch();\n\n  const body = useMemo(() => {\n    return item.draft\n      ? get(item, 'draft.examples', []).find((e) => e.uid === exampleUid)?.request?.body || { mode: 'none' }\n      : get(item, 'examples', []).find((e) => e.uid === exampleUid)?.request?.body || { mode: 'none' };\n  }, [item, exampleUid]);\n\n  const onBodyEdit = (value) => {\n    if (editMode && item && collection.uid && exampleUid) {\n      const updatedBody = { ...body };\n      switch (body.mode) {\n        case 'json':\n          updatedBody.json = value;\n          break;\n        case 'text':\n          updatedBody.text = value;\n          break;\n        case 'xml':\n          updatedBody.xml = value;\n          break;\n        case 'sparql':\n          updatedBody.sparql = value;\n          break;\n        default:\n          break;\n      }\n\n      dispatch(updateResponseExampleRequest({\n        itemUid: item.uid,\n        collectionUid: collection.uid,\n        exampleUid: exampleUid,\n        request: {\n          body: updatedBody\n        }\n      }));\n    }\n  };\n\n  return (\n    <StyledWrapper className=\"w-full mt-4\">\n      <div className=\"flex items-center justify-between\">\n        <div className=\"flex items-center\">\n          <div className=\"title text-xs mr-2\">Body</div>\n        </div>\n        <ResponseExampleBodyMode\n          item={item}\n          collection={collection}\n          exampleUid={exampleUid}\n          body={body}\n          bodyMode={body.mode}\n          onBodyEdit={onBodyEdit}\n          editMode={editMode}\n        />\n      </div>\n\n      <ResponseExampleBodyRenderer\n        bodyMode={body.mode}\n        body={body}\n        editMode={editMode}\n        item={item}\n        collection={collection}\n        exampleUid={exampleUid}\n        onBodyEdit={onBodyEdit}\n        onSave={onSave}\n      />\n    </StyledWrapper>\n  );\n};\n\nexport default ResponseExampleBody;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleBodyMode/index.js",
    "content": "import React from 'react';\nimport { useDispatch } from 'react-redux';\nimport { updateResponseExampleRequest } from 'providers/ReduxStore/slices/collections';\nimport BodyModeSelector from 'components/BodyModeSelector';\nimport { format, applyEdits } from 'jsonc-parser';\nimport xmlFormat from 'xml-formatter';\nimport { toastError } from 'utils/common/error';\n\nconst ResponseExampleBodyMode = ({ item, collection, exampleUid, body, bodyMode, onBodyEdit, editMode = false }) => {\n  const dispatch = useDispatch();\n\n  const onModeChange = (value) => {\n    if (item && collection && exampleUid) {\n      // Initialize the new body structure based on the selected mode\n      let newBody = { mode: value };\n\n      // Initialize body content based on selected mode\n      switch (value) {\n        case 'json':\n          newBody.json = body?.json || '';\n          break;\n        case 'text':\n          newBody.text = body?.text || '';\n          break;\n        case 'xml':\n          newBody.xml = body?.xml || '';\n          break;\n        case 'sparql':\n          newBody.sparql = body?.sparql || '';\n          break;\n        case 'formUrlEncoded':\n          newBody.formUrlEncoded = body?.formUrlEncoded || [];\n          break;\n        case 'multipartForm':\n          newBody.multipartForm = body?.multipartForm || [];\n          break;\n        case 'file':\n          newBody.file = Array.isArray(body?.file) ? body.file : [];\n          break;\n        case 'none':\n          // No additional data needed for 'none' mode\n          break;\n        default:\n          break;\n      }\n\n      dispatch(updateResponseExampleRequest({\n        itemUid: item.uid,\n        collectionUid: collection.uid,\n        exampleUid: exampleUid,\n        request: {\n          body: newBody\n        }\n      }));\n    }\n  };\n\n  const onPrettify = () => {\n    if (body?.json && bodyMode === 'json') {\n      try {\n        const edits = format(body.json, undefined, { tabSize: 2, insertSpaces: true });\n        const prettyBodyJson = applyEdits(body.json, edits);\n        onBodyEdit(prettyBodyJson);\n      } catch (e) {\n        toastError(new Error('Unable to prettify. Invalid JSON format.'));\n      }\n    } else if (body?.xml && bodyMode === 'xml') {\n      try {\n        const prettyBodyXML = xmlFormat(body.xml, { collapseContent: true });\n        onBodyEdit(prettyBodyXML);\n      } catch (e) {\n        toastError(new Error('Unable to prettify. Invalid XML format.'));\n      }\n    }\n  };\n\n  return (\n    <div className=\"flex items-center\">\n      {['json', 'xml'].includes(bodyMode) && (\n        <button\n          className=\"btn-action text-link mr-2 py-1 px-2 text-xs\"\n          onClick={onPrettify}\n        >\n          Prettify\n        </button>\n      )}\n      <BodyModeSelector\n        currentMode={bodyMode}\n        onModeChange={onModeChange}\n        disabled={!editMode}\n      />\n    </div>\n  );\n};\n\nexport default ResponseExampleBodyMode;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleBodyRenderer/index.js",
    "content": "import React from 'react';\nimport { useSelector } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport get from 'lodash/get';\nimport CodeEditor from 'components/CodeEditor';\nimport ResponseExampleFormUrlEncodedParams from '../ResponseExampleFormUrlEncodedParams';\nimport ResponseExampleMultipartFormParams from '../ResponseExampleMultipartFormParams';\nimport ResponseExampleFileBody from '../ResponseExampleFileBody';\n\nconst ResponseExampleBodyRenderer = ({\n  bodyMode,\n  body,\n  editMode,\n  item,\n  collection,\n  exampleUid,\n  onBodyEdit,\n  onSave\n}) => {\n  const { displayedTheme } = useTheme();\n  const preferences = useSelector((state) => state.app.preferences);\n\n  const getBodyContent = () => {\n    if (!body) return '';\n\n    switch (bodyMode) {\n      case 'json':\n        return body.json || '';\n      case 'text':\n        return body.text || '';\n      case 'xml':\n        return body.xml || '';\n      case 'sparql':\n        return body.sparql || '';\n      default:\n        return '';\n    }\n  };\n\n  const getCodeMirrorMode = () => {\n    const modeMap = {\n      json: 'application/ld+json',\n      text: 'application/text',\n      xml: 'application/xml',\n      sparql: 'application/sparql-query'\n    };\n    return modeMap[bodyMode] || 'application/text';\n  };\n\n  const renderBodyContent = () => {\n    switch (bodyMode) {\n      case 'none':\n        return (\n          <div className=\"no-body-text\">\n            No Body\n          </div>\n        );\n\n      case 'json':\n      case 'xml':\n      case 'text':\n      case 'sparql':\n        return (\n          <div className=\"min-h-96\">\n            <CodeEditor\n              collection={collection}\n              item={item}\n              theme={displayedTheme}\n              font={get(preferences, 'font.codeFont', 'default')}\n              fontSize={get(preferences, 'font.codeFontSize')}\n              value={getBodyContent()}\n              onEdit={onBodyEdit}\n              onRun={() => {}}\n              onSave={onSave}\n              mode={getCodeMirrorMode()}\n              enableVariableHighlighting={true}\n              showHintsFor={['variables']}\n              readOnly={!editMode}\n            />\n          </div>\n        );\n\n      case 'formUrlEncoded':\n        return <ResponseExampleFormUrlEncodedParams item={item} collection={collection} exampleUid={exampleUid} editMode={editMode} />;\n\n      case 'multipartForm':\n        return <ResponseExampleMultipartFormParams item={item} collection={collection} exampleUid={exampleUid} editMode={editMode} />;\n\n      case 'file':\n        return <ResponseExampleFileBody item={item} collection={collection} exampleUid={exampleUid} editMode={editMode} />;\n\n      default:\n        return (\n          <div className=\"no-body-text\">\n            No Body\n          </div>\n        );\n    }\n  };\n\n  return renderBodyContent();\n};\n\nexport default ResponseExampleBodyRenderer;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleDescription/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  textarea {\n    background-color: transparent;\n    color: ${(props) => props.theme.text};\n    font-family: inherit;\n    font-size: ${(props) => props.theme.font.size.base};\n    line-height: 1.5;\n    border: 1px solid transparent;\n    padding: 0;\n    \n    &:not([readonly]) {\n      border: 1px solid ${(props) => props.theme.input.border};\n      padding: 8px;\n    }\n    \n    &:focus {\n      outline: none;\n      box-shadow: none;\n      border: 1px solid ${(props) => props.theme.examples.urlBar.border};\n    }\n    \n    &:disabled {\n      background: transparent;\n      color: ${(props) => props.theme.colors.text.muted};\n      cursor: not-allowed;\n      box-shadow: none;\n    }\n    \n    &::placeholder {\n      color: ${(props) => props.theme.input.placeholder.color};\n      opacity: ${(props) => props.theme.input.placeholder.opacity};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleDescription/index.js",
    "content": "import React, { useState, useMemo } from 'react';\nimport { useDispatch } from 'react-redux';\nimport get from 'lodash/get';\nimport { updateResponseExampleDetails } from 'providers/ReduxStore/slices/collections';\nimport StyledWrapper from './StyledWrapper';\n\nconst ResponseExampleDescription = ({ editMode, item, collection, exampleUid }) => {\n  const dispatch = useDispatch();\n\n  const description = useMemo(() => {\n    return item.draft\n      ? get(item, 'draft.examples', []).find((e) => e.uid === exampleUid)?.description || ''\n      : get(item, 'examples', []).find((e) => e.uid === exampleUid)?.description || '';\n  }, [item, exampleUid]);\n\n  const handleChange = (e) => {\n    const newValue = e.target.value;\n\n    if (editMode && item && collection && exampleUid) {\n      dispatch(updateResponseExampleDetails({\n        itemUid: item.uid,\n        collectionUid: collection.uid,\n        exampleUid: exampleUid,\n        details: {\n          description: newValue\n        }\n      }));\n    }\n  };\n\n  return (\n    <StyledWrapper className=\"w-full\">\n      <div className=\"mb-2\">\n        <textarea\n          data-testid=\"response-example-description-input\"\n          value={description}\n          onChange={handleChange}\n          readOnly={!editMode}\n          placeholder=\"Enter example description...\"\n          className=\"w-full p-3 border rounded-md\"\n          rows={1}\n        />\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default ResponseExampleDescription;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleFileBody/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  table {\n    width: 100%;\n    border-collapse: collapse;\n    font-weight: 500;\n    table-layout: fixed;\n\n    thead,\n    td {\n      border: 1px solid ${(props) => props.theme.table.border};\n    }\n\n    thead {\n      color: ${(props) => props.theme.table.thead.color};\n      font-size: ${(props) => props.theme.font.size.base};\n      user-select: none;\n    }\n    td {\n      padding: 6px 10px;\n      }\n    }\n\n  .btn-add-param {\n    font-size: ${(props) => props.theme.font.size.base};\n  }\n\n  input[type='text'] {\n    width: 100%;\n    border: solid 1px transparent;\n    outline: none !important;\n    color: ${(props) => props.theme.table.input.color};\n    background: transparent;\n\n    &:focus {\n      outline: none !important;\n      border: solid 1px transparent;\n    }\n  }\n\n  input[type='checkbox'] {\n    cursor: pointer;\n    position: relative;\n    top: 1px;\n  }\n\n  .btn-action {\n    background: none;\n    border: none;\n    cursor: pointer;\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-weight: 500;\n    color: ${(props) => props.theme.colors.text.muted};\n    \n    &:hover {\n      opacity: 0.8;\n    }\n    \n    &:disabled {\n      opacity: 0.5;\n      cursor: not-allowed;\n    }\n  }\n\n  .btn-secondary {\n    &.edit-mode {\n      background-color: ${(props) => props.theme.colors.text.yellow}20;\n      border-color: ${(props) => props.theme.colors.text.yellow};\n      color: ${(props) => props.theme.colors.text.yellow};\n    }\n\n    &.view-mode {\n      background-color: transparent;\n      border-color: ${(props) => props.theme.colors.text.muted};\n      color: ${(props) => props.theme.colors.text.muted};\n      cursor: default;\n    }\n\n    /* Fix alignment for file picker content */\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    \n    button {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      padding: 2px;\n      margin-right: 4px;\n    }\n  }\n\n  tr {\n    position: relative;\n    \n    &:hover .delete-button.edit-mode {\n      opacity: 1;\n      visibility: visible;\n    }\n  }\n\n  .delete-button {\n    opacity: 0;\n    visibility: hidden;\n    background: none;\n    border: none;\n    cursor: pointer;\n    padding: 4px;\n    border-radius: 4px;\n    color: ${(props) => props.theme.colors.text.muted};\n    margin-left: 8px;\n    \n    &:hover {\n      color: ${(props) => props.theme.colors.text.red};\n    }\n\n    &:disabled {\n      opacity: 0.3;\n      cursor: not-allowed;\n    }\n\n    svg {\n      width: 16px;\n      height: 16px;\n      color: ${(props) => props.theme.text};\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleFileBody/index.js",
    "content": "import React, { useState, useMemo, useCallback } from 'react';\nimport { get } from 'lodash';\nimport { useDispatch } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport { updateResponseExampleFileBodyParams } from 'providers/ReduxStore/slices/collections';\nimport mime from 'mime-types';\nimport path from 'utils/common/path';\nimport EditableTable from 'components/EditableTable';\nimport StyledWrapper from './StyledWrapper';\nimport FilePickerEditor from 'components/FilePickerEditor/index';\nimport SingleLineEditor from 'components/SingleLineEditor/index';\nimport RadioButton from 'components/RadioButton';\n\nconst ResponseExampleFileBody = ({ item, collection, exampleUid, editMode = false }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n\n  // Get file data from the specific example\n  const params = useMemo(() => {\n    const _params = item.draft\n      ? get(item, 'draft.examples', []).find((e) => e.uid === exampleUid)?.request?.body?.file || []\n      : get(item, 'examples', []).find((e) => e.uid === exampleUid)?.request?.body?.file || [];\n    return Array.isArray(_params) ? _params : [];\n  }, [item.draft, item.examples, item, exampleUid]);\n\n  const handleParamsChange = useCallback((updatedParams) => {\n    if (!editMode) return;\n\n    dispatch(updateResponseExampleFileBodyParams({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: exampleUid,\n      params: updatedParams\n    }));\n  }, [editMode, dispatch, item.uid, collection.uid, exampleUid]);\n\n  const handleFilePathChange = useCallback((row, newFilePath, onChange) => {\n    if (!editMode) return;\n\n    const currentParams = params || [];\n    const existingParam = currentParams.find((p) => p.uid === row.uid);\n\n    let updatedParams;\n    if (existingParam) {\n      // Update existing param\n      updatedParams = currentParams.map((p) => {\n        if (p.uid === row.uid) {\n          const updated = { ...p, filePath: newFilePath };\n          // Auto-detect content type from file extension\n          if (newFilePath) {\n            const contentType = mime.contentType(path.extname(newFilePath));\n            updated.contentType = contentType || '';\n          } else {\n            updated.contentType = '';\n          }\n          return updated;\n        }\n        return p;\n      });\n    } else {\n      // Add new param (from EditableTable's empty row)\n      // Deselect all existing params and select the new one\n      const deselectedParams = currentParams.map((p) => ({ ...p, selected: false }));\n      const newParam = {\n        uid: row.uid,\n        filePath: newFilePath,\n        contentType: '',\n        selected: true\n      };\n      // Auto-detect content type from file extension\n      if (newFilePath) {\n        const contentType = mime.contentType(path.extname(newFilePath));\n        newParam.contentType = contentType || '';\n      }\n      updatedParams = [...deselectedParams, newParam];\n    }\n\n    handleParamsChange(updatedParams);\n  }, [editMode, params, handleParamsChange]);\n\n  const handleSelectedChange = useCallback((row, checked) => {\n    if (!editMode) return;\n\n    // When a file is selected, deselect all others and select this one\n    const updatedParams = params.map((p) => ({\n      ...p,\n      selected: p.uid === row.uid ? checked : false\n    }));\n\n    handleParamsChange(updatedParams);\n  }, [editMode, params, handleParamsChange]);\n\n  const handleParamDrag = useCallback(({ updateReorderedItem }) => {\n    if (!editMode) return;\n\n    const reorderedParams = updateReorderedItem.map((uid) => {\n      return params.find((p) => p.uid === uid);\n    }).filter(Boolean);\n\n    dispatch(updateResponseExampleFileBodyParams({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: exampleUid,\n      params: reorderedParams\n    }));\n  }, [editMode, dispatch, item.uid, collection.uid, exampleUid, params]);\n\n  const columns = [\n    {\n      key: 'filePath',\n      name: 'File',\n      isKeyField: true,\n      placeholder: 'File',\n      width: '50%',\n      readOnly: !editMode,\n      render: ({ row, value, onChange, isLastEmptyRow }) => (\n        <FilePickerEditor\n          isSingleFilePicker={true}\n          value={value || ''}\n          onChange={(newPath) => handleFilePathChange(row, newPath, onChange)}\n          collection={collection}\n          readOnly={!editMode}\n          displayMode=\"labelAndIcon\"\n        />\n      )\n    },\n    {\n      key: 'contentType',\n      name: 'Content-Type',\n      placeholder: 'Auto',\n      width: '30%',\n      readOnly: !editMode,\n      render: ({ value, onChange }) => (\n        <SingleLineEditor\n          className=\"flex items-center justify-center\"\n          onSave={() => {}}\n          theme={storedTheme}\n          placeholder={!value ? 'Auto' : ''}\n          value={value || ''}\n          onChange={onChange}\n          onRun={() => {}}\n          collection={collection}\n          readOnly={!editMode}\n        />\n      )\n    },\n    {\n      key: 'selected',\n      name: 'Selected',\n      width: '20%',\n      readOnly: !editMode,\n      render: ({ row, value, onChange, isLastEmptyRow, rowIndex }) => (\n        <div className=\"flex items-center justify-center pl-4\">\n          <RadioButton\n            key={row.uid}\n            id={`file-${row.uid}`}\n            name=\"selectedFile\"\n            value={row.uid}\n            checked={row.selected}\n            onChange={(e) => handleSelectedChange(row, e.target.checked)}\n            disabled={!editMode}\n            className=\"mr-1 mousetrap\"\n            dataTestId={`file-radio-button-${rowIndex}`}\n          />\n        </div>\n      )\n    }\n  ];\n\n  const defaultRow = {\n    filePath: '',\n    contentType: '',\n    selected: false\n  };\n\n  if (params.length === 0 && !editMode) {\n    return null;\n  }\n\n  return (\n    <StyledWrapper className=\"w-full mt-4\">\n      <EditableTable\n        columns={columns}\n        rows={params || []}\n        onChange={handleParamsChange}\n        defaultRow={defaultRow}\n        reorderable={editMode}\n        onReorder={handleParamDrag}\n        showAddRow={editMode}\n        showCheckbox={false}\n        showDelete={editMode}\n      />\n    </StyledWrapper>\n  );\n};\n\nexport default ResponseExampleFileBody;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleFormUrlEncodedParams/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  table {\n    width: 100%;\n    border-collapse: collapse;\n    font-weight: 500;\n    table-layout: fixed;\n\n    thead,\n    td {\n      border: 1px solid ${(props) => props.theme.table.border};\n    }\n\n    thead {\n      color: ${(props) => props.theme.table.thead.color};\n      font-size: ${(props) => props.theme.font.size.base};\n      user-select: none;\n    }\n    td {\n      padding: 6px 10px;\n    }\n  }\n\n  .btn-add-param {\n    font-size: ${(props) => props.theme.font.size.base};\n  }\n\n  input[type='text'] {\n    width: 100%;\n    border: solid 1px transparent;\n    outline: none !important;\n    color: ${(props) => props.theme.table.input.color};\n    background: transparent;\n\n    &:focus {\n      outline: none !important;\n      border: solid 1px transparent;\n    }\n  }\n\n  input[type='checkbox'] {\n    cursor: pointer;\n    position: relative;\n    top: 1px;\n  }\n\n  tr {\n    position: relative;\n    \n    &:hover .delete-button.edit-mode {\n      opacity: 1;\n      visibility: visible;\n    }\n  }\n\n  .delete-button {\n    opacity: 0;\n    visibility: hidden;\n    background: none;\n    border: none;\n    cursor: pointer;\n    padding: 4px;\n    border-radius: 4px;\n    color: ${(props) => props.theme.colors.text.muted};\n    margin-left: 8px;\n    \n    &:hover {\n      color: ${(props) => props.theme.colors.text.red};\n    }\n\n    svg {\n      width: 16px;\n      height: 16px;\n      color: ${(props) => props.theme.text};\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleFormUrlEncodedParams/index.js",
    "content": "import React, { useMemo, useCallback } from 'react';\nimport get from 'lodash/get';\nimport { useDispatch } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport { updateResponseExampleFormUrlEncodedParams } from 'providers/ReduxStore/slices/collections';\nimport EditableTable from 'components/EditableTable';\nimport MultiLineEditor from 'components/MultiLineEditor';\nimport StyledWrapper from './StyledWrapper';\n\nconst ResponseExampleFormUrlEncodedParams = ({ item, collection, exampleUid, editMode = false }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n\n  const params = useMemo(() => {\n    return item.draft\n      ? get(item, 'draft.examples', []).find((e) => e.uid === exampleUid)?.request?.body?.formUrlEncoded || []\n      : get(item, 'examples', []).find((e) => e.uid === exampleUid)?.request?.body?.formUrlEncoded || [];\n  }, [item, exampleUid]);\n\n  const handleParamsChange = useCallback((updatedParams) => {\n    if (!editMode) return;\n\n    dispatch(updateResponseExampleFormUrlEncodedParams({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: exampleUid,\n      params: updatedParams\n    }));\n  }, [editMode, dispatch, item.uid, collection.uid, exampleUid]);\n\n  const handleParamDrag = useCallback(({ updateReorderedItem }) => {\n    if (!editMode) return;\n\n    const reorderedParams = updateReorderedItem.map((uid) => {\n      return params.find((p) => p.uid === uid);\n    }).filter(Boolean);\n\n    dispatch(updateResponseExampleFormUrlEncodedParams({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: exampleUid,\n      params: reorderedParams\n    }));\n  }, [editMode, dispatch, item.uid, collection.uid, exampleUid, params]);\n\n  const columns = [\n    {\n      key: 'name',\n      name: 'Key',\n      isKeyField: true,\n      placeholder: 'Key',\n      width: '40%',\n      readOnly: !editMode\n    },\n    {\n      key: 'value',\n      name: 'Value',\n      placeholder: 'Value',\n      width: '60%',\n      readOnly: !editMode,\n      render: ({ value, onChange }) => (\n        <MultiLineEditor\n          value={value || ''}\n          theme={storedTheme}\n          onSave={() => {}}\n          onChange={onChange}\n          allowNewlines={true}\n          onRun={() => {}}\n          collection={collection}\n          item={item}\n          placeholder={!value ? 'Value' : ''}\n        />\n      )\n    }\n  ];\n\n  const defaultRow = {\n    name: '',\n    value: '',\n    enabled: true\n  };\n\n  if (params.length === 0 && !editMode) {\n    return null;\n  }\n\n  return (\n    <StyledWrapper className=\"w-full mt-4\">\n      <EditableTable\n        columns={columns}\n        rows={params || []}\n        onChange={handleParamsChange}\n        defaultRow={defaultRow}\n        reorderable={editMode}\n        onReorder={handleParamDrag}\n        showAddRow={editMode}\n        showDelete={editMode}\n        disableCheckbox={!editMode}\n      />\n    </StyledWrapper>\n  );\n};\n\nexport default ResponseExampleFormUrlEncodedParams;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleHeaders/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .title {\n    color: ${(props) => props.theme.text};\n  }\n  \n  .btn-action {\n    background: none;\n    border: none;\n    cursor: pointer;\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-weight: 500;\n    transition: opacity 0.2s ease;\n    color: ${(props) => props.theme.colors.text.muted};\n    \n    &:hover {\n      opacity: 0.8;\n    }\n    \n    &:disabled {\n      opacity: 0.5;\n      cursor: not-allowed;\n    }\n  }\n  \n  tr {\n    position: relative;\n    \n    &:hover .delete-button {\n      opacity: 1;\n      visibility: visible;\n    }\n  }\n\n  .delete-button {\n    opacity: 0;\n    visibility: hidden;\n    transition: all 0.2s ease;\n    background: none;\n    border: none;\n    cursor: pointer;\n    padding: 4px;\n    border-radius: 4px;\n    color: ${(props) => props.theme.colors.text.muted};\n    margin-left: 8px;\n    \n    &:hover {\n      color: ${(props) => props.theme.colors.text.red};\n    }\n\n    svg {\n      width: 16px;\n      height: 16px;\n      color: ${(props) => props.theme.text};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleHeaders/index.js",
    "content": "import React, { useState, useMemo, useCallback } from 'react';\nimport { useDispatch } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport get from 'lodash/get';\nimport { moveResponseExampleRequestHeader, setResponseExampleRequestHeaders } from 'providers/ReduxStore/slices/collections';\nimport EditableTable from 'components/EditableTable';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport BulkEditor from 'components/BulkEditor';\nimport { headers as StandardHTTPHeaders } from 'know-your-http-well';\nimport { MimeTypes } from 'utils/codemirror/autocompleteConstants';\nimport StyledWrapper from './StyledWrapper';\n\nconst headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);\n\nconst ResponseExampleHeaders = ({ editMode, item, collection, exampleUid }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n  const [isBulkEditMode, setIsBulkEditMode] = useState(false);\n\n  const headers = useMemo(() => {\n    return item.draft\n      ? get(item, 'draft.examples', []).find((e) => e.uid === exampleUid)?.request?.headers || []\n      : get(item, 'examples', []).find((e) => e.uid === exampleUid)?.request?.headers || [];\n  }, [item, exampleUid]);\n\n  const handleHeadersChange = useCallback((updatedHeaders) => {\n    if (editMode) {\n      dispatch(setResponseExampleRequestHeaders({\n        itemUid: item.uid,\n        collectionUid: collection.uid,\n        exampleUid: exampleUid,\n        headers: updatedHeaders\n      }));\n    }\n  }, [editMode, dispatch, item.uid, collection.uid, exampleUid]);\n\n  const handleHeaderDrag = useCallback(({ updateReorderedItem }) => {\n    if (editMode) {\n      dispatch(moveResponseExampleRequestHeader({\n        itemUid: item.uid,\n        collectionUid: collection.uid,\n        exampleUid: exampleUid,\n        updateReorderedItem\n      }));\n    }\n  }, [editMode, dispatch, item.uid, collection.uid, exampleUid]);\n\n  const toggleBulkEditMode = () => {\n    setIsBulkEditMode(!isBulkEditMode);\n  };\n\n  const handleBulkHeadersChange = (newHeaders) => {\n    if (editMode) {\n      dispatch(setResponseExampleRequestHeaders({\n        itemUid: item.uid,\n        collectionUid: collection.uid,\n        exampleUid: exampleUid,\n        headers: newHeaders\n      }));\n    }\n  };\n\n  const columns = [\n    {\n      key: 'name',\n      name: 'Key',\n      isKeyField: true,\n      placeholder: 'Key',\n      width: '40%',\n      readOnly: !editMode,\n      render: ({ value, onChange }) => (\n        <SingleLineEditor\n          value={value || ''}\n          readOnly={!editMode}\n          theme={storedTheme}\n          onSave={() => {}}\n          onChange={(newValue) => onChange(newValue.replace(/[\\r\\n]/g, ''))}\n          autocomplete={headerAutoCompleteList}\n          onRun={() => {}}\n          collection={collection}\n          placeholder={!value ? 'Key' : ''}\n        />\n      )\n    },\n    {\n      key: 'value',\n      name: 'Value',\n      placeholder: 'Value',\n      width: '60%',\n      readOnly: !editMode,\n      render: ({ value, onChange }) => (\n        <SingleLineEditor\n          value={value || ''}\n          readOnly={!editMode}\n          theme={storedTheme}\n          onSave={() => {}}\n          onChange={onChange}\n          onRun={() => {}}\n          autocomplete={MimeTypes}\n          allowNewlines={true}\n          collection={collection}\n          item={item}\n          placeholder={!value ? 'Value' : ''}\n        />\n      )\n    }\n  ];\n\n  const defaultRow = {\n    name: '',\n    value: '',\n    enabled: true\n  };\n\n  if (isBulkEditMode && editMode) {\n    return (\n      <StyledWrapper className=\"w-full mt-3\">\n        <BulkEditor\n          params={headers}\n          onChange={handleBulkHeadersChange}\n          onToggle={toggleBulkEditMode}\n        />\n      </StyledWrapper>\n    );\n  }\n\n  if (headers.length === 0 && !editMode) {\n    return null;\n  }\n\n  return (\n    <StyledWrapper className=\"w-full mt-4\">\n      <div className=\"mb-3 title text-xs font-bold\">Headers</div>\n      <EditableTable\n        columns={columns}\n        rows={headers || []}\n        onChange={handleHeadersChange}\n        defaultRow={defaultRow}\n        reorderable={editMode}\n        onReorder={handleHeaderDrag}\n        showAddRow={editMode}\n        showDelete={editMode}\n        disableCheckbox={!editMode}\n      />\n      {editMode && (\n        <div className=\"flex justify-end mt-2\">\n          <button\n            className=\"btn-action text-link select-none\"\n            onClick={toggleBulkEditMode}\n          >\n            Bulk Edit\n          </button>\n        </div>\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default ResponseExampleHeaders;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleMultipartFormParams/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  table {\n    width: 100%;\n    border-collapse: collapse;\n    font-weight: 500;\n    table-layout: fixed;\n\n    thead,\n    td {\n      border: 1px solid ${(props) => props.theme.table.border};\n    }\n\n    thead {\n      color: ${(props) => props.theme.table.thead.color};\n      font-size: ${(props) => props.theme.font.size.base};\n      user-select: none;\n    }\n    td {\n      padding: 6px 10px;\n    }\n  }\n\n  .btn-add-param {\n    font-size: ${(props) => props.theme.font.size.base};\n  }\n\n  input[type='text'] {\n    width: 100%;\n    border: solid 1px transparent;\n    outline: none !important;\n    color: ${(props) => props.theme.table.input.color};\n    background: transparent;\n\n    &:focus {\n      outline: none !important;\n      border: solid 1px transparent;\n    }\n  }\n\n  input[type='checkbox'] {\n    cursor: pointer;\n    position: relative;\n    top: 1px;\n  }\n\n  .btn-secondary {\n    &.edit-mode {\n      background-color: ${(props) => props.theme.colors.text.yellow}20;\n      border-color: ${(props) => props.theme.colors.text.yellow};\n      color: ${(props) => props.theme.colors.text.yellow};\n    }\n\n    &.view-mode {\n      background-color: transparent;\n      border-color: ${(props) => props.theme.colors.text.muted};\n      color: ${(props) => props.theme.colors.text.muted};\n      cursor: default;\n    }\n  }\n\n  tr {\n    position: relative;\n    \n    &:hover .delete-button.edit-mode {\n      opacity: 1;\n      visibility: visible;\n    }\n  }\n\n  .delete-button {\n    opacity: 0;\n    visibility: hidden;\n    background: none;\n    border: none;\n    cursor: pointer;\n    padding: 4px;\n    border-radius: 4px;\n    color: ${(props) => props.theme.colors.text.muted};\n    margin-left: 8px;\n    \n    &:hover {\n      color: ${(props) => props.theme.colors.text.red};\n    }\n\n    &:disabled {\n      opacity: 0.3;\n      cursor: not-allowed;\n    }\n\n    svg {\n      width: 16px;\n      height: 16px;\n      color: ${(props) => props.theme.text};\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleMultipartFormParams/index.js",
    "content": "import React, { useMemo, useCallback } from 'react';\nimport get from 'lodash/get';\nimport { useDispatch } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport { IconUpload, IconX, IconFile } from '@tabler/icons';\nimport { updateResponseExampleMultipartFormParams } from 'providers/ReduxStore/slices/collections';\nimport { browseFiles } from 'providers/ReduxStore/slices/collections/actions';\nimport mime from 'mime-types';\nimport path from 'utils/common/path';\nimport EditableTable from 'components/EditableTable';\nimport MultiLineEditor from 'components/MultiLineEditor';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport StyledWrapper from './StyledWrapper';\nimport { isWindowsOS } from 'utils/common/platform';\n\nconst ResponseExampleMultipartFormParams = ({ item, collection, exampleUid, editMode = false }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n\n  const params = useMemo(() => {\n    return item.draft\n      ? get(item, 'draft.examples', []).find((e) => e.uid === exampleUid)?.request?.body?.multipartForm || []\n      : get(item, 'examples', []).find((e) => e.uid === exampleUid)?.request?.body?.multipartForm || [];\n  }, [item, exampleUid]);\n\n  const handleParamsChange = useCallback((updatedParams) => {\n    if (!editMode) return;\n\n    dispatch(updateResponseExampleMultipartFormParams({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: exampleUid,\n      params: updatedParams\n    }));\n  }, [editMode, dispatch, item.uid, collection.uid, exampleUid]);\n\n  const handleBrowseFiles = useCallback((row, onChange) => {\n    if (!editMode) return;\n\n    dispatch(browseFiles())\n      .then((filePaths) => {\n        const processedPaths = filePaths.map((filePath) => {\n          const collectionDir = collection.pathname;\n          if (filePath.startsWith(collectionDir)) {\n            return path.relative(collectionDir, filePath);\n          }\n          return filePath;\n        });\n\n        const currentParams = params || [];\n        const existingParam = currentParams.find((p) => p.uid === row.uid);\n\n        let updatedParams;\n        if (existingParam) {\n          // Update existing param\n          updatedParams = currentParams.map((p) => {\n            if (p.uid === row.uid) {\n              const updated = { ...p, type: 'file', value: processedPaths };\n              // Auto-detect content type from first file\n              if (processedPaths.length > 0) {\n                const contentType = mime.contentType(path.extname(processedPaths[0]));\n                updated.contentType = contentType || '';\n              }\n              return updated;\n            }\n            return p;\n          });\n        } else {\n          // Add new param (from EditableTable's empty row)\n          const newParam = {\n            uid: row.uid,\n            name: row.name || '',\n            type: 'file',\n            value: processedPaths,\n            contentType: '',\n            enabled: true\n          };\n          // Auto-detect content type from first file\n          if (processedPaths.length > 0) {\n            const contentType = mime.contentType(path.extname(processedPaths[0]));\n            newParam.contentType = contentType || '';\n          }\n          updatedParams = [...currentParams, newParam];\n        }\n\n        handleParamsChange(updatedParams);\n      })\n      .catch((error) => {\n        console.error(error);\n      });\n  }, [editMode, dispatch, collection.pathname, params, handleParamsChange]);\n\n  const handleClearFile = useCallback((row) => {\n    if (!editMode) return;\n\n    const currentParams = params || [];\n    const existingParam = currentParams.find((p) => p.uid === row.uid);\n\n    if (existingParam) {\n      const updatedParams = currentParams.map((p) => {\n        if (p.uid === row.uid) {\n          return { ...p, type: 'text', value: '' };\n        }\n        return p;\n      });\n      handleParamsChange(updatedParams);\n    }\n  }, [editMode, params, handleParamsChange]);\n\n  const handleValueChange = useCallback((row, newValue, onChange) => {\n    if (!editMode) return;\n\n    const currentParams = params || [];\n    const existingParam = currentParams.find((p) => p.uid === row.uid);\n    if (existingParam) {\n      const updatedParams = currentParams.map((p) => {\n        if (p.uid === row.uid) {\n          return { ...p, type: 'text', value: newValue };\n        }\n        return p;\n      });\n      handleParamsChange(updatedParams);\n    } else {\n      onChange(newValue);\n    }\n  }, [editMode, params, handleParamsChange]);\n\n  const handleParamDrag = useCallback(({ updateReorderedItem }) => {\n    if (!editMode) return;\n\n    const reorderedParams = updateReorderedItem.map((uid) => {\n      return params.find((p) => p.uid === uid);\n    });\n\n    dispatch(updateResponseExampleMultipartFormParams({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: exampleUid,\n      params: reorderedParams\n    }));\n  }, [editMode, dispatch, item.uid, collection.uid, exampleUid, params]);\n\n  const getFileName = (filePaths) => {\n    if (!filePaths || (Array.isArray(filePaths) && filePaths.length === 0)) {\n      return null;\n    }\n    const paths = Array.isArray(filePaths) ? filePaths : [filePaths];\n    const validPaths = paths.filter((v) => v != null && v !== '');\n    if (validPaths.length === 0) return null;\n\n    const separator = isWindowsOS() ? '\\\\' : '/';\n    if (validPaths.length === 1) {\n      return validPaths[0].split(separator).pop();\n    }\n    return `${validPaths.length} file(s)`;\n  };\n\n  const columns = [\n    {\n      key: 'name',\n      name: 'Key',\n      isKeyField: true,\n      placeholder: 'Key',\n      width: '30%',\n      readOnly: !editMode\n    },\n    {\n      key: 'value',\n      name: 'Value',\n      placeholder: 'Value',\n      width: '40%',\n      readOnly: !editMode,\n      render: ({ row, value, onChange, isLastEmptyRow }) => {\n        const isFile = row.type === 'file';\n        const fileName = isFile ? getFileName(value) : null;\n        const hasTextValue = !isFile && value && value.length > 0;\n\n        if (fileName) {\n          return (\n            <div className=\"flex items-center file-value-cell\">\n              <IconFile size={16} className=\"text-muted mr-1\" />\n              <span className=\"file-name flex-1 truncate\" title={Array.isArray(value) ? value.join(', ') : value}>\n                {fileName}\n              </span>\n              <button\n                className=\"clear-file-btn ml-1\"\n                onClick={() => handleClearFile(row)}\n                title=\"Remove file\"\n              >\n                <IconX size={16} />\n              </button>\n            </div>\n          );\n        }\n\n        return (\n          <div className=\"flex items-center value-cell\">\n            <div className=\"flex-1\">\n              <MultiLineEditor\n                onSave={() => {}}\n                theme={storedTheme}\n                value={value || ''}\n                onChange={(newValue) => handleValueChange(row, newValue, onChange)}\n                onRun={() => {}}\n                allowNewlines={true}\n                collection={collection}\n                item={item}\n                readOnly={!editMode}\n                placeholder={!value ? 'Value' : ''}\n              />\n            </div>\n            {!hasTextValue && !isLastEmptyRow && (\n              <button\n                className=\"upload-btn ml-1\"\n                onClick={() => handleBrowseFiles(row, onChange)}\n                title=\"Select file\"\n              >\n                <IconUpload size={16} />\n              </button>\n            )}\n          </div>\n        );\n      }\n    },\n    {\n      key: 'contentType',\n      name: 'Content-Type',\n      placeholder: 'Auto',\n      width: '30%',\n      readOnly: !editMode,\n      render: ({ value, onChange }) => (\n        <SingleLineEditor\n          onSave={() => {}}\n          theme={storedTheme}\n          placeholder={!value ? 'Auto' : ''}\n          value={value || ''}\n          onChange={onChange}\n          onRun={() => {}}\n          collection={collection}\n          readOnly={!editMode}\n        />\n      )\n    }\n  ];\n\n  const defaultRow = {\n    name: '',\n    value: '',\n    contentType: '',\n    enabled: true,\n    type: 'text'\n  };\n\n  if (params.length === 0 && !editMode) {\n    return null;\n  }\n\n  return (\n    <StyledWrapper className=\"w-full mt-4\">\n      <EditableTable\n        columns={columns}\n        rows={params || []}\n        onChange={handleParamsChange}\n        defaultRow={defaultRow}\n        reorderable={editMode}\n        onReorder={handleParamDrag}\n        showAddRow={editMode}\n        showDelete={editMode}\n        disableCheckbox={!editMode}\n      />\n    </StyledWrapper>\n  );\n};\n\nexport default ResponseExampleMultipartFormParams;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleParams/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .title {\n    font-weight: 700;\n    color: ${(props) => props.theme.text};\n  }\n  \n  .btn-action {\n    background: none;\n    border: none;\n    cursor: pointer;\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-weight: 500;\n    transition: opacity 0.2s ease;\n    color: ${(props) => props.theme.colors.text.muted};\n    \n    &:hover {\n      opacity: 0.8;\n    }\n    \n    &:disabled {\n      opacity: 0.5;\n      cursor: not-allowed;\n    }\n  }\n  \n  table {\n    border-collapse: collapse;\n    width: 100%;\n    \n    thead {\n      td {\n        font-weight: 500;\n        font-size: ${(props) => props.theme.font.size.sm};\n        text-transform: uppercase;\n        letter-spacing: 0.5px;\n        padding: 8px 0;\n        border-bottom: 1px solid ${(props) => props.theme.table.border};\n      }\n    }\n    \n    tbody {\n      tr {\n        border-bottom: 1px solid ${(props) => props.theme.table.border};\n      }\n    }\n  }\n\n  /* Override styles for EditableTable to prevent uppercase transformation and ensure proper spacing */\n  /* The .table-container is from EditableTable component */\n  .table-container table thead td {\n    text-transform: none !important;\n    letter-spacing: normal !important;\n    padding: 8px 10px !important;\n  }\n  \n\n  tr {\n    position: relative;\n    \n    &:hover .delete-button {\n      opacity: 1;\n      visibility: visible;\n    }\n  }\n\n  .delete-button {\n    opacity: 0;\n    visibility: hidden;\n    transition: all 0.2s ease;\n    background: none;\n    border: none;\n    cursor: pointer;\n    padding: 4px;\n    border-radius: 4px;\n    color: ${(props) => props.theme.colors.text.muted};\n    margin-left: 8px;\n    \n    &:hover {\n      color: ${(props) => props.theme.colors.text.red};\n    }\n\n    svg {\n      width: 16px;\n      height: 16px;\n      color: ${(props) => props.theme.text};\n    }\n  }\n  \n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleParams/index.js",
    "content": "import React, { useState, useMemo, useCallback } from 'react';\nimport { useDispatch } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport get from 'lodash/get';\nimport { moveResponseExampleParam, setResponseExampleParams } from 'providers/ReduxStore/slices/collections';\nimport EditableTable from 'components/EditableTable';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport BulkEditor from 'components/BulkEditor';\nimport InfoTip from 'components/InfoTip';\nimport StyledWrapper from './StyledWrapper';\n\nconst ResponseExampleParams = ({ editMode, item, collection, exampleUid }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n  const [isBulkEditMode, setIsBulkEditMode] = useState(false);\n\n  const params = useMemo(() => {\n    return item.draft\n      ? get(item, 'draft.examples', []).find((e) => e.uid === exampleUid)?.request?.params || []\n      : get(item, 'examples', []).find((e) => e.uid === exampleUid)?.request?.params || [];\n  }, [item, exampleUid]);\n\n  const queryParams = params.filter((param) => param.type === 'query');\n  const pathParams = params.filter((param) => param.type === 'path');\n\n  const handleQueryParamsChange = useCallback((updatedQueryParams) => {\n    if (!editMode) {\n      return;\n    }\n\n    // Merge updated query params with path params\n    const allParams = [...updatedQueryParams, ...pathParams];\n    dispatch(setResponseExampleParams({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: exampleUid,\n      params: allParams\n    }));\n  }, [editMode, dispatch, item.uid, collection.uid, exampleUid, pathParams]);\n\n  const handleQueryParamDrag = useCallback(({ updateReorderedItem }) => {\n    if (!editMode) {\n      return;\n    }\n\n    dispatch(moveResponseExampleParam({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: exampleUid,\n      updateReorderedItem\n    }));\n  }, [editMode, dispatch, item.uid, collection.uid, exampleUid]);\n\n  const handlePathParamsChange = useCallback((updatedPathParams) => {\n    if (!editMode) {\n      return;\n    }\n\n    // Merge updated path params with query params\n    const allParams = [...queryParams, ...updatedPathParams];\n    dispatch(setResponseExampleParams({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: exampleUid,\n      params: allParams\n    }));\n  }, [editMode, dispatch, item.uid, collection.uid, exampleUid, queryParams]);\n\n  const toggleBulkEditMode = () => {\n    setIsBulkEditMode(!isBulkEditMode);\n  };\n\n  const handleBulkParamsChange = (newParams) => {\n    if (!editMode) {\n      return;\n    }\n\n    // Merge bulk edited query params with path params\n    const allParams = [...newParams, ...pathParams];\n    dispatch(setResponseExampleParams({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: exampleUid,\n      params: allParams\n    }));\n  };\n\n  if (isBulkEditMode && editMode) {\n    return (\n      <StyledWrapper className=\"w-full mt-3\">\n        <BulkEditor\n          params={queryParams}\n          onChange={handleBulkParamsChange}\n          onToggle={toggleBulkEditMode}\n        />\n      </StyledWrapper>\n    );\n  }\n\n  const queryColumns = [\n    {\n      key: 'name',\n      name: 'Name',\n      isKeyField: true,\n      placeholder: 'Name',\n      width: '40%',\n      readOnly: !editMode,\n      render: ({ value, onChange }) => (\n        <SingleLineEditor\n          value={value || ''}\n          theme={storedTheme}\n          onSave={() => {}}\n          onChange={onChange}\n          onRun={() => {}}\n          collection={collection}\n          variablesAutocomplete={true}\n          readOnly={!editMode}\n          placeholder={!value ? 'Name' : ''}\n        />\n      )\n    },\n    {\n      key: 'value',\n      name: 'Value',\n      placeholder: 'Value',\n      width: '60%',\n      readOnly: !editMode,\n      render: ({ value, onChange }) => (\n        <SingleLineEditor\n          value={value || ''}\n          theme={storedTheme}\n          onSave={() => {}}\n          onChange={onChange}\n          onRun={() => {}}\n          collection={collection}\n          variablesAutocomplete={true}\n          readOnly={!editMode}\n          placeholder={!value ? 'Value' : ''}\n        />\n      )\n    }\n  ];\n\n  const pathColumns = [\n    {\n      key: 'name',\n      name: 'Name',\n      readOnly: true,\n      width: '40%'\n    },\n    {\n      key: 'value',\n      name: 'Value',\n      placeholder: 'Value',\n      width: '60%',\n      readOnly: !editMode,\n      render: ({ value, onChange }) => (\n        <SingleLineEditor\n          value={value || ''}\n          theme={storedTheme}\n          onSave={() => {}}\n          onChange={onChange}\n          onRun={() => {}}\n          collection={collection}\n          variablesAutocomplete={true}\n          readOnly={!editMode}\n          placeholder={!value ? 'Value' : ''}\n        />\n      )\n    }\n  ];\n\n  const defaultQueryRow = {\n    name: '',\n    value: '',\n    enabled: true,\n    type: 'query'\n  };\n\n  if (queryParams.length === 0 && pathParams.length === 0 && !editMode) {\n    return null;\n  }\n\n  return (\n    <StyledWrapper className=\"w-full mt-4\">\n      <div className=\"mb-3 title text-xs font-bold\">Query parameters</div>\n      <EditableTable\n        columns={queryColumns}\n        rows={queryParams || []}\n        onChange={handleQueryParamsChange}\n        defaultRow={defaultQueryRow}\n        reorderable={editMode}\n        onReorder={handleQueryParamDrag}\n        showAddRow={editMode}\n        showDelete={editMode}\n        disableCheckbox={!editMode}\n      />\n      {editMode && (\n        <div className=\"flex justify-end mt-2\">\n          <button\n            className=\"btn-action text-link select-none\"\n            onClick={toggleBulkEditMode}\n          >\n            Bulk Edit\n          </button>\n        </div>\n      )}\n      {pathParams && pathParams.length > 0 && (\n        <>\n          <div className=\"mb-3 title text-xs font-bold flex items-stretch mt-4\">\n            <span>Path parameters</span>\n            <InfoTip infotipId=\"path-param-InfoTip\">\n              <div>\n                Path variables are automatically added whenever the\n                <code className=\"font-mono mx-2\">:name</code>\n                template is used in the URL. <br /> For example:\n                <code className=\"font-mono mx-2\">\n                  https://example.com/v1/users/<span>:id</span>\n                </code>\n              </div>\n            </InfoTip>\n          </div>\n          <EditableTable\n            columns={pathColumns}\n            rows={pathParams}\n            onChange={handlePathParamsChange}\n            defaultRow={{}}\n            showCheckbox={false}\n            showDelete={false}\n            showAddRow={false}\n            reorderable={false}\n          />\n        </>\n      )}\n\n    </StyledWrapper>\n  );\n};\n\nexport default ResponseExampleParams;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleUrlBar/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .url-bar-container {\n    border: 1px solid ${(props) => props.theme.examples.urlBar.border};\n  }\n\n  .method {\n    color: #fff;\n  }\n\n  .method-get {\n    background-color: ${(props) => props.theme.request.methods.get};\n  }\n\n  .method-post {\n    background-color: ${(props) => props.theme.request.methods.post};\n  }\n\n  .method-put {\n    background-color: ${(props) => props.theme.request.methods.put};\n  }\n\n  .method-delete {\n    background-color: ${(props) => props.theme.request.methods.delete};\n  }\n\n  .method-patch {\n    background-color: ${(props) => props.theme.request.methods.patch};\n  }\n\n  .method-options {\n    background-color: ${(props) => props.theme.request.methods.options};\n  }\n\n  .method-head {\n    background-color: ${(props) => props.theme.request.methods.head};\n  }\n\n  .method-trace {\n    background-color: ${(props) => props.theme.request.methods.options};\n  }\n\n  .method-connect {\n    background-color: ${(props) => props.theme.request.methods.options};\n  }\n\n  .method-custom {\n    background-color: ${(props) => props.theme.colors.text.muted};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/ResponseExampleUrlBar/index.js",
    "content": "import React, { useMemo } from 'react';\nimport { useDispatch } from 'react-redux';\nimport { updateResponseExampleRequestUrl } from 'providers/ReduxStore/slices/collections';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport StyledWrapper from './StyledWrapper';\nimport get from 'lodash/get';\n\nconst ResponseExampleUrlBar = ({ item, collection, editMode, onSave, exampleUid }) => {\n  const dispatch = useDispatch();\n\n  const exampleData = useMemo(() => {\n    return item.draft ? get(item, 'draft.examples', []).find((e) => e.uid === exampleUid) : get(item, 'examples', []).find((e) => e.uid === exampleUid);\n  }, [item, exampleUid]);\n  const method = get(exampleData, 'request.method');\n  const url = get(exampleData, 'request.url');\n\n  const onChange = (value) => {\n    if (!editMode) {\n      return;\n    }\n\n    dispatch(updateResponseExampleRequestUrl({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: exampleUid,\n      request: { url: value }\n    }));\n  };\n\n  const getMethodClass = () => {\n    switch (method?.toUpperCase()) {\n      case 'GET':\n        return 'method-get';\n      case 'POST':\n        return 'method-post';\n      case 'PUT':\n        return 'method-put';\n      case 'DELETE':\n        return 'method-delete';\n      case 'PATCH':\n        return 'method-patch';\n      case 'OPTIONS':\n        return 'method-options';\n      case 'HEAD':\n        return 'method-head';\n      case 'OPTIONS':\n        return 'method-options';\n      case 'HEAD':\n        return 'method-head';\n      default:\n        return 'method-get';\n    };\n  };\n\n  return (\n    <StyledWrapper className=\"flex items-center\">\n      <div className=\"url-bar-container w-full flex p-2 text-xs rounded-md items-center justify-between\" data-testid=\"url-bar-container\">\n        <div className={`method flex text-xs items-center justify-center px-2 rounded h-6 flex-shrink-0 mr-2 overflow-hidden whitespace-nowrap font-medium uppercase ${getMethodClass()}`}>\n          {method || 'GET'}\n        </div>\n\n        <div\n          id=\"response-example-url\"\n          className=\"response-example-url flex items-center flex-1 h-6 min-w-0 overflow-hidden\"\n        >\n          <SingleLineEditor\n            value={url}\n            onSave={onSave}\n            onChange={onChange}\n            collection={collection}\n            highlightPathParams={true}\n            item={item}\n            readOnly={!editMode}\n          />\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default ResponseExampleUrlBar;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  font-size: ${(props) => props.theme.font.size.base};\n  height: 300px;\n\n  .body-mode-selector {\n    background: transparent;\n    border-radius: 3px;\n\n    .dropdown-item {\n      padding: 0.2rem 0.6rem !important;\n      padding-left: 1.5rem !important;\n    }\n\n    .label-item {\n      padding: 0.2rem 0.6rem !important;\n    }\n\n    .selected-body-mode {\n      color: ${(props) => props.theme.primary.text};\n    }\n  }\n\n  .caret {\n    color: rgb(140, 140, 140);\n    fill: rgb(140 140 140);\n  }\n\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleRequestPane/index.js",
    "content": "import React from 'react';\nimport ResponseExampleUrlBar from './ResponseExampleUrlBar';\nimport ResponseExampleParams from './ResponseExampleParams';\nimport ResponseExampleHeaders from './ResponseExampleHeaders';\nimport ResponseExampleBody from './ResponseExampleBody';\nimport StyledWrapper from './StyledWrapper';\nimport HeightBoundContainer from 'ui/HeightBoundContainer';\n\nconst ResponseExampleRequestPane = ({ item, collection, editMode, exampleUid, onSave }) => {\n  return (\n    <HeightBoundContainer>\n      <StyledWrapper className=\"flex flex-col h-full w-full\">\n        <ResponseExampleUrlBar\n          item={item}\n          collection={collection}\n          exampleUid={exampleUid}\n          editMode={editMode}\n          onSave={onSave}\n        />\n\n        <ResponseExampleParams\n          editMode={editMode}\n          item={item}\n          collection={collection}\n          exampleUid={exampleUid}\n        />\n\n        <ResponseExampleHeaders\n          editMode={editMode}\n          item={item}\n          collection={collection}\n          exampleUid={exampleUid}\n        />\n\n        <ResponseExampleBody\n          editMode={editMode}\n          item={item}\n          collection={collection}\n          exampleUid={exampleUid}\n          onSave={onSave}\n        />\n      </StyledWrapper>\n    </HeightBoundContainer>\n  );\n};\n\nexport default ResponseExampleRequestPane;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleResponsePane/ResponseExampleResponseContent/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n\n  /* CodeEditor container */\n  .code-editor-container {\n    flex: 1;\n    min-height: 300px;\n    height: 300px;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleResponsePane/ResponseExampleResponseContent/index.js",
    "content": "import React, { useMemo } from 'react';\nimport { useDispatch } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport { useSelector } from 'react-redux';\nimport get from 'lodash/get';\nimport { updateResponseExampleResponse } from 'providers/ReduxStore/slices/collections';\nimport CodeEditor from 'components/CodeEditor';\nimport { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror';\nimport StyledWrapper from './StyledWrapper';\n\nconst ResponseExampleResponseContent = ({ editMode, item, collection, exampleUid, onSave }) => {\n  const dispatch = useDispatch();\n  const { displayedTheme } = useTheme();\n  const preferences = useSelector((state) => state.app.preferences);\n\n  const response = useMemo(() => {\n    return item.draft ? get(item, 'draft.examples', []).find((e) => e.uid === exampleUid)?.response || {} : get(item, 'examples', []).find((e) => e.uid === exampleUid)?.response || {};\n  }, [item, exampleUid]);\n\n  const getResponseContent = () => {\n    if (!response) {\n      return '';\n    }\n\n    if (!response.body) {\n      return '';\n    }\n\n    return response.body.content;\n  };\n\n  const getCodeMirrorMode = () => {\n    if (!response) {\n      return null;\n    }\n\n    if (response.body && response.body.type) {\n      const bodyType = response.body.type;\n      if (bodyType === 'json') {\n        return 'application/ld+json';\n      } else if (bodyType === 'xml') {\n        return 'application/xml';\n      } else if (bodyType === 'html') {\n        return 'application/html';\n      } else if (bodyType === 'text') {\n        return 'application/text';\n      }\n    }\n\n    const contentType = response.headers?.find((h) => h.name?.toLowerCase() === 'content-type')?.value?.toLowerCase() || '';\n\n    return getCodeMirrorModeBasedOnContentType(contentType);\n  };\n\n  const onResponseEdit = (value) => {\n    if (editMode && item && collection && exampleUid) {\n      const currentBody = response.body || {};\n      dispatch(updateResponseExampleResponse({\n        itemUid: item.uid,\n        collectionUid: collection.uid,\n        exampleUid: exampleUid,\n        response: {\n          body: {\n            type: currentBody.type || 'text',\n            content: value\n          }\n        }\n      }));\n    }\n  };\n\n  return (\n    <StyledWrapper className=\"w-full px-4\">\n      <div className=\"code-editor-container\">\n        <CodeEditor\n          collection={collection}\n          item={item}\n          theme={displayedTheme}\n          font={get(preferences, 'font.codeFont', 'default')}\n          fontSize={get(preferences, 'font.codeFontSize')}\n          value={getResponseContent()}\n          onEdit={onResponseEdit}\n          onRun={() => {}}\n          onSave={onSave}\n          mode={getCodeMirrorMode()}\n          enableVariableHighlighting={false}\n          readOnly={!editMode}\n        />\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default ResponseExampleResponseContent;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleResponsePane/ResponseExampleResponseHeaders/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .btn-action {\n    background: none;\n    color: ${(props) => props.theme.colors.text.muted};\n    border: none;\n    cursor: pointer;\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-weight: 500;\n    transition: opacity 0.2s ease;\n    \n    &:hover {\n      opacity: 0.8;\n    }\n    \n    &:disabled {\n      opacity: 0.5;\n      cursor: not-allowed;\n    }\n  }\n  \n  tr {\n    position: relative;\n    \n    &:hover .delete-button {\n      opacity: 1;\n      visibility: visible;\n    }\n  }\n\n  .delete-button {\n    opacity: 0;\n    visibility: hidden;\n    transition: all 0.2s ease;\n    background: none;\n    border: none;\n    cursor: pointer;\n    padding: 4px;\n    border-radius: 4px;\n    color: ${(props) => props.theme.colors.text.muted};\n    margin-left: 8px;\n    \n    &:hover {\n      color: ${(props) => props.theme.colors.text.red};\n    }\n\n    svg {\n      width: 16px;\n      height: 16px;\n      color: ${(props) => props.theme.text};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleResponsePane/ResponseExampleResponseHeaders/index.js",
    "content": "import React, { useState, useMemo, useCallback } from 'react';\nimport { useDispatch } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport get from 'lodash/get';\nimport { moveResponseExampleHeader, setResponseExampleHeaders, updateResponseExampleResponse } from 'providers/ReduxStore/slices/collections';\nimport { getBodyType } from 'utils/responseBodyProcessor';\nimport EditableTable from 'components/EditableTable';\nimport SingleLineEditor from 'components/SingleLineEditor';\nimport BulkEditor from 'components/BulkEditor';\nimport { headers as StandardHTTPHeaders } from 'know-your-http-well';\nimport { MimeTypes } from 'utils/codemirror/autocompleteConstants';\nimport StyledWrapper from './StyledWrapper';\n\nconst headerAutoCompleteList = StandardHTTPHeaders.map((e) => e.header);\n\nconst ResponseExampleResponseHeaders = ({ editMode, item, collection, exampleUid }) => {\n  const dispatch = useDispatch();\n  const { storedTheme } = useTheme();\n  const [isBulkEditMode, setIsBulkEditMode] = useState(false);\n\n  const headers = useMemo(() => {\n    return item.draft ? get(item, 'draft.examples', []).find((e) => e.uid === exampleUid)?.response?.headers || [] : get(item, 'examples', []).find((e) => e.uid === exampleUid)?.response?.headers || [];\n  }, [item, exampleUid]);\n\n  const response = useMemo(() => {\n    return item.draft ? get(item, 'draft.examples', []).find((e) => e.uid === exampleUid)?.response || {} : get(item, 'examples', []).find((e) => e.uid === exampleUid)?.response || {};\n  }, [item, exampleUid]);\n\n  const handleHeadersChange = useCallback((updatedHeaders) => {\n    if (!editMode) {\n      return;\n    }\n\n    // Check if content-type header was updated\n    const contentTypeHeader = updatedHeaders.find((h) => h.name?.toLowerCase() === 'content-type');\n    const oldContentTypeHeader = headers.find((h) => h.name?.toLowerCase() === 'content-type');\n\n    if (contentTypeHeader && oldContentTypeHeader && contentTypeHeader.value !== oldContentTypeHeader.value) {\n      const newContentType = contentTypeHeader.value?.toLowerCase() || '';\n      const newBodyType = getBodyType(newContentType);\n      const currentBodyType = response.body?.type || 'text';\n\n      // Only update if the body type has changed\n      if (newBodyType !== currentBodyType) {\n        dispatch(updateResponseExampleResponse({\n          itemUid: item.uid,\n          collectionUid: collection.uid,\n          exampleUid: exampleUid,\n          response: {\n            body: {\n              type: newBodyType,\n              content: response.body?.content || ''\n            }\n          }\n        }));\n      }\n    }\n\n    dispatch(setResponseExampleHeaders({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: exampleUid,\n      headers: updatedHeaders\n    }));\n  }, [editMode, dispatch, item.uid, collection.uid, exampleUid, headers, response]);\n\n  const handleHeaderDrag = useCallback(({ updateReorderedItem }) => {\n    if (!editMode) {\n      return;\n    }\n\n    dispatch(moveResponseExampleHeader({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: exampleUid,\n      updateReorderedItem\n    }));\n  }, [editMode, dispatch, item.uid, collection.uid, exampleUid]);\n\n  const toggleBulkEditMode = () => {\n    setIsBulkEditMode(!isBulkEditMode);\n  };\n\n  const handleBulkHeadersChange = (newHeaders) => {\n    if (!editMode) {\n      return;\n    }\n    const cleanedHeaders = newHeaders.map((header) => ({\n      uid: header.uid,\n      name: header.name,\n      value: header.value\n    }));\n\n    dispatch(setResponseExampleHeaders({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: exampleUid,\n      headers: cleanedHeaders\n    }));\n  };\n\n  if (isBulkEditMode && editMode) {\n    // Ensure all headers have enabled: true for bulk edit display\n    const headersForBulkEdit = headers.map((header) => ({\n      ...header,\n      enabled: true\n    }));\n    return (\n      <StyledWrapper className=\"w-full overflow-auto\">\n        <BulkEditor\n          params={headersForBulkEdit}\n          onChange={handleBulkHeadersChange}\n          onToggle={toggleBulkEditMode}\n        />\n      </StyledWrapper>\n    );\n  }\n\n  const columns = [\n    {\n      key: 'name',\n      name: 'Key',\n      isKeyField: true,\n      placeholder: 'Key',\n      width: '40%',\n      readOnly: !editMode,\n      render: ({ value, onChange }) => (\n        <SingleLineEditor\n          value={value || ''}\n          theme={storedTheme}\n          onSave={() => {}}\n          onChange={(newValue) => onChange(newValue.replace(/[\\r\\n]/g, ''))}\n          autocomplete={headerAutoCompleteList}\n          onRun={() => {}}\n          collection={collection}\n          readOnly={!editMode}\n          placeholder={!value ? 'Key' : ''}\n        />\n      )\n    },\n    {\n      key: 'value',\n      name: 'Value',\n      placeholder: 'Value',\n      width: '60%',\n      readOnly: !editMode,\n      render: ({ value, onChange }) => (\n        <SingleLineEditor\n          value={value || ''}\n          theme={storedTheme}\n          onSave={() => {}}\n          onChange={onChange}\n          onRun={() => {}}\n          autocomplete={MimeTypes}\n          allowNewlines={true}\n          collection={collection}\n          item={item}\n          readOnly={!editMode}\n          placeholder={!value ? 'Value' : ''}\n        />\n      )\n    }\n  ];\n\n  const defaultRow = {\n    name: '',\n    value: ''\n  };\n\n  return (\n    <StyledWrapper className=\"w-full px-4\">\n      <EditableTable\n        columns={columns}\n        rows={headers || []}\n        onChange={handleHeadersChange}\n        defaultRow={defaultRow}\n        reorderable={editMode}\n        onReorder={handleHeaderDrag}\n        showAddRow={editMode}\n        showCheckbox={false}\n        showDelete={editMode}\n      />\n      {editMode && (\n        <div className=\"flex justify-end mt-2 flex-shrink-0\">\n          <button\n            className=\"btn-action text-link select-none\"\n            onClick={toggleBulkEditMode}\n          >\n            Bulk Edit\n          </button>\n        </div>\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default ResponseExampleResponseHeaders;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleResponsePane/ResponseExampleStatusInput/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  position: relative;\n  display: inline-block;\n\n  .response-status-input {\n    background: ${(props) => props.theme.requestTabPanel.url.bg};\n    border: 1px solid ${(props) => props.theme.border.border1};\n    border-radius: 3px;\n    padding: 0.35rem 0.6rem;\n    font-size: ${(props) => props.theme.font.size.base};\n    font-weight: 500;\n    color: ${(props) => props.theme.text.primary};\n    min-width: 120px;\n    transition: all 0.2s ease;\n\n    &:focus {\n      outline: none;\n      border-color: ${(props) => props.theme.colors.primary};\n      box-shadow: 0 0 0 2px ${(props) => props.theme.colors.primary}20;\n    }\n\n    &::placeholder {\n      color: ${(props) => props.theme.text.muted};\n    }\n\n    &.text-ok {\n      color: ${(props) => props.theme.colors.success};\n    }\n\n    &.text-warning {\n      color: ${(props) => props.theme.colors.warning};\n    }\n\n    &.text-error {\n      color: ${(props) => props.theme.colors.error};\n    }\n  }\n\n  .status-suggestions {\n    position: absolute;\n    top: 100%;\n    left: 0;\n    right: 0;\n    background: ${(props) => props.theme.dropdown.bg};\n    border: 1px solid ${(props) => props.theme.border.border1};\n    border-top: none;\n    border-radius: 0 0 3px 3px;\n    box-shadow: ${(props) => props.theme.dropdown.shadow};\n    z-index: 1000;\n    max-height: 200px;\n    overflow-y: auto;\n    overflow-x: hidden;\n\n    .suggestion-item {\n      display: flex;\n      align-items: center;\n      padding: 0.35rem 0.6rem;\n      margin: 0;\n      cursor: pointer;\n      transition: background-color 0.15s ease;\n      font-size: ${(props) => props.theme.font.size.base};\n      color: ${(props) => props.theme.text};\n      width: 100%;\n      box-sizing: border-box;\n\n      &:hover:not(:disabled) {\n        background-color: ${(props) => props.theme.dropdown.hoverBg};\n      }\n\n      .status {\n        font-weight: 500;\n        color: inherit;\n        margin-right: 0.5rem;\n        min-width: 40px;\n        flex-shrink: 0;\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleResponsePane/ResponseExampleStatusInput/index.js",
    "content": "import React, { useState, useRef, useEffect } from 'react';\nimport { useDispatch } from 'react-redux';\nimport { updateResponseExampleStatusCode, updateResponseExampleStatusText } from 'providers/ReduxStore/slices/collections';\nimport statusCodePhraseMap from 'components/ResponsePane/StatusCode/get-status-code-phrase';\nimport StyledWrapper from './StyledWrapper';\n\nconst ResponseExampleStatusInput = ({ item, collection, exampleUid, status, statusText }) => {\n  const dispatch = useDispatch();\n  const [showSuggestions, setShowSuggestions] = useState(false);\n  const [filteredSuggestions, setFilteredSuggestions] = useState([]);\n  const [inputValue, setInputValue] = useState('');\n  const inputRef = useRef(null);\n  const suggestionsRef = useRef(null);\n\n  // Initialize inputValue from Redux state on mount or when prop changes\n  useEffect(() => {\n    const displayValue = () => {\n      if (status && statusText) {\n        return `${status} ${statusText}`;\n      } else if (status) {\n        return status;\n      }\n      return '';\n    };\n    setInputValue(displayValue());\n  }, [status, statusText]);\n\n  // Create suggestions from status code map\n  const suggestions = Object.entries(statusCodePhraseMap).map(([code, phrase]) => ({\n    code: parseInt(code),\n    phrase,\n    display: `${code} ${phrase}`\n  }));\n\n  const handleInputChange = (e) => {\n    const value = e.target.value;\n\n    // Update local state to allow typing freely (including spaces)\n    setInputValue(value);\n\n    if (value.trim()) {\n      // Filter suggestions based on input\n      const filtered = suggestions.filter((suggestion) =>\n        suggestion.display.toLowerCase().includes(value.toLowerCase())\n        || suggestion.code.toString().includes(value)\n        || suggestion.phrase.toLowerCase().includes(value.toLowerCase()));\n      setFilteredSuggestions(filtered);\n      setShowSuggestions(true);\n    } else {\n      setShowSuggestions(false);\n      setFilteredSuggestions([]);\n    }\n  };\n\n  const handleKeyDown = (e) => {\n    // Handle Cmd+S to save status to Redux\n    if ((e.metaKey || e.ctrlKey) && e.key === 's') {\n      e.preventDefault();\n      parseAndSaveStatus(inputValue);\n    }\n\n    if (!showSuggestions) return;\n\n    switch (e.key) {\n      case 'Escape':\n        setShowSuggestions(false);\n        break;\n    }\n  };\n\n  const selectSuggestion = (suggestion) => {\n    setShowSuggestions(false);\n\n    // Update local input value\n    const newValue = `${suggestion.code} ${suggestion.phrase}`;\n    setInputValue(newValue);\n\n    // Save the status and statusText\n    dispatch(updateResponseExampleStatusCode({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: exampleUid,\n      statusCode: String(suggestion.code)\n    }));\n\n    dispatch(updateResponseExampleStatusText({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: exampleUid,\n      statusText: suggestion.phrase\n    }));\n  };\n\n  const parseAndSaveStatus = (value) => {\n    // Find the first space\n    const firstSpaceIndex = value.indexOf(' ');\n\n    let statusCode, statusText;\n\n    if (firstSpaceIndex === -1) {\n      // No space found, treat entire value as status code\n      statusCode = value;\n      statusText = '';\n    } else {\n      // Split on first space only, preserving all other spaces\n      statusCode = value.substring(0, firstSpaceIndex);\n      statusText = value.substring(firstSpaceIndex + 1);\n    }\n\n    // Save both as strings - no validation needed\n    dispatch(updateResponseExampleStatusCode({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: exampleUid,\n      statusCode: statusCode\n    }));\n\n    dispatch(updateResponseExampleStatusText({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: exampleUid,\n      statusText: statusText\n    }));\n\n    setShowSuggestions(false);\n  };\n\n  const handleBlur = (e) => {\n    // Check if the blur is caused by clicking on a suggestion\n    const relatedTarget = e.relatedTarget;\n    if (relatedTarget && relatedTarget.closest('.status-suggestions')) {\n      return; // Don't close suggestions if clicking on them\n    }\n\n    // Save the status to Redux\n    parseAndSaveStatus(inputValue);\n\n    // Small delay to allow click events on suggestions\n    setTimeout(() => {\n      setShowSuggestions(false);\n    }, 150);\n  };\n\n  const handleFocus = () => {\n    if (inputValue.trim()) {\n      const filtered = suggestions.filter((suggestion) =>\n        suggestion.display.toLowerCase().includes(inputValue.toLowerCase())\n        || suggestion.code.toString().includes(inputValue)\n        || suggestion.phrase.toLowerCase().includes(inputValue.toLowerCase()));\n      setFilteredSuggestions(filtered);\n      setShowSuggestions(true);\n    }\n  };\n\n  const getStatusClass = (status) => {\n    const numStatus = parseInt(status);\n    if (!isNaN(numStatus)) {\n      if (numStatus >= 200 && numStatus < 300) return 'text-ok';\n      if (numStatus >= 300 && numStatus < 400) return 'text-warning';\n      if (numStatus >= 400) return 'text-error';\n    }\n    return 'text-ok';\n  };\n\n  return (\n    <StyledWrapper className=\"relative\">\n      <input\n        ref={inputRef}\n        type=\"text\"\n        value={inputValue}\n        onChange={handleInputChange}\n        onKeyDown={handleKeyDown}\n        onBlur={handleBlur}\n        onFocus={handleFocus}\n        placeholder=\"e.g., 200 OK, 404 Unknown, 999 Custom Error\"\n        className={`response-status-input ${getStatusClass(status)}`}\n        data-testid=\"response-status-input\"\n      />\n\n      {showSuggestions && filteredSuggestions.length > 0 && (\n        <div\n          ref={suggestionsRef}\n          className=\"status-suggestions\"\n          data-testid=\"status-suggestions\"\n          onMouseDown={(e) => e.preventDefault()} // Prevent input blur when clicking on suggestions\n        >\n          {filteredSuggestions.map((suggestion) => (\n            <div\n              key={suggestion.code}\n              className=\"suggestion-item\"\n              onClick={(e) => {\n                e.preventDefault();\n                e.stopPropagation();\n                selectSuggestion(suggestion);\n              }}\n              onMouseDown={(e) => e.preventDefault()}\n              data-testid={`suggestion-${suggestion.code}`}\n            >\n              <span className=\"status\">{`${suggestion.code} ${suggestion.phrase}`}</span>\n            </div>\n          ))}\n        </div>\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default ResponseExampleStatusInput;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleResponsePane/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  div.tabs {\n    div.tab {\n      padding: 6px 0px;\n      border: none;\n      border-bottom: solid 2px transparent;\n      margin-right: ${(props) => props.theme.tabs.marginRight};\n      color: ${(props) => props.theme.colors.text.subtext0};\n      cursor: pointer;\n\n      &:focus,\n      &:active,\n      &:focus-within,\n      &:focus-visible,\n      &:target {\n        outline: none !important;\n        box-shadow: none !important;\n      }\n\n      &:hover {\n        color: ${(props) => props.theme.tabs.active.color} !important;\n      }\n\n      &.active {\n        font-weight: ${(props) => props.theme.tabs.active.fontWeight} !important;\n        color: ${(props) => props.theme.tabs.active.color} !important;\n        border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;\n      }\n    }\n  }\n\n  .some-tests-failed {\n    color: ${(props) => props.theme.colors.text.danger} !important;\n  }\n\n  .all-tests-passed {\n    color: ${(props) => props.theme.colors.text.green} !important;\n  }\n\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleResponsePane/index.js",
    "content": "import React, { useMemo } from 'react';\nimport get from 'lodash/get';\nimport find from 'lodash/find';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';\nimport Tab from 'components/Tab';\nimport ResponseLayoutToggle from 'components/ResponsePane/ResponseLayoutToggle';\nimport StatusCode from 'components/ResponsePane/StatusCode';\nimport ResponseExampleResponseContent from './ResponseExampleResponseContent';\nimport ResponseExampleResponseHeaders from './ResponseExampleResponseHeaders';\nimport ResponseExampleStatusInput from './ResponseExampleStatusInput';\nimport StyledWrapper from './StyledWrapper';\nimport HeightBoundContainer from 'ui/HeightBoundContainer';\n\nconst ResponseExampleResponsePane = ({ item, collection, editMode, exampleUid, onSave }) => {\n  const dispatch = useDispatch();\n  const tabs = useSelector((state) => state.tabs.tabs);\n  const activeTabUid = useSelector((state) => state.tabs.activeTabUid);\n\n  // Get the focused tab for reading persisted tab state\n  const focusedTab = find(tabs, (t) => t.uid === activeTabUid);\n  const activeTab = focusedTab?.responsePaneTab || 'response';\n\n  const selectTab = (tab) => {\n    dispatch(updateResponsePaneTab({\n      uid: exampleUid,\n      responsePaneTab: tab\n    }));\n  };\n\n  const exampleData = useMemo(() => {\n    return item.draft ? get(item, 'draft.examples', []).find((e) => e.uid === exampleUid) || {} : get(item, 'examples', []).find((e) => e.uid === exampleUid) || {};\n  }, [item, exampleUid]);\n\n  const getTabPanel = (tab) => {\n    switch (tab) {\n      case 'response': {\n        return (\n          <ResponseExampleResponseContent\n            editMode={editMode}\n            item={item}\n            collection={collection}\n            exampleUid={exampleUid}\n            onSave={onSave}\n          />\n        );\n      }\n      case 'headers': {\n        return (\n          <ResponseExampleResponseHeaders\n            editMode={editMode}\n            item={item}\n            collection={collection}\n            exampleUid={exampleUid}\n            onSave={onSave}\n          />\n        );\n      }\n      default: {\n        return <div>404 | Not found</div>;\n      }\n    }\n  };\n\n  const tabConfig = [\n    {\n      name: 'response',\n      label: 'Response'\n    },\n    {\n      name: 'headers',\n      label: 'Headers',\n      count: (exampleData?.response?.headers || []).length\n    }\n  ];\n\n  return (\n    <StyledWrapper className=\"flex flex-col h-full relative\">\n      <div className=\"flex flex-wrap items-center tabs mb-4 px-4\" role=\"tablist\">\n        {tabConfig.map((tab) => (\n          <Tab\n            key={tab.name}\n            name={tab.name}\n            label={tab.label}\n            isActive={activeTab === tab.name}\n            onClick={selectTab}\n            count={tab.count}\n          />\n        ))}\n\n        <div className=\"flex flex-grow justify-end items-center\">\n          <ResponseLayoutToggle />\n          {editMode ? (\n            <ResponseExampleStatusInput\n              item={item}\n              collection={collection}\n              exampleUid={exampleUid}\n              status={exampleData?.response?.status}\n              statusText={exampleData?.response?.statusText}\n            />\n          ) : (\n            exampleData?.response?.status && (\n              <StatusCode status={exampleData.response.status} statusText={exampleData.response.statusText} />\n            )\n          )}\n        </div>\n      </div>\n\n      <section className=\"flex w-full flex-1 relative\">\n        <HeightBoundContainer>\n          {getTabPanel(activeTab)}\n        </HeightBoundContainer>\n      </section>\n    </StyledWrapper>\n  );\n};\n\nexport default ResponseExampleResponsePane;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleTopBar/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  background-color: ${(props) => props.theme.bg};\n  border-bottom: 1px solid ${(props) => props.theme.examples.border};\n\n  .response-example-title {\n    color: ${(props) => props.theme.text};\n  }\n\n  .response-example-description {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .primary-btn {\n    background-color: ${(props) => props.theme.examples.buttonColor};\n    border: 1px solid ${(props) => props.theme.examples.buttonColor};\n    color: white;\n  }\n\n  .secondary-btn {\n    background-color: transparent;\n    color: ${(props) => props.theme.text};\n    border: 1px solid ${(props) => props.theme.examples.border};\n\n    svg {\n      color: ${(props) => props.theme.text};\n    }\n  }\n\n  .example-input-label {\n    display: block;\n    font-size: ${(props) => props.theme.font.size.base};\n    font-weight: 500;\n    color: ${(props) => props.theme.text};\n    margin-bottom: 4px;\n  }\n\n  .example-input {\n    width: 100%;\n    padding: 8px 12px;\n    border: 1px solid ${(props) => props.theme.examples.border};\n    border-radius: 6px;\n    background-color: transparent;\n    color: ${(props) => props.theme.text};\n    font-family: inherit;\n    font-size: ${(props) => props.theme.font.size.base};\n    line-height: 1.5;\n    transition: all 0.2s ease;\n    outline: none;\n    box-shadow: none;\n\n    &::placeholder {\n      color: ${(props) => props.theme.input.placeholder.color};\n      opacity: ${(props) => props.theme.input.placeholder.opacity};\n    }\n\n    &:focus {\n      border-color: ${(props) => props.theme.input.focusBorder};\n      outline: none;\n      box-shadow: none;\n    }\n\n    &:disabled {\n      background-color: transparent;\n      color: ${(props) => props.theme.text};\n      cursor: not-allowed;\n      opacity: 0.6;\n    }\n  }\n\n  .example-input-description {\n    font-size: ${(props) => props.theme.font.size.base};\n    line-height: 1.6;\n    resize: none;\n    min-height: 80px;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/ResponseExampleTopBar/index.js",
    "content": "import React, { useMemo } from 'react';\nimport { useDispatch } from 'react-redux';\nimport IconEdit from 'components/Icons/IconEdit';\nimport { IconCode, IconDeviceFloppy } from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\nimport { useTheme } from 'providers/Theme';\nimport TruncatedText from 'components/TruncatedText';\nimport { updateResponseExampleName, updateResponseExampleDescription } from 'providers/ReduxStore/slices/collections';\nimport get from 'lodash/get';\nimport Button from 'ui/Button';\n\nconst ResponseExampleTopBar = ({\n  item,\n  collection,\n  exampleUid,\n  editMode,\n  onEditToggle,\n  onSave,\n  onCancel,\n  onGenerateCode\n}) => {\n  const { theme } = useTheme();\n  const dispatch = useDispatch();\n\n  const example = useMemo(() => {\n    return item.draft ? get(item, 'draft.examples', []).find((e) => e.uid === exampleUid) : get(item, 'examples', []).find((e) => e.uid === exampleUid);\n  }, [item.draft, item.examples, item, exampleUid]);\n\n  const handleGenerateCode = () => {\n    if (onGenerateCode) {\n      onGenerateCode({\n        ...example,\n        isExample: true,\n        exampleUid: exampleUid\n      });\n    }\n  };\n\n  const handleNameChange = (e) => {\n    // Validate required fields before dispatching\n    if (!item?.uid) {\n      console.error('item.uid is missing');\n      return;\n    }\n    if (!collection?.uid) {\n      console.error('collection.uid is missing');\n      return;\n    }\n    if (!exampleUid) {\n      console.error('exampleUid is missing');\n      return;\n    }\n\n    dispatch(updateResponseExampleName({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: exampleUid,\n      name: e.target.value\n    }));\n  };\n\n  const handleDescriptionChange = (e) => {\n    // Validate required fields before dispatching\n    if (!item?.uid) {\n      console.error('item.uid is missing');\n      return;\n    }\n    if (!collection?.uid) {\n      console.error('collection.uid is missing');\n      return;\n    }\n    if (!exampleUid) {\n      console.error('exampleUid is missing');\n      return;\n    }\n\n    dispatch(updateResponseExampleDescription({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: exampleUid,\n      description: e.target.value\n    }));\n  };\n\n  const handleSave = () => {\n    // Call the parent save handler\n    if (onSave) {\n      onSave();\n    }\n  };\n\n  const handleCancel = () => {\n    if (onCancel) {\n      onCancel();\n    }\n  };\n\n  if (!example || !exampleUid) {\n    return null;\n  }\n\n  if (editMode) {\n    return (\n      <StyledWrapper className=\"p-4\">\n        <div className=\"max-w-full\">\n          <div className=\"flex items-start justify-between gap-6 md:flex-row flex-col\">\n            <div className=\"flex-1 min-w-0\">\n              <div className=\"space-y-3\">\n                <div>\n                  <input\n                    type=\"text\"\n                    value={example?.name || ''}\n                    onChange={handleNameChange}\n                    className=\"example-input example-input-name\"\n                    placeholder=\"Enter example name\"\n                    autoFocus\n                    data-testid=\"response-example-name-input\"\n                  />\n                </div>\n                <div>\n                  <textarea\n                    value={example?.description || ''}\n                    onChange={handleDescriptionChange}\n                    className=\"example-input example-input-description\"\n                    placeholder=\"Enter example description\"\n                    rows={3}\n                    data-testid=\"response-example-description-input\"\n                  />\n                </div>\n              </div>\n            </div>\n\n            <div className=\"flex items-center gap-3 flex-shrink-0 md:w-auto w-full md:justify-end\">\n              <Button\n                color=\"secondary\"\n                onClick={handleCancel}\n                data-testid=\"response-example-cancel-btn\"\n              >\n                Cancel\n              </Button>\n              <Button\n                color=\"primary\"\n                style={{ padding: '6px 12px' }}\n                icon={<IconDeviceFloppy size={16} />}\n                onClick={handleSave}\n                data-testid=\"response-example-save-btn\"\n              >\n                Save\n              </Button>\n            </div>\n          </div>\n        </div>\n      </StyledWrapper>\n    );\n  }\n\n  // Default view mode\n  return (\n    <StyledWrapper className=\"p-4\">\n      <div className=\"max-w-full\">\n        <div className=\"flex items-center justify-between gap-6 md:flex-row flex-col\">\n          <div className=\"flex-1 min-w-0\">\n            <h2 className=\"response-example-title font-medium leading-tight\" data-testid=\"response-example-title\">\n              <span className=\"opacity-60\">{item.name}</span>\n              {' / '}\n              <span>{example.name}</span>\n            </h2>\n            {example.description && example.description.trim().length > 0 && (\n              <TruncatedText\n                text={example.description}\n                maxLines={2}\n                className=\"response-example-description-container\"\n                textClassName=\"response-example-description leading-relaxed max-w-fit\"\n                buttonClassName=\"text-blue-600 hover:text-blue-800 font-medium\"\n                viewMoreText=\"View More\"\n                viewLessText=\"View Less\"\n                dataTestId=\"response-example-description\"\n              />\n            )}\n          </div>\n\n          <div className=\"flex items-center gap-3 flex-shrink-0 md:w-auto w-full md:justify-end\">\n            <Button\n              color=\"secondary\"\n              size=\"sm\"\n              icon={<IconCode size={16} color={theme.examples.buttonIconColor} />}\n              onClick={handleGenerateCode}\n              title=\"Generate Code\"\n              data-testid=\"response-example-generate-code-btn\"\n            />\n            <Button\n              color=\"secondary\"\n              size=\"sm\"\n              icon={<IconEdit size={16} color={theme.examples.buttonIconColor} />}\n              onClick={onEditToggle}\n              data-testid=\"response-example-edit-btn\"\n            >\n              Edit Example\n            </Button>\n          </div>\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default ResponseExampleTopBar;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  &.dragging {\n    cursor: col-resize;\n\n    &.vertical-layout {\n      cursor: row-resize;\n    }\n  }\n\n  .request-pane {\n    flex-shrink: 0;\n  }\n\n  .response-pane {\n    min-width: 0;\n  }\n\n  div.dragbar-wrapper {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 10px;\n    min-width: 10px;\n    padding: 0;\n    cursor: col-resize;\n    background: transparent;\n    position: relative;\n\n    div.dragbar-handle {\n      display: flex;\n      height: 100%;\n      width: 1px;\n      border-left: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.border};\n    }\n\n    &:hover div.dragbar-handle {\n      border-left: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.activeBorder};\n    }\n  }\n\n  &.vertical-layout {\n    .request-pane {\n      padding-bottom: 0.5rem;\n    }\n\n    .response-pane {\n      padding-top: 0.5rem;\n    }\n\n    div.dragbar-wrapper {\n      width: 100%;\n      height: 10px;\n      cursor: row-resize;\n      padding: 0 1rem;\n      position: relative;\n\n      div.dragbar-handle {\n        width: 100%;\n        height: 1px;\n        border-left: none;\n        border-top: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.border};\n      }\n\n      &:hover div.dragbar-handle {\n        border-left: none;\n        border-top: solid 1px ${(props) => props.theme.requestTabPanel.dragbar.activeBorder};\n      }\n    }\n  }\n\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponseExample/index.js",
    "content": "import React, { useState, useEffect, useRef } from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { updateRequestPaneTabWidth } from 'providers/ReduxStore/slices/tabs';\nimport { saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport { cancelResponseExampleEdit } from 'providers/ReduxStore/slices/collections';\nimport ResponseExampleTopBar from './ResponseExampleTopBar';\nimport ResponseExampleRequestPane from './ResponseExampleRequestPane';\nimport ResponseExampleResponsePane from './ResponseExampleResponsePane';\nimport GenerateCodeItem from 'components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem';\nimport StyledWrapper from './StyledWrapper';\n\nconst MIN_LEFT_PANE_WIDTH = 300;\nconst MIN_RIGHT_PANE_WIDTH = 350;\nconst MIN_TOP_PANE_HEIGHT = 150;\nconst MIN_BOTTOM_PANE_HEIGHT = 150;\n\nconst ResponseExample = ({ item, collection, example }) => {\n  const dispatch = useDispatch();\n  const preferences = useSelector((state) => state.app.preferences);\n  const screenWidth = useSelector((state) => state.app.screenWidth);\n  const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth);\n  const isVerticalLayout = preferences?.layout?.responsePaneOrientation === 'vertical';\n\n  const [leftPaneWidth, setLeftPaneWidth] = useState((screenWidth - leftSidebarWidth) / 2.2);\n  const [topPaneHeight, setTopPaneHeight] = useState(MIN_TOP_PANE_HEIGHT);\n  const [dragging, setDragging] = useState(false);\n  const [editMode, setEditMode] = useState(false);\n  const [showGenerateCodeModal, setShowGenerateCodeModal] = useState(false);\n  const dragOffset = useRef({ x: 0, y: 0 });\n  const mainSectionRef = useRef(null);\n\n  const handleMouseMove = (e) => {\n    if (dragging && mainSectionRef.current) {\n      e.preventDefault();\n      const mainRect = mainSectionRef.current.getBoundingClientRect();\n\n      if (isVerticalLayout) {\n        const newHeight = e.clientY - mainRect.top - dragOffset.current.y;\n        if (newHeight < MIN_TOP_PANE_HEIGHT || newHeight > mainRect.height - MIN_BOTTOM_PANE_HEIGHT) {\n          return;\n        }\n        setTopPaneHeight(newHeight);\n      } else {\n        const newWidth = e.clientX - mainRect.left - dragOffset.current.x;\n        if (newWidth < MIN_LEFT_PANE_WIDTH || newWidth > mainRect.width - MIN_RIGHT_PANE_WIDTH) {\n          return;\n        }\n        setLeftPaneWidth(newWidth);\n      }\n    }\n  };\n\n  const handleMouseUp = (e) => {\n    if (dragging && mainSectionRef.current) {\n      e.preventDefault();\n      setDragging(false);\n      if (!isVerticalLayout) {\n        const mainRect = mainSectionRef.current.getBoundingClientRect();\n        dispatch(updateRequestPaneTabWidth({\n          uid: item.uid,\n          requestPaneWidth: e.clientX - mainRect.left\n        }));\n      }\n    }\n  };\n\n  const handleDragbarMouseDown = (e) => {\n    e.preventDefault();\n    setDragging(true);\n\n    if (isVerticalLayout) {\n      const dragBar = e.currentTarget;\n      const dragBarRect = dragBar.getBoundingClientRect();\n      dragOffset.current.y = e.clientY - dragBarRect.top;\n    } else {\n      const dragBar = e.currentTarget;\n      const dragBarRect = dragBar.getBoundingClientRect();\n      dragOffset.current.x = e.clientX - dragBarRect.left;\n    }\n  };\n\n  useEffect(() => {\n    document.addEventListener('mouseup', handleMouseUp);\n    document.addEventListener('mousemove', handleMouseMove);\n\n    return () => {\n      document.removeEventListener('mouseup', handleMouseUp);\n      document.removeEventListener('mousemove', handleMouseMove);\n    };\n  }, [dragging]);\n\n  const handleEditToggle = () => {\n    setEditMode(!editMode);\n  };\n\n  const handleSave = () => {\n    if (item && collection) {\n      dispatch(saveRequest(item.uid, collection.uid));\n      setEditMode(false);\n    }\n  };\n\n  const handleCancel = () => {\n    if (item && collection && example?.uid) {\n      dispatch(cancelResponseExampleEdit({\n        itemUid: item.uid,\n        collectionUid: collection.uid,\n        exampleUid: example.uid\n      }));\n    }\n    setEditMode(false);\n  };\n\n  const handleGenerateCode = (exampleData) => {\n    setShowGenerateCodeModal(true);\n  };\n\n  const handleCloseGenerateCodeModal = () => {\n    setShowGenerateCodeModal(false);\n  };\n\n  const handleTryExample = (example) => {\n    // TODO: Implement try example functionality\n  };\n\n  // Update width when screen width or sidebar width changes\n  useEffect(() => {\n    if (mainSectionRef.current) {\n      const mainRect = mainSectionRef.current.getBoundingClientRect();\n      if (isVerticalLayout) {\n        // In vertical mode, set leftPaneWidth to full container width\n        setLeftPaneWidth(mainRect.width);\n      } else {\n        // In horizontal mode, set to roughly half width\n        setLeftPaneWidth((screenWidth - leftSidebarWidth) / 2.2);\n      }\n    }\n  }, [isVerticalLayout, screenWidth, leftSidebarWidth]);\n\n  // Keyboard shortcut support for Ctrl/Cmd+S\n  useEffect(() => {\n    const handleKeyDown = (e) => {\n      if ((e.ctrlKey || e.metaKey) && e.key === 's') {\n        e.preventDefault();\n        if (editMode && item && collection) {\n          handleSave();\n        }\n      }\n    };\n\n    document.addEventListener('keydown', handleKeyDown);\n    return () => {\n      document.removeEventListener('keydown', handleKeyDown);\n    };\n  }, [editMode, item, collection]);\n\n  return (\n    <>\n      <StyledWrapper className={`flex flex-col flex-grow relative ${dragging ? 'dragging' : ''} ${isVerticalLayout ? 'vertical-layout' : ''}`}>\n        <ResponseExampleTopBar\n          item={item}\n          collection={collection}\n          exampleUid={example.uid}\n          editMode={editMode}\n          onEditToggle={handleEditToggle}\n          onSave={handleSave}\n          onCancel={handleCancel}\n          onGenerateCode={handleGenerateCode}\n          onTryExample={handleTryExample}\n        />\n        <section ref={mainSectionRef} className={`main wrapper flex mt-4 ${isVerticalLayout ? 'flex-col' : ''} flex-grow pb-4 relative overflow-auto scrollbar-hover`}>\n          <section className=\"request-pane\">\n            <div\n              className=\"px-4 h-full\"\n              style={isVerticalLayout ? {\n                height: `${Math.max(topPaneHeight, MIN_TOP_PANE_HEIGHT)}px`,\n                minHeight: `${MIN_TOP_PANE_HEIGHT}px`,\n                width: '100%'\n              } : {\n                width: `${Math.max(leftPaneWidth, MIN_LEFT_PANE_WIDTH)}px`\n              }}\n            >\n              <ResponseExampleRequestPane\n                item={item}\n                collection={collection}\n                example={example}\n                editMode={editMode}\n                exampleUid={example?.uid}\n                onSave={handleSave}\n              />\n            </div>\n          </section>\n\n          <div className=\"dragbar-wrapper\" onMouseDown={handleDragbarMouseDown}>\n            <div className=\"dragbar-handle\" />\n          </div>\n\n          <section className=\"response-pane flex-grow overflow-x-auto\">\n            <ResponseExampleResponsePane\n              item={item}\n              collection={collection}\n              example={example}\n              editMode={editMode}\n              exampleUid={example?.uid}\n              onSave={handleSave}\n            />\n          </section>\n        </section>\n      </StyledWrapper>\n\n      {showGenerateCodeModal && (\n        <GenerateCodeItem\n          collectionUid={collection.uid}\n          item={item}\n          onClose={handleCloseGenerateCodeModal}\n          isExample={true}\n          exampleUid={example.uid}\n        />\n      )}\n    </>\n  );\n};\n\nexport default ResponseExample;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ClearTimeline/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  font-size: ${(props) => props.theme.font.size.base};\n  color: ${(props) => props.theme.requestTabPanel.responseStatus};\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ClearTimeline/index.js",
    "content": "import React from 'react';\nimport { useDispatch } from 'react-redux';\nimport StyledWrapper from './StyledWrapper';\nimport { clearRequestTimeline } from 'providers/ReduxStore/slices/collections/index';\n\nconst ClearTimeline = ({ collection, item }) => {\n  const dispatch = useDispatch();\n\n  const clearResponse = () =>\n    dispatch(\n      clearRequestTimeline({\n        itemUid: item.uid,\n        collectionUid: collection.uid\n      })\n    );\n\n  return (\n    <StyledWrapper className=\"flex items-center\">\n      <button type=\"button\" onClick={clearResponse} className=\"text-link hover:underline whitespace-nowrap\" title=\"Clear Timeline\">\n        Clear Timeline\n      </button>\n    </StyledWrapper>\n  );\n};\n\nexport default ClearTimeline;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/GrpcResponsePane/GrpcError/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  max-height: 160px;\n  overflow-y: auto;\n  margin-bottom: 8px;\n  background-color: ${(props) => props.theme.background.base};\n  border: solid 1px ${(props) => props.theme.border.border2};\n  border-left: 4px solid ${(props) => props.theme.colors.text.danger};\n  border-radius: ${(props) => props.theme.border.radius.base};\n\n  .close-button {\n    opacity: 0.6;\n    transition: opacity 0.15s ease;\n\n    &:hover {\n      opacity: 1;\n    }\n\n    svg {\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n  }\n\n  .error-title {\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-weight: 500;\n    margin-bottom: 4px;\n    color: ${(props) => props.theme.colors.text.danger};\n  }\n\n  .error-message {\n    font-family: monospace;\n    font-size: ${(props) => props.theme.font.size.xs};\n    line-height: 1.4;\n    white-space: pre-wrap;\n    word-break: break-word;\n    color: ${(props) => props.theme.text};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/GrpcResponsePane/GrpcError/index.js",
    "content": "import React from 'react';\nimport { IconX } from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\n\nconst GrpcError = ({ error, onClose }) => {\n  if (!error) return null;\n\n  return (\n    <StyledWrapper>\n      <div className=\"flex items-start gap-3 px-4 py-3\">\n        <div className=\"flex-1 min-w-0\">\n          <div className=\"error-title\">gRPC Server Error</div>\n          <div className=\"error-message\">{typeof error === 'string' ? error : JSON.stringify(error, null, 2)}</div>\n        </div>\n        <div className=\"close-button flex-shrink-0 cursor-pointer\" onClick={onClose}>\n          <IconX size={16} strokeWidth={1.5} />\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default GrpcError;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/GrpcResponsePane/GrpcQueryResult/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  height: 100%;\n  overflow: hidden;\n\n  .empty-state {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .responses-container {\n    height: 100%;\n\n    &.single {\n      height: 100%;\n    }\n\n    &.multi {\n      overflow-y: auto;\n    }\n  }\n\n  .messages-list {\n    display: flex;\n    flex-direction: column;\n  }\n\n  .message-item {\n    display: flex;\n    flex-direction: column;\n\n    &:not(.last) {\n      border-bottom: 1px solid ${(props) => props.theme.border.border1};\n    }\n  }\n\n  .message-header {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 8px 0;\n    cursor: pointer;\n    user-select: none;\n\n    &:hover {\n      .toggle-btn {\n        color: ${(props) => props.theme.text};\n      }\n    }\n  }\n\n  .message-label {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.subtext1};\n  }\n\n  .latest-badge {\n    margin-left: 8px;\n    padding: 2px 6px;\n    font-size: ${(props) => props.theme.font.size.xs};\n    font-weight: 500;\n    color: ${(props) => props.theme.colors.text.green};\n    background-color: color-mix(in srgb, ${(props) => props.theme.colors.text.green} 10%, transparent);\n    border-radius: ${(props) => props.theme.border.radius.sm};\n  }\n\n  .toggle-btn {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: ${(props) => props.theme.colors.text.muted};\n    transition: color 0.15s ease;\n  }\n\n  .message-content {\n    height: 240px;\n    margin-bottom: 8px;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/GrpcResponsePane/GrpcQueryResult/index.js",
    "content": "import React, { useState, useEffect } from 'react';\nimport CodeEditor from 'components/CodeEditor';\nimport { get } from 'lodash';\nimport { useSelector } from 'react-redux';\nimport { useTheme } from 'providers/Theme/index';\nimport StyledWrapper from './StyledWrapper';\nimport GrpcError from '../GrpcError';\nimport { IconChevronDown, IconChevronUp } from '@tabler/icons';\n\nconst GrpcQueryResult = ({ item, collection }) => {\n  const { displayedTheme } = useTheme();\n  const preferences = useSelector((state) => state.app.preferences);\n  const [showErrorMessage, setShowErrorMessage] = useState(true);\n  const [expandedIndex, setExpandedIndex] = useState(0);\n\n  const response = item.response || {};\n  const responsesList = response?.responses || [];\n  const reversedResponsesList = [...responsesList].reverse();\n  const hasError = response.isError;\n  const hasResponses = responsesList.length > 0;\n  const errorMessage = response.error;\n\n  useEffect(() => {\n    if (hasError) {\n      setShowErrorMessage(true);\n    }\n  }, [response, hasError]);\n\n  const formatJSON = (data) => {\n    try {\n      if (typeof data === 'string') {\n        return JSON.stringify(JSON.parse(data), null, 2);\n      }\n      return JSON.stringify(data, null, 2);\n    } catch (e) {\n      return typeof data === 'string' ? data : JSON.stringify(data);\n    }\n  };\n\n  if (!hasResponses && !hasError) {\n    return (\n      <StyledWrapper className=\"w-full h-full relative flex flex-col\">\n        <div className=\"empty-state\">No messages received</div>\n      </StyledWrapper>\n    );\n  }\n\n  return (\n    <StyledWrapper className=\"w-full h-full relative flex flex-col mt-2\" data-testid=\"grpc-response-content\">\n      {hasError && showErrorMessage && <GrpcError error={errorMessage} onClose={() => setShowErrorMessage(false)} />}\n      {hasResponses && (\n        <div className={`responses-container ${responsesList.length === 1 ? 'single' : 'multi'}`} data-testid=\"grpc-responses-container\">\n          {responsesList.length === 1 ? (\n            <div className=\"h-full\" data-testid=\"grpc-single-response\">\n              <CodeEditor\n                collection={collection}\n                font={get(preferences, 'font.codeFont', 'default')}\n                fontSize={get(preferences, 'font.codeFontSize')}\n                theme={displayedTheme}\n                value={formatJSON(reversedResponsesList[0])}\n                mode=\"application/json\"\n                readOnly={true}\n              />\n            </div>\n          ) : (\n            <div className=\"messages-list\" data-testid=\"grpc-responses-list\">\n              {reversedResponsesList.map((resp, index) => {\n                const originalIndex = responsesList.length - index - 1;\n                const isExpanded = expandedIndex === index;\n\n                return (\n                  <div\n                    key={originalIndex}\n                    className={`message-item ${isExpanded ? 'expanded' : 'collapsed'} ${index === reversedResponsesList.length - 1 ? 'last' : ''}`}\n                    data-testid={`grpc-response-item-${originalIndex}`}\n                  >\n                    <div\n                      className=\"message-header\"\n                      onClick={() => setExpandedIndex(isExpanded ? -1 : index)}\n                    >\n                      <span className=\"message-label\">\n                        Response {originalIndex + 1}\n                        {index === 0 && <span className=\"latest-badge\">Latest</span>}\n                      </span>\n                      <button className=\"toggle-btn\">\n                        {isExpanded ? (\n                          <IconChevronUp size={16} strokeWidth={1.5} />\n                        ) : (\n                          <IconChevronDown size={16} strokeWidth={1.5} />\n                        )}\n                      </button>\n                    </div>\n                    {isExpanded && (\n                      <div className=\"message-content\">\n                        <CodeEditor\n                          collection={collection}\n                          font={get(preferences, 'font.codeFont', 'default')}\n                          fontSize={get(preferences, 'font.codeFontSize')}\n                          theme={displayedTheme}\n                          value={formatJSON(resp)}\n                          mode=\"application/json\"\n                          readOnly={true}\n                        />\n                      </div>\n                    )}\n                  </div>\n                );\n              })}\n            </div>\n          )}\n        </div>\n      )}\n      {hasError && !hasResponses && !showErrorMessage && (\n        <div className=\"empty-state\">\n          No messages received. A server error occurred but has been dismissed.\n        </div>\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default GrpcQueryResult;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/GrpcResponsePane/GrpcResponseHeaders/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  table {\n    width: 100%;\n    border-collapse: collapse;\n\n    thead {\n      color: ${(props) => props.theme.colors.text.muted};\n      font-size: ${(props) => props.theme.font.size.xs};\n      font-weight: 500;\n      text-transform: uppercase;\n    }\n\n    td {\n      padding: 8px 10px;\n      font-size: ${(props) => props.theme.font.size.sm};\n\n      &.key {\n        color: ${(props) => props.theme.colors.text.subtext1};\n        font-weight: 500;\n      }\n\n      &.value {\n        word-break: break-all;\n        color: ${(props) => props.theme.text};\n      }\n    }\n\n    tbody {\n      tr:nth-child(odd) {\n        background-color: ${(props) => props.theme.table.striped};\n      }\n    }\n\n    .empty-message {\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/GrpcResponsePane/GrpcResponseHeaders/index.js",
    "content": "import React from 'react';\nimport StyledWrapper from './StyledWrapper';\n\nconst GrpcResponseHeaders = ({ metadata }) => {\n  // Ensure headers is an array\n  const metadataArray = Array.isArray(metadata) ? metadata : [];\n\n  return (\n    <StyledWrapper className=\"pb-4 w-full\">\n      <table>\n        <thead>\n          <tr>\n            <td>Name</td>\n            <td>Value</td>\n          </tr>\n        </thead>\n        <tbody>\n          {metadataArray && metadataArray.length ? (\n            metadataArray.map((metadata, index) => (\n              <tr key={index}>\n                <td className=\"key\">{metadata.name}</td>\n                <td className=\"value\">{metadata.value}</td>\n              </tr>\n            ))\n          ) : (\n            <tr>\n              <td colSpan=\"2\" className=\"text-center py-4 empty-message\">\n                No metadata received\n              </td>\n            </tr>\n          )}\n        </tbody>\n      </table>\n    </StyledWrapper>\n  );\n};\n\nexport default GrpcResponseHeaders;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/GrpcResponsePane/GrpcStatusCode/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  font-size: ${(props) => props.theme.font.size.sm};\n  font-weight: 500;\n  display: flex;\n  align-items: center;\n\n  &.text-ok {\n    color: ${(props) => props.theme.requestTabPanel.responseOk};\n  }\n\n  &.text-pending {\n    color: ${(props) => props.theme.requestTabPanel.responsePending};\n  }\n\n  &.text-error {\n    color: ${(props) => props.theme.requestTabPanel.responseError};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/GrpcResponsePane/GrpcStatusCode/get-grpc-status-code-phrase.js",
    "content": "// https://grpc.github.io/grpc/core/md_doc_statuscodes.html\nconst grpcStatusCodePhraseMap = {\n  0: 'OK',\n  1: 'Cancelled',\n  2: 'Unknown',\n  3: 'Invalid Argument',\n  4: 'Deadline Exceeded',\n  5: 'Not Found',\n  6: 'Already Exists',\n  7: 'Permission Denied',\n  8: 'Resource Exhausted',\n  9: 'Failed Precondition',\n  10: 'Aborted',\n  11: 'Out of Range',\n  12: 'Unimplemented',\n  13: 'Internal',\n  14: 'Unavailable',\n  15: 'Data Loss',\n  16: 'Unauthenticated'\n};\n\nexport default grpcStatusCodePhraseMap;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/GrpcResponsePane/GrpcStatusCode/index.js",
    "content": "import React from 'react';\nimport classnames from 'classnames';\nimport grpcStatusCodePhraseMap from './get-grpc-status-code-phrase';\nimport StyledWrapper from './StyledWrapper';\n\nconst GrpcStatusCode = ({ status, text }) => {\n  // gRPC status codes: 0 is success, anything else is an error\n  const getTabClassname = (status) => {\n    const isPending = text === 'PENDING' || text === 'STREAMING';\n    return classnames('ml-2', {\n      'text-ok': parseInt(status) === 0,\n      'text-pending': isPending,\n      'text-error': parseInt(status) > 0 && !isPending\n    });\n  };\n\n  const statusText = text || grpcStatusCodePhraseMap[status];\n\n  return (\n    <StyledWrapper className={getTabClassname(status)}>\n      {Number.isInteger(status) ? <div className=\"mr-1\" data-testid=\"grpc-response-status-code\">{status}</div> : null}\n      {statusText && <div data-testid=\"grpc-response-status-text\">{statusText}</div>}\n    </StyledWrapper>\n  );\n};\n\nexport default GrpcStatusCode;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/GrpcResponsePane/ResponseTrailers/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  table {\n    width: 100%;\n    border-collapse: collapse;\n\n    thead {\n      color: ${(props) => props.theme.colors.text.muted};\n      font-size: ${(props) => props.theme.font.size.xs};\n      font-weight: 500;\n      text-transform: uppercase;\n    }\n\n    td {\n      padding: 8px 10px;\n      font-size: ${(props) => props.theme.font.size.sm};\n\n      &.key {\n        color: ${(props) => props.theme.colors.text.subtext1};\n        font-weight: 500;\n      }\n\n      &.value {\n        word-break: break-all;\n        color: ${(props) => props.theme.text};\n      }\n    }\n\n    tbody {\n      tr:nth-child(odd) {\n        background-color: ${(props) => props.theme.table.striped};\n      }\n    }\n\n    .empty-message {\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/GrpcResponsePane/ResponseTrailers/index.js",
    "content": "import React from 'react';\nimport StyledWrapper from './StyledWrapper';\n\nconst ResponseTrailers = ({ trailers }) => {\n  const trailersArray = Array.isArray(trailers) ? trailers : [];\n\n  return (\n    <StyledWrapper className=\"pb-4 w-full\">\n      <table>\n        <thead>\n          <tr>\n            <td>Name</td>\n            <td>Value</td>\n          </tr>\n        </thead>\n        <tbody>\n          {trailersArray && trailersArray.length ? (\n            trailersArray.map((trailer, index) => (\n              <tr key={index}>\n                <td className=\"key\">{trailer.name}</td>\n                <td className=\"value\">{trailer.value}</td>\n              </tr>\n            ))\n          ) : (\n            <tr>\n              <td colSpan=\"2\" className=\"text-center py-4 empty-message\">\n                No trailers received\n              </td>\n            </tr>\n          )}\n        </tbody>\n      </table>\n    </StyledWrapper>\n  );\n};\n\nexport default ResponseTrailers;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/GrpcResponsePane/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  height: 100%;\n  overflow: hidden;\n  border-radius: 4px;\n\n  div.tabs {\n    div.tab {\n      padding: 6px 0px;\n      border: none;\n      border-bottom: solid 2px transparent;\n      margin-right: ${(props) => props.theme.tabs.marginRight};\n      color: ${(props) => props.theme.colors.text.subtext0};\n      cursor: pointer;\n\n      &:focus,\n      &:active,\n      &:focus-within,\n      &:focus-visible,\n      &:target {\n        outline: none !important;\n        box-shadow: none !important;\n      }\n\n      &.active {\n        font-weight: ${(props) => props.theme.tabs.active.fontWeight} !important;\n        color: ${(props) => props.theme.tabs.active.color} !important;\n        border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;\n      }\n    }\n  }\n\n  .stream-status {\n    display: inline-flex;\n    align-items: center;\n\n    &.complete {\n      color: ${(props) => props.theme.colors.text.green};\n    }\n\n    &.cancelled {\n      color: ${(props) => props.theme.colors.text.danger};\n    }\n\n    &.streaming {\n      color: ${(props) => props.theme.colors.text.blue};\n    }\n  }\n\n  .message-counter {\n    display: inline-flex;\n    align-items: center;\n    margin-left: 10px;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/GrpcResponsePane/index.js",
    "content": "import React, { useRef } from 'react';\nimport find from 'lodash/find';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';\nimport Overlay from '../Overlay';\nimport Placeholder from '../Placeholder';\nimport GrpcResponseHeaders from './GrpcResponseHeaders';\nimport GrpcStatusCode from './GrpcStatusCode';\nimport ResponseTime from '../ResponseTime/index';\nimport Timeline from '../Timeline';\nimport ClearTimeline from '../ClearTimeline';\nimport ResponseClear from '../ResponseClear';\nimport StyledWrapper from './StyledWrapper';\nimport ResponseTrailers from './ResponseTrailers';\nimport GrpcQueryResult from './GrpcQueryResult';\nimport ResponseLayoutToggle from '../ResponseLayoutToggle';\nimport ResponsiveTabs from 'ui/ResponsiveTabs';\n\nconst GrpcResponsePane = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  const tabs = useSelector((state) => state.tabs.tabs);\n  const activeTabUid = useSelector((state) => state.tabs.activeTabUid);\n  const isLoading = ['queued', 'sending'].includes(item.requestState);\n  const rightContentRef = useRef(null);\n\n  const requestTimeline = [...(collection?.timeline || [])].filter((obj) => {\n    if (obj.itemUid === item.uid) return true;\n  });\n\n  const selectTab = (tab) => {\n    dispatch(\n      updateResponsePaneTab({\n        uid: item.uid,\n        responsePaneTab: tab\n      })\n    );\n  };\n\n  const response = item.response || {};\n\n  const metadataCount = Array.isArray(response.metadata) ? response.metadata.length : 0;\n  const trailersCount = Array.isArray(response.trailers) ? response.trailers.length : 0;\n  const responsesCount = Array.isArray(response.responses) ? response.responses.length : 0;\n\n  const allTabs = [\n    {\n      key: 'response',\n      label: 'Response',\n      indicator:\n        responsesCount > 0 ? (\n          <sup data-testid=\"grpc-tab-response-count\" className=\"ml-1 font-medium\">\n            {responsesCount}\n          </sup>\n        ) : null\n    },\n    {\n      key: 'headers',\n      label: 'Metadata',\n      indicator: metadataCount > 0 ? <sup className=\"ml-1 font-medium\">{metadataCount}</sup> : null\n    },\n    {\n      key: 'trailers',\n      label: 'Trailers',\n      indicator: trailersCount > 0 ? <sup className=\"ml-1 font-medium\">{trailersCount}</sup> : null\n    },\n    {\n      key: 'timeline',\n      label: 'Timeline',\n      indicator: null\n    }\n  ];\n\n  const getTabPanel = (tab) => {\n    switch (tab) {\n      case 'response': {\n        return <GrpcQueryResult item={item} collection={collection} />;\n      }\n      case 'headers': {\n        return <GrpcResponseHeaders metadata={response.metadata} />;\n      }\n      case 'trailers': {\n        return <ResponseTrailers trailers={response.trailers} />;\n      }\n      case 'timeline': {\n        return <Timeline collection={collection} item={item} />;\n      }\n      default: {\n        return <div>404 | Not found</div>;\n      }\n    }\n  };\n\n  if (isLoading && !item.response) {\n    return (\n      <StyledWrapper className=\"flex flex-col h-full relative\">\n        <Overlay item={item} collection={collection} />\n      </StyledWrapper>\n    );\n  }\n\n  if (!item.response && !requestTimeline?.length) {\n    return (\n      <StyledWrapper className=\"flex h-full relative\">\n        <Placeholder />\n      </StyledWrapper>\n    );\n  }\n\n  if (!activeTabUid) {\n    return <div>Something went wrong</div>;\n  }\n\n  const focusedTab = find(tabs, (t) => t.uid === activeTabUid);\n  if (!focusedTab || !focusedTab.uid || !focusedTab.responsePaneTab) {\n    return <div className=\"pb-4 px-4\">An error occurred!</div>;\n  }\n\n  const rightContent = !isLoading ? (\n    <div ref={rightContentRef} className=\"flex items-center\">\n      {focusedTab?.responsePaneTab === 'timeline' ? (\n        <>\n          <ResponseLayoutToggle />\n          <ClearTimeline item={item} collection={collection} />\n        </>\n      ) : item?.response ? (\n        <>\n          <ResponseLayoutToggle />\n          <ResponseClear item={item} collection={collection} />\n          <GrpcStatusCode\n            status={response.statusCode}\n            text={response.statusText}\n            details={response.statusDescription}\n          />\n          <ResponseTime duration={response.duration} />\n        </>\n      ) : null}\n    </div>\n  ) : null;\n\n  return (\n    <StyledWrapper className=\"flex flex-col h-full relative\">\n      <div className=\"px-4\">\n        <ResponsiveTabs\n          tabs={allTabs}\n          activeTab={focusedTab.responsePaneTab}\n          onTabSelect={selectTab}\n          rightContent={rightContent}\n          rightContentRef={rightContentRef}\n        />\n      </div>\n      <section className=\"flex flex-col flex-grow px-4 h-0 mt-4\">\n        {isLoading ? <Overlay item={item} collection={collection} /> : null}\n        {!item?.response ? (\n          focusedTab?.responsePaneTab === 'timeline' && requestTimeline?.length ? (\n            <Timeline collection={collection} item={item} />\n          ) : null\n        ) : (\n          <>{getTabPanel(focusedTab.responsePaneTab)}</>\n        )}\n      </section>\n    </StyledWrapper>\n  );\n};\n\nexport default GrpcResponsePane;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/LargeResponseWarning/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n\n  .warning-container {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    margin-bottom: 1.5rem;\n    margin-top: 10%;\n    text-align: center;\n    max-width: 480px;\n  }\n\n  .warning-icon {\n    margin-bottom: 1rem;\n    color: ${(props) => props.theme.colors.text.yellow};\n  }\n\n  .warning-title {\n    font-weight: 500;\n    color: ${(props) => props.theme.text};\n    margin-bottom: 1rem;\n  }\n\n  .warning-description {\n    color: ${(props) => props.theme.colors.text.muted};\n\n    .size-highlight {\n      padding: 0.125rem 0.375rem;\n      border-radius: 4px;\n      font-size: 0.8rem;\n    }\n\n    .current-size {\n      color: ${(props) => props.theme.colors.text.danger};\n      background: ${(props) => props.theme.colors.text.danger}15;\n    }\n\n    .supported-size {\n      color: ${(props) => props.theme.colors.text.yellow};\n      background: ${(props) => props.theme.colors.text.yellow}15;\n    }\n  }\n\n  .warning-actions {\n    display: flex;\n    gap: 0.75rem;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/LargeResponseWarning/index.js",
    "content": "import React from 'react';\nimport { IconDownload, IconCopy, IconEye, IconAlertTriangle } from '@tabler/icons';\nimport toast from 'react-hot-toast';\nimport get from 'lodash/get';\nimport StyledWrapper from './StyledWrapper';\nimport { formatSize } from 'utils/common/index';\nimport Button from 'ui/Button/index';\n\nconst LargeResponseWarning = ({ item, responseSize, onRevealResponse }) => {\n  const { ipcRenderer } = window;\n  const response = item.response || {};\n\n  const downloadResponseToFile = () => {\n    return new Promise((resolve, reject) => {\n      ipcRenderer\n        .invoke('renderer:save-response-to-file', response, item.requestSent.url, item.pathname)\n        .then((result) => {\n          if (result && result.success) {\n            toast.success('Response downloaded to file');\n          }\n          resolve();\n        })\n        .catch((err) => {\n          toast.error(get(err, 'error.message') || 'Something went wrong!');\n          reject(err);\n        });\n    });\n  };\n\n  const copyResponse = () => {\n    try {\n      const textToCopy = typeof response.data === 'string'\n        ? response.data\n        : JSON.stringify(response.data, null, 2);\n\n      navigator.clipboard.writeText(textToCopy).then(() => {\n        toast.success('Response copied to clipboard');\n      }).catch(() => {\n        toast.error('Failed to copy response');\n      });\n    } catch (error) {\n      toast.error('Failed to copy response');\n    }\n  };\n\n  return (\n    <StyledWrapper>\n      <div className=\"warning-container\">\n        <div className=\"warning-icon\">\n          <IconAlertTriangle size={45} strokeWidth={2} />\n        </div>\n        <div className=\"warning-content\">\n          <div className=\"warning-title\">\n            Large Response Warning\n          </div>\n          <div className=\"warning-description\">\n            Handling responses over <span className=\"size-highlight supported-size\">{formatSize(10 * 1024 * 1024)}</span> could degrade performance.\n            <br />\n            Size of current response: <span className=\"size-highlight current-size\">{formatSize(responseSize)}</span>\n          </div>\n        </div>\n      </div>\n      <div className=\"warning-actions\">\n        <Button\n          icon={<IconEye size={18} strokeWidth={1.5} />}\n          iconPosition=\"left\"\n          onClick={onRevealResponse}\n          title=\"Show response content\"\n          color=\"secondary\"\n          size=\"sm\"\n        >\n          View\n        </Button>\n        <Button\n          icon={<IconDownload size={18} strokeWidth={1.5} />}\n          iconPosition=\"left\"\n          onClick={downloadResponseToFile}\n          disabled={!response.dataBuffer}\n          title=\"Download response to file\"\n          color=\"secondary\"\n          size=\"sm\"\n        >\n          Download\n        </Button>\n        <Button\n          icon={<IconCopy size={18} strokeWidth={1.5} />}\n          iconPosition=\"left\"\n          onClick={copyResponse}\n          disabled={!response.data}\n          title=\"Copy response to clipboard\"\n          color=\"secondary\"\n          size=\"sm\"\n        >\n          Copy\n        </Button>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default LargeResponseWarning;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/NetworkError/index.js",
    "content": "import React from 'react';\n\nconst NetworkError = ({ onClose }) => {\n  return (\n    <div className=\"max-w-md w-full bg-white shadow-lg rounded-lg pointer-events-auto flex bg-red-100\">\n      <div className=\"flex-1 w-0 p-4\">\n        <div className=\"flex items-start\">\n          <div className=\"ml-3 flex-1\">\n            <p className=\"font-medium text-red-800\">Network Error</p>\n          </div>\n        </div>\n      </div>\n      <div className=\"flex\">\n        <button\n          onClick={onClose}\n          className=\"w-full border border-transparent rounded-none rounded-r-lg p-4 flex items-center justify-center font-medium focus:outline-none\"\n        >\n          Close\n        </button>\n      </div>\n    </div>\n  );\n};\n\nexport default NetworkError;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/Overlay/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  position: absolute;\n  height: 100%;\n  width: calc(100% - 0.75rem);\n  z-index: 1;\n  background-color: ${(props) => props.theme.requestTabPanel.responseOverlayBg};\n\n  div.overlay {\n    height: 100%;\n    z-index: 9;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    padding-top: 20%;\n    overflow: hidden;\n    text-align: center;\n\n    .loading-icon {\n      transform: scaleY(-1);\n      animation: rotateCounterClockwise 1s linear infinite;\n    }\n  }\n\n  // spinner and request time content looks better centered vertically in vertical layout\n  // while in horizontal layout, it looks better when the content is aligned to the top\n  &.vertical-layout {\n    div.overlay {\n      justify-content: center;\n      padding: 1rem;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/Overlay/index.js",
    "content": "import React from 'react';\nimport { IconRefresh } from '@tabler/icons';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { cancelRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport StopWatch from '../../StopWatch';\nimport StyledWrapper from './StyledWrapper';\nimport Button from 'ui/Button/index';\n\nconst ResponseLoadingOverlay = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  const preferences = useSelector((state) => state.app.preferences);\n  const isVerticalLayout = preferences?.layout?.responsePaneOrientation === 'vertical';\n\n  const handleCancelRequest = () => {\n    dispatch(cancelRequest(item.cancelTokenUid, item, collection));\n  };\n\n  return (\n    <StyledWrapper className={`w-full ${isVerticalLayout ? 'vertical-layout' : ''}`}>\n      <div className=\"overlay\">\n        <div style={{ marginBottom: 15, fontSize: 26 }}>\n          <div style={{ display: 'inline-block', fontSize: 20, marginLeft: 5, marginRight: 5 }}>\n            <StopWatch startTime={item?.requestStartTime} />\n          </div>\n        </div>\n        <IconRefresh size={24} className=\"loading-icon\" />\n        <Button\n          color=\"secondary\"\n          size=\"sm\"\n          onClick={handleCancelRequest}\n          className=\"mt-4\"\n        >\n          Cancel Request\n        </Button>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default ResponseLoadingOverlay;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/Placeholder/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  padding-top: 20%;\n  width: 100%;\n\n  .send-icon {\n    color: ${(props) => props.theme.background.surface2};\n  }\n\n  &.vertical-layout {\n    padding: 1rem;\n    justify-content: center;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/Placeholder/index.js",
    "content": "import React from 'react';\nimport { IconSend } from '@tabler/icons';\nimport { useSelector } from 'react-redux';\nimport StyledWrapper from './StyledWrapper';\nimport { isMacOS } from 'utils/common/platform';\n\nconst Placeholder = () => {\n  const isMac = isMacOS();\n  const sendRequestShortcut = isMac ? 'Cmd + Enter' : 'Ctrl + Enter';\n  const newRequestShortcut = isMac ? 'Cmd + B' : 'Ctrl + B';\n  const editEnvironmentShortcut = isMac ? 'Cmd + E' : 'Ctrl + E';\n  const preferences = useSelector((state) => state.app.preferences);\n  const isVerticalLayout = preferences?.layout?.responsePaneOrientation === 'vertical';\n\n  const iconSize = isVerticalLayout ? 80 : 150;\n\n  return (\n    <StyledWrapper className={`${isVerticalLayout ? 'vertical-layout' : ''}`}>\n      <div className=\"send-icon flex justify-center\" style={{ fontSize: isVerticalLayout ? 100 : 200 }}>\n        <IconSend size={iconSize} strokeWidth={1} />\n      </div>\n      <div className={`flex ${isVerticalLayout ? 'mt-2' : 'mt-4'}`}>\n        <div className=\"flex flex-1 flex-col items-end px-1\">\n          <div className=\"px-1 py-2\">Send Request</div>\n          <div className=\"px-1 py-2\">New Request</div>\n          <div className=\"px-1 py-2\">Edit Environments</div>\n        </div>\n        <div className=\"flex flex-1 flex-col px-1\">\n          <div className=\"px-1 py-2\">{sendRequestShortcut}</div>\n          <div className=\"px-1 py-2\">{newRequestShortcut}</div>\n          <div className=\"px-1 py-2\">{editEnvironmentShortcut}</div>\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Placeholder;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/QueryResponse/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n  width: 100%;\n  border-radius: 4px;\n  border: 1px solid ${(props) => props.theme.table.border};\n\n  .result-type-selector {\n    border-bottom: 1px solid ${(props) => props.theme.table.border};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/QueryResponse/index.js",
    "content": "import React, { useEffect, useState } from 'react';\nimport QueryResult from '../QueryResult';\nimport { useInitialResponseFormat, useResponsePreviewFormatOptions } from '../QueryResult/index';\nimport QueryResultTypeSelector from '../QueryResult/QueryResultTypeSelector/index';\nimport StyledWrapper from './StyledWrapper';\nimport classnames from 'classnames';\n\nconst QueryResponse = ({\n  item,\n  collection,\n  data,\n  dataBuffer,\n  disableRunEventListener,\n  headers,\n  error,\n  hideResultTypeSelector\n}) => {\n  const { initialFormat, initialTab } = useInitialResponseFormat(dataBuffer, headers);\n  const previewFormatOptions = useResponsePreviewFormatOptions(dataBuffer, headers);\n  const [selectedFormat, setSelectedFormat] = useState('raw');\n  const [selectedTab, setSelectedTab] = useState('editor');\n\n  useEffect(() => {\n    if (initialFormat !== null && initialTab !== null) {\n      setSelectedFormat(initialFormat);\n      setSelectedTab(initialTab);\n    }\n  }, [initialFormat, initialTab]);\n  return (\n    <StyledWrapper>\n      {!hideResultTypeSelector && (\n        <div className=\"flex items-center justify-end p-2 result-type-selector\">\n\n          <QueryResultTypeSelector\n            formatOptions={previewFormatOptions}\n            formatValue={selectedFormat}\n            onFormatChange={(newFormat) => {\n              setSelectedFormat(newFormat);\n            }}\n            onPreviewTabSelect={() => {\n              setSelectedTab((prev) => prev === 'editor' ? 'preview' : 'editor');\n            }}\n            selectedTab={selectedTab}\n            isActiveTab={true}\n          />\n        </div>\n      )}\n      <div className={classnames('flex-1 result-content', selectedTab === 'editor' ? 'px-2 py-1' : '')}>\n        <QueryResult\n          item={item}\n          collection={collection}\n          data={data}\n          dataBuffer={dataBuffer}\n          disableRunEventListener={disableRunEventListener}\n          headers={headers}\n          error={error}\n          selectedFormat={selectedFormat}\n          selectedTab={selectedTab}\n        />\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default QueryResponse;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultFilter/index.js",
    "content": "import { IconFilter, IconX } from '@tabler/icons';\nimport React, { useMemo } from 'react';\nimport { useRef } from 'react';\nimport { useState } from 'react';\nimport { Tooltip as ReactInfotip } from 'react-tooltip';\n\nconst QueryResultFilter = ({ filter, onChange, mode }) => {\n  const inputRef = useRef(null);\n  const [isExpanded, toggleExpand] = useState(false);\n\n  const handleFilterClick = () => {\n    // Toggle filter search bar\n    toggleExpand(!isExpanded);\n    // Reset filter search input\n    onChange({ target: { value: '' } });\n    // Reset input value\n    if (inputRef?.current) {\n      inputRef.current.value = '';\n    }\n  };\n\n  const infotipText = useMemo(() => {\n    if (mode.includes('json')) {\n      return 'Filter with JSONPath';\n    }\n\n    if (mode.includes('xml')) {\n      return 'Filter with XPath';\n    }\n\n    return null;\n  }, [mode]);\n\n  const placeholderText = useMemo(() => {\n    if (mode.includes('json')) {\n      return '$.store.books..author';\n    }\n\n    if (mode.includes('xml')) {\n      return '/store/books//author';\n    }\n\n    return null;\n  }, [mode]);\n\n  return (\n    <div\n      className=\"response-filter absolute bottom-2 w-full justify-end right-0 flex flex-row items-center gap-2 py-4 px-2 pointer-events-none\"\n    >\n      {infotipText && !isExpanded && <ReactInfotip anchorId=\"request-filter-icon\" html={infotipText} />}\n      <input\n        ref={inputRef}\n        type=\"text\"\n        name=\"response-filter\"\n        id=\"response-filter\"\n        placeholder={placeholderText}\n        autoComplete=\"off\"\n        autoCorrect=\"off\"\n        autoCapitalize=\"off\"\n        spellCheck=\"false\"\n        className={`block ml-14 p-2 py-1 transition-all duration-200 ease-in-out border border-gray-300 rounded-md ${\n          isExpanded ? 'w-full opacity-100 pointer-events-auto' : 'w-[0] opacity-0'\n        }`}\n        onChange={onChange}\n      />\n      <div className=\"text-gray-500 cursor-pointer pointer-events-auto\" id=\"request-filter-icon\" onClick={handleFilterClick}>\n        {isExpanded ? <IconX size={20} strokeWidth={1.5} /> : <IconFilter size={20} strokeWidth={1.5} />}\n      </div>\n    </div>\n  );\n};\n\nexport default QueryResultFilter;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/HtmlPreview.js",
    "content": "import React, { useRef, useState, useEffect } from 'react';\nimport { escapeHtml } from 'utils/response/index';\n\nconst HtmlPreview = React.memo(({ data, baseUrl }) => {\n  const webviewContainerRef = useRef(null);\n  const [isDragging, setIsDragging] = useState(false);\n\n  useEffect(() => {\n    if (!webviewContainerRef.current) return;\n\n    const checkDragging = () => {\n      const hasDraggingParent = webviewContainerRef.current?.closest('.dragging');\n      setIsDragging(!!hasDraggingParent);\n    };\n\n    // Watch from a common ancestor where .dragging gets added\n    const watchTarget = webviewContainerRef.current.closest('.main-section')\n      || document.body;\n\n    const mutationObserver = new MutationObserver(checkDragging);\n    mutationObserver.observe(watchTarget, {\n      attributes: true,\n      attributeFilter: ['class'],\n      subtree: true\n    });\n\n    // Check initial state\n    checkDragging();\n\n    return () => mutationObserver.disconnect();\n  }, []);\n\n  const renderHtmlPreview = (data, baseUrl, isDragging, webviewContainerRef) => {\n    const htmlContent = data.includes('<head>')\n      ? data.replace('<head>', `<head><base href=\"${escapeHtml(baseUrl)}\">`)\n      : `<head><base href=\"${escapeHtml(baseUrl)}\"></head>${data}`;\n\n    const dragStyles = isDragging ? { pointerEvents: 'none', userSelect: 'none' } : {};\n\n    return (\n      <div\n        ref={webviewContainerRef}\n        className=\"h-full bg-white webview-container\"\n        style={dragStyles}\n      >\n        <webview\n          src={`data:text/html; charset=utf-8,${encodeURIComponent(htmlContent)}`}\n          webpreferences=\"disableDialogs=true, javascript=yes\"\n          className=\"h-full bg-white\"\n          style={dragStyles}\n        />\n      </div>\n    );\n  };\n\n  // For all other data types, render safely as formatted text\n  let displayContent = '';\n  if (data === null || data === undefined) {\n    displayContent = String(data);\n  } else if (typeof data === 'object') {\n    displayContent = JSON.stringify(data, null);\n  } else {\n    displayContent = String(data);\n  }\n\n  return (\n    <>{renderHtmlPreview(displayContent, baseUrl, isDragging, webviewContainerRef)}</>\n  );\n});\n\nexport default HtmlPreview;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/JsonPreview.js",
    "content": "import React from 'react';\nimport ReactJson from 'react-json-view';\nimport ErrorBanner from 'ui/ErrorBanner';\n\nconst JsonPreview = ({ data, displayedTheme }) => {\n  // Helper function to validate and parse JSON data\n  const validateJsonData = (data) => {\n    // If data is already an object or array, use it directly\n    if (typeof data === 'object' && data !== null) {\n      return { data, error: null };\n    }\n\n    // If data is a string, try to parse it\n    if (typeof data === 'string') {\n      try {\n        const parsed = JSON.parse(data);\n        return { data: parsed, error: null };\n      } catch (e) {\n        return { data: null, error: `Invalid JSON format: ${e.message}` };\n      }\n    }\n\n    // For other types, return error\n    return { data: null, error: 'Invalid input. Expected a JSON object, array, or valid JSON string.' };\n  };\n\n  // Validate and parse JSON data\n  const jsonData = validateJsonData(data);\n\n  // Show error if parsing failed\n  if (jsonData.error) {\n    return <ErrorBanner errors={[{ title: 'Cannot preview as JSON', message: jsonData.error }]} />;\n  }\n\n  // Validate that data can be rendered as JSON tree\n  if (jsonData.data === null || jsonData.data === undefined) {\n    return <ErrorBanner errors={[{ title: 'Cannot preview as JSON', message: 'Data is null or undefined. Expected a valid JSON object or array.' }]} />;\n  }\n\n  if (typeof jsonData.data !== 'object') {\n    return <ErrorBanner errors={[{ title: 'Cannot preview as JSON', message: 'Data cannot be rendered as a JSON tree. Expected a JSON object or array.' }]} />;\n  }\n\n  return (\n    <ReactJson\n      src={jsonData.data}\n      theme={displayedTheme === 'light' ? 'rjv-default' : 'monokai'}\n      collapsed={1}\n      displayDataTypes={false}\n      displayObjectSize={true}\n      enableClipboard={true}\n      name={false}\n      style={{\n        backgroundColor: 'transparent',\n        fontSize: '12px',\n        fontFamily: 'ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace',\n        padding: '16px'\n      }}\n    />\n  );\n};\n\nexport default JsonPreview;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/TextPreview.js",
    "content": "import React, { memo, useMemo } from 'react';\n\nconst TextPreview = memo(({ data }) => {\n  const displayData = useMemo(() => {\n    if (data === null || data === undefined) {\n      return String(data);\n    }\n    if (typeof data === 'object') {\n      try {\n        return JSON.stringify(data);\n      } catch {\n        return String(data);\n      }\n    }\n    return String(data);\n  }, [data]);\n\n  return (\n    <div className=\"p-4 font-mono text-[13px] whitespace-pre-wrap break-words overflow-auto overflow-x-hidden w-full max-w-full h-full\">\n      {displayData}\n    </div>\n  );\n});\n\nexport default TextPreview;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/VideoPreview.js",
    "content": "import React from 'react';\nimport { useEffect, useState } from 'react';\nimport ReactPlayer from 'react-player';\n\nconst VideoPreview = React.memo(({ contentType, dataBuffer }) => {\n  const [videoUrl, setVideoUrl] = useState(null);\n\n  useEffect(() => {\n    const videoType = contentType.split(';')[0];\n    const byteArray = Buffer.from(dataBuffer, 'base64');\n    const blob = new Blob([byteArray], { type: videoType });\n    const url = URL.createObjectURL(blob);\n    setVideoUrl(url);\n    return () => URL.revokeObjectURL(url);\n  }, [contentType, dataBuffer]);\n\n  if (!videoUrl) return <div>Loading video...</div>;\n\n  return (\n    <ReactPlayer\n      url={videoUrl}\n      controls\n      muted={true}\n      width=\"100%\"\n      height=\"100%\"\n      onError={(e) => console.error('Error loading video:', e)}\n    />\n  );\n});\n\nexport default VideoPreview;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/XmlPreview/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n  font-size: 12px;\n  line-height: 20px;\n  padding: 16px;\n  overflow: auto;\n  color: ${(props) => props.theme.text};\n\n  .xml-container {\n    color: ${(props) => props.theme.text};\n  }\n\n  .xml-node-name {\n    color: ${(props) => props.theme.codemirror.tokens.property};\n    font-weight: 500;\n  }\n\n  .xml-separator {\n    color: ${(props) => props.theme.codemirror.tokens.operator};\n    margin: 0 8px;\n  }\n\n  .xml-value {\n    color: ${(props) => props.theme.codemirror.tokens.string};\n    white-space: pre-wrap;\n    word-break: break-all;\n  }\n\n  .xml-empty-value {\n    color: ${(props) => props.theme.codemirror.tokens.comment};\n  }\n\n  .xml-count {\n    color: ${(props) => props.theme.codemirror.tokens.comment};\n    margin-left: 8px;\n  }\n\n  .xml-toggle-button {\n    margin-right: 8px;\n    cursor: pointer;\n    width: 16px;\n    height: 16px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: ${(props) => props.theme.codemirror.tokens.atom};\n    flex-shrink: 0;\n    border-radius: 4px;\n    transition: background-color 0.2s;\n\n    &:hover {\n      background-color: ${(props) => props.theme.console.buttonHoverBg};\n    }\n  }\n\n  .xml-array-toggle-button {\n    margin-right: 8px;\n    cursor: pointer;\n    width: 16px;\n    height: 16px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: ${(props) => props.theme.codemirror.tokens.atom};\n    flex-shrink: 0;\n    border-radius: 4px;\n    transition: background-color 0.2s;\n\n    &:hover {\n      background-color: ${(props) => props.theme.console.buttonHoverBg};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/XmlPreview/index.js",
    "content": "import ErrorBanner from 'ui/ErrorBanner';\nimport React, { useState, useMemo } from 'react';\nimport StyledWrapper from './StyledWrapper';\n\n// The expected \"data\" prop must be an XML string.\nexport default function XmlPreview({ data, defaultExpanded = true }) {\n  // Parse XML string\n  const parsedData = useMemo(() => {\n    if (typeof data !== 'string') {\n      return { error: 'Invalid input. Expected an XML string.' };\n    }\n\n    const parsed = parseXMLString(data);\n    if (parsed === null) {\n      return { error: 'Failed to parse XML string. Invalid XML format.' };\n    }\n    return parsed;\n  }, [data]);\n\n  // Check for parsing error\n  if (parsedData && typeof parsedData === 'object' && parsedData.error) {\n    return (\n      <div className=\"px-2\">\n        <ErrorBanner errors={[{ title: 'Cannot preview as XML', message: parsedData.error }]} />\n      </div>\n    );\n  }\n\n  // Validate that data can be rendered as a tree\n  const isValidTreeData = (data) => {\n    if (data === null || data === undefined) return false;\n    if (typeof data === 'object' && !Array.isArray(data)) return true;\n    if (Array.isArray(data)) return true;\n    return false;\n  };\n\n  if (!isValidTreeData(parsedData)) {\n    return (\n      <div className=\"px-2\">\n        <ErrorBanner errors={[{ title: 'Cannot preview as XML', message: 'Data cannot be rendered as a tree. Expected a valid XML string.' }]} />\n      </div>\n    );\n  }\n\n  // If root is an object with a single key, unwrap it to show the actual root element\n  let rootNode = parsedData;\n  let rootNodeName = '';\n\n  if (typeof parsedData === 'object' && !Array.isArray(parsedData) && parsedData !== null) {\n    const keys = Object.keys(parsedData).filter((k) => k !== '$' && k !== '@_' && k !== '#text');\n    if (keys.length === 1) {\n      rootNodeName = keys[0];\n      rootNode = parsedData[keys[0]];\n    } else if (keys.length === 0) {\n      // Empty object with no children\n      return (\n        <div className=\"px-2\">\n          <ErrorBanner errors={[{ title: 'Cannot preview as XML', message: 'Cannot render XML tree. Root object is empty.' }]} />\n        </div>\n      );\n    }\n  }\n\n  return (\n    <StyledWrapper>\n      <div className=\"xml-container\">\n        <XmlNode\n          node={rootNode}\n          nodeName={rootNodeName}\n          isRoot={true}\n          isLast={true}\n          defaultExpanded={defaultExpanded}\n        />\n      </div>\n    </StyledWrapper>\n  );\n}\n\n// Component for rendering array entries with expand/collapse functionality\nconst XmlArrayNode = ({ arrayKey, items, depth, defaultExpanded = true }) => {\n  const [expanded, setExpanded] = useState(defaultExpanded);\n\n  const toggle = (e) => {\n    e.stopPropagation();\n    setExpanded((v) => !v);\n  };\n\n  return (\n    <div style={{ paddingLeft: `${(depth + 1) * 20}px` }}>\n      <div className=\"flex items-center mb-1\">\n        <button\n          onClick={toggle}\n          className=\"xml-array-toggle-button\"\n          tabIndex={-1}\n          aria-expanded={expanded}\n        >\n          {expanded ? '▼' : '▶'}\n        </button>\n        <span className=\"xml-node-name\">{arrayKey}</span>\n        <span className=\"xml-count\">[{items.length}]</span>\n      </div>\n      {expanded && (\n        <div className=\"array-content\">\n          {items.map((item, itemIdx) => (\n            <XmlNode\n              key={`${arrayKey}-${itemIdx}`}\n              node={item}\n              nodeName={`${itemIdx}`}\n              isLast={itemIdx === items.length - 1}\n              defaultExpanded={false}\n              depth={depth + 2}\n            />\n          ))}\n        </div>\n      )}\n    </div>\n  );\n};\n\nconst XmlNode = ({\n  node,\n  nodeName = '',\n  isRoot = false,\n  isLast = true,\n  defaultExpanded = true,\n  depth = 0\n}) => {\n  const [expanded, setExpanded] = useState(defaultExpanded);\n\n  let displayNodeName = nodeName;\n\n  if (Array.isArray(node)) {\n    // For repeated XML elements with same name (e.g. <item>...</item><item>...</item>)\n    return (\n      <>\n        {node.map((item, idx) => (\n          <XmlNode\n            key={idx}\n            node={item}\n            nodeName={displayNodeName}\n            isRoot={false}\n            isLast={idx === node.length - 1}\n            defaultExpanded={false}\n            depth={depth}\n          />\n        ))}\n      </>\n    );\n  }\n\n  const childEntries = getChildrenEntries(node);\n  const childCount = getChildCount(node);\n  const isLeaf = isTextNode(node) || (typeof node === 'object' && childCount === 0);\n\n  const toggle = (e) => {\n    e.stopPropagation();\n    setExpanded((v) => !v);\n  };\n\n  // For leaf nodes with text content or attributes with empty values\n  if (isLeaf && isTextNode(node)) {\n    const value = String(node);\n\n    return (\n      <div className=\"flex items-start mb-1\" style={{ paddingLeft: `${depth * 20}px` }}>\n        {displayNodeName && (\n          <>\n            <span className=\"xml-node-name\">{displayNodeName}</span>\n            <span className=\"xml-separator\">:</span>\n          </>\n        )}\n        <span className=\"xml-value\">{value}</span>\n      </div>\n    );\n  }\n\n  // For empty leaf nodes (attributes without values, etc)\n  if (isLeaf && !isTextNode(node)) {\n    // Check if this is an attribute-only node with _text\n    if (typeof node === 'object' && node !== null && '_text' in node) {\n      // This node has both attributes and text, handle in expandable section\n      // Fall through to expandable node rendering\n    } else {\n      return (\n        <div className=\"flex items-center mb-1\" style={{ paddingLeft: `${depth * 20}px` }}>\n          {displayNodeName && (\n            <>\n              <span className=\"xml-node-name\">{displayNodeName}</span>\n              <span className=\"xml-separator\">:</span>\n              <span className=\"xml-empty-value\">{'{}'}</span>\n            </>\n          )}\n        </div>\n      );\n    }\n  }\n\n  // For expandable nodes - show as tree structure\n  // If no node name at root level, render children directly\n  if (!displayNodeName && depth === 0) {\n    if (childEntries.length > 0) {\n      return (\n        <div>\n          {childEntries.map(([key, value], idx) => (\n            <XmlNode\n              key={key + idx}\n              node={value}\n              nodeName={key}\n              isLast={idx === childEntries.length - 1}\n              defaultExpanded={defaultExpanded}\n              depth={0}\n            />\n          ))}\n        </div>\n      );\n    }\n    return null;\n  }\n\n  // If no display name at non-root level, use a fallback\n  if (!displayNodeName) {\n    displayNodeName = '(unnamed)';\n  }\n\n  // Determine if this node's value is an array\n  const hasArrayValue = Array.isArray(node);\n  const arrayLength = hasArrayValue ? node.length : 0;\n\n  return (\n    <div style={{ paddingLeft: `${depth * 20}px` }}>\n      <div className=\"flex items-center mb-1\">\n        <button\n          onClick={toggle}\n          className=\"xml-toggle-button\"\n          tabIndex={-1}\n          aria-expanded={expanded}\n        >\n          {expanded ? '▼' : '▶'}\n        </button>\n\n        <span className=\"xml-node-name\">\n          {displayNodeName}\n        </span>\n\n        {childCount > 0 && (\n          <span className=\"xml-count\">\n            {`{${childCount}}`}\n          </span>\n        )}\n      </div>\n\n      {expanded && childEntries.length > 0 && (\n        <div>\n          {childEntries.map(([key, value], idx) => {\n            // Check if this is an attribute (starts with _)\n            const isAttribute = key.startsWith('_');\n\n            // Handle attributes\n            if (isAttribute) {\n              const displayValue = value === '' ? 'value' : value;\n\n              return (\n                <div key={key + idx} className=\"flex items-start mb-1\" style={{ paddingLeft: `${(depth + 1) * 20}px` }}>\n                  <span className=\"xml-node-name\">{key}</span>\n                  <span className=\"xml-separator\">:</span>\n                  <span className={value === '' ? 'xml-empty-value' : 'xml-value'}>{displayValue}</span>\n                </div>\n              );\n            }\n\n            // Check if this child is an array\n            const isArrayChild = Array.isArray(value);\n\n            if (isArrayChild) {\n              return (\n                <XmlArrayNode\n                  key={`${key}-${idx}`}\n                  arrayKey={key}\n                  items={value}\n                  depth={depth}\n                  defaultExpanded={true}\n                />\n              );\n            }\n\n            return (\n              <XmlNode\n                key={key + idx}\n                node={value}\n                nodeName={key}\n                isLast={idx === childEntries.length - 1}\n                defaultExpanded={false}\n                depth={depth + 1}\n              />\n            );\n          })}\n        </div>\n      )}\n    </div>\n  );\n};\n\n// Helper function to parse XML string to object\nfunction parseXMLString(xmlString) {\n  if (typeof xmlString !== 'string') return null;\n\n  try {\n    const parser = new DOMParser();\n    // Parse as XML only\n    const xmlDoc = parser.parseFromString(xmlString, 'text/xml');\n\n    // Check for parsing errors\n    const parserError = xmlDoc.querySelector('parsererror');\n    if (parserError) {\n      return null;\n    }\n\n    // Convert XML DOM to object\n    function xmlToObject(node) {\n      if (node.nodeType !== 1) return null; // Not an element node\n\n      const result = {};\n\n      // Get attributes - store them directly with underscore prefix\n      if (node.attributes && node.attributes.length > 0) {\n        for (let i = 0; i < node.attributes.length; i++) {\n          const attr = node.attributes[i];\n          result[`_${attr.name}`] = attr.value || '';\n        }\n      }\n\n      // Get child nodes\n      const childNodes = Array.from(node.childNodes);\n      const elementChildren = childNodes.filter((child) => child.nodeType === 1);\n      const textChildren = childNodes.filter((child) => child.nodeType === 3 && child.textContent.trim());\n\n      // If only text children and no element children, return text content\n      if (elementChildren.length === 0 && textChildren.length > 0) {\n        const textContent = textChildren.map((t) => t.textContent.trim()).join(' ').trim();\n        // If has attributes, store text as a special property\n        if (Object.keys(result).length > 0) {\n          result['_text'] = textContent;\n          return result;\n        }\n        return textContent || null;\n      }\n\n      // Process element children\n      if (elementChildren.length > 0) {\n        const childMap = {};\n        elementChildren.forEach((child) => {\n          const childName = child.nodeName; // Preserve original casing\n          const childValue = xmlToObject(child);\n\n          if (childValue !== null || elementChildren.filter((c) => c.nodeName.toLowerCase() === childName).length > 1) {\n            if (childMap[childName]) {\n              // Multiple children with same name - convert to array\n              if (!Array.isArray(childMap[childName])) {\n                childMap[childName] = [childMap[childName]];\n              }\n              childMap[childName].push(childValue);\n            } else {\n              childMap[childName] = childValue;\n            }\n          }\n        });\n\n        // Merge children into result\n        Object.assign(result, childMap);\n      }\n\n      return Object.keys(result).length > 0 ? result : null;\n    }\n\n    const rootElement = xmlDoc.documentElement;\n    if (!rootElement) return null;\n\n    const parsed = xmlToObject(rootElement);\n    return parsed ? { [rootElement.nodeName]: parsed } : null;\n  } catch (error) {\n    return null;\n  }\n}\n\nfunction isTextNode(node) {\n  return typeof node === 'string' || typeof node === 'number' || node === null;\n}\n\nfunction getChildrenEntries(node) {\n  // Given an XML-like JS object, return an array of [key, value] for all properties\n  // This includes attributes (with _ prefix) and child elements\n  if (typeof node !== 'object' || node === null) return [];\n  return Object.entries(node);\n}\n\nfunction getChildCount(node) {\n  if (Array.isArray(node)) {\n    return node.length;\n  }\n  const children = getChildrenEntries(node);\n  return children.length;\n}\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultPreview/index.js",
    "content": "import React, { useState, useMemo } from 'react';\nimport CodeEditor from 'components/CodeEditor/index';\nimport { get } from 'lodash';\nimport find from 'lodash/find';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { updateResponsePaneScrollPosition } from 'providers/ReduxStore/slices/tabs';\nimport { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport { Document, Page } from 'react-pdf';\nimport 'pdfjs-dist/build/pdf.worker';\nimport 'react-pdf/dist/esm/Page/AnnotationLayer.css';\nimport 'react-pdf/dist/esm/Page/TextLayer.css';\nimport { GlobalWorkerOptions } from 'pdfjs-dist/build/pdf';\nGlobalWorkerOptions.workerSrc = 'pdfjs-dist/legacy/build/pdf.worker.min.mjs';\nimport XmlPreview from './XmlPreview/index';\nimport TextPreview from './TextPreview';\nimport HtmlPreview from './HtmlPreview';\nimport VideoPreview from './VideoPreview';\nimport JsonPreview from './JsonPreview';\n\nconst QueryResultPreview = ({\n  selectedTab,\n  data,\n  dataBuffer,\n  formattedData,\n  item,\n  contentType,\n  collection,\n  codeMirrorMode,\n  previewMode,\n  disableRunEventListener,\n  displayedTheme\n}) => {\n  const preferences = useSelector((state) => state.app.preferences);\n  const tabs = useSelector((state) => state.tabs.tabs);\n  const activeTabUid = useSelector((state) => state.tabs.activeTabUid);\n  const focusedTab = find(tabs, (t) => t.uid === activeTabUid);\n\n  const dispatch = useDispatch();\n\n  const [numPages, setNumPages] = useState(null);\n  function onDocumentLoadSuccess({ numPages }) {\n    setNumPages(numPages);\n  }\n\n  const onRun = () => {\n    if (disableRunEventListener) {\n      return;\n    }\n\n    dispatch(sendRequest(item, collection.uid));\n  };\n\n  const onSave = () => dispatch(saveRequest(item.uid, collection.uid));\n\n  const onScroll = (event) => {\n    dispatch(\n      updateResponsePaneScrollPosition({\n        uid: focusedTab.uid,\n        scrollY: event.doc.scrollTop\n      })\n    );\n  };\n\n  if (selectedTab === 'editor') {\n    return (\n      <CodeEditor\n        collection={collection}\n        font={get(preferences, 'font.codeFont', 'default')}\n        fontSize={get(preferences, 'font.codeFontSize')}\n        theme={displayedTheme}\n        onRun={onRun}\n        onSave={onSave}\n        onScroll={onScroll}\n        value={formattedData}\n        mode={codeMirrorMode}\n        initialScroll={focusedTab.responsePaneScrollPosition || 0}\n        readOnly\n      />\n    );\n  }\n\n  switch (previewMode) {\n    case 'preview-web': {\n      const baseUrl = item.requestSent?.url || '';\n      return <HtmlPreview data={data} baseUrl={baseUrl} />;\n    }\n    case 'preview-image': {\n      return <img src={`data:${contentType.replace(/\\;(.*)/, '')};base64,${dataBuffer}`} />;\n    }\n    case 'preview-pdf': {\n      return (\n        <div className=\"preview-pdf\" style={{ height: '100%', overflow: 'auto', maxHeight: 'calc(100vh - 220px)' }}>\n          <Document file={`data:application/pdf;base64,${dataBuffer}`} onLoadSuccess={onDocumentLoadSuccess}>\n            {Array.from(new Array(numPages), (el, index) => (\n              <Page key={`page_${index + 1}`} pageNumber={index + 1} renderAnnotationLayer={false} />\n            ))}\n          </Document>\n        </div>\n      );\n    }\n    case 'preview-audio': {\n      return (\n        <audio controls src={`data:${contentType.replace(/\\;(.*)/, '')};base64,${dataBuffer}`} className=\"mx-auto\" />\n      );\n    }\n    case 'preview-video': {\n      return <VideoPreview contentType={contentType} dataBuffer={dataBuffer} />;\n    }\n    case 'preview-json': {\n      return <JsonPreview data={data} displayedTheme={displayedTheme} />;\n    }\n\n    case 'preview-text': {\n      return <TextPreview data={data} />;\n    }\n\n    case 'preview-xml': {\n      return <XmlPreview data={data} />;\n    }\n\n    default:\n      return (\n        <div className=\"p-4 flex flex-col items-center justify-center h-full text-center\">\n          <div className=\"text-lg font-semibold text-gray-700 dark:text-gray-200 mb-2\">\n            No Preview Available\n          </div>\n          <div className=\"text-sm text-gray-500 dark:text-gray-400\">\n            Sorry, no preview is available for this content type.\n          </div>\n        </div>\n      );\n  }\n};\n\nexport default QueryResultPreview;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultTypeSelector/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .caret {\n    color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.caret};\n    fill: ${(props) => props.theme.app.collection.toolbar.environmentSelector.caret};\n  }\n\n  .button-dropdown-button {\n    color: ${(props) => props.theme.text};\n    border-color: ${(props) => props.theme.workspace.border};\n\n  }\n\n  .dropdown-divider {\n    background-color: ${(props) => props.theme.dropdown.separator};\n    height: 1px;\n    margin: 4px 0;\n  }\n\n  .active {\n    color: ${(props) => props.theme.primary.text};\n  }\n\n  .icon-muted {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .preview-response-tab-label {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultTypeSelector/index.jsx",
    "content": "import React, { forwardRef, useState } from 'react';\nimport { IconEye, IconCaretDown, IconBraces, IconCode, IconFileCode, IconBrandJavascript, IconFileText, IconHexagons, IconBinaryTree } from '@tabler/icons';\nimport classnames from 'classnames';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport ToggleSwitch from 'components/ToggleSwitch';\nimport StyledWrapper from './StyledWrapper';\n\n// Icon mapping for format options\nconst FORMAT_ICONS = {\n  json: IconBraces,\n  html: IconCode,\n  xml: IconFileCode,\n  javascript: IconBrandJavascript,\n  raw: IconFileText,\n  hex: IconHexagons,\n  base64: IconBinaryTree\n};\n\nconst ButtonIcon = forwardRef(({ disabled, className, style, prefix, selectedLabel, suffix, isActive, ...props }, ref) => {\n  return (\n    <button\n      ref={ref}\n      className={classnames('button-dropdown-button flex items-center gap-1.5 text-xs',\n        'cursor-pointer select-none',\n        'h-7 rounded-[6px] border px-2 transition-colors',\n        { 'opacity-50 cursor-not-allowed': disabled },\n        className)}\n      disabled={disabled}\n      data-testid={props['data-testid']}\n      style={style}\n      role=\"button\"\n      {...props}\n    >\n      {prefix && <span className={isActive ? 'active' : 'icon-muted'}>{prefix}</span>}\n      <span>{selectedLabel}</span>\n      {suffix && <span>{suffix}</span>}\n      {isActive && <IconCaretDown className=\"caret ml-0.5\" size={12} strokeWidth={2} />}\n    </button>\n  );\n});\nButtonIcon.displayName = 'ButtonIcon';\n\nconst QueryResultTypeSelector = ({\n  formatOptions,\n  formatValue,\n  onFormatChange,\n  onPreviewTabSelect,\n  selectedTab,\n  isActiveTab,\n  onTabSelect\n}) => {\n  const [dropdownOpen, setDropdownOpen] = useState(false);\n\n  // Handle dropdown state change - only allow opening when active tab\n  const handleDropdownChange = (open) => {\n    if (!isActiveTab && open) {\n      // First click when not active - select this tab, don't open dropdown\n      onTabSelect?.();\n      return;\n    }\n    setDropdownOpen(open);\n  };\n  // Find the selected item's label\n  const findSelectedLabel = () => {\n    if (formatValue != null) {\n      const selectedItem = formatOptions.find((item) => item.id === formatValue && (item.type === 'item' || !item.type));\n      if (selectedItem) return selectedItem.label;\n    }\n    return formatValue;\n  };\n\n  const selectedLabel = findSelectedLabel();\n\n  // Get the icon for the currently selected format\n  const SelectedFormatIcon = FORMAT_ICONS[formatValue];\n\n  // Determine the prefix icon - eye icon when in preview mode, format icon otherwise\n  const getPrefixIcon = () => {\n    if (selectedTab === 'preview') {\n      return <IconEye size={14} strokeWidth={2} />;\n    }\n    if (SelectedFormatIcon) {\n      return <SelectedFormatIcon size={14} strokeWidth={1.5} />;\n    }\n    return null;\n  };\n\n  // Enhance items with onChange handler and icons\n  const enhancedItems = formatOptions.map((item) => {\n    const IconComponent = FORMAT_ICONS[item.id];\n    return {\n      ...item,\n      leftSection: IconComponent ? <IconComponent size={14} strokeWidth={1.5} /> : null,\n      onClick: () => {\n        if (onFormatChange) {\n          onFormatChange(item.id);\n        }\n      }\n    };\n  });\n\n  const header = (\n    <div className=\"flex items-center justify-between gap-3 py-[0.35rem] px-[0.6rem]\">\n      <span className=\"text-[0.8125rem] preview-response-tab-label\">Preview</span>\n      <ToggleSwitch\n        isOn={selectedTab === 'preview'}\n        handleToggle={(e) => {\n          e.preventDefault();\n          // e.stopPropagation();\n          onPreviewTabSelect(selectedTab === 'preview' ? 'editor' : 'preview');\n        }}\n        size=\"2xs\"\n        data-testid=\"preview-response-tab\"\n        title={selectedTab === 'preview' ? 'Turn off Preview Mode' : 'Turn on Preview Mode'}\n      />\n    </div>\n  );\n\n  return (\n    <StyledWrapper className={isActiveTab ? 'tab-active' : ''}>\n      <MenuDropdown\n        items={enhancedItems}\n        header={header}\n        selectedItemId={formatValue}\n        showTickMark={true}\n        placement=\"bottom-end\"\n        data-testid=\"format-response-tab\"\n        opened={dropdownOpen}\n        onChange={handleDropdownChange}\n      >\n        <ButtonIcon\n          selectedLabel={selectedLabel}\n          prefix={getPrefixIcon()}\n          isActive={isActiveTab}\n          disabled={false}\n          className=\"h-[22px] text-[10px]\"\n          data-testid=\"format-response-tab\"\n        />\n      </MenuDropdown>\n    </StyledWrapper>\n  );\n};\n\nexport default QueryResultTypeSelector;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/QueryResult/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n\n  /* This is a hack to force Codemirror to use all available space */\n  > div {\n    position: relative;\n  }\n\n  div.CodeMirror {\n    position: absolute;\n    top: 0;\n    bottom: 0;\n    height: 100%;\n    width: 100%;\n  }\n\n  .react-pdf__Page {\n    margin-top: 10px;\n    background-color: transparent !important;\n  }\n  .react-pdf__Page__textContent {\n    border: 1px solid darkgrey;\n    box-shadow: 5px 5px 5px 1px #ccc;\n    border-radius: 0px;\n    margin: 0 auto;\n  }\n  .react-pdf__Page__canvas {\n    margin: 0 auto;\n  }\n  div[role='tablist'] {\n    .active {\n      color: ${(props) => props.theme.colors.text.yellow};\n    }\n  }\n\n  .muted {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .error {\n    color: ${(props) => props.theme.colors.text.danger};\n  }\n\n  .response-filter {\n    position: absolute;\n    bottom: 0;\n    width: 100%;\n\n    input {\n      border: solid 1px ${(props) => props.theme.border.border2};\n      border-radius: ${(props) => props.theme.border.radius.sm};\n      background-color: ${(props) => props.theme.background.base};\n\n      &:focus {\n        outline: none;\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/QueryResult/index.js",
    "content": "import { debounce } from 'lodash';\nimport { useTheme } from 'providers/Theme/index';\nimport React, { useMemo, useState } from 'react';\nimport { formatResponse, getContentType } from 'utils/common';\nimport { getDefaultResponseFormat, detectContentTypeFromBase64 } from 'utils/response';\nimport LargeResponseWarning from '../LargeResponseWarning';\nimport QueryResultFilter from './QueryResultFilter';\nimport QueryResultPreview from './QueryResultPreview';\nimport StyledWrapper from './StyledWrapper';\n\n// Raw format options (for byte format types)\nconst RAW_FORMAT_OPTIONS = [\n  { id: 'raw', label: 'Raw', type: 'item', codeMirrorMode: 'text/plain' },\n  { id: 'hex', label: 'Hex', type: 'item', codeMirrorMode: 'text/plain' },\n  { id: 'base64', label: 'Base64', type: 'item', codeMirrorMode: 'text/plain' }\n];\n\n// Preview format options\nconst PREVIEW_FORMAT_OPTIONS = [\n  // Structured formats\n  { id: 'json', label: 'JSON', type: 'item', codeMirrorMode: 'application/ld+json' },\n  { id: 'html', label: 'HTML', type: 'item', codeMirrorMode: 'xml' },\n  { id: 'xml', label: 'XML', type: 'item', codeMirrorMode: 'xml' },\n  { id: 'javascript', label: 'JavaScript', type: 'item', codeMirrorMode: 'javascript' },\n  // Divider\n  { type: 'divider', id: 'divider-structured-raw' },\n  // Raw formats\n  ...RAW_FORMAT_OPTIONS\n];\n\nconst formatErrorMessage = (error) => {\n  if (!error) return 'Something went wrong';\n\n  const remoteMethodError = 'Error invoking remote method \\'send-http-request\\':';\n\n  if (error?.includes(remoteMethodError)) {\n    const parts = error.split(remoteMethodError);\n    return parts[1]?.trim() || error;\n  }\n\n  return error;\n};\n\n// Custom hook to determine the initial format and tab based on the data buffer and headers\nexport const useInitialResponseFormat = (dataBuffer, headers) => {\n  return useMemo(() => {\n    const detectedContentType = detectContentTypeFromBase64(dataBuffer);\n    const contentType = getContentType(headers);\n\n    // Wait until both content types are available\n    if (detectedContentType === null || contentType === undefined) {\n      return { initialFormat: null, initialTab: null, contentType: contentType };\n    }\n\n    const initial = getDefaultResponseFormat(contentType);\n    return { initialFormat: initial.format, initialTab: initial.tab, contentType: contentType };\n  }, [dataBuffer, headers]);\n};\n\n// Custom hook to determine preview format options based on content type\nexport const useResponsePreviewFormatOptions = (dataBuffer, headers) => {\n  return useMemo(() => {\n    const detectedContentType = detectContentTypeFromBase64(dataBuffer);\n    const contentType = getContentType(headers);\n\n    const byteFormatTypes = ['image', 'video', 'audio', 'pdf', 'zip'];\n\n    const isByteFormatType = (contentType) => {\n      if (contentType.toLowerCase().includes('svg')) return false; // SVG is text-based\n      return byteFormatTypes.some((type) => contentType.includes(type));\n    };\n\n    const getContentTypeToCheck = () => {\n      if (detectedContentType) {\n        return detectedContentType;\n      }\n      return contentType;\n    };\n\n    const contentTypeToCheck = getContentTypeToCheck();\n\n    if (contentTypeToCheck && isByteFormatType(contentTypeToCheck)) {\n      // Return only raw format options (no structured formats)\n      return RAW_FORMAT_OPTIONS;\n    }\n\n    // Return all format options\n    return PREVIEW_FORMAT_OPTIONS;\n  }, [dataBuffer, headers]);\n};\n\nconst QueryResult = ({\n  item,\n  collection,\n  data,\n  dataBuffer,\n  disableRunEventListener,\n  headers,\n  error,\n  selectedFormat, // one of the options in PREVIEW_FORMAT_OPTIONS\n  selectedTab // 'editor' or 'preview'\n}) => {\n  const contentType = getContentType(headers);\n  const [filter, setFilter] = useState(null);\n  const [showLargeResponse, setShowLargeResponse] = useState(false);\n  const { displayedTheme } = useTheme();\n\n  const responseSize = useMemo(() => {\n    const response = item.response || {};\n    if (typeof response.size === 'number') {\n      return response.size;\n    }\n\n    // Fallback: estimate from base64 length (base64 is ~4/3 of original size)\n    if (dataBuffer && typeof dataBuffer === 'string') {\n      return Math.floor(dataBuffer.length * 0.75);\n    }\n    return 0;\n  }, [dataBuffer, item.response]);\n\n  const isLargeResponse = responseSize > 10 * 1024 * 1024; // 10 MB\n\n  const detectedContentType = useMemo(() => {\n    return detectContentTypeFromBase64(dataBuffer);\n  }, [dataBuffer, isLargeResponse]);\n\n  const formattedData = useMemo(\n    () => {\n      if (isLargeResponse && !showLargeResponse) {\n        return '';\n      }\n      return formatResponse(data, dataBuffer, selectedFormat, filter);\n    },\n    [data, dataBuffer, selectedFormat, filter, isLargeResponse, showLargeResponse]\n  );\n\n  const debouncedResultFilterOnChange = debounce((e) => {\n    setFilter(e.target.value);\n  }, 250);\n\n  const previewMode = useMemo(() => {\n    // Derive preview mode based on selected format\n    if (selectedFormat === 'html') return 'preview-web';\n    if (selectedFormat === 'json') return 'preview-json';\n    if (selectedFormat === 'xml') return 'preview-xml';\n    if (selectedFormat === 'raw') return 'preview-text';\n    if (selectedFormat === 'javascript') return 'preview-web';\n\n    // For base64/hex, check content type to determine binary preview type\n    if (selectedFormat === 'base64' || selectedFormat === 'hex') {\n      if (detectedContentType) {\n        if (detectedContentType.includes('image')) return 'preview-image';\n        if (detectedContentType.includes('pdf')) return 'preview-pdf';\n        if (detectedContentType.includes('audio')) return 'preview-audio';\n        if (detectedContentType.includes('video')) return 'preview-video';\n      }\n      // for all other content types, return preview-text\n      return 'preview-text';\n    }\n    return 'preview-text';\n  }, [selectedFormat, detectedContentType]);\n\n  const codeMirrorMode = useMemo(() => {\n    // Find the codeMirrorMode from PREVIEW_FORMAT_OPTIONS (contains all format options)\n    return PREVIEW_FORMAT_OPTIONS\n      .filter((option) => option.type === 'item' || !option.type)\n      .find((option) => option.id === selectedFormat)?.codeMirrorMode || 'text/plain';\n  }, [selectedFormat]);\n\n  const queryFilterEnabled = useMemo(() => codeMirrorMode.includes('json') && selectedFormat === 'json' && selectedTab === 'editor', [codeMirrorMode, selectedFormat, selectedTab]);\n  const hasScriptError = item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage;\n\n  return (\n    <StyledWrapper\n      className=\"w-full h-full relative flex\"\n      queryFilterEnabled={queryFilterEnabled}\n    >\n      {error ? (\n        <div>\n          {hasScriptError ? null : (\n            <div className=\"error\" style={{ whiteSpace: 'pre-line' }}>{formatErrorMessage(error)}</div>\n          )}\n\n          {error && typeof error === 'string' && error.toLowerCase().includes('self signed certificate') ? (\n            <div className=\"mt-6 muted text-xs\">\n              You can disable SSL verification in the Preferences. <br />\n              To open the Preferences, click on the gear icon in the bottom left corner.\n            </div>\n          ) : null}\n        </div>\n      ) : isLargeResponse && !showLargeResponse ? (\n        <LargeResponseWarning\n          item={item}\n          responseSize={responseSize}\n          onRevealResponse={() => setShowLargeResponse(true)}\n        />\n      ) : (\n        <div className=\"h-full flex flex-col\">\n          <div className=\"flex-1 relative\">\n            <div className=\"absolute top-0 left-0 h-full w-full\" data-testid=\"response-preview-container\">\n              <QueryResultPreview\n                selectedTab={selectedTab}\n                data={data}\n                dataBuffer={dataBuffer}\n                formattedData={formattedData}\n                item={item}\n                contentType={detectedContentType ?? contentType}\n                previewMode={previewMode}\n                codeMirrorMode={codeMirrorMode}\n                collection={collection}\n                disableRunEventListener={disableRunEventListener}\n                displayedTheme={displayedTheme}\n              />\n            </div>\n            {queryFilterEnabled && (\n              <QueryResultFilter filter={filter} onChange={debouncedResultFilterOnChange} mode={codeMirrorMode} />\n            )}\n          </div>\n        </div>\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default QueryResult;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponseActions/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  button {\n    color: ${(props) => props.theme.colors.text.subtext0};\n    cursor: pointer;\n\n    &:hover {\n      color: var(--color-tab-active);\n    }\n\n    &:disabled {\n      cursor: not-allowed;\n      opacity: 0.5;\n    }\n  }\n\n  .cursor-pointer {\n    display: flex;\n    align-items: center;\n    color: ${(props) => props.theme.colors.text.subtext0};\n\n    &:hover {\n      color: var(--color-tab-active);\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponseActions/index.js",
    "content": "import React, { useRef, forwardRef } from 'react';\nimport { IconDots } from '@tabler/icons';\nimport Dropdown from 'components/Dropdown';\nimport StyledWrapper from './StyledWrapper';\nimport ResponseClear from 'src/components/ResponsePane/ResponseClear';\nimport ResponseDownload from 'src/components/ResponsePane/ResponseDownload';\n\nconst ResponseActions = ({ collection, item }) => {\n  const menuDropdownTippyRef = useRef();\n\n  const onMenuDropdownCreate = (ref) => (menuDropdownTippyRef.current = ref);\n\n  const MenuIcon = forwardRef((_props, ref) => {\n    return (\n      <div ref={ref} className=\"cursor-pointer\">\n        <IconDots size={18} strokeWidth={1.5} />\n      </div>\n    );\n  });\n\n  const handleClose = () => {\n    menuDropdownTippyRef.current.hide();\n  };\n\n  return (\n    <StyledWrapper className=\"ml-2 flex items-center\">\n      <Dropdown onCreate={onMenuDropdownCreate} icon={<MenuIcon />} placement=\"bottom-end\">\n        <ResponseClear item={item} collection={collection} asDropdownItem onClose={handleClose} />\n        <ResponseDownload item={item} asDropdownItem onClose={handleClose} />\n      </Dropdown>\n    </StyledWrapper>\n  );\n};\n\nexport default ResponseActions;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponseBookmark/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  align-items: center;\n  color: ${(props) => props.theme.dropdown.iconColor};\n  border-radius: 4px;\n\n  &:hover {\n    background-color: ${(props) => props.theme.workspace.button.bg};\n    color: ${(props) => props.theme.text};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponseBookmark/index.js",
    "content": "import React, { useState, useMemo, forwardRef, useImperativeHandle, useRef } from 'react';\nimport { useDispatch } from 'react-redux';\nimport { IconBookmark } from '@tabler/icons';\nimport { addResponseExample } from 'providers/ReduxStore/slices/collections';\nimport { saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport { insertTaskIntoQueue } from 'providers/ReduxStore/slices/app';\nimport { uuid, formatResponse } from 'utils/common';\nimport toast from 'react-hot-toast';\nimport CreateExampleModal from 'components/ResponseExample/CreateExampleModal';\nimport { getBodyType } from 'utils/responseBodyProcessor';\nimport { getInitialExampleName } from 'utils/collections/index';\nimport classnames from 'classnames';\nimport StyledWrapper from './StyledWrapper';\nimport ActionIcon from 'ui/ActionIcon/index';\n\nconst getTitleText = ({ isResponseTooLarge, isStreamingResponse }) => {\n  if (isStreamingResponse) {\n    return 'Response Examples aren\\'t supported in streaming responses yet.';\n  }\n\n  if (isResponseTooLarge) {\n    return 'Response size exceeds 5MB limit. Cannot save as example.';\n  }\n\n  return 'Save current response as example';\n};\n\nconst ResponseBookmark = forwardRef(({ item, collection, responseSize, children }, ref) => {\n  const dispatch = useDispatch();\n  const [showSaveResponseExampleModal, setShowSaveResponseExampleModal] = useState(false);\n  const response = item.response || {};\n  const elementRef = useRef(null);\n\n  const isResponseTooLarge = responseSize >= 5 * 1024 * 1024; // 5 MB\n  const isStreamingResponse = response.stream;\n  const isDisabled = isResponseTooLarge || isStreamingResponse ? true : false;\n\n  useImperativeHandle(ref, () => ({\n    click: () => elementRef.current?.click(),\n    isDisabled\n  }), [isDisabled]);\n\n  // Only show for HTTP requests\n  if (item.type !== 'http-request') {\n    return null;\n  }\n\n  const handleSaveClick = (e) => {\n    if (!response || response.error) {\n      toast.error('No valid response to save as example');\n      e.preventDefault();\n      e.stopPropagation();\n      return;\n    }\n\n    if (isResponseTooLarge) {\n      toast.error('Response size exceeds 5MB limit. Cannot save as example.');\n      e.preventDefault();\n      e.stopPropagation();\n      return;\n    }\n\n    if (isDisabled) {\n      e.preventDefault();\n      e.stopPropagation();\n      return;\n    }\n\n    setShowSaveResponseExampleModal(true);\n  };\n\n  const saveAsExample = async (name, description = '') => {\n    // Convert headers object to array format expected by schema\n    const headersArray = response.headers && typeof response.headers === 'object'\n      ? Object.entries(response.headers).map(([name, value]) => ({\n          name,\n          value,\n          enabled: true\n        }))\n      : [];\n\n    const contentTypeHeader = headersArray.find((h) => h.name?.toLowerCase() === 'content-type');\n    const contentType = contentTypeHeader?.value?.toLowerCase() || '';\n\n    const bodyType = getBodyType(contentType);\n    const content = formatResponse(response.data, response.dataBuffer, bodyType);\n\n    const exampleData = {\n      name: name,\n      status: response.status || 200,\n      headers: headersArray,\n      body: {\n        type: bodyType,\n        content: content\n      },\n      description: description\n    };\n\n    // Calculate the index where the example will be saved\n    // This will be the length of the examples array after adding the new one\n    const existingExamples = item.draft?.examples || item.examples || [];\n    const exampleIndex = existingExamples.length;\n    const exampleUid = uuid();\n\n    dispatch(addResponseExample({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      example: {\n        ...exampleData,\n        uid: exampleUid\n      }\n    }));\n\n    // Save the request\n    await dispatch(saveRequest(item.uid, collection.uid, true));\n\n    // Task middleware will track this and open the example in a new tab once the file is reloaded\n    dispatch(insertTaskIntoQueue({\n      uid: exampleUid,\n      type: 'OPEN_EXAMPLE',\n      collectionUid: collection.uid,\n      itemUid: item.uid,\n      exampleIndex: exampleIndex\n    }));\n\n    setShowSaveResponseExampleModal(false);\n    toast.success(`Example \"${name}\" created successfully`);\n  };\n\n  const disabledMessage = getTitleText({\n    isResponseTooLarge,\n    isStreamingResponse\n  });\n\n  return (\n    <>\n      <div\n        ref={elementRef}\n        onClick={handleSaveClick}\n        title={\n          !children ? disabledMessage : (isDisabled ? disabledMessage : null)\n        }\n        className={classnames({\n          'opacity-50 cursor-not-allowed': isDisabled && !children\n        })}\n        data-testid=\"response-bookmark-btn\"\n      >\n        {children ?? (\n          <StyledWrapper className=\"flex items-center\">\n            <ActionIcon className=\"p-1\" disabled={isDisabled}>\n              <IconBookmark size={16} strokeWidth={2} />\n            </ActionIcon>\n          </StyledWrapper>\n        )}\n      </div>\n\n      <CreateExampleModal\n        isOpen={showSaveResponseExampleModal}\n        onClose={() => setShowSaveResponseExampleModal(false)}\n        onSave={saveAsExample}\n        title=\"Save Response as Example\"\n        initialName={getInitialExampleName(item)}\n      />\n    </>\n  );\n});\n\nResponseBookmark.displayName = 'ResponseBookmark';\n\nexport default ResponseBookmark;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponseClear/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  font-size: ${(props) => props.theme.font.size.base};\n  color: ${(props) => props.theme.dropdown.iconColor};\n  border-radius: 4px;\n\n  &:hover {\n    background-color: ${(props) => props.theme.workspace.button.bg};\n    color: ${(props) => props.theme.text};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponseClear/index.js",
    "content": "import React, { forwardRef, useImperativeHandle, useRef } from 'react';\nimport { IconEraser } from '@tabler/icons';\nimport { useDispatch } from 'react-redux';\nimport StyledWrapper from './StyledWrapper';\nimport { responseCleared } from 'providers/ReduxStore/slices/collections/index';\nimport ActionIcon from 'ui/ActionIcon/index';\n\n// Hook to get clear response function\nexport const useResponseClear = (item, collection) => {\n  const dispatch = useDispatch();\n\n  const clearResponse = () => {\n    dispatch(\n      responseCleared({\n        itemUid: item.uid,\n        collectionUid: collection.uid,\n        response: null\n      })\n    );\n  };\n\n  return { clearResponse };\n};\n\nconst ResponseClear = forwardRef(({ collection, item, children }, ref) => {\n  const { clearResponse } = useResponseClear(item, collection);\n  const elementRef = useRef(null);\n\n  useImperativeHandle(ref, () => ({\n    click: () => elementRef.current?.click(),\n    isDisabled: false\n  }), []);\n\n  return (\n    <div ref={elementRef} onClick={clearResponse} title={!children ? 'Clear response' : null} data-testid=\"response-clear-btn\">\n      {children ? children : (\n        <StyledWrapper className=\"flex items-center\">\n          <ActionIcon className=\"p-1\">\n            <IconEraser size={16} strokeWidth={2} />\n          </ActionIcon>\n        </StyledWrapper>\n      )}\n    </div>\n  );\n});\n\nResponseClear.displayName = 'ResponseClear';\n\nexport default ResponseClear;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponseCopy/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  font-size: 0.8125rem;\n  color: ${(props) => props.theme.dropdown.iconColor};\n  border-radius: 4px;\n\n  &:hover {\n    background-color: ${(props) => props.theme.workspace.button.bg};\n    color: ${(props) => props.theme.text};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponseCopy/index.js",
    "content": "import React, { useState, useEffect, forwardRef, useImperativeHandle, useRef, useCallback } from 'react';\nimport StyledWrapper from './StyledWrapper';\nimport toast from 'react-hot-toast';\nimport { IconCopy, IconCheck } from '@tabler/icons';\nimport classnames from 'classnames';\nimport ActionIcon from 'ui/ActionIcon/index';\nimport { formatResponse } from 'utils/common';\n\n// Helper function to get text to copy\nconst getTextToCopy = (selectedTab, selectedFormat, data, dataBuffer) => {\n  // If preview is on, copy raw data (what's shown in TextPreview)\n  if (selectedTab === 'preview') {\n    return typeof data === 'string' ? data : JSON.stringify(data, null, 2);\n  }\n  // If editor is on, copy formatted data based on selected format\n  if (selectedFormat && data && dataBuffer) {\n    return formatResponse(data, dataBuffer, selectedFormat, null);\n  }\n  return typeof data === 'string' ? data : JSON.stringify(data, null, 2);\n};\n\n// Hook to get copy response function\nexport const useResponseCopy = (item, selectedFormat, selectedTab, data, dataBuffer) => {\n  const [copied, setCopied] = useState(false);\n\n  useEffect(() => {\n    if (copied) {\n      const timer = setTimeout(() => {\n        setCopied(false);\n      }, 2000);\n      return () => clearTimeout(timer);\n    }\n  }, [copied]);\n\n  const copyResponse = useCallback(async () => {\n    try {\n      const textToCopy = getTextToCopy(selectedTab, selectedFormat, data, dataBuffer);\n      await navigator.clipboard.writeText(textToCopy);\n      toast.success('Response copied to clipboard');\n      setCopied(true);\n    } catch (error) {\n      toast.error('Failed to copy response');\n    }\n  }, [selectedTab, selectedFormat, data, dataBuffer]);\n\n  return { copyResponse, copied, hasData: !!data };\n};\n\nconst ResponseCopy = forwardRef(({ item, children, selectedFormat, selectedTab, data, dataBuffer }, ref) => {\n  const { copyResponse, copied, hasData } = useResponseCopy(item, selectedFormat, selectedTab, data, dataBuffer);\n  const elementRef = useRef(null);\n\n  const isDisabled = !hasData ? true : false;\n\n  useImperativeHandle(ref, () => ({\n    click: () => elementRef.current?.click(),\n    isDisabled\n  }), [isDisabled]);\n\n  const handleKeyDown = (e) => {\n    if ((e.key === 'Enter' || e.key === ' ') && hasData) {\n      e.preventDefault();\n      copyResponse();\n    }\n  };\n\n  const handleClick = () => {\n    if (hasData) {\n      copyResponse();\n    }\n  };\n\n  return (\n    <div\n      ref={elementRef}\n      onClick={handleClick}\n      title={!children ? 'Copy response to clipboard' : null}\n      onKeyDown={handleKeyDown}\n      aria-disabled={isDisabled}\n      className={classnames({\n        'opacity-50 cursor-not-allowed': isDisabled && !children\n      })}\n      data-testid=\"response-copy-btn\"\n    >\n      {children ? children : (\n        <StyledWrapper className=\"flex items-center\">\n          <ActionIcon className=\"p-1\" disabled={isDisabled}>\n            {copied ? (\n              <IconCheck size={16} strokeWidth={2} />\n            ) : (\n              <IconCopy size={16} strokeWidth={2} />\n            )}\n          </ActionIcon>\n        </StyledWrapper>\n      )}\n    </div>\n  );\n});\n\nResponseCopy.displayName = 'ResponseCopy';\n\nexport default ResponseCopy;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponseDownload/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  font-size: ${(props) => props.theme.font.size.base};\n  color: ${(props) => props.theme.dropdown.iconColor};\n  border-radius: 4px;\n\n  &:hover {\n    background-color: ${(props) => props.theme.workspace.button.bg};\n    color: ${(props) => props.theme.text};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponseDownload/index.js",
    "content": "import React, { forwardRef, useImperativeHandle, useRef } from 'react';\nimport StyledWrapper from './StyledWrapper';\nimport toast from 'react-hot-toast';\nimport get from 'lodash/get';\nimport { IconDownload } from '@tabler/icons';\nimport classnames from 'classnames';\nimport ActionIcon from 'ui/ActionIcon/index';\n\nconst ResponseDownload = forwardRef(({ item, children }, ref) => {\n  const { ipcRenderer } = window;\n  const response = item.response || {};\n  const isDisabled = !response.dataBuffer || response.stream?.running;\n  const elementRef = useRef(null);\n\n  useImperativeHandle(ref, () => ({\n    click: () => elementRef.current?.click(),\n    isDisabled\n  }), [isDisabled]);\n\n  const saveResponseToFile = () => {\n    if (isDisabled) {\n      return;\n    }\n    return new Promise((resolve, reject) => {\n      ipcRenderer\n        .invoke('renderer:save-response-to-file', response, item?.requestSent?.url, item.pathname)\n        .then((result) => {\n          if (result && result.success) {\n            toast.success('Response downloaded to file');\n          }\n          resolve();\n        })\n        .catch((err) => {\n          toast.error(get(err, 'error.message') || 'Something went wrong!');\n          reject(err);\n        });\n    });\n  };\n\n  return (\n    <div\n      ref={elementRef}\n      aria-disabled={isDisabled}\n      onClick={saveResponseToFile}\n      title={!children ? 'Save response to file' : null}\n      className={classnames({\n        'opacity-50 cursor-not-allowed': isDisabled && !children\n      })}\n      data-testid=\"response-download-btn\"\n    >\n      {children ? children : (\n        <StyledWrapper className=\"flex items-center\">\n          <ActionIcon className=\"p-1\" disabled={isDisabled}>\n            <IconDownload size={16} strokeWidth={2} />\n          </ActionIcon>\n        </StyledWrapper>\n      )}\n    </div>\n  );\n});\n\nResponseDownload.displayName = 'ResponseDownload';\n\nexport default ResponseDownload;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponseHeaders/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  .table-wrapper {\n    border: 1px solid ${(props) => props.theme.table.border};\n    border-radius: 4px;\n    overflow: hidden;\n    font-size: ${(props) => props.theme.font.size.sm};\n  }\n\n  table {\n    width: 100%;\n    border-collapse: collapse;\n\n    thead {\n      color: ${(props) => props.theme.table.thead.color};\n      font-size: ${(props) => props.theme.font.size.sm};\n      font-weight: 500;\n\n      td {\n        border-top: none;\n      }\n    }\n\n    td {\n      border: 1px solid ${(props) => props.theme.table.border};\n      padding: 4px 8px;\n\n      &:first-child {\n        border-left: none;\n      }\n\n      &:last-child {\n        border-right: none;\n      }\n\n      &.value {\n        word-break: break-all;\n      }\n    }\n\n    tbody {\n      tr:nth-child(odd) {\n        background-color: ${(props) => props.theme.table.striped};\n      }\n\n      tr:last-child td {\n        border-bottom: none;\n      }\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponseHeaders/index.js",
    "content": "import React from 'react';\nimport StyledWrapper from './StyledWrapper';\n\nconst ResponseHeaders = ({ headers }) => {\n  const headersArray = typeof headers === 'object' ? Object.entries(headers) : [];\n\n  return (\n    <StyledWrapper className=\"pb-4 w-full\">\n      <div className=\"table-wrapper\">\n        <table>\n          <thead>\n            <tr>\n              <td>Name</td>\n              <td>Value</td>\n            </tr>\n          </thead>\n          <tbody>\n            {headersArray && headersArray.length\n              ? headersArray.map((header, index) => {\n                  return (\n                    <tr key={index}>\n                      <td className=\"key\">{header[0]}</td>\n                      <td className=\"value\">{header[1]}</td>\n                    </tr>\n                  );\n                })\n              : null}\n          </tbody>\n        </table>\n      </div>\n    </StyledWrapper>\n  );\n};\nexport default ResponseHeaders;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponseLayoutToggle/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  button {\n    display: flex;\n    align-items: center;\n    padding: 0.25rem;\n    background: transparent;\n    border: none;\n    cursor: pointer;\n    color: ${(props) => props.theme.dropdown.iconColor};\n    border-radius: 4px;\n\n    &:hover {\n      background-color: ${(props) => props.theme.workspace.button.bg};\n      color: ${(props) => props.theme.text};\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponseLayoutToggle/index.js",
    "content": "import React, { forwardRef, useImperativeHandle, useRef } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { savePreferences } from 'providers/ReduxStore/slices/app';\nimport StyledWrapper from './StyledWrapper';\nimport { IconLayoutColumns, IconLayoutRows } from '@tabler/icons';\nimport ActionIcon from 'ui/ActionIcon/index';\n\nexport const IconDockToBottom = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"16\"\n      height=\"16\"\n      viewBox=\"0 0 24 24\"\n      strokeWidth=\"2\"\n      stroke=\"currentColor\"\n      fill=\"none\"\n    >\n      <path stroke=\"none\" fill=\"none\" d=\"M0 0h24v24H0z\" />\n      <path d=\"M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z\" />\n      <path d=\"M4 15l16 0\" />\n      <path\n        fill=\"currentColor\"\n        d=\"M 5.5135136,19.111502 C 5.2542477,18.995986 5.0221761,18.756859 4.8928709,18.47199 4.7922381,18.250288 4.7788524,18.078909 4.7777079,16.997543 l -0.0013,-1.223586 H 12 19.223587 v 1.22675 c 0,1.194609 -0.0039,1.234605 -0.149369,1.526503 -0.09333,0.187285 -0.240773,0.363095 -0.392978,0.46858 l -0.243606,0.168829 -6.373606,0.0129 c -5.2129418,0.0105 -6.4058225,-0.0015 -6.5505114,-0.06597 z\"\n      />\n    </svg>\n  );\n};\n\nexport const IconDockToRight = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"16\"\n      height=\"16\"\n      viewBox=\"0 0 24 24\"\n      strokeWidth=\"2\"\n      stroke=\"currentColor\"\n      fill=\"none\"\n    >\n      <path fill=\"none\" stroke=\"none\" d=\"M 0,24 V 0 h 24 v 24 z\" />\n      <path d=\"m 4,20 m 2,0 A 2,2 0 0 1 4,18 V 6 A 2,2 0 0 1 6,4 h 12 a 2,2 0 0 1 2,2 v 12 a 2,2 0 0 1 -2,2 z\" />\n      <path d=\"M 15,20 V 4\" />\n      <path\n        fill=\"currentColor\"\n        stroke=\"currentColor\"\n        d=\"m 19.111502,18.486486 c -0.115516,0.259266 -0.354643,0.491338 -0.639512,0.620643 -0.221702,0.100633 -0.393081,0.114019 -1.474447,0.115163 l -1.223586,0.0013 V 12 4.7764125 h 1.22675 c 1.194609,0 1.234605,0.0039 1.526503,0.14937 0.187285,0.09333 0.363095,0.2407725 0.46858,0.3929775 l 0.168829,0.243606 0.0129,6.373606 c 0.0105,5.212942 -0.0015,6.405822 -0.06597,6.550511 z\"\n      />\n    </svg>\n  );\n};\n\n// Hook to get orientation and toggle function\nexport const useResponseLayoutToggle = () => {\n  const dispatch = useDispatch();\n  const preferences = useSelector((state) => state.app.preferences);\n  const orientation = preferences?.layout?.responsePaneOrientation || 'horizontal';\n\n  const toggleOrientation = () => {\n    const newOrientation = orientation === 'horizontal' ? 'vertical' : 'horizontal';\n    const updatedPreferences = {\n      ...preferences,\n      layout: {\n        ...preferences.layout,\n        responsePaneOrientation: newOrientation\n      }\n    };\n    dispatch(savePreferences(updatedPreferences));\n  };\n\n  return { orientation, toggleOrientation };\n};\n\nconst ResponseLayoutToggle = forwardRef(({ children }, ref) => {\n  const { orientation, toggleOrientation } = useResponseLayoutToggle();\n  const elementRef = useRef(null);\n\n  useImperativeHandle(ref, () => ({\n    click: () => elementRef.current?.click(),\n    isDisabled: false\n  }), []);\n\n  const title = !children ? (orientation === 'horizontal' ? 'Switch to vertical layout' : 'Switch to horizontal layout') : null;\n\n  return (\n    <div\n      ref={elementRef}\n      onClick={toggleOrientation}\n      title={title}\n      data-testid=\"response-layout-toggle-btn\"\n    >\n      {children ? children : (\n        <StyledWrapper className=\"flex items-center w-full\">\n          <ActionIcon size=\"lg\" className=\"p-1\">\n            {orientation === 'vertical' ? (\n              <IconLayoutColumns size={16} strokeWidth={2} />\n            ) : (\n              <IconLayoutRows size={16} strokeWidth={2} />\n            )}\n          </ActionIcon>\n        </StyledWrapper>\n      )}\n    </div>\n  );\n});\n\nResponseLayoutToggle.displayName = 'ResponseLayoutToggle';\n\nexport default ResponseLayoutToggle;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponseLayoutToggle/index.spec.js",
    "content": "import '@testing-library/jest-dom';\nimport React from 'react';\nimport { render, screen, fireEvent } from '@testing-library/react';\nimport { Provider } from 'react-redux';\nimport { ThemeProvider } from 'providers/Theme';\nimport { configureStore, createSlice } from '@reduxjs/toolkit';\nimport ResponseLayoutToggle from './index';\n\nconst mockSavePreferences = jest.fn((payload) => ({ type: 'app/savePreferences', payload }));\n\n// Mock the savePreferences action\njest.mock('providers/ReduxStore/slices/app', () => ({\n  savePreferences: (payload) => mockSavePreferences(payload)\n}));\n\n// Mock localStorage\nconst mockLocalStorage = {\n  getItem: jest.fn(() => 'dark'),\n  setItem: jest.fn(),\n  removeItem: jest.fn()\n};\n\n// Mock matchMedia\nbeforeAll(() => {\n  Object.defineProperty(window, 'matchMedia', {\n    writable: true,\n    value: jest.fn().mockImplementation((query) => ({\n      matches: false,\n      media: query,\n      addEventListener: jest.fn(),\n      removeEventListener: jest.fn()\n    }))\n  });\n  Object.defineProperty(window, 'localStorage', {\n    value: mockLocalStorage\n  });\n});\n\nbeforeEach(() => {\n  mockSavePreferences.mockClear();\n});\n\nconst initialState = {\n  app: {\n    preferences: {\n      layout: {\n        responsePaneOrientation: 'horizontal'\n      }\n    }\n  }\n};\n\nconst createTestStore = (initialState) => {\n  const appSlice = createSlice({\n    name: 'app',\n    initialState: initialState.app,\n    reducers: {\n      savePreferences: (state, action) => {\n        state.preferences = action.payload;\n      }\n    }\n  });\n\n  return configureStore({\n    reducer: { app: appSlice.reducer }\n  });\n};\n\nconst renderWithProviders = (component, customState = initialState) => {\n  const store = createTestStore(customState);\n  return {\n    store,\n    ...render(\n      <Provider store={store}>\n        <ThemeProvider>\n          {component}\n        </ThemeProvider>\n      </Provider>\n    )\n  };\n};\n\ndescribe('ResponseLayoutToggle', () => {\n  describe('Initial Render', () => {\n    it('should render with horizontal orientation by default', () => {\n      renderWithProviders(<ResponseLayoutToggle />);\n      const button = screen.getByTestId('response-layout-toggle-btn');\n      expect(button).toBeInTheDocument();\n      expect(button).toHaveAttribute('title', 'Switch to vertical layout');\n    });\n\n    it('should render with vertical orientation when specified', () => {\n      const customState = {\n        app: {\n          preferences: {\n            layout: {\n              responsePaneOrientation: 'vertical'\n            }\n          }\n        }\n      };\n      renderWithProviders(<ResponseLayoutToggle />, customState);\n      const button = screen.getByTestId('response-layout-toggle-btn');\n      expect(button).toBeInTheDocument();\n      expect(button).toHaveAttribute('title', 'Switch to horizontal layout');\n    });\n  });\n\n  describe('Interaction', () => {\n    it('should switch to vertical layout when clicked in horizontal mode', () => {\n      const { store } = renderWithProviders(<ResponseLayoutToggle />);\n      const button = screen.getByTestId('response-layout-toggle-btn');\n\n      // Initial state check\n      expect(button).toHaveAttribute('title', 'Switch to vertical layout');\n\n      fireEvent.click(button);\n\n      // Check if action was called\n      expect(mockSavePreferences).toHaveBeenCalledWith({\n        layout: {\n          responsePaneOrientation: 'vertical'\n        }\n      });\n\n      // Manually update store to simulate state change\n      store.dispatch(mockSavePreferences({\n        layout: {\n          responsePaneOrientation: 'vertical'\n        }\n      }));\n\n      // Check if button title was updated\n      expect(button).toHaveAttribute('title', 'Switch to horizontal layout');\n    });\n\n    it('should switch to horizontal layout when clicked in vertical mode', () => {\n      const customState = {\n        app: {\n          preferences: {\n            layout: {\n              responsePaneOrientation: 'vertical'\n            }\n          }\n        }\n      };\n      const { store } = renderWithProviders(<ResponseLayoutToggle />, customState);\n      const button = screen.getByTestId('response-layout-toggle-btn');\n\n      // Initial state check\n      expect(button).toHaveAttribute('title', 'Switch to horizontal layout');\n\n      fireEvent.click(button);\n\n      // Check if action was called\n      expect(mockSavePreferences).toHaveBeenCalledWith({\n        layout: {\n          responsePaneOrientation: 'horizontal'\n        }\n      });\n\n      // Manually update store to simulate state change\n      store.dispatch(mockSavePreferences({\n        layout: {\n          responsePaneOrientation: 'horizontal'\n        }\n      }));\n\n      // Check if button title was updated\n      expect(button).toHaveAttribute('title', 'Switch to vertical layout');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponsePaneActions/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  align-items: center;\n  gap: 0.5rem;\n\n  /* Default: show dropdown, hide buttons */\n  .actions-dropdown {\n    display: flex;\n  }\n\n  .actions-buttons {\n    display: none;\n  }\n\n  /* When right side is expandible, show buttons and hide dropdown */\n  .expandable & {\n    .actions-dropdown {\n      display: none;\n    }\n\n    .actions-buttons {\n      display: flex;\n      align-items: center;\n      gap: 2px;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponsePaneActions/index.js",
    "content": "import React, { forwardRef, useRef } from 'react';\nimport styled from 'styled-components';\nimport { IconDots, IconDownload, IconEraser, IconBookmark, IconCopy, IconLayoutColumns, IconLayoutRows } from '@tabler/icons';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport ResponseDownload from '../ResponseDownload';\nimport ResponseBookmark from '../ResponseBookmark';\nimport ResponseClear from '../ResponseClear';\nimport ResponseLayoutToggle, { useResponseLayoutToggle } from '../ResponseLayoutToggle';\nimport ResponseCopy from '../ResponseCopy/index';\nimport StyledWrapper from './StyledWrapper';\n\nconst StyledMenuIcon = styled.button`\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  height: 1.25rem;\n  width: 1.5rem;\n  border: 1px solid ${(props) => props.theme.workspace.border};\n  color: ${(props) => props.theme.dropdown.iconColor};\n  border-radius: 4px;\n\n  &:hover {\n    border-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.hoverBorder} !important;\n    color: ${(props) => props.theme.text};\n  }\n`;\n\nconst MenuIcon = forwardRef((props, ref) => (\n  <StyledMenuIcon\n    ref={ref}\n    title=\"More actions\"\n    {...props}\n  >\n    <IconDots size={16} strokeWidth={1.5} />\n  </StyledMenuIcon>\n));\n\nMenuIcon.displayName = 'MenuIcon';\n\nconst ResponsePaneActions = ({ item, collection, responseSize, selectedFormat, selectedTab, data, dataBuffer }) => {\n  const { orientation } = useResponseLayoutToggle();\n\n  // Refs to access child component imperative handles (click, isDisabled)\n  const bookmarkButtonRef = useRef(null);\n  const downloadButtonRef = useRef(null);\n  const clearButtonRef = useRef(null);\n  const copyButtonRef = useRef(null);\n  const layoutToggleButtonRef = useRef(null);\n\n  /**\n   * GQL response actions missing with Save response - because their is schema validation missing for saving GQL response will undo once example\n   * scehem is updated\n   */\n  const gqlMenuItems = [\n    {\n      id: 'copy-response',\n      label: 'Copy response',\n      leftSection: IconCopy,\n      get disabled() {\n        return copyButtonRef.current?.isDisabled ?? false;\n      },\n      onClick: () => copyButtonRef.current?.click()\n    },\n    {\n      id: 'download-response',\n      label: 'Download response',\n      leftSection: IconDownload,\n      get disabled() {\n        return downloadButtonRef.current?.isDisabled ?? false;\n      },\n      onClick: () => downloadButtonRef.current?.click()\n    },\n    {\n      id: 'clear-response',\n      label: 'Clear response',\n      leftSection: IconEraser,\n      get disabled() {\n        return clearButtonRef.current?.isDisabled ?? false;\n      },\n      onClick: () => clearButtonRef.current?.click()\n    },\n    {\n      id: 'change-layout',\n      label: 'Change layout',\n      leftSection: orientation === 'vertical' ? IconLayoutColumns : IconLayoutRows,\n      get disabled() {\n        return layoutToggleButtonRef.current?.isDisabled ?? false;\n      },\n      onClick: () => layoutToggleButtonRef.current?.click()\n    }\n  ];\n\n  const menuItems = [\n    {\n      id: 'copy-response',\n      label: 'Copy response',\n      leftSection: IconCopy,\n      get disabled() {\n        return copyButtonRef.current?.isDisabled ?? false;\n      },\n      onClick: () => copyButtonRef.current?.click()\n    },\n    {\n      id: 'save-response',\n      label: 'Save response',\n      leftSection: IconBookmark,\n      get disabled() {\n        return bookmarkButtonRef.current?.isDisabled ?? false;\n      },\n      onClick: () => bookmarkButtonRef.current?.click()\n    },\n    {\n      id: 'download-response',\n      label: 'Download response',\n      leftSection: IconDownload,\n      get disabled() {\n        return downloadButtonRef.current?.isDisabled ?? false;\n      },\n      onClick: () => downloadButtonRef.current?.click()\n    },\n    {\n      id: 'clear-response',\n      label: 'Clear response',\n      leftSection: IconEraser,\n      get disabled() {\n        return clearButtonRef.current?.isDisabled ?? false;\n      },\n      onClick: () => clearButtonRef.current?.click()\n    },\n    {\n      id: 'change-layout',\n      label: 'Change layout',\n      leftSection: orientation === 'vertical' ? IconLayoutColumns : IconLayoutRows,\n      get disabled() {\n        return layoutToggleButtonRef.current?.isDisabled ?? false;\n      },\n      onClick: () => layoutToggleButtonRef.current?.click()\n    }\n  ];\n\n  if (!['http-request', 'graphql-request'].includes(item.type)) {\n    return null;\n  }\n\n  return (\n    <StyledWrapper className=\"response-pane-actions-wrapper\">\n      <div className=\"actions-dropdown\">\n        <MenuDropdown\n          items={item.type !== 'graphql-request' ? menuItems : gqlMenuItems}\n          placement=\"bottom-end\"\n          data-testid=\"response-actions-menu\"\n        >\n          <MenuIcon />\n        </MenuDropdown>\n      </div>\n      <div className=\"actions-buttons flex items-center gap-[2px]\">\n        <ResponseCopy\n          ref={copyButtonRef}\n          item={item}\n          selectedFormat={selectedFormat}\n          selectedTab={selectedTab}\n          data={data}\n          dataBuffer={dataBuffer}\n        />\n        {item.type !== 'graphql-request' && <ResponseBookmark ref={bookmarkButtonRef} item={item} collection={collection} responseSize={responseSize} />}\n        <ResponseDownload ref={downloadButtonRef} item={item} />\n        <ResponseClear ref={clearButtonRef} item={item} collection={collection} />\n        <ResponseLayoutToggle ref={layoutToggleButtonRef} />\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default ResponsePaneActions;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponseSize/ResponseSize.spec.js",
    "content": "import '@testing-library/jest-dom';\nimport React from 'react';\nimport { render, screen } from '@testing-library/react';\nimport { ThemeProvider } from 'styled-components';\nimport ResponseSize from './index';\n\n// Create minimal theme with only the properties needed for the component\nconst theme = {\n  requestTabPanel: {\n    responseStatus: '#666'\n  },\n  font: {\n    size: {\n      sm: '0.75rem'\n    }\n  }\n};\n\n// Wrap component with theme provider for styled-components\nconst renderWithTheme = (component) => {\n  return render(\n    <ThemeProvider theme={theme}>\n      {component}\n    </ThemeProvider>\n  );\n};\n\ndescribe('ResponseSize', () => {\n  describe('Invalid or excluded size values', () => {\n    it('should not render when size is undefined', () => {\n      const { container } = renderWithTheme(<ResponseSize size={undefined} />);\n      expect(container).toBeEmptyDOMElement();\n    });\n\n    it('should not render when size is null', () => {\n      const { container } = renderWithTheme(<ResponseSize size={null} />);\n      expect(container).toBeEmptyDOMElement();\n    });\n\n    it('should not render when size is NaN', () => {\n      const { container } = renderWithTheme(<ResponseSize size={NaN} />);\n      expect(container).toBeEmptyDOMElement();\n    });\n\n    it('should not render when size is Infinity', () => {\n      const { container } = renderWithTheme(<ResponseSize size={Infinity} />);\n      expect(container).toBeEmptyDOMElement();\n    });\n\n    it('should not render when size is -Infinity', () => {\n      const { container } = renderWithTheme(<ResponseSize size={-Infinity} />);\n      expect(container).toBeEmptyDOMElement();\n    });\n\n    it('should not render when size is a string', () => {\n      const { container } = renderWithTheme(<ResponseSize size=\"1024\" />);\n      expect(container).toBeEmptyDOMElement();\n    });\n\n    it('should not render when size is an object', () => {\n      const { container } = renderWithTheme(<ResponseSize size={{ value: 1024 }} />);\n      expect(container).toBeEmptyDOMElement();\n    });\n  });\n\n  describe('Valid size values', () => {\n    it('should handle zero bytes', () => {\n      renderWithTheme(<ResponseSize size={0} />);\n      const element = screen.getByText(/0B/);\n      expect(element).toBeInTheDocument();\n      expect(element.textContent).toMatch(/^0B$/);\n      expect(element).toHaveAttribute('title', '0B');\n    });\n\n    it('should render bytes when size is less than 1024', () => {\n      renderWithTheme(<ResponseSize size={500} />);\n      const element = screen.getByText(/500B/);\n      expect(element).toBeInTheDocument();\n      expect(element.textContent).toMatch(/^500B$/);\n      expect(element).toHaveAttribute('title', '500B');\n    });\n\n    it('should handle exactly 1024 bytes as size', () => {\n      const size = 1024;\n      renderWithTheme(<ResponseSize size={size} />);\n      const element = screen.getByText(/1024B/);\n      expect(element).toBeInTheDocument();\n      expect(element.textContent).toMatch(/^1024B$/);\n      expect(element).toHaveAttribute('title', `${size.toLocaleString()}B`);\n    });\n\n    it('should render kilobytes when size is greater than 1024', () => {\n      const size = 1500;\n      renderWithTheme(<ResponseSize size={size} />);\n      const element = screen.getByText(/1\\.46KB/);\n      expect(element).toBeInTheDocument();\n      expect(element.textContent).toMatch(/^\\d+\\.\\d+KB$/);\n      expect(element).toHaveAttribute('title', `${size.toLocaleString()}B`);\n    });\n\n    it('should handle large size numbers', () => {\n      const size = 10240;\n      renderWithTheme(<ResponseSize size={size} />);\n      const element = screen.getByText(/10\\.0KB/);\n      expect(element).toBeInTheDocument();\n      expect(element.textContent).toMatch(/^\\d+\\.\\d+KB$/);\n      expect(element).toHaveAttribute('title', `${size.toLocaleString()}B`);\n    });\n\n    it('should handle decimal size numbers', () => {\n      const size = 1126.5;\n      renderWithTheme(<ResponseSize size={size} />);\n      const element = screen.getByText(/1\\.10KB/);\n      expect(element).toBeInTheDocument();\n      expect(element.textContent).toMatch(/^\\d+\\.\\d+KB$/);\n      expect(element).toHaveAttribute('title', `${size.toLocaleString()}B`);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponseSize/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  font-size: ${(props) => props.theme.font.size.sm};\n  font-weight: 500;\n  color: ${(props) => props.theme.requestTabPanel.responseStatus};\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponseSize/index.js",
    "content": "import React from 'react';\nimport StyledWrapper from './StyledWrapper';\n\nconst ResponseSize = ({ size }) => {\n  if (!Number.isFinite(size)) {\n    return null;\n  }\n\n  let sizeToDisplay = '';\n\n  // If size is greater than 1024 bytes, format as KB\n  if (size > 1024) {\n    let kb = Math.floor(size / 1024);\n    let decimal = Math.round(((size % 1024) / 1024).toFixed(2) * 100);\n    sizeToDisplay = kb + '.' + decimal + 'KB';\n  } else {\n    // If size is less than or equal to 1024 bytes, display as bytes (B)\n    sizeToDisplay = size + 'B';\n  }\n\n  return (\n    <StyledWrapper title={(size?.toLocaleString() || '0') + 'B'} className=\"ml-2\">\n      {sizeToDisplay}\n    </StyledWrapper>\n  );\n};\nexport default ResponseSize;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponseStopWatch/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  font-size: ${(props) => props.theme.font.size.sm};\n  font-weight: 500;\n  color: ${(props) => props.theme.requestTabPanel.responseStatus};\n  text-align: center;\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponseStopWatch/index.js",
    "content": "import React, { useState, useEffect } from 'react';\nimport StyledWrapper from './StyledWrapper';\n\nconst ResponseStopWatch = ({ startMillis }) => {\n  const [milliseconds, setMilliseconds] = useState(startMillis);\n\n  const tickInterval = 100;\n  const tick = () => {\n    setMilliseconds((_milliseconds) => _milliseconds + tickInterval);\n  };\n\n  useEffect(() => {\n    let timerID = setInterval(() => {\n      tick();\n    }, tickInterval);\n    return () => {\n      clearInterval(timerID);\n    };\n  }, []);\n\n  let seconds = milliseconds / 1000;\n  let secondsFormatted = `${seconds.toFixed(1)}s`;\n  let width = secondsFormatted.length * 0.4; // Calculate width manually to stop parent layout from \"flickering\" by changing width too fast\n  return <StyledWrapper className=\"ml-2\" style={{ width: `${width}rem` }}>{secondsFormatted}</StyledWrapper>;\n};\n\nexport default React.memo(ResponseStopWatch);\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponseTime/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  font-size: ${(props) => props.theme.font.size.sm};\n  font-weight: 500;\n  color: ${(props) => props.theme.requestTabPanel.responseStatus};\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ResponseTime/index.js",
    "content": "import React from 'react';\nimport StyledWrapper from './StyledWrapper';\nimport isNumber from 'lodash/isNumber';\n\nconst ResponseTime = ({ duration }) => {\n  let durationToDisplay = '';\n  if (duration > 1000) {\n    // duration greater than a second\n    let seconds = Math.floor(duration / 1000);\n    let decimal = ((duration % 1000) / 1000) * 100;\n    durationToDisplay = seconds + '.' + decimal.toFixed(0) + 's';\n  } else {\n    durationToDisplay = duration + 'ms';\n  }\n\n  if (!isNumber(duration)) {\n    return null;\n  }\n\n  return <StyledWrapper className=\"ml-2\">{durationToDisplay}</StyledWrapper>;\n};\nexport default ResponseTime;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/RunnerTimeline/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .timeline-event {\n    padding: 8px 0 0 0;\n    cursor: pointer;\n  }\n\n  .timeline-event-content {\n    border-radius: 4px;\n    padding: 12px;\n    margin-top: 0.5rem;\n  }\n\n  .timeline-event-header {\n    color: ${(props) => props.theme.text};\n  }\n\n  .method-label {\n    font-weight: 500;\n  }\n\n  .status-code {\n    font-weight: 500;\n  }\n\n  .url-text {\n    color: ${(props) => props.theme.colors.text.muted};\n    font-size: ${(props) => props.theme.font.size.base};\n    margin-top: 0.25rem;\n  }\n\n  .timestamp {\n    color: ${(props) => props.theme.colors.text.muted};\n    font-size: ${(props) => props.theme.font.size.base};\n  }\n\n  .meta-info {\n    color: ${(props) => props.theme.colors.text.muted};\n    font-size: ${(props) => props.theme.font.size.base};\n  }\n\n  .oauth-section {\n    .oauth-header {\n      display: flex;\n      align-items: center;\n      color: ${(props) => props.theme.text};\n      font-weight: 500;\n\n      span {\n        margin-left: 0.5rem;\n      }\n    }\n  }\n\n  .tabs-switcher {\n    border-bottom: 1px solid ${(props) => props.theme.border.border1};\n    margin-bottom: 16px;\n    \n    button {\n      position: relative;\n      padding: 8px 16px;\n      color: ${(props) => props.theme.colors.text.muted};\n\n      &.active {\n        color: ${(props) => props.theme.tabs.active.color};\n        &:after {\n          content: '';\n          position: absolute;\n          bottom: -1px;\n          left: 0;\n          right: 0;\n          height: 2px;\n          background: ${(props) => props.theme.tabs.active.border};\n        }\n      }\n    }\n  }\n\n  .network-logs {\n    background: ${(props) => props.theme.codemirror.bg};\n    color: ${(props) => props.theme.text};\n    border-radius: 4px;\n  }\n\n  .oauth-request-item-content {\n    border-radius: 4px;\n    margin-top: 0.5rem;\n  }\n\n  .collapsible-section {\n    margin-bottom: 12px;\n\n    .section-header {\n      cursor: pointer;\n      &:hover {\n        opacity: 0.8;\n      }\n    }\n  }\n\n  .line {\n    white-space: pre-line;\n    word-wrap: break-word;\n    word-break: break-all;\n    font-family: ${(props) => props.theme.font || 'Inter, sans-serif'} !important;\n\n    .arrow {\n      opacity: 0.5;\n    }\n\n    &.request {\n      color: ${(props) => props.theme.colors.text.green};\n    }\n\n    &.response {\n      color: ${(props) => props.theme.colors.text.purple};\n    }\n  }\n    \n  .request-label {\n    font-size: ${(props) => props.theme.font.size.base};\n    padding: 2px 6px;\n    border-radius: 3px;\n    margin-left: 8px;\n    background: ${(props) => props.theme.requestTabs.bg};\n  }\n\n  table {\n    width: 100%;\n    border-collapse: collapse;\n    font-weight: 500;\n    table-layout: fixed;\n\n    thead,\n    td {\n      border: 1px solid ${(props) => props.theme.table.border};\n    }\n\n    thead {\n      color: ${(props) => props.theme.table.thead.color};\n      font-size: ${(props) => props.theme.font.size.base};\n      user-select: none;\n    }\n    td {\n      padding: 6px 10px;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/RunnerTimeline/index.js",
    "content": "import React, { useMemo } from 'react';\nimport forOwn from 'lodash/forOwn';\nimport StyledWrapper from './StyledWrapper';\nimport TimelineItem from '../Timeline/TimelineItem';\n\nconst RunnerTimeline = ({ request = {}, response = {}, item, collection }) => {\n  const requestHeaders = [];\n\n  forOwn(request.headers, (value, key) => {\n    requestHeaders.push({\n      name: key,\n      value\n    });\n  });\n\n  const oauth2Events = useMemo(\n    () =>\n      collection?.timeline?.filter(\n        (event) => event.type === 'oauth2' && event.itemUid === item.uid\n      ) || [],\n    [collection?.timeline, item.uid]\n  );\n\n  return (\n    <StyledWrapper className=\"pb-4 w-full\">\n      {/* Show the main request/response timeline item */}\n      <TimelineItem\n        request={request}\n        response={response}\n        item={item}\n        collection={collection}\n        hideTimestamp={true}\n      />\n\n      {oauth2Events.map((event, index) => {\n        const { data, timestamp } = event;\n        const { debugInfo } = data;\n        return (\n          <div key={`oauth2-${index}`} className=\"timeline-event mt-4\">\n            <div className=\"timeline-event-header cursor-pointer flex items-center\">\n              <div className=\"flex items-center\">\n                <span className=\"font-bold\">OAuth2.0 Calls</span>\n              </div>\n            </div>\n            <div className=\"mt-2\">\n              {debugInfo && debugInfo.length > 0 ? (\n                debugInfo.map((data, idx) => (\n                  <div key={idx} className=\"ml-4\">\n                    <TimelineItem\n                      timestamp={timestamp}\n                      request={data?.request}\n                      response={data?.response}\n                      item={item}\n                      collection={collection}\n                      isOauth2={true}\n                    />\n                  </div>\n                ))\n              ) : (\n                <div>No debug information available.</div>\n              )}\n            </div>\n          </div>\n        );\n      })}\n    </StyledWrapper>\n  );\n};\n\nexport default RunnerTimeline;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ScriptError/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  max-height: 200px;\n  min-height: 70px;\n  overflow-y: auto;\n  background-color: ${(props) => props.theme.background.base};\n  border: solid 1px ${(props) => props.theme.border.border2};\n  border-left: 4px solid ${(props) => props.theme.colors.text.danger};\n  border-radius: ${(props) => props.theme.border.radius.base};\n  \n  .close-button {\n    opacity: 0.7;\n    transition: opacity 0.2s;\n    \n    &:hover {\n      opacity: 1;\n    }\n    \n    svg {\n      color: ${(props) => props.theme.text};\n    }\n  }\n  \n  .error-title {\n    font-weight: 500;\n    margin-bottom: 0.375rem;\n    color: ${(props) => props.theme.colors.text.danger};\n  }\n  \n  .error-message {\n    font-family: monospace;\n    font-size: ${(props) => props.theme.font.size.xs};\n    line-height: 1.25rem;\n    white-space: pre-wrap;\n    word-break: break-all;\n    color: ${(props) => props.theme.text};\n  }\n\n  .separator {\n    border-top: 1px solid ${(props) => props.theme.border.border1};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ScriptError/index.js",
    "content": "import React from 'react';\nimport ErrorBanner from 'ui/ErrorBanner';\n\nconst ScriptError = ({ item, onClose }) => {\n  const preRequestError = item?.preRequestScriptErrorMessage;\n  const postResponseError = item?.postResponseScriptErrorMessage;\n  const testScriptError = item?.testScriptErrorMessage;\n\n  if (!preRequestError && !postResponseError && !testScriptError) return null;\n\n  const errors = [];\n\n  if (preRequestError) {\n    errors.push({\n      title: 'Pre-Request Script Error',\n      message: preRequestError\n    });\n  }\n\n  if (postResponseError) {\n    errors.push({\n      title: 'Post-Response Script Error',\n      message: postResponseError\n    });\n  }\n\n  if (testScriptError) {\n    errors.push({\n      title: 'Test Script Error',\n      message: testScriptError\n    });\n  }\n\n  return <ErrorBanner errors={errors} onClose={onClose} className=\"mt-4 mb-2\" />;\n};\n\nexport default ScriptError;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/ScriptErrorIcon/index.js",
    "content": "import React from 'react';\nimport { IconAlertCircle } from '@tabler/icons';\nimport ToolHint from 'components/ToolHint';\n\nconst ScriptErrorIcon = ({ itemUid, onClick }) => {\n  const toolhintId = `script-error-icon-${itemUid}`;\n\n  return (\n    <>\n      <div\n        id={toolhintId}\n        className=\"cursor-pointer ml-2\"\n        onClick={onClick}\n      >\n        <div className=\"flex items-center text-red-400\">\n          <IconAlertCircle size={16} strokeWidth={1.5} className=\"stroke-current\" />\n        </div>\n      </div>\n      <ToolHint\n        toolhintId={toolhintId}\n        text=\"Script execution error occurred\"\n        place=\"bottom\"\n      />\n    </>\n  );\n};\n\nexport default ScriptErrorIcon;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/SkippedRequest/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  padding-top: 20%;\n  width: 100%;\n  .send-icon {\n    color: ${(props) => props.theme.background.surface2};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/SkippedRequest/index.js",
    "content": "import React from 'react';\nimport { IconCircleOff } from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\n\nconst SkippedRequest = () => {\n  return (\n    <StyledWrapper>\n      <div className=\"send-icon flex justify-center\" style={{ fontSize: 200 }}>\n        <IconCircleOff size={150} strokeWidth={1} />\n      </div>\n      <div className=\"flex mt-4 justify-center\" style={{ fontSize: 25 }}>\n        Request skipped\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default SkippedRequest;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/StatusCode/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  font-size: ${(props) => props.theme.font.size.sm};\n  font-weight: 600;\n  white-space: nowrap;\n\n  &.text-ok {\n    color: ${(props) => props.theme.requestTabPanel.responseOk};\n  }\n\n  &.text-error {\n    color: ${(props) => props.theme.requestTabPanel.responseError};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/StatusCode/get-status-code-phrase.js",
    "content": "// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status\nconst statusCodePhraseMap = {\n  100: 'Continue',\n  101: 'Switching Protocols',\n  102: 'Processing',\n  103: 'Early Hints',\n  200: 'OK',\n  201: 'Created',\n  202: 'Accepted',\n  203: 'Non-Authoritative Information',\n  204: 'No Content',\n  205: 'Reset Content',\n  206: 'Partial Content',\n  207: 'Multi-Status',\n  208: 'Already Reported',\n  226: 'IM Used',\n  300: 'Multiple Choice',\n  301: 'Moved Permanently',\n  302: 'Found',\n  303: 'See Other',\n  304: 'Not Modified',\n  305: 'Use Proxy',\n  306: 'unused',\n  307: 'Temporary Redirect',\n  308: 'Permanent Redirect',\n  400: 'Bad Request',\n  401: 'Unauthorized',\n  402: 'Payment Required',\n  403: 'Forbidden',\n  404: 'Not Found',\n  405: 'Method Not Allowed',\n  406: 'Not Acceptable',\n  407: 'Proxy Authentication Required',\n  408: 'Request Timeout',\n  409: 'Conflict',\n  410: 'Gone',\n  411: 'Length Required',\n  412: 'Precondition Failed',\n  413: 'Payload Too Large',\n  414: 'URI Too Long',\n  415: 'Unsupported Media Type',\n  416: 'Range Not Satisfiable',\n  417: 'Expectation Failed',\n  418: 'I\\'m a teapot',\n  421: 'Misdirected Request',\n  422: 'Unprocessable Entity',\n  423: 'Locked',\n  424: 'Failed Dependency',\n  425: 'Too Early',\n  426: 'Upgrade Required',\n  428: 'Precondition Required',\n  429: 'Too Many Requests',\n  431: 'Request Header Fields Too Large',\n  451: 'Unavailable For Legal Reasons',\n  500: 'Internal Server Error',\n  501: 'Not Implemented',\n  502: 'Bad Gateway',\n  503: 'Service Unavailable',\n  504: 'Gateway Timeout',\n  505: 'HTTP Version Not Supported',\n  506: 'Variant Also Negotiates',\n  507: 'Insufficient Storage',\n  508: 'Loop Detected',\n  510: 'Not Extended',\n  511: 'Network Authentication Required'\n};\n\nexport default statusCodePhraseMap;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/StatusCode/index.js",
    "content": "import React from 'react';\nimport classnames from 'classnames';\nimport statusCodePhraseMap from './get-status-code-phrase';\nimport StyledWrapper from './StyledWrapper';\n\n// Todo: text-error class is not getting pulled in for 500 errors\nconst StatusCode = ({ status, statusText, isStreaming }) => {\n  const getTabClassname = (status) => {\n    return classnames({\n      'text-ok': status >= 100 && status < 200,\n      'text-ok': status >= 200 && status < 300,\n      'text-error': status >= 300 && status < 400,\n      'text-error': status >= 400 && status < 500,\n      'text-error': status >= 500 && status < 600\n    });\n  };\n\n  return (\n    <StyledWrapper className={`response-status-code ${getTabClassname(status)}`} data-testid=\"response-status-code\">\n      {status} {statusText || statusCodePhraseMap[status]} {isStreaming ? ' - STREAMING' : null}\n    </StyledWrapper>\n  );\n};\nexport default StatusCode;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  overflow: hidden;\n  min-width: 0;\n\n  > div:first-child {\n    overflow: hidden;\n    min-width: 0;\n  }\n\n  div.tabs {\n    overflow: hidden;\n    min-width: 0;\n    max-width: 100%;\n\n    > div:first-child {\n      overflow: hidden;\n      min-width: 0;\n      max-width: 100%;\n    }\n\n    div.tab {\n      padding: 6px 0px;\n      border: none;\n      border-bottom: solid 2px transparent;\n      margin-right: ${(props) => props.theme.tabs.marginRight};\n      color: ${(props) => props.theme.colors.text.subtext0};\n      cursor: pointer;\n      flex-shrink: 1;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      white-space: nowrap;\n\n      &:focus,\n      &:active,\n      &:focus-within,\n      &:focus-visible,\n      &:target {\n        outline: none !important;\n        box-shadow: none !important;\n      }\n\n      &.active {\n        font-weight: ${(props) => props.theme.tabs.active.fontWeight} !important;\n        color: ${(props) => props.theme.tabs.active.color} !important;\n        border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;\n      }\n    }\n  }\n\n  .right-side-container {\n    min-width: 0;\n    flex-shrink: 1;\n    flex-grow: 1;\n  }\n\n  .response-pane-status {\n    min-width: 0;\n    flex-shrink: 1;\n    flex-grow: 0;\n  }\n\n  .response-pane-actions {\n    min-width: 0;\n    flex-shrink: 1;\n    flex-grow: 0;\n  }\n\n  .some-tests-failed {\n    color: ${(props) => props.theme.colors.text.danger} !important;\n  }\n\n  .all-tests-passed {\n    color: ${(props) => props.theme.colors.text.green} !important;\n  }\n\n  .result-view-tabs {\n    display: flex;\n    align-items: center;\n    gap: 2px;\n    padding: 3px;\n    border-radius: 8px;\n\n    .button-dropdown-button {\n      border: 1px solid transparent !important;\n      background-color: transparent;\n      border-radius: 5px;\n      font-size: ${(props) => props.theme.font.size.sm};\n\n      &:hover {\n        border-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.border} !important;\n      }\n    }\n\n    .tab-active .button-dropdown-button {\n      border-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.border} !important;\n\n      &:hover {\n        border-color: ${(props) => props.theme.app.collection.toolbar.environmentSelector.hoverBorder} !important;\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/TestResults/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  color: ${(props) => props.theme.text};\n\n  .test-summary {\n    transition: background-color 0.2s;\n    color: ${(props) => props.theme.text};\n\n    &:hover {\n      background-color: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n    }\n  }\n\n  .test-success {\n    color: ${(props) => props.theme.colors.text.green};\n  }\n\n  .test-failure {\n    color: ${(props) => props.theme.colors.text.danger};\n  }\n\n  .test-success-count {\n    color: ${(props) => props.theme.colors.text.green};\n  }\n\n  .test-failure-count {\n    color: ${(props) => props.theme.colors.text.danger};\n  }\n\n  .error-message {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .test-results-list {\n    transition: all 0.3s ease;\n  }\n\n  .dropdown-icon {\n    color: ${(props) => props.theme.sidebar.dropdownIcon.color};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/TestResults/index.js",
    "content": "import React, { useState, useEffect } from 'react';\nimport StyledWrapper from './StyledWrapper';\nimport {\n  IconChevronDown,\n  IconChevronRight,\n  IconCircleCheck,\n  IconCircleX\n} from '@tabler/icons';\n\nconst ResultIcon = ({ status }) => (\n  <span className={`inline-flex items-center ${status === 'pass' ? 'test-success' : 'test-failure'}`}>\n    {status === 'pass' ? (\n      <IconCircleCheck size={14} className=\"mr-1\" aria-label=\"Test passed\" />\n    ) : (\n      <IconCircleX size={14} className=\"mr-1\" aria-label=\"Test failed\" />\n    )}\n  </span>\n);\n\nconst ErrorMessage = ({ error }) => error && (\n  <>\n    <br />\n    <span className=\"error-message pl-8\" role=\"alert\">\n      {error}\n    </span>\n  </>\n);\n\nconst ResultItem = ({ result, type }) => (\n  <div className=\"test-result-item\">\n    <ResultIcon status={result.status} />\n    <span className={result.status === 'pass' ? 'test-success' : 'test-failure'}>\n      {type === 'assertion'\n        ? `${result.lhsExpr}: ${result.rhsExpr}`\n        : result.description}\n    </span>\n    <ErrorMessage error={result.error} />\n  </div>\n);\n\nconst TestSection = ({\n  title,\n  results,\n  isExpanded,\n  onToggle,\n  type = 'test'\n}) => {\n  const passedResults = results.filter((result) => result.status === 'pass');\n  const failedResults = results.filter((result) => result.status === 'fail');\n\n  if (results.length === 0) return null;\n\n  return (\n    <div className=\"mb-4\">\n      <div\n        className=\"font-medium test-summary flex items-center cursor-pointer hover:bg-opacity-10 hover:bg-gray-500 rounded py-2\"\n        onClick={onToggle}\n      >\n        <span className=\"dropdown-icon mr-2 flex items-center\">\n          {isExpanded\n            ? <IconChevronDown size={18} stroke={1.5} />\n            : <IconChevronRight size={18} stroke={1.5} />}\n        </span>\n        <span className=\"flex-grow\">\n          {title} ({results.length}), Passed: {passedResults.length}, Failed: {failedResults.length}\n        </span>\n      </div>\n      {isExpanded && (\n        <ul className=\"ml-5\">\n          {results.map((result) => (\n            <li key={result.uid} className=\"py-1\">\n              <ResultItem result={result} type={type} />\n            </li>\n          ))}\n        </ul>\n      )}\n    </div>\n  );\n};\n\nconst TestResults = ({ results, assertionResults, preRequestTestResults, postResponseTestResults }) => {\n  results = results || [];\n  assertionResults = assertionResults || [];\n  preRequestTestResults = preRequestTestResults || [];\n  postResponseTestResults = postResponseTestResults || [];\n\n  const [expandedSections, setExpandedSections] = useState({\n    preRequest: true,\n    tests: true,\n    postResponse: true,\n    assertions: true\n  });\n\n  useEffect(() => {\n    setExpandedSections({\n      preRequest: preRequestTestResults.length > 0,\n      tests: results.length > 0,\n      postResponse: postResponseTestResults.length > 0,\n      assertions: assertionResults.length > 0\n    });\n  }, [results.length, assertionResults.length, preRequestTestResults.length, postResponseTestResults.length]);\n\n  const toggleSection = (section) => {\n    setExpandedSections({\n      ...expandedSections,\n      [section]: !expandedSections[section]\n    });\n  };\n\n  if (!results.length && !assertionResults.length && !preRequestTestResults.length && !postResponseTestResults.length) {\n    return <div>No tests found</div>;\n  }\n\n  return (\n    <StyledWrapper className=\"flex flex-col\">\n      <TestSection\n        title=\"Pre-Request Tests\"\n        results={preRequestTestResults}\n        isExpanded={expandedSections.preRequest}\n        onToggle={() => toggleSection('preRequest')}\n        type=\"test\"\n      />\n\n      <TestSection\n        title=\"Post-Response Tests\"\n        results={postResponseTestResults}\n        isExpanded={expandedSections.postResponse}\n        onToggle={() => toggleSection('postResponse')}\n        type=\"test\"\n      />\n\n      <TestSection\n        title=\"Tests\"\n        results={results}\n        isExpanded={expandedSections.tests}\n        onToggle={() => toggleSection('tests')}\n        type=\"test\"\n      />\n\n      <TestSection\n        title=\"Assertions\"\n        results={assertionResults}\n        isExpanded={expandedSections.assertions}\n        onToggle={() => toggleSection('assertions')}\n        type=\"assertion\"\n      />\n    </StyledWrapper>\n  );\n};\n\nexport default TestResults;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/TestResultsLabel/index.js",
    "content": "import React from 'react';\nimport { IconCircleCheck, IconCircleX } from '@tabler/icons';\n\nconst TestResultsLabel = ({ results, assertionResults, preRequestTestResults, postResponseTestResults }) => {\n  results = results || [];\n  assertionResults = assertionResults || [];\n  preRequestTestResults = preRequestTestResults || [];\n  postResponseTestResults = postResponseTestResults || [];\n\n  if (!results.length && !assertionResults.length && !preRequestTestResults.length && !postResponseTestResults.length) {\n    return 'Tests';\n  }\n\n  const numberOfTests = results.length;\n  const numberOfFailedTests = results.filter((result) => result.status === 'fail').length;\n\n  const numberOfAssertions = assertionResults.length;\n  const numberOfFailedAssertions = assertionResults.filter((result) => result.status === 'fail').length;\n\n  const numberOfPreRequestTests = preRequestTestResults.length;\n  const numberOfFailedPreRequestTests = preRequestTestResults.filter((result) => result.status === 'fail').length;\n\n  const numberOfPostResponseTests = postResponseTestResults.length;\n  const numberOfFailedPostResponseTests = postResponseTestResults.filter((result) => result.status === 'fail').length;\n\n  const totalNumberOfTests = numberOfTests + numberOfAssertions + numberOfPreRequestTests + numberOfPostResponseTests;\n  const totalNumberOfFailedTests = numberOfFailedTests + numberOfFailedAssertions + numberOfFailedPreRequestTests + numberOfFailedPostResponseTests;\n\n  return (\n    <div className=\"flex items-center\">\n      <div>Tests</div>\n      {totalNumberOfFailedTests ? (\n        <sup className=\"sups some-tests-failed ml-1 font-medium\">{totalNumberOfFailedTests}</sup>\n      ) : (\n        <sup className=\"sups all-tests-passed ml-1 font-medium\">{totalNumberOfTests}</sup>\n      )}\n    </div>\n  );\n};\n\nexport default TestResultsLabel;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/Timeline/GrpcTimelineItem/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst StyledWrapper = styled.div`\n  font-size: ${(props) => props.theme.font.size.base};\n\n  /* Event type styles */\n  &.event-metadata {\n    border-left: 4px solid ${(props) => rgba(props.theme.request.methods.post, 0.2)};\n  }\n\n  &.event-response {\n    border-left: 4px solid ${(props) => rgba(props.theme.request.methods.get, 0.2)};\n  }\n\n  &.event-request,\n  &.event-message {\n    border-left: 4px solid ${(props) => rgba(props.theme.request.methods.put, 0.2)};\n  }\n\n  &.event-status {\n    border-left: 4px solid ${(props) => rgba(props.theme.colors.text.purple, 0.2)};\n  }\n\n  &.event-error {\n    border-left: 4px solid ${(props) => rgba(props.theme.colors.text.danger, 0.2)};\n  }\n\n  &.event-end {\n    border-left: 4px solid ${(props) => rgba(props.theme.colors.text.muted, 0.2)};\n  }\n\n  &.event-cancel {\n    border-left: 4px solid ${(props) => rgba(props.theme.colors.text.warning, 0.2)};\n  }\n\n  /* Event type icon colors */\n  .icon-metadata {\n    color: ${(props) => props.theme.request.methods.post};\n  }\n\n  .icon-response {\n    color: ${(props) => props.theme.request.methods.get};\n  }\n\n  .icon-request,\n  .icon-message {\n    color: ${(props) => props.theme.request.methods.put};\n  }\n\n  .icon-status {\n    color: ${(props) => props.theme.colors.text.purple};\n  }\n\n  .icon-error {\n    color: ${(props) => props.theme.colors.text.danger};\n  }\n\n  .icon-end {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .icon-cancel {\n    color: ${(props) => props.theme.colors.text.warning};\n  }\n\n  /* Event Header */\n  .event-header {\n    display: flex;\n    align-items: center;\n    gap: 0.375rem;\n    cursor: pointer;\n\n    .event-icon-container {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      width: 1.25rem;\n      flex-shrink: 0;\n    }\n\n    span:nth-of-type(1) {\n      font-weight: 500;\n    }\n\n    pre {\n      font-family: var(--font-family-mono);\n      font-size: ${(props) => props.theme.font.size.xs};\n      margin: 0;\n    }\n\n    .event-timestamp {\n      opacity: 0.7;\n    }\n  }\n\n  /* Common content container styles */\n  .content-request,\n  .content-message,\n  .content-metadata,\n  .content-response,\n  .content-status,\n  .content-error,\n  .content-end,\n  .content-cancel {\n    margin-top: 0.375rem;\n    padding: 0.375rem;\n    border-radius: ${(props) => props.theme.border.radius.base};\n    display: flex;\n    flex-direction: column;\n    gap: 0.5rem;\n  }\n\n  /* Request/Message content */\n  .content-request,\n  .content-message {\n    background-color: ${(props) => rgba(props.theme.request.methods.put, 0.1)};\n  }\n\n  .content-request-label,\n  .content-message-label {\n    color: ${(props) => props.theme.request.methods.put};\n    font-weight: 500;\n    font-size: ${(props) => props.theme.font.size.sm};\n  }\n\n  /* Metadata content */\n  .content-metadata {\n    background-color: ${(props) => rgba(props.theme.request.methods.post, 0.1)};\n  }\n\n  .content-metadata-label {\n    color: ${(props) => props.theme.request.methods.post};\n    font-weight: 500;\n    font-size: ${(props) => props.theme.font.size.sm};\n  }\n\n  /* Response content */\n  .content-response {\n    background-color: ${(props) => rgba(props.theme.request.methods.get, 0.1)};\n  }\n\n  .content-response-label {\n    color: ${(props) => props.theme.request.methods.get};\n    font-weight: 500;\n    font-size: ${(props) => props.theme.font.size.sm};\n  }\n\n  /* Status content */\n  .content-status {\n    background-color: ${(props) => rgba(props.theme.colors.text.purple, 0.1)};\n  }\n\n  .content-status-label {\n    color: ${(props) => props.theme.colors.text.purple};\n    font-weight: 500;\n    font-size: ${(props) => props.theme.font.size.sm};\n  }\n\n  /* Error content */\n  .content-error {\n    background-color: ${(props) => rgba(props.theme.colors.text.danger, 0.1)};\n  }\n\n  .content-error-label {\n    color: ${(props) => props.theme.colors.text.danger};\n    font-weight: 500;\n    font-size: ${(props) => props.theme.font.size.sm};\n  }\n\n  /* End content */\n  .content-end {\n    background-color: ${(props) => rgba(props.theme.colors.text.muted, 0.1)};\n    font-weight: 500;\n    font-size: ${(props) => props.theme.font.size.sm};\n  }\n\n  /* Cancel content */\n  .content-cancel {\n    background-color: ${(props) => rgba(props.theme.colors.text.warning, 0.1)};\n  }\n\n  .content-cancel-label {\n    color: ${(props) => props.theme.colors.text.warning};\n    font-weight: 500;\n    font-size: ${(props) => props.theme.font.size.sm};\n  }\n\n  /* Common content styles */\n  .content-box {\n    background-color: ${(props) => props.theme.bg};\n    border-radius: ${(props) => props.theme.border.radius.base};\n    padding: 0.375rem;\n    margin: 0;\n\n    &,\n    pre {\n      font-family: var(--font-family-mono);\n    }\n\n    pre {\n      margin: 0;\n    }\n  }\n\n  .empty-text {\n    color: ${(props) => props.theme.colors.text.muted};\n    font-style: italic;\n    font-size: ${(props) => props.theme.font.size.xs};\n  }\n\n  /* Method type badge */\n  .method-type-badge {\n    background-color: ${(props) => rgba(props.theme.request.methods.put, 0.15)};\n    color: ${(props) => props.theme.request.methods.put};\n    border-radius: ${(props) => props.theme.border.radius.base};\n    font-size: ${(props) => props.theme.font.size.xs};\n    font-weight: 500;\n  }\n\n  /* Timestamp and URL */\n  .timestamp-text {\n    color: ${(props) => props.theme.colors.text.muted};\n    font-size: ${(props) => props.theme.font.size.xs};\n  }\n\n  .url-text {\n    color: ${(props) => props.theme.colors.text.muted};\n    font-size: ${(props) => props.theme.font.size.xs};\n    margin-left: 1.5rem;\n    margin-top: 0.25rem;\n  }\n\n  .contents {\n    display: contents;\n    font-size: ${(props) => props.theme.font.size.xs};\n    \n    div:first-child {\n      font-weight: 500;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/Timeline/GrpcTimelineItem/index.js",
    "content": "import { useState } from 'react';\nimport { RelativeTime } from '../TimelineItem/Common/Time/index';\nimport Status from '../TimelineItem/Common/Status/index';\nimport {\n  IconChevronDown,\n  IconChevronRight,\n  IconServer,\n  IconDatabase,\n  IconAlertCircle,\n  IconCircleCheck,\n  IconCircleX,\n  IconX,\n  IconSend\n} from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\n\n// Event type display names\nconst EventTypeNames = {\n  metadata: 'Metadata',\n  response: 'Response Message',\n  request: 'Request',\n  message: 'Message',\n  status: 'Status',\n  error: 'Error',\n  end: 'Stream Ended',\n  cancel: 'Cancelled'\n};\n\nconst GrpcTimelineItem = ({ timestamp, request, response, eventType, eventData, item }) => {\n  const [isCollapsed, setIsCollapsed] = useState(true);\n  const toggleCollapse = () => setIsCollapsed((prev) => !prev);\n\n  // Use requestSent if available, otherwise fall back to request\n  const effectiveRequest = item.requestSent || request || item.request || {};\n\n  // Extract relevant data from request and response\n  const { method, url = '' } = effectiveRequest;\n  const { statusCode, statusText, duration } = response || {};\n\n  // Get event-specific icon and class names\n  const getEventIcon = () => {\n    const iconClass = `icon-${eventType}`;\n    switch (eventType) {\n      case 'metadata':\n        return <IconServer size={16} strokeWidth={1.5} className={iconClass} />;\n      case 'response':\n        return <IconSend style={{ transform: 'rotate(225deg)' }} size={16} strokeWidth={1.5} className={iconClass} />;\n      case 'request':\n        return <IconSend style={{ transform: 'rotate(45deg)' }} size={16} strokeWidth={1.5} className={iconClass} />;\n      case 'message':\n        return <IconSend style={{ transform: 'rotate(45deg)' }} size={16} strokeWidth={1.5} className={iconClass} />;\n      case 'status':\n        return <IconCircleCheck size={16} strokeWidth={1.5} className={iconClass} />;\n      case 'error':\n        return <IconAlertCircle size={16} strokeWidth={1.5} className={iconClass} />;\n      case 'end':\n        return <IconX size={16} strokeWidth={1.5} className={iconClass} />;\n      case 'cancel':\n        return <IconCircleX size={16} strokeWidth={1.5} className={iconClass} />;\n      default:\n        return <IconDatabase size={16} strokeWidth={1.5} />;\n    }\n  };\n\n  const eventIcon = getEventIcon();\n  const eventName = EventTypeNames[eventType] || 'Event';\n  const eventClass = `event-${eventType}`;\n\n  // Render appropriate content based on event type\n  const renderEventContent = () => {\n    const isClientStreaming = effectiveRequest.methodType === 'client-streaming' || effectiveRequest.methodType === 'bidi-streaming';\n\n    switch (eventType) {\n      case 'request':\n        return (\n          <div className=\"content-request\">\n            {effectiveRequest.headers && Object.keys(effectiveRequest.headers).length > 0 && (\n              <div>\n                <div className=\"content-request-label mb-1\">Metadata</div>\n                <div className=\"content-box grid grid-cols-2 gap-1\">\n                  {Object.entries(effectiveRequest.headers).map(([key, value], idx) => (\n                    <div key={idx} className=\"contents\">\n                      <div className=\"overflow-hidden text-ellipsis\">{key}:</div>\n                      <div className=\"overflow-hidden text-ellipsis\">{typeof value === 'string' ? value : '[Buffer Buffer]'}</div>\n                    </div>\n                  ))}\n                </div>\n              </div>\n            )}\n\n            {/* gRPC Messages section */}\n            {!isClientStreaming && effectiveRequest.body?.mode === 'grpc' && effectiveRequest.body?.grpc?.length > 0 && (\n              <div>\n                <div className=\"content-request-label mb-1\">Message</div>\n                <div className=\"space-y-1\">\n                  {effectiveRequest.body.grpc.filter((_, index) => index === 0).map((message, idx) => (\n                    <div key={idx} className=\"content-box\">\n                      <pre className=\"overflow-auto max-h-[150px]\">\n                        {typeof message.content === 'string'\n                          ? message.content\n                          : JSON.stringify(message.content, null, 2)}\n                      </pre>\n                    </div>\n                  ))}\n                </div>\n              </div>\n            )}\n          </div>\n        );\n\n      case 'message':\n        return (\n          <div className=\"content-message\">\n            <div>\n              <div className=\"content-message-label mb-1\">Message</div>\n              <pre className=\"content-box overflow-auto max-h-[200px]\">\n                {typeof eventData === 'string'\n                  ? eventData\n                  : JSON.stringify(eventData, null, 2)}\n              </pre>\n            </div>\n          </div>\n        );\n\n      case 'metadata':\n        return (\n          <div className=\"content-metadata\">\n            <div>\n              <div className=\"content-metadata-label mb-1\">Metadata Headers</div>\n              {response.metadata && response.metadata.length > 0 ? (\n                <div className=\"content-box grid grid-cols-2 gap-1\">\n                  {response.metadata.map((header, idx) => (\n                    <div key={idx} className=\"contents\">\n                      <div>{header.name}:</div>\n                      <div>{header.value}</div>\n                    </div>\n                  ))}\n                </div>\n              ) : (\n                <div className=\"empty-text\">No metadata headers</div>\n              )}\n            </div>\n          </div>\n        );\n\n      case 'response':\n        // For message responses, show the response data\n        return (\n          <div className=\"content-response\">\n            <div>\n              <div className=\"content-response-label mb-1\">\n                Response Message #{(response?.responses?.length) || 0}\n              </div>\n              {response?.responses && response.responses.length > 0 ? (\n                <pre className=\"content-box overflow-auto max-h-[200px]\">\n                  {JSON.stringify(response.responses[response.responses.length - 1], null, 2)}\n                </pre>\n              ) : (\n                <div className=\"empty-text\">Empty message</div>\n              )}\n            </div>\n          </div>\n        );\n\n      case 'status':\n        // For status events, show status and trailers\n        return (\n          <div className=\"content-status\">\n            <div className=\"flex items-center gap-2\">\n              <Status statusCode={statusCode} statusText={statusText} />\n            </div>\n\n            {response.statusDescription && (\n              <div>{response.statusDescription}</div>\n            )}\n\n            {response.trailers && response.trailers.length > 0 && (\n              <div>\n                <div className=\"content-status-label mb-1\">Trailers</div>\n                <div className=\"content-box grid grid-cols-2 gap-1\">\n                  {response.trailers.map((trailer, idx) => (\n                    <div key={idx} className=\"contents\">\n                      <div>{trailer.name}:</div>\n                      <div>{trailer.value || ''}</div>\n                    </div>\n                  ))}\n                </div>\n              </div>\n            )}\n          </div>\n        );\n\n      case 'error':\n        // For error events, show error details\n        return (\n          <div className=\"content-error\">\n            <div>\n              <div className=\"content-error-label mb-1\">Error</div>\n              <div>{response.error || 'Unknown error'}</div>\n            </div>\n\n            <div className=\"flex items-center gap-2\">\n              <Status statusCode={statusCode} statusText={statusText} />\n            </div>\n\n            {response.trailers && response.trailers.length > 0 && (\n              <div>\n                <div className=\"content-error-label mb-1\">Error Metadata</div>\n                <div className=\"content-box grid grid-cols-2 gap-1\">\n                  {response.trailers.map((trailer, idx) => (\n                    <div key={idx} className=\"contents\">\n                      <div>{trailer.name}:</div>\n                      <div>{trailer.value}</div>\n                    </div>\n                  ))}\n                </div>\n              </div>\n            )}\n          </div>\n        );\n\n      case 'end':\n        // For end events, show summary\n        return (\n          <div className=\"content-end\">\n            <div>Stream Ended</div>\n            <div>\n              Total messages: {(response?.responses?.length) || 0}\n            </div>\n          </div>\n        );\n\n      case 'cancel':\n        // For cancel events, show cancellation info\n        return (\n          <div className=\"content-cancel\">\n            <div className=\"content-cancel-label mb-1\">Stream Cancelled</div>\n            <div>{response.statusDescription || 'The gRPC stream was cancelled'}</div>\n          </div>\n        );\n\n      default:\n        return null;\n    }\n  };\n\n  return (\n    <StyledWrapper className={`${eventClass} pl-1 mb-2`}>\n      <div className=\"event-header\" onClick={toggleCollapse}>\n        {isCollapsed ? <IconChevronRight size={16} strokeWidth={1.5} /> : <IconChevronDown size={16} strokeWidth={1.5} />}\n        <div className=\"event-icon-container\">\n          {eventIcon}\n        </div>\n        <span>{eventName}</span>\n        {eventType === 'request' && effectiveRequest.methodType && (\n          <span className=\"method-type-badge px-2 py-0.5\">\n            {effectiveRequest.methodType}\n          </span>\n        )}\n        {eventType === 'status' && (\n          <div className=\"flex items-center gap-2\">\n            <Status statusCode={statusCode} statusText={statusText} />\n          </div>\n        )}\n        <pre className=\"event-timestamp\">[{new Date(timestamp).toISOString()}]</pre>\n        <span className=\"timestamp-text ml-auto\">\n          <RelativeTime timestamp={timestamp} />\n        </span>\n      </div>\n\n      {/* Always show the URL */}\n      <div className=\"url-text\">{url}</div>\n\n      {/* Expanded content - only show for non-status items */}\n      {!isCollapsed && renderEventContent()}\n    </StyledWrapper>\n  );\n};\n\nexport default GrpcTimelineItem;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/Timeline/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  position: relative;\n  overflow-y: auto;\n  height: 100%;\n  flex: 1;\n\n  .timeline-container {\n    flex: 1;\n  }\n\n  .timeline-item {\n    border-color: ${(props) => props.theme.border.border1};\n  }\n\n  .timeline-event {\n    cursor: pointer;\n  }\n\n  .timeline-event-content {\n    border-radius: 4px;\n    padding: 12px;\n    margin-top: 0.5rem;\n  }\n\n  .timeline-event-header {\n    color: ${(props) => props.theme.text};\n  }\n\n  .method-label {\n    font-weight: 500;\n  }\n\n  .status-code {\n    font-weight: 500;\n  }\n\n  .url-text {\n    color: ${(props) => props.theme.colors.text.muted};\n    font-size: ${(props) => props.theme.font.size.base};\n    margin-top: 0.25rem;\n  }\n\n  .timestamp {\n    color: ${(props) => props.theme.colors.text.muted};\n    font-size: ${(props) => props.theme.font.size.base};\n  }\n\n  .meta-info {\n    color: ${(props) => props.theme.colors.text.muted};\n    font-size: ${(props) => props.theme.font.size.base};\n  }\n\n  .oauth-section {\n    .oauth-header {\n      display: flex;\n      align-items: center;\n      color: ${(props) => props.theme.text};\n      font-weight: 500;\n\n      span {\n        margin-left: 0.5rem;\n      }\n    }\n  }\n\n  .tabs-switcher {\n    border-bottom: 1px solid ${(props) => props.theme.border.border1};\n    margin-bottom: 16px;\n\n    button {\n      position: relative;\n      padding: 8px 16px;\n      color: ${(props) => props.theme.colors.text.muted};\n\n      &.active {\n        color: ${(props) => props.theme.tabs.active.color};\n        &:after {\n          content: '';\n          position: absolute;\n          bottom: -1px;\n          left: 0;\n          right: 0;\n          height: 2px;\n          background: ${(props) => props.theme.tabs.active.border};\n        }\n      }\n    }\n  }\n\n  .network-logs {\n    background: ${(props) => props.theme.codemirror.bg};\n    color: ${(props) => props.theme.text};\n    border-radius: 4px;\n  }\n\n  .oauth-request-item-content {\n    border-radius: 4px;\n    margin-top: 0.5rem;\n  }\n\n  .collapsible-section {\n    margin-bottom: 12px;\n\n    .section-header {\n      cursor: pointer;\n      &:hover {\n        opacity: 0.8;\n      }\n    }\n  }\n\n  .line {\n    white-space: pre-line;\n    word-wrap: break-word;\n    word-break: break-all;\n    font-family: ${(props) => props.theme.font || 'Inter, sans-serif'} !important;\n\n    .arrow {\n      opacity: 0.5;\n    }\n\n    &.request {\n      color: ${(props) => props.theme.colors.text.green};\n    }\n\n    &.response {\n      color: ${(props) => props.theme.colors.text.purple};\n    }\n  }\n\n  .request-label {\n    font-size: ${(props) => props.theme.font.size.base};\n    padding: 2px 6px;\n    border-radius: 3px;\n    margin-left: 8px;\n    background: ${(props) => props.theme.requestTabs.bg};\n  }\n\n  table {\n    width: 100%;\n    border-collapse: collapse;\n    font-weight: 500;\n    table-layout: fixed;\n\n    thead,\n    td {\n      border: 1px solid ${(props) => props.theme.table.border};\n    }\n\n    thead {\n      color: ${(props) => props.theme.table.thead.color};\n      font-size: ${(props) => props.theme.font.size.base};\n      user-select: none;\n    }\n    td {\n      padding: 6px 10px;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Body/index.js",
    "content": "import QueryResponse from 'components/ResponsePane/QueryResponse/index';\nimport { useState } from 'react';\n\nconst BodyBlock = ({ collection, data, dataBuffer, headers, error, item, type }) => {\n  const [isBodyCollapsed, toggleBody] = useState(true);\n  return (\n    <div className=\"collapsible-section\">\n      <div className=\"section-header\" onClick={() => toggleBody(!isBodyCollapsed)}>\n        <pre className=\"flex flex-row items-center\">\n          <div className=\"opacity-70\">{isBodyCollapsed ? '▼' : '▶'}</div> Body\n        </pre>\n      </div>\n      {isBodyCollapsed && (\n        <div className=\"mt-2\">\n          {data || dataBuffer ? (\n            <div className=\"h-96 overflow-auto\">\n              <QueryResponse\n                item={item}\n                collection={collection}\n                data={data}\n                dataBuffer={dataBuffer}\n                headers={headers}\n                error={error}\n                key={item?.uid}\n                hideResultTypeSelector={type === 'request'}\n              />\n            </div>\n          ) : (\n            <div className=\"timeline-item-timestamp\">No Body found</div>\n          )}\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default BodyBlock;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Headers/index.js",
    "content": "import { useState } from 'react';\n\nconst HeadersBlock = ({ headers, type }) => {\n  const [areHeadersCollapsed, toggleHeaders] = useState(true);\n\n  return (\n    <div className=\"collapsible-section mt-2\">\n      <div className=\"section-header\" onClick={() => toggleHeaders(!areHeadersCollapsed)}>\n        <pre className=\"flex flex-row items-center\">\n          <div className=\"opacity-70\">{areHeadersCollapsed ? '▼' : '▶'}</div> Headers\n          {headers && Object.keys(headers).length > 0\n            && <div className=\"ml-1\">({Object.keys(headers).length})</div>}\n        </pre>\n      </div>\n      {areHeadersCollapsed && (\n        <div className=\"mt-1\">\n          {headers && Object.keys(headers).length > 0\n            ? <Headers headers={headers} type={type} />\n            : <div className=\"timeline-item-timestamp\">No Headers found</div>}\n        </div>\n      )}\n    </div>\n  );\n};\n\nconst Headers = ({ headers, type }) => {\n  if (Array.isArray(headers)) {\n    return (\n      <div className=\"mt-1\">\n        {headers.map((header, index) => (\n          <pre key={index} className=\"mb-1 whitespace-pre-wrap\">\n            {type === 'request' ? '>' : '<'}&nbsp;<span className=\"opacity-60\">{header?.name}:</span>\n            <span className=\"whitespace-pre-wrap\">{String(header?.value)}</span>\n          </pre>\n        ))}\n      </div>\n    );\n  } else {\n    return (\n      <div className=\"mt-1\">\n        {Object.entries(headers).map(([key, value], index) => (\n          <pre key={index} className=\"mb-1 whitespace-pre-wrap\">\n            {type === 'request' ? '>' : '<'}&nbsp;<span className=\"opacity-60\">{key}:</span>\n            <span>{String(value)}</span>\n          </pre>\n        ))}\n      </div>\n    );\n  }\n};\n\nexport default HeadersBlock;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Method/index.js",
    "content": "import { useMemo } from 'react';\nimport { useTheme } from 'providers/Theme';\n\nconst Method = ({ method }) => {\n  const { theme } = useTheme();\n\n  const methodColor = useMemo(() => {\n    const methodLower = method?.toLowerCase();\n    return theme.request.methods[methodLower] || theme.text;\n  }, [method, theme]);\n\n  return (\n    <span className=\"font-medium uppercase\" style={{ color: methodColor, fontSize: theme.font.size.xs }}>\n      {method}\n    </span>\n  );\n};\n\nexport default Method;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Status/index.js",
    "content": "import { useTheme } from 'providers/Theme';\n\nconst Status = ({ statusCode, statusText }) => {\n  const { theme } = useTheme();\n\n  let statusColor = theme.colors.text.muted;\n  if (statusCode >= 200 && statusCode < 300) {\n    statusColor = theme.requestTabPanel.responseOk;\n  } else if (statusCode >= 300 && statusCode < 400) {\n    statusColor = theme.colors.text.warning;\n  } else if (statusCode >= 400 && statusCode < 600) {\n    statusColor = theme.requestTabPanel.responseError;\n  }\n\n  return (\n    <span className=\"timeline-status\" style={{ color: statusColor, fontWeight: 'bold' }}>\n      {statusCode}{' '}\n      {statusText || ''}\n    </span>\n  );\n};\n\nexport default Status;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Common/Time/index.js",
    "content": "import { useState, useEffect } from 'react';\nimport { useTheme } from 'providers/Theme';\n\nconst getRelativeTime = (date) => {\n  const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });\n  const diff = (date - new Date()) / 1000;\n\n  const timeUnits = [\n    { unit: 'year', seconds: 31536000 },\n    { unit: 'month', seconds: 2592000 },\n    { unit: 'week', seconds: 604800 },\n    { unit: 'day', seconds: 86400 },\n    { unit: 'hour', seconds: 3600 },\n    { unit: 'minute', seconds: 60 },\n    { unit: 'second', seconds: 1 }\n  ];\n\n  for (const { unit, seconds } of timeUnits) {\n    if (Math.abs(diff) >= seconds || unit === 'second') {\n      return rtf.format(Math.round(diff / seconds), unit);\n    }\n  }\n};\n\nexport const RelativeTime = ({ timestamp }) => {\n  const [relativeTime, setRelativeTime] = useState(getRelativeTime(new Date(timestamp)));\n  const { theme } = useTheme();\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      setRelativeTime(getRelativeTime(new Date(timestamp)));\n    }, 1000);\n\n    return () => clearInterval(interval);\n  }, [timestamp]);\n\n  return (\n    <span\n      title={new Date(timestamp).toLocaleString()}\n      style={{\n        fontSize: theme.font.size.xs,\n        color: theme.colors.text.muted\n      }}\n    >\n      {relativeTime}\n    </span>\n  );\n};\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Network/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .network-logs-container {\n    background: ${(props) => props.theme.codemirror.bg};\n    color: ${(props) => props.theme.text};\n    border-radius: 4px;\n    overflow: auto;\n    height: 24rem;\n  }\n\n  .network-logs-pre {\n    white-space: pre-wrap;\n    font-size: ${(props) => props.theme.font.size.base};\n    font-family: var(--font-family-mono);\n  }\n\n  .network-logs-entry {\n    color: ${(props) => props.theme.colors.text.muted};\n\n    &--request {\n      color: ${(props) => props.theme.textLink};\n    }\n\n    &--response {\n      color: ${(props) => props.theme.colors.text.green};\n    }\n    \n    &--error {\n      color: ${(props) => props.theme.colors.text.danger};\n    }\n\n    &--tls {\n      color: ${(props) => props.theme.colors.text.purple};\n    }\n    \n    &--info {\n      color: ${(props) => props.theme.colors.text.yellow};\n    }   \n  }\n\n  .network-logs-separator {\n    border-top: 2px solid ${(props) => props.theme.border.border1};\n    width: 100%;\n    margin: 0.5rem 0;\n  }\n\n  .network-logs-spacing {\n    margin-top: 1rem;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Network/index.js",
    "content": "import StyledWrapper from './StyledWrapper';\n\nconst Network = ({ logs }) => {\n  return (\n    <StyledWrapper>\n      <div className=\"network-logs-container\">\n        <pre className=\"network-logs-pre\">\n          {logs.map((currentLog, index) => {\n            if (index > 0 && currentLog?.type === 'separator') {\n              return <div className=\"network-logs-separator\" key={index} />;\n            }\n            const nextLog = logs[index + 1];\n            const isSameLogType = nextLog?.type === currentLog?.type;\n            return (\n              <div key={index}>\n                <NetworkLogsEntry entry={currentLog} />\n                {!isSameLogType && <div className=\"network-logs-spacing\" />}\n              </div>\n            );\n          })}\n        </pre>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nconst NetworkLogsEntry = ({ entry }) => {\n  const { type, message } = entry;\n  let className = 'network-logs-entry';\n\n  switch (type) {\n    case 'request':\n      className = 'network-logs-entry network-logs-entry--request';\n      break;\n    case 'response':\n      className = 'network-logs-entry network-logs-entry--response';\n      break;\n    case 'error':\n      className = 'network-logs-entry network-logs-entry--error';\n      break;\n    case 'tls':\n      className = 'network-logs-entry network-logs-entry--tls';\n      break;\n    case 'info':\n      className = 'network-logs-entry network-logs-entry--info';\n      break;\n    default:\n      className = 'network-logs-entry';\n      break;\n  }\n\n  return (\n    <div className={className}>\n      <div>{message}</div>\n    </div>\n  );\n};\n\nexport default Network;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Request/index.js",
    "content": "import Headers from '../Common/Headers/index';\nimport BodyBlock from '../Common/Body/index';\n\nconst safeStringifyJSONIfNotString = (obj) => {\n  if (obj === null || obj === undefined) return '';\n\n  if (typeof obj === 'string') {\n    return obj;\n  }\n\n  try {\n    return JSON.stringify(obj);\n  } catch (e) {\n    return '[Unserializable Object]';\n  }\n};\n\nconst Request = ({ collection, request, item }) => {\n  let { url, headers, data, dataBuffer, error } = request || {};\n  if (!dataBuffer) {\n    dataBuffer = Buffer.from(safeStringifyJSONIfNotString(data))?.toString('base64');\n  }\n\n  return (\n    <div>\n      {/* Method and URL */}\n      <div className=\"mb-1 flex gap-2\">\n        <pre className=\"whitespace-pre-wrap\" title={url}>{url}</pre>\n      </div>\n\n      {/* Headers */}\n      <Headers headers={headers} type=\"request\" />\n\n      {/* Body */}\n      <BodyBlock collection={collection} data={data} dataBuffer={dataBuffer} error={error} headers={headers} item={item} type=\"request\" />\n    </div>\n  );\n};\n\nexport default Request;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Response/index.js",
    "content": "import BodyBlock from '../Common/Body/index';\nimport Headers from '../Common/Headers/index';\nimport Status from '../Common/Status/index';\n\nconst safeStringifyJSONIfNotString = (obj) => {\n  if (obj === null || obj === undefined) return '';\n\n  if (typeof obj === 'string') {\n    return obj;\n  }\n\n  try {\n    return JSON.stringify(obj);\n  } catch (e) {\n    return '[Unserializable Object]';\n  }\n};\n\nconst Response = ({ collection, response, item }) => {\n  let { status, statusCode, statusText, dataBuffer, headers, data, error } = response || {};\n  if (!dataBuffer) {\n    dataBuffer = Buffer.from(safeStringifyJSONIfNotString(data))?.toString('base64');\n  }\n\n  return (\n    <div>\n      {/* Status */}\n      <div className=\"mb-1\">\n        <Status statusCode={status || statusCode} statusText={statusText} />\n        {response.duration && <span className=\"timeline-item-metadata\">{response.duration}ms</span>}\n        {response.size && <span className=\"timeline-item-metadata\">{response.size}B</span>}\n      </div>\n\n      {/* Headers */}\n      <Headers headers={headers} type=\"response\" />\n\n      {/* Body */}\n      <BodyBlock collection={collection} data={data} dataBuffer={dataBuffer} error={error} headers={headers} item={item} type=\"response\" />\n    </div>\n  );\n};\n\nexport default Response;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst StyledWrapper = styled.div`\n  .timeline-item {\n    border-bottom: 1px solid ${(props) => props.theme.border.border1};\n    padding: 0.5rem 0;\n\n    &--oauth2 {\n      border-bottom: 1px solid ${(props) => props.theme.border.border1};\n    }\n  }\n\n  .timeline-item-header {\n    position: relative;\n    cursor: pointer;\n  }\n\n  .timeline-item-header-content {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    min-width: 0;\n  }\n\n  .timeline-item-header-items {\n    display: flex;\n    align-items: center;\n    gap: 0.5rem;\n    min-width: 0;\n  }\n\n  .timeline-item-url {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    margin-top: 0.25rem;\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .timeline-item-timestamp {\n    color: ${(props) => props.theme.colors.text.muted};\n    flex-shrink: 0;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n\n  .timeline-item-timestamp-iso {\n    opacity: 0.7;\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .timeline-item-oauth-label {\n    opacity: 0.5;\n    color: ${(props) => props.theme.text};\n  }\n\n  .timeline-item-content {\n    overflow: hidden;\n  }\n\n  .timeline-item-tabs {\n    display: flex;\n    margin-bottom: 1rem;\n  }\n\n  .timeline-item-tab {\n    margin-right: 1rem;\n    position: relative;\n    padding: 0.5rem 1rem;\n    color: ${(props) => props.theme.colors.text.muted};\n    background: none;\n    border: none;\n    cursor: pointer;\n    font-size: ${(props) => props.theme.font.size.base};\n\n    &--active {\n      color: ${(props) => props.theme.tabs.active.color};\n\n      &:after {\n        content: '';\n        position: absolute;\n        bottom: -1px;\n        left: 0;\n        right: 0;\n        height: 2px;\n        background: ${(props) => props.theme.tabs.active.border};\n      }\n    }\n  }\n\n  .timeline-item-tab-content {\n    word-break: break-all;\n  }\n\n  .timeline-item-metadata {\n    color: ${(props) => props.theme.colors.text.muted};\n    margin-left: 0.5rem;\n    font-size: ${(props) => props.theme.font.size.base};\n  }\n\n  .collapsible-section {\n    .section-header {\n      cursor: pointer;\n      pre {\n        color: ${(props) => rgba(props.theme.primary.text, 0.8)};\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/index.js",
    "content": "import { useState } from 'react';\nimport { useTheme } from 'providers/Theme';\nimport Network from './Network/index';\nimport Request from './Request/index';\nimport Response from './Response/index';\nimport Method from './Common/Method/index';\nimport Status from './Common/Status/index';\nimport { RelativeTime } from './Common/Time/index';\nimport StyledWrapper from './StyledWrapper';\n\nconst TimelineItem = ({ timestamp, request, response, item, collection, isOauth2, hideTimestamp = false }) => {\n  const { theme } = useTheme();\n  const [isCollapsed, _toggleCollapse] = useState(false);\n  const [activeTab, setActiveTab] = useState('request');\n  const toggleCollapse = () => _toggleCollapse((prev) => !prev);\n  const { method, status, statusCode, statusText, url = '' } = request || {};\n  const { status: responseStatus, statusCode: responseStatusCode, statusText: responseStatusText } = response || {};\n  const showNetworkLogs = response.timeline && response.timeline.length > 0;\n\n  return (\n    <StyledWrapper>\n      <div className={`timeline-item ${isOauth2 ? 'timeline-item--oauth2' : ''}`}>\n        <div className=\"oauth-request-item-header relative cursor-pointer flex items-center justify-between gap-3 min-w-0\" onClick={toggleCollapse}>\n          <Status statusCode={responseStatus || responseStatusCode} statusText={responseStatusText} />\n          <div className=\"flex items-center gap-1\">\n            <Method method={method} />\n            <div className=\"truncate flex-1 min-w-0\">{url}</div>\n            {isOauth2 && <span className=\"text-xs flex-shrink-0\" style={{ color: theme.colors.text.muted }}>[oauth2.0]</span>}\n          </div>\n          {!hideTimestamp && (\n            <span className=\"flex-shrink-0 ml-auto\">\n              <RelativeTime timestamp={timestamp} />\n            </span>\n          )}\n        </div>\n        {isCollapsed && (\n          <div className=\"timeline-item-content\">\n            {/* Tabs */}\n            <div className=\"timeline-item-tabs\">\n              <button\n                className={`timeline-item-tab ${activeTab === 'request' ? 'timeline-item-tab--active' : ''}`}\n                onClick={() => setActiveTab('request')}\n              >\n                Request\n              </button>\n              <button\n                className={`timeline-item-tab ${activeTab === 'response' ? 'timeline-item-tab--active' : ''}`}\n                onClick={() => setActiveTab('response')}\n              >\n                Response\n              </button>\n              {showNetworkLogs && (\n                <button\n                  className={`timeline-item-tab ${activeTab === 'networkLogs' ? 'timeline-item-tab--active' : ''}`}\n                  onClick={() => setActiveTab('networkLogs')}\n                >\n                  Network Logs\n                </button>\n              )}\n            </div>\n\n            {/* Tab Content */}\n            <div className=\"timeline-item-tab-content\">\n              {/* Request Tab */}\n              {activeTab === 'request' && (\n                <Request request={request} item={item} collection={collection} />\n              )}\n\n              {/* Response Tab */}\n              {activeTab === 'response' && (\n                <Response response={response} item={item} collection={collection} />\n              )}\n\n              {/* Network Logs Tab */}\n              {activeTab === 'networkLogs' && showNetworkLogs && (\n                <Network logs={response?.timeline} />\n              )}\n            </div>\n          </div>\n        )}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default TimelineItem;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/Timeline/index.js",
    "content": "import React from 'react';\nimport StyledWrapper from './StyledWrapper';\nimport { findItemInCollection, findParentItemInCollection } from 'utils/collections/index';\nimport { get } from 'lodash';\nimport TimelineItem from './TimelineItem/index';\nimport GrpcTimelineItem from './GrpcTimelineItem/index';\n\nconst getEffectiveAuthSource = (collection, item) => {\n  const authMode = item.draft ? get(item, 'draft.request.auth.mode') : get(item, 'request.auth.mode');\n  if (authMode !== 'inherit') return null;\n\n  const collectionRoot = collection?.draft?.root || collection?.root || {};\n  const collectionAuth = get(collectionRoot, 'request.auth');\n  let effectiveSource = {\n    type: 'collection',\n    uid: collection.uid,\n    auth: collectionAuth\n  };\n\n  // Get path from collection to item\n  let path = [];\n  let currentItem = findItemInCollection(collection, item?.uid);\n  while (currentItem) {\n    path.unshift(currentItem);\n    currentItem = findParentItemInCollection(collection, currentItem?.uid);\n  }\n\n  // Check folders in reverse to find the closest auth configuration\n  for (let i of [...path].reverse()) {\n    if (i.type === 'folder') {\n      const folderAuth = get(i, 'root.request.auth');\n      if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {\n        effectiveSource = {\n          type: 'folder',\n          uid: i.uid,\n          auth: folderAuth\n        };\n        break;\n      }\n    }\n  }\n\n  return effectiveSource;\n};\n\nconst Timeline = ({ collection, item }) => {\n  // Get the effective auth source if auth mode is inherit\n  const authSource = getEffectiveAuthSource(collection, item);\n  const isGrpcRequest = item.type === 'grpc-request' || item.type === 'ws-request';\n\n  // Filter timeline entries based on new rules\n  const combinedTimeline = ([...(collection?.timeline || [])]).filter((obj) => {\n    // Always show entries for this item\n    if (obj.itemUid === item.uid) return true;\n\n    // For OAuth2 entries, also show if auth is inherited\n    if (obj.type === 'oauth2' && authSource) {\n      if (authSource.type === 'folder' && obj.folderUid === authSource.uid) return true;\n      if (authSource.type === 'collection' && !obj.folderUid) return true;\n    }\n\n    return false;\n  }).sort((a, b) => b.timestamp - a.timestamp);\n\n  return (\n    <StyledWrapper\n      className=\"pb-4 w-full flex flex-grow flex-col\"\n    >\n      {/* Timeline container with scrollbar */}\n      <div\n        className=\"timeline-container\"\n      >\n        {combinedTimeline.map((event, index) => {\n          // Handle regular requests\n          if (event.type === 'request') {\n            const { data, timestamp, eventType } = event;\n            const { request, response, eventData = {}, timestamp: eventTimestamp = timestamp } = data;\n\n            if (isGrpcRequest) {\n              return (\n                <div key={index} className=\"timeline-event\">\n                  <GrpcTimelineItem\n                    timestamp={eventTimestamp}\n                    request={request}\n                    response={response}\n                    eventType={eventType}\n                    eventData={eventData}\n                    item={item}\n                    collection={collection}\n                  />\n                </div>\n              );\n            }\n\n            // Regular HTTP request\n            return (\n              <div key={index} className=\"timeline-event\">\n                <TimelineItem\n                  timestamp={timestamp}\n                  request={request}\n                  response={response}\n                  item={item}\n                  collection={collection}\n                />\n              </div>\n            );\n          } else if (event.type === 'oauth2') { // Handle OAuth2 events\n            const { data, timestamp } = event;\n            const { debugInfo } = data;\n            return (\n              <div key={index} className=\"timeline-event\">\n                <div className=\"timeline-event-header cursor-pointer flex items-center\">\n                  <div className=\"flex items-center\">\n                    <span className=\"font-bold\">OAuth2.0 Calls</span>\n                  </div>\n                </div>\n                <div className=\"mt-2\">\n                  {debugInfo && debugInfo.length > 0 ? (\n                    debugInfo.map((data, idx) => (\n                      <div className=\"ml-4\" key={idx}>\n                        <TimelineItem\n                          timestamp={timestamp}\n                          request={data?.request}\n                          response={data?.response}\n                          item={item}\n                          collection={collection}\n                          isOauth2={true}\n                        />\n                      </div>\n                    ))\n                  ) : (\n                    <div>No debug information available.</div>\n                  )}\n                </div>\n              </div>\n            );\n          }\n\n          return null;\n        })}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Timeline;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/WsResponsePane/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  height: 100%;\n  overflow: hidden;\n  border-radius: 4px;\n\n  div.tabs {\n    div.tab {\n      padding: 6px 0px;\n      border: none;\n      border-bottom: solid 2px transparent;\n      margin-right: ${(props) => props.theme.tabs.marginRight};\n      color: ${(props) => props.theme.colors.text.subtext0};\n      cursor: pointer;\n\n      &:focus,\n      &:active,\n      &:focus-within,\n      &:focus-visible,\n      &:target {\n        outline: none !important;\n        box-shadow: none !important;\n      }\n\n      &.active {\n        font-weight: ${(props) => props.theme.tabs.active.fontWeight} !important;\n        color: ${(props) => props.theme.tabs.active.color} !important;\n        border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;\n      }\n    }\n  }\n\n  .stream-status {\n    display: inline-flex;\n    align-items: center;\n\n    &.complete {\n      color: ${(props) => props.theme.colors.text.green};\n    }\n\n    &.cancelled {\n      color: ${(props) => props.theme.colors.text.danger};\n    }\n\n    &.streaming {\n      color: ${(props) => props.theme.colors.text.blue};\n    }\n  }\n\n  .message-counter {\n    display: inline-flex;\n    align-items: center;\n    margin-left: 10px;\n  }\n\n  div.tabs .action-icon {\n    color: ${(props) => props.theme.dropdown.iconColor};\n    opacity: 0.8;\n\n    &:hover {\n      color: ${(props) => props.theme.text};\n      opacity: 1;\n      background-color: ${(props) => props.theme.workspace.button.bg};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSMessagesList/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  flex: 1;\n  min-height: 0; \n  height: 100%;\n\n  .empty-state {\n    padding: 1rem;\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .ws-message {\n    background: ${(props) => props.theme.bg};\n\n    &.new {\n      background-color: ${({ theme }) => theme.table.striped};\n    }\n\n    &:not(:last-child) {\n      border-bottom: 1px solid ${({ theme }) => theme.border.border1};\n    }\n\n    &:not(:last-child).open {\n      border-bottom-width: 0px;\n    }\n\n    .message-content {\n      color: ${(props) => props.theme.text};\n    }\n\n    .message-timestamp {\n      font-size: ${(props) => props.theme.font.size.xs};\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n\n    .chevron-icon {\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n  }\n\n  .ws-incoming .message-type-icon {\n    color: ${(props) => props.theme.colors.text.green};\n  }\n\n  .ws-outgoing .message-type-icon {\n    color: ${(props) => props.theme.colors.text.yellow};\n  }\n\n  .ws-info .message-type-icon {\n    color: ${(props) => props.theme.colors.text.blue};\n  }\n\n  .ws-error .message-type-icon {\n    color: ${(props) => props.theme.colors.text.danger};\n  }\n\n  .CodeMirror {\n    border-radius: 0.25rem;\n  }\n\n  .CodeMirror-foldgutter, .CodeMirror-linenumbers, .CodeMirror-lint-markers {\n    background: ${({ theme }) => theme.bg};\n  }\n\n  div[role='tablist'] {\n    color: ${(props) => props.theme.colors.text.muted};\n\n    .active {\n      color: ${(props) => props.theme.colors.text.yellow};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSMessagesList/index.js",
    "content": "import React, { useState, useRef, useEffect, useCallback, memo } from 'react';\nimport classnames from 'classnames';\nimport StyledWrapper from './StyledWrapper';\nimport { IconExclamationCircle, IconChevronRight, IconInfoCircle, IconChevronDown, IconArrowUpRight, IconArrowDownLeft } from '@tabler/icons';\nimport CodeEditor from 'components/CodeEditor/index';\nimport { useTheme } from 'providers/Theme';\nimport { useSelector } from 'react-redux';\nimport { Virtuoso } from 'react-virtuoso';\n\nconst getContentMeta = (content) => {\n  if (typeof content === 'object') {\n    return {\n      isJSON: true,\n      content: JSON.stringify(content, null, 0)\n    };\n  }\n  try {\n    return {\n      isJSON: true,\n      content: JSON.stringify(JSON.parse(content), null, 0)\n    };\n  } catch {\n    return {\n      isJSON: false,\n      content: content\n    };\n  }\n};\n\nconst parseContent = (content) => {\n  let contentMeta = getContentMeta(content);\n  return {\n    type: contentMeta.isJSON ? 'application/json' : 'text/plain',\n    content: contentMeta.isJSON ? JSON.stringify(JSON.parse(contentMeta.content), null, 2) : contentMeta.content\n  };\n};\n\nconst getDataTypeText = (type) => {\n  const textMap = {\n    'text/plain': 'RAW',\n    'application/json': 'JSON'\n  };\n  return textMap[type] ?? 'RAW';\n};\n\n/**\n *\n * @param {\"incoming\"|\"outgoing\"|\"info\"} type\n */\nconst TypeIcon = ({ type }) => {\n  const commonProps = {\n    size: 18\n  };\n  return {\n    incoming: <IconArrowDownLeft {...commonProps} />,\n    outgoing: <IconArrowUpRight {...commonProps} />,\n    info: <IconInfoCircle {...commonProps} />,\n    error: <IconExclamationCircle {...commonProps} />\n  }[type];\n};\n\nconst WSMessageItem = memo(({ message, isOpen, onToggle }) => {\n  const [showHex, setShowHex] = useState(false);\n  const preferences = useSelector((state) => state.app.preferences);\n  const { displayedTheme } = useTheme();\n  const [isNew, setIsNew] = useState(false);\n  const notified = useRef(false);\n\n  const isIncoming = message.type === 'incoming';\n  const isInfo = message.type === 'info';\n  const isError = message.type === 'error';\n  const isOutgoing = message.type === 'outgoing';\n  let contentHexdump = message.messageHexdump;\n  let parsedContent = parseContent(message.message);\n  const dataType = getDataTypeText(parsedContent.type);\n\n  useEffect(() => {\n    if (notified.current === true) return;\n    const dateDiff = Date.now() - new Date(message.timestamp).getTime();\n    if (dateDiff < 1000 * 10) {\n      setIsNew(true);\n      const timer = setTimeout(() => {\n        notified.current = true;\n        setIsNew(false);\n      }, 2500);\n      return () => clearTimeout(timer);\n    }\n  }, [message.timestamp]);\n\n  const canOpenMessage = !isInfo && !isError;\n\n  const handleToggle = () => {\n    if (!canOpenMessage) return;\n    onToggle?.(message.timestamp);\n  };\n\n  return (\n    <div\n      className={classnames('ws-message flex flex-col p-2', {\n        'ws-incoming': isIncoming,\n        'ws-outgoing': isOutgoing,\n        'ws-info': isInfo,\n        'ws-error': isError,\n        'open': isOpen,\n        'new': isNew\n      })}\n    >\n      <div\n        className={classnames('flex items-center justify-between', {\n          'cursor-pointer': canOpenMessage,\n          'cursor-not-allowed': !canOpenMessage\n        })}\n        onClick={handleToggle}\n      >\n        <div className=\"flex min-w-0 shrink\">\n          <span className=\"message-type-icon\">\n            <TypeIcon type={message.type} />\n          </span>\n          <span className=\"ml-3 text-ellipsis max-w-full overflow-hidden text-nowrap message-content\">{parsedContent.content}</span>\n        </div>\n        <div className=\"flex shrink-0 gap-2 items-center\">\n          {message.timestamp && (\n            <span className=\"message-timestamp\">{new Date(message.timestamp).toISOString()}</span>\n          )}\n          {canOpenMessage\n            ? (\n                <span className=\"chevron-icon\">\n                  {isOpen ? (\n                    <IconChevronDown size={16} strokeWidth={1.5} />\n                  ) : (\n                    <IconChevronRight size={16} strokeWidth={1.5} />\n                  )}\n                </span>\n              )\n            : <span className=\"w-4\"></span>}\n        </div>\n      </div>\n      {isOpen && (\n        <>\n          <div className=\"mt-2 flex justify-end gap-2 text-xs ws-message-toolbar\" role=\"tablist\">\n            <div\n              className={classnames('select-none capitalize', {\n                'active': showHex,\n                'cursor-pointer': !showHex\n              })}\n              role=\"tab\"\n              onClick={() => setShowHex(true)}\n            >\n              hexdump\n            </div>\n            <div\n              className={classnames('select-none capitalize', {\n                'active': !showHex,\n                'cursor-pointer': showHex\n              })}\n              role=\"tab\"\n              onClick={() => setShowHex(false)}\n            >\n              {dataType.toLowerCase()}\n            </div>\n          </div>\n          <div className=\"mt-1 h-[300px] w-full\">\n            <CodeEditor\n              mode={showHex ? 'text/plain' : parsedContent.type}\n              theme={displayedTheme}\n              enableLineWrapping={showHex ? false : true}\n              font={preferences.codeFont || 'default'}\n              value={showHex ? contentHexdump : parsedContent.content}\n            />\n          </div>\n        </>\n      )}\n    </div>\n  );\n});\n\nconst WSMessagesList = ({ messages = [] }) => {\n  const virtuosoRef = useRef(null);\n  const [scrollerElement, setScrollerElement] = useState(null);\n  const [openMessages, setOpenMessages] = useState(new Set());\n  const userScrolledAwayRef = useRef(false);\n\n  // Toggle message open/closed state by timestamp\n  const handleMessageToggle = useCallback((timestamp) => {\n    setOpenMessages((prev) => {\n      const next = new Set(prev);\n      if (next.has(timestamp)) {\n        next.delete(timestamp);\n      } else {\n        next.add(timestamp);\n      }\n      return next;\n    });\n  }, []);\n\n  useEffect(() => {\n    if (!scrollerElement) return;\n\n    const handleWheel = (e) => {\n      // deltaY < 0 means scrolling up\n      if (e.deltaY < 0) {\n        userScrolledAwayRef.current = true;\n      }\n    };\n\n    scrollerElement.addEventListener('wheel', handleWheel, { passive: true });\n\n    return () => {\n      scrollerElement.removeEventListener('wheel', handleWheel);\n    };\n  }, [scrollerElement]);\n\n  const handleAtBottomStateChange = useCallback((atBottom) => {\n    if (atBottom) {\n      // User scrolled back to bottom, re-enable auto-scroll\n      userScrolledAwayRef.current = false;\n    }\n  }, []);\n\n  const followOutput = useCallback((isAtBottom) => {\n    // Don't auto-scroll if user has scrolled away or has messages open\n    if (userScrolledAwayRef.current || openMessages.size > 0) {\n      return false;\n    }\n    if (isAtBottom) {\n      return 'smooth';\n    }\n    return false;\n  }, [openMessages.size]);\n\n  const renderItem = useCallback((_, msg) => {\n    const isOpen = openMessages.has(msg.timestamp);\n    return <WSMessageItem message={msg} isOpen={isOpen} onToggle={handleMessageToggle} />;\n  }, [openMessages, handleMessageToggle]);\n\n  const computeItemKey = useCallback((_, msg) => {\n    return msg.seq ?? msg.timestamp;\n  }, []);\n\n  if (!messages.length) {\n    return <StyledWrapper><div className=\"empty-state\">No messages yet.</div></StyledWrapper>;\n  }\n\n  return (\n    <StyledWrapper className=\"ws-messages-list flex flex-col\">\n      <Virtuoso\n        ref={virtuosoRef}\n        scrollerRef={setScrollerElement}\n        data={messages}\n        itemContent={renderItem}\n        computeItemKey={computeItemKey}\n        followOutput={followOutput}\n        initialTopMostItemIndex={messages.length - 1}\n        atBottomStateChange={handleAtBottomStateChange}\n      />\n    </StyledWrapper>\n  );\n};\n\nexport default WSMessagesList;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSResponseHeaders/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  table {\n    width: 100%;\n    border-collapse: collapse;\n\n    thead {\n      color: ${(props) => props.theme.table.thead.color};\n      font-size: ${(props) => props.theme.font.size.sm};\n      font-weight: 500;\n      text-transform: uppercase;\n    }\n\n    td {\n      padding: 6px 10px;\n\n      &.value {\n        word-break: break-all;\n      }\n    }\n\n    tbody {\n      tr:nth-child(odd) {\n        background-color: ${(props) => props.theme.table.striped};\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSResponseHeaders/index.js",
    "content": "import React from 'react';\nimport StyledWrapper from './StyledWrapper';\n\nconst WSResponseHeaders = ({ response }) => {\n  const formatHeaders = (headers) => {\n    if (!headers) return [];\n    if (Array.isArray(headers)) return headers;\n    return Object.entries(headers).map(([key, value]) => ({ name: key, value }));\n  };\n\n  const headersArray = formatHeaders(response.headers);\n\n  return (\n    <StyledWrapper className=\"pb-4 w-full\">\n      <table>\n        <thead>\n          <tr>\n            <td>Name</td>\n            <td>Value</td>\n          </tr>\n        </thead>\n        <tbody>\n          {headersArray && headersArray.length ? (\n            headersArray.map((header, index) => (\n              <tr key={index}>\n                <td className=\"key\">{header.name}</td>\n                <td className=\"value\">{header.value}</td>\n              </tr>\n            ))\n          ) : (\n            <tr>\n              <td colSpan=\"2\" className=\"text-center py-4 text-gray-500\">\n                No headers received\n              </td>\n            </tr>\n          )}\n        </tbody>\n      </table>\n    </StyledWrapper>\n  );\n};\n\nexport default WSResponseHeaders;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSResponseSortOrder/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  font-size: ${(props) => props.theme.font.size.base};\n  color: ${(props) => props.theme.requestTabPanel.responseStatus};\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSResponseSortOrder/index.js",
    "content": "import React from 'react';\nimport { IconSortDescending2, IconSortAscending2 } from '@tabler/icons';\nimport { useDispatch } from 'react-redux';\nimport StyledWrapper from './StyledWrapper';\nimport { wsUpdateResponseSortOrder } from 'providers/ReduxStore/slices/collections/index';\n\nconst WSResponseSortOrder = ({ collection, item }) => {\n  const dispatch = useDispatch();\n\n  const order = item.response?.sortOrder ?? -1;\n\n  const toggleSortOrder = () => {\n    dispatch(wsUpdateResponseSortOrder({\n      itemUid: item.uid,\n      collectionUid: collection.uid\n    }));\n  };\n\n  return (\n    <StyledWrapper className=\"ml-2 flex items-center\">\n      <button onClick={toggleSortOrder} title={order === -1 ? 'Latest Last' : 'Latest First'}>\n        { order === -1\n          ? <IconSortDescending2 size={16} strokeWidth={1.5} />\n          : <IconSortAscending2 size={16} strokeWidth={1.5} />}\n      </button>\n    </StyledWrapper>\n  );\n};\n\nexport default WSResponseSortOrder;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSStatusCode/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  font-size: ${(props) => props.theme.font.size.sm};\n  font-weight: 500;\n  display: flex;\n  align-items: center;\n\n  &.text-ok {\n    color: ${(props) => props.theme.requestTabPanel.responseOk};\n  }\n\n  &.text-pending {\n    color: ${(props) => props.theme.requestTabPanel.responsePending};\n  }\n\n  &.text-error {\n    color: ${(props) => props.theme.requestTabPanel.responseError};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSStatusCode/get-ws-status-code-phrase.js",
    "content": "const wsStatusCodePhraseMap = {\n  1000: 'NORMAL_CLOSURE',\n  1001: 'GOING_AWAY',\n  1002: 'PROTOCOL_ERROR',\n  1003: 'UNSUPPORTED_DATA',\n  1004: 'RESERVED',\n  1005: 'NO_STATUS_RECEIVED',\n  1006: 'ABNORMAL_CLOSURE',\n  1007: 'INVALID_FRAME_PAYLOAD_DATA',\n  1008: 'POLICY_VIOLATION',\n  1009: 'MESSAGE_TOO_BIG',\n  1010: 'MANDATORY_EXTENSION',\n  1011: 'INTERNAL_ERROR',\n  1012: 'SERVICE_RESTART',\n  1013: 'TRY_AGAIN_LATER',\n  1014: 'BAD_GATEWAY',\n  1015: 'TLS_HANDSHAKE'\n};\n\nexport default wsStatusCodePhraseMap;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/WsResponsePane/WSStatusCode/index.js",
    "content": "import React from 'react';\nimport classnames from 'classnames';\nimport wsStatusCodePhraseMap from './get-ws-status-code-phrase';\nimport StyledWrapper from './StyledWrapper';\n\nconst WSStatusCode = ({ status, text }) => {\n  const getTabClassname = (status) => {\n    return classnames('ml-2', {\n      // ok if normal connect and normal closure\n      'text-ok': parseInt(status) === 0 || parseInt(status) === 1000,\n      'text-error': parseInt(status) !== 1000 && parseInt(status) !== 0\n    });\n  };\n\n  const statusText = text || wsStatusCodePhraseMap[status];\n\n  return (\n    <StyledWrapper className={getTabClassname(status)}>\n      {Number.isInteger(status) && status != 0 ? <div className=\"mr-1\">{status}</div> : null}\n      {statusText && <div>{statusText}</div>}\n    </StyledWrapper>\n  );\n};\n\nexport default WSStatusCode;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/WsResponsePane/index.js",
    "content": "import React, { useMemo, useRef } from 'react';\nimport find from 'lodash/find';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { updateResponsePaneTab } from 'providers/ReduxStore/slices/tabs';\nimport Overlay from '../Overlay';\nimport Placeholder from '../Placeholder';\nimport WSStatusCode from './WSStatusCode';\nimport ResponseTime from '../ResponseTime/index';\nimport Timeline from '../Timeline';\nimport ClearTimeline from '../ClearTimeline';\nimport ResponseClear from '../ResponseClear';\nimport StyledWrapper from './StyledWrapper';\nimport ResponseLayoutToggle from '../ResponseLayoutToggle';\nimport ResponsiveTabs from 'ui/ResponsiveTabs';\nimport WSMessagesList from './WSMessagesList';\nimport WSResponseHeaders from './WSResponseHeaders';\n\nconst WSResult = ({ response }) => {\n  return <WSMessagesList messages={response.responses || []} />;\n};\n\nconst WSResponsePane = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  const tabs = useSelector((state) => state.tabs.tabs);\n  const activeTabUid = useSelector((state) => state.tabs.activeTabUid);\n  const isLoading = ['queued', 'sending'].includes(item.requestState);\n  const rightContentRef = useRef(null);\n\n  const requestTimeline = [...(collection?.timeline || [])].filter((obj) => {\n    if (obj.itemUid === item.uid) return true;\n  });\n\n  const selectTab = (tab) => {\n    dispatch(updateResponsePaneTab({\n      uid: item.uid,\n      responsePaneTab: tab\n    }));\n  };\n\n  const response = item.response || {};\n\n  const messagesCount = Array.isArray(response.responses) ? response.responses.length : 0;\n  const headersCount = response.headers ? Object.keys(response.headers).length : 0;\n\n  const allTabs = useMemo(() => {\n    return [\n      {\n        key: 'response',\n        label: 'Messages',\n        indicator: messagesCount > 0 ? <sup className=\"ml-1 font-medium\">{messagesCount}</sup> : null\n      },\n      {\n        key: 'headers',\n        label: 'Headers',\n        indicator: headersCount > 0 ? <sup className=\"ml-1 font-medium\">{headersCount}</sup> : null\n      },\n      {\n        key: 'timeline',\n        label: 'Timeline',\n        indicator: null\n      }\n    ];\n  }, [messagesCount, headersCount]);\n\n  const getTabPanel = (tab) => {\n    switch (tab) {\n      case 'response': {\n        return <WSResult response={response} />;\n      }\n      case 'headers': {\n        return <WSResponseHeaders response={response} />;\n      }\n      case 'timeline': {\n        return <Timeline collection={collection} item={item} />;\n      }\n      default: {\n        return <div>404 | Not found</div>;\n      }\n    }\n  };\n\n  if (isLoading && !item.response) {\n    return (\n      <StyledWrapper className=\"flex flex-col h-full relative\">\n        <Overlay item={item} collection={collection} />\n      </StyledWrapper>\n    );\n  }\n\n  if (!item.response && !requestTimeline?.length) {\n    return (\n      <StyledWrapper className=\"flex h-full relative\">\n        <Placeholder />\n      </StyledWrapper>\n    );\n  }\n\n  if (!activeTabUid) {\n    return <div>Something went wrong</div>;\n  }\n\n  const focusedTab = find(tabs, (t) => t.uid === activeTabUid);\n  if (!focusedTab || !focusedTab.uid || !focusedTab.responsePaneTab) {\n    return <div className=\"pb-4 px-4\">An error occurred!</div>;\n  }\n\n  const rightContent = !isLoading ? (\n    <div ref={rightContentRef} className=\"flex items-center\">\n      {focusedTab?.responsePaneTab === 'timeline' ? (\n        <>\n          <ResponseLayoutToggle />\n          <ClearTimeline item={item} collection={collection} />\n        </>\n      ) : item?.response ? (\n        <>\n          <ResponseLayoutToggle />\n          <ResponseClear item={item} collection={collection} />\n          <WSStatusCode\n            status={response.statusCode}\n            text={response.statusText}\n            details={response.statusDescription}\n          />\n          <ResponseTime duration={response.duration} />\n        </>\n      ) : null}\n    </div>\n  ) : null;\n\n  return (\n    <StyledWrapper className=\"flex flex-col h-full relative\">\n      <div className=\"px-4\">\n        <ResponsiveTabs\n          tabs={allTabs}\n          activeTab={focusedTab.responsePaneTab}\n          onTabSelect={selectTab}\n          rightContent={rightContent}\n          rightContentRef={rightContentRef}\n        />\n      </div>\n      <section className=\"flex flex-col flex-grow px-4 h-0 mt-4\">\n        {isLoading ? <Overlay item={item} collection={collection} /> : null}\n        {!item?.response ? (\n          focusedTab?.responsePaneTab === 'timeline' && requestTimeline?.length ? (\n            <Timeline collection={collection} item={item} />\n          ) : null\n        ) : (\n          <>{getTabPanel(focusedTab.responsePaneTab)}</>\n        )}\n      </section>\n    </StyledWrapper>\n  );\n};\n\nexport default WSResponsePane;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ResponsePane/index.js",
    "content": "import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react';\nimport find from 'lodash/find';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { updateResponsePaneTab, updateResponseFormat, updateResponseViewTab } from 'providers/ReduxStore/slices/tabs';\nimport QueryResult from './QueryResult';\nimport Overlay from './Overlay';\nimport Placeholder from './Placeholder';\nimport ResponseHeaders from './ResponseHeaders';\nimport StatusCode from './StatusCode';\nimport ResponseTime from './ResponseTime';\nimport ResponseSize from './ResponseSize';\nimport Timeline from './Timeline';\nimport TestResults from './TestResults';\nimport TestResultsLabel from './TestResultsLabel';\nimport ScriptError from './ScriptError';\nimport ScriptErrorIcon from './ScriptErrorIcon';\nimport StyledWrapper from './StyledWrapper';\nimport ResponsePaneActions from './ResponsePaneActions';\nimport QueryResultTypeSelector from './QueryResult/QueryResultTypeSelector/index';\nimport { useInitialResponseFormat, useResponsePreviewFormatOptions } from './QueryResult/index';\nimport SkippedRequest from './SkippedRequest';\nimport ClearTimeline from './ClearTimeline/index';\nimport HeightBoundContainer from 'ui/HeightBoundContainer';\nimport ResponseStopWatch from 'components/ResponsePane/ResponseStopWatch';\nimport WSMessagesList from './WsResponsePane/WSMessagesList';\nimport ResponsiveTabs from 'ui/ResponsiveTabs';\n\n// Width threshold for expanded right-side action buttons\nconst RIGHT_CONTENT_EXPANDED_WIDTH = 135;\n\nconst ResponsePane = ({ item, collection }) => {\n  const dispatch = useDispatch();\n  const tabs = useSelector((state) => state.tabs.tabs);\n  const activeTabUid = useSelector((state) => state.tabs.activeTabUid);\n  const isLoading = ['queued', 'sending'].includes(item.requestState);\n  const [showScriptErrorCard, setShowScriptErrorCard] = useState(false);\n  const rightContentRef = useRef(null);\n\n  const response = item.response || {};\n\n  // Get the focused tab for reading persisted format/view state\n  const focusedTab = find(tabs, (t) => t.uid === activeTabUid);\n\n  // Initialize format and tab only once when data loads.\n  const { initialFormat, initialTab, contentType } = useInitialResponseFormat(response?.dataBuffer, response?.headers);\n  const previewFormatOptions = useResponsePreviewFormatOptions(response?.dataBuffer, response?.headers);\n\n  // Track previous response headers to detect when content-type changes\n  const previousContentRef = useRef(contentType);\n\n  const persistedFormat = focusedTab?.responseFormat;\n  const persistedViewTab = focusedTab?.responseViewTab;\n\n  // Use persisted values from Redux, falling back to initial values or defaults\n  const selectedFormat = persistedFormat ?? initialFormat ?? 'raw';\n  const selectedViewTab = persistedViewTab ?? initialTab ?? 'editor';\n\n  useEffect(() => {\n    if (!focusedTab || initialFormat === null || initialTab === null) {\n      return;\n    }\n\n    // Check if response headers (content-type) changed using deep comparison\n    const contentTypeChanged = contentType !== previousContentRef.current;\n    if (contentTypeChanged) {\n      previousContentRef.current = contentType;\n    }\n    if (contentTypeChanged || persistedFormat === null) {\n      dispatch(updateResponseFormat({ uid: item.uid, responseFormat: initialFormat }));\n    }\n    if (contentTypeChanged || persistedViewTab === null) {\n      dispatch(updateResponseViewTab({ uid: item.uid, responseViewTab: initialTab }));\n    }\n  }, [contentType, initialFormat, initialTab, persistedFormat, persistedViewTab, focusedTab, item.uid, dispatch]);\n\n  const handleFormatChange = useCallback((newFormat) => {\n    dispatch(updateResponseFormat({ uid: item.uid, responseFormat: newFormat }));\n  }, [dispatch, item.uid]);\n\n  const handleViewTabChange = useCallback((newViewTab) => {\n    dispatch(updateResponseViewTab({ uid: item.uid, responseViewTab: newViewTab }));\n  }, [dispatch, item.uid]);\n\n  const requestTimeline = ([...(collection.timeline || [])]).filter((obj) => {\n    if (obj.itemUid === item.uid) return true;\n  });\n\n  useEffect(() => {\n    if (item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage || item?.testScriptErrorMessage) {\n      setShowScriptErrorCard(true);\n    }\n  }, [item?.preRequestScriptErrorMessage, item?.postResponseScriptErrorMessage, item?.testScriptErrorMessage]);\n\n  const selectTab = (tab) => {\n    dispatch(\n      updateResponsePaneTab({\n        uid: item.uid,\n        responsePaneTab: tab\n      })\n    );\n  };\n  const responseSize = useMemo(() => {\n    if (typeof response.size === 'number') {\n      return response.size;\n    }\n\n    if (!response.dataBuffer) return 0;\n\n    try {\n      // dataBuffer is base64 encoded, so we need to calculate the actual size\n      const buffer = Buffer.from(response.dataBuffer, 'base64');\n      return buffer.length;\n    } catch (error) {\n      return 0;\n    }\n  }, [response.size, response.dataBuffer]);\n  const responseHeadersCount = typeof response.headers === 'object' ? Object.entries(response.headers).length : 0;\n\n  const hasScriptError = item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage || item?.testScriptErrorMessage;\n\n  const allTabs = useMemo(() => {\n    return [\n      {\n        key: 'response',\n        label: 'Response',\n        indicator: null\n      },\n      {\n        key: 'headers',\n        label: 'Headers',\n        indicator: responseHeadersCount > 0 ? <sup className=\"ml-1 font-medium\">{responseHeadersCount}</sup> : null\n      },\n      {\n        key: 'timeline',\n        label: 'Timeline',\n        indicator: null\n      },\n      {\n        key: 'tests',\n        label: (\n          <TestResultsLabel\n            results={item.testResults}\n            assertionResults={item.assertionResults}\n            preRequestTestResults={item.preRequestTestResults}\n            postResponseTestResults={item.postResponseTestResults}\n          />\n        ),\n        indicator: null\n      }\n    ];\n  }, [responseHeadersCount, item.testResults, item.assertionResults, item.preRequestTestResults, item.postResponseTestResults]);\n\n  const getTabPanel = (tab) => {\n    switch (tab) {\n      case 'response': {\n        const isStream = item.response?.stream ?? false;\n        if (isStream) {\n          return <WSMessagesList order={-1} messages={item.response.data} />;\n        }\n        return (\n          <QueryResult\n            item={item}\n            collection={collection}\n            data={response.data}\n            dataBuffer={response.dataBuffer}\n            headers={response.headers}\n            error={response.error}\n            key={item.filename}\n            selectedFormat={selectedFormat}\n            selectedTab={selectedViewTab}\n          />\n        );\n      }\n      case 'headers': {\n        return <ResponseHeaders headers={response.headers} />;\n      }\n      case 'timeline': {\n        return <Timeline collection={collection} item={item} />;\n      }\n      case 'tests': {\n        return (\n          <TestResults\n            results={item.testResults}\n            assertionResults={item.assertionResults}\n            preRequestTestResults={item.preRequestTestResults}\n            postResponseTestResults={item.postResponseTestResults}\n          />\n        );\n      }\n\n      default: {\n        return <div>404 | Not found</div>;\n      }\n    }\n  };\n\n  if (item.response && item.status === 'skipped') {\n    return (\n      <StyledWrapper className=\"flex h-full relative\">\n        <SkippedRequest />\n      </StyledWrapper>\n    );\n  }\n\n  if (isLoading && !item.response) {\n    return (\n      <StyledWrapper className=\"flex flex-col h-full relative\">\n        <Overlay item={item} collection={collection} />\n      </StyledWrapper>\n    );\n  }\n\n  if (!item.response && !requestTimeline?.length) {\n    return (\n      <HeightBoundContainer>\n        <Placeholder />\n      </HeightBoundContainer>\n    );\n  }\n\n  if (!activeTabUid) {\n    return <div>Something went wrong</div>;\n  }\n\n  if (!focusedTab || !focusedTab.uid || !focusedTab.responsePaneTab) {\n    return <div className=\"pb-4 px-4\">An error occurred!</div>;\n  }\n\n  const rightContent = !isLoading ? (\n    <div ref={rightContentRef} className=\"flex justify-end items-center right-side-container gap-3\">\n      {hasScriptError && !showScriptErrorCard && (\n        <ScriptErrorIcon\n          itemUid={item.uid}\n          onClick={() => setShowScriptErrorCard(true)}\n        />\n      )}\n      {focusedTab?.responsePaneTab === 'response' && item?.response && !(item.response?.stream ?? false) ? (\n        <>\n          {/* Result View Tabs (Visualizations + Response Format) */}\n          <div className=\"result-view-tabs\">\n\n            {/* Response Format */}\n            <QueryResultTypeSelector\n              formatOptions={previewFormatOptions}\n              formatValue={selectedFormat}\n              onFormatChange={handleFormatChange}\n              onPreviewTabSelect={handleViewTabChange}\n              selectedTab={selectedViewTab}\n              isActiveTab={selectedViewTab === 'editor' || selectedViewTab === 'preview'}\n              onTabSelect={() => {\n                handleViewTabChange('editor');\n              }}\n            />\n          </div>\n        </>\n      ) : null}\n      <div className=\"flex items-center response-pane-status\">\n        <StatusCode status={response.status} isStreaming={item.response?.stream?.running} />\n        {item.response?.stream?.running\n          ? <ResponseStopWatch startMillis={response.duration} />\n          : <ResponseTime duration={response.duration} />}\n        <ResponseSize size={responseSize} />\n      </div>\n\n      <div className=\"flex items-center response-pane-actions\">\n        {focusedTab?.responsePaneTab === 'timeline' ? (\n          <ClearTimeline item={item} collection={collection} />\n        ) : item?.response && !item?.response?.error ? (\n          <ResponsePaneActions\n            item={item}\n            collection={collection}\n            responseSize={responseSize}\n            selectedFormat={selectedFormat}\n            selectedTab={selectedViewTab}\n            data={response.data}\n            dataBuffer={response.dataBuffer}\n          />\n        ) : null}\n      </div>\n    </div>\n  ) : null;\n\n  return (\n    <StyledWrapper className=\"flex flex-col h-full relative\">\n      <div className=\"px-4\">\n        <ResponsiveTabs\n          tabs={allTabs}\n          activeTab={focusedTab.responsePaneTab}\n          onTabSelect={selectTab}\n          rightContent={rightContent}\n          rightContentRef={rightContentRef}\n          rightContentExpandedWidth={RIGHT_CONTENT_EXPANDED_WIDTH}\n        />\n      </div>\n      <section\n        className=\"flex flex-col min-h-0 relative px-4 auto overflow-auto mt-4\"\n        style={{\n          flex: '1 1 0',\n          height: hasScriptError && showScriptErrorCard ? 'auto' : '100%'\n        }}\n      >\n        {isLoading ? <Overlay item={item} collection={collection} /> : null}\n        {hasScriptError && showScriptErrorCard && (\n          <ScriptError\n            item={item}\n            onClose={() => setShowScriptErrorCard(false)}\n          />\n        )}\n        <div className=\"flex-1 overflow-y-auto\">\n          {!item?.response ? (\n            focusedTab?.responsePaneTab === 'timeline' && requestTimeline?.length ? (\n              <Timeline\n                collection={collection}\n                item={item}\n              />\n            ) : null\n          ) : (\n            <>{getTabPanel(focusedTab.responsePaneTab)}</>\n          )}\n        </div>\n      </section>\n    </StyledWrapper>\n  );\n};\n\nexport default ResponsePane;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RunnerResults/ResponsePane/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  div.tabs {\n    div.tab {\n      padding: 6px 0px;\n      border: none;\n      border-bottom: solid 2px transparent;\n      margin-right: ${(props) => props.theme.tabs.marginRight};\n      color: ${(props) => props.theme.colors.text.subtext0};\n      cursor: pointer;\n\n      &:focus,\n      &:active,\n      &:focus-within,\n      &:focus-visible,\n      &:target {\n        outline: none !important;\n        box-shadow: none !important;\n      }\n\n      &.active {\n        font-weight: ${(props) => props.theme.tabs.active.fontWeight} !important;\n        color: ${(props) => props.theme.tabs.active.color} !important;\n        border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;\n      }\n    }\n  }\n\n  .some-tests-failed {\n    color: ${(props) => props.theme.colors.text.danger} !important;\n  }\n\n  .all-tests-passed {\n    color: ${(props) => props.theme.colors.text.green} !important;\n  }\n\n  .skipped-request {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RunnerResults/ResponsePane/index.js",
    "content": "import React, { useState, useEffect } from 'react';\nimport get from 'lodash/get';\nimport classnames from 'classnames';\nimport QueryResponse from 'components/ResponsePane/QueryResponse/index';\nimport ResponseHeaders from 'components/ResponsePane/ResponseHeaders';\nimport StatusCode from 'components/ResponsePane/StatusCode';\nimport ResponseTime from 'components/ResponsePane/ResponseTime';\nimport ResponseSize from 'components/ResponsePane/ResponseSize';\nimport TestResults from 'components/ResponsePane/TestResults';\nimport TestResultsLabel from 'components/ResponsePane/TestResultsLabel';\nimport StyledWrapper from './StyledWrapper';\nimport SkippedRequest from 'components/ResponsePane/SkippedRequest';\nimport RunnerTimeline from 'components/ResponsePane/RunnerTimeline';\nimport ScriptError from 'components/ResponsePane/ScriptError';\nimport ScriptErrorIcon from 'components/ResponsePane/ScriptErrorIcon';\n\nconst ResponsePane = ({ rightPaneWidth, item, collection }) => {\n  const [selectedTab, setSelectedTab] = useState('response');\n  const [showScriptErrorCard, setShowScriptErrorCard] = useState(false);\n\n  const { requestSent, responseReceived, testResults, assertionResults, preRequestTestResults, postResponseTestResults, error } = item;\n\n  useEffect(() => {\n    if (item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage || item?.testScriptErrorMessage) {\n      setShowScriptErrorCard(true);\n    }\n  }, [item?.preRequestScriptErrorMessage, item?.postResponseScriptErrorMessage, item?.testScriptErrorMessage]);\n\n  const headers = get(item, 'responseReceived.headers', []);\n  const status = get(item, 'responseReceived.status', 0);\n  const size = get(item, 'responseReceived.size', 0);\n  const duration = get(item, 'responseReceived.duration', 0);\n\n  const hasScriptError = item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage || item?.testScriptErrorMessage;\n\n  const selectTab = (tab) => setSelectedTab(tab);\n\n  const getTabPanel = (tab) => {\n    switch (tab) {\n      case 'response': {\n        return (\n          <QueryResponse\n            item={item}\n            collection={collection}\n            width={rightPaneWidth}\n            disableRunEventListener={true}\n            data={responseReceived.data}\n            dataBuffer={responseReceived.dataBuffer}\n            headers={responseReceived.headers}\n            error={error}\n            key={item.filename}\n          />\n        );\n      }\n      case 'headers': {\n        return <ResponseHeaders headers={headers} />;\n      }\n      case 'timeline': {\n        return (\n          <RunnerTimeline\n            request={requestSent}\n            response={responseReceived}\n            item={item}\n            collection={collection}\n          />\n        );\n      }\n      case 'tests': {\n        return (\n          <TestResults\n            results={testResults}\n            assertionResults={assertionResults}\n            preRequestTestResults={preRequestTestResults}\n            postResponseTestResults={postResponseTestResults}\n          />\n        );\n      }\n\n      default: {\n        return <div>404 | Not found</div>;\n      }\n    }\n  };\n\n  const getTabClassname = (tabName) => {\n    return classnames(`tab select-none ${tabName}`, {\n      active: tabName === selectedTab\n    });\n  };\n\n  if (item.status === 'skipped') {\n    return (\n      <StyledWrapper className=\"flex h-full relative\">\n        <SkippedRequest />\n      </StyledWrapper>\n    );\n  }\n\n  return (\n    <StyledWrapper className=\"flex flex-col h-full relative overflow-auto\">\n      <div className=\"flex items-center tabs overflow-visible\" role=\"tablist\">\n        <div className={getTabClassname('response')} role=\"tab\" onClick={() => selectTab('response')}>\n          Response\n        </div>\n        <div className={getTabClassname('headers')} role=\"tab\" onClick={() => selectTab('headers')}>\n          Headers\n          {headers?.length > 0 && <sup className=\"ml-1 font-medium\">{headers.length}</sup>}\n        </div>\n        <div className={getTabClassname('timeline')} role=\"tab\" onClick={() => selectTab('timeline')}>\n          Timeline\n        </div>\n        <div className={getTabClassname('tests')} role=\"tab\" onClick={() => selectTab('tests')}>\n          <TestResultsLabel\n            results={testResults}\n            assertionResults={assertionResults}\n            preRequestTestResults={preRequestTestResults}\n            postResponseTestResults={postResponseTestResults}\n          />\n        </div>\n        <div className=\"flex flex-grow justify-end items-center\">\n          {hasScriptError && !showScriptErrorCard && (\n            <ScriptErrorIcon\n              itemUid={item.uid}\n              onClick={() => setShowScriptErrorCard(true)}\n            />\n          )}\n          <StatusCode status={status} />\n          <ResponseTime duration={duration} />\n          <ResponseSize size={size} />\n        </div>\n      </div>\n      <section className=\"flex flex-col pt-3 flex-grow overflow-auto\">\n        {hasScriptError && showScriptErrorCard && (\n          <ScriptError\n            item={item}\n            onClose={() => setShowScriptErrorCard(false)}\n          />\n        )}\n        <div className=\"flex-1\">\n          {getTabPanel(selectedTab)}\n        </div>\n      </section>\n    </StyledWrapper>\n  );\n};\n\nexport default ResponsePane;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RunnerResults/RunConfigurationPanel/StyledWrapper.jsx",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  background-color: ${(props) => props.theme.sidebar.bg};\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  overflow: hidden;\n\n  .header {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 1rem;\n    border-bottom: 1px solid ${(props) => props.theme.sidebar.dragbar};\n    margin-bottom: 0.5rem;\n\n    .counter {\n      font-size: ${(props) => props.theme.font.size.base};\n      font-weight: 500;\n    }\n\n    .actions {\n      display: flex;\n      align-items: center;\n    }\n  }\n\n  .request-list {\n    flex: 1;\n    overflow-y: auto;\n\n    &::-webkit-scrollbar {\n      width: 6px;\n    }\n\n    &::-webkit-scrollbar-track {\n      background: transparent;\n    }\n\n    &::-webkit-scrollbar-thumb {\n      background-color: ${(props) => props.theme.console.scrollbarThumb};\n      border-radius: 3px;\n    }\n\n    .loading-message,\n    .empty-message {\n      padding: 0.75rem;\n      color: ${(props) => props.theme.colors.text.muted};\n      font-size: ${(props) => props.theme.font.size.base};\n    }\n\n    .requests-container {\n      padding: 0.5rem;\n      position: relative;\n    }\n  }\n\n  .request-item {\n    display: flex;\n    align-items: center;\n    padding: 0.5rem;\n    border-radius: 4px;\n    margin-bottom: 0.25rem;\n    position: relative;\n    height: 2.5rem;\n    border: 1px solid transparent;\n    background-color: ${(props) => props.theme.sidebar.bg};\n    transition: transform 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;\n\n    &.is-selected {\n      background-color: ${(props) => props.theme.background.surface0};\n\n      .checkbox {\n        background-color: ${(props) => props.theme.primary.solid};\n        border-color: ${(props) => props.theme.primary.solid};\n      }\n\n      .checkbox-icon {\n        color: ${(props) => props.theme.bg};\n      }\n    }\n\n    &.is-dragging {\n      opacity: 0.5;\n      background-color: ${(props) => props.theme.sidebar.bg};\n      border: 1px dashed ${(props) => props.theme.sidebar.dragbar};\n      transform: scale(0.98);\n      box-shadow: ${(props) => props.theme.shadow.md};\n      z-index: 5;\n    }\n\n    &::before,\n    &::after {\n      content: '';\n      position: absolute;\n      left: 0;\n      right: 0;\n      height: 2px;\n      background: ${(props) => props.theme.dragAndDrop?.border || props.theme.textLink};\n      opacity: 0;\n      pointer-events: none;\n      transition: opacity 0.2s ease;\n    }\n\n    &::before {\n      top: -1px;\n    }\n\n    &::after {\n      bottom: -1px;\n    }\n\n    &.drop-target-above {\n      &::before {\n        opacity: 1;\n        height: 2px;\n        background: ${(props) => props.theme.dragAndDrop?.border || props.theme.textLink};\n      }\n    }\n\n    &.drop-target-below {\n      &::after {\n        opacity: 1;\n        height: 2px;\n        background: ${(props) => props.theme.dragAndDrop?.border || props.theme.textLink};\n      }\n    }\n\n    .drag-handle {\n      cursor: grab;\n      margin-right: 0.25rem;\n      color: ${(props) => props.theme.sidebar.muted};\n      display: flex;\n      align-items: center;\n      transition: color 0.15s ease;\n\n      &:hover {\n        color: ${(props) => props.theme.text};\n      }\n\n      &:active {\n        cursor: grabbing;\n        color: ${(props) => props.theme.textLink};\n      }\n    }\n\n    .checkbox-container {\n      cursor: pointer;\n      margin-right: 0.5rem;\n\n      .checkbox {\n        width: 1rem;\n        height: 1rem;\n        border: 1px solid ${(props) => props.theme.border.border2};\n        border-radius: 3px;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        transition: all 0.1s ease;\n\n        &:hover {\n          border-color: ${(props) => props.theme.primary.solid};\n        }\n      }\n    }\n\n    .method {\n      font-family: monospace;\n      font-size: ${(props) => props.theme.font.size.base};\n      font-weight: 500;\n      margin-right: 0.5rem;\n      min-width: 3rem;\n      color: ${(props) => props.theme.sidebar.muted}; // Default color for unknown methods\n\n      &.method-get {\n        color: ${(props) => props.theme.request.methods.get};\n      }\n\n      &.method-post {\n        color: ${(props) => props.theme.request.methods.post};\n      }\n\n      &.method-put {\n        color: ${(props) => props.theme.request.methods.put};\n      }\n\n      &.method-delete {\n        color: ${(props) => props.theme.request.methods.delete};\n      }\n\n      &.method-patch {\n        color: ${(props) => props.theme.request.methods.patch};\n      }\n\n      &.method-options {\n        color: ${(props) => props.theme.request.methods.options};\n      }\n\n      &.method-head {\n        color: ${(props) => props.theme.request.methods.head};\n      }\n\n      &.method-grpc {\n        color: ${(props) => props.theme.request.grpc};\n      }\n\n      &.method-ws {\n        color: ${(props) => props.theme.request.ws};\n      }\n\n      &.method-gql {\n        color: ${(props) => props.theme.request.gql};\n      }\n    }\n\n    .request-name {\n      flex: 1;\n      font-size: ${(props) => props.theme.font.size.base};\n      white-space: nowrap;\n      overflow: hidden;\n      text-overflow: ellipsis;\n\n      .folder-path {\n        margin-left: 0.5rem;\n        font-size: ${(props) => props.theme.font.size.base};\n        color: ${(props) => props.theme.sidebar.muted};\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RunnerResults/RunConfigurationPanel/index.jsx",
    "content": "import React, { useEffect, useState, useCallback, useRef } from 'react';\nimport { useDrag, useDrop } from 'react-dnd';\nimport { getEmptyImage } from 'react-dnd-html5-backend';\nimport { IconGripVertical, IconCheck, IconAdjustmentsAlt } from '@tabler/icons';\nimport { useDispatch } from 'react-redux';\nimport { updateRunnerConfiguration } from 'providers/ReduxStore/slices/collections/actions';\nimport StyledWrapper from './StyledWrapper';\nimport { isItemARequest } from 'utils/collections';\nimport path from 'utils/common/path';\nimport { cloneDeep, get } from 'lodash';\nimport Button from 'ui/Button/index';\n\nconst ItemTypes = {\n  REQUEST_ITEM: 'request-item'\n};\n\nconst getMethodInfo = (item) => {\n  const isGrpc = item.type === 'grpc-request';\n  const isWS = item.type === 'ws-request';\n  const isGraphQL = item.type === 'graphql-request';\n\n  let methodText;\n  let methodClass;\n\n  if (isGrpc) {\n    methodText = 'GRPC';\n    methodClass = 'method-grpc';\n  } else if (isWS) {\n    methodText = 'WS';\n    methodClass = 'method-ws';\n  } else if (isGraphQL) {\n    methodText = 'GQL';\n    methodClass = 'method-gql';\n  } else {\n    const method = item.request?.method || '';\n    methodText = method.length > 5 ? method.substring(0, 3).toUpperCase() : method.toUpperCase();\n    methodClass = `method-${method.toLowerCase()}`;\n  }\n\n  return { methodText, methodClass };\n};\n\nconst RequestItem = ({ item, index, moveItem, isSelected, onSelect, onDrop }) => {\n  const ref = useRef(null);\n  const [dropType, setDropType] = useState(null);\n\n  const determineDropType = (monitor) => {\n    const hoverBoundingRect = ref.current?.getBoundingClientRect();\n    const clientOffset = monitor.getClientOffset();\n    if (!hoverBoundingRect || !clientOffset) return null;\n\n    const clientY = clientOffset.y - hoverBoundingRect.top;\n    const middleY = hoverBoundingRect.height / 2;\n\n    return clientY < middleY ? 'above' : 'below';\n  };\n\n  const [{ isDragging }, drag, preview] = useDrag({\n    type: ItemTypes.REQUEST_ITEM,\n    item: { uid: item.uid, name: item.name, request: item.request, index },\n    collect: (monitor) => ({ isDragging: monitor.isDragging() }),\n    options: {\n      dropEffect: 'move'\n    },\n    end: (draggedItem, monitor) => {\n      if (monitor.didDrop()) {\n        onDrop();\n      }\n    }\n  });\n\n  const [{ isOver, canDrop }, drop] = useDrop({\n    accept: ItemTypes.REQUEST_ITEM,\n    hover: (draggedItem, monitor) => {\n      if (draggedItem.uid === item.uid) {\n        setDropType(null);\n        return;\n      }\n\n      const dropType = determineDropType(monitor);\n      setDropType(dropType);\n    },\n    drop: (draggedItem, monitor) => {\n      if (draggedItem.uid === item.uid) return;\n\n      const dropType = determineDropType(monitor);\n      let targetIndex = index;\n\n      if (dropType === 'below') {\n        targetIndex = index + 1;\n      }\n\n      if (draggedItem.index < targetIndex) {\n        targetIndex = targetIndex - 1;\n      }\n\n      moveItem(draggedItem.uid, targetIndex);\n      setDropType(null);\n      return { item: draggedItem };\n    },\n    collect: (monitor) => ({\n      isOver: monitor.isOver(),\n      canDrop: monitor.canDrop()\n    })\n  });\n\n  useEffect(() => {\n    preview(getEmptyImage(), { captureDraggingState: true });\n  }, []);\n\n  // Clear drop type when not hovering\n  useEffect(() => {\n    if (!isOver) {\n      setDropType(null);\n    }\n  }, [isOver]);\n\n  drag(drop(ref));\n\n  const itemClasses = [\n    'request-item',\n    isDragging ? 'is-dragging' : '',\n    isSelected ? 'is-selected' : '',\n    isOver && canDrop && dropType === 'above' ? 'drop-target-above' : '',\n    isOver && canDrop && dropType === 'below' ? 'drop-target-below' : ''\n  ].filter(Boolean).join(' ');\n\n  return (\n    <div ref={ref} className={itemClasses}>\n      <div className=\"drag-handle\">\n        <IconGripVertical size={16} strokeWidth={1.5} />\n      </div>\n\n      <div className=\"checkbox-container\" onClick={() => onSelect(item)}>\n        <div className=\"checkbox\">\n          {isSelected && <IconCheck className=\"checkbox-icon\" size={12} strokeWidth={3} />}\n        </div>\n      </div>\n\n      <div className={`method ${getMethodInfo(item).methodClass}`}>\n        {getMethodInfo(item).methodText}\n      </div>\n\n      <div className=\"request-name\">\n        <span>{item.name}</span>\n        {item.folderPath && (\n          <span className=\"folder-path\">{item.folderPath}</span>\n        )}\n      </div>\n    </div>\n  );\n};\n\nconst RunConfigurationPanel = ({ collection, selectedItems, setSelectedItems }) => {\n  const dispatch = useDispatch();\n  const [flattenedRequests, setFlattenedRequests] = useState([]);\n  const [originalRequests, setOriginalRequests] = useState([]);\n  const [isLoading, setIsLoading] = useState(true);\n\n  const flattenRequests = useCallback((collection) => {\n    const result = [];\n\n    const processItems = (items) => {\n      if (!items?.length) return;\n\n      items.forEach((item) => {\n        if (isItemARequest(item) && !item.partial && !item.isTransient) {\n          const relativePath = path.relative(collection.pathname, path.dirname(item.pathname));\n          const folderPath = relativePath !== '.' ? relativePath : '';\n\n          result.push({\n            ...item,\n            folderPath: folderPath.replace(/\\\\/g, '/')\n          });\n        }\n\n        if (item.items?.length) {\n          processItems(item.items);\n        }\n      });\n    };\n\n    processItems(collection.items);\n    return result;\n  }, []);\n\n  useEffect(() => {\n    setIsLoading(true);\n\n    try {\n      const structureCopy = cloneDeep(collection);\n      const requests = flattenRequests(structureCopy);\n\n      const savedConfiguration = get(collection, 'runnerConfiguration', null);\n      if (savedConfiguration?.requestItemsOrder?.length > 0) {\n        const orderedRequests = [];\n        const requestMap = new Map(requests.map((req) => [req.uid, req]));\n\n        savedConfiguration.requestItemsOrder.forEach((uid) => {\n          const request = requestMap.get(uid);\n          if (request) {\n            orderedRequests.push(request);\n            requestMap.delete(uid);\n          }\n        });\n\n        requestMap.forEach((request) => {\n          orderedRequests.push(request);\n        });\n\n        setFlattenedRequests(orderedRequests);\n      } else {\n        setFlattenedRequests(requests);\n      }\n\n      setOriginalRequests(cloneDeep(requests));\n    } catch (error) {\n      console.error('Error loading collection structure:', error);\n    } finally {\n      setIsLoading(false);\n    }\n  }, [collection, flattenRequests]);\n\n  const moveItem = useCallback((draggedItemUid, hoverIndex) => {\n    setFlattenedRequests((prevRequests) => {\n      const dragIndex = prevRequests.findIndex((item) => item.uid === draggedItemUid);\n\n      if (dragIndex === -1 || dragIndex === hoverIndex) {\n        return prevRequests;\n      }\n\n      const updatedRequests = [...prevRequests];\n      const [draggedItem] = updatedRequests.splice(dragIndex, 1);\n      updatedRequests.splice(hoverIndex, 0, draggedItem);\n\n      return updatedRequests;\n    });\n  }, []);\n\n  const handleDrop = useCallback(() => {\n    const selectedUids = new Set(selectedItems);\n\n    setFlattenedRequests((currentRequests) => {\n      const newOrderedSelectedUids = currentRequests\n        .filter((item) => selectedUids.has(item.uid))\n        .map((item) => item.uid);\n\n      const allRequestUidsOrder = currentRequests.map((item) => item.uid);\n\n      setSelectedItems(newOrderedSelectedUids);\n      dispatch(updateRunnerConfiguration(collection.uid, newOrderedSelectedUids, allRequestUidsOrder));\n\n      return currentRequests;\n    });\n  }, [selectedItems, collection.uid, dispatch, setSelectedItems]);\n\n  const handleRequestSelect = useCallback((item) => {\n    try {\n      if (selectedItems.includes(item.uid)) {\n        const newSelectedUids = selectedItems.filter((uid) => uid !== item.uid);\n        setSelectedItems(newSelectedUids);\n\n        const allRequestUidsOrder = flattenedRequests.map((item) => item.uid);\n        dispatch(updateRunnerConfiguration(collection.uid, newSelectedUids, allRequestUidsOrder));\n      } else {\n        const newSelectedUids = [...selectedItems, item.uid];\n\n        const orderedSelectedUids = flattenedRequests\n          .filter((req) => newSelectedUids.includes(req.uid))\n          .map((req) => req.uid);\n\n        setSelectedItems(orderedSelectedUids);\n\n        const allRequestUidsOrder = flattenedRequests.map((item) => item.uid);\n        dispatch(updateRunnerConfiguration(collection.uid, orderedSelectedUids, allRequestUidsOrder));\n      }\n    } catch (error) {\n      console.error('Error selecting item:', error);\n    }\n  }, [selectedItems, setSelectedItems, flattenedRequests, dispatch, collection.uid]);\n\n  const handleSelectAll = useCallback(() => {\n    try {\n      const allRequestUidsOrder = flattenedRequests.map((item) => item.uid);\n\n      if (selectedItems.length === flattenedRequests.length) {\n        setSelectedItems([]);\n        dispatch(updateRunnerConfiguration(collection.uid, [], allRequestUidsOrder));\n      } else {\n        setSelectedItems(allRequestUidsOrder);\n        dispatch(updateRunnerConfiguration(collection.uid, allRequestUidsOrder, allRequestUidsOrder));\n      }\n    } catch (error) {\n      console.error('Error selecting/deselecting all items:', error);\n    }\n  }, [flattenedRequests, selectedItems, setSelectedItems, dispatch, collection.uid]);\n\n  const handleReset = useCallback(() => {\n    try {\n      setFlattenedRequests(cloneDeep(originalRequests));\n      setSelectedItems([]);\n      dispatch(updateRunnerConfiguration(collection.uid, [], []));\n    } catch (error) {\n      console.error('Error resetting configuration:', error);\n    }\n  }, [originalRequests, setSelectedItems, collection.uid, dispatch]);\n\n  return (\n    <StyledWrapper>\n      <div className=\"header\">\n        <div className=\"counter\">\n          {selectedItems.length} of {flattenedRequests.length} selected\n        </div>\n        <div className=\"actions\">\n          <Button\n            variant=\"ghost\"\n            onClick={handleSelectAll}\n          >\n            {selectedItems.length === flattenedRequests.length ? 'Deselect All' : 'Select All'}\n          </Button>\n          <Button\n            variant=\"ghost\"\n            onClick={handleReset}\n            title=\"Reset selection and order\"\n          >\n            Reset\n          </Button>\n        </div>\n      </div>\n\n      <div className=\"request-list\">\n        {isLoading ? (\n          <div className=\"loading-message\">Loading requests...</div>\n        ) : flattenedRequests.length === 0 ? (\n          <div className=\"empty-message\">No requests found in this collection</div>\n        ) : (\n          <div className=\"requests-container\">\n            {flattenedRequests.map((item, idx) => {\n              const isSelected = selectedItems.includes(item.uid);\n\n              return (\n                <RequestItem\n                  key={item.uid}\n                  item={item}\n                  index={idx}\n                  isSelected={isSelected}\n                  onSelect={() => handleRequestSelect(item)}\n                  moveItem={moveItem}\n                  onDrop={handleDrop}\n                />\n              );\n            })}\n          </div>\n        )}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default RunConfigurationPanel;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RunnerResults/RunnerTags/index.jsx",
    "content": "import React, { useEffect } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { get, cloneDeep, find } from 'lodash';\nimport { updateCollectionTagsList, updateRunnerTagsDetails } from 'providers/ReduxStore/slices/collections';\nimport TagList from 'components/TagList';\n\nconst RunnerTags = ({ collectionUid, className = '' }) => {\n  const dispatch = useDispatch();\n  const collections = useSelector((state) => state.collections.collections);\n  const collection = cloneDeep(find(collections, (c) => c.uid === collectionUid));\n\n  // tags for the collection run\n  const tags = get(collection, 'runnerTags', { include: [], exclude: [] });\n\n  // have tags been enabled for the collection run\n  const tagsEnabled = get(collection, 'runnerTagsEnabled', false);\n\n  // all available tags in the collection that can be used for filtering\n  const availableTags = get(collection, 'allTags', []);\n  const tagsHintList = availableTags.filter((t) => !tags.exclude.includes(t) && !tags.include.includes(t));\n\n  useEffect(() => {\n    dispatch(updateCollectionTagsList({ collectionUid }));\n  }, [collection.uid, dispatch]);\n\n  const handleValidation = (tag) => {\n    const trimmedTag = tag.trim();\n    if (!availableTags.includes(trimmedTag)) {\n      return 'tag does not exist!';\n    }\n    if (tags.include.includes(trimmedTag)) {\n      return 'tag already present in the include list!';\n    }\n    if (tags.exclude.includes(trimmedTag)) {\n      return 'tag is present in the exclude list!';\n    }\n  };\n\n  const handleAddTag = ({ tag, to }) => {\n    const trimmedTag = tag.trim();\n    if (!trimmedTag) return;\n    // add tag to the `include` list\n    if (to === 'include') {\n      if (tags.include.includes(trimmedTag) || tags.exclude.includes(trimmedTag)) return;\n      if (!availableTags.includes(trimmedTag)) {\n        return;\n      }\n      const newTags = { ...tags, include: [...tags.include, trimmedTag].sort() };\n      setTags(newTags);\n      return;\n    }\n    // add tag to the `exclude` list\n    if (to === 'exclude') {\n      if (tags.include.includes(trimmedTag) || tags.exclude.includes(trimmedTag)) return;\n      if (!availableTags.includes(trimmedTag)) {\n        return;\n      }\n      const newTags = { ...tags, exclude: [...tags.exclude, trimmedTag].sort() };\n      setTags(newTags);\n    }\n  };\n\n  const handleRemoveTag = ({ tag, from }) => {\n    const trimmedTag = tag.trim();\n    if (!trimmedTag) return;\n    // remove tag from the `include` list\n    if (from === 'include') {\n      if (!tags.include.includes(trimmedTag)) return;\n      const newTags = { ...tags, include: tags.include.filter((t) => t !== trimmedTag) };\n      setTags(newTags);\n      return;\n    }\n    // remove tag from the `exclude` list\n    if (from === 'exclude') {\n      if (!tags.exclude.includes(trimmedTag)) return;\n      const newTags = { ...tags, exclude: tags.exclude.filter((t) => t !== trimmedTag) };\n      setTags(newTags);\n    }\n  };\n\n  const setTags = (tags) => {\n    dispatch(updateRunnerTagsDetails({ collectionUid: collection.uid, tags }));\n  };\n\n  const setTagsEnabled = (tagsEnabled) => {\n    dispatch(updateRunnerTagsDetails({ collectionUid: collection.uid, tagsEnabled }));\n  };\n\n  return (\n    <div className={`mt-6 flex flex-col ${className}`}>\n      <div className=\"flex gap-2\">\n        <input\n          className=\"cursor-pointer\"\n          id=\"filter-tags\"\n          type=\"radio\"\n          name=\"filterMode\"\n          checked={tagsEnabled}\n          onChange={() => setTagsEnabled(!tagsEnabled)}\n        />\n        <label htmlFor=\"filter-tags\" className=\"block font-medium\">Filter requests with tags</label>\n      </div>\n      {tagsEnabled && (\n        <div className=\"flex flex-row mt-4 gap-4 w-full\">\n          <div className=\"w-1/2 flex flex-col gap-2 max-w-[400px]\">\n            <span>Included tags:</span>\n            <TagList\n              tags={tags.include}\n              handleAddTag={(tag) => handleAddTag({ tag, to: 'include' })}\n              handleRemoveTag={(tag) => handleRemoveTag({ tag, from: 'include' })}\n              tagsHintList={tagsHintList}\n              handleValidation={handleValidation}\n            />\n          </div>\n          <div className=\"w-1/2 flex flex-col gap-2 max-w-[400px]\">\n            <span>Excluded tags:</span>\n            <TagList\n              tags={tags.exclude}\n              handleAddTag={(tag) => handleAddTag({ tag, to: 'exclude' })}\n              handleRemoveTag={(tag) => handleRemoveTag({ tag, from: 'exclude' })}\n              tagsHintList={tagsHintList}\n              handleValidation={handleValidation}\n            />\n          </div>\n        </div>\n      )}\n    </div>\n  );\n};\n\nexport default RunnerTags;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RunnerResults/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  .textbox {\n    padding: 0.2rem 0.5rem;\n    box-shadow: none;\n    border-radius: 0px;\n    outline: none;\n    box-shadow: none;\n    transition: border-color ease-in-out 0.1s;\n    border-radius: 3px;\n    background-color: ${(props) => props.theme.input.bg};\n    border: 1px solid ${(props) => props.theme.input.border};\n\n    &:focus {\n      border: solid 1px ${(props) => props.theme.input.focusBorder} !important;\n      outline: none !important;\n    }\n  }\n\n  .item-path {\n    .link {\n      color: ${(props) => props.theme.textLink};\n    }\n  }\n  .danger {\n    color: ${(props) => props.theme.colors.text.danger};\n  }\n\n  .test-summary {\n    color: ${(props) => props.theme.tabs.active.border};\n  }\n\n  /* test results */\n  .test-success {\n    color: ${(props) => props.theme.colors.text.green};\n  }\n\n  .test-failure {\n    color: ${(props) => props.theme.colors.text.danger};\n\n    .error-message {\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n  }\n  \n  .skipped-request {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .text-muted {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .text-green {\n    color: ${(props) => props.theme.colors.text.green};\n  }\n\n  .text-subtext0 {\n    color: ${(props) => props.theme.colors.text.subtext0};\n  }\n\n  .text-subtext1 {\n    color: ${(props) => props.theme.colors.text.subtext1};\n  }\n\n  .hover-bg-surface {\n    &:hover {\n      background-color: ${(props) => props.theme.background.surface1};\n    }\n  }\n\n  .button-sm {\n    font-size: ${(props) => props.theme.font.size.sm};\n  }\n\n  .run-config-panel, .run-config-option {\n    border-color: ${(props) => props.theme.background.surface1};\n  }\n\n  .filter-bar {\n    display: flex;\n    align-items: stretch;\n    border-radius: ${(props) => props.theme.border.radius.base};\n    border: 1px solid ${(props) => props.theme.border.border0};\n    max-height: 35px;\n    flex-shrink: 0;\n    overflow: hidden;\n\n    .filter-label {\n      display: flex;\n      align-items: center;\n      padding: 0.5rem 0.75rem;\n      border-top-left-radius: ${(props) => props.theme.border.radius.base};\n      border-bottom-left-radius: ${(props) => props.theme.border.radius.base};\n      background-color: ${(props) => props.theme.background.mantle};\n\n      span {\n        font-family: Inter, sans-serif;\n        font-weight: 400;\n        font-size: ${(props) => props.theme.font.size.sm};\n        color: ${(props) => props.theme.colors.text.text};\n      }\n    }\n\n    .filter-buttons {\n      display: flex;\n      align-items: center;\n      gap: 1.25rem;\n      padding: 0.5rem 0.75rem 0 0.75rem;\n      border-top-right-radius: ${(props) => props.theme.border.radius.base};\n      border-bottom-right-radius: ${(props) => props.theme.border.radius.base};\n      background: transparent;\n    }\n  }\n\n  .filter-button {\n    display: inline-flex;\n    align-items: center;\n    gap: 0.375rem;\n    padding: 0;\n    padding-bottom: 0.4rem;\n    border: none;\n    border-bottom: 2px solid transparent;\n    background: transparent;\n    font-family: Inter, sans-serif;\n    line-height: 100%;\n    letter-spacing: 0%;\n    cursor: pointer;\n    transition: color 0.15s ease, border-bottom-color 0.15s ease;\n    outline: none;\n\n    &:focus-visible {\n      outline: 2px solid ${(props) => props.theme.tabs.active.border};\n      outline-offset: 2px;\n    }\n\n    .filter-count {\n      padding: 2px 4.5px;\n      border-radius: 2px;\n      border: 1px solid ${(props) => props.theme.border.border0};\n      background-color: ${(props) => props.theme.background.surface0};\n      font-family: Inter, sans-serif;\n      font-size: ${(props) => props.theme.font.size.xs};\n      font-weight: 500;\n      line-height: 100%;\n      letter-spacing: 0%;\n    }\n\n    &.active {\n      font-weight: ${(props) => props.theme.tabs.active.fontWeight};\n      color: ${(props) => props.theme.tabs.active.color};\n      border-bottom-color: ${(props) => props.theme.tabs.active.border};\n\n      .filter-count {\n        color: ${(props) => props.theme.tabs.active.color};\n      }\n    }\n\n    &:not(.active) {\n      font-weight: 500;\n      color: ${(props) => props.theme.colors.text.subtext0};\n\n      .filter-count {\n        color: ${(props) => props.theme.colors.text.subtext0};\n      }\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/RunnerResults/index.jsx",
    "content": "import React, { useState, useRef, useEffect } from 'react';\nimport path from 'utils/common/path';\nimport { useDispatch } from 'react-redux';\nimport { get, cloneDeep } from 'lodash';\nimport { runCollectionFolder, cancelRunnerExecution, mountCollection, updateRunnerConfiguration } from 'providers/ReduxStore/slices/collections/actions';\nimport { resetCollectionRunner, updateRunnerTagsDetails } from 'providers/ReduxStore/slices/collections';\nimport { findItemInCollection, getTotalRequestCountInCollection, areItemsLoading, getRequestItemsForCollectionRun } from 'utils/collections';\nimport { IconRefresh, IconCircleCheck, IconCircleX, IconCircleOff, IconCheck, IconX, IconRun, IconExternalLink } from '@tabler/icons';\nimport ResponsePane from './ResponsePane';\nimport StyledWrapper from './StyledWrapper';\nimport RunnerTags from './RunnerTags/index';\nimport RunConfigurationPanel from './RunConfigurationPanel';\nimport Button from 'ui/Button/index';\n\nconst getDisplayName = (fullPath, pathname, name = '') => {\n  let relativePath = path.relative(fullPath, pathname);\n  const { dir = '' } = path.parse(relativePath);\n  return path.join(dir, name);\n};\n\nconst getTestStatus = (results) => {\n  if (!results || !results.length) return 'pass';\n  const failed = results.filter((result) => result.status === 'fail');\n  return failed.length ? 'fail' : 'pass';\n};\n\nconst allTestsPassed = (item) => {\n  return item.status !== 'error'\n    && item.testStatus === 'pass'\n    && item.assertionStatus === 'pass'\n    && item.preRequestTestStatus === 'pass'\n    && item.postResponseTestStatus === 'pass';\n};\n\nconst anyTestFailed = (item) => {\n  return item.status === 'error'\n    || item.testStatus === 'fail'\n    || item.assertionStatus === 'fail'\n    || item.preRequestTestStatus === 'fail'\n    || item.postResponseTestStatus === 'fail';\n};\n\n// === Centralized filters definition ===\nconst FILTERS = {\n  all: {\n    label: 'All',\n    predicate: () => true,\n    resultFilter: (results) => results\n  },\n  passed: {\n    label: 'Passed',\n    predicate: (item) => allTestsPassed(item),\n    resultFilter: (results) => results?.filter((r) => r.status === 'pass')\n  },\n  failed: {\n    label: 'Failed',\n    predicate: (item) => anyTestFailed(item),\n    resultFilter: (results) => results?.filter((r) => ['fail', 'error'].includes(r.status))\n  },\n  skipped: {\n    label: 'Skipped',\n    predicate: (item) => item.status === 'skipped',\n    resultFilter: (results) => results\n  }\n};\n\n// === Reusable filter button ===\nconst FilterButton = ({ label, count, active, onClick }) => (\n  <button\n    onClick={onClick}\n    className={`filter-button ${active ? 'active' : ''}`}\n  >\n    {label}\n    <span className=\"filter-count\">{count}</span>\n  </button>\n);\n\nexport default function RunnerResults({ collection }) {\n  const dispatch = useDispatch();\n  const [selectedItem, setSelectedItem] = useState(null);\n  const [delay, setDelay] = useState(null);\n  const [activeFilter, setActiveFilter] = useState('all');\n  const [selectedRequestItems, setSelectedRequestItems] = useState([]);\n  const [configureMode, setConfigureMode] = useState(false);\n  // ref for the runner output body\n  const runnerBodyRef = useRef();\n\n  const collectionCopy = cloneDeep(collection);\n  const runnerInfo = get(collection, 'runnerResult.info', {});\n\n  // tags for the collection run\n  const tags = get(collection, 'runnerTags', { include: [], exclude: [] });\n\n  // have tags been enabled for the collection run\n  const tagsEnabled = get(collection, 'runnerTagsEnabled', false);\n\n  // have tags been added for the collection run\n  const areTagsAdded = tags.include.length > 0 || tags.exclude.length > 0;\n\n  const requestItemsForCollectionRun = getRequestItemsForCollectionRun({ recursive: true, tags, items: collection.items });\n  const totalRequestItemsCountForCollectionRun = requestItemsForCollectionRun.length;\n  const shouldDisableCollectionRun = totalRequestItemsCountForCollectionRun <= 0;\n\n  const items = cloneDeep(get(collection, 'runnerResult.items', []))\n    .map((item) => {\n      const info = findItemInCollection(collectionCopy, item.uid);\n      if (!info) {\n        return null;\n      }\n      const newItem = {\n        ...item,\n        name: info.name,\n        type: info.type,\n        filename: info.filename,\n        pathname: info.pathname,\n        displayName: getDisplayName(collection.pathname, info.pathname, info.name),\n        tags: [...(info.request?.tags || [])].sort()\n      };\n      if (newItem.status !== 'error' && newItem.status !== 'skipped' && newItem.status !== 'running') {\n        newItem.testStatus = getTestStatus(newItem.testResults);\n        newItem.assertionStatus = getTestStatus(newItem.assertionResults);\n        newItem.preRequestTestStatus = getTestStatus(newItem.preRequestTestResults);\n        newItem.postResponseTestStatus = getTestStatus(newItem.postResponseTestResults);\n      }\n      return newItem;\n    })\n    .filter(Boolean);\n\n  const activeFilterConfig = FILTERS[activeFilter];\n  const filteredItems = items.filter(activeFilterConfig.predicate);\n\n  const filterTestResults = (results) => {\n    if (!results || !Array.isArray(results)) return [];\n    return activeFilterConfig.resultFilter(results);\n  };\n\n  const autoScrollRunnerBody = () => {\n    if (runnerBodyRef?.current) {\n      const element = runnerBodyRef.current;\n      const scrollThreshold = 100; // pixels from bottom to consider \"at bottom\"\n      const isNearBottom\n        = element.scrollHeight - element.scrollTop - element.clientHeight < scrollThreshold;\n\n      // Only auto-scroll if user is already near the bottom\n      if (isNearBottom) {\n        // mimics the native terminal scroll style\n        element.scrollTo(0, 100000);\n      }\n    }\n  };\n\n  useEffect(() => {\n    if (!collection.runnerResult) {\n      setSelectedItem(null);\n    }\n    autoScrollRunnerBody();\n  }, [collection, setSelectedItem]);\n\n  useEffect(() => {\n    // Auto-scroll when items are added or updated during execution\n    // Only scrolls if user is already at/near the bottom\n    if (filteredItems.length > 0) {\n      autoScrollRunnerBody();\n    }\n  }, [filteredItems]);\n\n  useEffect(() => {\n    const runnerInfo = get(collection, 'runnerResult.info', {});\n    if (runnerInfo.status === 'running') {\n      setConfigureMode(false);\n    }\n  }, [collection.runnerResult]);\n\n  useEffect(() => {\n    const savedConfiguration = get(collection, 'runnerConfiguration', null);\n    if (savedConfiguration) {\n      if (savedConfiguration.selectedRequestItems && configureMode) {\n        setSelectedRequestItems(savedConfiguration.selectedRequestItems);\n      }\n      if (savedConfiguration.delay !== undefined && delay === null) {\n        setDelay(savedConfiguration.delay);\n      }\n    }\n  }, [collection.runnerConfiguration, configureMode, delay]);\n\n  const ensureCollectionIsMounted = () => {\n    if (collection.mountStatus === 'mounted') {\n      return;\n    }\n    dispatch(mountCollection({\n      collectionUid: collection.uid,\n      collectionPathname: collection.pathname,\n      brunoConfig: collection.brunoConfig\n    }));\n  };\n\n  const runCollection = () => {\n    if (configureMode && selectedRequestItems.length > 0) {\n      dispatch(updateRunnerConfiguration(collection.uid, selectedRequestItems, selectedRequestItems, delay));\n      dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags, selectedRequestItems));\n    } else {\n      dispatch(updateRunnerConfiguration(collection.uid, [], [], delay));\n      dispatch(runCollectionFolder(collection.uid, null, true, Number(delay), tagsEnabled && tags));\n    }\n  };\n\n  const runAgain = () => {\n    ensureCollectionIsMounted();\n    // Get the saved configuration to determine what to run\n    const savedConfiguration = get(collection, 'runnerConfiguration', null);\n    const savedSelectedItems = savedConfiguration?.selectedRequestItems || [];\n    const savedDelay = savedConfiguration?.delay !== undefined ? savedConfiguration.delay : delay;\n    dispatch(\n      runCollectionFolder(\n        collection.uid,\n        runnerInfo.folderUid,\n        true,\n        Number(savedDelay),\n        tagsEnabled && tags,\n        savedSelectedItems\n      )\n    );\n  };\n\n  const resetRunner = () => {\n    dispatch(\n      resetCollectionRunner({\n        collectionUid: collection.uid\n      })\n    );\n    setSelectedRequestItems([]);\n    setConfigureMode(false);\n    setDelay(null);\n  };\n\n  const cancelExecution = () => {\n    dispatch(cancelRunnerExecution(runnerInfo.cancelTokenUid));\n  };\n\n  const toggleConfigureMode = () => {\n    dispatch(updateRunnerTagsDetails({ collectionUid: collection.uid, tagsEnabled: false }));\n    setConfigureMode(!configureMode);\n  };\n\n  useEffect(() => {\n    if (tagsEnabled) {\n      setConfigureMode(false);\n    }\n  }, [tagsEnabled]);\n\n  const totalRequestsInCollection = getTotalRequestCountInCollection(collectionCopy);\n  const filterCounts = {\n    all: items.length,\n    passed: items.filter(allTestsPassed).length,\n    failed: items.filter(anyTestFailed).length,\n    skipped: items.filter((i) => i.status === 'skipped').length\n  };\n\n  let isCollectionLoading = areItemsLoading(collection);\n  if (!items || !items.length) {\n    return (\n      <StyledWrapper className=\"pl-4 overflow-hidden h-full\">\n        <div className=\"flex overflow-hidden max-h-full h-full\">\n          <div className={`${configureMode ? 'w-1/2 pr-4' : 'w-full'}`}>\n            <div className=\"font-medium mt-6 title flex items-center\">\n              Runner\n              <IconRun size={20} strokeWidth={1.5} className=\"ml-2\" />\n            </div>\n            <div className=\"mt-6\">\n              You have <span className=\"font-medium\">{totalRequestsInCollection}</span> requests in this collection.\n              {isCollectionLoading && (\n                <span className=\"ml-2 text-muted\">\n                  (Loading...)\n                </span>\n              )}\n            </div>\n            {isCollectionLoading ? <div className=\"my-1 danger\">Requests in this collection are still loading.</div> : null}\n            <div className=\"mt-6\">\n              <label>Delay (in ms)</label>\n              <input\n                type=\"number\"\n                className=\"block textbox mt-2 py-5\"\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n                value={delay}\n                onChange={(e) => setDelay(e.target.value)}\n              />\n            </div>\n\n            {/* Tags for the collection run */}\n            <RunnerTags collectionUid={collection.uid} className=\"mb-6\" />\n\n            {/* Configure requests option */}\n            <div className=\"run-config-option flex flex-col border-b pb-6 mb-6\">\n              <div className=\"flex gap-2\">\n                <input\n                  className=\"cursor-pointer\"\n                  id=\"filter-config\"\n                  type=\"radio\"\n                  name=\"filterMode\"\n                  checked={configureMode}\n                  onChange={toggleConfigureMode}\n                />\n                <label htmlFor=\"filter-config\" className=\"block font-medium\">Configure requests to run</label>\n              </div>\n            </div>\n\n            <div className=\"flex flex-row gap-2\">\n              <Button\n                type=\"submit\"\n                disabled={shouldDisableCollectionRun || (configureMode && selectedRequestItems.length === 0) || isCollectionLoading}\n                onClick={runCollection}\n              >\n                {configureMode && selectedRequestItems.length > 0\n                  ? `Run ${selectedRequestItems.length} Selected Request${selectedRequestItems.length > 1 ? 's' : ''}`\n                  : 'Run Collection'}\n              </Button>\n\n              <Button type=\"button\" variant=\"ghost\" onClick={resetRunner}>\n                Reset\n              </Button>\n            </div>\n          </div>\n\n          {configureMode && (\n            <div className=\"run-config-panel w-1/2 border-l\">\n              <RunConfigurationPanel\n                collection={collection}\n                selectedItems={selectedRequestItems}\n                setSelectedItems={setSelectedRequestItems}\n              />\n            </div>\n          )}\n        </div>\n      </StyledWrapper>\n    );\n  }\n\n  return (\n    <StyledWrapper className=\"px-4 pb-4 flex flex-grow flex-col relative overflow-auto\">\n      {/* Filter Bar and Actions */}\n      <div className=\"flex items-center justify-between mb-4 pt-[14px] gap-4\">\n        <div className=\"filter-bar\">\n          <div className=\"filter-label\">\n            <span>Filter by:</span>\n          </div>\n          <div className=\"filter-buttons\">\n            {Object.entries(FILTERS).map(([key, { label }]) => (\n              <FilterButton\n                key={key}\n                label={label}\n                count={filterCounts[key]}\n                active={activeFilter === key}\n                onClick={() => setActiveFilter(key)}\n              />\n            ))}\n          </div>\n        </div>\n\n        {runnerInfo.status !== 'ended' && runnerInfo.cancelTokenUid ? (\n          <div className=\"flex items-center flex-shrink-0\">\n            <Button\n              type=\"button\"\n              onClick={cancelExecution}\n              size=\"sm\"\n              variant=\"filled\"\n              color=\"danger\"\n            >\n              Cancel Execution\n            </Button>\n          </div>\n        ) : runnerInfo.status === 'ended' ? (\n          <div className=\"flex items-center gap-3 flex-shrink-0\">\n            <Button\n              type=\"button\"\n              onClick={runAgain}\n              size=\"sm\"\n              variant=\"filled\"\n              color=\"secondary\"\n            >\n              Run Again\n            </Button>\n            <Button\n              type=\"button\"\n              onClick={resetRunner}\n              size=\"sm\"\n              variant=\"filled\"\n              color=\"secondary\"\n            >\n              Reset\n            </Button>\n          </div>\n        ) : null}\n      </div>\n\n      <div className=\"flex gap-4 h-[calc(100vh_-_10rem)] overflow-hidden\">\n        <div\n          className=\"flex flex-col w-1/2\"\n        >\n          {tagsEnabled && areTagsAdded && (\n            <div className=\"pb-2 text-xs flex flex-row gap-1\">\n              Tags:\n              <div className=\"flex flex-row items-center gap-x-2\">\n                <div className=\"text-green\">\n                  {tags.include.join(', ')}\n                </div>\n                <div className=\"text-muted\">\n                  {tags.exclude.join(', ')}\n                </div>\n              </div>\n            </div>\n          )}\n          {runnerInfo?.statusText\n            ? (\n                <div className=\"pb-2 font-medium danger\">\n                  {runnerInfo?.statusText}\n                </div>\n              )\n            : null}\n\n          {/* Items list */}\n          <div className=\"overflow-y-auto flex-1 \" ref={runnerBodyRef}>\n            {filteredItems.map((item) => {\n              return (\n                <div key={item.uid}>\n                  <div className=\"item-path mt-2\">\n                    <div className=\"flex items-center\">\n                      <span>\n                        {allTestsPassed(item)\n                          ? <IconCircleCheck className=\"test-success\" size={20} strokeWidth={1.5} />\n                          : null}\n                        {item.status === 'skipped'\n                          ? <IconCircleOff className=\"skipped-request\" size={20} strokeWidth={1.5} />\n                          : null}\n                        {anyTestFailed(item)\n                          ? <IconCircleX className=\"test-failure\" size={20} strokeWidth={1.5} />\n                          : null}\n                      </span>\n                      <span\n                        className={`mr-1 ml-2 ${item.status == 'skipped' ? 'skipped-request' : anyTestFailed(item) ? 'danger' : ''}`}\n                      >\n                        {item.displayName}\n                      </span>\n                      {item.status !== 'error' && item.status !== 'skipped' && item.status !== 'completed' ? (\n                        <IconRefresh className=\"animate-spin ml-1\" size={18} strokeWidth={1.5} />\n                      ) : item.responseReceived?.status ? (\n                        <span className=\"text-xs link cursor-pointer\" onClick={() => setSelectedItem(item)}>\n                          <span className=\"mr-1\">{item.responseReceived?.status}</span>\n                          -&nbsp;\n                          <span>{item.responseReceived?.statusText}</span>\n                        </span>\n                      ) : (\n                        <span className=\"danger text-xs cursor-pointer\" onClick={() => setSelectedItem(item)}>\n                          (request failed)\n                        </span>\n                      )}\n                    </div>\n                    {tagsEnabled && areTagsAdded && item?.tags?.length > 0 && (\n                      <div className=\"pl-7 text-xs text-muted\">\n                        Tags: {item.tags.filter((t) => tags.include.includes(t)).join(', ')}\n                      </div>\n                    )}\n                    {item.status == 'error' ? <div className=\"error-message pl-8 pt-2 text-xs\">{item.error}</div> : null}\n\n                    <ul className=\"pl-8\">\n                      {item.preRequestTestResults\n                        ? filterTestResults(item.preRequestTestResults).map((result) => (\n                            <li key={result.uid}>\n                              {result.status === 'pass' ? (\n                                <span className=\"test-success flex items-center\">\n                                  <IconCheck size={18} strokeWidth={2} className=\"mr-2\" />\n                                  {result.description}\n                                </span>\n                              ) : (\n                                <>\n                                  <span className=\"test-failure flex items-center\">\n                                    <IconX size={18} strokeWidth={2} className=\"mr-2\" />\n                                    {result.description}\n                                  </span>\n                                  <span className=\"error-message pl-8 text-xs\">{result.error}</span>\n                                </>\n                              )}\n                            </li>\n                          ))\n                        : null}\n                      {item.postResponseTestResults\n                        ? filterTestResults(item.postResponseTestResults).map((result) => (\n                            <li key={result.uid}>\n                              {result.status === 'pass' ? (\n                                <span className=\"test-success flex items-center\">\n                                  <IconCheck size={18} strokeWidth={2} className=\"mr-2\" />\n                                  {result.description}\n                                </span>\n                              ) : (\n                                <>\n                                  <span className=\"test-failure flex items-center\">\n                                    <IconX size={18} strokeWidth={2} className=\"mr-2\" />\n                                    {result.description}\n                                  </span>\n                                  <span className=\"error-message pl-8 text-xs\">{result.error}</span>\n                                </>\n                              )}\n                            </li>\n                          ))\n                        : null}\n                      {item.testResults\n                        ? filterTestResults(item.testResults).map((result) => (\n                            <li key={result.uid}>\n                              {result.status === 'pass' ? (\n                                <span className=\"test-success flex items-center\">\n                                  <IconCheck size={18} strokeWidth={2} className=\"mr-2\" />\n                                  {result.description}\n                                </span>\n                              ) : (\n                                <>\n                                  <span className=\"test-failure flex items-center\">\n                                    <IconX size={18} strokeWidth={2} className=\"mr-2\" />\n                                    {result.description}\n                                  </span>\n                                  <span className=\"error-message pl-8 text-xs\">{result.error}</span>\n                                </>\n                              )}\n                            </li>\n                          ))\n                        : null}\n                      {filterTestResults(item.assertionResults).map((result) => (\n                        <li key={result.uid}>\n                          {result.status === 'pass' ? (\n                            <span className=\"test-success flex items-center\">\n                              <IconCheck size={18} strokeWidth={2} className=\"mr-2\" />\n                              {result.lhsExpr}: {result.rhsExpr}\n                            </span>\n                          ) : (\n                            <>\n                              <span className=\"test-failure flex items-center\">\n                                <IconX size={18} strokeWidth={2} className=\"mr-2\" />\n                                {result.lhsExpr}: {result.rhsExpr}\n                              </span>\n                              <span className=\"error-message pl-8 text-xs\">{result.error}</span>\n                            </>\n                          )}\n                        </li>\n                      ))}\n                    </ul>\n                  </div>\n                </div>\n              );\n            })}\n          </div>\n        </div>\n\n        {selectedItem ? (\n          <div className=\"flex flex-1 w-[50%] overflow-y-auto\">\n            <div className=\"flex flex-col w-full overflow-hidden\">\n              <div className=\"flex items-center justify-between mb-4 font-medium\">\n                <div className=\"flex items-center\">\n                  <span className=\"mr-2\">{selectedItem.displayName}</span>\n                  <span>\n                    {allTestsPassed(selectedItem)\n                      ? <IconCircleCheck className=\"test-success\" size={20} strokeWidth={1.5} />\n                      : null}\n                    {anyTestFailed(selectedItem)\n                      ? <IconCircleX className=\"test-failure\" size={20} strokeWidth={1.5} />\n                      : null}\n                    {selectedItem.status === 'skipped'\n                      ? <IconCircleOff className=\"skipped-request\" size={20} strokeWidth={1.5} />\n                      : null}\n                  </span>\n                </div>\n                <button\n                  onClick={() => setSelectedItem(null)}\n                  className=\"p-1 rounded hover-bg-surface transition-colors cursor-pointer flex items-center justify-center\"\n                  title=\"Close\"\n                  aria-label=\"Close response view\"\n                >\n                  <IconX size={16} strokeWidth={1.5} />\n                </button>\n              </div>\n              <ResponsePane item={selectedItem} collection={collection} />\n            </div>\n          </div>\n        ) : (\n          <div className=\"flex flex-1 w-[50%] overflow-y-auto\">\n            <div className=\"flex flex-col w-full h-full items-center justify-center text-center\">\n              <div className=\"mb-4 text-subtext0\">\n                <IconExternalLink size={64} strokeWidth={1.5} />\n              </div>\n              <p className=\"text-subtext1\">\n                Click on the status code to view the response\n              </p>\n            </div>\n          </div>\n        )}\n      </div>\n    </StyledWrapper>\n  );\n}\n"
  },
  {
    "path": "packages/bruno-app/src/components/SaveTransientRequest/CollectionListItem/index.js",
    "content": "import React, { useMemo, useCallback, memo } from 'react';\nimport { useSelector } from 'react-redux';\nimport { IconDatabase, IconLoader2 } from '@tabler/icons';\nimport { areItemsLoading } from 'utils/collections';\n\nconst CollectionListItem = memo(({ collectionUid, collectionPath, collectionName, isSelected, onSelect }) => {\n  const collection = useSelector((state) =>\n    state.collections.collections.find((c) => c.uid === collectionUid || c.pathname === collectionPath)\n  );\n\n  const isLoading = useMemo(() => {\n    const isMounted = collection?.mountStatus === 'mounted';\n    const fullyLoaded = isMounted && !areItemsLoading(collection);\n    return isSelected && !fullyLoaded;\n  }, [collection, isSelected]);\n\n  const handleClick = useCallback(() => {\n    if (!isLoading) {\n      onSelect();\n    }\n  }, [isLoading, onSelect]);\n\n  return (\n    <li\n      className={`collection-item ${isLoading ? 'mounting' : ''} ${isSelected ? 'selected' : ''}`}\n      onClick={handleClick}\n    >\n      <div className=\"collection-item-content\">\n        <IconDatabase size={16} strokeWidth={1.5} />\n        <span className=\"collection-item-name\">{collectionName}</span>\n      </div>\n      {isLoading && (\n        <IconLoader2 size={16} strokeWidth={1.5} className=\"animate-spin\" />\n      )}\n    </li>\n  );\n});\n\nexport default CollectionListItem;\n"
  },
  {
    "path": "packages/bruno-app/src/components/SaveTransientRequest/Container.js",
    "content": "import React, { useState, useEffect } from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { pluralizeWord } from 'utils/common';\nimport { IconAlertTriangle, IconDeviceFloppy } from '@tabler/icons';\nimport { clearAllSaveTransientRequestModals } from 'providers/ReduxStore/slices/collections';\nimport { closeTabs } from 'providers/ReduxStore/slices/collections/actions';\nimport toast from 'react-hot-toast';\nimport Modal from 'components/Modal';\nimport Button from 'ui/Button';\nimport SaveTransientRequest from './index';\n\nconst SaveTransientRequestContainer = () => {\n  const dispatch = useDispatch();\n  const modals = useSelector((state) => state.collections.saveTransientRequestModals);\n  const [openItemUid, setOpenItemUid] = useState(null);\n\n  // Reset openItemUid if the modal no longer exists in the array\n  useEffect(() => {\n    if (openItemUid && !modals.find((modal) => modal.item.uid === openItemUid)) {\n      setOpenItemUid(null);\n    }\n  }, [modals, openItemUid]);\n\n  const handleDiscardAll = () => {\n    // Close all tabs for the transient requests (this will also delete the transient files)\n    const tabUids = modals.map((modal) => modal.item.uid);\n    dispatch(closeTabs({ tabUids }));\n\n    // Clear all modals\n    dispatch(clearAllSaveTransientRequestModals());\n\n    // Show success message\n    toast.success(`Discarded ${modals.length} ${pluralizeWord('request', modals.length)}`);\n  };\n\n  const handleCancel = () => {\n    // Clear all modals on close\n    dispatch(clearAllSaveTransientRequestModals());\n  };\n\n  const handleOpenSpecificModal = (itemUid) => {\n    setOpenItemUid(itemUid);\n  };\n\n  // If a specific modal is open, show it\n  if (openItemUid) {\n    const modalToOpen = modals.find((modal) => modal.item.uid === openItemUid);\n    if (modalToOpen) {\n      return (\n        <SaveTransientRequest\n          item={modalToOpen.item}\n          collection={modalToOpen.collection}\n          isOpen={true}\n        />\n      );\n    }\n  }\n\n  // Show list of multiple modals\n  return (\n    <Modal\n      size=\"md\"\n      title=\"Unsaved Transient Requests\"\n      hideFooter={true}\n      disableEscapeKey={true}\n      disableCloseOnOutsideClick={true}\n      handleCancel={handleCancel}\n    >\n      <div className=\"flex items-center\">\n        <IconAlertTriangle size={32} strokeWidth={1.5} className=\"text-yellow-600\" />\n        <h1 className=\"ml-2 text-lg font-medium\">You have unsaved transient requests</h1>\n      </div>\n      <p className=\"mt-4\">\n        You have <span className=\"font-medium\">{modals.length}</span>{' '}\n        {pluralizeWord('request', modals.length)} that need to be saved.\n      </p>\n\n      <div className=\"mt-4\">\n        <p className=\"text-sm font-medium mb-2\">\n          Transient {pluralizeWord('Request', modals.length)} ({modals.length})\n        </p>\n        <p className=\"text-xs text-orange-600 mb-3\">\n          These requests need to be saved before you can proceed.\n        </p>\n        <div className=\"space-y-2 max-h-96 overflow-y-auto pr-1\">\n          {modals.map((modal) => {\n            const { item, collection } = modal;\n            return (\n              <div\n                key={item.uid}\n                className=\"flex items-center justify-between py-2 px-3 bg-gray-50 rounded border border-gray-200\"\n              >\n                <div className=\"flex flex-col flex-1 min-w-0 mr-3\">\n                  <span className=\"text-sm text-gray-700 truncate\">{item.name}</span>\n                  <span className=\"text-xs text-gray-500 truncate\">\n                    {collection.name}\n                  </span>\n                </div>\n                <Button\n                  color=\"primary\"\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  onClick={() => handleOpenSpecificModal(item.uid)}\n                  icon={<IconDeviceFloppy size={14} strokeWidth={1.5} />}\n                >\n                  Save\n                </Button>\n              </div>\n            );\n          })}\n        </div>\n      </div>\n\n      <div className=\"flex justify-end mt-6 pt-4 border-t\">\n        <Button color=\"danger\" onClick={handleDiscardAll}>\n          Discard All\n        </Button>\n      </div>\n    </Modal>\n  );\n};\n\nexport default SaveTransientRequestContainer;\n"
  },
  {
    "path": "packages/bruno-app/src/components/SaveTransientRequest/FolderBreadcrumbs/index.js",
    "content": "import React from 'react';\nimport { IconChevronRight } from '@tabler/icons';\n\nconst FolderBreadcrumbs = ({\n  collectionName,\n  breadcrumbs,\n  isAtRoot,\n  onNavigateToRoot,\n  onNavigateToBreadcrumb\n}) => {\n  return (\n    <>\n      <span\n        className={!isAtRoot ? 'collection-name-breadcrumb' : ''}\n        onClick={!isAtRoot ? onNavigateToRoot : undefined}\n      >\n        {collectionName}\n      </span>\n      {breadcrumbs.map((breadcrumb, index) => (\n        <React.Fragment key={breadcrumb.uid}>\n          <IconChevronRight size={16} strokeWidth={1.5} className=\"collection-name-chevron\" />\n          <span\n            className=\"collection-name-breadcrumb\"\n            onClick={(e) => {\n              e.stopPropagation();\n              onNavigateToBreadcrumb(index);\n            }}\n          >\n            {breadcrumb.name}\n          </span>\n        </React.Fragment>\n      ))}\n      {isAtRoot && <IconChevronRight size={16} strokeWidth={1.5} className=\"collection-name-chevron\" />}\n    </>\n  );\n};\n\nexport default FolderBreadcrumbs;\n"
  },
  {
    "path": "packages/bruno-app/src/components/SaveTransientRequest/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .save-request-form {\n    display: flex;\n    flex-direction: column;\n    gap: 24px;\n  }\n\n  .form-section {\n    display: flex;\n    flex-direction: column;\n  }\n\n  .form-label {\n    display: block;\n    font-weight: 500;\n    margin-bottom: 8px;\n    color: ${(props) => props.theme.text};\n  }\n\n  .form-input {\n    display: block;\n    width: 100%;\n    line-height: 1.42857143;\n    padding: 0.45rem;\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    background-color: ${(props) => props.theme.input.bg};\n    border: 1px solid ${(props) => props.theme.input.border};\n    color: ${(props) => props.theme.text};\n    transition: border-color ease-in-out 0.1s;\n\n    &:focus {\n      border: solid 1px ${(props) => props.theme.input.focusBorder} !important;\n      outline: none !important;\n    }\n  }\n\n  .collections-section {\n    display: flex;\n    flex-direction: column;\n  }\n\n  .collections-label {\n    display: block;\n    font-weight: 500;\n    margin-bottom: 8px;\n    color: ${(props) => props.theme.text};\n  }\n\n  .collection-name {\n    display: flex;\n    align-items: center;\n    font-size: 14px;\n    margin-bottom: 12px;\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .collection-name-clickable {\n    cursor: pointer;\n  }\n\n  .collection-name-breadcrumb {\n    cursor: pointer;\n  }\n\n  .collection-name-chevron {\n    margin: 0 4px;\n  }\n\n  .search-container {\n    margin-bottom: 12px;\n  }\n\n  .folder-list {\n    border: 1px solid ${(props) => props.theme.border.border1};\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    max-height: 256px;\n    overflow-y: auto;\n    background-color: ${(props) => props.theme.modal.body.bg};\n    padding: 8px 8px;\n  }\n\n  .folder-list-items {\n    display: flex;\n    flex-direction: column;\n    gap: 4px;\n    list-style: none;\n    padding: 0;\n    margin: 0;\n    border-radius: ${(props) => props.theme.border.radius.sm};\n  }\n\n  .folder-item {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 10px 12px;\n    cursor: pointer;\n    transition: background-color 0.15s ease;\n    color: ${(props) => props.theme.text};\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    user-select: none;\n    &:hover {\n      background-color: ${(props) => props.theme.plainGrid.hoverBg};\n    }\n\n    &.selected {\n      background-color: ${(props) => props.theme.plainGrid.hoverBg};\n    }\n  }\n\n  .folder-item-content {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n  }\n\n  .folder-item-name {\n    color: ${(props) => props.theme.text};\n  }\n\n  .folder-empty-state {\n    padding: 16px 12px;\n    text-align: center;\n    font-size: 14px;\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .collection-list {\n    border: 1px solid ${(props) => props.theme.border.border1};\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    max-height: 320px;\n    overflow-y: auto;\n    background-color: ${(props) => props.theme.modal.body.bg};\n    padding: 8px 8px;\n  }\n\n  .collection-list-items {\n    display: flex;\n    flex-direction: column;\n    gap: 4px;\n    list-style: none;\n    padding: 0;\n    margin: 0;\n    border-radius: ${(props) => props.theme.border.radius.sm};\n  }\n\n  .collection-item {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 12px 14px;\n    cursor: pointer;\n    transition: background-color 0.15s ease;\n    color: ${(props) => props.theme.text};\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    user-select: none;\n    border: 1px solid ${(props) => props.theme.border.border1};\n\n    &:hover {\n      background-color: ${(props) => props.theme.plainGrid.hoverBg};\n      border-color: ${(props) => props.theme.colors.text.muted};\n    }\n  }\n\n  .collection-item-content {\n    display: flex;\n    align-items: center;\n    gap: 10px;\n  }\n\n  .collection-item-name {\n    color: ${(props) => props.theme.text};\n    font-weight: 500;\n  }\n\n  .collection-empty-state {\n    padding: 20px 16px;\n    text-align: center;\n    font-size: 14px;\n    color: ${(props) => props.theme.colors.text.muted};\n    line-height: 1.5;\n  }\n\n  .animate-spin {\n    animation: spin 1s linear infinite;\n  }\n\n  @keyframes spin {\n    from {\n      transform: rotate(0deg);\n    }\n    to {\n      transform: rotate(360deg);\n    }\n  }\n\n  .icon-success {\n    color: ${(props) => props.theme.colors.success};\n  }\n\n  .custom-modal-footer {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: 16px 0px 0px 0px;\n    background-color: ${(props) => props.theme.modal.body.bg};\n    border-top: 1px solid ${(props) => props.theme.border.border0};\n    border-bottom-left-radius: ${(props) => props.theme.border.radius.base};\n    border-bottom-right-radius: ${(props) => props.theme.border.radius.base};\n  }\n\n  .footer-left {\n    display: flex;\n    align-items: center;\n  }\n\n  .footer-right {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n  }\n\n  .text-muted {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .new-folder-item {\n    display: flex;\n    flex-direction: column;\n    gap: 8px;\n    padding: 10px 12px;\n    border-top: 1px solid ${(props) => props.theme.border.border1};\n    margin-top: 4px;\n    padding-top: 12px;\n  }\n\n  .new-folder-header {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    margin-bottom: 4px;\n  }\n\n  .new-folder-header-label {\n    font-size: 13px;\n    font-weight: 500;\n    color: ${(props) => props.theme.text};\n  }\n\n  .new-folder-input-row {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n  }\n\n  .new-folder-input {\n    flex: 1;\n    padding: 6px 8px;\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    background-color: ${(props) => props.theme.input.bg};\n    border: 1px solid ${(props) => props.theme.input.border};\n    color: ${(props) => props.theme.text};\n    font-size: 14px;\n    transition: border-color ease-in-out 0.1s;\n\n    &:focus {\n      border: solid 1px ${(props) => props.theme.input.focusBorder} !important;\n      outline: none !important;\n    }\n\n    &::placeholder {\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n  }\n\n  .new-folder-actions {\n    display: flex;\n    align-items: center;\n    gap: 4px;\n  }\n\n  .new-folder-action-btn {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    padding: 4px;\n    border: none;\n    background: transparent;\n    color: ${(props) => props.theme.colors.text.muted};\n    cursor: pointer;\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    transition: all 0.15s ease;\n\n    &:hover {\n      background-color: ${(props) => props.theme.plainGrid.hoverBg};\n      color: ${(props) => props.theme.text};\n    }\n\n    &:active {\n      opacity: 0.7;\n    }\n  }\n\n  .new-folder-filesystem-wrapper {\n    display: flex;\n    flex-direction: column;\n    gap: 6px;\n  }\n\n  .new-folder-filesystem-label {\n    font-size: 13px;\n    font-weight: 500;\n    color: ${(props) => props.theme.text};\n  }\n\n  .filesystem-input-container {\n    display: flex;\n    align-items: center;\n    background: ${(props) => props.theme.requestTabPanel.url.bg};\n    border-radius: 4px;\n    padding: 8px 12px;\n    border: 1px solid rgba(0, 0, 0, 0.08);\n    margin-top: 8px;\n  }\n\n  .filesystem-input-icon {\n    flex-shrink: 0;\n    margin-right: 8px;\n    color: ${(props) => props.theme.colors.text.yellow};\n  }\n\n  .filesystem-input {\n    flex: 1;\n    background: transparent;\n    border: none;\n    outline: none;\n    color: ${(props) => props.theme.colors.text.yellow};\n    font-size: ${(props) => props.theme.font.size.base};\n\n    &::placeholder {\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n  }\n\n  .new-folder-toggle-filesystem-btn {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    padding: 6px 8px;\n    margin-top: 4px;\n    border: none;\n    background: transparent;\n    color: ${(props) => props.theme.colors.text.muted};\n    cursor: pointer;\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    font-size: 12px;\n    transition: all 0.15s ease;\n    align-self: flex-start;\n\n    &:hover {\n      background-color: ${(props) => props.theme.plainGrid.hoverBg};\n      color: ${(props) => props.theme.text};\n    }\n  }\n\n  .new-folder-error {\n    color: ${(props) => props.theme.colors.danger};\n    font-size: 12px;\n    margin-top: 4px;\n  }\n\n  /* New Collection Input Styles */\n  .new-collection-item {\n    display: flex;\n    flex-direction: column;\n    gap: 12px;\n    padding: 12px;\n    border-top: 1px solid ${(props) => props.theme.border.border1};\n    margin-top: 4px;\n\n    &:first-child {\n      border-top: none;\n      margin-top: 0;\n    }\n  }\n\n  .new-collection-field {\n    display: flex;\n    flex-direction: column;\n    gap: 6px;\n  }\n\n  .new-collection-label {\n    font-size: 13px;\n    font-weight: 500;\n    color: ${(props) => props.theme.text};\n  }\n\n  .new-collection-input {\n    width: 100%;\n    padding: 8px 10px;\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    background-color: ${(props) => props.theme.input.bg};\n    border: 1px solid ${(props) => props.theme.input.border};\n    color: ${(props) => props.theme.text};\n    font-size: 14px;\n    transition: border-color ease-in-out 0.1s;\n\n    &:focus {\n      border: solid 1px ${(props) => props.theme.input.focusBorder} !important;\n      outline: none !important;\n    }\n\n    &::placeholder {\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n\n    &.cursor-pointer {\n      cursor: pointer;\n    }\n  }\n\n  .new-collection-location-row {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n  }\n\n  .new-collection-select {\n    width: 100%;\n    padding: 8px 10px;\n    padding-right: 28px;\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    background-color: ${(props) => props.theme.input.bg};\n    border: 1px solid ${(props) => props.theme.input.border};\n    color: ${(props) => props.theme.text};\n    font-size: 14px;\n    cursor: pointer;\n    transition: border-color ease-in-out 0.1s;\n    appearance: none;\n    background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23666' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E\");\n    background-repeat: no-repeat;\n    background-position: right 10px center;\n\n    &:focus {\n      border: solid 1px ${(props) => props.theme.input.focusBorder} !important;\n      outline: none !important;\n    }\n  }\n\n  .new-collection-actions-footer {\n    display: flex;\n    justify-content: flex-end;\n    gap: 8px;\n    margin-top: 4px;\n  }\n\n  .collection-empty-state-subtitle {\n    font-size: 12px;\n    margin-top: 4px;\n    opacity: 0.8;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/SaveTransientRequest/index.js",
    "content": "import React, { useState, useMemo, useEffect, useCallback } from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport Modal from 'components/Modal';\nimport SearchInput from 'components/SearchInput';\nimport Button from 'ui/Button';\nimport { IconFolder, IconChevronRight, IconCheck, IconX, IconEye, IconEyeOff, IconEdit, IconArrowBackUp } from '@tabler/icons';\nimport PathDisplay from 'components/PathDisplay/index';\nimport Help from 'components/Help';\nimport filter from 'lodash/filter';\nimport toast from 'react-hot-toast';\nimport StyledWrapper from './StyledWrapper';\nimport CollectionListItem from './CollectionListItem';\nimport FolderBreadcrumbs from './FolderBreadcrumbs';\nimport useCollectionFolderTree from 'hooks/useCollectionFolderTree';\nimport { removeSaveTransientRequestModal } from 'providers/ReduxStore/slices/collections';\nimport { insertTaskIntoQueue } from 'providers/ReduxStore/slices/app';\nimport { newFolder, closeTabs, mountCollection, createCollection, browseDirectory } from 'providers/ReduxStore/slices/collections/actions';\nimport { sanitizeName, validateName, validateNameError } from 'utils/common/regex';\nimport { resolveRequestFilename } from 'utils/common/platform';\nimport path, { normalizePath } from 'utils/common/path';\nimport { transformRequestToSaveToFilesystem, findCollectionByUid, findItemInCollection, areItemsLoading } from 'utils/collections';\nimport { DEFAULT_COLLECTION_FORMAT } from 'utils/common/constants';\nimport { itemSchema } from '@usebruno/schema';\nimport { uuid } from 'utils/common';\nimport { formatIpcError } from 'utils/common/error';\nimport get from 'lodash/get';\n\nconst SaveTransientRequest = ({ item: itemProp, collection: collectionProp, isOpen = false, onClose }) => {\n  const dispatch = useDispatch();\n\n  const latestCollection = useSelector((state) =>\n    collectionProp ? findCollectionByUid(state.collections.collections, collectionProp.uid) : null\n  );\n  const latestItem = latestCollection && itemProp ? findItemInCollection(latestCollection, itemProp.uid) : itemProp;\n\n  const item = itemProp;\n  const collection = collectionProp;\n\n  const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);\n  const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);\n  const allCollections = useSelector((state) => state.collections.collections);\n  const isScratchCollection = activeWorkspace?.scratchCollectionUid === collection?.uid;\n  const preferences = useSelector((state) => state.app.preferences);\n  const isDefaultWorkspace = activeWorkspace?.type === 'default';\n  const defaultCollectionLocation = isDefaultWorkspace\n    ? get(preferences, 'general.defaultLocation', '')\n    : (activeWorkspace?.pathname ? path.join(activeWorkspace.pathname, 'collections') : '');\n\n  const availableCollections = useMemo(() => {\n    if (!isScratchCollection || !activeWorkspace) return [];\n\n    return (activeWorkspace.collections || []).map((wc) => {\n      const fullCollection = allCollections.find((c) => normalizePath(c.pathname) === normalizePath(wc.path));\n      // Use stable deterministic UID based on path to avoid duplicate Redux entries\n      const stableUid = wc.path ? `pending-${wc.path.replace(/[^a-zA-Z0-9]/g, '-')}` : uuid();\n      return fullCollection || { ...wc, uid: stableUid, mountStatus: 'unmounted' };\n    }).filter((c) => !workspaces.some((w) => w.scratchCollectionUid === c.uid));\n  }, [isScratchCollection, activeWorkspace, allCollections, workspaces]);\n\n  const handleClose = () => {\n    if (onClose) {\n      onClose();\n      return;\n    }\n    dispatch(removeSaveTransientRequestModal({ itemUid: item.uid }));\n  };\n  const [requestName, setRequestName] = useState(item?.name || '');\n  const [searchText, setSearchText] = useState('');\n  const [showNewFolderInput, setShowNewFolderInput] = useState(false);\n  const [newFolderName, setNewFolderName] = useState('');\n  const [newFolderDirectoryName, setNewFolderDirectoryName] = useState('');\n  const [showFilesystemName, setShowFilesystemName] = useState(false);\n  const [isEditingFolderFilename, setIsEditingFolderFilename] = useState(false);\n  const [pendingFolderNavigation, setPendingFolderNavigation] = useState(null);\n\n  // State for new collection creation\n  const [newCollection, setNewCollection] = useState({ show: false, name: '', location: '', format: DEFAULT_COLLECTION_FORMAT });\n\n  const [selectedTargetCollectionPath, setSelectedTargetCollectionPath] = useState(null);\n  const [isSelectingCollection, setIsSelectingCollection] = useState(isScratchCollection);\n  const folderTreeCollectionUid = selectedTargetCollectionPath\n    ? availableCollections.find((c) => (c.path || c.pathname) === selectedTargetCollectionPath)?.uid\n    : collection?.uid;\n\n  const selectedTargetCollection = selectedTargetCollectionPath\n    ? availableCollections.find((c) => (c.path || c.pathname) === selectedTargetCollectionPath)\n    : null;\n\n  useEffect(() => {\n    const isMounted = selectedTargetCollection?.mountStatus === 'mounted';\n    const isFullyLoaded = isMounted && !areItemsLoading(selectedTargetCollection);\n    if (selectedTargetCollectionPath && isFullyLoaded) {\n      setIsSelectingCollection(false);\n    }\n  }, [selectedTargetCollectionPath, selectedTargetCollection]);\n\n  const {\n    currentFolders,\n    breadcrumbs,\n    selectedFolderUid,\n    navigateIntoFolder,\n    navigateToRoot,\n    navigateToBreadcrumb,\n    getCurrentParentFolder,\n    getCurrentSelectedFolder,\n    reset,\n    isAtRoot\n  } = useCollectionFolderTree(folderTreeCollectionUid);\n\n  const resetForm = useCallback(() => {\n    setRequestName(item?.name || '');\n    setSearchText('');\n    reset();\n    setShowNewFolderInput(false);\n    setNewFolderName('');\n    setNewFolderDirectoryName('');\n    setShowFilesystemName(false);\n    setIsEditingFolderFilename(false);\n    setPendingFolderNavigation(null);\n    setSelectedTargetCollectionPath(null);\n    setIsSelectingCollection(isScratchCollection);\n    // Reset new collection state\n    setNewCollection({ show: false, name: '', location: '', format: DEFAULT_COLLECTION_FORMAT });\n  }, [item?.name, isScratchCollection, reset]);\n\n  useEffect(() => {\n    if (isOpen && item) {\n      resetForm();\n    }\n  }, [isOpen, item, resetForm]);\n\n  useEffect(() => {\n    if (pendingFolderNavigation) {\n      const newFolder = currentFolders.find((f) => f.filename === pendingFolderNavigation);\n      if (newFolder) {\n        navigateIntoFolder(newFolder.uid);\n        setPendingFolderNavigation(null);\n      }\n    }\n  }, [currentFolders, pendingFolderNavigation, navigateIntoFolder]);\n\n  const filteredFolders = useMemo(() => {\n    if (!searchText.trim()) {\n      return currentFolders;\n    }\n    const searchLower = searchText.toLowerCase();\n    return filter(currentFolders, (folder) => folder.name.toLowerCase().includes(searchLower));\n  }, [currentFolders, searchText]);\n\n  const handleCancel = () => {\n    resetForm();\n    handleClose();\n  };\n\n  const handleSelectCollection = useCallback((selectedCollection) => {\n    const collectionPath = selectedCollection.path || selectedCollection.pathname;\n    const isMounted = selectedCollection.mountStatus === 'mounted';\n    const isFullyLoaded = isMounted && !areItemsLoading(selectedCollection);\n\n    setSelectedTargetCollectionPath(collectionPath);\n\n    if (isFullyLoaded) {\n      setIsSelectingCollection(false);\n      return;\n    }\n\n    if (!isMounted && selectedCollection.mountStatus !== 'mounting') {\n      dispatch(\n        mountCollection({\n          collectionUid: selectedCollection.uid || uuid(),\n          collectionPathname: collectionPath,\n          brunoConfig: selectedCollection.brunoConfig\n        })\n      );\n    }\n  }, [dispatch]);\n\n  const handleConfirm = async () => {\n    if (!item || !collection || !latestItem) {\n      return;\n    }\n\n    const targetCollection = selectedTargetCollection || collection;\n\n    try {\n      const { ipcRenderer } = window;\n\n      const selectedFolder = getCurrentSelectedFolder();\n      const targetDirname = selectedFolder ? selectedFolder.pathname : targetCollection.pathname;\n\n      const trimmedName = requestName.trim();\n      if (!trimmedName || trimmedName.length === 0) {\n        toast.error('Request name is required');\n        return;\n      }\n\n      if (!validateName(trimmedName)) {\n        toast.error(validateNameError(trimmedName));\n        return;\n      }\n\n      const sanitizedFilename = sanitizeName(trimmedName);\n\n      const itemToSave = latestItem.draft ? { ...latestItem, ...latestItem.draft } : { ...latestItem };\n      itemToSave.name = sanitizedFilename;\n      delete itemToSave.draft;\n\n      const transformedItem = transformRequestToSaveToFilesystem(itemToSave);\n      await itemSchema.validate(transformedItem);\n\n      const targetFormat = targetCollection.format || DEFAULT_COLLECTION_FORMAT;\n      const sourceFormat = collection.format || DEFAULT_COLLECTION_FORMAT;\n      const targetFilename = resolveRequestFilename(sanitizedFilename, targetFormat);\n      const targetPathname = path.join(targetDirname, targetFilename);\n\n      await ipcRenderer.invoke('renderer:save-transient-request', {\n        sourcePathname: item.pathname,\n        targetDirname,\n        targetFilename,\n        request: transformedItem,\n        format: targetFormat,\n        sourceFormat\n      });\n\n      dispatch(\n        insertTaskIntoQueue({\n          uid: uuid(),\n          type: 'OPEN_REQUEST',\n          collectionUid: targetCollection.uid,\n          itemPathname: targetPathname,\n          preview: false\n        })\n      );\n\n      dispatch(closeTabs({ tabUids: [item.uid] }));\n\n      dispatch({\n        type: 'collections/deleteItem',\n        payload: {\n          itemUid: item.uid,\n          collectionUid: collection.uid\n        }\n      });\n\n      toast.success('Request saved successfully');\n      handleClose();\n    } catch (err) {\n      toast.error(formatIpcError(err) || 'Failed to save request');\n      console.error('Error saving request:', err);\n    }\n  };\n\n  const handleShowNewFolder = () => {\n    setShowNewFolderInput(true);\n    setNewFolderName('');\n    setNewFolderDirectoryName('');\n    setShowFilesystemName(false);\n    setIsEditingFolderFilename(false);\n  };\n\n  const handleCancelNewFolder = () => {\n    setShowNewFolderInput(false);\n    setNewFolderName('');\n    setNewFolderDirectoryName('');\n    setShowFilesystemName(false);\n    setIsEditingFolderFilename(false);\n  };\n\n  const handleNewFolderNameChange = (value) => {\n    setNewFolderName(value);\n    if (!isEditingFolderFilename) {\n      setNewFolderDirectoryName(sanitizeName(value));\n    }\n  };\n\n  const handleCreateNewFolder = async () => {\n    const trimmedFolderName = newFolderName.trim();\n\n    if (!trimmedFolderName) {\n      toast.error('Folder name is required');\n      return;\n    }\n\n    if (!validateName(trimmedFolderName)) {\n      toast.error(validateNameError(trimmedFolderName));\n      return;\n    }\n\n    const directoryName = newFolderDirectoryName.trim() || sanitizeName(trimmedFolderName);\n    const parentFolder = getCurrentParentFolder();\n    const targetCollectionUid = selectedTargetCollection?.uid || collection?.uid;\n\n    try {\n      await dispatch(newFolder(trimmedFolderName, directoryName, targetCollectionUid, parentFolder?.uid));\n      toast.success('New folder created!');\n\n      setPendingFolderNavigation(directoryName);\n      handleCancelNewFolder();\n    } catch (err) {\n      const errorMessage = err?.message || 'An error occurred while adding the folder';\n      toast.error(errorMessage);\n    }\n  };\n\n  // New Collection handlers\n  const handleShowNewCollection = () => {\n    setNewCollection({ show: true, name: '', location: defaultCollectionLocation, format: DEFAULT_COLLECTION_FORMAT });\n  };\n\n  const handleCancelNewCollection = () => {\n    setNewCollection({ show: false, name: '', location: '', format: DEFAULT_COLLECTION_FORMAT });\n  };\n\n  const handleBrowseCollectionLocation = () => {\n    dispatch(browseDirectory())\n      .then((dirPath) => {\n        if (typeof dirPath === 'string') {\n          setNewCollection((prev) => ({ ...prev, location: dirPath }));\n        }\n      })\n      .catch(() => {});\n  };\n\n  const handleCreateNewCollection = async () => {\n    const trimmedName = newCollection.name.trim();\n    if (!trimmedName) {\n      toast.error('Collection name is required');\n      return;\n    }\n    if (!validateName(trimmedName)) {\n      toast.error(validateNameError(trimmedName));\n      return;\n    }\n    if (!newCollection.location) {\n      toast.error('Location is required');\n      return;\n    }\n    try {\n      await dispatch(createCollection(trimmedName, sanitizeName(trimmedName), newCollection.location, { format: newCollection.format }));\n      toast.success('Collection created!');\n      handleCancelNewCollection();\n    } catch (err) {\n      toast.error(err?.message || 'An error occurred while creating the collection');\n    }\n  };\n\n  const handleFolderClick = (folderUid) => {\n    navigateIntoFolder(folderUid);\n    setSearchText('');\n  };\n\n  const handleBreadcrumbNavigate = useCallback((index) => {\n    navigateToBreadcrumb(index);\n    setSearchText('');\n  }, [navigateToBreadcrumb]);\n\n  if (!isOpen) {\n    return null;\n  }\n\n  return (\n    <StyledWrapper>\n      <Modal\n        size=\"md\"\n        title={isSelectingCollection ? 'Select Collection' : 'Save Request'}\n        handleCancel={handleCancel}\n        handleConfirm={handleConfirm}\n        confirmText=\"Save\"\n        cancelText=\"Cancel\"\n        hideFooter={true}\n      >\n        <div className=\"save-request-form\">\n          <div className=\"form-section\">\n            <label htmlFor=\"request-name\" className=\"form-label\">\n              Request name\n            </label>\n            <input\n              id=\"request-name\"\n              type=\"text\"\n              className=\"form-input textbox\"\n              autoComplete=\"off\"\n              autoCorrect=\"off\"\n              autoCapitalize=\"off\"\n              spellCheck=\"false\"\n              value={requestName}\n              onChange={(e) => setRequestName(e.target.value)}\n              autoFocus={!isSelectingCollection}\n              onFocus={(e) => e.target.select()}\n            />\n          </div>\n\n          <div className=\"collections-section\">\n            <div className=\"collections-label\">\n              {isSelectingCollection ? 'Select a collection to save to' : 'Save to Collections'}\n            </div>\n\n            {isScratchCollection && (\n              <div className=\"collection-name\">\n                <span\n                  className={isSelectingCollection ? '' : 'collection-name-breadcrumb'}\n                  onClick={!isSelectingCollection ? () => {\n                    setIsSelectingCollection(true);\n                    setSelectedTargetCollectionPath(null);\n                    reset();\n                  } : undefined}\n                >\n                  Collections\n                </span>\n                {!isSelectingCollection && (\n                  <>\n                    <IconChevronRight size={16} strokeWidth={1.5} className=\"collection-name-chevron\" />\n                    <FolderBreadcrumbs\n                      collectionName={(selectedTargetCollection || collection).name}\n                      breadcrumbs={breadcrumbs}\n                      isAtRoot={isAtRoot}\n                      onNavigateToRoot={navigateToRoot}\n                      onNavigateToBreadcrumb={handleBreadcrumbNavigate}\n                    />\n                  </>\n                )}\n              </div>\n            )}\n\n            {isSelectingCollection ? (\n              <div className=\"collection-list\">\n                {availableCollections.length > 0 || newCollection.show ? (\n                  <ul className=\"collection-list-items\">\n                    {availableCollections.map((coll) => {\n                      const collPath = coll.path || coll.pathname;\n                      return (\n                        <CollectionListItem\n                          key={collPath}\n                          collectionUid={coll.uid}\n                          collectionPath={collPath}\n                          collectionName={coll.name}\n                          isSelected={selectedTargetCollectionPath === collPath}\n                          onSelect={() => handleSelectCollection(coll)}\n                        />\n                      );\n                    })}\n                    {newCollection.show && (\n                      <li className=\"new-collection-item\">\n                        <div className=\"new-collection-field\">\n                          <label className=\"new-collection-label\">\n                            Collection name\n                          </label>\n                          <input\n                            ref={(node) => node?.focus()}\n                            type=\"text\"\n                            className=\"new-collection-input\"\n                            placeholder=\"Enter collection name\"\n                            value={newCollection.name}\n                            onChange={(e) => setNewCollection((prev) => ({ ...prev, name: e.target.value }))}\n                            onKeyDown={(e) => {\n                              if (e.key === 'Enter') {\n                                e.preventDefault();\n                                e.stopPropagation();\n                                handleCreateNewCollection();\n                              } else if (e.key === 'Escape') {\n                                e.stopPropagation();\n                                handleCancelNewCollection();\n                              }\n                            }}\n                          />\n                        </div>\n\n                        <div className=\"new-collection-field\">\n                          <label className=\"new-collection-label flex items-center\">\n                            Location\n                            <Help width={250} placement=\"top\">\n                              <p>\n                                Bruno stores your collections on your computer's filesystem.\n                              </p>\n                              <p className=\"mt-2\">\n                                Choose the location where you want to store this collection.\n                              </p>\n                            </Help>\n                          </label>\n                          <div className=\"new-collection-location-row\">\n                            <input\n                              type=\"text\"\n                              className=\"new-collection-input cursor-pointer\"\n                              placeholder=\"Select location\"\n                              value={newCollection.location}\n                              readOnly\n                              onClick={handleBrowseCollectionLocation}\n                            />\n                            <Button\n                              type=\"button\"\n                              variant=\"outline\"\n                              color=\"secondary\"\n                              size=\"sm\"\n                              rounded=\"sm\"\n                              onClick={handleBrowseCollectionLocation}\n                            >\n                              Browse\n                            </Button>\n                          </div>\n                        </div>\n\n                        <div className=\"new-collection-field\">\n                          <label className=\"new-collection-label flex items-center\">\n                            File Format\n                            <Help width={300} placement=\"top\">\n                              <p>\n                                Choose the file format for storing requests in this collection.\n                              </p>\n                              <p className=\"mt-2\">\n                                <strong>OpenCollection (YAML):</strong> Industry-standard YAML format (.yml files)\n                              </p>\n                              <p className=\"mt-1\">\n                                <strong>BRU:</strong> Bruno's native file format (.bru files)\n                              </p>\n                            </Help>\n                          </label>\n                          <select\n                            className=\"new-collection-select\"\n                            value={newCollection.format}\n                            onChange={(e) => setNewCollection((prev) => ({ ...prev, format: e.target.value }))}\n                          >\n                            <option value=\"yml\">OpenCollection (YAML)</option>\n                            <option value=\"bru\">BRU Format (.bru)</option>\n                          </select>\n                        </div>\n\n                        <div className=\"new-collection-actions-footer\">\n                          <Button\n                            type=\"button\"\n                            color=\"secondary\"\n                            variant=\"ghost\"\n                            size=\"sm\"\n                            onClick={handleCancelNewCollection}\n                          >\n                            Cancel\n                          </Button>\n                          <Button\n                            type=\"button\"\n                            color=\"primary\"\n                            size=\"sm\"\n                            onClick={handleCreateNewCollection}\n                          >\n                            Save\n                          </Button>\n                        </div>\n                      </li>\n                    )}\n                  </ul>\n                ) : (\n                  <div className=\"collection-empty-state\">\n                    <p>No collections Yet</p>\n                    <p className=\"collection-empty-state-subtitle\">Collections help you organize your requests. Create your first one to save this request.</p>\n                  </div>\n                )}\n              </div>\n            ) : (\n              <>\n                {!isScratchCollection && (selectedTargetCollection || collection) && (\n                  <div className=\"collection-name\">\n                    <FolderBreadcrumbs\n                      collectionName={(selectedTargetCollection || collection).name}\n                      breadcrumbs={breadcrumbs}\n                      isAtRoot={isAtRoot}\n                      onNavigateToRoot={navigateToRoot}\n                      onNavigateToBreadcrumb={handleBreadcrumbNavigate}\n                    />\n                  </div>\n                )}\n\n                <div className=\"search-container\">\n                  <SearchInput\n                    searchText={searchText}\n                    setSearchText={setSearchText}\n                    placeholder=\"Search for folder\"\n                    autoFocus={false}\n                  />\n                </div>\n\n                <div className=\"folder-list\">\n                  {filteredFolders.length > 0 || showNewFolderInput ? (\n                    <ul className=\"folder-list-items\">\n                      {filteredFolders.map((folder) => (\n                        <li\n                          key={folder.uid}\n                          className={`folder-item ${selectedFolderUid === folder.uid ? 'selected' : ''}`}\n                          onClick={() => handleFolderClick(folder.uid)}\n                        >\n                          <div className=\"folder-item-content\">\n                            <IconFolder size={16} strokeWidth={1.5} />\n                            <span className=\"folder-item-name\">{folder.name}</span>\n                          </div>\n                          <IconChevronRight size={16} strokeWidth={1.5} />\n                        </li>\n                      ))}\n                      {showNewFolderInput && (\n                        <li className=\"new-folder-item\">\n                          <div className=\"new-folder-header\">\n                            <IconFolder size={16} strokeWidth={1.5} />\n                            <label className=\"new-folder-header-label\">\n                              {showFilesystemName ? 'New Folder name (in bruno)' : 'New Folder name'}\n                            </label>\n                          </div>\n                          <div className=\"new-folder-input-row\">\n                            <input\n                              ref={(node) => node?.focus()}\n                              type=\"text\"\n                              className=\"new-folder-input\"\n                              placeholder=\"Untitled new folder\"\n                              value={newFolderName}\n                              onChange={(e) => handleNewFolderNameChange(e.target.value)}\n                              onKeyDown={(e) => {\n                                if (e.key === 'Enter') {\n                                  e.preventDefault();\n                                  e.stopPropagation();\n                                  handleCreateNewFolder();\n                                } else if (e.key === 'Escape') {\n                                  e.stopPropagation();\n                                  handleCancelNewFolder();\n                                }\n                              }}\n                            />\n                            <div className=\"new-folder-actions\">\n                              <button\n                                type=\"button\"\n                                className=\"new-folder-action-btn\"\n                                onClick={handleCancelNewFolder}\n                                title=\"Cancel\"\n                              >\n                                <IconX size={16} strokeWidth={1.5} />\n                              </button>\n                              <button\n                                type=\"button\"\n                                className=\"new-folder-action-btn\"\n                                onClick={handleCreateNewFolder}\n                                title=\"Create folder\"\n                              >\n                                <IconCheck size={16} strokeWidth={1.5} />\n                              </button>\n                            </div>\n                          </div>\n\n                          {showFilesystemName && (\n                            <div className=\"new-folder-filesystem-wrapper\">\n                              <div className=\"flex items-center justify-between\">\n                                <label className=\"new-folder-filesystem-label flex items-center font-medium\">\n                                  Folder Name <small className=\"font-normal text-muted ml-1\">(on filesystem)</small>\n                                  <Help width={300} placement=\"top\">\n                                    <p>\n                                      You can choose to save the folder as a different name on your file system versus what is displayed in the app.\n                                    </p>\n                                  </Help>\n                                </label>\n                                {isEditingFolderFilename ? (\n                                  <IconArrowBackUp\n                                    className=\"cursor-pointer opacity-50 hover:opacity-80\"\n                                    size={16}\n                                    strokeWidth={1.5}\n                                    onClick={() => setIsEditingFolderFilename(false)}\n                                  />\n                                ) : (\n                                  <IconEdit\n                                    className=\"cursor-pointer opacity-50 hover:opacity-80\"\n                                    size={16}\n                                    strokeWidth={1.5}\n                                    onClick={() => setIsEditingFolderFilename(true)}\n                                  />\n                                )}\n                              </div>\n                              {isEditingFolderFilename ? (\n                                <div className=\"relative flex flex-row gap-1 items-center justify-between\">\n                                  <input\n                                    type=\"text\"\n                                    className=\"block textbox mt-2 w-full\"\n                                    placeholder=\"Folder Name\"\n                                    value={newFolderDirectoryName}\n                                    autoComplete=\"off\"\n                                    autoCorrect=\"off\"\n                                    autoCapitalize=\"off\"\n                                    spellCheck=\"false\"\n                                    onChange={(e) => setNewFolderDirectoryName(e.target.value)}\n                                    onKeyDown={(e) => {\n                                      if (e.key === 'Enter') {\n                                        e.preventDefault();\n                                        e.stopPropagation();\n                                        handleCreateNewFolder();\n                                      } else if (e.key === 'Escape') {\n                                        e.stopPropagation();\n                                        handleCancelNewFolder();\n                                      }\n                                    }}\n                                  />\n                                </div>\n                              ) : (\n                                <div className=\"relative flex flex-row gap-1 items-center justify-between\">\n                                  <PathDisplay\n                                    iconType=\"folder\"\n                                    baseName={newFolderDirectoryName}\n                                  />\n                                </div>\n                              )}\n                            </div>\n                          )}\n\n                          <button\n                            type=\"button\"\n                            className=\"new-folder-toggle-filesystem-btn\"\n                            onClick={() => {\n                              setShowFilesystemName(!showFilesystemName);\n                              setNewFolderDirectoryName(sanitizeName(newFolderName));\n                              setIsEditingFolderFilename(false);\n                            }}\n                          >\n                            {showFilesystemName ? (\n                              <>\n                                <IconEyeOff size={16} strokeWidth={1.5} />\n                                <span>Hide filesystem name</span>\n                              </>\n                            ) : (\n                              <>\n                                <IconEye size={16} strokeWidth={1.5} />\n                                <span>Show filesystem name</span>\n                              </>\n                            )}\n                          </button>\n                        </li>\n                      )}\n                    </ul>\n                  ) : (\n                    <div className=\"folder-empty-state\">\n                      {searchText.trim() ? 'No folders found' : 'No folders available'}\n                    </div>\n                  )}\n                </div>\n              </>\n            )}\n          </div>\n        </div>\n\n        <div className=\"custom-modal-footer\">\n          <div className=\"footer-left\">\n            {!showNewFolderInput && !isSelectingCollection && (\n              <Button\n                type=\"button\"\n                color=\"primary\"\n                variant=\"ghost\"\n                icon={<IconFolder size={16} strokeWidth={1.5} />}\n                onClick={handleShowNewFolder}\n              >\n                New Folder\n              </Button>\n            )}\n            {isSelectingCollection && !newCollection.show && (\n              <Button\n                type=\"button\"\n                color=\"primary\"\n                variant=\"ghost\"\n                icon={<IconFolder size={16} strokeWidth={1.5} />}\n                onClick={handleShowNewCollection}\n              >\n                New collection\n              </Button>\n            )}\n          </div>\n          <div className=\"footer-right\">\n            <Button type=\"button\" color=\"secondary\" variant=\"ghost\" onClick={handleCancel}>\n              Cancel\n            </Button>\n            {!isSelectingCollection && (\n              <Button type=\"button\" color=\"primary\" onClick={handleConfirm}>\n                Save\n              </Button>\n            )}\n          </div>\n        </div>\n      </Modal>\n    </StyledWrapper>\n  );\n};\n\nexport default SaveTransientRequest;\n"
  },
  {
    "path": "packages/bruno-app/src/components/SearchInput/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  position: relative;\n\n  .search-icon {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .close-icon {\n    color: ${(props) => props.theme.colors.text.muted};\n    cursor: pointer;\n\n    &:hover {\n      color: ${(props) => props.theme.text};\n    }\n  }\n\n  input#search-input {\n    background-color: ${(props) => props.theme.input.bg};\n    border: 1px solid ${(props) => props.theme.input.border};\n    color: ${(props) => props.theme.text};\n\n    &:focus {\n      outline: none;\n      border-color: ${(props) => props.theme.input.focusBorder};\n    }\n\n    &::placeholder {\n      color: ${(props) => props.theme.input.placeholder.color};\n      opacity: ${(props) => props.theme.input.placeholder.opacity};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/SearchInput/index.js",
    "content": "import React from 'react';\nimport { IconSearch, IconX } from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\n\nconst SearchInput = ({\n  searchText,\n  setSearchText,\n  placeholder = 'Search',\n  className = '',\n  onChange,\n  ...props\n}) => {\n  const handleChange = (e) => {\n    setSearchText(e.target.value);\n    if (onChange) {\n      onChange(e);\n    }\n  };\n\n  return (\n    <StyledWrapper className={`px-2 ${className}`}>\n      <div className=\"absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none\">\n        <span className=\"search-icon\">\n          <IconSearch size={16} strokeWidth={1.5} />\n        </span>\n      </div>\n      <input\n        type=\"text\"\n        name=\"search\"\n        placeholder={placeholder}\n        id=\"search-input\"\n        autoComplete=\"off\"\n        autoCorrect=\"off\"\n        autoCapitalize=\"off\"\n        spellCheck=\"false\"\n        className=\"block w-full pl-7 py-2 rounded-md\"\n        value={searchText}\n        autoFocus\n        onChange={handleChange}\n        {...props}\n      />\n      {searchText !== '' && (\n        <div className=\"absolute inset-y-0 right-0 pr-4 flex items-center\">\n          <span\n            className=\"close-icon\"\n            onClick={() => {\n              setSearchText('');\n            }}\n          >\n            <IconX size={16} strokeWidth={1.5} className=\"cursor-pointer\" />\n          </span>\n        </div>\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default SearchInput;\n"
  },
  {
    "path": "packages/bruno-app/src/components/SecuritySettings/JsSandboxMode/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst StyledWrapper = styled.div`\n  .sandbox-icon {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 1.375rem;\n    height: 1.375rem;\n    border-radius: ${(props) => props.theme.border.radius.base};\n    cursor: pointer;\n    transition: all 0.15s ease;\n\n    &.safe-mode {\n      color: ${(props) => props.theme.app.collection.toolbar.sandboxMode.safeMode.color};\n      background-color: ${(props) => props.theme.app.collection.toolbar.sandboxMode.safeMode.bg};\n    }\n\n    &.developer-mode {\n      color: ${(props) => props.theme.app.collection.toolbar.sandboxMode.developerMode.color};\n      background-color: ${(props) => props.theme.app.collection.toolbar.sandboxMode.developerMode.bg};\n    }\n\n    &:hover {\n      opacity: 0.8;\n    }\n  }\n\n  .sandbox-dropdown {\n    min-width: 260px;\n    max-width: 380px;\n  }\n\n  .sandbox-header {\n    padding: 0.5rem 0.625rem;\n    font-size: ${(props) => props.theme.font.size.base};\n    color: ${(props) => props.theme.dropdown.headingText};\n  }\n\n  .sandbox-option {\n    display: flex;\n    margin: 5px;\n    border-radius: ${(props) => props.theme.border.radius.md};\n    padding: 12px;\n    align-items: flex-start;\n    text-align: left;\n    gap: 0.5rem;\n    position: relative;\n\n    &.safe-mode {\n      border: 1px solid ${(props) => props.theme.input.border};\n      color: ${(props) => props.theme.colors.text.green};\n      margin-bottom: 10px;\n    }\n\n    &.developer-mode {\n      border: 1px solid ${(props) => props.theme.input.border};\n      color: ${(props) => props.theme.colors.text.warning};\n    }\n\n    &.active {\n      cursor: default;\n\n      &.developer-mode {\n        border: 1px solid ${(props) => props.theme.colors.text.warning};\n        background-color: ${(props) => rgba(props.theme.colors.text.warning, 0.04)};\n\n        .sandbox-option-radio input:checked {\n          border-color: ${(props) => props.theme.colors.text.warning};\n        }\n\n        .sandbox-option-radio input::after {\n          background: ${(props) => props.theme.colors.text.warning};\n        }\n      }\n\n      &.safe-mode {\n        border: 1px solid ${(props) => props.theme.colors.text.green};\n        background-color: ${(props) => rgba(props.theme.colors.text.green, 0.04)};\n\n        .sandbox-option-radio input:checked {\n          border-color: ${(props) => props.theme.colors.text.green};\n        }\n\n        .sandbox-option-radio input::after {\n          background: ${(props) => props.theme.colors.text.green};\n        }\n      }\n    }\n\n    svg {\n      width: 2rem;\n    }\n  }\n\n  .recommended-badge {\n    padding: 0.125rem 0.5rem;\n    font-size: ${(props) => props.theme.font.size.xs};\n    background-color: ${(props) => rgba(props.theme.colors.text.green, 0.1)};\n    color: ${(props) => props.theme.colors.text.green};\n    border-radius: ${(props) => props.theme.border.radius.sm};\n  }\n\n  .sandbox-option-title {\n    display: flex;\n    align-items: center;\n    font-size: ${(props) => props.theme.font.size.base};\n    gap: 0.25rem;\n    line-height: 1.25rem;\n    color: ${(props) => props.theme.colors.text.subtext2};\n  }\n\n  .sandbox-option-radio {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    margin-right: 0.25rem;\n  }\n\n  .sandbox-option-radio input {\n    appearance: none;\n    width: 18px;\n    height: 18px;\n    border-radius: 9999px;\n    border: 1px solid currentColor;\n    background: transparent;\n    cursor: pointer;\n    position: relative;\n    transition: all 0.15s ease;\n  }\n\n  .sandbox-option-radio input::after {\n    content: '';\n    position: absolute;\n    inset: 3px;\n    border-radius: 9999px;\n    background: ${(props) => props.theme.background.base};\n    opacity: 0;\n    transform: scale(0.5);\n    transition: all 0.15s ease;\n  }\n\n  .sandbox-option-radio input:checked::after {\n    opacity: 1;\n    transform: scale(1);\n  }\n\n  .sandbox-option-radio input:focus-visible {\n    outline: none;\n  }\n\n  .sandbox-option-description {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.muted};\n    line-height: 1.1rem;\n    margin-top: 0.25rem;\n  }\n\n  .developer-mode-warning {\n    margin-top: 0.5rem;\n    padding: 0.25rem 0.5rem;\n    display: inline-block;\n    background-color:   ${(props) => rgba(props.theme.colors.text.warning, 0.1)};\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    color: ${(props) => props.theme.colors.text.warning};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/SecuritySettings/JsSandboxMode/index.js",
    "content": "import { useEffect, useRef, useState } from 'react';\nimport { useDispatch } from 'react-redux';\nimport toast from 'react-hot-toast';\nimport { IconShieldCheck, IconCode } from '@tabler/icons';\nimport Dropdown from 'components/Dropdown';\nimport { saveCollectionSecurityConfig } from 'providers/ReduxStore/slices/collections/actions';\nimport StyledWrapper from './StyledWrapper';\nimport ToolHint from 'components/ToolHint';\n\nconst SANDBOX_OPTIONS = [\n  {\n    key: 'safe',\n    label: 'Safe Mode',\n    description: 'JavaScript code is executed in a secure sandbox and cannot access your filesystem or execute system commands.',\n    icon: IconShieldCheck,\n    recommended: true\n  },\n  {\n    key: 'developer',\n    label: 'Developer Mode',\n    description: 'JavaScript code has access to the filesystem, can execute system commands and access sensitive information.',\n    icon: IconCode,\n    warning: 'Use only if you trust the authors of the collection',\n    recommended: false\n  }\n];\n\nconst JsSandboxMode = ({ collection }) => {\n  const dispatch = useDispatch();\n  const dropdownRef = useRef(null);\n  const [selectedMode, setSelectedMode] = useState(collection?.securityConfig?.jsSandboxMode || 'safe');\n\n  useEffect(() => {\n    setSelectedMode(collection?.securityConfig?.jsSandboxMode || 'safe');\n  }, [collection?.securityConfig?.jsSandboxMode]);\n\n  const onDropdownCreate = (instance) => {\n    dropdownRef.current = instance;\n  };\n\n  const closeDropdown = () => {\n    dropdownRef.current?.hide();\n  };\n\n  const handleKeyDown = (e) => {\n    if (e && e.key === 'Escape') {\n      closeDropdown();\n    }\n  };\n\n  const handleModeChange = (mode) => {\n    if (!collection?.uid || mode === selectedMode) {\n      return;\n    }\n\n    dispatch(\n      saveCollectionSecurityConfig(collection.uid, {\n        jsSandboxMode: mode\n      })\n    )\n      .then(() => {\n        setSelectedMode(mode);\n      })\n      .catch((err) => {\n        console.error(err);\n        toast.error('Failed to update sandbox mode');\n      });\n  };\n\n  const renderOption = (option) => {\n    const OptionIcon = option.icon;\n    const isActive = selectedMode === option.key;\n\n    return (\n      <button\n        type=\"button\"\n        key={option.key}\n        className={`sandbox-option ${option.key}-mode ${isActive ? 'active' : ''}`}\n        onClick={() => handleModeChange(option.key)}\n        role=\"menuitemradio\"\n        aria-checked={isActive}\n        data-testid={`sandbox-mode-${option.key}`}\n      >\n\n        <div className=\"dropdown-label\">\n          <div className=\"sandbox-option-title\">\n            <div className=\"sandbox-option-radio\">\n              <input\n                type=\"radio\"\n                name=\"sandbox-mode\"\n                value={option.key}\n                checked={isActive}\n              />\n            </div>\n            <OptionIcon size={24} strokeWidth={1.5} />\n            {option.label}\n            {option.recommended && <span className=\"recommended-badge\">Recommended</span>}\n          </div>\n          {option.warning && (<div><span className=\"developer-mode-warning\">{option.warning}</span></div>)}\n          <div className=\"sandbox-option-description\">{option.description}</div>\n        </div>\n      </button>\n    );\n  };\n\n  const triggerIcon = (\n    <div>\n      <ToolHint text={`${selectedMode === 'developer' ? 'Developer Mode' : 'Safe Mode'}`} toolhintId=\"JavascriptSandboxToolhintId\" place=\"bottom\">\n        <div className={`sandbox-icon ${selectedMode === 'developer' ? 'developer-mode' : 'safe-mode'}`} data-testid=\"sandbox-mode-selector\">\n          {selectedMode === 'developer' ? <IconCode size={14} strokeWidth={2} /> : <IconShieldCheck size={14} strokeWidth={2} />}\n        </div>\n      </ToolHint>\n    </div>\n  );\n\n  return (\n    <StyledWrapper className=\"flex\" onKeyDown={handleKeyDown}>\n      <Dropdown onCreate={onDropdownCreate} icon={triggerIcon} placement=\"bottom-start\">\n        <div className=\"sandbox-dropdown\">\n          <div className=\"sandbox-header\">JavaScript Sandbox</div>\n          {SANDBOX_OPTIONS.map(renderOption)}\n        </div>\n      </Dropdown>\n    </StyledWrapper>\n  );\n};\n\nexport default JsSandboxMode;\n"
  },
  {
    "path": "packages/bruno-app/src/components/SensitiveFieldWarning/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  .tooltip-mod {\n    width: 150px !important;\n  }\n\n  .tooltip-icon { \n    color: ${(props) => props.theme.colors.text.danger};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/SensitiveFieldWarning/index.js",
    "content": "import React from 'react';\nimport { IconAlertTriangle } from '@tabler/icons';\nimport { Tooltip } from 'react-tooltip';\nimport StyledWrapper from './StyledWrapper';\n\nconst SensitiveFieldWarning = ({ fieldName, warningMessage }) => {\n  const tooltipId = `sensitive-field-warning-${fieldName}`;\n\n  return (\n    <StyledWrapper>\n      <span className=\"ml-2 flex items-center\">\n        <IconAlertTriangle id={tooltipId} className=\"tooltip-icon cursor-pointer\" size={20} />\n        <Tooltip\n          anchorId={tooltipId}\n          className=\"tooltip-mod max-w-lg\"\n          content={(\n            <div>\n              <p>\n                <span>{warningMessage}</span>\n              </p>\n            </div>\n          )}\n        />\n      </span>\n    </StyledWrapper>\n  );\n};\n\nexport default SensitiveFieldWarning;\n"
  },
  {
    "path": "packages/bruno-app/src/components/SettingsInput/index.js",
    "content": "import React from 'react';\nimport { useTheme } from 'providers/Theme';\n\nconst SettingsInput = ({\n  id,\n  label,\n  value,\n  onChange,\n  className = '',\n  description = '',\n  onKeyDown\n}) => {\n  const { theme } = useTheme();\n\n  return (\n    <div className=\"flex items-center justify-between\">\n      <div className=\"flex flex-col\">\n        <label className=\"text-xs font-medium text-gray-900 dark:text-gray-100\" htmlFor={id}>\n          {label}\n        </label>\n        {description && (\n          <p className=\"text-xs text-gray-700 dark:text-gray-400\">\n            {description}\n          </p>\n        )}\n      </div>\n      <input\n        id={id}\n        type=\"text\"\n        className={`block px-2 py-1 rounded-sm outline-none transition-colors duration-100 w-24 h-8 ${className}`}\n        style={{\n          backgroundColor: theme.input.bg,\n          border: `1px solid ${theme.input.border}`\n        }}\n        autoComplete=\"off\"\n        autoCorrect=\"off\"\n        autoCapitalize=\"off\"\n        spellCheck=\"false\"\n        value={value}\n        onChange={onChange}\n        onKeyDown={onKeyDown}\n      />\n    </div>\n  );\n};\n\nexport default SettingsInput;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ShareCollection/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .tabs {\n    .tab {\n      padding: 6px 0px;\n      border: none;\n      border-bottom: solid 2px transparent;\n      margin-right: ${(props) => props.theme.tabs.marginRight};\n      color: ${(props) => props.theme.colors.text.subtext0};\n      cursor: pointer;\n\n      &:focus,\n      &:active,\n      &:focus-within,\n      &:focus-visible,\n      &:target {\n        outline: none !important;\n        box-shadow: none !important;\n      }\n\n      &.active {\n        font-weight: ${(props) => props.theme.tabs.active.fontWeight} !important;\n        color: ${(props) => props.theme.tabs.active.color} !important;\n        border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;\n      }\n    }\n  }\n\n  .section-title {\n    font-weight: 600;\n    font-size: 0.75rem;\n    text-transform: uppercase;\n    letter-spacing: 0.05em;\n    color: ${(props) => props.theme.colors.text.subtext0};\n    margin-bottom: 0.75rem;\n  }\n\n  .opencollection-link {\n    color: ${(props) => props.theme.textLink};\n    text-decoration: none;\n    &:hover {\n      text-decoration: underline;\n    }\n  }\n\n  .bruno-format-grid {\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    gap: 1rem;\n  }\n\n  .format-card {\n    display: flex;\n    flex-direction: column;\n    border-radius: ${(props) => props.theme.border.radius.base};\n    padding: 1rem;\n    border: 2px solid ${(props) => props.theme.border.border0};\n    background-color: ${(props) => props.theme.background.base};\n    cursor: pointer;\n    transition: border-color 0.15s ease;\n    min-height: 180px;\n\n    &:hover:not(.selected) {\n      border-color: ${(props) => props.theme.border.border2};\n    }\n\n    &.selected {\n      border-color: ${(props) => props.theme.primary.solid};\n    }\n\n    .card-header {\n      display: flex;\n      align-items: center;\n      gap: 0.5rem;\n      margin-bottom: 0.5rem;\n\n      .card-title {\n        font-weight: 600;\n        font-size: 0.9375rem;\n      }\n\n      .recommended-badge {\n        padding: 0.125rem 0.5rem;\n        font-size: 0.6875rem;\n        font-weight: 600;\n        border-radius: 0.25rem;\n        background-color: ${(props) => props.theme.colors.text.warning};\n        color: white;\n      }\n    }\n\n    .card-description {\n      font-size: 0.8125rem;\n      color: ${(props) => props.theme.colors.text.subtext0};\n      margin-bottom: 0.75rem;\n    }\n\n    .feature-list {\n      flex: 1;\n      display: flex;\n      flex-direction: column;\n      gap: 0.375rem;\n\n      .feature-item {\n        display: flex;\n        align-items: flex-start;\n        gap: 0.5rem;\n        font-size: 0.8125rem;\n        color: ${(props) => props.theme.colors.text.subtext0};\n\n        .checkmark {\n          color: ${(props) => props.theme.colors.text.subtext0};\n          flex-shrink: 0;\n          margin-top: 0.125rem;\n        }\n      }\n    }\n\n    .best-for {\n      margin-top: 0.75rem;\n      font-size: 0.75rem;\n      font-style: italic;\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n  }\n\n  .other-format-grid {\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    gap: 1rem;\n  }\n\n  .other-format-card {\n    display: flex;\n    align-items: center;\n    gap: 0.75rem;\n    border-radius: ${(props) => props.theme.border.radius.base};\n    padding: 0.75rem 1rem;\n    border: 2px solid ${(props) => props.theme.border.border0};\n    background-color: ${(props) => props.theme.background.base};\n    cursor: pointer;\n    transition: border-color 0.15s ease;\n\n    &:hover:not(.selected) {\n      border-color: ${(props) => props.theme.border.border2};\n    }\n\n    &.selected {\n      border-color: ${(props) => props.theme.primary.solid};\n    }\n\n    .format-icon {\n      width: 32px;\n      height: 32px;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      flex-shrink: 0;\n    }\n\n    .format-info {\n      .format-name {\n        font-weight: 600;\n        font-size: 0.875rem;\n      }\n\n      .format-description {\n        font-size: 0.75rem;\n        color: ${(props) => props.theme.colors.text.subtext0};\n      }\n    }\n  }\n\n  .modal-footer {\n    display: flex;\n    justify-content: flex-end;\n    margin-top: 1.5rem;\n    padding-top: 1rem;\n    border-top: 1px solid ${(props) => props.theme.border.border0};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ShareCollection/index.js",
    "content": "import React, { useState, useMemo } from 'react';\nimport Modal from 'components/Modal';\nimport Button from 'ui/Button';\nimport { IconCheck, IconAlertTriangle, IconFileExport } from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\nimport exportPostmanCollection from 'utils/exporters/postman-collection';\nimport exportOpenCollection from 'utils/exporters/opencollection';\nimport { cloneDeep } from 'lodash';\nimport { transformCollectionToSaveToExportAsFile } from 'utils/collections/index';\nimport { useSelector } from 'react-redux';\nimport { findCollectionByUid, areItemsLoading } from 'utils/collections/index';\nimport toast from 'react-hot-toast';\n\nconst EXPORT_FORMATS = {\n  ZIP: 'zip',\n  YAML: 'yaml',\n  POSTMAN: 'postman'\n};\n\nconst ShareCollection = ({ onClose, collectionUid }) => {\n  const collection = useSelector((state) => findCollectionByUid(state.collections.collections, collectionUid));\n  const isCollectionLoading = areItemsLoading(collection);\n  const [selectedFormat, setSelectedFormat] = useState(EXPORT_FORMATS.ZIP);\n  const [isExporting, setIsExporting] = useState(false);\n\n  const hasNonExportableRequestTypes = useMemo(() => {\n    let types = new Set();\n    const checkItem = (item) => {\n      if (item.type === 'grpc-request') {\n        types.add('gRPC');\n        return true;\n      }\n      if (item.type === 'ws-request') {\n        types.add('WebSocket');\n        return true;\n      }\n      if (item.items) {\n        return item.items.some(checkItem);\n      }\n      return false;\n    };\n    return {\n      has: collection?.items?.filter(checkItem).length || false,\n      types: [...types]\n    };\n  }, [collection]);\n\n  const handleExportZip = async () => {\n    try {\n      const { ipcRenderer } = window;\n      const result = await ipcRenderer.invoke('renderer:export-collection-zip', collection.pathname, collection.name);\n      if (result.success) {\n        toast.success('Collection exported successfully');\n      }\n    } catch (error) {\n      toast.error('Failed to export collection: ' + error.message);\n    }\n  };\n\n  const handleExportYaml = () => {\n    const collectionCopy = cloneDeep(collection);\n    exportOpenCollection(transformCollectionToSaveToExportAsFile(collectionCopy));\n  };\n\n  const handleExportPostman = () => {\n    const collectionCopy = cloneDeep(collection);\n    exportPostmanCollection(collectionCopy);\n  };\n\n  const handleProceed = async () => {\n    if (isCollectionLoading || isExporting) return;\n\n    setIsExporting(true);\n    try {\n      switch (selectedFormat) {\n        case EXPORT_FORMATS.ZIP:\n          await handleExportZip();\n          break;\n        case EXPORT_FORMATS.YAML:\n          handleExportYaml();\n          break;\n        case EXPORT_FORMATS.POSTMAN:\n          handleExportPostman();\n          break;\n      }\n      onClose();\n    } catch (error) {\n      console.error('Export error:', error);\n    } finally {\n      setIsExporting(false);\n    }\n  };\n\n  const isDisabled = isCollectionLoading || isExporting;\n\n  return (\n    <Modal size=\"lg\" title=\"Share Collection\" handleCancel={onClose} hideFooter>\n      <StyledWrapper className=\"flex flex-col\">\n        <p className=\"text-sm mb-4\">\n          Bruno uses{' '}\n          <a\n            href=\"https://opencollection.com\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n            className=\"opencollection-link\"\n          >\n            OpenCollection\n          </a>\n          {' '}- An open format for API collections\n        </p>\n\n        {/* Bruno Format Section */}\n        <div className=\"section-title\">Bruno Format</div>\n        <div className=\"bruno-format-grid mb-6\">\n          {/* ZIP Option */}\n          <div\n            className={`format-card ${selectedFormat === EXPORT_FORMATS.ZIP ? 'selected' : ''} ${isDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}\n            onClick={() => !isDisabled && setSelectedFormat(EXPORT_FORMATS.ZIP)}\n          >\n            <div className=\"card-header\">\n              <span className=\"card-title\">Bruno Collection (ZIP)</span>\n              <span className=\"recommended-badge\">Recommended</span>\n            </div>\n            <p className=\"card-description\">OpenCollection format organized as folders and files</p>\n            <div className=\"feature-list\">\n              <div className=\"feature-item\">\n                <IconCheck size={14} className=\"checkmark\" />\n                <span>Folder structure with individual .yml files</span>\n              </div>\n              <div className=\"feature-item\">\n                <IconCheck size={14} className=\"checkmark\" />\n                <span>Collaborate with your team via pull requests</span>\n              </div>\n              <div className=\"feature-item\">\n                <IconCheck size={14} className=\"checkmark\" />\n                <span>Extract and open directly in Bruno</span>\n              </div>\n            </div>\n            <p className=\"best-for\">Best for: Team collaboration, version control, publishing</p>\n          </div>\n\n          {/* Single File YAML Option */}\n          <div\n            className={`format-card ${selectedFormat === EXPORT_FORMATS.YAML ? 'selected' : ''} ${isDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}\n            onClick={() => !isDisabled && setSelectedFormat(EXPORT_FORMATS.YAML)}\n          >\n            <div className=\"card-header\">\n              <span className=\"card-title\">Single File (YAML)</span>\n            </div>\n            <p className=\"card-description\">OpenCollection format bundled into one .yml file</p>\n            <div className=\"feature-list\">\n              <div className=\"feature-item\">\n                <IconCheck size={14} className=\"checkmark\" />\n                <span>Everything in a single YAML file</span>\n              </div>\n              <div className=\"feature-item\">\n                <IconCheck size={14} className=\"checkmark\" />\n                <span>Paste in a gist or attach to an issue</span>\n              </div>\n            </div>\n            <p className=\"best-for\">Best for: Quick sharing as a single file</p>\n          </div>\n        </div>\n\n        <div className=\"section-title\">Other Format</div>\n        <div className=\"other-format-grid\">\n          <div\n            className={`other-format-card ${selectedFormat === EXPORT_FORMATS.POSTMAN ? 'selected' : ''} ${isDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}\n            onClick={() => !isDisabled && setSelectedFormat(EXPORT_FORMATS.POSTMAN)}\n          >\n            <div className=\"format-icon\">\n              <IconFileExport size={28} strokeWidth={1.5} />\n            </div>\n            <div className=\"format-info\">\n              <div className=\"format-name\">Postman</div>\n              <div className=\"format-description\">Export for Postman</div>\n            </div>\n          </div>\n        </div>\n\n        {selectedFormat === EXPORT_FORMATS.POSTMAN && hasNonExportableRequestTypes.has && (\n          <div className=\"flex items-center mt-4 p-3 rounded\" style={{ backgroundColor: 'rgba(251, 191, 36, 0.1)' }}>\n            <IconAlertTriangle size={16} className=\"mr-2 flex-shrink-0\" style={{ color: '#f59e0b' }} />\n            <span className=\"text-sm\" style={{ color: '#f59e0b' }}>\n              Note: {hasNonExportableRequestTypes.types.join(', ')} requests in this collection will not be exported\n            </span>\n          </div>\n        )}\n\n        <div className=\"modal-footer\">\n          <Button\n            onClick={handleProceed}\n            disabled={isDisabled}\n            loading={isExporting}\n          >\n            {isExporting ? 'Exporting...' : 'Proceed'}\n          </Button>\n        </div>\n      </StyledWrapper>\n    </Modal>\n  );\n};\n\nexport default ShareCollection;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/ApiSpecs/ApiSpecItem/index.js",
    "content": "import { setActiveApiSpecUid } from 'providers/ReduxStore/slices/apiSpec';\nimport { showApiSpecPage as _showApiSpecPage } from 'providers/ReduxStore/slices/app';\nimport Dropdown from 'components/Dropdown';\nimport { IconDots, IconX } from '@tabler/icons';\nimport { useState, useRef } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport CloseApiSpec from '../CloseApiSpec/index';\nimport { forwardRef } from 'react';\n\nconst ApiSpecItem = ({ apiSpec }) => {\n  const dispatch = useDispatch();\n\n  const activeApiSpecUid = useSelector((state) => state.apiSpec.activeApiSpecUid);\n  const showApiSpecPage = useSelector((state) => state.app.showApiSpecPage);\n\n  const [closeApiSpecModal, setCloseApiSpecModal] = useState(false);\n\n  const dropdownTippyRef = useRef();\n  const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);\n\n  const handleOpenApiSpec = (apiSpec) => (e) => {\n    dispatch(_showApiSpecPage());\n    dispatch(setActiveApiSpecUid({ uid: apiSpec.uid }));\n  };\n\n  const MenuIcon = forwardRef((props, ref) => {\n    return (\n      <div ref={ref}>\n        <IconDots size={22} />\n      </div>\n    );\n  });\n\n  return (\n    <div\n      className={`flex flex-grow api-spec-item items-center h-full overflow-hidden w-full justify-between ${\n        showApiSpecPage && apiSpec?.uid == activeApiSpecUid ? 'active' : ''\n      }`}\n    >\n      {closeApiSpecModal && <CloseApiSpec apiSpec={apiSpec} onClose={() => setCloseApiSpecModal(false)} />}\n      <div\n        className=\"cursor-pointer py-2 pl-4 h-8 flex items-center flex-grow w-[80%] justify-between\"\n        onClick={handleOpenApiSpec(apiSpec)}\n      >\n        <span className=\"flex-nowrap whitespace-nowrap overflow-ellipsis overflow-hidden w-full\">{apiSpec?.name}</span>\n      </div>\n      <div className=\"menu-icon pr-2\">\n        <Dropdown onCreate={onDropdownCreate} icon={<MenuIcon />} placement=\"bottom-start\">\n          <div\n            className=\"dropdown-item close-item\"\n            onClick={(e) => {\n              dropdownTippyRef.current.hide();\n              setCloseApiSpecModal(true);\n            }}\n          >\n            <span className=\"dropdown-icon\">\n              <IconX size={16} strokeWidth={2} />\n            </span>\n            Remove\n          </div>\n        </Dropdown>\n      </div>\n    </div>\n  );\n};\n\nexport default ApiSpecItem;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/ApiSpecs/CloseApiSpec/index.js",
    "content": "import React from 'react';\nimport toast from 'react-hot-toast';\nimport Modal from 'components/Modal';\nimport { useDispatch } from 'react-redux';\nimport { IconFileCode } from '@tabler/icons';\nimport { closeApiSpecFile } from 'providers/ReduxStore/slices/apiSpec';\n\nconst CloseApiSpec = ({ onClose, apiSpec }) => {\n  const dispatch = useDispatch();\n\n  const onConfirm = () => {\n    dispatch(closeApiSpecFile({ uid: apiSpec.uid }))\n      .then(() => {\n        toast.success('API Spec closed');\n        onClose();\n      })\n      .catch(() => toast.error('An error occurred while closing the API Spec'));\n  };\n\n  return (\n    <Modal size=\"sm\" title=\"Close Api Spec\" confirmText=\"Close\" handleConfirm={onConfirm} handleCancel={onClose}>\n      <div className=\"flex items-center\">\n        <IconFileCode size={18} strokeWidth={1.5} />\n        <span className=\"ml-2 mr-4 font-semibold\">{apiSpec.name}</span>\n      </div>\n      <div className=\"break-words text-xs mt-1\">{apiSpec.pathname}</div>\n      <div className=\"mt-4\">\n        Are you sure you want to close API Spec <span className=\"font-semibold\">{apiSpec.name}</span> in Bruno?\n      </div>\n      <div className=\"mt-4\">\n        It will still be available in the file system at the above location and can be re-opened later.\n      </div>\n    </Modal>\n  );\n};\n\nexport default CloseApiSpec;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/ApiSpecs/CreateApiSpec/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .api-spec-file-extension {\n    color: ${(props) => props.theme.colors.text.darkOrange};\n  }\n  select {\n    background: ${(props) => props.theme.bg};\n  }\n  option {\n    background: ${(props) => props.theme.bg};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/ApiSpecs/CreateApiSpec/index.js",
    "content": "import React, { useRef, useEffect } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { useFormik } from 'formik';\nimport * as Yup from 'yup';\nimport { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';\nimport toast from 'react-hot-toast';\nimport Modal from 'components/Modal';\nimport { createApiSpecFile } from 'providers/ReduxStore/slices/apiSpec';\nimport { useState } from 'react';\nimport StyledWrapper from './StyledWrapper';\nimport { exportApiSpec } from 'utils/exporters/openapi-spec';\nimport { each } from 'lodash';\nimport { showApiSpecPage } from 'providers/ReduxStore/slices/app';\nimport { validateName, validateNameError } from 'utils/common/regex';\n\nexport const getEnvironmentVariablesKeyValuePairs = (envVariables) => {\n  let variables = {};\n  each(envVariables, (variable) => {\n    if (variable.name && variable.value && variable.enabled) {\n      variables[variable.name] = variable.value;\n    }\n  });\n  return variables;\n};\n\nconst CreateApiSpec = ({ onClose }) => {\n  const inputRef = useRef();\n  const dispatch = useDispatch();\n  const workspaces = useSelector((state) => state.workspaces.workspaces);\n  const activeWorkspaceUid = useSelector((state) => state.workspaces.activeWorkspaceUid);\n  const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);\n  const [defaultApiSpecLocation, setDefaultApiSpecLocation] = React.useState('');\n\n  const isDefaultWorkspace = !activeWorkspace || activeWorkspace.type === 'default';\n\n  React.useEffect(() => {\n    const getDefaultLocation = async () => {\n      if (activeWorkspace && activeWorkspace.pathname && activeWorkspace.type !== 'default') {\n        try {\n          const { ipcRenderer } = window;\n          const apiSpecPath = await ipcRenderer.invoke('renderer:ensure-apispec-folder', activeWorkspace.pathname);\n          setDefaultApiSpecLocation(apiSpecPath);\n        } catch (error) {\n          console.error('Error getting apispec folder:', error);\n        }\n      }\n    };\n    getDefaultLocation();\n  }, [activeWorkspace]);\n\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: {\n      importFrom: 'blank',\n      collectionLocation: '',\n      environment: '',\n      apiSpecName: '',\n      apiSpecLocation: defaultApiSpecLocation || ''\n    },\n    validationSchema: Yup.object({\n      importFrom: Yup.string().oneOf(['blank', 'collection']),\n      collectionLocation: Yup.string().min(1, 'location is required'),\n      environment: Yup.string(),\n      apiSpecName: Yup.string()\n        .min(1, 'Must be at least 1 character')\n        .max(255, 'Must be 255 characters or less')\n        .test('is-valid-filename', function (value) {\n          const isValid = validateName(value);\n          return isValid ? true : this.createError({ message: validateNameError(value) });\n        })\n        .required('Name is required'),\n      apiSpecLocation: Yup.string().min(1, 'location is required').required('location is required')\n    }),\n    onSubmit: async (values) => {\n      let yamlContent = '';\n      if (values?.importFrom === 'collection' && values?.collectionLocation && collectionData) {\n        const { files, envVariables, processEnvVariables } = collectionData;\n        let variables = {\n          processEnvVariables\n        };\n        // Get selected env's variables\n        if (values?.environment && values?.environment?.length) {\n          variables = {\n            ...getEnvironmentVariablesKeyValuePairs(envVariables[values?.environment] || {}),\n            ...variables\n          };\n        }\n        // Convert envVariables (keyed by filename) to environments array for multi-server export\n        const environmentsList = Object.entries(envVariables || {}).map(([envFile, vars]) => ({\n          name: envFile.replace(/\\.(bru|yml)$/, ''),\n          variables: vars\n        }));\n        // Create API spec yaml\n        let exportedYamlContentData = exportApiSpec({ name: values?.apiSpecName, variables, items: files, environments: environmentsList });\n        if (exportedYamlContentData?.content) {\n          yamlContent = exportedYamlContentData?.content;\n        }\n      }\n\n      dispatch(createApiSpecFile(`${values.apiSpecName}.yaml`, values.apiSpecLocation, yamlContent))\n        .then(() => {\n          setTimeout(() => {\n            dispatch(showApiSpecPage());\n          }, 200);\n          toast.success('ApiSpec created');\n          onClose();\n        })\n        .catch((err) => toast.error(err?.message));\n    }\n  });\n\n  const browse = () => {\n    dispatch(browseDirectory())\n      .then((dirPath) => {\n        // When the user closes the diolog without selecting anything dirPath will be false\n        if (typeof dirPath === 'string') {\n          formik.setFieldValue('apiSpecLocation', dirPath);\n        }\n      })\n      .catch((error) => {\n        formik.setFieldValue('apiSpecLocation', '');\n        console.error(error);\n      });\n  };\n\n  const browseCollection = () => {\n    dispatch(browseDirectory())\n      .then((dirPath) => {\n        if (typeof dirPath === 'string') {\n          formik.setFieldValue('collectionLocation', dirPath);\n        }\n      })\n      .catch((error) => {\n        formik.setFieldValue('collectionLocation', '');\n        console.error(error);\n      });\n  };\n\n  useEffect(() => {\n    if (inputRef && inputRef.current) {\n      inputRef.current.focus();\n    }\n  }, [inputRef]);\n\n  const [environments, setEnvironments] = useState([]);\n  const [collectionData, setCollectionData] = useState(null);\n\n  useEffect(() => {\n    const collectionLocation = formik.values.collectionLocation;\n    if (collectionLocation) {\n      const { ipcRenderer } = window;\n      ipcRenderer\n        .invoke('renderer:get-collection-json', collectionLocation)\n        .then(({ files, name, envVariables, processEnvVariables }) => {\n          setCollectionData({ name, files, envVariables, processEnvVariables });\n          const environments = envVariables || {};\n          const environmentNames = Object.keys(environments);\n          if (environmentNames?.length) {\n            setEnvironments(environments);\n            formik.setFieldValue('environment', environmentNames[0] || '');\n          }\n        })\n        .catch((err) => {\n          console.error('Error loading collection:', err);\n          toast.error('Failed to load collection');\n        });\n    }\n  }, [formik.values.collectionLocation]);\n\n  const onSubmit = () => formik.handleSubmit();\n\n  return (\n    <StyledWrapper>\n      <Modal size=\"md\" title=\"Create API Spec\" confirmText=\"Create\" handleConfirm={onSubmit} handleCancel={onClose}>\n        <form className=\"bruno-form\" onSubmit={(e) => e.preventDefault()}>\n          <div>\n            <label htmlFor=\"api-spec-location\" className=\"block font-semibold mb-2\">\n              Template\n            </label>\n            <div className=\"flex items-center\">\n              <input\n                id=\"blank\"\n                className=\"cursor-pointer\"\n                type=\"radio\"\n                name=\"importFrom\"\n                onChange={formik.handleChange}\n                value=\"blank\"\n                checked={formik.values.importFrom === 'blank'}\n              />\n              <label htmlFor=\"blank\" className=\"ml-1 cursor-pointer select-none\">\n                Blank spec\n              </label>\n              <input\n                id=\"collection\"\n                className=\"ml-4 cursor-pointer\"\n                type=\"radio\"\n                name=\"importFrom\"\n                onChange={formik.handleChange}\n                value=\"collection\"\n                checked={formik.values.importFrom === 'collection'}\n              />\n              <label htmlFor=\"collection\" className=\"ml-1 cursor-pointer select-none\">\n                From Bruno Collection\n              </label>\n            </div>\n            {formik.touched.importFrom && formik.errors.importFrom ? (\n              <div className=\"text-red-500\">{formik.errors.importFrom}</div>\n            ) : null}\n            {formik.values.importFrom === 'collection' ? (\n              <>\n                <label htmlFor=\"collection-location\" className=\"block font-semibold mt-3\">\n                  Collection Location\n                </label>\n                <input\n                  id=\"collection-location\"\n                  type=\"text\"\n                  name=\"collectionLocation\"\n                  readOnly={true}\n                  className=\"block textbox mt-2 w-full cursor-pointer\"\n                  autoComplete=\"off\"\n                  autoCorrect=\"off\"\n                  autoCapitalize=\"off\"\n                  spellCheck=\"false\"\n                  title={formik.values.collectionLocation || ''}\n                  value={formik.values.collectionLocation || ''}\n                  onClick={browseCollection}\n                />\n                {formik.touched.collectionLocation && formik.errors.collectionLocation ? (\n                  <div className=\"text-red-500\">{formik.errors.collectionLocation}</div>\n                ) : null}\n                <div className=\"mt-1\">\n                  <span className=\"text-link cursor-pointer hover:underline\" onClick={browseCollection}>\n                    Browse\n                  </span>\n                </div>\n                {environments && Object.keys(environments || {})?.length > 0 ? (\n                  <>\n                    <label htmlFor=\"api-spec-name\" className=\"flex items-center font-semibold mt-3\">\n                      Environment\n                    </label>\n                    <div className=\"relative\">\n                      <select\n                        value={formik.values.environment || ''}\n                        onChange={(e) => {\n                          formik.setFieldValue('environment', e.target.value);\n                        }}\n                        className=\"block textbox mt-2 w-full mousetrap\"\n                      >\n                        {Object.keys(environments).map((env) => (\n                          <option key={env} value={env}>\n                            {env}\n                          </option>\n                        ))}\n                      </select>\n                    </div>\n                  </>\n                ) : (\n                  <></>\n                )}\n              </>\n            ) : (\n              <></>\n            )}\n            {formik.touched.environment && formik.errors.environment ? (\n              <div className=\"text-red-500\">{formik.errors.environment}</div>\n            ) : null}\n            <label htmlFor=\"api-spec-name\" className=\"flex items-center font-semibold mt-3\">\n              Spec Name\n            </label>\n            <div className=\"relative\">\n              <input\n                id=\"api-spec-name\"\n                type=\"text\"\n                name=\"apiSpecName\"\n                ref={inputRef}\n                className=\"block textbox mt-2 !pr-11 w-full\"\n                onChange={(e) => {\n                  formik.handleChange(e);\n                }}\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n                value={formik.values.apiSpecName || ''}\n              />\n              <div className=\"absolute right-2 top-0 bottom-0 h-full flex items-center api-spec-file-extension\">\n                .yaml\n              </div>\n            </div>\n            {formik.touched.apiSpecName && formik.errors.apiSpecName ? (\n              <div className=\"text-red-500\">{formik.errors.apiSpecName}</div>\n            ) : null}\n\n            <label htmlFor=\"api-spec-location\" className=\"block font-semibold mt-3\">\n              Spec Location\n            </label>\n            <input\n              id=\"api-spec-location\"\n              type=\"text\"\n              name=\"apiSpecLocation\"\n              readOnly={true}\n              className=\"block textbox mt-2 w-full cursor-pointer\"\n              autoComplete=\"off\"\n              autoCorrect=\"off\"\n              autoCapitalize=\"off\"\n              spellCheck=\"false\"\n              title={formik.values.apiSpecLocation || ''}\n              value={formik.values.apiSpecLocation || ''}\n              onClick={browse}\n            />\n            {formik.touched.apiSpecLocation && formik.errors.apiSpecLocation ? (\n              <div className=\"text-red-500\">{formik.errors.apiSpecLocation}</div>\n            ) : null}\n            <div className=\"mt-1\">\n              <span className=\"text-link cursor-pointer hover:underline\" onClick={browse}>\n                Browse\n              </span>\n              {!isDefaultWorkspace && (\n                <span className=\"text-xs opacity-60 ml-2\">\n                  (defaults to workspace's apispec folder)\n                </span>\n              )}\n            </div>\n          </div>\n        </form>\n      </Modal>\n    </StyledWrapper>\n  );\n};\n\nexport default CreateApiSpec;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/ApiSpecs/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  flex: 1 1 0%;\n  min-height: 0;\n  height: 100%;\n  overflow: hidden;\n  padding-top: 4px;\n  padding-bottom: 4px;\n\n  .api-specs-list {\n    flex: 1 1 0%;\n    min-height: 0;\n    padding-top: 4px;\n    padding-bottom: 4px;\n    overflow-y: auto;\n    overflow-x: hidden;\n  }\n\n  .api-spec-item {\n    height: 1.6rem;\n    cursor: pointer;\n    &.active {\n      background: ${(props) => props.theme.sidebar.collection.item.bg};\n    }\n    &:hover {\n      background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n      .menu-icon {\n        .dropdown {\n          div[aria-expanded='false'] {\n            visibility: visible;\n          }\n        }\n      }\n    }\n  }\n\n  .menu-icon {\n    cursor: pointer;\n    color: ${(props) => props.theme.sidebar.dropdownIcon.color};\n\n    .dropdown {\n      div[aria-expanded='true'] {\n        visibility: visible;\n      }\n      div[aria-expanded='false'] {\n        visibility: hidden;\n      }\n    }\n  }\n\n  div.tippy-box {\n    position: relative;\n    top: -0.625rem;\n  }\n\n  .placeholder {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/ApiSpecs/index.js",
    "content": "import React from 'react';\nimport styled from 'styled-components';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { useTheme } from 'providers/Theme';\nimport { openApiSpec } from 'providers/ReduxStore/slices/apiSpec';\nimport ApiSpecItem from './ApiSpecItem';\nimport StyledWrapper from './StyledWrapper';\nimport toast from 'react-hot-toast';\n\nconst LinkStyle = styled.span`\n  color: ${(props) => props.theme['text-link']};\n`;\n\nconst ApiSpecs = () => {\n  const dispatch = useDispatch();\n  const { theme } = useTheme();\n  const allApiSpecs = useSelector((state) => state.apiSpec.apiSpecs);\n  const workspaces = useSelector((state) => state.workspaces.workspaces);\n  const activeWorkspaceUid = useSelector((state) => state.workspaces.activeWorkspaceUid);\n  const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);\n\n  const apiSpecs = React.useMemo(() => {\n    if (!activeWorkspace) return [];\n\n    const workspaceApiSpecs = activeWorkspace.apiSpecs || [];\n\n    // Map workspace API specs to loaded API specs from Redux store\n    return workspaceApiSpecs.map((ws) => {\n      const loadedApiSpec = allApiSpecs.find((apiSpec) => apiSpec.pathname === ws.path);\n      return loadedApiSpec;\n    }).filter(Boolean);\n  }, [allApiSpecs, activeWorkspace, activeWorkspace?.apiSpecs]);\n\n  const handleOpenApiSpec = () => {\n    dispatch(openApiSpec()).catch(\n      (err) => console.log(err) && toast.error('An error occurred while opening the API spec')\n    );\n  };\n\n  const OpenLink = () => (\n    <LinkStyle className=\"underline text-link cursor-pointer\" theme={theme} onClick={() => handleOpenApiSpec()}>\n      Open\n    </LinkStyle>\n  );\n\n  if (!apiSpecs || !apiSpecs.length) {\n    return (\n      <StyledWrapper>\n        <div className=\"text-xs text-center placeholder py-4\">\n          <div>No API Specs found.</div>\n          <div className=\"mt-2\">\n            <OpenLink /> API Spec.\n          </div>\n        </div>\n      </StyledWrapper>\n    );\n  }\n\n  return (\n    <StyledWrapper>\n      <div className=\"api-specs-list\">\n        {apiSpecs && apiSpecs.length\n          ? apiSpecs.map((apiSpec) => {\n              return <ApiSpecItem apiSpec={apiSpec} key={apiSpec.uid} />;\n            })\n          : null}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default ApiSpecs;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/BulkImportCollectionLocation/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { darken } from 'polished';\n\nconst StyledWrapper = styled.div`\n  .current-group {\n    background-color: ${(props) => props.theme.background.surface1};\n    border-radius: 4px;\n    padding: 0.4rem;\n    cursor: pointer;\n    border: 1px solid ${(props) => props.theme.background.surface2};\n  }\n\n  .current-group:hover {\n    background-color: ${(props) => darken(0.03, props.theme.background.surface1)};\n    border-color: ${(props) => darken(0.03, props.theme.background.surface2)};\n  }\n\n  /* Fix dropdown positioning */\n  [data-tippy-root] {\n    left: 0 !important;\n  }\n\n  .bruno-modal-footer {\n    padding-top: 0;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/BulkImportCollectionLocation/index.js",
    "content": "import React, { useRef, useEffect, useState, useMemo, forwardRef } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { useFormik } from 'formik';\nimport * as Yup from 'yup';\nimport path from 'utils/common/path';\nimport { browseDirectory, importCollection } from 'providers/ReduxStore/slices/collections/actions';\nimport Modal from 'components/Modal';\nimport { isElectron } from 'utils/common/platform';\nimport { IconX, IconLoader2, IconCheck, IconCaretDown } from '@tabler/icons';\nimport InfoTip from 'components/InfoTip/index';\nimport Help from 'components/Help';\nimport { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';\nimport Dropdown from 'components/Dropdown';\nimport { postmanToBruno } from 'utils/importers/postman-collection';\nimport { convertInsomniaToBruno } from 'utils/importers/insomnia-collection';\nimport { convertOpenapiToBruno } from 'utils/importers/openapi-collection';\nimport { processBrunoCollection } from 'utils/importers/bruno-collection';\nimport { wsdlToBruno } from '@usebruno/converters';\nimport StyledWrapper from './StyledWrapper';\nimport toast from 'react-hot-toast';\nimport get from 'lodash/get';\n\nconst STATUS = {\n  LOADING: 'loading',\n  SUCCESS: 'success',\n  ERROR: 'error'\n};\n\nconst IMPORT_TYPE = {\n  BULK: 'bulk',\n  MULTIPLE: 'multiple'\n};\n\nconst groupingOptions = [\n  { value: 'tags', label: 'Tags', description: 'Group requests by OpenAPI tags', testId: 'grouping-option-tags' },\n  { value: 'path', label: 'Paths', description: 'Group requests by URL path structure', testId: 'grouping-option-path' }\n];\n\n// Extract collection name from raw data\nconst getCollectionName = (format, rawData) => {\n  if (!rawData) return 'Collection';\n\n  switch (format) {\n    case 'openapi':\n      return rawData.info?.title || 'OpenAPI Collection';\n    case 'postman':\n      return rawData.info?.name || rawData.collection?.info?.name || 'Postman Collection';\n    case 'insomnia':\n      // For Insomnia v4 format, name is in the workspace resource\n      if (rawData.resources && Array.isArray(rawData.resources)) {\n        const workspace = rawData.resources.find((r) => r._type === 'workspace');\n        if (workspace?.name) {\n          return workspace.name;\n        }\n      }\n      // Fallback to root name property\n      return rawData.name || 'Insomnia Collection';\n    case 'bruno':\n      return rawData.name || 'Bruno Collection';\n    case 'wsdl':\n      return 'WSDL Collection';\n    default:\n      return 'Collection';\n  }\n};\n\n// Convert raw data to Bruno collection format\nconst convertCollection = async (format, rawData, groupingType) => {\n  let collection;\n\n  switch (format) {\n    case 'openapi':\n      collection = convertOpenapiToBruno(rawData, { groupBy: groupingType });\n      break;\n    case 'wsdl':\n      collection = await wsdlToBruno(rawData);\n      break;\n    case 'postman':\n      collection = await postmanToBruno(rawData);\n      break;\n    case 'insomnia':\n      collection = convertInsomniaToBruno(rawData);\n      break;\n    case 'bruno':\n      collection = await processBrunoCollection(rawData);\n      break;\n    default:\n      throw new Error('Unknown collection format');\n  }\n\n  return collection;\n};\n\nexport function normalizeName(name) {\n  if (typeof name !== 'string') {\n    return '';\n  }\n  return name.trim().toLowerCase();\n}\n\n/**\n * Generate a unique name by adding \"copy\" suffix if the name already exists.\n * @param {string} baseName - The original name\n * @param {function} checkExists - Function that returns true if name exists\n * @returns {string} - Unique name with \"copy\" suffix if needed\n */\nexport function generateUniqueName(baseName, checkExists) {\n  const normalizedBase = normalizeName(baseName);\n  if (!checkExists(normalizedBase)) {\n    return baseName;\n  }\n\n  let counter = 1;\n  let uniqueName = `${baseName} copy`;\n\n  while (checkExists(normalizeName(uniqueName))) {\n    counter++;\n    uniqueName = `${baseName} copy ${counter}`;\n  }\n\n  return uniqueName;\n}\n\nexport const BulkImportCollectionLocation = ({\n  onClose,\n  handleSubmit,\n  importData\n}) => {\n  const dispatch = useDispatch();\n  const dropdownTippyRef = useRef();\n\n  const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);\n  const preferences = useSelector((state) => state.app.preferences);\n  const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);\n  const isDefaultWorkspace = !activeWorkspace || activeWorkspace.type === 'default';\n  const defaultLocation = isDefaultWorkspace\n    ? get(preferences, 'general.defaultLocation', '')\n    : (activeWorkspace?.pathname ? path.join(activeWorkspace.pathname, 'collections') : '');\n\n  const [status, setStatus] = useState({});\n  const [errorMessages, setErrorMessages] = useState({});\n  const [importStarted, setImportStarted] = useState(false);\n  const [environmentStatus, setEnvironmentStatus] = useState({});\n  const [showErrorModal, setShowErrorModal] = useState(false);\n  const [selectedError, setSelectedError] = useState(null);\n  const [applyToGlobal, setApplyToGlobal] = useState(true);\n  const [applyToCollection, setApplyToCollection] = useState(false);\n  const [groupingType, setGroupingType] = useState('tags');\n  const [collectionFormat, setCollectionFormat] = useState('bru');\n  const [renamedCollectionNames, setRenamedCollectionNames] = useState({});\n  const [renamedEnvironmentNames, setRenamedEnvironmentNames] = useState({});\n\n  // Extract data based on import type\n  const importType = importData?.type;\n  const isBulkImport = importType === IMPORT_TYPE.BULK;\n  const isMultipleImport = importType === IMPORT_TYPE.MULTIPLE;\n\n  // For bulk import (ZIP files)\n  const importedCollectionFromBulk = isBulkImport ? importData.collection : [];\n  const importedEnvironmentFromBulk = isBulkImport ? (importData.environment || []) : [];\n\n  // For multiple files import\n  const filesData = isMultipleImport ? importData.filesData : [];\n  const hasOpenApiSpec = filesData.some((f) => f.type === 'openapi');\n\n  // Create unified collection structure for display\n  const importedCollection = isMultipleImport\n    ? filesData.map((fileData, index) => ({\n        uid: `file-${index}`,\n        name: getCollectionName(fileData.type, fileData.data),\n        _fileData: fileData\n      }))\n    : importedCollectionFromBulk;\n\n  const importedEnvironment = isBulkImport ? importedEnvironmentFromBulk : [];\n\n  const globalEnvironments = useSelector((state) => state?.globalEnvironments?.globalEnvironments);\n  const existingCollections = useSelector((state) => state?.collections?.collections || []);\n\n  // Initialize selected items based on import type\n  const [selectedCollections, setSelectedCollections] = useState(importedCollection.map((col) => col.uid));\n  const [selectedEnvironments, setSelectedEnvironments] = useState(isBulkImport ? importedEnvironmentFromBulk.map((env) => env.uid) : []);\n\n  const allCollectionsSelected = selectedCollections.length === importedCollection.length;\n  const allEnvironmentsSelected = selectedEnvironments.length === importedEnvironment.length;\n\n  // Sort collections to show selected items first, then unselected items\n  // This helps users see their selections at the top of the list\n  const sortedCollections = useMemo(() => {\n    const arr = [...importedCollection];\n    arr.sort((a, b) => {\n      const aSelected = selectedCollections.includes(a.uid);\n      const bSelected = selectedCollections.includes(b.uid);\n      // Convert boolean to number: true = 1, false = 0\n      // bSelected - aSelected means: selected items (1) come before unselected (0)\n      return Number(bSelected) - Number(aSelected);\n    });\n    return arr;\n  }, [importedCollection, selectedCollections]);\n\n  // Sort environments to show selected items first, then unselected items\n  // This helps users see their selections at the top of the list\n  const sortedEnvironments = useMemo(() => {\n    const arr = [...importedEnvironment];\n    arr.sort((a, b) => {\n      const aSelected = selectedEnvironments.includes(a.uid);\n      const bSelected = selectedEnvironments.includes(b.uid);\n      // selected (true) should come before unselected (false)\n      return Number(bSelected) - Number(aSelected);\n    });\n    return arr;\n  }, [importedEnvironment, selectedEnvironments]);\n\n  const importStatus = useMemo(() => {\n    const selectedSet = new Set(selectedCollections);\n    const totalSelected = selectedCollections.length;\n    const failedCount = Object.entries(status).reduce((acc, [uid, s]) => {\n      return selectedSet.has(uid) && s === STATUS.ERROR ? acc + 1 : acc;\n    }, 0);\n\n    return {\n      totalSelected,\n      failedCount\n    };\n  }, [status, selectedCollections]);\n\n  // Handlers\n  const handleCollectionToggle = (uid) => {\n    setSelectedCollections((prev) =>\n      prev.includes(uid) ? prev.filter((id) => id !== uid) : [...prev, uid]\n    );\n  };\n  const handleEnvironmentToggle = (uid) => {\n    setSelectedEnvironments((prev) =>\n      prev.includes(uid) ? prev.filter((id) => id !== uid) : [...prev, uid]\n    );\n  };\n  const handleSelectAllCollections = (e) => {\n    setSelectedCollections(e.target.checked ? importedCollection.map((col) => col.uid) : []);\n  };\n  const handleSelectAllEnvironments = (e) => {\n    setSelectedEnvironments(\n      e.target.checked ? importedEnvironment.map((env) => env.uid) : []\n    );\n  };\n\n  const onDropdownCreate = (ref) => {\n    dropdownTippyRef.current = ref;\n  };\n\n  const GroupingDropdownIcon = forwardRef((props, ref) => {\n    const selectedOption = groupingOptions.find((option) => option.value === groupingType);\n    return (\n      <div ref={ref} className=\"flex items-center justify-between w-full current-group\" data-testid=\"grouping-dropdown\">\n        <div>\n          <div className=\"text-sm font-medium text-gray-900 dark:text-gray-100\">{selectedOption.label}</div>\n        </div>\n        <IconCaretDown size={16} className=\"text-gray-400 ml-[0.25rem]\" fill=\"currentColor\" />\n      </div>\n    );\n  });\n\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: {\n      collectionLocation: defaultLocation\n    },\n    validationSchema: Yup.object({\n      collectionLocation: Yup.string()\n        .min(1, 'must be at least 1 character')\n        .max(500, 'must be 500 characters or less')\n        .required('Location is required')\n    }),\n    onSubmit: async (values) => {\n      let filteredCollections = [];\n      const selectedItems = importedCollection.filter((col) => selectedCollections.includes(col.uid));\n\n      if (isMultipleImport) {\n        // Convert selected files to collections at submit time\n        for (const item of selectedItems) {\n          try {\n            const collection = await convertCollection(item._fileData.type, item._fileData.data, groupingType);\n            if (collection) {\n              // Preserve the synthetic UID so status tracking, rename tracking,\n              // and UI rendering all use the same key\n              collection.uid = item.uid;\n              filteredCollections.push(collection);\n            }\n          } catch (err) {\n            console.warn(`Failed to convert file ${item._fileData.file.name}:`, err);\n          }\n        }\n      } else if (isBulkImport) {\n        // For bulk import, use selected collections directly\n        filteredCollections = selectedItems;\n      }\n\n      const initialStatus = {};\n      filteredCollections.forEach((col) => {\n        initialStatus[col.uid] = STATUS.LOADING;\n      });\n\n      setStatus(initialStatus);\n      setErrorMessages({});\n\n      const filteredEnvironments = importedEnvironment.filter((env) =>\n        selectedEnvironments.includes(env.uid)\n      );\n\n      // Handle duplicate collection names by renaming new ones to a unique \"{originalName} N\" suffix\n      const existingCollectionNames = new Set(existingCollections.map((col) => normalizeName(col.name)));\n      const usedNames = new Set();\n      const renamedNames = {};\n\n      filteredCollections.forEach((collection) => {\n        const originalName = collection.name;\n        let finalName = originalName;\n        let index = 0;\n\n        while (existingCollectionNames.has(normalizeName(finalName)) || usedNames.has(normalizeName(finalName))) {\n          finalName = `${originalName} ${index + 1}`;\n          index++;\n        }\n\n        collection.name = finalName;\n        usedNames.add(normalizeName(finalName));\n        // Store renamed name for summary display\n        if (finalName !== originalName) {\n          renamedNames[collection.uid] = finalName;\n        }\n      });\n\n      setRenamedCollectionNames(renamedNames);\n\n      // Process all selected environments and rename duplicates\n      // Don't use getUniqueEnvironments as it filters out duplicates - we want to rename them instead\n      const collectionRenamedEnvNames = {};\n      const globalRenamedEnvNames = {};\n\n      if (applyToCollection) {\n        // add selected environments to each selected collection\n        // Rename duplicates with \"copy\" suffix instead of filtering them out\n        filteredCollections.forEach((collection) => {\n          const existingNamesSet = new Set((collection.environments || []).map((e) => normalizeName(e?.name)));\n          const usedNamesInBatch = new Set();\n\n          const envsForCollection = filteredEnvironments.map((env) => {\n            const originalName = env.name;\n            const normalizedOriginalName = normalizeName(originalName);\n\n            // Check if name exists in collection or was already used in this batch\n            const checkExists = (name) => existingNamesSet.has(name) || usedNamesInBatch.has(name);\n            const finalName = generateUniqueName(originalName, checkExists);\n\n            // Track renamed name for summary display\n            if (finalName !== originalName) {\n              collectionRenamedEnvNames[env.uid] = finalName;\n            }\n\n            usedNamesInBatch.add(normalizeName(finalName));\n            existingNamesSet.add(normalizeName(finalName));\n            return { ...env, name: finalName };\n          });\n\n          collection.environments = envsForCollection;\n        });\n\n        // Mark all collection environments as success (they're processed with the collection import)\n        const envStatusUpdate = {};\n        filteredEnvironments.forEach((env) => {\n          envStatusUpdate[env.uid] = STATUS.SUCCESS;\n        });\n        setEnvironmentStatus((prev) => ({ ...prev, ...envStatusUpdate }));\n\n        if (Object.keys(collectionRenamedEnvNames).length > 0) {\n          setRenamedEnvironmentNames((prev) => ({ ...prev, ...collectionRenamedEnvNames }));\n        }\n      }\n\n      if (applyToGlobal && filteredEnvironments.length > 0) {\n        // Pre-compute unique names for all environments to avoid race conditions\n        const existingGlobalNames = new Set((globalEnvironments || []).map((env) => normalizeName(env?.name)));\n        const usedNamesInBatch = new Set();\n        const envsToImport = [];\n\n        filteredEnvironments.forEach((environment) => {\n          const checkExists = (name) => existingGlobalNames.has(name) || usedNamesInBatch.has(name);\n          const uniqueName = generateUniqueName(environment.name, checkExists);\n\n          if (uniqueName !== environment.name) {\n            globalRenamedEnvNames[environment.uid] = uniqueName;\n          }\n          usedNamesInBatch.add(normalizeName(uniqueName));\n          envsToImport.push({ ...environment, name: uniqueName });\n        });\n\n        if (Object.keys(globalRenamedEnvNames).length > 0) {\n          setRenamedEnvironmentNames((prev) => ({ ...prev, ...globalRenamedEnvNames }));\n        }\n\n        envsToImport.forEach((envToImport) => {\n          const originalUid = envToImport.uid;\n          setEnvironmentStatus((prev) => ({ ...prev, [originalUid]: STATUS.LOADING }));\n\n          dispatch(addGlobalEnvironment(envToImport))\n            .then(() => setEnvironmentStatus((prev) => ({ ...prev, [originalUid]: STATUS.SUCCESS })))\n            .catch((error) => {\n              setEnvironmentStatus((prev) => ({ ...prev, [originalUid]: STATUS.ERROR }));\n              setErrorMessages((prev) => ({ ...prev, [originalUid]: error.message || 'Failed to add environment' }));\n            });\n        });\n      }\n\n      setImportStarted(true);\n\n      if (filteredCollections.length > 1 || isBulkImport || isMultipleImport) {\n        dispatch(importCollection(filteredCollections, values.collectionLocation, { format: collectionFormat }))\n          .catch((err) => {\n            console.error('Failed to import collections', err);\n            filteredCollections.forEach((collection) => {\n              setStatus((prev) => ({ ...prev, [collection.uid]: STATUS.ERROR }));\n              setErrorMessages((prev) => ({ ...prev, [collection.uid]: err.message || 'Failed to import collection' }));\n            });\n          });\n      } else {\n        handleSubmit(filteredCollections[0], values.collectionLocation, { format: collectionFormat });\n      }\n    }\n  });\n\n  const browse = () => {\n    dispatch(browseDirectory())\n      .then((dirPath) => {\n        if (typeof dirPath === 'string' && dirPath.length > 0) {\n          formik.setFieldValue('collectionLocation', dirPath);\n        }\n      })\n      .catch((error) => {\n        formik.setFieldValue('collectionLocation', '');\n        console.error(error);\n      });\n  };\n\n  useEffect(() => {\n    if (!isElectron()) {\n      return () => {};\n    }\n\n    const { ipcRenderer } = window;\n\n    const handleImportStatus = (collectionId, status, errorMessage = '') => {\n      setStatus((prev) => ({ ...prev, [collectionId]: status }));\n      if (status === STATUS.ERROR) {\n        setErrorMessages((prev) => ({\n          ...prev,\n          [collectionId]: errorMessage\n        }));\n      }\n    };\n\n    const importingCollectionStarted = ipcRenderer.on(\n      'main:collection-import-started',\n      (collectionId) => {\n        handleImportStatus(collectionId, STATUS.LOADING);\n      }\n    );\n    const importingCollectionCompleted = ipcRenderer.on(\n      'main:collection-import-ended',\n      (collectionId) => {\n        handleImportStatus(collectionId, STATUS.SUCCESS);\n      }\n    );\n    const importingCollectionFailed = ipcRenderer.on(\n      'main:collection-import-failed',\n      (collectionId, { message }) => {\n        handleImportStatus(collectionId, STATUS.ERROR, message);\n      }\n    );\n    const allCollectionsImportCompleted = ipcRenderer.on(\n      'main:all-collections-import-ended',\n      (report) => {\n        toast.success(report?.message);\n      }\n    );\n    return () => {\n      importingCollectionStarted();\n      importingCollectionCompleted();\n      importingCollectionFailed();\n      allCollectionsImportCompleted();\n    };\n  }, []);\n\n  const onSubmit = () => {\n    if (importStarted) {\n      onClose();\n    } else {\n      formik.handleSubmit();\n    }\n  };\n\n  const handleErrorClick = (error, uid) => {\n    setSelectedError({ message: error, uid });\n    setShowErrorModal(true);\n  };\n\n  const ErrorModal = ({ error, onClose }) => (\n    <Modal\n      size=\"sm\"\n      title=\"Error Details\"\n      handleConfirm={onClose}\n      handleCancel={onClose}\n      showCancelButton={false}\n      disableCloseOnOutsideClick={true}\n      hideFooter={true}\n    >\n      <div className=\"p-4\">\n        <pre className=\"whitespace-pre-wrap text-red-600 text-sm\">{error}</pre>\n      </div>\n    </Modal>\n  );\n\n  return (\n    <StyledWrapper>\n      <Modal\n        size=\"md\"\n        title=\"Bulk Import\"\n        confirmText={importStarted ? 'Close' : 'Import'}\n        confirmDisabled={Boolean(!selectedCollections?.length)}\n        handleConfirm={onSubmit}\n        handleCancel={onClose}\n        showConfirm={true}\n        disableCloseOnOutsideClick={true}\n        disableEscapeKey={false}\n        hideCancel={importStarted}\n      >\n        <form className=\"bruno-form\" onSubmit={(e) => e.preventDefault()}>\n          <div className=\"flex flex-col\">\n            {importStarted ? (\n              <>\n                <div className=\"mb-6\">\n                  <div className=\"flex items-center justify-between relative mb-5 w-full\">\n                    <div className=\"font-semibold\">Location</div>\n                    <div className=\"text-sm border border-slate-600 rounded px-3 py-1.5 ml-4 flex-1\">\n                      {formik.values.collectionLocation\n                        || 'No location selected'}\n                    </div>\n                  </div>\n\n                  <div className=\"flex items-center justify-between mb-2\">\n                    <div className=\"font-semibold\">\n                      Importing Collections ({importStatus.totalSelected})\n                    </div>\n                    {importStatus.failedCount > 0 && importStatus.totalSelected > 0 && (\n                      <div className=\"text-sm text-red-500\">\n                        ({importStatus.failedCount}/{importStatus.totalSelected} failed)\n                      </div>\n                    )}\n                  </div>\n                  <div className=\"max-h-[180px] overflow-y-scroll border border-slate-600 rounded-md py-2 scrollbar-visible\">\n                    {sortedCollections\n                      .filter((collection) =>\n                        selectedCollections.includes(collection.uid)\n                      )\n                      .map((collection) => (\n                        <div\n                          key={collection.uid}\n                          className=\"flex items-center px-4 py-1.5 text-sm font-normal justify-between\"\n                        >\n                          <div className=\"flex items-center flex-1\">\n                            <div className=\"flex items-center mr-2\">\n                              {status[collection.uid] === STATUS.LOADING && (\n                                <IconLoader2\n                                  className=\"animate-spin text-blue-500\"\n                                  size={16}\n                                  strokeWidth={1.5}\n                                />\n                              )}\n                              {status[collection.uid] === STATUS.SUCCESS && (\n                                <div className=\"flex items-center text-green-500\">\n                                  <IconCheck size={16} strokeWidth={1.5} />\n                                </div>\n                              )}\n                              {status[collection.uid] === STATUS.ERROR && (\n                                <div className=\"flex items-center\">\n                                  <IconX\n                                    className=\"text-red-500\"\n                                    size={16}\n                                    strokeWidth={1.5}\n                                  />\n                                </div>\n                              )}\n                            </div>\n                            <span>{renamedCollectionNames[collection.uid] || collection.name}</span>\n                          </div>\n                          {status[collection.uid] === STATUS.ERROR && (\n                            <button\n                              onClick={() =>\n                                handleErrorClick(\n                                  errorMessages[collection.uid],\n                                  collection.uid\n                                )}\n                              className=\"text-red-500 text-sm hover:underline\"\n                            >\n                              See error\n                            </button>\n                          )}\n                        </div>\n                      ))}\n                  </div>\n                </div>\n\n                {selectedEnvironments.length > 0 && (\n                  <div className=\"mb-6\">\n                    <div className=\"font-semibold mb-2\">\n                      Importing Environments ({selectedEnvironments.length})\n                    </div>\n                    <div className=\"max-h-[180px] overflow-y-scroll border border-slate-600 rounded-md py-2 scrollbar-visible\">\n                      {sortedEnvironments\n                        .filter((env) => selectedEnvironments.includes(env.uid))\n                        .map((env) => (\n                          <div\n                            key={env.uid}\n                            className=\"flex items-center px-4 py-1.5 text-sm font-normal justify-between\"\n                          >\n                            <div className=\"flex items-center flex-1\">\n                              <div className=\"flex items-center mr-2\">\n                                {!environmentStatus[env.uid] || environmentStatus[env.uid] === STATUS.LOADING ? (\n                                  <IconLoader2\n                                    className=\"animate-spin text-blue-500\"\n                                    size={16}\n                                    strokeWidth={1.5}\n                                  />\n                                ) : environmentStatus[env.uid] === STATUS.SUCCESS ? (\n                                  <div className=\"flex items-center text-green-500\">\n                                    <IconCheck size={16} strokeWidth={1.5} />\n                                  </div>\n                                ) : environmentStatus[env.uid] === STATUS.ERROR ? (\n                                  <div className=\"flex items-center\">\n                                    <IconX\n                                      className=\"text-red-500\"\n                                      size={16}\n                                      strokeWidth={1.5}\n                                    />\n                                  </div>\n                                ) : null}\n                              </div>\n                              <span>{renamedEnvironmentNames[env.uid] || env.name}</span>\n                            </div>\n                            {environmentStatus[env.uid] === STATUS.ERROR && (\n                              <button\n                                onClick={() =>\n                                  handleErrorClick(\n                                    errorMessages[env.uid],\n                                    env.uid\n                                  )}\n                                className=\"text-red-500 text-sm hover:underline\"\n                              >\n                                See error\n                              </button>\n                            )}\n                          </div>\n                        ))}\n                    </div>\n                  </div>\n                )}\n              </>\n            ) : (\n              <>\n                <div className=\"mb-6\">\n                  <div className=\"font-semibold mb-2 flex justify-between items-center\">\n                    <span>Collections ({importedCollection.length})</span>\n                    <label className=\"flex items-center text-sm font-normal select-none cursor-pointer\">\n                      <input\n                        type=\"checkbox\"\n                        checked={allCollectionsSelected}\n                        onChange={handleSelectAllCollections}\n                        className=\"mr-2\"\n                      />\n                      Select All\n                    </label>\n                  </div>\n                  <div className=\"max-h-[180px] overflow-y-scroll border border-slate-600 rounded-md py-2\">\n                    {importedCollection.length === 0 && (\n                      <div className=\"px-4 py-2 text-gray-400 italic\">\n                        No collections found\n                      </div>\n                    )}\n                    {sortedCollections.map((collection) => (\n                      <label\n                        key={collection.uid}\n                        className=\"flex items-center px-4 py-1.5 text-sm font-normal select-none cursor-pointer justify-between\"\n                      >\n                        <div className=\"flex items-center flex-1\">\n                          <input\n                            type=\"checkbox\"\n                            checked={selectedCollections.includes(collection.uid)}\n                            onChange={() => handleCollectionToggle(collection.uid)}\n                            className=\"mr-3\"\n                          />\n                          <span>{collection.name}</span>\n                        </div>\n                      </label>\n                    ))}\n                  </div>\n                </div>\n\n                {importType === 'bulk' && (\n                  <>\n                    <div className=\"mb-4\">\n                      <div className=\"font-semibold mb-2 flex justify-between items-center\">\n                        <span>Environments ({importedEnvironment.length})</span>\n                        <label className=\"flex items-center text-sm font-normal select-none cursor-pointer\">\n                          <input\n                            type=\"checkbox\"\n                            checked={allEnvironmentsSelected}\n                            onChange={handleSelectAllEnvironments}\n                            className=\"mr-2\"\n                          />\n                          Select All\n                        </label>\n                      </div>\n                      <div className=\"max-h-[180px] overflow-y-scroll border border-slate-600 rounded-md py-2 scrollbar-visible\">\n                        {importedEnvironment.length === 0 && (\n                          <div className=\"px-4 py-2 text-gray-400 italic\">\n                            No environments found\n                          </div>\n                        )}\n                        {sortedEnvironments.map((env) => (\n                          <label\n                            key={env.uid}\n                            className=\"flex items-center px-4 py-1.5 text-sm font-normal select-none cursor-pointer\"\n                          >\n                            <input\n                              type=\"checkbox\"\n                              checked={selectedEnvironments.includes(env.uid)}\n                              onChange={() => handleEnvironmentToggle(env.uid)}\n                              className=\"mr-3\"\n                            />\n                            <span>{env.name}</span>\n                          </label>\n                        ))}\n                      </div>\n                    </div>\n\n                    <div className=\"mb-6\">\n                      <div className=\"font-semibold mb-2\">\n                        Environment Assignment\n                      </div>\n                      <div className=\"flex gap-8 mt-2 ml-2\">\n                        <label className=\"flex items-center\">\n                          <input\n                            type=\"checkbox\"\n                            checked={applyToGlobal}\n                            onChange={(e) => setApplyToGlobal(e.target.checked)}\n                            className=\"mr-2\"\n                          />\n                          <span className=\"ml-2\">\n                            Global Environment\n                            <InfoTip\n                              content=\"Environments will be imported and stored as global, accessible across collections.\"\n                              infotipId=\"apply-to-global-infotip\"\n                            />\n                          </span>\n                        </label>\n                        <label className=\"flex items-center\">\n                          <input\n                            type=\"checkbox\"\n                            checked={applyToCollection}\n                            onChange={(e) =>\n                              setApplyToCollection(e.target.checked)}\n                            className=\"mr-2\"\n                          />\n                          <span className=\"ml-2\">\n                            Duplicate Across Collections\n                            <InfoTip\n                              content=\"Each imported collection will receive its own copy of the environments.\"\n                              infotipId=\"apply-to-each-infotip\"\n                            />\n                          </span>\n                        </label>\n                      </div>\n                    </div>\n                  </>\n                )}\n\n                <div className=\"flex items-start flex-col relative\">\n                  <div className=\"font-semibold mb-2\">Location</div>\n                  <input\n                    id=\"collection-location\"\n                    type=\"text\"\n                    placeholder=\"Select a location to save the collection\"\n                    name=\"collectionLocation\"\n                    className=\"block textbox w-full cursor-pointer\"\n                    autoComplete=\"off\"\n                    autoCorrect=\"off\"\n                    autoCapitalize=\"off\"\n                    spellCheck=\"false\"\n                    value={formik.values.collectionLocation || ''}\n                    onClick={browse}\n                    onChange={(e) => {\n                      formik.setFieldValue('collectionLocation', e.target.value);\n                    }}\n                  />\n                  {formik.touched.collectionLocation && formik.errors.collectionLocation ? (\n                    <div className=\"text-red-500 mt-1\">\n                      {formik.errors.collectionLocation}\n                    </div>\n                  ) : null}\n                  <div className=\"mt-1\">\n                    <span className=\"text-link cursor-pointer hover:underline\" onClick={browse}>\n                      Browse\n                    </span>\n                  </div>\n                </div>\n\n                <div className=\"mt-4\">\n                  <label htmlFor=\"format\" className=\"flex items-center font-semibold\">\n                    File Format\n                    <Help width=\"300\">\n                      <p>Choose the file format for storing requests in this collection.</p>\n                      <p className=\"mt-2\">\n                        <strong>OpenCollection (YAML):</strong> Industry-standard YAML format (.yml files)\n                      </p>\n                      <p className=\"mt-1\">\n                        <strong>BRU:</strong> Bruno's native file format (.bru files)\n                      </p>\n                    </Help>\n                  </label>\n                  <select\n                    id=\"format\"\n                    name=\"format\"\n                    className=\"block textbox mt-2 w-full\"\n                    value={collectionFormat}\n                    onChange={(e) => setCollectionFormat(e.target.value)}\n                  >\n                    <option value=\"yml\">OpenCollection (YAML)</option>\n                    <option value=\"bru\">BRU Format (.bru)</option>\n                  </select>\n                </div>\n\n                {isMultipleImport && hasOpenApiSpec && (\n                  <div>\n                    <div className=\"flex gap-4 items-center mt-4\">\n                      <div>\n                        <label htmlFor=\"groupingType\" className=\"block font-semibold\">\n                          Folder arrangement\n                        </label>\n                        <p className=\"text-sm text-gray-600 dark:text-gray-400 mt-1 mb-2\">\n                          Select whether to create folders according to the spec's paths or tags.\n                        </p>\n                      </div>\n                      <div className=\"relative\">\n                        <Dropdown onCreate={onDropdownCreate} icon={<GroupingDropdownIcon />} placement=\"bottom-start\">\n                          {groupingOptions.map((option) => (\n                            <div\n                              key={option.value}\n                              className=\"dropdown-item\"\n                              data-testid={option.testId}\n                              onClick={() => {\n                                dropdownTippyRef?.current?.hide();\n                                setGroupingType(option.value);\n                              }}\n                            >\n                              {option.label}\n                            </div>\n                          ))}\n                        </Dropdown>\n                      </div>\n                    </div>\n                  </div>\n                )}\n              </>\n            )}\n          </div>\n        </form>\n      </Modal>\n\n      {showErrorModal && (\n        <ErrorModal\n          error={selectedError?.message}\n          onClose={() => setShowErrorModal(false)}\n        />\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default BulkImportCollectionLocation;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/BulkImportCollectionLocation/index.spec.js",
    "content": "import { normalizeName, generateUniqueName } from './index';\n\ndescribe('BulkImportCollectionLocation helpers', () => {\n  describe('normalizeName', () => {\n    it('should trim and lowercase names', () => {\n      expect(normalizeName('  Beta  ')).toBe('beta');\n      expect(normalizeName('TEST')).toBe('test');\n      expect(normalizeName(null)).toBe('');\n    });\n  });\n\n  describe('generateUniqueName', () => {\n    it('should return original name if no conflict', () => {\n      const checkExists = () => false;\n      expect(generateUniqueName('Beta', checkExists)).toBe('Beta');\n    });\n\n    it('should add \"copy\" suffix on first conflict', () => {\n      const existing = new Set(['beta']);\n      const checkExists = (name) => existing.has(name);\n      expect(generateUniqueName('Beta', checkExists)).toBe('Beta copy');\n    });\n\n    it('should increment copy number on multiple conflicts', () => {\n      const existing = new Set(['beta', 'beta copy']);\n      const checkExists = (name) => existing.has(name);\n      expect(generateUniqueName('Beta', checkExists)).toBe('Beta copy 2');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/CloneGitRespository/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .info-box {\n    background-color: ${(props) => props.theme.background.mantle};\n    color: ${(props) => props.theme.text};\n    border: 1px solid ${(props) => props.theme.border.border2};\n    padding: 10px;\n    border-radius: 5px;\n    margin-top: 5px;\n    width: 400px;\n    white-space: pre-wrap;\n    max-height: 150px;\n    overflow-y: auto;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/CloneGitRespository/index.js",
    "content": "import React, { useRef, useEffect, useState } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { useFormik } from 'formik';\nimport * as Yup from 'yup';\nimport {\n  browseDirectory,\n  cloneGitRepository,\n  openMultipleCollections,\n  scanForBrunoFiles\n} from 'providers/ReduxStore/slices/collections/actions';\nimport { removeGitOperationProgress } from 'providers/ReduxStore/slices/app';\nimport Modal from 'components/Modal';\nimport path from 'utils/common/path';\nimport Portal from 'components/Portal';\nimport { IconRefresh, IconCheck, IconAlertCircle, IconBrandGit } from '@tabler/icons';\nimport { uuid } from 'utils/common/index';\nimport StyledWrapper from './StyledWrapper';\nimport { getRepoNameFromUrl } from 'utils/git';\nimport GitNotFoundModal from 'components/Git/GitNotFoundModal/index';\nimport get from 'lodash/get';\n\nconst CloneGitRepository = ({ onClose, onFinish, collectionRepositoryUrl = null }) => {\n  const [collectionPaths, setCollectionPaths] = useState([]);\n  const [selectedCollectionPaths, setSelectedCollectionPaths] = useState([]);\n  const [processUid, setProcessUid] = useState(uuid());\n  const [steps, setSteps] = useState([]);\n  const [view, setView] = useState('form');\n\n  const progressData = useSelector((state) => state.app.gitOperationProgress[processUid]);\n  const { gitVersion } = useSelector((state) => state.app);\n  const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);\n  const preferences = useSelector((state) => state.app.preferences);\n  const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);\n  const isDefaultWorkspace = !activeWorkspace || activeWorkspace.type === 'default';\n  const defaultLocation = isDefaultWorkspace\n    ? get(preferences, 'general.defaultLocation', '')\n    : (activeWorkspace?.pathname ? path.join(activeWorkspace.pathname, 'collections') : '');\n  const inputRef = useRef();\n  const dispatch = useDispatch();\n\n  useEffect(() => {\n    if (progressData) {\n      setSteps((prev) =>\n        prev.map((step) =>\n          step.step === 'clone' && !step?.completed\n            ? { ...step, title: 'Cloning repository', completed: false, info: progressData.progressData }\n            : step\n        )\n      );\n    }\n  }, [progressData]);\n\n  useEffect(() => {\n    if (inputRef?.current) {\n      inputRef.current.focus();\n    }\n  }, []);\n\n  const cloneInProgress = () => {\n    setSteps((prev) => [\n      ...prev,\n      {\n        step: 'clone',\n        title: 'Cloning repository',\n        completed: false\n      }\n    ]);\n  };\n\n  const cloneFinished = () => {\n    setSteps((prev) =>\n      prev.map((step) =>\n        step.step === 'clone'\n          ? { ...step, title: 'Cloning successful', completed: true, info: '' }\n          : step\n      )\n    );\n  };\n\n  const cloneError = () => {\n    setSteps((prev) =>\n      prev.map((step) =>\n        step.step === 'clone'\n          ? { ...step, title: 'Cloning failed', completed: true, error: true }\n          : step\n      )\n    );\n  };\n\n  const scanInProgress = () => {\n    setSteps((prev) => [\n      ...prev,\n      {\n        step: 'scan',\n        title: 'Scanning for Bruno files',\n        completed: false\n      }\n    ]);\n  };\n\n  const scanFinished = () => {\n    setSteps((prev) =>\n      prev.map((step) =>\n        step.step === 'scan' ? { ...step, title: 'Scan successful', completed: true, info: '' } : step\n      )\n    );\n  };\n\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: {\n      repositoryUrl: collectionRepositoryUrl || '',\n      collectionLocation: defaultLocation\n    },\n    validationSchema: Yup.object({\n      repositoryUrl: Yup.string().required('Repository URL is required'),\n      collectionLocation: Yup.string().min(1, 'Location is required').required('Location is required')\n    }),\n    onSubmit: async (values) => {\n      try {\n        setView('progress');\n        cloneInProgress();\n        const { repositoryUrl, collectionLocation } = values;\n\n        const repoName = getRepoNameFromUrl(repositoryUrl);\n        const targetPath = path.join(collectionLocation, repoName);\n\n        await dispatch(cloneGitRepository({ url: values.repositoryUrl, path: targetPath, processUid }));\n\n        cloneFinished();\n        dispatch(removeGitOperationProgress(processUid));\n\n        scanInProgress();\n        const foundCollectionPaths = await dispatch(scanForBrunoFiles(targetPath));\n\n        scanFinished();\n        setCollectionPaths(foundCollectionPaths);\n      } catch (err) {\n        cloneError();\n        dispatch(removeGitOperationProgress(processUid));\n        console.error(err);\n      }\n    }\n  });\n\n  const browse = () => {\n    dispatch(browseDirectory())\n      .then((dirPath) => {\n        if (typeof dirPath === 'string') {\n          formik.setFieldValue('collectionLocation', dirPath);\n        }\n      })\n      .catch((error) => {\n        formik.setFieldValue('collectionLocation', '');\n        console.error(error);\n      });\n  };\n\n  const handleCollectionSelect = (collection) => {\n    setSelectedCollectionPaths((prevSelected) =>\n      prevSelected.includes(collection)\n        ? prevSelected.filter((c) => c !== collection)\n        : [...prevSelected, collection]\n    );\n  };\n\n  const getRelativePath = (fullPath, pathname) => {\n    let relativePath = path.relative(fullPath, pathname);\n    const { dir, name } = path.parse(relativePath);\n    return path.join(dir, name);\n  };\n\n  const isScanCompleted = () => steps.some((step) => step.step === 'scan' && step.completed);\n\n  const isConfirmDisabled = () => isScanCompleted() && collectionPaths?.length > 0 && selectedCollectionPaths?.length === 0;\n\n  const isFooterHidden = () => steps.some((step) => !step.completed);\n\n  const isError = () => steps.some((step) => step.error);\n\n  const handleConfirm = () => {\n    const buttonText = getConfirmText();\n    switch (buttonText) {\n      case 'Clone':\n        formik.handleSubmit();\n        break;\n      case 'Close':\n        onClose();\n        break;\n      case 'Open':\n        if (collectionPaths.length > 0 && selectedCollectionPaths.length > 0) {\n          dispatch(openMultipleCollections(selectedCollectionPaths));\n          onClose();\n          onFinish();\n        }\n        break;\n      default:\n        break;\n    }\n  };\n\n  const getConfirmText = () =>\n    !steps.length\n      ? 'Clone'\n      : steps.some((step) => !step.completed || step.error || (isScanCompleted() && !collectionPaths?.length))\n        ? 'Close'\n        : 'Open';\n\n  const handleBackButtonClick = () => {\n    setView('form');\n    setSteps([]);\n    setSelectedCollectionPaths([]);\n  };\n\n  if (!gitVersion) {\n    return <GitNotFoundModal onClose={onClose} />;\n  }\n\n  return (\n    <Portal id=\"clone-repository-portal\">\n      <Modal\n        size=\"md\"\n        title=\"Clone Git Repository\"\n        confirmText={getConfirmText()}\n        handleConfirm={handleConfirm}\n        handleCancel={onClose}\n        confirmDisabled={isConfirmDisabled()}\n        hideFooter={isFooterHidden()}\n        hideCancel={isError() || (isScanCompleted() && !collectionPaths?.length)}\n        showBackButton={isError()}\n        handleBack={handleBackButtonClick}\n      >\n        <StyledWrapper>\n          {view === 'form' && (\n            <form className=\"bruno-form\" onSubmit={(e) => e.preventDefault()}>\n              <div>\n                {collectionRepositoryUrl\n                  ? (\n                      <div className=\"flex items-start\">\n                        <div className=\"flex-shrink-0 p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg\">\n                          <IconBrandGit className=\"w-6 h-6 text-purple-500\" stroke={1.5} />\n                        </div>\n                        <div className=\"ml-4\">\n                          <div className=\"font-semibold text-sm\">{getRepoNameFromUrl(collectionRepositoryUrl)}</div>\n                          <div className=\"mt-1 text-xs text-muted font-mono\">\n                            {collectionRepositoryUrl}\n                          </div>\n                        </div>\n                      </div>\n                    )\n                  : (\n                      <>\n                        <label htmlFor=\"repository-url\" className=\"flex items-center font-semibold\">\n                          Git Repository URL\n                        </label>\n                        <input\n                          id=\"repository-url\"\n                          type=\"text\"\n                          name=\"repositoryUrl\"\n                          ref={inputRef}\n                          className=\"block textbox mt-2 w-full\"\n                          autoComplete=\"off\"\n                          autoCorrect=\"off\"\n                          autoCapitalize=\"off\"\n                          spellCheck=\"false\"\n                          onChange={formik.handleChange}\n                          value={formik.values.repositoryUrl || ''}\n                        />\n                      </>\n                    )}\n                {formik.touched.repositoryUrl && formik.errors.repositoryUrl && (\n                  <div className=\"text-red-500\">{formik.errors.repositoryUrl}</div>\n                )}\n                <label htmlFor=\"collection-location\" className=\"block font-semibold mt-3\">\n                  Location\n                </label>\n                <input\n                  id=\"collection-location\"\n                  type=\"text\"\n                  name=\"collectionLocation\"\n                  readOnly\n                  className=\"block textbox mt-2 w-full cursor-pointer\"\n                  autoComplete=\"off\"\n                  autoCorrect=\"off\"\n                  autoCapitalize=\"off\"\n                  spellCheck=\"false\"\n                  value={formik.values.collectionLocation || ''}\n                  onClick={browse}\n                />\n                {formik.touched.collectionLocation && formik.errors.collectionLocation && (\n                  <div className=\"text-red-500\">{formik.errors.collectionLocation}</div>\n                )}\n                <div className=\"mt-1\">\n                  <span className=\"text-link cursor-pointer hover:underline\" onClick={browse}>\n                    Browse\n                  </span>\n                </div>\n              </div>\n            </form>\n          )}\n          {view === 'progress' && (\n            <>\n              {steps.length > 0 && (\n                <div className=\"mt-4\">\n                  <ul>\n                    {steps.map((step, index) => (\n                      <li key={index} className=\"flex-col items-center space-x-2 mt-1\">\n                        <div className=\"flex\">\n                          {step.error ? (\n                            <IconAlertCircle className=\"text-red-500\" size={18} strokeWidth={1.5} />\n                          ) : (\n                            <>\n                              {step.completed ? (\n                                <IconCheck className=\"text-green-500\" size={18} strokeWidth={1.5} />\n                              ) : (\n                                <IconRefresh className=\"text-yellow-500 animate-spin\" size={18} strokeWidth={1.5} />\n                              )}\n                            </>\n                          )}\n                          <span className=\"ml-2\">{step.title}</span>\n                        </div>\n                        {step.info && (\n                          <div className=\"w-full mt-2\">\n                            <pre className=\"info-box ml-4\">{step.info}</pre>\n                          </div>\n                        )}\n                      </li>\n                    ))}\n                  </ul>\n                </div>\n              )}\n              {isScanCompleted() && (\n                <div className=\"mt-4 mb-4\">\n                  {collectionPaths.length === 0 && (\n                    <div className=\"flex\">\n                      <IconAlertCircle className=\"text-yellow-500\" size={18} strokeWidth={1.5} />\n                      <h3 className=\"text-sm ml-2\">No bruno collections found in this repository.</h3>\n                    </div>\n                  )}\n                  {collectionPaths.length > 0 && (\n                    <>\n                      <h3 className=\"text-sm mb-2\">\n                        {collectionPaths.length} bruno collections found. Please select the collections to open:\n                      </h3>\n                      <ul>\n                        {collectionPaths.map((collection) => (\n                          <li key={collection} className=\"mb-2\">\n                            <label className=\"flex items-center space-x-2\">\n                              <input\n                                type=\"checkbox\"\n                                checked={selectedCollectionPaths.includes(collection)}\n                                onChange={() => handleCollectionSelect(collection)}\n                                className=\"form-checkbox\"\n                              />\n                              <span>{getRelativePath(formik.values.collectionLocation, collection)}</span>\n                            </label>\n                          </li>\n                        ))}\n                      </ul>\n                    </>\n                  )}\n                </div>\n              )}\n            </>\n          )}\n        </StyledWrapper>\n      </Modal>\n    </Portal>\n  );\n};\n\nexport default CloneGitRepository;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/CloseWorkspace/index.js",
    "content": "import React from 'react';\nimport toast from 'react-hot-toast';\nimport Modal from 'components/Modal';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { IconFolder } from '@tabler/icons';\nimport { closeWorkspaceAction } from 'providers/ReduxStore/slices/workspaces/actions';\n\nconst CloseWorkspace = ({ workspaceUid, onClose }) => {\n  const dispatch = useDispatch();\n  const { workspaces } = useSelector((state) => state.workspaces);\n  const workspace = workspaces.find((w) => w.uid === workspaceUid);\n\n  const onConfirm = async () => {\n    try {\n      if (!workspace) {\n        toast.error('Workspace not found');\n        onClose();\n        return;\n      }\n      if (workspace.type === 'default') {\n        toast.error('Cannot close the default workspace');\n        onClose();\n        return;\n      }\n\n      await dispatch(closeWorkspaceAction(workspace.uid));\n      toast.success('Workspace closed');\n      onClose();\n    } catch (error) {\n      console.error('Error closing workspace:', error);\n      toast.error('An error occurred while closing the workspace');\n    }\n  };\n\n  return (\n    <Modal\n      size=\"sm\"\n      title=\"Close Workspace\"\n      confirmText=\"Close\"\n      handleConfirm={onConfirm}\n      handleCancel={onClose}\n    >\n      <div className=\"flex items-center\">\n        <IconFolder size={18} strokeWidth={1.5} />\n        <span className=\"ml-2 mr-4 font-semibold\">{workspace?.name}</span>\n      </div>\n      {workspace?.pathname && (\n        <div className=\"break-words text-xs mt-1\">{workspace.pathname}</div>\n      )}\n      <div className=\"mt-4\">\n        Are you sure you want to close workspace <span className=\"font-semibold\">{workspace?.name}</span>?\n      </div>\n      <div className=\"mt-4\">\n        It will still be available in the file system at the above location and can be re-opened later.\n      </div>\n    </Modal>\n  );\n};\n\nexport default CloseWorkspace;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CloneCollection/index.js",
    "content": "import React, { useRef, useEffect } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { useFormik } from 'formik';\nimport * as Yup from 'yup';\nimport path from 'utils/common/path';\nimport { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';\nimport { cloneCollection } from 'providers/ReduxStore/slices/collections/actions';\nimport toast from 'react-hot-toast';\nimport Modal from 'components/Modal';\nimport { sanitizeName, validateName, validateNameError } from 'utils/common/regex';\nimport Help from 'components/Help';\nimport PathDisplay from 'components/PathDisplay';\nimport { useState } from 'react';\nimport { IconArrowBackUp, IconEdit } from '@tabler/icons';\nimport { findCollectionByUid } from 'utils/collections/index';\nimport get from 'lodash/get';\n\nconst CloneCollection = ({ onClose, collectionUid }) => {\n  const inputRef = useRef();\n  const dispatch = useDispatch();\n  const [isEditing, toggleEditing] = useState(false);\n  const collection = useSelector((state) => findCollectionByUid(state.collections.collections, collectionUid));\n  const preferences = useSelector((state) => state.app.preferences);\n  const workspaces = useSelector((state) => state.workspaces?.workspaces || []);\n  const workspaceUid = useSelector((state) => state.workspaces?.activeWorkspaceUid);\n  const activeWorkspace = workspaces.find((w) => w.uid === workspaceUid);\n  const isDefaultWorkspace = activeWorkspace?.type === 'default';\n\n  const defaultLocation = isDefaultWorkspace\n    ? get(preferences, 'general.defaultLocation', '')\n    : (activeWorkspace?.pathname ? path.join(activeWorkspace.pathname, 'collections') : '');\n  const { name } = collection;\n\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: {\n      collectionName: `${name} copy`,\n      collectionFolderName: `${sanitizeName(name)} copy`,\n      collectionLocation: defaultLocation\n    },\n    validationSchema: Yup.object({\n      collectionName: Yup.string()\n        .min(1, 'must be at least 1 character')\n        .max(255, 'must be 255 characters or less')\n        .required('collection name is required'),\n      collectionFolderName: Yup.string()\n        .min(1, 'must be at least 1 character')\n        .max(255, 'must be 255 characters or less')\n        .test('is-valid-collection-name', function (value) {\n          const isValid = validateName(value);\n          return isValid ? true : this.createError({ message: validateNameError(value) });\n        })\n        .required('folder name is required'),\n      collectionLocation: Yup.string().min(1, 'location is required').required('location is required')\n    }),\n    onSubmit: (values) => {\n      dispatch(\n        cloneCollection(\n          values.collectionName,\n          values.collectionFolderName,\n          values.collectionLocation,\n          collection?.pathname\n        )\n      )\n        .then(() => {\n          toast.success('Collection created!');\n          onClose();\n        })\n        .catch((e) => toast.error('An error occurred while creating the collection - ' + e));\n    }\n  });\n\n  const browse = () => {\n    dispatch(browseDirectory())\n      .then((dirPath) => {\n        // When the user closes the dialog without selecting anything dirPath will be false\n        if (typeof dirPath === 'string') {\n          formik.setFieldValue('collectionLocation', dirPath);\n        }\n      })\n      .catch((error) => {\n        formik.setFieldValue('collectionLocation', '');\n        console.error(error);\n      });\n  };\n\n  useEffect(() => {\n    if (inputRef && inputRef.current) {\n      inputRef.current.focus();\n    }\n  }, [inputRef]);\n\n  const onSubmit = () => formik.handleSubmit();\n\n  return (\n    <Modal size=\"md\" title=\"Clone Collection\" confirmText=\"Create\" handleConfirm={onSubmit} handleCancel={onClose}>\n      <form className=\"bruno-form\" onSubmit={(e) => e.preventDefault()}>\n        <div>\n          <label htmlFor=\"collection-name\" className=\"flex items-center font-medium\">\n            Name\n          </label>\n          <input\n            id=\"collection-name\"\n            type=\"text\"\n            name=\"collectionName\"\n            ref={inputRef}\n            className=\"block textbox mt-2 w-full\"\n            onChange={(e) => {\n              formik.handleChange(e);\n              !isEditing && formik.setFieldValue('collectionFolderName', sanitizeName(e.target.value));\n            }}\n            autoComplete=\"off\"\n            autoCorrect=\"off\"\n            autoCapitalize=\"off\"\n            spellCheck=\"false\"\n            value={formik.values.collectionName || ''}\n          />\n          {formik.touched.collectionName && formik.errors.collectionName ? (\n            <div className=\"text-red-500\">{formik.errors.collectionName}</div>\n          ) : null}\n\n          <label htmlFor=\"collection-location\" className=\"block font-medium mt-3\">\n            Location\n          </label>\n          <input\n            id=\"collection-location\"\n            type=\"text\"\n            name=\"collectionLocation\"\n            readOnly={true}\n            className=\"block textbox mt-2 w-full cursor-pointer\"\n            autoComplete=\"off\"\n            autoCorrect=\"off\"\n            autoCapitalize=\"off\"\n            spellCheck=\"false\"\n            value={formik.values.collectionLocation || ''}\n            onClick={browse}\n          />\n          {formik.touched.collectionLocation && formik.errors.collectionLocation ? (\n            <div className=\"text-red-500\">{formik.errors.collectionLocation}</div>\n          ) : null}\n          <div className=\"mt-1\">\n            <span\n              className=\"text-link cursor-pointer hover:underline\"\n              onClick={browse}\n            >\n              Browse\n            </span>\n          </div>\n\n          <div className=\"mt-4\">\n            <div className=\"flex items-center justify-between\">\n              <label htmlFor=\"filename\" className=\"flex items-center font-medium\">\n                Folder Name\n                <Help width=\"300\">\n                  <p>\n                    The name of the folder used to store the collection.\n                  </p>\n                  <p className=\"mt-2\">\n                    You can choose a folder name different from your collection's name or one compatible with filesystem rules.\n                  </p>\n                </Help>\n              </label>\n              {isEditing ? (\n                <IconArrowBackUp\n                  className=\"cursor-pointer opacity-50 hover:opacity-80\"\n                  size={16}\n                  strokeWidth={1.5}\n                  onClick={() => toggleEditing(false)}\n                />\n              ) : (\n                <IconEdit\n                  className=\"cursor-pointer opacity-50 hover:opacity-80\"\n                  size={16}\n                  strokeWidth={1.5}\n                  onClick={() => toggleEditing(true)}\n                />\n              )}\n            </div>\n            {isEditing ? (\n              <input\n                id=\"collection-folder-name\"\n                type=\"text\"\n                name=\"collectionFolderName\"\n                className=\"block textbox mt-2 w-full\"\n                onChange={formik.handleChange}\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n                value={formik.values.collectionFolderName || ''}\n              />\n            ) : (\n              <div className=\"relative flex flex-row gap-1 items-center justify-between\">\n                <PathDisplay\n                  baseName={formik.values.collectionFolderName}\n                />\n              </div>\n            )}\n\n            {formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (\n              <div className=\"text-red-500\">{formik.errors.collectionFolderName}</div>\n            ) : null}\n          </div>\n        </div>\n      </form>\n    </Modal>\n  );\n};\n\nexport default CloneCollection;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .advanced-options {\n    .caret {\n      color: ${(props) => props.theme.textLink};\n      fill: ${(props) => props.theme.textLink};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CloneCollectionItem/index.js",
    "content": "import React, { useState, useRef, useEffect, forwardRef } from 'react';\nimport toast from 'react-hot-toast';\nimport { useFormik } from 'formik';\nimport * as Yup from 'yup';\nimport Modal from 'components/Modal';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { isItemAFolder } from 'utils/tabs';\nimport { cloneItem } from 'providers/ReduxStore/slices/collections/actions';\nimport { IconArrowBackUp, IconEdit, IconCaretDown } from '@tabler/icons';\nimport { sanitizeName, validateName, validateNameError } from 'utils/common/regex';\nimport Help from 'components/Help';\nimport PathDisplay from 'components/PathDisplay/index';\nimport path from 'utils/common/path';\nimport Portal from 'components/Portal';\nimport Dropdown from 'components/Dropdown';\nimport StyledWrapper from './StyledWrapper';\nimport Button from 'ui/Button';\n\nconst CloneCollectionItem = ({ collectionUid, item, onClose }) => {\n  const dispatch = useDispatch();\n  const collection = useSelector((state) => state.collections.collections?.find((c) => c.uid === collectionUid));\n  const isFolder = isItemAFolder(item);\n  const inputRef = useRef();\n  const [isEditing, toggleEditing] = useState(false);\n  const itemName = item?.name;\n  const itemType = item?.type;\n  const [showFilesystemName, toggleShowFilesystemName] = useState(false);\n\n  const dropdownTippyRef = useRef();\n  const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);\n\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: {\n      name: `${itemName} copy`,\n      filename: `${sanitizeName(itemName)} copy`\n    },\n    validationSchema: Yup.object({\n      name: Yup.string()\n        .min(1, 'must be at least 1 character')\n        .max(255, 'must be 255 characters or less')\n        .required('name is required'),\n      filename: Yup.string()\n        .min(1, 'must be at least 1 character')\n        .max(255, 'must be 255 characters or less')\n        .required('name is required')\n        .test('is-valid-name', function (value) {\n          const isValid = validateName(value);\n          return isValid ? true : this.createError({ message: validateNameError(value) });\n        })\n        .test('not-reserved', `The file names \"collection\" and \"folder\" are reserved in bruno`, (value) => !['collection', 'folder'].includes(value))\n    }),\n    onSubmit: (values) => {\n      dispatch(cloneItem(values.name, values.filename, item.uid, collectionUid))\n        .then(() => {\n          toast.success('Request cloned!');\n          onClose();\n        })\n        .catch((err) => {\n          toast.error(err ? err.message : 'An error occurred while cloning the request');\n        });\n    }\n  });\n\n  useEffect(() => {\n    if (inputRef && inputRef.current) {\n      inputRef.current.focus();\n    }\n  }, [inputRef]);\n\n  const AdvancedOptions = forwardRef((props, ref) => {\n    return (\n      <div ref={ref} className=\"flex mr-2 text-link cursor-pointer items-center\">\n        <button\n          className=\"btn-advanced\"\n          type=\"button\"\n        >\n          Options\n        </button>\n        <IconCaretDown className=\"caret ml-1\" size={14} strokeWidth={2} />\n      </div>\n    );\n  });\n\n  return (\n    <Portal>\n      <StyledWrapper>\n        <Modal\n          size=\"md\"\n          title={`Clone ${isFolder ? 'Folder' : 'Request'}`}\n          handleCancel={onClose}\n          hideFooter\n        >\n          <form className=\"bruno-form\" onSubmit={formik.handleSubmit}>\n            <div>\n              <label htmlFor=\"name\" className=\"block font-medium\">\n                {isFolder ? 'Folder' : 'Request'} Name\n              </label>\n              <input\n                id=\"collection-item-name\"\n                type=\"text\"\n                name=\"name\"\n                placeholder=\"Enter Item name\"\n                ref={inputRef}\n                className=\"block textbox mt-2 w-full\"\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n                onChange={(e) => {\n                  formik.setFieldValue('name', e.target.value);\n                  !isEditing && formik.setFieldValue('filename', sanitizeName(e.target.value));\n                }}\n                value={formik.values.name || ''}\n              />\n              {formik.touched.name && formik.errors.name ? <div className=\"text-red-500\">{formik.errors.name}</div> : null}\n            </div>\n\n            {showFilesystemName && (\n              <div className=\"mt-4\">\n                <div className=\"flex items-center justify-between\">\n                  <label htmlFor=\"filename\" className=\"flex items-center font-medium\">\n                    {isFolder ? 'Folder' : 'File'} Name <small className=\"font-normal text-muted ml-1\">(on filesystem)</small>\n                    { isFolder ? (\n                      <Help width=\"300\">\n                        <p>\n                          You can choose to save the folder as a different name on your file system versus what is displayed in the app.\n                        </p>\n                      </Help>\n                    ) : (\n                      <Help width=\"300\">\n                        <p>\n                          Bruno saves each request as a file in your collection's folder.\n                        </p>\n                        <p className=\"mt-2\">\n                          You can choose a file name different from your request's name or one compatible with filesystem rules.\n                        </p>\n                      </Help>\n                    )}\n                  </label>\n                  {isEditing ? (\n                    <IconArrowBackUp\n                      className=\"cursor-pointer opacity-50 hover:opacity-80\"\n                      size={16}\n                      strokeWidth={1.5}\n                      onClick={() => toggleEditing(false)}\n                    />\n                  ) : (\n                    <IconEdit\n                      className=\"cursor-pointer opacity-50 hover:opacity-80\"\n                      size={16}\n                      strokeWidth={1.5}\n                      onClick={() => toggleEditing(true)}\n                    />\n                  )}\n                </div>\n                {isEditing ? (\n                  <div className=\"relative flex flex-row gap-1 items-center justify-between\">\n                    <input\n                      id=\"file-name\"\n                      type=\"text\"\n                      name=\"filename\"\n                      placeholder={isFolder ? 'Folder Name' : 'File Name'}\n                      className=\"!pr-10 block textbox mt-2 w-full\"\n                      autoComplete=\"off\"\n                      autoCorrect=\"off\"\n                      autoCapitalize=\"off\"\n                      spellCheck=\"false\"\n                      onChange={formik.handleChange}\n                      value={formik.values.filename || ''}\n                    />\n                    {itemType !== 'folder' && <span className=\"absolute right-2 top-4 flex justify-center items-center file-extension\">.{collection?.format || 'bru'}</span>}\n                  </div>\n                ) : (\n                  <div className=\"relative flex flex-row gap-1 items-center justify-between\">\n                    <PathDisplay\n                      baseName={formik.values.filename}\n                    />\n                  </div>\n                )}\n                {formik.touched.filename && formik.errors.filename ? (\n                  <div className=\"text-red-500\">{formik.errors.filename}</div>\n                ) : null}\n              </div>\n            )}\n\n            <div className=\"flex justify-between items-center mt-8 bruno-modal-footer\">\n              <div className=\"flex advanced-options\">\n                <Dropdown onCreate={onDropdownCreate} icon={<AdvancedOptions />} placement=\"bottom-start\">\n                  <div\n                    className=\"dropdown-item\"\n                    key=\"show-filesystem-name\"\n                    onClick={(e) => {\n                      dropdownTippyRef.current.hide();\n                      toggleShowFilesystemName(!showFilesystemName);\n                    }}\n                  >\n                    {showFilesystemName ? 'Hide Filesystem Name' : 'Show Filesystem Name'}\n                  </div>\n                </Dropdown>\n              </div>\n              <div className=\"flex justify-end\">\n                <Button type=\"button\" color=\"secondary\" variant=\"ghost\" onClick={onClose} className=\"mr-2\">\n                  Cancel\n                </Button>\n                <Button type=\"submit\">\n                  Clone\n                </Button>\n              </div>\n            </div>\n          </form>\n        </Modal>\n      </StyledWrapper>\n    </Portal>\n  );\n};\n\nexport default CloneCollectionItem;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemDragPreview/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .drag-preview {\n    background-color: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemDragPreview/index.js",
    "content": "import { useDragLayer } from 'react-dnd';\nimport {\n  IconFile,\n  IconFolder\n} from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\n\nfunction getItemStyles({ x, y }) {\n  if (Number.isNaN(x) || Number.isNaN(y)) return { display: 'none' };\n  const transform = `translate(${x}px, ${y}px)`;\n\n  return {\n    position: 'fixed',\n    pointerEvents: 'none',\n    top: 0,\n    transform,\n    WebkitTransform: transform,\n    zIndex: 100\n  };\n}\n\nexport const CollectionItemDragPreview = () => {\n  const {\n    item,\n    isDragging,\n    clientOffset\n  } = useDragLayer((monitor) => ({\n    item: monitor.getItem(),\n    isDragging: monitor.isDragging(),\n    clientOffset: monitor.getClientOffset()\n  }));\n  if (!isDragging) return null;\n  if (!item.type) return null;\n  const { x, y } = clientOffset || {};\n  const shouldShowFolderIcon = item.type === 'folder';\n  return (\n    <StyledWrapper>\n      <div style={getItemStyles({ x, y })} className=\"p-2\">\n        <div className=\"flex items-center gap-2 border border-gray-500/10 rounded-md px-2 py-1 drag-preview\">\n          {shouldShowFolderIcon ? (\n            <IconFolder size={16} />\n          ) : (\n            <IconFile size={16} />\n          )}\n          {item.name}\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemIcon/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  .partial {\n    color: ${(props) => props.theme.colors.text.yellow};\n  }\n  .error {\n    color: ${(props) => props.theme.colors.text.danger};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemIcon/index.js",
    "content": "import RequestMethod from '../RequestMethod';\nimport { IconLoader2, IconAlertTriangle, IconAlertCircle } from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\n\nconst CollectionItemIcon = ({ item }) => {\n  if (item?.error) {\n    return <StyledWrapper><IconAlertCircle className=\"w-fit mr-2 error\" size={18} strokeWidth={1.5} /></StyledWrapper>;\n  }\n\n  if (item?.loading) {\n    return <IconLoader2 className=\"animate-spin w-fit mr-2\" size={18} strokeWidth={1.5} />;\n  }\n\n  if (item?.partial) {\n    return <StyledWrapper><IconAlertTriangle size={18} className=\"w-fit mr-2 partial\" strokeWidth={1.5} /></StyledWrapper>;\n  }\n\n  return <RequestMethod item={item} />;\n};\n\nexport default CollectionItemIcon;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/CollectionItemInfo/index.js",
    "content": "import React from 'react';\nimport Modal from 'components/Modal';\nimport Help from 'components/Help';\n\nconst CollectionItemInfo = ({ item, onClose }) => {\n  const { name, filename, type } = item;\n\n  return (\n    <Modal\n      size=\"md\"\n      title=\"Info\"\n      handleCancel={onClose}\n      hideCancel={true}\n      hideFooter={true}\n    >\n      <div className=\"w-fit flex flex-col h-full\">\n        <table className=\"w-full border-collapse\">\n          <tbody>\n            <tr className=\"\">\n              <td className=\"py-2 px-2 text-left text-muted \">\n                {type == 'folder' ? 'Folder Name' : 'Request Name'}\n              </td>\n              <td className=\"py-2 px-2 text-nowrap truncate max-w-[500px]\" title={name}>\n                <span className=\"mr-2\">:</span>{name}\n              </td>\n            </tr>\n            <tr className=\"\">\n              <td className=\"py-2 px-2 text-left text-muted flex items-center\">\n                {type == 'folder' ? 'Folder Name' : 'File Name'}\n                <small className=\"font-normal text-muted ml-1\">(on filesystem)</small>\n                {type == 'folder' ? (\n                  <Help width=\"300\">\n                    <p>\n                      The name of the folder on your filesystem.\n                    </p>\n                  </Help>\n                ) : (\n                  <Help width=\"300\">\n                    <p>\n                      Bruno saves each request as a file in your collection's folder.\n                    </p>\n                  </Help>\n                )}\n              </td>\n              <td className=\"py-2 px-2 break-all text-nowrap truncate max-w-[500px]\" title={filename}>\n                <span className=\"mr-2\">:</span>\n                {filename}\n              </td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n    </Modal>\n  );\n};\n\nexport default CollectionItemInfo;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/DeleteCollectionItem/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  button.submit {\n    color: white;\n    background-color: var(--color-background-danger) !important;\n    border: inherit !important;\n\n    &:hover {\n      border: inherit !important;\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/DeleteCollectionItem/index.js",
    "content": "import React from 'react';\nimport Modal from 'components/Modal';\nimport { isItemAFolder } from 'utils/tabs';\nimport { useDispatch } from 'react-redux';\nimport { deleteItem, closeTabs } from 'providers/ReduxStore/slices/collections/actions';\nimport { recursivelyGetAllItemUids } from 'utils/collections';\nimport StyledWrapper from './StyledWrapper';\nimport toast from 'react-hot-toast';\n\nconst DeleteCollectionItem = ({ onClose, item, collectionUid }) => {\n  const dispatch = useDispatch();\n  const isFolder = isItemAFolder(item);\n  const onConfirm = () => {\n    dispatch(deleteItem(item.uid, collectionUid)).then(() => {\n      if (isFolder) {\n        // close all tabs that belong to the folder\n        // including the folder itself and its children\n        const tabUids = [...recursivelyGetAllItemUids(item.items), item.uid];\n\n        dispatch(\n          closeTabs({\n            tabUids: tabUids\n          })\n        );\n      } else {\n        dispatch(\n          closeTabs({\n            tabUids: [item.uid]\n          })\n        );\n      }\n    }).catch((error) => {\n      console.error('Error deleting item', error);\n      toast.error(error?.message || 'Error deleting item');\n    });\n    onClose();\n  };\n\n  return (\n    <StyledWrapper>\n      <Modal\n        size=\"md\"\n        title={`Delete ${isFolder ? 'Folder' : 'Request'}`}\n        confirmText=\"Delete\"\n        confirmButtonColor=\"danger\"\n        handleConfirm={onConfirm}\n        handleCancel={onClose}\n      >\n        Are you sure you want to delete <span className=\"font-medium\">{item.name}</span> ?\n      </Modal>\n    </StyledWrapper>\n  );\n};\n\nexport default DeleteCollectionItem;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/ExampleItem/DeleteResponseExampleModal/index.js",
    "content": "import React from 'react';\nimport Modal from 'components/Modal';\nimport Portal from 'components/Portal';\nimport { useDispatch } from 'react-redux';\nimport { deleteResponseExample } from 'providers/ReduxStore/slices/collections';\nimport { saveRequest, closeTabs } from 'providers/ReduxStore/slices/collections/actions';\n\nconst DeleteResponseExampleModal = ({ onClose, example, item, collection }) => {\n  const dispatch = useDispatch();\n\n  const onConfirm = (e) => {\n    e.stopPropagation();\n    dispatch(closeTabs({ tabUids: [example.uid] }));\n    dispatch(deleteResponseExample({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: example.uid\n    }));\n    dispatch(saveRequest(item.uid, collection.uid, true))\n      .then(() => {\n        onClose();\n      });\n  };\n\n  return (\n    <Portal>\n      <Modal\n        size=\"sm\"\n        title=\"Delete Example\"\n        confirmText=\"Delete\"\n        handleConfirm={onConfirm}\n        handleCancel={onClose}\n        confirmButtonColor=\"danger\"\n      >\n        Are you sure you want to delete the example <span className=\"font-medium\">{example.name}</span>?\n      </Modal>\n    </Portal>\n  );\n};\n\nexport default DeleteResponseExampleModal;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/ExampleItem/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  position: relative;\n  \n  .menu-icon {\n    color: ${(props) => props.theme.sidebar.dropdownIcon.color};\n    visibility: hidden;\n\n    .dropdown {\n      div[aria-expanded='true'] {\n        visibility: visible;\n      }\n      div[aria-expanded='false'] {\n        visibility: visible;\n      }\n    }\n  }\n\n  .collection-item-name {\n    height: 1.6rem;\n    cursor: pointer;\n    user-select: none;\n    position: relative;\n\n    span.item-name {\n      white-space: nowrap;\n      text-overflow: ellipsis;\n      overflow: hidden;\n    }\n\n    &:hover,\n    &.item-hovered {\n      background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n      .menu-icon {\n        visibility: visible;\n      }\n    }\n\n    &.item-focused-in-tab {\n      background: ${(props) => props.theme.sidebar.collection.item.bg};\n\n      &:hover {\n        background: ${(props) => props.theme.sidebar.collection.item.bg} !important;\n      }\n    }\n  }\n\n  .example-icon {\n    color: ${(props) => props.theme.sidebar.collection.item.example.iconColor};\n  }\n\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/ExampleItem/index.js",
    "content": "import React, { useState, useRef, useEffect } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';\nimport {\n  updateResponseExample,\n  cloneResponseExample\n} from 'providers/ReduxStore/slices/collections';\nimport { saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport { insertTaskIntoQueue } from 'providers/ReduxStore/slices/app';\nimport { uuid } from 'utils/common';\nimport { IconDots, IconEdit, IconCopy, IconTrash, IconCode } from '@tabler/icons';\nimport ExampleIcon from 'components/Icons/ExampleIcon';\nimport range from 'lodash/range';\nimport classnames from 'classnames';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport ActionIcon from 'ui/ActionIcon';\nimport Modal from 'components/Modal';\nimport DeleteResponseExampleModal from './DeleteResponseExampleModal';\nimport GenerateCodeItem from '../GenerateCodeItem';\nimport toast from 'react-hot-toast';\nimport StyledWrapper from './StyledWrapper';\nimport { useSidebarAccordion } from 'components/Sidebar/SidebarAccordionContext';\n\nconst ExampleItem = ({ example, item, collection }) => {\n  const { dropdownContainerRef } = useSidebarAccordion();\n  const dispatch = useDispatch();\n  // Check if this example is the active tab\n  const activeTabUid = useSelector((state) => state.tabs?.activeTabUid);\n  const isExampleActive = activeTabUid === example.uid;\n  const [editName, setEditName] = useState(example.name || '');\n  const [showRenameModal, setShowRenameModal] = useState(false);\n  const [showDeleteModal, setShowDeleteModal] = useState(false);\n  const [generateCodeItemModalOpen, setGenerateCodeItemModalOpen] = useState(false);\n  const exampleRef = useRef(null);\n  const menuDropdownRef = useRef(null);\n\n  // Calculate indentation: item depth + 1 for examples\n  const indents = range((item.depth || 0) + 1);\n\n  const handleExampleClick = () => {\n    dispatch(addTab({\n      uid: example.uid, // Use example.uid as the tab uid\n      exampleUid: example.uid,\n      collectionUid: collection.uid,\n      type: 'response-example',\n      itemUid: item.uid\n    }));\n  };\n\n  const handleDoubleClick = () => {\n    dispatch(makeTabPermanent({ uid: example.uid }));\n  };\n\n  const handleRename = () => {\n    setEditName(example.name); // Set current name when opening modal\n    setShowRenameModal(true);\n  };\n\n  // Update editName when example changes\n  useEffect(() => {\n    setEditName(example.name);\n  }, [example.name]);\n\n  useEffect(() => {\n    if (isExampleActive && exampleRef.current) {\n      try {\n        exampleRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });\n      } catch (err) {\n        // ignore scroll errors\n      }\n    }\n  }, [isExampleActive]);\n\n  const handleClone = async () => {\n    // Calculate the index where the cloned example will be saved\n    // It will be at the end of the examples array\n    const existingExamples = item.draft?.examples || item.examples || [];\n    const clonedExampleIndex = existingExamples.length;\n    const clonedExampleUid = uuid();\n\n    dispatch(cloneResponseExample({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: example.uid,\n      clonedUid: clonedExampleUid\n    }));\n\n    // Save the request\n    await dispatch(saveRequest(item.uid, collection.uid, true));\n\n    // Task middleware will track this and open the example in a new tab once the file is reloaded\n    dispatch(insertTaskIntoQueue({\n      uid: clonedExampleUid,\n      type: 'OPEN_EXAMPLE',\n      collectionUid: collection.uid,\n      itemUid: item.uid,\n      exampleIndex: clonedExampleIndex\n    }));\n  };\n\n  const handleDelete = () => {\n    setShowDeleteModal(true);\n  };\n\n  const handleGenerateCode = () => {\n    // Check if example has a request URL\n    if (\n      (example?.request?.url !== '' && example?.request?.url !== undefined)\n      || (item?.request?.url !== '' && item?.request?.url !== undefined)\n      || (item?.draft?.request?.url !== undefined && item?.draft?.request?.url !== '')\n    ) {\n      setGenerateCodeItemModalOpen(true);\n    } else {\n      toast.error('URL is required');\n    }\n  };\n\n  const handleRenameConfirm = (newName) => {\n    // Find the example index in the original examples array\n    dispatch(updateResponseExample({\n      itemUid: item.uid,\n      collectionUid: collection.uid,\n      exampleUid: example.uid,\n      example: {\n        name: newName\n      }\n    }));\n    dispatch(saveRequest(item.uid, collection.uid, true))\n      .then(() => {\n        toast.success(`Example renamed to \"${newName}\"`);\n        setShowRenameModal(false);\n      });\n  };\n\n  // Build menu items for MenuDropdown\n  const buildMenuItems = () => {\n    return [\n      {\n        id: 'rename',\n        leftSection: IconEdit,\n        label: 'Rename',\n        onClick: handleRename,\n        testId: 'response-example-rename-option'\n      },\n      {\n        id: 'clone',\n        leftSection: IconCopy,\n        label: 'Clone',\n        onClick: handleClone,\n        testId: 'response-example-clone-option'\n      },\n      {\n        id: 'generate-code',\n        leftSection: IconCode,\n        label: 'Generate Code',\n        onClick: handleGenerateCode,\n        testId: 'response-example-generate-code-option'\n      },\n      { id: 'separator-1', type: 'divider' },\n      {\n        id: 'delete',\n        leftSection: IconTrash,\n        label: 'Delete',\n        className: 'delete-item',\n        onClick: handleDelete,\n        testId: 'response-example-delete-option'\n      }\n    ];\n  };\n\n  // Handle right-click context menu\n  const handleContextMenu = (e) => {\n    e.preventDefault();\n    e.stopPropagation();\n    menuDropdownRef.current?.show();\n  };\n\n  const itemRowClassName = classnames('flex collection-item-name relative items-center', {\n    'item-focused-in-tab': isExampleActive\n  });\n\n  return (\n    <StyledWrapper\n      ref={exampleRef}\n      className={itemRowClassName}\n      onClick={handleExampleClick}\n      onDoubleClick={handleDoubleClick}\n      onContextMenu={handleContextMenu}\n    >\n      {indents && indents.length\n        ? indents.map((i) => (\n            <div\n              className=\"indent-block\"\n              key={i}\n              style={{ width: 16, minWidth: 16, height: '100%' }}\n            >\n              &nbsp;{/* Indent */}\n            </div>\n          ))\n        : null}\n      <div\n        className=\"flex flex-grow items-center h-full overflow-hidden\"\n        style={{ paddingLeft: 8 }}\n      >\n        <div style={{ width: 16, minWidth: 16 }}></div>\n        <ExampleIcon size={16} color=\"currentColor\" className=\"example-icon mr-1 flex-shrink-0\" />\n        <span className=\"item-name truncate\">{example.name}</span>\n      </div>\n      <div className=\"menu-icon pr-2\">\n        <MenuDropdown\n          ref={menuDropdownRef}\n          items={buildMenuItems()}\n          placement=\"bottom-start\"\n          appendTo={dropdownContainerRef?.current || document.body}\n          popperOptions={{ strategy: 'fixed' }}\n          data-testid=\"response-example-menu\"\n        >\n          <IconDots size={22} data-testid=\"response-example-menu-icon\" />\n        </MenuDropdown>\n      </div>\n\n      {showRenameModal && (\n        <Modal\n          size=\"sm\"\n          title=\"Rename Example\"\n          handleCancel={() => {\n            setShowRenameModal(false);\n            setEditName(example.name); // Reset to original name on cancel\n          }}\n          handleConfirm={() => handleRenameConfirm(editName)}\n          confirmText=\"Rename\"\n          cancelText=\"Cancel\"\n          confirmDisabled={!editName || !editName.trim()}\n        >\n          <div>\n            <label htmlFor=\"renameExampleName\" className=\"block font-medium\">\n              Example Name\n            </label>\n            <input\n              data-testid=\"rename-example-name-input\"\n              id=\"renameExampleName\"\n              type=\"text\"\n              className=\"textbox mt-2\"\n              value={editName}\n              onChange={(e) => setEditName(e.target.value)}\n              placeholder=\"Enter example name...\"\n              autoFocus\n              required\n            />\n          </div>\n        </Modal>\n      )}\n\n      {showDeleteModal && (\n        <DeleteResponseExampleModal\n          onClose={() => setShowDeleteModal(false)}\n          example={example}\n          item={item}\n          collection={collection}\n        />\n      )}\n\n      {generateCodeItemModalOpen && (\n        <GenerateCodeItem\n          collectionUid={collection.uid}\n          item={item}\n          onClose={() => setGenerateCodeItemModalOpen(false)}\n          isExample={true}\n          exampleUid={example.uid}\n        />\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default ExampleItem;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  height: 100%;\n  position: relative;\n\n  .editor-content {\n    height: 100%;\n\n    .CodeMirror {\n      height: 100%;\n      font-size: ${(props) => props.theme.font.size.sm};\n      background: ${(props) => props.theme.modal.bg};\n      line-height: 1.5;\n      padding: 0;\n      background: transparent !important;\n      border: none;\n\n      .CodeMirror-gutters {\n        background: transparent !important;\n        border-right: none;\n      }\n\n      .CodeMirror-linenumber {\n        color: ${(props) => props.theme.colors.text.muted};\n        font-size: ${(props) => props.theme.font.size.xs};\n        padding: 0 3px 0 5px;\n      }\n\n      .CodeMirror-lines {\n        padding: 0;\n      }\n\n      .CodeMirror-line {\n        padding: 0 4px;\n      }\n    }\n  }\n\n  .copy-to-clipboard {\n    position: absolute;\n    top: 10px;\n    right: 10px;\n    z-index: 10;\n    background: transparent;\n    border: none;\n    color: ${(props) => props.theme.colors.text.muted};\n    cursor: pointer;\n    padding: 6px;\n    opacity: 0.7;\n    transition: all 0.2s ease;\n\n    &:hover {\n      opacity: 1;\n      color: ${(props) => props.theme.text};\n    }\n\n    &:active {\n      transform: translateY(1px);\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeView/index.js",
    "content": "import CodeEditor from 'components/CodeEditor/index';\nimport get from 'lodash/get';\nimport { useTheme } from 'providers/Theme/index';\nimport StyledWrapper from './StyledWrapper';\nimport { useSelector } from 'react-redux';\nimport { CopyToClipboard } from 'react-copy-to-clipboard';\nimport toast from 'react-hot-toast';\nimport { IconCopy } from '@tabler/icons';\nimport { findCollectionByItemUid, getGlobalEnvironmentVariables } from 'utils/collections/index';\nimport { cloneDeep } from 'lodash';\nimport { useMemo } from 'react';\nimport { generateSnippet } from '../utils/snippet-generator';\nconst CodeView = ({ language, item }) => {\n  const { displayedTheme } = useTheme();\n  const preferences = useSelector((state) => state.app.preferences);\n  const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);\n  const generateCodePrefs = useSelector((state) => state.app.generateCode);\n\n  let collectionOriginal = findCollectionByItemUid(\n    useSelector((state) => state.collections.collections),\n    item.uid\n  );\n\n  const collection = useMemo(() => {\n    const c = cloneDeep(collectionOriginal);\n    const globalEnvironmentVariables = getGlobalEnvironmentVariables({\n      globalEnvironments,\n      activeGlobalEnvironmentUid\n    });\n    c.globalEnvironmentVariables = globalEnvironmentVariables;\n    return c;\n  }, [collectionOriginal, globalEnvironments, activeGlobalEnvironmentUid]);\n\n  const snippet = useMemo(() => {\n    return generateSnippet({\n      language,\n      item,\n      collection,\n      shouldInterpolate: generateCodePrefs.shouldInterpolate\n    });\n  }, [language, item, collection, generateCodePrefs.shouldInterpolate]);\n\n  return (\n    <StyledWrapper>\n      <CopyToClipboard\n        text={snippet}\n        options={{ format: 'text/plain' }}\n        onCopy={() => toast.success('Copied to clipboard!')}\n      >\n        <button className=\"copy-to-clipboard\">\n          <IconCopy size={20} strokeWidth={1.5} />\n        </button>\n      </CopyToClipboard>\n      <div className=\"editor-content\">\n        <CodeEditor\n          readOnly\n          collection={collection}\n          item={item}\n          value={snippet}\n          font={get(preferences, 'font.codeFont', 'default')}\n          fontSize={get(preferences, 'font.codeFontSize')}\n          theme={displayedTheme}\n          mode={language.language}\n          enableVariableHighlighting={true}\n          showHintsFor={['variables']}\n        />\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default CodeView;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeViewToolbar/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .toolbar {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    background: ${(props) => props.theme.modal.bg};\n    gap: 12px;\n    flex-shrink: 0;\n  }\n\n  .left-controls {\n    display: flex;\n    align-items: center;\n    gap: 12px;\n  }\n\n  .select-wrapper {\n    position: relative;\n    display: flex;\n    align-items: center;\n  }\n\n  .select-arrow {\n    position: absolute;\n    right: 8px;\n    top: 50%;\n    transform: translateY(-50%);\n    pointer-events: none;\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .native-select {\n    background: ${(props) => props.theme.requestTabPanel.url.bg};\n    border: 1px solid ${(props) => props.theme.input.border};\n    border-radius: 3px;\n    color: ${(props) => props.theme.text};\n    font-size: ${(props) => props.theme.font.size.sm};\n    padding: 6px 28px 6px 10px;\n    min-width: 140px;\n    height: 32px;\n    cursor: pointer;\n    transition: all 0.2s ease;\n    appearance: none;\n    outline: none;\n    box-shadow: none;\n\n    &:hover {\n      border-color: ${(props) => props.theme.input.focusBorder};\n    }\n\n    &:focus {\n      outline: none;\n      border-color: ${(props) => props.theme.input.focusBorder};\n      box-shadow: 0 0 0 2px ${(props) => props.theme.input.focusBoxShadow};\n    }\n\n    option {\n      background: ${(props) => props.theme.bg};\n      color: ${(props) => props.theme.text};\n      padding: 8px 12px;\n    }\n  }\n\n  .library-options {\n    display: flex;\n    gap: 6px;\n  }\n\n  .lib-btn {\n    height: 32px;\n    padding: 0 12px;\n    background: ${(props) => props.theme.requestTabPanel.url.bg};\n    border: 1px solid ${(props) => props.theme.input.border};\n    border-radius: 3px;\n    color: ${(props) => props.theme.text};\n    font-size: ${(props) => props.theme.font.size.sm};\n    cursor: pointer;\n    transition: all 0.15s ease;\n    display: flex;\n    align-items: center;\n\n    &:hover {\n      background: ${(props) => props.theme.dropdown.hoverBg};\n      border-color: ${(props) => props.theme.input.focusBorder};\n    }\n\n    &.active {\n      background: ${(props) => props.theme.button.secondary.bg};\n      border-color: ${(props) => props.theme.button.secondary.border};\n      color: ${(props) => props.theme.button.secondary.color};\n    }\n  }\n\n  .right-controls {\n    .interpolate-checkbox {\n      display: flex;\n      align-items: center;\n      gap: 8px;\n      cursor: pointer;\n      font-size: ${(props) => props.theme.font.size.base};\n      color: ${(props) => props.theme.text};\n\n      input[type=\"checkbox\"] {\n        cursor: pointer;\n        margin: 0;\n      }\n\n      &:hover {\n        opacity: 0.8;\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/CodeViewToolbar/index.js",
    "content": "import { IconChevronDown } from '@tabler/icons';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { useMemo } from 'react';\nimport { getLanguages } from 'utils/codegenerator/targets';\nimport { updateGenerateCode } from 'providers/ReduxStore/slices/app';\nimport StyledWrapper from './StyledWrapper';\n\nconst CodeViewToolbar = () => {\n  const dispatch = useDispatch();\n  const languages = getLanguages();\n  const generateCodePrefs = useSelector((state) => state.app.generateCode);\n\n  // Group languages by their main language type\n  const languageGroups = useMemo(() => {\n    return languages.reduce((acc, lang) => {\n      const mainLang = lang.name.split('-')[0];\n      if (!acc[mainLang]) {\n        acc[mainLang] = [];\n      }\n      acc[mainLang].push({\n        ...lang,\n        libraryName: lang.name.split('-')[1] || 'default'\n      });\n      return acc;\n    }, {});\n  }, [languages]);\n\n  const mainLanguages = useMemo(() => Object.keys(languageGroups), [languageGroups]);\n\n  const availableLibraries = useMemo(() => {\n    return languageGroups[generateCodePrefs.mainLanguage] || [];\n  }, [generateCodePrefs.mainLanguage, languageGroups]);\n\n  // Event handlers\n  const handleMainLanguageChange = (e) => {\n    const newMainLang = e.target.value;\n    const defaultLibrary = languageGroups[newMainLang][0].libraryName;\n\n    dispatch(updateGenerateCode({\n      mainLanguage: newMainLang,\n      library: defaultLibrary\n    }));\n  };\n\n  const handleLibraryChange = (libraryName) => {\n    dispatch(updateGenerateCode({\n      library: libraryName\n    }));\n  };\n\n  const handleInterpolateChange = (e) => {\n    dispatch(updateGenerateCode({\n      shouldInterpolate: e.target.checked\n    }));\n  };\n\n  return (\n    <StyledWrapper>\n      <div className=\"toolbar\">\n        <div className=\"left-controls\">\n          <div className=\"select-wrapper\">\n            <select\n              className=\"native-select\"\n              value={generateCodePrefs.mainLanguage}\n              onChange={handleMainLanguageChange}\n            >\n              {mainLanguages.map((lang) => (\n                <option key={lang} value={lang}>\n                  {lang}\n                </option>\n              ))}\n            </select>\n            <IconChevronDown size={16} className=\"select-arrow\" />\n          </div>\n\n          {availableLibraries.length > 1 && (\n            <div className=\"library-options\">\n              {availableLibraries.map((lib) => (\n                <button\n                  key={lib.libraryName}\n                  className={`lib-btn ${generateCodePrefs.library === lib.libraryName ? 'active' : ''}`}\n                  onClick={() => handleLibraryChange(lib.libraryName)}\n                >\n                  {lib.libraryName}\n                </button>\n              ))}\n            </div>\n          )}\n        </div>\n\n        <div className=\"right-controls\">\n          <label className=\"interpolate-checkbox\">\n            <input\n              type=\"checkbox\"\n              checked={generateCodePrefs.shouldInterpolate}\n              onChange={handleInterpolateChange}\n            />\n            <span>Interpolate Variables</span>\n          </label>\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default CodeViewToolbar;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  height: 50vh;\n  display: flex;\n  flex-direction: column;\n  background-color: ${(props) => props.theme.modal.bg};\n\n  .code-generator {\n    display: flex;\n    flex-direction: column;\n    height: 100%;\n  }\n\n  .editor-container {\n    flex: 1;\n    min-height: 0;\n    overflow: hidden;\n    position: relative;\n    background: ${(props) => props.theme.modal.bg};\n    margin-top: 0.5rem;\n  }\n\n  .error-message {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    height: 100%;\n    color: ${(props) => props.theme.colors.text.muted};\n    text-align: center;\n    padding: 20px;\n\n    h1 {\n      font-size: ${(props) => props.theme.font.size.base};\n      margin-bottom: 8px;\n      color: ${(props) => props.theme.text};\n    }\n\n    p {\n      font-size: ${(props) => props.theme.font.size.sm};\n      opacity: 0.8;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js",
    "content": "import Modal from 'components/Modal/index';\nimport { useMemo } from 'react';\nimport CodeView from './CodeView';\nimport CodeViewToolbar from './CodeViewToolbar';\nimport StyledWrapper from './StyledWrapper';\nimport { isValidUrl } from 'utils/url';\nimport { get } from 'lodash';\nimport {\n  findEnvironmentInCollection\n} from 'utils/collections';\nimport { interpolateUrl, interpolateUrlPathParams } from 'utils/url/index';\nimport { getLanguages } from 'utils/codegenerator/targets';\nimport { useSelector } from 'react-redux';\nimport { getAllVariables, getGlobalEnvironmentVariables } from 'utils/collections/index';\nimport { resolveInheritedAuth } from 'utils/auth';\n\nconst TEMPLATE_VAR_PATTERN = /\\{\\{([^}]+)\\}\\}/;\n\nconst validateURLWithVars = (url) => {\n  const isValid = isValidUrl(url);\n  const hasMissingInterpolations = TEMPLATE_VAR_PATTERN.test(url);\n  return isValid && !hasMissingInterpolations;\n};\n\nconst GenerateCodeItem = ({ collectionUid, item, onClose, isExample = false, exampleUid = null }) => {\n  const languages = getLanguages();\n  const collection = useSelector((state) => state.collections.collections?.find((c) => c.uid === collectionUid));\n  const { globalEnvironments, activeGlobalEnvironmentUid } = useSelector((state) => state.globalEnvironments);\n  const generateCodePrefs = useSelector((state) => state.app.generateCode);\n  const globalEnvironmentVariables = getGlobalEnvironmentVariables({\n    globalEnvironments,\n    activeGlobalEnvironmentUid\n  });\n  const environment = findEnvironmentInCollection(collection, collection?.activeEnvironmentUid);\n\n  let envVars = {};\n  if (environment) {\n    const vars = get(environment, 'variables', []);\n    envVars = vars.reduce((acc, curr) => {\n      acc[curr.name] = curr.value;\n      return acc;\n    }, {});\n  }\n\n  // Function to handle normal request data\n  const getNormalRequestData = () => {\n    const requestUrl = get(item, 'draft.request.url') !== undefined ? get(item, 'draft.request.url') : get(item, 'request.url');\n    const requestParams = get(item, 'draft.request.params') !== undefined ? get(item, 'draft.request.params') : get(item, 'request.params');\n\n    return {\n      url: requestUrl,\n      params: requestParams,\n      request: get(item, 'draft.request') !== undefined ? get(item, 'draft.request') : get(item, 'request')\n    };\n  };\n\n  // Function to handle request example data\n  const getExampleRequestData = () => {\n    if (!isExample || !exampleUid) {\n      return getNormalRequestData();\n    }\n\n    // Find the specific example - check both draft and non-draft examples\n    const examples = item.draft ? get(item, 'draft.examples', []) : get(item, 'examples', []);\n    const example = examples.find((e) => e.uid === exampleUid);\n\n    if (!example) {\n      return getNormalRequestData();\n    }\n\n    // Use example request data\n    const requestUrl = get(example, 'request.url');\n    const requestParams = get(example, 'request.params');\n    const requestData = get(example, 'request');\n\n    return {\n      url: requestUrl,\n      params: requestParams,\n      request: requestData\n    };\n  };\n\n  // Get the appropriate request data based on mode\n  const requestData = isExample ? getExampleRequestData() : getNormalRequestData();\n\n  const variables = useMemo(() => {\n    return getAllVariables({ ...collection, globalEnvironmentVariables }, item);\n  }, [collection, globalEnvironmentVariables, item]);\n\n  const interpolatedUrl = interpolateUrl({\n    url: requestData.url,\n    variables\n  });\n\n  // interpolate the path params\n  const finalUrl = interpolateUrlPathParams(\n    interpolatedUrl,\n    requestData.params,\n    variables\n  );\n\n  // Raw URL: path params resolved via string replacement (no new URL() encoding),\n  // preserving the user's original encoding choices for snippet generation.\n  const rawUrl = interpolateUrlPathParams(interpolatedUrl, requestData.params, variables, { raw: true });\n\n  // Get the full language object based on current preferences\n  const selectedLanguage = useMemo(() => {\n    const fullName = generateCodePrefs.library === 'default'\n      ? generateCodePrefs.mainLanguage\n      : `${generateCodePrefs.mainLanguage}-${generateCodePrefs.library}`;\n\n    return languages.find((lang) => lang.name === fullName) || languages[0];\n  }, [generateCodePrefs.mainLanguage, generateCodePrefs.library, languages]);\n\n  // Resolve auth inheritance\n  const resolvedRequest = resolveInheritedAuth(item, collection);\n\n  // requestData.request contains either the normal request or example request data.\n  // We explicitly set auth from resolvedRequest to ensure inherited auth\n  // (from folders/collection) is resolved correctly in generated code.\n  const finalItem = {\n    ...item,\n    request: {\n      ...requestData.request,\n      auth: resolvedRequest.auth,\n      url: finalUrl\n    },\n    rawUrl\n  };\n\n  // Update modal title based on mode\n  const modalTitle = isExample ? `Generate Code - ${get(item, 'draft.examples', []).find((e) => e.uid === exampleUid)?.name || 'Example'}` : 'Generate Code';\n\n  return (\n    <Modal size=\"lg\" title={modalTitle} handleCancel={onClose} hideFooter={true}>\n      <StyledWrapper>\n        <div className=\"code-generator\">\n          <CodeViewToolbar />\n\n          <div className=\"editor-container\">\n            {validateURLWithVars(finalUrl) ? (\n              <CodeView\n                language={selectedLanguage}\n                item={finalItem}\n              />\n            ) : (\n              <div className=\"error-message\">\n                <h1>Invalid URL: {finalUrl}</h1>\n                <p>Please check the URL and try again</p>\n              </div>\n            )}\n          </div>\n        </div>\n      </StyledWrapper>\n    </Modal>\n  );\n};\n\nexport default GenerateCodeItem;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/interpolation.js",
    "content": "import { interpolate, interpolateObject } from '@usebruno/common';\nimport { cloneDeep } from 'lodash';\n\nexport const interpolateAuth = (auth, variables = {}) => {\n  if (!auth) return auth;\n  return interpolateObject(auth, variables);\n};\n\nexport const interpolateHeaders = (headers = [], variables = {}) => {\n  if (!headers) return [];\n  return headers.map((header) => {\n    if (header.enabled) {\n      return interpolateObject(header, variables);\n    }\n    return header;\n  });\n};\n\nexport const interpolateParams = (params = [], variables = {}) => {\n  if (!params) return [];\n  return params.map((param) => {\n    if (param.enabled) {\n      return interpolateObject(param, variables);\n    }\n    return param;\n  });\n};\n\nexport const interpolateBody = (body, variables = {}) => {\n  if (!body) return null;\n\n  const interpolatedBody = cloneDeep(body);\n\n  switch (body.mode) {\n    case 'json':\n      let parsed = body.json;\n      // If it's already a string, use it directly; if it's an object, stringify it first\n      if (typeof parsed === 'object') {\n        parsed = JSON.stringify(parsed);\n      }\n      parsed = interpolate(parsed, variables, { escapeJSONStrings: true });\n      try {\n        const jsonObj = JSON.parse(parsed);\n        interpolatedBody.json = JSON.stringify(jsonObj, null, 2);\n      } catch {\n        interpolatedBody.json = parsed;\n      }\n      break;\n\n    case 'text':\n      interpolatedBody.text = interpolate(body.text, variables);\n      break;\n\n    case 'xml':\n      interpolatedBody.xml = interpolate(body.xml, variables);\n      break;\n\n    case 'sparql':\n      interpolatedBody.sparql = interpolate(body.sparql, variables);\n      break;\n\n    case 'formUrlEncoded':\n      interpolatedBody.formUrlEncoded = Array.isArray(body.formUrlEncoded)\n        ? body.formUrlEncoded.map((param) => ({\n            ...param,\n            value: param.enabled ? interpolate(param.value, variables) : param.value\n          }))\n        : [];\n      break;\n\n    case 'multipartForm':\n      interpolatedBody.multipartForm = Array.isArray(body.multipartForm)\n        ? body.multipartForm.map((param) => ({\n            ...param,\n            value:\n              param.type === 'text' && param.enabled\n                ? interpolate(param.value, variables)\n                : param.value\n          }))\n        : [];\n      break;\n\n    default:\n      break;\n  }\n\n  return interpolatedBody;\n};\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/interpolation.spec.js",
    "content": "import { interpolateAuth, interpolateHeaders, interpolateBody, interpolateParams } from './interpolation';\n\ndescribe('interpolation utils', () => {\n  describe('interpolateAuth', () => {\n    it('should interpolate auth object', () => {\n      const auth = {\n        mode: 'basic',\n        basic: {\n          username: '{{user}}',\n          password: '{{pass}}'\n        }\n      };\n      const variables = { user: 'admin', pass: 'secret' };\n\n      const result = interpolateAuth(auth, variables);\n\n      expect(result).toEqual({\n        mode: 'basic',\n        basic: {\n          username: 'admin',\n          password: 'secret'\n        }\n      });\n    });\n\n    it('should return null for null auth', () => {\n      expect(interpolateAuth(null, {})).toBeNull();\n    });\n\n    it('should return undefined for undefined auth', () => {\n      expect(interpolateAuth(undefined, {})).toBeUndefined();\n    });\n  });\n\n  describe('interpolateHeaders', () => {\n    it('should interpolate header names and values', () => {\n      const headers = [\n        { name: 'X-{{headerName}}', value: '{{headerValue}}', enabled: true },\n        { name: 'Content-Type', value: 'application/json', enabled: true }\n      ];\n      const variables = { headerName: 'Custom', headerValue: 'test-value' };\n\n      const result = interpolateHeaders(headers, variables);\n\n      expect(result).toEqual([\n        { name: 'X-Custom', value: 'test-value', enabled: true },\n        { name: 'Content-Type', value: 'application/json', enabled: true }\n      ]);\n    });\n\n    it('should return empty array for empty headers', () => {\n      expect(interpolateHeaders([], {})).toEqual([]);\n    });\n  });\n\n  describe('interpolateBody', () => {\n    it('should return null for null body', () => {\n      expect(interpolateBody(null, {})).toBeNull();\n    });\n\n    it('should interpolate JSON body with escaping', () => {\n      const body = {\n        mode: 'json',\n        json: '{\"name\": \"{{name}}\", \"count\": {{count}}}'\n      };\n      const variables = { name: 'Test', count: 42 };\n\n      const result = interpolateBody(body, variables);\n\n      expect(result.mode).toBe('json');\n      expect(JSON.parse(result.json)).toEqual({ name: 'Test', count: 42 });\n    });\n\n    it('should interpolate text body', () => {\n      const body = { mode: 'text', text: 'Hello {{name}}' };\n      const result = interpolateBody(body, { name: 'World' });\n      expect(result.text).toBe('Hello World');\n    });\n\n    it('should interpolate xml body', () => {\n      const body = { mode: 'xml', xml: '<user>{{name}}</user>' };\n      const result = interpolateBody(body, { name: 'Alice' });\n      expect(result.xml).toBe('<user>Alice</user>');\n    });\n\n    it('should interpolate formUrlEncoded body for enabled params only', () => {\n      const body = {\n        mode: 'formUrlEncoded',\n        formUrlEncoded: [\n          { name: 'key1', value: '{{val1}}', enabled: true },\n          { name: 'key2', value: '{{val2}}', enabled: false }\n        ]\n      };\n      const variables = { val1: 'value1', val2: 'value2' };\n\n      const result = interpolateBody(body, variables);\n\n      expect(result.formUrlEncoded[0].value).toBe('value1');\n      expect(result.formUrlEncoded[1].value).toBe('{{val2}}');\n    });\n\n    it('should interpolate multipartForm body for enabled text params only', () => {\n      const body = {\n        mode: 'multipartForm',\n        multipartForm: [\n          { name: 'field1', value: '{{val}}', type: 'text', enabled: true },\n          { name: 'field2', value: '{{val}}', type: 'file', enabled: true }\n        ]\n      };\n      const variables = { val: 'interpolated' };\n\n      const result = interpolateBody(body, variables);\n\n      expect(result.multipartForm[0].value).toBe('interpolated');\n      expect(result.multipartForm[1].value).toBe('{{val}}');\n    });\n  });\n\n  describe('interpolateParams', () => {\n    it('should interpolate param names and values', () => {\n      const params = [\n        { name: '{{paramName}}', value: '{{paramValue}}', enabled: true },\n        { name: 'static', value: '{{val}}', enabled: false }\n      ];\n      const variables = { paramName: 'key', paramValue: 'value', val: 'skipped' };\n\n      const result = interpolateParams(params, variables);\n\n      expect(result[0].name).toBe('key');\n      expect(result[0].value).toBe('value');\n      expect(result[1].name).toBe('static');\n      expect(result[1].value).toBe('{{val}}');\n    });\n\n    it('should return empty array for empty params', () => {\n      expect(interpolateParams([], {})).toEqual([]);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.js",
    "content": "import { buildHarRequest } from 'utils/codegenerator/har';\nimport { getAuthHeaders } from 'utils/codegenerator/auth';\nimport { getAllVariables, getTreePathFromCollectionToItem, mergeHeaders } from 'utils/collections/index';\nimport { resolveInheritedAuth } from 'utils/auth';\nimport { get } from 'lodash';\nimport { interpolateAuth, interpolateHeaders, interpolateBody, interpolateParams } from './interpolation';\nimport { encodeUrl as encodeUrlCommon, stripOrigin } from '@usebruno/common/utils';\nimport { parse } from 'url';\nimport { stringify } from 'query-string';\n\nconst addCurlAuthFlags = (curlCommand, auth) => {\n  if (!auth || !curlCommand) return curlCommand;\n\n  const authMode = auth.mode;\n\n  if (authMode === 'digest' || authMode === 'ntlm') {\n    const username = get(auth, `${authMode}.username`, '');\n    const password = get(auth, `${authMode}.password`, '');\n    const credentials = password ? `${username}:${password}` : username;\n    const authFlag = authMode === 'digest' ? '--digest' : '--ntlm';\n    // Escape single quotes for shell safety: ' becomes '\\''\n    const escapedCredentials = credentials.replace(/'/g, `'\\\\''`);\n\n    const curlMatch = curlCommand.match(/^(curl(?:\\.exe)?)/i);\n    if (curlMatch) {\n      const curlCmd = curlMatch[1];\n      const restOfCommand = curlCommand.slice(curlCmd.length);\n      return `${curlCmd} ${authFlag} --user '${escapedCredentials}'${restOfCommand}`;\n    }\n  }\n\n  return curlCommand;\n};\n\nconst generateSnippet = ({ language, item, collection, shouldInterpolate = false }) => {\n  try {\n    // Get HTTPSnippet dynamically so mocks can be applied in tests\n    const { HTTPSnippet } = require('httpsnippet');\n\n    const variables = getAllVariables(collection, item);\n    const request = item.request;\n\n    let effectiveAuth = request.auth;\n    if (request.auth?.mode === 'inherit') {\n      const resolvedRequest = resolveInheritedAuth(item, collection);\n      effectiveAuth = resolvedRequest.auth;\n    }\n\n    // Get the request tree path and merge headers\n    const requestTreePath = getTreePathFromCollectionToItem(collection, item);\n    let headers = mergeHeaders(collection, request, requestTreePath);\n\n    // Add auth headers if needed (auth inheritance is resolved upstream)\n    if (request.auth && request.auth.mode !== 'none') {\n      if (shouldInterpolate) {\n        request.auth = interpolateAuth(request.auth, variables);\n      }\n\n      const authHeaders = getAuthHeaders(request.auth, collection, item);\n      headers = [...headers, ...authHeaders];\n    }\n\n    // Interpolate headers, body and params if needed\n    if (shouldInterpolate) {\n      headers = interpolateHeaders(headers, variables);\n      request.body = interpolateBody(request.body, variables);\n      request.params = interpolateParams(request.params, variables);\n    }\n\n    // Build HAR request\n    const harRequest = buildHarRequest({\n      request,\n      headers\n    });\n\n    // Generate snippet using HTTPSnippet\n    const snippet = new HTTPSnippet(harRequest);\n    let result = snippet.convert(language.target, language.client);\n\n    // For curl target, add special auth flags for digest/ntlm\n    if (language.target === 'shell' && language.client === 'curl') {\n      result = addCurlAuthFlags(result, effectiveAuth);\n    }\n\n    // Respect encodeUrl setting: when not explicitly true, replace HTTPSnippet's encoded path+query with the raw version.\n    // Replacing the path portion works for all targets since it's a substring of the full URL.\n    // encodeUrl defaults to false in the UI when undefined/null\n    const settings = item.draft ? get(item, 'draft.settings') : get(item, 'settings');\n    const rawUrl = item.rawUrl || request.url;\n    const parsed = parse(request.url, true, true);\n    const search = stringify(parsed.query);\n    const httpSnippetPath = search ? `${parsed.pathname}?${search}` : parsed.pathname;\n\n    let desiredPath;\n    if (settings?.encodeUrl === true) {\n      // Apply the same encodeUrl() transform used by the actual request execution path\n      // so the snippet matches what's sent on the wire.\n      const encodedUrl = encodeUrlCommon(rawUrl);\n      desiredPath = stripOrigin(encodedUrl);\n      // Strip fragment per RFC 3986 §3.5\n      desiredPath = desiredPath.replace(/#.*$/, '');\n    } else {\n      desiredPath = stripOrigin(rawUrl);\n      // The HTTP raw target (http/http1.1) uses the request line format:\n      //   METHOD <request-target> HTTP-version\n      // Spaces delimit these fields, so a literal space in the request-target\n      // would be parsed as the end of the URI (RFC 7230 §3.1.1).\n      if (language.target === 'http') {\n        desiredPath = desiredPath.replace(/ /g, '%20');\n      }\n    }\n\n    if (httpSnippetPath !== desiredPath && httpSnippetPath?.length > 1) {\n      result = result.replaceAll(httpSnippetPath, desiredPath);\n    }\n\n    return result;\n  } catch (error) {\n    console.error('Error generating code snippet:', error);\n    return 'Error generating code snippet';\n  }\n};\n\nexport {\n  generateSnippet\n};\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/utils/snippet-generator.spec.js",
    "content": "import { getAuthHeaders } from 'utils/codegenerator/auth';\n\njest.mock('httpsnippet', () => {\n  return {\n    HTTPSnippet: jest.fn().mockImplementation((harRequest) => ({\n      convert: jest.fn(() => {\n        const method = harRequest?.method || 'GET';\n        const url = harRequest?.url || 'http://example.com';\n        const hasBody = harRequest?.postData?.text;\n\n        if (method === 'POST' && hasBody) {\n          return `curl -X POST ${url} -H \"Content-Type: application/json\" -d '${hasBody}'`;\n        }\n        return `curl -X ${method} ${url}`;\n      })\n    }))\n  };\n});\n\njest.mock('utils/codegenerator/har', () => ({\n  buildHarRequest: jest.fn((data) => {\n    const request = data.request || {};\n    const method = request.method || 'GET';\n    const url = request.url || 'http://example.com';\n    const body = request.body || {};\n\n    const harRequest = {\n      method: method,\n      url: url,\n      headers: data.headers || [],\n      httpVersion: 'HTTP/1.1'\n    };\n\n    // Add body data for POST requests\n    if (method === 'POST' && body.mode === 'json' && body.json) {\n      harRequest.postData = {\n        mimeType: 'application/json',\n        text: body.json\n      };\n    }\n\n    return harRequest;\n  })\n}));\n\njest.mock('utils/codegenerator/auth', () => ({\n  getAuthHeaders: jest.fn(() => [])\n}));\n\njest.mock('utils/collections/index', () => {\n  const actual = jest.requireActual('utils/collections/index');\n\n  return {\n    ...actual,\n    getAllVariables: jest.fn((collection) => ({\n      ...collection?.globalEnvironmentVariables,\n      ...collection?.runtimeVariables,\n      ...collection?.processEnvVariables,\n      baseUrl: 'https://api.example.com',\n      apiKey: 'secret-key-123',\n      userId: '12345',\n      user: 'admin',\n      pass: 'secret123'\n    })),\n    getTreePathFromCollectionToItem: jest.fn(() => [])\n  };\n});\n\nimport { generateSnippet } from './snippet-generator';\n\ndescribe('Snippet Generator - Simple Tests', () => {\n  // Simple test request - easy to understand\n  const testRequest = {\n    uid: 'test-request-123',\n    name: 'test api call',\n    type: 'http-request',\n    request: {\n      method: 'POST',\n      url: 'https://api.example.com/{{endpoint}}',\n      headers: [\n        { uid: 'h1', name: 'Authorization', value: 'Bearer {{apiToken}}', enabled: true },\n        { uid: 'h2', name: 'Content-Type', value: 'application/json', enabled: true },\n        { uid: 'h3', name: 'X-Custom', value: '{{customValue}}', enabled: true }\n      ],\n      body: {\n        mode: 'json',\n        json: '{\"message\": \"{{greeting}}\", \"count\": {{number}}}'\n      },\n      auth: { mode: 'none' },\n      assertions: [],\n      tests: '',\n      docs: '',\n      params: [],\n      vars: { req: [] }\n    }\n  };\n\n  const testCollection = {\n    root: {\n      request: {\n        auth: { mode: 'none' },\n        headers: []\n      }\n    },\n    globalEnvironmentVariables: {\n      endpoint: 'data',\n      apiToken: 'token123',\n      customValue: 'test-value',\n      greeting: 'Hello World',\n      number: 42\n    },\n    runtimeVariables: {},\n    processEnvVariables: {}\n  };\n\n  const curlLanguage = { target: 'shell', client: 'curl' };\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n    require('httpsnippet').HTTPSnippet = jest.fn().mockImplementation((harRequest) => ({\n      convert: jest.fn(() => {\n        const method = harRequest?.method || 'GET';\n        const url = harRequest?.url || 'http://example.com';\n        const hasBody = harRequest?.postData?.text;\n\n        if (method === 'POST' && hasBody) {\n          return `curl -X POST ${url} -H \"Content-Type: application/json\" -d '${hasBody}'`;\n        }\n        return `curl -X ${method} ${url}`;\n      })\n    }));\n  });\n\n  it('should generate curl for POST request with JSON body', () => {\n    const result = generateSnippet({\n      language: curlLanguage,\n      item: testRequest,\n      collection: testCollection,\n      shouldInterpolate: false\n    });\n\n    expect(result).toBe('curl -X POST https://api.example.com/{{endpoint}} -H \"Content-Type: application/json\" -d \\'{\"message\": \"{{greeting}}\", \"count\": {{number}}}\\'');\n  });\n\n  it('should interpolate variables when enabled', () => {\n    const result = generateSnippet({\n      language: curlLanguage,\n      item: testRequest,\n      collection: testCollection,\n      shouldInterpolate: true\n    });\n\n    const expectedBody = `{\n  \"message\": \"Hello World\",\n  \"count\": 42\n}`;\n    expect(result).toBe(`curl -X POST https://api.example.com/{{endpoint}} -H \"Content-Type: application/json\" -d '${expectedBody}'`);\n  });\n\n  it('should handle GET requests', () => {\n    const getRequest = {\n      ...testRequest,\n      request: {\n        ...testRequest.request,\n        method: 'GET',\n        body: { mode: 'none' }\n      }\n    };\n\n    const result = generateSnippet({\n      language: curlLanguage,\n      item: getRequest,\n      collection: testCollection,\n      shouldInterpolate: false\n    });\n\n    expect(result).toBe('curl -X GET https://api.example.com/{{endpoint}}');\n  });\n\n  it('should handle requests with different headers', () => {\n    const requestWithDifferentHeaders = {\n      ...testRequest,\n      request: {\n        ...testRequest.request,\n        headers: [\n          { uid: 'h1', name: 'X-API-Key', value: '{{apiKey}}', enabled: true },\n          { uid: 'h2', name: 'Accept', value: 'application/json', enabled: true },\n          { uid: 'h3', name: 'User-Agent', value: 'TestApp/{{version}}', enabled: true }\n        ]\n      }\n    };\n\n    const collectionWithDifferentVars = {\n      ...testCollection,\n      globalEnvironmentVariables: {\n        ...testCollection.globalEnvironmentVariables,\n        apiKey: 'secret-key-456',\n        version: '1.0.0'\n      }\n    };\n\n    const result = generateSnippet({\n      language: curlLanguage,\n      item: requestWithDifferentHeaders,\n      collection: collectionWithDifferentVars,\n      shouldInterpolate: true\n    });\n\n    // Body should have interpolated variables with proper formatting\n    const expectedBody = `{\n  \"message\": \"Hello World\",\n  \"count\": 42\n}`;\n    expect(result).toBe(`curl -X POST https://api.example.com/{{endpoint}} -H \"Content-Type: application/json\" -d '${expectedBody}'`);\n  });\n\n  it('should handle complex nested JSON body', () => {\n    const complexBody = {\n      user: {\n        name: '{{userName}}',\n        settings: {\n          theme: '{{userTheme}}',\n          active: true\n        }\n      },\n      data: {\n        items: ['{{item1}}', '{{item2}}'],\n        total: '{{totalCount}}'\n      }\n    };\n\n    const requestWithComplexBody = {\n      ...testRequest,\n      request: {\n        ...testRequest.request,\n        body: {\n          mode: 'json',\n          json: JSON.stringify(complexBody, null, 2)\n        }\n      }\n    };\n\n    const collectionWithComplexVars = {\n      ...testCollection,\n      globalEnvironmentVariables: {\n        ...testCollection.globalEnvironmentVariables,\n        userName: 'Alice',\n        userTheme: 'dark',\n        item1: 'first',\n        item2: 'second',\n        totalCount: 100\n      }\n    };\n\n    const result = generateSnippet({\n      language: curlLanguage,\n      item: requestWithComplexBody,\n      collection: collectionWithComplexVars,\n      shouldInterpolate: true\n    });\n\n    const expectedComplexBody = JSON.stringify({\n      user: {\n        name: 'Alice',\n        settings: {\n          theme: 'dark',\n          active: true\n        }\n      },\n      data: {\n        items: ['first', 'second'],\n        total: '100'\n      }\n    }, null, 2);\n\n    expect(result).toBe(`curl -X POST https://api.example.com/{{endpoint}} -H \"Content-Type: application/json\" -d '${expectedComplexBody}'`);\n  });\n\n  it('should handle errors gracefully', () => {\n    // Set up the error mock after beforeEach has run\n    const originalHTTPSnippet = require('httpsnippet').HTTPSnippet;\n    require('httpsnippet').HTTPSnippet = jest.fn(() => {\n      throw new Error('Mock error!');\n    });\n\n    const originalConsoleError = console.error;\n    console.error = jest.fn();\n\n    const result = generateSnippet({\n      language: curlLanguage,\n      item: testRequest,\n      collection: testCollection,\n      shouldInterpolate: false\n    });\n\n    expect(result).toBe('Error generating code snippet');\n\n    require('httpsnippet').HTTPSnippet = originalHTTPSnippet;\n    console.error = originalConsoleError;\n  });\n\n  it('should work with JavaScript language', () => {\n    const javascriptLanguage = { target: 'javascript', client: 'fetch' };\n\n    const expectedJavaScriptCode = `fetch(\"https://api.example.com/data\", {\n  method: \"POST\",\n  headers: { \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ \"message\": \"Hello World\", \"count\": 42 })\n})`;\n\n    const originalHTTPSnippet = require('httpsnippet').HTTPSnippet;\n    require('httpsnippet').HTTPSnippet = jest.fn().mockImplementation(() => ({\n      convert: jest.fn(() => expectedJavaScriptCode)\n    }));\n\n    const result = generateSnippet({\n      language: javascriptLanguage,\n      item: testRequest,\n      collection: testCollection,\n      shouldInterpolate: false\n    });\n\n    expect(result).toBe(expectedJavaScriptCode);\n\n    // Restore the original mock\n    require('httpsnippet').HTTPSnippet = originalHTTPSnippet;\n  });\n\n  it('should interpolate simple headers and body variables', () => {\n    const simpleTestRequest = {\n      uid: 'test-123',\n      name: 'simple test',\n      type: 'http-request',\n      request: {\n        method: 'POST',\n        url: 'https://api.test.com/{{endpoint}}',\n        headers: [\n          { uid: 'h1', name: 'Authorization', value: 'Bearer {{token}}', enabled: true },\n          { uid: 'h2', name: 'X-User-ID', value: '{{userId}}', enabled: true },\n          { uid: 'h3', name: 'Content-Type', value: 'application/json', enabled: true }\n        ],\n        body: {\n          mode: 'json',\n          json: '{\"name\": \"{{userName}}\", \"email\": \"{{userEmail}}\", \"age\": {{userAge}}}'\n        }\n      }\n    };\n\n    // Simple collection with clear variable values\n    const simpleTestCollection = {\n      root: {\n        request: {\n          auth: { mode: 'none' },\n          headers: []\n        }\n      },\n      globalEnvironmentVariables: {\n        endpoint: 'users',\n        token: 'abc123token',\n        userId: 'user456',\n        userName: 'John Smith',\n        userEmail: 'john@test.com',\n        userAge: 30\n      },\n      runtimeVariables: {},\n      processEnvVariables: {}\n    };\n\n    const result = generateSnippet({\n      language: curlLanguage,\n      item: simpleTestRequest,\n      collection: simpleTestCollection,\n      shouldInterpolate: true\n    });\n\n    const expectedInterpolatedBody = `{\n  \"name\": \"John Smith\",\n  \"email\": \"john@test.com\",\n  \"age\": 30\n}`;\n\n    expect(result).toBe(`curl -X POST https://api.test.com/{{endpoint}} -H \"Content-Type: application/json\" -d '${expectedInterpolatedBody}'`);\n  });\n\n  it('should NOT interpolate when shouldInterpolate is false', () => {\n    const simpleTestRequest = {\n      uid: 'test-123',\n      name: 'simple test',\n      type: 'http-request',\n      request: {\n        method: 'POST',\n        url: 'https://api.test.com/{{endpoint}}',\n        headers: [\n          { uid: 'h1', name: 'Authorization', value: 'Bearer {{token}}', enabled: true },\n          { uid: 'h2', name: 'X-User-ID', value: '{{userId}}', enabled: true },\n          { uid: 'h3', name: 'Content-Type', value: 'application/json', enabled: true }\n        ],\n        body: {\n          mode: 'json',\n          json: '{\"name\": \"{{userName}}\", \"email\": \"{{userEmail}}\", \"age\": {{userAge}}}'\n        }\n      }\n    };\n\n    const simpleTestCollection = {\n      root: {\n        request: {\n          auth: { mode: 'none' },\n          headers: []\n        }\n      },\n      globalEnvironmentVariables: {\n        endpoint: 'users',\n        token: 'abc123token',\n        userId: 'user456',\n        userName: 'John Smith',\n        userEmail: 'john@test.com',\n        userAge: 30\n      },\n      runtimeVariables: {},\n      processEnvVariables: {}\n    };\n\n    const result = generateSnippet({\n      language: curlLanguage,\n      item: simpleTestRequest,\n      collection: simpleTestCollection,\n      shouldInterpolate: false\n    });\n\n    expect(result).toBe('curl -X POST https://api.test.com/{{endpoint}} -H \"Content-Type: application/json\" -d \\'{\"name\": \"{{userName}}\", \"email\": \"{{userEmail}}\", \"age\": {{userAge}}}\\'');\n  });\n\n  it('should interpolate auth credentials correctly', () => {\n    // Auth inheritance is resolved upstream in index.js before calling generateSnippet\n    // So the item already has the resolved auth (not 'inherit' mode)\n    const item = {\n      request: {\n        method: 'GET',\n        url: 'https://api.example.com',\n        auth: {\n          mode: 'basic',\n          basic: {\n            username: '{{user}}',\n            password: '{{pass}}'\n          }\n        }\n      }\n    };\n\n    const collection = {\n      root: {\n        request: {\n          auth: { mode: 'none' }\n        }\n      }\n    };\n\n    const { HTTPSnippet: mockedHTTPSnippet } = require('httpsnippet');\n    const { getAuthHeaders: actualGetAuthHeaders } = jest.requireActual('utils/codegenerator/auth');\n    getAuthHeaders.mockImplementation(actualGetAuthHeaders);\n\n    const language = { target: 'shell', client: 'curl' };\n\n    generateSnippet({\n      language,\n      item,\n      collection,\n      shouldInterpolate: true\n    });\n\n    const harRequest = mockedHTTPSnippet.mock.calls[0][0];\n\n    // \"admin:secret123\" encoded is \"YWRtaW46c2VjcmV0MTIz\"\n    expect(harRequest.headers).toContainEqual(\n      expect.objectContaining({\n        name: 'Authorization',\n        value: 'Basic YWRtaW46c2VjcmV0MTIz'\n      })\n    );\n  });\n});\n\n// Snippet should include inherited headers\ndescribe('generateSnippet – header inclusion in output', () => {\n  it('should include collection and folder headers in generated snippet', () => {\n    const language = { target: 'shell', client: 'curl' };\n\n    const collection = {\n      root: {\n        request: {\n          headers: [\n            { name: 'X-Collection', value: 'c', enabled: true }\n          ],\n          auth: { mode: 'none' }\n        }\n      }\n    };\n\n    const folder = {\n      uid: 'f1',\n      type: 'folder',\n      root: {\n        request: {\n          headers: [\n            { name: 'X-Folder', value: 'f', enabled: true }\n          ]\n        }\n      }\n    };\n\n    const item = {\n      uid: 'r1',\n      request: {\n        method: 'GET',\n        url: 'https://example.com',\n        headers: [],\n        auth: { mode: 'none' }\n      }\n    };\n\n    // Override tree path to include folder\n    const utilsCollections = require('utils/collections/index');\n    utilsCollections.getTreePathFromCollectionToItem.mockImplementation(() => [folder]);\n\n    // Custom HTTPSnippet mock that outputs headers list\n    const originalHTTPSnippet = require('httpsnippet').HTTPSnippet;\n    require('httpsnippet').HTTPSnippet = jest.fn().mockImplementation((harRequest) => ({\n      convert: jest.fn(() => `HEADERS:${harRequest.headers.map((h) => h.name).join(',')}`)\n    }));\n\n    const result = generateSnippet({ language, item, collection, shouldInterpolate: false });\n\n    // Restore original mock\n    require('httpsnippet').HTTPSnippet = originalHTTPSnippet;\n\n    expect(result).toContain('X-Collection');\n    expect(result).toContain('X-Folder');\n  });\n});\n\ndescribe('generateSnippet with edge-case bodies', () => {\n  const language = { target: 'shell', client: 'curl' };\n  const baseCollection = { root: { request: { auth: { mode: 'none' }, headers: [] } } };\n\n  it('should generate snippet for empty formUrlEncoded body when interpolation is disabled', () => {\n    const item = {\n      uid: 'req1',\n      request: {\n        method: 'POST',\n        url: 'https://example.com',\n        headers: [],\n        body: { mode: 'formUrlEncoded', formUrlEncoded: [] },\n        auth: { mode: 'none' }\n      }\n    };\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    expect(result).toMatch(/^curl -X POST/);\n  });\n\n  it('should generate snippet for empty multipartForm body when interpolation is disabled', () => {\n    const item = {\n      uid: 'req2',\n      request: {\n        method: 'POST',\n        url: 'https://example.com',\n        headers: [],\n        body: { mode: 'multipartForm' },\n        auth: { mode: 'none' }\n      }\n    };\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    expect(result).toMatch(/^curl -X POST/);\n  });\n\n  it('should generate snippet for undefined formUrlEncoded array with interpolation enabled', () => {\n    const item = {\n      uid: 'req3',\n      request: {\n        method: 'POST',\n        url: 'https://example.com',\n        headers: [],\n        body: { mode: 'formUrlEncoded' },\n        auth: { mode: 'none' }\n      }\n    };\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: true });\n    expect(result).toMatch(/^curl -X POST/);\n  });\n\n  it('should generate snippet for empty multipartForm array with interpolation enabled', () => {\n    const item = {\n      uid: 'req4',\n      request: {\n        method: 'POST',\n        url: 'https://example.com',\n        headers: [],\n        body: { mode: 'multipartForm', multipartForm: [] },\n        auth: { mode: 'none' }\n      }\n    };\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: true });\n    expect(result).toMatch(/^curl -X POST/);\n  });\n});\n\ndescribe('generateSnippet with OAuth2 authentication', () => {\n  const language = { target: 'shell', client: 'curl' };\n  const baseCollection = { root: { request: { auth: { mode: 'none' }, headers: [] } } };\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n    // Mock getAuthHeaders to return OAuth2 headers based on the auth config\n    const authUtils = require('utils/codegenerator/auth');\n    authUtils.getAuthHeaders.mockImplementation((requestAuth, collection = null, item = null) => {\n      if (requestAuth?.mode === 'oauth2') {\n        const oauth2Config = requestAuth.oauth2 || {};\n        const tokenPlacement = oauth2Config.tokenPlacement || 'header';\n        // Use the actual value from config, defaulting to 'Bearer' only if undefined\n        // Empty string should be preserved to test no-prefix scenarios\n        const tokenHeaderPrefix = oauth2Config.tokenHeaderPrefix !== undefined\n          ? oauth2Config.tokenHeaderPrefix\n          : 'Bearer';\n        let accessToken = oauth2Config.accessToken || '<access_token>';\n\n        // If collection and item are provided, try to look up stored credentials\n        if (collection && item && collection.oauth2Credentials) {\n          const grantType = oauth2Config.grantType || '';\n          const urlToLookup = grantType === 'implicit'\n            ? oauth2Config.authorizationUrl || ''\n            : oauth2Config.accessTokenUrl || '';\n          const credentialsId = oauth2Config.credentialsId || 'credentials';\n          const collectionUid = collection.uid;\n\n          if (urlToLookup && collectionUid) {\n            // Look up stored credentials (simplified - assumes URL is already interpolated in test data)\n            const credentialsData = collection.oauth2Credentials.find(\n              (creds) =>\n                creds?.url === urlToLookup\n                && creds?.collectionUid === collectionUid\n                && creds?.credentialsId === credentialsId\n            );\n\n            if (credentialsData?.credentials?.access_token) {\n              accessToken = credentialsData.credentials.access_token;\n            }\n          }\n        }\n\n        if (tokenPlacement === 'header') {\n          // Always trim the final result for consistent formatting\n          const headerValue = tokenHeaderPrefix\n            ? `${tokenHeaderPrefix} ${accessToken}`.trim()\n            : accessToken.trim();\n          return [\n            {\n              enabled: true,\n              name: 'Authorization',\n              value: headerValue\n            }\n          ];\n        }\n      }\n      return [];\n    });\n  });\n\n  it('should include OAuth2 Bearer token in Authorization header when tokenPlacement is header', () => {\n    const item = {\n      uid: 'oauth-req',\n      request: {\n        method: 'GET',\n        url: 'https://api.example.com/users',\n        headers: [],\n        auth: {\n          mode: 'oauth2',\n          oauth2: {\n            grantType: 'client_credentials',\n            tokenPlacement: 'header',\n            tokenHeaderPrefix: 'Bearer',\n            accessToken: 'test-access-token-123'\n          }\n        }\n      }\n    };\n\n    generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n\n    const harUtils = require('utils/codegenerator/har');\n    const harCall = harUtils.buildHarRequest.mock.calls[0][0];\n    expect(harCall.headers).toEqual(\n      expect.arrayContaining([\n        expect.objectContaining({\n          name: 'Authorization',\n          value: 'Bearer test-access-token-123'\n        })\n      ])\n    );\n  });\n\n  it('should use custom tokenHeaderPrefix when provided', () => {\n    const item = {\n      uid: 'oauth-req-custom',\n      request: {\n        method: 'GET',\n        url: 'https://api.example.com/users',\n        headers: [],\n        auth: {\n          mode: 'oauth2',\n          oauth2: {\n            grantType: 'client_credentials',\n            tokenPlacement: 'header',\n            tokenHeaderPrefix: 'OAuth',\n            accessToken: 'custom-token-456'\n          }\n        }\n      }\n    };\n\n    generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n\n    const harUtils = require('utils/codegenerator/har');\n    const harCall = harUtils.buildHarRequest.mock.calls[0][0];\n    expect(harCall.headers).toEqual(\n      expect.arrayContaining([\n        expect.objectContaining({\n          name: 'Authorization',\n          value: 'OAuth custom-token-456'\n        })\n      ])\n    );\n  });\n\n  it('should not include Authorization header when tokenPlacement is url', () => {\n    const item = {\n      uid: 'oauth-req-url',\n      request: {\n        method: 'GET',\n        url: 'https://api.example.com/users',\n        headers: [],\n        auth: {\n          mode: 'oauth2',\n          oauth2: {\n            grantType: 'client_credentials',\n            tokenPlacement: 'url',\n            tokenQueryKey: 'access_token',\n            accessToken: 'token-in-url'\n          }\n        }\n      }\n    };\n\n    generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n\n    const harUtils = require('utils/codegenerator/har');\n    const harCall = harUtils.buildHarRequest.mock.calls[0][0];\n    const authHeader = harCall.headers.find((h) => h.name === 'Authorization');\n    expect(authHeader).toBeUndefined();\n  });\n\n  it('should use placeholder when accessToken is not available', () => {\n    const item = {\n      uid: 'oauth-req-placeholder',\n      request: {\n        method: 'GET',\n        url: 'https://api.example.com/users',\n        headers: [],\n        auth: {\n          mode: 'oauth2',\n          oauth2: {\n            grantType: 'client_credentials',\n            tokenPlacement: 'header',\n            tokenHeaderPrefix: 'Bearer'\n          }\n        }\n      }\n    };\n\n    generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n\n    const harUtils = require('utils/codegenerator/har');\n    const harCall = harUtils.buildHarRequest.mock.calls[0][0];\n    expect(harCall.headers).toEqual(\n      expect.arrayContaining([\n        expect.objectContaining({\n          name: 'Authorization',\n          value: 'Bearer <access_token>'\n        })\n      ])\n    );\n  });\n\n  it('should handle empty tokenHeaderPrefix', () => {\n    const item = {\n      uid: 'oauth-req-no-prefix',\n      request: {\n        method: 'GET',\n        url: 'https://api.example.com/users',\n        headers: [],\n        auth: {\n          mode: 'oauth2',\n          oauth2: {\n            grantType: 'client_credentials',\n            tokenPlacement: 'header',\n            tokenHeaderPrefix: '',\n            accessToken: 'token-without-prefix'\n          }\n        }\n      }\n    };\n\n    generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n\n    const harUtils = require('utils/codegenerator/har');\n    const harCall = harUtils.buildHarRequest.mock.calls[0][0];\n    expect(harCall.headers).toEqual(\n      expect.arrayContaining([\n        expect.objectContaining({\n          name: 'Authorization',\n          value: 'token-without-prefix'\n        })\n      ])\n    );\n  });\n});\n\ndescribe('generateSnippet – digest and NTLM auth curl export', () => {\n  const language = { target: 'shell', client: 'curl' };\n\n  const baseCollection = {\n    root: {\n      request: {\n        headers: [],\n        auth: { mode: 'none' }\n      }\n    }\n  };\n\n  it('should add --digest flag and --user for digest auth', () => {\n    const item = {\n      uid: 'digest-req',\n      request: {\n        method: 'GET',\n        url: 'https://example.com/api',\n        headers: [],\n        body: { mode: 'none' },\n        auth: {\n          mode: 'digest',\n          digest: {\n            username: 'myuser',\n            password: 'mypass'\n          }\n        }\n      }\n    };\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    expect(result).toMatch(/^curl --digest --user 'myuser:mypass'/);\n  });\n\n  it('should add --ntlm flag and --user for NTLM auth', () => {\n    const item = {\n      uid: 'ntlm-req',\n      request: {\n        method: 'GET',\n        url: 'https://example.com/api',\n        headers: [],\n        body: { mode: 'none' },\n        auth: {\n          mode: 'ntlm',\n          ntlm: {\n            username: 'myuser',\n            password: 'mypass'\n          }\n        }\n      }\n    };\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    expect(result).toMatch(/^curl --ntlm --user 'myuser:mypass'/);\n  });\n\n  it('should handle digest auth with username only (no password)', () => {\n    const item = {\n      uid: 'digest-no-pass',\n      request: {\n        method: 'GET',\n        url: 'https://example.com/api',\n        headers: [],\n        body: { mode: 'none' },\n        auth: {\n          mode: 'digest',\n          digest: {\n            username: 'myuser',\n            password: ''\n          }\n        }\n      }\n    };\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    expect(result).toMatch(/^curl --digest --user 'myuser'/);\n  });\n});\n\ndescribe('generateSnippet – encodeUrl setting', () => {\n  const language = { target: 'shell', client: 'curl' };\n  const baseCollection = { root: { request: { auth: { mode: 'none' }, headers: [] } } };\n\n  // Replicate HTTPSnippet's internal encoding to get encoded path+query\n  const getEncodedPath = (url) => {\n    const { parse } = require('url');\n    const { stringify } = require('query-string');\n    const parsed = parse(url, true, true);\n    if (!parsed.query || Object.keys(parsed.query).length === 0) {\n      return parsed.pathname;\n    }\n    const search = stringify(parsed.query);\n    return search ? `${parsed.pathname}?${search}` : parsed.pathname;\n  };\n\n  const makeItem = (url, settings, draft) => ({\n    uid: 'enc-req',\n    request: {\n      method: 'GET',\n      url,\n      headers: [],\n      body: { mode: 'none' },\n      auth: { mode: 'none' }\n    },\n    ...(settings !== undefined && { settings }),\n    ...(draft !== undefined && { draft })\n  });\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n    // Mock HTTPSnippet to simulate encoding (same pipeline as the real library)\n    require('httpsnippet').HTTPSnippet = jest.fn().mockImplementation((harRequest) => ({\n      convert: jest.fn((target) => {\n        const method = harRequest?.method || 'GET';\n        const url = harRequest?.url || 'http://example.com';\n        const { parse } = require('url');\n        const parsed = parse(url, false, true);\n        const encodedPath = getEncodedPath(url);\n        // Simulate targets that use only the path (e.g., python http.client, raw HTTP)\n        if (target === 'python') {\n          return `conn.request(\"${method}\", \"${encodedPath}\", headers=headers)`;\n        }\n        // Full URL targets: reconstruct with encoded path\n        const fullEncodedUrl = `${parsed.protocol}//${parsed.host}${encodedPath}`;\n        return `curl -X ${method} '${fullEncodedUrl}'`;\n      })\n    }));\n  });\n\n  it('should preserve equals signs in query values when encodeUrl is false', () => {\n    const rawUrl = 'https://example.com/api?token=abc123==&type=test';\n    const item = makeItem(rawUrl, { encodeUrl: false });\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    expect(result).toContain('token=abc123==');\n    // %3D = encoded '='\n    expect(result).not.toContain('%3D');\n  });\n\n  it('should preserve email with plus alias and @ when encodeUrl is false', () => {\n    const rawUrl = 'https://example.com/invite?email=test+alias@example.com';\n    const item = makeItem(rawUrl, { encodeUrl: false });\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    expect(result).toContain('email=test+alias@example.com');\n  });\n\n  it('should preserve redirect URL with colons and slashes when encodeUrl is false', () => {\n    const rawUrl = 'https://example.com/auth?redirect=https://other.com/callback&scope=read';\n    const item = makeItem(rawUrl, { encodeUrl: false });\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    expect(result).toContain('redirect=https://other.com/callback');\n    // %3A = encoded ':'\n    expect(result).not.toContain('%3A');\n    // %2F = encoded '/'\n    expect(result).not.toContain('%2F');\n  });\n\n  it('should preserve comma-separated values when encodeUrl is false', () => {\n    const rawUrl = 'https://example.com/filter?tags=a,b,c&time=10:30';\n    const item = makeItem(rawUrl, { encodeUrl: false });\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    expect(result).toContain('tags=a,b,c');\n    expect(result).toContain('time=10:30');\n  });\n\n  it('should encode URL when encodeUrl is true', () => {\n    const rawUrl = 'https://example.com/api?token=abc123==&type=test';\n    const item = makeItem(rawUrl, { encodeUrl: true });\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    // %3D%3D = encoded '=='\n    expect(result).toContain('%3D%3D');\n  });\n\n  it('should preserve raw URL when settings are absent (encodeUrl defaults to false)', () => {\n    const rawUrl = 'https://example.com/auth?redirect=https://other.com/callback';\n    const item = makeItem(rawUrl);\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    expect(result).toContain('redirect=https://other.com/callback');\n    // %3A = encoded ':'\n    expect(result).not.toContain('%3A');\n  });\n\n  it('should be a no-op for URLs without query params and no encoding needed', () => {\n    const rawUrl = 'https://example.com/api/users';\n    const item = makeItem(rawUrl, { encodeUrl: false });\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    expect(result).toBe(`curl -X GET '${rawUrl}'`);\n  });\n\n  it('should preserve spaces in pathname when encodeUrl is false and rawUrl is provided', () => {\n    const encodedUrl = 'https://example.com/my%20path/hello%20world?token=abc123==';\n    const item = {\n      ...makeItem(encodedUrl, { encodeUrl: false }),\n      rawUrl: 'https://example.com/my path/hello world?token=abc123=='\n    };\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    expect(result).toContain('/my path/hello world?token=abc123==');\n    expect(result).not.toContain('%20');\n    expect(result).not.toContain('%3D');\n  });\n\n  it('should preserve spaces in pathname without query params when encodeUrl is false', () => {\n    const encodedUrl = 'https://example.com/my%20path/hello%20world';\n    const item = {\n      ...makeItem(encodedUrl, { encodeUrl: false }),\n      rawUrl: 'https://example.com/my path/hello world'\n    };\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    expect(result).toContain('/my path/hello world');\n    expect(result).not.toContain('%20');\n  });\n\n  it('should preserve spaces in path-only targets (e.g., python) when encodeUrl is false', () => {\n    const pythonLanguage = { target: 'python', client: 'python3' };\n    const encodedUrl = 'https://example.com/my%20path/hello%20world?q=test';\n    const item = {\n      ...makeItem(encodedUrl, { encodeUrl: false }),\n      rawUrl: 'https://example.com/my path/hello world?q=test'\n    };\n\n    const result = generateSnippet({ language: pythonLanguage, item, collection: baseCollection, shouldInterpolate: false });\n    expect(result).toContain('/my path/hello world?q=test');\n    expect(result).not.toContain('%20');\n  });\n\n  it('should preserve spaces in query values when encodeUrl is false and rawUrl is provided', () => {\n    const encodedUrl = 'https://example.com/api?token=abc%20123==&type=test';\n    const item = {\n      ...makeItem(encodedUrl, { encodeUrl: false }),\n      rawUrl: 'https://example.com/api?token=abc 123==&type=test'\n    };\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    expect(result).toContain('token=abc 123==');\n    expect(result).not.toContain('%20');\n    expect(result).not.toContain('%3D');\n  });\n\n  it('should still work when rawUrl is not provided (backward compatibility)', () => {\n    const rawUrl = 'https://example.com/api?token=abc123==&type=test';\n    const item = makeItem(rawUrl, { encodeUrl: false });\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    expect(result).toContain('token=abc123==');\n    expect(result).not.toContain('%3D');\n  });\n\n  it('should keep spaces as %20 for http target when encodeUrl is false (HTTP spec compliance)', () => {\n    const httpLanguage = { target: 'http', client: 'http1.1' };\n    const encodedUrl = 'https://example.com/api?token=abc%20123==&type=test';\n    const item = {\n      ...makeItem(encodedUrl, { encodeUrl: false }),\n      rawUrl: 'https://example.com/api?token=abc 123==&type=test'\n    };\n    const result = generateSnippet({ language: httpLanguage, item, collection: baseCollection, shouldInterpolate: false });\n    // Spaces must remain encoded for valid HTTP request line\n    expect(result).toContain('%20');\n    // But other chars like = should still be decoded\n    expect(result).not.toContain('%3D');\n  });\n\n  it('should preserve user-typed %20 when encodeUrl is false (not decode to space)', () => {\n    const preEncodedUrl = 'https://example.com/api?token=abc%20123%3D%3D&type=test';\n    const item = {\n      ...makeItem(preEncodedUrl, { encodeUrl: false }),\n      rawUrl: preEncodedUrl // rawUrl has %20 intact (no decodeURI applied)\n    };\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    // %20 should be preserved, not decoded to a literal space\n    expect(result).toContain('%20');\n    // %3D should also be preserved\n    expect(result).toContain('%3D%3D');\n    // No double-encoding\n    expect(result).not.toContain('%2520');\n    expect(result).not.toContain('%253D');\n  });\n\n  it('should double-encode pre-encoded %20 when encodeUrl is true', () => {\n    const preEncodedUrl = 'https://example.com/api?token=abc%20123%3D%3D&type=test';\n    const item = {\n      ...makeItem(preEncodedUrl, { encodeUrl: true }),\n      rawUrl: preEncodedUrl\n    };\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    // %20 → %2520 because encodeURIComponent encodes the literal '%' in the already-encoded value\n    expect(result).toContain('%2520');\n    // %3D → %253D for the same reason\n    expect(result).toContain('%253D');\n  });\n\n  it('should preserve OData-style paths with parenthesized params when encodeUrl is false', () => {\n    const rawUrl = 'https://example.com/odata/Products(123)/Categories(456)?$expand=Items&$filter=Price gt 10';\n    const item = {\n      ...makeItem(rawUrl, { encodeUrl: false }),\n      rawUrl\n    };\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    expect(result).toContain('Products(123)/Categories(456)');\n    expect(result).toContain('$expand=Items');\n    expect(result).toContain('$filter=Price gt 10');\n    // $ should not be encoded\n    expect(result).not.toContain('%24');\n  });\n\n  it('should use draft settings when draft exists', () => {\n    const rawUrl = 'https://example.com/api?token=abc123==&type=test';\n    const item = makeItem(rawUrl, { encodeUrl: true }, { settings: { encodeUrl: false } });\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    expect(result).toContain('token=abc123==');\n    // %3D%3D = encoded '=='\n    expect(result).not.toContain('%3D%3D');\n  });\n\n  it('should replace encoded path for targets that use only path+query (e.g., python http.client)', () => {\n    const pythonLanguage = { target: 'python', client: 'python3' };\n    const rawUrl = 'https://example.com/api?token=abc123==&type=test';\n    const item = makeItem(rawUrl, { encodeUrl: false });\n\n    const result = generateSnippet({ language: pythonLanguage, item, collection: baseCollection, shouldInterpolate: false });\n    expect(result).toContain('/api?token=abc123==&type=test');\n    // %3D = encoded '='\n    expect(result).not.toContain('%3D');\n  });\n\n  it('should preserve URL fragment (#) in snippet when encodeUrl is false', () => {\n    // Intentional asymmetry: when encodeUrl is false (raw mode), generateSnippet preserves the\n    // user-supplied URL as-is, including any fragment. This contrasts with encodeUrl: true,\n    // which strips fragments per RFC 3986 §3.5. The rawUrl is preserved through the makeItem\n    // call with { encodeUrl: false } and passed to generateSnippet, which intentionally treats\n    // it as a user-specified string not subject to RFC-compliant stripping. This is a designed\n    // behavior to honor user intent in raw mode, not a bug. This behavior can be revisited in\n    // the future if requirements or RFC interpretations change.\n    const rawUrl = 'https://example.com/api?token=abc==#section';\n    const item = makeItem(rawUrl, { encodeUrl: false });\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    expect(result).toContain('#section');\n    expect(result).toContain('token=abc==');\n    expect(result).not.toContain('%3D');\n  });\n\n  it('should not include URL fragment (#) in snippet when encodeUrl is true', () => {\n    const rawUrl = 'https://example.com/api?token=abc==#section';\n    const item = makeItem(rawUrl, { encodeUrl: true });\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    // Fragment is stripped — correct per RFC 3986 §3.5: user agents MUST NOT include the fragment\n    // in the HTTP request target sent to the origin server (though fragments can still appear in\n    // user-facing URLs, SPA routing, and are inherited across redirects per RFC 9110 §10.2.2).\n    // https://datatracker.ietf.org/doc/html/rfc3986#section-3.5\n    // https://datatracker.ietf.org/doc/html/rfc9110#section-10.2.2\n    expect(result).not.toContain('#section');\n    expect(result).toContain('%3D%3D');\n  });\n\n  it('should single-encode spaces and special chars when encodeUrl is true and rawUrl is provided', () => {\n    // The raw URL (before new URL() encoding) contains literal spaces and @.\n    // encodeUrl() should encode them once: space → %20, @ → %40.\n    // Previously this double-encoded because request.url was already encoded by new URL().\n    const encodedUrl = 'https://example.com/api?name=abc%20os&email=user%40test.com';\n    const item = {\n      ...makeItem(encodedUrl, { encodeUrl: true }),\n      rawUrl: 'https://example.com/api?name=abc os&email=user@test.com'\n    };\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    // space → %20 (single encoding, not %2520)\n    expect(result).toContain('%20');\n    expect(result).not.toContain('%2520');\n    // @ → %40 (single encoding, not %2540)\n    expect(result).toContain('%40');\n    expect(result).not.toContain('%2540');\n  });\n\n  it('should encode special chars in query values when encodeUrl is true (e.g., redirect URLs)', () => {\n    const rawUrl = 'https://example.com/auth?redirect=https://other.com/callback&scope=read';\n    const item = makeItem(rawUrl, { encodeUrl: true });\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    // : → %3A, / → %2F when encodeURIComponent is applied to query values\n    expect(result).toContain('%3A');\n    expect(result).toContain('%2F');\n  });\n\n  it('should strip fragment and apply encodeUrl when both are present and encodeUrl is true', () => {\n    const rawUrl = 'https://example.com/api?redirect=https://other.com/cb#section';\n    const item = makeItem(rawUrl, { encodeUrl: true });\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    // Fragment stripped per RFC 3986\n    expect(result).not.toContain('#section');\n    // Query value should be encoded\n    expect(result).toContain('%3A');\n    expect(result).toContain('%2F');\n  });\n\n  it('should be a no-op for path-only URLs when encodeUrl is true (no query params to encode)', () => {\n    const rawUrl = 'https://example.com/api/users';\n    const item = makeItem(rawUrl, { encodeUrl: true });\n\n    const result = generateSnippet({ language, item, collection: baseCollection, shouldInterpolate: false });\n    expect(result).toBe(`curl -X GET '${rawUrl}'`);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .advanced-options {\n    .caret {\n      color: ${(props) => props.theme.textLink};\n      fill: ${(props) => props.theme.textLink};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js",
    "content": "import React, { useRef, useEffect, useState, forwardRef } from 'react';\nimport { useFormik } from 'formik';\nimport * as Yup from 'yup';\nimport Modal from 'components/Modal';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { isItemAFolder } from 'utils/tabs';\nimport { renameItem, saveRequest, closeTabs } from 'providers/ReduxStore/slices/collections/actions';\nimport path from 'utils/common/path';\nimport { IconArrowBackUp, IconEdit, IconCaretDown } from '@tabler/icons';\nimport { sanitizeName, validateName, validateNameError } from 'utils/common/regex';\nimport toast from 'react-hot-toast';\nimport Help from 'components/Help';\nimport PathDisplay from 'components/PathDisplay';\nimport Portal from 'components/Portal';\nimport Dropdown from 'components/Dropdown';\nimport StyledWrapper from './StyledWrapper';\nimport Button from 'ui/Button';\n\nconst RenameCollectionItem = ({ collectionUid, item, onClose }) => {\n  const dispatch = useDispatch();\n  const collection = useSelector((state) => state.collections.collections?.find((c) => c.uid === collectionUid));\n  const isFolder = isItemAFolder(item);\n  const inputRef = useRef();\n  const [isEditing, toggleEditing] = useState(false);\n  const itemName = item?.name;\n  const itemType = item?.type;\n  const itemFilename = item?.filename ? path.parse(item?.filename).name : '';\n  const [showFilesystemName, toggleShowFilesystemName] = useState(false);\n\n  const dropdownTippyRef = useRef();\n  const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);\n\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: {\n      name: itemName,\n      filename: sanitizeName(itemFilename)\n    },\n    validationSchema: Yup.object({\n      name: Yup.string()\n        .min(1, 'must be at least 1 character')\n        .max(255, 'must be 255 characters or less')\n        .required('name is required'),\n      filename: Yup.string()\n        .min(1, 'must be at least 1 character')\n        .max(255, 'must be 255 characters or less')\n        .required('name is required')\n        .test('is-valid-name', function (value) {\n          const isValid = validateName(value);\n          return isValid ? true : this.createError({ message: validateNameError(value) });\n        })\n        .test('not-reserved', `The file names \"collection\" and \"folder\" are reserved in bruno`, (value) => !['collection', 'folder'].includes(value))\n    }),\n    onSubmit: async (values) => {\n      // if there is unsaved changes in the request,\n      // save them before renaming the request\n      if ((item.name === values.name) && (itemFilename === values.filename)) {\n        return;\n      }\n      if (!isFolder && item.draft) {\n        await dispatch(saveRequest(item.uid, collectionUid, true));\n      }\n      const { name: newName, filename: newFilename } = values;\n      try {\n        let renameConfig = {\n          itemUid: item.uid,\n          collectionUid\n        };\n        renameConfig['newName'] = newName;\n        if (itemFilename !== newFilename) {\n          renameConfig['newFilename'] = newFilename;\n        }\n        await dispatch(renameItem(renameConfig));\n        if (isFolder) {\n          dispatch(closeTabs({ tabUids: [item.uid] }));\n        }\n        onClose();\n      } catch (error) {\n        toast.error(error.message || 'An error occurred while renaming');\n      }\n    }\n  });\n\n  useEffect(() => {\n    if (inputRef && inputRef.current) {\n      inputRef.current.focus();\n    }\n  }, [inputRef]);\n\n  const AdvancedOptions = forwardRef((props, ref) => {\n    return (\n      <div ref={ref} className=\"flex mr-2 text-link cursor-pointer items-center\">\n        <button\n          className=\"btn-advanced\"\n          type=\"button\"\n        >\n          Options\n        </button>\n        <IconCaretDown className=\"caret ml-1\" size={14} strokeWidth={2} />\n      </div>\n    );\n  });\n\n  return (\n    <Portal>\n      <StyledWrapper>\n        <Modal\n          size=\"md\"\n          title={`Rename ${isFolder ? 'Folder' : 'Request'}`}\n          handleCancel={onClose}\n          hideFooter\n        >\n          <form className=\"bruno-form\" onSubmit={formik.handleSubmit}>\n            <div className=\"flex flex-col mt-2\">\n              <label htmlFor=\"name\" className=\"block font-medium\">\n                {isFolder ? 'Folder' : 'Request'} Name\n              </label>\n              <input\n                id=\"collection-item-name\"\n                type=\"text\"\n                name=\"name\"\n                ref={inputRef}\n                className=\"block textbox mt-2 w-full\"\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n                onChange={(e) => {\n                  formik.setFieldValue('name', e.target.value);\n                  !isEditing && formik.setFieldValue('filename', sanitizeName(e.target.value));\n                }}\n                value={formik.values.name || ''}\n              />\n              {formik.touched.name && formik.errors.name ? <div className=\"text-red-500\">{formik.errors.name}</div> : null}\n            </div>\n\n            {showFilesystemName && (\n              <div className=\"mt-4\">\n                <div className=\"flex items-center justify-between\">\n                  <label htmlFor=\"filename\" className=\"flex items-center font-medium\">\n                    {isFolder ? 'Folder' : 'File'} Name <small className=\"font-normal text-muted ml-1\">(on filesystem)</small>\n                    { isFolder ? (\n                      <Help width=\"300\">\n                        <p>\n                          You can choose to save the folder as a different name on your file system versus what is displayed in the app.\n                        </p>\n                      </Help>\n                    ) : (\n                      <Help width=\"300\">\n                        <p>\n                          Bruno saves each request as a file in your collection's folder.\n                        </p>\n                        <p className=\"mt-2\">\n                          You can choose a file name different from your request's name or one compatible with filesystem rules.\n                        </p>\n                      </Help>\n                    )}\n                  </label>\n                  {isEditing ? (\n                    <IconArrowBackUp\n                      className=\"cursor-pointer opacity-50 hover:opacity-80\"\n                      size={16}\n                      strokeWidth={1.5}\n                      onClick={() => toggleEditing(false)}\n                    />\n                  ) : (\n                    <IconEdit\n                      className=\"cursor-pointer opacity-50 hover:opacity-80\"\n                      size={16}\n                      strokeWidth={1.5}\n                      onClick={() => toggleEditing(true)}\n                      data-testid=\"rename-request-edit-icon\"\n                    />\n                  )}\n                </div>\n                {isEditing ? (\n                  <div className=\"relative flex flex-row gap-1 items-center justify-between\">\n                    <input\n                      id=\"file-name\"\n                      type=\"text\"\n                      name=\"filename\"\n                      placeholder={isFolder ? 'Folder Name' : 'File Name'}\n                      className=\"!pr-10 block textbox mt-2 w-full\"\n                      autoComplete=\"off\"\n                      autoCorrect=\"off\"\n                      autoCapitalize=\"off\"\n                      spellCheck=\"false\"\n                      onChange={formik.handleChange}\n                      value={formik.values.filename || ''}\n                    />\n                    {itemType !== 'folder' && <span className=\"absolute right-2 top-4 flex justify-center items-center file-extension\">.{collection?.format || 'bru'}</span>}\n                  </div>\n                ) : (\n                  <div className=\"relative flex flex-row gap-1 items-center justify-between\">\n                    <PathDisplay\n                      baseName={formik.values.filename}\n                    />\n                  </div>\n                )}\n                {formik.touched.filename && formik.errors.filename ? (\n                  <div className=\"text-red-500\">{formik.errors.filename}</div>\n                ) : null}\n              </div>\n            )}\n            <div className=\"flex justify-between items-center mt-8 bruno-modal-footer\">\n              <div className=\"flex advanced-options\">\n                <Dropdown onCreate={onDropdownCreate} icon={<AdvancedOptions />} placement=\"bottom-start\">\n                  <div\n                    className=\"dropdown-item\"\n                    key=\"show-filesystem-name\"\n                    onClick={(e) => {\n                      dropdownTippyRef.current.hide();\n                      toggleShowFilesystemName(!showFilesystemName);\n                    }}\n                  >\n                    {showFilesystemName ? 'Hide Filesystem Name' : 'Show Filesystem Name'}\n                  </div>\n                </Dropdown>\n              </div>\n              <div className=\"flex justify-end\">\n                <Button type=\"button\" color=\"secondary\" variant=\"ghost\" onClick={onClose} className=\"mr-2\">\n                  Cancel\n                </Button>\n                <Button type=\"submit\">\n                  Rename\n                </Button>\n              </div>\n            </div>\n          </form>\n        </Modal>\n      </StyledWrapper>\n    </Portal>\n  );\n};\n\nexport default RenameCollectionItem;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RequestMethod/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  font-size: ${(props) => props.theme.font.size.xs};\n  display: flex;\n  align-self: stretch;\n  align-items: center;\n  min-width: 34px;\n  flex-shrink: 0;\n\n  span {\n    position: relative;\n    top: 1px;\n  }\n\n  .method-get {\n    color: ${(props) => props.theme.request.methods.get};\n  }\n  .method-post {\n    color: ${(props) => props.theme.request.methods.post};\n  }\n  .method-put {\n    color: ${(props) => props.theme.request.methods.put};\n  }\n  .method-delete {\n    color: ${(props) => props.theme.request.methods.delete};\n  }\n  .method-patch {\n    color: ${(props) => props.theme.request.methods.patch};\n  }\n  .method-options {\n    color: ${(props) => props.theme.request.methods.options};\n  }\n  .method-head {\n    color: ${(props) => props.theme.request.methods.head};\n  }\n  .method-grpc {\n    color: ${(props) => props.theme.request.grpc};\n  }\n  .method-ws {\n    color: ${(props) => props.theme.request.ws};\n  }\n  .method-graphql {\n    color: ${(props) => props.theme.request.gql};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RequestMethod/index.js",
    "content": "import classnames from 'classnames';\nimport React from 'react';\nimport StyledWrapper from './StyledWrapper';\n\nconst getMethodFlags = (item) => ({\n  isGrpc: item.type === 'grpc-request',\n  isWS: item.type === 'ws-request',\n  isGraphQL: item.type === 'graphql-request'\n});\n\nconst getMethodText = (item, { isGrpc, isWS, isGraphQL }) => {\n  if (isGrpc) return 'grpc';\n  if (isWS) return 'ws';\n  if (isGraphQL) return 'gql';\n  return item.request.method.length > 5\n    ? item.request.method.substring(0, 3)\n    : item.request.method;\n};\n\nconst getClassname = (method = '', { isGrpc, isWS, isGraphQL }) => {\n  method = method.toLocaleLowerCase();\n  return classnames('mr-1', {\n    'method-get': method === 'get',\n    'method-post': method === 'post',\n    'method-put': method === 'put',\n    'method-delete': method === 'delete',\n    'method-patch': method === 'patch',\n    'method-head': method === 'head',\n    'method-options': method === 'options',\n    'method-grpc': isGrpc,\n    'method-ws': isWS,\n    'method-graphql': isGraphQL\n  });\n};\n\nconst RequestMethod = ({ item }) => {\n  if (!['http-request', 'graphql-request', 'grpc-request', 'ws-request'].includes(item.type)) {\n    return null;\n  }\n\n  const flags = getMethodFlags(item);\n  const methodText = getMethodText(item, flags);\n  const className = getClassname(item.request.method, flags);\n\n  return (\n    <StyledWrapper>\n      <div className={className}>\n        <span className=\"uppercase\">\n          {methodText}\n        </span>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default RequestMethod;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  .bruno-modal-content {\n    padding-bottom: 1rem;\n  }\n  .warning {\n    color: ${(props) => props.theme.colors.text.danger};\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RunCollectionItem/index.js",
    "content": "import React from 'react';\nimport get from 'lodash/get';\nimport { uuid } from 'utils/common';\nimport Modal from 'components/Modal';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { addTab } from 'providers/ReduxStore/slices/tabs';\nimport { runCollectionFolder } from 'providers/ReduxStore/slices/collections/actions';\nimport { flattenItems } from 'utils/collections';\nimport StyledWrapper from './StyledWrapper';\nimport { areItemsLoading } from 'utils/collections';\nimport RunnerTags from 'components/RunnerResults/RunnerTags/index';\nimport { getRequestItemsForCollectionRun } from 'utils/collections/index';\nimport Button from 'ui/Button';\n\nconst RunCollectionItem = ({ collectionUid, item, onClose }) => {\n  const dispatch = useDispatch();\n\n  const collection = useSelector((state) => state.collections.collections?.find((c) => c.uid === collectionUid));\n  const isCollectionRunInProgress = collection?.runnerResult?.info?.status && (collection?.runnerResult?.info?.status !== 'ended');\n\n  // tags for the collection run\n  const tags = get(collection, 'runnerTags', { include: [], exclude: [] });\n\n  // have tags been enabled for the collection run\n  const tagsEnabled = get(collection, 'runnerTagsEnabled', false);\n\n  const onSubmit = (recursive) => {\n    dispatch(\n      addTab({\n        uid: uuid(),\n        collectionUid: collection.uid,\n        type: 'collection-runner'\n      })\n    );\n    if (!isCollectionRunInProgress) {\n      dispatch(runCollectionFolder(collection.uid, item ? item.uid : null, recursive, 0, tagsEnabled && tags));\n    }\n    onClose();\n  };\n\n  const handleViewRunner = (e) => {\n    e.preventDefault();\n    dispatch(\n      addTab({\n        uid: uuid(),\n        collectionUid: collection.uid,\n        type: 'collection-runner'\n      })\n    );\n    onClose();\n  };\n\n  const isFolderLoading = areItemsLoading(item);\n\n  const requestItemsForRecursiveFolderRun = getRequestItemsForCollectionRun({ recursive: true, tags, items: item ? item.items : collection.items });\n  const totalRequestItemsCountForRecursiveFolderRun = requestItemsForRecursiveFolderRun.length;\n  const shouldDisableRecursiveFolderRun = totalRequestItemsCountForRecursiveFolderRun <= 0;\n\n  const requestItemsForFolderRun = getRequestItemsForCollectionRun({ recursive: false, tags, items: item ? item.items : collection.items });\n  const totalRequestItemsCountForFolderRun = requestItemsForFolderRun.length;\n  const shouldDisableFolderRun = totalRequestItemsCountForFolderRun <= 0;\n\n  return (\n    <StyledWrapper>\n      <Modal size=\"md\" title=\"Collection Runner\" hideFooter={true} handleCancel={onClose}>\n        <div>\n          <div className=\"mb-1\">\n            <span className=\"font-medium\">Run</span>\n            <span className=\"ml-1 text-xs\">({totalRequestItemsCountForFolderRun} requests)</span>\n          </div>\n          <div className=\"mb-8\">This will only run the requests in this folder.</div>\n          <div className=\"mb-1\">\n            <span className=\"font-medium\">Recursive Run</span>\n            <span className=\"ml-1 text-xs\">({totalRequestItemsCountForRecursiveFolderRun} requests)</span>\n          </div>\n          <div className={isFolderLoading ? 'mb-2' : 'mb-8'}>This will run all the requests in this folder and all its subfolders.</div>\n          {isFolderLoading ? <div className=\"mb-8 warning\">Requests in this folder are still loading.</div> : null}\n          {isCollectionRunInProgress ? <div className=\"mb-6 warning\">A Collection Run is already in progress.</div> : null}\n\n          {/* Tags for the collection run */}\n          <RunnerTags collectionUid={collection.uid} className=\"mb-6\" />\n\n          <div className=\"flex justify-end bruno-modal-footer\">\n            <Button type=\"button\" color=\"secondary\" variant=\"ghost\" onClick={onClose} className=\"mr-3\">\n              Cancel\n            </Button>\n            {\n              isCollectionRunInProgress\n                ? (\n                    <Button type=\"submit\" onClick={handleViewRunner}>\n                      View Run\n                    </Button>\n                  )\n                : (\n                    <>\n                      <Button type=\"submit\" disabled={shouldDisableRecursiveFolderRun} onClick={() => onSubmit(true)} className=\"mr-3\">\n                        Recursive Run\n                      </Button>\n                      <Button type=\"submit\" disabled={shouldDisableFolderRun} onClick={() => onSubmit(false)}>\n                        Run\n                      </Button>\n                    </>\n                  )\n            }\n          </div>\n        </div>\n      </Modal>\n    </StyledWrapper>\n  );\n};\n\nexport default RunCollectionItem;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  position: relative;\n  .menu-icon {\n    color: ${(props) => props.theme.sidebar.dropdownIcon.color};\n    visibility: hidden;\n\n    .dropdown {\n      div[aria-expanded='true'] {\n        visibility: visible;\n      }\n      div[aria-expanded='false'] {\n        visibility: visible;\n      }\n    }\n  }\n\n  .indent-block {\n    border-right: 1px solid ${(props) => props.theme.sidebar.collection.item.indentBorder};\n  }\n\n  .collection-item-name {\n    height: 1.6rem;\n    cursor: pointer;\n    user-select: none;\n    position: relative;\n\n    /* Default: menu icon hidden, shown on hover/focus states (see consolidated rule below) */\n    .collection-item-menu-icon {\n      visibility: hidden;\n    }\n\n    /* Common styles for drop indicators */\n    &::before,\n    &::after {\n      content: '';\n      position: absolute;\n      left: 0;\n      right: 0;\n      height: 2px;\n      background: ${(props) => props.theme.dragAndDrop.border};\n      opacity: 0;\n      pointer-events: none;\n    }\n\n    &::before {\n      top: 0;\n    }\n\n    &::after {\n      bottom: 0;\n    }\n\n    /* Drop target styles */\n    &.drop-target {\n      background-color: ${(props) => props.theme.dragAndDrop.hoverBg};\n\n      &::before,\n      &::after {\n        opacity: 0;\n      }\n    }\n\n    &.drop-target-above {\n      &::before {\n        opacity: 1;\n        height: 2px;\n      }\n    }\n\n    &.drop-target-below {\n      &::after {\n        opacity: 1;\n        height: 2px;\n      }\n    }\n\n    /* Inside drop target style */\n    &.drop-target {\n      &::before {\n        top: 0;\n        bottom: 0;\n        height: 100%;\n        opacity: 1;\n        background: ${(props) => props.theme.dragAndDrop.hoverBg};\n        border: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border};\n        // border-radius: 4px;\n      }\n    }\n\n    .rotate-90 {\n      transform: rotateZ(90deg);\n    }\n\n    span.item-name {\n      white-space: nowrap;\n      text-overflow: ellipsis;\n      overflow: hidden;\n    }\n\n    /* Single source of truth for hover/focus states: background and menu icon visibility */\n    &:hover,\n    &.item-hovered,\n    &.item-keyboard-focused {\n      background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n      .menu-icon,\n      .collection-item-menu-icon {\n        visibility: visible;\n        background-color: transparent !important;\n      }\n    }\n\n    &.item-target {\n      background: #ccc3;\n    }\n\n    &.item-seperator {\n      .seperator {\n        bottom: 0px;\n        position: absolute;\n        height: 3px;\n        width: 100%;\n        background: #ccc3;\n      }\n    }\n\n    &.item-focused-in-tab {\n      background: ${(props) => props.theme.sidebar.collection.item.bg};\n\n      &:hover {\n        background: ${(props) => props.theme.sidebar.collection.item.bg} !important;\n      }\n\n      .indent-block {\n        border-right: 1px solid ${(props) => props.theme.sidebar.collection.item.indentBorder} !important;\n      }\n    }\n\n    &.item-keyboard-focused {\n      border-top: 1px solid ${(props) => props.theme.sidebar.collection.item.focusBorder};\n      border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.focusBorder};\n      outline: none;\n    }\n\n    div.tippy-box {\n      position: relative;\n      top: -0.625rem;\n    }\n\n    div.dropdown-item.delete-item {\n      color: ${(props) => props.theme.colors.text.danger};\n      &:hover {\n        background-color: ${(props) => props.theme.colors.bg.danger} !important;\n        color: white;\n      }\n    }\n  }\n\n  .empty-folder-message {\n    display: flex;\n    align-items: center;\n    height: 1.6rem;\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.sidebar.muted};\n\n    .add-request-link {\n      color: ${(props) => props.theme.textLink};\n      cursor: pointer;\n    }\n  }\n\n  &.is-sidebar-dragging .collection-item-name {\n    cursor: inherit;\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js",
    "content": "import React, { useState, useRef, useEffect } from 'react';\nimport { getEmptyImage } from 'react-dnd-html5-backend';\nimport range from 'lodash/range';\nimport filter from 'lodash/filter';\nimport classnames from 'classnames';\nimport { useDrag, useDrop } from 'react-dnd';\nimport {\n  IconChevronRight,\n  IconDots,\n  IconFilePlus,\n  IconFolderPlus,\n  IconPlayerPlay,\n  IconEdit,\n  IconCopy,\n  IconClipboard,\n  IconCode,\n  IconFolder,\n  IconTrash,\n  IconSettings,\n  IconInfoCircle,\n  IconTerminal2\n} from '@tabler/icons';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { addTab, focusTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';\nimport { handleCollectionItemDrop, sendRequest, showInFolder, pasteItem, saveRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport { toggleCollectionItem, addResponseExample } from 'providers/ReduxStore/slices/collections';\nimport { insertTaskIntoQueue } from 'providers/ReduxStore/slices/app';\nimport { uuid } from 'utils/common';\nimport { copyRequest } from 'providers/ReduxStore/slices/app';\nimport NewRequest from 'components/Sidebar/NewRequest';\nimport NewFolder from 'components/Sidebar/NewFolder';\nimport RenameCollectionItem from './RenameCollectionItem';\nimport CloneCollectionItem from './CloneCollectionItem';\nimport DeleteCollectionItem from './DeleteCollectionItem';\nimport RunCollectionItem from './RunCollectionItem';\nimport GenerateCodeItem from './GenerateCodeItem';\nimport { isItemARequest, isItemAFolder } from 'utils/tabs';\nimport { doesRequestMatchSearchText, doesFolderHaveItemsMatchSearchText } from 'utils/collections/search';\nimport { getDefaultRequestPaneTab } from 'utils/collections';\nimport toast from 'react-hot-toast';\nimport StyledWrapper from './StyledWrapper';\nimport { getKeyBindingsForActionAllOS } from 'providers/Hotkeys/keyMappings';\nimport NetworkError from 'components/ResponsePane/NetworkError/index';\nimport CollectionItemInfo from './CollectionItemInfo/index';\nimport CollectionItemIcon from './CollectionItemIcon';\nimport ExampleItem from './ExampleItem';\nimport ExampleIcon from 'components/Icons/ExampleIcon';\nimport { scrollToTheActiveTab } from 'utils/tabs';\nimport { isTabForItemActive as isTabForItemActiveSelector, isTabForItemPresent as isTabForItemPresentSelector } from 'src/selectors/tab';\nimport { isEqual } from 'lodash';\nimport { createEmptyStateMenuItems } from 'utils/collections/emptyStateRequest';\nimport { calculateDraggedItemNewPathname, getInitialExampleName, findParentItemInCollection } from 'utils/collections/index';\nimport { sortByNameThenSequence } from 'utils/common/index';\nimport { getRevealInFolderLabel } from 'utils/common/platform';\nimport CreateExampleModal from 'components/ResponseExample/CreateExampleModal';\nimport { openDevtoolsAndSwitchToTerminal } from 'utils/terminal';\nimport ActionIcon from 'ui/ActionIcon';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport { useSidebarAccordion } from 'components/Sidebar/SidebarAccordionContext';\n\nconst CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) => {\n  const { dropdownContainerRef } = useSidebarAccordion();\n  const _isTabForItemActiveSelector = isTabForItemActiveSelector({ itemUid: item.uid });\n  const isTabForItemActive = useSelector(_isTabForItemActiveSelector, isEqual);\n\n  const _isTabForItemPresentSelector = isTabForItemPresentSelector({ itemUid: item.uid });\n  const isTabForItemPresent = useSelector(_isTabForItemPresentSelector, isEqual);\n\n  const isSidebarDragging = useSelector((state) => state.app.isDragging);\n  const collection = useSelector((state) => state.collections.collections?.find((c) => c.uid === collectionUid));\n  const { hasCopiedItems } = useSelector((state) => state.app.clipboard);\n  const dispatch = useDispatch();\n\n  // We use a single ref for drag and drop.\n  const ref = useRef(null);\n  const menuDropdownRef = useRef(null);\n\n  const [renameItemModalOpen, setRenameItemModalOpen] = useState(false);\n  const [cloneItemModalOpen, setCloneItemModalOpen] = useState(false);\n  const [deleteItemModalOpen, setDeleteItemModalOpen] = useState(false);\n  const [createExampleModalOpen, setCreateExampleModalOpen] = useState(false);\n  const [generateCodeItemModalOpen, setGenerateCodeItemModalOpen] = useState(false);\n  const [newRequestModalOpen, setNewRequestModalOpen] = useState(false);\n  const [newFolderModalOpen, setNewFolderModalOpen] = useState(false);\n  const [runCollectionModalOpen, setRunCollectionModalOpen] = useState(false);\n  const [itemInfoModalOpen, setItemInfoModalOpen] = useState(false);\n  const [examplesExpanded, setExamplesExpanded] = useState(false);\n  const [isKeyboardFocused, setIsKeyboardFocused] = useState(false);\n  const hasSearchText = searchText && searchText?.trim()?.length;\n  const itemIsCollapsed = hasSearchText ? false : item.collapsed;\n  const isFolder = isItemAFolder(item);\n\n  // Check if request has examples (only for HTTP requests)\n  const hasExamples = isItemARequest(item) && item.type === 'http-request' && item.examples && item.examples.length > 0;\n\n  const [dropType, setDropType] = useState(null); // 'adjacent' or 'inside'\n\n  const [{ isDragging }, drag, dragPreview] = useDrag({\n    type: 'collection-item',\n    item: { ...item, sourceCollectionUid: collectionUid },\n    collect: (monitor) => ({\n      isDragging: monitor.isDragging()\n    }),\n    options: {\n      dropEffect: 'move'\n    }\n  });\n\n  useEffect(() => {\n    dragPreview(getEmptyImage(), { captureDraggingState: true });\n  }, []);\n\n  // Auto-scroll to show this item when its tab becomes active\n  useEffect(() => {\n    if (isTabForItemActive && ref.current) {\n      try {\n        ref.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });\n      } catch (err) {\n        // ignore scroll errors (some environments may not support smooth scrolling)\n      }\n    }\n  }, [isTabForItemActive]);\n\n  const determineDropType = (monitor) => {\n    const hoverBoundingRect = ref.current?.getBoundingClientRect();\n    const clientOffset = monitor.getClientOffset();\n    if (!hoverBoundingRect || !clientOffset) return null;\n\n    const clientY = clientOffset.y - hoverBoundingRect.top;\n    const folderUpperThreshold = hoverBoundingRect.height * 0.35;\n    const fileUpperThreshold = hoverBoundingRect.height * 0.5;\n\n    if (isItemAFolder(item)) {\n      return clientY < folderUpperThreshold ? 'adjacent' : 'inside';\n    } else {\n      return clientY < fileUpperThreshold ? 'adjacent' : null;\n    }\n  };\n\n  const canItemBeDropped = ({ draggedItem, targetItem, dropType }) => {\n    const { uid: targetItemUid, pathname: targetItemPathname } = targetItem;\n    const { uid: draggedItemUid, pathname: draggedItemPathname, sourceCollectionUid } = draggedItem;\n\n    if (draggedItemUid === targetItemUid) return false;\n\n    // For cross-collection moves, we allow the drop\n    if (sourceCollectionUid !== collectionUid) {\n      return true;\n    }\n\n    const newPathname = calculateDraggedItemNewPathname({ draggedItem, targetItem, dropType, collectionPathname });\n    if (!newPathname) return false;\n\n    if (targetItemPathname?.startsWith(draggedItemPathname)) return false;\n\n    return true;\n  };\n\n  const [{ isOver, canDrop }, drop] = useDrop({\n    accept: 'collection-item',\n    hover: (draggedItem, monitor) => {\n      const { uid: targetItemUid } = item;\n      const { uid: draggedItemUid } = draggedItem;\n\n      if (draggedItemUid === targetItemUid) return;\n\n      const dropType = determineDropType(monitor);\n\n      const _canItemBeDropped = canItemBeDropped({ draggedItem, targetItem: item, dropType });\n\n      setDropType(_canItemBeDropped ? dropType : null);\n    },\n    drop: async (draggedItem, monitor) => {\n      const { uid: targetItemUid } = item;\n      const { uid: draggedItemUid } = draggedItem;\n\n      if (draggedItemUid === targetItemUid) return;\n\n      const dropType = determineDropType(monitor);\n      if (!dropType) return;\n\n      await dispatch(handleCollectionItemDrop({ targetItem: item, draggedItem, dropType, collectionUid }));\n      setDropType(null);\n    },\n    canDrop: (draggedItem) => draggedItem.uid !== item.uid,\n    collect: (monitor) => ({\n      isOver: monitor.isOver()\n    })\n  });\n\n  const iconClassName = classnames({\n    'rotate-90': !itemIsCollapsed\n  });\n\n  const examplesIconClassName = classnames({\n    'rotate-90': examplesExpanded\n  });\n\n  const itemRowClassName = classnames('flex collection-item-name relative items-center', {\n    'item-focused-in-tab': isTabForItemActive,\n    'item-hovered': isOver && canDrop,\n    'drop-target': isOver && dropType === 'inside',\n    'drop-target-above': isOver && dropType === 'adjacent',\n    'item-keyboard-focused': isKeyboardFocused\n  });\n\n  const handleRun = async () => {\n    dispatch(sendRequest(item, collectionUid)).catch((err) =>\n      toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {\n        duration: 5000\n      })\n    );\n  };\n\n  const handleClick = (event) => {\n    if (event && event.detail != 1) return;\n    // scroll to the active tab\n    setTimeout(scrollToTheActiveTab, 50);\n    const isRequest = isItemARequest(item);\n    if (isRequest) {\n      if (isTabForItemPresent) {\n        dispatch(\n          focusTab({\n            uid: item.uid\n          })\n        );\n        return;\n      }\n      dispatch(\n        addTab({\n          uid: item.uid,\n          collectionUid: collectionUid,\n          requestPaneTab: getDefaultRequestPaneTab(item),\n          type: 'request'\n        })\n      );\n    } else {\n      dispatch(\n        addTab({\n          uid: item.uid,\n          collectionUid: collectionUid,\n          type: 'folder-settings'\n        })\n      );\n      if (item.collapsed) {\n        dispatch(\n          toggleCollectionItem({\n            itemUid: item.uid,\n            collectionUid: collectionUid\n          })\n        );\n      }\n    }\n  };\n\n  const handleFolderCollapse = (e) => {\n    e.stopPropagation();\n    e.preventDefault();\n    dispatch(\n      toggleCollectionItem({\n        itemUid: item.uid,\n        collectionUid: collectionUid\n      })\n    );\n  };\n\n  // prevent the parent's double-click handler from firing\n  const handleFolderDoubleClick = (e) => {\n    e.stopPropagation();\n    e.preventDefault();\n  };\n\n  const handleExamplesCollapse = (e) => {\n    e.stopPropagation();\n    e.preventDefault();\n    setExamplesExpanded(!examplesExpanded);\n  };\n\n  // prevent the parent's double-click handler from firing\n  const handleExamplesDoubleClick = (e) => {\n    e.stopPropagation();\n    e.preventDefault();\n  };\n\n  // Handle right-click context menu\n  const handleContextMenu = (e) => {\n    e.preventDefault();\n    e.stopPropagation();\n    menuDropdownRef.current?.show();\n  };\n\n  let indents = range(item.depth);\n\n  // Build menu items for MenuDropdown\n  const buildMenuItems = () => {\n    const items = [];\n\n    if (isFolder) {\n      items.push(\n        {\n          id: 'new-request',\n          leftSection: IconFilePlus,\n          label: 'New Request',\n          onClick: () => setNewRequestModalOpen(true)\n        },\n        {\n          id: 'new-folder',\n          leftSection: IconFolderPlus,\n          label: 'New Folder',\n          onClick: () => setNewFolderModalOpen(true)\n        },\n        {\n          id: 'run',\n          leftSection: IconPlayerPlay,\n          label: 'Run',\n          onClick: () => setRunCollectionModalOpen(true)\n        }\n      );\n    }\n\n    items.push(\n      {\n        id: 'clone',\n        leftSection: IconCopy,\n        label: 'Clone',\n        onClick: () => setCloneItemModalOpen(true)\n      },\n      {\n        id: 'copy',\n        leftSection: IconCopy,\n        label: 'Copy',\n        onClick: handleCopyItem\n      }\n    );\n\n    if (isFolder && hasCopiedItems) {\n      items.push({\n        id: 'paste',\n        leftSection: IconClipboard,\n        label: 'Paste',\n        onClick: handlePasteItem\n      });\n    }\n\n    items.push(\n      {\n        id: 'rename',\n        leftSection: IconEdit,\n        label: 'Rename',\n        onClick: () => setRenameItemModalOpen(true)\n      }\n    );\n    if (!isFolder && isItemARequest(item) && !(item.type === 'http-request' || item.type === 'graphql-request')) {\n      items.push({\n        id: 'run',\n        leftSection: IconPlayerPlay,\n        label: 'Run',\n        onClick: () => {\n          handleRun();\n        }\n      });\n    }\n\n    if (!isFolder && (item.type === 'http-request' || item.type === 'graphql-request')) {\n      items.push({\n        id: 'generate-code',\n        leftSection: IconCode,\n        label: 'Generate Code',\n        onClick: handleGenerateCode\n      });\n    }\n\n    if (!isFolder && isItemARequest(item) && item.type === 'http-request') {\n      items.push({\n        id: 'create-example',\n        leftSection: ExampleIcon,\n        label: 'Create Example',\n        onClick: () => setCreateExampleModalOpen(true)\n      });\n    }\n\n    items.push(\n      {\n        id: 'show-in-folder',\n        leftSection: IconFolder,\n        label: getRevealInFolderLabel(),\n        onClick: handleShowInFolder\n      }\n    );\n\n    items.push({ id: 'separator-1', type: 'divider' });\n\n    items.push({\n      id: 'info',\n      leftSection: IconInfoCircle,\n      label: 'Info',\n      onClick: () => setItemInfoModalOpen(true)\n    });\n\n    if (isFolder) {\n      items.push(\n        {\n          id: 'settings',\n          leftSection: IconSettings,\n          label: 'Settings',\n          onClick: viewFolderSettings\n        },\n        {\n          id: 'open-terminal',\n          leftSection: IconTerminal2,\n          label: 'Open in Terminal',\n          onClick: async () => {\n            const folderCwd = item.pathname || collectionPathname;\n            await openDevtoolsAndSwitchToTerminal(dispatch, folderCwd);\n          }\n        }\n      );\n    }\n\n    items.push({\n      id: 'delete',\n      leftSection: IconTrash,\n      label: 'Delete',\n      className: 'delete-item',\n      onClick: () => setDeleteItemModalOpen(true)\n    });\n\n    return items;\n  };\n\n  const className = classnames('flex flex-col w-full', {\n    'is-sidebar-dragging': isSidebarDragging\n  });\n\n  if (searchText && searchText.length) {\n    if (isItemARequest(item)) {\n      if (!doesRequestMatchSearchText(item, searchText)) {\n        return null;\n      }\n    } else {\n      if (!doesFolderHaveItemsMatchSearchText(item, searchText)) {\n        return null;\n      }\n    }\n  }\n\n  const handleDoubleClick = (event) => {\n    dispatch(makeTabPermanent({ uid: item.uid }));\n  };\n\n  // Sort items by their \"seq\" property.\n  const sortItemsBySequence = (items = []) => {\n    return items.sort((a, b) => a.seq - b.seq);\n  };\n\n  const handleShowInFolder = () => {\n    dispatch(showInFolder(item.pathname)).catch((error) => {\n      console.error('Error opening the folder', error);\n      toast.error('Error opening the folder');\n    });\n  };\n\n  const handleCreateExample = async (name, description = '') => {\n    // Create example with default values\n    const exampleData = {\n      name: name,\n      description: description,\n      status: 200,\n      statusText: 'OK',\n      headers: [],\n      body: {\n        type: 'text',\n        content: ''\n      }\n    };\n\n    // Calculate the index where the example will be saved\n    const existingExamples = item.draft?.examples || item.examples || [];\n    const exampleIndex = existingExamples.length;\n    const exampleUid = uuid();\n\n    dispatch(addResponseExample({\n      itemUid: item.uid,\n      collectionUid: collectionUid,\n      example: {\n        ...exampleData,\n        uid: exampleUid\n      }\n    }));\n\n    // Save the request\n    await dispatch(saveRequest(item.uid, collectionUid, true));\n\n    // Task middleware will track this and open the example in a new tab once the file is reloaded\n    dispatch(insertTaskIntoQueue({\n      uid: exampleUid,\n      type: 'OPEN_EXAMPLE',\n      collectionUid: collectionUid,\n      itemUid: item.uid,\n      exampleIndex: exampleIndex\n    }));\n\n    toast.success(`Example \"${name}\" created successfully`);\n    setCreateExampleModalOpen(false);\n  };\n\n  const folderItems = sortByNameThenSequence(filter(item.items, (i) => isItemAFolder(i) && !i.isTransient));\n  const requestItems = sortItemsBySequence(filter(item.items, (i) => isItemARequest(i) && !i.isTransient));\n  const showEmptyFolderMessage = isFolder && !hasSearchText && !folderItems?.length && !requestItems?.length;\n\n  const emptyFolderMenuItems = createEmptyStateMenuItems({ dispatch, collection, itemUid: item.uid });\n\n  const handleGenerateCode = () => {\n    if (\n      (item?.request?.url !== '')\n      || (item?.draft?.request?.url !== undefined && item?.draft?.request?.url !== '')\n    ) {\n      setGenerateCodeItemModalOpen(true);\n    } else {\n      toast.error('URL is required');\n    }\n  };\n\n  const viewFolderSettings = () => {\n    if (isItemAFolder(item)) {\n      if (isTabForItemPresent) {\n        dispatch(focusTab({ uid: item.uid }));\n        return;\n      }\n      dispatch(\n        addTab({\n          uid: item.uid,\n          collectionUid,\n          type: 'folder-settings'\n        })\n      );\n    }\n  };\n\n  const handleCopyItem = () => {\n    dispatch(copyRequest(item));\n    const itemType = isFolder ? 'Folder' : 'Request';\n    toast.success(`${itemType} copied`);\n  };\n\n  const handlePasteItem = () => {\n    // Determine target folder: if item is a folder, paste into it; otherwise paste into parent folder\n    let targetFolderUid = item.uid;\n    if (!isFolder) {\n      const parentFolder = findParentItemInCollection(collection, item.uid);\n      targetFolderUid = parentFolder ? parentFolder.uid : null;\n    }\n\n    dispatch(pasteItem(collectionUid, targetFolderUid))\n      .then(() => {\n        toast.success('Item pasted successfully');\n      })\n      .catch((err) => {\n        toast.error(err ? err.message : 'An error occurred while pasting the item');\n      });\n  };\n\n  // Keyboard shortcuts handler\n  const handleKeyDown = (e) => {\n    // Detect Mac by checking both metaKey and platform\n    const isMac = navigator.userAgent?.includes('Mac') || navigator.platform?.startsWith('Mac');\n    const isModifierPressed = isMac ? e.metaKey : e.ctrlKey;\n\n    const [macRenameKey, winRenameKey] = getKeyBindingsForActionAllOS('renameItem');\n    const renameKey = isMac ? macRenameKey : winRenameKey;\n\n    // Only trigger rename if no modifier keys are pressed (allow Cmd+Enter for run request)\n    const hasModifier = e.metaKey || e.ctrlKey || e.shiftKey || e.altKey;\n    if (e.key.toLowerCase() === renameKey && !hasModifier) {\n      e.preventDefault();\n      e.stopPropagation();\n      setRenameItemModalOpen(true);\n    } else if (isModifierPressed && e.key.toLowerCase() === 'c') {\n      e.preventDefault();\n      e.stopPropagation();\n      handleCopyItem();\n    } else if (isModifierPressed && e.key.toLowerCase() === 'v') {\n      e.preventDefault();\n      e.stopPropagation();\n      handlePasteItem();\n    }\n  };\n\n  const handleFocus = () => {\n    setIsKeyboardFocused(true);\n  };\n\n  const handleBlur = () => {\n    setIsKeyboardFocused(false);\n  };\n\n  return (\n    <StyledWrapper className={className}>\n      {renameItemModalOpen && (\n        <RenameCollectionItem item={item} collectionUid={collectionUid} onClose={() => setRenameItemModalOpen(false)} />\n      )}\n      {cloneItemModalOpen && (\n        <CloneCollectionItem item={item} collectionUid={collectionUid} onClose={() => setCloneItemModalOpen(false)} />\n      )}\n      {deleteItemModalOpen && (\n        <DeleteCollectionItem item={item} collectionUid={collectionUid} onClose={() => setDeleteItemModalOpen(false)} />\n      )}\n      {newRequestModalOpen && (\n        <NewRequest item={item} collectionUid={collectionUid} onClose={() => setNewRequestModalOpen(false)} />\n      )}\n      {newFolderModalOpen && (\n        <NewFolder item={item} collectionUid={collectionUid} onClose={() => setNewFolderModalOpen(false)} />\n      )}\n      {runCollectionModalOpen && (\n        <RunCollectionItem collectionUid={collectionUid} item={item} onClose={() => setRunCollectionModalOpen(false)} />\n      )}\n      {generateCodeItemModalOpen && (\n        <GenerateCodeItem collectionUid={collectionUid} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />\n      )}\n      {itemInfoModalOpen && (\n        <CollectionItemInfo item={item} onClose={() => setItemInfoModalOpen(false)} />\n      )}\n      <CreateExampleModal\n        isOpen={createExampleModalOpen}\n        onClose={() => setCreateExampleModalOpen(false)}\n        onSave={handleCreateExample}\n        title=\"Create Response Example\"\n        initialName={getInitialExampleName(item)}\n      />\n      <div\n        className={itemRowClassName}\n        ref={(node) => {\n          ref.current = node;\n          drag(drop(node));\n        }}\n        tabIndex={0}\n        onKeyDown={handleKeyDown}\n        onFocus={handleFocus}\n        onBlur={handleBlur}\n        onContextMenu={handleContextMenu}\n        data-testid=\"sidebar-collection-item-row\"\n      >\n        <div className=\"flex items-center h-full w-full\">\n          {indents && indents.length\n            ? indents.map((i) => (\n                <div\n                  onClick={handleClick}\n                  onDoubleClick={handleDoubleClick}\n                  className=\"indent-block\"\n                  key={i}\n                  style={{ width: 16, minWidth: 16, height: '100%' }}\n                >\n                  &nbsp;{/* Indent */}\n                </div>\n              ))\n            : null}\n          <div\n            className=\"flex flex-grow items-center h-full overflow-hidden\"\n            style={{ paddingLeft: 8 }}\n            onClick={handleClick}\n            onDoubleClick={handleDoubleClick}\n          >\n\n            {isFolder ? (\n              <ActionIcon style={{ width: 16, minWidth: 16 }}>\n                <IconChevronRight\n                  size={16}\n                  strokeWidth={2}\n                  className={iconClassName}\n                  style={{ color: 'rgb(160 160 160)' }}\n                  onClick={handleFolderCollapse}\n                  onDoubleClick={handleFolderDoubleClick}\n                  data-testid=\"folder-chevron\"\n                />\n              </ActionIcon>\n            ) : hasExamples ? (\n              <ActionIcon style={{ width: 16, minWidth: 16 }}>\n                <IconChevronRight\n                  size={16}\n                  strokeWidth={2}\n                  className={examplesIconClassName}\n                  style={{ color: 'rgb(160 160 160)' }}\n                  onClick={handleExamplesCollapse}\n                  onDoubleClick={handleExamplesDoubleClick}\n                  data-testid=\"request-item-chevron\"\n                />\n              </ActionIcon>\n            ) : null}\n\n            <div className=\"ml-1 flex w-full h-full items-center overflow-hidden\">\n              <CollectionItemIcon item={item} />\n              <span className=\"item-name\" title={item.name}>\n                {item.name}\n              </span>\n            </div>\n          </div>\n          <div className=\"pr-2\">\n            <MenuDropdown\n              ref={menuDropdownRef}\n              items={buildMenuItems()}\n              placement=\"bottom-start\"\n              data-testid=\"collection-item-menu\"\n              popperOptions={{ strategy: 'fixed' }}\n              appendTo={dropdownContainerRef?.current || document.body}\n            >\n              <ActionIcon className=\"menu-icon\">\n                <IconDots size={18} className=\"collection-item-menu-icon\" />\n              </ActionIcon>\n            </MenuDropdown>\n          </div>\n        </div>\n      </div>\n      {!itemIsCollapsed ? (\n        <div>\n          {folderItems && folderItems.length\n            ? folderItems.map((i) => {\n                return <CollectionItem key={i.uid} item={i} collectionUid={collectionUid} collectionPathname={collectionPathname} searchText={searchText} />;\n              })\n            : null}\n          {requestItems && requestItems.length\n            ? requestItems.map((i) => {\n                return <CollectionItem key={i.uid} item={i} collectionUid={collectionUid} collectionPathname={collectionPathname} searchText={searchText} />;\n              })\n            : null}\n          {showEmptyFolderMessage ? (\n            <div className=\"empty-folder-message\">\n              {range(item.depth + 1).map((i) => (\n                <div className=\"indent-block\" key={i} style={{ width: 16, minWidth: 16, height: '100%' }}>\n                  &nbsp;\n                </div>\n              ))}\n              <div style={{ paddingLeft: 8 }}>\n                <MenuDropdown\n                  items={emptyFolderMenuItems}\n                  placement=\"bottom-start\"\n                  appendTo={dropdownContainerRef?.current || document.body}\n                  popperOptions={{ strategy: 'fixed' }}\n                >\n                  <button className=\"ml-1 add-request-link\">+ Add request</button>\n                </MenuDropdown>\n              </div>\n            </div>\n          ) : null}\n        </div>\n      ) : null}\n\n      {/* Show examples when expanded (only for HTTP requests) */}\n      {isItemARequest(item) && item.type === 'http-request' && examplesExpanded && hasExamples && (\n        <div>\n          {(item.examples || []).map((example, index) => {\n            return (\n              <ExampleItem\n                key={example.uid || index}\n                example={example}\n                item={item}\n                index={index}\n                collection={collection}\n              />\n            );\n          })}\n        </div>\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default React.memo(CollectionItem);\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/DeleteCollection/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst StyledWrapper = styled.div`\n  .modal-description {\n    color: ${(props) => props.theme.text};\n    margin-bottom: 12px;\n\n    strong {\n      font-weight: 600;\n    }\n  }\n\n  .collection-info-card {\n    background-color: ${(props) => props.theme.modal.title.bg};\n    border-radius: 4px;\n    padding: 12px;\n    margin-bottom: 12px;\n  }\n\n  .collection-name {\n    font-weight: 500;\n    color: ${(props) => props.theme.text};\n    margin-bottom: 4px;\n  }\n\n  .collection-path {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.muted};\n    word-break: break-all;\n  }\n\n  .warning-text {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.danger};\n    margin-bottom: 16px;\n  }\n\n  .delete-confirmation {\n    padding-top: 16px;\n    border-top: 1px solid ${(props) => props.theme.border.border0};\n\n    label {\n      display: block;\n      font-size: ${(props) => props.theme.font.size.sm};\n      color: ${(props) => props.theme.text};\n      margin-bottom: 8px;\n    }\n\n    .delete-keyword {\n      font-weight: 600;\n      color: ${(props) => props.theme.colors.text.danger};\n      font-family: monospace;\n      background-color: ${(props) => rgba(props.theme.colors.text.danger, 0.1)};\n      padding: 2px 6px;\n      border-radius: 4px;\n    }\n\n    input {\n      width: 100%;\n      padding: 8px 12px;\n      font-size: ${(props) => props.theme.font.size.sm};\n      border: 1px solid ${(props) => props.theme.input.border};\n      border-radius: 6px;\n      background-color: ${(props) => props.theme.input.bg};\n      color: ${(props) => props.theme.text};\n      outline: none;\n      transition: border-color 0.15s ease;\n\n      &::placeholder {\n        color: ${(props) => props.theme.colors.text.muted};\n        opacity: 0.6;\n      }\n\n      &:focus {\n        border-color: ${(props) => props.theme.colors.text.danger};\n        box-shadow: 0 0 0 2px ${(props) => rgba(props.theme.colors.text.danger, 0.15)};\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/DeleteCollection/index.js",
    "content": "import React, { useState } from 'react';\nimport toast from 'react-hot-toast';\nimport Modal from 'components/Modal';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { IconAlertTriangle } from '@tabler/icons';\nimport { removeCollectionFromWorkspaceAction } from 'providers/ReduxStore/slices/workspaces/actions';\nimport { findCollectionByUid } from 'utils/collections/index';\nimport StyledWrapper from './StyledWrapper';\n\nconst DeleteCollection = ({ onClose, collectionUid, workspaceUid }) => {\n  const dispatch = useDispatch();\n  const [confirmText, setConfirmText] = useState('');\n  const collection = useSelector((state) => findCollectionByUid(state.collections.collections, collectionUid));\n  const workspace = useSelector((state) => state.workspaces.workspaces.find((w) => w.uid === workspaceUid));\n\n  const isConfirmed = confirmText.toLowerCase() === 'delete';\n\n  const onConfirm = async () => {\n    if (!collection || !workspace) {\n      toast.error('Collection or workspace not found');\n      onClose();\n      return;\n    }\n\n    try {\n      await dispatch(removeCollectionFromWorkspaceAction(workspace.uid, collection.pathname, { deleteFiles: true }));\n      toast.success(`Deleted \"${collection.name}\" collection`);\n      onClose();\n    } catch (error) {\n      console.error('Error deleting collection:', error);\n      toast.error(error.message || 'An error occurred while deleting the collection');\n    }\n  };\n\n  if (!collection) {\n    return null;\n  }\n\n  const customHeader = (\n    <div className=\"flex items-center gap-2\">\n      <IconAlertTriangle size={18} strokeWidth={1.5} className=\"text-red-500\" />\n      <span>Delete Collection</span>\n    </div>\n  );\n\n  return (\n    <StyledWrapper>\n      <Modal\n        size=\"sm\"\n        title=\"Delete Collection\"\n        customHeader={customHeader}\n        confirmText=\"Delete\"\n        cancelText=\"Cancel\"\n        confirmButtonColor=\"danger\"\n        confirmDisabled={!isConfirmed}\n        handleConfirm={onConfirm}\n        handleCancel={onClose}\n      >\n        <p className=\"modal-description\">\n          Are you sure you want to permanently delete <strong>\"{collection.name}\"</strong>?\n        </p>\n        <div className=\"collection-info-card\">\n          <div className=\"collection-name\">{collection.name}</div>\n          <div className=\"collection-path\">{collection.pathname}</div>\n        </div>\n        <p className=\"warning-text\">\n          This action cannot be undone. The collection files will be permanently deleted from disk.\n        </p>\n        <div className=\"delete-confirmation\">\n          <label htmlFor=\"delete-confirm-input\">\n            Type <span className=\"delete-keyword\">delete</span> to confirm\n          </label>\n          <input\n            id=\"delete-confirm-input\"\n            type=\"text\"\n            value={confirmText}\n            onChange={(e) => setConfirmText(e.target.value)}\n            placeholder=\"delete\"\n            autoComplete=\"off\"\n            autoFocus\n          />\n        </div>\n      </Modal>\n    </StyledWrapper>\n  );\n};\n\nexport default DeleteCollection;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/GenerateDocumentation/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .content {\n    .title {\n      font-size: ${(props) => props.theme.font.size.base};\n      color: ${(props) => props.theme.text};\n    }\n\n    .description {\n      font-size: ${(props) => props.theme.font.size.sm};\n      color: ${(props) => props.theme.colors.text.muted};\n      line-height: 1.6;\n    }\n\n    .preview-container {\n      border-radius: ${(props) => props.theme.border.radius.md};\n      overflow: hidden;\n      border: 1px solid ${(props) => props.theme.border.border1};\n\n      .preview-label {\n        top: 0.5rem;\n        right: 0.5rem;\n        padding: 0.125rem 0.5rem;\n        font-size: ${(props) => props.theme.font.size.xs};\n        font-weight: 500;\n        color: #3b82f6;\n        background-color: rgba(59, 130, 246, 0.1);\n        border: 1px dashed rgba(59, 130, 246, 0.4);\n        border-radius: ${(props) => props.theme.border.radius.sm};\n      }\n\n      .preview-image {\n        width: 100%;\n        height: auto;\n        display: block;\n      }\n    }\n\n    .features {\n      li {\n        font-size: ${(props) => props.theme.font.size.sm};\n        color: ${(props) => props.theme.text};\n      }\n\n      .check-icon {\n        color: ${(props) => props.theme.colors.text.green};\n      }\n    }\n\n    .note {\n      font-size: ${(props) => props.theme.font.size.xs};\n      color: ${(props) => props.theme.colors.text.muted};\n      line-height: 1.5;\n    }\n  }\n\n  .text-warning {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.warning};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/GenerateDocumentation/index.js",
    "content": "import React, { useCallback, useMemo } from 'react';\nimport { useSelector } from 'react-redux';\nimport { cloneDeep } from 'lodash';\nimport * as FileSaver from 'file-saver';\nimport jsyaml from 'js-yaml';\nimport jsesc from 'jsesc';\nimport toast from 'react-hot-toast';\nimport { IconBook, IconCheck, IconAlertTriangle, IconLoader2 } from '@tabler/icons';\n\nimport Modal from 'components/Modal';\nimport StyledWrapper from './StyledWrapper';\nimport demoImage from './demo.png';\nimport { useApp } from 'providers/App';\nimport { transformCollectionToSaveToExportAsFile, findCollectionByUid, areItemsLoading } from 'utils/collections/index';\nimport { brunoToOpenCollection } from '@usebruno/converters';\nimport { sanitizeName } from 'utils/common/regex';\nimport { escapeHtml } from 'utils/response';\n\nconst CDN_BASE_URL = 'https://cdn.opencollection.com';\n\nconst FEATURES = [\n  'Standalone HTML file - no server required',\n  'Interactive API playground',\n  'Host on any static file server'\n];\n\nconst buildHtmlDocument = (collectionName, escapedYamlContent) => `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>${collectionName} - API Documentation</title>\n    <style>\n        body { margin: 0; padding: 0; }\n        #opencollection-container { width: 100vw; height: 100vh; }\n    </style>\n    <link rel=\"stylesheet\" href=\"${CDN_BASE_URL}/docs.css\">\n    <script src=\"${CDN_BASE_URL}/docs.js\"></script>\n</head>\n<body>\n    <div id=\"opencollection-container\"></div>\n    <script>\n        const collectionData = ${escapedYamlContent};\n        new window.OpenCollection({\n            target: document.getElementById('opencollection-container'),\n            opencollection: collectionData,\n            theme: 'light'\n        });\n    </script>\n</body>\n</html>`;\n\nconst CollectionNotFound = ({ onClose }) => (\n  <Modal size=\"md\" title=\"Generate Documentation\" confirmText=\"Close\" handleConfirm={onClose} hideCancel>\n    <StyledWrapper className=\"w-[500px]\">\n      <div className=\"flex items-center gap-2 text-warning\">\n        <IconAlertTriangle size={16} className=\"shrink-0\" />\n        <span>Collection not found. It may have been deleted or is no longer available.</span>\n      </div>\n    </StyledWrapper>\n  </Modal>\n);\n\nconst GenerateDocumentation = ({ onClose, collectionUid }) => {\n  const { version } = useApp();\n  const collection = useSelector((state) =>\n    findCollectionByUid(state.collections.collections, collectionUid)\n  );\n\n  const isLoading = useMemo(\n    () => (collection ? areItemsLoading(collection) : false),\n    [collection]\n  );\n\n  const handleGenerate = useCallback(() => {\n    try {\n      const collectionCopy = cloneDeep(collection);\n      const transformedCollection = transformCollectionToSaveToExportAsFile(collectionCopy);\n      const openCollection = brunoToOpenCollection(transformedCollection);\n\n      openCollection.extensions = {\n        ...openCollection.extensions,\n        bruno: {\n          ...openCollection.extensions?.bruno,\n          exportedAt: new Date().toISOString(),\n          exportedUsing: version ? `Bruno/${version}` : 'Bruno'\n        }\n      };\n\n      const yamlContent = jsyaml.dump(openCollection, {\n        indent: 2,\n        lineWidth: -1,\n        noRefs: true,\n        sortKeys: false\n      });\n\n      // jsesc handles all edge cases: Unicode, special chars, quotes, template literals, etc.\n      let escapedYaml = jsesc(yamlContent, { quotes: 'double', wrap: true });\n\n      // Escape closing tags to prevent HTML parser from breaking out of the script block\n      escapedYaml = escapedYaml.replace(/<\\//g, '<\\\\/');\n\n      const htmlContent = buildHtmlDocument(\n        escapeHtml(collection.name),\n        escapedYaml\n      );\n\n      const fileName = `${sanitizeName(collection.name)}-documentation.html`;\n      FileSaver.saveAs(new Blob([htmlContent], { type: 'text/html' }), fileName);\n\n      toast.success('Documentation generated successfully');\n      onClose();\n    } catch (error) {\n      console.error('Error generating documentation:', error);\n      toast.error('Failed to generate documentation');\n    }\n  }, [collection, version, onClose]);\n\n  if (!collection) {\n    return <CollectionNotFound onClose={onClose} />;\n  }\n\n  return (\n    <Modal\n      size=\"md\"\n      title=\"Generate Documentation\"\n      confirmText={isLoading ? 'Loading...' : 'Generate'}\n      cancelText=\"Cancel\"\n      handleConfirm={isLoading ? undefined : handleGenerate}\n      handleCancel={onClose}\n      confirmDisabled={isLoading}\n    >\n      <StyledWrapper className=\"w-[500px]\">\n        {isLoading ? (\n          <div className=\"flex items-center justify-center gap-3 py-8\">\n            <IconLoader2 size={20} className=\"animate-spin\" />\n            <span>Loading collection...</span>\n          </div>\n        ) : (\n          <div className=\"content\">\n            <h3 className=\"title flex items-center gap-2 mt-2 font-medium\">\n              <IconBook size={18} />\n              <span>Interactive API Documentation</span>\n            </h3>\n            <p className=\"description mb-4\">\n              Generate a standalone HTML file that can be hosted anywhere or shared with your team.\n            </p>\n\n            <div className=\"preview-container relative mb-4\">\n              <span className=\"preview-label absolute\">Sample Output</span>\n              <img src={demoImage} alt=\"Documentation preview\" className=\"preview-image\" />\n            </div>\n\n            <ul className=\"features flex flex-col list-none gap-2 p-0 mb-4\">\n              {FEATURES.map((feature) => (\n                <li key={feature} className=\"flex items-center gap-2.5\">\n                  <IconCheck size={16} className=\"check-icon flex-shrink-0\" />\n                  <span>{feature}</span>\n                </li>\n              ))}\n            </ul>\n\n            <p className=\"note m-0\">\n              The generated file loads OpenCollection's JavaScript and CSS files from a CDN, which requires an internet connection.\n            </p>\n          </div>\n        )}\n      </StyledWrapper>\n    </Modal>\n  );\n};\n\nexport default GenerateDocumentation;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/RemoveCollection/ConfirmCollectionCloseDrafts.js",
    "content": "import React, { useMemo } from 'react';\nimport filter from 'lodash/filter';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { flattenItems, isItemARequest, hasRequestChanges, findCollectionByUid } from 'utils/collections';\nimport { pluralizeWord } from 'utils/common';\nimport { saveRequest, saveMultipleRequests } from 'providers/ReduxStore/slices/collections/actions';\nimport { deleteRequestDraft } from 'providers/ReduxStore/slices/collections';\nimport { removeCollection } from 'providers/ReduxStore/slices/collections/actions';\nimport { IconAlertTriangle, IconDeviceFloppy } from '@tabler/icons';\nimport Modal from 'components/Modal';\nimport toast from 'react-hot-toast';\nimport Button from 'ui/Button';\n\nconst MAX_UNSAVED_REQUESTS_TO_SHOW = 5;\n\nconst ConfirmCollectionCloseDrafts = ({ onClose, collection, collectionUid }) => {\n  const dispatch = useDispatch();\n\n  const latestCollection = useSelector((state) => findCollectionByUid(state.collections.collections, collectionUid));\n\n  const activeCollection = latestCollection || collection;\n\n  const currentDrafts = useMemo(() => {\n    if (!activeCollection) return [];\n    const items = flattenItems(activeCollection.items);\n    return items\n      ?.filter((item) => isItemARequest(item) && hasRequestChanges(item) && !item.isTransient)\n      .map((item) => {\n        return {\n          ...item,\n          collectionUid: collectionUid\n        };\n      });\n  }, [activeCollection, collectionUid]);\n\n  const currentTransientDrafts = useMemo(() => {\n    if (!activeCollection) return [];\n    const items = flattenItems(activeCollection.items);\n    return items\n      ?.filter((item) => isItemARequest(item) && hasRequestChanges(item) && item.isTransient)\n      .map((item) => {\n        return {\n          ...item,\n          collectionUid: collectionUid\n        };\n      });\n  }, [activeCollection, collectionUid]);\n\n  const allDrafts = useMemo(() => {\n    return [...currentDrafts, ...currentTransientDrafts];\n  }, [currentDrafts, currentTransientDrafts]);\n\n  const handleSaveAll = () => {\n    // If there are transient drafts, we can't proceed with batch save\n    if (currentTransientDrafts.length > 0) {\n      toast.error('Please save or discard transient requests first');\n      return;\n    }\n    // Save only non-transient drafts\n    if (currentDrafts.length > 0) {\n      dispatch(saveMultipleRequests(currentDrafts))\n        .then(() => {\n          dispatch(removeCollection(collectionUid))\n            .then(() => {\n              toast.success('Collection removed from workspace');\n              onClose();\n            })\n            .catch(() => toast.error('An error occurred while removing the collection'));\n        })\n        .catch(() => {\n          toast.error('Failed to save requests!');\n        });\n    } else {\n      // No non-transient drafts, just remove the collection\n      dispatch(removeCollection(collectionUid))\n        .then(() => {\n          toast.success('Collection removed from workspace');\n          onClose();\n        })\n        .catch(() => toast.error('An error occurred while removing the collection'));\n    }\n  };\n\n  const handleDiscardAll = () => {\n    // Discard all drafts (both regular and transient)\n    allDrafts.forEach((draft) => {\n      dispatch(deleteRequestDraft({\n        collectionUid: collectionUid,\n        itemUid: draft.uid\n      }));\n    });\n\n    // Then remove the collection\n    dispatch(removeCollection(collectionUid))\n      .then(() => {\n        toast.success('Collection removed from workspace');\n        onClose();\n      })\n      .catch(() => toast.error('An error occurred while removing the collection'));\n  };\n\n  const handleSaveTransient = (draft) => {\n    dispatch(saveRequest(draft.uid, collectionUid));\n  };\n\n  if (!currentDrafts.length && !currentTransientDrafts.length) {\n    return null;\n  }\n\n  return (\n    <Modal\n      size=\"md\"\n      title=\"Remove Collection\"\n      confirmText=\"Save and Remove\"\n      cancelText=\"Remove without saving\"\n      handleCancel={onClose}\n      disableEscapeKey={true}\n      disableCloseOnOutsideClick={true}\n      closeModalFadeTimeout={150}\n      hideFooter={true}\n    >\n      <div className=\"flex items-center\">\n        <IconAlertTriangle size={32} strokeWidth={1.5} className=\"text-yellow-600\" />\n        <h1 className=\"ml-2 text-lg font-medium\">Hold on..</h1>\n      </div>\n      <p className=\"mt-4\">\n        You have unsaved changes in <span className=\"font-medium\">{allDrafts.length}</span>{' '}\n        {pluralizeWord('request', allDrafts.length)}.\n      </p>\n\n      {/* Regular (saved) requests with changes */}\n      {currentDrafts.length > 0 && (\n        <div className=\"mt-4\">\n          <p className=\"text-sm font-medium mb-2\">\n            Saved {pluralizeWord('Request', currentDrafts.length)} ({currentDrafts.length})\n          </p>\n          <ul className=\"ml-2\">\n            {currentDrafts.slice(0, MAX_UNSAVED_REQUESTS_TO_SHOW).map((item) => {\n              return (\n                <li key={item.uid} className=\"mt-1 text-xs text-gray-600\">\n                  • {item.filename || item.name}\n                </li>\n              );\n            })}\n          </ul>\n          {currentDrafts.length > MAX_UNSAVED_REQUESTS_TO_SHOW && (\n            <p className=\"ml-2 mt-1 text-xs text-gray-500\">\n              ...{currentDrafts.length - MAX_UNSAVED_REQUESTS_TO_SHOW} additional{' '}\n              {pluralizeWord('request', currentDrafts.length - MAX_UNSAVED_REQUESTS_TO_SHOW)} not shown\n            </p>\n          )}\n        </div>\n      )}\n\n      {/* Transient (unsaved) requests */}\n      {currentTransientDrafts.length > 0 && (\n        <div className=\"mt-4\">\n          <p className=\"text-sm font-medium mb-2\">\n            Transient {pluralizeWord('Request', currentTransientDrafts.length)} ({currentTransientDrafts.length})\n          </p>\n          <p className=\"text-xs text-orange-600 mb-3\">\n            These requests need to be saved individually before closing the collection.\n          </p>\n          <div className=\"space-y-2 max-h-64 overflow-y-auto pr-1\">\n            {currentTransientDrafts.map((item) => {\n              return (\n                <div\n                  key={item.uid}\n                  className=\"flex items-center justify-between py-2 px-3 bg-gray-50 rounded border border-gray-200\"\n                >\n                  <span className=\"text-sm text-gray-700 truncate mr-3\">{item.name}</span>\n                  <Button\n                    color=\"primary\"\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    onClick={() => handleSaveTransient(item)}\n                    icon={<IconDeviceFloppy size={14} strokeWidth={1.5} />}\n                  >\n                    Save\n                  </Button>\n                </div>\n              );\n            })}\n          </div>\n        </div>\n      )}\n\n      <div className=\"flex justify-between mt-6\">\n        <div>\n          <Button color=\"danger\" onClick={handleDiscardAll}>\n            Discard All and Remove\n          </Button>\n        </div>\n        <div>\n          <Button className=\"mr-2\" color=\"secondary\" variant=\"ghost\" onClick={onClose}>\n            Cancel\n          </Button>\n          <Button\n            onClick={handleSaveAll}\n            disabled={currentTransientDrafts.length > 0}\n            title={currentTransientDrafts.length > 0 ? 'Please save or discard transient requests first' : ''}\n          >\n            {currentDrafts.length > 1 ? 'Save All and Remove' : 'Save and Remove'}\n          </Button>\n        </div>\n      </div>\n    </Modal>\n  );\n};\n\nexport default ConfirmCollectionCloseDrafts;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/RemoveCollection/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .collection-info-card {\n    background-color: ${(props) => props.theme.modal.title.bg};\n    border-radius: 4px;\n    padding: 12px;\n  }\n  .collection-name {\n    font-weight: 500;\n    padding-left: 0 !important;\n    color: ${(props) => props.theme.text};\n    margin-bottom: 4px;\n    cursor: default !important;\n    &:hover {\n      background: none !important;\n    }\n  }\n  .collection-path {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.muted};\n    word-break: break-all;\n  }\n  .warning-icon {\n    color: ${(props) => props.theme.status.warning.text};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/RemoveCollection/index.js",
    "content": "import React, { useMemo } from 'react';\nimport toast from 'react-hot-toast';\nimport Modal from 'components/Modal';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { IconAlertCircle } from '@tabler/icons';\nimport { removeCollection } from 'providers/ReduxStore/slices/collections/actions';\nimport { findCollectionByUid, flattenItems, isItemARequest, hasRequestChanges } from 'utils/collections/index';\nimport filter from 'lodash/filter';\nimport ConfirmCollectionCloseDrafts from './ConfirmCollectionCloseDrafts';\nimport StyledWrapper from './StyledWrapper';\n\nconst RemoveCollection = ({ onClose, collectionUid }) => {\n  const dispatch = useDispatch();\n  const collection = useSelector((state) => findCollectionByUid(state.collections.collections, collectionUid));\n\n  // Detect drafts in the collection\n  const drafts = useMemo(() => {\n    if (!collection) return [];\n    const items = flattenItems(collection.items);\n    return filter(items, (item) => isItemARequest(item) && hasRequestChanges(item));\n  }, [collection]);\n\n  const onConfirm = () => {\n    if (!collection) {\n      toast.error('Collection not found');\n      onClose();\n      return;\n    }\n    dispatch(removeCollection(collection.uid))\n      .then(() => {\n        toast.success('Collection removed from workspace');\n        onClose();\n      })\n      .catch(() => toast.error('An error occurred while removing the collection'));\n  };\n\n  if (!collection) {\n    return <div>Collection not found</div>;\n  }\n\n  // If there are drafts, show the draft confirmation modal\n  if (drafts.length > 0) {\n    return <ConfirmCollectionCloseDrafts onClose={onClose} collection={collection} collectionUid={collectionUid} />;\n  }\n\n  const customHeader = (\n    <div className=\"flex items-center gap-2\" data-testid=\"close-collection-modal-title\">\n      <IconAlertCircle size={18} strokeWidth={1.5} className=\"warning-icon\" />\n      <span>Remove Collection</span>\n    </div>\n  );\n\n  // Otherwise, show the standard remove confirmation modal\n  return (\n    <StyledWrapper>\n      <Modal\n        size=\"sm\"\n        title=\"Remove Collection\"\n        customHeader={customHeader}\n        confirmText=\"Remove\"\n        confirmButtonColor=\"warning\"\n        handleConfirm={onConfirm}\n        handleCancel={onClose}\n      >\n        <p className=\"mb-4\">Are you sure you want to close following collection in Bruno?</p>\n        <div className=\"collection-info-card\">\n          <div className=\"collection-name\">{collection.name}</div>\n          <div className=\"collection-path\">{collection.pathname}</div>\n        </div>\n        <p className=\"mt-4 text-muted text-sm\">\n          It will still be available in the filesystem at the above location and can be re-opened later.\n        </p>\n      </Modal>\n    </StyledWrapper>\n  );\n};\n\nexport default RemoveCollection;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/RenameCollection/index.js",
    "content": "import React, { useRef, useEffect } from 'react';\nimport { useFormik } from 'formik';\nimport * as Yup from 'yup';\nimport Modal from 'components/Modal';\nimport { useDispatch, useSelector } from 'react-redux';\nimport toast from 'react-hot-toast';\nimport { renameCollection } from 'providers/ReduxStore/slices/collections/actions';\nimport { findCollectionByUid } from 'utils/collections/index';\n\nconst RenameCollection = ({ collectionUid, onClose }) => {\n  const dispatch = useDispatch();\n  const inputRef = useRef();\n  const collection = useSelector((state) => findCollectionByUid(state.collections.collections, collectionUid));\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: {\n      name: collection.name\n    },\n    validationSchema: Yup.object({\n      name: Yup.string()\n        .min(1, 'must be at least 1 character')\n        .required('name is required')\n    }),\n    onSubmit: (values) => {\n      dispatch(renameCollection(values.name, collection.uid))\n        .then(() => {\n          toast.success('Collection renamed!');\n          onClose();\n        })\n        .catch((err) => {\n          toast.error(err ? err.message : 'An error occurred while renaming the collection');\n        });\n    }\n  });\n\n  useEffect(() => {\n    if (inputRef && inputRef.current) {\n      inputRef.current.focus();\n    }\n  }, [inputRef]);\n\n  const onSubmit = () => formik.handleSubmit();\n\n  return (\n    <Modal size=\"md\" title=\"Rename Collection\" confirmText=\"Rename\" handleConfirm={onSubmit} handleCancel={onClose}>\n      <form className=\"bruno-form\" onSubmit={(e) => e.preventDefault()}>\n        <div>\n          <label htmlFor=\"name\" className=\"block font-medium\">\n            Name\n          </label>\n          <input\n            id=\"collection-name\"\n            type=\"text\"\n            name=\"name\"\n            ref={inputRef}\n            className=\"block textbox mt-2 w-full\"\n            autoComplete=\"off\"\n            autoCorrect=\"off\"\n            autoCapitalize=\"off\"\n            spellCheck=\"false\"\n            onChange={formik.handleChange}\n            value={formik.values.name || ''}\n          />\n          {formik.touched.name && formik.errors.name ? <div className=\"text-red-500\">{formik.errors.name}</div> : null}\n        </div>\n      </form>\n    </Modal>\n  );\n};\n\nexport default RenameCollection;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  .collection-name {\n    height: 1.6rem;\n    cursor: pointer;\n    user-select: none;\n    padding-left: 4px;\n    border: ${(props) => props.theme.dragAndDrop.borderStyle} transparent;\n\n    .rotate-90 {\n      transform: rotateZ(90deg);\n    }\n    .collection-actions {\n      visibility: hidden;\n    }\n\n    /* Single source of truth for hover/focus states: background and menu icon visibility */\n    &:hover,\n    &:focus-within,\n    &.collection-keyboard-focused {\n      background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n      .collection-actions {\n        visibility: visible;\n        background-color: transparent !important;\n      }\n    }\n\n    &.item-hovered {\n      border-top: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border};\n      border-bottom: 2px solid transparent;\n    }\n\n    &:hover {\n      background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n    }\n\n    div.tippy-box {\n      position: relative;\n      top: -0.625rem;\n      font-weight: 400;\n    }\n\n    div.dropdown-item.delete-collection {\n      color: ${(props) => props.theme.colors.text.danger};\n      &:hover {\n        background-color: ${(props) => props.theme.colors.bg.danger};\n        color: white;\n      }\n    }\n\n    &.drop-target {\n      border: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border};\n      background-color: ${(props) => props.theme.dragAndDrop.hoverBg};\n      transition: ${(props) => props.theme.dragAndDrop.transition};\n    }\n\n    &.drop-target-above {\n      border: none;\n      border-top: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border};\n      margin-top: -2px;\n      background: transparent;\n      transition: ${(props) => props.theme.dragAndDrop.transition};\n    }\n\n    &.drop-target-below {\n      border: none;\n      border-bottom: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border};\n      margin-bottom: -2px;\n      background: transparent;\n      transition: ${(props) => props.theme.dragAndDrop.transition};\n    }\n\n    &.collection-focused-in-tab {\n      background: ${(props) => props.theme.sidebar.collection.item.bg};\n\n      &:hover {\n        background: ${(props) => props.theme.sidebar.collection.item.bg} !important;\n      }\n    }\n\n    &.collection-keyboard-focused {\n      border-top: 1px solid ${(props) => props.theme.sidebar.collection.item.focusBorder};\n      border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.focusBorder};\n      outline: none;\n\n       &:hover {\n        background: ${(props) => props.theme.sidebar.collection.item.keyboardFocusBg} !important;\n      }\n    }\n  }\n\n  #sidebar-collection-name {\n    white-space: nowrap;\n    text-overflow: ellipsis;\n    overflow: hidden;\n  }\n\n  .indent-block {\n    border-right: 1px solid ${(props) => props.theme.sidebar.collection.item.indentBorder};\n  }\n\n  .empty-collection-message {\n    display: flex;\n    align-items: center;\n    height: 1.6rem;\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.sidebar.muted};\n\n    .add-request-link {\n      color: ${(props) => props.theme.textLink};\n      cursor: pointer;\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js",
    "content": "import React, { useState, useRef, useEffect } from 'react';\nimport { getEmptyImage } from 'react-dnd-html5-backend';\nimport classnames from 'classnames';\nimport { uuid } from 'utils/common';\nimport filter from 'lodash/filter';\nimport { useDrop, useDrag } from 'react-dnd';\nimport {\n  IconChevronRight,\n  IconDots,\n  IconLoader2,\n  IconFilePlus,\n  IconFolderPlus,\n  IconCopy,\n  IconClipboard,\n  IconPlayerPlay,\n  IconEdit,\n  IconShare,\n  IconFoldDown,\n  IconX,\n  IconSettings,\n  IconTerminal2,\n  IconFolder,\n  IconBook\n} from '@tabler/icons';\nimport OpenAPISyncIcon from 'components/Icons/OpenAPISync';\nimport { toggleCollection, collapseFullCollection } from 'providers/ReduxStore/slices/collections';\nimport { mountCollection, moveCollectionAndPersist, handleCollectionItemDrop, pasteItem, showInFolder, saveCollectionSecurityConfig } from 'providers/ReduxStore/slices/collections/actions';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';\nimport toast from 'react-hot-toast';\nimport NewRequest from 'components/Sidebar/NewRequest';\nimport NewFolder from 'components/Sidebar/NewFolder';\nimport CollectionItem from './CollectionItem';\nimport RemoveCollection from './RemoveCollection';\nimport { doesCollectionHaveItemsMatchingSearchText } from 'utils/collections/search';\nimport { isItemAFolder, isItemARequest, areItemsLoading } from 'utils/collections';\nimport { isTabForItemActive } from 'src/selectors/tab';\n\nimport RenameCollection from './RenameCollection';\nimport StyledWrapper from './StyledWrapper';\nimport CloneCollection from './CloneCollection';\nimport { scrollToTheActiveTab } from 'utils/tabs';\nimport ShareCollection from 'components/ShareCollection/index';\nimport GenerateDocumentation from './GenerateDocumentation';\nimport { CollectionItemDragPreview } from './CollectionItem/CollectionItemDragPreview/index';\nimport { sortByNameThenSequence } from 'utils/common/index';\nimport { getRevealInFolderLabel } from 'utils/common/platform';\nimport { openDevtoolsAndSwitchToTerminal } from 'utils/terminal';\nimport ActionIcon from 'ui/ActionIcon';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport StatusBadge from 'ui/StatusBadge';\nimport { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';\nimport { useSidebarAccordion } from 'components/Sidebar/SidebarAccordionContext';\nimport { createEmptyStateMenuItems } from 'utils/collections/emptyStateRequest';\n\n// Delay before showing empty collection state (ms)\n// This prevents flicker from race condition between loading state and item batch updates\nconst EMPTY_STATE_DELAY_MS = 300;\n\nconst Collection = ({ collection, searchText }) => {\n  const isOpenAPISyncEnabled = useBetaFeature(BETA_FEATURES.OPENAPI_SYNC);\n  const { dropdownContainerRef } = useSidebarAccordion();\n  const [showNewFolderModal, setShowNewFolderModal] = useState(false);\n  const [showNewRequestModal, setShowNewRequestModal] = useState(false);\n  const [showRenameCollectionModal, setShowRenameCollectionModal] = useState(false);\n  const [showCloneCollectionModalOpen, setShowCloneCollectionModalOpen] = useState(false);\n  const [showShareCollectionModal, setShowShareCollectionModal] = useState(false);\n  const [showGenerateDocumentationModal, setShowGenerateDocumentationModal] = useState(false);\n  const [showRemoveCollectionModal, setShowRemoveCollectionModal] = useState(false);\n  const [dropType, setDropType] = useState(null);\n  const [isKeyboardFocused, setIsKeyboardFocused] = useState(false);\n  const [showEmptyState, setShowEmptyState] = useState(false);\n  const dispatch = useDispatch();\n  const isLoading = collection.isLoading;\n  const collectionRef = useRef(null);\n  // Only count persisted items; transients don't affect empty state\n  const itemCount = collection.items?.filter((i) => !i.isTransient).length || 0;\n\n  const isCollectionFocused = useSelector(isTabForItemActive({ itemUid: collection.uid }));\n  const { hasCopiedItems } = useSelector((state) => state.app.clipboard);\n  const menuDropdownRef = useRef(null);\n\n  // Open the OpenAPI Sync tab\n  const openOpenAPISyncTab = () => {\n    ensureCollectionIsMounted();\n    dispatch(\n      addTab({\n        uid: uuid(),\n        collectionUid: collection.uid,\n        type: 'openapi-sync'\n      })\n    );\n  };\n\n  const handleRun = () => {\n    dispatch(\n      addTab({\n        uid: uuid(),\n        collectionUid: collection.uid,\n        type: 'collection-runner'\n      })\n    );\n  };\n\n  const ensureCollectionIsMounted = () => {\n    if (collection.mountStatus === 'mounted') {\n      return;\n    }\n    dispatch(mountCollection({\n      collectionUid: collection.uid,\n      collectionPathname: collection.pathname,\n      brunoConfig: collection.brunoConfig\n    }));\n  };\n\n  const hasSearchText = searchText && searchText?.trim()?.length;\n  const collectionIsCollapsed = hasSearchText ? false : collection.collapsed;\n\n  const iconClassName = classnames({\n    'rotate-90': !collectionIsCollapsed\n  });\n\n  const handleClick = (event) => {\n    if (event.detail != 1) return;\n    // Check if the click came from the chevron icon\n    const isChevronClick = event.target.closest('svg')?.classList.contains('chevron-icon');\n    setTimeout(scrollToTheActiveTab, 50);\n\n    ensureCollectionIsMounted();\n\n    if (collection.collapsed) {\n      dispatch(toggleCollection(collection.uid));\n      // Set default jsSandboxMode to 'safe' if not present and save to disk\n      if (!collection.securityConfig?.jsSandboxMode) {\n        dispatch(saveCollectionSecurityConfig(collection.uid, {\n          jsSandboxMode: 'safe'\n        }));\n      }\n    }\n\n    if (!isChevronClick) {\n      dispatch(\n        addTab({\n          uid: collection.uid,\n          collectionUid: collection.uid,\n          type: 'collection-settings'\n        })\n      );\n    }\n  };\n\n  const handleDoubleClick = (_event) => {\n    dispatch(makeTabPermanent({ uid: collection.uid }));\n  };\n\n  const handleCollectionCollapse = (e) => {\n    e.stopPropagation();\n    e.preventDefault();\n    ensureCollectionIsMounted();\n    dispatch(toggleCollection(collection.uid));\n  };\n\n  // prevent the parent's double-click handler from firing\n  const handleCollectionDoubleClick = (e) => {\n    e.stopPropagation();\n    e.preventDefault();\n  };\n\n  const handleRightClick = (event) => {\n    event.preventDefault();\n    menuDropdownRef.current?.show();\n  };\n\n  const handleCollapseFullCollection = () => {\n    dispatch(collapseFullCollection({ collectionUid: collection.uid }));\n  };\n\n  const viewCollectionSettings = () => {\n    dispatch(\n      addTab({\n        uid: collection.uid,\n        collectionUid: collection.uid,\n        type: 'collection-settings'\n      })\n    );\n  };\n\n  const handleShowInFolder = () => {\n    dispatch(showInFolder(collection.pathname)).catch((error) => {\n      console.error('Error opening the folder', error);\n      toast.error('Error opening the folder');\n    });\n  };\n\n  const handlePasteItem = () => {\n    dispatch(pasteItem(collection.uid, null))\n      .then(() => {\n        toast.success('Item pasted successfully');\n      })\n      .catch((err) => {\n        toast.error(err ? err.message : 'An error occurred while pasting the item');\n      });\n  };\n\n  // Keyboard shortcuts handler for collection\n  const handleKeyDown = (e) => {\n    // Detect Mac by checking both metaKey and platform\n    const isMac = navigator.userAgent?.includes('Mac') || navigator.platform?.startsWith('Mac');\n    const isModifierPressed = isMac ? e.metaKey : e.ctrlKey;\n\n    if (isModifierPressed && e.key.toLowerCase() === 'v') {\n      e.preventDefault();\n      e.stopPropagation();\n      handlePasteItem();\n    }\n  };\n\n  const handleFocus = () => {\n    setIsKeyboardFocused(true);\n  };\n\n  const handleBlur = () => {\n    setIsKeyboardFocused(false);\n  };\n\n  const isCollectionItem = (itemType) => {\n    return itemType === 'collection-item';\n  };\n\n  const [{ isDragging }, drag, dragPreview] = useDrag({\n    type: 'collection',\n    item: collection,\n    collect: (monitor) => ({\n      isDragging: monitor.isDragging()\n    }),\n    options: {\n      dropEffect: 'move'\n    }\n  });\n\n  const [{ isOver }, drop] = useDrop({\n    accept: ['collection', 'collection-item'],\n    hover: (_draggedItem, monitor) => {\n      const itemType = monitor.getItemType();\n      if (isCollectionItem(itemType)) {\n        // For collection items, always show full highlight (inside drop)\n        setDropType('inside');\n      } else {\n        // For collections, show line indicator (adjacent drop)\n        setDropType('adjacent');\n      }\n    },\n    drop: (draggedItem, monitor) => {\n      const itemType = monitor.getItemType();\n      if (isCollectionItem(itemType)) {\n        dispatch(handleCollectionItemDrop({ targetItem: collection, draggedItem, dropType: 'inside', collectionUid: collection.uid }));\n      } else {\n        dispatch(moveCollectionAndPersist({ draggedItem, targetItem: collection }));\n      }\n      setDropType(null);\n    },\n    canDrop: (draggedItem) => {\n      return draggedItem.uid !== collection.uid;\n    },\n    collect: (monitor) => ({\n      isOver: monitor.isOver()\n    })\n  });\n\n  useEffect(() => {\n    dragPreview(getEmptyImage(), { captureDraggingState: true });\n  }, []);\n\n  useEffect(() => {\n    if (isCollectionFocused && collectionRef.current) {\n      try {\n        collectionRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });\n      } catch (err) {\n        // ignore scroll errors\n      }\n    }\n  }, [isCollectionFocused]);\n\n  // Debounce showing empty state to prevent flicker\n  // Race condition: isLoading can become false before items batch arrives from IPC\n  useEffect(() => {\n    const isMounted = collection.mountStatus === 'mounted';\n    const hasItems = itemCount > 0;\n\n    if (hasItems || isLoading || !isMounted) {\n      setShowEmptyState(false);\n      return;\n    }\n\n    const timer = setTimeout(() => setShowEmptyState(true), EMPTY_STATE_DELAY_MS);\n    return () => clearTimeout(timer);\n  }, [itemCount, isLoading, collection.mountStatus]);\n\n  if (searchText && searchText.length) {\n    if (!doesCollectionHaveItemsMatchingSearchText(collection, searchText)) {\n      return null;\n    }\n  }\n\n  const collectionRowClassName = classnames('flex py-1 collection-name items-center', {\n    'item-hovered': isOver && dropType === 'adjacent', // For collection-to-collection moves (show line)\n    'drop-target': isOver && dropType === 'inside', // For collection-item drops (highlight full area)\n    'collection-focused-in-tab': isCollectionFocused && !isKeyboardFocused,\n    'collection-keyboard-focused': isKeyboardFocused\n  });\n\n  // we need to sort request items by seq property\n  const sortItemsBySequence = (items = []) => {\n    return items.sort((a, b) => a.seq - b.seq);\n  };\n\n  const requestItems = sortItemsBySequence(filter(collection.items, (i) => isItemARequest(i) && !i.isTransient));\n  const folderItems = sortByNameThenSequence(filter(collection.items, (i) => isItemAFolder(i) && !i.isTransient));\n  const showEmptyCollectionMessage = showEmptyState && !hasSearchText;\n\n  const emptyStateMenuItems = createEmptyStateMenuItems({ dispatch, collection, itemUid: null });\n\n  const menuItems = [\n    {\n      id: 'new-request',\n      leftSection: IconFilePlus,\n      label: 'New Request',\n      onClick: () => {\n        ensureCollectionIsMounted();\n        setShowNewRequestModal(true);\n      }\n    },\n    {\n      id: 'new-folder',\n      leftSection: IconFolderPlus,\n      label: 'New Folder',\n      onClick: () => {\n        ensureCollectionIsMounted();\n        setShowNewFolderModal(true);\n      }\n    },\n    {\n      id: 'run',\n      leftSection: IconPlayerPlay,\n      label: 'Run',\n      onClick: () => {\n        ensureCollectionIsMounted();\n        handleRun();\n      }\n    },\n    {\n      id: 'clone',\n      leftSection: IconCopy,\n      label: 'Clone',\n      testId: 'clone-collection',\n      onClick: () => {\n        setShowCloneCollectionModalOpen(true);\n      }\n    },\n    ...(isOpenAPISyncEnabled ? [{\n      id: 'sync-openapi',\n      leftSection: OpenAPISyncIcon,\n      label: 'OpenAPI',\n      rightSection: <StatusBadge status=\"info\" size=\"xs\">Beta</StatusBadge>,\n      onClick: openOpenAPISyncTab\n    }] : []),\n    ...(hasCopiedItems\n      ? [\n          {\n            id: 'paste',\n            leftSection: IconClipboard,\n            label: 'Paste',\n            onClick: handlePasteItem\n          }\n        ]\n      : []),\n    {\n      id: 'rename',\n      leftSection: IconEdit,\n      label: 'Rename',\n      onClick: () => {\n        setShowRenameCollectionModal(true);\n      }\n    },\n    {\n      id: 'share',\n      leftSection: IconShare,\n      label: 'Share',\n      onClick: () => {\n        ensureCollectionIsMounted();\n        setShowShareCollectionModal(true);\n      }\n    },\n    {\n      id: 'generate-docs',\n      leftSection: IconBook,\n      label: 'Generate Docs',\n      onClick: () => {\n        ensureCollectionIsMounted();\n        setShowGenerateDocumentationModal(true);\n      }\n    },\n    {\n      id: 'collapse',\n      leftSection: IconFoldDown,\n      label: 'Collapse',\n      onClick: handleCollapseFullCollection\n    },\n    {\n      id: 'show-in-folder',\n      leftSection: IconFolder,\n      label: getRevealInFolderLabel(),\n      onClick: handleShowInFolder\n    },\n    {\n      id: 'divider-1',\n      type: 'divider'\n    },\n    {\n      id: 'settings',\n      leftSection: IconSettings,\n      label: 'Settings',\n      onClick: viewCollectionSettings\n    },\n    {\n      id: 'terminal',\n      leftSection: IconTerminal2,\n      label: 'Open in Terminal',\n      onClick: async () => {\n        const collectionCwd = collection.pathname;\n        await openDevtoolsAndSwitchToTerminal(dispatch, collectionCwd);\n      }\n    },\n    {\n      id: 'remove',\n      leftSection: IconX,\n      label: 'Remove',\n      onClick: () => {\n        setShowRemoveCollectionModal(true);\n      }\n    }\n  ];\n\n  return (\n    <StyledWrapper className=\"flex flex-col\" id={`collection-${collection.name.replace(/\\s+/g, '-').toLowerCase()}`}>\n      {showNewRequestModal && <NewRequest collectionUid={collection.uid} onClose={() => setShowNewRequestModal(false)} />}\n      {showNewFolderModal && <NewFolder collectionUid={collection.uid} onClose={() => setShowNewFolderModal(false)} />}\n      {showRenameCollectionModal && (\n        <RenameCollection collectionUid={collection.uid} onClose={() => setShowRenameCollectionModal(false)} />\n      )}\n      {showRemoveCollectionModal && (\n        <RemoveCollection collectionUid={collection.uid} onClose={() => setShowRemoveCollectionModal(false)} />\n      )}\n      {showShareCollectionModal && (\n        <ShareCollection collectionUid={collection.uid} onClose={() => setShowShareCollectionModal(false)} />\n      )}\n      {showGenerateDocumentationModal && (\n        <GenerateDocumentation collectionUid={collection.uid} onClose={() => setShowGenerateDocumentationModal(false)} />\n      )}\n      {showCloneCollectionModalOpen && (\n        <CloneCollection collectionUid={collection.uid} onClose={() => setShowCloneCollectionModalOpen(false)} />\n      )}\n      <CollectionItemDragPreview />\n      <div\n        className={collectionRowClassName}\n        ref={(node) => {\n          collectionRef.current = node;\n          drag(drop(node));\n        }}\n        tabIndex={0}\n        onKeyDown={handleKeyDown}\n        onFocus={handleFocus}\n        onBlur={handleBlur}\n        data-testid=\"sidebar-collection-row\"\n      >\n        <div\n          className=\"flex flex-grow items-center overflow-hidden\"\n          onClick={handleClick}\n          onDoubleClick={handleDoubleClick}\n          onContextMenu={handleRightClick}\n        >\n          <ActionIcon style={{ width: 16, minWidth: 16 }}>\n            <IconChevronRight\n              size={16}\n              strokeWidth={2}\n              className={`chevron-icon ${iconClassName}`}\n              style={{ width: 16, minWidth: 16, color: 'rgb(160 160 160)' }}\n              onClick={handleCollectionCollapse}\n              onDoubleClick={handleCollectionDoubleClick}\n            />\n          </ActionIcon>\n          <div className=\"ml-1 w-full\" id=\"sidebar-collection-name\" title={collection.name}>\n            {collection.name}\n          </div>\n          {isLoading ? <IconLoader2 className=\"animate-spin mx-1\" size={18} strokeWidth={1.5} /> : null}\n        </div>\n        <div>\n          <div className=\"pr-2\">\n            <MenuDropdown\n              ref={menuDropdownRef}\n              items={menuItems}\n              placement=\"bottom-start\"\n              appendTo={dropdownContainerRef?.current || document.body}\n              popperOptions={{ strategy: 'fixed' }}\n              data-testid=\"collection-actions\"\n            >\n              <ActionIcon className=\"collection-actions\">\n                <IconDots size={18} />\n              </ActionIcon>\n            </MenuDropdown>\n          </div>\n        </div>\n      </div>\n      <div>\n        {!collectionIsCollapsed ? (\n          <div>\n            {folderItems?.map?.((i) => {\n              return <CollectionItem key={i.uid} item={i} collectionUid={collection.uid} collectionPathname={collection.pathname} searchText={searchText} />;\n            })}\n            {requestItems?.map?.((i) => {\n              return <CollectionItem key={i.uid} item={i} collectionUid={collection.uid} collectionPathname={collection.pathname} searchText={searchText} />;\n            })}\n            {showEmptyCollectionMessage ? (\n              <div className=\"empty-collection-message\">\n                <div className=\"indent-block\" style={{ width: 16, minWidth: 16, height: '100%' }}>\n                  &nbsp;\n                </div>\n                <div style={{ paddingLeft: 8 }}>\n                  <MenuDropdown\n                    items={emptyStateMenuItems}\n                    placement=\"bottom-start\"\n                    appendTo={dropdownContainerRef?.current || document.body}\n                    popperOptions={{ strategy: 'fixed' }}\n                  >\n                    <button className=\"ml-1 add-request-link\">+ Add request</button>\n                  </MenuDropdown>\n                </div>\n              </div>\n            ) : null}\n          </div>\n        ) : null}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Collection;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/CollectionSearch/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  margin: 4px 10px 8px 10px;\n  position: relative;\n\n  .search-icon {\n    position: absolute;\n    left: 10px;\n    top: 50%;\n    transform: translateY(-50%);\n    color: ${(props) => props.theme.sidebar.muted};\n    pointer-events: none;\n  }\n\n  input {\n    width: 100%;\n    height: 32px;\n    padding: 0 32px 0 32px;\n    font-size: 12px;\n    color: ${(props) => props.theme.sidebar.color};\n    background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n    border: 1px solid transparent;\n    border-radius: 6px;\n    outline: none;\n    transition: all 0.15s ease;\n\n    &::placeholder {\n      color: ${(props) => props.theme.sidebar.muted};\n    }\n\n    &:hover {\n      border-color: ${(props) => props.theme.input.border};\n    }\n\n    &:focus {\n      background: ${(props) => props.theme.input.bg};\n      border-color: ${(props) => props.theme.input.border};\n    }\n  }\n\n  .clear-icon {\n    position: absolute;\n    right: 8px;\n    top: 50%;\n    transform: translateY(-50%);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 18px;\n    height: 18px;\n    border-radius: 4px;\n    color: ${(props) => props.theme.sidebar.muted};\n    cursor: pointer;\n    transition: all 0.15s ease;\n\n    &:hover {\n      color: ${(props) => props.theme.sidebar.color};\n      background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/CollectionSearch/index.js",
    "content": "import { IconSearch, IconX } from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\n\nconst CollectionSearch = ({ searchText, setSearchText }) => {\n  return (\n    <StyledWrapper>\n      <IconSearch size={14} strokeWidth={1.5} className=\"search-icon\" />\n      <input\n        type=\"text\"\n        name=\"search\"\n        placeholder=\"Search requests...\"\n        id=\"search\"\n        autoComplete=\"off\"\n        autoCorrect=\"off\"\n        autoCapitalize=\"off\"\n        autoFocus\n        spellCheck=\"false\"\n        value={searchText}\n        onChange={(e) => setSearchText(e.target.value.toLowerCase())}\n      />\n      {searchText !== '' && (\n        <div className=\"clear-icon\" onClick={() => setSearchText('')}>\n          <IconX size={14} strokeWidth={1.5} />\n        </div>\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default CollectionSearch;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/CreateOrOpenCollection/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  color: ${(props) => props.theme.colors.text.muted};\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/CreateOrOpenCollection/index.js",
    "content": "import { useTheme } from '../../../../providers/Theme';\nimport { useDispatch } from 'react-redux';\nimport { openCollection } from 'providers/ReduxStore/slices/collections/actions';\n\nimport toast from 'react-hot-toast';\nimport styled from 'styled-components';\nimport StyledWrapper from './StyledWrapper';\n\nconst LinkStyle = styled.span`\n  color: ${(props) => props.theme['text-link']};\n`;\n\nconst CreateOrOpenCollection = ({ onCreateClick }) => {\n  const { theme } = useTheme();\n  const dispatch = useDispatch();\n\n  const handleOpenCollection = () => {\n    dispatch(openCollection()).catch(\n      (err) => {\n        console.log(err);\n        toast.error('An error occurred while opening the collection');\n      }\n    );\n  };\n  const CreateLink = () => (\n    <LinkStyle\n      className=\"underline text-link cursor-pointer\"\n      theme={theme}\n      onClick={onCreateClick}\n    >\n      Create\n    </LinkStyle>\n  );\n  const OpenLink = () => (\n    <LinkStyle className=\"underline text-link cursor-pointer\" theme={theme} onClick={() => handleOpenCollection(true)}>\n      Open\n    </LinkStyle>\n  );\n\n  return (\n    <StyledWrapper className=\"px-2 mt-4\">\n      <div className=\"text-xs text-center\">\n        <div>No collections found.</div>\n        <div className=\"mt-2\">\n          <CreateLink /> or <OpenLink /> Collection.\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default CreateOrOpenCollection;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/InlineCollectionCreator/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .inline-collection-creator {\n    display: flex;\n    align-items: center;\n    gap: 4px;\n    height: 1.6rem;\n    padding-left: 8px;\n    padding-right: 4px;\n  }\n\n  .input-wrapper {\n    display: flex;\n    align-items: center;\n    flex: 1;\n    min-width: 0;\n    border: 1px solid ${(props) => props.theme.input.border};\n    border-radius: 3px;\n    background: ${(props) => props.theme.input.bg};\n\n    &:focus-within {\n      border-color: ${(props) => props.theme.input.focusBorder};\n    }\n  }\n\n  .inline-collection-input {\n    font-size: 13px;\n    padding: 1px 4px;\n    border: none;\n    background: transparent;\n    color: ${(props) => props.theme.text};\n    outline: none;\n    flex: 1;\n    min-width: 0;\n  }\n\n  .cog-btn {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    flex-shrink: 0;\n    width: 20px;\n    height: 100%;\n    border: none;\n    cursor: pointer;\n    background: transparent;\n    color: ${(props) => props.theme.text};\n    opacity: 0.5;\n\n    &:hover {\n      opacity: 1;\n    }\n  }\n\n  .inline-actions {\n    display: flex;\n    align-items: center;\n    gap: 2px;\n    flex-shrink: 0;\n  }\n\n  .inline-action-btn {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 20px;\n    height: 20px;\n    border: none;\n    border-radius: 3px;\n    cursor: pointer;\n    background: transparent;\n    color: ${(props) => props.theme.text};\n\n    &:hover {\n      background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n    }\n\n    &.save {\n      color: ${(props) => props.theme.colors.text.green};\n    }\n\n    &.cancel {\n      color: ${(props) => props.theme.colors.text.danger};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/InlineCollectionCreator/index.js",
    "content": "import { useRef, useEffect, useState, useCallback } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { IconCheck, IconX, IconSettings } from '@tabler/icons';\nimport get from 'lodash/get';\nimport path from 'utils/common/path';\nimport toast from 'react-hot-toast';\nimport { createCollection } from 'providers/ReduxStore/slices/collections/actions';\nimport { sanitizeName, validateName, validateNameError } from 'utils/common/regex';\nimport { DEFAULT_COLLECTION_FORMAT } from 'utils/common/constants';\nimport { multiLineMsg } from 'utils/common';\nimport { formatIpcError } from 'utils/common/error';\nimport StyledWrapper from './StyledWrapper';\n\nconst InlineCollectionCreator = ({ onComplete, onCancel, onOpenAdvanced }) => {\n  const inputRef = useRef(null);\n  const containerRef = useRef(null);\n  const dispatch = useDispatch();\n  const [isCreating, setIsCreating] = useState(false);\n  const openingAdvancedRef = useRef(false);\n  const clickedOutsideRef = useRef(false);\n\n  const preferences = useSelector((state) => state.app.preferences);\n  const workspaces = useSelector((state) => state.workspaces?.workspaces || []);\n  const activeWorkspaceUid = useSelector((state) => state.workspaces?.activeWorkspaceUid);\n  const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);\n  const isDefaultWorkspace = activeWorkspace?.type === 'default';\n\n  const defaultLocation = isDefaultWorkspace\n    ? get(preferences, 'general.defaultLocation', '')\n    : (activeWorkspace?.pathname ? path.join(activeWorkspace.pathname, 'collections') : '');\n\n  useEffect(() => {\n    const focusAndSelect = (value) => {\n      if (!inputRef.current) {\n        return;\n      }\n      if (value) {\n        inputRef.current.value = value;\n      }\n      inputRef.current.focus();\n      inputRef.current.select();\n    };\n\n    if (defaultLocation) {\n      window.ipcRenderer?.invoke('renderer:find-unique-folder-name', 'Untitled Collection', defaultLocation)\n        ?.then((name) => focusAndSelect(name))\n        ?.catch(() => focusAndSelect());\n    } else {\n      focusAndSelect();\n    }\n  }, [defaultLocation]);\n\n  const handleCancel = () => {\n    if (isCreating || openingAdvancedRef.current) return;\n    onCancel();\n  };\n\n  const handleCreate = useCallback(async () => {\n    const fromOutside = clickedOutsideRef.current;\n    clickedOutsideRef.current = false;\n\n    if (isCreating || openingAdvancedRef.current) return;\n\n    const name = inputRef.current?.value?.trim();\n    if (!name) {\n      if (fromOutside) {\n        onCancel();\n      } else {\n        toast.error('Collection name is required');\n      }\n      return;\n    }\n\n    if (!validateName(name)) {\n      toast.error(validateNameError(name));\n      if (fromOutside) {\n        onCancel();\n      }\n      return;\n    }\n\n    if (!defaultLocation) {\n      toast.error('Please set a default location in Preferences > General');\n      onCancel();\n      return;\n    }\n\n    setIsCreating(true);\n    try {\n      const folderName = sanitizeName(name);\n      await dispatch(createCollection(name, folderName, defaultLocation, { format: DEFAULT_COLLECTION_FORMAT }));\n      toast.success('Collection created!');\n      onComplete();\n    } catch (e) {\n      toast.error(multiLineMsg('An error occurred while creating the collection', formatIpcError(e)));\n      setIsCreating(false);\n    }\n  }, [isCreating, defaultLocation, dispatch, onCancel, onComplete]);\n\n  // Click outside to create\n  useEffect(() => {\n    const handleClickOutside = (e) => {\n      if (containerRef.current && !containerRef.current.contains(e.target)) {\n        clickedOutsideRef.current = true;\n        handleCreate();\n      }\n    };\n    document.addEventListener('mousedown', handleClickOutside);\n    return () => document.removeEventListener('mousedown', handleClickOutside);\n  }, [handleCreate]);\n\n  const handleKeyDown = (e) => {\n    if (e.key === 'Enter') {\n      e.preventDefault();\n      handleCreate();\n    } else if (e.key === 'Escape') {\n      e.preventDefault();\n      handleCancel();\n    }\n  };\n\n  return (\n    <StyledWrapper>\n      <div className=\"inline-collection-creator\" ref={containerRef}>\n        <div className=\"input-wrapper\">\n          <input\n            ref={inputRef}\n            type=\"text\"\n            className=\"inline-collection-input\"\n            defaultValue=\"Untitled Collection\"\n            onKeyDown={handleKeyDown}\n            autoComplete=\"off\"\n            autoCorrect=\"off\"\n            autoCapitalize=\"off\"\n            spellCheck=\"false\"\n            disabled={isCreating}\n          />\n          <button\n            className=\"cog-btn\"\n            onMouseDown={(e) => e.preventDefault()}\n            onClick={() => {\n              openingAdvancedRef.current = true;\n              onOpenAdvanced(inputRef.current?.value?.trim());\n            }}\n            title=\"Advanced options\"\n            disabled={isCreating}\n          >\n            <IconSettings size={13} strokeWidth={1.5} />\n          </button>\n        </div>\n        <div className=\"inline-actions\">\n          <button\n            className=\"inline-action-btn save\"\n            onClick={handleCreate}\n            onMouseDown={(e) => e.preventDefault()}\n            title=\"Create\"\n            disabled={isCreating}\n          >\n            <IconCheck size={14} strokeWidth={2} />\n          </button>\n          <button\n            className=\"inline-action-btn cancel\"\n            onClick={handleCancel}\n            onMouseDown={(e) => e.preventDefault()}\n            title=\"Cancel\"\n            disabled={isCreating}\n          >\n            <IconX size={14} strokeWidth={2} />\n          </button>\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default InlineCollectionCreator;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/RemoveCollectionsModal/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  width: 600px;\n  overflow: hidden;\n  box-sizing: border-box;\n\n  .collections-list-container {\n    width: 100%;\n    max-height: 150px;\n    overflow-y: auto;\n    overflow-x: hidden;\n    padding: 4px 0;\n    box-sizing: border-box;\n  }\n\n  .collections-list {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 8px;\n    width: 100%;\n  }\n\n  .collection-tag {\n    display: inline-flex;\n    align-items: center;\n    padding: 6px 12px;\n    background-color: ${(props) => props.theme.background.surface2};\n    border: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};\n    border-radius: 4px;\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-weight: 500;\n    color: ${(props) => props.theme.text};\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n\n  .collection-tag-text {\n    display: inline-block;\n    max-width: 100%;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n\n  .show-more-link,\n  .show-less-link {\n    display: inline-flex;\n    align-items: center;\n    \n    &:hover {\n      span {\n        text-decoration: underline;\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/RemoveCollectionsModal/index.js",
    "content": "import React, { useState, useMemo } from 'react';\nimport toast from 'react-hot-toast';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { filter, groupBy } from 'lodash';\nimport Modal from 'components/Modal';\nimport Portal from 'components/Portal';\nimport {\n  removeCollection,\n  saveMultipleRequests,\n  saveMultipleCollections,\n  saveMultipleFolders\n} from 'providers/ReduxStore/slices/collections/actions';\nimport {\n  findCollectionByUid,\n  flattenItems,\n  isItemARequest,\n  isItemAFolder,\n  hasRequestChanges\n} from 'utils/collections/index';\nimport { IconAlertTriangle } from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\nimport Button from 'ui/Button';\n\nconst MAX_COLLECTIONS_WIDTH = 530;\nconst CHARACTER_WIDTH = 8;\nconst COLLECTION_PADDING = 24;\nconst COLLECTION_GAP = 12;\n\nconst getDisplayItems = (items, maxWidth = MAX_COLLECTIONS_WIDTH) => {\n  const visibleItems = [];\n  let totalWidth = 0;\n\n  for (let i = 0; i < items.length; i += 1) {\n    const currentItem = items[i];\n    const name = typeof currentItem === 'string' ? currentItem : currentItem?.name || '';\n    const width = name.length * CHARACTER_WIDTH + COLLECTION_PADDING + COLLECTION_GAP;\n\n    if (i === 0 || totalWidth + width <= maxWidth) {\n      totalWidth += width;\n      visibleItems.push(currentItem);\n    } else {\n      break;\n    }\n  }\n\n  return visibleItems;\n};\n\nconst RemoveCollectionsModal = ({ collectionUids, onClose }) => {\n  const dispatch = useDispatch();\n  const allCollections = useSelector((state) => state.collections.collections || []);\n  const [showAllCollections, setShowAllCollections] = useState(false);\n\n  const allDrafts = useMemo(() => {\n    const requestDrafts = [];\n    const collectionDrafts = [];\n    const folderDrafts = [];\n\n    collectionUids.forEach((collectionUid) => {\n      const collection = findCollectionByUid(allCollections, collectionUid);\n      if (!collection) {\n        return;\n      }\n\n      // Check for collection draft\n      if (collection.draft) {\n        collectionDrafts.push({\n          name: collection.name,\n          collectionUid: collectionUid\n        });\n      }\n\n      // Check for request and folder drafts\n      const items = flattenItems(collection.items);\n\n      // Request drafts\n      const unsavedRequests = filter(items, (item) => isItemARequest(item) && hasRequestChanges(item));\n      unsavedRequests.forEach((request) => {\n        requestDrafts.push({\n          ...request,\n          collectionUid: collectionUid\n        });\n      });\n\n      // Folder drafts\n      const unsavedFolders = filter(items, (item) => isItemAFolder(item) && item.draft);\n      unsavedFolders.forEach((folder) => {\n        folderDrafts.push({\n          name: folder.name,\n          folderUid: folder.uid,\n          collectionUid: collectionUid\n        });\n      });\n    });\n\n    return { requestDrafts, collectionDrafts, folderDrafts };\n  }, [collectionUids, allCollections]);\n\n  const collectionsWithUnsavedChanges = useMemo(() => {\n    const allDraftTypes = [...allDrafts.collectionDrafts, ...allDrafts.folderDrafts, ...allDrafts.requestDrafts];\n    const draftsByCollection = groupBy(allDraftTypes, 'collectionUid');\n    return Object.keys(draftsByCollection)\n      .map((collectionUid) => {\n        const collection = findCollectionByUid(allCollections, collectionUid);\n        return collection ? { uid: collectionUid, name: collection.name } : null;\n      })\n      .filter(Boolean);\n  }, [allDrafts, allCollections]);\n\n  const hasUnsavedChanges\n    = allDrafts.collectionDrafts.length > 0 || allDrafts.folderDrafts.length > 0 || allDrafts.requestDrafts.length > 0;\n\n  const handleCloseAllCollections = () => {\n    const removalPromises = collectionUids.map((uid) => dispatch(removeCollection(uid)));\n\n    Promise.all(removalPromises)\n      .then(() => {\n        toast.success('Closed all collections');\n      })\n      .catch((error) => {\n        console.error('Error closing collections:', error);\n        toast.error('An error occurred while closing collections');\n      })\n      .finally(() => {\n        onClose();\n      });\n  };\n\n  const handleDiscard = () => {\n    handleCloseAllCollections();\n  };\n\n  const handleCancel = () => {\n    onClose();\n  };\n\n  const handleSave = async () => {\n    try {\n      const savePromises = [];\n\n      // Save all collection drafts\n      if (allDrafts.collectionDrafts.length > 0) {\n        savePromises.push(dispatch(saveMultipleCollections(allDrafts.collectionDrafts)));\n      }\n\n      // Save all folder drafts\n      if (allDrafts.folderDrafts.length > 0) {\n        savePromises.push(dispatch(saveMultipleFolders(allDrafts.folderDrafts)));\n      }\n\n      // Save all request drafts\n      if (allDrafts.requestDrafts.length > 0) {\n        savePromises.push(dispatch(saveMultipleRequests(allDrafts.requestDrafts)));\n      }\n\n      await Promise.all(savePromises);\n      handleCloseAllCollections();\n    } catch (error) {\n      console.error('Error saving drafts:', error);\n      toast.error('An error occurred while saving changes');\n      handleCancel();\n    }\n  };\n\n  if (collectionUids.length === 0) {\n    return null;\n  }\n\n  const hasMultipleCollections = collectionUids.length > 1;\n  const singleCollectionName = hasMultipleCollections\n    ? null\n    : findCollectionByUid(allCollections, collectionUids[0])?.name;\n\n  const displayedCollections = useMemo(() => showAllCollections ? collectionsWithUnsavedChanges : getDisplayItems(collectionsWithUnsavedChanges),\n    [collectionsWithUnsavedChanges, showAllCollections]);\n  const hasMoreCollections = collectionsWithUnsavedChanges.length > displayedCollections.length;\n  const hiddenCollectionsCount = collectionsWithUnsavedChanges.length - displayedCollections.length;\n\n  const toggleButton = hasMoreCollections || showAllCollections ? (\n    <span\n      className={`${showAllCollections ? 'show-less-link' : 'show-more-link'} w-fit flex items-center mt-2 cursor-pointer`}\n      onClick={() => setShowAllCollections(!showAllCollections)}\n    >\n      <span className=\"text-link\">\n        {showAllCollections ? 'Show less' : `Show ${hiddenCollectionsCount} more`}\n      </span>\n    </span>\n  ) : null;\n\n  return (\n    <Portal>\n      <Modal\n        size=\"md\"\n        title=\"Close all collections\"\n        disableEscapeKey={hasUnsavedChanges}\n        disableCloseOnOutsideClick={hasUnsavedChanges}\n        handleCancel={handleCancel}\n        hideFooter={true}\n      >\n        <StyledWrapper>\n          {hasUnsavedChanges ? (\n            <>\n              <div className=\"flex items-center font-normal\">\n                <IconAlertTriangle size={32} strokeWidth={1.5} className=\"text-yellow-600\" />\n                <h1 className=\"ml-2 text-lg font-medium\">Hold on..</h1>\n              </div>\n              <div className=\"font-normal mt-4\">\n                Do you want to save changes you made to the following{' '}\n                {collectionsWithUnsavedChanges.length === 1 ? 'collection' : 'collections'}?\n              </div>\n              <div className=\"mt-2 text-xs text-gray-500\">\n                Collections will be removed from the current workspace but will still be available in the file system and can be re-opened later.\n              </div>\n\n              <div className=\"mt-4\">\n                <div className=\"collections-list-container\">\n                  <div className=\"collections-list\">\n                    {displayedCollections.map(({ uid, name }) => (\n                      <span key={uid} className=\"collection-tag\">\n                        <span className=\"collection-tag-text\">{name}</span>\n                      </span>\n                    ))}\n                    {toggleButton}\n                  </div>\n                </div>\n              </div>\n\n              <div className=\"flex justify-between mt-6\">\n                <div>\n                  <Button color=\"danger\" onClick={handleDiscard}>\n                    Discard and Close\n                  </Button>\n                </div>\n                <div>\n                  <Button className=\"mr-2\" color=\"secondary\" variant=\"ghost\" onClick={handleCancel}>\n                    Cancel\n                  </Button>\n                  <Button onClick={handleSave}>\n                    Save and Close\n                  </Button>\n                </div>\n              </div>\n            </>\n          ) : (\n            <>\n              <div className=\"mt-4\">\n                {hasMultipleCollections ? (\n                  `Are you sure you want to close all ${collectionUids.length} collections in this workspace?`\n                ) : (\n                  <>\n                    Are you sure you want to close the collection <strong>{singleCollectionName}</strong> from this workspace?\n                  </>\n                )}\n              </div>\n              <div className=\"mt-4 text-xs text-gray-500\">\n                Collections will be removed from the current workspace but will still be available in the file system and can be re-opened later.\n              </div>\n              <div className=\"flex justify-end mt-6\">\n                <Button className=\"mr-2\" color=\"secondary\" variant=\"ghost\" onClick={handleCancel} data-testid=\"modal-close-button\">\n                  Cancel\n                </Button>\n                <Button color=\"warning\" onClick={handleCloseAllCollections}>\n                  {hasMultipleCollections ? 'Close All' : 'Close'}\n                </Button>\n              </div>\n            </>\n          )}\n        </StyledWrapper>\n      </Modal>\n    </Portal>\n  );\n};\n\nexport default RemoveCollectionsModal;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/SelectCollection/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  div.collection {\n    padding: 4px 6px;\n    padding-left: 8px;\n    display: flex;\n    align-items: center;\n    border-radius: 3px;\n    cursor: pointer;\n\n    &:hover {\n      background-color: ${(props) => props.theme.plainGrid.hoverBg};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/SelectCollection/index.js",
    "content": "import React from 'react';\nimport Modal from 'components/Modal/index';\nimport { IconFiles } from '@tabler/icons';\nimport { useSelector } from 'react-redux';\nimport StyledWrapper from './StyledWrapper';\n\nconst SelectCollection = ({ onClose, onSelect, title }) => {\n  const { collections } = useSelector((state) => state.collections);\n\n  return (\n    <StyledWrapper>\n      <Modal size=\"sm\" title={title || 'Select Collection'} hideFooter={true} handleCancel={onClose}>\n        <ul className=\"mb-2\">\n          {collections && collections.length ? (\n            collections.map((c) => (\n              <div className=\"collection\" key={c.uid} onClick={() => onSelect(c.uid)}>\n                <IconFiles size={18} strokeWidth={1.5} /> <span className=\"ml-2\">{c.name}</span>\n              </div>\n            ))\n          ) : (\n            <div>No collections found</div>\n          )}\n        </ul>\n      </Modal>\n    </StyledWrapper>\n  );\n};\n\nexport default SelectCollection;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  flex: 1 1 0%;\n  min-height: 0;\n  overflow: hidden;\n  padding-top: 4px;\n  padding-bottom: 4px;\n\n  .collections-list {\n    flex: 1 1 0%;\n    min-height: 0;\n    padding-top: 4px;\n    padding-bottom: 4px;\n    overflow-y: auto;\n    overflow-x: hidden;\n\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Collections/index.js",
    "content": "import React, { useState, useMemo } from 'react';\nimport { useSelector } from 'react-redux';\nimport Collection from './Collection';\nimport StyledWrapper from './StyledWrapper';\nimport CreateOrOpenCollection from './CreateOrOpenCollection';\nimport CollectionSearch from './CollectionSearch/index';\nimport InlineCollectionCreator from './InlineCollectionCreator';\nimport { normalizePath } from 'utils/common/path';\nimport { isScratchCollection } from 'utils/collections';\n\nconst Collections = ({ showSearch, isCreatingCollection, onCreateClick, onDismissCreate, onOpenAdvancedCreate }) => {\n  const [searchText, setSearchText] = useState('');\n  const { collections } = useSelector((state) => state.collections);\n  const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);\n\n  const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid) || workspaces.find((w) => w.type === 'default');\n\n  const workspaceCollections = useMemo(() => {\n    if (!activeWorkspace) return [];\n\n    return collections.filter((c) => {\n      if (isScratchCollection(c, workspaces)) {\n        return false;\n      }\n      return activeWorkspace.collections?.some((wc) => normalizePath(wc.path) === normalizePath(c.pathname));\n    });\n  }, [activeWorkspace, collections, workspaces]);\n\n  if (!workspaceCollections || !workspaceCollections.length) {\n    return (\n      <StyledWrapper>\n        {isCreatingCollection && (\n          <InlineCollectionCreator\n            onComplete={onDismissCreate}\n            onCancel={onDismissCreate}\n            onOpenAdvanced={onOpenAdvancedCreate}\n          />\n        )}\n        {!isCreatingCollection && <CreateOrOpenCollection onCreateClick={onCreateClick} />}\n      </StyledWrapper>\n    );\n  }\n\n  return (\n    <StyledWrapper data-testid=\"collections\">\n      {showSearch && (\n        <CollectionSearch searchText={searchText} setSearchText={setSearchText} />\n      )}\n\n      <div className=\"collections-list\">\n        {isCreatingCollection && (\n          <InlineCollectionCreator\n            onComplete={onDismissCreate}\n            onCancel={onDismissCreate}\n            onOpenAdvanced={onOpenAdvancedCreate}\n          />\n        )}\n        {workspaceCollections && workspaceCollections.length\n          ? workspaceCollections.map((c) => {\n              return (\n                <Collection searchText={searchText} collection={c} key={c.uid} />\n              );\n            })\n          : null}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Collections;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/CreateCollection/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .advanced-options {\n    .caret {\n      color: ${(props) => props.theme.textLink};\n      fill: ${(props) => props.theme.textLink};\n    }\n  }\n\n  .report-issue-link {\n    display: inline-flex;\n    align-items: center;\n    gap: 0.375rem;\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.textLink};\n    cursor: pointer;\n    transition: opacity 0.15s ease;\n\n    &:hover {\n      opacity: 0.8;\n      text-decoration: underline;\n    }\n\n    svg {\n      flex-shrink: 0;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/CreateCollection/index.js",
    "content": "import React, { useRef, useEffect, useState, forwardRef } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { useFormik } from 'formik';\nimport * as Yup from 'yup';\nimport path from 'utils/common/path';\nimport { browseDirectory, createCollection } from 'providers/ReduxStore/slices/collections/actions';\nimport toast from 'react-hot-toast';\nimport Portal from 'components/Portal';\nimport Modal from 'components/Modal';\nimport { sanitizeName, validateName, validateNameError } from 'utils/common/regex';\nimport PathDisplay from 'components/PathDisplay/index';\nimport { IconArrowBackUp, IconEdit, IconCaretDown } from '@tabler/icons';\nimport Help from 'components/Help';\nimport Dropdown from 'components/Dropdown';\nimport { multiLineMsg } from 'utils/common';\nimport { formatIpcError } from 'utils/common/error';\nimport { DEFAULT_COLLECTION_FORMAT } from 'utils/common/constants';\nimport StyledWrapper from './StyledWrapper';\nimport get from 'lodash/get';\nimport Button from 'ui/Button';\n\nconst CreateCollection = ({ onClose, defaultLocation: propDefaultLocation, initialCollectionName = '' }) => {\n  const inputRef = useRef();\n  const dispatch = useDispatch();\n  const workspaces = useSelector((state) => state.workspaces?.workspaces || []);\n  const workspaceUid = useSelector((state) => state.workspaces?.activeWorkspaceUid);\n  const [isEditing, toggleEditing] = useState(false);\n  const [showFileFormat, setShowFileFormat] = useState(false);\n  const preferences = useSelector((state) => state.app.preferences);\n\n  const dropdownTippyRef = useRef();\n  const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);\n  const activeWorkspace = workspaces.find((w) => w.uid === workspaceUid);\n  const isDefaultWorkspace = activeWorkspace?.type === 'default';\n\n  const defaultLocation = isDefaultWorkspace ? get(preferences, 'general.defaultLocation', '') : (activeWorkspace?.pathname ? path.join(activeWorkspace.pathname, 'collections') : '');\n\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: {\n      collectionName: initialCollectionName,\n      collectionFolderName: initialCollectionName ? sanitizeName(initialCollectionName) : '',\n      collectionLocation: defaultLocation || '',\n      format: DEFAULT_COLLECTION_FORMAT\n    },\n    validationSchema: Yup.object({\n      collectionName: Yup.string()\n        .min(1, 'must be at least 1 character')\n        .max(255, 'must be 255 characters or less')\n        .required('collection name is required'),\n      collectionFolderName: Yup.string()\n        .min(1, 'must be at least 1 character')\n        .max(255, 'must be 255 characters or less')\n        .test('is-valid-collection-name', function (value) {\n          const isValid = validateName(value);\n          return isValid ? true : this.createError({ message: validateNameError(value) });\n        })\n        .required('folder name is required'),\n      collectionLocation: Yup.string().min(1, 'location is required').required('location is required'),\n      format: Yup.string().oneOf(['bru', 'yml'], 'invalid format').required('format is required')\n    }),\n    onSubmit: async (values) => {\n      try {\n        await dispatch(createCollection(values.collectionName,\n          values.collectionFolderName,\n          values.collectionLocation,\n          { format: values.format }));\n\n        toast.success('Collection created!');\n        onClose();\n      } catch (e) {\n        toast.error(multiLineMsg('An error occurred while creating the collection', formatIpcError(e)));\n      }\n    }\n  });\n\n  const browse = () => {\n    dispatch(browseDirectory())\n      .then((dirPath) => {\n        if (typeof dirPath === 'string') {\n          formik.setFieldValue('collectionLocation', dirPath);\n        }\n      })\n      .catch(() => {\n        formik.setFieldValue('collectionLocation', '');\n      });\n  };\n\n  useEffect(() => {\n    const timer = setTimeout(() => {\n      if (inputRef && inputRef.current) {\n        inputRef.current.focus();\n        inputRef.current.select();\n      }\n    }, 50);\n    return () => clearTimeout(timer);\n  }, [inputRef]);\n\n  const AdvancedOptions = forwardRef((props, ref) => {\n    return (\n      <div ref={ref} className=\"flex mr-2 text-link cursor-pointer items-center\">\n        <button\n          className=\"btn-advanced\"\n          type=\"button\"\n        >\n          Options\n        </button>\n        <IconCaretDown className=\"caret ml-1\" size={14} strokeWidth={2} />\n      </div>\n    );\n  });\n\n  return (\n    <Portal>\n      <StyledWrapper>\n        <Modal size=\"md\" title=\"Create Collection\" hideFooter={true} handleCancel={onClose}>\n          <form className=\"bruno-form\" onSubmit={formik.handleSubmit}>\n            <div>\n              <label htmlFor=\"collection-name\" className=\"flex items-center font-medium\">\n                Name\n              </label>\n              <input\n                id=\"collection-name\"\n                type=\"text\"\n                name=\"collectionName\"\n                ref={inputRef}\n                className=\"block textbox mt-2 w-full\"\n                onChange={(e) => {\n                  formik.handleChange(e);\n                  !isEditing && formik.setFieldValue('collectionFolderName', sanitizeName(e.target.value));\n                }}\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n                value={formik.values.collectionName || ''}\n              />\n              {formik.touched.collectionName && formik.errors.collectionName ? (\n                <div className=\"text-red-500\">{formik.errors.collectionName}</div>\n              ) : null}\n\n              <label htmlFor=\"collection-location\" className=\"font-medium mt-3 flex items-center\">\n                Location\n                <Help>\n                  <p>\n                    Bruno stores your collections on your computer's filesystem.\n                  </p>\n                  <p className=\"mt-2\">\n                    Choose the location where you want to store this collection.\n                  </p>\n                </Help>\n              </label>\n              <input\n                id=\"collection-location\"\n                type=\"text\"\n                name=\"collectionLocation\"\n                className=\"block textbox mt-2 w-full cursor-pointer\"\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n                readOnly={true}\n                value={formik.values.collectionLocation || ''}\n                onClick={browse}\n                onChange={(e) => {\n                  formik.setFieldValue('collectionLocation', e.target.value);\n                }}\n              />\n              {formik.touched.collectionLocation && formik.errors.collectionLocation ? (\n                <div className=\"text-red-500\">{formik.errors.collectionLocation}</div>\n              ) : null}\n              <div className=\"mt-1\">\n                <span\n                  className=\"text-link cursor-pointer hover:underline\"\n                  onClick={browse}\n                >\n                  Browse\n                </span>\n              </div>\n              {formik.values.collectionName?.trim()?.length > 0 && (\n                <div className=\"mt-4\">\n                  <div className=\"flex items-center justify-between\">\n                    <label htmlFor=\"filename\" className=\"flex items-center font-medium\">\n                      Folder Name\n                      <Help width=\"300\">\n                        <p>\n                          The name of the folder used to store the collection.\n                        </p>\n                        <p className=\"mt-2\">\n                          You can choose a folder name different from your collection's name or one compatible with filesystem rules.\n                        </p>\n                      </Help>\n                    </label>\n                    {isEditing ? (\n                      <IconArrowBackUp\n                        className=\"cursor-pointer opacity-50 hover:opacity-80\"\n                        size={16}\n                        strokeWidth={1.5}\n                        onClick={() => toggleEditing(false)}\n                      />\n                    ) : (\n                      <IconEdit\n                        className=\"cursor-pointer opacity-50 hover:opacity-80\"\n                        size={16}\n                        strokeWidth={1.5}\n                        onClick={() => toggleEditing(true)}\n                      />\n                    )}\n                  </div>\n                  {isEditing ? (\n                    <input\n                      id=\"collection-folder-name\"\n                      type=\"text\"\n                      name=\"collectionFolderName\"\n                      className=\"block textbox mt-2 w-full\"\n                      onChange={formik.handleChange}\n                      autoComplete=\"off\"\n                      autoCorrect=\"off\"\n                      autoCapitalize=\"off\"\n                      spellCheck=\"false\"\n                      value={formik.values.collectionFolderName || ''}\n                    />\n                  ) : (\n                    <div className=\"relative flex flex-row gap-1 items-center justify-between\">\n                      <PathDisplay\n                        baseName={formik.values.collectionFolderName}\n                      />\n                    </div>\n                  )}\n                  {formik.touched.collectionFolderName && formik.errors.collectionFolderName ? (\n                    <div className=\"text-red-500\">{formik.errors.collectionFolderName}</div>\n                  ) : null}\n                </div>\n              )}\n\n              {showFileFormat && (\n                <div className=\"mt-4\">\n                  <label htmlFor=\"format\" className=\"flex items-center font-medium\">\n                    File Format\n                    <Help width=\"300\">\n                      <p>\n                        Choose the file format for storing requests in this collection.\n                      </p>\n                      <p className=\"mt-2\">\n                        <strong>OpenCollection (YAML):</strong> Industry-standard YAML format (.yml files)\n                      </p>\n                      <p className=\"mt-1\">\n                        <strong>BRU:</strong> Bruno's native file format (.bru files)\n                      </p>\n                    </Help>\n                  </label>\n                  <select\n                    id=\"format\"\n                    name=\"format\"\n                    className=\"block textbox mt-2 w-full\"\n                    value={formik.values.format}\n                    onChange={formik.handleChange}\n                  >\n                    <option value=\"yml\">OpenCollection (YAML)</option>\n                    <option value=\"bru\">BRU Format (.bru)</option>\n                  </select>\n                  {formik.touched.format && formik.errors.format ? (\n                    <div className=\"text-red-500\">{formik.errors.format}</div>\n                  ) : null}\n                </div>\n              )}\n            </div>\n            <div className=\"flex justify-between items-center mt-8 bruno-modal-footer\">\n              <div className=\"flex advanced-options\">\n                <Dropdown onCreate={onDropdownCreate} icon={<AdvancedOptions />} placement=\"bottom-start\">\n                  <div\n                    className=\"dropdown-item\"\n                    key=\"show-file-format\"\n                    onClick={(e) => {\n                      dropdownTippyRef.current.hide();\n                      setShowFileFormat(!showFileFormat);\n                    }}\n                  >\n                    {showFileFormat ? 'Hide File Format' : 'Show File Format'}\n                  </div>\n                </Dropdown>\n              </div>\n              <div className=\"flex justify-end\">\n                <Button type=\"button\" color=\"secondary\" variant=\"ghost\" onClick={onClose} className=\"mr-2\">\n                  Cancel\n                </Button>\n                <Button type=\"submit\">\n                  Create\n                </Button>\n              </div>\n            </div>\n          </form>\n        </Modal>\n      </StyledWrapper>\n    </Portal>\n  );\n};\n\nexport default CreateCollection;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/GoldenEdition/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  color: ${(props) => props.theme.text};\n  .collection-options {\n    svg {\n      position: relative;\n      top: -1px;\n    }\n\n    .label {\n      cursor: pointer;\n      &:hover {\n        text-decoration: underline;\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/GoldenEdition/index.js",
    "content": "import React, { useState, useEffect } from 'react';\nimport Modal from 'components/Modal/index';\nimport { PostHog } from 'posthog-node';\nimport { uuid } from 'utils/common';\nimport { IconHeart, IconUser, IconUsers, IconPlus } from '@tabler/icons';\nimport platformLib from 'platform';\nimport StyledWrapper from './StyledWrapper';\nimport { useTheme } from 'providers/Theme/index';\n\nlet posthogClient = null;\nconst posthogApiKey = process.env.NEXT_PUBLIC_POSTHOG_API_KEY;\nconst getPosthogClient = () => {\n  if (posthogClient) {\n    return posthogClient;\n  }\n\n  posthogClient = new PostHog(posthogApiKey);\n  return posthogClient;\n};\nconst getAnonymousTrackingId = () => {\n  let id = localStorage.getItem('bruno.anonymousTrackingId');\n\n  if (!id || !id.length || id.length !== 21) {\n    id = uuid();\n    localStorage.setItem('bruno.anonymousTrackingId', id);\n  }\n\n  return id;\n};\n\nconst HeartIcon = () => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill=\"currentColor\"\n      className=\"flex-shrink-0 w-5 h-4 text-yellow-600\"\n      viewBox=\"0 0 16 16\"\n    >\n      <path fillRule=\"evenodd\" d=\"M8 1.314C12.438-3.248 23.534 4.735 8 15-7.534 4.736 3.562-3.248 8 1.314z\" />\n    </svg>\n  );\n};\n\nconst CheckIcon = () => {\n  return (\n    <svg\n      className=\"flex-shrink-0 w-5 h-5 text-green-500\"\n      fill=\"currentColor\"\n      viewBox=\"0 0 20 20\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n    >\n      <path\n        fillRule=\"evenodd\"\n        d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\"\n        clipRule=\"evenodd\"\n      >\n      </path>\n    </svg>\n  );\n};\n\nconst GoldenEdition = ({ onClose }) => {\n  const { displayedTheme } = useTheme();\n\n  useEffect(() => {\n    const anonymousId = getAnonymousTrackingId();\n    const client = getPosthogClient();\n    client.capture({\n      distinctId: anonymousId,\n      event: 'golden-edition-modal-opened',\n      properties: {\n        os: platformLib.os.family\n      }\n    });\n  }, []);\n\n  const goldenEditionBuyClick = () => {\n    const anonymousId = getAnonymousTrackingId();\n    const client = getPosthogClient();\n    client.capture({\n      distinctId: anonymousId,\n      event: 'golden-edition-buy-clicked',\n      properties: {\n        os: platformLib.os.family\n      }\n    });\n  };\n\n  const goldenEditionIndividuals = [\n    'Inbuilt Bru File Explorer',\n    'Visual Git (Like Gitlens for Vscode)',\n    'GRPC, Websocket, SocketIO, MQTT',\n    'Load Data from File for Collection Run',\n    'Developer Tools',\n    'OpenAPI Designer',\n    'Performance/Load Testing',\n    'Inbuilt Terminal',\n    'Custom Themes'\n  ];\n\n  const goldenEditionOrganizations = [\n    'Centralized License Management',\n    'Integration with Secret Managers',\n    'Private Collection Registry',\n    'Request Forms',\n    'Priority Support'\n  ];\n\n  const [pricingOption, setPricingOption] = useState('individuals');\n\n  const handlePricingOptionChange = (option) => {\n    setPricingOption(option);\n  };\n\n  const themeBasedContainerClassNames = displayedTheme === 'light' ? 'text-gray-900' : 'text-white';\n  const themeBasedTabContainerClassNames = displayedTheme === 'light' ? 'bg-gray-200' : 'bg-gray-800';\n  const themeBasedActiveTabClassNames\n    = displayedTheme === 'light' ? 'bg-white text-gray-900 font-medium' : 'bg-gray-700 text-white font-medium';\n\n  return (\n    <StyledWrapper>\n      <Modal size=\"sm\" title=\"Golden Edition\" handleCancel={onClose} hideFooter={true}>\n        <div className={`flex flex-col w-full ${themeBasedContainerClassNames}`}>\n          <div className=\"flex items-center justify-between\">\n            <h3 className=\"text-lg font-medium\">Golden Edition</h3>\n            <a\n              onClick={() => {\n                goldenEditionBuyClick();\n                window.open('https://www.usebruno.com/pricing', '_blank');\n              }}\n              target=\"_blank\"\n              className=\"flex text-white bg-yellow-600 hover:bg-yellow-700 font-medium rounded-lg px-4 py-2 text-center cursor-pointer\"\n            >\n              <IconHeart size={18} strokeWidth={1.5} /> <span className=\"ml-2\">Buy</span>\n            </a>\n          </div>\n          {pricingOption === 'individuals' ? (\n            <div>\n              <div className=\"my-4\">\n                <span className=\"text-3xl font-extrabold\">$19</span>\n              </div>\n              <p className=\"bg-yellow-200 text-black rounded-md px-2 py-1 mb-2 inline-flex\">One Time Payment</p>\n              <p>perpetual license for 2 devices, with 2 years of updates</p>\n            </div>\n          ) : (\n            <div>\n              <div className=\"my-4\">\n                <span className=\"text-3xl font-extrabold\">$49</span>\n                <span className=\"ml-2\">/&nbsp;user</span>\n              </div>\n              <p className=\"bg-yellow-200 text-black rounded-md px-2 py-1 mb-2 inline-flex\">One Time Payment</p>\n              <p>perpetual license with 2 years of updates</p>\n            </div>\n          )}\n          <div\n            className={`flex items-center justify-between my-8 w-40 rounded-full p-1 ${themeBasedTabContainerClassNames}`}\n            style={{ width: '24rem' }}\n          >\n            <div\n              className={`cursor-pointer w-1/2 h-8 flex items-center justify-center rounded-full ${\n                pricingOption === 'individuals' ? themeBasedActiveTabClassNames : 'text-gray-500'\n              }`}\n              onClick={() => handlePricingOptionChange('individuals')}\n            >\n              <IconUser className=\"text-gray-500 mr-2 icon\" size={16} strokeWidth={1.5} /> Individuals\n            </div>\n            <div\n              className={`cursor-pointer w-1/2 h-8 flex items-center justify-center rounded-full ${\n                pricingOption === 'organizations' ? themeBasedActiveTabClassNames : 'text-gray-500'\n              }`}\n              onClick={() => handlePricingOptionChange('organizations')}\n            >\n              <IconUsers className=\"text-gray-500 mr-2 icon\" size={16} strokeWidth={1.5} /> Organizations\n            </div>\n          </div>\n          <ul role=\"list\" className=\"space-y-3 text-left\">\n            <li className=\"flex items-center space-x-3\">\n              <HeartIcon />\n              <span>Support Bruno's Development</span>\n            </li>\n            {pricingOption === 'individuals' ? (\n              <>\n                {goldenEditionIndividuals.map((item, index) => (\n                  <li className=\"flex items-center space-x-3\" key={index}>\n                    <CheckIcon />\n                    <span>{item}</span>\n                  </li>\n                ))}\n              </>\n            ) : (\n              <>\n                <li className=\"flex items-center space-x-3 pb-4\">\n                  <IconPlus size={16} strokeWidth={1.5} style={{ marginLeft: '2px' }} />\n                  <span>Everything in the Individual Plan</span>\n                </li>\n                {goldenEditionOrganizations.map((item, index) => (\n                  <li className=\"flex items-center space-x-3\" key={index}>\n                    <CheckIcon />\n                    <span>{item}</span>\n                  </li>\n                ))}\n              </>\n            )}\n          </ul>\n        </div>\n      </Modal>\n    </StyledWrapper>\n  );\n};\n\nexport default GoldenEdition;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/ImportCollection/FileTab.js",
    "content": "import React, { useState, useRef } from 'react';\nimport { IconFileImport } from '@tabler/icons';\nimport { toastError } from 'utils/common/error';\nimport jsyaml from 'js-yaml';\nimport { isPostmanCollection } from 'utils/importers/postman-collection';\nimport { isInsomniaCollection } from 'utils/importers/insomnia-collection';\nimport { isOpenApiSpec } from 'utils/importers/openapi-collection';\nimport { isWSDLCollection } from 'utils/importers/wsdl-collection';\nimport { isBrunoCollection } from 'utils/importers/bruno-collection';\nimport { isOpenCollection } from 'utils/importers/opencollection';\nimport { useTheme } from 'providers/Theme';\n\nconst convertFileToObject = async (file) => {\n  const text = await file.text();\n\n  // Handle WSDL files - return as plain text\n  if (file.name.endsWith('.wsdl') || file.type === 'text/xml' || file.type === 'application/xml') {\n    return text;\n  }\n\n  try {\n    if (file.type === 'application/json' || file.name.endsWith('.json')) {\n      return JSON.parse(text);\n    }\n\n    const parsed = jsyaml.load(text);\n    if (typeof parsed !== 'object' || parsed === null) {\n      throw new Error();\n    }\n    return parsed;\n  } catch {\n    throw new Error('Failed to parse the file – ensure it is valid JSON or YAML');\n  }\n};\n\nconst FileTab = ({\n  setIsLoading,\n  handleSubmit,\n  setErrorMessage\n}) => {\n  const [dragActive, setDragActive] = useState(false);\n  const fileInputRef = useRef(null);\n  const { theme } = useTheme();\n\n  const acceptedFileTypes = [\n    '.json',\n    '.yaml',\n    '.yml',\n    '.wsdl',\n    '.zip',\n    'application/json',\n    'application/yaml',\n    'application/x-yaml',\n    'application/zip',\n    'application/x-zip-compressed',\n    'text/xml',\n    'application/xml'\n  ];\n\n  const handleDrag = (e) => {\n    e.preventDefault();\n    e.stopPropagation();\n\n    if (e.dataTransfer) {\n      e.dataTransfer.dropEffect = 'copy';\n    }\n\n    if (e.type === 'dragenter' || e.type === 'dragover') {\n      setDragActive(true);\n    } else if (e.type === 'dragleave') {\n      setDragActive(false);\n    }\n  };\n\n  const processZipFile = async (zipFile) => {\n    setIsLoading(true);\n    try {\n      const filePath = window.ipcRenderer.getFilePath(zipFile);\n      const isBrunoZip = await window.ipcRenderer.invoke('renderer:is-bruno-collection-zip', filePath);\n\n      if (isBrunoZip) {\n        const collectionName = zipFile.name.replace(/\\.zip$/i, '');\n        await handleSubmit({ rawData: { zipFilePath: filePath, collectionName }, type: 'bruno-zip' });\n        return;\n      }\n\n      toastError(new Error('The ZIP file is not a valid Bruno collection'));\n    } catch (err) {\n      toastError(err, 'Import ZIP file failed');\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const handleMultipleFiles = async (fileArray) => {\n    setIsLoading(true);\n    try {\n      const filesData = [];\n\n      // Parse all files\n      for (const file of fileArray) {\n        try {\n          const data = await convertFileToObject(file);\n\n          // Determine type for each file\n          let type = null;\n          if (isOpenApiSpec(data)) {\n            type = 'openapi';\n          } else if (isWSDLCollection(data)) {\n            type = 'wsdl';\n          } else if (isPostmanCollection(data)) {\n            type = 'postman';\n          } else if (isInsomniaCollection(data)) {\n            type = 'insomnia';\n          } else if (isOpenCollection(data)) {\n            type = 'opencollection';\n          } else if (isBrunoCollection(data)) {\n            type = 'bruno';\n          }\n\n          if (type) {\n            filesData.push({ file, data, type });\n          }\n        } catch (err) {\n          console.warn(`Failed to process file ${file.name}:`, err);\n        }\n      }\n\n      if (filesData.length > 0) {\n        // Pass raw filesData to be processed in BulkImportCollectionLocation\n        handleSubmit({ filesData, type: 'multiple' });\n      } else {\n        throw new Error('No valid collections found in the selected files');\n      }\n    } catch (err) {\n      toastError(err, 'Import multiple files failed');\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const processFile = async (file) => {\n    setIsLoading(true);\n    try {\n      const data = await convertFileToObject(file);\n\n      if (!data) {\n        throw new Error('Failed to parse file content');\n      }\n\n      let type = null;\n\n      if (isOpenApiSpec(data)) {\n        type = 'openapi';\n      } else if (isWSDLCollection(data)) {\n        type = 'wsdl';\n      } else if (isPostmanCollection(data)) {\n        type = 'postman';\n      } else if (isInsomniaCollection(data)) {\n        type = 'insomnia';\n      } else if (isOpenCollection(data)) {\n        type = 'opencollection';\n      } else if (isBrunoCollection(data)) {\n        type = 'bruno';\n      } else {\n        throw new Error('Unsupported collection format');\n      }\n\n      if (type === 'openapi') {\n        const filePath = window.ipcRenderer.getFilePath(file);\n        const rawContent = await file.text();\n        await handleSubmit({ rawData: data, type, filePath, rawContent });\n      } else {\n        await handleSubmit({ rawData: data, type });\n      }\n    } catch (err) {\n      toastError(err, 'Import collection failed');\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  const processFiles = async (files) => {\n    setErrorMessage('');\n\n    const fileArray = Array.from(files);\n    const zipFiles = fileArray.filter((file) => file.name.endsWith('.zip'));\n\n    // If both ZIP and non-ZIP files are selected, show error\n    if (zipFiles.length && (fileArray.length - zipFiles.length > 0)) {\n      setErrorMessage('Cannot mix ZIP files with other file types. Please select either a single ZIP file OR collection files (JSON/YAML)');\n      return;\n    }\n\n    if (zipFiles.length > 1) {\n      setErrorMessage('Multiple ZIP files selected. Please select only one ZIP file at a time for import.');\n      return;\n    }\n\n    if (zipFiles.length) {\n      await processZipFile(zipFiles[0]);\n      return;\n    }\n\n    if (fileArray.length > 1) {\n      // Process multiple non-ZIP files normally\n      await handleMultipleFiles(fileArray);\n    } else if (fileArray.length === 1) {\n      await processFile(fileArray[0]);\n    }\n  };\n\n  const handleDrop = async (e) => {\n    e.preventDefault();\n    e.stopPropagation();\n    setDragActive(false);\n\n    if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {\n      await processFiles(e.dataTransfer.files);\n    }\n  };\n\n  const handleBrowseFiles = () => {\n    setErrorMessage('');\n    fileInputRef.current.click();\n  };\n\n  const handleFileInputChange = async (e) => {\n    if (e.target.files && e.target.files.length > 0) {\n      await processFiles(e.target.files);\n      e.target.value = '';\n    }\n  };\n\n  return (\n    <div className=\"mb-4\">\n      <div\n        onDragEnter={handleDrag}\n        onDragOver={handleDrag}\n        onDragLeave={handleDrag}\n        onDrop={handleDrop}\n        className={`\n          border-2 border-dashed rounded-lg p-6 transition-colors duration-200\n          ${dragActive\n      ? 'border-blue-500 bg-blue-50 dark:border-blue-400 dark:bg-blue-900/20'\n      : 'border-gray-200 dark:border-gray-700'\n    }\n        `}\n      >\n        <div className=\"flex flex-col items-center justify-center\">\n          <IconFileImport\n            size={28}\n            className=\"text-gray-400 dark:text-gray-500 mb-3\"\n          />\n          <input\n            ref={fileInputRef}\n            type=\"file\"\n            className=\"hidden\"\n            multiple\n            onChange={handleFileInputChange}\n            accept={acceptedFileTypes.join(',')}\n          />\n          <p className=\"text-sm text-gray-600 dark:text-gray-300 mb-2\">\n            Drop file(s) to import or{' '}\n            <button\n              className=\"underline cursor-pointer\"\n              onClick={handleBrowseFiles}\n              style={{ color: theme.textLink }}\n            >\n              choose file(s)\n            </button>\n          </p>\n          <p className=\"text-xs text-gray-500 dark:text-gray-400 text-center\">\n            Supports Bruno, OpenCollection, Postman, Insomnia, OpenAPI v3, WSDL, and ZIP formats\n          </p>\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default FileTab;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/ImportCollection/FullscreenLoader/index.js",
    "content": "import { useState, useEffect } from 'react';\nimport { IconLoader2 } from '@tabler/icons';\n\n// Messages to cycle through while loading\nconst loadingMessages = [\n  'Processing collection...',\n  'Analyzing requests...',\n  'Translating scripts...',\n  'Preparing collection...',\n  'Almost done...'\n];\n\nconst FullscreenLoader = ({ isLoading }) => {\n  const [loadingMessage, setLoadingMessage] = useState('');\n\n  useEffect(() => {\n    if (!isLoading) return;\n\n    let messageIndex = 0;\n    const interval = setInterval(() => {\n      messageIndex = (messageIndex + 1) % loadingMessages.length;\n      setLoadingMessage(loadingMessages[messageIndex]);\n    }, 2000);\n\n    setLoadingMessage(loadingMessages[0]);\n\n    return () => clearInterval(interval);\n  }, [isLoading]);\n\n  return (\n    <div className=\"fixed inset-0 z-50 flex items-center justify-center bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm transition-all duration-300\">\n      <div className=\"flex flex-col items-center p-8 rounded-lg bg-white dark:bg-zinc-800 shadow-lg max-w-md text-center\">\n        <IconLoader2 className=\"animate-spin h-12 w-12 mb-4\" strokeWidth={1.5} />\n        <h3 className=\"text-lg font-medium text-zinc-900 dark:text-zinc-50 mb-2\">{loadingMessage}</h3>\n        <p className=\"text-zinc-500 dark:text-zinc-400\">\n          This may take a moment depending on the collection size\n        </p>\n      </div>\n    </div>\n  );\n};\n\nexport default FullscreenLoader;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/ImportCollection/GitHubTab.js",
    "content": "import React, { useState } from 'react';\nimport { isGitRepositoryUrl } from 'utils/git';\nimport toast from 'react-hot-toast';\nimport Button from 'ui/Button';\nconst GitHubTab = ({\n  handleSubmit,\n  setErrorMessage\n}) => {\n  const [urlInput, setUrlInput] = useState('');\n\n  const handleGitRepositoryImport = (url) => {\n    if (!isGitRepositoryUrl(url)) {\n      setErrorMessage('Please enter a valid git repository URL');\n      return;\n    }\n    handleSubmit({ repositoryUrl: url, type: 'git-repository' });\n  };\n\n  const handleFormSubmit = (e) => {\n    e.preventDefault();\n    if (urlInput.trim()) {\n      handleGitRepositoryImport(urlInput.trim());\n    }\n  };\n\n  return (\n    <form onSubmit={handleFormSubmit}>\n      <div className=\"flex gap-2\">\n        <input\n          id=\"gitUrlInput\"\n          data-testid=\"git-url-input\"\n          type=\"text\"\n          value={urlInput}\n          autoFocus\n          onChange={(e) => setUrlInput(e.target.value)}\n          placeholder=\"Enter Git repository URL\"\n          className=\"flex-1 px-3 py-1 textbox\"\n        />\n        <Button\n          type=\"submit\"\n          id=\"clone-git-button\"\n          disabled={!urlInput.trim()}\n          variant=\"filled\"\n          color=\"primary\"\n          style={{ height: '100%' }}\n        >\n          Clone\n        </Button>\n      </div>\n    </form>\n  );\n};\n\nexport default GitHubTab;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/ImportCollection/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .tabs {\n    .tab {\n      padding: 6px 0px;\n      border: none;\n      border-bottom: solid 2px transparent;\n      margin-right: 1.25rem;\n      color: var(--color-tab-inactive);\n      cursor: pointer;\n\n      &:focus,\n      &:active,\n      &:focus-within,\n      &:focus-visible,\n      &:target {\n        outline: none !important;\n        box-shadow: none !important;\n      }\n\n      &.active {\n        color: ${(props) => props.theme.tabs.active.color} !important;\n        border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/ImportCollection/UrlTab.js",
    "content": "import React, { useState } from 'react';\nimport { fetchAndValidateApiSpecFromUrl } from 'utils/importers/common';\nimport { isValidUrl } from 'utils/url/index';\nimport Button from 'ui/Button';\nconst UrlTab = ({\n  setIsLoading,\n  handleSubmit,\n  setErrorMessage\n}) => {\n  const [urlInput, setUrlInput] = useState('');\n\n  const handleUrlImport = async (event) => {\n    event.preventDefault();\n    if (!urlInput.trim() || !isValidUrl(urlInput.trim())) {\n      setErrorMessage('Please enter a valid URL');\n      return;\n    }\n    setIsLoading(true);\n    try {\n      const { data, specType, rawContent } = await fetchAndValidateApiSpecFromUrl({ url: urlInput.trim() });\n      // Pass raw data for all types, include sourceUrl and rawContent for OpenAPI sync\n      handleSubmit({ rawData: data, type: specType, sourceUrl: urlInput.trim(), rawContent });\n    } catch (err) {\n      console.error(err);\n      setErrorMessage('URL import failed. Please check the URL and try again.');\n    } finally {\n      setIsLoading(false);\n    }\n  };\n\n  return (\n    <form onSubmit={handleUrlImport}>\n      <div className=\"flex gap-2\">\n        <input\n          id=\"urlInput\"\n          data-testid=\"url-input\"\n          type=\"text\"\n          value={urlInput}\n          autoFocus\n          onChange={(e) => {\n            setUrlInput(e.target.value);\n            setErrorMessage('');\n          }}\n          placeholder=\"Enter URL (OpenAPI/Swagger, Postman, or Insomnia specification)\"\n          className=\"flex-1 px-3 py-1 textbox\"\n        />\n        <Button\n          type=\"submit\"\n          id=\"import-url-button\"\n          disabled={!urlInput.trim()}\n          variant=\"filled\"\n          color=\"primary\"\n          style={{ height: '100%' }}\n        >\n          Import\n        </Button>\n      </div>\n    </form>\n  );\n};\n\nexport default UrlTab;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/ImportCollection/index.js",
    "content": "import React, { useState } from 'react';\nimport { IconFileImport, IconBrandGit, IconUnlink, IconX } from '@tabler/icons';\nimport Modal from 'components/Modal';\nimport classnames from 'classnames';\nimport StyledWrapper from './StyledWrapper';\nimport FileTab from './FileTab';\nimport GitHubTab from './GitHubTab';\nimport UrlTab from './UrlTab';\nimport FullscreenLoader from './FullscreenLoader/index';\nimport { useTheme } from 'providers/Theme';\n\nconst IMPORT_TABS = {\n  FILE: 'file',\n  GITHUB: 'github',\n  URL: 'url'\n};\n\nconst ImportCollection = ({ onClose, handleSubmit }) => {\n  const { theme } = useTheme();\n  const [isLoading, setIsLoading] = useState(false);\n  const [errorMessage, setErrorMessage] = useState('');\n  const [tab, setTab] = useState(IMPORT_TABS.FILE);\n\n  const handleTabSelect = (value) => () => {\n    setTab(value);\n    setErrorMessage('');\n  };\n\n  const getTabClassname = (tabName) => {\n    return classnames(`flex tab items-center py-2 px-4 ${tabName}`, {\n      active: tabName === tab\n    });\n  };\n\n  if (isLoading) {\n    return <FullscreenLoader isLoading={isLoading} />;\n  }\n\n  return (\n    <Modal size=\"md\" title=\"Import Collection\" hideFooter={true} handleCancel={onClose} dataTestId=\"import-collection-modal\">\n      <StyledWrapper className=\"flex flex-col h-full w-[600px] max-w-[600px]\">\n        <div className=\"flex w-full mb-6\">\n          <div className=\"flex justify-start w-full tabs\">\n            <div\n              className={getTabClassname(IMPORT_TABS.FILE)}\n              onClick={handleTabSelect(IMPORT_TABS.FILE)}\n              data-testid=\"file-tab\"\n            >\n              <IconFileImport size={18} strokeWidth={1.5} className=\"mr-2\" />\n              File\n            </div>\n            <div\n              className={getTabClassname(IMPORT_TABS.GITHUB)}\n              onClick={handleTabSelect(IMPORT_TABS.GITHUB)}\n              data-testid=\"github-tab\"\n            >\n              <IconBrandGit size={18} strokeWidth={1.5} className=\"mr-2\" />\n              Git Repository\n            </div>\n            <div\n              className={getTabClassname(IMPORT_TABS.URL)}\n              onClick={handleTabSelect(IMPORT_TABS.URL)}\n              data-testid=\"url-tab\"\n            >\n              <IconUnlink size={18} strokeWidth={1.5} className=\"mr-2\" />\n              URL\n            </div>\n          </div>\n        </div>\n\n        {errorMessage && (\n          <div\n            className=\"mb-4 p-2 border rounded-md\"\n            style={{\n              backgroundColor: theme.status.danger.background,\n              borderColor: theme.status.danger.border\n            }}\n          >\n            <div className=\"flex gap-2\">\n              <div\n                className=\"text-xs flex-1\"\n                style={{ color: theme.status.danger.text }}\n              >\n                {errorMessage}\n              </div>\n              <div\n                className=\"close-button flex items-center cursor-pointer\"\n                onClick={() => setErrorMessage('')}\n                style={{ color: theme.status.danger.text }}\n              >\n                <IconX size={16} strokeWidth={1.5} />\n              </div>\n            </div>\n          </div>\n        )}\n\n        {tab === IMPORT_TABS.FILE && (\n          <FileTab\n            setIsLoading={setIsLoading}\n            handleSubmit={handleSubmit}\n            setErrorMessage={setErrorMessage}\n          />\n        )}\n        {tab === IMPORT_TABS.GITHUB && (\n          <GitHubTab\n            handleSubmit={handleSubmit}\n            setErrorMessage={setErrorMessage}\n          />\n        )}\n        {tab === IMPORT_TABS.URL && (\n          <UrlTab\n            setIsLoading={setIsLoading}\n            handleSubmit={handleSubmit}\n            setErrorMessage={setErrorMessage}\n          />\n        )}\n      </StyledWrapper>\n    </Modal>\n  );\n};\n\nexport default ImportCollection;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { darken, rgba } from 'polished';\n\nconst Wrapper = styled.div`\n  .current-group {\n    background-color: ${(props) => props.theme.background.surface1};\n    border-radius: 4px;\n    padding: 0.3rem 0.6rem;\n    cursor: pointer;\n    border: 1px solid ${(props) => props.theme.background.surface2};\n  }\n\n  .current-group:hover {\n    background-color: ${(props) => darken(0.03, props.theme.background.surface1)};\n    border-color: ${(props) => darken(0.03, props.theme.background.surface2)};\n\n  /* Fix dropdown positioning */\n  [data-tippy-root] {\n    left: 0 !important;\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/ImportCollectionLocation/index.js",
    "content": "import React, { useRef, useEffect, useState, forwardRef } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { useFormik } from 'formik';\nimport * as Yup from 'yup';\nimport get from 'lodash/get';\nimport path from 'utils/common/path';\nimport { IconCaretDown } from '@tabler/icons';\nimport { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';\nimport { postmanToBruno } from 'utils/importers/postman-collection';\nimport { convertInsomniaToBruno } from 'utils/importers/insomnia-collection';\nimport { convertOpenapiToBruno } from 'utils/importers/openapi-collection';\nimport { processBrunoCollection } from 'utils/importers/bruno-collection';\nimport { processOpenCollection } from 'utils/importers/opencollection';\nimport { wsdlToBruno } from '@usebruno/converters';\nimport { toastError } from 'utils/common/error';\nimport { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';\nimport Modal from 'components/Modal';\nimport Help from 'components/Help';\nimport Dropdown from 'components/Dropdown';\nimport StyledWrapper from './StyledWrapper';\nimport { DEFAULT_COLLECTION_FORMAT } from 'utils/common/constants';\n\n// Extract collection name from raw data\nconst getCollectionName = (format, rawData) => {\n  if (!rawData) return 'Collection';\n\n  switch (format) {\n    case 'openapi':\n      return rawData.info?.title || 'OpenAPI Collection';\n    case 'postman':\n      return rawData.info?.name || rawData.collection?.info?.name || 'Postman Collection';\n    case 'insomnia':\n      // For Insomnia v4 format, name is in the workspace resource\n      if (rawData.resources && Array.isArray(rawData.resources)) {\n        const workspace = rawData.resources.find((r) => r._type === 'workspace');\n        if (workspace?.name) {\n          return workspace.name;\n        }\n      }\n      // Fallback to root name property\n      return rawData.name || 'Insomnia Collection';\n    case 'bruno':\n      return rawData.name || 'Bruno Collection';\n    case 'opencollection':\n      return rawData.info?.name || 'OpenCollection';\n    case 'wsdl':\n      return 'WSDL Collection';\n    case 'bruno-zip':\n      return rawData.collectionName || 'Bruno Collection';\n    default:\n      return 'Collection';\n  }\n};\n\n// Convert raw data to Bruno collection format\nconst convertCollection = async (format, rawData, groupingType, collectionFormat) => {\n  try {\n    let collection;\n\n    switch (format) {\n      case 'openapi':\n        collection = convertOpenapiToBruno(rawData, { groupBy: groupingType, collectionFormat });\n        break;\n      case 'wsdl':\n        collection = await wsdlToBruno(rawData);\n        break;\n      case 'postman':\n        collection = await postmanToBruno(rawData);\n        break;\n      case 'insomnia':\n        collection = convertInsomniaToBruno(rawData);\n        break;\n      case 'bruno':\n        collection = await processBrunoCollection(rawData);\n        break;\n      case 'opencollection':\n        collection = await processOpenCollection(rawData);\n        break;\n      case 'bruno-zip':\n        // ZIP doesn't need conversion\n        collection = rawData;\n        break;\n      default:\n        throw new Error('Unknown collection format');\n    }\n\n    return collection;\n  } catch (err) {\n    console.error('Conversion error:', err);\n    toastError(err, 'Failed to convert collection');\n    throw err;\n  }\n};\n\nconst groupingOptions = [\n  { value: 'tags', label: 'Tags', description: 'Group requests by OpenAPI tags', testId: 'grouping-option-tags' },\n  { value: 'path', label: 'Paths', description: 'Group requests by URL path structure', testId: 'grouping-option-path' }\n];\n\nconst ImportCollectionLocation = ({ onClose, handleSubmit, rawData, format, sourceUrl, filePath, rawContent }) => {\n  const inputRef = useRef();\n  const dispatch = useDispatch();\n  const [groupingType, setGroupingType] = useState('tags');\n  const [collectionFormat, setCollectionFormat] = useState(DEFAULT_COLLECTION_FORMAT);\n  const isOpenAPISyncEnabled = useBetaFeature(BETA_FEATURES.OPENAPI_SYNC);\n  const [enableCheckForSpecUpdates, setEnableCheckForSpecUpdates] = useState(isOpenAPISyncEnabled);\n  const dropdownTippyRef = useRef();\n  const isOpenApi = format === 'openapi';\n  const isZipImport = format === 'bruno-zip';\n  const isOpenApiFromUrl = isOpenApi && !!sourceUrl && !filePath;\n  const isOpenApiFromFile = isOpenApi && !!filePath && !sourceUrl;\n  const showCheckForSpecUpdatesOption = isOpenAPISyncEnabled && (isOpenApiFromUrl || isOpenApiFromFile);\n\n  const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);\n  const preferences = useSelector((state) => state.app.preferences);\n  const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);\n  const isDefaultWorkspace = !activeWorkspace || activeWorkspace.type === 'default';\n\n  const defaultLocation = isDefaultWorkspace\n    ? get(preferences, 'general.defaultLocation', '')\n    : (activeWorkspace?.pathname ? path.join(activeWorkspace.pathname, 'collections') : '');\n\n  const collectionName = getCollectionName(format, rawData);\n\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: {\n      collectionLocation: defaultLocation\n    },\n    validationSchema: Yup.object({\n      collectionLocation: Yup.string()\n        .min(1, 'must be at least 1 character')\n        .max(500, 'must be 500 characters or less')\n        .required('Location is required')\n    }),\n    onSubmit: async (values) => {\n      const convertedCollection = await convertCollection(format, rawData, groupingType, collectionFormat);\n      const options = { format: collectionFormat };\n\n      if (showCheckForSpecUpdatesOption && enableCheckForSpecUpdates) {\n        const syncSourceUrl = sourceUrl || filePath; // URL or absolute path (backend converts to relative)\n        const baseBrunoConfig = {\n          version: convertedCollection.version || '1',\n          name: convertedCollection.name || 'Untitled Collection',\n          type: 'collection',\n          ignore: ['node_modules', '.git']\n        };\n\n        convertedCollection.brunoConfig = {\n          ...baseBrunoConfig,\n          ...convertedCollection.brunoConfig,\n          openapi: [\n            {\n              sourceUrl: syncSourceUrl,\n              groupBy: groupingType,\n              autoCheck: true,\n              autoCheckInterval: 5\n            }\n          ]\n        };\n\n        options.rawOpenAPISpec = rawContent || rawData;\n      }\n\n      handleSubmit(convertedCollection, values.collectionLocation, options);\n    }\n  });\n\n  const onDropdownCreate = (ref) => {\n    dropdownTippyRef.current = ref;\n  };\n\n  const GroupingDropdownIcon = forwardRef((props, ref) => {\n    const selectedOption = groupingOptions.find((option) => option.value === groupingType);\n    return (\n      <div ref={ref} className=\"flex items-center justify-between w-full current-group\" data-testid=\"grouping-dropdown\">\n        <div>\n          <div className=\"font-medium text-gray-900 dark:text-gray-100\">{selectedOption.label}</div>\n        </div>\n        <IconCaretDown size={16} className=\"text-gray-400 ml-[0.25rem]\" fill=\"currentColor\" />\n      </div>\n    );\n  });\n  const browse = () => {\n    dispatch(browseDirectory())\n      .then((dirPath) => {\n        if (typeof dirPath === 'string' && dirPath.length > 0) {\n          formik.setFieldValue('collectionLocation', dirPath);\n        }\n      })\n      .catch((error) => {\n        formik.setFieldValue('collectionLocation', '');\n        console.error(error);\n      });\n  };\n\n  useEffect(() => {\n    if (inputRef && inputRef.current) {\n      inputRef.current.focus();\n    }\n  }, [inputRef]);\n\n  const onSubmit = async () => {\n    if (isZipImport) {\n      const errors = await formik.validateForm();\n      if (Object.keys(errors).length > 0) {\n        formik.setTouched({ collectionLocation: true });\n        return;\n      }\n      const collectionLocation = formik.values.collectionLocation;\n      handleSubmit(rawData, collectionLocation, { format: collectionFormat, isZipImport: true });\n    } else {\n      formik.handleSubmit();\n    }\n  };\n\n  return (\n    <StyledWrapper>\n      <Modal\n        size=\"md\"\n        title=\"Import Collection\"\n        confirmText=\"Import\"\n        handleConfirm={onSubmit}\n        handleCancel={onClose}\n        dataTestId=\"import-collection-location-modal\"\n      >\n        <form className=\"bruno-form\" onSubmit={(e) => e.preventDefault()}>\n          <div>\n            <label htmlFor=\"collectionName\" className=\"block font-medium\">\n              Name\n            </label>\n            <div className=\"mt-2\">{collectionName}</div>\n\n            <>\n              <label htmlFor=\"collectionLocation\" className=\"font-medium mt-4 flex items-center\">\n                Location\n                <Help>\n                  <p>Bruno stores your collections on your computer's filesystem.</p>\n                  <p className=\"mt-2\">Choose the location where you want to store this collection.</p>\n                </Help>\n              </label>\n              <input\n                id=\"collection-location\"\n                type=\"text\"\n                name=\"collectionLocation\"\n                className=\"block textbox mt-2 w-full cursor-pointer\"\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n                value={formik.values.collectionLocation || ''}\n                onClick={browse}\n                onChange={(e) => {\n                  formik.setFieldValue('collectionLocation', e.target.value);\n                }}\n              />\n            </>\n            {formik.touched.collectionLocation && formik.errors.collectionLocation ? (\n              <div className=\"text-red-500\">{formik.errors.collectionLocation}</div>\n            ) : null}\n\n            <div className=\"mt-1\">\n              <span className=\"text-link cursor-pointer hover:underline\" onClick={browse}>\n                Browse\n              </span>\n            </div>\n\n            {!isZipImport && (\n              <div className=\"mt-4\">\n                <label htmlFor=\"format\" className=\"flex items-center font-medium\">\n                  File Format\n                  <Help width=\"300\">\n                    <p>Choose the file format for storing requests in this collection.</p>\n                    <p className=\"mt-2\">\n                      <strong>OpenCollection (YAML):</strong> Industry-standard YAML format (.yml files)\n                    </p>\n                    <p className=\"mt-1\">\n                      <strong>BRU:</strong> Bruno's native file format (.bru files)\n                    </p>\n                  </Help>\n                </label>\n                <select\n                  id=\"format\"\n                  name=\"format\"\n                  className=\"block textbox mt-2 w-full\"\n                  value={collectionFormat}\n                  onChange={(e) => setCollectionFormat(e.target.value)}\n                >\n                  <option value=\"yml\">OpenCollection (YAML)</option>\n                  <option value=\"bru\">BRU Format (.bru)</option>\n                </select>\n              </div>\n            )}\n          </div>\n\n          {isOpenApi && (\n            <div className=\"mt-4 flex gap-4 items-center justify-between\">\n              <div>\n                <label htmlFor=\"groupingType\" className=\"block font-medium\">\n                  Folder arrangement\n                </label>\n                <p className=\"text-muted text-xs mt-1 mb-2\">\n                  Select whether to create folders according to the spec's paths or tags.\n                </p>\n              </div>\n              <div className=\"relative\">\n                <Dropdown onCreate={onDropdownCreate} icon={<GroupingDropdownIcon />} placement=\"bottom-start\">\n                  {groupingOptions.map((option) => (\n                    <div\n                      key={option.value}\n                      className=\"dropdown-item\"\n                      data-testid={option.testId}\n                      onClick={() => {\n                        dropdownTippyRef?.current?.hide();\n                        setGroupingType(option.value);\n                      }}\n                    >\n                      {option.label}\n                    </div>\n                  ))}\n                </Dropdown>\n              </div>\n            </div>\n          )}\n\n          {showCheckForSpecUpdatesOption && (\n            <div className=\"mt-4\">\n              <label className=\"flex items-center gap-2 cursor-pointer\">\n                <input\n                  type=\"checkbox\"\n                  checked={enableCheckForSpecUpdates}\n                  onChange={(e) => setEnableCheckForSpecUpdates(e.target.checked)}\n                  className=\"cursor-pointer checkbox\"\n                />\n                <span className=\"font-medium\">Check for Spec Updates</span>\n              </label>\n              <p className=\"text-muted text-xs mt-1\">\n                Stay notified of spec changes and sync your collection with the spec.\n              </p>\n            </div>\n          )}\n        </form>\n      </Modal>\n    </StyledWrapper>\n  );\n};\n\nexport default ImportCollectionLocation;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/NewFolder/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .advanced-options {\n    .caret {\n      color: ${(props) => props.theme.textLink};\n      fill: ${(props) => props.theme.textLink};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/NewFolder/index.js",
    "content": "import React, { useRef, useEffect, useState, forwardRef } from 'react';\nimport { useFormik } from 'formik';\nimport toast from 'react-hot-toast';\nimport * as Yup from 'yup';\nimport Portal from 'components/Portal';\nimport Modal from 'components/Modal';\nimport { useDispatch } from 'react-redux';\nimport { newFolder } from 'providers/ReduxStore/slices/collections/actions';\nimport { IconArrowBackUp, IconEdit } from '@tabler/icons';\nimport { sanitizeName, validateName, validateNameError } from 'utils/common/regex';\nimport PathDisplay from 'components/PathDisplay/index';\nimport Help from 'components/Help';\nimport Dropdown from 'components/Dropdown';\nimport { IconCaretDown } from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\nimport Button from 'ui/Button';\n\nconst NewFolder = ({ collectionUid, item, onClose }) => {\n  const dispatch = useDispatch();\n  const inputRef = useRef();\n  const [isEditing, toggleEditing] = useState(false);\n  const [showFilesystemName, toggleShowFilesystemName] = useState(false);\n\n  const dropdownTippyRef = useRef();\n  const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);\n\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: {\n      folderName: '',\n      directoryName: ''\n    },\n    validationSchema: Yup.object({\n      folderName: Yup.string()\n        .trim()\n        .min(1, 'must be at least 1 character')\n        .required('name is required'),\n      directoryName: Yup.string()\n        .trim()\n        .min(1, 'must be at least 1 character')\n        .required('foldername is required')\n        .test('is-valid-folder-name', function (value) {\n          const isValid = validateName(value);\n          return isValid ? true : this.createError({ message: validateNameError(value) });\n        })\n        .test({\n          name: 'folderName',\n          message: 'The folder name \"environments\" at the root of the collection is reserved in bruno',\n          test: (value) => {\n            if (item?.uid) return true;\n            return value && !value.trim().toLowerCase().includes('environments');\n          }\n        })\n    }),\n    onSubmit: (values) => {\n      dispatch(newFolder(values.folderName, values.directoryName, collectionUid, item ? item.uid : null))\n        .then(() => {\n          toast.success('New folder created!');\n          onClose();\n        })\n        .catch((err) => toast.error(err ? err.message : 'An error occurred while adding the folder'));\n    }\n  });\n\n  useEffect(() => {\n    if (inputRef && inputRef.current) {\n      inputRef.current.focus();\n    }\n  }, [inputRef]);\n\n  const AdvancedOptions = forwardRef((props, ref) => {\n    return (\n      <div ref={ref} className=\"flex mr-2 text-link cursor-pointer items-center\">\n        <button\n          className=\"btn-advanced\"\n          type=\"button\"\n        >\n          Options\n        </button>\n        <IconCaretDown className=\"caret ml-1\" size={14} strokeWidth={2} />\n      </div>\n    );\n  });\n\n  return (\n    <Portal>\n      <StyledWrapper>\n        <Modal size=\"md\" title=\"New Folder\" hideFooter={true} handleCancel={onClose}>\n          <form className=\"bruno-form\" onSubmit={formik.handleSubmit}>\n            <label htmlFor=\"folderName\" className=\"block font-medium\">\n              Folder Name\n            </label>\n            <input\n              id=\"folder-name\"\n              type=\"text\"\n              name=\"folderName\"\n              ref={inputRef}\n              className=\"block textbox mt-2 w-full\"\n              autoComplete=\"off\"\n              autoCorrect=\"off\"\n              autoCapitalize=\"off\"\n              spellCheck=\"false\"\n              onChange={(e) => {\n                formik.setFieldValue('folderName', e.target.value);\n                !isEditing && formik.setFieldValue('directoryName', sanitizeName(e.target.value));\n              }}\n              value={formik.values.folderName || ''}\n            />\n            {formik.touched.folderName && formik.errors.folderName ? (\n              <div className=\"text-red-500\">{formik.errors.folderName}</div>\n            ) : null}\n\n            {showFilesystemName && (\n              <div className=\"mt-4\">\n                <div className=\"flex items-center justify-between\">\n                  <label htmlFor=\"directoryName\" className=\"flex items-center font-medium\">\n                    Folder Name <small className=\"font-normal text-muted ml-1\">(on filesystem)</small>\n                    <Help width=\"300\">\n                      <p>\n                        You can choose to save the folder as a different name on your file system versus what is displayed in the app.\n                      </p>\n                    </Help>\n                  </label>\n                  {isEditing ? (\n                    <IconArrowBackUp\n                      className=\"cursor-pointer opacity-50 hover:opacity-80\"\n                      size={16}\n                      strokeWidth={1.5}\n                      onClick={() => toggleEditing(false)}\n                    />\n                  ) : (\n                    <IconEdit\n                      className=\"cursor-pointer opacity-50 hover:opacity-80\"\n                      size={16}\n                      strokeWidth={1.5}\n                      onClick={() => toggleEditing(true)}\n                    />\n                  )}\n                </div>\n                {isEditing ? (\n                  <div className=\"relative flex flex-row gap-1 items-center justify-between\">\n                    <input\n                      id=\"file-name\"\n                      type=\"text\"\n                      name=\"directoryName\"\n                      placeholder=\"Folder Name\"\n                      className=\"block textbox mt-2 w-full\"\n                      autoComplete=\"off\"\n                      autoCorrect=\"off\"\n                      autoCapitalize=\"off\"\n                      spellCheck=\"false\"\n                      onChange={formik.handleChange}\n                      value={formik.values.directoryName || ''}\n                    />\n                  </div>\n                ) : (\n                  <div className=\"relative flex flex-row gap-1 items-center justify-between\">\n                    <PathDisplay\n                      iconType=\"folder\"\n                      baseName={formik.values.directoryName}\n                    />\n                  </div>\n                )}\n                {formik.touched.directoryName && formik.errors.directoryName ? (\n                  <div className=\"text-red-500\">{formik.errors.directoryName}</div>\n                ) : null}\n              </div>\n            )}\n            <div className=\"flex justify-between items-center mt-8 bruno-modal-footer\">\n              <div className=\"flex advanced-options\">\n                <Dropdown onCreate={onDropdownCreate} icon={<AdvancedOptions />} placement=\"bottom-start\">\n                  <div\n                    className=\"dropdown-item\"\n                    key=\"show-filesystem-name\"\n                    onClick={(e) => {\n                      dropdownTippyRef.current.hide();\n                      toggleShowFilesystemName(!showFilesystemName);\n                    }}\n                  >\n                    {showFilesystemName ? 'Hide Filesystem Name' : 'Show Filesystem Name'}\n                  </div>\n                </Dropdown>\n              </div>\n              <div className=\"flex justify-end\">\n                <Button type=\"button\" color=\"secondary\" variant=\"ghost\" onClick={onClose} className=\"mr-2\">\n                  Cancel\n                </Button>\n                <Button type=\"submit\">\n                  Create\n                </Button>\n              </div>\n            </div>\n          </form>\n        </Modal>\n      </StyledWrapper>\n    </Portal>\n  );\n};\n\nexport default NewFolder;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/NewRequest/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  div.method-selector-container {\n    border: solid 1px ${(props) => props.theme.input.border};\n    border-right: none;\n    background-color: ${(props) => props.theme.input.bg};\n    border-top-left-radius: ${(props) => props.theme.border.radius.base};\n    border-bottom-left-radius: ${(props) => props.theme.border.radius.base};\n  }\n  div.method-selector-container,\n  div.input-container {\n    background-color: ${(props) => props.theme.input.bg};\n    height: 2.1rem;\n  }\n  div.input-container {\n    border: solid 1px ${(props) => props.theme.input.border};\n    border-top-right-radius: ${(props) => props.theme.border.radius.base};\n    border-bottom-right-radius: ${(props) => props.theme.border.radius.base};\n    input {\n      background-color: ${(props) => props.theme.input.bg};\n      outline: none;\n      box-shadow: none;\n      &:focus {\n        outline: none !important;\n        box-shadow: none !important;\n      }\n    }\n  }\n\n  .textbox {\n    border-radius: ${(props) => props.theme.border.radius.base} !important;\n    height: 2.1rem;\n  }\n\n  textarea.curl-command {\n    min-height: 150px;\n  }\n  .dropdown {\n    width: fit-content;\n\n    .dropdown-item {\n      padding: 0.2rem 0.6rem !important;\n    }\n  }\n\n  .advanced-options {\n    .caret {\n      color: ${(props) => props.theme.textLink};\n      fill: ${(props) => props.theme.textLink};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/NewRequest/index.js",
    "content": "import React, { useRef, useEffect, useCallback, forwardRef, useState } from 'react';\nimport get from 'lodash/get';\nimport { useFormik } from 'formik';\nimport * as Yup from 'yup';\nimport toast from 'react-hot-toast';\nimport path from 'utils/common/path';\nimport { uuid } from 'utils/common';\nimport Modal from 'components/Modal';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { newEphemeralHttpRequest } from 'providers/ReduxStore/slices/collections';\nimport { newHttpRequest, newGrpcRequest, newWsRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport { addTab } from 'providers/ReduxStore/slices/tabs';\nimport HttpMethodSelector from 'components/RequestPane/QueryUrl/HttpMethodSelector';\nimport { getDefaultRequestPaneTab } from 'utils/collections';\nimport { getRequestFromCurlCommand } from 'utils/curl';\nimport { IconArrowBackUp, IconCaretDown, IconEdit } from '@tabler/icons';\nimport { sanitizeName, validateName, validateNameError } from 'utils/common/regex';\nimport Dropdown from 'components/Dropdown';\nimport PathDisplay from 'components/PathDisplay';\nimport Portal from 'components/Portal';\nimport Help from 'components/Help';\nimport StyledWrapper from './StyledWrapper';\nimport SingleLineEditor from 'components/SingleLineEditor/index';\nimport { useTheme } from 'styled-components';\nimport Button from 'ui/Button';\n\nconst NewRequest = ({ collectionUid, item, isEphemeral, onClose }) => {\n  const dispatch = useDispatch();\n  const inputRef = useRef();\n\n  const storedTheme = useTheme();\n\n  const collection = useSelector((state) => state.collections.collections?.find((c) => c.uid === collectionUid));\n  const collectionPresets = get(\n    collection,\n    collection?.draft?.brunoConfig ? 'draft.brunoConfig.presets' : 'brunoConfig.presets',\n    {}\n  );\n  const [curlRequestTypeDetected, setCurlRequestTypeDetected] = useState(null);\n  const [showFilesystemName, toggleShowFilesystemName] = useState(false);\n\n  const dropdownTippyRef = useRef();\n  const onDropdownCreate = (ref) => (dropdownTippyRef.current = ref);\n\n  const advancedDropdownTippyRef = useRef();\n  const onAdvancedDropdownCreate = (ref) => (advancedDropdownTippyRef.current = ref);\n\n  const Icon = forwardRef((props, ref) => {\n    return (\n      <div ref={ref} className=\"flex items-center justify-end auth-type-label select-none\">\n        {curlRequestTypeDetected === 'http-request' ? 'HTTP' : 'GraphQL'}\n        <IconCaretDown className=\"caret ml-1 mr-1\" size={14} strokeWidth={2} />\n      </div>\n    );\n  });\n\n  // This function analyzes a given cURL command string and determines whether the request is a GraphQL or HTTP request.\n  const identifyCurlRequestType = (url, headers, body) => {\n    if (url.endsWith('/graphql')) {\n      setCurlRequestTypeDetected('graphql-request');\n      return;\n    }\n\n    const contentType = headers?.find((h) => h.name.toLowerCase() === 'content-type')?.value;\n    if (contentType && contentType.includes('application/graphql')) {\n      setCurlRequestTypeDetected('graphql-request');\n      return;\n    }\n\n    setCurlRequestTypeDetected('http-request');\n  };\n\n  const curlRequestTypeChange = (type) => {\n    setCurlRequestTypeDetected(type);\n  };\n\n  const [isEditing, toggleEditing] = useState(false);\n\n  const getRequestType = (collectionPresets) => {\n    if (!collectionPresets || !collectionPresets.requestType) {\n      return 'http-request';\n    }\n\n    // Note: Why different labels for the same thing?\n    // http-request and graphql-request are used inside the app's json representation of a request\n    // http and graphql are used in Bru DSL as well as collection exports\n    // We need to eventually standardize the app's DSL to use the same labels as bru DSL\n    if (collectionPresets.requestType === 'http') {\n      return 'http-request';\n    }\n\n    if (collectionPresets.requestType === 'graphql') {\n      return 'graphql-request';\n    }\n\n    if (collectionPresets.requestType === 'grpc') {\n      return 'grpc-request';\n    }\n\n    if (collectionPresets.requestType === 'ws') {\n      return 'ws-request';\n    }\n\n    return 'http-request';\n  };\n\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: {\n      requestName: '',\n      filename: '',\n      requestType: getRequestType(collectionPresets),\n      requestUrl: collectionPresets.requestUrl || '',\n      requestMethod: 'GET',\n      curlCommand: ''\n    },\n    validationSchema: Yup.object({\n      requestName: Yup.string()\n        .trim()\n        .min(1, 'must be at least 1 character')\n        .max(255, 'must be 255 characters or less')\n        .required('name is required'),\n      filename: Yup.string()\n        .trim()\n        .min(1, 'must be at least 1 character')\n        .max(255, 'must be 255 characters or less')\n        .required('filename is required')\n        .test('is-valid-filename', function (value) {\n          const isValid = validateName(value);\n          return isValid ? true : this.createError({ message: validateNameError(value) });\n        })\n        .test(\n          'not-reserved',\n          `The file names \"collection\" and \"folder\" are reserved in bruno`,\n          (value) => !['collection', 'folder'].includes(value)\n        ),\n      curlCommand: Yup.string().when('requestType', {\n        is: (requestType) => requestType === 'from-curl',\n        then: Yup.string()\n          .min(1, 'must be at least 1 character')\n          .required('curlCommand is required')\n          .test({\n            name: 'curlCommand',\n            message: `Invalid cURL Command`,\n            test: (value) => getRequestFromCurlCommand(value) !== null\n          })\n      })\n    }),\n    onSubmit: (values) => {\n      const isGrpcRequest = values.requestType === 'grpc-request';\n      const isWsRequest = values.requestType === 'ws-request';\n      const filename = values.filename;\n\n      if (isGrpcRequest) {\n        dispatch(\n          newGrpcRequest({\n            requestName: values.requestName,\n            filename: filename,\n            requestType: values.requestType,\n            requestUrl: values.requestUrl,\n            collectionUid: collection.uid,\n            itemUid: item ? item.uid : null\n          })\n        )\n          .then(() => {\n            toast.success('New request created!');\n            onClose();\n          })\n          .catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));\n\n        // will need to handle import from grpcurl command when we support it, now it is just for creating new requests\n      } else if (isWsRequest) {\n        dispatch(newWsRequest({\n          requestName: values.requestName,\n          requestMethod: values.requestMethod,\n          filename: filename,\n          requestType: values.requestType,\n          requestUrl: values.requestUrl,\n          collectionUid: collection.uid,\n          itemUid: item ? item.uid : null\n        }))\n          .then(() => {\n            toast.success('New request created!');\n            onClose();\n          })\n          .catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));\n      } else if (isEphemeral) {\n        const uid = uuid();\n        dispatch(\n          newEphemeralHttpRequest({\n            uid: uid,\n            requestName: values.requestName,\n            filename: filename,\n            requestType: values.requestType,\n            requestUrl: values.requestUrl,\n            requestMethod: values.requestMethod,\n            collectionUid: collectionUid\n          })\n        )\n          .then(() => {\n            dispatch(\n              addTab({\n                uid: uid,\n                collectionUid: collectionUid,\n                requestPaneTab: getDefaultRequestPaneTab({ type: values.requestType })\n              })\n            );\n            onClose();\n          })\n          .catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));\n      } else if (values.requestType === 'from-curl') {\n        const request = getRequestFromCurlCommand(values.curlCommand, curlRequestTypeDetected);\n        const settings = { encodeUrl: false };\n\n        dispatch(\n          newHttpRequest({\n            requestName: values.requestName,\n            filename: filename,\n            requestType: curlRequestTypeDetected,\n            requestUrl: request.url,\n            requestMethod: request.method,\n            collectionUid: collectionUid,\n            itemUid: item ? item.uid : null,\n            headers: request.headers,\n            body: request.body,\n            auth: request.auth,\n            settings: settings\n          })\n        )\n          .then(() => {\n            toast.success('New request created!');\n            onClose();\n          })\n          .catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));\n      } else {\n        dispatch(\n          newHttpRequest({\n            requestName: values.requestName,\n            filename: filename,\n            requestType: values.requestType,\n            requestUrl: values.requestUrl,\n            requestMethod: values.requestMethod,\n            collectionUid: collectionUid,\n            itemUid: item ? item.uid : null\n          })\n        )\n          .then(() => {\n            toast.success('New request created!');\n            onClose();\n          })\n          .catch((err) => toast.error(err ? err.message : 'An error occurred while adding the request'));\n      }\n    }\n  });\n\n  useEffect(() => {\n    if (inputRef && inputRef.current) {\n      inputRef.current.focus();\n    }\n  }, [inputRef]);\n\n  const onSubmit = () => formik.handleSubmit();\n\n  const handlePaste = useCallback(\n    (event) => {\n      const clipboardData = event.clipboardData || window.clipboardData;\n      const pastedData = clipboardData.getData('Text');\n\n      // Check if pasted data looks like a cURL command\n      const curlCommandRegex = /^\\s*curl\\s/i;\n      if (curlCommandRegex.test(pastedData)) {\n        // Switch to the 'from-curl' request type\n        formik.setFieldValue('requestType', 'from-curl');\n        formik.setFieldValue('curlCommand', pastedData);\n\n        // Identify the request type\n        const request = getRequestFromCurlCommand(pastedData);\n        if (request) {\n          identifyCurlRequestType(request.url, request.headers, request.body);\n        }\n\n        // Prevent the default paste behavior to avoid pasting into the textarea\n        event.preventDefault();\n      }\n    },\n    [formik]\n  );\n\n  const handleCurlCommandChange = (event) => {\n    formik.handleChange(event);\n\n    if (event.target.name === 'curlCommand') {\n      const curlCommand = event.target.value;\n      const request = getRequestFromCurlCommand(curlCommand);\n      if (request) {\n        identifyCurlRequestType(request.url, request.headers, request.body);\n      }\n    }\n  };\n\n  const AdvancedOptions = forwardRef((props, ref) => {\n    return (\n      <div ref={ref} className=\"flex mr-2 text-link cursor-pointer items-center\">\n        <button className=\"btn-advanced\" type=\"button\">\n          Options\n        </button>\n        <IconCaretDown className=\"caret ml-1\" size={14} strokeWidth={2} />\n      </div>\n    );\n  });\n\n  return (\n    <Portal>\n      <StyledWrapper>\n        <Modal size=\"md\" title=\"New Request\" hideFooter handleCancel={onClose}>\n          <form\n            className=\"bruno-form\"\n            onSubmit={formik.handleSubmit}\n            onKeyDown={(e) => {\n              if (e.key === 'Enter') {\n                e.preventDefault();\n                formik.handleSubmit();\n              }\n            }}\n          >\n            <div>\n              <label htmlFor=\"requestName\" className=\"block font-medium\">\n                Type\n              </label>\n\n              <div className=\"mt-2 grid grid-cols-3 gap-2\">\n                <div className=\"flex flex-col gap-2\">\n                  <div className=\"flex items-center gap-2\">\n                    <input\n                      type=\"radio\"\n                      id=\"http-request\"\n                      name=\"requestType\"\n                      value=\"http-request\"\n                      checked={formik.values.requestType === 'http-request'}\n                      onChange={formik.handleChange}\n                      data-testid=\"http-request\"\n                    />\n                    <label htmlFor=\"http-request\" className=\"ml-1 cursor-pointer select-none\">\n                      HTTP\n                    </label>\n                  </div>\n                  <div className=\"flex items-center gap-2\">\n                    <input\n                      type=\"radio\"\n                      id=\"graphql-request\"\n                      name=\"requestType\"\n                      value=\"graphql-request\"\n                      checked={formik.values.requestType === 'graphql-request'}\n                      onChange={formik.handleChange}\n                      data-testid=\"graphql-request\"\n                    />\n                    <label htmlFor=\"graphql-request\" className=\"ml-1 cursor-pointer select-none\">\n                      GraphQL\n                    </label>\n                  </div>\n                </div>\n\n                <div className=\"flex flex-col gap-2\">\n                  <div className=\"flex items-center gap-2\">\n                    <input\n                      type=\"radio\"\n                      id=\"grpc-request\"\n                      name=\"requestType\"\n                      value=\"grpc-request\"\n                      checked={formik.values.requestType === 'grpc-request'}\n                      onChange={formik.handleChange}\n                      data-testid=\"grpc-request\"\n                    />\n                    <label htmlFor=\"grpc-request\" className=\"ml-1 cursor-pointer select-none\">\n                      gRPC\n                    </label>\n                  </div>\n\n                  <div className=\"flex items-center gap-2\">\n                    <input\n                      type=\"radio\"\n                      id=\"ws-request\"\n                      name=\"requestType\"\n                      value=\"ws-request\"\n                      checked={formik.values.requestType === 'ws-request'}\n                      onChange={formik.handleChange}\n                      data-testid=\"ws-request\"\n                    />\n                    <label htmlFor=\"ws-request\" className=\"ml-1 cursor-pointer select-none\">\n                      WebSocket\n                    </label>\n                  </div>\n                </div>\n\n                <div className=\"flex flex-col gap-2\">\n                  <div className=\"flex items-center gap-2\">\n                    <input\n                      type=\"radio\"\n                      id=\"from-curl\"\n                      name=\"requestType\"\n                      value=\"from-curl\"\n                      checked={formik.values.requestType === 'from-curl'}\n                      onChange={formik.handleChange}\n                      data-testid=\"from-curl\"\n                    />\n                    <label htmlFor=\"from-curl\" className=\"ml-1 cursor-pointer select-none\">\n                      From cURL\n                    </label>\n                  </div>\n                </div>\n              </div>\n            </div>\n            <div className=\"mt-4\">\n              <label htmlFor=\"requestName\" className=\"block font-medium\">\n                Request Name\n              </label>\n              <input\n                id=\"request-name\"\n                type=\"text\"\n                name=\"requestName\"\n                placeholder=\"Request Name\"\n                ref={inputRef}\n                className=\"block textbox mt-2 w-full\"\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n                onChange={(e) => {\n                  formik.setFieldValue('requestName', e.target.value);\n                  !isEditing && formik.setFieldValue('filename', sanitizeName(e.target.value));\n                }}\n                value={formik.values.requestName || ''}\n                data-testid=\"request-name\"\n              />\n              {formik.touched.requestName && formik.errors.requestName ? (\n                <div className=\"text-red-500\">{formik.errors.requestName}</div>\n              ) : null}\n            </div>\n            {showFilesystemName && (\n              <div className=\"mt-4\">\n                <div className=\"flex items-center justify-between\">\n                  <label htmlFor=\"filename\" className=\"flex items-center font-medium\">\n                    File Name <small className=\"font-normal text-muted ml-1\">(on filesystem)</small>\n                    <Help width=\"300\">\n                      <p>Bruno saves each request as a file in your collection's folder.</p>\n                      <p className=\"mt-2\">\n                        You can choose a file name different from your request's name or one compatible with filesystem\n                        rules.\n                      </p>\n                    </Help>\n                  </label>\n                  {isEditing ? (\n                    <IconArrowBackUp\n                      className=\"cursor-pointer opacity-50 hover:opacity-80\"\n                      size={16}\n                      strokeWidth={1.5}\n                      onClick={() => toggleEditing(false)}\n                    />\n                  ) : (\n                    <IconEdit\n                      className=\"cursor-pointer opacity-50 hover:opacity-80\"\n                      size={16}\n                      strokeWidth={1.5}\n                      onClick={() => toggleEditing(true)}\n                    />\n                  )}\n                </div>\n                {isEditing ? (\n                  <div className=\"relative flex flex-row gap-1 items-center justify-between\">\n                    <input\n                      id=\"file-name\"\n                      type=\"text\"\n                      name=\"filename\"\n                      placeholder=\"File Name\"\n                      className=\"!pr-10 block textbox mt-2 w-full\"\n                      autoComplete=\"off\"\n                      autoCorrect=\"off\"\n                      autoCapitalize=\"off\"\n                      spellCheck=\"false\"\n                      onChange={formik.handleChange}\n                      value={formik.values.filename || ''}\n                      data-testid=\"file-name\"\n                    />\n                    <span className=\"absolute right-2 top-4 flex justify-center items-center file-extension\">.{collection.format}</span>\n                  </div>\n                ) : (\n                  <div className=\"relative flex flex-row gap-1 items-center justify-between\">\n                    <PathDisplay\n                      baseName={formik.values.filename ? `${formik.values.filename}.${collection.format}` : ''}\n                    />\n                  </div>\n                )}\n                {formik.touched.filename && formik.errors.filename ? (\n                  <div className=\"text-red-500\">{formik.errors.filename}</div>\n                ) : null}\n              </div>\n            )}\n            {formik.values.requestType !== 'from-curl' ? (\n              <>\n                <div className=\"mt-4\">\n                  <label htmlFor=\"request-url\" className=\"block font-medium\">\n                    URL\n                  </label>\n                  <div className=\"flex items-center mt-2 \">\n                    {!['grpc-request', 'ws-request'].includes(formik.values.requestType) ? (\n                      <div className=\"flex items-center h-full method-selector-container\">\n                        <HttpMethodSelector\n                          method={formik.values.requestMethod}\n                          onMethodSelect={(val) => formik.setFieldValue('requestMethod', val)}\n                          showCaret\n                        />\n                      </div>\n                    ) : null}\n                    <div\n                      id=\"new-request-url\"\n                      data-testid=\"new-request-url\"\n                      className=\"flex px-2 items-center flex-grow input-container h-full min-w-0\"\n                    >\n                      <SingleLineEditor\n                        onPaste={handlePaste}\n                        placeholder=\"Request URL\"\n                        value={formik.values.requestUrl || ''}\n                        theme={storedTheme}\n                        onChange={(value) => {\n                          formik.handleChange({\n                            target: {\n                              name: 'requestUrl',\n                              value: value\n                            }\n                          });\n                        }}\n                        collection={collection}\n                        variablesAutocomplete={true}\n                      />\n                    </div>\n                  </div>\n                  {formik.touched.requestUrl && formik.errors.requestUrl ? (\n                    <div className=\"text-red-500\">{formik.errors.requestUrl}</div>\n                  ) : null}\n                </div>\n              </>\n            ) : (\n              <div className=\"mt-4\">\n                <div className=\"flex justify-between\">\n                  <label htmlFor=\"request-url\" className=\"block font-medium\">\n                    cURL Command\n                  </label>\n                  <Dropdown className=\"dropdown\" onCreate={onDropdownCreate} icon={<Icon />} placement=\"bottom-end\">\n                    <div\n                      className=\"dropdown-item\"\n                      onClick={() => {\n                        dropdownTippyRef.current.hide();\n                        curlRequestTypeChange('http-request');\n                      }}\n                    >\n                      HTTP\n                    </div>\n                    <div\n                      className=\"dropdown-item\"\n                      onClick={() => {\n                        dropdownTippyRef.current.hide();\n                        curlRequestTypeChange('graphql-request');\n                      }}\n                    >\n                      GraphQL\n                    </div>\n                  </Dropdown>\n                </div>\n                <textarea\n                  name=\"curlCommand\"\n                  placeholder=\"Enter cURL request here..\"\n                  className=\"block textbox w-full mt-4 curl-command\"\n                  value={formik.values.curlCommand}\n                  onChange={handleCurlCommandChange}\n                  data-testid=\"curl-command\"\n                >\n                </textarea>\n                {formik.touched.curlCommand && formik.errors.curlCommand ? (\n                  <div className=\"text-red-500\">{formik.errors.curlCommand}</div>\n                ) : null}\n              </div>\n            )}\n            <div className=\"flex justify-between items-center mt-8 bruno-modal-footer\">\n              <div className=\"flex advanced-options\">\n                <Dropdown onCreate={onAdvancedDropdownCreate} icon={<AdvancedOptions />} placement=\"bottom-start\">\n                  <div\n                    className=\"dropdown-item\"\n                    key=\"show-filesystem-name\"\n                    onClick={(e) => {\n                      advancedDropdownTippyRef.current.hide();\n                      toggleShowFilesystemName(!showFilesystemName);\n                    }}\n                  >\n                    {showFilesystemName ? 'Hide Filesystem Name' : 'Show Filesystem Name'}\n                  </div>\n                </Dropdown>\n              </div>\n              <div className=\"flex justify-end\">\n                <Button type=\"button\" color=\"secondary\" variant=\"ghost\" onClick={onClose} className=\"mr-2\">\n                  Cancel\n                </Button>\n                <Button type=\"submit\">\n                  Create\n                </Button>\n              </div>\n            </div>\n          </form>\n        </Modal>\n      </StyledWrapper>\n    </Portal>\n  );\n};\n\nexport default NewRequest;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Sections/ApiSpecsSection/index.js",
    "content": "import { useState } from 'react';\nimport toast from 'react-hot-toast';\nimport { useDispatch } from 'react-redux';\nimport { IconFileCode, IconPlus } from '@tabler/icons';\n\nimport { openApiSpec } from 'providers/ReduxStore/slices/apiSpec';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport ActionIcon from 'ui/ActionIcon';\nimport CreateApiSpec from 'components/Sidebar/ApiSpecs/CreateApiSpec';\nimport ApiSpecs from 'components/Sidebar/ApiSpecs';\nimport SidebarSection from 'components/Sidebar/SidebarSection';\n\nconst ApiSpecsSection = () => {\n  const dispatch = useDispatch();\n  const [createApiSpecModalOpen, setCreateApiSpecModalOpen] = useState(false);\n\n  const handleOpenApiSpec = () => {\n    dispatch(openApiSpec()).catch((err) => {\n      console.error(err);\n      toast.error('An error occurred while opening the API spec');\n    });\n  };\n\n  const addDropdownItems = [\n    {\n      id: 'create-api-spec',\n      leftSection: IconPlus,\n      label: 'Create API Spec',\n      onClick: () => {\n        setCreateApiSpecModalOpen(true);\n      }\n    },\n    {\n      id: 'open-api-spec',\n      leftSection: IconFileCode,\n      label: 'Open API Spec',\n      onClick: () => {\n        handleOpenApiSpec();\n      }\n    }\n  ];\n\n  const sectionActions = (\n    <>\n      <MenuDropdown\n        data-testid=\"api-specs-header-add-menu\"\n        items={addDropdownItems}\n        placement=\"bottom-end\"\n      >\n        <ActionIcon\n          label=\"Add new API Spec\"\n        >\n          <IconPlus size={14} stroke={1.5} aria-hidden=\"true\" />\n        </ActionIcon>\n      </MenuDropdown>\n    </>\n  );\n\n  return (\n    <>\n      {createApiSpecModalOpen && (\n        <CreateApiSpec\n          onClose={() => setCreateApiSpecModalOpen(false)}\n        />\n      )}\n      <SidebarSection\n        id=\"api-specs\"\n        title=\"API Specs\"\n        icon={IconFileCode}\n        actions={sectionActions}\n        className=\"api-specs-section\"\n      >\n        <ApiSpecs />\n      </SidebarSection>\n    </>\n  );\n};\n\nexport default ApiSpecsSection;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/Sections/CollectionsSection/index.js",
    "content": "import { useState, useMemo } from 'react';\nimport toast from 'react-hot-toast';\nimport get from 'lodash/get';\nimport { useDispatch, useSelector } from 'react-redux';\nimport {\n  IconArrowsSort,\n  IconDotsVertical,\n  IconDownload,\n  IconFolder,\n  IconPlus,\n  IconSearch,\n  IconSortAscendingLetters,\n  IconSortDescendingLetters,\n  IconSquareX,\n  IconBox,\n  IconTerminal2\n} from '@tabler/icons';\n\nimport { importCollection, openCollection, importCollectionFromZip, newHttpRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport { sortCollections } from 'providers/ReduxStore/slices/collections/index';\nimport { savePreferences, setIsCreatingCollection } from 'providers/ReduxStore/slices/app';\nimport { normalizePath } from 'utils/common/path';\nimport { isScratchCollection, flattenItems, isItemTransientRequest } from 'utils/collections';\nimport { sanitizeName } from 'utils/common/regex';\nimport filter from 'lodash/filter';\n\nimport MenuDropdown from 'ui/MenuDropdown';\nimport ActionIcon from 'ui/ActionIcon';\nimport ImportCollection from 'components/Sidebar/ImportCollection';\nimport ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation';\nimport BulkImportCollectionLocation from 'components/Sidebar/BulkImportCollectionLocation';\nimport CloneGitRepository from 'components/Sidebar/CloneGitRespository';\nimport RemoveCollectionsModal from 'components/Sidebar/Collections/RemoveCollectionsModal/index';\nimport CreateCollection from 'components/Sidebar/CreateCollection';\nimport WelcomeModal from 'components/WelcomeModal';\nimport Collections from 'components/Sidebar/Collections';\nimport SidebarSection from 'components/Sidebar/SidebarSection';\nimport { openDevtoolsAndSwitchToTerminal } from 'utils/terminal';\n\nconst CollectionsSection = () => {\n  const [showSearch, setShowSearch] = useState(false);\n  const dispatch = useDispatch();\n\n  const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);\n  const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);\n\n  const { collections } = useSelector((state) => state.collections);\n  const { collectionSortOrder } = useSelector((state) => state.collections);\n  const { isCreatingCollection } = useSelector((state) => state.app);\n  const preferences = useSelector((state) => state.app.preferences);\n  const [collectionsToClose, setCollectionsToClose] = useState([]);\n\n  const [importData, setImportData] = useState(null);\n  const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);\n  const [advancedCreateName, setAdvancedCreateName] = useState('');\n  const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);\n  const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);\n  const [showCloneGitModal, setShowCloneGitModal] = useState(false);\n  const [gitRepositoryUrl, setGitRepositoryUrl] = useState(null);\n\n  // Default to true (don't show modal) so that:\n  // 1. Existing users who upgrade (no hasSeenWelcomeModal in their prefs) don't see it\n  // 2. The modal doesn't flash before preferences are loaded from the electron process\n  // Only genuinely new users will have hasSeenWelcomeModal explicitly set to false by onboarding\n  const hasSeenWelcomeModal = get(preferences, 'onboarding.hasSeenWelcomeModal', true);\n  const showWelcomeModal = !hasSeenWelcomeModal;\n\n  const handleDismissWelcomeModal = () => {\n    const updatedPreferences = {\n      ...preferences,\n      onboarding: {\n        ...preferences.onboarding,\n        hasSeenWelcomeModal: true\n      }\n    };\n    dispatch(savePreferences(updatedPreferences)).catch(() => {\n      toast.error('Failed to save preferences');\n    });\n  };\n\n  const workspaceCollections = useMemo(() => {\n    if (!activeWorkspace) return [];\n\n    return collections.filter((c) => {\n      if (isScratchCollection(c, workspaces)) {\n        return false;\n      }\n      return activeWorkspace.collections?.some((wc) => normalizePath(wc.path) === normalizePath(c.pathname));\n    });\n  }, [activeWorkspace, collections, workspaces]);\n\n  const handleImportCollection = ({ rawData, type, repositoryUrl, ...rest }) => {\n    setImportCollectionModalOpen(false);\n\n    if (type === 'git-repository') {\n      setGitRepositoryUrl(repositoryUrl);\n      setShowCloneGitModal(true);\n      return;\n    }\n\n    setImportData({ rawData, type, ...rest });\n    setImportCollectionLocationModalOpen(true);\n  };\n\n  const handleImportCollectionLocation = (convertedCollection, collectionLocation, options = {}) => {\n    const importAction = options.isZipImport\n      ? importCollectionFromZip(convertedCollection.zipFilePath, collectionLocation)\n      : importCollection(convertedCollection, collectionLocation, options);\n\n    dispatch(importAction)\n      .then(() => {\n        setImportCollectionLocationModalOpen(false);\n        setImportData(null);\n      });\n  };\n\n  const handleCloseGitModal = () => {\n    setShowCloneGitModal(false);\n    setGitRepositoryUrl(null);\n  };\n\n  const handleToggleSearch = () => {\n    setShowSearch((prev) => !prev);\n  };\n\n  const handleSortCollections = () => {\n    let order;\n    switch (collectionSortOrder) {\n      case 'default':\n        order = 'alphabetical';\n        break;\n      case 'alphabetical':\n        order = 'reverseAlphabetical';\n        break;\n      case 'reverseAlphabetical':\n        order = 'default';\n        break;\n      default:\n        order = 'default';\n        break;\n    }\n    dispatch(sortCollections({ order }));\n  };\n\n  const getSortIcon = () => {\n    switch (collectionSortOrder) {\n      case 'alphabetical':\n        return IconSortDescendingLetters;\n      case 'reverseAlphabetical':\n        return IconArrowsSort;\n      default:\n        return IconSortAscendingLetters;\n    }\n  };\n\n  const getSortLabel = () => {\n    switch (collectionSortOrder) {\n      case 'alphabetical':\n        return 'Sort Z-A';\n      case 'reverseAlphabetical':\n        return 'Clear sort';\n      default:\n        return 'Sort A-Z';\n    }\n  };\n\n  const selectAllCollectionsToClose = () => {\n    setCollectionsToClose(workspaceCollections.map((c) => c.uid));\n  };\n\n  const clearCollectionsToClose = () => {\n    setCollectionsToClose([]);\n  };\n\n  const handleOpenCollection = () => {\n    const options = {};\n    if (activeWorkspace?.pathname) {\n      options.workspaceId = activeWorkspace.pathname;\n    }\n\n    dispatch(openCollection(options)).catch((err) => {\n      toast.error('An error occurred while opening the collection');\n    });\n  };\n\n  const handleStartRequest = () => {\n    const scratchCollectionUid = activeWorkspace?.scratchCollectionUid;\n    if (!scratchCollectionUid) {\n      toast.error('Unable to create request');\n      return;\n    }\n\n    const scratchCollection = collections.find((c) => c.uid === scratchCollectionUid);\n    if (!scratchCollection) {\n      toast.error('Unable to create request');\n      return;\n    }\n\n    const allItems = flattenItems(scratchCollection.items || []);\n    const transientRequests = filter(allItems, (item) => isItemTransientRequest(item));\n    let maxNumber = 0;\n    transientRequests.forEach((item) => {\n      const match = item.name?.match(/^Untitled (\\d+)$/);\n      if (match) {\n        const number = parseInt(match[1], 10);\n        if (number > maxNumber) {\n          maxNumber = number;\n        }\n      }\n    });\n    const requestName = `Untitled ${maxNumber + 1}`;\n    const filename = sanitizeName(requestName);\n\n    dispatch(\n      newHttpRequest({\n        requestName,\n        filename,\n        requestType: 'http-request',\n        requestUrl: '',\n        requestMethod: 'GET',\n        collectionUid: scratchCollectionUid,\n        itemUid: null,\n        isTransient: true\n      })\n    ).catch((err) => {\n      toast.error('An error occurred while creating the request');\n    });\n  };\n\n  const handleOpenAdvancedCreate = (name) => {\n    dispatch(setIsCreatingCollection(false));\n    setAdvancedCreateName(name || '');\n    setCreateCollectionModalOpen(true);\n  };\n\n  const addDropdownItems = [\n    {\n      id: 'create',\n      leftSection: IconPlus,\n      label: 'Create collection',\n      onClick: () => {\n        dispatch(setIsCreatingCollection(true));\n      }\n    },\n    {\n      id: 'open',\n      leftSection: IconFolder,\n      label: 'Open collection',\n      onClick: () => {\n        handleOpenCollection();\n      }\n    },\n    {\n      id: 'import',\n      leftSection: IconDownload,\n      label: 'Import collection',\n      onClick: () => {\n        setImportCollectionModalOpen(true);\n      }\n    }\n  ];\n\n  const actionsDropdownItems = [\n    {\n      id: 'sort',\n      leftSection: getSortIcon(),\n      label: getSortLabel(),\n      onClick: () => {\n        handleSortCollections();\n      }\n    },\n    {\n      id: 'close-all',\n      leftSection: IconSquareX,\n      label: 'Close all',\n      onClick: () => {\n        selectAllCollectionsToClose();\n      }\n    },\n    {\n      id: 'open-in-terminal',\n      leftSection: IconTerminal2,\n      label: 'Open in Terminal',\n      onClick: () => {\n        openDevtoolsAndSwitchToTerminal(dispatch, activeWorkspace?.pathname);\n      }\n    }\n  ];\n\n  const sectionActions = (\n    <>\n      <ActionIcon\n        onClick={handleToggleSearch}\n        label=\"Search requests\"\n      >\n        <IconSearch size={14} stroke={1.5} aria-hidden=\"true\" />\n      </ActionIcon>\n\n      <MenuDropdown\n        data-testid=\"collections-header-add-menu\"\n        items={addDropdownItems}\n        placement=\"bottom-end\"\n      >\n        <ActionIcon\n          label=\"Add new collection\"\n        >\n          <IconPlus size={14} stroke={1.5} aria-hidden=\"true\" />\n        </ActionIcon>\n      </MenuDropdown>\n\n      <MenuDropdown\n        data-testid=\"collections-header-actions-menu\"\n        items={actionsDropdownItems}\n        placement=\"bottom-end\"\n      >\n        <ActionIcon\n          label=\"More actions\"\n        >\n          <IconDotsVertical size={14} stroke={1.5} aria-hidden=\"true\" />\n        </ActionIcon>\n      </MenuDropdown>\n\n      {collectionsToClose.length > 0 && (\n        <RemoveCollectionsModal collectionUids={collectionsToClose} onClose={clearCollectionsToClose} />\n      )}\n    </>\n  );\n\n  return (\n    <>\n      {showWelcomeModal && (\n        <WelcomeModal\n          onDismiss={handleDismissWelcomeModal}\n          onImportCollection={() => {\n            handleDismissWelcomeModal();\n            setImportCollectionModalOpen(true);\n          }}\n          onCreateCollection={() => {\n            handleDismissWelcomeModal();\n            setCreateCollectionModalOpen(true);\n          }}\n          onOpenCollection={() => {\n            handleDismissWelcomeModal();\n            handleOpenCollection();\n          }}\n          onStartRequest={() => {\n            handleDismissWelcomeModal();\n            handleStartRequest();\n          }}\n        />\n      )}\n      {createCollectionModalOpen && (\n        <CreateCollection\n          onClose={() => {\n            setCreateCollectionModalOpen(false);\n            setAdvancedCreateName('');\n          }}\n          initialCollectionName={advancedCreateName}\n        />\n      )}\n      {importCollectionModalOpen && (\n        <ImportCollection\n          onClose={() => setImportCollectionModalOpen(false)}\n          handleSubmit={handleImportCollection}\n        />\n      )}\n      {importCollectionLocationModalOpen && importData && (importData.type !== 'multiple' && importData.type !== 'bulk') && (\n        <ImportCollectionLocation\n          rawData={importData.rawData}\n          format={importData.type}\n          sourceUrl={importData.sourceUrl}\n          filePath={importData.filePath}\n          rawContent={importData.rawContent}\n          onClose={() => setImportCollectionLocationModalOpen(false)}\n          handleSubmit={handleImportCollectionLocation}\n        />\n      )}\n      {importCollectionLocationModalOpen && importData && (importData.type === 'multiple' || importData.type === 'bulk') && (\n        <BulkImportCollectionLocation\n          importData={importData}\n          onClose={() => setImportCollectionLocationModalOpen(false)}\n          handleSubmit={handleImportCollectionLocation}\n        />\n      )}\n      {showCloneGitModal && (\n        <CloneGitRepository\n          onClose={handleCloseGitModal}\n          onFinish={handleCloseGitModal}\n          collectionRepositoryUrl={gitRepositoryUrl}\n        />\n      )}\n      <SidebarSection\n        id=\"collections\"\n        title=\"Collections\"\n        icon={IconBox}\n        actions={sectionActions}\n      >\n        <Collections\n          showSearch={showSearch}\n          isCreatingCollection={isCreatingCollection}\n          onCreateClick={() => dispatch(setIsCreatingCollection(true))}\n          onDismissCreate={() => dispatch(setIsCreatingCollection(false))}\n          onOpenAdvancedCreate={handleOpenAdvancedCreate}\n        />\n      </SidebarSection>\n    </>\n  );\n};\n\nexport default CollectionsSection;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/SidebarAccordionContext.js",
    "content": "import React, { createContext, useContext, useState, useCallback, useRef } from 'react';\n\nconst SidebarAccordionContext = createContext();\n\nexport const useSidebarAccordion = () => {\n  const context = useContext(SidebarAccordionContext);\n  if (!context) {\n    throw new Error('useSidebarAccordion must be used within SidebarAccordionProvider');\n  }\n  return context;\n};\n\nexport const SidebarAccordionProvider = ({ children, defaultExpanded = ['collections'] }) => {\n  const [expandedSections, setExpandedSections] = useState(new Set(defaultExpanded));\n  const dropdownContainerRef = useRef(null);\n\n  const toggleSection = useCallback((sectionId) => {\n    setExpandedSections((prev) => {\n      const newSet = new Set(prev);\n      if (newSet.has(sectionId)) {\n        newSet.delete(sectionId);\n      } else {\n        newSet.add(sectionId);\n      }\n      return newSet;\n    });\n  }, []);\n\n  const setSectionExpanded = useCallback((sectionId, expanded) => {\n    setExpandedSections((prev) => {\n      const newSet = new Set(prev);\n      if (expanded) {\n        newSet.add(sectionId);\n      } else {\n        newSet.delete(sectionId);\n      }\n      return newSet;\n    });\n  }, []);\n\n  const isExpanded = useCallback((sectionId) => {\n    return expandedSections.has(sectionId);\n  }, [expandedSections]);\n\n  const getExpandedCount = useCallback(() => {\n    return expandedSections.size;\n  }, [expandedSections]);\n\n  return (\n    <SidebarAccordionContext.Provider\n      value={{\n        expandedSections,\n        toggleSection,\n        setSectionExpanded,\n        isExpanded,\n        getExpandedCount,\n        dropdownContainerRef\n      }}\n    >\n      <div ref={dropdownContainerRef}>\n        {children}\n      </div>\n    </SidebarAccordionContext.Provider>\n  );\n};\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/SidebarContent.js",
    "content": "import { useSidebarAccordion } from './SidebarAccordionContext';\n\n/**\n * Sections configuration\n *\n * All sections use the same generic accordion behavior with the class 'accordion-section-wrapper'.\n * Layout behavior is fully automatic based on section order and expansion state:\n * - Single expanded: When only one section is expanded, it fills available space\n * - Multi-expanded: When multiple sections are expanded, they split space equally\n * - Automatic pinning: Sections below an expanded section are automatically pinned to bottom\n *\n * To add a new section, simply add a new entry to this array:\n *\n * {\n *   id: 'my-section',                    // Unique identifier\n *   component: MySectionComponent,       // React component to render\n *   getProps: (context) => ({ ... })     // Function to get props for component\n * }\n */\n\nconst SidebarContent = ({ sections }) => {\n  const { isExpanded, getExpandedCount } = useSidebarAccordion();\n\n  const expandedCount = getExpandedCount();\n\n  const getWrapperClassName = (section, sectionIndex) => {\n    const sectionExpanded = isExpanded(section.id);\n    // Use generic accordion-section-wrapper class for all sections\n    const classes = ['accordion-section-wrapper'];\n\n    // Multi-expanded: when multiple sections are expanded\n    if (expandedCount > 1 && sectionExpanded) {\n      classes.push('multi-expanded');\n    }\n\n    // Single expanded wrapper behavior: when only one section is expanded, it fills space\n    if (sectionExpanded && expandedCount === 1) {\n      classes.push('single-expanded-wrapper');\n    }\n\n    // Automatic pinning: if section is not expanded and any section above it (earlier in array) is expanded\n    if (!sectionExpanded) {\n      // Check if any section before this one (earlier in array) is expanded\n      const hasExpandedAbove = sections.slice(0, sectionIndex).some((s) => isExpanded(s.id));\n      if (hasExpandedAbove) {\n        classes.push('pinned-to-bottom');\n      }\n    }\n\n    return classes.join(' ');\n  };\n\n  return (\n    <>\n      {sections.map((section, index) => {\n        const SectionComponent = section.component;\n        const wrapperClassName = getWrapperClassName(section, index);\n\n        return (\n          <div key={section.id} className={wrapperClassName}>\n            <SectionComponent />\n          </div>\n        );\n      })}\n    </>\n  );\n};\n\nexport default SidebarContent;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/SidebarSection/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  height: 100%;\n\n  .sidebar-section {\n    display: flex;\n    flex-direction: column;\n    min-height: 0;\n    height: 100%;\n\n    &.expanded {\n      flex: 1 1 0%;\n      min-height: 0;\n    }\n\n    &:not(.expanded) {\n      flex: 0 0 auto;\n    }\n\n    &.multi-expanded {\n      flex: 1 1 0%;\n      margin-bottom: 0;\n    }\n  }\n\n  .section-header {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    gap: 16px;\n    padding: 6px 4px 6px 8px;\n    min-height: 28px;\n    height: 28px;\n    user-select: none;\n    transition: background-color 0.15s ease;\n    flex-shrink: 0;\n    border-bottom: 1px solid transparent;\n\n    .section-header-left {\n      display: flex;\n      align-items: center;\n      gap: 6px;\n      flex: 1;\n      min-width: 0;\n      cursor: pointer;\n\n\n      &:hover {\n        .section-toggle {\n          display: flex;\n        }\n\n        .section-toggle {\n          background: ${(props) => props.theme.dropdown.hoverBg};\n          color: ${(props) => props.theme.text} !important;\n        }\n\n        .section-icon {\n            display: none;\n          }\n        }\n      }\n    }\n\n    .section-icon-wrapper {\n      width: 24px;\n      height: 24px;\n      flex-shrink: 0;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n    }\n\n    .section-toggle {\n      display: none;\n    }\n\n    .section-icon {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      width: 16px;\n      height: 16px;\n      color: ${(props) => props.theme.sidebar.muted};\n    }\n\n    .section-title {\n      color: ${(props) => props.theme.sidebar.color};\n      font-size: 12px;\n      font-weight: 600;\n      white-space: nowrap;\n      overflow: hidden;\n      text-overflow: ellipsis;\n    }\n\n    .section-actions {\n      display: flex;\n      align-items: center;\n      gap: 1px;\n      flex-shrink: 0;\n    }\n  }\n\n  .section-content {\n    display: flex;\n    flex-direction: column;\n    flex: 1 1 0%;\n    min-height: 0;\n    overflow-y: auto;\n    overflow-x: hidden;\n    position: relative;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/SidebarSection/index.js",
    "content": "import { useState, useEffect, useRef } from 'react';\nimport { IconChevronRight, IconChevronDown } from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\nimport { useSidebarAccordion } from '../SidebarAccordionContext';\nimport ActionIcon from 'ui/ActionIcon/index';\n\nconst SidebarSection = ({\n  id,\n  title,\n  icon: Icon,\n  actions,\n  children,\n  className = ''\n}) => {\n  const { isExpanded, setSectionExpanded, getExpandedCount } = useSidebarAccordion();\n  const [localExpanded, setLocalExpanded] = useState(() => isExpanded(id));\n  const sectionRef = useRef(null);\n\n  // Sync with context\n  useEffect(() => {\n    const expanded = isExpanded(id);\n    setLocalExpanded(expanded);\n  }, [id, isExpanded]);\n\n  const handleToggle = () => {\n    const newExpanded = !localExpanded;\n    setLocalExpanded(newExpanded);\n    setSectionExpanded(id, newExpanded);\n  };\n\n  const expandedCount = getExpandedCount();\n  // Check if this is the only expanded section\n  const isOnlyExpanded = expandedCount === 1 && localExpanded;\n\n  return (\n    <StyledWrapper className={className}>\n      <div\n        ref={sectionRef}\n        className={`sidebar-section ${localExpanded ? 'expanded' : ''} ${isOnlyExpanded ? 'single-expanded' : ''} ${expandedCount > 1 && localExpanded ? 'multi-expanded' : ''}`}\n      >\n        <div\n          className=\"section-header\"\n          onClick={handleToggle}\n        >\n          <div className=\"section-header-left\">\n            <div\n              className=\"section-icon-wrapper\"\n              tabIndex={0}\n              onKeyDown={(e) => {\n                if (e.key === 'Enter' || e.key === ' ') {\n                  e.preventDefault(); handleToggle();\n                }\n              }}\n            >\n              <ActionIcon size=\"sm\" className=\"section-toggle\">\n                {localExpanded ? (\n                  <IconChevronDown size={12} stroke={1.5} />\n                ) : (\n                  <IconChevronRight size={12} stroke={1.5} />\n                )}\n              </ActionIcon>\n              {Icon && <Icon size={14} stroke={1.5} className=\"section-icon\" />}\n            </div>\n            <span className=\"section-title\">{title}</span>\n          </div>\n          {actions && (\n            <div\n              className=\"section-actions\"\n              onClick={(e) => {\n                e.stopPropagation();\n                if (!localExpanded) {\n                  setSectionExpanded(id, true);\n                }\n              }}\n            >\n              {actions}\n            </div>\n          )}\n        </div>\n        {localExpanded && (\n          <div className=\"section-content\">\n            {children}\n          </div>\n        )}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default SidebarSection;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  color: ${(props) => props.theme.sidebar.color};\n  max-height: 100%;\n\n  aside {\n    background-color: ${(props) => props.theme.sidebar.bg};\n    overflow: hidden;\n\n    .sidebar-sections-container {\n      display: flex;\n      flex-direction: column;\n    }\n\n    .sidebar-sections {\n      min-height: 0;\n      display: flex;\n      flex-direction: column;\n      height: 100%;\n    }\n\n    /* Expanded sections grow to fill available space but are constrained */\n    .sidebar-section.expanded {\n      flex: 1 1 0%;\n      min-height: 0;\n\n      .section-header {\n        border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.hoverBg};\n      }\n    }\n\n    /* Single expanded section: add margin-bottom to push others down */\n    .sidebar-section.single-expanded {\n      margin-bottom: auto !important;\n      flex: 1 1 0% !important;\n      min-height: 0;\n      max-height: 100%;\n    }\n\n    /* Multiple expanded sections: equal split, no margin-bottom */\n    .sidebar-section.multi-expanded {\n      margin-bottom: 0;\n      flex: 1 1 0% !important;\n\n      min-height: 0;\n      overflow: hidden;\n      max-height: 100%;\n    }\n\n    /* Collapsed sections only take header height */\n    .sidebar-section:not(.expanded) {\n      flex: 0 0 auto;\n    }\n\n    /* Always push bottom accordions wrapper to the bottom */\n    .bottom-accordions-wrapper {\n      display: flex;\n      flex-direction: column;\n      flex: 0 0 auto;\n    }\n\n    /* Generic accordion section wrapper - applies to all accordion sections */\n    .accordion-section-wrapper {\n      display: flex;\n      flex-direction: column;\n      min-height: 0;\n      position: relative;\n      overflow: visible;\n    }\n\n    /* Add border-top to all accordion items except the first child */\n    .accordion-section-wrapper:not(:first-child) {\n      border-top: 1px solid ${(props) => props.theme.sidebar.collection.item.hoverBg};\n    }\n\n    /* When a section is single expanded, wrapper should fill space but respect pinned sections */\n    .accordion-section-wrapper.single-expanded-wrapper {\n      flex: 1 1 0% !important;\n      min-height: 0;\n      overflow: hidden;\n    }\n\n    /* Normal flow: sections not pinned and not multi-expanded */\n    .accordion-section-wrapper:not(.pinned-to-bottom):not(.multi-expanded) {\n      flex: 0 0 auto;\n    }\n\n    /* When a section is pinned to bottom */\n    .accordion-section-wrapper.pinned-to-bottom {\n      flex: 0 0 auto;\n      margin-top: auto;\n    }\n\n    /* When multiple sections are expanded, split space equally */\n    .accordion-section-wrapper.multi-expanded {\n      flex: 1 1 0% !important;\n      min-height: 0;\n      margin-top: 0 !important;\n      height: auto !important;\n    }\n\n  }\n\n  div.sidebar-drag-handle {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    height: 100%;\n    cursor: col-resize;\n    background-color: transparent;\n    width: 6px;\n    right: -3px;\n    transition: opacity 0.2s ease;\n\n    div.drag-request-border {\n      width: 1px;\n      height: 100%;\n      border-left: solid 1px ${(props) => props.theme.sidebar.dragbar.border};\n    }\n\n    &:hover div.drag-request-border {\n      width: 1px;\n      height: 100%;\n      border-left: solid 1px ${(props) => props.theme.sidebar.dragbar.activeBorder};\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Sidebar/index.js",
    "content": "import { SidebarAccordionProvider } from './SidebarAccordionContext';\nimport SidebarContent from './SidebarContent';\nimport StyledWrapper from './StyledWrapper';\n\nimport { useState, useEffect, useRef } from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { updateLeftSidebarWidth, updateIsDragging } from 'providers/ReduxStore/slices/app';\nimport CollectionsSection from './Sections/CollectionsSection/index';\nimport ApiSpecsSection from './Sections/ApiSpecsSection/index';\n\nconst MIN_LEFT_SIDEBAR_WIDTH = 220;\nconst MAX_LEFT_SIDEBAR_WIDTH = 600;\n\nconst SIDEBAR_SECTIONS = [\n  {\n    id: 'collections',\n    component: CollectionsSection\n  },\n  {\n    id: 'api-specs',\n    component: ApiSpecsSection\n  }\n];\n\nconst Sidebar = () => {\n  const leftSidebarWidth = useSelector((state) => state.app.leftSidebarWidth);\n  const sidebarCollapsed = useSelector((state) => state.app.sidebarCollapsed);\n  const [asideWidth, setAsideWidth] = useState(leftSidebarWidth);\n  const lastWidthRef = useRef(leftSidebarWidth);\n\n  const dispatch = useDispatch();\n  const [dragging, setDragging] = useState(false);\n\n  const currentWidth = sidebarCollapsed ? 0 : asideWidth;\n\n  // Clamp helper keeps width in allowed range\n  const clamp = (value, min, max) => Math.min(max, Math.max(min, value));\n\n  const handleMouseMove = (e) => {\n    if (!dragging || sidebarCollapsed) return;\n    e.preventDefault();\n    const nextWidth = clamp(e.clientX + 2, MIN_LEFT_SIDEBAR_WIDTH, MAX_LEFT_SIDEBAR_WIDTH);\n    if (Math.abs(nextWidth - lastWidthRef.current) < 3) return;\n    lastWidthRef.current = nextWidth;\n    setAsideWidth(nextWidth);\n  };\n\n  const handleMouseUp = (e) => {\n    if (dragging) {\n      e.preventDefault();\n      setDragging(false);\n      dispatch(\n        updateLeftSidebarWidth({\n          leftSidebarWidth: asideWidth\n        })\n      );\n      dispatch(\n        updateIsDragging({\n          isDragging: false\n        })\n      );\n    }\n  };\n  const handleDragbarMouseDown = (e) => {\n    e.preventDefault();\n    if (sidebarCollapsed) {\n      return;\n    }\n    setDragging(true);\n    dispatch(\n      updateIsDragging({\n        isDragging: true\n      })\n    );\n  };\n\n  useEffect(() => {\n    document.addEventListener('mouseup', handleMouseUp);\n    document.addEventListener('mousemove', handleMouseMove);\n\n    return () => {\n      document.removeEventListener('mouseup', handleMouseUp);\n      document.removeEventListener('mousemove', handleMouseMove);\n    };\n  }, [dragging, asideWidth]);\n\n  useEffect(() => {\n    setAsideWidth(leftSidebarWidth);\n  }, [leftSidebarWidth]);\n\n  return (\n    <SidebarAccordionProvider defaultExpanded={['collections']}>\n      <StyledWrapper className=\"flex relative h-full\">\n        <aside className=\"sidebar\" style={{ width: currentWidth, transition: dragging ? 'none' : 'width 0.2s ease-in-out' }}>\n          <div className=\"flex flex-row h-full w-full\">\n            <div className=\"flex flex-col w-full\" style={{ width: asideWidth }}>\n              <div className=\"flex flex-col flex-grow sidebar-sections-container\" style={{ minHeight: 0, overflow: 'hidden' }}>\n                <div className=\"sidebar-sections flex flex-col flex-1\">\n                  <SidebarContent\n                    sections={SIDEBAR_SECTIONS}\n                  />\n                </div>\n              </div>\n            </div>\n          </div>\n        </aside>\n\n        {!sidebarCollapsed && (\n          <div className=\"absolute sidebar-drag-handle h-full\" onMouseDown={handleDragbarMouseDown}>\n            <div className=\"drag-request-border\" />\n          </div>\n        )}\n      </StyledWrapper>\n    </SidebarAccordionProvider>\n  );\n};\n\nexport default Sidebar;\n"
  },
  {
    "path": "packages/bruno-app/src/components/SingleLineEditor/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  width: 100%;\n  height: ${(props) => (props.$isCompact ? '1.375rem' : '1.875rem')};\n  overflow-y: hidden;\n  overflow-x: hidden;\n\n  &.read-only {\n    .CodeMirror-cursor {\n      display: none !important;\n    }\n  }\n\n  .CodeMirror {\n    background: transparent;\n    height: ${(props) => (props.$isCompact ? '1.375rem' : '2.125rem')};\n    font-size: ${(props) => props.theme.font.size.base};\n    line-height: ${(props) => (props.$isCompact ? '1.375rem' : '1.875rem')};\n    overflow: hidden;\n\n    .CodeMirror-scroll {\n      overflow: hidden !important;\n      padding-bottom: 3.125rem !important;\n    }\n\n    .CodeMirror-vscrollbar,\n    .CodeMirror-hscrollbar,\n    .CodeMirror-scrollbar-filler {\n      display: none;\n    }\n\n    .CodeMirror-lines {\n      padding: 0;\n\n      .CodeMirror-placeholder {\n        color: ${(props) => props.theme.codemirror.placeholder.color} !important;\n        opacity:  ${(props) => props.theme.codemirror.placeholder.opacity} !important\n      }\n    }\n\n    .CodeMirror-cursor {\n      height: ${(props) => (props.$isCompact ? '0.875rem' : '1.25rem')} !important;\n      margin-top: ${(props) => (props.$isCompact ? '0.25rem' : '0.3125rem')} !important;\n      border-left: 1px solid ${(props) => props.theme.text} !important;\n    }\n\n    pre {\n      font-family: Inter, sans-serif !important;\n      font-weight: 400;\n    }\n\n    .CodeMirror-line {\n      color: ${(props) => props.theme.text};\n      padding: 0;\n    }\n\n    .CodeMirror-selected {\n      background-color: rgba(212, 125, 59, 0.3);\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/SingleLineEditor/index.js",
    "content": "import React, { Component } from 'react';\nimport isEqual from 'lodash/isEqual';\nimport { getAllVariables } from 'utils/collections';\nimport { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';\nimport { MaskedEditor } from 'utils/common/masked-editor';\nimport { setupAutoComplete } from 'utils/codemirror/autocomplete';\nimport StyledWrapper from './StyledWrapper';\nimport { IconEye, IconEyeOff } from '@tabler/icons';\nimport { setupLinkAware } from 'utils/codemirror/linkAware';\n\nconst CodeMirror = require('codemirror');\n\nclass SingleLineEditor extends Component {\n  constructor(props) {\n    super(props);\n    // Keep a cached version of the value, this cache will be updated when the\n    // editor is updated, which can later be used to protect the editor from\n    // unnecessary updates during the update lifecycle.\n    this.cachedValue = props.value || '';\n    this.editorRef = React.createRef();\n    this.variables = {};\n    this.readOnly = props.readOnly || false;\n\n    this.state = {\n      maskInput: props.isSecret || false // Always mask the input by default (if it's a secret)\n    };\n  }\n\n  componentDidMount() {\n    // Initialize CodeMirror as a single line editor\n    /** @type {import(\"codemirror\").Editor} */\n    const variables = getAllVariables(this.props.collection, this.props.item);\n\n    const runHandler = () => {\n      if (this.props.onRun) {\n        this.props.onRun();\n      }\n    };\n    const saveHandler = () => {\n      if (this.props.onSave) {\n        this.props.onSave();\n      }\n    };\n    const noopHandler = () => { };\n\n    this.editor = CodeMirror(this.editorRef.current, {\n      placeholder: this.props.placeholder ?? '',\n      lineWrapping: false,\n      lineNumbers: false,\n      theme: this.props.theme === 'dark' ? 'monokai' : 'default',\n      mode: 'brunovariables',\n      brunoVarInfo: this.props.enableBrunoVarInfo !== false ? {\n        variables,\n        collection: this.props.collection,\n        item: this.props.item\n      } : false,\n      scrollbarStyle: null,\n      tabindex: 0,\n      readOnly: this.props.readOnly,\n      extraKeys: {\n        'Enter': runHandler,\n        'Ctrl-Enter': runHandler,\n        'Cmd-Enter': runHandler,\n        'Alt-Enter': () => {\n          if (this.props.allowNewlines) {\n            this.editor.setValue(this.editor.getValue() + '\\n');\n            this.editor.setCursor({ line: this.editor.lineCount(), ch: 0 });\n          } else if (this.props.onRun) {\n            this.props.onRun();\n          }\n        },\n        'Shift-Enter': runHandler,\n        'Cmd-S': saveHandler,\n        'Ctrl-S': saveHandler,\n        'Cmd-F': noopHandler,\n        'Ctrl-F': noopHandler,\n        // Tabbing disabled to make tabindex work\n        'Tab': false,\n        'Shift-Tab': false\n      }\n    });\n\n    const getAllVariablesHandler = () => getAllVariables(this.props.collection, this.props.item);\n    const getAnywordAutocompleteHints = () => this.props.autocomplete || [];\n\n    // Setup AutoComplete Helper\n    const autoCompleteOptions = {\n      getAllVariables: getAllVariablesHandler,\n      getAnywordAutocompleteHints,\n      showHintsFor: this.props.showHintsFor || ['variables'],\n      showHintsOnClick: this.props.showHintsOnClick\n    };\n\n    this.brunoAutoCompleteCleanup = setupAutoComplete(\n      this.editor,\n      autoCompleteOptions\n    );\n\n    this.editor.setValue(String(this.props.value ?? ''));\n    this.editor.on('change', this._onEdit);\n    this.editor.on('paste', this._onPaste);\n    this.addOverlay(variables);\n    this._enableMaskedEditor(this.props.isSecret);\n    this.setState({ maskInput: this.props.isSecret });\n\n    // Add newline arrow markers if enabled\n    if (this.props.showNewlineArrow) {\n      this._updateNewlineMarkers();\n    }\n    setupLinkAware(this.editor);\n  }\n\n  /** Enable or disable masking the rendered content of the editor */\n  _enableMaskedEditor = (enabled) => {\n    if (typeof enabled !== 'boolean') return;\n\n    if (enabled == true) {\n      if (!this.maskedEditor) this.maskedEditor = new MaskedEditor(this.editor, '*');\n      this.maskedEditor.enable();\n    } else {\n      if (this.maskedEditor) {\n        this.maskedEditor.disable();\n        this.maskedEditor.destroy();\n        this.maskedEditor = null;\n      }\n    }\n  };\n\n  _onEdit = () => {\n    if (!this.ignoreChangeEvent && this.editor) {\n      this.cachedValue = this.editor.getValue();\n      if (this.props.onChange && (this.props.value !== this.cachedValue)) {\n        this.props.onChange(this.cachedValue);\n      }\n\n      // Update newline markers after edit\n      if (this.props.showNewlineArrow) {\n        this._updateNewlineMarkers();\n      }\n    }\n  };\n\n  _onPaste = (_, event) => this.props.onPaste?.(event);\n\n  componentDidUpdate(prevProps) {\n    // Ensure the changes caused by this update are not interpreted as\n    // user-input changes which could otherwise result in an infinite\n    // event loop.\n    this.ignoreChangeEvent = true;\n\n    let variables = getAllVariables(this.props.collection, this.props.item);\n    if (!isEqual(variables, this.variables)) {\n      if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) {\n        this.editor.options.brunoVarInfo.variables = variables;\n      }\n      this.addOverlay(variables);\n    }\n\n    // Update collection and item when they change\n    if (this.props.enableBrunoVarInfo !== false && this.editor.options.brunoVarInfo) {\n      if (!isEqual(this.props.collection, this.editor.options.brunoVarInfo.collection)) {\n        this.editor.options.brunoVarInfo.collection = this.props.collection;\n      }\n      if (!isEqual(this.props.item, this.editor.options.brunoVarInfo.item)) {\n        this.editor.options.brunoVarInfo.item = this.props.item;\n      }\n    }\n    if (this.props.theme !== prevProps.theme && this.editor) {\n      this.editor.setOption('theme', this.props.theme === 'dark' ? 'monokai' : 'default');\n    }\n    if (this.props.value !== prevProps.value && this.props.value !== this.cachedValue && this.editor) {\n      // TODO: temporary fix for keeping cursor state when auto save and new line insertion collide PR#7098\n      const nextValue = String(this.props.value ?? '');\n      const currentValue = this.editor.getValue();\n      if (this.editor.hasFocus?.() && currentValue !== nextValue && nextValue !== '') {\n        this.cachedValue = currentValue;\n      } else {\n        const cursor = this.editor.getCursor();\n        this.cachedValue = nextValue;\n        this.editor.setValue(nextValue);\n        this.editor.setCursor(cursor);\n\n        // Update newline markers after value change\n        if (this.props.showNewlineArrow) {\n          this._updateNewlineMarkers();\n        }\n      }\n    }\n    if (!isEqual(this.props.isSecret, prevProps.isSecret)) {\n      // If the secret flag has changed, update the editor to reflect the change\n      this._enableMaskedEditor(this.props.isSecret);\n      // also set the maskInput flag to the new value\n      this.setState({ maskInput: this.props.isSecret });\n    }\n    if (this.props.readOnly !== prevProps.readOnly && this.editor) {\n      this.editor.setOption('readOnly', this.props.readOnly);\n    }\n    if (this.props.placeholder !== prevProps.placeholder && this.editor) {\n      this.editor.setOption('placeholder', this.props.placeholder);\n    }\n    this.ignoreChangeEvent = false;\n  }\n\n  componentWillUnmount() {\n    if (this.editor) {\n      if (this.editor?._destroyLinkAware) {\n        this.editor._destroyLinkAware();\n      }\n      this.editor.off('change', this._onEdit);\n      this.editor.off('paste', this._onPaste);\n      this._clearNewlineMarkers();\n      this.editor.getWrapperElement().remove();\n      this.editor = null;\n    }\n    if (this.brunoAutoCompleteCleanup) {\n      this.brunoAutoCompleteCleanup();\n    }\n    if (this.maskedEditor) {\n      this.maskedEditor.destroy();\n      this.maskedEditor = null;\n    }\n  }\n\n  addOverlay = (variables) => {\n    this.variables = variables;\n    defineCodeMirrorBrunoVariablesMode(variables, 'text/plain', this.props.highlightPathParams, true);\n    this.editor.setOption('mode', 'brunovariables');\n  };\n\n  /**\n   * Update markers to show arrows for newlines\n   */\n  _updateNewlineMarkers = () => {\n    if (!this.editor) return;\n\n    // Clear existing markers\n    this._clearNewlineMarkers();\n\n    this.newlineMarkers = [];\n    const content = this.editor.getValue();\n\n    // Find all newlines and replace them with arrow widgets\n    for (let i = 0; i < content.length; i++) {\n      if (content[i] === '\\n') {\n        const pos = this.editor.posFromIndex(i);\n        const nextPos = this.editor.posFromIndex(i + 1);\n\n        // Create a widget to display the arrow\n        const arrow = document.createElement('span');\n        arrow.className = 'newline-arrow';\n        arrow.textContent = '↲';\n        arrow.style.cssText = `\n          color: #888;\n          font-size: 8px;\n          margin: 0 2px;\n          vertical-align: middle;\n          display: inline-block;\n        `;\n\n        // Mark the newline character and replace it with the arrow widget\n        const marker = this.editor.markText(pos, nextPos, {\n          replacedWith: arrow,\n          handleMouseEvents: true\n        });\n\n        this.newlineMarkers.push(marker);\n      }\n    }\n  };\n\n  /**\n   * Clear all newline markers\n   */\n  _clearNewlineMarkers = () => {\n    if (this.newlineMarkers) {\n      this.newlineMarkers.forEach((marker) => {\n        try {\n          marker.clear();\n        } catch (e) {\n          // Marker might already be cleared\n        }\n      });\n      this.newlineMarkers = [];\n    }\n  };\n\n  toggleVisibleSecret = () => {\n    const isVisible = !this.state.maskInput;\n    this.setState({ maskInput: isVisible });\n    this._enableMaskedEditor(isVisible);\n  };\n\n  /**\n   * @brief Eye icon to show/hide the secret value\n   * @returns ReactComponent The eye icon\n   */\n  secretEye = (isSecret) => {\n    return isSecret === true ? (\n      <button type=\"button\" className=\"mx-2\" onClick={() => this.toggleVisibleSecret()}>\n        {this.state.maskInput === true ? (\n          <IconEyeOff size={18} strokeWidth={2} />\n        ) : (\n          <IconEye size={18} strokeWidth={2} />\n        )}\n      </button>\n    ) : null;\n  };\n\n  render() {\n    return (\n      <div className={`flex flex-row items-center w-full overflow-x-auto ${this.props.className}`}>\n        <StyledWrapper\n          ref={this.editorRef}\n          className={`single-line-editor grow ${this.props.readOnly ? 'read-only' : ''}`}\n          $isCompact={this.props.isCompact}\n          {...(this.props['data-testid'] ? { 'data-testid': this.props['data-testid'] } : {})}\n        />\n        <div className=\"flex items-center\">\n          {this.secretEye(this.props.isSecret)}\n        </div>\n      </div>\n    );\n  }\n}\nexport default SingleLineEditor;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Spinner/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .spinner {\n    display: inline-block;\n    width: 2rem;\n    height: 2rem;\n    vertical-align: text-bottom;\n    border: 0.25em solid currentColor;\n    border-right-color: transparent;\n    border-radius: 50%;\n    animation: spinner-border 0.75s linear infinite;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Spinner/index.js",
    "content": "import React from 'react';\nimport StyledWrapper from './StyledWrapper';\n\n// Todo: Size, Color config support\nconst Spinner = ({ size, color, children }) => {\n  return (\n    <StyledWrapper>\n      <div className=\"animate-spin\"></div>\n      {children && <div>{children}</div>}\n    </StyledWrapper>\n  );\n};\n\nexport default Spinner;\n"
  },
  {
    "path": "packages/bruno-app/src/components/StatusBar/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .status-bar {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 0 1rem;\n    height: 1.5rem;\n    background: ${(props) => props.theme.sidebar.bg};\n    border-top: 1px solid ${(props) => props.theme.statusBar.border};\n    color: ${(props) => props.theme.statusBar.color};\n    font-size: ${(props) => props.theme.font.size.sm};\n    user-select: none;\n    position: relative;\n  }\n\n  .status-bar-section {\n    display: flex;\n    align-items: center;\n    position: relative;\n  }\n\n  .status-bar-group {\n    display: flex;\n    align-items: center;\n    gap: 2px;\n  }\n\n  .status-bar-button {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    padding: 0 4px;\n    cursor: pointer;\n    position: relative;\n    outline: none;\n  }\n\n  .console-button-content {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    gap: 0.25rem;\n    position: relative;\n  }\n\n  .console-label {\n    white-space: nowrap;\n  }\n\n  .error-count-inline {\n    font-size: 10px;\n    font-weight: 500;\n    color: ${(props) => props.theme.colors.text.danger};\n    background: ${(props) => props.theme.colors.bg.danger}20;\n    padding: 1px 4px;\n    border-radius: 4px;\n  }\n\n  .status-bar-divider {\n    width: 1px;\n    height: 16px;\n    background: ${(props) => props.theme.sidebar.dragbar};\n    opacity: 0.4;\n  }\n\n  .status-bar-version {\n    display: flex;\n    align-items: center;\n    padding: 2px 6px;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/StatusBar/ThemeDropdown/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst StyledWrapper = styled.div`\n  /* Main container */\n  .theme-menu {\n    min-width: 200px;\n    height: 325px;\n    padding: 8px;\n    background: ${(props) => props.theme.dropdown.bg};\n    border: 1px solid ${(props) => props.theme.dropdown.border};\n    border-radius: 6px;\n    box-shadow: ${(props) => props.theme.dropdown.shadow};\n    outline: none;\n\n    &.two-columns {\n      min-width: 400px;\n    }\n  }\n\n  /* Mode section */\n  .mode-section {\n    padding: 0 8px 12px 8px;\n    margin: 0 -8px;\n    border-bottom: 1px solid ${(props) => props.theme.dropdown.separator};\n  }\n\n  .mode-label {\n    font-size: 12px;\n    color: ${(props) => props.theme.dropdown.mutedText};\n    margin-bottom: 8px;\n  }\n\n  .mode-buttons {\n    display: flex;\n    gap: 10px;\n  }\n\n  .mode-button {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 24px;\n    height: 24px;\n    padding: 8px 4px;\n    border: 1px solid ${(props) => props.theme.dropdown.separator};\n    border-radius: 4px;\n    background: transparent;\n    color: ${(props) => props.theme.dropdown.mutedText};\n    cursor: pointer;\n    transition: all 0.15s ease;\n\n    &:hover {\n      background: ${(props) => props.theme.dropdown.hoverBg};\n      color: ${(props) => props.theme.dropdown.color};\n    }\n\n    &.focused {\n      background: ${(props) => props.theme.dropdown.hoverBg};\n      color: ${(props) => props.theme.dropdown.color};\n      outline: none;\n    }\n\n    &.active {\n      background: ${(props) => rgba(props.theme.dropdown.selectedColor, 0.08)};\n      border-color: ${(props) => props.theme.dropdown.selectedColor};\n      color: ${(props) => props.theme.dropdown.selectedColor};\n\n      &.focused {\n        background: ${(props) => rgba(props.theme.dropdown.selectedColor, 0.15)};\n        outline: none;\n      }\n    }\n  }\n\n  /* Theme lists container */\n  .theme-lists {\n    display: flex;\n    gap: 24px;\n\n    &.two-columns {\n      gap: 0;\n\n      .theme-list {\n        flex: 1;\n        padding: 8px 0;\n\n        &:first-child {\n          padding-right: 12px;\n          border-right: 1px solid ${(props) => props.theme.dropdown.separator};\n        }\n\n        &:last-child {\n          padding-left: 12px;\n        }\n      }\n    }\n  }\n\n  /* Individual theme list */\n  .theme-list {\n    min-width: 180px;\n    padding-top: 8px;\n  }\n\n  .theme-list-label {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    font-size: 12px;\n    color: ${(props) => props.theme.dropdown.mutedText};\n    margin-bottom: 8px;\n  }\n\n  .active-badge {\n    font-size: 10px;\n    font-weight: 500;\n    padding: 2px 6px;\n    border-radius: 4px;\n    background: ${(props) => rgba(props.theme.dropdown.selectedColor, 0.12)};\n    color: ${(props) => props.theme.dropdown.selectedColor};\n  }\n\n  /* Theme item */\n  .theme-item {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    height: 26px;\n    padding: 4px 8px;\n    border-radius: 4px;\n    cursor: pointer;\n    outline: none;\n    color: ${(props) => props.theme.dropdown.color};\n    font-size: ${(props) => props.theme.font.size.sm};\n\n    &:hover,\n    &.focused {\n      background: ${(props) => props.theme.dropdown.hoverBg};\n    }\n\n    &.active {\n      color: ${(props) => props.theme.dropdown.selectedColor};\n      background: ${(props) => rgba(props.theme.dropdown.selectedColor, 0.08)};\n\n      &.focused {\n        background: ${(props) => rgba(props.theme.dropdown.selectedColor, 0.15)};\n      }\n    }\n  }\n\n  .theme-item-label {\n    flex: 1;\n    white-space: nowrap;\n  }\n\n  .check-icon {\n    flex-shrink: 0;\n    margin-left: 12px;\n    color: ${(props) => props.theme.dropdown.selectedColor};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/StatusBar/ThemeDropdown/index.js",
    "content": "import React, { useState, useRef, useCallback, useEffect } from 'react';\nimport Tippy from '@tippyjs/react';\nimport { IconCheck, IconSun, IconMoon, IconDeviceDesktop } from '@tabler/icons';\nimport ToolHint from 'components/ToolHint';\nimport { useTheme } from 'providers/Theme';\nimport { getLightThemes, getDarkThemes } from 'themes/index';\nimport StyledWrapper from './StyledWrapper';\n\n// Constants\nconst MODES = ['light', 'dark', 'system'];\nconst MODE_BUTTONS = [\n  { mode: 'light', icon: IconSun, title: 'Light' },\n  { mode: 'dark', icon: IconMoon, title: 'Dark' },\n  { mode: 'system', icon: IconDeviceDesktop, title: 'System' }\n];\n\nconst ThemeDropdown = ({ children }) => {\n  // Dropdown state\n  const [isOpen, setIsOpen] = useState(false);\n  const [tooltipEnabled, setTooltipEnabled] = useState(true);\n\n  // Keyboard navigation state\n  const [focusedSection, setFocusedSection] = useState('mode');\n  const [focusedIndex, setFocusedIndex] = useState(0);\n  const [isKeyboardNav, setIsKeyboardNav] = useState(false);\n\n  // Refs for focus management\n  const menuRef = useRef(null);\n  const modeButtonRefs = useRef([]);\n  const lightItemRefs = useRef([]);\n  const darkItemRefs = useRef([]);\n\n  // Theme context\n  const {\n    storedTheme,\n    setStoredTheme,\n    displayedTheme,\n    themeVariantLight,\n    themeVariantDark,\n    setThemeVariantLight,\n    setThemeVariantDark\n  } = useTheme();\n\n  // Theme data\n  const lightThemes = getLightThemes();\n  const darkThemes = getDarkThemes();\n  const isSystemMode = storedTheme === 'system';\n\n  // Helper to get class names for focusable items\n  const getFocusedClass = (section, index) => {\n    return isKeyboardNav && focusedSection === section && focusedIndex === index ? 'focused' : '';\n  };\n\n  // Handlers\n  const handleModeSelect = (mode) => setStoredTheme(mode);\n\n  const handleThemeSelect = (themeId, isLight) => {\n    if (isLight) {\n      setThemeVariantLight(themeId);\n    } else {\n      setThemeVariantDark(themeId);\n    }\n  };\n\n  const handleOpen = () => {\n    setTooltipEnabled(false);\n    setIsOpen(true);\n    setFocusedSection('mode');\n    setFocusedIndex(0);\n    setIsKeyboardNav(false);\n  };\n\n  const handleClose = () => {\n    setIsOpen(false);\n    setTimeout(() => setTooltipEnabled(true), 100);\n  };\n\n  const handleMouseEnter = (section, index) => {\n    setIsKeyboardNav(false);\n    setFocusedSection(section);\n    setFocusedIndex(index);\n  };\n\n  // Get available sections based on current mode\n  const getAvailableSections = useCallback(() => {\n    if (isSystemMode) return ['mode', 'light', 'dark'];\n    return storedTheme === 'light' ? ['mode', 'light'] : ['mode', 'dark'];\n  }, [isSystemMode, storedTheme]);\n\n  // Get max index for a section\n  const getMaxIndex = useCallback((section) => {\n    switch (section) {\n      case 'mode': return 2;\n      case 'light': return lightThemes.length - 1;\n      case 'dark': return darkThemes.length - 1;\n      default: return 0;\n    }\n  }, [lightThemes.length, darkThemes.length]);\n\n  // Get mode index for returning to mode section\n  const getModeIndex = useCallback(() => {\n    return MODES.indexOf(storedTheme);\n  }, [storedTheme]);\n\n  // Focus element based on current section and index\n  useEffect(() => {\n    if (!isOpen) return;\n\n    const timer = setTimeout(() => {\n      const refs = {\n        mode: modeButtonRefs,\n        light: lightItemRefs,\n        dark: darkItemRefs\n      };\n      refs[focusedSection]?.current[focusedIndex]?.focus();\n    }, 0);\n\n    return () => clearTimeout(timer);\n  }, [isOpen, focusedSection, focusedIndex]);\n\n  // Keyboard navigation handler\n  const handleKeyDown = useCallback((e) => {\n    if (!isOpen) return;\n\n    const sections = getAvailableSections();\n    const maxIndex = getMaxIndex(focusedSection);\n\n    const navigationHandlers = {\n      'Escape': () => {\n        e.preventDefault();\n        handleClose();\n      },\n\n      'ArrowDown': () => {\n        e.preventDefault();\n        setIsKeyboardNav(true);\n        if (focusedSection === 'mode') {\n          setFocusedSection(sections[1]);\n          setFocusedIndex(0);\n        } else if (focusedIndex < maxIndex) {\n          setFocusedIndex(focusedIndex + 1);\n        }\n      },\n\n      'ArrowUp': () => {\n        e.preventDefault();\n        setIsKeyboardNav(true);\n        if (focusedSection !== 'mode') {\n          if (focusedIndex > 0) {\n            setFocusedIndex(focusedIndex - 1);\n          } else {\n            setFocusedSection('mode');\n            setFocusedIndex(getModeIndex());\n          }\n        }\n      },\n\n      'ArrowLeft': () => {\n        e.preventDefault();\n        setIsKeyboardNav(true);\n        if (focusedSection === 'mode') {\n          if (focusedIndex > 0) setFocusedIndex(focusedIndex - 1);\n        } else if (isSystemMode && focusedSection === 'dark') {\n          setFocusedSection('light');\n          setFocusedIndex(Math.min(focusedIndex, lightThemes.length - 1));\n        }\n      },\n\n      'ArrowRight': () => {\n        e.preventDefault();\n        setIsKeyboardNav(true);\n        if (focusedSection === 'mode') {\n          if (focusedIndex < 2) setFocusedIndex(focusedIndex + 1);\n        } else if (isSystemMode && focusedSection === 'light') {\n          setFocusedSection('dark');\n          setFocusedIndex(Math.min(focusedIndex, darkThemes.length - 1));\n        }\n      },\n\n      'Enter': () => {\n        e.preventDefault();\n        if (focusedSection === 'mode') {\n          handleModeSelect(MODES[focusedIndex]);\n        } else if (focusedSection === 'light') {\n          handleThemeSelect(lightThemes[focusedIndex].id, true);\n        } else if (focusedSection === 'dark') {\n          handleThemeSelect(darkThemes[focusedIndex].id, false);\n        }\n      },\n\n      ' ': () => navigationHandlers.Enter(),\n\n      'Tab': () => handleClose()\n    };\n\n    navigationHandlers[e.key]?.();\n  }, [\n    isOpen, focusedSection, focusedIndex, getAvailableSections,\n    getMaxIndex, getModeIndex, isSystemMode, lightThemes, darkThemes\n  ]);\n\n  // Set up keyboard listener\n  useEffect(() => {\n    if (!isOpen) return;\n    document.addEventListener('keydown', handleKeyDown);\n    return () => document.removeEventListener('keydown', handleKeyDown);\n  }, [isOpen, handleKeyDown]);\n\n  // Render theme list\n  const renderThemeList = (themes, isLight, currentVariant, label) => {\n    const refs = isLight ? lightItemRefs : darkItemRefs;\n    const section = isLight ? 'light' : 'dark';\n    const isActiveSystemTheme = isSystemMode && ((isLight && displayedTheme === 'light') || (!isLight && displayedTheme === 'dark'));\n\n    return (\n      <div className=\"theme-list\" role=\"listbox\" aria-label={label}>\n        <div className=\"theme-list-label\">\n          {label}\n          {isActiveSystemTheme && <span className=\"active-badge\">Active</span>}\n        </div>\n        {themes.map((theme, index) => {\n          const isActive = currentVariant === theme.id;\n          return (\n            <div\n              key={theme.id}\n              ref={(el) => (refs.current[index] = el)}\n              className={`theme-item ${isActive ? 'active' : ''} ${getFocusedClass(section, index)}`}\n              role=\"option\"\n              aria-selected={isActive}\n              tabIndex={-1}\n              onClick={() => handleThemeSelect(theme.id, isLight)}\n              onMouseEnter={() => handleMouseEnter(section, index)}\n            >\n              <span className=\"theme-item-label\">{theme.name}</span>\n              {isActive && <IconCheck size={14} strokeWidth={2} className=\"check-icon\" />}\n            </div>\n          );\n        })}\n      </div>\n    );\n  };\n\n  // Render mode buttons\n  const renderModeButtons = () => (\n    <div className=\"mode-buttons\" role=\"radiogroup\" aria-labelledby=\"mode-label\">\n      {MODE_BUTTONS.map((btn, index) => {\n        const Icon = btn.icon;\n        const isActive = storedTheme === btn.mode;\n        return (\n          <button\n            key={btn.mode}\n            ref={(el) => (modeButtonRefs.current[index] = el)}\n            className={`mode-button ${isActive ? 'active' : ''} ${getFocusedClass('mode', index)}`}\n            role=\"radio\"\n            aria-checked={isActive}\n            tabIndex={-1}\n            onClick={() => handleModeSelect(btn.mode)}\n            onMouseEnter={() => handleMouseEnter('mode', index)}\n            title={btn.title}\n          >\n            <Icon size={18} strokeWidth={1.5} />\n          </button>\n        );\n      })}\n    </div>\n  );\n\n  // Menu content\n  const menuContent = (\n    <StyledWrapper>\n      <div\n        ref={menuRef}\n        className={`theme-menu ${isSystemMode ? 'two-columns' : ''}`}\n        role=\"dialog\"\n        aria-label=\"Theme selector\"\n      >\n        <div className=\"mode-section\">\n          <div className=\"mode-label\" id=\"mode-label\">Appearance</div>\n          {renderModeButtons()}\n        </div>\n\n        <div className={`theme-lists ${isSystemMode ? 'two-columns' : ''}`}>\n          {(storedTheme === 'light' || isSystemMode)\n            && renderThemeList(lightThemes, true, themeVariantLight, 'Light theme')}\n          {(storedTheme === 'dark' || isSystemMode)\n            && renderThemeList(darkThemes, false, themeVariantDark, 'Dark theme')}\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n\n  return (\n    <ToolHint text=\"Theme\" toolhintId=\"ThemeDropdown\" place=\"top\" offset={10} hidden={!tooltipEnabled}>\n      <Tippy\n        content={menuContent}\n        placement=\"top-start\"\n        interactive\n        arrow={false}\n        animation={false}\n        visible={isOpen}\n        onClickOutside={handleClose}\n        appendTo=\"parent\"\n      >\n        <div onClick={() => (isOpen ? handleClose() : handleOpen())}>\n          {children}\n        </div>\n      </Tippy>\n    </ToolHint>\n  );\n};\n\nexport default ThemeDropdown;\n"
  },
  {
    "path": "packages/bruno-app/src/components/StatusBar/index.js",
    "content": "import React, { useState } from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport find from 'lodash/find';\nimport { IconSettings, IconCookie, IconTool, IconSearch, IconPalette, IconBrandGithub } from '@tabler/icons';\nimport Mousetrap from 'mousetrap';\nimport { getKeyBindingsForActionAllOS } from 'providers/Hotkeys/keyMappings';\nimport ToolHint from 'components/ToolHint';\nimport Cookies from 'components/Cookies';\nimport Notifications from 'components/Notifications';\nimport Portal from 'components/Portal';\nimport ThemeDropdown from './ThemeDropdown';\nimport { openConsole } from 'providers/ReduxStore/slices/logs';\nimport { addTab } from 'providers/ReduxStore/slices/tabs';\nimport { useApp } from 'providers/App';\nimport StyledWrapper from './StyledWrapper';\n\nconst StatusBar = () => {\n  const dispatch = useDispatch();\n  const activeWorkspaceUid = useSelector((state) => state.workspaces.activeWorkspaceUid);\n  const workspaces = useSelector((state) => state.workspaces.workspaces);\n  const showHomePage = useSelector((state) => state.app.showHomePage);\n  const showManageWorkspacePage = useSelector((state) => state.app.showManageWorkspacePage);\n  const showApiSpecPage = useSelector((state) => state.app.showApiSpecPage);\n  const tabs = useSelector((state) => state.tabs.tabs);\n  const activeTabUid = useSelector((state) => state.tabs.activeTabUid);\n  const activeTab = find(tabs, (t) => t.uid === activeTabUid);\n  const logs = useSelector((state) => state.logs.logs);\n  const [cookiesOpen, setCookiesOpen] = useState(false);\n  const { version } = useApp();\n\n  const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);\n\n  const errorCount = logs.filter((log) => log.type === 'error').length;\n\n  const handleConsoleClick = () => {\n    dispatch(openConsole());\n  };\n\n  const handlePreferencesClick = () => {\n    const collectionUid = activeTab?.collectionUid || activeWorkspace?.scratchCollectionUid;\n\n    dispatch(\n      addTab({\n        type: 'preferences',\n        uid: collectionUid ? `${collectionUid}-preferences` : 'preferences',\n        collectionUid: collectionUid\n      })\n    );\n  };\n\n  const openGlobalSearch = () => {\n    const bindings = getKeyBindingsForActionAllOS('globalSearch') || [];\n    bindings.forEach((binding) => {\n      Mousetrap.trigger(binding);\n    });\n  };\n\n  return (\n    <StyledWrapper>\n      {cookiesOpen && (\n        <Portal>\n          <Cookies\n            onClose={() => {\n              setCookiesOpen(false);\n              document.querySelector('[data-trigger=\"cookies\"]').focus();\n            }}\n            aria-modal=\"true\"\n            role=\"dialog\"\n            aria-labelledby=\"cookies-title\"\n            aria-describedby=\"cookies-description\"\n          />\n        </Portal>\n      )}\n\n      <div className=\"status-bar\">\n        <div className=\"status-bar-section\">\n          <div className=\"status-bar-group\">\n            <ToolHint text=\"Preferences\" toolhintId=\"Preferences\" place=\"top-start\" offset={10}>\n              <button\n                className=\"status-bar-button preferences-button\"\n                data-trigger=\"preferences\"\n                onClick={handlePreferencesClick}\n                tabIndex={0}\n                aria-label=\"Open Preferences\"\n              >\n                <IconSettings size={16} strokeWidth={1.5} aria-hidden=\"true\" />\n              </button>\n            </ToolHint>\n\n            <ThemeDropdown>\n              <button\n                className=\"status-bar-button\"\n                data-trigger=\"theme\"\n                tabIndex={0}\n                aria-label=\"Change Theme\"\n              >\n                <IconPalette size={16} strokeWidth={1.5} aria-hidden=\"true\" />\n              </button>\n            </ThemeDropdown>\n\n            <ToolHint text=\"Notifications\" toolhintId=\"Notifications\" place=\"top\" offset={10}>\n              <div className=\"status-bar-button\">\n                <Notifications />\n              </div>\n            </ToolHint>\n\n            <ToolHint text=\"GitHub Repository\" toolhintId=\"GitHub\" place=\"top\" offset={10}>\n              <button\n                className=\"status-bar-button\"\n                onClick={() => {\n                  window?.ipcRenderer?.openExternal('https://github.com/usebruno/bruno');\n                }}\n                tabIndex={0}\n                aria-label=\"Open GitHub Repository\"\n              >\n                <IconBrandGithub size={16} strokeWidth={1.5} aria-hidden=\"true\" />\n              </button>\n            </ToolHint>\n          </div>\n        </div>\n\n        <div className=\"status-bar-section\">\n          <div className=\"flex items-center gap-3\">\n            <button\n              className=\"status-bar-button\"\n              data-trigger=\"search\"\n              onClick={openGlobalSearch}\n              tabIndex={0}\n              aria-label=\"Global Search\"\n            >\n              <div className=\"console-button-content\">\n                <IconSearch size={16} strokeWidth={1.5} aria-hidden=\"true\" />\n                <span className=\"console-label\">Search</span>\n              </div>\n            </button>\n\n            <button\n              className=\"status-bar-button\"\n              data-trigger=\"cookies\"\n              onClick={() => setCookiesOpen(true)}\n              tabIndex={0}\n              aria-label=\"Open Cookies\"\n            >\n              <div className=\"console-button-content\">\n                <IconCookie size={16} strokeWidth={1.5} aria-hidden=\"true\" />\n                <span className=\"console-label\">Cookies</span>\n              </div>\n            </button>\n\n            <button\n              className={`status-bar-button ${errorCount > 0 ? 'has-errors' : ''}`}\n              data-trigger=\"dev-tools\"\n              onClick={handleConsoleClick}\n              tabIndex={0}\n              aria-label={`Open Dev Tools${errorCount > 0 ? ` (${errorCount} errors)` : ''}`}\n            >\n              <div className=\"console-button-content\">\n                <IconTool size={16} strokeWidth={1.5} aria-hidden=\"true\" />\n                <span className=\"console-label\">Dev Tools</span>\n                {errorCount > 0 && (\n                  <span className=\"error-count-inline\">{errorCount}</span>\n                )}\n              </div>\n            </button>\n\n            <div className=\"status-bar-divider\"></div>\n\n            <div className=\"status-bar-version\">\n              v{version}\n            </div>\n          </div>\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default StatusBar;\n"
  },
  {
    "path": "packages/bruno-app/src/components/StatusDot/index.js",
    "content": "import React from 'react';\nimport DotIcon from 'components/Icons/Dot';\n\nconst StatusDot = ({ type = 'default' }) => (\n  <sup\n    className={`ml-[.125rem] opacity-80 font-medium ${\n      type === 'error' ? 'text-red-500' : ''\n    }`}\n  >\n    <DotIcon width=\"10\" />\n  </sup>\n);\n\nexport default StatusDot;\n"
  },
  {
    "path": "packages/bruno-app/src/components/StopWatch/index.js",
    "content": "import React, { useState, useEffect } from 'react';\n\nconst StopWatch = ({ startTime }) => {\n  const [currentTime, setCurrentTime] = useState(Date.now());\n\n  useEffect(() => {\n    if (!startTime) return;\n\n    const intervalId = setInterval(() => {\n      setCurrentTime(Date.now());\n    }, 100);\n\n    return () => clearInterval(intervalId);\n  }, [startTime]);\n\n  if (!startTime) return <span>Loading...</span>;\n\n  const elapsedTime = currentTime - startTime;\n  if (elapsedTime < 250) return <span>Loading...</span>;\n\n  const seconds = elapsedTime / 1000;\n  return <span>{seconds.toFixed(1)}s</span>;\n};\n\nexport default React.memo(StopWatch);\n"
  },
  {
    "path": "packages/bruno-app/src/components/Tab/index.js",
    "content": "import React from 'react';\nimport classnames from 'classnames';\n\nconst Tab = ({ name, label, isActive, onClick, count = 0, className = '', ...props }) => {\n  const tabClassName = classnames('tab select-none', {\n    active: isActive\n  }, className);\n\n  return (\n    <div\n      className={tabClassName}\n      role=\"tab\"\n      onClick={() => onClick(name)}\n      data-testid={`tab-${name}`}\n      {...props}\n    >\n      {label}\n      {count > 0 && <sup className=\"ml-1 font-medium\" data-testid={`tab-${name}-count`}>{count}</sup>}\n    </div>\n  );\n};\n\nexport default Tab;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Table/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  table {\n    width: 100%;\n    display: grid;\n    overflow-y: hidden;\n    overflow-x: auto;\n    padding: 0 1.5px;\n\n    // for icon hover\n    position: inherit;\n\n    grid-template-columns: ${({ columns }) =>\n      columns?.[0]?.width\n        ? columns.map((col) => `${col?.width}`).join(' ')\n        : columns.map((col) => `${100 / columns.length}%`).join(' ')};\n  }\n\n  table thead,\n  table tbody,\n  table tr {\n    display: contents;\n  }\n\n  table th {\n    position: relative;\n    font-weight: 400;\n    border-bottom: 1px solid ${(props) => props.theme.table.border};\n  }\n\n  table tr td {\n    padding: 0.5rem;\n    text-align: left;\n    border-top: 1px solid ${(props) => props.theme.table.border};\n    border-right: 1px solid ${(props) => props.theme.table.border};\n  }\n\n  tr {\n    transition: transform 0.2s ease-in-out;\n  }\n\n  tr.dragging {\n    opacity: 0.5;\n  }\n\n  tr.hovered {\n    transform: translateY(10px); /* Adjust the value as needed for the animation effect */\n  }\n\n  table tr th {\n    padding: 0.5rem;\n    text-align: left;\n    border-top: 1px solid ${(props) => props.theme.table.border};\n    border-right: 1px solid ${(props) => props.theme.table.border};\n\n    &:nth-child(1) {\n      border-left: 1px solid ${(props) => props.theme.table.border};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Table/index.js",
    "content": "import { useState, useRef, useEffect, useCallback } from 'react';\nimport StyledWrapper from './StyledWrapper';\n\nconst Table = ({ minColumnWidth = 1, headers = [], children }) => {\n  const [activeColumnIndex, setActiveColumnIndex] = useState(null);\n  const tableRef = useRef(null);\n\n  const columns = headers?.map((item) => ({\n    ...item,\n    ref: useRef()\n  }));\n\n  const updateDivHeights = () => {\n    if (tableRef.current) {\n      const height = tableRef.current.offsetHeight;\n      columns.forEach((col) => {\n        if (col.ref.current) {\n          col.ref.current.querySelector('.resizer').style.height = `${height}px`;\n        }\n      });\n    }\n  };\n\n  useEffect(() => {\n    updateDivHeights();\n    window.addEventListener('resize', updateDivHeights);\n\n    return () => {\n      window.removeEventListener('resize', updateDivHeights);\n    };\n  }, [columns]);\n\n  useEffect(() => {\n    if (tableRef.current) {\n      const observer = new MutationObserver(updateDivHeights);\n      observer.observe(tableRef.current, { childList: true, subtree: true });\n\n      return () => {\n        observer.disconnect();\n      };\n    }\n  }, [columns]);\n\n  const handleMouseDown = (index) => (e) => {\n    setActiveColumnIndex(index);\n  };\n\n  const handleMouseMove = useCallback(\n    (e) => {\n      const gridColumns = columns.map((col, i) => {\n        if (i === activeColumnIndex) {\n          const width = e.clientX - col.ref?.current?.getBoundingClientRect()?.left;\n\n          if (width >= minColumnWidth) {\n            return `${width}px`;\n          }\n        }\n        return `${col.ref.current.offsetWidth}px`;\n      });\n\n      tableRef.current.style.gridTemplateColumns = `${gridColumns.join(' ')}`;\n    },\n    [activeColumnIndex, columns, minColumnWidth]\n  );\n\n  const removeListeners = useCallback(() => {\n    window.removeEventListener('mousemove', handleMouseMove);\n    window.removeEventListener('mouseup', removeListeners);\n  }, [handleMouseMove]);\n\n  const handleMouseUp = useCallback(() => {\n    setActiveColumnIndex(null);\n    removeListeners?.();\n  }, [removeListeners]);\n\n  useEffect(() => {\n    if (activeColumnIndex !== null) {\n      window.addEventListener('mousemove', handleMouseMove);\n      window.addEventListener('mouseup', handleMouseUp);\n    }\n    return () => {\n      removeListeners();\n    };\n  }, [activeColumnIndex, handleMouseMove, handleMouseUp, removeListeners]);\n\n  return (\n    <StyledWrapper columns={columns}>\n      <div className=\"relative\">\n        <table ref={tableRef} className=\"inherit\">\n          <thead>\n            <tr>\n              {columns.map(({ ref, name }, i) => (\n                <th ref={ref} key={name} title={name}>\n                  <span>{name}</span>\n                  <div\n                    className=\"resizer absolute cursor-col-resize w-[4px] right-[-2px] top-0 z-10 opacity-50 hover:bg-blue-500 active:bg-blue-500\"\n                    onMouseDown={handleMouseDown(i)}\n                  >\n                  </div>\n                </th>\n              ))}\n            </tr>\n          </thead>\n          {children}\n        </table>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default Table;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Tabs/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .tabs-list {\n    display: inline-flex;\n    height: 2rem;\n    width: fit-content;\n    justify-content: center;\n    gap: 0.25rem;\n  }\n\n  .tab-trigger {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    border-radius: 4px;\n    padding: 8px;\n    font-size: 0.75rem;\n    white-space: nowrap;\n    cursor: pointer;\n    border: 1px solid transparent;\n    background: transparent;\n    color: ${(props) => props.theme.tabs.secondary.inactive.color};\n    transition: all 0.15s ease;\n\n    &:hover {\n      background: ${(props) => props.theme.tabs.secondary.inactive.bg};\n    }\n\n    &.active {\n      background: ${(props) => props.theme.tabs.secondary.active.bg};\n      color: ${(props) => props.theme.tabs.secondary.active.color};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/Tabs/index.js",
    "content": "import React, { createContext, useContext } from 'react';\nimport classnames from 'classnames';\nimport StyledWrapper from './StyledWrapper';\n\nconst TabsContext = createContext();\n\nexport const Tabs = ({ value, onValueChange, children, className = '' }) => {\n  return (\n    <TabsContext.Provider value={{ value, onValueChange }}>\n      <StyledWrapper className={`flex flex-col h-full flex-1 ${className}`}>{children}</StyledWrapper>\n    </TabsContext.Provider>\n  );\n};\n\nexport const TabsList = ({ children, className = '' }) => {\n  return <div className={`tabs-list ${className}`}>{children}</div>;\n};\n\nexport const TabsTrigger = ({ value: triggerValue, children, className = '' }) => {\n  const { value, onValueChange } = useContext(TabsContext);\n  const isActive = value === triggerValue;\n\n  return (\n    <button\n      onClick={() => onValueChange(triggerValue)}\n      className={classnames('tab-trigger', className, { active: isActive })}\n    >\n      {children}\n    </button>\n  );\n};\n\nexport const TabsContent = ({ value: contentValue, children, className = '', dataTestId = '' }) => {\n  const { value } = useContext(TabsContext);\n  const isActive = value === contentValue;\n\n  return (\n    <div\n      className={`outline-none flex flex-col h-full flex-1 ${className}`}\n      data-testid={dataTestId}\n      style={{ display: isActive ? 'flex' : 'none' }}\n    >\n      {children}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/bruno-app/src/components/TagList/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .tags-container {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 8px;\n    min-height: 40px;\n    padding: 8px 0;\n  }\n\n  .tag-item {\n    display: inline-flex;\n    align-items: center;\n    gap: 6px;\n    padding: 3px 7px;\n    background-color: ${(props) => props.theme.sidebar.bg};\n    border: 1px solid ${(props) => props.theme.requestTabs.bottomBorder};\n    border-radius: 3px;\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-weight: 500;\n    color: ${(props) => props.theme.text};\n    max-width: 200px;\n    transition: all 0.2s ease;\n    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n    cursor: default;\n\n    &:has(.tag-remove:hover) {\n      background-color: ${(props) => props.theme.background.surface2};\n      border-color: ${(props) => props.theme.requestTabs.bottomBorder};\n      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n      transform: translateY(-1px);\n    }\n\n    .tag-remove {\n      cursor: pointer;\n    }\n  }\n\n  .tag-icon {\n    color: ${(props) => props.theme.textSecondary || props.theme.text};\n    opacity: 0.7;\n    flex-shrink: 0;\n  }\n\n  .tag-text {\n    flex: 1;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    min-width: 0;\n  }\n\n  .tag-remove {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    background: none;\n    border: none;\n    cursor: pointer;\n    padding: 2px;\n    border-radius: 3px;\n    color: ${(props) => props.theme.textSecondary || props.theme.text};\n    transition: all 0.2s ease;\n    flex-shrink: 0;\n    opacity: 0.7;\n\n    &:hover {\n      background-color: ${(props) => props.theme.danger};\n      color: white;\n      opacity: 1;\n      transform: scale(1.1);\n    }\n\n    &:focus-visible {\n      outline: 2px solid ${(props) => props.theme.danger};\n      outline-offset: 1px;\n    }\n  }\n\n  .empty-state {\n    display: flex;\n    align-items: center;\n    gap: 12px;\n    padding: 24px 16px;\n    background-color: ${(props) => props.theme.sidebar.bg};\n    border: 2px dashed ${(props) => props.theme.requestTabs.bottomBorder};\n    border-radius: 3px;\n    color: ${(props) => props.theme.textSecondary || props.theme.text};\n    text-align: left;\n  }\n\n  .empty-icon {\n    opacity: 0.5;\n    flex-shrink: 0;\n  }\n\n  .empty-text {\n    flex: 1;\n    min-width: 0;\n  }\n\n  .empty-title {\n    font-weight: 500;\n    margin: 0 0 4px 0;\n    font-size: ${(props) => props.theme.font.size.base};\n    color: ${(props) => props.theme.text};\n  }\n\n  .empty-subtitle {\n    margin: 0;\n    font-size: ${(props) => props.theme.font.size.sm};\n    opacity: 0.8;\n    line-height: 1.5;\n    color: ${(props) => props.theme.textSecondary || props.theme.text};\n  }\n\n  /* Responsive design */\n  @media (max-width: 480px) {\n    .tags-container {\n      gap: 6px;\n    }\n    \n    .tag-item {\n      padding: 4px 8px;\n      font-size: ${(props) => props.theme.font.size.xs};\n    }\n    \n    .empty-state {\n      padding: 16px 12px;\n      flex-direction: column;\n      text-align: center;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/TagList/index.js",
    "content": "import { useState } from 'react';\nimport { IconX, IconTag } from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\nimport SingleLineEditor from 'components/SingleLineEditor/index';\nimport { useTheme } from 'providers/Theme/index';\n\nconst TagList = ({ tagsHintList = [], handleAddTag, tags, handleRemoveTag, onSave, handleValidation, collectionFormat }) => {\n  const { displayedTheme } = useTheme();\n  const isBruFormat = collectionFormat === 'bru';\n  const tagNameRegex = isBruFormat ? /^[\\p{L}\\p{N}_-]+$/u : /^[\\p{L}\\p{N}_-](?:[\\p{L}\\p{N}_\\s-]*[\\p{L}\\p{N}_-])?$/u;\n  const [text, setText] = useState('');\n  const [error, setError] = useState('');\n\n  const handleInputChange = (value) => {\n    setError('');\n    setText(value);\n  };\n\n  const handleKeyDown = (e) => {\n    if (!text.trim()) {\n      return;\n    }\n    if (!tagNameRegex.test(text)) {\n      setError(isBruFormat\n        ? 'Tags in BRU format must only contain letters, numbers, \"-\", \"_\".'\n        : 'Tags must only contain letters, numbers, spaces, \"-\", \"_\"'\n      );\n      return;\n    }\n    if (tags.includes(text)) {\n      setError(`Tag \"${text}\" already exists`);\n      return;\n    }\n    if (handleValidation) {\n      const error = handleValidation(text);\n      if (error) {\n        setError(error);\n        return;\n      }\n    }\n    handleAddTag(text);\n    setText('');\n  };\n\n  return (\n    <StyledWrapper className=\"flex flex-wrap flex-col gap-2\">\n      <SingleLineEditor\n        className=\"border border-gray-500/50 px-2\"\n        value={text}\n        placeholder=\"e.g., smoke, regression etc\"\n        autocomplete={tagsHintList}\n        showHintsOnClick={true}\n        showHintsFor={[]}\n        theme={displayedTheme}\n        onChange={handleInputChange}\n        onRun={handleKeyDown}\n        onSave={onSave}\n        data-testid=\"tag-input\"\n      />\n      {error && <span className=\"text-xs text-red-500\">{error}</span>}\n      <ul className=\"flex flex-wrap gap-1\">\n        {tags && tags.length\n          ? tags.map((_tag) => (\n              <li key={_tag}>\n                <button\n                  className=\"tag-item\"\n                  type=\"button\"\n                >\n                  <IconTag size={12} className=\"tag-icon\" aria-hidden=\"true\" />\n                  <span className=\"tag-text\" title={_tag}>\n                    {_tag}\n                  </span>\n                  <span className=\"tag-remove\" title=\"Remove tag\" onClick={() => handleRemoveTag(_tag)}>\n                    <IconX size={12} strokeWidth={2} aria-hidden=\"true\" />\n                  </span>\n                </button>\n              </li>\n            ))\n          : null}\n      </ul>\n    </StyledWrapper>\n  );\n};\n\nexport default TagList;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ToggleSwitch/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst switchSizes = {\n  '2xs': { width: 32, height: 16, buttonSize: 14 },\n  'xs': { width: 40, height: 20, buttonSize: 18 },\n  's': { width: 44, height: 22, buttonSize: 20 },\n  'm': { width: 50, height: 24, buttonSize: 22 }, // default size\n  'l': { width: 56, height: 28, buttonSize: 26 },\n  'xl': { width: 64, height: 32, buttonSize: 30 },\n  '2xl': { width: 72, height: 36, buttonSize: 34 }\n};\n\nconst getSizeValues = (size = 'm') => switchSizes[size] || switchSizes.m;\n\nexport const Switch = styled.div`\n  position: relative;\n  display: inline-block;\n  width: ${(props) => getSizeValues(props.size).width}px;\n  height: ${(props) => getSizeValues(props.size).height}px;\n  border-radius: ${(props) => getSizeValues(props.size).height}px;\n`;\n\nexport const Checkbox = styled.input`\n  opacity: 0;\n  width: 0;\n  height: 0;\n\n  &:checked + label div {\n    background-color: ${(props) => props.activeColor || props.theme.primary.solid};\n  }\n\n  &:checked + label div:before {\n    transform: translateX(${(props) => getSizeValues(props.size).width - getSizeValues(props.size).buttonSize - 2}px);\n  }\n`;\n\nexport const Label = styled.label`\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  cursor: pointer;\n  background-color: ${(props) => props.theme.input.bg};\n  border-radius: 24px;\n\n  div {\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background-color: ${(props) => props.theme.colors.text.muted};\n    border-radius: 24px;\n    transition: transform 0.2s;\n  }\n`;\n\nexport const Inner = styled.div`\n  position: absolute;\n  top: 2px;\n  left: 2px;\n  right: 2px;\n  bottom: 2px;\n  background-color: #fafafa;\n  transition: 0.4s;\n  border-radius: ${(props) => getSizeValues(props.size).height - 2}px;\n`;\n\nexport const SwitchButton = styled.div`\n  position: absolute;\n  height: ${(props) => getSizeValues(props.size).buttonSize}px;\n  width: ${(props) => getSizeValues(props.size).buttonSize}px;\n  left: 2px;\n  bottom: 2px;\n  background-color: white;\n  transition: 0.4s;\n  border-radius: 50%;\n\n  &:before {\n    content: '';\n    position: absolute;\n    height: ${(props) => getSizeValues(props.size).buttonSize - 2}px;\n    width: ${(props) => getSizeValues(props.size).buttonSize - 2}px;\n    background-color: white;\n    top: 2px;\n    left: 2px;\n    transition: 0.4s;\n    border-radius: 50%;\n  }\n`;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ToggleSwitch/index.js",
    "content": "import { Checkbox, Inner, Label, Switch, SwitchButton } from './StyledWrapper';\n\nconst ToggleSwitch = ({ isOn, handleToggle, size = 'm', activeColor, ...props }) => {\n  return (\n    <Switch size={size} {...props} onClick={handleToggle}>\n      <Checkbox checked={isOn} id=\"toggle-switch\" type=\"checkbox\" size={size} activeColor={activeColor} onChange={() => {}} />\n      <Label htmlFor=\"toggle-switch\">\n        <Inner size={size} />\n        <SwitchButton size={size} />\n      </Label>\n    </Switch>\n  );\n};\n\nexport default ToggleSwitch;\n"
  },
  {
    "path": "packages/bruno-app/src/components/ToolHint/index.js",
    "content": "import React from 'react';\nimport { Tooltip as ReactToolHint } from 'react-tooltip';\nimport { useTheme } from 'providers/Theme';\n\nconst ToolHint = ({\n  text,\n  toolhintId,\n  anchorSelect,\n  children,\n  tooltipStyle = {},\n  place = 'top',\n  hidden = false,\n  offset,\n  positionStrategy,\n  theme = null,\n  className = '',\n  delayShow = 200\n}) => {\n  const { theme: contextTheme } = useTheme();\n  const appliedTheme = theme || contextTheme;\n\n  const toolhintBackgroundColor = appliedTheme?.background.surface0;\n  const toolhintTextColor = appliedTheme?.text;\n\n  const combinedToolhintStyle = {\n    ...tooltipStyle,\n    fontSize: '0.75rem',\n    padding: '0.25rem 0.5rem',\n    zIndex: 9999,\n    backgroundColor: toolhintBackgroundColor,\n    color: toolhintTextColor\n  };\n\n  const toolhintProps_final = anchorSelect\n    ? { anchorSelect }\n    : { anchorId: toolhintId };\n\n  return (\n    <>\n      {!anchorSelect && <span id={toolhintId} className={className}>{children}</span>}\n      {anchorSelect && children}\n      <ReactToolHint\n        {...toolhintProps_final}\n        content={anchorSelect ? undefined : text}\n        className=\"toolhint\"\n        offset={offset}\n        place={place}\n        hidden={hidden}\n        positionStrategy={positionStrategy}\n        noArrow={true}\n        delayShow={delayShow}\n        style={combinedToolhintStyle}\n        opacity={1}\n      />\n    </>\n  );\n};\n\nexport default ToolHint;\n"
  },
  {
    "path": "packages/bruno-app/src/components/TruncatedText/index.js",
    "content": "import React, { useState, useRef, useEffect } from 'react';\n\nconst TruncatedText = ({\n  text,\n  maxLines = 2,\n  className = '',\n  textClassName = '',\n  buttonClassName = '',\n  viewMoreText = 'View More',\n  viewLessText = 'View Less',\n  showButton = true,\n  onToggle = null,\n  children,\n  dataTestId = ''\n}) => {\n  const [isExpanded, setIsExpanded] = useState(false);\n  const [shouldTruncate, setShouldTruncate] = useState(false);\n  const textRef = useRef(null);\n\n  useEffect(() => {\n    if (textRef.current) {\n      const element = textRef.current;\n      const computedStyle = window.getComputedStyle(element);\n      const lineHeight = computedStyle.lineHeight;\n\n      let actualLineHeight;\n      if (lineHeight === 'normal') {\n        // If line-height is 'normal', calculate it from font-size (typically 1.2x font-size)\n        const fontSize = parseInt(computedStyle.fontSize, 10);\n        actualLineHeight = fontSize * 1.2;\n      } else {\n        actualLineHeight = parseInt(lineHeight, 10);\n      }\n\n      const maxHeight = actualLineHeight * maxLines;\n\n      // Add a small tolerance (3px) to account for sub-pixel rendering and rounding errors\n      const tolerance = 3;\n\n      // Check if text needs truncation\n      if (element.scrollHeight > maxHeight + tolerance) {\n        setShouldTruncate(true);\n      } else {\n        setShouldTruncate(false);\n      }\n    }\n  }, [text, maxLines]);\n\n  const handleToggle = () => {\n    const newExpandedState = !isExpanded;\n    setIsExpanded(newExpandedState);\n\n    if (onToggle) {\n      onToggle(newExpandedState);\n    }\n  };\n\n  const defaultTextStyles = {\n    display: '-webkit-box',\n    WebkitLineClamp: isExpanded ? 'none' : maxLines,\n    WebkitBoxOrient: 'vertical',\n    overflow: 'hidden',\n    wordBreak: 'break-word'\n  };\n\n  const defaultButtonStyles = {\n    background: 'none',\n    border: 'none',\n    color: 'inherit',\n    cursor: 'pointer',\n    padding: '0',\n    marginLeft: '4px',\n    textDecoration: 'underline',\n    fontSize: 'inherit',\n    fontFamily: 'inherit'\n  };\n\n  if (!text || text.trim().length === 0) {\n    return null;\n  }\n\n  return (\n    <div className={className} data-testid={dataTestId}>\n      <div\n        ref={textRef}\n        className={textClassName}\n        style={!isExpanded && shouldTruncate ? defaultTextStyles : {}}\n      >\n        {children || text}\n      </div>\n\n      {shouldTruncate && showButton && (\n        <button\n          type=\"button\"\n          className={buttonClassName}\n          style={defaultButtonStyles}\n          onClick={handleToggle}\n          aria-label={isExpanded ? viewLessText : viewMoreText}\n        >\n          {isExpanded ? viewLessText : viewMoreText}\n        </button>\n      )}\n    </div>\n  );\n};\n\nexport default TruncatedText;\n"
  },
  {
    "path": "packages/bruno-app/src/components/VariablesEditor/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  table {\n    thead,\n    td {\n      border: 1px solid ${(props) => props.theme.table.border};\n\n      li {\n        background-color: ${(props) => props.theme.bg} !important;\n      }\n    }\n  }\n\n  .muted {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/VariablesEditor/index.js",
    "content": "import React, { useState } from 'react';\nimport get from 'lodash/get';\nimport filter from 'lodash/filter';\nimport { Inspector, chromeDark, chromeLight } from 'react-inspector';\nimport { useTheme } from 'providers/Theme';\nimport { findEnvironmentInCollection, maskInputValue } from 'utils/collections';\nimport StyledWrapper from './StyledWrapper';\nimport { IconEye, IconEyeOff } from '@tabler/icons';\n\nconst KeyValueExplorer = ({ data = [], theme }) => {\n  const [showSecret, setShowSecret] = useState(false);\n\n  return (\n    <div>\n      <SecretToggle showSecret={showSecret} onClick={() => setShowSecret(!showSecret)} />\n      <table className=\"border-collapse\">\n        <tbody>\n          {data.toSorted((a, b) => a.name.localeCompare(b.name)).map((envVar) => (\n            <tr key={envVar.name}>\n              <td className=\"px-2 py-1\">{envVar.name}</td>\n              <td className=\"px-2 py-1\">\n                <Inspector\n                  data={!showSecret && envVar.secret ? maskInputValue(envVar.value) : envVar.value}\n                  theme={theme}\n                />\n              </td>\n            </tr>\n          ))}\n        </tbody>\n      </table>\n    </div>\n  );\n};\n\nconst EnvVariables = ({ collection, theme }) => {\n  const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);\n\n  if (!environment) {\n    return (\n      <>\n        <h1 className=\"font-medium mt-4 mb-2\">Environment Variables</h1>\n        <div className=\"muted text-xs\">No environment selected</div>\n      </>\n    );\n  }\n\n  const envVars = get(environment, 'variables', []);\n  const enabledEnvVars = filter(envVars, (variable) => variable.enabled);\n\n  return (\n    <>\n      <div className=\"flex items-center mt-4 mb-2\">\n        <h1 className=\"font-medium\">Environment Variables</h1>\n        <span className=\"muted ml-2\">({environment.name})</span>\n      </div>\n      {enabledEnvVars.length > 0 ? (\n        <KeyValueExplorer data={enabledEnvVars} theme={theme} />\n      ) : (\n        <div className=\"muted text-xs\">No environment variables found</div>\n      )}\n    </>\n  );\n};\n\nconst RuntimeVariables = ({ collection, theme }) => {\n  const runtimeVariablesFound = Object.keys(collection.runtimeVariables).length > 0;\n\n  const runtimeVariableArray = Object.entries(collection.runtimeVariables).map(([name, value]) => ({\n    name,\n    value,\n    secret: false\n  }));\n\n  return (\n    <>\n      <h1 className=\"font-medium mb-2\">Runtime Variables</h1>\n      {runtimeVariablesFound ? (\n        <KeyValueExplorer data={runtimeVariableArray} theme={theme} />\n      ) : (\n        <div className=\"muted text-xs\">No runtime variables found</div>\n      )}\n    </>\n  );\n};\n\nconst VariablesEditor = ({ collection }) => {\n  const { displayedTheme, theme } = useTheme();\n\n  const reactInspectorTheme\n    = displayedTheme === 'light'\n      ? { ...chromeLight, OBJECT_VALUE_STRING_COLOR: theme.text.base }\n      : { ...chromeDark, OBJECT_VALUE_STRING_COLOR: theme.text.base };\n\n  return (\n    <StyledWrapper className=\"px-4 py-4 overflow-auto\">\n      <RuntimeVariables collection={collection} theme={reactInspectorTheme} />\n      <EnvVariables collection={collection} theme={reactInspectorTheme} />\n\n      <div className=\"mt-8 muted text-xs\">\n        Note: As of today, runtime variables can only be set via the API - <span className=\"font-medium\">getVar()</span>{' '}\n        and <span className=\"font-medium\">setVar()</span>. <br />\n        You can use the <span className=\"font-medium\">var</span> variable with the\n        <span className=\"font-medium\">{'{{var}}'}</span> syntax.<br />\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default VariablesEditor;\n\nconst SecretToggle = ({ showSecret, onClick }) => (\n  <div className=\"cursor-pointer mb-2 text-xs\" onClick={onClick}>\n    <div className=\"flex items-center\">\n      {showSecret ? <IconEyeOff size={16} strokeWidth={1.5} /> : <IconEye size={16} strokeWidth={1.5} />}\n      <span className=\"pl-1\">{showSecret ? 'Hide secret variable values' : 'Show secret variable values'}</span>\n    </div>\n  </div>\n);\n"
  },
  {
    "path": "packages/bruno-app/src/components/WelcomeModal/GetStartedStep/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst StyledWrapper = styled.div`\n  .primary-actions {\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    gap: 0.75rem;\n    margin-bottom: 0.75rem;\n  }\n\n  .primary-action-card {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    gap: 0.5rem;\n    padding: 1.25rem 1rem;\n    border-radius: ${(props) => props.theme.border.radius.md};\n    border: 1px solid ${(props) => props.theme.border.border1};\n    background: transparent;\n    cursor: pointer;\n    text-align: center;\n    color: ${(props) => props.theme.text};\n    transition: all 0.15s ease;\n\n    &:hover {\n      border-color: ${(props) => props.theme.primary.subtle};\n      background: ${(props) => rgba(props.theme.primary.solid, 0.06)};\n    }\n\n    &:active {\n      transform: scale(0.98);\n    }\n\n    .card-icon {\n      width: 40px;\n      height: 40px;\n      border-radius: ${(props) => props.theme.border.radius.md};\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      background: ${(props) => rgba(props.theme.primary.solid, 0.1)};\n      color: ${(props) => props.theme.primary.solid};\n    }\n\n    .card-title {\n      font-weight: 600;\n      font-size: 0.875rem;\n    }\n\n    .card-desc {\n      font-size: 0.75rem;\n      color: ${(props) => props.theme.colors.text.subtext0};\n      line-height: 1.4;\n    }\n  }\n\n  .secondary-actions {\n    display: flex;\n    flex-direction: column;\n    gap: 0.5rem;\n  }\n\n  .secondary-action {\n    display: flex;\n    align-items: center;\n    gap: 0.75rem;\n    padding: 0.625rem 0.75rem;\n    border-radius: ${(props) => props.theme.border.radius.base};\n    border: 1px solid ${(props) => props.theme.border.border0};\n    background: transparent;\n    cursor: pointer;\n    text-align: left;\n    width: 100%;\n    color: ${(props) => props.theme.text};\n    transition: all 0.15s ease;\n\n    &:hover {\n      border-color: ${(props) => props.theme.primary.subtle};\n      background: ${(props) => rgba(props.theme.primary.solid, 0.06)};\n    }\n\n    &:active {\n      transform: scale(0.98);\n    }\n\n    .secondary-icon {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      flex-shrink: 0;\n      color: ${(props) => props.theme.colors.text.subtext0};\n    }\n\n    .secondary-label {\n      font-size: 0.8125rem;\n      font-weight: 500;\n    }\n\n    .secondary-desc {\n      font-size: 0.6875rem;\n      color: ${(props) => props.theme.colors.text.subtext0};\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WelcomeModal/GetStartedStep/index.js",
    "content": "import React from 'react';\nimport { IconPlus, IconDownload, IconFileImport, IconSend } from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\n\nconst GetStartedStep = ({ onCreateCollection, onImportCollection, onOpenCollection, onStartRequest }) => (\n  <StyledWrapper className=\"step-body\">\n    <div className=\"step-label\">Your first collection</div>\n    <div className=\"step-title\">You're all set! What's next?</div>\n    <div className=\"step-description\">\n      Create a new collection to start building requests, or import one you already have.\n    </div>\n\n    <div className=\"primary-actions\">\n      <button className=\"primary-action-card\" onClick={onCreateCollection}>\n        <div className=\"card-icon\">\n          <IconPlus size={20} stroke={1.5} />\n        </div>\n        <div className=\"card-title\">Create Collection</div>\n        <div className=\"card-desc\">Start fresh with a new API collection</div>\n      </button>\n\n      <button className=\"primary-action-card\" onClick={onImportCollection}>\n        <div className=\"card-icon\">\n          <IconDownload size={20} stroke={1.5} />\n        </div>\n        <div className=\"card-title\">Import Collection</div>\n        <div className=\"card-desc\">Bring in Postman, OpenAPI, or Insomnia</div>\n      </button>\n    </div>\n\n    <div className=\"secondary-actions\">\n      <button className=\"secondary-action\" onClick={onOpenCollection}>\n        <span className=\"secondary-icon\">\n          <IconFileImport size={16} stroke={1.5} />\n        </span>\n        <div>\n          <div className=\"secondary-label\">Open existing collection</div>\n          <div className=\"secondary-desc\">Open a Bruno collection from your filesystem</div>\n        </div>\n      </button>\n      <button className=\"secondary-action\" onClick={onStartRequest}>\n        <span className=\"secondary-icon\">\n          <IconSend size={16} stroke={1.5} />\n        </span>\n        <div>\n          <div className=\"secondary-label\">Get started with a request</div>\n          <div className=\"secondary-desc\">Jump right in with a new HTTP request</div>\n        </div>\n      </button>\n    </div>\n  </StyledWrapper>\n);\n\nexport default GetStartedStep;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WelcomeModal/StorageStep/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  .location-input-group {\n    margin-bottom: 0.5rem;\n  }\n\n  .location-path-display {\n    display: flex;\n    align-items: center;\n    width: 100%;\n    padding: 0.5rem 0.75rem;\n    border-radius: ${(props) => props.theme.border.radius.base};\n    border: 1px solid ${(props) => props.theme.input.border};\n    background: ${(props) => props.theme.input.bg};\n    color: ${(props) => props.theme.text};\n    font-size: 0.8125rem;\n    line-height: 1.42857143;\n    cursor: pointer;\n    transition: border-color 0.15s ease;\n    gap: 0.625rem;\n    min-height: 38px;\n\n    &:hover {\n      border-color: ${(props) => props.theme.input.focusBorder};\n    }\n\n    .path-text {\n      flex: 1;\n      min-width: 0;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      white-space: nowrap;\n    }\n\n    .path-placeholder {\n      color: ${(props) => props.theme.colors.text.subtext0};\n    }\n\n    .browse-label {\n      flex-shrink: 0;\n      font-size: 0.75rem;\n      font-weight: 500;\n      color: ${(props) => props.theme.primary.text};\n    }\n  }\n\n  .location-hint {\n    color: ${(props) => props.theme.colors.text.subtext0};\n    font-size: 0.75rem;\n    line-height: 1.4;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WelcomeModal/StorageStep/index.js",
    "content": "import React from 'react';\nimport StyledWrapper from './StyledWrapper';\n\nconst StorageStep = ({ collectionLocation, onBrowse }) => (\n  <StyledWrapper className=\"step-body\">\n    <div className=\"step-label\">Storage</div>\n    <div className=\"step-title\">Where should we store your collections?</div>\n    <div className=\"step-description\">\n      Bruno saves collections as plain files on your filesystem, perfect for version control with Git.\n    </div>\n\n    <div className=\"location-input-group\">\n      <div\n        className=\"location-path-display\"\n        onClick={onBrowse}\n        onKeyDown={(e) => {\n          if (e.key === 'Enter' || e.key === ' ') {\n            e.preventDefault();\n            onBrowse();\n          }\n        }}\n        role=\"button\"\n        tabIndex={0}\n      >\n        {collectionLocation ? (\n          <span className=\"path-text\">{collectionLocation}</span>\n        ) : (\n          <span className=\"path-text path-placeholder\">Click to choose a folder...</span>\n        )}\n        <span className=\"browse-label\">Browse</span>\n      </div>\n    </div>\n    <div className=\"location-hint\">\n      Each collection and workspace gets its own folder inside this directory. You can change this later.\n    </div>\n  </StyledWrapper>\n);\n\nexport default StorageStep;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WelcomeModal/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst StyledWrapper = styled.div`\n  position: fixed;\n  inset: 0;\n  z-index: 100;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  background: rgba(0, 0, 0, 0.55);\n\n  .welcome-card {\n    background: ${(props) => props.theme.modal.body.bg};\n    border: 1px solid ${(props) => props.theme.border.border1};\n    border-radius: ${(props) => props.theme.border.radius.xl};\n    box-shadow: ${(props) => props.theme.shadow.lg};\n    width: 660px;\n    max-width: 92vw;\n    max-height: 90vh;\n    overflow-y: auto;\n    animation: welcomeSlideIn 0.4s cubic-bezier(0.16, 1, 0.3, 1);\n  }\n\n  @keyframes welcomeSlideIn {\n    from {\n      opacity: 0;\n      transform: translateY(12px) scale(0.98);\n    }\n    to {\n      opacity: 1;\n      transform: translateY(0) scale(1);\n    }\n  }\n\n  .welcome-header {\n    text-align: center;\n    padding: 2.25rem 2.5rem 0 2.5rem;\n  }\n\n  .logo-container {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    margin-bottom: 0.75rem;\n  }\n\n  .welcome-heading {\n    font-size: 1.375rem;\n    font-weight: 700;\n    color: ${(props) => props.theme.text};\n    margin: 0;\n    line-height: 1.3;\n  }\n\n  .welcome-tagline {\n    color: ${(props) => props.theme.colors.text.subtext1};\n    font-size: 0.875rem;\n    margin-top: 0.25rem;\n    line-height: 1.5;\n  }\n\n  .step-body {\n    padding: 1.5rem 2.5rem;\n  }\n\n  .step-label {\n    font-size: 0.6875rem;\n    font-weight: 600;\n    text-transform: uppercase;\n    letter-spacing: 0.06em;\n    color: ${(props) => props.theme.primary.text};\n    margin-bottom: 0.375rem;\n  }\n\n  .step-title {\n    font-size: 1.05rem;\n    font-weight: 600;\n    color: ${(props) => props.theme.text};\n    margin-bottom: 0.25rem;\n  }\n\n  .step-description {\n    color: ${(props) => props.theme.colors.text.subtext1};\n    font-size: 0.8125rem;\n    line-height: 1.5;\n    margin-bottom: 1.25rem;\n  }\n\n  .welcome-footer {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 0.75rem 2.5rem 1.75rem 2.5rem;\n  }\n\n  .progress-dots {\n    display: flex;\n    gap: 6px;\n    align-items: center;\n\n    .dot {\n      width: 8px;\n      height: 8px;\n      padding: 0;\n      border: none;\n      border-radius: 50%;\n      background: ${(props) => props.theme.border.border2};\n      transition: all 0.25s ease;\n      cursor: pointer;\n\n      &.active {\n        background: ${(props) => props.theme.primary.solid};\n        width: 20px;\n        border-radius: 4px;\n      }\n\n      &.completed {\n        background: ${(props) => rgba(props.theme.primary.solid, 0.45)};\n      }\n    }\n  }\n\n  .footer-buttons {\n    display: flex;\n    align-items: center;\n    gap: 0.5rem;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WelcomeModal/ThemeStep/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst StyledWrapper = styled.div`\n  .theme-mode-buttons {\n    display: flex;\n    gap: 0.5rem;\n    margin-bottom: 1.25rem;\n  }\n\n  .theme-mode-btn {\n    flex: 1;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    gap: 0.5rem;\n    padding: 0.5rem 0.75rem;\n    border-radius: ${(props) => props.theme.border.radius.md};\n    border: 1.5px solid ${(props) => props.theme.border.border1};\n    background: transparent;\n    color: ${(props) => props.theme.colors.text.subtext1};\n    cursor: pointer;\n    font-size: 0.8125rem;\n    font-weight: 500;\n    transition: all 0.15s ease;\n\n    &:hover {\n      border-color: ${(props) => props.theme.border.border2};\n      color: ${(props) => props.theme.text};\n    }\n\n    &.active {\n      border-color: ${(props) => props.theme.primary.solid};\n      background: ${(props) => rgba(props.theme.primary.solid, 0.07)};\n      color: ${(props) => props.theme.text};\n    }\n  }\n\n  .theme-variants-grid {\n    display: grid;\n    grid-template-columns: repeat(auto-fill, minmax(105px, 1fr));\n    gap: 0.5rem;\n  }\n\n  .theme-variant-option {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    gap: 0.375rem;\n    padding: 0.5rem 0.375rem;\n    border-radius: ${(props) => props.theme.border.radius.base};\n    border: 1.5px solid ${(props) => props.theme.border.border0};\n    background: transparent;\n    cursor: pointer;\n    transition: all 0.15s ease;\n    font-family: inherit;\n\n    &:hover {\n      border-color: ${(props) => props.theme.border.border2};\n    }\n\n    &.selected {\n      border-color: ${(props) => props.theme.primary.solid};\n      background: ${(props) => rgba(props.theme.primary.solid, 0.06)};\n    }\n\n    .variant-name {\n      font-size: 0.6875rem;\n      color: ${(props) => props.theme.colors.text.subtext0};\n      text-align: center;\n      white-space: nowrap;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      max-width: 100%;\n    }\n  }\n\n  .theme-preview-box {\n    width: 52px;\n    height: 34px;\n    border-radius: 3px;\n    display: flex;\n    overflow: hidden;\n\n    .preview-sidebar {\n      width: 13px;\n      height: 100%;\n    }\n\n    .preview-main {\n      flex: 1;\n      display: flex;\n      flex-direction: column;\n      padding: 4px;\n      gap: 3px;\n    }\n\n    .preview-line {\n      height: 3px;\n      border-radius: 2px;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WelcomeModal/ThemeStep/index.js",
    "content": "import React from 'react';\nimport { rgba } from 'polished';\nimport { IconBrightnessUp, IconMoon, IconDeviceDesktop } from '@tabler/icons';\nimport themes, { getLightThemes, getDarkThemes } from 'themes/index';\nimport StyledWrapper from './StyledWrapper';\n\nconst themeModes = [\n  { key: 'light', label: 'Light', icon: IconBrightnessUp },\n  { key: 'dark', label: 'Dark', icon: IconMoon },\n  { key: 'system', label: 'System', icon: IconDeviceDesktop }\n];\n\nconst ThemePreviewBox = ({ themeId, isDark }) => {\n  const themeData = themes[themeId] || themes[isDark ? 'dark' : 'light'];\n  const bgColor = themeData.background.base;\n  const sidebarColor = themeData.sidebar.bg;\n  const lineColor = rgba(themeData.brand, 0.5);\n\n  return (\n    <div className=\"theme-preview-box\" style={{ background: bgColor, border: `1px solid ${lineColor}` }}>\n      <div className=\"preview-sidebar\" style={{ background: sidebarColor }} />\n      <div className=\"preview-main\">\n        <div className=\"preview-line\" style={{ background: lineColor, width: '80%' }} />\n        <div className=\"preview-line\" style={{ background: lineColor, width: '55%' }} />\n        <div className=\"preview-line\" style={{ background: lineColor, width: '70%' }} />\n      </div>\n    </div>\n  );\n};\n\nconst ThemeStep = ({ storedTheme, setStoredTheme, themeVariantLight, setThemeVariantLight, themeVariantDark, setThemeVariantDark }) => {\n  const lightThemeList = getLightThemes();\n  const darkThemeList = getDarkThemes();\n\n  const showLight = storedTheme === 'light' || storedTheme === 'system';\n  const showDark = storedTheme === 'dark' || storedTheme === 'system';\n\n  return (\n    <StyledWrapper className=\"step-body\">\n      <div className=\"step-label\">Appearance</div>\n      <div className=\"step-title\">Choose your theme</div>\n      <div className=\"step-description\">\n        Pick a look that feels right. You can always change this later in Preferences.\n      </div>\n\n      <div className=\"theme-mode-buttons\">\n        {themeModes.map((mode) => {\n          const Icon = mode.icon;\n          return (\n            <button\n              key={mode.key}\n              className={`theme-mode-btn ${storedTheme === mode.key ? 'active' : ''}`}\n              onClick={() => setStoredTheme(mode.key)}\n            >\n              <Icon size={16} stroke={1.5} />\n              {mode.label}\n            </button>\n          );\n        })}\n      </div>\n\n      {showLight && (\n        <div className=\"theme-variants-grid\" style={{ marginBottom: showDark ? '1rem' : 0 }}>\n          {lightThemeList.map((t) => (\n            <button\n              type=\"button\"\n              key={t.id}\n              className={`theme-variant-option ${themeVariantLight === t.id ? 'selected' : ''}`}\n              onClick={() => setThemeVariantLight(t.id)}\n              aria-pressed={themeVariantLight === t.id}\n            >\n              <ThemePreviewBox themeId={t.id} isDark={false} />\n              <span className=\"variant-name\">{t.name}</span>\n            </button>\n          ))}\n        </div>\n      )}\n\n      {showDark && (\n        <div className=\"theme-variants-grid\">\n          {darkThemeList.map((t) => (\n            <button\n              type=\"button\"\n              key={t.id}\n              className={`theme-variant-option ${themeVariantDark === t.id ? 'selected' : ''}`}\n              onClick={() => setThemeVariantDark(t.id)}\n              aria-pressed={themeVariantDark === t.id}\n            >\n              <ThemePreviewBox themeId={t.id} isDark={true} />\n              <span className=\"variant-name\">{t.name}</span>\n            </button>\n          ))}\n        </div>\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default ThemeStep;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WelcomeModal/WelcomeStep/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst StyledWrapper = styled.div`\n  .highlights {\n    display: flex;\n    flex-direction: column;\n    gap: 0.875rem;\n  }\n\n  .highlight-item {\n    display: flex;\n    align-items: flex-start;\n    gap: 0.875rem;\n\n    .highlight-icon {\n      flex-shrink: 0;\n      width: 34px;\n      height: 34px;\n      border-radius: ${(props) => props.theme.border.radius.base};\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      background: ${(props) => rgba(props.theme.primary.solid, 0.1)};\n      color: ${(props) => props.theme.primary.solid};\n      margin-top: 1px;\n    }\n\n    .highlight-title {\n      font-weight: 600;\n      font-size: 0.8125rem;\n      color: ${(props) => props.theme.text};\n      margin-bottom: 0.125rem;\n    }\n\n    .highlight-desc {\n      font-size: 0.75rem;\n      color: ${(props) => props.theme.colors.text.subtext1};\n      line-height: 1.45;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WelcomeModal/WelcomeStep/index.js",
    "content": "import React from 'react';\nimport {\n  IconFolder as IconFolderTabler,\n  IconGitFork,\n  IconLock,\n  IconRocket\n} from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\n\nconst highlights = [\n  {\n    icon: IconFolderTabler,\n    title: 'Filesystem only',\n    desc: 'Collections are plain files on your disk. No cloud sync, no proprietary lock-in.'\n  },\n  {\n    icon: IconGitFork,\n    title: 'Git-friendly',\n    desc: 'Every request is a readable file. Commit, branch, review, and collaborate using the tools you already know.'\n  },\n  {\n    icon: IconLock,\n    title: 'Privacy-focused',\n    desc: 'No account, no login. Bruno works entirely offline, your API keys never leave your machine.'\n  },\n  {\n    icon: IconRocket,\n    title: 'Fast and lightweight',\n    desc: 'Built to be snappy. No bloated runtimes, just a fast, focused tool for exploring and testing APIs.'\n  }\n];\n\nconst WelcomeStep = () => (\n  <StyledWrapper className=\"step-body\">\n    <div className=\"highlights\">\n      {highlights.map((item) => {\n        const Icon = item.icon;\n        return (\n          <div key={item.title} className=\"highlight-item\">\n            <div className=\"highlight-icon\">\n              <Icon size={18} stroke={1.5} />\n            </div>\n            <div>\n              <div className=\"highlight-title\">{item.title}</div>\n              <div className=\"highlight-desc\">{item.desc}</div>\n            </div>\n          </div>\n        );\n      })}\n    </div>\n  </StyledWrapper>\n);\n\nexport default WelcomeStep;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WelcomeModal/index.js",
    "content": "import React, { useState } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport get from 'lodash/get';\nimport toast from 'react-hot-toast';\nimport Bruno from 'components/Bruno';\nimport Button from 'ui/Button';\nimport { useTheme } from 'providers/Theme';\nimport { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';\nimport { savePreferences } from 'providers/ReduxStore/slices/app';\nimport WelcomeStep from './WelcomeStep';\nimport ThemeStep from './ThemeStep';\nimport StorageStep from './StorageStep';\nimport GetStartedStep from './GetStartedStep';\nimport StyledWrapper from './StyledWrapper';\n\nconst TOTAL_STEPS = 4;\n\nconst WelcomeModal = ({ onDismiss, onImportCollection, onCreateCollection, onOpenCollection, onStartRequest }) => {\n  const dispatch = useDispatch();\n  const preferences = useSelector((state) => state.app.preferences);\n  const defaultLocation = get(preferences, 'general.defaultLocation', '');\n  const {\n    storedTheme,\n    setStoredTheme,\n    themeVariantLight,\n    setThemeVariantLight,\n    themeVariantDark,\n    setThemeVariantDark\n  } = useTheme();\n\n  const [step, setStep] = useState(1);\n  const [collectionLocation, setCollectionLocation] = useState(defaultLocation);\n\n  const handleBrowse = () => {\n    dispatch(browseDirectory())\n      .then((dirPath) => {\n        if (typeof dirPath === 'string') {\n          setCollectionLocation(dirPath);\n        }\n      })\n      .catch(() => {});\n  };\n\n  const persistPreferences = () => {\n    if (collectionLocation && collectionLocation !== defaultLocation) {\n      const updatedPreferences = {\n        ...preferences,\n        general: {\n          ...preferences.general,\n          defaultLocation: collectionLocation\n        }\n      };\n      return dispatch(savePreferences(updatedPreferences)).catch(() => {\n        toast.error('Failed to save preferences');\n      });\n    }\n    return Promise.resolve();\n  };\n\n  const handleSaveAndDismiss = () => {\n    persistPreferences().finally(() => {\n      onDismiss();\n    });\n  };\n\n  const handleActionAndDismiss = (action) => () => {\n    persistPreferences().finally(() => {\n      onDismiss();\n      action();\n    });\n  };\n\n  const goTo = (s) => setStep(s);\n\n  const steps = [\n    <WelcomeStep key=\"welcome\" />,\n    <ThemeStep\n      key=\"theme\"\n      storedTheme={storedTheme}\n      setStoredTheme={setStoredTheme}\n      themeVariantLight={themeVariantLight}\n      setThemeVariantLight={setThemeVariantLight}\n      themeVariantDark={themeVariantDark}\n      setThemeVariantDark={setThemeVariantDark}\n    />,\n    <StorageStep\n      key=\"storage\"\n      collectionLocation={collectionLocation}\n      onBrowse={handleBrowse}\n    />,\n    <GetStartedStep\n      key=\"getstarted\"\n      onCreateCollection={handleActionAndDismiss(onCreateCollection)}\n      onImportCollection={handleActionAndDismiss(onImportCollection)}\n      onOpenCollection={handleActionAndDismiss(onOpenCollection)}\n      onStartRequest={handleActionAndDismiss(onStartRequest)}\n    />\n  ];\n\n  const isLastStep = step === TOTAL_STEPS;\n\n  return (\n    <StyledWrapper data-testid=\"welcome-modal\">\n      <div className=\"welcome-card\">\n        <div className=\"welcome-header\">\n          <div className=\"logo-container\">\n            <Bruno width={48} />\n          </div>\n          <h1 className=\"welcome-heading\">\n            {step === 1 ? 'Welcome to Bruno' : step === 4 ? 'Ready to go!' : 'Set up Bruno'}\n          </h1>\n          {step === 1 && (\n            <p className=\"welcome-tagline\">\n              A fast, Git-friendly, and open-source API client.\n            </p>\n          )}\n        </div>\n\n        {steps[step - 1]}\n\n        <div className=\"welcome-footer\">\n          <div className=\"progress-dots\">\n            {Array.from({ length: TOTAL_STEPS }, (_, i) => (\n              <button\n                type=\"button\"\n                key={i}\n                className={`dot ${i + 1 === step ? 'active' : ''} ${i + 1 < step ? 'completed' : ''}`}\n                onClick={() => goTo(i + 1)}\n                aria-label={`Go to step ${i + 1}`}\n                aria-current={i + 1 === step ? 'step' : undefined}\n              />\n            ))}\n          </div>\n\n          <div className=\"footer-buttons\">\n            <Button type=\"button\" color=\"secondary\" variant=\"ghost\" onClick={handleSaveAndDismiss}>\n              Skip\n            </Button>\n            {step > 1 && (\n              <Button type=\"button\" color=\"secondary\" variant=\"ghost\" onClick={() => goTo(step - 1)}>\n                Back\n              </Button>\n            )}\n            {!isLastStep && (\n              <Button type=\"button\" onClick={() => goTo(step + 1)}>\n                {step === 1 ? 'Get Started' : 'Next'}\n              </Button>\n            )}\n            {isLastStep && (\n              <Button type=\"button\" color=\"secondary\" onClick={handleSaveAndDismiss}>\n                I'll explore on my own\n              </Button>\n            )}\n          </div>\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default WelcomeModal;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WorkspaceHome/WorkspaceDocs/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n\n  .docs-header {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 12px 16px;\n    border-bottom: 1px solid ${(props) => props.theme.workspace.border};\n  }\n\n  .docs-title {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-weight: 500;\n    color: ${(props) => props.theme.text};\n  }\n\n  .edit-btn {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    border-radius: 4px;\n    color: ${(props) => props.theme.colors.text.muted};\n    cursor: pointer;\n    transition: all 0.15s ease;\n  }\n\n  .docs-content {\n    flex: 1;\n    overflow-y: auto;\n    padding: 16px;\n  }\n\n  .editor-container {\n    display: flex;\n    flex-direction: column;\n    height: 100%;\n  }\n\n  .editor-actions {\n    display: flex;\n    justify-content: flex-end;\n    padding-top: 10px;\n  }\n\n  .save-btn {\n    padding: 6px 14px;\n    background: ${(props) => props.theme.button.secondary.bg};\n    color: ${(props) => props.theme.button.secondary.color};\n    border: 1px solid ${(props) => props.theme.button.secondary.border};\n    border-radius: ${(props) => props.theme.border.radius.sm};\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-weight: 500;\n    cursor: pointer;\n    transition: all 0.15s ease;\n\n    &:hover {\n      border-color: ${(props) => props.theme.button.secondary.hoverBorder};\n    }\n  }\n\n  .docs-markdown {\n    height: 100%;\n    overflow-y: auto;\n  }\n\n  .empty-state {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    text-align: center;\n    padding: 32px 16px;\n    height: 100%;\n  }\n\n  .empty-icon-wrapper {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 52px;\n    height: 52px;\n    border-radius: 8px;\n    color: ${(props) => props.theme.colors.text.muted};\n    margin-bottom: 16px;\n  }\n\n  .empty-text {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.muted};\n    margin-bottom: 2px;\n    line-height: 1.4;\n  }\n\n  .empty-subtext {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.muted};\n    margin-bottom: 8px;\n  }\n\n  .suggestions-list {\n    list-style: none;\n    padding: 0;\n    margin: 0 0 20px 0;\n    text-align: center;\n\n    li {\n      font-size: ${(props) => props.theme.font.size.sm};\n      color: ${(props) => props.theme.colors.text.muted};\n      padding: 2px 0;\n\n      &::before {\n        content: '\\\\2022';\n        color: ${(props) => props.theme.brand};\n        margin-right: 6px;\n      }\n    }\n  }\n\n  .add-docs-btn {\n    padding: 8px 16px;\n    background: transparent;\n    color: ${(props) => props.theme.brand};\n    border: 1px solid ${(props) => props.theme.brand};\n    border-radius: ${(props) => props.theme.border.radius.base};\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-weight: 500;\n    cursor: pointer;\n    transition: all 0.15s ease;\n\n    &:hover {\n      background: ${(props) => props.theme.brand}10;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WorkspaceHome/WorkspaceDocs/index.js",
    "content": "import 'github-markdown-css/github-markdown.css';\nimport get from 'lodash/get';\nimport { useTheme } from 'providers/Theme';\nimport { useState, useEffect } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { saveWorkspaceDocs } from 'providers/ReduxStore/slices/workspaces/actions';\nimport Markdown from 'components/MarkDown';\nimport CodeEditor from 'components/CodeEditor';\nimport StyledWrapper from './StyledWrapper';\nimport { IconFileText, IconEdit, IconX, IconPlus } from '@tabler/icons';\nimport Button from 'ui/Button';\nimport toast from 'react-hot-toast';\nimport ActionIcon from 'ui/ActionIcon/index';\n\nconst WorkspaceDocs = ({ workspace }) => {\n  const dispatch = useDispatch();\n  const { displayedTheme } = useTheme();\n  const [isEditing, setIsEditing] = useState(false);\n  const [localDocs, setLocalDocs] = useState(workspace?.docs || '');\n  const preferences = useSelector((state) => state.app.preferences);\n\n  useEffect(() => {\n    setLocalDocs(workspace?.docs || '');\n    setIsEditing(false);\n  }, [workspace?.uid, workspace?.docs]);\n\n  const toggleViewMode = () => {\n    setIsEditing((prev) => !prev);\n  };\n\n  const onEdit = (value) => {\n    setLocalDocs(value);\n  };\n\n  const handleDiscardChanges = () => {\n    setLocalDocs(workspace?.docs || '');\n    toggleViewMode();\n  };\n\n  const onSave = async () => {\n    if (!workspace) {\n      toast.error('Workspace not found');\n      return;\n    }\n\n    try {\n      await dispatch(saveWorkspaceDocs(workspace.uid, localDocs));\n      toast.success('Documentation saved successfully');\n      toggleViewMode();\n    } catch (error) {\n      console.error('Error saving workspace docs:', error);\n      toast.error('Failed to save documentation');\n    }\n  };\n\n  const handleAddDocumentation = () => {\n    setIsEditing(true);\n  };\n\n  const hasDocs = localDocs && localDocs.trim().length > 0;\n\n  return (\n    <StyledWrapper className=\"h-full w-full flex flex-col\">\n      <div className=\"docs-header\">\n        <div className=\"docs-title\">\n          <IconFileText size={16} strokeWidth={1.5} />\n          <span>Documentation</span>\n        </div>\n        {hasDocs && !isEditing && (\n          <ActionIcon className=\"edit-btn\" onClick={toggleViewMode}>\n            <IconEdit size={16} strokeWidth={1.5} />\n          </ActionIcon>\n        )}\n        {isEditing && (\n          <ActionIcon className=\"edit-btn\" onClick={handleDiscardChanges}>\n            <IconX size={16} strokeWidth={1.5} />\n          </ActionIcon>\n        )}\n      </div>\n\n      <div className=\"docs-content\">\n        {isEditing ? (\n          <div className=\"editor-container\">\n            <CodeEditor\n              theme={displayedTheme}\n              value={localDocs}\n              onEdit={onEdit}\n              onSave={onSave}\n              mode=\"markdown\"\n              font={get(preferences, 'font.codeFont', 'default')}\n              fontSize={get(preferences, 'font.codeFontSize')}\n            />\n            <div className=\"editor-actions\">\n              <Button onClick={onSave}>\n                Save\n              </Button>\n            </div>\n          </div>\n        ) : hasDocs ? (\n          <div className=\"docs-markdown\">\n            <Markdown collectionPath={workspace?.pathname || ''} onDoubleClick={toggleViewMode} content={localDocs} />\n          </div>\n        ) : (\n          <div className=\"empty-state\">\n            <div className=\"empty-icon-wrapper\">\n              <IconFileText size={52} strokeWidth={1} />\n            </div>\n            <p className=\"empty-text\">\n              Add documentation to help your team work smoothly.\n            </p>\n            <p className=\"empty-subtext\">You can include:</p>\n            <ul className=\"suggestions-list\">\n              <li>Project overview</li>\n              <li>Setup instructions</li>\n              <li>Key workflows</li>\n              <li>Resources & FAQs</li>\n            </ul>\n            <Button color=\"light\" size=\"sm\" icon={<IconPlus size={14} strokeWidth={1.5} />} onClick={handleAddDocumentation}>\n              Add Documentation\n            </Button>\n          </div>\n        )}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default WorkspaceDocs;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/CopyEnvironment/index.js",
    "content": "import Modal from 'components/Modal/index';\nimport Portal from 'components/Portal/index';\nimport { useFormik } from 'formik';\nimport { copyGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';\nimport { useEffect, useRef } from 'react';\nimport toast from 'react-hot-toast';\nimport { useDispatch } from 'react-redux';\nimport * as Yup from 'yup';\n\nconst CopyEnvironment = ({ environment, onClose }) => {\n  const dispatch = useDispatch();\n  const inputRef = useRef();\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: {\n      name: environment.name + ' - Copy'\n    },\n    validationSchema: Yup.object({\n      name: Yup.string()\n        .min(1, 'must be at least 1 character')\n        .max(50, 'must be 50 characters or less')\n        .required('name is required')\n    }),\n    onSubmit: (values) => {\n      dispatch(copyGlobalEnvironment({ name: values.name, environmentUid: environment.uid }))\n        .then(() => {\n          toast.success('Environment created!');\n          onClose();\n        })\n        .catch((error) => {\n          toast.error('An error occurred while creating the environment');\n          console.error(error);\n        });\n    }\n  });\n\n  useEffect(() => {\n    if (inputRef && inputRef.current) {\n      inputRef.current.focus();\n    }\n  }, [inputRef]);\n\n  const onSubmit = () => {\n    formik.handleSubmit();\n  };\n\n  return (\n    <Portal>\n      <Modal size=\"sm\" title=\"Copy Environment\" confirmText=\"Copy\" handleConfirm={onSubmit} handleCancel={onClose}>\n        <form className=\"bruno-form\" onSubmit={(e) => e.preventDefault()}>\n          <div>\n            <label htmlFor=\"environment-name\" className=\"block font-semibold\">\n              New Environment Name\n            </label>\n            <input\n              id=\"environment-name\"\n              type=\"text\"\n              name=\"name\"\n              ref={inputRef}\n              className=\"block textbox mt-2 w-full\"\n              autoComplete=\"off\"\n              autoCorrect=\"off\"\n              autoCapitalize=\"off\"\n              spellCheck=\"false\"\n              onChange={formik.handleChange}\n              value={formik.values.name || ''}\n            />\n            {formik.touched.name && formik.errors.name ? (\n              <div className=\"text-red-500\">{formik.errors.name}</div>\n            ) : null}\n          </div>\n        </form>\n      </Modal>\n    </Portal>\n  );\n};\n\nexport default CopyEnvironment;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/CreateEnvironment/index.js",
    "content": "import React, { useEffect, useRef } from 'react';\nimport toast from 'react-hot-toast';\nimport { useFormik } from 'formik';\nimport * as Yup from 'yup';\nimport { useDispatch, useSelector } from 'react-redux';\nimport Portal from 'components/Portal';\nimport Modal from 'components/Modal';\nimport { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';\nimport { validateName, validateNameError } from 'utils/common/regex';\n\nconst CreateEnvironment = ({ onClose, onEnvironmentCreated }) => {\n  const globalEnvs = useSelector((state) => state?.globalEnvironments?.globalEnvironments);\n\n  const validateEnvironmentName = (name) => {\n    const trimmedName = name?.toLowerCase().trim();\n    return (globalEnvs || []).every((env) => env?.name?.toLowerCase().trim() !== trimmedName);\n  };\n\n  const dispatch = useDispatch();\n  const inputRef = useRef();\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: {\n      name: ''\n    },\n    validationSchema: Yup.object({\n      name: Yup.string()\n        .min(1, 'Must be at least 1 character')\n        .max(255, 'Must be 255 characters or less')\n        .test('is-valid-filename', function (value) {\n          const isValid = validateName(value);\n          return isValid ? true : this.createError({ message: validateNameError(value) });\n        })\n        .required('Name is required')\n        .test('duplicate-name', 'Global environment already exists', validateEnvironmentName)\n    }),\n    onSubmit: (values) => {\n      dispatch(addGlobalEnvironment({ name: values.name }))\n        .then(() => {\n          toast.success('Global environment created!');\n          onClose();\n          // Call the callback if provided\n          if (onEnvironmentCreated) {\n            onEnvironmentCreated();\n          }\n        })\n        .catch(() => toast.error('An error occurred while creating the environment'));\n    }\n  });\n\n  useEffect(() => {\n    if (inputRef && inputRef.current) {\n      inputRef.current.focus();\n    }\n  }, [inputRef]);\n\n  const onSubmit = () => {\n    formik.handleSubmit();\n  };\n\n  return (\n    <Portal>\n      <Modal\n        size=\"md\"\n        title=\"Create Global Environment\"\n        confirmText=\"Create\"\n        handleConfirm={onSubmit}\n        handleCancel={onClose}\n      >\n        <form className=\"bruno-form\" onSubmit={(e) => e.preventDefault()}>\n          <div>\n            <label htmlFor=\"environment-name\" className=\"block font-semibold\">\n              Environment Name\n            </label>\n            <div className=\"flex items-center mt-2\">\n              <input\n                id=\"environment-name\"\n                type=\"text\"\n                name=\"name\"\n                ref={inputRef}\n                className=\"block textbox w-full\"\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n                onChange={formik.handleChange}\n                value={formik.values.name || ''}\n              />\n            </div>\n            {formik.touched.name && formik.errors.name ? (\n              <div className=\"text-red-500\">{formik.errors.name}</div>\n            ) : null}\n          </div>\n        </form>\n      </Modal>\n    </Portal>\n  );\n};\n\nexport default CreateEnvironment;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/DeleteEnvironment/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  button.submit {\n    color: white;\n    background-color: var(--color-background-danger) !important;\n    border: inherit !important;\n\n    &:hover {\n      border: inherit !important;\n    }\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/DeleteEnvironment/index.js",
    "content": "import React from 'react';\nimport Portal from 'components/Portal/index';\nimport toast from 'react-hot-toast';\nimport Modal from 'components/Modal/index';\nimport { useDispatch } from 'react-redux';\nimport StyledWrapper from './StyledWrapper';\nimport { deleteGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';\n\nconst DeleteEnvironment = ({ onClose, environment }) => {\n  const dispatch = useDispatch();\n  const onConfirm = () => {\n    dispatch(deleteGlobalEnvironment({ environmentUid: environment.uid }))\n      .then(() => {\n        toast.success('Environment deleted successfully');\n        onClose();\n      })\n      .catch(() => toast.error('An error occurred while deleting the environment'));\n  };\n\n  return (\n    <Portal>\n      <StyledWrapper>\n        <Modal\n          size=\"sm\"\n          title=\"Delete Environment\"\n          confirmText=\"Delete\"\n          handleConfirm={onConfirm}\n          handleCancel={onClose}\n        >\n          Are you sure you want to delete <span className=\"font-semibold\">{environment.name}</span> ?\n        </Modal>\n      </StyledWrapper>\n    </Portal>\n  );\n};\n\nexport default DeleteEnvironment;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/ConfirmSwitchEnv.js",
    "content": "import React from 'react';\nimport { IconAlertTriangle } from '@tabler/icons';\nimport Modal from 'components/Modal';\nimport { createPortal } from 'react-dom';\nimport Button from 'ui/Button';\nimport { useTheme } from 'providers/Theme';\n\nconst ConfirmSwitchEnv = ({ onCancel }) => {\n  const { theme } = useTheme();\n  const warningColor = theme.status.warning.text;\n\n  const modalContent = (\n    <Modal\n      size=\"md\"\n      title=\"Unsaved changes\"\n      disableEscapeKey={true}\n      disableCloseOnOutsideClick={true}\n      closeModalFadeTimeout={150}\n      handleCancel={onCancel}\n      onClick={(e) => {\n        e.stopPropagation();\n        e.preventDefault();\n      }}\n      hideFooter={true}\n    >\n      <div className=\"flex items-center font-normal\">\n        <IconAlertTriangle color={warningColor} size={32} strokeWidth={1.5} />\n        <h1 className=\"ml-2 text-lg font-semibold\">Hold on..</h1>\n      </div>\n      <div className=\"font-normal mt-4\">You have unsaved changes in this environment.</div>\n\n      <div className=\"flex justify-end mt-6\">\n        <div>\n          <Button color=\"warning\" onClick={onCancel}>\n            Close\n          </Button>\n        </div>\n      </div>\n    </Modal>\n  );\n\n  return createPortal(modalContent, document.body);\n};\n\nexport default ConfirmSwitchEnv;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/EnvironmentVariables/index.js",
    "content": "import React, { useCallback, useRef, useMemo } from 'react';\nimport { TableVirtuoso } from 'react-virtuoso';\nimport cloneDeep from 'lodash/cloneDeep';\nimport { useDispatch, useSelector } from 'react-redux';\nimport {\n  saveGlobalEnvironment,\n  setGlobalEnvironmentDraft,\n  clearGlobalEnvironmentDraft\n} from 'providers/ReduxStore/slices/global-environments';\nimport EnvironmentVariablesTable from 'components/EnvironmentVariablesTable';\n\nconst EnvironmentVariables = ({ environment, setIsModified, collection, searchQuery = '' }) => {\n  const dispatch = useDispatch();\n  const { globalEnvironmentDraft } = useSelector((state) => state.globalEnvironments);\n\n  const hasDraftForThisEnv = globalEnvironmentDraft?.environmentUid === environment.uid;\n\n  const handleSave = useCallback(\n    (variables) => {\n      return dispatch(saveGlobalEnvironment({ environmentUid: environment.uid, variables: cloneDeep(variables) }));\n    },\n    [dispatch, environment.uid]\n  );\n\n  const handleDraftChange = useCallback(\n    (variables) => {\n      dispatch(\n        setGlobalEnvironmentDraft({\n          environmentUid: environment.uid,\n          variables\n        })\n      );\n    },\n    [dispatch, environment.uid]\n  );\n\n  const handleDraftClear = useCallback(() => {\n    dispatch(clearGlobalEnvironmentDraft());\n  }, [dispatch]);\n\n  return (\n    <EnvironmentVariablesTable\n      environment={environment}\n      collection={collection}\n      onSave={handleSave}\n      draft={hasDraftForThisEnv ? globalEnvironmentDraft : null}\n      onDraftChange={handleDraftChange}\n      onDraftClear={handleDraftClear}\n      setIsModified={setIsModified}\n      searchQuery={searchQuery}\n    />\n  );\n};\n\nexport default EnvironmentVariables;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n  background: ${(props) => props.theme.bg};\n\n  .header {\n    position: relative;\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 9px 20px 8px 20px;\n    flex-shrink: 0;\n    \n    .title {\n      font-size: ${(props) => props.theme.font.size.base};\n      font-weight: 500;\n      color: ${(props) => props.theme.text};\n      margin: 0;\n    }\n    \n    .title-container {\n      display: flex;\n      align-items: center;\n      gap: 8px;\n      flex: 1;\n      \n      &.renaming {\n        .title-input {\n          flex: 1;\n          background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n          outline: none;\n          color: ${(props) => props.theme.text};\n          font-size: 15px;\n          font-weight: 600;\n          padding: 4px 8px;\n          border-radius: 5px;\n        }\n        \n        .inline-actions {\n          display: flex;\n          gap: 2px;\n        }\n        \n        .inline-action-btn {\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          width: 26px;\n          height: 26px;\n          padding: 0;\n          background: transparent;\n          border: none;\n          border-radius: 4px;\n          cursor: pointer;\n          transition: all 0.15s ease;\n          \n          &.save {\n            color: ${(props) => props.theme.textLink};\n            \n            &:hover {\n              background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n            }\n          }\n          \n          &.cancel {\n            color: ${(props) => props.theme.colors.text.muted};\n            \n            &:hover {\n              background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n              color: ${(props) => props.theme.text};\n            }\n          }\n        }\n      }\n    }\n    \n    .title-error {\n      position: absolute;\n      top: 100%;\n      left: 20px;\n      margin-top: 4px;\n      padding: 4px 8px;\n      font-size: 11px;\n      color: ${(props) => props.theme.colors.text.danger};\n      background: ${(props) => props.theme.bg};\n      border: 1px solid ${(props) => props.theme.colors.text.danger};\n      border-radius: 4px;\n      white-space: nowrap;\n    }\n    \n    .actions {\n      display: flex;\n      align-items: center;\n      gap: 2px;\n\n      .search-input-wrapper {\n        position: relative;\n        display: flex;\n        align-items: center;\n\n        .search-icon {\n          position: absolute;\n          left: 8px;\n          color: ${(props) => props.theme.colors.text.muted};\n          pointer-events: none;\n        }\n\n        .search-input {\n          width: 200px;\n          padding: 5px 32px 5px 32px;\n          border: 1px solid ${(props) => props.theme.input.border};\n          border-radius: ${(props) => props.theme.border.radius.sm};\n          background: ${(props) => props.theme.input.bg};\n          color: ${(props) => props.theme.text};\n          font-size: ${(props) => props.theme.font.size.base};\n          outline: none;\n          transition: border-color 0.15s ease;\n\n          &:focus {\n            border-color: ${(props) => props.theme.input.focusBorder};\n          }\n\n          &::placeholder {\n            color: ${(props) => props.theme.input.placeholder.color};\n            opacity: ${(props) => props.theme.input.placeholder.opacity};\n          }\n        }\n\n        .clear-search {\n          position: absolute;\n          right: 1px;\n          padding: 4px;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          color: ${(props) => props.theme.colors.text.muted};\n          background: transparent;\n          border: none;\n          cursor: pointer;\n          border-radius: ${(props) => props.theme.border.radius.sm};\n          transition: all 0.15s ease;\n\n          &:hover {\n            color: ${(props) => props.theme.text};\n            background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n          }\n        }\n      }\n\n      button {\n        display: inline-flex;\n        align-items: center;\n        justify-content: center;\n        width: 28px;\n        height: 28px;\n        padding: 0;\n        color: ${(props) => props.theme.colors.text.muted};\n        background: transparent;\n        border: none;\n        border-radius: 5px;\n        cursor: pointer;\n        transition: all 0.15s ease;\n        \n        &:hover {\n          background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n          color: ${(props) => props.theme.text};\n        }\n        \n        &:last-child:hover {\n          color: ${(props) => props.theme.colors.text.danger};\n        }\n      }\n    }\n  }\n  \n  .content {\n    flex: 1;\n    overflow: hidden;\n    display: flex;\n    flex-direction: column;\n    padding: 0 20px 20px 20px;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/EnvironmentDetails/index.js",
    "content": "import { IconCopy, IconEdit, IconTrash, IconCheck, IconX, IconSearch } from '@tabler/icons';\nimport { useState, useRef } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { renameGlobalEnvironment, updateGlobalEnvironmentColor } from 'providers/ReduxStore/slices/global-environments';\nimport { validateName, validateNameError } from 'utils/common/regex';\nimport toast from 'react-hot-toast';\nimport CopyEnvironment from '../../CopyEnvironment';\nimport DeleteEnvironment from '../../DeleteEnvironment';\nimport EnvironmentVariables from './EnvironmentVariables';\nimport ColorPicker from 'components/ColorPicker';\nimport StyledWrapper from './StyledWrapper';\n\nconst EnvironmentDetails = ({ environment, setIsModified, collection, searchQuery, setSearchQuery, isSearchExpanded, setIsSearchExpanded, debouncedSearchQuery, searchInputRef }) => {\n  const dispatch = useDispatch();\n  const globalEnvs = useSelector((state) => state?.globalEnvironments?.globalEnvironments);\n\n  const [openDeleteModal, setOpenDeleteModal] = useState(false);\n  const [openCopyModal, setOpenCopyModal] = useState(false);\n  const [isRenaming, setIsRenaming] = useState(false);\n  const [newName, setNewName] = useState('');\n  const [nameError, setNameError] = useState('');\n  const inputRef = useRef(null);\n\n  const validateEnvironmentName = (name) => {\n    if (!name || name.trim() === '') {\n      return 'Name is required';\n    }\n\n    if (name.length < 1) {\n      return 'Must be at least 1 character';\n    }\n\n    if (name.length > 255) {\n      return 'Must be 255 characters or less';\n    }\n\n    if (!validateName(name)) {\n      return validateNameError(name);\n    }\n\n    const trimmedName = name.toLowerCase().trim();\n    const isDuplicate = (globalEnvs || []).some((env) =>\n      env?.uid !== environment.uid && env?.name?.toLowerCase().trim() === trimmedName);\n    if (isDuplicate) {\n      return 'Environment already exists';\n    }\n\n    return null;\n  };\n\n  const handleRenameClick = () => {\n    setIsRenaming(true);\n    setNewName(environment.name);\n    setNameError('');\n    setTimeout(() => {\n      inputRef.current?.focus();\n      inputRef.current?.select();\n    }, 50);\n  };\n\n  const handleSaveRename = () => {\n    const error = validateEnvironmentName(newName);\n    if (error) {\n      setNameError(error);\n      return;\n    }\n\n    dispatch(renameGlobalEnvironment({ name: newName, environmentUid: environment.uid }))\n      .then(() => {\n        toast.success('Environment renamed!');\n        setIsRenaming(false);\n        setNewName('');\n        setNameError('');\n      })\n      .catch(() => {\n        toast.error('An error occurred while renaming the environment');\n      });\n  };\n\n  const handleCancelRename = () => {\n    setIsRenaming(false);\n    setNewName('');\n    setNameError('');\n  };\n\n  const handleNameChange = (e) => {\n    setNewName(e.target.value);\n    if (nameError) {\n      setNameError('');\n    }\n  };\n\n  const handleNameBlur = () => {\n    if (newName.trim() === '') {\n      handleCancelRename();\n    } else {\n      const error = validateEnvironmentName(newName);\n      if (error) {\n        setNameError(error);\n      }\n    }\n  };\n\n  const handleNameKeyDown = (e) => {\n    if (e.key === 'Enter') {\n      e.preventDefault();\n      handleSaveRename();\n    } else if (e.key === 'Escape') {\n      e.preventDefault();\n      handleCancelRename();\n    }\n  };\n\n  const handleSearchIconClick = () => {\n    setIsSearchExpanded(true);\n    setTimeout(() => {\n      searchInputRef.current?.focus();\n    }, 50);\n  };\n\n  const handleClearSearch = () => {\n    setSearchQuery('');\n  };\n\n  const handleSearchBlur = () => {\n    if (searchQuery === '') {\n      setIsSearchExpanded(false);\n    }\n  };\n\n  const handleColorChange = (color) => {\n    dispatch(updateGlobalEnvironmentColor(environment.uid, color));\n  };\n\n  return (\n    <StyledWrapper>\n      {openDeleteModal && (\n        <DeleteEnvironment\n          onClose={() => setOpenDeleteModal(false)}\n          environment={environment}\n        />\n      )}\n      {openCopyModal && (\n        <CopyEnvironment onClose={() => setOpenCopyModal(false)} environment={environment} />\n      )}\n\n      <div className=\"header\">\n        <div className={`title-container ${isRenaming ? 'renaming' : ''}`}>\n          {isRenaming ? (\n            <>\n              <input\n                ref={inputRef}\n                type=\"text\"\n                className=\"title-input\"\n                value={newName}\n                onChange={handleNameChange}\n                onBlur={handleNameBlur}\n                onKeyDown={handleNameKeyDown}\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n              />\n              <div className=\"inline-actions\">\n                <button\n                  className=\"inline-action-btn save\"\n                  onClick={handleSaveRename}\n                  onMouseDown={(e) => e.preventDefault()}\n                  title=\"Save\"\n                >\n                  <IconCheck size={14} strokeWidth={2} />\n                </button>\n                <button\n                  className=\"inline-action-btn cancel\"\n                  onClick={handleCancelRename}\n                  onMouseDown={(e) => e.preventDefault()}\n                  title=\"Cancel\"\n                >\n                  <IconX size={14} strokeWidth={2} />\n                </button>\n              </div>\n            </>\n          ) : (\n            <div className=\"flex items-center gap-2\">\n              <h2 className=\"title\">{environment.name}</h2>\n              <ColorPicker color={environment.color} onChange={handleColorChange} />\n            </div>\n          )}\n        </div>\n        {nameError && isRenaming && <div className=\"title-error\">{nameError}</div>}\n        <div className=\"actions\">\n          {isSearchExpanded ? (\n            <div className=\"search-input-wrapper\">\n              <IconSearch size={14} strokeWidth={1.5} className=\"search-icon\" />\n              <input\n                ref={searchInputRef}\n                type=\"text\"\n                placeholder=\"Search variables...\"\n                value={searchQuery}\n                onChange={(e) => setSearchQuery(e.target.value)}\n                onBlur={handleSearchBlur}\n                className=\"search-input\"\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                autoCapitalize=\"off\"\n                spellCheck=\"false\"\n              />\n              {searchQuery && (\n                <button\n                  className=\"clear-search\"\n                  onClick={handleClearSearch}\n                  onMouseDown={(e) => e.preventDefault()}\n                  title=\"Clear search\"\n                >\n                  <IconX size={14} strokeWidth={1.5} />\n                </button>\n              )}\n            </div>\n          ) : (\n            <button onClick={handleSearchIconClick} title=\"Search variables\">\n              <IconSearch size={15} strokeWidth={1.5} />\n            </button>\n          )}\n          <button onClick={handleRenameClick} title=\"Rename\">\n            <IconEdit size={15} strokeWidth={1.5} />\n          </button>\n          <button onClick={() => setOpenCopyModal(true)} title=\"Copy\">\n            <IconCopy size={15} strokeWidth={1.5} />\n          </button>\n          <button onClick={() => setOpenDeleteModal(true)} title=\"Delete\">\n            <IconTrash size={15} strokeWidth={1.5} />\n          </button>\n        </div>\n      </div>\n\n      <div className=\"content\">\n        <EnvironmentVariables\n          environment={environment}\n          setIsModified={setIsModified}\n          collection={collection}\n          searchQuery={debouncedSearchQuery}\n        />\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default EnvironmentDetails;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/StyledWrapper.js",
    "content": "import styled from 'styled-components';\nimport { rgba } from 'polished';\n\nconst StyledWrapper = styled.div`\n  display: flex;\n  height: 100%;\n  overflow: hidden;\n  position: relative;\n\n  .environments-container {\n    display: flex;\n    height: 100%;\n    width: 100%;\n    overflow: hidden;\n  }\n\n  .confirm-switch-overlay {\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    z-index: 10;\n    background: ${(props) => props.theme.bg};\n    padding: 12px;\n  }\n\n  /* Left Sidebar */\n  .sidebar {\n    width: 240px;\n    min-width: 240px;\n    display: flex;\n    flex-direction: column;\n  }\n\n\n    .btn-action {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      width: 24px;\n      height: 24px;\n      padding: 0;\n      background: transparent;\n      border: none;\n      border-radius: 4px;\n      color: ${(props) => props.theme.colors.text.muted};\n      cursor: pointer;\n      transition: all 0.15s ease;\n      \n      &:hover {\n        background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n        color: ${(props) => props.theme.text};\n      }\n    }\n  }\n\n  .env-list-search {\n    position: relative;\n    display: flex;\n    align-items: center;\n    margin: 0 4px 6px 4px;\n\n    .env-list-search-icon {\n      position: absolute;\n      left: 8px;\n      color: ${(props) => props.theme.colors.text.muted};\n      pointer-events: none;\n    }\n\n    .env-list-search-input {\n      width: 100%;\n      padding: 5px 24px 5px 26px;\n      font-size: 12px;\n      background: transparent;\n      border: 1px solid ${(props) => props.theme.border.border1};\n      border-radius: 6px;\n      color: ${(props) => props.theme.text};\n      transition: border-color 0.15s ease;\n\n      &::placeholder {\n        color: ${(props) => props.theme.colors.text.muted};\n      }\n      \n      &:focus {\n        outline: none;\n        border-color: ${(props) => props.theme.colors.accent};\n      }\n    }\n\n    .env-list-search-clear {\n      position: absolute;\n      right: 4px;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      padding: 2px;\n      background: transparent;\n      border: none;\n      cursor: pointer;\n      color: ${(props) => props.theme.colors.text.muted};\n      border-radius: 3px;\n\n      &:hover {\n        color: ${(props) => props.theme.text};\n      }\n    }\n  }\n\n  .sections-container {\n    flex: 1;\n    display: flex;\n    flex-direction: column;\n    overflow: hidden;\n    padding: 8px;\n  }\n\n  .section-header {\n    margin-inline: 4px;\n    padding-left: 6px;\n    border-radius: 6px;\n    padding-right: 3px;\n    padding-block: 4px;\n  }\n\n  .environments-list {\n    overflow-y: auto;\n    padding: 0 4px;\n  }\n\n  .btn-action {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 22px;\n    height: 22px;\n    padding: 0;\n    background: transparent;\n    border: none;\n    border-radius: 4px;\n    color: ${(props) => props.theme.colors.text.muted};\n    cursor: pointer;\n    transition: all 0.15s ease;\n\n    &:hover {\n      background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n      color: ${(props) => props.theme.text};\n    }\n\n    &.active {\n      color: ${(props) => props.theme.colors.accent};\n    }\n  }\n\n  .environment-item {\n    position: relative;\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    gap: 8px;\n    padding: 4px 8px;\n    margin-bottom: 1px;\n    font-size: 13px;\n    color: ${(props) => props.theme.text};\n    cursor: pointer;\n    border-radius: 6px;\n    transition: background 0.15s ease;\n    \n    .environment-name {\n      flex: 1;\n      white-space: nowrap;\n      overflow: hidden;\n      text-overflow: ellipsis;\n    }\n\n    .environment-actions {\n      display: flex;\n      align-items: center;\n      opacity: 0;\n      transition: opacity 0.15s ease;\n\n      .activate-btn {\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        padding: 4px;\n        border: none;\n        background: transparent;\n        cursor: pointer;\n        color: ${(props) => props.theme.text.muted};\n        border-radius: 3px;\n        transition: all 0.15s ease;\n\n        &:hover {\n          background: ${(props) => props.theme.workspace.button.bg};\n          color: ${(props) => props.theme.colors.text.green};\n        }\n      }\n\n      .activated-checkmark {\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        padding: 4px;\n        color: ${(props) => props.theme.colors.text.green};\n        opacity: 1;\n      }\n    }\n\n    &:hover .environment-actions {\n      opacity: 1;\n    }\n\n    &.activated .environment-actions {\n      opacity: 1;\n    }\n\n    &:hover {\n      background: ${(props) => props.theme.workspace.button.bg};\n    }\n    \n    &.active {\n      background: ${(props) => props.theme.background.surface0};\n      color: ${(props) => props.theme.text};\n    }\n    \n    &.renaming,\n    &.creating {\n      cursor: default;\n      padding: 4px 4px 4px 8px;\n      background: ${(props) => props.theme.sidebar.collection.item.hoverBg};\n      \n      &:hover {\n        background: ${(props) => props.theme.workspace.button.bg};\n      }\n    }\n\n    .rename-container {\n      display: flex;\n      align-items: center;\n      flex: 1;\n      min-width: 0;\n      overflow: hidden;\n      \n      .environment-name-input {\n        flex: 1;\n        min-width: 0;\n        background: transparent;\n        border: none;\n        outline: none;\n        color: ${(props) => props.theme.text};\n        font-size: 13px;\n        padding: 2px 4px;\n        \n        &::placeholder {\n          color: ${(props) => props.theme.colors.text.muted};\n        }\n      }\n      \n      .inline-actions {\n        display: flex;\n        gap: 2px;\n        margin-left: 4px;\n        flex-shrink: 0;\n      }\n    }\n\n    &.creating {\n      .environment-name-input {\n        flex: 1;\n        min-width: 0;\n        background: transparent;\n        border: none;\n        outline: none;\n        color: ${(props) => props.theme.text};\n        font-size: 13px;\n        padding: 2px 4px;\n\n        &::placeholder {\n          color: ${(props) => props.theme.colors.text.muted};\n        }\n      }\n\n      .inline-actions {\n        display: flex;\n        gap: 2px;\n        margin-left: 4px;\n        flex-shrink: 0;\n      }\n    }\n\n    .inline-action-btn {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      width: 22px;\n      height: 22px;\n      padding: 0;\n      background: transparent;\n      border: none;\n      border-radius: 4px;\n      cursor: pointer;\n      transition: all 0.15s ease;\n\n      &.save {\n        color: ${(props) => props.theme.colors.text.green};\n\n        &:hover {\n          background: ${(props) => rgba(props.theme.colors.text.green, 0.1)};\n        }\n      }\n\n      &.cancel {\n        color: ${(props) => props.theme.colors.text.danger};\n\n        &:hover {\n          background: ${(props) => rgba(props.theme.colors.text.danger, 0.1)};\n        }\n      }\n    }\n  }\n  \n  .env-error {\n    padding: 4px 12px;\n    margin-top: 4px;\n    font-size: 11px;\n    color: ${(props) => props.theme.colors.text.danger};\n    background: ${(props) => `${props.theme.colors.text.danger}15`};\n    border-radius: 4px;\n  }\n\n  .no-env-file {\n    padding: 8px 12px;\n    font-size: 12px;\n    color: ${(props) => props.theme.colors.text.muted};\n    font-style: italic;\n  }\n\n  .empty-state {\n    flex: 1;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    padding-top: 10%;\n    color: ${(props) => props.theme.colors.text.muted};\n\n    svg {\n      opacity: 0.3;\n      margin-bottom: 8px;\n    }\n\n    .title {\n      font-size: 13px;\n      font-weight: 500;\n      margin-bottom: 12px;\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n\n    .actions {\n      display: flex;\n      gap: 8px;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/EnvironmentList/index.js",
    "content": "import React, { useEffect, useState, useRef, useCallback } from 'react';\nimport usePrevious from 'hooks/usePrevious';\nimport useOnClickOutside from 'hooks/useOnClickOutside';\nimport useDebounce from 'hooks/useDebounce';\nimport EnvironmentDetails from './EnvironmentDetails';\nimport { IconDownload, IconUpload, IconSearch, IconPlus, IconCheck, IconX, IconFileAlert } from '@tabler/icons';\nimport Button from 'ui/Button';\nimport StyledWrapper from './StyledWrapper';\nimport ConfirmSwitchEnv from './ConfirmSwitchEnv';\nimport ImportEnvironmentModal from 'components/Environments/Common/ImportEnvironmentModal';\nimport CollapsibleSection from 'components/Environments/CollapsibleSection';\nimport DotEnvFileEditor from 'components/Environments/DotEnvFileEditor';\nimport DotEnvFileDetails from 'components/Environments/DotEnvFileDetails';\nimport ColorBadge from 'components/ColorBadge';\nimport { isEqual } from 'lodash';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { addGlobalEnvironment, renameGlobalEnvironment, selectGlobalEnvironment, setGlobalEnvironmentDraft, clearGlobalEnvironmentDraft } from 'providers/ReduxStore/slices/global-environments';\nimport {\n  saveWorkspaceDotEnvVariables,\n  saveWorkspaceDotEnvRaw,\n  createWorkspaceDotEnvFile,\n  deleteWorkspaceDotEnvFile\n} from 'providers/ReduxStore/slices/workspaces/actions';\nimport { setEnvVarSearchQuery, setEnvVarSearchExpanded } from 'providers/ReduxStore/slices/app';\nimport { validateName, validateNameError } from 'utils/common/regex';\nimport toast from 'react-hot-toast';\nimport classnames from 'classnames';\n\nconst EMPTY_ARRAY = [];\n\nconst EnvironmentList = ({\n  environments,\n  activeEnvironmentUid,\n  selectedEnvironment,\n  setSelectedEnvironment,\n  isModified,\n  setIsModified,\n  collection,\n  workspace,\n  setShowExportModal\n}) => {\n  const dispatch = useDispatch();\n  const globalEnvs = useSelector((state) => state?.globalEnvironments?.globalEnvironments);\n  const envSearchQuery = useSelector((state) => state.app.envVarSearch?.global?.query ?? '');\n  const isEnvSearchExpanded = useSelector((state) => state.app.envVarSearch?.global?.expanded ?? false);\n  const setEnvSearchQuery = (q) => dispatch(setEnvVarSearchQuery({ context: 'global', query: q }));\n  const setIsEnvSearchExpanded = (v) => dispatch(setEnvVarSearchExpanded({ context: 'global', expanded: v }));\n\n  const [openImportModal, setOpenImportModal] = useState(false);\n  const [searchText, setSearchText] = useState('');\n  const envListSearchInputRef = useRef(null);\n  const [isCreatingInline, setIsCreatingInline] = useState(false);\n  const [renamingEnvUid, setRenamingEnvUid] = useState(null);\n  const [newEnvName, setNewEnvName] = useState('');\n  const [envNameError, setEnvNameError] = useState('');\n  const inputRef = useRef(null);\n  const renameContainerRef = useRef(null);\n  const createContainerRef = useRef(null);\n\n  const [switchEnvConfirmClose, setSwitchEnvConfirmClose] = useState(false);\n  const [originalEnvironmentVariables, setOriginalEnvironmentVariables] = useState([]);\n  const [environmentsExpanded, setEnvironmentsExpanded] = useState(true);\n  const [dotEnvExpanded, setDotEnvExpanded] = useState(false);\n  const [activeView, setActiveView] = useState('environment');\n  const [isDotEnvModified, setIsDotEnvModified] = useState(false);\n  const [dotEnvViewMode, setDotEnvViewMode] = useState('table');\n  const [selectedDotEnvFile, setSelectedDotEnvFile] = useState(null);\n  const [isCreatingDotEnvInline, setIsCreatingDotEnvInline] = useState(false);\n  const [newDotEnvName, setNewDotEnvName] = useState('.env');\n  const [dotEnvNameError, setDotEnvNameError] = useState('');\n  const dotEnvInputRef = useRef(null);\n  const dotEnvCreateContainerRef = useRef(null);\n\n  const debouncedEnvSearchQuery = useDebounce(envSearchQuery, 300);\n  const envSearchInputRef = useRef(null);\n\n  const dotEnvFiles = useSelector((state) => {\n    const ws = state.workspaces.workspaces.find((w) => w.uid === workspace?.uid);\n    return ws?.dotEnvFiles || EMPTY_ARRAY;\n  });\n\n  const envUids = environments ? environments.map((env) => env.uid) : [];\n  const prevEnvUids = usePrevious(envUids);\n\n  const globalEnvironmentDraftUid = useSelector((state) => state.globalEnvironments.globalEnvironmentDraft?.environmentUid);\n\n  const handleDotEnvModifiedChange = useCallback((modified) => {\n    setIsDotEnvModified(modified);\n    if (modified) {\n      dispatch(setGlobalEnvironmentDraft({\n        environmentUid: `dotenv:${selectedDotEnvFile}`,\n        variables: []\n      }));\n    } else if (globalEnvironmentDraftUid?.startsWith('dotenv:')) {\n      dispatch(clearGlobalEnvironmentDraft());\n    }\n  }, [dispatch, selectedDotEnvFile, globalEnvironmentDraftUid]);\n\n  useEffect(() => {\n    if (dotEnvFiles.length === 0) {\n      setSelectedDotEnvFile(null);\n      handleDotEnvModifiedChange(false);\n      return;\n    }\n\n    const fileExists = dotEnvFiles.some((f) => f.filename === selectedDotEnvFile);\n    if (!selectedDotEnvFile || !fileExists) {\n      setSelectedDotEnvFile(dotEnvFiles[0].filename);\n    }\n  }, [dotEnvFiles]);\n\n  useEffect(() => {\n    if (!environments?.length) {\n      setSelectedEnvironment(null);\n      setOriginalEnvironmentVariables([]);\n      return;\n    }\n\n    if (selectedEnvironment) {\n      let _selectedEnvironment = environments?.find((env) => env?.uid === selectedEnvironment?.uid);\n\n      if (!_selectedEnvironment) {\n        _selectedEnvironment = environments?.find((env) => env?.name === selectedEnvironment?.name);\n      }\n\n      if (!_selectedEnvironment) {\n        _selectedEnvironment = environments?.find((env) => env.uid === activeEnvironmentUid) || environments?.[0];\n      }\n\n      const hasSelectedEnvironmentChanged = !isEqual(selectedEnvironment, _selectedEnvironment);\n      if (hasSelectedEnvironmentChanged || selectedEnvironment.uid !== _selectedEnvironment?.uid) {\n        setSelectedEnvironment(_selectedEnvironment);\n      }\n      setOriginalEnvironmentVariables(_selectedEnvironment?.variables || []);\n      return;\n    }\n\n    const environment = environments?.find((env) => env.uid === activeEnvironmentUid) || environments?.[0];\n\n    setSelectedEnvironment(environment);\n    setOriginalEnvironmentVariables(environment?.variables || []);\n  }, [environments, activeEnvironmentUid, selectedEnvironment]);\n\n  useEffect(() => {\n    if (prevEnvUids && prevEnvUids.length && envUids.length > prevEnvUids.length) {\n      const newEnv = environments.find((env) => !prevEnvUids.includes(env.uid));\n      if (newEnv) {\n        setSelectedEnvironment(newEnv);\n      }\n    }\n\n    if (prevEnvUids && prevEnvUids.length && envUids.length < prevEnvUids.length) {\n      setSelectedEnvironment(environments && environments.length ? environments[0] : null);\n    }\n  }, [envUids, environments, prevEnvUids]);\n\n  const handleEnvironmentClick = (env) => {\n    if (activeView === 'dotenv' && isDotEnvModified) {\n      setSwitchEnvConfirmClose(true);\n      return;\n    }\n    if (!isModified) {\n      setSelectedEnvironment(env);\n      setActiveView('environment');\n      setEnvironmentsExpanded(true);\n    } else {\n      setSwitchEnvConfirmClose(true);\n    }\n  };\n\n  const handleDotEnvClick = (filename) => {\n    if (isModified) {\n      setSwitchEnvConfirmClose(true);\n      return;\n    }\n    if (activeView === 'dotenv' && isDotEnvModified && selectedDotEnvFile !== filename) {\n      setSwitchEnvConfirmClose(true);\n      return;\n    }\n    setSelectedDotEnvFile(filename);\n    setActiveView('dotenv');\n    setDotEnvExpanded(true);\n  };\n\n  const handleEnvironmentDoubleClick = (env) => {\n    setRenamingEnvUid(env.uid);\n    setNewEnvName(env.name);\n    setEnvNameError('');\n    setTimeout(() => {\n      inputRef.current?.focus();\n      inputRef.current?.select();\n    }, 50);\n  };\n\n  const handleActivateEnvironment = useCallback((e, env) => {\n    e.stopPropagation();\n    dispatch(selectGlobalEnvironment({ environmentUid: env.uid }))\n      .then(() => {\n        toast.success(`Environment \"${env.name}\" activated`);\n      })\n      .catch(() => {\n        toast.error('Failed to activate environment');\n      });\n  }, [dispatch]);\n\n  const validateEnvironmentName = (name, excludeUid = null) => {\n    if (!name || name.trim() === '') {\n      return 'Name is required';\n    }\n\n    if (!validateName(name)) {\n      return validateNameError(name);\n    }\n\n    const trimmedName = name.toLowerCase().trim();\n    const isDuplicate = globalEnvs?.some(\n      (env) => env?.uid !== excludeUid && env?.name?.toLowerCase().trim() === trimmedName\n    );\n    if (isDuplicate) {\n      return 'Environment already exists';\n    }\n\n    return null;\n  };\n\n  const handleCreateEnvClick = () => {\n    if (!isModified && !isDotEnvModified) {\n      setIsCreatingInline(true);\n      setNewEnvName('');\n      setEnvNameError('');\n      setTimeout(() => {\n        inputRef.current?.focus();\n      }, 50);\n    } else {\n      setSwitchEnvConfirmClose(true);\n    }\n  };\n\n  const handleCancelCreate = useCallback(() => {\n    setIsCreatingInline(false);\n    setNewEnvName('');\n    setEnvNameError('');\n  }, []);\n\n  useOnClickOutside(createContainerRef, handleCancelCreate, isCreatingInline);\n\n  const handleSaveNewEnv = () => {\n    const error = validateEnvironmentName(newEnvName);\n    if (error) {\n      setEnvNameError(error);\n      return;\n    }\n\n    dispatch(addGlobalEnvironment({ name: newEnvName }))\n      .then(() => {\n        toast.success('Environment created!');\n        setIsCreatingInline(false);\n        setNewEnvName('');\n        setEnvNameError('');\n      })\n      .catch(() => {\n        toast.error('An error occurred while creating the environment');\n      });\n  };\n\n  const handleEnvNameChange = (e) => {\n    const value = e.target.value;\n    setNewEnvName(value);\n\n    if (envNameError) {\n      setEnvNameError('');\n    }\n  };\n\n  const handleEnvNameKeyDown = (e) => {\n    if (e.key === 'Enter') {\n      e.preventDefault();\n      if (renamingEnvUid) {\n        handleSaveRename();\n      } else {\n        handleSaveNewEnv();\n      }\n    } else if (e.key === 'Escape') {\n      e.preventDefault();\n      if (renamingEnvUid) {\n        handleCancelRename();\n      } else {\n        handleCancelCreate();\n      }\n    }\n  };\n\n  const handleSaveRename = () => {\n    const error = validateEnvironmentName(newEnvName, renamingEnvUid);\n    if (error) {\n      setEnvNameError(error);\n      return;\n    }\n\n    dispatch(renameGlobalEnvironment({ name: newEnvName, environmentUid: renamingEnvUid }))\n      .then(() => {\n        toast.success('Environment renamed!');\n        setRenamingEnvUid(null);\n        setNewEnvName('');\n        setEnvNameError('');\n      })\n      .catch(() => {\n        toast.error('An error occurred while renaming the environment');\n      });\n  };\n\n  const handleCancelRename = useCallback(() => {\n    setRenamingEnvUid(null);\n    setNewEnvName('');\n    setEnvNameError('');\n  }, []);\n\n  useOnClickOutside(renameContainerRef, handleCancelRename, !!renamingEnvUid);\n\n  const handleImportClick = () => {\n    if (!isModified && !isDotEnvModified) {\n      setOpenImportModal(true);\n    } else {\n      setSwitchEnvConfirmClose(true);\n    }\n  };\n\n  const handleExportClick = () => {\n    if (setShowExportModal) {\n      setShowExportModal(true);\n    }\n  };\n\n  const handleConfirmSwitch = (saveChanges) => {\n    if (!saveChanges) {\n      setSwitchEnvConfirmClose(false);\n    }\n  };\n\n  const handleSaveDotEnv = (variables) => {\n    if (!selectedDotEnvFile) return Promise.reject(new Error('No file selected'));\n    return dispatch(saveWorkspaceDotEnvVariables(workspace.uid, variables, selectedDotEnvFile));\n  };\n\n  const handleSaveDotEnvRaw = (content) => {\n    if (!selectedDotEnvFile) return Promise.reject(new Error('No file selected'));\n    return dispatch(saveWorkspaceDotEnvRaw(workspace.uid, content, selectedDotEnvFile));\n  };\n\n  const handleCreateDotEnvInlineClick = () => {\n    if (isModified || isDotEnvModified) {\n      setSwitchEnvConfirmClose(true);\n      return;\n    }\n    setIsCreatingDotEnvInline(true);\n    setNewDotEnvName('.env');\n    setDotEnvNameError('');\n    setTimeout(() => {\n      dotEnvInputRef.current?.focus();\n      const input = dotEnvInputRef.current;\n      if (input) {\n        input.setSelectionRange(input.value.length, input.value.length);\n      }\n    }, 50);\n  };\n\n  const handleCancelDotEnvCreate = useCallback(() => {\n    setIsCreatingDotEnvInline(false);\n    setNewDotEnvName('.env');\n    setDotEnvNameError('');\n  }, []);\n\n  useOnClickOutside(dotEnvCreateContainerRef, handleCancelDotEnvCreate, isCreatingDotEnvInline);\n\n  const validateDotEnvName = (name) => {\n    if (!name || name.trim() === '') {\n      return 'Name is required';\n    }\n\n    if (!name.startsWith('.env')) {\n      return 'File name must start with .env';\n    }\n\n    const validPattern = /^\\.env[a-zA-Z0-9._-]*$/;\n    if (!validPattern.test(name)) {\n      return 'Invalid file name';\n    }\n\n    const exists = dotEnvFiles.some((f) => f.filename === name);\n    if (exists) {\n      return 'File already exists';\n    }\n\n    return null;\n  };\n\n  const handleSaveNewDotEnv = () => {\n    const error = validateDotEnvName(newDotEnvName);\n    if (error) {\n      setDotEnvNameError(error);\n      return;\n    }\n\n    dispatch(createWorkspaceDotEnvFile(workspace.uid, newDotEnvName))\n      .then(() => {\n        toast.success(`${newDotEnvName} file created!`);\n        setIsCreatingDotEnvInline(false);\n        setNewDotEnvName('.env');\n        setDotEnvNameError('');\n        setSelectedDotEnvFile(newDotEnvName);\n        setActiveView('dotenv');\n        setDotEnvExpanded(true);\n      })\n      .catch((error) => {\n        toast.error(error.message || 'Failed to create .env file');\n      });\n  };\n\n  const handleDotEnvNameChange = (e) => {\n    const value = e.target.value;\n    if (!value.startsWith('.env')) {\n      setNewDotEnvName('.env');\n    } else {\n      setNewDotEnvName(value);\n    }\n    if (dotEnvNameError) {\n      setDotEnvNameError('');\n    }\n  };\n\n  const handleDotEnvNameKeyDown = (e) => {\n    if (e.key === 'Enter') {\n      e.preventDefault();\n      handleSaveNewDotEnv();\n    } else if (e.key === 'Escape') {\n      e.preventDefault();\n      handleCancelDotEnvCreate();\n    } else if (e.key === 'Backspace') {\n      const input = e.target;\n      if (input.selectionStart <= 4 && input.selectionEnd <= 4) {\n        e.preventDefault();\n      }\n    }\n  };\n\n  const handleDeleteDotEnvFile = (filename) => {\n    dispatch(deleteWorkspaceDotEnvFile(workspace.uid, filename))\n      .then(() => {\n        toast.success(`${filename} file deleted!`);\n        handleDotEnvModifiedChange(false);\n        if (selectedDotEnvFile === filename) {\n          const remainingFiles = dotEnvFiles.filter((f) => f.filename !== filename);\n          if (remainingFiles.length > 0) {\n            setSelectedDotEnvFile(remainingFiles[0].filename);\n          } else {\n            setActiveView('environment');\n            if (environments?.length) {\n              const env = environments.find((e) => e.uid === activeEnvironmentUid) || environments[0];\n              setSelectedEnvironment(env);\n            }\n          }\n        }\n      })\n      .catch((error) => {\n        toast.error(error.message || 'Failed to delete .env file');\n      });\n  };\n\n  const handleDotEnvViewModeChange = (mode) => {\n    setDotEnvViewMode(mode);\n  };\n\n  const filteredEnvironments\n    = environments?.filter((env) => env.name.toLowerCase().includes(searchText.toLowerCase())) || [];\n\n  const selectedDotEnvData = dotEnvFiles.find((f) => f.filename === selectedDotEnvFile);\n\n  const renderContent = () => {\n    if (activeView === 'dotenv' && selectedDotEnvFile && selectedDotEnvData) {\n      return (\n        <DotEnvFileDetails\n          title={selectedDotEnvFile}\n          onDelete={() => handleDeleteDotEnvFile(selectedDotEnvFile)}\n          dotEnvExists={selectedDotEnvData?.exists}\n          viewMode={dotEnvViewMode}\n          onViewModeChange={handleDotEnvViewModeChange}\n        >\n          <DotEnvFileEditor\n            variables={selectedDotEnvData?.variables || []}\n            onSave={handleSaveDotEnv}\n            onSaveRaw={handleSaveDotEnvRaw}\n            isModified={isDotEnvModified}\n            setIsModified={handleDotEnvModifiedChange}\n            dotEnvExists={selectedDotEnvData?.exists}\n            viewMode={dotEnvViewMode}\n          />\n        </DotEnvFileDetails>\n      );\n    }\n\n    if (selectedEnvironment) {\n      return (\n        <EnvironmentDetails\n          environment={selectedEnvironment}\n          setIsModified={setIsModified}\n          originalEnvironmentVariables={originalEnvironmentVariables}\n          collection={collection}\n          searchQuery={envSearchQuery}\n          setSearchQuery={setEnvSearchQuery}\n          isSearchExpanded={isEnvSearchExpanded}\n          setIsSearchExpanded={setIsEnvSearchExpanded}\n          debouncedSearchQuery={debouncedEnvSearchQuery}\n          searchInputRef={envSearchInputRef}\n        />\n      );\n    }\n\n    return (\n      <div className=\"empty-state\">\n        <IconFileAlert size={48} strokeWidth={1.5} />\n        <div className=\"title\">No Environments</div>\n        <div className=\"actions\">\n          <Button size=\"sm\" color=\"secondary\" onClick={() => handleCreateEnvClick()}>\n            Create Environment\n          </Button>\n          <Button size=\"sm\" color=\"secondary\" onClick={() => handleImportClick()}>\n            Import Environment\n          </Button>\n        </div>\n      </div>\n    );\n  };\n\n  return (\n    <StyledWrapper>\n      {openImportModal && <ImportEnvironmentModal type=\"global\" onClose={() => setOpenImportModal(false)} />}\n\n      <div className=\"environments-container\">\n        {switchEnvConfirmClose && (\n          <div className=\"confirm-switch-overlay\">\n            <ConfirmSwitchEnv onCancel={() => handleConfirmSwitch(false)} />\n          </div>\n        )}\n\n        <div className=\"sidebar\">\n\n          <div className=\"sections-container\">\n            <CollapsibleSection\n              title=\"Environments\"\n              expanded={environmentsExpanded}\n              onToggle={() => setEnvironmentsExpanded(!environmentsExpanded)}\n              actions={(\n                <>\n                  <button\n                    type=\"button\"\n                    className=\"btn-action\"\n                    onClick={() => {\n                      if (!environmentsExpanded) {\n                        setEnvironmentsExpanded(true);\n                      }\n                      handleCreateEnvClick();\n                    }}\n                    title=\"Create environment\"\n                  >\n                    <IconPlus size={14} strokeWidth={1.5} />\n                  </button>\n                  <button\n                    type=\"button\"\n                    className=\"btn-action\"\n                    onClick={() => {\n                      if (!environmentsExpanded) {\n                        setEnvironmentsExpanded(true);\n                      }\n                      handleImportClick();\n                    }}\n                    title=\"Import environment\"\n                  >\n                    <IconDownload size={14} strokeWidth={1.5} />\n                  </button>\n                  <button\n                    type=\"button\"\n                    className=\"btn-action\"\n                    onClick={() => {\n                      if (!environmentsExpanded) {\n                        setEnvironmentsExpanded(true);\n                      }\n                      handleExportClick();\n                    }}\n                    title=\"Export environment\"\n                  >\n                    <IconUpload size={14} strokeWidth={1.5} />\n                  </button>\n                </>\n              )}\n            >\n              <div className=\"env-list-search\">\n                <IconSearch size={13} strokeWidth={1.5} className=\"env-list-search-icon\" />\n                <input\n                  ref={envListSearchInputRef}\n                  type=\"text\"\n                  placeholder=\"Search environments...\"\n                  value={searchText}\n                  onChange={(e) => setSearchText(e.target.value)}\n                  className=\"env-list-search-input\"\n                  autoComplete=\"off\"\n                  autoCorrect=\"off\"\n                  autoCapitalize=\"off\"\n                  spellCheck=\"false\"\n                />\n                {searchText && (\n                  <button className=\"env-list-search-clear\" title=\"Clear search\" onClick={() => setSearchText('')} onMouseDown={(e) => e.preventDefault()}>\n                    <IconX size={12} strokeWidth={1.5} />\n                  </button>\n                )}\n              </div>\n              <div className=\"environments-list\">\n                {filteredEnvironments.map((env) => (\n                  <div\n                    key={env.uid}\n                    id={env.uid}\n                    className={classnames('environment-item', {\n                      active: activeView === 'environment' && selectedEnvironment?.uid === env.uid,\n                      renaming: renamingEnvUid === env.uid,\n                      activated: activeEnvironmentUid === env.uid\n                    })}\n                    onClick={() => renamingEnvUid !== env.uid && handleEnvironmentClick(env)}\n                    onDoubleClick={() => handleEnvironmentDoubleClick(env)}\n                  >\n                    {renamingEnvUid === env.uid ? (\n                      <div className=\"rename-container\" ref={renameContainerRef}>\n                        <input\n                          ref={inputRef}\n                          type=\"text\"\n                          className=\"environment-name-input\"\n                          value={newEnvName}\n                          onChange={handleEnvNameChange}\n                          onKeyDown={handleEnvNameKeyDown}\n                          autoComplete=\"off\"\n                          autoCorrect=\"off\"\n                          autoCapitalize=\"off\"\n                          spellCheck=\"false\"\n                        />\n                        <div className=\"inline-actions\">\n                          <button\n                            className=\"inline-action-btn save\"\n                            onClick={handleSaveRename}\n                            onMouseDown={(e) => e.preventDefault()}\n                            title=\"Save\"\n                          >\n                            <IconCheck size={14} strokeWidth={2} />\n                          </button>\n                          <button\n                            className=\"inline-action-btn cancel\"\n                            onClick={handleCancelRename}\n                            onMouseDown={(e) => e.preventDefault()}\n                            title=\"Cancel\"\n                          >\n                            <IconX size={14} strokeWidth={2} />\n                          </button>\n                        </div>\n                      </div>\n                    ) : (\n                      <>\n                        <ColorBadge color={env.color} size={8} />\n                        <span className=\"environment-name\">{env.name}</span>\n                        <div className=\"environment-actions\">\n                          {activeEnvironmentUid === env.uid ? (\n                            <div className=\"activated-checkmark\" title=\"Active environment\">\n                              <IconCheck size={16} strokeWidth={2} />\n                            </div>\n                          ) : (\n                            <button\n                              className=\"activate-btn\"\n                              onClick={(e) => handleActivateEnvironment(e, env)}\n                              title=\"Activate environment\"\n                            >\n                              <IconCheck size={16} strokeWidth={2} />\n                            </button>\n                          )}\n                        </div>\n                      </>\n                    )}\n                  </div>\n                ))}\n\n                {isCreatingInline && (\n                  <div className=\"environment-item creating\" ref={createContainerRef}>\n                    <input\n                      ref={inputRef}\n                      type=\"text\"\n                      className=\"environment-name-input\"\n                      value={newEnvName}\n                      onChange={handleEnvNameChange}\n                      onKeyDown={handleEnvNameKeyDown}\n                      placeholder=\"Environment name...\"\n                      autoComplete=\"off\"\n                      autoCorrect=\"off\"\n                      autoCapitalize=\"off\"\n                      spellCheck=\"false\"\n                    />\n                    <div className=\"inline-actions\">\n                      <button\n                        className=\"inline-action-btn save\"\n                        onClick={handleSaveNewEnv}\n                        onMouseDown={(e) => e.preventDefault()}\n                        title=\"Save\"\n                      >\n                        <IconCheck size={14} strokeWidth={2} />\n                      </button>\n                      <button\n                        className=\"inline-action-btn cancel\"\n                        onClick={handleCancelCreate}\n                        onMouseDown={(e) => e.preventDefault()}\n                        title=\"Cancel\"\n                      >\n                        <IconX size={14} strokeWidth={2} />\n                      </button>\n                    </div>\n                  </div>\n                )}\n\n                {envNameError && (isCreatingInline || renamingEnvUid) && <div className=\"env-error\">{envNameError}</div>}\n\n                {filteredEnvironments.length === 0 && !isCreatingInline && (\n                  <div className=\"no-env-file\">\n                    <span>No environments</span>\n                  </div>\n                )}\n              </div>\n            </CollapsibleSection>\n\n            <CollapsibleSection\n              title=\".env Files\"\n              expanded={dotEnvExpanded}\n              onToggle={() => setDotEnvExpanded(!dotEnvExpanded)}\n              badge={dotEnvFiles.length}\n              actions={(\n                <button\n                  className=\"btn-action\"\n                  onClick={handleCreateDotEnvInlineClick}\n                  title=\"Create .env file\"\n                >\n                  <IconPlus size={14} strokeWidth={1.5} />\n                </button>\n              )}\n            >\n              <div className=\"environments-list\">\n                {dotEnvFiles.map((file) => (\n                  <div\n                    key={file.filename}\n                    className={classnames('environment-item', {\n                      active: activeView === 'dotenv' && selectedDotEnvFile === file.filename\n                    })}\n                    onClick={() => handleDotEnvClick(file.filename)}\n                  >\n                    <span className=\"environment-name\">{file.filename}</span>\n                  </div>\n                ))}\n\n                {isCreatingDotEnvInline && (\n                  <div className=\"environment-item creating\" ref={dotEnvCreateContainerRef}>\n                    <input\n                      ref={dotEnvInputRef}\n                      type=\"text\"\n                      className=\"environment-name-input\"\n                      value={newDotEnvName}\n                      onChange={handleDotEnvNameChange}\n                      onKeyDown={handleDotEnvNameKeyDown}\n                      autoComplete=\"off\"\n                      autoCorrect=\"off\"\n                      autoCapitalize=\"off\"\n                      spellCheck=\"false\"\n                    />\n                    <div className=\"inline-actions\">\n                      <button\n                        className=\"inline-action-btn save\"\n                        onClick={handleSaveNewDotEnv}\n                        onMouseDown={(e) => e.preventDefault()}\n                        title=\"Create\"\n                      >\n                        <IconCheck size={14} strokeWidth={2} />\n                      </button>\n                      <button\n                        className=\"inline-action-btn cancel\"\n                        onClick={handleCancelDotEnvCreate}\n                        onMouseDown={(e) => e.preventDefault()}\n                        title=\"Cancel\"\n                      >\n                        <IconX size={14} strokeWidth={2} />\n                      </button>\n                    </div>\n                  </div>\n                )}\n\n                {dotEnvNameError && isCreatingDotEnvInline && <div className=\"env-error\">{dotEnvNameError}</div>}\n\n                {dotEnvFiles.length === 0 && !isCreatingDotEnvInline && (\n                  <div className=\"no-env-file\">\n                    <span>No .env files</span>\n                  </div>\n                )}\n              </div>\n            </CollapsibleSection>\n          </div>\n        </div>\n\n        {renderContent()}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default EnvironmentList;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/ImportEnvironment/index.js",
    "content": "import React from 'react';\nimport Portal from 'components/Portal';\nimport Modal from 'components/Modal';\nimport toast from 'react-hot-toast';\nimport { useDispatch } from 'react-redux';\nimport importPostmanEnvironment from 'utils/importers/postman-environment';\nimport { toastError } from 'utils/common/error';\nimport { IconDatabaseImport } from '@tabler/icons';\nimport { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';\n\nconst ImportEnvironment = ({ onClose, onEnvironmentCreated }) => {\n  const dispatch = useDispatch();\n\n  const handleImportPostmanEnvironment = () => {\n    importPostmanEnvironment()\n      .then((environments) => {\n        const importPromises = environments\n          .filter((env) =>\n            env.name && env.name !== 'undefined')\n          .map((environment) =>\n            dispatch(addGlobalEnvironment({ name: environment.name, variables: environment.variables }))\n              .then(() => {\n                toast.success('Environment imported successfully');\n              })\n              .catch((error) => {\n                toast.error('An error occurred while importing the environment');\n                console.error(error);\n              }));\n        return Promise.all(importPromises);\n      })\n      .then(() => {\n        onClose();\n        // Call the callback if provided\n        if (onEnvironmentCreated) {\n          onEnvironmentCreated();\n        }\n      })\n      .catch((err) => toastError(err, 'Postman Import environment failed'));\n  };\n\n  return (\n    <Portal>\n      <Modal size=\"sm\" title=\"Import Environment\" hideFooter={true} handleConfirm={onClose} handleCancel={onClose} dataTestId=\"import-environment-modal\">\n        <button\n          type=\"button\"\n          onClick={handleImportPostmanEnvironment}\n          className=\"flex justify-center flex-col items-center w-full dark:bg-zinc-700 rounded-lg border-2 border-dashed border-zinc-300 dark:border-zinc-400 p-12 text-center hover:border-zinc-400 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-2\"\n          data-testid=\"import-postman-environment\"\n        >\n          <IconDatabaseImport size={64} />\n          <span className=\"mt-2 block text-sm font-semibold\">Import your Postman environments</span>\n        </button>\n      </Modal>\n    </Portal>\n  );\n};\n\nexport default ImportEnvironment;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/RenameEnvironment/index.js",
    "content": "import React, { useEffect, useRef } from 'react';\nimport Portal from 'components/Portal/index';\nimport Modal from 'components/Modal/index';\nimport toast from 'react-hot-toast';\nimport { useFormik } from 'formik';\nimport * as Yup from 'yup';\nimport { useDispatch } from 'react-redux';\nimport { renameGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';\nimport { validateName, validateNameError } from 'utils/common/regex';\nimport { useSelector } from 'react-redux';\n\nconst RenameEnvironment = ({ onClose, environment }) => {\n  const dispatch = useDispatch();\n  const globalEnvs = useSelector((state) => state?.globalEnvironments?.globalEnvironments);\n  const inputRef = useRef();\n\n  const validateEnvironmentName = (name) => {\n    const trimmedName = name?.toLowerCase().trim();\n    return (globalEnvs || []).every((env) =>\n      env.uid === environment.uid || env?.name?.toLowerCase().trim() !== trimmedName);\n  };\n\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: {\n      name: environment.name\n    },\n    validationSchema: Yup.object({\n      name: Yup.string()\n        .min(1, 'must be at least 1 character')\n        .max(255, 'Must be 255 characters or less')\n        .test('is-valid-filename', function (value) {\n          const isValid = validateName(value);\n          return isValid ? true : this.createError({ message: validateNameError(value) });\n        })\n        .required('name is required')\n        .test('duplicate-name', 'Environment already exists', validateEnvironmentName)\n    }),\n    onSubmit: (values) => {\n      if (values.name === environment.name) {\n        return;\n      }\n      dispatch(renameGlobalEnvironment({ name: values.name, environmentUid: environment.uid }))\n        .then(() => {\n          toast.success('Environment renamed successfully');\n          onClose();\n        })\n        .catch((error) => {\n          toast.error('An error occurred while renaming the environment');\n          console.error(error);\n        });\n    }\n  });\n\n  useEffect(() => {\n    if (inputRef && inputRef.current) {\n      inputRef.current.focus();\n    }\n  }, [inputRef]);\n\n  const onSubmit = () => {\n    formik.handleSubmit();\n  };\n\n  return (\n    <Portal>\n      <Modal\n        size=\"sm\"\n        title=\"Rename Environment\"\n        confirmText=\"Rename\"\n        handleConfirm={onSubmit}\n        handleCancel={onClose}\n      >\n        <form className=\"bruno-form\" onSubmit={(e) => e.preventDefault()}>\n          <div>\n            <label htmlFor=\"environment-name\" className=\"block font-semibold\">\n              Environment Name\n            </label>\n            <input\n              id=\"environment-name\"\n              type=\"text\"\n              name=\"name\"\n              ref={inputRef}\n              className=\"block textbox mt-2 w-full\"\n              autoComplete=\"off\"\n              autoCorrect=\"off\"\n              autoCapitalize=\"off\"\n              spellCheck=\"false\"\n              onChange={formik.handleChange}\n              value={formik.values.name || ''}\n            />\n            {formik.touched.name && formik.errors.name ? (\n              <div className=\"text-red-500\">{formik.errors.name}</div>\n            ) : null}\n          </div>\n        </form>\n      </Modal>\n    </Portal>\n  );\n};\n\nexport default RenameEnvironment;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n  background-color: ${(props) => props.theme.bg};\n  \n  .empty-state {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    padding-top: 10%;\n    color: ${(props) => props.theme.colors.text.muted};\n    \n    svg {\n      opacity: 0.3;\n      margin-bottom: 8px;\n    }\n    \n    .title {\n      font-size: 13px;\n      font-weight: 500;\n      margin-bottom: 12px;\n      color: ${(props) => props.theme.colors.text.muted};\n    }\n    \n    .actions {\n      display: flex;\n      gap: 8px;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WorkspaceHome/WorkspaceEnvironments/index.js",
    "content": "import React, { useState } from 'react';\nimport { useSelector } from 'react-redux';\nimport EnvironmentList from './EnvironmentList';\nimport StyledWrapper from './StyledWrapper';\nimport ExportEnvironmentModal from 'components/Environments/Common/ExportEnvironmentModal';\n\nconst WorkspaceEnvironments = ({ workspace }) => {\n  const [isModified, setIsModified] = useState(false);\n  const [showExportModal, setShowExportModal] = useState(false);\n\n  const globalEnvironments = useSelector((state) => state.globalEnvironments.globalEnvironments);\n  const activeGlobalEnvironmentUid = useSelector((state) => state.globalEnvironments.activeGlobalEnvironmentUid);\n\n  const [selectedEnvironment, setSelectedEnvironment] = useState(() => {\n    const environments = globalEnvironments || [];\n    if (!environments.length) return null;\n    return environments.find((env) => env.uid === activeGlobalEnvironmentUid) || environments[0];\n  });\n\n  return (\n    <StyledWrapper>\n      <EnvironmentList\n        environments={globalEnvironments || []}\n        activeEnvironmentUid={activeGlobalEnvironmentUid}\n        selectedEnvironment={selectedEnvironment}\n        setSelectedEnvironment={setSelectedEnvironment}\n        isModified={isModified}\n        setIsModified={setIsModified}\n        collection={null}\n        workspace={workspace}\n        setShowExportModal={setShowExportModal}\n      />\n      {showExportModal && (\n        <ExportEnvironmentModal\n          onClose={() => setShowExportModal(false)}\n          environments={globalEnvironments || []}\n          environmentType=\"global\"\n        />\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default WorkspaceEnvironments;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WorkspaceHome/WorkspaceOverview/CollectionsList/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  flex: 1;\n  overflow-y: auto;\n\n  .collections-list {\n    display: flex;\n    flex-direction: column;\n    gap: 6px;\n  }\n\n  .empty-state {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    padding: 32px 20px;\n    text-align: center;\n  }\n\n  .empty-icon {\n    color: ${(props) => props.theme.colors.text.muted};\n    margin-bottom: 12px;\n  }\n\n  .empty-title {\n    font-size: ${(props) => props.theme.font.size.md};\n    font-weight: 500;\n    color: ${(props) => props.theme.text};\n    margin-bottom: 6px;\n  }\n\n  .empty-description {\n    font-size: ${(props) => props.theme.font.size.sm};\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .collection-card {\n    display: flex;\n    align-items: center;\n    gap: 10px;\n    padding: 10px 0;\n    border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.hoverBg};\n    cursor: pointer;\n\n    &:last-child {\n      border-bottom: none;\n    }\n  }\n\n  .collection-icon-wrapper {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    flex-shrink: 0;\n  }\n\n  .collection-info {\n    flex: 1;\n    min-width: 0;\n  }\n\n  .collection-header {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    margin-bottom: 1px;\n  }\n\n  .collection-name {\n    font-size: ${(props) => props.theme.font.size.base};\n    color: ${(props) => props.theme.text};\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n\n  .collection-path {\n    font-size: ${(props) => props.theme.font.size.xs};\n    color: ${(props) => props.theme.colors.text.muted};\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n\n  .collection-menu {\n    flex-shrink: 0;\n    color: ${(props) => props.theme.colors.text.muted};\n    cursor: pointer;\n\n    &:hover {\n      color: ${(props) => props.theme.text};\n    }\n  }\n\n  .collection-dropdown {\n    min-width: 120px;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WorkspaceHome/WorkspaceOverview/CollectionsList/index.js",
    "content": "import React, { useState, useMemo, useRef } from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { IconBox, IconTrash, IconEdit, IconShare, IconDots, IconX, IconFolder } from '@tabler/icons';\nimport { addTab } from 'providers/ReduxStore/slices/tabs';\nimport { mountCollection, showInFolder } from 'providers/ReduxStore/slices/collections/actions';\nimport { getRevealInFolderLabel } from 'utils/common/platform';\nimport { normalizePath } from 'utils/common/path';\nimport toast from 'react-hot-toast';\nimport RenameCollection from 'components/Sidebar/Collections/Collection/RenameCollection';\nimport RemoveCollection from 'components/Sidebar/Collections/Collection/RemoveCollection';\nimport DeleteCollection from 'components/Sidebar/Collections/Collection/DeleteCollection';\nimport ShareCollection from 'components/ShareCollection';\nimport Dropdown from 'components/Dropdown';\nimport StyledWrapper from './StyledWrapper';\n\nconst CollectionsList = ({ workspace }) => {\n  const dispatch = useDispatch();\n  const { collections } = useSelector((state) => state.collections);\n  const dropdownRefs = useRef({});\n\n  const [renameCollectionModalOpen, setRenameCollectionModalOpen] = useState(false);\n  const [removeCollectionModalOpen, setRemoveCollectionModalOpen] = useState(false);\n  const [deleteCollectionModalOpen, setDeleteCollectionModalOpen] = useState(false);\n  const [shareCollectionModalOpen, setShareCollectionModalOpen] = useState(false);\n  const [selectedCollectionUid, setSelectedCollectionUid] = useState(null);\n\n  const workspaceCollections = useMemo(() => {\n    if (!workspace.collections || workspace.collections.length === 0) {\n      return [];\n    }\n\n    const filteredCollections = workspace.collections.filter((wc) => {\n      if (workspace.scratchTempDirectory) {\n        return normalizePath(wc.path) !== normalizePath(workspace.scratchTempDirectory);\n      }\n      return true;\n    });\n\n    return filteredCollections.map((wc) => {\n      const loadedCollection = collections.find(\n        (c) => normalizePath(c.pathname) === normalizePath(wc.path)\n      );\n\n      if (loadedCollection) {\n        return {\n          ...loadedCollection,\n          isGitBacked: !!wc.remote,\n          gitRemoteUrl: wc.remote\n        };\n      }\n\n      return {\n        uid: `unloaded-${wc.path}`,\n        name: wc.name,\n        pathname: wc.path,\n        items: [],\n        environments: [],\n        isGitBacked: !!wc.remote,\n        isLoaded: false,\n        gitRemoteUrl: wc.remote,\n        git: { gitRootPath: null },\n        brunoConfig: {},\n        root: {\n          request: {\n            headers: [],\n            auth: { mode: 'none' },\n            vars: { req: [], res: [] },\n            script: { req: '', res: '' },\n            tests: ''\n          },\n          docs: ''\n        }\n      };\n    });\n  }, [workspace.collections, workspace.scratchTempDirectory, collections]);\n\n  const handleOpenCollectionClick = (collection, event) => {\n    if (event.target.closest('.collection-menu')) {\n      return;\n    }\n\n    if (collection.isLoaded === false) {\n      if (collection.isGitBacked) {\n        toast.error(`Collection \"${collection.name}\" needs to be cloned first`);\n      } else {\n        toast.error(`Collection \"${collection.name}\" does not exist on disk`);\n      }\n      return;\n    }\n\n    dispatch(\n      mountCollection({\n        collectionUid: collection.uid,\n        collectionPathname: collection.pathname,\n        brunoConfig: collection.brunoConfig\n      })\n    );\n\n    dispatch(\n      addTab({\n        uid: collection.uid,\n        collectionUid: collection.uid,\n        type: 'collection-settings'\n      })\n    );\n  };\n\n  const handleRenameCollection = (collection) => {\n    dropdownRefs.current[collection.uid]?.hide();\n    if (collection.isLoaded === false) {\n      toast.error('Cannot rename collections that are not cloned yet');\n      return;\n    }\n    setSelectedCollectionUid(collection.uid);\n    setRenameCollectionModalOpen(true);\n  };\n\n  const handleShareCollection = (collection) => {\n    dropdownRefs.current[collection.uid]?.hide();\n    if (collection.isLoaded === false) {\n      toast.error('Please clone this collection first before sharing it');\n      return;\n    }\n\n    dispatch(\n      mountCollection({\n        collectionUid: collection.uid,\n        collectionPathname: collection.pathname,\n        brunoConfig: collection.brunoConfig\n      })\n    );\n\n    setSelectedCollectionUid(collection.uid);\n    setShareCollectionModalOpen(true);\n  };\n\n  const handleRemoveCollection = (collection) => {\n    dropdownRefs.current[collection.uid]?.hide();\n    if (collection.isLoaded === false) {\n      toast.error('Cannot remove collections that are not loaded');\n      return;\n    }\n    setSelectedCollectionUid(collection.uid);\n    setRemoveCollectionModalOpen(true);\n  };\n\n  const handleDeleteCollection = (collection) => {\n    dropdownRefs.current[collection.uid]?.hide();\n    if (collection.isLoaded === false) {\n      toast.error('Cannot delete collections that are not loaded');\n      return;\n    }\n    setSelectedCollectionUid(collection.uid);\n    setDeleteCollectionModalOpen(true);\n  };\n\n  const handleShowInFolder = (collection) => {\n    dropdownRefs.current[collection.uid]?.hide();\n    dispatch(showInFolder(collection.pathname)).catch((error) => {\n      console.error('Error opening the folder', error);\n      toast.error('Error opening the folder');\n    });\n  };\n\n  return (\n    <StyledWrapper>\n      {renameCollectionModalOpen && selectedCollectionUid && (\n        <RenameCollection\n          collectionUid={selectedCollectionUid}\n          onClose={() => {\n            setRenameCollectionModalOpen(false);\n            setSelectedCollectionUid(null);\n          }}\n        />\n      )}\n\n      {removeCollectionModalOpen && selectedCollectionUid && (\n        <RemoveCollection\n          collectionUid={selectedCollectionUid}\n          onClose={() => {\n            setRemoveCollectionModalOpen(false);\n            setSelectedCollectionUid(null);\n          }}\n        />\n      )}\n\n      {deleteCollectionModalOpen && selectedCollectionUid && (\n        <DeleteCollection\n          collectionUid={selectedCollectionUid}\n          workspaceUid={workspace.uid}\n          onClose={() => {\n            setDeleteCollectionModalOpen(false);\n            setSelectedCollectionUid(null);\n          }}\n        />\n      )}\n\n      {shareCollectionModalOpen && selectedCollectionUid && (\n        <ShareCollection\n          collectionUid={selectedCollectionUid}\n          onClose={() => {\n            setShareCollectionModalOpen(false);\n            setSelectedCollectionUid(null);\n          }}\n        />\n      )}\n\n      <div className=\"collections-list\">\n        {workspaceCollections.length === 0 ? (\n          <div className=\"empty-state\">\n            <IconBox size={32} strokeWidth={1.5} className=\"empty-icon\" />\n            <h3 className=\"empty-title\">No collections yet</h3>\n            <p className=\"empty-description\">Create your first collection or open an existing one to get started.</p>\n          </div>\n        ) : (\n          workspaceCollections.map((collection, index) => (\n            <div\n              key={collection.uid || index}\n              className=\"collection-card\"\n              onClick={(e) => handleOpenCollectionClick(collection, e)}\n            >\n              <div className=\"collection-info\">\n                <div className=\"collection-header\">\n                  <div className=\"collection-icon-wrapper\">\n                    <IconBox size={18} strokeWidth={1.5} />\n                  </div>\n                  <div className=\"collection-name\">{collection.name}</div>\n                </div>\n                <div className=\"collection-path\">{collection.pathname}</div>\n              </div>\n              <div className=\"collection-menu\">\n                <Dropdown\n                  style=\"new\"\n                  placement=\"bottom-end\"\n                  onCreate={(ref) => (dropdownRefs.current[collection.uid] = ref)}\n                  icon={<IconDots size={18} strokeWidth={1.5} />}\n                >\n                  <div className=\"collection-dropdown\">\n                    <div\n                      className=\"dropdown-item\"\n                      onClick={(e) => {\n                        e.stopPropagation();\n                        handleRenameCollection(collection);\n                      }}\n                    >\n                      <IconEdit size={16} strokeWidth={1.5} />\n                      <span>Rename</span>\n                    </div>\n                    <div\n                      className=\"dropdown-item\"\n                      onClick={(e) => {\n                        e.stopPropagation();\n                        handleShareCollection(collection);\n                      }}\n                    >\n                      <IconShare size={16} strokeWidth={1.5} />\n                      <span>Share</span>\n                    </div>\n                    <div\n                      className=\"dropdown-item\"\n                      onClick={(e) => {\n                        e.stopPropagation();\n                        handleShowInFolder(collection);\n                      }}\n                    >\n                      <IconFolder size={16} strokeWidth={1.5} />\n                      <span>{getRevealInFolderLabel()}</span>\n                    </div>\n                    <div\n                      className=\"dropdown-item\"\n                      onClick={(e) => {\n                        e.stopPropagation();\n                        handleRemoveCollection(collection);\n                      }}\n                    >\n                      <IconX size={16} strokeWidth={1.5} />\n                      <span>Remove</span>\n                    </div>\n                    <div\n                      className=\"dropdown-item delete-item\"\n                      onClick={(e) => {\n                        e.stopPropagation();\n                        handleDeleteCollection(collection);\n                      }}\n                    >\n                      <IconTrash size={16} strokeWidth={1.5} />\n                      <span>Delete</span>\n                    </div>\n                  </div>\n                </Dropdown>\n              </div>\n            </div>\n          ))\n        )}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default CollectionsList;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WorkspaceHome/WorkspaceOverview/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  height: 100%;\n\n  .overview-layout {\n    display: flex;\n    height: 100%;\n  }\n\n  .overview-main {\n    flex: 3;\n    padding: 20px 16px 16px;\n    overflow-y: auto;\n    border-right: 1px solid ${(props) => props.theme.workspace.border};\n  }\n\n  .overview-docs {\n    display: flex;\n    flex: 2;\n    flex-direction: column;\n    overflow: hidden;\n  }\n\n  .stats-row {\n    display: flex;\n    gap: 24px;\n    margin-bottom: 16px;\n  }\n\n  .stat-item {\n    display: flex;\n    flex-direction: column;\n    gap: 2px;\n  }\n\n  .stat-value {\n    font-size: 22px;\n    font-weight: 600;\n    color: ${(props) => props.theme.text};\n    line-height: 1;\n  }\n\n  .stat-label {\n    font-size: ${(props) => props.theme.font.size.xs};\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .quick-actions-section {\n    margin-bottom: 16px;\n  }\n\n  .section-title {\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-weight: 500;\n    color: ${(props) => props.theme.colors.text.muted};\n    margin-bottom: 8px;\n  }\n\n  .quick-actions-buttons {\n    display: flex;\n    gap: 8px;\n  }\n\n  .quick-action-btn {\n    display: flex;\n    align-items: center;\n    gap: 5px;\n    padding: 4px 8px;\n    border: 1px solid ${(props) => props.theme.brand};\n    border-radius: ${(props) => props.theme.border.radius.base};\n    background: transparent;\n    color: ${(props) => props.theme.brand};\n    font-size: ${(props) => props.theme.font.size.sm};\n    cursor: pointer;\n    transition: all 0.15s ease;\n\n    &:hover {\n      background: ${(props) => props.theme.brand}10;\n    }\n  }\n\n  .collections-section {\n    flex: 1;\n    display: flex;\n    flex-direction: column;\n    overflow: hidden;\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WorkspaceHome/WorkspaceOverview/index.js",
    "content": "import React, { useState } from 'react';\nimport { useSelector, useDispatch } from 'react-redux';\nimport { IconPlus, IconFolder, IconDownload } from '@tabler/icons';\nimport { importCollection, openCollection, importCollectionFromZip } from 'providers/ReduxStore/slices/collections/actions';\nimport { setIsCreatingCollection, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';\nimport toast from 'react-hot-toast';\nimport ImportCollection from 'components/Sidebar/ImportCollection';\nimport ImportCollectionLocation from 'components/Sidebar/ImportCollectionLocation';\nimport BulkImportCollectionLocation from 'components/Sidebar/BulkImportCollectionLocation';\nimport CloneGitRepository from 'components/Sidebar/CloneGitRespository';\nimport Button from 'ui/Button';\nimport CollectionsList from './CollectionsList';\nimport WorkspaceDocs from '../WorkspaceDocs';\nimport StyledWrapper from './StyledWrapper';\n\nconst WorkspaceOverview = ({ workspace }) => {\n  const dispatch = useDispatch();\n  const { globalEnvironments } = useSelector((state) => state.globalEnvironments);\n  const { sidebarCollapsed, isCreatingCollection } = useSelector((state) => state.app);\n\n  const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);\n  const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);\n  const [importData, setImportData] = useState(null);\n  const [showCloneGitModal, setShowCloneGitModal] = useState(false);\n  const [gitRepositoryUrl, setGitRepositoryUrl] = useState(null);\n\n  const workspaceCollectionsCount = workspace?.collections?.length || 0;\n\n  const workspaceEnvironmentsCount = globalEnvironments?.length || 0;\n\n  const handleCreateCollection = async () => {\n    if (isCreatingCollection) {\n      return;\n    }\n\n    if (!workspace?.pathname) {\n      toast.error('Workspace path not found');\n      return;\n    }\n\n    try {\n      const { ipcRenderer } = window;\n      await ipcRenderer.invoke('renderer:ensure-collections-folder', workspace.pathname);\n      if (sidebarCollapsed) {\n        dispatch(toggleSidebarCollapse());\n      }\n      dispatch(setIsCreatingCollection(true));\n    } catch (error) {\n      console.error('Error ensuring collections folder exists:', error);\n      toast.error('Error preparing workspace for collection creation');\n    }\n  };\n\n  const handleOpenCollection = () => {\n    dispatch(openCollection()).catch((err) => {\n      console.error(err);\n      toast.error('An error occurred while opening the collection');\n    });\n  };\n\n  const handleImportCollection = () => {\n    setImportCollectionModalOpen(true);\n  };\n\n  const handleImportCollectionSubmit = ({ rawData, type, repositoryUrl, ...rest }) => {\n    setImportCollectionModalOpen(false);\n\n    if (type === 'git-repository') {\n      setGitRepositoryUrl(repositoryUrl);\n      setShowCloneGitModal(true);\n      return;\n    }\n\n    setImportData({ rawData, type, ...rest });\n    setImportCollectionLocationModalOpen(true);\n  };\n\n  const handleImportCollectionLocation = (convertedCollection, collectionLocation, options = {}) => {\n    const importAction = options.isZipImport\n      ? importCollectionFromZip(convertedCollection.zipFilePath, collectionLocation)\n      : importCollection(convertedCollection, collectionLocation, options);\n\n    dispatch(importAction)\n      .then(() => {\n        setImportCollectionLocationModalOpen(false);\n        setImportData(null);\n      });\n  };\n\n  const handleCloseGitModal = () => {\n    setShowCloneGitModal(false);\n    setGitRepositoryUrl(null);\n  };\n\n  return (\n    <StyledWrapper>\n      {importCollectionModalOpen && (\n        <ImportCollection\n          onClose={() => setImportCollectionModalOpen(false)}\n          handleSubmit={handleImportCollectionSubmit}\n        />\n      )}\n\n      {importCollectionLocationModalOpen && importData && (importData.type !== 'multiple' && importData.type !== 'bulk') && (\n        <ImportCollectionLocation\n          rawData={importData.rawData}\n          format={importData.type}\n          sourceUrl={importData.sourceUrl}\n          filePath={importData.filePath}\n          rawContent={importData.rawContent}\n          onClose={() => setImportCollectionLocationModalOpen(false)}\n          handleSubmit={handleImportCollectionLocation}\n        />\n      )}\n      {importCollectionLocationModalOpen && importData && (importData.type === 'multiple' || importData.type === 'bulk') && (\n        <BulkImportCollectionLocation\n          importData={importData}\n          onClose={() => setImportCollectionLocationModalOpen(false)}\n          handleSubmit={handleImportCollectionLocation}\n        />\n      )}\n      {showCloneGitModal && (\n        <CloneGitRepository\n          onClose={handleCloseGitModal}\n          onFinish={handleCloseGitModal}\n          collectionRepositoryUrl={gitRepositoryUrl}\n        />\n      )}\n\n      <div className=\"overview-layout\">\n        <div className=\"overview-main\">\n          <div className=\"stats-row\">\n            <div className=\"stat-item\">\n              <span className=\"stat-value\">{workspaceCollectionsCount}</span>\n              <span className=\"stat-label\">Collections</span>\n            </div>\n            <div className=\"stat-item\">\n              <span className=\"stat-value\">{workspaceEnvironmentsCount}</span>\n              <span className=\"stat-label\">Environments</span>\n            </div>\n          </div>\n\n          <div className=\"quick-actions-section\">\n            <div className=\"section-title\">Quick Actions</div>\n            <div className=\"quick-actions-buttons\">\n              <Button\n                color=\"light\"\n                size=\"sm\"\n                icon={<IconPlus size={14} strokeWidth={1.5} />}\n                onClick={handleCreateCollection}\n                disabled={isCreatingCollection}\n              >\n                Create Collection\n              </Button>\n              <Button\n                color=\"light\"\n                size=\"sm\"\n                icon={<IconFolder size={14} strokeWidth={1.5} />}\n                onClick={handleOpenCollection}\n              >\n                Open Collection\n              </Button>\n              <Button\n                color=\"light\"\n                size=\"sm\"\n                icon={<IconDownload size={14} strokeWidth={1.5} />}\n                onClick={handleImportCollection}\n              >\n                Import Collection\n              </Button>\n            </div>\n          </div>\n\n          <div className=\"collections-section\">\n            <div className=\"section-title\">Collections</div>\n            <CollectionsList workspace={workspace} />\n          </div>\n        </div>\n\n        <div className=\"overview-docs\">\n          <WorkspaceDocs workspace={workspace} />\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default WorkspaceOverview;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WorkspaceSidebar/CreateWorkspace/index.js",
    "content": "import React, { useRef, useEffect, useState } from 'react';\nimport { useFormik } from 'formik';\nimport { useDispatch, useSelector } from 'react-redux';\nimport * as Yup from 'yup';\nimport toast from 'react-hot-toast';\nimport { IconArrowBackUp, IconEdit } from '@tabler/icons';\nimport Modal from 'components/Modal';\nimport Help from 'components/Help';\nimport PathDisplay from 'components/PathDisplay/index';\nimport { createWorkspaceAction } from 'providers/ReduxStore/slices/workspaces/actions';\nimport { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';\nimport { multiLineMsg } from 'utils/common/index';\nimport { formatIpcError } from 'utils/common/error';\nimport { sanitizeName, validateName, validateNameError } from 'utils/common/regex';\nimport get from 'lodash/get';\n\nconst CreateWorkspace = ({ onClose }) => {\n  const inputRef = useRef();\n  const dispatch = useDispatch();\n  const workspaces = useSelector((state) => state.workspaces.workspaces);\n  const preferences = useSelector((state) => state.app.preferences);\n  const [isSubmitting, setIsSubmitting] = useState(false);\n  const [isEditing, setIsEditing] = useState(false);\n\n  const defaultLocation = get(preferences, 'general.defaultLocation', '');\n\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: {\n      workspaceName: '',\n      workspaceFolderName: '',\n      workspaceLocation: defaultLocation\n    },\n    validationSchema: Yup.object({\n      workspaceName: Yup.string()\n        .min(1, 'Must be at least 1 character')\n        .max(255, 'Must be 255 characters or less')\n        .required('Workspace name is required')\n        .test('unique-name', 'A workspace with this name already exists', function (value) {\n          if (!value) return true;\n\n          return !workspaces.some((w) =>\n            !w.isCreating && w.name && w.name.toLowerCase() === value.toLowerCase());\n        }),\n      workspaceFolderName: Yup.string()\n        .min(1, 'Must be at least 1 character')\n        .max(255, 'Must be 255 characters or less')\n        .test('is-valid-folder-name', function (value) {\n          const isValid = validateName(value);\n          return isValid ? true : this.createError({ message: validateNameError(value) });\n        })\n        .required('Folder name is required'),\n      workspaceLocation: Yup.string().min(1, 'Location is required').required('Location is required')\n    }),\n    onSubmit: async (values) => {\n      if (isSubmitting) return;\n\n      try {\n        setIsSubmitting(true);\n\n        await dispatch(createWorkspaceAction(values.workspaceName, values.workspaceFolderName, values.workspaceLocation));\n        toast.success('Workspace created!');\n        onClose();\n      } catch (error) {\n        toast.error(multiLineMsg('An error occurred while creating the workspace', formatIpcError(error)));\n      } finally {\n        setIsSubmitting(false);\n      }\n    }\n  });\n\n  const browse = () => {\n    dispatch(browseDirectory())\n      .then((dirPath) => {\n        if (typeof dirPath === 'string') {\n          formik.setFieldValue('workspaceLocation', dirPath);\n        }\n      })\n      .catch((error) => {\n        formik.setFieldValue('workspaceLocation', '');\n        console.error(error);\n      });\n  };\n\n  useEffect(() => {\n    if (inputRef && inputRef.current) {\n      inputRef.current.focus();\n    }\n  }, [inputRef]);\n\n  return (\n    <Modal\n      size=\"md\"\n      title=\"Create Workspace\"\n      description=\"Give your new workspace a name and choose its type to get started.\"\n      confirmText={isSubmitting ? 'Creating...' : 'Create Workspace'}\n      handleConfirm={formik.handleSubmit}\n      handleCancel={onClose}\n      style=\"new\"\n      confirmDisabled={isSubmitting}\n    >\n      <div>\n        <form className=\"bruno-form\" onSubmit={formik.handleSubmit}>\n          <div className=\"mb-4\">\n            <label htmlFor=\"workspaceName\" className=\"block font-semibold mb-2\">\n              Name\n            </label>\n            <input\n              id=\"workspace-name\"\n              type=\"text\"\n              name=\"workspaceName\"\n              ref={inputRef}\n              className=\"block textbox w-full\"\n              autoComplete=\"off\"\n              autoCorrect=\"off\"\n              autoCapitalize=\"off\"\n              spellCheck=\"false\"\n              onChange={(e) => {\n                formik.handleChange(e);\n                if (!isEditing) {\n                  formik.setFieldValue('workspaceFolderName', sanitizeName(e.target.value));\n                }\n              }}\n              value={formik.values.workspaceName || ''}\n            />\n            {formik.touched.workspaceName && formik.errors.workspaceName ? (\n              <div className=\"text-red-500 text-sm mt-1\">{formik.errors.workspaceName}</div>\n            ) : null}\n          </div>\n\n          {formik.values.workspaceName?.trim()?.length > 0 && (\n            <div className=\"mb-4\">\n              <div className=\"flex items-center justify-between mb-2\">\n                <label htmlFor=\"workspaceFolderName\" className=\"flex items-center font-semibold\">\n                  Folder Name\n                  <Help width=\"300\">\n                    <p>\n                      The name of the folder used to store the workspace.\n                    </p>\n                    <p className=\"mt-2\">\n                      You can choose a folder name different from your workspace's name or one compatible with filesystem rules.\n                    </p>\n                  </Help>\n                </label>\n                {isEditing ? (\n                  <IconArrowBackUp\n                    className=\"cursor-pointer opacity-50 hover:opacity-80\"\n                    size={16}\n                    strokeWidth={1.5}\n                    onClick={() => setIsEditing(false)}\n                  />\n                ) : (\n                  <IconEdit\n                    className=\"cursor-pointer opacity-50 hover:opacity-80\"\n                    size={16}\n                    strokeWidth={1.5}\n                    onClick={() => setIsEditing(true)}\n                  />\n                )}\n              </div>\n              {isEditing ? (\n                <input\n                  id=\"workspace-folder-name\"\n                  type=\"text\"\n                  name=\"workspaceFolderName\"\n                  className=\"block textbox w-full\"\n                  onChange={formik.handleChange}\n                  autoComplete=\"off\"\n                  autoCorrect=\"off\"\n                  autoCapitalize=\"off\"\n                  spellCheck=\"false\"\n                  value={formik.values.workspaceFolderName || ''}\n                />\n              ) : (\n                <PathDisplay baseName={formik.values.workspaceFolderName} />\n              )}\n              {formik.touched.workspaceFolderName && formik.errors.workspaceFolderName ? (\n                <div className=\"text-red-500 text-sm mt-1\">{formik.errors.workspaceFolderName}</div>\n              ) : null}\n            </div>\n          )}\n\n          <div className=\"mb-4\">\n            <label htmlFor=\"workspaceLocation\" className=\"font-semibold mb-2 flex items-center\">\n              Location\n              <Help>\n                <p>\n                  Bruno stores your workspaces on your computer's filesystem.\n                </p>\n                <p className=\"mt-2\">\n                  Choose the location where you want to store this workspace.\n                </p>\n              </Help>\n            </label>\n            <input\n              id=\"workspace-location\"\n              type=\"text\"\n              name=\"workspaceLocation\"\n              readOnly={true}\n              className=\"block textbox mt-2 w-full cursor-pointer\"\n              autoComplete=\"off\"\n              autoCorrect=\"off\"\n              autoCapitalize=\"off\"\n              spellCheck=\"false\"\n              value={formik.values.workspaceLocation || ''}\n              onClick={browse}\n            />\n            {formik.touched.workspaceLocation && formik.errors.workspaceLocation ? (\n              <div className=\"text-red-500 text-sm mt-1\">{formik.errors.workspaceLocation}</div>\n            ) : null}\n            <div className=\"mt-1\">\n              <span\n                className=\"text-link cursor-pointer hover:underline\"\n                onClick={browse}\n              >\n                Browse\n              </span>\n            </div>\n          </div>\n        </form>\n      </div>\n    </Modal>\n  );\n};\n\nexport default CreateWorkspace;\n"
  },
  {
    "path": "packages/bruno-app/src/components/WorkspaceSidebar/ImportWorkspace/index.js",
    "content": "import React, { useState, useRef, useEffect } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { useFormik } from 'formik';\nimport * as Yup from 'yup';\nimport toast from 'react-hot-toast';\nimport get from 'lodash/get';\nimport { IconFileZip } from '@tabler/icons';\nimport Modal from 'components/Modal';\nimport { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';\nimport { importWorkspaceAction } from 'providers/ReduxStore/slices/workspaces/actions';\nimport { formatIpcError } from 'utils/common/error';\nimport { multiLineMsg } from 'utils/common/index';\nimport Help from 'components/Help';\n\nconst ImportWorkspace = ({ onClose }) => {\n  const dispatch = useDispatch();\n  const preferences = useSelector((state) => state.app.preferences);\n  const [dragActive, setDragActive] = useState(false);\n  const [selectedFile, setSelectedFile] = useState(null);\n  const [isSubmitting, setIsSubmitting] = useState(false);\n  const fileInputRef = useRef(null);\n  const locationInputRef = useRef(null);\n\n  const defaultLocation = get(preferences, 'general.defaultLocation', '');\n\n  const formik = useFormik({\n    enableReinitialize: true,\n    initialValues: {\n      workspaceLocation: defaultLocation\n    },\n    validationSchema: Yup.object({\n      workspaceLocation: Yup.string().min(1, 'location is required').required('location is required')\n    }),\n    onSubmit: async (values) => {\n      if (isSubmitting || !selectedFile) return;\n\n      try {\n        setIsSubmitting(true);\n        await dispatch(importWorkspaceAction(selectedFile.path, values.workspaceLocation));\n        toast.success('Workspace imported successfully!');\n        onClose();\n      } catch (error) {\n        toast.error(multiLineMsg('Failed to import workspace', formatIpcError(error)));\n      } finally {\n        setIsSubmitting(false);\n      }\n    }\n  });\n\n  const handleDrag = (e) => {\n    e.preventDefault();\n    e.stopPropagation();\n\n    if (e.dataTransfer) {\n      e.dataTransfer.dropEffect = 'copy';\n    }\n\n    if (e.type === 'dragenter' || e.type === 'dragover') {\n      setDragActive(true);\n    } else if (e.type === 'dragleave') {\n      setDragActive(false);\n    }\n  };\n\n  const validateAndGetFilePath = (file) => {\n    if (!file) return null;\n\n    const isZip = file.name.endsWith('.zip') || file.type === 'application/zip' || file.type === 'application/x-zip-compressed';\n    if (!isZip) {\n      toast.error('Please select a valid zip file');\n      return null;\n    }\n\n    const filePath = window?.ipcRenderer?.getFilePath(file);\n    if (!filePath) {\n      toast.error('Could not get file path');\n      return null;\n    }\n\n    return { name: file.name, path: filePath };\n  };\n\n  const handleDrop = (e) => {\n    e.preventDefault();\n    e.stopPropagation();\n    setDragActive(false);\n\n    if (e.dataTransfer.files && e.dataTransfer.files[0]) {\n      const fileInfo = validateAndGetFilePath(e.dataTransfer.files[0]);\n      if (fileInfo) {\n        setSelectedFile(fileInfo);\n      }\n    }\n  };\n\n  const handleBrowseFiles = () => {\n    fileInputRef.current.click();\n  };\n\n  const handleFileInputChange = (e) => {\n    if (e.target.files && e.target.files[0]) {\n      const fileInfo = validateAndGetFilePath(e.target.files[0]);\n      if (fileInfo) {\n        setSelectedFile(fileInfo);\n      }\n    }\n  };\n\n  const browse = () => {\n    dispatch(browseDirectory())\n      .then((dirPath) => {\n        if (typeof dirPath === 'string' && dirPath.length > 0) {\n          formik.setFieldValue('workspaceLocation', dirPath);\n        }\n      })\n      .catch((error) => {\n        formik.setFieldValue('workspaceLocation', '');\n        console.error(error);\n      });\n  };\n\n  const handleClearFile = () => {\n    setSelectedFile(null);\n    if (fileInputRef.current) {\n      fileInputRef.current.value = '';\n    }\n  };\n\n  useEffect(() => {\n    if (locationInputRef && locationInputRef.current) {\n      locationInputRef.current.focus();\n    }\n  }, [locationInputRef]);\n\n  const canSubmit = selectedFile && formik.values.workspaceLocation && !isSubmitting;\n\n  return (\n    <Modal\n      size=\"md\"\n      title=\"Import Workspace\"\n      confirmText={isSubmitting ? 'Importing...' : 'Import'}\n      handleConfirm={formik.handleSubmit}\n      handleCancel={onClose}\n      confirmDisabled={!canSubmit}\n    >\n      <div className=\"flex flex-col\">\n        <div className=\"mb-4\">\n          <h3 className=\"font-semibold mb-2\">Workspace File</h3>\n          {selectedFile ? (\n            <div className=\"flex items-center justify-between p-3 border border-gray-200 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-800\">\n              <div className=\"flex items-center gap-2\">\n                <IconFileZip size={20} className=\"text-gray-500\" />\n                <span className=\"text-gray-700 dark:text-gray-300\">{selectedFile.name}</span>\n              </div>\n              <button\n                type=\"button\"\n                className=\"text-gray-500 hover:text-red-500 text-sm\"\n                onClick={handleClearFile}\n              >\n                Remove\n              </button>\n            </div>\n          ) : (\n            <div\n              onDragEnter={handleDrag}\n              onDragOver={handleDrag}\n              onDragLeave={handleDrag}\n              onDrop={handleDrop}\n              className={`\n                border-2 border-dashed rounded-lg p-6 transition-colors duration-200\n                ${dragActive ? 'border-blue-500 bg-blue-50 dark:border-blue-400 dark:bg-blue-900/20' : 'border-gray-200 dark:border-gray-700'}\n              `}\n            >\n              <div className=\"flex flex-col items-center justify-center\">\n                <IconFileZip\n                  size={28}\n                  className=\"text-gray-400 dark:text-gray-500 mb-3\"\n                />\n                <input\n                  ref={fileInputRef}\n                  type=\"file\"\n                  className=\"hidden\"\n                  onChange={handleFileInputChange}\n                  accept=\".zip,application/zip,application/x-zip-compressed\"\n                />\n                <p className=\"text-gray-600 dark:text-gray-300 mb-2\">\n                  Drop workspace zip file here or{' '}\n                  <button\n                    type=\"button\"\n                    className=\"text-blue-500 underline cursor-pointer\"\n                    onClick={handleBrowseFiles}\n                  >\n                    choose a file\n                  </button>\n                </p>\n                <p className=\"text-xs text-gray-500 dark:text-gray-400\">\n                  Supports exported Bruno workspace zip files\n                </p>\n              </div>\n            </div>\n          )}\n        </div>\n\n        <div className=\"mb-4\">\n          <label htmlFor=\"workspace-location\" className=\"font-semibold mb-2 flex items-center\">\n            Extract Location\n            <Help>\n              <p>\n                Choose the location where you want to extract this workspace.\n              </p>\n              <p className=\"mt-2\">\n                The workspace folder will be created at this location.\n              </p>\n            </Help>\n          </label>\n          <input\n            id=\"workspace-location\"\n            type=\"text\"\n            name=\"workspaceLocation\"\n            ref={locationInputRef}\n            readOnly={true}\n            className=\"block textbox mt-2 w-full cursor-pointer\"\n            autoComplete=\"off\"\n            autoCorrect=\"off\"\n            autoCapitalize=\"off\"\n            spellCheck=\"false\"\n            value={formik.values.workspaceLocation || ''}\n            onClick={browse}\n          />\n          {formik.touched.workspaceLocation && formik.errors.workspaceLocation ? (\n            <div className=\"text-red-500 text-sm mt-1\">{formik.errors.workspaceLocation}</div>\n          ) : null}\n          <div className=\"mt-1\">\n            <span\n              className=\"text-link cursor-pointer hover:underline\"\n              onClick={browse}\n            >\n              Browse\n            </span>\n          </div>\n        </div>\n      </div>\n    </Modal>\n  );\n};\n\nexport default ImportWorkspace;\n"
  },
  {
    "path": "packages/bruno-app/src/globalStyles.js",
    "content": "import { createGlobalStyle } from 'styled-components';\nimport { rgba } from 'polished';\n\nconst GlobalStyle = createGlobalStyle`\n\n  body {\n    font-size: ${(props) => props.theme.font.size.base};\n  }\n\n  .CodeMirror-gutters {\n    background-color: ${(props) => props.theme.codemirror.gutter.bg} !important;\n    border-right: solid 1px ${(props) => props.theme.codemirror.border};\n  }\n\n  .text-link {\n    color: ${(props) => props.theme.textLink};\n  }\n  .text-muted {\n    color: ${(props) => props.theme.colors.text.muted};\n  }\n\n  .tooltip-mod {\n    background-color: ${(props) => props.theme.infoTip.bg} !important;\n    color: ${(props) => props.theme.text} !important;\n    border: 1px solid ${(props) => props.theme.infoTip.border} !important;\n    box-shadow: ${(props) => props.theme.infoTip.boxShadow} !important;\n    font-size: ${(props) => props.theme.font.size.xs} !important;\n    padding: 4px 8px !important;\n    border-radius: 4px !important;\n    opacity: 1 !important;\n    z-index: 9999 !important;\n  }\n\n  .btn {\n    text-align: center;\n    white-space: nowrap;\n    outline: none;\n    box-shadow: none;\n    border-radius: 3px;\n  }\n\n  .btn-sm {\n    padding: .215rem .6rem .215rem .6rem;\n  }\n\n  .btn-xs {\n    padding: .2rem .4rem .2rem .4rem;\n  }\n\n  .btn-md {\n    padding: .4rem 1.1rem;\n    line-height: 1.47;\n  }\n\n  .btn-default {\n    &:active,\n    &:hover,\n    &:focus {\n      outline: none;\n      box-shadow: none;\n    }\n  }\n\n  .btn-close {\n    color: ${(props) => props.theme.button.close.color};\n    background: ${(props) => props.theme.button.close.bg};\n    border: solid 1px ${(props) => props.theme.button.close.border};\n\n    &.btn-border {\n      border: solid 1px #696969;\n    }\n\n    &:hover,\n    &:focus {\n      outline: none;\n      box-shadow: none;\n      border: solid 1px #696969;\n    }\n  }\n\n  .btn-danger {\n    color: ${(props) => props.theme.button.danger.color};\n    background: ${(props) => props.theme.button.danger.bg};\n    border: solid 1px ${(props) => props.theme.button.danger.border};\n\n    &:hover,\n    &:focus {\n      outline: none;\n      box-shadow: none;\n    }\n  }\n\n  .btn-secondary {\n    color: ${(props) => props.theme.button.secondary.color};\n    background: ${(props) => props.theme.button.secondary.bg};\n    border: solid 1px ${(props) => props.theme.button.secondary.border};\n\n    .btn-icon {\n      color: #3f3f3f;\n    }\n\n    &:hover,\n    &:focus {\n      border-color: ${(props) => props.theme.button.secondary.hoverBorder};\n      outline: none;\n      box-shadow: none;\n    }\n\n    &:disabled {\n      color: ${(props) => props.theme.button.disabled.color};\n      background: ${(props) => props.theme.button.disabled.bg};\n      border: solid 1px ${(props) => props.theme.button.disabled.border};\n      cursor: not-allowed;\n    }\n\n    &:disabled.btn-icon {\n      color: #545454;\n    }\n  }\n\n  input::placeholder {\n    color: ${(props) => props.theme.input.placeholder.color};\n    opacity:  ${(props) => props.theme.input.placeholder.opacity};\n  }\n\n  @keyframes fade-in {\n    from {\n      opacity: 0;\n    }\n    to {\n      opacity: 1;\n    }\n  }\n\n  @keyframes fade-out {\n    from {\n     opacity: 1;\n    }\n    to {\n      opacity: 0;\n    }\n  }\n\n  @keyframes fade-and-slide-in-from-top {\n    from {\n      opacity: 0;\n      -webkit-transform: translateY(-30px);\n              transform: translateY(-30px);\n    }\n    to {\n      opacity: 1;\n      -webkit-transform: none;\n              transform: none;\n    }\n  }\n\n  @keyframes fade-and-slide-out-from-top {\n    from {\n      opacity: 1;\n      -webkit-transform: none;\n              transform: none;\n    }\n    to {\n      opacity: 2;\n      -webkit-transform: translateY(-30px);\n              transform: translateY(-30px);\n    }\n  }\n\n  @keyframes rotateClockwise {\n    0% {\n      transform: scaleY(-1) rotate(0deg);\n    }\n    100% {\n      transform: scaleY(-1) rotate(360deg);\n    }\n  }\n\n  @keyframes rotateCounterClockwise {\n    0% {\n      transform: scaleY(-1) rotate(360deg);\n    }\n    100% {\n      transform: scaleY(-1) rotate(0deg);\n    }\n  }\n\n\n  // scrollbar styling\n  // the below media query targets non-touch devices\n  @media not all and (pointer: coarse) {\n    * {\n      scrollbar-color: ${(props) => props.theme.scrollbar.color} transparent;\n    }\n\n    *::-webkit-scrollbar {\n      width: 5px;\n    }\n\n    *::-webkit-scrollbar-track {\n      background: transparent;\n      border-radius: 5px;\n    }\n\n    *::-webkit-scrollbar-thumb {\n      background-color: ${(props) => props.theme.scrollbar.color};\n      border-radius: 14px;\n      border: 3px solid ${(props) => props.theme.scrollbar.color};\n    }\n  }\n\n  // Utility class for scrollbars that are hidden by default and shown on hover\n  .scrollbar-hover {\n    scrollbar-width: thin;\n    scrollbar-color: transparent transparent;\n\n    &::-webkit-scrollbar {\n      width: 5px;\n    }\n\n    &::-webkit-scrollbar-track {\n      background: transparent;\n      border-radius: 5px;\n    }\n\n    &::-webkit-scrollbar-thumb {\n      background-color: transparent;\n      border-radius: 14px;\n      border: 3px solid transparent;\n      background-clip: content-box;\n      transition: background-color 0.2s ease;\n    }\n\n    &:hover {\n      scrollbar-color: ${(props) => props.theme.scrollbar.color} transparent;\n\n      &::-webkit-scrollbar-thumb {\n        background-color: ${(props) => props.theme.scrollbar.color};\n      }\n    }\n\n    &::-webkit-scrollbar-thumb:hover {\n      background-color: ${(props) => props.theme.scrollbar.color};\n      opacity: 0.8;\n    }\n  }\n\n\n  // codemirror\n  .CodeMirror {\n    .cm-variable-valid {\n      color: ${(props) => props.theme.codemirror.variable.valid};\n    }\n    .cm-variable-invalid {\n      color: ${(props) => props.theme.codemirror.variable.invalid};\n    }\n    .cm-variable-prompt {\n      color: ${(props) => props.theme.codemirror.variable.prompt};\n    }\n  }\n  .CodeMirror-brunoVarInfo {\n    color: ${(props) => props.theme.text};\n    background: ${(props) => props.theme.dropdown.bg};\n    ${(props) =>\n      props.theme.dropdown.shadow && props.theme.dropdown.shadow !== 'none'\n        ? `box-shadow: ${props.theme.dropdown.shadow};`\n        : ''}\n    border-radius: ${(props) => props.theme.border.radius.base};\n    ${(props) =>\n      props.theme.dropdown.border && props.theme.dropdown.border !== 'none'\n        ? `border: 1px solid ${props.theme.dropdown.border};`\n        : ''}\n    box-sizing: border-box;\n    font-size: ${(props) => props.theme.font.size.base};\n    line-height: 1.25rem;\n    margin: 0;\n    min-width: 18.1875rem;\n    max-width: 18.1875rem;\n    opacity: 0;\n    overflow: visible;\n    padding: 0.5rem;\n    position: fixed;\n    transition: opacity 0.15s;\n    z-index: 10;\n  }\n\n  // Autocomplete hints dropdown container\n  .CodeMirror-hints {\n    z-index: 50 !important;\n    background: ${(props) => props.theme.dropdown.bg};\n    ${(props) =>\n      props.theme.dropdown.border !== 'none'\n        ? `border: 1px solid ${props.theme.dropdown.border};`\n        : ''}\n    ${(props) =>\n      props.theme.dropdown.shadow !== 'none'\n        ? `box-shadow: ${props.theme.dropdown.shadow};`\n        : ''}\n    border-radius: ${(props) => props.theme.border.radius.base};\n    padding: 0.25rem;\n    font-size: ${(props) => props.theme.font.size.sm};\n    font-family: inherit;\n  }\n\n  // Individual hint items\n  .CodeMirror-hint {\n    color: ${(props) => props.theme.dropdown.color};\n    border-radius: ${(props) => props.theme.border.radius.base};\n    line-height: 1.5rem;\n    font-size: ${(props) => props.theme.font.size.sm};\n    cursor: pointer;\n  }\n\n  .CodeMirror-brunoVarInfo :first-child {\n    margin-top: 0;\n  }\n\n  .CodeMirror-brunoVarInfo :last-child {\n    margin-bottom: 0;\n  }\n\n  .CodeMirror-brunoVarInfo p {\n    margin: 1em 0;\n  }\n\n  .CodeMirror-lint-tooltip {\n    padding: 4px 8px;\n    background-color: ${(props) => props.theme.infoTip.bg};\n    border: 1px solid ${(props) => props.theme.infoTip.border};\n    box-shadow: ${(props) => props.theme.infoTip.boxShadow};\n    border-radius: ${(props) => props.theme.border.radius.sm};\n  }\n\n  .CodeMirror-lint-message {\n    font-size: ${(props) => props.theme.font.size.xs};\n    color: ${(props) => props.theme.text};\n  }\n\n  .CodeMirror-lint-message-warning {\n    color: ${(props) => props.theme.status.warning.text};\n  }\n\n  .CodeMirror-lint-message-error {\n    color: ${(props) => props.theme.status.danger.text};\n  }\n\n  /* Header */\n  .CodeMirror-brunoVarInfo .var-info-header {\n    display: flex;\n    align-items: center;\n    margin-bottom: 0.375rem;\n    gap: 0.375rem;\n  }\n\n  .CodeMirror-brunoVarInfo .var-name {\n    font-size: ${(props) => props.theme.font.size.base};\n    color: ${(props) => props.theme.dropdown.color};\n    font-weight: 500;\n    flex: 1;\n    min-width: 0;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n\n  /* Scope Badge */\n  .CodeMirror-brunoVarInfo .var-scope-badge {\n    display: inline-block;\n    padding: 0.125rem 0.375rem;\n    background: ${(props) => rgba(props.theme.brand, 0.07)};\n    border: 1px solid ${(props) => rgba(props.theme.brand, 0.08)};\n    border-radius: ${(props) => props.theme.border.radius.base};\n    font-size: ${(props) => props.theme.font.size.xs};\n    color: ${(props) => props.theme.brand};\n    letter-spacing: 0.03125rem;\n    flex-shrink: 0;\n  }\n\n  /* Value Container */\n  .CodeMirror-brunoVarInfo .var-value-container {\n    position: relative;\n    border: 1px solid ${(props) => props.theme.border.border2};\n    border-radius: ${(props) => props.theme.border.radius.base};\n    background: ${(props) => props.theme.dropdown.hoverBg};\n    overflow-y: auto;\n    overflow-x: hidden;\n    min-width: 17.3125rem;\n    max-height: 13.1875rem;\n  }\n\n  /* Value Display (Read-only) */\n  .CodeMirror-brunoVarInfo .var-value-display {\n    padding: 0.375rem 1.5rem 0.375rem 0.5rem;\n    font-size: ${(props) => props.theme.font.size.base};\n    font-family: Inter, sans-serif;\n    font-weight: 400;\n    overflow-wrap: break-word;\n    white-space: pre-wrap;\n    line-height: 1.25rem;\n    color: ${(props) => props.theme.dropdown.color};\n    min-height: 1.75rem;\n    max-width: 17.1875rem;\n  }\n\n  /* Value Editor (CodeMirror) */\n  .CodeMirror-brunoVarInfo .var-value-editor {\n    width: 100%;\n    min-width: 17.1875rem;\n    max-width: 17.1875rem;\n    max-height: 11.125rem;\n    position: relative;\n  }\n\n  .CodeMirror-brunoVarInfo .var-value-editor .CodeMirror {\n    height: 100%;\n    min-height: 1.75rem;\n    max-height: 11.125rem;\n    font-size: ${(props) => props.theme.font.size.base};\n    font-family: Inter, sans-serif;\n    font-weight: 400;\n    line-height: 1.25rem;\n    border: 1px solid ${(props) => props.theme.input.focusBorder};\n    border-radius: ${(props) => props.theme.border.radius.base};\n    background: ${(props) => props.theme.dropdown.hoverBg};\n    color: ${(props) => props.theme.dropdown.color};\n    transition: border-color 0.15s;\n  }\n\n  .CodeMirror-brunoVarInfo .var-value-editor .CodeMirror-scroll {\n    min-height: 1.75rem;\n    max-height: 11.125rem;\n    overflow-y: auto !important;\n    overflow-x: hidden !important;\n  }\n\n  .CodeMirror-brunoVarInfo .var-value-editor .CodeMirror-focused {\n    background: ${(props) => props.theme.input.bg};\n    border-color: ${(props) => props.theme.input.focusBorder};\n  }\n\n  .CodeMirror-brunoVarInfo .var-value-editor .CodeMirror-lines {\n    padding: 0.375rem 1.5rem 0.375rem 0.5rem;\n    max-width: 13.1875rem;\n    font-family: Inter, sans-serif;\n    font-weight: 400;\n    line-height: 1.25rem;\n    word-break: break-all;\n    word-wrap: break-word;\n    overflow-wrap: break-word;\n  }\n\n  .CodeMirror-brunoVarInfo .var-value-editor .CodeMirror pre {\n    font-size: ${(props) => props.theme.font.size.base};\n    font-family: Inter, sans-serif;\n    font-weight: 400;\n    line-height: 1.25rem;\n    word-break: break-all;\n    word-wrap: break-word;\n    overflow-wrap: break-word;\n    white-space: pre-wrap;\n    color: ${(props) => props.theme.dropdown.color};\n  }\n\n  .CodeMirror-brunoVarInfo .var-value-editor .CodeMirror-line {\n    padding: 0;\n    max-width: 13.1875rem;\n    line-height: 1.25rem;\n    font-size: ${(props) => props.theme.font.size.base};\n    font-family: Inter, sans-serif;\n    font-weight: 400;\n    word-break: break-all;\n    word-wrap: break-word;\n    overflow-wrap: break-word;\n    color: ${(props) => props.theme.dropdown.color};\n  }\n\n  .CodeMirror-brunoVarInfo .var-value-editor .CodeMirror-sizer {\n    margin-left: 0 !important;\n    margin-bottom: 0 !important;\n    max-width: 13.1875rem !important;\n  }\n\n  /* Editable value display (shows interpolated value, click to edit) */\n  .CodeMirror-brunoVarInfo .var-value-editable-display {\n    width: 17.1875rem;\n    max-width: 13.1875rem;\n    padding: 0.375rem 1.5rem 0.375rem 0.5rem;\n    font-size: ${(props) => props.theme.font.size.base};\n    font-family: Inter, sans-serif;\n    font-weight: 400;\n    word-break: break-all;\n    word-wrap: break-word;\n    overflow-wrap: break-word;\n    white-space: pre-wrap;\n    line-height: 1.25rem;\n    color: ${(props) => props.theme.dropdown.color};\n    min-height: 1.75rem;\n    cursor: text;\n    border-radius: ${(props) => props.theme.border.radius.base};\n  }\n\n  /* Icons Container */\n  .CodeMirror-brunoVarInfo .var-icons {\n    position: absolute;\n    top: 0.375rem;\n    right: 0.5rem;\n    display: flex;\n    gap: 0.25rem;\n    z-index: 10;\n  }\n\n  .CodeMirror-brunoVarInfo .secret-toggle-button,\n  .CodeMirror-brunoVarInfo .copy-button {\n    background: transparent;\n    border: none;\n    cursor: pointer;\n    padding: 0.125rem;\n    opacity: 1;\n    transition: opacity 0.2s;\n    color: ${(props) => props.theme.dropdown.iconColor};\n    opacity: 0.7;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n  }\n\n  .CodeMirror-brunoVarInfo .secret-toggle-button:hover,\n  .CodeMirror-brunoVarInfo .copy-button:hover {\n    opacity: 1;\n  }\n\n  .CodeMirror-brunoVarInfo .copy-success {\n    color: ${(props) => props.theme.colors.text.green} !important;\n  }\n\n  /* Read-only Note */\n  .CodeMirror-brunoVarInfo .var-readonly-note {\n    font-size: ${(props) => props.theme.font.size.xs};\n    color: ${(props) => props.theme.dropdown.mutedText};\n    opacity: 0.6;\n    margin-top: 0.25rem;\n  }\n\n  .CodeMirror-brunoVarInfo .var-warning-note {\n    font-size: 0.75rem;\n    color: ${(props) => props.theme.colors.text.danger};\n    margin-top: 0.375rem;\n    line-height: 1.25rem;\n  }\n\n  // Active/selected hint - using theme colors instead of hardcoded blue\n  .CodeMirror-hint-active {\n    background: ${(props) => props.theme.dropdown.hoverBg} !important;\n    color: ${(props) => props.theme.dropdown.color} !important;\n  }\n\n  .hovered-link.CodeMirror-link {\n    text-decoration: underline !important;\n  }\n  .cmd-ctrl-pressed .hovered-link.CodeMirror-link[data-url] {\n    cursor: pointer;\n    color: ${(props) => props.theme.textLink} !important;\n  }\n\n  // Native select styling\n  select {\n    background-color: ${(props) => props.theme.input.bg};\n    color: ${(props) => props.theme.text};\n    font-size: ${(props) => props.theme.font.size.base};\n    font-weight: 400;\n  }\n\n  select option {\n    background-color: ${(props) => props.theme.dropdown.bg};\n    color: ${(props) => props.theme.dropdown.color};\n  }\n\n  select option:hover,\n  select option:focus {\n    background-color: ${(props) => props.theme.dropdown.hoverBg} !important;\n    color: ${(props) => props.theme.dropdown.color} !important;\n  }\n`;\n\nexport default GlobalStyle;\n"
  },
  {
    "path": "packages/bruno-app/src/hooks/useCollectionFolderTree/index.js",
    "content": "import { useState, useMemo, useCallback, useEffect } from 'react';\nimport { isItemAFolder } from 'utils/collections';\nimport { sortByNameThenSequence } from 'utils/common/index';\nimport filter from 'lodash/filter';\nimport { useSelector } from 'react-redux';\nimport { findCollectionByUid } from 'utils/collections';\n\nconst buildTree = (items) => {\n  const tree = {};\n\n  if (!items || items.length === 0) {\n    return tree;\n  }\n\n  const folders = filter(items, (i) => isItemAFolder(i) && !i.isTransient);\n  const sortedFolders = sortByNameThenSequence(folders);\n\n  for (const folder of sortedFolders) {\n    tree[folder.name] = {\n      uid: folder.uid,\n      name: folder.name,\n      item: folder,\n      children: folder.items && folder.items.length > 0 ? buildTree(folder.items) : {}\n    };\n  }\n\n  return tree;\n};\n\nconst findFolderByUidInTree = (tree, uid) => {\n  for (const folderName in tree) {\n    const folder = tree[folderName];\n    if (folder.uid === uid) {\n      return folder;\n    }\n    if (folder.children && Object.keys(folder.children).length > 0) {\n      const found = findFolderByUidInTree(folder.children, uid);\n      if (found) return found;\n    }\n  }\n  return null;\n};\n\nconst getFoldersAtPath = (tree, path) => {\n  if (path.length === 0) {\n    return Object.values(tree).map((folder) => folder.item);\n  }\n\n  let currentTree = tree;\n  for (const folderUid of path) {\n    const folder = findFolderByUidInTree(currentTree, folderUid);\n    if (folder && folder.children) {\n      currentTree = folder.children;\n    } else {\n      return [];\n    }\n  }\n\n  return Object.values(currentTree).map((folder) => folder.item);\n};\n\nconst useCollectionFolderTree = (collectionUid) => {\n  const collection = useSelector((state) => findCollectionByUid(state.collections.collections, collectionUid));\n  const [currentFolderPath, setCurrentFolderPath] = useState([]);\n  const [selectedFolderUid, setSelectedFolderUid] = useState(null);\n\n  const tree = useMemo(() => {\n    if (!collection || !collection.items) {\n      return {};\n    }\n    return buildTree(collection.items);\n  }, [collection]);\n\n  const currentFolders = useMemo(() => {\n    return getFoldersAtPath(tree, currentFolderPath);\n  }, [tree, currentFolderPath]);\n\n  const breadcrumbs = useMemo(() => {\n    if (currentFolderPath.length === 0) {\n      return [];\n    }\n\n    const breadcrumbParts = [];\n    let currentTree = tree;\n\n    for (const folderUid of currentFolderPath) {\n      const folder = findFolderByUidInTree(currentTree, folderUid);\n      if (folder) {\n        breadcrumbParts.push({\n          uid: folder.uid,\n          name: folder.name\n        });\n        currentTree = folder.children;\n      }\n    }\n\n    return breadcrumbParts;\n  }, [tree, currentFolderPath]);\n\n  const navigateIntoFolder = useCallback((folderUid) => {\n    setCurrentFolderPath((prev) => [...prev, folderUid]);\n    setSelectedFolderUid(folderUid);\n  }, []);\n\n  const goBack = useCallback(() => {\n    setCurrentFolderPath((prev) => {\n      if (prev.length > 0) {\n        return prev.slice(0, -1);\n      }\n      return prev;\n    });\n    setSelectedFolderUid(null);\n  }, []);\n\n  const navigateToRoot = useCallback(() => {\n    setCurrentFolderPath([]);\n    setSelectedFolderUid(null);\n  }, []);\n\n  const navigateToBreadcrumb = useCallback((index) => {\n    setCurrentFolderPath((prev) => prev.slice(0, index + 1));\n    setSelectedFolderUid(null);\n  }, []);\n\n  const getCurrentParentFolder = useCallback(() => {\n    if (currentFolderPath.length === 0) {\n      return null;\n    }\n    const lastFolderUid = currentFolderPath[currentFolderPath.length - 1];\n    const folder = findFolderByUidInTree(tree, lastFolderUid);\n    return folder ? folder.item : null;\n  }, [tree, currentFolderPath]);\n\n  const getCurrentSelectedFolder = useCallback(() => {\n    if (selectedFolderUid) {\n      const folder = findFolderByUidInTree(tree, selectedFolderUid);\n      return folder ? folder.item : null;\n    }\n    return null;\n  }, [tree, selectedFolderUid]);\n\n  const reset = useCallback(() => {\n    setCurrentFolderPath([]);\n    setSelectedFolderUid(null);\n  }, []);\n\n  useEffect(() => {\n    reset();\n  }, [collectionUid, reset]);\n\n  return {\n    currentFolders,\n    breadcrumbs,\n    selectedFolderUid,\n    setSelectedFolderUid,\n    navigateIntoFolder,\n    goBack,\n    navigateToRoot,\n    navigateToBreadcrumb,\n    getCurrentParentFolder,\n    getCurrentSelectedFolder,\n    reset,\n    isAtRoot: currentFolderPath.length === 0\n  };\n};\n\nexport default useCollectionFolderTree;\n"
  },
  {
    "path": "packages/bruno-app/src/hooks/useDebounce/index.js",
    "content": "import { useState, useEffect } from 'react';\n\nfunction useDebounce(value, delay) {\n  const [debouncedValue, setDebouncedValue] = useState(value);\n\n  useEffect(() => {\n    const handler = setTimeout(() => {\n      setDebouncedValue(value);\n    }, delay);\n\n    return () => {\n      clearTimeout(handler);\n    };\n  }, [value, delay]);\n\n  return debouncedValue;\n}\n\nexport default useDebounce;\n"
  },
  {
    "path": "packages/bruno-app/src/hooks/useDeferredLoading/index.js",
    "content": "import { useState, useEffect, useRef } from 'react';\n\n/**\n * A hook that defers showing loading state until a minimum delay has passed.\n * This prevents flickering UI for fast operations.\n *\n * @param {boolean} isLoading - The actual loading state\n * @param {number} delay - Minimum time (ms) before showing loading state (default: 200ms)\n * @returns {boolean} - The deferred loading state\n */\nfunction useDeferredLoading(isLoading, delay = 200) {\n  const [showLoading, setShowLoading] = useState(false);\n  const timerRef = useRef(null);\n\n  useEffect(() => {\n    if (isLoading) {\n      timerRef.current = setTimeout(() => {\n        setShowLoading(true);\n      }, delay);\n    } else {\n      if (timerRef.current) {\n        clearTimeout(timerRef.current);\n        timerRef.current = null;\n      }\n      setShowLoading(false);\n    }\n\n    return () => {\n      if (timerRef.current) {\n        clearTimeout(timerRef.current);\n        timerRef.current = null;\n      }\n    };\n  }, [isLoading, delay]);\n\n  return showLoading;\n}\n\nexport default useDeferredLoading;\n"
  },
  {
    "path": "packages/bruno-app/src/hooks/useDeferredLoading/index.spec.js",
    "content": "const { describe, it, expect, beforeEach, afterEach } = require('@jest/globals');\nimport { renderHook, act } from '@testing-library/react';\nimport useDeferredLoading from './index';\n\ndescribe('useDeferredLoading', () => {\n  beforeEach(() => {\n    jest.useFakeTimers();\n  });\n\n  afterEach(() => {\n    jest.useRealTimers();\n  });\n\n  it('should return false initially when isLoading is false', () => {\n    const { result } = renderHook(() => useDeferredLoading(false));\n    expect(result.current).toBe(false);\n  });\n\n  it('should not show loading immediately when isLoading becomes true', () => {\n    const { result } = renderHook(() => useDeferredLoading(true, 200));\n    expect(result.current).toBe(false);\n  });\n\n  it('should show loading after the delay has passed', () => {\n    const { result } = renderHook(() => useDeferredLoading(true, 200));\n\n    expect(result.current).toBe(false);\n\n    act(() => {\n      jest.advanceTimersByTime(200);\n    });\n\n    expect(result.current).toBe(true);\n  });\n\n  it('should not show loading if isLoading becomes false before delay', () => {\n    const { result, rerender } = renderHook(\n      ({ isLoading }) => useDeferredLoading(isLoading, 200),\n      { initialProps: { isLoading: true } }\n    );\n\n    expect(result.current).toBe(false);\n\n    act(() => {\n      jest.advanceTimersByTime(100);\n    });\n\n    expect(result.current).toBe(false);\n\n    rerender({ isLoading: false });\n\n    act(() => {\n      jest.advanceTimersByTime(200);\n    });\n\n    expect(result.current).toBe(false);\n  });\n\n  it('should reset to false immediately when isLoading becomes false', () => {\n    const { result, rerender } = renderHook(\n      ({ isLoading }) => useDeferredLoading(isLoading, 200),\n      { initialProps: { isLoading: true } }\n    );\n\n    act(() => {\n      jest.advanceTimersByTime(200);\n    });\n\n    expect(result.current).toBe(true);\n\n    rerender({ isLoading: false });\n\n    expect(result.current).toBe(false);\n  });\n\n  it('should use default delay of 200ms', () => {\n    const { result } = renderHook(() => useDeferredLoading(true));\n\n    expect(result.current).toBe(false);\n\n    act(() => {\n      jest.advanceTimersByTime(199);\n    });\n\n    expect(result.current).toBe(false);\n\n    act(() => {\n      jest.advanceTimersByTime(1);\n    });\n\n    expect(result.current).toBe(true);\n  });\n\n  it('should respect custom delay values', () => {\n    const { result } = renderHook(() => useDeferredLoading(true, 500));\n\n    act(() => {\n      jest.advanceTimersByTime(400);\n    });\n\n    expect(result.current).toBe(false);\n\n    act(() => {\n      jest.advanceTimersByTime(100);\n    });\n\n    expect(result.current).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/hooks/useDetectSensitiveField/index.js",
    "content": "import { useMemo } from 'react';\n\nconst VARIABLE_NAME_REGEX = /\\{\\{([^}]+)\\}\\}/g;\nconst ENV_VAR_REFERENCE_REGEX = /^\\s*\\{\\{.*\\}\\}\\s*$/;\n\nexport const useDetectSensitiveField = (collection) => {\n  const envVars = useMemo(() => {\n    if (!collection) {\n      return [];\n    }\n    const activeEnv = collection?.environments?.find((env) => env.uid === collection.activeEnvironmentUid);\n    if (!activeEnv || !Array.isArray(activeEnv.variables)) {\n      return [];\n    }\n    return activeEnv.variables;\n  }, [collection]);\n\n  // Checks if the value is a single environment variable reference (e.g., {{API_KEY}})\n  const isEnvVarReference = (value) => {\n    return typeof value === 'string' && ENV_VAR_REFERENCE_REGEX.test(value);\n  };\n\n  // Extracts all variable names from a string (e.g., \"Bearer {{TOKEN}}-{{SUFFIX}}\" → [\"TOKEN\", \"SUFFIX\"])\n  const extractVarNames = (value) => {\n    if (!value || typeof value !== 'string') {\n      return [];\n    }\n    const matches = [];\n    let match;\n    while ((match = VARIABLE_NAME_REGEX.exec(value)) !== null) {\n      matches.push(match[1].trim());\n    }\n    return matches;\n  };\n\n  // Checks if a variable is present and not marked as secret in the environment\n  const isVarNotSecret = (varName, envVars = []) => {\n    const found = envVars.find((v) => v.name === varName);\n    return found && !found.secret;\n  };\n\n  const isSensitive = (value) => {\n    if (value && !isEnvVarReference(value)) {\n      return {\n        showWarning: true,\n        warningMessage: 'Store sensitive info as a secret variable or in a .env file'\n      };\n    }\n\n    if (value && typeof value === 'string') {\n      const varNames = extractVarNames(value);\n      if (varNames.some((varName) => isVarNotSecret(varName, envVars))) {\n        return {\n          showWarning: true,\n          warningMessage: 'Mark the environment variable as secret for better security.'\n        };\n      }\n    }\n\n    // No warning needed\n    return { showWarning: false };\n  };\n\n  return {\n    isSensitive\n  };\n};\n"
  },
  {
    "path": "packages/bruno-app/src/hooks/useFocusTrap/index.js",
    "content": "import { useEffect, useRef } from 'react';\n\nconst useFocusTrap = (modalRef) => {\n  // refer to this implementation for modal focus: https://stackoverflow.com/a/38865836\n  const focusableSelector = 'a[href], area[href], input:not([disabled]):not([type=\"hidden\"]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex]:not([tabindex=\"-1\"]), *[contenteditable]';\n\n  useEffect(() => {\n    const modalElement = modalRef.current;\n    if (!modalElement) return;\n\n    const focusableElements = Array.from(document.querySelectorAll(focusableSelector));\n    const modalFocusableElements = Array.from(modalElement.querySelectorAll(focusableSelector));\n    const elementsToHide = focusableElements.filter((el) => !modalFocusableElements.includes(el));\n\n    // Hide elements outside the modal\n    elementsToHide.forEach((el) => {\n      const originalTabIndex = el.getAttribute('tabindex');\n      el.setAttribute('data-tabindex', originalTabIndex || 'inline');\n      el.setAttribute('tabindex', -1);\n    });\n\n    // Set focus to the first focusable element in the modal\n    const firstElement = modalFocusableElements[0];\n    const lastElement = modalFocusableElements[modalFocusableElements.length - 1];\n\n    const handleKeyDown = (event) => {\n      if (event.key === 'Tab') {\n        if (event.shiftKey && document.activeElement === firstElement) {\n          event.preventDefault();\n          lastElement.focus();\n        } else if (!event.shiftKey && document.activeElement === lastElement) {\n          event.preventDefault();\n          firstElement.focus();\n        }\n      }\n    };\n\n    modalElement.addEventListener('keydown', handleKeyDown);\n\n    return () => {\n      modalElement.removeEventListener('keydown', handleKeyDown);\n\n      // Restore original tabindex values\n      elementsToHide.forEach((el) => {\n        const originalTabIndex = el.getAttribute('data-tabindex');\n        el.setAttribute('tabindex', originalTabIndex === 'inline' ? '' : originalTabIndex);\n      });\n    };\n  }, [modalRef]);\n};\n\nexport default useFocusTrap;\n"
  },
  {
    "path": "packages/bruno-app/src/hooks/useLocalStorage/index.js",
    "content": "import { useState, useEffect } from 'react';\n\nexport default function useLocalStorage(key, defaultValue) {\n  const [value, setValue] = useState(() => {\n    try {\n      const saved = localStorage.getItem(key);\n      if (saved !== null) {\n        return JSON.parse(saved);\n      }\n      return defaultValue;\n    } catch {\n      return defaultValue;\n    }\n  });\n\n  useEffect(() => {\n    const rawValue = JSON.stringify(value);\n    localStorage.setItem(key, rawValue);\n  }, [key, value]);\n\n  return [value, setValue];\n}\n"
  },
  {
    "path": "packages/bruno-app/src/hooks/useOnClickOutside/index.js",
    "content": "// See https://usehooks.com/useOnClickOutside/\nimport { useEffect } from 'react';\n\nconst useOnClickOutside = (ref, handler, enabled = true) => {\n  useEffect(\n    () => {\n      if (!enabled) return;\n\n      const listener = (event) => {\n        // Do nothing if clicking ref's element or descendant elements\n        if (!ref.current || ref.current.contains(event.target)) {\n          return;\n        }\n\n        handler(event);\n      };\n\n      document.addEventListener('mousedown', listener);\n      document.addEventListener('touchstart', listener);\n\n      return () => {\n        document.removeEventListener('mousedown', listener);\n        document.removeEventListener('touchstart', listener);\n      };\n    },\n    // Add ref and handler to effect dependencies\n    // It's worth noting that because passed in handler is a new ...\n    // ... function on every render that will cause this effect ...\n    // ... callback/cleanup to run every render. It's not a big deal ...\n    // ... but to optimize you can wrap handler in useCallback before ...\n    // ... passing it into this hook.\n    [ref, handler, enabled]\n  );\n};\n\nexport default useOnClickOutside;\n"
  },
  {
    "path": "packages/bruno-app/src/hooks/usePrevious/index.js",
    "content": "import { useRef, useEffect } from 'react';\n\nfunction usePrevious(value) {\n  const ref = useRef();\n\n  useEffect(() => {\n    ref.current = value; // assign the value of ref to the argument\n  }, [value]); // this code will run when the value of 'value' changes\n\n  return ref.current; // in the end, return the current ref value.\n}\n\nexport default usePrevious;\n"
  },
  {
    "path": "packages/bruno-app/src/hooks/useProtoFileManagement/index.js",
    "content": "import { useState, useRef, useMemo } from 'react';\nimport { useDispatch } from 'react-redux';\nimport { browseFiles, updateBrunoConfig } from 'providers/ReduxStore/slices/collections/actions';\nimport { updateCollectionProtobuf } from 'providers/ReduxStore/slices/collections';\nimport { getRelativePath, getAbsoluteFilePath } from 'utils/common/path';\nimport { browseDirectory } from 'utils/filesystem';\nimport { loadGrpcMethodsFromProtoFile } from 'utils/network/index';\nimport useLocalStorage from 'hooks/useLocalStorage/index';\nimport { cloneDeep } from 'lodash';\nimport get from 'lodash/get';\n\n/**\n * Custom hook for managing protofile data and collection configuration\n * @param {Object} collection - The collection object\n * @param {string} currentProtoPath - Currently selected proto file path\n */\nexport default function useProtoFileManagement(collection) {\n  const dispatch = useDispatch();\n\n  const [protofileCache, setProtofileCache] = useLocalStorage('bruno.grpc.protofileCache', {});\n  const [isLoadingMethods, setIsLoadingMethods] = useState(false);\n\n  // Get protobuf config from draft if exists, otherwise from brunoConfig\n  const protobufConfig = collection?.draft?.brunoConfig\n    ? get(collection, 'draft.brunoConfig.protobuf', {})\n    : get(collection, 'brunoConfig.protobuf', {});\n\n  const collectionProtoFiles = useMemo(() => protobufConfig?.protoFiles || [], [protobufConfig?.protoFiles]);\n  const collectionImportPaths = useMemo(() => protobufConfig?.importPaths || [], [protobufConfig?.importPaths]);\n\n  const protoFilesWithExistence = useMemo(() =>\n    collectionProtoFiles.map((protoFile) => ({\n      path: protoFile.path,\n      exists: protoFile.exists || false\n    })), [collectionProtoFiles]);\n\n  const importPathsWithExistence = useMemo(() =>\n    collectionImportPaths.map((importPath) => ({\n      path: importPath.path,\n      exists: importPath.exists || false,\n      enabled: importPath.enabled || false\n    })), [collectionImportPaths]);\n\n  const loadMethodsFromProtoFile = async (filePath, isManualRefresh = false) => {\n    if (!filePath) {\n      return { methods: [], error: new Error('No proto file selected') };\n    }\n\n    const absolutePath = getAbsoluteFilePath(collection.pathname, filePath);\n\n    const cachedMethods = protofileCache[absolutePath];\n    if (cachedMethods && !isLoadingMethods && !isManualRefresh) {\n      return { methods: cachedMethods, error: null, fromCache: true };\n    }\n\n    setIsLoadingMethods(true);\n    try {\n      const { methods, error } = await loadGrpcMethodsFromProtoFile(absolutePath, collection);\n\n      if (error) {\n        console.error('Error loading gRPC methods:', error);\n        return { methods: [], error };\n      }\n\n      setProtofileCache((prevCache) => ({\n        ...prevCache,\n        [absolutePath]: methods\n      }));\n\n      return { methods, error: null, fromCache: false };\n    } catch (err) {\n      console.error('Error loading gRPC methods:', err);\n      return { methods: [], error: err };\n    } finally {\n      setIsLoadingMethods(false);\n    }\n  };\n\n  const addProtoFileToCollection = async (filePath) => {\n    const relativePath = getRelativePath(collection.pathname, filePath, true);\n\n    const exists = collectionProtoFiles.some((pf) => pf.path === relativePath);\n\n    if (exists) {\n      return { success: true, relativePath, alreadyExists: true };\n    }\n\n    try {\n      const protoFileObj = {\n        path: relativePath,\n        type: 'file',\n        exists: true\n      };\n\n      const updatedProtobuf = {\n        ...protobufConfig,\n        protoFiles: [...collectionProtoFiles, protoFileObj]\n      };\n\n      dispatch(updateCollectionProtobuf({\n        collectionUid: collection.uid,\n        protobuf: updatedProtobuf\n      }));\n\n      return { success: true, relativePath };\n    } catch (error) {\n      console.error('Error adding proto file to collection:', error);\n      return { success: false, error };\n    }\n  };\n\n  const addProtoFileFromRequest = async (filePath) => {\n    const relativePath = getRelativePath(collection.pathname, filePath, true);\n\n    const exists = collectionProtoFiles.some((pf) => pf.path === relativePath);\n\n    if (exists) {\n      return { success: true, relativePath, alreadyExists: true };\n    }\n\n    try {\n      const protoFileObj = {\n        path: relativePath,\n        type: 'file'\n      };\n\n      const brunoConfig = cloneDeep(collection.brunoConfig);\n      if (!brunoConfig.protobuf) {\n        brunoConfig.protobuf = {};\n      }\n      if (!brunoConfig.protobuf.protoFiles) {\n        brunoConfig.protobuf.protoFiles = [];\n      }\n\n      brunoConfig.protobuf.protoFiles = [...collectionProtoFiles, protoFileObj];\n\n      await dispatch(updateBrunoConfig(brunoConfig, collection.uid));\n\n      return { success: true, relativePath };\n    } catch (error) {\n      console.error('Error adding proto file to collection:', error);\n      return { success: false, error };\n    }\n  };\n\n  const addImportPathToCollection = async (directoryPath) => {\n    const relativePath = getRelativePath(collection.pathname, directoryPath, true);\n    const importPathObj = {\n      path: relativePath,\n      enabled: true,\n      exists: true\n    };\n\n    const exists = collectionImportPaths.some((ip) => ip.path === importPathObj.path);\n\n    if (exists) {\n      return { success: false, error: new Error('Import path already exists') };\n    }\n\n    try {\n      const updatedProtobuf = {\n        ...protobufConfig,\n        importPaths: [...collectionImportPaths, importPathObj]\n      };\n\n      dispatch(updateCollectionProtobuf({\n        collectionUid: collection.uid,\n        protobuf: updatedProtobuf\n      }));\n\n      return { success: true, relativePath };\n    } catch (error) {\n      console.error('Error adding import path:', error);\n      return { success: false, error };\n    }\n  };\n\n  const addImportPathFromRequest = async (directoryPath) => {\n    const relativePath = getRelativePath(collection.pathname, directoryPath, true);\n    const importPathObj = {\n      path: relativePath,\n      enabled: true\n    };\n\n    const exists = collectionImportPaths.some((ip) => ip.path === importPathObj.path);\n\n    if (exists) {\n      return { success: false, error: new Error('Import path already exists') };\n    }\n\n    try {\n      const brunoConfig = cloneDeep(collection.brunoConfig);\n      if (!brunoConfig.protobuf) {\n        brunoConfig.protobuf = {};\n      }\n      if (!brunoConfig.protobuf.importPaths) {\n        brunoConfig.protobuf.importPaths = [];\n      }\n\n      brunoConfig.protobuf.importPaths = [...collectionImportPaths, importPathObj];\n\n      await dispatch(updateBrunoConfig(brunoConfig, collection.uid));\n\n      return { success: true, relativePath };\n    } catch (error) {\n      console.error('Error adding import path:', error);\n      return { success: false, error };\n    }\n  };\n\n  const toggleImportPath = async (index) => {\n    try {\n      const updatedImportPaths = [...collectionImportPaths];\n      updatedImportPaths[index] = {\n        ...updatedImportPaths[index],\n        enabled: !updatedImportPaths[index].enabled\n      };\n\n      const updatedProtobuf = {\n        ...protobufConfig,\n        importPaths: updatedImportPaths\n      };\n\n      dispatch(updateCollectionProtobuf({\n        collectionUid: collection.uid,\n        protobuf: updatedProtobuf\n      }));\n\n      return {\n        success: true,\n        enabled: updatedImportPaths[index].enabled\n      };\n    } catch (error) {\n      console.error('Error toggling import path:', error);\n      return { success: false, error };\n    }\n  };\n\n  const toggleImportPathFromRequest = async (index) => {\n    try {\n      const updatedImportPaths = [...collectionImportPaths];\n      updatedImportPaths[index] = {\n        ...updatedImportPaths[index],\n        enabled: !updatedImportPaths[index].enabled\n      };\n\n      const brunoConfig = cloneDeep(collection.brunoConfig);\n      if (!brunoConfig.protobuf) {\n        brunoConfig.protobuf = {};\n      }\n      brunoConfig.protobuf.importPaths = updatedImportPaths;\n\n      await dispatch(updateBrunoConfig(brunoConfig, collection.uid));\n\n      return {\n        success: true,\n        enabled: updatedImportPaths[index].enabled\n      };\n    } catch (error) {\n      console.error('Error toggling import path:', error);\n      return { success: false, error };\n    }\n  };\n\n  const browseForProtoFile = async () => {\n    const filters = [{ name: 'Proto Files', extensions: ['proto'] }];\n\n    try {\n      const filePaths = await dispatch(browseFiles(filters, ['']));\n      if (filePaths && filePaths.length > 0) {\n        return { success: true, filePath: filePaths[0] };\n      }\n      return { success: false, error: new Error('No file selected') };\n    } catch (error) {\n      console.error('Error browsing for proto file:', error);\n      return { success: false, error };\n    }\n  };\n\n  const browseForImportDirectory = async () => {\n    try {\n      const selectedPath = await browseDirectory(collection.pathname);\n      if (selectedPath) {\n        return { success: true, directoryPath: selectedPath };\n      }\n      return { success: false, error: new Error('No directory selected') };\n    } catch (error) {\n      console.error('Error browsing for import directory:', error);\n      return { success: false, error };\n    }\n  };\n\n  const removeProtoFileFromCollection = async (index) => {\n    try {\n      const updatedProtoFiles = [...collectionProtoFiles];\n      updatedProtoFiles.splice(index, 1);\n\n      const updatedProtobuf = {\n        ...protobufConfig,\n        protoFiles: updatedProtoFiles\n      };\n\n      dispatch(updateCollectionProtobuf({\n        collectionUid: collection.uid,\n        protobuf: updatedProtobuf\n      }));\n\n      return { success: true };\n    } catch (error) {\n      console.error('Error removing proto file:', error);\n      return { success: false, error };\n    }\n  };\n\n  const removeImportPathFromCollection = async (index) => {\n    try {\n      const updatedImportPaths = [...collectionImportPaths];\n      updatedImportPaths.splice(index, 1);\n\n      const updatedProtobuf = {\n        ...protobufConfig,\n        importPaths: updatedImportPaths\n      };\n\n      dispatch(updateCollectionProtobuf({\n        collectionUid: collection.uid,\n        protobuf: updatedProtobuf\n      }));\n\n      return { success: true };\n    } catch (error) {\n      console.error('Error removing import path:', error);\n      return { success: false, error };\n    }\n  };\n\n  const replaceImportPathInCollection = async (index, newDirectoryPath) => {\n    try {\n      const relativePath = getRelativePath(collection.pathname, newDirectoryPath, true);\n      const updatedImportPaths = [...collectionImportPaths];\n      updatedImportPaths[index] = {\n        ...updatedImportPaths[index],\n        path: relativePath,\n        exists: true\n      };\n\n      const updatedProtobuf = {\n        ...protobufConfig,\n        importPaths: updatedImportPaths\n      };\n\n      dispatch(updateCollectionProtobuf({\n        collectionUid: collection.uid,\n        protobuf: updatedProtobuf\n      }));\n\n      return { success: true };\n    } catch (error) {\n      console.error('Error replacing import path:', error);\n      return { success: false, error };\n    }\n  };\n\n  const replaceProtoFileInCollection = async (index, newFilePath) => {\n    try {\n      const relativePath = getRelativePath(collection.pathname, newFilePath, true);\n      const updatedProtoFiles = [...collectionProtoFiles];\n      updatedProtoFiles[index] = {\n        ...updatedProtoFiles[index],\n        path: relativePath,\n        type: 'file',\n        exists: true\n      };\n\n      const updatedProtobuf = {\n        ...protobufConfig,\n        protoFiles: updatedProtoFiles\n      };\n\n      dispatch(updateCollectionProtobuf({\n        collectionUid: collection.uid,\n        protobuf: updatedProtobuf\n      }));\n\n      return { success: true };\n    } catch (error) {\n      console.error('Error replacing proto file:', error);\n      return { success: false, error };\n    }\n  };\n\n  return {\n    protoFiles: protoFilesWithExistence,\n    importPaths: importPathsWithExistence,\n    isLoadingMethods,\n    loadMethodsFromProtoFile,\n    addProtoFileToCollection,\n    addImportPathToCollection,\n    addImportPathFromRequest,\n    toggleImportPath,\n    toggleImportPathFromRequest,\n    browseForProtoFile,\n    browseForImportDirectory,\n    removeProtoFileFromCollection,\n    removeImportPathFromCollection,\n    replaceImportPathInCollection,\n    replaceProtoFileInCollection,\n    addProtoFileFromRequest\n  };\n}\n"
  },
  {
    "path": "packages/bruno-app/src/hooks/useReflectionManagement/index.js",
    "content": "import { useState } from 'react';\nimport { useDispatch } from 'react-redux';\nimport { loadGrpcMethodsFromReflection } from 'providers/ReduxStore/slices/collections/actions';\nimport useLocalStorage from 'hooks/useLocalStorage/index';\n\n/**\n * Custom hook for managing reflection data and server discovery\n * @param {Object} item - The request item\n * @param {string} collectionUid - Collection UID\n */\nexport default function useReflectionManagement(item, collectionUid) {\n  const dispatch = useDispatch();\n\n  const [reflectionCache, setReflectionCache] = useLocalStorage('bruno.grpc.reflectionCache', {});\n  const [isLoadingMethods, setIsLoadingMethods] = useState(false);\n\n  /**\n   * Load gRPC methods from server reflection\n   * @param {string} url - The gRPC server URL\n   * @param {boolean} isManualRefresh - Whether this is a manual refresh\n   * @returns {Promise<{methods: Array, error: Error|null}>}\n   */\n  const loadMethodsFromReflection = async (url, isManualRefresh = false) => {\n    if (!url) {\n      return { methods: [], error: new Error('No URL provided') };\n    }\n\n    const cachedMethods = reflectionCache[url];\n    if (!isManualRefresh && cachedMethods && !isLoadingMethods) {\n      return { methods: cachedMethods, error: null, fromCache: true };\n    }\n\n    setIsLoadingMethods(true);\n    try {\n      const { methods, error } = await dispatch(loadGrpcMethodsFromReflection(item, collectionUid, url));\n\n      if (error) {\n        console.error('Error loading gRPC methods:', error);\n        return { methods: [], error };\n      }\n\n      setReflectionCache((prevCache) => ({\n        ...prevCache,\n        [url]: methods\n      }));\n\n      return { methods, error: null, fromCache: false };\n    } catch (error) {\n      console.error('Error loading gRPC methods:', error);\n      return { methods: [], error };\n    } finally {\n      setIsLoadingMethods(false);\n    }\n  };\n\n  /**\n   * Check if methods are cached for a URL\n   * @param {string} url - The gRPC server URL\n   * @returns {boolean}\n   */\n  const hasCachedMethods = (url) => {\n    return !!(reflectionCache[url] && reflectionCache[url].length > 0);\n  };\n\n  /**\n   * Get cached methods for a URL\n   * @param {string} url - The gRPC server URL\n   * @returns {Array}\n   */\n  const getCachedMethods = (url) => {\n    return reflectionCache[url] || [];\n  };\n\n  /**\n   * Clear cache for a specific URL\n   * @param {string} url - The gRPC server URL\n   */\n  const clearCacheForUrl = (url) => {\n    setReflectionCache((prevCache) => {\n      const newCache = { ...prevCache };\n      delete newCache[url];\n      return newCache;\n    });\n  };\n\n  /**\n   * Clear all reflection cache\n   */\n  const clearAllCache = () => {\n    setReflectionCache({});\n  };\n\n  return {\n    isLoadingMethods,\n    reflectionCache,\n    loadMethodsFromReflection,\n    hasCachedMethods,\n    getCachedMethods,\n    clearCacheForUrl,\n    clearAllCache\n  };\n}\n"
  },
  {
    "path": "packages/bruno-app/src/hooks/useTabPaneBoundaries/index.js",
    "content": "import find from 'lodash/find';\nimport { updateRequestPaneTabHeight, updateRequestPaneTabWidth } from 'providers/ReduxStore/slices/tabs';\nimport { useDispatch, useSelector } from 'react-redux';\n\nconst MIN_TOP_PANE_HEIGHT = 380;\n\nexport function useTabPaneBoundaries(activeTabUid) {\n  const DEFAULT_PANE_WIDTH_DIVISOR = 2.2;\n\n  const tabs = useSelector((state) => state.tabs.tabs);\n  const focusedTab = find(tabs, (t) => t.uid === activeTabUid);\n  const screenWidth = useSelector((state) => state.app.screenWidth);\n  let asideWidth = useSelector((state) => state.app.leftSidebarWidth);\n  const left = focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : (screenWidth - asideWidth) / DEFAULT_PANE_WIDTH_DIVISOR;\n  const top = focusedTab?.requestPaneHeight || MIN_TOP_PANE_HEIGHT;\n  const dispatch = useDispatch();\n\n  return {\n    left,\n    top,\n    setLeft(value) {\n      dispatch(updateRequestPaneTabWidth({\n        uid: activeTabUid,\n        requestPaneWidth: value\n      }));\n    },\n    setTop(value) {\n      dispatch(updateRequestPaneTabHeight({\n        uid: activeTabUid,\n        requestPaneHeight: value\n      }));\n    },\n    reset() {\n      dispatch(updateRequestPaneTabHeight({\n        uid: activeTabUid,\n        requestPaneHeight: MIN_TOP_PANE_HEIGHT\n      }));\n      dispatch(updateRequestPaneTabWidth({\n        uid: activeTabUid,\n        requestPaneWidth: (screenWidth - asideWidth) / DEFAULT_PANE_WIDTH_DIVISOR\n      }));\n    }\n  };\n}\n"
  },
  {
    "path": "packages/bruno-app/src/i18n/index.js",
    "content": "import i18n from 'i18next';\nimport { initReactI18next } from 'react-i18next';\nimport translationEn from './translation/en.json';\n\nconst resources = {\n  en: {\n    translation: translationEn\n  }\n};\n\ni18n\n  .use(initReactI18next) // passes i18n down to react-i18next\n  .init({\n    resources,\n    lng: 'en', // Use \"en\" as the default language. \"cimode\" can be used to debug / show translation placeholder\n\n    ns: 'translation', // Use translation as the default Namespace that will be loaded by default\n\n    interpolation: {\n      escapeValue: false // react already safes from xss\n    }\n  });\n\nexport default i18n;\n"
  },
  {
    "path": "packages/bruno-app/src/i18n/translation/en.json",
    "content": "{\n    \"COMMON\": {\n        \"COLLECTIONS\": \"Collections\",\n        \"DOCUMENTATION\": \"Documentation\",\n        \"REPORT_ISSUES\": \"Report Issues\",\n        \"GITHUB\": \"GitHub\",\n        \"DISCORD\": \"Discord\",\n        \"TWITTER\": \"Twitter\"\n    },\n    \"WELCOME\": {\n        \"ABOUT_BRUNO\": \"Opensource IDE for exploring and testing APIs\",\n        \"LINKS\": \"Links\",\n        \"CREATE_COLLECTION\": \"Create Collection\",\n        \"OPEN_COLLECTION\": \"Open Collection\",\n        \"IMPORT_COLLECTION\": \"Import Collection\",\n        \"COLLECTION_IMPORT_SUCCESS\": \"Collection imported successfully\",\n        \"COLLECTION_IMPORT_ERROR\": \"An error occurred while importing the collection. Check the logs for more information.\",\n        \"COLLECTION_OPEN_ERROR\": \"An error occurred while opening the collection\",\n        \"GLOBAL_SEARCH_TIP_PART1\": \"Press\",\n        \"GLOBAL_SEARCH_TIP_PART2\": \"(mac) or\",\n        \"GLOBAL_SEARCH_TIP_PART3\": \"(windows) anytime to quickly search collections, folders, and requests\"\n    }\n}\n"
  },
  {
    "path": "packages/bruno-app/src/index.js",
    "content": "import React, { useEffect } from 'react';\nimport ReactDOM from 'react-dom/client';\nimport App from './pages/index';\nimport { DndProvider } from 'react-dnd';\nimport { HTML5Backend } from 'react-dnd-html5-backend';\n\nconst rootElement = document.getElementById('root');\n\nconst Main = () => {\n  useEffect(() => {\n    const link = document.createElement('link');\n    link.rel = 'stylesheet';\n    link.type = 'text/css';\n    link.href = `static/diff2html.min.css`;\n    document.head.appendChild(link);\n    const script = document.createElement('script');\n    script.type = 'text/javascript';\n    script.src = `static/diff2Html.js`;\n    script.async = true;\n    document.body.appendChild(script);\n\n    return () => {\n      document.head.removeChild(link);\n      document.body.removeChild(script);\n    };\n  }, []);\n\n  return (\n    <React.StrictMode>\n      <DndProvider backend={HTML5Backend}>\n        <App />\n      </DndProvider>\n    </React.StrictMode>\n  );\n};\n\nif (rootElement) {\n  const root = ReactDOM.createRoot(rootElement);\n  root.render(<Main />);\n}\n"
  },
  {
    "path": "packages/bruno-app/src/pages/Bruno/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst Wrapper = styled.div`\n  display: flex;\n  width: 100%;\n  height: 100%;\n  flex: 1;\n  border-top: 1px solid ${(props) => props.theme.sidebar.collection.item.hoverBg};\n\n  &.is-dragging {\n    cursor: col-resize !important;\n  }\n\n  section.main {\n    display: flex;\n\n    section.request-pane,\n    section.response-pane {\n      overflow: hidden;\n    }\n  }\n\n  .fw-600 {\n    font-weight: 500;\n  }\n`;\n\nexport default Wrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/pages/Bruno/index.js",
    "content": "import React, { useState, useRef, useEffect } from 'react';\nimport classnames from 'classnames';\nimport ManageWorkspace from 'components/ManageWorkspace';\nimport RequestTabs from 'components/RequestTabs';\nimport RequestTabPanel from 'components/RequestTabPanel';\nimport Sidebar from 'components/Sidebar';\nimport StatusBar from 'components/StatusBar';\nimport AppTitleBar from 'components/AppTitleBar';\nimport ApiSpecPanel from 'components/ApiSpecPanel';\n// import ErrorCapture from 'components/ErrorCapture';\nimport { useSelector } from 'react-redux';\nimport { isElectron } from 'utils/common/platform';\nimport StyledWrapper from './StyledWrapper';\nimport 'codemirror/theme/material.css';\nimport 'codemirror/theme/monokai.css';\nimport 'codemirror/addon/scroll/simplescrollbars.css';\nimport 'swagger-ui-react/swagger-ui.css';\nimport Devtools from 'components/Devtools';\nimport useGrpcEventListeners from 'utils/network/grpc-event-listeners';\nimport useWsEventListeners from 'utils/network/ws-event-listeners';\nimport Portal from 'components/Portal';\nimport SaveTransientRequestContainer from 'components/SaveTransientRequest/Container';\nimport SaveTransientRequest from 'components/SaveTransientRequest';\n\nrequire('codemirror/mode/javascript/javascript');\nrequire('codemirror/mode/xml/xml');\nrequire('codemirror/mode/sparql/sparql');\nrequire('codemirror/addon/comment/comment');\nrequire('codemirror/addon/dialog/dialog');\nrequire('codemirror/addon/edit/closebrackets');\nrequire('codemirror/addon/edit/matchbrackets');\nrequire('codemirror/addon/fold/brace-fold');\nrequire('codemirror/addon/fold/foldgutter');\nrequire('codemirror/addon/fold/xml-fold');\nrequire('codemirror/addon/hint/javascript-hint');\nrequire('codemirror/addon/hint/show-hint');\nrequire('codemirror/addon/lint/lint');\nrequire('codemirror/addon/lint/json-lint');\nrequire('codemirror/addon/mode/overlay');\nrequire('codemirror/addon/scroll/simplescrollbars');\nrequire('codemirror/addon/search/jump-to-line');\nrequire('codemirror/addon/search/search');\nrequire('codemirror/addon/search/searchcursor');\nrequire('codemirror/addon/display/placeholder');\nrequire('codemirror/keymap/sublime');\n\nrequire('codemirror-graphql/hint');\nrequire('codemirror-graphql/info');\nrequire('codemirror-graphql/jump');\nrequire('codemirror-graphql/lint');\nrequire('codemirror-graphql/mode');\n\nrequire('utils/codemirror/brunoVarInfo');\nrequire('utils/codemirror/javascript-lint');\nrequire('utils/codemirror/autocomplete');\n\nconst TransientRequestModalsRenderer = ({ modals }) => {\n  if (modals.length === 0) {\n    return null;\n  }\n\n  if (modals.length === 1) {\n    return (\n      <SaveTransientRequest\n        item={modals[0].item}\n        collection={modals[0].collection}\n        isOpen={true}\n      />\n    );\n  }\n\n  return <SaveTransientRequestContainer />;\n};\n\nexport default function Main() {\n  const activeTabUid = useSelector((state) => state.tabs.activeTabUid);\n  const activeApiSpecUid = useSelector((state) => state.apiSpec.activeApiSpecUid);\n  const isDragging = useSelector((state) => state.app.isDragging);\n  const showApiSpecPage = useSelector((state) => state.app.showApiSpecPage);\n  const showManageWorkspacePage = useSelector((state) => state.app.showManageWorkspacePage);\n  const isConsoleOpen = useSelector((state) => state.logs.isConsoleOpen);\n  const saveTransientRequestModals = useSelector((state) => state.collections.saveTransientRequestModals);\n  const mainSectionRef = useRef(null);\n  const [showRosettaBanner, setShowRosettaBanner] = useState(false);\n\n  // Initialize event listeners\n  useGrpcEventListeners();\n  useWsEventListeners();\n\n  const className = classnames({\n    'is-dragging': isDragging\n  });\n\n  useEffect(() => {\n    if (!isElectron()) {\n      return;\n    }\n\n    const { ipcRenderer } = window;\n\n    const removeAppLoadedListener = ipcRenderer.on('main:app-loaded', (init) => {\n      if (mainSectionRef.current) {\n        mainSectionRef.current.setAttribute('data-app-state', 'loaded');\n      }\n      setShowRosettaBanner(init.isRunningInRosetta);\n    });\n\n    return () => {\n      removeAppLoadedListener();\n    };\n  }, []);\n\n  return (\n    // <ErrorCapture>\n    <div id=\"main-container\" className=\"flex flex-col h-screen max-h-screen overflow-hidden\">\n      <AppTitleBar />\n      {showRosettaBanner ? (\n        <Portal>\n          <div className=\"fixed bottom-0 left-0 right-0 z-10 bg-amber-100 border border-amber-400 text-amber-700 px-4 py-3\" role=\"alert\">\n            <strong className=\"font-bold\">WARNING:</strong>\n            <div>\n              It looks like Bruno was launched as the Intel (x64) build under Rosetta on your Apple Silicon Mac. This can cause reduced performance and unexpected behavior.\n            </div>\n            <button className=\"absolute right-2 top-0 text-xl\" onClick={() => setShowRosettaBanner(!showRosettaBanner)}>\n              &times;\n            </button>\n          </div>\n        </Portal>\n      ) : null}\n      <div\n        ref={mainSectionRef}\n        className=\"flex-1 min-h-0 flex\"\n        data-app-state=\"loading\"\n        style={{\n          height: isConsoleOpen ? `calc(100vh - 60px - ${isConsoleOpen ? '300px' : '0px'})` : 'calc(100vh - 60px)'\n        }}\n      >\n        <StyledWrapper className={className} style={{ height: '100%', zIndex: 1 }}>\n          <Sidebar />\n          <section className=\"flex flex-grow flex-col overflow-hidden\">\n            {showApiSpecPage && activeApiSpecUid ? (\n              <ApiSpecPanel key={activeApiSpecUid} />\n            ) : showManageWorkspacePage ? (\n              <ManageWorkspace />\n            ) : (\n              <>\n                <RequestTabs />\n                <RequestTabPanel key={activeTabUid} />\n              </>\n            )}\n          </section>\n        </StyledWrapper>\n      </div>\n\n      <Devtools mainSectionRef={mainSectionRef} />\n      <StatusBar />\n      <TransientRequestModalsRenderer modals={saveTransientRequestModals} />\n    </div>\n    // </ErrorCapture>\n  );\n}\n"
  },
  {
    "path": "packages/bruno-app/src/pages/ErrorBoundary/index.js",
    "content": "import React from 'react';\n\nimport Bruno from 'components/Bruno/index';\n\nclass ErrorBoundary extends React.Component {\n  constructor(props) {\n    super(props);\n\n    this.state = { hasError: false };\n  }\n\n  componentDidMount() {\n    // Add a global error event listener to capture client-side errors\n    window.onerror = (message, source, lineno, colno, error) => {\n      this.setState({ hasError: true, error });\n    };\n  }\n\n  componentDidCatch(error, errorInfo) {\n    console.log({ error, errorInfo });\n    this.setState({ hasError: true, error, errorInfo });\n  }\n\n  returnToApp() {\n    const { ipcRenderer } = window;\n    ipcRenderer.invoke('open-file');\n\n    this.setState({ hasError: false, error: null, errorInfo: null });\n  }\n\n  forceQuit() {\n    const { ipcRenderer } = window;\n    ipcRenderer.invoke('main:force-quit');\n  }\n\n  render() {\n    if (this.state.hasError) {\n      return (\n        <div className=\"flex text-center justify-center p-20 h-full\">\n          <div className=\"bg-white rounded-lg p-10 w-full\">\n            <div className=\"m-auto\" style={{ width: '256px' }}>\n              <Bruno width={256} />\n            </div>\n\n            <h1 className=\"text-2xl font-medium text-red-600 mb-2\">Oops! Something went wrong</h1>\n            <p className=\"text-red-500 mb-2\">\n              If you are using an official production build: the above error is most likely a bug!\n              <br />\n              Please report this under:\n              <a\n                className=\"text-link hover:underline cursor-pointer ml-2\"\n                href=\"https://github.com/usebruno/bruno/issues\"\n                target=\"_blank\"\n              >\n                https://github.com/usebruno/bruno/issues\n              </a>\n            </p>\n\n            <button\n              className=\"bg-red-500 text-white px-4 py-2 mt-4 rounded hover:bg-red-600 transition\"\n              onClick={() => this.returnToApp()}\n            >\n              Return to App\n            </button>\n\n            <div className=\"text-red-500 mt-3\">\n              <a href=\"\" className=\"hover:underline cursor-pointer\" onClick={this.forceQuit}>\n                Force Quit\n              </a>\n            </div>\n          </div>\n        </div>\n      );\n    }\n\n    return this.props.children;\n  }\n}\n\nexport default ErrorBoundary;\n"
  },
  {
    "path": "packages/bruno-app/src/pages/Main.js",
    "content": "import { useState, useEffect } from 'react';\nimport { Provider } from 'react-redux';\nimport { AppProvider } from 'providers/App';\nimport { ToastProvider } from 'providers/Toaster';\nimport { HotkeysProvider } from 'providers/Hotkeys';\nimport { PromptVariablesProvider } from 'providers/PromptVariables';\n\nimport ReduxStore from 'providers/ReduxStore';\nimport ThemeProvider from 'providers/Theme/index';\nimport ErrorBoundary from './ErrorBoundary';\n\nimport '../styles/globals.css';\nimport 'codemirror/lib/codemirror.css';\nimport 'graphiql/graphiql.min.css';\nimport 'react-tooltip/dist/react-tooltip.css';\nimport '@usebruno/graphql-docs/dist/esm/index.css';\nimport '@fontsource/inter/100.css';\nimport '@fontsource/inter/200.css';\nimport '@fontsource/inter/300.css';\nimport '@fontsource/inter/400.css';\nimport '@fontsource/inter/500.css';\nimport '@fontsource/inter/600.css';\nimport '@fontsource/inter/700.css';\nimport '@fontsource/inter/800.css';\nimport '@fontsource/inter/900.css';\nimport { setupPolyfills } from 'utils/common/setupPolyfills';\nsetupPolyfills();\n\nfunction Main({ children }) {\n  if (!window.ipcRenderer) {\n    return (\n      <div class=\"bg-red-100 border border-red-400 text-red-700 px-4 py-3 mx-10 my-10 rounded relative\" role=\"alert\">\n        <strong class=\"font-bold\">ERROR:</strong>\n        <span className=\"block inline ml-1\">\"ipcRenderer\" not found in window object.</span>\n        <div>\n          You most likely opened Bruno inside your web browser. Bruno only works within Electron, you can start Electron\n          in an adjacent terminal using \"npm run dev:electron\".\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <ErrorBoundary>\n      <Provider store={ReduxStore}>\n        <ThemeProvider>\n          <ToastProvider>\n            <PromptVariablesProvider>\n              <AppProvider>\n                <HotkeysProvider>\n                  {children}\n                </HotkeysProvider>\n              </AppProvider>\n            </PromptVariablesProvider>\n          </ToastProvider>\n        </ThemeProvider>\n      </Provider>\n    </ErrorBoundary>\n  );\n}\n\nexport default Main;\n"
  },
  {
    "path": "packages/bruno-app/src/pages/index.js",
    "content": "import Bruno from './Bruno';\nimport GlobalStyle from '../globalStyles';\nimport '../i18n';\nimport Main from './Main';\n\nexport default function App() {\n  return (\n    <div>\n      <main>\n        <Main>\n          <GlobalStyle />\n          <Bruno />\n        </Main>\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/bruno-app/src/providers/App/ConfirmAppClose/SaveRequestsModal.js",
    "content": "import React, { useEffect, useMemo } from 'react';\nimport each from 'lodash/each';\nimport filter from 'lodash/filter';\nimport groupBy from 'lodash/groupBy';\nimport { useSelector } from 'react-redux';\nimport { useDispatch } from 'react-redux';\nimport { findCollectionByUid, flattenItems, isItemARequest, hasRequestChanges, findEnvironmentInCollection } from 'utils/collections';\nimport { pluralizeWord } from 'utils/common';\nimport { completeQuitFlow } from 'providers/ReduxStore/slices/app';\nimport { saveMultipleRequests, saveMultipleCollections, saveMultipleFolders, saveEnvironment } from 'providers/ReduxStore/slices/collections/actions';\nimport { saveGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';\nimport { IconAlertTriangle } from '@tabler/icons';\nimport Modal from 'components/Modal';\nimport Button from 'ui/Button';\n\nconst SaveRequestsModal = ({ onClose }) => {\n  const MAX_UNSAVED_ITEMS_TO_SHOW = 5;\n  const collections = useSelector((state) => state.collections.collections);\n  const tabs = useSelector((state) => state.tabs.tabs);\n  const globalEnvironments = useSelector((state) => state.globalEnvironments.globalEnvironments);\n  const globalEnvironmentDraft = useSelector((state) => state.globalEnvironments.globalEnvironmentDraft);\n  const dispatch = useDispatch();\n\n  const allDrafts = useMemo(() => {\n    const requestDrafts = [];\n    const collectionDrafts = [];\n    const folderDrafts = [];\n    const environmentDrafts = [];\n    const tabsByCollection = groupBy(tabs, (t) => t.collectionUid);\n\n    Object.keys(tabsByCollection).forEach((collectionUid) => {\n      const collection = findCollectionByUid(collections, collectionUid);\n      if (collection) {\n        // Check for collection draft\n        if (collection.draft) {\n          collectionDrafts.push({\n            type: 'collection',\n            name: collection.name,\n            collectionUid: collectionUid\n          });\n        }\n\n        // Check for collection environment draft\n        if (collection.environmentsDraft) {\n          const { environmentUid, variables } = collection.environmentsDraft;\n          const environment = findEnvironmentInCollection(collection, environmentUid);\n          if (environment && variables) {\n            environmentDrafts.push({\n              type: 'collection-environment',\n              name: environment.name,\n              environmentUid,\n              variables,\n              collectionUid: collectionUid\n            });\n          }\n        }\n\n        // Check for request and folder drafts\n        const items = flattenItems(collection.items);\n\n        // Request drafts\n        const requests = filter(items, (item) => isItemARequest(item) && hasRequestChanges(item));\n        each(requests, (draft) => {\n          requestDrafts.push({\n            ...draft,\n            collectionUid: collectionUid\n          });\n        });\n\n        // Folder drafts\n        const folders = filter(items, (item) => item.type === 'folder' && item.draft);\n        each(folders, (folder) => {\n          folderDrafts.push({\n            type: 'folder',\n            name: folder.name,\n            folderUid: folder.uid,\n            collectionUid: collectionUid\n          });\n        });\n      }\n    });\n\n    // Check for global environment draft\n    if (globalEnvironmentDraft) {\n      const { environmentUid, variables } = globalEnvironmentDraft;\n      const environment = globalEnvironments?.find((env) => env.uid === environmentUid);\n      if (environment && variables) {\n        environmentDrafts.push({\n          type: 'global-environment',\n          name: environment.name,\n          environmentUid,\n          variables\n        });\n      }\n    }\n\n    return [...collectionDrafts, ...folderDrafts, ...environmentDrafts, ...requestDrafts];\n  }, [collections, tabs, globalEnvironments, globalEnvironmentDraft]);\n\n  const totalDraftsCount = allDrafts.length;\n\n  useEffect(() => {\n    if (totalDraftsCount === 0) {\n      return dispatch(completeQuitFlow());\n    }\n  }, [totalDraftsCount, dispatch]);\n\n  const closeWithoutSave = () => {\n    dispatch(completeQuitFlow());\n    onClose();\n  };\n\n  const closeWithSave = async () => {\n    try {\n      // Separate drafts by type\n      const collectionDrafts = allDrafts.filter((d) => d.type === 'collection');\n      const folderDrafts = allDrafts.filter((d) => d.type === 'folder');\n      const requestDrafts = allDrafts.filter((d) => isItemARequest(d));\n      const collectionEnvironmentDrafts = allDrafts.filter((d) => d.type === 'collection-environment');\n      const globalEnvironmentDrafts = allDrafts.filter((d) => d.type === 'global-environment');\n\n      // Save all collection drafts\n      if (collectionDrafts.length > 0) {\n        await dispatch(saveMultipleCollections(collectionDrafts));\n      }\n\n      // Save all folder drafts\n      if (folderDrafts.length > 0) {\n        await dispatch(saveMultipleFolders(folderDrafts));\n      }\n\n      // Save all request drafts\n      if (requestDrafts.length > 0) {\n        await dispatch(saveMultipleRequests(requestDrafts));\n      }\n\n      // Save all collection environment drafts\n      for (const draft of collectionEnvironmentDrafts) {\n        await dispatch(saveEnvironment(draft.variables, draft.environmentUid, draft.collectionUid));\n      }\n\n      // Save all global environment drafts\n      for (const draft of globalEnvironmentDrafts) {\n        await dispatch(saveGlobalEnvironment({ variables: draft.variables, environmentUid: draft.environmentUid }));\n      }\n\n      dispatch(completeQuitFlow());\n      onClose();\n    } catch (error) {\n      console.error('Error saving drafts:', error);\n    }\n  };\n\n  if (totalDraftsCount === 0) {\n    return null;\n  }\n\n  return (\n    <Modal\n      size=\"md\"\n      title=\"Unsaved changes\"\n      confirmText=\"Save and Close\"\n      cancelText=\"Close without saving\"\n      handleCancel={onClose}\n      disableEscapeKey={true}\n      disableCloseOnOutsideClick={true}\n      closeModalFadeTimeout={150}\n      hideFooter={true}\n    >\n      <div className=\"flex items-center\">\n        <IconAlertTriangle size={32} strokeWidth={1.5} className=\"text-yellow-600\" />\n        <h1 className=\"ml-2 text-lg font-medium\">Hold on..</h1>\n      </div>\n      <p className=\"mt-4\">\n        Do you want to save the changes you made to the following{' '}\n        <span className=\"font-medium\">{totalDraftsCount}</span> {pluralizeWord('item', totalDraftsCount)}?\n      </p>\n\n      <ul className=\"mt-4\">\n        {allDrafts.slice(0, MAX_UNSAVED_ITEMS_TO_SHOW).map((item, index) => {\n          let prefix;\n          switch (item.type) {\n            case 'collection':\n              prefix = 'Collection: ';\n              break;\n            case 'folder':\n              prefix = 'Folder: ';\n              break;\n            case 'collection-environment':\n              prefix = 'Collection Environment: ';\n              break;\n            case 'global-environment':\n              prefix = 'Global Environment: ';\n              break;\n            default:\n              prefix = 'Request: ';\n          }\n          return (\n            <li key={`${item.type}-${item.collectionUid || item.uid}-${index}`} className=\"mt-1 text-xs\">\n              {prefix}\n              {item.name || item.filename}\n            </li>\n          );\n        })}\n      </ul>\n\n      {totalDraftsCount > MAX_UNSAVED_ITEMS_TO_SHOW && (\n        <p className=\"mt-1 text-xs\">\n          ...{totalDraftsCount - MAX_UNSAVED_ITEMS_TO_SHOW} additional{' '}\n          {pluralizeWord('item', totalDraftsCount - MAX_UNSAVED_ITEMS_TO_SHOW)} not shown\n        </p>\n      )}\n\n      <div className=\"flex justify-between mt-6\">\n        <div>\n          <Button color=\"danger\" onClick={closeWithoutSave}>\n            Don't Save\n          </Button>\n        </div>\n        <div className=\"flex gap-2\">\n          <Button color=\"secondary\" variant=\"ghost\" onClick={onClose}>\n            Cancel\n          </Button>\n          <Button onClick={closeWithSave}>\n            {totalDraftsCount > 1 ? 'Save All' : 'Save'}\n          </Button>\n        </div>\n      </div>\n    </Modal>\n  );\n};\n\nexport default SaveRequestsModal;\n"
  },
  {
    "path": "packages/bruno-app/src/providers/App/ConfirmAppClose/index.js",
    "content": "import React, { useState, useEffect } from 'react';\nimport { useDispatch } from 'react-redux';\nimport SaveRequestsModal from './SaveRequestsModal';\nimport { isElectron } from 'utils/common/platform';\n\nconst ConfirmAppClose = () => {\n  const { ipcRenderer } = window;\n  const [showConfirmClose, setShowConfirmClose] = useState(false);\n  const dispatch = useDispatch();\n\n  useEffect(() => {\n    if (!isElectron()) {\n      return;\n    }\n\n    const clearListener = ipcRenderer.on('main:start-quit-flow', () => {\n      setShowConfirmClose(true);\n    });\n\n    return () => {\n      clearListener();\n    };\n  }, [isElectron, ipcRenderer, dispatch, setShowConfirmClose]);\n\n  if (!showConfirmClose) {\n    return null;\n  }\n\n  return <SaveRequestsModal onClose={() => setShowConfirmClose(false)} />;\n};\n\nexport default ConfirmAppClose;\n"
  },
  {
    "path": "packages/bruno-app/src/providers/App/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  color: ${(props) => props.theme.text};\n  background-color: ${(props) => props.theme.bg};\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/providers/App/index.js",
    "content": "import React, { useEffect } from 'react';\nimport { get } from 'lodash';\nimport { useDispatch } from 'react-redux';\nimport { refreshScreenWidth } from 'providers/ReduxStore/slices/app';\nimport ConfirmAppClose from './ConfirmAppClose';\nimport useIpcEvents from './useIpcEvents';\nimport useTelemetry from './useTelemetry';\nimport StyledWrapper from './StyledWrapper';\nimport useOpenAPISyncPolling from './useOpenAPISyncPolling';\nimport { version } from '../../../package.json';\n\nexport const AppContext = React.createContext();\n\nexport const AppProvider = (props) => {\n  useTelemetry({ version });\n  useIpcEvents();\n  useOpenAPISyncPolling();\n  const dispatch = useDispatch();\n\n  useEffect(() => {\n    dispatch(refreshScreenWidth());\n  }, []);\n\n  useEffect(() => {\n    const platform = get(navigator, 'platform', '').toLowerCase();\n\n    if (!platform) {\n      return;\n    }\n\n    if (platform.includes('mac')) {\n      document.body.classList.add('os-mac');\n      return;\n    }\n\n    if (platform.includes('win')) {\n      document.body.classList.add('os-windows');\n      return;\n    }\n\n    if (platform.includes('linux')) {\n      document.body.classList.add('os-linux');\n    }\n  }, []);\n\n  useEffect(() => {\n    const handleResize = () => {\n      dispatch(refreshScreenWidth());\n    };\n\n    window.addEventListener('resize', handleResize);\n\n    return () => window.removeEventListener('resize', handleResize);\n  }, []);\n\n  return (\n    <AppContext.Provider {...props} value={{ version }}>\n      <StyledWrapper>\n        <ConfirmAppClose />\n        {props.children}\n      </StyledWrapper>\n    </AppContext.Provider>\n  );\n};\n\nexport const useApp = () => {\n  const context = React.useContext(AppContext);\n  if (!context) {\n    throw new Error('useApp must be used within an AppProvider');\n  }\n  return context;\n};\n\nexport default AppProvider;\n"
  },
  {
    "path": "packages/bruno-app/src/providers/App/useIpcEvents.js",
    "content": "import { useEffect } from 'react';\nimport {\n  updateCookies,\n  updatePreferences,\n  setGitVersion\n} from 'providers/ReduxStore/slices/app';\nimport {\n  addTab\n} from 'providers/ReduxStore/slices/tabs';\nimport {\n  brunoConfigUpdateEvent,\n  collectionAddDirectoryEvent,\n  collectionAddFileEvent,\n  collectionChangeFileEvent,\n  collectionRenamedEvent,\n  collectionUnlinkDirectoryEvent,\n  collectionUnlinkEnvFileEvent,\n  collectionUnlinkFileEvent,\n  processEnvUpdateEvent,\n  workspaceEnvUpdateEvent,\n  requestCancelled,\n  runFolderEvent,\n  runRequestEvent,\n  scriptEnvironmentUpdateEvent,\n  streamDataReceived,\n  setDotEnvVariables\n} from 'providers/ReduxStore/slices/collections';\nimport { collectionAddEnvFileEvent, openCollectionEvent, hydrateCollectionWithUiStateSnapshot, mergeAndPersistEnvironment } from 'providers/ReduxStore/slices/collections/actions';\nimport {\n  workspaceOpenedEvent,\n  workspaceConfigUpdatedEvent\n} from 'providers/ReduxStore/slices/workspaces/actions';\nimport { workspaceDotEnvUpdateEvent, setWorkspaceDotEnvVariables } from 'providers/ReduxStore/slices/workspaces';\nimport toast from 'react-hot-toast';\nimport { useDispatch, useStore } from 'react-redux';\nimport { isElectron } from 'utils/common/platform';\nimport { globalEnvironmentsUpdateEvent, updateGlobalEnvironments } from 'providers/ReduxStore/slices/global-environments';\nimport { collectionAddOauth2CredentialsByUrl, collectionClearOauth2CredentialsByCredentialsId, updateCollectionLoadingState } from 'providers/ReduxStore/slices/collections/index';\nimport { addLog } from 'providers/ReduxStore/slices/logs';\nimport { updateSystemResources } from 'providers/ReduxStore/slices/performance';\nimport { apiSpecAddFileEvent, apiSpecChangeFileEvent } from 'providers/ReduxStore/slices/apiSpec';\n\nconst useIpcEvents = () => {\n  const dispatch = useDispatch();\n  const store = useStore();\n\n  useEffect(() => {\n    if (!isElectron()) {\n      return () => {};\n    }\n\n    const { ipcRenderer } = window;\n\n    const _collectionTreeUpdated = (type, val) => {\n      if (window.__IS_DEV__) {\n        console.log(type);\n        console.log(val);\n      }\n      if (type === 'addDir') {\n        dispatch(\n          collectionAddDirectoryEvent({\n            dir: val\n          })\n        );\n      }\n      if (type === 'addFile') {\n        dispatch(\n          collectionAddFileEvent({\n            file: val\n          })\n        );\n      }\n      if (type === 'change') {\n        dispatch(\n          collectionChangeFileEvent({\n            file: val\n          })\n        );\n      }\n      if (type === 'unlink') {\n        setTimeout(() => {\n          dispatch(\n            collectionUnlinkFileEvent({\n              file: val\n            })\n          );\n        }, 100);\n      }\n      if (type === 'unlinkDir') {\n        dispatch(\n          collectionUnlinkDirectoryEvent({\n            directory: val\n          })\n        );\n      }\n      if (type === 'addEnvironmentFile') {\n        dispatch(collectionAddEnvFileEvent(val));\n      }\n      if (type === 'unlinkEnvironmentFile') {\n        dispatch(collectionUnlinkEnvFileEvent(val));\n      }\n    };\n\n    const _apiSpecTreeUpdated = (type, val) => {\n      if (window.__IS_DEV__) {\n        console.log('API Spec update:', type);\n        console.log(val);\n      }\n      if (type === 'addFile') {\n        dispatch(apiSpecAddFileEvent({ data: val }));\n      }\n      if (type === 'changeFile') {\n        dispatch(apiSpecChangeFileEvent({ data: val }));\n      }\n    };\n\n    ipcRenderer.invoke('renderer:ready');\n\n    const removeCollectionTreeUpdateListener = ipcRenderer.on('main:collection-tree-updated', _collectionTreeUpdated);\n\n    const removeApiSpecTreeUpdateListener = ipcRenderer.on('main:apispec-tree-updated', _apiSpecTreeUpdated);\n\n    const removeOpenCollectionListener = ipcRenderer.on('main:collection-opened', (pathname, uid, brunoConfig) => {\n      dispatch(openCollectionEvent(uid, pathname, brunoConfig));\n    });\n\n    const removeOpenWorkspaceListener = ipcRenderer.on('main:workspace-opened', (workspacePath, workspaceUid, workspaceConfig) => {\n      dispatch(workspaceOpenedEvent(workspacePath, workspaceUid, workspaceConfig));\n    });\n\n    const removeWorkspaceConfigUpdatedListener = ipcRenderer.on('main:workspace-config-updated', (workspacePath, workspaceUid, workspaceConfig) => {\n      dispatch(workspaceConfigUpdatedEvent(workspacePath, workspaceUid, workspaceConfig));\n    });\n\n    const removeWorkspaceEnvironmentAddedListener = ipcRenderer.on('main:workspace-environment-added', (workspaceUid, file) => {\n      const state = store.getState();\n      const activeWorkspaceUid = state.workspaces?.activeWorkspaceUid;\n      if (activeWorkspaceUid === workspaceUid) {\n        const workspace = state.workspaces?.workspaces?.find((w) => w.uid === workspaceUid);\n        if (workspace) {\n          ipcRenderer.invoke('renderer:get-global-environments', {\n            workspaceUid,\n            workspacePath: workspace.pathname\n          }).then((result) => {\n            dispatch(updateGlobalEnvironments(result));\n          }).catch((error) => {\n            console.error('Error refreshing global environments:', error);\n          });\n        }\n      }\n    });\n\n    const removeWorkspaceEnvironmentChangedListener = ipcRenderer.on('main:workspace-environment-changed', (workspaceUid, file) => {\n      const state = store.getState();\n      const activeWorkspaceUid = state.workspaces?.activeWorkspaceUid;\n      if (activeWorkspaceUid === workspaceUid) {\n        const workspace = state.workspaces?.workspaces?.find((w) => w.uid === workspaceUid);\n        if (workspace) {\n          ipcRenderer.invoke('renderer:get-global-environments', {\n            workspaceUid,\n            workspacePath: workspace.pathname\n          }).then((result) => {\n            dispatch(updateGlobalEnvironments(result));\n          }).catch((error) => {\n            console.error('Error refreshing global environments:', error);\n          });\n        }\n      }\n    });\n\n    const removeWorkspaceEnvironmentDeletedListener = ipcRenderer.on('main:workspace-environment-deleted', (workspaceUid, environmentUid) => {\n      const state = store.getState();\n      const activeWorkspaceUid = state.workspaces?.activeWorkspaceUid;\n      if (activeWorkspaceUid === workspaceUid) {\n        const workspace = state.workspaces?.workspaces?.find((w) => w.uid === workspaceUid);\n        if (workspace) {\n          ipcRenderer.invoke('renderer:get-global-environments', {\n            workspaceUid,\n            workspacePath: workspace.pathname\n          }).then((result) => {\n            dispatch(updateGlobalEnvironments(result));\n          }).catch((error) => {\n            console.error('Error refreshing global environments:', error);\n          });\n        }\n      }\n    });\n\n    const removeDisplayErrorListener = ipcRenderer.on('main:display-error', (error) => {\n      if (typeof error === 'string') {\n        return toast.error(error || 'Something went wrong!');\n      }\n      if (typeof error === 'object') {\n        return toast.error(error.message || 'Something went wrong!');\n      }\n    });\n\n    const removeScriptEnvUpdateListener = ipcRenderer.on('main:script-environment-update', (val) => {\n      dispatch(scriptEnvironmentUpdateEvent(val));\n    });\n\n    const removePersistentEnvVariablesUpdateListener = ipcRenderer.on('main:persistent-env-variables-update', (val) => {\n      dispatch(mergeAndPersistEnvironment(val));\n    });\n\n    const removeGlobalEnvironmentVariablesUpdateListener = ipcRenderer.on('main:global-environment-variables-update', (val) => {\n      dispatch(globalEnvironmentsUpdateEvent(val));\n    });\n\n    const removeCollectionRenamedListener = ipcRenderer.on('main:collection-renamed', (val) => {\n      dispatch(collectionRenamedEvent(val));\n    });\n\n    const removeRunFolderEventListener = ipcRenderer.on('main:run-folder-event', (val) => {\n      dispatch(runFolderEvent(val));\n    });\n\n    const removeRunRequestEventListener = ipcRenderer.on('main:run-request-event', (val) => {\n      dispatch(runRequestEvent(val));\n    });\n\n    const removeProcessEnvUpdatesListener = ipcRenderer.on('main:process-env-update', (val) => {\n      dispatch(processEnvUpdateEvent(val));\n    });\n\n    const removeWorkspaceDotEnvUpdatesListener = ipcRenderer.on('main:workspace-dotenv-update', (val) => {\n      dispatch(workspaceDotEnvUpdateEvent(val));\n      dispatch(workspaceEnvUpdateEvent({ processEnvVariables: val.processEnvVariables }));\n    });\n\n    const removeDotEnvFileUpdateListener = ipcRenderer.on('main:dotenv-file-update', (val) => {\n      const { type, collectionUid, workspaceUid, filename, variables, exists, processEnvVariables } = val;\n\n      if (type === 'collection' && collectionUid) {\n        dispatch(setDotEnvVariables({\n          collectionUid,\n          variables,\n          exists,\n          filename\n        }));\n        if (filename === '.env') {\n          dispatch(processEnvUpdateEvent({ collectionUid, processEnvVariables }));\n        }\n      } else if (type === 'workspace' && workspaceUid) {\n        dispatch(setWorkspaceDotEnvVariables({\n          workspaceUid,\n          variables,\n          exists,\n          filename\n        }));\n        if (filename === '.env') {\n          dispatch(workspaceDotEnvUpdateEvent(val));\n          dispatch(workspaceEnvUpdateEvent({ processEnvVariables }));\n        }\n      }\n    });\n\n    const removeConsoleLogListener = ipcRenderer.on('main:console-log', (val) => {\n      console[val.type](...val.args);\n      dispatch(addLog({\n        type: val.type,\n        args: val.args,\n        timestamp: new Date().toISOString()\n      }));\n    });\n\n    const removeSystemResourcesListener = ipcRenderer.on('main:filesync-system-resources', (resourceData) => {\n      dispatch(updateSystemResources(resourceData));\n    });\n\n    const removeConfigUpdatesListener = ipcRenderer.on('main:bruno-config-update', (val) =>\n      dispatch(brunoConfigUpdateEvent(val))\n    );\n\n    const removeShowPreferencesListener = ipcRenderer.on('main:open-preferences', () => {\n      const state = store.getState();\n      const activeWorkspaceUid = state.workspaces?.activeWorkspaceUid;\n      const workspaces = state.workspaces?.workspaces;\n      const tabs = state.tabs?.tabs;\n      const activeTabUid = state.tabs?.activeTabUid;\n      const activeTab = tabs?.find((t) => t.uid === activeTabUid);\n\n      const activeWorkspace = workspaces?.find((w) => w.uid === activeWorkspaceUid);\n      const collectionUid = activeTab?.collectionUid || activeWorkspace?.scratchCollectionUid;\n\n      dispatch(\n        addTab({\n          type: 'preferences',\n          uid: collectionUid ? `${collectionUid}-preferences` : 'preferences',\n          collectionUid\n        })\n      );\n    });\n\n    const removePreferencesUpdatesListener = ipcRenderer.on('main:load-preferences', (val) => {\n      dispatch(updatePreferences(val));\n    });\n\n    const removeCookieUpdateListener = ipcRenderer.on('main:cookies-update', (val) => {\n      dispatch(updateCookies(val));\n    });\n\n    const removeGlobalEnvironmentsUpdatesListener = ipcRenderer.on('main:load-global-environments', (val) => {\n      dispatch(updateGlobalEnvironments(val));\n    });\n\n    const removeSnapshotHydrationListener = ipcRenderer.on('main:hydrate-app-with-ui-state-snapshot', (val) => {\n      dispatch(hydrateCollectionWithUiStateSnapshot(val));\n    });\n\n    const removeCollectionOauth2CredentialsUpdatesListener = ipcRenderer.on('main:credentials-update', (val) => {\n      const payload = {\n        ...val,\n        itemUid: val.itemUid || null,\n        folderUid: val.folderUid || null,\n        credentialsId: val.credentialsId || 'credentials'\n      };\n      dispatch(collectionAddOauth2CredentialsByUrl(payload));\n    });\n\n    const removeCollectionOauth2CredentialsClearListener = ipcRenderer.on('main:credentials-clear', (val) => {\n      dispatch(collectionClearOauth2CredentialsByCredentialsId(val));\n    });\n\n    const removeHttpStreamNewDataListener = ipcRenderer.on('main:http-stream-new-data', (val) => {\n      dispatch(streamDataReceived(val));\n    });\n\n    const removeHttpStreamEndListener = ipcRenderer.on('main:http-stream-end', (val) => {\n      dispatch(requestCancelled(val));\n    });\n\n    const removeCollectionLoadingStateListener = ipcRenderer.on('main:collection-loading-state-updated', (val) => {\n      dispatch(updateCollectionLoadingState(val));\n    });\n\n    const gitVersionListener = ipcRenderer.on('main:git-version', (val) => {\n      dispatch(setGitVersion(val));\n    });\n\n    return () => {\n      removeCollectionTreeUpdateListener();\n      removeApiSpecTreeUpdateListener();\n      removeOpenCollectionListener();\n      removeOpenWorkspaceListener();\n      removeWorkspaceConfigUpdatedListener();\n      removeWorkspaceEnvironmentAddedListener();\n      removeWorkspaceEnvironmentChangedListener();\n      removeWorkspaceEnvironmentDeletedListener();\n      removeDisplayErrorListener();\n      removeScriptEnvUpdateListener();\n      removeGlobalEnvironmentVariablesUpdateListener();\n      removeCollectionRenamedListener();\n      removeRunFolderEventListener();\n      removeRunRequestEventListener();\n      removeProcessEnvUpdatesListener();\n      removeWorkspaceDotEnvUpdatesListener();\n      removeDotEnvFileUpdateListener();\n      removeConsoleLogListener();\n      removeConfigUpdatesListener();\n      removeShowPreferencesListener();\n      removePreferencesUpdatesListener();\n      removeCookieUpdateListener();\n      removeGlobalEnvironmentsUpdatesListener();\n      removeSnapshotHydrationListener();\n      removeCollectionOauth2CredentialsUpdatesListener();\n      removeCollectionOauth2CredentialsClearListener();\n      removeHttpStreamNewDataListener();\n      removeHttpStreamEndListener();\n      removeCollectionLoadingStateListener();\n      removePersistentEnvVariablesUpdateListener();\n      removeSystemResourcesListener();\n      gitVersionListener();\n    };\n  }, [isElectron]);\n};\n\nexport default useIpcEvents;\n"
  },
  {
    "path": "packages/bruno-app/src/providers/App/useOpenAPISyncPolling.js",
    "content": "import { useEffect, useMemo, useRef } from 'react';\nimport { useDispatch, useSelector } from 'react-redux';\nimport { checkActiveWorkspaceCollectionsForUpdates } from 'providers/ReduxStore/slices/openapi-sync';\nimport { normalizePath } from 'utils/common/path';\nimport { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';\n\nconst POLL_INTERVAL = 5 * 60 * 1000; // 5 minutes\n\nconst useOpenAPISyncPolling = () => {\n  const dispatch = useDispatch();\n\n  const isOpenAPISyncEnabled = useBetaFeature(BETA_FEATURES.OPENAPI_SYNC);\n  // Global toggle for pausing all OpenAPI sync polling\n  const pollingEnabled = useSelector((state) => state.openapiSync?.pollingEnabled ?? true) && isOpenAPISyncEnabled;\n  const collections = useSelector((state) => state.collections?.collections || []);\n  const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces);\n  const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);\n  const intervalRef = useRef(null);\n\n  // Filter to only active workspace collections\n  const activeWorkspaceCollections = useMemo(() => {\n    if (!activeWorkspace) return [];\n    return collections.filter((c) =>\n      activeWorkspace.collections?.some((wc) => normalizePath(wc.path) === normalizePath(c.pathname))\n    );\n  }, [activeWorkspace, collections]);\n\n  // Derive a stable boolean so polling doesn't restart on every collection mutation\n  const hasSyncableCollections = useMemo(\n    () => activeWorkspaceCollections.some((c) => {\n      const syncConfig = c.brunoConfig?.openapi?.[0];\n      return syncConfig?.sourceUrl && syncConfig.autoCheck !== false;\n    }),\n    [activeWorkspaceCollections]\n  );\n\n  useEffect(() => {\n    if (!pollingEnabled || !hasSyncableCollections) {\n      if (intervalRef.current) {\n        clearInterval(intervalRef.current);\n        intervalRef.current = null;\n      }\n      return;\n    }\n\n    // Initial check after a short delay (to let the app initialize)\n    const initialTimeout = setTimeout(() => {\n      dispatch(checkActiveWorkspaceCollectionsForUpdates());\n    }, 10000); // 10 seconds after app starts\n\n    // Set up polling interval\n    intervalRef.current = setInterval(() => {\n      dispatch(checkActiveWorkspaceCollectionsForUpdates());\n    }, POLL_INTERVAL);\n\n    return () => {\n      clearTimeout(initialTimeout);\n      if (intervalRef.current) {\n        clearInterval(intervalRef.current);\n        intervalRef.current = null;\n      }\n    };\n  }, [dispatch, pollingEnabled, hasSyncableCollections]);\n\n  return null;\n};\n\nexport default useOpenAPISyncPolling;\n"
  },
  {
    "path": "packages/bruno-app/src/providers/App/useTelemetry.js",
    "content": "/**\n * Telemetry in bruno is just an anonymous visit counter (triggered once per day).\n * The only details shared are:\n *      - OS (ex: mac, windows, linux)\n *      - Bruno Version (ex: 1.3.0)\n * We don't track usage analytics / micro-interactions / crash logs / anything else.\n */\n\nimport { useEffect } from 'react';\nimport { PostHog } from 'posthog-node';\nimport platformLib from 'platform';\nimport { uuid } from 'utils/common';\n\nconst posthogApiKey = process.env.NEXT_PUBLIC_POSTHOG_API_KEY;\nlet posthogClient = null;\n\nconst isPlaywrightTestRunning = () => {\n  return process.env.PLAYWRIGHT ? true : false;\n};\n\nconst isDevEnv = () => {\n  return import.meta.env.MODE === 'development';\n};\n\nconst getPosthogClient = () => {\n  if (posthogClient) {\n    return posthogClient;\n  }\n\n  posthogClient = new PostHog(posthogApiKey);\n  return posthogClient;\n};\n\nconst getAnonymousTrackingId = () => {\n  let id = localStorage.getItem('bruno.anonymousTrackingId');\n\n  if (!id || !id.length || id.length !== 21) {\n    id = uuid();\n    localStorage.setItem('bruno.anonymousTrackingId', id);\n  }\n\n  return id;\n};\n\nconst trackStart = (version) => {\n  if (isPlaywrightTestRunning()) {\n    return;\n  }\n\n  if (isDevEnv()) {\n    return;\n  }\n\n  const trackingId = getAnonymousTrackingId();\n  const client = getPosthogClient();\n  client.capture({\n    distinctId: trackingId,\n    event: 'start',\n    properties: {\n      os: platformLib.os.family,\n      version: version\n    }\n  });\n};\n\nconst useTelemetry = ({ version }) => {\n  useEffect(() => {\n    if (posthogApiKey && posthogApiKey.length) {\n      trackStart(version);\n      setInterval(trackStart, 24 * 60 * 60 * 1000);\n    }\n  }, [posthogApiKey]);\n};\n\nexport default useTelemetry;\n"
  },
  {
    "path": "packages/bruno-app/src/providers/Hotkeys/index.js",
    "content": "import React, { useState, useEffect } from 'react';\nimport toast from 'react-hot-toast';\nimport find from 'lodash/find';\nimport Mousetrap from 'mousetrap';\nimport { useSelector, useDispatch } from 'react-redux';\nimport NetworkError from 'components/ResponsePane/NetworkError';\nimport NewRequest from 'components/Sidebar/NewRequest';\nimport GlobalSearchModal from 'components/GlobalSearchModal';\nimport {\n  sendRequest,\n  saveRequest,\n  saveCollectionRoot,\n  saveFolderRoot,\n  saveCollectionSettings,\n  closeTabs\n} from 'providers/ReduxStore/slices/collections/actions';\nimport { findCollectionByUid, findItemInCollection } from 'utils/collections';\nimport { addTab, reorderTabs, switchTab } from 'providers/ReduxStore/slices/tabs';\nimport { toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';\nimport { getKeyBindingsForActionAllOS } from './keyMappings';\n\nexport const HotkeysContext = React.createContext();\n\nexport const HotkeysProvider = (props) => {\n  const dispatch = useDispatch();\n  const tabs = useSelector((state) => state.tabs.tabs);\n  const collections = useSelector((state) => state.collections.collections);\n  const activeTabUid = useSelector((state) => state.tabs.activeTabUid);\n  const [showNewRequestModal, setShowNewRequestModal] = useState(false);\n  const [showGlobalSearchModal, setShowGlobalSearchModal] = useState(false);\n\n  const getCurrentCollection = () => {\n    const activeTab = find(tabs, (t) => t.uid === activeTabUid);\n    if (activeTab) {\n      const collection = findCollectionByUid(collections, activeTab.collectionUid);\n\n      return collection;\n    }\n  };\n\n  // save hotkey\n  useEffect(() => {\n    Mousetrap.bind([...getKeyBindingsForActionAllOS('save')], (e) => {\n      const activeTab = find(tabs, (t) => t.uid === activeTabUid);\n      if (activeTab) {\n        if (activeTab.type === 'environment-settings' || activeTab.type === 'global-environment-settings') {\n          window.dispatchEvent(new CustomEvent('environment-save'));\n          return false;\n        }\n\n        const collection = findCollectionByUid(collections, activeTab.collectionUid);\n        if (collection) {\n          const item = findItemInCollection(collection, activeTab.uid);\n          if (item && item.uid) {\n            if (activeTab.type === 'folder-settings') {\n              dispatch(saveFolderRoot(collection.uid, item.uid));\n            } else {\n              dispatch(saveRequest(activeTab.uid, activeTab.collectionUid));\n            }\n          } else if (activeTab.type === 'collection-settings') {\n            dispatch(saveCollectionSettings(collection.uid));\n          }\n        }\n      }\n\n      return false; // this stops the event bubbling\n    });\n\n    return () => {\n      Mousetrap.unbind([...getKeyBindingsForActionAllOS('save')]);\n    };\n  }, [activeTabUid, tabs, saveRequest, collections, dispatch]);\n\n  // send request (ctrl/cmd + enter)\n  useEffect(() => {\n    Mousetrap.bind([...getKeyBindingsForActionAllOS('sendRequest')], (e) => {\n      const activeTab = find(tabs, (t) => t.uid === activeTabUid);\n      if (activeTab) {\n        const collection = findCollectionByUid(collections, activeTab.collectionUid);\n\n        if (collection) {\n          const item = findItemInCollection(collection, activeTab.uid);\n          if (item) {\n            if (item.type === 'grpc-request') {\n              const request = item.draft ? item.draft.request : item.request;\n              if (!request.url) {\n                toast.error('Please enter a valid gRPC server URL');\n                return;\n              }\n              if (!request.method) {\n                toast.error('Please select a gRPC method');\n                return;\n              }\n            }\n\n            dispatch(sendRequest(item, collection.uid)).catch((err) =>\n              toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {\n                duration: 5000\n              })\n            );\n          }\n        }\n      }\n\n      return false; // this stops the event bubbling\n    });\n\n    return () => {\n      Mousetrap.unbind([...getKeyBindingsForActionAllOS('sendRequest')]);\n    };\n  }, [activeTabUid, tabs, saveRequest, collections]);\n\n  // edit environments (ctrl/cmd + e)\n  useEffect(() => {\n    Mousetrap.bind([...getKeyBindingsForActionAllOS('editEnvironment')], (e) => {\n      const activeTab = find(tabs, (t) => t.uid === activeTabUid);\n      if (activeTab) {\n        const collection = findCollectionByUid(collections, activeTab.collectionUid);\n\n        if (collection) {\n          dispatch(\n            addTab({\n              uid: `${collection.uid}-environment-settings`,\n              collectionUid: collection.uid,\n              type: 'environment-settings'\n            })\n          );\n        }\n      }\n\n      return false; // this stops the event bubbling\n    });\n\n    return () => {\n      Mousetrap.unbind([...getKeyBindingsForActionAllOS('editEnvironment')]);\n    };\n  }, [activeTabUid, tabs, collections, dispatch]);\n\n  // new request (ctrl/cmd + b)\n  useEffect(() => {\n    Mousetrap.bind([...getKeyBindingsForActionAllOS('newRequest')], (e) => {\n      const activeTab = find(tabs, (t) => t.uid === activeTabUid);\n      if (activeTab) {\n        const collection = findCollectionByUid(collections, activeTab.collectionUid);\n\n        if (collection) {\n          setShowNewRequestModal(true);\n        }\n      }\n\n      return false; // this stops the event bubbling\n    });\n\n    return () => {\n      Mousetrap.unbind([...getKeyBindingsForActionAllOS('newRequest')]);\n    };\n  }, [activeTabUid, tabs, collections, setShowNewRequestModal]);\n\n  // global search (ctrl/cmd + k)\n  useEffect(() => {\n    Mousetrap.bind([...getKeyBindingsForActionAllOS('globalSearch')], (e) => {\n      setShowGlobalSearchModal(true);\n\n      return false; // stop bubbling\n    });\n\n    return () => {\n      Mousetrap.unbind([...getKeyBindingsForActionAllOS('globalSearch')]);\n    };\n  }, []);\n\n  // close tab hotkey\n  useEffect(() => {\n    Mousetrap.bind([...getKeyBindingsForActionAllOS('closeTab')], (e) => {\n      if (activeTabUid) {\n        dispatch(\n          closeTabs({\n            tabUids: [activeTabUid]\n          })\n        );\n      }\n\n      return false; // this stops the event bubbling\n    });\n\n    return () => {\n      Mousetrap.unbind([...getKeyBindingsForActionAllOS('closeTab')]);\n    };\n  }, [activeTabUid]);\n\n  // Switch to the previous tab\n  useEffect(() => {\n    Mousetrap.bind([...getKeyBindingsForActionAllOS('switchToPreviousTab')], (e) => {\n      dispatch(\n        switchTab({\n          direction: 'pageup'\n        })\n      );\n\n      return false; // this stops the event bubbling\n    });\n\n    return () => {\n      Mousetrap.unbind([...getKeyBindingsForActionAllOS('switchToPreviousTab')]);\n    };\n  }, [dispatch]);\n\n  // Switch to the next tab\n  useEffect(() => {\n    Mousetrap.bind([...getKeyBindingsForActionAllOS('switchToNextTab')], (e) => {\n      dispatch(\n        switchTab({\n          direction: 'pagedown'\n        })\n      );\n\n      return false; // this stops the event bubbling\n    });\n\n    return () => {\n      Mousetrap.unbind([...getKeyBindingsForActionAllOS('switchToNextTab')]);\n    };\n  }, [dispatch]);\n\n  // Close all tabs\n  useEffect(() => {\n    Mousetrap.bind([...getKeyBindingsForActionAllOS('closeAllTabs')], (e) => {\n      const activeTab = find(tabs, (t) => t.uid === activeTabUid);\n      if (activeTab) {\n        const collection = findCollectionByUid(collections, activeTab.collectionUid);\n\n        if (collection) {\n          const tabUids = tabs.filter((tab) => tab.collectionUid === collection.uid).map((tab) => tab.uid);\n          dispatch(\n            closeTabs({\n              tabUids: tabUids\n            })\n          );\n        }\n      }\n\n      return false; // this stops the event bubbling\n    });\n\n    return () => {\n      Mousetrap.unbind([...getKeyBindingsForActionAllOS('closeAllTabs')]);\n    };\n  }, [activeTabUid, tabs, collections, dispatch]);\n\n  // Collapse sidebar (ctrl/cmd + \\)\n  useEffect(() => {\n    Mousetrap.bind([...getKeyBindingsForActionAllOS('collapseSidebar')], (e) => {\n      dispatch(toggleSidebarCollapse());\n      return false;\n    });\n\n    return () => {\n      Mousetrap.unbind([...getKeyBindingsForActionAllOS('collapseSidebar')]);\n    };\n  }, [dispatch]);\n\n  // Move tab left\n  useEffect(() => {\n    Mousetrap.bind([...getKeyBindingsForActionAllOS('moveTabLeft')], (e) => {\n      dispatch(reorderTabs({ direction: -1 }));\n      return false; // this stops the event bubbling\n    });\n\n    return () => {\n      Mousetrap.unbind([...getKeyBindingsForActionAllOS('moveTabLeft')]);\n    };\n  }, [dispatch]);\n\n  // Move tab right\n  useEffect(() => {\n    Mousetrap.bind([...getKeyBindingsForActionAllOS('moveTabRight')], (e) => {\n      dispatch(reorderTabs({ direction: 1 }));\n      return false; // this stops the event bubbling\n    });\n\n    return () => {\n      Mousetrap.unbind([...getKeyBindingsForActionAllOS('moveTabRight')]);\n    };\n  }, [dispatch]);\n\n  const currentCollection = getCurrentCollection();\n\n  return (\n    <HotkeysContext.Provider {...props} value=\"hotkey\">\n      {showNewRequestModal && (\n        <NewRequest collectionUid={currentCollection?.uid} onClose={() => setShowNewRequestModal(false)} />\n      )}\n      {showGlobalSearchModal && (\n        <GlobalSearchModal isOpen={showGlobalSearchModal} onClose={() => setShowGlobalSearchModal(false)} />\n      )}\n      <div>{props.children}</div>\n    </HotkeysContext.Provider>\n  );\n};\n\nexport const useHotkeys = () => {\n  const context = React.useContext(HotkeysContext);\n\n  if (!context) {\n    throw new Error(`useHotkeys must be used within a HotkeysProvider`);\n  }\n\n  return context;\n};\n\nexport default HotkeysProvider;\n"
  },
  {
    "path": "packages/bruno-app/src/providers/Hotkeys/keyMappings.js",
    "content": "const KeyMapping = {\n  save: { mac: 'command+s', windows: 'ctrl+s', name: 'Save' },\n  sendRequest: { mac: 'command+enter', windows: 'ctrl+enter', name: 'Send Request' },\n  editEnvironment: { mac: 'command+e', windows: 'ctrl+e', name: 'Edit Environment' },\n  newRequest: { mac: 'command+b', windows: 'ctrl+b', name: 'New Request' },\n  globalSearch: { mac: 'command+k', windows: 'ctrl+k', name: 'Global Search' },\n  closeTab: { mac: 'command+w', windows: 'ctrl+w', name: 'Close Tab' },\n  openPreferences: { mac: 'command+,', windows: 'ctrl+,', name: 'Open Preferences' },\n  closeBruno: {\n    mac: 'command+Q',\n    windows: 'ctrl+shift+q',\n    name: 'Close Bruno'\n  },\n  switchToPreviousTab: {\n    mac: 'command+pageup',\n    windows: 'ctrl+pageup',\n    name: 'Switch to Previous Tab'\n  },\n  switchToNextTab: {\n    mac: 'command+pagedown',\n    windows: 'ctrl+pagedown',\n    name: 'Switch to Next Tab'\n  },\n  moveTabLeft: {\n    mac: 'command+shift+pageup',\n    windows: 'ctrl+shift+pageup',\n    name: 'Move Tab Left'\n  },\n  moveTabRight: {\n    mac: 'command+shift+pagedown',\n    windows: 'ctrl+shift+pagedown',\n    name: 'Move Tab Right'\n  },\n  closeAllTabs: { mac: 'command+shift+w', windows: 'ctrl+shift+w', name: 'Close All Tabs' },\n  collapseSidebar: { mac: 'command+\\\\', windows: 'ctrl+\\\\', name: 'Collapse Sidebar' },\n  zoomIn: { mac: 'command+=', windows: 'ctrl+=', name: 'Zoom In' },\n  zoomOut: { mac: 'command+-', windows: 'ctrl+-', name: 'Zoom Out' },\n  resetZoom: { mac: 'command+0', windows: 'ctrl+0', name: 'Reset Zoom' },\n  renameItem: { mac: 'enter', windows: 'f2', name: 'Rename Collection Item' }\n};\n\n/**\n * Retrieves the key bindings for a specific operating system.\n *\n * @param {string} os - The operating system (e.g., 'mac', 'windows').\n * @returns {Object} An object containing the key bindings for the specified OS.\n */\nexport const getKeyBindingsForOS = (os) => {\n  const keyBindings = {};\n  for (const [action, { name, ...keys }] of Object.entries(KeyMapping)) {\n    if (keys[os]) {\n      keyBindings[action] = {\n        keys: keys[os],\n        name\n      };\n    }\n  }\n  return keyBindings;\n};\n\n/**\n * Retrieves the key bindings for a specific action across all operating systems.\n *\n * @param {string} action - The action for which to retrieve key bindings.\n * @returns {Object|null} An object containing the key bindings for macOS, Windows, or null if the action is not found.\n */\nexport const getKeyBindingsForActionAllOS = (action) => {\n  const actionBindings = KeyMapping[action];\n\n  if (!actionBindings) {\n    console.warn(`Action \"${action}\" not found in KeyMapping.`);\n    return null;\n  }\n\n  return [actionBindings.mac, actionBindings.windows];\n};\n"
  },
  {
    "path": "packages/bruno-app/src/providers/PromptVariables/index.js",
    "content": "import PromptVariablesModal from 'components/RequestPane/PromptVariables/PromptVariablesModal';\nimport React, { createContext, useCallback, useState } from 'react';\n\nconst PromptVariablesContext = createContext();\n\nexport function PromptVariablesProvider({ children }) {\n  const [modalState, setModalState] = useState({ open: false, prompts: [], resolve: null, reject: null });\n\n  const prompt = useCallback((prompts) => {\n    return new Promise((resolve, reject) => {\n      setModalState({ open: true, prompts, resolve, reject });\n    });\n  }, []);\n\n  // Expose globally for non-component code (e.g., Redux thunks)\n  if (typeof window !== 'undefined') {\n    window.promptForVariables = async (prompts) => {\n      try {\n        return await prompt(prompts);\n      } catch (err) {\n        if (err !== 'cancelled') console.error('window.promptForVariables encountered an error:', err);\n        throw err;\n      }\n    };\n  }\n\n  const handleSubmit = (values) => {\n    modalState.resolve(values);\n    setModalState({ open: false, prompts: [], resolve: null, reject: null });\n  };\n\n  const handleCancel = () => {\n    modalState.reject('cancelled');\n    setModalState({ open: false, prompts: [], resolve: null, reject: null });\n  };\n\n  return (\n    <PromptVariablesContext.Provider value={{ prompt }}>\n      {children}\n      {modalState.open && (\n        <PromptVariablesModal\n          title=\"Input Required\"\n          prompts={modalState.prompts}\n          onSubmit={handleSubmit}\n          onCancel={handleCancel}\n        />\n      )}\n    </PromptVariablesContext.Provider>\n  );\n}\n\nexport default PromptVariablesProvider;\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/index.js",
    "content": "import { configureStore } from '@reduxjs/toolkit';\nimport tasksMiddleware from './middlewares/tasks/middleware';\nimport debugMiddleware from './middlewares/debug/middleware';\nimport appReducer from './slices/app';\nimport collectionsReducer from './slices/collections';\nimport tabsReducer from './slices/tabs';\nimport notificationsReducer from './slices/notifications';\nimport globalEnvironmentsReducer from './slices/global-environments';\nimport logsReducer from './slices/logs';\nimport performanceReducer from './slices/performance';\nimport workspacesReducer from './slices/workspaces';\nimport apiSpecReducer from './slices/apiSpec';\nimport openapiSyncReducer from './slices/openapi-sync';\nimport { draftDetectMiddleware } from './middlewares/draft/middleware';\nimport { autosaveMiddleware } from './middlewares/autosave/middleware';\n\nconst isDevEnv = () => {\n  return import.meta.env.MODE === 'development';\n};\n\nlet middleware = [tasksMiddleware.middleware, draftDetectMiddleware, autosaveMiddleware];\nif (isDevEnv()) {\n  middleware = [...middleware, debugMiddleware.middleware];\n}\n\nexport const store = configureStore({\n  reducer: {\n    app: appReducer,\n    collections: collectionsReducer,\n    tabs: tabsReducer,\n    notifications: notificationsReducer,\n    globalEnvironments: globalEnvironmentsReducer,\n    logs: logsReducer,\n    performance: performanceReducer,\n    workspaces: workspacesReducer,\n    apiSpec: apiSpecReducer,\n    openapiSync: openapiSyncReducer\n  },\n  middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(middleware)\n});\n\nexport default store;\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/middlewares/autosave/middleware.js",
    "content": "import { saveRequest, saveCollectionSettings, saveFolderRoot, saveEnvironment } from '../../slices/collections/actions';\nimport { saveGlobalEnvironment } from '../../slices/global-environments';\nimport { flattenItems, isItemARequest, isItemAFolder, findItemInCollection, findCollectionByUid, isItemTransientRequest } from 'utils/collections';\n\nconst actionsToIntercept = [\n  // Request-level actions\n  'collections/requestUrlChanged',\n  'collections/updateAuth',\n  'collections/addQueryParam',\n  'collections/moveQueryParam',\n  'collections/updateQueryParam',\n  'collections/deleteQueryParam',\n  'collections/setQueryParams',\n  'collections/updatePathParam',\n  'collections/addRequestHeader',\n  'collections/updateRequestHeader',\n  'collections/deleteRequestHeader',\n  'collections/moveRequestHeader',\n  'collections/setRequestHeaders',\n  'collections/addFormUrlEncodedParam',\n  'collections/updateFormUrlEncodedParam',\n  'collections/deleteFormUrlEncodedParam',\n  'collections/moveFormUrlEncodedParam',\n  'collections/setFormUrlEncodedParams',\n  'collections/addMultipartFormParam',\n  'collections/updateMultipartFormParam',\n  'collections/deleteMultipartFormParam',\n  'collections/moveMultipartFormParam',\n  'collections/setMultipartFormParams',\n  'collections/updateRequestAuthMode',\n  'collections/updateRequestBodyMode',\n  'collections/updateRequestBody',\n  'collections/updateRequestGraphqlQuery',\n  'collections/updateRequestGraphqlVariables',\n  'collections/updateRequestScript',\n  'collections/updateResponseScript',\n  'collections/updateRequestTests',\n  'collections/updateRequestMethod',\n  'collections/addAssertion',\n  'collections/updateAssertion',\n  'collections/deleteAssertion',\n  'collections/moveAssertion',\n  'collections/addVar',\n  'collections/updateVar',\n  'collections/deleteVar',\n  'collections/moveVar',\n  'collections/updateRequestDocs',\n  'collections/runRequestEvent',\n  'collections/updateCollectionPresets',\n  'collections/setRequestVars',\n  'collections/setRequestAssertions',\n  'collections/updateItemSettings',\n  'collections/addRequestTag',\n  'collections/deleteRequestTag',\n\n  // Folder-level actions\n  'collections/addFolderHeader',\n  'collections/updateFolderHeader',\n  'collections/deleteFolderHeader',\n  'collections/setFolderHeaders',\n  'collections/addFolderVar',\n  'collections/updateFolderVar',\n  'collections/deleteFolderVar',\n  'collections/setFolderVars',\n  'collections/updateFolderRequestScript',\n  'collections/updateFolderResponseScript',\n  'collections/updateFolderTests',\n  'collections/updateFolderAuth',\n  'collections/updateFolderAuthMode',\n  'collections/updateFolderDocs',\n\n  // Collection-level actions\n  'collections/addCollectionHeader',\n  'collections/updateCollectionHeader',\n  'collections/deleteCollectionHeader',\n  'collections/setCollectionHeaders',\n  'collections/addCollectionVar',\n  'collections/updateCollectionVar',\n  'collections/deleteCollectionVar',\n  'collections/setCollectionVars',\n  'collections/updateCollectionAuth',\n  'collections/updateCollectionAuthMode',\n  'collections/updateCollectionRequestScript',\n  'collections/updateCollectionResponseScript',\n  'collections/updateCollectionTests',\n  'collections/updateCollectionDocs',\n  'collections/updateCollectionClientCertificates',\n  'collections/updateCollectionProtobuf',\n  'collections/updateCollectionProxy',\n\n  // Environment draft actions\n  'collections/setEnvironmentsDraft',\n  'global-environments/setGlobalEnvironmentDraft'\n];\n\n// Simple object to track pending save timers\nconst pendingTimers = {};\n\n// Helper to schedule autosave for an item\nconst scheduleAutoSave = (key, save, interval) => {\n  // Clear any existing timer for this entity\n  clearTimeout(pendingTimers[key]);\n\n  // Schedule a new save\n  pendingTimers[key] = setTimeout(() => {\n    save();\n    delete pendingTimers[key];\n  }, interval);\n};\n\n// Helper to find and schedule saves for all existing drafts\nconst saveExistingDrafts = (dispatch, getState, interval) => {\n  const state = getState();\n  const collections = state.collections.collections;\n\n  collections.forEach((collection) => {\n    // Check collection-level draft\n    if (collection.draft) {\n      const key = `collection-${collection.uid}`;\n      scheduleAutoSave(key, () => dispatch(saveCollectionSettings(collection.uid, null, true)), interval);\n    }\n\n    // Check collection environment drafts\n    if (collection.environmentsDraft) {\n      const { environmentUid, variables } = collection.environmentsDraft;\n      if (environmentUid && variables) {\n        const key = `environment-${collection.uid}-${environmentUid}`;\n        scheduleAutoSave(key, () => dispatch(saveEnvironment(variables, environmentUid, collection.uid)), interval);\n      }\n    }\n\n    // Check all items (requests and folders) for drafts\n    const allItems = flattenItems(collection.items);\n    allItems.forEach((item) => {\n      if (item.draft) {\n        if (isItemARequest(item)) {\n          // Skip auto-save for transient requests\n          if (isItemTransientRequest(item)) {\n            return;\n          }\n          const key = `request-${item.uid}`;\n          scheduleAutoSave(key, () => dispatch(saveRequest(item.uid, collection.uid, true)), interval);\n        } else if (isItemAFolder(item)) {\n          const key = `folder-${item.uid}`;\n          scheduleAutoSave(key, () => dispatch(saveFolderRoot(collection.uid, item.uid, true)), interval);\n        }\n      }\n    });\n  });\n\n  // Check global environment drafts\n  const globalEnvironmentDraft = state.globalEnvironments?.globalEnvironmentDraft;\n  if (globalEnvironmentDraft) {\n    const { environmentUid, variables } = globalEnvironmentDraft;\n    if (environmentUid && variables) {\n      const key = `global-environment-${environmentUid}`;\n      scheduleAutoSave(key, () => dispatch(saveGlobalEnvironment({ variables, environmentUid })), interval);\n    }\n  }\n};\n\n// Helper to determine entity type and create save handler\nconst determineSaveHandler = (actionType, payload, dispatch, getState) => {\n  const { itemUid, folderUid, collectionUid, environmentUid } = payload;\n\n  // Handle environment drafts\n  if (actionType === 'collections/setEnvironmentsDraft') {\n    if (!environmentUid || !collectionUid) return null;\n    return {\n      key: `environment-${collectionUid}-${environmentUid}`,\n      save: () => {\n        const state = getState();\n        const collection = state.collections.collections.find((c) => c.uid === collectionUid);\n        const draft = collection?.environmentsDraft;\n        if (draft?.environmentUid === environmentUid && draft?.variables) {\n          dispatch(saveEnvironment(draft.variables, environmentUid, collectionUid));\n        }\n      }\n    };\n  }\n\n  if (actionType === 'global-environments/setGlobalEnvironmentDraft') {\n    if (!environmentUid) return null;\n    return {\n      key: `global-environment-${environmentUid}`,\n      save: () => {\n        const state = getState();\n        const draft = state.globalEnvironments?.globalEnvironmentDraft;\n        if (draft?.environmentUid === environmentUid && draft?.variables) {\n          dispatch(saveGlobalEnvironment({ variables: draft.variables, environmentUid }));\n        }\n      }\n    };\n  }\n\n  // Handle folder actions\n  if (folderUid) {\n    return {\n      key: `folder-${folderUid}`,\n      save: () => dispatch(saveFolderRoot(collectionUid, folderUid, true))\n    };\n  }\n\n  // Handle request actions\n  if (itemUid) {\n    // Check if this is a transient request and skip auto-save\n    const state = getState();\n    const collection = findCollectionByUid(state.collections.collections, collectionUid);\n    if (collection) {\n      const item = findItemInCollection(collection, itemUid);\n      if (item && isItemTransientRequest(item)) {\n        return null; // Skip auto-save for transient requests\n      }\n    }\n\n    return {\n      key: `request-${itemUid}`,\n      save: () => dispatch(saveRequest(itemUid, collectionUid, true))\n    };\n  }\n\n  // Handle collection-level changes\n  if (collectionUid) {\n    return {\n      key: `collection-${collectionUid}`,\n      save: () => dispatch(saveCollectionSettings(collectionUid, null, true))\n    };\n  }\n\n  return null;\n};\n\nexport const autosaveMiddleware = ({ dispatch, getState }) => (next) => (action) => {\n  // Let the action update the state first\n  const result = next(action);\n\n  // Check if autosave is enabled\n  const { autoSave } = getState().app.preferences;\n  if (!autoSave?.enabled) return result;\n\n  // When autosave is enabled (or settings change), save any existing drafts\n  if (action.type === 'app/updatePreferences' && action.payload?.autoSave?.enabled) {\n    saveExistingDrafts(dispatch, getState, autoSave.interval);\n    return result;\n  }\n\n  if (action.type === 'app/updatePreferences' && action.payload?.autoSave?.enabled === false) {\n    Object.keys(pendingTimers).forEach((key) => {\n      clearTimeout(pendingTimers[key]);\n      delete pendingTimers[key];\n    });\n    return result;\n  }\n\n  // Only handle actions that create dirty state\n  if (!actionsToIntercept.includes(action.type)) return result;\n\n  const handler = determineSaveHandler(action.type, action.payload, dispatch, getState);\n  if (handler) {\n    scheduleAutoSave(handler.key, handler.save, autoSave.interval);\n  }\n\n  return result;\n};\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/middlewares/debug/middleware.js",
    "content": "import { createListenerMiddleware } from '@reduxjs/toolkit';\n\nconst debugMiddleware = createListenerMiddleware();\n\ndebugMiddleware.startListening({\n  predicate: () => true, // it'll track every change\n  effect: (action, listenerApi) => {\n    console.debug('---redux action---');\n    console.debug('action', action.type); // which action did it\n    console.debug('action.payload', action.payload);\n    console.debug(listenerApi.getState()); // the updated store\n  }\n});\n\nexport default debugMiddleware;\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/middlewares/draft/middleware.js",
    "content": "import { handleMakeTabParmanent } from './utils';\n\nconst actionsToIntercept = [\n  // Request-level actions\n  'collections/requestUrlChanged',\n  'collections/updateAuth',\n  'collections/addQueryParam',\n  'collections/moveQueryParam',\n  'collections/updateQueryParam',\n  'collections/deleteQueryParam',\n  'collections/setQueryParams',\n  'collections/updatePathParam',\n  'collections/addRequestHeader',\n  'collections/updateRequestHeader',\n  'collections/deleteRequestHeader',\n  'collections/moveRequestHeader',\n  'collections/setRequestHeaders',\n  'collections/addFormUrlEncodedParam',\n  'collections/updateFormUrlEncodedParam',\n  'collections/deleteFormUrlEncodedParam',\n  'collections/moveFormUrlEncodedParam',\n  'collections/setFormUrlEncodedParams',\n  'collections/addMultipartFormParam',\n  'collections/updateMultipartFormParam',\n  'collections/deleteMultipartFormParam',\n  'collections/moveMultipartFormParam',\n  'collections/setMultipartFormParams',\n  'collections/updateRequestAuthMode',\n  'collections/updateRequestBodyMode',\n  'collections/updateRequestBody',\n  'collections/updateRequestGraphqlQuery',\n  'collections/updateRequestGraphqlVariables',\n  'collections/updateRequestScript',\n  'collections/updateResponseScript',\n  'collections/updateRequestTests',\n  'collections/updateRequestMethod',\n  'collections/addAssertion',\n  'collections/updateAssertion',\n  'collections/deleteAssertion',\n  'collections/moveAssertion',\n  'collections/addVar',\n  'collections/updateVar',\n  'collections/deleteVar',\n  'collections/moveVar',\n  'collections/updateRequestDocs',\n  'collections/runRequestEvent', // TODO: This doesn't necessarily related to a draft state, need to rethink.\n\n  // Folder-level actions\n  'collections/addFolderHeader',\n  'collections/updateFolderHeader',\n  'collections/deleteFolderHeader',\n  'collections/setFolderHeaders',\n  'collections/addFolderVar',\n  'collections/updateFolderVar',\n  'collections/deleteFolderVar',\n  'collections/setFolderVars',\n  'collections/updateFolderRequestScript',\n  'collections/updateFolderResponseScript',\n  'collections/updateFolderTests',\n  'collections/updateFolderAuth',\n  'collections/updateFolderAuthMode',\n  'collections/updateFolderDocs',\n\n  // Collection-level actions\n  'collections/addCollectionHeader',\n  'collections/updateCollectionHeader',\n  'collections/deleteCollectionHeader',\n  'collections/setCollectionHeaders',\n  'collections/addCollectionVar',\n  'collections/updateCollectionVar',\n  'collections/deleteCollectionVar',\n  'collections/setCollectionVars',\n  'collections/updateCollectionAuth',\n  'collections/updateCollectionAuthMode',\n  'collections/updateCollectionRequestScript',\n  'collections/updateCollectionResponseScript',\n  'collections/updateCollectionTests',\n  'collections/updateCollectionDocs',\n  'collections/updateCollectionClientCertificates',\n  'collections/updateCollectionProtobuf',\n  'collections/updateCollectionProxy'\n];\n\nexport const draftDetectMiddleware = ({ dispatch, getState }) => (next) => (action) => {\n  if (actionsToIntercept.includes(action.type)) {\n    const state = getState();\n    handleMakeTabParmanent(state, action, dispatch);\n  }\n  return next(action);\n};\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/middlewares/draft/utils.js",
    "content": "import { makeTabPermanent } from 'providers/ReduxStore/slices/tabs';\nimport { findCollectionByUid, findItemInCollection } from 'utils/collections/index';\nimport find from 'lodash/find';\n\nfunction handleMakeTabParmanent(state, action, dispatch) {\n  const tabs = state.tabs.tabs;\n  const activeTabUid = state.tabs.activeTabUid;\n  const focusedTab = find(tabs, (t) => t.uid === activeTabUid);\n\n  if (!focusedTab || focusedTab.preview !== true) {\n    return;\n  }\n\n  const { itemUid, folderUid, collectionUid } = action.payload;\n  const collection = findCollectionByUid(state.collections.collections, collectionUid);\n\n  if (!collection) {\n    return;\n  }\n\n  // Handle request-level changes\n  if (itemUid) {\n    const item = findItemInCollection(collection, itemUid);\n    if (item) {\n      dispatch(makeTabPermanent({ uid: itemUid }));\n    }\n  } else if (folderUid) { // Handle folder-level changes (folder settings tab)\n    const folder = findItemInCollection(collection, folderUid);\n    if (folder) {\n      dispatch(makeTabPermanent({ uid: folderUid }));\n    }\n  } else if (collectionUid) {\n    // Handle collection-level changes (collection settings tab)\n    dispatch(makeTabPermanent({ uid: collectionUid }));\n  }\n}\n\nexport {\n  handleMakeTabParmanent\n};\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/middleware.js",
    "content": "import get from 'lodash/get';\nimport each from 'lodash/each';\nimport filter from 'lodash/filter';\nimport { createListenerMiddleware } from '@reduxjs/toolkit';\nimport { removeTaskFromQueue } from 'providers/ReduxStore/slices/app';\nimport { addTab } from 'providers/ReduxStore/slices/tabs';\nimport { collectionAddFileEvent, collectionChangeFileEvent } from 'providers/ReduxStore/slices/collections';\nimport { findCollectionByUid, findItemInCollectionByPathname, getDefaultRequestPaneTab, findItemInCollectionByItemUid } from 'utils/collections/index';\nimport { taskTypes } from './utils';\n\nconst taskMiddleware = createListenerMiddleware();\n\n/*\n * When a new request is created in the app, a task to open the request is added to the queue.\n * We wait for the File IO to complete, after which the \"collectionAddFileEvent\" gets dispatched.\n * This middleware listens for the event and checks if there is a task in the queue that matches\n * the collectionUid and itemPathname. If there is a match, we open the request and remove the task\n * from the queue.\n */\ntaskMiddleware.startListening({\n  actionCreator: collectionAddFileEvent,\n  effect: (action, listenerApi) => {\n    const state = listenerApi.getState();\n    const collectionUid = get(action, 'payload.file.meta.collectionUid');\n\n    const openRequestTasks = filter(state.app.taskQueue, { type: taskTypes.OPEN_REQUEST });\n    each(openRequestTasks, (task) => {\n      if (collectionUid === task.collectionUid) {\n        const collection = findCollectionByUid(state.collections.collections, collectionUid);\n        if (collection && collection.mountStatus === 'mounted' && !collection.isLoading) {\n          const item = findItemInCollectionByPathname(collection, task.itemPathname);\n          if (item) {\n            listenerApi.dispatch(\n              addTab({\n                uid: item.uid,\n                collectionUid: collection.uid,\n                requestPaneTab: getDefaultRequestPaneTab(item),\n                preview: task?.preview ?? true\n              })\n            );\n          }\n        }\n\n        listenerApi.dispatch(\n          removeTaskFromQueue({\n            taskUid: task.uid\n          })\n        );\n      }\n    });\n  }\n});\n\n/*\n * When an example is created or cloned, a task to open the example is added to the queue.\n * We wait for the File IO to complete, after which the \"collectionChangeFileEvent\" gets dispatched.\n * This middleware listens for the event and checks if there is a task in the queue that matches\n * the collectionUid, itemPathname, and exampleIndex. If there is a match, we open the example\n * tab and remove the task from the queue.\n */\ntaskMiddleware.startListening({\n  actionCreator: collectionChangeFileEvent,\n  effect: (action, listenerApi) => {\n    const state = listenerApi.getState();\n    const collectionUid = get(action, 'payload.file.meta.collectionUid');\n\n    const openExampleTasks = filter(state.app.taskQueue, { type: taskTypes.OPEN_EXAMPLE });\n    each(openExampleTasks, (task) => {\n      if (collectionUid === task.collectionUid) {\n        const collection = findCollectionByUid(state.collections.collections, collectionUid);\n        if (collection && collection.mountStatus === 'mounted' && !collection.isLoading) {\n          const item = findItemInCollectionByItemUid(collection, task.itemUid);\n          if (item && item.examples && item.examples.length > task.exampleIndex) {\n            const example = item.examples[task.exampleIndex];\n            if (example) {\n              listenerApi.dispatch(addTab({\n                uid: example.uid,\n                exampleUid: example.uid,\n                collectionUid: collection.uid,\n                type: 'response-example',\n                itemUid: item.uid\n              }));\n            }\n          }\n        }\n\n        listenerApi.dispatch(removeTaskFromQueue({\n          taskUid: task.uid\n        }));\n      }\n    });\n  }\n});\n\nexport default taskMiddleware;\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/middlewares/tasks/utils.js",
    "content": "export const taskTypes = {\n  OPEN_REQUEST: 'OPEN_REQUEST',\n  OPEN_EXAMPLE: 'OPEN_EXAMPLE'\n};\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/slices/apiSpec.js",
    "content": "import { createSlice } from '@reduxjs/toolkit';\nimport { find } from 'lodash';\nimport toast from 'react-hot-toast';\n\nconst initialState = {\n  apiSpecs: [],\n  activeApiSpecUid: null\n};\n\nexport const apiSpecSlice = createSlice({\n  name: 'apiSpec',\n  initialState,\n  reducers: {\n    apiSpecAddFileEvent: (state, action) => {\n      const { name, raw, uid, filename, pathname, json } = action?.payload?.data || {};\n      if (!uid) {\n        toast.error('Error adding API spec');\n      }\n      const apiSpec = findApiSpecByUid(state.apiSpecs, uid);\n      if (apiSpec) {\n        apiSpec.raw = raw;\n        apiSpec.name = name;\n        apiSpec.filename = filename;\n        apiSpec.pathname = pathname;\n        apiSpec.json = json;\n      } else {\n        const newApiSpec = {\n          name,\n          raw,\n          uid,\n          filename,\n          pathname,\n          json\n        };\n        state.apiSpecs.push(newApiSpec);\n      }\n      state.activeApiSpecUid = uid;\n    },\n    apiSpecChangeFileEvent: (state, action) => {\n      const { name, raw, uid, filename, pathname, json } = action?.payload?.data || {};\n      if (!uid) return;\n\n      const apiSpec = findApiSpecByUid(state.apiSpecs, uid);\n      if (apiSpec) {\n        apiSpec.raw = raw;\n        apiSpec.name = name;\n        apiSpec.filename = filename;\n        apiSpec.pathname = pathname;\n        apiSpec.json = json;\n      }\n    },\n    saveApiSpec: (state, action) => {\n      const { content, uid } = action.payload;\n      const apiSpec = findApiSpecByUid(state.apiSpecs, uid);\n      if (apiSpec) {\n        apiSpec.raw = content;\n      }\n    },\n    setActiveApiSpecUid: (state, action) => {\n      state.activeApiSpecUid = action.payload.uid;\n    },\n    removeApiSpec: (state, action) => {\n      const { uid } = action.payload;\n      let apiSpecIndex = state.apiSpecs.findIndex((c) => c.uid == uid);\n      state.apiSpecs = state.apiSpecs.filter((c) => c.uid !== uid);\n      let shiftedApiSpec = state.apiSpecs.at(apiSpecIndex);\n      let lastApiSpec = state.apiSpecs.at(-1);\n      state.activeApiSpecUid = shiftedApiSpec?.uid || lastApiSpec?.uid || null;\n    }\n  }\n});\n\nexport const { apiSpecAddFileEvent, apiSpecChangeFileEvent, saveApiSpec, removeApiSpec, setActiveApiSpecUid } = apiSpecSlice.actions;\n\nexport default apiSpecSlice.reducer;\n\nconst findApiSpecByUid = (apiSpecs, uid) => {\n  return find(apiSpecs, (apiSpec) => apiSpec.uid === uid);\n};\n\nexport const openApiSpec = (workspacePath = null) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n\n    if (!workspacePath) {\n      const state = getState();\n      const activeWorkspace = state.workspaces.workspaces.find((w) => w.uid === state.workspaces.activeWorkspaceUid);\n      workspacePath = activeWorkspace?.pathname || null;\n    }\n\n    ipcRenderer.invoke('renderer:open-api-spec', workspacePath).then(resolve).catch(reject);\n  });\n};\n\nexport const saveApiSpecToFile\n  = ({ uid, content }) =>\n    (dispatch, getState) => {\n      return new Promise((resolve, reject) => {\n        const { ipcRenderer } = window;\n        const state = getState();\n        const apiSpec = findApiSpecByUid(state.apiSpec.apiSpecs, uid);\n        const { pathname } = apiSpec;\n        ipcRenderer\n          .invoke('renderer:save-api-spec', pathname, content)\n          .then(() => {\n            dispatch(saveApiSpec({ content, uid }));\n            toast.success('Saved API spec successfully!');\n            resolve();\n          })\n          .catch((reject) => {\n            toast.error('Error saving file');\n            resolve();\n          });\n      });\n    };\n\nexport const createApiSpecFile = (apiSpecName, apiSpecLocation, content, workspacePath = null) => (dispatch, getState) => {\n  const { ipcRenderer } = window;\n\n  if (!workspacePath) {\n    const state = getState();\n    const activeWorkspace = state.workspaces.workspaces.find((w) => w.uid === state.workspaces.activeWorkspaceUid);\n    workspacePath = activeWorkspace?.pathname || null;\n  }\n\n  return new Promise((resolve, reject) => {\n    ipcRenderer.invoke('renderer:create-api-spec', apiSpecName, apiSpecLocation, content, workspacePath).then(resolve).catch(reject);\n  });\n};\n\nexport const closeApiSpecFile\n  = ({ uid }) =>\n    (dispatch, getState) => {\n      return new Promise((resolve, reject) => {\n        const state = getState();\n        const apiSpec = findApiSpecByUid(state.apiSpec.apiSpecs, uid);\n        if (!apiSpec) {\n          return reject(new Error('API Spec not found'));\n        }\n        if (apiSpec) {\n          const { ipcRenderer } = window;\n\n          const activeWorkspace = state.workspaces.workspaces.find((w) => w.uid === state.workspaces.activeWorkspaceUid);\n          const workspacePath = activeWorkspace?.pathname || null;\n\n          ipcRenderer\n            .invoke('renderer:remove-api-spec', apiSpec.pathname, workspacePath)\n            .then(async () => {\n              dispatch(removeApiSpec({ uid }));\n\n              if (activeWorkspace) {\n                const { loadWorkspaceApiSpecs } = require('./workspaces/actions');\n                await dispatch(loadWorkspaceApiSpecs(activeWorkspace.uid));\n              }\n\n              resolve();\n            })\n            .catch((error) => reject(error));\n        }\n        return;\n      });\n    };\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/slices/app.js",
    "content": "import { createSlice } from '@reduxjs/toolkit';\nimport filter from 'lodash/filter';\nimport brunoClipboard from 'utils/bruno-clipboard';\nimport { addTab, focusTab } from './tabs';\n\nconst initialState = {\n  isDragging: false,\n  idbConnectionReady: false,\n  leftSidebarWidth: 250,\n  sidebarCollapsed: false,\n  screenWidth: 500,\n  showHomePage: false,\n  showApiSpecPage: false,\n  showManageWorkspacePage: false,\n  isEnvironmentSettingsModalOpen: false,\n  isGlobalEnvironmentSettingsModalOpen: false,\n  activePreferencesTab: 'general',\n  preferences: {\n    request: {\n      sslVerification: true,\n      customCaCertificate: {\n        enabled: false,\n        filePath: null\n      },\n      keepDefaultCaCertificates: {\n        enabled: true\n      },\n      timeout: 0,\n      oauth2: {\n        useSystemBrowser: false\n      }\n    },\n    font: {\n      codeFont: 'default'\n    },\n    general: {\n      defaultLocation: ''\n    },\n    onboarding: {\n      hasLaunchedBefore: false,\n      hasSeenWelcomeModal: true\n    },\n    autoSave: {\n      enabled: false,\n      interval: 1000\n    },\n    cache: {\n      sslSession: {\n        enabled: false\n      }\n    }\n  },\n  generateCode: {\n    mainLanguage: 'Shell',\n    library: 'curl',\n    shouldInterpolate: true\n  },\n  cookies: [],\n  taskQueue: [],\n  gitOperationProgress: {},\n  gitVersion: null,\n  clipboard: {\n    hasCopiedItems: false // Whether clipboard has Bruno data (for UI)\n  },\n  systemProxyVariables: {},\n  envVarSearch: {\n    collection: { query: '', expanded: false },\n    global: { query: '', expanded: false }\n  },\n  isCreatingCollection: false\n};\n\nexport const appSlice = createSlice({\n  name: 'app',\n  initialState,\n  reducers: {\n    idbConnectionReady: (state) => {\n      state.idbConnectionReady = true;\n    },\n    refreshScreenWidth: (state) => {\n      state.screenWidth = window.innerWidth;\n    },\n    updateLeftSidebarWidth: (state, action) => {\n      state.leftSidebarWidth = action.payload.leftSidebarWidth;\n    },\n    updateIsDragging: (state, action) => {\n      state.isDragging = action.payload.isDragging;\n    },\n    showHomePage: (state) => {\n      state.showHomePage = true;\n      state.showApiSpecPage = false;\n      state.showManageWorkspacePage = false;\n    },\n    hideHomePage: (state) => {\n      state.showHomePage = false;\n    },\n    showManageWorkspacePage: (state) => {\n      state.showManageWorkspacePage = true;\n      state.showHomePage = false;\n      state.showApiSpecPage = false;\n    },\n    hideManageWorkspacePage: (state) => {\n      state.showManageWorkspacePage = false;\n    },\n    showApiSpecPage: (state) => {\n      state.showHomePage = false;\n      state.showApiSpecPage = true;\n    },\n    hideApiSpecPage: (state) => {\n      state.showApiSpecPage = false;\n    },\n    updatePreferences: (state, action) => {\n      state.preferences = action.payload;\n    },\n    updateActivePreferencesTab: (state, action) => {\n      state.activePreferencesTab = action.payload.tab;\n    },\n    updateCookies: (state, action) => {\n      state.cookies = action.payload;\n    },\n    insertTaskIntoQueue: (state, action) => {\n      state.taskQueue.push(action.payload);\n    },\n    removeTaskFromQueue: (state, action) => {\n      state.taskQueue = filter(state.taskQueue, (task) => task.uid !== action.payload.taskUid);\n    },\n    removeAllTasksFromQueue: (state) => {\n      state.taskQueue = [];\n    },\n    updateSystemProxyVariables: (state, action) => {\n      state.systemProxyVariables = action.payload;\n    },\n    updateGenerateCode: (state, action) => {\n      state.generateCode = {\n        ...state.generateCode,\n        ...action.payload\n      };\n    },\n    toggleSidebarCollapse: (state) => {\n      state.sidebarCollapsed = !state.sidebarCollapsed;\n    },\n    updateGitOperationProgress: (state, action) => {\n      const { uid, data } = action.payload;\n      if (!state.gitOperationProgress[uid]) {\n        state.gitOperationProgress[uid] = { progressData: [] };\n      }\n      state.gitOperationProgress[uid].progressData.push(data);\n    },\n    removeGitOperationProgress: (state, action) => {\n      delete state.gitOperationProgress[action.payload];\n    },\n    setGitVersion: (state, action) => {\n      state.gitVersion = action.payload;\n    },\n    setClipboard: (state, action) => {\n      // Update clipboard UI state\n      state.clipboard.hasCopiedItems = action.payload.hasCopiedItems;\n    },\n    setEnvVarSearchQuery: (state, { payload: { context, query } }) => {\n      if (!state.envVarSearch[context]) return;\n      state.envVarSearch[context].query = query;\n    },\n    setEnvVarSearchExpanded: (state, { payload: { context, expanded } }) => {\n      if (!state.envVarSearch[context]) return;\n      state.envVarSearch[context].expanded = expanded;\n    },\n    setIsCreatingCollection: (state, action) => {\n      state.isCreatingCollection = action.payload;\n    }\n  },\n  extraReducers: (builder) => {\n    // Automatically hide special pages when any tab is added or focused\n    builder\n      .addCase(addTab, (state) => {\n        state.showHomePage = false;\n        state.showApiSpecPage = false;\n        state.showManageWorkspacePage = false;\n      })\n      .addCase(focusTab, (state) => {\n        state.showHomePage = false;\n        state.showApiSpecPage = false;\n        state.showManageWorkspacePage = false;\n      });\n  }\n});\n\nexport const {\n  idbConnectionReady,\n  refreshScreenWidth,\n  updateLeftSidebarWidth,\n  updateIsDragging,\n  showHomePage,\n  hideHomePage,\n  showManageWorkspacePage,\n  hideManageWorkspacePage,\n  showApiSpecPage,\n  hideApiSpecPage,\n  updatePreferences,\n  updateActivePreferencesTab,\n  updateCookies,\n  insertTaskIntoQueue,\n  removeTaskFromQueue,\n  removeAllTasksFromQueue,\n  updateSystemProxyVariables,\n  updateGenerateCode,\n  toggleSidebarCollapse,\n  updateGitOperationProgress,\n  removeGitOperationProgress,\n  setGitVersion,\n  setClipboard,\n  setEnvVarSearchQuery,\n  setEnvVarSearchExpanded,\n  setIsCreatingCollection\n} = appSlice.actions;\n\nexport const savePreferences = (preferences) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n\n    ipcRenderer\n      .invoke('renderer:save-preferences', preferences)\n      .then(() => dispatch(updatePreferences(preferences)))\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const deleteCookiesForDomain = (domain) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n\n    ipcRenderer.invoke('renderer:delete-cookies-for-domain', domain).then(resolve).catch(reject);\n  });\n};\n\nexport const deleteCookie = (domain, path, cookieKey) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n\n    ipcRenderer.invoke('renderer:delete-cookie', domain, path, cookieKey).then(resolve).catch(reject);\n  });\n};\n\nexport const addCookie = (domain, cookie) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n\n    ipcRenderer.invoke('renderer:add-cookie', domain, cookie).then(resolve).catch(reject);\n  });\n};\n\nexport const modifyCookie = (domain, oldCookie, cookie) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n\n    ipcRenderer.invoke('renderer:modify-cookie', domain, oldCookie, cookie).then(resolve).catch(reject);\n  });\n};\n\nexport const getParsedCookie = (cookieStr) => () => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    ipcRenderer.invoke('renderer:get-parsed-cookie', cookieStr).then(resolve).catch(reject);\n  });\n};\n\nexport const createCookieString = (cookieObj) => () => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    ipcRenderer.invoke('renderer:create-cookie-string', cookieObj).then(resolve).catch(reject);\n  });\n};\n\nexport const completeQuitFlow = () => (dispatch, getState) => {\n  const { ipcRenderer } = window;\n  return ipcRenderer.invoke('main:complete-quit-flow');\n};\n\nexport const copyRequest = (item) => (dispatch, getState) => {\n  brunoClipboard.write(item);\n  dispatch(setClipboard({ hasCopiedItems: true }));\n  return Promise.resolve();\n};\n\nexport const getSystemProxyVariables = () => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    ipcRenderer.invoke('renderer:get-system-proxy-variables')\n      .then((variables) => {\n        dispatch(updateSystemProxyVariables(variables));\n        return variables;\n      })\n      .then(resolve).catch(reject);\n  });\n};\n\nexport const refreshSystemProxy = () => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    ipcRenderer.invoke('renderer:refresh-system-proxy')\n      .then((variables) => {\n        dispatch(updateSystemProxyVariables(variables));\n        return variables;\n      })\n      .then(resolve).catch(reject);\n  });\n};\n\nexport const clearHttpHttpsAgentCache = () => () => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    ipcRenderer.invoke('renderer:clear-http-https-agent-cache').then(resolve).catch(reject);\n  });\n};\n\nexport default appSlice.reducer;\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js",
    "content": "import { collectionSchema, environmentSchema, itemSchema } from '@usebruno/schema';\nimport { parseQueryParams, extractPromptVariables } from '@usebruno/common/utils';\nimport { REQUEST_TYPES, DEFAULT_COLLECTION_FORMAT } from 'utils/common/constants';\nimport cloneDeep from 'lodash/cloneDeep';\nimport filter from 'lodash/filter';\nimport find from 'lodash/find';\nimport get from 'lodash/get';\nimport set from 'lodash/set';\nimport trim from 'lodash/trim';\nimport path, { normalizePath } from 'utils/common/path';\nimport { insertTaskIntoQueue, toggleSidebarCollapse } from 'providers/ReduxStore/slices/app';\nimport toast from 'react-hot-toast';\nimport IpcErrorModal from 'components/Errors/IpcErrorModal/index';\nimport {\n  findCollectionByUid,\n  findEnvironmentInCollection,\n  findItemInCollection,\n  findParentItemInCollection,\n  isItemAFolder,\n  refreshUidsInItem,\n  isItemARequest,\n  getAllVariables,\n  transformRequestToSaveToFilesystem,\n  transformCollectionRootToSave,\n  flattenItems\n} from 'utils/collections';\nimport { uuid, waitForNextTick } from 'utils/common';\nimport { cancelNetworkRequest, connectWS, sendGrpcRequest, sendNetworkRequest, sendWsRequest } from 'utils/network/index';\nimport { callIpc } from 'utils/common/ipc';\nimport brunoClipboard from 'utils/bruno-clipboard';\n\nimport {\n  collectionAddEnvFileEvent as _collectionAddEnvFileEvent,\n  createCollection as _createCollection,\n  removeCollection as _removeCollection,\n  selectEnvironment as _selectEnvironment,\n  sortCollections as _sortCollections,\n  updateCollectionMountStatus,\n  moveCollection,\n  workspaceEnvUpdateEvent,\n  requestCancelled,\n  resetRunResults,\n  responseReceived,\n  updateLastAction,\n  setCollectionSecurityConfig,\n  collectionAddOauth2CredentialsByUrl,\n  collectionClearOauth2CredentialsByUrlAndCredentialsId,\n  initRunRequestEvent,\n  updateRunnerConfiguration as _updateRunnerConfiguration,\n  updateActiveConnections,\n  saveRequest as _saveRequest,\n  saveEnvironment as _saveEnvironment,\n  updateEnvironmentColor as _updateEnvironmentColor,\n  saveCollectionDraft,\n  saveFolderDraft,\n  addVar,\n  updateVar,\n  addFolderVar,\n  updateFolderVar,\n  addCollectionVar,\n  updateCollectionVar,\n  addTransientDirectory,\n  addSaveTransientRequestModal,\n  updatePathParam\n} from './index';\n\nimport { each } from 'lodash';\nimport { closeAllCollectionTabs, closeTabs as _closeTabs, focusTab, updateResponsePaneScrollPosition } from 'providers/ReduxStore/slices/tabs';\nimport { removeCollectionFromWorkspace } from 'providers/ReduxStore/slices/workspaces';\nimport { resolveRequestFilename } from 'utils/common/platform';\nimport { interpolateUrl, parsePathParams, splitOnFirst } from 'utils/url/index';\nimport { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';\nimport {\n  getGlobalEnvironmentVariables,\n  findCollectionByPathname,\n  findEnvironmentInCollectionByName,\n  getReorderedItemsInTargetDirectory,\n  resetSequencesInFolder,\n  getReorderedItemsInSourceDirectory,\n  calculateDraggedItemNewPathname,\n  transformFolderRootToSave,\n  getTreePathFromCollectionToItem,\n  mergeHeaders\n} from 'utils/collections/index';\nimport { sanitizeName } from 'utils/common/regex';\nimport { buildPersistedEnvVariables } from 'utils/environments';\nimport { safeParseJSON, safeStringifyJSON } from 'utils/common/index';\nimport { resolveInheritedAuth } from 'utils/auth';\nimport { addTab } from 'providers/ReduxStore/slices/tabs';\nimport { updateSettingsSelectedTab } from './index';\nimport { saveGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';\nimport { getTabToFocusForCurrentWorkspace } from 'providers/ReduxStore/slices/workspaces/getTabToFocusForCurrentWorkspace';\n\n// generate a unique names\nconst generateUniqueName = (originalName, existingItems, isFolder) => {\n  // Extract base name by removing any existing \" (number)\" suffix\n  const baseName = originalName.replace(/\\s*\\(\\d+\\)$/, '');\n  const baseFilename = sanitizeName(baseName);\n\n  // Get normalized filenames for items of the same type\n  const existingFilenames = existingItems\n    .filter((item) => isFolder ? item.type === 'folder' : item.type !== 'folder')\n    .map((item) => {\n      let filename = trim(item.filename);\n      // For requests, remove file extension (.bru, .yml, .yaml)\n      return isFolder ? filename : filename.replace(/\\.(bru|yml|yaml)$/, '');\n    });\n\n  // Check if base name conflicts with existing items\n  if (!existingFilenames.includes(baseFilename)) {\n    return { newName: baseName, newFilename: baseFilename };\n  }\n\n  // Find highest counter among conflicting names\n  const counters = existingFilenames\n    .filter((filename) => filename === baseFilename || filename.startsWith(`${baseFilename} (`))\n    .map((filename) => {\n      if (filename === baseFilename) return 0;\n      const match = filename.match(/\\((\\d+)\\)$/);\n      return match ? parseInt(match[1], 10) : 0;\n    });\n\n  const nextCounter = Math.max(0, ...counters) + 1;\n  return {\n    newName: `${baseName} (${nextCounter})`,\n    newFilename: `${baseFilename} (${nextCounter})`\n  };\n};\n\nexport const renameCollection = (newName, collectionUid) => (dispatch, getState) => {\n  const state = getState();\n  const collection = findCollectionByUid(state.collections.collections, collectionUid);\n\n  return new Promise((resolve, reject) => {\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n    const { ipcRenderer } = window;\n    ipcRenderer.invoke('renderer:rename-collection', newName, collection.pathname).then(resolve).catch(reject);\n  });\n};\n\nexport const saveRequest = (itemUid, collectionUid, silent = false) => (dispatch, getState) => {\n  const state = getState();\n  const collection = findCollectionByUid(state.collections.collections, collectionUid);\n  const tempDirectory = state.collections.tempDirectories?.[collectionUid];\n  return new Promise((resolve, reject) => {\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    const collectionCopy = cloneDeep(collection);\n    const item = findItemInCollection(collectionCopy, itemUid);\n    if (!item) {\n      return reject(new Error('Not able to locate item'));\n    }\n\n    const isTransient = tempDirectory && item.pathname.startsWith(tempDirectory);\n    if (isTransient) {\n      dispatch(addSaveTransientRequestModal({ item, collection }));\n      return reject();\n    }\n\n    const itemToSave = transformRequestToSaveToFilesystem(item);\n    const { ipcRenderer } = window;\n\n    itemSchema\n      .validate(itemToSave)\n      .then(() => ipcRenderer.invoke('renderer:save-request', item.pathname, itemToSave, collection.format))\n      .then(() => {\n        if (!silent) {\n          toast.success('Request saved successfully');\n        }\n        dispatch(\n          _saveRequest({\n            itemUid,\n            collectionUid\n          })\n        );\n      })\n      .then(resolve)\n      .catch((err) => {\n        toast.error(err.message || 'Failed to save request!');\n        reject(err);\n      });\n  });\n};\n\nexport const saveMultipleRequests = (items) => (dispatch, getState) => {\n  const state = getState();\n  const { collections } = state.collections;\n\n  return new Promise((resolve, reject) => {\n    const itemsToSave = [];\n    each(items, (item) => {\n      const collection = findCollectionByUid(collections, item.collectionUid);\n      if (collection) {\n        const itemToSave = transformRequestToSaveToFilesystem(item);\n        const itemIsValid = itemSchema.validateSync(itemToSave);\n        if (itemIsValid) {\n          itemsToSave.push({\n            item: itemToSave,\n            pathname: item.pathname,\n            format: collection.format\n          });\n        }\n      }\n    });\n\n    const { ipcRenderer } = window;\n\n    ipcRenderer\n      .invoke('renderer:save-multiple-requests', itemsToSave)\n      .then(resolve)\n      .catch((err) => {\n        toast.error('Failed to save requests!');\n        reject(err);\n      });\n  });\n};\n\nexport const saveCollectionRoot = (collectionUid) => (dispatch, getState) => {\n  const state = getState();\n  const collection = findCollectionByUid(state.collections.collections, collectionUid);\n\n  return new Promise((resolve, reject) => {\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    const collectionCopy = cloneDeep(collection);\n\n    // Transform collection root (uses draft if exists)\n    const collectionRootToSave = transformCollectionRootToSave(collectionCopy);\n    const { ipcRenderer } = window;\n\n    ipcRenderer\n      .invoke('renderer:save-collection-root', collectionCopy.pathname, collectionRootToSave, collectionCopy.brunoConfig)\n      .then(() => {\n        toast.success('Collection Settings saved successfully');\n        dispatch(saveCollectionDraft({ collectionUid }));\n      })\n      .then(resolve)\n      .catch((err) => {\n        toast.error('Failed to save collection settings!');\n        reject(err);\n      });\n  });\n};\n\nexport const saveFolderRoot = (collectionUid, folderUid, silent = false) => (dispatch, getState) => {\n  const state = getState();\n  const collection = findCollectionByUid(state.collections.collections, collectionUid);\n  const folder = findItemInCollection(collection, folderUid);\n\n  return new Promise((resolve, reject) => {\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    if (!folder) {\n      return reject(new Error('Folder not found'));\n    }\n\n    const { ipcRenderer } = window;\n\n    // Use draft if it exists, otherwise use root\n    const folderRootToSave = transformFolderRootToSave(folder);\n\n    const folderData = {\n      name: folder.name,\n      folderPathname: folder.pathname,\n      collectionPathname: collection.pathname,\n      root: folderRootToSave\n    };\n\n    ipcRenderer\n      .invoke('renderer:save-folder-root', folderData)\n      .then(() => {\n        if (!silent) {\n          toast.success('Folder Settings saved successfully');\n        }\n        // If there was a draft, save it to root and clear the draft\n        if (folder.draft) {\n          dispatch(saveFolderDraft({ collectionUid, folderUid }));\n        }\n      })\n      .then(resolve)\n      .catch((err) => {\n        toast.error('Failed to save folder settings!');\n        reject(err);\n      });\n  });\n};\n\nexport const saveMultipleCollections = (collectionDrafts) => (dispatch, getState) => {\n  const state = getState();\n  const { collections } = state.collections;\n\n  return new Promise((resolve, reject) => {\n    const savePromises = [];\n\n    each(collectionDrafts, (collectionDraft) => {\n      const collection = findCollectionByUid(collections, collectionDraft.collectionUid);\n      if (collection) {\n        const collectionCopy = cloneDeep(collection);\n        const collectionRootToSave = transformCollectionRootToSave(collectionCopy);\n        const { ipcRenderer } = window;\n\n        let savePromises = [];\n\n        savePromises.push(ipcRenderer.invoke('renderer:save-collection-root', collectionCopy.pathname, collectionRootToSave, collectionCopy.brunoConfig));\n\n        if (collectionCopy.draft?.brunoConfig) {\n          savePromises.push(ipcRenderer.invoke('renderer:update-bruno-config', collectionCopy.draft.brunoConfig, collectionCopy.pathname, collectionCopy.root));\n        }\n\n        Promise.all(savePromises)\n          .then(() => {\n            dispatch(saveCollectionDraft({ collectionUid: collectionDraft.collectionUid }));\n          })\n          .catch((err) => {\n            toast.error('Failed to save collection settings!');\n            reject(err);\n          });\n      }\n    });\n\n    Promise.all(savePromises)\n      .then(resolve)\n      .catch((err) => {\n        toast.error('Failed to save collection settings!');\n        reject(err);\n      });\n  });\n};\n\nexport const saveMultipleFolders = (folderDrafts) => (dispatch, getState) => {\n  const state = getState();\n  const { collections } = state.collections;\n\n  return new Promise((resolve, reject) => {\n    const savePromises = [];\n\n    each(folderDrafts, (folderDraft) => {\n      const collection = findCollectionByUid(collections, folderDraft.collectionUid);\n      const folder = collection ? findItemInCollection(collection, folderDraft.folderUid) : null;\n\n      if (collection && folder) {\n        const folderRootToSave = transformFolderRootToSave(folder);\n        const folderData = {\n          name: folder.name,\n          folderPathname: folder.pathname,\n          collectionPathname: collection.pathname,\n          root: folderRootToSave\n        };\n\n        const { ipcRenderer } = window;\n        const savePromise = ipcRenderer\n          .invoke('renderer:save-folder-root', folderData)\n          .then(() => {\n            if (folder.draft) {\n              dispatch(saveFolderDraft({ collectionUid: folderDraft.collectionUid, folderUid: folderDraft.folderUid }));\n            }\n          });\n\n        savePromises.push(savePromise);\n      }\n    });\n\n    Promise.all(savePromises)\n      .then(resolve)\n      .catch((err) => {\n        toast.error('Failed to save folder settings!');\n        reject(err);\n      });\n  });\n};\n\nexport const sendCollectionOauth2Request = (collectionUid, itemUid) => (dispatch, getState) => {\n  const state = getState();\n  const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;\n  const collection = findCollectionByUid(state.collections.collections, collectionUid);\n\n  return new Promise((resolve, reject) => {\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    let collectionCopy = cloneDeep(collection);\n\n    // add selected global env variables to the collection object\n    const globalEnvironmentVariables = getGlobalEnvironmentVariables({\n      globalEnvironments,\n      activeGlobalEnvironmentUid\n    });\n    collectionCopy.globalEnvironmentVariables = globalEnvironmentVariables;\n\n    const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);\n\n    _sendCollectionOauth2Request(collectionCopy, environment, collectionCopy.runtimeVariables)\n      .then((response) => {\n        if (response?.data?.error) {\n          toast.error(response?.data?.error);\n        } else {\n          toast.success('Request made successfully');\n        }\n        return response;\n      })\n      .then(resolve)\n      .catch((err) => {\n        toast.error(err.message);\n      });\n  });\n};\n\nexport const wsConnectOnly = (item, collectionUid) => (dispatch, getState) => {\n  const state = getState();\n  const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;\n  const collection = findCollectionByUid(state.collections.collections, collectionUid);\n\n  return new Promise(async (resolve, reject) => {\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    let collectionCopy = cloneDeep(collection);\n\n    const itemCopy = cloneDeep(item);\n\n    const requestUid = uuid();\n    itemCopy.requestUid = requestUid;\n\n    const globalEnvironmentVariables = getGlobalEnvironmentVariables({\n      globalEnvironments,\n      activeGlobalEnvironmentUid\n    });\n    collectionCopy.globalEnvironmentVariables = globalEnvironmentVariables;\n\n    const environment = findEnvironmentInCollection(collectionCopy, collectionCopy.activeEnvironmentUid);\n\n    connectWS(itemCopy, collectionCopy, environment, collectionCopy.runtimeVariables, { connectOnly: true })\n      .then(resolve)\n      .catch((err) => {\n        toast.error(err.message);\n      });\n  });\n};\n\n/**\n * Extract prompt variables from a request, collection, and environment variables.\n * Tries to respect the hierarchy of the variables and avoid unnecessary prompts as much as possible\n *\n * @param {*} item\n * @param {*} collection\n * @returns {Promise<Object>} A promise that resolves with the prompt variables or null if no prompt variables are found\n */\nconst extractPromptVariablesForRequest = async (item, collection) => {\n  return new Promise(async (resolve, reject) => {\n    // Ensure window contains promptForVariables function\n    if (typeof window === 'undefined' || typeof window.promptForVariables !== 'function') {\n      console.error('Failed to initialize prompt variables: window.promptForVariables is not available. '\n        + 'This may indicate an initialization issue with the app environment.');\n      return resolve(null);\n    }\n\n    const prompts = [];\n    const request = item.draft?.request ?? item.request ?? {};\n    const allVariables = getAllVariables(collection, item);\n    const clientCertConfig = get(collection, 'brunoConfig.clientCertificates.certs', []);\n    const requestTreePath = getTreePathFromCollectionToItem(collection, item);\n    // Get active headers from collection, folders, and request by priority order\n    const headers = mergeHeaders(collection, request, requestTreePath);\n    // Get request auth or inherited auth\n    const resolvedAuthRequest = resolveInheritedAuth(item, collection);\n\n    for (let clientCert of clientCertConfig) {\n      const domain = interpolateUrl({ url: clientCert?.domain, variables: allVariables });\n\n      if (domain) {\n        const hostRegex = '^(https:\\\\/\\\\/|grpc:\\\\/\\\\/|grpcs:\\\\/\\\\/)?' + domain.replaceAll('.', '\\\\.').replaceAll('*', '.*');\n        const requestUrl = interpolateUrl({ url: request.url, variables: allVariables });\n        if (requestUrl.match(hostRegex)) {\n          prompts.push(...extractPromptVariables(clientCert));\n        }\n      }\n    }\n\n    // Attempt to extract unique prompt variables from anywhere in the request and environment variables.\n    prompts.push(...extractPromptVariables(allVariables));\n    prompts.push(...extractPromptVariables(request.body?.[request.body.mode]));\n    prompts.push(...extractPromptVariables(headers));\n    prompts.push(...extractPromptVariables(request.params));\n    prompts.push(...extractPromptVariables(resolvedAuthRequest.auth));\n    prompts.push(...extractPromptVariables(request.url));\n\n    // Remove duplicates\n    const uniquePrompts = Array.from(new Set(prompts));\n\n    // If no prompt variables are found, return null\n    if (!uniquePrompts?.length) {\n      return resolve(null);\n    }\n\n    try {\n      // Prompt user for values if any prompt variables are found\n      const userValues = await window.promptForVariables(uniquePrompts);\n      const promptVariables = {};\n      // Populate runtimeVariables with user input for prompt variables\n      for (const prompt of uniquePrompts) {\n        promptVariables[`?${prompt}`] = userValues[prompt] ?? '';\n      }\n\n      return resolve(promptVariables);\n    } catch (error) {\n      return reject(error);\n    }\n  });\n};\n\nexport const sendRequest = (item, collectionUid) => (dispatch, getState) => {\n  const state = getState();\n  const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;\n  const collection = findCollectionByUid(state.collections.collections, collectionUid);\n  const itemUid = item?.uid;\n\n  return new Promise(async (resolve, reject) => {\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    let collectionCopy = cloneDeep(collection);\n\n    const itemCopy = cloneDeep(item);\n\n    // add selected global env variables to the collection object\n    const globalEnvironmentVariables = getGlobalEnvironmentVariables({\n      globalEnvironments,\n      activeGlobalEnvironmentUid\n    });\n    collectionCopy.globalEnvironmentVariables = globalEnvironmentVariables;\n\n    const requestUid = uuid();\n    itemCopy.requestUid = requestUid;\n\n    try {\n      const promptVariables = await extractPromptVariablesForRequest(itemCopy, collectionCopy);\n      collectionCopy.promptVariables = promptVariables ?? {};\n    } catch (error) {\n      if (error === 'cancelled') {\n        return resolve(); // Resolve without error if user cancels prompt\n      }\n      return reject(error);\n    }\n\n    await dispatch(\n      updateResponsePaneScrollPosition({\n        uid: state.tabs.activeTabUid,\n        scrollY: 0\n      })\n    );\n\n    await dispatch(\n      initRunRequestEvent({\n        requestUid,\n        itemUid,\n        collectionUid\n      })\n    );\n\n    const environment = findEnvironmentInCollection(collectionCopy, collectionCopy.activeEnvironmentUid);\n    const isGrpcRequest = itemCopy.type === 'grpc-request';\n    const isWsRequest = itemCopy.type === 'ws-request';\n    if (isGrpcRequest) {\n      sendGrpcRequest(itemCopy, collectionCopy, environment, collectionCopy.runtimeVariables)\n        .then(resolve)\n        .catch((err) => {\n          toast.error(err.message);\n        });\n    } else if (isWsRequest) {\n      sendWsRequest(itemCopy, collectionCopy, environment, collectionCopy.runtimeVariables)\n        .then(resolve)\n        .catch((err) => {\n          toast.error(err.message);\n        });\n    } else {\n      sendNetworkRequest(itemCopy, collectionCopy, environment, collectionCopy.runtimeVariables)\n        .then((response) => {\n          // Ensure any timestamps in the response are converted to numbers\n          const serializedResponse = {\n            ...response,\n            timeline: response.timeline?.map((entry) => ({\n              ...entry,\n              timestamp: entry.timestamp instanceof Date ? entry.timestamp.getTime() : entry.timestamp\n            }))\n          };\n\n          return dispatch(\n            responseReceived({\n              itemUid,\n              collectionUid,\n              response: serializedResponse\n            })\n          );\n        })\n        .then(resolve)\n        .catch((err) => {\n          if (err && err.message === 'Error invoking remote method \\'send-http-request\\': Error: Request cancelled') {\n            dispatch(\n              responseReceived({\n                itemUid,\n                collectionUid,\n                response: null\n              })\n            );\n            return;\n          }\n\n          const errorResponse = {\n            status: 'Error',\n            isError: true,\n            error: err.message ?? 'Something went wrong',\n            size: 0,\n            duration: 0\n          };\n\n          dispatch(\n            responseReceived({\n              itemUid,\n              collectionUid,\n              response: errorResponse\n            })\n          );\n        });\n    }\n  });\n};\n\nexport const cancelRequest = (cancelTokenUid, item, collection) => (dispatch) => {\n  cancelNetworkRequest(cancelTokenUid)\n    .then(() => {\n      dispatch(\n        requestCancelled({\n          itemUid: item.uid,\n          collectionUid: collection.uid\n        })\n      );\n    })\n    .catch((err) => console.log(err));\n};\n\nexport const cancelRunnerExecution = (cancelTokenUid) => (dispatch) => {\n  cancelNetworkRequest(cancelTokenUid).catch((err) => console.log(err));\n};\n\nexport const runCollectionFolder\n  = (collectionUid, folderUid, recursive, delay, tags, selectedRequestUids) => (dispatch, getState) => {\n    const state = getState();\n    const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;\n    const collection = findCollectionByUid(state.collections.collections, collectionUid);\n\n    return new Promise((resolve, reject) => {\n      if (!collection) {\n        return reject(new Error('Collection not found'));\n      }\n\n      let collectionCopy = cloneDeep(collection);\n\n      // add selected global env variables to the collection object\n      const globalEnvironmentVariables = getGlobalEnvironmentVariables({\n        globalEnvironments,\n        activeGlobalEnvironmentUid\n      });\n      collectionCopy.globalEnvironmentVariables = globalEnvironmentVariables;\n\n      const folder = findItemInCollection(collectionCopy, folderUid);\n\n      if (folderUid && !folder) {\n        return reject(new Error('Folder not found'));\n      }\n\n      const environment = findEnvironmentInCollection(collectionCopy, collection.activeEnvironmentUid);\n\n      dispatch(\n        resetRunResults({\n          collectionUid: collection.uid\n        })\n      );\n\n      const { ipcRenderer } = window;\n      ipcRenderer\n        .invoke(\n          'renderer:run-collection-folder',\n          folder,\n          collectionCopy,\n          environment,\n          collectionCopy.runtimeVariables,\n          recursive,\n          delay,\n          tags,\n          selectedRequestUids\n        )\n        .then(resolve)\n        .catch((err) => {\n          toast.error(get(err, 'error.message') || 'Something went wrong!');\n          reject(err);\n        });\n    });\n  };\n\nexport const newFolder = (folderName, directoryName, collectionUid, itemUid) => (dispatch, getState) => {\n  const state = getState();\n  const collection = findCollectionByUid(state.collections.collections, collectionUid);\n  const parentItem = itemUid ? findItemInCollection(collection, itemUid) : collection;\n  const items = filter(parentItem.items, (i) => isItemAFolder(i) || isItemARequest(i));\n\n  return new Promise((resolve, reject) => {\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    if (!itemUid) {\n      const folderWithSameNameExists = find(\n        collection.items,\n        (i) => i.type === 'folder' && trim(i.filename) === trim(directoryName)\n      );\n      if (!folderWithSameNameExists) {\n        const fullName = path.join(collection.pathname, directoryName);\n        const { ipcRenderer } = window;\n\n        const folderData = {\n          meta: {\n            name: folderName,\n            seq: items?.length + 1\n          },\n          request: {\n            auth: {\n              mode: 'inherit'\n            }\n          }\n        };\n\n        ipcRenderer\n          .invoke('renderer:new-folder', { pathname: fullName, folderData, format: collection.format })\n          .then(resolve)\n          .catch((error) => {\n            toast.error('Failed to create a new folder!');\n            reject(error);\n          });\n      } else {\n        return reject(new Error('Duplicate folder names under same parent folder are not allowed'));\n      }\n    } else {\n      const currentItem = findItemInCollection(collection, itemUid);\n      if (currentItem) {\n        const folderWithSameNameExists = find(\n          currentItem.items,\n          (i) => i.type === 'folder' && trim(i.filename) === trim(directoryName)\n        );\n        if (!folderWithSameNameExists) {\n          const fullName = path.join(currentItem.pathname, directoryName);\n          const { ipcRenderer } = window;\n\n          const folderData = {\n            meta: {\n              name: folderName,\n              seq: items?.length + 1\n            },\n            request: {\n              auth: {\n                mode: 'inherit'\n              }\n            }\n          };\n\n          ipcRenderer\n            .invoke('renderer:new-folder', { pathname: fullName, folderData, format: collection.format })\n            .then(resolve)\n            .catch((error) => {\n              toast.error('Failed to create a new folder!');\n              reject(error);\n            });\n        } else {\n          return reject(new Error('Duplicate folder names under same parent folder are not allowed'));\n        }\n      } else {\n        return reject(new Error('unable to find parent folder'));\n      }\n    }\n  });\n};\n\nexport const renameItem\n  = ({ newName, newFilename, itemUid, collectionUid }) =>\n    (dispatch, getState) => {\n      const state = getState();\n      const collection = findCollectionByUid(state.collections.collections, collectionUid);\n\n      return new Promise((resolve, reject) => {\n        if (!collection) {\n          return reject(new Error('Collection not found'));\n        }\n\n        const collectionCopy = cloneDeep(collection);\n        const item = findItemInCollection(collectionCopy, itemUid);\n        if (!item) {\n          return reject(new Error('Unable to locate item'));\n        }\n\n        const { ipcRenderer } = window;\n\n        const renameName = async () => {\n          return ipcRenderer.invoke('renderer:rename-item-name', { itemPath: item.pathname, newName, collectionPathname: collection.pathname }).catch((err) => {\n            toast.error('Failed to rename the item name');\n            console.error(err);\n            throw new Error('Failed to rename the item name');\n          });\n        };\n\n        const renameFile = async () => {\n          const dirname = path.dirname(item.pathname);\n          let newPath = '';\n          if (item.type === 'folder') {\n            newPath = path.join(dirname, trim(newFilename));\n          } else {\n            const filename = resolveRequestFilename(newFilename, collection.format);\n            newPath = path.join(dirname, filename);\n          }\n\n          return ipcRenderer\n            .invoke('renderer:rename-item-filename', { oldPath: item.pathname, newPath, newName, newFilename, collectionPathname: collection.pathname })\n            .catch((err) => {\n              console.error(err);\n              throw new Error('Duplicate request names are not allowed under the same folder');\n            });\n        };\n\n        let renameOperation = null;\n        if (newName) renameOperation = renameName;\n        if (newFilename) renameOperation = renameFile;\n\n        if (!renameOperation) {\n          resolve();\n        }\n\n        renameOperation()\n          .then(() => {\n            toast.success('Item renamed successfully');\n            resolve();\n          })\n          .catch((err) => reject(err));\n      });\n    };\n\nexport const cloneItem = (newName, newFilename, itemUid, collectionUid) => (dispatch, getState) => {\n  const state = getState();\n  const collection = findCollectionByUid(state.collections.collections, collectionUid);\n\n  return new Promise((resolve, reject) => {\n    if (!collection) {\n      throw new Error('Collection not found');\n    }\n    const collectionCopy = cloneDeep(collection);\n    const item = findItemInCollection(collectionCopy, itemUid);\n    if (!item) {\n      throw new Error('Unable to locate item');\n    }\n\n    if (isItemAFolder(item)) {\n      const parentFolder = findParentItemInCollection(collection, item.uid) || collection;\n\n      const folderWithSameNameExists = find(\n        parentFolder.items,\n        (i) => i.type === 'folder' && trim(i?.filename) === trim(newFilename)\n      );\n\n      if (folderWithSameNameExists) {\n        return reject(new Error('Duplicate folder names under same parent folder are not allowed'));\n      }\n\n      set(item, 'name', newName);\n      set(item, 'filename', newFilename);\n      set(item, 'root.meta.name', newName);\n      set(item, 'root.meta.seq', parentFolder?.items?.length + 1);\n\n      const collectionPath = path.join(parentFolder.pathname, newFilename);\n\n      const { ipcRenderer } = window;\n      ipcRenderer.invoke('renderer:clone-folder', item, collectionPath, collection.pathname).then(resolve).catch(reject);\n      return;\n    }\n\n    const parentItem = findParentItemInCollection(collectionCopy, itemUid);\n    const filename = resolveRequestFilename(newFilename, collection.format);\n    const itemToSave = refreshUidsInItem(transformRequestToSaveToFilesystem(item));\n    set(itemToSave, 'name', trim(newName));\n    set(itemToSave, 'filename', trim(filename));\n    if (!parentItem) {\n      const reqWithSameNameExists = find(\n        collection.items,\n        (i) => i.type !== 'folder' && trim(i.filename) === trim(filename)\n      );\n      if (!reqWithSameNameExists) {\n        const fullPathname = path.join(collection.pathname, filename);\n        const { ipcRenderer } = window;\n        const requestItems = filter(collection.items, (i) => i.type !== 'folder');\n        itemToSave.seq = requestItems ? requestItems.length + 1 : 1;\n\n        itemSchema\n          .validate(itemToSave)\n          .then(() => ipcRenderer.invoke('renderer:new-request', fullPathname, itemToSave))\n          .then(resolve)\n          .catch(reject);\n\n        dispatch(\n          insertTaskIntoQueue({\n            uid: uuid(),\n            type: 'OPEN_REQUEST',\n            collectionUid,\n            itemPathname: fullPathname\n          })\n        );\n      } else {\n        return reject(new Error('Duplicate request names are not allowed under the same folder'));\n      }\n    } else {\n      const reqWithSameNameExists = find(\n        parentItem.items,\n        (i) => i.type !== 'folder' && trim(i.filename) === trim(filename)\n      );\n      if (!reqWithSameNameExists) {\n        const dirname = path.dirname(item.pathname);\n        const fullName = path.join(dirname, filename);\n        const { ipcRenderer } = window;\n        const requestItems = filter(parentItem.items, (i) => i.type !== 'folder');\n        itemToSave.seq = requestItems ? requestItems.length + 1 : 1;\n\n        itemSchema\n          .validate(itemToSave)\n          .then(() => ipcRenderer.invoke('renderer:new-request', fullName, itemToSave))\n          .then(resolve)\n          .catch(reject);\n\n        dispatch(\n          insertTaskIntoQueue({\n            uid: uuid(),\n            type: 'OPEN_REQUEST',\n            collectionUid,\n            itemPathname: fullName\n          })\n        );\n      } else {\n        return reject(new Error('Duplicate request names are not allowed under the same folder'));\n      }\n    }\n  });\n};\n\nexport const pasteItem = (targetCollectionUid, targetItemUid = null) => (dispatch, getState) => {\n  const state = getState();\n\n  const clipboardResult = brunoClipboard.read();\n\n  if (!clipboardResult.hasData) {\n    return Promise.reject(new Error('No item in clipboard'));\n  }\n\n  const targetCollection = findCollectionByUid(state.collections.collections, targetCollectionUid);\n\n  if (!targetCollection) {\n    return Promise.reject(new Error('Target collection not found'));\n  }\n\n  return new Promise(async (resolve, reject) => {\n    try {\n      for (const clipboardItem of clipboardResult.items) {\n        const copiedItem = cloneDeep(clipboardItem);\n\n        const targetCollectionCopy = cloneDeep(targetCollection);\n        let targetItem = null;\n        let targetParentPathname = targetCollection.pathname;\n\n        // If targetItemUid is provided, we're pasting into a folder\n        if (targetItemUid) {\n          targetItem = findItemInCollection(targetCollectionCopy, targetItemUid);\n          if (!targetItem) {\n            return reject(new Error('Target folder not found'));\n          }\n          if (!isItemAFolder(targetItem)) {\n            return reject(new Error('Target must be a folder or collection'));\n          }\n          targetParentPathname = targetItem.pathname;\n        }\n\n        const existingItems = targetItem ? targetItem.items : targetCollection.items;\n\n        // Handle folder pasting\n        if (isItemAFolder(copiedItem)) {\n          // Generate unique name for folder\n          const { newName, newFilename } = generateUniqueName(copiedItem.name, existingItems, true);\n\n          set(copiedItem, 'name', newName);\n          set(copiedItem, 'filename', newFilename);\n          set(copiedItem, 'root.meta.name', newName);\n          set(copiedItem, 'root.meta.seq', (existingItems?.length ?? 0) + 1);\n\n          const fullPathname = path.join(targetParentPathname, newFilename);\n          const { ipcRenderer } = window;\n\n          await ipcRenderer.invoke('renderer:clone-folder', copiedItem, fullPathname, targetCollection.pathname);\n        } else {\n          // Handle request pasting\n          // Generate unique name for request\n          const { newName, newFilename } = generateUniqueName(copiedItem.name, existingItems, false);\n\n          const filename = resolveRequestFilename(newFilename, targetCollection.format);\n          const itemToSave = refreshUidsInItem(transformRequestToSaveToFilesystem(copiedItem));\n          set(itemToSave, 'name', trim(newName));\n          set(itemToSave, 'filename', trim(filename));\n\n          const fullPathname = path.join(targetParentPathname, filename);\n          const { ipcRenderer } = window;\n          const requestItems = filter(existingItems, (i) => i.type !== 'folder');\n          itemToSave.seq = requestItems ? requestItems.length + 1 : 1;\n\n          await itemSchema.validate(itemToSave);\n          await ipcRenderer.invoke('renderer:new-request', fullPathname, itemToSave, targetCollection.format);\n\n          dispatch(insertTaskIntoQueue({\n            uid: uuid(),\n            type: 'OPEN_REQUEST',\n            collectionUid: targetCollectionUid,\n            itemPathname: fullPathname\n          }));\n        }\n      }\n\n      resolve();\n    } catch (error) {\n      reject(error);\n    }\n  });\n};\n\nexport const deleteItem = (itemUid, collectionUid) => (dispatch, getState) => {\n  const state = getState();\n  const collection = findCollectionByUid(state.collections.collections, collectionUid);\n\n  return new Promise((resolve, reject) => {\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    const item = findItemInCollection(collection, itemUid);\n    if (item) {\n      const parentDirectoryItem = findParentItemInCollection(collection, itemUid) || collection;\n      const { ipcRenderer } = window;\n\n      ipcRenderer\n        .invoke('renderer:delete-item', item.pathname, item.type, collection.pathname)\n        .then(async () => {\n          // Reorder items in parent directory after deletion\n          if (parentDirectoryItem.items) {\n            const requestAndFolderTypes = [...REQUEST_TYPES, 'folder'];\n            const directoryItemsWithOnlyRequestAndFolders = parentDirectoryItem.items.filter((i) => requestAndFolderTypes.includes(i.type));\n            const directoryItemsWithoutDeletedItem = directoryItemsWithOnlyRequestAndFolders.filter((i) => i.uid !== itemUid);\n            const reorderedSourceItems = getReorderedItemsInSourceDirectory({\n              items: directoryItemsWithoutDeletedItem\n            });\n            if (reorderedSourceItems?.length) {\n              await dispatch(updateItemsSequences({ itemsToResequence: reorderedSourceItems, collectionUid }));\n            }\n          }\n          resolve();\n        })\n        .catch((error) => reject(error));\n    } else {\n      return reject(new Error('Unable to locate item'));\n    }\n  });\n};\n\nexport const sortCollections = (payload) => (dispatch) => {\n  dispatch(_sortCollections(payload));\n};\n\nexport const moveItem\n  = ({ targetDirname, sourcePathname }) =>\n    (dispatch, getState) => {\n      return new Promise((resolve, reject) => {\n        const { ipcRenderer } = window;\n\n        ipcRenderer.invoke('renderer:move-item', { targetDirname, sourcePathname }).then(resolve).catch(reject);\n      });\n    };\n\nexport const handleCollectionItemDrop\n  = ({ targetItem, draggedItem, dropType, collectionUid }) =>\n    (dispatch, getState) => {\n      const state = getState();\n      const collection = findCollectionByUid(state.collections.collections, collectionUid);\n      // if its withincollection set the source to current collection,\n      // if its cross collection set the source to the source collection\n      const sourceCollectionUid = draggedItem.sourceCollectionUid;\n      const isCrossCollectionMove = sourceCollectionUid && collectionUid !== sourceCollectionUid;\n      const sourceCollection = isCrossCollectionMove ? findCollectionByUid(state.collections.collections, sourceCollectionUid) : collection;\n      const { uid: draggedItemUid, pathname: draggedItemPathname } = draggedItem;\n      const { uid: targetItemUid, pathname: targetItemPathname } = targetItem;\n      const targetItemDirectory = findParentItemInCollection(collection, targetItemUid) || collection;\n      const targetItemDirectoryItems = cloneDeep(targetItemDirectory.items);\n      const draggedItemDirectory = findParentItemInCollection(sourceCollection, draggedItemUid) || sourceCollection;\n      const draggedItemDirectoryItems = cloneDeep(draggedItemDirectory.items);\n\n      const handleMoveToNewLocation = async ({\n        draggedItem,\n        draggedItemDirectoryItems,\n        targetItem,\n        targetItemDirectoryItems,\n        newPathname,\n        dropType\n      }) => {\n        const { uid: targetItemUid } = targetItem;\n        const { pathname: draggedItemPathname, uid: draggedItemUid } = draggedItem;\n\n        const newDirname = path.dirname(newPathname);\n        await dispatch(moveItem({\n          targetDirname: newDirname,\n          sourcePathname: draggedItemPathname\n        }));\n\n        // Update sequences in the source directory\n        if (draggedItemDirectoryItems?.length) {\n          // reorder items in the source directory\n          const draggedItemDirectoryItemsWithoutDraggedItem = draggedItemDirectoryItems.filter((i) => i.uid !== draggedItemUid);\n          const reorderedSourceItems = getReorderedItemsInSourceDirectory({\n            items: draggedItemDirectoryItemsWithoutDraggedItem\n          });\n          if (reorderedSourceItems?.length) {\n            await dispatch(updateItemsSequences({ itemsToResequence: reorderedSourceItems, collectionUid: sourceCollectionUid || collectionUid }));\n          }\n        }\n\n        // Update sequences in the target directory (if dropping adjacent)\n        if (dropType === 'adjacent') {\n          const targetItemSequence = targetItemDirectoryItems.find((i) => i.uid === targetItemUid)?.seq;\n\n          const draggedItemWithNewPathAndSequence = {\n            ...draggedItem,\n            pathname: newPathname,\n            seq: targetItemSequence\n          };\n\n          // draggedItem is added to the targetItem's directory\n          const reorderedTargetItems = getReorderedItemsInTargetDirectory({\n            items: [...targetItemDirectoryItems, draggedItemWithNewPathAndSequence],\n            targetItemUid,\n            draggedItemUid\n          });\n\n          if (reorderedTargetItems?.length) {\n            await dispatch(updateItemsSequences({ itemsToResequence: reorderedTargetItems, collectionUid }));\n          }\n        }\n      };\n\n      const handleReorderInSameLocation = async ({ draggedItem, targetItem, targetItemDirectoryItems }) => {\n        const { uid: targetItemUid } = targetItem;\n        const { uid: draggedItemUid } = draggedItem;\n\n        // reorder items in the targetItem's directory\n        const reorderedItems = getReorderedItemsInTargetDirectory({\n          items: targetItemDirectoryItems,\n          targetItemUid,\n          draggedItemUid\n        });\n\n        if (reorderedItems?.length) {\n          await dispatch(updateItemsSequences({ itemsToResequence: reorderedItems, collectionUid }));\n        }\n      };\n\n      return new Promise(async (resolve, reject) => {\n        try {\n          const newPathname = calculateDraggedItemNewPathname({\n            draggedItem,\n            targetItem,\n            dropType,\n            collectionPathname: collection.pathname\n          });\n          if (!newPathname) return;\n          if (targetItemPathname?.startsWith(draggedItemPathname)) return;\n\n          // Discard operation if dragging a root item to the collection name (same location)\n          const isTargetTheCollection = targetItemPathname === collection.pathname;\n          const isDraggedItemAtRoot = draggedItemDirectory === sourceCollection;\n          if (isTargetTheCollection && isDraggedItemAtRoot && !isCrossCollectionMove) {\n            return;\n          }\n\n          if (newPathname !== draggedItemPathname) {\n            await handleMoveToNewLocation({\n              targetItem,\n              targetItemDirectoryItems,\n              draggedItem,\n              draggedItemDirectoryItems,\n              newPathname,\n              dropType\n            });\n          } else {\n            await handleReorderInSameLocation({ draggedItem, targetItemDirectoryItems, targetItem });\n          }\n          resolve();\n        } catch (error) {\n          console.error(error);\n          toast.error(error?.message);\n          reject(error);\n        }\n      });\n    };\n\nexport const updateItemsSequences\n  = ({ itemsToResequence, collectionUid }) =>\n    (dispatch, getState) => {\n      return new Promise((resolve, reject) => {\n        const state = getState();\n        const collection = findCollectionByUid(state.collections.collections, collectionUid);\n\n        if (!collection) {\n          return reject(new Error('Collection not found'));\n        }\n\n        const { ipcRenderer } = window;\n\n        ipcRenderer.invoke('renderer:resequence-items', itemsToResequence, collection.pathname).then(resolve).catch(reject);\n      });\n    };\n\nexport const newHttpRequest = (params) => (dispatch, getState) => {\n  const {\n    requestName,\n    filename,\n    requestType,\n    requestUrl,\n    requestMethod,\n    collectionUid,\n    itemUid,\n    headers,\n    body,\n    auth,\n    settings,\n    isTransient = false\n  } = params;\n\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const collection = findCollectionByUid(state.collections.collections, collectionUid);\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    // Get temp directory if isTransient is true\n    const tempDirectory = isTransient ? state.collections.tempDirectories?.[collectionUid] : null;\n\n    const parts = splitOnFirst(requestUrl, '?');\n    const queryParams = parseQueryParams(parts[1]);\n    each(queryParams, (urlParam) => {\n      urlParam.enabled = true;\n      urlParam.type = 'query';\n    });\n\n    const pathParams = parsePathParams(requestUrl);\n    each(pathParams, (pathParm) => {\n      pathParams.enabled = true;\n      pathParm.type = 'path';\n    });\n\n    const params = [...queryParams, ...pathParams];\n\n    const item = {\n      uid: uuid(),\n      type: requestType,\n      name: requestName,\n      filename,\n      isTransient: isTransient,\n      request: {\n        method: requestMethod,\n        url: requestUrl,\n        headers: headers ?? [],\n        params,\n        body: body ?? {\n          mode: 'none',\n          json: null,\n          text: null,\n          xml: null,\n          sparql: null,\n          multipartForm: [],\n          formUrlEncoded: [],\n          file: []\n        },\n        vars: {\n          req: [],\n          res: []\n        },\n        assertions: [],\n        auth: auth ?? {\n          mode: 'inherit'\n        }\n      },\n      settings: settings ?? {\n        encodeUrl: true\n      }\n    };\n\n    // itemUid is null when we are creating a new request at the root level\n    // For transient requests, itemUid is always null\n    const resolvedFilename = resolveRequestFilename(filename, collection.format);\n\n    if (isTransient) {\n      // Transient requests are always created in temp directory\n      // Check for duplicates only among other transient requests\n      const allItems = flattenItems(collection.items);\n      const transientRequests = filter(\n        allItems,\n        (i) => isItemARequest(i) && i.pathname && i.pathname.startsWith(tempDirectory)\n      );\n      const reqWithSameNameExists = find(transientRequests, (i) => trim(i.filename) === trim(resolvedFilename));\n      const items = filter(collection.items, (i) => isItemAFolder(i) || isItemARequest(i));\n      item.seq = items.length + 1;\n\n      if (!reqWithSameNameExists) {\n        const fullName = path.join(tempDirectory, resolvedFilename);\n        const { ipcRenderer } = window;\n\n        ipcRenderer\n          .invoke('renderer:new-request', fullName, item)\n          .then(() => {\n            // task middleware will track this and open the new request in a new tab once request is created\n            dispatch(\n              insertTaskIntoQueue({\n                uid: uuid(),\n                type: 'OPEN_REQUEST',\n                collectionUid,\n                itemPathname: fullName,\n                preview: false\n              })\n            );\n            resolve();\n          })\n          .catch(reject);\n      } else {\n        return reject(new Error('Duplicate request names are not allowed under the same folder'));\n      }\n    } else if (!itemUid) {\n      // Regular request at root level\n      const reqWithSameNameExists = find(\n        collection.items,\n        (i) => i.type !== 'folder' && trim(i.filename) === trim(resolvedFilename)\n      );\n      const items = filter(collection.items, (i) => isItemAFolder(i) || isItemARequest(i));\n      item.seq = items.length + 1;\n\n      if (!reqWithSameNameExists) {\n        const fullName = path.join(collection.pathname, resolvedFilename);\n        const { ipcRenderer } = window;\n\n        ipcRenderer\n          .invoke('renderer:new-request', fullName, item)\n          .then(() => {\n            // task middleware will track this and open the new request in a new tab once request is created\n            dispatch(\n              insertTaskIntoQueue({\n                uid: uuid(),\n                type: 'OPEN_REQUEST',\n                collectionUid,\n                itemPathname: fullName\n              })\n            );\n            resolve();\n          })\n          .catch(reject);\n      } else {\n        return reject(new Error('Duplicate request names are not allowed under the same folder'));\n      }\n    } else {\n      const currentItem = findItemInCollection(collection, itemUid);\n      if (currentItem) {\n        const reqWithSameNameExists = find(\n          currentItem.items,\n          (i) => i.type !== 'folder' && trim(i.filename) === trim(resolvedFilename)\n        );\n        const items = filter(currentItem.items, (i) => isItemAFolder(i) || isItemARequest(i));\n        item.seq = items.length + 1;\n        if (!reqWithSameNameExists) {\n          const fullName = path.join(currentItem.pathname, resolvedFilename);\n          const { ipcRenderer } = window;\n          ipcRenderer\n            .invoke('renderer:new-request', fullName, item)\n            .then(() => {\n              // task middleware will track this and open the new request in a new tab once request is created\n              dispatch(\n                insertTaskIntoQueue({\n                  uid: uuid(),\n                  type: 'OPEN_REQUEST',\n                  collectionUid,\n                  itemPathname: fullName\n                })\n              );\n              resolve();\n            })\n            .catch(reject);\n        } else {\n          return reject(new Error('Duplicate request names are not allowed under the same folder'));\n        }\n      }\n    }\n  });\n};\n\nexport const newGrpcRequest = (params) => (dispatch, getState) => {\n  const { requestName, filename, requestUrl, collectionUid, body, auth, headers, itemUid, isTransient = false } = params;\n\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const collection = findCollectionByUid(state.collections.collections, collectionUid);\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    // Get temp directory if isTransient is true\n    const tempDirectory = isTransient ? state.collections.tempDirectories?.[collectionUid] : null;\n\n    // do we need to handle query, path params for grpc requests?\n    // skipping for now\n\n    const item = {\n      uid: uuid(),\n      name: requestName,\n      filename,\n      type: 'grpc-request',\n      isTransient: isTransient,\n      headers: headers ?? [],\n      request: {\n        url: requestUrl,\n        body: body ?? {\n          mode: 'grpc',\n          grpc: [\n            {\n              name: 'message 1',\n              content: '{}'\n            }\n          ]\n        },\n        auth: auth ?? {\n          mode: 'inherit'\n        },\n        vars: {\n          req: [],\n          res: []\n        },\n        script: {\n          req: null,\n          res: null\n        },\n        assertions: [],\n        tests: null\n      }\n    };\n\n    // itemUid is null when we are creating a new request at the root level\n    // For transient requests, itemUid is always null\n    const resolvedFilename = resolveRequestFilename(filename, collection.format);\n\n    if (isTransient) {\n      // Transient requests are always created in temp directory\n      // Check for duplicates only among other transient requests\n      const allItems = flattenItems(collection.items);\n      const transientRequests = filter(\n        allItems,\n        (i) => isItemARequest(i) && i.pathname && i.pathname.startsWith(tempDirectory)\n      );\n      const reqWithSameNameExists = find(transientRequests, (i) => trim(i.filename) === trim(resolvedFilename));\n\n      if (reqWithSameNameExists) {\n        return reject(new Error('Duplicate request names are not allowed under the same folder'));\n      }\n\n      const items = filter(collection.items, (i) => isItemAFolder(i) || isItemARequest(i));\n      item.seq = items.length + 1;\n      const fullName = path.join(tempDirectory, resolvedFilename);\n      const { ipcRenderer } = window;\n      ipcRenderer\n        .invoke('renderer:new-request', fullName, item)\n        .then(() => {\n          // task middleware will track this and open the new request in a new tab once request is created\n          dispatch(\n            insertTaskIntoQueue({\n              uid: uuid(),\n              type: 'OPEN_REQUEST',\n              collectionUid,\n              itemPathname: fullName,\n              preview: false\n            })\n          );\n          resolve();\n        })\n        .catch(reject);\n    } else {\n      // Regular request (can be at root or in a folder)\n      const parentItem = itemUid ? findItemInCollection(collection, itemUid) : collection;\n\n      if (!parentItem) {\n        return reject(new Error('Parent item not found'));\n      }\n\n      const reqWithSameNameExists = find(\n        parentItem.items,\n        (i) => i.type !== 'folder' && trim(i.filename) === trim(resolvedFilename)\n      );\n\n      if (reqWithSameNameExists) {\n        return reject(new Error('Duplicate request names are not allowed under the same folder'));\n      }\n\n      const items = filter(parentItem.items, (i) => isItemAFolder(i) || isItemARequest(i));\n      item.seq = items.length + 1;\n      const fullName = path.join(parentItem.pathname, resolvedFilename);\n      const { ipcRenderer } = window;\n      ipcRenderer\n        .invoke('renderer:new-request', fullName, item)\n        .then(() => {\n          // task middleware will track this and open the new request in a new tab once request is created\n          dispatch(\n            insertTaskIntoQueue({\n              uid: uuid(),\n              type: 'OPEN_REQUEST',\n              collectionUid,\n              itemPathname: fullName\n            })\n          );\n          resolve();\n        })\n        .catch(reject);\n    }\n  });\n};\n\nexport const newWsRequest = (params) => (dispatch, getState) => {\n  const { requestName, requestMethod, filename, requestUrl, collectionUid, body, auth, headers, itemUid, isTransient = false } = params;\n\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const collection = findCollectionByUid(state.collections.collections, collectionUid);\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    // Get temp directory if isTransient is true\n    const tempDirectory = isTransient ? state.collections.tempDirectories?.[collectionUid] : null;\n\n    const item = {\n      uid: uuid(),\n      name: requestName,\n      filename,\n      type: 'ws-request',\n      isTransient: isTransient,\n      headers: headers ?? [],\n      request: {\n        url: requestUrl,\n        method: requestMethod,\n        params: [],\n        body: body ?? {\n          mode: 'ws',\n          ws: [\n            {\n              name: 'message 1',\n              type: 'json',\n              content: '{}'\n            }\n          ]\n        },\n        auth: auth ?? {\n          mode: 'inherit'\n        },\n        vars: {\n          req: [],\n          res: []\n        },\n        script: {\n          req: null,\n          res: null\n        },\n        assertions: [],\n        tests: null\n      }\n    };\n\n    // itemUid is null when we are creating a new request at the root level\n    // For transient requests, itemUid is always null\n    const resolvedFilename = resolveRequestFilename(filename, collection.format);\n\n    if (isTransient) {\n      // Transient requests are always created in temp directory\n      // Check for duplicates only among other transient requests\n      const allItems = flattenItems(collection.items);\n      const transientRequests = filter(\n        allItems,\n        (i) => isItemARequest(i) && i.pathname && i.pathname.startsWith(tempDirectory)\n      );\n      const reqWithSameNameExists = find(transientRequests, (i) => trim(i.filename) === trim(resolvedFilename));\n\n      if (reqWithSameNameExists) {\n        return reject(new Error('Duplicate request names are not allowed under the same folder'));\n      }\n\n      const items = filter(collection.items, (i) => isItemAFolder(i) || isItemARequest(i));\n      item.seq = items.length + 1;\n      const fullName = path.join(tempDirectory, resolvedFilename);\n      const { ipcRenderer } = window;\n      ipcRenderer\n        .invoke('renderer:new-request', fullName, item)\n        .then(() => {\n          // task middleware will track this and open the new request in a new tab once request is created\n          dispatch(\n            insertTaskIntoQueue({\n              uid: uuid(),\n              type: 'OPEN_REQUEST',\n              collectionUid,\n              itemPathname: fullName,\n              preview: false\n            })\n          );\n          resolve();\n        })\n        .catch(reject);\n    } else {\n      // Regular request (can be at root or in a folder)\n      const parentItem = itemUid ? findItemInCollection(collection, itemUid) : collection;\n\n      if (!parentItem) {\n        return reject(new Error('Parent item not found'));\n      }\n\n      const reqWithSameNameExists = find(\n        parentItem.items,\n        (i) => i.type !== 'folder' && trim(i.filename) === trim(resolvedFilename)\n      );\n\n      if (reqWithSameNameExists) {\n        return reject(new Error('Duplicate request names are not allowed under the same folder'));\n      }\n\n      const items = filter(parentItem.items, (i) => isItemAFolder(i) || isItemARequest(i));\n      item.seq = items.length + 1;\n      const fullName = path.join(parentItem.pathname, resolvedFilename);\n      const { ipcRenderer } = window;\n      ipcRenderer\n        .invoke('renderer:new-request', fullName, item)\n        .then(() => {\n          // task middleware will track this and open the new request in a new tab once request is created\n          dispatch(\n            insertTaskIntoQueue({\n              uid: uuid(),\n              type: 'OPEN_REQUEST',\n              collectionUid,\n              itemPathname: fullName\n            })\n          );\n          resolve();\n        })\n        .catch(reject);\n    }\n  });\n};\n\nexport const loadGrpcMethodsFromReflection = (item, collectionUid, url) => async (dispatch, getState) => {\n  const state = getState();\n  const collection = findCollectionByUid(state.collections.collections, collectionUid);\n  const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;\n\n  return new Promise(async (resolve, reject) => {\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    const itemCopy = cloneDeep(item);\n    const requestItem = itemCopy.draft ? itemCopy.draft : itemCopy;\n    requestItem.request.url = url;\n    const collectionCopy = cloneDeep(collection);\n    const globalEnvironmentVariables = getGlobalEnvironmentVariables({\n      globalEnvironments,\n      activeGlobalEnvironmentUid\n    });\n    collectionCopy.globalEnvironmentVariables = globalEnvironmentVariables;\n    const environment = findEnvironmentInCollection(collectionCopy, collectionCopy.activeEnvironmentUid);\n    const runtimeVariables = collectionCopy.runtimeVariables;\n\n    try {\n      const promptVariables = await extractPromptVariablesForRequest(itemCopy, collectionCopy);\n      if (promptVariables) {\n        collectionCopy.promptVariables = promptVariables;\n      }\n    } catch (error) {\n      if (error === 'cancelled') {\n        return resolve(); // Resolve without error if user cancels prompt\n      }\n      return reject(error);\n    }\n\n    const { ipcRenderer } = window;\n    ipcRenderer\n      .invoke('grpc:load-methods-reflection', {\n        request: requestItem,\n        collection: collectionCopy,\n        environment,\n        runtimeVariables\n      })\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const generateGrpcurlCommand = (item, collectionUid) => async (dispatch, getState) => {\n  const state = getState();\n  const collection = findCollectionByUid(state.collections.collections, collectionUid);\n  const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;\n\n  return new Promise((resolve, reject) => {\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    const itemCopy = cloneDeep(item);\n    const collectionCopy = cloneDeep(collection);\n\n    const globalEnvironmentVariables = getGlobalEnvironmentVariables({\n      globalEnvironments,\n      activeGlobalEnvironmentUid\n    });\n    collectionCopy.globalEnvironmentVariables = globalEnvironmentVariables;\n    const environment = findEnvironmentInCollection(collectionCopy, collectionCopy.activeEnvironmentUid);\n    const runtimeVariables = collectionCopy.runtimeVariables;\n\n    const { ipcRenderer } = window;\n    ipcRenderer\n      .invoke('grpc:generate-grpcurl', { request: itemCopy, collection: collectionCopy, environment, runtimeVariables })\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const addEnvironment = (name, collectionUid) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const collection = findCollectionByUid(state.collections.collections, collectionUid);\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    const { ipcRenderer } = window;\n    ipcRenderer\n      .invoke('renderer:create-environment', collection.pathname, name)\n      .then(\n        dispatch(\n          updateLastAction({\n            collectionUid,\n            lastAction: {\n              type: 'ADD_ENVIRONMENT',\n              payload: name\n            }\n          })\n        )\n      )\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const importEnvironment = ({ name, variables, color, collectionUid }) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const collection = findCollectionByUid(state.collections.collections, collectionUid);\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    const sanitizedName = sanitizeName(name);\n\n    const { ipcRenderer } = window;\n    ipcRenderer\n      .invoke('renderer:create-environment', collection.pathname, sanitizedName, variables, color)\n      .then(\n        dispatch(\n          updateLastAction({\n            collectionUid,\n            lastAction: {\n              type: 'ADD_ENVIRONMENT',\n              payload: sanitizedName\n            }\n          })\n        )\n      )\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const copyEnvironment = (name, baseEnvUid, collectionUid) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const collection = findCollectionByUid(state.collections.collections, collectionUid);\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    const baseEnv = findEnvironmentInCollection(collection, baseEnvUid);\n    if (!collection) {\n      return reject(new Error('Environment not found'));\n    }\n\n    const sanitizedName = sanitizeName(name);\n\n    const { ipcRenderer } = window;\n\n    // strip \"ephemeral\" metadata\n    const variablesToCopy = (baseEnv.variables || [])\n      .filter((v) => !v.ephemeral)\n      .map(({ ephemeral, ...rest }) => {\n        return rest;\n      });\n\n    ipcRenderer\n      .invoke('renderer:create-environment', collection.pathname, sanitizedName, variablesToCopy)\n      .then(\n        dispatch(\n          updateLastAction({\n            collectionUid,\n            lastAction: {\n              type: 'ADD_ENVIRONMENT',\n              payload: sanitizedName\n            }\n          })\n        )\n      )\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const renameEnvironment = (newName, environmentUid, collectionUid) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const collection = findCollectionByUid(state.collections.collections, collectionUid);\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    const collectionCopy = cloneDeep(collection);\n    const environment = findEnvironmentInCollection(collectionCopy, environmentUid);\n    if (!environment) {\n      return reject(new Error('Environment not found'));\n    }\n\n    const sanitizedName = sanitizeName(newName);\n    const oldName = environment.name;\n    environment.name = sanitizedName;\n\n    const { ipcRenderer } = window;\n    environmentSchema\n      .validate(environment)\n      .then(() => ipcRenderer.invoke('renderer:rename-environment', collection.pathname, oldName, sanitizedName))\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const deleteEnvironment = (environmentUid, collectionUid) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const collection = findCollectionByUid(state.collections.collections, collectionUid);\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    const collectionCopy = cloneDeep(collection);\n\n    const environment = findEnvironmentInCollection(collectionCopy, environmentUid);\n    if (!environment) {\n      return reject(new Error('Environment not found'));\n    }\n\n    const { ipcRenderer } = window;\n    ipcRenderer\n      .invoke('renderer:delete-environment', collection.pathname, environment.name)\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const saveEnvironment = (variables, environmentUid, collectionUid) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const collection = findCollectionByUid(state.collections.collections, collectionUid);\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    const collectionCopy = cloneDeep(collection);\n    const environment = findEnvironmentInCollection(collectionCopy, environmentUid);\n    if (!environment) {\n      return reject(new Error('Environment not found'));\n    }\n\n    /*\n     Modal Save writes what the user sees:\n     - Non-ephemeral vars are saved as-is (without metadata)\n     - Ephemeral vars:\n       - if persistedValue exists, save that (explicit persisted case)\n       - otherwise save the current UI value (treat as user-authored)\n     */\n    const persisted = buildPersistedEnvVariables(variables, { mode: 'save' });\n    environment.variables = persisted;\n\n    const { ipcRenderer } = window;\n    const envForValidation = cloneDeep(environment);\n\n    environmentSchema\n      .validate(environment)\n      .then(() => ipcRenderer.invoke('renderer:save-environment', collection.pathname, envForValidation))\n      .then(() => {\n        // Immediately sync Redux to the saved (persisted) set so old ephemerals\n        // aren’t around when the watcher event arrives.\n        dispatch(_saveEnvironment({ variables: persisted, environmentUid, collectionUid }));\n      })\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const updateEnvironmentColor = (environmentUid, color, collectionUid) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const collection = findCollectionByUid(state.collections.collections, collectionUid);\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    const collectionCopy = cloneDeep(collection);\n    const environment = findEnvironmentInCollection(collectionCopy, environmentUid);\n    if (!environment) {\n      return reject(new Error('Environment not found'));\n    }\n\n    environment.color = color;\n    const { ipcRenderer } = window;\n    ipcRenderer.invoke('renderer:update-environment-color', collection.pathname, environment.name, color)\n      .then(() => {\n        dispatch(_updateEnvironmentColor({ environmentUid, color, collectionUid }));\n        resolve();\n      })\n      .catch(reject);\n  });\n};\n\n/**\n * Update a variable value directly in the file without affecting draft state\n * @param {string} pathname - File path\n * @param {Object} variable - Variable object with uid, name, value, type, enabled\n * @param {string} scopeType - Type of scope ('request', 'folder', 'collection')\n * @param {string} collectionUid - Collection UID\n * @param {string} itemUid - Item/Folder UID (for request/folder)\n */\nconst updateVariableInFile = (pathname, variable, scopeType, collectionUid, itemUid) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n\n    const state = getState();\n    const collection = findCollectionByUid(state.collections.collections, collectionUid);\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    const collectionCopy = cloneDeep(collection);\n\n    ipcRenderer\n      .invoke('renderer:update-variable-in-file', pathname, variable, scopeType, collectionCopy.root, collectionCopy.format)\n      .then(() => {\n        // Update Redux state to reflect the change\n        if (scopeType === 'request') {\n          dispatch({\n            type: 'collections/updateRequestVarValue',\n            payload: { collectionUid, itemUid, variable }\n          });\n        } else if (scopeType === 'folder') {\n          dispatch({\n            type: 'collections/updateFolderVarValue',\n            payload: { collectionUid, folderUid: itemUid, variable }\n          });\n        } else if (scopeType === 'collection') {\n          dispatch({\n            type: 'collections/updateCollectionVarValue',\n            payload: { collectionUid, variable }\n          });\n        }\n\n        resolve();\n      })\n      .catch(reject);\n  });\n};\n\n/**\n * Helper: Execute update action with toast notification\n * @param {Function} action - The action to dispatch\n * @param {string} successMessage - Success toast message\n * @returns {Promise}\n */\nconst executeVariableUpdate = (dispatch, action, successMessage) => {\n  return dispatch(action)\n    .then(() => {\n      toast.success(successMessage);\n    });\n};\n\n/**\n * Update a variable value in its detected scope (inline editing)\n * @param {string} variableName - Name of the variable to update\n * @param {string} newValue - New value for the variable\n * @param {Object} scopeInfo - Scope information from getVariableScope()\n * @param {string} collectionUid - Collection UID\n */\nexport const updateVariableInScope = (variableName, newValue, scopeInfo, collectionUid) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    if (!scopeInfo || !variableName) {\n      return reject(new Error('Invalid scope information or variable name'));\n    }\n\n    const state = getState();\n    const collection = findCollectionByUid(state.collections.collections, collectionUid);\n\n    try {\n      const { type, data } = scopeInfo;\n\n      // Handle read-only variables early\n      if (type === 'process.env') {\n        toast.error('Process environment variables cannot be edited');\n        return reject(new Error('Process environment variables are read-only'));\n      }\n\n      if (type === 'runtime' || (collection && collection.runtimeVariables && collection.runtimeVariables[variableName])) {\n        toast.error('Runtime variables are set by scripts and cannot be edited');\n        return reject(new Error('Runtime variables are read-only'));\n      }\n\n      // Validate collection for non-global scopes\n      if (type !== 'global' && !collection) {\n        return reject(new Error('Collection not found'));\n      }\n\n      switch (type) {\n        case 'environment': {\n          const { environment, variable } = data;\n\n          if (!variable) {\n            return reject(new Error('Variable not found'));\n          }\n\n          const updatedVariables = environment.variables.map((v) => {\n            if (v.uid === variable.uid) {\n              // Clear ephemeral metadata when user manually edits the value\n              const { ephemeral, persistedValue, ...rest } = v;\n              return { ...rest, value: newValue };\n            }\n            return v;\n          });\n\n          return dispatch(saveEnvironment(updatedVariables, environment.uid, collectionUid))\n            .then(() => {\n              toast.success(`Variable \"${variableName}\" updated`);\n            })\n            .then(resolve)\n            .catch(reject);\n        }\n\n        case 'collection': {\n          const { variable } = data;\n\n          if (variable) {\n            // Update existing variable in draft\n            dispatch(updateCollectionVar({\n              collectionUid,\n              type: 'request',\n              var: { ...variable, value: newValue }\n            }));\n          } else {\n            // Create new variable in draft with actual values\n            dispatch(addCollectionVar({\n              collectionUid,\n              type: 'request',\n              var: { name: variableName, value: newValue, enabled: true }\n            }));\n          }\n\n          // Save collection root to persist the changes\n          return dispatch(saveCollectionRoot(collectionUid))\n            .then(resolve)\n            .catch(reject);\n        }\n\n        case 'folder': {\n          const { folder, variable } = data;\n\n          if (variable) {\n            // Update existing variable in draft\n            dispatch(updateFolderVar({\n              collectionUid,\n              folderUid: folder.uid,\n              type: 'request',\n              var: { ...variable, value: newValue }\n            }));\n          } else {\n            // Create new variable in draft with actual values\n            dispatch(addFolderVar({\n              collectionUid,\n              folderUid: folder.uid,\n              type: 'request',\n              var: { name: variableName, value: newValue, enabled: true }\n            }));\n          }\n\n          // Save folder root to persist the changes\n          return dispatch(saveFolderRoot(collectionUid, folder.uid))\n            .then(resolve)\n            .catch(reject);\n        }\n\n        case 'request': {\n          const { item, variable } = data;\n\n          if (variable) {\n            // Update existing variable in draft\n            dispatch(updateVar({\n              collectionUid,\n              itemUid: item.uid,\n              type: 'request',\n              var: { ...variable, value: newValue }\n            }));\n          } else {\n            // Create new variable in draft with actual values\n            dispatch(addVar({\n              collectionUid,\n              itemUid: item.uid,\n              type: 'request',\n              var: { name: variableName, value: newValue, local: false, enabled: true }\n            }));\n          }\n\n          // Save request to persist the changes\n          return dispatch(saveRequest(item.uid, collectionUid, true))\n            .then(resolve)\n            .catch(reject);\n        }\n\n        case 'global': {\n          const globalEnvironments = state.globalEnvironments?.globalEnvironments || [];\n          const activeGlobalEnvUid = state.globalEnvironments?.activeGlobalEnvironmentUid;\n\n          if (!activeGlobalEnvUid) {\n            return reject(new Error('No active global environment'));\n          }\n\n          const environment = globalEnvironments.find((env) => env.uid === activeGlobalEnvUid);\n\n          if (!environment) {\n            return reject(new Error('Global environment not found'));\n          }\n\n          const variable = environment.variables.find((v) => v.name === variableName && v.enabled);\n\n          if (!variable) {\n            return reject(new Error('Variable not found'));\n          }\n\n          const updatedVariables = environment.variables.map((v) => {\n            if (v.uid === variable.uid) {\n              // Clear ephemeral metadata when user manually edits the value\n              const { ephemeral, persistedValue, ...rest } = v;\n              return { ...rest, value: newValue };\n            }\n            return v;\n          });\n\n          return dispatch(saveGlobalEnvironment({ variables: updatedVariables, environmentUid: activeGlobalEnvUid }))\n            .then(() => {\n              toast.success(`Variable \"${variableName}\" updated`);\n            })\n            .then(resolve)\n            .catch(reject);\n        }\n        case 'pathParam': {\n          const { item } = data;\n          const params = item.draft ? get(item, 'draft.request.params', []) : get(item, 'request.params', []);\n          const pathParam = params.find((p) => p.type === 'path' && p.name === variableName);\n\n          if (pathParam) {\n            const updatedParam = { ...pathParam, value: newValue };\n            dispatch(updatePathParam({\n              pathParam: updatedParam,\n              itemUid: item.uid,\n              collectionUid: collection.uid\n            }));\n          }\n          return dispatch(saveRequest(item.uid, collection.uid, true))\n            .then(resolve)\n            .catch(reject);\n        }\n        default:\n          return reject(new Error(`Unknown scope type: ${type}`));\n      }\n    } catch (error) {\n      toast.error(`Failed to update variable: ${error.message}`);\n      reject(error);\n    }\n  });\n};\n\nexport const mergeAndPersistEnvironment\n  = ({ persistentEnvVariables, collectionUid }) =>\n    (_dispatch, getState) => {\n      return new Promise((resolve, reject) => {\n        const state = getState();\n        const collection = findCollectionByUid(state.collections.collections, collectionUid);\n\n        if (!collection) {\n          return reject(new Error('Collection not found'));\n        }\n\n        const environmentUid = collection.activeEnvironmentUid;\n        if (!environmentUid) {\n          return reject(new Error('No active environment found'));\n        }\n\n        const collectionCopy = cloneDeep(collection);\n        const environment = findEnvironmentInCollection(collectionCopy, environmentUid);\n        if (!environment) {\n          return reject(new Error('Environment not found'));\n        }\n\n        // Only proceed if there are persistent variables to save\n        if (!persistentEnvVariables || Object.keys(persistentEnvVariables).length === 0) {\n          return resolve();\n        }\n\n        let existingVars = environment.variables || [];\n\n        let normalizedNewVars = Object.entries(persistentEnvVariables).map(([name, value]) => ({\n          uid: uuid(),\n          name,\n          value,\n          type: 'text',\n          enabled: true,\n          secret: false\n        }));\n\n        const merged = existingVars.map((v) => {\n          const found = normalizedNewVars.find((nv) => nv.name === v.name);\n          if (found) {\n            return { ...v, value: found.value };\n          }\n          return v;\n        });\n        normalizedNewVars.forEach((nv) => {\n          if (!merged.some((v) => v.name === nv.name)) {\n            merged.push(nv);\n          }\n        });\n\n        // Save all non-ephemeral vars and all variables that were previously persisted\n        const persistedNames = new Set(Object.keys(persistentEnvVariables));\n\n        // Add all existing non-ephemeral variables to persistedNames so they are preserved\n        existingVars.forEach((v) => {\n          if (!v.ephemeral) {\n            persistedNames.add(v.name);\n          }\n        });\n\n        const environmentToSave = cloneDeep(environment);\n        environmentToSave.variables = buildPersistedEnvVariables(merged, { mode: 'merge', persistedNames });\n\n        const { ipcRenderer } = window;\n        environmentSchema\n          .validate(environmentToSave)\n          .then(() => ipcRenderer.invoke('renderer:save-environment', collection.pathname, environmentToSave))\n          .then(resolve)\n          .catch(reject);\n      });\n    };\n\nexport const selectEnvironment = (environmentUid, collectionUid) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const collection = findCollectionByUid(state.collections.collections, collectionUid);\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    const collectionCopy = cloneDeep(collection);\n\n    const environmentName = environmentUid ? findEnvironmentInCollection(collectionCopy, environmentUid)?.name : null;\n\n    if (environmentUid && !environmentName) {\n      return reject(new Error('Environment not found'));\n    }\n\n    const { ipcRenderer } = window;\n    ipcRenderer.invoke('renderer:update-ui-state-snapshot', {\n      type: 'COLLECTION_ENVIRONMENT',\n      data: { collectionPath: collection?.pathname, environmentName }\n    });\n\n    dispatch(_selectEnvironment({ environmentUid, collectionUid }));\n    resolve();\n  });\n};\n\nexport const removeCollection = (collectionUid) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const collection = findCollectionByUid(state.collections.collections, collectionUid);\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n    const { ipcRenderer } = window;\n\n    // Get active workspace to determine which workspace we're removing from\n    const { workspaces } = state;\n    const activeWorkspace = workspaces.workspaces.find((w) => w.uid === workspaces.activeWorkspaceUid);\n\n    let workspaceId = 'default';\n    if (activeWorkspace) {\n      if (activeWorkspace.pathname) {\n        workspaceId = activeWorkspace.pathname;\n      } else {\n        workspaceId = activeWorkspace.uid;\n      }\n    }\n\n    ipcRenderer\n      .invoke('renderer:remove-collection', collection.pathname, collectionUid, workspaceId)\n      .then(() => {\n        // Check if the collection still exists in other workspaces\n        return ipcRenderer.invoke('renderer:get-collection-workspaces', collection.pathname);\n      })\n      .then((remainingWorkspaces) => {\n        // Close tabs for this collection\n        dispatch(closeAllCollectionTabs({ collectionUid }));\n\n        // Remove collection from workspace in Redux state\n        if (activeWorkspace) {\n          dispatch(removeCollectionFromWorkspace({\n            workspaceUid: activeWorkspace.uid,\n            collectionLocation: collection.pathname\n          }));\n        }\n\n        dispatch(ensureActiveTabInCurrentWorkspace());\n\n        // Only remove from Redux if no workspaces remain\n        if (!remainingWorkspaces || remainingWorkspaces.length === 0) {\n          return waitForNextTick().then(() => {\n            dispatch(_removeCollection({\n              collectionUid: collectionUid\n            }));\n          });\n        } else {\n          // Collection still exists in other workspaces\n        }\n      })\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const browseDirectory = () => (dispatch, getState) => {\n  const { ipcRenderer } = window;\n\n  return new Promise((resolve, reject) => {\n    ipcRenderer.invoke('renderer:browse-directory').then(resolve).catch(reject);\n  });\n};\n\nexport const browseFiles = (filters, properties) => (_dispatch, _getState) => {\n  const { ipcRenderer } = window;\n\n  return new Promise((resolve, reject) => {\n    ipcRenderer.invoke('renderer:browse-files', filters, properties).then(resolve).catch(reject);\n  });\n};\n\nexport const saveCollectionSettings = (collectionUid, brunoConfig = null, silent = false) => (dispatch, getState) => {\n  const state = getState();\n  const collection = findCollectionByUid(state.collections.collections, collectionUid);\n\n  return new Promise((resolve, reject) => {\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    const collectionCopy = cloneDeep(collection);\n\n    // Transform collection root (uses draft if exists)\n    const collectionRootToSave = transformCollectionRootToSave(collectionCopy);\n    const { ipcRenderer } = window;\n\n    const savePromises = [];\n\n    // Save collection.bru file\n    savePromises.push(ipcRenderer.invoke('renderer:save-collection-root', collectionCopy.pathname, collectionRootToSave, collectionCopy.brunoConfig));\n\n    // Save bruno.json if brunoConfig is provided or if there's a brunoConfig draft\n    const brunoConfigToSave = brunoConfig || (collectionCopy.draft && collectionCopy.draft.brunoConfig);\n    if (brunoConfigToSave) {\n      savePromises.push(ipcRenderer.invoke('renderer:update-bruno-config', brunoConfigToSave, collectionCopy.pathname, collectionCopy.root));\n    }\n\n    Promise.all(savePromises)\n      .then(() => {\n        if (!silent) {\n          toast.success('Collection Settings saved successfully');\n        }\n        dispatch(saveCollectionDraft({ collectionUid }));\n      })\n      .then(resolve)\n      .catch((err) => {\n        toast.error('Failed to save collection settings!');\n        reject(err);\n      });\n  });\n};\n\nexport const updateBrunoConfig = (brunoConfig, collectionUid) => (dispatch, getState) => {\n  const state = getState();\n\n  const collection = findCollectionByUid(state.collections.collections, collectionUid);\n\n  return new Promise((resolve, reject) => {\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    const { ipcRenderer } = window;\n    ipcRenderer\n      .invoke('renderer:update-bruno-config', brunoConfig, collection.pathname, collection.root)\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\n/**\n * Opens a scratch collection and creates it in Redux state.\n * This is a simplified version of openCollectionEvent for scratch collections,\n * without workspace management, toasts, or sidebar toggles.\n *\n * @param {string} uid - The unique identifier for the scratch collection\n * @param {string} pathname - The filesystem path to the scratch collection\n * @param {Object} brunoConfig - The Bruno configuration object for the collection\n * @returns {Promise} Resolves when the collection is created, rejects on error\n */\nexport const openScratchCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, getState) => {\n  const { ipcRenderer } = window;\n\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const existingCollection = state.collections.collections.find(\n      (c) => normalizePath(c.pathname) === normalizePath(pathname)\n    );\n\n    if (existingCollection) {\n      resolve();\n      return;\n    }\n\n    const collection = {\n      version: '1',\n      uid,\n      name: brunoConfig.name,\n      pathname,\n      items: [],\n      runtimeVariables: {},\n      brunoConfig\n    };\n\n    ipcRenderer\n      .invoke('renderer:get-collection-security-config', pathname)\n      .then((securityConfig) => {\n        collectionSchema\n          .validate(collection)\n          .then(() => dispatch(_createCollection({ ...collection, securityConfig })))\n          .then(resolve)\n          .catch(reject);\n      })\n      .catch(reject);\n  });\n};\n\nexport const openCollectionEvent = (uid, pathname, brunoConfig) => (dispatch, getState) => {\n  const { ipcRenderer } = window;\n\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const activeWorkspace = state.workspaces.workspaces.find((w) => w.uid === state.workspaces.activeWorkspaceUid);\n    const workspaceProcessEnvVariables = activeWorkspace?.processEnvVariables || {};\n\n    const existingCollection = state.collections.collections.find(\n      (c) => normalizePath(c.pathname) === normalizePath(pathname)\n    );\n\n    const isAlreadyInWorkspace = activeWorkspace?.collections?.some(\n      (c) => normalizePath(c.path) === normalizePath(pathname)\n    );\n\n    if (existingCollection && isAlreadyInWorkspace) {\n      toast.success('Collection is already opened');\n      resolve();\n      return;\n    }\n\n    if (existingCollection) {\n      if (state.app.sidebarCollapsed) {\n        dispatch(toggleSidebarCollapse());\n      }\n\n      if (activeWorkspace) {\n        const workspaceCollection = {\n          name: brunoConfig.name,\n          path: pathname\n        };\n\n        ipcRenderer\n          .invoke('renderer:add-collection-to-workspace', activeWorkspace.pathname, workspaceCollection)\n          .then(() => {\n            toast.success('Collection added to workspace');\n          })\n          .catch((err) => {\n            console.error('Failed to add collection to workspace', err);\n            toast.error('Failed to add collection to workspace');\n          });\n      }\n\n      dispatch(workspaceEnvUpdateEvent({ processEnvVariables: workspaceProcessEnvVariables }));\n\n      resolve();\n      return;\n    }\n\n    const collection = {\n      version: '1',\n      uid: uid,\n      name: brunoConfig.name,\n      pathname: pathname,\n      items: [],\n      runtimeVariables: {},\n      workspaceProcessEnvVariables,\n      brunoConfig: brunoConfig\n    };\n\n    ipcRenderer.invoke('renderer:get-collection-security-config', pathname).then((securityConfig) => {\n      collectionSchema\n        .validate(collection)\n        .then(() => dispatch(_createCollection({ ...collection, securityConfig })))\n        .then(() => {\n          const currentState = getState();\n          if (currentState.app.sidebarCollapsed) {\n            dispatch(toggleSidebarCollapse());\n          }\n\n          const currentWorkspace = currentState.workspaces.workspaces.find(\n            (w) => w.uid === currentState.workspaces.activeWorkspaceUid\n          );\n\n          if (currentWorkspace) {\n            ipcRenderer.invoke('renderer:set-collection-workspace', uid, currentWorkspace.pathname);\n\n            const alreadyInWorkspace = currentWorkspace.collections?.some(\n              (c) => normalizePath(c.path) === normalizePath(pathname)\n            );\n\n            if (!alreadyInWorkspace) {\n              const workspaceCollection = {\n                name: brunoConfig.name,\n                path: pathname\n              };\n\n              ipcRenderer\n                .invoke('renderer:add-collection-to-workspace', currentWorkspace.pathname, workspaceCollection)\n                .catch((err) => {\n                  console.error('Failed to add collection to workspace', err);\n                  toast.error('Failed to add collection to workspace');\n                });\n            }\n          }\n\n          resolve();\n        })\n        .catch(reject);\n    });\n  });\n};\n\nexport const createCollection = (collectionName, collectionFolderName, collectionLocation, options = {}) => (dispatch, getState) => {\n  const { ipcRenderer } = window;\n\n  if (!options.workspaceId) {\n    const { workspaces } = getState();\n    const activeWorkspace = workspaces.workspaces.find((w) => w.uid === workspaces.activeWorkspaceUid);\n\n    if (activeWorkspace && activeWorkspace.pathname) {\n      options.workspaceId = activeWorkspace.pathname;\n    } else {\n      options.workspaceId = 'default';\n    }\n  }\n\n  return new Promise((resolve, reject) => {\n    ipcRenderer\n      .invoke('renderer:create-collection', collectionName, collectionFolderName, collectionLocation, options)\n      .then(resolve)\n      .catch(reject);\n  });\n};\nexport const cloneCollection = (collectionName, collectionFolderName, collectionLocation, previousPath) => () => {\n  const { ipcRenderer } = window;\n\n  return ipcRenderer.invoke(\n    'renderer:clone-collection',\n    collectionName,\n    collectionFolderName,\n    collectionLocation,\n    previousPath\n  );\n};\nexport const openCollection = (options = {}) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n\n    const state = getState();\n    const activeWorkspace = state.workspaces.workspaces.find((w) => w.uid === state.workspaces.activeWorkspaceUid);\n\n    if (!options.workspaceId) {\n      options.workspaceId = activeWorkspace?.pathname || 'default';\n    }\n\n    ipcRenderer.invoke('renderer:open-collection', options)\n      .then((result) => {\n        resolve(result);\n      })\n      .catch(reject);\n  });\n};\n\nexport const openMultipleCollections = (collectionPaths, options = {}) => () => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n\n    ipcRenderer.invoke('renderer:open-multiple-collections', collectionPaths, options)\n      .then(resolve)\n      .catch((err) => {\n        reject();\n      });\n  });\n};\n\nexport const collectionAddEnvFileEvent = (payload) => (dispatch, getState) => {\n  const { data: environment, meta } = payload;\n\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const collection = findCollectionByUid(state.collections.collections, meta.collectionUid);\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    environmentSchema\n      .validate(environment)\n      .then(() =>\n        dispatch(\n          _collectionAddEnvFileEvent({\n            environment,\n            collectionUid: meta.collectionUid\n          })\n        )\n      )\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const importCollection = (collection, collectionLocation, options = {}) => (dispatch, getState) => {\n  return new Promise(async (resolve, reject) => {\n    const { ipcRenderer } = window;\n\n    try {\n      const state = getState();\n      const activeWorkspace = state.workspaces.workspaces.find((w) => w.uid === state.workspaces.activeWorkspaceUid);\n      const isMultiple = Array.isArray(collection);\n\n      const result = await ipcRenderer.invoke('renderer:import-collection', collection, collectionLocation, {\n        format: options.format || DEFAULT_COLLECTION_FORMAT,\n        rawOpenAPISpec: options.rawOpenAPISpec\n      });\n      const importedPaths = result.success.items;\n\n      if (importedPaths.length > 0 && activeWorkspace && activeWorkspace.pathname && activeWorkspace.type !== 'default') {\n        for (const importedItem of importedPaths) {\n          const workspaceCollection = {\n            name: importedItem.name,\n            path: importedItem.path\n          };\n          await ipcRenderer.invoke('renderer:add-collection-to-workspace', activeWorkspace.pathname, workspaceCollection);\n        }\n      }\n\n      resolve(isMultiple ? importedPaths : importedPaths[0]);\n    } catch (error) {\n      reject(error);\n    }\n  });\n};\n\nexport const importCollectionFromZip = (zipFilePath, collectionLocation) => async (dispatch, getState) => {\n  const { ipcRenderer } = window;\n  const state = getState();\n  const activeWorkspace = state.workspaces.workspaces.find((w) => w.uid === state.workspaces.activeWorkspaceUid);\n\n  const collectionPath = await ipcRenderer.invoke('renderer:import-collection-zip', zipFilePath, collectionLocation);\n\n  if (activeWorkspace && activeWorkspace.pathname && activeWorkspace.type !== 'default') {\n    const collectionName = path.basename(collectionPath);\n    await ipcRenderer.invoke('renderer:add-collection-to-workspace', activeWorkspace.pathname, {\n      name: collectionName,\n      path: collectionPath\n    });\n  }\n\n  return collectionPath;\n};\n\n/**\n * Updates Redux collection order and persists it to the active workspace's workspace.yml.\n */\nexport const moveCollectionAndPersist\n  = ({ draggedItem, targetItem }) =>\n    (dispatch, getState) => {\n      const state = getState();\n      const activeWorkspace = state.workspaces.workspaces.find(\n        (w) => w.uid === state.workspaces.activeWorkspaceUid\n      );\n      if (!activeWorkspace?.pathname || !activeWorkspace.collections?.length) {\n        return Promise.resolve();\n      }\n\n      const workspacePathSet = new Set(\n        activeWorkspace.collections.map((wc) => normalizePath(wc.path))\n      );\n      const collectionsInWorkspace = state.collections.collections\n        .filter((c) => workspacePathSet.has(normalizePath(c.pathname)));\n      if (collectionsInWorkspace.length === 0) {\n        return Promise.resolve();\n      }\n\n      const reordered = collectionsInWorkspace.filter((i) => i.uid !== draggedItem.uid);\n      const targetIndex = reordered.findIndex((i) => i.uid === targetItem.uid);\n      reordered.splice(targetIndex, 0, draggedItem);\n      const collectionPaths = reordered.map((c) => c.pathname);\n\n      return window.ipcRenderer\n        .invoke('renderer:reorder-workspace-collections', activeWorkspace.pathname, collectionPaths)\n        .then(() => {\n          dispatch(moveCollection({ draggedItem, targetItem }));\n        })\n        .catch((err) => {\n          console.error('Failed to reorder workspace collections', err);\n          return Promise.reject(err);\n        });\n    };\n\nexport const saveCollectionSecurityConfig = (collectionUid, securityConfig) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    const state = getState();\n    const collection = findCollectionByUid(state.collections.collections, collectionUid);\n\n    ipcRenderer\n      .invoke('renderer:save-collection-security-config', collection?.pathname, securityConfig)\n      .then(async () => {\n        await dispatch(setCollectionSecurityConfig({ collectionUid, securityConfig }));\n        resolve();\n      })\n      .catch(reject);\n  });\n};\n\nexport const hydrateCollectionWithUiStateSnapshot = (payload) => (dispatch, getState) => {\n  const collectionSnapshotData = payload;\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    try {\n      if (!collectionSnapshotData) {\n        resolve();\n        return;\n      }\n      const { pathname, selectedEnvironment } = collectionSnapshotData;\n      const collection = findCollectionByPathname(state.collections.collections, pathname);\n      const collectionCopy = cloneDeep(collection);\n      const collectionUid = collectionCopy?.uid;\n\n      // update selected environment\n      if (selectedEnvironment) {\n        const environment = findEnvironmentInCollectionByName(collectionCopy, selectedEnvironment);\n        if (environment) {\n          dispatch(_selectEnvironment({ environmentUid: environment?.uid, collectionUid }));\n        }\n      }\n\n      // todo: add any other redux state that you want to save\n\n      resolve();\n    } catch (error) {\n      reject(error);\n    }\n  });\n};\n\nexport const fetchOauth2Credentials = (payload) => async (dispatch, getState) => {\n  const { request, collection, itemUid, folderUid } = payload;\n  const state = getState();\n  const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;\n  const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });\n  request.globalEnvironmentVariables = globalEnvironmentVariables;\n  return new Promise((resolve, reject) => {\n    window.ipcRenderer\n      .invoke('renderer:fetch-oauth2-credentials', { itemUid, request, collection })\n      .then(({ credentials, url, collectionUid, credentialsId, debugInfo }) => {\n        dispatch(\n          collectionAddOauth2CredentialsByUrl({\n            credentials,\n            url,\n            collectionUid,\n            credentialsId,\n            debugInfo: safeParseJSON(safeStringifyJSON(debugInfo)),\n            folderUid: folderUid || null,\n            itemUid: !folderUid ? itemUid : null\n          })\n        );\n        resolve(credentials);\n      })\n      .catch(reject);\n  });\n};\n\nexport const refreshOauth2Credentials = (payload) => async (dispatch, getState) => {\n  const { request, collection, folderUid, itemUid } = payload;\n  const state = getState();\n  const { globalEnvironments, activeGlobalEnvironmentUid } = state.globalEnvironments;\n  const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });\n  request.globalEnvironmentVariables = globalEnvironmentVariables;\n  return new Promise((resolve, reject) => {\n    window.ipcRenderer\n      .invoke('renderer:refresh-oauth2-credentials', { itemUid, request, collection })\n      .then(({ credentials, url, collectionUid, debugInfo, credentialsId }) => {\n        dispatch(\n          collectionAddOauth2CredentialsByUrl({\n            credentials,\n            url,\n            collectionUid,\n            credentialsId,\n            debugInfo: safeParseJSON(safeStringifyJSON(debugInfo)),\n            folderUid: folderUid || null,\n            itemUid: !folderUid ? itemUid : null\n          })\n        );\n        resolve(credentials);\n      })\n      .catch(reject);\n  });\n};\n\nexport const clearOauth2Cache = (payload) => async (dispatch, getState) => {\n  const { collectionUid, url, credentialsId } = payload;\n  return new Promise((resolve, reject) => {\n    window.ipcRenderer\n      .invoke('clear-oauth2-cache', collectionUid, url, credentialsId)\n      .then(() => {\n        dispatch(\n          collectionClearOauth2CredentialsByUrlAndCredentialsId({\n            url,\n            collectionUid,\n            credentialsId\n          })\n        );\n        resolve();\n      })\n      .catch(reject);\n  });\n};\n\nexport const isOauth2AuthorizationRequestInProgress = () => async () => {\n  return new Promise((resolve, reject) => {\n    window.ipcRenderer\n      .invoke('renderer:is-oauth2-authorization-request-in-progress')\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const cancelOauth2AuthorizationRequest = () => async () => {\n  return new Promise((resolve, reject) => {\n    window.ipcRenderer\n      .invoke('renderer:cancel-oauth2-authorization-request')\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\n// todo: could be removed\nexport const loadRequestViaWorker\n  = ({ collectionUid, pathname }) =>\n    (dispatch, getState) => {\n      return new Promise(async (resolve, reject) => {\n        const { ipcRenderer } = window;\n        ipcRenderer.invoke('renderer:load-request-via-worker', { collectionUid, pathname }).then(resolve).catch(reject);\n      });\n    };\n\n// todo: could be removed\nexport const loadRequest\n  = ({ collectionUid, pathname }) =>\n    (dispatch, getState) => {\n      return new Promise(async (resolve, reject) => {\n        const { ipcRenderer } = window;\n        ipcRenderer.invoke('renderer:load-request', { collectionUid, pathname }).then(resolve).catch(reject);\n      });\n    };\n\nexport const loadLargeRequest\n  = ({ collectionUid, pathname }) =>\n    (dispatch, getState) => {\n      return new Promise(async (resolve, reject) => {\n        const { ipcRenderer } = window;\n        ipcRenderer.invoke('renderer:load-large-request', { collectionUid, pathname }).then(resolve).catch(reject);\n      });\n    };\n\nexport const mountCollection\n  = ({ collectionUid, collectionPathname, brunoConfig }) =>\n    (dispatch, getState) => {\n      dispatch(updateCollectionMountStatus({ collectionUid, mountStatus: 'mounting' }));\n      return new Promise(async (resolve, reject) => {\n        callIpc('renderer:mount-collection', { collectionUid, collectionPathname, brunoConfig })\n          .then((transientDirPath) => {\n            dispatch(updateCollectionMountStatus({ collectionUid, mountStatus: 'mounted' }));\n            dispatch(addTransientDirectory({ collectionUid, pathname: transientDirPath }));\n          })\n          .then(resolve)\n          .catch(() => {\n            dispatch(updateCollectionMountStatus({ collectionUid, mountStatus: 'unmounted' }));\n            reject();\n          });\n      });\n    };\n\nexport const showInFolder = (collectionPath) => () => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    ipcRenderer.invoke('renderer:show-in-folder', collectionPath).then(resolve).catch(reject);\n  });\n};\n\nexport const updateRunnerConfiguration\n  = (collectionUid, selectedRequestItems, requestItemsOrder, delay) => (dispatch) => {\n    dispatch(\n      _updateRunnerConfiguration({\n        collectionUid,\n        selectedRequestItems,\n        requestItemsOrder,\n        delay\n      })\n    );\n  };\n\nexport const updateActiveConnectionsInStore = (activeConnectionIds) => (dispatch, getState) => {\n  dispatch(updateActiveConnections(activeConnectionIds));\n};\n\nexport const openCollectionSettings\n  = (collectionUid, tabName = 'overview') =>\n    (dispatch, getState) => {\n      const state = getState();\n      const collection = findCollectionByUid(state.collections.collections, collectionUid);\n\n      return new Promise((resolve, reject) => {\n        if (!collection) {\n          return reject(new Error('Collection not found'));\n        }\n\n        dispatch(updateSettingsSelectedTab({\n          collectionUid: collection.uid,\n          tab: tabName\n        }));\n\n        dispatch(addTab({\n          uid: collection.uid,\n          collectionUid: collection.uid,\n          type: 'collection-settings'\n        }));\n\n        resolve();\n      });\n    };\n\nexport const saveDotEnvVariables = (collectionUid, variables, filename = '.env') => (dispatch, getState) => {\n  const { ipcRenderer } = window;\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const collection = findCollectionByUid(state.collections.collections, collectionUid);\n\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    ipcRenderer\n      .invoke('renderer:save-dotenv-variables', collection.pathname, variables, filename)\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const saveDotEnvRaw = (collectionUid, content, filename = '.env') => (dispatch, getState) => {\n  const { ipcRenderer } = window;\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const collection = findCollectionByUid(state.collections.collections, collectionUid);\n\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    ipcRenderer\n      .invoke('renderer:save-dotenv-raw', collection.pathname, content, filename)\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const createDotEnvFile = (collectionUid, filename = '.env') => (dispatch, getState) => {\n  const { ipcRenderer } = window;\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const collection = findCollectionByUid(state.collections.collections, collectionUid);\n\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    ipcRenderer\n      .invoke('renderer:create-dotenv-file', collection.pathname, filename)\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const deleteDotEnvFile = (collectionUid, filename = '.env') => (dispatch, getState) => {\n  const { ipcRenderer } = window;\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const collection = findCollectionByUid(state.collections.collections, collectionUid);\n\n    if (!collection) {\n      return reject(new Error('Collection not found'));\n    }\n\n    ipcRenderer\n      .invoke('renderer:delete-dotenv-file', collection.pathname, filename)\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const cloneGitRepository = (data) => (dispatch, getState) => {\n  const { ipcRenderer } = window;\n  return new Promise((resolve, reject) => {\n    ipcRenderer\n      .invoke('renderer:clone-git-repository', data)\n      .then((res) => {\n        console.log('clone done', res);\n      })\n      .then(resolve)\n      .catch((err) => {\n        toast.custom(<IpcErrorModal error={err?.message} />);\n        reject();\n      });\n  });\n};\n\nexport const scanForBrunoFiles = (dir) => (dispatch, getState) => {\n  const { ipcRenderer } = window;\n  return new Promise((resolve, reject) => {\n    ipcRenderer\n      .invoke('renderer:scan-for-bruno-files', dir)\n      .then(resolve)\n      .catch((err) => {\n        reject();\n      });\n  });\n};\n\n/**\n * If the current active tab belongs to another workspace, focus a tab in the current workspace.\n */\nexport const ensureActiveTabInCurrentWorkspace = () => (dispatch, getState) => {\n  const state = getState();\n  const result = getTabToFocusForCurrentWorkspace(state);\n  if (!result) {\n    return; // Already in workspace, no active workspace, or unfixable (no workspace tabs and no scratch).\n  }\n  if (result.addOverviewFirst && result.scratchCollectionUid) {\n    dispatch(addTab({\n      uid: result.uid,\n      collectionUid: result.scratchCollectionUid,\n      type: 'workspaceOverview'\n    }));\n  }\n  dispatch(focusTab({ uid: result.uid }));\n};\n\n/**\n * Close tabs and delete any transient request files from the filesystem.\n * This thunk wraps the closeTabs reducer to handle transient file cleanup automatically.\n */\nexport const closeTabs = ({ tabUids }) => async (dispatch, getState) => {\n  const { ipcRenderer } = window;\n  const state = getState();\n  const collections = state.collections.collections;\n  const tempDirectories = state.collections.tempDirectories || {};\n\n  // Find transient items and group by temp directory before closing tabs\n  const transientByTempDir = {};\n  each(tabUids, (tabUid) => {\n    for (const collection of collections) {\n      const item = findItemInCollection(collection, tabUid);\n      if (item?.isTransient && item.pathname) {\n        const tempDir = tempDirectories[collection.uid];\n        if (tempDir) {\n          if (!transientByTempDir[tempDir]) {\n            transientByTempDir[tempDir] = [];\n          }\n          transientByTempDir[tempDir].push(item.pathname);\n        }\n        break;\n      }\n    }\n  });\n\n  // Close the tabs first\n  await dispatch(_closeTabs({ tabUids }));\n\n  // After close, the reducer may have set active tab to one from another workspace. Ensure it belongs to this workspace: prefer any open in-workspace tab, then workspace overview if none.\n  // Dispatch is synchronous; state is already updated by _closeTabs above.\n  await dispatch(ensureActiveTabInCurrentWorkspace());\n\n  // Delete transient files after tabs are closed\n  for (const [tempDir, filePaths] of Object.entries(transientByTempDir)) {\n    try {\n      const results = await ipcRenderer.invoke('renderer:delete-transient-requests', filePaths, tempDir);\n      if (results.errors?.length > 0) {\n        console.error('Errors deleting transient files:', results.errors);\n      }\n    } catch (err) {\n      console.error('Failed to delete transient request files:', err);\n    }\n  }\n};\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/slices/collections/exampleReducers.js",
    "content": "import { find, map, filter, cloneDeep, each, concat } from 'lodash';\nimport { parseQueryParams, buildQueryString as stringifyQueryParams } from '@usebruno/common/utils';\nimport { uuid } from 'utils/common';\nimport { findCollectionByUid, findItemInCollection } from 'utils/collections';\nimport { parsePathParams, splitOnFirst, interpolateUrlPathParams } from 'utils/url';\nimport statusCodePhraseMap from 'components/ResponsePane/StatusCode/get-status-code-phrase';\n\nexport const addResponseExample = (state, action) => {\n  const { itemUid, collectionUid, example } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n  if (!item.draft.examples) {\n    item.draft.examples = [];\n  }\n\n  // Ensure body always has a mode field (default to 'none' if not present)\n  const requestBody = item.draft.request.body || {};\n  if (!requestBody.mode) {\n    requestBody.mode = 'none';\n  }\n\n  const newExample = {\n    uid: example.uid || uuid(),\n    itemUid: item.uid,\n    name: example.name,\n    description: example.description,\n    type: item.draft.type,\n    request: {\n      url: item.draft.request.url,\n      method: item.draft.request.method,\n      headers: item.draft.request.headers,\n      params: item.draft.request.params,\n      body: requestBody\n    },\n    response: {\n      status: example.status ? Number(example.status) : null,\n      statusText: String(example.statusText ?? (example.status ? (statusCodePhraseMap[Number(example.status)] ?? '') : '')),\n      headers: (example.headers || []).map((header) => ({\n        uid: uuid(),\n        name: String(header.name),\n        value: String(header.value),\n        description: String(header.description),\n        enabled: header.enabled\n      })),\n      body: example.body\n    }\n  };\n\n  item.draft.examples.push(newExample);\n};\n\nexport const cloneResponseExample = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, clonedUid } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const originalExample = item.draft.examples.find((e) => e.uid === exampleUid);\n\n  if (!originalExample) return;\n\n  const clonedExample = cloneDeep(originalExample);\n\n  clonedExample.uid = clonedUid || uuid();\n\n  clonedExample.name = `${originalExample.name} (Copy)`;\n\n  if (clonedExample.request && clonedExample.request.body) {\n    if (!clonedExample.request.body.mode) {\n      clonedExample.request.body.mode = 'none';\n    }\n  }\n\n  if (clonedExample.request && clonedExample.request.headers) {\n    clonedExample.request.headers = clonedExample.request.headers.map((header) => ({\n      ...header,\n      uid: uuid()\n    }));\n  }\n\n  if (clonedExample.response && clonedExample.response.headers) {\n    clonedExample.response.headers = clonedExample.response.headers.map((header) => ({\n      ...header,\n      uid: uuid()\n    }));\n  }\n\n  if (clonedExample.request && clonedExample.request.params) {\n    clonedExample.request.params = clonedExample.request.params.map((param) => ({\n      ...param,\n      uid: uuid()\n    }));\n  }\n\n  if (clonedExample.request && clonedExample.request.body) {\n    if (clonedExample.request.body.multipartForm) {\n      clonedExample.request.body.multipartForm = clonedExample.request.body.multipartForm.map((param) => ({\n        ...param,\n        uid: uuid()\n      }));\n    }\n    if (clonedExample.request.body.formUrlEncoded) {\n      clonedExample.request.body.formUrlEncoded = clonedExample.request.body.formUrlEncoded.map((param) => ({\n        ...param,\n        uid: uuid()\n      }));\n    }\n    if (clonedExample.request.body.file) {\n      clonedExample.request.body.file = clonedExample.request.body.file.map((param) => ({\n        ...param,\n        uid: uuid()\n      }));\n    }\n  }\n\n  item.draft.examples.push(clonedExample);\n};\n\nexport const updateResponseExample = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, example: details } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (item.draft.examples.length === 0) return;\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n\n  item.draft.examples = item.draft.examples.map((e) =>\n    e.uid === exampleUid ? { ...e, ...details } : e);\n};\n\nexport const deleteResponseExample = (state, action) => {\n  const { itemUid, collectionUid, exampleUid } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) return;\n\n  item.draft.examples = item.draft.examples.filter((e) => e.uid !== exampleUid);\n};\n\nexport const cancelResponseExampleEdit = (state, action) => {\n  const { itemUid, collectionUid, exampleUid } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n  if (!item.draft) return;\n  if (!item.draft.examples) return;\n  if (!item.examples) return;\n\n  const originalExample = item.examples.find((e) => e.uid === exampleUid);\n  if (!originalExample) return;\n\n  // Replace the draft example with the original example\n  const exampleIndex = item.draft.examples.findIndex((e) => e.uid === exampleUid);\n  if (exampleIndex === -1) return;\n\n  item.draft.examples[exampleIndex] = cloneDeep(originalExample);\n};\n\n// Response Example Headers\nexport const addResponseExampleHeader = (state, action) => {\n  const { itemUid, collectionUid, exampleUid } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n\n  example.response.headers = example.response.headers || [];\n  example.response.headers.push({\n    uid: uuid(),\n    name: '',\n    value: '',\n    description: '',\n    enabled: true\n  });\n};\n\nexport const updateResponseExampleHeader = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, header } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n  if (!example.response.headers) return;\n\n  const headerToUpdate = find(example.response.headers, (h) => h.uid === header.uid);\n  if (!headerToUpdate) return;\n\n  headerToUpdate.name = header.name;\n  headerToUpdate.value = header.value;\n  headerToUpdate.description = header.description;\n  headerToUpdate.enabled = header.enabled;\n};\n\nexport const deleteResponseExampleHeader = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, headerUid } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n  if (!example.response.headers) return;\n\n  example.response.headers = filter(example.response.headers, (h) => h.uid !== headerUid);\n};\n\nexport const moveResponseExampleHeader = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, updateReorderedItem } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n  if (!example.response) return;\n  if (!example.response.headers) return;\n\n  example.response.headers = updateReorderedItem.map((uid) => {\n    return example.response.headers.find((h) => h.uid === uid);\n  });\n};\n\nexport const setResponseExampleHeaders = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, headers } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n\n  example.response.headers = map(headers, ({ uid, name = '', value = '', enabled = true }) => ({\n    uid: uid || uuid(),\n    name: name,\n    value: value,\n    description: '',\n    enabled: enabled\n  }));\n};\n\n// Response Example Params\nexport const addResponseExampleParam = (state, action) => {\n  const { itemUid, collectionUid, exampleUid } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n\n  example.request.params = example.request.params || [];\n  example.request.params.push({\n    uid: uuid(),\n    name: '',\n    value: '',\n    description: '',\n    type: 'query',\n    enabled: true\n  });\n};\n\nexport const updateResponseExampleParam = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, param } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n  if (!example.request.params) return;\n\n  const paramToUpdate = find(example.request.params, (p) => p.uid === param.uid);\n  if (!paramToUpdate) return;\n\n  paramToUpdate.name = param.name;\n  paramToUpdate.value = param.value;\n  paramToUpdate.description = param.description;\n  paramToUpdate.enabled = param.enabled;\n\n  if (paramToUpdate.type === 'query') {\n    const parts = splitOnFirst(example.request.url, '?');\n    const query = stringifyQueryParams(filter(example.request.params, (p) => p.enabled && p.type === 'query'));\n\n    if (!query || !query.length) {\n      if (parts.length) {\n        example.request.url = parts[0];\n      }\n    } else {\n      if (!parts.length) {\n        example.request.url += '?' + query;\n      } else {\n        example.request.url = parts[0] + '?' + query;\n      }\n    }\n  }\n};\n\nexport const deleteResponseExampleParam = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, paramUid } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n  if (!example.request.params) return;\n\n  const paramToDelete = find(example.request.params, (p) => p.uid === paramUid);\n  example.request.params = filter(example.request.params, (p) => p.uid !== paramUid);\n\n  if (paramToDelete && paramToDelete.type === 'query') {\n    const parts = splitOnFirst(example.request.url, '?');\n    const query = stringifyQueryParams(filter(example.request.params, (p) => p.enabled && p.type === 'query'));\n\n    if (!query || !query.length) {\n      if (parts.length) {\n        example.request.url = parts[0];\n      }\n    } else {\n      if (!parts.length) {\n        example.request.url += '?' + query;\n      } else {\n        example.request.url = parts[0] + '?' + query;\n      }\n    }\n  }\n};\n\nexport const moveResponseExampleParam = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, updateReorderedItem } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n  if (!example.request.params) return;\n\n  example.request.params = updateReorderedItem.map((uid) => {\n    return example.request.params.find((p) => p.uid === uid);\n  });\n};\n\n// Response Example Request/Response Updates\nexport const updateResponseExampleMultipartFormParams = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, params } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n\n  if (!example.request) {\n    example.request = {};\n  }\n  if (!example.request.body) {\n    example.request.body = { mode: 'multipartForm', multipartForm: [] };\n  }\n\n  // Ensure all params have unique UIDs\n  const paramsWithUids = params.map((param, index) => ({\n    ...param,\n    uid: param.uid || uuid()\n  }));\n\n  example.request.body.multipartForm = paramsWithUids;\n};\n\nexport const updateResponseExampleFileBodyParams = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, params } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n\n  if (!example.request) {\n    example.request = {};\n  }\n  if (!example.request.body) {\n    example.request.body = { mode: 'file', file: [] };\n  }\n\n  // Ensure all params have unique UIDs\n  const paramsWithUids = params.map((param, index) => ({\n    ...param,\n    uid: param.uid || uuid()\n  }));\n\n  example.request.body.file = paramsWithUids;\n};\n\nexport const updateResponseExampleFormUrlEncodedParams = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, params } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n\n  if (!example.request) {\n    example.request = {};\n  }\n  if (!example.request.body) {\n    example.request.body = { mode: 'formUrlEncoded', formUrlEncoded: [] };\n  }\n\n  // Ensure all params have unique UIDs\n  const paramsWithUids = params.map((param, index) => ({\n    ...param,\n    uid: param.uid || uuid()\n  }));\n\n  example.request.body.formUrlEncoded = paramsWithUids;\n};\n\nexport const updateResponseExampleRequest = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, request } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n\n  // Ensure body always has a mode field if it's being updated\n  if (request.body) {\n    if (!request.body.mode) {\n      request.body.mode = 'none';\n    }\n  }\n\n  example.request = { ...example.request, ...request };\n};\n\nexport const updateResponseExampleRequestUrl = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, request } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n\n  if (!example.request) {\n    example.request = {};\n  }\n\n  example.request.url = request.url;\n\n  const parts = splitOnFirst(example.request.url, '?');\n  const urlQueryParams = parseQueryParams(parts[1]);\n  let urlPathParams = [];\n\n  try {\n    urlPathParams = parsePathParams(parts[0]);\n  } catch (err) {\n    console.error(err);\n  }\n\n  const existingParams = example.request.params || [];\n  const disabledQueryParams = filter(existingParams, (p) => !p.enabled && p.type === 'query');\n  let enabledQueryParams = filter(existingParams, (p) => p.enabled && p.type === 'query');\n  let oldPathParams = filter(existingParams, (p) => p.enabled && p.type === 'path');\n  let newPathParams = [];\n\n  each(urlQueryParams, (urlQueryParam) => {\n    const existingQueryParam = find(enabledQueryParams, (p) => p?.name === urlQueryParam?.name || p?.value === urlQueryParam?.value);\n    urlQueryParam.uid = existingQueryParam?.uid || uuid();\n    urlQueryParam.enabled = true;\n    urlQueryParam.type = 'query';\n\n    if (existingQueryParam) {\n      enabledQueryParams = filter(enabledQueryParams, (p) => p?.uid !== existingQueryParam?.uid);\n    }\n  });\n\n  newPathParams = filter(urlPathParams, (urlPath) => {\n    const existingPathParam = find(oldPathParams, (p) => p.name === urlPath.name);\n    if (existingPathParam) {\n      // Preserve existing path parameter values\n      urlPath.value = existingPathParam.value;\n      return false;\n    }\n    urlPath.uid = uuid();\n    urlPath.enabled = true;\n    urlPath.type = 'path';\n    return true;\n  });\n\n  oldPathParams = filter(oldPathParams, (urlPath) => {\n    return find(urlPathParams, (p) => p.name === urlPath.name);\n  });\n\n  example.request.params = concat(urlQueryParams, newPathParams, disabledQueryParams, oldPathParams);\n};\n\nexport const updateResponseExampleResponse = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, response } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n\n  // Ensure status is a number if provided\n  const processedResponse = { ...response };\n  if (processedResponse.status !== undefined) {\n    processedResponse.status = processedResponse.status ? Number(processedResponse.status) : null;\n  }\n\n  example.response = { ...example.response, ...processedResponse };\n};\n\nexport const updateResponseExampleDetails = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, details } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n\n  example.name = details.name || example.name;\n  example.description = details.description || example.description;\n};\n\nexport const updateResponseExampleName = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, name } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n\n  example.name = name;\n};\n\nexport const updateResponseExampleDescription = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, description } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n\n  example.description = description;\n};\n\n// Response Example Request Headers\nexport const addResponseExampleRequestHeader = (state, action) => {\n  const { itemUid, collectionUid, exampleUid } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n\n  example.request.headers = example.request.headers || [];\n  example.request.headers.push({\n    uid: uuid(),\n    name: '',\n    value: '',\n    description: '',\n    enabled: true\n  });\n};\n\nexport const updateResponseExampleRequestHeader = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, header } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n  if (!example.request.headers) return;\n\n  const headerToUpdate = find(example.request.headers, (h) => h.uid === header.uid);\n  if (!headerToUpdate) return;\n\n  headerToUpdate.name = header.name;\n  headerToUpdate.value = header.value;\n  headerToUpdate.description = header.description;\n  headerToUpdate.enabled = header.enabled;\n};\n\nexport const deleteResponseExampleRequestHeader = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, headerUid } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n  if (!example.request.headers) return;\n\n  example.request.headers = filter(example.request.headers, (h) => h.uid !== headerUid);\n};\n\nexport const moveResponseExampleRequestHeader = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, updateReorderedItem } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n  if (!example.request.headers) return;\n\n  example.request.headers = updateReorderedItem.map((uid) => {\n    return example.request.headers.find((h) => h.uid === uid);\n  });\n};\n\nexport const setResponseExampleRequestHeaders = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, headers } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n\n  example.request.headers = map(headers, ({ uid, name = '', value = '', enabled = true }) => ({\n    uid: uid || uuid(),\n    name: name,\n    value: value,\n    description: '',\n    enabled: enabled\n  }));\n};\n\nexport const setResponseExampleParams = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, params } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n\n  example.request.params = map(params, ({ uid, name = '', value = '', enabled = true, type = 'query' }) => ({\n    uid: uid || uuid(),\n    name: name,\n    value: value,\n    description: '',\n    enabled: enabled,\n    type: type\n  }));\n\n  // Update URL when query parameters change\n  const queryParams = filter(example.request.params, (p) => p.enabled && p.type === 'query');\n  const query = stringifyQueryParams(queryParams);\n\n  if (!example.request.url) {\n    example.request.url = '';\n  }\n\n  const parts = splitOnFirst(example.request.url, '?');\n\n  if (!query || !query.length) {\n    if (parts.length) {\n      example.request.url = parts[0];\n    }\n  } else {\n    if (!parts.length) {\n      example.request.url += '?' + query;\n    } else {\n      example.request.url = parts[0] + '?' + query;\n    }\n  }\n};\n\n// Response Example Body Types\nexport const updateResponseExampleBody = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, body } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n\n  // Ensure body always has a mode field\n  if (!body.mode && !example.request.body?.mode) {\n    body.mode = 'none';\n  }\n\n  example.request.body = { ...example.request.body, ...body };\n};\n\n// Response Example File Body\nexport const addResponseExampleFileParam = (state, action) => {\n  const { itemUid, collectionUid, exampleUid } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n\n  if (!example.request.body) {\n    example.request.body = { mode: 'file', file: [] };\n  }\n  if (!example.request.body.file) {\n    example.request.body.file = [];\n  }\n\n  example.request.body.file.push({\n    uid: uuid(),\n    name: '',\n    value: '',\n    description: '',\n    enabled: true\n  });\n};\n\nexport const updateResponseExampleFileParam = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, param } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n  if (!example.request.body) return;\n  if (!example.request.body.file) return;\n\n  const paramToUpdate = find(example.request.body.file, (p) => p.uid === param.uid);\n  if (!paramToUpdate) return;\n\n  paramToUpdate.name = param.name;\n  paramToUpdate.value = param.value;\n  paramToUpdate.description = param.description;\n  paramToUpdate.enabled = param.enabled;\n};\n\nexport const deleteResponseExampleFileParam = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, paramUid } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n  if (!example.request.body) return;\n  if (!example.request.body.file) return;\n\n  example.request.body.file = filter(example.request.body.file, (p) => p.uid !== paramUid);\n};\n\n// Response Example Form URL Encoded Body\nexport const addResponseExampleFormUrlEncodedParam = (state, action) => {\n  const { itemUid, collectionUid, exampleUid } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n\n  if (!example.request.body) {\n    example.request.body = { mode: 'formUrlEncoded', formUrlEncoded: [] };\n  }\n  if (!example.request.body.formUrlEncoded) {\n    example.request.body.formUrlEncoded = [];\n  }\n\n  example.request.body.formUrlEncoded.push({\n    uid: uuid(),\n    name: '',\n    value: '',\n    description: '',\n    enabled: true\n  });\n};\n\nexport const updateResponseExampleFormUrlEncodedParam = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, param } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n  if (!example.request.body) return;\n  if (!example.request.body.formUrlEncoded) return;\n\n  const paramToUpdate = find(example.request.body.formUrlEncoded, (p) => p.uid === param.uid);\n  if (!paramToUpdate) return;\n\n  paramToUpdate.name = param.name;\n  paramToUpdate.value = param.value;\n  paramToUpdate.description = param.description;\n  paramToUpdate.enabled = param.enabled;\n};\n\nexport const deleteResponseExampleFormUrlEncodedParam = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, paramUid } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n  if (!example.request.body) return;\n  if (!example.request.body.formUrlEncoded) return;\n\n  example.request.body.formUrlEncoded = filter(example.request.body.formUrlEncoded, (p) => p.uid !== paramUid);\n};\n\n// Response Example Multipart Form Body\nexport const addResponseExampleMultipartFormParam = (state, action) => {\n  const { itemUid, collectionUid, exampleUid } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n\n  if (!example.request.body) {\n    example.request.body = { mode: 'multipartForm', multipartForm: [] };\n  }\n  if (!example.request.body.multipartForm) {\n    example.request.body.multipartForm = [];\n  }\n\n  example.request.body.multipartForm.push({\n    uid: uuid(),\n    name: '',\n    value: '',\n    description: '',\n    enabled: true,\n    type: 'text',\n    contentType: ''\n  });\n};\n\nexport const updateResponseExampleMultipartFormParam = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, param } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n  if (!example.request.body) return;\n  if (!example.request.body.multipartForm) return;\n\n  const paramToUpdate = find(example.request.body.multipartForm, (p) => p.uid === param.uid);\n  if (!paramToUpdate) return;\n\n  paramToUpdate.name = param.name;\n  paramToUpdate.value = param.value;\n  paramToUpdate.description = param.description;\n  paramToUpdate.enabled = param.enabled;\n  paramToUpdate.type = param.type;\n  paramToUpdate.contentType = param.contentType;\n};\n\nexport const deleteResponseExampleMultipartFormParam = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, paramUid } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n  if (!example.request.body) return;\n  if (!example.request.body.multipartForm) return;\n\n  example.request.body.multipartForm = filter(example.request.body.multipartForm, (p) => p.uid !== paramUid);\n};\n\n// Response Status Code and Status Text Reducers\nexport const updateResponseExampleStatusCode = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, statusCode } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n\n  if (!example.response) {\n    example.response = {};\n  }\n\n  example.response.status = statusCode ? Number(statusCode) : null;\n};\n\nexport const updateResponseExampleStatusText = (state, action) => {\n  const { itemUid, collectionUid, exampleUid, statusText } = action.payload;\n  const collection = findCollectionByUid(state.collections, collectionUid);\n\n  if (!collection) return;\n\n  const item = findItemInCollection(collection, itemUid);\n  if (!item) return;\n\n  if (!item.draft) {\n    item.draft = cloneDeep(item);\n  }\n\n  if (!item.draft.examples) {\n    item.draft.examples = item.examples ? cloneDeep(item.examples) : [];\n  }\n\n  const example = item.draft.examples.find((e) => e.uid === exampleUid);\n  if (!example) return;\n\n  if (!example.response) {\n    example.response = {};\n  }\n\n  example.response.statusText = String(statusText ?? '');\n};\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js",
    "content": "import { parseQueryParams, buildQueryString as stringifyQueryParams } from '@usebruno/common/utils';\nimport { uuid } from 'utils/common';\nimport { find, map, forOwn, concat, filter, each, cloneDeep, get, set, findIndex } from 'lodash';\nimport { createSlice } from '@reduxjs/toolkit';\nimport { hexy as hexdump } from 'hexy';\nimport {\n  addDepth,\n  areItemsTheSameExceptSeqUpdate,\n  collapseAllItemsInCollection,\n  deleteItemInCollection,\n  deleteItemInCollectionByPathname,\n  findCollectionByPathname,\n  findCollectionByUid,\n  findEnvironmentInCollection,\n  findItemInCollection,\n  findItemInCollectionByPathname,\n  isItemAFolder,\n  isItemARequest\n} from 'utils/collections';\nimport { parsePathParams, splitOnFirst } from 'utils/url';\nimport { getSubdirectoriesFromRoot } from 'utils/common/platform';\nimport toast from 'react-hot-toast';\nimport mime from 'mime-types';\nimport path from 'utils/common/path';\nimport { getUniqueTagsFromItems } from 'utils/collections/index';\nimport * as exampleReducers from './exampleReducers';\n\n// gRPC status code meanings\nconst grpcStatusCodes = {\n  0: 'OK',\n  1: 'CANCELLED',\n  2: 'UNKNOWN',\n  3: 'INVALID_ARGUMENT',\n  4: 'DEADLINE_EXCEEDED',\n  5: 'NOT_FOUND',\n  6: 'ALREADY_EXISTS',\n  7: 'PERMISSION_DENIED',\n  8: 'RESOURCE_EXHAUSTED',\n  9: 'FAILED_PRECONDITION',\n  10: 'ABORTED',\n  11: 'OUT_OF_RANGE',\n  12: 'UNIMPLEMENTED',\n  13: 'INTERNAL',\n  14: 'UNAVAILABLE',\n  15: 'DATA_LOSS',\n  16: 'UNAUTHENTICATED'\n};\n\n// WebSocket status code meanings\nconst wsStatusCodes = {\n  1000: 'NORMAL_CLOSURE',\n  1001: 'GOING_AWAY',\n  1002: 'PROTOCOL_ERROR',\n  1003: 'UNSUPPORTED_DATA',\n  1004: 'RESERVED',\n  1005: 'NO_STATUS_RECEIVED',\n  1006: 'ABNORMAL_CLOSURE',\n  1007: 'INVALID_FRAME_PAYLOAD_DATA',\n  1008: 'POLICY_VIOLATION',\n  1009: 'MESSAGE_TOO_BIG',\n  1010: 'MANDATORY_EXTENSION',\n  1011: 'INTERNAL_ERROR',\n  1012: 'SERVICE_RESTART',\n  1013: 'TRY_AGAIN_LATER',\n  1014: 'BAD_GATEWAY',\n  1015: 'TLS_HANDSHAKE'\n};\n\n/**\n * Preserves UIDs from existing array items when merging with new data.\n * UIDs are matched by position to keep React keys stable after file reloads.\n */\nconst preserveUidsAtPaths = (existing, updated, paths) => {\n  if (!existing || !updated) return updated;\n\n  const merged = cloneDeep(updated);\n\n  paths.forEach((path) => {\n    const newArray = get(merged, path);\n    const existingArray = get(existing, path, []);\n\n    if (Array.isArray(newArray) && newArray.length) {\n      set(\n        merged,\n        path,\n        newArray.map((item, i) => (existingArray[i]?.uid ? { ...item, uid: existingArray[i].uid } : item))\n      );\n    }\n  });\n\n  return merged;\n};\n\n// Paths containing arrays with UIDs that need preservation\nconst REQUEST_UID_PATHS = [\n  'params',\n  'headers',\n  'vars.req',\n  'vars.res',\n  'assertions',\n  'body.formUrlEncoded',\n  'body.multipartForm',\n  'body.file'\n];\n\nconst ROOT_UID_PATHS = ['request.headers', 'request.vars.req', 'request.vars.res'];\n\nconst mergeRequestWithPreservedUids = (existingRequest, newRequest) =>\n  preserveUidsAtPaths(existingRequest, newRequest, REQUEST_UID_PATHS);\n\nconst mergeRootWithPreservedUids = (existingRoot, newRoot) =>\n  preserveUidsAtPaths(existingRoot, newRoot, ROOT_UID_PATHS);\n\nconst initialState = {\n  collections: [],\n  collectionSortOrder: 'default',\n  activeConnections: [],\n  tempDirectories: {},\n  saveTransientRequestModals: []\n};\n\nconst initiatedGrpcResponse = {\n  statusCode: null,\n  statusText: 'STREAMING',\n  statusDescription: null,\n  headers: [],\n  metadata: null,\n  trailers: null,\n  statusDetails: null,\n  error: null,\n  isError: false,\n  duration: 0,\n  responses: [],\n  timestamp: Date.now()\n};\n\nconst initiatedWsResponse = {\n  status: 'PENDING',\n  statusText: 'PENDING',\n  statusCode: 0,\n  headers: [],\n  body: '',\n  size: 0,\n  duration: 0,\n  sortOrder: -1,\n  responses: [],\n  isError: false,\n  error: null,\n  errorDetails: null,\n  metadata: [],\n  trailers: []\n};\n\nexport const collectionsSlice = createSlice({\n  name: 'collections',\n  initialState,\n  reducers: {\n    createCollection: (state, action) => {\n      const collectionUids = map(state.collections, (c) => c.uid);\n      const collection = action.payload;\n\n      collection.settingsSelectedTab = 'overview';\n      collection.folderLevelSettingsSelectedTab = {};\n      collection.allTags = []; // Initialize collection-level tags\n\n      // Collection mount status is used to track the mount status of the collection\n      // values can be 'unmounted', 'mounting', 'mounted'\n      collection.mountStatus = 'unmounted';\n\n      // Add format property from brunoConfig for easy access\n      // YAML collections have 'opencollection' field, BRU collections have 'version' field\n      if (collection.brunoConfig?.opencollection) {\n        collection.format = 'yml';\n      } else {\n        collection.format = collection.brunoConfig?.format || 'bru';\n      }\n\n      // TODO: move this to use the nextAction approach\n      // last action is used to track the last action performed on the collection\n      // this is optional\n      // this is used in scenarios where we want to know the last action performed on the collection\n      // and take some extra action based on that\n      // for example, when a env is created, we want to auto select it the env modal\n      collection.importedAt = new Date().getTime();\n      collection.lastAction = null;\n\n      collapseAllItemsInCollection(collection);\n      addDepth(collection.items);\n      if (!collectionUids.includes(collection.uid)) {\n        state.collections.push(collection);\n      }\n    },\n    collapseFullCollection: (state, action) => {\n      const { collectionUid } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n      if (collection) {\n        collapseAllItemsInCollection(collection);\n      }\n    },\n    updateCollectionMountStatus: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      if (collection) {\n        if (action.payload.mountStatus) {\n          collection.mountStatus = action.payload.mountStatus;\n        }\n      }\n    },\n    updateCollectionLoadingState: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      if (collection) {\n        collection.isLoading = action.payload.isLoading;\n      }\n    },\n    setCollectionSecurityConfig: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      if (collection) {\n        collection.securityConfig = action.payload.securityConfig;\n      }\n    },\n    brunoConfigUpdateEvent: (state, action) => {\n      const { collectionUid, brunoConfig } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (collection) {\n        collection.brunoConfig = brunoConfig;\n      }\n    },\n    renameCollection: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        collection.name = action.payload.newName;\n      }\n    },\n    removeCollection: (state, action) => {\n      state.collections = filter(state.collections, (c) => c.uid !== action.payload.collectionUid);\n    },\n    sortCollections: (state, action) => {\n      state.collectionSortOrder = action.payload.order;\n      const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });\n      switch (action.payload.order) {\n        case 'default':\n          state.collections = state.collections.sort((a, b) => a.importedAt - b.importedAt);\n          break;\n        case 'alphabetical':\n          state.collections = state.collections.sort((a, b) => collator.compare(a.name, b.name));\n          break;\n        case 'reverseAlphabetical':\n          state.collections = state.collections.sort((a, b) => -collator.compare(a.name, b.name));\n          break;\n      }\n    },\n    moveCollection: (state, action) => {\n      const { draggedItem, targetItem } = action.payload;\n      state.collections = state.collections.filter((i) => i.uid !== draggedItem.uid); // Remove dragged item\n      const targetItemIndex = state.collections.findIndex((i) => i.uid === targetItem.uid); // Find target item\n      state.collections.splice(targetItemIndex, 0, draggedItem); // Insert dragged-item above target-item\n    },\n    updateLastAction: (state, action) => {\n      const { collectionUid, lastAction } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (collection) {\n        collection.lastAction = lastAction;\n      }\n    },\n    updateSettingsSelectedTab: (state, action) => {\n      const { collectionUid, folderUid, tab } = action.payload;\n\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (collection) {\n        collection.settingsSelectedTab = tab;\n      }\n    },\n    updatedFolderSettingsSelectedTab: (state, action) => {\n      const { collectionUid, folderUid, tab } = action.payload;\n\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (collection) {\n        const folder = findItemInCollection(collection, folderUid);\n\n        if (folder) {\n          collection.folderLevelSettingsSelectedTab[folderUid] = tab;\n        }\n      }\n    },\n    collectionUnlinkEnvFileEvent: (state, action) => {\n      const { data: environment, meta } = action.payload;\n      const collection = findCollectionByUid(state.collections, meta.collectionUid);\n\n      if (collection) {\n        collection.environments = filter(collection.environments, (e) => e.uid !== environment.uid);\n      }\n    },\n    saveEnvironment: (state, action) => {\n      const { variables, environmentUid, collectionUid } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (collection) {\n        const environment = findEnvironmentInCollection(collection, environmentUid);\n\n        if (environment) {\n          environment.variables = variables;\n        }\n      }\n    },\n    selectEnvironment: (state, action) => {\n      const { environmentUid, collectionUid } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (collection) {\n        if (environmentUid) {\n          const environment = findEnvironmentInCollection(collection, environmentUid);\n\n          if (environment) {\n            collection.activeEnvironmentUid = environmentUid;\n          }\n        } else {\n          collection.activeEnvironmentUid = null;\n        }\n      }\n    },\n    updateEnvironmentColor: (state, action) => {\n      const { environmentUid, color, collectionUid } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (collection) {\n        const environment = findEnvironmentInCollection(collection, environmentUid);\n\n        if (environment) {\n          environment.color = color;\n        }\n      }\n    },\n    newItem: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        if (!action.payload.currentItemUid) {\n          collection.items.push(action.payload.item);\n        } else {\n          const item = findItemInCollection(collection, action.payload.currentItemUid);\n\n          if (item) {\n            item.items = item.items || [];\n            item.items.push(action.payload.item);\n          }\n        }\n        addDepth(collection.items);\n      }\n    },\n    deleteItem: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        deleteItemInCollection(action.payload.itemUid, collection);\n      }\n    },\n    renameItem: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item) {\n          item.name = action.payload.newName;\n        }\n      }\n    },\n    cloneItem: (state, action) => {\n      const collectionUid = action.payload.collectionUid;\n      const clonedItem = action.payload.clonedItem;\n      const parentItemUid = action.payload.parentItemUid;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (collection) {\n        if (parentItemUid) {\n          const parentItem = findItemInCollection(collection, parentItemUid);\n          parentItem.items.push(clonedItem);\n        } else {\n          collection.items.push(clonedItem);\n        }\n      }\n    },\n    scriptEnvironmentUpdateEvent: (state, action) => {\n      const { collectionUid, envVariables, runtimeVariables, persistentEnvVariables } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (collection) {\n        const activeEnvironmentUid = collection.activeEnvironmentUid;\n        const activeEnvironment = findEnvironmentInCollection(collection, activeEnvironmentUid);\n\n        if (activeEnvironment) {\n          const existingEnvVarNames = new Set(Object.keys(envVariables));\n\n          // Update or add variables that exist in envVariables\n          forOwn(envVariables, (value, key) => {\n            const variable = find(activeEnvironment.variables, (v) => v.name === key);\n            const isPersistent = persistentEnvVariables && persistentEnvVariables[key] !== undefined;\n\n            if (variable) {\n              // For updates coming from scripts, treat them as ephemeral overlays unless they are persistent.\n              if (variable.value !== value) {\n                /*\n                 Overlay (persist: false): keep new value in Redux for UI and mark ephemeral\n                 so it isn't written to disk. persistedValue stores the previous on-disk value;\n                 save/persist uses that base unless the key is explicitly persisted.\n                */\n                const previousValue = variable.value;\n                variable.value = value;\n                variable.ephemeral = !isPersistent;\n                if (variable.persistedValue === undefined) {\n                  variable.persistedValue = previousValue;\n                }\n              }\n            } else {\n              // __name__ is a private variable used to store the name of the environment\n              // this is not a user defined variable and hence should not be updated\n              if (key !== '__name__') {\n                activeEnvironment.variables.push({\n                  name: key,\n                  value,\n                  secret: false,\n                  enabled: true,\n                  type: 'text',\n                  uid: uuid(),\n                  ephemeral: !isPersistent\n                });\n              }\n            }\n          });\n\n          // Handle variables that were deleted via bru.deleteEnvVar()\n          activeEnvironment.variables = activeEnvironment.variables.filter((variable) => {\n            // Variable still exists in envVariables after script execution - keep it\n            if (existingEnvVarNames.has(variable.name)) {\n              return true;\n            }\n\n            // Variable was deleted via bru.deleteEnvVar() - handle based on its state\n            // If variable was modified by script (has persistedValue), restore original value\n            if (variable.persistedValue !== undefined) {\n              variable.value = variable.persistedValue;\n              variable.ephemeral = false;\n              delete variable.persistedValue;\n              return true;\n            }\n\n            // Remove variable: either ephemeral (created by scripts) or non-ephemeral deleted via API\n            return false;\n          });\n        }\n\n        collection.runtimeVariables = runtimeVariables;\n      }\n    },\n    processEnvUpdateEvent: (state, action) => {\n      const { collectionUid, processEnvVariables } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (collection) {\n        collection.processEnvVariables = processEnvVariables;\n      }\n    },\n    workspaceEnvUpdateEvent: (state, action) => {\n      const { processEnvVariables } = action.payload;\n      state.collections.forEach((collection) => {\n        collection.workspaceProcessEnvVariables = processEnvVariables;\n      });\n    },\n    setDotEnvVariables: (state, action) => {\n      const { collectionUid, variables, exists, filename = '.env' } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (collection) {\n        if (!collection.dotEnvFiles) {\n          collection.dotEnvFiles = [];\n        }\n\n        const existingIndex = collection.dotEnvFiles.findIndex((f) => f.filename === filename);\n        if (existingIndex >= 0) {\n          if (exists) {\n            collection.dotEnvFiles[existingIndex] = { filename, variables, exists };\n          } else {\n            collection.dotEnvFiles.splice(existingIndex, 1);\n          }\n        } else if (exists) {\n          collection.dotEnvFiles.push({ filename, variables, exists });\n        }\n\n        collection.dotEnvFiles.sort((a, b) => {\n          if (a.filename === '.env') return -1;\n          if (b.filename === '.env') return 1;\n          return a.filename.localeCompare(b.filename);\n        });\n\n        const mainEnvFile = collection.dotEnvFiles.find((f) => f.filename === '.env');\n        collection.dotEnvVariables = mainEnvFile?.variables || [];\n        collection.dotEnvExists = mainEnvFile?.exists || false;\n      }\n    },\n    requestCancelled: (state, action) => {\n      const { itemUid, collectionUid, seq, timestamp } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, itemUid);\n        if (item) {\n          if (item.response?.stream?.running) {\n            item.response.stream.running = null;\n\n            const startTimestamp = item.requestSent.timestamp;\n            item.response.duration = startTimestamp ? Date.now() - startTimestamp : item.response.duration;\n            item.response.data = [{ type: 'info', timestamp: Date.now(), seq: seq, message: 'Connection Closed' }].concat(item.response.data);\n          } else {\n            item.response = null;\n            item.requestUid = null;\n          }\n          item.cancelTokenUid = null;\n          item.requestStartTime = null;\n        }\n      }\n    },\n    responseReceived: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n        if (item) {\n          item.requestState = 'received';\n          item.response = action.payload.response;\n          item.cancelTokenUid = item.response.stream?.running ? item.cancelTokenUid : null;\n          item.requestStartTime = null;\n\n          if (!collection.timeline) {\n            collection.timeline = [];\n          }\n\n          // Ensure timestamp is a number (milliseconds since epoch)\n          const timestamp = item?.requestSent?.timestamp instanceof Date\n            ? item.requestSent.timestamp.getTime()\n            : item?.requestSent?.timestamp || Date.now();\n\n          // Append the new timeline entry with numeric timestamp\n          collection.timeline.push({\n            type: 'request',\n            collectionUid: collection.uid,\n            folderUid: null,\n            itemUid: item.uid,\n            timestamp: timestamp,\n            data: {\n              request: item.requestSent || item.request,\n              response: action.payload.response,\n              timestamp: timestamp\n            }\n          });\n        }\n      }\n    },\n    runGrpcRequestEvent: (state, action) => {\n      const { itemUid, collectionUid, eventType, eventData } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n      if (!collection) return;\n\n      const item = findItemInCollection(collection, itemUid);\n      if (!item) return;\n      const request = item.draft ? item.draft.request : item.request;\n      const isUnary = request.methodType === 'unary';\n\n      if (eventType === 'request') {\n        item.requestSent = eventData;\n        item.requestSent.timestamp = Date.now();\n        item.response = {\n          initiatedGrpcResponse,\n          statusText: isUnary ? 'PENDING' : 'STREAMING'\n        };\n      }\n\n      if (!collection.timeline) {\n        collection.timeline = [];\n      }\n\n      collection.timeline.push({\n        type: 'request',\n        eventType: eventType, // Add the specific gRPC event type\n        collectionUid: collection.uid,\n        folderUid: null,\n        itemUid: item.uid,\n        timestamp: Date.now(),\n        data: {\n          request: eventData || item.requestSent || item.request,\n          timestamp: Date.now(),\n          eventData: eventData\n        }\n      });\n    },\n    grpcResponseReceived: (state, action) => {\n      const { itemUid, collectionUid, eventType, eventData } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (!collection) return;\n\n      const item = findItemInCollection(collection, itemUid);\n\n      if (!item) return;\n\n      // Get current response state or create initial state\n      const currentResponse = item.response || initiatedGrpcResponse;\n      const timestamp = item?.requestSent?.timestamp;\n      let updatedResponse = { ...currentResponse, duration: Date.now() - (timestamp || Date.now()) };\n\n      // Process based on event type\n      switch (eventType) {\n        case 'response':\n          const { error, res } = eventData;\n\n          //  Handle error if present\n          if (error) {\n            const errorCode = error.code || 2; // Default to UNKNOWN if no code\n\n            updatedResponse.error = error.details || 'gRPC error occurred';\n            updatedResponse.statusCode = errorCode;\n            updatedResponse.statusText = grpcStatusCodes[errorCode] || 'UNKNOWN';\n            updatedResponse.errorDetails = error;\n            updatedResponse.isError = true;\n          }\n\n          // Add response to list\n          updatedResponse.responses = res\n            ? [...(currentResponse?.responses || []), res]\n            : [...(currentResponse?.responses || [])];\n          break;\n\n        case 'metadata':\n          updatedResponse.headers = eventData.metadata;\n          updatedResponse.metadata = eventData.metadata;\n          break;\n\n        case 'status':\n          // Extract status info\n          const statusCode = eventData.status?.code;\n          const statusDetails = eventData.status?.details;\n          const statusMetadata = eventData.status?.metadata;\n\n          // Set status based on actual code and details\n          updatedResponse.statusCode = statusCode;\n          updatedResponse.statusText = grpcStatusCodes[statusCode] || 'UNKNOWN';\n          updatedResponse.statusDescription = statusDetails;\n          updatedResponse.statusDetails = eventData.status;\n\n          // Store trailers (status metadata)\n          if (statusMetadata) {\n            updatedResponse.trailers = statusMetadata;\n          }\n\n          // Handle error status (non-zero code)\n          if (statusCode !== 0) {\n            updatedResponse.isError = true;\n            updatedResponse.error = statusDetails || `gRPC error with code ${statusCode} (${updatedResponse.statusText})`;\n          }\n\n          break;\n\n        case 'error':\n          // Extract error details\n          const errorCode = eventData.error?.code || 2; // Default to UNKNOWN if no code\n          const errorDetails = eventData.error?.details || eventData.error?.message;\n          const errorMetadata = eventData.error?.metadata;\n\n          updatedResponse.isError = true;\n          updatedResponse.error = errorDetails || 'Unknown gRPC error';\n          updatedResponse.statusCode = errorCode;\n          updatedResponse.statusText = grpcStatusCodes[errorCode] || 'UNKNOWN';\n          updatedResponse.statusDescription = errorDetails;\n\n          // Store error metadata as trailers if present\n          if (errorMetadata) {\n            updatedResponse.trailers = errorMetadata;\n          }\n\n          break;\n\n        case 'end':\n          state.activeConnections = state.activeConnections.filter((id) => id !== itemUid);\n          break;\n\n        case 'cancel':\n          updatedResponse.statusCode = 1; // CANCELLED\n          updatedResponse.statusText = 'CANCELLED';\n          updatedResponse.statusDescription = 'Stream cancelled by client or server';\n          state.activeConnections = state.activeConnections.filter((id) => id !== itemUid);\n          break;\n      }\n\n      item.requestState = 'received';\n      item.response = updatedResponse;\n\n      // Update the timeline\n      if (!collection?.timeline) {\n        collection.timeline = [];\n      }\n\n      // Append the new timeline entry with specific gRPC event type\n      collection.timeline.push({\n        type: 'request',\n        eventType: eventType, // Add the specific gRPC event type\n        collectionUid: collection.uid,\n        folderUid: null,\n        itemUid: item.uid,\n        timestamp: Date.now(),\n        data: {\n          request: item.requestSent || item.request,\n          response: updatedResponse,\n          eventData: eventData, // Store the original event data\n          timestamp: Date.now()\n        }\n      });\n    },\n    responseCleared: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n        if (item) {\n          if (item.response && item.response.stream?.running) {\n            item.response.data = '';\n            item.response.size = 0;\n            return;\n          }\n          item.response = null;\n        }\n      }\n    },\n    clearTimeline: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        collection.timeline = [];\n      }\n    },\n    clearRequestTimeline: (state, action) => {\n      const { collectionUid, itemUid } = action.payload || {};\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (collection) {\n        if (itemUid) {\n          collection.timeline = collection?.timeline?.filter((t) => t?.itemUid !== itemUid);\n        }\n      }\n    },\n    saveRequest: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && item.draft) {\n          item.request = item.draft.request;\n          if (item.draft.settings) {\n            item.settings = item.draft.settings;\n          }\n          item.draft = null;\n        }\n      }\n    },\n    deleteRequestDraft: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && item.draft) {\n          item.draft = null;\n        }\n      }\n    },\n    saveCollectionDraft: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection && collection.draft) {\n        if (collection.draft.root) {\n          collection.root = collection.draft.root;\n        }\n        if (collection.draft.brunoConfig) {\n          collection.brunoConfig = collection.draft.brunoConfig;\n        }\n        collection.draft = null;\n      }\n    },\n    saveFolderDraft: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;\n\n      if (folder && folder.draft) {\n        folder.root = folder.draft;\n        folder.draft = null;\n      }\n    },\n    deleteCollectionDraft: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection && collection.draft) {\n        collection.draft = null;\n      }\n    },\n    deleteFolderDraft: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;\n\n      if (folder && folder.draft) {\n        folder.draft = null;\n      }\n    },\n    setEnvironmentsDraft: (state, action) => {\n      const { collectionUid, environmentUid, variables } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n      if (collection) {\n        collection.environmentsDraft = { environmentUid, variables };\n      }\n    },\n    clearEnvironmentsDraft: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      if (collection) {\n        collection.environmentsDraft = null;\n      }\n    },\n    newEphemeralHttpRequest: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection && collection.items && collection.items.length) {\n        const parts = splitOnFirst(action.payload.requestUrl, '?');\n        const queryParams = parseQueryParams(parts[1]);\n\n        let pathParams = [];\n        try {\n          pathParams = parsePathParams(parts[0]);\n        } catch (err) {\n          console.error(err);\n          toast.error(err.message);\n        }\n\n        const queryParamObjects = queryParams.map((param) => ({\n          uid: uuid(),\n          name: param.key,\n          value: param.value,\n          description: '',\n          type: 'query',\n          enabled: true\n        }));\n\n        const pathParamObjects = pathParams.map((param) => ({\n          uid: uuid(),\n          name: param.key,\n          value: param.value,\n          description: '',\n          type: 'path',\n          enabled: true\n        }));\n\n        const params = [...queryParamObjects, ...pathParamObjects];\n\n        const item = {\n          uid: action.payload.uid,\n          name: action.payload.requestName,\n          type: action.payload.requestType,\n          isTransient: false,\n          request: {\n            url: action.payload.requestUrl,\n            method: action.payload.requestMethod,\n            params,\n            headers: [],\n            body: {\n              mode: null,\n              content: null\n            }\n          },\n          draft: null\n        };\n        item.draft = cloneDeep(item);\n        collection.items.push(item);\n      }\n    },\n    toggleCollection: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload);\n\n      if (collection) {\n        collection.collapsed = !collection.collapsed;\n      }\n    },\n    toggleCollectionItem: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && item.type === 'folder') {\n          item.collapsed = !item.collapsed;\n        }\n      }\n    },\n    requestUrlChanged: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.request.url = action.payload.url;\n          item.draft.request.params = item?.draft?.request?.params ?? [];\n          item.request.params = item?.request?.params ?? [];\n\n          const parts = splitOnFirst(item?.draft?.request?.url, '?');\n          const urlQueryParams = parseQueryParams(parts[1]);\n          let urlPathParams = [];\n\n          try {\n            urlPathParams = parsePathParams(parts[0]);\n          } catch (err) {\n            console.error(err);\n            toast.error(err.message);\n          }\n\n          const disabledQueryParams = filter(item?.draft?.request?.params, (p) => !p.enabled && p.type === 'query');\n          let enabledQueryParams = filter(item?.draft?.request?.params, (p) => p.enabled && p.type === 'query');\n          let oldPathParams = filter(item?.draft?.request?.params, (p) => p.enabled && p.type === 'path');\n          let newPathParams = [];\n\n          // try and connect as much as old params uid's as possible\n          each(urlQueryParams, (urlQueryParam) => {\n            const existingQueryParam = find(\n              enabledQueryParams,\n              (p) => p?.name === urlQueryParam?.name || p?.value === urlQueryParam?.value\n            );\n            urlQueryParam.uid = existingQueryParam?.uid || uuid();\n            urlQueryParam.enabled = true;\n            urlQueryParam.type = 'query';\n\n            // once found, remove it - trying our best here to accommodate duplicate query params\n            if (existingQueryParam) {\n              enabledQueryParams = filter(enabledQueryParams, (p) => p?.uid !== existingQueryParam?.uid);\n            }\n          });\n\n          // filter the newest path param and compare with previous data that already inserted\n          newPathParams = filter(urlPathParams, (urlPath) => {\n            const existingPathParam = find(oldPathParams, (p) => p.name === urlPath.name);\n            if (existingPathParam) {\n              return false;\n            }\n            urlPath.uid = uuid();\n            urlPath.enabled = true;\n            urlPath.type = 'path';\n            return true;\n          });\n\n          // remove path param that not used or deleted when typing url\n          oldPathParams = filter(oldPathParams, (urlPath) => {\n            return find(urlPathParams, (p) => p.name === urlPath.name);\n          });\n\n          // ultimately params get replaced with params in url + the disabled ones that existed prior\n          // the query params are the source of truth, the url in the queryurl input gets constructed using these params\n          // we however are also storing the full url (with params) in the url itself\n          item.draft.request.params = concat(urlQueryParams, newPathParams, disabledQueryParams, oldPathParams);\n        }\n      }\n    },\n    updateItemSettings: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.settings = { ...item.draft.settings, ...action.payload.settings };\n        }\n      }\n    },\n    updateAuth: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n\n          item.draft.request.auth = item.draft.request.auth || {};\n          switch (action.payload.mode) {\n            case 'awsv4':\n              item.draft.request.auth.mode = 'awsv4';\n              item.draft.request.auth.awsv4 = action.payload.content;\n              break;\n            case 'bearer':\n              item.draft.request.auth.mode = 'bearer';\n              item.draft.request.auth.bearer = action.payload.content;\n              break;\n            case 'basic':\n              item.draft.request.auth.mode = 'basic';\n              item.draft.request.auth.basic = action.payload.content;\n              break;\n            case 'digest':\n              item.draft.request.auth.mode = 'digest';\n              item.draft.request.auth.digest = action.payload.content;\n              break;\n            case 'ntlm':\n              item.draft.request.auth.mode = 'ntlm';\n              item.draft.request.auth.ntlm = action.payload.content;\n              break;\n            case 'oauth2':\n              item.draft.request.auth.mode = 'oauth2';\n              item.draft.request.auth.oauth2 = action.payload.content;\n              break;\n            case 'wsse':\n              item.draft.request.auth.mode = 'wsse';\n              item.draft.request.auth.wsse = action.payload.content;\n              break;\n            case 'apikey':\n              item.draft.request.auth.mode = 'apikey';\n              item.draft.request.auth.apikey = action.payload.content;\n              break;\n          }\n        }\n      }\n    },\n    addQueryParam: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.request.params = item.draft.request.params || [];\n          item.draft.request.params.push({\n            uid: uuid(),\n            name: '',\n            value: '',\n            description: '',\n            type: 'query',\n            enabled: true\n          });\n        }\n      }\n    },\n    setQueryParams: (state, action) => {\n      const { collectionUid, itemUid, params } = action.payload;\n\n      const collection = findCollectionByUid(state.collections, collectionUid);\n      if (!collection) {\n        return;\n      }\n\n      const item = findItemInCollection(collection, itemUid);\n      if (!item || !isItemARequest(item)) {\n        return;\n      }\n\n      if (!item.draft) {\n        item.draft = cloneDeep(item);\n      }\n      const existingOtherParams = item.draft.request.params?.filter((p) => p.type !== 'query') || [];\n      const newQueryParams = map(params, ({ uid, name = '', value = '', description = '', type = 'query', enabled = true }) => ({\n        uid: uid || uuid(),\n        name,\n        value,\n        description,\n        type,\n        enabled\n      }));\n\n      item.draft.request.params = [...newQueryParams, ...existingOtherParams];\n\n      // Update the request URL to reflect the new query params\n      const parts = splitOnFirst(item.draft.request.url, '?');\n      const query = stringifyQueryParams(\n        filter(item.draft.request.params, (p) => p.enabled && p.type === 'query')\n      );\n\n      // If there are enabled query params, append them to the URL\n      if (query && query.length) {\n        item.draft.request.url = parts[0] + '?' + query;\n      } else {\n        // If no enabled query params, remove the query part from URL\n        item.draft.request.url = parts[0];\n      }\n    },\n    moveQueryParam: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          // Ensure item.draft is a deep clone of item if not already present\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n\n          // Extract payload data\n          const { updateReorderedItem } = action.payload;\n          const params = item.draft.request.params;\n\n          const queryParams = params.filter((param) => param.type === 'query');\n          const pathParams = params.filter((param) => param.type === 'path');\n\n          // Reorder only query params based on updateReorderedItem\n          const reorderedQueryParams = updateReorderedItem.map((uid) => {\n            return queryParams.find((param) => param.uid === uid);\n          });\n          item.draft.request.params = [...reorderedQueryParams, ...pathParams];\n\n          // Update request URL\n          const parts = splitOnFirst(item.draft.request.url, '?');\n          const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled && p.type === 'query'));\n          if (query && query.length) {\n            item.draft.request.url = parts[0] + '?' + query;\n          } else {\n            item.draft.request.url = parts[0];\n          }\n        }\n      }\n    },\n\n    updateQueryParam: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          const queryParam = find(\n            item.draft.request.params,\n            (h) => h.uid === action.payload.queryParam.uid && h.type === 'query'\n          );\n          if (queryParam) {\n            queryParam.name = action.payload.queryParam.name;\n            queryParam.value = action.payload.queryParam.value;\n            queryParam.enabled = action.payload.queryParam.enabled;\n\n            // update request url\n            const parts = splitOnFirst(item.draft.request.url, '?');\n            const query = stringifyQueryParams(\n              filter(item.draft.request.params, (p) => p.enabled && p.type === 'query')\n            );\n\n            // if no query is found, then strip the query params in url\n            if (!query || !query.length) {\n              if (parts.length) {\n                item.draft.request.url = parts[0];\n              }\n              return;\n            }\n\n            // if no parts were found, then append the query\n            if (!parts.length) {\n              item.draft.request.url += '?' + query;\n              return;\n            }\n\n            // control reaching here means the request has parts and query is present\n            item.draft.request.url = parts[0] + '?' + query;\n          }\n        }\n      }\n    },\n    deleteQueryParam: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.request.params = filter(item.draft.request.params, (p) => p.uid !== action.payload.paramUid);\n\n          // update request url\n          const parts = splitOnFirst(item.draft.request.url, '?');\n          const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled && p.type === 'query'));\n          if (query && query.length) {\n            item.draft.request.url = parts[0] + '?' + query;\n          } else {\n            item.draft.request.url = parts[0];\n          }\n        }\n      }\n    },\n    updatePathParam: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n\n          const param = find(\n            item.draft.request.params,\n            (p) => p.uid === action.payload.pathParam.uid && p.type === 'path'\n          );\n\n          if (param) {\n            param.name = action.payload.pathParam.name;\n            param.value = action.payload.pathParam.value;\n          }\n        }\n      }\n    },\n    addRequestHeader: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.request.headers = item.draft.request.headers || [];\n          item.draft.request.headers.push({\n            uid: uuid(),\n            name: '',\n            value: '',\n            description: '',\n            enabled: true\n          });\n        }\n      }\n    },\n    updateRequestHeader: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          const header = find(item.draft.request.headers, (h) => h.uid === action.payload.header.uid);\n          if (header) {\n            header.name = action.payload.header.name;\n            header.value = action.payload.header.value;\n            header.description = action.payload.header.description;\n            header.enabled = action.payload.header.enabled;\n          }\n        }\n      }\n    },\n    deleteRequestHeader: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.request.headers = filter(item.draft.request.headers, (h) => h.uid !== action.payload.headerUid);\n        }\n      }\n    },\n    moveRequestHeader: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          // Ensure item.draft is a deep clone of item if not already present\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n\n          // Extract payload data\n          const { updateReorderedItem } = action.payload;\n          const params = item.draft.request.headers;\n\n          item.draft.request.headers = updateReorderedItem.map((uid) => {\n            return params.find((param) => param.uid === uid);\n          });\n        }\n      }\n    },\n    setRequestHeaders: (state, action) => {\n      const { collectionUid, itemUid, headers } = action.payload;\n\n      const collection = findCollectionByUid(state.collections, collectionUid);\n      if (!collection) {\n        return;\n      }\n\n      const item = findItemInCollection(collection, itemUid);\n      if (!item || !isItemARequest(item)) {\n        return;\n      }\n\n      if (!item.draft) {\n        item.draft = cloneDeep(item);\n      }\n      item.draft.request.headers = map(action.payload.headers, ({ uid, name = '', value = '', description = '', enabled = true }) => ({\n        uid: uid || uuid(),\n        name,\n        value,\n        description,\n        enabled\n      }));\n    },\n    setCollectionHeaders: (state, action) => {\n      const { collectionUid, headers } = action.payload;\n\n      const collection = findCollectionByUid(state.collections, collectionUid);\n      if (!collection) {\n        return;\n      }\n\n      if (!collection.draft) {\n        collection.draft = {\n          root: cloneDeep(collection.root) || {}\n        };\n      }\n      if (!collection.draft.root) {\n        collection.draft.root = {};\n      }\n      if (!collection.draft.root.request) {\n        collection.draft.root.request = {};\n      }\n\n      collection.draft.root.request.headers = map(headers, ({ uid, name = '', value = '', description = '', enabled = true }) => ({\n        uid: uid || uuid(),\n        name,\n        value,\n        description,\n        enabled\n      }));\n    },\n    setFolderHeaders: (state, action) => {\n      const { collectionUid, folderUid, headers } = action.payload;\n\n      const collection = findCollectionByUid(state.collections, collectionUid);\n      if (!collection) {\n        return;\n      }\n\n      const folder = findItemInCollection(collection, folderUid);\n      if (!folder || !isItemAFolder(folder)) {\n        return;\n      }\n\n      if (!folder.draft) {\n        folder.draft = cloneDeep(folder.root) || {};\n      }\n      if (!folder.draft.request) {\n        folder.draft.request = {};\n      }\n      folder.draft.request.headers = map(headers, ({ uid, name = '', value = '', description = '', enabled = true }) => ({\n        uid: uid || uuid(),\n        name,\n        value,\n        description,\n        enabled\n      }));\n    },\n    addFormUrlEncodedParam: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.request.body.formUrlEncoded = item.draft.request.body.formUrlEncoded || [];\n          item.draft.request.body.formUrlEncoded.push({\n            uid: uuid(),\n            name: '',\n            value: '',\n            description: '',\n            enabled: true\n          });\n        }\n      }\n    },\n    updateFormUrlEncodedParam: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          const param = find(item.draft.request.body.formUrlEncoded, (p) => p.uid === action.payload.param.uid);\n          if (param) {\n            param.name = action.payload.param.name;\n            param.value = action.payload.param.value;\n            param.description = action.payload.param.description;\n            param.enabled = action.payload.param.enabled;\n          }\n        }\n      }\n    },\n    deleteFormUrlEncodedParam: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.request.body.formUrlEncoded = filter(\n            item.draft.request.body.formUrlEncoded,\n            (p) => p.uid !== action.payload.paramUid\n          );\n        }\n      }\n    },\n    setFormUrlEncodedParams: (state, action) => {\n      const { collectionUid, itemUid, params } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n      if (!collection) return;\n\n      const item = findItemInCollection(collection, itemUid);\n      if (!item || !isItemARequest(item)) return;\n\n      if (!item.draft) {\n        item.draft = cloneDeep(item);\n      }\n      item.draft.request.body.formUrlEncoded = map(params, ({ uid, name = '', value = '', description = '', enabled = true }) => ({\n        uid: uid || uuid(),\n        name,\n        value,\n        description,\n        enabled\n      }));\n    },\n    moveFormUrlEncodedParam: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n\n          const { updateReorderedItem } = action.payload;\n          const params = item.draft.request.body.formUrlEncoded;\n\n          item.draft.request.body.formUrlEncoded = updateReorderedItem.map((uid) => {\n            return params.find((param) => param.uid === uid);\n          });\n        }\n      }\n    },\n    addMultipartFormParam: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.request.body.multipartForm = item.draft.request.body.multipartForm || [];\n          item.draft.request.body.multipartForm.push({\n            uid: uuid(),\n            type: action.payload.type,\n            name: '',\n            value: action.payload.value,\n            description: '',\n            contentType: '',\n            enabled: true\n          });\n        }\n      }\n    },\n    updateMultipartFormParam: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          const param = find(item.draft.request.body.multipartForm, (p) => p.uid === action.payload.param.uid);\n          if (param) {\n            param.type = action.payload.param.type;\n            param.name = action.payload.param.name;\n            param.value = action.payload.param.value;\n            param.description = action.payload.param.description;\n            param.contentType = action.payload.param.contentType;\n            param.enabled = action.payload.param.enabled;\n          }\n        }\n      }\n    },\n    deleteMultipartFormParam: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.request.body.multipartForm = filter(\n            item.draft.request.body.multipartForm,\n            (p) => p.uid !== action.payload.paramUid\n          );\n        }\n      }\n    },\n    setMultipartFormParams: (state, action) => {\n      const { collectionUid, itemUid, params } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n      if (!collection) return;\n\n      const item = findItemInCollection(collection, itemUid);\n      if (!item || !isItemARequest(item)) return;\n\n      if (!item.draft) {\n        item.draft = cloneDeep(item);\n      }\n      item.draft.request.body.multipartForm = map(params, ({ uid, name = '', value = '', contentType = '', type = 'text', enabled = true }) => ({\n        uid: uid || uuid(),\n        name,\n        value,\n        contentType,\n        type,\n        enabled\n      }));\n    },\n    moveMultipartFormParam: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n\n          const { updateReorderedItem } = action.payload;\n          const params = item.draft.request.body.multipartForm;\n\n          item.draft.request.body.multipartForm = updateReorderedItem.map((uid) => {\n            return params.find((param) => param.uid === uid);\n          });\n        }\n      }\n    },\n    addFile: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.request.body.file = item.draft.request.body.file || [];\n\n          item.draft.request.body.file.push({\n            uid: uuid(),\n            filePath: '',\n            contentType: '',\n            selected: false\n          });\n        }\n      }\n    },\n    updateFile: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n\n          const param = find(item.draft.request.body.file, (p) => p.uid === action.payload.param.uid);\n\n          if (param) {\n            const contentType = mime.contentType(path.extname(action.payload.param.filePath));\n            param.filePath = action.payload.param.filePath;\n            param.contentType = action.payload.param.contentType || contentType || '';\n            param.selected = action.payload.param.selected;\n\n            item.draft.request.body.file = item.draft.request.body.file.map((p) => {\n              p.selected = p.uid === param.uid;\n              return p;\n            });\n          }\n        }\n      }\n    },\n    deleteFile: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n\n          item.draft.request.body.file = filter(\n            item.draft.request.body.file,\n            (p) => p.uid !== action.payload.paramUid\n          );\n\n          if (item.draft.request.body.file.length > 0) {\n            item.draft.request.body.file[0].selected = true;\n          }\n        }\n      }\n    },\n    updateRequestAuthMode: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection && collection.items && collection.items.length) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.request.auth = {};\n          item.draft.request.auth.mode = action.payload.mode;\n        }\n      }\n    },\n    updateRequestBodyMode: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.request.body.mode = action.payload.mode;\n        }\n      }\n    },\n    updateRequestBody: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n\n          switch (item.draft.request.body.mode) {\n            case 'json': {\n              item.draft.request.body.json = action.payload.content;\n              break;\n            }\n            case 'text': {\n              item.draft.request.body.text = action.payload.content;\n              break;\n            }\n            case 'xml': {\n              item.draft.request.body.xml = action.payload.content;\n              break;\n            }\n            case 'sparql': {\n              item.draft.request.body.sparql = action.payload.content;\n              break;\n            }\n            case 'file': {\n              item.draft.request.body.file = action.payload.content;\n              break;\n            }\n            case 'formUrlEncoded': {\n              item.draft.request.body.formUrlEncoded = action.payload.content;\n              break;\n            }\n            case 'multipartForm': {\n              item.draft.request.body.multipartForm = action.payload.content;\n              break;\n            }\n            case 'grpc': {\n              item.draft.request.body.grpc = action.payload.content;\n              break;\n            }\n            case 'ws': {\n              item.draft.request.body.ws = action.payload.content;\n              break;\n            }\n          }\n        }\n      }\n    },\n    updateRequestGraphqlQuery: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.request.body.mode = 'graphql';\n          item.draft.request.body.graphql = item.draft.request.body.graphql || {};\n          item.draft.request.body.graphql.query = action.payload.query;\n        }\n      }\n    },\n    updateRequestGraphqlVariables: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.request.body.mode = 'graphql';\n          item.draft.request.body.graphql = item.draft.request.body.graphql || {};\n          item.draft.request.body.graphql.variables = action.payload.variables;\n        }\n      }\n    },\n    updateRequestScript: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.request.script = item.draft.request.script || {};\n          item.draft.request.script.req = action.payload.script;\n        }\n      }\n    },\n    updateResponseScript: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.request.script = item.draft.request.script || {};\n          item.draft.request.script.res = action.payload.script;\n        }\n      }\n    },\n    updateRequestTests: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.request.tests = action.payload.tests;\n        }\n      }\n    },\n    updateRequestMethod: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.request.method = action.payload.method;\n          item.draft.request.methodType = action.payload.methodType;\n        }\n      }\n    },\n    updateRequestProtoPath: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.request.protoPath = action.payload.protoPath;\n        }\n      }\n    },\n    addAssertion: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.request.assertions = item.draft.request.assertions || [];\n          item.draft.request.assertions.push({\n            uid: uuid(),\n            name: '',\n            value: '',\n            enabled: true\n          });\n        }\n      }\n    },\n    updateAssertion: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          const assertion = item.draft.request.assertions.find((a) => a.uid === action.payload.assertion.uid);\n          if (assertion) {\n            assertion.name = action.payload.assertion.name;\n            assertion.value = action.payload.assertion.value;\n            assertion.enabled = action.payload.assertion.enabled;\n          }\n        }\n      }\n    },\n    deleteAssertion: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.request.assertions = item.draft.request.assertions.filter(\n            (a) => a.uid !== action.payload.assertUid\n          );\n        }\n      }\n    },\n    setRequestAssertions: (state, action) => {\n      const { collectionUid, itemUid, assertions } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n      if (!collection) return;\n\n      const item = findItemInCollection(collection, itemUid);\n      if (!item || !isItemARequest(item)) return;\n\n      if (!item.draft) {\n        item.draft = cloneDeep(item);\n      }\n      item.draft.request.assertions = map(assertions, ({ uid, name = '', value = '', operator = 'eq', enabled = true }) => ({\n        uid: uid || uuid(),\n        name,\n        value,\n        operator,\n        enabled\n      }));\n    },\n    moveAssertion: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n\n          const { updateReorderedItem } = action.payload;\n          const params = item.draft.request.assertions;\n\n          item.draft.request.assertions = updateReorderedItem.map((uid) => {\n            return params.find((param) => param.uid === uid);\n          });\n        }\n      }\n    },\n    addVar: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      const type = action.payload.type;\n      const varData = action.payload.var || {};\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          if (type === 'request') {\n            item.draft.request.vars = item.draft.request.vars || {};\n            item.draft.request.vars.req = item.draft.request.vars.req || [];\n            item.draft.request.vars.req.push({\n              uid: uuid(),\n              name: varData.name || '',\n              value: varData.value || '',\n              local: varData.local === true,\n              enabled: varData.enabled !== false\n            });\n          } else if (type === 'response') {\n            item.draft.request.vars = item.draft.request.vars || {};\n            item.draft.request.vars.res = item.draft.request.vars.res || [];\n            item.draft.request.vars.res.push({\n              uid: uuid(),\n              name: '',\n              value: '',\n              local: false,\n              enabled: true\n            });\n          }\n        }\n      }\n    },\n    updateVar: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      const type = action.payload.type;\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          if (type === 'request') {\n            item.draft.request.vars = item.draft.request.vars || {};\n            item.draft.request.vars.req = item.draft.request.vars.req || [];\n\n            const reqVar = find(item.draft.request.vars.req, (v) => v.uid === action.payload.var.uid);\n            if (reqVar) {\n              reqVar.name = action.payload.var.name;\n              reqVar.value = action.payload.var.value;\n              reqVar.description = action.payload.var.description;\n              reqVar.enabled = action.payload.var.enabled;\n            }\n          } else if (type === 'response') {\n            item.draft.request.vars = item.draft.request.vars || {};\n            item.draft.request.vars.res = item.draft.request.vars.res || [];\n            const resVar = find(item.draft.request.vars.res, (v) => v.uid === action.payload.var.uid);\n            if (resVar) {\n              resVar.name = action.payload.var.name;\n              resVar.value = action.payload.var.value;\n              resVar.description = action.payload.var.description;\n              resVar.enabled = action.payload.var.enabled;\n            }\n          }\n        }\n      }\n    },\n    deleteVar: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      const type = action.payload.type;\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          if (type === 'request') {\n            item.draft.request.vars = item.draft.request.vars || {};\n            item.draft.request.vars.req = item.draft.request.vars.req || [];\n            item.draft.request.vars.req = item.draft.request.vars.req.filter((v) => v.uid !== action.payload.varUid);\n          } else if (type === 'response') {\n            item.draft.request.vars = item.draft.request.vars || {};\n            item.draft.request.vars.res = item.draft.request.vars.res || [];\n            item.draft.request.vars.res = item.draft.request.vars.res.filter((v) => v.uid !== action.payload.varUid);\n          }\n        }\n      }\n    },\n    setRequestVars: (state, action) => {\n      const { collectionUid, itemUid, vars, type } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n      if (!collection) return;\n\n      const item = findItemInCollection(collection, itemUid);\n      if (!item || !isItemARequest(item)) return;\n\n      if (!item.draft) {\n        item.draft = cloneDeep(item);\n      }\n      item.draft.request.vars = item.draft.request.vars || {};\n      const mappedVars = map(vars, ({ uid, name = '', value = '', enabled = true, local = false }) => ({\n        uid: uid || uuid(),\n        name,\n        value,\n        enabled,\n        ...(type === 'response' ? { local } : {})\n      }));\n      if (type === 'request') {\n        item.draft.request.vars.req = mappedVars;\n      } else if (type === 'response') {\n        item.draft.request.vars.res = mappedVars;\n      }\n    },\n    moveVar: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      const type = action.payload.type;\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          // Ensure item.draft is a deep clone of item if not already present\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n\n          // Extract payload data\n          const { updateReorderedItem } = action.payload;\n          if (type == 'request') {\n            const params = item.draft.request.vars.req;\n\n            item.draft.request.vars.req = updateReorderedItem.map((uid) => {\n              return params.find((param) => param.uid === uid);\n            });\n          } else if (type === 'response') {\n            const params = item.draft.request.vars.res;\n\n            item.draft.request.vars.res = updateReorderedItem.map((uid) => {\n              return params.find((param) => param.uid === uid);\n            });\n          }\n        }\n      }\n    },\n    updateCollectionAuthMode: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        if (!collection.draft) {\n          collection.draft = {\n            root: cloneDeep(collection.root)\n          };\n        }\n        set(collection, 'draft.root.request.auth', {});\n        set(collection, 'draft.root.request.auth.mode', action.payload.mode);\n      }\n    },\n    updateCollectionAuth: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        if (!collection.draft) {\n          collection.draft = {\n            root: cloneDeep(collection.root)\n          };\n        }\n        set(collection, 'draft.root.request.auth', {});\n        set(collection, 'draft.root.request.auth.mode', action.payload.mode);\n        switch (action.payload.mode) {\n          case 'awsv4':\n            set(collection, 'draft.root.request.auth.awsv4', action.payload.content);\n            break;\n          case 'bearer':\n            set(collection, 'draft.root.request.auth.bearer', action.payload.content);\n            break;\n          case 'basic':\n            set(collection, 'draft.root.request.auth.basic', action.payload.content);\n            break;\n          case 'digest':\n            set(collection, 'draft.root.request.auth.digest', action.payload.content);\n            break;\n          case 'ntlm':\n            set(collection, 'draft.root.request.auth.ntlm', action.payload.content);\n            break;\n          case 'oauth2':\n            set(collection, 'draft.root.request.auth.oauth2', action.payload.content);\n            break;\n          case 'wsse':\n            set(collection, 'draft.root.request.auth.wsse', action.payload.content);\n            break;\n          case 'apikey':\n            set(collection, 'draft.root.request.auth.apikey', action.payload.content);\n            break;\n        }\n      }\n    },\n    updateCollectionRequestScript: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        if (!collection.draft) {\n          collection.draft = {\n            root: cloneDeep(collection.root)\n          };\n        }\n        set(collection, 'draft.root.request.script.req', action.payload.script);\n      }\n    },\n    updateCollectionResponseScript: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        if (!collection.draft) {\n          collection.draft = {\n            root: cloneDeep(collection.root)\n          };\n        }\n        set(collection, 'draft.root.request.script.res', action.payload.script);\n      }\n    },\n    updateCollectionTests: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        if (!collection.draft) {\n          collection.draft = {\n            root: cloneDeep(collection.root)\n          };\n        }\n        set(collection, 'draft.root.request.tests', action.payload.tests);\n      }\n    },\n    updateCollectionDocs: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        if (!collection.draft) {\n          collection.draft = {\n            root: cloneDeep(collection.root)\n          };\n        }\n        set(collection, 'draft.root.docs', action.payload.docs);\n      }\n    },\n    updateCollectionProxy: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        if (!collection.draft) {\n          collection.draft = {\n            root: cloneDeep(collection.root),\n            brunoConfig: cloneDeep(collection.brunoConfig)\n          };\n        }\n        if (!collection.draft.brunoConfig) {\n          collection.draft.brunoConfig = cloneDeep(collection.brunoConfig);\n        }\n        set(collection, 'draft.brunoConfig.proxy', action.payload.proxy);\n      }\n    },\n    updateCollectionClientCertificates: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        if (!collection.draft) {\n          collection.draft = {\n            root: cloneDeep(collection.root),\n            brunoConfig: cloneDeep(collection.brunoConfig)\n          };\n        }\n        if (!collection.draft.brunoConfig) {\n          collection.draft.brunoConfig = cloneDeep(collection.brunoConfig);\n        }\n        set(collection, 'draft.brunoConfig.clientCertificates', action.payload.clientCertificates);\n      }\n    },\n    updateCollectionPresets: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        if (!collection.draft) {\n          collection.draft = {\n            root: cloneDeep(collection.root),\n            brunoConfig: cloneDeep(collection.brunoConfig)\n          };\n        }\n        if (!collection.draft.brunoConfig) {\n          collection.draft.brunoConfig = cloneDeep(collection.brunoConfig);\n        }\n        set(collection, 'draft.brunoConfig.presets', action.payload.presets);\n      }\n    },\n    updateCollectionProtobuf: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        if (!collection.draft) {\n          collection.draft = {\n            root: cloneDeep(collection.root),\n            brunoConfig: cloneDeep(collection.brunoConfig)\n          };\n        }\n        if (!collection.draft.brunoConfig) {\n          collection.draft.brunoConfig = cloneDeep(collection.brunoConfig);\n        }\n        set(collection, 'draft.brunoConfig.protobuf', action.payload.protobuf);\n      }\n    },\n    addFolderHeader: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;\n      if (folder) {\n        if (!folder.draft) {\n          folder.draft = cloneDeep(folder.root);\n        }\n        const headers = get(folder, 'draft.request.headers', []);\n        headers.push({\n          uid: uuid(),\n          name: '',\n          value: '',\n          description: '',\n          enabled: true\n        });\n        set(folder, 'draft.request.headers', headers);\n      }\n    },\n    updateFolderHeader: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;\n      if (folder) {\n        if (!folder.draft) {\n          folder.draft = cloneDeep(folder.root);\n        }\n        const headers = get(folder, 'draft.request.headers', []);\n        const header = find(headers, (h) => h.uid === action.payload.header.uid);\n        if (header) {\n          header.name = action.payload.header.name;\n          header.value = action.payload.header.value;\n          header.description = action.payload.header.description;\n          header.enabled = action.payload.header.enabled;\n        }\n      }\n    },\n    deleteFolderHeader: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;\n      if (folder) {\n        if (!folder.draft) {\n          folder.draft = cloneDeep(folder.root);\n        }\n        let headers = get(folder, 'draft.request.headers', []);\n        headers = filter(headers, (h) => h.uid !== action.payload.headerUid);\n        set(folder, 'draft.request.headers', headers);\n      }\n    },\n    addFolderVar: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;\n      const type = action.payload.type;\n      const varData = action.payload.var || {};\n      if (folder) {\n        if (!folder.draft) {\n          folder.draft = cloneDeep(folder.root);\n        }\n        if (type === 'request') {\n          const vars = get(folder, 'draft.request.vars.req', []);\n          vars.push({\n            uid: uuid(),\n            name: varData.name || '',\n            value: varData.value || '',\n            enabled: varData.enabled !== false\n          });\n          set(folder, 'draft.request.vars.req', vars);\n        } else if (type === 'response') {\n          const vars = get(folder, 'draft.request.vars.res', []);\n          vars.push({\n            uid: uuid(),\n            name: '',\n            value: '',\n            enabled: true\n          });\n          set(folder, 'draft.request.vars.res', vars);\n        }\n      }\n    },\n    updateFolderVar: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;\n      const type = action.payload.type;\n      if (folder) {\n        if (!folder.draft) {\n          folder.draft = cloneDeep(folder.root);\n        }\n        if (type === 'request') {\n          let vars = get(folder, 'draft.request.vars.req', []);\n          const _var = find(vars, (h) => h.uid === action.payload.var.uid);\n          if (_var) {\n            _var.name = action.payload.var.name;\n            _var.value = action.payload.var.value;\n            _var.description = action.payload.var.description;\n            _var.enabled = action.payload.var.enabled;\n          }\n          set(folder, 'draft.request.vars.req', vars);\n        } else if (type === 'response') {\n          let vars = get(folder, 'draft.request.vars.res', []);\n          const _var = find(vars, (h) => h.uid === action.payload.var.uid);\n          if (_var) {\n            _var.name = action.payload.var.name;\n            _var.value = action.payload.var.value;\n            _var.description = action.payload.var.description;\n            _var.enabled = action.payload.var.enabled;\n          }\n          set(folder, 'draft.request.vars.res', vars);\n        }\n      }\n    },\n    deleteFolderVar: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;\n      const type = action.payload.type;\n      if (folder) {\n        if (!folder.draft) {\n          folder.draft = cloneDeep(folder.root);\n        }\n        if (type === 'request') {\n          let vars = get(folder, 'draft.request.vars.req', []);\n          vars = filter(vars, (h) => h.uid !== action.payload.varUid);\n          set(folder, 'draft.request.vars.req', vars);\n        } else if (type === 'response') {\n          let vars = get(folder, 'draft.request.vars.res', []);\n          vars = filter(vars, (h) => h.uid !== action.payload.varUid);\n          set(folder, 'draft.request.vars.res', vars);\n        }\n      }\n    },\n    setFolderVars: (state, action) => {\n      const { collectionUid, folderUid, vars, type } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n      const folder = collection ? findItemInCollection(collection, folderUid) : null;\n      if (!folder) {\n        return;\n      }\n      if (!folder.draft) {\n        folder.draft = cloneDeep(folder.root);\n      }\n      const mappedVars = map(vars, ({ uid, name = '', value = '', enabled = true, local = false }) => ({\n        uid: uid || uuid(),\n        name,\n        value,\n        enabled,\n        ...(type === 'response' ? { local } : {})\n      }));\n      if (type === 'request') {\n        set(folder, 'draft.request.vars.req', mappedVars);\n      } else if (type === 'response') {\n        set(folder, 'draft.request.vars.res', mappedVars);\n      }\n    },\n    updateFolderRequestScript: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;\n      if (folder) {\n        if (!folder.draft) {\n          folder.draft = cloneDeep(folder.root);\n        }\n        set(folder, 'draft.request.script.req', action.payload.script);\n      }\n    },\n    updateFolderResponseScript: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;\n      if (folder) {\n        if (!folder.draft) {\n          folder.draft = cloneDeep(folder.root);\n        }\n        set(folder, 'draft.request.script.res', action.payload.script);\n      }\n    },\n    updateFolderTests: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;\n      if (folder) {\n        if (!folder.draft) {\n          folder.draft = cloneDeep(folder.root);\n        }\n        set(folder, 'draft.request.tests', action.payload.tests);\n      }\n    },\n    updateFolderAuth: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      if (!collection) return;\n\n      const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;\n      if (!folder) return;\n\n      if (folder) {\n        if (!folder.draft) {\n          folder.draft = cloneDeep(folder.root);\n        }\n        set(folder, 'draft.request.auth', {});\n        set(folder, 'draft.request.auth.mode', action.payload.mode);\n        switch (action.payload.mode) {\n          case 'oauth2':\n            set(folder, 'draft.request.auth.oauth2', action.payload.content);\n            break;\n          case 'basic':\n            set(folder, 'draft.request.auth.basic', action.payload.content);\n            break;\n          case 'bearer':\n            set(folder, 'draft.request.auth.bearer', action.payload.content);\n            break;\n          case 'digest':\n            set(folder, 'draft.request.auth.digest', action.payload.content);\n            break;\n          case 'ntlm':\n            set(folder, 'draft.request.auth.ntlm', action.payload.content);\n            break;\n          case 'apikey':\n            set(folder, 'draft.request.auth.apikey', action.payload.content);\n            break;\n          case 'awsv4':\n            set(folder, 'draft.request.auth.awsv4', action.payload.content);\n            break;\n          case 'wsse':\n            set(folder, 'draft.request.auth.wsse', action.payload.content);\n            break;\n          case 'ws':\n            set(folder, 'draft.request.auth.ws', action.payload.content);\n            break;\n        }\n      }\n    },\n    addCollectionHeader: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        if (!collection.draft) {\n          collection.draft = {\n            root: cloneDeep(collection.root)\n          };\n        }\n        const headers = get(collection, 'draft.root.request.headers', []);\n        headers.push({\n          uid: uuid(),\n          name: '',\n          value: '',\n          description: '',\n          enabled: true\n        });\n        set(collection, 'draft.root.request.headers', headers);\n      }\n    },\n    updateCollectionHeader: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        if (!collection.draft) {\n          collection.draft = {\n            root: cloneDeep(collection.root)\n          };\n        }\n        const headers = get(collection, 'draft.root.request.headers', []);\n        const header = find(headers, (h) => h.uid === action.payload.header.uid);\n        if (header) {\n          header.name = action.payload.header.name;\n          header.value = action.payload.header.value;\n          header.description = action.payload.header.description;\n          header.enabled = action.payload.header.enabled;\n        }\n      }\n    },\n    deleteCollectionHeader: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        if (!collection.draft) {\n          collection.draft = {\n            root: cloneDeep(collection.root)\n          };\n        }\n        let headers = get(collection, 'draft.root.request.headers', []);\n        headers = filter(headers, (h) => h.uid !== action.payload.headerUid);\n        set(collection, 'draft.root.request.headers', headers);\n      }\n    },\n    addCollectionVar: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      const type = action.payload.type;\n      const varData = action.payload.var || {};\n      if (collection) {\n        if (!collection.draft) {\n          collection.draft = {\n            root: cloneDeep(collection.root)\n          };\n        }\n        if (type === 'request') {\n          const vars = get(collection, 'draft.root.request.vars.req', []);\n          vars.push({\n            uid: uuid(),\n            name: varData.name || '',\n            value: varData.value || '',\n            enabled: varData.enabled !== false\n          });\n          set(collection, 'draft.root.request.vars.req', vars);\n        } else if (type === 'response') {\n          const vars = get(collection, 'draft.root.request.vars.res', []);\n          vars.push({\n            uid: uuid(),\n            name: '',\n            value: '',\n            local: false,\n            enabled: true\n          });\n          set(collection, 'draft.root.request.vars.res', vars);\n        }\n      }\n    },\n    updateCollectionVar: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      const type = action.payload.type;\n      if (collection) {\n        if (!collection.draft) {\n          collection.draft = {\n            root: cloneDeep(collection.root)\n          };\n        }\n        if (type === 'request') {\n          let vars = get(collection, 'draft.root.request.vars.req', []);\n          const _var = find(vars, (h) => h.uid === action.payload.var.uid);\n          if (_var) {\n            _var.name = action.payload.var.name;\n            _var.value = action.payload.var.value;\n            _var.description = action.payload.var.description;\n            _var.enabled = action.payload.var.enabled;\n          }\n          set(collection, 'draft.root.request.vars.req', vars);\n        } else if (type === 'response') {\n          let vars = get(collection, 'draft.root.request.vars.res', []);\n          const _var = find(vars, (h) => h.uid === action.payload.var.uid);\n          if (_var) {\n            _var.name = action.payload.var.name;\n            _var.value = action.payload.var.value;\n            _var.description = action.payload.var.description;\n            _var.enabled = action.payload.var.enabled;\n          }\n          set(collection, 'draft.root.request.vars.res', vars);\n        }\n      }\n    },\n    deleteCollectionVar: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      const type = action.payload.type;\n      if (collection) {\n        if (!collection.draft) {\n          collection.draft = {\n            root: cloneDeep(collection.root)\n          };\n        }\n        if (type === 'request') {\n          let vars = get(collection, 'draft.root.request.vars.req', []);\n          vars = filter(vars, (h) => h.uid !== action.payload.varUid);\n          set(collection, 'draft.root.request.vars.req', vars);\n        } else if (type === 'response') {\n          let vars = get(collection, 'draft.root.request.vars.res', []);\n          vars = filter(vars, (h) => h.uid !== action.payload.varUid);\n          set(collection, 'draft.root.request.vars.res', vars);\n        }\n      }\n    },\n    setCollectionVars: (state, action) => {\n      const { collectionUid, vars, type } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n      if (!collection) {\n        return;\n      }\n      if (!collection.draft) {\n        collection.draft = {\n          root: cloneDeep(collection.root)\n        };\n      }\n      const mappedVars = map(vars, ({ uid, name = '', value = '', enabled = true, local = false }) => ({\n        uid: uid || uuid(),\n        name,\n        value,\n        enabled,\n        ...(type === 'response' ? { local } : {})\n      }));\n      if (type === 'request') {\n        set(collection, 'draft.root.request.vars.req', mappedVars);\n      } else if (type === 'response') {\n        set(collection, 'draft.root.request.vars.res', mappedVars);\n      }\n    },\n    collectionAddFileEvent: (state, action) => {\n      const file = action.payload.file;\n      const isCollectionRoot = file.meta.collectionRoot ? true : false;\n      const isFolderRoot = file.meta.folderRoot ? true : false;\n      const collection = findCollectionByUid(state.collections, file.meta.collectionUid);\n      if (isCollectionRoot) {\n        if (collection) {\n          collection.root = mergeRootWithPreservedUids(collection.root, file.data);\n        }\n        return;\n      }\n\n      if (isFolderRoot) {\n        const folderPath = path.dirname(file.meta.pathname);\n        const folderItem = findItemInCollectionByPathname(collection, folderPath);\n        if (folderItem) {\n          if (file?.data?.meta?.name) {\n            folderItem.name = file?.data?.meta?.name;\n          }\n          folderItem.root = mergeRootWithPreservedUids(folderItem.root, file.data);\n          if (file?.data?.meta?.seq) {\n            folderItem.seq = file.data?.meta?.seq;\n          }\n        }\n        return;\n      }\n\n      if (collection) {\n        const dirname = path.dirname(file.meta.pathname);\n\n        const tempDirectory = state.tempDirectories?.[file.meta.collectionUid];\n        const isTransientFile = tempDirectory && file.meta.pathname.startsWith(tempDirectory);\n\n        const subDirectories = getSubdirectoriesFromRoot(collection.pathname, dirname);\n        let currentPath = collection.pathname;\n        let currentSubItems = collection.items;\n        for (const directoryName of subDirectories) {\n          let childItem = currentSubItems.find((f) => f.type === 'folder' && f.filename === directoryName);\n          currentPath = path.join(currentPath, directoryName);\n          if (!childItem) {\n            childItem = {\n              uid: uuid(),\n              pathname: currentPath,\n              name: directoryName,\n              collapsed: true,\n              type: 'folder',\n              isTransient: isTransientFile,\n              items: []\n            };\n            currentSubItems.push(childItem);\n          } else if (isTransientFile && !childItem.isTransient) {\n            // Update existing folder to be transient if the file is transient\n            childItem.isTransient = true;\n          }\n          currentSubItems = childItem.items;\n        }\n\n        if (file.meta.name != 'folder.bru' && !currentSubItems.find((f) => f.name === file.meta.name)) {\n          // this happens when you rename a file\n          // the add event might get triggered first, before the unlink event\n          // this results in duplicate uids causing react renderer to go mad\n          const currentItem = find(currentSubItems, (i) => i.uid === file.data.uid);\n          if (currentItem) {\n            currentItem.name = file.data.name;\n            currentItem.type = file.data.type;\n            currentItem.seq = file.data.seq;\n            currentItem.tags = file.data.tags;\n            currentItem.request = mergeRequestWithPreservedUids(currentItem.request, file.data.request);\n            currentItem.filename = file.meta.name;\n            currentItem.pathname = file.meta.pathname;\n            currentItem.settings = file.data.settings;\n            currentItem.examples = file.data.examples;\n            currentItem.draft = null;\n            currentItem.partial = file.partial;\n            currentItem.loading = file.loading;\n            currentItem.size = file.size;\n            currentItem.error = file.error;\n            currentItem.isTransient = isTransientFile;\n          } else {\n            currentSubItems.push({\n              uid: file.data.uid,\n              name: file.data.name,\n              type: file.data.type,\n              seq: file.data.seq,\n              tags: file.data.tags,\n              request: file.data.request,\n              settings: file.data.settings,\n              examples: file.data.examples,\n              filename: file.meta.name,\n              pathname: file.meta.pathname,\n              draft: null,\n              partial: file.partial,\n              loading: file.loading,\n              size: file.size,\n              error: file.error,\n              isTransient: isTransientFile\n            });\n          }\n        }\n        addDepth(collection.items);\n      }\n    },\n    collectionAddDirectoryEvent: (state, action) => {\n      const { dir } = action.payload;\n      const collection = findCollectionByUid(state.collections, dir.meta.collectionUid);\n\n      if (collection) {\n        // Check if this directory is in a temp directory (transient request)\n        const tempDirectory = state.tempDirectories?.[dir.meta.collectionUid];\n        const isTransientDir = tempDirectory && dir.meta.pathname.startsWith(tempDirectory);\n\n        const subDirectories = getSubdirectoriesFromRoot(collection.pathname, dir.meta.pathname);\n        let currentPath = collection.pathname;\n        let currentSubItems = collection.items;\n        for (const directoryName of subDirectories) {\n          let childItem = currentSubItems.find((f) => f.type === 'folder' && f.filename === directoryName);\n          currentPath = path.join(currentPath, directoryName);\n          if (!childItem) {\n            childItem = {\n              uid: dir?.meta?.uid || uuid(),\n              pathname: currentPath,\n              name: dir?.meta?.name || directoryName,\n              seq: dir?.meta?.seq,\n              filename: directoryName,\n              collapsed: true,\n              type: 'folder',\n              isTransient: isTransientDir,\n              items: []\n            };\n            currentSubItems.push(childItem);\n          } else if (isTransientDir && !childItem.isTransient) {\n            // Update existing folder to be transient if the directory is transient\n            childItem.isTransient = true;\n          }\n          currentSubItems = childItem.items;\n        }\n        addDepth(collection.items);\n      }\n    },\n    collectionChangeFileEvent: (state, action) => {\n      const { file } = action.payload;\n      const isCollectionRoot = file.meta.collectionRoot ? true : false;\n      const isFolderRoot = file.meta.folderRoot ? true : false;\n      const collection = findCollectionByUid(state.collections, file.meta.collectionUid);\n      if (isCollectionRoot) {\n        if (collection) {\n          collection.root = mergeRootWithPreservedUids(collection.root, file.data);\n        }\n        return;\n      }\n\n      if (isFolderRoot) {\n        const folderPath = path.dirname(file.meta.pathname);\n        const folderItem = findItemInCollectionByPathname(collection, folderPath);\n        if (folderItem) {\n          if (file?.data?.meta?.name) {\n            folderItem.name = file?.data?.meta?.name;\n          }\n          if (file?.data?.meta?.seq) {\n            folderItem.seq = file?.data?.meta?.seq;\n          }\n          folderItem.root = mergeRootWithPreservedUids(folderItem.root, file.data);\n        }\n        return;\n      }\n\n      if (collection) {\n        const item = findItemInCollection(collection, file.data.uid);\n\n        if (item) {\n          // whenever a user attempts to sort a req within the same folder\n          // the seq is updated, but everything else remains the same\n          // we don't want to lose the draft in this case\n          if (areItemsTheSameExceptSeqUpdate(item, file.data)) {\n            item.seq = file.data.seq;\n            if (item?.draft) {\n              item.draft.seq = file.data.seq;\n            }\n            if (item?.draft && areItemsTheSameExceptSeqUpdate(item?.draft, file.data)) {\n              item.draft = null;\n            }\n          } else {\n            item.name = file.data.name;\n            item.type = file.data.type;\n            item.seq = file.data.seq;\n            item.tags = file.data.tags;\n            item.request = mergeRequestWithPreservedUids(item.request, file.data.request);\n            item.settings = file.data.settings;\n            item.examples = file.data.examples;\n            item.filename = file.meta.name;\n            item.pathname = file.meta.pathname;\n\n            // Only clear draft if it matches the file content\n            // This preserves characters typed during autosave\n            if (item.draft && areItemsTheSameExceptSeqUpdate(item.draft, file.data)) {\n              item.draft = null;\n            }\n          }\n        }\n      }\n    },\n    collectionUnlinkFileEvent: (state, action) => {\n      const { file } = action.payload;\n      const collection = findCollectionByUid(state.collections, file.meta.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollectionByPathname(collection, file.meta.pathname);\n\n        if (item) {\n          deleteItemInCollectionByPathname(file.meta.pathname, collection);\n        }\n      }\n    },\n    collectionUnlinkDirectoryEvent: (state, action) => {\n      const { directory } = action.payload;\n      const collection = findCollectionByUid(state.collections, directory.meta.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollectionByPathname(collection, directory.meta.pathname);\n\n        if (item) {\n          deleteItemInCollectionByPathname(directory.meta.pathname, collection);\n        }\n      }\n    },\n    collectionAddEnvFileEvent: (state, action) => {\n      const { environment, collectionUid } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (collection) {\n        collection.environments = collection.environments || [];\n\n        const existingEnv = collection.environments.find((e) => e.uid === environment.uid);\n\n        if (existingEnv) {\n          const prevEphemerals = (existingEnv.variables || []).filter((v) => v.ephemeral);\n          existingEnv.name = environment.name;\n          existingEnv.variables = environment.variables;\n          existingEnv.color = environment.color;\n          /*\n           Apply temporary (ephemeral) values only to variables that actually exist in the file. This prevents deleted temporaries from “popping back” after a save. If a variable is present in the file, we temporarily override the UI value while also remembering the on-disk value in persistedValue for future saves.\n          */\n          prevEphemerals.forEach((ev) => {\n            const target = existingEnv.variables?.find((v) => v.name === ev.name);\n            if (target) {\n              if (target.value !== ev.value) {\n                if (target.persistedValue === undefined) target.persistedValue = target.value;\n                target.value = ev.value;\n              }\n              target.ephemeral = true;\n            }\n          });\n        } else {\n          collection.environments.push(environment);\n          collection.environments.sort((a, b) => a.name.localeCompare(b.name));\n\n          const lastAction = collection.lastAction;\n          if (lastAction && lastAction.type === 'ADD_ENVIRONMENT') {\n            collection.lastAction = null;\n            if (lastAction.payload === environment.name) {\n              collection.activeEnvironmentUid = environment.uid;\n              // Persist the selection to the UI state snapshot\n              const { ipcRenderer } = window;\n              if (ipcRenderer) {\n                ipcRenderer.invoke('renderer:update-ui-state-snapshot', {\n                  type: 'COLLECTION_ENVIRONMENT',\n                  data: { collectionPath: collection?.pathname, environmentName: environment.name }\n                });\n              }\n            }\n          }\n        }\n      }\n    },\n    collectionRenamedEvent: (state, action) => {\n      const { collectionPathname, newName } = action.payload;\n      const collection = findCollectionByPathname(state.collections, collectionPathname);\n\n      if (collection) {\n        collection.name = newName;\n      }\n    },\n    resetRunResults: (state, action) => {\n      const { collectionUid } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (collection) {\n        collection.runnerResult = null;\n      }\n    },\n    initRunRequestEvent: (state, action) => {\n      const { requestUid, itemUid, collectionUid } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n      if (!collection) return;\n\n      const item = findItemInCollection(collection, itemUid);\n      if (!item) return;\n\n      item.requestState = null;\n      item.requestUid = requestUid;\n      item.requestStartTime = Date.now();\n      item.testResults = [];\n      item.preRequestTestResults = [];\n      item.postResponseTestResults = [];\n      item.assertionResults = [];\n      item.preRequestScriptErrorMessage = null;\n      item.postResponseScriptErrorMessage = null;\n      item.testScriptErrorMessage = null;\n    },\n    runRequestEvent: (state, action) => {\n      const { itemUid, collectionUid, type, requestUid } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, itemUid);\n        if (item) {\n          // ignore outdated updates in case multiple requests are fired rapidly to avoid state inconsistency\n          if (item.requestUid !== requestUid) return;\n\n          if (type === 'pre-request-script-execution') {\n            item.preRequestScriptErrorMessage = action.payload.errorMessage;\n          }\n\n          if (type === 'post-response-script-execution') {\n            item.postResponseScriptErrorMessage = action.payload.errorMessage;\n          }\n\n          if (type === 'test-script-execution') {\n            item.testScriptErrorMessage = action.payload.errorMessage;\n          }\n\n          if (type === 'request-queued') {\n            const { cancelTokenUid } = action.payload;\n            // ignore if request is already in progress or completed\n            if (['sending', 'received'].includes(item.requestState)) return;\n            item.requestState = 'queued';\n            item.cancelTokenUid = cancelTokenUid;\n          }\n\n          if (type === 'request-sent') {\n            const { cancelTokenUid, requestSent } = action.payload;\n            item.requestSent = requestSent;\n\n            // sometimes the response is received before the request-sent event arrives\n            if (item.requestState === 'queued') {\n              item.requestState = 'sending';\n              item.cancelTokenUid = cancelTokenUid;\n            }\n          }\n\n          if (type === 'assertion-results') {\n            const { results } = action.payload;\n            item.assertionResults = results;\n          }\n\n          if (type === 'test-results') {\n            const { results } = action.payload;\n            item.testResults = results;\n          }\n\n          if (type === 'test-results-pre-request') {\n            const { results } = action.payload;\n            item.preRequestTestResults = results;\n          }\n\n          if (type === 'test-results-post-response') {\n            const { results } = action.payload;\n            item.postResponseTestResults = results;\n          }\n        }\n      }\n    },\n    runFolderEvent: (state, action) => {\n      const { collectionUid, folderUid, itemUid, type, isRecursive, error, cancelTokenUid } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (collection) {\n        const folder = findItemInCollection(collection, folderUid);\n        const request = findItemInCollection(collection, itemUid);\n\n        collection.runnerResult = collection.runnerResult || { info: {}, items: [] };\n\n        // todo\n        // get startedAt and endedAt from the runner and display it in the UI\n        if (type === 'testrun-started') {\n          const info = collection.runnerResult.info;\n          info.collectionUid = collectionUid;\n          info.folderUid = folderUid;\n          info.isRecursive = isRecursive;\n          info.cancelTokenUid = cancelTokenUid;\n          info.status = 'started';\n        }\n\n        if (type === 'testrun-ended') {\n          const info = collection.runnerResult.info;\n          info.status = 'ended';\n          if (action.payload.runCompletionTime) {\n            info.runCompletionTime = action.payload.runCompletionTime;\n          }\n          if (action.payload.statusText) {\n            info.statusText = action.payload.statusText;\n          }\n        }\n\n        if (type === 'request-queued') {\n          collection.runnerResult.items.push({\n            uid: request.uid,\n            status: 'queued'\n          });\n        }\n\n        if (type === 'request-sent') {\n          const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);\n          item.status = 'running';\n          item.requestSent = action.payload.requestSent;\n        }\n\n        if (type === 'response-received') {\n          const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);\n          item.status = 'completed';\n          item.responseReceived = action.payload.responseReceived;\n        }\n\n        if (type === 'test-results') {\n          const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);\n          item.testResults = action.payload.testResults;\n        }\n\n        if (type === 'test-results-pre-request') {\n          const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);\n          item.preRequestTestResults = action.payload.preRequestTestResults;\n        }\n\n        if (type === 'test-results-post-response') {\n          const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);\n          item.postResponseTestResults = action.payload.postResponseTestResults;\n        }\n\n        if (type === 'assertion-results') {\n          const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);\n          item.assertionResults = action.payload.assertionResults;\n        }\n\n        if (type === 'error') {\n          const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);\n          item.error = action.payload.error;\n          item.responseReceived = action.payload.responseReceived;\n          item.status = 'error';\n        }\n\n        if (type === 'runner-request-skipped') {\n          const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);\n          item.status = 'skipped';\n          item.responseReceived = action.payload.responseReceived;\n        }\n\n        if (type === 'post-response-script-execution') {\n          const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);\n          item.postResponseScriptErrorMessage = action.payload.errorMessage;\n        }\n\n        if (type === 'test-script-execution') {\n          const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);\n          item.testScriptErrorMessage = action.payload.errorMessage;\n        }\n\n        if (type === 'pre-request-script-execution') {\n          const item = collection.runnerResult.items.findLast((i) => i.uid === request.uid);\n          item.preRequestScriptErrorMessage = action.payload.errorMessage;\n        }\n      }\n    },\n    resetCollectionRunner: (state, action) => {\n      const { collectionUid } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (collection) {\n        collection.runnerResult = null;\n        collection.runnerTags = { include: [], exclude: [] };\n        collection.runnerTagsEnabled = false;\n        collection.runnerConfiguration = null;\n      }\n    },\n    updateRunnerTagsDetails: (state, action) => {\n      const { collectionUid, tags, tagsEnabled } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n      if (collection) {\n        if (tags) {\n          collection.runnerTags = tags;\n        }\n        if (typeof tagsEnabled === 'boolean') {\n          collection.runnerTagsEnabled = tagsEnabled;\n        }\n      }\n    },\n    updateRunnerConfiguration: (state, action) => {\n      const { collectionUid, selectedRequestItems, requestItemsOrder, delay } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n      if (collection) {\n        collection.runnerConfiguration = {\n          selectedRequestItems: selectedRequestItems || [],\n          requestItemsOrder: requestItemsOrder || [],\n          delay: delay\n        };\n      }\n    },\n    updateRequestDocs: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.request.docs = action.payload.docs;\n        }\n      }\n    },\n    updateFolderDocs: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;\n      if (folder) {\n        if (isItemAFolder(folder)) {\n          if (!folder.draft) {\n            folder.draft = cloneDeep(folder.root);\n          }\n          set(folder, 'draft.docs', action.payload.docs);\n        }\n      }\n    },\n    collectionAddOauth2CredentialsByUrl: (state, action) => {\n      const { collectionUid, folderUid, itemUid, url, credentials, credentialsId, debugInfo } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n      if (!collection) return;\n\n      // Update oauth2Credentials (latest token)\n      if (!collection.oauth2Credentials) {\n        collection.oauth2Credentials = [];\n      }\n      let collectionOauth2Credentials = cloneDeep(collection.oauth2Credentials);\n\n      // Remove existing credentials for the same combination\n      const filteredOauth2Credentials = filter(\n        collectionOauth2Credentials,\n        (creds) =>\n          !(creds.url === url && creds.collectionUid === collectionUid && creds.credentialsId === credentialsId)\n      );\n\n      // Add the new credential with folderUid and itemUid\n      filteredOauth2Credentials.push({\n        collectionUid,\n        folderUid,\n        itemUid,\n        url,\n        credentials,\n        credentialsId,\n        debugInfo\n      });\n\n      collection.oauth2Credentials = filteredOauth2Credentials;\n\n      if (!collection.timeline) {\n        collection.timeline = [];\n      }\n\n      if (debugInfo) {\n        collection.timeline.push({\n          type: 'oauth2',\n          collectionUid,\n          folderUid,\n          itemUid,\n          timestamp: Date.now(),\n          data: {\n            collectionUid,\n            folderUid,\n            itemUid,\n            url,\n            credentials,\n            credentialsId,\n            debugInfo: debugInfo.data\n          }\n        });\n      }\n    },\n\n    // Clears a specific credential matching url + collectionUid + credentialsId (used by UI \"Clear OAuth2 Cache\")\n    collectionClearOauth2CredentialsByUrlAndCredentialsId: (state, action) => {\n      const { collectionUid, url, credentialsId } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n      if (!collection) return;\n\n      if (collection.oauth2Credentials) {\n        let collectionOauth2Credentials = cloneDeep(collection.oauth2Credentials);\n        const filteredOauth2Credentials = filter(\n          collectionOauth2Credentials,\n          (creds) =>\n            !(creds.url === url && creds.collectionUid === collectionUid && creds.credentialsId === credentialsId)\n        );\n        collection.oauth2Credentials = filteredOauth2Credentials;\n      }\n    },\n\n    // Clears all credentials matching credentialsId regardless of URL (used by script bru.resetOauth2Credential)\n    collectionClearOauth2CredentialsByCredentialsId: (state, action) => {\n      const { collectionUid, credentialsId } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n      if (!collection) return;\n\n      if (collection.oauth2Credentials) {\n        collection.oauth2Credentials = collection.oauth2Credentials.filter(\n          (creds) => creds.credentialsId !== credentialsId\n        );\n      }\n    },\n\n    updateFolderAuthMode: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n      const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;\n\n      if (folder) {\n        if (!folder.draft) {\n          folder.draft = cloneDeep(folder.root);\n        }\n        set(folder, 'draft.request.auth', {});\n        set(folder, 'draft.request.auth.mode', action.payload.mode);\n      }\n    },\n    streamDataReceived: (state, action) => {\n      const { itemUid, collectionUid, seq, timestamp, data } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, itemUid);\n        if (data.data) {\n          item.response.data ||= [];\n          item.response.data.push({\n            type: 'incoming',\n            seq,\n            message: data.data,\n            messageHexdump: hexdump(data.data),\n            timestamp: timestamp || Date.now()\n          });\n        }\n        if (data.dataBuffer) {\n          item.response.dataBuffer = Buffer.concat([Buffer.from(item.response.dataBuffer), Buffer.from(data.dataBuffer)]);\n        }\n        item.response.size = data.data?.length + (item.response.size || 0);\n      }\n    },\n    addRequestTag: (state, action) => {\n      const { tag, collectionUid, itemUid } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.tags = item.draft.tags || [];\n          if (!item.draft.tags.includes(tag.trim())) {\n            item.draft.tags.push(tag.trim());\n          }\n\n          collection.allTags = getUniqueTagsFromItems(collection.items);\n        }\n      }\n    },\n    deleteRequestTag: (state, action) => {\n      const { tag, collectionUid, itemUid } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, itemUid);\n\n        if (item && isItemARequest(item)) {\n          if (!item.draft) {\n            item.draft = cloneDeep(item);\n          }\n          item.draft.tags = item.draft.tags || [];\n          item.draft.tags = item.draft.tags.filter((t) => t !== tag.trim());\n\n          collection.allTags = getUniqueTagsFromItems(collection.items);\n        }\n      }\n    },\n    updateCollectionTagsList: (state, action) => {\n      const { collectionUid } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (collection) {\n        collection.allTags = getUniqueTagsFromItems(collection.items);\n      }\n    },\n    updateActiveConnections: (state, action) => {\n      state.activeConnections = [...action.payload.activeConnectionIds];\n    },\n    runWsRequestEvent: (state, action) => {\n      const { itemUid, collectionUid, eventType, eventData } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n      if (!collection) return;\n\n      const item = findItemInCollection(collection, itemUid);\n      if (!item) return;\n      const request = item.draft ? item.draft.request : item.request;\n\n      if (eventType === 'request') {\n        item.requestSent = eventData;\n        item.requestSent.timestamp = Date.now();\n        item.response = {\n          ...initiatedWsResponse,\n          initiatedWsResponse,\n          statusText: 'CONNECTING'\n        };\n      }\n\n      if (!collection.timeline) {\n        collection.timeline = [];\n      }\n\n      collection.timeline.push({\n        type: 'request',\n        eventType: eventType,\n        collectionUid: collection.uid,\n        folderUid: null,\n        itemUid: item.uid,\n        timestamp: Date.now(),\n        data: {\n          request: eventData || item.requestSent || item.request,\n          timestamp: Date.now(),\n          eventData: eventData\n        }\n      });\n    },\n    wsResponseReceived: (state, action) => {\n      const { itemUid, collectionUid, eventType, eventData } = action.payload;\n      const collection = findCollectionByUid(state.collections, collectionUid);\n\n      if (!collection) return;\n\n      const item = findItemInCollection(collection, itemUid);\n\n      if (!item) return;\n\n      // Get current response state or create initial state\n      const currentResponse = item.response || initiatedWsResponse;\n      const timestamp = item?.requestSent?.timestamp;\n      let updatedResponse = {\n        ...currentResponse,\n        isError: false,\n        error: '',\n        duration: Date.now() - (timestamp || Date.now())\n      };\n\n      // Process based on event type\n      switch (eventType) {\n        case 'message':\n          // Add message to responses list\n          updatedResponse.responses = (currentResponse?.responses || []).concat(eventData);\n          break;\n\n        case 'redirect':\n          updatedResponse.requestHeaders = eventData.headers;\n          updatedResponse.responses ||= [];\n          updatedResponse.responses.push({\n            message: eventData.message,\n            type: eventData.type,\n            timestamp: eventData.timestamp,\n            seq: eventData.seq\n          });\n          break;\n\n        case 'upgrade':\n          updatedResponse.headers = eventData.headers;\n          break;\n\n        case 'open':\n          updatedResponse.status = 'CONNECTED';\n          updatedResponse.statusText = 'CONNECTED';\n          updatedResponse.statusCode = 0;\n          updatedResponse.responses ||= [];\n          updatedResponse.responses.push({\n            message: `Connected to ${eventData.url}`,\n            type: 'info',\n            timestamp: eventData.timestamp,\n            seq: eventData.seq\n          });\n          break;\n\n        case 'close':\n          const { code, reason } = eventData;\n          updatedResponse.isError = false;\n          updatedResponse.error = '';\n          updatedResponse.status = 'CLOSED';\n          updatedResponse.statusCode = code;\n          updatedResponse.statusText = wsStatusCodes[code] || 'CLOSED';\n          updatedResponse.statusDescription = reason;\n\n          updatedResponse.responses.push({\n            type: code !== 1000 ? 'info' : 'error',\n            message: reason.trim().length ? ['Closed:', reason.trim()].join(' ') : 'Closed',\n            timestamp: eventData.timestamp,\n            seq: eventData.seq\n          });\n          break;\n\n        case 'error':\n          const errorDetails = eventData.error || eventData.message;\n          updatedResponse.isError = true;\n          updatedResponse.error = errorDetails || 'WebSocket error occurred';\n          updatedResponse.status = 'ERROR';\n          updatedResponse.statusCode = wsStatusCodes[1011];\n          updatedResponse.statusText = 'ERROR';\n\n          updatedResponse.responses.push({\n            type: 'error',\n            message: errorDetails || 'WebSocket error occurred',\n            timestamp: eventData.timestamp,\n            seq: eventData.seq\n          });\n\n          break;\n\n        case 'connecting':\n          updatedResponse.status = 'CONNECTING';\n          updatedResponse.statusText = 'CONNECTING';\n          break;\n      }\n\n      item.response = updatedResponse;\n    },\n    wsUpdateResponseSortOrder: (state, action) => {\n      const collection = findCollectionByUid(state.collections, action.payload.collectionUid);\n\n      if (collection) {\n        const item = findItemInCollection(collection, action.payload.itemUid);\n        if (item) {\n          item.response.sortOrder = item.response.sortOrder ? -item.response.sortOrder : -1;\n        }\n      }\n    },\n\n    addTransientDirectory: (state, action) => {\n      state.tempDirectories[action.payload.collectionUid] = action.payload.pathname;\n    },\n    addSaveTransientRequestModal: (state, action) => {\n      const { item, collection } = action.payload;\n      // Avoid duplicates - check if this item is already in the array\n      const exists = state.saveTransientRequestModals.some((modal) => modal.item.uid === item.uid);\n      if (!exists) {\n        state.saveTransientRequestModals.push({ item, collection });\n      }\n    },\n    removeSaveTransientRequestModal: (state, action) => {\n      const { itemUid } = action.payload;\n      state.saveTransientRequestModals = state.saveTransientRequestModals.filter(\n        (modal) => modal.item.uid !== itemUid\n      );\n    },\n    clearAllSaveTransientRequestModals: (state) => {\n      state.saveTransientRequestModals = [];\n    },\n    /* Response Example Actions */\n    addResponseExample: exampleReducers.addResponseExample,\n    cloneResponseExample: exampleReducers.cloneResponseExample,\n    updateResponseExample: exampleReducers.updateResponseExample,\n    deleteResponseExample: exampleReducers.deleteResponseExample,\n    cancelResponseExampleEdit: exampleReducers.cancelResponseExampleEdit,\n    addResponseExampleHeader: exampleReducers.addResponseExampleHeader,\n    updateResponseExampleHeader: exampleReducers.updateResponseExampleHeader,\n    deleteResponseExampleHeader: exampleReducers.deleteResponseExampleHeader,\n    moveResponseExampleHeader: exampleReducers.moveResponseExampleHeader,\n    setResponseExampleHeaders: exampleReducers.setResponseExampleHeaders,\n    addResponseExampleParam: exampleReducers.addResponseExampleParam,\n    updateResponseExampleParam: exampleReducers.updateResponseExampleParam,\n    deleteResponseExampleParam: exampleReducers.deleteResponseExampleParam,\n    moveResponseExampleParam: exampleReducers.moveResponseExampleParam,\n    updateResponseExampleRequest: exampleReducers.updateResponseExampleRequest,\n    updateResponseExampleMultipartFormParams: exampleReducers.updateResponseExampleMultipartFormParams,\n    updateResponseExampleFileBodyParams: exampleReducers.updateResponseExampleFileBodyParams,\n    updateResponseExampleFormUrlEncodedParams: exampleReducers.updateResponseExampleFormUrlEncodedParams,\n    updateResponseExampleStatusCode: exampleReducers.updateResponseExampleStatusCode,\n    updateResponseExampleStatusText: exampleReducers.updateResponseExampleStatusText,\n    updateResponseExampleRequestUrl: exampleReducers.updateResponseExampleRequestUrl,\n    updateResponseExampleResponse: exampleReducers.updateResponseExampleResponse,\n    updateResponseExampleDetails: exampleReducers.updateResponseExampleDetails,\n    updateResponseExampleName: exampleReducers.updateResponseExampleName,\n    updateResponseExampleDescription: exampleReducers.updateResponseExampleDescription,\n    addResponseExampleRequestHeader: exampleReducers.addResponseExampleRequestHeader,\n    updateResponseExampleRequestHeader: exampleReducers.updateResponseExampleRequestHeader,\n    deleteResponseExampleRequestHeader: exampleReducers.deleteResponseExampleRequestHeader,\n    moveResponseExampleRequestHeader: exampleReducers.moveResponseExampleRequestHeader,\n    setResponseExampleRequestHeaders: exampleReducers.setResponseExampleRequestHeaders,\n    setResponseExampleParams: exampleReducers.setResponseExampleParams,\n    updateResponseExampleBody: exampleReducers.updateResponseExampleBody,\n    addResponseExampleFileParam: exampleReducers.addResponseExampleFileParam,\n    updateResponseExampleFileParam: exampleReducers.updateResponseExampleFileParam,\n    deleteResponseExampleFileParam: exampleReducers.deleteResponseExampleFileParam,\n    addResponseExampleFormUrlEncodedParam: exampleReducers.addResponseExampleFormUrlEncodedParam,\n    updateResponseExampleFormUrlEncodedParam: exampleReducers.updateResponseExampleFormUrlEncodedParam,\n    deleteResponseExampleFormUrlEncodedParam: exampleReducers.deleteResponseExampleFormUrlEncodedParam,\n    addResponseExampleMultipartFormParam: exampleReducers.addResponseExampleMultipartFormParam,\n    updateResponseExampleMultipartFormParam: exampleReducers.updateResponseExampleMultipartFormParam,\n    deleteResponseExampleMultipartFormParam: exampleReducers.deleteResponseExampleMultipartFormParam\n    /* End Response Example Actions */\n  }\n});\n\nexport const {\n  createCollection,\n  updateCollectionMountStatus,\n  updateCollectionLoadingState,\n  setCollectionSecurityConfig,\n  brunoConfigUpdateEvent,\n  renameCollection,\n  removeCollection,\n  sortCollections,\n  updateLastAction,\n  updateSettingsSelectedTab,\n  updatedFolderSettingsSelectedTab,\n  collectionUnlinkEnvFileEvent,\n  saveEnvironment,\n  selectEnvironment,\n  updateEnvironmentColor,\n  newItem,\n  deleteItem,\n  renameItem,\n  cloneItem,\n  scriptEnvironmentUpdateEvent,\n  processEnvUpdateEvent,\n  workspaceEnvUpdateEvent,\n  setDotEnvVariables,\n  requestCancelled,\n  responseReceived,\n  runGrpcRequestEvent,\n  grpcResponseReceived,\n  responseCleared,\n  clearTimeline,\n  clearRequestTimeline,\n  saveRequest,\n  deleteRequestDraft,\n  saveCollectionDraft,\n  saveFolderDraft,\n  deleteCollectionDraft,\n  deleteFolderDraft,\n  setEnvironmentsDraft,\n  clearEnvironmentsDraft,\n  newEphemeralHttpRequest,\n  collapseFullCollection,\n  toggleCollection,\n  toggleCollectionItem,\n  requestUrlChanged,\n  updateItemSettings,\n  updateAuth,\n  addQueryParam,\n  setQueryParams,\n  moveQueryParam,\n  updateQueryParam,\n  deleteQueryParam,\n  updatePathParam,\n  addRequestHeader,\n  updateRequestHeader,\n  deleteRequestHeader,\n  moveRequestHeader,\n  setRequestHeaders,\n  setCollectionHeaders,\n  setFolderHeaders,\n  addFormUrlEncodedParam,\n  updateFormUrlEncodedParam,\n  deleteFormUrlEncodedParam,\n  setFormUrlEncodedParams,\n  moveFormUrlEncodedParam,\n  addMultipartFormParam,\n  updateMultipartFormParam,\n  deleteMultipartFormParam,\n  setMultipartFormParams,\n  addFile,\n  updateFile,\n  deleteFile,\n  moveMultipartFormParam,\n  updateRequestAuthMode,\n  updateRequestBodyMode,\n  updateRequestBody,\n  updateRequestGraphqlQuery,\n  updateRequestGraphqlVariables,\n  updateRequestScript,\n  updateResponseScript,\n  updateRequestTests,\n  updateRequestMethod,\n  updateRequestProtoPath,\n  addAssertion,\n  updateAssertion,\n  deleteAssertion,\n  setRequestAssertions,\n  moveAssertion,\n  addVar,\n  updateVar,\n  deleteVar,\n  setRequestVars,\n  moveVar,\n  addFolderHeader,\n  updateFolderHeader,\n  deleteFolderHeader,\n  addFolderVar,\n  updateFolderVar,\n  deleteFolderVar,\n  setFolderVars,\n  updateFolderRequestScript,\n  updateFolderResponseScript,\n  updateFolderTests,\n  addCollectionHeader,\n  updateCollectionHeader,\n  deleteCollectionHeader,\n  addCollectionVar,\n  updateCollectionVar,\n  deleteCollectionVar,\n  setCollectionVars,\n  updateCollectionAuthMode,\n  updateCollectionAuth,\n  updateCollectionRequestScript,\n  updateCollectionResponseScript,\n  updateCollectionTests,\n  updateCollectionDocs,\n  updateCollectionProxy,\n  updateCollectionClientCertificates,\n  updateCollectionPresets,\n  updateCollectionProtobuf,\n  collectionAddFileEvent,\n  collectionAddDirectoryEvent,\n  collectionChangeFileEvent,\n  collectionUnlinkFileEvent,\n  collectionUnlinkDirectoryEvent,\n  collectionAddEnvFileEvent,\n  collectionRenamedEvent,\n  resetRunResults,\n  initRunRequestEvent,\n  runRequestEvent,\n  runFolderEvent,\n  resetCollectionRunner,\n  updateRunnerTagsDetails,\n  updateRunnerConfiguration,\n  updateRequestDocs,\n  updateFolderDocs,\n  moveCollection,\n  streamDataReceived,\n  collectionAddOauth2CredentialsByUrl,\n  collectionClearOauth2CredentialsByUrlAndCredentialsId,\n  collectionClearOauth2CredentialsByCredentialsId,\n  updateFolderAuth,\n  updateFolderAuthMode,\n  addRequestTag,\n  deleteRequestTag,\n  updateCollectionTagsList,\n  updateActiveConnections,\n  runWsRequestEvent,\n  wsResponseReceived,\n  wsUpdateResponseSortOrder,\n\n  /* Response Example Actions - Start */\n  addResponseExample,\n  cloneResponseExample,\n  updateResponseExample,\n  deleteResponseExample,\n  cancelResponseExampleEdit,\n  addResponseExampleHeader,\n  updateResponseExampleHeader,\n  deleteResponseExampleHeader,\n  moveResponseExampleHeader,\n  setResponseExampleHeaders,\n  addResponseExampleParam,\n  updateResponseExampleParam,\n  deleteResponseExampleParam,\n  moveResponseExampleParam,\n  updateResponseExampleRequest,\n  updateResponseExampleMultipartFormParams,\n  updateResponseExampleFileBodyParams,\n  updateResponseExampleFormUrlEncodedParams,\n  updateResponseExampleStatusCode,\n  updateResponseExampleStatusText,\n  updateResponseExampleRequestUrl,\n  updateResponseExampleResponse,\n  updateResponseExampleDetails,\n  updateResponseExampleName,\n  updateResponseExampleDescription,\n  addResponseExampleRequestHeader,\n  updateResponseExampleRequestHeader,\n  deleteResponseExampleRequestHeader,\n  moveResponseExampleRequestHeader,\n  setResponseExampleRequestHeaders,\n  setResponseExampleParams,\n  /* Response Example Actions - End */\n  addTransientDirectory,\n  addSaveTransientRequestModal,\n  removeSaveTransientRequestModal,\n  clearAllSaveTransientRequestModals\n} = collectionsSlice.actions;\n\nexport default collectionsSlice.reducer;\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/slices/global-environments.js",
    "content": "import { createSlice } from '@reduxjs/toolkit';\nimport { uuid } from 'utils/common/index';\nimport { environmentSchema } from '@usebruno/schema';\nimport { cloneDeep, has } from 'lodash';\n\nconst initialState = {\n  globalEnvironments: [],\n  activeGlobalEnvironmentUid: null,\n  globalEnvironmentDraft: null\n};\n\nexport const globalEnvironmentsSlice = createSlice({\n  name: 'global-environments',\n  initialState,\n  reducers: {\n    updateGlobalEnvironments: (state, action) => {\n      state.globalEnvironments = action.payload?.globalEnvironments;\n      state.activeGlobalEnvironmentUid = action.payload?.activeGlobalEnvironmentUid;\n    },\n    _addGlobalEnvironment: (state, action) => {\n      const { name, uid, variables = [], color } = action.payload;\n      if (name?.length) {\n        state.globalEnvironments.push({\n          uid,\n          name,\n          variables,\n          color\n        });\n      }\n    },\n    _saveGlobalEnvironment: (state, action) => {\n      const { environmentUid: globalEnvironmentUid, variables } = action.payload;\n      if (globalEnvironmentUid) {\n        const environment = state.globalEnvironments.find((env) => env?.uid == globalEnvironmentUid);\n        if (environment) {\n          environment.variables = variables;\n        }\n      }\n    },\n    _renameGlobalEnvironment: (state, action) => {\n      const { environmentUid: globalEnvironmentUid, name } = action.payload;\n      if (globalEnvironmentUid) {\n        const environment = state.globalEnvironments.find((env) => env?.uid == globalEnvironmentUid);\n        if (environment) {\n          environment.name = name;\n        }\n      }\n    },\n    _copyGlobalEnvironment: (state, action) => {\n      const { name, uid, variables } = action.payload;\n      if (name?.length && uid) {\n        state.globalEnvironments.push({\n          uid,\n          name,\n          variables\n        });\n      }\n    },\n    _selectGlobalEnvironment: (state, action) => {\n      const { environmentUid: globalEnvironmentUid } = action.payload;\n      if (globalEnvironmentUid) {\n        const environment = state.globalEnvironments.find((env) => env?.uid == globalEnvironmentUid);\n        if (environment) {\n          state.activeGlobalEnvironmentUid = globalEnvironmentUid;\n        }\n      } else {\n        state.activeGlobalEnvironmentUid = null;\n      }\n    },\n    _deleteGlobalEnvironment: (state, action) => {\n      const { environmentUid: uid } = action.payload;\n      if (uid) {\n        state.globalEnvironments = state.globalEnvironments.filter((env) => env?.uid !== uid);\n        if (uid === state.activeGlobalEnvironmentUid) {\n          state.activeGlobalEnvironmentUid = null;\n        }\n      }\n    },\n    setGlobalEnvironmentDraft: (state, action) => {\n      const { environmentUid, variables } = action.payload;\n      state.globalEnvironmentDraft = { environmentUid, variables };\n    },\n    clearGlobalEnvironmentDraft: (state) => {\n      state.globalEnvironmentDraft = null;\n    },\n    _updateGlobalEnvironmentColor: (state, action) => {\n      const { environmentUid, color } = action.payload;\n      if (environmentUid) {\n        state.globalEnvironments = state.globalEnvironments.map((env) => env?.uid == environmentUid ? { ...env, color } : env);\n      }\n    }\n  }\n});\n\nexport const {\n  updateGlobalEnvironments,\n  _addGlobalEnvironment,\n  _saveGlobalEnvironment,\n  _renameGlobalEnvironment,\n  _copyGlobalEnvironment,\n  _selectGlobalEnvironment,\n  _deleteGlobalEnvironment,\n  _updateGlobalEnvironmentColor,\n  setGlobalEnvironmentDraft,\n  clearGlobalEnvironmentDraft\n} = globalEnvironmentsSlice.actions;\n\nconst getWorkspaceContext = (state) => {\n  const workspaceUid = state.workspaces?.activeWorkspaceUid;\n  const workspace = state.workspaces?.workspaces?.find((w) => w.uid === workspaceUid);\n  return { workspaceUid, workspacePath: workspace?.pathname };\n};\n\nexport const addGlobalEnvironment = ({ name, variables = [], color }) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const uid = uuid();\n    const environment = { name, uid, variables };\n    const { ipcRenderer } = window;\n    const state = getState();\n    const { workspaceUid, workspacePath } = getWorkspaceContext(state);\n\n    environmentSchema\n      .validate(environment)\n      .then(() => ipcRenderer.invoke('renderer:create-global-environment', { name, uid, variables, color, workspaceUid, workspacePath }))\n      .then((result) => {\n        const finalUid = result?.uid || uid;\n        const finalName = result?.name || name;\n        const finalVariables = result?.variables || variables;\n        const finalColor = result?.color || color;\n        dispatch(_addGlobalEnvironment({ name: finalName, uid: finalUid, variables: finalVariables, color: finalColor }));\n        return finalUid;\n      })\n      .then((finalUid) => dispatch(selectGlobalEnvironment({ environmentUid: finalUid })))\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const copyGlobalEnvironment = ({ name, environmentUid: baseEnvUid }) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const { workspaceUid, workspacePath } = getWorkspaceContext(state);\n    const globalEnvironments = state.globalEnvironments.globalEnvironments;\n    const baseEnv = globalEnvironments?.find((env) => env?.uid == baseEnvUid);\n    if (!baseEnv) {\n      return reject(new Error('Base environment not found'));\n    }\n    const uid = uuid();\n    const environment = { uid, name, variables: baseEnv.variables };\n    const { ipcRenderer } = window;\n\n    environmentSchema\n      .validate(environment)\n      .then(() => ipcRenderer.invoke('renderer:create-global-environment', { uid, name, variables: baseEnv.variables, workspaceUid, workspacePath }))\n      .then((result) => {\n        const finalUid = result?.uid || uid;\n        const finalName = result?.name || name;\n        const finalVariables = result?.variables || baseEnv.variables;\n        dispatch(_copyGlobalEnvironment({ name: finalName, uid: finalUid, variables: finalVariables }));\n      })\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const renameGlobalEnvironment = ({ name: newName, environmentUid }) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    const state = getState();\n    const { workspaceUid, workspacePath } = getWorkspaceContext(state);\n    const globalEnvironments = state.globalEnvironments.globalEnvironments;\n    const environment = globalEnvironments?.find((env) => env?.uid == environmentUid);\n    if (!environment) {\n      return reject(new Error('Environment not found'));\n    }\n    environmentSchema\n      .validate(environment)\n      .then(() => ipcRenderer.invoke('renderer:rename-global-environment', { name: newName, environmentUid, workspaceUid, workspacePath }))\n      .then((result) => {\n        const resolvedUid = result?.uid || environmentUid;\n        dispatch(_renameGlobalEnvironment({ name: newName, environmentUid: resolvedUid }));\n        return ipcRenderer\n          .invoke('renderer:get-global-environments', { workspaceUid, workspacePath })\n          .then((data) => {\n            dispatch(updateGlobalEnvironments(data));\n            if (resolvedUid !== environmentUid) {\n              const currentState = getState();\n              const draft = currentState.globalEnvironments.globalEnvironmentDraft;\n              if (draft?.environmentUid === environmentUid) {\n                dispatch(setGlobalEnvironmentDraft({ environmentUid: resolvedUid, variables: draft.variables }));\n              }\n            }\n            return resolvedUid;\n          });\n      })\n      .then((resolvedUid) => dispatch(_selectGlobalEnvironment({ environmentUid: resolvedUid })))\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const saveGlobalEnvironment = ({ variables, environmentUid }) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const { workspaceUid, workspacePath } = getWorkspaceContext(state);\n    const globalEnvironments = state.globalEnvironments.globalEnvironments;\n    let environment = globalEnvironments?.find((env) => env?.uid == environmentUid);\n    if (!environment) {\n      const activeUid = state.globalEnvironments?.activeGlobalEnvironmentUid;\n      const activeEnv = globalEnvironments?.find((env) => env?.uid == activeUid);\n      if (activeEnv) {\n        environment = activeEnv;\n        environmentUid = activeEnv.uid;\n      }\n    }\n\n    if (!environment) {\n      return reject(new Error('Environment not found'));\n    }\n\n    const environmentToSave = { ...environment, variables };\n    const { ipcRenderer } = window;\n\n    environmentSchema\n      .validate(environmentToSave)\n      .then(() => ipcRenderer.invoke('renderer:save-global-environment', {\n        environmentUid,\n        variables,\n        workspaceUid,\n        workspacePath\n      }))\n      .then(() => dispatch(_saveGlobalEnvironment({ environmentUid, variables })))\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const selectGlobalEnvironment = ({ environmentUid }) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    const state = getState();\n    const { workspaceUid, workspacePath } = getWorkspaceContext(state);\n\n    ipcRenderer\n      .invoke('renderer:select-global-environment', { environmentUid, workspaceUid, workspacePath })\n      .then(() => dispatch(_selectGlobalEnvironment({ environmentUid })))\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const deleteGlobalEnvironment = ({ environmentUid }) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    const state = getState();\n    const { workspaceUid, workspacePath } = getWorkspaceContext(state);\n\n    ipcRenderer\n      .invoke('renderer:delete-global-environment', { environmentUid, workspaceUid, workspacePath })\n      .then(() => dispatch(_deleteGlobalEnvironment({ environmentUid })))\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const globalEnvironmentsUpdateEvent = ({ globalEnvironmentVariables }) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    if (!globalEnvironmentVariables) resolve();\n\n    const state = getState();\n    const { workspaceUid, workspacePath } = getWorkspaceContext(state);\n    const globalEnvironments = state?.globalEnvironments?.globalEnvironments || [];\n    const environmentUid = state?.globalEnvironments?.activeGlobalEnvironmentUid;\n    const environment = globalEnvironments?.find((env) => env?.uid == environmentUid);\n\n    if (!environment || !environmentUid) {\n      return resolve();\n    }\n\n    let variables = cloneDeep(environment?.variables);\n\n    // \"globalEnvironmentVariables\" will include only the enabled variables and newly added variables created using the script.\n    // Update the value of each variable if it's present in \"globalEnvironmentVariables\", otherwise keep the existing value.\n    variables = variables?.map?.((variable) => ({\n      ...variable,\n      value: has(globalEnvironmentVariables, variable?.name)\n        ? globalEnvironmentVariables[variable?.name]\n        : variable?.value\n    }));\n\n    Object.entries(globalEnvironmentVariables)?.forEach?.(([key, value]) => {\n      const isAnExistingVariable = variables?.find((v) => v?.name == key);\n      if (!isAnExistingVariable) {\n        variables.push({\n          uid: uuid(),\n          name: key,\n          value,\n          type: 'text',\n          secret: false,\n          enabled: true\n        });\n      }\n    });\n\n    const environmentToSave = { ...environment, variables };\n\n    environmentSchema\n      .validate(environmentToSave)\n      .then(() => ipcRenderer.invoke('renderer:save-global-environment', {\n        environmentUid,\n        variables,\n        workspaceUid,\n        workspacePath\n      }))\n      .then(() => dispatch(_saveGlobalEnvironment({ environmentUid, variables })))\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const updateGlobalEnvironmentColor = (environmentUid, color) => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    const state = getState();\n    const { workspaceUid, workspacePath } = getWorkspaceContext(state);\n    ipcRenderer.invoke('renderer:update-global-environment-color', { environmentUid, color, workspaceUid, workspacePath })\n      .then(() => dispatch(_updateGlobalEnvironmentColor({ environmentUid, color })))\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport default globalEnvironmentsSlice.reducer;\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/slices/logs.js",
    "content": "import { createSlice } from '@reduxjs/toolkit';\n\nconst initialState = {\n  logs: [],\n  debugErrors: [],\n  isConsoleOpen: false,\n  activeTab: 'console',\n  filters: {\n    info: true,\n    warn: true,\n    error: true,\n    debug: true,\n    log: true\n  },\n  networkFilters: {\n    GET: true,\n    POST: true,\n    PUT: true,\n    DELETE: true,\n    PATCH: true,\n    HEAD: true,\n    OPTIONS: true\n  },\n  selectedRequest: null,\n  selectedError: null,\n  maxLogs: 1000,\n  maxDebugErrors: 500\n};\n\nexport const logsSlice = createSlice({\n  name: 'logs',\n  initialState,\n  reducers: {\n    addLog: (state, action) => {\n      const { type, args, timestamp } = action.payload;\n      const newLog = {\n        id: Date.now() + Math.random(),\n        type: type || 'log',\n        message: args ? args.join(' ') : '',\n        args: args || [],\n        timestamp: timestamp || new Date().toISOString()\n      };\n\n      state.logs.push(newLog);\n\n      if (state.logs.length > state.maxLogs) {\n        state.logs = state.logs.slice(-state.maxLogs);\n      }\n    },\n    addDebugError: (state, action) => {\n      const { message, stack, filename, lineno, colno, args, timestamp } = action.payload;\n      const newError = {\n        id: Date.now() + Math.random(),\n        message: message || 'Unknown error',\n        stack: stack,\n        filename: filename,\n        lineno: lineno,\n        colno: colno,\n        args: args || [],\n        timestamp: timestamp || new Date().toISOString()\n      };\n\n      state.debugErrors.push(newError);\n\n      if (state.debugErrors.length > state.maxDebugErrors) {\n        state.debugErrors = state.debugErrors.slice(-state.maxDebugErrors);\n      }\n    },\n    clearLogs: (state) => {\n      state.logs = [];\n    },\n    clearDebugErrors: (state) => {\n      state.debugErrors = [];\n    },\n    openConsole: (state) => {\n      state.isConsoleOpen = true;\n    },\n    closeConsole: (state) => {\n      state.isConsoleOpen = false;\n    },\n    setActiveTab: (state, action) => {\n      state.activeTab = action.payload;\n      if (action.payload !== 'network') {\n        state.selectedRequest = null;\n      }\n      if (action.payload !== 'debug') {\n        state.selectedError = null;\n      }\n    },\n    updateFilter: (state, action) => {\n      const { filterType, enabled } = action.payload;\n      state.filters[filterType] = enabled;\n    },\n    toggleAllFilters: (state, action) => {\n      const enabled = action.payload;\n      Object.keys(state.filters).forEach((key) => {\n        state.filters[key] = enabled;\n      });\n    },\n    updateNetworkFilter: (state, action) => {\n      const { method, enabled } = action.payload;\n      state.networkFilters[method] = enabled;\n    },\n    toggleAllNetworkFilters: (state, action) => {\n      const enabled = action.payload;\n      Object.keys(state.networkFilters).forEach((key) => {\n        state.networkFilters[key] = enabled;\n      });\n    },\n    setSelectedRequest: (state, action) => {\n      state.selectedRequest = action.payload;\n    },\n    clearSelectedRequest: (state) => {\n      state.selectedRequest = null;\n    },\n    setSelectedError: (state, action) => {\n      state.selectedError = action.payload;\n    },\n    clearSelectedError: (state) => {\n      state.selectedError = null;\n    }\n  }\n});\n\nexport const {\n  addLog,\n  addDebugError,\n  clearLogs,\n  clearDebugErrors,\n  openConsole,\n  closeConsole,\n  setActiveTab,\n  updateFilter,\n  toggleAllFilters,\n  updateNetworkFilter,\n  toggleAllNetworkFilters,\n  setSelectedRequest,\n  clearSelectedRequest,\n  setSelectedError,\n  clearSelectedError\n} = logsSlice.actions;\n\nexport default logsSlice.reducer;\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/slices/notifications.js",
    "content": "import toast from 'react-hot-toast';\nimport { createSlice } from '@reduxjs/toolkit';\nimport { getAppInstallDate } from 'utils/common/platform';\nimport semver from 'semver';\nconst getReadNotificationIds = () => {\n  try {\n    let readNotificationIdsString = window.localStorage.getItem('bruno.notifications.read');\n    let readNotificationIds = readNotificationIdsString ? JSON.parse(readNotificationIdsString) : [];\n    return readNotificationIds;\n  } catch (err) {\n    toast.error('An error occurred while fetching read notifications');\n    return [];\n  }\n};\n\nconst setReadNotificationsIds = (val) => {\n  try {\n    window.localStorage.setItem('bruno.notifications.read', JSON.stringify(val));\n  } catch (err) {\n    toast.error('An error occurred while setting read notifications');\n  }\n};\n\nconst initialState = {\n  loading: false,\n  notifications: [],\n  readNotificationIds: getReadNotificationIds() || []\n};\n\nexport const filterNotificationsByVersion = (notifications, currentVersion) => {\n  try {\n    if (!notifications) return [];\n\n    if (!currentVersion) return notifications;\n\n    return notifications.filter((notification) => {\n      const { minVersion, maxVersion } = notification;\n      if (!minVersion && !maxVersion) return true;\n      if (!minVersion) return semver.lte(currentVersion, maxVersion);\n      if (!maxVersion) return semver.gte(currentVersion, minVersion);\n\n      return semver.gte(currentVersion, minVersion) && semver.lte(currentVersion, maxVersion);\n    });\n  } catch (error) {\n    console.error(error);\n    return [];\n  }\n};\n\nexport const notificationSlice = createSlice({\n  name: 'notifications',\n  initialState,\n  reducers: {\n    setFetchingStatus: (state, action) => {\n      state.loading = action.payload.fetching;\n    },\n    setNotifications: (state, action) => {\n      let notifications = action.payload.notifications || [];\n      let readNotificationIds = state.readNotificationIds;\n\n      // Ignore notifications sent before the app was installed\n      let appInstalledOnDate = getAppInstallDate();\n      notifications = notifications.filter((notification) => {\n        const notificationDate = new Date(notification.date);\n        const appInstalledOn = new Date(appInstalledOnDate);\n\n        notificationDate.setHours(0, 0, 0, 0);\n        appInstalledOn.setHours(0, 0, 0, 0);\n\n        return notificationDate >= appInstalledOn;\n      });\n\n      state.notifications = notifications.map((notification) => {\n        return {\n          ...notification,\n          read: readNotificationIds.includes(notification.id)\n        };\n      });\n    },\n    markNotificationAsRead: (state, action) => {\n      const { notificationId } = action.payload;\n\n      if (state.readNotificationIds.includes(notificationId)) return;\n\n      const notification = state.notifications.find(\n        (notification) => notification.id === notificationId\n      );\n      if (!notification) return;\n\n      state.readNotificationIds.push(notificationId);\n      setReadNotificationsIds(state.readNotificationIds);\n      notification.read = true;\n    },\n    markAllNotificationsAsRead: (state) => {\n      let readNotificationIds = state.notifications.map((notification) => notification.id);\n      state.readNotificationIds = readNotificationIds;\n      setReadNotificationsIds(readNotificationIds);\n\n      state.notifications.forEach((notification) => {\n        notification.read = true;\n      });\n    }\n  }\n});\n\nexport const { setNotifications, setFetchingStatus, markNotificationAsRead, markAllNotificationsAsRead }\n  = notificationSlice.actions;\n\nexport const fetchNotifications = ({ currentVersion }) => (dispatch, getState) => {\n  return new Promise((resolve) => {\n    const { ipcRenderer } = window;\n    dispatch(setFetchingStatus(true));\n    ipcRenderer\n      .invoke('renderer:fetch-notifications')\n      .then((notifications) => {\n        notifications = filterNotificationsByVersion(notifications, currentVersion);\n        dispatch(setNotifications({ notifications }));\n        dispatch(setFetchingStatus(false));\n        resolve(notifications);\n      })\n      .catch((err) => {\n        dispatch(setFetchingStatus(false));\n        console.error(err);\n        resolve([]);\n      });\n  });\n};\n\nexport default notificationSlice.reducer;\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/slices/notifications.spec.js",
    "content": "const { filterNotificationsByVersion } = require('./notifications');\n\ndescribe('filterNotificationsByVersion - basic', () => {\n  it('should filter notifications by version', () => {\n    const notifications = [{ minVersion: '1.0.0', maxVersion: '1.1.0' }];\n    const currentVersion = '1.0.5';\n    const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);\n    expect(filteredNotifications).toEqual([{ minVersion: '1.0.0', maxVersion: '1.1.0' }]);\n  });\n\n  it('should gracefully handle no notifications', () => {\n    const notifications = [];\n    const currentVersion = '1.0.5';\n    const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);\n    expect(filteredNotifications).toEqual([]);\n  });\n\n  it('should gracefully handle notifications are undefined', () => {\n    const notifications = undefined;\n    const currentVersion = '1.0.5';\n    const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);\n    expect(filteredNotifications).toEqual([]);\n  });\n\n  it('should gracefully handle scenario when no current version is provided', () => {\n    const notifications = [{ minVersion: '1.0.0', maxVersion: '1.1.0' }];\n    const filteredNotifications = filterNotificationsByVersion(notifications);\n    expect(filteredNotifications).toEqual(notifications);\n  });\n\n  it('should gracefully handle scenario minVersion is undefined', () => {\n    const notifications = [{ minVersion: undefined, maxVersion: '1.1.0' }];\n    const currentVersion = '1.0.5';\n    const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);\n    expect(filteredNotifications).toEqual(notifications);\n  });\n\n  it('should gracefully handle scenario maxVersion is undefined', () => {\n    const notifications = [{ minVersion: '1.0.0', maxVersion: undefined }];\n    const currentVersion = '1.0.5';\n    const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);\n    expect(filteredNotifications).toEqual(notifications);\n  });\n\n  it('should gracefully handle scenario minVersion and maxVersion are undefined', () => {\n    const notifications = [{ minVersion: undefined, maxVersion: undefined }];\n    const currentVersion = '1.0.5';\n    const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);\n    expect(filteredNotifications).toEqual(notifications);\n  });\n});\n\ndescribe('filterNotificationsByVersion - semver', () => {\n  it('should filter out notifications outside version range', () => {\n    const notifications = [\n      { minVersion: '1.0.0', maxVersion: '1.1.0' }, // should be included\n      { minVersion: '2.0.0', maxVersion: '2.1.0' }, // should be filtered out\n      { minVersion: '0.5.0', maxVersion: '0.9.0' } // should be filtered out\n    ];\n    const currentVersion = '1.0.5';\n    const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);\n    expect(filteredNotifications).toEqual([\n      { minVersion: '1.0.0', maxVersion: '1.1.0' }\n    ]);\n  });\n\n  it('should handle mixed valid and invalid version ranges', () => {\n    const notifications = [\n      { minVersion: '1.0.0', maxVersion: '2.0.0' }, // should be included\n      { minVersion: '3.0.0', maxVersion: '4.0.0' }, // should be filtered out\n      { minVersion: '1.5.0', maxVersion: '1.8.0' }, // should be included\n      { minVersion: '0.1.0', maxVersion: '0.5.0' } // should be filtered out\n    ];\n    const currentVersion = '1.6.0';\n    const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);\n    expect(filteredNotifications).toEqual([\n      { minVersion: '1.0.0', maxVersion: '2.0.0' },\n      { minVersion: '1.5.0', maxVersion: '1.8.0' }\n    ]);\n  });\n\n  it('should handle edge cases of version ranges', () => {\n    const notifications = [\n      { minVersion: '1.0.0', maxVersion: '1.0.0' }, // should be included\n      { minVersion: '1.0.1', maxVersion: '2.0.0' }, // should be filtered out\n      { minVersion: '0.9.9', maxVersion: '1.0.0' } // should be included\n    ];\n    const currentVersion = '1.0.0';\n    const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);\n    expect(filteredNotifications).toEqual([\n      { minVersion: '1.0.0', maxVersion: '1.0.0' },\n      { minVersion: '0.9.9', maxVersion: '1.0.0' }\n    ]);\n  });\n});\n\ndescribe('filterNotificationsByVersion - undefined version bounds', () => {\n  it('should include notifications when minVersion is undefined and current version is below maxVersion', () => {\n    const notifications = [\n      { minVersion: undefined, maxVersion: '2.0.0' }\n    ];\n    const currentVersion = '1.5.0';\n    const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);\n    expect(filteredNotifications).toEqual(notifications);\n  });\n\n  it('should exclude notifications when minVersion is undefined and current version is above maxVersion', () => {\n    const notifications = [\n      { minVersion: undefined, maxVersion: '2.0.0' }\n    ];\n    const currentVersion = '2.1.0';\n    const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);\n    expect(filteredNotifications).toEqual([]);\n  });\n\n  it('should include notifications when maxVersion is undefined and current version is above minVersion', () => {\n    const notifications = [\n      { minVersion: '1.0.0', maxVersion: undefined }\n    ];\n    const currentVersion = '2.0.0';\n    const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);\n    expect(filteredNotifications).toEqual(notifications);\n  });\n\n  it('should exclude notifications when maxVersion is undefined and current version is below minVersion', () => {\n    const notifications = [\n      { minVersion: '1.0.0', maxVersion: undefined }\n    ];\n    const currentVersion = '0.9.0';\n    const filteredNotifications = filterNotificationsByVersion(notifications, currentVersion);\n    expect(filteredNotifications).toEqual([]);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/slices/openapi-sync.js",
    "content": "import { createSlice } from '@reduxjs/toolkit';\nimport { normalizePath } from 'utils/common/path';\n\nconst initialState = {\n  // Map of collectionUid -> { hasUpdates, diff, lastChecked, error }\n  collectionUpdates: {},\n  // Whether App level OpenAPI polling is enabled\n  pollingEnabled: true,\n  // Last poll timestamp\n  lastPollTime: null,\n  // Map of collectionUid -> { activeTab, expandedSections, expandedRows }\n  tabUiState: {},\n  // Map of collectionUid -> { title, version, endpointCount } (persists across tab navigations)\n  storedSpecMeta: {}\n};\n\nexport const openapiSyncSlice = createSlice({\n  name: 'openapiSync',\n  initialState,\n  reducers: {\n    setCollectionUpdate: (state, action) => {\n      const { collectionUid, hasUpdates, diff, error } = action.payload;\n      state.collectionUpdates[collectionUid] = {\n        hasUpdates,\n        diff,\n        error,\n        lastChecked: Date.now()\n      };\n    },\n    clearCollectionUpdate: (state, action) => {\n      const { collectionUid } = action.payload;\n      delete state.collectionUpdates[collectionUid];\n    },\n    clearCollectionState: (state, action) => {\n      const { collectionUid } = action.payload;\n      delete state.collectionUpdates[collectionUid];\n      delete state.tabUiState[collectionUid];\n      delete state.storedSpecMeta[collectionUid];\n    },\n    setStoredSpecMeta: (state, action) => {\n      const { collectionUid, title, version, endpointCount } = action.payload;\n      state.storedSpecMeta[collectionUid] = { title, version, endpointCount };\n    },\n    setPollingEnabled: (state, action) => {\n      state.pollingEnabled = action.payload;\n    },\n    setLastPollTime: (state, action) => {\n      state.lastPollTime = action.payload;\n    },\n    // UI state reducers\n    setTabUiState: (state, action) => {\n      const { collectionUid, ...uiState } = action.payload;\n      if (!state.tabUiState[collectionUid]) {\n        state.tabUiState[collectionUid] = {};\n      }\n      Object.assign(state.tabUiState[collectionUid], uiState);\n    },\n    toggleSectionExpanded: (state, action) => {\n      const { collectionUid, sectionKey } = action.payload;\n      if (!state.tabUiState[collectionUid]) {\n        state.tabUiState[collectionUid] = {};\n      }\n      if (!state.tabUiState[collectionUid].expandedSections) {\n        state.tabUiState[collectionUid].expandedSections = {};\n      }\n      const current = state.tabUiState[collectionUid].expandedSections[sectionKey];\n      state.tabUiState[collectionUid].expandedSections[sectionKey] = !current;\n    },\n    setSectionExpanded: (state, action) => {\n      const { collectionUid, sectionKey, expanded } = action.payload;\n      if (!state.tabUiState[collectionUid]) {\n        state.tabUiState[collectionUid] = {};\n      }\n      if (!state.tabUiState[collectionUid].expandedSections) {\n        state.tabUiState[collectionUid].expandedSections = {};\n      }\n      state.tabUiState[collectionUid].expandedSections[sectionKey] = expanded;\n    },\n    toggleRowExpanded: (state, action) => {\n      const { collectionUid, rowKey } = action.payload;\n      if (!state.tabUiState[collectionUid]) {\n        state.tabUiState[collectionUid] = {};\n      }\n      if (!state.tabUiState[collectionUid].expandedRows) {\n        state.tabUiState[collectionUid].expandedRows = {};\n      }\n      const current = state.tabUiState[collectionUid].expandedRows[rowKey];\n      state.tabUiState[collectionUid].expandedRows[rowKey] = !current;\n    },\n    setReviewDecision: (state, action) => {\n      const { collectionUid, endpointId, decision } = action.payload;\n      if (!state.tabUiState[collectionUid]) {\n        state.tabUiState[collectionUid] = {};\n      }\n      if (!state.tabUiState[collectionUid].reviewDecisions) {\n        state.tabUiState[collectionUid].reviewDecisions = {};\n      }\n      state.tabUiState[collectionUid].reviewDecisions[endpointId] = decision;\n    },\n    setReviewDecisions: (state, action) => {\n      const { collectionUid, decisions } = action.payload;\n      if (!state.tabUiState[collectionUid]) {\n        state.tabUiState[collectionUid] = {};\n      }\n      // Merge into existing decisions instead of replacing, so decisions\n      // for other change types (e.g., specChanges) are preserved\n      state.tabUiState[collectionUid].reviewDecisions = {\n        ...state.tabUiState[collectionUid].reviewDecisions,\n        ...decisions\n      };\n    }\n  }\n});\n\nexport const {\n  setCollectionUpdate,\n  clearCollectionUpdate,\n  clearCollectionState,\n  setPollingEnabled,\n  setTabUiState,\n  toggleSectionExpanded,\n  setSectionExpanded,\n  toggleRowExpanded,\n  setLastPollTime,\n  setReviewDecision,\n  setReviewDecisions,\n  setStoredSpecMeta\n} = openapiSyncSlice.actions;\n\n// Lightweight thunk for polling — only checks hash, no deep comparison\nexport const checkCollectionForUpdates = (collection) => async (dispatch) => {\n  if (!collection?.brunoConfig?.openapi?.[0]?.sourceUrl) {\n    return null;\n  }\n\n  try {\n    const { ipcRenderer } = window;\n    const syncConfig = collection.brunoConfig.openapi[0];\n    const result = await ipcRenderer.invoke('renderer:check-openapi-updates', {\n      collectionUid: collection.uid,\n      collectionPath: collection.pathname,\n      sourceUrl: syncConfig.sourceUrl,\n      storedSpecHash: syncConfig.specHash || null,\n      environmentContext: {\n        activeEnvironmentUid: collection.activeEnvironmentUid,\n        environments: collection.environments,\n        runtimeVariables: collection.runtimeVariables,\n        globalEnvironmentVariables: collection.globalEnvironmentVariables\n      }\n    });\n\n    dispatch(setCollectionUpdate({\n      collectionUid: collection.uid,\n      hasUpdates: result.hasUpdates || false,\n      diff: null,\n      error: result.error || null\n    }));\n\n    return result;\n  } catch (error) {\n    console.error('[OpenAPI Sync] Error checking for updates:', error);\n    dispatch(setCollectionUpdate({\n      collectionUid: collection.uid,\n      hasUpdates: false,\n      diff: null,\n      error: error.message\n    }));\n    return null;\n  }\n};\n\n// Thunk to check active workspace collections for updates (respects per-collection autoCheck and autoCheckInterval)\nexport const checkActiveWorkspaceCollectionsForUpdates = () => async (dispatch, getState) => {\n  const state = getState();\n  const collections = state.collections?.collections || [];\n  const { workspaces, activeWorkspaceUid } = state.workspaces;\n  const activeWorkspace = workspaces.find((w) => w.uid === activeWorkspaceUid);\n  const now = Date.now();\n\n  // Filter to active workspace collections that have OpenAPI sync configured and auto-check enabled\n  const syncableCollections = collections.filter((c) => {\n    if (!activeWorkspace?.collections?.some((wc) => normalizePath(wc.path) === normalizePath(c.pathname))) {\n      return false;\n    }\n    const syncConfig = c.brunoConfig?.openapi?.[0];\n    if (!syncConfig?.sourceUrl) return false;\n    if (syncConfig.autoCheck === false) return false;\n    return true;\n  });\n\n  for (const collection of syncableCollections) {\n    const syncConfig = collection.brunoConfig.openapi[0];\n    const intervalMs = (syncConfig.autoCheckInterval || 5) * 60 * 1000;\n    const lastChecked = state.openapiSync?.collectionUpdates?.[collection.uid]?.lastChecked || 0;\n\n    // Only check if enough time has elapsed since last check for this collection\n    if (now - lastChecked >= intervalMs) {\n      await dispatch(checkCollectionForUpdates(collection));\n    }\n  }\n\n  dispatch(setLastPollTime(Date.now()));\n};\n\n// Selector to get UI state for a specific collection's sync tab\nexport const selectTabUiState = (collectionUid) => (state) => {\n  return state.openapiSync?.tabUiState?.[collectionUid] || {};\n};\n\n// Selector for stored spec metadata (title, version, endpointCount)\nexport const selectStoredSpecMeta = (collectionUid) => (state) => {\n  return state.openapiSync?.storedSpecMeta?.[collectionUid] || null;\n};\n\nexport default openapiSyncSlice.reducer;\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/slices/performance.js",
    "content": "import { createSlice } from '@reduxjs/toolkit';\n\nconst initialState = {\n  systemResources: {\n    cpu: 0,\n    memory: 0,\n    pid: null,\n    uptime: 0,\n    lastUpdated: null\n  }\n};\n\nexport const performanceSlice = createSlice({\n  name: 'performance',\n  initialState,\n  reducers: {\n    updateSystemResources: (state, action) => {\n      state.systemResources = {\n        ...state.systemResources,\n        ...action.payload,\n        lastUpdated: new Date().toISOString()\n      };\n    }\n  }\n});\n\nexport const { updateSystemResources } = performanceSlice.actions;\nexport default performanceSlice.reducer;\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/slices/tabs.js",
    "content": "import { createSlice } from '@reduxjs/toolkit';\nimport filter from 'lodash/filter';\nimport find from 'lodash/find';\nimport last from 'lodash/last';\n\n// todo: errors should be tracked in each slice and displayed as toasts\n\nconst initialState = {\n  tabs: [],\n  activeTabUid: null\n};\n\nconst tabTypeAlreadyExists = (tabs, collectionUid, type) => {\n  return find(tabs, (tab) => tab.collectionUid === collectionUid && tab.type === type);\n};\n\nexport const tabsSlice = createSlice({\n  name: 'tabs',\n  initialState,\n  reducers: {\n    addTab: (state, action) => {\n      const { uid, collectionUid, type, requestPaneTab, preview, exampleUid, itemUid } = action.payload;\n\n      const nonReplaceableTabTypes = [\n        'variables',\n        'collection-runner',\n        'environment-settings',\n        'global-environment-settings',\n        'preferences',\n        'workspaceOverview',\n        'workspaceEnvironments',\n        'openapi-sync',\n        'openapi-spec'\n      ];\n\n      const existingTab = find(state.tabs, (tab) => tab.uid === uid);\n      if (existingTab) {\n        state.activeTabUid = existingTab.uid;\n        return;\n      }\n\n      if (nonReplaceableTabTypes.includes(type)) {\n        const existingTab = tabTypeAlreadyExists(state.tabs, collectionUid, type);\n        if (existingTab) {\n          state.activeTabUid = existingTab.uid;\n          return;\n        }\n      }\n\n      // Determine the default requestPaneTab based on request type\n      let defaultRequestPaneTab = 'params';\n      if (type === 'grpc-request' || type === 'ws-request') {\n        defaultRequestPaneTab = 'body';\n      } else if (type === 'graphql-request') {\n        defaultRequestPaneTab = 'query';\n      }\n\n      const lastTab = state.tabs[state.tabs.length - 1];\n      if (state.tabs.length > 0 && lastTab.preview) {\n        state.tabs[state.tabs.length - 1] = {\n          uid,\n          collectionUid,\n          requestPaneWidth: null,\n          requestPaneTab: requestPaneTab || defaultRequestPaneTab,\n          responsePaneTab: 'response',\n          responseFormat: null,\n          responseViewTab: null,\n          scriptPaneTab: null,\n          type: type || 'request',\n          preview: preview !== undefined\n            ? preview\n            : !nonReplaceableTabTypes.includes(type),\n          ...(uid ? { folderUid: uid } : {}),\n          ...(exampleUid ? { exampleUid } : {}),\n          ...(itemUid ? { itemUid } : {})\n        };\n\n        state.activeTabUid = uid;\n        return;\n      }\n\n      state.tabs.push({\n        uid,\n        collectionUid,\n        requestPaneWidth: null,\n        requestPaneTab: requestPaneTab || defaultRequestPaneTab,\n        responsePaneTab: 'response',\n        responsePaneScrollPosition: null,\n        responseFormat: null,\n        responseViewTab: null,\n        scriptPaneTab: null,\n        type: type || 'request',\n        ...(uid ? { folderUid: uid } : {}),\n        preview: preview !== undefined\n          ? preview\n          : !nonReplaceableTabTypes.includes(type),\n        ...(exampleUid ? { exampleUid } : {}),\n        ...(itemUid ? { itemUid } : {})\n      });\n      state.activeTabUid = uid;\n    },\n    focusTab: (state, action) => {\n      const { uid } = action.payload;\n      const tabExists = state.tabs.some((t) => t.uid === uid);\n      if (tabExists) {\n        state.activeTabUid = uid;\n      }\n    },\n    switchTab: (state, action) => {\n      if (!state.tabs || !state.tabs.length) {\n        state.activeTabUid = null;\n        return;\n      }\n\n      const direction = action.payload.direction;\n\n      const activeTabIndex = state.tabs.findIndex((t) => t.uid === state.activeTabUid);\n\n      let toBeActivatedTabIndex = 0;\n\n      if (direction == 'pageup') {\n        toBeActivatedTabIndex = (activeTabIndex - 1 + state.tabs.length) % state.tabs.length;\n      } else if (direction == 'pagedown') {\n        toBeActivatedTabIndex = (activeTabIndex + 1) % state.tabs.length;\n      }\n\n      state.activeTabUid = state.tabs[toBeActivatedTabIndex].uid;\n    },\n    updateRequestPaneTabWidth: (state, action) => {\n      const tab = find(state.tabs, (t) => t.uid === action.payload.uid);\n\n      if (tab) {\n        tab.requestPaneWidth = action.payload.requestPaneWidth;\n      }\n    },\n    updateRequestPaneTabHeight: (state, action) => {\n      const tab = find(state.tabs, (t) => t.uid === action.payload.uid);\n\n      if (tab) {\n        tab.requestPaneHeight = action.payload.requestPaneHeight;\n      }\n    },\n    updateRequestPaneTab: (state, action) => {\n      const tab = find(state.tabs, (t) => t.uid === action.payload.uid);\n\n      if (tab) {\n        tab.requestPaneTab = action.payload.requestPaneTab;\n      }\n    },\n    updateResponsePaneTab: (state, action) => {\n      const tab = find(state.tabs, (t) => t.uid === action.payload.uid);\n\n      if (tab) {\n        tab.responsePaneTab = action.payload.responsePaneTab;\n      }\n    },\n    updateResponsePaneScrollPosition: (state, action) => {\n      const tab = find(state.tabs, (t) => t.uid === action.payload.uid);\n\n      if (tab) {\n        tab.responsePaneScrollPosition = action.payload.scrollY;\n      }\n    },\n    updateRequestBodyScrollPosition: (state, action) => {\n      const tab = find(state.tabs, (t) => t.uid === action.payload.uid);\n\n      if (tab) {\n        tab.requestBodyScrollPosition = action.payload.scrollY;\n      }\n    },\n    updateResponseFormat: (state, action) => {\n      const tab = find(state.tabs, (t) => t.uid === action.payload.uid);\n\n      if (tab) {\n        tab.responseFormat = action.payload.responseFormat;\n      }\n    },\n    updateResponseViewTab: (state, action) => {\n      const tab = find(state.tabs, (t) => t.uid === action.payload.uid);\n\n      if (tab) {\n        tab.responseViewTab = action.payload.responseViewTab;\n      }\n    },\n    updateScriptPaneTab: (state, action) => {\n      const tab = find(state.tabs, (t) => t.uid === action.payload.uid);\n\n      if (tab) {\n        tab.scriptPaneTab = action.payload.scriptPaneTab;\n      }\n    },\n    closeTabs: (state, action) => {\n      const activeTab = find(state.tabs, (t) => t.uid === state.activeTabUid);\n      const tabUids = action.payload.tabUids || [];\n\n      const nonClosableTypes = ['workspaceOverview', 'workspaceEnvironments'];\n      state.tabs = filter(state.tabs, (t) =>\n        !tabUids.includes(t.uid) || nonClosableTypes.includes(t.type)\n      );\n\n      if (activeTab && state.tabs.length) {\n        const { collectionUid } = activeTab;\n        const activeTabStillExists = find(state.tabs, (t) => t.uid === state.activeTabUid);\n\n        // if the active tab no longer exists, set the active tab to the last tab in the list\n        // this implies that the active tab was closed\n        if (!activeTabStillExists) {\n          // load sibling tabs of the current collection\n          const siblingTabs = filter(state.tabs, (t) => t.collectionUid === collectionUid);\n\n          // if there are sibling tabs, set the active tab to the last sibling tab\n          // otherwise, set the active tab to the last tab in the list\n          if (siblingTabs && siblingTabs.length) {\n            state.activeTabUid = last(siblingTabs).uid;\n          } else {\n            state.activeTabUid = last(state.tabs).uid;\n          }\n        }\n      }\n\n      if (!state.tabs || !state.tabs.length) {\n        state.activeTabUid = null;\n      }\n    },\n    closeAllCollectionTabs: (state, action) => {\n      const { collectionUid } = action.payload;\n      const prevActiveTabUid = state.activeTabUid;\n      state.tabs = filter(state.tabs, (t) => t.collectionUid !== collectionUid);\n\n      const activeTabStillExists = state.tabs.some((t) => t.uid === prevActiveTabUid);\n      if (!activeTabStillExists) {\n        state.activeTabUid = state.tabs.length > 0 ? last(state.tabs).uid : null;\n      }\n    },\n    makeTabPermanent: (state, action) => {\n      const { uid } = action.payload;\n      const tab = find(state.tabs, (t) => t.uid === uid);\n      if (tab) {\n        tab.preview = false;\n      } else {\n        console.error('Tab not found!');\n      }\n    },\n    reorderTabs: (state, action) => {\n      const { direction, sourceUid, targetUid } = action.payload;\n      const tabs = state.tabs;\n\n      let sourceIdx, targetIdx;\n      if (direction) {\n        sourceIdx = tabs.findIndex((t) => t.uid === state.activeTabUid);\n        if (sourceIdx < 0) {\n          return;\n        }\n        targetIdx = sourceIdx + (direction === -1 ? -1 : 1);\n      } else {\n        sourceIdx = tabs.findIndex((t) => t.uid === sourceUid);\n        targetIdx = tabs.findIndex((t) => t.uid === targetUid);\n      }\n\n      const sourceBoundary = sourceIdx < 0;\n      const targetBoundary = targetIdx < 0 || targetIdx >= tabs.length;\n      if (sourceBoundary || sourceIdx === targetIdx || targetBoundary) {\n        return;\n      }\n\n      const [moved] = tabs.splice(sourceIdx, 1);\n      tabs.splice(targetIdx, 0, moved);\n\n      state.tabs = tabs;\n    }\n  }\n});\n\nexport const {\n  addTab,\n  focusTab,\n  switchTab,\n  updateRequestPaneTabWidth,\n  updateRequestPaneTabHeight,\n  updateRequestPaneTab,\n  updateResponsePaneTab,\n  updateResponsePaneScrollPosition,\n  updateRequestBodyScrollPosition,\n  updateResponseFormat,\n  updateResponseViewTab,\n  updateScriptPaneTab,\n  closeTabs,\n  closeAllCollectionTabs,\n  makeTabPermanent,\n  reorderTabs\n} = tabsSlice.actions;\n\nexport default tabsSlice.reducer;\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/slices/workspaces/actions.js",
    "content": "import path from 'utils/common/path';\nimport {\n  createWorkspace,\n  removeWorkspace,\n  setActiveWorkspace,\n  updateWorkspace,\n  removeCollectionFromWorkspace,\n  updateWorkspaceLoadingState,\n  setWorkspaceScratchCollection\n} from '../workspaces';\nimport { createCollection, openCollection, openMultipleCollections, openScratchCollectionEvent } from '../collections/actions';\nimport { removeCollection, addTransientDirectory, updateCollectionMountStatus } from '../collections';\nimport { sanitizeName } from 'utils/common/regex';\nimport { clearCollectionState } from '../openapi-sync';\nimport { updateGlobalEnvironments } from '../global-environments';\nimport { addTab, focusTab } from '../tabs';\nimport { normalizePath } from 'utils/common/path';\nimport toast from 'react-hot-toast';\n\nconst { ipcRenderer } = window;\n\nconst transformCollection = async (collection, type) => {\n  switch (type) {\n    case 'bruno': {\n      const { processBrunoCollection } = await import('utils/importers/bruno-collection');\n      return processBrunoCollection(collection);\n    }\n    case 'postman': {\n      const { postmanToBruno } = await import('utils/importers/postman-collection');\n      return postmanToBruno(collection);\n    }\n    case 'insomnia': {\n      const { convertInsomniaToBruno } = await import('utils/importers/insomnia-collection');\n      return convertInsomniaToBruno(collection);\n    }\n    case 'openapi': {\n      const { convertOpenapiToBruno } = await import('utils/importers/openapi-collection');\n      return convertOpenapiToBruno(collection);\n    }\n    case 'opencollection': {\n      const { processOpenCollection } = await import('utils/importers/opencollection');\n      return processOpenCollection(collection);\n    }\n    case 'wsdl': {\n      const { wsdlToBruno } = await import('@usebruno/converters');\n      return wsdlToBruno(collection);\n    }\n    default:\n      throw new Error(`Unsupported collection type: ${type}`);\n  }\n};\n\n/**\n * Creates a temporary workspace in Redux without touching the filesystem.\n * The workspace is only persisted to disk when the user confirms the name.\n */\nexport const createWorkspaceWithUniqueName = (location) => {\n  return async (dispatch) => {\n    const { uuid: generateUuid } = await import('utils/common');\n    const tempUid = generateUuid();\n    const name = await ipcRenderer?.invoke('renderer:find-unique-folder-name', 'Untitled Workspace', location) || 'Untitled Workspace';\n\n    dispatch(createWorkspace({\n      uid: tempUid,\n      name,\n      pathname: null,\n      collections: [],\n      isCreating: true,\n      creationLocation: location\n    }));\n\n    dispatch(updateWorkspace({ uid: tempUid, isNewlyCreated: true }));\n    await dispatch(switchWorkspace(tempUid));\n\n    return { workspaceUid: tempUid };\n  };\n};\n\n/**\n * Confirms creation of a temporary workspace by persisting it to the filesystem.\n */\nexport const confirmWorkspaceCreation = (tempWorkspaceUid, workspaceName) => {\n  return async (dispatch, getState) => {\n    const tempWorkspace = getState().workspaces.workspaces.find((w) => w.uid === tempWorkspaceUid);\n    if (!tempWorkspace) {\n      throw new Error('Temporary workspace not found');\n    }\n\n    const location = tempWorkspace.creationLocation;\n    if (!location) {\n      throw new Error('Workspace creation location not found');\n    }\n\n    const baseFolderName = sanitizeName(workspaceName);\n    const folderName = await ipcRenderer?.invoke('renderer:find-unique-folder-name', baseFolderName, location) || baseFolderName;\n\n    const result = await ipcRenderer.invoke(\n      'renderer:create-workspace',\n      workspaceName,\n      folderName,\n      location\n    );\n\n    const { workspaceUid: realUid, workspacePath, workspaceConfig } = result;\n\n    // Clean up the temp workspace's scratch collection after IPC succeeds\n    // (doing it before would leave a broken state if the IPC call fails)\n    if (tempWorkspace.scratchCollectionUid) {\n      dispatch(removeCollection({ collectionUid: tempWorkspace.scratchCollectionUid }));\n    }\n\n    // Remove the temporary workspace\n    dispatch(removeWorkspace(tempWorkspaceUid));\n\n    // Ensure the real workspace exists in Redux (the workspace-opened event may or may not have fired yet)\n    const existing = getState().workspaces.workspaces.find((w) => w.uid === realUid);\n    if (!existing) {\n      dispatch(createWorkspace({\n        uid: realUid,\n        pathname: workspacePath,\n        ...workspaceConfig\n      }));\n    }\n\n    dispatch(updateWorkspace({ uid: realUid, name: workspaceName }));\n\n    await dispatch(switchWorkspace(realUid));\n\n    return result;\n  };\n};\n\n/**\n * Cancels creation of a temporary workspace, removing it from Redux.\n * Only switches to default workspace if the temp workspace was the active one.\n */\nexport const cancelWorkspaceCreation = (tempWorkspaceUid) => {\n  return async (dispatch, getState) => {\n    const tempWorkspace = getState().workspaces.workspaces.find((w) => w.uid === tempWorkspaceUid);\n    if (!tempWorkspace) return;\n\n    // Clean up the scratch collection if one was mounted\n    if (tempWorkspace.scratchCollectionUid) {\n      dispatch(removeCollection({ collectionUid: tempWorkspace.scratchCollectionUid }));\n    }\n\n    const wasActive = getState().workspaces.activeWorkspaceUid === tempWorkspaceUid;\n    dispatch(removeWorkspace(tempWorkspaceUid));\n\n    // Only switch to default if the cancelled workspace was the active one\n    if (wasActive) {\n      const defaultWorkspace = getState().workspaces.workspaces.find((w) => w.type === 'default');\n      if (defaultWorkspace) {\n        await dispatch(switchWorkspace(defaultWorkspace.uid));\n      }\n    }\n  };\n};\n\nexport const createWorkspaceAction = (workspaceName, workspaceFolderName, workspaceLocation) => {\n  return async (dispatch) => {\n    try {\n      const result = await ipcRenderer.invoke('renderer:create-workspace',\n        workspaceName,\n        workspaceFolderName,\n        workspaceLocation);\n\n      const { workspaceConfig, workspaceUid, workspacePath } = result;\n\n      dispatch(createWorkspace({\n        uid: workspaceUid,\n        name: workspaceName,\n        pathname: workspacePath,\n        ...workspaceConfig\n      }));\n\n      await dispatch(switchWorkspace(workspaceUid));\n\n      return result;\n    } catch (error) {\n      throw error;\n    }\n  };\n};\n\nexport const openWorkspace = () => {\n  return async (dispatch) => {\n    try {\n      const workspacePath = await ipcRenderer.invoke('renderer:browse-directory');\n      if (workspacePath) {\n        const result = await ipcRenderer.invoke('renderer:open-workspace', workspacePath);\n        const { workspaceConfig, workspaceUid } = result;\n\n        dispatch(createWorkspace({\n          uid: workspaceUid,\n          pathname: workspacePath,\n          ...workspaceConfig\n        }));\n\n        await dispatch(switchWorkspace(workspaceUid));\n\n        return result;\n      }\n    } catch (error) {\n      throw error;\n    }\n  };\n};\n\nexport const openWorkspaceDialog = () => {\n  return async (dispatch) => {\n    try {\n      const result = await ipcRenderer.invoke('renderer:open-workspace-dialog');\n      if (result) {\n        const { workspaceConfig, workspaceUid } = result;\n\n        dispatch(createWorkspace({\n          uid: workspaceUid,\n          pathname: result.workspacePath,\n          ...workspaceConfig\n        }));\n\n        await dispatch(switchWorkspace(workspaceUid));\n\n        return result;\n      }\n    } catch (error) {\n      throw error;\n    }\n  };\n};\n\nexport const removeCollectionFromWorkspaceAction = (workspaceUid, collectionPath, options = {}) => {\n  return async (dispatch, getState) => {\n    try {\n      const { deleteFiles = false } = options;\n      const workspacesState = getState().workspaces;\n      const collectionsState = getState().collections;\n      const workspace = workspacesState.workspaces.find((w) => w.uid === workspaceUid);\n\n      if (!workspace) {\n        throw new Error('Workspace not found');\n      }\n\n      const normalizedCollectionPath = normalizePath(collectionPath);\n\n      const collection = collectionsState.collections.find(\n        (c) => normalizePath(c.pathname) === normalizedCollectionPath\n      );\n\n      await ipcRenderer.invoke('renderer:remove-collection-from-workspace',\n        workspaceUid,\n        workspace.pathname,\n        collectionPath,\n        { deleteFiles });\n\n      if (collection) {\n        const workspaceCollection = workspace.collections?.find(\n          (wc) => normalizePath(wc.path) === normalizedCollectionPath\n        );\n\n        if (workspaceCollection) {\n          dispatch(removeCollection({ collectionUid: collection.uid }));\n          dispatch(clearCollectionState({ collectionUid: collection.uid }));\n        }\n      }\n\n      dispatch(removeCollectionFromWorkspace({\n        workspaceUid,\n        collectionLocation: collectionPath\n      }));\n\n      return true;\n    } catch (error) {\n      throw error;\n    }\n  };\n};\n\nconst loadWorkspaceCollectionsForSwitch = async (dispatch, workspace) => {\n  const openCollectionsFunction = (collectionPaths, workspacePath) => {\n    return dispatch(openMultipleCollections(collectionPaths, { workspacePath }));\n  };\n\n  try {\n    await dispatch(loadWorkspaceCollections(workspace.uid));\n    const updatedWorkspace = await dispatch((_, getState) => getState().workspaces.workspaces.find((w) => w.uid === workspace.uid));\n\n    if (updatedWorkspace?.collections?.length > 0) {\n      const alreadyOpenCollections = await dispatch((_, getState) =>\n        getState().collections.collections.map((c) => normalizePath(c.pathname))\n      );\n\n      const collectionPaths = updatedWorkspace.collections\n        .map((wc) => wc.path)\n        .filter((p) => p && !alreadyOpenCollections.includes(normalizePath(p)));\n\n      const uniqueCollectionPaths = [...new Map(\n        collectionPaths.map((p) => [normalizePath(p), p])\n      ).values()];\n\n      if (uniqueCollectionPaths.length > 0) {\n        await openCollectionsFunction(uniqueCollectionPaths, updatedWorkspace.pathname);\n      }\n    }\n\n    // Load API specs for this workspace\n    await dispatch(loadWorkspaceApiSpecs(workspace.uid));\n  } catch (error) {\n    console.error('Failed to load workspace collections:', error);\n  }\n};\n\nexport const loadWorkspaceApiSpecs = (workspaceUid) => {\n  return async (dispatch, getState) => {\n    try {\n      const workspace = getState().workspaces.workspaces.find((w) => w.uid === workspaceUid);\n      if (!workspace || !workspace.pathname) {\n        return;\n      }\n\n      const apiSpecs = await ipcRenderer.invoke('renderer:load-workspace-apispecs', workspace.pathname);\n\n      dispatch(updateWorkspace({\n        uid: workspaceUid,\n        apiSpecs: apiSpecs\n      }));\n\n      const allApiSpecs = getState().apiSpec.apiSpecs;\n      const alreadyOpenApiSpecs = allApiSpecs.map((a) => a.pathname);\n\n      for (const apiSpec of apiSpecs) {\n        if (apiSpec.path && !alreadyOpenApiSpecs.includes(apiSpec.path)) {\n          try {\n            await ipcRenderer.invoke('renderer:open-api-spec-file', apiSpec.path, workspace.pathname);\n          } catch (error) {\n            console.error('Error opening API spec:', error);\n          }\n        }\n      }\n    } catch (error) {\n      console.error('Error loading workspace API specs:', error);\n    }\n  };\n};\n\nexport const switchWorkspace = (workspaceUid) => {\n  return async (dispatch, getState) => {\n    dispatch(setActiveWorkspace(workspaceUid));\n\n    const workspace = getState().workspaces.workspaces.find((w) => w.uid === workspaceUid);\n\n    if (!workspace) {\n      return;\n    }\n\n    try {\n      const { ipcRenderer } = window;\n\n      const result = await ipcRenderer.invoke('renderer:get-global-environments',\n        {\n          workspaceUid,\n          workspacePath: workspace.pathname\n        });\n\n      const globalEnvironments = result?.globalEnvironments || [];\n      const activeGlobalEnvironmentUid = result?.activeGlobalEnvironmentUid || null;\n\n      dispatch(updateGlobalEnvironments({ globalEnvironments, activeGlobalEnvironmentUid }));\n    } catch (error) {\n      dispatch(updateGlobalEnvironments({ globalEnvironments: [], activeGlobalEnvironmentUid: null }));\n    }\n\n    const scratchCollection = await dispatch(mountScratchCollection(workspaceUid));\n    await loadWorkspaceCollectionsForSwitch(dispatch, workspace);\n\n    if (scratchCollection?.uid) {\n      const overviewTabUid = `${scratchCollection.uid}-overview`;\n      const environmentsTabUid = `${scratchCollection.uid}-environments`;\n\n      dispatch(addTab({\n        uid: overviewTabUid,\n        collectionUid: scratchCollection.uid,\n        type: 'workspaceOverview'\n      }));\n\n      dispatch(addTab({\n        uid: environmentsTabUid,\n        collectionUid: scratchCollection.uid,\n        type: 'workspaceEnvironments'\n      }));\n\n      dispatch(focusTab({\n        uid: overviewTabUid\n      }));\n    }\n  };\n};\n\nexport const loadWorkspaceCollections = (workspaceUid, force = false) => {\n  return async (dispatch, getState) => {\n    try {\n      const workspace = getState().workspaces.workspaces.find((w) => w.uid === workspaceUid);\n      if (!workspace) {\n        throw new Error('Workspace not found');\n      }\n\n      const hasProcessedCollections = workspace.collections\n        && workspace.collections.length > 0\n        && workspace.collections.some((c) => c.path && path.isAbsolute(c.path));\n\n      if (!force && hasProcessedCollections) {\n        return workspace.collections;\n      }\n\n      dispatch(updateWorkspaceLoadingState({ workspaceUid, loadingState: 'loading' }));\n\n      let collections = [];\n\n      if (!workspace.pathname) {\n        collections = [];\n      } else {\n        const rawCollections = await ipcRenderer.invoke('renderer:load-workspace-collections', workspace.pathname);\n\n        collections = rawCollections.map((collection) => {\n          return {\n            ...collection\n          };\n        });\n      }\n\n      dispatch(updateWorkspace({\n        uid: workspaceUid,\n        collections\n      }));\n\n      dispatch(updateWorkspaceLoadingState({ workspaceUid, loadingState: 'loaded' }));\n\n      return collections;\n    } catch (error) {\n      dispatch(updateWorkspaceLoadingState({ workspaceUid, loadingState: 'error' }));\n      throw error;\n    }\n  };\n};\n\nexport const removeWorkspaceAction = (workspaceUid) => {\n  return (dispatch) => {\n    dispatch(removeWorkspace(workspaceUid));\n  };\n};\n\nexport const loadLastOpenedWorkspaces = () => {\n  return async (dispatch, getState) => {\n    try {\n      const workspaces = await ipcRenderer.invoke('renderer:get-last-opened-workspaces');\n      const currentWorkspaces = getState().workspaces.workspaces;\n      const validWorkspaceUids = new Set(workspaces.map((w) => w.uid));\n\n      for (const currentWorkspace of currentWorkspaces) {\n        if (currentWorkspace.type !== 'default' && !validWorkspaceUids.has(currentWorkspace.uid)) {\n          dispatch(removeWorkspace(currentWorkspace.uid));\n        }\n      }\n\n      for (const workspace of workspaces) {\n        const existingWorkspace = currentWorkspaces.find((w) => w.uid === workspace.uid);\n\n        if (!existingWorkspace) {\n          dispatch(createWorkspace(workspace));\n\n          if (workspace.pathname) {\n            try {\n              await ipcRenderer.invoke('renderer:start-workspace-watcher', workspace.pathname);\n            } catch (error) {\n            }\n          }\n        }\n      }\n\n      return workspaces;\n    } catch (error) {\n      throw error;\n    }\n  };\n};\n\nexport const workspaceOpenedEvent = (workspacePath, workspaceUid, workspaceConfig) => {\n  return async (dispatch, getState) => {\n    dispatch(createWorkspace({\n      uid: workspaceUid,\n      pathname: workspacePath,\n      ...workspaceConfig\n    }));\n\n    try {\n      await dispatch(loadWorkspaceCollections(workspaceUid));\n    } catch (error) {\n    }\n\n    // If this is the default workspace or no workspace is active yet, switch to it\n    const state = getState();\n    const activeWorkspaceUid = state.workspaces.activeWorkspaceUid;\n\n    if (!activeWorkspaceUid || workspaceConfig.type === 'default') {\n      dispatch(switchWorkspace(workspaceUid));\n    }\n  };\n};\n\nexport const workspaceConfigUpdatedEvent = (workspacePath, workspaceUid, workspaceConfig) => {\n  return async (dispatch, getState) => {\n    if (!workspaceConfig) {\n      return;\n    }\n\n    const { collections, apiSpecs, ...configWithoutCollections } = workspaceConfig;\n\n    dispatch(updateWorkspace({\n      uid: workspaceUid,\n      ...configWithoutCollections\n    }));\n\n    const activeWorkspaceUid = getState().workspaces.activeWorkspaceUid;\n    if (activeWorkspaceUid === workspaceUid) {\n      try {\n        await dispatch(loadWorkspaceCollections(workspaceUid, true));\n\n        const workspace = getState().workspaces.workspaces.find((w) => w.uid === workspaceUid);\n        const openCollections = getState().collections.collections.map((c) => normalizePath(c.pathname));\n\n        if (workspace?.collections?.length > 0) {\n          const newCollectionPaths = workspace.collections\n            .map((workspaceCollection) => workspaceCollection.path)\n            .filter((collectionPath) => collectionPath && !openCollections.includes(normalizePath(collectionPath)));\n\n          // Deduplicate paths to prevent \"collection already opened\" toast\n          const uniqueNewCollectionPaths = [...new Map(\n            newCollectionPaths.map((p) => [normalizePath(p), p])\n          ).values()];\n\n          if (uniqueNewCollectionPaths.length > 0) {\n            try {\n              await dispatch(openMultipleCollections(uniqueNewCollectionPaths, { workspacePath: workspace.pathname }));\n            } catch (error) {\n            }\n          }\n        }\n\n        // Load API specs when workspace config is updated\n        await dispatch(loadWorkspaceApiSpecs(workspaceUid));\n      } catch (error) {\n      }\n    }\n  };\n};\n\nexport const saveWorkspaceDocs = (workspaceUid, docs) => {\n  return async (dispatch, getState) => {\n    try {\n      const workspace = getState().workspaces.workspaces.find((w) => w.uid === workspaceUid);\n      if (!workspace) {\n        throw new Error('Workspace not found');\n      }\n\n      if (!workspace.pathname) {\n        throw new Error('Workspace path not found');\n      }\n\n      await ipcRenderer.invoke('renderer:save-workspace-docs', workspace.pathname, docs || '');\n\n      dispatch(updateWorkspace({\n        uid: workspaceUid,\n        docs: docs\n      }));\n\n      return docs;\n    } catch (error) {\n      throw error;\n    }\n  };\n};\n\nexport const createCollectionInWorkspace = (collectionName, collectionFolderName, collectionLocation, workspaceUid) => {\n  return async (dispatch, getState) => {\n    const currentWorkspace = getState().workspaces.workspaces.find((w) => w.uid === workspaceUid);\n    if (!currentWorkspace) {\n      throw new Error('Workspace not found');\n    }\n\n    const projectCollectionLocation = path.join(currentWorkspace.pathname, 'collections');\n\n    return await dispatch(createCollection(collectionName, collectionFolderName, projectCollectionLocation, {\n      workspaceId: currentWorkspace.pathname\n    }));\n  };\n};\n\nexport const openCollectionInWorkspace = () => {\n  return (dispatch) => dispatch(openCollection());\n};\n\nconst handleWorkspaceAction = async (action, workspaceUid, ...args) => {\n  try {\n    await action(workspaceUid, ...args);\n    return true;\n  } catch (error) {\n    const actionName = action.name.replace('renderer:', '').replace('-', ' ');\n    toast.error(error.message || `Failed to ${actionName} workspace`);\n    throw error;\n  }\n};\n\nexport const renameWorkspaceAction = (workspaceUid, newName) => {\n  return async (dispatch, getState) => {\n    try {\n      const { workspaces } = getState().workspaces;\n      const workspace = workspaces.find((w) => w.uid === workspaceUid);\n\n      if (!workspace) {\n        throw new Error('Workspace not found');\n      }\n\n      await handleWorkspaceAction((...args) => ipcRenderer.invoke('renderer:rename-workspace', ...args),\n        workspace.pathname,\n        newName);\n\n      dispatch(updateWorkspace({\n        uid: workspaceUid,\n        name: newName\n      }));\n    } catch (error) {\n      throw error;\n    }\n  };\n};\n\nexport const closeWorkspaceAction = (workspaceUid) => {\n  return async (dispatch, getState) => {\n    try {\n      const { workspaces } = getState().workspaces;\n      const workspace = workspaces.find((w) => w.uid === workspaceUid);\n\n      if (!workspace) {\n        throw new Error('Workspace not found');\n      }\n\n      await ipcRenderer.invoke('renderer:close-workspace', workspace.pathname);\n      dispatch(removeWorkspace(workspaceUid));\n    } catch (error) {\n      toast.error(error.message || 'Failed to close workspace');\n      throw error;\n    }\n  };\n};\n\nexport const importCollectionInWorkspace = (collection, workspaceUid, collectionLocation, type) => {\n  return async (dispatch, getState) => {\n    const currentWorkspace = getState().workspaces.workspaces.find((w) => w.uid === workspaceUid);\n\n    if (!currentWorkspace) {\n      throw new Error('Workspace not found');\n    }\n\n    const location = collectionLocation || path.join(currentWorkspace.pathname, 'collections');\n    const transformedCollection = await transformCollection(collection, type);\n    const collectionPath = await ipcRenderer.invoke('renderer:import-collection', transformedCollection, location);\n\n    const workspaceCollection = {\n      name: transformedCollection.name,\n      path: collectionPath\n    };\n\n    await ipcRenderer.invoke('renderer:add-collection-to-workspace', currentWorkspace.pathname, workspaceCollection);\n\n    return collectionPath;\n  };\n};\n\nexport const loadWorkspaceEnvironments = (workspaceUid) => {\n  return async (dispatch, getState) => {\n    try {\n      const workspace = getState().workspaces.workspaces.find((w) => w.uid === workspaceUid);\n      if (!workspace) {\n        throw new Error('Workspace not found');\n      }\n\n      const environments = await ipcRenderer.invoke('renderer:load-workspace-environments', workspace.pathname);\n\n      dispatch(updateWorkspace({\n        uid: workspaceUid,\n        environments: environments\n      }));\n\n      return environments;\n    } catch (error) {\n      throw error;\n    }\n  };\n};\n\nexport const createWorkspaceEnvironment = (workspaceUid, environmentName) => {\n  return async (dispatch, getState) => {\n    try {\n      const workspace = getState().workspaces.workspaces.find((w) => w.uid === workspaceUid);\n      if (!workspace) {\n        throw new Error('Workspace not found');\n      }\n\n      const environment = await ipcRenderer.invoke('renderer:create-workspace-environment', workspace.pathname, environmentName);\n\n      await dispatch(loadWorkspaceEnvironments(workspaceUid));\n\n      return environment;\n    } catch (error) {\n      throw error;\n    }\n  };\n};\n\nexport const deleteWorkspaceEnvironment = (workspaceUid, environmentUid) => {\n  return async (dispatch, getState) => {\n    try {\n      const workspace = getState().workspaces.workspaces.find((w) => w.uid === workspaceUid);\n      if (!workspace) {\n        throw new Error('Workspace not found');\n      }\n\n      await ipcRenderer.invoke('renderer:delete-workspace-environment', workspace.pathname, environmentUid);\n\n      await dispatch(loadWorkspaceEnvironments(workspaceUid));\n\n      return true;\n    } catch (error) {\n      throw error;\n    }\n  };\n};\n\nexport const selectWorkspaceEnvironment = (workspaceUid, environmentUid) => {\n  return async (dispatch, getState) => {\n    try {\n      const workspace = getState().workspaces.workspaces.find((w) => w.uid === workspaceUid);\n      if (!workspace) {\n        throw new Error('Workspace not found');\n      }\n\n      await ipcRenderer.invoke('renderer:select-workspace-environment', workspace.pathname, environmentUid);\n\n      dispatch(updateWorkspace({\n        uid: workspaceUid,\n        activeEnvironmentUid: environmentUid\n      }));\n\n      return true;\n    } catch (error) {\n      throw error;\n    }\n  };\n};\n\nexport const importWorkspaceEnvironment = (workspaceUid, environmentData) => {\n  return async (dispatch, getState) => {\n    try {\n      const workspace = getState().workspaces.workspaces.find((w) => w.uid === workspaceUid);\n      if (!workspace) {\n        throw new Error('Workspace not found');\n      }\n\n      const environment = await ipcRenderer.invoke('renderer:import-workspace-environment', workspace.pathname, environmentData);\n\n      await dispatch(loadWorkspaceEnvironments(workspaceUid));\n\n      return environment;\n    } catch (error) {\n      throw error;\n    }\n  };\n};\n\nexport const updateWorkspaceEnvironment = (workspaceUid, environmentUid, environmentData) => {\n  return async (dispatch, getState) => {\n    try {\n      const workspace = getState().workspaces.workspaces.find((w) => w.uid === workspaceUid);\n      if (!workspace) {\n        throw new Error('Workspace not found');\n      }\n\n      await ipcRenderer.invoke('renderer:update-workspace-environment', workspace.pathname, environmentUid, environmentData);\n\n      await dispatch(loadWorkspaceEnvironments(workspaceUid));\n\n      return true;\n    } catch (error) {\n      throw error;\n    }\n  };\n};\n\nexport const renameWorkspaceEnvironment = (workspaceUid, environmentUid, newName) => {\n  return async (dispatch, getState) => {\n    try {\n      const workspace = getState().workspaces.workspaces.find((w) => w.uid === workspaceUid);\n      if (!workspace) {\n        throw new Error('Workspace not found');\n      }\n\n      await ipcRenderer.invoke('renderer:rename-workspace-environment', workspace.pathname, environmentUid, newName);\n\n      await dispatch(loadWorkspaceEnvironments(workspaceUid));\n\n      return true;\n    } catch (error) {\n      throw error;\n    }\n  };\n};\n\nexport const copyWorkspaceEnvironment = (workspaceUid, environmentUid, newName) => {\n  return async (dispatch, getState) => {\n    try {\n      const workspace = getState().workspaces.workspaces.find((w) => w.uid === workspaceUid);\n      if (!workspace) {\n        throw new Error('Workspace not found');\n      }\n\n      const newEnvironment = await ipcRenderer.invoke('renderer:copy-workspace-environment', workspace.pathname, environmentUid, newName);\n\n      await dispatch(loadWorkspaceEnvironments(workspaceUid));\n\n      return newEnvironment;\n    } catch (error) {\n      throw error;\n    }\n  };\n};\n\nexport const exportWorkspaceAction = (workspaceUid) => {\n  return async (dispatch, getState) => {\n    try {\n      const { workspaces } = getState().workspaces;\n      const workspace = workspaces.find((w) => w.uid === workspaceUid);\n\n      if (!workspace) {\n        throw new Error('Workspace not found');\n      }\n\n      if (!workspace.pathname) {\n        throw new Error('Workspace path not found');\n      }\n\n      const result = await ipcRenderer.invoke('renderer:export-workspace', workspace.pathname, workspace.name);\n\n      if (result.canceled) {\n        return { canceled: true };\n      }\n\n      return result;\n    } catch (error) {\n      throw error;\n    }\n  };\n};\n\nexport const importWorkspaceAction = (zipFilePath, extractLocation) => {\n  return async (dispatch) => {\n    try {\n      const result = await ipcRenderer.invoke('renderer:import-workspace', zipFilePath, extractLocation);\n\n      if (result.success) {\n        dispatch(createWorkspace({\n          uid: result.workspaceUid,\n          pathname: result.workspacePath,\n          ...result.workspaceConfig\n        }));\n\n        await dispatch(switchWorkspace(result.workspaceUid));\n      }\n\n      return result;\n    } catch (error) {\n      throw error;\n    }\n  };\n};\n\nexport const saveWorkspaceDotEnvVariables = (workspaceUid, variables, filename = '.env') => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const workspace = state.workspaces.workspaces.find((w) => w.uid === workspaceUid);\n\n    if (!workspace) {\n      return reject(new Error('Workspace not found'));\n    }\n\n    if (!workspace.pathname) {\n      return reject(new Error('Workspace path not found'));\n    }\n\n    ipcRenderer\n      .invoke('renderer:save-workspace-dotenv-variables', { workspacePath: workspace.pathname, variables, filename })\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const saveWorkspaceDotEnvRaw = (workspaceUid, content, filename = '.env') => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const workspace = state.workspaces.workspaces.find((w) => w.uid === workspaceUid);\n\n    if (!workspace) {\n      return reject(new Error('Workspace not found'));\n    }\n\n    if (!workspace.pathname) {\n      return reject(new Error('Workspace path not found'));\n    }\n\n    ipcRenderer\n      .invoke('renderer:save-workspace-dotenv-raw', { workspacePath: workspace.pathname, content, filename })\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const createWorkspaceDotEnvFile = (workspaceUid, filename = '.env') => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const workspace = state.workspaces.workspaces.find((w) => w.uid === workspaceUid);\n\n    if (!workspace) {\n      return reject(new Error('Workspace not found'));\n    }\n\n    if (!workspace.pathname) {\n      return reject(new Error('Workspace path not found'));\n    }\n\n    ipcRenderer\n      .invoke('renderer:create-workspace-dotenv-file', { workspacePath: workspace.pathname, filename })\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const deleteWorkspaceDotEnvFile = (workspaceUid, filename = '.env') => (dispatch, getState) => {\n  return new Promise((resolve, reject) => {\n    const state = getState();\n    const workspace = state.workspaces.workspaces.find((w) => w.uid === workspaceUid);\n\n    if (!workspace) {\n      return reject(new Error('Workspace not found'));\n    }\n\n    if (!workspace.pathname) {\n      return reject(new Error('Workspace path not found'));\n    }\n\n    ipcRenderer\n      .invoke('renderer:delete-workspace-dotenv-file', { workspacePath: workspace.pathname, filename })\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\n// Scratch Collection Actions\n\n/**\n * Get the scratch collection for a workspace\n */\nexport const getScratchCollection = (workspaceUid) => {\n  return (dispatch, getState) => {\n    const state = getState();\n    const workspace = state.workspaces.workspaces.find((w) => w.uid === workspaceUid);\n    if (!workspace?.scratchCollectionUid) {\n      return null;\n    }\n    return state.collections.collections.find((c) => c.uid === workspace.scratchCollectionUid);\n  };\n};\n\n/**\n * Mount scratch collection for a workspace\n */\nexport const mountScratchCollection = (workspaceUid) => {\n  return async (dispatch, getState) => {\n    const state = getState();\n    const workspace = state.workspaces.workspaces.find((w) => w.uid === workspaceUid);\n\n    if (!workspace) {\n      return null;\n    }\n\n    if (workspace.scratchCollectionUid) {\n      const existingCollection = state.collections.collections.find(\n        (c) => c.uid === workspace.scratchCollectionUid\n      );\n      if (existingCollection) {\n        return existingCollection;\n      }\n    }\n\n    try {\n      const tempDirectoryPath = await ipcRenderer.invoke('renderer:mount-workspace-scratch', {\n        workspaceUid,\n        workspacePath: workspace.pathname || 'default'\n      });\n\n      const { generateUidBasedOnHash } = await import('utils/common');\n      const scratchCollectionUid = generateUidBasedOnHash(tempDirectoryPath);\n\n      const brunoConfig = {\n        opencollection: '1.0.0',\n        name: 'Scratch',\n        type: 'collection',\n        ignore: ['node_modules', '.git']\n      };\n\n      await ipcRenderer.invoke('renderer:add-collection-watcher', {\n        collectionPath: tempDirectoryPath,\n        collectionUid: scratchCollectionUid,\n        brunoConfig\n      });\n\n      await dispatch(openScratchCollectionEvent(scratchCollectionUid, tempDirectoryPath, brunoConfig));\n\n      dispatch(setWorkspaceScratchCollection({\n        workspaceUid,\n        scratchCollectionUid,\n        scratchTempDirectory: tempDirectoryPath\n      }));\n\n      dispatch(addTransientDirectory({\n        collectionUid: scratchCollectionUid,\n        pathname: tempDirectoryPath\n      }));\n\n      dispatch(updateCollectionMountStatus({ collectionUid: scratchCollectionUid, mountStatus: 'mounted' }));\n\n      return { uid: scratchCollectionUid, pathname: tempDirectoryPath };\n    } catch (error) {\n      console.error('Error mounting scratch collection:', error);\n      if (workspace.scratchCollectionUid) {\n        dispatch(updateCollectionMountStatus({ collectionUid: workspace.scratchCollectionUid, mountStatus: 'unmounted' }));\n      }\n      return null;\n    }\n  };\n};\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/slices/workspaces/getTabToFocusForCurrentWorkspace.js",
    "content": "import filter from 'lodash/filter';\nimport find from 'lodash/find';\nimport last from 'lodash/last';\nimport { normalizePath } from 'utils/common/path';\n\n/**\n * Returns the set of collection UIDs that belong to the given workspace\n * (scratch collection + collections whose path is in workspace.collections).\n */\nexport function getWorkspaceCollectionUids(state, workspace) {\n  if (!workspace) {\n    return new Set();\n  }\n  const uids = new Set();\n  if (workspace.scratchCollectionUid) {\n    uids.add(workspace.scratchCollectionUid);\n  }\n  const workspacePaths = new Set(\n    (workspace.collections || [])\n      .filter((wc) => wc.path)\n      .map((wc) => normalizePath(wc.path))\n  );\n  state.collections?.collections?.forEach((c) => {\n    if (!c.pathname) return;\n    if (workspacePaths.has(normalizePath(c.pathname))) {\n      uids.add(c.uid);\n    }\n  });\n  return uids;\n}\n\n/**\n * Returns the tab to focus so the active tab is in the current workspace, or null if no change needed.\n * Returns { uid } or { uid, addOverviewFirst: true, scratchCollectionUid }.\n */\nexport function getTabToFocusForCurrentWorkspace(state) {\n  const activeTabUid = state.tabs?.activeTabUid;\n  if (!activeTabUid || !state.tabs?.tabs?.length) {\n    return null;\n  }\n  const activeTab = find(state.tabs.tabs, (t) => t.uid === activeTabUid);\n  if (!activeTab) {\n    return null;\n  }\n  const activeWorkspace = state.workspaces?.workspaces?.find(\n    (w) => w.uid === state.workspaces?.activeWorkspaceUid\n  );\n  if (!activeWorkspace) {\n    return null;\n  }\n  const workspaceCollectionUids = getWorkspaceCollectionUids(state, activeWorkspace);\n  if (workspaceCollectionUids.has(activeTab.collectionUid)) {\n    return null;\n  }\n  const inWorkspaceTabs = filter(state.tabs.tabs, (t) => workspaceCollectionUids.has(t.collectionUid));\n  if (inWorkspaceTabs.length > 0) {\n    return { uid: last(inWorkspaceTabs).uid };\n  }\n  const scratchCollectionUid = activeWorkspace.scratchCollectionUid;\n  if (!scratchCollectionUid) {\n    return null; // No tabs in current workspace and no scratch; cannot focus a valid tab.\n  }\n  const overviewTabUid = `${scratchCollectionUid}-overview`;\n  const overviewTabExists = state.tabs.tabs.some((t) => t.uid === overviewTabUid);\n  if (overviewTabExists) {\n    return { uid: overviewTabUid };\n  }\n  return { uid: overviewTabUid, addOverviewFirst: true, scratchCollectionUid };\n}\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/slices/workspaces/getTabToFocusForCurrentWorkspace.spec.js",
    "content": "import { getTabToFocusForCurrentWorkspace } from './getTabToFocusForCurrentWorkspace';\n\nfunction buildState(overrides = {}) {\n  const wsA = {\n    uid: 'workspace-a',\n    scratchCollectionUid: 'scratch-a',\n    collections: [{ path: '/path/col-a' }]\n  };\n  const wsB = {\n    uid: 'workspace-b',\n    scratchCollectionUid: 'scratch-b',\n    collections: [{ path: '/path/col-b' }]\n  };\n  const colA = { uid: 'col-a', pathname: '/path/col-a' };\n  const colB = { uid: 'col-b', pathname: '/path/col-b' };\n\n  return {\n    tabs: {\n      activeTabUid: null,\n      tabs: []\n    },\n    workspaces: {\n      activeWorkspaceUid: 'workspace-b',\n      workspaces: [wsA, wsB]\n    },\n    collections: {\n      collections: [colA, colB]\n    },\n    ...overrides\n  };\n}\n\ndescribe('getTabToFocusForCurrentWorkspace', () => {\n  it('returns null when there is no active tab', () => {\n    const state = buildState();\n    expect(getTabToFocusForCurrentWorkspace(state)).toBeNull();\n  });\n\n  it('returns null when there are no tabs', () => {\n    const state = buildState({ tabs: { activeTabUid: 'tab-1', tabs: [] } });\n    expect(getTabToFocusForCurrentWorkspace(state)).toBeNull();\n  });\n\n  it('returns null when active tab is already in current workspace', () => {\n    const state = buildState({\n      tabs: {\n        activeTabUid: 'req-b',\n        tabs: [\n          { uid: 'req-a', collectionUid: 'col-a' },\n          { uid: 'req-b', collectionUid: 'col-b' }\n        ]\n      }\n    });\n    expect(getTabToFocusForCurrentWorkspace(state)).toBeNull();\n  });\n\n  it('returns in-workspace tab when active tab is from another workspace', () => {\n    const state = buildState({\n      tabs: {\n        activeTabUid: 'req-a',\n        tabs: [\n          { uid: 'req-a', collectionUid: 'col-a' },\n          { uid: 'req-b', collectionUid: 'col-b' },\n          { uid: 'scratch-b-overview', collectionUid: 'scratch-b', type: 'workspaceOverview' }\n        ]\n      }\n    });\n    const result = getTabToFocusForCurrentWorkspace(state);\n    expect(result).not.toBeNull();\n    expect(result.uid).toBe('scratch-b-overview');\n    expect(result.addOverviewFirst).toBeUndefined();\n  });\n\n  it('returns last in-workspace tab when multiple request tabs exist in current workspace', () => {\n    const state = buildState({\n      tabs: {\n        activeTabUid: 'req-a',\n        tabs: [\n          { uid: 'req-a', collectionUid: 'col-a' },\n          { uid: 'req-b1', collectionUid: 'col-b' },\n          { uid: 'req-b2', collectionUid: 'col-b' }\n        ]\n      }\n    });\n    const result = getTabToFocusForCurrentWorkspace(state);\n    expect(result).not.toBeNull();\n    expect(result.uid).toBe('req-b2');\n  });\n\n  it('treats active tab with no collectionUid as not in workspace and returns in-workspace tab', () => {\n    const state = buildState({\n      tabs: {\n        activeTabUid: 'malformed',\n        tabs: [\n          { uid: 'malformed' },\n          { uid: 'req-b', collectionUid: 'col-b' }\n        ]\n      }\n    });\n    const result = getTabToFocusForCurrentWorkspace(state);\n    expect(result).not.toBeNull();\n    expect(result.uid).toBe('req-b');\n  });\n\n  it('returns overview with addOverviewFirst when no in-workspace tabs and overview missing', () => {\n    const state = buildState({\n      tabs: {\n        activeTabUid: 'req-a',\n        tabs: [\n          { uid: 'req-a', collectionUid: 'col-a' }\n        ]\n      }\n    });\n    const result = getTabToFocusForCurrentWorkspace(state);\n    expect(result).not.toBeNull();\n    expect(result.uid).toBe('scratch-b-overview');\n    expect(result.addOverviewFirst).toBe(true);\n    expect(result.scratchCollectionUid).toBe('scratch-b');\n  });\n\n  it('returns null when no in-workspace tabs and no scratch', () => {\n    const wsBNoScratch = {\n      uid: 'workspace-b',\n      scratchCollectionUid: null,\n      collections: [{ path: '/path/col-b' }]\n    };\n    const state = buildState({\n      workspaces: {\n        activeWorkspaceUid: 'workspace-b',\n        workspaces: [\n          { uid: 'workspace-a', scratchCollectionUid: 'scratch-a', collections: [{ path: '/path/col-a' }] },\n          wsBNoScratch\n        ]\n      },\n      tabs: {\n        activeTabUid: 'req-a',\n        tabs: [{ uid: 'req-a', collectionUid: 'col-a' }]\n      }\n    });\n    expect(getTabToFocusForCurrentWorkspace(state)).toBeNull();\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/providers/ReduxStore/slices/workspaces/index.js",
    "content": "import { createSlice } from '@reduxjs/toolkit';\nimport { normalizePath } from 'utils/common/path';\n\nconst DEFAULT_WORKSPACE_UID = 'default';\n\nconst initialState = {\n  workspaces: [],\n  activeWorkspaceUid: DEFAULT_WORKSPACE_UID\n};\n\nexport const workspacesSlice = createSlice({\n  name: 'workspaces',\n  initialState,\n  reducers: {\n    setActiveWorkspace: (state, action) => {\n      state.activeWorkspaceUid = action.payload;\n    },\n\n    createWorkspace: (state, action) => {\n      const workspace = action.payload;\n      workspace.collections = workspace.collections || [];\n\n      const existingWorkspace = state.workspaces.find((w) => w.uid === workspace.uid);\n      if (!existingWorkspace) {\n        state.workspaces.push(workspace);\n      } else {\n        Object.assign(existingWorkspace, workspace);\n      }\n    },\n\n    removeWorkspace: (state, action) => {\n      const workspaceUid = action.payload;\n      state.workspaces = state.workspaces.filter((w) => w.uid !== workspaceUid);\n\n      if (state.activeWorkspaceUid === workspaceUid) {\n        state.activeWorkspaceUid = DEFAULT_WORKSPACE_UID;\n      }\n    },\n\n    updateWorkspace: (state, action) => {\n      const { uid, ...updates } = action.payload;\n      const workspace = state.workspaces.find((w) => w.uid === uid);\n      if (workspace) {\n        Object.assign(workspace, updates);\n      }\n    },\n\n    addCollectionToWorkspace: (state, action) => {\n      const { workspaceUid, collection } = action.payload;\n      const workspace = state.workspaces.find((w) => w.uid === workspaceUid);\n      if (workspace) {\n        workspace.collections = workspace.collections || [];\n        const existingCollection = workspace.collections.find((c) =>\n          c.uid === collection.uid || c.path === collection.path);\n        if (!existingCollection) {\n          workspace.collections.push(collection);\n        }\n      }\n    },\n\n    removeCollectionFromWorkspace: (state, action) => {\n      const { workspaceUid, collectionLocation } = action.payload;\n      const workspace = state.workspaces.find((w) => w.uid === workspaceUid);\n      if (workspace?.collections) {\n        const normalizedLocation = normalizePath(collectionLocation);\n        workspace.collections = workspace.collections.filter((c) => {\n          const normalizedPath = normalizePath(c.path);\n          return normalizedPath !== normalizedLocation;\n        });\n      }\n    },\n\n    updateWorkspaceLoadingState: (state, action) => {\n      const { workspaceUid, loadingState } = action.payload;\n      const workspace = state.workspaces.find((w) => w.uid === workspaceUid);\n      if (workspace) {\n        workspace.loadingState = loadingState;\n      }\n    },\n\n    workspaceDotEnvUpdateEvent: (state, action) => {\n      const { workspaceUid, processEnvVariables } = action.payload;\n      const workspace = state.workspaces.find((w) => w.uid === workspaceUid);\n      if (workspace) {\n        workspace.processEnvVariables = processEnvVariables;\n      }\n    },\n\n    setWorkspaceDotEnvVariables: (state, action) => {\n      const { workspaceUid, variables, exists, filename = '.env' } = action.payload;\n      const workspace = state.workspaces.find((w) => w.uid === workspaceUid);\n\n      if (workspace) {\n        if (!workspace.dotEnvFiles) {\n          workspace.dotEnvFiles = [];\n        }\n\n        const existingIndex = workspace.dotEnvFiles.findIndex((f) => f.filename === filename);\n        if (existingIndex >= 0) {\n          if (exists) {\n            workspace.dotEnvFiles[existingIndex] = { filename, variables, exists };\n          } else {\n            workspace.dotEnvFiles.splice(existingIndex, 1);\n          }\n        } else if (exists) {\n          workspace.dotEnvFiles.push({ filename, variables, exists });\n        }\n\n        workspace.dotEnvFiles.sort((a, b) => {\n          if (a.filename === '.env') return -1;\n          if (b.filename === '.env') return 1;\n          return a.filename.localeCompare(b.filename);\n        });\n\n        const mainEnvFile = workspace.dotEnvFiles.find((f) => f.filename === '.env');\n        workspace.dotEnvVariables = mainEnvFile?.variables || [];\n        workspace.dotEnvExists = mainEnvFile?.exists || false;\n      }\n    },\n\n    // Set scratch collection info on workspace\n    setWorkspaceScratchCollection: (state, action) => {\n      const { workspaceUid, scratchCollectionUid, scratchTempDirectory } = action.payload;\n      const workspace = state.workspaces.find((w) => w.uid === workspaceUid);\n      if (workspace) {\n        workspace.scratchCollectionUid = scratchCollectionUid;\n        workspace.scratchTempDirectory = scratchTempDirectory;\n      }\n    }\n  }\n});\n\nexport const {\n  setActiveWorkspace,\n  createWorkspace,\n  removeWorkspace,\n  updateWorkspace,\n  addCollectionToWorkspace,\n  removeCollectionFromWorkspace,\n  updateWorkspaceLoadingState,\n  workspaceDotEnvUpdateEvent,\n  setWorkspaceDotEnvVariables,\n  setWorkspaceScratchCollection\n} = workspacesSlice.actions;\n\nexport default workspacesSlice.reducer;\n"
  },
  {
    "path": "packages/bruno-app/src/providers/Theme/index.js",
    "content": "import React from 'react';\nimport { Validator } from 'jsonschema';\nimport toast from 'react-hot-toast';\nimport themes from 'themes/index';\nimport themeSchema from 'themes/schema';\nimport useLocalStorage from 'hooks/useLocalStorage/index';\n\nimport { createContext, useContext, useEffect, useState, useMemo } from 'react';\nimport { ThemeProvider as SCThemeProvider } from 'styled-components';\n\nconst validator = new Validator();\n\n// Helper: Get effective theme ('light' or 'dark') based on storedTheme\nconst getEffectiveTheme = (storedTheme) => {\n  if (storedTheme === 'system') {\n    return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';\n  }\n  return storedTheme;\n};\n\n// Helper: Apply theme class to root element\nconst applyThemeToRoot = (theme) => {\n  const root = window.document.documentElement;\n  root.classList.remove('light', 'dark');\n  root.classList.add(theme);\n};\n\nexport const ThemeContext = createContext();\nexport const ThemeProvider = (props) => {\n  const [storedTheme, setStoredTheme] = useLocalStorage('bruno.theme', 'system');\n  const [displayedTheme, setDisplayedTheme] = useState(() => getEffectiveTheme(storedTheme));\n  const [themeVariantLight, setThemeVariantLight] = useLocalStorage('bruno.themeVariantLight', 'light');\n  const [themeVariantDark, setThemeVariantDark] = useLocalStorage('bruno.themeVariantDark', 'dark');\n\n  // Listen for system theme changes (only affects 'system' mode)\n  useEffect(() => {\n    const mediaQuery = window.matchMedia('(prefers-color-scheme: light)');\n    const handleChange = (e) => {\n      if (storedTheme !== 'system') return;\n      const newTheme = e.matches ? 'light' : 'dark';\n      setDisplayedTheme(newTheme);\n      applyThemeToRoot(newTheme);\n    };\n    mediaQuery.addEventListener('change', handleChange);\n    return () => mediaQuery.removeEventListener('change', handleChange);\n  }, [storedTheme]);\n\n  // Apply theme when storedTheme changes\n  useEffect(() => {\n    const effectiveTheme = getEffectiveTheme(storedTheme);\n    setDisplayedTheme(effectiveTheme);\n    applyThemeToRoot(effectiveTheme);\n\n    if (window.ipcRenderer) {\n      window.ipcRenderer.send('renderer:theme-change', storedTheme);\n    }\n  }, [storedTheme]);\n\n  // storedTheme can have 3 values: 'light', 'dark', 'system'\n  // displayedTheme can have 2 values: 'light', 'dark'\n\n  // Compute theme object directly from storedTheme to avoid race conditions\n  const theme = useMemo(() => {\n    const isLightMode = getEffectiveTheme(storedTheme) === 'light';\n    const variantName = isLightMode ? themeVariantLight : themeVariantDark;\n    const fallbackTheme = isLightMode ? themes.light : themes.dark;\n    const fallbackName = isLightMode ? 'light' : 'dark';\n\n    // Check if the variant exists in themes\n    const selectedTheme = themes[variantName];\n    if (!selectedTheme) {\n      // Only show toast if using a non-default variant that doesn't exist\n      if (variantName !== fallbackName) {\n        toast.error(`Theme \"${variantName}\" not found. Using default ${fallbackName} theme.`, {\n          duration: 4000,\n          id: `theme-not-found-${variantName}` // Prevent duplicate toasts\n        });\n      }\n      return fallbackTheme;\n    }\n\n    // Validate the theme against the schema\n    const validationResult = validator.validate(selectedTheme, themeSchema);\n    if (!validationResult.valid) {\n      const errors = validationResult.errors?.map((e) => e.stack).join(', ') || 'Unknown validation error';\n      console.error(`Theme \"${variantName}\" validation failed:`, errors);\n      toast.error(`Invalid theme \"${variantName}\". Using default ${fallbackName} theme.`, {\n        duration: 4000,\n        id: `theme-invalid-${variantName}` // Prevent duplicate toasts\n      });\n      return fallbackTheme;\n    }\n\n    return selectedTheme;\n  }, [storedTheme, themeVariantLight, themeVariantDark]);\n\n  const value = {\n    theme,\n    storedTheme,\n    displayedTheme,\n    setStoredTheme,\n    themeVariantLight,\n    setThemeVariantLight,\n    themeVariantDark,\n    setThemeVariantDark\n  };\n\n  return (\n    <ThemeContext.Provider value={value}>\n      <SCThemeProvider theme={theme} {...props} />\n    </ThemeContext.Provider>\n  );\n};\n\nexport const useTheme = () => {\n  const context = useContext(ThemeContext);\n\n  if (context === undefined) {\n    throw new Error(`useTheme must be used within a ThemeProvider`);\n  }\n\n  return context;\n};\n\nexport default ThemeProvider;\n"
  },
  {
    "path": "packages/bruno-app/src/providers/Toaster/index.js",
    "content": "import React from 'react';\nimport { Toaster } from 'react-hot-toast';\nimport { useTheme } from 'providers/Theme';\nimport { isPlaywright } from 'utils/common';\n\nexport const ToastContext = React.createContext();\n\nexport const ToastProvider = (props) => {\n  const { theme, displayedTheme } = useTheme();\n\n  const toastOptions = {\n    duration: isPlaywright() ? 500 : 2000,\n    style: {\n      // Break long word like file-path, URL etc. to prevent overflow\n      overflowWrap: 'anywhere',\n      borderRadius: theme.border.radius.lg,\n      background: displayedTheme === 'light'\n        ? theme.background.base\n        : theme.background.crust,\n      color: theme.text\n    }\n  };\n\n  return (\n    <ToastContext.Provider {...props} value=\"toastProvider\">\n      <Toaster toastOptions={toastOptions} />\n      <div>{props.children}</div>\n    </ToastContext.Provider>\n  );\n};\n\nexport default ToastProvider;\n"
  },
  {
    "path": "packages/bruno-app/src/selectors/tab.js",
    "content": "import { createSelector } from '@reduxjs/toolkit';\n\nexport const isTabForItemActive = ({ itemUid }) => createSelector([\n  (state) => state.tabs?.activeTabUid\n], (activeTabUid) => activeTabUid === itemUid);\n\nexport const isTabForItemPresent = ({ itemUid }) => createSelector([\n  (state) => state.tabs.tabs\n], (tabs) => tabs.some((tab) => tab.uid === itemUid));\n"
  },
  {
    "path": "packages/bruno-app/src/styles/globals.css",
    "content": "@import 'tailwindcss/base';\n@import 'tailwindcss/components';\n@import 'tailwindcss/utilities';\n\n:root {\n  --color-brand: #546de5;\n  --color-text: rgb(52 52 52);\n  --color-sidebar-collection-item-active-indent-border: #d0d0d0;\n  --color-sidebar-background: #f3f3f3;\n  --color-request-dragbar-background: #efefef;\n  --color-request-dragbar-background-active: rgb(200, 200, 200);\n  --color-tab-inactive: rgb(155 155 155);\n  --color-tab-active-border: #546de5;\n  --color-layout-border: #dedede;\n  --color-text-danger: rgb(185, 28, 28);\n  --color-background-danger: #dc3545;\n  --color-method-get: rgb(5, 150, 105);\n  --color-method-post: #8e44ad;\n  --color-method-put: #ca7811;\n  --color-method-delete: rgb(185, 28, 28);\n  --color-method-patch: rgb(52 52 52);\n  --color-method-options: rgb(52 52 52);\n  --color-method-head: rgb(52 52 52);\n}\n\n:root,.graphiql-container,.CodeMirror-info,.CodeMirror-lint-tooltip,reach-portal {\n  /* Required CSS variables after upgrading GraphiQL from v1.5.9 to v2.4.7 */\n  /* Colors */\n  --color-primary: 0, 0%, 0% !important;\n  --color-secondary: 0, 0%, 0% !important;\n  --color-tertiary: 0, 0%, 0% !important;\n  --color-info: 0, 0%, 0% !important;\n  --color-success: 0, 0%, 0% !important;\n  --color-warning: 0, 0%, 0% !important;\n  --color-error: 0, 0%, 0% !important;\n  --color-neutral: 0, 0%, 0% !important;\n  --color-base: 0, 0%, 100% !important;\n\n  /* Color alpha values */\n  --alpha-secondary: 0.76 !important;\n  --alpha-tertiary: 0.5 !important;\n  --alpha-background-heavy: 0.15 !important;\n  --alpha-background-medium: 0.1 !important;\n  --alpha-background-light: 0.07 !important;\n  \n  --font-family: Consolas,Inconsolata,Droid Sans Mono,Monaco,monospace;\n  --font-family-mono: 'Fira Code', monospace;\n  --font-size-hint: .75rem;\n  --font-size-inline-code: .8125rem;\n  --font-size-body: 1rem; \n  --font-size-h4: 1.125rem;\n  --font-size-h3: 1.375rem;\n  --font-size-h2: 1.8125rem;\n  --font-weight-regular: 400;\n  --font-weight-medium: 500;\n  --line-height: 1.5;\n  --px-2: 0px;\n  --px-4: 0px;\n  --px-6: 2px;\n  --px-8: 8px;\n  --px-10: 10px;\n  --px-12: 12px;\n  --px-16: 16px;\n  --px-20: 20px;\n  --px-24: 24px;\n  --border-radius-2: 0px !important;\n  --border-radius-4: 0px !important;\n  --border-radius-8: 0px !important;\n  --border-radius-12: 0px !important;\n  --popover-box-shadow: 0px 0px 1px #000 !important;\n  --popover-border: none;\n  --sidebar-width: 60px;\n  --toolbar-width: 40px;\n  --session-header-height: 51px\n}\n\n/* Required CSS variables after upgrading GraphiQL from v1.5.9 to v2.4.7 */\n.graphiql-container, .CodeMirror-info, .CodeMirror-lint-tooltip, reach-portal {\n  /* General Colors */\n  --color-primary: 0, 0%, 0% !important;\n  --color-secondary: 0, 0%, 0% !important;\n  --color-tertiary: 0, 0%, 0% !important;\n  --color-info: 0, 0%, 0% !important;\n  --color-success: 0, 0%, 0% !important;\n  --color-warning: 0, 0%, 0% !important;\n  --color-error: 0, 0%, 0% !important;\n  --color-base: 0, 0%, 100% !important;\n  --color-neutral: 0, 0%, 60% !important;\n  \n  /* Color alpha values */\n  --alpha-secondary: 0.76 !important;\n  --alpha-tertiary: 0.5 !important;\n  --alpha-background-heavy: 0.15 !important;\n  --alpha-background-medium: 0.1 !important;\n  --alpha-background-light: 0.07 !important;\n  \n  --font-family: Consolas,Inconsolata,Droid Sans Mono,Monaco,monospace;\n  --font-family-mono: 'Fira Code', monospace;\n  --font-size-hint: .75rem;\n  --font-size-inline-code: .8125rem;\n  --font-size-body: 1rem; \n  --font-size-h4: 1.125rem;\n  --font-size-h3: 1.375rem;\n  --font-size-h2: 1.8125rem;\n  --font-weight-regular: 400;\n  --font-weight-medium: 500;\n  --line-height: 1.5;\n  --px-2: 2px !important;\n  --px-4: 4px !important;\n  --px-6: 6px !important;\n  --px-8: 8px !important;\n  --px-10: 10px !important;\n  --px-12: 12px !important;\n  --px-16: 16px !important;\n  --px-20: 20px !important;\n  --px-24: 24px !important;\n  --border-radius-2: 2px !important;\n  --border-radius-4: 2px !important;\n  --border-radius-8: 2px !important;\n  --border-radius-12: 2px !important;\n  --popover-box-shadow: 0px 0px 1px #000 !important;\n  --popover-border: none;\n  --sidebar-width: 60px;\n  --toolbar-width: 40px;\n  --session-header-height: 51px\n}\n\n.CodeMirror-dialog {\n  --px-4: 0px !important;\n  --px-12: 2px !important;\n}\n\n.CodeMirror-hints {\n  z-index: 20 !important;\n}\n\n.graphiql-container {\n  background: transparent !important;\n}\n\nhtml,\nbody {\n  margin: 0;\n  padding: 0;\n  font-size: 1rem;\n  color: rgb(52 52 52);\n\n  font-kerning: none;\n  text-rendering: optimizeSpeed;\n  letter-spacing: normal;\n  font-family: Inter, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif !important;\n  overflow-x: hidden;\n}\n\nbody::-webkit-scrollbar,\n.CodeMirror-vscrollbar::-webkit-scrollbar {\n  width: 0.6rem;\n}\n\nbody::-webkit-scrollbar-track,\n.CodeMirror-vscrollbar::-webkit-scrollbar-track {\n  background-color: #f1f1f1;\n}\n\nbody::-webkit-scrollbar-thumb,\n.CodeMirror-vscrollbar::-webkit-scrollbar-thumb {\n  background-color: #cdcdcd;\n  border-radius: 5rem;\n}\n\n/*\n * Mac-specific scrollbar styling\n * This ensures that scrollbars are only visible when the user starts to scroll,\n * providing a cleaner and more minimalistic appearance.\n */\nbody.os-mac * {\n  scrollbar-width: thin;\n}\n\n/*\n * todo: this will be supported in the future to be changed via applying a theme\n * making all the checkboxes and radios bigger\n * input[type='checkbox'],\n * input[type='radio'] {\n * transform: scale(1.1);\n * }\n */\n"
  },
  {
    "path": "packages/bruno-app/src/test-utils/mocks/codemirror.js",
    "content": "const CodeMirror = jest.fn((node, options) => {\n  const editor = {\n    options,\n    _currentValue: '',\n    _onKeyUpMockDataHints: null,\n    getCursor: jest.fn(() => ({ line: 0, ch: editor._currentValue?.length || 0 })),\n    getRange: jest.fn((from, to) => editor._currentValue?.slice(0, to.ch) || ''),\n    getValue: jest.fn(() => editor._currentValue),\n    setValue: jest.fn(function (val) {\n      editor._currentValue = val;\n    }),\n    getLine: jest.fn(() => editor._currentValue || ''),\n    setOption: jest.fn(),\n    refresh: jest.fn(),\n    off: jest.fn(),\n    showHint: jest.fn(),\n    on: jest.fn(function (event, handler) {\n      if (event === 'keyup') {\n        if (handler && handler.name === '_onKeyUpMockDataHints') {\n          this._onKeyUpMockDataHints = handler;\n        }\n      }\n    })\n  };\n  return editor;\n});\n\nCodeMirror.commands = {\n  autocomplete: jest.fn()\n};\n\nCodeMirror.hint = {};\n\nCodeMirror.registerHelper = jest.fn((type, name, value) => {\n  if (!CodeMirror[type]) {\n    CodeMirror[type] = {};\n  }\n\n  CodeMirror[type][name] = value;\n});\n\nCodeMirror.fromTextArea = jest.fn();\nCodeMirror.defineMode = jest.fn();\n\nmodule.exports = CodeMirror;\n"
  },
  {
    "path": "packages/bruno-app/src/themes/DesignSystem/DesignSystem.stories.jsx",
    "content": "import {\n  IntroductionRender,\n  PrimaryColorsRender,\n  BackgroundLayersRender,\n  TextColorsRender,\n  BordersAndOverlaysRender\n} from './Overview';\n\nexport default {\n  title: 'Design System/Overview',\n  parameters: {\n    layout: 'padded'\n  }\n};\n\nexport const Introduction = {\n  render: IntroductionRender\n};\n\nexport const PrimaryColors = {\n  render: PrimaryColorsRender\n};\n\nexport const BackgroundLayers = {\n  render: BackgroundLayersRender\n};\n\nexport const TextColors = {\n  render: TextColorsRender\n};\n\nexport const BordersAndOverlays = {\n  render: BordersAndOverlaysRender\n};\n"
  },
  {
    "path": "packages/bruno-app/src/themes/DesignSystem/Overview.jsx",
    "content": "import React from 'react';\nimport { useTheme } from 'styled-components';\nimport { palette as darkPalette } from '../dark/dark';\nimport { palette as lightPalette } from '../light/light';\n\n// Shared Components\nexport const Section = ({ title, description, children }) => (\n  <div style={{ marginBottom: '48px' }}>\n    <h2 style={{ fontSize: '20px', fontWeight: 600, marginBottom: '8px' }}>{title}</h2>\n    {description && (\n      <p style={{ fontSize: '14px', opacity: 0.7, marginBottom: '24px', maxWidth: '600px', lineHeight: 1.6 }}>\n        {description}\n      </p>\n    )}\n    {children}\n  </div>\n);\n\nexport const ColorToken = ({ name, color, description, example }) => {\n  const theme = useTheme();\n  return (\n    <div style={{\n      display: 'flex',\n      alignItems: 'flex-start',\n      gap: '16px',\n      padding: '16px',\n      borderRadius: '8px',\n      backgroundColor: theme.dropdown.hoverBg,\n      marginBottom: '12px'\n    }}\n    >\n      <div style={{\n        width: '48px',\n        height: '48px',\n        borderRadius: '8px',\n        backgroundColor: color,\n        flexShrink: 0,\n        border: `1px solid ${theme.border.border1}`\n      }}\n      />\n      <div style={{ flex: 1 }}>\n        <div style={{\n          fontFamily: 'monospace',\n          fontSize: '13px',\n          fontWeight: 600,\n          marginBottom: '4px'\n        }}\n        >\n          {name}\n        </div>\n        <div style={{ fontSize: '13px', opacity: 0.8, marginBottom: '4px' }}>\n          {description}\n        </div>\n        {example && (\n          <div style={{\n            fontSize: '12px',\n            opacity: 0.6,\n            fontStyle: 'italic'\n          }}\n          >\n            Example: {example}\n          </div>\n        )}\n      </div>\n      <div style={{\n        fontFamily: 'monospace',\n        fontSize: '11px',\n        opacity: 0.5,\n        flexShrink: 0\n      }}\n      >\n        {color}\n      </div>\n    </div>\n  );\n};\n\nexport const LayerDemo = ({ layers }) => {\n  const theme = useTheme();\n  return (\n    <div style={{\n      display: 'flex',\n      gap: '24px',\n      flexWrap: 'wrap',\n      marginTop: '16px'\n    }}\n    >\n      {layers.map(({ name, color, description }) => (\n        <div key={name} style={{ textAlign: 'center' }}>\n          <div style={{\n            width: '120px',\n            height: '80px',\n            borderRadius: '8px',\n            backgroundColor: color,\n            border: `1px solid ${theme.border.border1}`,\n            marginBottom: '8px',\n            display: 'flex',\n            alignItems: 'center',\n            justifyContent: 'center',\n            fontSize: '11px',\n            fontFamily: 'monospace',\n            opacity: 0.7\n          }}\n          >\n            {color}\n          </div>\n          <div style={{ fontSize: '13px', fontWeight: 600, marginBottom: '2px' }}>{name}</div>\n          <div style={{ fontSize: '11px', opacity: 0.6, maxWidth: '120px' }}>{description}</div>\n        </div>\n      ))}\n    </div>\n  );\n};\n\n// Story Render Functions\nexport const IntroductionRender = () => {\n  const theme = useTheme();\n  const isDark = theme.mode === 'dark';\n  const palette = isDark ? darkPalette : lightPalette;\n\n  const hueColors = [\n    palette.hues.RED,\n    palette.hues.ORANGE,\n    palette.hues.YELLOW,\n    palette.hues.GREEN,\n    palette.hues.TEAL,\n    palette.hues.BLUE,\n    palette.hues.INDIGO,\n    palette.hues.PURPLE,\n    palette.hues.PINK\n  ];\n\n  return (\n    <div style={{\n      padding: '48px 32px',\n      backgroundColor: theme.bg,\n      color: theme.text,\n      minHeight: '100vh'\n    }}\n    >\n      {/* Hero Section */}\n      <div style={{ marginBottom: '64px' }}>\n        <div style={{\n          display: 'flex',\n          gap: '3px',\n          marginBottom: '24px'\n        }}\n        >\n          {hueColors.map((color, i) => (\n            <div\n              key={i}\n              style={{\n                width: '32px',\n                height: '6px',\n                backgroundColor: color,\n                borderRadius: i === 0 ? '3px 0 0 3px' : i === hueColors.length - 1 ? '0 3px 3px 0' : '0'\n              }}\n            />\n          ))}\n        </div>\n\n        <h1 style={{\n          fontSize: '40px',\n          fontWeight: 700,\n          marginBottom: '16px',\n          letterSpacing: '-0.5px'\n        }}\n        >\n          Bruno Design System\n        </h1>\n\n        <p style={{\n          fontSize: '18px',\n          opacity: 0.7,\n          marginBottom: '32px',\n          maxWidth: '640px',\n          lineHeight: 1.7\n        }}\n        >\n          A unified visual language for building consistent, accessible, and beautiful interfaces across Bruno's ecosystem.\n        </p>\n\n        {/* Theme indicator */}\n        <div style={{\n          display: 'inline-flex',\n          alignItems: 'center',\n          gap: '8px',\n          padding: '8px 16px',\n          backgroundColor: theme.background.surface0,\n          borderRadius: '20px',\n          fontSize: '13px',\n          border: `1px solid ${theme.border.border0}`\n        }}\n        >\n          <div style={{\n            width: '8px',\n            height: '8px',\n            borderRadius: '50%',\n            backgroundColor: theme.brand\n          }}\n          />\n          Currently viewing: <strong>{isDark ? 'Dark' : 'Light'} Theme</strong>\n        </div>\n      </div>\n\n      {/* Principles Grid */}\n      <Section title=\"Core Principles\">\n        <div style={{\n          display: 'grid',\n          gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))',\n          gap: '20px',\n          marginTop: '24px'\n        }}\n        >\n          {[\n            {\n              icon: '◐',\n              title: 'Semantic',\n              desc: 'Every color carries meaning. Primary draws attention, muted recedes, semantic colors (green, red, yellow) communicate status instantly.'\n            },\n            {\n              icon: '◉',\n              title: 'Layered',\n              desc: 'Backgrounds stack like geological strata—crust, mantle, base—creating natural depth and visual hierarchy.'\n            },\n            {\n              icon: '◧',\n              title: 'Accessible',\n              desc: 'All color combinations meet WCAG contrast requirements. Text remains readable across both light and dark themes.'\n            },\n            {\n              icon: '◈',\n              title: 'Consistent',\n              desc: 'Uniform spacing, predictable colors, and harmonious typography. Every element follows the same rhythm and visual rules.'\n            }\n          ].map(({ icon, title, desc }) => (\n            <div\n              key={title}\n              style={{\n                padding: '24px',\n                borderRadius: '12px',\n                backgroundColor: theme.background.surface0,\n                border: `1px solid ${theme.border.border0}`,\n                transition: 'border-color 0.2s'\n              }}\n            >\n              <div style={{\n                fontSize: '24px',\n                marginBottom: '12px',\n                opacity: 0.8\n              }}\n              >{icon}\n              </div>\n              <div style={{\n                fontWeight: 600,\n                fontSize: '15px',\n                marginBottom: '8px'\n              }}\n              >{title}\n              </div>\n              <div style={{\n                fontSize: '13px',\n                opacity: 0.7,\n                lineHeight: 1.6\n              }}\n              >{desc}\n              </div>\n            </div>\n          ))}\n        </div>\n      </Section>\n\n      {/* What's Inside */}\n      <Section title=\"What's Inside\">\n        <div style={{\n          display: 'grid',\n          gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',\n          gap: '12px',\n          marginTop: '24px'\n        }}\n        >\n          {[\n            { name: 'Primary Colors', desc: '4 variants for brand identity', color: palette.primary.SOLID },\n            { name: 'Backgrounds', desc: '6 layered surface colors', color: theme.background.mantle },\n            { name: 'Text', desc: '8 semantic text colors', color: theme.text },\n            { name: 'Borders', desc: '3 hierarchy levels', color: theme.border.border2 },\n            { name: 'Overlays', desc: '3 depth levels', color: theme.overlay.overlay1 },\n            { name: 'Hues', desc: '14 hue-spread colors', color: palette.hues.BLUE }\n          ].map(({ name, desc, color }) => (\n            <div\n              key={name}\n              style={{\n                display: 'flex',\n                alignItems: 'center',\n                gap: '12px',\n                padding: '16px',\n                borderRadius: '8px',\n                backgroundColor: theme.dropdown.hoverBg,\n                border: `1px solid ${theme.border.border0}`\n              }}\n            >\n              <div style={{\n                width: '36px',\n                height: '36px',\n                borderRadius: '8px',\n                backgroundColor: color,\n                flexShrink: 0,\n                border: `1px solid ${theme.border.border1}`\n              }}\n              />\n              <div>\n                <div style={{ fontWeight: 600, fontSize: '13px' }}>{name}</div>\n                <div style={{ fontSize: '12px', opacity: 0.6 }}>{desc}</div>\n              </div>\n            </div>\n          ))}\n        </div>\n      </Section>\n\n    </div>\n  );\n};\n\nexport const PrimaryColorsRender = () => {\n  const theme = useTheme();\n  const isDark = theme.mode === 'dark';\n  const palette = isDark ? darkPalette : lightPalette;\n\n  const primaryVariants = [\n    {\n      name: 'solid',\n      token: 'primary.solid',\n      color: palette.primary.SOLID,\n      purpose: 'Buttons, toggles, active pills',\n      desc: 'The foundation of interactive elements'\n    },\n    {\n      name: 'text',\n      token: 'primary.text',\n      color: palette.primary.TEXT,\n      purpose: 'Links, emphasized text',\n      desc: 'Optimized for readable colored text'\n    },\n    {\n      name: 'strong',\n      token: 'primary.strong',\n      color: palette.primary.STRONG,\n      purpose: 'Thick borders, tab underlines',\n      desc: 'High-visibility accents and indicators'\n    },\n    {\n      name: 'subtle',\n      token: 'primary.subtle',\n      color: palette.primary.SUBTLE,\n      purpose: 'Focus rings, subtle outlines',\n      desc: 'Gentle emphasis without distraction'\n    }\n  ];\n\n  return (\n    <div style={{\n      padding: '48px 32px',\n      backgroundColor: theme.bg,\n      color: theme.text,\n      minHeight: '100vh'\n    }}\n    >\n      {/* Hero */}\n      <div style={{ marginBottom: '48px' }}>\n        <h1 style={{ fontSize: '32px', fontWeight: 700, marginBottom: '12px' }}>\n          Primary Colors\n        </h1>\n        <p style={{\n          fontSize: '16px',\n          opacity: 0.7,\n          maxWidth: '540px',\n          lineHeight: 1.7\n        }}\n        >\n          Four carefully calibrated variants of Bruno's brand color, each designed for a specific role in the interface.\n        </p>\n      </div>\n\n      {/* Variant Cards */}\n      <Section title=\"The Four Variants\">\n        <div style={{\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '12px',\n          marginTop: '24px'\n        }}\n        >\n          {primaryVariants.map(({ name, token, color, purpose, desc }) => (\n            <div\n              key={name}\n              style={{\n                display: 'flex',\n                alignItems: 'center',\n                gap: '20px',\n                padding: '12px 0',\n                borderBottom: `1px solid ${theme.border.border0}`\n              }}\n            >\n              {/* Color swatch */}\n              <div style={{\n                width: '40px',\n                height: '40px',\n                borderRadius: '8px',\n                backgroundColor: color,\n                flexShrink: 0\n              }}\n              />\n\n              {/* Content */}\n              <div style={{ flex: 1 }}>\n                <div style={{\n                  display: 'flex',\n                  alignItems: 'baseline',\n                  gap: '12px',\n                  marginBottom: '4px'\n                }}\n                >\n                  <span style={{ fontSize: '15px', fontWeight: 600 }}>{purpose}</span>\n                  <code style={{\n                    fontSize: '12px',\n                    fontFamily: 'monospace',\n                    opacity: 0.4\n                  }}\n                  >\n                    {token}\n                  </code>\n                </div>\n                <div style={{\n                  fontSize: '13px',\n                  opacity: 0.6,\n                  lineHeight: 1.4\n                }}\n                >\n                  {desc}\n                </div>\n              </div>\n\n              {/* Color value */}\n              <code style={{\n                fontSize: '11px',\n                fontFamily: 'monospace',\n                opacity: 0.4,\n                flexShrink: 0\n              }}\n              >\n                {color}\n              </code>\n            </div>\n          ))}\n        </div>\n      </Section>\n\n      {/* Live Examples */}\n      <Section title=\"In Context\">\n        <div style={{\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '32px',\n          marginTop: '24px'\n        }}\n        >\n          {/* Solid example */}\n          <div style={{ display: 'flex', alignItems: 'center', gap: '24px', flexWrap: 'wrap' }}>\n            <div style={{ width: '100px', fontSize: '12px', opacity: 0.5, fontFamily: 'monospace' }}>solid</div>\n            <button style={{\n              backgroundColor: palette.primary.SOLID,\n              color: isDark ? '#1a1a1a' : '#ffffff',\n              border: 'none',\n              padding: '10px 20px',\n              borderRadius: '6px',\n              fontSize: '14px',\n              fontWeight: 500,\n              cursor: 'pointer'\n            }}\n            >\n              Primary Button\n            </button>\n            <div style={{\n              display: 'flex',\n              gap: '8px'\n            }}\n            >\n              <div style={{\n                padding: '6px 12px',\n                backgroundColor: palette.primary.SOLID,\n                color: isDark ? '#1a1a1a' : '#ffffff',\n                borderRadius: '16px',\n                fontSize: '12px',\n                fontWeight: 500\n              }}\n              >Active\n              </div>\n              <div style={{\n                padding: '6px 12px',\n                backgroundColor: theme.background.surface1,\n                borderRadius: '16px',\n                fontSize: '12px',\n                opacity: 0.7\n              }}\n              >Inactive\n              </div>\n            </div>\n          </div>\n\n          {/* Text example */}\n          <div style={{ display: 'flex', alignItems: 'center', gap: '24px', flexWrap: 'wrap' }}>\n            <div style={{ width: '100px', fontSize: '12px', opacity: 0.5, fontFamily: 'monospace' }}>text</div>\n            <span style={{ color: palette.primary.TEXT, fontSize: '14px' }}>\n              View documentation →\n            </span>\n            <span style={{ fontSize: '14px' }}>\n              Click <span style={{ color: palette.primary.TEXT, fontWeight: 500 }}>here</span> to learn more\n            </span>\n          </div>\n\n          {/* Strong example */}\n          <div style={{ display: 'flex', alignItems: 'center', gap: '24px', flexWrap: 'wrap' }}>\n            <div style={{ width: '100px', fontSize: '12px', opacity: 0.5, fontFamily: 'monospace' }}>strong</div>\n            <div style={{ display: 'flex', gap: '24px' }}>\n              <div style={{\n                padding: '8px 0',\n                borderBottom: `3px solid ${palette.primary.STRONG}`,\n                fontSize: '14px',\n                fontWeight: 500\n              }}\n              >Active Tab\n              </div>\n              <div style={{\n                padding: '8px 0',\n                borderBottom: '3px solid transparent',\n                fontSize: '14px',\n                opacity: 0.6\n              }}\n              >Other Tab\n              </div>\n            </div>\n            <div style={{\n              width: '120px',\n              height: '6px',\n              backgroundColor: theme.background.surface1,\n              borderRadius: '3px',\n              overflow: 'hidden'\n            }}\n            >\n              <div style={{\n                width: '70%',\n                height: '100%',\n                backgroundColor: palette.primary.STRONG,\n                borderRadius: '3px'\n              }}\n              />\n            </div>\n          </div>\n\n          {/* Subtle example */}\n          <div style={{ display: 'flex', alignItems: 'center', gap: '24px', flexWrap: 'wrap' }}>\n            <div style={{ width: '100px', fontSize: '12px', opacity: 0.5, fontFamily: 'monospace' }}>subtle</div>\n            <input\n              type=\"text\"\n              placeholder=\"Focused input\"\n              readOnly\n              style={{\n                padding: '8px 12px',\n                border: `1px solid ${palette.primary.SUBTLE}`,\n                borderRadius: '6px',\n                fontSize: '14px',\n                backgroundColor: theme.input?.bg || 'transparent',\n                color: theme.text,\n                outline: 'none',\n                width: '200px'\n              }}\n            />\n          </div>\n        </div>\n      </Section>\n    </div>\n  );\n};\n\nexport const BackgroundLayersRender = () => {\n  const theme = useTheme();\n  const isDark = theme.mode === 'dark';\n  const palette = isDark ? darkPalette : lightPalette;\n\n  const layers = [\n    {\n      name: 'base',\n      token: 'background.base',\n      color: theme.background.base,\n      purpose: 'Main content area',\n      desc: 'Where primary content and interactions live'\n    },\n    {\n      name: 'mantle',\n      token: 'background.mantle',\n      color: theme.background.mantle,\n      purpose: 'Sidebars, panels',\n      desc: 'Secondary areas that frame the content'\n    },\n    {\n      name: 'crust',\n      token: 'background.crust',\n      color: theme.background.crust,\n      purpose: 'Status bars, app shell',\n      desc: 'The deepest layer forming the foundation'\n    }\n  ];\n\n  return (\n    <div style={{\n      padding: '48px 32px',\n      backgroundColor: theme.bg,\n      color: theme.text,\n      minHeight: '100vh'\n    }}\n    >\n      {/* Hero */}\n      <div style={{ marginBottom: '48px' }}>\n        <h1 style={{ fontSize: '32px', fontWeight: 700, marginBottom: '12px' }}>\n          Background Layers\n        </h1>\n        <p style={{\n          fontSize: '16px',\n          opacity: 0.7,\n          maxWidth: '540px',\n          lineHeight: 1.7\n        }}\n        >\n          A layered background system inspired by geological strata. Each layer serves a distinct purpose in creating visual hierarchy.\n        </p>\n      </div>\n\n      {/* App Skeleton */}\n      <Section title=\"The Layer Model\">\n        <div style={{ marginTop: '24px' }}>\n          {/* App frame */}\n          <div style={{\n            borderRadius: '12px',\n            overflow: 'hidden',\n            border: `1px solid ${theme.border.border1}`,\n            maxWidth: '600px'\n          }}\n          >\n            {/* Title bar */}\n            <div style={{\n              backgroundColor: theme.background.crust,\n              padding: '8px 12px',\n              display: 'flex',\n              alignItems: 'center',\n              gap: '8px',\n              borderBottom: `1px solid ${theme.border.border0}`\n            }}\n            >\n              <div style={{ display: 'flex', gap: '6px' }}>\n                <div style={{ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: theme.colors?.text?.danger || '#ff5f57' }} />\n                <div style={{ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: theme.colors?.text?.warning || '#ffbd2e' }} />\n                <div style={{ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: theme.colors?.text?.green || '#28c840' }} />\n              </div>\n              <div style={{ flex: 1, textAlign: 'center', fontSize: '12px', opacity: 0.5 }}>Bruno</div>\n            </div>\n\n            {/* Main area */}\n            <div style={{ display: 'flex', minHeight: '360px' }}>\n              {/* Sidebar - Mantle */}\n              <div style={{\n                width: '160px',\n                backgroundColor: theme.background.mantle,\n                padding: '16px 12px',\n                borderRight: `1px solid ${theme.border.border0}`,\n                display: 'flex',\n                flexDirection: 'column',\n                gap: '4px'\n              }}\n              >\n                <div style={{\n                  fontSize: '10px',\n                  fontFamily: 'monospace',\n                  color: theme.primary?.text || theme.brand,\n                  marginBottom: '8px'\n                }}\n                >mantle\n                </div>\n                {['Stripe API', 'GitHub REST', 'Internal Auth', 'Analytics', 'Payments'].map((item, i) => (\n                  <div\n                    key={item}\n                    style={{\n                      padding: '6px 10px',\n                      fontSize: '12px',\n                      borderRadius: '4px',\n                      backgroundColor: i === 0 ? theme.background.surface0 : 'transparent',\n                      opacity: i === 0 ? 1 : 0.6\n                    }}\n                  >{item}\n                  </div>\n                ))}\n              </div>\n\n              {/* Content - Base */}\n              <div style={{\n                flex: 1,\n                backgroundColor: theme.background.base,\n                padding: '16px',\n                display: 'flex',\n                flexDirection: 'column'\n              }}\n              >\n                <div style={{\n                  fontSize: '10px',\n                  fontFamily: 'monospace',\n                  color: theme.primary?.text || theme.brand,\n                  marginBottom: '12px'\n                }}\n                >base\n                </div>\n\n                {/* URL bar */}\n                <div style={{\n                  display: 'flex',\n                  alignItems: 'center',\n                  gap: '10px',\n                  padding: '8px 12px',\n                  backgroundColor: isDark ? theme.background.base : '#ffffff',\n                  border: `1px solid ${theme.border.border1}`,\n                  borderRadius: '6px',\n                  marginBottom: '16px'\n                }}\n                >\n                  <span style={{\n                    fontSize: '12px',\n                    fontWeight: 600,\n                    color: palette.hues.GREEN\n                  }}\n                  >GET\n                  </span>\n                  <span style={{\n                    fontSize: '12px',\n                    opacity: 0.6\n                  }}\n                  >https://api.example.com/users\n                  </span>\n                </div>\n\n                {/* Two-pane content */}\n                <div style={{\n                  flex: 1,\n                  display: 'flex',\n                  gap: '12px'\n                }}\n                >\n                  {/* Payload pane */}\n                  <div style={{\n                    flex: 1,\n                    display: 'flex',\n                    flexDirection: 'column'\n                  }}\n                  >\n                    <div style={{\n                      fontSize: '11px',\n                      fontWeight: 500,\n                      opacity: 0.5,\n                      marginBottom: '8px'\n                    }}\n                    >Payload\n                    </div>\n                    <div style={{\n                      flex: 1,\n                      border: `1px solid ${theme.border.border0}`,\n                      borderRadius: '4px',\n                      padding: '10px',\n                      fontSize: '11px',\n                      fontFamily: 'monospace',\n                      opacity: 0.5\n                    }}\n                    >\n                      {'{\\n  \"name\": \"...\",\\n  \"email\": \"...\"\\n}'}\n                    </div>\n                  </div>\n                  {/* Response pane */}\n                  <div style={{\n                    flex: 1,\n                    display: 'flex',\n                    flexDirection: 'column'\n                  }}\n                  >\n                    <div style={{\n                      fontSize: '11px',\n                      fontWeight: 500,\n                      opacity: 0.5,\n                      marginBottom: '8px'\n                    }}\n                    >Response\n                    </div>\n                    <div style={{\n                      flex: 1,\n                      border: `1px solid ${theme.border.border0}`,\n                      borderRadius: '4px',\n                      padding: '10px',\n                      fontSize: '11px',\n                      fontFamily: 'monospace',\n                      opacity: 0.5\n                    }}\n                    >\n                      {'{\\n  \"status\": 200,\\n  \"data\": [...]\\n}'}\n                    </div>\n                  </div>\n                </div>\n              </div>\n            </div>\n\n            {/* Status bar - Crust */}\n            <div style={{\n              backgroundColor: theme.background.crust,\n              padding: '6px 12px',\n              display: 'flex',\n              justifyContent: 'space-between',\n              alignItems: 'center',\n              borderTop: `1px solid ${theme.border.border0}`\n            }}\n            >\n              <div style={{\n                fontSize: '10px',\n                fontFamily: 'monospace',\n                color: theme.primary?.text || theme.brand\n              }}\n              >crust\n              </div>\n              <div style={{ fontSize: '11px', opacity: 0.5 }}>Ready</div>\n            </div>\n          </div>\n        </div>\n      </Section>\n\n      {/* Layer Definitions */}\n      <Section title=\"Layer Definitions\">\n        <div style={{\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '12px',\n          marginTop: '24px'\n        }}\n        >\n          {layers.map(({ name, token, color, purpose, desc }) => (\n            <div\n              key={name}\n              style={{\n                display: 'flex',\n                alignItems: 'center',\n                gap: '20px',\n                padding: '12px 0',\n                borderBottom: `1px solid ${theme.border.border0}`\n              }}\n            >\n              <div style={{\n                width: '40px',\n                height: '40px',\n                borderRadius: '8px',\n                backgroundColor: color,\n                flexShrink: 0,\n                border: `1px solid ${theme.border.border1}`\n              }}\n              />\n              <div style={{ flex: 1 }}>\n                <div style={{\n                  display: 'flex',\n                  alignItems: 'baseline',\n                  gap: '12px',\n                  marginBottom: '4px'\n                }}\n                >\n                  <span style={{ fontSize: '15px', fontWeight: 600 }}>{purpose}</span>\n                  <code style={{\n                    fontSize: '12px',\n                    fontFamily: 'monospace',\n                    opacity: 0.4\n                  }}\n                  >\n                    {token}\n                  </code>\n                </div>\n                <div style={{\n                  fontSize: '13px',\n                  opacity: 0.6,\n                  lineHeight: 1.4\n                }}\n                >\n                  {desc}\n                </div>\n              </div>\n              <code style={{\n                fontSize: '11px',\n                fontFamily: 'monospace',\n                opacity: 0.4,\n                flexShrink: 0\n              }}\n              >\n                {color}\n              </code>\n            </div>\n          ))}\n        </div>\n      </Section>\n\n      {/* Surface Variants */}\n      <Section title=\"Surface Variants\">\n        <div style={{\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '12px',\n          marginTop: '24px'\n        }}\n        >\n          {[\n            { name: 'surface0', token: 'background.surface0', color: theme.background.surface0, purpose: 'Cards & Inputs', desc: 'Slightly elevated from base, used for cards, input fields, and contained elements.' },\n            { name: 'surface1', token: 'background.surface1', color: theme.background.surface1, purpose: 'Hover States', desc: 'One step brighter, indicates hover or light interaction feedback.' },\n            { name: 'surface2', token: 'background.surface2', color: theme.background.surface2, purpose: 'Active States', desc: 'Highest elevation surface, used for active selections and pressed states.' }\n          ].map(({ name, token, color, purpose, desc }) => (\n            <div\n              key={name}\n              style={{\n                display: 'flex',\n                alignItems: 'center',\n                gap: '20px',\n                padding: '12px 0',\n                borderBottom: `1px solid ${theme.border.border0}`\n              }}\n            >\n              <div style={{\n                width: '40px',\n                height: '40px',\n                borderRadius: '8px',\n                backgroundColor: color,\n                flexShrink: 0,\n                border: `1px solid ${theme.border.border1}`\n              }}\n              />\n              <div style={{ flex: 1 }}>\n                <div style={{\n                  display: 'flex',\n                  alignItems: 'baseline',\n                  gap: '12px',\n                  marginBottom: '4px'\n                }}\n                >\n                  <span style={{ fontSize: '15px', fontWeight: 600 }}>{purpose}</span>\n                  <code style={{\n                    fontSize: '12px',\n                    fontFamily: 'monospace',\n                    opacity: 0.4\n                  }}\n                  >\n                    {token}\n                  </code>\n                </div>\n                <div style={{\n                  fontSize: '13px',\n                  opacity: 0.6,\n                  lineHeight: 1.4\n                }}\n                >\n                  {desc}\n                </div>\n              </div>\n              <code style={{\n                fontSize: '11px',\n                fontFamily: 'monospace',\n                opacity: 0.4,\n                flexShrink: 0\n              }}\n              >\n                {color}\n              </code>\n            </div>\n          ))}\n        </div>\n      </Section>\n    </div>\n  );\n};\n\nexport const TextColorsRender = () => {\n  const theme = useTheme();\n\n  const hierarchyLevels = [\n    { name: 'Base', color: theme.text, desc: 'Primary content, headings, and emphasized information' },\n    { name: 'Subtext2', color: theme.colors.text.subtext2, desc: 'Strong secondary text, labels, and descriptions' },\n    { name: 'Subtext1', color: theme.colors.text.subtext1, desc: 'Supporting content and supplementary details' },\n    { name: 'Subtext0', color: theme.colors.text.subtext0, desc: 'Timestamps, hints, placeholders, and disabled states' }\n  ];\n\n  const semanticColors = [\n    { name: 'Success', color: theme.colors.text.green, desc: 'Positive states and confirmations', example: 'GET method, success messages' },\n    { name: 'Danger', color: theme.colors.text.danger, desc: 'Errors and destructive actions', example: 'DELETE method, error messages' },\n    { name: 'Warning', color: theme.colors.text.warning, desc: 'Caution states and notices', example: 'PUT/PATCH methods, deprecation notices' },\n    { name: 'Accent', color: theme.colors.text.purple, desc: 'Special content markers', example: 'GraphQL indicators, unique tags' },\n    { name: 'Link', color: theme.textLink, desc: 'Interactive text and navigation', example: 'Hyperlinks, clickable references' }\n  ];\n\n  return (\n    <div style={{\n      padding: '32px',\n      backgroundColor: theme.bg,\n      color: theme.text,\n      minHeight: '100vh'\n    }}\n    >\n      {/* Hero */}\n      <div style={{ marginBottom: '48px' }}>\n        <h1 style={{ fontSize: '32px', fontWeight: 700, marginBottom: '12px' }}>\n          Text Colors\n        </h1>\n        <p style={{\n          fontSize: '16px',\n          opacity: 0.7,\n          maxWidth: '540px',\n          lineHeight: 1.7\n        }}\n        >\n          Text colors create visual hierarchy and convey meaning. Use progressively muted colors for less important information.\n        </p>\n      </div>\n\n      {/* Text Hierarchy Demo */}\n      <Section title=\"Text Hierarchy\">\n        {/* Demo: Collection item style */}\n        <div style={{\n          marginTop: '24px',\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '12px',\n          maxWidth: '320px'\n        }}\n        >\n          {/* Request item 1 */}\n          <div style={{\n            display: 'flex',\n            alignItems: 'center',\n            gap: '10px',\n            padding: '8px 12px',\n            borderRadius: '6px',\n            border: `1px solid ${theme.border.border0}`\n          }}\n          >\n            <span style={{ color: theme.colors.text.green, fontSize: '11px', fontWeight: 600 }}>GET</span>\n            <div style={{ flex: 1 }}>\n              <div style={{ color: theme.text, fontSize: '13px', fontWeight: 500 }}>Get Users</div>\n              <div style={{ color: theme.colors.text.subtext0, fontSize: '11px' }}>/api/v1/users</div>\n            </div>\n          </div>\n          {/* Request item 2 */}\n          <div style={{\n            display: 'flex',\n            alignItems: 'center',\n            gap: '10px',\n            padding: '8px 12px',\n            borderRadius: '6px',\n            border: `1px solid ${theme.border.border0}`\n          }}\n          >\n            <span style={{ color: theme.colors.text.warning, fontSize: '11px', fontWeight: 600 }}>POST</span>\n            <div style={{ flex: 1 }}>\n              <div style={{ color: theme.text, fontSize: '13px', fontWeight: 500 }}>Create User</div>\n              <div style={{ color: theme.colors.text.subtext0, fontSize: '11px' }}>/api/v1/users</div>\n            </div>\n          </div>\n        </div>\n\n        {/* Hierarchy Definitions */}\n        <div style={{\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '12px',\n          marginTop: '32px'\n        }}\n        >\n          {hierarchyLevels.map(({ name, color, desc }) => (\n            <div\n              key={name}\n              style={{\n                display: 'flex',\n                alignItems: 'center',\n                gap: '20px',\n                padding: '12px 0',\n                borderBottom: `1px solid ${theme.border.border0}`\n              }}\n            >\n              <div style={{\n                width: '40px',\n                height: '40px',\n                borderRadius: '8px',\n                backgroundColor: color,\n                flexShrink: 0,\n                border: `1px solid ${theme.border.border1}`\n              }}\n              />\n              <div style={{ flex: 1 }}>\n                <div style={{ fontSize: '15px', fontWeight: 600, marginBottom: '4px' }}>{name}</div>\n                <div style={{ fontSize: '13px', opacity: 0.6 }}>{desc}</div>\n              </div>\n              <code style={{\n                fontSize: '11px',\n                fontFamily: 'monospace',\n                opacity: 0.4,\n                flexShrink: 0\n              }}\n              >\n                {color}\n              </code>\n            </div>\n          ))}\n        </div>\n      </Section>\n\n      {/* Semantic Colors */}\n      <Section title=\"Semantic Colors\">\n        <div style={{\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '12px',\n          marginTop: '24px'\n        }}\n        >\n          {semanticColors.map(({ name, color, desc, example }) => (\n            <div\n              key={name}\n              style={{\n                display: 'flex',\n                alignItems: 'center',\n                gap: '20px',\n                padding: '12px 0',\n                borderBottom: `1px solid ${theme.border.border0}`\n              }}\n            >\n              <div style={{\n                width: '40px',\n                height: '40px',\n                borderRadius: '8px',\n                backgroundColor: color,\n                flexShrink: 0,\n                border: `1px solid ${theme.border.border1}`\n              }}\n              />\n              <div style={{ flex: 1 }}>\n                <div style={{\n                  display: 'flex',\n                  alignItems: 'baseline',\n                  gap: '12px',\n                  marginBottom: '4px'\n                }}\n                >\n                  <span style={{ fontSize: '15px', fontWeight: 600 }}>{name}</span>\n                  <span style={{ fontSize: '12px', opacity: 0.4 }}>{example}</span>\n                </div>\n                <div style={{ fontSize: '13px', opacity: 0.6 }}>{desc}</div>\n              </div>\n              <code style={{\n                fontSize: '11px',\n                fontFamily: 'monospace',\n                opacity: 0.4,\n                flexShrink: 0\n              }}\n              >\n                {color}\n              </code>\n            </div>\n          ))}\n        </div>\n      </Section>\n    </div>\n  );\n};\n\nexport const BordersAndOverlaysRender = () => {\n  const theme = useTheme();\n\n  const overlays = [\n    { name: 'Overlay0', color: theme.overlay.overlay0, purpose: 'Subtle', desc: 'Light dimming, gentle hover states' },\n    { name: 'Overlay1', color: theme.overlay.overlay1, purpose: 'Medium', desc: 'Standard overlays, dropdown backdrops' },\n    { name: 'Overlay2', color: theme.overlay.overlay2, purpose: 'Strong', desc: 'Modal backdrops, disabled content' }\n  ];\n\n  const radii = [\n    { name: 'sm', value: theme.border.radius.sm },\n    { name: 'base', value: theme.border.radius.base },\n    { name: 'md', value: theme.border.radius.md },\n    { name: 'lg', value: theme.border.radius.lg },\n    { name: 'xl', value: theme.border.radius.xl }\n  ];\n\n  return (\n    <div style={{\n      padding: '32px',\n      backgroundColor: theme.bg,\n      color: theme.text,\n      minHeight: '100vh'\n    }}\n    >\n      {/* Hero */}\n      <div style={{ marginBottom: '48px' }}>\n        <h1 style={{ fontSize: '32px', fontWeight: 700, marginBottom: '12px' }}>\n          Borders & Overlays\n        </h1>\n        <p style={{\n          fontSize: '16px',\n          opacity: 0.7,\n          maxWidth: '540px',\n          lineHeight: 1.7\n        }}\n        >\n          Borders define boundaries and create structure. Overlays add depth for focus states and modal backdrops.\n        </p>\n      </div>\n\n      <Section title=\"Border Hierarchy\">\n        <div style={{ display: 'flex', flexDirection: 'column', gap: '16px', maxWidth: '400px', marginTop: '24px' }}>\n          <div style={{\n            padding: '16px',\n            border: `1px solid ${theme.border.border0}`,\n            borderRadius: '8px'\n          }}\n          >\n            <div style={{ fontSize: '13px', fontWeight: 600 }}>border0 (Subtle)</div>\n            <div style={{ fontSize: '12px', opacity: 0.6 }}>Gentle separations, card outlines</div>\n          </div>\n\n          <div style={{\n            padding: '16px',\n            border: `1px solid ${theme.border.border1}`,\n            borderRadius: '8px'\n          }}\n          >\n            <div style={{ fontSize: '13px', fontWeight: 600 }}>border1 (Default)</div>\n            <div style={{ fontSize: '12px', opacity: 0.6 }}>Standard dividers, input borders</div>\n          </div>\n\n          <div style={{\n            padding: '16px',\n            border: `1px solid ${theme.border.border2}`,\n            borderRadius: '8px'\n          }}\n          >\n            <div style={{ fontSize: '13px', fontWeight: 600 }}>border2 (Prominent)</div>\n            <div style={{ fontSize: '12px', opacity: 0.6 }}>Focus states, selected items</div>\n          </div>\n        </div>\n      </Section>\n\n      <Section title=\"Overlay Colors\">\n        <div style={{\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '12px',\n          marginTop: '24px'\n        }}\n        >\n          {overlays.map(({ name, color, purpose, desc }) => (\n            <div\n              key={name}\n              style={{\n                display: 'flex',\n                alignItems: 'center',\n                gap: '20px',\n                padding: '12px 0',\n                borderBottom: `1px solid ${theme.border.border0}`\n              }}\n            >\n              <div style={{\n                width: '40px',\n                height: '40px',\n                borderRadius: '8px',\n                backgroundColor: color,\n                flexShrink: 0,\n                border: `1px solid ${theme.border.border1}`\n              }}\n              />\n              <div style={{ flex: 1 }}>\n                <div style={{ fontSize: '15px', fontWeight: 600, marginBottom: '4px' }}>{purpose}</div>\n                <div style={{ fontSize: '13px', opacity: 0.6 }}>{desc}</div>\n              </div>\n              <code style={{\n                fontSize: '11px',\n                fontFamily: 'monospace',\n                opacity: 0.4,\n                flexShrink: 0\n              }}\n              >\n                {color}\n              </code>\n            </div>\n          ))}\n        </div>\n      </Section>\n\n      <Section title=\"Border Colors\">\n        <div style={{\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '12px',\n          marginTop: '24px'\n        }}\n        >\n          {[\n            { name: 'border0', color: theme.border.border0, desc: 'Subtle separations, card outlines' },\n            { name: 'border1', color: theme.border.border1, desc: 'Standard dividers, input borders' },\n            { name: 'border2', color: theme.border.border2, desc: 'Focus states, selected items' }\n          ].map(({ name, color, desc }) => (\n            <div\n              key={name}\n              style={{\n                display: 'flex',\n                alignItems: 'center',\n                gap: '20px',\n                padding: '12px 0',\n                borderBottom: `1px solid ${theme.border.border0}`\n              }}\n            >\n              <div style={{\n                width: '40px',\n                height: '40px',\n                borderRadius: '8px',\n                backgroundColor: color,\n                flexShrink: 0,\n                border: `1px solid ${theme.border.border1}`\n              }}\n              />\n              <div style={{ flex: 1 }}>\n                <div style={{ fontSize: '15px', fontWeight: 600, marginBottom: '4px' }}>{name}</div>\n                <div style={{ fontSize: '13px', opacity: 0.6 }}>{desc}</div>\n              </div>\n              <code style={{\n                fontSize: '11px',\n                fontFamily: 'monospace',\n                opacity: 0.4,\n                flexShrink: 0\n              }}\n              >\n                {color}\n              </code>\n            </div>\n          ))}\n        </div>\n      </Section>\n\n      <Section title=\"Border Radius\">\n        <div style={{\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '12px',\n          marginTop: '24px'\n        }}\n        >\n          {radii.map(({ name, value }) => (\n            <div\n              key={name}\n              style={{\n                display: 'flex',\n                alignItems: 'center',\n                gap: '20px',\n                padding: '12px 0',\n                borderBottom: `1px solid ${theme.border.border0}`\n              }}\n            >\n              <div style={{\n                width: '40px',\n                height: '40px',\n                borderRadius: value,\n                flexShrink: 0,\n                border: `2px solid ${theme.primary?.text || theme.brand}`\n              }}\n              />\n              <div style={{ flex: 1 }}>\n                <span style={{ fontSize: '15px', fontWeight: 600 }}>{name}</span>\n              </div>\n              <code style={{ fontSize: '12px', fontFamily: 'monospace', opacity: 0.4 }}>{value}</code>\n            </div>\n          ))}\n        </div>\n      </Section>\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/bruno-app/src/themes/DesignSystem/Theme.stories.jsx",
    "content": "import React from 'react';\nimport { useTheme } from 'styled-components';\nimport { palette as darkPalette } from '../dark/dark';\nimport { palette as lightPalette } from '../light/light';\nimport { ColorSection } from '../PaletteViewer/components';\n\nexport default {\n  title: 'Design System/Theme',\n  parameters: {\n    layout: 'padded'\n  }\n};\n\nexport const Palette = {\n  render: () => {\n    const theme = useTheme();\n    const isDark = theme.mode === 'dark';\n    const palette = isDark ? darkPalette : lightPalette;\n\n    return (\n      <div style={{ padding: '24px', backgroundColor: theme.bg, minHeight: '100vh' }}>\n        <div style={{ marginBottom: '32px' }}>\n          <h1 style={{ fontSize: '32px', fontWeight: 700, marginBottom: '12px', color: theme.text }}>\n            Palette\n          </h1>\n          <p style={{\n            fontSize: '16px',\n            opacity: 0.7,\n            maxWidth: '540px',\n            lineHeight: 1.7,\n            color: theme.text\n          }}\n          >\n            The foundational color tokens that make up the {isDark ? 'dark' : 'light'} theme. All semantic colors are derived from these base values.\n          </p>\n        </div>\n\n        <ColorSection title=\"Primary\" colors={palette.primary} textColor={theme.text} size={48} />\n        <ColorSection title=\"Hues\" colors={palette.hues} textColor={theme.text} size={48} />\n        <ColorSection title=\"System\" colors={palette.system} textColor={theme.text} size={48} />\n        <ColorSection title=\"Background\" colors={palette.background} textColor={theme.text} size={48} />\n        <ColorSection title=\"Text\" colors={palette.text} textColor={theme.text} size={48} />\n        <ColorSection title=\"Overlay\" colors={palette.overlay} textColor={theme.text} size={48} />\n        <ColorSection title=\"Border\" colors={palette.border} textColor={theme.text} size={48} />\n        <ColorSection title=\"Utility\" colors={palette.utility} textColor={theme.text} size={48} />\n      </div>\n    );\n  }\n};\n\nexport const IntentAndSyntax = {\n  render: () => {\n    const theme = useTheme();\n    const isDark = theme.mode === 'dark';\n    const palette = isDark ? darkPalette : lightPalette;\n\n    return (\n      <div style={{ padding: '24px', backgroundColor: theme.bg, minHeight: '100vh' }}>\n        <div style={{ marginBottom: '32px' }}>\n          <h1 style={{ fontSize: '32px', fontWeight: 700, marginBottom: '12px', color: theme.text }}>\n            Intent & Syntax\n          </h1>\n          <p style={{\n            fontSize: '16px',\n            opacity: 0.7,\n            maxWidth: '540px',\n            lineHeight: 1.7,\n            color: theme.text\n          }}\n          >\n            Semantic color mappings derived from the base palette. Intent colors convey meaning, while syntax colors provide code highlighting.\n          </p>\n        </div>\n\n        <ColorSection title=\"Intent\" colors={palette.intent} textColor={theme.text} size={48} />\n        <ColorSection title=\"Syntax\" colors={palette.syntax} textColor={theme.text} size={48} />\n      </div>\n    );\n  }\n};\n\nexport const HueWheel = {\n  render: () => {\n    const theme = useTheme();\n    const isDark = theme.mode === 'dark';\n    const palette = isDark ? darkPalette : lightPalette;\n\n    const hues = Object.entries(palette.hues);\n    // Sort by hue for visualization\n    const sorted = [...hues].sort((a, b) => {\n      const hueA = parseInt(a[1].match(/hsl\\((\\d+)/)?.[1] || 0);\n      const hueB = parseInt(b[1].match(/hsl\\((\\d+)/)?.[1] || 0);\n      return hueA - hueB;\n    });\n\n    return (\n      <div style={{ padding: '24px', backgroundColor: theme.bg, minHeight: '100vh' }}>\n        <div style={{ marginBottom: '32px' }}>\n          <h1 style={{ fontSize: '32px', fontWeight: 700, marginBottom: '12px', color: theme.text }}>\n            Hue Wheel\n          </h1>\n          <p style={{\n            fontSize: '16px',\n            opacity: 0.7,\n            maxWidth: '540px',\n            lineHeight: 1.7,\n            color: theme.text\n          }}\n          >\n            Distribution of the 14 hue colors across the color wheel (0° to 360°). Provides full spectrum coverage for diverse UI needs.\n          </p>\n        </div>\n\n        {/* Visual wheel representation */}\n        <div style={{\n          display: 'flex',\n          justifyContent: 'center',\n          marginBottom: '48px'\n        }}\n        >\n          <div style={{\n            width: '280px',\n            height: '280px',\n            borderRadius: '50%',\n            background: `conic-gradient(${sorted.map(([, color], i) => `${color} ${(i / sorted.length) * 100}% ${((i + 1) / sorted.length) * 100}%`).join(', ')})`,\n            display: 'flex',\n            alignItems: 'center',\n            justifyContent: 'center'\n          }}\n          >\n            <div style={{\n              width: '140px',\n              height: '140px',\n              borderRadius: '50%',\n              backgroundColor: theme.bg,\n              display: 'flex',\n              alignItems: 'center',\n              justifyContent: 'center',\n              flexDirection: 'column'\n            }}\n            >\n              <div style={{ fontSize: '24px', fontWeight: 700, color: theme.text }}>14</div>\n              <div style={{ fontSize: '12px', opacity: 0.6, color: theme.text }}>hues</div>\n            </div>\n          </div>\n        </div>\n\n        {/* Hue distribution bars */}\n        <div style={{\n          display: 'flex',\n          flexDirection: 'column',\n          gap: '8px'\n        }}\n        >\n          {sorted.map(([name, color]) => {\n            const hueMatch = color.match(/hsl\\((\\d+)/);\n            const hue = hueMatch ? parseInt(hueMatch[1]) : 0;\n            return (\n              <div key={name} style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>\n                <div style={{ width: '70px', fontSize: '12px', fontWeight: 600, color: theme.text }}>{name}</div>\n                <div style={{ width: '40px', fontSize: '11px', opacity: 0.5, fontFamily: 'monospace', color: theme.text }}>{hue}°</div>\n                <div style={{ flex: 1, display: 'flex', alignItems: 'center' }}>\n                  <div\n                    style={{\n                      width: `${(hue / 360) * 100}%`,\n                      minWidth: '20px',\n                      height: '28px',\n                      backgroundColor: color,\n                      borderRadius: '4px'\n                    }}\n                  />\n                </div>\n                <code style={{\n                  fontSize: '10px',\n                  fontFamily: 'monospace',\n                  opacity: 0.4,\n                  width: '140px',\n                  color: theme.text\n                }}\n                >\n                  {color}\n                </code>\n              </div>\n            );\n          })}\n        </div>\n      </div>\n    );\n  }\n};\n\n// Helper to parse HSL values\nconst parseHSL = (hslString) => {\n  const match = hslString.match(/hsl\\(\\s*(\\d+)\\s*,?\\s*(\\d+)%?\\s*,?\\s*(\\d+)%?\\s*\\)/);\n  if (match) {\n    return {\n      h: parseInt(match[1]),\n      s: parseInt(match[2]),\n      l: parseInt(match[3])\n    };\n  }\n  return null;\n};\n\n// Calculate statistics\nconst calcStats = (values) => {\n  const n = values.length;\n  const mean = values.reduce((a, b) => a + b, 0) / n;\n  const variance = values.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / n;\n  const stdDev = Math.sqrt(variance);\n  const min = Math.min(...values);\n  const max = Math.max(...values);\n  const range = max - min;\n  return { mean, stdDev, min, max, range };\n};\n\nexport const HueAnalysis = {\n  render: () => {\n    const theme = useTheme();\n    const isDark = theme.mode === 'dark';\n    const palette = isDark ? darkPalette : lightPalette;\n\n    const hues = Object.entries(palette.hues).map(([name, color]) => ({\n      name,\n      color,\n      ...parseHSL(color)\n    })).filter((h) => h.h !== null);\n\n    const saturations = hues.map((h) => h.s);\n    const lightnesses = hues.map((h) => h.l);\n\n    const satStats = calcStats(saturations);\n    const lightStats = calcStats(lightnesses);\n\n    const StatCard = ({ title, stats, unit = '%', values, hueData }) => (\n      <div style={{\n        padding: '20px',\n        backgroundColor: theme.background.surface0,\n        borderRadius: '12px',\n        border: `1px solid ${theme.border.border0}`,\n        marginBottom: '24px'\n      }}\n      >\n        <h3 style={{ fontSize: '16px', fontWeight: 600, marginBottom: '16px', color: theme.text }}>{title}</h3>\n\n        {/* Stats grid */}\n        <div style={{\n          display: 'grid',\n          gridTemplateColumns: 'repeat(5, 1fr)',\n          gap: '12px',\n          marginBottom: '20px'\n        }}\n        >\n          {[\n            { label: 'Mean', value: stats.mean.toFixed(1) },\n            { label: 'Std Dev', value: stats.stdDev.toFixed(1) },\n            { label: 'Min', value: stats.min },\n            { label: 'Max', value: stats.max },\n            { label: 'Range', value: stats.range }\n          ].map(({ label, value }) => (\n            <div key={label} style={{ textAlign: 'center' }}>\n              <div style={{ fontSize: '20px', fontWeight: 700, color: theme.text }}>{value}{unit}</div>\n              <div style={{ fontSize: '11px', opacity: 0.5, color: theme.text }}>{label}</div>\n            </div>\n          ))}\n        </div>\n\n        {/* Distribution visualization */}\n        <div style={{ marginTop: '16px' }}>\n          <div style={{ fontSize: '12px', opacity: 0.5, marginBottom: '8px', color: theme.text }}>Distribution</div>\n          <div style={{\n            position: 'relative',\n            height: '60px',\n            backgroundColor: theme.background.surface1,\n            borderRadius: '6px',\n            overflow: 'hidden'\n          }}\n          >\n            {/* Mean line */}\n            <div style={{\n              position: 'absolute',\n              left: `${stats.mean}%`,\n              top: 0,\n              bottom: 0,\n              width: '2px',\n              backgroundColor: theme.primary?.strong || theme.brand,\n              zIndex: 2\n            }}\n            />\n            {/* Std dev range */}\n            <div style={{\n              position: 'absolute',\n              left: `${Math.max(0, stats.mean - stats.stdDev)}%`,\n              width: `${Math.min(100, stats.stdDev * 2)}%`,\n              top: '15px',\n              bottom: '15px',\n              backgroundColor: theme.primary?.subtle || theme.brand,\n              opacity: 0.3,\n              borderRadius: '4px'\n            }}\n            />\n            {/* Individual values */}\n            {hueData.map((h, i) => (\n              <div\n                key={h.name}\n                title={`${h.name}: ${values[i]}%`}\n                style={{\n                  position: 'absolute',\n                  left: `${values[i]}%`,\n                  top: '50%',\n                  transform: 'translate(-50%, -50%)',\n                  width: '12px',\n                  height: '12px',\n                  borderRadius: '50%',\n                  backgroundColor: h.color,\n                  border: `2px solid ${theme.bg}`,\n                  zIndex: 1\n                }}\n              />\n            ))}\n          </div>\n          {/* Scale */}\n          <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '4px' }}>\n            <span style={{ fontSize: '10px', opacity: 0.4, color: theme.text }}>0%</span>\n            <span style={{ fontSize: '10px', opacity: 0.4, color: theme.text }}>50%</span>\n            <span style={{ fontSize: '10px', opacity: 0.4, color: theme.text }}>100%</span>\n          </div>\n        </div>\n      </div>\n    );\n\n    return (\n      <div style={{ padding: '24px', backgroundColor: theme.bg, minHeight: '100vh' }}>\n        <div style={{ marginBottom: '32px' }}>\n          <h1 style={{ fontSize: '32px', fontWeight: 700, marginBottom: '12px', color: theme.text }}>\n            Hue Analysis\n          </h1>\n          <p style={{\n            fontSize: '16px',\n            opacity: 0.7,\n            maxWidth: '540px',\n            lineHeight: 1.7,\n            color: theme.text\n          }}\n          >\n            Statistical analysis of saturation and lightness consistency across the 14 hue colors. Lower standard deviation indicates more uniform values.\n          </p>\n        </div>\n\n        <StatCard\n          title=\"Saturation Distribution\"\n          stats={satStats}\n          values={saturations}\n          hueData={hues}\n        />\n\n        <StatCard\n          title=\"Lightness Distribution\"\n          stats={lightStats}\n          values={lightnesses}\n          hueData={hues}\n        />\n\n        {/* Saturation Breakdown */}\n        <div style={{\n          padding: '20px',\n          backgroundColor: theme.background.surface0,\n          borderRadius: '12px',\n          border: `1px solid ${theme.border.border0}`,\n          marginBottom: '24px'\n        }}\n        >\n          <h3 style={{ fontSize: '16px', fontWeight: 600, marginBottom: '16px', color: theme.text }}>\n            Saturation by Hue\n          </h3>\n          <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>\n            {hues.map((h) => {\n              const diff = h.s - satStats.mean;\n              const isOutlier = Math.abs(diff) > satStats.stdDev;\n              return (\n                <div\n                  key={h.name}\n                  style={{\n                    display: 'flex',\n                    alignItems: 'center',\n                    gap: '12px'\n                  }}\n                >\n                  <div style={{ width: '70px', fontSize: '12px', fontWeight: 500, color: theme.text }}>{h.name}</div>\n                  <div style={{\n                    width: '24px',\n                    height: '24px',\n                    borderRadius: '4px',\n                    backgroundColor: h.color,\n                    flexShrink: 0\n                  }}\n                  />\n                  <div style={{ flex: 1, position: 'relative', height: '24px' }}>\n                    {/* Background track */}\n                    <div style={{\n                      position: 'absolute',\n                      top: '50%',\n                      transform: 'translateY(-50%)',\n                      left: 0,\n                      right: 0,\n                      height: '8px',\n                      backgroundColor: theme.background.surface1,\n                      borderRadius: '4px'\n                    }}\n                    />\n                    {/* Mean line */}\n                    <div style={{\n                      position: 'absolute',\n                      left: `${satStats.mean}%`,\n                      top: '2px',\n                      bottom: '2px',\n                      width: '2px',\n                      backgroundColor: theme.primary?.subtle || theme.brand,\n                      opacity: 0.5,\n                      borderRadius: '1px'\n                    }}\n                    />\n                    {/* Value bar */}\n                    <div style={{\n                      position: 'absolute',\n                      top: '50%',\n                      transform: 'translateY(-50%)',\n                      left: 0,\n                      width: `${h.s}%`,\n                      height: '8px',\n                      backgroundColor: h.color,\n                      borderRadius: '4px'\n                    }}\n                    />\n                  </div>\n                  <div style={{\n                    width: '70px',\n                    fontSize: '12px',\n                    fontFamily: 'monospace',\n                    textAlign: 'right',\n                    color: isOutlier ? palette.hues.ORANGE : theme.text\n                  }}\n                  >\n                    {h.s}%\n                    <span style={{ fontSize: '10px', opacity: 0.5, marginLeft: '2px' }}>\n                      {diff > 0 ? '+' : ''}{diff.toFixed(0)}\n                    </span>\n                  </div>\n                </div>\n              );\n            })}\n          </div>\n          <div style={{ marginTop: '12px', fontSize: '11px', opacity: 0.4, color: theme.text }}>\n            Mean line shown at {satStats.mean.toFixed(0)}% · Outliers ({'>'} 1σ) highlighted\n          </div>\n        </div>\n\n        {/* Lightness Breakdown */}\n        <div style={{\n          padding: '20px',\n          backgroundColor: theme.background.surface0,\n          borderRadius: '12px',\n          border: `1px solid ${theme.border.border0}`\n        }}\n        >\n          <h3 style={{ fontSize: '16px', fontWeight: 600, marginBottom: '16px', color: theme.text }}>\n            Lightness by Hue\n          </h3>\n          <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>\n            {hues.map((h) => {\n              const diff = h.l - lightStats.mean;\n              const isOutlier = Math.abs(diff) > lightStats.stdDev;\n              return (\n                <div\n                  key={h.name}\n                  style={{\n                    display: 'flex',\n                    alignItems: 'center',\n                    gap: '12px'\n                  }}\n                >\n                  <div style={{ width: '70px', fontSize: '12px', fontWeight: 500, color: theme.text }}>{h.name}</div>\n                  <div style={{\n                    width: '24px',\n                    height: '24px',\n                    borderRadius: '4px',\n                    backgroundColor: h.color,\n                    flexShrink: 0\n                  }}\n                  />\n                  <div style={{ flex: 1, position: 'relative', height: '24px' }}>\n                    {/* Background track */}\n                    <div style={{\n                      position: 'absolute',\n                      top: '50%',\n                      transform: 'translateY(-50%)',\n                      left: 0,\n                      right: 0,\n                      height: '8px',\n                      backgroundColor: theme.background.surface1,\n                      borderRadius: '4px'\n                    }}\n                    />\n                    {/* Mean line */}\n                    <div style={{\n                      position: 'absolute',\n                      left: `${lightStats.mean}%`,\n                      top: '2px',\n                      bottom: '2px',\n                      width: '2px',\n                      backgroundColor: theme.primary?.subtle || theme.brand,\n                      opacity: 0.5,\n                      borderRadius: '1px'\n                    }}\n                    />\n                    {/* Value bar */}\n                    <div style={{\n                      position: 'absolute',\n                      top: '50%',\n                      transform: 'translateY(-50%)',\n                      left: 0,\n                      width: `${h.l}%`,\n                      height: '8px',\n                      backgroundColor: h.color,\n                      borderRadius: '4px'\n                    }}\n                    />\n                  </div>\n                  <div style={{\n                    width: '70px',\n                    fontSize: '12px',\n                    fontFamily: 'monospace',\n                    textAlign: 'right',\n                    color: isOutlier ? palette.hues.ORANGE : theme.text\n                  }}\n                  >\n                    {h.l}%\n                    <span style={{ fontSize: '10px', opacity: 0.5, marginLeft: '2px' }}>\n                      {diff > 0 ? '+' : ''}{diff.toFixed(0)}\n                    </span>\n                  </div>\n                </div>\n              );\n            })}\n          </div>\n          <div style={{ marginTop: '12px', fontSize: '11px', opacity: 0.4, color: theme.text }}>\n            Mean line shown at {lightStats.mean.toFixed(0)}% · Outliers ({'>'} 1σ) highlighted\n          </div>\n        </div>\n      </div>\n    );\n  }\n};\n"
  },
  {
    "path": "packages/bruno-app/src/themes/PaletteViewer/Catppuccin.stories.jsx",
    "content": "import React from 'react';\nimport { ColorSection } from './components';\n\n// Catppuccin Latte (Light)\nconst latte = {\n  name: 'Latte',\n  mode: 'light',\n  base: '#eff1f5',\n  accents: {\n    ROSEWATER: '#dc8a78',\n    FLAMINGO: '#dd7878',\n    PINK: '#ea76cb',\n    MAUVE: '#8839ef',\n    RED: '#d20f39',\n    MAROON: '#e64553',\n    PEACH: '#fe640b',\n    YELLOW: '#df8e1d',\n    GREEN: '#40a02b',\n    TEAL: '#179299',\n    SKY: '#04a5e5',\n    SAPPHIRE: '#209fb5',\n    BLUE: '#1e66f5',\n    LAVENDER: '#7287fd'\n  },\n  surface: {\n    TEXT: '#4c4f69',\n    SUBTEXT1: '#5c5f77',\n    SUBTEXT0: '#6c6f85',\n    OVERLAY2: '#7c7f93',\n    OVERLAY1: '#8c8fa1',\n    OVERLAY0: '#9ca0b0',\n    SURFACE2: '#acb0be',\n    SURFACE1: '#bcc0cc',\n    SURFACE0: '#ccd0da',\n    BASE: '#eff1f5',\n    MANTLE: '#e6e9ef',\n    CRUST: '#dce0e8'\n  }\n};\n\n// Catppuccin Frappé (Dark)\nconst frappe = {\n  name: 'Frappé',\n  mode: 'dark',\n  base: '#303446',\n  accents: {\n    ROSEWATER: '#f2d5cf',\n    FLAMINGO: '#eebebe',\n    PINK: '#f4b8e4',\n    MAUVE: '#ca9ee6',\n    RED: '#e78284',\n    MAROON: '#ea999c',\n    PEACH: '#ef9f76',\n    YELLOW: '#e5c890',\n    GREEN: '#a6d189',\n    TEAL: '#81c8be',\n    SKY: '#99d1db',\n    SAPPHIRE: '#85c1dc',\n    BLUE: '#8caaee',\n    LAVENDER: '#babbf1'\n  },\n  surface: {\n    TEXT: '#c6d0f5',\n    SUBTEXT1: '#b5bfe2',\n    SUBTEXT0: '#a5adce',\n    OVERLAY2: '#949cbb',\n    OVERLAY1: '#838ba7',\n    OVERLAY0: '#737994',\n    SURFACE2: '#626880',\n    SURFACE1: '#51576d',\n    SURFACE0: '#414559',\n    BASE: '#303446',\n    MANTLE: '#292c3c',\n    CRUST: '#232634'\n  }\n};\n\n// Catppuccin Macchiato (Dark)\nconst macchiato = {\n  name: 'Macchiato',\n  mode: 'dark',\n  base: '#24273a',\n  accents: {\n    ROSEWATER: '#f4dbd6',\n    FLAMINGO: '#f0c6c6',\n    PINK: '#f5bde6',\n    MAUVE: '#c6a0f6',\n    RED: '#ed8796',\n    MAROON: '#ee99a0',\n    PEACH: '#f5a97f',\n    YELLOW: '#eed49f',\n    GREEN: '#a6da95',\n    TEAL: '#8bd5ca',\n    SKY: '#91d7e3',\n    SAPPHIRE: '#7dc4e4',\n    BLUE: '#8aadf4',\n    LAVENDER: '#b7bdf8'\n  },\n  surface: {\n    TEXT: '#cad3f5',\n    SUBTEXT1: '#b8c0e0',\n    SUBTEXT0: '#a5adcb',\n    OVERLAY2: '#939ab7',\n    OVERLAY1: '#8087a2',\n    OVERLAY0: '#6e738d',\n    SURFACE2: '#5b6078',\n    SURFACE1: '#494d64',\n    SURFACE0: '#363a4f',\n    BASE: '#24273a',\n    MANTLE: '#1e2030',\n    CRUST: '#181926'\n  }\n};\n\n// Catppuccin Mocha (Dark)\nconst mocha = {\n  name: 'Mocha',\n  mode: 'dark',\n  base: '#1e1e2e',\n  accents: {\n    ROSEWATER: '#f5e0dc',\n    FLAMINGO: '#f2cdcd',\n    PINK: '#f5c2e7',\n    MAUVE: '#cba6f7',\n    RED: '#f38ba8',\n    MAROON: '#eba0ac',\n    PEACH: '#fab387',\n    YELLOW: '#f9e2af',\n    GREEN: '#a6e3a1',\n    TEAL: '#94e2d5',\n    SKY: '#89dceb',\n    SAPPHIRE: '#74c7ec',\n    BLUE: '#89b4fa',\n    LAVENDER: '#b4befe'\n  },\n  surface: {\n    TEXT: '#cdd6f4',\n    SUBTEXT1: '#bac2de',\n    SUBTEXT0: '#a6adc8',\n    OVERLAY2: '#9399b2',\n    OVERLAY1: '#7f849c',\n    OVERLAY0: '#6c7086',\n    SURFACE2: '#585b70',\n    SURFACE1: '#45475a',\n    SURFACE0: '#313244',\n    BASE: '#1e1e2e',\n    MANTLE: '#181825',\n    CRUST: '#11111b'\n  }\n};\n\nconst themes = [latte, frappe, macchiato, mocha];\n\nconst ThemeSection = ({ theme }) => {\n  const textColor = theme.mode === 'dark' ? '#cdd6f4' : '#4c4f69';\n  const mutedColor = theme.mode === 'dark' ? '#a6adc8' : '#6c6f85';\n\n  return (\n    <div\n      style={{\n        padding: '24px',\n        backgroundColor: theme.base,\n        borderRadius: '12px',\n        marginBottom: '24px'\n      }}\n    >\n      <h2 style={{ fontSize: '20px', fontWeight: 700, marginBottom: '4px', color: textColor }}>\n        Catppuccin {theme.name}\n      </h2>\n      <p style={{ fontSize: '12px', color: mutedColor, marginBottom: '20px' }}>\n        {theme.mode === 'light' ? 'Light theme' : 'Dark theme'} — Base: {theme.base}\n      </p>\n\n      <ColorSection title=\"Accents\" colors={theme.accents} textColor={textColor} />\n      <ColorSection title=\"Surface & Text\" colors={theme.surface} textColor={textColor} />\n    </div>\n  );\n};\n\nexport default {\n  title: 'Themes/Catppuccin',\n  parameters: {\n    layout: 'padded'\n  }\n};\n\nexport const AllFlavors = {\n  render: () => (\n    <div style={{ padding: '24px', backgroundColor: '#1a1a2e', minHeight: '100vh' }}>\n      <h1 style={{ fontSize: '28px', fontWeight: 700, marginBottom: '8px', color: '#cdd6f4' }}>\n        Catppuccin Palette\n      </h1>\n      <p style={{ fontSize: '14px', color: '#a6adc8', marginBottom: '32px' }}>\n        All 4 flavors: Latte, Frappé, Macchiato, Mocha\n      </p>\n      {themes.map((theme) => (\n        <ThemeSection key={theme.name} theme={theme} />\n      ))}\n    </div>\n  )\n};\n\nexport const Latte = {\n  render: () => (\n    <div style={{ padding: '24px', backgroundColor: '#dce0e8', minHeight: '100vh' }}>\n      <ThemeSection theme={latte} />\n    </div>\n  )\n};\n\nexport const Frappe = {\n  render: () => (\n    <div style={{ padding: '24px', backgroundColor: '#232634', minHeight: '100vh' }}>\n      <ThemeSection theme={frappe} />\n    </div>\n  )\n};\n\nexport const Macchiato = {\n  render: () => (\n    <div style={{ padding: '24px', backgroundColor: '#181926', minHeight: '100vh' }}>\n      <ThemeSection theme={macchiato} />\n    </div>\n  )\n};\n\nexport const Mocha = {\n  render: () => (\n    <div style={{ padding: '24px', backgroundColor: '#11111b', minHeight: '100vh' }}>\n      <ThemeSection theme={mocha} />\n    </div>\n  )\n};\n\nexport const AccentComparison = {\n  render: () => {\n    const accentNames = Object.keys(latte.accents);\n\n    return (\n      <div style={{ padding: '24px', backgroundColor: '#1a1a2e', minHeight: '100vh' }}>\n        <h2 style={{ fontSize: '20px', fontWeight: 700, marginBottom: '24px', color: '#cdd6f4' }}>\n          Accent Comparison Across Flavors\n        </h2>\n        <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>\n          <div style={{ display: 'flex', gap: '16px', marginBottom: '8px' }}>\n            <div style={{ width: '80px' }} />\n            {themes.map((t) => (\n              <div\n                key={t.name}\n                style={{\n                  width: '80px',\n                  textAlign: 'center',\n                  fontSize: '12px',\n                  fontWeight: 600,\n                  color: '#cdd6f4'\n                }}\n              >\n                {t.name}\n              </div>\n            ))}\n          </div>\n          {accentNames.map((name) => (\n            <div key={name} style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>\n              <div style={{ width: '80px', fontSize: '11px', fontWeight: 600, color: '#a6adc8' }}>\n                {name}\n              </div>\n              {themes.map((t) => (\n                <div\n                  key={t.name}\n                  style={{\n                    width: '80px',\n                    height: '32px',\n                    backgroundColor: t.accents[name],\n                    borderRadius: '4px'\n                  }}\n                />\n              ))}\n            </div>\n          ))}\n        </div>\n      </div>\n    );\n  }\n};\n"
  },
  {
    "path": "packages/bruno-app/src/themes/PaletteViewer/components.jsx",
    "content": "import React from 'react';\n\n// Shorten long names for display\nconst formatName = (name) => {\n  if (name === 'CONTROL_ACCENT') return 'CTRL_ACC';\n  return name;\n};\n\n// Shorten HSL colors for display\nconst formatColor = (color) => {\n  if (color.startsWith('hsl(')) {\n    // Handle all HSL formats: \"hsl(0 70% 71%)\", \"hsl(0, 70%, 71%)\", \"hsl(0deg 0% 10%)\"\n    const match = color.match(/hsl\\((\\d+)(?:deg)?\\s*,?\\s*(\\d+)%\\s*,?\\s*(\\d+)%\\)/);\n    if (match) {\n      return `${match[1]}/${match[2]}/${match[3]}`;\n    }\n  }\n  return color;\n};\n\nexport const ColorSwatch = ({ name, color, textColor = '#cccccc', size = 56 }) => (\n  <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '6px' }}>\n    <div\n      style={{\n        width: `${size}px`,\n        height: `${size}px`,\n        backgroundColor: color,\n        borderRadius: '8px',\n        border: '1px solid rgba(128,128,128,0.2)'\n      }}\n    />\n    <div style={{ textAlign: 'center' }}>\n      <div style={{ fontSize: '10px', fontWeight: 600, color: textColor }}>{formatName(name)}</div>\n      <div style={{ fontSize: '9px', color: textColor, opacity: 0.7, fontFamily: 'monospace' }}>{formatColor(color)}</div>\n    </div>\n  </div>\n);\n\nexport const ColorSection = ({ title, colors, textColor = '#cccccc', size = 56 }) => (\n  <div style={{ marginBottom: '24px' }}>\n    <h3 style={{ fontSize: '14px', fontWeight: 600, marginBottom: '12px', color: textColor }}>{title}</h3>\n    <div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px' }}>\n      {Object.entries(colors).map(([name, color]) => (\n        <ColorSwatch key={name} name={name} color={color} textColor={textColor} size={size} />\n      ))}\n    </div>\n  </div>\n);\n"
  },
  {
    "path": "packages/bruno-app/src/themes/dark/catppuccin-frappe.js",
    "content": "// Catppuccin Frappé - Dark Theme\n// Based on https://catppuccin.com/palette/\n\nconst { rgba } = require('polished');\n\nconst colors = {\n  // Catppuccin Frappé Palette\n  ROSEWATER: '#f2d5cf',\n  FLAMINGO: '#eebebe',\n  PINK: '#f4b8e4',\n  MAUVE: '#ca9ee6',\n  RED: '#e78284',\n  MAROON: '#ea999c',\n  PEACH: '#ef9f76',\n  YELLOW: '#e5c890',\n  GREEN: '#a6d189',\n  TEAL: '#81c8be',\n  SKY: '#99d1db',\n  SAPPHIRE: '#85c1dc',\n  BLUE: '#8caaee',\n  LAVENDER: '#babbf1',\n\n  TEXT: '#c6d0f5',\n  SUBTEXT1: '#b5bfe2',\n  SUBTEXT0: '#a5adce',\n  OVERLAY2: '#949cbb',\n  OVERLAY1: '#838ba7',\n  OVERLAY0: '#737994',\n  SURFACE2: '#626880',\n  SURFACE1: '#51576d',\n  SURFACE0: '#414559',\n  BASE: '#303446',\n  MANTLE: '#292c3c',\n  CRUST: '#232634',\n\n  WHITE: '#fff',\n  BLACK: '#000',\n\n  CODEMIRROR_TOKENS: {\n    DEFINITION: '#a6d189',\n    PROPERTY: '#8caaee',\n    STRING: '#e5c890',\n    NUMBER: '#ef9f76',\n    ATOM: '#f4b8e4',\n    VARIABLE: '#85c1dc',\n    KEYWORD: '#e78284',\n    COMMENT: '#737994',\n    OPERATOR: '#81c8be',\n    TAG: '#8caaee',\n    TAG_BRACKET: '#737994'\n  }\n};\n\nexport const palette = {};\n\npalette.intent = {\n  INFO: colors.BLUE,\n  SUCCESS: colors.GREEN,\n  WARNING: colors.PEACH,\n  DANGER: colors.RED\n};\n\nconst catppuccinFrappeTheme = {\n  mode: 'dark',\n  brand: colors.MAUVE,\n  text: colors.TEXT,\n  textLink: colors.BLUE,\n  draftColor: '#cc7b1b',\n  bg: colors.BASE,\n\n  primary: {\n    solid: colors.MAUVE,\n    text: colors.MAUVE,\n    strong: colors.MAUVE,\n    subtle: colors.MAUVE\n  },\n\n  accents: {\n    primary: colors.MAUVE\n  },\n\n  background: {\n    base: colors.BASE,\n    mantle: colors.MANTLE,\n    crust: colors.CRUST,\n    surface0: colors.SURFACE0,\n    surface1: colors.SURFACE1,\n    surface2: colors.SURFACE2\n  },\n\n  status: {\n    info: {\n      background: rgba(palette.intent.INFO, 0.15),\n      text: palette.intent.INFO,\n      border: palette.intent.INFO\n    },\n    success: {\n      background: rgba(palette.intent.SUCCESS, 0.15),\n      text: palette.intent.SUCCESS,\n      border: palette.intent.SUCCESS\n    },\n    warning: {\n      background: rgba(palette.intent.WARNING, 0.15),\n      text: palette.intent.WARNING,\n      border: palette.intent.WARNING\n    },\n    danger: {\n      background: rgba(palette.intent.DANGER, 0.15),\n      text: palette.intent.DANGER,\n      border: palette.intent.DANGER\n    }\n  },\n\n  overlay: {\n    overlay2: colors.OVERLAY2,\n    overlay1: colors.OVERLAY1,\n    overlay0: colors.OVERLAY0\n  },\n\n  font: {\n    size: {\n      xs: '0.6875rem', // 11px\n      sm: '0.75rem', // 12px\n      base: '0.8125rem', // 13px\n      md: '0.875rem', // 14px\n      lg: '1rem', // 16px\n      xl: '1.125rem' // 18px\n    }\n  },\n\n  shadow: {\n    sm: '0 1px 3px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(0, 0, 0, 0.3)',\n    md: '0 2px 8px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(0, 0, 0, 0.4)',\n    lg: '0 2px 12px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(0, 0, 0, 0.4)'\n  },\n\n  border: {\n    radius: {\n      sm: '4px',\n      base: '6px',\n      md: '8px',\n      lg: '10px',\n      xl: '12px'\n    },\n    border2: colors.SURFACE2,\n    border1: colors.SURFACE1,\n    border0: colors.SURFACE0\n  },\n\n  colors: {\n    text: {\n      white: colors.WHITE,\n      green: colors.GREEN,\n      danger: colors.RED,\n      warning: colors.PEACH,\n      muted: colors.SUBTEXT0,\n      purple: colors.MAUVE,\n      yellow: colors.YELLOW,\n      subtext2: colors.TEXT,\n      subtext1: colors.SUBTEXT1,\n      subtext0: colors.SUBTEXT0\n    },\n    bg: {\n      danger: colors.MAROON\n    },\n    accent: colors.MAUVE\n  },\n\n  input: {\n    bg: 'transparent',\n    border: colors.SURFACE1,\n    focusBorder: colors.LAVENDER,\n    placeholder: {\n      color: colors.OVERLAY0,\n      opacity: 0.75\n    }\n  },\n\n  sidebar: {\n    color: colors.TEXT,\n    muted: colors.SUBTEXT0,\n    bg: colors.BASE,\n    dragbar: {\n      border: colors.SURFACE0,\n      activeBorder: colors.OVERLAY0\n    },\n\n    collection: {\n      item: {\n        bg: colors.SURFACE0,\n        hoverBg: colors.SURFACE0,\n        focusBorder: colors.SURFACE1,\n        indentBorder: colors.SURFACE0,\n        active: {\n          indentBorder: colors.SURFACE0\n        },\n        example: {\n          iconColor: colors.OVERLAY1\n        }\n      }\n    },\n\n    dropdownIcon: {\n      color: colors.TEXT\n    }\n  },\n\n  dropdown: {\n    color: colors.TEXT,\n    iconColor: colors.SUBTEXT1,\n    bg: colors.SURFACE0,\n    hoverBg: 'rgba(115, 121, 148, 0.16)',\n    shadow: 'none',\n    border: rgba(colors.SURFACE1, 0.5),\n    separator: colors.SURFACE1,\n    selectedColor: colors.MAUVE,\n    mutedText: colors.SUBTEXT0\n  },\n\n  workspace: {\n    accent: colors.MAUVE,\n    border: colors.SURFACE1,\n    button: {\n      bg: colors.SURFACE0\n    }\n  },\n\n  request: {\n    methods: {\n      get: colors.GREEN,\n      post: colors.BLUE,\n      put: colors.YELLOW,\n      delete: colors.RED,\n      patch: colors.PEACH,\n      options: colors.TEAL,\n      head: colors.SAPPHIRE\n    },\n\n    grpc: colors.SKY,\n    ws: colors.MAUVE,\n    gql: colors.PINK\n  },\n\n  requestTabPanel: {\n    url: {\n      bg: colors.BASE,\n      icon: colors.TEXT,\n      iconDanger: colors.RED,\n      border: `solid 1px ${colors.SURFACE0}`\n    },\n    dragbar: {\n      border: colors.SURFACE0,\n      activeBorder: colors.OVERLAY0\n    },\n    responseStatus: colors.TEXT,\n    responseOk: colors.GREEN,\n    responseError: colors.RED,\n    responsePending: colors.BLUE,\n    responseOverlayBg: 'rgba(48, 52, 70, 0.6)',\n\n    card: {\n      bg: colors.MANTLE,\n      border: 'transparent',\n      hr: colors.SURFACE0\n    },\n\n    graphqlDocsExplorer: {\n      bg: colors.BASE,\n      color: colors.TEXT\n    }\n  },\n\n  notifications: {\n    bg: colors.SURFACE0,\n    list: {\n      bg: colors.SURFACE0,\n      borderRight: colors.SURFACE2,\n      borderBottom: colors.SURFACE1,\n      hoverBg: colors.SURFACE1,\n      active: {\n        border: colors.BLUE,\n        bg: colors.SURFACE2,\n        hoverBg: colors.SURFACE2\n      }\n    }\n  },\n\n  modal: {\n    title: {\n      color: colors.TEXT,\n      bg: colors.MANTLE\n    },\n    body: {\n      color: colors.TEXT,\n      bg: colors.BASE\n    },\n    input: {\n      bg: 'transparent',\n      border: colors.SURFACE1,\n      focusBorder: colors.LAVENDER\n    },\n    backdrop: {\n      opacity: 0.2\n    }\n  },\n\n  button: {\n    secondary: {\n      color: colors.TEXT,\n      bg: colors.SURFACE0,\n      border: colors.SURFACE0,\n      hoverBorder: colors.OVERLAY0\n    },\n    close: {\n      color: colors.TEXT,\n      bg: 'transparent',\n      border: 'transparent',\n      hoverBorder: ''\n    },\n    disabled: {\n      color: colors.OVERLAY0,\n      bg: colors.SURFACE1,\n      border: colors.SURFACE1\n    },\n    danger: {\n      color: colors.CRUST,\n      bg: colors.RED,\n      border: colors.RED\n    }\n  },\n  button2: {\n    color: {\n      primary: {\n        bg: colors.MAUVE,\n        text: colors.CRUST,\n        border: colors.MAUVE\n      },\n      light: {\n        bg: rgba(colors.MAUVE, 0.08),\n        text: colors.MAUVE,\n        border: rgba(colors.MAUVE, 0.06)\n      },\n      secondary: {\n        bg: colors.SURFACE0,\n        text: colors.TEXT,\n        border: colors.SURFACE1\n      },\n      success: {\n        bg: colors.GREEN,\n        text: colors.CRUST,\n        border: colors.GREEN\n      },\n      warning: {\n        bg: colors.PEACH,\n        text: colors.CRUST,\n        border: colors.PEACH\n      },\n      danger: {\n        bg: colors.RED,\n        text: colors.CRUST,\n        border: colors.RED\n      }\n    }\n  },\n\n  tabs: {\n    marginRight: '1.2rem',\n    active: {\n      fontWeight: 400,\n      color: colors.TEXT,\n      border: colors.MAUVE\n    },\n    secondary: {\n      active: {\n        bg: colors.SURFACE0,\n        color: colors.TEXT\n      },\n      inactive: {\n        bg: colors.SURFACE0,\n        color: colors.SUBTEXT0\n      }\n    }\n  },\n\n  requestTabs: {\n    color: colors.TEXT,\n    bg: colors.SURFACE0,\n    bottomBorder: colors.SURFACE1,\n    icon: {\n      color: colors.OVERLAY0,\n      hoverColor: colors.TEXT,\n      hoverBg: colors.BASE\n    },\n    example: {\n      iconColor: colors.OVERLAY1\n    }\n  },\n\n  codemirror: {\n    bg: colors.BASE,\n    border: colors.BASE,\n    placeholder: {\n      color: colors.OVERLAY0,\n      opacity: 0.5\n    },\n    gutter: {\n      bg: colors.BASE\n    },\n    variable: {\n      valid: colors.GREEN,\n      invalid: colors.RED,\n      prompt: colors.BLUE\n    },\n    tokens: {\n      definition: colors.CODEMIRROR_TOKENS.DEFINITION,\n      property: colors.CODEMIRROR_TOKENS.PROPERTY,\n      string: colors.CODEMIRROR_TOKENS.STRING,\n      number: colors.CODEMIRROR_TOKENS.NUMBER,\n      atom: colors.CODEMIRROR_TOKENS.ATOM,\n      variable: colors.CODEMIRROR_TOKENS.VARIABLE,\n      keyword: colors.CODEMIRROR_TOKENS.KEYWORD,\n      comment: colors.CODEMIRROR_TOKENS.COMMENT,\n      operator: colors.CODEMIRROR_TOKENS.OPERATOR,\n      tag: colors.CODEMIRROR_TOKENS.TAG,\n      tagBracket: colors.CODEMIRROR_TOKENS.TAG_BRACKET\n    },\n    searchLineHighlightCurrent: 'rgba(115, 121, 148, 0.18)',\n    searchMatch: colors.YELLOW,\n    searchMatchActive: colors.PEACH\n  },\n\n  table: {\n    border: colors.SURFACE0,\n    thead: {\n      color: colors.TEXT\n    },\n    striped: colors.SURFACE0,\n    input: {\n      color: colors.TEXT\n    }\n  },\n\n  plainGrid: {\n    hoverBg: colors.SURFACE0\n  },\n\n  scrollbar: {\n    color: colors.SURFACE0\n  },\n\n  dragAndDrop: {\n    border: colors.LAVENDER,\n    borderStyle: '2px solid',\n    hoverBg: 'rgba(186, 187, 241, 0.08)',\n    transition: 'all 0.1s ease'\n  },\n  infoTip: {\n    bg: colors.SURFACE0,\n    border: colors.SURFACE1,\n    boxShadow: '0 4px 12px rgba(0, 0, 0, 0.5)'\n  },\n\n  statusBar: {\n    border: colors.SURFACE0,\n    color: colors.SUBTEXT0\n  },\n\n  console: {\n    bg: colors.BASE,\n    headerBg: colors.MANTLE,\n    contentBg: colors.BASE,\n    border: colors.SURFACE0,\n    titleColor: colors.TEXT,\n    countColor: colors.SUBTEXT0,\n    buttonColor: colors.TEXT,\n    buttonHoverBg: 'rgba(198, 208, 245, 0.1)',\n    buttonHoverColor: colors.TEXT,\n    messageColor: colors.TEXT,\n    timestampColor: colors.SUBTEXT0,\n    emptyColor: colors.SUBTEXT0,\n    logHoverBg: 'rgba(198, 208, 245, 0.05)',\n    resizeHandleHover: colors.BLUE,\n    resizeHandleActive: colors.BLUE,\n    dropdownBg: colors.MANTLE,\n    dropdownHeaderBg: colors.SURFACE0,\n    optionHoverBg: 'rgba(198, 208, 245, 0.05)',\n    optionLabelColor: colors.TEXT,\n    optionCountColor: colors.SUBTEXT0,\n    checkboxColor: colors.MAUVE,\n    scrollbarTrack: colors.MANTLE,\n    scrollbarThumb: colors.SURFACE2,\n    scrollbarThumbHover: colors.OVERLAY0\n  },\n\n  grpc: {\n    tabNav: {\n      container: {\n        bg: colors.CRUST\n      },\n      button: {\n        active: {\n          bg: colors.SURFACE0,\n          color: colors.TEXT\n        },\n        inactive: {\n          bg: 'transparent',\n          color: colors.SUBTEXT0\n        }\n      }\n    },\n    importPaths: {\n      header: {\n        text: colors.SUBTEXT0,\n        button: {\n          color: colors.SUBTEXT0,\n          hoverColor: colors.TEXT\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: colors.RED,\n        link: {\n          color: colors.RED,\n          hoverColor: colors.MAROON\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: 'rgba(198, 208, 245, 0.05)',\n        text: colors.TEXT,\n        icon: colors.SUBTEXT0,\n        checkbox: {\n          color: colors.TEXT\n        },\n        invalid: {\n          opacity: 0.6,\n          text: colors.RED\n        }\n      },\n      empty: {\n        text: colors.SUBTEXT0\n      },\n      button: {\n        bg: colors.SURFACE0,\n        color: colors.TEXT,\n        border: colors.SURFACE0,\n        hoverBorder: colors.OVERLAY0\n      }\n    },\n    protoFiles: {\n      header: {\n        text: colors.SUBTEXT0,\n        button: {\n          color: colors.SUBTEXT0,\n          hoverColor: colors.TEXT\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: colors.RED,\n        link: {\n          color: colors.RED,\n          hoverColor: colors.MAROON\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: 'rgba(198, 208, 245, 0.05)',\n        selected: {\n          bg: 'rgba(202, 158, 230, 0.2)',\n          border: colors.MAUVE\n        },\n        text: colors.TEXT,\n        secondaryText: colors.SUBTEXT0,\n        icon: colors.SUBTEXT0,\n        invalid: {\n          opacity: 0.6,\n          text: colors.RED\n        }\n      },\n      empty: {\n        text: colors.SUBTEXT0\n      },\n      button: {\n        bg: colors.SURFACE0,\n        color: colors.TEXT,\n        border: colors.SURFACE0,\n        hoverBorder: colors.OVERLAY0\n      }\n    }\n  },\n  deprecationWarning: {\n    bg: 'rgba(231, 130, 132, 0.1)',\n    border: 'rgba(231, 130, 132, 0.1)',\n    icon: colors.RED,\n    text: colors.SUBTEXT1\n  },\n\n  examples: {\n    buttonBg: 'rgba(202, 158, 230, 0.1)',\n    buttonColor: colors.MAUVE,\n    buttonText: colors.TEXT,\n    buttonIconColor: colors.TEXT,\n    border: colors.SURFACE1,\n    urlBar: {\n      border: colors.SURFACE0,\n      bg: colors.MANTLE\n    },\n    table: {\n      thead: {\n        bg: colors.MANTLE,\n        color: colors.SUBTEXT0\n      }\n    },\n    checkbox: {\n      color: colors.CRUST\n    }\n  },\n\n  app: {\n    collection: {\n      toolbar: {\n        environmentSelector: {\n          bg: colors.BASE,\n          border: colors.SURFACE0,\n          icon: colors.MAUVE,\n          text: colors.TEXT,\n          caret: colors.SUBTEXT0,\n          separator: colors.SURFACE0,\n          hoverBg: colors.BASE,\n          hoverBorder: colors.SURFACE1,\n\n          noEnvironment: {\n            text: colors.SUBTEXT0,\n            bg: colors.BASE,\n            border: colors.SURFACE0,\n            hoverBg: colors.BASE,\n            hoverBorder: colors.SURFACE1\n          }\n        },\n        sandboxMode: {\n          safeMode: {\n            bg: 'rgba(166, 209, 137, 0.12)',\n            color: colors.GREEN\n          },\n          developerMode: {\n            bg: 'rgba(229, 200, 144, 0.11)',\n            color: colors.YELLOW\n          }\n        }\n      }\n    }\n  }\n};\n\nexport default catppuccinFrappeTheme;\n"
  },
  {
    "path": "packages/bruno-app/src/themes/dark/catppuccin-macchiato.js",
    "content": "// Catppuccin Macchiato - Dark Theme\n// Based on https://catppuccin.com/palette/\n\nconst { rgba } = require('polished');\n\nconst colors = {\n  // Catppuccin Macchiato Palette\n  ROSEWATER: '#f4dbd6',\n  FLAMINGO: '#f0c6c6',\n  PINK: '#f5bde6',\n  MAUVE: '#c6a0f6',\n  RED: '#ed8796',\n  MAROON: '#ee99a0',\n  PEACH: '#f5a97f',\n  YELLOW: '#eed49f',\n  GREEN: '#a6da95',\n  TEAL: '#8bd5ca',\n  SKY: '#91d7e3',\n  SAPPHIRE: '#7dc4e4',\n  BLUE: '#8aadf4',\n  LAVENDER: '#b7bdf8',\n\n  TEXT: '#cad3f5',\n  SUBTEXT1: '#b8c0e0',\n  SUBTEXT0: '#a5adcb',\n  OVERLAY2: '#939ab7',\n  OVERLAY1: '#8087a2',\n  OVERLAY0: '#6e738d',\n  SURFACE2: '#5b6078',\n  SURFACE1: '#494d64',\n  SURFACE0: '#363a4f',\n  BASE: '#24273a',\n  MANTLE: '#1e2030',\n  CRUST: '#181926',\n\n  WHITE: '#fff',\n  BLACK: '#000',\n\n  CODEMIRROR_TOKENS: {\n    DEFINITION: '#a6da95',\n    PROPERTY: '#8aadf4',\n    STRING: '#eed49f',\n    NUMBER: '#f5a97f',\n    ATOM: '#f5bde6',\n    VARIABLE: '#7dc4e4',\n    KEYWORD: '#ed8796',\n    COMMENT: '#6e738d',\n    OPERATOR: '#8bd5ca',\n    TAG: '#8aadf4',\n    TAG_BRACKET: '#6e738d'\n  }\n};\n\nexport const palette = {};\n\npalette.intent = {\n  INFO: colors.BLUE,\n  SUCCESS: colors.GREEN,\n  WARNING: colors.PEACH,\n  DANGER: colors.RED\n};\n\nconst catppuccinMacchiatoTheme = {\n  mode: 'dark',\n  brand: colors.MAUVE,\n  text: colors.TEXT,\n  textLink: colors.BLUE,\n  draftColor: '#cc7b1b',\n  bg: colors.BASE,\n\n  primary: {\n    solid: colors.MAUVE,\n    text: colors.MAUVE,\n    strong: colors.MAUVE,\n    subtle: colors.MAUVE\n  },\n\n  accents: {\n    primary: colors.MAUVE\n  },\n\n  background: {\n    base: colors.BASE,\n    mantle: colors.MANTLE,\n    crust: colors.CRUST,\n    surface0: colors.SURFACE0,\n    surface1: colors.SURFACE1,\n    surface2: colors.SURFACE2\n  },\n\n  status: {\n    info: {\n      background: rgba(palette.intent.INFO, 0.15),\n      text: palette.intent.INFO,\n      border: palette.intent.INFO\n    },\n    success: {\n      background: rgba(palette.intent.SUCCESS, 0.15),\n      text: palette.intent.SUCCESS,\n      border: palette.intent.SUCCESS\n    },\n    warning: {\n      background: rgba(palette.intent.WARNING, 0.15),\n      text: palette.intent.WARNING,\n      border: palette.intent.WARNING\n    },\n    danger: {\n      background: rgba(palette.intent.DANGER, 0.15),\n      text: palette.intent.DANGER,\n      border: palette.intent.DANGER\n    }\n  },\n\n  overlay: {\n    overlay2: colors.OVERLAY2,\n    overlay1: colors.OVERLAY1,\n    overlay0: colors.OVERLAY0\n  },\n\n  font: {\n    size: {\n      xs: '0.6875rem', // 11px\n      sm: '0.75rem', // 12px\n      base: '0.8125rem', // 13px\n      md: '0.875rem', // 14px\n      lg: '1rem', // 16px\n      xl: '1.125rem' // 18px\n    }\n  },\n\n  shadow: {\n    sm: '0 1px 3px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(0, 0, 0, 0.3)',\n    md: '0 2px 8px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(0, 0, 0, 0.4)',\n    lg: '0 2px 12px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(0, 0, 0, 0.4)'\n  },\n\n  border: {\n    radius: {\n      sm: '4px',\n      base: '6px',\n      md: '8px',\n      lg: '10px',\n      xl: '12px'\n    },\n    border2: colors.SURFACE2,\n    border1: colors.SURFACE1,\n    border0: colors.SURFACE0\n  },\n\n  colors: {\n    text: {\n      white: colors.WHITE,\n      green: colors.GREEN,\n      danger: colors.RED,\n      warning: colors.PEACH,\n      muted: colors.SUBTEXT0,\n      purple: colors.MAUVE,\n      yellow: colors.YELLOW,\n      subtext2: colors.TEXT,\n      subtext1: colors.SUBTEXT1,\n      subtext0: colors.SUBTEXT0\n    },\n    bg: {\n      danger: colors.MAROON\n    },\n    accent: colors.MAUVE\n  },\n\n  input: {\n    bg: 'transparent',\n    border: colors.SURFACE1,\n    focusBorder: colors.LAVENDER,\n    placeholder: {\n      color: colors.OVERLAY0,\n      opacity: 0.75\n    }\n  },\n\n  sidebar: {\n    color: colors.TEXT,\n    muted: colors.SUBTEXT0,\n    bg: colors.BASE,\n    dragbar: {\n      border: colors.SURFACE0,\n      activeBorder: colors.OVERLAY0\n    },\n\n    collection: {\n      item: {\n        bg: colors.SURFACE0,\n        hoverBg: colors.SURFACE0,\n        focusBorder: colors.SURFACE1,\n        indentBorder: colors.SURFACE0,\n        active: {\n          indentBorder: colors.SURFACE0\n        },\n        example: {\n          iconColor: colors.OVERLAY1\n        }\n      }\n    },\n\n    dropdownIcon: {\n      color: colors.TEXT\n    }\n  },\n\n  dropdown: {\n    color: colors.TEXT,\n    iconColor: colors.SUBTEXT1,\n    bg: colors.SURFACE0,\n    hoverBg: 'rgba(110, 115, 141, 0.16)',\n    shadow: 'none',\n    border: rgba(colors.SURFACE1, 0.5),\n    separator: colors.SURFACE1,\n    selectedColor: colors.MAUVE,\n    mutedText: colors.SUBTEXT0\n  },\n\n  workspace: {\n    accent: colors.MAUVE,\n    border: colors.SURFACE1,\n    button: {\n      bg: colors.SURFACE0\n    }\n  },\n\n  request: {\n    methods: {\n      get: colors.GREEN,\n      post: colors.BLUE,\n      put: colors.YELLOW,\n      delete: colors.RED,\n      patch: colors.PEACH,\n      options: colors.TEAL,\n      head: colors.SAPPHIRE\n    },\n\n    grpc: colors.SKY,\n    ws: colors.MAUVE,\n    gql: colors.PINK\n  },\n\n  requestTabPanel: {\n    url: {\n      bg: colors.BASE,\n      icon: colors.TEXT,\n      iconDanger: colors.RED,\n      border: `solid 1px ${colors.SURFACE0}`\n    },\n    dragbar: {\n      border: colors.SURFACE0,\n      activeBorder: colors.OVERLAY0\n    },\n    responseStatus: colors.TEXT,\n    responseOk: colors.GREEN,\n    responseError: colors.RED,\n    responsePending: colors.BLUE,\n    responseOverlayBg: 'rgba(36, 39, 58, 0.6)',\n\n    card: {\n      bg: colors.MANTLE,\n      border: 'transparent',\n      hr: colors.SURFACE0\n    },\n\n    graphqlDocsExplorer: {\n      bg: colors.BASE,\n      color: colors.TEXT\n    }\n  },\n\n  notifications: {\n    bg: colors.SURFACE0,\n    list: {\n      bg: colors.SURFACE0,\n      borderRight: colors.SURFACE2,\n      borderBottom: colors.SURFACE1,\n      hoverBg: colors.SURFACE1,\n      active: {\n        border: colors.BLUE,\n        bg: colors.SURFACE2,\n        hoverBg: colors.SURFACE2\n      }\n    }\n  },\n\n  modal: {\n    title: {\n      color: colors.TEXT,\n      bg: colors.MANTLE\n    },\n    body: {\n      color: colors.TEXT,\n      bg: colors.BASE\n    },\n    input: {\n      bg: 'transparent',\n      border: colors.SURFACE1,\n      focusBorder: colors.LAVENDER\n    },\n    backdrop: {\n      opacity: 0.2\n    }\n  },\n\n  button: {\n    secondary: {\n      color: colors.TEXT,\n      bg: colors.SURFACE0,\n      border: colors.SURFACE0,\n      hoverBorder: colors.OVERLAY0\n    },\n    close: {\n      color: colors.TEXT,\n      bg: 'transparent',\n      border: 'transparent',\n      hoverBorder: ''\n    },\n    disabled: {\n      color: colors.OVERLAY0,\n      bg: colors.SURFACE1,\n      border: colors.SURFACE1\n    },\n    danger: {\n      color: colors.CRUST,\n      bg: colors.RED,\n      border: colors.RED\n    }\n  },\n  button2: {\n    color: {\n      primary: {\n        bg: colors.MAUVE,\n        text: colors.CRUST,\n        border: colors.MAUVE\n      },\n      light: {\n        bg: rgba(colors.MAUVE, 0.08),\n        text: colors.MAUVE,\n        border: rgba(colors.MAUVE, 0.06)\n      },\n      secondary: {\n        bg: colors.SURFACE0,\n        text: colors.TEXT,\n        border: colors.SURFACE1\n      },\n      success: {\n        bg: colors.GREEN,\n        text: colors.CRUST,\n        border: colors.GREEN\n      },\n      warning: {\n        bg: colors.PEACH,\n        text: colors.CRUST,\n        border: colors.PEACH\n      },\n      danger: {\n        bg: colors.RED,\n        text: colors.CRUST,\n        border: colors.RED\n      }\n    }\n  },\n\n  tabs: {\n    marginRight: '1.2rem',\n    active: {\n      fontWeight: 400,\n      color: colors.TEXT,\n      border: colors.MAUVE\n    },\n    secondary: {\n      active: {\n        bg: colors.SURFACE0,\n        color: colors.TEXT\n      },\n      inactive: {\n        bg: colors.SURFACE0,\n        color: colors.SUBTEXT0\n      }\n    }\n  },\n\n  requestTabs: {\n    color: colors.TEXT,\n    bg: colors.SURFACE0,\n    bottomBorder: colors.SURFACE1,\n    icon: {\n      color: colors.OVERLAY0,\n      hoverColor: colors.TEXT,\n      hoverBg: colors.BASE\n    },\n    example: {\n      iconColor: colors.OVERLAY1\n    }\n  },\n\n  codemirror: {\n    bg: colors.BASE,\n    border: colors.BASE,\n    placeholder: {\n      color: colors.OVERLAY0,\n      opacity: 0.5\n    },\n    gutter: {\n      bg: colors.BASE\n    },\n    variable: {\n      valid: colors.GREEN,\n      invalid: colors.RED,\n      prompt: colors.BLUE\n    },\n    tokens: {\n      definition: colors.CODEMIRROR_TOKENS.DEFINITION,\n      property: colors.CODEMIRROR_TOKENS.PROPERTY,\n      string: colors.CODEMIRROR_TOKENS.STRING,\n      number: colors.CODEMIRROR_TOKENS.NUMBER,\n      atom: colors.CODEMIRROR_TOKENS.ATOM,\n      variable: colors.CODEMIRROR_TOKENS.VARIABLE,\n      keyword: colors.CODEMIRROR_TOKENS.KEYWORD,\n      comment: colors.CODEMIRROR_TOKENS.COMMENT,\n      operator: colors.CODEMIRROR_TOKENS.OPERATOR,\n      tag: colors.CODEMIRROR_TOKENS.TAG,\n      tagBracket: colors.CODEMIRROR_TOKENS.TAG_BRACKET\n    },\n    searchLineHighlightCurrent: 'rgba(110, 115, 141, 0.18)',\n    searchMatch: colors.YELLOW,\n    searchMatchActive: colors.PEACH\n  },\n\n  table: {\n    border: colors.SURFACE0,\n    thead: {\n      color: colors.TEXT\n    },\n    striped: colors.SURFACE0,\n    input: {\n      color: colors.TEXT\n    }\n  },\n\n  plainGrid: {\n    hoverBg: colors.SURFACE0\n  },\n\n  scrollbar: {\n    color: colors.SURFACE0\n  },\n\n  dragAndDrop: {\n    border: colors.LAVENDER,\n    borderStyle: '2px solid',\n    hoverBg: 'rgba(183, 189, 248, 0.08)',\n    transition: 'all 0.1s ease'\n  },\n  infoTip: {\n    bg: colors.SURFACE0,\n    border: colors.SURFACE1,\n    boxShadow: '0 4px 12px rgba(0, 0, 0, 0.5)'\n  },\n\n  statusBar: {\n    border: colors.SURFACE0,\n    color: colors.SUBTEXT0\n  },\n\n  console: {\n    bg: colors.BASE,\n    headerBg: colors.MANTLE,\n    contentBg: colors.BASE,\n    border: colors.SURFACE0,\n    titleColor: colors.TEXT,\n    countColor: colors.SUBTEXT0,\n    buttonColor: colors.TEXT,\n    buttonHoverBg: 'rgba(202, 211, 245, 0.1)',\n    buttonHoverColor: colors.TEXT,\n    messageColor: colors.TEXT,\n    timestampColor: colors.SUBTEXT0,\n    emptyColor: colors.SUBTEXT0,\n    logHoverBg: 'rgba(202, 211, 245, 0.05)',\n    resizeHandleHover: colors.BLUE,\n    resizeHandleActive: colors.BLUE,\n    dropdownBg: colors.MANTLE,\n    dropdownHeaderBg: colors.SURFACE0,\n    optionHoverBg: 'rgba(202, 211, 245, 0.05)',\n    optionLabelColor: colors.TEXT,\n    optionCountColor: colors.SUBTEXT0,\n    checkboxColor: colors.MAUVE,\n    scrollbarTrack: colors.MANTLE,\n    scrollbarThumb: colors.SURFACE2,\n    scrollbarThumbHover: colors.OVERLAY0\n  },\n\n  grpc: {\n    tabNav: {\n      container: {\n        bg: colors.CRUST\n      },\n      button: {\n        active: {\n          bg: colors.SURFACE0,\n          color: colors.TEXT\n        },\n        inactive: {\n          bg: 'transparent',\n          color: colors.SUBTEXT0\n        }\n      }\n    },\n    importPaths: {\n      header: {\n        text: colors.SUBTEXT0,\n        button: {\n          color: colors.SUBTEXT0,\n          hoverColor: colors.TEXT\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: colors.RED,\n        link: {\n          color: colors.RED,\n          hoverColor: colors.MAROON\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: 'rgba(202, 211, 245, 0.05)',\n        text: colors.TEXT,\n        icon: colors.SUBTEXT0,\n        checkbox: {\n          color: colors.TEXT\n        },\n        invalid: {\n          opacity: 0.6,\n          text: colors.RED\n        }\n      },\n      empty: {\n        text: colors.SUBTEXT0\n      },\n      button: {\n        bg: colors.SURFACE0,\n        color: colors.TEXT,\n        border: colors.SURFACE0,\n        hoverBorder: colors.OVERLAY0\n      }\n    },\n    protoFiles: {\n      header: {\n        text: colors.SUBTEXT0,\n        button: {\n          color: colors.SUBTEXT0,\n          hoverColor: colors.TEXT\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: colors.RED,\n        link: {\n          color: colors.RED,\n          hoverColor: colors.MAROON\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: 'rgba(202, 211, 245, 0.05)',\n        selected: {\n          bg: 'rgba(198, 160, 246, 0.2)',\n          border: colors.MAUVE\n        },\n        text: colors.TEXT,\n        secondaryText: colors.SUBTEXT0,\n        icon: colors.SUBTEXT0,\n        invalid: {\n          opacity: 0.6,\n          text: colors.RED\n        }\n      },\n      empty: {\n        text: colors.SUBTEXT0\n      },\n      button: {\n        bg: colors.SURFACE0,\n        color: colors.TEXT,\n        border: colors.SURFACE0,\n        hoverBorder: colors.OVERLAY0\n      }\n    }\n  },\n  deprecationWarning: {\n    bg: 'rgba(237, 135, 150, 0.1)',\n    border: 'rgba(237, 135, 150, 0.1)',\n    icon: colors.RED,\n    text: colors.SUBTEXT1\n  },\n\n  examples: {\n    buttonBg: 'rgba(198, 160, 246, 0.1)',\n    buttonColor: colors.MAUVE,\n    buttonText: colors.TEXT,\n    buttonIconColor: colors.TEXT,\n    border: colors.SURFACE1,\n    urlBar: {\n      border: colors.SURFACE0,\n      bg: colors.MANTLE\n    },\n    table: {\n      thead: {\n        bg: colors.MANTLE,\n        color: colors.SUBTEXT0\n      }\n    },\n    checkbox: {\n      color: colors.CRUST\n    }\n  },\n\n  app: {\n    collection: {\n      toolbar: {\n        environmentSelector: {\n          bg: colors.BASE,\n          border: colors.SURFACE0,\n          icon: colors.MAUVE,\n          text: colors.TEXT,\n          caret: colors.SUBTEXT0,\n          separator: colors.SURFACE0,\n          hoverBg: colors.BASE,\n          hoverBorder: colors.SURFACE1,\n\n          noEnvironment: {\n            text: colors.SUBTEXT0,\n            bg: colors.BASE,\n            border: colors.SURFACE0,\n            hoverBg: colors.BASE,\n            hoverBorder: colors.SURFACE1\n          }\n        },\n        sandboxMode: {\n          safeMode: {\n            bg: 'rgba(166, 218, 149, 0.12)',\n            color: colors.GREEN\n          },\n          developerMode: {\n            bg: 'rgba(238, 212, 159, 0.11)',\n            color: colors.YELLOW\n          }\n        }\n      }\n    }\n  }\n};\n\nexport default catppuccinMacchiatoTheme;\n"
  },
  {
    "path": "packages/bruno-app/src/themes/dark/catppuccin-mocha.js",
    "content": "// Catppuccin Mocha - Dark Theme (Original)\n// Based on https://catppuccin.com/palette/\n\nimport { rgba, darken, lighten } from 'polished';\n\nconst colors = {\n  // Catppuccin Mocha Palette\n  ROSEWATER: '#f5e0dc',\n  FLAMINGO: '#f2cdcd',\n  PINK: '#f5c2e7',\n  MAUVE: '#cba6f7',\n  RED: '#f38ba8',\n  MAROON: '#eba0ac',\n  PEACH: '#fab387',\n  YELLOW: '#f9e2af',\n  GREEN: '#a6e3a1',\n  TEAL: '#94e2d5',\n  SKY: '#89dceb',\n  SAPPHIRE: '#74c7ec',\n  BLUE: '#89b4fa',\n  LAVENDER: '#b4befe',\n\n  TEXT: '#cdd6f4',\n  SUBTEXT1: '#bac2de',\n  SUBTEXT0: '#a6adc8',\n  OVERLAY2: '#9399b2',\n  OVERLAY1: '#7f849c',\n  OVERLAY0: '#6c7086',\n  SURFACE2: '#585b70',\n  SURFACE1: '#45475a',\n  SURFACE0: '#313244',\n  BASE: '#1e1e2e',\n  MANTLE: '#181825',\n  CRUST: '#11111b',\n\n  WHITE: '#fff',\n  BLACK: '#000',\n\n  CODEMIRROR_TOKENS: {\n    DEFINITION: '#a6e3a1',\n    PROPERTY: '#89b4fa',\n    STRING: '#f9e2af',\n    NUMBER: '#fab387',\n    ATOM: '#f5c2e7',\n    VARIABLE: '#74c7ec',\n    KEYWORD: '#f38ba8',\n    COMMENT: '#6c7086',\n    OPERATOR: '#94e2d5',\n    TAG: '#89b4fa',\n    TAG_BRACKET: '#6c7086'\n  }\n};\n\nexport const palette = {};\n\npalette.intent = {\n  INFO: colors.BLUE,\n  SUCCESS: colors.GREEN,\n  WARNING: colors.PEACH,\n  DANGER: colors.RED\n};\n\nconst catppuccinMochaTheme = {\n  mode: 'dark',\n  brand: colors.MAUVE,\n  text: colors.TEXT,\n  textLink: colors.BLUE,\n  draftColor: '#cc7b1b',\n  bg: colors.BASE,\n\n  primary: {\n    solid: colors.MAUVE,\n    text: colors.MAUVE,\n    strong: colors.MAUVE,\n    subtle: colors.MAUVE\n  },\n\n  accents: {\n    primary: colors.MAUVE\n  },\n\n  background: {\n    base: colors.BASE,\n    mantle: colors.MANTLE,\n    crust: colors.CRUST,\n    surface0: colors.SURFACE0,\n    surface1: colors.SURFACE1,\n    surface2: colors.SURFACE2\n  },\n\n  status: {\n    info: {\n      background: rgba(palette.intent.INFO, 0.15),\n      text: palette.intent.INFO,\n      border: palette.intent.INFO\n    },\n    success: {\n      background: rgba(palette.intent.SUCCESS, 0.15),\n      text: palette.intent.SUCCESS,\n      border: palette.intent.SUCCESS\n    },\n    warning: {\n      background: rgba(palette.intent.WARNING, 0.15),\n      text: palette.intent.WARNING,\n      border: palette.intent.WARNING\n    },\n    danger: {\n      background: rgba(palette.intent.DANGER, 0.15),\n      text: palette.intent.DANGER,\n      border: palette.intent.DANGER\n    }\n  },\n\n  overlay: {\n    overlay2: colors.OVERLAY2,\n    overlay1: colors.OVERLAY1,\n    overlay0: colors.OVERLAY0\n  },\n\n  font: {\n    size: {\n      xs: '0.6875rem', // 11px\n      sm: '0.75rem', // 12px\n      base: '0.8125rem', // 13px\n      md: '0.875rem', // 14px\n      lg: '1rem', // 16px\n      xl: '1.125rem' // 18px\n    }\n  },\n\n  shadow: {\n    sm: '0 1px 3px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(0, 0, 0, 0.3)',\n    md: '0 2px 8px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(0, 0, 0, 0.4)',\n    lg: '0 2px 12px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(0, 0, 0, 0.4)'\n  },\n\n  border: {\n    radius: {\n      sm: '4px',\n      base: '6px',\n      md: '8px',\n      lg: '10px',\n      xl: '12px'\n    },\n    border2: colors.SURFACE2,\n    border1: colors.SURFACE1,\n    border0: colors.SURFACE0\n  },\n\n  colors: {\n    text: {\n      white: colors.WHITE,\n      green: colors.GREEN,\n      danger: colors.RED,\n      warning: colors.PEACH,\n      muted: colors.SUBTEXT0,\n      purple: colors.MAUVE,\n      yellow: colors.YELLOW,\n      subtext2: colors.TEXT,\n      subtext1: colors.SUBTEXT1,\n      subtext0: colors.SUBTEXT0\n    },\n    bg: {\n      danger: colors.MAROON\n    },\n    accent: colors.MAUVE\n  },\n\n  input: {\n    bg: 'transparent',\n    border: colors.SURFACE1,\n    focusBorder: colors.LAVENDER,\n    placeholder: {\n      color: colors.OVERLAY0,\n      opacity: 0.75\n    }\n  },\n\n  sidebar: {\n    color: colors.TEXT,\n    muted: colors.SUBTEXT0,\n    bg: colors.BASE,\n    dragbar: {\n      border: colors.SURFACE0,\n      activeBorder: colors.OVERLAY0\n    },\n\n    collection: {\n      item: {\n        bg: colors.SURFACE0,\n        hoverBg: colors.SURFACE0,\n        focusBorder: colors.SURFACE1,\n        indentBorder: colors.SURFACE0,\n        active: {\n          indentBorder: colors.SURFACE0\n        },\n        example: {\n          iconColor: colors.OVERLAY1\n        }\n      }\n    },\n\n    dropdownIcon: {\n      color: colors.TEXT\n    }\n  },\n\n  dropdown: {\n    color: colors.TEXT,\n    iconColor: colors.SUBTEXT1,\n    bg: lighten(0.03, colors.BASE),\n    hoverBg: 'rgba(108, 112, 134, 0.16)',\n    shadow: 'none',\n    border: rgba(colors.SURFACE1, 0.5),\n    separator: rgba(colors.SURFACE1, 0.5),\n    selectedColor: colors.MAUVE,\n    mutedText: colors.SUBTEXT0\n  },\n\n  workspace: {\n    accent: colors.MAUVE,\n    border: colors.SURFACE1,\n    button: {\n      bg: colors.SURFACE0\n    }\n  },\n\n  request: {\n    methods: {\n      get: colors.GREEN,\n      post: colors.BLUE,\n      put: colors.YELLOW,\n      delete: colors.RED,\n      patch: colors.PEACH,\n      options: colors.TEAL,\n      head: colors.SAPPHIRE\n    },\n\n    grpc: colors.SKY,\n    ws: colors.MAUVE,\n    gql: colors.PINK\n  },\n\n  requestTabPanel: {\n    url: {\n      bg: colors.BASE,\n      icon: colors.TEXT,\n      iconDanger: colors.RED,\n      border: `solid 1px ${colors.SURFACE0}`\n    },\n    dragbar: {\n      border: colors.SURFACE0,\n      activeBorder: colors.OVERLAY0\n    },\n    responseStatus: colors.TEXT,\n    responseOk: colors.GREEN,\n    responseError: colors.RED,\n    responsePending: colors.BLUE,\n    responseOverlayBg: 'rgba(30, 30, 46, 0.6)',\n\n    card: {\n      bg: colors.MANTLE,\n      border: 'transparent',\n      hr: colors.SURFACE0\n    },\n\n    graphqlDocsExplorer: {\n      bg: colors.BASE,\n      color: colors.TEXT\n    }\n  },\n\n  notifications: {\n    bg: colors.SURFACE0,\n    list: {\n      bg: colors.SURFACE0,\n      borderRight: colors.SURFACE2,\n      borderBottom: colors.SURFACE1,\n      hoverBg: colors.SURFACE1,\n      active: {\n        border: colors.BLUE,\n        bg: colors.SURFACE2,\n        hoverBg: colors.SURFACE2\n      }\n    }\n  },\n\n  modal: {\n    title: {\n      color: colors.TEXT,\n      bg: colors.MANTLE\n    },\n    body: {\n      color: colors.TEXT,\n      bg: colors.BASE\n    },\n    input: {\n      bg: 'transparent',\n      border: colors.SURFACE1,\n      focusBorder: colors.LAVENDER\n    },\n    backdrop: {\n      opacity: 0.2\n    }\n  },\n\n  button: {\n    secondary: {\n      color: colors.TEXT,\n      bg: colors.SURFACE0,\n      border: colors.SURFACE0,\n      hoverBorder: colors.OVERLAY0\n    },\n    close: {\n      color: colors.TEXT,\n      bg: 'transparent',\n      border: 'transparent',\n      hoverBorder: ''\n    },\n    disabled: {\n      color: colors.OVERLAY0,\n      bg: colors.SURFACE1,\n      border: colors.SURFACE1\n    },\n    danger: {\n      color: colors.CRUST,\n      bg: colors.RED,\n      border: colors.RED\n    }\n  },\n  button2: {\n    color: {\n      primary: {\n        bg: colors.MAUVE,\n        text: colors.CRUST,\n        border: colors.MAUVE\n      },\n      light: {\n        bg: rgba(colors.MAUVE, 0.08),\n        text: colors.MAUVE,\n        border: rgba(colors.MAUVE, 0.06)\n      },\n      secondary: {\n        bg: colors.SURFACE0,\n        text: colors.TEXT,\n        border: colors.SURFACE1\n      },\n      success: {\n        bg: colors.GREEN,\n        text: colors.CRUST,\n        border: colors.GREEN\n      },\n      warning: {\n        bg: colors.PEACH,\n        text: colors.CRUST,\n        border: colors.PEACH\n      },\n      danger: {\n        bg: colors.RED,\n        text: colors.CRUST,\n        border: colors.RED\n      }\n    }\n  },\n\n  tabs: {\n    marginRight: '1.2rem',\n    active: {\n      fontWeight: 400,\n      color: colors.TEXT,\n      border: colors.MAUVE\n    },\n    secondary: {\n      active: {\n        bg: colors.SURFACE0,\n        color: colors.TEXT\n      },\n      inactive: {\n        bg: colors.SURFACE0,\n        color: colors.SUBTEXT0\n      }\n    }\n  },\n\n  requestTabs: {\n    color: colors.TEXT,\n    bg: colors.SURFACE0,\n    bottomBorder: colors.SURFACE1,\n    icon: {\n      color: colors.OVERLAY0,\n      hoverColor: colors.TEXT,\n      hoverBg: colors.BASE\n    },\n    example: {\n      iconColor: colors.OVERLAY1\n    }\n  },\n\n  codemirror: {\n    bg: colors.BASE,\n    border: colors.BASE,\n    placeholder: {\n      color: colors.OVERLAY0,\n      opacity: 0.5\n    },\n    gutter: {\n      bg: colors.BASE\n    },\n    variable: {\n      valid: colors.GREEN,\n      invalid: colors.RED,\n      prompt: colors.BLUE\n    },\n    tokens: {\n      definition: colors.CODEMIRROR_TOKENS.DEFINITION,\n      property: colors.CODEMIRROR_TOKENS.PROPERTY,\n      string: colors.CODEMIRROR_TOKENS.STRING,\n      number: colors.CODEMIRROR_TOKENS.NUMBER,\n      atom: colors.CODEMIRROR_TOKENS.ATOM,\n      variable: colors.CODEMIRROR_TOKENS.VARIABLE,\n      keyword: colors.CODEMIRROR_TOKENS.KEYWORD,\n      comment: colors.CODEMIRROR_TOKENS.COMMENT,\n      operator: colors.CODEMIRROR_TOKENS.OPERATOR,\n      tag: colors.CODEMIRROR_TOKENS.TAG,\n      tagBracket: colors.CODEMIRROR_TOKENS.TAG_BRACKET\n    },\n    searchLineHighlightCurrent: 'rgba(108, 112, 134, 0.18)',\n    searchMatch: colors.YELLOW,\n    searchMatchActive: colors.PEACH\n  },\n\n  table: {\n    border: colors.SURFACE0,\n    thead: {\n      color: colors.TEXT\n    },\n    striped: rgba(colors.SURFACE0, 0.2),\n    input: {\n      color: colors.TEXT\n    }\n  },\n\n  plainGrid: {\n    hoverBg: colors.SURFACE0\n  },\n\n  scrollbar: {\n    color: colors.SURFACE0\n  },\n\n  dragAndDrop: {\n    border: colors.LAVENDER,\n    borderStyle: '2px solid',\n    hoverBg: 'rgba(180, 190, 254, 0.08)',\n    transition: 'all 0.1s ease'\n  },\n  infoTip: {\n    bg: colors.SURFACE0,\n    border: colors.SURFACE1,\n    boxShadow: '0 4px 12px rgba(0, 0, 0, 0.5)'\n  },\n\n  statusBar: {\n    border: colors.SURFACE0,\n    color: colors.SUBTEXT0\n  },\n\n  console: {\n    bg: colors.BASE,\n    headerBg: colors.MANTLE,\n    contentBg: colors.BASE,\n    border: colors.SURFACE0,\n    titleColor: colors.TEXT,\n    countColor: colors.SUBTEXT0,\n    buttonColor: colors.TEXT,\n    buttonHoverBg: 'rgba(205, 214, 244, 0.1)',\n    buttonHoverColor: colors.TEXT,\n    messageColor: colors.TEXT,\n    timestampColor: colors.SUBTEXT0,\n    emptyColor: colors.SUBTEXT0,\n    logHoverBg: 'rgba(205, 214, 244, 0.05)',\n    resizeHandleHover: colors.BLUE,\n    resizeHandleActive: colors.BLUE,\n    dropdownBg: colors.MANTLE,\n    dropdownHeaderBg: colors.SURFACE0,\n    optionHoverBg: 'rgba(205, 214, 244, 0.05)',\n    optionLabelColor: colors.TEXT,\n    optionCountColor: colors.SUBTEXT0,\n    checkboxColor: colors.MAUVE,\n    scrollbarTrack: colors.MANTLE,\n    scrollbarThumb: colors.SURFACE2,\n    scrollbarThumbHover: colors.OVERLAY0\n  },\n\n  grpc: {\n    tabNav: {\n      container: {\n        bg: colors.CRUST\n      },\n      button: {\n        active: {\n          bg: colors.SURFACE0,\n          color: colors.TEXT\n        },\n        inactive: {\n          bg: 'transparent',\n          color: colors.SUBTEXT0\n        }\n      }\n    },\n    importPaths: {\n      header: {\n        text: colors.SUBTEXT0,\n        button: {\n          color: colors.SUBTEXT0,\n          hoverColor: colors.TEXT\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: colors.RED,\n        link: {\n          color: colors.RED,\n          hoverColor: colors.MAROON\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: 'rgba(205, 214, 244, 0.05)',\n        text: colors.TEXT,\n        icon: colors.SUBTEXT0,\n        checkbox: {\n          color: colors.TEXT\n        },\n        invalid: {\n          opacity: 0.6,\n          text: colors.RED\n        }\n      },\n      empty: {\n        text: colors.SUBTEXT0\n      },\n      button: {\n        bg: colors.SURFACE0,\n        color: colors.TEXT,\n        border: colors.SURFACE0,\n        hoverBorder: colors.OVERLAY0\n      }\n    },\n    protoFiles: {\n      header: {\n        text: colors.SUBTEXT0,\n        button: {\n          color: colors.SUBTEXT0,\n          hoverColor: colors.TEXT\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: colors.RED,\n        link: {\n          color: colors.RED,\n          hoverColor: colors.MAROON\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: 'rgba(205, 214, 244, 0.05)',\n        selected: {\n          bg: 'rgba(203, 166, 247, 0.2)',\n          border: colors.MAUVE\n        },\n        text: colors.TEXT,\n        secondaryText: colors.SUBTEXT0,\n        icon: colors.SUBTEXT0,\n        invalid: {\n          opacity: 0.6,\n          text: colors.RED\n        }\n      },\n      empty: {\n        text: colors.SUBTEXT0\n      },\n      button: {\n        bg: colors.SURFACE0,\n        color: colors.TEXT,\n        border: colors.SURFACE0,\n        hoverBorder: colors.OVERLAY0\n      }\n    }\n  },\n  deprecationWarning: {\n    bg: 'rgba(243, 139, 168, 0.1)',\n    border: 'rgba(243, 139, 168, 0.1)',\n    icon: colors.RED,\n    text: colors.SUBTEXT1\n  },\n\n  examples: {\n    buttonBg: 'rgba(203, 166, 247, 0.1)',\n    buttonColor: colors.MAUVE,\n    buttonText: colors.TEXT,\n    buttonIconColor: colors.TEXT,\n    border: colors.SURFACE1,\n    urlBar: {\n      border: colors.SURFACE0,\n      bg: colors.MANTLE\n    },\n    table: {\n      thead: {\n        bg: colors.MANTLE,\n        color: colors.SUBTEXT0\n      }\n    },\n    checkbox: {\n      color: colors.CRUST\n    }\n  },\n\n  app: {\n    collection: {\n      toolbar: {\n        environmentSelector: {\n          bg: colors.BASE,\n          border: colors.SURFACE0,\n          icon: colors.MAUVE,\n          text: colors.TEXT,\n          caret: colors.SUBTEXT0,\n          separator: colors.SURFACE0,\n          hoverBg: colors.BASE,\n          hoverBorder: colors.SURFACE1,\n\n          noEnvironment: {\n            text: colors.SUBTEXT0,\n            bg: colors.BASE,\n            border: colors.SURFACE0,\n            hoverBg: colors.BASE,\n            hoverBorder: colors.SURFACE1\n          }\n        },\n        sandboxMode: {\n          safeMode: {\n            bg: 'rgba(166, 227, 161, 0.12)',\n            color: colors.GREEN\n          },\n          developerMode: {\n            bg: 'rgba(249, 226, 175, 0.11)',\n            color: colors.YELLOW\n          }\n        }\n      }\n    }\n  }\n};\n\nexport default catppuccinMochaTheme;\n"
  },
  {
    "path": "packages/bruno-app/src/themes/dark/dark-monochrome.js",
    "content": "import { rgba } from 'polished';\n\nconst colors = {\n  BRAND: '#a3a3a3',\n  TEXT: '#d4d4d4',\n  TEXT_MUTED: '#858585',\n  TEXT_LINK: '#b0b0b0',\n  BG: '#1e1e1e',\n\n  GREEN: '#a3a3a3',\n  YELLOW: '#a3a3a3',\n  WHITE: '#fff',\n  BLACK: '#000',\n\n  GRAY_1: '#252526',\n  GRAY_2: '#3D3D3D',\n  GRAY_3: '#444444',\n  GRAY_4: '#666666',\n  GRAY_5: '#b0b0b0',\n  GRAY_6: '#cbcbcb',\n  GRAY_7: '#e5e5e5',\n  GRAY_8: '#eaeaea',\n  GRAY_9: '#f3f3f3',\n  GRAY_10: '#f8f8f8',\n\n  CODEMIRROR_TOKENS: {\n    DEFINITION: '#b0b0b0',\n    PROPERTY: '#a3a3a3',\n    STRING: '#c0c0c0',\n    NUMBER: '#b0b0b0',\n    ATOM: '#a3a3a3',\n    VARIABLE: '#b0b0b0',\n    KEYWORD: '#d4d4d4',\n    COMMENT: '#6a6a6a',\n    OPERATOR: '#d4d4d4',\n    TAG: '#d4d4d4',\n    TAG_BRACKET: '#6a6a6a'\n  }\n};\n\nexport const palette = {};\n\npalette.intent = {\n  INFO: '#8a8a8a',\n  SUCCESS: '#a3a3a3',\n  WARNING: '#b0b0b0',\n  DANGER: '#c0c0c0'\n};\n\nconst darkMonochromeTheme = {\n  mode: 'dark',\n  brand: colors.BRAND,\n  text: colors.TEXT,\n  textLink: colors.TEXT_LINK,\n  draftColor: '#8a8a8a',\n  bg: colors.BG,\n\n  primary: {\n    solid: colors.BRAND,\n    text: colors.BRAND,\n    strong: colors.BRAND,\n    subtle: colors.BRAND\n  },\n\n  accents: {\n    primary: colors.BRAND\n  },\n\n  background: {\n    base: colors.BG,\n    mantle: colors.GRAY_1,\n    crust: '#333333',\n    surface0: colors.GRAY_2,\n    surface1: colors.GRAY_3,\n    surface2: colors.GRAY_4\n  },\n\n  status: {\n    info: {\n      background: rgba(palette.intent.INFO, 0.15),\n      text: palette.intent.INFO,\n      border: palette.intent.INFO\n    },\n    success: {\n      background: rgba(palette.intent.SUCCESS, 0.15),\n      text: palette.intent.SUCCESS,\n      border: palette.intent.SUCCESS\n    },\n    warning: {\n      background: rgba(palette.intent.WARNING, 0.15),\n      text: palette.intent.WARNING,\n      border: palette.intent.WARNING\n    },\n    danger: {\n      background: rgba(palette.intent.DANGER, 0.15),\n      text: palette.intent.DANGER,\n      border: palette.intent.DANGER\n    }\n  },\n\n  overlay: {\n    overlay2: '#666666',\n    overlay1: '#555555',\n    overlay0: '#444444'\n  },\n\n  font: {\n    size: {\n      xs: '0.6875rem',\n      sm: '0.75rem',\n      base: '0.8125rem',\n      md: '0.875rem',\n      lg: '1rem',\n      xl: '1.125rem'\n    }\n  },\n\n  shadow: {\n    sm: '0 1px 3px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(0, 0, 0, 0.3)',\n    md: '0 2px 8px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(0, 0, 0, 0.4)',\n    lg: '0 2px 12px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(0, 0, 0, 0.4)'\n  },\n\n  border: {\n    radius: {\n      sm: '4px',\n      base: '6px',\n      md: '8px',\n      lg: '10px',\n      xl: '12px'\n    },\n    border2: colors.GRAY_4,\n    border1: colors.GRAY_3,\n    border0: colors.GRAY_2\n  },\n\n  colors: {\n    text: {\n      white: colors.WHITE,\n      green: '#a3a3a3',\n      danger: '#b0b0b0',\n      warning: '#a3a3a3',\n      muted: colors.TEXT_MUTED,\n      purple: '#a3a3a3',\n      yellow: colors.YELLOW,\n      subtext2: colors.GRAY_6,\n      subtext1: colors.GRAY_5,\n      subtext0: colors.GRAY_4\n    },\n    bg: {\n      danger: '#666666'\n    },\n    accent: colors.BRAND\n  },\n\n  input: {\n    bg: 'transparent',\n    border: colors.GRAY_3,\n    focusBorder: colors.BRAND,\n    placeholder: {\n      color: colors.TEXT_MUTED,\n      opacity: 0.6\n    }\n  },\n\n  sidebar: {\n    color: '#ccc',\n    muted: '#9d9d9d',\n    bg: colors.GRAY_1,\n    dragbar: {\n      border: 'transparent',\n      activeBorder: colors.GRAY_4\n    },\n\n    collection: {\n      item: {\n        bg: '#37373D',\n        hoverBg: '#2A2D2F',\n        focusBorder: '#4e4e4e',\n        indentBorder: colors.GRAY_2,\n        active: {\n          indentBorder: colors.GRAY_2\n        },\n        example: {\n          iconColor: colors.GRAY_5\n        }\n      }\n    },\n\n    dropdownIcon: {\n      color: '#ccc'\n    }\n  },\n\n  dropdown: {\n    color: 'rgb(204, 204, 204)',\n    iconColor: 'rgb(204, 204, 204)',\n    bg: 'rgb(48, 48, 49)',\n    hoverBg: '#6A6A6A29',\n    shadow: 'none',\n    border: '#444',\n    separator: '#444',\n    selectedColor: '#a3a3a3',\n    mutedText: '#9B9B9B'\n  },\n\n  workspace: {\n    accent: '#a3a3a3',\n    border: '#444',\n    button: {\n      bg: colors.GRAY_2\n    }\n  },\n\n  request: {\n    methods: {\n      get: '#a3a3a3',\n      post: '#b0b0b0',\n      put: '#9a9a9a',\n      delete: '#c0c0c0',\n      patch: '#9a9a9a',\n      options: '#8a8a8a',\n      head: '#9da5b4'\n    },\n\n    grpc: '#a3a3a3',\n    ws: '#b0b0b0',\n    gql: '#9a9a9a'\n  },\n\n  requestTabPanel: {\n    url: {\n      bg: colors.BG,\n      icon: 'rgb(204, 204, 204)',\n      iconDanger: '#b0b0b0',\n      border: `solid 1px ${colors.GRAY_3}`\n    },\n    dragbar: {\n      border: '#444',\n      activeBorder: '#8a8a8a'\n    },\n    responseStatus: '#ccc',\n    responseOk: '#a3a3a3',\n    responseError: '#b0b0b0',\n    responsePending: '#a3a3a3',\n    responseOverlayBg: 'rgba(30, 30, 30, 0.6)',\n\n    card: {\n      bg: '#252526',\n      border: 'transparent',\n      hr: '#424242'\n    },\n\n    graphqlDocsExplorer: {\n      bg: '#1e1e1e',\n      color: '#d4d4d4'\n    }\n  },\n\n  notifications: {\n    bg: colors.GRAY_3,\n    list: {\n      bg: '3D3D3D',\n      borderRight: '#4f4f4f',\n      borderBottom: '#545454',\n      hoverBg: '#434343',\n      active: {\n        border: '#a3a3a3',\n        bg: '#4f4f4f',\n        hoverBg: '#4f4f4f'\n      }\n    }\n  },\n\n  modal: {\n    title: {\n      color: '#ccc',\n      bg: 'rgb(38, 38, 39)'\n    },\n    body: {\n      color: '#ccc',\n      bg: 'rgb(48, 48, 49)'\n    },\n    input: {\n      bg: 'transparent',\n      border: colors.GRAY_3,\n      focusBorder: colors.BRAND\n    },\n    backdrop: {\n      opacity: 0.2\n    }\n  },\n\n  button: {\n    secondary: {\n      color: 'rgb(204, 204, 204)',\n      bg: '#525252',\n      border: '#525252',\n      hoverBorder: '#696969'\n    },\n    close: {\n      color: '#ccc',\n      bg: 'transparent',\n      border: 'transparent',\n      hoverBorder: ''\n    },\n    disabled: {\n      color: '#a5a5a5',\n      bg: '#626262',\n      border: '#626262'\n    },\n    danger: {\n      color: '#fff',\n      bg: '#666666',\n      border: '#666666'\n    }\n  },\n  button2: {\n    color: {\n      primary: {\n        bg: colors.BRAND,\n        text: colors.BLACK,\n        border: colors.BRAND\n      },\n      light: {\n        bg: rgba(colors.TEXT, 0.08),\n        text: colors.TEXT,\n        border: rgba(colors.TEXT, 0.06)\n      },\n      secondary: {\n        bg: colors.BG,\n        text: colors.TEXT,\n        border: colors.GRAY_5\n      },\n      success: {\n        bg: '#666666',\n        text: '#fff',\n        border: '#666666'\n      },\n      warning: {\n        bg: '#858585',\n        text: '#1e1e1e',\n        border: '#858585'\n      },\n      danger: {\n        bg: '#737373',\n        text: '#fff',\n        border: '#737373'\n      }\n    }\n  },\n\n  tabs: {\n    marginRight: '1.2rem',\n    active: {\n      fontWeight: 400,\n      color: '#CCCCCC',\n      border: '#a3a3a3'\n    },\n    secondary: {\n      active: {\n        bg: '#3F3F3F',\n        color: '#CCCCCC'\n      },\n      inactive: {\n        bg: '#3F3F3F',\n        color: '#999999'\n      }\n    }\n  },\n\n  requestTabs: {\n    color: '#ccc',\n    bg: '#2A2D2F',\n    bottomBorder: '#444',\n    icon: {\n      color: '#9f9f9f',\n      hoverColor: 'rgb(204, 204, 204)',\n      hoverBg: '#1e1e1e'\n    },\n    example: {\n      iconColor: colors.GRAY_5\n    }\n  },\n\n  codemirror: {\n    bg: colors.BG,\n    border: colors.BG,\n    placeholder: {\n      color: '#a2a2a2',\n      opacity: 0.5\n    },\n    gutter: {\n      bg: colors.BG\n    },\n    variable: {\n      valid: '#a3a3a3',\n      invalid: '#b0b0b0',\n      prompt: '#a3a3a3'\n    },\n    tokens: {\n      definition: colors.CODEMIRROR_TOKENS.DEFINITION,\n      property: colors.CODEMIRROR_TOKENS.PROPERTY,\n      string: colors.CODEMIRROR_TOKENS.STRING,\n      number: colors.CODEMIRROR_TOKENS.NUMBER,\n      atom: colors.CODEMIRROR_TOKENS.ATOM,\n      variable: colors.CODEMIRROR_TOKENS.VARIABLE,\n      keyword: colors.CODEMIRROR_TOKENS.KEYWORD,\n      comment: colors.CODEMIRROR_TOKENS.COMMENT,\n      operator: colors.CODEMIRROR_TOKENS.OPERATOR,\n      tag: colors.CODEMIRROR_TOKENS.TAG,\n      tagBracket: colors.CODEMIRROR_TOKENS.TAG_BRACKET\n    },\n    searchLineHighlightCurrent: 'rgba(120,120,120,0.18)',\n    searchMatch: '#a3a3a3',\n    searchMatchActive: '#d4d4d4'\n  },\n\n  table: {\n    border: '#333',\n    thead: {\n      color: 'rgb(204, 204, 204)'\n    },\n    striped: '#2A2D2F',\n    input: {\n      color: '#ccc'\n    }\n  },\n\n  plainGrid: {\n    hoverBg: colors.GRAY_3\n  },\n\n  scrollbar: {\n    color: 'rgb(52 51 49)'\n  },\n\n  dragAndDrop: {\n    border: '#666666',\n    borderStyle: '2px solid',\n    hoverBg: 'rgba(102, 102, 102, 0.08)',\n    transition: 'all 0.1s ease'\n  },\n  infoTip: {\n    bg: '#1f1f1f',\n    border: '#333333',\n    boxShadow: '0 4px 12px rgba(0, 0, 0, 0.5)'\n  },\n\n  statusBar: {\n    border: '#323233',\n    color: 'rgb(169, 169, 169)'\n  },\n\n  console: {\n    bg: '#1e1e1e',\n    headerBg: '#2d2d30',\n    contentBg: '#1e1e1e',\n    border: '#3c3c3c',\n    titleColor: '#cccccc',\n    countColor: '#858585',\n    buttonColor: '#cccccc',\n    buttonHoverBg: 'rgba(255, 255, 255, 0.1)',\n    buttonHoverColor: '#ffffff',\n    messageColor: '#cccccc',\n    timestampColor: '#858585',\n    emptyColor: '#858585',\n    logHoverBg: 'rgba(255, 255, 255, 0.05)',\n    resizeHandleHover: '#a3a3a3',\n    resizeHandleActive: '#a3a3a3',\n    dropdownBg: '#2d2d30',\n    dropdownHeaderBg: '#3c3c3c',\n    optionHoverBg: 'rgba(255, 255, 255, 0.05)',\n    optionLabelColor: '#cccccc',\n    optionCountColor: '#858585',\n    checkboxColor: colors.BRAND,\n    scrollbarTrack: '#2d2d30',\n    scrollbarThumb: '#5a5a5a',\n    scrollbarThumbHover: '#6a6a6a'\n  },\n\n  grpc: {\n    tabNav: {\n      container: {\n        bg: '#262626'\n      },\n      button: {\n        active: {\n          bg: '#404040',\n          color: '#ffffff'\n        },\n        inactive: {\n          bg: 'transparent',\n          color: '#a3a3a3'\n        }\n      }\n    },\n    importPaths: {\n      header: {\n        text: '#9d9d9d',\n        button: {\n          color: '#9d9d9d',\n          hoverColor: '#d4d4d4'\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: '#b0b0b0',\n        link: {\n          color: '#b0b0b0',\n          hoverColor: '#c0c0c0'\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: 'rgba(255, 255, 255, 0.05)',\n        text: '#d4d4d4',\n        icon: '#9d9d9d',\n        checkbox: {\n          color: '#d4d4d4'\n        },\n        invalid: {\n          opacity: 0.6,\n          text: '#b0b0b0'\n        }\n      },\n      empty: {\n        text: '#9d9d9d'\n      },\n      button: {\n        bg: '#525252',\n        color: '#d4d4d4',\n        border: '#525252',\n        hoverBorder: '#696969'\n      }\n    },\n    protoFiles: {\n      header: {\n        text: '#9d9d9d',\n        button: {\n          color: '#9d9d9d',\n          hoverColor: '#d4d4d4'\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: '#b0b0b0',\n        link: {\n          color: '#b0b0b0',\n          hoverColor: '#c0c0c0'\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: 'rgba(255, 255, 255, 0.05)',\n        selected: {\n          bg: 'rgba(163, 163, 163, 0.2)',\n          border: '#a3a3a3'\n        },\n        text: '#d4d4d4',\n        secondaryText: '#9d9d9d',\n        icon: '#9d9d9d',\n        invalid: {\n          opacity: 0.6,\n          text: '#b0b0b0'\n        }\n      },\n      empty: {\n        text: '#9d9d9d'\n      },\n      button: {\n        bg: '#525252',\n        color: '#d4d4d4',\n        border: '#525252',\n        hoverBorder: '#696969'\n      }\n    }\n  },\n  deprecationWarning: {\n    bg: 'rgba(176, 176, 176, 0.1)',\n    border: 'rgba(176, 176, 176, 0.1)',\n    icon: '#b0b0b0',\n    text: '#B8B8B8'\n  },\n\n  examples: {\n    buttonBg: '#a3a3a31A',\n    buttonColor: '#a3a3a3',\n    buttonText: '#fff',\n    buttonIconColor: '#fff',\n    border: '#444',\n    urlBar: {\n      border: colors.GRAY_3,\n      bg: '#292929'\n    },\n    table: {\n      thead: {\n        bg: '#292929',\n        color: '#969696'\n      }\n    },\n    checkbox: {\n      color: '#000'\n    }\n  },\n\n  app: {\n    collection: {\n      toolbar: {\n        environmentSelector: {\n          bg: colors.BG,\n          border: colors.GRAY_3,\n          icon: colors.BRAND,\n          text: colors.TEXT,\n          caret: colors.TEXT_MUTED,\n          separator: colors.GRAY_3,\n          hoverBg: colors.BG,\n          hoverBorder: colors.GRAY_4,\n\n          noEnvironment: {\n            text: colors.TEXT_MUTED,\n            bg: colors.BG,\n            border: colors.GRAY_3,\n            hoverBg: colors.BG,\n            hoverBorder: colors.GRAY_4\n          }\n        },\n        sandboxMode: {\n          safeMode: {\n            bg: 'rgba(163, 163, 163, 0.12)',\n            color: '#a3a3a3'\n          },\n          developerMode: {\n            bg: 'rgba(163, 163, 163, 0.11)',\n            color: '#a3a3a3'\n          }\n        }\n      }\n    }\n  }\n};\n\nexport default darkMonochromeTheme;\n"
  },
  {
    "path": "packages/bruno-app/src/themes/dark/dark-pastel.js",
    "content": "/**\n * Dark Pastel Theme - \"Nebula\"\n * A rich, dreamy dark theme with luminous pastel accents.\n * Inspired by northern lights, night gardens, and starlit dreams.\n * Deep enough to be easy on the eyes, vibrant enough to inspire.\n */\n\nimport { rgba } from 'polished';\n\nconst colors = {\n  // Primary palette - glowing pastels against deep purple-black\n  BRAND: '#f0a6ca', // Soft rose - warm and inviting\n  TEXT: '#e8e0f0', // Lavender white - soft, readable\n  TEXT_MUTED: '#a89fc4', // Dusty lavender\n  TEXT_LINK: '#a8c5f0', // Soft periwinkle blue\n  BG: '#1a1625', // Deep plum-black\n\n  // Core colors\n  WHITE: '#ffffff',\n  BLACK: '#0d0a12',\n  SLATE: '#e8e0f0',\n\n  // Luminous pastels - glowing against darkness\n  GREEN: '#7dd3a8', // Mint glow\n  YELLOW: '#f0d77d', // Soft gold\n  RED: '#f0887d', // Coral blush\n  PURPLE: '#c4a6f0', // Lavender dream\n  BLUE: '#7db8f0', // Sky shimmer\n  PINK: '#f0a6ca', // Rose petal\n  ORANGE: '#f0b87d', // Peach sunset\n  TEAL: '#7dd3c9', // Aqua glow\n  MAGENTA: '#e09fd9', // Orchid\n\n  // Deep grayscale with purple undertones\n  GRAY_1: '#1f1a2e', // Deepest plum\n  GRAY_2: '#2a2440', // Dark violet\n  GRAY_3: '#352e4d', // Muted purple\n  GRAY_4: '#453d5c', // Dusty violet\n  GRAY_5: '#5c5478', // Medium violet\n  GRAY_6: '#7a7294', // Soft violet\n  GRAY_7: '#9890ad', // Light violet\n  GRAY_8: '#b8b0cc', // Pale violet\n  GRAY_9: '#d8d0e8', // Whisper violet\n  GRAY_10: '#f0e8ff', // Lightest violet\n\n  // CodeMirror syntax - a constellation of colors\n  CODEMIRROR_TOKENS: {\n    DEFINITION: '#7dd3a8', // Mint - definitions stand out fresh\n    PROPERTY: '#7db8f0', // Sky blue - clear and calm\n    STRING: '#f0a6ca', // Rose - strings feel warm\n    NUMBER: '#7dd3c9', // Teal - numbers are precise\n    ATOM: '#c4a6f0', // Lavender - atoms are special\n    VARIABLE: '#a8c5f0', // Periwinkle - variables flow\n    KEYWORD: '#e09fd9', // Orchid - keywords command attention\n    COMMENT: '#7a7294', // Muted violet - comments recede\n    OPERATOR: '#b8b0cc', // Pale violet - operators connect\n    TAG: '#7db8f0', // Sky blue - tags are structural\n    TAG_BRACKET: '#7a7294' // Muted violet - brackets recede\n  }\n};\n\nexport const palette = {};\n\npalette.intent = {\n  INFO: colors.BLUE,\n  SUCCESS: colors.GREEN,\n  WARNING: colors.ORANGE,\n  DANGER: colors.RED\n};\n\nconst darkPastelTheme = {\n  mode: 'dark',\n  brand: colors.BRAND,\n  text: colors.TEXT,\n  textLink: colors.TEXT_LINK,\n  draftColor: '#cc7b1b',\n  bg: colors.BG,\n\n  primary: {\n    solid: colors.BRAND,\n    text: colors.BRAND,\n    strong: colors.BRAND,\n    subtle: colors.BRAND\n  },\n\n  accents: {\n    primary: colors.BRAND\n  },\n\n  background: {\n    base: colors.BG,\n    mantle: colors.GRAY_1,\n    crust: colors.GRAY_2,\n    surface0: colors.GRAY_3,\n    surface1: colors.GRAY_4,\n    surface2: colors.GRAY_5\n  },\n\n  status: {\n    info: {\n      background: rgba(palette.intent.INFO, 0.15),\n      text: palette.intent.INFO,\n      border: palette.intent.INFO\n    },\n    success: {\n      background: rgba(palette.intent.SUCCESS, 0.15),\n      text: palette.intent.SUCCESS,\n      border: palette.intent.SUCCESS\n    },\n    warning: {\n      background: rgba(palette.intent.WARNING, 0.15),\n      text: palette.intent.WARNING,\n      border: palette.intent.WARNING\n    },\n    danger: {\n      background: rgba(palette.intent.DANGER, 0.15),\n      text: palette.intent.DANGER,\n      border: palette.intent.DANGER\n    }\n  },\n\n  overlay: {\n    overlay2: colors.GRAY_6,\n    overlay1: '#555555',\n    overlay0: '#444444'\n  },\n\n  font: {\n    size: {\n      xs: '0.6875rem',\n      sm: '0.75rem',\n      base: '0.8125rem',\n      md: '0.875rem',\n      lg: '1rem',\n      xl: '1.125rem'\n    }\n  },\n\n  shadow: {\n    sm: '0 1px 3px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(196, 166, 240, 0.08)',\n    md: '0 2px 8px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(196, 166, 240, 0.10)',\n    lg: '0 4px 16px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(196, 166, 240, 0.12)'\n  },\n\n  border: {\n    radius: {\n      sm: '4px',\n      base: '6px',\n      md: '8px',\n      lg: '10px',\n      xl: '12px'\n    },\n    border2: colors.GRAY_5,\n    border1: colors.GRAY_4,\n    border0: colors.GRAY_3\n  },\n\n  colors: {\n    text: {\n      white: colors.WHITE,\n      green: colors.GREEN,\n      danger: colors.RED,\n      warning: colors.ORANGE,\n      muted: colors.TEXT_MUTED,\n      purple: colors.PURPLE,\n      yellow: colors.YELLOW,\n      subtext2: colors.GRAY_8,\n      subtext1: colors.GRAY_7,\n      subtext0: colors.GRAY_6\n    },\n    bg: {\n      danger: '#c4626a'\n    },\n    accent: colors.BRAND\n  },\n\n  input: {\n    bg: 'transparent',\n    border: colors.GRAY_4,\n    focusBorder: colors.BRAND,\n    placeholder: {\n      color: colors.TEXT_MUTED,\n      opacity: 0.6\n    }\n  },\n\n  sidebar: {\n    color: colors.TEXT,\n    muted: colors.TEXT_MUTED,\n    bg: '#1d1928',\n    dragbar: {\n      border: colors.GRAY_3,\n      activeBorder: colors.BRAND\n    },\n    collection: {\n      item: {\n        bg: colors.GRAY_2,\n        hoverBg: colors.GRAY_3,\n        focusBorder: colors.GRAY_5,\n        indentBorder: colors.GRAY_3,\n        active: {\n          indentBorder: colors.GRAY_3\n        },\n        example: {\n          iconColor: colors.GRAY_6\n        }\n      }\n    },\n    dropdownIcon: {\n      color: colors.TEXT_MUTED\n    }\n  },\n\n  dropdown: {\n    color: colors.TEXT,\n    iconColor: colors.TEXT_MUTED,\n    bg: colors.GRAY_2,\n    hoverBg: colors.GRAY_3,\n    shadow: 'none',\n    border: colors.GRAY_4,\n    separator: colors.GRAY_4,\n    selectedColor: colors.BRAND,\n    mutedText: colors.GRAY_6\n  },\n\n  workspace: {\n    accent: colors.BRAND,\n    border: colors.GRAY_4,\n    button: {\n      bg: colors.GRAY_3\n    }\n  },\n\n  request: {\n    methods: {\n      get: colors.GREEN, // Mint - success, retrieval\n      post: colors.PURPLE, // Lavender - creation\n      put: colors.ORANGE, // Peach - update\n      delete: colors.RED, // Coral - deletion\n      patch: colors.YELLOW, // Gold - modification\n      options: colors.TEAL, // Aqua - metadata\n      head: colors.BLUE // Sky - lightweight\n    },\n    grpc: '#9e8fd9', // Soft indigo\n    ws: colors.MAGENTA, // Orchid\n    gql: colors.PINK // Rose\n  },\n\n  requestTabPanel: {\n    url: {\n      bg: colors.BG,\n      icon: colors.TEXT_MUTED,\n      iconDanger: colors.RED,\n      border: `solid 1px ${colors.GRAY_4}`\n    },\n    dragbar: {\n      border: colors.GRAY_4,\n      activeBorder: colors.BRAND\n    },\n    responseStatus: colors.TEXT_MUTED,\n    responseOk: colors.GREEN,\n    responseError: colors.RED,\n    responsePending: colors.BLUE,\n    responseOverlayBg: 'rgba(26, 22, 37, 0.75)',\n    card: {\n      bg: colors.GRAY_1,\n      border: 'transparent',\n      hr: colors.GRAY_4\n    },\n    graphqlDocsExplorer: {\n      bg: colors.BG,\n      color: colors.TEXT\n    }\n  },\n\n  notifications: {\n    bg: colors.GRAY_3,\n    list: {\n      bg: colors.GRAY_2,\n      borderRight: colors.GRAY_4,\n      borderBottom: colors.GRAY_4,\n      hoverBg: colors.GRAY_3,\n      active: {\n        border: colors.BRAND,\n        bg: colors.GRAY_4,\n        hoverBg: colors.GRAY_4\n      }\n    }\n  },\n\n  modal: {\n    title: {\n      color: colors.TEXT,\n      bg: colors.GRAY_1\n    },\n    body: {\n      color: colors.TEXT,\n      bg: colors.GRAY_2\n    },\n    input: {\n      bg: 'transparent',\n      border: colors.GRAY_4,\n      focusBorder: colors.BRAND\n    },\n    backdrop: {\n      opacity: 0.5\n    }\n  },\n\n  button: {\n    secondary: {\n      color: colors.TEXT,\n      bg: colors.GRAY_4,\n      border: colors.GRAY_4,\n      hoverBorder: colors.GRAY_6\n    },\n    close: {\n      color: colors.TEXT,\n      bg: 'transparent',\n      border: 'transparent',\n      hoverBorder: ''\n    },\n    disabled: {\n      color: colors.GRAY_6,\n      bg: colors.GRAY_4,\n      border: colors.GRAY_4\n    },\n    danger: {\n      color: colors.WHITE,\n      bg: '#c4626a',\n      border: '#c4626a'\n    }\n  },\n\n  button2: {\n    color: {\n      primary: {\n        bg: colors.BRAND,\n        text: colors.BLACK,\n        border: colors.BRAND\n      },\n      light: {\n        bg: rgba(colors.BRAND, 0.08),\n        text: colors.BRAND,\n        border: rgba(colors.BRAND, 0.06)\n      },\n      secondary: {\n        bg: colors.GRAY_4,\n        text: colors.TEXT,\n        border: colors.GRAY_5\n      },\n      success: {\n        bg: colors.GREEN,\n        text: colors.BLACK,\n        border: colors.GREEN\n      },\n      warning: {\n        bg: colors.ORANGE,\n        text: colors.BLACK,\n        border: colors.ORANGE\n      },\n      danger: {\n        bg: colors.RED,\n        text: colors.WHITE,\n        border: colors.RED\n      }\n    }\n  },\n\n  tabs: {\n    marginRight: '1.2rem',\n    active: {\n      fontWeight: 400,\n      color: colors.TEXT,\n      border: colors.BRAND\n    },\n    secondary: {\n      active: {\n        bg: colors.GRAY_3,\n        color: colors.TEXT\n      },\n      inactive: {\n        bg: colors.GRAY_3,\n        color: colors.TEXT_MUTED\n      }\n    }\n  },\n\n  requestTabs: {\n    color: colors.TEXT,\n    bg: colors.GRAY_2,\n    bottomBorder: colors.GRAY_4,\n    icon: {\n      color: colors.GRAY_6,\n      hoverColor: colors.TEXT,\n      hoverBg: colors.GRAY_3\n    },\n    example: {\n      iconColor: colors.GRAY_6\n    }\n  },\n\n  codemirror: {\n    bg: colors.BG,\n    border: colors.BG,\n    placeholder: {\n      color: colors.GRAY_6,\n      opacity: 0.6\n    },\n    gutter: {\n      bg: colors.BG\n    },\n    variable: {\n      valid: colors.GREEN,\n      invalid: colors.RED,\n      prompt: colors.BLUE\n    },\n    tokens: {\n      definition: colors.CODEMIRROR_TOKENS.DEFINITION,\n      property: colors.CODEMIRROR_TOKENS.PROPERTY,\n      string: colors.CODEMIRROR_TOKENS.STRING,\n      number: colors.CODEMIRROR_TOKENS.NUMBER,\n      atom: colors.CODEMIRROR_TOKENS.ATOM,\n      variable: colors.CODEMIRROR_TOKENS.VARIABLE,\n      keyword: colors.CODEMIRROR_TOKENS.KEYWORD,\n      comment: colors.CODEMIRROR_TOKENS.COMMENT,\n      operator: colors.CODEMIRROR_TOKENS.OPERATOR,\n      tag: colors.CODEMIRROR_TOKENS.TAG,\n      tagBracket: colors.CODEMIRROR_TOKENS.TAG_BRACKET\n    },\n    searchLineHighlightCurrent: `${colors.BRAND}20`,\n    searchMatch: colors.YELLOW,\n    searchMatchActive: colors.ORANGE\n  },\n\n  table: {\n    border: colors.GRAY_3,\n    thead: {\n      color: colors.TEXT_MUTED\n    },\n    striped: colors.GRAY_1,\n    input: {\n      color: colors.TEXT\n    }\n  },\n\n  plainGrid: {\n    hoverBg: colors.GRAY_3\n  },\n\n  scrollbar: {\n    color: colors.GRAY_5\n  },\n\n  dragAndDrop: {\n    border: colors.BRAND,\n    borderStyle: '2px dashed',\n    hoverBg: `${colors.BRAND}10`,\n    transition: 'all 0.15s ease'\n  },\n\n  infoTip: {\n    bg: colors.GRAY_2,\n    border: colors.GRAY_4,\n    boxShadow: '0 4px 16px rgba(0, 0, 0, 0.4)'\n  },\n\n  statusBar: {\n    border: colors.GRAY_3,\n    color: colors.TEXT_MUTED\n  },\n\n  console: {\n    bg: colors.BG,\n    headerBg: colors.GRAY_2,\n    contentBg: colors.BG,\n    border: colors.GRAY_4,\n    titleColor: colors.TEXT,\n    countColor: colors.TEXT_MUTED,\n    buttonColor: colors.TEXT,\n    buttonHoverBg: colors.GRAY_3,\n    buttonHoverColor: colors.WHITE,\n    messageColor: colors.TEXT,\n    timestampColor: colors.TEXT_MUTED,\n    emptyColor: colors.TEXT_MUTED,\n    logHoverBg: colors.GRAY_2,\n    resizeHandleHover: colors.BRAND,\n    resizeHandleActive: colors.BRAND,\n    dropdownBg: colors.GRAY_2,\n    dropdownHeaderBg: colors.GRAY_3,\n    optionHoverBg: colors.GRAY_3,\n    optionLabelColor: colors.TEXT,\n    optionCountColor: colors.TEXT_MUTED,\n    checkboxColor: colors.BRAND,\n    scrollbarTrack: colors.GRAY_2,\n    scrollbarThumb: colors.GRAY_5,\n    scrollbarThumbHover: colors.GRAY_6\n  },\n\n  grpc: {\n    tabNav: {\n      container: {\n        bg: colors.GRAY_1\n      },\n      button: {\n        active: {\n          bg: colors.GRAY_3,\n          color: colors.TEXT\n        },\n        inactive: {\n          bg: 'transparent',\n          color: colors.TEXT_MUTED\n        }\n      }\n    },\n    importPaths: {\n      header: {\n        text: colors.TEXT_MUTED,\n        button: {\n          color: colors.TEXT_MUTED,\n          hoverColor: colors.TEXT\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: colors.RED,\n        link: {\n          color: colors.RED,\n          hoverColor: '#f5a09a'\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: colors.GRAY_3,\n        text: colors.TEXT,\n        icon: colors.TEXT_MUTED,\n        checkbox: {\n          color: colors.TEXT\n        },\n        invalid: {\n          opacity: 0.6,\n          text: colors.RED\n        }\n      },\n      empty: {\n        text: colors.TEXT_MUTED\n      },\n      button: {\n        bg: colors.GRAY_4,\n        color: colors.TEXT,\n        border: colors.GRAY_4,\n        hoverBorder: colors.GRAY_6\n      }\n    },\n    protoFiles: {\n      header: {\n        text: colors.TEXT_MUTED,\n        button: {\n          color: colors.TEXT_MUTED,\n          hoverColor: colors.TEXT\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: colors.RED,\n        link: {\n          color: colors.RED,\n          hoverColor: '#f5a09a'\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: colors.GRAY_3,\n        selected: {\n          bg: `${colors.BRAND}25`,\n          border: colors.BRAND\n        },\n        text: colors.TEXT,\n        secondaryText: colors.TEXT_MUTED,\n        icon: colors.TEXT_MUTED,\n        invalid: {\n          opacity: 0.6,\n          text: colors.RED\n        }\n      },\n      empty: {\n        text: colors.TEXT_MUTED\n      },\n      button: {\n        bg: colors.GRAY_4,\n        color: colors.TEXT,\n        border: colors.GRAY_4,\n        hoverBorder: colors.GRAY_6\n      }\n    }\n  },\n\n  deprecationWarning: {\n    bg: `${colors.ORANGE}18`,\n    border: `${colors.ORANGE}35`,\n    icon: colors.ORANGE,\n    text: colors.TEXT\n  },\n\n  examples: {\n    buttonBg: `${colors.BRAND}20`,\n    buttonColor: colors.BRAND,\n    buttonText: colors.WHITE,\n    buttonIconColor: colors.WHITE,\n    border: colors.GRAY_4,\n    urlBar: {\n      border: colors.GRAY_4,\n      bg: colors.GRAY_2\n    },\n    table: {\n      thead: {\n        bg: colors.GRAY_2,\n        color: colors.TEXT_MUTED\n      }\n    },\n    checkbox: {\n      color: colors.BLACK\n    }\n  },\n\n  app: {\n    collection: {\n      toolbar: {\n        environmentSelector: {\n          bg: colors.BG,\n          border: colors.GRAY_4,\n          icon: colors.BRAND,\n          text: colors.TEXT,\n          caret: colors.TEXT_MUTED,\n          separator: colors.GRAY_4,\n          hoverBg: colors.BG,\n          hoverBorder: colors.GRAY_5,\n          noEnvironment: {\n            text: colors.TEXT_MUTED,\n            bg: colors.BG,\n            border: colors.GRAY_4,\n            hoverBg: colors.BG,\n            hoverBorder: colors.GRAY_5\n          }\n        },\n        sandboxMode: {\n          safeMode: {\n            bg: `${colors.GREEN}18`,\n            color: colors.GREEN\n          },\n          developerMode: {\n            bg: `${colors.ORANGE}18`,\n            color: colors.ORANGE\n          }\n        }\n      }\n    }\n  }\n};\n\nexport default darkPastelTheme;\n"
  },
  {
    "path": "packages/bruno-app/src/themes/dark/dark.js",
    "content": "import { rgba, lighten } from 'polished';\n\nexport const palette = {\n  primary: {\n    SOLID: 'hsl(39, 74%, 59%)',\n    TEXT: 'hsl(39, 74%, 64%)',\n    STRONG: 'hsl(39, 74%, 64%)',\n    SUBTLE: 'hsl(39, 74%, 54%)'\n  },\n  hues: {\n    RED: 'hsl(8, 70%, 52%)',\n    ROSE: 'hsl(367, 84%, 70%)',\n    BROWN: 'hsl(35,  65%, 72%)',\n    ORANGE: 'hsl(24,  88%, 72%)',\n    YELLOW: 'hsl(41, 93%, 72%)',\n    GREEN: 'hsl(140, 72%, 68%)',\n    GREEN_DARK: 'hsl(160, 90%, 44%)',\n    TEAL: 'hsl(170, 70%, 60%)',\n    CYAN: 'hsl(190, 82%, 72%)',\n    BLUE: 'hsl(210, 90%, 76%)',\n    INDIGO: 'hsl(202, 88%, 72%)',\n    VIOLET: 'hsl(260, 75%, 78%)',\n    PURPLE: 'hsl(285, 72%, 75%)',\n    PINK: 'hsl(305, 59%, 74%)'\n  },\n  system: {\n    CONTROL_ACCENT: '#D9A342'\n  },\n  background: {\n    BASE: 'hsl(0deg 0% 10%)',\n    MANTLE: '#222224',\n    CRUST: '#1e1e1e',\n    SURFACE0: '#26292b',\n    SURFACE1: 'hsl(204, 4%, 23%)',\n    SURFACE2: '#666666'\n  },\n  text: {\n    BASE: 'hsl(0deg 0% 80%)',\n    SUBTEXT2: '#bbb',\n    SUBTEXT1: '#aaa',\n    SUBTEXT0: '#999'\n  },\n  overlay: {\n    OVERLAY2: '#666666',\n    OVERLAY1: '#555555',\n    OVERLAY0: '#444444'\n  },\n  border: {\n    BORDER2: '#444444',\n    BORDER1: '#333333',\n    BORDER0: '#2a2a2a'\n  },\n  utility: {\n    WHITE: '#ffffff',\n    BLACK: '#000000'\n  }\n};\n\npalette.intent = {\n  INFO: palette.hues.BLUE,\n  SUCCESS: palette.hues.GREEN,\n  WARNING: palette.hues.ORANGE,\n  DANGER: palette.hues.RED\n};\n\npalette.syntax = {\n  // Core language structure\n  KEYWORD: palette.hues.ROSE,\n  TAG: palette.hues.ROSE,\n  // Identifiers & properties (collapsed)\n  VARIABLE: palette.hues.PINK,\n  PROPERTY: palette.hues.BLUE,\n  DEFINITION: palette.hues.BLUE,\n\n  // Literals\n  STRING: palette.hues.BROWN,\n  NUMBER: palette.hues.PINK,\n  ATOM: palette.hues.ROSE,\n\n  // Operators & punctuation (quiet)\n  OPERATOR: palette.text.SUBTEXT1,\n  TAG_BRACKET: palette.text.SUBTEXT1,\n\n  // Comments should recede\n  COMMENT: palette.text.SUBTEXT0\n};\n\nconst colors = {\n  GRAY_2: '#3D3D3D',\n  GRAY_3: '#444444',\n  GRAY_4: '#666666',\n  GRAY_5: '#b0b0b0'\n};\n\nconst darkTheme = {\n  mode: 'dark',\n  brand: palette.primary.SOLID,\n  text: palette.text.BASE,\n  textLink: palette.hues.BLUE,\n  draftColor: '#cc7b1b',\n  bg: palette.background.BASE,\n\n  primary: {\n    solid: palette.primary.SOLID,\n    text: palette.primary.TEXT,\n    strong: palette.primary.STRONG,\n    subtle: palette.primary.SUBTLE\n  },\n\n  accents: {\n    primary: palette.primary.SOLID\n  },\n\n  background: {\n    base: palette.background.BASE,\n    mantle: palette.background.MANTLE,\n    crust: '#333333',\n    surface0: palette.background.SURFACE0,\n    surface1: colors.GRAY_3,\n    surface2: colors.GRAY_4\n  },\n\n  status: {\n    info: {\n      background: rgba(palette.intent.INFO, 0.15),\n      text: palette.intent.INFO,\n      border: palette.intent.INFO\n    },\n    success: {\n      background: rgba(palette.intent.SUCCESS, 0.15),\n      text: palette.intent.SUCCESS,\n      border: palette.intent.SUCCESS\n    },\n    warning: {\n      background: rgba(palette.intent.WARNING, 0.15),\n      text: palette.intent.WARNING,\n      border: palette.intent.WARNING\n    },\n    danger: {\n      background: rgba(palette.intent.DANGER, 0.15),\n      text: palette.intent.DANGER,\n      border: palette.intent.DANGER\n    }\n  },\n\n  overlay: {\n    overlay2: '#666666',\n    overlay1: '#555555',\n    overlay0: '#444444'\n  },\n\n  font: {\n    size: {\n      xs: '0.6875rem', // 11px\n      sm: '0.75rem', // 12px\n      base: '0.8125rem', // 13px\n      md: '0.875rem', // 14px\n      lg: '1rem', // 16px\n      xl: '1.125rem' // 18px\n    }\n  },\n\n  shadow: {\n    sm: '0 1px 3px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(0, 0, 0, 0.3)',\n    md: '0 2px 8px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(0, 0, 0, 0.4)',\n    lg: '0 2px 12px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(0, 0, 0, 0.4)'\n  },\n\n  border: {\n    radius: {\n      sm: '4px',\n      base: '6px',\n      md: '8px',\n      lg: '10px',\n      xl: '12px'\n    },\n    border2: palette.border.BORDER2,\n    border1: palette.border.BORDER1,\n    border0: palette.border.BORDER0\n  },\n\n  colors: {\n    text: {\n      white: palette.text.BASE,\n      green: palette.intent.SUCCESS,\n      danger: palette.intent.DANGER,\n      warning: palette.intent.WARNING,\n      muted: palette.text.SUBTEXT1,\n      purple: palette.hues.PURPLE,\n      yellow: palette.hues.YELLOW,\n      subtext2: palette.text.SUBTEXT2,\n      subtext1: palette.text.SUBTEXT1,\n      subtext0: palette.text.SUBTEXT0\n    },\n    bg: {\n      danger: palette.hues.RED\n    },\n    accent: palette.system.CONTROL_ACCENT\n  },\n\n  input: {\n    bg: 'transparent',\n    border: palette.border.BORDER2,\n    focusBorder: rgba(palette.primary.SOLID, 0.8),\n    placeholder: {\n      color: palette.text.SUBTEXT1,\n      opacity: 0.6\n    }\n  },\n\n  sidebar: {\n    color: palette.text.BASE,\n    muted: palette.text.SUBTEXT1,\n    bg: palette.background.BASE,\n    dragbar: {\n      border: palette.border.BORDER1,\n      activeBorder: palette.border.BORDER2\n    },\n\n    collection: {\n      item: {\n        bg: palette.background.SURFACE0,\n        hoverBg: palette.background.MANTLE,\n        focusBorder: palette.border.BORDER2,\n        indentBorder: palette.background.SURFACE0,\n        active: {\n          indentBorder: palette.background.SURFACE0\n        },\n        example: {\n          iconColor: palette.text.BASE\n        }\n      }\n    },\n\n    dropdownIcon: {\n      color: palette.text.BASE\n    }\n  },\n\n  dropdown: {\n    color: palette.text.BASE,\n    iconColor: palette.text.SUBTEXT2,\n    bg: palette.background.MANTLE,\n    hoverBg: palette.background.SURFACE0,\n    shadow: 'none',\n    border: palette.border.BORDER1,\n    separator: palette.border.BORDER1,\n    selectedColor: palette.primary.TEXT,\n    mutedText: palette.text.SUBTEXT1\n  },\n\n  workspace: {\n    accent: '#D9A342',\n    border: '#444',\n    button: {\n      bg: colors.GRAY_2\n    }\n  },\n\n  request: {\n    methods: {\n      get: palette.hues.GREEN,\n      post: palette.hues.INDIGO,\n      put: palette.hues.ORANGE,\n      delete: lighten(0.08, palette.hues.RED),\n      patch: palette.hues.ORANGE,\n      options: palette.hues.TEAL,\n      head: palette.hues.CYAN\n    },\n\n    grpc: palette.hues.TEAL,\n    ws: palette.hues.ORANGE,\n    gql: palette.hues.PINK\n  },\n\n  requestTabPanel: {\n    url: {\n      bg: palette.background.BASE,\n      icon: 'rgb(204, 204, 204)',\n      iconDanger: '#fa5343',\n      border: `solid 1px ${palette.border.BORDER1}`\n    },\n    dragbar: {\n      border: palette.border.BORDER1,\n      activeBorder: palette.border.BORDER2\n    },\n    responseStatus: '#ccc',\n    responseOk: palette.hues.GREEN,\n    responseError: palette.hues.RED,\n    responsePending: palette.hues.BLUE,\n    responseOverlayBg: rgba(palette.background.BASE, 0.8),\n\n    card: {\n      bg: '#252526',\n      border: 'transparent',\n      hr: '#424242'\n    },\n\n    graphqlDocsExplorer: {\n      bg: '#1e1e1e',\n      color: '#d4d4d4'\n    }\n  },\n\n  notifications: {\n    bg: colors.GRAY_3,\n    list: {\n      bg: '3D3D3D',\n      borderRight: '#4f4f4f',\n      borderBottom: '#545454',\n      hoverBg: '#434343',\n      active: {\n        border: '#569cd6',\n        bg: '#4f4f4f',\n        hoverBg: '#4f4f4f'\n      }\n    }\n  },\n\n  modal: {\n    title: {\n      color: palette.text.BASE,\n      bg: palette.background.BASE\n    },\n    body: {\n      color: palette.text.BASE,\n      bg: palette.background.MANTLE\n    },\n    input: {\n      bg: 'transparent',\n      border: palette.border.BORDER2,\n      focusBorder: rgba(palette.primary.SOLID, 0.8)\n    },\n    backdrop: {\n      opacity: 0.2\n    }\n  },\n\n  button: {\n    secondary: {\n      color: 'rgb(204, 204, 204)',\n      bg: '#185387',\n      border: '#185387',\n      hoverBorder: '#696969'\n    },\n    close: {\n      color: '#ccc',\n      bg: 'transparent',\n      border: 'transparent',\n      hoverBorder: ''\n    },\n    disabled: {\n      color: '#a5a5a5',\n      bg: '#626262',\n      border: '#626262'\n    },\n    danger: {\n      color: '#fff',\n      bg: '#dc3545',\n      border: '#dc3545'\n    }\n  },\n  button2: {\n    color: {\n      primary: {\n        bg: palette.primary.SOLID,\n        text: palette.utility.BLACK,\n        border: palette.primary.SOLID\n      },\n      light: {\n        bg: rgba(palette.primary.SOLID, 0.08),\n        text: palette.primary.SOLID,\n        border: rgba(palette.primary.SOLID, 0.06)\n      },\n      secondary: {\n        bg: palette.background.MANTLE,\n        text: palette.text.BASE,\n        border: palette.border.BORDER1\n      },\n      success: {\n        bg: palette.hues.GREEN,\n        text: palette.utility.WHITE,\n        border: palette.hues.GREEN\n      },\n      warning: {\n        bg: palette.hues.ORANGE,\n        text: '#1e1e1e',\n        border: palette.hues.ORANGE\n      },\n      danger: {\n        bg: palette.hues.RED,\n        text: palette.utility.WHITE,\n        border: palette.hues.RED\n      }\n    }\n  },\n\n  tabs: {\n    marginRight: '1.2rem',\n    active: {\n      fontWeight: 400,\n      color: '#CCCCCC',\n      border: palette.primary.STRONG\n    },\n    secondary: {\n      active: {\n        bg: palette.background.SURFACE0,\n        color: palette.text.BASE\n      },\n      inactive: {\n        bg: palette.background.SURFACE0,\n        color: palette.text.SUBTEXT1\n      }\n    }\n  },\n\n  requestTabs: {\n    color: palette.text.BASE,\n    bg: palette.background.SURFACE0,\n    bottomBorder: palette.border.BORDER2,\n    icon: {\n      color: '#9f9f9f',\n      hoverColor: 'rgb(204, 204, 204)',\n      hoverBg: '#1e1e1e'\n    },\n    example: {\n      iconColor: colors.GRAY_5\n    }\n  },\n\n  codemirror: {\n    bg: palette.background.BASE,\n    border: palette.background.BASE,\n    placeholder: {\n      color: '#a2a2a2',\n      opacity: 0.5\n    },\n    gutter: {\n      bg: palette.background.BASE\n    },\n    variable: {\n      valid: palette.hues.GREEN_DARK,\n      invalid: palette.hues.RED,\n      prompt: palette.hues.BLUE\n    },\n    tokens: {\n      definition: palette.syntax.DEFINITION,\n      property: palette.syntax.PROPERTY,\n      string: palette.syntax.STRING,\n      number: palette.syntax.NUMBER,\n      atom: palette.syntax.ATOM,\n      variable: palette.syntax.VARIABLE,\n      keyword: palette.syntax.KEYWORD,\n      comment: palette.syntax.COMMENT,\n      operator: palette.syntax.OPERATOR,\n      tag: palette.syntax.TAG,\n      tagBracket: palette.syntax.TAG_BRACKET\n    },\n    searchLineHighlightCurrent: 'rgba(120,120,120,0.18)',\n    searchMatch: '#FFD700',\n    searchMatchActive: '#FFFF00'\n  },\n\n  table: {\n    border: '#333',\n    thead: {\n      color: 'rgb(204, 204, 204)'\n    },\n    striped: '#1e1e1e',\n    input: {\n      color: '#ccc'\n    }\n  },\n\n  plainGrid: {\n    hoverBg: colors.GRAY_3\n  },\n\n  scrollbar: {\n    color: 'rgb(52 51 49)'\n  },\n\n  dragAndDrop: {\n    border: '#666666',\n    borderStyle: '2px solid',\n    hoverBg: 'rgba(102, 102, 102, 0.08)',\n    transition: 'all 0.1s ease'\n  },\n  infoTip: {\n    bg: palette.background.MANTLE,\n    border: '#333333',\n    boxShadow: '0 4px 12px rgba(0, 0, 0, 0.5)'\n  },\n\n  statusBar: {\n    border: '#323233',\n    color: 'rgb(169, 169, 169)'\n  },\n\n  console: {\n    bg: '#1e1e1e',\n    headerBg: '#242424',\n    contentBg: '#1e1e1e',\n    border: '#3c3c3c',\n    titleColor: '#cccccc',\n    countColor: '#858585',\n    buttonColor: '#cccccc',\n    buttonHoverBg: 'rgba(255, 255, 255, 0.1)',\n    buttonHoverColor: '#ffffff',\n    messageColor: '#cccccc',\n    timestampColor: '#858585',\n    emptyColor: '#858585',\n    logHoverBg: 'rgba(255, 255, 255, 0.05)',\n    resizeHandleHover: '#0078d4',\n    resizeHandleActive: '#0078d4',\n    dropdownBg: '#2d2d30',\n    dropdownHeaderBg: '#3c3c3c',\n    optionHoverBg: 'rgba(255, 255, 255, 0.05)',\n    optionLabelColor: '#cccccc',\n    optionCountColor: '#858585',\n    checkboxColor: palette.primary.SOLID,\n    scrollbarTrack: '#2d2d30',\n    scrollbarThumb: '#5a5a5a',\n    scrollbarThumbHover: '#6a6a6a'\n  },\n\n  grpc: {\n    tabNav: {\n      container: {\n        bg: '#262626'\n      },\n      button: {\n        active: {\n          bg: '#404040',\n          color: '#ffffff'\n        },\n        inactive: {\n          bg: 'transparent',\n          color: '#a3a3a3'\n        }\n      }\n    },\n    importPaths: {\n      header: {\n        text: palette.text.SUBTEXT1,\n        button: {\n          color: palette.text.SUBTEXT1,\n          hoverColor: '#d4d4d4'\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: '#f06f57',\n        link: {\n          color: '#f06f57',\n          hoverColor: '#ff8a7a'\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: 'rgba(255, 255, 255, 0.05)',\n        text: '#d4d4d4',\n        icon: palette.text.SUBTEXT1,\n        checkbox: {\n          color: '#d4d4d4'\n        },\n        invalid: {\n          opacity: 0.6,\n          text: '#f06f57'\n        }\n      },\n      empty: {\n        text: palette.text.SUBTEXT1\n      },\n      button: {\n        bg: '#185387',\n        color: '#d4d4d4',\n        border: '#185387',\n        hoverBorder: '#696969'\n      }\n    },\n    protoFiles: {\n      header: {\n        text: palette.text.SUBTEXT1,\n        button: {\n          color: palette.text.SUBTEXT1,\n          hoverColor: '#d4d4d4'\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: '#f06f57',\n        link: {\n          color: '#f06f57',\n          hoverColor: '#ff8a7a'\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: 'rgba(255, 255, 255, 0.05)',\n        selected: {\n          bg: 'rgba(245, 158, 11, 0.2)',\n          border: '#d9a342'\n        },\n        text: '#d4d4d4',\n        secondaryText: palette.text.SUBTEXT1,\n        icon: palette.text.SUBTEXT1,\n        invalid: {\n          opacity: 0.6,\n          text: '#f06f57'\n        }\n      },\n      empty: {\n        text: palette.text.SUBTEXT1\n      },\n      button: {\n        bg: '#185387',\n        color: '#d4d4d4',\n        border: '#185387',\n        hoverBorder: '#696969'\n      }\n    }\n  },\n  deprecationWarning: {\n    bg: 'rgba(250, 83, 67, 0.1)',\n    border: 'rgba(250, 83, 67, 0.1)',\n    icon: '#FA5343',\n    text: '#B8B8B8'\n  },\n\n  examples: {\n    buttonBg: '#d9a3421A',\n    buttonColor: '#d9a342',\n    buttonText: '#fff',\n    buttonIconColor: '#fff',\n    border: '#444',\n    urlBar: {\n      border: colors.GRAY_3,\n      bg: '#292929'\n    },\n    table: {\n      thead: {\n        bg: '#292929',\n        color: '#969696'\n      }\n    },\n    checkbox: {\n      color: '#000'\n    }\n  },\n\n  app: {\n    collection: {\n      toolbar: {\n        environmentSelector: {\n          bg: palette.background.BASE,\n          border: colors.GRAY_3,\n          icon: palette.primary.TEXT,\n          text: palette.text.BASE,\n          caret: palette.text.SUBTEXT1,\n          separator: colors.GRAY_3,\n          hoverBg: palette.background.BASE,\n          hoverBorder: colors.GRAY_4,\n\n          noEnvironment: {\n            text: palette.text.SUBTEXT1,\n            bg: palette.background.BASE,\n            border: colors.GRAY_3,\n            hoverBg: palette.background.BASE,\n            hoverBorder: colors.GRAY_4\n          }\n        },\n        sandboxMode: {\n          safeMode: {\n            bg: 'rgba(78, 201, 176, 0.12)',\n            color: palette.hues.GREEN\n          },\n          developerMode: {\n            bg: 'rgba(217, 163, 66, 0.11)',\n            color: palette.hues.YELLOW\n          }\n        }\n      }\n    }\n  }\n};\n\nexport default darkTheme;\n"
  },
  {
    "path": "packages/bruno-app/src/themes/dark/nord.js",
    "content": "// Nord Theme for Bruno\n// https://www.nordtheme.com/\n//\n// Polar Night: nord0-nord3 (#2e3440, #3b4252, #434c5e, #4c566a)\n// Snow Storm: nord4-nord6 (#d8dee9, #e5e9f0, #eceff4)\n// Frost: nord7-nord10 (#8fbcbb, #88c0d0, #81a1c1, #5e81ac)\n// Aurora: nord11-nord15 (#bf616a, #d08770, #ebcb8b, #a3be8c, #b48ead)\n\nimport { rgba } from 'polished';\n\nconst colors = {\n  // Polar Night\n  NORD0: '#2e3440',\n  NORD1: '#3b4252',\n  NORD2: '#434c5e',\n  NORD3: '#4c566a',\n\n  // Snow Storm\n  NORD4: '#d8dee9',\n  NORD5: '#e5e9f0',\n  NORD6: '#eceff4',\n\n  // Frost\n  NORD7: '#8fbcbb',\n  NORD8: '#88c0d0',\n  NORD9: '#81a1c1',\n  NORD10: '#5e81ac',\n\n  // Aurora\n  NORD11: '#bf616a',\n  NORD12: '#d08770',\n  NORD13: '#ebcb8b',\n  NORD14: '#a3be8c',\n  NORD15: '#b48ead',\n\n  // Semantic aliases\n  BRAND: '#88c0d0',\n  TEXT: '#d8dee9',\n  TEXT_MUTED: '#7b88a1',\n  TEXT_LINK: '#88c0d0',\n  BG: '#2e3440',\n\n  WHITE: '#eceff4',\n  BLACK: '#2e3440',\n\n  CODEMIRROR_TOKENS: {\n    DEFINITION: '#a3be8c',\n    PROPERTY: '#88c0d0',\n    STRING: '#a3be8c',\n    NUMBER: '#b48ead',\n    ATOM: '#81a1c1',\n    VARIABLE: '#d8dee9',\n    KEYWORD: '#81a1c1',\n    COMMENT: '#616e88',\n    OPERATOR: '#81a1c1',\n    TAG: '#81a1c1',\n    TAG_BRACKET: '#616e88'\n  }\n};\n\nexport const palette = {};\n\npalette.intent = {\n  INFO: colors.NORD10,\n  SUCCESS: colors.NORD14,\n  WARNING: colors.NORD12,\n  DANGER: colors.NORD11\n};\n\nconst nordTheme = {\n  mode: 'dark',\n  brand: colors.BRAND,\n  text: colors.TEXT,\n  textLink: colors.TEXT_LINK,\n  draftColor: '#cc7b1b',\n  bg: colors.BG,\n\n  primary: {\n    solid: colors.BRAND,\n    text: colors.BRAND,\n    strong: colors.BRAND,\n    subtle: colors.BRAND\n  },\n\n  accents: {\n    primary: colors.BRAND\n  },\n\n  background: {\n    base: colors.NORD0,\n    mantle: colors.NORD1,\n    crust: colors.NORD2,\n    surface0: colors.NORD2,\n    surface1: colors.NORD3,\n    surface2: '#5d6b83'\n  },\n\n  status: {\n    info: {\n      background: rgba(palette.intent.INFO, 0.15),\n      text: palette.intent.INFO,\n      border: palette.intent.INFO\n    },\n    success: {\n      background: rgba(palette.intent.SUCCESS, 0.15),\n      text: palette.intent.SUCCESS,\n      border: palette.intent.SUCCESS\n    },\n    warning: {\n      background: rgba(palette.intent.WARNING, 0.15),\n      text: palette.intent.WARNING,\n      border: palette.intent.WARNING\n    },\n    danger: {\n      background: rgba(palette.intent.DANGER, 0.15),\n      text: palette.intent.DANGER,\n      border: palette.intent.DANGER\n    }\n  },\n\n  overlay: {\n    overlay2: '#616e88',\n    overlay1: '#5d6b83',\n    overlay0: '#4c566a'\n  },\n\n  font: {\n    size: {\n      xs: '0.6875rem',\n      sm: '0.75rem',\n      base: '0.8125rem',\n      md: '0.875rem',\n      lg: '1rem',\n      xl: '1.125rem'\n    }\n  },\n\n  shadow: {\n    sm: '0 1px 3px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(0, 0, 0, 0.2)',\n    md: '0 2px 8px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(0, 0, 0, 0.3)',\n    lg: '0 2px 12px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(0, 0, 0, 0.3)'\n  },\n\n  border: {\n    radius: {\n      sm: '4px',\n      base: '6px',\n      md: '8px',\n      lg: '10px',\n      xl: '12px'\n    },\n    border2: colors.NORD3,\n    border1: colors.NORD2,\n    border0: colors.NORD1\n  },\n\n  colors: {\n    text: {\n      white: colors.WHITE,\n      green: colors.NORD14,\n      danger: colors.NORD11,\n      warning: colors.NORD12,\n      muted: colors.TEXT_MUTED,\n      purple: colors.NORD15,\n      yellow: colors.NORD13,\n      subtext2: colors.NORD4,\n      subtext1: colors.TEXT_MUTED,\n      subtext0: '#616e88'\n    },\n    bg: {\n      danger: colors.NORD11\n    },\n    accent: colors.BRAND\n  },\n\n  input: {\n    bg: 'transparent',\n    border: colors.NORD3,\n    focusBorder: colors.NORD8,\n    placeholder: {\n      color: colors.TEXT_MUTED,\n      opacity: 0.6\n    }\n  },\n\n  sidebar: {\n    color: colors.NORD4,\n    muted: colors.TEXT_MUTED,\n    bg: colors.BG,\n    dragbar: {\n      border: colors.NORD2,\n      activeBorder: colors.NORD3\n    },\n    collection: {\n      item: {\n        bg: colors.NORD1,\n        hoverBg: colors.NORD2,\n        focusBorder: colors.NORD3,\n        indentBorder: colors.NORD2,\n        active: {\n          indentBorder: colors.NORD2\n        },\n        example: {\n          iconColor: colors.TEXT_MUTED\n        }\n      }\n    },\n    dropdownIcon: {\n      color: colors.NORD4\n    }\n  },\n\n  dropdown: {\n    color: colors.NORD4,\n    iconColor: colors.NORD4,\n    bg: colors.NORD1,\n    hoverBg: colors.NORD2,\n    shadow: 'none',\n    border: colors.NORD3,\n    separator: colors.NORD3,\n    selectedColor: colors.NORD8,\n    mutedText: colors.TEXT_MUTED\n  },\n\n  workspace: {\n    accent: colors.BRAND,\n    border: colors.NORD3,\n    button: {\n      bg: colors.NORD2\n    }\n  },\n\n  request: {\n    methods: {\n      get: colors.NORD14,\n      post: colors.NORD15,\n      put: colors.NORD13,\n      delete: colors.NORD11,\n      patch: colors.NORD12,\n      options: colors.NORD7,\n      head: colors.NORD9\n    },\n    grpc: colors.NORD8,\n    ws: colors.NORD13,\n    gql: colors.NORD15\n  },\n\n  requestTabPanel: {\n    url: {\n      bg: colors.BG,\n      icon: colors.NORD4,\n      iconDanger: colors.NORD11,\n      border: `solid 1px ${colors.NORD2}`\n    },\n    dragbar: {\n      border: colors.NORD3,\n      activeBorder: colors.NORD8\n    },\n    responseStatus: colors.NORD4,\n    responseOk: colors.NORD14,\n    responseError: colors.NORD11,\n    responsePending: colors.NORD8,\n    responseOverlayBg: 'rgba(46, 52, 64, 0.6)',\n    card: {\n      bg: colors.NORD1,\n      border: 'transparent',\n      hr: colors.NORD3\n    },\n    graphqlDocsExplorer: {\n      bg: colors.BG,\n      color: colors.NORD4\n    }\n  },\n\n  notifications: {\n    bg: colors.NORD2,\n    list: {\n      bg: colors.NORD1,\n      borderRight: colors.NORD3,\n      borderBottom: colors.NORD3,\n      hoverBg: colors.NORD2,\n      active: {\n        border: colors.NORD8,\n        bg: colors.NORD2,\n        hoverBg: colors.NORD2\n      }\n    }\n  },\n\n  modal: {\n    title: {\n      color: colors.NORD4,\n      bg: colors.NORD1\n    },\n    body: {\n      color: colors.NORD4,\n      bg: colors.NORD1\n    },\n    input: {\n      bg: 'transparent',\n      border: colors.NORD3,\n      focusBorder: colors.NORD8\n    },\n    backdrop: {\n      opacity: 0.3\n    }\n  },\n\n  button: {\n    secondary: {\n      color: colors.NORD4,\n      bg: colors.NORD10,\n      border: colors.NORD10,\n      hoverBorder: colors.NORD9\n    },\n    close: {\n      color: colors.NORD4,\n      bg: 'transparent',\n      border: 'transparent',\n      hoverBorder: ''\n    },\n    disabled: {\n      color: colors.TEXT_MUTED,\n      bg: colors.NORD3,\n      border: colors.NORD3\n    },\n    danger: {\n      color: colors.WHITE,\n      bg: colors.NORD11,\n      border: colors.NORD11\n    }\n  },\n\n  button2: {\n    color: {\n      primary: {\n        bg: colors.BRAND,\n        text: colors.NORD0,\n        border: colors.BRAND\n      },\n      light: {\n        bg: rgba(colors.BRAND, 0.08),\n        text: colors.BRAND,\n        border: rgba(colors.BRAND, 0.06)\n      },\n      secondary: {\n        bg: colors.NORD1,\n        text: colors.NORD4,\n        border: colors.NORD3\n      },\n      success: {\n        bg: colors.NORD14,\n        text: colors.NORD0,\n        border: colors.NORD14\n      },\n      warning: {\n        bg: colors.NORD13,\n        text: colors.NORD0,\n        border: colors.NORD13\n      },\n      danger: {\n        bg: colors.NORD11,\n        text: colors.NORD6,\n        border: colors.NORD11\n      }\n    }\n  },\n\n  tabs: {\n    marginRight: '1.2rem',\n    active: {\n      fontWeight: 400,\n      color: colors.NORD4,\n      border: colors.BRAND\n    },\n    secondary: {\n      active: {\n        bg: colors.NORD2,\n        color: colors.NORD4\n      },\n      inactive: {\n        bg: colors.NORD2,\n        color: colors.NORD4\n      }\n    }\n  },\n\n  requestTabs: {\n    color: colors.NORD4,\n    bg: colors.NORD1,\n    bottomBorder: colors.NORD3,\n    icon: {\n      color: colors.TEXT_MUTED,\n      hoverColor: colors.NORD4,\n      hoverBg: colors.NORD0\n    },\n    example: {\n      iconColor: colors.TEXT_MUTED\n    }\n  },\n\n  codemirror: {\n    bg: colors.BG,\n    border: colors.BG,\n    placeholder: {\n      color: colors.TEXT_MUTED,\n      opacity: 0.5\n    },\n    gutter: {\n      bg: colors.BG\n    },\n    variable: {\n      valid: colors.NORD14,\n      invalid: colors.NORD11,\n      prompt: colors.NORD8\n    },\n    tokens: {\n      definition: colors.CODEMIRROR_TOKENS.DEFINITION,\n      property: colors.CODEMIRROR_TOKENS.PROPERTY,\n      string: colors.CODEMIRROR_TOKENS.STRING,\n      number: colors.CODEMIRROR_TOKENS.NUMBER,\n      atom: colors.CODEMIRROR_TOKENS.ATOM,\n      variable: colors.CODEMIRROR_TOKENS.VARIABLE,\n      keyword: colors.CODEMIRROR_TOKENS.KEYWORD,\n      comment: colors.CODEMIRROR_TOKENS.COMMENT,\n      operator: colors.CODEMIRROR_TOKENS.OPERATOR,\n      tag: colors.CODEMIRROR_TOKENS.TAG,\n      tagBracket: colors.CODEMIRROR_TOKENS.TAG_BRACKET\n    },\n    searchLineHighlightCurrent: 'rgba(136, 192, 208, 0.15)',\n    searchMatch: colors.NORD13,\n    searchMatchActive: colors.NORD12\n  },\n\n  table: {\n    border: colors.NORD2,\n    thead: {\n      color: colors.NORD4\n    },\n    striped: colors.NORD1,\n    input: {\n      color: colors.NORD4\n    }\n  },\n\n  plainGrid: {\n    hoverBg: colors.NORD2\n  },\n\n  scrollbar: {\n    color: colors.NORD2\n  },\n\n  dragAndDrop: {\n    border: colors.NORD3,\n    borderStyle: '2px solid',\n    hoverBg: 'rgba(76, 86, 106, 0.15)',\n    transition: 'all 0.1s ease'\n  },\n\n  infoTip: {\n    bg: colors.NORD1,\n    border: colors.NORD3,\n    boxShadow: '0 4px 12px rgba(0, 0, 0, 0.4)'\n  },\n\n  statusBar: {\n    border: colors.NORD2,\n    color: colors.TEXT_MUTED\n  },\n\n  console: {\n    bg: colors.BG,\n    headerBg: colors.NORD1,\n    contentBg: colors.BG,\n    border: colors.NORD2,\n    titleColor: colors.NORD4,\n    countColor: colors.TEXT_MUTED,\n    buttonColor: colors.NORD4,\n    buttonHoverBg: 'rgba(255, 255, 255, 0.1)',\n    buttonHoverColor: colors.NORD6,\n    messageColor: colors.NORD4,\n    timestampColor: colors.TEXT_MUTED,\n    emptyColor: colors.TEXT_MUTED,\n    logHoverBg: 'rgba(255, 255, 255, 0.05)',\n    resizeHandleHover: colors.NORD8,\n    resizeHandleActive: colors.NORD8,\n    dropdownBg: colors.NORD1,\n    dropdownHeaderBg: colors.NORD2,\n    optionHoverBg: 'rgba(255, 255, 255, 0.05)',\n    optionLabelColor: colors.NORD4,\n    optionCountColor: colors.TEXT_MUTED,\n    checkboxColor: colors.BRAND,\n    scrollbarTrack: colors.NORD1,\n    scrollbarThumb: colors.NORD3,\n    scrollbarThumbHover: colors.NORD9\n  },\n\n  grpc: {\n    tabNav: {\n      container: {\n        bg: colors.NORD1\n      },\n      button: {\n        active: {\n          bg: colors.NORD2,\n          color: colors.NORD6\n        },\n        inactive: {\n          bg: 'transparent',\n          color: colors.TEXT_MUTED\n        }\n      }\n    },\n    importPaths: {\n      header: {\n        text: colors.TEXT_MUTED,\n        button: {\n          color: colors.TEXT_MUTED,\n          hoverColor: colors.NORD4\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: colors.NORD11,\n        link: {\n          color: colors.NORD11,\n          hoverColor: '#d08770'\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: 'rgba(255, 255, 255, 0.05)',\n        text: colors.NORD4,\n        icon: colors.TEXT_MUTED,\n        checkbox: {\n          color: colors.NORD4\n        },\n        invalid: {\n          opacity: 0.6,\n          text: colors.NORD11\n        }\n      },\n      empty: {\n        text: colors.TEXT_MUTED\n      },\n      button: {\n        bg: colors.NORD10,\n        color: colors.NORD4,\n        border: colors.NORD10,\n        hoverBorder: colors.NORD9\n      }\n    },\n    protoFiles: {\n      header: {\n        text: colors.TEXT_MUTED,\n        button: {\n          color: colors.TEXT_MUTED,\n          hoverColor: colors.NORD4\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: colors.NORD11,\n        link: {\n          color: colors.NORD11,\n          hoverColor: '#d08770'\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: 'rgba(255, 255, 255, 0.05)',\n        selected: {\n          bg: 'rgba(136, 192, 208, 0.2)',\n          border: colors.BRAND\n        },\n        text: colors.NORD4,\n        secondaryText: colors.TEXT_MUTED,\n        icon: colors.TEXT_MUTED,\n        invalid: {\n          opacity: 0.6,\n          text: colors.NORD11\n        }\n      },\n      empty: {\n        text: colors.TEXT_MUTED\n      },\n      button: {\n        bg: colors.NORD10,\n        color: colors.NORD4,\n        border: colors.NORD10,\n        hoverBorder: colors.NORD9\n      }\n    }\n  },\n\n  deprecationWarning: {\n    bg: 'rgba(191, 97, 106, 0.1)',\n    border: 'rgba(191, 97, 106, 0.2)',\n    icon: colors.NORD11,\n    text: colors.NORD4\n  },\n\n  examples: {\n    buttonBg: 'rgba(136, 192, 208, 0.1)',\n    buttonColor: colors.BRAND,\n    buttonText: colors.NORD6,\n    buttonIconColor: colors.NORD6,\n    border: colors.NORD3,\n    urlBar: {\n      border: colors.NORD2,\n      bg: colors.NORD1\n    },\n    table: {\n      thead: {\n        bg: colors.NORD1,\n        color: colors.TEXT_MUTED\n      }\n    },\n    checkbox: {\n      color: colors.NORD0\n    }\n  },\n\n  app: {\n    collection: {\n      toolbar: {\n        environmentSelector: {\n          bg: colors.BG,\n          border: colors.NORD2,\n          icon: colors.BRAND,\n          text: colors.TEXT,\n          caret: colors.TEXT_MUTED,\n          separator: colors.NORD2,\n          hoverBg: colors.BG,\n          hoverBorder: colors.NORD3,\n          noEnvironment: {\n            text: colors.TEXT_MUTED,\n            bg: colors.BG,\n            border: colors.NORD2,\n            hoverBg: colors.BG,\n            hoverBorder: colors.NORD3\n          }\n        },\n        sandboxMode: {\n          safeMode: {\n            bg: 'rgba(163, 190, 140, 0.12)',\n            color: colors.NORD14\n          },\n          developerMode: {\n            bg: 'rgba(235, 203, 139, 0.12)',\n            color: colors.NORD13\n          }\n        }\n      }\n    }\n  }\n};\n\nexport default nordTheme;\n"
  },
  {
    "path": "packages/bruno-app/src/themes/dark/vscode.js",
    "content": "// VS Code Dark+ Theme for Bruno\n// Based on the default Visual Studio Code Dark+ theme\n\nimport { rgba } from 'polished';\n\nconst colors = {\n  // VS Code Dark+ Core Colors\n  EDITOR_BG: '#1e1e1e',\n  SIDEBAR_BG: '#252526',\n  ACTIVITY_BAR_BG: '#333333',\n  PANEL_BG: '#1e1e1e',\n\n  // Text colors\n  TEXT: '#d4d4d4',\n  TEXT_MUTED: '#808080',\n  TEXT_LINK: '#3794ff',\n  BRAND_TEXT: '#4dabfc', // VS Code blue\n\n  // Brand - VS Code blue\n  BRAND: '#007acc',\n  BRAND_HOVER: '#1177bb',\n\n  // Semantic colors\n  GREEN: '#4ec9b0',\n  YELLOW: '#dcdcaa',\n  ORANGE: '#ce9178',\n  RED: '#f14c4c',\n  PURPLE: '#c586c0',\n  BLUE: '#569cd6',\n  CYAN: '#4fc1ff',\n\n  WHITE: '#ffffff',\n  BLACK: '#000000',\n\n  // Grays (VS Code specific)\n  GRAY_1: '#252526',\n  GRAY_2: '#2d2d2d',\n  GRAY_3: '#3c3c3c',\n  GRAY_4: '#474747',\n  GRAY_5: '#5a5a5a',\n  GRAY_6: '#6e6e6e',\n  GRAY_7: '#858585',\n  GRAY_8: '#a0a0a0',\n\n  // Borders\n  BORDER: '#454545',\n  BORDER_LIGHT: '#3c3c3c',\n\n  CODEMIRROR_TOKENS: {\n    DEFINITION: '#9cdcfe',\n    PROPERTY: '#9cdcfe',\n    STRING: '#ce9178',\n    NUMBER: '#b5cea8',\n    ATOM: '#569cd6',\n    VARIABLE: '#9cdcfe',\n    KEYWORD: '#c586c0',\n    COMMENT: '#6a9955',\n    OPERATOR: '#d4d4d4',\n    TAG: '#569cd6',\n    TAG_BRACKET: '#808080'\n  }\n};\n\nexport const palette = {};\n\npalette.intent = {\n  INFO: colors.BLUE,\n  SUCCESS: colors.GREEN,\n  WARNING: colors.ORANGE,\n  DANGER: colors.RED\n};\n\nconst vscodeDarkTheme = {\n  mode: 'dark',\n  brand: colors.BRAND,\n  text: colors.TEXT,\n  textLink: colors.TEXT_LINK,\n  draftColor: '#cc7b1b',\n  bg: colors.EDITOR_BG,\n\n  primary: {\n    solid: colors.BRAND,\n    text: colors.BRAND_TEXT,\n    strong: '#0098ff',\n    subtle: '#005a9e'\n  },\n\n  accents: {\n    primary: colors.BRAND\n  },\n\n  background: {\n    base: colors.EDITOR_BG,\n    mantle: colors.SIDEBAR_BG,\n    crust: colors.GRAY_2,\n    surface0: colors.GRAY_3,\n    surface1: colors.GRAY_4,\n    surface2: colors.GRAY_5\n  },\n\n  status: {\n    info: {\n      background: rgba(palette.intent.INFO, 0.15),\n      text: palette.intent.INFO,\n      border: palette.intent.INFO\n    },\n    success: {\n      background: rgba(palette.intent.SUCCESS, 0.15),\n      text: palette.intent.SUCCESS,\n      border: palette.intent.SUCCESS\n    },\n    warning: {\n      background: rgba(palette.intent.WARNING, 0.15),\n      text: palette.intent.WARNING,\n      border: palette.intent.WARNING\n    },\n    danger: {\n      background: rgba(palette.intent.DANGER, 0.15),\n      text: palette.intent.DANGER,\n      border: palette.intent.DANGER\n    }\n  },\n\n  overlay: {\n    overlay2: colors.GRAY_6,\n    overlay1: '#555555',\n    overlay0: '#444444'\n  },\n\n  font: {\n    size: {\n      xs: '0.6875rem',\n      sm: '0.75rem',\n      base: '0.8125rem',\n      md: '0.875rem',\n      lg: '1rem',\n      xl: '1.125rem'\n    }\n  },\n\n  shadow: {\n    sm: '0 1px 3px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(0, 0, 0, 0.3)',\n    md: '0 2px 8px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(0, 0, 0, 0.4)',\n    lg: '0 2px 12px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(0, 0, 0, 0.4)'\n  },\n\n  border: {\n    radius: {\n      sm: '4px',\n      base: '6px',\n      md: '8px',\n      lg: '10px',\n      xl: '12px'\n    },\n    border2: colors.GRAY_5,\n    border1: colors.BORDER,\n    border0: colors.BORDER_LIGHT\n  },\n\n  colors: {\n    text: {\n      white: colors.WHITE,\n      green: colors.GREEN,\n      danger: colors.RED,\n      warning: colors.ORANGE,\n      muted: colors.TEXT_MUTED,\n      purple: colors.PURPLE,\n      yellow: colors.YELLOW,\n      subtext2: colors.GRAY_8,\n      subtext1: colors.GRAY_7,\n      subtext0: colors.GRAY_6\n    },\n    bg: {\n      danger: '#f14c4c'\n    },\n    accent: colors.BRAND\n  },\n\n  input: {\n    bg: 'transparent',\n    border: colors.BORDER,\n    focusBorder: colors.BRAND,\n    placeholder: {\n      color: colors.TEXT_MUTED,\n      opacity: 0.6\n    }\n  },\n\n  sidebar: {\n    color: colors.TEXT,\n    muted: colors.TEXT_MUTED,\n    bg: colors.SIDEBAR_BG,\n    dragbar: {\n      border: colors.BORDER_LIGHT,\n      activeBorder: colors.GRAY_5\n    },\n    collection: {\n      item: {\n        bg: colors.GRAY_2,\n        hoverBg: colors.GRAY_3,\n        focusBorder: colors.GRAY_4,\n        indentBorder: colors.BORDER_LIGHT,\n        active: {\n          indentBorder: colors.BORDER_LIGHT\n        },\n        example: {\n          iconColor: colors.GRAY_7\n        }\n      }\n    },\n    dropdownIcon: {\n      color: colors.TEXT\n    }\n  },\n\n  dropdown: {\n    color: colors.TEXT,\n    iconColor: colors.TEXT,\n    bg: colors.SIDEBAR_BG,\n    hoverBg: colors.GRAY_3,\n    shadow: 'none',\n    border: colors.BORDER,\n    separator: colors.BORDER,\n    selectedColor: colors.TEXT_LINK,\n    mutedText: colors.TEXT_MUTED\n  },\n\n  workspace: {\n    accent: colors.BRAND,\n    border: colors.BORDER,\n    button: {\n      bg: colors.GRAY_2\n    }\n  },\n\n  request: {\n    methods: {\n      get: colors.GREEN,\n      post: '#dcdcaa',\n      put: colors.ORANGE,\n      delete: colors.RED,\n      patch: colors.ORANGE,\n      options: colors.GRAY_7,\n      head: colors.BLUE\n    },\n    grpc: colors.CYAN,\n    ws: colors.YELLOW,\n    gql: colors.PURPLE\n  },\n\n  requestTabPanel: {\n    url: {\n      bg: colors.EDITOR_BG,\n      icon: colors.TEXT,\n      iconDanger: colors.RED,\n      border: `solid 1px ${colors.BORDER}`\n    },\n    dragbar: {\n      border: colors.BORDER,\n      activeBorder: colors.BRAND\n    },\n    responseStatus: colors.TEXT,\n    responseOk: colors.GREEN,\n    responseError: colors.RED,\n    responsePending: colors.BRAND,\n    responseOverlayBg: 'rgba(30, 30, 30, 0.6)',\n    card: {\n      bg: colors.SIDEBAR_BG,\n      border: 'transparent',\n      hr: colors.BORDER\n    },\n    graphqlDocsExplorer: {\n      bg: colors.EDITOR_BG,\n      color: colors.TEXT\n    }\n  },\n\n  notifications: {\n    bg: colors.GRAY_3,\n    list: {\n      bg: colors.GRAY_2,\n      borderRight: colors.BORDER,\n      borderBottom: colors.BORDER,\n      hoverBg: colors.GRAY_3,\n      active: {\n        border: colors.BRAND,\n        bg: colors.GRAY_3,\n        hoverBg: colors.GRAY_3\n      }\n    }\n  },\n\n  modal: {\n    title: {\n      color: colors.TEXT,\n      bg: colors.SIDEBAR_BG\n    },\n    body: {\n      color: colors.TEXT,\n      bg: colors.GRAY_2\n    },\n    input: {\n      bg: 'transparent',\n      border: colors.BORDER,\n      focusBorder: colors.BRAND\n    },\n    backdrop: {\n      opacity: 0.25\n    }\n  },\n\n  button: {\n    secondary: {\n      color: colors.WHITE,\n      bg: colors.GRAY_4,\n      border: colors.GRAY_4,\n      hoverBorder: colors.GRAY_5\n    },\n    close: {\n      color: colors.TEXT,\n      bg: 'transparent',\n      border: 'transparent',\n      hoverBorder: ''\n    },\n    disabled: {\n      color: colors.GRAY_6,\n      bg: colors.GRAY_4,\n      border: colors.GRAY_4\n    },\n    danger: {\n      color: colors.WHITE,\n      bg: colors.RED,\n      border: colors.RED\n    }\n  },\n\n  button2: {\n    color: {\n      primary: {\n        bg: colors.BRAND,\n        text: colors.WHITE,\n        border: colors.BRAND\n      },\n      light: {\n        bg: rgba(colors.BRAND_TEXT, 0.08),\n        text: colors.BRAND_TEXT,\n        border: rgba(colors.BRAND_TEXT, 0.06)\n      },\n      secondary: {\n        bg: colors.GRAY_4,\n        text: colors.WHITE,\n        border: colors.GRAY_5\n      },\n      success: {\n        bg: '#388a34',\n        text: colors.WHITE,\n        border: '#388a34'\n      },\n      warning: {\n        bg: '#bf8803',\n        text: colors.BLACK,\n        border: '#bf8803'\n      },\n      danger: {\n        bg: colors.RED,\n        text: colors.WHITE,\n        border: colors.RED\n      }\n    }\n  },\n\n  tabs: {\n    marginRight: '1.2rem',\n    active: {\n      fontWeight: 400,\n      color: colors.TEXT,\n      border: colors.BRAND\n    },\n    secondary: {\n      active: {\n        bg: colors.GRAY_3,\n        color: colors.TEXT\n      },\n      inactive: {\n        bg: colors.GRAY_3,\n        color: colors.TEXT_MUTED\n      }\n    }\n  },\n\n  requestTabs: {\n    color: colors.TEXT,\n    bg: colors.SIDEBAR_BG,\n    bottomBorder: colors.BORDER,\n    icon: {\n      color: colors.TEXT_MUTED,\n      hoverColor: colors.TEXT,\n      hoverBg: colors.GRAY_3\n    },\n    example: {\n      iconColor: colors.GRAY_7\n    }\n  },\n\n  codemirror: {\n    bg: colors.EDITOR_BG,\n    border: colors.EDITOR_BG,\n    placeholder: {\n      color: colors.TEXT_MUTED,\n      opacity: 0.5\n    },\n    gutter: {\n      bg: colors.EDITOR_BG\n    },\n    variable: {\n      valid: colors.GREEN,\n      invalid: colors.RED,\n      prompt: colors.BRAND\n    },\n    tokens: {\n      definition: colors.CODEMIRROR_TOKENS.DEFINITION,\n      property: colors.CODEMIRROR_TOKENS.PROPERTY,\n      string: colors.CODEMIRROR_TOKENS.STRING,\n      number: colors.CODEMIRROR_TOKENS.NUMBER,\n      atom: colors.CODEMIRROR_TOKENS.ATOM,\n      variable: colors.CODEMIRROR_TOKENS.VARIABLE,\n      keyword: colors.CODEMIRROR_TOKENS.KEYWORD,\n      comment: colors.CODEMIRROR_TOKENS.COMMENT,\n      operator: colors.CODEMIRROR_TOKENS.OPERATOR,\n      tag: colors.CODEMIRROR_TOKENS.TAG,\n      tagBracket: colors.CODEMIRROR_TOKENS.TAG_BRACKET\n    },\n    searchLineHighlightCurrent: 'rgba(255, 255, 0, 0.1)',\n    searchMatch: '#515c6a',\n    searchMatchActive: '#613214'\n  },\n\n  table: {\n    border: colors.BORDER_LIGHT,\n    thead: {\n      color: colors.TEXT\n    },\n    striped: colors.GRAY_1,\n    input: {\n      color: colors.TEXT\n    }\n  },\n\n  plainGrid: {\n    hoverBg: colors.GRAY_3\n  },\n\n  scrollbar: {\n    color: colors.GRAY_4\n  },\n\n  dragAndDrop: {\n    border: colors.BRAND,\n    borderStyle: '2px solid',\n    hoverBg: 'rgba(0, 122, 204, 0.1)',\n    transition: 'all 0.1s ease'\n  },\n\n  infoTip: {\n    bg: colors.SIDEBAR_BG,\n    border: colors.BORDER,\n    boxShadow: '0 4px 12px rgba(0, 0, 0, 0.5)'\n  },\n\n  statusBar: {\n    border: colors.BORDER_LIGHT,\n    color: colors.TEXT_MUTED\n  },\n\n  console: {\n    bg: colors.EDITOR_BG,\n    headerBg: colors.SIDEBAR_BG,\n    contentBg: colors.EDITOR_BG,\n    border: colors.BORDER,\n    titleColor: colors.TEXT,\n    countColor: colors.TEXT_MUTED,\n    buttonColor: colors.TEXT,\n    buttonHoverBg: 'rgba(255, 255, 255, 0.1)',\n    buttonHoverColor: colors.WHITE,\n    messageColor: colors.TEXT,\n    timestampColor: colors.TEXT_MUTED,\n    emptyColor: colors.TEXT_MUTED,\n    logHoverBg: 'rgba(255, 255, 255, 0.05)',\n    resizeHandleHover: colors.BRAND,\n    resizeHandleActive: colors.BRAND,\n    dropdownBg: colors.SIDEBAR_BG,\n    dropdownHeaderBg: colors.GRAY_3,\n    optionHoverBg: 'rgba(255, 255, 255, 0.05)',\n    optionLabelColor: colors.TEXT,\n    optionCountColor: colors.TEXT_MUTED,\n    checkboxColor: colors.BRAND,\n    scrollbarTrack: colors.SIDEBAR_BG,\n    scrollbarThumb: colors.GRAY_5,\n    scrollbarThumbHover: colors.GRAY_6\n  },\n\n  grpc: {\n    tabNav: {\n      container: {\n        bg: colors.GRAY_2\n      },\n      button: {\n        active: {\n          bg: colors.GRAY_3,\n          color: colors.WHITE\n        },\n        inactive: {\n          bg: 'transparent',\n          color: colors.TEXT_MUTED\n        }\n      }\n    },\n    importPaths: {\n      header: {\n        text: colors.TEXT_MUTED,\n        button: {\n          color: colors.TEXT_MUTED,\n          hoverColor: colors.TEXT\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: colors.RED,\n        link: {\n          color: colors.RED,\n          hoverColor: '#ff6b6b'\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: 'rgba(255, 255, 255, 0.05)',\n        text: colors.TEXT,\n        icon: colors.TEXT_MUTED,\n        checkbox: {\n          color: colors.TEXT\n        },\n        invalid: {\n          opacity: 0.6,\n          text: colors.RED\n        }\n      },\n      empty: {\n        text: colors.TEXT_MUTED\n      },\n      button: {\n        bg: colors.GRAY_4,\n        color: colors.TEXT,\n        border: colors.GRAY_4,\n        hoverBorder: colors.GRAY_5\n      }\n    },\n    protoFiles: {\n      header: {\n        text: colors.TEXT_MUTED,\n        button: {\n          color: colors.TEXT_MUTED,\n          hoverColor: colors.TEXT\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: colors.RED,\n        link: {\n          color: colors.RED,\n          hoverColor: '#ff6b6b'\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: 'rgba(255, 255, 255, 0.05)',\n        selected: {\n          bg: 'rgba(0, 122, 204, 0.2)',\n          border: colors.BRAND\n        },\n        text: colors.TEXT,\n        secondaryText: colors.TEXT_MUTED,\n        icon: colors.TEXT_MUTED,\n        invalid: {\n          opacity: 0.6,\n          text: colors.RED\n        }\n      },\n      empty: {\n        text: colors.TEXT_MUTED\n      },\n      button: {\n        bg: colors.GRAY_4,\n        color: colors.TEXT,\n        border: colors.GRAY_4,\n        hoverBorder: colors.GRAY_5\n      }\n    }\n  },\n\n  deprecationWarning: {\n    bg: 'rgba(241, 76, 76, 0.1)',\n    border: 'rgba(241, 76, 76, 0.2)',\n    icon: colors.RED,\n    text: colors.TEXT\n  },\n\n  examples: {\n    buttonBg: 'rgba(0, 122, 204, 0.15)',\n    buttonColor: colors.TEXT_LINK,\n    buttonText: colors.WHITE,\n    buttonIconColor: colors.WHITE,\n    border: colors.BORDER,\n    urlBar: {\n      border: colors.BORDER,\n      bg: colors.GRAY_2\n    },\n    table: {\n      thead: {\n        bg: colors.GRAY_2,\n        color: colors.TEXT_MUTED\n      }\n    },\n    checkbox: {\n      color: colors.WHITE\n    }\n  },\n\n  app: {\n    collection: {\n      toolbar: {\n        environmentSelector: {\n          bg: colors.EDITOR_BG,\n          border: colors.BORDER,\n          icon: colors.BRAND,\n          text: colors.TEXT,\n          caret: colors.TEXT_MUTED,\n          separator: colors.BORDER,\n          hoverBg: colors.EDITOR_BG,\n          hoverBorder: colors.GRAY_5,\n          noEnvironment: {\n            text: colors.TEXT_MUTED,\n            bg: colors.EDITOR_BG,\n            border: colors.BORDER,\n            hoverBg: colors.EDITOR_BG,\n            hoverBorder: colors.GRAY_5\n          }\n        },\n        sandboxMode: {\n          safeMode: {\n            bg: 'rgba(78, 201, 176, 0.12)',\n            color: colors.GREEN\n          },\n          developerMode: {\n            bg: 'rgba(220, 220, 170, 0.12)',\n            color: colors.YELLOW\n          }\n        }\n      }\n    }\n  }\n};\n\nexport default vscodeDarkTheme;\n"
  },
  {
    "path": "packages/bruno-app/src/themes/index.js",
    "content": "import light from './light/light';\nimport lightMonochrome from './light/light-monochrome';\nimport lightPastel from './light/light-pastel';\nimport catppuccinLatte from './light/catppuccin-latte';\nimport vscodeLight from './light/vscode';\nimport dark from './dark/dark';\nimport darkMonochrome from './dark/dark-monochrome';\nimport darkPastel from './dark/dark-pastel';\nimport catppuccinFrappe from './dark/catppuccin-frappe';\nimport catppuccinMacchiato from './dark/catppuccin-macchiato';\nimport catppuccinMocha from './dark/catppuccin-mocha';\nimport nord from './dark/nord';\nimport vscodeDark from './dark/vscode';\n\nconst themes = {\n  light,\n  dark,\n  'light-monochrome': lightMonochrome,\n  'light-pastel': lightPastel,\n  'dark-monochrome': darkMonochrome,\n  'dark-pastel': darkPastel,\n  'catppuccin-latte': catppuccinLatte,\n  'catppuccin-frappe': catppuccinFrappe,\n  'catppuccin-macchiato': catppuccinMacchiato,\n  'catppuccin-mocha': catppuccinMocha,\n  nord,\n  'vscode-light': vscodeLight,\n  'vscode-dark': vscodeDark\n};\n\n// Theme metadata for UI display\nexport const themeRegistry = {\n  'light': {\n    id: 'light',\n    name: 'Light',\n    mode: 'light'\n  },\n  'light-monochrome': {\n    id: 'light-monochrome',\n    name: 'Light Monochrome',\n    mode: 'light'\n  },\n  'light-pastel': {\n    id: 'light-pastel',\n    name: 'Light Pastel',\n    mode: 'light'\n  },\n  'catppuccin-latte': {\n    id: 'catppuccin-latte',\n    name: 'Catppuccin Latte',\n    mode: 'light'\n  },\n  'dark': {\n    id: 'dark',\n    name: 'Dark',\n    mode: 'dark'\n  },\n  'dark-monochrome': {\n    id: 'dark-monochrome',\n    name: 'Dark Monochrome',\n    mode: 'dark'\n  },\n  'dark-pastel': {\n    id: 'dark-pastel',\n    name: 'Dark Pastel',\n    mode: 'dark'\n  },\n  'catppuccin-frappe': {\n    id: 'catppuccin-frappe',\n    name: 'Catppuccin Frappé',\n    mode: 'dark'\n  },\n  'catppuccin-macchiato': {\n    id: 'catppuccin-macchiato',\n    name: 'Catppuccin Macchiato',\n    mode: 'dark'\n  },\n  'catppuccin-mocha': {\n    id: 'catppuccin-mocha',\n    name: 'Catppuccin Mocha',\n    mode: 'dark'\n  },\n  'nord': {\n    id: 'nord',\n    name: 'Nord',\n    mode: 'dark'\n  },\n  'vscode-light': {\n    id: 'vscode-light',\n    name: 'VS Code Light',\n    mode: 'light'\n  },\n  'vscode-dark': {\n    id: 'vscode-dark',\n    name: 'VS Code Dark',\n    mode: 'dark'\n  }\n};\n\nexport const getLightThemes = () => Object.values(themeRegistry).filter((t) => t.mode === 'light');\nexport const getDarkThemes = () => Object.values(themeRegistry).filter((t) => t.mode === 'dark');\n\nexport default themes;\n"
  },
  {
    "path": "packages/bruno-app/src/themes/light/catppuccin-latte.js",
    "content": "// Catppuccin Latte - Light Theme\n// Based on https://catppuccin.com/palette/\n\nimport { rgba } from 'polished';\n\nconst colors = {\n  // Catppuccin Latte Palette\n  ROSEWATER: '#dc8a78',\n  FLAMINGO: '#dd7878',\n  PINK: '#ea76cb',\n  MAUVE: '#8839ef',\n  RED: '#d20f39',\n  MAROON: '#e64553',\n  PEACH: '#fe640b',\n  YELLOW: '#df8e1d',\n  GREEN: '#40a02b',\n  TEAL: '#179299',\n  SKY: '#04a5e5',\n  SAPPHIRE: '#209fb5',\n  BLUE: '#1e66f5',\n  LAVENDER: '#7287fd',\n\n  TEXT: '#4c4f69',\n  SUBTEXT1: '#5c5f77',\n  SUBTEXT0: '#6c6f85',\n  OVERLAY2: '#7c7f93',\n  OVERLAY1: '#8c8fa1',\n  OVERLAY0: '#9ca0b0',\n  SURFACE2: '#acb0be',\n  SURFACE1: '#bcc0cc',\n  SURFACE0: '#ccd0da',\n  BASE: '#eff1f5',\n  MANTLE: '#e6e9ef',\n  CRUST: '#dce0e8',\n\n  WHITE: '#fff',\n  BLACK: '#000',\n\n  CODEMIRROR_TOKENS: {\n    DEFINITION: '#40a02b',\n    PROPERTY: '#1e66f5',\n    STRING: '#df8e1d',\n    NUMBER: '#fe640b',\n    ATOM: '#ea76cb',\n    VARIABLE: '#209fb5',\n    KEYWORD: '#d20f39',\n    COMMENT: '#6c6f85',\n    OPERATOR: '#179299',\n    TAG: '#1e66f5',\n    TAG_BRACKET: '#6c6f85'\n  }\n};\n\nexport const palette = {};\n\npalette.intent = {\n  INFO: colors.BLUE,\n  SUCCESS: colors.GREEN,\n  WARNING: colors.PEACH,\n  DANGER: colors.RED\n};\n\nconst catppuccinLatteTheme = {\n  mode: 'light',\n  brand: colors.MAUVE,\n  text: colors.TEXT,\n  textLink: colors.BLUE,\n  draftColor: '#cc7b1b',\n  bg: colors.BASE,\n\n  primary: {\n    solid: colors.MAUVE,\n    text: colors.MAUVE,\n    strong: colors.MAUVE,\n    subtle: colors.MAUVE\n  },\n\n  accents: {\n    primary: colors.MAUVE\n  },\n\n  background: {\n    base: colors.BASE,\n    mantle: colors.MANTLE,\n    crust: colors.CRUST,\n    surface0: colors.SURFACE0,\n    surface1: colors.SURFACE1,\n    surface2: colors.SURFACE2\n  },\n\n  status: {\n    info: {\n      background: rgba(palette.intent.INFO, 0.15),\n      text: palette.intent.INFO,\n      border: palette.intent.INFO\n    },\n    success: {\n      background: rgba(palette.intent.SUCCESS, 0.15),\n      text: palette.intent.SUCCESS,\n      border: palette.intent.SUCCESS\n    },\n    warning: {\n      background: rgba(palette.intent.WARNING, 0.15),\n      text: palette.intent.WARNING,\n      border: palette.intent.WARNING\n    },\n    danger: {\n      background: rgba(palette.intent.DANGER, 0.15),\n      text: palette.intent.DANGER,\n      border: palette.intent.DANGER\n    }\n  },\n\n  overlay: {\n    overlay2: colors.OVERLAY2,\n    overlay1: colors.OVERLAY1,\n    overlay0: colors.OVERLAY0\n  },\n\n  font: {\n    size: {\n      xs: '0.6875rem', // 11px\n      sm: '0.75rem', // 12px\n      base: '0.8125rem', // 13px\n      md: '0.875rem', // 14px\n      lg: '1rem', // 16px\n      xl: '1.125rem' // 18px\n    }\n  },\n\n  shadow: {\n    sm: '0 1px 3px rgba(76, 79, 105, 0.12), 0 0 0 1px rgba(76, 79, 105, 0.05)',\n    md: '0 2px 8px rgba(76, 79, 105, 0.14), 0 0 0 1px rgba(76, 79, 105, 0.06)',\n    lg: '0 2px 12px rgba(76, 79, 105, 0.15), 0 0 0 1px rgba(76, 79, 105, 0.05)'\n  },\n\n  border: {\n    radius: {\n      sm: '4px',\n      base: '6px',\n      md: '8px',\n      lg: '10px',\n      xl: '12px'\n    },\n    border2: colors.SURFACE2,\n    border1: colors.SURFACE1,\n    border0: colors.SURFACE0\n  },\n\n  colors: {\n    text: {\n      white: colors.WHITE,\n      green: colors.GREEN,\n      danger: colors.RED,\n      warning: colors.PEACH,\n      muted: colors.SUBTEXT0,\n      purple: colors.MAUVE,\n      yellow: colors.YELLOW,\n      subtext2: colors.TEXT,\n      subtext1: colors.SUBTEXT1,\n      subtext0: colors.SUBTEXT0\n    },\n    bg: {\n      danger: colors.MAROON\n    },\n    accent: colors.MAUVE\n  },\n\n  input: {\n    bg: rgba(colors.SURFACE0, 0.2),\n    border: colors.SURFACE1,\n    focusBorder: colors.LAVENDER,\n    placeholder: {\n      color: colors.OVERLAY0,\n      opacity: 0.8\n    }\n  },\n\n  sidebar: {\n    color: colors.TEXT,\n    muted: colors.SUBTEXT0,\n    bg: colors.MANTLE,\n    dragbar: {\n      border: colors.SURFACE0,\n      activeBorder: colors.SURFACE2\n    },\n\n    collection: {\n      item: {\n        bg: rgba(colors.SURFACE0, 0.5),\n        hoverBg: rgba(colors.SURFACE0, 0.7),\n        focusBorder: colors.LAVENDER,\n        indentBorder: colors.SURFACE0,\n        active: {\n          indentBorder: colors.SURFACE0\n        },\n        example: {\n          iconColor: colors.OVERLAY1\n        }\n      }\n    },\n\n    dropdownIcon: {\n      color: colors.SUBTEXT1\n    }\n  },\n\n  dropdown: {\n    color: colors.TEXT,\n    iconColor: colors.SUBTEXT1,\n    bg: colors.BASE,\n    hoverBg: rgba(colors.SURFACE0, 0.5),\n    shadow: 'rgba(76, 79, 105, 0.25) 0px 6px 12px -2px, rgba(76, 79, 105, 0.3) 0px 3px 7px -3px',\n    border: 'none',\n    separator: colors.SURFACE1,\n    selectedColor: colors.MAUVE,\n    mutedText: colors.SUBTEXT0\n  },\n\n  workspace: {\n    accent: colors.MAUVE,\n    border: colors.SURFACE1,\n    button: {\n      bg: colors.SURFACE0\n    }\n  },\n\n  request: {\n    methods: {\n      get: colors.GREEN,\n      post: colors.BLUE,\n      put: colors.YELLOW,\n      delete: colors.RED,\n      patch: colors.PEACH,\n      options: colors.TEAL,\n      head: colors.SAPPHIRE\n    },\n\n    grpc: colors.SKY,\n    ws: colors.MAUVE,\n    gql: colors.PINK\n  },\n\n  requestTabPanel: {\n    url: {\n      bg: colors.BASE,\n      icon: colors.SUBTEXT1,\n      iconDanger: colors.RED,\n      border: `solid 1px ${colors.SURFACE1}`\n    },\n    dragbar: {\n      border: colors.SURFACE1,\n      activeBorder: colors.OVERLAY0\n    },\n    responseStatus: colors.SUBTEXT1,\n    responseOk: colors.GREEN,\n    responseError: colors.RED,\n    responsePending: colors.BLUE,\n    responseOverlayBg: 'rgba(239, 241, 245, 0.6)',\n    card: {\n      bg: colors.BASE,\n      border: colors.MANTLE,\n      hr: colors.SURFACE0\n    },\n    graphqlDocsExplorer: {\n      bg: colors.BASE,\n      color: colors.TEXT\n    }\n  },\n\n  notifications: {\n    bg: colors.BASE,\n    list: {\n      bg: colors.MANTLE,\n      borderRight: colors.SURFACE1,\n      borderBottom: colors.SURFACE1,\n      hoverBg: colors.SURFACE0,\n      active: {\n        border: colors.BLUE,\n        bg: colors.SURFACE1,\n        hoverBg: colors.SURFACE1\n      }\n    }\n  },\n\n  modal: {\n    title: {\n      color: colors.TEXT,\n      bg: colors.MANTLE\n    },\n    body: {\n      color: colors.TEXT,\n      bg: colors.BASE\n    },\n    input: {\n      bg: colors.SURFACE0,\n      border: colors.SURFACE1,\n      focusBorder: colors.LAVENDER\n    },\n    backdrop: {\n      opacity: 0.4\n    }\n  },\n\n  button: {\n    secondary: {\n      color: colors.TEXT,\n      bg: colors.SURFACE0,\n      border: colors.SURFACE1,\n      hoverBorder: colors.OVERLAY0\n    },\n    close: {\n      color: colors.TEXT,\n      bg: 'transparent',\n      border: 'transparent',\n      hoverBorder: ''\n    },\n    disabled: {\n      color: colors.OVERLAY0,\n      bg: colors.SURFACE1,\n      border: colors.SURFACE1\n    },\n    danger: {\n      color: colors.WHITE,\n      bg: colors.RED,\n      border: colors.RED\n    }\n  },\n  button2: {\n    color: {\n      primary: {\n        bg: colors.MAUVE,\n        text: colors.BASE,\n        border: colors.MAUVE\n      },\n      light: {\n        bg: rgba(colors.MAUVE, 0.08),\n        text: colors.MAUVE,\n        border: rgba(colors.MAUVE, 0.06)\n      },\n      secondary: {\n        bg: colors.SURFACE1,\n        text: colors.TEXT,\n        border: colors.SURFACE2\n      },\n      success: {\n        bg: colors.GREEN,\n        text: colors.BASE,\n        border: colors.GREEN\n      },\n      warning: {\n        bg: colors.PEACH,\n        text: colors.BASE,\n        border: colors.PEACH\n      },\n      danger: {\n        bg: colors.RED,\n        text: colors.BASE,\n        border: colors.RED\n      }\n    }\n  },\n  tabs: {\n    marginRight: '1.2rem',\n    active: {\n      fontWeight: 400,\n      color: colors.TEXT,\n      border: colors.MAUVE\n    },\n    secondary: {\n      active: {\n        bg: colors.SURFACE0,\n        color: colors.TEXT\n      },\n      inactive: {\n        bg: colors.SURFACE0,\n        color: colors.SUBTEXT0\n      }\n    }\n  },\n\n  requestTabs: {\n    color: colors.TEXT,\n    bg: '#E4E7EC',\n    bottomBorder: colors.SURFACE1,\n    icon: {\n      color: colors.OVERLAY0,\n      hoverColor: colors.SUBTEXT1,\n      hoverBg: colors.SURFACE1\n    },\n    example: {\n      iconColor: colors.OVERLAY1\n    }\n  },\n\n  codemirror: {\n    bg: colors.BASE,\n    border: colors.BASE,\n    placeholder: {\n      color: colors.OVERLAY0,\n      opacity: 0.75\n    },\n    gutter: {\n      bg: colors.BASE\n    },\n    variable: {\n      valid: colors.GREEN,\n      invalid: colors.RED,\n      prompt: colors.BLUE\n    },\n    tokens: {\n      definition: colors.CODEMIRROR_TOKENS.DEFINITION,\n      property: colors.CODEMIRROR_TOKENS.PROPERTY,\n      string: colors.CODEMIRROR_TOKENS.STRING,\n      number: colors.CODEMIRROR_TOKENS.NUMBER,\n      atom: colors.CODEMIRROR_TOKENS.ATOM,\n      variable: colors.CODEMIRROR_TOKENS.VARIABLE,\n      keyword: colors.CODEMIRROR_TOKENS.KEYWORD,\n      comment: colors.CODEMIRROR_TOKENS.COMMENT,\n      operator: colors.CODEMIRROR_TOKENS.OPERATOR,\n      tag: colors.CODEMIRROR_TOKENS.TAG,\n      tagBracket: colors.CODEMIRROR_TOKENS.TAG_BRACKET\n    },\n    searchLineHighlightCurrent: 'rgba(124, 127, 147, 0.10)',\n    searchMatch: colors.YELLOW,\n    searchMatchActive: colors.PEACH\n  },\n\n  table: {\n    border: colors.SURFACE1,\n    thead: {\n      color: colors.SUBTEXT1\n    },\n    striped: colors.MANTLE,\n    input: {\n      color: colors.TEXT\n    }\n  },\n\n  plainGrid: {\n    hoverBg: colors.SURFACE0\n  },\n\n  scrollbar: {\n    color: colors.OVERLAY0\n  },\n\n  dragAndDrop: {\n    border: colors.LAVENDER,\n    borderStyle: '2px solid',\n    hoverBg: 'rgba(114, 135, 253, 0.05)',\n    transition: 'all 0.1s ease'\n  },\n\n  infoTip: {\n    bg: colors.BASE,\n    border: colors.SURFACE1,\n    boxShadow: '0 4px 12px rgba(76, 79, 105, 0.15)'\n  },\n\n  statusBar: {\n    border: colors.SURFACE1,\n    color: colors.SUBTEXT0\n  },\n\n  console: {\n    bg: colors.BASE,\n    headerBg: colors.MANTLE,\n    contentBg: colors.BASE,\n    border: colors.SURFACE1,\n    titleColor: colors.TEXT,\n    countColor: colors.SUBTEXT0,\n    buttonColor: colors.SUBTEXT1,\n    buttonHoverBg: colors.SURFACE0,\n    buttonHoverColor: colors.TEXT,\n    messageColor: colors.TEXT,\n    timestampColor: colors.SUBTEXT0,\n    emptyColor: colors.SUBTEXT0,\n    logHoverBg: 'rgba(76, 79, 105, 0.03)',\n    resizeHandleHover: colors.BLUE,\n    resizeHandleActive: colors.BLUE,\n    dropdownBg: colors.BASE,\n    dropdownHeaderBg: colors.MANTLE,\n    optionHoverBg: colors.SURFACE0,\n    optionLabelColor: colors.TEXT,\n    optionCountColor: colors.SUBTEXT0,\n    checkboxColor: colors.MAUVE,\n    scrollbarTrack: colors.MANTLE,\n    scrollbarThumb: colors.SURFACE2,\n    scrollbarThumbHover: colors.OVERLAY0\n  },\n\n  grpc: {\n    tabNav: {\n      container: {\n        bg: colors.MANTLE\n      },\n      button: {\n        active: {\n          bg: colors.BASE,\n          color: colors.TEXT\n        },\n        inactive: {\n          bg: 'transparent',\n          color: colors.SUBTEXT0\n        }\n      }\n    },\n    importPaths: {\n      header: {\n        text: colors.SUBTEXT0,\n        button: {\n          color: colors.SUBTEXT0,\n          hoverColor: colors.TEXT\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: colors.RED,\n        link: {\n          color: colors.RED,\n          hoverColor: colors.MAROON\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: 'rgba(76, 79, 105, 0.05)',\n        text: colors.TEXT,\n        icon: colors.SUBTEXT0,\n        checkbox: {\n          color: colors.TEXT\n        },\n        invalid: {\n          opacity: 0.6,\n          text: colors.RED\n        }\n      },\n      empty: {\n        text: colors.SUBTEXT0\n      },\n      button: {\n        bg: colors.SURFACE0,\n        color: colors.TEXT,\n        border: colors.SURFACE1,\n        hoverBorder: colors.OVERLAY0\n      }\n    },\n    protoFiles: {\n      header: {\n        text: colors.SUBTEXT0,\n        button: {\n          color: colors.SUBTEXT0,\n          hoverColor: colors.TEXT\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: colors.RED,\n        link: {\n          color: colors.RED,\n          hoverColor: colors.MAROON\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: 'rgba(76, 79, 105, 0.05)',\n        selected: {\n          bg: 'rgba(136, 57, 239, 0.2)',\n          border: colors.MAUVE\n        },\n        text: colors.TEXT,\n        secondaryText: colors.SUBTEXT0,\n        icon: colors.SUBTEXT0,\n        invalid: {\n          opacity: 0.6,\n          text: colors.RED\n        }\n      },\n      empty: {\n        text: colors.SUBTEXT0\n      },\n      button: {\n        bg: colors.SURFACE0,\n        color: colors.TEXT,\n        border: colors.SURFACE1,\n        hoverBorder: colors.OVERLAY0\n      }\n    }\n  },\n  deprecationWarning: {\n    bg: 'rgba(210, 15, 57, 0.1)',\n    border: 'rgba(210, 15, 57, 0.1)',\n    icon: colors.RED,\n    text: colors.TEXT\n  },\n\n  examples: {\n    buttonBg: 'rgba(136, 57, 239, 0.1)',\n    buttonColor: colors.MAUVE,\n    buttonText: colors.BASE,\n    buttonIconColor: colors.TEXT,\n    border: colors.SURFACE1,\n    urlBar: {\n      border: colors.SURFACE1,\n      bg: colors.MANTLE\n    },\n    table: {\n      thead: {\n        bg: colors.MANTLE,\n        color: colors.SUBTEXT1\n      }\n    },\n    checkbox: {\n      color: colors.BASE\n    }\n  },\n\n  app: {\n    collection: {\n      toolbar: {\n        environmentSelector: {\n          bg: colors.BASE,\n          border: colors.SURFACE1,\n          icon: colors.MAUVE,\n          text: colors.TEXT,\n          caret: colors.OVERLAY0,\n          separator: colors.SURFACE1,\n          hoverBg: colors.BASE,\n          hoverBorder: colors.SURFACE2,\n\n          noEnvironment: {\n            text: colors.SUBTEXT0,\n            bg: colors.BASE,\n            border: colors.SURFACE1,\n            hoverBg: colors.BASE,\n            hoverBorder: colors.SURFACE2\n          }\n        },\n        sandboxMode: {\n          safeMode: {\n            bg: 'rgba(64, 160, 43, 0.12)',\n            color: colors.GREEN\n          },\n          developerMode: {\n            bg: 'rgba(223, 142, 29, 0.15)',\n            color: colors.YELLOW\n          }\n        }\n      }\n    }\n  }\n};\n\nexport default catppuccinLatteTheme;\n"
  },
  {
    "path": "packages/bruno-app/src/themes/light/light-monochrome.js",
    "content": "import { rgba } from 'polished';\n\nconst colors = {\n  BRAND: '#525252',\n  TEXT: 'rgb(52, 52, 52)',\n  TEXT_MUTED: '#737373',\n  TEXT_LINK: '#404040',\n  BACKGROUND: '#fff',\n\n  WHITE: '#fff',\n  BLACK: '#000',\n  SLATE_BLACK: '#343434',\n  GREEN: '#525252',\n  YELLOW: '#525252',\n\n  GRAY_1: '#f8f8f8',\n  GRAY_2: '#f3f3f3',\n  GRAY_3: '#eaeaea',\n  GRAY_4: '#e5e5e5',\n  GRAY_5: '#cbcbcb',\n  GRAY_6: '#b0b0b0',\n  GRAY_7: '#666666',\n  GRAY_8: '#444444',\n  GRAY_9: '#3D3D3D',\n  GRAY_10: '#252526',\n\n  CODEMIRROR_TOKENS: {\n    DEFINITION: '#525252',\n    PROPERTY: '#666666',\n    STRING: '#737373',\n    NUMBER: '#525252',\n    ATOM: '#666666',\n    VARIABLE: '#525252',\n    KEYWORD: '#404040',\n    COMMENT: '#a3a3a3',\n    OPERATOR: '#737373',\n    TAG: '#404040',\n    TAG_BRACKET: '#a3a3a3'\n  }\n};\n\nexport const palette = {};\n\npalette.intent = {\n  INFO: '#737373',\n  SUCCESS: '#525252',\n  WARNING: '#666666',\n  DANGER: '#404040'\n};\n\nconst lightMonochromeTheme = {\n  mode: 'light',\n  brand: colors.BRAND,\n  text: colors.TEXT,\n  textLink: colors.TEXT_LINK,\n  draftColor: '#8a8a8a',\n  bg: colors.BACKGROUND,\n\n  primary: {\n    solid: colors.BRAND,\n    text: colors.BRAND,\n    strong: colors.BRAND,\n    subtle: colors.BRAND\n  },\n\n  accents: {\n    primary: colors.BRAND\n  },\n\n  background: {\n    base: colors.BACKGROUND,\n    mantle: colors.GRAY_1,\n    crust: colors.GRAY_2,\n    surface0: colors.GRAY_3,\n    surface1: colors.GRAY_4,\n    surface2: colors.GRAY_5\n  },\n\n  status: {\n    info: {\n      background: rgba(palette.intent.INFO, 0.15),\n      text: palette.intent.INFO,\n      border: palette.intent.INFO\n    },\n    success: {\n      background: rgba(palette.intent.SUCCESS, 0.15),\n      text: palette.intent.SUCCESS,\n      border: palette.intent.SUCCESS\n    },\n    warning: {\n      background: rgba(palette.intent.WARNING, 0.15),\n      text: palette.intent.WARNING,\n      border: palette.intent.WARNING\n    },\n    danger: {\n      background: rgba(palette.intent.DANGER, 0.15),\n      text: palette.intent.DANGER,\n      border: palette.intent.DANGER\n    }\n  },\n\n  overlay: {\n    overlay2: colors.GRAY_6,\n    overlay1: '#c0c0c0',\n    overlay0: '#d0d0d0'\n  },\n\n  font: {\n    size: {\n      xs: '0.6875rem',\n      sm: '0.75rem',\n      base: '0.8125rem',\n      md: '0.875rem',\n      lg: '1rem',\n      xl: '1.125rem'\n    }\n  },\n\n  shadow: {\n    sm: '0 1px 3px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.05)',\n    md: '0 2px 8px rgba(0, 0, 0, 0.14), 0 0 0 1px rgba(0, 0, 0, 0.06)',\n    lg: '0 2px 12px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05)'\n  },\n\n  border: {\n    radius: {\n      sm: '4px',\n      base: '6px',\n      md: '8px',\n      lg: '10px',\n      xl: '12px'\n    },\n    border2: colors.GRAY_5,\n    border1: colors.GRAY_4,\n    border0: colors.GRAY_3\n  },\n\n  colors: {\n    text: {\n      white: '#fff',\n      green: '#525252',\n      danger: '#525252',\n      warning: '#525252',\n      muted: '#838383',\n      purple: '#525252',\n      yellow: colors.YELLOW,\n      subtext2: colors.GRAY_7,\n      subtext1: '#838383',\n      subtext0: '#9B9B9B'\n    },\n    bg: {\n      danger: '#525252'\n    },\n    accent: '#525252'\n  },\n\n  input: {\n    bg: 'white',\n    border: '#ccc',\n    focusBorder: '#8b8b8b',\n    placeholder: {\n      color: '#a2a2a2',\n      opacity: 0.8\n    }\n  },\n\n  sidebar: {\n    color: 'rgb(52, 52, 52)',\n    muted: '#4b5563',\n    bg: colors.GRAY_1,\n    dragbar: {\n      border: colors.GRAY_4,\n      activeBorder: colors.GRAY_5\n    },\n\n    collection: {\n      item: {\n        bg: colors.GRAY_3,\n        hoverBg: colors.GRAY_3,\n        focusBorder: colors.GRAY_5,\n        indentBorder: colors.GRAY_4,\n        active: {\n          indentBorder: colors.GRAY_4\n        },\n        example: {\n          iconColor: colors.GRAY_7\n        }\n      }\n    },\n\n    dropdownIcon: {\n      color: 'rgb(110 110 110)'\n    }\n  },\n\n  dropdown: {\n    color: 'rgb(48 48 48)',\n    iconColor: 'rgb(75, 85, 99)',\n    bg: '#fff',\n    hoverBg: '#e9ecef',\n    shadow: 'rgb(50 50 93 / 25%) 0px 6px 12px -2px, rgb(0 0 0 / 30%) 0px 3px 7px -3px',\n    border: 'none',\n    separator: '#e7e7e7',\n    selectedColor: '#525252',\n    mutedText: '#9B9B9B'\n  },\n\n  workspace: {\n    accent: '#525252',\n    border: '#e7e7e7',\n    button: {\n      bg: colors.GRAY_3\n    }\n  },\n\n  request: {\n    methods: {\n      get: '#525252',\n      post: '#525252',\n      put: '#737373',\n      delete: '#404040',\n      patch: '#737373',\n      options: '#8a8a8a',\n      head: '#6b6b6b'\n    },\n\n    grpc: '#525252',\n    ws: '#737373',\n    gql: '#404040'\n  },\n\n  requestTabPanel: {\n    url: {\n      bg: colors.WHITE,\n      icon: '#515151',\n      iconDanger: '#404040',\n      border: `solid 1px ${colors.GRAY_4}`\n    },\n    dragbar: {\n      border: '#efefef',\n      activeBorder: 'rgb(200, 200, 200)'\n    },\n    responseStatus: 'rgb(117 117 117)',\n    responseOk: '#525252',\n    responseError: '#404040',\n    responsePending: '#525252',\n    responseOverlayBg: 'rgba(255, 255, 255, 0.6)',\n    card: {\n      bg: '#fff',\n      border: '#f4f4f4',\n      hr: '#f4f4f4'\n    },\n    graphqlDocsExplorer: {\n      bg: '#fff',\n      color: 'rgb(52, 52, 52)'\n    }\n  },\n\n  notifications: {\n    bg: 'white',\n    list: {\n      bg: '#eaeaea',\n      borderRight: 'transparent',\n      borderBottom: '#d3d3d3',\n      hoverBg: '#e4e4e4',\n      active: {\n        border: '#525252',\n        bg: '#dcdcdc',\n        hoverBg: '#dcdcdc'\n      }\n    }\n  },\n\n  modal: {\n    title: {\n      color: 'rgb(86 86 86)',\n      bg: '#f1f1f1'\n    },\n    body: {\n      color: 'rgb(52, 52, 52)',\n      bg: 'white'\n    },\n    input: {\n      bg: 'white',\n      border: '#ccc',\n      focusBorder: '#8b8b8b'\n    },\n    backdrop: {\n      opacity: 0.4\n    }\n  },\n\n  button: {\n    secondary: {\n      color: '#212529',\n      bg: '#e2e6ea',\n      border: '#dae0e5',\n      hoverBorder: '#696969'\n    },\n    close: {\n      color: '#212529',\n      bg: 'white',\n      border: 'white',\n      hoverBorder: ''\n    },\n    disabled: {\n      color: '#9f9f9f',\n      bg: '#efefef',\n      border: 'rgb(234, 234, 234)'\n    },\n    danger: {\n      color: '#fff',\n      bg: '#525252',\n      border: '#525252'\n    }\n  },\n  button2: {\n    color: {\n      primary: {\n        bg: colors.BRAND,\n        text: '#fff',\n        border: colors.BRAND\n      },\n      light: {\n        bg: rgba(colors.TEXT, 0.08),\n        text: colors.TEXT,\n        border: rgba(colors.TEXT, 0.06)\n      },\n      secondary: {\n        bg: '#e5e7eb',\n        text: colors.TEXT,\n        border: '#d1d5db'\n      },\n      success: {\n        bg: '#525252',\n        text: '#fff',\n        border: '#525252'\n      },\n      warning: {\n        bg: '#737373',\n        text: '#fff',\n        border: '#737373'\n      },\n      danger: {\n        bg: '#404040',\n        text: '#fff',\n        border: '#404040'\n      }\n    }\n  },\n  tabs: {\n    marginRight: '1.2rem',\n    active: {\n      fontWeight: 400,\n      color: colors.SLATE_BLACK,\n      border: '#525252'\n    },\n    secondary: {\n      active: {\n        bg: '#ECECEE',\n        color: '#343434'\n      },\n      inactive: {\n        bg: '#ECECEE',\n        color: '#989898'\n      }\n    }\n  },\n\n  requestTabs: {\n    color: 'rgb(52, 52, 52)',\n    bg: '#f6f6f6',\n    bottomBorder: '#efefef',\n    icon: {\n      color: '#9f9f9f',\n      hoverColor: 'rgb(76 76 76)',\n      hoverBg: 'rgb(234, 234, 234)'\n    },\n    example: {\n      iconColor: colors.GRAY_7\n    }\n  },\n\n  codemirror: {\n    bg: colors.WHITE,\n    border: colors.WHITE,\n    placeholder: {\n      color: '#a2a2a2',\n      opacity: 0.75\n    },\n    gutter: {\n      bg: colors.WHITE\n    },\n    variable: {\n      valid: '#525252',\n      invalid: '#404040',\n      prompt: '#525252'\n    },\n    tokens: {\n      definition: colors.CODEMIRROR_TOKENS.DEFINITION,\n      property: colors.CODEMIRROR_TOKENS.PROPERTY,\n      string: colors.CODEMIRROR_TOKENS.STRING,\n      number: colors.CODEMIRROR_TOKENS.NUMBER,\n      atom: colors.CODEMIRROR_TOKENS.ATOM,\n      variable: colors.CODEMIRROR_TOKENS.VARIABLE,\n      keyword: colors.CODEMIRROR_TOKENS.KEYWORD,\n      comment: colors.CODEMIRROR_TOKENS.COMMENT,\n      operator: colors.CODEMIRROR_TOKENS.OPERATOR,\n      tag: colors.CODEMIRROR_TOKENS.TAG,\n      tagBracket: colors.CODEMIRROR_TOKENS.TAG_BRACKET\n    },\n    searchLineHighlightCurrent: 'rgba(120,120,120,0.10)',\n    searchMatch: '#737373',\n    searchMatchActive: '#525252'\n  },\n\n  table: {\n    border: '#efefef',\n    thead: {\n      color: '#616161'\n    },\n    striped: '#f3f3f3',\n    input: {\n      color: '#000000'\n    }\n  },\n\n  plainGrid: {\n    hoverBg: '#f4f4f4'\n  },\n\n  scrollbar: {\n    color: 'rgb(152 151 149)'\n  },\n\n  dragAndDrop: {\n    border: '#8b8b8b',\n    borderStyle: '2px solid',\n    hoverBg: 'rgba(139, 139, 139, 0.05)',\n    transition: 'all 0.1s ease'\n  },\n\n  infoTip: {\n    bg: 'white',\n    border: '#e0e0e0',\n    boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)'\n  },\n\n  statusBar: {\n    border: '#E9E9E9',\n    color: 'rgb(100, 100, 100)'\n  },\n  console: {\n    bg: '#f8f9fa',\n    headerBg: '#f8f9fa',\n    contentBg: '#ffffff',\n    border: '#dee2e6',\n    titleColor: '#212529',\n    countColor: '#6c757d',\n    buttonColor: '#495057',\n    buttonHoverBg: '#e9ecef',\n    buttonHoverColor: '#212529',\n    messageColor: '#212529',\n    timestampColor: '#6c757d',\n    emptyColor: '#6c757d',\n    logHoverBg: 'rgba(0, 0, 0, 0.03)',\n    resizeHandleHover: '#525252',\n    resizeHandleActive: '#525252',\n    dropdownBg: '#ffffff',\n    dropdownHeaderBg: '#f8f9fa',\n    optionHoverBg: '#f8f9fa',\n    optionLabelColor: '#212529',\n    optionCountColor: '#6c757d',\n    checkboxColor: colors.BRAND,\n    scrollbarTrack: '#f8f9fa',\n    scrollbarThumb: '#ced4da',\n    scrollbarThumbHover: '#adb5bd'\n  },\n\n  grpc: {\n    tabNav: {\n      container: {\n        bg: '#f5f5f5'\n      },\n      button: {\n        active: {\n          bg: '#ffffff',\n          color: '#000000'\n        },\n        inactive: {\n          bg: 'transparent',\n          color: '#525252'\n        }\n      }\n    },\n    importPaths: {\n      header: {\n        text: '#838383',\n        button: {\n          color: '#838383',\n          hoverColor: '#343434'\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: '#404040',\n        link: {\n          color: '#404040',\n          hoverColor: '#525252'\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: 'rgba(0, 0, 0, 0.05)',\n        text: '#343434',\n        icon: '#838383',\n        checkbox: {\n          color: '#343434'\n        },\n        invalid: {\n          opacity: 0.6,\n          text: '#404040'\n        }\n      },\n      empty: {\n        text: '#838383'\n      },\n      button: {\n        bg: '#e2e6ea',\n        color: '#212529',\n        border: '#dae0e5',\n        hoverBorder: '#696969'\n      }\n    },\n    protoFiles: {\n      header: {\n        text: '#838383',\n        button: {\n          color: '#838383',\n          hoverColor: '#343434'\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: '#404040',\n        link: {\n          color: '#404040',\n          hoverColor: '#525252'\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: 'rgba(0, 0, 0, 0.05)',\n        selected: {\n          bg: 'rgba(82, 82, 82, 0.2)',\n          border: '#525252'\n        },\n        text: '#343434',\n        secondaryText: '#838383',\n        icon: '#838383',\n        invalid: {\n          opacity: 0.6,\n          text: '#404040'\n        }\n      },\n      empty: {\n        text: '#838383'\n      },\n      button: {\n        bg: '#e2e6ea',\n        color: '#212529',\n        border: '#dae0e5',\n        hoverBorder: '#696969'\n      }\n    }\n  },\n  deprecationWarning: {\n    bg: 'rgba(64, 64, 64, 0.1)',\n    border: 'rgba(64, 64, 64, 0.1)',\n    icon: '#404040',\n    text: '#343434'\n  },\n\n  examples: {\n    buttonBg: '#5252521A',\n    buttonColor: '#525252',\n    buttonText: '#fff',\n    buttonIconColor: '#000',\n    border: '#efefef',\n    urlBar: {\n      border: '#efefef',\n      bg: '#F5F5F5'\n    },\n    table: {\n      thead: {\n        bg: '#f8f9fa',\n        color: '#212529'\n      }\n    },\n    checkbox: {\n      color: '#fff'\n    }\n  },\n\n  app: {\n    collection: {\n      toolbar: {\n        environmentSelector: {\n          bg: colors.WHITE,\n          border: colors.GRAY_4,\n          icon: colors.BRAND,\n          text: colors.TEXT,\n          caret: colors.GRAY_6,\n          separator: colors.GRAY_4,\n          hoverBg: colors.WHITE,\n          hoverBorder: colors.GRAY_5,\n\n          noEnvironment: {\n            text: colors.TEXT_MUTED,\n            bg: colors.WHITE,\n            border: colors.GRAY_5,\n            hoverBg: colors.WHITE,\n            hoverBorder: colors.GRAY_6\n          }\n        },\n        sandboxMode: {\n          safeMode: {\n            bg: 'rgba(82, 82, 82, 0.12)',\n            color: '#525252'\n          },\n          developerMode: {\n            bg: 'rgba(115, 115, 115, 0.15)',\n            color: '#737373'\n          }\n        }\n      }\n    }\n  }\n};\n\nexport default lightMonochromeTheme;\n"
  },
  {
    "path": "packages/bruno-app/src/themes/light/light-pastel.js",
    "content": "/**\n * Light Pastel Theme - \"Serenity Bloom\"\n * Soft, deep pastels with warm undertones for a calm, refined look.\n * Gentle contrast that stays readable and inviting.\n */\nimport { rgba } from 'polished';\n\nconst colors = {\n  // Primary palette - soft yet deep\n  BRAND: '#d16c6c', // Dusty coral - warm but calm\n  TEXT: '#2b2f3a', // Deep charcoal with a violet hint\n  TEXT_MUTED: '#6e7380', // Muted slate\n  TEXT_LINK: '#3f7cac', // Steel blue\n  BACKGROUND: '#fdfaf7', // Warm ivory\n\n  // Core colors\n  WHITE: '#ffffff',\n  BLACK: '#17181f',\n  SLATE: '#1f2530',\n\n  // Soft pastels with depth\n  GREEN: '#3c9d7c', // Soft emerald\n  YELLOW: '#d2a13f', // Golden ochre\n  RED: '#c75b63', // Rosewood red\n  PURPLE: '#7a70b5', // Dusty periwinkle\n  BLUE: '#3a7cc4', // Muted azure\n  PINK: '#c57a92', // Dusty rose\n  ORANGE: '#dd8a52', // Muted amber\n  TEAL: '#3a8f98', // Steel teal\n\n  // Warm grayscale with soft contrast\n  GRAY_1: '#f7f4ef', // Near white with warmth\n  GRAY_2: '#f0ebe4', // Light greige\n  GRAY_3: '#e2dbd1', // Soft stone\n  GRAY_4: '#d3cabc', // Gentle taupe\n  GRAY_5: '#b8b0a3', // Mid greige\n  GRAY_6: '#8d8577', // Warm smoke\n  GRAY_7: '#6f675b', // Earthy brown-gray\n  GRAY_8: '#574f45', // Deep stone\n  GRAY_9: '#3f382f', // Charcoal brown\n  GRAY_10: '#29231d', // Deepest warm charcoal\n\n  // CodeMirror syntax - soft contrast with clarity\n  CODEMIRROR_TOKENS: {\n    DEFINITION: '#3c9d7c', // Soft emerald\n    PROPERTY: '#3a7cc4', // Muted azure\n    STRING: '#c75b63', // Rosewood\n    NUMBER: '#2d8fa1', // Dusty teal\n    ATOM: '#7a70b5', // Dusty periwinkle\n    VARIABLE: '#3f7cac', // Steel blue\n    KEYWORD: '#c57a92', // Dusty rose\n    COMMENT: '#9a9488', // Warm muted gray\n    OPERATOR: '#7c7a73', // Soft graphite\n    TAG: '#3a7cc4', // Muted azure\n    TAG_BRACKET: '#9a9488' // Warm muted gray\n  }\n};\n\nexport const palette = {};\n\npalette.intent = {\n  INFO: colors.BLUE,\n  SUCCESS: colors.GREEN,\n  WARNING: colors.ORANGE,\n  DANGER: colors.RED\n};\n\nconst lightPastelTheme = {\n  mode: 'light',\n  brand: colors.BRAND,\n  text: colors.TEXT,\n  textLink: colors.TEXT_LINK,\n  draftColor: '#cc7b1b',\n  bg: colors.BACKGROUND,\n\n  primary: {\n    solid: colors.BRAND,\n    text: colors.BRAND,\n    strong: colors.BRAND,\n    subtle: colors.BRAND\n  },\n\n  accents: {\n    primary: colors.BRAND\n  },\n\n  background: {\n    base: colors.BACKGROUND,\n    mantle: colors.GRAY_1,\n    crust: colors.GRAY_2,\n    surface0: colors.GRAY_3,\n    surface1: colors.GRAY_4,\n    surface2: colors.GRAY_5\n  },\n\n  status: {\n    info: {\n      background: rgba(palette.intent.INFO, 0.15),\n      text: palette.intent.INFO,\n      border: palette.intent.INFO\n    },\n    success: {\n      background: rgba(palette.intent.SUCCESS, 0.15),\n      text: palette.intent.SUCCESS,\n      border: palette.intent.SUCCESS\n    },\n    warning: {\n      background: rgba(palette.intent.WARNING, 0.15),\n      text: palette.intent.WARNING,\n      border: palette.intent.WARNING\n    },\n    danger: {\n      background: rgba(palette.intent.DANGER, 0.15),\n      text: palette.intent.DANGER,\n      border: palette.intent.DANGER\n    }\n  },\n\n  overlay: {\n    overlay2: colors.GRAY_6,\n    overlay1: '#c0c0c0',\n    overlay0: '#d0d0d0'\n  },\n\n  font: {\n    size: {\n      xs: '0.6875rem',\n      sm: '0.75rem',\n      base: '0.8125rem',\n      md: '0.875rem',\n      lg: '1rem',\n      xl: '1.125rem'\n    }\n  },\n\n  shadow: {\n    sm: '0 1px 3px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.05)',\n    md: '0 2px 8px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.06)',\n    lg: '0 4px 16px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05)'\n  },\n\n  border: {\n    radius: {\n      sm: '4px',\n      base: '6px',\n      md: '8px',\n      lg: '10px',\n      xl: '12px'\n    },\n    border2: colors.GRAY_4,\n    border1: colors.GRAY_3,\n    border0: colors.GRAY_2\n  },\n\n  colors: {\n    text: {\n      white: colors.WHITE,\n      green: colors.GREEN,\n      danger: colors.RED,\n      warning: colors.ORANGE,\n      muted: colors.TEXT_MUTED,\n      purple: colors.PURPLE,\n      yellow: colors.YELLOW,\n      subtext2: colors.GRAY_7,\n      subtext1: colors.TEXT_MUTED,\n      subtext0: colors.GRAY_6\n    },\n    bg: {\n      danger: colors.RED\n    },\n    accent: colors.BRAND\n  },\n\n  input: {\n    bg: colors.WHITE,\n    border: colors.GRAY_4,\n    focusBorder: colors.BRAND,\n    placeholder: {\n      color: colors.GRAY_6,\n      opacity: 0.8\n    }\n  },\n\n  sidebar: {\n    color: colors.TEXT,\n    muted: colors.TEXT_MUTED,\n    bg: colors.GRAY_1,\n    dragbar: {\n      border: colors.GRAY_4,\n      activeBorder: colors.BRAND\n    },\n    collection: {\n      item: {\n        bg: colors.GRAY_2,\n        hoverBg: rgba(colors.GRAY_3, 0.5),\n        focusBorder: colors.BRAND,\n        indentBorder: colors.GRAY_3,\n        active: {\n          indentBorder: colors.GRAY_3\n        },\n        example: {\n          iconColor: colors.GRAY_7\n        }\n      }\n    },\n    dropdownIcon: {\n      color: colors.GRAY_7\n    }\n  },\n\n  dropdown: {\n    color: colors.TEXT,\n    iconColor: colors.GRAY_7,\n    bg: colors.GRAY_1,\n    hoverBg: colors.GRAY_2,\n    shadow: 'rgba(0, 0, 0, 0.15) 0px 6px 16px -2px, rgba(0, 0, 0, 0.1) 0px 3px 8px -3px',\n    border: 'none',\n    separator: colors.GRAY_3,\n    selectedColor: colors.BRAND,\n    mutedText: colors.GRAY_6\n  },\n\n  workspace: {\n    accent: colors.BRAND,\n    border: colors.GRAY_3,\n    button: {\n      bg: colors.GRAY_3\n    }\n  },\n\n  request: {\n    methods: {\n      get: '#3c9d7c', // Soft emerald - success\n      post: '#7a70b5', // Dusty periwinkle - creation\n      put: '#dd8a52', // Muted amber - update\n      delete: '#c75b63', // Rosewood red - deletion\n      patch: '#3a8f98', // Steel teal - modification\n      options: '#2d8fa1', // Dusty teal - metadata\n      head: '#3a7cc4' // Muted azure - lightweight\n    },\n    grpc: '#7a70b5', // Dusty periwinkle\n    ws: '#c57a92', // Dusty rose\n    gql: '#7a70b5' // Dusty periwinkle\n  },\n\n  requestTabPanel: {\n    url: {\n      bg: 'transparent',\n      icon: colors.GRAY_7,\n      iconDanger: colors.RED,\n      border: `solid 1px ${colors.GRAY_4}`\n    },\n    dragbar: {\n      border: colors.GRAY_3,\n      activeBorder: colors.BRAND\n    },\n    responseStatus: colors.TEXT_MUTED,\n    responseOk: colors.GREEN,\n    responseError: colors.RED,\n    responsePending: colors.BLUE,\n    responseOverlayBg: 'rgba(254, 251, 255, 0.7)',\n    card: {\n      bg: colors.WHITE,\n      border: colors.GRAY_3,\n      hr: colors.GRAY_3\n    },\n    graphqlDocsExplorer: {\n      bg: colors.WHITE,\n      color: colors.TEXT\n    }\n  },\n\n  notifications: {\n    bg: colors.WHITE,\n    list: {\n      bg: colors.GRAY_2,\n      borderRight: 'transparent',\n      borderBottom: colors.GRAY_4,\n      hoverBg: colors.GRAY_3,\n      active: {\n        border: colors.BRAND,\n        bg: colors.GRAY_3,\n        hoverBg: colors.GRAY_3\n      }\n    }\n  },\n\n  modal: {\n    title: {\n      color: colors.TEXT,\n      bg: colors.GRAY_1\n    },\n    body: {\n      color: colors.TEXT,\n      bg: colors.WHITE\n    },\n    input: {\n      bg: colors.WHITE,\n      border: colors.GRAY_4,\n      focusBorder: colors.BRAND\n    },\n    backdrop: {\n      opacity: 0.35\n    }\n  },\n\n  button: {\n    secondary: {\n      color: colors.TEXT,\n      bg: colors.GRAY_2,\n      border: colors.GRAY_4,\n      hoverBorder: colors.GRAY_6\n    },\n    close: {\n      color: colors.TEXT,\n      bg: colors.WHITE,\n      border: colors.WHITE,\n      hoverBorder: ''\n    },\n    disabled: {\n      color: colors.GRAY_6,\n      bg: colors.GRAY_2,\n      border: colors.GRAY_3\n    },\n    danger: {\n      color: colors.WHITE,\n      bg: colors.RED,\n      border: colors.RED\n    }\n  },\n\n  button2: {\n    color: {\n      primary: {\n        bg: colors.BRAND,\n        text: colors.WHITE,\n        border: colors.BRAND\n      },\n      light: {\n        bg: rgba(colors.BRAND, 0.08),\n        text: colors.BRAND,\n        border: rgba(colors.BRAND, 0.06)\n      },\n      secondary: {\n        bg: colors.GRAY_3,\n        text: colors.TEXT,\n        border: colors.GRAY_4\n      },\n      success: {\n        bg: colors.GREEN,\n        text: colors.WHITE,\n        border: colors.GREEN\n      },\n      warning: {\n        bg: colors.ORANGE,\n        text: colors.WHITE,\n        border: colors.ORANGE\n      },\n      danger: {\n        bg: colors.RED,\n        text: colors.WHITE,\n        border: colors.RED\n      }\n    }\n  },\n\n  tabs: {\n    marginRight: '1.2rem',\n    active: {\n      fontWeight: 400,\n      color: colors.TEXT,\n      border: colors.BRAND\n    },\n    secondary: {\n      active: {\n        bg: colors.GRAY_2,\n        color: colors.TEXT\n      },\n      inactive: {\n        bg: colors.GRAY_2,\n        color: colors.TEXT_MUTED\n      }\n    }\n  },\n\n  requestTabs: {\n    color: colors.TEXT,\n    bg: colors.GRAY_1,\n    bottomBorder: colors.GRAY_3,\n    icon: {\n      color: colors.GRAY_6,\n      hoverColor: colors.TEXT,\n      hoverBg: colors.GRAY_3\n    },\n    example: {\n      iconColor: colors.GRAY_7\n    }\n  },\n\n  codemirror: {\n    bg: colors.BACKGROUND,\n    border: colors.WHITE,\n    placeholder: {\n      color: colors.GRAY_6,\n      opacity: 0.75\n    },\n    gutter: {\n      bg: colors.BACKGROUND\n    },\n    variable: {\n      valid: colors.GREEN,\n      invalid: colors.RED,\n      prompt: colors.BLUE\n    },\n    tokens: {\n      definition: colors.CODEMIRROR_TOKENS.DEFINITION,\n      property: colors.CODEMIRROR_TOKENS.PROPERTY,\n      string: colors.CODEMIRROR_TOKENS.STRING,\n      number: colors.CODEMIRROR_TOKENS.NUMBER,\n      atom: colors.CODEMIRROR_TOKENS.ATOM,\n      variable: colors.CODEMIRROR_TOKENS.VARIABLE,\n      keyword: colors.CODEMIRROR_TOKENS.KEYWORD,\n      comment: colors.CODEMIRROR_TOKENS.COMMENT,\n      operator: colors.CODEMIRROR_TOKENS.OPERATOR,\n      tag: colors.CODEMIRROR_TOKENS.TAG,\n      tagBracket: colors.CODEMIRROR_TOKENS.TAG_BRACKET\n    },\n    searchLineHighlightCurrent: `${colors.BRAND}12`,\n    searchMatch: '#e5c27a',\n    searchMatchActive: '#d7b35f'\n  },\n\n  table: {\n    border: colors.GRAY_3,\n    thead: {\n      color: colors.TEXT_MUTED\n    },\n    striped: colors.GRAY_1,\n    input: {\n      color: colors.TEXT\n    }\n  },\n\n  plainGrid: {\n    hoverBg: colors.GRAY_2\n  },\n\n  scrollbar: {\n    color: colors.GRAY_5\n  },\n\n  dragAndDrop: {\n    border: colors.BRAND,\n    borderStyle: '2px dashed',\n    hoverBg: `${colors.BRAND}08`,\n    transition: 'all 0.15s ease'\n  },\n\n  infoTip: {\n    bg: colors.WHITE,\n    border: colors.GRAY_4,\n    boxShadow: '0 4px 12px rgba(0, 0, 0, 0.12)'\n  },\n\n  statusBar: {\n    border: colors.GRAY_3,\n    color: colors.TEXT_MUTED\n  },\n\n  console: {\n    bg: colors.GRAY_1,\n    headerBg: colors.GRAY_1,\n    contentBg: colors.BACKGROUND,\n    border: colors.GRAY_3,\n    titleColor: colors.TEXT,\n    countColor: colors.TEXT_MUTED,\n    buttonColor: colors.TEXT,\n    buttonHoverBg: colors.GRAY_2,\n    buttonHoverColor: colors.TEXT,\n    messageColor: colors.TEXT,\n    timestampColor: colors.TEXT_MUTED,\n    emptyColor: colors.TEXT_MUTED,\n    logHoverBg: colors.GRAY_1,\n    resizeHandleHover: colors.BRAND,\n    resizeHandleActive: colors.BRAND,\n    dropdownBg: colors.BACKGROUND,\n    dropdownHeaderBg: colors.GRAY_1,\n    optionHoverBg: colors.GRAY_1,\n    optionLabelColor: colors.TEXT,\n    optionCountColor: colors.TEXT_MUTED,\n    checkboxColor: colors.BRAND,\n    scrollbarTrack: colors.GRAY_1,\n    scrollbarThumb: colors.GRAY_4,\n    scrollbarThumbHover: colors.GRAY_5\n  },\n\n  grpc: {\n    tabNav: {\n      container: {\n        bg: colors.GRAY_1\n      },\n      button: {\n        active: {\n          bg: colors.WHITE,\n          color: colors.TEXT\n        },\n        inactive: {\n          bg: 'transparent',\n          color: colors.TEXT_MUTED\n        }\n      }\n    },\n    importPaths: {\n      header: {\n        text: colors.TEXT_MUTED,\n        button: {\n          color: colors.TEXT_MUTED,\n          hoverColor: colors.TEXT\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: '#c75b63',\n        link: {\n          color: '#c75b63',\n          hoverColor: '#d98b8f'\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: colors.GRAY_2,\n        text: colors.TEXT,\n        icon: colors.TEXT_MUTED,\n        checkbox: {\n          color: colors.TEXT\n        },\n        invalid: {\n          opacity: 0.6,\n          text: '#c75b63'\n        }\n      },\n      empty: {\n        text: colors.TEXT_MUTED\n      },\n      button: {\n        bg: colors.GRAY_2,\n        color: colors.TEXT,\n        border: colors.GRAY_4,\n        hoverBorder: colors.GRAY_6\n      }\n    },\n    protoFiles: {\n      header: {\n        text: colors.TEXT_MUTED,\n        button: {\n          color: colors.TEXT_MUTED,\n          hoverColor: colors.TEXT\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: '#c75b63',\n        link: {\n          color: '#c75b63',\n          hoverColor: '#d98b8f'\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: colors.GRAY_2,\n        selected: {\n          bg: `${colors.BRAND}18`,\n          border: colors.BRAND\n        },\n        text: colors.TEXT,\n        secondaryText: colors.TEXT_MUTED,\n        icon: colors.TEXT_MUTED,\n        invalid: {\n          opacity: 0.6,\n          text: '#c75b63'\n        }\n      },\n      empty: {\n        text: colors.TEXT_MUTED\n      },\n      button: {\n        bg: colors.GRAY_2,\n        color: colors.TEXT,\n        border: colors.GRAY_4,\n        hoverBorder: colors.GRAY_6\n      }\n    }\n  },\n\n  deprecationWarning: {\n    bg: '#fef3c7',\n    border: '#fcd34d',\n    icon: '#d97706',\n    text: colors.TEXT\n  },\n\n  examples: {\n    buttonBg: `${colors.BRAND}15`,\n    buttonColor: colors.BRAND,\n    buttonText: colors.WHITE,\n    buttonIconColor: colors.TEXT,\n    border: colors.GRAY_3,\n    urlBar: {\n      border: colors.GRAY_3,\n      bg: colors.GRAY_1\n    },\n    table: {\n      thead: {\n        bg: colors.GRAY_1,\n        color: colors.TEXT\n      }\n    },\n    checkbox: {\n      color: colors.WHITE\n    }\n  },\n\n  app: {\n    collection: {\n      toolbar: {\n        environmentSelector: {\n          bg: colors.WHITE,\n          border: colors.GRAY_4,\n          icon: colors.BRAND,\n          text: colors.TEXT,\n          caret: colors.GRAY_6,\n          separator: colors.GRAY_4,\n          hoverBg: colors.WHITE,\n          hoverBorder: colors.GRAY_5,\n          noEnvironment: {\n            text: colors.TEXT_MUTED,\n            bg: colors.WHITE,\n            border: colors.GRAY_4,\n            hoverBg: colors.WHITE,\n            hoverBorder: colors.GRAY_5\n          }\n        },\n        sandboxMode: {\n          safeMode: {\n            bg: `${colors.GREEN}18`,\n            color: colors.GREEN\n          },\n          developerMode: {\n            bg: `${colors.ORANGE}18`,\n            color: colors.ORANGE\n          }\n        }\n      }\n    }\n  }\n};\n\nexport default lightPastelTheme;\n"
  },
  {
    "path": "packages/bruno-app/src/themes/light/light.js",
    "content": "import { rgba } from 'polished';\nexport const palette = {\n  primary: {\n    SOLID: 'hsl(33, 80%, 46%)',\n    TEXT: 'hsl(33, 67%, 45%)',\n    STRONG: 'hsl(33, 67%, 50%)',\n    SUBTLE: 'hsl(33, 69%, 56%)'\n  },\n  hues: {\n    RED: 'hsl(8,   60%, 52%)',\n    ROSE: 'hsl(352, 45%, 50%)',\n    BROWN: 'hsl(28,  55%, 38%)',\n    ORANGE: 'hsl(35,  85%, 42%)',\n    YELLOW: 'hsl(45,  75%, 42%)',\n    LIME: 'hsl(85,  45%, 40%)',\n    GREEN: 'hsl(145, 50%, 36%)',\n    TEAL: 'hsl(178, 50%, 36%)',\n    CYAN: 'hsl(195, 55%, 42%)',\n    BLUE: 'hsl(214, 55%, 45%)',\n    INDIGO: 'hsl(235, 45%, 45%)',\n    VIOLET: 'hsl(258, 42%, 50%)',\n    PURPLE: 'hsl(280, 45%, 48%)',\n    PINK: 'hsl(328, 50%, 48%)'\n  },\n  system: {\n    CONTROL_ACCENT: '#b96f1d'\n  },\n  background: {\n    BASE: '#ffffff',\n    MANTLE: '#f8f8f8',\n    CRUST: '#f6f6f6',\n    SURFACE0: '#f1f1f1',\n    SURFACE1: '#eaeaea',\n    SURFACE2: '#e5e5e5'\n  },\n  text: {\n    BASE: '#343434',\n    SUBTEXT2: '#666666',\n    SUBTEXT1: '#838383',\n    SUBTEXT0: '#9B9B9B'\n  },\n  overlay: {\n    OVERLAY2: '#8b8b8b',\n    OVERLAY1: '#B0B0B0',\n    OVERLAY0: '#C0C0C0'\n  },\n  border: {\n    BORDER2: '#cccccc',\n    BORDER1: '#e5e5e5',\n    BORDER0: '#efefef'\n  },\n  utility: {\n    WHITE: '#ffffff',\n    BLACK: '#000000'\n  }\n};\n\npalette.intent = {\n  INFO: palette.hues.BLUE,\n  SUCCESS: palette.hues.GREEN,\n  WARNING: palette.hues.ORANGE,\n  DANGER: palette.hues.RED\n};\n\npalette.syntax = {\n  // Core language structure\n  KEYWORD: palette.hues.ROSE,\n  TAG: palette.hues.ROSE,\n  // Identifiers & properties (collapsed)\n  VARIABLE: palette.hues.PINK,\n  PROPERTY: palette.hues.BLUE,\n  DEFINITION: palette.hues.BLUE,\n\n  // Literals\n  STRING: palette.hues.BROWN,\n  NUMBER: palette.hues.PINK,\n  ATOM: palette.hues.ROSE,\n\n  // Operators & punctuation (quiet)\n  OPERATOR: palette.text.SUBTEXT1,\n  TAG_BRACKET: palette.text.SUBTEXT1,\n\n  // Comments should recede\n  COMMENT: palette.text.SUBTEXT0\n};\n\nconst lightTheme = {\n  mode: 'light',\n  brand: palette.primary.SOLID,\n  text: palette.text.BASE,\n  textLink: palette.hues.BLUE,\n  draftColor: '#cc7b1b',\n  bg: palette.background.BASE,\n\n  primary: {\n    solid: palette.primary.SOLID,\n    text: palette.primary.TEXT,\n    strong: palette.primary.STRONG,\n    subtle: palette.primary.SUBTLE\n  },\n\n  accents: {\n    primary: palette.primary.SOLID\n  },\n\n  background: {\n    base: palette.background.BASE,\n    mantle: palette.background.MANTLE,\n    crust: palette.background.CRUST,\n    surface2: palette.background.SURFACE2,\n    surface1: palette.background.SURFACE1,\n    surface0: palette.background.SURFACE0\n  },\n\n  status: {\n    info: {\n      background: rgba(palette.intent.INFO, 0.15),\n      text: palette.intent.INFO,\n      border: palette.intent.INFO\n    },\n    success: {\n      background: rgba(palette.intent.SUCCESS, 0.15),\n      text: palette.intent.SUCCESS,\n      border: palette.intent.SUCCESS\n    },\n    warning: {\n      background: rgba(palette.intent.WARNING, 0.15),\n      text: palette.intent.WARNING,\n      border: palette.intent.WARNING\n    },\n    danger: {\n      background: rgba(palette.intent.DANGER, 0.15),\n      text: palette.intent.DANGER,\n      border: palette.intent.DANGER\n    }\n  },\n\n  overlay: {\n    overlay2: palette.overlay.OVERLAY2,\n    overlay1: palette.overlay.OVERLAY1,\n    overlay0: palette.overlay.OVERLAY0\n  },\n\n  font: {\n    size: {\n      xs: '0.6875rem', // 11px\n      sm: '0.75rem', // 12px\n      base: '0.8125rem', // 13px\n      md: '0.875rem', // 14px\n      lg: '1rem', // 16px\n      xl: '1.125rem' // 18px\n    }\n  },\n\n  shadow: {\n    sm: '0 1px 3px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.05)',\n    md: '0 2px 8px rgba(0, 0, 0, 0.14), 0 0 0 1px rgba(0, 0, 0, 0.06)',\n    lg: '0 2px 12px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05)'\n  },\n\n  border: {\n    radius: {\n      sm: '4px',\n      base: '6px',\n      md: '8px',\n      lg: '10px',\n      xl: '12px'\n    },\n    border2: palette.border.BORDER2,\n    border1: palette.border.BORDER1,\n    border0: palette.border.BORDER0\n  },\n\n  colors: {\n    text: {\n      white: palette.utility.WHITE,\n      green: palette.intent.SUCCESS,\n      danger: palette.intent.DANGER,\n      warning: palette.intent.WARNING,\n      muted: palette.text.SUBTEXT1,\n      purple: palette.hues.PURPLE,\n      yellow: palette.hues.YELLOW,\n      subtext2: palette.text.SUBTEXT2,\n      subtext1: palette.text.SUBTEXT1,\n      subtext0: palette.text.SUBTEXT0\n    },\n    bg: {\n      danger: palette.hues.RED\n    },\n    accent: palette.system.CONTROL_ACCENT\n  },\n\n  input: {\n    bg: palette.utility.WHITE,\n    border: palette.border.BORDER2,\n    focusBorder: palette.overlay.OVERLAY2,\n    placeholder: {\n      color: palette.overlay.OVERLAY1,\n      opacity: 0.8\n    }\n  },\n\n  sidebar: {\n    color: palette.text.BASE,\n    muted: palette.text.SUBTEXT1,\n    bg: palette.background.MANTLE,\n    dragbar: {\n      border: palette.background.SURFACE2,\n      activeBorder: palette.background.SURFACE2\n    },\n\n    collection: {\n      item: {\n        bg: palette.background.SURFACE1,\n        hoverBg: palette.background.SURFACE1,\n        focusBorder: palette.border.BORDER2,\n        indentBorder: palette.border.BORDER1,\n        active: {\n          indentBorder: palette.border.BORDER1\n        },\n        example: {\n          iconColor: palette.text.SUBTEXT2\n        }\n      }\n    },\n\n    dropdownIcon: {\n      color: palette.text.SUBTEXT2\n    }\n  },\n\n  dropdown: {\n    color: palette.text.BASE,\n    iconColor: palette.text.SUBTEXT2,\n    bg: palette.utility.WHITE,\n    hoverBg: palette.background.CRUST,\n    shadow: '0 0px 3px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.05)',\n    border: 'none',\n    separator: palette.border.BORDER1,\n    selectedColor: palette.primary.TEXT,\n    mutedText: palette.text.SUBTEXT0\n  },\n\n  workspace: {\n    accent: palette.system.CONTROL_ACCENT,\n    border: palette.border.BORDER1,\n    button: {\n      bg: palette.background.MANTLE\n    }\n  },\n\n  request: {\n    methods: {\n      get: palette.hues.GREEN,\n      post: palette.hues.PURPLE,\n      put: palette.hues.ORANGE,\n      delete: palette.hues.RED,\n      patch: palette.hues.PURPLE,\n      options: palette.hues.TEAL,\n      head: palette.hues.CYAN\n    },\n\n    grpc: palette.hues.INDIGO,\n    ws: palette.hues.ORANGE,\n    gql: palette.hues.PINK\n  },\n\n  requestTabPanel: {\n    url: {\n      bg: palette.utility.WHITE,\n      icon: palette.text.SUBTEXT2,\n      iconDanger: palette.hues.RED,\n      border: `solid 1px ${palette.border.BORDER1}`\n    },\n    dragbar: {\n      border: palette.background.SURFACE2,\n      activeBorder: palette.border.BORDER2\n    },\n    responseStatus: palette.text.SUBTEXT1,\n    responseOk: palette.hues.GREEN,\n    responseError: palette.hues.RED,\n    responsePending: palette.hues.BLUE,\n    responseOverlayBg: 'rgba(255, 255, 255, 0.6)',\n    card: {\n      bg: palette.background.BASE,\n      border: palette.border.BORDER1,\n      hr: palette.border.BORDER1\n    },\n    graphqlDocsExplorer: {\n      bg: palette.background.BASE,\n      color: palette.text.BASE\n    }\n  },\n\n  notifications: {\n    bg: palette.background.BASE,\n    list: {\n      bg: palette.background.SURFACE0,\n      borderRight: 'transparent',\n      borderBottom: palette.border.BORDER2,\n      hoverBg: palette.background.SURFACE1,\n      active: {\n        border: palette.hues.BLUE,\n        bg: palette.background.SURFACE1,\n        hoverBg: palette.background.SURFACE2\n      }\n    }\n  },\n\n  modal: {\n    title: {\n      color: palette.text.BASE,\n      bg: palette.background.SURFACE0\n    },\n    body: {\n      color: palette.text.BASE,\n      bg: palette.background.BASE\n    },\n    input: {\n      bg: palette.background.BASE,\n      border: palette.border.BORDER2,\n      focusBorder: palette.overlay.OVERLAY2\n    },\n    backdrop: {\n      opacity: 0.4\n    }\n  },\n\n  button: {\n    secondary: {\n      color: '#212529',\n      bg: '#e2e6ea',\n      border: '#dae0e5',\n      hoverBorder: '#696969'\n    },\n    close: {\n      color: '#212529',\n      bg: 'white',\n      border: 'white',\n      hoverBorder: ''\n    },\n    disabled: {\n      color: '#9f9f9f',\n      bg: palette.border.BORDER0,\n      border: 'rgb(234, 234, 234)'\n    },\n    danger: {\n      color: '#fff',\n      bg: '#dc3545',\n      border: '#dc3545'\n    }\n  },\n  button2: {\n    color: {\n      primary: {\n        bg: palette.primary.SOLID,\n        text: palette.utility.WHITE,\n        border: palette.primary.SOLID\n      },\n      light: {\n        bg: rgba(palette.primary.SOLID, 0.08),\n        text: palette.primary.SOLID,\n        border: rgba(palette.primary.SOLID, 0.06)\n      },\n      secondary: {\n        bg: palette.background.MANTLE,\n        border: palette.border.BORDER2,\n        text: palette.text.BASE\n      },\n      success: {\n        bg: palette.hues.GREEN,\n        text: palette.utility.WHITE,\n        border: palette.hues.GREEN\n      },\n      warning: {\n        bg: palette.hues.ORANGE,\n        text: palette.utility.WHITE,\n        border: palette.hues.ORANGE\n      },\n      danger: {\n        bg: palette.hues.RED,\n        text: palette.utility.WHITE,\n        border: palette.hues.RED\n      }\n    }\n  },\n  tabs: {\n    marginRight: '1.2rem',\n    active: {\n      fontWeight: 400,\n      color: palette.text.BASE,\n      border: palette.primary.STRONG\n    },\n    secondary: {\n      active: {\n        bg: palette.background.SURFACE1,\n        color: palette.text.BASE\n      },\n      inactive: {\n        bg: palette.background.SURFACE0,\n        color: palette.text.SUBTEXT1\n      }\n    }\n  },\n\n  requestTabs: {\n    color: palette.text.BASE,\n    bg: palette.background.CRUST,\n    bottomBorder: palette.border.BORDER0,\n    icon: {\n      color: palette.text.SUBTEXT0,\n      hoverColor: palette.text.BASE,\n      hoverBg: palette.background.SURFACE1\n    },\n    example: {\n      iconColor: palette.text.SUBTEXT2\n    }\n  },\n\n  codemirror: {\n    bg: palette.utility.WHITE,\n    border: palette.utility.WHITE,\n    placeholder: {\n      color: palette.overlay.OVERLAY1,\n      opacity: 0.75\n    },\n    gutter: {\n      bg: palette.utility.WHITE\n    },\n    variable: {\n      valid: palette.hues.GREEN,\n      invalid: palette.hues.RED,\n      prompt: palette.hues.BLUE\n    },\n    tokens: {\n      definition: palette.syntax.DEFINITION,\n      property: palette.syntax.PROPERTY,\n      string: palette.syntax.STRING,\n      number: palette.syntax.NUMBER,\n      atom: palette.syntax.ATOM,\n      variable: palette.syntax.VARIABLE,\n      keyword: palette.syntax.KEYWORD,\n      comment: palette.syntax.COMMENT,\n      operator: palette.syntax.OPERATOR,\n      tag: palette.syntax.TAG,\n      tagBracket: palette.syntax.TAG_BRACKET\n    },\n    searchLineHighlightCurrent: 'rgba(120,120,120,0.10)',\n    searchMatch: '#B8860B',\n    searchMatchActive: '#DAA520'\n  },\n\n  table: {\n    border: palette.border.BORDER0,\n    thead: {\n      color: palette.text.SUBTEXT2\n    },\n    striped: palette.background.SURFACE0,\n    input: {\n      color: palette.text.BASE\n    }\n  },\n\n  plainGrid: {\n    hoverBg: palette.background.CRUST\n  },\n\n  scrollbar: {\n    color: 'rgb(152 151 149)'\n  },\n\n  dragAndDrop: {\n    border: palette.overlay.OVERLAY2, // Using the same gray as focusBorder from input\n    borderStyle: '2px solid',\n    hoverBg: 'rgba(139, 139, 139, 0.05)', // Matching the border color with reduced opacity\n    transition: 'all 0.1s ease'\n  },\n\n  infoTip: {\n    bg: 'white',\n    border: palette.background.SURFACE1,\n    boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)'\n  },\n\n  statusBar: {\n    border: '#E9E9E9',\n    color: 'rgb(100, 100, 100)'\n  },\n  console: {\n    bg: '#f8f9fa',\n    headerBg: '#f8f9fa',\n    contentBg: '#ffffff',\n    border: '#dee2e6',\n    titleColor: '#212529',\n    countColor: '#6c757d',\n    buttonColor: '#495057',\n    buttonHoverBg: '#e9ecef',\n    buttonHoverColor: '#212529',\n    messageColor: '#212529',\n    timestampColor: '#6c757d',\n    emptyColor: '#6c757d',\n    logHoverBg: 'rgba(0, 0, 0, 0.03)',\n    resizeHandleHover: '#0d6efd',\n    resizeHandleActive: '#0d6efd',\n    dropdownBg: '#ffffff',\n    dropdownHeaderBg: '#f8f9fa',\n    optionHoverBg: '#f8f9fa',\n    optionLabelColor: '#212529',\n    optionCountColor: '#6c757d',\n    checkboxColor: palette.primary.SOLID,\n    scrollbarTrack: '#f8f9fa',\n    scrollbarThumb: '#ced4da',\n    scrollbarThumbHover: '#adb5bd'\n  },\n\n  grpc: {\n    tabNav: {\n      container: {\n        bg: '#f5f5f5'\n      },\n      button: {\n        active: {\n          bg: '#ffffff',\n          color: '#000000'\n        },\n        inactive: {\n          bg: 'transparent',\n          color: '#525252'\n        }\n      }\n    },\n    importPaths: {\n      header: {\n        text: '#838383',\n        button: {\n          color: '#838383',\n          hoverColor: '#343434'\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: '#B91C1C',\n        link: {\n          color: '#B91C1C',\n          hoverColor: '#dc2626'\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: 'rgba(0, 0, 0, 0.05)',\n        text: '#343434',\n        icon: '#838383',\n        checkbox: {\n          color: '#343434'\n        },\n        invalid: {\n          opacity: 0.6,\n          text: '#B91C1C'\n        }\n      },\n      empty: {\n        text: '#838383'\n      },\n      button: {\n        bg: '#e2e6ea',\n        color: '#212529',\n        border: '#dae0e5',\n        hoverBorder: '#696969'\n      }\n    },\n    protoFiles: {\n      header: {\n        text: '#838383',\n        button: {\n          color: '#838383',\n          hoverColor: '#343434'\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: '#B91C1C',\n        link: {\n          color: '#B91C1C',\n          hoverColor: '#dc2626'\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: 'rgba(0, 0, 0, 0.05)',\n        selected: {\n          bg: 'rgba(217, 119, 6, 0.2)',\n          border: '#d97706'\n        },\n        text: '#343434',\n        secondaryText: '#838383',\n        icon: '#838383',\n        invalid: {\n          opacity: 0.6,\n          text: '#B91C1C'\n        }\n      },\n      empty: {\n        text: '#838383'\n      },\n      button: {\n        bg: '#e2e6ea',\n        color: '#212529',\n        border: '#dae0e5',\n        hoverBorder: '#696969'\n      }\n    }\n  },\n  deprecationWarning: {\n    bg: 'rgba(217, 31, 17, 0.1)',\n    border: 'rgba(217, 31, 17, 0.1)',\n    icon: '#D91F11',\n    text: palette.text.BASE\n  },\n\n  examples: {\n    buttonBg: '#D977061A',\n    buttonColor: '#D97706',\n    buttonText: '#fff',\n    buttonIconColor: '#000',\n    border: palette.border.BORDER0,\n    urlBar: {\n      border: palette.border.BORDER0,\n      bg: '#F5F5F5'\n    },\n    table: {\n      thead: {\n        bg: '#f8f9fa',\n        color: '#212529'\n      }\n    },\n    checkbox: {\n      color: '#fff'\n    }\n  },\n\n  app: {\n    collection: {\n      toolbar: {\n        environmentSelector: {\n          bg: palette.utility.WHITE,\n          border: palette.border.BORDER1,\n          icon: palette.primary.TEXT,\n          text: palette.text.BASE,\n          caret: palette.overlay.OVERLAY1,\n          separator: palette.border.BORDER1,\n          hoverBg: palette.utility.WHITE,\n          hoverBorder: palette.border.BORDER2,\n\n          noEnvironment: {\n            text: palette.text.SUBTEXT1,\n            bg: palette.utility.WHITE,\n            border: palette.border.BORDER2,\n            hoverBg: palette.utility.WHITE,\n            hoverBorder: palette.overlay.OVERLAY1\n          }\n        },\n        sandboxMode: {\n          safeMode: {\n            bg: 'rgba(4, 120, 87, 0.12)',\n            color: palette.hues.GREEN\n          },\n          developerMode: {\n            bg: 'rgba(204, 145, 73, 0.15)',\n            color: palette.hues.YELLOW\n          }\n        }\n      }\n    }\n  }\n};\n\nexport default lightTheme;\n"
  },
  {
    "path": "packages/bruno-app/src/themes/light/vscode.js",
    "content": "// VS Code Light+ Theme for Bruno\n// Based on the default Visual Studio Code Light+ theme\nimport { rgba } from 'polished';\n\nconst colors = {\n  // VS Code Light+ Core Colors\n  EDITOR_BG: '#ffffff',\n  SIDEBAR_BG: '#f3f3f3',\n  ACTIVITY_BAR_BG: '#2c2c2c',\n  PANEL_BG: '#ffffff',\n\n  // Text colors\n  TEXT: '#000000',\n  TEXT_SECONDARY: '#1f1f1f',\n  TEXT_MUTED: '#6e7681',\n  TEXT_LINK: '#006ab1',\n\n  // Brand - VS Code blue\n  BRAND: '#007acc',\n  BRAND_HOVER: '#0062a3',\n\n  // Semantic colors\n  GREEN: '#098658',\n  YELLOW: '#795e26',\n  ORANGE: '#a31515',\n  RED: '#cd3131',\n  PURPLE: '#af00db',\n  BLUE: '#0000ff',\n  CYAN: '#008080',\n\n  WHITE: '#ffffff',\n  BLACK: '#000000',\n\n  // Grays (VS Code Light specific)\n  GRAY_1: '#f3f3f3',\n  GRAY_2: '#e8e8e8',\n  GRAY_3: '#dddddd',\n  GRAY_4: '#d4d4d4',\n  GRAY_5: '#c6c6c6',\n  GRAY_6: '#a0a0a0',\n  GRAY_7: '#7a7a7a',\n  GRAY_8: '#5a5a5a',\n\n  // Borders\n  BORDER: '#e5e5e5',\n  BORDER_DARK: '#cecece',\n\n  CODEMIRROR_TOKENS: {\n    DEFINITION: '#267f99',\n    PROPERTY: '#0451a5',\n    STRING: '#a31515',\n    NUMBER: '#098658',\n    ATOM: '#0000ff',\n    VARIABLE: '#001080',\n    KEYWORD: '#af00db',\n    COMMENT: '#008000',\n    OPERATOR: '#000000',\n    TAG: '#800000',\n    TAG_BRACKET: '#800000'\n  }\n};\n\nexport const palette = {};\n\npalette.intent = {\n  INFO: colors.BLUE,\n  SUCCESS: colors.GREEN,\n  WARNING: colors.YELLOW,\n  DANGER: colors.RED\n};\n\nconst vscodeLightTheme = {\n  mode: 'light',\n  brand: colors.BRAND,\n  text: colors.TEXT,\n  textLink: colors.TEXT_LINK,\n  draftColor: '#cc7b1b',\n  bg: colors.EDITOR_BG,\n\n  primary: {\n    solid: colors.BRAND,\n    text: colors.TEXT_LINK,\n    strong: '#0078d4',\n    subtle: '#4da6ff'\n  },\n\n  accents: {\n    primary: colors.BRAND\n  },\n\n  background: {\n    base: colors.EDITOR_BG,\n    mantle: colors.SIDEBAR_BG,\n    crust: colors.GRAY_2,\n    surface0: colors.GRAY_3,\n    surface1: colors.GRAY_4,\n    surface2: colors.GRAY_5\n  },\n\n  status: {\n    info: {\n      background: rgba(palette.intent.INFO, 0.15),\n      text: palette.intent.INFO,\n      border: palette.intent.INFO\n    },\n    success: {\n      background: rgba(palette.intent.SUCCESS, 0.15),\n      text: palette.intent.SUCCESS,\n      border: palette.intent.SUCCESS\n    },\n    warning: {\n      background: rgba(palette.intent.WARNING, 0.15),\n      text: palette.intent.WARNING,\n      border: palette.intent.WARNING\n    },\n    danger: {\n      background: rgba(palette.intent.DANGER, 0.15),\n      text: palette.intent.DANGER,\n      border: palette.intent.DANGER\n    }\n  },\n\n  overlay: {\n    overlay2: colors.GRAY_6,\n    overlay1: '#c0c0c0',\n    overlay0: '#d0d0d0'\n  },\n\n  font: {\n    size: {\n      xs: '0.6875rem',\n      sm: '0.75rem',\n      base: '0.8125rem',\n      md: '0.875rem',\n      lg: '1rem',\n      xl: '1.125rem'\n    }\n  },\n\n  shadow: {\n    sm: '0 1px 3px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.04)',\n    md: '0 2px 8px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.05)',\n    lg: '0 2px 12px rgba(0, 0, 0, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.05)'\n  },\n\n  border: {\n    radius: {\n      sm: '4px',\n      base: '6px',\n      md: '8px',\n      lg: '10px',\n      xl: '12px'\n    },\n    border2: colors.GRAY_4,\n    border1: colors.BORDER,\n    border0: colors.GRAY_2\n  },\n\n  colors: {\n    text: {\n      white: colors.WHITE,\n      green: colors.GREEN,\n      danger: colors.RED,\n      warning: '#bf8803',\n      muted: colors.TEXT_MUTED,\n      purple: colors.PURPLE,\n      yellow: colors.YELLOW,\n      subtext2: colors.TEXT_SECONDARY,\n      subtext1: colors.TEXT_MUTED,\n      subtext0: colors.GRAY_7\n    },\n    bg: {\n      danger: colors.RED\n    },\n    accent: colors.BRAND\n  },\n\n  input: {\n    bg: colors.WHITE,\n    border: colors.BORDER_DARK,\n    focusBorder: colors.BRAND,\n    placeholder: {\n      color: colors.TEXT_MUTED,\n      opacity: 0.8\n    }\n  },\n\n  sidebar: {\n    color: colors.TEXT,\n    muted: colors.TEXT_MUTED,\n    bg: colors.SIDEBAR_BG,\n    dragbar: {\n      border: colors.BORDER,\n      activeBorder: colors.GRAY_5\n    },\n    collection: {\n      item: {\n        bg: colors.GRAY_2,\n        hoverBg: colors.GRAY_3,\n        focusBorder: colors.GRAY_5,\n        indentBorder: colors.BORDER,\n        active: {\n          indentBorder: colors.BORDER\n        },\n        example: {\n          iconColor: colors.GRAY_7\n        }\n      }\n    },\n    dropdownIcon: {\n      color: colors.TEXT_SECONDARY\n    }\n  },\n\n  dropdown: {\n    color: colors.TEXT,\n    iconColor: colors.TEXT_SECONDARY,\n    bg: colors.WHITE,\n    hoverBg: colors.GRAY_2,\n    shadow: 'rgba(0, 0, 0, 0.16) 0px 6px 12px -2px, rgba(0, 0, 0, 0.1) 0px 3px 7px -3px',\n    border: 'none',\n    separator: colors.BORDER,\n    selectedColor: colors.BRAND,\n    mutedText: colors.TEXT_MUTED\n  },\n\n  workspace: {\n    accent: colors.BRAND,\n    border: colors.BORDER,\n    button: {\n      bg: colors.GRAY_2\n    }\n  },\n\n  request: {\n    methods: {\n      get: colors.GREEN,\n      post: colors.YELLOW,\n      put: '#d18616',\n      delete: colors.RED,\n      patch: '#d18616',\n      options: colors.GRAY_7,\n      head: colors.BLUE\n    },\n    grpc: colors.CYAN,\n    ws: '#795e26',\n    gql: colors.PURPLE\n  },\n\n  requestTabPanel: {\n    url: {\n      bg: colors.WHITE,\n      icon: colors.TEXT_SECONDARY,\n      iconDanger: colors.RED,\n      border: `solid 1px ${colors.BORDER}`\n    },\n    dragbar: {\n      border: colors.BORDER,\n      activeBorder: colors.BRAND\n    },\n    responseStatus: colors.TEXT_SECONDARY,\n    responseOk: colors.GREEN,\n    responseError: colors.RED,\n    responsePending: colors.BRAND,\n    responseOverlayBg: 'rgba(255, 255, 255, 0.6)',\n    card: {\n      bg: colors.WHITE,\n      border: colors.BORDER,\n      hr: colors.BORDER\n    },\n    graphqlDocsExplorer: {\n      bg: colors.WHITE,\n      color: colors.TEXT\n    }\n  },\n\n  notifications: {\n    bg: colors.WHITE,\n    list: {\n      bg: colors.GRAY_2,\n      borderRight: 'transparent',\n      borderBottom: colors.BORDER,\n      hoverBg: colors.GRAY_3,\n      active: {\n        border: colors.BRAND,\n        bg: colors.GRAY_3,\n        hoverBg: colors.GRAY_3\n      }\n    }\n  },\n\n  modal: {\n    title: {\n      color: colors.TEXT_SECONDARY,\n      bg: colors.GRAY_1\n    },\n    body: {\n      color: colors.TEXT,\n      bg: colors.WHITE\n    },\n    input: {\n      bg: colors.WHITE,\n      border: colors.BORDER_DARK,\n      focusBorder: colors.BRAND\n    },\n    backdrop: {\n      opacity: 0.4\n    }\n  },\n\n  button: {\n    secondary: {\n      color: colors.TEXT,\n      bg: colors.GRAY_2,\n      border: colors.GRAY_4,\n      hoverBorder: colors.GRAY_5\n    },\n    close: {\n      color: colors.TEXT,\n      bg: colors.WHITE,\n      border: colors.WHITE,\n      hoverBorder: ''\n    },\n    disabled: {\n      color: colors.GRAY_6,\n      bg: colors.GRAY_2,\n      border: colors.BORDER\n    },\n    danger: {\n      color: colors.WHITE,\n      bg: colors.RED,\n      border: colors.RED\n    }\n  },\n\n  button2: {\n    color: {\n      primary: {\n        bg: colors.BRAND,\n        text: colors.WHITE,\n        border: colors.BRAND\n      },\n      light: {\n        bg: rgba(colors.BRAND, 0.08),\n        text: colors.BRAND,\n        border: rgba(colors.BRAND, 0.06)\n      },\n      secondary: {\n        bg: colors.GRAY_3,\n        text: colors.TEXT,\n        border: colors.GRAY_4\n      },\n      success: {\n        bg: colors.GREEN,\n        text: colors.WHITE,\n        border: colors.GREEN\n      },\n      warning: {\n        bg: '#bf8803',\n        text: colors.WHITE,\n        border: '#bf8803'\n      },\n      danger: {\n        bg: colors.RED,\n        text: colors.WHITE,\n        border: colors.RED\n      }\n    }\n  },\n\n  tabs: {\n    marginRight: '1.2rem',\n    active: {\n      fontWeight: 400,\n      color: colors.TEXT,\n      border: colors.BRAND\n    },\n    secondary: {\n      active: {\n        bg: colors.GRAY_2,\n        color: colors.TEXT\n      },\n      inactive: {\n        bg: colors.GRAY_2,\n        color: colors.TEXT_MUTED\n      }\n    }\n  },\n\n  requestTabs: {\n    color: colors.TEXT,\n    bg: colors.SIDEBAR_BG,\n    bottomBorder: colors.BORDER,\n    icon: {\n      color: colors.GRAY_6,\n      hoverColor: colors.TEXT_SECONDARY,\n      hoverBg: colors.GRAY_3\n    },\n    example: {\n      iconColor: colors.GRAY_7\n    }\n  },\n\n  codemirror: {\n    bg: colors.WHITE,\n    border: colors.WHITE,\n    placeholder: {\n      color: colors.TEXT_MUTED,\n      opacity: 0.75\n    },\n    gutter: {\n      bg: colors.WHITE\n    },\n    variable: {\n      valid: colors.GREEN,\n      invalid: colors.RED,\n      prompt: colors.BRAND\n    },\n    tokens: {\n      definition: colors.CODEMIRROR_TOKENS.DEFINITION,\n      property: colors.CODEMIRROR_TOKENS.PROPERTY,\n      string: colors.CODEMIRROR_TOKENS.STRING,\n      number: colors.CODEMIRROR_TOKENS.NUMBER,\n      atom: colors.CODEMIRROR_TOKENS.ATOM,\n      variable: colors.CODEMIRROR_TOKENS.VARIABLE,\n      keyword: colors.CODEMIRROR_TOKENS.KEYWORD,\n      comment: colors.CODEMIRROR_TOKENS.COMMENT,\n      operator: colors.CODEMIRROR_TOKENS.OPERATOR,\n      tag: colors.CODEMIRROR_TOKENS.TAG,\n      tagBracket: colors.CODEMIRROR_TOKENS.TAG_BRACKET\n    },\n    searchLineHighlightCurrent: 'rgba(255, 255, 0, 0.2)',\n    searchMatch: '#a8ac94',\n    searchMatchActive: '#f8e8a6'\n  },\n\n  table: {\n    border: colors.BORDER,\n    thead: {\n      color: colors.TEXT_SECONDARY\n    },\n    striped: colors.GRAY_1,\n    input: {\n      color: colors.BLACK\n    }\n  },\n\n  plainGrid: {\n    hoverBg: colors.GRAY_1\n  },\n\n  scrollbar: {\n    color: colors.GRAY_5\n  },\n\n  dragAndDrop: {\n    border: colors.BRAND,\n    borderStyle: '2px solid',\n    hoverBg: 'rgba(0, 122, 204, 0.08)',\n    transition: 'all 0.1s ease'\n  },\n\n  infoTip: {\n    bg: colors.WHITE,\n    border: colors.BORDER,\n    boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)'\n  },\n\n  statusBar: {\n    border: colors.BORDER,\n    color: colors.TEXT_MUTED\n  },\n\n  console: {\n    bg: colors.GRAY_1,\n    headerBg: colors.GRAY_1,\n    contentBg: colors.WHITE,\n    border: colors.BORDER,\n    titleColor: colors.TEXT,\n    countColor: colors.TEXT_MUTED,\n    buttonColor: colors.TEXT_SECONDARY,\n    buttonHoverBg: colors.GRAY_2,\n    buttonHoverColor: colors.TEXT,\n    messageColor: colors.TEXT,\n    timestampColor: colors.TEXT_MUTED,\n    emptyColor: colors.TEXT_MUTED,\n    logHoverBg: 'rgba(0, 0, 0, 0.03)',\n    resizeHandleHover: colors.BRAND,\n    resizeHandleActive: colors.BRAND,\n    dropdownBg: colors.WHITE,\n    dropdownHeaderBg: colors.GRAY_1,\n    optionHoverBg: colors.GRAY_1,\n    optionLabelColor: colors.TEXT,\n    optionCountColor: colors.TEXT_MUTED,\n    checkboxColor: colors.BRAND,\n    scrollbarTrack: colors.GRAY_1,\n    scrollbarThumb: colors.GRAY_4,\n    scrollbarThumbHover: colors.GRAY_5\n  },\n\n  grpc: {\n    tabNav: {\n      container: {\n        bg: colors.GRAY_1\n      },\n      button: {\n        active: {\n          bg: colors.WHITE,\n          color: colors.BLACK\n        },\n        inactive: {\n          bg: 'transparent',\n          color: colors.TEXT_SECONDARY\n        }\n      }\n    },\n    importPaths: {\n      header: {\n        text: colors.TEXT_MUTED,\n        button: {\n          color: colors.TEXT_MUTED,\n          hoverColor: colors.TEXT\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: colors.RED,\n        link: {\n          color: colors.RED,\n          hoverColor: colors.RED\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: rgba(colors.BLACK, 0.05),\n        text: colors.TEXT,\n        icon: colors.TEXT_MUTED,\n        checkbox: {\n          color: colors.TEXT\n        },\n        invalid: {\n          opacity: 0.6,\n          text: colors.RED\n        }\n      },\n      empty: {\n        text: colors.TEXT_MUTED\n      },\n      button: {\n        bg: colors.GRAY_2,\n        color: colors.TEXT,\n        border: colors.GRAY_4,\n        hoverBorder: colors.GRAY_5\n      }\n    },\n    protoFiles: {\n      header: {\n        text: colors.TEXT_MUTED,\n        button: {\n          color: colors.TEXT_MUTED,\n          hoverColor: colors.TEXT\n        }\n      },\n      error: {\n        bg: 'transparent',\n        text: colors.RED,\n        link: {\n          color: colors.RED,\n          hoverColor: colors.RED\n        }\n      },\n      item: {\n        bg: 'transparent',\n        hoverBg: rgba(colors.BLACK, 0.05),\n        selected: {\n          bg: rgba(colors.BRAND, 0.15),\n          border: colors.BRAND\n        },\n        text: colors.TEXT,\n        secondaryText: colors.TEXT_MUTED,\n        icon: colors.TEXT_MUTED,\n        invalid: {\n          opacity: 0.6,\n          text: colors.RED\n        }\n      },\n      empty: {\n        text: colors.TEXT_MUTED\n      },\n      button: {\n        bg: colors.GRAY_2,\n        color: colors.TEXT,\n        border: colors.GRAY_4,\n        hoverBorder: colors.GRAY_5\n      }\n    }\n  },\n\n  deprecationWarning: {\n    bg: 'rgba(205, 49, 49, 0.1)',\n    border: 'rgba(205, 49, 49, 0.15)',\n    icon: colors.RED,\n    text: colors.TEXT\n  },\n\n  examples: {\n    buttonBg: 'rgba(0, 122, 204, 0.1)',\n    buttonColor: colors.BRAND,\n    buttonText: colors.WHITE,\n    buttonIconColor: colors.BLACK,\n    border: colors.BORDER,\n    urlBar: {\n      border: colors.BORDER,\n      bg: colors.GRAY_1\n    },\n    table: {\n      thead: {\n        bg: colors.GRAY_1,\n        color: colors.TEXT\n      }\n    },\n    checkbox: {\n      color: colors.WHITE\n    }\n  },\n\n  app: {\n    collection: {\n      toolbar: {\n        environmentSelector: {\n          bg: colors.WHITE,\n          border: colors.BORDER,\n          icon: colors.BRAND,\n          text: colors.TEXT,\n          caret: colors.GRAY_6,\n          separator: colors.BORDER,\n          hoverBg: colors.WHITE,\n          hoverBorder: colors.GRAY_5,\n          noEnvironment: {\n            text: colors.TEXT_MUTED,\n            bg: colors.WHITE,\n            border: colors.BORDER,\n            hoverBg: colors.WHITE,\n            hoverBorder: colors.GRAY_5\n          }\n        },\n        sandboxMode: {\n          safeMode: {\n            bg: 'rgba(9, 134, 88, 0.12)',\n            color: colors.GREEN\n          },\n          developerMode: {\n            bg: 'rgba(121, 94, 38, 0.12)',\n            color: colors.YELLOW\n          }\n        }\n      }\n    }\n  }\n};\n\nexport default vscodeLightTheme;\n"
  },
  {
    "path": "packages/bruno-app/src/themes/schema/index.js",
    "content": "import { ossSchema } from './oss';\n\nexport default ossSchema;\n"
  },
  {
    "path": "packages/bruno-app/src/themes/schema/oss.js",
    "content": "export const ossSchema = {\n  type: 'object',\n  properties: {\n    mode: { type: 'string', description: 'Theme mode', enum: ['light', 'dark'] },\n    brand: { type: 'string', description: 'Primary brand color' },\n    text: { type: 'string', description: 'Default text color' },\n    textLink: { type: 'string', description: 'Link text color' },\n    draftColor: { type: 'string', description: 'Color for draft/unsaved indicator' },\n    bg: { type: 'string', description: 'Background color' },\n\n    primary: {\n      type: 'object',\n      properties: {\n        solid: { type: 'string', description: 'Buttons, toggles, active pills' },\n        text: { type: 'string', description: 'Links, emphasized text' },\n        strong: { type: 'string', description: 'Thick borders, tab underlines' },\n        subtle: { type: 'string', description: 'Focus rings, subtle outlines' }\n      },\n      required: ['solid', 'text', 'strong', 'subtle'],\n      additionalProperties: false\n    },\n\n    accents: {\n      type: 'object',\n      properties: {\n        primary: { type: 'string', description: 'Primary accent color' }\n      },\n      required: ['primary'],\n      additionalProperties: false\n    },\n\n    background: {\n      type: 'object',\n      properties: {\n        base: { type: 'string', description: 'App canvas background' },\n        mantle: { type: 'string', description: 'Sidebars background' },\n        crust: { type: 'string', description: 'Panels background' },\n        surface0: { type: 'string', description: 'Cards background' },\n        surface1: { type: 'string', description: 'Raised elements background' },\n        surface2: { type: 'string', description: 'Borders / dividers' }\n      },\n      required: ['base', 'mantle', 'crust', 'surface0', 'surface1', 'surface2'],\n      additionalProperties: false\n    },\n\n    status: {\n      type: 'object',\n      description: 'Status colors for info, success, warning, and danger states',\n      properties: {\n        info: {\n          type: 'object',\n          properties: {\n            background: { type: 'string', description: 'Info status background color' },\n            text: { type: 'string', description: 'Info status text color' },\n            border: { type: 'string', description: 'Info status border color' }\n          },\n          required: ['background', 'text', 'border'],\n          additionalProperties: false\n        },\n        success: {\n          type: 'object',\n          properties: {\n            background: { type: 'string', description: 'Success status background color' },\n            text: { type: 'string', description: 'Success status text color' },\n            border: { type: 'string', description: 'Success status border color' }\n          },\n          required: ['background', 'text', 'border'],\n          additionalProperties: false\n        },\n        warning: {\n          type: 'object',\n          properties: {\n            background: { type: 'string', description: 'Warning status background color' },\n            text: { type: 'string', description: 'Warning status text color' },\n            border: { type: 'string', description: 'Warning status border color' }\n          },\n          required: ['background', 'text', 'border'],\n          additionalProperties: false\n        },\n        danger: {\n          type: 'object',\n          properties: {\n            background: { type: 'string', description: 'Danger status background color' },\n            text: { type: 'string', description: 'Danger status text color' },\n            border: { type: 'string', description: 'Danger status border color' }\n          },\n          required: ['background', 'text', 'border'],\n          additionalProperties: false\n        }\n      },\n      required: ['info', 'success', 'warning', 'danger'],\n      additionalProperties: false\n    },\n\n    overlay: {\n      type: 'object',\n      properties: {\n        overlay2: { type: 'string', description: 'Overlay level 2' },\n        overlay1: { type: 'string', description: 'Overlay level 1' },\n        overlay0: { type: 'string', description: 'Overlay level 0' }\n      },\n      required: ['overlay2', 'overlay1', 'overlay0'],\n      additionalProperties: false\n    },\n\n    font: {\n      type: 'object',\n      properties: {\n        size: {\n          type: 'object',\n          properties: {\n            xs: { type: 'string', description: 'Extra small font size (11px)' },\n            sm: { type: 'string', description: 'Small font size (12px)' },\n            base: { type: 'string', description: 'Base font size (13px)' },\n            md: { type: 'string', description: 'Medium font size (14px)' },\n            lg: { type: 'string', description: 'Large font size (16px)' },\n            xl: { type: 'string', description: 'Extra large font size (18px)' }\n          },\n          required: ['xs', 'sm', 'base', 'md', 'lg', 'xl'],\n          additionalProperties: false\n        }\n      },\n      required: ['size'],\n      additionalProperties: false\n    },\n\n    shadow: {\n      type: 'object',\n      properties: {\n        sm: { type: 'string', description: 'Small shadow' },\n        md: { type: 'string', description: 'Medium shadow' },\n        lg: { type: 'string', description: 'Large shadow' }\n      },\n      required: ['sm', 'md', 'lg'],\n      additionalProperties: false\n    },\n\n    border: {\n      type: 'object',\n      properties: {\n        radius: {\n          type: 'object',\n          properties: {\n            sm: { type: 'string' },\n            base: { type: 'string' },\n            md: { type: 'string' },\n            lg: { type: 'string' },\n            xl: { type: 'string' }\n          },\n          required: ['sm', 'base', 'md', 'lg', 'xl'],\n          additionalProperties: false\n        },\n        border2: { type: 'string' },\n        border1: { type: 'string' },\n        border0: { type: 'string' }\n      },\n      required: ['radius', 'border2', 'border1', 'border0'],\n      additionalProperties: false\n    },\n\n    colors: {\n      type: 'object',\n      properties: {\n        text: {\n          type: 'object',\n          properties: {\n            white: { type: 'string' },\n            green: { type: 'string' },\n            danger: { type: 'string' },\n            warning: { type: 'string' },\n            muted: { type: 'string' },\n            purple: { type: 'string' },\n            yellow: { type: 'string' },\n            subtext2: { type: 'string' },\n            subtext1: { type: 'string' },\n            subtext0: { type: 'string' }\n          },\n          required: ['white', 'green', 'danger', 'warning', 'muted', 'purple', 'yellow', 'subtext2', 'subtext1', 'subtext0'],\n          additionalProperties: false\n        },\n        bg: {\n          type: 'object',\n          properties: {\n            danger: { type: 'string' }\n          },\n          required: ['danger'],\n          additionalProperties: false\n        },\n        accent: { type: 'string' }\n      },\n      required: ['text', 'bg', 'accent'],\n      additionalProperties: false\n    },\n\n    input: {\n      type: 'object',\n      properties: {\n        bg: { type: 'string' },\n        border: { type: 'string' },\n        focusBorder: { type: 'string' },\n        placeholder: {\n          type: 'object',\n          properties: {\n            color: { type: 'string' },\n            opacity: { type: 'number' }\n          },\n          required: ['color', 'opacity'],\n          additionalProperties: false\n        }\n      },\n      required: ['bg', 'border', 'focusBorder', 'placeholder'],\n      additionalProperties: false\n    },\n\n    sidebar: {\n      type: 'object',\n      properties: {\n        color: { type: 'string' },\n        muted: { type: 'string' },\n        bg: { type: 'string' },\n        dragbar: {\n          type: 'object',\n          properties: {\n            border: { type: 'string' },\n            activeBorder: { type: 'string' }\n          },\n          required: ['border', 'activeBorder'],\n          additionalProperties: false\n        },\n        collection: {\n          type: 'object',\n          properties: {\n            item: {\n              type: 'object',\n              properties: {\n                bg: { type: 'string' },\n                hoverBg: { type: 'string' },\n                focusBorder: { type: 'string' },\n                indentBorder: { type: 'string' },\n                active: {\n                  type: 'object',\n                  properties: {\n                    indentBorder: { type: 'string' }\n                  },\n                  required: ['indentBorder'],\n                  additionalProperties: false\n                },\n                example: {\n                  type: 'object',\n                  properties: {\n                    iconColor: { type: 'string' }\n                  },\n                  required: ['iconColor'],\n                  additionalProperties: false\n                }\n              },\n              required: ['bg', 'hoverBg', 'focusBorder', 'indentBorder', 'active', 'example'],\n              additionalProperties: false\n            }\n          },\n          required: ['item'],\n          additionalProperties: false\n        },\n        dropdownIcon: {\n          type: 'object',\n          properties: {\n            color: { type: 'string' }\n          },\n          required: ['color'],\n          additionalProperties: false\n        }\n      },\n      required: ['color', 'muted', 'bg', 'dragbar', 'collection', 'dropdownIcon'],\n      additionalProperties: false\n    },\n\n    dropdown: {\n      type: 'object',\n      properties: {\n        color: { type: 'string' },\n        iconColor: { type: 'string' },\n        bg: { type: 'string' },\n        hoverBg: { type: 'string' },\n        shadow: { type: 'string', description: 'Box shadow. Use \"none\" for no shadow.' },\n        separator: { type: 'string' },\n        selectedColor: { type: 'string' },\n        mutedText: { type: 'string' },\n        border: { type: 'string', description: 'Border color. Use \"none\" for no border.' }\n      },\n      required: ['color', 'iconColor', 'bg', 'hoverBg', 'shadow', 'separator', 'selectedColor', 'mutedText', 'border'],\n      additionalProperties: false\n    },\n\n    workspace: {\n      type: 'object',\n      properties: {\n        accent: { type: 'string' },\n        border: { type: 'string' },\n        button: {\n          type: 'object',\n          properties: {\n            bg: { type: 'string' }\n          },\n          required: ['bg'],\n          additionalProperties: false\n        }\n      },\n      required: ['accent', 'border', 'button'],\n      additionalProperties: false\n    },\n\n    request: {\n      type: 'object',\n      properties: {\n        methods: {\n          type: 'object',\n          properties: {\n            get: { type: 'string' },\n            post: { type: 'string' },\n            put: { type: 'string' },\n            delete: { type: 'string' },\n            patch: { type: 'string' },\n            options: { type: 'string' },\n            head: { type: 'string' }\n          },\n          required: ['get', 'post', 'put', 'delete', 'patch', 'options', 'head'],\n          additionalProperties: false\n        },\n        grpc: { type: 'string' },\n        ws: { type: 'string' },\n        gql: { type: 'string' }\n      },\n      required: ['methods', 'grpc', 'ws', 'gql'],\n      additionalProperties: false\n    },\n\n    requestTabPanel: {\n      type: 'object',\n      properties: {\n        url: {\n          type: 'object',\n          properties: {\n            bg: { type: 'string' },\n            icon: { type: 'string' },\n            iconDanger: { type: 'string' },\n            border: { type: 'string' }\n          },\n          required: ['bg', 'icon', 'iconDanger', 'border'],\n          additionalProperties: false\n        },\n        dragbar: {\n          type: 'object',\n          properties: {\n            border: { type: 'string' },\n            activeBorder: { type: 'string' }\n          },\n          required: ['border', 'activeBorder'],\n          additionalProperties: false\n        },\n        responseStatus: { type: 'string' },\n        responseOk: { type: 'string' },\n        responseError: { type: 'string' },\n        responsePending: { type: 'string' },\n        responseOverlayBg: { type: 'string' },\n        card: {\n          type: 'object',\n          properties: {\n            bg: { type: 'string' },\n            border: { type: 'string' },\n            hr: { type: 'string' }\n          },\n          required: ['bg', 'border', 'hr'],\n          additionalProperties: false\n        },\n        graphqlDocsExplorer: {\n          type: 'object',\n          properties: {\n            bg: { type: 'string' },\n            color: { type: 'string' }\n          },\n          required: ['bg', 'color'],\n          additionalProperties: false\n        }\n      },\n      required: ['url', 'dragbar', 'responseStatus', 'responseOk', 'responseError', 'responsePending', 'responseOverlayBg', 'card', 'graphqlDocsExplorer'],\n      additionalProperties: false\n    },\n\n    notifications: {\n      type: 'object',\n      properties: {\n        bg: { type: 'string' },\n        list: {\n          type: 'object',\n          properties: {\n            bg: { type: 'string' },\n            borderRight: { type: 'string' },\n            borderBottom: { type: 'string' },\n            hoverBg: { type: 'string' },\n            active: {\n              type: 'object',\n              properties: {\n                border: { type: 'string' },\n                bg: { type: 'string' },\n                hoverBg: { type: 'string' }\n              },\n              required: ['border', 'bg', 'hoverBg'],\n              additionalProperties: false\n            }\n          },\n          required: ['bg', 'borderRight', 'borderBottom', 'hoverBg', 'active'],\n          additionalProperties: false\n        }\n      },\n      required: ['bg', 'list'],\n      additionalProperties: false\n    },\n\n    modal: {\n      type: 'object',\n      properties: {\n        title: {\n          type: 'object',\n          properties: {\n            color: { type: 'string' },\n            bg: { type: 'string' }\n          },\n          required: ['color', 'bg'],\n          additionalProperties: false\n        },\n        body: {\n          type: 'object',\n          properties: {\n            color: { type: 'string' },\n            bg: { type: 'string' }\n          },\n          required: ['color', 'bg'],\n          additionalProperties: false\n        },\n        input: {\n          type: 'object',\n          properties: {\n            bg: { type: 'string' },\n            border: { type: 'string' },\n            focusBorder: { type: 'string' }\n          },\n          required: ['bg', 'border', 'focusBorder'],\n          additionalProperties: false\n        },\n        backdrop: {\n          type: 'object',\n          properties: {\n            opacity: { type: 'number' }\n          },\n          required: ['opacity'],\n          additionalProperties: false\n        }\n      },\n      required: ['title', 'body', 'input', 'backdrop'],\n      additionalProperties: false\n    },\n\n    button: {\n      type: 'object',\n      properties: {\n        secondary: {\n          type: 'object',\n          properties: {\n            color: { type: 'string' },\n            bg: { type: 'string' },\n            border: { type: 'string' },\n            hoverBorder: { type: 'string' }\n          },\n          required: ['color', 'bg', 'border', 'hoverBorder'],\n          additionalProperties: false\n        },\n        close: {\n          type: 'object',\n          properties: {\n            color: { type: 'string' },\n            bg: { type: 'string' },\n            border: { type: 'string' },\n            hoverBorder: { type: 'string' }\n          },\n          required: ['color', 'bg', 'border', 'hoverBorder'],\n          additionalProperties: false\n        },\n        disabled: {\n          type: 'object',\n          properties: {\n            color: { type: 'string' },\n            bg: { type: 'string' },\n            border: { type: 'string' }\n          },\n          required: ['color', 'bg', 'border'],\n          additionalProperties: false\n        },\n        danger: {\n          type: 'object',\n          properties: {\n            color: { type: 'string' },\n            bg: { type: 'string' },\n            border: { type: 'string' }\n          },\n          required: ['color', 'bg', 'border'],\n          additionalProperties: false\n        }\n      },\n      required: ['secondary', 'close', 'disabled', 'danger'],\n      additionalProperties: false\n    },\n\n    button2: {\n      type: 'object',\n      properties: {\n        color: {\n          type: 'object',\n          properties: {\n            primary: {\n              type: 'object',\n              properties: {\n                bg: { type: 'string' },\n                text: { type: 'string' },\n                border: { type: 'string' }\n              },\n              required: ['bg', 'text', 'border'],\n              additionalProperties: false\n            },\n            light: {\n              type: 'object',\n              properties: {\n                bg: { type: 'string' },\n                text: { type: 'string' },\n                border: { type: 'string' }\n              },\n              required: ['bg', 'text', 'border'],\n              additionalProperties: false\n            },\n            secondary: {\n              type: 'object',\n              properties: {\n                bg: { type: 'string' },\n                text: { type: 'string' },\n                border: { type: 'string' }\n              },\n              required: ['bg', 'text', 'border'],\n              additionalProperties: false\n            },\n            success: {\n              type: 'object',\n              properties: {\n                bg: { type: 'string' },\n                text: { type: 'string' },\n                border: { type: 'string' }\n              },\n              required: ['bg', 'text', 'border'],\n              additionalProperties: false\n            },\n            warning: {\n              type: 'object',\n              properties: {\n                bg: { type: 'string' },\n                text: { type: 'string' },\n                border: { type: 'string' }\n              },\n              required: ['bg', 'text', 'border'],\n              additionalProperties: false\n            },\n            danger: {\n              type: 'object',\n              properties: {\n                bg: { type: 'string' },\n                text: { type: 'string' },\n                border: { type: 'string' }\n              },\n              required: ['bg', 'text', 'border'],\n              additionalProperties: false\n            }\n          },\n          required: ['primary', 'light', 'secondary', 'success', 'warning', 'danger'],\n          additionalProperties: false\n        }\n      },\n      required: ['color'],\n      additionalProperties: false\n    },\n\n    tabs: {\n      type: 'object',\n      properties: {\n        marginRight: { type: 'string' },\n        active: {\n          type: 'object',\n          properties: {\n            fontWeight: { type: 'number' },\n            color: { type: 'string' },\n            border: { type: 'string' }\n          },\n          required: ['fontWeight', 'color', 'border'],\n          additionalProperties: false\n        },\n        secondary: {\n          type: 'object',\n          properties: {\n            active: {\n              type: 'object',\n              properties: {\n                bg: { type: 'string' },\n                color: { type: 'string' }\n              },\n              required: ['bg', 'color'],\n              additionalProperties: false\n            },\n            inactive: {\n              type: 'object',\n              properties: {\n                bg: { type: 'string' },\n                color: { type: 'string' }\n              },\n              required: ['bg', 'color'],\n              additionalProperties: false\n            }\n          },\n          required: ['active', 'inactive'],\n          additionalProperties: false\n        }\n      },\n      required: ['marginRight', 'active', 'secondary'],\n      additionalProperties: false\n    },\n\n    requestTabs: {\n      type: 'object',\n      properties: {\n        color: { type: 'string' },\n        bg: { type: 'string' },\n        bottomBorder: { type: 'string' },\n        icon: {\n          type: 'object',\n          properties: {\n            color: { type: 'string' },\n            hoverColor: { type: 'string' },\n            hoverBg: { type: 'string' }\n          },\n          required: ['color', 'hoverColor', 'hoverBg'],\n          additionalProperties: false\n        },\n        example: {\n          type: 'object',\n          properties: {\n            iconColor: { type: 'string' }\n          },\n          required: ['iconColor'],\n          additionalProperties: false\n        }\n      },\n      required: ['color', 'bg', 'bottomBorder', 'icon', 'example'],\n      additionalProperties: false\n    },\n\n    codemirror: {\n      type: 'object',\n      properties: {\n        bg: { type: 'string' },\n        border: { type: 'string' },\n        placeholder: {\n          type: 'object',\n          properties: {\n            color: { type: 'string' },\n            opacity: { type: 'number' }\n          },\n          required: ['color', 'opacity'],\n          additionalProperties: false\n        },\n        gutter: {\n          type: 'object',\n          properties: {\n            bg: { type: 'string' }\n          },\n          required: ['bg'],\n          additionalProperties: false\n        },\n        variable: {\n          type: 'object',\n          properties: {\n            valid: { type: 'string' },\n            invalid: { type: 'string' },\n            prompt: { type: 'string' }\n          },\n          required: ['valid', 'invalid', 'prompt'],\n          additionalProperties: false\n        },\n        tokens: {\n          type: 'object',\n          properties: {\n            definition: { type: 'string' },\n            property: { type: 'string' },\n            string: { type: 'string' },\n            number: { type: 'string' },\n            atom: { type: 'string' },\n            variable: { type: 'string' },\n            keyword: { type: 'string' },\n            comment: { type: 'string' },\n            operator: { type: 'string' },\n            tag: { type: 'string' },\n            tagBracket: { type: 'string' }\n          },\n          required: ['definition', 'property', 'string', 'number', 'atom', 'variable', 'keyword', 'comment', 'operator', 'tag', 'tagBracket'],\n          additionalProperties: false\n        },\n        searchLineHighlightCurrent: { type: 'string' },\n        searchMatch: { type: 'string' },\n        searchMatchActive: { type: 'string' }\n      },\n      required: ['bg', 'border', 'placeholder', 'gutter', 'variable', 'tokens', 'searchLineHighlightCurrent', 'searchMatch', 'searchMatchActive'],\n      additionalProperties: false\n    },\n\n    table: {\n      type: 'object',\n      properties: {\n        border: { type: 'string' },\n        thead: {\n          type: 'object',\n          properties: {\n            color: { type: 'string' }\n          },\n          required: ['color'],\n          additionalProperties: false\n        },\n        striped: { type: 'string' },\n        input: {\n          type: 'object',\n          properties: {\n            color: { type: 'string' }\n          },\n          required: ['color'],\n          additionalProperties: false\n        }\n      },\n      required: ['border', 'thead', 'striped', 'input'],\n      additionalProperties: false\n    },\n\n    plainGrid: {\n      type: 'object',\n      properties: {\n        hoverBg: { type: 'string' }\n      },\n      required: ['hoverBg'],\n      additionalProperties: false\n    },\n\n    scrollbar: {\n      type: 'object',\n      properties: {\n        color: { type: 'string' }\n      },\n      required: ['color'],\n      additionalProperties: false\n    },\n\n    dragAndDrop: {\n      type: 'object',\n      properties: {\n        border: { type: 'string' },\n        borderStyle: { type: 'string' },\n        hoverBg: { type: 'string' },\n        transition: { type: 'string' }\n      },\n      required: ['border', 'borderStyle', 'hoverBg', 'transition'],\n      additionalProperties: false\n    },\n\n    infoTip: {\n      type: 'object',\n      properties: {\n        bg: { type: 'string' },\n        border: { type: 'string' },\n        boxShadow: { type: 'string' }\n      },\n      required: ['bg', 'border', 'boxShadow'],\n      additionalProperties: false\n    },\n\n    statusBar: {\n      type: 'object',\n      properties: {\n        border: { type: 'string' },\n        color: { type: 'string' }\n      },\n      required: ['border', 'color'],\n      additionalProperties: false\n    },\n\n    console: {\n      type: 'object',\n      properties: {\n        bg: { type: 'string' },\n        headerBg: { type: 'string' },\n        contentBg: { type: 'string' },\n        border: { type: 'string' },\n        titleColor: { type: 'string' },\n        countColor: { type: 'string' },\n        buttonColor: { type: 'string' },\n        buttonHoverBg: { type: 'string' },\n        buttonHoverColor: { type: 'string' },\n        messageColor: { type: 'string' },\n        timestampColor: { type: 'string' },\n        emptyColor: { type: 'string' },\n        logHoverBg: { type: 'string' },\n        resizeHandleHover: { type: 'string' },\n        resizeHandleActive: { type: 'string' },\n        dropdownBg: { type: 'string' },\n        dropdownHeaderBg: { type: 'string' },\n        optionHoverBg: { type: 'string' },\n        optionLabelColor: { type: 'string' },\n        optionCountColor: { type: 'string' },\n        checkboxColor: { type: 'string' },\n        scrollbarTrack: { type: 'string' },\n        scrollbarThumb: { type: 'string' },\n        scrollbarThumbHover: { type: 'string' }\n      },\n      required: ['bg', 'headerBg', 'contentBg', 'border', 'titleColor', 'countColor', 'buttonColor', 'buttonHoverBg', 'buttonHoverColor', 'messageColor', 'timestampColor', 'emptyColor', 'logHoverBg', 'resizeHandleHover', 'resizeHandleActive', 'dropdownBg', 'dropdownHeaderBg', 'optionHoverBg', 'optionLabelColor', 'optionCountColor', 'checkboxColor', 'scrollbarTrack', 'scrollbarThumb', 'scrollbarThumbHover'],\n      additionalProperties: false\n    },\n\n    grpc: {\n      type: 'object',\n      properties: {\n        tabNav: {\n          type: 'object',\n          properties: {\n            container: {\n              type: 'object',\n              properties: {\n                bg: { type: 'string' }\n              },\n              required: ['bg'],\n              additionalProperties: false\n            },\n            button: {\n              type: 'object',\n              properties: {\n                active: {\n                  type: 'object',\n                  properties: {\n                    bg: { type: 'string' },\n                    color: { type: 'string' }\n                  },\n                  required: ['bg', 'color'],\n                  additionalProperties: false\n                },\n                inactive: {\n                  type: 'object',\n                  properties: {\n                    bg: { type: 'string' },\n                    color: { type: 'string' }\n                  },\n                  required: ['bg', 'color'],\n                  additionalProperties: false\n                }\n              },\n              required: ['active', 'inactive'],\n              additionalProperties: false\n            }\n          },\n          required: ['container', 'button'],\n          additionalProperties: false\n        },\n        importPaths: {\n          type: 'object',\n          properties: {\n            header: {\n              type: 'object',\n              properties: {\n                text: { type: 'string' },\n                button: {\n                  type: 'object',\n                  properties: {\n                    color: { type: 'string' },\n                    hoverColor: { type: 'string' }\n                  },\n                  required: ['color', 'hoverColor'],\n                  additionalProperties: false\n                }\n              },\n              required: ['text', 'button'],\n              additionalProperties: false\n            },\n            error: {\n              type: 'object',\n              properties: {\n                bg: { type: 'string' },\n                text: { type: 'string' },\n                link: {\n                  type: 'object',\n                  properties: {\n                    color: { type: 'string' },\n                    hoverColor: { type: 'string' }\n                  },\n                  required: ['color', 'hoverColor'],\n                  additionalProperties: false\n                }\n              },\n              required: ['bg', 'text', 'link'],\n              additionalProperties: false\n            },\n            item: {\n              type: 'object',\n              properties: {\n                bg: { type: 'string' },\n                hoverBg: { type: 'string' },\n                text: { type: 'string' },\n                icon: { type: 'string' },\n                checkbox: {\n                  type: 'object',\n                  properties: {\n                    color: { type: 'string' }\n                  },\n                  required: ['color'],\n                  additionalProperties: false\n                },\n                invalid: {\n                  type: 'object',\n                  properties: {\n                    opacity: { type: 'number' },\n                    text: { type: 'string' }\n                  },\n                  required: ['opacity', 'text'],\n                  additionalProperties: false\n                }\n              },\n              required: ['bg', 'hoverBg', 'text', 'icon', 'checkbox', 'invalid'],\n              additionalProperties: false\n            },\n            empty: {\n              type: 'object',\n              properties: {\n                text: { type: 'string' }\n              },\n              required: ['text'],\n              additionalProperties: false\n            },\n            button: {\n              type: 'object',\n              properties: {\n                bg: { type: 'string' },\n                color: { type: 'string' },\n                border: { type: 'string' },\n                hoverBorder: { type: 'string' }\n              },\n              required: ['bg', 'color', 'border', 'hoverBorder'],\n              additionalProperties: false\n            }\n          },\n          required: ['header', 'error', 'item', 'empty', 'button'],\n          additionalProperties: false\n        },\n        protoFiles: {\n          type: 'object',\n          properties: {\n            header: {\n              type: 'object',\n              properties: {\n                text: { type: 'string' },\n                button: {\n                  type: 'object',\n                  properties: {\n                    color: { type: 'string' },\n                    hoverColor: { type: 'string' }\n                  },\n                  required: ['color', 'hoverColor'],\n                  additionalProperties: false\n                }\n              },\n              required: ['text', 'button'],\n              additionalProperties: false\n            },\n            error: {\n              type: 'object',\n              properties: {\n                bg: { type: 'string' },\n                text: { type: 'string' },\n                link: {\n                  type: 'object',\n                  properties: {\n                    color: { type: 'string' },\n                    hoverColor: { type: 'string' }\n                  },\n                  required: ['color', 'hoverColor'],\n                  additionalProperties: false\n                }\n              },\n              required: ['bg', 'text', 'link'],\n              additionalProperties: false\n            },\n            item: {\n              type: 'object',\n              properties: {\n                bg: { type: 'string' },\n                hoverBg: { type: 'string' },\n                selected: {\n                  type: 'object',\n                  properties: {\n                    bg: { type: 'string' },\n                    border: { type: 'string' }\n                  },\n                  required: ['bg', 'border'],\n                  additionalProperties: false\n                },\n                text: { type: 'string' },\n                secondaryText: { type: 'string' },\n                icon: { type: 'string' },\n                invalid: {\n                  type: 'object',\n                  properties: {\n                    opacity: { type: 'number' },\n                    text: { type: 'string' }\n                  },\n                  required: ['opacity', 'text'],\n                  additionalProperties: false\n                }\n              },\n              required: ['bg', 'hoverBg', 'selected', 'text', 'secondaryText', 'icon', 'invalid'],\n              additionalProperties: false\n            },\n            empty: {\n              type: 'object',\n              properties: {\n                text: { type: 'string' }\n              },\n              required: ['text'],\n              additionalProperties: false\n            },\n            button: {\n              type: 'object',\n              properties: {\n                bg: { type: 'string' },\n                color: { type: 'string' },\n                border: { type: 'string' },\n                hoverBorder: { type: 'string' }\n              },\n              required: ['bg', 'color', 'border', 'hoverBorder'],\n              additionalProperties: false\n            }\n          },\n          required: ['header', 'error', 'item', 'empty', 'button'],\n          additionalProperties: false\n        }\n      },\n      required: ['tabNav', 'importPaths', 'protoFiles'],\n      additionalProperties: false\n    },\n\n    deprecationWarning: {\n      type: 'object',\n      properties: {\n        bg: { type: 'string' },\n        border: { type: 'string' },\n        icon: { type: 'string' },\n        text: { type: 'string' }\n      },\n      required: ['bg', 'border', 'icon', 'text'],\n      additionalProperties: false\n    },\n\n    examples: {\n      type: 'object',\n      properties: {\n        buttonBg: { type: 'string' },\n        buttonColor: { type: 'string' },\n        buttonText: { type: 'string' },\n        buttonIconColor: { type: 'string' },\n        border: { type: 'string' },\n        urlBar: {\n          type: 'object',\n          properties: {\n            border: { type: 'string' },\n            bg: { type: 'string' }\n          },\n          required: ['border', 'bg'],\n          additionalProperties: false\n        },\n        table: {\n          type: 'object',\n          properties: {\n            thead: {\n              type: 'object',\n              properties: {\n                bg: { type: 'string' },\n                color: { type: 'string' }\n              },\n              required: ['bg', 'color'],\n              additionalProperties: false\n            }\n          },\n          required: ['thead'],\n          additionalProperties: false\n        },\n        checkbox: {\n          type: 'object',\n          properties: {\n            color: { type: 'string' }\n          },\n          required: ['color'],\n          additionalProperties: false\n        }\n      },\n      required: ['buttonBg', 'buttonColor', 'buttonText', 'buttonIconColor', 'border', 'urlBar', 'table', 'checkbox'],\n      additionalProperties: false\n    },\n\n    app: {\n      type: 'object',\n      properties: {\n        collection: {\n          type: 'object',\n          properties: {\n            toolbar: {\n              type: 'object',\n              properties: {\n                environmentSelector: {\n                  type: 'object',\n                  properties: {\n                    bg: { type: 'string' },\n                    border: { type: 'string' },\n                    icon: { type: 'string' },\n                    text: { type: 'string' },\n                    caret: { type: 'string' },\n                    separator: { type: 'string' },\n                    hoverBg: { type: 'string' },\n                    hoverBorder: { type: 'string' },\n                    noEnvironment: {\n                      type: 'object',\n                      properties: {\n                        text: { type: 'string' },\n                        bg: { type: 'string' },\n                        border: { type: 'string' },\n                        hoverBg: { type: 'string' },\n                        hoverBorder: { type: 'string' }\n                      },\n                      required: ['text', 'bg', 'border', 'hoverBg', 'hoverBorder'],\n                      additionalProperties: false\n                    }\n                  },\n                  required: ['bg', 'border', 'icon', 'text', 'caret', 'separator', 'hoverBg', 'hoverBorder', 'noEnvironment'],\n                  additionalProperties: false\n                },\n                sandboxMode: {\n                  type: 'object',\n                  properties: {\n                    safeMode: {\n                      type: 'object',\n                      properties: {\n                        bg: { type: 'string' },\n                        color: { type: 'string' }\n                      },\n                      required: ['bg', 'color'],\n                      additionalProperties: false\n                    },\n                    developerMode: {\n                      type: 'object',\n                      properties: {\n                        bg: { type: 'string' },\n                        color: { type: 'string' }\n                      },\n                      required: ['bg', 'color'],\n                      additionalProperties: false\n                    }\n                  },\n                  required: ['safeMode', 'developerMode'],\n                  additionalProperties: false\n                }\n              },\n              required: ['environmentSelector', 'sandboxMode'],\n              additionalProperties: false\n            }\n          },\n          required: ['toolbar'],\n          additionalProperties: false\n        }\n      },\n      required: ['collection'],\n      additionalProperties: false\n    }\n  },\n  required: [\n    'mode', 'brand', 'text', 'textLink', 'draftColor', 'bg', 'primary', 'accents', 'background', 'status', 'overlay', 'font', 'shadow', 'border', 'colors', 'input',\n    'sidebar', 'dropdown', 'workspace', 'request',\n    'requestTabPanel', 'notifications', 'modal', 'button', 'button2', 'tabs',\n    'requestTabs', 'codemirror', 'table', 'plainGrid', 'scrollbar', 'dragAndDrop',\n    'infoTip', 'statusBar', 'console', 'grpc', 'deprecationWarning', 'examples', 'app'\n  ],\n  additionalProperties: false\n};\n"
  },
  {
    "path": "packages/bruno-app/src/ui/ActionIcon/StyledWrapper.js",
    "content": "import styled, { css } from 'styled-components';\n\nconst sizeMap = {\n  xs: 20,\n  sm: 22,\n  md: 24,\n  lg: 28,\n  xl: 32\n};\n\nconst variants = {\n  subtle: css`\n    color: ${(props) => props.theme.colors.text.muted};\n    background: transparent;\n    &:hover:not(:disabled) {\n      color: ${(props) => props.theme.text};\n      background: ${(props) => props.theme.dropdown.hoverBg};\n    }\n  `\n};\n\nconst StyledWrapper = styled.button`\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  border: none;\n  border-radius: 4px;\n  cursor: pointer;\n  transition: all 0.15s ease;\n  padding: 0;\n\n  width: ${(props) => sizeMap[props.$size] || props.$size}px;\n  height: ${(props) => sizeMap[props.$size] || props.$size}px;\n\n  ${(props) => variants[props.$variant] || variants.subtle}\n\n  ${(props) => props.$color && css`\n    color: ${props.$color};\n  `}\n\n  &:disabled {\n    opacity: 0.5;\n    cursor: not-allowed;\n  }\n\n  ${(props) => props.$colorOnHover && css`\n    &:hover:not(:disabled) {\n      color: ${props.$colorOnHover};\n    }\n  `}\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/ui/ActionIcon/index.js",
    "content": "import React from 'react';\nimport StyledWrapper from './StyledWrapper';\n\n/**\n * ActionIcon - A reusable icon button component\n *\n * @param {Object} props\n * @param {ReactNode} props.children - The icon component to render\n * @param {string} props.variant - Visual variant: 'subtle' (default), 'filled', 'outline', etc.\n * @param {string} props.size - Size of the button: 'sm', 'md', 'lg', etc. (default: 'md')\n * @param {boolean} props.disabled - Whether the button is disabled\n * @param {string} props.className - Additional CSS class names\n * @param {string} props.component - Polymorphic component (default: 'button')\n * @param {string} props.label - Label for both title and aria-label (preferred)\n * @param {string} props.title - Title attribute (falls back to label or aria-label)\n * @param {string} [props.ariaLabel] - Accessibility label (falls back to label or title)\n * @param {string} props.colorOnHover - Color to apply to icon on hover/focus (e.g., 'red', '#ef4444', 'var(--color-danger)')\n * @param {string} props.color - Color to override the default variant color (e.g., 'red', '#ef4444', 'var(--color-text)')\n * @param {Object} props.style - Style object to override the default variant style (e.g., 'width: 16px; min-width: 16px;')\n * @param {Object} props...rest - Other props passed to the underlying element\n */\nconst ActionIcon = ({\n  children,\n  variant = 'subtle',\n  size = 'md',\n  disabled = false,\n  className = '',\n  component: Component = 'button',\n  label,\n  'aria-label': ariaLabel,\n  colorOnHover,\n  color,\n  style,\n  ...rest\n}) => {\n  // Build className array and filter out empty strings\n  const classNames = ['action-icon', className].filter(Boolean).join(' ');\n\n  return (\n    <StyledWrapper\n      as={Component}\n      $variant={variant}\n      $size={size}\n      $colorOnHover={colorOnHover}\n      $color={color}\n      disabled={disabled}\n      className={classNames}\n      title={label}\n      aria-label={ariaLabel}\n      style={style}\n      {...rest}\n    >\n      {children}\n    </StyledWrapper>\n  );\n};\n\nexport default ActionIcon;\n"
  },
  {
    "path": "packages/bruno-app/src/ui/Button/Button.stories.jsx",
    "content": "import React from 'react';\nimport Button from './index';\n\nexport default {\n  title: 'Components/Button',\n  component: Button,\n  parameters: {\n    layout: 'centered'\n  },\n  tags: ['autodocs'],\n  argTypes: {\n    size: {\n      control: 'select',\n      options: ['xs', 'sm', 'base', 'md', 'lg'],\n      description: 'The size of the button'\n    },\n    variant: {\n      control: 'select',\n      options: ['filled', 'outline', 'ghost'],\n      description: 'The visual style variant of the button'\n    },\n    color: {\n      control: 'select',\n      options: ['primary', 'secondary', 'success', 'warning', 'danger'],\n      description: 'The color of the button'\n    },\n    fontWeight: {\n      control: 'select',\n      options: ['regular', 'medium'],\n      description: 'Font weight (default: regular for filled/ghost, medium for outline)'\n    },\n    rounded: {\n      control: 'select',\n      options: ['sm', 'base', 'md', 'lg', 'full'],\n      description: 'Border radius style'\n    },\n    disabled: {\n      control: 'boolean',\n      description: 'Whether the button is disabled'\n    },\n    loading: {\n      control: 'boolean',\n      description: 'Whether the button is in loading state'\n    },\n    fullWidth: {\n      control: 'boolean',\n      description: 'Whether the button takes full width'\n    },\n    iconPosition: {\n      control: 'select',\n      options: ['left', 'right'],\n      description: 'Position of the icon relative to text'\n    },\n    children: {\n      control: 'text',\n      description: 'Button text content'\n    },\n    onClick: { action: 'clicked' },\n    onDoubleClick: { action: 'double-clicked' },\n    onMouseEnter: { action: 'mouse-entered' },\n    onMouseLeave: { action: 'mouse-left' }\n  }\n};\n\n// Sample icon component for stories\nconst PlusIcon = () => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width=\"16\"\n    height=\"16\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n  >\n    <line x1=\"12\" y1=\"5\" x2=\"12\" y2=\"19\"></line>\n    <line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"></line>\n  </svg>\n);\n\nconst SendIcon = () => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width=\"16\"\n    height=\"16\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n  >\n    <line x1=\"22\" y1=\"2\" x2=\"11\" y2=\"13\"></line>\n    <polygon points=\"22 2 15 22 11 13 2 9 22 2\"></polygon>\n  </svg>\n);\n\nconst TrashIcon = () => (\n  <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    width=\"16\"\n    height=\"16\"\n    viewBox=\"0 0 24 24\"\n    fill=\"none\"\n    stroke=\"currentColor\"\n    strokeWidth=\"2\"\n    strokeLinecap=\"round\"\n    strokeLinejoin=\"round\"\n  >\n    <polyline points=\"3 6 5 6 21 6\"></polyline>\n    <path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\"></path>\n  </svg>\n);\n\n// Default story\nexport const Default = {\n  args: {\n    children: 'Button'\n  }\n};\n\n// Variants\nexport const Filled = {\n  render: () => (\n    <div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>\n      <Button variant=\"filled\" color=\"primary\">Primary</Button>\n      <Button variant=\"filled\" color=\"secondary\">Secondary</Button>\n      <Button variant=\"filled\" color=\"success\">Success</Button>\n      <Button variant=\"filled\" color=\"warning\">Warning</Button>\n      <Button variant=\"filled\" color=\"danger\">Danger</Button>\n    </div>\n  )\n};\n\nexport const Outline = {\n  render: () => (\n    <div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>\n      <Button variant=\"outline\" color=\"primary\">Primary</Button>\n      <Button variant=\"outline\" color=\"secondary\">Secondary</Button>\n      <Button variant=\"outline\" color=\"success\">Success</Button>\n      <Button variant=\"outline\" color=\"warning\">Warning</Button>\n      <Button variant=\"outline\" color=\"danger\">Danger</Button>\n    </div>\n  )\n};\n\nexport const Ghost = {\n  render: () => (\n    <div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>\n      <Button variant=\"ghost\" color=\"primary\">Primary</Button>\n      <Button variant=\"ghost\" color=\"secondary\">Secondary</Button>\n      <Button variant=\"ghost\" color=\"success\">Success</Button>\n      <Button variant=\"ghost\" color=\"warning\">Warning</Button>\n      <Button variant=\"ghost\" color=\"danger\">Danger</Button>\n    </div>\n  )\n};\n\n// With Icons\nexport const WithIconLeft = {\n  render: () => (\n    <div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>\n      <div>\n        <h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Filled</h3>\n        <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>\n          <Button variant=\"filled\" size=\"xs\" icon={<PlusIcon />} iconPosition=\"left\">Add</Button>\n          <Button variant=\"filled\" size=\"sm\" icon={<PlusIcon />} iconPosition=\"left\">Add Item</Button>\n          <Button variant=\"filled\" size=\"base\" icon={<PlusIcon />} iconPosition=\"left\">Add Item</Button>\n          <Button variant=\"filled\" size=\"md\" icon={<PlusIcon />} iconPosition=\"left\">Add Item</Button>\n          <Button variant=\"filled\" size=\"lg\" icon={<PlusIcon />} iconPosition=\"left\">Add Item</Button>\n        </div>\n      </div>\n      <div>\n        <h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Outline</h3>\n        <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>\n          <Button variant=\"outline\" size=\"xs\" icon={<PlusIcon />} iconPosition=\"left\">Add</Button>\n          <Button variant=\"outline\" size=\"sm\" icon={<PlusIcon />} iconPosition=\"left\">Add Item</Button>\n          <Button variant=\"outline\" size=\"base\" icon={<PlusIcon />} iconPosition=\"left\">Add Item</Button>\n          <Button variant=\"outline\" size=\"md\" icon={<PlusIcon />} iconPosition=\"left\">Add Item</Button>\n          <Button variant=\"outline\" size=\"lg\" icon={<PlusIcon />} iconPosition=\"left\">Add Item</Button>\n        </div>\n      </div>\n      <div>\n        <h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Ghost</h3>\n        <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>\n          <Button variant=\"ghost\" size=\"xs\" icon={<PlusIcon />} iconPosition=\"left\">Add</Button>\n          <Button variant=\"ghost\" size=\"sm\" icon={<PlusIcon />} iconPosition=\"left\">Add Item</Button>\n          <Button variant=\"ghost\" size=\"base\" icon={<PlusIcon />} iconPosition=\"left\">Add Item</Button>\n          <Button variant=\"ghost\" size=\"md\" icon={<PlusIcon />} iconPosition=\"left\">Add Item</Button>\n          <Button variant=\"ghost\" size=\"lg\" icon={<PlusIcon />} iconPosition=\"left\">Add Item</Button>\n        </div>\n      </div>\n    </div>\n  )\n};\n\nexport const WithIconRight = {\n  render: () => (\n    <div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>\n      <div>\n        <h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Filled</h3>\n        <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>\n          <Button variant=\"filled\" size=\"xs\" icon={<SendIcon />} iconPosition=\"right\">Send</Button>\n          <Button variant=\"filled\" size=\"sm\" icon={<SendIcon />} iconPosition=\"right\">Send</Button>\n          <Button variant=\"filled\" size=\"base\" icon={<SendIcon />} iconPosition=\"right\">Send</Button>\n          <Button variant=\"filled\" size=\"md\" icon={<SendIcon />} iconPosition=\"right\">Send</Button>\n          <Button variant=\"filled\" size=\"lg\" icon={<SendIcon />} iconPosition=\"right\">Send</Button>\n        </div>\n      </div>\n      <div>\n        <h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Outline</h3>\n        <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>\n          <Button variant=\"outline\" size=\"xs\" icon={<SendIcon />} iconPosition=\"right\">Send</Button>\n          <Button variant=\"outline\" size=\"sm\" icon={<SendIcon />} iconPosition=\"right\">Send</Button>\n          <Button variant=\"outline\" size=\"base\" icon={<SendIcon />} iconPosition=\"right\">Send</Button>\n          <Button variant=\"outline\" size=\"md\" icon={<SendIcon />} iconPosition=\"right\">Send</Button>\n          <Button variant=\"outline\" size=\"lg\" icon={<SendIcon />} iconPosition=\"right\">Send</Button>\n        </div>\n      </div>\n      <div>\n        <h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Ghost</h3>\n        <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>\n          <Button variant=\"ghost\" size=\"xs\" icon={<SendIcon />} iconPosition=\"right\">Send</Button>\n          <Button variant=\"ghost\" size=\"sm\" icon={<SendIcon />} iconPosition=\"right\">Send</Button>\n          <Button variant=\"ghost\" size=\"base\" icon={<SendIcon />} iconPosition=\"right\">Send</Button>\n          <Button variant=\"ghost\" size=\"md\" icon={<SendIcon />} iconPosition=\"right\">Send</Button>\n          <Button variant=\"ghost\" size=\"lg\" icon={<SendIcon />} iconPosition=\"right\">Send</Button>\n        </div>\n      </div>\n    </div>\n  )\n};\n\nexport const IconOnly = {\n  args: {\n    icon: <PlusIcon />,\n    rounded: 'full',\n    size: 'base'\n  }\n};\n\n// States\nexport const Disabled = {\n  render: () => (\n    <div style={{ display: 'flex', gap: '48px' }}>\n      <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>\n        <h3 style={{ marginBottom: '4px', fontSize: '14px', fontWeight: 600 }}>Enabled</h3>\n        <Button variant=\"filled\" color=\"primary\">Primary</Button>\n        <Button variant=\"filled\" color=\"secondary\">Secondary</Button>\n        <Button variant=\"filled\" color=\"success\">Success</Button>\n        <Button variant=\"filled\" color=\"warning\">Warning</Button>\n        <Button variant=\"filled\" color=\"danger\">Danger</Button>\n        <Button variant=\"outline\" color=\"primary\">Outline</Button>\n        <Button variant=\"ghost\" color=\"primary\">Ghost</Button>\n      </div>\n      <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>\n        <h3 style={{ marginBottom: '4px', fontSize: '14px', fontWeight: 600 }}>Disabled</h3>\n        <Button variant=\"filled\" color=\"primary\" disabled>Primary</Button>\n        <Button variant=\"filled\" color=\"secondary\" disabled>Secondary</Button>\n        <Button variant=\"filled\" color=\"success\" disabled>Success</Button>\n        <Button variant=\"filled\" color=\"warning\" disabled>Warning</Button>\n        <Button variant=\"filled\" color=\"danger\" disabled>Danger</Button>\n        <Button variant=\"outline\" color=\"primary\" disabled>Outline</Button>\n        <Button variant=\"ghost\" color=\"primary\" disabled>Ghost</Button>\n      </div>\n    </div>\n  )\n};\n\nexport const Loading = {\n  render: () => (\n    <div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>\n      <div>\n        <h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Filled</h3>\n        <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>\n          <Button variant=\"filled\" size=\"xs\" loading>Loading</Button>\n          <Button variant=\"filled\" size=\"sm\" loading>Loading</Button>\n          <Button variant=\"filled\" size=\"base\" loading>Loading</Button>\n          <Button variant=\"filled\" size=\"md\" loading>Loading</Button>\n          <Button variant=\"filled\" size=\"lg\" loading>Loading</Button>\n        </div>\n      </div>\n      <div>\n        <h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Outline</h3>\n        <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>\n          <Button variant=\"outline\" size=\"xs\" loading>Loading</Button>\n          <Button variant=\"outline\" size=\"sm\" loading>Loading</Button>\n          <Button variant=\"outline\" size=\"base\" loading>Loading</Button>\n          <Button variant=\"outline\" size=\"md\" loading>Loading</Button>\n          <Button variant=\"outline\" size=\"lg\" loading>Loading</Button>\n        </div>\n      </div>\n      <div>\n        <h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Ghost</h3>\n        <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>\n          <Button variant=\"ghost\" size=\"xs\" loading>Loading</Button>\n          <Button variant=\"ghost\" size=\"sm\" loading>Loading</Button>\n          <Button variant=\"ghost\" size=\"base\" loading>Loading</Button>\n          <Button variant=\"ghost\" size=\"md\" loading>Loading</Button>\n          <Button variant=\"ghost\" size=\"lg\" loading>Loading</Button>\n        </div>\n      </div>\n    </div>\n  )\n};\n\n// Rounded variants\nexport const Rounded = {\n  render: () => (\n    <div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>\n      <Button rounded=\"sm\">Small</Button>\n      <Button rounded=\"base\">Base</Button>\n      <Button rounded=\"md\">Medium</Button>\n      <Button rounded=\"lg\">Large</Button>\n      <Button rounded=\"full\">Full</Button>\n    </div>\n  )\n};\n\n// Full Width\nexport const FullWidth = {\n  args: {\n    children: 'Full Width Button',\n    fullWidth: true\n  },\n  decorators: [\n    (Story) => (\n      <div style={{ width: '300px' }}>\n        <Story />\n      </div>\n    )\n  ]\n};\n\n// Combined Examples\nexport const DangerWithIcon = {\n  args: {\n    children: 'Delete',\n    variant: 'filled',\n    color: 'danger',\n    icon: <TrashIcon />\n  }\n};\n\n// All Colors Showcase\nexport const AllColors = {\n  render: () => (\n    <div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>\n      <div>\n        <h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Filled Variant</h3>\n        <div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>\n          <Button variant=\"filled\" color=\"primary\">Primary</Button>\n          <Button variant=\"filled\" color=\"secondary\">Secondary</Button>\n          <Button variant=\"filled\" color=\"success\">Success</Button>\n          <Button variant=\"filled\" color=\"warning\">Warning</Button>\n          <Button variant=\"filled\" color=\"danger\">Danger</Button>\n        </div>\n      </div>\n      <div>\n        <h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Outline Variant</h3>\n        <div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>\n          <Button variant=\"outline\" color=\"primary\">Primary</Button>\n          <Button variant=\"outline\" color=\"secondary\">Secondary</Button>\n          <Button variant=\"outline\" color=\"success\">Success</Button>\n          <Button variant=\"outline\" color=\"warning\">Warning</Button>\n          <Button variant=\"outline\" color=\"danger\">Danger</Button>\n        </div>\n      </div>\n      <div>\n        <h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Ghost Variant</h3>\n        <div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>\n          <Button variant=\"ghost\" color=\"primary\">Primary</Button>\n          <Button variant=\"ghost\" color=\"secondary\">Secondary</Button>\n          <Button variant=\"ghost\" color=\"success\">Success</Button>\n          <Button variant=\"ghost\" color=\"warning\">Warning</Button>\n          <Button variant=\"ghost\" color=\"danger\">Danger</Button>\n        </div>\n      </div>\n    </div>\n  )\n};\n\n// All Sizes Showcase\nexport const AllSizes = {\n  render: () => (\n    <div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>\n      <div>\n        <h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Filled</h3>\n        <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>\n          <Button variant=\"filled\" size=\"xs\">Extra Small</Button>\n          <Button variant=\"filled\" size=\"sm\">Small</Button>\n          <Button variant=\"filled\" size=\"base\">Base</Button>\n          <Button variant=\"filled\" size=\"md\">Medium</Button>\n          <Button variant=\"filled\" size=\"lg\">Large</Button>\n        </div>\n      </div>\n      <div>\n        <h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Outline</h3>\n        <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>\n          <Button variant=\"outline\" size=\"xs\">Extra Small</Button>\n          <Button variant=\"outline\" size=\"sm\">Small</Button>\n          <Button variant=\"outline\" size=\"base\">Base</Button>\n          <Button variant=\"outline\" size=\"md\">Medium</Button>\n          <Button variant=\"outline\" size=\"lg\">Large</Button>\n        </div>\n      </div>\n      <div>\n        <h3 style={{ marginBottom: '12px', fontSize: '14px', fontWeight: 600 }}>Ghost</h3>\n        <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>\n          <Button variant=\"ghost\" size=\"xs\">Extra Small</Button>\n          <Button variant=\"ghost\" size=\"sm\">Small</Button>\n          <Button variant=\"ghost\" size=\"base\">Base</Button>\n          <Button variant=\"ghost\" size=\"md\">Medium</Button>\n          <Button variant=\"ghost\" size=\"lg\">Large</Button>\n        </div>\n      </div>\n    </div>\n  )\n};\n"
  },
  {
    "path": "packages/bruno-app/src/ui/Button/StyledWrapper.js",
    "content": "import styled, { css, keyframes } from 'styled-components';\nimport { darken, rgba } from 'polished';\n\nconst spin = keyframes`\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n`;\n\nconst sizeStyles = {\n  xs: css`\n    padding: 0.25rem 0.5rem;\n    font-size: ${(props) => props.theme.font.size.xs};\n    gap: 0.25rem;\n\n    .button-icon {\n      width: 0.75rem;\n      height: 0.75rem;\n    }\n\n    .spinner-icon {\n      width: 0.75rem;\n      height: 0.75rem;\n    }\n  `,\n  sm: css`\n    padding: 0.375rem 0.75rem;\n    font-size: ${(props) => props.theme.font.size.sm};\n    gap: 0.375rem;\n\n    .button-icon {\n      width: 0.875rem;\n      height: 0.875rem;\n    }\n\n    .spinner-icon {\n      width: 0.875rem;\n      height: 0.875rem;\n    }\n  `,\n  base: css`\n    padding: 0.5rem 1rem;\n    font-size: ${(props) => props.theme.font.size.sm};\n    gap: 0.5rem;\n\n    .button-icon {\n      width: 1rem;\n      height: 1rem;\n    }\n\n    .spinner-icon {\n      width: 1rem;\n      height: 1rem;\n    }\n  `,\n  md: css`\n    padding: 0.625rem 1.125rem;\n    font-size: ${(props) => props.theme.font.size.sm};\n    gap: 0.5rem;\n\n    .button-icon {\n      width: 1rem;\n      height: 1rem;\n    }\n\n    .spinner-icon {\n      width: 1rem;\n      height: 1rem;\n    }\n  `,\n  lg: css`\n    padding: 0.75rem 1.5rem;\n    font-size: ${(props) => props.theme.font.size.base};\n    gap: 0.75rem;\n\n    .button-icon {\n      width: 1.125rem;\n      height: 1.125rem;\n    }\n\n    .spinner-icon {\n      width: 1.125rem;\n      height: 1.125rem;\n    }\n  `\n};\n\nconst roundedStyles = {\n  sm: css`\n    border-radius: ${(props) => props.theme.border.radius.sm};\n  `,\n  base: css`\n    border-radius: ${(props) => props.theme.border.radius.base};\n  `,\n  md: css`\n    border-radius: ${(props) => props.theme.border.radius.md};\n  `,\n  lg: css`\n    border-radius: ${(props) => props.theme.border.radius.lg};\n  `,\n  full: css`\n    border-radius: 9999px;\n  `\n};\n\nconst fontWeightStyles = {\n  regular: 400,\n  medium: 500\n};\n\n// For secondary, use text color for outline/ghost; for others, use bg\nconst getDisplayColor = (colorConfig, colorName) => {\n  return colorName === 'secondary' ? colorConfig.text : colorConfig.bg;\n};\n\nconst getVariantStyles = (variant, color) => {\n  if (variant === 'filled') {\n    return css`\n      background-color: ${(props) => props.theme.button2.color[color].bg};\n      color: ${(props) => props.theme.button2.color[color].text};\n      border: 1px solid ${(props) => props.theme.button2.color[color].border};\n\n      &:disabled {\n        color: ${(props) => props.theme.button2.color[color].text} !important;\n      }\n\n      &:hover:not(:disabled) {\n        ${(props) => {\n          return css`\n            background-color: ${darken(0.03, props.theme.button2.color[color].bg)};\n            border-color: ${darken(0.03, props.theme.button2.color[color].border)};\n          `;\n        }}\n      }\n\n      &:active:not(:disabled) {\n        ${(props) => {\n          const bg = props.theme.button2.color[color].bg;\n          return css`\n            background-color: ${darken(0.07, bg)};\n          `;\n        }}\n      }\n    `;\n  }\n\n  if (variant === 'outline') {\n    return css`\n      background-color: transparent;\n      color: ${(props) => getDisplayColor(props.theme.button2.color[color], color)};\n      border: 1px solid ${(props) => getDisplayColor(props.theme.button2.color[color], color)};\n\n      &:hover:not(:disabled) {\n        ${(props) => {\n          const displayColor = getDisplayColor(props.theme.button2.color[color], color);\n          return css`\n            background-color: ${rgba(displayColor, 0.05)};\n          `;\n        }}\n      }\n\n      &:active:not(:disabled) {\n        ${(props) => {\n          const displayColor = getDisplayColor(props.theme.button2.color[color], color);\n          return css`\n            background-color: ${rgba(displayColor, 0.1)};\n          `;\n        }}\n      }\n    `;\n  }\n\n  if (variant === 'ghost') {\n    return css`\n      background-color: transparent;\n      color: ${(props) => getDisplayColor(props.theme.button2.color[color], color)};\n      border: 1px solid transparent;\n\n      &:hover:not(:disabled) {\n        ${(props) => {\n          const displayColor = getDisplayColor(props.theme.button2.color[color], color);\n          return css`\n            background-color: ${rgba(displayColor, 0.1)};\n          `;\n        }}\n      }\n\n      &:active:not(:disabled) {\n        ${(props) => {\n          const displayColor = getDisplayColor(props.theme.button2.color[color], color);\n          return css`\n            background-color: ${rgba(displayColor, 0.15)};\n          `;\n        }}\n      }\n    `;\n  }\n\n  return css``;\n};\n\nconst StyledWrapper = styled.div`\n  display: ${(props) => (props.$fullWidth ? 'block' : 'inline-block')};\n  width: ${(props) => (props.$fullWidth ? '100%' : 'auto')};\n\n  button {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    width: ${(props) => (props.$fullWidth ? '100%' : 'auto')};\n    font-family: inherit;\n    font-weight: ${(props) => fontWeightStyles[props.$fontWeight] || 400};\n    line-height: 1;\n    cursor: pointer;\n    transition: all 0.15s ease;\n    outline: none;\n    white-space: nowrap;\n    user-select: none;\n\n    ${(props) => sizeStyles[props.$size]}\n    ${(props) => roundedStyles[props.$rounded]}\n    ${(props) => getVariantStyles(props.$variant, props.$color)}\n\n    &:focus-visible {\n      ${(props) => {\n        const colorConfig = props.theme.button2.color[props.$color];\n        const focusColor = props.$color === 'secondary' ? colorConfig.text : colorConfig.bg;\n        return css`\n          box-shadow: 0 0 0 2px ${rgba(focusColor, 0.4)};\n        `;\n      }}\n    }\n\n    &:disabled {\n      cursor: not-allowed;\n      pointer-events: none;\n      opacity: 0.7;\n    }\n\n    .button-content {\n      display: inline-flex;\n      align-items: center;\n    }\n\n    .button-icon {\n      display: inline-flex;\n      align-items: center;\n      justify-content: center;\n      flex-shrink: 0;\n\n      svg {\n        width: 100%;\n        height: 100%;\n      }\n    }\n\n    .button-spinner {\n      display: inline-flex;\n      align-items: center;\n      justify-content: center;\n\n      .spinner-icon {\n        animation: ${spin} 0.75s linear infinite;\n      }\n    }\n\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/ui/Button/index.js",
    "content": "import React from 'react';\nimport StyledWrapper from './StyledWrapper';\n\nconst Button = ({\n  children,\n  size = 'base',\n  variant = 'filled',\n  color = 'primary',\n  disabled = false,\n  loading = false,\n  icon,\n  iconPosition = 'left',\n  fullWidth = false,\n  type = 'button',\n  rounded = 'base',\n  fontWeight,\n  onClick,\n  onDoubleClick,\n  className = '',\n  ...rest\n}) => {\n  const handleClick = (e) => {\n    if (disabled || loading) return;\n    onClick?.(e);\n  };\n\n  const handleDoubleClick = (e) => {\n    if (disabled || loading) return;\n    onDoubleClick?.(e);\n  };\n\n  return (\n    <StyledWrapper\n      $size={size}\n      $variant={variant}\n      $color={color}\n      $disabled={disabled}\n      $loading={loading}\n      $fullWidth={fullWidth}\n      $rounded={rounded}\n      $fontWeight={fontWeight}\n      $hasIcon={!!icon}\n      $iconPosition={iconPosition}\n      className={className}\n    >\n      <button\n        type={type}\n        disabled={disabled || loading}\n        onClick={handleClick}\n        onDoubleClick={handleDoubleClick}\n        {...rest}\n      >\n        {loading && (\n          <span className=\"button-spinner\">\n            <svg className=\"spinner-icon\" viewBox=\"0 0 24 24\">\n              <circle\n                cx=\"12\"\n                cy=\"12\"\n                r=\"10\"\n                stroke=\"currentColor\"\n                strokeWidth=\"3\"\n                fill=\"none\"\n                strokeLinecap=\"round\"\n                strokeDasharray=\"31.4 31.4\"\n              />\n            </svg>\n          </span>\n        )}\n        {icon && iconPosition === 'left' && !loading && (\n          <span className=\"button-icon button-icon-left\">{icon}</span>\n        )}\n        {children && <span className=\"button-content\">{children}</span>}\n        {icon && iconPosition === 'right' && !loading && (\n          <span className=\"button-icon button-icon-right\">{icon}</span>\n        )}\n      </button>\n    </StyledWrapper>\n  );\n};\n\nexport default Button;\n"
  },
  {
    "path": "packages/bruno-app/src/ui/ErrorBanner/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  max-height: 200px;\n  min-height: 70px;\n  overflow-y: auto;\n  background-color: ${(props) => props.theme.background.base};\n  border: solid 1px ${(props) => props.theme.border.border2};\n  border-left: 4px solid ${(props) => props.theme.colors.text.danger};\n  border-radius: ${(props) => props.theme.border.radius.base};\n  \n  .close-button {\n    opacity: 0.7;\n    transition: opacity 0.2s;\n    \n    &:hover {\n      opacity: 1;\n    }\n    \n    svg {\n      color: ${(props) => props.theme.text};\n    }\n  }\n  \n  .error-title {\n    font-weight: 500;\n    margin-bottom: 0.375rem;\n    color: ${(props) => props.theme.colors.text.danger};\n  }\n  \n  .error-message {\n    font-family: monospace;\n    font-size: ${(props) => props.theme.font.size.xs};\n    line-height: 1.25rem;\n    white-space: pre-wrap;\n    word-break: break-all;\n    color: ${(props) => props.theme.text};\n  }\n\n  .separator {\n    border-top: 1px solid ${(props) => props.theme.border.border1};\n  }\n`;\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/ui/ErrorBanner/index.js",
    "content": "import React from 'react';\nimport { IconX } from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\n\nconst ErrorBanner = ({ errors, onClose, className = '' }) => {\n  if (!errors || errors.length === 0) return null;\n\n  return (\n    <StyledWrapper className={className}>\n      <div className=\"flex items-start gap-3 px-4 py-3\">\n        <div className=\"flex-1 min-w-0\">\n          {errors.map((error, index) => (\n            <div key={index}>\n              {index > 0 && <div className=\"separator my-2\"></div>}\n              <div className=\"error-title\">\n                {error.title}\n              </div>\n              <div className=\"error-message\">\n                {error.message}\n              </div>\n            </div>\n          ))}\n        </div>\n        {onClose && (\n          <div\n            className=\"close-button flex-shrink-0 cursor-pointer\"\n            onClick={onClose}\n          >\n            <IconX size={16} strokeWidth={1.5} />\n          </div>\n        )}\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default ErrorBanner;\n"
  },
  {
    "path": "packages/bruno-app/src/ui/HeightBoundContainer/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  /* Primary container - establishes flex context */\n  display: flex;\n  flex-direction: column;\n  width: 100%;\n  height: 100%;\n\n  /* Flex shrink container - allows content to be constrained */\n  .height-constraint {\n    display: flex;\n    flex: 1 1 0;\n    min-height: 0;\n  }\n\n  /* flex container - enforces boundaries */\n  .flex-boundary {\n    width: 100%;\n    min-width: 0;\n    display: flex;\n    flex-direction: column;\n    overflow-y: auto;\n\n    > * {\n      flex: 1 1 0;\n      min-height: 0;\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/ui/HeightBoundContainer/index.js",
    "content": "import React from 'react';\nimport StyledWrapper from './StyledWrapper';\n\nconst HeightBoundContainer = ({ children, className }) => {\n  return (\n    <StyledWrapper className={className}>\n      <div className=\"height-constraint\">\n        <div className=\"flex-boundary\">\n          {children}\n        </div>\n      </div>\n    </StyledWrapper>\n  );\n};\n\nexport default HeightBoundContainer;\n"
  },
  {
    "path": "packages/bruno-app/src/ui/MenuDropdown/SubMenuItem/index.js",
    "content": "import React, { useState } from 'react';\nimport { IconChevronRight, IconChevronLeft } from '@tabler/icons';\n\nconst SubMenuItem = ({\n  item,\n  onRootClose,\n  submenuPlacement,\n  getMenuItemProps,\n  renderMenuItemContent,\n  MenuDropdownComponent\n}) => {\n  const [submenuOpen, setSubmenuOpen] = useState(false);\n  const isLeftPlacement = submenuPlacement === 'left';\n  const submenuTippyPlacement = isLeftPlacement ? 'left-start' : 'right-start';\n  const ArrowIcon = isLeftPlacement ? IconChevronLeft : IconChevronRight;\n\n  const submenuItemsWithClose = item.submenu.map((subItem) => {\n    if (subItem.type === 'divider') return subItem;\n    return {\n      ...subItem,\n      onClick: () => {\n        subItem.onClick?.();\n        onRootClose();\n      }\n    };\n  });\n\n  const itemProps = getMenuItemProps(item, {\n    'className': 'has-submenu',\n    'aria-haspopup': 'true',\n    'aria-expanded': submenuOpen,\n    'aria-current': undefined // submenu triggers don't need aria-current\n  });\n\n  const arrowElement = (\n    <span className=\"submenu-arrow\">\n      <ArrowIcon size={14} />\n    </span>\n  );\n\n  return (\n    <div\n      className=\"submenu-trigger\"\n      onMouseEnter={() => setSubmenuOpen(true)}\n      onMouseLeave={() => setSubmenuOpen(false)}\n    >\n      <MenuDropdownComponent\n        items={submenuItemsWithClose}\n        placement={submenuTippyPlacement}\n        opened={submenuOpen}\n        onChange={setSubmenuOpen}\n        showTickMark={false}\n        submenuPlacement={submenuPlacement}\n        appendTo={() => document.body}\n        offset={[0, 0]}\n      >\n        <div {...itemProps}>\n          {renderMenuItemContent(item, arrowElement)}\n        </div>\n      </MenuDropdownComponent>\n    </div>\n  );\n};\n\nexport default SubMenuItem;\n"
  },
  {
    "path": "packages/bruno-app/src/ui/MenuDropdown/index.js",
    "content": "import React, { forwardRef, useRef, useCallback, useState, useImperativeHandle, useEffect, useMemo } from 'react';\nimport Dropdown from 'components/Dropdown';\nimport SubMenuItem from './SubMenuItem';\n\n// Constants\nconst NAVIGATION_KEYS = ['ArrowDown', 'ArrowUp', 'Home', 'End', 'Escape'];\nconst ACTION_KEYS = ['Enter', ' '];\n\n// Calculate next index for keyboard navigation\nconst getNextIndex = (currentIndex, total, key, noFocus) => {\n  if (key === 'Home') return 0;\n  if (key === 'End') return total - 1;\n  if (key === 'ArrowDown') return noFocus ? 0 : (currentIndex + 1) % total;\n  if (key === 'ArrowUp') return noFocus ? total - 1 : (currentIndex - 1 + total) % total;\n  return currentIndex;\n};\n\n/**\n * MenuDropdown - A reusable dropdown menu component with keyboard navigation\n *\n * @param {Object} props\n * @param {Array} props.items - Array of menu items. Supports multiple formats:\n *   Standard format (MenuDropdown items):\n *   - id: string (unique identifier)\n *   - type: 'item' | 'label' | 'divider' (default: 'item')\n *   - leftSection: React component or React element (rendered on the left side, for items only)\n *   - rightSection: React component or React element (rendered on the right side, for items only)\n *   - label: string (display text for items, or label text for labels; also used for aria-label and title if not provided)\n *   - ariaLabel: string (accessibility label, falls back to label or title if not provided)\n *   - onClick: function (handler when item is clicked, for items only)\n *   - title: string (tooltip text, falls back to label or ariaLabel if not provided)\n *   - testId: string (optional, for testing, for items only)\n *   - disabled: boolean (optional, for items only)\n *   - className: string (optional, additional CSS classes for the item)\n *   - submenu: Array (optional, array of menu items for nested submenu, opens on hover)\n *\n *   Grouped format: [{name: string, options: [{id, label, ...}]}, ...]\n *   Flat format: [{id, label, ...}, ...]\n * @param {ReactNode} props.children - The trigger element (button, etc.)\n * @param {string} props.placement - Tippy placement (default: 'bottom-end')\n * @param {string} props.className - Optional className for the dropdown\n * @param {string} props.selectedItemId - Optional ID of the selected/active item to focus on open\n * @param {boolean} props.opened - Controlled open state (when provided, component is controlled)\n * @param {function} props.onChange - Callback when dropdown state changes: (opened: boolean) => void\n * @param {ReactNode} props.header - Optional header content to render above menu items\n * @param {ReactNode} props.footer - Optional footer content to render below menu items\n * @param {boolean} props.showTickMark - Optional flag to show checkmark (✓) on selected items (default: true)\n * @param {boolean} props.showGroupDividers - Optional flag to show dividers between groups in grouped format (default: true)\n * @param {string} props.groupStyle - Style for grouped items: 'action' (default, normal case) or 'select' (uppercase labels, indented items)\n * @param {boolean} props.autoFocusFirstOption - Optional flag to auto-focus first option when dropdown opens (default: false)\n * @param {string} props.submenuPlacement - Placement of submenus: 'right' (default) or 'left'. Controls both position and arrow direction.\n * @param {Object} props.dropdownProps - Other props passed to underlying Dropdown component\n * @param {React.Ref} ref - Optional ref to expose open/close methods\n */\nconst MenuDropdown = forwardRef(({\n  items = [],\n  children,\n  placement = 'bottom-end',\n  className,\n  selectedItemId,\n  opened,\n  onChange,\n  header,\n  footer,\n  showTickMark = true,\n  showGroupDividers = true,\n  groupStyle = 'action',\n  autoFocusFirstOption = false,\n  submenuPlacement = 'right',\n  'data-testid': testId = 'menu-dropdown',\n  ...dropdownProps\n}, ref) => {\n  const tippyRef = useRef();\n  const selectedItemIdRef = useRef(selectedItemId);\n  const autoFocusFirstOptionRef = useRef(autoFocusFirstOption);\n  const [internalIsOpen, setInternalIsOpen] = useState(false);\n\n  // Keep refs in sync\n  useEffect(() => {\n    selectedItemIdRef.current = selectedItemId;\n  }, [selectedItemId]);\n\n  useEffect(() => {\n    autoFocusFirstOptionRef.current = autoFocusFirstOption;\n  }, [autoFocusFirstOption]);\n\n  // Determine if component is controlled\n  const isControlled = opened !== undefined;\n\n  // Use controlled state if provided, otherwise use internal state\n  const isOpen = isControlled ? opened : internalIsOpen;\n\n  // Get all focusable menu items from the menu dropdown\n  const getMenuItems = useCallback(() => {\n    const popper = tippyRef.current?.popper;\n    if (!popper) return [];\n\n    const menuContainer = popper.querySelector('[role=\"menu\"]');\n    if (!menuContainer) return [];\n\n    return Array.from(\n      menuContainer.querySelectorAll('[role=\"menuitem\"]:not([aria-disabled=\"true\"])')\n    );\n  }, []);\n\n  // Update state (respects controlled vs uncontrolled mode)\n  const updateOpenState = useCallback((newState) => {\n    if (isControlled) {\n      onChange?.(newState);\n    } else {\n      setInternalIsOpen(newState);\n    }\n  }, [isControlled, onChange]);\n\n  // Handle item click and close dropdown\n  const handleItemClick = useCallback((item) => {\n    if (item.disabled) return;\n    item.onClick?.();\n    updateOpenState(false);\n  }, [updateOpenState]);\n\n  // Convert legacy formats (grouped or flat) to standard MenuDropdown items format\n  const normalizeItems = useCallback((itemsToNormalize) => {\n    if (!Array.isArray(itemsToNormalize) || itemsToNormalize.length === 0) {\n      return [];\n    }\n\n    // Check if it's a grouped format: [{options: [{value, label, ...}]}, ...]\n    const firstItem = itemsToNormalize[0];\n    const isGrouped = firstItem != null && typeof firstItem === 'object' && 'options' in firstItem;\n\n    if (isGrouped) {\n      const result = [];\n      itemsToNormalize.forEach((group, groupIndex) => {\n        // Add divider before each group except the first (if showGroupDividers is true)\n        if (groupIndex > 0 && showGroupDividers) {\n          result.push({ type: 'divider', id: `divider-${groupIndex}` });\n        }\n\n        // Add group name as label\n        if (group.name) {\n          const normalizeGroupNameForId = (group.name || '').toLowerCase().replace(/ /g, '-');\n          result.push({ type: 'label', id: `label-${normalizeGroupNameForId}-${groupIndex}`, label: group.name, groupStyle });\n        }\n\n        // Convert group options to menu items\n        group.options.forEach((option) => {\n          result.push({\n            id: option.id,\n            label: option.label,\n            type: 'item',\n            onClick: option.onClick,\n            disabled: option.disabled,\n            className: option.className,\n            leftSection: option.leftSection,\n            rightSection: option.rightSection,\n            ariaLabel: option.ariaLabel,\n            title: option.title,\n            groupStyle: groupStyle\n          });\n        });\n      });\n      return result;\n    }\n\n    // Already in standard format, return as-is\n    return itemsToNormalize;\n  }, [showGroupDividers, groupStyle]);\n\n  // Normalize items to standard format\n  const normalizedItems = useMemo(() => normalizeItems(items), [items, normalizeItems]);\n\n  // Enhance items with tick mark for selected item if showTickMark is enabled\n  const enhancedItems = useMemo(() => {\n    if (!showTickMark || selectedItemId == null) {\n      return normalizedItems;\n    }\n\n    return normalizedItems.map((item) => {\n      // Skip non-item types (dividers, labels)\n      if (item.type && item.type !== 'item') {\n        return item;\n      }\n\n      const isSelected = item.id === selectedItemId;\n\n      // Only add tick mark if item is selected and doesn't already have a rightSection\n      if (isSelected && !item.rightSection) {\n        return {\n          ...item,\n          rightSection: <span className=\"ml-auto\">✓</span>\n        };\n      }\n\n      return item;\n    });\n  }, [normalizedItems, showTickMark, selectedItemId]);\n\n  // Clear focused class from all items\n  const clearFocusedClass = (menuContainer) => {\n    if (menuContainer) {\n      menuContainer.querySelectorAll('.dropdown-item-focused').forEach((el) => {\n        el.classList.remove('dropdown-item-focused');\n      });\n    }\n  };\n\n  // Focus a menu item\n  const focusMenuItem = (item, addFocusedClass = true) => {\n    if (item) {\n      // Remove focused class from all items first\n      const menuContainer = item.closest('[role=\"menu\"]');\n      clearFocusedClass(menuContainer);\n\n      if (addFocusedClass) {\n        item.classList.add('dropdown-item-focused');\n      }\n      item.focus();\n      // scrollIntoView may not be available in test environments (jsdom)\n      if (typeof item.scrollIntoView === 'function') {\n        item.scrollIntoView({ block: 'nearest' });\n      }\n    }\n  };\n\n  // Keyboard navigation handler (handles all keyboard events at menu level)\n  const handleMenuKeyDown = useCallback((e) => {\n    const itemsToNavigate = getMenuItems();\n    if (itemsToNavigate.length === 0) return;\n\n    const currentIndex = itemsToNavigate.findIndex((el) => el === document.activeElement);\n    const isNoMenuItemFocused = currentIndex === -1;\n\n    // Handle Escape\n    if (e.key === 'Escape') {\n      e.preventDefault();\n      e.stopPropagation();\n      updateOpenState(false);\n      return;\n    }\n\n    // Handle action keys (Enter, Space)\n    if (ACTION_KEYS.includes(e.key) && !isNoMenuItemFocused) {\n      e.preventDefault();\n      e.stopPropagation();\n      const currentItem = itemsToNavigate[currentIndex];\n      const itemId = currentItem?.getAttribute('data-item-id');\n      // Use enhancedItems for finding the item\n      const item = enhancedItems.find((i) => i.id === itemId);\n      if (item && !item.disabled) {\n        handleItemClick(item);\n      }\n      return;\n    }\n\n    // Handle navigation keys\n    if (NAVIGATION_KEYS.includes(e.key)) {\n      e.preventDefault();\n      e.stopPropagation();\n      const nextIndex = getNextIndex(currentIndex, itemsToNavigate.length, e.key, isNoMenuItemFocused);\n      focusMenuItem(itemsToNavigate[nextIndex], true);\n    }\n  }, [getMenuItems, enhancedItems, handleItemClick, updateOpenState]);\n\n  // Toggle dropdown visibility\n  const handleTriggerClick = useCallback(() => {\n    updateOpenState(!isOpen);\n  }, [isOpen, updateOpenState]);\n\n  // Close dropdown when clicking outside\n  const handleClickOutside = useCallback((instance, event) => {\n    // Don't close if clicking inside a submenu (another tippy popper)\n    if (event?.target?.closest?.('[data-tippy-root]')) {\n      return;\n    }\n    updateOpenState(false);\n  }, [updateOpenState]);\n\n  // Expose imperative methods via ref\n  useImperativeHandle(ref, () => ({\n    show: () => {\n      updateOpenState(true);\n    },\n    hide: () => {\n      updateOpenState(false);\n    },\n    toggle: () => {\n      updateOpenState(!isOpen);\n    }\n  }), [updateOpenState, isOpen]);\n\n  // Setup Tippy instance\n  const onDropdownCreate = useCallback((ref) => {\n    tippyRef.current = ref;\n    if (ref) {\n      ref.setProps({\n        onShow: () => {\n          // Focus selected item if available, otherwise focus menu container\n          // Use requestAnimationFrame to ensure DOM is ready\n          requestAnimationFrame(() => {\n            const menuContainer = ref.popper?.querySelector('[role=\"menu\"]');\n            if (!menuContainer) return;\n\n            const menuItems = Array.from(\n              menuContainer.querySelectorAll('[role=\"menuitem\"]:not([aria-disabled=\"true\"])')\n            );\n\n            // If selectedItemId is provided, find and focus that item\n            // Use ref to get the latest value\n            const currentSelectedItemId = selectedItemIdRef.current;\n            if (currentSelectedItemId != null) {\n              // Convert to string for comparison since data attributes are always strings\n              const selectedItemIdStr = String(currentSelectedItemId);\n              const selectedItem = menuItems.find(\n                (item) => item.getAttribute('data-item-id') === selectedItemIdStr\n              );\n\n              if (selectedItem) {\n                focusMenuItem(selectedItem, true);\n                return;\n              }\n            }\n\n            // If autoFocusFirstOption is true, focus the first item\n            if (autoFocusFirstOptionRef.current && menuItems.length > 0) {\n              focusMenuItem(menuItems[0], true);\n              return;\n            }\n\n            // Fallback: focus menu container\n            menuContainer.focus();\n          });\n        },\n        onHide: () => {\n          // Clear focused class when dropdown closes\n          const menuContainer = ref.popper?.querySelector('[role=\"menu\"]');\n          clearFocusedClass(menuContainer);\n        }\n      });\n    }\n  }, []);\n\n  // Render section (left or right)\n  const renderSection = (section) => {\n    if (!section) return null;\n\n    // If it's a React component (function), render it with default icon props\n    if (typeof section === 'function') {\n      const SectionComponent = section;\n      return <SectionComponent size={16} stroke={1.5} className=\"dropdown-icon\" aria-hidden=\"true\" />;\n    }\n\n    // If it's already a React element, render it as-is\n    return section;\n  };\n\n  // Get common props for menu items (shared between regular items and submenu triggers)\n  const getMenuItemProps = (item, extraProps = {}) => {\n    const selectIndentClass = item.groupStyle === 'select' ? 'dropdown-item-select' : '';\n    const isActive = item.id === selectedItemId;\n    const activeClass = isActive ? 'dropdown-item-active' : '';\n\n    // Destructure className from extraProps to avoid it being overwritten by spread\n    const { className: extraClassName, ...restExtraProps } = extraProps;\n\n    return {\n      'className': `dropdown-item ${item.disabled ? 'disabled' : ''} ${selectIndentClass} ${activeClass} ${extraClassName || ''} ${item.className || ''}`.trim(),\n      'role': 'menuitem',\n      'data-item-id': item.id,\n      'tabIndex': item.disabled ? -1 : 0,\n      'aria-label': item.ariaLabel,\n      'aria-disabled': item.disabled,\n      'aria-current': isActive ? 'true' : undefined,\n      'title': item.title,\n      'data-testid': `${testId}-${String(item.id).toLowerCase()}`,\n      ...restExtraProps\n    };\n  };\n\n  // Render the content inside a menu item (leftSection, label, and rightSection/arrow)\n  const renderMenuItemContent = (item, rightContent = null) => (\n    <>\n      {renderSection(item.leftSection)}\n      <span className=\"dropdown-label\">{item.label}</span>\n      {rightContent}\n    </>\n  );\n\n  // Render menu item\n  const renderMenuItem = (item) => {\n    if (item.submenu) {\n      return (\n        <SubMenuItem\n          key={item.id}\n          item={item}\n          onRootClose={() => updateOpenState(false)}\n          submenuPlacement={submenuPlacement}\n          getMenuItemProps={getMenuItemProps}\n          renderMenuItemContent={renderMenuItemContent}\n          MenuDropdownComponent={MenuDropdown}\n        />\n      );\n    }\n\n    const itemProps = getMenuItemProps(item);\n\n    const rightContent = item.rightSection ? (\n      <div\n        className=\"dropdown-right-section\"\n        onClick={(e) => {\n          e.preventDefault();\n          e.stopPropagation();\n        }}\n      >\n        {renderSection(item.rightSection)}\n      </div>\n    ) : null;\n\n    return (\n      <div\n        key={item.id}\n        {...itemProps}\n        onClick={() => !item.disabled && handleItemClick(item)}\n      >\n        {renderMenuItemContent(item, rightContent)}\n      </div>\n    );\n  };\n\n  // Render label item\n  const renderLabel = (item) => (\n    <div\n      key={item.id || `label-${item.label}`}\n      className={`label-item ${item.groupStyle === 'select' ? 'label-select' : ''}`}\n      role=\"presentation\"\n      data-testid={`${testId}-label-${(item.label || '').toLowerCase().replace(/ /g, '-')}`}\n    >\n      {item.groupStyle === 'select' ? (item.label || '').toUpperCase() : item.label || ''}\n    </div>\n  );\n\n  // Render divider item\n  const renderDivider = (item, index) => (\n    <div key={item.id || `divider-${index}`} className=\"dropdown-separator\" role=\"separator\" />\n  );\n\n  // Render menu content\n  const renderMenuContent = () => {\n    let dividerIndex = 0;\n\n    return enhancedItems.map((item) => {\n      const itemType = item.type || 'item';\n\n      if (itemType === 'label') {\n        return renderLabel(item);\n      }\n\n      if (itemType === 'divider') {\n        return renderDivider(item, dividerIndex++);\n      }\n\n      return renderMenuItem(item);\n    });\n  };\n\n  // Clone children to attach click handler and aria-expanded\n  const triggerElement = React.isValidElement(children)\n    ? React.cloneElement(children, {\n        'onClick': (e) => {\n          children.props.onClick?.(e);\n          handleTriggerClick();\n        },\n        'aria-expanded': isOpen,\n        'data-testid': testId\n      })\n    : <div onClick={handleTriggerClick} aria-expanded={isOpen} data-testid={testId}>{children}</div>;\n\n  return (\n    <Dropdown\n      onCreate={onDropdownCreate}\n      icon={triggerElement}\n      placement={placement}\n      className={className}\n      visible={isOpen}\n      onClickOutside={handleClickOutside}\n      {...dropdownProps}\n    >\n      <div {...(testId && { 'data-testid': testId + '-dropdown' })}>\n        {header && (\n          <div className=\"dropdown-header-container\" onClick={handleClickOutside}>\n            {header}\n            <div className=\"dropdown-divider\"></div>\n          </div>\n        )}\n        <div role=\"menu\" tabIndex={-1} onKeyDown={handleMenuKeyDown}>\n          {renderMenuContent()}\n        </div>\n        {footer && (\n          <>\n            <div className=\"dropdown-divider\"></div>\n            <div className=\"dropdown-footer-container\">\n              {footer}\n            </div>\n          </>\n        )}\n      </div>\n    </Dropdown>\n  );\n});\n\nexport default MenuDropdown;\n"
  },
  {
    "path": "packages/bruno-app/src/ui/MethodBadge/StyledWrapper.js",
    "content": "import styled, { css } from 'styled-components';\n\nconst methodColor = (props) => {\n  const method = props.$method;\n  return props.theme.request.methods[method] || props.theme.colors.text.muted;\n};\n\nconst sizeStyles = {\n  md: css`\n    display: inline-block;\n    font-size: ${(props) => props.theme.font.size.xs};\n    font-weight: 600;\n    text-transform: uppercase;\n    width: 52px;\n    flex-shrink: 0;\n    text-align: left;\n  `,\n  sm: css`\n    font-size: 9px;\n    font-weight: 600;\n    font-family: monospace;\n    padding: 0.05rem 0.25rem;\n    border-radius: 3px;\n    text-transform: uppercase;\n    flex-shrink: 0;\n  `\n};\n\nconst StyledWrapper = styled.span`\n  color: ${methodColor};\n  ${(props) => sizeStyles[props.$size] || sizeStyles.md}\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/ui/MethodBadge/index.js",
    "content": "import React from 'react';\nimport StyledWrapper from './StyledWrapper';\n\nconst MethodBadge = ({ method, size = 'md', className = '' }) => {\n  const normalizedMethod = method?.toLowerCase() || 'get';\n  const displayText = method?.toUpperCase() || 'GET';\n\n  return (\n    <StyledWrapper\n      $method={normalizedMethod}\n      $size={size}\n      className={className}\n    >\n      {displayText}\n    </StyledWrapper>\n  );\n};\n\nexport default MethodBadge;\n"
  },
  {
    "path": "packages/bruno-app/src/ui/ResponsiveTabs/StyledWrapper.js",
    "content": "import styled from 'styled-components';\n\nconst StyledWrapper = styled.div`\n  &.tabs {\n    overflow: hidden;\n    min-width: 0;\n\n    > div:first-child {\n      overflow: hidden;\n      min-width: 0;\n      flex-shrink: 1;\n    }\n\n    .more-tabs {\n      color: ${(props) => props.theme.colors.text.subtext0} !important;\n      border-bottom: solid 2px transparent;\n\n      &:hover {\n        color: ${(props) => props.theme.tabs.active.color} !important;\n      }\n    }\n\n    .tab {\n      display: inline-flex;\n      align-items: center;\n      gap: 0.25rem;\n      padding: 6px 0px;\n      border: none;\n      border-bottom: solid 2px transparent;\n      margin-right: ${(props) => props.theme.tabs.marginRight};\n      color: ${(props) => props.theme.colors.text.subtext0};\n      cursor: pointer;\n      white-space: nowrap;\n      vertical-align: middle;\n      flex-shrink: 0;\n      font-size: ${(props) => props.theme.font.size.sm};\n\n      &:focus,\n      &:active,\n      &:focus-within,\n      &:focus-visible,\n      &:target {\n        outline: none !important;\n        box-shadow: none !important;\n      }\n\n      &:hover {\n        color: ${(props) => props.theme.tabs.active.color} !important;\n      }\n\n      &.active {\n        font-weight: ${(props) => props.theme.tabs.active.fontWeight} !important;\n        color: ${(props) => props.theme.tabs.active.color} !important;\n        border-bottom: solid 2px ${(props) => props.theme.tabs.active.border} !important;\n      }\n\n      .content-indicator {\n        color: ${(props) => props.theme.text};\n      }\n\n      .tab-count {\n        font-size: 11px;\n        font-weight: 600;\n        min-width: 18px;\n        height: 18px;\n        display: inline-flex;\n        align-items: center;\n        justify-content: center;\n        border-radius: 9px;\n        padding: 0 5px;\n        background: ${(props) => props.theme.colors.text.muted}20;\n        color: ${(props) => props.theme.colors.text.muted};\n      }\n\n      sup {\n        display: inline-flex;\n        align-items: center;\n        line-height: 1;\n        vertical-align: baseline;\n        margin-left: 0;\n      }\n    }\n  }\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/ui/ResponsiveTabs/index.js",
    "content": "import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react';\nimport classnames from 'classnames';\nimport MenuDropdown from 'ui/MenuDropdown';\nimport { IconChevronsRight } from '@tabler/icons';\nimport StyledWrapper from './StyledWrapper';\n\nconst DROPDOWN_WIDTH = 60;\nconst CALCULATION_DELAY_DEFAULT = 20;\nconst CALCULATION_DELAY_EXTENDED = 150;\nconst GAP_BETWEEN_LEFT_AND_RIGHT_CONTENT = 80;\nconst EXPANDABLE_HYSTERESIS = 20; // Buffer to prevent flickering at boundary\n\n// Compare two key arrays for equality\nconst areKeysEqual = (prevKeys, newKeys) => {\n  if (prevKeys.length !== newKeys.length) return false;\n  return prevKeys.every((key, i) => key === newKeys[i]);\n};\n\nconst ResponsiveTabs = ({\n  tabs,\n  activeTab,\n  onTabSelect,\n  rightContent,\n  rightContentRef,\n  delayedTabs = [],\n  rightContentExpandedWidth, // Optional: width of the expandable element when expanded\n  expandableElementIndex = -1 // Optional: index of the expandable child element (-1 means last child)\n}) => {\n  const [visibleTabKeys, setVisibleTabKeys] = useState([]);\n  const [overflowTabKeys, setOverflowTabKeys] = useState([]);\n  const [rightSideExpandable, setRightSideExpandable] = useState(false);\n\n  const tabsContainerRef = useRef(null);\n  const tabRefsMap = useRef({});\n  const menuDropdownRef = useRef(null);\n\n  const handleTabSelect = useCallback(\n    (tabKey) => {\n      onTabSelect(tabKey);\n      menuDropdownRef.current?.hide();\n    },\n    [onTabSelect]\n  );\n\n  const calculateTabVisibility = useCallback(() => {\n    const container = tabsContainerRef.current;\n    if (!container || !tabs.length) return;\n\n    const containerWidth = container.offsetWidth;\n    const rightContentWidth = rightContentRef?.current?.offsetWidth + 20 || 0;\n    const availableWidth = containerWidth - rightContentWidth - DROPDOWN_WIDTH;\n\n    const visible = [];\n    const overflow = [];\n    let currentWidth = 0;\n\n    for (const tab of tabs) {\n      const tabElement = tabRefsMap.current[tab.key];\n      const tabWidth = tabElement?.offsetWidth + 20 || 100;\n\n      if (currentWidth + tabWidth <= availableWidth && !overflow.length) {\n        visible.push(tab);\n        currentWidth += tabWidth;\n      } else {\n        overflow.push(tab);\n      }\n    }\n\n    // Ensure active tab is always visible\n    if (!visible.some((t) => t.key === activeTab) && overflow.length) {\n      const activeTabIndex = overflow.findIndex((t) => t.key === activeTab);\n      if (activeTabIndex !== -1) {\n        const [activeTabItem] = overflow.splice(activeTabIndex, 1);\n        const lastVisible = visible.pop();\n        if (lastVisible) {\n          overflow.unshift(lastVisible);\n        }\n        visible.push(activeTabItem);\n      }\n    }\n\n    // Extract keys and update state only if changed (prevents infinite loops)\n    const visibleKeys = visible.map((t) => t.key);\n    const overflowKeys = overflow.map((t) => t.key);\n\n    setVisibleTabKeys((prev) => {\n      return areKeysEqual(prev, visibleKeys) ? prev : visibleKeys;\n    });\n    setOverflowTabKeys((prev) => {\n      return areKeysEqual(prev, overflowKeys) ? prev : overflowKeys;\n    });\n\n    // Only calculate expandibility if rightContentExpandedWidth is provided\n    if (rightContentExpandedWidth && rightContentRef?.current) {\n      const leftContentWidth = currentWidth + (overflow.length ? DROPDOWN_WIDTH : 0);\n\n      // Calculate total expanded width by summing children widths\n      // and replacing the expandable element's current width with its expanded width\n      const children = rightContentRef.current.children;\n      const childrenCount = children.length;\n\n      if (childrenCount > 0) {\n        // Resolve the expandable element index (-1 means last child)\n        const targetIndex = expandableElementIndex < 0 ? childrenCount + expandableElementIndex : expandableElementIndex;\n        const validTargetIndex = Math.max(0, Math.min(targetIndex, childrenCount - 1));\n\n        let totalExpandedWidth = 0;\n        for (let i = 0; i < childrenCount; i++) {\n          if (i === validTargetIndex) {\n            // Use the expanded width for the expandable element\n            totalExpandedWidth += rightContentExpandedWidth;\n          } else {\n            // Use the current width for other elements\n            totalExpandedWidth += children[i].offsetWidth;\n          }\n        }\n\n        const availableSpace = containerWidth - leftContentWidth - GAP_BETWEEN_LEFT_AND_RIGHT_CONTENT;\n\n        // Use hysteresis to prevent flickering at boundary\n        // When expanded: only collapse if significantly less space available\n        // When collapsed: expand when there's enough space\n        setRightSideExpandable((prev) => {\n          if (prev) {\n            // Currently expanded - only collapse if space drops below threshold minus hysteresis\n            return availableSpace > totalExpandedWidth - EXPANDABLE_HYSTERESIS;\n          } else {\n            // Currently collapsed - expand if there's enough space\n            return availableSpace > totalExpandedWidth;\n          }\n        });\n      }\n    }\n  }, [tabs, activeTab, rightContentRef, rightContentExpandedWidth, expandableElementIndex]);\n\n  // Recalculate on tab/activeTab changes\n  useEffect(() => {\n    const delay = delayedTabs.includes(activeTab) ? CALCULATION_DELAY_EXTENDED : CALCULATION_DELAY_DEFAULT;\n    const timeoutId = setTimeout(() => {\n      requestAnimationFrame(calculateTabVisibility);\n    }, delay);\n\n    return () => clearTimeout(timeoutId);\n  }, [calculateTabVisibility, activeTab, delayedTabs]);\n\n  // Recalculate on container resize only\n  useEffect(() => {\n    let frameId = null;\n\n    const observer = new ResizeObserver(() => {\n      if (frameId) {\n        cancelAnimationFrame(frameId);\n      }\n      frameId = requestAnimationFrame(calculateTabVisibility);\n    });\n\n    if (tabsContainerRef.current) {\n      observer.observe(tabsContainerRef.current);\n    }\n\n    return () => {\n      if (frameId) {\n        cancelAnimationFrame(frameId);\n      }\n      observer.disconnect();\n    };\n  }, [calculateTabVisibility]);\n\n  // Clean up stale refs when tabs change\n  useEffect(() => {\n    const currentKeys = new Set(tabs.map((t) => t.key));\n    for (const key of Object.keys(tabRefsMap.current)) {\n      if (!currentKeys.has(key)) {\n        delete tabRefsMap.current[key];\n      }\n    }\n  }, [tabs]);\n\n  const hiddenStyle = useMemo(\n    () => ({\n      visibility: 'hidden',\n      position: 'absolute',\n      display: 'flex',\n      pointerEvents: 'none'\n    }),\n    []\n  );\n\n  const setTabRef = useCallback((el, key) => {\n    if (el) {\n      tabRefsMap.current[key] = el;\n    }\n  }, []);\n\n  const renderTab = (tab) => {\n    const isActive = tab.key === activeTab;\n\n    return (\n      <div\n        key={tab.key}\n        role=\"tab\"\n        aria-selected={isActive}\n        className={classnames('tab select-none', tab.key, { active: isActive })}\n        onClick={() => handleTabSelect(tab.key)}\n      >\n        {tab.label}\n        {tab.indicator}\n      </div>\n    );\n  };\n\n  const rightContentClassName = classnames('flex justify-end items-center', {\n    expandable: rightSideExpandable\n  });\n\n  // Map stored keys to fresh tab objects from props (ensures indicators stay up-to-date)\n  const visibleTabs = visibleTabKeys.map((key) => tabs.find((t) => t.key === key)).filter(Boolean);\n  const overflowTabs = overflowTabKeys.map((key) => tabs.find((t) => t.key === key)).filter(Boolean);\n\n  // Convert overflow tabs to MenuDropdown items format\n  const overflowMenuItems = useMemo(() => {\n    return overflowTabs.map((tab) => ({\n      id: tab.key,\n      label: (\n        <span className=\"flex items-center gap-1\">\n          {tab.label}\n          {tab.indicator}\n        </span>\n      ),\n      ariaLabel: typeof tab.label === 'string' ? tab.label : tab.key,\n      onClick: () => handleTabSelect(tab.key),\n      className: classnames({ active: tab.key === activeTab })\n    }));\n  }, [overflowTabs, activeTab, handleTabSelect]);\n\n  return (\n    <StyledWrapper ref={tabsContainerRef} role=\"tablist\" className=\"tabs flex items-center justify-between gap-6\">\n      <div className=\"flex items-center\">\n        {/* Hidden tabs for measurement */}\n        <div style={hiddenStyle}>\n          {tabs.map((tab) => (\n            <div\n              key={tab.key}\n              ref={(el) => setTabRef(el, tab.key)}\n              className={classnames('tab select-none', tab.key, { active: tab.key === activeTab })}\n            >\n              {tab.label}\n              {tab.indicator}\n            </div>\n          ))}\n        </div>\n\n        {/* Visible tabs */}\n        {visibleTabs.map((tab) => renderTab(tab))}\n\n        {/* Overflow dropdown */}\n        {overflowTabs.length > 0 && (\n          <MenuDropdown\n            ref={menuDropdownRef}\n            items={overflowMenuItems}\n            placement=\"bottom-start\"\n            selectedItemId={activeTab}\n          >\n            <div className=\"more-tabs select-none flex items-center cursor-pointer gap-1\">\n              <IconChevronsRight size={18} strokeWidth={2} />\n            </div>\n          </MenuDropdown>\n        )}\n      </div>\n\n      {rightContent && (\n        <div className={rightContentClassName}>\n          {rightContent}\n        </div>\n      )}\n    </StyledWrapper>\n  );\n};\n\nexport default ResponsiveTabs;\n"
  },
  {
    "path": "packages/bruno-app/src/ui/StatusBadge/StyledWrapper.js",
    "content": "import styled, { css } from 'styled-components';\n\n/**\n * Resolves status tokens from the theme.\n *\n * Each status (info, success, warning, danger) provides three tokens:\n *   - background: light tinted color (15% opacity of intent)\n *   - text: full-intensity intent color\n *   - border: full-intensity intent color (same as text)\n *\n * The 'muted' status falls back to surface/muted colors for neutral badges.\n *\n * @see packages/bruno-app/src/themes/schema/oss.js — status schema\n * @see packages/bruno-app/src/themes/light/light.js — light theme tokens\n */\nconst getStatusTokens = (theme, status) => {\n  switch (status) {\n    case 'danger':\n      return { background: theme.status.danger.background, text: theme.status.danger.text, border: theme.status.danger.border };\n    case 'warning':\n      return { background: theme.status.warning.background, text: theme.status.warning.text, border: theme.status.warning.border };\n    case 'info':\n      return { background: theme.status.info.background, text: theme.status.info.text, border: theme.status.info.border };\n    case 'success':\n      return { background: theme.status.success.background, text: theme.status.success.text, border: theme.status.success.border };\n    case 'muted':\n    default:\n      return { background: theme.background.surface1, text: theme.colors.text.muted, border: theme.border.border1 };\n  }\n};\n\n/**\n * Variant styles — follows the same pattern as Button (ui/Button/StyledWrapper.js).\n *\n * - light:       tinted background + colored text (default, most common in codebase)\n * - filled:      solid colored background + contrast text\n * - outline:     transparent background + colored border + colored text\n * - ghost:       no background or border, just colored text\n */\nconst getVariantStyles = (props) => {\n  const { theme, $variant, $status } = props;\n  const tokens = getStatusTokens(theme, $status);\n\n  switch ($variant) {\n    case 'filled':\n      return css`\n        background: ${tokens.text};\n        color: ${tokens.background};\n        border: 1px solid ${tokens.text};\n      `;\n    case 'outline':\n      return css`\n        background: transparent;\n        color: ${tokens.text};\n        border: 1px solid ${tokens.border};\n      `;\n    case 'ghost':\n      return css`\n        background: transparent;\n        color: ${tokens.text};\n        border: 1px solid transparent;\n      `;\n    case 'light':\n    default:\n      return css`\n        background: ${tokens.background};\n        color: ${tokens.text};\n        border: 1px solid transparent;\n      `;\n  }\n};\n\n/**\n * Resolves border-radius from theme keys or raw CSS values.\n *\n * Accepts theme radius keys (sm, base, md, lg, xl), the 'full' alias for pill\n * shapes (9999px), or any raw CSS value (e.g. '20px').\n * Defaults to theme.border.radius.sm when no radius is specified.\n *\n * @see packages/bruno-app/src/themes/light/light.js — radius: { sm: '4px', base: '6px', md: '8px', lg: '10px', xl: '12px' }\n */\nconst resolveRadius = (props) => {\n  const { theme, $radius } = props;\n  if (!$radius) return theme.border.radius.sm;\n  if ($radius === 'full') return '9999px';\n  if (theme.border.radius[$radius]) return theme.border.radius[$radius];\n  return $radius;\n};\n\n/**\n * Size presets — derived from existing badge patterns in the codebase.\n *\n * - xs: 9px font, minimal padding (inline labels, tab badges)\n * - sm: 10px font, compact padding (matches .conflict-badge, .source-tag, .required-badge)\n * - md: theme xs font, wider padding (matches .deprecated-tag, .changes-tag, .context-pill)\n */\nconst sizeStyles = {\n  xs: css`\n    font-size: 9px;\n    padding: 0.0625rem 0.25rem;\n  `,\n  sm: css`\n    font-size: 10px;\n    padding: 0.125rem 0.375rem;\n  `,\n  md: css`\n    font-size: ${(props) => props.theme.font.size.xs};\n    padding: 0.125rem 0.5rem;\n  `\n};\n\nconst StyledWrapper = styled.div`\n  display: inline-flex;\n  align-items: center;\n  position: relative;\n  gap: 3px;\n  font-weight: 500;\n  white-space: nowrap;\n  cursor: default;\n  border-radius: ${resolveRadius};\n  ${(props) => sizeStyles[props.$size] || sizeStyles.sm}\n  ${(props) => getVariantStyles(props)}\n`;\n\nexport default StyledWrapper;\n"
  },
  {
    "path": "packages/bruno-app/src/ui/StatusBadge/index.js",
    "content": "import React from 'react';\nimport StyledWrapper from './StyledWrapper';\n\n/**\n * StatusBadge — reusable themed badge component.\n *\n * Props:\n * - children:     badge text content\n * - status:       theme status key — 'danger' | 'warning' | 'info' | 'success' | 'muted' (default: 'muted')\n * - variant:      visual style — 'light' | 'filled' | 'outline' | 'ghost' (default: 'light')\n * - size:         size preset — 'xs' | 'sm' | 'md' (default: 'sm')\n * - radius:       theme radius key ('sm','base','md','lg','xl') or CSS value (default: theme sm)\n * - leftSection:  ReactNode rendered before children (e.g. icon)\n * - rightSection: ReactNode rendered after children (e.g. Help tooltip)\n * - className:    passthrough for additional styling\n *\n * @example\n * <StatusBadge status=\"danger\">Error</StatusBadge>\n * <StatusBadge status=\"info\" variant=\"outline\" radius=\"xl\">v2.1</StatusBadge>\n * <StatusBadge status=\"warning\" rightSection={<Help icon=\"info\" size={11}>tooltip</Help>}>Conflict</StatusBadge>\n */\nconst StatusBadge = ({\n  children,\n  status = 'muted',\n  variant = 'light',\n  size = 'sm',\n  radius,\n  leftSection,\n  rightSection,\n  className = ''\n}) => {\n  return (\n    <StyledWrapper\n      $status={status}\n      $variant={variant}\n      $size={size}\n      $radius={radius}\n      className={className}\n    >\n      {leftSection}\n      {children}\n      {rightSection}\n    </StyledWrapper>\n  );\n};\n\nexport default StatusBadge;\n"
  },
  {
    "path": "packages/bruno-app/src/utils/auth/index.js",
    "content": "import { get } from 'lodash';\nimport {\n  getTreePathFromCollectionToItem\n} from 'utils/collections/index';\n\n// Resolve inherited auth by traversing up the folder hierarchy\nexport const resolveInheritedAuth = (item, collection) => {\n  const mergedRequest = {\n    ...(item.request || {}),\n    ...(item.draft?.request || {})\n  };\n\n  const authMode = mergedRequest.auth.mode;\n\n  // If auth is not inherit or no auth defined, return the merged request as is\n  if (!authMode || authMode !== 'inherit') {\n    return mergedRequest;\n  }\n\n  // Get the tree path from collection to item\n  const requestTreePath = getTreePathFromCollectionToItem(collection, item);\n\n  // Default to collection auth\n  const collectionRoot = collection?.draft?.root || collection?.root || {};\n  const collectionAuth = get(collectionRoot, 'request.auth', { mode: 'none' });\n  let effectiveAuth = collectionAuth;\n\n  // Check folders in reverse to find the closest auth configuration\n  for (let i of [...requestTreePath].reverse()) {\n    if (i.type === 'folder') {\n      const folderAuth = i?.draft ? get(i, 'draft.request.auth') : get(i, 'root.request.auth');\n      if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {\n        effectiveAuth = folderAuth;\n        break;\n      }\n    }\n  }\n\n  return {\n    ...mergedRequest,\n    auth: effectiveAuth\n  };\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/auth/index.spec.js",
    "content": "import { resolveInheritedAuth } from './index';\n\njest.mock('utils/collections/index', () => ({\n  getTreePathFromCollectionToItem: (collection, item) => {\n    const itemUid = item.uid;\n\n    if (itemUid === 'r1') {\n      return [collection.items[0], collection.items[0].items[0]];\n    }\n    return [];\n  }\n}));\n\n// Helper to build mock collection structure\nconst buildCollection = () => {\n  return {\n    uid: 'c1',\n    root: {\n      request: {\n        auth: { mode: 'bearer', bearer: { token: 'COLLECTION' } }\n      }\n    },\n    items: [\n      {\n        uid: 'f1',\n        type: 'folder',\n        name: 'Folder',\n        root: {\n          request: {\n            auth: { mode: 'basic', basic: { username: 'user', password: 'pass' } }\n          }\n        },\n        items: [\n          {\n            uid: 'r1',\n            type: 'request',\n            name: 'Request',\n            request: {\n              auth: { mode: 'inherit' },\n              url: 'http://example.com',\n              method: 'GET'\n            }\n          }\n        ]\n      }\n    ]\n  };\n};\n\ndescribe('auth-utils.resolveInheritedAuth', () => {\n  it('should resolve to nearest folder auth when request mode is inherit', () => {\n    const collection = buildCollection();\n    const item = collection.items[0].items[0]; // r1\n\n    const resolved = resolveInheritedAuth(item, collection);\n    expect(resolved.auth.mode).toBe('basic');\n    expect(resolved.auth.basic.username).toBe('user');\n  });\n\n  it('should resolve to collection auth if no folder auth', () => {\n    const collection = buildCollection();\n    collection.items[0].root.request.auth = { mode: 'inherit' };\n    const item = collection.items[0].items[0];\n\n    const resolved = resolveInheritedAuth(item, collection);\n    expect(resolved.auth.mode).toBe('bearer');\n    expect(resolved.auth.bearer.token).toBe('COLLECTION');\n  });\n\n  it('should return original request when mode is not inherit', () => {\n    const collection = buildCollection();\n    const item = collection.items[0].items[0];\n    item.request.auth = { mode: 'basic', basic: { username: 'override', password: 'pwd' } };\n\n    const resolved = resolveInheritedAuth(item, collection);\n    expect(resolved.auth.mode).toBe('basic');\n    expect(resolved.auth.basic.username).toBe('override');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/utils/beta-features.js",
    "content": "import { useSelector } from 'react-redux';\n\n/**\n * Beta features configuration object\n * Contains all available beta feature keys\n */\nexport const BETA_FEATURES = Object.freeze({\n  NODE_VM: 'nodevm',\n  OPENAPI_SYNC: 'openapi-sync'\n});\n\n/**\n * Hook to check if a beta feature is enabled\n * @param {string} featureName - The name of the beta feature\n * @returns {boolean} - Whether the feature is enabled\n */\nexport const useBetaFeature = (featureName) => {\n  const preferences = useSelector((state) => state.app.preferences);\n  return preferences?.beta?.[featureName] || false;\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/bruno-clipboard.js",
    "content": "class BrunoClipboard {\n  constructor() {\n    this.items = [];\n  }\n\n  /**\n   * @param {Object} item - Item to copy\n   */\n  write(item) {\n    // Limit to one item for now\n    this.items = [item];\n  }\n\n  /**\n   * @returns {Object} Result with items array\n   */\n  read() {\n    return {\n      items: this.items,\n      hasData: this.items.length > 0\n    };\n  }\n}\n\nconst brunoClipboard = new BrunoClipboard();\n\nexport default brunoClipboard;\n"
  },
  {
    "path": "packages/bruno-app/src/utils/codegenerator/auth.js",
    "content": "import get from 'lodash/get';\nimport { find } from 'lodash';\nimport { interpolate } from '@usebruno/common';\nimport { getAllVariables } from 'utils/collections/index';\n\nexport const getAuthHeaders = (requestAuth, collection = null, item = null) => {\n  // Auth inheritance is resolved upstream, so requestAuth should never have mode 'inherit'\n  if (!requestAuth) {\n    return [];\n  }\n\n  switch (requestAuth.mode) {\n    case 'basic':\n      const username = get(requestAuth, 'basic.username', '');\n      const password = get(requestAuth, 'basic.password', '');\n      const basicToken = Buffer.from(`${username}:${password}`).toString('base64');\n\n      return [\n        {\n          enabled: true,\n          name: 'Authorization',\n          value: `Basic ${basicToken}`\n        }\n      ];\n    case 'bearer':\n      return [\n        {\n          enabled: true,\n          name: 'Authorization',\n          value: `Bearer ${get(requestAuth, 'bearer.token', '')}`\n        }\n      ];\n    case 'apikey':\n      const apiKeyAuth = get(requestAuth, 'apikey', {});\n      const key = get(apiKeyAuth, 'key', '');\n      const value = get(apiKeyAuth, 'value', '');\n      const placement = get(apiKeyAuth, 'placement', 'header');\n\n      if (placement === 'header') {\n        return [\n          {\n            enabled: true,\n            name: key,\n            value: value\n          }\n        ];\n      }\n      return [];\n    case 'oauth2': {\n      const oauth2Config = get(requestAuth, 'oauth2', {});\n      const tokenPlacement = get(oauth2Config, 'tokenPlacement', 'header');\n      const tokenHeaderPrefix = get(oauth2Config, 'tokenHeaderPrefix', 'Bearer');\n\n      // Only add header if token placement is 'header'\n      if (tokenPlacement === 'header') {\n        // Try to get access token from persisted credentials\n        let accessToken = '<access_token>';\n\n        if (collection && item) {\n          try {\n            const grantType = get(oauth2Config, 'grantType', '');\n            // For implicit grant type, use authorizationUrl; for others, use accessTokenUrl\n            const urlToLookup = grantType === 'implicit'\n              ? get(oauth2Config, 'authorizationUrl', '')\n              : get(oauth2Config, 'accessTokenUrl', '');\n            const credentialsId = get(oauth2Config, 'credentialsId', 'credentials');\n            const collectionUid = get(collection, 'uid');\n\n            if (urlToLookup && collectionUid) {\n              // Interpolate the URL with variables\n              const variables = getAllVariables(collection, item);\n              const interpolatedUrl = interpolate(urlToLookup, variables);\n\n              // Look up stored credentials\n              const credentialsData = find(\n                collection?.oauth2Credentials || [],\n                (creds) =>\n                  creds?.url === interpolatedUrl\n                  && creds?.collectionUid === collectionUid\n                  && creds?.credentialsId === credentialsId\n              );\n\n              if (credentialsData?.credentials?.access_token) {\n                accessToken = credentialsData.credentials.access_token;\n              }\n            }\n          } catch (error) {\n            console.error('Error retrieving OAuth2 access token:', error);\n            // Fall back to placeholder if lookup fails\n          }\n        }\n\n        // Build the authorization header value\n        // If tokenHeaderPrefix is empty, just use the token\n        // Otherwise, use the format: \"prefix token\"\n        // Always trim the final result for consistent formatting\n        const headerValue = (\n          tokenHeaderPrefix\n            ? `${tokenHeaderPrefix} ${accessToken}`\n            : accessToken\n        ).trim();\n\n        return [\n          {\n            enabled: true,\n            name: 'Authorization',\n            value: headerValue\n          }\n        ];\n      }\n      // If tokenPlacement is 'url', this function does not add any auth headers;\n      // token placement in the URL/query params must be handled elsewhere.\n      return [];\n    }\n    default:\n      return [];\n  }\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/codegenerator/har.js",
    "content": "const createContentType = (mode) => {\n  switch (mode) {\n    case 'json':\n      return 'application/json';\n    case 'text':\n      return 'text/plain';\n    case 'xml':\n      return 'application/xml';\n    case 'sparql':\n      return 'application/sparql-query';\n    case 'formUrlEncoded':\n      return 'application/x-www-form-urlencoded';\n    case 'graphql':\n      return 'application/json';\n    case 'multipartForm':\n      return 'multipart/form-data';\n    case 'file':\n      return 'application/octet-stream';\n    default:\n      return '';\n  }\n};\n\n/**\n * Creates a list of enabled headers for the request, ensuring no duplicate content-type headers.\n *\n * @param {Object} request - The request object.\n * @param {Object[]} headers - The array of header objects, each containing name, value, and enabled properties.\n * @returns {Object[]} - An array of enabled headers with normalized names and values.\n */\nconst createHeaders = (request, headers) => {\n  const enabledHeaders = headers\n    .filter((header) => header.enabled)\n    .map((header) => ({\n      name: header.name.toLowerCase(),\n      value: header.value\n    }));\n\n  const contentType = createContentType(request.body?.mode);\n  if (contentType !== '' && !enabledHeaders.some((header) => header.name === 'content-type')) {\n    enabledHeaders.push({ name: 'content-type', value: contentType });\n  }\n\n  return enabledHeaders;\n};\n\nconst createQuery = (queryParams = [], request) => {\n  const params = queryParams\n    .filter((param) => param.enabled && param.type === 'query')\n    .map((param) => ({\n      name: param.name,\n      value: param.value\n    }));\n\n  if (request?.auth?.mode === 'apikey'\n    && request?.auth?.apikey?.placement === 'queryparams'\n    && request?.auth?.apikey?.key\n    && request?.auth?.apikey?.value) {\n    params.push({\n      name: request.auth.apikey.key,\n      value: request.auth.apikey.value\n    });\n  }\n\n  return params;\n};\n\nconst createPostData = (body) => {\n  const contentType = createContentType(body.mode);\n\n  switch (body.mode) {\n    case 'formUrlEncoded':\n      return {\n        mimeType: contentType,\n        text: new URLSearchParams(\n          (Array.isArray(body[body.mode]) ? body[body.mode] : [])\n            .filter((param) => param?.enabled)\n            .reduce((acc, param) => {\n              acc[param.name] = param.value;\n              return acc;\n            }, {})\n        ).toString(),\n        params: (Array.isArray(body[body.mode]) ? body[body.mode] : [])\n          .filter((param) => param?.enabled)\n          .map((param) => ({\n            name: param.name,\n            value: param.value\n          }))\n      };\n    case 'multipartForm':\n      return {\n        mimeType: contentType,\n        params: (Array.isArray(body[body.mode]) ? body[body.mode] : [])\n          .filter((param) => param?.enabled)\n          .map((param) => ({\n            name: param.name,\n            value: param.value,\n            ...(param.type === 'file' && { fileName: param.value })\n          }))\n      };\n    case 'file': {\n      const files = Array.isArray(body[body.mode]) ? body[body.mode] : [];\n      const selectedFile = files.find((param) => param.selected) || files[0];\n      const filePath = selectedFile?.filePath || '';\n      return {\n        mimeType: selectedFile?.contentType || 'application/octet-stream',\n        text: filePath,\n        params: filePath\n          ? [\n              {\n                name: selectedFile?.name || 'file',\n                value: filePath,\n                fileName: filePath,\n                contentType: selectedFile?.contentType || 'application/octet-stream'\n              }\n            ]\n          : []\n      };\n    }\n    case 'graphql':\n      return {\n        mimeType: contentType,\n        text: JSON.stringify(body[body.mode])\n      };\n    default:\n      return {\n        mimeType: contentType,\n        text: body[body.mode]\n      };\n  }\n};\n\nexport const buildHarRequest = ({ request, headers }) => {\n  // NOTE:\n  // This is just a safety check.\n  // The interpolateUrlPathParams method validates the url, but it does not throw\n  if (!URL.canParse(request.url)) {\n    throw new Error('invalid request url');\n  }\n\n  return {\n    method: request.method,\n    url: request.url,\n    httpVersion: 'HTTP/1.1',\n    cookies: [],\n    headers: createHeaders(request, headers),\n    queryString: createQuery(request.params, request),\n    postData: createPostData(request.body),\n    headersSize: 0,\n    bodySize: 0,\n    binary: true\n  };\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/codegenerator/targets.js",
    "content": "import { targets } from 'httpsnippet';\n\nexport const getLanguages = () => {\n  const allLanguages = [];\n  for (const target of Object.values(targets)) {\n    const { key, title } = target.info;\n    const clients = Object.keys(target.clientsById);\n    const languages\n      = (clients.length === 1)\n        ? [{\n            name: title,\n            target: key,\n            client: clients[0]\n          }]\n        : clients.map((client) => ({\n            name: `${title}-${client}`,\n            target: key,\n            client\n          }));\n    allLanguages.push(...languages);\n\n    // Move \"Shell-curl\" to the top of the array\n    const shellCurlIndex = allLanguages.findIndex((lang) => lang.name === 'Shell-curl');\n    if (shellCurlIndex !== -1) {\n      const [shellCurl] = allLanguages.splice(shellCurlIndex, 1);\n      allLanguages.unshift(shellCurl);\n    }\n  }\n\n  return allLanguages;\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/codemirror/autocomplete.js",
    "content": "import { mockDataFunctions } from '@usebruno/common';\n\nconst CodeMirror = require('codemirror');\n\n// Static API hints - Bruno JavaScript API (subgrouped by category)\n// TODO: Restore the commented-out APIs once the UI update fixes are live.\n// Currently these APIs only work within the request lifecycle but fail to update the UI tables.\n// e.g., setCollectionVar only sets the variable in the request lifecycle, fails to update the table in the UI.\nconst STATIC_API_HINTS = {\n  req: [\n    'req',\n    'req.url',\n    'req.method',\n    'req.headers',\n    'req.body',\n    'req.timeout',\n    'req.getUrl()',\n    'req.setUrl(url)',\n    'req.getHost()',\n    'req.getPath()',\n    'req.getQueryString()',\n    'req.getMethod()',\n    'req.getAuthMode()',\n    'req.setMethod(method)',\n    'req.getHeader(name)',\n    'req.getHeaders()',\n    'req.setHeader(name, value)',\n    'req.setHeaders(data)',\n    'req.deleteHeader(name)',\n    'req.deleteHeaders(data)',\n    'req.getBody()',\n    'req.setBody(data)',\n    'req.setMaxRedirects(maxRedirects)',\n    'req.getTimeout()',\n    'req.setTimeout(timeout)',\n    'req.getExecutionMode()',\n    'req.getName()',\n    'req.getPathParams()',\n    'req.getTags()',\n    'req.disableParsingResponseJson()',\n    'req.onFail(function(err) {})'\n  ],\n  res: [\n    'res',\n    'res.status',\n    'res.statusText',\n    'res.headers',\n    'res.body',\n    'res.responseTime',\n    'res.url',\n    'res.getStatus()',\n    'res.getStatusText()',\n    'res.getHeader(name)',\n    'res.getHeaders()',\n    'res.getBody()',\n    'res.setBody(data)',\n    'res.getResponseTime()',\n    'res.getSize()',\n    'res.getSize().header',\n    'res.getSize().body',\n    'res.getSize().total',\n    'res.getUrl()'\n  ],\n  bru: [\n    'bru',\n    'bru.cwd()',\n    'bru.getEnvName()',\n    'bru.getProcessEnv(key)',\n    'bru.hasEnvVar(key)',\n    'bru.getEnvVar(key)',\n    'bru.getFolderVar(key)',\n    'bru.getCollectionVar(key)',\n    // 'bru.setCollectionVar(key, value)',\n    'bru.hasCollectionVar(key)',\n    // 'bru.deleteCollectionVar(key)',\n    // 'bru.deleteAllCollectionVars()',\n    // 'bru.getAllCollectionVars()',\n    'bru.setEnvVar(key, value)',\n    'bru.setEnvVar(key, value, options)',\n    'bru.deleteEnvVar(key)',\n    'bru.getAllEnvVars()',\n    'bru.deleteAllEnvVars()',\n    'bru.hasVar(key)',\n    'bru.getVar(key)',\n    'bru.setVar(key,value)',\n    'bru.deleteVar(key)',\n    'bru.deleteAllVars()',\n    'bru.getAllVars()',\n    'bru.setNextRequest(requestName)',\n    'bru.getRequestVar(key)',\n    'bru.runRequest(requestPathName)',\n    'bru.sendRequest(requestConfig)',\n    'bru.sendRequest(requestConfig, callback)',\n    'bru.getAssertionResults()',\n    'bru.getTestResults()',\n    'bru.sleep(ms)',\n    'bru.getCollectionName()',\n    'bru.isSafeMode()',\n    'bru.getOauth2CredentialVar(key)',\n    'bru.getGlobalEnvVar(key)',\n    'bru.setGlobalEnvVar(key, value)',\n    // 'bru.deleteGlobalEnvVar(key)',\n    'bru.getAllGlobalEnvVars()',\n    // 'bru.deleteAllGlobalEnvVars()',\n    'bru.runner',\n    'bru.runner.setNextRequest(requestName)',\n    'bru.runner.skipRequest()',\n    'bru.runner.stopExecution()',\n    'bru.interpolate(str)',\n    'bru.cookies',\n    'bru.cookies.jar()',\n    'bru.cookies.jar().getCookie(url, name, callback)',\n    'bru.cookies.jar().getCookies(url, callback)',\n    'bru.cookies.jar().setCookie(url, name, value, callback)',\n    'bru.cookies.jar().setCookie(url, cookieObject, callback)',\n    'bru.cookies.jar().setCookies(url, cookiesArray, callback)',\n    'bru.cookies.jar().clear(callback)',\n    'bru.cookies.jar().deleteCookies(url, callback)',\n    'bru.cookies.jar().deleteCookie(url, name, callback)',\n    'bru.utils',\n    'bru.utils.minifyJson(json)',\n    'bru.utils.minifyXml(xml)',\n    'bru.resetOauth2Credential(credentialId)'\n  ]\n};\n\n// Mock data functions - prefixed with $\nconst MOCK_DATA_HINTS = Object.keys(mockDataFunctions).map((key) => `$${key}`);\n\n// Constants for word pattern matching\nconst WORD_PATTERN = /[\\w.$-/]/;\nconst VARIABLE_PATTERN = /\\{\\{([\\w$.-]*)$/;\nconst NON_CHARACTER_KEYS = /^(?!Shift|Tab|Enter|Escape|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Meta|Alt|Home|End\\s)\\w*/;\n\n/**\n * Generate progressive hints for a given full hint\n * @param {string} fullHint - The complete hint string\n * @returns {string[]} Array of progressive hints\n */\nconst generateProgressiveHints = (fullHint) => {\n  const parts = fullHint.split('.');\n  const progressiveHints = [];\n\n  for (let i = 1; i <= parts.length; i++) {\n    progressiveHints.push(parts.slice(0, i).join('.'));\n  }\n\n  return progressiveHints;\n};\n\n/**\n * Check if a variable key should be skipped\n * @param {string} key - The variable key to check\n * @returns {boolean} True if the key should be skipped\n */\nconst shouldSkipVariableKey = (key) => {\n  return key === 'pathParams' || key === 'maskedEnvVariables' || key === 'process';\n};\n\n/**\n * Transform variables object into flat hint list\n * @param {Object} allVariables - All available variables\n * @returns {string[]} Array of variable hints\n */\nconst transformVariablesToHints = (allVariables = {}) => {\n  const hints = [];\n\n  // Process all variables without type-specific handling\n  Object.keys(allVariables).forEach((key) => {\n    if (!shouldSkipVariableKey(key)) {\n      hints.push(key);\n    }\n  });\n\n  // Handle process environment variables\n  if (allVariables.process && allVariables.process.env) {\n    Object.keys(allVariables.process.env).forEach((key) => {\n      hints.push(`process.env.${key}`);\n    });\n  }\n\n  return hints;\n};\n\n/**\n * Add API hints to categorized hints based on showHintsFor configuration\n * @param {Set} apiHints - Set to add API hints to\n * @param {string[]} showHintsFor - Array of hint types to show\n */\nconst addApiHintsToSet = (apiHints, showHintsFor) => {\n  const apiTypes = ['req', 'res', 'bru'];\n\n  apiTypes.forEach((apiType) => {\n    if (showHintsFor.includes(apiType)) {\n      STATIC_API_HINTS[apiType].forEach((hint) => {\n        generateProgressiveHints(hint).forEach((h) => apiHints.add(h));\n      });\n    }\n  });\n};\n\n/**\n * Add variable hints to categorized hints\n * @param {Set} variableHints - Set to add variable hints to\n * @param {Object} allVariables - All available variables\n */\nconst addVariableHintsToSet = (variableHints, allVariables) => {\n  // Add mock data hints\n  MOCK_DATA_HINTS.forEach((hint) => {\n    generateProgressiveHints(hint).forEach((h) => variableHints.add(h));\n  });\n\n  // Add variable hints with progressive hints\n  const variableHintsList = transformVariablesToHints(allVariables);\n  variableHintsList.forEach((hint) => {\n    generateProgressiveHints(hint).forEach((h) => variableHints.add(h));\n  });\n};\n\n/**\n * Add custom hints to categorized hints\n * @param {Set} anywordHints - Set to add custom hints to\n * @param {string[]} customHints - Array of custom hints\n */\nconst addCustomHintsToSet = (anywordHints, customHints) => {\n  if (customHints && Array.isArray(customHints)) {\n    customHints.forEach((hint) => {\n      generateProgressiveHints(hint).forEach((h) => anywordHints.add(h));\n    });\n  }\n};\n\n/**\n * Build categorized hints list from all sources\n * @param {Object} allVariables - All available variables\n * @param {string[]} anywordAutocompleteHints - Custom autocomplete hints\n * @param {Object} options - Configuration options\n * @returns {Object} Categorized hints object\n */\nconst buildCategorizedHintsList = (allVariables = {}, anywordAutocompleteHints = [], options = {}) => {\n  const categorizedHints = {\n    api: new Set(),\n    variables: new Set(),\n    anyword: new Set()\n  };\n\n  const showHintsFor = options.showHintsFor || [];\n\n  // Add different types of hints\n  addApiHintsToSet(categorizedHints.api, showHintsFor);\n  addVariableHintsToSet(categorizedHints.variables, allVariables);\n  addCustomHintsToSet(categorizedHints.anyword, anywordAutocompleteHints);\n\n  return {\n    api: Array.from(categorizedHints.api).sort(),\n    variables: Array.from(categorizedHints.variables).sort(),\n    anyword: Array.from(categorizedHints.anyword).sort()\n  };\n};\n\n/**\n * Calculate replacement positions for variable context\n * @param {Object} cursor - Current cursor position\n * @param {Object} startPos - Start position of variable\n * @param {string} wordMatch - The matched word\n * @returns {Object} From and to positions for replacement\n */\nconst calculateVariableReplacementPositions = (cursor, startPos, wordMatch) => {\n  let replaceFrom, replaceTo;\n\n  if (wordMatch.endsWith('.')) {\n    replaceFrom = cursor;\n    replaceTo = cursor;\n  } else {\n    const lastDotIndex = wordMatch.lastIndexOf('.');\n    if (lastDotIndex !== -1) {\n      replaceFrom = { line: cursor.line, ch: startPos.ch + lastDotIndex + 1 };\n      replaceTo = cursor;\n    } else {\n      replaceFrom = startPos;\n      replaceTo = cursor;\n    }\n  }\n\n  return { replaceFrom, replaceTo };\n};\n\n/**\n * Calculate replacement positions for regular word context\n * @param {Object} cursor - Current cursor position\n * @param {number} start - Start position of word\n * @param {number} end - End position of word\n * @param {string} word - The matched word\n * @returns {Object} From and to positions for replacement\n */\nconst calculateWordReplacementPositions = (cursor, start, end, word) => {\n  let replaceFrom, replaceTo;\n\n  if (word.endsWith('.')) {\n    replaceFrom = { line: cursor.line, ch: end };\n    replaceTo = cursor;\n  } else {\n    const lastDotIndex = word.lastIndexOf('.');\n    if (lastDotIndex !== -1) {\n      replaceFrom = { line: cursor.line, ch: start + lastDotIndex + 1 };\n      replaceTo = { line: cursor.line, ch: end };\n    } else {\n      replaceFrom = { line: cursor.line, ch: start };\n      replaceTo = { line: cursor.line, ch: end };\n    }\n  }\n\n  return { replaceFrom, replaceTo };\n};\n\n/**\n * Determine context based on word prefix\n * @param {string} word - The word to analyze\n * @returns {string} The determined context\n */\nconst determineWordContext = (word) => {\n  const isApiHint = Object.keys(STATIC_API_HINTS).some(\n    (apiRoot) => apiRoot.toLowerCase().startsWith(word.toLowerCase()) || word.toLowerCase().startsWith(apiRoot.toLowerCase())\n  );\n\n  if (isApiHint) {\n    return 'api';\n  }\n\n  return 'anyword';\n};\n\n/**\n * Extract word from current line with boundaries\n * @param {string} currentLine - The current line content\n * @param {number} cursorPosition - Current cursor position\n * @returns {Object|null} Word information or null if no word found\n */\nconst extractWordFromLine = (currentLine, cursorPosition) => {\n  let start = cursorPosition;\n  let end = start;\n\n  while (end < currentLine.length && WORD_PATTERN.test(currentLine.charAt(end))) {\n    ++end;\n  }\n  while (start && WORD_PATTERN.test(currentLine.charAt(start - 1))) {\n    --start;\n  }\n\n  if (start === end) {\n    return null;\n  }\n\n  return {\n    word: currentLine.slice(start, end),\n    start,\n    end\n  };\n};\n\n/**\n * Get current word being typed at cursor position with context information\n * @param {Object} cm - CodeMirror instance\n * @returns {Object|null} Word information with context or null\n */\nconst getCurrentWordWithContext = (cm) => {\n  const cursor = cm.getCursor();\n  const currentLine = cm.getLine(cursor.line);\n  const currentString = cm.getRange({ line: cursor.line, ch: 0 }, cursor);\n\n  // Check for variable pattern {{word\n  const variableMatch = currentString.match(VARIABLE_PATTERN);\n  if (variableMatch) {\n    const wordMatch = variableMatch[1];\n    const startPos = { line: cursor.line, ch: currentString.lastIndexOf('{{') + 2 };\n    const { replaceFrom, replaceTo } = calculateVariableReplacementPositions(cursor, startPos, wordMatch);\n\n    return {\n      word: wordMatch,\n      from: replaceFrom,\n      to: replaceTo,\n      context: 'variables',\n      requiresBraces: true\n    };\n  }\n\n  // Check for regular word\n  const wordInfo = extractWordFromLine(currentLine, cursor.ch);\n  if (!wordInfo) {\n    return null;\n  }\n\n  const { word, start, end } = wordInfo;\n  const { replaceFrom, replaceTo } = calculateWordReplacementPositions(cursor, start, end, word);\n  const context = determineWordContext(word);\n\n  return {\n    word,\n    from: replaceFrom,\n    to: replaceTo,\n    context,\n    requiresBraces: false\n  };\n};\n\n/**\n * Extract next segment suggestions from filtered hints\n * @param {string[]} filteredHints - Pre-filtered hints\n * @param {string} currentInput - Current user input\n * @returns {string[]} Array of suggestion segments\n */\nconst extractNextSegmentSuggestions = (filteredHints, currentInput) => {\n  const prefixMatches = new Set();\n  const substringMatches = new Set();\n  const lowerInput = currentInput.toLowerCase();\n\n  filteredHints.forEach((hint) => {\n    const lowerHint = hint.toLowerCase();\n\n    // For prefix matches, use the original progressive logic\n    if (lowerHint.startsWith(lowerInput)) {\n      // Handle exact match case\n      if (lowerHint === lowerInput) {\n        prefixMatches.add(hint.substring(hint.lastIndexOf('.') + 1));\n        return;\n      }\n\n      const inputLength = currentInput.length;\n\n      if (currentInput.endsWith('.')) {\n        // Show next segment after the dot\n        const afterDot = hint.substring(inputLength);\n        const nextDot = afterDot.indexOf('.');\n        const segment = nextDot === -1 ? afterDot : afterDot.substring(0, nextDot);\n        prefixMatches.add(segment);\n      } else {\n        // Show complete current segment\n        const lastDotInInput = currentInput.lastIndexOf('.');\n        const currentSegmentStart = lastDotInInput + 1;\n        const nextDotAfterInput = hint.indexOf('.', currentSegmentStart);\n        const segment\n          = nextDotAfterInput === -1\n            ? hint.substring(currentSegmentStart)\n            : hint.substring(currentSegmentStart, nextDotAfterInput);\n        prefixMatches.add(segment);\n      }\n    } else if (lowerHint.includes(lowerInput)) {\n      // For substring matches (search within words), suggest the complete hint\n      substringMatches.add(hint);\n    }\n  });\n\n  // Return prefix matches first, then substring matches\n  return [...Array.from(prefixMatches).sort(), ...Array.from(substringMatches).sort()];\n};\n\n/**\n * Extract the relevant part of hints based on user input\n * @param {string[]} filteredHints - Pre-filtered hints\n * @param {string} currentInput - Current user input\n * @returns {string[]} Array of hint parts\n */\nconst getHintParts = (filteredHints, currentInput) => {\n  if (!filteredHints || filteredHints.length === 0) {\n    return [];\n  }\n\n  return extractNextSegmentSuggestions(filteredHints, currentInput);\n};\n\n/**\n * Get allowed hints based on context and configuration\n * @param {Object} categorizedHints - All categorized hints\n * @param {string} context - Current context\n * @param {string[]} showHintsFor - Allowed hint types\n * @returns {string[]} Array of allowed hints\n */\nconst getAllowedHintsByContext = (categorizedHints, context, showHintsFor) => {\n  let allowedHints = [];\n\n  if (context === 'variables' && showHintsFor.includes('variables')) {\n    allowedHints = [...categorizedHints.variables];\n  } else if (context === 'api') {\n    const hasApiHints = showHintsFor.some((hint) => ['req', 'res', 'bru'].includes(hint));\n    if (hasApiHints) {\n      allowedHints = [...categorizedHints.api];\n    }\n  } else if (context === 'anyword') {\n    allowedHints = [...categorizedHints.anyword];\n  }\n\n  return allowedHints;\n};\n\n/**\n * Filter hints based on current word and allowed hint types\n * @param {Object} categorizedHints - All categorized hints\n * @param {string} currentWord - Current word being typed\n * @param {string} context - Current context\n * @param {string[]} showHintsFor - Allowed hint types\n * @returns {string[]} Filtered hints\n */\nconst filterHintsByContext = (categorizedHints, currentWord, context, showHintsFor = []) => {\n  if (!currentWord) {\n    return [];\n  }\n\n  const allowedHints = getAllowedHintsByContext(categorizedHints, context, showHintsFor);\n\n  const lowerWord = currentWord.toLowerCase();\n  const filtered = allowedHints.filter((hint) => {\n    return hint.toLowerCase().includes(lowerWord);\n  });\n\n  const hintParts = getHintParts(filtered, currentWord);\n\n  return hintParts.slice(0, 50);\n};\n\n/**\n * Create hint list for variables context\n * @param {string[]} filteredHints - Filtered hints\n * @param {Object} from - Start position\n * @param {Object} to - End position\n * @returns {Object} Hint object with list and positions\n */\nconst createVariableHintList = (filteredHints, from, to) => {\n  const hintList = filteredHints.map((hint) => ({\n    text: hint,\n    displayText: hint\n  }));\n\n  return {\n    list: hintList,\n    from,\n    to\n  };\n};\n\n/**\n * Create hint list for non-variable contexts\n * @param {string[]} filteredHints - Filtered hints\n * @param {Object} from - Start position\n * @param {Object} to - End position\n * @returns {Object} Hint object with list and positions\n */\nconst createStandardHintList = (filteredHints, from, to) => {\n  return {\n    list: filteredHints,\n    from,\n    to\n  };\n};\n\n/**\n * Show root-level API hints when the editor is empty\n * @param {Object} cm - CodeMirror instance\n * @param {string[]} showHintsFor - Array of hint types to show (e.g., ['req', 'res', 'bru'])\n * @returns {boolean} True if hints were shown, false otherwise\n */\nexport const showRootHints = (cm, showHintsFor = []) => {\n  const wordInfo = getCurrentWordWithContext(cm);\n  // If user is currently typing a word, let handleKeyupForAutocomplete\n  // handle it instead of showing root hints.\n  if (wordInfo) {\n    return false;\n  }\n\n  const hints = Object.keys(STATIC_API_HINTS).filter((rootHint) => showHintsFor.includes(rootHint));\n\n  if (hints.length === 0) return false;\n\n  const cursor = cm.getCursor();\n  const hintList = createStandardHintList(hints, cursor, cursor);\n\n  cm.showHint({\n    hint: () => hintList,\n    completeSingle: false\n  });\n  return true;\n};\n\n/**\n * Bruno AutoComplete Helper - Main function with context awareness\n * @param {Object} cm - CodeMirror instance\n * @param {Object} allVariables - All available variables\n * @param {string[]} anywordAutocompleteHints - Custom autocomplete hints\n * @param {Object} options - Configuration options\n * @returns {Object|null} Hint object or null\n */\nexport const getAutoCompleteHints = (cm, allVariables = {}, anywordAutocompleteHints = [], options = {}) => {\n  if (!allVariables) {\n    return null;\n  }\n\n  const wordInfo = getCurrentWordWithContext(cm);\n  if (!wordInfo) {\n    return null;\n  }\n\n  const { word, from, to, context, requiresBraces } = wordInfo;\n  const showHintsFor = options.showHintsFor || [];\n\n  // Check if this context requires braces but we're not in a brace context\n  if (context === 'variables' && !requiresBraces) {\n    return null;\n  }\n\n  const categorizedHints = buildCategorizedHintsList(allVariables, anywordAutocompleteHints, options);\n  const filteredHints = filterHintsByContext(categorizedHints, word, context, showHintsFor);\n\n  if (filteredHints.length === 0) {\n    return null;\n  }\n\n  if (context === 'variables') {\n    return createVariableHintList(filteredHints, from, to);\n  }\n\n  return createStandardHintList(filteredHints, from, to);\n};\n\n/**\n * Handle click events for autocomplete\n * @param {Object} cm - CodeMirror instance\n * @param {Object} options - Configuration options\n */\nconst handleClickForAutocomplete = (cm, options) => {\n  const allVariables = options.getAllVariables?.() || {};\n  const anywordAutocompleteHints = options.getAnywordAutocompleteHints?.() || [];\n  const showHintsFor = options.showHintsFor || [];\n\n  // Build all available hints\n  const categorizedHints = buildCategorizedHintsList(allVariables, anywordAutocompleteHints, options);\n\n  // Combine all hints based on showHintsFor configuration\n  let allHints = [];\n\n  // Add API hints if enabled\n  const hasApiHints = showHintsFor.some((hint) => ['req', 'res', 'bru'].includes(hint));\n  if (hasApiHints) {\n    allHints = [...allHints, ...categorizedHints.api];\n  }\n\n  // Add variable hints if enabled\n  if (showHintsFor.includes('variables')) {\n    allHints = [...allHints, ...categorizedHints.variables];\n  }\n\n  // Add anyword hints (always included)\n  allHints = [...allHints, ...categorizedHints.anyword];\n\n  // Remove duplicates and sort\n  allHints = [...new Set(allHints)].sort();\n\n  if (allHints.length === 0) {\n    return;\n  }\n\n  const cursor = cm.getCursor();\n\n  if (cursor.ch > 0) return;\n\n  // Defer showHint to ensure editor is focused\n  setTimeout(() => {\n    cm.showHint({\n      hint: () => ({\n        list: allHints,\n        from: cursor,\n        to: cursor\n      }),\n      completeSingle: false\n    });\n  }, 0);\n};\n\n/**\n * Handle keyup events for autocomplete\n * @param {Object} cm - CodeMirror instance\n * @param {Event} event - The keyup event\n * @param {Object} options - Configuration options\n */\nconst handleKeyupForAutocomplete = (cm, event, options) => {\n  // Skip non-character keys\n  if (!NON_CHARACTER_KEYS.test(event?.key)) {\n    return;\n  }\n\n  const allVariables = options.getAllVariables?.() || {};\n  const anywordAutocompleteHints = options.getAnywordAutocompleteHints?.() || [];\n  const hints = getAutoCompleteHints(cm, allVariables, anywordAutocompleteHints, options);\n\n  if (!hints) {\n    const wordInfo = getCurrentWordWithContext(cm);\n    if (cm.state.completionActive && wordInfo) {\n      cm.state.completionActive.close();\n    }\n    return;\n  }\n\n  cm.showHint({\n    hint: () => hints,\n    completeSingle: false\n  });\n};\n\n/**\n * Setup Bruno AutoComplete Helper on a CodeMirror editor\n * @param {Object} editor - CodeMirror editor instance\n * @param {Object} options - Configuration options\n * @returns {Function} Cleanup function\n */\nexport const setupAutoComplete = (editor, options = {}) => {\n  if (!editor) {\n    return;\n  }\n\n  const keyupHandler = (cm, event) => {\n    handleKeyupForAutocomplete(cm, event, options);\n  };\n\n  editor.on('keyup', keyupHandler);\n\n  const clickHandler = (cm) => {\n    // Only show hints on click if the option is enabled and there's no active completion\n    if (options.showHintsOnClick) {\n      handleClickForAutocomplete(cm, options);\n    }\n  };\n\n  // Add click handler if showHintsOnClick is enabled\n  if (options.showHintsOnClick) {\n    editor.on('mousedown', clickHandler);\n  }\n\n  return () => {\n    editor.off('keyup', keyupHandler);\n    if (options.showHintsOnClick) {\n      editor.off('mousedown', clickHandler);\n    }\n  };\n};\n\n// Exported for testing\nexport { extractNextSegmentSuggestions };\n\n// Initialize autocomplete command if not already present\nif (!CodeMirror.commands.autocomplete) {\n  CodeMirror.commands.autocomplete = (cm, hint, options) => {\n    cm.showHint({ hint, ...options });\n  };\n}\n"
  },
  {
    "path": "packages/bruno-app/src/utils/codemirror/autocomplete.spec.js",
    "content": "const { describe, it, expect, jest, beforeEach, afterEach } = require('@jest/globals');\n\nconst _mockedCodemirror = {\n  commands: {},\n  getCursor: jest.fn(),\n  getLine: jest.fn(),\n  getRange: jest.fn(),\n  showHint: jest.fn(),\n  on: jest.fn(),\n  off: jest.fn(),\n  state: {}\n};\n\njest.mock('codemirror', () => {\n  return _mockedCodemirror;\n});\n\n// Import the functions to test\nimport {\n  getAutoCompleteHints,\n  setupAutoComplete,\n  extractNextSegmentSuggestions\n} from './autocomplete';\n\ndescribe('Bruno Autocomplete', () => {\n  let mockedCodemirror;\n\n  beforeEach(() => {\n    mockedCodemirror = _mockedCodemirror;\n  });\n\n  afterEach(() => {\n    jest.clearAllMocks();\n  });\n\n  describe('getAutoCompleteHints', () => {\n    describe('Variable autocomplete', () => {\n      it('should provide variable hints when typing inside double curly braces', () => {\n        mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 9 });\n        mockedCodemirror.getLine.mockReturnValue('{{envVar}}');\n        mockedCodemirror.getRange.mockReturnValue('{{envVar');\n        const allVariables = {\n          envVar1: 'value1',\n          envVar2: 'value2'\n        };\n\n        const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], {\n          showHintsFor: ['variables']\n        });\n\n        expect(result).toBeTruthy();\n        expect(result.list).toEqual(\n          expect.arrayContaining([\n            expect.objectContaining({ text: 'envVar1', displayText: 'envVar1' }),\n            expect.objectContaining({ text: 'envVar2', displayText: 'envVar2' })\n          ])\n        );\n      });\n\n      it('should include mock data functions with $ prefix', () => {\n        mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 9 });\n        mockedCodemirror.getRange.mockReturnValue('{{$randomI');\n\n        const result = getAutoCompleteHints(mockedCodemirror, {}, [], {\n          showHintsFor: ['variables']\n        });\n\n        expect(result.list).toEqual(\n          expect.arrayContaining([\n            expect.objectContaining({ displayText: '$randomInt' })\n          ])\n        );\n      });\n\n      it('should handle process environment variables', () => {\n        const allVariables = {\n          process: {\n            env: {\n              NODE_ENV: 'development',\n              API_URL: 'https://api.example.com'\n            }\n          }\n        };\n\n        mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 14 });\n        mockedCodemirror.getRange.mockReturnValue('{{process.env.N');\n\n        const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], {\n          showHintsFor: ['variables']\n        });\n\n        expect(result).toBeTruthy();\n        expect(result.list).toEqual(\n          expect.arrayContaining([\n            expect.objectContaining({ displayText: 'NODE_ENV' })\n          ])\n        );\n      });\n\n      it('should skip special internal keys', () => {\n        mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 10 });\n        mockedCodemirror.getRange.mockReturnValue('{{path');\n\n        const allVariables = {\n          pathParams: { id: '123' },\n          maskedEnvVariables: { secret: '***' },\n          path: 'value'\n        };\n\n        const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], {\n          showHintsFor: ['variables']\n        });\n\n        expect(result).toBeTruthy();\n        expect(result.list).toEqual(\n          expect.arrayContaining([\n            expect.objectContaining({ displayText: 'path' })\n          ])\n        );\n        expect(result.list).not.toEqual(\n          expect.arrayContaining([\n            expect.objectContaining({ displayText: 'pathParams' })\n          ])\n        );\n      });\n\n      it('should handle nested object variables', () => {\n        mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 12 });\n        mockedCodemirror.getRange.mockReturnValue('{{config.api.');\n\n        const allVariables = {\n          'config.api.url': 'https://echo.usebruno.com',\n          'config.api.client_id': 'client_id',\n          'config.api.client_secret': 'client_secret',\n          'config.app.name': 'bruno'\n        };\n\n        const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], {\n          showHintsFor: ['variables']\n        });\n\n        expect(result).toBeTruthy();\n        expect(result.list).toEqual(\n          expect.arrayContaining([\n            expect.objectContaining({ displayText: 'url' }),\n            expect.objectContaining({ displayText: 'client_id' }),\n            expect.objectContaining({ displayText: 'client_secret' })\n          ])\n        );\n      });\n    });\n\n    describe('API object context (req, res, bru)', () => {\n      const testCases = [\n        {\n          name: 'req object',\n          input: 'req.',\n          expected: ['url', 'method', 'headers', 'body', 'timeout']\n        },\n        {\n          name: 'res object',\n          input: 'res.',\n          expected: ['status', 'statusText', 'headers', 'body', 'responseTime']\n        },\n        {\n          name: 'bru object',\n          input: 'bru.',\n          expected: ['cwd()', 'getEnvName()', 'getProcessEnv(key)', 'hasEnvVar(key)', 'getEnvVar(key)']\n        }\n      ];\n\n      testCases.forEach(({ name, input, expected }) => {\n        it(`should provide ${name} hints`, () => {\n          mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: input.length });\n          mockedCodemirror.getLine.mockReturnValue(input);\n          mockedCodemirror.getRange.mockReturnValue(input);\n\n          const result = getAutoCompleteHints(mockedCodemirror, {}, [], {\n            showHintsFor: ['req', 'res', 'bru']\n          });\n\n          expect(result).toBeTruthy();\n          expect(result.list).toEqual(expect.arrayContaining(expected));\n        });\n      });\n\n      it('should provide method hints for nested req objects', () => {\n        mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 7 });\n        mockedCodemirror.getLine.mockReturnValue('req.get');\n        mockedCodemirror.getRange.mockReturnValue('req.get');\n\n        const result = getAutoCompleteHints(mockedCodemirror, {}, [], {\n          showHintsFor: ['req']\n        });\n\n        expect(result).toBeTruthy();\n        expect(result.list).toEqual(\n          expect.arrayContaining([\n            'getUrl()',\n            'getMethod()',\n            'getAuthMode()',\n            'getHeader(name)',\n            'getHeaders()',\n            'getBody()',\n            'getTimeout()',\n            'getExecutionMode()',\n            'getName()'\n          ])\n        );\n      });\n\n      it('should handle bru.runner sub-object', () => {\n        mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 11 });\n        mockedCodemirror.getLine.mockReturnValue('bru.runner.');\n        mockedCodemirror.getRange.mockReturnValue('bru.runner.');\n\n        const result = getAutoCompleteHints(mockedCodemirror, {}, [], {\n          showHintsFor: ['bru']\n        });\n\n        expect(result).toBeTruthy();\n        expect(result.list).toEqual(\n          expect.arrayContaining([\n            'setNextRequest(requestName)',\n            'skipRequest()',\n            'stopExecution()'\n          ])\n        );\n      });\n    });\n\n    describe('Custom hints and anyword context', () => {\n      it('should provide custom anyword hints', () => {\n        mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 7 });\n        mockedCodemirror.getLine.mockReturnValue('Content-');\n        mockedCodemirror.getRange.mockReturnValue('Content-');\n\n        const customHints = ['Content-Type', 'Content-Encoding', 'Content-Length'];\n\n        const result = getAutoCompleteHints(mockedCodemirror, {}, customHints, {\n          showHintsFor: ['variables']\n        });\n\n        expect(result).toBeTruthy();\n        expect(result.list).toEqual(\n          expect.arrayContaining(['Content-Type', 'Content-Encoding', 'Content-Length'])\n        );\n      });\n\n      it('should handle progressive hints for custom hints', () => {\n        mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 6 });\n        mockedCodemirror.getLine.mockReturnValue('utils.');\n        mockedCodemirror.getRange.mockReturnValue('utils.');\n\n        const customHints = ['utils.string.trim', 'utils.string.capitalize', 'utils.array.map'];\n\n        const result = getAutoCompleteHints(mockedCodemirror, {}, customHints, {\n          showHintsFor: ['variables']\n        });\n\n        expect(result).toBeTruthy();\n        expect(result.list).toEqual(\n          expect.arrayContaining(['string', 'array'])\n        );\n      });\n    });\n\n    describe('Filtering and options', () => {\n      beforeEach(() => {\n        mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 4 });\n        mockedCodemirror.getLine.mockReturnValue('req.');\n        mockedCodemirror.getRange.mockReturnValue('req.');\n      });\n\n      it('should respect showHintsFor option for excluding hints', () => {\n        const options = { showHintsFor: ['res', 'bru'] };\n        const result = getAutoCompleteHints(mockedCodemirror, {}, [], options);\n\n        expect(result).toBeNull();\n      });\n\n      it('should show hints when included in showHintsFor', () => {\n        const options = { showHintsFor: ['req'] };\n        const result = getAutoCompleteHints(mockedCodemirror, {}, [], options);\n\n        expect(result).toBeTruthy();\n        expect(result.list).toEqual(\n          expect.arrayContaining(['url', 'method'])\n        );\n      });\n\n      it('should filter variables based on showHintsFor', () => {\n        mockedCodemirror.getLine.mockReturnValue('{{varNa}}');\n        mockedCodemirror.getRange.mockReturnValue('{{varNa');\n\n        const allVariables = { envVar1: 'value1' };\n        const options = { showHintsFor: ['req', 'res', 'bru'] };\n\n        const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], options);\n\n        expect(result).toBeNull();\n      });\n\n      it('should limit results to 50 hints', () => {\n        mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 10 });\n        mockedCodemirror.getLine.mockReturnValue('{{varName}}');\n        mockedCodemirror.getRange.mockReturnValue('{{v');\n\n        const allVariables = {};\n        for (let i = 0; i < 100; i++) {\n          allVariables[`var${i}`] = `value${i}`;\n        }\n\n        const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], {\n          showHintsFor: ['variables']\n        });\n\n        expect(result).toBeTruthy();\n        expect(result.list.length).toBeLessThanOrEqual(50);\n      });\n\n      it('should sort hints alphabetically', () => {\n        mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 10 });\n        mockedCodemirror.getLine.mockReturnValue('{{v.');\n        mockedCodemirror.getRange.mockReturnValue('{{v.');\n\n        const allVariables = {\n          'v.zebra': 'value1',\n          'v.apple': 'value2',\n          'v.banana': 'value3'\n        };\n\n        const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], {\n          showHintsFor: ['variables']\n        });\n\n        expect(result).toBeTruthy();\n        const displayTexts = result.list.map((item) =>\n          typeof item === 'object' ? item.displayText : item\n        );\n\n        const userVars = displayTexts.filter((text) => !text.startsWith('$'));\n        expect(userVars).toEqual(['apple', 'banana', 'zebra']);\n      });\n    });\n\n    describe('Edge cases', () => {\n      it('should return null when no word is found at cursor', () => {\n        mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 0 });\n        mockedCodemirror.getLine.mockReturnValue('   ');\n        mockedCodemirror.getRange.mockReturnValue('');\n\n        const result = getAutoCompleteHints(mockedCodemirror, {}, []);\n\n        expect(result).toBeNull();\n      });\n\n      it('should handle empty or null variables', () => {\n        mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 10 });\n        mockedCodemirror.getLine.mockReturnValue('{{varName}}');\n        mockedCodemirror.getRange.mockReturnValue('{{varName');\n\n        const emptyResult = getAutoCompleteHints(mockedCodemirror, {}, []);\n        const nullResult = getAutoCompleteHints(mockedCodemirror, null, []);\n\n        expect(emptyResult).toBeNull();\n        expect(nullResult).toBeNull();\n      });\n\n      it('should handle cursor at end of line', () => {\n        const line = 'req.getHea';\n        mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: line.length });\n        mockedCodemirror.getLine.mockReturnValue(line);\n        mockedCodemirror.getRange.mockReturnValue(line);\n\n        const result = getAutoCompleteHints(mockedCodemirror, {}, [], {\n          showHintsFor: ['req']\n        });\n\n        expect(result).toBeTruthy();\n        expect(result.list).toEqual(\n          expect.arrayContaining(['getHeader(name)', 'getHeaders()'])\n        );\n      });\n\n      it('should provide deleteHeader and deleteHeaders hints for req.delete prefix', () => {\n        const line = 'req.delete';\n        mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: line.length });\n        mockedCodemirror.getLine.mockReturnValue(line);\n        mockedCodemirror.getRange.mockReturnValue(line);\n\n        const result = getAutoCompleteHints(mockedCodemirror, {}, [], {\n          showHintsFor: ['req']\n        });\n\n        expect(result).toBeTruthy();\n        expect(result.list).toEqual(\n          expect.arrayContaining(['deleteHeader(name)', 'deleteHeaders(data)'])\n        );\n      });\n\n      it('should handle case-insensitive matching', () => {\n        mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 10 });\n        mockedCodemirror.getLine.mockReturnValue('{{varName}}');\n        mockedCodemirror.getRange.mockReturnValue('{{var');\n\n        const allVariables = {\n          variable1: 'value1',\n          Variable2: 'value2',\n          VARIABLE3: 'value3'\n        };\n\n        const result = getAutoCompleteHints(mockedCodemirror, allVariables, [], {\n          showHintsFor: ['variables']\n        });\n\n        expect(result).toBeTruthy();\n        expect(result.list.length).toBe(3);\n      });\n    });\n  });\n\n  describe('extractNextSegmentSuggestions', () => {\n    describe('prefix matching', () => {\n      it('should extract the current segment for a partial prefix match', () => {\n        const hints = ['req.getUrl()', 'req.getMethod()', 'req.setUrl(url)'];\n        const result = extractNextSegmentSuggestions(hints, 'req.get');\n\n        expect(result).toEqual(['getMethod()', 'getUrl()']);\n      });\n\n      it('should return the next segment after a trailing dot', () => {\n        const hints = ['bru.cookies.jar()', 'bru.runner.skipRequest()'];\n        const result = extractNextSegmentSuggestions(hints, 'bru.');\n\n        expect(result).toEqual(['cookies', 'runner']);\n      });\n\n      it('should return the last segment on exact match', () => {\n        const hints = ['req.url'];\n        const result = extractNextSegmentSuggestions(hints, 'req.url');\n\n        expect(result).toEqual(['url']);\n      });\n\n      it('should deduplicate segments from multiple hints', () => {\n        const hints = ['bru.cookies.jar().getCookie(url, name, callback)', 'bru.cookies.jar().getCookies(url, callback)'];\n        const result = extractNextSegmentSuggestions(hints, 'bru.');\n\n        expect(result).toEqual(['cookies']);\n      });\n\n      it('should extract top-level segment when input has no dots', () => {\n        const hints = ['req.url', 'req.getUrl()', 'res.url'];\n        const result = extractNextSegmentSuggestions(hints, 'r');\n\n        expect(result).toEqual(['req', 'res']);\n      });\n    });\n\n    describe('substring matching', () => {\n      it('should return full hints for substring-only matches', () => {\n        const hints = ['base_url', 'api_url', 'url_prefix'];\n        const result = extractNextSegmentSuggestions(hints, 'url');\n\n        // url_prefix is a prefix match (segment), base_url and api_url are substring matches (full hints)\n        expect(result).toEqual(['url_prefix', 'api_url', 'base_url']);\n      });\n\n      it('should return full hints for dotted substring matches', () => {\n        const hints = ['req.getUrl()', 'req.setUrl(url)', 'req.url'];\n        const result = extractNextSegmentSuggestions(hints, 'Url');\n\n        expect(result).toEqual(['req.getUrl()', 'req.setUrl(url)', 'req.url']);\n      });\n\n      it('should not include hints that do not contain the input', () => {\n        const hints = ['base_url', 'api_key', 'url_prefix'];\n        const result = extractNextSegmentSuggestions(hints, 'url');\n\n        expect(result).not.toContain('api_key');\n      });\n    });\n\n    describe('ordering', () => {\n      it('should return prefix matches before substring matches', () => {\n        const hints = ['base_url', 'url_prefix'];\n        const result = extractNextSegmentSuggestions(hints, 'url');\n\n        // url_prefix is prefix → segment \"url_prefix\"; base_url is substring → full hint\n        expect(result).toEqual(['url_prefix', 'base_url']);\n      });\n\n      it('should sort prefix matches alphabetically among themselves', () => {\n        const hints = ['req.setUrl(url)', 'req.getUrl()', 'req.getMethod()'];\n        const result = extractNextSegmentSuggestions(hints, 'req.');\n\n        expect(result).toEqual(['getMethod()', 'getUrl()', 'setUrl(url)']);\n      });\n\n      it('should sort substring matches alphabetically among themselves', () => {\n        const hints = ['z_url', 'a_url'];\n        const result = extractNextSegmentSuggestions(hints, 'url');\n\n        // Both are substring-only matches\n        expect(result).toEqual(['a_url', 'z_url']);\n      });\n    });\n\n    describe('case insensitivity', () => {\n      it('should match prefix regardless of case', () => {\n        const hints = ['Content-Type', 'Content-Length'];\n        const result = extractNextSegmentSuggestions(hints, 'content');\n\n        expect(result).toEqual(['Content-Length', 'Content-Type']);\n      });\n\n      it('should match substring regardless of case', () => {\n        const hints = ['X-Custom-Type', 'Accept'];\n        const result = extractNextSegmentSuggestions(hints, 'type');\n\n        expect(result).toEqual(['X-Custom-Type']);\n      });\n    });\n\n    describe('edge cases', () => {\n      it('should return an empty array when no hints match', () => {\n        const hints = ['foo', 'bar', 'baz'];\n        const result = extractNextSegmentSuggestions(hints, 'xyz');\n\n        expect(result).toEqual([]);\n      });\n\n      it('should return an empty array for empty hints list', () => {\n        const result = extractNextSegmentSuggestions([], 'url');\n\n        expect(result).toEqual([]);\n      });\n\n      it('should handle single-character input', () => {\n        const hints = ['apple', 'banana', 'avocado'];\n        const result = extractNextSegmentSuggestions(hints, 'a');\n\n        // apple and avocado are prefix matches, banana contains 'a' as substring\n        expect(result).toEqual(['apple', 'avocado', 'banana']);\n      });\n    });\n  });\n\n  describe('setupAutoComplete', () => {\n    let mockGetAllVariables;\n    let cleanupFn;\n\n    beforeEach(() => {\n      mockGetAllVariables = jest.fn(() => ({ }));\n      mockedCodemirror.state = {};\n    });\n\n    afterEach(() => {\n      if (cleanupFn) {\n        cleanupFn();\n      }\n    });\n\n    describe('Setup and cleanup', () => {\n      it('should setup keyup event listener and return cleanup function', () => {\n        const options = { getAllVariables: mockGetAllVariables };\n        cleanupFn = setupAutoComplete(mockedCodemirror, options);\n\n        expect(mockedCodemirror.on).toHaveBeenCalledWith('keyup', expect.any(Function));\n        expect(cleanupFn).toBeInstanceOf(Function);\n\n        cleanupFn();\n        expect(mockedCodemirror.off).toHaveBeenCalledWith('keyup', expect.any(Function));\n      });\n\n      it('should not setup if editor is null', () => {\n        const result = setupAutoComplete(null, { getAllVariables: mockGetAllVariables });\n\n        expect(result).toBeUndefined();\n        expect(mockedCodemirror.on).not.toHaveBeenCalled();\n      });\n    });\n\n    describe('Event handling', () => {\n      it('should trigger hints on character key press', () => {\n        const options = {\n          getAllVariables: mockGetAllVariables,\n          showHintsFor: ['req']\n        };\n        cleanupFn = setupAutoComplete(mockedCodemirror, options);\n        const keyupHandler = mockedCodemirror.on.mock.calls[0][1];\n\n        mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 4 });\n        mockedCodemirror.getLine.mockReturnValue('req.');\n        mockedCodemirror.getRange.mockReturnValue('req.');\n\n        const mockEvent = { key: 'a' };\n        keyupHandler(mockedCodemirror, mockEvent);\n\n        expect(mockGetAllVariables).toHaveBeenCalled();\n        expect(mockedCodemirror.showHint).toHaveBeenCalled();\n      });\n\n      it('should not trigger hints on non-character keys', () => {\n        const options = { getAllVariables: mockGetAllVariables };\n        cleanupFn = setupAutoComplete(mockedCodemirror, options);\n        const keyupHandler = mockedCodemirror.on.mock.calls[0][1];\n\n        const nonCharacterKeys = ['Shift', 'Tab', 'Enter', 'Escape', 'ArrowUp', 'ArrowDown', 'Meta'];\n\n        nonCharacterKeys.forEach((key) => {\n          const mockEvent = { key };\n          keyupHandler(mockedCodemirror, mockEvent);\n        });\n\n        expect(mockedCodemirror.showHint).not.toHaveBeenCalled();\n      });\n\n      it('should close existing completion when no hints available', () => {\n        const options = { getAllVariables: mockGetAllVariables };\n        cleanupFn = setupAutoComplete(mockedCodemirror, options);\n        const keyupHandler = mockedCodemirror.on.mock.calls[0][1];\n\n        const mockCompletion = { close: jest.fn() };\n        mockedCodemirror.state.completionActive = mockCompletion;\n\n        mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 0 });\n        mockedCodemirror.getLine.mockReturnValue('req.bodyy');\n        mockedCodemirror.getRange.mockReturnValue('');\n\n        const mockEvent = { key: 'a' };\n        keyupHandler(mockedCodemirror, mockEvent);\n\n        expect(mockCompletion.close).toHaveBeenCalled();\n      });\n\n      it('should pass options to getAutoCompleteHints', () => {\n        const options = {\n          getAllVariables: mockGetAllVariables,\n          showHintsFor: ['req']\n        };\n        cleanupFn = setupAutoComplete(mockedCodemirror, options);\n        const keyupHandler = mockedCodemirror.on.mock.calls[0][1];\n\n        mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 4 });\n        mockedCodemirror.getLine.mockReturnValue('req.');\n        mockedCodemirror.getRange.mockReturnValue('req.');\n\n        const mockEvent = { key: 'a' };\n        keyupHandler(mockedCodemirror, mockEvent);\n\n        expect(mockedCodemirror.showHint).toHaveBeenCalledWith({\n          hint: expect.any(Function),\n          completeSingle: false\n        });\n      });\n    });\n\n    describe('Click event handling (showHintsOnClick)', () => {\n      it('should setup mousedown event listener when showHintsOnClick is enabled', () => {\n        const options = {\n          getAllVariables: mockGetAllVariables,\n          showHintsOnClick: true\n        };\n        cleanupFn = setupAutoComplete(mockedCodemirror, options);\n\n        expect(mockedCodemirror.on).toHaveBeenCalledWith('keyup', expect.any(Function));\n        expect(mockedCodemirror.on).toHaveBeenCalledWith('mousedown', expect.any(Function));\n        expect(mockedCodemirror.on).toHaveBeenCalledTimes(2);\n      });\n\n      it('should not setup mousedown event listener when showHintsOnClick is disabled', () => {\n        const options = {\n          getAllVariables: mockGetAllVariables,\n          showHintsOnClick: false\n        };\n        cleanupFn = setupAutoComplete(mockedCodemirror, options);\n\n        expect(mockedCodemirror.on).toHaveBeenCalledWith('keyup', expect.any(Function));\n        expect(mockedCodemirror.on).toHaveBeenCalledTimes(1);\n      });\n\n      it('should not setup mousedown event listener when showHintsOnClick is undefined', () => {\n        const options = {\n          getAllVariables: mockGetAllVariables\n        };\n        cleanupFn = setupAutoComplete(mockedCodemirror, options);\n\n        expect(mockedCodemirror.on).toHaveBeenCalledWith('keyup', expect.any(Function));\n        expect(mockedCodemirror.on).toHaveBeenCalledTimes(1);\n      });\n\n      it('should show hints on click when showHintsOnClick is enabled', () => {\n        jest.useFakeTimers();\n\n        const mockGetAnywordAutocompleteHints = jest.fn(() => ['Content-Type', 'Accept']);\n        const options = {\n          getAllVariables: mockGetAllVariables,\n          getAnywordAutocompleteHints: mockGetAnywordAutocompleteHints,\n          showHintsOnClick: true,\n          showHintsFor: ['req', 'variables']\n        };\n        cleanupFn = setupAutoComplete(mockedCodemirror, options);\n\n        // Find the click handler (mousedown event)\n        const clickHandler = mockedCodemirror.on.mock.calls.find((call) => call[0] === 'mousedown')[1];\n\n        mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 0 });\n\n        clickHandler(mockedCodemirror);\n\n        // Run all timers to execute the setTimeout\n        jest.runAllTimers();\n\n        expect(mockGetAllVariables).toHaveBeenCalled();\n        expect(mockGetAnywordAutocompleteHints).toHaveBeenCalled();\n        expect(mockedCodemirror.showHint).toHaveBeenCalled();\n\n        jest.useRealTimers();\n      });\n\n      it('should not show hints on click when showHintsOnClick is disabled', () => {\n        const options = {\n          getAllVariables: mockGetAllVariables,\n          showHintsOnClick: false\n        };\n        cleanupFn = setupAutoComplete(mockedCodemirror, options);\n\n        // There should be no mousedown handler\n        const mousedownCalls = mockedCodemirror.on.mock.calls.filter((call) => call[0] === 'mousedown');\n        expect(mousedownCalls).toHaveLength(0);\n      });\n\n      it('should cleanup mousedown event listener when showHintsOnClick was enabled', () => {\n        const options = {\n          getAllVariables: mockGetAllVariables,\n          showHintsOnClick: true\n        };\n        cleanupFn = setupAutoComplete(mockedCodemirror, options);\n\n        cleanupFn();\n\n        expect(mockedCodemirror.off).toHaveBeenCalledWith('keyup', expect.any(Function));\n        expect(mockedCodemirror.off).toHaveBeenCalledWith('mousedown', expect.any(Function));\n        expect(mockedCodemirror.off).toHaveBeenCalledTimes(2);\n      });\n\n      it('should only cleanup keyup event listener when showHintsOnClick was disabled', () => {\n        const options = {\n          getAllVariables: mockGetAllVariables,\n          showHintsOnClick: false\n        };\n        cleanupFn = setupAutoComplete(mockedCodemirror, options);\n\n        cleanupFn();\n\n        expect(mockedCodemirror.off).toHaveBeenCalledWith('keyup', expect.any(Function));\n        expect(mockedCodemirror.off).toHaveBeenCalledTimes(1);\n      });\n\n      it('should show all available hints on click based on showHintsFor configuration', () => {\n        jest.useFakeTimers();\n\n        const mockGetAnywordAutocompleteHints = jest.fn(() => ['Content-Type', 'Accept']);\n        const options = {\n          getAllVariables: mockGetAllVariables.mockReturnValue({\n            envVar1: 'value1',\n            envVar2: 'value2'\n          }),\n          getAnywordAutocompleteHints: mockGetAnywordAutocompleteHints,\n          showHintsOnClick: true,\n          showHintsFor: ['req', 'variables']\n        };\n        cleanupFn = setupAutoComplete(mockedCodemirror, options);\n\n        // Find the click handler (mousedown event)\n        const clickHandler = mockedCodemirror.on.mock.calls.find((call) => call[0] === 'mousedown')[1];\n\n        const mockCursor = { line: 0, ch: 0 };\n        mockedCodemirror.getCursor.mockReturnValue(mockCursor);\n\n        clickHandler(mockedCodemirror);\n\n        // Run all timers to execute the setTimeout\n        jest.runAllTimers();\n\n        expect(mockedCodemirror.showHint).toHaveBeenCalledWith({\n          hint: expect.any(Function),\n          completeSingle: false\n        });\n\n        // Verify the hint function returns the expected structure\n        const hintCall = mockedCodemirror.showHint.mock.calls[0][0];\n        const hintResult = hintCall.hint();\n\n        expect(hintResult).toEqual({\n          list: expect.any(Array),\n          from: mockCursor,\n          to: mockCursor\n        });\n        expect(hintResult.list.length).toBeGreaterThan(0);\n\n        jest.useRealTimers();\n      });\n\n      it('should not show hints on click when no hints are available', () => {\n        const options = {\n          getAllVariables: mockGetAllVariables.mockReturnValue({}),\n          getAnywordAutocompleteHints: jest.fn(() => []),\n          showHintsOnClick: true,\n          showHintsFor: []\n        };\n        cleanupFn = setupAutoComplete(mockedCodemirror, options);\n\n        // Find the click handler (mousedown event)\n        const clickHandler = mockedCodemirror.on.mock.calls.find((call) => call[0] === 'mousedown')[1];\n\n        mockedCodemirror.getCursor.mockReturnValue({ line: 0, ch: 0 });\n\n        clickHandler(mockedCodemirror);\n\n        expect(mockedCodemirror.showHint).not.toHaveBeenCalled();\n      });\n    });\n  });\n\n  describe('CodeMirror integration', () => {\n    it('should define autocomplete command if not exists', () => {\n      delete mockedCodemirror.commands.autocomplete;\n\n      jest.isolateModules(() => {\n        require('./autocomplete');\n      });\n\n      expect(mockedCodemirror.commands.autocomplete).toBeDefined();\n      expect(typeof mockedCodemirror.commands.autocomplete).toBe('function');\n    });\n\n    it('should not override existing autocomplete command', () => {\n      const existingCommand = jest.fn();\n      mockedCodemirror.commands.autocomplete = existingCommand;\n\n      jest.isolateModules(() => {\n        require('./autocomplete');\n      });\n\n      expect(mockedCodemirror.commands.autocomplete).toBe(existingCommand);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/utils/codemirror/autocompleteConstants.js",
    "content": "export const MimeTypes = [\n  'application/atom+xml',\n  'application/ecmascript',\n  'application/json',\n  'application/vnd.api+json',\n  'application/javascript',\n  'application/octet-stream',\n  'application/ogg',\n  'application/pdf',\n  'application/postscript',\n  'application/rdf+xml',\n  'application/rss+xml',\n  'application/soap+xml',\n  'application/font-woff',\n  'application/x-yaml',\n  'application/xhtml+xml',\n  'application/xml',\n  'application/xml-dtd',\n  'application/xop+xml',\n  'application/zip',\n  'application/gzip',\n  'application/graphql',\n  'application/x-www-form-urlencoded',\n  'audio/basic',\n  'audio/L24',\n  'audio/mp4',\n  'audio/mpeg',\n  'audio/ogg',\n  'audio/vorbis',\n  'audio/vnd.rn-realaudio',\n  'audio/vnd.wave',\n  'audio/webm',\n  'image/gif',\n  'image/jpeg',\n  'image/pjpeg',\n  'image/png',\n  'image/svg+xml',\n  'image/tiff',\n  'message/http',\n  'message/imdn+xml',\n  'message/partial',\n  'message/rfc822',\n  'multipart/mixed',\n  'multipart/alternative',\n  'multipart/related',\n  'multipart/form-data',\n  'multipart/signed',\n  'multipart/encrypted',\n  'text/cmd',\n  'text/css',\n  'text/csv',\n  'text/html',\n  'text/plain',\n  'text/vcard',\n  'text/xml'\n];\n"
  },
  {
    "path": "packages/bruno-app/src/utils/codemirror/brunoVarInfo.js",
    "content": "/**\n *  Copyright (c) 2017, Facebook, Inc.\n *  All rights reserved.\n *\n *  This source code is licensed under the BSD-style license found in the\n *  LICENSE file at https://github.com/graphql/codemirror-graphql/tree/v0.8.3\n */\n\nimport { interpolate, mockDataFunctions, timeBasedDynamicVars } from '@usebruno/common';\nimport { getVariableScope, isVariableSecret, getAllVariables, findCollectionByUid, findItemInCollectionByItemUid } from 'utils/collections';\nimport { updateVariableInScope } from 'providers/ReduxStore/slices/collections/actions';\nimport store from 'providers/ReduxStore';\nimport { defineCodeMirrorBrunoVariablesMode } from 'utils/common/codemirror';\nimport { MaskedEditor } from 'utils/common/masked-editor';\nimport { setupAutoComplete } from 'utils/codemirror/autocomplete';\nimport { variableNameRegex } from 'utils/common/regex';\n\nlet CodeMirror;\nconst SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;\nconst { get } = require('lodash');\n\nconst COPY_ICON_SVG_TEXT = `\n    <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n      <rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"></rect>\n      <path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path>\n    </svg>\n`;\n\nconst CHECKMARK_ICON_SVG_TEXT = `\n<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n  <polyline points=\"20,6 9,17 4,12\"></polyline>\n</svg>\n`;\n\nconst COPY_SUCCESS_COLOR = '#22c55e';\n\nexport const COPY_SUCCESS_TIMEOUT = 1000;\n\n// Editor height constraints\nconst EDITOR_MIN_HEIGHT = 1.75;\nconst EDITOR_MAX_HEIGHT = 11.125;\n\n/**\n * Calculate editor height based on content, clamped between min and max\n * @param {number} contentHeight - The actual content height from CodeMirror\n * @returns {number} The clamped height value\n */\nconst calculateEditorHeight = (contentHeight) => {\n  const contentHeightRem = contentHeight / 16;\n  return Math.min(Math.max(contentHeightRem, EDITOR_MIN_HEIGHT), EDITOR_MAX_HEIGHT);\n};\n\nconst EYE_ICON_SVG = `\n  <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n    <path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\"></path>\n    <circle cx=\"12\" cy=\"12\" r=\"3\"></circle>\n  </svg>\n`;\n\nconst EYE_OFF_ICON_SVG = `\n  <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n    <path d=\"M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24\"></path>\n    <line x1=\"1\" y1=\"1\" x2=\"23\" y2=\"23\"></line>\n  </svg>\n`;\n\nconst getScopeLabel = (scopeType) => {\n  const labels = {\n    'global': 'Global',\n    'environment': 'Environment',\n    'collection': 'Collection',\n    'folder': 'Folder',\n    'request': 'Request',\n    'runtime': 'Runtime',\n    'process.env': 'Process Env',\n    'dynamic': 'Dynamic',\n    'oauth2': 'OAuth2',\n    'undefined': 'Undefined',\n    'pathParam': 'Path Param'\n  };\n  return labels[scopeType] || scopeType;\n};\n\n// Get the masked display text based on the value length\nconst getMaskedDisplay = (value) => {\n  const contentLength = (value || '').length;\n  return contentLength > 0 ? '*'.repeat(contentLength) : '';\n};\n\n// Update the value display based on the secret and masked state\nconst updateValueDisplay = (valueDisplay, value, isSecret, isMasked, isRevealed) => {\n  if ((isSecret || isMasked) && !isRevealed) {\n    valueDisplay.textContent = getMaskedDisplay(value);\n    return;\n  }\n\n  if (typeof value === 'object') {\n    valueDisplay.textContent = value === null ? 'null' : JSON.stringify(value, null, 2);\n    return;\n  }\n\n  if (typeof value === 'undefined' || value === undefined) {\n    valueDisplay.textContent = '';\n    return;\n  }\n\n  valueDisplay.textContent = value;\n};\n\n// Check if the raw value contains references to secret variables\nconst containsSecretVariableReferences = (rawValue, collection, item) => {\n  if (!rawValue || typeof rawValue !== 'string') {\n    return false;\n  }\n\n  // Match all variable references like {{varName}}\n  const variableReferencePattern = /\\{\\{([^}]+)\\}\\}/g;\n  const matches = rawValue.matchAll(variableReferencePattern);\n\n  for (const match of matches) {\n    const referencedVarName = match[1].trim();\n\n    // Get scope info for the referenced variable\n    const referencedScopeInfo = getVariableScope(referencedVarName, collection, item);\n\n    // Check if the referenced variable is a secret\n    if (referencedScopeInfo && isVariableSecret(referencedScopeInfo)) {\n      return true;\n    }\n  }\n\n  return false;\n};\n\nconst getCopyButton = (variableValue, onCopyCallback) => {\n  const copyButton = document.createElement('button');\n\n  copyButton.className = 'copy-button';\n  copyButton.innerHTML = COPY_ICON_SVG_TEXT;\n  copyButton.type = 'button';\n\n  let isCopied = false;\n\n  copyButton.addEventListener('click', (e) => {\n    e.stopPropagation();\n    e.preventDefault();\n\n    // Prevent clicking if showing success checkmark\n    if (isCopied) {\n      return;\n    }\n\n    navigator.clipboard\n      .writeText(variableValue)\n      .then(() => {\n        isCopied = true;\n        copyButton.innerHTML = CHECKMARK_ICON_SVG_TEXT;\n        copyButton.style.color = COPY_SUCCESS_COLOR;\n        copyButton.style.cursor = 'default';\n        copyButton.classList.add('copy-success');\n\n        setTimeout(() => {\n          isCopied = false;\n          copyButton.innerHTML = COPY_ICON_SVG_TEXT;\n          copyButton.style.color = '#989898';\n          copyButton.style.cursor = 'pointer';\n          copyButton.classList.remove('copy-success');\n        }, COPY_SUCCESS_TIMEOUT);\n\n        // Call callback if provided\n        if (onCopyCallback) {\n          onCopyCallback();\n        }\n      })\n      .catch((err) => {\n        console.error('Failed to copy to clipboard:', err.message);\n      });\n  });\n\n  return copyButton;\n};\n\nexport const renderVarInfo = (token, options) => {\n  // Extract variable name and value based on token\n  const { variableName, variableValue } = extractVariableInfo(token.string, options.variables);\n\n  // Don't show popover if we can't extract a variable name or if it's empty/whitespace\n  if (!variableName || !variableName.trim()) {\n    return;\n  }\n\n  const collection = options.collection;\n  const item = options.item;\n\n  // Check if this is a dynamic/faker variable (starts with \"$\")\n  let scopeInfo;\n  if (variableName.startsWith('$oauth2.')) {\n    // OAuth2 token variable - look up in variables object\n    const oauth2Value = get(options.variables, variableName);\n    scopeInfo = {\n      type: 'oauth2',\n      value: oauth2Value !== undefined ? oauth2Value : '',\n      data: null,\n      isValidOAuth2Variable: oauth2Value !== undefined\n    };\n  } else if (variableName.startsWith('$')) {\n    const fakerKeyword = variableName.substring(1); // Remove the $ prefix\n    const fakerFunction = mockDataFunctions[fakerKeyword];\n    const isTimeBased = timeBasedDynamicVars.has(fakerKeyword);\n    scopeInfo = {\n      type: 'dynamic',\n      value: '',\n      data: null,\n      isValidDynamicVariable: !!fakerFunction,\n      isTimeBased\n    };\n  } else if (variableName.startsWith('process.env.')) {\n    // Check if this is a process.env variable (starts with \"process.env.\")\n    scopeInfo = {\n      type: 'process.env',\n      value: variableValue || '',\n      data: null\n    };\n  } else if (token.string.startsWith('/:')) {\n    scopeInfo = {\n      type: 'pathParam',\n      value: variableValue || '',\n      data: { item }\n    };\n  } else {\n    // Detect variable scope\n    scopeInfo = getVariableScope(variableName, collection, item);\n\n    // If variable doesn't exist in any scope, determine scope based on context\n    if (!scopeInfo) {\n      if (item && item.uid) {\n        // Determine if item is a folder or request\n        const isFolder = item.type === 'folder';\n\n        if (isFolder) {\n          // We're in folder settings - create as folder variable\n          scopeInfo = {\n            type: 'folder',\n            value: '', // Empty value for new variable\n            data: { folder: item, variable: null } // variable is null since it doesn't exist yet\n          };\n        } else {\n          // We're in a request - create as request variable\n          scopeInfo = {\n            type: 'request',\n            value: '', // Empty value for new variable\n            data: { item, variable: null } // variable is null since it doesn't exist yet\n          };\n        }\n      } else if (collection) {\n        // No item context but we have collection - create as collection variable\n        scopeInfo = {\n          type: 'collection',\n          value: '',\n          data: { collection, variable: null }\n        };\n      } else {\n        // No context at all, show as undefined\n        scopeInfo = {\n          type: 'undefined',\n          value: '',\n          data: null\n        };\n      }\n    }\n  }\n\n  // Check if a runtime variable exists with the same name (even if scope is detected as collection/folder/environment)\n  const hasRuntimeVariable = collection && collection.runtimeVariables && collection.runtimeVariables[variableName];\n  // Check if variable is read-only (process.env, runtime, dynamic/faker, oauth2, and undefined variables cannot be edited)\n  const isReadOnly = scopeInfo.type === 'process.env' || scopeInfo.type === 'runtime' || scopeInfo.type === 'dynamic' || scopeInfo.type === 'oauth2' || scopeInfo.type === 'undefined' || hasRuntimeVariable;\n\n  // Get raw value from scope\n  const rawValue = scopeInfo.value || '';\n\n  // Check if variable should be masked:\n  const isSecret = scopeInfo.type !== 'undefined' ? isVariableSecret(scopeInfo) : false;\n  const hasSecretReferences = containsSecretVariableReferences(rawValue, collection, item);\n  const shouldMaskValue = isSecret || hasSecretReferences;\n\n  const isMasked = options.variables?.maskedEnvVariables?.includes(variableName);\n\n  const into = document.createElement('div');\n  into.className = 'bruno-var-info-container';\n\n  // Header: Variable name + Scope badge\n  const header = document.createElement('div');\n  header.className = 'var-info-header';\n\n  const varName = document.createElement('span');\n  varName.className = 'var-name';\n  varName.textContent = variableName;\n\n  const scopeBadge = document.createElement('span');\n  scopeBadge.className = 'var-scope-badge';\n\n  // Check if a runtime variable exists - if so, show Runtime scope (even if detected as collection/folder/environment)\n  const displayScopeType = hasRuntimeVariable ? 'runtime' : (scopeInfo ? scopeInfo.type : 'Unknown');\n  // Show scope label with indication if it's a new variable\n  const scopeLabel = getScopeLabel(displayScopeType);\n  const isNewVariable = scopeInfo && scopeInfo.data && scopeInfo.data.variable === null;\n  scopeBadge.textContent = isNewVariable ? `${scopeLabel}` : scopeLabel;\n\n  header.appendChild(varName);\n  header.appendChild(scopeBadge);\n  into.appendChild(header);\n\n  // Check if variable name is valid\n  const isValidVariableName = scopeInfo.type === 'process.env' || scopeInfo.type === 'dynamic' || scopeInfo.type === 'oauth2' || variableNameRegex.test(variableName);\n\n  // Show warning if variable name is invalid\n  if (!isValidVariableName) {\n    const warningNote = document.createElement('div');\n    warningNote.className = 'var-warning-note';\n    warningNote.textContent = 'Invalid variable name! Variables must only contain alpha-numeric characters, \"-\", \"_\", \".\"';\n    into.appendChild(warningNote);\n\n    // Don't show value or any other content for invalid variable names\n    return into;\n  }\n\n  // Show warning for invalid dynamic variable (starts with $ but not a valid dynamic function)\n  if (scopeInfo.type === 'dynamic' && !scopeInfo.isValidDynamicVariable) {\n    const warningNote = document.createElement('div');\n    warningNote.className = 'var-warning-note';\n    warningNote.textContent = `Unknown dynamic variable \"${variableName}\". Check the variable name.`;\n    into.appendChild(warningNote);\n    return into;\n  }\n\n  // For valid dynamic variables, show appropriate read-only note based on type\n  if (scopeInfo.type === 'dynamic' && scopeInfo.isValidDynamicVariable) {\n    const readOnlyNote = document.createElement('div');\n    readOnlyNote.className = 'var-readonly-note';\n    readOnlyNote.textContent = scopeInfo.isTimeBased\n      ? 'Generates current timestamp on each request'\n      : 'Generates random value on each request';\n    into.appendChild(readOnlyNote);\n    return into;\n  }\n\n  // Show warning for invalid OAuth2 variable (token not found)\n  if (scopeInfo.type === 'oauth2' && !scopeInfo.isValidOAuth2Variable) {\n    const warningNote = document.createElement('div');\n    warningNote.className = 'var-warning-note';\n    warningNote.textContent = `OAuth2 token not found. Make sure you have fetched the token with the correct Token ID.`;\n    into.appendChild(warningNote);\n    return into;\n  }\n\n  // Value container with icons\n  const valueContainer = document.createElement('div');\n  valueContainer.className = 'var-value-container';\n\n  // Create editable value display/editor (if editable)\n  if (!isReadOnly && scopeInfo) {\n    // Handle secret/masked variables state\n    let isRevealed = false;\n\n    // Create display element (shows interpolated value by default)\n    const valueDisplay = document.createElement('div');\n    valueDisplay.className = 'var-value-editable-display';\n    // Mask the displayed value if it contains secrets or references to secrets\n    updateValueDisplay(valueDisplay, variableValue, shouldMaskValue, isMasked, false);\n\n    // Create container for CodeMirror (hidden by default)\n    const editorContainer = document.createElement('div');\n    editorContainer.className = 'var-value-editor';\n    editorContainer.style.display = 'none'; // Hidden initially\n\n    // Detect current theme from DOM\n    const isDarkTheme = document.documentElement.classList.contains('dark');\n    const cmTheme = isDarkTheme ? 'monokai' : 'default';\n\n    // Get all variables for syntax highlighting (but prevent recursive tooltips)\n    const allVariables = collection ? getAllVariables(collection, item) : {};\n\n    // Create CodeMirror instance\n    const cmEditor = CodeMirror(editorContainer, {\n      value: typeof rawValue === 'string' ? rawValue : String(rawValue), // Use raw value (e.g., {{echo-host}} not resolved value) (ensure it's always a string for CodeMirror) #usebruno/bruno/#6265\n      mode: 'brunovariables',\n      theme: cmTheme,\n      lineWrapping: true,\n      lineNumbers: false,\n      brunoVarInfo: false, // Disable tooltips within the editor to prevent recursion\n      scrollbarStyle: null,\n      viewportMargin: Infinity\n    });\n\n    // Setup variable mode for syntax highlighting\n    defineCodeMirrorBrunoVariablesMode(allVariables, 'text/plain', false, true);\n    cmEditor.setOption('mode', 'brunovariables');\n\n    // Setup autocomplete\n    const getAllVariablesHandler = () => allVariables;\n    const autoCompleteOptions = {\n      getAllVariables: getAllVariablesHandler,\n      showHintsFor: ['variables']\n    };\n    const autoCompleteCleanup = setupAutoComplete(cmEditor, autoCompleteOptions);\n\n    // Handle secret/masked variables\n    let maskedEditor = null;\n\n    if (shouldMaskValue || isMasked) {\n      maskedEditor = new MaskedEditor(cmEditor);\n      maskedEditor.enable();\n    }\n\n    // Store original value for comparison and track editing state\n    let originalValue = rawValue;\n    let isEditing = false;\n\n    cmEditor.setOption('extraKeys', {\n      'Enter': (cm) => {\n        // Enter: save and blur\n        cm.getInputField().blur();\n      },\n      'Shift-Enter': (cm) => {\n        // Shift+Enter: insert new line\n        cm.replaceSelection('\\n', 'end');\n      }\n    });\n\n    // Dynamically adjust editor height as content changes\n    cmEditor.on('change', () => {\n      if (isEditing) {\n        // Use requestAnimationFrame for smoother updates after DOM changes\n        requestAnimationFrame(() => {\n          cmEditor.refresh();\n          // Get height from the actual rendered sizer element (more accurate)\n          const sizer = cmEditor.getWrapperElement().querySelector('.CodeMirror-sizer');\n          const contentHeight = sizer ? sizer.clientHeight : cmEditor.getScrollInfo().height;\n          const newHeight = calculateEditorHeight(contentHeight);\n          editorContainer.style.height = `${newHeight}rem`;\n        });\n      }\n    });\n\n    // Icons container (top-right)\n    const iconsContainer = document.createElement('div');\n    iconsContainer.className = 'var-icons';\n\n    // Eye toggle button (show if the displayed value is masked)\n    if (shouldMaskValue || isMasked) {\n      const toggleButton = document.createElement('button');\n      toggleButton.className = 'secret-toggle-button';\n      toggleButton.innerHTML = EYE_ICON_SVG;\n      toggleButton.type = 'button';\n\n      toggleButton.addEventListener('click', (e) => {\n        e.stopPropagation();\n        e.preventDefault();\n        isRevealed = !isRevealed;\n\n        // Update icon\n        toggleButton.innerHTML = isRevealed ? EYE_OFF_ICON_SVG : EYE_ICON_SVG;\n\n        // Update display mode\n        updateValueDisplay(valueDisplay, variableValue, shouldMaskValue, isMasked, isRevealed);\n\n        // Update editor mode\n        if (maskedEditor) {\n          isRevealed ? maskedEditor.disable() : maskedEditor.enable();\n        }\n\n        // Refocus the editor if it's currently in edit mode\n        if (isEditing) {\n          setTimeout(() => {\n            cmEditor.focus();\n          }, 0);\n        }\n      });\n\n      iconsContainer.appendChild(toggleButton);\n    }\n\n    // Copy button (copy actual value, not masked)\n    const copyButton = getCopyButton(variableValue || '', () => {\n      // Refocus the editor if it's currently in edit mode\n      if (isEditing) {\n        setTimeout(() => {\n          cmEditor.focus();\n        }, 0);\n      }\n    });\n    iconsContainer.appendChild(copyButton);\n\n    valueContainer.appendChild(valueDisplay);\n    valueContainer.appendChild(editorContainer);\n    valueContainer.appendChild(iconsContainer);\n\n    // Click on display to enter edit mode\n    valueDisplay.addEventListener('click', () => {\n      if (isEditing) return;\n\n      isEditing = true;\n      valueDisplay.style.display = 'none';\n      editorContainer.style.display = 'block';\n\n      // Focus the editor and ensure proper sizing\n      setTimeout(() => {\n        cmEditor.refresh();\n        cmEditor.focus();\n\n        // Set cursor to end of content\n        const lineCount = cmEditor.lineCount();\n        const lastLine = cmEditor.getLine(lineCount - 1);\n        cmEditor.setCursor(lineCount - 1, lastLine ? lastLine.length : 0);\n\n        // Adjust height based on content\n        const contentHeight = cmEditor.getScrollInfo().height;\n        editorContainer.style.height = `${calculateEditorHeight(contentHeight)}rem`;\n      }, 0);\n    });\n\n    // Save on blur and return to display mode\n    cmEditor.on('blur', () => {\n      const newValue = cmEditor.getValue();\n\n      // Switch back to display mode\n      editorContainer.style.display = 'none';\n      editorContainer.style.height = `${EDITOR_MIN_HEIGHT}rem`; // Reset to minimum height\n      valueDisplay.style.display = 'block';\n      isEditing = false;\n\n      if (newValue !== originalValue) {\n        // Dispatch Redux action to update variable\n        const dispatch = store.dispatch;\n        dispatch(updateVariableInScope(variableName, newValue, scopeInfo, collection.uid))\n          .then(() => {\n            originalValue = newValue;\n\n            // Re-fetch scopeInfo to get the updated variable reference after save\n            const state = store.getState();\n            const freshCollection = findCollectionByUid(state.collections.collections, collection.uid);\n            if (collection) {\n              const freshItem = item ? findItemInCollectionByItemUid(freshCollection, item.uid) : null;\n              const updatedScopeInfo = getVariableScope(variableName, freshCollection, freshItem);\n              if (updatedScopeInfo) {\n                scopeInfo = updatedScopeInfo;\n              }\n            }\n\n            // Re-interpolate the new value to show the resolved value in display\n            const interpolatedValue = interpolate(newValue, allVariables);\n            // Check if the NEW value contains secret references\n            const newHasSecretRefs = containsSecretVariableReferences(newValue, collection, item);\n            const newShouldMask = isSecret || newHasSecretRefs;\n            updateValueDisplay(valueDisplay, interpolatedValue, newShouldMask, isMasked, isRevealed);\n          })\n          .catch((err) => {\n            console.error('Failed to update variable:', err);\n            // Revert on error\n            cmEditor.setValue(originalValue);\n            updateValueDisplay(valueDisplay, variableValue, shouldMaskValue, isMasked, isRevealed);\n          });\n      }\n    });\n\n    // Store references for cleanup\n    valueContainer._cmEditor = cmEditor;\n    valueContainer._maskedEditor = maskedEditor;\n    valueContainer._autoCompleteCleanup = autoCompleteCleanup;\n  } else {\n    // Read-only display (for runtime, process.env, undefined variables)\n    let isRevealed = false;\n\n    const valueDisplay = document.createElement('div');\n    valueDisplay.className = 'var-value-display';\n    // For read-only variables, still check if they reference secrets\n    updateValueDisplay(valueDisplay, variableValue, shouldMaskValue, isMasked, false);\n\n    // Icons container\n    const iconsContainer = document.createElement('div');\n    iconsContainer.className = 'var-icons';\n\n    // Eye toggle button (for read-only variables that reference secrets or are masked)\n    if (shouldMaskValue || isMasked) {\n      const toggleButton = document.createElement('button');\n      toggleButton.className = 'secret-toggle-button';\n      toggleButton.innerHTML = EYE_ICON_SVG;\n      toggleButton.type = 'button';\n\n      toggleButton.addEventListener('click', (e) => {\n        e.stopPropagation();\n        e.preventDefault();\n        isRevealed = !isRevealed;\n\n        toggleButton.innerHTML = isRevealed ? EYE_OFF_ICON_SVG : EYE_ICON_SVG;\n        updateValueDisplay(valueDisplay, variableValue, shouldMaskValue, isMasked, isRevealed);\n      });\n\n      iconsContainer.appendChild(toggleButton);\n    }\n\n    // Copy button (always copy actual value, not masked)\n    const copyButton = getCopyButton(variableValue || '');\n    iconsContainer.appendChild(copyButton);\n\n    valueContainer.appendChild(valueDisplay);\n    valueContainer.appendChild(iconsContainer);\n\n    // Read-only note\n    if (scopeInfo.type === 'process.env') {\n      const readOnlyNote = document.createElement('div');\n      readOnlyNote.className = 'var-readonly-note';\n      readOnlyNote.textContent = 'read-only';\n      into.appendChild(readOnlyNote);\n    } else if (scopeInfo.type === 'runtime' || hasRuntimeVariable) {\n      const readOnlyNote = document.createElement('div');\n      readOnlyNote.className = 'var-readonly-note';\n      readOnlyNote.textContent = 'Set by scripts (read-only)';\n      into.appendChild(readOnlyNote);\n    } else if (scopeInfo.type === 'oauth2') {\n      const readOnlyNote = document.createElement('div');\n      readOnlyNote.className = 'var-readonly-note';\n      readOnlyNote.textContent = 'read-only';\n      into.appendChild(readOnlyNote);\n    } else if (scopeInfo.type === 'undefined') {\n      const readOnlyNote = document.createElement('div');\n      readOnlyNote.className = 'var-readonly-note';\n      readOnlyNote.textContent = 'No active environment';\n      into.appendChild(readOnlyNote);\n    }\n  }\n\n  into.appendChild(valueContainer);\n\n  return into;\n};\n\nif (!SERVER_RENDERED) {\n  CodeMirror = require('codemirror');\n\n  // Global state to track active popup\n  let activePopup = null;\n\n  CodeMirror.defineOption('brunoVarInfo', false, function (cm, options, old) {\n    if (old && old !== CodeMirror.Init) {\n      const oldOnMouseOver = cm.state.brunoVarInfo.onMouseOver;\n      CodeMirror.off(cm.getWrapperElement(), 'mouseover', oldOnMouseOver);\n      clearTimeout(cm.state.brunoVarInfo.hoverTimeout);\n      delete cm.state.brunoVarInfo;\n    }\n\n    if (options) {\n      const state = (cm.state.brunoVarInfo = createState(options));\n      state.onMouseOver = onMouseOver.bind(null, cm);\n      CodeMirror.on(cm.getWrapperElement(), 'mouseover', state.onMouseOver);\n    }\n  });\n\n  function createState(options) {\n    return {\n      options: options instanceof Function ? { render: options } : options === true ? {} : options\n    };\n  }\n\n  function getHoverTime(cm) {\n    const options = cm.state.brunoVarInfo.options;\n    return (options && options.hoverTime) || 50;\n  }\n\n  function onMouseOver(cm, e) {\n    const state = cm.state.brunoVarInfo;\n    const target = e.target || e.srcElement;\n\n    // Prevent new tooltips if one is already active\n    if (target.nodeName !== 'SPAN' || state.hoverTimeout !== undefined) {\n      return;\n    }\n    // Show popover for both valid and invalid variables\n    if (!target.classList.contains('cm-variable-valid') && !target.classList.contains('cm-variable-invalid')) {\n      return;\n    }\n\n    const box = target.getBoundingClientRect();\n\n    const onMouseMove = function () {\n      clearTimeout(state.hoverTimeout);\n      state.hoverTimeout = setTimeout(onHover, hoverTime);\n    };\n\n    const onMouseOut = function () {\n      CodeMirror.off(document, 'mousemove', onMouseMove);\n      CodeMirror.off(cm.getWrapperElement(), 'mouseout', onMouseOut);\n      clearTimeout(state.hoverTimeout);\n      state.hoverTimeout = undefined;\n    };\n\n    const onHover = function () {\n      CodeMirror.off(document, 'mousemove', onMouseMove);\n      CodeMirror.off(cm.getWrapperElement(), 'mouseout', onMouseOut);\n      state.hoverTimeout = undefined;\n      onMouseHover(cm, box);\n    };\n\n    const hoverTime = getHoverTime(cm);\n    state.hoverTimeout = setTimeout(onHover, hoverTime);\n\n    CodeMirror.on(document, 'mousemove', onMouseMove);\n    CodeMirror.on(cm.getWrapperElement(), 'mouseout', onMouseOut);\n  }\n\n  function onMouseHover(cm, box) {\n    const pos = cm.coordsChar({\n      left: (box.left + box.right) / 2,\n      top: (box.top + box.bottom) / 2\n    });\n\n    const state = cm.state.brunoVarInfo;\n    const options = state.options;\n\n    const line = cm.getLine(pos.line);\n    if (!line) return;\n\n    // ---------- 1) MODE: Double-Brace Variable {{ ... }} ----------\n    // We check this first as it's the most common variable type.\n    if (line.includes('{{') && line.includes('}}')) {\n      // Check if the cursor is roughly between a '{{' to the left and '}}' to the right\n      if (line.lastIndexOf('{{', pos.ch) !== -1 && line.indexOf('}}', pos.ch) !== -1) {\n        let start = pos.ch;\n        let end = pos.ch;\n\n        // Scan LEFT to find the nearest '{{'\n        while (start > 0) {\n          const leftTwo = line.substring(start - 2, start);\n          if (leftTwo === '{{') {\n            start -= 2;\n            break;\n          }\n          // If we hit a '}}' while looking for '{{', the cursor is outside a pair\n          if (leftTwo === '}}') break;\n          start--;\n        }\n\n        // Validate we actually found a '{{'\n        if (start >= 0 && line.substring(start, start + 2) === '{{') {\n          // Scan RIGHT to find the nearest '}}'\n          while (end < line.length) {\n            const rightTwo = line.substring(end, end + 2);\n            if (rightTwo === '}}') {\n              end += 2;\n              break;\n            }\n            // If we hit another '{{' before a closing '}}', the structure is invalid\n            if (rightTwo === '{{') {\n              end = line.length + 1;\n              break;\n            }\n            end++;\n          }\n\n          // Validate the final string and show popup\n          if (end <= line.length && line.substring(end - 2, end) === '}}') {\n            const fullVariableString = line.substring(start, end);\n            const inner = fullVariableString.slice(2, -2).trim();\n\n            if (inner) {\n              const token = { string: fullVariableString, start, end };\n              const brunoVarInfo = renderVarInfo(token, options);\n              if (brunoVarInfo) {\n                showPopup(cm, box, brunoVarInfo);\n                return; // EXIT: We found a variable, don't look for path params\n              }\n            }\n          }\n        }\n      }\n    }\n\n    // ---------- 2) MODE: Path Parameter /:varName ----------\n    // If we didn't return from the brace logic, check if cursor is on a path param\n    const pathParamStart = line.substring(0, pos.ch + 1).lastIndexOf('/:');\n\n    if (pathParamStart !== -1) {\n      let pathValueEnd = pathParamStart + 2;\n\n      // Path params end at the next URL separator (/, ?, &, =) or end of line\n      const separators = ['/', '?', '&', '='];\n      while (pathValueEnd < line.length && !separators.includes(line[pathValueEnd])) {\n        pathValueEnd++;\n      }\n\n      // Check if cursor is actually inside the detected /:param range\n      if (pos.ch >= pathParamStart && pos.ch < pathValueEnd) {\n        const fullVariableString = line.substring(pathParamStart, pathValueEnd);\n\n        // Ensure it's not just \"/:\" but has a name (e.g., \"/:id\")\n        if (fullVariableString.length > 2) {\n          const token = {\n            string: fullVariableString,\n            start: pathParamStart,\n            end: pathValueEnd\n          };\n          const brunoVarInfo = renderVarInfo(token, options);\n          if (brunoVarInfo) {\n            showPopup(cm, box, brunoVarInfo);\n            return; // EXIT: Popup shown\n          }\n        }\n      }\n    }\n  }\n\n  function showPopup(cm, box, brunoVarInfo) {\n    // If there's already an active popup, remove it first\n    if (activePopup && activePopup.parentNode) {\n      activePopup.parentNode.removeChild(activePopup);\n      activePopup = null;\n    }\n\n    const popup = document.createElement('div');\n    popup.className = 'CodeMirror-brunoVarInfo';\n    popup.appendChild(brunoVarInfo);\n    document.body.appendChild(popup);\n\n    // Track this popup as the active one\n    activePopup = popup;\n\n    const popupBox = popup.getBoundingClientRect();\n    const popupStyle = popup.currentStyle || window.getComputedStyle(popup);\n    const popupWidth\n      = popupBox.right - popupBox.left + parseFloat(popupStyle.marginLeft) + parseFloat(popupStyle.marginRight);\n    const popupHeight\n      = popupBox.bottom - popupBox.top + parseFloat(popupStyle.marginTop) + parseFloat(popupStyle.marginBottom);\n\n    const GAP_REM = 0.5;\n    const EDGE_MARGIN_REM = 0.9375;\n\n    // Position below the trigger by default with gap\n    let topPos = box.bottom + (GAP_REM * 16);\n\n    // Check if there's enough space below; if not, position above\n    if (popupHeight > window.innerHeight - box.bottom - (EDGE_MARGIN_REM * 16) && box.top > window.innerHeight - box.bottom) {\n      topPos = box.top - popupHeight - (GAP_REM * 16);\n    }\n\n    // Ensure it doesn't go off the top of the screen\n    if (topPos < 0) {\n      topPos = box.bottom + (GAP_REM * 16);\n    }\n\n    // Horizontal positioning - align to left of trigger\n    let leftPos = box.left;\n\n    // Ensure it doesn't go off the right edge\n    if (leftPos + popupWidth > window.innerWidth - (EDGE_MARGIN_REM * 16)) {\n      leftPos = window.innerWidth - popupWidth - (EDGE_MARGIN_REM * 16);\n    }\n\n    // Ensure it doesn't go off the left edge\n    if (leftPos < 0) {\n      leftPos = 0;\n    }\n\n    popup.style.opacity = 1;\n    popup.style.top = `${topPos / 16}rem`;\n    popup.style.left = `${leftPos / 16}rem`;\n\n    let popupTimeout;\n\n    const onMouseOverPopup = function () {\n      clearTimeout(popupTimeout);\n    };\n\n    const onMouseOut = function () {\n      clearTimeout(popupTimeout);\n      popupTimeout = setTimeout(hidePopup, 500);\n    };\n\n    const hidePopup = function () {\n      CodeMirror.off(popup, 'mouseover', onMouseOverPopup);\n      CodeMirror.off(popup, 'mouseout', onMouseOut);\n      CodeMirror.off(cm.getWrapperElement(), 'mouseout', onMouseOut);\n      CodeMirror.off(cm, 'change', onEditorChange);\n\n      // Cleanup CodeMirror and MaskedEditor instances\n      const valueContainer = popup.querySelector('.var-value-container');\n      if (valueContainer) {\n        // Cleanup autocomplete\n        if (valueContainer._autoCompleteCleanup) {\n          valueContainer._autoCompleteCleanup();\n          valueContainer._autoCompleteCleanup = null;\n        }\n\n        // Cleanup MaskedEditor\n        if (valueContainer._maskedEditor) {\n          valueContainer._maskedEditor.destroy();\n          valueContainer._maskedEditor = null;\n        }\n\n        // Cleanup CodeMirror\n        if (valueContainer._cmEditor) {\n          valueContainer._cmEditor.getWrapperElement().remove();\n          valueContainer._cmEditor = null;\n        }\n      }\n\n      // Clear the active popup reference\n      if (activePopup === popup) {\n        activePopup = null;\n      }\n\n      if (popup.style.opacity) {\n        popup.style.opacity = 0;\n        setTimeout(function () {\n          if (popup.parentNode) {\n            popup.parentNode.removeChild(popup);\n          }\n        }, 600);\n      } else if (popup.parentNode) {\n        popup.parentNode.removeChild(popup);\n      }\n    };\n\n    // Hide popup when user types in the main editor\n    const onEditorChange = function () {\n      hidePopup();\n    };\n\n    CodeMirror.on(popup, 'mouseover', onMouseOverPopup);\n    CodeMirror.on(popup, 'mouseout', onMouseOut);\n    CodeMirror.on(cm.getWrapperElement(), 'mouseout', onMouseOut);\n    CodeMirror.on(cm, 'change', onEditorChange);\n  }\n}\n\nexport const extractVariableInfo = (str, variables) => {\n  let variableName;\n  let variableValue;\n\n  if (!str || !str.length || typeof str !== 'string') {\n    return { variableName, variableValue };\n  }\n\n  // Regex to match double brace variable syntax: {{variableName}}\n  const DOUBLE_BRACE_PATTERN = /\\{\\{([^}]+)\\}\\}/;\n\n  if (DOUBLE_BRACE_PATTERN.test(str)) {\n    variableName = str.replace('{{', '').replace('}}', '').trim();\n    // Don't return empty variable names\n    if (!variableName) {\n      return { variableName: undefined, variableValue: undefined };\n    }\n    variableValue = interpolate(get(variables, variableName), variables);\n  } else if (str.startsWith('/:')) {\n    variableName = str.replace('/:', '').trim();\n    // Don't return empty variable names\n    if (!variableName) {\n      return { variableName: undefined, variableValue: undefined };\n    }\n    variableValue = variables?.pathParams?.[variableName];\n  } else if (str.startsWith('{{') && str.endsWith('}}')) {\n    // Handle cases like {{}} or {{   }} (empty or whitespace only)\n    // These don't match the pattern but look like variables\n    return { variableName: undefined, variableValue: undefined };\n  } else {\n    // direct variable reference (e.g., for numeric values in JSON mode or plain variable names)\n    variableName = str;\n    variableValue = interpolate(get(variables, variableName), variables);\n  }\n\n  return { variableName, variableValue };\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/codemirror/brunoVarInfo.spec.js",
    "content": "import { interpolate } from '@usebruno/common';\nimport { COPY_SUCCESS_TIMEOUT, extractVariableInfo, renderVarInfo } from './brunoVarInfo';\n\n// Mock the dependencies\njest.mock('@usebruno/common', () => ({\n  interpolate: jest.fn(),\n  mockDataFunctions: {\n    randomFirstName: jest.fn(() => 'John'),\n    randomLastName: jest.fn(() => 'Doe'),\n    randomEmail: jest.fn(() => 'john.doe@example.com'),\n    randomUUID: jest.fn(() => '123e4567-e89b-12d3-a456-426614174000'),\n    timestamp: jest.fn(() => '1704067200'),\n    isoTimestamp: jest.fn(() => '2024-01-01T00:00:00.000Z')\n  },\n  timeBasedDynamicVars: new Set(['timestamp', 'isoTimestamp'])\n}));\n\njest.mock('providers/ReduxStore', () => ({\n  default: {\n    dispatch: jest.fn(),\n    getState: jest.fn()\n  }\n}));\n\njest.mock('providers/ReduxStore/slices/collections/actions', () => ({\n  updateVariableInScope: jest.fn()\n}));\n\njest.mock('utils/collections', () => ({\n  getVariableScope: jest.fn(),\n  isVariableSecret: jest.fn(),\n  getAllVariables: jest.fn(),\n  findEnvironmentInCollection: jest.fn()\n}));\n\njest.mock('utils/common/codemirror', () => ({\n  defineCodeMirrorBrunoVariablesMode: jest.fn()\n}));\n\njest.mock('utils/common/masked-editor', () => ({\n  MaskedEditor: jest.fn()\n}));\n\njest.mock('utils/codemirror/autocomplete', () => ({\n  setupAutoComplete: jest.fn(() => jest.fn())\n}));\n\n// Mock CodeMirror\nglobal.CodeMirror = jest.fn((element, options) => {\n  const mockEditor = {\n    getValue: jest.fn(() => options.value || ''),\n    setValue: jest.fn(),\n    on: jest.fn(),\n    off: jest.fn(),\n    refresh: jest.fn(),\n    focus: jest.fn(),\n    options: options || {},\n    getWrapperElement: jest.fn(() => element)\n  };\n  return mockEditor;\n});\n\ndescribe('extractVariableInfo', () => {\n  let mockVariables;\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n\n    // Setup mock variables\n    mockVariables = {\n      apiKey: 'test-api-key-123',\n      baseUrl: 'https://api.example.com',\n      userId: 12345,\n      pathParams: {\n        id: 'user-123',\n        slug: 'test-post'\n      }\n    };\n\n    // Setup interpolate mock\n    interpolate.mockImplementation((value, variables) => {\n      if (typeof value === 'string' && value.includes('{{')) {\n        return value.replace(/\\{\\{(\\w+)\\}\\}/g, (match, key) => variables[key] || match);\n      }\n      return value;\n    });\n  });\n\n  describe('input validation', () => {\n    it('should return undefined for null input', () => {\n      const result = extractVariableInfo(null, mockVariables);\n      expect(result.variableName).toBeUndefined();\n      expect(result.variableValue).toBeUndefined();\n    });\n\n    it('should return undefined for undefined input', () => {\n      const result = extractVariableInfo(undefined, mockVariables);\n      expect(result.variableName).toBeUndefined();\n      expect(result.variableValue).toBeUndefined();\n    });\n\n    it('should return undefined for empty string', () => {\n      const result = extractVariableInfo('', mockVariables);\n      expect(result.variableName).toBeUndefined();\n      expect(result.variableValue).toBeUndefined();\n    });\n\n    it('should return undefined for non-string input', () => {\n      const result = extractVariableInfo(123, mockVariables);\n      expect(result.variableName).toBeUndefined();\n      expect(result.variableValue).toBeUndefined();\n    });\n\n    it('should return undefined for object input', () => {\n      const result = extractVariableInfo({}, mockVariables);\n      expect(result.variableName).toBeUndefined();\n      expect(result.variableValue).toBeUndefined();\n    });\n  });\n\n  describe('double brace format ({{variableName}})', () => {\n    it('should parse double brace variables correctly', () => {\n      const result = extractVariableInfo('{{apiKey}}', mockVariables);\n\n      expect(result).toEqual({\n        variableName: 'apiKey',\n        variableValue: 'test-api-key-123'\n      });\n\n      expect(interpolate).toHaveBeenCalledWith('test-api-key-123', mockVariables);\n    });\n\n    it('should handle whitespace in double brace variables', () => {\n      const result = extractVariableInfo('{{  apiKey  }}', mockVariables);\n\n      expect(result).toEqual({\n        variableName: 'apiKey',\n        variableValue: 'test-api-key-123'\n      });\n    });\n\n    it('should return undefined variableValue for non-existent double brace variable', () => {\n      const result = extractVariableInfo('{{nonExistent}}', mockVariables);\n\n      expect(result).toEqual({\n        variableName: 'nonExistent',\n        variableValue: undefined\n      });\n    });\n\n    it('should return undefined for empty double brace variables', () => {\n      const result = extractVariableInfo('{{}}', mockVariables);\n\n      expect(result).toEqual({\n        variableName: undefined,\n        variableValue: undefined\n      });\n    });\n\n    it('should return undefined for whitespace-only double brace variables', () => {\n      const result = extractVariableInfo('{{   }}', mockVariables);\n\n      expect(result).toEqual({\n        variableName: undefined,\n        variableValue: undefined\n      });\n    });\n  });\n\n  describe('path parameter format (/:variableName)', () => {\n    it('should parse path parameter variables correctly', () => {\n      const result = extractVariableInfo('/:id', mockVariables);\n\n      expect(result).toEqual({\n        variableName: 'id',\n        variableValue: 'user-123'\n      });\n    });\n\n    it('should return undefined for non-existent path parameter', () => {\n      const result = extractVariableInfo('/:nonExistent', mockVariables);\n\n      expect(result).toEqual({\n        variableName: 'nonExistent',\n        variableValue: undefined\n      });\n    });\n\n    it('should handle missing pathParams object', () => {\n      const variablesWithoutPathParams = { ...mockVariables };\n      delete variablesWithoutPathParams.pathParams;\n\n      const result = extractVariableInfo('/:id', variablesWithoutPathParams);\n\n      expect(result).toEqual({\n        variableName: 'id',\n        variableValue: undefined\n      });\n    });\n\n    it('should handle null pathParams', () => {\n      const variablesWithNullPathParams = { ...mockVariables, pathParams: null };\n\n      const result = extractVariableInfo('/:id', variablesWithNullPathParams);\n\n      expect(result).toEqual({\n        variableName: 'id',\n        variableValue: undefined\n      });\n    });\n\n    it('should return undefined for empty path parameters', () => {\n      const result = extractVariableInfo('/:', mockVariables);\n\n      expect(result).toEqual({\n        variableName: undefined,\n        variableValue: undefined\n      });\n    });\n\n    it('should return undefined for whitespace-only path parameters', () => {\n      const result = extractVariableInfo('/:   ', mockVariables);\n\n      expect(result).toEqual({\n        variableName: undefined,\n        variableValue: undefined\n      });\n    });\n  });\n\n  describe('direct variable format', () => {\n    it('should parse direct variable names correctly', () => {\n      const result = extractVariableInfo('baseUrl', mockVariables);\n\n      expect(result).toEqual({\n        variableName: 'baseUrl',\n        variableValue: 'https://api.example.com'\n      });\n\n      expect(interpolate).toHaveBeenCalledWith('https://api.example.com', mockVariables);\n    });\n\n    it('should handle numeric variable values', () => {\n      const result = extractVariableInfo('userId', mockVariables);\n\n      expect(result).toEqual({\n        variableName: 'userId',\n        variableValue: 12345\n      });\n    });\n\n    it('should return undefined for non-existent direct variable', () => {\n      const result = extractVariableInfo('nonExistent', mockVariables);\n\n      expect(result).toEqual({\n        variableName: 'nonExistent',\n        variableValue: undefined\n      });\n    });\n\n    it('should handle variables with special characters', () => {\n      mockVariables['special-var_name'] = 'special-var_value';\n\n      const result = extractVariableInfo('special-var_name', mockVariables);\n\n      expect(result).toEqual({\n        variableName: 'special-var_name',\n        variableValue: 'special-var_value'\n      });\n    });\n  });\n\n  describe('edge cases', () => {\n    it('should handle empty variables object', () => {\n      const result = extractVariableInfo('{{apiKey}}', {});\n\n      expect(result).toEqual({\n        variableName: 'apiKey',\n        variableValue: undefined\n      });\n    });\n\n    it('should handle null variables object', () => {\n      const result = extractVariableInfo('{{apiKey}}', null);\n\n      expect(result).toEqual({\n        variableName: 'apiKey',\n        variableValue: undefined\n      });\n    });\n\n    it('should handle undefined variables object', () => {\n      const result = extractVariableInfo('{{apiKey}}', undefined);\n\n      expect(result).toEqual({\n        variableName: 'apiKey',\n        variableValue: undefined\n      });\n    });\n  });\n\n  describe('return value structure', () => {\n    it('should always return an object with variableName and variableValue properties', () => {\n      const result = extractVariableInfo('{{apiKey}}', mockVariables);\n\n      expect(result).toHaveProperty('variableName');\n      expect(result).toHaveProperty('variableValue');\n      expect(typeof result.variableName).toBe('string');\n    });\n\n    it('should return variableValue as the interpolated value', () => {\n      const result = extractVariableInfo('{{apiKey}}', mockVariables);\n\n      expect(result.variableValue).toBe('test-api-key-123');\n    });\n  });\n});\n\ndescribe('renderVarInfo', () => {\n  let clipboardText = '';\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n    jest.useFakeTimers();\n\n    // setup mock clipboard\n    clipboardText = '';\n    Object.defineProperty(navigator, 'clipboard', {\n      value: {\n        writeText: jest.fn((text) => {\n          if (text === 'cause-clipboard-error') {\n            return Promise.reject(new Error('Clipboard error'));\n          }\n\n          clipboardText = text;\n\n          return Promise.resolve();\n        })\n      },\n      configurable: true\n    });\n\n    // mock console.error\n    console.error = jest.fn();\n  });\n\n  afterEach(() => {\n    jest.useRealTimers();\n  });\n\n  function setupRender(variables, collection = null, item = null) {\n    const result = renderVarInfo({ string: '{{apiKey}}' }, { variables, collection, item });\n    if (!result) return { result: null, containerDiv: null, valueDisplay: null, copyButton: null };\n\n    const containerDiv = result;\n    const valueDisplay = containerDiv.querySelector('.var-value-editable-display') || containerDiv.querySelector('.var-value-display');\n    const copyButton = containerDiv.querySelector('.copy-button');\n\n    return { result, containerDiv, valueDisplay, copyButton };\n  }\n\n  describe('popup functionality', () => {\n    it('should create a popup', () => {\n      const { result } = setupRender({ apiKey: 'test-value' });\n\n      expect(result).toBeDefined();\n    });\n\n    it('should create a popup with the correct variable name and value', () => {\n      const { valueDisplay } = setupRender({ apiKey: 'test-value' });\n\n      expect(valueDisplay.textContent).toBe('test-value');\n    });\n\n    it('should correctly mask the variable value in the popup', () => {\n      const { valueDisplay } = setupRender({\n        apiKey: 'test-value',\n        maskedEnvVariables: ['apiKey']\n      });\n\n      expect(valueDisplay.textContent).toBe('**********');\n    });\n  });\n\n  describe('copy button functionality', () => {\n    it('should create a copy button', () => {\n      const { copyButton } = setupRender({ apiKey: 'test-value' });\n\n      expect(copyButton).toBeDefined();\n    });\n\n    it('should copy the variable value to the clipboard', () => {\n      const { copyButton } = setupRender({ apiKey: 'test-value' });\n\n      copyButton.click();\n\n      expect(clipboardText).toBe('test-value');\n      expect(navigator.clipboard.writeText).toHaveBeenCalledWith('test-value');\n    });\n\n    it('should copy the variable value of masked variables to the clipboard', () => {\n      const { copyButton } = setupRender({ apiKey: 'test-value', maskedEnvVariables: ['apiKey'] });\n\n      copyButton.click();\n\n      expect(clipboardText).toBe('test-value');\n      expect(navigator.clipboard.writeText).toHaveBeenCalledWith('test-value');\n    });\n\n    it('should show a success checkmark when the variable value is copied', async () => {\n      const { copyButton } = setupRender({ apiKey: 'test-value' });\n\n      expect(copyButton.classList.contains('copy-success')).toBe(false);\n\n      await copyButton.click();\n\n      expect(copyButton.classList.contains('copy-success')).toBe(true);\n\n      jest.advanceTimersByTime(COPY_SUCCESS_TIMEOUT);\n\n      expect(copyButton.classList.contains('copy-success')).toBe(false);\n    });\n\n    it('should log to the console when the variable value is not copied', async () => {\n      const { copyButton } = setupRender({ apiKey: 'cause-clipboard-error' });\n\n      copyButton.click();\n\n      // wait for .catch() microtask to run\n      await jest.runAllTimersAsync();\n\n      expect(clipboardText).toBe('');\n      expect(console.error).toHaveBeenCalledWith('Failed to copy to clipboard:', 'Clipboard error');\n    });\n  });\n\n  describe('dynamic/faker variable rendering', () => {\n    function setupDynamicRender(variableName, variables = {}) {\n      const result = renderVarInfo({ string: `{{${variableName}}}` }, { variables, collection: null, item: null });\n      if (!result) return { result: null, containerDiv: null };\n\n      const containerDiv = result;\n      const header = containerDiv.querySelector('.var-info-header');\n      const scopeBadge = containerDiv.querySelector('.var-scope-badge');\n      const readOnlyNote = containerDiv.querySelector('.var-readonly-note');\n      const warningNote = containerDiv.querySelector('.var-warning-note');\n      const valueContainer = containerDiv.querySelector('.var-value-container');\n\n      return { result, containerDiv, header, scopeBadge, readOnlyNote, warningNote, valueContainer };\n    }\n\n    it('should show read-only note for dynamic variables', () => {\n      const { readOnlyNote } = setupDynamicRender('$randomFirstName');\n\n      expect(readOnlyNote).not.toBeNull();\n      expect(readOnlyNote.textContent).toBe('Generates random value on each request');\n    });\n\n    it('should not show value container for valid dynamic variables', () => {\n      const { valueContainer } = setupDynamicRender('$randomFirstName');\n\n      // Value is generated at runtime, so no value display\n      expect(valueContainer).toBeNull();\n    });\n\n    it('should show warning for unknown dynamic variable', () => {\n      const { warningNote, scopeBadge } = setupDynamicRender('$unknownFaker');\n\n      expect(scopeBadge.textContent).toBe('Dynamic');\n      expect(warningNote).not.toBeNull();\n      expect(warningNote.textContent).toContain('Unknown dynamic variable');\n    });\n\n    it('should show time-based note for $timestamp variable', () => {\n      const { readOnlyNote, scopeBadge } = setupDynamicRender('$timestamp');\n\n      expect(scopeBadge.textContent).toBe('Dynamic');\n      expect(readOnlyNote).not.toBeNull();\n      expect(readOnlyNote.textContent).toBe('Generates current timestamp on each request');\n    });\n\n    it('should show time-based note for $isoTimestamp variable', () => {\n      const { readOnlyNote, scopeBadge } = setupDynamicRender('$isoTimestamp');\n\n      expect(scopeBadge.textContent).toBe('Dynamic');\n      expect(readOnlyNote).not.toBeNull();\n      expect(readOnlyNote.textContent).toBe('Generates current timestamp on each request');\n    });\n\n    it('should show random note for non-time-based dynamic variables', () => {\n      const { readOnlyNote } = setupDynamicRender('$randomEmail');\n\n      expect(readOnlyNote).not.toBeNull();\n      expect(readOnlyNote.textContent).toBe('Generates random value on each request');\n    });\n  });\n\n  describe('OAuth2 variable rendering', () => {\n    function setupOAuth2Render(variableName, variables = {}) {\n      const result = renderVarInfo({ string: `{{${variableName}}}` }, { variables, collection: null, item: null });\n      if (!result) return { result: null, containerDiv: null };\n\n      const containerDiv = result;\n      const header = containerDiv.querySelector('.var-info-header');\n      const scopeBadge = containerDiv.querySelector('.var-scope-badge');\n      const readOnlyNote = containerDiv.querySelector('.var-readonly-note');\n      const warningNote = containerDiv.querySelector('.var-warning-note');\n      const valueContainer = containerDiv.querySelector('.var-value-container');\n      const valueDisplay = containerDiv.querySelector('.var-value-display');\n\n      return { result, containerDiv, header, scopeBadge, readOnlyNote, warningNote, valueContainer, valueDisplay };\n    }\n\n    it('should show OAuth2 scope badge for $oauth2 variables', () => {\n      const { scopeBadge } = setupOAuth2Render('$oauth2.credentials.access_token', {\n        '$oauth2.credentials.access_token': 'test-token-123'\n      });\n\n      expect(scopeBadge.textContent).toBe('OAuth2');\n    });\n\n    it('should show read-only note for valid OAuth2 variables', () => {\n      const { readOnlyNote } = setupOAuth2Render('$oauth2.credentials.access_token', {\n        '$oauth2.credentials.access_token': 'test-token-123'\n      });\n\n      expect(readOnlyNote).not.toBeNull();\n      expect(readOnlyNote.textContent).toBe('read-only');\n    });\n\n    it('should display the token value for valid OAuth2 variables', () => {\n      const { valueDisplay } = setupOAuth2Render('$oauth2.credentials.access_token', {\n        '$oauth2.credentials.access_token': 'test-token-123'\n      });\n\n      expect(valueDisplay).not.toBeNull();\n      expect(valueDisplay.textContent).toBe('test-token-123');\n    });\n\n    it('should show warning for OAuth2 variable when token is not found', () => {\n      const { warningNote, scopeBadge } = setupOAuth2Render('$oauth2.credentials.access_token', {});\n\n      expect(scopeBadge.textContent).toBe('OAuth2');\n      expect(warningNote).not.toBeNull();\n      expect(warningNote.textContent).toContain('OAuth2 token not found');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/utils/codemirror/javascript-lint.js",
    "content": "/**\n * MIT License\n * https://github.com/codemirror/codemirror5/blob/master/LICENSE\n *\n * Copyright (C) 2017 by Marijn Haverbeke <marijnh@gmail.com> and others\n */\n\nimport { JSHINT } from 'jshint';\n\nlet CodeMirror;\nconst SERVER_RENDERED = typeof window === 'undefined' || global['PREVENT_CODEMIRROR_RENDER'] === true;\n\nif (!SERVER_RENDERED) {\n  CodeMirror = require('codemirror');\n  const { filter } = require('lodash');\n\n  function validator(text, options) {\n    if (!window.JSHINT) {\n      if (window.console) {\n        window.console.error('Error: window.JSHINT not defined, CodeMirror JavaScript linting cannot run.');\n      }\n      return [];\n    }\n\n    // Set default options for Bruno\n    const defaultOptions = {\n      esversion: 11,\n      expr: true,\n      asi: true,\n      undef: true,\n      browser: true,\n      devel: true,\n      module: true,\n      node: true,\n      predef: {\n        bru: false,\n        req: false,\n        res: false,\n        test: false,\n        expect: false,\n        require: false,\n        module: false\n      }\n    };\n\n    // Merge provided options with defaults\n    options = Object.assign({}, defaultOptions, options);\n\n    if (!options.indent)\n      // JSHint error.character actually is a column index, this fixes underlining on lines using tabs for indentation\n      options.indent = 1; // JSHint default value is 4\n    JSHINT(text, options, options.globals);\n    var errors = JSHINT.data().errors,\n      result = [];\n\n    /*\n     * Filter out errors due to top level awaits\n     * See https://github.com/usebruno/bruno/issues/1214\n     *\n     * - E058: Missing semicolon at top level await\n     *  codemirror error: \"Missing semicolon.\"\n     * - W024: 'await' used as identifier (JSHint doesn't recognize top-level await syntax)\n     *  codemirror error: \"Expected an identifier and instead saw 'await' (a reserved word).\"\n     *\n     * Once JSHINT top level await support is added, this file can be removed\n     * and we can use the default javascript-lint addon from codemirror\n     */\n    errors = filter(errors, (error) => {\n      if (error.code === 'E058' || error.code === 'W024') {\n        if (\n          error.evidence\n          && error.evidence.includes('await')\n          && error.scope === '(main)'\n        ) {\n          return false;\n        }\n\n        return true;\n      }\n\n      /*\n       * Filter out errors due to atob/btoa redefinition\n       *\n       * - W079: Redefinition of '{a}'\n       *   This JSHint warning triggers when a variable name conflicts with a built-in global.\n       *   We filter this for atob/btoa to allow explicit requires in Node.js environments\n       *   where these browser functions might not be available.\n       */\n      if (error.code === 'W079' && (error.a === 'atob' || error.a === 'btoa')) {\n        return false;\n      }\n\n      return true;\n    });\n\n    if (errors) parseErrors(errors, result);\n\n    return result;\n  }\n\n  CodeMirror.registerHelper('lint', 'javascript', validator);\n\n  function parseErrors(errors, output) {\n    for (var i = 0; i < errors.length; i++) {\n      var error = errors[i];\n      if (error) {\n        if (error.line <= 0) {\n          if (window.console) {\n            window.console.warn('Cannot display JSHint error (invalid line ' + error.line + ')', error);\n          }\n          continue;\n        }\n\n        var start = error.character - 1,\n          end = start + 1;\n        if (error.evidence) {\n          var index = error.evidence.substring(start).search(/.\\b/);\n          if (index > -1) {\n            end += index;\n          }\n        }\n\n        // Convert to format expected by validation service\n        var hint = {\n          message: error.reason,\n          severity: error.code ? (error.code.startsWith('W') ? 'warning' : 'error') : 'error',\n          from: CodeMirror.Pos(error.line - 1, start),\n          to: CodeMirror.Pos(error.line - 1, end)\n        };\n\n        output.push(hint);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bruno-app/src/utils/codemirror/lang-detect.js",
    "content": "/**\n * @param {string} snippet\n * @returns {boolean}\n */\nexport function isXML(snippet) {\n  return /<\\/?[a-z][\\s\\S]*>/i.test(snippet);\n}\n\n/**\n * @param {string} snippet\n * @returns {boolean}\n */\nexport function isJSON(snippet) {\n  try {\n    JSON.parse(snippet);\n    return true;\n  } catch (err) {\n    return false;\n  }\n}\n\n/**\n * @param {string} snippet\n * @returns {string}\n */\nexport function autoDetectLang(snippet) {\n  if (isJSON(snippet)) {\n    return 'json';\n  }\n  if (isXML(snippet)) {\n    return 'xml';\n  }\n  return 'text';\n}\n"
  },
  {
    "path": "packages/bruno-app/src/utils/codemirror/linkAware.js",
    "content": "import LinkifyIt from 'linkify-it';\nimport { isMacOS } from 'utils/common/platform';\nimport { debounce } from 'lodash';\n/**\n * Gets the visible line range using scroll info and lineAtHeight\n * @param {Object} editor - The CodeMirror editor instance\n * @param {number} padding - Number of lines to add above and below viewport\n * @returns {Object} Object with from and to line numbers\n */\nfunction getVisibleLineRange(editor, padding = 3) {\n  const doc = editor.getDoc();\n  const scroll = editor.getScrollInfo();\n  const topLine = editor.lineAtHeight(scroll.top, 'local');\n  const bottomLine = editor.lineAtHeight(scroll.top + scroll.clientHeight, 'local');\n\n  return {\n    from: Math.max(0, topLine - padding),\n    to: Math.min(doc.lineCount(), bottomLine + padding + 1) // +1 because to is exclusive\n  };\n}\n\n/**\n * Marks URLs in the CodeMirror editor with clickable link styling\n * Only processes links in the visible viewport for performance\n * @param {Object} editor - The CodeMirror editor instance\n * @param {Object} linkify - The LinkifyIt instance for URL detection\n * @param {string} linkClass - CSS class name for links\n * @param {string} linkHint - Tooltip text for links\n */\nfunction markUrls(editor, linkify, linkClass, linkHint) {\n  const doc = editor.getDoc();\n  const { from: fromLine, to: toLine } = getVisibleLineRange(editor, 3);\n\n  // Use editor.operation() to batch all mark operations for better performance\n  editor.operation(() => {\n    // Clear only link marks that overlap the visible range\n    editor.getAllMarks().forEach((mark) => {\n      if (mark.className !== linkClass) return;\n\n      // Check if mark overlaps visible range\n      const pos = mark.find?.();\n      if (!pos) {\n        // If we can't find position, clear it to be safe\n        mark.clear();\n        return;\n      }\n\n      // Clear marks that overlap the visible range\n      if (pos.to.line >= fromLine && pos.from.line < toLine) {\n        mark.clear();\n      }\n    });\n\n    // Find and mark URLs in visible lines only\n    for (let lineNum = fromLine; lineNum < toLine; lineNum++) {\n      const lineContent = doc.getLine(lineNum);\n      if (!lineContent) continue;\n\n      const matches = linkify.match(lineContent);\n      if (!matches) continue;\n\n      const variablePatterns = [];\n      const variablePattern = /\\{\\{[^}]*\\}\\}/g;\n      let varMatch;\n      while ((varMatch = variablePattern.exec(lineContent)) !== null) {\n        variablePatterns.push({ start: varMatch.index, end: varMatch.index + varMatch[0].length });\n      }\n      matches.forEach(({ index, lastIndex, url }) => {\n        const isInVariable = variablePatterns.some(\n          ({ start, end }) => index < end && lastIndex > start\n        );\n        if (isInVariable) return;\n\n        try {\n          editor.markText(\n            { line: lineNum, ch: index },\n            { line: lineNum, ch: lastIndex },\n            {\n              className: linkClass,\n              attributes: {\n                'data-url': url,\n                'title': linkHint\n              }\n            }\n          );\n        } catch (e) {\n          // Silently ignore marking errors (e.g., if positions are invalid)\n          // This can happen if the line content changed between getting it and marking\n        }\n      });\n    }\n  });\n}\n\n/**\n * Handles mouse enter events on links to show hover effects\n * @param {Event} event - The mouse enter event\n * @param {string} linkClass - CSS class name for links\n * @param {string} linkHoverClass - CSS class name for hovered links\n * @param {Function} updateCmdCtrlClass - Function to update Cmd/Ctrl state\n */\nfunction handleMouseEnter(event, linkClass, linkHoverClass, updateCmdCtrlClass) {\n  const el = event.target;\n  if (!el.classList.contains(linkClass)) return;\n\n  updateCmdCtrlClass(event);\n\n  el.classList.add(linkHoverClass);\n\n  // Add hover effect to previous siblings that are also links\n  let sibling = el.previousElementSibling;\n  while (sibling && sibling.classList.contains(linkClass)) {\n    sibling.classList.add(linkHoverClass);\n    sibling = sibling.previousElementSibling;\n  }\n\n  // Add hover effect to next siblings that are also links\n  sibling = el.nextElementSibling;\n  while (sibling && sibling.classList.contains(linkClass)) {\n    sibling.classList.add(linkHoverClass);\n    sibling = sibling.nextElementSibling;\n  }\n}\n\n/**\n * Handles mouse leave events on links to remove hover effects\n * @param {Event} event - The mouse leave event\n * @param {string} linkClass - CSS class name for links\n * @param {string} linkHoverClass - CSS class name for hovered links\n */\nfunction handleMouseLeave(event, linkClass, linkHoverClass) {\n  const el = event.target;\n  el.classList.remove(linkHoverClass);\n\n  // Remove hover effect from previous siblings that are also links\n  let sibling = el.previousElementSibling;\n  while (sibling && sibling.classList.contains(linkClass)) {\n    sibling.classList.remove(linkHoverClass);\n    sibling = sibling.previousElementSibling;\n  }\n\n  // Remove hover effect from next siblings that are also links\n  sibling = el.nextElementSibling;\n  while (sibling && sibling.classList.contains(linkClass)) {\n    sibling.classList.remove(linkHoverClass);\n    sibling = sibling.nextElementSibling;\n  }\n}\n\n/**\n * Updates the CSS class on the editor wrapper based on Cmd/Ctrl key state\n * @param {Event} event - The keyboard event\n * @param {HTMLElement} editorWrapper - The editor wrapper element\n * @param {string} cmdCtrlClass - CSS class name for Cmd/Ctrl pressed state\n * @param {Function} isCmdOrCtrlPressed - Function to check if Cmd/Ctrl is pressed\n */\nfunction updateCmdCtrlClass(event, editorWrapper, cmdCtrlClass, isCmdOrCtrlPressed) {\n  if (isCmdOrCtrlPressed(event)) {\n    editorWrapper.classList.add(cmdCtrlClass);\n  } else {\n    editorWrapper.classList.remove(cmdCtrlClass);\n  }\n}\n\n/**\n * Handles click events on links to open them externally\n * @param {Event} event - The click event\n * @param {string} linkClass - CSS class name for links\n * @param {Function} isCmdOrCtrlPressed - Function to check if Cmd/Ctrl is pressed\n */\nfunction handleClick(event, linkClass, isCmdOrCtrlPressed) {\n  if (!isCmdOrCtrlPressed(event)) return;\n\n  if (event.target.classList.contains(linkClass)) {\n    event.preventDefault();\n    event.stopPropagation();\n    const url = event.target.getAttribute('data-url');\n    if (url) {\n      window?.ipcRenderer?.openExternal(url);\n    }\n  }\n}\n\n/**\n * Sets up link awareness for a CodeMirror editor instance.\n * This enables automatic URL detection, styling, and click-to-open functionality.\n * @param {Object} editor - The CodeMirror editor instance\n * @param {Object} options - Configuration options (currently unused but reserved for future use)\n * @returns {void}\n */\nfunction setupLinkAware(editor, options = {}) {\n  if (!editor) {\n    return;\n  }\n\n  // CSS class names and configuration\n  const cmdCtrlClass = 'cmd-ctrl-pressed';\n  const linkClass = 'CodeMirror-link';\n  const linkHoverClass = 'hovered-link';\n  const linkHint = isMacOS() ? 'Hold Cmd and click to open link' : 'Hold Ctrl and click to open link';\n\n  // Helper function to check if Cmd/Ctrl is pressed\n  const isCmdOrCtrlPressed = (event) => (isMacOS() ? event.metaKey : event.ctrlKey);\n\n  // Initialize LinkifyIt for URL detection\n  const linkify = new LinkifyIt();\n  const editorWrapper = editor.getWrapperElement();\n\n  // Create bound versions of event handlers with proper parameters\n  const boundMarkUrls = () => markUrls(editor, linkify, linkClass, linkHint);\n  const boundUpdateCmdCtrlClass = (event) => updateCmdCtrlClass(event, editorWrapper, cmdCtrlClass, isCmdOrCtrlPressed);\n  const boundHandleClick = (event) => handleClick(event, linkClass, isCmdOrCtrlPressed);\n  const boundHandleMouseEnter = (event) => handleMouseEnter(event, linkClass, linkHoverClass, boundUpdateCmdCtrlClass);\n  const boundHandleMouseLeave = (event) => handleMouseLeave(event, linkClass, linkHoverClass);\n\n  // Create debounced version of markUrls that runs after rendering\n  const debouncedMarkUrls = debounce(() => {\n    requestAnimationFrame(() => {\n      // Skip if the editor is hidden (e.g., tab not visible)\n      if (!editorWrapper.offsetParent) return;\n      boundMarkUrls();\n    });\n  }, 150);\n\n  // Run after the first render/refresh\n  editor.on('refresh', debouncedMarkUrls);\n\n  // Set up event listeners\n  editor.on('changes', debouncedMarkUrls);\n\n  // Listen for scroll events to update marks when viewport changes\n  editor.on('scroll', debouncedMarkUrls);\n\n  window.addEventListener('keydown', boundUpdateCmdCtrlClass);\n  window.addEventListener('keyup', boundUpdateCmdCtrlClass);\n  editorWrapper.addEventListener('click', boundHandleClick);\n  editorWrapper.addEventListener('mouseover', boundHandleMouseEnter);\n  editorWrapper.addEventListener('mouseout', boundHandleMouseLeave);\n\n  // Cleanup function to remove all event listeners\n  editor._destroyLinkAware = () => {\n    editor.off('refresh', debouncedMarkUrls);\n    editor.off('changes', debouncedMarkUrls);\n    editor.off('scroll', debouncedMarkUrls);\n    window.removeEventListener('keydown', boundUpdateCmdCtrlClass);\n    window.removeEventListener('keyup', boundUpdateCmdCtrlClass);\n    editorWrapper.removeEventListener('click', boundHandleClick);\n    editorWrapper.removeEventListener('mouseover', boundHandleMouseEnter);\n    editorWrapper.removeEventListener('mouseout', boundHandleMouseLeave);\n  };\n}\n\nexport { setupLinkAware };\n"
  },
  {
    "path": "packages/bruno-app/src/utils/codemirror/linkAware.spec.js",
    "content": "import { setupLinkAware } from './linkAware';\nimport LinkifyIt from 'linkify-it';\nimport { isMacOS } from 'utils/common/platform';\n\n// No need to mock CodeMirror since setupLinkAware works with an existing editor\n\n// Mock linkify-it\njest.mock('linkify-it', () => {\n  return jest.fn().mockImplementation(() => ({\n    match: jest.fn()\n  }));\n});\n\njest.mock('utils/common/platform', () => ({\n  isMacOS: jest.fn()\n}));\n// Mock requestAnimationFrame\nglobal.requestAnimationFrame = jest.fn((cb) => cb());\n\n// Mock window.ipcRenderer\nglobal.window = {\n  ...global.window,\n  ipcRenderer: {\n    openExternal: jest.fn()\n  },\n  addEventListener: jest.fn(),\n  removeEventListener: jest.fn()\n};\n\ndescribe('setupLinkAware', () => {\n  let mockEditor;\n  let mockDoc;\n  let mockWrapperElement;\n  let mockLinkify;\n  let mockMark;\n  let originalTimeout;\n  let mockSetTimeout;\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n    jest.useFakeTimers();\n\n    // Create a Jest mock for setTimeout\n    mockSetTimeout = jest.spyOn(global, 'setTimeout');\n\n    // Store original timeout and mock requestAnimationFrame\n    originalTimeout = global.setTimeout;\n    global.requestAnimationFrame = jest.fn((cb) => cb());\n\n    // Setup DOM mocks\n    mockWrapperElement = {\n      classList: {\n        add: jest.fn(),\n        remove: jest.fn()\n      },\n      addEventListener: jest.fn(),\n      removeEventListener: jest.fn(),\n      // Link marking is skipped if editor is hidden; offsetParent is null when hidden\n      offsetParent: {}\n    };\n\n    mockMark = {\n      clear: jest.fn(),\n      className: 'CodeMirror-link'\n    };\n\n    mockDoc = {\n      getValue: jest.fn().mockReturnValue('Check out https://example.com and http://test.org'),\n      getLine: jest.fn().mockImplementation((lineNum) =>\n        lineNum === 0 ? 'Check out https://example.com and http://test.org' : ''\n      ),\n      lineCount: jest.fn().mockReturnValue(1)\n    };\n\n    mockEditor = {\n      getDoc: jest.fn().mockReturnValue(mockDoc),\n      getAllMarks: jest.fn().mockReturnValue([mockMark]),\n      markText: jest.fn(),\n      posFromIndex: jest.fn().mockImplementation((index) => ({ line: 0, ch: index })),\n      getWrapperElement: jest.fn().mockReturnValue(mockWrapperElement),\n      operation: jest.fn((fn) => fn()),\n      getScrollInfo: jest.fn().mockReturnValue({ top: 0, clientHeight: 100 }),\n      lineAtHeight: jest.fn().mockReturnValue(0),\n      on: jest.fn(),\n      off: jest.fn(),\n      _destroyLinkAware: undefined\n    };\n\n    mockLinkify = {\n      match: jest.fn().mockReturnValue([\n        { index: 10, lastIndex: 28, url: 'https://example.com' },\n        { index: 33, lastIndex: 48, url: 'http://test.org' }\n      ])\n    };\n\n    LinkifyIt.mockImplementation(() => mockLinkify);\n\n    // Mock window and ipcRenderer\n    global.window = {\n      addEventListener: jest.fn(),\n      removeEventListener: jest.fn(),\n      ipcRenderer: {\n        openExternal: jest.fn()\n      }\n    };\n  });\n\n  afterEach(() => {\n    delete global.window;\n    delete global.requestAnimationFrame;\n    global.setTimeout = originalTimeout;\n    mockSetTimeout.mockRestore();\n    jest.useRealTimers();\n  });\n\n  describe('editor setup and configuration', () => {\n    it('should set up link awareness on an existing editor', () => {\n      setupLinkAware(mockEditor);\n\n      expect(mockEditor.getWrapperElement).toHaveBeenCalled();\n      expect(mockEditor.on).toHaveBeenCalledWith('changes', expect.any(Function));\n      expect(mockWrapperElement.addEventListener).toHaveBeenCalledWith('click', expect.any(Function));\n      expect(mockWrapperElement.addEventListener).toHaveBeenCalledWith('mouseover', expect.any(Function));\n      expect(mockWrapperElement.addEventListener).toHaveBeenCalledWith('mouseout', expect.any(Function));\n    });\n\n    it('should accept options parameter', () => {\n      const options = { someOption: true };\n\n      setupLinkAware(mockEditor, options);\n\n      expect(mockEditor.getWrapperElement).toHaveBeenCalled();\n    });\n\n    it('should return early if editor is null', () => {\n      const result = setupLinkAware(null);\n\n      expect(result).toBeUndefined();\n      expect(mockEditor.getWrapperElement).not.toHaveBeenCalled();\n    });\n\n    it('should add _destroyLinkAware method to editor', () => {\n      setupLinkAware(mockEditor);\n\n      expect(mockEditor._destroyLinkAware).toBeInstanceOf(Function);\n    });\n  });\n\n  describe('platform-specific behavior', () => {\n    it('should use Cmd key hint on macOS', () => {\n      isMacOS.mockReturnValue(true);\n      setupLinkAware(mockEditor);\n\n      const changeHandler = mockEditor.on.mock.calls.find((call) => call[0] === 'changes')[1];\n      changeHandler();\n      jest.runAllTimers();\n\n      // Verify that markUrls was called which sets the hint\n      expect(mockEditor.markText).toHaveBeenCalledWith(expect.anything(),\n        expect.anything(),\n        expect.objectContaining({\n          attributes: expect.objectContaining({\n            title: 'Hold Cmd and click to open link'\n          })\n        }));\n    });\n\n    it('should use Ctrl key hint on non-macOS', () => {\n      isMacOS.mockReturnValue(false);\n      setupLinkAware(mockEditor);\n\n      const changeHandler = mockEditor.on.mock.calls.find((call) => call[0] === 'changes')[1];\n      changeHandler();\n      jest.runAllTimers();\n\n      // Verify that markUrls was called which sets the hint\n      expect(mockEditor.markText).toHaveBeenCalledWith(expect.anything(),\n        expect.anything(),\n        expect.objectContaining({\n          attributes: expect.objectContaining({\n            title: 'Hold Ctrl and click to open link'\n          })\n        }));\n    });\n  });\n\n  describe('CSS class management', () => {\n    it('should add cmd-ctrl-pressed class when modifier key is pressed', () => {\n      isMacOS.mockReturnValue(true);\n      setupLinkAware(mockEditor);\n\n      const keydownHandler = global.window.addEventListener.mock.calls.find((call) => call[0] === 'keydown')[1];\n      const mockEvent = { metaKey: true };\n\n      keydownHandler(mockEvent);\n\n      expect(mockWrapperElement.classList.add).toHaveBeenCalledWith('cmd-ctrl-pressed');\n    });\n\n    it('should remove cmd-ctrl-pressed class when modifier key is released', () => {\n      isMacOS.mockReturnValue(false);\n      setupLinkAware(mockEditor);\n\n      const keyupHandler = global.window.addEventListener.mock.calls.find((call) => call[0] === 'keyup')[1];\n      const mockEvent = { ctrlKey: false };\n\n      keyupHandler(mockEvent);\n\n      expect(mockWrapperElement.classList.remove).toHaveBeenCalledWith('cmd-ctrl-pressed');\n    });\n  });\n\n  describe('click handling', () => {\n    it('should open external URL when Cmd+clicking on a link', () => {\n      isMacOS.mockReturnValue(true);\n      setupLinkAware(mockEditor);\n\n      const clickHandler = mockWrapperElement.addEventListener.mock.calls.find((call) => call[0] === 'click')[1];\n      const mockEvent = {\n        metaKey: true,\n        target: {\n          classList: { contains: (className) => className === 'CodeMirror-link' },\n          getAttribute: () => 'https://example.com'\n        },\n        preventDefault: jest.fn(),\n        stopPropagation: jest.fn()\n      };\n\n      clickHandler(mockEvent);\n\n      expect(mockEvent.preventDefault).toHaveBeenCalled();\n      expect(mockEvent.stopPropagation).toHaveBeenCalled();\n      expect(global.window.ipcRenderer.openExternal).toHaveBeenCalledWith('https://example.com');\n    });\n\n    it('should not open URL when clicking without modifier key', () => {\n      setupLinkAware(mockEditor);\n\n      const clickHandler = mockWrapperElement.addEventListener.mock.calls.find((call) => call[0] === 'click')[1];\n      const mockEvent = {\n        metaKey: false,\n        ctrlKey: false,\n        target: {\n          classList: { contains: (className) => className === 'CodeMirror-link' },\n          getAttribute: () => 'https://example.com'\n        }\n      };\n\n      clickHandler(mockEvent);\n\n      expect(global.window.ipcRenderer.openExternal).not.toHaveBeenCalled();\n    });\n\n    it('should not open URL when clicking on non-link element', () => {\n      isMacOS.mockReturnValue(true);\n      setupLinkAware(mockEditor);\n\n      const clickHandler = mockWrapperElement.addEventListener.mock.calls.find((call) => call[0] === 'click')[1];\n      const mockEvent = {\n        metaKey: true,\n        target: {\n          classList: { contains: () => false }\n        }\n      };\n\n      clickHandler(mockEvent);\n\n      expect(global.window.ipcRenderer.openExternal).not.toHaveBeenCalled();\n    });\n\n    it('should not open URL when data-url attribute is missing', () => {\n      isMacOS.mockReturnValue(true);\n      setupLinkAware(mockEditor);\n\n      const clickHandler = mockWrapperElement.addEventListener.mock.calls.find((call) => call[0] === 'click')[1];\n      const mockEvent = {\n        metaKey: true,\n        target: {\n          classList: { contains: (className) => className === 'CodeMirror-link' },\n          getAttribute: () => null\n        },\n        preventDefault: jest.fn(),\n        stopPropagation: jest.fn()\n      };\n\n      clickHandler(mockEvent);\n\n      expect(mockEvent.preventDefault).toHaveBeenCalled();\n      expect(mockEvent.stopPropagation).toHaveBeenCalled();\n      expect(global.window.ipcRenderer.openExternal).not.toHaveBeenCalled();\n    });\n  });\n\n  // Test debouncing behavior\n  describe('debouncing', () => {\n    it('should debounce URL marking on content changes', () => {\n      setupLinkAware(mockEditor);\n\n      // Clear the calls from initial setup\n      mockEditor.getAllMarks.mockClear();\n      requestAnimationFrame.mockClear();\n\n      // Simulate multiple rapid content changes\n      const changeHandler = mockEditor.on.mock.calls.find((call) => call[0] === 'changes')[1];\n      changeHandler();\n      changeHandler();\n      changeHandler();\n\n      // With debouncing, setTimeout should be called (lodash debounce uses it internally)\n      // The exact number may vary, but we should see at least one call\n      expect(setTimeout).toHaveBeenCalled();\n      expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 150);\n\n      // Fast-forward timers\n      jest.runAllTimers();\n\n      // Should only mark URLs once despite multiple rapid changes\n      expect(requestAnimationFrame).toHaveBeenCalledTimes(1);\n      expect(mockEditor.getAllMarks).toHaveBeenCalledTimes(1);\n    });\n\n    it('should apply link tooltips when marking URLs', () => {\n      isMacOS.mockReturnValue(true);\n      setupLinkAware(mockEditor);\n\n      const changeHandler = mockEditor.on.mock.calls.find((call) => call[0] === 'changes')[1];\n      changeHandler();\n      jest.runAllTimers();\n\n      expect(mockEditor.markText).toHaveBeenCalledWith({ line: 0, ch: 10 },\n        { line: 0, ch: 28 },\n        {\n          className: 'CodeMirror-link',\n          attributes: {\n            'data-url': 'https://example.com',\n            'title': 'Hold Cmd and click to open link'\n          }\n        });\n    });\n  });\n\n  // Test animation frame handling\n  describe('animation frame handling', () => {\n    it('should use requestAnimationFrame for URL marking', () => {\n      setupLinkAware(mockEditor);\n\n      const changeHandler = mockEditor.on.mock.calls.find((call) => call[0] === 'changes')[1];\n      changeHandler();\n\n      jest.runAllTimers();\n\n      expect(requestAnimationFrame).toHaveBeenCalled();\n    });\n  });\n\n  describe('hover behavior', () => {\n    it('should add hover class on mouseover for link elements', () => {\n      setupLinkAware(mockEditor);\n\n      const mouseoverHandler = mockWrapperElement.addEventListener.mock.calls.find((call) => call[0] === 'mouseover')[1];\n\n      const mockTarget = {\n        classList: {\n          contains: jest.fn().mockReturnValue(true),\n          add: jest.fn(),\n          remove: jest.fn()\n        },\n        previousElementSibling: {\n          classList: {\n            contains: jest.fn().mockReturnValue(true),\n            add: jest.fn(),\n            remove: jest.fn()\n          },\n          previousElementSibling: null\n        },\n        nextElementSibling: {\n          classList: {\n            contains: jest.fn().mockReturnValue(true),\n            add: jest.fn(),\n            remove: jest.fn()\n          },\n          nextElementSibling: null\n        }\n      };\n\n      const mockEvent = { target: mockTarget };\n      mouseoverHandler(mockEvent);\n\n      expect(mockTarget.classList.add).toHaveBeenCalledWith('hovered-link');\n      expect(mockTarget.previousElementSibling.classList.add).toHaveBeenCalledWith('hovered-link');\n      expect(mockTarget.nextElementSibling.classList.add).toHaveBeenCalledWith('hovered-link');\n    });\n\n    it('should not add hover class for non-link elements', () => {\n      setupLinkAware(mockEditor);\n\n      const mouseoverHandler = mockWrapperElement.addEventListener.mock.calls.find((call) => call[0] === 'mouseover')[1];\n\n      const mockTarget = {\n        classList: {\n          contains: jest.fn().mockReturnValue(false),\n          add: jest.fn()\n        }\n      };\n\n      const mockEvent = { target: mockTarget };\n      mouseoverHandler(mockEvent);\n\n      expect(mockTarget.classList.add).not.toHaveBeenCalled();\n    });\n\n    it('should remove hover class on mouseout', () => {\n      setupLinkAware(mockEditor);\n\n      const mouseoutHandler = mockWrapperElement.addEventListener.mock.calls.find((call) => call[0] === 'mouseout')[1];\n\n      const mockTarget = {\n        classList: {\n          contains: jest.fn().mockReturnValue(true),\n          remove: jest.fn()\n        },\n        previousElementSibling: {\n          classList: {\n            contains: jest.fn().mockReturnValue(true),\n            remove: jest.fn()\n          },\n          previousElementSibling: null\n        },\n        nextElementSibling: {\n          classList: {\n            contains: jest.fn().mockReturnValue(true),\n            remove: jest.fn()\n          },\n          nextElementSibling: null\n        }\n      };\n\n      const mockEvent = { target: mockTarget };\n      mouseoutHandler(mockEvent);\n\n      expect(mockTarget.classList.remove).toHaveBeenCalledWith('hovered-link');\n      expect(mockTarget.previousElementSibling.classList.remove).toHaveBeenCalledWith('hovered-link');\n      expect(mockTarget.nextElementSibling.classList.remove).toHaveBeenCalledWith('hovered-link');\n    });\n\n    it('should handle multi-span links correctly on hover', () => {\n      setupLinkAware(mockEditor);\n\n      const mouseoverHandler = mockWrapperElement.addEventListener.mock.calls.find((call) => call[0] === 'mouseover')[1];\n\n      // Create a mock with a chain of link spans\n      const mockNestedPrev = {\n        classList: {\n          contains: jest.fn().mockReturnValue(true),\n          add: jest.fn()\n        },\n        previousElementSibling: null\n      };\n\n      const mockPrev = {\n        classList: {\n          contains: jest.fn().mockReturnValue(true),\n          add: jest.fn()\n        },\n        previousElementSibling: mockNestedPrev\n      };\n\n      const mockNestedNext = {\n        classList: {\n          contains: jest.fn().mockReturnValue(true),\n          add: jest.fn()\n        },\n        nextElementSibling: null\n      };\n\n      const mockNext = {\n        classList: {\n          contains: jest.fn().mockReturnValue(true),\n          add: jest.fn()\n        },\n        nextElementSibling: mockNestedNext\n      };\n\n      const mockTarget = {\n        classList: {\n          contains: jest.fn().mockReturnValue(true),\n          add: jest.fn()\n        },\n        previousElementSibling: mockPrev,\n        nextElementSibling: mockNext\n      };\n\n      const mockEvent = { target: mockTarget };\n      mouseoverHandler(mockEvent);\n\n      expect(mockTarget.classList.add).toHaveBeenCalledWith('hovered-link');\n      expect(mockPrev.classList.add).toHaveBeenCalledWith('hovered-link');\n      expect(mockNestedPrev.classList.add).toHaveBeenCalledWith('hovered-link');\n      expect(mockNext.classList.add).toHaveBeenCalledWith('hovered-link');\n      expect(mockNestedNext.classList.add).toHaveBeenCalledWith('hovered-link');\n    });\n  });\n\n  // Test memory cleanup\n  describe('memory cleanup', () => {\n    it('should properly clean up all event listeners and marks', () => {\n      setupLinkAware(mockEditor);\n\n      mockEditor._destroyLinkAware();\n\n      expect(mockEditor.off).toHaveBeenCalled();\n      expect(global.window.removeEventListener).toHaveBeenCalledTimes(2);\n      expect(mockWrapperElement.removeEventListener).toHaveBeenCalledTimes(3); // click, mouseover, mouseout\n      expect(mockWrapperElement.removeEventListener).toHaveBeenCalledWith('click', expect.any(Function));\n      expect(mockWrapperElement.removeEventListener).toHaveBeenCalledWith('mouseover', expect.any(Function));\n      expect(mockWrapperElement.removeEventListener).toHaveBeenCalledWith('mouseout', expect.any(Function));\n    });\n  });\n\n  describe('edge cases', () => {\n    it('should handle missing target in mouse event', () => {\n      setupLinkAware(mockEditor);\n\n      const mouseoverHandler = mockWrapperElement.addEventListener.mock.calls.find((call) => call[0] === 'mouseover')[1];\n      const mockEvent = { target: null };\n\n      // Note: This will throw as the implementation accesses target.classList without null check\n      expect(() => mouseoverHandler(mockEvent)).toThrow();\n    });\n\n    it('should handle missing ipcRenderer', () => {\n      delete global.window.ipcRenderer;\n      isMacOS.mockReturnValue(true);\n      setupLinkAware(mockEditor);\n\n      const clickHandler = mockWrapperElement.addEventListener.mock.calls.find((call) => call[0] === 'click')[1];\n      const mockEvent = {\n        metaKey: true,\n        target: {\n          classList: { contains: (className) => className === 'CodeMirror-link' },\n          getAttribute: () => 'https://example.com'\n        },\n        preventDefault: jest.fn(),\n        stopPropagation: jest.fn()\n      };\n\n      expect(() => clickHandler(mockEvent)).not.toThrow();\n    });\n\n    it('should handle LinkifyIt returning null matches', () => {\n      mockLinkify.match.mockReturnValue(null);\n\n      expect(() => setupLinkAware(mockEditor)).not.toThrow();\n      // markText may still be called to clear existing marks\n    });\n\n    it('should handle null siblings in mouseover events', () => {\n      setupLinkAware(mockEditor);\n\n      const mouseoverHandler = mockWrapperElement.addEventListener.mock.calls.find((call) => call[0] === 'mouseover')[1];\n\n      const mockTarget = {\n        classList: {\n          contains: jest.fn().mockReturnValue(true),\n          add: jest.fn()\n        },\n        previousElementSibling: null,\n        nextElementSibling: null\n      };\n\n      const mockEvent = { target: mockTarget };\n\n      expect(() => mouseoverHandler(mockEvent)).not.toThrow();\n      expect(mockTarget.classList.add).toHaveBeenCalledWith('hovered-link');\n    });\n\n    it('should handle non-link siblings in mouseover events', () => {\n      setupLinkAware(mockEditor);\n\n      const mouseoverHandler = mockWrapperElement.addEventListener.mock.calls.find((call) => call[0] === 'mouseover')[1];\n\n      const mockPrev = {\n        classList: {\n          contains: jest.fn().mockReturnValue(false),\n          add: jest.fn()\n        }\n      };\n\n      const mockNext = {\n        classList: {\n          contains: jest.fn().mockReturnValue(false),\n          add: jest.fn()\n        }\n      };\n\n      const mockTarget = {\n        classList: {\n          contains: jest.fn().mockReturnValue(true),\n          add: jest.fn()\n        },\n        previousElementSibling: mockPrev,\n        nextElementSibling: mockNext\n      };\n\n      const mockEvent = { target: mockTarget };\n      mouseoverHandler(mockEvent);\n\n      expect(mockTarget.classList.add).toHaveBeenCalledWith('hovered-link');\n      expect(mockPrev.classList.add).not.toHaveBeenCalled();\n      expect(mockNext.classList.add).not.toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/utils/codemirror/lint-errors.js",
    "content": "/**\n * Lint Error Tooltip for CodeMirror\n * Shows lint errors in a popover when hovering over line numbers\n */\n\nlet activeTooltip = null;\n\n/**\n * Get lint errors for a specific line from the editor's lint state\n * @param {CodeMirror} editor - The CodeMirror editor instance\n * @param {number} lineNumber - The 0-indexed line number\n * @returns {Array} Array of lint error annotations\n */\nfunction getLintErrorsForLine(editor, lineNumber) {\n  if (!editor) return [];\n\n  const errors = [];\n  const lintState = editor.state.lint;\n\n  if (lintState && lintState.marked) {\n    lintState.marked.forEach((mark) => {\n      if (mark.__annotation) {\n        // Use annotation's from position directly (mark.find() can return null if lines array is empty)\n        const annotationLine = mark.__annotation.from?.line;\n\n        if (annotationLine === lineNumber) {\n          // Avoid duplicate messages\n          if (!errors.find((e) => e.message === mark.__annotation.message)) {\n            errors.push(mark.__annotation);\n          }\n        }\n      }\n    });\n  }\n\n  return errors;\n}\n\n/**\n * Show the lint error tooltip next to the target element\n * @param {Array} errors - Array of lint error annotations\n * @param {HTMLElement} targetElement - The element to position the tooltip near\n * @param {HTMLElement} container - The container to append the tooltip to\n */\nfunction showLintTooltip(errors, targetElement, container) {\n  hideLintTooltip();\n\n  const tooltip = document.createElement('div');\n  tooltip.className = 'lint-error-tooltip';\n\n  errors.forEach((error, index) => {\n    const errorDiv = document.createElement('div');\n    errorDiv.className = `lint-tooltip-message ${error.severity || 'error'}`;\n    errorDiv.textContent = error.message;\n    tooltip.appendChild(errorDiv);\n  });\n\n  container.appendChild(tooltip);\n  activeTooltip = tooltip;\n\n  // Position the tooltip\n  const rect = targetElement.getBoundingClientRect();\n  tooltip.style.left = `${rect.right + 8}px`;\n  tooltip.style.top = `${rect.top + (rect.height / 2)}px`;\n  tooltip.style.transform = 'translateY(-50%)';\n}\n\n/**\n * Hide and remove the active lint error tooltip\n */\nfunction hideLintTooltip() {\n  if (activeTooltip) {\n    activeTooltip.remove();\n    activeTooltip = null;\n  }\n}\n\n/**\n * Setup lint error tooltip functionality for a CodeMirror editor\n * Shows lint errors when hovering over line numbers\n *\n * @param {CodeMirror} editor - The CodeMirror editor instance\n * @returns {Function} Cleanup function to remove event listeners\n */\nexport function setupLintErrorTooltip(editor) {\n  const wrapper = editor.getWrapperElement();\n  // Get the StyledWrapper container (parent of CodeMirror wrapper)\n  const container = wrapper.closest('.graphiql-container') || wrapper.parentElement;\n\n  const handleMouseOver = (e) => {\n    const target = e.target;\n\n    // Check if hovering over a line number element\n    if (target.classList.contains('CodeMirror-linenumber')) {\n      const lineNumber = parseInt(target.textContent, 10) - 1; // 0-indexed\n\n      if (isNaN(lineNumber) || lineNumber < 0) {\n        hideLintTooltip();\n        return;\n      }\n\n      const lintErrors = getLintErrorsForLine(editor, lineNumber);\n\n      if (lintErrors.length > 0) {\n        showLintTooltip(lintErrors, target, container);\n      } else {\n        hideLintTooltip();\n      }\n    } else if (!target.closest('.lint-error-tooltip')) {\n      hideLintTooltip();\n    }\n  };\n\n  const handleMouseOut = (e) => {\n    const relatedTarget = e.relatedTarget;\n    // Don't hide if moving to another line number or the tooltip\n    if (relatedTarget\n      && (relatedTarget.classList?.contains('CodeMirror-linenumber')\n        || relatedTarget.closest?.('.lint-error-tooltip'))) {\n      return;\n    }\n    hideLintTooltip();\n  };\n\n  const handleScroll = () => {\n    hideLintTooltip();\n  };\n\n  // Add event listeners\n  wrapper.addEventListener('mouseover', handleMouseOver);\n  wrapper.addEventListener('mouseout', handleMouseOut);\n  editor.on('scroll', handleScroll);\n\n  // Return cleanup function\n  return () => {\n    wrapper.removeEventListener('mouseover', handleMouseOver);\n    wrapper.removeEventListener('mouseout', handleMouseOut);\n    editor.off('scroll', handleScroll);\n    hideLintTooltip();\n  };\n}\n"
  },
  {
    "path": "packages/bruno-app/src/utils/codemirror/mock-data-hints.js",
    "content": "import { mockDataFunctions } from '@usebruno/common';\n\nconst MOCK_FUNCTION_SUGGESTIONS = Object.keys(mockDataFunctions).map((key) => `$${key}`);\n\nexport const getMockDataHints = (cm) => {\n  const cursor = cm.getCursor();\n  const currentString = cm.getRange({ line: cursor.line, ch: 0 }, cursor);\n\n  const match = currentString.match(/\\{\\{\\$(\\w*)$/);\n  if (!match) return null;\n\n  const wordMatch = match[1];\n  if (!wordMatch) return null;\n\n  const suggestions = MOCK_FUNCTION_SUGGESTIONS.filter((name) => name.startsWith(`$${wordMatch}`));\n  if (!suggestions.length) return null;\n\n  const startPos = { line: cursor.line, ch: currentString.lastIndexOf('{{$') + 2 }; // +2 accounts for `{{`\n\n  return {\n    list: suggestions,\n    from: startPos,\n    to: cm.getCursor()\n  };\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/collections/emptyStateRequest.js",
    "content": "import React from 'react';\nimport { IconApi, IconBrandGraphql, IconPlugConnected, IconCode } from '@tabler/icons';\nimport { newHttpRequest, newWsRequest, newGrpcRequest } from 'providers/ReduxStore/slices/collections/actions';\nimport { generateUniqueRequestName } from 'utils/collections';\nimport { sanitizeName } from 'utils/common/regex';\nimport { formatIpcError } from 'utils/common/error';\nimport toast from 'react-hot-toast';\n\nconst createRequest = async ({ dispatch, collection, itemUid, requestType }) => {\n  try {\n    const uniqueName = await generateUniqueRequestName(collection, 'Untitled', itemUid);\n    const filename = sanitizeName(uniqueName);\n\n    const baseParams = {\n      requestName: uniqueName,\n      filename,\n      requestUrl: '',\n      collectionUid: collection.uid,\n      itemUid\n    };\n\n    switch (requestType) {\n      case 'http':\n        await dispatch(newHttpRequest({ ...baseParams, requestType: 'http-request', requestMethod: 'GET' }));\n        break;\n      case 'graphql':\n        await dispatch(\n          newHttpRequest({\n            ...baseParams,\n            requestType: 'graphql-request',\n            requestMethod: 'POST',\n            body: { mode: 'graphql', graphql: { query: '', variables: '' } }\n          })\n        );\n        break;\n      case 'websocket':\n        await dispatch(newWsRequest({ ...baseParams, requestMethod: 'ws' }));\n        break;\n      case 'grpc':\n        await dispatch(newGrpcRequest(baseParams));\n        break;\n    }\n  } catch (err) {\n    toast.error(formatIpcError(err) || 'An error occurred while adding the request');\n  }\n};\n\n/**\n * Returns menu items for the empty state \"Add request\" dropdown.\n * Used by both Collection (empty collection) and CollectionItem (empty folder).\n */\nexport const createEmptyStateMenuItems = ({ dispatch, collection, itemUid }) => {\n  const handleCreate = (requestType) => () => {\n    createRequest({ dispatch, collection, itemUid, requestType });\n  };\n\n  return [\n    {\n      id: 'http',\n      label: 'HTTP',\n      leftSection: <IconApi size={16} strokeWidth={2} />,\n      onClick: handleCreate('http')\n    },\n    {\n      id: 'graphql',\n      label: 'GraphQL',\n      leftSection: <IconBrandGraphql size={16} strokeWidth={2} />,\n      onClick: handleCreate('graphql')\n    },\n    {\n      id: 'grpc',\n      label: 'gRPC',\n      leftSection: <IconCode size={16} strokeWidth={2} />,\n      onClick: handleCreate('grpc')\n    },\n    {\n      id: 'websocket',\n      label: 'WebSocket',\n      leftSection: <IconPlugConnected size={16} strokeWidth={2} />,\n      onClick: handleCreate('websocket')\n    }\n  ];\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/collections/export.js",
    "content": "import * as FileSaver from 'file-saver';\nimport get from 'lodash/get';\nimport each from 'lodash/each';\nimport { filterTransientItems } from 'utils/collections';\n\nexport const deleteUidsInItems = (items) => {\n  each(items, (item) => {\n    delete item.uid;\n\n    if (['http-request', 'graphql-request', 'grpc-request'].includes(item.type)) {\n      each(get(item, 'request.headers'), (header) => delete header.uid);\n      each(get(item, 'request.params'), (param) => delete param.uid);\n      each(get(item, 'request.vars.req'), (v) => delete v.uid);\n      each(get(item, 'request.vars.res'), (v) => delete v.uid);\n      each(get(item, 'request.vars.assertions'), (a) => delete a.uid);\n      each(get(item, 'request.body.multipartForm'), (param) => delete param.uid);\n      each(get(item, 'request.body.formUrlEncoded'), (param) => delete param.uid);\n      each(get(item, 'request.body.file'), (param) => delete param.uid);\n\n      each(get(item, 'examples'), (example) => {\n        delete example.uid;\n        delete example.itemUid;\n        each(get(example, 'request.headers'), (header) => delete header.uid);\n        each(get(example, 'request.params'), (param) => delete param.uid);\n        each(get(example, 'request.body.multipartForm'), (param) => delete param.uid);\n        each(get(example, 'request.body.formUrlEncoded'), (param) => delete param.uid);\n        each(get(example, 'request.body.file'), (param) => delete param.uid);\n        each(get(example, 'response.headers'), (header) => delete header.uid);\n      });\n    }\n\n    if (item.items && item.items.length) {\n      deleteUidsInItems(item.items);\n    }\n  });\n};\n\n/**\n * Some of the models in the app are not consistent with the Collection Json format\n * This function is used to transform the models to the Collection Json format\n */\nexport const transformItem = (items = []) => {\n  each(items, (item) => {\n    if (['http-request', 'graphql-request', 'grpc-request', 'ws-request'].includes(item.type)) {\n      if (item.type === 'graphql-request') {\n        item.type = 'graphql';\n      }\n\n      if (item.type === 'http-request') {\n        item.type = 'http';\n      }\n\n      if (item.type === 'grpc-request') {\n        item.type = 'grpc';\n      }\n\n      if (item.type === 'ws-request') {\n        item.type = 'ws';\n      }\n    }\n\n    each(get(item, 'examples'), (example) => {\n      if (example.type === 'graphql-request') {\n        example.type = 'graphql';\n      } else if (example.type === 'http-request') {\n        example.type = 'http';\n      } else if (example.type === 'grpc-request') {\n        example.type = 'grpc';\n      } else if (example.type === 'ws-request') {\n        example.type = 'ws';\n      }\n    });\n\n    if (item.items && item.items.length) {\n      transformItem(item.items);\n    }\n  });\n};\n\nexport const deleteUidsInEnvs = (envs) => {\n  each(envs, (env) => {\n    delete env.uid;\n    each(env.variables, (variable) => delete variable.uid);\n  });\n};\n\nexport const deleteSecretsInEnvs = (envs) => {\n  each(envs, (env) => {\n    each(env.variables, (variable) => {\n      if (variable.secret) {\n        variable.value = '';\n      }\n    });\n  });\n};\n\nexport const exportCollection = (collection, version) => {\n  // delete uids\n  delete collection.uid;\n\n  // delete process variables\n  delete collection.processEnvVariables;\n  delete collection.workspaceProcessEnvVariables;\n\n  // filter out transient items\n  collection.items = filterTransientItems(collection.items);\n\n  deleteUidsInItems(collection.items);\n  deleteUidsInEnvs(collection.environments);\n  deleteSecretsInEnvs(collection.environments);\n  transformItem(collection.items);\n\n  collection.exportedAt = new Date().toISOString();\n  collection.exportedUsing = version ? `Bruno/${version}` : 'Bruno';\n\n  const fileName = `${collection.name}.json`;\n  const fileBlob = new Blob([JSON.stringify(collection, null, 2)], { type: 'application/json' });\n\n  FileSaver.saveAs(fileBlob, fileName);\n};\n\nexport default exportCollection;\n"
  },
  {
    "path": "packages/bruno-app/src/utils/collections/index.js",
    "content": "import { cloneDeep, isEqual, sortBy, filter, map, isString, findIndex, find, each, get } from 'lodash';\nimport { uuid } from 'utils/common';\nimport { buildPersistedEnvVariables } from 'utils/environments';\nimport { sortByNameThenSequence } from 'utils/common/index';\nimport path from 'utils/common/path';\nimport { isRequestTagsIncluded } from '@usebruno/common';\n\nconst replaceTabsWithSpaces = (str, numSpaces = 2) => {\n  if (!str || !str.length || !isString(str)) {\n    return '';\n  }\n\n  return str.replaceAll('\\t', ' '.repeat(numSpaces));\n};\n\nexport const addDepth = (items = []) => {\n  const depth = (itms, initialDepth) => {\n    each(itms, (i) => {\n      i.depth = initialDepth;\n\n      if (i.items && i.items.length) {\n        depth(i.items, initialDepth + 1);\n      }\n    });\n  };\n\n  depth(items, 1);\n};\n\nexport const collapseAllItemsInCollection = (collection) => {\n  collection.collapsed = true;\n\n  const collapseItem = (items) => {\n    each(items, (i) => {\n      i.collapsed = true;\n\n      if (i.items && i.items.length) {\n        collapseItem(i.items);\n      }\n    });\n  };\n\n  collapseItem(collection.items);\n};\n\nexport const sortItems = (collection) => {\n  const sort = (obj) => {\n    if (obj.items && obj.items.length) {\n      obj.items = sortBy(obj.items, 'type');\n    }\n\n    each(obj.items, (i) => sort(i));\n  };\n\n  sort(collection);\n};\n\nexport const flattenItems = (items = []) => {\n  const flattenedItems = [];\n\n  const flatten = (itms, flattened) => {\n    each(itms, (i) => {\n      flattened.push(i);\n\n      if (i.items && i.items.length) {\n        flatten(i.items, flattened);\n      }\n    });\n  };\n\n  flatten(items, flattenedItems);\n\n  return flattenedItems;\n};\n\nexport const findItem = (items = [], itemUid) => {\n  return find(items, (i) => i.uid === itemUid);\n};\n\nexport const findCollectionByUid = (collections, collectionUid) => {\n  return find(collections, (c) => c.uid === collectionUid);\n};\n\nexport const findCollectionByPathname = (collections, pathname) => {\n  return find(collections, (c) => c.pathname === pathname);\n};\n\nexport const findCollectionByItemUid = (collections, itemUid) => {\n  return find(collections, (c) => {\n    return findItemInCollection(c, itemUid);\n  });\n};\n\nexport const findItemByPathname = (items = [], pathname) => {\n  return find(items, (i) => i.pathname === pathname);\n};\n\nexport const findItemInCollectionByPathname = (collection, pathname) => {\n  let flattenedItems = flattenItems(collection.items);\n\n  return findItemByPathname(flattenedItems, pathname);\n};\n\nexport const findItemInCollectionByItemUid = (collection, itemUid) => {\n  let flattenedItems = flattenItems(collection.items);\n  return findItem(flattenedItems, itemUid);\n};\n\nexport const findParentItemInCollectionByPathname = (collection, pathname) => {\n  let flattenedItems = flattenItems(collection.items);\n\n  return find(flattenedItems, (item) => {\n    return item.items && find(item.items, (i) => i.pathname === pathname);\n  });\n};\n\nexport const findItemInCollection = (collection, itemUid) => {\n  if (!collection || !collection.items) {\n    return null;\n  }\n  let flattenedItems = flattenItems(collection.items);\n\n  return findItem(flattenedItems, itemUid);\n};\n\nexport const findParentItemInCollection = (collection, itemUid) => {\n  let flattenedItems = flattenItems(collection.items);\n\n  return find(flattenedItems, (item) => {\n    return item.items && find(item.items, (i) => i.uid === itemUid);\n  });\n};\n\nexport const recursivelyGetAllItemUids = (items = []) => {\n  let flattenedItems = flattenItems(items);\n\n  return map(flattenedItems, (i) => i.uid);\n};\n\nexport const findEnvironmentInCollection = (collection, envUid) => {\n  return find(collection.environments, (e) => e.uid === envUid);\n};\n\nexport const findEnvironmentInCollectionByName = (collection, name) => {\n  return find(collection.environments, (e) => e.name === name);\n};\n\nexport const areItemsLoading = (folder) => {\n  if (!folder || folder.isLoading) {\n    return true;\n  }\n\n  let flattenedItems = flattenItems(folder.items);\n  return flattenedItems?.reduce((isLoading, i) => {\n    if (i?.loading) {\n      isLoading = true;\n    }\n    return isLoading;\n  }, false);\n};\n\nexport const getItemsLoadStats = (folder) => {\n  let loadingCount = 0;\n  let flattenedItems = flattenItems(folder.items);\n  flattenedItems?.forEach((i) => {\n    if (i?.loading) {\n      loadingCount += 1;\n    }\n  });\n  return {\n    loading: loadingCount,\n    total: flattenedItems?.length\n  };\n};\n\nexport const transformCollectionToSaveToExportAsFile = (collection, options = {}) => {\n  const copyHeaders = (headers) => {\n    return map(headers, (header) => {\n      return {\n        uid: header.uid,\n        name: header.name,\n        value: header.value,\n        description: header.description,\n        enabled: header.enabled\n      };\n    });\n  };\n\n  const copyParams = (params) => {\n    return map(params, (param) => {\n      return {\n        uid: param.uid,\n        name: param.name,\n        value: param.value,\n        description: param.description,\n        type: param.type,\n        enabled: param.enabled\n      };\n    });\n  };\n\n  const copyFormUrlEncodedParams = (params = []) => {\n    return map(params, (param) => {\n      return {\n        uid: param.uid,\n        name: param.name,\n        value: param.value,\n        description: param.description,\n        enabled: param.enabled\n      };\n    });\n  };\n\n  const copyMultipartFormParams = (params = []) => {\n    return map(params, (param) => {\n      return {\n        uid: param.uid,\n        type: param.type,\n        name: param.name,\n        value: param.value,\n        description: param.description,\n        enabled: param.enabled\n      };\n    });\n  };\n\n  const copyFileParams = (params = []) => {\n    return map(params, (param) => {\n      return {\n        uid: param.uid,\n        filePath: param.filePath,\n        contentType: param.contentType,\n        selected: param.selected\n      };\n    });\n  };\n\n  const copyExamples = (examples = []) => {\n    return map(examples, (example) => {\n      const copiedExample = {\n        uid: example.uid,\n        itemUid: example.itemUid,\n        name: example.name,\n        description: example.description,\n        type: example.type,\n        request: {\n          url: example.request.url,\n          method: example.request.method,\n          headers: copyHeaders(example.request.headers),\n          params: copyParams(example.request.params),\n          body: {\n            mode: example.request.body.mode,\n            json: example.request.body.json,\n            text: example.request.body.text,\n            xml: example.request.body.xml,\n            graphql: example.request.body.graphql,\n            sparql: example.request.body.sparql,\n            formUrlEncoded: copyFormUrlEncodedParams(example.request.body.formUrlEncoded),\n            multipartForm: copyMultipartFormParams(example.request.body.multipartForm),\n            file: copyFileParams(example.request.body.file),\n            grpc: example.request.body.grpc,\n            ws: example.request.body.ws\n          },\n          auth: example.request.auth\n        },\n        response: {\n          status: example.response.status,\n          statusText: example.response.statusText,\n          headers: copyHeaders(example.response.headers),\n          body: example.response.body\n        }\n      };\n\n      // Handle gRPC-specific fields if present\n      if (example.request.methodType) {\n        copiedExample.request.methodType = example.request.methodType;\n      }\n      if (example.request.protoPath) {\n        copiedExample.request.protoPath = example.request.protoPath;\n      }\n\n      return copiedExample;\n    });\n  };\n\n  const normalizeFilenameToBru = (filename) => {\n    if (!filename) return filename;\n    return filename.replace(/\\.(yml|yaml)$/i, '.bru');\n  };\n\n  const copyItems = (sourceItems, destItems) => {\n    each(sourceItems, (si) => {\n      if (!isItemAFolder(si) && !isItemARequest(si) && si.type !== 'js') {\n        return;\n      }\n\n      // Skip transient requests\n      if (si.isTransient) {\n        return;\n      }\n\n      const isGrpcRequest = si.type === 'grpc-request';\n\n      const di = {\n        uid: si.uid,\n        type: si.type,\n        name: si.name,\n        filename: isItemARequest(si) ? normalizeFilenameToBru(si.filename) : si.filename,\n        seq: si.seq,\n        settings: si.settings,\n        tags: si.tags,\n        examples: copyExamples(si.examples || [])\n      };\n\n      if (si.request) {\n        di.request = {\n          url: si.request.url,\n          method: si.request.method,\n          headers: copyHeaders(si.request.headers),\n          params: copyParams(si.request.params),\n          body: {\n            mode: si.request.body.mode,\n            json: si.request.body.json,\n            text: si.request.body.text,\n            xml: si.request.body.xml,\n            graphql: si.request.body.graphql,\n            sparql: si.request.body.sparql,\n            formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded),\n            multipartForm: copyMultipartFormParams(si.request.body.multipartForm),\n            file: copyFileParams(si.request.body.file),\n            grpc: si.request.body.grpc,\n            ws: si.request.body.ws\n          },\n          script: si.request.script,\n          vars: si.request.vars,\n          assertions: si.request.assertions,\n          tests: si.request.tests,\n          docs: si.request.docs\n        };\n\n        if (isGrpcRequest) {\n          di.request.methodType = si.request.methodType;\n          di.request.protoPath = si.request.protoPath;\n          delete di.request.params;\n        }\n\n        // Handle auth object dynamically\n        di.request.auth = {\n          mode: get(si.request, 'auth.mode', 'none')\n        };\n\n        switch (di.request.auth.mode) {\n          case 'awsv4':\n            di.request.auth.awsv4 = {\n              accessKeyId: get(si.request, 'auth.awsv4.accessKeyId', ''),\n              secretAccessKey: get(si.request, 'auth.awsv4.secretAccessKey', ''),\n              sessionToken: get(si.request, 'auth.awsv4.sessionToken', ''),\n              service: get(si.request, 'auth.awsv4.service', ''),\n              region: get(si.request, 'auth.awsv4.region', ''),\n              profileName: get(si.request, 'auth.awsv4.profileName', '')\n            };\n            break;\n          case 'basic':\n            di.request.auth.basic = {\n              username: get(si.request, 'auth.basic.username', ''),\n              password: get(si.request, 'auth.basic.password', '')\n            };\n            break;\n          case 'bearer':\n            di.request.auth.bearer = {\n              token: get(si.request, 'auth.bearer.token', '')\n            };\n            break;\n          case 'digest':\n            di.request.auth.digest = {\n              username: get(si.request, 'auth.digest.username', ''),\n              password: get(si.request, 'auth.digest.password', '')\n            };\n            break;\n          case 'ntlm':\n            di.request.auth.ntlm = {\n              username: get(si.request, 'auth.ntlm.username', ''),\n              password: get(si.request, 'auth.ntlm.password', ''),\n              domain: get(si.request, 'auth.ntlm.domain', '')\n            };\n            break;\n          case 'oauth2':\n            let grantType = get(si.request, 'auth.oauth2.grantType', '');\n            switch (grantType) {\n              case 'password':\n                di.request.auth.oauth2 = {\n                  grantType: grantType,\n                  accessTokenUrl: get(si.request, 'auth.oauth2.accessTokenUrl', ''),\n                  refreshTokenUrl: get(si.request, 'auth.oauth2.refreshTokenUrl', ''),\n                  username: get(si.request, 'auth.oauth2.username', ''),\n                  password: get(si.request, 'auth.oauth2.password', ''),\n                  clientId: get(si.request, 'auth.oauth2.clientId', ''),\n                  clientSecret: get(si.request, 'auth.oauth2.clientSecret', ''),\n                  scope: get(si.request, 'auth.oauth2.scope', ''),\n                  credentialsPlacement: get(si.request, 'auth.oauth2.credentialsPlacement', 'body'),\n                  credentialsId: get(si.request, 'auth.oauth2.credentialsId', 'credentials'),\n                  tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'),\n                  tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', ''),\n                  tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),\n                  autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),\n                  autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),\n                  additionalParameters: get(si.request, 'auth.oauth2.additionalParameters', {})\n                };\n                break;\n              case 'authorization_code':\n                di.request.auth.oauth2 = {\n                  grantType: grantType,\n                  callbackUrl: get(si.request, 'auth.oauth2.callbackUrl', ''),\n                  authorizationUrl: get(si.request, 'auth.oauth2.authorizationUrl', ''),\n                  accessTokenUrl: get(si.request, 'auth.oauth2.accessTokenUrl', ''),\n                  refreshTokenUrl: get(si.request, 'auth.oauth2.refreshTokenUrl', ''),\n                  clientId: get(si.request, 'auth.oauth2.clientId', ''),\n                  clientSecret: get(si.request, 'auth.oauth2.clientSecret', ''),\n                  scope: get(si.request, 'auth.oauth2.scope', ''),\n                  credentialsPlacement: get(si.request, 'auth.oauth2.credentialsPlacement', 'body'),\n                  pkce: get(si.request, 'auth.oauth2.pkce', false),\n                  credentialsId: get(si.request, 'auth.oauth2.credentialsId', 'credentials'),\n                  tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'),\n                  tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', ''),\n                  tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),\n                  autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),\n                  autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),\n                  additionalParameters: get(si.request, 'auth.oauth2.additionalParameters', {})\n                };\n                break;\n              case 'implicit':\n                di.request.auth.oauth2 = {\n                  grantType: grantType,\n                  callbackUrl: get(si.request, 'auth.oauth2.callbackUrl', ''),\n                  authorizationUrl: get(si.request, 'auth.oauth2.authorizationUrl', ''),\n                  clientId: get(si.request, 'auth.oauth2.clientId', ''),\n                  scope: get(si.request, 'auth.oauth2.scope', ''),\n                  state: get(si.request, 'auth.oauth2.state', ''),\n                  credentialsId: get(si.request, 'auth.oauth2.credentialsId', 'credentials'),\n                  tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'),\n                  tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', 'Bearer'),\n                  tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),\n                  autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),\n                  additionalParameters: get(si.request, 'auth.oauth2.additionalParameters', {})\n                };\n                break;\n              case 'client_credentials':\n                di.request.auth.oauth2 = {\n                  grantType: grantType,\n                  accessTokenUrl: get(si.request, 'auth.oauth2.accessTokenUrl', ''),\n                  refreshTokenUrl: get(si.request, 'auth.oauth2.refreshTokenUrl', ''),\n                  clientId: get(si.request, 'auth.oauth2.clientId', ''),\n                  clientSecret: get(si.request, 'auth.oauth2.clientSecret', ''),\n                  scope: get(si.request, 'auth.oauth2.scope', ''),\n                  credentialsPlacement: get(si.request, 'auth.oauth2.credentialsPlacement', 'body'),\n                  credentialsId: get(si.request, 'auth.oauth2.credentialsId', 'credentials'),\n                  tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'),\n                  tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', ''),\n                  tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),\n                  autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),\n                  autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),\n                  additionalParameters: get(si.request, 'auth.oauth2.additionalParameters', {})\n                };\n                break;\n            }\n            break;\n          case 'apikey':\n            di.request.auth.apikey = {\n              key: get(si.request, 'auth.apikey.key', ''),\n              value: get(si.request, 'auth.apikey.value', ''),\n              placement: get(si.request, 'auth.apikey.placement', 'header')\n            };\n            break;\n          case 'wsse':\n            di.request.auth.wsse = {\n              username: get(si.request, 'auth.wsse.username', ''),\n              password: get(si.request, 'auth.wsse.password', '')\n            };\n            break;\n          default:\n            break;\n        }\n\n        if (di.request.body.mode === 'json') {\n          di.request.body.json = replaceTabsWithSpaces(di.request.body.json);\n        }\n\n        if (di.request.body.mode === 'grpc') {\n          di.request.body.grpc = di.request.body.grpc.map(({ name, content }, index) => ({\n            name: name ? name : `message ${index + 1}`,\n            content: replaceTabsWithSpaces(content)\n          }));\n        }\n\n        if (di.request.body.mode === 'ws') {\n          di.request.body.ws = di.request.body.ws.map(({ name, content, type }, index) => ({\n            name: name ? name : `message ${index + 1}`,\n            type: type ?? 'json',\n            content: replaceTabsWithSpaces(content)\n          }));\n        }\n      }\n\n      if (si.type == 'folder' && si?.root) {\n        di.root = {\n          request: {}\n        };\n\n        let { request, meta, docs } = si?.root || {};\n        let { auth, headers, script = {}, vars = {}, tests } = request || {};\n\n        // folder level auth\n        if (auth?.mode) {\n          di.root.request.auth = auth;\n        }\n\n        // folder level headers\n        if (headers?.length) {\n          di.root.request.headers = headers;\n        }\n        // folder level script\n        if (Object.keys(script)?.length) {\n          di.root.request.script = {};\n          if (script?.req?.length) {\n            di.root.request.script.req = script?.req;\n          }\n          if (script?.res?.length) {\n            di.root.request.script.res = script?.res;\n          }\n        }\n        // folder level vars\n        if (Object.keys(vars)?.length) {\n          di.root.request.vars = {};\n          if (vars?.req?.length) {\n            di.root.request.vars.req = vars?.req;\n          }\n          if (vars?.res?.length) {\n            di.root.request.vars.res = vars?.res;\n          }\n        }\n        // folder level tests\n        if (tests?.length) {\n          di.root.request.tests = tests;\n        }\n\n        // folder level docs\n        if (docs?.length) {\n          di.root.docs = docs;\n        }\n\n        if (meta?.name) {\n          di.root.meta = {};\n          di.root.meta.name = meta?.name;\n          di.root.meta.seq = meta?.seq;\n        }\n        if (!Object.keys(di.root.request)?.length) {\n          delete di.root.request;\n        }\n        if (!Object.keys(di.root)?.length) {\n          delete di.root;\n        }\n      }\n\n      if (si.type === 'js') {\n        di.fileContent = si.raw;\n      }\n\n      destItems.push(di);\n\n      if (si.items && si.items.length) {\n        di.items = [];\n        copyItems(si.items, di.items);\n      }\n    });\n  };\n\n  const collectionToSave = {};\n  collectionToSave.name = collection.name;\n  collectionToSave.uid = collection.uid;\n\n  // todo: move this to the place where collection gets created\n  collectionToSave.version = '1';\n  collectionToSave.items = [];\n  collectionToSave.activeEnvironmentUid = collection.activeEnvironmentUid;\n  // Save environments without runtime metadata (ephemeral/persistedValue)\n  collectionToSave.environments = (collection.environments || []).map((env) => ({\n    ...env,\n    variables: buildPersistedEnvVariables(env?.variables, { mode: 'save' })\n  }));\n\n  collectionToSave.root = {\n    request: {}\n  };\n\n  let { request, docs, meta } = collection?.root || {};\n  let { auth, headers, script = {}, vars = {}, tests } = request || {};\n\n  // collection level auth\n  if (auth?.mode) {\n    collectionToSave.root.request.auth = auth;\n  }\n  // collection level headers\n  if (headers?.length) {\n    collectionToSave.root.request.headers = headers;\n  }\n  // collection level script\n  if (Object.keys(script)?.length) {\n    collectionToSave.root.request.script = {};\n    if (script?.req?.length) {\n      collectionToSave.root.request.script.req = script?.req;\n    }\n    if (script?.res?.length) {\n      collectionToSave.root.request.script.res = script?.res;\n    }\n  }\n  // collection level vars\n  if (Object.keys(vars)?.length) {\n    collectionToSave.root.request.vars = {};\n    if (vars?.req?.length) {\n      collectionToSave.root.request.vars.req = vars?.req;\n    }\n    if (vars?.res?.length) {\n      collectionToSave.root.request.vars.res = vars?.res;\n    }\n  }\n  // collection level tests\n  if (tests?.length) {\n    collectionToSave.root.request.tests = tests;\n  }\n  // collection level docs\n  if (docs?.length) {\n    collectionToSave.root.docs = docs;\n  }\n  if (meta?.name) {\n    collectionToSave.root.meta = {};\n    collectionToSave.root.meta.name = meta?.name;\n  }\n  if (!Object.keys(collectionToSave.root.request)?.length) {\n    delete collectionToSave.root.request;\n  }\n  if (!Object.keys(collectionToSave.root)?.length) {\n    delete collectionToSave.root;\n  }\n\n  collectionToSave.brunoConfig = cloneDeep(collection?.brunoConfig);\n\n  // delete proxy password if present\n  if (collectionToSave?.brunoConfig?.proxy?.auth?.password) {\n    delete collectionToSave.brunoConfig.proxy.auth.password;\n  }\n\n  if (collectionToSave?.brunoConfig?.protobuf?.importPaths) {\n    collectionToSave.brunoConfig.protobuf.importPaths = collectionToSave.brunoConfig.protobuf.importPaths.map((importPath) => {\n      delete importPath.exists;\n      return importPath;\n    });\n  }\n\n  if (collectionToSave?.brunoConfig?.protobuf?.protoFiles) {\n    collectionToSave.brunoConfig.protobuf.protoFiles = collectionToSave.brunoConfig.protobuf.protoFiles.map((protoFile) => {\n      delete protoFile.exists;\n      return protoFile;\n    });\n  }\n\n  copyItems(collection.items, collectionToSave.items);\n  return collectionToSave;\n};\n\nexport const transformRequestToSaveToFilesystem = (item) => {\n  const _item = item.draft ? item.draft : item;\n\n  // Transform examples to ensure status is a number\n  const transformExamples = (examples = []) => {\n    return map(examples, (example) => ({\n      ...example,\n      response: example.response ? {\n        ...example.response,\n        status: example.response.status !== undefined && example.response.status !== null\n          ? Number(example.response.status)\n          : null\n      } : example.response\n    }));\n  };\n\n  const itemToSave = {\n    uid: _item.uid,\n    type: _item.type,\n    name: _item.name,\n    seq: _item.seq,\n    settings: _item.settings,\n    tags: _item.tags,\n    examples: transformExamples(_item.examples || []),\n    request: {\n      method: _item.request.method,\n      url: _item.request.url,\n      params: [],\n      headers: [],\n      auth: _item.request.auth,\n      body: _item.request.body,\n      script: _item.request.script,\n      vars: _item.request.vars,\n      assertions: _item.request.assertions,\n      tests: _item.request.tests,\n      docs: _item.request.docs\n    }\n  };\n\n  if (_item.type === 'grpc-request') {\n    itemToSave.request.methodType = _item.request.methodType;\n    itemToSave.request.protoPath = _item.request.protoPath;\n    delete itemToSave.request.params;\n  }\n\n  if (_item.type === 'ws-request') {\n    delete itemToSave.request.method;\n    delete itemToSave.request.methodType;\n    delete itemToSave.request.params;\n  }\n\n  // Only process params for non-gRPC requests\n  if (!['grpc-request', 'ws-request'].includes(_item.type)) {\n    each(_item.request.params, (param) => {\n      itemToSave.request.params.push({\n        uid: param.uid,\n        name: param.name,\n        value: param.value,\n        description: param.description,\n        type: param.type,\n        enabled: param.enabled\n      });\n    });\n  }\n\n  each(_item.request.headers, (header) => {\n    itemToSave.request.headers.push({\n      uid: header.uid,\n      name: header.name,\n      value: header.value,\n      description: header.description,\n      enabled: header.enabled\n    });\n  });\n\n  if (itemToSave.request.body.mode === 'json') {\n    itemToSave.request.body = {\n      ...itemToSave.request.body,\n      json: replaceTabsWithSpaces(itemToSave.request.body.json)\n    };\n  }\n\n  if (itemToSave.request.body.mode === 'grpc') {\n    itemToSave.request.body = {\n      ...itemToSave.request.body,\n      grpc: itemToSave.request.body.grpc.map(({ name, content }, index) => ({\n        name: name ? name : `message ${index + 1}`,\n        content: replaceTabsWithSpaces(content)\n      }))\n    };\n  }\n\n  if (itemToSave.request.body.mode === 'ws') {\n    itemToSave.request.body = {\n      ...itemToSave.request.body,\n      ws: itemToSave.request.body.ws.map(({ name, content, type }, index) => ({\n        name: name ? name : `message ${index + 1}`,\n        type,\n        content: replaceTabsWithSpaces(content)\n      }))\n    };\n  }\n\n  return itemToSave;\n};\n\nexport const transformCollectionRootToSave = (collection) => {\n  const _collection = collection.draft?.root ? collection.draft.root : collection.root;\n\n  const collectionRootToSave = {\n    docs: _collection?.docs,\n    meta: _collection?.meta,\n    request: {\n      auth: _collection?.request?.auth,\n      headers: [],\n      script: _collection?.request?.script,\n      vars: _collection?.request?.vars,\n      tests: _collection?.request?.tests\n    }\n  };\n\n  each(_collection?.request?.headers, (header) => {\n    collectionRootToSave.request.headers.push({\n      uid: header.uid,\n      name: header.name,\n      value: header.value,\n      description: header.description,\n      enabled: header.enabled\n    });\n  });\n\n  return collectionRootToSave;\n};\n\nexport const transformFolderRootToSave = (folder) => {\n  const _folder = folder.draft ? folder.draft : folder.root;\n  const folderRootToSave = {\n    docs: _folder.docs,\n    request: {\n      auth: _folder?.request?.auth,\n      headers: [],\n      script: _folder?.request?.script,\n      vars: _folder?.request?.vars,\n      tests: _folder?.request?.tests\n    }\n  };\n\n  each(_folder.request.headers, (header) => {\n    folderRootToSave.request.headers.push({\n      uid: header.uid,\n      name: header.name,\n      value: header.value,\n      description: header.description,\n      enabled: header.enabled\n    });\n  });\n\n  return folderRootToSave;\n};\n\n// todo: optimize this\nexport const deleteItemInCollection = (itemUid, collection) => {\n  collection.items = filter(collection.items, (i) => i.uid !== itemUid);\n\n  let flattenedItems = flattenItems(collection.items);\n  each(flattenedItems, (i) => {\n    if (i.items && i.items.length) {\n      i.items = filter(i.items, (i) => i.uid !== itemUid);\n    }\n  });\n};\n\nexport const deleteItemInCollectionByPathname = (pathname, collection) => {\n  collection.items = filter(collection.items, (i) => i.pathname !== pathname);\n\n  let flattenedItems = flattenItems(collection.items);\n  each(flattenedItems, (i) => {\n    if (i.items && i.items.length) {\n      i.items = filter(i.items, (i) => i.pathname !== pathname);\n    }\n  });\n};\n\nexport const isItemARequest = (item) => {\n  return item.hasOwnProperty('request') && ['http-request', 'graphql-request', 'grpc-request', 'ws-request'].includes(item.type) && !item.items;\n};\n\nexport const isItemAFolder = (item) => {\n  return !item.hasOwnProperty('request') && item.type === 'folder';\n};\n\nexport const humanizeRequestBodyMode = (mode) => {\n  let label = 'No Body';\n  switch (mode) {\n    case 'json': {\n      label = 'JSON';\n      break;\n    }\n    case 'text': {\n      label = 'TEXT';\n      break;\n    }\n    case 'xml': {\n      label = 'XML';\n      break;\n    }\n    case 'sparql': {\n      label = 'SPARQL';\n      break;\n    }\n    case 'file': {\n      label = 'File / Binary';\n      break;\n    }\n    case 'formUrlEncoded': {\n      label = 'Form URL Encoded';\n      break;\n    }\n    case 'multipartForm': {\n      label = 'Multipart Form';\n      break;\n    }\n  }\n\n  return label;\n};\n\nexport const humanizeRequestAuthMode = (mode) => {\n  let label = 'No Auth';\n  switch (mode) {\n    case 'inherit': {\n      label = 'Inherit';\n      break;\n    }\n    case 'awsv4': {\n      label = 'AWS Sig V4';\n      break;\n    }\n    case 'basic': {\n      label = 'Basic Auth';\n      break;\n    }\n    case 'bearer': {\n      label = 'Bearer Token';\n      break;\n    }\n    case 'digest': {\n      label = 'Digest Auth';\n      break;\n    }\n    case 'ntlm': {\n      label = 'NTLM';\n      break;\n    }\n    case 'oauth2': {\n      label = 'OAuth 2.0';\n      break;\n    }\n    case 'wsse': {\n      label = 'WSSE Auth';\n      break;\n    }\n    case 'apikey': {\n      label = 'API Key';\n      break;\n    }\n  }\n\n  return label;\n};\n\nexport const humanizeRequestAPIKeyPlacement = (placement) => {\n  let label = 'Header';\n  switch (placement) {\n    case 'header': {\n      label = 'Header';\n      break;\n    }\n    case 'queryparams': {\n      label = 'Query Params';\n      break;\n    }\n  }\n\n  return label;\n};\n\nexport const humanizeGrantType = (mode) => {\n  if (!mode || typeof mode !== 'string') {\n    return '';\n  }\n\n  switch (mode) {\n    case 'password':\n      return 'Password Credentials';\n    case 'authorization_code':\n      return 'Authorization Code';\n    case 'client_credentials':\n      return 'Client Credentials';\n    case 'implicit':\n      return 'Implicit';\n    default:\n      return mode;\n  }\n};\n\nexport const refreshUidsInItem = (item) => {\n  item.uid = uuid();\n\n  each(get(item, 'request.headers'), (header) => (header.uid = uuid()));\n  each(get(item, 'request.params'), (param) => (param.uid = uuid()));\n  each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid()));\n  each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid()));\n  each(get(item, 'request.body.file'), (param) => (param.uid = uuid()));\n  each(get(item, 'request.assertions'), (assertion) => (assertion.uid = uuid()));\n\n  return item;\n};\n\nexport const deleteUidsInItem = (item) => {\n  delete item.uid;\n  const params = get(item, 'request.params', []);\n  const headers = get(item, 'request.headers', []);\n  const bodyFormUrlEncoded = get(item, 'request.body.formUrlEncoded', []);\n  const bodyMultipartForm = get(item, 'request.body.multipartForm', []);\n  const file = get(item, 'request.body.file', []);\n  const assertions = get(item, 'request.assertions', []);\n\n  params.forEach((param) => delete param.uid);\n  headers.forEach((header) => delete header.uid);\n  bodyFormUrlEncoded.forEach((param) => delete param.uid);\n  bodyMultipartForm.forEach((param) => delete param.uid);\n  file.forEach((param) => delete param.uid);\n  assertions.forEach((assertion) => delete assertion.uid);\n\n  return item;\n};\n\nexport const areItemsTheSameExceptSeqUpdate = (_item1, _item2) => {\n  let item1 = cloneDeep(_item1);\n  let item2 = cloneDeep(_item2);\n\n  // remove seq from both items\n  delete item1.seq;\n  delete item2.seq;\n\n  // remove draft from both items\n  delete item1.draft;\n  delete item2.draft;\n\n  // get projection of both items\n  item1 = transformRequestToSaveToFilesystem(item1);\n  item2 = transformRequestToSaveToFilesystem(item2);\n\n  // delete uids from both items\n  deleteUidsInItem(item1);\n  deleteUidsInItem(item2);\n\n  return isEqual(item1, item2);\n};\n\n/**\n * Check if a request has actual changes (excluding examples)\n * This function compares the request data between the original item and its draft,\n * but excludes examples from the comparison to determine if the save dot should be shown\n */\nexport const hasRequestChanges = (item) => {\n  if (!item || !item.draft) {\n    return false;\n  }\n\n  // Create copies of the item and draft without examples for comparison\n  const originalItem = cloneDeep(item);\n  const draftItem = cloneDeep(item.draft);\n\n  // Remove examples from both items for comparison\n  delete originalItem.examples;\n  delete originalItem.draft;\n  delete draftItem.examples;\n  delete draftItem.draft;\n\n  return !isEqual(originalItem, draftItem);\n};\n\n/**\n * Check if a specific example has unsaved changes\n * This function compares the example data between the original item and its draft\n */\nexport const hasExampleChanges = (_item, exampleUid) => {\n  if (!_item || !_item.draft || !exampleUid) {\n    return false;\n  }\n\n  const item = cloneDeep(_item);\n  deleteUidsInItem(item);\n\n  // Get the original example from the saved item\n  const originalExample = item.examples?.find((ex) => ex.uid === exampleUid);\n  if (!originalExample) {\n    return false;\n  }\n\n  // Get the draft example from the draft item\n  const draftExample = item.draft.examples?.find((ex) => ex.uid === exampleUid);\n  if (!draftExample) {\n    return false;\n  }\n\n  // Compare the examples (excluding any internal metadata)\n  return !isEqual(originalExample, draftExample);\n};\n\nexport const getDefaultRequestPaneTab = (item) => {\n  if (item.type === 'http-request') {\n    // If no params are enabled and body mode is set, default to 'body' tab\n    // This provides better UX for POST/PUT requests with a body\n    const request = item.draft?.request || item.request;\n    const params = request?.params || [];\n    const bodyMode = request?.body?.mode;\n    const hasEnabledParams = params.some((p) => p.enabled);\n\n    if (!hasEnabledParams && bodyMode && bodyMode !== 'none') {\n      return 'body';\n    }\n    return 'params';\n  }\n\n  if (item.type === 'graphql-request') {\n    return 'query';\n  }\n\n  if (['ws-request', 'grpc-request'].includes(item.type)) {\n    return 'body';\n  }\n};\n\nexport const getGlobalEnvironmentVariables = ({ globalEnvironments, activeGlobalEnvironmentUid }) => {\n  let variables = {};\n  const environment = globalEnvironments?.find((env) => env?.uid === activeGlobalEnvironmentUid);\n  if (environment) {\n    each(environment.variables, (variable) => {\n      if (variable.name && variable.enabled) {\n        variables[variable.name] = variable.value;\n      }\n    });\n  }\n  return variables;\n};\n\nexport const getGlobalEnvironmentVariablesMasked = ({ globalEnvironments, activeGlobalEnvironmentUid }) => {\n  const environment = globalEnvironments?.find((env) => env?.uid === activeGlobalEnvironmentUid);\n\n  if (environment && Array.isArray(environment.variables)) {\n    return environment.variables\n      .filter((variable) => variable.name && variable.value && variable.enabled && variable.secret)\n      .map((variable) => variable.name);\n  }\n\n  return [];\n};\n\nexport const getEnvironmentVariables = (collection) => {\n  let variables = {};\n  if (collection) {\n    const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);\n    if (environment) {\n      each(environment.variables, (variable) => {\n        if (variable.name && variable.value && variable.enabled) {\n          variables[variable.name] = variable.value;\n        }\n      });\n    }\n  }\n\n  return variables;\n};\n\nexport const getEnvironmentVariablesMasked = (collection) => {\n  // Return an empty array if the collection is invalid or not provided\n  if (!collection || !collection.activeEnvironmentUid) {\n    return [];\n  }\n\n  // Find the active environment in the collection\n  const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);\n  if (!environment || !environment.variables) {\n    return [];\n  }\n\n  // Filter the environment variables to get only the masked (secret) ones\n  return environment.variables\n    .filter((variable) => variable.name && variable.value && variable.enabled && variable.secret)\n    .map((variable) => variable.name);\n};\n\nconst getPathParams = (item) => {\n  let pathParams = {};\n  if (item && item.request && item.request.params) {\n    item.request.params.forEach((param) => {\n      if (param.type === 'path' && param.name && param.value) {\n        pathParams[param.name] = param.value;\n      }\n    });\n  }\n  return pathParams;\n};\n\nexport const getTotalRequestCountInCollection = (collection) => {\n  let count = 0;\n  each(collection.items, (item) => {\n    if (isItemARequest(item) && !item.isTransient) {\n      count++;\n    } else if (isItemAFolder(item)) {\n      count += getTotalRequestCountInCollection(item);\n    }\n  });\n\n  return count;\n};\n\nexport const getAllVariables = (collection, item) => {\n  if (!collection) return {};\n  const envVariables = getEnvironmentVariables(collection);\n  const requestTreePath = getTreePathFromCollectionToItem(collection, item);\n  let { collectionVariables, folderVariables, requestVariables } = mergeVars(collection, requestTreePath);\n  const pathParams = getPathParams(item);\n  const { globalEnvironmentVariables = {} } = collection;\n\n  const { processEnvVariables = {}, runtimeVariables = {}, promptVariables = {}, workspaceProcessEnvVariables = {} } = collection;\n\n  // Merge workspace and collection processEnvVariables (collection takes priority)\n  const mergedProcessEnvVariables = {\n    ...workspaceProcessEnvVariables,\n    ...processEnvVariables\n  };\n\n  const mergedVariables = {\n    ...folderVariables,\n    ...requestVariables,\n    ...runtimeVariables,\n    ...promptVariables\n  };\n\n  const mergedVariablesGlobal = {\n    ...collectionVariables,\n    ...envVariables,\n    ...folderVariables,\n    ...requestVariables,\n    ...runtimeVariables,\n    ...promptVariables\n  };\n\n  const maskedEnvVariables = getEnvironmentVariablesMasked(collection) || [];\n  const maskedGlobalEnvVariables = collection?.globalEnvSecrets || [];\n\n  const filteredMaskedEnvVariables = maskedEnvVariables.filter((key) => !(key in mergedVariables));\n  const filteredMaskedGlobalEnvVariables = maskedGlobalEnvVariables.filter((key) => !(key in mergedVariablesGlobal));\n\n  const uniqueMaskedVariables = [...new Set([...filteredMaskedEnvVariables, ...filteredMaskedGlobalEnvVariables])];\n\n  const oauth2CredentialVariables = getFormattedCollectionOauth2Credentials({ oauth2Credentials: collection?.oauth2Credentials });\n\n  return {\n    ...globalEnvironmentVariables,\n    ...collectionVariables,\n    ...envVariables,\n    ...folderVariables,\n    ...requestVariables,\n    ...oauth2CredentialVariables,\n    ...runtimeVariables,\n    ...promptVariables,\n    pathParams: {\n      ...pathParams\n    },\n    maskedEnvVariables: uniqueMaskedVariables,\n    process: {\n      env: {\n        ...mergedProcessEnvVariables\n      }\n    }\n  };\n};\n\n// Merge headers from collection, folders, and request\nexport const mergeHeaders = (collection, request, requestTreePath) => {\n  let headers = new Map();\n\n  // Add collection headers first\n  const collectionHeaders = collection?.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []);\n  collectionHeaders.forEach((header) => {\n    if (header.enabled) {\n      headers.set(header.name, header);\n    }\n  });\n\n  // Add folder headers next, traversing from root to leaf\n  if (requestTreePath && requestTreePath.length > 0) {\n    for (let i of requestTreePath) {\n      if (i.type === 'folder') {\n        const folderHeaders = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'root.request.headers', []);\n        folderHeaders.forEach((header) => {\n          if (header.enabled) {\n            headers.set(header.name, header);\n          }\n        });\n      }\n    }\n  }\n\n  // Add request headers last (they take precedence)\n  const requestHeaders = request.headers || [];\n  requestHeaders.forEach((header) => {\n    if (header.enabled) {\n      headers.set(header.name, header);\n    }\n  });\n\n  // Convert Map back to array\n  return Array.from(headers.values());\n};\n\nexport const maskInputValue = (value) => {\n  if (!value || typeof value !== 'string') {\n    return '';\n  }\n\n  return value\n    .split('')\n    .map(() => '*')\n    .join('');\n};\n\nexport const getTreePathFromCollectionToItem = (collection, _item) => {\n  let path = [];\n  let item = findItemInCollection(collection, _item?.uid);\n  while (item) {\n    path.unshift(item);\n    item = findParentItemInCollection(collection, item?.uid);\n  }\n  return path;\n};\n\nconst mergeVars = (collection, requestTreePath = []) => {\n  let collectionVariables = {};\n  let folderVariables = {};\n  let requestVariables = {};\n  const collectionRoot = collection?.draft?.root || collection?.root || {};\n  let collectionRequestVars = get(collectionRoot, 'request.vars.req', []);\n  collectionRequestVars.forEach((_var) => {\n    if (_var.enabled) {\n      collectionVariables[_var.name] = _var.value;\n    }\n  });\n  for (let i of requestTreePath) {\n    if (!i) {\n      continue;\n    }\n\n    if (i.type === 'folder') {\n      // Check draft first, then fall back to root\n      const folderRoot = i.draft || i.root;\n      let vars = get(folderRoot, 'request.vars.req', []);\n      vars.forEach((_var) => {\n        if (_var.enabled) {\n          folderVariables[_var.name] = _var.value;\n        }\n      });\n    } else {\n      let vars = i.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []);\n      vars.forEach((_var) => {\n        if (_var.enabled) {\n          requestVariables[_var.name] = _var.value;\n        }\n      });\n    }\n  }\n  return {\n    collectionVariables,\n    folderVariables,\n    requestVariables\n  };\n};\n\nexport const getEnvVars = (environment = {}) => {\n  const variables = environment.variables;\n  if (!variables || !variables.length) {\n    return {\n      __name__: environment.name\n    };\n  }\n\n  const envVars = {};\n  each(variables, (variable) => {\n    if (variable.enabled) {\n      envVars[variable.name] = variable.value;\n    }\n  });\n\n  return {\n    ...envVars,\n    __name__: environment.name\n  };\n};\n\nexport const getFormattedCollectionOauth2Credentials = ({ oauth2Credentials = [] }) => {\n  let credentialsVariables = {};\n  oauth2Credentials.forEach(({ credentialsId, credentials }) => {\n    if (credentials) {\n      Object.entries(credentials).forEach(([key, value]) => {\n        credentialsVariables[`$oauth2.${credentialsId}.${key}`] = value;\n      });\n    }\n  });\n  return credentialsVariables;\n};\n\n// item sequence utils - START\n\nexport const resetSequencesInFolder = (folderItems) => {\n  const items = folderItems;\n  const sortedItems = sortByNameThenSequence(items);\n  return sortedItems.map((item, index) => {\n    item.seq = index + 1;\n    return item;\n  });\n};\n\nexport const isItemBetweenSequences = (itemSequence, sourceItemSequence, targetItemSequence) => {\n  if (targetItemSequence > sourceItemSequence) {\n    return itemSequence > sourceItemSequence && itemSequence < targetItemSequence;\n  }\n  return itemSequence < sourceItemSequence && itemSequence >= targetItemSequence;\n};\n\nexport const calculateNewSequence = (isDraggedItem, targetSequence, draggedSequence) => {\n  if (!isDraggedItem) {\n    return null;\n  }\n  return targetSequence > draggedSequence ? targetSequence - 1 : targetSequence;\n};\n\nexport const getReorderedItemsInTargetDirectory = ({ items, targetItemUid, draggedItemUid }) => {\n  const itemsWithFixedSequences = resetSequencesInFolder(cloneDeep(items));\n  const targetItem = findItem(itemsWithFixedSequences, targetItemUid);\n  const draggedItem = findItem(itemsWithFixedSequences, draggedItemUid);\n  const targetSequence = targetItem?.seq;\n  const draggedSequence = draggedItem?.seq;\n  itemsWithFixedSequences?.forEach((item) => {\n    const isDraggedItem = item?.uid === draggedItemUid;\n    const isBetween = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence);\n    if (isBetween) {\n      item.seq += targetSequence > draggedSequence ? -1 : 1;\n    }\n    const newSequence = calculateNewSequence(isDraggedItem, targetSequence, draggedSequence);\n    if (newSequence !== null) {\n      item.seq = newSequence;\n    }\n  });\n  // only return items that have been reordered\n  return itemsWithFixedSequences.filter((item) =>\n    items?.find((originalItem) => originalItem?.uid === item?.uid)?.seq !== item?.seq\n  );\n};\n\nexport const getReorderedItemsInSourceDirectory = ({ items }) => {\n  const itemsWithFixedSequences = resetSequencesInFolder(cloneDeep(items));\n  return itemsWithFixedSequences.filter((item) =>\n    items?.find((originalItem) => originalItem?.uid === item?.uid)?.seq !== item?.seq\n  );\n};\n\nexport const calculateDraggedItemNewPathname = ({ draggedItem, targetItem, dropType, collectionPathname }) => {\n  const { pathname: targetItemPathname } = targetItem;\n  const { filename: draggedItemFilename } = draggedItem;\n  const targetItemDirname = path.dirname(targetItemPathname);\n  const isTargetTheCollection = targetItemPathname === collectionPathname;\n  const isTargetItemAFolder = isItemAFolder(targetItem);\n\n  if (dropType === 'inside' && (isTargetItemAFolder || isTargetTheCollection)) {\n    return path.join(targetItemPathname, draggedItemFilename);\n  } else if (dropType === 'adjacent') {\n    return path.join(targetItemDirname, draggedItemFilename);\n  }\n  return null;\n};\n\n// item sequence utils - END\n\nexport const getUniqueTagsFromItems = (items = []) => {\n  const allTags = new Set();\n  const getTags = (items) => {\n    items.forEach((item) => {\n      if (isItemARequest(item)) {\n        const tags = item.draft ? get(item, 'draft.tags', []) : get(item, 'tags', []);\n        tags.forEach((tag) => allTags.add(tag));\n      }\n      if (item.items) {\n        getTags(item.items);\n      }\n    });\n  };\n  getTags(items);\n  return Array.from(allTags).sort();\n};\n\nexport const getRequestItemsForCollectionRun = ({ recursive, items = [], tags }) => {\n  let requestItems = [];\n\n  if (recursive) {\n    requestItems = flattenItems(items);\n  } else {\n    each(items, (item) => {\n      if (item.request) {\n        requestItems.push(item);\n      }\n    });\n  }\n\n  const requestTypes = ['http-request', 'graphql-request'];\n  requestItems = requestItems.filter((request) => requestTypes.includes(request.type) && !request.isTransient);\n\n  if (tags && tags.include && tags.exclude) {\n    const includeTags = tags.include ? tags.include : [];\n    const excludeTags = tags.exclude ? tags.exclude : [];\n    requestItems = requestItems.filter(({ tags: requestTags = [], draft }) => {\n      requestTags = draft?.tags || requestTags || [];\n      return isRequestTagsIncluded(requestTags, includeTags, excludeTags);\n    });\n  }\n\n  return requestItems;\n};\n\nexport const getPropertyFromDraftOrRequest = (item, propertyKey, defaultValue = null) => {\n  return item.draft ? get(item, `draft.${propertyKey}`, defaultValue) : get(item, propertyKey, defaultValue);\n};\n\nexport const transformExampleToDraft = (example, newExample) => {\n  const exampleToDraft = cloneDeep(example);\n\n  if (newExample.name) {\n    exampleToDraft.name = newExample.name;\n  }\n  if (newExample.description) {\n    exampleToDraft.description = newExample.description;\n  }\n  if (newExample.status) {\n    exampleToDraft.response.status = Number(newExample.status);\n  }\n  if (newExample.statusText) {\n    exampleToDraft.response.statusText = newExample.statusText;\n  }\n  if (newExample.headers && newExample.headers.length) {\n    exampleToDraft.response.headers = newExample.headers.map((header) => ({\n      uid: uuid(),\n      name: String(header.name),\n      value: String(header.value),\n      description: String(header.description),\n      enabled: header.enabled\n    }));\n  }\n  if (newExample.body) {\n    exampleToDraft.response.body = newExample.body;\n  }\n\n  return exampleToDraft;\n};\n\n/**\n * Generate an initial name for a new response example\n * @param {Object} item - The request item that will contain the example\n * @returns {string} - The suggested name for the new example\n */\nexport const getInitialExampleName = (item) => {\n  const baseName = 'example';\n  const existingExamples = item.draft?.examples || item.examples || [];\n  const existingNames = new Set(existingExamples.map((example) => example.name || '').filter(Boolean));\n\n  if (!existingNames.has(baseName)) {\n    return baseName;\n  }\n\n  let counter = 1;\n  while (true) {\n    const candidateName = `${baseName} (${counter})`;\n    if (!existingNames.has(candidateName)) {\n      return candidateName;\n    }\n    counter++;\n  }\n};\n\n// Get the scope and raw value of a variable by checking all scopes in priority order\nexport const getVariableScope = (variableName, collection, item) => {\n  if (!variableName || !collection) {\n    return null;\n  }\n\n  // 1. Check Request Variables (highest priority)\n  if (item) {\n    const requestVars = item.draft ? get(item, 'draft.request.vars.req', []) : get(item, 'request.vars.req', []);\n    const requestVar = requestVars.find((v) => v.name === variableName && v.enabled);\n    if (requestVar) {\n      return {\n        type: 'request',\n        value: requestVar.value,\n        data: { item, variable: requestVar }\n      };\n    }\n  }\n\n  // 2. Check Folder Variables\n  const requestTreePath = getTreePathFromCollectionToItem(collection, item);\n  for (let i = requestTreePath.length - 1; i >= 0; i--) {\n    const pathItem = requestTreePath[i];\n    if (!pathItem) {\n      continue;\n    }\n\n    if (pathItem.type === 'folder') {\n      // Check draft first, then fall back to root\n      const folderRoot = pathItem.draft || pathItem.root;\n      const folderVars = get(folderRoot, 'request.vars.req', []);\n      const folderVar = folderVars.find((v) => v.name === variableName && v.enabled);\n      if (folderVar) {\n        return {\n          type: 'folder',\n          value: folderVar.value,\n          data: { folder: pathItem, variable: folderVar }\n        };\n      }\n    }\n  }\n\n  // 3. Check Environment Variables\n  if (collection.activeEnvironmentUid) {\n    const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);\n    if (environment && environment.variables) {\n      const envVar = environment.variables.find((v) => v.name === variableName && v.enabled);\n      if (envVar) {\n        return {\n          type: 'environment',\n          value: envVar.value,\n          data: { environment, variable: envVar }\n        };\n      }\n    }\n  }\n\n  // 4. Check Collection Variables\n  // Check draft first, then fall back to root\n  const collectionRoot = (collection.draft && collection.draft.root) || collection.root || {};\n  const collectionVars = get(collectionRoot, 'request.vars.req', []);\n  const collectionVar = collectionVars.find((v) => v.name === variableName && v.enabled);\n  if (collectionVar) {\n    return {\n      type: 'collection',\n      value: collectionVar.value,\n      data: { collection, variable: collectionVar }\n    };\n  }\n\n  // 5. Check Global Environment Variables\n  const { globalEnvironmentVariables = {} } = collection;\n  if (globalEnvironmentVariables && globalEnvironmentVariables[variableName]) {\n    return {\n      type: 'global',\n      value: globalEnvironmentVariables[variableName],\n      data: { variableName, value: globalEnvironmentVariables[variableName] }\n    };\n  }\n\n  // 6. Check Runtime Variables (set during request execution via scripts)\n  const { runtimeVariables = {} } = collection;\n  if (runtimeVariables && runtimeVariables[variableName]) {\n    return {\n      type: 'runtime',\n      value: runtimeVariables[variableName],\n      data: { variableName, value: runtimeVariables[variableName], readonly: true }\n    };\n  }\n\n  // Process.env variables are not checked here\n\n  return null;\n};\n\n// Check if a variable is marked as secret\nexport const isVariableSecret = (scopeInfo) => {\n  if (!scopeInfo) {\n    return false;\n  }\n\n  // Only environment variables can be marked as secret\n  if (scopeInfo.type === 'environment') {\n    return !!scopeInfo.data.variable?.secret;\n  }\n\n  // Global variables are not checked here\n  if (scopeInfo.type === 'global') {\n    return false;\n  }\n\n  return false;\n};\n\n/**\n * Generate a unique request name by checking existing filenames in the collection and filesystem\n * @param {Object} collection - The collection object\n * @param {string} baseName - The base name (default: 'Untitled')\n * @param {string} itemUid - The parent item UID (null for root level, folder UID for folder level)\n * @returns {Promise<string>} - A unique request name (Untitled, Untitled1, Untitled2, etc.)\n */\nexport const generateUniqueRequestName = async (collection, baseName = 'Untitled', itemUid = null) => {\n  if (!collection) {\n    return baseName;\n  }\n\n  const trim = require('lodash/trim');\n  const parentItem = itemUid ? findItemInCollection(collection, itemUid) : null;\n  const parentItems = parentItem ? (parentItem.items || []) : (collection.items || []);\n  const baseNamePattern = new RegExp(`^${baseName}(\\\\d+)?$`);\n  // Support .bru, .yml, and .yaml file extensions\n  const requestExtensions = /\\.(bru|yml|yaml)$/i;\n  const matchingItems = parentItems\n    .filter((item) => {\n      if (item.type === 'folder') return false;\n\n      const filename = trim(item.filename);\n      if (!requestExtensions.test(filename)) return false;\n\n      const filenameWithoutExt = filename.replace(requestExtensions, '');\n      return baseNamePattern.test(filenameWithoutExt);\n    })\n    .map((item) => {\n      const filenameWithoutExt = trim(item.filename).replace(requestExtensions, '');\n      const match = filenameWithoutExt.match(baseNamePattern);\n\n      if (!match) return null;\n\n      const number = match[1] ? parseInt(match[1], 10) : 0;\n      return { name: filenameWithoutExt, number: isNaN(number) ? null : number };\n    })\n    .filter((item) => item !== null && item.number !== null);\n\n  if (matchingItems.length === 0) {\n    return baseName;\n  }\n\n  const sortedMatches = matchingItems.sort((a, b) => a.number - b.number);\n  const lastElement = sortedMatches[sortedMatches.length - 1];\n  const nextNumber = lastElement.number + 1;\n\n  return `${baseName}${nextNumber}`;\n};\n\nexport const isItemTransientRequest = (item) => {\n  return isItemARequest(item) && item?.isTransient;\n};\n\n/**\n * Recursively filter out transient items from a collection's items array.\n * Used for collection runner, exports, and other operations that shouldn't include transient requests.\n * @param {Array} items - The items array to filter\n * @returns {Array} A new array with transient items removed\n */\nexport const filterTransientItems = (items) => {\n  if (!items || !Array.isArray(items)) {\n    return [];\n  }\n\n  return items\n    .filter((item) => !item?.isTransient)\n    .map((item) => {\n      if (item.items && item.items.length > 0) {\n        return {\n          ...item,\n          items: filterTransientItems(item.items)\n        };\n      }\n      return item;\n    });\n};\n\n/**\n * Checks if a collection is a scratch collection for any workspace\n * @param {Object} collection - The collection to check\n * @param {Array} workspaces - Array of workspace objects\n * @returns {boolean} True if the collection is a scratch collection\n */\nexport const isScratchCollection = (collection, workspaces) => {\n  if (!collection || !workspaces) return false;\n  return workspaces.some((w) => w.scratchCollectionUid === collection.uid);\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/collections/index.spec.js",
    "content": "const { describe, it, expect } = require('@jest/globals');\nimport { mergeHeaders } from './index';\n\ndescribe('mergeHeaders', () => {\n  it('should include headers from collection, folder and request (with correct precedence)', () => {\n    const collection = {\n      root: {\n        request: {\n          headers: [\n            { name: 'X-Collection', value: 'c', enabled: true }\n          ]\n        }\n      }\n    };\n\n    const folder = {\n      type: 'folder',\n      root: {\n        request: {\n          headers: [\n            { name: 'X-Folder', value: 'f', enabled: true }\n          ]\n        }\n      }\n    };\n\n    const request = {\n      headers: [\n        { name: 'X-Request', value: 'r', enabled: true }\n      ]\n    };\n\n    const headers = mergeHeaders(collection, request, [folder]);\n    const names = headers.map((h) => h.name);\n    expect(names).toEqual(expect.arrayContaining(['X-Collection', 'X-Folder', 'X-Request']));\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/utils/collections/search.js",
    "content": "import { flattenItems, isItemARequest } from './index';\nimport filter from 'lodash/filter';\nimport find from 'lodash/find';\n\nexport const doesRequestMatchSearchText = (request, searchText = '') => {\n  return request?.name?.toLowerCase().includes(searchText.toLowerCase());\n};\n\nexport const doesFolderHaveItemsMatchSearchText = (item, searchText = '') => {\n  let flattenedItems = flattenItems(item.items);\n  let requestItems = filter(flattenedItems, (item) => isItemARequest(item) && !item.isTransient);\n\n  return find(requestItems, (request) => doesRequestMatchSearchText(request, searchText));\n};\n\nexport const doesCollectionHaveItemsMatchingSearchText = (collection, searchText = '') => {\n  let flattenedItems = flattenItems(collection.items);\n  let requestItems = filter(flattenedItems, (item) => isItemARequest(item) && !item.isTransient);\n\n  return find(requestItems, (request) => doesRequestMatchSearchText(request, searchText));\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/common/bulkKeyValueUtils.js",
    "content": "export function parseBulkKeyValue(value) {\n  return value\n    .split(/\\r?\\n/)\n    .map((pair) => {\n      const isEnabled = !pair.trim().startsWith('//');\n      const cleanPair = pair.replace(/^\\/\\/\\s*/, '');\n      const sep = cleanPair.indexOf(':');\n      if (sep < 0) return null;\n      return {\n        name: cleanPair.slice(0, sep).trim(),\n        value: cleanPair.slice(sep + 1).trim(),\n        enabled: isEnabled\n      };\n    })\n    .filter(Boolean);\n}\n\nexport function serializeBulkKeyValue(items) {\n  return items.map((item) => `${item.enabled ? '' : '//'}${item.name}:${item.value}`).join('\\n');\n}\n"
  },
  {
    "path": "packages/bruno-app/src/utils/common/codemirror.js",
    "content": "import get from 'lodash/get';\nimport { mockDataFunctions } from '@usebruno/common';\nimport { PROMPT_VARIABLE_TEXT_PATTERN } from '@usebruno/common/utils';\n\nconst CodeMirror = require('codemirror');\n\nconst pathFoundInVariables = (path, obj) => {\n  const value = get(obj, path);\n  return value !== undefined;\n};\n\n/**\n * Defines a custom CodeMirror mode for Bruno variables highlighting.\n * This function creates a specialized mode that can highlight both Bruno template\n * variables (in the format {{variable}}) and URL path parameters (in the format /:param).\n *\n * @param {Object} _variables - The variables object containing data to validate against\n * @param {string} mode - The base CodeMirror mode to extend (e.g., 'javascript', 'application/json')\n * @param {boolean} highlightPathParams - Whether to highlight URL path parameters\n * @param {boolean} highlightVariables - Whether to highlight template variables\n * @returns {void} - Registers the mode with CodeMirror for later use\n */\nexport const defineCodeMirrorBrunoVariablesMode = (_variables, mode, highlightPathParams, highlightVariables) => {\n  CodeMirror.defineMode('brunovariables', function (config, parserConfig) {\n    const { pathParams = {}, ...variables } = _variables || {};\n    const variablesOverlay = {\n      token: function (stream) {\n        if (stream.match('{{', true)) {\n          let ch;\n          let word = '';\n          while ((ch = stream.next()) != null) {\n            if (ch === '}' && stream.peek() === '}') {\n              stream.eat('}');\n\n              // Prompt variable: starts with '?', no leading/trailing spaces, no braces\n              if (PROMPT_VARIABLE_TEXT_PATTERN.test(word)) {\n                return `variable-prompt`;\n              }\n\n              // Check if it's a mock variable (starts with $) and exists in mockDataFunctions\n              const isMockVariable = word.startsWith('$') && mockDataFunctions.hasOwnProperty(word.substring(1));\n              const found = isMockVariable || pathFoundInVariables(word, variables);\n              const status = found ? 'valid' : 'invalid';\n              const randomClass = `random-${(Math.random() + 1).toString(36).substring(9)}`;\n              return `variable-${status} ${randomClass}`;\n            }\n            word += ch;\n          }\n        }\n        stream.skipTo('{{') || stream.skipToEnd();\n        return null;\n      }\n    };\n\n    const urlPathParamsOverlay = {\n      token: function (stream) {\n        if (stream.match('/:', true)) {\n          let ch;\n          let word = '';\n          while ((ch = stream.next()) != null) {\n            if (ch === '/' || ch === '?' || ch === '&' || ch === '=') {\n              stream.backUp(1);\n              const found = pathFoundInVariables(word, pathParams);\n              const status = found ? 'valid' : 'invalid';\n              const randomClass = `random-${(Math.random() + 1).toString(36).substring(9)}`;\n              return `variable-${status} ${randomClass}`;\n            }\n            word += ch;\n          }\n\n          // If we've consumed all characters and the word is not empty, it might be a path parameter at the end of the URL.\n          if (word) {\n            const found = pathFoundInVariables(word, pathParams);\n            const status = found ? 'valid' : 'invalid';\n            const randomClass = `random-${(Math.random() + 1).toString(36).substring(9)}`;\n            return `variable-${status} ${randomClass}`;\n          }\n        }\n        stream.skipTo('/:') || stream.skipToEnd();\n        return null;\n      }\n    };\n\n    let baseMode = CodeMirror.getMode(config, parserConfig.backdrop || mode);\n\n    if (highlightVariables) {\n      baseMode = CodeMirror.overlayMode(baseMode, variablesOverlay);\n    }\n    if (highlightPathParams) {\n      baseMode = CodeMirror.overlayMode(baseMode, urlPathParamsOverlay);\n    }\n    return baseMode;\n  });\n};\n\nexport const getCodeMirrorModeBasedOnContentType = (contentType, body) => {\n  if (typeof body === 'object') {\n    return 'application/ld+json';\n  }\n  if (!contentType || typeof contentType !== 'string') {\n    return 'application/text';\n  }\n\n  if (contentType.includes('json')) {\n    return 'application/ld+json';\n  } else if (contentType.includes('javascript') || contentType.includes('ecmascript')) {\n    return 'application/javascript';\n  } else if (contentType.includes('image')) {\n    return 'application/image';\n  } else if (contentType.includes('xml')) {\n    return 'application/xml';\n  } else if (contentType.includes('html')) {\n    return 'application/html';\n  } else if (contentType.includes('text')) {\n    return 'application/text';\n  } else if (contentType.includes('application/edn')) {\n    return 'application/xml';\n  } else if (contentType.includes('yaml')) {\n    return 'application/yaml';\n  } else {\n    return 'application/text';\n  }\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/common/constants.js",
    "content": "export const REQUEST_TYPES = ['http-request', 'graphql-request', 'grpc-request', 'ws-request'];\n\nexport const DEFAULT_COLLECTION_FORMAT = 'yml';\n"
  },
  {
    "path": "packages/bruno-app/src/utils/common/error.js",
    "content": "import toast from 'react-hot-toast';\n\n// levels: 'warning, error'\nexport class BrunoError extends Error {\n  constructor(message, level) {\n    super(message);\n    this.name = 'BrunoError';\n    this.level = level || 'error';\n  }\n}\n\nexport const parseError = (error, defaultErrorMsg = 'An error occurred') => {\n  if (error instanceof BrunoError) {\n    return error.message;\n  }\n\n  return error.message ? error.message : defaultErrorMsg;\n};\n\nexport const toastError = (error, defaultErrorMsg = 'An error occurred') => {\n  let errorMsg = parseError(error, defaultErrorMsg);\n\n  if (error instanceof BrunoError) {\n    if (error.level === 'warning') {\n      return toast(errorMsg, {\n        icon: '⚠️',\n        duration: 3000\n      });\n    }\n    return toast.error(errorMsg, {\n      duration: 3000\n    });\n  }\n\n  return toast.error(errorMsg);\n};\n\nexport function formatIpcError(error) {\n  if (!(error instanceof Error)) return error;\n  if (!error?.message) return ''; // Avoid returning `null` or `undefined`\n  // https://github.com/electron/electron/blob/659e79fc08c6ffc2f7506dd1358918d97d240147/lib/renderer/api/ipc-renderer.ts#L24-L30\n  // There is no other way to get rid of this error prefix as of now.\n  return error.message.replace(/^Error invoking remote method '.+?': (Error: )?/, '');\n}\n"
  },
  {
    "path": "packages/bruno-app/src/utils/common/folders-requests-sorting.spec.js",
    "content": "const { describe, it, expect } = require('@jest/globals');\nconst { sortByNameThenSequence } = require('./index');\n\ndescribe('sortByNameThenSequence', () => {\n  describe('Basic functionality', () => {\n    it('should return an empty array when given an empty array', () => {\n      const items = [];\n      const result = sortByNameThenSequence(items);\n      expect(result).toEqual([]);\n    });\n\n    it('should not mutate the original array', () => {\n      const items = [\n        { name: 'folder_2', seq: 2 },\n        { name: 'folder_1', seq: 1 }\n      ];\n      const originalItems = JSON.parse(JSON.stringify(items));\n      sortByNameThenSequence(items);\n      expect(items).toEqual(originalItems);\n    });\n\n    it('should return a new array instance', () => {\n      const items = [{ name: 'folder_1' }];\n      const result = sortByNameThenSequence(items);\n      expect(result).not.toBe(items);\n    });\n  });\n\n  describe('Alphabetical sorting (no sequence numbers)', () => {\n    it('should sort items alphabetically by name when no sequence numbers are present', () => {\n      const items = [\n        { name: 'folder_3' },\n        { name: 'folder_1' },\n        { name: 'folder_2' }\n      ];\n      const result = sortByNameThenSequence(items);\n      expect(result).toEqual([\n        { name: 'folder_1' },\n        { name: 'folder_2' },\n        { name: 'folder_3' }\n      ]);\n    });\n\n    it('should handle case-sensitive sorting correctly', () => {\n      const items = [\n        { name: 'Folder_2' },\n        { name: 'folder_1' },\n        { name: 'FOLDER_3' }\n      ];\n      const result = sortByNameThenSequence(items);\n      expect(result).toEqual([\n        { name: 'folder_1' },\n        { name: 'Folder_2' },\n        { name: 'FOLDER_3' }\n      ]);\n    });\n\n    it('should handle special characters in names', () => {\n      const items = [\n        { name: 'folder-2' },\n        { name: 'folder_1' },\n        { name: 'folder 3' }\n      ];\n      const result = sortByNameThenSequence(items);\n      expect(result).toEqual([\n        { name: 'folder 3' },\n        { name: 'folder_1' },\n        { name: 'folder-2' }\n      ]);\n    });\n  });\n\n  describe('Sequence-based sorting (valid sequence numbers)', () => {\n    it('should sort items by sequence when all items have valid sequence numbers', () => {\n      const items = [\n        { name: 'folder_3', seq: 3 },\n        { name: 'folder_1', seq: 1 },\n        { name: 'folder_2', seq: 2 }\n      ];\n      const result = sortByNameThenSequence(items);\n      expect(result).toEqual([\n        { name: 'folder_1', seq: 1 },\n        { name: 'folder_2', seq: 2 },\n        { name: 'folder_3', seq: 3 }\n      ]);\n    });\n\n    it('should handle duplicate sequence numbers by inserting them in alphabetical order', () => {\n      const items = [\n        { name: 'folder_3', seq: 1 },\n        { name: 'folder_1', seq: 1 },\n        { name: 'folder_2', seq: 2 }\n      ];\n      const result = sortByNameThenSequence(items);\n      expect(result).toEqual([\n        { name: 'folder_1', seq: 1 },\n        { name: 'folder_3', seq: 1 },\n        { name: 'folder_2', seq: 2 }\n      ]);\n    });\n\n    it('should handle large sequence numbers correctly', () => {\n      const items = [\n        { name: 'folder_1', seq: 100 },\n        { name: 'folder_2', seq: 1 },\n        { name: 'folder_3', seq: 50 }\n      ];\n      const result = sortByNameThenSequence(items);\n      expect(result).toEqual([\n        { name: 'folder_2', seq: 1 },\n        { name: 'folder_3', seq: 50 },\n        { name: 'folder_1', seq: 100 }\n      ]);\n    });\n  });\n\n  describe('Invalid sequence numbers', () => {\n    it('should treat undefined sequence as invalid and sort alphabetically', () => {\n      const items = [\n        { name: 'folder_3', seq: undefined },\n        { name: 'folder_1', seq: undefined },\n        { name: 'folder_2', seq: undefined }\n      ];\n      const result = sortByNameThenSequence(items);\n      expect(result).toEqual([\n        { name: 'folder_1', seq: undefined },\n        { name: 'folder_2', seq: undefined },\n        { name: 'folder_3', seq: undefined }\n      ]);\n    });\n\n    it('should treat null sequence as invalid and sort alphabetically', () => {\n      const items = [\n        { name: 'folder_3', seq: null },\n        { name: 'folder_1', seq: null },\n        { name: 'folder_2', seq: null }\n      ];\n      const result = sortByNameThenSequence(items);\n      expect(result).toEqual([\n        { name: 'folder_1', seq: null },\n        { name: 'folder_2', seq: null },\n        { name: 'folder_3', seq: null }\n      ]);\n    });\n\n    it('should treat boolean values as invalid sequence numbers', () => {\n      const items = [\n        { name: 'folder_3', seq: true },\n        { name: 'folder_1', seq: false },\n        { name: 'folder_2', seq: true }\n      ];\n      const result = sortByNameThenSequence(items);\n      expect(result).toEqual([\n        { name: 'folder_1', seq: false },\n        { name: 'folder_2', seq: true },\n        { name: 'folder_3', seq: true }\n      ]);\n    });\n\n    it('should treat string values as invalid sequence numbers', () => {\n      const items = [\n        { name: 'folder_3', seq: '3' },\n        { name: 'folder_1', seq: '1' },\n        { name: 'folder_2', seq: 'invalid' }\n      ];\n      const result = sortByNameThenSequence(items);\n      expect(result).toEqual([\n        { name: 'folder_1', seq: '1' },\n        { name: 'folder_2', seq: 'invalid' },\n        { name: 'folder_3', seq: '3' }\n      ]);\n    });\n\n    it('should treat non-integer numbers as invalid sequence numbers', () => {\n      const items = [\n        { name: 'folder_3', seq: 3.5 },\n        { name: 'folder_1', seq: 1.2 },\n        { name: 'folder_2', seq: 2.0 }\n      ];\n      const result = sortByNameThenSequence(items);\n      expect(result).toEqual([\n        { name: 'folder_1', seq: 1.2 },\n        { name: 'folder_2', seq: 2.0 },\n        { name: 'folder_3', seq: 3.5 }\n      ]);\n    });\n\n    it('should treat zero and negative numbers as invalid sequence numbers', () => {\n      const items = [\n        { name: 'folder_3', seq: 0 },\n        { name: 'folder_1', seq: -1 },\n        { name: 'folder_2', seq: -5 }\n      ];\n      const result = sortByNameThenSequence(items);\n      expect(result).toEqual([\n        { name: 'folder_1', seq: -1 },\n        { name: 'folder_2', seq: -5 },\n        { name: 'folder_3', seq: 0 }\n      ]);\n    });\n\n    it('should treat NaN and Infinity as invalid sequence numbers', () => {\n      const items = [\n        { name: 'folder_3', seq: NaN },\n        { name: 'folder_1', seq: Infinity },\n        { name: 'folder_2', seq: -Infinity }\n      ];\n      const result = sortByNameThenSequence(items);\n      expect(result).toEqual([\n        { name: 'folder_1', seq: Infinity },\n        { name: 'folder_2', seq: -Infinity },\n        { name: 'folder_3', seq: NaN }\n      ]);\n    });\n\n    it('should handle invalid sequence numbers correctly', () => {\n      const items = [\n        { name: 'folder_4', seq: undefined },\n        { name: 'folder_1', seq: false },\n        { name: 'folder_5', seq: 'invalid' },\n        { name: 'folder_2', seq: true },\n        { name: 'folder_3', seq: null }\n      ];\n      const sorted = sortByNameThenSequence(items);\n      expect(sorted).toEqual([\n        { name: 'folder_1', seq: false },\n        { name: 'folder_2', seq: true },\n        { name: 'folder_3', seq: null },\n        { name: 'folder_4', seq: undefined },\n        { name: 'folder_5', seq: 'invalid' }\n      ]);\n    });\n  });\n\n  describe('Mixed valid and invalid sequence numbers', () => {\n    it('should handle mixed valid and invalid sequence numbers correctly', () => {\n      const items = [\n        { name: 'folder_4', seq: undefined },\n        { name: 'folder_1', seq: false },\n        { name: 'folder_5', seq: 3 },\n        { name: 'folder_2', seq: 2 },\n        { name: 'folder_3', seq: null },\n        { name: 'folder_6', seq: 9 },\n        { name: 'folder_8', seq: 'invalid' },\n        { name: 'folder_7', seq: 4 }\n      ];\n      const sorted = sortByNameThenSequence(items);\n      expect(sorted).toEqual([\n        { name: 'folder_1', seq: false },\n        { name: 'folder_2', seq: 2 },\n        { name: 'folder_5', seq: 3 },\n        { name: 'folder_7', seq: 4 },\n        { name: 'folder_3', seq: null },\n        { name: 'folder_4', seq: undefined },\n        { name: 'folder_8', seq: 'invalid' },\n        { name: 'folder_6', seq: 9 }\n      ]);\n    });\n\n    it('should insert sequenced items at their positions among non-sequenced items', () => {\n      const items = [\n        { name: 'folder_6' },\n        { name: 'folder_1', seq: 1 },\n        { name: 'folder_5' },\n        { name: 'folder_2', seq: 2 },\n        { name: 'folder_4' },\n        { name: 'folder_3', seq: 4 }\n      ];\n      const result = sortByNameThenSequence(items);\n      expect(result).toEqual([\n        { name: 'folder_1', seq: 1 },\n        { name: 'folder_2', seq: 2 },\n        { name: 'folder_4' },\n        { name: 'folder_3', seq: 4 },\n        { name: 'folder_5' },\n        { name: 'folder_6' }\n      ]);\n    });\n\n    it('should handle sequence numbers beyond the array length', () => {\n      const items = [\n        { name: 'folder_1', seq: 10 },\n        { name: 'folder_2' },\n        { name: 'folder_3', seq: 20 }\n      ];\n      const result = sortByNameThenSequence(items);\n      expect(result).toEqual([\n        { name: 'folder_2' },\n        { name: 'folder_1', seq: 10 },\n        { name: 'folder_3', seq: 20 }\n      ]);\n    });\n  });\n\n  describe('Edge cases and boundary conditions', () => {\n    it('should handle items with missing name property without throwing errors', () => {\n      const items = [\n        { seq: 1 },\n        { name: 'folder_1' },\n        { name: 'folder_2', seq: 2 }\n      ];\n      // Note: This might cause issues in production, but we test the current behavior\n      expect(() => sortByNameThenSequence(items)).not.toThrow();\n    });\n\n    it('should handle items with no seq property (equivalent to undefined)', () => {\n      const items = [\n        { name: 'folder_3' },\n        { name: 'folder_1', seq: 1 },\n        { name: 'folder_2' }\n      ];\n      const result = sortByNameThenSequence(items);\n      expect(result).toEqual([\n        { name: 'folder_1', seq: 1 },\n        { name: 'folder_2' },\n        { name: 'folder_3' }\n      ]);\n    });\n\n    it('should handle single item arrays', () => {\n      const items = [{ name: 'folder_1', seq: 1 }];\n      const result = sortByNameThenSequence(items);\n      expect(result).toEqual([{ name: 'folder_1', seq: 1 }]);\n    });\n\n    it('should handle items with identical names but different sequences', () => {\n      const items = [\n        { name: 'folder', seq: 2 },\n        { name: 'folder', seq: 1 },\n        { name: 'folder' }\n      ];\n      const result = sortByNameThenSequence(items);\n      expect(result).toEqual([\n        { name: 'folder', seq: 1 },\n        { name: 'folder', seq: 2 },\n        { name: 'folder' }\n      ]);\n    });\n  });\n\n  describe('Complex scenarios', () => {\n    it('should handle a comprehensive mix of all scenarios', () => {\n      const items = [\n        { name: 'folder_10', seq: 'invalid' },\n        { name: 'folder_1', seq: false },\n        { name: 'folder_11', seq: 3 },\n        { name: 'folder_2', seq: 2 },\n        { name: 'folder_3', seq: null },\n        { name: 'folder_12', seq: 9 },\n        { name: 'folder_4', seq: undefined },\n        { name: 'folder_5' },\n        { name: 'folder_6', seq: 0 },\n        { name: 'folder_7', seq: 4 },\n        { name: 'folder_8', seq: 1 },\n        { name: 'folder_9', seq: -1 }\n      ];\n      const result = sortByNameThenSequence(items);\n      expect(result).toEqual([\n        { name: 'folder_8', seq: 1 },\n        { name: 'folder_2', seq: 2 },\n        { name: 'folder_11', seq: 3 },\n        { name: 'folder_7', seq: 4 },\n        { name: 'folder_1', seq: false },\n        { name: 'folder_10', seq: 'invalid' },\n        { name: 'folder_3', seq: null },\n        { name: 'folder_4', seq: undefined },\n        { name: 'folder_12', seq: 9 },\n        { name: 'folder_5' },\n        { name: 'folder_6', seq: 0 },\n        { name: 'folder_9', seq: -1 }\n      ]);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/utils/common/format-response.spec.js",
    "content": "import { formatResponse } from './index';\n\ndescribe('formatResponse', () => {\n  const createBase64Buffer = (content) => Buffer.from(content).toString('base64');\n  const createLargeBase64Buffer = (data) => {\n    // Create buffer from the actual data without modification\n    const content = typeof data === 'string' ? data : JSON.stringify(data);\n    return Buffer.from(content).toString('base64');\n  };\n\n  describe('invalid inputs', () => {\n    it('should return empty string for invalid inputs', () => {\n      const invalidCases = [\n        [undefined, 'dGVzdA==', 'json'],\n        [{ test: 'data' }, null, 'json'],\n        [{ test: 'data' }, 'dGVzdA==', null],\n        [undefined, undefined, undefined]\n      ];\n\n      invalidCases.forEach(([data, buffer, mode]) => {\n        const result = formatResponse(data, buffer, mode);\n        expect(result).toBe('');\n        expect(typeof result).toBe('string');\n      });\n    });\n  });\n\n  describe('JSON mode', () => {\n    it('should format JSON data with JSONPath filter', () => {\n      const data = { users: [{ name: 'John' }, { name: 'Jane' }] };\n      const dataBuffer = createBase64Buffer(JSON.stringify(data));\n      const result = formatResponse(data, dataBuffer, 'application/json', '$.users[0].name');\n\n      expect(result).toBe('[\\n  \"John\"\\n]');\n      expect(typeof result).toBe('string');\n    });\n\n    it('should format normal sized JSON responses', () => {\n      const data = { name: 'John', age: 30 };\n      const dataBuffer = createBase64Buffer(JSON.stringify(data));\n      const result = formatResponse(data, dataBuffer, 'application/json');\n\n      expect(result).toBe('{\\n  \"name\": \"John\",\\n  \"age\": 30\\n}');\n      expect(typeof result).toBe('string');\n    });\n\n    it('should format normal sized JSON responses when data is already a JSON string', () => {\n      const data = '{\"name\":\"John\",\"age\":30}';\n      const dataBuffer = createBase64Buffer(data); // Use data directly, not JSON.stringify(data)\n      const result = formatResponse(data, dataBuffer, 'application/json');\n\n      expect(result).toBe('{\\n  \"name\": \"John\",\\n  \"age\": 30\\n}');\n      expect(typeof result).toBe('string');\n    });\n\n    it('should preserve bigint value after JSON format', () => {\n      const data = '{ \"data\": 1736184243098437392 }';\n      const dataBuffer = createBase64Buffer(data);\n      const result = formatResponse(data, dataBuffer, 'application/json');\n\n      expect(result).toBe('{\\n  \"data\": 1736184243098437392\\n}');\n      expect(typeof result).toBe('string');\n    });\n\n    it('should format large JSON responses without indentation', () => {\n      // This test uses a custom threshold of 100 bytes to trigger large buffer behavior\n      const data = {\n        test: 'value',\n        description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua',\n        content: 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat'\n      };\n      const buffer = createLargeBase64Buffer(data);\n      const result = formatResponse(data, buffer, 'application/json', undefined, 100);\n\n      // Since the data exceeds the 100 byte threshold, it should return unformatted JSON\n      expect(result).toBe('{\"test\":\"value\",\"description\":\"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua\",\"content\":\"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat\"}');\n      expect(typeof result).toBe('string');\n    });\n  });\n\n  describe('XML mode', () => {\n    it('should format normal sized XML responses', () => {\n      const xmlData = '<root><item>value</item></root>';\n      const dataBuffer = createBase64Buffer(xmlData);\n      const result = formatResponse(xmlData, dataBuffer, 'application/xml');\n\n      expect(typeof result).toBe('string');\n      expect(result).toContain('root');\n      expect(result).toContain('item');\n    });\n\n    it('should handle large XML responses', () => {\n      const xmlData = '<root><item>value</item><description>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore</description><content>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo</content></root>';\n      const largeBuffer = createLargeBase64Buffer(xmlData);\n      const result = formatResponse(xmlData, largeBuffer, 'application/xml', undefined, 100);\n\n      expect(typeof result).toBe('string');\n      expect(result).toContain('Lorem ipsum');\n    });\n  });\n\n  describe('other modes', () => {\n    it('should handle string data for non-JSON/XML modes', () => {\n      const data = 'plain text content';\n      const dataBuffer = createBase64Buffer(data);\n      const result = formatResponse(data, dataBuffer, 'text/plain');\n\n      expect(result).toBe('plain text content');\n      expect(typeof result).toBe('string');\n    });\n\n    it('should handle large object data for other modes', () => {\n      const data = {\n        message: 'hello',\n        description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua',\n        content: 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat'\n      };\n      const largeBuffer = createLargeBase64Buffer(data);\n      const result = formatResponse(data, largeBuffer, 'text/plain', undefined, 100);\n\n      expect(typeof result).toBe('string');\n      expect(result).toContain('Lorem ipsum');\n    });\n  });\n\n  describe('data type handling', () => {\n    it('should handle different data types and always return string', () => {\n      const testCases = [\n        [123, createBase64Buffer('123'), 'application/json'],\n        [true, createBase64Buffer('true'), 'application/json'],\n        [null, createBase64Buffer('null'), 'application/json'],\n        [[], createBase64Buffer('[]'), 'application/json']\n      ];\n\n      testCases.forEach(([data, buffer, mode]) => {\n        const result = formatResponse(data, buffer, mode);\n        expect(typeof result).toBe('string');\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/utils/common/index.js",
    "content": "import { customAlphabet } from 'nanoid';\nimport xmlFormat from 'xml-formatter';\nimport { JSONPath } from 'jsonpath-plus';\nimport fastJsonFormat from 'fast-json-format';\nimport { format, applyEdits } from 'jsonc-parser';\nimport { patternHasher } from '@usebruno/common/utils';\nimport prettierFormat from 'prettier/standalone';\nimport parserBabel from 'prettier/parser-babel';\n\nexport const isPlaywright = () => {\n  return typeof window !== 'undefined' && window.isPlaywright === true;\n};\n\n// a customized version of nanoid without using _ and -\nexport const uuid = () => {\n  // https://github.com/ai/nanoid/blob/main/url-alphabet/index.js\n  const urlAlphabet = 'useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict';\n  const customNanoId = customAlphabet(urlAlphabet, 21);\n\n  return customNanoId();\n};\n\nexport const simpleHash = (str) => {\n  let hash = 0;\n  for (let i = 0; i < str.length; i++) {\n    const char = str.charCodeAt(i);\n    hash = (hash << 5) - hash + char;\n    hash &= hash; // Convert to 32bit integer\n  }\n  return new Uint32Array([hash])[0].toString(36);\n};\n\nexport const waitForNextTick = () => {\n  return new Promise((resolve, reject) => {\n    setTimeout(() => resolve(), 0);\n  });\n};\n\nexport const safeParseJSON = (str) => {\n  if (!str || !str.length || typeof str !== 'string') {\n    return str;\n  }\n  try {\n    return JSON.parse(str);\n  } catch (e) {\n    return str;\n  }\n};\n\nexport const safeStringifyJSON = (obj, indent = false) => {\n  if (obj === undefined) {\n    return obj;\n  }\n  try {\n    if (indent) {\n      return JSON.stringify(obj, null, 2);\n    }\n    return JSON.stringify(obj);\n  } catch (e) {\n    return obj;\n  }\n};\n\nexport const safeParseXML = (str, options) => {\n  if (!str || !str.length || typeof str !== 'string') {\n    return str;\n  }\n  try {\n    return xmlFormat(str, options);\n  } catch (e) {\n    return str;\n  }\n};\n\n// Remove any characters that are not alphanumeric, spaces, hyphens, or underscores\nexport const normalizeFileName = (name) => {\n  if (!name) {\n    return name;\n  }\n\n  const validChars = /[^\\w\\s-]/g;\n  const formattedName = name.replace(validChars, '-');\n\n  return formattedName;\n};\n\nexport const getContentType = (headers) => {\n  // Return empty string for invalid headers\n  if (!headers || typeof headers !== 'object' || Object.keys(headers).length === 0) {\n    return '';\n  }\n\n  // Get content-type header value\n  const contentTypeHeader = Object.entries(headers)\n    .find(([key]) => key.toLowerCase() === 'content-type');\n\n  const contentType = contentTypeHeader && contentTypeHeader[1];\n\n  // Return empty string if no content-type or not a string\n  if (!contentType || typeof contentType !== 'string') {\n    return '';\n  }\n  // This pattern matches content types like application/json, application/ld+json, text/json, etc.\n  const JSON_PATTERN = /^[\\w\\-]+\\/([\\w\\-]+\\+)?json/;\n  // This pattern matches content types like image/svg.\n  const SVG_PATTERN = /^image\\/svg/i;\n  // This pattern matches content types like application/xml, text/xml, application/atom+xml, etc.\n  const XML_PATTERN = /^[\\w\\-]+\\/([\\w\\-]+\\+)?xml/;\n  // This pattern matches JavaScript content types: application/javascript, text/javascript, application/ecmascript, text/ecmascript\n  const JAVASCRIPT_PATTERN = /^(application|text)\\/(javascript|ecmascript)/i;\n\n  if (JSON_PATTERN.test(contentType)) {\n    return 'application/ld+json';\n  } else if (SVG_PATTERN.test(contentType)) {\n    return 'image/svg+xml';\n  } else if (XML_PATTERN.test(contentType)) {\n    return 'application/xml';\n  } else if (JAVASCRIPT_PATTERN.test(contentType)) {\n    return 'application/javascript';\n  }\n\n  return contentType;\n};\n\nexport const startsWith = (str, search) => {\n  if (!str || !str.length || typeof str !== 'string') {\n    return false;\n  }\n\n  if (!search || !search.length || typeof search !== 'string') {\n    return false;\n  }\n\n  return str.substr(0, search.length) === search;\n};\n\nexport const pluralizeWord = (word, count) => {\n  return count === 1 ? word : `${word}s`;\n};\n\nexport const relativeDate = (dateString) => {\n  const date = new Date(dateString);\n  const currentDate = new Date();\n\n  const difference = currentDate - date;\n  const secondsDifference = Math.floor(difference / 1000);\n  const minutesDifference = Math.floor(secondsDifference / 60);\n  const hoursDifference = Math.floor(minutesDifference / 60);\n  const daysDifference = Math.floor(hoursDifference / 24);\n  const weeksDifference = Math.floor(daysDifference / 7);\n  const monthsDifference = Math.floor(daysDifference / 30);\n\n  if (secondsDifference < 60) {\n    return 'Few seconds ago';\n  } else if (minutesDifference < 60) {\n    return `${minutesDifference} minute${minutesDifference > 1 ? 's' : ''} ago`;\n  } else if (hoursDifference < 24) {\n    return `${hoursDifference} hour${hoursDifference > 1 ? 's' : ''} ago`;\n  } else if (daysDifference < 7) {\n    return `${daysDifference} day${daysDifference > 1 ? 's' : ''} ago`;\n  } else if (weeksDifference < 4) {\n    return `${weeksDifference} week${weeksDifference > 1 ? 's' : ''} ago`;\n  } else {\n    return `${monthsDifference} month${monthsDifference > 1 ? 's' : ''} ago`;\n  }\n};\n\nexport const humanizeDate = (dateString) => {\n  // See this discussion for why .split is necessary\n  // https://stackoverflow.com/questions/7556591/is-the-javascript-date-object-always-one-day-off\n\n  if (!dateString || typeof dateString !== 'string') {\n    return 'Invalid Date';\n  }\n  const date = new Date(dateString);\n  if (isNaN(date.getTime())) {\n    return 'Invalid Date';\n  }\n\n  return date.toLocaleDateString('en-US', {\n    year: 'numeric',\n    month: 'long',\n    day: 'numeric'\n  });\n};\n\nexport const generateUidBasedOnHash = (str) => {\n  const hash = simpleHash(str);\n\n  return `${hash}`.padEnd(21, '0');\n};\n\nexport const stringifyIfNot = (v) => typeof v === 'string' ? v : String(v);\n\nexport const getEncoding = (headers) => {\n  // Parse the charset from content type: https://stackoverflow.com/a/33192813\n  const charsetMatch = /charset=([^()<>@,;:\"/[\\]?.=\\s]*)/i.exec(headers?.['content-type'] || '');\n  return charsetMatch?.[1];\n};\n\nexport const multiLineMsg = (...messages) => {\n  return messages.filter((m) => m !== undefined && m !== null && m !== '').join('\\n');\n};\n\nexport const formatSize = (bytes) => {\n  // Handle invalid inputs\n  if (isNaN(bytes) || typeof bytes !== 'number') {\n    return '0B';\n  }\n\n  if (bytes < 1024) {\n    return bytes + 'B';\n  }\n  if (bytes < 1024 * 1024) {\n    return (bytes / 1024).toFixed(1) + 'KB';\n  }\n  if (bytes < 1024 * 1024 * 1024) {\n    return (bytes / (1024 * 1024)).toFixed(1) + 'MB';\n  }\n\n  return (bytes / (1024 * 1024 * 1024)).toFixed(1) + 'GB';\n};\n\nexport const sortByNameThenSequence = (items) => {\n  const isSeqValid = (seq) => Number.isFinite(seq) && Number.isInteger(seq) && seq > 0;\n\n  // Sort folders alphabetically by name\n  const alphabeticallySorted = [...items].sort((a, b) => a.name && b.name && a.name.localeCompare(b.name));\n\n  // Extract folders without 'seq'\n  const withoutSeq = alphabeticallySorted.filter((f) => !isSeqValid(f['seq']));\n\n  // Extract folders with 'seq' and sort them by 'seq'\n  const withSeq = alphabeticallySorted.filter((f) => isSeqValid(f['seq'])).sort((a, b) => a.seq - b.seq);\n\n  const sortedItems = withoutSeq;\n\n  // Insert folders with 'seq' at their specified positions\n  withSeq.forEach((item) => {\n    const position = item.seq - 1;\n    const existingItem = withoutSeq[position];\n\n    // Check if there's already an item with the same sequence number\n    const hasItemWithSameSeq = Array.isArray(existingItem)\n      ? existingItem?.[0]?.seq === item.seq\n      : existingItem?.seq === item.seq;\n\n    if (hasItemWithSameSeq) {\n      // If there's a conflict, group items with same sequence together\n      const newGroup = Array.isArray(existingItem)\n        ? [...existingItem, item]\n        : [existingItem, item];\n\n      withoutSeq.splice(position, 1, newGroup);\n    } else {\n      // Insert item at the specified position\n      withoutSeq.splice(position, 0, item);\n    }\n  });\n\n  // return flattened sortedItems\n  return sortedItems.flat();\n};\n\n// Memory threshold to prevent crashes when decoding large buffers\nconst LARGE_BUFFER_THRESHOLD = 50 * 1024 * 1024; // 50 MB\n\nconst applyJSONPathFilter = (data, filter) => {\n  try {\n    return JSONPath({ path: filter, json: data });\n  } catch (e) {\n    console.warn('Could not apply JSONPath filter:', e.message);\n    return data;\n  }\n};\n\nexport const formatResponse = (data, dataBufferString, mode, filter, bufferThreshold = LARGE_BUFFER_THRESHOLD) => {\n  if (data === undefined || !dataBufferString || !mode) {\n    return '';\n  }\n\n  let bufferSize = 0, rawData = '', isVeryLargeResponse = false;\n  try {\n    const dataBuffer = Buffer.from(dataBufferString, 'base64');\n    bufferSize = dataBuffer.length;\n    isVeryLargeResponse = bufferSize > bufferThreshold;\n    if (!isVeryLargeResponse) {\n      rawData = dataBuffer.toString();\n    }\n  } catch (error) {\n    console.warn('Failed to calculate buffer size:', error);\n  }\n\n  if (mode.includes('json')) {\n    try {\n      if (filter) {\n        return safeStringifyJSON(applyJSONPathFilter(data, filter), true);\n      }\n    } catch (error) {}\n\n    if (isVeryLargeResponse) {\n      return safeStringifyJSON(data, false);\n    }\n\n    try {\n      return fastJsonFormat(rawData);\n    } catch (error) {}\n\n    if (typeof data === 'string') {\n      return data;\n    }\n    // Try to stringify the data, fallback to String conversion if needed\n    const stringified = safeStringifyJSON(data, false);\n    return typeof stringified === 'string' ? stringified : String(data);\n  }\n\n  if (mode.includes('xml')) {\n    if (isVeryLargeResponse) {\n      return typeof data === 'string' ? data : safeStringifyJSON(data, false);\n    }\n\n    let parsed = safeParseXML(data, { collapseContent: true });\n    if (typeof parsed === 'string') {\n      return parsed;\n    }\n    return safeStringifyJSON(parsed, true);\n  }\n\n  if (mode.includes('html')) {\n    if (isVeryLargeResponse) {\n      if (typeof data === 'string') {\n        return data;\n      }\n      if (data === null || data === undefined) {\n        return String(data);\n      }\n      if (typeof data === 'object') {\n        return safeStringifyJSON(data, false);\n      }\n      return String(data);\n    }\n\n    // Get HTML string from rawData\n    let htmlString = rawData;\n    // Prettify HTML\n    try {\n      return prettifyHtmlString(htmlString);\n    } catch (error) {\n      return htmlString;\n    }\n  }\n\n  if (mode.includes('javascript')) {\n    if (isVeryLargeResponse) {\n      if (typeof data === 'string') {\n        return data;\n      }\n      if (data === null || data === undefined) {\n        return String(data);\n      }\n      if (typeof data === 'object') {\n        return safeStringifyJSON(data, false);\n      }\n      return String(data);\n    }\n\n    // Get JavaScript string from rawData\n    let jsString = rawData;\n\n    // Prettify JavaScript\n    try {\n      return prettifyJavaScriptString(jsString);\n    } catch (error) {\n      return jsString;\n    }\n  }\n\n  // Handle hex format - return hex representation\n  if (mode.includes('hex')) {\n    // Check if data is already in hex format\n    if (typeof data === 'string' && isHexFormat(data)) {\n      // Data is already in hex format, return it as-is\n      return data;\n    }\n\n    // Data is not in hex format, encode it to hex\n    try {\n      const dataBuffer = Buffer.from(dataBufferString, 'base64');\n      const hexView = formatHexView(dataBuffer);\n      return hexView;\n    } catch (error) {\n      // If buffer conversion fails, try to encode the string data directly\n      if (typeof data === 'string') {\n        try {\n          const stringBuffer = Buffer.from(data, 'utf8');\n          return formatHexView(stringBuffer);\n        } catch (stringError) {\n          return '';\n        }\n      }\n      return '';\n    }\n  }\n\n  // Handle base64 format - return base64 string as-is\n  if (mode.includes('base64')) {\n    return dataBufferString;\n  }\n\n  // Handle raw format - return data as-is without any formatting\n  if (mode.includes('text') || mode.includes('raw')) {\n    if (isVeryLargeResponse) {\n      if (typeof data === 'string') {\n        return data;\n      }\n      if (data === null || data === undefined) {\n        return String(data);\n      }\n      if (typeof data === 'object') {\n        return safeStringifyJSON(data, false);\n      }\n      return String(data);\n    }\n    // Return the raw decoded buffer data\n    return rawData;\n  }\n\n  if (typeof data === 'string') {\n    return data;\n  }\n\n  return safeStringifyJSON(data, !isVeryLargeResponse);\n};\n\nexport const prettifyJsonString = (jsonDataString) => {\n  if (typeof jsonDataString !== 'string') return jsonDataString;\n\n  try {\n    const { hashed, restore } = patternHasher(jsonDataString);\n    const edits = format(hashed, undefined, { tabSize: 2, insertSpaces: true });\n    const formattedJsonDataStringHashed = applyEdits(hashed, edits);\n    const formattedJsonDataString = restore(formattedJsonDataStringHashed);\n    return formattedJsonDataString;\n  } catch (error) {\n    console.log('error formatting json data!');\n    console.error(error);\n  }\n  return jsonDataString;\n};\n\n/**\n * Returns the given string value converted to title case.\n * - If the value is falsy, returns an empty string.\n * - Special-case: if the value is 'default', returns 'Default'.\n * - Otherwise, splits the string on whitespace, hyphens, or underscores,\n *   uppercases the first letter of each word, and lowercases the rest.\n *\n * @param {string} str - The input string to convert.\n * @returns {string} - The converted title-case string.\n */\n\nexport const toTitleCase = (str) => {\n  if (!str) return '';\n  if (str === 'default') return 'Default';\n  return str\n    .split(/[\\s-_]+/)\n    .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n    .join(' ');\n};\n// Simple HTML formatter that indents HTML properly\nexport function prettifyHtmlString(htmlString) {\n  if (typeof htmlString !== 'string') return htmlString;\n\n  try {\n    // Use xml-formatter which works well for HTML\n    return xmlFormat(htmlString, {\n      collapseContent: true,\n      lineSeparator: '\\n',\n      whiteSpaceAtEndOfSelfClosingTag: true\n    });\n  } catch (error) {\n    console.log('error formatting html data!');\n    console.error(error);\n    // Fallback: return original string if formatting fails\n    return htmlString;\n  }\n};\n\n// Simple JavaScript formatter that uses prettier\nexport function prettifyJavaScriptString(jsString) {\n  if (typeof jsString !== 'string') return jsString;\n\n  try {\n    return prettierFormat.format(jsString, {\n      parser: 'babel',\n      plugins: [parserBabel],\n      semi: true,\n      singleQuote: true,\n      tabWidth: 2,\n      trailingComma: 'none',\n      printWidth: 120\n    });\n  } catch (error) {\n    // If prettier fails, return the original string\n    return jsString;\n  }\n};\n\nexport function formatHexView(buffer) {\n  const width = 16;\n  let output = '';\n\n  for (let i = 0; i < buffer.length; i += width) {\n    const slice = buffer.slice(i, i + width);\n    const hex = Array.from(slice)\n      .map((b) => b.toString(16).padStart(2, '0').toUpperCase())\n      .join(' ');\n    const ascii = Array.from(slice)\n      .map((b) => (b >= 32 && b <= 126 ? String.fromCharCode(b) : '.'))\n      .join('');\n\n    output += `${i.toString(16).padStart(8, '0')}: ${hex.padEnd(48)} ${ascii}\\n`;\n  }\n\n  return output;\n}\n\n// Function to detect if a string is already in hex format\n// Checks if the string looks like hex dump format (with addresses and ASCII) or plain hex\nexport function isHexFormat(str) {\n  if (typeof str !== 'string' || !str.trim()) {\n    return false;\n  }\n\n  const trimmed = str.trim();\n\n  // Check for hex dump format (e.g., \"00000000: 48 65 6C 6C 6F 20 57 6F 72 6C 64 21 00 00 00 00  Hello World!....\")\n  const hexDumpPattern = /^[0-9a-fA-F]{8}:\\s+([0-9a-fA-F]{2}\\s+){1,16}/m;\n  if (hexDumpPattern.test(trimmed)) {\n    return true;\n  }\n\n  // Check for plain hex string (only hex characters, possibly with spaces)\n  // Remove spaces and check if all characters are hex\n  const hexOnly = trimmed.replace(/\\s+/g, '');\n  if (hexOnly.length > 0 && /^[0-9a-fA-F]+$/i.test(hexOnly)) {\n    // Make sure it's not too short (could be a regular number) and has even length\n    // Require minimum length of 6 to reduce false positives (e.g., \"dead\", \"beef\")\n    // Also require at least one digit 0-9 to avoid matching all-letter words\n    if (hexOnly.length >= 6 && hexOnly.length % 2 === 0 && /[0-9]/.test(hexOnly)) {\n      return true;\n    }\n  }\n\n  return false;\n}\n"
  },
  {
    "path": "packages/bruno-app/src/utils/common/index.spec.js",
    "content": "const { describe, it, expect } = require('@jest/globals');\n\nimport {\n  normalizeFileName,\n  startsWith,\n  humanizeDate,\n  relativeDate,\n  getContentType,\n  formatSize,\n  prettifyJsonString\n} from './index';\n\ndescribe('common utils', () => {\n  describe('normalizeFileName', () => {\n    it('should remove special characters', () => {\n      expect(normalizeFileName('hello world')).toBe('hello world');\n      expect(normalizeFileName('hello-world')).toBe('hello-world');\n      expect(normalizeFileName('hello_world')).toBe('hello_world');\n      expect(normalizeFileName('hello_world-')).toBe('hello_world-');\n      expect(normalizeFileName('hello_world-123')).toBe('hello_world-123');\n      expect(normalizeFileName('hello_world-123!@#$%^&*()')).toBe('hello_world-123----------');\n      expect(normalizeFileName('hello_world?')).toBe('hello_world-');\n      expect(normalizeFileName('foo/bar/')).toBe('foo-bar-');\n      expect(normalizeFileName('foo\\\\bar\\\\')).toBe('foo-bar-');\n    });\n  });\n\n  describe('startsWith', () => {\n    it('should return false if str is not a string', () => {\n      expect(startsWith(null, 'foo')).toBe(false);\n      expect(startsWith(undefined, 'foo')).toBe(false);\n      expect(startsWith(123, 'foo')).toBe(false);\n      expect(startsWith({}, 'foo')).toBe(false);\n      expect(startsWith([], 'foo')).toBe(false);\n    });\n\n    it('should return false if search is not a string', () => {\n      expect(startsWith('foo', null)).toBe(false);\n      expect(startsWith('foo', undefined)).toBe(false);\n      expect(startsWith('foo', 123)).toBe(false);\n      expect(startsWith('foo', {})).toBe(false);\n      expect(startsWith('foo', [])).toBe(false);\n    });\n\n    it('should return false if str does not start with search', () => {\n      expect(startsWith('foo', 'bar')).toBe(false);\n      expect(startsWith('foo', 'baz')).toBe(false);\n      expect(startsWith('foo', 'bar')).toBe(false);\n      expect(startsWith('foo', 'baz')).toBe(false);\n      expect(startsWith('foo', 'bar')).toBe(false);\n      expect(startsWith('foo', 'baz')).toBe(false);\n    });\n\n    it('should return true if str starts with search', () => {\n      expect(startsWith('foo', 'f')).toBe(true);\n      expect(startsWith('foo', 'fo')).toBe(true);\n      expect(startsWith('foo', 'foo')).toBe(true);\n    });\n  });\n\n  describe('humanizeDate', () => {\n    it('should return a date string in the en-US locale', () => {\n      expect(humanizeDate('2024-03-17')).toBe('March 17, 2024');\n    });\n\n    it('should return invalid date if the date is invalid', () => {\n      expect(humanizeDate('9999-99-99')).toBe('Invalid Date');\n    });\n\n    it('should return \"Invalid Date\" if the date is null', () => {\n      expect(humanizeDate(null)).toBe('Invalid Date');\n    });\n\n    it('should return a humanized date for a valid date in ISO format', () => {\n      expect(humanizeDate('2024-11-28T00:00:00Z')).toBe('November 28, 2024');\n    });\n\n    it('should return \"Invalid Date\" for a non-date string', () => {\n      expect(humanizeDate('some random text')).toBe('Invalid Date');\n    });\n  });\n\n  describe('relativeDate', () => {\n    it('should return few seconds ago', () => {\n      expect(relativeDate(new Date())).toBe('Few seconds ago');\n    });\n\n    it('should return minutes ago', () => {\n      let date = new Date();\n      date.setMinutes(date.getMinutes() - 30);\n      expect(relativeDate(date)).toBe('30 minutes ago');\n    });\n\n    it('should return hours ago', () => {\n      let date = new Date();\n      date.setHours(date.getHours() - 10);\n      expect(relativeDate(date)).toBe('10 hours ago');\n    });\n\n    it('should return days ago', () => {\n      let date = new Date();\n      date.setDate(date.getDate() - 5);\n      expect(relativeDate(date)).toBe('5 days ago');\n    });\n\n    it('should return weeks ago', () => {\n      let date = new Date();\n      date.setDate(date.getDate() - 8);\n      expect(relativeDate(date)).toBe('1 week ago');\n    });\n\n    it('should return months ago', () => {\n      let date = new Date();\n      date.setDate(date.getDate() - 60);\n      expect(relativeDate(date)).toBe('2 months ago');\n    });\n  });\n\n  describe('getContentType', () => {\n    it('should handle JSON content types correctly', () => {\n      expect(getContentType({ 'content-type': 'application/json' })).toBe('application/ld+json');\n      expect(getContentType({ 'content-type': 'text/json' })).toBe('application/ld+json');\n      expect(getContentType({ 'content-type': 'application/ld+json' })).toBe('application/ld+json');\n    });\n\n    it('should handle XML content types correctly', () => {\n      expect(getContentType({ 'content-type': 'text/xml' })).toBe('application/xml');\n      expect(getContentType({ 'content-type': 'application/xml' })).toBe('application/xml');\n      expect(getContentType({ 'content-type': 'application/atom+xml' })).toBe('application/xml');\n    });\n\n    it('should handle image content types correctly', () => {\n      expect(getContentType({ 'content-type': 'image/svg+xml;charset=utf-8' })).toBe('image/svg+xml');\n      expect(getContentType({ 'content-type': 'IMAGE/SVG+xml' })).toBe('image/svg+xml');\n    });\n\n    it('should return original content type when no pattern matches', () => {\n      expect(getContentType({ 'content-type': 'image/jpeg' })).toBe('image/jpeg');\n      expect(getContentType({ 'content-type': 'application/pdf' })).toBe('application/pdf');\n    });\n\n    it('should not be case sensitive', () => {\n      expect(getContentType({ 'content-type': 'text/json' })).toBe('application/ld+json');\n      expect(getContentType({ 'Content-Type': 'text/json' })).toBe('application/ld+json');\n    });\n\n    it('should handle empty content type', () => {\n      expect(getContentType({ 'content-type': '' })).toBe('');\n      expect(getContentType({ 'content-type': null })).toBe('');\n      expect(getContentType({ 'content-type': undefined })).toBe('');\n    });\n\n    it('should handle empty or invalid inputs', () => {\n      expect(getContentType({})).toBe('');\n      expect(getContentType(null)).toBe('');\n      expect(getContentType(undefined)).toBe('');\n    });\n  });\n\n  describe('formatSize', () => {\n    it('should format bytes', () => {\n      expect(formatSize(0)).toBe('0B');\n      expect(formatSize(1023)).toBe('1023B');\n    });\n\n    it('should format kilobytes', () => {\n      expect(formatSize(1024)).toBe('1.0KB');\n      expect(formatSize(1048575)).toBe('1024.0KB');\n    });\n\n    it('should format megabytes', () => {\n      expect(formatSize(1048576)).toBe('1.0MB');\n      expect(formatSize(1073741823)).toBe('1024.0MB');\n    });\n\n    it('should format gigabytes', () => {\n      expect(formatSize(1073741824)).toBe('1.0GB');\n      expect(formatSize(1099511627776)).toBe('1024.0GB');\n    });\n\n    it('should format decimal values', () => {\n      expect(formatSize(1126.5)).toBe('1.1KB');\n      expect(formatSize(1153433.6)).toBe('1.1MB');\n      expect(formatSize(1153433600)).toBe('1.1GB');\n      expect(formatSize(1024.1)).toBe('1.0KB');\n      expect(formatSize(1048576.1)).toBe('1.0MB');\n    });\n\n    it('should format invalid inputs', () => {\n      expect(formatSize(null)).toBe('0B');\n      expect(formatSize(undefined)).toBe('0B');\n      expect(formatSize(NaN)).toBe('0B');\n    });\n  });\n\n  describe('prettifyJsonString', () => {\n    test('should return non-string inputs unchanged', () => {\n      expect(prettifyJsonString(null)).toBe(null);\n      expect(prettifyJsonString(undefined)).toBe(undefined);\n      expect(prettifyJsonString(123)).toBe(123);\n      expect(prettifyJsonString([])).toEqual([]);\n      expect(prettifyJsonString({})).toEqual({});\n      expect(prettifyJsonString(true)).toBe(true);\n    });\n\n    test('should format valid JSON without Bruno variables', () => {\n      const input = '{\"name\":\"John\",\"age\":30}';\n      const expected = `{\\n  \"name\": \"John\",\\n  \"age\": 30\\n}`;\n      console.log(prettifyJsonString(input));\n      expect(prettifyJsonString(input)).toBe(expected);\n    });\n\n    test('should format valid JSON with Bruno variables', () => {\n      const input = '{\"name\": {{userName}}}';\n      const expected = `{\\n  \"name\": {{userName}}\\n}`;\n      console.log(prettifyJsonString(input));\n      expect(prettifyJsonString(input)).toBe(expected);\n    });\n\n    test('should format complex json string', () => {\n      const input = `{\"id\": 123456789123456789123456789,\"name\": \"Test 'JSON' Data with \\\"quotes\\\" — Pretty Print \",\"active\": true,\"price\": 199.9999999,\"decimals\": 1.00,\"nullValue\": null,\"unicodeText\": \"こんにちは世界 \",\"escapedCharacters\": \"Line1\\\\nLine2\\\\tTabbed\\\"Quoted\\\" and 'single quoted' with 'code' style\",\"nestedObject\": {  \"level1\": {    \"level2\": {      \"emptyArray\": [],      \"specialChars\": \"@#$%^&*()_+-=[]{}|;':,./<>?~\",      \"booleanValues\": [        true,        false,        true      ],      \"numbers\": [        0,        -1,        1.23e10,        3.1415926535      ]    }  }},\"mixedArray\": [  \"string with 'apostrophe'\",  42,  false,  null,  {    \"innerObj\": {      \"keyWithQuotes\": \"value containing \\`backticks\\` and 'single quotes'\",      \"nestedArray\": [        {          \"a\": \"O'Reilly\"        }{          \"b\": \"'inline code'\"        },        [          \"deep\",          \"array\",          {            \"c\": \"contains 'quotes'\"          }        ]      ]    }  }],\"nonStringVariable\": {{nonStringVar}},\"withBrunoVariable\": \"{{string}} '{{with}}' \"{{variety}}\" of '{{variables}}'\",\"dateExample\": \"2025-11-07T12:34:56Z\",\"regexExample\": \"^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$\",\"urls\": {  \"website\": \"https://example.com?param='value'&flag='true'\",  \"escapedURL\": \"https:\\/\\/escaped-url.com\\/path\\?q='search'\\&debug='on'\"},\"multiLineString\": \"This is a long text\\\\nthat spans multiple\\\\nlines with \\`backticks\\` 'quotes' and 'code' snippets \"}`;\n      const expectedOutput = `{\n  \"id\": 123456789123456789123456789,\n  \"name\": \"Test 'JSON' Data with \\\"quotes\\\" — Pretty Print \",\n  \"active\": true,\n  \"price\": 199.9999999,\n  \"decimals\": 1.00,\n  \"nullValue\": null,\n  \"unicodeText\": \"こんにちは世界 \",\n  \"escapedCharacters\": \"Line1\\\\nLine2\\\\tTabbed\\\"Quoted\\\" and 'single quoted' with 'code' style\",\n  \"nestedObject\": {\n    \"level1\": {\n      \"level2\": {\n        \"emptyArray\": [],\n        \"specialChars\": \"@#$%^&*()_+-=[]{}|;':,./<>?~\",\n        \"booleanValues\": [\n          true,\n          false,\n          true\n        ],\n        \"numbers\": [\n          0,\n          -1,\n          1.23e10,\n          3.1415926535\n        ]\n      }\n    }\n  },\n  \"mixedArray\": [\n    \"string with 'apostrophe'\",\n    42,\n    false,\n    null,\n    {\n      \"innerObj\": {\n        \"keyWithQuotes\": \"value containing \\`backticks\\` and 'single quotes'\",\n        \"nestedArray\": [\n          {\n            \"a\": \"O'Reilly\"\n          }{\n            \"b\": \"'inline code'\"\n          },\n          [\n            \"deep\",\n            \"array\",\n            {\n              \"c\": \"contains 'quotes'\"\n            }\n          ]\n        ]\n      }\n    }\n  ],\n  \"nonStringVariable\": {{nonStringVar}},\n  \"withBrunoVariable\": \"{{string}} '{{with}}' \"{{variety}}\" of '{{variables}}'\",\n  \"dateExample\": \"2025-11-07T12:34:56Z\",\n  \"regexExample\": \"^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$\",\n  \"urls\": {\n    \"website\": \"https://example.com?param='value'&flag='true'\",\n    \"escapedURL\": \"https:\\/\\/escaped-url.com\\/path\\?q='search'\\&debug='on'\"\n  },\n  \"multiLineString\": \"This is a long text\\\\nthat spans multiple\\\\nlines with \\`backticks\\` 'quotes' and 'code' snippets \"\n}`;\n      expect(prettifyJsonString(input)).toBe(expectedOutput);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/utils/common/ipc.js",
    "content": "/**\n * Wrapper for ipcRenderer.invoke that handles error cases\n * @param {string} channel - The IPC channel name\n * @param {...any} args - Arguments to pass to the channel\n * @returns {Promise} - Resolves with the result or rejects with error\n */\nexport const callIpc = (channel, ...args) => {\n  const { ipcRenderer } = window;\n  if (!ipcRenderer) {\n    return Promise.reject(new Error('IPC Renderer not available'));\n  }\n\n  return ipcRenderer.invoke(channel, ...args);\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/common/masked-editor.js",
    "content": "/**\n * MaskedEditor - A robust, multiline-capable masking system for CodeMirror editors\n *\n * OVERVIEW:\n * This implementation provides flawless masking of sensitive content with proper\n * multiline support, error handling, and memory management. It replaces visible\n * characters with mask characters while preserving the actual content.\n *\n * KEY FEATURES:\n * - Zero race conditions with proper state management\n * - Perfect performance for any content size (small or large)\n * - Proper event handling with cleanup\n * - Memory leak prevention with comprehensive cleanup\n * - Cursor position preservation across multiline edits\n * - Copy/paste compatibility with masked content\n * - Full multiline support (JSON, XML, certificates, etc.)\n * - State consistency across all operations\n * - Error handling for problematic content\n * - Performance optimization strategies\n *\n * MULTILINE SUPPORT:\n * The MaskedEditor automatically handles multiline content efficiently:\n * - Small content (< 500 chars): Character-by-character masking\n * - Large content (>= 500 chars): Line-by-line masking for performance\n * - Preserves line breaks and cursor position across line boundaries\n * - Handles empty lines gracefully\n *\n * USAGE PATTERNS:\n * 1. Create: new MaskedEditor(editor, maskChar)\n * 2. Enable: maskedEditor.enable() - Start masking\n * 3. Disable: maskedEditor.disable() - Show real content\n * 4. Cleanup: maskedEditor.destroy() - CRITICAL for memory management\n *\n * MEMORY MANAGEMENT:\n * Always call destroy() when done to prevent memory leaks:\n * - Removes all event listeners\n * - Clears all DOM marks and references\n * - Cancels pending timeouts\n * - Nullifies object references\n *\n * API METHODS:\n * - enable(): Start masking the editor content\n * - disable(): Stop masking and show real content\n * - update(): Refresh masking (called automatically)\n * - destroy(): Clean up all resources (CRITICAL!)\n * - isEnabled(): Check if masking is currently active\n * - getMaskChar(): Get current mask character\n * - setMaskChar(char): Change mask character\n *\n * PERFORMANCE:\n * - Uses debounced updates (10ms) to prevent excessive re-renders\n * - Character-by-character masking for precise control on small content\n * - Line-by-line masking for efficiency on large content\n * - Efficient mark cleanup and reuse\n * - Bounds checking to prevent errors\n *\n * ERROR HANDLING:\n * - Try-catch blocks for problematic content\n * - Bounds checking for cursor positions\n * - Graceful degradation when marks fail\n * - Memory cleanup even on errors\n */\n\nexport class MaskedEditor {\n  constructor(editor, maskChar = '*') {\n    this.editor = editor;\n    this.maskChar = maskChar;\n    this.enabled = false;\n    this.isProcessing = false;\n    this.marks = new Set();\n\n    // Bind methods to preserve context\n    this.handleInputRead = this.handleInputRead.bind(this);\n    this.handleBeforeChange = this.handleBeforeChange.bind(this);\n    this.handleCursorActivity = this.handleCursorActivity.bind(this);\n    this.handleSelectionChange = this.handleSelectionChange.bind(this);\n  }\n\n  /**\n   * Enable masking with perfect state management\n   */\n  enable() {\n    if (this.enabled || this.isProcessing) return;\n\n    this.enabled = true;\n    this.isProcessing = true;\n\n    try {\n      // Add event listeners with proper cleanup\n      this.editor.on('inputRead', this.handleInputRead);\n      this.editor.on('beforeChange', this.handleBeforeChange);\n      this.editor.on('cursorActivity', this.handleCursorActivity);\n      this.editor.on('selectionChange', this.handleSelectionChange);\n\n      // Apply masking with editor operation for better performance\n      this.applyMasking();\n    } finally {\n      this.isProcessing = false;\n    }\n  }\n\n  /**\n   * Disable masking with complete cleanup\n   */\n  disable() {\n    if (!this.enabled || this.isProcessing) return;\n\n    this.enabled = false;\n    this.isProcessing = true;\n\n    try {\n      // Remove event listeners\n      this.editor.off('inputRead', this.handleInputRead);\n      this.editor.off('beforeChange', this.handleBeforeChange);\n      this.editor.off('cursorActivity', this.handleCursorActivity);\n      this.editor.off('selectionChange', this.handleSelectionChange);\n\n      // Clear all marks\n      this.clearAllMarks();\n\n      // Refresh editor to show real content\n      this.editor.refresh();\n\n      // Move cursor to end of content\n      this.moveCursorToEnd();\n    } finally {\n      this.isProcessing = false;\n    }\n  }\n\n  /**\n   * Update masking (called when content changes)\n   */\n  update() {\n    if (!this.enabled || this.isProcessing) return;\n\n    this.isProcessing = true;\n\n    try {\n      this.applyMasking();\n    } finally {\n      this.isProcessing = false;\n    }\n  }\n\n  /**\n   * Handle multiline content changes efficiently\n   */\n  handleMultilineChange() {\n    if (!this.enabled || this.isProcessing) return;\n\n    this.isProcessing = true;\n\n    try {\n      const content = this.editor.getValue();\n      const lineCount = this.editor.lineCount();\n\n      // For multiline content, use more efficient line-based masking\n      if (lineCount > 1) {\n        this.clearAllMarks();\n        this.applyLineMasking(lineCount);\n      } else {\n        this.update();\n      }\n    } finally {\n      this.isProcessing = false;\n    }\n  }\n\n  /**\n   * Move cursor to the end of the content\n   */\n  moveCursorToEnd() {\n    const lineCount = this.editor.lineCount();\n    if (lineCount > 0) {\n      const lastLine = lineCount - 1;\n      const lastLineLength = this.editor.getLine(lastLine).length;\n      this.editor.setCursor({ line: lastLine, ch: lastLineLength });\n    }\n  }\n\n  /**\n   * Handle input read events\n   */\n  handleInputRead() {\n    if (!this.enabled || this.isProcessing) return;\n\n    // Debounce masking to prevent excessive updates\n    clearTimeout(this.maskTimeout);\n    this.maskTimeout = setTimeout(() => {\n      this.update();\n    }, 10);\n  }\n\n  /**\n   * Handle before change events\n   */\n  handleBeforeChange(cm, changeObj) {\n    if (!this.enabled || this.isProcessing) return;\n    // No cursor state management needed\n  }\n\n  /**\n   * Handle cursor activity\n   */\n  handleCursorActivity() {\n    if (!this.enabled || this.isProcessing) return;\n    // No cursor state management needed\n  }\n\n  /**\n   * Handle selection changes\n   */\n  handleSelectionChange() {\n    if (!this.enabled || this.isProcessing) return;\n    // No cursor state management needed\n  }\n\n  /**\n   * Apply masking with perfect performance\n   */\n  applyMasking() {\n    const content = this.editor.getValue();\n    const lineCount = this.editor.lineCount();\n\n    if (lineCount === 0) {\n      return;\n    }\n\n    this.clearAllMarks();\n\n    // Apply new masking based on content size\n    if (content.length <= 500) {\n      this.applyCharacterMasking(content);\n    } else {\n      // For large content, we apply line-by-line masking for high performance\n      this.applyLineMasking(lineCount);\n    }\n  }\n\n  /**\n   * Apply masking with editor operation for enable operations\n   */\n  applyMasking() {\n    const content = this.editor.getValue();\n    const lineCount = this.editor.lineCount();\n\n    if (lineCount === 0) {\n      return;\n    }\n\n    this.clearAllMarks();\n\n    // Apply new masking based on content size with editor operation\n    if (content.length <= 500) {\n      this.applyCharacterMasking(content);\n    } else {\n      // For large content, we apply line-by-line masking (fast synchronous)\n      this.applyLineMasking(lineCount);\n    }\n  }\n\n  /**\n   * Apply character-by-character masking for small content\n   */\n  applyCharacterMasking(content) {\n    let currentLine = 0;\n    let currentCh = 0;\n\n    for (let i = 0; i < content.length; i++) {\n      const char = content[i];\n\n      if (char === '\\n') {\n        currentLine++;\n        currentCh = 0;\n      } else {\n        // Create masked node\n        const maskedNode = document.createTextNode(this.maskChar);\n\n        // Create mark with proper bounds checking\n        const fromPos = { line: currentLine, ch: currentCh };\n        const toPos = { line: currentLine, ch: currentCh + 1 };\n\n        // Ensure positions are within editor bounds\n        const lineCount = this.editor.lineCount();\n        if (currentLine < lineCount) {\n          const lineLength = this.editor.getLine(currentLine).length;\n          if (currentCh < lineLength) {\n            const mark = this.editor.markText(fromPos, toPos, {\n              replacedWith: maskedNode,\n              handleMouseEvents: true,\n              className: 'masked-character'\n            });\n\n            // Store mark for cleanup\n            this.marks.add(mark);\n          }\n        }\n\n        currentCh++;\n      }\n    }\n  }\n\n  /**\n   * Apply character-by-character masking with editor operation for enable operations\n   */\n  applyCharacterMasking(content) {\n    let currentLine = 0;\n    let currentCh = 0;\n\n    // Use editor operation to batch all DOM operations\n    this.editor.operation(() => {\n      for (let i = 0; i < content.length; i++) {\n        const char = content[i];\n\n        if (char === '\\n') {\n          currentLine++;\n          currentCh = 0;\n        } else {\n          // Create masked node\n          const maskedNode = document.createTextNode(this.maskChar);\n\n          // Create mark with proper bounds checking\n          const fromPos = { line: currentLine, ch: currentCh };\n          const toPos = { line: currentLine, ch: currentCh + 1 };\n\n          // Ensure positions are within editor bounds\n          const lineCount = this.editor.lineCount();\n          if (currentLine < lineCount) {\n            const lineLength = this.editor.getLine(currentLine).length;\n            if (currentCh < lineLength) {\n              const mark = this.editor.markText(fromPos, toPos, {\n                replacedWith: maskedNode,\n                handleMouseEvents: true,\n                className: 'masked-character'\n              });\n\n              // Store mark for cleanup\n              this.marks.add(mark);\n            }\n          }\n\n          currentCh++;\n        }\n      }\n    });\n  }\n\n  /**\n   * Apply line-by-line masking for large content\n   */\n  applyLineMasking(lineCount) {\n    for (let line = 0; line < lineCount; line++) {\n      try {\n        const lineLength = this.editor.getLine(line).length;\n\n        if (lineLength > 0) {\n          // Create masked node for entire line\n          const maskedNode = document.createTextNode(this.maskChar.repeat(lineLength));\n\n          // Create mark with proper bounds checking\n          const mark = this.editor.markText({ line, ch: 0 },\n            { line, ch: lineLength },\n            {\n              replacedWith: maskedNode,\n              handleMouseEvents: false,\n              className: 'masked-line'\n            });\n\n          // Store mark for cleanup\n          this.marks.add(mark);\n        }\n      } catch (error) {\n        // Skip problematic lines to prevent crashes\n        console.warn(`Failed to mask line ${line}:`, error);\n      }\n    }\n  }\n\n  /**\n   * Clear all marks with proper cleanup\n   */\n  clearAllMarks() {\n    // Use editor operation for better performance\n    this.editor.operation(() => {\n      // Clear all marks in the editor\n      const marks = this.editor.getAllMarks();\n      marks.forEach((mark) => {\n        try {\n          mark.clear();\n        } catch (error) {\n          // Skip problematic marks\n          console.warn('Failed to clear mark:', error);\n        }\n      });\n    });\n\n    // Clear our mark tracking\n    this.marks.clear();\n  }\n\n  /**\n   * Check if masking is enabled\n   */\n  isEnabled() {\n    return this.enabled;\n  }\n\n  /**\n   * Get current mask character\n   */\n  getMaskChar() {\n    return this.maskChar;\n  }\n\n  /**\n   * Set new mask character\n   */\n  setMaskChar(newMaskChar) {\n    if (typeof newMaskChar !== 'string' || newMaskChar.length !== 1) {\n      throw new Error('Mask character must be a single character string');\n    }\n\n    this.maskChar = newMaskChar;\n\n    if (this.enabled) {\n      this.update();\n    }\n  }\n\n  /**\n   * Destroy the masked editor instance\n   *\n   * CRITICAL: Always call this method when done with the MaskedEditor\n   * to prevent memory leaks. This method:\n   * 1. Disables masking and removes event listeners\n   * 2. Clears all DOM marks and references\n   * 3. Cancels any pending timeouts\n   * 4. Nullifies all object references\n   */\n  destroy() {\n    this.disable();\n    this.marks.clear();\n\n    if (this.maskTimeout) {\n      clearTimeout(this.maskTimeout);\n      this.maskTimeout = null;\n    }\n  }\n}\n\n/**\n * Factory function to create a perfect masked editor\n */\nexport function createMaskedEditor(editor, maskChar = '*') {\n  return new MaskedEditor(editor, maskChar);\n}\n\n/**\n * Utility function to check if an editor supports masking\n */\nexport function supportsMasking(editor) {\n  return editor\n    && typeof editor.getValue === 'function'\n    && typeof editor.markText === 'function'\n    && typeof editor.operation === 'function';\n}\n"
  },
  {
    "path": "packages/bruno-app/src/utils/common/path.js",
    "content": "import platform from 'platform';\nimport path from 'path';\n\nconst isWindowsOS = () => {\n  const os = platform.os;\n  const osFamily = os.family.toLowerCase();\n  return osFamily.includes('windows');\n};\n\n/**\n * Cross-Platform Path Standardization for Bruno Configuration Files\n *\n * Bruno stores relative paths in configuration files (bruno.json) that are committed to version control.\n * This creates cross-platform compatibility challenges when Windows and Unix users collaborate on the same project.\n *\n * PROBLEM:\n * - Windows users naturally create paths with backslashes (e.g., \"certs\\\\client.pem\")\n * - Unix systems don't recognize backslashes as path separators\n * - When a Windows user commits a bruno.json with Windows-style paths, Unix users cannot resolve these paths\n * - This forces manual path conversion before git commits, which is error-prone and inconvenient\n *\n * SOLUTION:\n * - Standardize all stored paths to POSIX format (forward slashes) across all platforms\n * - Windows natively supports forward slashes as valid path separators\n * - Use the posixify parameter to ensure consistent path storage\n * - This enables seamless collaboration between Windows and Unix developers\n *\n * IMPLEMENTATION:\n * - Always enable posixify by default when storing paths in configuration files\n * - Client certificates, protobuf files, and other relative paths should use POSIX format\n * - Both platforms can then resolve the same paths accurately without manual intervention\n *\n * BENEFITS:\n * - No manual path conversion required before git commits\n * - Consistent behavior across all platforms\n * - Improved developer experience for cross-platform teams\n * - Reduced git conflicts and merge issues related to path differences\n */\n/** @param {string} str */\nconst posixify = (str) => {\n  return str.replace(/\\\\/g, '/');\n};\n\nconst brunoPath = isWindowsOS() ? path.win32 : path.posix;\n\n/**\n * Get a relative path from one location to another.\n *\n * This function attempts to compute the relative path between two given paths\n *\n * @param {string} fromPath - The starting path.\n * @param {string} toPath - The target path.\n * @param {boolean} [shouldPosixify=true] - Whether to convert backslashes to forward slashes for cross-platform compatibility.\n * @returns {string} The relative path from `fromPath` to `toPath`, `\".\"` if both are the same,\n *                   or `toPath` if resolution fails.\n *\n * @example\n * Assuming current dir: /users/john/projects\n * getRelativePath('/users/john/projects', '/users/john/projects/app');\n *  → \"app\"\n *\n * @example\n * getRelativePath('/users/john/projects', '/users/john/projects');\n *  → \".\"\n *\n * @example\n * getRelativePath('/users/john/projects', '/users/john/docs/readme.md');\n *  → \"../docs/readme.md\"\n *\n * @example\n * On Windows with posixify enabled\n * getRelativePath('C:\\\\Users\\\\John\\\\Projects', 'C:\\\\Users\\\\John\\\\Docs\\\\readme.md', true);\n *  → \"../Docs/readme.md\"\n */\nconst getRelativePath = (fromPath, toPath, shouldPosixify = true) => {\n  try {\n    const relativePath = brunoPath.relative(fromPath, toPath);\n\n    if (relativePath === '') {\n      return '.';\n    }\n\n    const result = relativePath || toPath;\n\n    return shouldPosixify ? posixify(result) : result;\n  } catch (error) {\n    return shouldPosixify ? posixify(toPath) : toPath;\n  }\n};\n\n/**\n * Get the basename (filename) of a file from a relative path.\n *\n * This function resolves a relative path against a base path and returns\n * just the filename portion. It handles cross-platform path separators\n * and returns an empty string for invalid inputs.\n *\n * @param {string} basePath - The base path to resolve against (e.g., \"/users/john/projects\")\n * @param {string} relativePath - The relative path to resolve (e.g., \"../docs/file.txt\")\n * @returns {string} The basename of the resolved path, or empty string if relativePath is falsy\n *\n * @example\n * getBasename(\"/users/john/projects\", \"../docs/readme.md\");\n *  → \"readme.md\"\n *\n * @example\n * getBasename(\"/users/john/projects\", \"subfolder/config.json\");\n *  → \"config.json\"\n *\n * @example\n * getBasename(\"/users/john/projects\", \"..\");\n *  → \"john\"\n *\n * @example\n * getBasename(\"/users/john/projects\", \".\");\n *  → \"projects\"\n */\nconst getBasename = (basePath, relativePath) => {\n  if (!relativePath) {\n    return '';\n  }\n\n  const resolvedPath = brunoPath.resolve(basePath, relativePath);\n  const basename = brunoPath.basename(resolvedPath);\n\n  return basename;\n};\n\n/**\n * Resolve a relative file path against a base path to get an absolute file path.\n *\n * This function resolves a relative path against a base path using the appropriate\n * path resolution method for the current platform (Windows or Unix). It handles\n * cross-platform path separators and returns a normalized absolute path.\n *\n * @param {string} basePath - The base path to resolve against (e.g., \"/users/john/collections\" or \"C:\\\\Users\\\\John\\\\Collections\")\n * @param {string} relativePath - The relative path to resolve (e.g., \"config/settings.json\" or \"config\\\\settings.json\")\n * @param {boolean} [shouldPosixify=false] - Whether to convert backslashes to forward slashes for cross-platform compatibility.\n * @returns {string} The resolved absolute file path\n *\n * @example\n * Basic relative path resolution\n * getAbsoluteFilePath('/users/john/collections', 'config/settings.json');\n * → \"/users/john/collections/config/settings.json\"\n *\n * @example\n * Handle parent directory references\n * getAbsoluteFilePath('/users/john/collections/api', '../shared/config.json');\n * → \"/users/john/collections/shared/config.json\"\n *\n * @example\n * Handle current directory reference\n * getAbsoluteFilePath('/users/john/collections', './local-file.json');\n * → \"/users/john/collections/local-file.json\"\n *\n * @example\n * On Windows with posixify enabled\n * getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Collections', 'config\\\\settings.json', true);\n * → \"C:/Users/John/Collections/config/settings.json\"\n */\nconst getAbsoluteFilePath = (basePath, relativePath, shouldPosixify = false) => {\n  const result = brunoPath.resolve(basePath, relativePath);\n  return shouldPosixify ? posixify(result) : result;\n};\n\nconst normalizePath = (p) => {\n  if (!p) return '';\n  return p.replace(/\\\\/g, '/').replace(/\\/+$/, '');\n};\n\nexport default brunoPath;\nexport { getRelativePath, getBasename, getAbsoluteFilePath, normalizePath };\n"
  },
  {
    "path": "packages/bruno-app/src/utils/common/path.spec.js",
    "content": "// Mock platform module for Unix before importing path utilities\njest.mock('platform', () => ({\n  os: {\n    family: 'Unix'\n  }\n}));\n\nimport { getRelativePath, getBasename, getAbsoluteFilePath } from './path';\n\ndescribe('Path Utilities - Unix Platform', () => {\n  describe('getRelativePath', () => {\n    it('should return relative path between two directories', () => {\n      expect(getRelativePath('/users/john/projects', '/users/john/projects/app')).toBe('app');\n    });\n\n    it('should return \".\" when both paths are the same', () => {\n      expect(getRelativePath('/users/john/projects', '/users/john/projects')).toBe('.');\n    });\n\n    it('should return parent directory path', () => {\n      expect(getRelativePath('/users/john/projects', '/users/john/docs/readme.md')).toBe('../docs/readme.md');\n    });\n\n    it('should return nested subdirectory path', () => {\n      expect(getRelativePath('/users/john/projects', '/users/john/projects/src/components')).toBe('src/components');\n    });\n\n    it('should handle null/undefined inputs', () => {\n      expect(getRelativePath(null, '/users/john/projects')).toBe('/users/john/projects');\n      expect(getRelativePath(undefined, '/users/john/projects')).toBe('/users/john/projects');\n    });\n  });\n\n  describe('getBasename', () => {\n    it('should return filename from relative path', () => {\n      expect(getBasename('/users/john/projects', '../docs/readme.md')).toBe('readme.md');\n    });\n\n    it('should return filename from subdirectory path', () => {\n      expect(getBasename('/users/john/projects', 'subfolder/config.json')).toBe('config.json');\n    });\n\n    it('should return filename from direct file path', () => {\n      expect(getBasename('/users/john/projects', 'package.json')).toBe('package.json');\n    });\n\n    it('should return directory name for parent directory', () => {\n      expect(getBasename('/users/john/projects', '..')).toBe('john');\n    });\n\n    it('should return directory name for current directory', () => {\n      expect(getBasename('/users/john/projects', '.')).toBe('projects');\n    });\n\n    it('should return filename from nested path', () => {\n      expect(getBasename('/users/john/projects', 'src/components/Button.jsx')).toBe('Button.jsx');\n    });\n\n    it('should return empty string for falsy relativePath', () => {\n      expect(getBasename('/users/john/projects', '')).toBe('');\n      expect(getBasename('/users/john/projects', null)).toBe('');\n      expect(getBasename('/users/john/projects', undefined)).toBe('');\n    });\n\n    it('should handle complex relative paths', () => {\n      expect(getBasename('/users/john/projects', '../../docs/api/spec.md')).toBe('spec.md');\n    });\n\n    it('should handle paths with multiple extensions', () => {\n      expect(getBasename('/users/john/projects', 'src/utils/common/path.spec.js')).toBe('path.spec.js');\n    });\n  });\n\n  describe('getAbsoluteFilePath', () => {\n    it('should resolve relative file path against collection path', () => {\n      const result = getAbsoluteFilePath('/users/john/collections', 'config/settings.json');\n      expect(result).toBe('/users/john/collections/config/settings.json');\n    });\n\n    it('should handle nested file paths', () => {\n      const result = getAbsoluteFilePath('/users/john/collections', 'api/v1/users.json');\n      expect(result).toBe('/users/john/collections/api/v1/users.json');\n    });\n\n    it('should handle parent directory references', () => {\n      const result = getAbsoluteFilePath('/users/john/collections/api', '../shared/config.json');\n      expect(result).toBe('/users/john/collections/shared/config.json');\n    });\n\n    it('should handle empty file path', () => {\n      const result = getAbsoluteFilePath('/users/john/collections', '');\n      expect(result).toBe('/users/john/collections');\n    });\n\n    it('should handle current directory reference', () => {\n      const result = getAbsoluteFilePath('/users/john/collections', '.');\n      expect(result).toBe('/users/john/collections');\n    });\n\n    it('should handle previous directory reference', () => {\n      const result = getAbsoluteFilePath('/users/john/collections', '..');\n      expect(result).toBe('/users/john');\n    });\n\n    it('should handle root file path', () => {\n      const result = getAbsoluteFilePath('/users/john/collections', '/absolute/path/file.json');\n      expect(result).toBe('/absolute/path/file.json');\n    });\n\n    it('should handle current directory reference', () => {\n      const result = getAbsoluteFilePath('/users/john/collections', './local-file.json');\n      expect(result).toBe('/users/john/collections/local-file.json');\n    });\n  });\n\n  describe('Edge cases', () => {\n    it('should handle very long paths', () => {\n      const longPath = '/users/john/projects/' + 'a'.repeat(100);\n      const result = getBasename(longPath, 'file.txt');\n      expect(result).toBe('file.txt');\n    });\n\n    it('should handle paths with special characters', () => {\n      expect(getBasename('/users/john/projects', 'file with spaces.txt')).toBe('file with spaces.txt');\n      expect(getBasename('/users/john/projects', 'file-with-dashes.txt')).toBe('file-with-dashes.txt');\n      expect(getBasename('/users/john/projects', 'file_with_underscores.txt')).toBe('file_with_underscores.txt');\n    });\n\n    it('should handle paths with unicode characters', () => {\n      expect(getBasename('/users/john/projects', 'файл.txt')).toBe('файл.txt');\n      expect(getBasename('/users/john/projects', '文件.txt')).toBe('文件.txt');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/utils/common/path.windows.spec.js",
    "content": "// Mock platform module for Windows before importing path utilities\njest.mock('platform', () => ({\n  os: {\n    family: 'Windows'\n  }\n}));\n\nimport { getRelativePath, getBasename, getAbsoluteFilePath } from './path';\n\ndescribe('Path Utilities - Windows Platform', () => {\n  describe('getRelativePath', () => {\n    it('should return relative path between two directories', () => {\n      expect(getRelativePath('C:\\\\Users\\\\John\\\\Projects', 'C:\\\\Users\\\\John\\\\Projects\\\\App')).toBe('App');\n    });\n\n    it('should return \".\" when both paths are the same', () => {\n      expect(getRelativePath('C:\\\\Users\\\\John\\\\Projects', 'C:\\\\Users\\\\John\\\\Projects')).toBe('.');\n    });\n\n    it('should return parent directory path', () => {\n      expect(getRelativePath('C:\\\\Users\\\\John\\\\Projects', 'C:\\\\Users\\\\John\\\\Docs\\\\readme.md', false)).toBe('..\\\\Docs\\\\readme.md');\n    });\n\n    it('should return nested subdirectory path', () => {\n      expect(getRelativePath('C:\\\\Users\\\\John\\\\Projects', 'C:\\\\Users\\\\John\\\\Projects\\\\src\\\\components', false)).toBe('src\\\\components');\n    });\n\n    describe('with posixify enabled', () => {\n      it('should convert backslashes to forward slashes', () => {\n        expect(getRelativePath('C:\\\\Users\\\\John\\\\Projects', 'C:\\\\Users\\\\John\\\\Projects\\\\App')).toBe('App');\n      });\n\n      it('should convert parent directory path to posix format', () => {\n        expect(getRelativePath('C:\\\\Users\\\\John\\\\Projects', 'C:\\\\Users\\\\John\\\\Docs\\\\readme.md')).toBe('../Docs/readme.md');\n      });\n\n      it('should convert nested subdirectory path to posix format', () => {\n        expect(getRelativePath('C:\\\\Users\\\\John\\\\Projects', 'C:\\\\Users\\\\John\\\\Projects\\\\src\\\\components')).toBe('src/components');\n      });\n\n      it('should handle complex paths with posixify', () => {\n        expect(getRelativePath('C:\\\\Users\\\\John\\\\Projects\\\\api', 'C:\\\\Users\\\\John\\\\Projects\\\\src\\\\utils\\\\common')).toBe('../src/utils/common');\n      });\n\n      it('should handle deep nested paths with posixify', () => {\n        expect(getRelativePath('C:\\\\Users\\\\John\\\\Projects', 'C:\\\\Users\\\\John\\\\Projects\\\\src\\\\components\\\\ui\\\\forms')).toBe('src/components/ui/forms');\n      });\n\n      it('should handle paths with multiple backslashes', () => {\n        expect(getRelativePath('C:\\\\Users\\\\John\\\\Projects', 'C:\\\\Users\\\\John\\\\Projects\\\\src\\\\\\\\components')).toBe('src/components');\n      });\n    });\n  });\n\n  describe('getBasename', () => {\n    it('should return filename from relative path', () => {\n      expect(getBasename('C:\\\\Users\\\\John\\\\Projects', '..\\\\Docs\\\\readme.md')).toBe('readme.md');\n    });\n\n    it('should return filename from subdirectory path', () => {\n      expect(getBasename('C:\\\\Users\\\\John\\\\Projects', 'subfolder\\\\config.json')).toBe('config.json');\n    });\n\n    it('should return filename from direct file path', () => {\n      expect(getBasename('C:\\\\Users\\\\John\\\\Projects', 'package.json')).toBe('package.json');\n    });\n\n    it('should return directory name for parent directory', () => {\n      expect(getBasename('C:\\\\Users\\\\John\\\\Projects', '..')).toBe('John');\n    });\n\n    it('should return directory name for current directory', () => {\n      expect(getBasename('C:\\\\Users\\\\John\\\\Projects', '.')).toBe('Projects');\n    });\n\n    it('should return filename from nested path', () => {\n      expect(getBasename('C:\\\\Users\\\\John\\\\Projects', 'src\\\\components\\\\Button.jsx')).toBe('Button.jsx');\n    });\n\n    it('should return empty string for falsy relativePath', () => {\n      expect(getBasename('C:\\\\Users\\\\John\\\\Projects', '')).toBe('');\n      expect(getBasename('C:\\\\Users\\\\John\\\\Projects', null)).toBe('');\n      expect(getBasename('C:\\\\Users\\\\John\\\\Projects', undefined)).toBe('');\n    });\n\n    it('should handle complex relative paths', () => {\n      expect(getBasename('C:\\\\Users\\\\John\\\\Projects', '..\\\\..\\\\Docs\\\\api\\\\spec.md')).toBe('spec.md');\n    });\n\n    it('should handle paths with multiple extensions', () => {\n      expect(getBasename('C:\\\\Users\\\\John\\\\Projects', 'src\\\\utils\\\\common\\\\path.spec.js')).toBe('path.spec.js');\n    });\n  });\n\n  describe('getAbsoluteFilePath', () => {\n    it('should resolve relative file path against collection path', () => {\n      const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Collections', 'config\\\\settings.json');\n      expect(result).toBe('C:\\\\Users\\\\John\\\\Collections\\\\config\\\\settings.json');\n    });\n\n    it('should handle nested file paths', () => {\n      const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Collections', 'api\\\\v1\\\\users.json');\n      expect(result).toBe('C:\\\\Users\\\\John\\\\Collections\\\\api\\\\v1\\\\users.json');\n    });\n\n    it('should handle parent directory references', () => {\n      const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Collections\\\\api', '..\\\\shared\\\\config.json');\n      expect(result).toBe('C:\\\\Users\\\\John\\\\Collections\\\\shared\\\\config.json');\n    });\n\n    it('should handle empty file path', () => {\n      const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Collections', '');\n      expect(result).toBe('C:\\\\Users\\\\John\\\\Collections');\n    });\n\n    it('should handle current directory reference', () => {\n      const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Collections', '.');\n      expect(result).toBe('C:\\\\Users\\\\John\\\\Collections');\n    });\n\n    it('should handle previous directory reference', () => {\n      const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Collections', '..');\n      expect(result).toBe('C:\\\\Users\\\\John');\n    });\n\n    it('should handle root file path', () => {\n      const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Collections', 'D:\\\\absolute\\\\path\\\\file.json');\n      expect(result).toBe('D:\\\\absolute\\\\path\\\\file.json');\n    });\n\n    it('should handle current directory reference', () => {\n      const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Collections', '.\\\\local-file.json');\n      expect(result).toBe('C:\\\\Users\\\\John\\\\Collections\\\\local-file.json');\n    });\n\n    describe('with posixify enabled', () => {\n      it('should convert backslashes to forward slashes in resolved path', () => {\n        const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Collections', 'config\\\\settings.json', true);\n        expect(result).toBe('C:/Users/John/Collections/config/settings.json');\n      });\n\n      it('should handle nested file paths with posixify', () => {\n        const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Collections', 'api\\\\v1\\\\users.json', true);\n        expect(result).toBe('C:/Users/John/Collections/api/v1/users.json');\n      });\n\n      it('should handle parent directory references with posixify', () => {\n        const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Collections\\\\api', '..\\\\shared\\\\config.json', true);\n        expect(result).toBe('C:/Users/John/Collections/shared/config.json');\n      });\n\n      it('should handle current directory reference with posixify', () => {\n        const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Collections', '.', true);\n        expect(result).toBe('C:/Users/John/Collections');\n      });\n\n      it('should handle previous directory reference with posixify', () => {\n        const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Collections', '..', true);\n        expect(result).toBe('C:/Users/John');\n      });\n\n      it('should handle root file path with posixify', () => {\n        const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Collections', 'D:\\\\absolute\\\\path\\\\file.json', true);\n        expect(result).toBe('D:/absolute/path/file.json');\n      });\n\n      it('should handle current directory reference with posixify', () => {\n        const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Collections', '.\\\\local-file.json', true);\n        expect(result).toBe('C:/Users/John/Collections/local-file.json');\n      });\n\n      it('should handle complex nested paths with posixify', () => {\n        const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Projects', 'src\\\\components\\\\ui\\\\forms\\\\login.jsx', true);\n        expect(result).toBe('C:/Users/John/Projects/src/components/ui/forms/login.jsx');\n      });\n\n      it('should handle paths with multiple backslashes with posixify', () => {\n        const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Projects', 'src\\\\\\\\components\\\\button.jsx', true);\n        expect(result).toBe('C:/Users/John/Projects/src/components/button.jsx');\n      });\n    });\n  });\n\n  describe('Cross-platform path handling', () => {\n    describe('Windows fromPath with POSIX toPath', () => {\n      it('should handle Windows fromPath with POSIX toPath in getAbsoluteFilePath', () => {\n        // This demonstrates the current behavior\n        const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Projects', 'App/config.json');\n        expect(result).toBe('C:\\\\Users\\\\John\\\\Projects\\\\App\\\\config.json');\n      });\n\n      it('should handle Windows fromPath with mixed separators in getRelativePath', () => {\n        const result = getRelativePath('C:\\\\Users\\\\John\\\\Projects', 'C:/Users/John/Projects/App');\n        // This should work since both are Windows paths, just different separators\n        expect(result).toBe('App');\n      });\n\n      it('should handle Windows fromPath with mixed separators in getAbsoluteFilePath', () => {\n        const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Projects', 'App/config.json');\n        expect(result).toBe('C:\\\\Users\\\\John\\\\Projects\\\\App\\\\config.json');\n      });\n\n      it('should handle Windows fromPath with mixed separators in getAbsoluteFilePath', () => {\n        const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Projects', '../config.json');\n        expect(result).toBe('C:\\\\Users\\\\John\\\\config.json');\n      });\n    });\n\n    describe('Mixed path separators within same platform', () => {\n      it('should handle mixed separators in Windows paths for getRelativePath', () => {\n        const result = getRelativePath('C:/Users/John/Projects', 'C:\\\\Users\\\\John\\\\Projects\\\\App', false);\n        expect(result).toBe('App');\n      });\n\n      it('should handle mixed separators in Windows paths for getAbsoluteFilePath', () => {\n        const result = getAbsoluteFilePath('C:/Users/John/Projects', 'App\\\\config.json');\n        expect(result).toBe('C:\\\\Users\\\\John\\\\Projects\\\\App\\\\config.json');\n      });\n\n      it('should handle mixed separators with posixify in getRelativePath', () => {\n        const result = getRelativePath('C:/Users/John/Projects', 'C:\\\\Users\\\\John\\\\Projects\\\\App', true);\n        expect(result).toBe('App');\n      });\n\n      it('should handle mixed separators with posixify in getAbsoluteFilePath', () => {\n        const result = getAbsoluteFilePath('C:/Users/John/Projects', 'App\\\\config.json', true);\n        expect(result).toBe('C:/Users/John/Projects/App/config.json');\n      });\n    });\n\n    describe('Cross-platform with posixify', () => {\n      it('should normalize cross-platform paths with posixify in getRelativePath', () => {\n        const result = getRelativePath('C:\\\\Users\\\\John\\\\Projects', 'C:/Users/John/Projects/App', true);\n        expect(result).toBe('App');\n      });\n\n      it('should normalize cross-platform paths with posixify in getAbsoluteFilePath', () => {\n        const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Projects', 'App/config.json', true);\n        expect(result).toBe('C:/Users/John/Projects/App/config.json');\n      });\n\n      it('should handle complex mixed separators with posixify', () => {\n        const result = getAbsoluteFilePath('C:/Users/John/Projects', 'src\\\\components/ui\\\\forms', true);\n        expect(result).toBe('C:/Users/John/Projects/src/components/ui/forms');\n      });\n\n      it('should handle POSIX absolute paths with posixify', () => {\n        const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Projects', './absolute/path/file.json', true);\n        expect(result).toBe('C:/Users/John/Projects/absolute/path/file.json');\n      });\n\n      it('should handle Windows absolute paths with posixify', () => {\n        const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Projects', '.\\\\absolute\\\\path\\\\file.json', true);\n        expect(result).toBe('C:/Users/John/Projects/absolute/path/file.json');\n      });\n    });\n  });\n\n  describe('Edge cases', () => {\n    it('should handle very long paths', () => {\n      const longPath = 'C:\\\\Users\\\\John\\\\Projects\\\\' + 'a'.repeat(100);\n      const result = getBasename(longPath, 'file.txt');\n      expect(result).toBe('file.txt');\n    });\n\n    it('should handle paths with special characters', () => {\n      expect(getBasename('C:\\\\Users\\\\John\\\\Projects', 'file with spaces.txt')).toBe('file with spaces.txt');\n      expect(getBasename('C:\\\\Users\\\\John\\\\Projects', 'file-with-dashes.txt')).toBe('file-with-dashes.txt');\n      expect(getBasename('C:\\\\Users\\\\John\\\\Projects', 'file_with_underscores.txt')).toBe('file_with_underscores.txt');\n    });\n\n    it('should handle paths with unicode characters', () => {\n      expect(getBasename('C:\\\\Users\\\\John\\\\Projects', 'файл.txt')).toBe('файл.txt');\n      expect(getBasename('C:\\\\Users\\\\John\\\\Projects', '文件.txt')).toBe('文件.txt');\n    });\n\n    describe('with posixify enabled', () => {\n      it('should handle very long paths with posixify', () => {\n        const longPath = 'C:\\\\Users\\\\John\\\\Projects\\\\' + 'a'.repeat(100);\n        const result = getAbsoluteFilePath(longPath, 'file.txt', true);\n        expect(result).toBe(`C:/Users/John/Projects/${'a'.repeat(100)}/file.txt`);\n      });\n\n      it('should handle paths with special characters and posixify', () => {\n        const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Projects', 'file with spaces.txt', true);\n        expect(result).toBe('C:/Users/John/Projects/file with spaces.txt');\n      });\n\n      it('should handle paths with unicode characters and posixify', () => {\n        const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Projects', 'файл.txt', true);\n        expect(result).toBe('C:/Users/John/Projects/файл.txt');\n      });\n\n      it('should handle mixed path separators with posixify', () => {\n        const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Projects', 'src/components\\\\ui/forms', true);\n        expect(result).toBe('C:/Users/John/Projects/src/components/ui/forms');\n      });\n\n      it('should handle empty relative path with posixify', () => {\n        const result = getAbsoluteFilePath('C:\\\\Users\\\\John\\\\Projects', '', true);\n        expect(result).toBe('C:/Users/John/Projects');\n      });\n\n      it('should handle relative path with posixify', () => {\n        const result = getRelativePath('C:\\\\Users\\\\John\\\\Projects', 'C:\\\\Users\\\\John\\\\Projects\\\\файл.txt', true);\n        expect(result).toBe('файл.txt');\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/utils/common/platform.js",
    "content": "import trim from 'lodash/trim';\nimport platform from 'platform';\nimport path from './path';\n\nexport const isElectron = () => {\n  if (!window) {\n    return false;\n  }\n\n  return window.ipcRenderer ? true : false;\n};\n\nexport const resolveRequestFilename = (name, extension = 'bru') => {\n  return `${trim(name)}.${extension}`;\n};\n\nexport const getSubdirectoriesFromRoot = (rootPath, pathname) => {\n  const relativePath = path.relative(rootPath, pathname);\n  return relativePath ? relativePath.split(path.sep) : [];\n};\n\nexport const isWindowsOS = () => {\n  const os = platform.os;\n  const osFamily = os.family.toLowerCase();\n\n  return osFamily.includes('windows');\n};\n\nexport const isMacOS = () => {\n  const os = platform.os;\n  const osFamily = os.family.toLowerCase();\n\n  return osFamily.includes('os x');\n};\n\nexport const isLinuxOS = () => {\n  const os = platform.os;\n  const osFamily = os.family.toLowerCase();\n\n  return osFamily.includes('linux') || osFamily.includes('ubuntu') || osFamily.includes('debian') || osFamily.includes('fedora') || osFamily.includes('centos') || osFamily.includes('arch');\n};\n\nexport const getRevealInFolderLabel = () => {\n  if (isMacOS()) return 'Reveal in Finder';\n  if (isWindowsOS()) return 'Reveal in File Explorer';\n  return 'Reveal in File Manager';\n};\n\nexport const getAppInstallDate = () => {\n  let dateString = localStorage.getItem('bruno.installedOn');\n\n  if (!dateString) {\n    dateString = new Date().toISOString();\n    localStorage.setItem('bruno.installedOn', dateString);\n  }\n\n  const date = new Date(dateString);\n  return date;\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/common/regex.js",
    "content": "const invalidCharacters = /[<>:\"/\\\\|?*\\x00-\\x1F]/g; // replace invalid characters with hyphens\nconst reservedDeviceNames = /^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])$/i;\nconst firstCharacter = /^[^\\s\\-<>:\"/\\\\|?*\\x00-\\x1F]/; // no space, hyphen and `invalidCharacters`\nconst middleCharacters = /^[^<>:\"/\\\\|?*\\x00-\\x1F]*$/; // no `invalidCharacters`\nconst lastCharacter = /[^.\\s<>:\"/\\\\|?*\\x00-\\x1F]$/; // no dot, space and `invalidCharacters`\n\nexport const variableNameRegex = /^[\\w-.]*$/;\n\n// HTTP header name should not contain spaces, newlines, or control characters\nexport const headerNameRegex = /^[^\\s\\r\\n]*$/;\n\n// HTTP header value should not contain newlines\nexport const headerValueRegex = /^[^\\r\\n]*$/;\n\nexport const sanitizeName = (name) => {\n  name = name\n    .replace(invalidCharacters, '-') // replace invalid characters with hyphens\n    .replace(/^[\\s\\-]+/, '') // remove leading spaces and hyphens\n    .replace(/[.\\s]+$/, ''); // remove trailing dots and spaces\n  return name;\n};\n\nexport const validateName = (name) => {\n  if (!name) return false;\n  if (name.length > 255) return false; // max name length\n\n  if (reservedDeviceNames.test(name)) return false; // windows reserved names\n\n  return (\n    firstCharacter.test(name)\n    && middleCharacters.test(name)\n    && lastCharacter.test(name)\n  );\n};\n\nexport const validateNameError = (name) => {\n  if (!name) return 'Name cannot be empty.';\n\n  if (name.length > 255) {\n    return 'Name cannot exceed 255 characters.';\n  }\n\n  if (reservedDeviceNames.test(name)) {\n    return 'Name cannot be a reserved device name.';\n  }\n\n  if (!firstCharacter.test(name[0])) {\n    return `Special characters aren't allowed in the name. Invalid character '${name[0]}'.`;\n  }\n\n  for (let i = 1; i < name.length - 1; i++) {\n    if (!middleCharacters.test(name[i])) {\n      return `Special characters aren't allowed in the name. Invalid character '${name[i]}'.`;\n    }\n  }\n\n  if (!lastCharacter.test(name[name.length - 1])) {\n    return `Special characters aren't allowed in the name. Invalid character '${name[name.length - 1]}'.`;\n  }\n\n  return '';\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/common/regex.spec.js",
    "content": "const { describe, it, expect } = require('@jest/globals');\n\nimport { sanitizeName, validateName } from './regex';\n\ndescribe('regex validators', () => {\n  describe('sanitize name', () => {\n    it('should remove invalid characters', () => {\n      expect(sanitizeName('hello world')).toBe('hello world');\n      expect(sanitizeName('hello-world')).toBe('hello-world');\n      expect(sanitizeName('hello_world')).toBe('hello_world');\n      expect(sanitizeName('hello_world-')).toBe('hello_world-');\n      expect(sanitizeName('hello_world-123')).toBe('hello_world-123');\n      expect(sanitizeName('hello_world-123!@#$%^&*()')).toBe('hello_world-123!@#$%^&-()');\n      expect(sanitizeName('hello_world?')).toBe('hello_world-');\n      expect(sanitizeName('foo/bar/')).toBe('foo-bar-');\n      expect(sanitizeName('foo\\\\bar\\\\')).toBe('foo-bar-');\n    });\n\n    it('should remove leading hyphens', () => {\n      expect(sanitizeName('-foo')).toBe('foo');\n      expect(sanitizeName('---foo')).toBe('foo');\n      expect(sanitizeName('-foo-bar')).toBe('foo-bar');\n    });\n\n    it('should remove trailing periods', () => {\n      expect(sanitizeName('.file')).toBe('.file');\n      expect(sanitizeName('.file.')).toBe('.file');\n      expect(sanitizeName('file.')).toBe('file');\n      expect(sanitizeName('file.name.')).toBe('file.name');\n      expect(sanitizeName('hello world.')).toBe('hello world');\n    });\n\n    it('should handle filenames with only invalid characters', () => {\n      expect(sanitizeName('<>:\"/\\\\|?*')).toBe('');\n      expect(sanitizeName('::::')).toBe('');\n    });\n\n    it('should handle filenames with a mix of valid and invalid characters', () => {\n      expect(sanitizeName('test<>:\"/\\\\|?*')).toBe('test---------');\n      expect(sanitizeName('foo<bar>')).toBe('foo-bar-');\n    });\n\n    it('should remove control characters', () => {\n      expect(sanitizeName('foo\\x00bar')).toBe('foo-bar');\n      expect(sanitizeName('file\\x1Fname')).toBe('file-name');\n    });\n\n    it('should return an empty string if the name is empty or consists only of invalid characters', () => {\n      expect(sanitizeName('')).toBe('');\n      expect(sanitizeName('<>:\"/\\\\|?*')).toBe('');\n    });\n\n    it('should handle filenames with multiple consecutive invalid characters', () => {\n      expect(sanitizeName('foo<<bar')).toBe('foo--bar');\n      expect(sanitizeName('test||name')).toBe('test--name');\n    });\n\n    it('should handle names with spaces only', () => {\n      expect(sanitizeName('     ')).toBe('');\n    });\n\n    it('should handle names with leading/trailing spaces', () => {\n      expect(sanitizeName('  foo bar  ')).toBe('foo bar');\n    });\n\n    it('should preserve valid non-ASCII characters', () => {\n      expect(sanitizeName('brunó')).toBe('brunó');\n      expect(sanitizeName('文件')).toBe('文件');\n      expect(sanitizeName('brunfais')).toBe('brunfais');\n      expect(sanitizeName('brunai')).toBe('brunai');\n      expect(sanitizeName('brunsборка')).toBe('brunsборка');\n      expect(sanitizeName('brunпривет')).toBe('brunпривет');\n      expect(sanitizeName('🐶')).toBe('🐶');\n      expect(sanitizeName('brunfais🐶')).toBe('brunfais🐶');\n      expect(sanitizeName('file-🐶-bruno')).toBe('file-🐶-bruno');\n      expect(sanitizeName('helló')).toBe('helló');\n    });\n\n    it('should preserve case sensitivity', () => {\n      expect(sanitizeName('FileName')).toBe('FileName');\n      expect(sanitizeName('fileNAME')).toBe('fileNAME');\n    });\n\n    it('should handle filenames with multiple consecutive periods (only remove trailing)', () => {\n      expect(sanitizeName('file.name...')).toBe('file.name');\n      expect(sanitizeName('...file')).toBe('...file');\n      expect(sanitizeName('file.name...  ')).toBe('file.name');\n      expect(sanitizeName('  ...file')).toBe('...file');\n      expect(sanitizeName('  ...file   ')).toBe('...file');\n      expect(sanitizeName('  ...file....   ')).toBe('...file');\n    });\n\n    it('should handle very long filenames', () => {\n      const longName = 'a'.repeat(250) + '.txt';\n      expect(sanitizeName(longName)).toBe(longName);\n    });\n\n    it('should handle names with leading/trailing invalid characters', () => {\n      expect(sanitizeName('-foo/bar-')).toBe('foo-bar-');\n      expect(sanitizeName('/foo\\\\bar/')).toBe('foo-bar-');\n    });\n\n    it('should handle different language unicode characters', () => {\n      expect(sanitizeName('你好世界!?@#$%^&*()')).toBe('你好世界!-@#$%^&-()');\n      expect(sanitizeName('こんにちは世界!?@#$%^&*()')).toBe('こんにちは世界!-@#$%^&-()');\n      expect(sanitizeName('안녕하세요 세계!?@#$%^&*()')).toBe('안녕하세요 세계!-@#$%^&-()');\n      expect(sanitizeName('مرحبا بالعالم!?@#$%^&*()')).toBe('مرحبا بالعالم!-@#$%^&-()');\n      expect(sanitizeName('Здравствуй мир!?@#$%^&*()')).toBe('Здравствуй мир!-@#$%^&-()');\n      expect(sanitizeName('नमस्ते दुनिया!?@#$%^&*()')).toBe('नमस्ते दुनिया!-@#$%^&-()');\n      expect(sanitizeName('สวัสดีชาวโลก!?@#$%^&*()')).toBe('สวัสดีชาวโลก!-@#$%^&-()');\n      expect(sanitizeName('γειά σου κόσμος!?@#$%^&*()')).toBe('γειά σου κόσμος!-@#$%^&-()');\n    });\n  });\n});\n\ndescribe('sanitizeName and validateName', () => {\n  it('should sanitize and then validate valid names', () => {\n    const validNames = [\n      'valid_filename.txt',\n      '  valid name ',\n      '   valid-name   ',\n      'valid<>name.txt',\n      'file/with?invalid*chars'\n    ];\n\n    validNames.forEach((name) => {\n      const sanitized = sanitizeName(name);\n      expect(validateName(sanitized)).toBe(true);\n    });\n  });\n\n  it('should sanitize and then validate names with reserved device names', () => {\n    const reservedNames = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'LPT2'];\n\n    reservedNames.forEach((name) => {\n      const sanitized = sanitizeName(name);\n      expect(validateName(sanitized)).toBe(false);\n    });\n  });\n\n  it('should sanitize invalid names to empty strings', () => {\n    const invalidNames = [\n      '  <>:\"/\\\\|?*  ',\n      '   ...   ',\n      '    '\n    ];\n\n    invalidNames.forEach((name) => {\n      const sanitized = sanitizeName(name);\n      expect(validateName(sanitized)).toBe(false);\n    });\n  });\n\n  it('should return false for reserved device names with leading/trailing spaces', () => {\n    const mixedNames = [\n      'AUX   ',\n      '   COM1   '\n    ];\n\n    mixedNames.forEach((name) => {\n      const sanitized = sanitizeName(name);\n      expect(validateName(sanitized)).toBe(false);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/utils/common/setupPolyfills.js",
    "content": "export const setupPolyfills = () => {\n  // polyfill required to make react-pdf\n  if (typeof Promise.withResolvers === 'undefined') {\n    if (typeof window !== 'undefined') {\n      window.Promise.withResolvers = function () {\n        let resolve, reject;\n        const promise = new Promise((res, rej) => {\n          resolve = res;\n          reject = rej;\n        });\n        return { promise, resolve, reject };\n      };\n    } else {\n      global.Promise.withResolvers = function () {\n        let resolve, reject;\n        const promise = new Promise((res, rej) => {\n          resolve = res;\n          reject = rej;\n        });\n        return { promise, resolve, reject };\n      };\n    }\n  }\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/curl/content-type.js",
    "content": "const normalizeContentType = (contentType) => {\n  if (!contentType || typeof contentType !== 'string') {\n    return '';\n  }\n\n  return contentType.toLowerCase();\n};\n\nexport const isNDJsonLikeContentType = (contentType) => {\n  const normalized = normalizeContentType(contentType);\n  return normalized.includes('application/x-ndjson') || normalized.includes('application/ndjson');\n};\n\nexport const isJsonLikeContentType = (contentType) => {\n  const normalized = normalizeContentType(contentType);\n\n  return normalized.includes('application/json') || normalized.includes('+json');\n};\n\nexport const isXmlLikeContentType = (contentType) => {\n  const normalized = normalizeContentType(contentType);\n\n  return normalized.includes('application/xml') || normalized.includes('+xml') || normalized.includes('text/xml');\n};\n\nexport const isPlainTextContentType = (contentType) => {\n  const normalized = normalizeContentType(contentType);\n\n  return normalized.includes('text/plain');\n};\n\nexport const isStructuredContentType = (contentType) => {\n  return isNDJsonLikeContentType(contentType) || isJsonLikeContentType(contentType) || isXmlLikeContentType(contentType) || isPlainTextContentType(contentType);\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/curl/curl-to-json.js",
    "content": "/**\n * Copyright (c) 2014-2016 Nick Carneiro\n * https://github.com/curlconverter/curlconverter\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport parseCurlCommand from './parse-curl';\nimport * as querystring from 'query-string';\nimport * as jsesc from 'jsesc';\nimport { buildQueryString } from '@usebruno/common/utils';\nimport { isStructuredContentType } from './content-type';\n\nfunction getContentType(headers = {}) {\n  const contentType = Object.keys(headers).find((key) => key.toLowerCase() === 'content-type');\n\n  return contentType ? headers[contentType] : null;\n}\n\nfunction repr(value, isKey) {\n  return isKey ? '\\'' + jsesc(value, { quotes: 'single' }) + '\\'' : value;\n}\n\n/**\n * Converts request data to a string based on its content type.\n *\n * @param {Object} request - The request object containing data and headers.\n * @returns {Object} An object containing the data string.\n */\nfunction getDataString(request) {\n  if (typeof request.data === 'number') {\n    request.data = request.data.toString();\n  }\n\n  const contentType = getContentType(request.headers);\n\n  if (isStructuredContentType(contentType)) {\n    return { data: request.data };\n  }\n\n  const parsedQueryString = querystring.parse(request.data, { sort: false });\n  // if missing `=`, `query-string` will set value as `null`. Reset value as empty string ('') here.\n  // https://github.com/sindresorhus/query-string/blob/3d8fbf2328220c06e45f166cdf58e70617c7ee68/base.js#L364-L366\n  Object.keys(parsedQueryString).forEach((key) => {\n    if (parsedQueryString[key] === null) {\n      parsedQueryString[key] = '';\n    }\n  });\n  const keyCount = Object.keys(parsedQueryString).length;\n  const singleKeyOnly = keyCount === 1 && !parsedQueryString[Object.keys(parsedQueryString)[0]];\n  const singularData = request.isDataBinary || singleKeyOnly;\n  if (singularData) {\n    const data = {};\n    data[repr(request.data)] = '';\n    return { data: data };\n  } else {\n    return getMultipleDataString(request, parsedQueryString);\n  }\n}\n\nfunction getMultipleDataString(request, parsedQueryString) {\n  const data = {};\n\n  for (const key in parsedQueryString) {\n    const value = parsedQueryString[key];\n    if (Array.isArray(value)) {\n      data[repr(key)] = value;\n    } else {\n      data[repr(key)] = repr(value);\n    }\n  }\n\n  return { data: data };\n}\n\nfunction getFilesString(request) {\n  const data = {};\n\n  data.data = {};\n\n  if (request.isDataBinary) {\n    let filePath = '';\n\n    if (request.data.startsWith('@')) {\n      filePath = request.data.slice(1);\n    } else {\n      filePath = request.data;\n    }\n\n    data.data = [\n      {\n        filePath: repr(filePath),\n        contentType: request.headers['Content-Type'],\n        selected: true\n      }\n    ];\n\n    return data;\n  }\n\n  data.files = {};\n\n  for (const multipartKey in request.multipartUploads) {\n    const multipartValue = request.multipartUploads[multipartKey];\n    if (multipartValue.startsWith('@')) {\n      const fileName = multipartValue.slice(1);\n      data.files[repr(multipartKey)] = repr(fileName);\n    } else {\n      data.data[repr(multipartKey)] = repr(multipartValue);\n    }\n  }\n\n  if (Object.keys(data.files).length === 0) {\n    delete data.files;\n  }\n\n  if (Object.keys(data.data).length === 0) {\n    delete data.data;\n  }\n\n  return data;\n}\n\nconst curlToJson = (curlCommand) => {\n  const request = parseCurlCommand(curlCommand);\n\n  if (!request?.url) {\n    return null;\n  }\n\n  const requestJson = {};\n\n  // curl automatically prepends 'http' if the scheme is missing, but python fails and returns an error\n  // we tack it on here to mimic curl\n  if (!request.url.match(/https?:/)) {\n    request.url = 'http://' + request.url;\n  }\n  if (!request.urlWithoutQuery.match(/https?:/)) {\n    request.urlWithoutQuery = 'http://' + request.urlWithoutQuery;\n  }\n\n  requestJson.url = request.urlWithoutQuery;\n  requestJson.raw_url = request.url;\n  requestJson.method = request.method;\n  requestJson.isDataBinary = request.isDataBinary;\n\n  if (request.cookies) {\n    const cookies = {};\n    for (const cookieName in request.cookies) {\n      cookies[repr(cookieName)] = repr(request.cookies[cookieName]);\n    }\n\n    requestJson.cookies = cookies;\n  }\n\n  if (request.headers) {\n    const headers = {};\n    for (const headerName in request.headers) {\n      headers[repr(headerName)] = repr(request.headers[headerName]);\n    }\n\n    requestJson.headers = headers;\n  }\n\n  if (request.queries) {\n    requestJson.url = requestJson.url + '?' + buildQueryString(request.queries, { encode: false });\n  }\n\n  if (request.multipartUploads) {\n    requestJson.data = request.multipartUploads;\n    if (!requestJson.headers) {\n      requestJson.headers = {};\n    }\n    requestJson.headers['Content-Type'] = 'multipart/form-data';\n  } else if (request.isDataBinary && (typeof request.data === 'string' && request.data.startsWith('@'))) {\n    Object.assign(requestJson, getFilesString(request)); // file case\n  } else if (typeof request.data === 'string' || typeof request.data === 'number') {\n    Object.assign(requestJson, getDataString(request));\n  }\n\n  if (request.insecure) {\n    requestJson.insecure = false;\n  }\n\n  if (request.auth) {\n    const authMode = request.auth.mode;\n    if (authMode === 'basic') {\n      requestJson.auth = {\n        mode: 'basic',\n        basic: {\n          username: repr(request.auth.basic?.username),\n          password: repr(request.auth.basic?.password)\n        }\n      };\n    } else if (authMode === 'digest') {\n      requestJson.auth = {\n        mode: 'digest',\n        digest: {\n          username: repr(request.auth.digest?.username),\n          password: repr(request.auth.digest?.password)\n        }\n      };\n    } else if (authMode === 'ntlm') {\n      requestJson.auth = {\n        mode: 'ntlm',\n        ntlm: {\n          username: repr(request.auth.ntlm?.username),\n          password: repr(request.auth.ntlm?.password)\n        }\n      };\n    }\n  }\n\n  return Object.keys(requestJson).length ? requestJson : null;\n};\n\nexport default curlToJson;\n"
  },
  {
    "path": "packages/bruno-app/src/utils/curl/curl-to-json.spec.js",
    "content": "const { describe, it, expect } = require('@jest/globals');\n\nimport curlToJson from './curl-to-json';\n\ndescribe('curlToJson', () => {\n  it('should return a parse a simple curl command', () => {\n    const curlCommand = 'curl https://www.usebruno.com';\n    const result = curlToJson(curlCommand);\n\n    expect(result).toEqual({\n      url: 'https://www.usebruno.com',\n      raw_url: 'https://www.usebruno.com',\n      method: 'get'\n    });\n  });\n\n  it('should return a parse a curl command with headers', () => {\n    const curlCommand = `curl https://www.usebruno.com\n    -H 'Accept: application/json, text/plain, */*'\n    -H 'Accept-Language: en-US,en;q=0.9,hi;q=0.8'\n    `;\n\n    const result = curlToJson(curlCommand);\n\n    expect(result).toEqual({\n      url: 'https://www.usebruno.com',\n      raw_url: 'https://www.usebruno.com',\n      method: 'get',\n      headers: {\n        'Accept': 'application/json, text/plain, */*',\n        'Accept-Language': 'en-US,en;q=0.9,hi;q=0.8'\n      }\n    });\n  });\n\n  it('should return a parse a curl with a post body', () => {\n    const curlCommand = `curl 'https://www.usebruno.com'\n    -H 'Accept: application/json, text/plain, */*'\n    -H 'Accept-Language: en-US,en;q=0.9,hi;q=0.8'\n    -H 'Content-Type: application/json;charset=utf-8'\n    -H 'Origin: https://www.usebruno.com'\n    -H 'Referer: https://www.usebruno.com/'\n    --data-raw '{\"email\":\"test@usebruno.com\",\"password\":\"test\"}'\n    `;\n\n    const result = curlToJson(curlCommand);\n\n    expect(result).toEqual({\n      url: 'https://www.usebruno.com',\n      raw_url: 'https://www.usebruno.com',\n      method: 'post',\n      headers: {\n        'Accept': 'application/json, text/plain, */*',\n        'Accept-Language': 'en-US,en;q=0.9,hi;q=0.8',\n        'Content-Type': 'application/json;charset=utf-8',\n        'Origin': 'https://www.usebruno.com',\n        'Referer': 'https://www.usebruno.com/'\n      },\n      data: '{\"email\":\"test@usebruno.com\",\"password\":\"test\"}'\n    });\n  });\n\n  it('should accept escaped curl string', () => {\n    const curlCommand = `curl https://www.usebruno.com\n    -H $'cookie: val_1=\\\\'\\\\'; val_2=\\\\^373:0\\\\^373:0; val_3=\\u0068\\u0065\\u006C\\u006C\\u006F'\n    `;\n    const result = curlToJson(curlCommand);\n\n    expect(result).toEqual({\n      url: 'https://www.usebruno.com',\n      raw_url: 'https://www.usebruno.com',\n      method: 'get',\n      headers: {\n        cookie: 'val_1=\\'\\'; val_2=\\\\^373:0\\\\^373:0; val_3=hello'\n      }\n    });\n  });\n\n  it('should return and parse a simple curl command with a trailing slash', () => {\n    const curlCommand = 'curl https://www.usebruno.com/';\n    const result = curlToJson(curlCommand);\n\n    expect(result).toEqual({\n      url: 'https://www.usebruno.com/',\n      raw_url: 'https://www.usebruno.com/',\n      method: 'get'\n    });\n  });\n\n  it('should return a parse a curl with a post body with binary file type', () => {\n    const curlCommand = `curl 'https://www.usebruno.com'\n    -H 'Accept: application/json, text/plain, */*'\n    -H 'Accept-Language: en-US,en;q=0.9,hi;q=0.8'\n    -H 'Content-Type: application/json;charset=utf-8'\n    -H 'Origin: https://www.usebruno.com'\n    -H 'Referer: https://www.usebruno.com/'\n    --data-binary '@/path/to/file'\n    `;\n\n    const result = curlToJson(curlCommand);\n\n    expect(result).toEqual({\n      url: 'https://www.usebruno.com',\n      raw_url: 'https://www.usebruno.com',\n      method: 'post',\n      headers: {\n        'Accept': 'application/json, text/plain, */*',\n        'Accept-Language': 'en-US,en;q=0.9,hi;q=0.8',\n        'Content-Type': 'application/json;charset=utf-8',\n        'Origin': 'https://www.usebruno.com',\n        'Referer': 'https://www.usebruno.com/'\n      },\n      isDataBinary: true,\n      data: [\n        {\n          filePath: '/path/to/file',\n          contentType: 'application/json;charset=utf-8',\n          selected: true\n        }\n      ]\n    });\n  });\n\n  it('should parse custom json content-types', () => {\n    const curlCommand = `curl 'https://api.example.com/test'\n    -H 'content-type: application/x.custom+json;version=1'\n    --data-raw '{\"test\":\"data\"}'\n    `;\n\n    const result = curlToJson(curlCommand);\n\n    expect(result).toEqual({\n      url: 'https://api.example.com/test',\n      raw_url: 'https://api.example.com/test',\n      method: 'post',\n      headers: {\n        'content-type': 'application/x.custom+json;version=1'\n      },\n      data: '{\"test\":\"data\"}'\n    });\n  });\n\n  it('should parse vendor tree json content-types', () => {\n    const curlCommand = `curl --request POST \\\\\n      --url https://api.example.com/orders/42/preferences \\\\\n      --header 'accept: */*' \\\\\n      --header 'content-type: application/vnd.vendor+json' \\\\\n      --data '{\\\\n  \"data\": {\\\\n    \"type\": \"order-preferences\",\\\\n    \"attributes\": {\\\\n      \"notes\": \"Leave at door\",\\\\n      \"priority\": true\\\\n    }\\\\n  }\\\\n}'`;\n\n    const result = curlToJson(curlCommand);\n    expect(result.data).toContain('\"type\": \"order-preferences\"');\n    expect(result.data).toContain('\"notes\": \"Leave at door\"');\n    expect(result.data).toContain('\"priority\": true');\n    expect(result.headers['content-type']).toBe('application/vnd.vendor+json');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/utils/curl/index.js",
    "content": "import { forOwn } from 'lodash';\nimport curlToJson from './curl-to-json';\nimport { prettifyJsonString } from 'utils/common/index';\nimport { isJsonLikeContentType, isPlainTextContentType, isXmlLikeContentType } from './content-type';\n\nexport const getRequestFromCurlCommand = (curlCommand, requestType = 'http-request') => {\n  const parseFormData = (parsedBody) => {\n    const formData = [];\n    forOwn(parsedBody, (value, key) => {\n      formData.push({ name: key, value, enabled: true });\n    });\n\n    return formData;\n  };\n\n  const parseGraphQL = (text) => {\n    try {\n      const graphql = JSON.parse(text);\n\n      return {\n        query: graphql.query,\n        variables: JSON.stringify(graphql.variables, null, 2)\n      };\n    } catch (e) {\n      return {\n        query: '',\n        variables: ''\n      };\n    }\n  };\n\n  try {\n    if (!curlCommand || typeof curlCommand !== 'string' || curlCommand.length === 0) {\n      return null;\n    }\n\n    const request = curlToJson(curlCommand);\n    if (!request || !request.url) {\n      return null;\n    }\n\n    const parsedHeaders = request?.headers;\n    const headers\n      = parsedHeaders\n        && Object.keys(parsedHeaders).map((key) => ({ name: key, value: parsedHeaders[key], enabled: true }));\n\n    const contentType = headers?.find((h) => h.name.toLowerCase() === 'content-type')?.value;\n    const parsedBody = request.data;\n\n    const body = {\n      mode: 'none',\n      json: null,\n      text: null,\n      xml: null,\n      sparql: null,\n      multipartForm: null,\n      formUrlEncoded: null,\n      graphql: null,\n      file: null\n    };\n\n    if (parsedBody && contentType && typeof contentType === 'string') {\n      const normalizedContentType = contentType.toLowerCase();\n\n      if (requestType === 'graphql-request' && (isJsonLikeContentType(contentType) || normalizedContentType.includes('application/graphql'))) {\n        body.mode = 'graphql';\n        body.graphql = parseGraphQL(parsedBody);\n      } else if (normalizedContentType.includes('application/x-ndjson') || normalizedContentType.includes('application/ndjson')) {\n        body.mode = 'text';\n        body.text = parsedBody;\n      } else if (requestType === 'http-request' && request.isDataBinary) {\n        body.mode = 'file';\n        body.file = parsedBody;\n      } else if (isJsonLikeContentType(contentType)) {\n        body.mode = 'json';\n        body.json = prettifyJsonString(parsedBody);\n      } else if (isXmlLikeContentType(contentType) || normalizedContentType.includes('xml')) {\n        body.mode = 'xml';\n        body.xml = parsedBody;\n      } else if (normalizedContentType.includes('application/x-www-form-urlencoded')) {\n        body.mode = 'formUrlEncoded';\n        body.formUrlEncoded = parseFormData(parsedBody);\n      } else if (normalizedContentType.includes('multipart/form-data')) {\n        body.mode = 'multipartForm';\n        body.multipartForm = parsedBody;\n      } else if (isPlainTextContentType(contentType)) {\n        body.mode = 'text';\n        body.text = parsedBody;\n      }\n    } else if (parsedBody) {\n      body.mode = 'formUrlEncoded';\n      body.formUrlEncoded = parseFormData(parsedBody);\n    }\n\n    return {\n      url: request.url,\n      method: request.method,\n      body,\n      headers: headers,\n      auth: request.auth\n    };\n  } catch (error) {\n    console.error(error);\n    return null;\n  }\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/curl/parse-curl.js",
    "content": "import cookie from 'cookie';\nimport URL from 'url';\nimport { parse } from 'shell-quote';\nimport { isEmpty } from 'lodash';\nimport { parseQueryParams } from '@usebruno/common/utils';\n\n/**\n * Flag definitions - maps flag names to their states and actions\n * State-returning flags expect a value, immediate action flags don't\n */\nconst FLAG_CATEGORIES = {\n  // State-returning flags (expect a value after the flag)\n  'user-agent': ['-A', '--user-agent'],\n  'header': ['-H', '--header'],\n  'data': ['-d', '--data', '--data-ascii', '--data-urlencode'],\n  'json': ['--json'],\n  'user': ['-u', '--user'],\n  'method': ['-X', '--request'],\n  'cookie': ['-b', '--cookie'],\n  'form': ['-F', '--form'],\n  // Special data flags with properties\n  'data-raw': ['--data-raw'],\n  'data-binary': ['--data-binary'],\n\n  // Immediate action flags (no value expected)\n  'head': ['-I', '--head'],\n  'compressed': ['--compressed'],\n  'insecure': ['-k', '--insecure'],\n  'digest': ['--digest'],\n  'ntlm': ['--ntlm'],\n  /**\n   * Query flags: mark data for conversion to query parameters.\n   * While this is an immediate action flag, the actual conversion to a query string occurs later during post-build request processing.\n   * Due to the unpredictable order of flags, query string construction is deferred to the end.\n   */\n  'query': ['-G', '--get']\n};\n\n/**\n * Parse a curl command into a request object\n *\n * @TODO\n * - Handle T (file upload)\n */\nconst parseCurlCommand = (curl) => {\n  const cleanedCommand = cleanCurlCommand(curl);\n  const parsedArgs = parse(cleanedCommand);\n  const request = buildRequest(parsedArgs);\n\n  return cleanRequest(postBuildProcessRequest(request));\n};\n\n/**\n * Build request object by processing parsed arguments\n * Uses a state machine pattern to handle flag-value pairs\n */\nconst buildRequest = (parsedArgs) => {\n  const request = { headers: {} };\n  let currentState = null;\n\n  for (const arg of parsedArgs) {\n    const newState = processArgument(arg, currentState, request);\n    // Reset state after handling a value, or update to new state\n    if (currentState && !newState) {\n      currentState = null;\n    } else if (newState) {\n      currentState = newState;\n    }\n  }\n\n  return request;\n};\n\n/**\n * Process a single argument and return new state if needed\n * State machine: flags set states, values are processed based on current state\n */\nconst processArgument = (arg, currentState, request) => {\n  // Handle flag arguments first (they set states)\n  const flagState = handleFlag(arg, request);\n  if (flagState) {\n    return flagState;\n  }\n\n  // Handle values based on current state (e.g., -H \"value\" where currentState is 'header')\n  if (arg && currentState) {\n    handleValue(arg, currentState, request);\n    return null;\n  }\n\n  // Handle URL detection (only when no current state to avoid conflicts)\n  if (!currentState && isURLOrFragment(arg)) {\n    setURL(request, arg);\n    return null;\n  }\n\n  return null;\n};\n\n/**\n * Handle flag arguments and return new state\n * Determines if flag expects a value or performs immediate action\n */\nconst handleFlag = (arg, request) => {\n  // Find which category this flag belongs to\n  for (const [category, flags] of Object.entries(FLAG_CATEGORIES)) {\n    if (flags.includes(arg)) {\n      return handleFlagCategory(category, arg, request);\n    }\n  }\n\n  return null;\n};\n\n/**\n * Handle flag based on its category\n * Returns state name for flags that expect values, null for immediate actions\n */\nconst handleFlagCategory = (category, arg, request) => {\n  switch (category) {\n    // State-returning flags (return category name to expect value)\n    case 'user-agent':\n    case 'header':\n    case 'data':\n    case 'json':\n    case 'user':\n    case 'method':\n    case 'cookie':\n    case 'form':\n      return category;\n\n    // Special data flags (set properties and return 'data' state)\n    case 'data-raw':\n      request.isDataRaw = true;\n      return 'data';\n\n    case 'data-binary':\n      request.isDataBinary = true;\n      return 'data';\n\n    // Immediate action flags (perform action and return null)\n    case 'head':\n      request.method = 'HEAD';\n      return null;\n\n    case 'compressed':\n      request.headers['Accept-Encoding'] = request.headers['Accept-Encoding'] || 'deflate, gzip';\n      return null;\n\n    case 'insecure':\n      request.insecure = true;\n      return null;\n\n    case 'digest':\n      request.isDigestAuth = true;\n      return null;\n\n    case 'ntlm':\n      request.isNtlmAuth = true;\n      return null;\n\n    case 'query':\n      // set temporary property isQuery to true to indicate that the data should be converted to query string\n      // this is processed later at post build request processing\n      request.isQuery = true;\n      return null;\n\n    default:\n      return null;\n  }\n};\n\n/**\n * Handle values based on the current parsing state\n * Maps state names to their value processing functions\n */\nconst handleValue = (value, state, request) => {\n  const valueHandlers = {\n    'header': () => setHeader(request, value),\n    'user-agent': () => setUserAgent(request, value),\n    'data': () => setData(request, value),\n    'json': () => setJsonData(request, value),\n    'form': () => setFormData(request, value),\n    'user': () => setAuth(request, value),\n    'method': () => setMethod(request, value),\n    'cookie': () => setCookie(request, value)\n  };\n\n  const handler = valueHandlers[state];\n  if (handler) {\n    handler();\n  }\n};\n\n/**\n * Set header from value\n */\nconst setHeader = (request, value) => {\n  const [headerName, headerValue] = value.split(/:\\s*(.+)/);\n  request.headers[headerName] = headerValue;\n};\n\n/**\n * Set user agent\n */\nconst setUserAgent = (request, value) => {\n  request.headers['User-Agent'] = value;\n};\n\n/**\n * Set authentication credentials\n * Stores credentials temporarily for finalization in post-processing\n */\nconst setAuth = (request, value) => {\n  if (typeof value !== 'string') {\n    return;\n  }\n\n  const [username, password] = value.split(':');\n\n  // Store credentials temporarily for finalization in post-processing\n  request.authCredentials = {\n    username: username || '',\n    password: password || ''\n  };\n};\n\n/**\n * Finalize authentication object based on credentials and auth type flags\n */\nconst normalizeAuthProperties = (request) => {\n  if (!request.authCredentials) {\n    delete request.isDigestAuth;\n    delete request.isNtlmAuth;\n    return;\n  }\n\n  const { username, password } = request.authCredentials;\n\n  // Determine auth mode based on flags\n  let mode = 'basic';\n  if (request.isDigestAuth) {\n    mode = 'digest';\n  } else if (request.isNtlmAuth) {\n    mode = 'ntlm';\n  }\n\n  request.auth = {\n    mode: mode,\n    [mode]: { username, password }\n  };\n\n  // Clean up temporary properties\n  delete request.authCredentials;\n  delete request.isDigestAuth;\n  delete request.isNtlmAuth;\n};\n\n/**\n * Set request method\n */\nconst setMethod = (request, value) => {\n  request.method = value.toUpperCase();\n};\n\n/**\n * Set request cookies\n */\nconst setCookie = (request, value) => {\n  if (typeof value !== 'string') {\n    return;\n  }\n\n  const parsedCookies = cookie.parse(value);\n  request.cookies = { ...request.cookies, ...parsedCookies };\n  request.cookieString = request.cookieString ? request.cookieString + '; ' + value : value;\n\n  request.headers['Cookie'] = request.cookieString;\n};\n\n/**\n * Set data (handles multiple -d flags by concatenating with &)\n */\nconst setData = (request, value) => {\n  request.data = request.data ? request.data + '&' + value : value;\n};\n\n/**\n * Set JSON data\n * JSON flag automatically sets Content-Type and converts GET/HEAD to POST\n */\nconst setJsonData = (request, value) => {\n  if (request.method === 'GET' || request.method === 'HEAD') {\n    request.method = 'POST';\n  }\n  request.headers['Content-Type'] = 'application/json';\n  // JSON data replaces existing data (don't append with &)\n  request.data = value;\n};\n\n/**\n * Set form data\n * Form data always sets method to POST and creates multipart uploads\n */\nconst setFormData = (request, value) => {\n  const formArray = Array.isArray(value) ? value : [value];\n  const multipartUploads = [];\n\n  formArray.forEach((field) => {\n    const upload = parseFormField(field);\n    if (upload) {\n      multipartUploads.push(upload);\n    }\n  });\n\n  request.multipartUploads = request.multipartUploads || [];\n  request.multipartUploads.push(...multipartUploads);\n  request.method = 'POST';\n};\n\n/**\n * Parse a single form field\n * Handles text fields, quoted values, and file uploads (@path)\n */\nconst parseFormField = (field) => {\n  const match = field.match(/^([^=]+)=(?:@?\"([^\"]*)\"|@([^@]*)|([^@]*))?$/);\n\n  if (!match) return null;\n\n  const fieldName = match[1];\n  const fieldValue = match[2] || match[3] || match[4] || '';\n  const isFile = field.includes('@');\n\n  return {\n    name: fieldName,\n    value: fieldValue,\n    type: isFile ? 'file' : 'text',\n    enabled: true\n  };\n};\n\n/**\n * Check if argument is a URL or URL fragment\n */\nconst isURLOrFragment = (arg) => {\n  return isURL(arg) || isURLFragment(arg);\n};\n\n/**\n * Check if argument looks like a URL\n */\nconst isURL = (arg) => {\n  if (typeof arg !== 'string') {\n    return false;\n  }\n\n  // First try to parse as a regular URL (with protocol)\n  if (URL.parse(arg || '').host) {\n    return true;\n  }\n\n  // Check if it looks like a domain without protocol\n  // This regex matches domain patterns like:\n  // - example.com\n  // - sub.example.com\n  // - example.com/path\n  // - example.com/path?query=value\n  // Must contain at least one dot to be considered a domain\n  const DOMAIN_PATTERN = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\\/[^\\s]*)?(\\?[^\\s]*)?$/;\n\n  return DOMAIN_PATTERN.test(arg);\n};\n\n/**\n * Check if argument looks like a URL fragment\n * Handles shell-quote operator objects and query parameter patterns\n */\nconst isURLFragment = (arg) => {\n  // If it's a glob pattern that looks like a URL, treat it as a complete URL\n  if (arg && typeof arg === 'object' && arg.op === 'glob') {\n    return isURL(arg.pattern);\n  }\n  if (arg && typeof arg === 'object' && arg.op === '&') {\n    return true;\n  }\n  if (typeof arg === 'string') {\n    // check if arg is a query string containing key=value pair\n    return /^[^=]+=[^&]*$/.test(arg);\n  }\n  return false;\n};\n\n/**\n * Set URL and related properties\n * Handles URL concatenation for shell-quote fragments\n */\nconst setURL = (request, url) => {\n  const urlString = getUrlString(url);\n  if (!urlString) return;\n\n  // Add default protocol if none is present\n  let processedUrl = urlString;\n  if (!request.url && !urlString.match(/^[a-zA-Z]+:\\/\\//)) {\n    processedUrl = 'https://' + urlString;\n  }\n\n  const newUrl = request.url ? request.url + processedUrl : processedUrl;\n\n  const { url: formattedUrl, queries, urlWithoutQuery } = parseUrl(newUrl);\n\n  request.url = formattedUrl;\n  request.urlWithoutQuery = urlWithoutQuery;\n  request.queries = queries;\n};\n\n/**\n * Convert URL fragment to string\n * Handles shell-quote operator objects\n */\nconst getUrlString = (url) => {\n  if (typeof url === 'string') return url;\n  if (url?.op === 'glob') return url.pattern;\n  if (url?.op === '&') return '&';\n  return null;\n};\n\n/**\n * Parse URL\n * Returns formatted URL, URL without query, and queries\n */\nconst parseUrl = (url) => {\n  const parsedUrl = URL.parse(url);\n\n  const queries = parseQueryParams(parsedUrl.query, { decode: false });\n\n  let formattedUrl = URL.format(parsedUrl);\n  if (!url.endsWith('/') && formattedUrl.endsWith('/')) {\n    // Remove trailing slashes if origin url does not have a trailing slash\n    formattedUrl = formattedUrl.slice(0, -1);\n  }\n\n  const urlWithoutQuery = formattedUrl.split('?')[0];\n\n  return {\n    url: formattedUrl,\n    urlWithoutQuery,\n    queries\n  };\n};\n\n/**\n * Convert data to query string\n * Used when -G or --get flag is present to move data from body to URL\n */\nconst convertDataToQueryString = (request) => {\n  let url = request.url;\n\n  if (url.indexOf('?') < 0) {\n    url += '?';\n  } else if (!url.endsWith('&')) {\n    url += '&';\n  }\n\n  // append data to url as query string\n  url += request.data;\n\n  const { url: formattedUrl, queries } = parseUrl(url);\n\n  request.url = formattedUrl;\n  request.queries = queries;\n\n  return request;\n};\n\n/**\n * Post-build processing of request\n * Handles method conversion, query parameter processing, and auth finalization\n */\nconst postBuildProcessRequest = (request) => {\n  if (request.isQuery && request.data) {\n    request = convertDataToQueryString(request);\n    // remove data and isQuery from request as they are no longer needed\n    delete request.data;\n    delete request.isQuery;\n  } else if (request.data) {\n    // if data is present, set method to POST unless the method is explicitly set\n    if (!request.method || request.method === 'HEAD') {\n      request.method = 'POST';\n    }\n  }\n\n  normalizeAuthProperties(request);\n\n  // if method is not set, set it to GET\n  if (!request.method) {\n    request.method = 'GET';\n  }\n\n  // bruno requires method to be lowercase\n  request.method = request.method.toLowerCase();\n\n  return request;\n};\n\n/**\n * Clean up the final request object\n */\nconst cleanRequest = (request) => {\n  if (isEmpty(request.headers)) {\n    delete request.headers;\n  }\n\n  if (isEmpty(request.queries)) {\n    delete request.queries;\n  }\n\n  return request;\n};\n\n/**\n * Clean up curl command\n * Handles escape sequences, line continuations, and method concatenation\n */\nconst cleanCurlCommand = (curlCommand) => {\n  // Handle bash ANSI $'..' escapes by decoding common sequences\n  curlCommand = curlCommand.replace(/\\$'((?:\\\\.|[^'])*)'/g, (match, group) => quoteForShell(decodeAnsiEscapes(group)));\n  // Convert escaped single quotes to shell quote pattern\n  curlCommand = curlCommand.replace(/\\\\'(?!')/g, '\\'\\\\\\'\\'');\n  // Fix concatenated HTTP methods\n  curlCommand = fixConcatenatedMethods(curlCommand);\n\n  return curlCommand.trim();\n};\n\n/**\n * Fix concatenated HTTP methods\n * Eg: Converts -XPOST to -X POST for proper parsing\n */\nconst fixConcatenatedMethods = (command) => {\n  const methodFixes = [\n    { from: / -XPOST/, to: ' -X POST' },\n    { from: / -XGET/, to: ' -X GET' },\n    { from: / -XPUT/, to: ' -X PUT' },\n    { from: / -XPATCH/, to: ' -X PATCH' },\n    { from: / -XDELETE/, to: ' -X DELETE' },\n    { from: / -XOPTIONS/, to: ' -X OPTIONS' },\n    { from: / -XHEAD/, to: ' -X HEAD' },\n    { from: / -Xnull/, to: ' ' }\n  ];\n\n  methodFixes.forEach(({ from, to }) => {\n    command = command.replace(from, to);\n  });\n\n  return command;\n};\n\n/**\n * Decode bash ANSI $'..' escape sequences\n */\nconst decodeAnsiEscapes = (value) => {\n  return value.replace(/\\\\(\\\\|'|n|r|t|v|f|a|b|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4})/g, (match, seq) => {\n    switch (seq[0]) {\n      case '\\\\': return '\\\\';\n      case '\\'': return '\\'';\n      case 'n': return '\\n';\n      case 'r': return '\\r';\n      case 't': return '\\t';\n      case 'v': return '\\v';\n      case 'f': return '\\f';\n      case 'a': return '\\x07';\n      case 'b': return '\\b';\n      case 'x': return String.fromCharCode(parseInt(seq.slice(1), 16));\n      case 'u': return String.fromCharCode(parseInt(seq.slice(1), 16));\n      default: return match;\n    }\n  });\n};\n\n/**\n * Wrap value in single quotes while preserving embedded single quotes\n */\nconst quoteForShell = (value) => {\n  return `'${value.replace(/'/g, '\\'\\\\\\'\\'')}'`;\n};\n\nexport default parseCurlCommand;\n"
  },
  {
    "path": "packages/bruno-app/src/utils/curl/parse-curl.spec.js",
    "content": "const { describe, it, expect } = require('@jest/globals');\nimport parseCurlCommand from './parse-curl';\n\ndescribe('parseCurlCommand', () => {\n  describe('Basic HTTP Methods', () => {\n    it('should parse simple GET request', () => {\n      const result = parseCurlCommand(`\n        curl https://api.example.com/users\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        url: 'https://api.example.com/users',\n        urlWithoutQuery: 'https://api.example.com/users'\n      });\n    });\n\n    it('should parse explicit POST method', () => {\n      const result = parseCurlCommand(`\n        curl -X POST https://api.example.com/users\n      `);\n\n      expect(result).toEqual({\n        method: 'post',\n        url: 'https://api.example.com/users',\n        urlWithoutQuery: 'https://api.example.com/users'\n      });\n    });\n\n    it('should parse PUT method', () => {\n      const result = parseCurlCommand(`\n        curl -X PUT https://api.example.com/users/1\n      `);\n\n      expect(result).toEqual({\n        method: 'put',\n        url: 'https://api.example.com/users/1',\n        urlWithoutQuery: 'https://api.example.com/users/1'\n      });\n    });\n\n    it('should parse DELETE method', () => {\n      const result = parseCurlCommand(`\n        curl -X DELETE https://api.example.com/users/1\n      `);\n\n      expect(result).toEqual({\n        method: 'delete',\n        url: 'https://api.example.com/users/1',\n        urlWithoutQuery: 'https://api.example.com/users/1'\n      });\n    });\n\n    it('should parse HEAD method', () => {\n      const result = parseCurlCommand(`\n        curl -I https://api.example.com/users\n      `);\n\n      expect(result).toEqual({\n        method: 'head',\n        url: 'https://api.example.com/users',\n        urlWithoutQuery: 'https://api.example.com/users'\n      });\n    });\n  });\n\n  describe('Headers', () => {\n    it('should parse single header', () => {\n      const result = parseCurlCommand(`\n        curl --header \"Content-Type: application/json\" https://api.example.com\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        headers: {\n          'Content-Type': 'application/json'\n        },\n        url: 'https://api.example.com',\n        urlWithoutQuery: 'https://api.example.com'\n      });\n    });\n\n    it('should parse single header (no space in header value)', () => {\n      const result = parseCurlCommand(`\n        curl --header \"Content-Type:application/json\" https://api.example.com\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        headers: {\n          'Content-Type': 'application/json'\n        },\n        url: 'https://api.example.com',\n        urlWithoutQuery: 'https://api.example.com'\n      });\n    });\n\n    it('should parse multiple headers', () => {\n      const result = parseCurlCommand(`\n        curl -H \"Content-Type: application/json\" \\\n             -H \"Authorization: Bearer token\" \\\n             https://api.example.com\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        headers: {\n          'Content-Type': 'application/json',\n          'Authorization': 'Bearer token'\n        },\n        url: 'https://api.example.com',\n        urlWithoutQuery: 'https://api.example.com'\n      });\n    });\n\n    it('should parse user-agent header', () => {\n      const result = parseCurlCommand(`\n        curl -A \"Custom User Agent\" https://api.example.com\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        headers: {\n          'User-Agent': 'Custom User Agent'\n        },\n        url: 'https://api.example.com',\n        urlWithoutQuery: 'https://api.example.com'\n      });\n    });\n  });\n\n  describe('Data and Request Body', () => {\n    it('should parse JSON data and change method to POST', () => {\n      const result = parseCurlCommand(`\n        curl -d '{\"name\": \"John\", \"age\": 30}' https://api.example.com/users\n      `);\n\n      expect(result).toEqual({\n        method: 'post',\n        data: '{\"name\": \"John\", \"age\": 30}',\n        url: 'https://api.example.com/users',\n        urlWithoutQuery: 'https://api.example.com/users'\n      });\n    });\n\n    it('should parse post data', () => {\n      const result = parseCurlCommand(`\n        curl --data \"name=John&age=30\" https://api.example.com/users\n      `);\n\n      expect(result).toEqual({\n        method: 'post',\n        data: 'name=John&age=30',\n        url: 'https://api.example.com/users',\n        urlWithoutQuery: 'https://api.example.com/users'\n      });\n    });\n\n    it('should handle multiple data flags', () => {\n      const result = parseCurlCommand(`\n        curl -d \"name=John\" \\\n             -d \"age=30\" \\\n             https://api.example.com/users\n      `);\n\n      expect(result).toEqual({\n        method: 'post',\n        data: 'name=John&age=30',\n        url: 'https://api.example.com/users',\n        urlWithoutQuery: 'https://api.example.com/users'\n      });\n    });\n\n    it('should keep multiline data', () => {\n      const result = parseCurlCommand(`\n        curl -d '{\"key\": \"some long message with line breaks\n\n\n             multiline\"}' \\\n             https://api.example.com/users\n      `);\n\n      expect(result).toEqual({\n        method: 'post',\n        data: `{\"key\": \"some long message with line breaks\n\n\n             multiline\"}`,\n        url: 'https://api.example.com/users',\n        urlWithoutQuery: 'https://api.example.com/users'\n      });\n    });\n\n    it('should keep multi space data', () => {\n      const result = parseCurlCommand(`\n        curl -d '{\"key\": \"some long    spaced     message\"}' \\\n             https://api.example.com/users\n      `);\n\n      expect(result).toEqual({\n        method: 'post',\n        data: '{\"key\": \"some long    spaced     message\"}',\n        url: 'https://api.example.com/users',\n        urlWithoutQuery: 'https://api.example.com/users'\n      });\n    });\n\n    it('should parse binary data flag', () => {\n      const result = parseCurlCommand(`\n        curl --data-binary \"@/path/to/file\" https://api.example.com/upload\n      `);\n\n      expect(result).toEqual({\n        method: 'post',\n        data: '@/path/to/file',\n        isDataBinary: true,\n        url: 'https://api.example.com/upload',\n        urlWithoutQuery: 'https://api.example.com/upload'\n      });\n    });\n\n    it('should parse raw data flag', () => {\n      const result = parseCurlCommand(`\n        curl --data-raw '{\"raw\": \"data\"}' https://api.example.com\n      `);\n\n      expect(result).toEqual({\n        method: 'post',\n        data: '{\"raw\": \"data\"}',\n        isDataRaw: true,\n        url: 'https://api.example.com',\n        urlWithoutQuery: 'https://api.example.com'\n      });\n    });\n  });\n\n  describe('Authentication', () => {\n    it('should parse basic authentication', () => {\n      const result = parseCurlCommand(`\n        curl -u \"username:password\" https://api.example.com\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        auth: {\n          mode: 'basic',\n          basic: {\n            username: 'username',\n            password: 'password'\n          }\n        },\n        url: 'https://api.example.com',\n        urlWithoutQuery: 'https://api.example.com'\n      });\n    });\n\n    it('should handle username without password', () => {\n      const result = parseCurlCommand(`\n        curl --user \"username\" https://api.example.com\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        auth: {\n          mode: 'basic',\n          basic: {\n            username: 'username',\n            password: ''\n          }\n        },\n        url: 'https://api.example.com',\n        urlWithoutQuery: 'https://api.example.com'\n      });\n    });\n\n    it('should parse digest authentication', () => {\n      const result = parseCurlCommand(`\n        curl --digest -u \"myuser:mypass\" https://api.example.com/digest\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        auth: {\n          mode: 'digest',\n          digest: {\n            username: 'myuser',\n            password: 'mypass'\n          }\n        },\n        url: 'https://api.example.com/digest',\n        urlWithoutQuery: 'https://api.example.com/digest'\n      });\n    });\n\n    it('should parse digest authentication with --user flag', () => {\n      const result = parseCurlCommand(`\n        curl --digest --user \"admin:secret\" https://api.example.com/secure\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        auth: {\n          mode: 'digest',\n          digest: {\n            username: 'admin',\n            password: 'secret'\n          }\n        },\n        url: 'https://api.example.com/secure',\n        urlWithoutQuery: 'https://api.example.com/secure'\n      });\n    });\n\n    it('should parse NTLM authentication', () => {\n      const result = parseCurlCommand(`\n        curl --ntlm -u \"myuser:mypass\" https://api.example.com/ntlm\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        auth: {\n          mode: 'ntlm',\n          ntlm: {\n            username: 'myuser',\n            password: 'mypass'\n          }\n        },\n        url: 'https://api.example.com/ntlm',\n        urlWithoutQuery: 'https://api.example.com/ntlm'\n      });\n    });\n\n    it('should parse NTLM authentication with --user flag', () => {\n      const result = parseCurlCommand(`\n        curl --ntlm --user \"domain\\\\username:password\" https://api.example.com/ntlm\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        auth: {\n          mode: 'ntlm',\n          ntlm: {\n            username: 'domain\\\\username',\n            password: 'password'\n          }\n        },\n        url: 'https://api.example.com/ntlm',\n        urlWithoutQuery: 'https://api.example.com/ntlm'\n      });\n    });\n\n    it('should handle digest auth flag before -u flag', () => {\n      const result = parseCurlCommand(`\n        curl -u \"user:pass\" --digest https://api.example.com\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        auth: {\n          mode: 'digest',\n          digest: {\n            username: 'user',\n            password: 'pass'\n          }\n        },\n        url: 'https://api.example.com',\n        urlWithoutQuery: 'https://api.example.com'\n      });\n    });\n  });\n\n  describe('Form Data', () => {\n    it('should parse form data with text fields', () => {\n      const result = parseCurlCommand(`\n        curl -F \"name=John\" \\\n             -F \"age=30\" \\\n             https://api.example.com/users\n      `);\n\n      expect(result).toEqual({\n        method: 'post',\n        multipartUploads: [\n          { name: 'name', value: 'John', type: 'text', enabled: true },\n          { name: 'age', value: '30', type: 'text', enabled: true }\n        ],\n        url: 'https://api.example.com/users',\n        urlWithoutQuery: 'https://api.example.com/users'\n      });\n    });\n\n    it('should parse form data with file uploads', () => {\n      const result = parseCurlCommand(`\n        curl --form \"file=@/path/to/file.txt\" https://api.example.com/upload\n      `);\n\n      expect(result).toEqual({\n        method: 'post',\n        multipartUploads: [\n          { name: 'file', value: '/path/to/file.txt', type: 'file', enabled: true }\n        ],\n        url: 'https://api.example.com/upload',\n        urlWithoutQuery: 'https://api.example.com/upload'\n      });\n    });\n  });\n\n  describe('Cookie', () => {\n    it('should handle cookie flag', () => {\n      const result = parseCurlCommand(`\n        curl -b \"session=abc123\" https://api.example.com\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        headers: {\n          Cookie: 'session=abc123'\n        },\n        cookieString: 'session=abc123',\n        cookies: {\n          session: 'abc123'\n        },\n        url: 'https://api.example.com',\n        urlWithoutQuery: 'https://api.example.com'\n      });\n    });\n\n    it('should handle cookie flag with multiple cookies', () => {\n      const result = parseCurlCommand(`\n        curl -b \"session=abc123; user=john\" https://api.example.com\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        headers: {\n          Cookie: 'session=abc123; user=john'\n        },\n        cookieString: 'session=abc123; user=john',\n        cookies: {\n          session: 'abc123',\n          user: 'john'\n        },\n        url: 'https://api.example.com',\n        urlWithoutQuery: 'https://api.example.com'\n      });\n    });\n\n    it('should handle multiple cookie flags', () => {\n      const result = parseCurlCommand(`\n        curl -b \"session=abc123\" -b \"user=john\" https://api.example.com\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        headers: {\n          Cookie: 'session=abc123; user=john'\n        },\n        cookieString: 'session=abc123; user=john',\n        cookies: {\n          session: 'abc123',\n          user: 'john'\n        },\n        url: 'https://api.example.com',\n        urlWithoutQuery: 'https://api.example.com'\n      });\n    });\n\n    it('should handle complex cookie string', () => {\n      const result = parseCurlCommand(`\n        curl -b \"session=abc123; user=john; path=/; domain=example.com; expires=Thu, 01 Jan 1970 00:00:00 GMT; secure; HttpOnly\" \\\n             https://api.example.com\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        headers: {\n          Cookie: 'session=abc123; user=john; path=/; domain=example.com; expires=Thu, 01 Jan 1970 00:00:00 GMT; secure; HttpOnly'\n        },\n        cookieString: 'session=abc123; user=john; path=/; domain=example.com; expires=Thu, 01 Jan 1970 00:00:00 GMT; secure; HttpOnly',\n        cookies: {\n          session: 'abc123',\n          user: 'john',\n          path: '/',\n          domain: 'example.com',\n          expires: 'Thu, 01 Jan 1970 00:00:00 GMT'\n        },\n        url: 'https://api.example.com',\n        urlWithoutQuery: 'https://api.example.com'\n      });\n    });\n  });\n\n  describe('Shell Quote Handling', () => {\n    it(`should handle shell quote patterns ('\\'' => \\')`, () => {\n      const result = parseCurlCommand(`\n        curl -d '{\"name\": \"John\\'\\\\'\\'s data\"}' https://api.example.com\n      `);\n\n      expect(result).toEqual({\n        method: 'post',\n        data: '{\"name\": \"John\\'s data\"}',\n        url: 'https://api.example.com',\n        urlWithoutQuery: 'https://api.example.com'\n      });\n    });\n\n    it('should handle complex escaped quotes', () => {\n      const result = parseCurlCommand(`\n        curl -d '{\"message\": \"Don\\\\'t stop believing\"}' https://api.example.com\n      `);\n\n      expect(result).toEqual({\n        method: 'post',\n        data: '{\"message\": \"Don\\'t stop believing\"}',\n        url: 'https://api.example.com',\n        urlWithoutQuery: 'https://api.example.com'\n      });\n    });\n  });\n\n  describe('URL Handling', () => {\n    it('should parse URLs with query parameters', () => {\n      const result = parseCurlCommand(`\n        curl https://api.example.com/users?page=1&limit=10&sort=asc\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        queries: [\n          { name: 'page', value: '1' },\n          { name: 'limit', value: '10' },\n          { name: 'sort', value: 'asc' }\n        ],\n        url: 'https://api.example.com/users?page=1&limit=10&sort=asc',\n        urlWithoutQuery: 'https://api.example.com/users'\n      });\n    });\n\n    it('should handle URLs with paths', () => {\n      const result = parseCurlCommand(`\n        curl https://api.example.com/v1/users/123\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        url: 'https://api.example.com/v1/users/123',\n        urlWithoutQuery: 'https://api.example.com/v1/users/123'\n      });\n    });\n  });\n\n  describe('handling URLs without protocols', () => {\n    it('should parse URL without protocol and default to https', () => {\n      const result = parseCurlCommand(`\n        curl echo.usebruno.com\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        url: 'https://echo.usebruno.com',\n        urlWithoutQuery: 'https://echo.usebruno.com'\n      });\n    });\n\n    it('should parse URL without protocol with path and query parameters', () => {\n      const result = parseCurlCommand(`\n        curl api.example.com/users?page=1&limit=10\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        url: 'https://api.example.com/users?page=1&limit=10',\n        urlWithoutQuery: 'https://api.example.com/users',\n        queries: [\n          { name: 'page', value: '1' },\n          { name: 'limit', value: '10' }\n        ]\n      });\n    });\n\n    it('should parse a complex curl command with multiple features and no protocol', () => {\n      const result = parseCurlCommand(`\n        curl -X POST \\\n             -H \"Content-Type: application/json\" \\\n             -H \"Authorization: Bearer token123\" \\\n             -H \"X-Custom-Header: custom header\" \\\n             -d '{\"name\": \"John\\\\'s data\", \"email\": \"john@example.com\", \"message\": \"Don\\\\'t stop believing!\", \"path\": \"/home/user/file.txt\", \"json\": {\"nested\": \"value\", \"array\": [1, 2, 3]}}' \\\n             -u \"api_user:api_pass\" \\\n             --compressed \\\n             api.example.com/v1/users?param1=value1&param2=custom+param\n      `);\n\n      expect(result).toEqual({\n        method: 'post',\n        headers: {\n          'Content-Type': 'application/json',\n          'Authorization': 'Bearer token123',\n          'X-Custom-Header': 'custom header',\n          'Accept-Encoding': 'deflate, gzip'\n        },\n        data: '{\"name\": \"John\\'s data\", \"email\": \"john@example.com\", \"message\": \"Don\\'t stop believing!\", \"path\": \"/home/user/file.txt\", \"json\": {\"nested\": \"value\", \"array\": [1, 2, 3]}}',\n        auth: {\n          mode: 'basic',\n          basic: {\n            username: 'api_user',\n            password: 'api_pass'\n          }\n        },\n        queries: [\n          { name: 'param1', value: 'value1' },\n          { name: 'param2', value: 'custom+param' }\n        ],\n        url: 'https://api.example.com/v1/users?param1=value1&param2=custom+param',\n        urlWithoutQuery: 'https://api.example.com/v1/users'\n      });\n    });\n  });\n\n  describe('Edge Cases', () => {\n    it('should handle compressed flag', () => {\n      const result = parseCurlCommand(`\n        curl --compressed https://api.example.com\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        headers: {\n          'Accept-Encoding': 'deflate, gzip'\n        },\n        url: 'https://api.example.com',\n        urlWithoutQuery: 'https://api.example.com'\n      });\n    });\n\n    it('should handle concatenated HTTP methods', () => {\n      const result = parseCurlCommand(`\n        curl -XPOST https://api.example.com/users\n      `);\n\n      expect(result).toEqual({\n        method: 'post',\n        url: 'https://api.example.com/users',\n        urlWithoutQuery: 'https://api.example.com/users'\n      });\n    });\n\n    it('should handle newlines and continuations', () => {\n      const result = parseCurlCommand(`\n        curl -H \"Content-Type: application/json\" \\\n             -d '{\"name\": \"John\"}' \\\n             https://api.example.com/users\n      `);\n\n      expect(result).toEqual({\n        method: 'post',\n        headers: {\n          'Content-Type': 'application/json'\n        },\n        data: '{\"name\": \"John\"}',\n        url: 'https://api.example.com/users',\n        urlWithoutQuery: 'https://api.example.com/users'\n      });\n    });\n  });\n\n  describe('Complex Examples', () => {\n    it('should parse a complex curl command with multiple features', () => {\n      const result = parseCurlCommand(`\n        curl -X POST \\\n             -H \"Content-Type: application/json\" \\\n             -H \"Authorization: Bearer token123\" \\\n             -H \"X-Custom-Header: custom header\" \\\n             -d '{\"name\": \"John\\\\'s data\", \"email\": \"john@example.com\", \"message\": \"Don\\\\'t stop believing!\", \"path\": \"/home/user/file.txt\", \"json\": {\"nested\": \"value\", \"array\": [1, 2, 3]}}' \\\n             -u \"api_user:api_pass\" \\\n             --compressed \\\n             https://api.example.com/v1/users?param1=value1&param2=custom+param\n      `);\n\n      expect(result).toEqual({\n        method: 'post',\n        headers: {\n          'Content-Type': 'application/json',\n          'Authorization': 'Bearer token123',\n          'X-Custom-Header': 'custom header',\n          'Accept-Encoding': 'deflate, gzip'\n        },\n        data: '{\"name\": \"John\\'s data\", \"email\": \"john@example.com\", \"message\": \"Don\\'t stop believing!\", \"path\": \"/home/user/file.txt\", \"json\": {\"nested\": \"value\", \"array\": [1, 2, 3]}}',\n        auth: {\n          mode: 'basic',\n          basic: {\n            username: 'api_user',\n            password: 'api_pass'\n          }\n        },\n        queries: [\n          { name: 'param1', value: 'value1' },\n          { name: 'param2', value: 'custom+param' }\n        ],\n        url: 'https://api.example.com/v1/users?param1=value1&param2=custom+param',\n        urlWithoutQuery: 'https://api.example.com/v1/users'\n      });\n    });\n  });\n\n  describe('curl command with complex escape characters', () => {\n    it('should parse a curl command with complex escape characters', () => {\n      const result = parseCurlCommand(`\n        curl -X POST \\\n             -H \"Content-Type: application/json\" \\\n             -H \"Authorization: Bearer token123\" \\\n             -d '{\"name\": \"John\\\\'s data\", \"email\": \"john@example.com\"}' \\\n             -u \"api_user:api_pass\" \\\n             --compressed \\\n             https://api.example.com/v1/users\n      `);\n\n      expect(result).toEqual({\n        method: 'post',\n        headers: {\n          'Content-Type': 'application/json',\n          'Authorization': 'Bearer token123',\n          'Accept-Encoding': 'deflate, gzip'\n        },\n        data: '{\"name\": \"John\\'s data\", \"email\": \"john@example.com\"}',\n        auth: {\n          mode: 'basic',\n          basic: {\n            username: 'api_user',\n            password: 'api_pass'\n          }\n        },\n        url: 'https://api.example.com/v1/users',\n        urlWithoutQuery: 'https://api.example.com/v1/users'\n      });\n    });\n  });\n\n  describe('JSON Flag', () => {\n    it('should handle basic JSON request', () => {\n      const result = parseCurlCommand(`\n        curl --json '{\"name\": \"John Doe\", \"email\": \"john@example.com\"}' \\\n             https://api.example.com/users\n      `);\n\n      expect(result).toEqual({\n        method: 'post',\n        headers: {\n          'Content-Type': 'application/json'\n        },\n        data: '{\"name\": \"John Doe\", \"email\": \"john@example.com\"}',\n        url: 'https://api.example.com/users',\n        urlWithoutQuery: 'https://api.example.com/users'\n      });\n    });\n\n    it('should handle JSON with authentication headers', () => {\n      const result = parseCurlCommand(`\n        curl --json '{\"title\": \"New Post\", \"content\": \"Post content\"}' \\\n             -H \"Authorization: Bearer token123\" \\\n             https://api.example.com/posts\n      `);\n\n      expect(result).toEqual({\n        method: 'post',\n        headers: {\n          'Content-Type': 'application/json',\n          'Authorization': 'Bearer token123'\n        },\n        data: '{\"title\": \"New Post\", \"content\": \"Post content\"}',\n        url: 'https://api.example.com/posts',\n        urlWithoutQuery: 'https://api.example.com/posts'\n      });\n    });\n\n    it('should handle complex JSON data', () => {\n      const result = parseCurlCommand(`\n        curl --json '{\"user\": {\"name\": \"Jane\", \"email\": \"jane@example.com\"}, \"metadata\": {\"source\": \"web\"}}' \\\n             https://api.example.com/users\n      `);\n\n      expect(result).toEqual({\n        method: 'post',\n        headers: {\n          'Content-Type': 'application/json'\n        },\n        data: '{\"user\": {\"name\": \"Jane\", \"email\": \"jane@example.com\"}, \"metadata\": {\"source\": \"web\"}}',\n        url: 'https://api.example.com/users',\n        urlWithoutQuery: 'https://api.example.com/users'\n      });\n    });\n\n    it('should handle JSON with escaped quotes', () => {\n      const result = parseCurlCommand(`\n        curl --json '{\"message\": \"Don\\\\'t stop believing!\", \"user\": \"John\\\\'s account\"}' \\\n             https://api.example.com/messages\n      `);\n\n      expect(result).toEqual({\n        method: 'post',\n        headers: {\n          'Content-Type': 'application/json'\n        },\n        data: '{\"message\": \"Don\\'t stop believing!\", \"user\": \"John\\'s account\"}',\n        url: 'https://api.example.com/messages',\n        urlWithoutQuery: 'https://api.example.com/messages'\n      });\n    });\n\n    it('should handle JSON with arrays and nested objects', () => {\n      const result = parseCurlCommand(`\n        curl --json '{\"items\": [{\"id\": 1, \"name\": \"Item 1\"}, {\"id\": 2, \"name\": \"Item 2\"}], \"total\": 2}' \\\n             https://api.example.com/orders\n      `);\n\n      expect(result).toEqual({\n        method: 'post',\n        headers: {\n          'Content-Type': 'application/json'\n        },\n        data: '{\"items\": [{\"id\": 1, \"name\": \"Item 1\"}, {\"id\": 2, \"name\": \"Item 2\"}], \"total\": 2}',\n        url: 'https://api.example.com/orders',\n        urlWithoutQuery: 'https://api.example.com/orders'\n      });\n    });\n\n    it('should handle JSON with custom method', () => {\n      const result = parseCurlCommand(`\n        curl -X PUT \\\n             --json '{\"status\": \"completed\", \"updated_at\": \"2024-01-15T10:30:00Z\"}' \\\n             https://api.example.com/tasks/123\n      `);\n\n      expect(result).toEqual({\n        method: 'put',\n        headers: {\n          'Content-Type': 'application/json'\n        },\n        data: '{\"status\": \"completed\", \"updated_at\": \"2024-01-15T10:30:00Z\"}',\n        url: 'https://api.example.com/tasks/123',\n        urlWithoutQuery: 'https://api.example.com/tasks/123'\n      });\n    });\n  });\n\n  describe('Insecure Flag', () => {\n    it('should handle -k flag', () => {\n      const result = parseCurlCommand(`\n        curl -k https://api.example.com\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        insecure: true,\n        url: 'https://api.example.com',\n        urlWithoutQuery: 'https://api.example.com'\n      });\n    });\n\n    it('should handle --insecure flag', () => {\n      const result = parseCurlCommand(`\n        curl --insecure https://api.example.com\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        insecure: true,\n        url: 'https://api.example.com',\n        urlWithoutQuery: 'https://api.example.com'\n      });\n    });\n  });\n\n  describe('Query Flag', () => {\n    it('should handle -G flag to convert POST data to GET query parameters', () => {\n      const result = parseCurlCommand(`\n        curl -G -d \"name=John\" -d \"age=30\" https://api.example.com/users\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        url: 'https://api.example.com/users?name=John&age=30',\n        urlWithoutQuery: 'https://api.example.com/users',\n        queries: [\n          { name: 'name', value: 'John' },\n          { name: 'age', value: '30' }\n        ]\n      });\n    });\n\n    it('should handle -G flag with --data-urlencode', () => {\n      const result = parseCurlCommand(`\n        curl -G --data-urlencode \"name=John Doe\" \\\n             --data-urlencode \"email=john@example.com\" \\\n             --data-urlencode \"hello\" \\\n             https://api.example.com/users?test=urlquery&hello\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        url: 'https://api.example.com/users?test=urlquery&name=John%20Doe&email=john@example.com&hello',\n        urlWithoutQuery: 'https://api.example.com/users',\n        queries: [\n          { name: 'test', value: 'urlquery' },\n          { name: 'name', value: 'John%20Doe' },\n          { name: 'email', value: 'john@example.com' },\n          { name: 'hello', value: '' }\n        ]\n      });\n    });\n\n    it('should handle -G flag with complex data', () => {\n      const result = parseCurlCommand(`\n        curl -G -d \"search=test+query\" \\\n             -d \"filter=active\" \\\n             -d \"sort=name\" \\\n             -d \"page=1\" \\\n             https://api.example.com/search\n      `);\n\n      expect(result).toEqual({\n        method: 'get',\n        url: 'https://api.example.com/search?search=test+query&filter=active&sort=name&page=1',\n        urlWithoutQuery: 'https://api.example.com/search',\n        queries: [\n          { name: 'search', value: 'test+query' },\n          { name: 'filter', value: 'active' },\n          { name: 'sort', value: 'name' },\n          { name: 'page', value: '1' }\n        ]\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/utils/environments.js",
    "content": "import { uuid } from './common/index';\n\nconst isPersistableEnvVarForMerge = (persistedNames) => (v) => {\n  return !v?.ephemeral || v?.persistedValue !== undefined || (v?.name && persistedNames.has(v.name));\n};\n\nconst toPersistedEnvVarForMerge = (persistedNames) => (v) => {\n  const { ephemeral, persistedValue, ...rest } = v || {};\n  if (v?.ephemeral && persistedValue !== undefined && !(v?.name && persistedNames.has(v.name))) {\n    return { ...rest, value: persistedValue };\n  }\n  return rest;\n};\n\nconst toPersistedEnvVarForSave = (v) => {\n  const { ephemeral, persistedValue, ...rest } = v || {};\n  return v?.ephemeral ? (persistedValue !== undefined ? { ...rest, value: persistedValue } : rest) : rest;\n};\n\n/*\n High-level builder for persisted variables\n - mode 'save': write what the user sees\n - mode 'merge': write only allowed vars (non-ephemeral, ephemerals with persistedValue, or explicitly persisted this run)\n*/\nexport const buildPersistedEnvVariables = (variables, { mode, persistedNames } = {}) => {\n  const src = Array.isArray(variables) ? variables : [];\n  if (mode === 'merge') {\n    const names = persistedNames instanceof Set ? persistedNames : new Set();\n    return src.filter(isPersistableEnvVarForMerge(names)).map(toPersistedEnvVarForMerge(names));\n  }\n  // default to save mode\n  return src.map(toPersistedEnvVarForSave);\n};\n\nexport const buildEnvVariable = ({ envVariable: obj, withUuid = false }) => {\n  let envVariable = {\n    name: obj.name ?? '',\n    value: !!obj.secret ? '' : (obj.value ?? ''),\n    type: 'text',\n    enabled: obj.enabled !== false,\n    secret: !!obj.secret\n  };\n\n  if (!withUuid) {\n    return envVariable;\n  }\n\n  return {\n    uid: uuid(),\n    ...envVariable\n  };\n};\n\n/**\n * Strips the UID from an environment variable for comparison purposes.\n * This is useful when comparing variables where UIDs may differ but the actual data is the same.\n */\nexport const stripEnvVarUid = (variable) => {\n  const { name, value, type, enabled, secret } = variable;\n  return { name, value, type, enabled, secret };\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/exporters/bruno-environment.js",
    "content": "import { buildEnvVariable } from 'utils/environments';\n\nexport const exportBrunoEnvironment = async ({ environments, environmentType, filePath, exportFormat = 'folder' }) => {\n  try {\n    const { ipcRenderer } = window;\n\n    let cleanEnvironments = environments.map((environment) => ({\n      name: environment.name,\n      variables: (environment.variables || []).map((envVariable) => buildEnvVariable({ envVariable })),\n      color: environment.color ?? undefined\n    }));\n\n    await ipcRenderer.invoke('renderer:export-environment', {\n      environments: cleanEnvironments,\n      environmentType,\n      format: 'json',\n      filePath,\n      exportFormat\n    });\n  } catch (error) {\n    console.error(`Error exporting ${environmentType} environment as .json:`, error);\n    throw new Error(`Failed to export ${environmentType} environments.`);\n  }\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/exporters/openapi-spec.js",
    "content": "import jsyaml from 'js-yaml';\nimport { interpolate } from '@usebruno/common';\nimport { isValidUrl } from 'utils/url/index';\nconst xml2js = require('xml2js');\n\nexport const exportApiSpec = ({ variables, items, name, environments }) => {\n  items = items.filter((item) => !['grpc-request'].includes(item.type));\n\n  const components = {\n    schemas: {},\n    requestBodies: {},\n    securitySchemes: {}\n  };\n\n  const servers = [];\n  const warnings = [];\n\n  const addWarning = (message, itemName) => {\n    warnings.push({\n      message,\n      itemName\n    });\n  };\n\n  const templateVarRegex = /\\{\\{([^}]+)\\}\\}/g;\n\n  // Build an OpenAPI server entry from a URL (supports {{var}} templates)\n  const buildServerEntry = (url, vars, description) => {\n    const cleanedUrl = url.endsWith('/') ? url.slice(0, -1) : url;\n    const matches = [...cleanedUrl.matchAll(templateVarRegex)];\n    const entry = {};\n\n    if (matches.length > 0) {\n      entry.url = cleanedUrl.replace(templateVarRegex, '{$1}');\n      const serverVariables = {};\n\n      // each match m is an array where m[0] is the full match (e.g. {{protocol}}) and m[1] is the capture group (e.g. protocol).\n      matches.forEach((m) => {\n        const varName = m[1];\n        serverVariables[varName] = { default: vars[varName] !== undefined ? String(vars[varName]) : '' };\n      });\n      if (Object.keys(serverVariables).length > 0) {\n        entry.variables = serverVariables;\n      }\n    } else {\n      entry.url = cleanedUrl;\n    }\n\n    if (description) entry.description = description;\n    return entry;\n  };\n\n  // Collect all baseUrl sources: collection variables + each environment\n  // Each source becomes a self-contained server entry in the OpenAPI spec.\n  // On import, each server entry maps to a separate Bruno environment.\n  const baseUrlSources = [];\n\n  // Add collection-level baseUrl if present\n  const collectionBaseUrl = variables?.baseUrl || '';\n  if (collectionBaseUrl) {\n    baseUrlSources.push({ baseUrl: collectionBaseUrl, description: 'Base Server', vars: variables });\n  }\n\n  // Add each environment that defines its own baseUrl\n  if (environments && environments.length > 0) {\n    for (const env of environments) {\n      const envVars = getEnabledVarsAsObject(env.variables);\n      if (envVars.baseUrl) {\n        baseUrlSources.push({ baseUrl: envVars.baseUrl, description: env.name, vars: envVars });\n      }\n    }\n  }\n\n  // Build root server entries\n  for (const source of baseUrlSources) {\n    servers.push(buildServerEntry(source.baseUrl, source.vars, source.description));\n  }\n\n  const extractTagFromDepth = (item) => {\n    const { pathname, depth } = item;\n    if (!pathname) return;\n\n    const parts = pathname.split('\\\\');\n    const baseDepth = parts.length - depth;\n    if (depth === 1) return '';\n\n    const tagIndex = Math.max(baseDepth, 0);\n\n    return parts[tagIndex];\n  };\n\n  // Resolve a raw request URL to a path and optional operation-level server override.\n  // Checks for request-level baseUrl overrides (vars.req), then {{baseUrl}} placeholder,\n  // then known baseUrl sources. Falls back to full resolution for unknown URLs.\n  const resolveRequestUrl = (rawUrl, requestVars) => {\n    // Request has a baseUrl override in vars.req — export as operation-level server\n    if (rawUrl.startsWith('{{baseUrl}}') && requestVars) {\n      const baseUrlOverride = requestVars.find((v) => v.name === 'baseUrl' && v.enabled);\n      if (baseUrlOverride) {\n        const reqVarsMap = {};\n        requestVars.filter((v) => v.enabled).forEach((v) => { reqVarsMap[v.name] = v.value; });\n        const path = rawUrl.slice('{{baseUrl}}'.length) || '/';\n        return { url: interpolate(path, reqVarsMap), operationLevelServer: buildServerEntry(baseUrlOverride.value, reqVarsMap) };\n      }\n    }\n\n    // URL uses {{baseUrl}} placeholder — strip it and resolve remaining path\n    if (rawUrl.startsWith('{{baseUrl}}')) {\n      const path = rawUrl.slice('{{baseUrl}}'.length) || '/';\n      return { url: interpolate(path, {}), operationLevelServer: null };\n    }\n\n    // URL matches a known baseUrl value directly (e.g. user typed template vars inline)\n    for (const source of baseUrlSources) {\n      if (rawUrl.startsWith(source.baseUrl)) {\n        const rawPath = rawUrl.slice(source.baseUrl.length);\n        const path = rawPath.startsWith('/') ? rawPath : `/${rawPath}`;\n        return { url: interpolate(path, {}), operationLevelServer: null };\n      }\n    }\n\n    // Unknown URL — resolve fully and add operation-level server override\n    const resolvedUrl = interpolate(rawUrl, variables);\n    if (isValidUrl(resolvedUrl)) {\n      const urlDetails = new URL(resolvedUrl);\n      return { url: urlDetails.pathname, operationLevelServer: buildServerEntry(urlDetails.origin, variables) };\n    }\n\n    return { url: rawUrl, operationLevelServer: null };\n  };\n\n  const generatePaths = () => {\n    const _items = items.map((item) => {\n      const { url, operationLevelServer } = resolveRequestUrl(item?.request?.url || '', item?.request?.vars?.req);\n      const { request } = item;\n      const { method, params = [], headers = [], body, auth } = request || {};\n\n      // PARAMS\n\n      const pathParamsRegex = /(?<!{){([^{}]+)}(?!})/g;\n\n      const pathMatches = url.match(pathParamsRegex) || [];\n\n      // Build known path param names from the params array\n      const knownPathParamNames = new Set(\n        params?.filter((p) => p?.type === 'path').map((p) => p?.name) || []\n      );\n\n      const parameters = [\n        // Query params (exclude path-type params to avoid duplication)\n        ...params?.filter((p) => p?.type !== 'path').map((param) => ({\n          name: param?.name,\n          in: 'query',\n          description: '',\n          required: param?.enabled,\n          example: param?.value\n        })),\n        ...headers?.map((header) => ({\n          name: header?.name,\n          in: 'header',\n          description: '',\n          required: header?.enabled,\n          example: header?.value\n        })),\n        // Path params from the params array (have values from Bruno)\n        ...params?.filter((p) => p?.type === 'path').map((param) => ({\n          name: param?.name,\n          in: 'path',\n          required: true,\n          example: param?.value\n        })),\n        // Path params from URL regex that aren't already in the params array\n        ...pathMatches\n          ?.map((path) => path.slice(1, path.length - 1))\n          .filter((name) => !knownPathParamNames.has(name))\n          .map((name) => ({\n            name,\n            in: 'path',\n            required: true\n          }))\n      ];\n\n      const pathBody = {\n        summary: item?.name,\n        operationId: item?.name,\n        description: '',\n        tags: [extractTagFromDepth(item)],\n        responses: {\n          200: {\n            description: ''\n          }\n        }\n      };\n\n      if (parameters?.length) {\n        pathBody['parameters'] = parameters;\n      }\n\n      // BODY\n\n      let schemaId = `${item?.name?.split(' ').join('_').toLowerCase()}`;\n      let securitySchemaId = `${item?.name?.split(' ').join('_').toLowerCase()}`;\n      let requestBodyId = `${item?.name?.split(' ').join('_').toLowerCase()}`;\n      if (body?.mode) {\n        switch (body?.mode) {\n          case 'json':\n            if (!body?.json) break;\n            try {\n              const parsedJson = JSON.parse(body.json);\n              const schema = generateProperyShape(parsedJson);\n              schema.example = parsedJson;\n              components.schemas[schemaId] = schema;\n              components.requestBodies[requestBodyId] = {\n                content: {\n                  'application/json': {\n                    schema: {\n                      $ref: `#/components/schemas/${schemaId}`\n                    }\n                  }\n                },\n                description: '',\n                required: true\n              };\n              pathBody['requestBody'] = {\n                $ref: `#/components/requestBodies/${requestBodyId}`\n              };\n            } catch (error) {\n              addWarning(`Failed to parse JSON in request body: ${error.message}`, item?.name);\n              components.schemas[schemaId] = {\n                type: 'object',\n                properties: {}\n              };\n              components.requestBodies[requestBodyId] = {\n                content: {\n                  'application/json': {\n                    schema: {\n                      $ref: `#/components/schemas/${schemaId}`\n                    }\n                  }\n                },\n                description: '',\n                required: true\n              };\n              pathBody['requestBody'] = {\n                $ref: `#/components/requestBodies/${requestBodyId}`\n              };\n            }\n            break;\n          case 'xml':\n            if (!body?.xml) break;\n            try {\n              const jsonResult = xmlToJson(body?.xml);\n              if (!jsonResult) {\n                addWarning('Failed to parse XML in request body', item?.name);\n                break;\n              }\n              const xmlSchema = generateProperyShape(jsonResult);\n              xmlSchema.example = jsonResult;\n              components.schemas[schemaId] = xmlSchema;\n              components.requestBodies[requestBodyId] = {\n                content: {\n                  'application/xml': {\n                    schema: {\n                      $ref: `#/components/schemas/${schemaId}`\n                    }\n                  }\n                },\n                description: '',\n                required: true\n              };\n              pathBody['requestBody'] = {\n                $ref: `#/components/requestBodies/${requestBodyId}`\n              };\n            } catch (error) {\n              addWarning(`Failed to parse XML in request body: ${error.message}`, item?.name);\n            }\n            break;\n          case 'multipartForm':\n            if (!body?.multipartForm) break;\n            let multipartFormToKeyValue = body?.multipartForm.reduce((acc, f) => {\n              acc[f?.name] = f.value;\n              return acc;\n            }, {});\n            components.schemas[schemaId] = generateProperyShape(multipartFormToKeyValue);\n            components.requestBodies[requestBodyId] = {\n              content: {\n                'multipart/form-data:': {\n                  schema: {\n                    $ref: `#/components/schemas/${schemaId}`\n                  }\n                }\n              },\n              description: '',\n              required: true\n            };\n            pathBody['requestBody'] = {\n              $ref: `#/components/requestBodies/${requestBodyId}`\n            };\n            break;\n          case 'formUrlEncoded':\n            if (!body?.formUrlEncoded) break;\n            let formUrlEncodedToKeyValue = body?.formUrlEncoded.reduce((acc, f) => {\n              acc[f?.name] = f.value;\n              return acc;\n            }, {});\n            components.schemas[schemaId] = generateProperyShape(formUrlEncodedToKeyValue);\n            components.requestBodies[requestBodyId] = {\n              content: {\n                'application/x-www-form-urlencoded:': {\n                  schema: {\n                    $ref: `#/components/schemas/${schemaId}`\n                  }\n                }\n              },\n              description: '',\n              required: true\n            };\n            pathBody['requestBody'] = {\n              $ref: `#/components/requestBodies/${requestBodyId}`\n            };\n            break;\n          case 'text':\n            if (!body?.text) break;\n            pathBody['requestBody'] = {\n              content: {\n                'text/plain': {\n                  schema: {\n                    type: 'string'\n                  }\n                }\n              }\n            };\n            break;\n          default:\n            break;\n        }\n      }\n\n      // AUTH\n\n      if (auth?.mode) {\n        switch (auth?.mode) {\n          case 'basic':\n            components.securitySchemes[securitySchemaId] = {\n              type: 'http',\n              scheme: 'basic'\n            };\n            pathBody['security'] = {\n              [securitySchemaId]: []\n            };\n            break;\n          case 'bearer':\n            components.securitySchemes[securitySchemaId] = {\n              type: 'http',\n              scheme: 'bearer'\n            };\n            pathBody['security'] = {\n              [securitySchemaId]: []\n            };\n            break;\n          case 'oauth2':\n            if (!auth?.oauth2?.grantType) break;\n            const { authorizationUrl, accessTokenUrl, callbackUrl, scope } = auth?.oauth2;\n            switch (auth?.oauth2?.grantType) {\n              case 'authorization_code':\n                components.securitySchemes[securitySchemaId] = {\n                  type: 'oauth2',\n                  flows: {\n                    authorizationCode: {\n                      authorizationUrl,\n                      tokenUrl: accessTokenUrl,\n                      ...(scope.length > 0\n                        ? {\n                            scopes: {\n                              [scope]: ''\n                            }\n                          }\n                        : {})\n                    }\n                  }\n                };\n                pathBody['security'] = {\n                  [securitySchemaId]: []\n                };\n                break;\n              case 'password':\n                components.securitySchemes[securitySchemaId] = {\n                  type: 'oauth2',\n                  flows: {\n                    password: {\n                      tokenUrl: accessTokenUrl,\n                      ...(scope.length > 0\n                        ? {\n                            scopes: {\n                              [scope]: ''\n                            }\n                          }\n                        : {})\n                    }\n                  }\n                };\n                pathBody['security'] = {\n                  [securitySchemaId]: []\n                };\n                break;\n              case 'client_credentials':\n                components.securitySchemes[securitySchemaId] = {\n                  type: 'oauth2',\n                  flows: {\n                    password: {\n                      tokenUrl: accessTokenUrl,\n                      ...(scope.length > 0\n                        ? {\n                            scopes: {\n                              [scope]: ''\n                            }\n                          }\n                        : {})\n                    }\n                  }\n                };\n                pathBody['security'] = {\n                  [securitySchemaId]: []\n                };\n                break;\n            }\n            break;\n          case 'awsv4':\n            components.securitySchemes[securitySchemaId] = {\n              'type': 'apiKey',\n              'name': 'Authorization',\n              'in': 'header',\n              'x-amazon-apigateway-authtype': 'awsSigv4'\n            };\n            pathBody['security'] = {\n              [securitySchemaId]: []\n            };\n            break;\n          case 'digest':\n            components.securitySchemes[securitySchemaId] = {\n              type: 'digest',\n              scheme: 'digest',\n              description: 'Digest Authentication'\n            };\n            pathBody['security'] = {\n              [securitySchemaId]: []\n            };\n            break;\n          default:\n            break;\n        }\n      }\n\n      return {\n        url,\n        method: method.toLowerCase(),\n        data: pathBody,\n        operationLevelServer\n      };\n    });\n\n    return _items.reduce((acc, item) => {\n      if (!acc[item?.url]) {\n        acc[item?.url] = {};\n      }\n      acc[item?.url][item?.method] = item?.data;\n      // Add operation-level server override inside the operation object (not path-item level)\n      // so the import can read it back from operationObject.servers\n      if (item?.operationLevelServer) {\n        acc[item?.url][item?.method].servers = [item.operationLevelServer];\n      }\n      return acc;\n    }, {});\n  };\n\n  const collectionToExport = {};\n  collectionToExport.openapi = '3.0.0';\n  collectionToExport.info = generateInfoSection(name);\n  collectionToExport.paths = generatePaths();\n  collectionToExport.servers = servers;\n  collectionToExport.components = components;\n\n  let yaml = jsyaml.dump(collectionToExport);\n\n  return {\n    content: yaml,\n    warnings\n  };\n};\n\nconst xmlToJson = (xmlString) => {\n  const parser = new xml2js.Parser({ explicitArray: false, trim: true });\n  let jsonResult = null;\n\n  parser.parseString(xmlString, (err, result) => {\n    if (err) {\n      throw err;\n    } else {\n      jsonResult = result;\n    }\n  });\n\n  return jsonResult;\n};\n\nconst generateInfoSection = (name) => {\n  return {\n    title: name,\n    version: '1.0.0'\n  };\n};\n\n// Convert env variable array to { name: value } object (only enabled vars)\nconst getEnabledVarsAsObject = (variables = []) => {\n  const result = {};\n  variables.forEach((v) => {\n    if (v.name && v.enabled) {\n      result[v.name] = v.value;\n    }\n  });\n  return result;\n};\n\nconst generateProperyShape = (obj) => {\n  let data = {};\n\n  // add 'type'\n  if (Array.isArray(obj)) {\n    data['type'] = 'array';\n    data['items'] = {\n      type: 'string'\n    };\n  } else {\n    data['type'] = typeof obj;\n  }\n\n  // add 'properties'\n  let properties = null;\n  if (obj && typeof obj == 'object') {\n    properties = {};\n    let keys = Object.keys(obj);\n    keys.forEach((key) => {\n      let value = obj[key];\n      properties[key] = generateProperyShape(value);\n    });\n    if (keys.length) {\n      data['properties'] = properties;\n    }\n  }\n  return data;\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/exporters/openapi-spec.spec.js",
    "content": "import { exportApiSpec } from './openapi-spec';\n\n// Mock @usebruno/common to provide a working interpolate function\njest.mock('@usebruno/common', () => ({\n  interpolate: (str, vars) => {\n    if (!str || typeof str !== 'string') return str;\n    let result = str;\n    // Simple recursive interpolation for tests\n    let changed = true;\n    while (changed) {\n      changed = false;\n      result = result.replace(/\\{\\{([^}]+)\\}\\}/g, (match, key) => {\n        if (vars && vars[key] !== undefined) {\n          changed = true;\n          return String(vars[key]);\n        }\n        return match;\n      });\n    }\n    return result;\n  }\n}));\n\ndescribe('exportApiSpec - server variables reconstruction', () => {\n  const makeItems = (urls) =>\n    urls.map((url, i) => ({\n      name: `Request ${i + 1}`,\n      type: 'http-request',\n      request: {\n        url,\n        method: 'GET',\n        params: [],\n        headers: [],\n        body: {},\n        auth: {}\n      }\n    }));\n\n  it('should reconstruct server URL template and variables map when baseUrl has template vars', () => {\n    const variables = {\n      baseUrl: '{{protocol}}://{{host}}:{{port}}/v1',\n      protocol: 'https',\n      host: 'api.example.com',\n      port: '443'\n    };\n    const items = makeItems(['{{baseUrl}}/users', '{{baseUrl}}/items']);\n\n    const { content } = exportApiSpec({ variables, items, name: 'Test API' });\n\n    // Should contain the template server URL\n    expect(content).toContain('url: \\'{protocol}://{host}:{port}/v1\\'');\n\n    // Should contain the variables with defaults (js-yaml may or may not quote values)\n    expect(content).toContain('protocol:');\n    expect(content).toMatch(/default:\\s*'?https'?/);\n    expect(content).toContain('host:');\n    expect(content).toMatch(/default:\\s*api\\.example\\.com/);\n    expect(content).toContain('port:');\n    expect(content).toMatch(/default:\\s*'443'/);\n\n    // Should NOT contain the resolved origin as a separate server\n    expect(content).not.toMatch(/url:\\s*'?https:\\/\\/api\\.example\\.com:443'?/);\n  });\n\n  it('should strip base path from request pathnames to avoid duplication', () => {\n    const variables = {\n      baseUrl: '{{protocol}}://{{host}}/v1',\n      protocol: 'https',\n      host: 'api.example.com'\n    };\n    const items = makeItems(['{{baseUrl}}/users']);\n\n    const { content } = exportApiSpec({ variables, items, name: 'Test API' });\n\n    // Path should be /users, not /v1/users (since server URL already has /v1)\n    expect(content).toContain('/users:');\n    expect(content).not.toMatch(/\\/v1\\/users:/);\n  });\n\n  it('should use plain resolved URL when baseUrl has no template vars', () => {\n    const variables = {\n      baseUrl: 'https://api.example.com/v1'\n    };\n    const items = makeItems(['{{baseUrl}}/users']);\n\n    const { content } = exportApiSpec({ variables, items, name: 'Test API' });\n\n    // Should contain the resolved origin as a plain server\n    expect(content).toMatch(/url:\\s*'?https:\\/\\/api\\.example\\.com'?/);\n\n    // Should NOT contain template syntax\n    expect(content).not.toContain('{protocol}');\n    expect(content).not.toContain('variables:');\n  });\n\n  it('should add non-baseUrl servers as operation-level overrides, not root servers', () => {\n    const variables = {\n      baseUrl: '{{protocol}}://{{host}}/v1',\n      protocol: 'https',\n      host: 'api.example.com'\n    };\n    // Mix of baseUrl requests and a direct URL request\n    const items = makeItems(['{{baseUrl}}/users', 'https://other-api.com/data']);\n\n    const { content } = exportApiSpec({ variables, items, name: 'Test API' });\n\n    // Template server should be in root servers\n    expect(content).toContain('url: \\'{protocol}://{host}/v1\\'');\n\n    // Other server should appear as an operation-level override under /data\n    expect(content).toContain('/data:');\n\n    // Root servers should only contain the baseUrl server\n    const parsed = require('js-yaml').load(content);\n    expect(parsed.servers).toHaveLength(1);\n    expect(parsed.servers[0].url).toBe('{protocol}://{host}/v1');\n\n    // The operation-level server should be inside the GET operation\n    expect(parsed.paths['/data'].get.servers).toHaveLength(1);\n    expect(parsed.paths['/data'].get.servers[0].url).toBe('https://other-api.com');\n  });\n\n  it('should export request-level baseUrl override as a path-level server', () => {\n    const variables = {\n      baseUrl: 'https://api.example.com/v1'\n    };\n    const items = [\n      {\n        name: 'Get users',\n        type: 'http-request',\n        request: {\n          url: '{{baseUrl}}/users',\n          method: 'GET',\n          params: [],\n          headers: [],\n          body: {},\n          auth: {}\n        }\n      },\n      {\n        name: 'Get files',\n        type: 'http-request',\n        request: {\n          url: '{{baseUrl}}/files',\n          method: 'GET',\n          params: [],\n          headers: [],\n          body: {},\n          auth: {},\n          vars: {\n            req: [\n              { name: 'baseUrl', value: 'https://files.example.com', enabled: true }\n            ]\n          }\n        }\n      }\n    ];\n\n    const { content } = exportApiSpec({ variables, items, name: 'Test API' });\n    const parsed = require('js-yaml').load(content);\n\n    // Root servers should only have the collection baseUrl\n    expect(parsed.servers).toHaveLength(1);\n    expect(parsed.servers[0].url).toBe('https://api.example.com/v1');\n\n    // /users GET should NOT have an operation-level server override\n    expect(parsed.paths['/users'].get.servers).toBeUndefined();\n\n    // /files GET should have an operation-level server override\n    expect(parsed.paths['/files'].get.servers).toHaveLength(1);\n    expect(parsed.paths['/files'].get.servers[0].url).toBe('https://files.example.com');\n  });\n\n  it('should export request-level baseUrl override with template variables', () => {\n    const variables = {\n      baseUrl: 'https://api.example.com/v1'\n    };\n    const items = [\n      {\n        name: 'Regional data',\n        type: 'http-request',\n        request: {\n          url: '{{baseUrl}}/data',\n          method: 'GET',\n          params: [],\n          headers: [],\n          body: {},\n          auth: {},\n          vars: {\n            req: [\n              { name: 'baseUrl', value: '{{protocol}}://{{region}}.example.com/v2', enabled: true },\n              { name: 'protocol', value: 'https', enabled: true },\n              { name: 'region', value: 'us-east', enabled: true }\n            ]\n          }\n        }\n      }\n    ];\n\n    const { content } = exportApiSpec({ variables, items, name: 'Test API' });\n    const parsed = require('js-yaml').load(content);\n\n    // Operation-level server should have template URL with variables\n    const pathServers = parsed.paths['/data'].get.servers;\n    expect(pathServers).toHaveLength(1);\n    expect(pathServers[0].url).toBe('{protocol}://{region}.example.com/v2');\n    expect(pathServers[0].variables.protocol.default).toBe('https');\n    expect(pathServers[0].variables.region.default).toBe('us-east');\n  });\n});\n\ndescribe('exportApiSpec - parameter and body value preservation', () => {\n  it('should export path parameter values from params array', () => {\n    const variables = { baseUrl: 'https://api.example.com' };\n    const items = [{\n      name: 'Get user',\n      type: 'http-request',\n      request: {\n        url: '{{baseUrl}}/users/{userId}',\n        method: 'GET',\n        params: [\n          { name: 'userId', value: '123', type: 'path', enabled: true },\n          { name: 'include', value: 'profile', type: 'query', enabled: true }\n        ],\n        headers: [], body: {}, auth: {}\n      }\n    }];\n    const { content } = exportApiSpec({ variables, items, name: 'Test' });\n    const parsed = require('js-yaml').load(content);\n    const params = parsed.paths['/users/{userId}'].get.parameters;\n\n    const pathParam = params.find((p) => p.in === 'path');\n    expect(pathParam.name).toBe('userId');\n    expect(pathParam.example).toBe('123');\n\n    const queryParam = params.find((p) => p.in === 'query');\n    expect(queryParam.name).toBe('include');\n    expect(queryParam.example).toBe('profile');\n  });\n\n  it('should not export path-type params as query params', () => {\n    const variables = { baseUrl: 'https://api.example.com' };\n    const items = [{\n      name: 'Get user',\n      type: 'http-request',\n      request: {\n        url: '{{baseUrl}}/users/{userId}',\n        method: 'GET',\n        params: [\n          { name: 'userId', value: '123', type: 'path', enabled: true }\n        ],\n        headers: [], body: {}, auth: {}\n      }\n    }];\n    const { content } = exportApiSpec({ variables, items, name: 'Test' });\n    const parsed = require('js-yaml').load(content);\n    const params = parsed.paths['/users/{userId}'].get.parameters;\n\n    const queryParams = params.filter((p) => p.in === 'query');\n    expect(queryParams).toHaveLength(0);\n\n    const pathParams = params.filter((p) => p.in === 'path');\n    expect(pathParams).toHaveLength(1);\n    expect(pathParams[0].name).toBe('userId');\n  });\n\n  it('should fall back to URL regex for path params not in params array', () => {\n    const variables = { baseUrl: 'https://api.example.com' };\n    const items = [{\n      name: 'Get user',\n      type: 'http-request',\n      request: {\n        url: '{{baseUrl}}/users/{userId}',\n        method: 'GET',\n        params: [],\n        headers: [], body: {}, auth: {}\n      }\n    }];\n    const { content } = exportApiSpec({ variables, items, name: 'Test' });\n    const parsed = require('js-yaml').load(content);\n    const params = parsed.paths['/users/{userId}'].get.parameters;\n\n    const pathParams = params.filter((p) => p.in === 'path');\n    expect(pathParams).toHaveLength(1);\n    expect(pathParams[0].name).toBe('userId');\n    expect(pathParams[0].example).toBeUndefined();\n  });\n\n  it('should preserve JSON body example for round-trip', () => {\n    const variables = { baseUrl: 'https://api.example.com' };\n    const items = [{\n      name: 'Create user',\n      type: 'http-request',\n      request: {\n        url: '{{baseUrl}}/users',\n        method: 'POST',\n        params: [], headers: [],\n        body: { mode: 'json', json: '{\"name\":\"John\",\"age\":30}' },\n        auth: {}\n      }\n    }];\n    const { content } = exportApiSpec({ variables, items, name: 'Test' });\n    const parsed = require('js-yaml').load(content);\n    const schema = parsed.components.schemas.create_user;\n\n    expect(schema.example).toEqual({ name: 'John', age: 30 });\n    expect(schema.properties.name.type).toBe('string');\n    expect(schema.properties.age.type).toBe('number');\n  });\n});\n\ndescribe('exportApiSpec - multi-environment servers', () => {\n  const makeItems = (urls) =>\n    urls.map((url, i) => ({\n      name: `Request ${i + 1}`,\n      type: 'http-request',\n      request: {\n        url,\n        method: 'GET',\n        params: [],\n        headers: [],\n        body: {},\n        auth: {}\n      }\n    }));\n\n  const makeEnv = (name, vars) => ({\n    uid: name.toLowerCase(),\n    name,\n    variables: Object.entries(vars).map(([k, v]) => ({\n      uid: `${name}-${k}`,\n      name: k,\n      value: v,\n      enabled: true,\n      type: 'text',\n      secret: false\n    }))\n  });\n\n  it('should create a server entry per environment when baseUrl has template vars', () => {\n    const variables = {};\n    const environments = [\n      makeEnv('Production', { baseUrl: '{{protocol}}://{{host}}/v1', protocol: 'https', host: 'api.prod.com' }),\n      makeEnv('Staging', { baseUrl: '{{protocol}}://{{host}}/v1', protocol: 'https', host: 'api.staging.com' })\n    ];\n    const items = makeItems(['{{baseUrl}}/users']);\n\n    const { content } = exportApiSpec({ variables, items, name: 'Test API', environments });\n\n    // Both environments should appear as servers\n    expect(content).toContain('description: Production');\n    expect(content).toContain('description: Staging');\n\n    // Template URL should be used\n    expect(content).toContain('url: \\'{protocol}://{host}/v1\\'');\n\n    // Production vars\n    expect(content).toContain('api.prod.com');\n    // Staging vars\n    expect(content).toContain('api.staging.com');\n  });\n\n  it('should create a server entry per environment when baseUrl is a plain URL in each env', () => {\n    const variables = {};\n    const environments = [\n      makeEnv('Production', { baseUrl: 'https://api.prod.com/v1' }),\n      makeEnv('Staging', { baseUrl: 'https://api.staging.com/v1' })\n    ];\n    const items = makeItems(['{{baseUrl}}/users']);\n\n    const { content } = exportApiSpec({ variables, items, name: 'Test API', environments });\n\n    // Both servers should appear\n    expect(content).toMatch(/url:\\s*'?https:\\/\\/api\\.prod\\.com\\/v1'?/);\n    expect(content).toMatch(/url:\\s*'?https:\\/\\/api\\.staging\\.com\\/v1'?/);\n\n    // Descriptions should match env names\n    expect(content).toContain('description: Production');\n    expect(content).toContain('description: Staging');\n\n    // No OpenAPI template variable syntax in servers section\n    expect(content).not.toMatch(/url:\\s*'?\\{baseUrl\\}'?/);\n  });\n\n  it('should use collection variables baseUrl when no environments are passed', () => {\n    const variables = {\n      baseUrl: '{{protocol}}://{{host}}/v1',\n      protocol: 'https',\n      host: 'api.example.com'\n    };\n    const items = makeItems(['{{baseUrl}}/users']);\n\n    const { content } = exportApiSpec({ variables, items, name: 'Test API' });\n\n    // Should use collection baseUrl as template\n    expect(content).toContain('url: \\'{protocol}://{host}/v1\\'');\n    expect(content).toMatch(/default:\\s*'?https'?/);\n    expect(content).toContain('api.example.com');\n    expect(content).toContain('description: Base Server');\n  });\n\n  it('should include both collection and env baseUrl as separate servers', () => {\n    const variables = {\n      baseUrl: '{{protocol}}://{{host}}/v1',\n      protocol: 'https',\n      host: 'api.default.com'\n    };\n    const environments = [\n      makeEnv('Production', { baseUrl: 'https://api.prod.com/v1' }),\n      makeEnv('Staging', { protocol: 'http', host: 'localhost' })\n    ];\n    const items = makeItems(['{{baseUrl}}/users']);\n\n    const { content } = exportApiSpec({ variables, items, name: 'Test API', environments });\n\n    // Collection template server should be present\n    expect(content).toContain('url: \\'{protocol}://{host}/v1\\'');\n    expect(content).toContain('description: Base Server');\n\n    // Production's plain URL override should also be present\n    expect(content).toMatch(/url:\\s*'?https:\\/\\/api\\.prod\\.com\\/v1'?/);\n    expect(content).toContain('description: Production');\n\n    // Staging doesn't define baseUrl — no separate server entry\n    expect(content).not.toContain('description: Staging');\n  });\n\n  it('should export both collection and env even when baseUrl resolves to the same value', () => {\n    const variables = {\n      baseUrl: 'https://api.example.com/v1'\n    };\n    const environments = [\n      makeEnv('Production', { baseUrl: 'https://api.example.com/v1' })\n    ];\n    const items = makeItems(['{{baseUrl}}/users']);\n\n    const { content } = exportApiSpec({ variables, items, name: 'Test API', environments });\n\n    // Both should appear as separate server entries\n    expect(content).toContain('description: Base Server');\n    expect(content).toContain('description: Production');\n  });\n\n  it('should skip environments that do not define baseUrl', () => {\n    const variables = {\n      baseUrl: '{{host}}/api'\n    };\n    const environments = [\n      makeEnv('Production', { host: 'https://api.prod.com', baseUrl: 'https://api.prod.com/api' }),\n      { uid: 'empty', name: 'Empty', variables: [] }\n    ];\n    const items = makeItems(['{{baseUrl}}/users']);\n\n    const { content } = exportApiSpec({ variables, items, name: 'Test API', environments });\n\n    // Collection template and Production should have server entries\n    expect(content).toContain('description: Base Server');\n    expect(content).toContain('description: Production');\n\n    // Empty env has no baseUrl — no server entry\n    expect(content).not.toContain('description: Empty');\n  });\n\n  it('should export both envs even when baseUrl is identical', () => {\n    const variables = {};\n    const environments = [\n      makeEnv('Production', { baseUrl: 'https://api.example.com/v1' }),\n      makeEnv('Staging', { baseUrl: 'https://api.example.com/v1' })\n    ];\n    const items = makeItems(['{{baseUrl}}/users']);\n\n    const { content } = exportApiSpec({ variables, items, name: 'Test API', environments });\n\n    // Both should appear as separate server entries (each maps to a Bruno environment on import)\n    expect(content).toContain('description: Production');\n    expect(content).toContain('description: Staging');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/utils/exporters/opencollection.js",
    "content": "import * as FileSaver from 'file-saver';\nimport jsyaml from 'js-yaml';\nimport { brunoToOpenCollection } from '@usebruno/converters';\nimport { sanitizeName } from 'utils/common/regex';\nimport { filterTransientItems } from 'utils/collections';\n\nexport const exportCollection = (collection, version) => {\n  // Filter out transient items before export\n  collection.items = filterTransientItems(collection.items);\n\n  const openCollection = brunoToOpenCollection(collection);\n\n  if (!openCollection.extensions) {\n    openCollection.extensions = {};\n  }\n  if (!openCollection.extensions.bruno) {\n    openCollection.extensions.bruno = {};\n  }\n  openCollection.extensions.bruno.exportedAt = new Date().toISOString();\n  openCollection.extensions.bruno.exportedUsing = version ? `Bruno/${version}` : 'Bruno';\n\n  const yamlContent = jsyaml.dump(openCollection, {\n    indent: 2,\n    lineWidth: -1,\n    noRefs: true,\n    sortKeys: false\n  });\n\n  const sanitizedName = sanitizeName(collection.name);\n  const fileName = `${sanitizedName}.yml`;\n  const fileBlob = new Blob([yamlContent], { type: 'application/x-yaml' });\n\n  FileSaver.saveAs(fileBlob, fileName);\n};\n\nexport default exportCollection;\n"
  },
  {
    "path": "packages/bruno-app/src/utils/exporters/postman-collection.js",
    "content": "import * as FileSaver from 'file-saver';\nimport { brunoToPostman } from '@usebruno/converters';\nimport { filterTransientItems } from 'utils/collections';\n\nexport const exportCollection = (collection) => {\n  // Filter out transient items before export\n  collection.items = filterTransientItems(collection.items);\n\n  const collectionToExport = brunoToPostman(collection);\n\n  const fileName = `${collection.name}.json`;\n  const fileBlob = new Blob([JSON.stringify(collectionToExport, null, 2)], { type: 'application/json' });\n\n  FileSaver.saveAs(fileBlob, fileName);\n};\n\nexport default exportCollection;\n"
  },
  {
    "path": "packages/bruno-app/src/utils/filesystem.js",
    "content": "/**\n * Filesystem utilities for the renderer process\n * These functions communicate with the main process via IPC\n */\n\n/**\n * Check if a file exists\n * @param {string} filePath - The file path to check\n * @returns {Promise<boolean>} - True if file exists, false otherwise\n */\nexport const existsSync = async (filePath) => {\n  return await window.ipcRenderer.invoke('renderer:exists-sync', filePath);\n};\n\n/**\n * Resolve a relative path against a base path\n * @param {string} relativePath - The relative path to resolve\n * @param {string} basePath - The base path to resolve against\n * @returns {Promise<string>} - The resolved absolute path\n */\nexport const resolvePath = async (relativePath, basePath) => {\n  return await window.ipcRenderer.invoke('renderer:resolve-path', relativePath, basePath);\n};\n\nexport const browseDirectory = async (pathname) => {\n  return await window.ipcRenderer.invoke('renderer:browse-directory', pathname);\n};\n\n/**\n * Check if a path is a directory\n * @param {string} dirPath - The directory path to check\n * @returns {Promise<boolean>} - True if path is a directory, false otherwise\n */\nexport const isDirectory = async (dirPath) => {\n  return await window.ipcRenderer.invoke('renderer:is-directory', dirPath);\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/git/index.js",
    "content": "import gitUrlParse from 'git-url-parse';\n\nconst isGitUrl = (str) => {\n  try {\n    const parsed = gitUrlParse(str);\n\n    if (!parsed) {\n      return false;\n    }\n\n    // Validate that it has the essential parts of a git URL and uses valid protocols\n    const validProtocols = ['git', 'ssh', 'http', 'https'];\n    return !!(\n      parsed\n      && parsed.owner\n      && parsed.source\n      && validProtocols.includes(parsed.protocol)\n    );\n  } catch (error) {\n    return false;\n  }\n};\n\nexport const getRepoNameFromUrl = (url) => {\n  try {\n    const parsedUrl = gitUrlParse(url);\n    return parsedUrl.name;\n  } catch (error) {\n    throw new Error('Invalid Git URL');\n  }\n};\n\nexport const containsGitHubToken = (remoteUrl) => {\n  const GITHUB_TOKEN_REGEX = /(ghp_|gho_|ghu_|ghs_|ghr_)[A-Za-z0-9_]{30,}/;\n  return GITHUB_TOKEN_REGEX.test(remoteUrl);\n};\n\nexport const getSafeGitRemoteUrls = (remotes = []) => {\n  const remoteUrls = remotes\n    ?.map((remote) => remote?.refs?.fetch)\n    ?.filter((url) => typeof url === 'string' && url?.trim()?.length > 0);\n\n  const safeRemoteUrls = remoteUrls\n    ?.filter((remoteUrl) => !containsGitHubToken(remoteUrl));\n  return safeRemoteUrls || [];\n};\n\nexport const isGitRepositoryUrl = (url) => {\n  try {\n    if (!url || typeof url !== 'string') {\n      return false;\n    }\n\n    // First try the URL as-is\n    if (isGitUrl(url)) {\n      return true;\n    }\n\n    return false;\n  } catch {\n    return false;\n  }\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/git/index.spec.js",
    "content": "import { containsGitHubToken, getSafeGitRemoteUrls, isGitRepositoryUrl } from './index';\n\ndescribe('containsGitHubToken', () => {\n  test('should return true for a URL containing a GitHub token', () => {\n    expect(containsGitHubToken('https://ghp_abcdefgh1234567890abcdefgh12345678@github.com'))\n      .toBe(true);\n  });\n\n  test('should return false for a URL without a GitHub token', () => {\n    expect(containsGitHubToken('https://github.com/user/repo.git'))\n      .toBe(false);\n  });\n\n  test('should return false for an empty string', () => {\n    expect(containsGitHubToken(''))\n      .toBe(false);\n  });\n\n  test('should return false for a null value', () => {\n    expect(containsGitHubToken(null))\n      .toBe(false);\n  });\n\n  test('should return false for a URL with a similar but invalid token', () => {\n    expect(containsGitHubToken('https://ghz_abcdefgh1234567890@github.com'))\n      .toBe(false);\n  });\n});\n\ndescribe('getSafeGitRemoteUrls', () => {\n  test('should filter out URLs containing GitHub tokens', () => {\n    const remotes = [\n      { refs: { fetch: 'https://ghp_abcdefgh1234567890abcdefgh12345678@github.com' } },\n      { refs: { fetch: 'https://github.com/user/repo.git' } },\n      { refs: { fetch: 'git@github.com:user/repo.git' } }\n    ];\n    expect(getSafeGitRemoteUrls(remotes)).toEqual([\n      'https://github.com/user/repo.git',\n      'git@github.com:user/repo.git'\n    ]);\n  });\n\n  test('should return an empty array if all URLs contain GitHub tokens', () => {\n    const remotes = [\n      { refs: { fetch: 'https://ghp_abcdefgh1234567890abcdefgh12345678@github.com' } },\n      { refs: { fetch: 'https://gho_abcdefgh1234567890abcdefgh12345678@github.com' } }\n    ];\n    expect(getSafeGitRemoteUrls(remotes)).toEqual([]);\n  });\n\n  test('should return an empty array if no valid URLs are present', () => {\n    const remotes = [\n      { refs: { fetch: '' } },\n      { refs: { fetch: null } },\n      { refs: { fetch: undefined } }\n    ];\n    expect(getSafeGitRemoteUrls(remotes)).toEqual([]);\n  });\n\n  test('should return an empty array if input is null or undefined', () => {\n    expect(getSafeGitRemoteUrls(null)).toEqual([]);\n    expect(getSafeGitRemoteUrls(undefined)).toEqual([]);\n  });\n\n  test('should ignore remotes with no fetch property', () => {\n    const remotes = [\n      { refs: {} },\n      {}\n    ];\n    expect(getSafeGitRemoteUrls(remotes)).toEqual([]);\n  });\n});\n\ndescribe('isGitRepositoryUrl', () => {\n  test('should return true for valid HTTPS GitHub URLs', () => {\n    expect(isGitRepositoryUrl('https://github.com/user/repo.git')).toBe(true);\n    expect(isGitRepositoryUrl('https://github.com/user/repo')).toBe(true); // automatically adds .git suffix\n  });\n\n  test('should return true for valid SSH GitHub URLs', () => {\n    expect(isGitRepositoryUrl('git@github.com:user/repo.git')).toBe(true);\n  });\n\n  test('should return true for custom Git server URLs', () => {\n    expect(isGitRepositoryUrl('https://git.example.com/user/repo.git')).toBe(true);\n    expect(isGitRepositoryUrl('git@git.example.com:user/repo.git')).toBe(true);\n  });\n\n  test('should return false for invalid URLs', () => {\n    expect(isGitRepositoryUrl('')).toBe(false);\n    expect(isGitRepositoryUrl('not-a-url')).toBe(false);\n    expect(isGitRepositoryUrl('https://example.com')).toBe(false);\n    expect(isGitRepositoryUrl('ftp://github.com/user/repo.git')).toBe(false);\n  });\n\n  test('should return true for HTTPS URLs without .git suffix for valid Git hosts', () => {\n    expect(isGitRepositoryUrl('https://github.com/user/repo')).toBe(true);\n    expect(isGitRepositoryUrl('https://gitlab.com/user/repo')).toBe(true);\n    expect(isGitRepositoryUrl('https://bitbucket.org/user/repo')).toBe(true);\n  });\n\n  test('should return false for null or undefined', () => {\n    expect(isGitRepositoryUrl(null)).toBe(false);\n    expect(isGitRepositoryUrl(undefined)).toBe(false);\n  });\n\n  test('should handle malformed URLs gracefully', () => {\n    expect(isGitRepositoryUrl('https://')).toBe(false);\n    expect(isGitRepositoryUrl('git@')).toBe(false);\n    expect(isGitRepositoryUrl('://invalid')).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/utils/idb/index.js",
    "content": "export const saveCollectionToIdb = (connection, collection) => {\n  return new Promise((resolve, reject) => {\n    connection\n      .then((db) => {\n        let tx = db.transaction(`collection`, 'readwrite');\n        let collectionStore = tx.objectStore('collection');\n\n        collectionStore.put(collection);\n\n        resolve(collection);\n      })\n      .catch((err) => reject(err));\n  });\n};\n\nexport const getCollectionsFromIdb = (connection) => {\n  return new Promise((resolve, reject) => {\n    connection\n      .then((db) => {\n        let tx = db.transaction('collection');\n        let collectionStore = tx.objectStore('collection');\n        return collectionStore.getAll();\n      })\n      .then((collections) => {\n        if (!Array.isArray(collections)) {\n          return new Error('IDB Corrupted');\n        }\n\n        return resolve(collections);\n      })\n      .catch((err) => reject(err));\n  });\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/importers/bruno-collection.js",
    "content": "import { BrunoError } from 'utils/common/error';\nimport { validateSchema, transformItemsInCollection, updateUidsInCollection, hydrateSeqInCollection } from './common';\nimport { transformExampleStatusInCollection } from '@usebruno/common';\n\nconst stripExportMetadata = (collection) => {\n  delete collection.exportedAt;\n  delete collection.exportedUsing;\n  return collection;\n};\n\nexport const processBrunoCollection = async (jsonData) => {\n  try {\n    let collection = stripExportMetadata(jsonData);\n    collection = hydrateSeqInCollection(collection);\n    collection = updateUidsInCollection(collection);\n    collection = transformItemsInCollection(collection);\n    collection = transformExampleStatusInCollection(collection);\n    await validateSchema(collection);\n    return collection;\n  } catch (err) {\n    console.error('Error processing Bruno collection:', err);\n    throw new BrunoError('Import collection failed');\n  }\n};\n\nexport const isBrunoCollection = (data) => {\n  // Check for Bruno collection format\n  if (typeof data !== 'object' || data === null) {\n    return false;\n  }\n\n  // Must have a version field that is a non-empty string\n  if (typeof data.version !== 'string' || !data.version.trim()) {\n    return false;\n  }\n\n  // Must have a name field that is a non-empty string\n  if (typeof data.name !== 'string' || !data.name.trim()) {\n    return false;\n  }\n\n  // Must have an items array\n  if (!Array.isArray(data.items)) {\n    return false;\n  }\n\n  return true;\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/importers/bruno-environment.js",
    "content": "import { BrunoError } from 'utils/common/error';\nimport { buildEnvVariable } from 'utils/environments';\n\nconst validateBrunoEnvironment = (env) => {\n  if (!env || typeof env !== 'object') {\n    throw new BrunoError('Invalid environment: expected an object');\n  }\n\n  if (!Array.isArray(env.variables)) {\n    throw new BrunoError('Invalid environment: missing or invalid variables array');\n  }\n\n  // Validate each variable\n  env.variables.forEach((variable, index) => {\n    if (!variable || typeof variable !== 'object') {\n      throw new BrunoError(`Invalid variable at index ${index}: expected an object`);\n    }\n    if (!variable.name || typeof variable.name !== 'string') {\n      throw new BrunoError(`Invalid variable at index ${index}: missing or invalid name`);\n    }\n  });\n\n  return {\n    name: env.name || 'Imported Environment',\n    variables: env.variables.map((envVariable) => buildEnvVariable({ envVariable, withUuid: true })),\n    color: env.color\n  };\n};\n\nconst processEnvironmentData = (data, fileName) => {\n  try {\n    // Handle new single-file format with environments array\n    if (data.info && data.info.type === 'bruno-environment' && Array.isArray(data.environments)) {\n      return data.environments.map((env, index) => {\n        try {\n          return validateBrunoEnvironment(env);\n        } catch (err) {\n          throw new BrunoError(`Error in environment ${index + 1} from ${fileName}: ${err.message}`);\n        }\n      });\n    }\n\n    // Handle array of environments (old format)\n    if (Array.isArray(data)) {\n      return data.map((env, index) => {\n        try {\n          return validateBrunoEnvironment(env);\n        } catch (err) {\n          throw new BrunoError(`Error in environment ${index + 1} from ${fileName}: ${err.message}`);\n        }\n      });\n    }\n\n    // Handle single environment object\n    return [validateBrunoEnvironment(data)];\n  } catch (err) {\n    throw new BrunoError(`Error processing ${fileName}: ${err.message}`);\n  }\n};\n\nconst processFiles = (parsedFiles) => {\n  const allEnvironments = [];\n\n  for (const parsedFile of parsedFiles) {\n    try {\n      const environments = processEnvironmentData(parsedFile.content, parsedFile.fileName);\n      allEnvironments.push(...environments);\n    } catch (err) {\n      throw new BrunoError(`Failed to process ${parsedFile.fileName}: ${err.message}`);\n    }\n  }\n\n  return allEnvironments;\n};\n\nconst importBrunoEnvironment = (parsedFiles) => {\n  try {\n    if (!parsedFiles || parsedFiles.length === 0) {\n      throw new BrunoError('No files provided');\n    }\n\n    const environments = processFiles(parsedFiles);\n    return environments;\n  } catch (err) {\n    console.error(err);\n    throw err instanceof BrunoError ? err : new BrunoError('Import Bruno environment failed');\n  }\n};\n\nexport { importBrunoEnvironment, processEnvironmentData };\nexport default importBrunoEnvironment;\n"
  },
  {
    "path": "packages/bruno-app/src/utils/importers/common.js",
    "content": "import jsyaml from 'js-yaml';\nimport each from 'lodash/each';\nimport get from 'lodash/get';\nimport filter from 'lodash/filter';\n\nimport cloneDeep from 'lodash/cloneDeep';\nimport { uuid } from 'utils/common';\nimport { isItemARequest } from 'utils/collections';\nimport { collectionSchema } from '@usebruno/schema';\nimport { BrunoError } from 'utils/common/error';\nimport { isOpenApiSpec } from './openapi-collection';\nimport { isPostmanCollection } from './postman-collection';\nimport { isInsomniaCollection } from './insomnia-collection';\n\nexport const validateSchema = async (collections = []) => {\n  collections = Array.isArray(collections) ? collections : [collections];\n\n  try {\n    await Promise.all(\n      collections.map(async (collection) => {\n        await collectionSchema.validate(collection);\n      })\n    );\n    return collections;\n  } catch (err) {\n    console.log(err);\n    throw new BrunoError('The Collection file is corrupted');\n  }\n};\n\nexport const updateUidsInCollection = (_collection) => {\n  const collection = cloneDeep(_collection);\n\n  collection.uid = uuid();\n\n  const updateItemUids = (items = []) => {\n    each(items, (item) => {\n      item.uid = uuid();\n\n      each(get(item, 'request.headers'), (header) => (header.uid = uuid()));\n      each(get(item, 'request.params'), (param) => (param.uid = uuid()));\n      each(get(item, 'request.vars.req'), (v) => (v.uid = uuid()));\n      each(get(item, 'request.vars.res'), (v) => (v.uid = uuid()));\n      each(get(item, 'request.assertions'), (a) => (a.uid = uuid()));\n      each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid()));\n      each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid()));\n      each(get(item, 'request.body.file'), (param) => (param.uid = uuid()));\n\n      each(get(item, 'examples'), (example) => {\n        example.uid = uuid();\n        example.itemUid = item.uid;\n        each(get(example, 'request.headers'), (header) => (header.uid = uuid()));\n        each(get(example, 'request.params'), (param) => (param.uid = uuid()));\n        each(get(example, 'request.body.multipartForm'), (param) => (param.uid = uuid()));\n        each(get(example, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid()));\n        each(get(example, 'request.body.file'), (param) => (param.uid = uuid()));\n        each(get(example, 'response.headers'), (header) => (header.uid = uuid()));\n      });\n\n      if (item.items && item.items.length) {\n        updateItemUids(item.items);\n      }\n    });\n  };\n  updateItemUids(collection.items);\n\n  const updateEnvUids = (envs = []) => {\n    each(envs, (env) => {\n      env.uid = uuid();\n      each(env.variables, (variable) => (variable.uid = uuid()));\n    });\n  };\n  updateEnvUids(collection.environments);\n\n  return collection;\n};\n\nexport const filterItemsInCollection = (collection) => {\n  // this filters out the bruno.json item in older collection exports\n  collection.items = filter(collection.items, (item) => {\n    if (item?.name === 'bruno' && item?.type === 'json') {\n      return false;\n    }\n    return true;\n  });\n\n  return collection;\n};\n\n// todo\n// need to eventually get rid of supporting old collection app models\n// 1. start with making request type a constant fetched from a single place\n// 2. move references of param and replace it with query inside the app\nexport const transformItemsInCollection = (collection) => {\n  const transformItems = (items = []) => {\n    each(items, (item) => {\n      if (['http', 'graphql', 'grpc', 'ws'].includes(item.type)) {\n        item.type = `${item.type}-request`;\n        const isGrpcRequest = item.type === 'grpc-request';\n        const isWSRequest = item.type === 'ws-request';\n\n        if (item.request.query) {\n          item.request.params = item.request.query.map((queryItem) => ({\n            ...queryItem,\n            type: 'query',\n            uid: queryItem.uid || uuid()\n          }));\n        }\n\n        if (isGrpcRequest) {\n          delete item.request.params;\n        }\n\n        if (isWSRequest) {\n          delete item.request.params;\n          delete item.request.method;\n        }\n\n        delete item.request.query;\n\n        // from 5 feb 2024, multipartFormData needs to have a type\n        // this was introduced when we added support for file uploads\n        // below logic is to make older collection exports backward compatible\n        let multipartFormData = get(item, 'request.body.multipartForm');\n        if (multipartFormData) {\n          each(multipartFormData, (form) => {\n            if (!form.type) {\n              form.type = 'text';\n            }\n          });\n        }\n\n        // Transform examples as well\n        each(get(item, 'examples'), (example) => {\n          if (['http', 'graphql', 'grpc', 'ws'].includes(example.type)) {\n            example.type = `${example.type}-request`;\n            const isGrpcExample = example.type === 'grpc-request';\n            const isWSExample = example.type === 'ws-request';\n\n            if (example.request && example.request.query) {\n              example.request.params = example.request.query.map((queryItem) => ({\n                ...queryItem,\n                type: 'query',\n                uid: queryItem.uid || uuid()\n              }));\n            }\n\n            if (isGrpcExample) {\n              delete example.request.params;\n            }\n\n            if (isWSExample) {\n              delete example.request.params;\n              delete example.request.method;\n            }\n\n            if (example.request) {\n              delete example.request.query;\n            }\n\n            // Handle multipartFormData for examples\n            let exampleMultipartFormData = get(example, 'request.body.multipartForm');\n            if (exampleMultipartFormData) {\n              each(exampleMultipartFormData, (form) => {\n                if (!form.type) {\n                  form.type = 'text';\n                }\n              });\n            }\n          }\n        });\n      }\n\n      if (item.items && item.items.length) {\n        transformItems(item.items);\n      }\n    });\n  };\n\n  if (Array.isArray(collection)) {\n    collection.forEach((col) => transformItems(col.items));\n  } else {\n    transformItems(collection.items);\n  }\n\n  return collection;\n};\n\nexport const hydrateSeqInCollection = (collection) => {\n  const hydrateSeq = (items = []) => {\n    let index = 1;\n    each(items, (item) => {\n      if (isItemARequest(item) && !item.seq) {\n        item.seq = index;\n        index++;\n      }\n      if (item.items && item.items.length) {\n        hydrateSeq(item.items);\n      }\n    });\n  };\n\n  if (Array.isArray(collection)) {\n    collection.forEach((col) => hydrateSeq(col.items));\n  } else {\n    hydrateSeq(collection.items);\n  }\n\n  return collection;\n};\n\n/**\n * Gets the schema type(postman, insomnia, openapi) of the CollectionJSON data\n * @param {Object} data - The JSON data to get the type of\n * @returns {'openapi' | 'postman' | 'insomnia' | 'unknown'} - The type of the CollectionJSON data\n */\nconst getCollectionSpecType = (data) => {\n  return isOpenApiSpec(data) ? 'openapi' : isPostmanCollection(data) ? 'postman' : isInsomniaCollection(data) ? 'insomnia' : 'unknown';\n};\n\nexport const fetchAndValidateApiSpecFromUrl = ({ url }) => {\n  const { ipcRenderer } = window;\n  return new Promise((resolve, reject) => {\n    ipcRenderer\n      .invoke('renderer:fetch-api-spec', url)\n      .then(async (res) => {\n        const data = await jsyaml.load(res);\n        const specType = getCollectionSpecType(data);\n        resolve({ data, specType, rawContent: res });\n      })\n      .catch((err) => {\n        console.error(err);\n        reject(new BrunoError('Failed to fetch API specification: ' + err.message));\n      });\n  });\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/importers/file-reader.js",
    "content": "import jsyaml from 'js-yaml';\nimport { BrunoError } from 'utils/common/error';\n\n/**\n * Parse a File object as JSON or YAML and return the parsed object.\n * Throws with a user-friendly message on parse failure.\n */\nexport const parseFileAsJsonOrYaml = async (file) => {\n  try {\n    const text = await file.text();\n    let parsed;\n    if (file.name.toLowerCase().endsWith('.json')) {\n      parsed = JSON.parse(text);\n    } else {\n      parsed = jsyaml.load(text);\n    }\n    if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n      throw new Error('Document root must be an object');\n    }\n    return parsed;\n  } catch {\n    throw new Error('Failed to parse the file – ensure it is valid JSON or YAML');\n  }\n};\n\nconst readFile = (file) => {\n  return new Promise((resolve, reject) => {\n    const fileReader = new FileReader();\n    fileReader.onload = (e) => {\n      try {\n        const parsed = JSON.parse(e.target.result);\n        resolve({ fileName: file.name, content: parsed });\n      } catch (err) {\n        console.error(err);\n        reject(new BrunoError(`Unable to parse JSON file: ${file.name}`));\n      }\n    };\n    fileReader.onerror = (err) => reject(err);\n    fileReader.readAsText(file);\n  });\n};\n\nexport const readMultipleFiles = async (files) => {\n  if (!files || files.length === 0) {\n    throw new BrunoError('No files selected');\n  }\n\n  const parsedFiles = [];\n\n  for (const file of files) {\n    if (!file.name.toLowerCase().endsWith('.json')) {\n      throw new BrunoError(`Invalid file type: ${file.name}. Only JSON files are supported.`);\n    }\n\n    try {\n      const parsedFile = await readFile(file);\n      parsedFiles.push(parsedFile);\n    } catch (err) {\n      throw new BrunoError(`Failed to read ${file.name}: ${err.message}`);\n    }\n  }\n\n  return parsedFiles;\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/importers/insomnia-collection.js",
    "content": "import { BrunoError } from 'utils/common/error';\nimport { insomniaToBruno } from '@usebruno/converters';\n\nexport const convertInsomniaToBruno = (data) => {\n  try {\n    return insomniaToBruno(data);\n  } catch (err) {\n    console.error('Error converting Insomnia to Bruno:', err);\n    throw new BrunoError('Import collection failed: ' + err.message);\n  }\n};\n\nexport const isInsomniaCollection = (data) => {\n  // Check for Insomnia v5 collection format – collection array must be present\n  if (typeof data.type === 'string' && data.type.startsWith('collection.insomnia.rest/5')) {\n    return Array.isArray(data.collection);\n  }\n\n  // Check for Insomnia v4 export format – must have __export_format and resources array\n  if (data._type === 'export') {\n    return Array.isArray(data.resources) && typeof data.__export_format === 'number';\n  }\n\n  return false;\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/importers/openapi-collection.js",
    "content": "import { BrunoError } from 'utils/common/error';\nimport { openApiToBruno } from '@usebruno/converters';\n\nexport const convertOpenapiToBruno = (data, options = {}) => {\n  try {\n    return openApiToBruno(data, options);\n  } catch (err) {\n    console.error('Error converting OpenAPI to Bruno:', err);\n    throw new BrunoError('Import collection failed: ' + err.message);\n  }\n};\n\nexport const isOpenApiSpec = (data) => {\n  if (typeof data.info !== 'object' || data.info === null) {\n    return false;\n  }\n\n  if (typeof data.openapi === 'string' && data.openapi.trim().length) {\n    return true;\n  }\n\n  if (typeof data.swagger === 'string' && data.swagger.trim().length) {\n    return true;\n  }\n\n  return false;\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/importers/opencollection.js",
    "content": "import each from 'lodash/each';\nimport { uuid } from 'utils/common';\nimport { BrunoError } from 'utils/common/error';\nimport { validateSchema, updateUidsInCollection, hydrateSeqInCollection } from './common';\nimport { transformExampleStatusInCollection } from '@usebruno/common';\nimport { openCollectionToBruno } from '@usebruno/converters';\n\nconst addUidsToRoot = (collection) => {\n  if (collection.root?.request?.headers) {\n    each(collection.root.request.headers, (header) => {\n      header.uid = uuid();\n    });\n  }\n  if (collection.root?.request?.vars?.req) {\n    each(collection.root.request.vars.req, (v) => {\n      v.uid = uuid();\n    });\n  }\n  if (collection.root?.request?.vars?.res) {\n    each(collection.root.request.vars.res, (v) => {\n      v.uid = uuid();\n    });\n  }\n\n  const addUidsToFolderRoot = (items) => {\n    each(items, (item) => {\n      if (item.type === 'folder') {\n        if (item.root?.request?.headers) {\n          each(item.root.request.headers, (header) => {\n            header.uid = uuid();\n          });\n        }\n        if (item.root?.request?.vars?.req) {\n          each(item.root.request.vars.req, (v) => {\n            v.uid = uuid();\n          });\n        }\n        if (item.root?.request?.vars?.res) {\n          each(item.root.request.vars.res, (v) => {\n            v.uid = uuid();\n          });\n        }\n        if (item.items?.length) {\n          addUidsToFolderRoot(item.items);\n        }\n      }\n    });\n  };\n\n  addUidsToFolderRoot(collection.items);\n  return collection;\n};\n\nexport const processOpenCollection = async (jsonData) => {\n  try {\n    let collection = openCollectionToBruno(jsonData);\n    collection = hydrateSeqInCollection(collection);\n    collection = updateUidsInCollection(collection);\n    collection = addUidsToRoot(collection);\n    collection = transformExampleStatusInCollection(collection);\n    await validateSchema(collection);\n    return collection;\n  } catch (err) {\n    console.error('Error processing OpenCollection:', err);\n    throw new BrunoError('Import OpenCollection failed');\n  }\n};\n\nexport const isOpenCollection = (data) => {\n  if (typeof data !== 'object' || data === null) {\n    return false;\n  }\n\n  if (typeof data.opencollection !== 'string' || !data.opencollection.trim()) {\n    return false;\n  }\n\n  if (typeof data.info !== 'object' || data.info === null) {\n    return false;\n  }\n\n  return true;\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/importers/postman-collection.js",
    "content": "import fileDialog from 'file-dialog';\nimport { BrunoError } from 'utils/common/error';\nimport { safeParseJSON } from 'utils/common/index';\n\nconst readFile = (files) => {\n  return new Promise((resolve, reject) => {\n    const fileReader = new FileReader();\n    fileReader.onload = (e) => resolve(safeParseJSON(e.target.result));\n    fileReader.onerror = (err) => reject(err);\n    fileReader.readAsText(files[0]);\n  });\n};\n\nconst postmanToBruno = (collection) => {\n  return new Promise((resolve, reject) => {\n    window.ipcRenderer.invoke('renderer:convert-postman-to-bruno', collection)\n      .then((result) => resolve(result))\n      .catch((err) => {\n        console.error('Error converting Postman to Bruno via Electron:', err);\n        reject(new BrunoError('Conversion failed'));\n      });\n  });\n};\n\nconst isPostmanCollection = (data) => {\n  const info = data.info;\n  if (!info || typeof info !== 'object') {\n    return false;\n  }\n\n  const schema = info.schema;\n  if (typeof schema !== 'string') {\n    return false;\n  }\n\n  // Only accept supported Postman v2.0 and v2.1 schemas\n  const supportedSchemas = [\n    'https://schema.getpostman.com/json/collection/v2.0.0/collection.json',\n    'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',\n    'https://schema.postman.com/json/collection/v2.0.0/collection.json',\n    'https://schema.postman.com/json/collection/v2.1.0/collection.json'\n  ];\n\n  return supportedSchemas.includes(schema);\n};\n\nexport { postmanToBruno, readFile, isPostmanCollection };\n"
  },
  {
    "path": "packages/bruno-app/src/utils/importers/postman-environment.js",
    "content": "import { BrunoError } from 'utils/common/error';\nimport { postmanToBrunoEnvironment } from '@usebruno/converters';\n\nconst importEnvironment = async (parsedFiles) => {\n  try {\n    const environments = [];\n\n    for (const parsedFile of parsedFiles) {\n      try {\n        const environment = postmanToBrunoEnvironment(parsedFile.content);\n        environments.push(environment);\n      } catch (err) {\n        console.error(`Error processing file: ${parsedFile.fileName}`, err);\n        throw new BrunoError(`Failed to process ${parsedFile.fileName}: ${err.message}`);\n      }\n    }\n\n    return environments;\n  } catch (err) {\n    console.log(err);\n    throw err instanceof BrunoError ? err : new BrunoError('Import Environment failed');\n  }\n};\n\nexport default importEnvironment;\n"
  },
  {
    "path": "packages/bruno-app/src/utils/importers/wsdl-collection.js",
    "content": "const isWSDLCollection = (data) => {\n  // Check if data is a string (WSDL content)\n  if (typeof data !== 'string') {\n    return false;\n  }\n\n  // Check for WSDL-specific XML elements\n  const wsdlIndicators = [\n    'wsdl:definitions',\n    'definitions',\n    'wsdl:types',\n    'wsdl:message',\n    'wsdl:portType',\n    'wsdl:binding',\n    'wsdl:service'\n  ];\n\n  // Check if the content contains WSDL namespace or elements\n  const hasWSDLNamespace = data.includes('xmlns:wsdl=')\n    || data.includes('xmlns=\"http://schemas.xmlsoap.org/wsdl/\"')\n    || data.includes('xmlns=\"http://www.w3.org/2001/XMLSchema\"');\n\n  const hasWSDLElements = wsdlIndicators.some((indicator) => data.includes(indicator));\n\n  return hasWSDLNamespace || hasWSDLElements;\n};\n\nexport { isWSDLCollection };\n"
  },
  {
    "path": "packages/bruno-app/src/utils/network/cancelTokens.js",
    "content": "// we maintain cancel tokens for a request separately as redux does not recommend to store\n// non-serializable value in the store\n\nconst cancelTokens = {};\n\nexport default cancelTokens;\n\nexport const saveCancelToken = (uid, axiosRequest) => {\n  cancelTokens[uid] = axiosRequest;\n};\n\nexport const deleteCancelToken = (uid) => {\n  delete cancelTokens[uid];\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/network/grpc-event-listeners.js",
    "content": "import { useEffect } from 'react';\nimport { grpcResponseReceived, runGrpcRequestEvent } from 'providers/ReduxStore/slices/collections/index';\nimport { useDispatch } from 'react-redux';\nimport { isElectron } from 'utils/common/platform';\nimport { updateActiveConnectionsInStore } from 'providers/ReduxStore/slices/collections/actions';\n\nconst useGrpcEventListeners = () => {\n  const { ipcRenderer } = window;\n  const dispatch = useDispatch();\n\n  useEffect(() => {\n    if (!isElectron()) {\n      return () => {};\n    }\n\n    // Handle gRPC requestSent event\n    const removeGrpcRequestSentListener = ipcRenderer.on('grpc:request', (requestId, collectionUid, eventData) => {\n      dispatch(runGrpcRequestEvent({\n        eventType: 'request',\n        itemUid: requestId,\n        collectionUid: collectionUid,\n        requestUid: requestId,\n        eventData\n      }));\n    });\n\n    const removeGrpcMessageSentListener = ipcRenderer.on('grpc:message', (requestId, collectionUid, eventData) => {\n      dispatch(runGrpcRequestEvent({\n        eventType: 'message',\n        itemUid: requestId,\n        collectionUid: collectionUid,\n        requestUid: requestId,\n        eventData\n      }));\n    });\n\n    // Handle gRPC response event (for unary calls and streaming)\n    const removeGrpcResponseListener = ipcRenderer.on(`grpc:response`, (requestId, collectionUid, data) => {\n      dispatch(grpcResponseReceived({\n        itemUid: requestId,\n        collectionUid: collectionUid,\n        eventType: 'response',\n        eventData: data\n      }));\n    });\n\n    // Handle gRPC metadata\n    const removeGrpcMetadataListener = ipcRenderer.on(`grpc:metadata`, (requestId, collectionUid, data) => {\n      dispatch(grpcResponseReceived({\n        itemUid: requestId,\n        collectionUid: collectionUid,\n        eventType: 'metadata',\n        eventData: data\n      }));\n    });\n\n    // Handle gRPC status updates\n    const removeGrpcStatusListener = ipcRenderer.on(`grpc:status`, (requestId, collectionUid, data) => {\n      dispatch(grpcResponseReceived({\n        itemUid: requestId,\n        collectionUid: collectionUid,\n        eventType: 'status',\n        eventData: data\n      }));\n    });\n\n    // Handle gRPC errors\n    const removeGrpcErrorListener = ipcRenderer.on(`grpc:error`, (requestId, collectionUid, data) => {\n      dispatch(grpcResponseReceived({\n        itemUid: requestId,\n        collectionUid: collectionUid,\n        eventType: 'error',\n        eventData: data\n      }));\n    });\n\n    // Handle gRPC end event\n    const removeGrpcEndListener = ipcRenderer.on(`grpc:server-end-stream`, (requestId, collectionUid, data) => {\n      dispatch(grpcResponseReceived({\n        itemUid: requestId,\n        collectionUid: collectionUid,\n        eventType: 'end',\n        eventData: data\n      }));\n    });\n\n    // Handle gRPC cancel event\n    const removeGrpcCancelListener = ipcRenderer.on(`grpc:server-cancel-stream`, (requestId, collectionUid, data) => {\n      dispatch(grpcResponseReceived({\n        itemUid: requestId,\n        collectionUid: collectionUid,\n        eventType: 'cancel',\n        eventData: data\n      }));\n    });\n\n    const removeGrpcConnectionsChangedListener = ipcRenderer.on(`grpc:connections-changed`, (data) => {\n      dispatch(updateActiveConnectionsInStore(data));\n    });\n\n    return () => {\n      removeGrpcRequestSentListener();\n      removeGrpcMessageSentListener();\n      removeGrpcResponseListener();\n      removeGrpcMetadataListener();\n      removeGrpcStatusListener();\n      removeGrpcErrorListener();\n      removeGrpcEndListener();\n      removeGrpcCancelListener();\n      removeGrpcConnectionsChangedListener();\n    };\n  }, [isElectron]);\n};\n\nexport default useGrpcEventListeners;\n"
  },
  {
    "path": "packages/bruno-app/src/utils/network/index.js",
    "content": "export const sendNetworkRequest = async (item, collection, environment, runtimeVariables) => {\n  return new Promise((resolve, reject) => {\n    if (['http-request', 'graphql-request'].includes(item.type)) {\n      sendHttpRequest(item, collection, environment, runtimeVariables)\n        .then((response) => {\n          // if there is an error, we return the response object as is\n          if (response?.error) {\n            resolve(response);\n          }\n\n          resolve({\n            state: 'success',\n            data: response.data,\n            // Note that the Buffer is encoded as a base64 string, because Buffers / TypedArrays are not allowed in the redux store\n            dataBuffer: response.dataBuffer,\n            headers: response.headers,\n            size: response.size,\n            status: response.status,\n            statusText: response.statusText,\n            duration: response.duration,\n            timeline: response.timeline,\n            stream: response.stream\n          });\n        })\n        .catch((err) => reject(err));\n    }\n  });\n};\n\nexport const sendGrpcRequest = async (item, collection, environment, runtimeVariables) => {\n  return new Promise((resolve, reject) => {\n    startGrpcRequest(item, collection, environment, runtimeVariables)\n      .then((initialState) => {\n        // Return an initial state object to update the UI\n        // The real response data will be handled by event listeners\n        resolve({\n          ...initialState,\n          timeline: []\n        });\n      })\n      .catch((err) => reject(err));\n  });\n};\n\nconst sendHttpRequest = async (item, collection, environment, runtimeVariables) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n\n    ipcRenderer\n      .invoke('send-http-request', item, collection, environment, runtimeVariables)\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const sendCollectionOauth2Request = async (collection, environment, runtimeVariables) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    resolve({});\n  });\n};\n\nexport const fetchGqlSchema = async (endpoint, environment, request, collection) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n\n    ipcRenderer.invoke('fetch-gql-schema', endpoint, environment, request, collection).then(resolve).catch(reject);\n  });\n};\n\nexport const cancelNetworkRequest = async (cancelTokenUid) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n\n    ipcRenderer.invoke('cancel-http-request', cancelTokenUid).then(resolve).catch(reject);\n  });\n};\n\nexport const startGrpcRequest = async (item, collection, environment, runtimeVariables) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    const request = item.draft ? item.draft : item;\n\n    ipcRenderer.invoke('grpc:start-connection', {\n      request,\n      collection,\n      environment,\n      runtimeVariables\n    })\n      .then(() => {\n        resolve();\n      })\n      .catch((err) => {\n        reject(err);\n      });\n  });\n};\n\n/**\n * Sends a message to an existing gRPC stream\n * @param {string} requestId - The request ID to send a message to\n * @param {Object} message - The message to send\n * @returns {Promise<Object>} - The result of the send operation\n */\nexport const sendGrpcMessage = async (item, collectionUid, message) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    ipcRenderer.invoke('grpc:send-message', item.uid, collectionUid, message)\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\n/**\n * Cancels a running gRPC request\n * @param {string} requestId - The request ID to cancel\n * @returns {Promise<Object>} - The result of the cancel operation\n */\nexport const cancelGrpcRequest = async (requestId) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    ipcRenderer.invoke('grpc:cancel', requestId)\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\n/**\n * Ends a gRPC streaming request (client-streaming or bidirectional)\n * @param {string} requestId - The request ID to end\n * @returns {Promise<Object>} - The result of the end operation\n */\nexport const endGrpcStream = async (requestId) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    ipcRenderer.invoke('grpc:end', requestId)\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const loadGrpcMethodsFromProtoFile = async (filePath, collection = null) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n\n    ipcRenderer.invoke('grpc:load-methods-proto', { filePath, collection }).then(resolve).catch(reject);\n  });\n};\n\nexport const cancelGrpcConnection = async (connectionId) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    ipcRenderer.invoke('grpc:cancel-request', { requestId: connectionId }).then(resolve).catch(reject);\n  });\n};\n\nexport const endGrpcConnection = async (connectionId) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    ipcRenderer.invoke('grpc:end-request', { requestId: connectionId }).then(resolve).catch(reject);\n  });\n};\n\n/**\n * Check if a gRPC connection is active\n * @param {string} connectionId - The connection ID to check\n * @returns {Promise<boolean>} - Whether the connection is active\n */\nexport const isGrpcConnectionActive = async (connectionId) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    ipcRenderer.invoke('grpc:is-connection-active', connectionId)\n      .then((response) => {\n        if (response.success) {\n          resolve(response.isActive);\n        } else {\n          // If there was an error, assume the connection is not active\n          console.error('Error checking connection status:', response.error);\n          resolve(false);\n        }\n      })\n      .catch((err) => {\n        console.error('Failed to check connection status:', err);\n        // On error, assume the connection is not active\n        resolve(false);\n      });\n  });\n};\n\n/**\n * Generates a sample gRPC message for a method\n * @param {string} methodPath - The full gRPC method path\n * @param {string|null} existingMessage - Optional existing message JSON string to use as a template\n * @param {Object} options - Additional options for message generation\n * @returns {Promise<Object>} The generated sample message or error\n */\nexport const generateGrpcSampleMessage = async (methodPath, existingMessage = null, options = {}) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n\n    ipcRenderer.invoke('grpc:generate-sample-message', {\n      methodPath,\n      existingMessage,\n      options\n    })\n      .then(resolve)\n      .catch(reject);\n  });\n};\n\nexport const connectWS = async (item, collection, environment, runtimeVariables, options) => {\n  return new Promise((resolve, reject) => {\n    startWsConnection(item, collection, environment, runtimeVariables, options)\n      .then((initialState) => {\n        // Return an initial state object to update the UI\n        // The real response data will be handled by event listeners\n        resolve({\n          ...initialState,\n          timeline: []\n        });\n      })\n      .catch((err) => reject(err));\n  });\n};\n\nexport const sendWsRequest = async (item, collection, environment, runtimeVariables) => {\n  const ensureConnection = async () => {\n    const connectionStatus = await isWsConnectionActive(item.uid);\n    if (!connectionStatus.isActive) {\n      await connectWS(item, collection, environment, runtimeVariables, { connectOnly: true });\n    }\n  };\n\n  await ensureConnection();\n\n  // Use queueWsMessage helper to queue all messages with proper variable interpolation\n  const result = await queueWsMessage(item, collection, environment, runtimeVariables, null);\n\n  if (result.success) {\n    return {};\n  } else {\n    throw new Error(result.error || 'Failed to queue messages');\n  }\n};\n\n/**\n * Queues a message to an existing WebSocket connection with variable interpolation\n * @param {Object} item - The request item\n * @param {Object} collection - The collection object\n * @param {Object} environment - The environment variables\n * @param {Object} runtimeVariables - The runtime variables\n * @param {string} messageContent - The message content to queue (or null to queue all messages)\n * @returns {Promise<Object>} - The result of the queue operation\n */\nexport const queueWsMessage = async (item, collection, environment, runtimeVariables, messageContent) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    ipcRenderer.invoke('renderer:ws:queue-message', {\n      item,\n      collection,\n      environment,\n      runtimeVariables,\n      messageContent\n    }).then(resolve).catch(reject);\n  });\n};\n\nexport const startWsConnection = async (item, collection, environment, runtimeVariables, options) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    const request = item.draft ? item.draft : item;\n    const settings = item.draft ? item.draft.settings : item.settings;\n\n    ipcRenderer\n      .invoke('renderer:ws:start-connection', {\n        request,\n        collection,\n        environment,\n        runtimeVariables,\n        settings,\n        options\n      })\n      .then(() => {\n        resolve();\n      })\n      .catch((err) => {\n        reject(err);\n      });\n  });\n};\n\n/**\n * Sends a message to an existing WebSocket connection\n * @param {string} requestId - The request ID to send a message to\n * @param {Object} message - The message to send\n * @returns {Promise<Object>} - The result of the send operation\n */\nexport const sendWsMessage = async (item, collectionUid, message) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    ipcRenderer.invoke('renderer:ws:send-message', item.uid, collectionUid, message).then(resolve).catch(reject);\n  });\n};\n\n/**\n * Closes a WebSocket connection\n * @param {string} requestId - The request ID to close\n * @returns {Promise<Object>} - The result of the close operation\n */\nexport const closeWsConnection = async (requestId) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    ipcRenderer.invoke('renderer:ws:close-connection', requestId).then(resolve).catch(reject);\n  });\n};\n\n/**\n * Checks if a WebSocket connection is active\n * @param {string} requestId - The request ID to check\n * @returns {Promise<boolean>} - Whether the connection is active\n */\nexport const isWsConnectionActive = async (requestId) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    ipcRenderer.invoke('renderer:ws:is-connection-active', requestId).then(resolve).catch(reject);\n  });\n};\n\n/**\n * Get the connection status of a WebSocket connection\n * @param {string} requestId - The request ID to get the connection status of\n * @returns {Promise<Object>} - The result of the get operation\n */\nexport const getWsConnectionStatus = async (requestId) => {\n  return new Promise((resolve, reject) => {\n    const { ipcRenderer } = window;\n    ipcRenderer.invoke('renderer:ws:connection-status', requestId).then(resolve).catch(reject);\n  });\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/network/ws-event-listeners.js",
    "content": "import { useEffect } from 'react';\nimport { wsResponseReceived, runWsRequestEvent } from 'providers/ReduxStore/slices/collections/index';\nimport { useDispatch } from 'react-redux';\nimport { isElectron } from 'utils/common/platform';\nimport { updateActiveConnectionsInStore } from 'providers/ReduxStore/slices/collections/actions';\n\nconst useWsEventListeners = () => {\n  const { ipcRenderer } = window;\n  const dispatch = useDispatch();\n\n  useEffect(() => {\n    if (!isElectron()) {\n      return () => {};\n    }\n\n    // Handle WebSocket requestSent event\n    const removeWsRequestSentListener = ipcRenderer.on('main:ws:request', (requestId, collectionUid, eventData) => {\n      dispatch(runWsRequestEvent({\n        eventType: 'request',\n        itemUid: requestId,\n        collectionUid: collectionUid,\n        requestUid: requestId,\n        eventData\n      }));\n    });\n\n    const removeWsUpgradeListener = ipcRenderer.on('main:ws:upgrade', (requestId, collectionUid, eventData) => {\n      dispatch(wsResponseReceived({\n        itemUid: requestId,\n        collectionUid: collectionUid,\n        eventType: 'upgrade',\n        eventData: eventData\n      }));\n    });\n\n    const removeWsRedirectListener = ipcRenderer.on('main:ws:redirect', (requestId, collectionUid, eventData) => {\n      dispatch(wsResponseReceived({\n        itemUid: requestId,\n        collectionUid: collectionUid,\n        eventType: 'redirect',\n        eventData: eventData\n      }));\n    });\n\n    // Handle WebSocket message event\n    const removeWsMessageListener = ipcRenderer.on('main:ws:message', (requestId, collectionUid, eventData) => {\n      dispatch(wsResponseReceived({\n        itemUid: requestId,\n        collectionUid: collectionUid,\n        eventType: 'message',\n        eventData: eventData\n      }));\n    });\n\n    // Handle WebSocket open event\n    const removeWsOpenListener = ipcRenderer.on('main:ws:open', (requestId, collectionUid, eventData) => {\n      dispatch(wsResponseReceived({\n        itemUid: requestId,\n        collectionUid: collectionUid,\n        eventType: 'open',\n        eventData: eventData\n      }));\n    });\n\n    // Handle WebSocket close event\n    const removeWsCloseListener = ipcRenderer.on('main:ws:close', (requestId, collectionUid, eventData) => {\n      dispatch(wsResponseReceived({\n        itemUid: requestId,\n        collectionUid: collectionUid,\n        eventType: 'close',\n        eventData: eventData\n      }));\n    });\n\n    // Handle WebSocket error event\n    const removeWsErrorListener = ipcRenderer.on('main:ws:error', (requestId, collectionUid, eventData) => {\n      dispatch(wsResponseReceived({\n        itemUid: requestId,\n        collectionUid: collectionUid,\n        eventType: 'error',\n        eventData: eventData\n      }));\n    });\n\n    // Handle WebSocket connecting event\n    const removeWsConnectingListener = ipcRenderer.on('main:ws:connecting', (requestId, collectionUid, eventData) => {\n      dispatch(wsResponseReceived({\n        itemUid: requestId,\n        collectionUid: collectionUid,\n        eventType: 'connecting',\n        eventData: eventData\n      }));\n    });\n\n    const removeWsConnectionsChangedListener = ipcRenderer.on('main:ws:connections-changed', (data) => {\n      dispatch(updateActiveConnectionsInStore(data));\n    });\n\n    return () => {\n      removeWsRequestSentListener();\n      removeWsUpgradeListener();\n      removeWsRedirectListener();\n      removeWsMessageListener();\n      removeWsOpenListener();\n      removeWsCloseListener();\n      removeWsErrorListener();\n      removeWsConnectingListener();\n      removeWsConnectionsChangedListener();\n    };\n  }, [isElectron]);\n};\n\nexport default useWsEventListeners;\n"
  },
  {
    "path": "packages/bruno-app/src/utils/response/index.js",
    "content": "// Normalize & extract MIME type from full header\nconst extractMimeType = (contentType = '') => {\n  const cleaned = String(contentType).trim().toLowerCase();\n  const match = cleaned.match(/^[^;]+/); // strip \"; charset=utf-8\"\n  return match ? match[0] : cleaned;\n};\n\nexport const getDefaultResponseFormat = (contentType) => {\n  const mime = extractMimeType(contentType);\n\n  const rules = [\n    // ====== HTML ======\n    { test: /^text\\/html$/, result: { format: 'html', tab: 'preview' } },\n\n    // ====== JSON (including custom +json types) ======\n    {\n      test: /^application\\/(json|.+\\+json)$/,\n      result: { format: 'json', tab: 'editor' }\n    },\n    {\n      test: /^text\\/(json|.+\\+json)$/,\n      result: { format: 'json', tab: 'editor' }\n    },\n\n    // ====== XML (including custom +xml types) ======\n    {\n      test: /^application\\/(xml|.+\\+xml)$/,\n      result: { format: 'xml', tab: 'editor' }\n    },\n    {\n      test: /^text\\/(xml|.+\\+xml)$/,\n      result: { format: 'xml', tab: 'editor' }\n    },\n\n    // ====== JavaScript ======\n    {\n      test: /^(application|text)\\/javascript$/,\n      result: { format: 'javascript', tab: 'editor' }\n    },\n\n    // ====== Images, audio, video, PDFs → preview (base64) ======\n    { test: /^image\\//, result: { format: 'base64', tab: 'preview' } },\n    { test: /^audio\\//, result: { format: 'base64', tab: 'preview' } },\n    { test: /^video\\//, result: { format: 'base64', tab: 'preview' } },\n    { test: /^application\\/pdf$/, result: { format: 'base64', tab: 'preview' } },\n\n    // ====== Any other text types ======\n    { test: /^text\\//, result: { format: 'raw', tab: 'editor' } }\n  ];\n\n  for (const rule of rules) {\n    if (rule.test.test(mime)) {\n      return rule.result;\n    }\n  }\n\n  // ====== Fallback ======\n  return { format: 'raw', tab: 'editor' };\n};\n\n// Safe HTML escaping for webview content\nexport const escapeHtml = (text) => {\n  if (typeof text !== 'string') return text;\n  return text\n    .replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;')\n    .replace(/\"/g, '&quot;')\n    .replace(/'/g, '&#039;');\n};\n\n/**\n * Helper to detect if buffer contains text data\n */\nconst isLikelyText = (buffer) => {\n  if (!buffer || buffer.length === 0) return false;\n  let textChars = 0;\n  const sampleSize = Math.min(buffer.length, 512);\n\n  for (let i = 0; i < sampleSize; i++) {\n    const byte = buffer[i];\n    // Check for common text characters (printable ASCII + common control chars)\n    if ((byte >= 0x20 && byte <= 0x7E) // Printable ASCII\n      || byte === 0x09 // Tab\n      || byte === 0x0A // Line feed\n      || byte === 0x0D) { // Carriage return\n      textChars++;\n    }\n  }\n\n  // If more than 85% are text characters, likely text\n  return (textChars / sampleSize) > 0.85;\n};\n\n/**\n * Helper to detect SVG content from text buffer\n * SVG files may start with XML declaration, comments, or whitespace before the <svg tag\n * @param {Buffer} buffer - The data buffer to analyze\n * @returns {boolean} - true if buffer contains SVG content\n */\nconst isSvgContent = (buffer) => {\n  const length = buffer.length;\n  if (length < 4 || buffer[0] !== 0x3C) return false;\n\n  // Fast path: <svg\n  if (buffer[1] === 0x73 && buffer[2] === 0x76 && buffer[3] === 0x67) {\n    return true;\n  }\n\n  // Slow path: <?xml or <!DOCTYPE or <!--\n  if (buffer[1] !== 0x3F && buffer[1] !== 0x21) return false;\n\n  // Search for <svg in first 512 bytes\n  const limit = Math.min(512, length - 3);\n  for (let i = 2; i < limit; i++) {\n    if (buffer[i] === 0x3C && buffer[i + 1] === 0x73\n      && buffer[i + 2] === 0x76 && buffer[i + 3] === 0x67) {\n      return true;\n    }\n  }\n\n  return false;\n};\n\n/**\n * Decode only the first N bytes from a Base64 string\n * Returns an empty buffer for invalid/missing input\n */\nconst decodeBase64Head = (base64, byteCount) => {\n  // Validate input is a non-empty string\n  if (!base64 || typeof base64 !== 'string') {\n    return Buffer.alloc(0);\n  }\n\n  try {\n    // Safely remove data URL prefix (e.g., \"data:image/png;base64,\")\n    const prefixMatch = base64.match(/^data:[^;]*;base64,/);\n    const cleanedBase64 = prefixMatch ? base64.slice(prefixMatch[0].length) : base64;\n\n    // Return empty buffer if nothing left after stripping prefix\n    if (!cleanedBase64) {\n      return Buffer.alloc(0);\n    }\n\n    // How many base64 chars needed to reconstruct \"byteCount\" bytes\n    const neededChars = Math.ceil(byteCount / 3) * 4;\n\n    // Slice only required chars\n    let slice = cleanedBase64.slice(0, neededChars);\n\n    // Sanitize: remove any non-base64 characters (whitespace, invalid chars)\n    slice = slice.replace(/[^A-Za-z0-9+/=]/g, '');\n\n    // Pad to valid base64 length (must be multiple of 4)\n    const padLength = (4 - (slice.length % 4)) % 4;\n    slice = slice + '='.repeat(padLength);\n\n    // Decode and trim to requested bytes\n    return Buffer.from(slice, 'base64').subarray(0, byteCount);\n  } catch (error) {\n    // On any decoding error, return an empty buffer\n    return Buffer.alloc(0);\n  }\n};\n\n/**\n* Detects content type from buffer by checking magic numbers (file signatures)\n* @param {Buffer} buffer - The data buffer to analyze\n* @returns {string|null} - Detected MIME type or null\n*/\nexport const detectContentTypeFromBuffer = (buffer) => {\n  if (!buffer || buffer.length < 4) {\n    return null;\n  }\n\n  // Get first few bytes for magic number checking\n  const bytes = buffer.subarray(0, 12);\n\n  // Image formats\n  if (bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF) {\n    return 'image/jpeg';\n  }\n  if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47) {\n    return 'image/png';\n  }\n  if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) {\n    return 'image/gif';\n  }\n  if (bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50) {\n    return 'image/webp';\n  }\n  if (bytes[4] === 0x66 && bytes[5] === 0x74 && bytes[6] === 0x79 && bytes[7] === 0x70\n    && bytes[8] === 0x61 && bytes[9] === 0x76 && bytes[10] === 0x69 && bytes[11] === 0x66) {\n    return 'image/avif';\n  }\n  if (bytes[0] === 0x42 && bytes[1] === 0x4D) {\n    return 'image/bmp';\n  }\n  if ((bytes[0] === 0x49 && bytes[1] === 0x49 && bytes[2] === 0x2A && bytes[3] === 0x00)\n    || (bytes[0] === 0x4D && bytes[1] === 0x4D && bytes[2] === 0x00 && bytes[3] === 0x2A)) {\n    return 'image/tiff';\n  }\n  if (bytes[0] === 0x00 && bytes[1] === 0x00 && bytes[2] === 0x01 && bytes[3] === 0x00) {\n    return 'image/x-icon';\n  }\n  if (bytes[0] === 0x3C && bytes[1] === 0x73 && bytes[2] === 0x76 && bytes[3] === 0x67 && bytes[4] === 0x20) {\n    return 'image/svg+xml';\n  }\n  // PDF\n  if (bytes[0] === 0x25 && bytes[1] === 0x50 && bytes[2] === 0x44 && bytes[3] === 0x46) {\n    return 'application/pdf';\n  }\n\n  // Video formats\n  if (bytes[0] === 0x00 && bytes[1] === 0x00 && bytes[2] === 0x00\n    && (bytes[3] === 0x18 || bytes[3] === 0x20)\n    && bytes[4] === 0x66 && bytes[5] === 0x74 && bytes[6] === 0x79 && bytes[7] === 0x70) {\n    return 'video/mp4';\n  }\n  if ((bytes[0] === 0x1A && bytes[1] === 0x45 && bytes[2] === 0xDF && bytes[3] === 0xA3)) {\n    return 'video/webm';\n  }\n  if (bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46\n    && bytes[8] === 0x41 && bytes[9] === 0x56 && bytes[10] === 0x49 && bytes[11] === 0x20) {\n    return 'video/x-msvideo'; // AVI\n  }\n\n  // Audio formats\n  if (bytes[0] === 0xFF && (bytes[1] & 0xE0) === 0xE0) {\n    return 'audio/mpeg'; // MP3\n  }\n  if (bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46\n    && bytes[8] === 0x57 && bytes[9] === 0x41 && bytes[10] === 0x56 && bytes[11] === 0x45) {\n    return 'audio/wav';\n  }\n  if (bytes[0] === 0x4F && bytes[1] === 0x67 && bytes[2] === 0x67 && bytes[3] === 0x53) {\n    return 'audio/ogg';\n  }\n  if (bytes[4] === 0x66 && bytes[5] === 0x74 && bytes[6] === 0x79 && bytes[7] === 0x70\n    && bytes[8] === 0x4D && bytes[9] === 0x34 && bytes[10] === 0x41) {\n    return 'audio/m4a';\n  }\n\n  // Archive formats\n  if (bytes[0] === 0x50 && bytes[1] === 0x4B\n    && (bytes[2] === 0x03 || bytes[2] === 0x05 || bytes[2] === 0x07)) {\n    return 'application/zip';\n  }\n  if (bytes[0] === 0x1F && bytes[1] === 0x8B) {\n    return 'application/gzip';\n  }\n\n  return null;\n};\n\n/**\n * Main: detect from base64 string\n */\nexport const detectContentTypeFromBase64 = (base64) => {\n  if (!base64) return null;\n\n  // 1. Decode first 12 bytes (magic numbers)\n  const magicHead = decodeBase64Head(base64, 12);\n\n  const magicType = detectContentTypeFromBuffer(magicHead);\n  if (magicType) return magicType;\n\n  // 2. If not binary → decode up to 512 bytes for text detection\n  const textHead = decodeBase64Head(base64, 512);\n\n  if (isSvgContent(textHead)) {\n    return 'image/svg+xml';\n  }\n\n  if (isLikelyText(textHead)) return 'text/plain';\n\n  return null;\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/responseBodyProcessor.js",
    "content": "/**\n * Utility functions for processing response body content and determining body type\n */\n\n/**\n * Determines the body type based on content-type header\n * @param {string} contentType - The content-type header value\n * @param {Buffer} dataBuffer - Optional binary data buffer\n * @returns {string} - The body type (json, xml, html, text, binary)\n */\nexport const getBodyType = (contentType = '') => {\n  const normalizedContentType = contentType.toLowerCase();\n\n  if (normalizedContentType.includes('application/json')) {\n    return 'json';\n  } else if (normalizedContentType.includes('text/xml') || normalizedContentType.includes('application/xml')) {\n    return 'xml';\n  } else if (normalizedContentType.includes('text/html')) {\n    return 'html';\n  }\n\n  return 'text';\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/tabs/index.js",
    "content": "import find from 'lodash/find';\n\nexport const isItemARequest = (item) => {\n  return item.hasOwnProperty('request') && ['http-request', 'graphql-request', 'grpc-request', 'ws-request'].includes(item.type);\n};\n\nexport const isItemAFolder = (item) => {\n  return !item.hasOwnProperty('request') && item.type === 'folder';\n};\n\nexport const itemIsOpenedInTabs = (item, tabs) => {\n  return find(tabs, (t) => t.uid === item.uid);\n};\n\nexport const scrollToTheActiveTab = () => {\n  const activeTab = document.querySelector('.request-tab.active');\n  if (activeTab) {\n    activeTab.scrollIntoView({ behavior: 'smooth', block: 'start' });\n  }\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/terminal.js",
    "content": "import { openConsole, setActiveTab } from 'providers/ReduxStore/slices/logs';\nimport { getSessionId } from 'components/Devtools/Console/TerminalTab';\n\n/**\n * Opens the devtools console and switches to the terminal tab\n * Optionally opens/switches to a terminal session at a specific CWD\n * @param {Function} dispatch - Redux dispatch function\n * @param {string} [cwd] - Optional CWD path. If provided, checks for existing session at that CWD or creates new one\n */\nexport const openDevtoolsAndSwitchToTerminal = async (dispatch, cwd = null) => {\n  // Open console if closed\n  dispatch(openConsole());\n\n  // Switch to terminal tab\n  dispatch(setActiveTab('terminal'));\n\n  // If CWD is provided, dispatch event to TerminalTab to handle session selection/creation\n  if (cwd) {\n    // Small delay to ensure terminal tab is mounted\n    setTimeout(() => {\n      window.dispatchEvent(new CustomEvent('terminal:open-at-cwd', { detail: { cwd } }));\n    }, 100);\n  }\n};\n\n/**\n * Gets the current terminal session ID if a terminal session is running\n * @returns {string|null} The session ID if terminal session exists, null otherwise\n */\nexport const getSessionID = () => {\n  return getSessionId();\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/tests/collections/examples-export-import.spec.js",
    "content": "import { transformCollectionToSaveToExportAsFile, transformRequestToSaveToFilesystem } from '../../collections/index';\nimport { transformItemsInCollection } from '../../importers/common';\nimport { deleteUidsInItems, transformItem } from '../../collections/export';\n\ndescribe('Examples Export/Import', () => {\n  describe('transformCollectionToSaveToExportAsFile', () => {\n    it('should preserve examples when exporting collection', () => {\n      const collection = {\n        uid: 'test-collection',\n        name: 'Test Collection',\n        items: [\n          {\n            uid: 'http-request-1',\n            type: 'http-request',\n            name: 'Test HTTP Request',\n            request: {\n              url: 'https://api.example.com/test',\n              method: 'POST',\n              headers: [\n                { uid: 'header-1', name: 'Content-Type', value: 'application/json', enabled: true }\n              ],\n              params: [],\n              body: {\n                mode: 'json',\n                json: '{\"message\": \"test\"}'\n              }\n            },\n            examples: [\n              {\n                uid: 'example-1',\n                itemUid: 'http-request-1',\n                name: 'Success Example',\n                description: 'Example of successful response',\n                type: 'http-request',\n                request: {\n                  url: 'https://api.example.com/test',\n                  method: 'POST',\n                  headers: [\n                    { uid: 'ex-header-1', name: 'Content-Type', value: 'application/json', enabled: true }\n                  ],\n                  params: [],\n                  body: {\n                    mode: 'json',\n                    json: '{\"message\": \"test\"}'\n                  }\n                },\n                response: {\n                  status: 200,\n                  statusText: 'OK',\n                  headers: [\n                    { uid: 'res-header-1', name: 'Content-Type', value: 'application/json', enabled: true }\n                  ],\n                  body: '{\"success\": true, \"data\": \"test\"}'\n                }\n              }\n            ]\n          }\n        ]\n      };\n\n      const result = transformCollectionToSaveToExportAsFile(collection);\n      const httpRequest = result.items[0];\n\n      expect(httpRequest.examples).toHaveLength(1);\n      expect(httpRequest.examples[0].name).toBe('Success Example');\n      expect(httpRequest.examples[0].description).toBe('Example of successful response');\n      expect(httpRequest.examples[0].type).toBe('http-request');\n      expect(httpRequest.examples[0].request.url).toBe('https://api.example.com/test');\n      expect(httpRequest.examples[0].request.method).toBe('POST');\n      expect(httpRequest.examples[0].response.status).toEqual(200);\n      expect(httpRequest.examples[0].response.statusText).toBe('OK');\n      expect(httpRequest.examples[0].response.body).toBe('{\"success\": true, \"data\": \"test\"}');\n    });\n\n    it('should handle multiple examples correctly', () => {\n      const collection = {\n        uid: 'test-collection',\n        name: 'Test Collection',\n        items: [\n          {\n            uid: 'http-request-1',\n            type: 'http-request',\n            name: 'Test HTTP Request',\n            request: {\n              url: 'https://api.example.com/test',\n              method: 'GET',\n              headers: [],\n              params: [],\n              body: { mode: 'none' }\n            },\n            examples: [\n              {\n                uid: 'example-1',\n                itemUid: 'http-request-1',\n                name: 'Success Example',\n                description: '200 response',\n                type: 'http-request',\n                request: {\n                  url: 'https://api.example.com/test',\n                  method: 'GET',\n                  headers: [],\n                  params: [],\n                  body: { mode: 'none' }\n                },\n                response: {\n                  status: 200,\n                  statusText: 'OK',\n                  headers: [],\n                  body: '{\"success\": true}'\n                }\n              },\n              {\n                uid: 'example-2',\n                itemUid: 'http-request-1',\n                name: 'Error Example',\n                description: '400 response',\n                type: 'http-request',\n                request: {\n                  url: 'https://api.example.com/test',\n                  method: 'GET',\n                  headers: [],\n                  params: [],\n                  body: { mode: 'none' }\n                },\n                response: {\n                  status: 400,\n                  statusText: 'Bad Request',\n                  headers: [],\n                  body: '{\"error\": \"Invalid request\"}'\n                }\n              }\n            ]\n          }\n        ]\n      };\n\n      const result = transformCollectionToSaveToExportAsFile(collection);\n      const httpRequest = result.items[0];\n\n      expect(httpRequest.examples).toHaveLength(2);\n      expect(httpRequest.examples[0].name).toBe('Success Example');\n      expect(httpRequest.examples[1].name).toBe('Error Example');\n      expect(httpRequest.examples[0].response.status).toEqual(200);\n      expect(httpRequest.examples[1].response.status).toEqual(400);\n    });\n\n    it('should handle examples with GraphQL requests', () => {\n      const collection = {\n        uid: 'test-collection',\n        name: 'Test Collection',\n        items: [\n          {\n            uid: 'graphql-request-1',\n            type: 'graphql-request',\n            name: 'Test GraphQL Request',\n            request: {\n              url: 'https://api.example.com/graphql',\n              method: 'POST',\n              headers: [\n                { uid: 'header-1', name: 'Content-Type', value: 'application/json', enabled: true }\n              ],\n              params: [],\n              body: {\n                mode: 'graphql',\n                graphql: {\n                  query: 'query { user { name } }',\n                  variables: '{}'\n                }\n              }\n            },\n            examples: [\n              {\n                uid: 'example-1',\n                itemUid: 'graphql-request-1',\n                name: 'GraphQL Success',\n                description: 'Successful GraphQL query',\n                type: 'graphql-request',\n                request: {\n                  url: 'https://api.example.com/graphql',\n                  method: 'POST',\n                  headers: [\n                    { uid: 'ex-header-1', name: 'Content-Type', value: 'application/json', enabled: true }\n                  ],\n                  params: [],\n                  body: {\n                    mode: 'graphql',\n                    graphql: {\n                      query: 'query { user { name } }',\n                      variables: '{}'\n                    }\n                  }\n                },\n                response: {\n                  status: 200,\n                  statusText: 'OK',\n                  headers: [\n                    { uid: 'res-header-1', name: 'Content-Type', value: 'application/json', enabled: true }\n                  ],\n                  body: '{\"data\": {\"user\": {\"name\": \"John Doe\"}}}'\n                }\n              }\n            ]\n          }\n        ]\n      };\n\n      const result = transformCollectionToSaveToExportAsFile(collection);\n      const graphqlRequest = result.items[0];\n\n      expect(graphqlRequest.examples).toHaveLength(1);\n      expect(graphqlRequest.examples[0].type).toBe('graphql-request');\n      expect(graphqlRequest.examples[0].request.url).toBe('https://api.example.com/graphql');\n      expect(graphqlRequest.examples[0].response.body).toBe('{\"data\": {\"user\": {\"name\": \"John Doe\"}}}');\n    });\n\n    it('should handle requests without examples', () => {\n      const collection = {\n        uid: 'test-collection',\n        name: 'Test Collection',\n        items: [\n          {\n            uid: 'http-request-1',\n            type: 'http-request',\n            name: 'Test HTTP Request',\n            request: {\n              url: 'https://api.example.com/test',\n              method: 'GET',\n              headers: [],\n              params: [],\n              body: { mode: 'none' }\n            }\n          }\n        ]\n      };\n\n      const result = transformCollectionToSaveToExportAsFile(collection);\n      const httpRequest = result.items[0];\n\n      expect(httpRequest.examples).toHaveLength(0);\n    });\n  });\n\n  describe('transformRequestToSaveToFilesystem', () => {\n    it('should preserve examples when saving request to filesystem', () => {\n      const httpRequest = {\n        uid: 'http-request-1',\n        type: 'http-request',\n        name: 'Test HTTP',\n        request: {\n          url: 'https://api.example.com/test',\n          method: 'POST',\n          headers: [],\n          params: [],\n          body: { mode: 'json', json: '{}' }\n        },\n        examples: [\n          {\n            uid: 'example-1',\n            itemUid: 'http-request-1',\n            name: 'Test Example',\n            description: 'Test description',\n            type: 'http-request',\n            request: {\n              url: 'https://api.example.com/test',\n              method: 'POST',\n              headers: [],\n              params: [],\n              body: { mode: 'json', json: '{}' }\n            },\n            response: {\n              status: 200,\n              statusText: 'OK',\n              headers: [],\n              body: '{\"success\": true}'\n            }\n          }\n        ]\n      };\n\n      const result = transformRequestToSaveToFilesystem(httpRequest);\n\n      expect(result.examples).toHaveLength(1);\n      expect(result.examples[0].name).toBe('Test Example');\n      expect(result.examples[0].response.status).toEqual(200);\n    });\n  });\n\n  describe('exportCollection', () => {\n    it('should remove UIDs from examples during export', () => {\n      const collection = {\n        uid: 'test-collection',\n        name: 'Test Collection',\n        items: [\n          {\n            uid: 'http-request-1',\n            type: 'http-request',\n            name: 'Test HTTP Request',\n            request: {\n              url: 'https://api.example.com/test',\n              method: 'POST',\n              headers: [\n                { uid: 'header-1', name: 'Content-Type', value: 'application/json', enabled: true }\n              ],\n              params: [],\n              body: { mode: 'json', json: '{}' }\n            },\n            examples: [\n              {\n                uid: 'example-1',\n                itemUid: 'http-request-1',\n                name: 'Test Example',\n                description: 'Test description',\n                type: 'http-request',\n                request: {\n                  url: 'https://api.example.com/test',\n                  method: 'POST',\n                  headers: [\n                    { uid: 'ex-header-1', name: 'Content-Type', value: 'application/json', enabled: true }\n                  ],\n                  params: [],\n                  body: { mode: 'json', json: '{}' }\n                },\n                response: {\n                  status: '200',\n                  statusText: 'OK',\n                  headers: [\n                    { uid: 'res-header-1', name: 'Content-Type', value: 'application/json', enabled: true }\n                  ],\n                  body: '{\"success\": true}'\n                }\n              }\n            ]\n          }\n        ]\n      };\n\n      // Test the deleteUidsInItems function directly\n      const itemsCopy = JSON.parse(JSON.stringify(collection.items));\n      deleteUidsInItems(itemsCopy);\n\n      const httpRequest = itemsCopy[0];\n      expect(httpRequest.uid).toBeUndefined();\n      expect(httpRequest.examples[0].uid).toBeUndefined();\n      expect(httpRequest.examples[0].itemUid).toBeUndefined();\n      expect(httpRequest.request.headers[0].uid).toBeUndefined();\n      expect(httpRequest.examples[0].request.headers[0].uid).toBeUndefined();\n      expect(httpRequest.examples[0].response.headers[0].uid).toBeUndefined();\n    });\n\n    it('should transform example types during export', () => {\n      const collection = {\n        uid: 'test-collection',\n        name: 'Test Collection',\n        items: [\n          {\n            uid: 'http-request-1',\n            type: 'http-request',\n            name: 'Test HTTP Request',\n            request: {\n              url: 'https://api.example.com/test',\n              method: 'POST',\n              headers: [],\n              params: [],\n              body: { mode: 'json', json: '{}' }\n            },\n            examples: [\n              {\n                uid: 'example-1',\n                itemUid: 'http-request-1',\n                name: 'Test Example',\n                description: 'Test description',\n                type: 'http-request',\n                request: {\n                  url: 'https://api.example.com/test',\n                  method: 'POST',\n                  headers: [],\n                  params: [],\n                  body: { mode: 'json', json: '{}' }\n                },\n                response: {\n                  status: 200,\n                  statusText: 'OK',\n                  headers: [],\n                  body: '{\"success\": true}'\n                }\n              }\n            ]\n          }\n        ]\n      };\n\n      // Test the transformItem function directly\n      const itemsCopy = JSON.parse(JSON.stringify(collection.items));\n      transformItem(itemsCopy);\n\n      const httpRequest = itemsCopy[0];\n      expect(httpRequest.type).toBe('http');\n      expect(httpRequest.examples[0].type).toBe('http');\n    });\n  });\n\n  describe('transformItemsInCollection', () => {\n    it('should transform example types correctly during import', () => {\n      const collection = {\n        uid: 'test-collection',\n        items: [\n          {\n            uid: 'http-request-1',\n            type: 'http',\n            name: 'Test HTTP',\n            request: {\n              url: 'https://api.example.com/test',\n              method: 'POST',\n              headers: [],\n              params: [],\n              body: { mode: 'json', json: '{}' }\n            },\n            examples: [\n              {\n                uid: 'example-1',\n                itemUid: 'http-request-1',\n                name: 'Test Example',\n                description: 'Test description',\n                type: 'http',\n                request: {\n                  url: 'https://api.example.com/test',\n                  method: 'POST',\n                  headers: [],\n                  params: [],\n                  body: { mode: 'json', json: '{}' }\n                },\n                response: {\n                  status: 200,\n                  statusText: 'OK',\n                  headers: [],\n                  body: '{\"success\": true}'\n                }\n              }\n            ]\n          }\n        ]\n      };\n\n      transformItemsInCollection(collection);\n      const httpRequest = collection.items[0];\n\n      expect(httpRequest.type).toBe('http-request');\n      expect(httpRequest.examples[0].type).toBe('http-request');\n    });\n\n    it('should handle examples without UIDs during import', () => {\n      const collection = {\n        uid: 'test-collection',\n        items: [\n          {\n            uid: 'http-request-1',\n            type: 'http',\n            name: 'Test HTTP',\n            request: {\n              url: 'https://api.example.com/test',\n              method: 'POST',\n              headers: [],\n              params: [],\n              body: { mode: 'json', json: '{}' }\n            },\n            examples: [\n              {\n                name: 'Test Example',\n                description: 'Test description',\n                type: 'http',\n                request: {\n                  url: 'https://api.example.com/test',\n                  method: 'POST',\n                  headers: [],\n                  params: [],\n                  body: { mode: 'json', json: '{}' }\n                },\n                response: {\n                  status: 200,\n                  statusText: 'OK',\n                  headers: [],\n                  body: '{\"success\": true}'\n                }\n              }\n            ]\n          }\n        ]\n      };\n\n      // Test that examples are preserved during transformation\n      transformItemsInCollection(collection);\n      const httpRequest = collection.items[0];\n\n      expect(httpRequest.examples).toHaveLength(1);\n      expect(httpRequest.examples[0].name).toBe('Test Example');\n      expect(httpRequest.examples[0].type).toBe('http-request');\n    });\n  });\n\n  describe('Full Export/Import Cycle', () => {\n    it('should preserve examples through export transformation', () => {\n      const originalCollection = {\n        uid: 'test-collection',\n        name: 'Test Collection',\n        items: [\n          {\n            uid: 'http-request-1',\n            type: 'http-request',\n            name: 'Test HTTP Request',\n            request: {\n              url: 'https://api.example.com/test',\n              method: 'POST',\n              headers: [\n                { uid: 'header-1', name: 'Content-Type', value: 'application/json', enabled: true }\n              ],\n              params: [],\n              body: { mode: 'json', json: '{\"message\": \"test\"}' }\n            },\n            examples: [\n              {\n                uid: 'example-1',\n                itemUid: 'http-request-1',\n                name: 'Success Example',\n                description: 'Example of successful response',\n                type: 'http-request',\n                request: {\n                  url: 'https://api.example.com/test',\n                  method: 'POST',\n                  headers: [\n                    { uid: 'ex-header-1', name: 'Content-Type', value: 'application/json', enabled: true }\n                  ],\n                  params: [],\n                  body: {\n                    mode: 'json',\n                    json: '{\"message\": \"test\"}'\n                  }\n                },\n                response: {\n                  status: 200,\n                  statusText: 'OK',\n                  headers: [\n                    { uid: 'res-header-1', name: 'Content-Type', value: 'application/json', enabled: true }\n                  ],\n                  body: '{\"success\": true, \"data\": \"test\"}'\n                }\n              }\n            ]\n          }\n        ]\n      };\n\n      // Step 1: Export transformation\n      const exportedCollection = transformCollectionToSaveToExportAsFile(originalCollection);\n\n      // Step 2: Simulate export process (remove UIDs and transform types)\n      const collectionCopy = JSON.parse(JSON.stringify(exportedCollection));\n      deleteUidsInItems(collectionCopy.items);\n      transformItem(collectionCopy.items);\n\n      // Step 3: Simulate import process (transform types)\n      transformItemsInCollection(collectionCopy);\n\n      const originalRequest = originalCollection.items[0];\n      const importedRequest = collectionCopy.items[0];\n\n      // Verify the request data is preserved\n      expect(importedRequest.name).toBe(originalRequest.name);\n      expect(importedRequest.type).toBe('http-request');\n      expect(importedRequest.request.url).toBe(originalRequest.request.url);\n      expect(importedRequest.request.method).toBe(originalRequest.request.method);\n\n      // Verify examples are preserved\n      expect(importedRequest.examples).toHaveLength(1);\n      expect(importedRequest.examples[0].name).toBe(originalRequest.examples[0].name);\n      expect(importedRequest.examples[0].description).toBe(originalRequest.examples[0].description);\n      expect(importedRequest.examples[0].type).toBe('http-request');\n      expect(importedRequest.examples[0].request.url).toBe(originalRequest.examples[0].request.url);\n      expect(importedRequest.examples[0].request.method).toBe(originalRequest.examples[0].request.method);\n      expect(importedRequest.examples[0].response.status).toBe(originalRequest.examples[0].response.status);\n      expect(importedRequest.examples[0].response.statusText).toBe(originalRequest.examples[0].response.statusText);\n      expect(importedRequest.examples[0].response.body).toBe(originalRequest.examples[0].response.body);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/utils/tests/collections/grpc-export-import.spec.js",
    "content": "import { transformCollectionToSaveToExportAsFile, transformRequestToSaveToFilesystem } from '../../collections/index';\nimport { transformItemsInCollection } from '../../importers/common';\n\ndescribe('gRPC Export/Import', () => {\n  describe('transformCollectionToSaveToExportAsFile', () => {\n    it('should preserve gRPC-specific fields when exporting collection', () => {\n      const collection = {\n        uid: 'test-collection',\n        name: 'Test Collection',\n        items: [\n          {\n            uid: 'grpc-request-1',\n            type: 'grpc-request',\n            name: 'Test gRPC Request',\n            request: {\n              url: 'grpc://localhost:50051',\n              method: '/randomService/randomMethod',\n              methodType: 'unary',\n              protoPath: 'proto/service.proto',\n              headers: [],\n              body: {\n                mode: 'grpc',\n                grpc: [{ name: 'message', content: '{}' }]\n              }\n            }\n          }\n        ]\n      };\n\n      const result = transformCollectionToSaveToExportAsFile(collection);\n      const grpcRequest = result.items[0];\n\n      expect(grpcRequest.request.methodType).toBe('unary');\n      expect(grpcRequest.request.method).toBe('/randomService/randomMethod');\n      expect(grpcRequest.request.protoPath).toBe('proto/service.proto');\n      expect(grpcRequest.request.params).toBeUndefined();\n    });\n\n    it('should handle different gRPC method types correctly', () => {\n      const collection = {\n        uid: 'test-collection',\n        name: 'Test Collection',\n        items: [\n          {\n            uid: 'grpc-request-1',\n            type: 'grpc-request',\n            name: 'Streaming Request',\n            request: {\n              url: 'grpc://localhost:50051',\n              method: '/randomService/randomMethod',\n              methodType: 'bidi-streaming',\n              protoPath: 'proto/streaming.proto',\n              headers: [],\n              body: { mode: 'grpc', grpc: [] }\n            }\n          }\n        ]\n      };\n\n      const result = transformCollectionToSaveToExportAsFile(collection);\n      const grpcRequest = result.items[0];\n\n      expect(grpcRequest.request.methodType).toBe('bidi-streaming');\n      expect(grpcRequest.request.method).toBe('/randomService/randomMethod');\n      expect(grpcRequest.request.protoPath).toBe('proto/streaming.proto');\n    });\n\n    it('should handle gRPC requests without method', () => {\n      const collection = {\n        uid: 'test-collection',\n        name: 'Test Collection',\n        items: [\n          {\n            uid: 'grpc-request-1',\n            type: 'grpc-request',\n            name: 'Streaming Request',\n            request: {\n              url: 'grpc://localhost:50051',\n              methodType: 'unary',\n              headers: [],\n              body: { mode: 'grpc', grpc: [] }\n            }\n          }\n        ]\n      };\n\n      const result = transformCollectionToSaveToExportAsFile(collection);\n      const grpcRequest = result.items[0];\n\n      expect(grpcRequest.request.methodType).toBe('unary');\n      expect(grpcRequest.request.method).toBeUndefined();\n      expect(grpcRequest.request.protoPath).toBeUndefined();\n    });\n  });\n\n  describe('transformRequestToSaveToFilesystem', () => {\n    it('should preserve gRPC fields and remove params for gRPC requests', () => {\n      const grpcRequest = {\n        uid: 'grpc-request-1',\n        type: 'grpc-request',\n        name: 'Test gRPC',\n        request: {\n          url: 'grpc://localhost:50051',\n          method: '/randomService/randomMethod',\n          methodType: 'server-streaming',\n          protoPath: 'proto/service.proto',\n          params: [{ uid: 'param-1', name: 'test', value: 'value' }],\n          headers: [],\n          body: { mode: 'grpc', grpc: [] }\n        }\n      };\n\n      const result = transformRequestToSaveToFilesystem(grpcRequest);\n\n      expect(result.request.methodType).toBe('server-streaming');\n      expect(result.request.protoPath).toBe('proto/service.proto');\n      expect(result.request.params).toBeUndefined();\n    });\n\n    it('should not remove params for non-gRPC requests', () => {\n      const httpRequest = {\n        uid: 'http-request-1',\n        type: 'http-request',\n        name: 'Test HTTP',\n        request: {\n          url: 'http://localhost:3000',\n          method: 'GET',\n          params: [{ uid: 'param-1', name: 'test', value: 'value' }],\n          headers: [],\n          body: { mode: 'json', json: '{}' }\n        }\n      };\n\n      const result = transformRequestToSaveToFilesystem(httpRequest);\n\n      expect(result.request.params).toHaveLength(1);\n      expect(result.request.params[0].name).toBe('test');\n    });\n  });\n\n  describe('transformItemsInCollection', () => {\n    it('should transform gRPC request type correctly during import', () => {\n      const collection = {\n        uid: 'test-collection',\n        items: [\n          {\n            uid: 'grpc-request-1',\n            type: 'grpc',\n            name: 'Test gRPC',\n            request: {\n              url: 'grpc://localhost:50051',\n              methodType: 'unary',\n              protoPath: 'proto/service.proto',\n              body: { mode: 'grpc', grpc: [] }\n            }\n          }\n        ]\n      };\n\n      transformItemsInCollection(collection);\n      const grpcRequest = collection.items[0];\n\n      expect(grpcRequest.type).toBe('grpc-request');\n      expect(grpcRequest.request.methodType).toBe('unary');\n      expect(grpcRequest.request.protoPath).toBe('proto/service.proto');\n    });\n\n    it('should handle gRPC requests without protoPath', () => {\n      const collection = {\n        uid: 'test-collection',\n        items: [\n          {\n            uid: 'grpc-request-1',\n            type: 'grpc',\n            name: 'Test gRPC',\n            request: {\n              url: 'grpc://localhost:50051',\n              method: '/randomService/randomMethod',\n              methodType: 'client-streaming',\n              body: { mode: 'grpc', grpc: [] }\n            }\n          }\n        ]\n      };\n\n      transformItemsInCollection(collection);\n      const grpcRequest = collection.items[0];\n\n      expect(grpcRequest.type).toBe('grpc-request');\n      expect(grpcRequest.request.methodType).toBe('client-streaming');\n      expect(grpcRequest.request.protoPath).toBeUndefined();\n    });\n\n    it('should handle gRPC requests without method', () => {\n      const collection = {\n        uid: 'test-collection',\n        items: [\n          {\n            uid: 'grpc-request-1',\n            type: 'grpc',\n            name: 'Test gRPC',\n            request: {\n              url: 'grpc://localhost:50051',\n              methodType: 'unary',\n              protoPath: 'proto/service.proto',\n              body: { mode: 'grpc', grpc: [] }\n            }\n          }\n        ]\n      };\n\n      transformItemsInCollection(collection);\n      const grpcRequest = collection.items[0];\n\n      expect(grpcRequest.type).toBe('grpc-request');\n      expect(grpcRequest.request.method).toBeUndefined();\n      expect(grpcRequest.request.methodType).toBe('unary');\n      expect(grpcRequest.request.protoPath).toBe('proto/service.proto');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/utils/tests/collections/items-sequencing.spec.js",
    "content": "import { resetSequencesInFolder, isItemBetweenSequences } from 'utils/collections/index';\n\ndescribe('resetSequencesInFolder', () => {\n  it('should fix the sequences in the folder 1', () => {\n    const folder = {\n      items: [\n        { uid: '1', seq: 1 },\n        { uid: '2', seq: 3 },\n        { uid: '3', seq: 6 }\n      ]\n    };\n\n    const fixedFolder = resetSequencesInFolder(folder.items);\n    expect(fixedFolder).toEqual([\n      { uid: '1', seq: 1 },\n      { uid: '2', seq: 2 },\n      { uid: '3', seq: 3 }\n    ]);\n  });\n\n  it('should fix the sequences in the folder 2', () => {\n    const folder = {\n      items: [\n        { uid: '1', seq: 3 },\n        { uid: '2', seq: 1 },\n        { uid: '3', seq: 2 }\n      ]\n    };\n\n    const fixedFolder = resetSequencesInFolder(folder.items);\n    expect(fixedFolder).toEqual([\n      { uid: '2', seq: 1 },\n      { uid: '3', seq: 2 },\n      { uid: '1', seq: 3 }\n    ]);\n  });\n\n  it('should fix the sequences in the folder with missing sequences', () => {\n    const folder = {\n      items: [\n        { uid: '1', seq: 1 },\n        { uid: '2', type: 'folder' },\n        { uid: '3', type: 'folder' },\n        { uid: '4', seq: 7 }\n      ]\n    };\n\n    const fixedFolder = resetSequencesInFolder(folder.items);\n    expect(fixedFolder).toEqual([\n      { uid: '1', seq: 1 },\n      { uid: '2', seq: 2, type: 'folder' },\n      { uid: '3', seq: 3, type: 'folder' },\n      { uid: '4', seq: 4 }\n    ]);\n  });\n\n  it('should fix the sequences in the folder with same sequences', () => {\n    const folder = {\n      items: [\n        { uid: '1', seq: 2 },\n        { uid: '2', seq: 2 },\n        { uid: '3', seq: 3 },\n        { uid: '4', seq: 1 }\n      ]\n    };\n\n    const fixedFolder = resetSequencesInFolder(folder.items);\n    expect(fixedFolder).toEqual([\n      { uid: '4', seq: 1 },\n      { uid: '1', seq: 2 },\n      { uid: '2', seq: 3 },\n      { uid: '3', seq: 4 }\n    ]);\n  });\n});\n\ndescribe('isItemBetweenSequences', () => {\n  it('should return true if the item is between the sequences 1', () => {\n    const item = { uid: '1', seq: 2 };\n    const draggedSequence = 1;\n    const targetSequence = 5;\n    const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence);\n    expect(result).toBe(true);\n  });\n\n  it('should return true if the item is between the sequences 2', () => {\n    const item = { uid: '1', seq: 2 };\n    const draggedSequence = 1;\n    const targetSequence = 5;\n    const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence);\n    expect(result).toBe(true);\n  });\n\n  it('should return true if the item is between the sequences 3', () => {\n    const item = { uid: '1', seq: 4 };\n    const draggedSequence = 1;\n    const targetSequence = 5;\n    const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence);\n    expect(result).toBe(true);\n  });\n\n  it('should return true if the item is between the sequences 4', () => {\n    const item = { uid: '1', seq: 1 };\n    const draggedSequence = 5;\n    const targetSequence = 1;\n    const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence);\n    expect(result).toBe(true);\n  });\n\n  it('should return false if the item is between the sequences 1', () => {\n    const item = { uid: '1', seq: 1 };\n    const draggedSequence = 1;\n    const targetSequence = 5;\n    const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence);\n    expect(result).toBe(false);\n  });\n\n  it('should return false if the item is between the sequences 2', () => {\n    const item = { uid: '1', seq: 5 };\n    const draggedSequence = 1;\n    const targetSequence = 5;\n    const result = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence);\n    expect(result).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/utils/url/index.js",
    "content": "import find from 'lodash/find';\n\nimport { interpolate } from '@usebruno/common';\n\nconst hasLength = (str) => {\n  if (!str || !str.length) {\n    return false;\n  }\n\n  str = str.trim();\n\n  return str.length > 0;\n};\n\nexport const parsePathParams = (url) => {\n  let uri = url.slice();\n\n  if (!uri || !uri.length) {\n    return [];\n  }\n\n  if (!uri.startsWith('http://') && !uri.startsWith('https://')) {\n    uri = `http://${uri}`;\n  }\n\n  let paths;\n\n  try {\n    uri = new URL(uri);\n    paths = uri.pathname.split('/');\n  } catch (e) {\n    paths = uri.split('/');\n  }\n\n  // Enhanced: also match :param inside parentheses and/or quotes\n  const foundParams = new Set();\n  paths.forEach((segment) => {\n    // traditional path parameters\n    if (segment.startsWith(':')) {\n      const name = segment.slice(1);\n      if (name && !foundParams.has(name)) {\n        foundParams.add(name);\n      }\n      return;\n    }\n\n    // for OData-style parameters (parameters inside parentheses)\n    // Check if segment matches valid OData syntax:\n    // 1. EntitySet('key') or EntitySet(key)\n    // 2. EntitySet(Key1=value1,Key2=value2)\n    // 3. Function(param=value)\n    if (!/^[A-Za-z0-9_.-]+\\([^)]*\\)$/.test(segment)) {\n      return;\n    }\n\n    const paramRegex = /[:](\\w+)/g;\n    let match;\n    while ((match = paramRegex.exec(segment))) {\n      if (!match[1]) continue;\n\n      let name = match[1].replace(/[')\"`]+$/, '');\n      name = name.replace(/^[('\"`]+/, '');\n      if (name && !foundParams.has(name)) {\n        foundParams.add(name);\n      }\n    }\n  });\n  return Array.from(foundParams).map((name) => ({ name, value: '' }));\n};\n\nexport const splitOnFirst = (str, char) => {\n  if (!str || !str.length) {\n    return [str];\n  }\n\n  let index = str.indexOf(char);\n  if (index === -1) {\n    return [str];\n  }\n\n  return [str.slice(0, index), str.slice(index + 1)];\n};\n\nexport const isValidUrl = (url) => {\n  try {\n    new URL(url);\n    return true;\n  } catch (err) {\n    return false;\n  }\n};\n\nexport const isHttpUrl = (url) => {\n  try {\n    const parsed = new URL(url);\n    return parsed.protocol === 'http:' || parsed.protocol === 'https:';\n  } catch {\n    return false;\n  }\n};\n\nexport const interpolateUrl = ({ url, variables }) => {\n  if (!url || !url.length || typeof url !== 'string') {\n    return;\n  }\n\n  return interpolate(url, variables);\n};\n\nexport const interpolateUrlPathParams = (url, params, variables = {}, options = {}) => {\n  const getInterpolatedBasePath = (pathname, params) => {\n    let replacedPathname = pathname\n      .split('/')\n      .map((segment) => {\n        // traditional path parameters\n        if (segment.startsWith(':')) {\n          const name = segment.slice(1);\n          const pathParam = params.find((p) => p?.name === name && p?.type === 'path');\n          return pathParam ? pathParam.value : segment;\n        }\n\n        // for OData-style parameters (parameters inside parentheses)\n        // Check if segment matches valid OData syntax:\n        // 1. EntitySet('key') or EntitySet(key)\n        // 2. EntitySet(Key1=value1,Key2=value2)\n        // 3. Function(param=value)\n        if (!/^[A-Za-z0-9_.-]+\\([^)]*\\)$/.test(segment)) {\n          return segment;\n        }\n\n        const regex = /[:](\\w+)/g;\n        let match;\n        let result = segment;\n        while ((match = regex.exec(segment))) {\n          if (!match[1]) continue;\n\n          let name = match[1].replace(/[')\"`]+$/, '');\n          name = name.replace(/^[('\"`]+/, '');\n          if (!name) continue;\n\n          const pathParam = params.find((p) => p?.name === name && p?.type === 'path');\n          if (pathParam) {\n            result = result.replace(':' + match[1], pathParam.value);\n          }\n        }\n        return result;\n      })\n      .join('/');\n\n    return interpolate(replacedPathname, variables);\n  };\n\n  if (!url.startsWith('http://') && !url.startsWith('https://')) {\n    url = `http://${url}`;\n  }\n\n  // When raw is true, resolve :params via pure string manipulation without\n  // passing through new URL(), which would percent-encode characters like spaces.\n  // This preserves the user's original encoding choices for snippet generation.\n  if (options.raw) {\n    const enabledPathParams = (params || []).filter((p) => p.enabled !== false && p.type === 'path');\n    if (enabledPathParams.length === 0) return url;\n\n    const separatorIdx = url.search(/[?#]/);\n    const pathPart = separatorIdx >= 0 ? url.substring(0, separatorIdx) : url;\n    const rest = separatorIdx >= 0 ? url.substring(separatorIdx) : '';\n\n    // resolvedPath includes the origin (scheme + host) since pathPart is the full URL before ?/#\n    const resolvedPath = getInterpolatedBasePath(pathPart, enabledPathParams);\n    return `${resolvedPath}${rest}`;\n  }\n\n  let uri;\n\n  try {\n    uri = new URL(url);\n  } catch (error) {\n    // if the URL is invalid, return the URL as is\n    return url;\n  }\n\n  const basePath = getInterpolatedBasePath(uri.pathname, params);\n\n  return `${uri.origin}${basePath}${uri?.search || ''}`;\n};\n"
  },
  {
    "path": "packages/bruno-app/src/utils/url/index.spec.js",
    "content": "import { splitOnFirst, parsePathParams, interpolateUrl, interpolateUrlPathParams } from './index';\n\ndescribe('Url Utils - parsePathParams', () => {\n  it('should parse path - case 1', () => {\n    const params = parsePathParams('www.example.com');\n    expect(params).toEqual([]);\n  });\n\n  it('should parse path - case 2', () => {\n    const params = parsePathParams('http://www.example.com');\n    expect(params).toEqual([]);\n  });\n\n  it('should parse path - case 3', () => {\n    const params = parsePathParams('https://www.example.com');\n    expect(params).toEqual([]);\n  });\n\n  it('should parse path - case 4', () => {\n    const params = parsePathParams('https://www.example.com/users/:id');\n    expect(params).toEqual([{ name: 'id', value: '' }]);\n  });\n\n  it('should parse path - case 5', () => {\n    const params = parsePathParams('https://www.example.com/users/:id/');\n    expect(params).toEqual([{ name: 'id', value: '' }]);\n  });\n\n  it('should parse path - case 6', () => {\n    const params = parsePathParams('https://www.example.com/users/:id/:');\n    expect(params).toEqual([{ name: 'id', value: '' }]);\n  });\n\n  it('should parse path - case 7', () => {\n    const params = parsePathParams('https://www.example.com/users/:id/posts/:id');\n    expect(params).toEqual([{ name: 'id', value: '' }]);\n  });\n\n  it('should parse path - case 8', () => {\n    const params = parsePathParams('https://www.example.com/users/:id/posts/:postId');\n    expect(params).toEqual([\n      { name: 'id', value: '' },\n      { name: 'postId', value: '' }\n    ]);\n  });\n\n  it('should parse path param inside parentheses and quotes', () => {\n    const params = parsePathParams('https://example.com/ExchangeRates(\\':ExchangeRateOID\\')');\n    expect(params).toEqual([{ name: 'ExchangeRateOID', value: '' }]);\n  });\n\n  it('should parse path param inside parentheses and no quotes', () => {\n    const params = parsePathParams('https://example.com/ExchangeRates(:ExchangeRateOID)');\n    expect(params).toEqual([{ name: 'ExchangeRateOID', value: '' }]);\n  });\n\n  it('should parse multiple path params inside parentheses', () => {\n    const params = parsePathParams('https://example.com/Exchange(:ExchangeId)/ExchangeRates(:ExchangeRateOID)');\n    expect(params).toEqual([{ name: 'ExchangeId', value: '' }, { name: 'ExchangeRateOID', value: '' }]);\n  });\n\n  it('should parse mix and match of normal and param inside parentheses', () => {\n    const params = parsePathParams('https://example.com/Exchange(:ExchangeId)/:key');\n    expect(params).toEqual([{ name: 'ExchangeId', value: '' }, { name: 'key', value: '' }]);\n  });\n\n  // OData-specific test cases for enhanced path parameter parsing\n  it('should parse OData entity key with single quotes', () => {\n    const params = parsePathParams('https://example.com/odata/Products(\\':productId\\')');\n    expect(params).toEqual([{ name: 'productId', value: '' }]);\n  });\n\n  it('should parse OData entity key with double quotes', () => {\n    const params = parsePathParams('https://example.com/odata/Products(\":productId\")');\n    expect(params).toEqual([{ name: 'productId', value: '' }]);\n  });\n\n  it('should parse OData entity key with backticks', () => {\n    const params = parsePathParams('https://example.com/odata/Products(`:productId`)');\n    expect(params).toEqual([{ name: 'productId', value: '' }]);\n  });\n\n  it('should handle OData parameters with mixed quote types', () => {\n    const params = parsePathParams('https://example.com/odata/Products(\\':productId\\')/Categories(\":categoryId\")');\n    expect(params).toEqual([{ name: 'productId', value: '' }, { name: 'categoryId', value: '' }]);\n  });\n\n  it('should parse OData entity key with parentheses only', () => {\n    const params = parsePathParams('https://example.com/odata/Products(:productId)');\n    expect(params).toEqual([{ name: 'productId', value: '' }]);\n  });\n\n  it('should parse OData composite key with mixed parameter styles', () => {\n    // Test both positional and named parameter styles in the same key\n    const params = parsePathParams('https://example.com/odata/Orders(:orderId,ProductId=:productId)');\n    expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'productId', value: '' }]);\n  });\n\n  it('should parse OData navigation property with key', () => {\n    const params = parsePathParams('https://example.com/odata/Orders(:orderId)/Items(\\':itemId\\')');\n    expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'itemId', value: '' }]);\n  });\n\n  it('should parse OData function with parameters', () => {\n    const params = parsePathParams('https://example.com/odata/GetProductsByCategory(categoryId=\\':categoryId\\')');\n    expect(params).toEqual([{ name: 'categoryId', value: '' }]);\n  });\n\n  it('should parse OData action with complex parameters', () => {\n    const params = parsePathParams('https://example.com/odata/Products(\\':productId\\')/Rate(rating=:rating,comment=\\':comment\\')');\n    expect(params).toEqual([{ name: 'productId', value: '' }, { name: 'rating', value: '' }, { name: 'comment', value: '' }]);\n  });\n\n  it('should handle OData parameters with special characters in names', () => {\n    const params = parsePathParams('https://example.com/odata/Products(\\':product-id\\')');\n    expect(params).toEqual([{ name: 'product', value: '' }]);\n  });\n\n  it('should handle OData parameters with underscores in names', () => {\n    const params = parsePathParams('https://example.com/odata/Products(\\':product_id\\')');\n    expect(params).toEqual([{ name: 'product_id', value: '' }]);\n  });\n\n  it('should parse OData composite key with positional parameters', () => {\n    const params = parsePathParams('https://example.com/odata/Orders(:orderId,:productId)');\n    expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'productId', value: '' }]);\n  });\n\n  it('should parse OData composite key with named parameters', () => {\n    const params = parsePathParams('https://example.com/odata/Orders(OrderId=:orderId,ProductId=:productId)');\n    expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'productId', value: '' }]);\n  });\n\n  it('should handle OData navigation properties', () => {\n    const params = parsePathParams('https://example.com/odata/Orders(:orderId)/Items');\n    expect(params).toEqual([{ name: 'orderId', value: '' }]);\n  });\n\n  it('should handle OData function parameters', () => {\n    const params = parsePathParams('https://example.com/odata/Products/GetProductsByCategory(categoryId=:categoryId)');\n    expect(params).toEqual([{ name: 'categoryId', value: '' }]);\n  });\n\n  it('should handle OData parameters with query options in path', () => {\n    const params = parsePathParams('https://example.com/odata/Products(\\':productId\\')?$expand=Category');\n    expect(params).toEqual([{ name: 'productId', value: '' }]);\n  });\n\n  it('should handle OData parameters with multiple segments and mixed syntax', () => {\n    const params = parsePathParams('https://example.com/odata/Orders(:orderId)/Items(\\':itemId\\')/Properties(:propName)');\n    expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'itemId', value: '' }, { name: 'propName', value: '' }]);\n  });\n\n  it('should handle OData parameters with empty string values', () => {\n    const params = parsePathParams('https://example.com/odata/Products(\\'\\')');\n    expect(params).toEqual([]);\n  });\n\n  it('should NOT treat embedded colons as path parameters (regression fix)', () => {\n    // This test case reproduces the bug reported in issue #5805\n    const params = parsePathParams('/start/1:2:AHLS-HASD/form');\n    expect(params).toEqual([]);\n  });\n\n  it('should NOT treat embedded colons as path parameters in full URLs', () => {\n    const params = parsePathParams('https://example.com/start/1:2:AHLS-HASD/form');\n    expect(params).toEqual([]);\n  });\n});\n\ndescribe('Url Utils - URN parsing', () => {\n  it('should handle basic URN segments correctly', () => {\n    // Test case from issue #5817 - Don't treat URN segments as path parameters\n    const params = parsePathParams('https://example.com/urn:ard:show:3479462da794e97');\n    expect(params).toEqual([]);\n\n    // Test case for path parameter that starts with urn:\n    const params2 = parsePathParams('https://example.com/:urn_type');\n    expect(params2).toEqual([{ name: 'urn_type', value: '' }]);\n  });\n\n  it('should handle URNs with special characters', () => {\n    const params = parsePathParams('https://example.com/urn:isbn:0-330.12345-X');\n    expect(params).toEqual([]);\n\n    // URN with percent-encoded characters\n    const params2 = parsePathParams('https://example.com/urn:uuid:6e8bc430%2D9c3a-11d9-9669-0800200c9a66');\n    expect(params).toEqual([]);\n  });\n\n  it('should handle mixed URN and path parameter scenarios', () => {\n    // URN followed by path parameter\n    const params = parsePathParams('https://example.com/urn:nbn:de:bvb/123/:section');\n    expect(params).toEqual([{ name: 'section', value: '' }]);\n\n    // Path parameter followed by URN\n    const params2 = parsePathParams('https://example.com/:type/urn:isbn:123');\n    expect(params2).toEqual([{ name: 'type', value: '' }]);\n\n    // URN-like path parameter (not a real URN)\n    const params3 = parsePathParams('https://example.com/:urn:type');\n    expect(params3).toEqual([{ name: 'urn:type', value: '' }]);\n  });\n\n  it('should handle edge cases with URN-like patterns', () => {\n    // URN with uppercase (should be case-insensitive)\n    const params = parsePathParams('https://example.com/URN:isbn:123');\n    expect(params).toEqual([]);\n\n    // Path that looks like URN but isn't\n    const params2 = parsePathParams('https://example.com/noturn:something:here');\n    expect(params2).toEqual([]);\n\n    // Multiple colons in path parameter\n    const params3 = parsePathParams('https://example.com/:urn:isbn:type');\n    expect(params3).toEqual([{ name: 'urn:isbn:type', value: '' }]);\n  });\n\n  it('should handle URNs in complex URLs', () => {\n    // URN with query parameters\n    const params = parsePathParams('https://example.com/urn:isbn:123?format=:format');\n    expect(params).toEqual([]);\n\n    // Multiple URNs and path parameters\n    const params2 = parsePathParams('https://example.com/:category/urn:isbn:123/:subcategory/urn:issn:456');\n    expect(params2).toEqual([\n      { name: 'category', value: '' },\n      { name: 'subcategory', value: '' }\n    ]);\n\n    // URN with fragment\n    const params3 = parsePathParams('https://example.com/urn:nbn:de:bvb:123#:section');\n    expect(params3).toEqual([]);\n  });\n});\n\ndescribe('Url Utils - OData parameters', () => {\n  it('should handle OData parameters with escaped quotes', () => {\n    const params = parsePathParams('https://example.com/odata/Products(\\'ABC\\'\\'123\\')');\n    expect(params).toEqual([]);\n  });\n\n  it('should handle OData parameters with spaces in quotes', () => {\n    const params = parsePathParams('https://example.com/odata/Products(\\'Product Name With Spaces\\')');\n    expect(params).toEqual([]);\n  });\n\n  it('should handle OData parameters with numeric keys', () => {\n    const params = parsePathParams('https://example.com/odata/Products(12345)');\n    expect(params).toEqual([]);\n  });\n\n  it('should handle OData parameters with GUID keys', () => {\n    const params = parsePathParams('https://example.com/odata/Products(\\'123e4567-e89b-12d3-a456-426614174000\\')');\n    expect(params).toEqual([]);\n  });\n\n  it('should handle OData with query parameters for variable interpolation', () => {\n    const params = parsePathParams('https://example.com/odata/Products?$filter=Category eq \\'{{category}}\\'&$orderby={{sortField}}');\n    expect(params).toEqual([]);\n  });\n});\n\ndescribe('Url Utils - splitOnFirst', () => {\n  it('should split on first - case 1', () => {\n    const params = splitOnFirst('a', '=');\n    expect(params).toEqual(['a']);\n  });\n\n  it('should split on first - case 2', () => {\n    const params = splitOnFirst('a=', '=');\n    expect(params).toEqual(['a', '']);\n  });\n\n  it('should split on first - case 3', () => {\n    const params = splitOnFirst('a=1', '=');\n    expect(params).toEqual(['a', '1']);\n  });\n\n  it('should split on first - case 4', () => {\n    const params = splitOnFirst('a=1&b=2', '=');\n    expect(params).toEqual(['a', '1&b=2']);\n  });\n\n  it('should split on first - case 5', () => {\n    const params = splitOnFirst('a=1&b=2', '&');\n    expect(params).toEqual(['a=1', 'b=2']);\n  });\n});\n\ndescribe('Url Utils - interpolateUrl, interpolateUrlPathParams', () => {\n  it('should interpolate url correctly', () => {\n    const url = '{{host}}/api/:id/path?foo={{foo}}&bar={{bar}}&baz={{process.env.baz}}';\n    const expectedUrl = 'https://example.com/api/:id/path?foo=foo_value&bar=bar_value&baz=baz_value';\n\n    const result = interpolateUrl({ url, variables: { 'host': 'https://example.com', 'foo': 'foo_value', 'bar': 'bar_value', 'process.env.baz': 'baz_value' } });\n\n    expect(result).toEqual(expectedUrl);\n  });\n\n  it('should interpolate path params correctly', () => {\n    const url = 'https://example.com/api/:id/path';\n    const params = [{ name: 'id', type: 'path', enabled: true, value: '123' }];\n    const expectedUrl = 'https://example.com/api/123/path';\n\n    const result = interpolateUrlPathParams(url, params);\n\n    expect(result).toEqual(expectedUrl);\n  });\n\n  it('should interpolate url and path params correctly', () => {\n    const url = '{{host}}/api/:id/path?foo={{foo}}&bar={{bar}}&baz={{process.env.baz}}';\n    const params = [{ name: 'id', type: 'path', enabled: true, value: '123' }];\n    const expectedUrl = 'https://example.com/api/123/path?foo=foo_value&bar=bar_value&baz=baz_value';\n\n    const intermediateResult = interpolateUrl({ url, variables: { 'host': 'https://example.com', 'foo': 'foo_value', 'bar': 'bar_value', 'process.env.baz': 'baz_value' } });\n    const result = interpolateUrlPathParams(intermediateResult, params);\n\n    expect(result).toEqual(expectedUrl);\n  });\n\n  it('should interpolate path params correctly', () => {\n    const url = 'https://example.com/api/:id/path/:user';\n    const params = [{ name: 'id', type: 'path', enabled: true, value: '123' }, { name: 'user', type: 'path', enabled: true, value: '{{user}}' }];\n    const variables = { user: 'John' };\n    const expectedUrl = 'https://example.com/api/123/path/John';\n\n    const result = interpolateUrlPathParams(url, params, variables);\n\n    expect(result).toEqual(expectedUrl);\n  });\n\n  it('should interpolate url and path params correctly', () => {\n    const url = '{{host}}/api/:id/path/:user?foo={{foo}}&bar={{bar}}&baz={{process.env.baz}}';\n    const params = [{ name: 'id', type: 'path', enabled: true, value: '123' }, { name: 'user', type: 'path', enabled: true, value: '{{user}}' }];\n    const variables = { 'host': 'https://example.com', 'foo': 'foo_value', 'bar': 'bar_value', 'process.env.baz': 'baz_value', 'user': 'John' };\n    const expectedUrl = 'https://example.com/api/123/path/John?foo=foo_value&bar=bar_value&baz=baz_value';\n\n    const intermediateResult = interpolateUrl({ url, variables });\n    const result = interpolateUrlPathParams(intermediateResult, params, variables);\n\n    expect(result).toEqual(expectedUrl);\n  });\n\n  it('should handle empty params', () => {\n    const url = 'https://example.com/api';\n    const params = [];\n    const expectedUrl = 'https://example.com/api';\n\n    const result = interpolateUrlPathParams(url, params);\n\n    expect(result).toEqual(expectedUrl);\n  });\n\n  it('should handle invalid URL, case 1', () => {\n    const url = 'example.com/api/:id';\n    const params = [{ name: 'id', type: 'path', enabled: true, value: '123' }];\n    const expectedUrl = 'http://example.com/api/123';\n\n    const result = interpolateUrlPathParams(url, params);\n\n    expect(result).toEqual(expectedUrl);\n  });\n\n  it('should handle invalid URL, case 2', () => {\n    const url = 'http://1.1.1.1:3000:id';\n    const params = [{ name: 'id', type: 'path', enabled: true, value: '123' }];\n    const expectedUrl = 'http://1.1.1.1:3000:id';\n\n    const result = interpolateUrlPathParams(url, params);\n\n    expect(result).toEqual(expectedUrl);\n  });\n});\n\ndescribe('Url Utils - interpolateUrlPathParams with { raw: true }', () => {\n  it('should resolve :params without encoding (spaces stay as spaces)', () => {\n    const url = 'https://example.com/api/:id/path';\n    const params = [{ name: 'id', type: 'path', enabled: true, value: 'hello world' }];\n\n    const result = interpolateUrlPathParams(url, params, {}, { raw: true });\n\n    expect(result).toEqual('https://example.com/api/hello world/path');\n  });\n\n  it('should preserve query string and fragment as-is', () => {\n    const url = 'https://example.com/api/:id?foo=bar&baz=qux#section';\n    const params = [{ name: 'id', type: 'path', enabled: true, value: '123' }];\n\n    const result = interpolateUrlPathParams(url, params, {}, { raw: true });\n\n    expect(result).toEqual('https://example.com/api/123?foo=bar&baz=qux#section');\n  });\n\n  it('should return URL unchanged when no path params match', () => {\n    const url = 'https://example.com/api/path?q=1';\n    const params = [{ name: 'id', type: 'path', enabled: true, value: '123' }];\n\n    const result = interpolateUrlPathParams(url, params, {}, { raw: true });\n\n    expect(result).toEqual('https://example.com/api/path?q=1');\n  });\n\n  it('should return URL unchanged when params array is empty', () => {\n    const url = 'https://example.com/api/:id';\n    const params = [];\n\n    const result = interpolateUrlPathParams(url, params, {}, { raw: true });\n\n    expect(result).toEqual('https://example.com/api/:id');\n  });\n\n  it('should handle OData-style params', () => {\n    const url = 'https://example.com/odata/Products(\\':productId\\')';\n    const params = [{ name: 'productId', type: 'path', enabled: true, value: 'ABC 123' }];\n\n    const result = interpolateUrlPathParams(url, params, {}, { raw: true });\n\n    expect(result).toEqual('https://example.com/odata/Products(\\'ABC 123\\')');\n  });\n\n  it('should preserve existing percent-encoding', () => {\n    const url = 'https://example.com/api/:id/already%20encoded';\n    const params = [{ name: 'id', type: 'path', enabled: true, value: '456' }];\n\n    const result = interpolateUrlPathParams(url, params, {}, { raw: true });\n\n    expect(result).toEqual('https://example.com/api/456/already%20encoded');\n  });\n\n  it('should skip disabled params', () => {\n    const url = 'https://example.com/api/:id';\n    const params = [{ name: 'id', type: 'path', enabled: false, value: '123' }];\n\n    const result = interpolateUrlPathParams(url, params, {}, { raw: true });\n\n    expect(result).toEqual('https://example.com/api/:id');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-app/src/utils/workspaces/index.js",
    "content": "// Utility functions for workspace pinning and reordering\n\nexport const sortWorkspaces = (workspaces, preferences) => {\n  const pinnedUids = preferences?.workspaces?.pinnedWorkspaceUids || [];\n  const pinnedOrder = preferences?.workspaces?.pinnedOrder || [];\n  const unpinnedOrder = preferences?.workspaces?.unpinnedOrder || [];\n\n  const defaultWs = workspaces.find((w) => w.type === 'default');\n  const pinnedWs = workspaces.filter((w) => w.type !== 'default' && pinnedUids.includes(w.uid));\n  const unpinnedWs = workspaces.filter((w) => w.type !== 'default' && !pinnedUids.includes(w.uid));\n\n  const sortedPinned = [...pinnedWs].sort((a, b) => {\n    const aIndex = pinnedOrder.indexOf(a.uid);\n    const bIndex = pinnedOrder.indexOf(b.uid);\n\n    if (aIndex !== -1 && bIndex !== -1) {\n      return aIndex - bIndex;\n    }\n    if (aIndex !== -1) return -1;\n    if (bIndex !== -1) return 1;\n\n    return (a.name || '').localeCompare(b.name || '');\n  });\n\n  const sortedUnpinned = [...unpinnedWs].sort((a, b) => {\n    const aIndex = unpinnedOrder.indexOf(a.uid);\n    const bIndex = unpinnedOrder.indexOf(b.uid);\n\n    if (aIndex !== -1 && bIndex !== -1) {\n      return aIndex - bIndex;\n    }\n    if (aIndex !== -1) return -1;\n    if (bIndex !== -1) return 1;\n\n    return (a.name || '').localeCompare(b.name || '');\n  });\n\n  // Combine: default -> pinned -> unpinned\n  return [\n    ...(defaultWs ? [defaultWs] : []),\n    ...sortedPinned,\n    ...sortedUnpinned\n  ];\n};\n\nexport const toggleWorkspacePin = (workspaceUid, preferences) => {\n  const pinnedUids = preferences?.workspaces?.pinnedWorkspaceUids || [];\n  const pinnedOrder = preferences?.workspaces?.pinnedOrder || [];\n  const unpinnedOrder = preferences?.workspaces?.unpinnedOrder || [];\n\n  const isPinned = pinnedUids.includes(workspaceUid);\n\n  if (isPinned) {\n    return {\n      ...preferences,\n      workspaces: {\n        ...preferences.workspaces,\n        pinnedWorkspaceUids: pinnedUids.filter((uid) => uid !== workspaceUid),\n        pinnedOrder: pinnedOrder.filter((uid) => uid !== workspaceUid),\n        unpinnedOrder: [...unpinnedOrder, workspaceUid]\n      }\n    };\n  } else {\n    return {\n      ...preferences,\n      workspaces: {\n        ...(preferences?.workspaces || {}),\n        pinnedWorkspaceUids: [...pinnedUids, workspaceUid],\n        pinnedOrder: [...pinnedOrder, workspaceUid],\n        unpinnedOrder: unpinnedOrder.filter((uid) => uid !== workspaceUid)\n      }\n    };\n  }\n};\n\nexport const reorderWorkspaces = (draggedUid, targetUid, dropPosition, preferences) => {\n  const pinnedUids = preferences?.workspaces?.pinnedWorkspaceUids || [];\n  const pinnedOrder = preferences?.workspaces?.pinnedOrder || [];\n  const unpinnedOrder = preferences?.workspaces?.unpinnedOrder || [];\n\n  const isDraggedPinned = pinnedUids.includes(draggedUid);\n  const isTargetPinned = pinnedUids.includes(targetUid);\n\n  if (isDraggedPinned !== isTargetPinned) {\n    return preferences;\n  }\n\n  const orderArray = isDraggedPinned ? [...pinnedOrder] : [...unpinnedOrder];\n\n  const filteredOrder = orderArray.filter((uid) => uid !== draggedUid);\n\n  let targetIndex = filteredOrder.indexOf(targetUid);\n\n  if (targetIndex === -1) {\n    filteredOrder.push(targetUid);\n    targetIndex = filteredOrder.length - 1;\n  }\n\n  const insertIndex = dropPosition === 'after' ? targetIndex + 1 : targetIndex;\n  filteredOrder.splice(insertIndex, 0, draggedUid);\n\n  if (isDraggedPinned) {\n    return {\n      ...preferences,\n      workspaces: {\n        ...(preferences?.workspaces || {}),\n        pinnedOrder: filteredOrder\n      }\n    };\n  } else {\n    return {\n      ...preferences,\n      workspaces: {\n        ...(preferences?.workspaces || {}),\n        unpinnedOrder: filteredOrder\n      }\n    };\n  }\n};\n"
  },
  {
    "path": "packages/bruno-app/storybook/main.js",
    "content": "const path = require('path');\n\n/** @type { import('@storybook/react-webpack5').StorybookConfig } */\nconst config = {\n  stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],\n  addons: [\n    '@storybook/addon-webpack5-compiler-babel'\n  ],\n  framework: {\n    name: '@storybook/react-webpack5',\n    options: {}\n  },\n  docs: {\n    autodocs: true\n  },\n  webpackFinal: async (config) => {\n    // Add path aliases to match jsconfig.json\n    config.resolve.alias = {\n      ...config.resolve.alias,\n      assets: path.resolve(__dirname, '../src/assets'),\n      ui: path.resolve(__dirname, '../src/ui'),\n      components: path.resolve(__dirname, '../src/components'),\n      hooks: path.resolve(__dirname, '../src/hooks'),\n      themes: path.resolve(__dirname, '../src/themes'),\n      api: path.resolve(__dirname, '../src/api'),\n      pageComponents: path.resolve(__dirname, '../src/pageComponents'),\n      providers: path.resolve(__dirname, '../src/providers'),\n      utils: path.resolve(__dirname, '../src/utils')\n    };\n\n    return config;\n  }\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "packages/bruno-app/storybook/preview.jsx",
    "content": "import React from 'react';\nimport { ThemeProvider as SCThemeProvider, createGlobalStyle } from 'styled-components';\nimport themes from 'themes/index';\nimport '@fontsource/inter/400.css';\nimport '@fontsource/inter/500.css';\nimport '@fontsource/inter/600.css';\nimport '@fontsource/inter/700.css';\n\nconst GlobalStyle = createGlobalStyle`\n  * {\n    font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;\n  }\n`;\n\n/** @type { import('@storybook/react').Preview } */\nconst preview = {\n  parameters: {\n    controls: {\n      matchers: {\n        color: /(background|color)$/i,\n        date: /Date$/i\n      }\n    },\n    backgrounds: {\n      default: 'light',\n      values: [\n        { name: 'light', value: '#ffffff' },\n        { name: 'dark', value: '#1e1e1e' }\n      ]\n    }\n  },\n  globalTypes: {\n    theme: {\n      description: 'Global theme for components',\n      toolbar: {\n        title: 'Theme',\n        icon: 'paintbrush',\n        items: [\n          { value: 'light', title: 'Light' },\n          { value: 'dark', title: 'Dark' }\n        ],\n        dynamicTitle: true\n      }\n    }\n  },\n  initialGlobals: {\n    theme: 'light'\n  },\n  decorators: [\n    (Story, context) => {\n      const themeName = context.globals.theme || 'light';\n      const theme = themes[themeName];\n\n      // Update background and text color based on theme\n      const isDark = themeName === 'dark';\n      const backgroundColor = isDark ? '#1e1e1e' : '#ffffff';\n      const textColor = isDark ? '#d4d4d4' : '#333333';\n      document.body.style.backgroundColor = backgroundColor;\n      document.body.style.color = textColor;\n\n      return (\n        <SCThemeProvider theme={theme}>\n          <GlobalStyle />\n          <div style={{ padding: '1rem', color: textColor }}>\n            <Story />\n          </div>\n        </SCThemeProvider>\n      );\n    }\n  ]\n};\n\nexport default preview;\n"
  },
  {
    "path": "packages/bruno-app/tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  darkMode: ['class'],\n  content: ['./src/**/*.{js,jsx}'],\n  prefix: '',\n  theme: {\n    extend: {}\n  },\n  plugins: []\n};\n"
  },
  {
    "path": "packages/bruno-cli/.gitignore",
    "content": "node_modules\nweb\nout\n\npnpm-lock.yaml\npackage-lock.json\nyarn.lock\n"
  },
  {
    "path": "packages/bruno-cli/bin/bru.js",
    "content": "#!/usr/bin/env node\n\nrequire('../src').run();\n"
  },
  {
    "path": "packages/bruno-cli/changelog.md",
    "content": "# Changelog\n\n## 6 Feb 2024\n\nGoing forward, we will release a new version of CLI for every new release of Bruno.\nThis will help us keep the CLI in sync with the Bruno App.\nFor the release notes please see https://github.com/usebruno/bruno/releases\n\n## 1.4.1\n\n- Fixing [bug](https://github.com/usebruno/bruno/issues/1487)\n\n## 1.4.0\n\n- --bail and --test-only flags\n\n## 1.3.0\n\n- Junit report generation\n\n## 1.2.1\n\n- Fixed bug related to `bru.setNextRequest()`\n\n## 1.2.0\n\n- Support for `bru.setNextRequest()`\n\n## 1.1.0\n\n- Upgraded axios to 1.5.1\n\n## 1.0.0\n\n- Announcing Stable Release\n\n## 0.13.0\n\n- feat(#306) Module whitelisting and filesystem access support\n\n## 0.12.0\n\n- show response time in milliseconds per request and total\n\n## 0.11.0\n\n- fix(#119) Support for Basic and Bearer Auth\n\n## 0.10.1\n\n- fix(#233) Fixed Issue related to content header parsing\n\n## 0.10.0\n\n- Support for proxying requests through a proxy server\n\n## 0.9.0\n\n- `--output` flag to collect the results of your API tests\n\n## 0.8.0\n\n- `--env-var` flag to set environment variables\n- loading environment variables from `.env` file\n\n## 0.7.1\n\n- `--cacert` flag to support custom CA certificates\n\n## 0.7.0\n\n- `--insecure` flag to disable SSL verification\n"
  },
  {
    "path": "packages/bruno-cli/examples/report.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <script src=\"https://unpkg.com/vue@3/dist/vue.global.js\"></script>\n    <!-- Would use latest version, you'd better specify a version -->\n    <script src=\"https://unpkg.com/naive-ui\"></script>\n\n    <title>Bruno</title>\n    <style>\n      .error > .status {\n        color: red;\n      }\n      .success > .status {\n        color: green;\n      }\n\n      .n-collapse-item.success > .n-collapse-item__header {\n        background-color: rgba(237, 247, 242, 1);\n      }\n      .n-collapse-item.error > .n-collapse-item__header {\n        background-color: rgba(251, 238, 241, 1);\n      }\n\n      .min-width-150 {\n        min-width: 150px;\n      }\n    </style>\n  </head>\n  <body>\n    <div id=\"app\">\n      <n-config-provider :theme=\"theme\">\n        <n-layout embedded position=\"absolute\" content-style=\"padding: 24px;\">\n          <n-card>\n            <n-flex>\n              <n-page-header title=\"Bruno run dashboard\">\n                <template #avatar>\n                  <n-avatar size=\"large\" style=\"background-color: transparent\">\n                    <svg id=\"emoji\" width=\"34\" viewBox=\"0 0 72 72\" xmlns=\"http://www.w3.org/2000/svg\">\n                      <g id=\"color\">\n                        <path\n                          fill=\"#F4AA41\"\n                          stroke=\"none\"\n                          d=\"M23.5,14.5855l-4.5,1.75l-7.25,8.5l-4.5,10.75l2,5.25c1.2554,3.7911,3.5231,7.1832,7.25,10l2.5-3.3333 c0,0,3.8218,7.7098,10.7384,8.9598c0,0,10.2616,1.936,15.5949-0.8765c3.4203-1.8037,4.4167-4.4167,4.4167-4.4167l3.4167-3.4167 l1.5833,2.3333l2.0833-0.0833l5.4167-7.25L64,37.3355l-0.1667-4.5l-2.3333-5.5l-4.8333-7.4167c0,0-2.6667-4.9167-8.1667-3.9167 c0,0-6.5-4.8333-11.8333-4.0833S32.0833,10.6688,23.5,14.5855z\"\n                        ></path>\n                        <polygon\n                          fill=\"#EA5A47\"\n                          stroke=\"none\"\n                          points=\"36,47.2521 32.9167,49.6688 30.4167,49.6688 30.3333,53.5021 31.0833,57.0021 32.1667,58.9188 35,60.4188 39.5833,59.8355 41.1667,58.0855 42.1667,53.8355 41.9167,49.8355 39.9167,50.0855\"\n                        ></polygon>\n                        <polygon\n                          fill=\"#3F3F3F\"\n                          stroke=\"none\"\n                          points=\"32.5,36.9188 30.9167,40.6688 33.0833,41.9188 34.3333,42.4188 38.6667,42.5855 41.5833,40.3355 39.8333,37.0855\"\n                        ></polygon>\n                      </g>\n                      <g id=\"hair\"></g>\n                      <g id=\"skin\"></g>\n                      <g id=\"skin-shadow\"></g>\n                      <g id=\"line\">\n                        <path\n                          fill=\"#000000\"\n                          stroke=\"none\"\n                          d=\"M29.5059,30.1088c0,0-1.8051,1.2424-2.7484,0.6679c-0.9434-0.5745-1.2424-1.8051-0.6679-2.7484 s1.805-1.2424,2.7484-0.6679S29.5059,30.1088,29.5059,30.1088z\"\n                        ></path>\n                        <path\n                          fill=\"none\"\n                          stroke=\"#000000\"\n                          stroke-linecap=\"round\"\n                          stroke-linejoin=\"round\"\n                          stroke-miterlimit=\"10\"\n                          stroke-width=\"2\"\n                          d=\"M33.1089,37.006h6.1457c0.4011,0,0.7634,0.2397,0.9203,0.6089l1.1579,2.7245l-2.1792,1.1456 c-0.6156,0.3236-1.3654-0.0645-1.4567-0.754\"\n                        ></path>\n                        <path\n                          fill=\"none\"\n                          stroke=\"#000000\"\n                          stroke-linecap=\"round\"\n                          stroke-linejoin=\"round\"\n                          stroke-miterlimit=\"10\"\n                          stroke-width=\"2\"\n                          d=\"M34.7606,40.763c-0.1132,0.6268-0.7757,0.9895-1.3647,0.7471l-2.3132-0.952l1.0899-2.9035 c0.1465-0.3901,0.5195-0.6486,0.9362-0.6486\"\n                        ></path>\n                        <path\n                          fill=\"none\"\n                          stroke=\"#000000\"\n                          stroke-linecap=\"round\"\n                          stroke-linejoin=\"round\"\n                          stroke-miterlimit=\"10\"\n                          stroke-width=\"2\"\n                          d=\"M30.4364,50.0268c0,0-0.7187,8.7934,3.0072,9.9375c2.6459,0.8125,5.1497,0.5324,6.0625-0.25 c0.875-0.75,2.6323-4.4741,1.8267-9.6875\"\n                        ></path>\n                        <path\n                          fill=\"#000000\"\n                          stroke=\"none\"\n                          d=\"M44.2636,30.1088c0,0,1.805,1.2424,2.7484,0.6679c0.9434-0.5745,1.2424-1.8051,0.6679-2.7484 c-0.5745-0.9434-1.805-1.2424-2.7484-0.6679C43.9881,27.9349,44.2636,30.1088,44.2636,30.1088z\"\n                        ></path>\n                        <path\n                          fill=\"none\"\n                          stroke=\"#000000\"\n                          stroke-linecap=\"round\"\n                          stroke-linejoin=\"round\"\n                          stroke-miterlimit=\"10\"\n                          stroke-width=\"2\"\n                          d=\"M25.6245,42.8393c-0.475,3.6024,2.2343,5.7505,4.2847,6.8414c1.1968,0.6367,2.6508,0.5182,3.7176-0.3181l2.581-2.0233l2.581,2.0233 c1.0669,0.8363,2.5209,0.9548,3.7176,0.3181c2.0504-1.0909,4.7597-3.239,4.2847-6.8414\"\n                        ></path>\n                        <path\n                          fill=\"none\"\n                          stroke=\"#000000\"\n                          stroke-linecap=\"round\"\n                          stroke-linejoin=\"round\"\n                          stroke-miterlimit=\"10\"\n                          stroke-width=\"2\"\n                          d=\"M19.9509,28.3572c-2.3166,5.1597-0.5084,13.0249,0.119,15.3759c0.122,0.4571,0.0755,0.9355-0.1271,1.3631l-1.9874,4.1937 c-0.623,1.3146-2.3934,1.5533-3.331,0.4409c-3.1921-3.7871-8.5584-11.3899-6.5486-16.686 c7.0625-18.6104,15.8677-18.1429,15.8677-18.1429c2.8453-1.9336,13.1042-6.9375,24.8125,0.875c0,0,8.6323-1.7175,14.9375,16.9375 c1.8036,5.3362-3.4297,12.8668-6.5506,16.6442c-0.9312,1.127-2.7162,0.8939-3.3423-0.4272l-1.9741-4.1656 c-0.2026-0.4275-0.2491-0.906-0.1271-1.3631c0.6275-2.3509,2.4356-10.2161,0.119-15.3759\"\n                        ></path>\n                        <path\n                          fill=\"none\"\n                          stroke=\"#000000\"\n                          stroke-linecap=\"round\"\n                          stroke-linejoin=\"round\"\n                          stroke-miterlimit=\"10\"\n                          stroke-width=\"2\"\n                          d=\"M52.6309,46.4628c0,0-3.0781,6.7216-7.8049,8.2712\"\n                        ></path>\n                        <path\n                          fill=\"none\"\n                          stroke=\"#000000\"\n                          stroke-linecap=\"round\"\n                          stroke-linejoin=\"round\"\n                          stroke-miterlimit=\"10\"\n                          stroke-width=\"2\"\n                          d=\"M19.437,46.969c0,0,3.0781,6.0823,7.8049,7.632\"\n                        ></path>\n                        <line\n                          x1=\"36.2078\"\n                          x2=\"36.2078\"\n                          y1=\"47.3393\"\n                          y2=\"44.3093\"\n                          fill=\"none\"\n                          stroke=\"#000000\"\n                          stroke-linecap=\"round\"\n                          stroke-linejoin=\"round\"\n                          stroke-miterlimit=\"10\"\n                          stroke-width=\"2\"\n                        ></line>\n                      </g>\n                    </svg>\n                  </n-avatar>\n                </template>\n                <template #extra>\n                  <n-flex justify=\"end\">\n                    <n-switch v-model:value=\"darkMode\" :rail-style=\"darkModeRailStyle\">\n                      <template #checked> Dark </template>\n                      <template #unchecked> Light </template>\n                    </n-switch>\n                  </n-flex>\n                </template>\n              </n-page-header>\n              <n-tabs type=\"segment\" animated>\n                <n-tab-pane name=\"summary\" tab=\"Summary\">\n                  <x-summary :res=\"res\"></x-summary>\n                </n-tab-pane>\n                <n-tab-pane name=\"Requests\" tab=\"Requests\">\n                  <x-requests :res=\"res\"></x-requests>\n                </n-tab-pane>\n              </n-tabs>\n            </n-flex>\n          </n-card>\n        </n-layout>\n      </n-config-provider>\n    </div>\n    <script type=\"text/x-template\" id=\"summary-component\">\n      <n-flex vertical>\n        <n-flex justify=\"center\">\n          <n-alert type=\"success\">\n            <n-statistic\n              label=\"Total Controls\"\n              :value=\"summaryTotalControls\"\n            >\n            </n-statistic>\n          </n-alert>\n          <n-alert :type=\"summaryFailedControls ? 'error' : 'success'\">\n            <n-statistic\n              label=\"Total Failed Controls\"\n              :value=\"summaryFailedControls\"\n            >\n            </n-statistic>\n          </n-alert>\n          <n-alert :type=\"summaryErrors ? 'error' : 'success'\">\n            <n-statistic label=\"Total errors\" :value=\"summaryErrors\">\n            </n-statistic>\n          </n-alert>\n        </n-flex>\n        <n-card title=\"TIMINGS AND DATA\">\n          <n-flex justify=\"center\">\n            <n-statistic\n              label=\"Total run duration\"\n              :value=\"Math.round(totalRunDuration*1000)/1000\"\n            >\n              <template #suffix>s</template>\n            </n-statistic>\n            <n-statistic\n              label=\"Total requests\"\n              :value=\"summaryTotalRequests\"\n            >\n            </n-statistic>\n          </n-flex>\n        </n-card>\n        <n-data-table :columns=\"summaryColumns\" :data=\"summaryData\" />\n      </n-flex>\n    </script>\n    <script type=\"text/x-template\" id=\"requests-component\">\n      <n-flex vertical>\n        <n-switch\n          v-model:value=\"onlyFailed\"\n          :rail-style=\"railStyle\"\n        >\n          <template #checked> Only Failed </template>\n          <template #unchecked> Only Failed </template>\n        </n-switch>\n\n        <n-collapse>\n          <x-results-group v-for=\"(results, group) in groupedResults\" :results=\"results\" :group=\"group\" :key=\"group + '-' + results.length\"></x-results-group>\n        </n-collapse>\n      </n-flex>\n    </script>\n    <script type=\"text/x-template\" id=\"results-group-component\">\n      <n-collapse-item\n        :name=\"group\"\n        arrow-placement=\"right\"\n      >\n        <template #header>\n          <n-alert\n            :type=\"hasError || hasFailure ? 'error' : 'success'\"\n            :bordered=\"false\"\n          >\n            <template #header>\n              {{group}} - {{totalPassed}} / {{total}} Passed {{ hasError? \" - Error\" : \"\" }}\n            </template>\n          </n-alert>\n        </template>\n        <n-collapse>\n          <x-result v-for=\"(result, index) in results\" :result=\"result\" :group=\"group\" :key=\"index\"></x-result>\n        </n-collapse>\n      </n-collapse-item>\n    </script>\n    <script type=\"text/x-template\" id=\"result-component\">\n      <n-collapse-item\n        :name=\"name\"\n        arrow-placement=\"right\"\n      >\n        <template #header>\n          <n-alert\n            :type=\"hasError || hasFailure ? 'error' : 'success'\"\n            :bordered=\"false\"\n          >\n            <template #header>\n              {{suitename}} - {{totalPassed}} / {{total}} Passed {{hasError ? \" - Error\" : \"\" }}\n            </template>\n          </n-alert>\n        </template>\n        <n-flex vertical>\n          <n-grid x-gap=\"12\" :cols=\"2\">\n            <n-gi>\n              <n-card title=\"REQUEST INFORMATION\">\n                <n-list>\n                  <n-list-item>\n                    <n-thing\n                      title=\"File\"\n                      :description=\"result.test.filename\"\n                    />\n                  </n-list-item>\n                  <n-list-item>\n                    <n-thing\n                      title=\"Request Method\"\n                      :description=\"result.request.method\"\n                    />\n                  </n-list-item>\n                  <n-list-item>\n                    <n-thing\n                      title=\"Request URL\"\n                      :description=\"result.request.url\"\n                    />\n                  </n-list-item>\n                </n-list>\n              </n-card>\n            </n-gi>\n            <n-gi>\n              <n-card title=\"RESPONSE INFORMATION\">\n                <n-list>\n                  <n-list-item>\n                    <n-thing\n                      title=\"Response Code\"\n                      :description=\"'' + result.response.status\"\n                    />\n                  </n-list-item>\n                  <n-list-item>\n                    <n-thing\n                      title=\"Response time\"\n                      :description=\"result.response.responseTime + ' ms'\"\n                    />\n                  </n-list-item>\n                  <n-list-item>\n                    <n-thing\n                      title=\"Test duration\"\n                      :description=\"testDuration\"\n                    />\n                  </n-list-item>\n                </n-list>\n              </n-card>\n            </n-gi>\n          </n-grid>\n          <n-alert v-if=\"hasError\" title=\"Error\" type=\"error\">\n            {{result.error}}\n          </n-alert>\n          <n-card title=\"REQUEST HEADERS\">\n            <n-data-table\n              :columns=\"headerColumns\"\n              :data=\"headerDataRequest\"\n            />\n          </n-card>\n          <n-card\n            v-if=\"result.request.data\"\n            title=\"REQUEST BODY\"\n          >\n            <pre>{{result.request.data}}</pre>\n          </n-card>\n          <n-card title=\"RESPONSE HEADERS\">\n            <n-data-table\n              :columns=\"headerColumns\"\n              :data=\"headerDataResponse\"\n            />\n          </n-card>\n          <n-card\n            v-if=\"result.response.data\"\n            title=\"RESPONSE BODY\"\n          >\n            <pre>{{result.response.data}}</pre>\n          </n-card>\n          <n-card title=\"ASSERTIONS INFORMATION\">\n            <n-data-table\n              :columns=\"assertionsColumns\"\n              :data=\"result.assertionResults\"\n              :row-class-name=\"assertionsRowClassName\"\n            />\n          </n-card>\n          <n-card title=\"TESTS INFORMATION\">\n            <n-data-table\n              :columns=\"testsColumns\"\n              :data=\"result.testResults\"\n              :row-class-name=\"testsRowClassName\"\n            />\n          </n-card>\n        </n-flex>\n      </n-collapse-item>\n    </script>\n    <script>\n      const { createApp, ref, computed } = Vue;\n\n      const App = {\n        setup() {\n          const res = {\n            summary: {\n              totalRequests: 10,\n              passedRequests: 10,\n              failedRequests: 0,\n              totalAssertions: 4,\n              passedAssertions: 0,\n              failedAssertions: 4,\n              totalTests: 0,\n              passedTests: 0,\n              failedTests: 0\n            },\n            results: [\n              {\n                test: {\n                  filename: 'group1/test1.bru'\n                },\n                request: {\n                  method: 'GET',\n                  url: 'http://localhost:3000/test/v4',\n                  headers: {\n                    Accept: '*/*',\n                    'Accept-Encoding': 'gzip, deflate, br'\n                  }\n                },\n                response: {\n                  status: 404,\n                  statusText: 'Not Found',\n                  headers: {\n                    'x-powered-by': 'Express',\n                    'content-security-policy': \"default-src 'none'\",\n                    'x-content-type-options': 'nosniff',\n                    'content-type': 'text/html; charset=utf-8',\n                    'content-length': '146',\n                    date: 'Fri, 29 Sep 2023 00:37:50 GMT',\n                    connection: 'close'\n                  },\n                  responseTime: 96,\n                  data: '<!DOCTYPE html>\\n<html lang=\"en\">\\n<head>\\n<meta charset=\"utf-8\">\\n<title>Error</title>\\n</head>\\n<body>\\n<pre>Cannot GET /test/v4</pre>\\n</body>\\n</html>\\n'\n                },\n                error: null,\n                assertionResults: [\n                  {\n                    uid: 'oidgfXLiyD8Jv0NBAHUHF',\n                    lhsExpr: 'res.status',\n                    rhsExpr: '200',\n                    rhsOperand: '200',\n                    operator: 'eq',\n                    status: 'fail',\n                    error: 'expected 404 to equal 200'\n                  }\n                ],\n                testResults: [\n                  {\n                    description: 'should be OK',\n                    status: 'pass',\n                    uid: 'P1nCbirrCv40DxI3bWvlR'\n                  }\n                ],\n                suitename: 'group1/test1',\n                runtime: 0.1\n              },\n              {\n                test: {\n                  filename: 'group1/test2.bru'\n                },\n                request: {\n                  method: 'GET',\n                  url: 'http://localhost:3000/test/v2',\n                  headers: {\n                    Accept: '*/*',\n                    'Accept-Encoding': 'gzip, deflate, br'\n                  }\n                },\n                response: {\n                  status: 404,\n                  statusText: 'Not Found',\n                  headers: {\n                    'x-powered-by': 'Express',\n                    'content-security-policy': \"default-src 'none'\",\n                    'x-content-type-options': 'nosniff',\n                    'content-type': 'text/html; charset=utf-8',\n                    'content-length': '146',\n                    date: 'Fri, 29 Sep 2023 00:37:50 GMT',\n                    connection: 'close'\n                  },\n                  responseTime: 96,\n                  data: '<!DOCTYPE html>\\n<html lang=\"en\">\\n<head>\\n<meta charset=\"utf-8\">\\n<title>Error</title>\\n</head>\\n<body>\\n<pre>Cannot GET /test/v2</pre>\\n</body>\\n</html>\\n'\n                },\n                error: null,\n                assertionResults: [\n                  {\n                    uid: 'IgliYuHd9wKp6JNyqyHFK',\n                    lhsExpr: 'res.status',\n                    rhsExpr: '200',\n                    rhsOperand: '200',\n                    operator: 'eq',\n                    status: 'fail',\n                    error: 'expected 404 to equal 200'\n                  }\n                ],\n                testResults: [],\n                suitename: 'group1/test2',\n                runtime: 0.1\n              },\n              {\n                test: {\n                  filename: 'group1/test3.bru'\n                },\n                request: {\n                  method: 'GET',\n                  url: 'http://localhost:3000/test/v3',\n                  headers: {\n                    Accept: '*/*',\n                    'Accept-Encoding': 'gzip, deflate, br'\n                  }\n                },\n                response: {\n                  status: 404,\n                  statusText: 'Not Found',\n                  headers: {\n                    'x-powered-by': 'Express',\n                    'content-security-policy': \"default-src 'none'\",\n                    'x-content-type-options': 'nosniff',\n                    'content-type': 'text/html; charset=utf-8',\n                    'content-length': '146',\n                    date: 'Fri, 29 Sep 2023 00:37:50 GMT',\n                    connection: 'close'\n                  },\n                  responseTime: 96,\n                  data: '<!DOCTYPE html>\\n<html lang=\"en\">\\n<head>\\n<meta charset=\"utf-8\">\\n<title>Error</title>\\n</head>\\n<body>\\n<pre>Cannot GET /test/v3</pre>\\n</body>\\n</html>\\n'\n                },\n                error: null,\n                assertionResults: [\n                  {\n                    uid: 'u-3sRebrCyuUbZOkwS0z8',\n                    lhsExpr: 'res.status',\n                    rhsExpr: '200',\n                    rhsOperand: '200',\n                    operator: 'eq',\n                    status: 'fail',\n                    error: 'expected 404 to equal 200'\n                  }\n                ],\n                testResults: [],\n                suitename: 'group1/test3',\n                runtime: 0.1\n              },\n              {\n                test: {\n                  filename: 'group2/test1.bru'\n                },\n                request: {\n                  method: 'POST',\n                  url: 'http://localhost:3000/test/v1',\n                  headers: {\n                    'content-type': 'application/json'\n                  },\n                  data: {\n                    test: 'hello'\n                  }\n                },\n                response: {\n                  status: 404,\n                  statusText: 'Not Found',\n                  headers: {\n                    'x-powered-by': 'Express',\n                    'content-security-policy': \"default-src 'none'\",\n                    'x-content-type-options': 'nosniff',\n                    'content-type': 'text/html; charset=utf-8',\n                    'content-length': '147',\n                    date: 'Fri, 29 Sep 2023 00:37:50 GMT',\n                    connection: 'close'\n                  },\n                  responseTime: 96,\n                  data: '<!DOCTYPE html>\\n<html lang=\"en\">\\n<head>\\n<meta charset=\"utf-8\">\\n<title>Error</title>\\n</head>\\n<body>\\n<pre>Cannot POST /test/v1</pre>\\n</body>\\n</html>\\n'\n                },\n                error: null,\n                assertionResults: [],\n                testResults: [\n                  {\n                    description: 'should fail',\n                    status: 'fail',\n                    error: 'expected 404 to deeply equal 200',\n                    actual: 404,\n                    expected: 200,\n                    uid: 'PpKLK6I38I5_ibw4lZqLb'\n                  },\n                  {\n                    description: 'should be OK',\n                    status: 'pass',\n                    uid: 'P1nCbirrCv40DxI3bWvlR'\n                  }\n                ],\n                suitename: 'group2/test1',\n                runtime: 0.1\n              },\n              {\n                test: {\n                  filename: 'group2/test2.bru'\n                },\n                request: {\n                  method: 'POST',\n                  url: 'http://localhost:3000/test',\n                  headers: {\n                    Accept: '*/*',\n                    'Accept-Encoding': 'gzip, deflate, br'\n                  }\n                },\n                response: {\n                  status: 404,\n                  statusText: 'Not Found',\n                  headers: {\n                    'x-powered-by': 'Express',\n                    'content-security-policy': \"default-src 'none'\",\n                    'x-content-type-options': 'nosniff',\n                    'content-type': 'text/html; charset=utf-8',\n                    'content-length': '144',\n                    date: 'Fri, 29 Sep 2023 00:37:50 GMT',\n                    connection: 'close'\n                  },\n                  responseTime: 96,\n                  data: '<!DOCTYPE html>\\n<html lang=\"en\">\\n<head>\\n<meta charset=\"utf-8\">\\n<title>Error</title>\\n</head>\\n<body>\\n<pre>Cannot POST /test</pre>\\n</body>\\n</html>\\n'\n                },\n                error: null,\n                assertionResults: [],\n                testResults: [],\n                suitename: 'group2/test2',\n                runtime: 0.1\n              },\n              {\n                test: {\n                  filename: 'test1.bru'\n                },\n                request: {\n                  method: 'HEAD',\n                  url: 'http://localhost:3000/',\n                  headers: {\n                    Accept: '*/*',\n                    'Accept-Encoding': 'gzip, deflate, br'\n                  }\n                },\n                response: {\n                  status: 200,\n                  statusText: 'OK',\n                  headers: {\n                    'x-powered-by': 'Express',\n                    'content-type': 'text/html; charset=utf-8',\n                    'content-length': '12',\n                    etag: 'W/\"c-Lve95gjOVATpfV8EL5X4nxwjKHE\"',\n                    date: 'Fri, 29 Sep 2023 00:37:50 GMT',\n                    connection: 'close'\n                  },\n                  responseTime: 96,\n                  data: ''\n                },\n                error: null,\n                assertionResults: [],\n                testResults: [],\n                suitename: 'test1',\n                runtime: 0.1\n              },\n              {\n                test: {\n                  filename: 'test2.bru'\n                },\n                request: {\n                  method: 'POST',\n                  url: 'http://localhost:3000',\n                  headers: {\n                    Accept: '*/*',\n                    'Accept-Encoding': 'gzip, deflate, br'\n                  }\n                },\n                response: {\n                  status: 404,\n                  statusText: 'Not Found',\n                  headers: {\n                    'x-powered-by': 'Express',\n                    'content-security-policy': \"default-src 'none'\",\n                    'x-content-type-options': 'nosniff',\n                    'content-type': 'text/html; charset=utf-8',\n                    'content-length': '140',\n                    date: 'Fri, 29 Sep 2023 00:37:50 GMT',\n                    connection: 'close'\n                  },\n                  responseTime: 96,\n                  data: '<!DOCTYPE html>\\n<html lang=\"en\">\\n<head>\\n<meta charset=\"utf-8\">\\n<title>Error</title>\\n</head>\\n<body>\\n<pre>Cannot POST /</pre>\\n</body>\\n</html>\\n'\n                },\n                error: null,\n                assertionResults: [],\n                testResults: [],\n                suitename: 'test2',\n                runtime: 0.1\n              },\n              {\n                test: {\n                  filename: 'test3.bru'\n                },\n                request: {\n                  method: 'POST',\n                  url: 'http://localhost:3000/',\n                  headers: {\n                    'content-type': 'multipart/form-data; boundary=--------------------------897965859410704836065858'\n                  },\n                  data: {\n                    _overheadLength: 103,\n                    _valueLength: 3,\n                    _valuesToMeasure: [],\n                    writable: false,\n                    readable: true,\n                    dataSize: 0,\n                    maxDataSize: 2097152,\n                    pauseStreams: true,\n                    _released: true,\n                    _streams: [],\n                    _currentStream: null,\n                    _insideLoop: false,\n                    _pendingNext: false,\n                    _boundary: '--------------------------897965859410704836065858',\n                    _events: {},\n                    _eventsCount: 3\n                  }\n                },\n                response: {\n                  status: 404,\n                  statusText: 'Not Found',\n                  headers: {\n                    'x-powered-by': 'Express',\n                    'content-security-policy': \"default-src 'none'\",\n                    'x-content-type-options': 'nosniff',\n                    'content-type': 'text/html; charset=utf-8',\n                    'content-length': '140',\n                    date: 'Fri, 29 Sep 2023 00:37:50 GMT',\n                    connection: 'close'\n                  },\n                  responseTime: 96,\n                  data: '<!DOCTYPE html>\\n<html lang=\"en\">\\n<head>\\n<meta charset=\"utf-8\">\\n<title>Error</title>\\n</head>\\n<body>\\n<pre>Cannot POST /</pre>\\n</body>\\n</html>\\n'\n                },\n                error: \"Cannot find module 'ajv-formats'\",\n                assertionResults: [],\n                testResults: [],\n                suitename: 'test3',\n                runtime: 0.1\n              },\n              {\n                test: {\n                  filename: 'test4.bru'\n                },\n                request: {\n                  method: 'POST',\n                  url: 'http://localhost:3000/',\n                  headers: {\n                    'content-type': 'application/x-www-form-urlencoded'\n                  },\n                  data: 'a=b&c=d'\n                },\n                response: {\n                  status: 404,\n                  statusText: 'Not Found',\n                  headers: {\n                    'x-powered-by': 'Express',\n                    'content-security-policy': \"default-src 'none'\",\n                    'x-content-type-options': 'nosniff',\n                    'content-type': 'text/html; charset=utf-8',\n                    'content-length': '140',\n                    date: 'Fri, 29 Sep 2023 00:37:50 GMT',\n                    connection: 'close'\n                  },\n                  responseTime: 96,\n                  data: '<!DOCTYPE html>\\n<html lang=\"en\">\\n<head>\\n<meta charset=\"utf-8\">\\n<title>Error</title>\\n</head>\\n<body>\\n<pre>Cannot POST /</pre>\\n</body>\\n</html>\\n'\n                },\n                error: null,\n                assertionResults: [],\n                testResults: [],\n                suitename: 'test4',\n                runtime: 0.1\n              },\n              {\n                test: {\n                  filename: 'test5.bru'\n                },\n                request: {\n                  method: 'POST',\n                  url: 'http://localhost:3000/test',\n                  headers: {\n                    'content-type': 'text/xml'\n                  },\n                  data: '<xml>\\n  <test>1</test>\\n</xml>'\n                },\n                response: {\n                  status: 404,\n                  statusText: 'Not Found',\n                  headers: {\n                    'x-powered-by': 'Express',\n                    'content-security-policy': \"default-src 'none'\",\n                    'x-content-type-options': 'nosniff',\n                    'content-type': 'text/html; charset=utf-8',\n                    'content-length': '144',\n                    date: 'Fri, 29 Sep 2023 00:37:50 GMT',\n                    connection: 'close'\n                  },\n                  responseTime: 96,\n                  data: '<!DOCTYPE html>\\n<html lang=\"en\">\\n<head>\\n<meta charset=\"utf-8\">\\n<title>Error</title>\\n</head>\\n<body>\\n<pre>Cannot POST /test</pre>\\n</body>\\n</html>\\n'\n                },\n                error: null,\n                assertionResults: [],\n                testResults: [],\n                suitename: 'test5',\n                runtime: 0.1\n              }\n            ]\n          };\n\n          const darkMode = ref(false);\n          const theme = computed(() => {\n            return darkMode.value ? naive.darkTheme : null;\n          });\n          if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {\n            darkMode.value = true;\n          }\n          // To watch for os theme changes\n          window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (event) => {\n            darkMode.value = event.matches;\n          });\n          return {\n            res,\n            theme,\n            darkMode,\n            darkModeRailStyle: () => ({ background: 'var(--n-rail-color)' })\n          };\n        }\n      };\n      const app = Vue.createApp(App);\n\n      app.component('x-summary', {\n        template: `#summary-component`,\n        props: ['res'],\n        setup(props) {\n          const summaryColumns = [\n            {\n              title: 'SUMMARY ITEM',\n              key: 'title'\n            },\n            {\n              title: 'TOTAL',\n              key: 'total'\n            },\n            {\n              title: 'PASSED',\n              key: 'passed'\n            },\n            {\n              title: 'FAILED',\n              key: 'failed'\n            }\n          ];\n          const summaryData = computed(() => [\n            {\n              title: 'Requests',\n              total: props.res.summary.totalRequests,\n              passed: props.res.summary.passedRequests,\n              failed: props.res.summary.failedRequests\n            },\n            {\n              title: 'Assertions',\n              total: props.res.summary.totalAssertions,\n              passed: props.res.summary.passedAssertions,\n              failed: props.res.summary.failedAssertions\n            },\n            {\n              title: 'Tests',\n              total: props.res.summary.totalTests,\n              passed: props.res.summary.passedTests,\n              failed: props.res.summary.failedTests\n            }\n          ]);\n          const summaryTotalRequests = computed(() => {\n            return props.res.summary.totalRequests;\n          });\n          const summaryTotalControls = computed(() => {\n            return props.res.summary.totalTests + props.res.summary.totalAssertions;\n          });\n          const summaryFailedControls = computed(\n            () => props.res.summary.failedRequests + props.res.summary.failedTests + props.res.summary.failedAssertions\n          );\n          const summaryErrors = computed(() => props.res.results.filter((r) => r.error).length);\n          const totalRunDuration = computed(() => props.res?.results?.reduce((total, test) => test.runtime + total, 0));\n          return {\n            summaryColumns,\n            summaryData,\n            summaryTotalControls,\n            summaryTotalRequests,\n            summaryFailedControls,\n            summaryErrors,\n            totalRunDuration\n          };\n        }\n      });\n\n      app.component('x-requests', {\n        template: `#requests-component`,\n        props: ['res'],\n        setup(props) {\n          const onlyFailed = ref(false);\n          const filteredResults = computed(() => {\n            if (onlyFailed.value) {\n              return props.res.results.filter(\n                (r) =>\n                  !!r.error ||\n                  !!r.testResults.find((t) => t.status !== 'pass') ||\n                  !!r.assertionResults.find((t) => t.status !== 'pass')\n              );\n            }\n            return props.res.results;\n          });\n          const groupedResults = computed(() => {\n            return filteredResults.value.reduce((groups, curr) => {\n              const path = curr.suitename.split('/');\n              const test = path.pop();\n              const name = path.length ? path.join('/') : '(root)';\n              if (!groups[name]) {\n                groups[name] = [];\n              }\n              groups[name].push(curr);\n              return groups;\n            }, {});\n          });\n          return {\n            onlyFailed,\n            groupedResults,\n            railStyle: ({ checked }) => {\n              const style = {};\n              if (checked) {\n                style.background = '#d03050';\n              }\n              return style;\n            }\n          };\n        }\n      });\n\n      app.component('x-results-group', {\n        template: `#results-group-component`,\n        props: ['group', 'results'],\n        setup(props) {\n          const totalPassed = computed(() => {\n            return props.results.reduce((total, curr) => {\n              return (\n                total +\n                curr.testResults.filter((t) => t.status === 'pass').length +\n                curr.assertionResults.filter((t) => t.status === 'pass').length\n              );\n            }, 0);\n          });\n          const total = computed(() => {\n            return props.results.reduce((total, curr) => {\n              return total + curr.testResults.length + curr.assertionResults.length;\n            }, 0);\n          });\n\n          const hasError = computed(() => props.results.some((r) => !!r.error));\n          const hasFailure = computed(() => totalPassed.value !== total.value);\n          return {\n            totalPassed,\n            total,\n            hasFailure,\n            hasError,\n            group: props.group,\n            results: props.results\n          };\n        }\n      });\n\n      app.component('x-result', {\n        template: `#result-component`,\n        props: ['group', 'result'],\n        setup(props) {\n          const headerColumns = [\n            {\n              title: 'Header Name',\n              key: 'name',\n              className: 'min-width-150'\n            },\n            {\n              title: 'Header Value',\n              key: 'value'\n            }\n          ];\n          const assertionsColumns = [\n            {\n              title: 'Expression',\n              key: 'lhsExpr'\n            },\n            {\n              title: 'Operator',\n              key: 'operator'\n            },\n            {\n              title: 'Operand',\n              key: 'rhsOperand'\n            },\n            {\n              title: 'Status',\n              key: 'status',\n              className: 'status'\n            },\n            {\n              title: 'Error',\n              key: 'error'\n            }\n          ];\n          const assertionsRowClassName = (row) => {\n            return row.status === 'fail' ? 'error' : 'success';\n          };\n          const testsRowClassName = (row) => {\n            return row.status === 'fail' ? 'error' : 'success';\n          };\n          const testsColumns = [\n            {\n              title: 'Description',\n              key: 'description'\n            },\n            {\n              title: 'Status',\n              key: 'status',\n              className: 'status'\n            },\n            {\n              title: 'Error',\n              key: 'error'\n            }\n          ];\n\n          function mapHeaderToTableData(headers) {\n            if (!headers) {\n              return [];\n            }\n            return Object.keys(headers).map((name) => ({\n              name,\n              value: headers[name]\n            }));\n          }\n          const headerDataRequest = computed(() => {\n            return mapHeaderToTableData(props.result.request.headers);\n          });\n          const headerDataResponse = computed(() => {\n            return mapHeaderToTableData(props.result.response.headers);\n          });\n          const totalPassed = computed(() => {\n            return (\n              props.result.testResults.filter((t) => t.status === 'pass').length +\n              props.result.assertionResults.filter((t) => t.status === 'pass').length\n            );\n          });\n          const total = computed(() => {\n            return props.result.testResults.length + props.result.assertionResults.length;\n          });\n\n          const hasError = computed(() => !!props.result.error);\n          const hasFailure = computed(() => total.value !== totalPassed.value);\n          const suitename = computed(() => props.result.suitename.replace(props.group + '/', ''));\n          const testDuration = computed(() => Math.round(props.result.runtime * 1000) + ' ms');\n          const name = computed(() => props.result.suitename + props.result.runtime);\n          return {\n            headerColumns,\n            headerDataRequest,\n            headerDataResponse,\n            assertionsColumns,\n            assertionsRowClassName,\n            testsRowClassName,\n            totalPassed,\n            total,\n            hasFailure,\n            hasError,\n            testsColumns,\n            result: props.result,\n            suitename,\n            testDuration,\n            name\n          };\n        }\n      });\n      app.use(naive);\n      app.mount('#app');\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/bruno-cli/examples/report.json",
    "content": "{\n  \"summary\": {\n    \"totalRequests\": 10,\n    \"passedRequests\": 10,\n    \"failedRequests\": 0,\n    \"totalAssertions\": 4,\n    \"passedAssertions\": 0,\n    \"failedAssertions\": 4,\n    \"totalTests\": 0,\n    \"passedTests\": 0,\n    \"failedTests\": 0\n  },\n  \"results\": [\n    {\n      \"request\": {\n        \"method\": \"GET\",\n        \"url\": \"http://localhost:3000/test/v4\",\n        \"headers\": {}\n      },\n      \"response\": {\n        \"status\": 404,\n        \"statusText\": \"Not Found\",\n        \"headers\": {\n          \"x-powered-by\": \"Express\",\n          \"content-security-policy\": \"default-src 'none'\",\n          \"x-content-type-options\": \"nosniff\",\n          \"content-type\": \"text/html; charset=utf-8\",\n          \"content-length\": \"146\",\n          \"date\": \"Fri, 29 Sep 2023 00:37:50 GMT\",\n          \"connection\": \"close\"\n        },\n        \"data\": \"<!DOCTYPE html>\\n<html lang=\\\"en\\\">\\n<head>\\n<meta charset=\\\"utf-8\\\">\\n<title>Error</title>\\n</head>\\n<body>\\n<pre>Cannot GET /test/v4</pre>\\n</body>\\n</html>\\n\"\n      },\n      \"error\": null,\n      \"assertionResults\": [\n        {\n          \"uid\": \"oidgfXLiyD8Jv0NBAHUHF\",\n          \"lhsExpr\": \"res.status\",\n          \"rhsExpr\": \"200\",\n          \"rhsOperand\": \"200\",\n          \"operator\": \"eq\",\n          \"status\": \"fail\",\n          \"error\": \"expected 404 to equal 200\"\n        }\n      ],\n      \"testResults\": []\n    },\n    {\n      \"request\": {\n        \"method\": \"GET\",\n        \"url\": \"http://localhost:3000/test/v2\",\n        \"headers\": {}\n      },\n      \"response\": {\n        \"status\": 404,\n        \"statusText\": \"Not Found\",\n        \"headers\": {\n          \"x-powered-by\": \"Express\",\n          \"content-security-policy\": \"default-src 'none'\",\n          \"x-content-type-options\": \"nosniff\",\n          \"content-type\": \"text/html; charset=utf-8\",\n          \"content-length\": \"146\",\n          \"date\": \"Fri, 29 Sep 2023 00:37:50 GMT\",\n          \"connection\": \"close\"\n        },\n        \"data\": \"<!DOCTYPE html>\\n<html lang=\\\"en\\\">\\n<head>\\n<meta charset=\\\"utf-8\\\">\\n<title>Error</title>\\n</head>\\n<body>\\n<pre>Cannot GET /test/v2</pre>\\n</body>\\n</html>\\n\"\n      },\n      \"error\": null,\n      \"assertionResults\": [\n        {\n          \"uid\": \"IgliYuHd9wKp6JNyqyHFK\",\n          \"lhsExpr\": \"res.status\",\n          \"rhsExpr\": \"200\",\n          \"rhsOperand\": \"200\",\n          \"operator\": \"eq\",\n          \"status\": \"fail\",\n          \"error\": \"expected 404 to equal 200\"\n        }\n      ],\n      \"testResults\": []\n    },\n    {\n      \"request\": {\n        \"method\": \"GET\",\n        \"url\": \"http://localhost:3000/test/v3\",\n        \"headers\": {}\n      },\n      \"response\": {\n        \"status\": 404,\n        \"statusText\": \"Not Found\",\n        \"headers\": {\n          \"x-powered-by\": \"Express\",\n          \"content-security-policy\": \"default-src 'none'\",\n          \"x-content-type-options\": \"nosniff\",\n          \"content-type\": \"text/html; charset=utf-8\",\n          \"content-length\": \"146\",\n          \"date\": \"Fri, 29 Sep 2023 00:37:50 GMT\",\n          \"connection\": \"close\"\n        },\n        \"data\": \"<!DOCTYPE html>\\n<html lang=\\\"en\\\">\\n<head>\\n<meta charset=\\\"utf-8\\\">\\n<title>Error</title>\\n</head>\\n<body>\\n<pre>Cannot GET /test/v3</pre>\\n</body>\\n</html>\\n\"\n      },\n      \"error\": null,\n      \"assertionResults\": [\n        {\n          \"uid\": \"u-3sRebrCyuUbZOkwS0z8\",\n          \"lhsExpr\": \"res.status\",\n          \"rhsExpr\": \"200\",\n          \"rhsOperand\": \"200\",\n          \"operator\": \"eq\",\n          \"status\": \"fail\",\n          \"error\": \"expected 404 to equal 200\"\n        }\n      ],\n      \"testResults\": []\n    },\n    {\n      \"request\": {\n        \"method\": \"POST\",\n        \"url\": \"http://localhost:3000/test/v1\",\n        \"headers\": {\n          \"content-type\": \"application/json\"\n        },\n        \"data\": {\n          \"test\": \"hello\"\n        }\n      },\n      \"response\": {\n        \"status\": 404,\n        \"statusText\": \"Not Found\",\n        \"headers\": {\n          \"x-powered-by\": \"Express\",\n          \"content-security-policy\": \"default-src 'none'\",\n          \"x-content-type-options\": \"nosniff\",\n          \"content-type\": \"text/html; charset=utf-8\",\n          \"content-length\": \"147\",\n          \"date\": \"Fri, 29 Sep 2023 00:37:50 GMT\",\n          \"connection\": \"close\"\n        },\n        \"data\": \"<!DOCTYPE html>\\n<html lang=\\\"en\\\">\\n<head>\\n<meta charset=\\\"utf-8\\\">\\n<title>Error</title>\\n</head>\\n<body>\\n<pre>Cannot POST /test/v1</pre>\\n</body>\\n</html>\\n\"\n      },\n      \"error\": null,\n      \"assertionResults\": [\n        {\n          \"uid\": \"PpKLK6I38I5_ibw4lZqLb\",\n          \"lhsExpr\": \"res.status\",\n          \"rhsExpr\": \"eq 200\",\n          \"rhsOperand\": \"200\",\n          \"operator\": \"eq\",\n          \"status\": \"fail\",\n          \"error\": \"expected 404 to equal 200\"\n        }\n      ],\n      \"testResults\": []\n    },\n    {\n      \"request\": {\n        \"method\": \"POST\",\n        \"url\": \"http://localhost:3000/test\",\n        \"headers\": {}\n      },\n      \"response\": {\n        \"status\": 404,\n        \"statusText\": \"Not Found\",\n        \"headers\": {\n          \"x-powered-by\": \"Express\",\n          \"content-security-policy\": \"default-src 'none'\",\n          \"x-content-type-options\": \"nosniff\",\n          \"content-type\": \"text/html; charset=utf-8\",\n          \"content-length\": \"144\",\n          \"date\": \"Fri, 29 Sep 2023 00:37:50 GMT\",\n          \"connection\": \"close\"\n        },\n        \"data\": \"<!DOCTYPE html>\\n<html lang=\\\"en\\\">\\n<head>\\n<meta charset=\\\"utf-8\\\">\\n<title>Error</title>\\n</head>\\n<body>\\n<pre>Cannot POST /test</pre>\\n</body>\\n</html>\\n\"\n      },\n      \"error\": null,\n      \"assertionResults\": [],\n      \"testResults\": []\n    },\n    {\n      \"request\": {\n        \"method\": \"HEAD\",\n        \"url\": \"http://localhost:3000/\",\n        \"headers\": {}\n      },\n      \"response\": {\n        \"status\": 200,\n        \"statusText\": \"OK\",\n        \"headers\": {\n          \"x-powered-by\": \"Express\",\n          \"content-type\": \"text/html; charset=utf-8\",\n          \"content-length\": \"12\",\n          \"etag\": \"W/\\\"c-Lve95gjOVATpfV8EL5X4nxwjKHE\\\"\",\n          \"date\": \"Fri, 29 Sep 2023 00:37:50 GMT\",\n          \"connection\": \"close\"\n        },\n        \"data\": \"\"\n      },\n      \"error\": null,\n      \"assertionResults\": [],\n      \"testResults\": []\n    },\n    {\n      \"request\": {\n        \"method\": \"POST\",\n        \"url\": \"http://localhost:3000\",\n        \"headers\": {}\n      },\n      \"response\": {\n        \"status\": 404,\n        \"statusText\": \"Not Found\",\n        \"headers\": {\n          \"x-powered-by\": \"Express\",\n          \"content-security-policy\": \"default-src 'none'\",\n          \"x-content-type-options\": \"nosniff\",\n          \"content-type\": \"text/html; charset=utf-8\",\n          \"content-length\": \"140\",\n          \"date\": \"Fri, 29 Sep 2023 00:37:50 GMT\",\n          \"connection\": \"close\"\n        },\n        \"data\": \"<!DOCTYPE html>\\n<html lang=\\\"en\\\">\\n<head>\\n<meta charset=\\\"utf-8\\\">\\n<title>Error</title>\\n</head>\\n<body>\\n<pre>Cannot POST /</pre>\\n</body>\\n</html>\\n\"\n      },\n      \"error\": null,\n      \"assertionResults\": [],\n      \"testResults\": []\n    },\n    {\n      \"request\": {\n        \"method\": \"POST\",\n        \"url\": \"http://localhost:3000/\",\n        \"headers\": {\n          \"content-type\": \"multipart/form-data; boundary=--------------------------897965859410704836065858\"\n        },\n        \"data\": {\n          \"_overheadLength\": 103,\n          \"_valueLength\": 3,\n          \"_valuesToMeasure\": [],\n          \"writable\": false,\n          \"readable\": true,\n          \"dataSize\": 0,\n          \"maxDataSize\": 2097152,\n          \"pauseStreams\": true,\n          \"_released\": true,\n          \"_streams\": [],\n          \"_currentStream\": null,\n          \"_insideLoop\": false,\n          \"_pendingNext\": false,\n          \"_boundary\": \"--------------------------897965859410704836065858\",\n          \"_events\": {},\n          \"_eventsCount\": 3\n        }\n      },\n      \"response\": {\n        \"status\": 404,\n        \"statusText\": \"Not Found\",\n        \"headers\": {\n          \"x-powered-by\": \"Express\",\n          \"content-security-policy\": \"default-src 'none'\",\n          \"x-content-type-options\": \"nosniff\",\n          \"content-type\": \"text/html; charset=utf-8\",\n          \"content-length\": \"140\",\n          \"date\": \"Fri, 29 Sep 2023 00:37:50 GMT\",\n          \"connection\": \"close\"\n        },\n        \"data\": \"<!DOCTYPE html>\\n<html lang=\\\"en\\\">\\n<head>\\n<meta charset=\\\"utf-8\\\">\\n<title>Error</title>\\n</head>\\n<body>\\n<pre>Cannot POST /</pre>\\n</body>\\n</html>\\n\"\n      },\n      \"error\": null,\n      \"assertionResults\": [],\n      \"testResults\": []\n    },\n    {\n      \"request\": {\n        \"method\": \"POST\",\n        \"url\": \"http://localhost:3000/\",\n        \"headers\": {\n          \"content-type\": \"application/x-www-form-urlencoded\"\n        },\n        \"data\": \"a=b&c=d\"\n      },\n      \"response\": {\n        \"status\": 404,\n        \"statusText\": \"Not Found\",\n        \"headers\": {\n          \"x-powered-by\": \"Express\",\n          \"content-security-policy\": \"default-src 'none'\",\n          \"x-content-type-options\": \"nosniff\",\n          \"content-type\": \"text/html; charset=utf-8\",\n          \"content-length\": \"140\",\n          \"date\": \"Fri, 29 Sep 2023 00:37:50 GMT\",\n          \"connection\": \"close\"\n        },\n        \"data\": \"<!DOCTYPE html>\\n<html lang=\\\"en\\\">\\n<head>\\n<meta charset=\\\"utf-8\\\">\\n<title>Error</title>\\n</head>\\n<body>\\n<pre>Cannot POST /</pre>\\n</body>\\n</html>\\n\"\n      },\n      \"error\": null,\n      \"assertionResults\": [],\n      \"testResults\": []\n    },\n    {\n      \"request\": {\n        \"method\": \"POST\",\n        \"url\": \"http://localhost:3000/test\",\n        \"headers\": {\n          \"content-type\": \"text/xml\"\n        },\n        \"data\": \"<xml>\\n  <test>1</test>\\n</xml>\"\n      },\n      \"response\": {\n        \"status\": 404,\n        \"statusText\": \"Not Found\",\n        \"headers\": {\n          \"x-powered-by\": \"Express\",\n          \"content-security-policy\": \"default-src 'none'\",\n          \"x-content-type-options\": \"nosniff\",\n          \"content-type\": \"text/html; charset=utf-8\",\n          \"content-length\": \"144\",\n          \"date\": \"Fri, 29 Sep 2023 00:37:50 GMT\",\n          \"connection\": \"close\"\n        },\n        \"data\": \"<!DOCTYPE html>\\n<html lang=\\\"en\\\">\\n<head>\\n<meta charset=\\\"utf-8\\\">\\n<title>Error</title>\\n</head>\\n<body>\\n<pre>Cannot POST /test</pre>\\n</body>\\n</html>\\n\"\n      },\n      \"error\": null,\n      \"assertionResults\": [],\n      \"testResults\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/bruno-cli/license.md",
    "content": "\nMIT License\n\nCopyright (c) 2022 Anoop M D, Anusree P S and Contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "packages/bruno-cli/package.json",
    "content": "{\n  \"name\": \"@usebruno/cli\",\n  \"version\": \"1.16.0\",\n  \"license\": \"MIT\",\n  \"main\": \"src/index.js\",\n  \"bin\": {\n    \"bru\": \"./bin/bru.js\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/usebruno/bruno/issues\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/usebruno/bruno.git\"\n  },\n  \"keywords\": [\n    \"API\",\n    \"testing\",\n    \"automation\",\n    \"cli\",\n    \"command-line\",\n    \"bruno\",\n    \"HTTP requests\",\n    \"rest-api\",\n    \"api-client\",\n    \"api-automation\",\n    \"request-handling\",\n    \"mock-api\",\n    \"http-client\",\n    \"async\",\n    \"promise\",\n    \"javascript\",\n    \"nodejs\",\n    \"automation-tool\",\n    \"postman-alternative\",\n    \"api-scripting\"\n  ],\n  \"scripts\": {\n    \"test\": \"node --experimental-vm-modules $(npx which jest)\"\n  },\n  \"files\": [\n    \"src\",\n    \"bin\",\n    \"readme.md\",\n    \"changelog.md\",\n    \"package.json\"\n  ],\n  \"dependencies\": {\n    \"@aws-sdk/credential-providers\": \"3.750.0\",\n    \"@usebruno/common\": \"0.1.0\",\n    \"@usebruno/converters\": \"^0.1.0\",\n    \"@usebruno/filestore\": \"^0.1.0\",\n    \"@usebruno/js\": \"0.12.0\",\n    \"@usebruno/lang\": \"0.12.0\",\n    \"@usebruno/requests\": \"^0.1.0\",\n    \"aws4-axios\": \"^3.3.0\",\n    \"axios\": \"^1.8.3\",\n    \"axios-ntlm\": \"^1.4.2\",\n    \"chai\": \"^4.3.7\",\n    \"chalk\": \"^3.0.0\",\n    \"decomment\": \"^0.9.5\",\n    \"form-data\": \"^4.0.0\",\n    \"fs-extra\": \"^10.1.0\",\n    \"http-proxy-agent\": \"^7.0.0\",\n    \"https-proxy-agent\": \"^7.0.2\",\n    \"iconv-lite\": \"^0.6.3\",\n    \"js-yaml\": \"^4.1.1\",\n    \"lodash\": \"^4.17.21\",\n    \"qs\": \"^6.14.1\",\n    \"socks-proxy-agent\": \"^8.0.2\",\n    \"xmlbuilder\": \"^15.1.1\",\n    \"yargs\": \"^17.6.2\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-cli/readme.md",
    "content": "# Bruno CLI\n\nWith Bruno CLI, you can now run your API collections with ease using simple command line commands.\n\nThis makes it easier to test your APIs in different environments, automate your testing process, and integrate your API tests with your continuous integration and deployment workflows.\n\nFor detailed documentation, visit [Bruno CLI Documentation](https://docs.usebruno.com/bru-cli/overview).\n\n## Installation\n\nTo install the Bruno CLI, use the node package manager of your choice, such as NPM:\n\n```bash\nnpm install -g @usebruno/cli\n```\n\n## Getting started\n\nNavigate to the directory where your API collection resides, and then run:\n\n```bash\nbru run\n```\n\nThis command will run all the requests in your collection. You can also run a single request by specifying its filename:\n\n```bash\nbru run request.bru\n```\n\nOr run all requests in a collection's subfolder:\n\n```bash\nbru run folder\n```\n\nIf you need to use an environment, you can specify it with the `--env` option:\n\n```bash\nbru run folder --env Local\n```\n\nIf you need to collect the results of your API tests, you can specify the `--output` option:\n\n```bash\nbru run folder --output results.json\n```\n\nIf you need to run a set of requests that connect to peers with both publicly and privately signed certificates respectively, you can add private CA certificates via the `--cacert` option. By default, these certificates will be used in addition to the default truststore:\n\n```bash\nbru run folder --cacert myCustomCA.pem\n```\n\nIf you need to limit the trusted CA to a specified set when validating the request peer, provide them via `--cacert` and in addition use `--ignore-truststore` to disable the default truststore:\n\n```bash\nbru run request.bru --cacert myCustomCA.pem --ignore-truststore\n```\n\n## Importing Collections\n\nYou can import collections from other formats, such as OpenAPI, using the import command:\n\n```bash\nbru import openapi --source api.yml --output ~/Desktop/my-collection --collection-name \"My API\"\n```\n\nYou can also use the shorter form with aliases:\n\n```bash\nbru import openapi -s api.yml -o ~/Desktop/my-collection -n \"My API\"\n```\n\nThis creates a Bruno collection directory that can be opened in Bruno.\n\nYou can also import directly from a URL:\n\n```bash\nbru import openapi --source https://example.com/api-spec.json --output ~/Desktop --collection-name \"Remote API\"\n```\n\nYou can also export the collection as a JSON file:\n\n```bash\nbru import openapi --source api.yml --output-file ~/Desktop/my-collection.json --collection-name \"My API\"\n```\n\nImport Options:\n\n| Option                    | Details                                            |\n| ------------------------- | -------------------------------------------------- |\n| --source, -s              | Path to the source file or URL (required)          |\n| --output, -o              | Path to the output directory                       |\n| --output-file, -f         | Path to the output JSON file                       |\n| --collection-name, -n     | Name for the imported collection                   |\n| --insecure                | Skip SSL certificate validation when fetching from URLs |\n\n## Command Line Options\n\n| Option                       | Details                                                                       |\n| ---------------------------- | ----------------------------------------------------------------------------- |\n| -h, --help                   | Show help                                                                     |\n| --version                    | Show version number                                                           |\n| -r                           | Indicates a recursive run (default: false)                                    |\n| --cacert [string]            | CA certificate to verify peer against                                         |\n| --env [string]               | Specify environment to run with                                               |\n| --env-var [string]           | Overwrite a single environment variable, multiple usages possible             |\n| -o, --output [string]        | Path to write file results to                                                 |\n| -f, --format [string]        | Format of the file results; available formats are \"json\" (default) or \"junit\" |\n| --reporter-json [string]     | Path to generate a JSON report                                                |\n| --reporter-junit [string]    | Path to generate a JUnit report                                               |\n| --reporter-html [string]     | Path to generate an HTML report                                               |\n| --insecure                   | Allow insecure server connections                                             |\n| --tests-only                 | Only run requests that have tests                                             |\n| --bail                       | Stop execution after a failure of a request, test, or assertion               |\n| --csv-file-path              | CSV file to run the collection with                                           |\n| --reporter--skip-all-headers | Skip all headers in the report                                                |\n| --reporter-skip-headers      | Skip specific headers in the report                                           |\n| --client-cert-config         | Client certificate configuration by passing a JSON file                       |\n| --delay [number]             | Add delay to each request                                                     |\n\n## Scripting\n\nBruno cli returns the following exit status codes:\n\n- `0` -- execution successful\n- `1` -- an assertion, test, or request in the executed collection failed\n- `2` -- the specified output directory does not exist\n- `3` -- the request chain seems to loop endlessly\n- `4` -- bru was called outside of a collection root directory\n- `5` -- the specified input file does not exist\n- `6` -- the specified environment does not exist\n- `7` -- the environment override was not a string or object\n- `8` -- an environment override is malformed\n- `9` -- an invalid output format was requested\n- `255` -- another error occurred\n\n## Demo\n\n![demo](assets/images/cli-demo.png)\n\n## Support\n\nIf you encounter any issues or have any feedback or suggestions, please raise them on our [GitHub repository](https://github.com/usebruno/bruno)\n\nThank you for using Bruno CLI!\n\n## Changelog\n\n<!-- An absolute link is used here because npm treats links differently -->\n\nSee [https://github.com/usebruno/bruno/releases](https://github.com/usebruno/bruno/releases)\n\n## License\n\n[MIT](license.md)\n"
  },
  {
    "path": "packages/bruno-cli/src/commands/import.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst chalk = require('chalk');\nconst jsyaml = require('js-yaml');\nconst axios = require('axios');\nconst { openApiToBruno, wsdlToBruno } = require('@usebruno/converters');\nconst { exists, isDirectory, sanitizeName } = require('../utils/filesystem');\nconst { createCollectionFromBrunoObject } = require('../utils/collection');\n\nconst command = 'import <type>';\nconst desc = 'Import a collection from other formats';\n\nconst COLLECTION_FORMATS = ['bru', 'opencollection'];\n\nconst builder = (yargs) => {\n  yargs\n    .positional('type', {\n      describe: 'Type of collection to import',\n      type: 'string',\n      choices: ['openapi', 'wsdl']\n    })\n    .option('source', {\n      alias: 's',\n      describe: 'Path to the source file or URL',\n      type: 'string',\n      demandOption: true\n    })\n    .option('output', {\n      alias: 'o',\n      describe: 'Path to the output directory',\n      type: 'string',\n      conflicts: 'output-file'\n    })\n    .option('output-file', {\n      alias: 'f',\n      describe: 'Path to the output JSON file',\n      type: 'string',\n      conflicts: 'output'\n    })\n    .option('collection-name', {\n      alias: 'n',\n      describe: 'Name for the imported collection',\n      type: 'string'\n    })\n    .option('collection-format', {\n      describe: 'Format of the imported collection (bru or opencollection). If not specified, the default is `opencollection`',\n      type: 'string',\n      choices: COLLECTION_FORMATS,\n      default: 'opencollection'\n    })\n    .option('insecure', {\n      type: 'boolean',\n      describe: 'Skip SSL certificate verification when fetching from URLs',\n      default: false\n    })\n    .option('group-by', {\n      alias: 'g',\n      describe: 'How to group the imported requests: \"tags\" groups by OpenAPI tags, \"path\" groups by URL path structure',\n      type: 'string',\n      choices: ['tags', 'path'],\n      default: 'tags'\n    })\n    .example('$0 import openapi --source api.yml --output ~/Desktop/my-collection --collection-name \"My API\"')\n    .example('$0 import openapi -s api.yml -o ~/Desktop/my-collection -n \"My API\"')\n    .example('$0 import openapi --source https://example.com/api-spec.json --output ~/Desktop --collection-name \"Remote API\"')\n    .example('$0 import openapi --source https://self-signed.example.com/api.json --insecure --output ~/Desktop')\n    .example('$0 import openapi --source api.yml --output-file ~/Desktop/my-collection.json --collection-name \"My API\"')\n    .example('$0 import openapi -s api.yml -f ~/Desktop/my-collection.json -n \"My API\"')\n    .example('$0 import openapi --source api.yml --output ~/Desktop/my-collection --group-by path')\n    .example('$0 import openapi -s api.yml -o ~/Desktop/my-collection -g tags')\n    .example('$0 import wsdl --source service.wsdl --output ~/Desktop/soap-collection --collection-name \"SOAP Service\"')\n    .example('$0 import wsdl -s https://example.com/service.wsdl -o ~/Desktop -n \"Remote SOAP Service\"');\n};\n\nconst isUrl = (str) => {\n  try {\n    return Boolean(new URL(str));\n  } catch (error) {\n    return false;\n  }\n};\n\nconst readOpenApiFile = async (source, options = {}) => {\n  try {\n    let content;\n\n    if (isUrl(source)) {\n      // Handle URL input\n      console.log(chalk.yellow(`Fetching specification from URL: ${source}`));\n      try {\n        const axiosOptions = {\n          timeout: 30000, // 30 second timeout\n          maxContentLength: 10 * 1024 * 1024,\n          validateStatus: (status) => status >= 200 && status < 300\n        };\n\n        // Skip SSL certificate validation if insecure flag is set\n        if (options.insecure) {\n          console.log(chalk.yellow('Warning: SSL certificate verification is disabled. Use with caution.'));\n          axiosOptions.httpsAgent = new (require('https')).Agent({ rejectUnauthorized: false });\n        }\n\n        const response = await axios.get(source, axiosOptions);\n        content = response.data;\n      } catch (error) {\n        if (error.code === 'ECONNABORTED') {\n          throw new Error('Request timed out. The server took too long to respond.');\n        } else if (error.code === 'CERT_HAS_EXPIRED' || error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT'\n          || error.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {\n          throw new Error(`SSL Certificate error: ${error.code}. Try using --insecure if you trust this source.`);\n        } else if (error.response) {\n          throw new Error(`Failed to fetch from URL: ${error.response.status} ${error.response.statusText}`);\n        } else if (error.request) {\n          throw new Error(`No response received from server. Check the URL and your network connection.`);\n        } else {\n          throw new Error(`Error fetching URL: ${error.message}`);\n        }\n      }\n\n      // If response is already an object, return it directly\n      if (typeof content === 'object' && content !== null) {\n        return content;\n      }\n    } else {\n      // Handle file input\n      if (!await exists(source)) {\n        throw new Error(`File does not exist: ${source}`);\n      }\n      content = fs.readFileSync(source, 'utf8');\n    }\n\n    // If content is a string, try to parse as JSON or YAML\n    if (typeof content === 'string') {\n      try {\n        return JSON.parse(content);\n      } catch (jsonError) {\n        try {\n          return jsyaml.load(content);\n        } catch (yamlError) {\n          throw new Error('Failed to parse content as JSON or YAML');\n        }\n      }\n    }\n\n    return content;\n  } catch (error) {\n    // Let the specific error handling from above propagate\n    throw error;\n  }\n};\n\nconst readWSDLFile = async (source, options = {}) => {\n  try {\n    let content;\n\n    if (isUrl(source)) {\n      // Handle URL input\n      console.log(chalk.yellow(`Fetching WSDL from URL: ${source}`));\n      try {\n        const axiosOptions = {\n          timeout: 30000, // 30 second timeout\n          maxContentLength: 10 * 1024 * 1024,\n          validateStatus: (status) => status >= 200 && status < 300\n        };\n\n        // Skip SSL certificate validation if insecure flag is set\n        if (options.insecure) {\n          console.log(chalk.yellow('Warning: SSL certificate verification is disabled. Use with caution.'));\n          axiosOptions.httpsAgent = new (require('https')).Agent({ rejectUnauthorized: false });\n        }\n\n        const response = await axios.get(source, axiosOptions);\n        content = response.data;\n      } catch (error) {\n        if (error.code === 'ECONNABORTED') {\n          throw new Error('Request timed out. The server took too long to respond.');\n        } else if (error.code === 'CERT_HAS_EXPIRED' || error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT'\n          || error.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {\n          throw new Error(`SSL Certificate error: ${error.code}. Try using --insecure if you trust this source.`);\n        } else if (error.response) {\n          throw new Error(`Failed to fetch from URL: ${error.response.status} ${error.response.statusText}`);\n        } else if (error.request) {\n          throw new Error(`No response received from server. Check the URL and your network connection.`);\n        } else {\n          throw new Error(`Error fetching URL: ${error.message}`);\n        }\n      }\n    } else {\n      // Handle file input\n      if (!await exists(source)) {\n        throw new Error(`File does not exist: ${source}`);\n      }\n      content = fs.readFileSync(source, 'utf8');\n    }\n\n    // WSDL files are XML, so we return the content as a string\n    if (typeof content === 'string') {\n      return content;\n    }\n\n    throw new Error('WSDL content must be a string');\n  } catch (error) {\n    // Let the specific error handling from above propagate\n    throw error;\n  }\n};\n\nconst handler = async (argv) => {\n  try {\n    const { type, source, output, collectionFormat, outputFile, collectionName, insecure, groupBy } = argv;\n\n    if (!type || !['openapi', 'wsdl'].includes(type)) {\n      console.error(chalk.red('Only OpenAPI and WSDL imports are supported currently'));\n      process.exit(1);\n    }\n\n    if (!source) {\n      console.error(chalk.red('Source file or URL is required'));\n      process.exit(1);\n    }\n\n    if (!output && !outputFile) {\n      console.error(chalk.red('Either --output or --output-file is required'));\n      process.exit(1);\n    }\n\n    let brunoCollection;\n\n    if (type === 'openapi') {\n      console.log(chalk.yellow(`Reading OpenAPI specification from ${source}...`));\n\n      const openApiSpec = await readOpenApiFile(source, { insecure });\n\n      if (!openApiSpec) {\n        console.error(chalk.red('Failed to parse OpenAPI specification'));\n        process.exit(1);\n      }\n\n      console.log(chalk.yellow('Converting OpenAPI specification to Bruno format...'));\n\n      // Convert OpenAPI to Bruno format\n      brunoCollection = openApiToBruno(openApiSpec, { groupBy });\n    } else if (type === 'wsdl') {\n      console.log(chalk.yellow(`Reading WSDL from ${source}...`));\n\n      const wsdlContent = await readWSDLFile(source, { insecure });\n\n      if (!wsdlContent) {\n        console.error(chalk.red('Failed to read WSDL file'));\n        process.exit(1);\n      }\n\n      console.log(chalk.yellow('Converting WSDL to Bruno format...'));\n\n      // Convert WSDL to Bruno format\n      brunoCollection = await wsdlToBruno(wsdlContent);\n    }\n\n    // Override collection name if provided\n    if (collectionName) {\n      brunoCollection.name = collectionName;\n    }\n\n    if (outputFile) {\n      // Save as JSON file\n      const outputPath = path.resolve(outputFile);\n      fs.writeFileSync(outputPath, JSON.stringify(brunoCollection, null, 2));\n      console.log(chalk.green(`Bruno collection saved as JSON to ${outputPath}`));\n    } else if (output) {\n      const resolvedOutput = path.resolve(output);\n\n      // Check if output is an existing directory\n      const isOutputDirectory = await exists(resolvedOutput) && isDirectory(resolvedOutput);\n\n      // Determine the final output directory\n      let outputDir;\n      if (isOutputDirectory) {\n        // If output is an existing directory, use collection name to create a subdirectory\n        const dirName = sanitizeName(brunoCollection.name);\n        outputDir = path.join(resolvedOutput, dirName);\n\n        // Check if this subfolder already exists\n        if (await exists(outputDir)) {\n          const dirContents = fs.readdirSync(outputDir);\n          if (dirContents.length > 0) {\n            console.error(chalk.red(`Output directory is not empty: ${outputDir}`));\n            process.exit(1);\n          }\n        } else {\n          // Create the subfolder\n          fs.mkdirSync(outputDir, { recursive: true });\n        }\n      } else {\n        // If output doesn't exist or is not a directory, use it directly\n        outputDir = resolvedOutput;\n\n        // Check if parent directory exists\n        const parentDir = path.dirname(outputDir);\n        if (!await exists(parentDir)) {\n          console.error(chalk.red(`Parent directory does not exist: ${parentDir}`));\n          process.exit(1);\n        }\n\n        fs.mkdirSync(outputDir, { recursive: true });\n      }\n\n      await createCollectionFromBrunoObject(brunoCollection, outputDir, {\n        format: collectionFormat === 'opencollection' ? 'yml' : 'bru'\n      });\n      console.log(chalk.green(`Bruno collection created at ${outputDir}`));\n    }\n  } catch (error) {\n    console.error(chalk.red(`Error: ${error.message}`));\n    process.exit(1);\n  }\n};\n\nmodule.exports = {\n  command,\n  desc,\n  builder,\n  handler,\n  isUrl,\n  readOpenApiFile,\n  readWSDLFile\n};\n"
  },
  {
    "path": "packages/bruno-cli/src/commands/run.js",
    "content": "const fs = require('fs');\nconst chalk = require('chalk');\nconst path = require('path');\nconst yaml = require('js-yaml');\nconst { forOwn, cloneDeep } = require('lodash');\nconst { getRunnerSummary } = require('@usebruno/common/runner');\nconst { exists, isFile, isDirectory } = require('../utils/filesystem');\nconst { runSingleRequest } = require('../runner/run-single-request');\nconst { getEnvVars } = require('../utils/bru');\nconst { parseEnvironmentJson } = require('../utils/environment');\nconst { isRequestTagsIncluded } = require('@usebruno/common');\nconst makeJUnitOutput = require('../reporters/junit');\nconst makeHtmlOutput = require('../reporters/html');\nconst { rpad } = require('../utils/common');\nconst { getOptions } = require('../utils/bru');\nconst { parseDotEnv, parseEnvironment } = require('@usebruno/filestore');\nconst constants = require('../constants');\nconst { findItemInCollection, createCollectionJsonFromPathname, getCallStack, FORMAT_CONFIG } = require('../utils/collection');\nconst { hasExecutableTestInScript } = require('../utils/request');\nconst { createSkippedFileResults } = require('../utils/run');\nconst { sanitizeResultsForReporter } = require('../utils/sanitize-results');\nconst { getSystemProxy } = require('@usebruno/requests');\nconst command = 'run [paths...]';\nconst desc = 'Run one or more requests/folders';\n\nconst formatTestSummary = (label, maxLength, passed, failed, total, errorCount = 0, skippedCount = 0) => {\n  const parts = [\n    `${rpad(label, maxLength)} ${chalk.green(`${passed} passed`)}`\n  ];\n\n  if (failed > 0) parts.push(chalk.red(`${failed} failed`));\n  if (errorCount > 0) parts.push(chalk.red(`${errorCount} error`));\n  if (skippedCount > 0) parts.push(chalk.magenta(`${skippedCount} skipped`));\n\n  parts.push(`${total} total`);\n\n  return parts.join(', ');\n};\n\nconst printRunSummary = (results) => {\n  const {\n    totalRequests,\n    passedRequests,\n    failedRequests,\n    skippedRequests,\n    errorRequests,\n    totalAssertions,\n    passedAssertions,\n    failedAssertions,\n    totalTests,\n    passedTests,\n    failedTests,\n    totalPreRequestTests,\n    passedPreRequestTests,\n    failedPreRequestTests,\n    totalPostResponseTests,\n    passedPostResponseTests,\n    failedPostResponseTests\n  } = getRunnerSummary(results);\n\n  const maxLength = 12;\n\n  const requestSummary = formatTestSummary('Requests:', maxLength, passedRequests, failedRequests, totalRequests, errorRequests, skippedRequests);\n  const testSummary = formatTestSummary('Tests:', maxLength, passedTests, failedTests, totalTests);\n  const assertSummary = formatTestSummary('Assertions:', maxLength, passedAssertions, failedAssertions, totalAssertions);\n\n  let preRequestTestSummary = '';\n  if (totalPreRequestTests > 0) {\n    preRequestTestSummary = formatTestSummary('Pre-Request Tests:', maxLength, passedPreRequestTests, failedPreRequestTests, totalPreRequestTests);\n  }\n\n  let postResponseTestSummary = '';\n  if (totalPostResponseTests > 0) {\n    postResponseTestSummary = formatTestSummary('Post-Response Tests:', maxLength, passedPostResponseTests, failedPostResponseTests, totalPostResponseTests);\n  }\n\n  console.log('\\n' + chalk.bold(requestSummary));\n  if (preRequestTestSummary) {\n    console.log(chalk.bold(preRequestTestSummary));\n  }\n  if (postResponseTestSummary) {\n    console.log(chalk.bold(postResponseTestSummary));\n  }\n  console.log(chalk.bold(testSummary));\n  console.log(chalk.bold(assertSummary));\n\n  return {\n    totalRequests,\n    passedRequests,\n    failedRequests,\n    skippedRequests,\n    errorRequests,\n    totalAssertions,\n    passedAssertions,\n    failedAssertions,\n    totalTests,\n    passedTests,\n    failedTests,\n    totalPreRequestTests,\n    passedPreRequestTests,\n    failedPreRequestTests,\n    totalPostResponseTests,\n    passedPostResponseTests,\n    failedPostResponseTests\n  };\n};\n\nconst getJsSandboxRuntime = (sandbox) => {\n  return sandbox === 'safe' ? 'quickjs' : 'nodevm';\n};\n\nconst builder = async (yargs) => {\n  yargs\n    .option('r', {\n      describe: 'Indicates a recursive run',\n      type: 'boolean',\n      default: false\n    })\n    .option('cacert', {\n      type: 'string',\n      description: 'CA certificate to verify peer against'\n    })\n    .option('ignore-truststore', {\n      type: 'boolean',\n      default: false,\n      description:\n        'The specified custom CA certificate (--cacert) will be used exclusively and the default truststore is ignored, if this option is specified. Evaluated in combination with \"--cacert\" only.'\n    })\n    .option('disable-cookies', {\n      type: 'boolean',\n      default: false,\n      description: 'Automatically save and sent cookies with requests'\n    })\n    .option('env', {\n      describe: 'Environment variables',\n      type: 'string'\n    })\n    .option('env-file', {\n      describe: 'Path to environment file (.bru or .json) - absolute or relative',\n      type: 'string'\n    })\n    .option('global-env', {\n      describe: 'Global environment name (requires collection to be in a workspace)',\n      type: 'string'\n    })\n    .option('workspace-path', {\n      describe: 'Path to workspace directory (auto-detected if not provided)',\n      type: 'string'\n    })\n    .option('env-var', {\n      describe: 'Overwrite a single environment variable, multiple usages possible',\n      type: 'string'\n    })\n    .option('sandbox', {\n      describe: 'Javascript sandbox to use; available sandboxes are \"safe\" (default) or \"developer\"',\n      default: 'safe',\n      type: 'string'\n    })\n    .option('output', {\n      alias: 'o',\n      describe: 'Path to write file results to',\n      type: 'string'\n    })\n    .option('format', {\n      alias: 'f',\n      describe: 'Format of the file results; available formats are \"json\" (default), \"junit\" or \"html\"',\n      default: 'json',\n      type: 'string'\n    })\n    .option('reporter-json', {\n      describe: 'Path to write json file results to',\n      type: 'string'\n    })\n    .option('reporter-junit', {\n      describe: 'Path to write junit file results to',\n      type: 'string'\n    })\n    .option('reporter-html', {\n      describe: 'Path to write html file results to',\n      type: 'string'\n    })\n    .option('insecure', {\n      type: 'boolean',\n      description: 'Allow insecure server connections'\n    })\n    .option('tests-only', {\n      type: 'boolean',\n      description: 'Only run requests that have a test or active assertion'\n    })\n    .option('bail', {\n      type: 'boolean',\n      description: 'Stop execution after a failure of a request, test, or assertion'\n    })\n    .option('reporter-skip-all-headers', {\n      type: 'boolean',\n      description: 'Omit headers from the reporter output',\n      default: false\n    })\n    .option('reporter-skip-headers', {\n      type: 'array',\n      description: 'Skip specific headers from the reporter output',\n      default: []\n    })\n    .option('reporter-skip-request-body', {\n      type: 'boolean',\n      description: 'Omit request body from the reporter output',\n      default: false\n    })\n    .option('reporter-skip-response-body', {\n      type: 'boolean',\n      description: 'Omit response body from the reporter output',\n      default: false\n    })\n    .option('reporter-skip-body', {\n      type: 'boolean',\n      description: 'Omit both request and response bodies from the reporter output',\n      default: false\n    })\n    .option('client-cert-config', {\n      type: 'string',\n      description: 'Path to the Client certificate config file used for securing the connection in the request'\n    })\n    .option('--noproxy', {\n      type: 'boolean',\n      description: 'Disable all proxy settings (both collection-defined and system proxies)',\n      default: false\n    })\n    .option('cache-ssl-session', {\n      type: 'boolean',\n      description: 'Enable SSL session caching — reuses TLS sessions across requests for faster handshakes',\n      default: false\n    })\n    .option('delay', {\n      type: 'number',\n      description: 'Delay between each requests (in miliseconds)'\n    })\n    .option('tags', {\n      type: 'string',\n      description: 'Tags to include in the run'\n    })\n    .option('exclude-tags', {\n      type: 'string',\n      description: 'Tags to exclude from the run'\n    })\n    .option('verbose', {\n      type: 'boolean',\n      description: 'Allow verbose output for debugging purposes'\n    })\n    .example('$0 run request.bru', 'Run a request')\n    .example('$0 run request.bru --env local', 'Run a request with the environment set to local')\n    .example('$0 run request.bru --env-file env.bru', 'Run a request with the environment from env.bru file')\n    .example('$0 run folder', 'Run all requests in a folder')\n    .example('$0 run folder -r', 'Run all requests in a folder recursively')\n    .example('$0 run request.bru folder', 'Run a request and all requests in a folder')\n    .example('$0 run --reporter-skip-all-headers', 'Run all requests in a folder recursively with omitted headers from the reporter output')\n    .example('$0 run --reporter-skip-request-body', 'Run all requests with request bodies omitted from the reporter output')\n    .example('$0 run --reporter-skip-response-body', 'Run all requests with response bodies omitted from the reporter output')\n    .example('$0 run --reporter-skip-body', 'Run all requests with both request and response bodies omitted from the reporter output')\n    .example(\n      '$0 run --reporter-skip-headers \"Authorization\"',\n      'Run all requests in a folder recursively with skipped headers from the reporter output'\n    )\n    .example(\n      '$0 run request.bru --env local --env-var secret=xxx',\n      'Run a request with the environment set to local and overwrite the variable secret with value xxx'\n    )\n    .example(\n      '$0 run request.bru --output results.json',\n      'Run a request and write the results to results.json in the current directory'\n    )\n    .example(\n      '$0 run request.bru --output results.xml --format junit',\n      'Run a request and write the results to results.xml in junit format in the current directory'\n    )\n    .example(\n      '$0 run request.bru --output results.html --format html',\n      'Run a request and write the results to results.html in html format in the current directory'\n    )\n    .example(\n      '$0 run request.bru --reporter-junit results.xml --reporter-html results.html',\n      'Run a request and write the results to results.html in html format and results.xml in junit format in the current directory'\n    )\n    .example('$0 run request.bru --tests-only', 'Run all requests that have a test')\n    .example(\n      '$0 run request.bru --cacert myCustomCA.pem',\n      'Use a custom CA certificate in combination with the default truststore when validating the peer of this request.'\n    )\n    .example(\n      '$0 run folder --cacert myCustomCA.pem --ignore-truststore',\n      'Use a custom CA certificate exclusively when validating the peers of the requests in the specified folder.'\n    )\n    .example('$0 run --client-cert-config client-cert-config.json', 'Run a request with Client certificate configurations')\n    .example('$0 run folder --delay delayInMs', 'Run a folder with given miliseconds delay between each requests.')\n    .example('$0 run --noproxy', 'Run requests with system proxy disabled')\n    .example(\n      '$0 run folder --tags=hello,world --exclude-tags=skip',\n      'Run only requests with tags \"hello\" or \"world\" and exclude any request with tag \"skip\".'\n    )\n    .example(\n      '$0 run request.bru --global-env production',\n      'Run a request with the global environment set to production'\n    )\n    .example(\n      '$0 run request.bru --global-env production --workspace-path /path/to/workspace',\n      'Run a request with a global environment from the specified workspace'\n    );\n};\n\nconst handler = async function (argv) {\n  try {\n    let {\n      paths,\n      cacert,\n      ignoreTruststore,\n      disableCookies,\n      env,\n      envFile,\n      globalEnv,\n      workspacePath,\n      envVar,\n      insecure,\n      r: recursive,\n      output: outputPath,\n      format,\n      reporterJson,\n      reporterJunit,\n      reporterHtml,\n      sandbox,\n      testsOnly,\n      bail,\n      reporterSkipAllHeaders,\n      reporterSkipHeaders,\n      reporterSkipRequestBody,\n      reporterSkipResponseBody,\n      reporterSkipBody,\n      clientCertConfig,\n      noproxy,\n      cacheSslSession,\n      delay,\n      tags: includeTags,\n      excludeTags,\n      verbose\n    } = argv;\n    const collectionPath = process.cwd();\n\n    let collection = createCollectionJsonFromPathname(collectionPath);\n    const { root: collectionRoot, brunoConfig } = collection;\n\n    if (clientCertConfig) {\n      try {\n        const clientCertConfigExists = await exists(clientCertConfig);\n        if (!clientCertConfigExists) {\n          console.error(chalk.red(`Client Certificate Config file \"${clientCertConfig}\" does not exist.`));\n          process.exit(constants.EXIT_STATUS.ERROR_FILE_NOT_FOUND);\n        }\n\n        const clientCertConfigFileContent = fs.readFileSync(clientCertConfig, 'utf8');\n        let clientCertConfigJson;\n\n        try {\n          clientCertConfigJson = JSON.parse(clientCertConfigFileContent);\n        } catch (err) {\n          console.error(chalk.red(`Failed to parse Client Certificate Config JSON: ${err.message}`));\n          process.exit(constants.EXIT_STATUS.ERROR_INVALID_FILE);\n        }\n\n        if (clientCertConfigJson?.enabled && Array.isArray(clientCertConfigJson?.certs)) {\n          if (brunoConfig.clientCertificates) {\n            brunoConfig.clientCertificates.certs.push(...clientCertConfigJson.certs);\n          } else {\n            brunoConfig.clientCertificates = { certs: clientCertConfigJson.certs };\n          }\n          console.log(chalk.green(`Client certificates has been added`));\n        } else {\n          console.warn(chalk.yellow(`Client certificate configuration is enabled, but it either contains no valid \"certs\" array or the added configuration has been set to false`));\n        }\n      } catch (err) {\n        console.error(chalk.red(`Unexpected error: ${err.message}`));\n        process.exit(constants.EXIT_STATUS.ERROR_GENERIC);\n      }\n    }\n\n    const runtimeVariables = {};\n    let envVars = {};\n\n    // Helper to load environment variables from a file\n    const loadEnvFromFile = (filePath, nameOverride) => {\n      const fileExt = path.extname(filePath).toLowerCase();\n      let result = {};\n\n      if (fileExt === '.json') {\n        const content = fs.readFileSync(filePath, 'utf8');\n        const parsed = JSON.parse(content);\n        const normalizedEnv = parseEnvironmentJson(parsed);\n        result = getEnvVars(normalizedEnv);\n        const rawName = normalizedEnv?.name;\n        const trimmedName = typeof rawName === 'string' ? rawName.trim() : '';\n        result.__name__ = trimmedName || path.basename(filePath, '.json');\n      } else if (fileExt === '.yml' || fileExt === '.yaml') {\n        const content = fs.readFileSync(filePath, 'utf8');\n        const envJson = parseEnvironment(content, { format: 'yml' });\n        result = getEnvVars(envJson);\n        result.__name__ = nameOverride || path.basename(filePath, fileExt);\n      } else {\n        const content = fs.readFileSync(filePath, 'utf8').replace(/\\r\\n/g, '\\n');\n        const envJson = parseEnvironment(content, { format: 'bru' });\n        result = getEnvVars(envJson);\n        result.__name__ = nameOverride || path.basename(filePath, '.bru');\n      }\n\n      return result;\n    };\n\n    // Load --env-file if provided\n    if (envFile) {\n      const envFilePath = path.resolve(collectionPath, envFile);\n      if (!(await exists(envFilePath))) {\n        console.error(chalk.red(`Environment file not found: `) + chalk.dim(envFile));\n        process.exit(constants.EXIT_STATUS.ERROR_ENV_NOT_FOUND);\n      }\n      try {\n        envVars = loadEnvFromFile(envFilePath);\n      } catch (err) {\n        console.error(chalk.red(`Failed to parse environment file: ${err.message}`));\n        process.exit(constants.EXIT_STATUS.ERROR_INVALID_FILE);\n      }\n    }\n\n    // Load --env and merge (collection env takes precedence)\n    if (env) {\n      const envExt = FORMAT_CONFIG[collection.format].ext;\n      const collectionEnvFilePath = path.join(collectionPath, 'environments', `${env}${envExt}`);\n      if (!(await exists(collectionEnvFilePath))) {\n        console.error(chalk.red(`Environment file not found: `) + chalk.dim(`environments/${env}${envExt}`));\n        process.exit(constants.EXIT_STATUS.ERROR_ENV_NOT_FOUND);\n      }\n      try {\n        const collectionEnvVars = loadEnvFromFile(collectionEnvFilePath, env);\n        envVars = { ...envVars, ...collectionEnvVars };\n      } catch (err) {\n        console.error(chalk.red(`Failed to parse Environment file: ${err.message}`));\n        process.exit(constants.EXIT_STATUS.ERROR_INVALID_FILE);\n      }\n    }\n\n    let globalEnvVars = {};\n    if (globalEnv) {\n      const findWorkspacePath = (startPath) => {\n        let currentPath = startPath;\n        while (currentPath !== path.dirname(currentPath)) {\n          const workspaceYmlPath = path.join(currentPath, 'workspace.yml');\n          if (fs.existsSync(workspaceYmlPath)) {\n            return currentPath;\n          }\n          currentPath = path.dirname(currentPath);\n        }\n        return null;\n      };\n\n      if (!workspacePath) {\n        workspacePath = findWorkspacePath(collectionPath);\n      }\n\n      if (!workspacePath) {\n        console.error(chalk.red(`Workspace not found. Please specify a workspace path using --workspace-path or ensure the collection is inside a workspace directory.`));\n        process.exit(constants.EXIT_STATUS.ERROR_GLOBAL_ENV_REQUIRES_WORKSPACE);\n      }\n\n      const workspaceExists = await exists(workspacePath);\n      if (!workspaceExists) {\n        console.error(chalk.red(`Workspace path not found: `) + chalk.dim(workspacePath));\n        process.exit(constants.EXIT_STATUS.ERROR_WORKSPACE_NOT_FOUND);\n      }\n\n      const workspaceYmlPath = path.join(workspacePath, 'workspace.yml');\n      const workspaceYmlExists = await exists(workspaceYmlPath);\n      if (!workspaceYmlExists) {\n        console.error(chalk.red(`Invalid workspace: workspace.yml not found in `) + chalk.dim(workspacePath));\n        process.exit(constants.EXIT_STATUS.ERROR_WORKSPACE_NOT_FOUND);\n      }\n\n      const globalEnvFilePath = path.join(workspacePath, 'environments', `${globalEnv}.yml`);\n      const globalEnvFileExists = await exists(globalEnvFilePath);\n      if (!globalEnvFileExists) {\n        console.error(chalk.red(`Global environment not found: `) + chalk.dim(`environments/${globalEnv}.yml`));\n        console.error(chalk.dim(`Workspace: ${workspacePath}`));\n        process.exit(constants.EXIT_STATUS.ERROR_GLOBAL_ENV_NOT_FOUND);\n      }\n\n      try {\n        const globalEnvContent = fs.readFileSync(globalEnvFilePath, 'utf8');\n        const globalEnvJson = parseEnvironment(globalEnvContent, { format: 'yml' });\n        globalEnvVars = getEnvVars(globalEnvJson);\n        globalEnvVars.__name__ = globalEnv;\n      } catch (err) {\n        console.error(chalk.red(`Failed to parse global environment: ${err.message}`));\n        process.exit(constants.EXIT_STATUS.ERROR_INVALID_FILE);\n      }\n    }\n\n    if (envVar) {\n      let processVars;\n      if (typeof envVar === 'string') {\n        processVars = [envVar];\n      } else if (typeof envVar === 'object' && Array.isArray(envVar)) {\n        processVars = envVar;\n      } else {\n        console.error(chalk.red(`overridable environment variables not parsable: use name=value`));\n        process.exit(constants.EXIT_STATUS.ERROR_MALFORMED_ENV_OVERRIDE);\n      }\n      if (processVars && Array.isArray(processVars)) {\n        for (const value of processVars.values()) {\n          // split the string at the first equals sign\n          const match = value.match(/^([^=]+)=(.*)$/);\n          if (!match) {\n            console.error(\n              chalk.red(`Overridable environment variable not correct: use name=value - presented: `)\n              + chalk.dim(`${value}`)\n            );\n            process.exit(constants.EXIT_STATUS.ERROR_INCORRECT_ENV_OVERRIDE);\n          }\n          envVars[match[1]] = match[2];\n        }\n      }\n    }\n\n    const options = getOptions();\n    if (bail) {\n      options['bail'] = true;\n    }\n    if (insecure) {\n      options['insecure'] = true;\n    }\n    if (disableCookies) {\n      options['disableCookies'] = true;\n    }\n    if (noproxy) {\n      options['noproxy'] = true;\n    }\n    if (cacheSslSession) {\n      options['cacheSslSession'] = true;\n    }\n    if (verbose) {\n      options['verbose'] = true;\n    }\n    if (cacert && cacert.length) {\n      if (insecure) {\n        console.error(chalk.red(`Ignoring the cacert option since insecure connections are enabled`));\n      } else {\n        const pathExists = await exists(cacert);\n        if (pathExists) {\n          options['cacert'] = cacert;\n        } else {\n          console.error(chalk.red(`Cacert File ${cacert} does not exist`));\n        }\n      }\n    }\n    options['ignoreTruststore'] = ignoreTruststore;\n\n    includeTags = includeTags ? includeTags.split(',') : [];\n    excludeTags = excludeTags ? excludeTags.split(',') : [];\n\n    if (['json', 'junit', 'html'].indexOf(format) === -1) {\n      console.error(chalk.red(`Format must be one of \"json\", \"junit or \"html\"`));\n      process.exit(constants.EXIT_STATUS.ERROR_INCORRECT_OUTPUT_FORMAT);\n    }\n\n    let formats = {};\n\n    // Maintains back compat with --format and --output\n    if (outputPath && outputPath.length) {\n      formats[format] = outputPath;\n    }\n\n    if (reporterHtml && reporterHtml.length) {\n      formats['html'] = reporterHtml;\n    }\n\n    if (reporterJson && reporterJson.length) {\n      formats['json'] = reporterJson;\n    }\n\n    if (reporterJunit && reporterJunit.length) {\n      formats['junit'] = reporterJunit;\n    }\n\n    // load .env file at root of collection if it exists\n    const dotEnvPath = path.join(collectionPath, '.env');\n    const dotEnvExists = await exists(dotEnvPath);\n    const processEnvVars = {\n      ...process.env\n    };\n    if (dotEnvExists) {\n      const content = fs.readFileSync(dotEnvPath, 'utf8');\n      const jsonData = parseDotEnv(content);\n\n      forOwn(jsonData, (value, key) => {\n        processEnvVars[key] = value;\n      });\n    }\n\n    let requestItems = [];\n    let results = [];\n\n    if (!paths || !paths.length) {\n      paths = ['./'];\n      recursive = true;\n    }\n\n    const resolvedPaths = paths.map((p) => path.resolve(process.cwd(), p));\n\n    for (const resolvedPath of resolvedPaths) {\n      const pathExists = await exists(resolvedPath);\n      if (!pathExists) {\n        console.error(chalk.red(`Path not found: ${resolvedPath}`));\n        process.exit(constants.EXIT_STATUS.ERROR_FILE_NOT_FOUND);\n      }\n    }\n\n    requestItems = getCallStack(resolvedPaths, collection, { recursive });\n\n    if (testsOnly) {\n      requestItems = requestItems.filter((item) => {\n        const requestHasTests = hasExecutableTestInScript(item.request?.tests);\n        const requestHasActiveAsserts = item.request?.assertions.some((x) => x.enabled) || false;\n\n        const preRequestScript = item.request?.script?.req;\n        const requestHasPreRequestTests = hasExecutableTestInScript(preRequestScript);\n\n        const postResponseScript = item.request?.script?.res;\n        const requestHasPostResponseTests = hasExecutableTestInScript(postResponseScript);\n\n        return requestHasTests || requestHasActiveAsserts || requestHasPreRequestTests || requestHasPostResponseTests;\n      });\n    }\n\n    requestItems = requestItems.filter((item) => {\n      return isRequestTagsIncluded(item.tags, includeTags, excludeTags);\n    });\n\n    const runtime = getJsSandboxRuntime(sandbox);\n\n    // Fetch system proxy once for all requests (skip if --noproxy flag is set)\n    if (!noproxy) {\n      try {\n        options['cachedSystemProxy'] = await getSystemProxy();\n      } catch (error) {\n        console.warn(chalk.yellow('Failed to detect system proxy, continuing without system proxy'));\n      }\n    }\n\n    const runSingleRequestByPathname = async (relativeItemPathname) => {\n      const ext = FORMAT_CONFIG[collection.format].ext;\n      return new Promise(async (resolve, reject) => {\n        let itemPathname = path.join(collectionPath, relativeItemPathname);\n        if (itemPathname && !itemPathname?.endsWith(ext)) {\n          itemPathname = `${itemPathname}${ext}`;\n        }\n        const requestItem = cloneDeep(findItemInCollection(collection, itemPathname));\n        if (requestItem) {\n          const res = await runSingleRequest(\n            requestItem,\n            collectionPath,\n            runtimeVariables,\n            envVars,\n            processEnvVars,\n            brunoConfig,\n            collectionRoot,\n            runtime,\n            collection,\n            runSingleRequestByPathname,\n            globalEnvVars\n          );\n          resolve(res?.response);\n        }\n        reject(`bru.runRequest: invalid request path - ${itemPathname}`);\n      });\n    };\n\n    let currentRequestIndex = 0;\n    let nJumps = 0; // count the number of jumps to avoid infinite loops\n    while (currentRequestIndex < requestItems.length) {\n      const requestItem = cloneDeep(requestItems[currentRequestIndex]);\n      const { name, pathname } = requestItem;\n\n      const start = process.hrtime();\n      const result = await runSingleRequest(\n        requestItem,\n        collectionPath,\n        runtimeVariables,\n        envVars,\n        processEnvVars,\n        brunoConfig,\n        collectionRoot,\n        runtime,\n        collection,\n        runSingleRequestByPathname,\n        globalEnvVars\n      );\n\n      const isLastRun = currentRequestIndex === requestItems.length - 1;\n      const isValidDelay = !Number.isNaN(delay) && delay > 0;\n      if (isValidDelay && !isLastRun) {\n        console.log(chalk.yellow(`Waiting for ${delay}ms or ${(delay / 1000).toFixed(3)}s before next request.`));\n        await new Promise((resolve) => setTimeout(resolve, delay));\n      }\n\n      if (Number.isNaN(delay) && !isLastRun) {\n        console.log(chalk.red(`Ignoring delay because it's not a valid number.`));\n      }\n\n      results.push({\n        ...result,\n        runDuration: process.hrtime(start)[0] + process.hrtime(start)[1] / 1e9,\n        suitename: pathname.replace('.bru', ''),\n        name,\n        path: result.test?.filename || path.relative(collectionPath, pathname)\n      });\n\n      sanitizeResultsForReporter(results, {\n        skipAllHeaders: reporterSkipAllHeaders,\n        skipHeaders: reporterSkipHeaders,\n        skipRequestBody: reporterSkipRequestBody || reporterSkipBody,\n        skipResponseBody: reporterSkipResponseBody || reporterSkipBody\n      });\n\n      // bail if option is set and there is a failure\n      if (bail) {\n        const requestFailure = result?.error && !result?.skipped;\n        const testFailure = result?.testResults?.find((iter) => iter.status === 'fail');\n        const assertionFailure = result?.assertionResults?.find((iter) => iter.status === 'fail');\n        const preRequestTestFailure = result?.preRequestTestResults?.find((iter) => iter.status === 'fail');\n        const postResponseTestFailure = result?.postResponseTestResults?.find((iter) => iter.status === 'fail');\n        if (requestFailure || testFailure || assertionFailure || preRequestTestFailure || postResponseTestFailure) {\n          break;\n        }\n      }\n\n      // determine next request\n      const nextRequestName = result?.nextRequestName;\n\n      if (result?.shouldStopRunnerExecution) {\n        break;\n      }\n\n      if (nextRequestName !== undefined) {\n        nJumps++;\n        if (nJumps > 10000) {\n          console.error(chalk.red(`Too many jumps, possible infinite loop`));\n          process.exit(constants.EXIT_STATUS.ERROR_INFINITE_LOOP);\n        }\n        if (nextRequestName === null) {\n          break;\n        }\n        const nextRequestIdx = requestItems.findIndex((iter) => iter.name === nextRequestName);\n        if (nextRequestIdx >= 0) {\n          currentRequestIndex = nextRequestIdx;\n        } else {\n          console.error('Could not find request with name \\'' + nextRequestName + '\\'');\n          currentRequestIndex++;\n        }\n      } else {\n        currentRequestIndex++;\n      }\n    }\n\n    const skippedFileResults = createSkippedFileResults(global.brunoSkippedFiles || [], collectionPath);\n    results.push(...skippedFileResults);\n\n    const summary = printRunSummary(results);\n    const runCompletionTime = new Date().toISOString();\n    const totalTime = results.reduce((acc, res) => acc + res.response.responseTime, 0);\n    console.log(chalk.dim(chalk.grey(`Ran all requests - ${totalTime} ms`)));\n\n    // Extract environment name from envVars if available\n    const environmentName = envVars?.__name__ || null;\n\n    const formatKeys = Object.keys(formats);\n    if (formatKeys && formatKeys.length > 0) {\n      const outputJson = {\n        summary,\n        results\n      };\n\n      const reporters = {\n        json: (path) => fs.writeFileSync(path, JSON.stringify(outputJson, null, 2)),\n        junit: (path) => makeJUnitOutput(results, path),\n        html: (path) => makeHtmlOutput(outputJson, path, runCompletionTime, environmentName)\n      };\n\n      for (const formatter of Object.keys(formats)) {\n        const reportPath = formats[formatter];\n        const reporter = reporters[formatter];\n\n        // Skip formatters lacking an output path.\n        if (!reportPath || reportPath.length === 0) {\n          continue;\n        }\n\n        const outputDir = path.dirname(reportPath);\n        const outputDirExists = await exists(outputDir);\n        if (!outputDirExists) {\n          console.error(chalk.red(`Output directory ${outputDir} does not exist`));\n          process.exit(constants.EXIT_STATUS.ERROR_MISSING_OUTPUT_DIR);\n        }\n\n        if (!reporter) {\n          console.error(chalk.red(`Reporter ${formatter} does not exist`));\n          process.exit(constants.EXIT_STATUS.ERROR_INCORRECT_OUTPUT_FORMAT);\n        }\n\n        reporter(reportPath);\n\n        console.log(chalk.dim(chalk.grey(`Wrote ${formatter} results to ${reportPath}`)));\n      }\n    }\n\n    if ((summary.failedAssertions + summary.failedTests + summary.failedPreRequestTests + summary.failedPostResponseTests + summary.failedRequests > 0) || (summary?.errorRequests > 0)) {\n      process.exit(constants.EXIT_STATUS.ERROR_FAILED_COLLECTION);\n    }\n  } catch (err) {\n    console.log('Something went wrong');\n    console.error(chalk.red(err.message));\n    process.exit(constants.EXIT_STATUS.ERROR_GENERIC);\n  }\n};\n\nmodule.exports = {\n  command,\n  desc,\n  builder,\n  handler,\n  printRunSummary\n};\n"
  },
  {
    "path": "packages/bruno-cli/src/constants.js",
    "content": "const { version } = require('../package.json');\n\nconst CLI_EPILOGUE = `Documentation: https://docs.usebruno.com (v${version})`;\nconst CLI_VERSION = version;\n\n// Exit codes\nconst EXIT_STATUS = {\n  // One or more assertions, tests, or requests failed\n  ERROR_FAILED_COLLECTION: 1,\n  // The specified output dir does not exist\n  ERROR_MISSING_OUTPUT_DIR: 2,\n  // request chain caused an endless loop\n  ERROR_INFINITE_LOOP: 3,\n  // bru was called outside of a collection root\n  ERROR_NOT_IN_COLLECTION: 4,\n  // The specified file was not found\n  ERROR_FILE_NOT_FOUND: 5,\n  // The specified environment was not found\n  ERROR_ENV_NOT_FOUND: 6,\n  // Environment override not presented as string or object\n  ERROR_MALFORMED_ENV_OVERRIDE: 7,\n  // Environment overrides format incorrect\n  ERROR_INCORRECT_ENV_OVERRIDE: 8,\n  // Invalid output format requested\n  ERROR_INCORRECT_OUTPUT_FORMAT: 9,\n  // Invalid file format\n  ERROR_INVALID_FILE: 10,\n  // The specified workspace was not found\n  ERROR_WORKSPACE_NOT_FOUND: 11,\n  // Global environment requires a workspace\n  ERROR_GLOBAL_ENV_REQUIRES_WORKSPACE: 12,\n  // The specified global environment was not found\n  ERROR_GLOBAL_ENV_NOT_FOUND: 13,\n  // Everything else\n  ERROR_GENERIC: 255\n};\n\nmodule.exports = {\n  CLI_EPILOGUE,\n  CLI_VERSION,\n  EXIT_STATUS\n};\n"
  },
  {
    "path": "packages/bruno-cli/src/index.js",
    "content": "const yargs = require('yargs');\nconst chalk = require('chalk');\nconst { initializeShellEnv } = require('@usebruno/requests');\n\nconst { CLI_EPILOGUE, CLI_VERSION } = require('./constants');\n\nconst printBanner = () => {\n  console.log(chalk.yellow(`Bru CLI ${CLI_VERSION}`));\n};\n\nconst run = async () => {\n  // Fetch shell environment (useful when CLI is run as subprocess from GUI app or cron)\n  await initializeShellEnv();\n\n  const argLength = process.argv.length;\n  const commandsToPrintBanner = ['--help', '-h'];\n\n  if (argLength <= 2 || process.argv.find((arg) => commandsToPrintBanner.includes(arg))) {\n    printBanner();\n  }\n\n  const { argv } = yargs\n    .strict()\n    .commandDir('commands')\n    .epilogue(CLI_EPILOGUE)\n    .usage('Usage: $0 <command> [options]')\n    .demandCommand(1, 'Woof!! Let\\'s play with some APIs!!')\n    .help('h')\n    .alias('h', 'help');\n};\n\nmodule.exports = {\n  run\n};\n"
  },
  {
    "path": "packages/bruno-cli/src/reporters/html.js",
    "content": "const fs = require('fs');\nconst { generateHtmlReport } = require('@usebruno/common/runner');\nconst { CLI_VERSION } = require('../constants');\n\nconst makeHtmlOutput = async (results, outputPath, runCompletionTime, environment = null) => {\n  let runnerResults = results;\n  if (!results) {\n    runnerResults = [];\n  } else if (results.results) {\n    // Convert CLI format to expected format: array of { iterationIndex, results, summary }\n    runnerResults = [{\n      iterationIndex: 0,\n      results: results.results,\n      summary: results.summary\n    }];\n  } else if (Array.isArray(results)) {\n    runnerResults = results;\n  }\n\n  const htmlString = generateHtmlReport({\n    runnerResults: runnerResults,\n    version: `usebruno v${CLI_VERSION}`,\n    environment: environment,\n    runCompletionTime: runCompletionTime\n  });\n  fs.writeFileSync(outputPath, htmlString);\n};\n\nmodule.exports = makeHtmlOutput;\n"
  },
  {
    "path": "packages/bruno-cli/src/reporters/junit.js",
    "content": "const os = require('os');\nconst fs = require('fs');\nconst xmlbuilder = require('xmlbuilder');\n\nconst makeJUnitOutput = async (results, outputPath) => {\n  const output = {\n    testsuites: {\n      testsuite: []\n    }\n  };\n\n  results.forEach((result) => {\n    const assertionTestCount = result.assertionResults ? result.assertionResults.length : 0;\n    const preRequestTestCount = result.preRequestTestResults ? result.preRequestTestResults.length : 0;\n    const testCount = result.testResults ? result.testResults.length : 0;\n    const postResponseTestCount = result.postResponseTestResults ? result.postResponseTestResults.length : 0;\n    const totalTests = assertionTestCount + preRequestTestCount + testCount + postResponseTestCount;\n\n    const suite = {\n      '@name': result.name,\n      '@file': result.test.filename,\n      '@errors': 0,\n      '@failures': 0,\n      '@skipped': 0,\n      '@tests': totalTests,\n      '@timestamp': new Date().toISOString().split('Z')[0],\n      '@hostname': os.hostname(),\n      '@time': result.runDuration.toFixed(3),\n      'testcase': []\n    };\n\n    result.assertionResults\n    && result.assertionResults.forEach((assertion) => {\n      const testcase = {\n        '@name': `${assertion.lhsExpr} ${assertion.rhsExpr}`,\n        '@status': assertion.status,\n        '@classname': result.request.url,\n        '@time': (result.runDuration / totalTests).toFixed(3)\n      };\n\n      if (assertion.status === 'fail') {\n        suite['@failures']++;\n\n        testcase.failure = [{ '@type': 'failure', '@message': assertion.error }];\n      }\n\n      suite.testcase.push(testcase);\n    });\n\n    result.preRequestTestResults\n    && result.preRequestTestResults.forEach((test) => {\n      const testcase = {\n        '@name': test.description,\n        '@status': test.status,\n        '@classname': result.request.url,\n        '@time': (result.runDuration / totalTests).toFixed(3)\n      };\n\n      if (test.status === 'fail') {\n        suite['@failures']++;\n\n        testcase.failure = [{ '@type': 'failure', '@message': test.error }];\n      }\n\n      suite.testcase.push(testcase);\n    });\n\n    result.testResults\n    && result.testResults.forEach((test) => {\n      const testcase = {\n        '@name': test.description,\n        '@status': test.status,\n        '@classname': result.request.url,\n        '@time': (result.runDuration / totalTests).toFixed(3)\n      };\n\n      if (test.status === 'fail') {\n        suite['@failures']++;\n\n        testcase.failure = [{ '@type': 'failure', '@message': test.error }];\n      }\n\n      suite.testcase.push(testcase);\n    });\n\n    result.postResponseTestResults\n    && result.postResponseTestResults.forEach((test) => {\n      const testcase = {\n        '@name': test.description,\n        '@status': test.status,\n        '@classname': result.request.url,\n        '@time': (result.runDuration / totalTests).toFixed(3)\n      };\n\n      if (test.status === 'fail') {\n        suite['@failures']++;\n\n        testcase.failure = [{ '@type': 'failure', '@message': test.error }];\n      }\n\n      suite.testcase.push(testcase);\n    });\n\n    if (result?.skipped) {\n      suite['@skipped'] = 1;\n    } else if (result.error) {\n      suite['@errors'] = 1;\n      suite['@tests'] = 1;\n      suite.testcase = [\n        {\n          '@name': 'Test suite has no errors',\n          '@status': 'fail',\n          '@classname': result.request.url,\n          '@time': result.runDuration.toFixed(3),\n          'error': [{ '@type': 'error', '@message': result.error }]\n        }\n      ];\n    }\n\n    output.testsuites.testsuite.push(suite);\n  });\n\n  fs.writeFileSync(outputPath, xmlbuilder.create(output).end({ pretty: true }));\n};\n\nmodule.exports = makeJUnitOutput;\n"
  },
  {
    "path": "packages/bruno-cli/src/runner/awsv4auth-helper.js",
    "content": "const { fromIni } = require('@aws-sdk/credential-providers');\nconst { aws4Interceptor } = require('aws4-axios');\n\nfunction isStrPresent(str) {\n  return str && str !== '' && str !== 'undefined';\n}\n\nasync function resolveAwsV4Credentials(request) {\n  const awsv4 = request.awsv4config;\n  if (isStrPresent(awsv4.profileName)) {\n    try {\n      const credentialsProvider = fromIni({\n        profile: awsv4.profileName,\n        ignoreCache: true\n      });\n      const credentials = await credentialsProvider();\n      awsv4.accessKeyId = credentials.accessKeyId;\n      awsv4.secretAccessKey = credentials.secretAccessKey;\n      awsv4.sessionToken = credentials.sessionToken;\n    } catch {\n      console.error('Failed to fetch credentials from AWS profile.');\n    }\n  }\n  return awsv4;\n}\n\nfunction addAwsV4Interceptor(axiosInstance, request) {\n  if (!request.awsv4config) {\n    console.warn('No Auth Config found!');\n    return;\n  }\n\n  const awsv4 = request.awsv4config;\n  if (!isStrPresent(awsv4.accessKeyId) || !isStrPresent(awsv4.secretAccessKey)) {\n    console.warn('Required Auth Fields are not present');\n    return;\n  }\n\n  const interceptor = aws4Interceptor({\n    options: {\n      region: awsv4.region,\n      service: awsv4.service\n    },\n    credentials: {\n      accessKeyId: awsv4.accessKeyId,\n      secretAccessKey: awsv4.secretAccessKey,\n      sessionToken: awsv4.sessionToken\n    }\n  });\n\n  axiosInstance.interceptors.request.use(interceptor);\n}\n\nmodule.exports = {\n  addAwsV4Interceptor,\n  resolveAwsV4Credentials\n};\n"
  },
  {
    "path": "packages/bruno-cli/src/runner/interpolate-string.js",
    "content": "const { forOwn, cloneDeep } = require('lodash');\nconst { interpolate, interpolateObject: interpolateObjectCommon } = require('@usebruno/common');\n\nconst buildCombinedVars = ({\n  collectionVariables,\n  envVars,\n  folderVariables,\n  requestVariables,\n  runtimeVariables,\n  processEnvVars,\n  globalEnvVars\n}) => {\n  processEnvVars = processEnvVars || {};\n  runtimeVariables = runtimeVariables || {};\n  collectionVariables = collectionVariables || {};\n  folderVariables = folderVariables || {};\n  requestVariables = requestVariables || {};\n  globalEnvVars = globalEnvVars || {};\n\n  // we clone envVars because we don't want to modify the original object\n  envVars = envVars ? cloneDeep(envVars) : {};\n\n  // envVars can inturn have values as {{process.env.VAR_NAME}}\n  // so we need to interpolate envVars first with processEnvVars\n  forOwn(envVars, (value, key) => {\n    envVars[key] = interpolate(value, {\n      process: {\n        env: {\n          ...processEnvVars\n        }\n      }\n    });\n  });\n\n  // runtimeVariables take precedence over envVars\n  return {\n    ...globalEnvVars,\n    ...collectionVariables,\n    ...envVars,\n    ...folderVariables,\n    ...requestVariables,\n    ...runtimeVariables,\n    process: {\n      env: {\n        ...processEnvVars\n      }\n    }\n  };\n};\n\nconst interpolateString = (str, interpolationOptions) => {\n  if (!str || !str.length || typeof str !== 'string') {\n    return str;\n  }\n\n  const combinedVars = buildCombinedVars(interpolationOptions);\n  return interpolate(str, combinedVars);\n};\n\n/**\n * recursively interpolating all string values in a object\n */\nconst interpolateObject = (obj, interpolationOptions) => {\n  const combinedVars = buildCombinedVars(interpolationOptions);\n  return interpolateObjectCommon(obj, combinedVars);\n};\n\nmodule.exports = {\n  interpolateString,\n  interpolateObject\n};\n"
  },
  {
    "path": "packages/bruno-cli/src/runner/interpolate-vars.js",
    "content": "const { interpolate } = require('@usebruno/common');\nconst { each, forOwn, cloneDeep, find } = require('lodash');\nconst { isFormData } = require('@usebruno/common').utils;\n\nconst getContentType = (headers = {}) => {\n  let contentType = '';\n  forOwn(headers, (value, key) => {\n    if (key && key.toLowerCase() === 'content-type') {\n      contentType = value;\n    }\n  });\n\n  // Return empty string if contentType is not a string (e.g., null/false for no body requests)\n  return typeof contentType === 'string' ? contentType : '';\n};\n\nconst interpolateVars = (request, envVariables = {}, runtimeVariables = {}, processEnvVars = {}) => {\n  const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};\n  const collectionVariables = request?.collectionVariables || {};\n  const folderVariables = request?.folderVariables || {};\n  const requestVariables = request?.requestVariables || {};\n  const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};\n  // we clone envVars because we don't want to modify the original object\n  envVariables = cloneDeep(envVariables);\n\n  // envVars can inturn have values as {{process.env.VAR_NAME}}\n  // so we need to interpolate envVars first with processEnvVars\n  forOwn(envVariables, (value, key) => {\n    envVariables[key] = interpolate(value, {\n      process: {\n        env: {\n          ...processEnvVars\n        }\n      }\n    });\n  });\n\n  const _interpolate = (str, { escapeJSONStrings } = {}) => {\n    if (!str || !str.length || typeof str !== 'string') {\n      return str;\n    }\n\n    // runtimeVariables take precedence over envVars\n    const combinedVars = {\n      ...globalEnvironmentVariables,\n      ...collectionVariables,\n      ...envVariables,\n      ...folderVariables,\n      ...requestVariables,\n      ...oauth2CredentialVariables,\n      ...runtimeVariables,\n      process: {\n        env: {\n          ...processEnvVars\n        }\n      }\n    };\n\n    return interpolate(str, combinedVars, { escapeJSONStrings });\n  };\n\n  request.url = _interpolate(request.url);\n\n  forOwn(request.headers, (value, key) => {\n    delete request.headers[key];\n    request.headers[_interpolate(key)] = _interpolate(value);\n  });\n\n  const contentType = getContentType(request.headers);\n  const isGraphqlRequest = request.mode === 'graphql';\n\n  // GraphQL: interpolate query and variables in place. We do not stringify the whole body and interpolate that, because variables is a JSON string. Full-body stringify would nest it and double-escape any {{var}} inside.\n  if (isGraphqlRequest && request.data && typeof request.data === 'object') {\n    request.data.query = _interpolate(request.data.query, { escapeJSONStrings: true });\n    request.data.variables = _interpolate(request.data.variables, { escapeJSONStrings: true });\n  }\n\n  // Skip body interpolation for GraphQL requests.\n  if (!isGraphqlRequest) {\n    if (contentType.includes('json') && !Buffer.isBuffer(request.data)) {\n      if (typeof request.data === 'string') {\n        if (request?.data?.length) {\n          request.data = _interpolate(request.data, { escapeJSONStrings: true });\n        }\n      } else if (typeof request.data === 'object') {\n        try {\n          let parsed = JSON.stringify(request.data);\n          parsed = _interpolate(parsed, { escapeJSONStrings: true });\n          request.data = JSON.parse(parsed);\n        } catch (err) {}\n      }\n    } else if (contentType === 'application/x-www-form-urlencoded') {\n      if (request.data && Array.isArray(request.data)) {\n        request.data = request.data.map((d) => ({\n          ...d,\n          value: _interpolate(d?.value)\n        }));\n      }\n    } else if (contentType.startsWith('multipart/')) {\n      if (Array.isArray(request?.data) && !isFormData(request.data)) {\n        try {\n          request.data = request?.data?.map((d) => ({\n            ...d,\n            value: _interpolate(d?.value)\n          }));\n        } catch (err) {}\n      }\n    } else {\n      request.data = _interpolate(request.data);\n    }\n  }\n\n  each(request?.pathParams, (param) => {\n    param.value = _interpolate(param.value);\n  });\n\n  if (request?.pathParams?.length) {\n    let url = request.url;\n\n    if (!url.startsWith('http://') && !url.startsWith('https://')) {\n      url = `http://${url}`;\n    }\n\n    try {\n      url = new URL(url);\n    } catch (e) {\n      throw { message: 'Invalid URL format', originalError: e.message };\n    }\n\n    const interpolatedUrlPath = url.pathname\n      .split('/')\n      .filter((path) => path !== '')\n      .map((path) => {\n        // traditional path parameters\n        if (path.startsWith(':')) {\n          const paramName = path.slice(1);\n          const existingPathParam = request.pathParams.find((param) => param.name === paramName);\n          if (!existingPathParam) {\n            return '/' + path;\n          }\n          return '/' + existingPathParam.value;\n        }\n\n        // for OData-style parameters (parameters inside parentheses)\n        // Check if path matches valid OData syntax:\n        // 1. EntitySet('key') or EntitySet(key)\n        // 2. EntitySet(Key1=value1,Key2=value2)\n        // 3. Function(param=value)\n        if (/^[A-Za-z0-9_.-]+\\([^)]*\\)$/.test(path)) {\n          const paramRegex = /[:](\\w+)/g;\n          let match;\n          let result = path;\n          while ((match = paramRegex.exec(path))) {\n            if (match[1]) {\n              let name = match[1].replace(/[')\"`]+$/, '');\n              name = name.replace(/^[('\"`]+/, '');\n              if (name) {\n                const existingPathParam = request.pathParams.find((param) => param.name === name);\n                if (existingPathParam) {\n                  result = result.replace(':' + match[1], existingPathParam.value);\n                }\n              }\n            }\n          }\n          return '/' + result;\n        }\n        return '/' + path;\n      })\n      .join('');\n\n    const trailingSlash = url.pathname.endsWith('/') ? '/' : '';\n    request.url = url.origin + interpolatedUrlPath + trailingSlash + url.search;\n  }\n\n  if (request.proxy) {\n    request.proxy.protocol = _interpolate(request.proxy.protocol);\n    request.proxy.hostname = _interpolate(request.proxy.hostname);\n    request.proxy.port = _interpolate(request.proxy.port);\n\n    if (request.proxy.auth) {\n      request.proxy.auth.username = _interpolate(request.proxy.auth.username);\n      request.proxy.auth.password = _interpolate(request.proxy.auth.password);\n    }\n  }\n\n  // todo: we have things happening in two places w.r.t basic auth\n  //       need to refactor this in the future\n  // the request.auth (basic auth) object gets set inside the prepare-request.js file\n  if (request.basicAuth) {\n    const username = _interpolate(request.basicAuth.username) || '';\n    const password = _interpolate(request.basicAuth.password) || '';\n\n    // use auth header based approach and delete the request.auth object\n    request.headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;\n    delete request.basicAuth;\n  }\n\n  if (request?.oauth2?.grantType) {\n    switch (request.oauth2.grantType) {\n      case 'password':\n        request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';\n        request.oauth2.refreshTokenUrl = _interpolate(request.oauth2.refreshTokenUrl) || '';\n        request.oauth2.username = _interpolate(request.oauth2.username) || '';\n        request.oauth2.password = _interpolate(request.oauth2.password) || '';\n        request.oauth2.clientId = _interpolate(request.oauth2.clientId) || '';\n        request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';\n        request.oauth2.scope = _interpolate(request.oauth2.scope) || '';\n        request.oauth2.credentialsPlacement = _interpolate(request.oauth2.credentialsPlacement) || '';\n        request.oauth2.credentialsId = _interpolate(request.oauth2.credentialsId) || '';\n        request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';\n        request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';\n        request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';\n        break;\n      case 'client_credentials':\n        request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';\n        request.oauth2.refreshTokenUrl = _interpolate(request.oauth2.refreshTokenUrl) || '';\n        request.oauth2.clientId = _interpolate(request.oauth2.clientId) || '';\n        request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';\n        request.oauth2.scope = _interpolate(request.oauth2.scope) || '';\n        request.oauth2.credentialsPlacement = _interpolate(request.oauth2.credentialsPlacement) || '';\n        request.oauth2.credentialsId = _interpolate(request.oauth2.credentialsId) || '';\n        request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';\n        request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';\n        request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';\n        break;\n      default:\n        break;\n    }\n\n    // Interpolate additional parameters for all OAuth2 grant types\n    if (request.oauth2.additionalParameters) {\n      // Interpolate authorization parameters\n      if (Array.isArray(request.oauth2.additionalParameters.authorization)) {\n        request.oauth2.additionalParameters.authorization.forEach((param) => {\n          if (param && param.enabled !== false) {\n            param.name = _interpolate(param.name) || '';\n            param.value = _interpolate(param.value) || '';\n          }\n        });\n      }\n\n      // Interpolate token parameters\n      if (Array.isArray(request.oauth2.additionalParameters.token)) {\n        request.oauth2.additionalParameters.token.forEach((param) => {\n          if (param && param.enabled !== false) {\n            param.name = _interpolate(param.name) || '';\n            param.value = _interpolate(param.value) || '';\n          }\n        });\n      }\n\n      // Interpolate refresh parameters\n      if (Array.isArray(request.oauth2.additionalParameters.refresh)) {\n        request.oauth2.additionalParameters.refresh.forEach((param) => {\n          if (param && param.enabled !== false) {\n            param.name = _interpolate(param.name) || '';\n            param.value = _interpolate(param.value) || '';\n          }\n        });\n      }\n    }\n  }\n\n  if (request.awsv4config) {\n    request.awsv4config.accessKeyId = _interpolate(request.awsv4config.accessKeyId) || '';\n    request.awsv4config.secretAccessKey = _interpolate(request.awsv4config.secretAccessKey) || '';\n    request.awsv4config.sessionToken = _interpolate(request.awsv4config.sessionToken) || '';\n    request.awsv4config.service = _interpolate(request.awsv4config.service) || '';\n    request.awsv4config.region = _interpolate(request.awsv4config.region) || '';\n    request.awsv4config.profileName = _interpolate(request.awsv4config.profileName) || '';\n  }\n\n  // interpolate vars for ntlmConfig auth\n  if (request.ntlmConfig) {\n    request.ntlmConfig.username = _interpolate(request.ntlmConfig.username) || '';\n    request.ntlmConfig.password = _interpolate(request.ntlmConfig.password) || '';\n    request.ntlmConfig.domain = _interpolate(request.ntlmConfig.domain) || '';\n  }\n\n  if (request?.auth) delete request.auth;\n\n  if (request) return request;\n};\n\nmodule.exports = interpolateVars;\n"
  },
  {
    "path": "packages/bruno-cli/src/runner/prepare-request.js",
    "content": "const get = require('lodash/get');\nconst each = require('lodash/each');\nconst filter = require('lodash/filter');\nconst find = require('lodash/find');\nconst decomment = require('decomment');\nconst crypto = require('node:crypto');\nconst fs = require('node:fs');\nconst { mergeHeaders, mergeScripts, mergeVars, mergeAuth, getTreePathFromCollectionToItem } = require('../utils/collection');\nconst path = require('node:path');\nconst { isLargeFile } = require('../utils/filesystem');\nconst { getFormattedOauth2Credentials } = require('../utils/oauth2');\n\nconst STREAMING_FILE_SIZE_THRESHOLD = 20 * 1024 * 1024; // 20MB\n\nconst prepareRequest = async (item = {}, collection = {}) => {\n  const request = item?.request;\n  const brunoConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig', {}) : get(collection, 'brunoConfig', {});\n  const collectionPath = collection?.pathname;\n  const headers = {};\n  let contentTypeDefined = false;\n\n  const scriptFlow = brunoConfig?.scripts?.flow ?? 'sandwich';\n  const requestTreePath = getTreePathFromCollectionToItem(collection, item);\n  if (requestTreePath && requestTreePath.length > 0) {\n    mergeHeaders(collection, request, requestTreePath);\n    mergeScripts(collection, request, requestTreePath, scriptFlow);\n    mergeVars(collection, request, requestTreePath);\n    mergeAuth(collection, request, requestTreePath);\n  }\n\n  each(get(request, 'headers', []), (h) => {\n    if (h.enabled) {\n      headers[h.name] = h.value;\n      if (h.name.toLowerCase() === 'content-type') {\n        contentTypeDefined = true;\n      }\n    }\n  });\n\n  let axiosRequest = {\n    method: request.method,\n    url: request.url,\n    headers: headers,\n    name: item.name,\n    pathname: item.pathname,\n    tags: item.tags || [],\n    pathParams: request.params?.filter((param) => param.type === 'path'),\n    settings: item.settings,\n    responseType: 'arraybuffer',\n    mode: request.body?.mode\n  };\n\n  const collectionRoot = collection?.draft?.root || collection?.root || {};\n  const collectionAuth = get(collectionRoot, 'request.auth');\n  if (collectionAuth && request.auth?.mode === 'inherit') {\n    if (collectionAuth.mode === 'basic') {\n      axiosRequest.basicAuth = {\n        username: get(collectionAuth, 'basic.username'),\n        password: get(collectionAuth, 'basic.password')\n      };\n    }\n\n    if (collectionAuth.mode === 'bearer') {\n      axiosRequest.headers['Authorization'] = `Bearer ${get(collectionAuth, 'bearer.token', '')}`;\n    }\n\n    if (collectionAuth.mode === 'apikey') {\n      if (collectionAuth.apikey?.placement === 'header') {\n        axiosRequest.headers[collectionAuth.apikey?.key] = collectionAuth.apikey?.value;\n      }\n\n      if (collectionAuth.apikey?.placement === 'queryparams') {\n        if (axiosRequest.url && collectionAuth.apikey?.key) {\n          try {\n            const urlObj = new URL(request.url);\n            urlObj.searchParams.set(collectionAuth.apikey?.key, collectionAuth.apikey?.value);\n            axiosRequest.url = urlObj.toString();\n          } catch (error) {\n            console.error('Invalid URL:', request.url, error);\n          }\n        }\n      }\n    }\n\n    if (collectionAuth.mode === 'digest') {\n      axiosRequest.digestConfig = {\n        username: get(collectionAuth, 'digest.username'),\n        password: get(collectionAuth, 'digest.password')\n      };\n    }\n\n    if (collectionAuth.mode === 'oauth2') {\n      const grantType = get(collectionAuth, 'oauth2.grantType');\n\n      if (grantType === 'client_credentials') {\n        axiosRequest.oauth2 = {\n          grantType,\n          accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),\n          refreshTokenUrl: get(collectionAuth, 'oauth2.refreshTokenUrl'),\n          clientId: get(collectionAuth, 'oauth2.clientId'),\n          clientSecret: get(collectionAuth, 'oauth2.clientSecret'),\n          scope: get(collectionAuth, 'oauth2.scope'),\n          credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),\n          credentialsId: get(collectionAuth, 'oauth2.credentialsId'),\n          tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),\n          tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),\n          tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),\n          autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),\n          autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken'),\n          additionalParameters: get(collectionAuth, 'oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })\n        };\n      } else if (grantType === 'password') {\n        axiosRequest.oauth2 = {\n          grantType,\n          accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),\n          refreshTokenUrl: get(collectionAuth, 'oauth2.refreshTokenUrl'),\n          username: get(collectionAuth, 'oauth2.username'),\n          password: get(collectionAuth, 'oauth2.password'),\n          clientId: get(collectionAuth, 'oauth2.clientId'),\n          clientSecret: get(collectionAuth, 'oauth2.clientSecret'),\n          scope: get(collectionAuth, 'oauth2.scope'),\n          credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),\n          credentialsId: get(collectionAuth, 'oauth2.credentialsId'),\n          tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),\n          tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),\n          tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),\n          autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),\n          autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken'),\n          additionalParameters: get(collectionAuth, 'oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })\n        };\n      }\n    }\n    if (collectionAuth.mode === 'awsv4') {\n      axiosRequest.awsv4config = {\n        accessKeyId: get(collectionAuth, 'awsv4.accessKeyId'),\n        secretAccessKey: get(collectionAuth, 'awsv4.secretAccessKey'),\n        sessionToken: get(collectionAuth, 'awsv4.sessionToken'),\n        service: get(collectionAuth, 'awsv4.service'),\n        region: get(collectionAuth, 'awsv4.region'),\n        profileName: get(collectionAuth, 'awsv4.profileName')\n      };\n    }\n\n    if (collectionAuth.mode === 'ntlm') {\n      axiosRequest.ntlmConfig = {\n        username: get(collectionAuth, 'ntlm.username'),\n        password: get(collectionAuth, 'ntlm.password'),\n        domain: get(collectionAuth, 'ntlm.domain')\n      };\n    }\n\n    if (collectionAuth.mode === 'wsse') {\n      const username = get(collectionAuth, 'wsse.username', '');\n      const password = get(collectionAuth, 'wsse.password', '');\n\n      const ts = new Date().toISOString();\n      const nonce = crypto.randomBytes(16).toString('hex');\n\n      // Create the password digest using SHA-1 as required for WSSE\n      const hash = crypto.createHash('sha1');\n      hash.update(nonce + ts + password);\n      const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64');\n\n      // Construct the WSSE header\n      axiosRequest.headers[\n        'X-WSSE'\n      ] = `UsernameToken Username=\"${username}\", PasswordDigest=\"${digest}\", Nonce=\"${nonce}\", Created=\"${ts}\"`;\n    }\n\n    console.log('axiosRequest', axiosRequest);\n  }\n\n  if (request.auth && request.auth.mode !== 'inherit') {\n    if (request.auth.mode === 'basic') {\n      axiosRequest.basicAuth = {\n        username: get(request, 'auth.basic.username'),\n        password: get(request, 'auth.basic.password')\n      };\n    }\n\n    if (request.auth.mode === 'awsv4') {\n      axiosRequest.awsv4config = {\n        accessKeyId: get(request, 'auth.awsv4.accessKeyId'),\n        secretAccessKey: get(request, 'auth.awsv4.secretAccessKey'),\n        sessionToken: get(request, 'auth.awsv4.sessionToken'),\n        service: get(request, 'auth.awsv4.service'),\n        region: get(request, 'auth.awsv4.region'),\n        profileName: get(request, 'auth.awsv4.profileName')\n      };\n    }\n\n    if (request.auth.mode === 'ntlm') {\n      axiosRequest.ntlmConfig = {\n        username: get(request, 'auth.ntlm.username'),\n        password: get(request, 'auth.ntlm.password'),\n        domain: get(request, 'auth.ntlm.domain')\n      };\n    }\n\n    if (request.auth.mode === 'bearer') {\n      axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token', '')}`;\n    }\n\n    if (request.auth.mode === 'wsse') {\n      const username = get(request, 'auth.wsse.username', '');\n      const password = get(request, 'auth.wsse.password', '');\n\n      const ts = new Date().toISOString();\n      const nonce = crypto.randomBytes(16).toString('hex');\n\n      // Create the password digest using SHA-1 as required for WSSE\n      const hash = crypto.createHash('sha1');\n      hash.update(nonce + ts + password);\n      const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64');\n\n      // Construct the WSSE header\n      axiosRequest.headers[\n        'X-WSSE'\n      ] = `UsernameToken Username=\"${username}\", PasswordDigest=\"${digest}\", Nonce=\"${nonce}\", Created=\"${ts}\"`;\n    }\n\n    if (request.auth.mode === 'digest') {\n      axiosRequest.digestConfig = {\n        username: get(request, 'auth.digest.username'),\n        password: get(request, 'auth.digest.password')\n      };\n    }\n\n    if (request.auth.mode === 'oauth2') {\n      const grantType = get(request, 'auth.oauth2.grantType');\n\n      if (grantType === 'client_credentials') {\n        axiosRequest.oauth2 = {\n          grantType: grantType,\n          accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),\n          refreshTokenUrl: get(request, 'auth.oauth2.refreshTokenUrl'),\n          clientId: get(request, 'auth.oauth2.clientId'),\n          clientSecret: get(request, 'auth.oauth2.clientSecret'),\n          scope: get(request, 'auth.oauth2.scope'),\n          credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),\n          credentialsId: get(request, 'auth.oauth2.credentialsId'),\n          tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),\n          tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),\n          tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),\n          autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),\n          autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken'),\n          additionalParameters: get(request, 'auth.oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })\n        };\n      } else if (grantType === 'password') {\n        axiosRequest.oauth2 = {\n          grantType: grantType,\n          accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),\n          refreshTokenUrl: get(request, 'auth.oauth2.refreshTokenUrl'),\n          username: get(request, 'auth.oauth2.username'),\n          password: get(request, 'auth.oauth2.password'),\n          clientId: get(request, 'auth.oauth2.clientId'),\n          clientSecret: get(request, 'auth.oauth2.clientSecret'),\n          scope: get(request, 'auth.oauth2.scope'),\n          credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),\n          credentialsId: get(request, 'auth.oauth2.credentialsId'),\n          tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),\n          tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),\n          tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),\n          autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),\n          autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken'),\n          additionalParameters: get(request, 'auth.oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })\n        };\n      }\n    }\n\n    if (request.auth.mode === 'apikey') {\n      if (request.auth.apikey?.placement === 'header') {\n        axiosRequest.headers[request.auth.apikey?.key] = request.auth.apikey?.value;\n      }\n\n      if (request.auth.apikey?.placement === 'queryparams') {\n        if (axiosRequest.url && request.auth.apikey?.key) {\n          try {\n            const urlObj = new URL(request.url);\n            urlObj.searchParams.set(request.auth.apikey?.key, request.auth.apikey?.value);\n            axiosRequest.url = urlObj.toString();\n          } catch (error) {\n            console.error('Invalid URL:', request.url, error);\n          }\n        }\n      }\n    }\n  }\n\n  request.body = request.body || {};\n\n  if (request.body.mode === 'json') {\n    const jsonBody = request.body.json;\n    if (jsonBody && jsonBody.length > 0) {\n      if (!contentTypeDefined) {\n        axiosRequest.headers['content-type'] = 'application/json';\n      }\n      try {\n        axiosRequest.data = decomment(jsonBody);\n      } catch (error) {\n        axiosRequest.data = jsonBody;\n      }\n    }\n  }\n\n  if (request.body.mode === 'text') {\n    if (!contentTypeDefined) {\n      axiosRequest.headers['content-type'] = 'text/plain';\n    }\n    axiosRequest.data = request.body.text;\n  }\n\n  if (request.body.mode === 'xml') {\n    if (!contentTypeDefined) {\n      axiosRequest.headers['content-type'] = 'application/xml';\n    }\n    axiosRequest.data = request.body.xml;\n  }\n\n  if (request.body.mode === 'sparql') {\n    if (!contentTypeDefined) {\n      axiosRequest.headers['content-type'] = 'application/sparql-query';\n    }\n    axiosRequest.data = request.body.sparql;\n  }\n\n  if (request.body.mode === 'file') {\n    if (!contentTypeDefined) {\n      axiosRequest.headers['content-type'] = 'application/octet-stream'; // Default headers for binary file uploads\n    }\n\n    const bodyFile = find(request.body.file, (param) => param.selected);\n    if (bodyFile) {\n      let { filePath, contentType } = bodyFile;\n\n      axiosRequest.headers['content-type'] = contentType;\n\n      if (filePath) {\n        if (!path.isAbsolute(filePath)) {\n          filePath = path.join(collectionPath, filePath);\n        }\n\n        try {\n          // Large files can cause \"JavaScript heap out of memory\" errors when loaded entirely into memory.\n          if (isLargeFile(filePath, STREAMING_FILE_SIZE_THRESHOLD)) {\n            // For large files: Use streaming to avoid memory issues\n            axiosRequest.data = fs.createReadStream(filePath);\n          } else {\n            // For smaller files: Use synchronous read for better performance\n            axiosRequest.data = fs.readFileSync(filePath);\n          }\n        } catch (error) {\n          console.error('Error reading file:', error);\n        }\n      }\n    }\n  }\n\n  if (request.body.mode === 'formUrlEncoded') {\n    if (!contentTypeDefined) {\n      axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded';\n    }\n    const enabledParams = filter(request.body.formUrlEncoded, (p) => p.enabled);\n    axiosRequest.data = enabledParams;\n  }\n\n  if (request.body.mode === 'multipartForm') {\n    axiosRequest.headers['content-type'] = 'multipart/form-data';\n    const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);\n    axiosRequest.data = enabledParams;\n  }\n\n  if (request.body.mode === 'graphql') {\n    const graphqlQuery = {\n      query: get(request, 'body.graphql.query'),\n      // Parse variables only after interpolation (github.com/usebruno/bruno/issues/884)\n      variables: decomment(get(request, 'body.graphql.variables') || '{}')\n    };\n    if (!contentTypeDefined) {\n      axiosRequest.headers['content-type'] = 'application/json';\n    }\n    axiosRequest.data = graphqlQuery;\n  }\n\n  // if the mode is 'none' then set the content-type header to null to prevent axios from adding default. #1693\n  // AWS SigV4 requires Content-Type header in canonical request for signature calculation,\n  // even with no body. Omitting it would cause authentication failures.\n  if (request.body.mode === 'none' && (!request.auth || request.auth.mode !== 'awsv4')) {\n    if (!contentTypeDefined) {\n      // Setting to null tells axios not to add a default Content-Type header\n      // Use lowercase to match what scripts use, avoiding duplicate headers\n      axiosRequest.headers['content-type'] = null;\n    }\n  }\n\n  if (request.script) {\n    axiosRequest.script = request.script;\n  }\n\n  if (request.tests) {\n    axiosRequest.tests = request.tests;\n    axiosRequest.testsMetadata = request.testsMetadata;\n  }\n\n  axiosRequest.vars = request.vars;\n  axiosRequest.collectionVariables = request.collectionVariables;\n  axiosRequest.folderVariables = request.folderVariables;\n  axiosRequest.requestVariables = request.requestVariables;\n  axiosRequest.oauth2CredentialVariables = getFormattedOauth2Credentials();\n\n  return axiosRequest;\n};\n\nmodule.exports = prepareRequest;\n"
  },
  {
    "path": "packages/bruno-cli/src/runner/run-single-request.js",
    "content": "const qs = require('qs');\nconst chalk = require('chalk');\nconst decomment = require('decomment');\nconst fs = require('fs');\nconst { forOwn, isUndefined, isNull, each, extend, get, compact } = require('lodash');\nconst prepareRequest = require('./prepare-request');\nconst interpolateVars = require('./interpolate-vars');\nconst { interpolateString, interpolateObject } = require('./interpolate-string');\nconst { ScriptRuntime, TestRuntime, VarsRuntime, AssertRuntime, formatErrorWithContext, SCRIPT_TYPES } = require('@usebruno/js');\nconst { stripExtension } = require('../utils/filesystem');\nconst { getOptions } = require('../utils/bru');\nconst https = require('node:https');\nconst http = require('node:http');\nconst { HttpProxyAgent } = require('http-proxy-agent');\nconst { SocksProxyAgent } = require('socks-proxy-agent');\nconst { makeAxiosInstance } = require('../utils/axios-instance');\nconst { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper');\nconst { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util');\nconst path = require('path');\nconst { parseDataFromResponse } = require('../utils/common');\nconst { getCookieStringForUrl, saveCookies } = require('../utils/cookies');\nconst { createFormData } = require('../utils/form-data');\nconst protocolRegex = /^([-+\\w]{1,25})(:?\\/\\/|:)/;\nconst { NtlmClient } = require('axios-ntlm');\nconst { addDigestInterceptor, getHttpHttpsAgents, makeAxiosInstance: makeAxiosInstanceForOauth2 } = require('@usebruno/requests');\nconst { getCACertificates, transformProxyConfig, getOrCreateHttpsAgent, getOrCreateHttpAgent } = require('@usebruno/requests');\nconst { getOAuth2Token, getFormattedOauth2Credentials } = require('../utils/oauth2');\nconst tokenStore = require('../store/tokenStore');\nconst { encodeUrl, buildFormUrlEncodedPayload, extractPromptVariables, isFormData } = require('@usebruno/common').utils;\n\nconst onConsoleLog = (type, args) => {\n  console[type](...args);\n};\n\nconst getCACertHostRegex = (domain) => {\n  return '^https:\\\\/\\\\/' + domain.replaceAll('.', '\\\\.').replaceAll('*', '.*');\n};\n\n/**\n * Extract prompt variables from a request\n * Tries to respect the hierarchy of the variables and avoid unnecessary prompts as much as possible\n * Note: TO BE CALLED ONLY AFTER THE PREPARE REQUEST\n *\n * @param {*} request - request object built by prepareRequest\n * @returns {string[]} An array of extracted prompt variables\n */\nconst extractPromptVariablesForRequest = ({ request, collection, envVariables, runtimeVariables, processEnvVars, brunoConfig }) => {\n  const { vars, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, ...requestObj } = request;\n\n  const allVariables = {\n    ...globalEnvironmentVariables,\n    ...envVariables,\n    ...collectionVariables,\n    ...folderVariables,\n    ...requestVariables,\n    ...runtimeVariables,\n    process: {\n      env: {\n        ...processEnvVars\n      }\n    }\n  };\n\n  const prompts = extractPromptVariables(requestObj);\n  prompts.push(...extractPromptVariables(allVariables));\n\n  const interpolationOptions = {\n    globalEnvVars: globalEnvironmentVariables,\n    envVars: envVariables,\n    runtimeVariables,\n    processEnvVars\n  };\n\n  // client certificate config\n  const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []);\n  for (let clientCert of clientCertConfig) {\n    const domain = interpolateString(clientCert?.domain, interpolationOptions);\n    if (domain) {\n      const hostRegex = getCACertHostRegex(domain);\n      if (request.url.match(hostRegex)) {\n        prompts.push(...extractPromptVariables(clientCert));\n      }\n    }\n  }\n\n  // return unique prompt variables\n  return Array.from(new Set(prompts));\n};\n\nconst runSingleRequest = async function (\n  item,\n  collectionPath,\n  runtimeVariables,\n  envVariables,\n  processEnvVars,\n  brunoConfig,\n  collectionRoot,\n  runtime,\n  collection,\n  runSingleRequestByPathname,\n  globalEnvVars = {}\n) {\n  const { pathname: itemPathname } = item;\n  const relativeItemPathname = path.relative(collectionPath, itemPathname);\n\n  const logResults = (results, title, scriptType = null, request = null) => {\n    if (results?.length) {\n      if (title) {\n        console.log(chalk.dim(title));\n      }\n      each(results, (r) => {\n        const message = r.description || `${r.lhsExpr}: ${r.rhsExpr}`;\n        if (r.status === 'pass') {\n          console.log(chalk.green(`   ✓ `) + chalk.dim(message));\n        } else {\n          console.log(chalk.red(`   ✕ `) + chalk.red(message));\n          if (r.stack && scriptType) {\n            const errorObj = {\n              message: r.error || message,\n              stack: r.stack,\n              name: r.errorName || 'Error'\n            };\n            const metadata = scriptType === SCRIPT_TYPES.PRE_REQUEST ? request?.script?.reqMetadata\n              : scriptType === SCRIPT_TYPES.POST_RESPONSE ? request?.script?.resMetadata\n                : scriptType === SCRIPT_TYPES.TEST ? request?.testsMetadata\n                  : null;\n            console.log('\\n' + formatErrorWithContext(errorObj, relativeItemPathname, scriptType, 5, metadata) + '\\n');\n          } else if (r.error) {\n            console.log(chalk.red(`      ${r.error}`));\n          }\n        }\n      });\n    }\n  };\n\n  try {\n    let request;\n    let nextRequestName;\n    let shouldStopRunnerExecution = false;\n    let preRequestTestResults = [];\n    let postResponseTestResults = [];\n\n    request = await prepareRequest(item, collection);\n\n    // Set global environment variables on the request for scripts to access via bru.getGlobalEnvVar()\n    request.globalEnvironmentVariables = globalEnvVars;\n\n    // Detect prompt variables before proceeding\n    const promptVars = extractPromptVariablesForRequest({ request, collection, envVariables, runtimeVariables, processEnvVars, brunoConfig });\n\n    if (promptVars.length > 0) {\n      const errorMsg = `Prompt variables detected in request. CLI execution is not supported for requests with prompt variables. \\nPrompts: ${promptVars.join(', ')}`;\n      console.log(chalk.yellow(stripExtension(relativeItemPathname) + ' Skipped:') + chalk.dim(` (${errorMsg})`));\n      return {\n        test: {\n          filename: relativeItemPathname\n        },\n        request: {\n          method: request.method,\n          url: request.url,\n          headers: request.headers,\n          data: request.data\n        },\n        response: {\n          status: 'skipped',\n          statusText: errorMsg,\n          data: null,\n          responseTime: 0\n        },\n        error: null,\n        status: 'skipped',\n        skipped: true,\n        assertionResults: [],\n        testResults: [],\n        preRequestTestResults: [],\n        postResponseTestResults: [],\n        shouldStopRunnerExecution\n      };\n    }\n\n    request.__bruno__executionMode = 'cli';\n\n    const scriptingConfig = get(brunoConfig, 'scripts', {});\n    scriptingConfig.runtime = runtime;\n\n    // Build certsAndProxyConfig for bru.sendRequest\n    const options = getOptions();\n    const systemProxyConfig = options['cachedSystemProxy'];\n    const sendRequestInterpolationOptions = {\n      envVars: envVariables,\n      runtimeVariables,\n      processEnvVars,\n      globalEnvVars,\n      collectionVariables: request.collectionVariables || {},\n      folderVariables: request.folderVariables || {},\n      requestVariables: request.requestVariables || {}\n    };\n    const rawClientCertificates = get(brunoConfig, 'clientCertificates');\n    const rawProxyConfig = get(brunoConfig, 'proxy', {});\n    const certsAndProxyConfig = {\n      collectionPath,\n      options: {\n        noproxy: get(options, 'noproxy', false),\n        shouldVerifyTls: !get(options, 'insecure', false),\n        shouldUseCustomCaCertificate: !!options['cacert'],\n        customCaCertificateFilePath: options['cacert'],\n        shouldKeepDefaultCaCertificates: !options['ignoreTruststore'],\n        cacheSslSession: get(options, 'cacheSslSession', false)\n      },\n      clientCertificates: rawClientCertificates ? interpolateObject(rawClientCertificates, sendRequestInterpolationOptions) : undefined,\n      collectionLevelProxy: transformProxyConfig(interpolateObject(rawProxyConfig, sendRequestInterpolationOptions)),\n      systemProxyConfig\n    };\n\n    // Add certsAndProxyConfig to request object for bru.sendRequest\n    request.certsAndProxyConfig = certsAndProxyConfig;\n\n    // run pre request script\n    const requestScriptFile = get(request, 'script.req');\n    const collectionName = collection?.brunoConfig?.name;\n    if (requestScriptFile?.length) {\n      const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });\n      try {\n        const result = await scriptRuntime.runRequestScript(decomment(requestScriptFile, { space: true }),\n          request,\n          envVariables,\n          runtimeVariables,\n          collectionPath,\n          onConsoleLog,\n          processEnvVars,\n          scriptingConfig,\n          runSingleRequestByPathname,\n          collectionName);\n        if (result?.nextRequestName !== undefined) {\n          nextRequestName = result.nextRequestName;\n        }\n\n        if (result?.stopExecution) {\n          shouldStopRunnerExecution = true;\n        }\n\n        if (result?.oauth2CredentialsToReset?.length) {\n          for (const credentialId of result.oauth2CredentialsToReset) {\n            tokenStore.deleteCredentialById(credentialId);\n          }\n        }\n\n        if (result?.skipRequest) {\n          return {\n            test: {\n              filename: relativeItemPathname\n            },\n            request: {\n              method: request.method,\n              url: request.url,\n              headers: request.headers,\n              data: request.data\n            },\n            response: {\n              status: 'skipped',\n              statusText: 'request skipped via pre-request script',\n              data: null,\n              responseTime: 0\n            },\n            error: null,\n            status: 'skipped',\n            skipped: true,\n            assertionResults: [],\n            testResults: [],\n            preRequestTestResults: result?.results || [],\n            postResponseTestResults: [],\n            shouldStopRunnerExecution\n          };\n        }\n\n        preRequestTestResults = result?.results || [];\n      } catch (error) {\n        // Pre-request errors are treated as request errors (we return early with status: 'error'), not as failures. Unlike post-response and test script errors, we do not add a synthetic fail and continue.\n        console.error(chalk.red(`[${relativeItemPathname}] Pre-request script error:`));\n        console.log('\\n' + formatErrorWithContext(error, relativeItemPathname, SCRIPT_TYPES.PRE_REQUEST, 5, request.script?.reqMetadata) + '\\n');\n\n        // Extract partial results from the error (tests that passed before the error)\n        preRequestTestResults = error?.partialResults?.results || [];\n\n        // Preserve nextRequestName if it was set before the error\n        if (error?.partialResults?.nextRequestName !== undefined) {\n          nextRequestName = error.partialResults.nextRequestName;\n        }\n\n        // Preserve stopExecution if it was set before the error\n        if (error?.partialResults?.stopExecution) {\n          shouldStopRunnerExecution = true;\n        }\n\n        logResults(preRequestTestResults, 'Pre-Request Tests', SCRIPT_TYPES.PRE_REQUEST, request);\n\n        // Pre-request script error: execution didn't complete (request never sent). Return early so we don't run the HTTP request, post-response script, assertions, or tests.\n        return {\n          test: {\n            filename: relativeItemPathname\n          },\n          request: {\n            method: request.method,\n            url: request.url,\n            headers: request.headers,\n            data: request.data\n          },\n          response: {\n            status: 'error',\n            statusText: null,\n            headers: null,\n            data: null,\n            url: null,\n            responseTime: 0\n          },\n          error: error?.message || 'An error occurred while executing the pre-request script.',\n          status: 'error',\n          assertionResults: [],\n          testResults: [],\n          preRequestTestResults,\n          postResponseTestResults: [],\n          nextRequestName: nextRequestName,\n          shouldStopRunnerExecution\n        };\n      }\n    }\n\n    // interpolate variables inside request\n    interpolateVars(request, envVariables, runtimeVariables, processEnvVars);\n\n    // if this is a graphql request, parse the variables, only after interpolation\n    // https://github.com/usebruno/bruno/issues/884\n    if (request.mode === 'graphql' && typeof request.data?.variables === 'string') {\n      try {\n        request.data.variables = JSON.parse(request.data.variables);\n      } catch (err) {\n        throw new Error(`Failed to parse GraphQL variables: ${err.message}`);\n      }\n    }\n\n    if (request.settings?.encodeUrl) {\n      request.url = encodeUrl(request.url);\n    }\n\n    if (!protocolRegex.test(request.url)) {\n      request.url = `http://${request.url}`;\n    }\n\n    const insecure = get(options, 'insecure', false);\n    const noproxy = get(options, 'noproxy', false);\n    const cachedSystemProxy = get(options, 'cachedSystemProxy', null);\n    const disableCache = !get(options, 'cacheSslSession', false);\n    const httpsAgentRequestFields = {};\n\n    if (insecure) {\n      httpsAgentRequestFields['rejectUnauthorized'] = false;\n    } else {\n      const caCertFilePath = options['cacert'];\n      let caCertificatesData = getCACertificates({ caCertFilePath, shouldKeepDefaultCerts: !options['ignoreTruststore'] });\n      let caCertificates = caCertificatesData.caCertificates;\n      httpsAgentRequestFields['ca'] = caCertificates || [];\n    }\n\n    const interpolationOptions = {\n      globalEnvVars: request.globalEnvironmentVariables || {},\n      envVars: envVariables,\n      runtimeVariables,\n      processEnvVars\n    };\n\n    // client certificate config\n    const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []);\n    for (let clientCert of clientCertConfig) {\n      const domain = interpolateString(clientCert?.domain, interpolationOptions);\n      const type = clientCert?.type || 'cert';\n      if (domain) {\n        const hostRegex = getCACertHostRegex(domain);\n        if (request.url.match(hostRegex)) {\n          if (type === 'cert') {\n            try {\n              let certFilePath = interpolateString(clientCert?.certFilePath, interpolationOptions);\n              certFilePath = path.isAbsolute(certFilePath) ? certFilePath : path.join(collectionPath, certFilePath);\n              let keyFilePath = interpolateString(clientCert?.keyFilePath, interpolationOptions);\n              keyFilePath = path.isAbsolute(keyFilePath) ? keyFilePath : path.join(collectionPath, keyFilePath);\n              httpsAgentRequestFields['cert'] = fs.readFileSync(certFilePath);\n              httpsAgentRequestFields['key'] = fs.readFileSync(keyFilePath);\n            } catch (err) {\n              console.log(chalk.red('Error reading cert/key file'), chalk.red(err?.message));\n            }\n          } else if (type === 'pfx') {\n            try {\n              let pfxFilePath = interpolateString(clientCert?.pfxFilePath, interpolationOptions);\n              pfxFilePath = path.isAbsolute(pfxFilePath) ? pfxFilePath : path.join(collectionPath, pfxFilePath);\n              httpsAgentRequestFields['pfx'] = fs.readFileSync(pfxFilePath);\n            } catch (err) {\n              console.log(chalk.red('Error reading pfx file'), chalk.red(err?.message));\n            }\n          }\n          httpsAgentRequestFields['passphrase'] = interpolateString(clientCert.passphrase, interpolationOptions);\n          break;\n        }\n      }\n    }\n\n    let proxyMode = 'off';\n    let proxyConfig = {};\n\n    const collectionProxyConfig = transformProxyConfig(get(brunoConfig, 'proxy', {}));\n    const collectionProxyDisabled = get(collectionProxyConfig, 'disabled', false);\n    const collectionProxyInherit = get(collectionProxyConfig, 'inherit', true);\n    const collectionProxyConfigData = get(collectionProxyConfig, 'config', {});\n\n    if (noproxy || collectionProxyDisabled) {\n      // If noproxy flag is set or collection proxy is disabled, don't use any proxy\n      proxyMode = 'off';\n    } else if (!collectionProxyDisabled && !collectionProxyInherit) {\n      // Use collection-specific proxy\n      proxyConfig = collectionProxyConfigData;\n      proxyMode = 'on';\n    } else if (!collectionProxyDisabled && collectionProxyInherit) {\n      // Inherit from system proxy\n      if (cachedSystemProxy) {\n        const { http_proxy, https_proxy } = cachedSystemProxy;\n        if (http_proxy?.length || https_proxy?.length) {\n          proxyMode = 'system';\n        }\n      }\n      // else: no system proxy available, proxyMode stays 'off'\n    }\n    // else: collection proxy is disabled, proxyMode stays 'off'\n\n    // Prepare TLS options for agent caching\n    const tlsOptions = {\n      ...httpsAgentRequestFields\n    };\n\n    // HTTP agent options — separate from tlsOptions to avoid leaking TLS fields\n    const httpAgentOptions = { keepAlive: true };\n\n    const parsedRequestUrl = new URL(request.url);\n    const isHttpsRequest = parsedRequestUrl.protocol === 'https:';\n    const hostname = parsedRequestUrl.hostname || null;\n\n    if (proxyMode === 'on') {\n      const shouldProxy = shouldUseProxy(request.url, get(proxyConfig, 'bypassProxy', ''));\n      if (shouldProxy) {\n        const proxyProtocol = interpolateString(get(proxyConfig, 'protocol'), interpolationOptions);\n        const proxyHostname = interpolateString(get(proxyConfig, 'hostname'), interpolationOptions);\n        const proxyPort = interpolateString(get(proxyConfig, 'port'), interpolationOptions);\n        const proxyAuthEnabled = !get(proxyConfig, 'auth.disabled', false);\n        const socksEnabled = proxyProtocol.includes('socks');\n        let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;\n        let proxyUri;\n        if (proxyAuthEnabled) {\n          const proxyAuthUsername = encodeURIComponent(interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions));\n          const proxyAuthPassword = encodeURIComponent(interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions));\n\n          proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;\n        } else {\n          proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;\n        }\n        // When the proxy itself uses HTTPS, the agent connecting to it needs TLS options\n        // (e.g., ca certs) even for plain HTTP requests\n        const isHttpsProxy = proxyProtocol === 'https';\n        const httpProxyAgentOptions = isHttpsProxy ? { ...httpAgentOptions, ...tlsOptions } : httpAgentOptions;\n\n        // Only set the agent needed for the request protocol\n        if (socksEnabled) {\n          if (isHttpsRequest) {\n            request.httpsAgent = getOrCreateHttpsAgent({ AgentClass: SocksProxyAgent, options: tlsOptions, proxyUri, disableCache, hostname });\n          } else {\n            request.httpAgent = getOrCreateHttpAgent({ AgentClass: SocksProxyAgent, options: httpProxyAgentOptions, proxyUri, disableCache, hostname });\n          }\n        } else {\n          if (isHttpsRequest) {\n            request.httpsAgent = getOrCreateHttpsAgent({ AgentClass: PatchedHttpsProxyAgent, options: tlsOptions, proxyUri, disableCache, hostname });\n          } else {\n            request.httpAgent = getOrCreateHttpAgent({ AgentClass: HttpProxyAgent, options: httpProxyAgentOptions, proxyUri, disableCache, hostname });\n          }\n        }\n      }\n    } else if (proxyMode === 'system') {\n      try {\n        const { http_proxy, https_proxy, no_proxy } = cachedSystemProxy || {};\n        const shouldUseSystemProxy = shouldUseProxy(request.url, no_proxy || '');\n        if (shouldUseSystemProxy) {\n          try {\n            if (http_proxy?.length && !isHttpsRequest) {\n              const parsedHttpProxy = new URL(http_proxy);\n              const isHttpsSystemProxy = parsedHttpProxy.protocol === 'https:';\n              const systemHttpProxyAgentOptions = isHttpsSystemProxy ? { ...httpAgentOptions, ...tlsOptions } : httpAgentOptions;\n              request.httpAgent = getOrCreateHttpAgent({ AgentClass: HttpProxyAgent, options: systemHttpProxyAgentOptions, proxyUri: http_proxy, disableCache, hostname });\n            }\n          } catch (error) {\n            throw new Error('Invalid system http_proxy');\n          }\n          try {\n            if (https_proxy?.length && isHttpsRequest) {\n              new URL(https_proxy);\n              request.httpsAgent = getOrCreateHttpsAgent({ AgentClass: PatchedHttpsProxyAgent, options: tlsOptions, proxyUri: https_proxy, disableCache, hostname });\n            }\n          } catch (error) {\n            throw new Error('Invalid system https_proxy');\n          }\n        }\n      } catch (error) {}\n    }\n\n    if (!request.httpAgent && !request.httpsAgent) {\n      if (isHttpsRequest) {\n        request.httpsAgent = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: tlsOptions, disableCache, hostname });\n      } else {\n        request.httpAgent = getOrCreateHttpAgent({ AgentClass: http.Agent, options: httpAgentOptions, disableCache, hostname });\n      }\n    }\n\n    // set cookies if enabled\n    if (!options.disableCookies) {\n      const cookieString = getCookieStringForUrl(request.url);\n      if (cookieString && typeof cookieString === 'string' && cookieString.length) {\n        const existingCookieHeaderName = Object.keys(request.headers).find(\n          (name) => name.toLowerCase() === 'cookie'\n        );\n        const existingCookieString = existingCookieHeaderName ? request.headers[existingCookieHeaderName] : '';\n\n        // Helper function to parse cookies into an object\n        const parseCookies = (str) => str.split(';').reduce((cookies, cookie) => {\n          const [name, ...rest] = cookie.split('=');\n          if (name && name.trim()) {\n            cookies[name.trim()] = rest.join('=').trim();\n          }\n          return cookies;\n        }, {});\n\n        const mergedCookies = {\n          ...parseCookies(existingCookieString),\n          ...parseCookies(cookieString)\n        };\n\n        const combinedCookieString = Object.entries(mergedCookies)\n          .map(([name, value]) => `${name}=${value}`)\n          .join('; ');\n\n        request.headers[existingCookieHeaderName || 'Cookie'] = combinedCookieString;\n      }\n    }\n\n    // stringify the request url encoded params\n    const contentTypeHeader = Object.keys(request.headers).find(\n      (name) => name.toLowerCase() === 'content-type'\n    );\n\n    if (contentTypeHeader && request.headers[contentTypeHeader] === 'application/x-www-form-urlencoded') {\n      if (Array.isArray(request.data)) {\n        request.data = buildFormUrlEncodedPayload(request.data);\n      } else if (typeof request.data !== 'string') {\n        request.data = qs.stringify(request.data, { arrayFormat: 'repeat' });\n      }\n      // if `data` is of string type - return as-is (assumes already encoded)\n    }\n\n    const contentType = contentTypeHeader ? request.headers[contentTypeHeader] : '';\n    if (typeof contentType === 'string' && contentType.startsWith('multipart/')) {\n      if (!isFormData(request?.data)) {\n        request._originalMultipartData = request.data;\n        request.collectionPath = collectionPath;\n        let form = createFormData(request.data, collectionPath);\n        request.data = form;\n\n        if (contentType !== 'multipart/form-data') {\n          // Patch: Axios leverages getHeaders method to get the headers so FormData should be monkey patched\n          const formHeaders = form.getHeaders();\n          formHeaders['content-type'] = `${contentType}; boundary=${form.getBoundary()}`;\n          form.getHeaders = function () {\n            return formHeaders;\n          };\n        }\n\n        extend(request.headers, form.getHeaders());\n      }\n    }\n\n    // Get followRedirects setting, default to true for backward compatibility\n    const followRedirects = request.settings?.followRedirects ?? true;\n\n    // Get maxRedirects from request settings, fallback to request.maxRedirects, then default to 5\n    let requestMaxRedirects = request.settings?.maxRedirects ?? request.maxRedirects ?? 5;\n\n    // Ensure it's a valid number\n    if (typeof requestMaxRedirects !== 'number' || requestMaxRedirects < 0) {\n      requestMaxRedirects = 5; // Default to 5 redirects\n    }\n\n    // If followRedirects is disabled, set maxRedirects to 0 to disable all redirects\n    if (!followRedirects) {\n      requestMaxRedirects = 0;\n    }\n\n    request.maxRedirects = 0;\n\n    // Handle OAuth2 authentication\n    if (request.oauth2) {\n      try {\n        // Prepare interpolation options with all available variables\n        const oauth2InterpolationOptions = {\n          globalEnvVars: request.globalEnvironmentVariables || {},\n          envVars: envVariables,\n          runtimeVariables,\n          processEnvVars,\n          collectionVariables: request.collectionVariables || {},\n          folderVariables: request.folderVariables || {},\n          requestVariables: request.requestVariables || {}\n        };\n\n        const accessTokenUrl = request.oauth2.accessTokenUrl ? interpolateString(request.oauth2.accessTokenUrl, oauth2InterpolationOptions) : undefined;\n        const refreshTokenUrl = request.oauth2.refreshTokenUrl ? interpolateString(request.oauth2.refreshTokenUrl, oauth2InterpolationOptions) : undefined;\n        const oauth2RequestUrl = accessTokenUrl || refreshTokenUrl;\n\n        let token;\n        if (oauth2RequestUrl) {\n          const oauth2ConfigOptions = {\n            noproxy: options.noproxy,\n            shouldVerifyTls: !insecure,\n            shouldUseCustomCaCertificate: !!options['cacert'],\n            customCaCertificateFilePath: options['cacert'],\n            shouldKeepDefaultCaCertificates: !options['ignoreTruststore'],\n            cacheSslSession: !disableCache\n          };\n\n          const clientCertificates = get(brunoConfig, 'clientCertificates');\n          const proxyConfig = get(brunoConfig, 'proxy');\n          const interpolatedClientCertificates = clientCertificates ? interpolateObject(clientCertificates, oauth2InterpolationOptions) : undefined;\n          const interpolatedProxyConfig = proxyConfig ? interpolateObject(proxyConfig, oauth2InterpolationOptions) : undefined;\n          const systemProxyConfig = cachedSystemProxy;\n\n          const { httpAgent: oauth2HttpAgent, httpsAgent: oauth2HttpsAgent } = await getHttpHttpsAgents({\n            requestUrl: oauth2RequestUrl,\n            collectionPath,\n            options: oauth2ConfigOptions,\n            clientCertificates: interpolatedClientCertificates,\n            collectionLevelProxy: interpolatedProxyConfig,\n            systemProxyConfig\n          });\n\n          const oauth2AxiosInstance = makeAxiosInstanceForOauth2({\n            requestMaxRedirects: requestMaxRedirects,\n            disableCookies: options.disableCookies,\n            httpAgent: oauth2HttpAgent,\n            httpsAgent: oauth2HttpsAgent\n          });\n\n          token = await getOAuth2Token(request.oauth2, oauth2AxiosInstance);\n        }\n\n        if (token) {\n          const { tokenPlacement = 'header', tokenHeaderPrefix = '', tokenQueryKey = 'access_token' } = request.oauth2;\n\n          if (tokenPlacement === 'header' && token) {\n            request.headers['Authorization'] = `${tokenHeaderPrefix} ${token}`.trim();\n          } else if (tokenPlacement === 'url') {\n            try {\n              const url = new URL(request.url);\n              url.searchParams.set(tokenQueryKey, token);\n              request.url = url.toString();\n            } catch (error) {\n              console.error('Error applying OAuth2 token to URL:', error.message);\n            }\n          }\n        }\n      } catch (error) {\n        console.error('OAuth2 token fetch error:', error.message);\n      }\n\n      request.oauth2CredentialVariables = getFormattedOauth2Credentials();\n\n      // Remove oauth2 config from request to prevent it from being sent\n      delete request.oauth2;\n    }\n\n    let response, responseTime;\n    try {\n      // Set timeout from request settings, default to 0 (no timeout)\n      const requestTimeout = request.settings?.timeout || 0;\n      if (requestTimeout > 0) {\n        request.timeout = requestTimeout;\n      }\n\n      let axiosInstance = makeAxiosInstance({\n        requestMaxRedirects: requestMaxRedirects,\n        disableCookies: options.disableCookies,\n        followRedirects: followRedirects\n      });\n\n      if (request.ntlmConfig) {\n        axiosInstance = NtlmClient(request.ntlmConfig, axiosInstance.defaults);\n        delete request.ntlmConfig;\n      }\n\n      if (request.awsv4config) {\n        // todo: make this happen in prepare-request.js\n        // interpolate the aws v4 config\n        request.awsv4config.accessKeyId = interpolateString(request.awsv4config.accessKeyId, interpolationOptions);\n        request.awsv4config.secretAccessKey = interpolateString(\n          request.awsv4config.secretAccessKey,\n          interpolationOptions\n        );\n        request.awsv4config.sessionToken = interpolateString(request.awsv4config.sessionToken, interpolationOptions);\n        request.awsv4config.service = interpolateString(request.awsv4config.service, interpolationOptions);\n        request.awsv4config.region = interpolateString(request.awsv4config.region, interpolationOptions);\n        request.awsv4config.profileName = interpolateString(request.awsv4config.profileName, interpolationOptions);\n\n        request.awsv4config = await resolveAwsV4Credentials(request);\n        addAwsV4Interceptor(axiosInstance, request);\n        delete request.awsv4config;\n      }\n\n      if (request.digestConfig) {\n        addDigestInterceptor(axiosInstance, request);\n        delete request.digestConfig;\n      }\n\n      /** @type {import('axios').AxiosResponse} */\n      response = await axiosInstance(request);\n\n      const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);\n      response.data = data;\n      response.dataBuffer = dataBuffer;\n\n      // Prevents the duration on leaking to the actual result\n      responseTime = response.headers.get('request-duration');\n      response.headers.delete('request-duration');\n\n      // save cookies if enabled\n      if (!options.disableCookies) {\n        saveCookies(request.url, response.headers);\n      }\n    } catch (err) {\n      if (err?.response) {\n        const { data, dataBuffer } = parseDataFromResponse(err?.response);\n        err.response.data = data;\n        err.response.dataBuffer = dataBuffer;\n        response = err.response;\n\n        // Prevents the duration on leaking to the actual result\n        responseTime = response.headers.get('request-duration');\n        response.headers.delete('request-duration');\n      } else {\n        console.log(chalk.red(stripExtension(relativeItemPathname)) + chalk.dim(` (${err.message})`));\n        return {\n          test: {\n            filename: relativeItemPathname\n          },\n          request: {\n            method: request.method,\n            url: request.url,\n            headers: request.headers,\n            data: request.data\n          },\n          response: {\n            status: 'error',\n            statusText: null,\n            headers: null,\n            data: null,\n            url: null,\n            responseTime: 0\n          },\n          error: err?.message || err?.errors?.map((e) => e?.message)?.at(0) || err?.code || 'Request Failed!',\n          status: 'error',\n          assertionResults: [],\n          testResults: [],\n          preRequestTestResults,\n          postResponseTestResults,\n          nextRequestName: nextRequestName,\n          shouldStopRunnerExecution\n        };\n      }\n    }\n\n    response.responseTime = responseTime;\n\n    console.log(\n      chalk.green(stripExtension(relativeItemPathname))\n      + chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`)\n    );\n\n    // Log pre-request test results\n    logResults(preRequestTestResults, 'Pre-Request Tests', SCRIPT_TYPES.PRE_REQUEST, request);\n\n    // run post-response vars\n    const postResponseVars = get(item, 'request.vars.res');\n    if (postResponseVars?.length) {\n      const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });\n      varsRuntime.runPostResponseVars(\n        postResponseVars,\n        request,\n        response,\n        envVariables,\n        runtimeVariables,\n        collectionPath,\n        processEnvVars\n      );\n    }\n\n    // run post response script\n    const responseScriptFile = get(request, 'script.res');\n    if (responseScriptFile?.length) {\n      const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });\n      try {\n        const result = await scriptRuntime.runResponseScript(\n          decomment(responseScriptFile, { space: true }),\n          request,\n          response,\n          envVariables,\n          runtimeVariables,\n          collectionPath,\n          onConsoleLog,\n          processEnvVars,\n          scriptingConfig,\n          runSingleRequestByPathname,\n          collectionName\n        );\n        if (result?.nextRequestName !== undefined) {\n          nextRequestName = result.nextRequestName;\n        }\n\n        if (result?.stopExecution) {\n          shouldStopRunnerExecution = true;\n        }\n\n        if (result?.oauth2CredentialsToReset?.length) {\n          for (const credentialId of result.oauth2CredentialsToReset) {\n            tokenStore.deleteCredentialById(credentialId);\n          }\n        }\n\n        postResponseTestResults = result?.results || [];\n        logResults(postResponseTestResults, 'Post-Response Tests', SCRIPT_TYPES.POST_RESPONSE, request);\n      } catch (error) {\n        console.error(chalk.red(`[${relativeItemPathname}] Post-response script error:`));\n        console.log('\\n' + formatErrorWithContext(error, relativeItemPathname, SCRIPT_TYPES.POST_RESPONSE, 5, request.script?.resMetadata) + '\\n');\n\n        const partialResults = error?.partialResults?.results || [];\n        postResponseTestResults = [\n          ...partialResults,\n          {\n            status: 'fail',\n            description: 'Post-Response Script Error',\n            error: error.message || 'An error occurred while executing the post-response script.',\n            isScriptError: true\n          }\n        ];\n\n        if (error?.partialResults?.nextRequestName !== undefined) {\n          nextRequestName = error.partialResults.nextRequestName;\n        }\n\n        if (error?.partialResults?.stopExecution) {\n          shouldStopRunnerExecution = true;\n        }\n\n        logResults(postResponseTestResults, 'Post-Response Tests', SCRIPT_TYPES.POST_RESPONSE, request);\n      }\n    }\n\n    let assertionResults = [];\n    const assertions = get(item, 'request.assertions');\n    if (assertions) {\n      const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime });\n      assertionResults = assertRuntime.runAssertions(\n        assertions,\n        request,\n        response,\n        envVariables,\n        runtimeVariables,\n        processEnvVars\n      );\n    }\n\n    // run tests\n    let testResults = [];\n    const testFile = get(request, 'tests');\n    if (typeof testFile === 'string') {\n      const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });\n      try {\n        const result = await testRuntime.runTests(\n          decomment(testFile, { space: true }),\n          request,\n          response,\n          envVariables,\n          runtimeVariables,\n          collectionPath,\n          onConsoleLog,\n          processEnvVars,\n          scriptingConfig,\n          runSingleRequestByPathname,\n          collectionName\n        );\n        testResults = get(result, 'results', []);\n\n        if (result?.nextRequestName !== undefined) {\n          nextRequestName = result.nextRequestName;\n        }\n\n        if (result?.stopExecution) {\n          shouldStopRunnerExecution = true;\n        }\n\n        if (result?.oauth2CredentialsToReset?.length) {\n          for (const credentialId of result.oauth2CredentialsToReset) {\n            tokenStore.deleteCredentialById(credentialId);\n          }\n        }\n\n        logResults(testResults, 'Tests', SCRIPT_TYPES.TEST, request);\n      } catch (error) {\n        console.error(chalk.red(`[${relativeItemPathname}] Test script error:`));\n        console.log('\\n' + formatErrorWithContext(error, relativeItemPathname, SCRIPT_TYPES.TEST, 5, request.testsMetadata) + '\\n');\n\n        const partialResults = error?.partialResults?.results || [];\n        testResults = [\n          ...partialResults,\n          {\n            status: 'fail',\n            description: 'Test Script Error',\n            error: error.message || 'An error occurred while executing the test script.',\n            isScriptError: true\n          }\n        ];\n\n        if (error?.partialResults?.nextRequestName !== undefined) {\n          nextRequestName = error.partialResults.nextRequestName;\n        }\n\n        if (error?.partialResults?.stopExecution) {\n          shouldStopRunnerExecution = true;\n        }\n\n        logResults(testResults, 'Tests', SCRIPT_TYPES.TEST, request);\n      }\n    }\n\n    logResults(assertionResults, 'Assertions');\n\n    return {\n      test: {\n        filename: relativeItemPathname\n      },\n      request: {\n        method: request.method,\n        url: request.url,\n        headers: request.headers,\n        data: request.data\n      },\n      response: {\n        status: response.status,\n        statusText: response.statusText,\n        headers: response.headers,\n        data: response.data,\n        url: response.request ? response.request.protocol + '//' + response.request.host + response.request.path : null,\n        responseTime\n      },\n      error: null,\n      status: 'pass',\n      assertionResults,\n      testResults,\n      preRequestTestResults,\n      postResponseTestResults,\n      nextRequestName: nextRequestName,\n      shouldStopRunnerExecution\n    };\n  } catch (err) {\n    console.log(chalk.red(stripExtension(relativeItemPathname)) + chalk.dim(` (${err.message})`));\n    return {\n      test: {\n        filename: relativeItemPathname\n      },\n      request: {\n        method: null,\n        url: null,\n        headers: null,\n        data: null\n      },\n      response: {\n        status: 'error',\n        statusText: null,\n        headers: null,\n        data: null,\n        url: null,\n        responseTime: 0\n      },\n      status: 'error',\n      error: err.message,\n      assertionResults: [],\n      testResults: [],\n      preRequestTestResults: [],\n      postResponseTestResults: []\n    };\n  }\n};\n\nmodule.exports = {\n  runSingleRequest\n};\n"
  },
  {
    "path": "packages/bruno-cli/src/store/tokenStore.js",
    "content": "// In-memory credential store implementation for OAuth2 credentials\nconst tokenStore = {\n  credentials: {},\n\n  // Save credentials\n  async saveCredential({ url, credentialsId, credentials }) {\n    if (!this.credentials[credentialsId]) {\n      this.credentials[credentialsId] = {};\n    }\n    this.credentials[credentialsId][url] = credentials;\n    return true;\n  },\n\n  // Get credentials\n  async getCredential({ url, credentialsId }) {\n    return this.credentials[credentialsId]?.[url];\n  },\n\n  // Delete credentials\n  async deleteCredential({ url, credentialsId }) {\n    if (this.credentials[credentialsId]?.[url]) {\n      delete this.credentials[credentialsId][url];\n      // Clean up empty credentialsId objects\n      if (Object.keys(this.credentials[credentialsId]).length === 0) {\n        delete this.credentials[credentialsId];\n      }\n      return true;\n    }\n    return false;\n  },\n\n  // Delete all credentials for a given credentialsId (all URLs)\n  deleteCredentialById(credentialsId) {\n    if (this.credentials[credentialsId]) {\n      delete this.credentials[credentialsId];\n      return true;\n    }\n    return false;\n  },\n\n  // Get all stored OAuth2 credentials\n  getAllCredentials() {\n    const result = [];\n    for (const [credentialsId, urlMap] of Object.entries(this.credentials)) {\n      for (const [url, credentials] of Object.entries(urlMap)) {\n        if (credentials) {\n          result.push({\n            url,\n            credentialsId,\n            credentials\n          });\n        }\n      }\n    }\n    return result;\n  }\n};\n\nmodule.exports = tokenStore;\n"
  },
  {
    "path": "packages/bruno-cli/src/utils/axios-instance.js",
    "content": "const axios = require('axios');\nconst { CLI_VERSION } = require('../constants');\nconst { addCookieToJar, getCookieStringForUrl } = require('./cookies');\nconst { createFormData } = require('./form-data');\n\nconst redirectResponseCodes = [301, 302, 303, 307, 308];\nconst METHOD_CHANGING_REDIRECTS = [301, 302, 303];\n\nconst saveCookies = (url, headers) => {\n  if (headers['set-cookie']) {\n    let setCookieHeaders = Array.isArray(headers['set-cookie'])\n      ? headers['set-cookie']\n      : [headers['set-cookie']];\n    for (let setCookieHeader of setCookieHeaders) {\n      if (typeof setCookieHeader === 'string' && setCookieHeader.length) {\n        addCookieToJar(setCookieHeader, url);\n      }\n    }\n  }\n};\n\nconst createRedirectConfig = (error, redirectUrl) => {\n  const requestConfig = {\n    ...error.config,\n    url: redirectUrl,\n    headers: { ...error.config.headers }\n  };\n\n  const statusCode = error.response.status;\n  const originalMethod = (error.config.method || 'get').toLowerCase();\n\n  // For 301, 302, 303: change method to GET unless it was HEAD\n  if (METHOD_CHANGING_REDIRECTS.includes(statusCode) && originalMethod !== 'head') {\n    requestConfig.method = 'get';\n    requestConfig.data = undefined;\n\n    // Clean up headers that are no longer relevant\n    delete requestConfig.headers['content-length'];\n    delete requestConfig.headers['Content-Length'];\n    delete requestConfig.headers['content-type'];\n    delete requestConfig.headers['Content-Type'];\n  } else {\n    // For 307, 308 and other status codes: preserve method and body\n    if (requestConfig.data && typeof requestConfig.data === 'object'\n      && requestConfig.data.constructor && requestConfig.data.constructor.name === 'FormData') {\n      const formData = requestConfig.data;\n      if (formData._released || (formData._streams && formData._streams.length === 0)) {\n        if (error.config._originalMultipartData && error.config.collectionPath) {\n          const recreatedForm = createFormData(error.config._originalMultipartData, error.config.collectionPath);\n          requestConfig.data = recreatedForm;\n          const formHeaders = recreatedForm.getHeaders();\n          Object.assign(requestConfig.headers, formHeaders);\n\n          // preserve the original data for potential future redirects\n          requestConfig._originalMultipartData = error.config._originalMultipartData;\n          requestConfig.collectionPath = error.config.collectionPath;\n        }\n      } else {\n        requestConfig._originalMultipartData = error.config._originalMultipartData;\n        requestConfig.collectionPath = error.config.collectionPath;\n      }\n    }\n  }\n\n  return requestConfig;\n};\n\n/**\n * Function that configures axios with timing interceptors\n * Important to note here that the timings are not completely accurate.\n * @see https://github.com/axios/axios/issues/695\n * @returns {axios.AxiosInstance}\n */\nfunction makeAxiosInstance({ requestMaxRedirects = 5, disableCookies, followRedirects = true } = {}) {\n  let redirectCount = 0;\n\n  /** @type {axios.AxiosInstance} */\n  const instance = axios.create({\n    proxy: false,\n    maxRedirects: 0,\n    headers: {}\n  });\n\n  // Set User-Agent manually (using transformRequest to delete headers instead)\n  instance.defaults.headers.common = {\n    'User-Agent': `bruno-runtime/${CLI_VERSION}`\n  };\n\n  instance.interceptors.request.use((config) => {\n    config.headers['request-start-time'] = Date.now();\n\n    /**\n      Apply header deletions requested via req.deleteHeader() in pre-request scripts.\n      Using set(name, null) rather than delete(): the axios http adapter guards its\n      own defaults (User-Agent, Accept-Encoding) with set(..., false) which only\n      skips writing when the key already exists. delete() removes the key entirely,\n      so the guard misses and the adapter re-adds the default. null keeps the key\n      present (blocking the guard) while toJSON() omits null values from the wire.\n    */\n    const headersToDelete = config.__headersToDelete;\n    if (headersToDelete && Array.isArray(headersToDelete)) {\n      headersToDelete.forEach((headerName) => {\n        const lower = headerName.toLowerCase();\n        if (lower === 'host' || lower === 'connection') return;\n        config.headers.set(headerName, null);\n      });\n      delete config.__headersToDelete;\n    }\n\n    // Add cookies to request if available and not disabled\n    if (!disableCookies) {\n      const cookieString = getCookieStringForUrl(config.url);\n      if (cookieString && typeof cookieString === 'string' && cookieString.length) {\n        config.headers['cookie'] = cookieString;\n      }\n    }\n\n    return config;\n  });\n\n  instance.interceptors.response.use(\n    (response) => {\n      const end = Date.now();\n      const start = response.config.headers['request-start-time'];\n      response.headers['request-duration'] = end - start;\n      redirectCount = 0;\n\n      return response;\n    },\n    (error) => {\n      if (error.response) {\n        const end = Date.now();\n        const start = error.config.headers['request-start-time'];\n        error.response.headers['request-duration'] = end - start;\n\n        if (redirectResponseCodes.includes(error.response.status)) {\n          if (!followRedirects) {\n            if (!disableCookies) {\n              saveCookies(error.config.url, error.response.headers);\n            }\n\n            return Promise.reject(error);\n          }\n\n          if (redirectCount >= requestMaxRedirects) {\n            // todo: needs to be discussed whether the original error response message should be modified or not\n            return Promise.reject(error);\n          }\n\n          const locationHeader = error.response.headers.location;\n          if (!locationHeader) {\n            // todo: needs to be discussed whether the original error response message should be modified or not\n            return Promise.reject(error);\n          }\n\n          redirectCount++;\n          let redirectUrl = locationHeader;\n\n          if (!locationHeader.match(/^https?:\\/\\//i)) {\n            const URL = require('url');\n            redirectUrl = URL.resolve(error.config.url, locationHeader);\n          }\n\n          if (!disableCookies) {\n            saveCookies(error.config.url, error.response.headers);\n          }\n\n          const requestConfig = createRedirectConfig(error, redirectUrl);\n\n          if (!disableCookies) {\n            const cookieString = getCookieStringForUrl(redirectUrl);\n            if (cookieString && typeof cookieString === 'string' && cookieString.length) {\n              requestConfig.headers['cookie'] = cookieString;\n            }\n          }\n\n          return instance(requestConfig);\n        }\n      }\n      return Promise.reject(error);\n    }\n  );\n\n  return instance;\n}\n\nmodule.exports = {\n  makeAxiosInstance\n};\n"
  },
  {
    "path": "packages/bruno-cli/src/utils/bru.js",
    "content": "const _ = require('lodash');\nconst {\n  parseRequest: _parseRequest,\n  parseCollection: _parseCollection\n} = require('@usebruno/filestore');\n\nconst collectionBruToJson = (bru) => {\n  try {\n    const json = _parseCollection(bru);\n\n    const transformedJson = {\n      request: {\n        headers: _.get(json, 'headers', []),\n        auth: _.get(json, 'auth', {}),\n        script: _.get(json, 'script', {}),\n        vars: _.get(json, 'vars', {}),\n        tests: _.get(json, 'tests', '')\n      }\n    };\n\n    // add meta if it exists\n    // this is only for folder bru file\n    // in the future, all of this will be replaced by standard bru lang\n    const sequence = _.get(json, 'meta.seq');\n    if (json?.meta) {\n      transformedJson.meta = {\n        name: json.meta.name\n      };\n\n      if (sequence) {\n        transformedJson.meta.seq = Number(sequence);\n      }\n    }\n\n    return transformedJson;\n  } catch (error) {\n    return Promise.reject(error);\n  }\n};\n\n/**\n * The transformer function for converting a BRU file to JSON.\n *\n * We map the json response from the bru lang and transform it into the DSL\n * format that is used by the bruno app\n *\n * @param {string} bru The BRU file content.\n * @returns {object} The JSON representation of the BRU file.\n */\nconst bruToJson = (bru) => {\n  try {\n    const json = _parseRequest(bru);\n\n    let requestType = _.get(json, 'meta.type');\n\n    switch (requestType) {\n      case 'http':\n        requestType = 'http-request';\n        break;\n      case 'graphql':\n        requestType = 'graphql-request';\n        break;\n      case 'grpc':\n        requestType = 'grpc-request';\n        break;\n      case 'ws':\n        requestType = 'ws-request';\n        break;\n      default:\n        requestType = 'http-request';\n    }\n\n    const sequence = _.get(json, 'meta.seq');\n    const transformedJson = {\n      type: requestType,\n      name: _.get(json, 'meta.name'),\n      seq: !_.isNaN(sequence) ? Number(sequence) : 1,\n      settings: _.get(json, 'settings', {}),\n      tags: _.get(json, 'meta.tags', []),\n      examples: _.get(json, 'examples', []),\n      request: {\n        url: _.get(json, requestType === 'grpc-request' ? 'grpc.url' : 'http.url'),\n        headers: requestType === 'grpc-request' ? _.get(json, 'metadata', []) : _.get(json, 'headers', []),\n        // Preserving special characters in custom methods. Using _.upperCase strips special characters.\n        method: String(_.get(json, 'http.method') ?? '').toUpperCase(),\n        auth: _.get(json, 'auth', {}),\n        params: _.get(json, 'params', []),\n        vars: _.get(json, 'vars', []),\n        assertions: _.get(json, 'assertions', []),\n        script: _.get(json, 'script', {}),\n        tests: _.get(json, 'tests', '')\n      }\n    };\n\n    if (requestType === 'grpc-request') {\n      const selectedMethod = _.get(json, 'grpc.method');\n      if (selectedMethod) transformedJson.request.method = selectedMethod;\n      const selectedMethodType = _.get(json, 'grpc.methodType');\n      if (selectedMethodType) transformedJson.request.methodType = selectedMethodType;\n      const protoPath = _.get(json, 'grpc.protoPath');\n      if (protoPath) transformedJson.request.protoPath = protoPath;\n      transformedJson.request.auth.mode = _.get(json, 'grpc.auth', 'none');\n      transformedJson.request.body = _.get(json, 'body', {\n        mode: 'grpc',\n        grpc: [{\n          name: 'message 1',\n          content: '{}'\n        }]\n      });\n    } else if (requestType === 'ws-request') {\n      transformedJson.request.auth.mode = _.get(json, 'ws.auth', 'none');\n      const bodyFromBru = _.get(json, 'body') || {};\n      transformedJson.request.body = {\n        mode: 'ws',\n        ws: [bodyFromBru]\n      };\n    } else {\n      transformedJson.request.method = _.upperCase(_.get(json, 'http.method'));\n      transformedJson.request.auth.mode = _.get(json, 'http.auth', 'none');\n      transformedJson.request.body = _.get(json, 'body', {});\n      transformedJson.request.body.mode = _.get(json, 'http.body', 'none');\n    }\n\n    return transformedJson;\n  } catch (err) {\n    return Promise.reject(err);\n  }\n};\n\nconst getEnvVars = (environment = {}) => {\n  const variables = environment.variables;\n  if (!variables || !variables.length) {\n    return {};\n  }\n\n  const envVars = {};\n  _.each(variables, (variable) => {\n    if (variable.enabled) {\n      envVars[variable.name] = variable.value;\n    }\n  });\n\n  return envVars;\n};\n\nconst options = {};\nconst getOptions = () => {\n  return options;\n};\n\nmodule.exports = {\n  bruToJson,\n  getEnvVars,\n  getOptions,\n  collectionBruToJson\n};\n"
  },
  {
    "path": "packages/bruno-cli/src/utils/collection.js",
    "content": "const { get, each, find } = require('lodash');\nconst os = require('os');\nconst fs = require('fs');\nconst path = require('path');\nconst { sanitizeName } = require('./filesystem');\nconst { parseRequest, parseCollection, parseFolder, stringifyCollection, stringifyFolder, stringifyEnvironment, stringifyRequest } = require('@usebruno/filestore');\nconst constants = require('../constants');\nconst chalk = require('chalk');\n\nconst FORMAT_CONFIG = {\n  yml: { ext: '.yml', collectionFile: 'opencollection.yml', folderFile: 'folder.yml' },\n  bru: { ext: '.bru', collectionFile: 'collection.bru', folderFile: 'folder.bru' }\n};\nconst REQUEST_ITEM_TYPES = ['http-request', 'graphql-request'];\n\nconst getCollectionFormat = (collectionPath) => {\n  if (fs.existsSync(path.join(collectionPath, 'opencollection.yml'))) return 'yml';\n  if (fs.existsSync(path.join(collectionPath, 'bruno.json'))) return 'bru';\n  return null;\n};\n\nconst getCollectionConfig = (collectionPath, format) => {\n  if (format === 'yml') {\n    const content = fs.readFileSync(path.join(collectionPath, 'opencollection.yml'), 'utf8');\n    const parsed = parseCollection(content, { format: 'yml' });\n    return { brunoConfig: parsed.brunoConfig, collectionRoot: parsed.collectionRoot || {} };\n  }\n  const brunoConfig = JSON.parse(fs.readFileSync(path.join(collectionPath, 'bruno.json'), 'utf8'));\n  const collectionBruPath = path.join(collectionPath, 'collection.bru');\n  const collectionRoot = fs.existsSync(collectionBruPath)\n    ? parseCollection(fs.readFileSync(collectionBruPath, 'utf8'), { format: 'bru' })\n    : {};\n  return { brunoConfig, collectionRoot };\n};\n\nconst getFolderRoot = (dir, format) => {\n  const folderPath = path.join(dir, FORMAT_CONFIG[format].folderFile);\n  if (!fs.existsSync(folderPath)) return null;\n  return parseFolder(fs.readFileSync(folderPath, 'utf8'), { format });\n};\n\nconst createCollectionJsonFromPathname = (collectionPath) => {\n  const format = getCollectionFormat(collectionPath);\n  if (!format) {\n    console.error(chalk.red(`You can run only at the root of a collection`));\n    process.exit(constants.EXIT_STATUS.ERROR_NOT_IN_COLLECTION);\n  }\n\n  const { brunoConfig, collectionRoot } = getCollectionConfig(collectionPath, format);\n  const { ext, collectionFile, folderFile } = FORMAT_CONFIG[format];\n  const environmentsPath = path.join(collectionPath, 'environments');\n\n  const traverse = (currentPath) => {\n    if (currentPath.includes('node_modules')) return [];\n    const currentDirItems = [];\n\n    for (const file of fs.readdirSync(currentPath)) {\n      const filePath = path.join(currentPath, file);\n      const stats = fs.lstatSync(filePath);\n\n      if (stats.isDirectory()) {\n        if (filePath === environmentsPath || file === '.git' || file === 'node_modules') continue;\n        const folderItem = { name: file, pathname: filePath, type: 'folder', items: traverse(filePath) };\n        const folderRoot = getFolderRoot(filePath, format);\n        if (folderRoot) {\n          folderItem.root = folderRoot;\n          folderItem.seq = folderRoot.meta?.seq;\n        }\n        currentDirItems.push(folderItem);\n      } else {\n        if (file === collectionFile || file === folderFile || path.extname(filePath) !== ext) continue;\n        try {\n          const requestItem = parseRequest(fs.readFileSync(filePath, 'utf8'), { format });\n          currentDirItems.push({ name: file, ...requestItem, pathname: filePath });\n        } catch (err) {\n          console.warn(chalk.yellow(`Warning: Skipping invalid file ${filePath}\\nError: ${err.message}`));\n          global.brunoSkippedFiles = global.brunoSkippedFiles || [];\n          global.brunoSkippedFiles.push({ path: filePath, error: err.message });\n        }\n      }\n    }\n\n    const folders = sortByNameThenSequence(currentDirItems.filter((i) => i.type === 'folder'));\n    const requests = currentDirItems.filter((i) => i.type !== 'folder').sort((a, b) => a.seq - b.seq);\n    return folders.concat(requests);\n  };\n\n  return {\n    brunoConfig,\n    format,\n    root: collectionRoot,\n    pathname: collectionPath,\n    items: traverse(collectionPath)\n  };\n};\n\nconst mergeHeaders = (collection, request, requestTreePath) => {\n  let headers = new Map();\n\n  const collectionRoot = collection?.draft?.root || collection?.root || {};\n  let collectionHeaders = get(collectionRoot, 'request.headers', []);\n  collectionHeaders.forEach((header) => {\n    if (header.enabled) {\n      headers.set(header.name, header.value);\n    }\n  });\n\n  for (let i of requestTreePath) {\n    if (i.type === 'folder') {\n      const folderRoot = i?.draft || i?.root;\n      let _headers = get(folderRoot, 'request.headers', []);\n      _headers.forEach((header) => {\n        if (header.enabled) {\n          headers.set(header.name, header.value);\n        }\n      });\n    } else {\n      const _headers = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'request.headers', []);\n      _headers.forEach((header) => {\n        if (header.enabled) {\n          headers.set(header.name, header.value);\n        }\n      });\n    }\n  }\n\n  request.headers = Array.from(headers, ([name, value]) => ({ name, value, enabled: true }));\n};\n\nconst mergeVars = (collection, request, requestTreePath) => {\n  let reqVars = new Map();\n  const collectionRoot = collection?.draft?.root || collection?.root || {};\n  let collectionRequestVars = get(collectionRoot, 'request.vars.req', []);\n  let collectionVariables = {};\n  collectionRequestVars.forEach((_var) => {\n    if (_var.enabled) {\n      reqVars.set(_var.name, _var.value);\n      collectionVariables[_var.name] = _var.value;\n    }\n  });\n  let folderVariables = {};\n  let requestVariables = {};\n  for (let i of requestTreePath) {\n    if (i.type === 'folder') {\n      const folderRoot = i?.draft || i?.root;\n      let vars = get(folderRoot, 'request.vars.req', []);\n      vars.forEach((_var) => {\n        if (_var.enabled) {\n          reqVars.set(_var.name, _var.value);\n          folderVariables[_var.name] = _var.value;\n        }\n      });\n    } else {\n      const vars = i?.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []);\n      vars.forEach((_var) => {\n        if (_var.enabled) {\n          reqVars.set(_var.name, _var.value);\n          requestVariables[_var.name] = _var.value;\n        }\n      });\n    }\n  }\n\n  request.collectionVariables = collectionVariables;\n  request.folderVariables = folderVariables;\n  request.requestVariables = requestVariables;\n\n  if (request?.vars) {\n    request.vars.req = Array.from(reqVars, ([name, value]) => ({\n      name,\n      value,\n      enabled: true,\n      type: 'request'\n    }));\n  }\n\n  let resVars = new Map();\n  let collectionResponseVars = get(collectionRoot, 'request.vars.res', []);\n  collectionResponseVars.forEach((_var) => {\n    if (_var.enabled) {\n      resVars.set(_var.name, _var.value);\n    }\n  });\n  for (let i of requestTreePath) {\n    if (i.type === 'folder') {\n      const folderRoot = i?.draft || i?.root;\n      let vars = get(folderRoot, 'request.vars.res', []);\n      vars.forEach((_var) => {\n        if (_var.enabled) {\n          resVars.set(_var.name, _var.value);\n        }\n      });\n    } else {\n      const vars = i?.draft ? get(i, 'draft.request.vars.res', []) : get(i, 'request.vars.res', []);\n      vars.forEach((_var) => {\n        if (_var.enabled) {\n          resVars.set(_var.name, _var.value);\n        }\n      });\n    }\n  }\n\n  if (request?.vars) {\n    request.vars.res = Array.from(resVars, ([name, value]) => ({\n      name,\n      value,\n      enabled: true,\n      type: 'response'\n    }));\n  }\n};\n\n/**\n * Wraps a script in an IIFE closure to isolate its scope\n * @param {string} script - The script code to wrap\n * @returns {string} The wrapped script\n */\nconst wrapScriptInClosure = (script) => {\n  if (!script || script.trim() === '') {\n    return '';\n  }\n  // Wrap script in async IIFE to create isolated scope\n  // This prevents variable re-declaration errors and allows early returns\n  // to only affect the current script segment\n  return `await (async () => {\n${script}\n})();`;\n};\n\n/**\n * Wraps each script segment in an async IIFE, joins them with double newlines,\n * and records the line range of the \"request\" segment for stack-trace mapping.\n *\n * Merged scripts = collection + folders + request; the runtime runs one combined\n * script, so we need requestStartLine/requestEndLine to map a VM line number\n * back to the request's script in the .bru file.\n *\n * @param {string[]} scripts - Script segments in order (e.g. collection, folders, request).\n * @param {number} requestIndex - Index in scripts of the request-level segment.\n * @returns {{ code: string, metadata: { requestStartLine: number, requestEndLine: number } | null }}\n */\nconst wrapAndJoinScripts = (scripts, requestIndex, segmentSources = null) => {\n  const wrapped = scripts.map((s) => wrapScriptInClosure(s));\n  const code = wrapped.filter(Boolean).join('\\n\\n');\n\n  let offset = 0;\n  let metadata = null;\n  const segments = [];\n\n  for (let i = 0; i < scripts.length; i++) {\n    if (!wrapped[i]) continue;\n    const lineCount = wrapped[i].split('\\n').length;\n    const startLine = offset + 1;\n    const endLine = offset + lineCount;\n\n    if (i === requestIndex) {\n      metadata = { requestStartLine: startLine, requestEndLine: endLine };\n    }\n\n    if (segmentSources?.[i]) {\n      segments.push({ startLine, endLine, ...segmentSources[i] });\n    }\n\n    offset += lineCount + 1;\n  }\n\n  // Request-level script was empty, but collection/folder scripts produced code.\n  // Use a zero line range to prevent stack traces from mapping to the request file.\n  if (!metadata && code) {\n    metadata = { requestStartLine: 0, requestEndLine: 0 };\n  }\n\n  if (metadata && segments.length > 0) {\n    metadata.segments = segments;\n  }\n\n  return { code, metadata };\n};\n\nconst mergeScripts = (collection, request, requestTreePath, scriptFlow) => {\n  const collectionRoot = collection?.draft?.root || collection?.root || {};\n  let collectionPreReqScript = get(collectionRoot, 'request.script.req', '');\n  let collectionPostResScript = get(collectionRoot, 'request.script.res', '');\n  let collectionTests = get(collectionRoot, 'request.tests', '');\n\n  // Build source file info for error trace mapping\n  const format = collection.format || 'bru';\n  const config = FORMAT_CONFIG[format];\n  const collectionSource = {\n    filePath: path.join(collection.pathname, config.collectionFile),\n    displayPath: config.collectionFile\n  };\n\n  let combinedPreReqScript = [];\n  let combinedPreReqSources = [];\n  let combinedPostResScript = [];\n  let combinedPostResSources = [];\n  let combinedTests = [];\n  let combinedTestsSources = [];\n\n  for (let i of requestTreePath) {\n    if (i.type === 'folder') {\n      const folderRoot = i?.draft || i?.root;\n      const folderSource = {\n        filePath: path.join(i.pathname, config.folderFile),\n        displayPath: path.relative(collection.pathname, path.join(i.pathname, config.folderFile))\n      };\n\n      let preReqScript = get(folderRoot, 'request.script.req', '');\n      if (preReqScript && preReqScript.trim() !== '') {\n        combinedPreReqScript.push(preReqScript);\n        combinedPreReqSources.push(folderSource);\n      }\n\n      let postResScript = get(folderRoot, 'request.script.res', '');\n      if (postResScript && postResScript.trim() !== '') {\n        combinedPostResScript.push(postResScript);\n        combinedPostResSources.push(folderSource);\n      }\n\n      let tests = get(folderRoot, 'request.tests', '');\n      if (tests && tests?.trim?.() !== '') {\n        combinedTests.push(tests);\n        combinedTestsSources.push(folderSource);\n      }\n    }\n  }\n\n  // Wrap each script segment in its own closure and join them\n  // This allows each script to run separately with its own scope,\n  // preventing variable re-declaration errors and allowing early returns\n  // to only affect that specific script segment\n  const preReqScripts = [\n    collectionPreReqScript,\n    ...combinedPreReqScript,\n    request?.script?.req || ''\n  ];\n  const preReqSources = [collectionSource, ...combinedPreReqSources, null];\n  const preReq = wrapAndJoinScripts(preReqScripts, preReqScripts.length - 1, preReqSources);\n  request.script.req = preReq.code;\n  request.script.reqMetadata = preReq.metadata;\n\n  // Handle post-response scripts based on scriptFlow\n  if (scriptFlow === 'sequential') {\n    const postResScripts = [\n      collectionPostResScript,\n      ...combinedPostResScript,\n      request?.script?.res || ''\n    ];\n    const postResSources = [collectionSource, ...combinedPostResSources, null];\n    const postRes = wrapAndJoinScripts(postResScripts, postResScripts.length - 1, postResSources);\n    request.script.res = postRes.code;\n    request.script.resMetadata = postRes.metadata;\n  } else {\n    // Reverse order for non-sequential flow\n    const postResScripts = [\n      request?.script?.res || '',\n      ...[...combinedPostResScript].reverse(),\n      collectionPostResScript\n    ];\n    const postResSources = [null, ...[...combinedPostResSources].reverse(), collectionSource];\n    const postRes = wrapAndJoinScripts(postResScripts, 0, postResSources);\n    request.script.res = postRes.code;\n    request.script.resMetadata = postRes.metadata;\n  }\n\n  // Handle tests based on scriptFlow\n  if (scriptFlow === 'sequential') {\n    const testScripts = [\n      collectionTests,\n      ...combinedTests,\n      request?.tests || ''\n    ];\n    const testSources = [collectionSource, ...combinedTestsSources, null];\n    const tests = wrapAndJoinScripts(testScripts, testScripts.length - 1, testSources);\n    request.tests = tests.code;\n    request.testsMetadata = tests.metadata;\n  } else {\n    // Reverse order for non-sequential flow\n    const testScripts = [\n      request?.tests || '',\n      ...[...combinedTests].reverse(),\n      collectionTests\n    ];\n    const testSources = [null, ...[...combinedTestsSources].reverse(), collectionSource];\n    const tests = wrapAndJoinScripts(testScripts, 0, testSources);\n    request.tests = tests.code;\n    request.testsMetadata = tests.metadata;\n  }\n};\n\nconst findItem = (items = [], pathname) => {\n  return find(items, (i) => i.pathname === pathname);\n};\n\nconst findItemInCollection = (collection, pathname) => {\n  let flattenedItems = flattenItems(collection.items);\n\n  return findItem(flattenedItems, pathname);\n};\n\nconst findParentItemInCollection = (collection, pathname) => {\n  let flattenedItems = flattenItems(collection.items);\n\n  return find(flattenedItems, (item) => {\n    return item.items && find(item.items, (i) => i.pathname === pathname);\n  });\n};\n\nconst flattenItems = (items = []) => {\n  const flattenedItems = [];\n\n  const flatten = (itms, flattened) => {\n    each(itms, (i) => {\n      flattened.push(i);\n\n      if (i.items && i.items.length) {\n        flatten(i.items, flattened);\n      }\n    });\n  };\n\n  flatten(items, flattenedItems);\n\n  return flattenedItems;\n};\n\nconst getTreePathFromCollectionToItem = (collection, _item) => {\n  let path = [];\n  let item = findItemInCollection(collection, _item.pathname);\n  while (item) {\n    path.unshift(item);\n    item = findParentItemInCollection(collection, item.pathname);\n  }\n  return path;\n};\n\nconst mergeAuth = (collection, request, requestTreePath) => {\n  const collectionRoot = collection?.draft?.root || collection?.root || {};\n  let collectionAuth = collectionRoot?.request?.auth || { mode: 'none' };\n  let effectiveAuth = collectionAuth;\n\n  for (let i of requestTreePath) {\n    if (i.type === 'folder') {\n      const folderRoot = i?.draft || i?.root;\n      const folderAuth = get(folderRoot, 'request.auth');\n      if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {\n        effectiveAuth = folderAuth;\n      }\n    }\n  }\n\n  if (request.auth && request.auth.mode === 'inherit') {\n    request.auth = effectiveAuth;\n  }\n};\n\nconst getAllRequestsInFolder = (folderItems = [], recursive = true) => {\n  let requests = [];\n\n  if (folderItems && folderItems.length) {\n    folderItems.forEach((item) => {\n      if (item.type !== 'folder') {\n        requests.push(item);\n      } else {\n        if (recursive) {\n          requests = requests.concat(getAllRequestsInFolder(item.items, recursive));\n        }\n      }\n    });\n  }\n  return requests;\n};\n\nconst getAllRequestsAtFolderRoot = (folderItems = []) => {\n  return getAllRequestsInFolder(folderItems, false);\n};\n\nconst getCallStack = (resolvedPaths = [], collection, { recursive }) => {\n  let requestItems = [];\n\n  if (!resolvedPaths || !resolvedPaths.length) {\n    return requestItems;\n  }\n\n  for (const resolvedPath of resolvedPaths) {\n    if (!resolvedPath || !resolvedPath.length) {\n      continue;\n    }\n\n    if (resolvedPath === collection.pathname) {\n      requestItems = requestItems.concat(getAllRequestsInFolder(collection.items, recursive));\n      continue;\n    }\n\n    const item = findItemInCollection(collection, resolvedPath);\n    if (!item) {\n      continue;\n    }\n\n    if (item.type === 'folder') {\n      requestItems = requestItems.concat(getAllRequestsInFolder(item.items, recursive));\n    } else {\n      requestItems.push(item);\n    }\n  }\n\n  return requestItems;\n};\n\n/**\n * Safe write file implementation to handle errors\n * @param {string} filePath - Path to write file\n * @param {string} content - Content to write\n */\nconst safeWriteFileSync = (filePath, content) => {\n  try {\n    fs.writeFileSync(filePath, content, { encoding: 'utf8' });\n  } catch (error) {\n    console.error(`Error writing file ${filePath}:`, error);\n  }\n};\n\n/**\n * Creates a Bruno collection directory structure from a Bruno collection object\n *\n * @param {Object} collection - The Bruno collection object\n * @param {string} dirPath - The output directory path\n */\nconst createCollectionFromBrunoObject = async (collection, dirPath, options = {}) => {\n  const { format = 'bru' } = options;\n  // Create brunoConfig for yml format\n  const brunoConfig = {\n    version: '1',\n    name: collection.name,\n    type: 'collection',\n    ignore: ['node_modules', '.git']\n  };\n\n  if (format === 'yml') {\n    brunoConfig.opencollection = '1.0.0';\n  }\n\n  const collectionContent = await stringifyCollection(collection.root || {}, brunoConfig, {\n    format\n  });\n  const collectionRootFilePath = format == 'bru' ? path.join(dirPath, 'collection.bru') : path.join(dirPath, 'opencollection.yml');\n\n  if (format === 'bru') {\n    fs.writeFileSync(\n      path.join(dirPath, 'bruno.json'),\n      JSON.stringify(brunoConfig, null, 2)\n    );\n  }\n\n  if (collection.root) {\n    fs.writeFileSync(collectionRootFilePath, collectionContent);\n  }\n\n  // Process environments\n  if (collection.environments && collection.environments.length) {\n    const envDirPath = path.join(dirPath, 'environments');\n    fs.mkdirSync(envDirPath, { recursive: true });\n\n    for (const env of collection.environments) {\n      const content = stringifyEnvironment(env, { format });\n      const filename = format === 'bru' ? sanitizeName(`${env.name}.bru`) : sanitizeName(`${env.name}.yml`);\n      fs.writeFileSync(path.join(envDirPath, filename), content);\n    }\n  }\n\n  // Process collection items\n  await processCollectionItems(collection.items, dirPath, { format });\n\n  return dirPath;\n};\n\n/**\n * Recursively processes collection items to create files and folders\n *\n * @param {Array} items - Collection items\n * @param {string} currentPath - Current directory path\n * @param {object} [options] - Current directory path\n * @param {\"bru\"|\"yml\"} options.format - Current directory path\n */\nconst processCollectionItems = async (items = [], currentPath, options = {}) => {\n  const { format = 'bru' } = options;\n  for (const item of items) {\n    if (item.type === 'folder') {\n      // Create folder\n      let sanitizedFolderName = sanitizeName(item?.filename || item?.name);\n      const folderPath = path.join(currentPath, sanitizedFolderName);\n      fs.mkdirSync(folderPath, { recursive: true });\n\n      // Create folder.yml file if root exists\n      if (item?.root?.meta?.name) {\n        const folderFileName = format === 'bru' ? 'folder.bru' : 'folder.yml';\n        const folderFilePath = path.join(folderPath, folderFileName);\n        if (item.seq) {\n          item.root.meta.seq = item.seq;\n        }\n        const folderContent = stringifyFolder(item.root, { format });\n        safeWriteFileSync(folderFilePath, folderContent);\n      }\n\n      // Process folder items recursively\n      if (item.items && item.items.length) {\n        await processCollectionItems(item.items, folderPath, options);\n      }\n    } else if (REQUEST_ITEM_TYPES.includes(item.type)) {\n      // Create request file\n      let sanitizedFilename;\n      if (format == 'yml') {\n        sanitizedFilename = sanitizeName(item?.filename || `${item.name}.yml`);\n        if (!sanitizedFilename.endsWith('.yml')) {\n          sanitizedFilename += '.yml';\n        }\n      } else {\n        sanitizedFilename = sanitizeName(item?.filename || `${item.name}.bru`);\n        if (!sanitizedFilename.endsWith('.bru')) {\n          sanitizedFilename += '.bru';\n        }\n      }\n\n      // Convert to YML format\n      const itemJson = {\n        type: item.type,\n        name: item.name,\n        seq: typeof item.seq === 'number' ? item.seq : 1,\n        tags: item.tags || [],\n        settings: {},\n        request: {\n          method: item.request?.method || 'GET',\n          url: item.request?.url || '',\n          headers: item.request?.headers || [],\n          params: item.request?.params || [],\n          auth: item.request?.auth || {},\n          body: item.request?.body || {},\n          script: item.request?.script || {},\n          vars: item.request?.vars || { req: [], res: [] },\n          assertions: item.request?.assertions || [],\n          tests: item.request?.tests || '',\n          docs: item.request?.docs || ''\n        }\n      };\n\n      // Convert to YML format and write to file\n      const content = stringifyRequest(itemJson, { format });\n      safeWriteFileSync(path.join(currentPath, sanitizedFilename), content);\n    } else {\n      throw new Error(`Unsupported item type: ${item.type}`);\n    }\n  }\n};\n\nconst sortByNameThenSequence = (items) => {\n  const isSeqValid = (seq) => Number.isFinite(seq) && Number.isInteger(seq) && seq > 0;\n\n  // Sort folders alphabetically by name\n  const alphabeticallySorted = [...items].sort((a, b) => a.name && b.name && a.name.localeCompare(b.name));\n\n  // Extract folders without 'seq'\n  const withoutSeq = alphabeticallySorted.filter((f) => !isSeqValid(f['seq']));\n\n  // Extract folders with 'seq' and sort them by 'seq'\n  const withSeq = alphabeticallySorted.filter((f) => isSeqValid(f['seq'])).sort((a, b) => a.seq - b.seq);\n\n  const sortedItems = withoutSeq;\n\n  // Insert folders with 'seq' at their specified positions\n  withSeq.forEach((item) => {\n    const position = item.seq - 1;\n    const existingItem = withoutSeq[position];\n\n    // Check if there's already an item with the same sequence number\n    const hasItemWithSameSeq = Array.isArray(existingItem)\n      ? existingItem?.[0]?.seq === item.seq\n      : existingItem?.seq === item.seq;\n\n    if (hasItemWithSameSeq) {\n      // If there's a conflict, group items with same sequence together\n      const newGroup = Array.isArray(existingItem)\n        ? [...existingItem, item]\n        : [existingItem, item];\n\n      withoutSeq.splice(position, 1, newGroup);\n    } else {\n      // Insert item at the specified position\n      withoutSeq.splice(position, 0, item);\n    }\n  });\n\n  // return flattened sortedItems\n  return sortedItems.flat();\n};\n\nmodule.exports = {\n  FORMAT_CONFIG,\n  getCollectionFormat,\n  createCollectionJsonFromPathname,\n  mergeHeaders,\n  mergeVars,\n  mergeScripts,\n  findItemInCollection,\n  getTreePathFromCollectionToItem,\n  createCollectionFromBrunoObject,\n  mergeAuth,\n  getAllRequestsInFolder,\n  getAllRequestsAtFolderRoot,\n  getCallStack\n};\n"
  },
  {
    "path": "packages/bruno-cli/src/utils/common.js",
    "content": "const iconv = require('iconv-lite');\n\nconst lpad = (str, width) => {\n  let paddedStr = str;\n  while (paddedStr.length < width) {\n    paddedStr = ' ' + paddedStr;\n  }\n  return paddedStr;\n};\n\nconst rpad = (str, width) => {\n  let paddedStr = str;\n  while (paddedStr.length < width) {\n    paddedStr = paddedStr + ' ';\n  }\n  return paddedStr;\n};\n\nconst parseDataFromResponse = (response, disableParsingResponseJson = false) => {\n  // Parse the charset from content type: https://stackoverflow.com/a/33192813\n  const charsetMatch = /charset=([^()<>@,;:\"/[\\]?.=\\s]*)/i.exec(response.headers['content-type'] || '');\n  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#using_exec_with_regexp_literals\n  const charsetValue = charsetMatch?.[1];\n  const dataBuffer = Buffer.from(response.data);\n  // Overwrite the original data for backwards compatibility\n  let data;\n  if (iconv.encodingExists(charsetValue)) {\n    data = iconv.decode(dataBuffer, charsetValue);\n  } else {\n    data = iconv.decode(dataBuffer, 'utf-8');\n  }\n  // Try to parse response to JSON, this can quietly fail\n  try {\n    // Filter out ZWNBSP character\n    // https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d\n    data = data.replace(/^\\uFEFF/, '');\n    if (!disableParsingResponseJson) {\n      data = JSON.parse(data);\n    }\n  } catch { }\n\n  return { data, dataBuffer };\n};\n\nmodule.exports = {\n  lpad,\n  rpad,\n  parseDataFromResponse\n};\n"
  },
  {
    "path": "packages/bruno-cli/src/utils/cookies.js",
    "content": "module.exports = require('@usebruno/requests').cookies;\n"
  },
  {
    "path": "packages/bruno-cli/src/utils/environment.js",
    "content": "/**\n * Parse a Bruno JSON environment object and normalize variables\n * Accepts only single environment object: { name?, uid?, variables: [...] }\n */\nconst parseEnvironmentJson = (parsed = {}) => {\n  if (!parsed || !Array.isArray(parsed.variables)) {\n    throw new Error('Invalid environment JSON: expected a single environment object with a \"variables\" array');\n  }\n\n  const normalized = {\n    name: parsed.name,\n    variables: (parsed.variables || []).filter(Boolean).map((variable) => ({\n      name: variable.name,\n      value: variable.value,\n      type: variable.type || 'text',\n      enabled: variable.enabled !== false,\n      secret: variable.secret || false\n    }))\n  };\n\n  return normalized;\n};\n\nmodule.exports = {\n  parseEnvironmentJson\n};\n"
  },
  {
    "path": "packages/bruno-cli/src/utils/filesystem.js",
    "content": "const path = require('path');\nconst fs = require('fs-extra');\nconst fsPromises = require('fs/promises');\n\nconst exists = async (p) => {\n  try {\n    await fsPromises.access(p);\n    return true;\n  } catch (_) {\n    return false;\n  }\n};\n\nconst isSymbolicLink = (filepath) => {\n  try {\n    return fs.existsSync(filepath) && fs.lstatSync(filepath).isSymbolicLink();\n  } catch (_) {\n    return false;\n  }\n};\n\nconst isFile = (filepath) => {\n  try {\n    return fs.existsSync(filepath) && fs.lstatSync(filepath).isFile();\n  } catch (_) {\n    return false;\n  }\n};\n\nconst isDirectory = (dirPath) => {\n  try {\n    return fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory();\n  } catch (_) {\n    return false;\n  }\n};\n\nconst normalizeAndResolvePath = (pathname) => {\n  if (isSymbolicLink(pathname)) {\n    const absPath = path.dirname(pathname);\n    const targetPath = path.resolve(absPath, fs.readlinkSync(pathname));\n    if (isFile(targetPath) || isDirectory(targetPath)) {\n      return path.resolve(targetPath);\n    }\n    console.error(`Cannot resolve link target \"${pathname}\" (${targetPath}).`);\n    return '';\n  }\n  return path.resolve(pathname);\n};\n\nconst writeFile = async (pathname, content) => {\n  try {\n    fs.writeFileSync(pathname, content, {\n      encoding: 'utf8'\n    });\n  } catch (err) {\n    return Promise.reject(err);\n  }\n};\n\nconst hasJsonExtension = (filename) => {\n  if (!filename || typeof filename !== 'string') return false;\n  return ['json'].some((ext) => filename.toLowerCase().endsWith(`.${ext}`));\n};\n\nconst hasBruExtension = (filename) => {\n  if (!filename || typeof filename !== 'string') return false;\n  return ['bru'].some((ext) => filename.toLowerCase().endsWith(`.${ext}`));\n};\n\nconst createDirectory = async (dir) => {\n  if (!dir) {\n    throw new Error(`directory: path is null`);\n  }\n\n  if (fs.existsSync(dir)) {\n    throw new Error(`directory: ${dir} already exists`);\n  }\n\n  return fs.mkdirSync(dir);\n};\n\nconst searchForFiles = (dir, extension) => {\n  let results = [];\n  const files = fs.readdirSync(dir);\n  for (const file of files) {\n    const filePath = path.join(dir, file);\n    const stat = fs.statSync(filePath);\n    if (stat.isDirectory()) {\n      results = results.concat(searchForFiles(filePath, extension));\n    } else if (path.extname(file) === extension) {\n      results.push(filePath);\n    }\n  }\n  return results;\n};\n\nconst searchForBruFiles = (dir) => {\n  return searchForFiles(dir, '.bru');\n};\n\nconst stripExtension = (filename = '') => {\n  return filename.replace(/\\.[^/.]+$/, '');\n};\n\nconst getSubDirectories = (dir) => {\n  try {\n    const files = fs.readdirSync(dir);\n    const subDirectories = files\n      .filter((file) => {\n        return fs.lstatSync(path.join(dir, file)).isDirectory();\n      })\n      .sort();\n\n    return subDirectories;\n  } catch (err) {\n    return [];\n  }\n};\n\n/**\n * Sanitizes a filename to make it safe for filesystem operations\n *\n * @param {string} name - The name to sanitize\n * @returns {string} - The sanitized name\n */\nconst sanitizeName = (name) => {\n  if (!name) return '';\n\n  const invalidCharacters = /[<>:\"/\\\\|?*\\x00-\\x1F]/g;\n  return name\n    .replace(invalidCharacters, '-') // replace invalid characters with hyphens\n    .replace(/^[.\\s-]+/, '') // remove leading dots, hyphens and spaces\n    .replace(/[.\\s]+$/, ''); // remove trailing dots and spaces (keep trailing hyphens)\n};\n\n/**\n * Validates if a name is valid for the filesystem\n *\n * @param {string} name - The name to validate\n * @returns {boolean} - True if the name is valid, false otherwise\n */\nconst validateName = (name) => {\n  if (!name) return false;\n\n  const reservedDeviceNames = /^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])$/i;\n  const firstCharacter = /^[^.\\s\\-\\<>:\"/\\\\|?*\\x00-\\x1F]/; // no dot, space, or hyphen at start\n  const middleCharacters = /^[^<>:\"/\\\\|?*\\x00-\\x1F]*$/; // no invalid characters\n  const lastCharacter = /[^.\\s]$/; // no dot or space at end, hyphen allowed\n\n  if (name.length > 255) return false; // max name length\n  if (reservedDeviceNames.test(name)) return false; // windows reserved names\n\n  return (\n    firstCharacter.test(name)\n    && middleCharacters.test(name)\n    && lastCharacter.test(name)\n  );\n};\n\n/**\n * Checks if a file is larger than a given threshold.\n * @param {string} filePath - The path to the file.\n * @param {number} threshold - The threshold in bytes. Default is 10MB.\n * @returns {boolean} True if the file is larger than the threshold, false otherwise.\n */\nconst isLargeFile = (filePath, threshold = 10 * 1024 * 1024) => {\n  if (!isFile(filePath)) {\n    throw new Error(`File ${filePath} is not a file`);\n  }\n\n  const size = fs.statSync(filePath).size;\n\n  return size > threshold;\n};\n\nmodule.exports = {\n  exists,\n  isSymbolicLink,\n  isFile,\n  isDirectory,\n  normalizeAndResolvePath,\n  writeFile,\n  hasJsonExtension,\n  hasBruExtension,\n  createDirectory,\n  searchForFiles,\n  searchForBruFiles,\n  stripExtension,\n  getSubDirectories,\n  sanitizeName,\n  validateName,\n  isLargeFile\n};\n"
  },
  {
    "path": "packages/bruno-cli/src/utils/form-data.js",
    "content": "const { forEach } = require('lodash');\nconst FormData = require('form-data');\nconst fs = require('fs');\nconst path = require('path');\n\nconst createFormData = (data, collectionPath) => {\n  // make axios work in node using form data\n  // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427\n  const form = new FormData();\n  forEach(data, (datum) => {\n    const { name, type, value, contentType } = datum;\n    let options = {};\n    if (contentType) {\n      options.contentType = contentType;\n    }\n    if (type === 'text') {\n      if (Array.isArray(value)) {\n        value.forEach((val) => form.append(name, val, options));\n      } else {\n        form.append(name, value, options);\n      }\n      return;\n    }\n\n    if (type === 'file') {\n      const filePaths = value || [];\n      filePaths.forEach((filePath) => {\n        let trimmedFilePath = filePath.trim();\n        if (!path.isAbsolute(trimmedFilePath)) {\n          trimmedFilePath = path.join(collectionPath, trimmedFilePath);\n        }\n        options.filename = path.basename(trimmedFilePath);\n        form.append(name, fs.createReadStream(trimmedFilePath), options);\n      });\n    }\n  });\n  return form;\n};\n\nmodule.exports = {\n  createFormData\n};\n"
  },
  {
    "path": "packages/bruno-cli/src/utils/oauth2.js",
    "content": "const { getOAuth2Token: _getOAuth2Token } = require('@usebruno/requests');\nconst tokenStore = require('../store/tokenStore');\nconst { getOptions } = require('./bru');\n\n/**\n * Formats OAuth2 credentials into variables that can be accessed via bru.getOauth2CredentialVar()\n * @returns {Object} Formatted OAuth2 credential variables\n */\nconst getFormattedOauth2Credentials = () => {\n  const oauth2Credentials = tokenStore.getAllCredentials();\n  let credentialsVariables = {};\n\n  oauth2Credentials.forEach(({ credentialsId, credentials }) => {\n    if (credentials) {\n      Object.entries(credentials).forEach(([key, value]) => {\n        credentialsVariables[`$oauth2.${credentialsId}.${key}`] = value;\n      });\n    }\n  });\n\n  return credentialsVariables;\n};\n\nconst getOAuth2Token = (oauth2Config, axiosInstance) => {\n  let options = getOptions();\n  let verbose = options?.verbose;\n  return _getOAuth2Token(oauth2Config, tokenStore, verbose, axiosInstance);\n};\n\nmodule.exports = {\n  getFormattedOauth2Credentials,\n  getOAuth2Token\n};\n"
  },
  {
    "path": "packages/bruno-cli/src/utils/proxy-util.js",
    "content": "const parseUrl = require('url').parse;\nconst { isEmpty } = require('lodash');\nconst { HttpsProxyAgent } = require('https-proxy-agent');\n\nconst DEFAULT_PORTS = {\n  ftp: 21,\n  gopher: 70,\n  http: 80,\n  https: 443,\n  ws: 80,\n  wss: 443\n};\n/**\n * check for proxy bypass, copied form 'proxy-from-env'\n */\nconst shouldUseProxy = (url, proxyBypass) => {\n  if (proxyBypass === '*') {\n    return false; // Never proxy if wildcard is set.\n  }\n\n  // use proxy if no proxyBypass is set\n  if (!proxyBypass || typeof proxyBypass !== 'string' || isEmpty(proxyBypass.trim())) {\n    return true;\n  }\n\n  const parsedUrl = typeof url === 'string' ? parseUrl(url) : url || {};\n  let proto = parsedUrl.protocol;\n  let hostname = parsedUrl.host;\n  let port = parsedUrl.port;\n  if (typeof hostname !== 'string' || !hostname || typeof proto !== 'string') {\n    return false; // Don't proxy URLs without a valid scheme or host.\n  }\n\n  proto = proto.split(':', 1)[0];\n  // Stripping ports in this way instead of using parsedUrl.hostname to make\n  // sure that the brackets around IPv6 addresses are kept.\n  hostname = hostname.replace(/:\\d*$/, '');\n  port = parseInt(port) || DEFAULT_PORTS[proto] || 0;\n\n  return proxyBypass.split(/[,;\\s]/).every(function (dontProxyFor) {\n    if (!dontProxyFor) {\n      return true; // Skip zero-length hosts.\n    }\n    const parsedProxy = dontProxyFor.match(/^(.+):(\\d+)$/);\n    let parsedProxyHostname = parsedProxy ? parsedProxy[1] : dontProxyFor;\n    const parsedProxyPort = parsedProxy ? parseInt(parsedProxy[2]) : 0;\n    if (parsedProxyPort && parsedProxyPort !== port) {\n      return true; // Skip if ports don't match.\n    }\n\n    if (!/^[.*]/.test(parsedProxyHostname)) {\n      // No wildcards, so stop proxying if there is an exact match.\n      return hostname !== parsedProxyHostname;\n    }\n\n    if (parsedProxyHostname.charAt(0) === '*') {\n      // Remove leading wildcard.\n      parsedProxyHostname = parsedProxyHostname.slice(1);\n    }\n    // Stop proxying if the hostname ends with the no_proxy host.\n    return !hostname.endsWith(parsedProxyHostname);\n  });\n};\n\n/**\n * Options that should be forwarded from the constructor to the target TLS upgrade.\n */\nconst TARGET_TLS_OPTIONS = ['cert', 'key', 'pfx', 'passphrase', 'rejectUnauthorized', 'secureContext'];\n\n/**\n * Patched version of HttpsProxyAgent that correctly handles TLS options for\n * both the proxy connection and the target server connection.\n *\n * The upstream HttpsProxyAgent (https://github.com/TooTallNate/proxy-agents/issues/194)\n * ignores constructor options when upgrading the tunneled socket to TLS for the\n * target server. This patch forwards the relevant TLS options to the target upgrade.\n */\nclass PatchedHttpsProxyAgent extends HttpsProxyAgent {\n  constructor(proxy, opts) {\n    super(proxy, opts);\n    this.constructorOpts = opts;\n  }\n\n  async connect(req, opts) {\n    const targetOpts = { ...opts };\n\n    if (this.constructorOpts) {\n      for (const key of TARGET_TLS_OPTIONS) {\n        if (key in this.constructorOpts) {\n          targetOpts[key] = this.constructorOpts[key];\n        }\n      }\n    }\n\n    return super.connect(req, targetOpts);\n  }\n}\n\nmodule.exports = {\n  shouldUseProxy,\n  PatchedHttpsProxyAgent\n};\n"
  },
  {
    "path": "packages/bruno-cli/src/utils/request.js",
    "content": "// Check for meaningful test() calls (not commented out or in strings)\nconst hasExecutableTestInScript = (script) => {\n  if (!script) return false;\n\n  // Remove single-line comments (// ...) and multi-line comments (/* ... */)\n  let cleanScript = script\n    .replace(/\\/\\/.*$/gm, '') // Remove line comments\n    .replace(/\\/\\*[\\s\\S]*?\\*\\//g, ''); // Remove block comments\n\n  // Remove string literals to avoid matching test() inside strings\n  cleanScript = cleanScript\n    .replace(/\"(?:[^\"\\\\]|\\\\.)*\"/g, '\"\"') // Remove double-quoted strings\n    .replace(/'(?:[^'\\\\]|\\\\.)*'/g, '\\'\\'') // Remove single-quoted strings\n    .replace(/`(?:[^`\\\\]|\\\\.)*`/g, '``'); // Remove template literals\n\n  // Look for standalone test() calls (not object method calls like obj.test())\n  // Find all test( occurrences and check they're not preceded by dots\n  let hasValidTest = false;\n  let searchFrom = 0;\n\n  while (true) {\n    const index = cleanScript.indexOf('test', searchFrom);\n    if (index === -1) break;\n\n    // Check if this looks like test( with optional whitespace\n    const afterTest = cleanScript.substring(index + 4);\n    if (/^\\s*\\(/.test(afterTest)) {\n      // Found test( - check if it's not preceded by a dot\n      if (index === 0 || cleanScript[index - 1] !== '.') {\n        hasValidTest = true;\n        break;\n      }\n    }\n\n    searchFrom = index + 1;\n  }\n\n  return hasValidTest;\n};\n\nmodule.exports = {\n  hasExecutableTestInScript\n};\n"
  },
  {
    "path": "packages/bruno-cli/src/utils/run.js",
    "content": "const path = require('path');\nconst { stripExtension } = require('./filesystem');\n\nconst createSkippedFileResults = (skippedFiles, collectionPath) => {\n  return skippedFiles.map((skippedFile) => {\n    const relativePath = path.relative(collectionPath, skippedFile.path);\n    return {\n      test: {\n        filename: relativePath\n      },\n      request: {\n        method: null,\n        url: null,\n        headers: null,\n        data: null\n      },\n      response: {\n        status: 'skipped',\n        statusText: skippedFile.error,\n        data: null,\n        responseTime: 0\n      },\n      error: skippedFile.error,\n      status: 'skipped',\n      skipped: true,\n      assertionResults: [],\n      testResults: [],\n      preRequestTestResults: [],\n      postResponseTestResults: [],\n      runDuration: 0,\n      suitename: stripExtension(relativePath),\n      name: path.basename(skippedFile.path),\n      path: relativePath\n    };\n  });\n};\n\nmodule.exports = {\n  createSkippedFileResults\n};\n"
  },
  {
    "path": "packages/bruno-cli/src/utils/sanitize-results.js",
    "content": "const deleteHeaderIfExists = (headers, header) => {\n  Object.keys(headers).forEach((key) => {\n    if (key.toLowerCase() === header.toLowerCase()) {\n      delete headers[key];\n    }\n  });\n};\n\nconst sanitizeResultsForReporter = (results, { skipAllHeaders = false, skipHeaders = [], skipRequestBody = false, skipResponseBody = false } = {}) => {\n  if (skipAllHeaders) {\n    results.forEach((result) => {\n      result.request.headers = {};\n      result.response.headers = {};\n    });\n  }\n\n  if (skipHeaders?.length) {\n    results.forEach((result) => {\n      if (result.request?.headers) {\n        skipHeaders.forEach((header) => {\n          deleteHeaderIfExists(result.request.headers, header);\n        });\n      }\n      if (result.response?.headers) {\n        skipHeaders.forEach((header) => {\n          deleteHeaderIfExists(result.response.headers, header);\n        });\n      }\n    });\n  }\n\n  if (skipRequestBody) {\n    results.forEach((result) => {\n      delete result.request?.data;\n    });\n  }\n\n  if (skipResponseBody) {\n    results.forEach((result) => {\n      delete result.response?.data;\n    });\n  }\n};\n\nmodule.exports = { sanitizeResultsForReporter };\n"
  },
  {
    "path": "packages/bruno-cli/tests/reporters/html.spec.js",
    "content": "const { describe, it, expect } = require('@jest/globals');\nconst fs = require('fs');\n\nconst mockGenerateHtmlReport = jest.fn(() => '<html>Mock HTML</html>');\n\njest.mock('@usebruno/common/runner', () => ({\n  generateHtmlReport: mockGenerateHtmlReport\n}));\n\nconst makeHtmlOutput = require('../../src/reporters/html');\n\ndescribe('makeHtmlOutput', () => {\n  let writeFileSyncSpy;\n\n  beforeEach(() => {\n    mockGenerateHtmlReport.mockClear();\n    writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});\n  });\n\n  afterEach(() => {\n    jest.restoreAllMocks();\n  });\n\n  it('should pass environment parameter to generateHtmlReport when provided', async () => {\n    const mockResults = {\n      results: [],\n      summary: {\n        totalRequests: 0,\n        passedRequests: 0,\n        failedRequests: 0,\n        errorRequests: 0,\n        skippedRequests: 0,\n        totalAssertions: 0,\n        passedAssertions: 0,\n        failedAssertions: 0,\n        totalTests: 0,\n        passedTests: 0,\n        failedTests: 0\n      }\n    };\n\n    await makeHtmlOutput(mockResults, '/tmp/test.html', '2024-01-15T14:30:45.123Z', 'production');\n\n    expect(mockGenerateHtmlReport).toHaveBeenCalledWith(expect.objectContaining({\n      environment: 'production'\n    }));\n  });\n\n  it('should pass null environment when not provided', async () => {\n    const mockResults = {\n      results: [],\n      summary: {\n        totalRequests: 0,\n        passedRequests: 0,\n        failedRequests: 0,\n        errorRequests: 0,\n        skippedRequests: 0,\n        totalAssertions: 0,\n        passedAssertions: 0,\n        failedAssertions: 0,\n        totalTests: 0,\n        passedTests: 0,\n        failedTests: 0\n      }\n    };\n\n    await makeHtmlOutput(mockResults, '/tmp/test.html', '2024-01-15T14:30:45.123Z');\n\n    expect(mockGenerateHtmlReport).toHaveBeenCalledWith(expect.objectContaining({\n      environment: null\n    }));\n  });\n});\n"
  },
  {
    "path": "packages/bruno-cli/tests/reporters/junit.spec.js",
    "content": "const { describe, it, expect } = require('@jest/globals');\nconst xmlbuilder = require('xmlbuilder');\nconst fs = require('fs');\n\nconst makeJUnitOutput = require('../../src/reporters/junit');\n\ndescribe('makeJUnitOutput', () => {\n  let createStub = jest.fn();\n\n  beforeEach(() => {\n    jest.spyOn(xmlbuilder, 'create').mockImplementation(() => {\n      return { end: createStub };\n    });\n    jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});\n  });\n\n  afterEach(() => {\n    jest.restoreAllMocks();\n  });\n\n  it('should produce a junit spec object for serialization', () => {\n    const results = [\n      {\n        description: 'description provided',\n        name: 'Tests/Suite A',\n        test: {\n          filename: 'Tests/Suite A.bru'\n        },\n        request: {\n          method: 'GET',\n          url: 'https://ima.test'\n        },\n        assertionResults: [\n          {\n            lhsExpr: 'res.status',\n            rhsExpr: 'eq 200',\n            status: 'pass'\n          },\n          {\n            lhsExpr: 'res.status',\n            rhsExpr: 'neq 200',\n            status: 'fail',\n            error: 'expected 200 to not equal 200'\n          }\n        ],\n        runDuration: 1.2345678\n      },\n      {\n        request: {\n          method: 'GET',\n          url: 'https://imanother.test'\n        },\n        name: 'Tests/Suite B',\n        test: {\n          filename: 'Tests/Suite B.bru'\n        },\n        testResults: [\n          {\n            lhsExpr: 'res.status',\n            rhsExpr: 'eq 200',\n            description: 'A test that passes',\n            status: 'pass'\n          },\n          {\n            description: 'A test that fails',\n            status: 'fail',\n            error: 'expected 200 to not equal 200',\n            status: 'fail'\n          }\n        ],\n        runDuration: 2.3456789\n      }\n    ];\n\n    makeJUnitOutput(results, '/tmp/testfile.xml');\n    expect(createStub).toBeCalled;\n\n    const junit = xmlbuilder.create.mock.calls[0][0];\n\n    expect(junit.testsuites).toBeDefined;\n    expect(junit.testsuites.testsuite.length).toBe(2);\n    expect(junit.testsuites.testsuite[0].testcase.length).toBe(2);\n    expect(junit.testsuites.testsuite[1].testcase.length).toBe(2);\n\n    expect(junit.testsuites.testsuite[0]['@name']).toBe('Tests/Suite A');\n    expect(junit.testsuites.testsuite[1]['@name']).toBe('Tests/Suite B');\n\n    expect(junit.testsuites.testsuite[0]['@file']).toBe('Tests/Suite A.bru');\n    expect(junit.testsuites.testsuite[1]['@file']).toBe('Tests/Suite B.bru');\n\n    expect(junit.testsuites.testsuite[0]['@tests']).toBe(2);\n    expect(junit.testsuites.testsuite[1]['@tests']).toBe(2);\n\n    const testcase = junit.testsuites.testsuite[0].testcase[0];\n\n    expect(testcase['@name']).toBe('res.status eq 200');\n    expect(testcase['@status']).toBe('pass');\n\n    const failcase = junit.testsuites.testsuite[0].testcase[1];\n\n    expect(failcase['@name']).toBe('res.status neq 200');\n    expect(failcase.failure).toBeDefined;\n    expect(failcase.failure[0]['@type']).toBe('failure');\n  });\n\n  it('should handle request errors', () => {\n    const results = [\n      {\n        description: 'description provided',\n        name: 'Tests/Suite A',\n        test: {\n          filename: 'Tests/Suite A.bru'\n        },\n        request: {\n          method: 'GET',\n          url: 'https://ima.test'\n        },\n        assertionResults: [\n          {\n            lhsExpr: 'res.status',\n            rhsExpr: 'eq 200',\n            status: 'fail'\n          }\n        ],\n        runDuration: 1.2345678,\n        error: 'timeout of 2000ms exceeded'\n      }\n    ];\n\n    makeJUnitOutput(results, '/tmp/testfile.xml');\n\n    const junit = xmlbuilder.create.mock.calls[0][0];\n\n    expect(createStub).toBeCalled;\n\n    expect(junit.testsuites).toBeDefined;\n    expect(junit.testsuites.testsuite.length).toBe(1);\n    expect(junit.testsuites.testsuite[0].testcase.length).toBe(1);\n    expect(junit.testsuites.testsuite[0]['@file']).toBe('Tests/Suite A.bru');\n\n    const failcase = junit.testsuites.testsuite[0].testcase[0];\n\n    expect(failcase['@name']).toBe('Test suite has no errors');\n    expect(failcase.error).toBeDefined;\n    expect(failcase.error[0]['@type']).toBe('error');\n    expect(failcase.error[0]['@message']).toBe('timeout of 2000ms exceeded');\n  });\n\n  it('should include preRequestTestResults and postResponseTestResults in the junit output', () => {\n    const results = [\n      {\n        name: 'Tests/Suite A',\n        test: {\n          filename: 'Tests/Suite A.bru'\n        },\n        request: {\n          method: 'GET',\n          url: 'https://ima.test'\n        },\n        preRequestTestResults: [\n          {\n            description: 'A test from Pre Request Script',\n            status: 'pass'\n          }\n        ],\n        testResults: [\n          {\n            description: 'A test from Tests tab',\n            status: 'pass'\n          }\n        ],\n        postResponseTestResults: [\n          {\n            description: 'A test from Post Response Script',\n            status: 'pass'\n          },\n          {\n            description: 'A failing test from Post Response Script',\n            status: 'fail',\n            error: 'expected 200 to equal 404'\n          }\n        ],\n        runDuration: 1.2345678\n      }\n    ];\n\n    makeJUnitOutput(results, '/tmp/testfile.xml');\n    expect(createStub).toBeCalled;\n\n    const junit = xmlbuilder.create.mock.calls[0][0];\n\n    expect(junit.testsuites).toBeDefined;\n    expect(junit.testsuites.testsuite.length).toBe(1);\n    expect(junit.testsuites.testsuite[0].testcase.length).toBe(4);\n    expect(junit.testsuites.testsuite[0]['@file']).toBe('Tests/Suite A.bru');\n    expect(junit.testsuites.testsuite[0]['@tests']).toBe(4);\n\n    const testcase1 = junit.testsuites.testsuite[0].testcase[0];\n    expect(testcase1['@name']).toBe('A test from Pre Request Script');\n    expect(testcase1['@status']).toBe('pass');\n\n    const testcase2 = junit.testsuites.testsuite[0].testcase[1];\n    expect(testcase2['@name']).toBe('A test from Tests tab');\n    expect(testcase2['@status']).toBe('pass');\n\n    const testcase3 = junit.testsuites.testsuite[0].testcase[2];\n    expect(testcase3['@name']).toBe('A test from Post Response Script');\n    expect(testcase3['@status']).toBe('pass');\n\n    const failcase = junit.testsuites.testsuite[0].testcase[3];\n    expect(failcase['@name']).toBe('A failing test from Post Response Script');\n    expect(failcase['@status']).toBe('fail');\n    expect(failcase.failure).toBeDefined;\n    expect(failcase.failure[0]['@type']).toBe('failure');\n    expect(failcase.failure[0]['@message']).toBe('expected 200 to equal 404');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-cli/tests/reporters/skip-body.spec.js",
    "content": "const { describe, it, expect } = require('@jest/globals');\nconst { generateHtmlReport } = require('@usebruno/common/runner');\n\nconst { sanitizeResultsForReporter } = require('../../src/utils/sanitize-results');\n\nconst REQUEST_DATA = { username: 'john', password: 'secret123' };\nconst RESPONSE_DATA = { id: 1, username: 'john', email: 'john@example.com' };\n\nconst createMockResult = () => ({\n  test: { filename: 'echo/echo-post.bru' },\n  request: {\n    method: 'POST',\n    url: 'https://echo.usebruno.com',\n    headers: { 'content-type': 'application/json' },\n    data: { ...REQUEST_DATA }\n  },\n  response: {\n    status: 200,\n    statusText: 'OK',\n    headers: { 'content-type': 'application/json' },\n    data: { ...RESPONSE_DATA },\n    url: 'https://echo.usebruno.com',\n    responseTime: 150\n  },\n  error: null,\n  status: 'pass',\n  assertionResults: [\n    { lhsExpr: 'res.status', rhsExpr: 'eq 200', status: 'pass' }\n  ],\n  testResults: [\n    { description: 'should return user data', status: 'pass' }\n  ],\n  preRequestTestResults: [],\n  postResponseTestResults: [],\n  name: 'echo post',\n  path: 'echo/echo-post.bru',\n  runDuration: 0.150\n});\n\ndescribe('reporter-skip-body', () => {\n  describe('JSON report', () => {\n    it('should exclude both request and response bodies with --reporter-skip-body', () => {\n      const results = [createMockResult()];\n      // --reporter-skip-body sets both skipRequestBody and skipResponseBody to true\n      sanitizeResultsForReporter(results, { skipRequestBody: true, skipResponseBody: true });\n      const json = JSON.parse(JSON.stringify({ summary: {}, results }));\n\n      expect(json.results[0].request).not.toHaveProperty('data');\n      expect(json.results[0].response).not.toHaveProperty('data');\n    });\n  });\n\n  describe('HTML report', () => {\n    const extractEmbeddedData = (htmlString) => {\n      const match = htmlString.match(/JSON\\.parse\\(decodeBase64\\('([^']+)'\\)\\)/);\n      expect(match).not.toBeNull();\n      const binary = atob(match[1]);\n      const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));\n      return JSON.parse(new TextDecoder().decode(bytes));\n    };\n\n    const generateHtml = (results) => generateHtmlReport({\n      runnerResults: [{\n        iterationIndex: 0,\n        results,\n        summary: { totalRequests: 1, passedRequests: 1, failedRequests: 0, errorRequests: 0, skippedRequests: 0, totalAssertions: 1, passedAssertions: 1, failedAssertions: 0, totalTests: 1, passedTests: 1, failedTests: 0 }\n      }],\n      version: 'usebruno v1.16.0',\n      environment: null,\n      runCompletionTime: '2024-01-15T14:30:45.123Z'\n    });\n\n    it('should exclude both bodies from HTML report with --reporter-skip-body', () => {\n      const results = [createMockResult()];\n      sanitizeResultsForReporter(results, { skipRequestBody: true, skipResponseBody: true });\n      const embedded = extractEmbeddedData(generateHtml(results));\n      const result = embedded.results[0].results[0];\n\n      expect(result.request).not.toHaveProperty('data');\n      expect(result.response).not.toHaveProperty('data');\n    });\n  });\n});\n\ndescribe('reporter-skip-request-body and reporter-skip-response-body', () => {\n  // --- JSON Report ---\n  describe('JSON report', () => {\n    it('should include both bodies by default', () => {\n      const results = [createMockResult()];\n      const json = JSON.parse(JSON.stringify({ summary: {}, results }));\n\n      expect(json.results[0].request.data).toEqual(REQUEST_DATA);\n      expect(json.results[0].response.data).toEqual(RESPONSE_DATA);\n    });\n\n    it('should exclude only request body with --reporter-skip-request-body', () => {\n      const results = [createMockResult()];\n      sanitizeResultsForReporter(results, { skipRequestBody: true });\n      const json = JSON.parse(JSON.stringify({ summary: {}, results }));\n\n      expect(json.results[0].request).not.toHaveProperty('data');\n      expect(json.results[0].response.data).toEqual(RESPONSE_DATA);\n    });\n\n    it('should exclude only response body with --reporter-skip-response-body', () => {\n      const results = [createMockResult()];\n      sanitizeResultsForReporter(results, { skipResponseBody: true });\n      const json = JSON.parse(JSON.stringify({ summary: {}, results }));\n\n      expect(json.results[0].request.data).toEqual(REQUEST_DATA);\n      expect(json.results[0].response).not.toHaveProperty('data');\n    });\n\n    it('should exclude both bodies when both flags are used', () => {\n      const results = [createMockResult()];\n      sanitizeResultsForReporter(results, { skipRequestBody: true, skipResponseBody: true });\n      const json = JSON.parse(JSON.stringify({ summary: {}, results }));\n\n      expect(json.results[0].request).not.toHaveProperty('data');\n      expect(json.results[0].response).not.toHaveProperty('data');\n    });\n  });\n\n  // --- HTML Report ---\n  describe('HTML report', () => {\n    const extractEmbeddedData = (htmlString) => {\n      const match = htmlString.match(/JSON\\.parse\\(decodeBase64\\('([^']+)'\\)\\)/);\n      expect(match).not.toBeNull();\n      const binary = atob(match[1]);\n      const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));\n      return JSON.parse(new TextDecoder().decode(bytes));\n    };\n\n    const generateHtml = (results) => generateHtmlReport({\n      runnerResults: [{\n        iterationIndex: 0,\n        results,\n        summary: { totalRequests: 1, passedRequests: 1, failedRequests: 0, errorRequests: 0, skippedRequests: 0, totalAssertions: 1, passedAssertions: 1, failedAssertions: 0, totalTests: 1, passedTests: 1, failedTests: 0 }\n      }],\n      version: 'usebruno v1.16.0',\n      environment: null,\n      runCompletionTime: '2024-01-15T14:30:45.123Z'\n    });\n\n    it('should include both bodies by default', () => {\n      const results = [createMockResult()];\n      const embedded = extractEmbeddedData(generateHtml(results));\n      const result = embedded.results[0].results[0];\n\n      expect(result.request).toHaveProperty('data');\n      expect(result.response).toHaveProperty('data');\n    });\n\n    it('should exclude only request body with --reporter-skip-request-body', () => {\n      const results = [createMockResult()];\n      sanitizeResultsForReporter(results, { skipRequestBody: true });\n      const embedded = extractEmbeddedData(generateHtml(results));\n      const result = embedded.results[0].results[0];\n\n      expect(result.request).not.toHaveProperty('data');\n      expect(result.response).toHaveProperty('data');\n    });\n\n    it('should exclude only response body with --reporter-skip-response-body', () => {\n      const results = [createMockResult()];\n      sanitizeResultsForReporter(results, { skipResponseBody: true });\n      const embedded = extractEmbeddedData(generateHtml(results));\n      const result = embedded.results[0].results[0];\n\n      expect(result.request).toHaveProperty('data');\n      expect(result.response).not.toHaveProperty('data');\n    });\n\n    it('should exclude both bodies when both flags are used', () => {\n      const results = [createMockResult()];\n      sanitizeResultsForReporter(results, { skipRequestBody: true, skipResponseBody: true });\n      const embedded = extractEmbeddedData(generateHtml(results));\n      const result = embedded.results[0].results[0];\n\n      expect(result.request).not.toHaveProperty('data');\n      expect(result.response).not.toHaveProperty('data');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/collection-json-from-pathname.spec.js",
    "content": "const path = require('node:path');\nconst fs = require('node:fs');\nconst { describe, it, expect } = require('@jest/globals');\nconst constants = require('../../src/constants');\nconst { createCollectionJsonFromPathname, getCollectionFormat, FORMAT_CONFIG } = require('../../src/utils/collection');\nconst { parseEnvironment } = require('@usebruno/filestore');\n\ndescribe('create collection json from pathname', () => {\n  it('should throw an error when the pathname is not a valid bruno collection root', () => {\n    const invalidCollectionPathname = path.join(__dirname, './fixtures/collection-invalid');\n    jest.spyOn(console, 'error').mockImplementation(() => { });\n    let mockProcessExit = jest.spyOn(process, 'exit').mockImplementation((code) => { throw new Error(code); });\n    try { createCollectionJsonFromPathname(invalidCollectionPathname); } catch { }\n    expect(mockProcessExit).toHaveBeenCalledWith(constants.EXIT_STATUS.ERROR_NOT_IN_COLLECTION);\n    jest.restoreAllMocks();\n  });\n\n  it('creates a bruno collection json from the collection bru files', () => {\n    const collectionPathname = path.join(__dirname, './fixtures/collection-json-from-pathname/collection');\n    const outputCollectionJson = createCollectionJsonFromPathname(collectionPathname);\n\n    let c = outputCollectionJson;\n    expect(c).toBeDefined();\n\n    /* collection bruno.json */\n    expect(c).toHaveProperty('brunoConfig.version', '1');\n    expect(c).toHaveProperty('brunoConfig.name', 'collection');\n    expect(c).toHaveProperty('brunoConfig.type', 'collection');\n    expect(c).toHaveProperty('brunoConfig.ignore', ['node_modules', '.git']);\n    expect(c).toHaveProperty('brunoConfig.proxy.enabled', false);\n    expect(c).toHaveProperty('brunoConfig.proxy.protocol', 'http');\n    expect(c).toHaveProperty('brunoConfig.proxy.hostname', '<proxy-hostname>');\n    expect(c).toHaveProperty('brunoConfig.proxy.port', 3000);\n    expect(c).toHaveProperty('brunoConfig.proxy.auth.enabled', false);\n    expect(c).toHaveProperty('brunoConfig.proxy.auth.username', '<user-name>');\n    expect(c).toHaveProperty('brunoConfig.proxy.auth.password', '<password>');\n    expect(c).toHaveProperty('brunoConfig.proxy.bypassProxy', '');\n    expect(c).toHaveProperty('brunoConfig.scripts.moduleWhitelist', ['crypto', 'buffer']);\n    expect(c).toHaveProperty('brunoConfig.clientCertificates.enabled', true);\n    expect(c).toHaveProperty('brunoConfig.clientCertificates.certs', []);\n\n    /* collection pathname */\n    expect(c).toHaveProperty('pathname', collectionPathname);\n\n    /* collection root */\n    // headers\n    expect(c).toHaveProperty('root.request.headers[0].name', 'collection_header');\n    expect(c).toHaveProperty('root.request.headers[0].value', 'collection_header_value');\n    expect(c).toHaveProperty('root.request.headers[0].enabled', true);\n    // auth\n    expect(c).toHaveProperty('root.request.auth.mode', 'basic');\n    expect(c).toHaveProperty('root.request.auth.basic.username', 'username');\n    expect(c).toHaveProperty('root.request.auth.basic.password', 'password');\n    // pre-request scripts\n    expect(c).toHaveProperty('root.request.script.req', 'const collectionPreRequestScript = true;');\n    // collection root - post-response scripts\n    expect(c).toHaveProperty('root.request.script.res', 'const collectionPostResponseScript = true;');\n    // pre-request vars\n    expect(c).toHaveProperty('root.request.vars.req[0].name', 'collection_pre_var');\n    expect(c).toHaveProperty('root.request.vars.req[0].value', 'collection_pre_var_value');\n    expect(c).toHaveProperty('root.request.vars.req[0].enabled', true);\n    // post-response vars\n    expect(c).toHaveProperty('root.request.vars.res[0].name', 'collection_post_var');\n    expect(c).toHaveProperty('root.request.vars.res[0].value', 'collection_post_var_value');\n    expect(c).toHaveProperty('root.request.vars.res[0].enabled', true);\n    // tests\n    expect(c).toHaveProperty('root.request.tests', 'test(\\\"collection level script\\\", function() {\\n  expect(\\\"test\\\").to.equal(\\\"test\\\");\\n});');\n\n    /* collection items names and sequences */\n    // <collection-root>/folder_2\n    expect(c).toHaveProperty('items[0].type', 'folder');\n    expect(c).toHaveProperty('items[0].name', 'folder_2');\n    expect(c).toHaveProperty('items[0].seq', 1);\n\n    // <collection-root>/folder_2/request_1\n    expect(c).toHaveProperty('items[0].items[0].name', 'request_1');\n    expect(c).toHaveProperty('items[0].items[0].seq', 1);\n\n    // <collection-root>/folder_2/request_3\n    expect(c).toHaveProperty('items[0].items[1].name', 'request_3');\n    expect(c).toHaveProperty('items[0].items[1].seq', 2);\n\n    // <collection-root>/folder_2/request_2\n    expect(c).toHaveProperty('items[0].items[2].name', 'request_2');\n    expect(c).toHaveProperty('items[0].items[2].seq', 3);\n\n    // <collection-root>/folder_1\n    expect(c).toHaveProperty('items[1].type', 'folder');\n    expect(c).toHaveProperty('items[1].name', 'folder_1');\n    expect(c).toHaveProperty('items[1].seq', 5);\n\n    // <collection-root>/folder_1/folder_2\n    expect(c).toHaveProperty('items[1].items[0].name', 'folder_2');\n    expect(c).toHaveProperty('items[1].items[0].seq', 1);\n\n    // <collection-root>/folder_1/folder_2/request_3\n    expect(c).toHaveProperty('items[1].items[0].items[0].name', 'request_3');\n    expect(c).toHaveProperty('items[1].items[0].items[0].seq', 1);\n\n    // <collection-root>/folder_1/folder_2/request_1\n    expect(c).toHaveProperty('items[1].items[0].items[1].name', 'request_1');\n    expect(c).toHaveProperty('items[1].items[0].items[1].seq', 2);\n\n    // <collection-root>/folder_1/folder_2/request_2\n    expect(c).toHaveProperty('items[1].items[0].items[2].name', 'request_2');\n    expect(c).toHaveProperty('items[1].items[0].items[2].seq', 3);\n\n    // <collection-root>/folder_1/folder_1\n    expect(c).toHaveProperty('items[1].items[1].name', 'folder_1');\n    expect(c).toHaveProperty('items[1].items[1].seq', 2);\n\n    // <collection-root>/folder_1/folder_1/request_3\n    expect(c).toHaveProperty('items[1].items[1].items[0].name', 'request_3');\n    expect(c).toHaveProperty('items[1].items[1].items[0].seq', 1);\n\n    // <collection-root>/folder_1/folder_1/request_2\n    expect(c).toHaveProperty('items[1].items[1].items[1].name', 'request_2');\n    expect(c).toHaveProperty('items[1].items[1].items[1].seq', 2);\n\n    // <collection-root>/folder_1/folder_1/request_1\n    expect(c).toHaveProperty('items[1].items[1].items[2].name', 'request_1');\n    expect(c).toHaveProperty('items[1].items[1].items[2].seq', 3);\n\n    // <collection-root>/folder_1/request_1\n    expect(c).toHaveProperty('items[1].items[2].name', 'request_1');\n    expect(c).toHaveProperty('items[1].items[2].seq', 3);\n\n    // <collection-root>/folder_1/request_3\n    expect(c).toHaveProperty('items[1].items[3].name', 'request_3');\n    expect(c).toHaveProperty('items[1].items[3].seq', 4);\n\n    // <collection-root>/folder_1/request_2\n    expect(c).toHaveProperty('items[1].items[4].name', 'request_2');\n    expect(c).toHaveProperty('items[1].items[4].seq', 5);\n\n    // <collection-root>/request_2\n    expect(c).toHaveProperty('items[2].name', 'request_3');\n    expect(c).toHaveProperty('items[2].seq', 2);\n\n    // <collection-root>/request_3\n    expect(c).toHaveProperty('items[3].name', 'request_1');\n    expect(c).toHaveProperty('items[3].seq', 3);\n\n    // <collection-root>/request_4\n    expect(c).toHaveProperty('items[4].name', 'request_2');\n    expect(c).toHaveProperty('items[4].seq', 4);\n\n    /* collection request item - <collection-root>/request_4 */\n    // <collection-root>/request_4\n    // headers\n    expect(c).toHaveProperty('items[4].request.headers[0].name', 'request_header');\n    expect(c).toHaveProperty('items[4].request.headers[0].value', 'request_header_value');\n    expect(c).toHaveProperty('items[4].request.headers[0].enabled', true);\n    // auth\n    expect(c).toHaveProperty('items[4].request.auth.mode', 'basic');\n    expect(c).toHaveProperty('items[4].request.auth.basic.username', 'username');\n    expect(c).toHaveProperty('items[4].request.auth.basic.password', 'password');\n    // pre-request scripts\n    expect(c).toHaveProperty('items[4].request.script.req', 'const requestPreRequestScript = true;');\n    // request items[4] - post-response scripts\n    expect(c).toHaveProperty('items[4].request.script.res', 'const requestPostResponseScript = true;');\n    // pre-request vars\n    expect(c).toHaveProperty('items[4].request.vars.req[0].name', 'request_pre_var');\n    expect(c).toHaveProperty('items[4].request.vars.req[0].value', 'request_pre_var_value');\n    expect(c).toHaveProperty('items[4].request.vars.req[0].enabled', true);\n    // post-response vars\n    expect(c).toHaveProperty('items[4].request.vars.res[0].name', 'request_post_var');\n    expect(c).toHaveProperty('items[4].request.vars.res[0].value', 'request_post_var_value');\n    expect(c).toHaveProperty('items[4].request.vars.res[0].enabled', true);\n    // tests\n    expect(c).toHaveProperty('items[4].request.tests', 'test(\\\"request level script\\\", function() {\\n  expect(\\\"test\\\").to.equal(\\\"test\\\");\\n});');\n  });\n\n  it('creates a collection json from OpenCollection yml files', () => {\n    const collectionPathname = path.join(__dirname, './fixtures/opencollection/collection');\n    const c = createCollectionJsonFromPathname(collectionPathname);\n\n    expect(c).toBeDefined();\n    expect(c).toHaveProperty('format', 'yml');\n    expect(c).toHaveProperty('brunoConfig.opencollection', '1.0.0');\n    expect(c).toHaveProperty('brunoConfig.name', 'Test OpenCollection');\n    expect(c).toHaveProperty('brunoConfig.type', 'collection');\n    expect(c).toHaveProperty('brunoConfig.ignore', ['node_modules', '.git']);\n    expect(c).toHaveProperty('pathname', collectionPathname);\n\n    // collection root headers\n    expect(c).toHaveProperty('root.request.headers[0].name', 'X-Collection-Header');\n    expect(c).toHaveProperty('root.request.headers[0].value', 'collection-header-value');\n    expect(c).toHaveProperty('root.request.headers[0].enabled', true);\n\n    // folder\n    expect(c.items.some((i) => i.type === 'folder' && i.name === 'users')).toBe(true);\n    const usersFolder = c.items.find((i) => i.name === 'users');\n    expect(usersFolder).toHaveProperty('root.meta.name', 'Users');\n    expect(usersFolder).toHaveProperty('root.meta.seq', 1);\n    expect(usersFolder.pathname).toContain('users');\n\n    // request in folder - name comes from info.name, pathname is correct\n    const createUserReq = usersFolder.items.find((i) => i.name === 'Create User');\n    expect(createUserReq).toBeDefined();\n    expect(createUserReq).toHaveProperty('type', 'http-request');\n    expect(createUserReq).toHaveProperty('request.method', 'POST');\n    expect(createUserReq).toHaveProperty('request.url', 'https://api.example.com/users');\n    expect(createUserReq.pathname).toContain('create-user.yml');\n\n    // root level request - name comes from info.name, pathname is correct\n    const getUsersReq = c.items.find((i) => i.name === 'Get Users');\n    expect(getUsersReq).toBeDefined();\n    expect(getUsersReq).toHaveProperty('type', 'http-request');\n    expect(getUsersReq).toHaveProperty('request.method', 'GET');\n    expect(getUsersReq).toHaveProperty('request.url', 'https://api.example.com/users');\n    expect(getUsersReq.pathname).toContain('get-users.yml');\n  });\n});\n\ndescribe('getCollectionFormat', () => {\n  it('returns yml for OpenCollection', () => {\n    const collectionPath = path.join(__dirname, './fixtures/opencollection/collection');\n    expect(getCollectionFormat(collectionPath)).toBe('yml');\n  });\n\n  it('returns bru for Bruno collection', () => {\n    const collectionPath = path.join(__dirname, './fixtures/collection-json-from-pathname/collection');\n    expect(getCollectionFormat(collectionPath)).toBe('bru');\n  });\n\n  it('returns null for invalid path', () => {\n    const collectionPath = path.join(__dirname, './fixtures/collection-invalid');\n    expect(getCollectionFormat(collectionPath)).toBe(null);\n  });\n});\n\ndescribe('FORMAT_CONFIG', () => {\n  it('has correct config for yml format', () => {\n    expect(FORMAT_CONFIG.yml).toEqual({\n      ext: '.yml',\n      collectionFile: 'opencollection.yml',\n      folderFile: 'folder.yml'\n    });\n  });\n\n  it('has correct config for bru format', () => {\n    expect(FORMAT_CONFIG.bru).toEqual({\n      ext: '.bru',\n      collectionFile: 'collection.bru',\n      folderFile: 'folder.bru'\n    });\n  });\n});\n\ndescribe('OpenCollection environment parsing', () => {\n  it('parses YML environment files correctly', () => {\n    const envPath = path.join(__dirname, './fixtures/opencollection/collection/environments/dev.yml');\n    const envContent = fs.readFileSync(envPath, 'utf8');\n    const env = parseEnvironment(envContent, { format: 'yml' });\n\n    expect(env).toBeDefined();\n    expect(env).toHaveProperty('name', 'Development');\n    expect(env.variables).toHaveLength(2);\n    expect(env.variables[0]).toHaveProperty('name', 'baseUrl');\n    expect(env.variables[0]).toHaveProperty('value', 'https://api.dev.example.com');\n    expect(env.variables[1]).toHaveProperty('name', 'apiKey');\n    expect(env.variables[1]).toHaveProperty('value', 'dev-api-key-123');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"collection\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ],\n  \"proxy\": {\n    \"enabled\": false,\n    \"protocol\": \"http\",\n    \"hostname\": \"<proxy-hostname>\",\n    \"port\": 3000,\n    \"auth\": {\n      \"enabled\": false,\n      \"username\": \"<user-name>\",\n      \"password\": \"<password>\"\n    },\n    \"bypassProxy\": \"\"\n  },\n  \"scripts\": {\n    \"moduleWhitelist\": [\"crypto\", \"buffer\"]\n  },\n  \"clientCertificates\": {\n    \"enabled\": true,\n    \"certs\": []\n  }\n}"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/collection.bru",
    "content": "headers {\n  collection_header: collection_header_value\n}\n\nauth {\n  mode: basic\n}\n\nauth:basic {\n  username: username\n  password: password\n}\n\nvars:pre-request {\n  collection_pre_var: collection_pre_var_value\n}\n\nvars:post-response {\n  collection_post_var: collection_post_var_value\n}\n\nscript:pre-request {\n  const collectionPreRequestScript = true;\n}\n\nscript:post-response {\n  const collectionPostResponseScript = true;\n}\n\ntests {\n  test(\"collection level script\", function() {\n    expect(\"test\").to.equal(\"test\");\n  });\n}\n\ndocs {\n  # docs\n}\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder.bru",
    "content": "meta {\n  name: folder_1\n  seq: 5\n}\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/folder.bru",
    "content": "meta {\n  name: folder_1\n  seq: 2\n}\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_1.bru",
    "content": "meta {\n  name: request_1\n  type: http\n  seq: 3\n}\n\nget {\n  url: https://echo.usebruno.com\n  body: text\n  auth: none\n}\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_2.bru",
    "content": "meta {\n  name: request_2\n  type: http\n  seq: 2\n}\n\nget {\n  url: https://echo.usebruno.com\n  body: text\n  auth: none\n}\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_1/request_3.bru",
    "content": "meta {\n  name: request_3\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://echo.usebruno.com\n  body: text\n  auth: none\n}\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/folder.bru",
    "content": "meta {\n  name: folder_2\n  seq: 1\n}\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_1.bru",
    "content": "meta {\n  name: request_1\n  type: http\n  seq: 2\n}\n\nget {\n  url: https://echo.usebruno.com\n  body: text\n  auth: none\n}\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_2.bru",
    "content": "meta {\n  name: request_2\n  type: http\n  seq: 3\n}\n\nget {\n  url: https://echo.usebruno.com\n  body: text\n  auth: none\n}\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/folder_2/request_3.bru",
    "content": "meta {\n  name: request_3\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://echo.usebruno.com\n  body: text\n  auth: none\n}\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_1.bru",
    "content": "meta {\n  name: request_1\n  type: http\n  seq: 3\n}\n\nget {\n  url: https://echo.usebruno.com\n  body: text\n  auth: none\n}\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_2.bru",
    "content": "meta {\n  name: request_2\n  type: http\n  seq: 5\n}\n\nget {\n  url: https://echo.usebruno.com\n  body: text\n  auth: none\n}\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_1/request_3.bru",
    "content": "meta {\n  name: request_3\n  type: http\n  seq: 4\n}\n\nget {\n  url: https://echo.usebruno.com\n  body: text\n  auth: none\n}\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/folder.bru",
    "content": "meta {\n  name: folder_2\n  seq: 1\n}\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_1.bru",
    "content": "meta {\n  name: request_1\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://echo.usebruno.com\n  body: text\n  auth: none\n}\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_2.bru",
    "content": "meta {\n  name: request_2\n  type: http\n  seq: 3\n}\n\nget {\n  url: https://echo.usebruno.com\n  body: text\n  auth: none\n}\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/folder_2/request_3.bru",
    "content": "meta {\n  name: request_3\n  type: http\n  seq: 2\n}\n\nget {\n  url: https://echo.usebruno.com\n  body: text\n  auth: none\n}\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_1.bru",
    "content": "meta {\n  name: request_1\n  type: http\n  seq: 3\n}\n\nget {\n  url: https://echo.usebruno.com\n  body: text\n  auth: none\n}\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_2.bru",
    "content": "meta {\n  name: request_2\n  type: http\n  seq: 4\n}\n\npost {\n  url: https://echo.usebruno.com/:request_path_param?request_query_param=request_query_param_value\n  body: text\n  auth: basic\n}\n\nparams:query {\n  request_query_param: request_query_param_value\n}\n\nparams:path {\n  request_path_param: request_path_param_value\n}\n\nheaders {\n  request_header: request_header_value\n}\n\nauth:basic {\n  username: username\n  password: password\n}\n\nbody:text {\n  ping\n}\n\nvars:pre-request {\n  request_pre_var: request_pre_var_value\n}\n\nvars:post-response {\n  request_post_var: request_post_var_value\n}\n\nassert {\n  res.status: eq 200\n}\n\nscript:pre-request {\n  const requestPreRequestScript = true;\n}\n\nscript:post-response {\n  const requestPostResponseScript = true;\n}\n\ntests {\n  test(\"request level script\", function() {\n    expect(\"test\").to.equal(\"test\");\n  });\n}\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/collection-json-from-pathname/collection/request_3.bru",
    "content": "meta {\n  name: request_3\n  type: http\n  seq: 2\n}\n\nget {\n  url: https://echo.usebruno.com\n  body: text\n  auth: none\n}\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/opencollection/collection/environments/dev.yml",
    "content": "name: Development\nvariables:\n  - name: baseUrl\n    value: https://api.dev.example.com\n  - name: apiKey\n    value: dev-api-key-123\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/opencollection/collection/get-users.yml",
    "content": "info:\n  name: Get Users\n  type: http\n  seq: 1\n\nhttp:\n  method: GET\n  url: https://api.example.com/users\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/opencollection/collection/opencollection.yml",
    "content": "opencollection: \"1.0.0\"\ninfo:\n  name: Test OpenCollection\n\nextensions:\n  bruno:\n    ignore:\n      - node_modules\n      - .git\n\nrequest:\n  headers:\n    - name: X-Collection-Header\n      value: collection-header-value\n      enabled: true\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/opencollection/collection/users/create-user.yml",
    "content": "info:\n  name: Create User\n  type: http\n  seq: 1\n\nhttp:\n  method: POST\n  url: https://api.example.com/users\n  body:\n    mode: json\n    json: |\n      {\n        \"name\": \"John Doe\"\n      }\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/fixtures/opencollection/collection/users/folder.yml",
    "content": "info:\n  name: Users\n  seq: 1\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/prepare-request.spec.js",
    "content": "const { describe, it, expect, beforeEach } = require('@jest/globals');\njest.mock('../../src/utils/filesystem', () => ({\n  isLargeFile: jest.fn()\n}));\nconst filesystemUtils = require('../../src/utils/filesystem');\nconst prepareRequest = require('../../src/runner/prepare-request');\n\ndescribe('prepare-request: prepareRequest', () => {\n  describe('Decomments request body', () => {\n    it('If request body is valid JSON', async () => {\n      const body = { mode: 'json', json: '{\\n\"test\": \"{{someVar}}\" // comment\\n}' };\n      const expected = `{\n\\\"test\\\": \\\"{{someVar}}\\\" \n}`;\n      const result = await prepareRequest({ request: { body } });\n      expect(result.data).toEqual(expected);\n    });\n\n    it('If request body is not valid JSON', async () => {\n      const body = { mode: 'json', json: '{\\n\"test\": {{someVar}} // comment\\n}' };\n      const expected = `{\n\\\"test\\\": {{someVar}} \n}`;\n      const result = await prepareRequest({ request: { body } });\n      expect(result.data).toEqual(expected);\n    });\n  });\n\n  describe('Properly maps inherited auth from collectionRoot', () => {\n    // Initialize Test Fixtures\n    let collection, item;\n\n    beforeEach(() => {\n      collection = {\n        name: 'Test Collection',\n        root: {\n          request: {\n            auth: {}\n          }\n        }\n      };\n\n      item = {\n        name: 'Test Request',\n        type: 'http-request',\n        request: {\n          method: 'GET',\n          headers: [],\n          params: [],\n          url: 'https://usebruno.com',\n          auth: {\n            mode: 'inherit'\n          },\n          script: {\n            req: 'console.log(\"Pre Request\")',\n            res: 'console.log(\"Post Response\")'\n          }\n        }\n      };\n    });\n\n    describe('API Key Authentication', () => {\n      it('If collection auth is apikey in header', async () => {\n        collection.root.request.auth = {\n          mode: 'apikey',\n          apikey: {\n            key: 'x-api-key',\n            value: '{{apiKey}}',\n            placement: 'header'\n          }\n        };\n\n        const result = await prepareRequest(item, collection);\n        expect(result.headers).toHaveProperty('x-api-key', '{{apiKey}}');\n      });\n\n      it('If collection auth is apikey in header and request has existing headers', async () => {\n        collection.root.request.auth = {\n          mode: 'apikey',\n          apikey: {\n            key: 'x-api-key',\n            value: '{{apiKey}}',\n            placement: 'header'\n          }\n        };\n\n        item.request.headers.push({ name: 'Content-Type', value: 'application/json', enabled: true });\n        const result = await prepareRequest(item, collection);\n        expect(result.headers).toHaveProperty('Content-Type', 'application/json');\n        expect(result.headers).toHaveProperty('x-api-key', '{{apiKey}}');\n      });\n\n      it('If collection auth is apikey in query parameters', async () => {\n        collection.root.request.auth = {\n          mode: 'apikey',\n          apikey: {\n            key: 'x-api-key',\n            value: '{{apiKey}}',\n            placement: 'queryparams'\n          }\n        };\n\n        const urlObj = new URL(item.request.url);\n        urlObj.searchParams.set(collection.root.request.auth.apikey.key, collection.root.request.auth.apikey.value);\n\n        const expected = urlObj.toString();\n        const result = await prepareRequest(item, collection);\n        expect(result.url).toEqual(expected);\n      });\n    });\n\n    describe('Basic Authentication', () => {\n      it('If collection auth is basic auth', async () => {\n        collection.root.request.auth = {\n          mode: 'basic',\n          basic: {\n            username: 'testUser',\n            password: 'testPass123'\n          }\n        };\n\n        const result = await prepareRequest(item, collection);\n        const expected = { username: 'testUser', password: 'testPass123' };\n        expect(result.basicAuth).toEqual(expected);\n      });\n    });\n\n    describe('Bearer Token Authentication', () => {\n      it('If collection auth is bearer token', async () => {\n        collection.root.request.auth = {\n          mode: 'bearer',\n          bearer: {\n            token: 'token'\n          }\n        };\n\n        const result = await prepareRequest(item, collection);\n        expect(result.headers).toHaveProperty('Authorization', 'Bearer token');\n      });\n\n      it('If collection auth is bearer token and request has existing headers', async () => {\n        collection.root.request.auth = {\n          mode: 'bearer',\n          bearer: {\n            token: 'token'\n          }\n        };\n\n        item.request.headers.push({ name: 'Content-Type', value: 'application/json', enabled: true });\n\n        const result = await prepareRequest(item, collection);\n        expect(result.headers).toHaveProperty('Authorization', 'Bearer token');\n        expect(result.headers).toHaveProperty('Content-Type', 'application/json');\n      });\n    });\n\n    describe('OAuth2 Authentication', () => {\n      it('If collection auth is OAuth2 with client credentials grant type', async () => {\n        collection.root.request.auth = {\n          mode: 'oauth2',\n          oauth2: {\n            grantType: 'client_credentials',\n            accessTokenUrl: 'https://auth.example.com/token',\n            clientId: 'test_client_id',\n            clientSecret: 'test_client_secret',\n            scope: 'read write',\n            credentialsPlacement: 'header',\n            tokenPlacement: 'header',\n            tokenHeaderPrefix: 'Bearer',\n            tokenQueryKey: 'access_token'\n          }\n        };\n\n        const result = await prepareRequest(item, collection);\n\n        expect(result.oauth2).toBeDefined();\n        expect(result.oauth2.grantType).toBe('client_credentials');\n        expect(result.oauth2.accessTokenUrl).toBe('https://auth.example.com/token');\n        expect(result.oauth2.clientId).toBe('test_client_id');\n        expect(result.oauth2.clientSecret).toBe('test_client_secret');\n        expect(result.oauth2.scope).toBe('read write');\n        expect(result.oauth2.credentialsPlacement).toBe('header');\n        expect(result.oauth2.tokenPlacement).toBe('header');\n        expect(result.oauth2.tokenHeaderPrefix).toBe('Bearer');\n        expect(result.oauth2.tokenQueryKey).toBe('access_token');\n      });\n\n      it('If collection auth is OAuth2 with password grant type', async () => {\n        collection.root.request.auth = {\n          mode: 'oauth2',\n          oauth2: {\n            grantType: 'password',\n            accessTokenUrl: 'https://auth.example.com/token',\n            username: 'test_user',\n            password: 'test_password',\n            clientId: 'test_client_id',\n            clientSecret: 'test_client_secret',\n            scope: 'read write',\n            credentialsPlacement: 'body',\n            tokenPlacement: 'url',\n            tokenHeaderPrefix: 'Bearer',\n            tokenQueryKey: 'access_token'\n          }\n        };\n\n        const result = await prepareRequest(item, collection);\n\n        expect(result.oauth2).toBeDefined();\n        expect(result.oauth2.grantType).toBe('password');\n        expect(result.oauth2.accessTokenUrl).toBe('https://auth.example.com/token');\n        expect(result.oauth2.username).toBe('test_user');\n        expect(result.oauth2.password).toBe('test_password');\n        expect(result.oauth2.clientId).toBe('test_client_id');\n        expect(result.oauth2.clientSecret).toBe('test_client_secret');\n        expect(result.oauth2.scope).toBe('read write');\n        expect(result.oauth2.credentialsPlacement).toBe('body');\n        expect(result.oauth2.tokenPlacement).toBe('url');\n        expect(result.oauth2.tokenHeaderPrefix).toBe('Bearer');\n        expect(result.oauth2.tokenQueryKey).toBe('access_token');\n      });\n    });\n\n    describe('AWS v4 Authentication', () => {\n      it('If collection auth is AWS v4', async () => {\n        collection.root.request.auth = {\n          mode: 'awsv4',\n          awsv4: {\n            accessKeyId: 'AKIAIOSFODNN7EXAMPLE',\n            secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',\n            sessionToken: 'session-token',\n            service: 's3',\n            region: 'us-west-2',\n            profileName: 'default'\n          }\n        };\n\n        const result = await prepareRequest(item, collection);\n        const expected = {\n          accessKeyId: 'AKIAIOSFODNN7EXAMPLE',\n          secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',\n          sessionToken: 'session-token',\n          service: 's3',\n          region: 'us-west-2',\n          profileName: 'default'\n        };\n        expect(result.awsv4config).toEqual(expected);\n      });\n    });\n\n    describe('NTLM Authentication', () => {\n      it('If collection auth is NTLM', async () => {\n        collection.root.request.auth = {\n          mode: 'ntlm',\n          ntlm: {\n            username: 'testUser',\n            password: 'testPass123',\n            domain: 'testDomain'\n          }\n        };\n\n        const result = await prepareRequest(item, collection);\n        const expected = {\n          username: 'testUser',\n          password: 'testPass123',\n          domain: 'testDomain'\n        };\n        expect(result.ntlmConfig).toEqual(expected);\n      });\n    });\n\n    describe('WSSE Authentication', () => {\n      it('If collection auth is WSSE', async () => {\n        collection.root.request.auth = {\n          mode: 'wsse',\n          wsse: {\n            username: 'testUser',\n            password: 'testPass123'\n          }\n        };\n\n        const result = await prepareRequest(item, collection);\n        expect(result.headers).toHaveProperty('X-WSSE');\n        expect(result.headers['X-WSSE']).toContain('UsernameToken Username=\"testUser\"');\n        expect(result.headers['X-WSSE']).toContain('PasswordDigest=\"');\n        expect(result.headers['X-WSSE']).toContain('Nonce=\"');\n        expect(result.headers['X-WSSE']).toContain('Created=\"');\n      });\n    });\n\n    describe('Digest Authentication', () => {\n      it('If collection auth is digest auth', async () => {\n        collection.root.request.auth = {\n          mode: 'digest',\n          digest: {\n            username: 'testUser',\n            password: 'testPass123'\n          }\n        };\n\n        const result = await prepareRequest(item, collection);\n\n        const expected = {\n          username: 'testUser',\n          password: 'testPass123'\n        };\n        expect(result.digestConfig).toEqual(expected);\n      });\n    });\n\n    describe('No Authentication', () => {\n      it('If request does not have auth configured', async () => {\n        delete item.request.auth;\n        let result;\n        expect(() => {\n          result = prepareRequest(item, collection);\n        }).not.toThrow();\n        expect(result).toBeDefined();\n      });\n    });\n  });\n\n  describe('Properly maps request-level auth', () => {\n    let item;\n\n    beforeEach(() => {\n      item = {\n        name: 'Test Request',\n        type: 'http-request',\n        request: {\n          method: 'GET',\n          headers: [],\n          params: [],\n          url: 'https://usebruno.com',\n          auth: {\n            mode: 'basic' // Will be overridden in each test\n          },\n          script: {\n            req: 'console.log(\"Pre Request\")',\n            res: 'console.log(\"Post Response\")'\n          }\n        }\n      };\n    });\n\n    describe('API Key Authentication', () => {\n      it('If request auth is apikey in header', async () => {\n        item.request.auth = {\n          mode: 'apikey',\n          apikey: {\n            key: 'x-api-key',\n            value: '{{apiKey}}',\n            placement: 'header'\n          }\n        };\n\n        const result = await prepareRequest(item);\n        expect(result.headers).toHaveProperty('x-api-key', '{{apiKey}}');\n      });\n\n      it('If request auth is apikey in header and request has existing headers', async () => {\n        item.request.auth = {\n          mode: 'apikey',\n          apikey: {\n            key: 'x-api-key',\n            value: '{{apiKey}}',\n            placement: 'header'\n          }\n        };\n\n        item.request.headers.push({ name: 'Content-Type', value: 'application/json', enabled: true });\n        const result = await prepareRequest(item);\n        expect(result.headers).toHaveProperty('Content-Type', 'application/json');\n        expect(result.headers).toHaveProperty('x-api-key', '{{apiKey}}');\n      });\n\n      it('If request auth is apikey in query parameters', async () => {\n        item.request.auth = {\n          mode: 'apikey',\n          apikey: {\n            key: 'x-api-key',\n            value: '{{apiKey}}',\n            placement: 'queryparams'\n          }\n        };\n\n        const urlObj = new URL(item.request.url);\n        urlObj.searchParams.set(item.request.auth.apikey.key, item.request.auth.apikey.value);\n\n        const expected = urlObj.toString();\n        const result = await prepareRequest(item);\n        expect(result.url).toEqual(expected);\n      });\n    });\n\n    describe('Basic Authentication', () => {\n      it('If request auth is basic auth', async () => {\n        item.request.auth = {\n          mode: 'basic',\n          basic: {\n            username: 'testUser',\n            password: 'testPass123'\n          }\n        };\n\n        const result = await prepareRequest(item);\n        const expected = { username: 'testUser', password: 'testPass123' };\n        expect(result.basicAuth).toEqual(expected);\n      });\n    });\n\n    describe('Bearer Token Authentication', () => {\n      it('If request auth is bearer token', async () => {\n        item.request.auth = {\n          mode: 'bearer',\n          bearer: {\n            token: 'token123'\n          }\n        };\n\n        const result = await prepareRequest(item);\n        expect(result.headers).toHaveProperty('Authorization', 'Bearer token123');\n      });\n\n      it('If request auth is bearer token and request has existing headers', async () => {\n        item.request.auth = {\n          mode: 'bearer',\n          bearer: {\n            token: 'token123'\n          }\n        };\n\n        item.request.headers.push({ name: 'Content-Type', value: 'application/json', enabled: true });\n\n        const result = await prepareRequest(item);\n        expect(result.headers).toHaveProperty('Authorization', 'Bearer token123');\n        expect(result.headers).toHaveProperty('Content-Type', 'application/json');\n      });\n    });\n\n    describe('AWS v4 Authentication', () => {\n      it('If request auth is AWS v4', async () => {\n        item.request.auth = {\n          mode: 'awsv4',\n          awsv4: {\n            accessKeyId: 'AKIAIOSFODNN7EXAMPLE',\n            secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',\n            sessionToken: 'request-session-token',\n            service: 'dynamodb',\n            region: 'us-east-1',\n            profileName: 'dev'\n          }\n        };\n\n        const result = await prepareRequest(item);\n        const expected = {\n          accessKeyId: 'AKIAIOSFODNN7EXAMPLE',\n          secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',\n          sessionToken: 'request-session-token',\n          service: 'dynamodb',\n          region: 'us-east-1',\n          profileName: 'dev'\n        };\n        expect(result.awsv4config).toEqual(expected);\n      });\n    });\n\n    describe('NTLM Authentication', () => {\n      it('If request auth is NTLM', async () => {\n        item.request.auth = {\n          mode: 'ntlm',\n          ntlm: {\n            username: 'testUser',\n            password: 'testPass123',\n            domain: 'testDomain'\n          }\n        };\n\n        const result = await prepareRequest(item);\n        const expected = {\n          username: 'testUser',\n          password: 'testPass123',\n          domain: 'testDomain'\n        };\n        expect(result.ntlmConfig).toEqual(expected);\n      });\n    });\n\n    describe('WSSE Authentication', () => {\n      it('If request auth is WSSE', async () => {\n        item.request.auth = {\n          mode: 'wsse',\n          wsse: {\n            username: 'requestUser',\n            password: 'requestPass'\n          }\n        };\n\n        const result = await prepareRequest(item);\n        expect(result.headers).toHaveProperty('X-WSSE');\n        expect(result.headers['X-WSSE']).toContain('UsernameToken Username=\"requestUser\"');\n        expect(result.headers['X-WSSE']).toContain('PasswordDigest=\"');\n        expect(result.headers['X-WSSE']).toContain('Nonce=\"');\n        expect(result.headers['X-WSSE']).toContain('Created=\"');\n      });\n    });\n\n    describe('Digest Authentication', () => {\n      it('If request auth is digest auth', async () => {\n        item.request.auth = {\n          mode: 'digest',\n          digest: {\n            username: 'requestUser',\n            password: 'requestPass123'\n          }\n        };\n\n        const result = await prepareRequest(item);\n        const expected = {\n          username: 'requestUser',\n          password: 'requestPass123'\n        };\n        expect(result.digestConfig).toEqual(expected);\n      });\n    });\n  });\n\n  describe('Request file body mode', () => {\n    const fs = require('node:fs');\n    let readFileSyncSpy;\n    let createReadStreamSpy;\n\n    beforeEach(() => {\n      readFileSyncSpy = jest.spyOn(fs, 'readFileSync');\n      createReadStreamSpy = jest.spyOn(fs, 'createReadStream');\n    });\n\n    afterEach(() => {\n      jest.restoreAllMocks();\n    });\n\n    it('should use readFileSync to read small files', async () => {\n      const fileContent = Buffer.from('small file content');\n      filesystemUtils.isLargeFile.mockReturnValue(false);\n      readFileSyncSpy.mockReturnValue(fileContent);\n\n      const item = {\n        name: 'File Request',\n        type: 'http-request',\n        request: {\n          method: 'POST',\n          headers: [],\n          params: [],\n          url: 'https://example.com/upload',\n          body: {\n            mode: 'file',\n            file: [{\n              contentType: 'text/plain',\n              filePath: '/path/to/file.txt',\n              selected: true\n            }]\n          }\n        }\n      };\n\n      const result = await prepareRequest(item);\n\n      expect(result.data).toBe(fileContent);\n      expect(readFileSyncSpy).toHaveBeenCalled();\n      expect(createReadStreamSpy).not.toHaveBeenCalled();\n    });\n\n    it('should use createReadStream to read large files', async () => {\n      const mockStream = { pipe: jest.fn() };\n      filesystemUtils.isLargeFile.mockReturnValue(true);\n      createReadStreamSpy.mockReturnValue(mockStream);\n\n      const item = {\n        name: 'File Request',\n        type: 'http-request',\n        request: {\n          method: 'POST',\n          headers: [],\n          params: [],\n          url: 'https://example.com/upload',\n          body: {\n            mode: 'file',\n            file: [{\n              contentType: 'application/octet-stream',\n              filePath: '/path/to/large-file.bin',\n              selected: true\n            }]\n          }\n        }\n      };\n\n      const result = await prepareRequest(item);\n\n      expect(result.data).toBe(mockStream);\n      expect(createReadStreamSpy).toHaveBeenCalled();\n      expect(readFileSyncSpy).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('GraphQL request', () => {\n    it('keeps variables as string for interpolation', async () => {\n      const item = {\n        request: {\n          method: 'POST',\n          headers: [],\n          params: [],\n          url: 'https://example.com',\n          body: {\n            mode: 'graphql',\n            graphql: {\n              query: 'query { x }',\n              variables: '{\"apiPermissions\": {{permissionsJSON}}}'\n            }\n          }\n        }\n      };\n      const result = await prepareRequest(item);\n      expect(result.mode).toBe('graphql');\n      expect(result.data).toMatchObject({ query: 'query { x }' });\n      expect(typeof result.data.variables).toBe('string');\n      expect(result.data.variables).toBe('{\"apiPermissions\": {{permissionsJSON}}}');\n    });\n\n    it('defaults variables to \"{}\" when missing', async () => {\n      const item = {\n        request: {\n          method: 'POST',\n          headers: [],\n          params: [],\n          url: 'https://example.com',\n          body: {\n            mode: 'graphql',\n            graphql: {\n              query: 'query { x }',\n              variables: undefined\n            }\n          }\n        }\n      };\n      const result = await prepareRequest(item);\n      expect(typeof result.data.variables).toBe('string');\n      expect(result.data.variables).toBe('{}');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-cli/tests/runner/report-metadata.spec.js",
    "content": "const { describe, it, expect } = require('@jest/globals');\nconst { generateHtmlReport } = require('@usebruno/common/runner');\n\ndescribe('HTML Report Generation', () => {\n  it('should include all metadata in the HTML report', async () => {\n    // Sample test results\n    const mockResults = [\n      {\n        iterationIndex: 0,\n        environment: 'production',\n        results: [],\n        summary: {\n          totalRequests: 1,\n          passedRequests: 1,\n          failedRequests: 0,\n          errorRequests: 0,\n          skippedRequests: 0,\n          totalAssertions: 0,\n          passedAssertions: 0,\n          failedAssertions: 0,\n          totalTests: 0,\n          passedTests: 0,\n          failedTests: 0\n        }\n      }\n    ];\n\n    // Generate HTML using mock data\n    const htmlString = generateHtmlReport({\n      runnerResults: mockResults,\n      version: 'usebruno v1.16.0',\n      environment: 'production',\n      runCompletionTime: '2024-01-15T14:30:45.123Z'\n    });\n\n    // Verify the HTML contains expected metadata structure\n    expect(htmlString).toContain('Bruno run dashboard');\n    expect(htmlString).toContain('Date & Time');\n    expect(htmlString).toContain('Version');\n    expect(htmlString).toContain('Environment');\n    expect(htmlString).toContain('Total run duration');\n    expect(htmlString).toContain('Total data received');\n    expect(htmlString).toContain('Average response time');\n\n    expect(htmlString).toContain('{{ runCompletionTime }}');\n    expect(htmlString).toContain('{{ brunoVersion }}');\n    expect(htmlString).toContain('{{ environment }}');\n    expect(htmlString).toContain('{{ totalDuration }}');\n    expect(htmlString).toContain('{{ totalDataReceived }}');\n    expect(htmlString).toContain('{{ averageResponseTime }}');\n  });\n\n  it('should include skipped requests with parsing errors in the HTML report', async () => {\n    const mockResults = [\n      {\n        iterationIndex: 0,\n        results: [\n          {\n            test: {\n              filename: 'invalid-request.bru'\n            },\n            request: {\n              method: null,\n              url: null,\n              headers: null,\n              data: null\n            },\n            response: {\n              status: 'skipped',\n              statusText: 'Unexpected token',\n              data: null,\n              responseTime: 0\n            },\n            error: 'Unexpected token',\n            status: 'skipped',\n            skipped: true,\n            assertionResults: [],\n            testResults: [],\n            preRequestTestResults: [],\n            postResponseTestResults: [],\n            name: 'invalid-request.bru',\n            path: 'invalid-request.bru',\n            runDuration: 0\n          }\n        ],\n        summary: {\n          totalRequests: 1,\n          passedRequests: 0,\n          failedRequests: 0,\n          errorRequests: 0,\n          skippedRequests: 1,\n          totalAssertions: 0,\n          passedAssertions: 0,\n          failedAssertions: 0,\n          totalTests: 0,\n          passedTests: 0,\n          failedTests: 0\n        }\n      }\n    ];\n\n    const htmlString = generateHtmlReport({\n      runnerResults: mockResults,\n      version: 'usebruno v1.16.0',\n      environment: null,\n      runCompletionTime: '2024-01-15T14:30:45.123Z'\n    });\n\n    expect(htmlString).toContain('Request Skipped');\n    expect(htmlString).toContain('summarySkippedRequests');\n    expect(htmlString).toContain('result.response.status === \\'skipped\\'');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-cli/tests/utils/collection/create-collection-from-bruno-object.spec.js",
    "content": "const fs = require('node:fs');\nconst os = require('node:os');\nconst path = require('node:path');\nconst { describe, it, expect, afterEach } = require('@jest/globals');\nconst { parseRequest, parseFolder } = require('@usebruno/filestore');\nconst { createCollectionFromBrunoObject } = require('../../../src/utils/collection');\n\ndescribe('createCollectionFromBrunoObject', () => {\n  let outputDir;\n  const createOutputDir = () => {\n    outputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bruno-cli-import-'));\n    return outputDir;\n  };\n  const parseBruRequestFromPath = (filePath) => parseRequest(fs.readFileSync(filePath, 'utf8'), { format: 'bru' });\n  const parseBruFolderFromPath = (filePath) => parseFolder(fs.readFileSync(filePath, 'utf8'), { format: 'bru' });\n\n  afterEach(() => {\n    if (outputDir && fs.existsSync(outputDir)) {\n      fs.rmSync(outputDir, { recursive: true, force: true });\n    }\n  });\n\n  it('writes http and graphql requests from imported collection items', async () => {\n    createOutputDir();\n\n    await createCollectionFromBrunoObject(\n      {\n        name: 'imported-collection',\n        items: [\n          {\n            type: 'http-request',\n            name: 'Get Users',\n            filename: 'get-users.bru',\n            seq: 1,\n            request: {\n              method: 'GET',\n              url: 'https://api.example.com/users'\n            }\n          },\n          {\n            type: 'graphql-request',\n            name: 'Get Viewer',\n            filename: 'get-viewer.bru',\n            seq: 2,\n            request: {\n              method: 'POST',\n              url: 'https://api.example.com/graphql',\n              body: {\n                mode: 'graphql',\n                graphql: {\n                  query: 'query { viewer { id } }',\n                  variables: '{}'\n                }\n              }\n            }\n          }\n        ]\n      },\n      outputDir,\n      { format: 'bru' }\n    );\n\n    const httpPath = path.join(outputDir, 'get-users.bru');\n    const graphqlPath = path.join(outputDir, 'get-viewer.bru');\n\n    expect(fs.existsSync(httpPath)).toBe(true);\n    expect(fs.existsSync(graphqlPath)).toBe(true);\n\n    const httpRequest = parseBruRequestFromPath(httpPath);\n    const graphqlRequest = parseBruRequestFromPath(graphqlPath);\n\n    expect(httpRequest).toHaveProperty('type', 'http-request');\n    expect(httpRequest).toHaveProperty('request.method', 'GET');\n    expect(graphqlRequest).toHaveProperty('type', 'graphql-request');\n    expect(graphqlRequest).toHaveProperty('request.method', 'POST');\n  });\n\n  it('writes folder.bru in bru format', async () => {\n    createOutputDir();\n\n    await createCollectionFromBrunoObject(\n      {\n        name: 'folder-collection',\n        items: [\n          {\n            type: 'folder',\n            name: 'Users',\n            seq: 3,\n            root: {\n              meta: { name: 'Users' }\n            },\n            items: [\n              {\n                type: 'http-request',\n                name: 'List Users',\n                filename: 'list-users.bru',\n                seq: 1,\n                request: {\n                  method: 'GET',\n                  url: 'https://api.example.com/users'\n                }\n              }\n            ]\n          }\n        ]\n      },\n      outputDir,\n      { format: 'bru' }\n    );\n\n    const folderPath = path.join(outputDir, 'Users');\n    const folderBruPath = path.join(folderPath, 'folder.bru');\n    const nestedRequestPath = path.join(folderPath, 'list-users.bru');\n\n    expect(fs.existsSync(folderBruPath)).toBe(true);\n    expect(fs.existsSync(nestedRequestPath)).toBe(true);\n\n    const folder = parseBruFolderFromPath(folderBruPath);\n    const nestedRequest = parseBruRequestFromPath(nestedRequestPath);\n\n    expect(folder).toHaveProperty('meta.name', 'Users');\n    expect(folder).toHaveProperty('meta.seq', 3);\n    expect(nestedRequest).toHaveProperty('type', 'http-request');\n    expect(nestedRequest).toHaveProperty('request.method', 'GET');\n  });\n\n  it('throws for unsupported item types', async () => {\n    createOutputDir();\n\n    await expect(\n      createCollectionFromBrunoObject(\n        {\n          name: 'invalid-item-type-collection',\n          items: [\n            {\n              type: 'unsupported-type',\n              name: 'Unsupported'\n            }\n          ]\n        },\n        outputDir,\n        { format: 'bru' }\n      )\n    ).rejects.toThrow('Unsupported item type: unsupported-type');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-cli/tests/utils/collection/get-call-stack.spec.js",
    "content": "const { describe, it, expect, beforeEach } = require('@jest/globals');\nconst { getCallStack } = require('../../../src/utils/collection');\n\nconst collection = {\n  brunoConfig: {\n    version: '1',\n    name: 'multirun-cli',\n    type: 'collection',\n    ignore: ['node_modules', '.git']\n  },\n  root: {\n    request: {\n      headers: [],\n      auth: {},\n      script: {},\n      vars: {},\n      tests: ''\n    }\n  },\n  pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20',\n  items: [\n    {\n      name: 'root-folder',\n      pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder',\n      type: 'folder',\n      items: [\n        {\n          name: 'root-child-folder',\n          pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder',\n          type: 'folder',\n          items: [\n            {\n              name: 'root-child-child-folder',\n              pathname:\n                '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-folder',\n              type: 'folder',\n              items: [\n                {\n                  name: 'root-child-child-child-req-0',\n                  pathname:\n                    '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-folder/root-child-child-child-req-0.bru',\n                  type: 'http-request',\n                  seq: 1,\n                  request: {\n                    method: 'GET',\n                    url: 'https://g.cn',\n                    auth: {\n                      mode: 'inherit'\n                    },\n                    params: [],\n                    headers: [],\n                    body: {\n                      mode: 'none'\n                    },\n                    vars: [],\n                    assertions: [],\n                    script: {\n                      req: 'console.log(\"root-child-child-child-file-0\")'\n                    },\n                    tests: ''\n                  }\n                },\n                {\n                  name: 'root-child-child-child-req-1',\n                  pathname:\n                    '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-folder/root-child-child-child-req-1.bru',\n                  type: 'http-request',\n                  seq: 2,\n                  request: {\n                    method: 'GET',\n                    url: 'https://g.cn',\n                    auth: {\n                      mode: 'inherit'\n                    },\n                    params: [],\n                    headers: [],\n                    body: {\n                      mode: 'none'\n                    },\n                    vars: [],\n                    assertions: [],\n                    script: {\n                      req: 'console.log(\"root-child-child-child-file-1\")'\n                    },\n                    tests: ''\n                  }\n                }\n              ],\n              root: {\n                request: {\n                  headers: [],\n                  auth: {},\n                  script: {},\n                  vars: {},\n                  tests: ''\n                },\n                meta: {\n                  name: 'root-child-child-folder',\n                  seq: 3\n                }\n              },\n              seq: 3\n            },\n            {\n              name: 'root-child-child-req-0',\n              pathname:\n                '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-req-0.bru',\n              type: 'http-request',\n              seq: 4,\n              request: {\n                method: 'GET',\n                url: 'https://g.cn',\n                auth: {\n                  mode: 'inherit'\n                },\n                params: [],\n                headers: [],\n                body: {\n                  mode: 'none'\n                },\n                vars: [],\n                assertions: [],\n                script: {\n                  req: 'console.log(\"root-child-child-file-0\")'\n                },\n                tests: ''\n              }\n            },\n            {\n              name: 'root-child-child-req-1',\n              pathname:\n                '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-req-1.bru',\n              type: 'http-request',\n              seq: 5,\n              request: {\n                method: 'GET',\n                url: 'https://g.cn',\n                auth: {\n                  mode: 'inherit'\n                },\n                params: [],\n                headers: [],\n                body: {\n                  mode: 'none'\n                },\n                vars: [],\n                assertions: [],\n                script: {\n                  req: 'console.log(\"root-child-child-file-1\")'\n                },\n                tests: ''\n              }\n            }\n          ],\n          root: {\n            request: {\n              headers: [],\n              auth: {},\n              script: {},\n              vars: {},\n              tests: ''\n            },\n            meta: {\n              name: 'root-child-folder',\n              seq: 6\n            }\n          },\n          seq: 6\n        },\n        {\n          name: 'root-child-req-0',\n          pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-req-0.bru',\n          type: 'http-request',\n          seq: 7,\n          request: {\n            method: 'GET',\n            url: 'https://g.cn',\n            auth: {\n              mode: 'inherit'\n            },\n            params: [],\n            headers: [],\n            body: {\n              mode: 'none'\n            },\n            vars: [],\n            assertions: [],\n            script: {\n              req: 'console.log(\"root-child-file-0\")'\n            },\n            tests: ''\n          }\n        },\n        {\n          name: 'root-child-req-1',\n          pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-req-1.bru',\n          type: 'http-request',\n          seq: 8,\n          request: {\n            method: 'GET',\n            url: 'https://g.cn',\n            auth: {\n              mode: 'inherit'\n            },\n            params: [],\n            headers: [],\n            body: {\n              mode: 'none'\n            },\n            vars: [],\n            assertions: [],\n            script: {\n              req: 'console.log(\"root-child-file-1\")'\n            },\n            tests: ''\n          }\n        }\n      ],\n      root: {\n        request: {\n          headers: [],\n          auth: {},\n          script: {},\n          vars: {},\n          tests: ''\n        },\n        meta: {\n          name: 'root-folder',\n          seq: 9\n        }\n      },\n      seq: 9\n    },\n    {\n      name: 'root-req-0',\n      pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-0.bru',\n      type: 'http-request',\n      seq: 10,\n      request: {\n        method: 'GET',\n        url: 'https://g.cn',\n        auth: {\n          mode: 'inherit'\n        },\n        params: [],\n        headers: [],\n        body: {\n          mode: 'none'\n        },\n        vars: [],\n        assertions: [],\n        script: {\n          req: 'console.log(\"root-file-0\")'\n        },\n        tests: ''\n      }\n    },\n    {\n      name: 'root-req-1',\n      pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-1.bru',\n      type: 'http-request',\n      seq: 11,\n      request: {\n        method: 'GET',\n        url: 'https://g.cn',\n        auth: {\n          mode: 'inherit'\n        },\n        params: [],\n        headers: [],\n        body: {\n          mode: 'none'\n        },\n        vars: [],\n        assertions: [],\n        script: {\n          req: 'console.log(\"root-file-1\")'\n        },\n        tests: ''\n      }\n    },\n    {\n      name: 'root-req-2',\n      pathname: '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-2.bru',\n      type: 'http-request',\n      seq: 12,\n      request: {\n        method: 'GET',\n        url: 'https://g.cn',\n        auth: {\n          mode: 'inherit'\n        },\n        params: [],\n        headers: [],\n        body: {\n          mode: 'none'\n        },\n        vars: [],\n        assertions: [],\n        script: {\n          req: 'console.log(\"root-file-2\")'\n        },\n        tests: ''\n      }\n    }\n  ]\n};\n\nconst sequenceChangedCollection = {\n  brunoConfig: {\n    version: '1',\n    name: 'sequenceChangedCollection',\n    type: 'collection',\n    ignore: ['node_modules', '.git']\n  },\n  root: {},\n  pathname: '/Users/tempo/Downloads/t-temp/sequenceChangedCollection',\n  items: [\n    {\n      name: 'three',\n      pathname: '/Users/tempo/Downloads/t-temp/sequenceChangedCollection/three.bru',\n      type: 'http-request',\n      seq: 1,\n      request: {\n        method: 'GET',\n        url: 'https://usebruno.com',\n        auth: {\n          mode: 'inherit'\n        },\n        params: [],\n        headers: [],\n        body: {\n          mode: 'none'\n        },\n        vars: [],\n        assertions: [],\n        script: {},\n        tests: ''\n      }\n    },\n    {\n      name: 'one',\n      pathname: '/Users/tempo/Downloads/t-temp/sequenceChangedCollection/one.bru',\n      type: 'http-request',\n      seq: 2,\n      request: {\n        method: 'GET',\n        url: 'https://usebruno.com',\n        auth: {\n          mode: 'inherit'\n        },\n        params: [],\n        headers: [],\n        body: {\n          mode: 'none'\n        },\n        vars: [],\n        assertions: [],\n        script: {},\n        tests: ''\n      }\n    },\n    {\n      name: 'two',\n      pathname: '/Users/tempo/Downloads/t-temp/sequenceChangedCollection/two.bru',\n      type: 'http-request',\n      seq: 2,\n      request: {\n        method: 'GET',\n        url: 'https://usebruno.com',\n        auth: {\n          mode: 'inherit'\n        },\n        params: [],\n        headers: [],\n        body: {\n          mode: 'none'\n        },\n        vars: [],\n        assertions: [],\n        script: {},\n        tests: ''\n      }\n    }\n  ]\n};\n\ndescribe('getCallStack', () => {\n  it('should return all requests in the collection', () => {\n    const callStack = getCallStack(['/Users/tempo/Downloads/t-temp/multirun-cli-20'], collection, { recursive: true });\n    const expectedCallStack = [\n      '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-folder/root-child-child-child-req-0.bru',\n      '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-folder/root-child-child-child-req-1.bru',\n      '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-req-0.bru',\n      '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-req-1.bru',\n      '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-req-0.bru',\n      '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-req-1.bru',\n      '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-0.bru',\n      '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-1.bru',\n      '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-2.bru'\n    ];\n    expect(callStack.map((item) => item.pathname)).toEqual(expectedCallStack);\n  });\n\n  it('should return all requests in the collection when sequence is changed', () => {\n    const callStack = getCallStack(\n      ['/Users/tempo/Downloads/t-temp/sequenceChangedCollection'],\n      sequenceChangedCollection,\n      {\n        recursive: true\n      }\n    );\n    const expectedCallStack = [\n      '/Users/tempo/Downloads/t-temp/sequenceChangedCollection/three.bru',\n      '/Users/tempo/Downloads/t-temp/sequenceChangedCollection/one.bru',\n      '/Users/tempo/Downloads/t-temp/sequenceChangedCollection/two.bru'\n    ];\n    expect(callStack.map((item) => item.pathname)).toEqual(expectedCallStack);\n  });\n});\n\ndescribe('getCallStack with collection sequence changed', () => {\n  it('should return an empty array', () => {\n    const callStack = getCallStack(\n      ['/Users/tempo/Downloads/t-temp/sequenceChangedCollection'],\n      sequenceChangedCollection,\n      {\n        recursive: true\n      }\n    );\n    const expectedCallStack = [\n      '/Users/tempo/Downloads/t-temp/sequenceChangedCollection/three.bru',\n      '/Users/tempo/Downloads/t-temp/sequenceChangedCollection/one.bru',\n      '/Users/tempo/Downloads/t-temp/sequenceChangedCollection/two.bru'\n    ];\n    expect(callStack.map((item) => item.pathname)).toEqual(expectedCallStack);\n  });\n});\n\ndescribe('getCallStack with muliple folders and requests run', () => {\n  it('should return an empty array', () => {\n    const callStack = getCallStack(\n      [\n        '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-0.bru',\n        '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-req-0.bru',\n        '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-2.bru'\n      ],\n      collection,\n      {\n        recursive: true\n      }\n    );\n    const expectedCallStack = [\n      '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-0.bru',\n      '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-folder/root-child-folder/root-child-child-req-0.bru',\n      '/Users/tempo/Downloads/t-temp/multirun-cli-20/root-req-2.bru'\n    ];\n    expect(callStack.map((item) => item.pathname)).toEqual(expectedCallStack);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-cli/tests/utils/common.spec.js",
    "content": "const { describe, it, expect } = require('@jest/globals');\nconst { hasExecutableTestInScript } = require('../../src/utils/request');\n\ndescribe('hasExecutableTestInScript', () => {\n  describe('should return true for valid test() calls', () => {\n    it('should detect basic test calls', () => {\n      const script = `\n        test(\"should work\", function() {\n          expect(true).to.be.true;\n        });\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(true);\n    });\n\n    it('should detect indented test calls', () => {\n      const script = `\n        if (true) {\n          test(\"indented test\", function() {\n            expect(1).to.equal(1);\n          });\n        }\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(true);\n    });\n\n    it('should detect test calls with extra whitespace', () => {\n      const script = `test   (\"with spaces\", function() { });`;\n      expect(hasExecutableTestInScript(script)).toBe(true);\n    });\n\n    it('should detect test calls after assignments', () => {\n      const script = `\n        const result = test(\"assignment test\", function() {\n          expect(\"hello\").to.be.a(\"string\");\n        });\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(true);\n    });\n\n    it('should detect test calls in conditionals', () => {\n      const script = `\n        if (condition) {\n          test(\"conditional test\", function() {\n            expect(true).to.be.true;\n          });\n        }\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(true);\n    });\n\n    it('should detect test calls in arrays', () => {\n      const script = `\n        const tests = [\n          test(\"array test\", function() {\n            expect(Array.isArray([])).to.be.true;\n          })\n        ];\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(true);\n    });\n\n    it('should detect test calls in ternary operators', () => {\n      const script = `\n        const result = condition ? test(\"ternary test\", function() {\n          expect(true).to.be.true;\n        }) : null;\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(true);\n    });\n\n    it('should detect test calls after semicolons', () => {\n      const script = `\n        const data = res.data; test(\"after semicolon\", function() {\n          expect(data).to.be.an(\"object\");\n        });\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(true);\n    });\n\n    it('should detect test calls in object values', () => {\n      const script = `\n        const config = {\n          validation: test(\"object value test\", function() {\n            expect(true).to.be.true;\n          })\n        };\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(true);\n    });\n\n    it('should detect multiple test calls', () => {\n      const script = `\n        test(\"first test\", function() {\n          expect(1).to.equal(1);\n        });\n        \n        test(\"second test\", function() {\n          expect(2).to.equal(2);\n        });\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(true);\n    });\n\n    it('should detect test calls at start of script', () => {\n      const script = `test(\"at start\", function() { expect(true).to.be.true; });`;\n      expect(hasExecutableTestInScript(script)).toBe(true);\n    });\n  });\n\n  describe('should return false for invalid test() calls', () => {\n    it('should ignore commented out test calls with //', () => {\n      const script = `\n        // test(\"commented test\", function() {\n        //   expect(true).to.be.true;\n        // });\n        console.log(\"no real tests here\");\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(false);\n    });\n\n    it('should ignore commented out test calls with /* */', () => {\n      const script = `\n        /* test(\"block commented test\", function() {\n           expect(true).to.be.true;\n         }); */\n        console.log(\"no real tests here\");\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(false);\n    });\n\n    it('should ignore test() in double-quoted strings', () => {\n      const script = `\n        console.log(\"This contains test() but should not match\");\n        console.log(\"Remember to test() your API\");\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(false);\n    });\n\n    it('should ignore test() in single-quoted strings', () => {\n      const script = `\n        console.log('Single quote test() should not match');\n        const message = 'Use test() for validation';\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(false);\n    });\n\n    it('should ignore test() in template literals', () => {\n      const script = `\n        console.log(\\`Template literal test() should not match\\`);\n        const message = \\`Remember to test() your code\\`;\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(false);\n    });\n\n    it('should ignore object method calls', () => {\n      const script = `\n        const obj = { test: function() { return \"not a real test\"; } };\n        obj.test(\"This is a method call\");\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(false);\n    });\n\n    it('should ignore this.test() calls', () => {\n      const script = `\n        this.test(\"Another method call\");\n        this.test();\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(false);\n    });\n\n    it('should ignore complex object chain calls', () => {\n      const script = `\n        api.client.test(\"Should not match\");\n        user.test.endpoint(\"Chained method\");\n        window.test(\"Should not match\");\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(false);\n    });\n\n    it('should ignore object methods in variables', () => {\n      const script = `\n        const validator = {\n          test: function(value) { return value > 0; }\n        };\n        validator.test(42);\n        \n        const tester = { test: () => \"mock\" };\n        tester.test(\"method call\");\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(false);\n    });\n\n    it('should return false for empty scripts', () => {\n      expect(hasExecutableTestInScript('')).toBe(false);\n      expect(hasExecutableTestInScript(null)).toBe(false);\n      expect(hasExecutableTestInScript(undefined)).toBe(false);\n    });\n\n    it('should return false for scripts with no test calls', () => {\n      const script = `\n        bru.setVar(\"userId\", \"12345\");\n        console.log(\"Setting up request\");\n        const data = res.data;\n        bru.setVar(\"responseData\", data);\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(false);\n    });\n\n    it('should return false when test is part of other words', () => {\n      const script = `\n        const testing = \"value\";\n        const protest = \"demo\";\n        const fastest = \"speed\";\n        console.log(\"contest results\");\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(false);\n    });\n  });\n\n  describe('should handle mixed scenarios correctly', () => {\n    it('should return true when valid test exists among invalid ones', () => {\n      const script = `\n        // test(\"commented out\");\n        console.log(\"test() in string\");\n        obj.test(\"method call\");\n        \n        test(\"real test\", function() {\n          expect(true).to.be.true;\n        });\n        \n        api.client.test(\"another method\");\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(true);\n    });\n\n    it('should return false when only invalid tests exist', () => {\n      const script = `\n        // test(\"commented out test\", function() {\n        //   expect(true).to.be.true;\n        // });\n        \n        console.log(\"test() inside string\");\n        console.log('test() in single quotes');\n        console.log(\\`test() in template\\`);\n        \n        const obj = { test: () => \"mock\" };\n        obj.test(\"method call\");\n        this.test(\"another method\");\n        api.client.test(\"chained method\");\n        \n        bru.setVar(\"test\", \"variable name\");\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(false);\n    });\n\n    it('should handle complex nested quotes correctly', () => {\n      const script = `\n        console.log(\"String with 'nested quotes' and test() call\");\n        console.log('String with \"nested quotes\" and test() call');\n        \n        test(\"real test with \\\\\"escaped quotes\\\\\"\", function() {\n          expect(true).to.be.true;\n        });\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(true);\n    });\n\n    it('should handle multi-line comments correctly', () => {\n      const script = `\n        /*\n         * This is a multi-line comment with\n         * test(\"commented test\", function() {\n         *   expect(true).to.be.true;\n         * });\n         */\n        \n        test(\"real test\", function() {\n          expect(true).to.be.true;\n        });\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(true);\n    });\n\n    it('should handle inline comments correctly', () => {\n      const script = `\n        const data = res.data; // test(\"inline comment\")\n        test(\"real test\", function() { // this is a real test\n          expect(data).to.be.an(\"object\");\n        });\n      `;\n      expect(hasExecutableTestInScript(script)).toBe(true);\n    });\n  });\n\n  describe('edge cases', () => {\n    it('should handle test calls immediately after dots (edge case)', () => {\n      const script = `\n        // This should not match because it's after a dot\n        console.test(\"should not match\");\n        \n        // But this should match because there's a space\n        console. test(\"should match due to space\");\n      `;\n      // Note: Our current implementation would consider the second one valid\n      // because there's a space between the dot and test\n      expect(hasExecutableTestInScript(script)).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-cli/tests/utils/filesystem.spec.js",
    "content": "const { isLargeFile } = require('../../src/utils/filesystem');\nconst fs = require('fs-extra');\n\ndescribe('isLargeFile', () => {\n  let existsSyncSpy;\n  let lstatSyncSpy;\n  let statSyncSpy;\n\n  beforeEach(() => {\n    existsSyncSpy = jest.spyOn(fs, 'existsSync');\n    lstatSyncSpy = jest.spyOn(fs, 'lstatSync');\n    statSyncSpy = jest.spyOn(fs, 'statSync');\n  });\n\n  afterEach(() => {\n    jest.restoreAllMocks();\n  });\n\n  it('should return false when file size is below default threshold (10MB)', () => {\n    existsSyncSpy.mockReturnValue(true);\n    lstatSyncSpy.mockReturnValue({ isFile: () => true });\n    statSyncSpy.mockReturnValue({ size: 5 * 1024 * 1024 }); // 5MB\n\n    expect(isLargeFile('/path/small.bin')).toBe(false);\n  });\n\n  it('should return true when file size is above default threshold (10MB)', () => {\n    existsSyncSpy.mockReturnValue(true);\n    lstatSyncSpy.mockReturnValue({ isFile: () => true });\n    statSyncSpy.mockReturnValue({ size: 15 * 1024 * 1024 }); // 15MB\n\n    expect(isLargeFile('/path/large.bin')).toBe(true);\n  });\n\n  it('should respect custom threshold (args true or false)', () => {\n    existsSyncSpy.mockReturnValue(true);\n    lstatSyncSpy.mockReturnValue({ isFile: () => true });\n    statSyncSpy.mockReturnValue({ size: 50 });\n\n    expect(isLargeFile('/path/file.bin', 100)).toBe(false); // 50 < 100\n    expect(isLargeFile('/path/file.bin', 10)).toBe(true); // 50 > 10\n  });\n\n  it('should throw on invalid values (not a file)', () => {\n    existsSyncSpy.mockReturnValue(false);\n    lstatSyncSpy.mockReturnValue({ isFile: () => false });\n\n    expect(() => isLargeFile('/path/not-a-file.bin')).toThrow('File /path/not-a-file.bin is not a file');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-cli/tests/utils/parse-environment-json.spec.js",
    "content": "const { describe, it, expect } = require('@jest/globals');\nconst { getEnvVars } = require('../../src/utils/bru');\nconst { parseEnvironmentJson } = require('../../src/utils/environment');\n\ndescribe('parseEnvironmentJson', () => {\n  it('normalizes single environment object', () => {\n    const input = {\n      name: 'My Env',\n      variables: [\n        { name: 'host', value: 'https://www.httpfaker.org' },\n        { name: 'token', value: 'abc', enabled: false, secret: true }\n      ]\n    };\n    const env = parseEnvironmentJson(input);\n    expect(Array.isArray(env.variables)).toBe(true);\n    expect(env.variables[0]).toEqual({\n      name: 'host',\n      value: 'https://www.httpfaker.org',\n      type: 'text',\n      enabled: true,\n      secret: false\n    });\n    expect(env.variables[1].enabled).toBe(false);\n    expect(env.variables[1].secret).toBe(true);\n\n    const vars = getEnvVars(env);\n    expect(vars).toEqual({ host: 'https://www.httpfaker.org' });\n  });\n\n  it('throws on invalid shape', () => {\n    expect(() => parseEnvironmentJson({ name: 'x' })).toThrow(/Invalid environment JSON/i);\n  });\n\n  it('respects explicit fields and preserves secret flag', () => {\n    const input = {\n      name: 'My Env',\n      variables: [\n        { name: 'one', value: '1', type: 'text', enabled: true, secret: true },\n        { name: 'two', value: '2', type: 'file', enabled: false, secret: false }\n      ]\n    };\n    const env = parseEnvironmentJson(input);\n\n    expect(env.variables[0]).toEqual({\n      name: 'one',\n      value: '1',\n      type: 'text',\n      enabled: true,\n      secret: true\n    });\n    expect(env.variables[1]).toEqual({\n      name: 'two',\n      value: '2',\n      type: 'file',\n      enabled: false,\n      secret: false\n    });\n\n    const vars = getEnvVars(env);\n    expect(vars).toEqual({ one: '1' });\n  });\n\n  it('defaults secret to false for undefined and null', () => {\n    const input = {\n      name: 'My Env',\n      variables: [\n        { name: 'three', value: '3', enabled: true },\n        { name: 'four', value: '4', enabled: true, secret: null }\n      ]\n    };\n    const env = parseEnvironmentJson(input);\n\n    expect(env.variables[0]).toEqual({\n      name: 'three',\n      value: '3',\n      type: 'text',\n      enabled: true,\n      secret: false\n    });\n    expect(env.variables[1]).toEqual({\n      name: 'four',\n      value: '4',\n      type: 'text',\n      enabled: true,\n      secret: false\n    });\n\n    const vars = getEnvVars(env);\n    expect(vars).toEqual({ three: '3', four: '4' });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-common/.gitignore",
    "content": "# dependencies\nnode_modules\nyarn.lock\npnpm-lock.yaml\npackage-lock.json\n.pnp\n.pnp.js\n\n# testing\ncoverage\n\n# production\ndist\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "packages/bruno-common/babel.config.js",
    "content": "module.exports = {\n  presets: [\n    ['@babel/preset-env', { modules: 'auto' }],\n    '@babel/preset-typescript',\n  ],\n};\n"
  },
  {
    "path": "packages/bruno-common/jest.config.js",
    "content": "module.exports = {\n  transform: {\n    '^.+\\\\.(ts|js)$': 'babel-jest',\n  },\n  transformIgnorePatterns: [\n    '/node_modules/(?!(lodash-es|is-ip|ip-regex|super-regex|function-timeout|time-span|convert-hrtime|clone-regexp|is-regexp)/)'\n  ],\n  testEnvironment: 'node'\n};\n"
  },
  {
    "path": "packages/bruno-common/license.md",
    "content": "MIT License\n\nCopyright (c) 2024 Anoop M D, Anusree P S and Contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/bruno-common/package.json",
    "content": "{\n  \"name\": \"@usebruno/common\",\n  \"version\": \"0.1.0\",\n  \"license\": \"MIT\",\n  \"main\": \"dist/cjs/index.js\",\n  \"module\": \"dist/esm/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"require\": \"./dist/cjs/index.js\",\n      \"import\": \"./dist/esm/index.js\",\n      \"types\": \"./dist/index.d.ts\"\n    },\n    \"./runner\": {\n      \"require\": \"./dist/runner/cjs/index.js\",\n      \"import\": \"./dist/runner/esm/index.js\",\n      \"types\": \"./dist/runner/index.d.ts\"\n    },\n    \"./utils\": {\n      \"require\": \"./dist/utils/cjs/index.js\",\n      \"import\": \"./dist/utils/esm/index.js\",\n      \"types\": \"./dist/utils/index.d.ts\"\n    }\n  },\n  \"files\": [\n    \"dist\",\n    \"src\",\n    \"package.json\"\n  ],\n  \"scripts\": {\n    \"clean\": \"rimraf dist\",\n    \"test\": \"jest\",\n    \"test:watch\": \"jest --watch\",\n    \"prebuild\": \"npm run clean\",\n    \"build\": \"rollup -c rollup.config.js\",\n    \"watch\": \"rollup -c -w\",\n    \"prepack\": \"npm run test && npm run build\"\n  },\n  \"devDependencies\": {\n    \"@babel/preset-env\": \"^7.26.9\",\n    \"@babel/preset-typescript\": \"^7.27.0\",\n    \"@faker-js/faker\": \"^9.7.0\",\n    \"@jest/globals\": \"^29.7.0\",\n    \"@rollup/plugin-commonjs\": \"^23.0.2\",\n    \"@rollup/plugin-node-resolve\": \"^15.0.1\",\n    \"@rollup/plugin-typescript\": \"^12.1.2\",\n    \"@types/jest\": \"^29.5.14\",\n    \"babel-jest\": \"^29.7.0\",\n    \"form-data\": \"^4.0.0\",\n    \"is-ip\": \"^5.0.1\",\n    \"moment\": \"^2.29.4\",\n    \"rollup\": \"3.29.5\",\n    \"rollup-plugin-dts\": \"^5.0.0\",\n    \"rollup-plugin-peer-deps-external\": \"^2.2.4\",\n    \"rollup-plugin-terser\": \"^7.0.2\",\n    \"typescript\": \"^5.8.3\"\n  },\n  \"overrides\": {\n    \"rollup\": \"3.29.5\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-common/readme.md",
    "content": "# bruno-common\n\nA collection of common utilities used across Bruno App, Electron and CLI packages.\n\n### Publish to Npm Registry\n\n```bash\nnpm publish --access=public\n```\n"
  },
  {
    "path": "packages/bruno-common/rollup.config.js",
    "content": "const { nodeResolve } = require('@rollup/plugin-node-resolve');\nconst commonjs = require('@rollup/plugin-commonjs');\nconst typescript = require('@rollup/plugin-typescript');\nconst dts = require('rollup-plugin-dts');\nconst { terser } = require('rollup-plugin-terser');\nconst peerDepsExternal = require('rollup-plugin-peer-deps-external');\n\nconst packageJson = require('./package.json');\n\nfunction createBuildConfig({ inputDir, input, cjsOutput, esmOutput }) {\n  return [\n    {\n      input,\n      output: [\n        {\n          file: cjsOutput,\n          format: 'cjs',\n          sourcemap: true\n        },\n        {\n          file: esmOutput,\n          format: 'esm',\n          sourcemap: true\n        }\n      ],\n      plugins: [\n        peerDepsExternal(),\n        nodeResolve(),\n        commonjs(),\n        typescript({\n          tsconfig: './tsconfig.json',\n          include: [inputDir]\n        }),\n        terser()\n      ],\n      treeshake: {\n        moduleSideEffects: false\n      }\n    }\n  ];\n}\n\n// todo: configure declarations\nmodule.exports = [\n  // Main package build\n  ...createBuildConfig({\n    inputDir: 'src/**/*',\n    input: 'src/index.ts',\n    cjsOutput: packageJson.main,\n    esmOutput: packageJson.module\n  }),\n  // reports/html\n  ...createBuildConfig({\n    inputDir: 'src/runner/**/*',\n    input: 'src/runner/index.ts',\n    cjsOutput: 'dist/runner/cjs/index.js',\n    esmOutput: 'dist/runner/esm/index.js'\n  }),\n  ...createBuildConfig({\n    inputDir: 'src/utils/**/*',\n    input: 'src/utils/index.ts',\n    cjsOutput: 'dist/utils/cjs/index.js',\n    esmOutput: 'dist/utils/esm/index.js'\n  })\n];\n"
  },
  {
    "path": "packages/bruno-common/src/example-status/index.ts",
    "content": "import each from 'lodash/each';\nimport get from 'lodash/get';\n\ninterface Collection {\n  items?: any[];\n  [key: string]: any;\n}\n\n/**\n * Backward compatibility: Convert string status to number in examples\n * Old collections exported before the fix had status as string\n * This function ensures status is always a number for schema validation\n */\nexport const transformExampleStatusInCollection = (collection: Collection | Collection[]): Collection => {\n  const transformItems = (items: any[] = []) => {\n    each(items, (item) => {\n      const examples = item.examples;\n      if (examples && Array.isArray(examples)) {\n        each(examples, (example) => {\n          if (example.response && typeof example.response.status === 'string') {\n            const statusValue = example.response.status;\n            // Convert string status to number, default to null if conversion fails\n            example.response.status = statusValue ? Number(statusValue) : null;\n          }\n        });\n      }\n\n      if (item.items && item.items.length) {\n        transformItems(item.items);\n      }\n    });\n  };\n\n  if (Array.isArray(collection)) {\n    collection.forEach((col) => transformItems(col.items));\n  } else {\n    transformItems(collection.items);\n  }\n\n  return collection;\n};\n"
  },
  {
    "path": "packages/bruno-common/src/index.ts",
    "content": "export { mockDataFunctions, timeBasedDynamicVars } from './utils/faker-functions';\nexport { default as interpolate, interpolateObject } from './interpolate';\nexport { percentageToZoomLevel } from './zoom';\nexport { default as isRequestTagsIncluded } from './tags';\nexport { transformExampleStatusInCollection } from './example-status';\n\nexport * as utils from './utils';\n"
  },
  {
    "path": "packages/bruno-common/src/interpolate/index.spec.ts",
    "content": "import interpolate, { interpolateObject } from './index';\nimport moment from 'moment';\n\nconst BRUNO_BIRTH_DATE = new Date('2019-08-08');\n\nconst calculateAgeFromBirthDate = (birthDate = BRUNO_BIRTH_DATE) => {\n  const today = new Date();\n  let age = today.getFullYear() - birthDate.getFullYear();\n\n  const hasBirthdayPassedThisYear\n    = today.getMonth() > birthDate.getMonth()\n      || (today.getMonth() === birthDate.getMonth() && today.getDate() >= birthDate.getDate());\n\n  if (!hasBirthdayPassedThisYear) {\n    age--;\n  }\n\n  return age;\n};\n\nconst BRUNO_AGE = calculateAgeFromBirthDate(BRUNO_BIRTH_DATE);\n\ndescribe('interpolate', () => {\n  it('should replace placeholders with values from the object', () => {\n    const inputString = 'Hello, my name is {{user.name}} and I am {{user.age}} years old';\n    const inputObject = {\n      'user.name': 'Bruno',\n      'user': {\n        age: BRUNO_AGE\n      }\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe(`Hello, my name is Bruno and I am ${BRUNO_AGE} years old`);\n  });\n\n  it('should handle missing values by leaving the placeholders unchanged using {{}} as delimiters', () => {\n    const inputString = 'Hello, my name is {{user.name}} and I am {{user.age}} years old';\n    const inputObject = {\n      user: {\n        name: 'Bruno'\n      }\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe('Hello, my name is Bruno and I am {{user.age}} years old');\n  });\n\n  it('should handle all valid keys', () => {\n    const inputObject = {\n      user: {\n        'full_name': 'Bruno',\n        'age': BRUNO_AGE,\n        'fav-food': ['egg', 'meat'],\n        'want.attention': true\n      }\n    };\n    const inputStr = `\n  Hi, I am {{user.full_name}},\n  I am {{user.age}} years old.\n  My favorite food is {{user.fav-food[0]}} and {{user.fav-food[1]}}.\n  I like attention: {{user['want.attention']}}\n`;\n    const expectedStr = `\n  Hi, I am Bruno,\n  I am ${BRUNO_AGE} years old.\n  My favorite food is egg and meat.\n  I like attention: true\n`;\n    const result = interpolate(inputStr, inputObject);\n    expect(result).toBe(expectedStr);\n  });\n\n  it('should strictly match the keys (whitespace matters)', () => {\n    const inputString = 'Hello, my name is {{ user.name }} and I am {{user.age}} years old';\n    const inputObject = {\n      'user.name': 'Bruno',\n      'user': {\n        age: BRUNO_AGE\n      }\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe(`Hello, my name is {{ user.name }} and I am ${BRUNO_AGE} years old`);\n  });\n\n  test('should give precedence to the last key in case of duplicates (not at the top level)', () => {\n    const inputString = `Hello, my name is {{data['user.name']}} and {{data.user.name}} I am {{data.user.age}} years old`;\n    const inputObject = {\n      data: {\n        'user.name': 'Bruno',\n        'user': {\n          name: 'Not _Bruno_',\n          age: BRUNO_AGE\n        }\n      }\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe(`Hello, my name is Bruno and Not _Bruno_ I am ${BRUNO_AGE} years old`);\n  });\n});\n\ndescribe('interpolate - template edge cases', () => {\n  it('should return the input string if the template is not a string', () => {\n    const inputString = 123;\n    const inputObject = {\n      user: 'Bruno'\n    };\n\n    const result = interpolate(inputString as any, inputObject);\n    expect(result).toBe(inputString);\n  });\n\n  it('should return the input string if the template is null', () => {\n    const inputString = null;\n    const inputObject = {\n      user: 'Bruno'\n    };\n\n    const result = interpolate(inputString as any, inputObject);\n    expect(result).toBe(inputString);\n  });\n\n  it('should return the input string if the template is undefined', () => {\n    const inputString = undefined;\n    const inputObject = {\n      user: 'Bruno'\n    };\n\n    const result = interpolate(inputString as any, inputObject);\n    expect(result).toBe(inputString);\n  });\n\n  it('should return the input string if the template is empty', () => {\n    const inputString = '';\n    const inputObject = {\n      user: 'Bruno'\n    };\n\n    const result = interpolate(inputString, inputObject);\n    expect(result).toBe(inputString);\n  });\n\n  it('should return preserve whitespaces', () => {\n    const inputString = '    ';\n    const inputObject = {\n      user: 'Bruno'\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe(inputString);\n  });\n});\n\ndescribe('interpolate - value edge cases', () => {\n  it('should return the input string if the value is not an object', () => {\n    const inputString = 'Hello, my name is {{user.name}}';\n    const inputObject = 123;\n\n    const result = interpolate(inputString, inputObject as any);\n    expect(result).toBe(inputString);\n  });\n\n  it('should return the input string if the value is null', () => {\n    const inputString = 'Hello, my name is {{user.name}}';\n    const inputObject = null;\n\n    const result = interpolate(inputString, inputObject as any);\n    expect(result).toBe(inputString);\n  });\n\n  it('should return the input string if the value is undefined', () => {\n    const inputString = 'Hello, my name is {{user.name}}';\n    const inputObject = undefined;\n\n    const result = interpolate(inputString, inputObject as any);\n    expect(result).toBe(inputString);\n  });\n\n  it('should return the input string if the value is empty', () => {\n    const inputString = 'Hello, my name is {{user.name}}';\n    const inputObject = {};\n\n    const result = interpolate(inputString, inputObject);\n    expect(result).toBe(inputString);\n  });\n});\n\ndescribe('interpolate - recursive', () => {\n  it('should replace placeholders with 1 level of recursion with values from the object', () => {\n    const inputString = '{{user.message}}';\n    const inputObject = {\n      'user.message': 'Hello, my name is {{user.name}} and I am {{user.age}} years old',\n      'user.name': 'Bruno',\n      'user': {\n        age: BRUNO_AGE\n      }\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe(`Hello, my name is Bruno and I am ${BRUNO_AGE} years old`);\n  });\n\n  it('should replace placeholders with 2 level of recursion with values from the object', () => {\n    const inputString = '{{user.message}}';\n    const inputObject = {\n      'user.message': 'Hello, my name is {{user.name}} and I am {{user.age}} years old',\n      'user.name': 'Bruno {{user.lastName}}',\n      'user.lastName': 'Dog',\n      'user': {\n        age: BRUNO_AGE\n      }\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe(`Hello, my name is Bruno Dog and I am ${BRUNO_AGE} years old`);\n  });\n\n  it('should replace placeholders with 3 level of recursion with values from the object', () => {\n    const inputString = '{{user.message}}';\n    const inputObject = {\n      'user.message': 'Hello, my name is {{user.full_name}} and I am {{user.age}} years old',\n      'user.full_name': '{{user.name}}',\n      'user.name': 'Bruno {{user.lastName}}',\n      'user.lastName': 'Dog',\n      'user': {\n        age: BRUNO_AGE\n      }\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe(`Hello, my name is Bruno Dog and I am ${BRUNO_AGE} years old`);\n  });\n\n  it('should handle missing values with 1 level of recursion by leaving the placeholders unchanged using {{}} as delimiters', () => {\n    const inputString = '{{user.message}}';\n    const inputObject = {\n      'user.message': 'Hello, my name is {{user.name}} and I am {{user.age}} years old',\n      'user': {\n        age: BRUNO_AGE\n      }\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe(`Hello, my name is {{user.name}} and I am ${BRUNO_AGE} years old`);\n  });\n\n  it('should handle all valid keys with 1 level of recursion', () => {\n    const message = `\n  Hi, I am {{user.full_name}},\n  I am {{user.age}} years old.\n  My favorite food is {{user.fav-food[0]}} and {{user.fav-food[1]}}.\n  I like attention: {{user['want.attention']}}\n`;\n    const inputObject = {\n      user: {\n        message,\n        'full_name': 'Bruno',\n        'age': BRUNO_AGE,\n        'fav-food': ['egg', 'meat'],\n        'want.attention': true\n      }\n    };\n\n    const inputStr = '{{user.message}}';\n    const expectedStr = `\n  Hi, I am Bruno,\n  I am ${BRUNO_AGE} years old.\n  My favorite food is egg and meat.\n  I like attention: true\n`;\n    const result = interpolate(inputStr, inputObject);\n    expect(result).toBe(expectedStr);\n  });\n\n  it('should not process 1 level of cycle recursion with values from the object', () => {\n    const inputString = '{{recursion}}';\n    const inputObject = {\n      recursion: '{{recursion}}'\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe('{{recursion}}');\n  });\n\n  it('should not process 2 level of cycle recursion with values from the object', () => {\n    const inputString = '{{recursion}}';\n    const inputObject = {\n      recursion: '{{recursion2}}',\n      recursion2: '{{recursion}}'\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe('{{recursion2}}');\n  });\n\n  it('should not process 3 level of cycle recursion with values from the object', () => {\n    const inputString = '{{recursion}}';\n    const inputObject = {\n      recursion: '{{recursion2}}',\n      recursion2: '{{recursion3}}',\n      recursion3: '{{recursion}}'\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe('{{recursion3}}');\n  });\n\n  it('should replace repeated placeholders with 1 level of recursion with values from the object', () => {\n    const inputString = '{{repeated}}';\n    const inputObject = {\n      repeated: '{{repeated2}} {{repeated2}}',\n      repeated2: 'repeated2'\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe(new Array(2).fill('repeated2').join(' '));\n  });\n\n  it('should replace repeated placeholders with 2 level of recursion with values from the object', () => {\n    const inputString = '{{repeated}}';\n    const inputObject = {\n      repeated: '{{repeated2}} {{repeated2}}',\n      repeated2: '{{repeated3}} {{repeated3}} {{repeated3}}',\n      repeated3: 'repeated3'\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe(new Array(6).fill('repeated3').join(' '));\n  });\n\n  it('should replace repeated placeholders with 3 level of recursion with values from the object', () => {\n    const inputString = '{{repeated}}';\n    const inputObject = {\n      repeated: '{{repeated2}} {{repeated2}}',\n      repeated2: '{{repeated3}} {{repeated3}} {{repeated3}}',\n      repeated3: '{{repeated4}} {{repeated4}} {{repeated4}} {{repeated4}}',\n      repeated4: 'repeated4'\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe(new Array(24).fill('repeated4').join(' '));\n  });\n\n  it('should replace multiple interdependent variables in the same input string', () => {\n    const inputString = `{\n      \"x\": \"{{v2}} {{v1}}\"\n    }`;\n    const inputObject = {\n      foo: 'bar',\n      v1: '{{foo}}',\n      v2: '{{bar}}',\n      bar: 'baz'\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe(`{\n      \"x\": \"baz bar\"\n    }`);\n  });\n\n  it('should replace variables pointing to mock data functions', () => {\n    const inputString = 'Timestamp: {{folderVar}}';\n    const inputObject = {\n      folderVar: '{{$isoTimestamp}}'\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    // Validate that the result is a valid ISO timestamp\n    const timestampPattern = /^Timestamp: \\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/;\n    expect(timestampPattern.test(result)).toBe(true);\n  });\n\n  it('should replace nested variables pointing to mock data functions', () => {\n    const inputString = 'Random values: {{var1}} and {{var2}}';\n    const inputObject = {\n      var1: '{{nestedVar}}',\n      nestedVar: '{{$randomInt}}',\n      var2: '{{$randomBoolean}}'\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    // Validate the result\n    const parts = result.split(' and ');\n    expect(parts.length).toBe(2);\n\n    const randomInt = parts[0].replace('Random values: ', '');\n    const randomBoolean = parts[1];\n\n    // Check if randomInt is a number\n    expect(!isNaN(Number(randomInt))).toBe(true);\n    expect(Number(randomInt)).toBeGreaterThanOrEqual(0);\n    expect(Number(randomInt)).toBeLessThanOrEqual(1000);\n\n    // Check if randomBoolean is a boolean\n    expect(['true', 'false'].includes(randomBoolean)).toBe(true);\n  });\n\n  it('should replace variables pointing to mock data functions with escapeJSONStrings option', () => {\n    const inputString = '{\"timestamp\": \"{{folderVar}}\"}';\n    const inputObject = {\n      folderVar: '{{$isoTimestamp}}'\n    };\n\n    const result = interpolate(inputString, inputObject, { escapeJSONStrings: true });\n\n    // Should produce valid JSON\n    expect(() => {\n      const parsed = JSON.parse(result);\n      // Validate that the timestamp is a valid ISO timestamp\n      const timestampPattern = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/;\n      expect(timestampPattern.test(parsed.timestamp)).toBe(true);\n    }).not.toThrow();\n  });\n});\n\ndescribe('interpolate - object handling', () => {\n  it('should stringify simple objects', () => {\n    const inputString = 'User: {{user}}';\n    const inputObject = {\n      user: { name: 'Bruno', age: BRUNO_AGE }\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe(`User: {\"name\":\"Bruno\",\"age\":${BRUNO_AGE}}`);\n  });\n\n  it('should stringify simple objects (dot notation)', () => {\n    const inputString = 'User: {{user.data}}';\n    const inputObject = {\n      'user.data': { name: 'Bruno', age: BRUNO_AGE }\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe(`User: {\"name\":\"Bruno\",\"age\":${BRUNO_AGE}}`);\n  });\n\n  it('should stringify nested objects', () => {\n    const inputString = 'User: {{user}}';\n    const inputObject = {\n      user: {\n        name: 'Bruno',\n        age: BRUNO_AGE,\n        preferences: {\n          food: ['egg', 'meat'],\n          toys: { favorite: 'ball' }\n        }\n      }\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe(`User: {\"name\":\"Bruno\",\"age\":${BRUNO_AGE},\"preferences\":{\"food\":[\"egg\",\"meat\"],\"toys\":{\"favorite\":\"ball\"}}}`);\n  });\n\n  it('should stringify arrays', () => {\n    const inputString = 'User favorites: {{favorites}}';\n    const inputObject = {\n      favorites: ['egg', 'meat', 'treats']\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe('User favorites: [\"egg\",\"meat\",\"treats\"]');\n  });\n\n  it('should handle null values correctly', () => {\n    const inputString = 'User: {{user}}';\n    const inputObject = {\n      user: null\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe('User: null');\n  });\n\n  it('should handle objects with nested interpolation', () => {\n    const inputString = 'User: {{user}}';\n    const inputObject = {\n      'user': {\n        name: 'Bruno',\n        message: '{{user.greeting}}'\n      },\n      'user.greeting': 'Hello there!'\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe('User: {\"name\":\"Bruno\",\"message\":\"Hello there!\"}');\n  });\n\n  it('should handle objects within arrays', () => {\n    const inputString = 'Items: {{items}}';\n    const inputObject = {\n      items: [\n        { id: 1, name: 'Toy' },\n        { id: 2, name: 'Bone' },\n        { id: 3, name: 'Ball', colors: ['red', 'blue'] }\n      ]\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe('Items: [{\"id\":1,\"name\":\"Toy\"},{\"id\":2,\"name\":\"Bone\"},{\"id\":3,\"name\":\"Ball\",\"colors\":[\"red\",\"blue\"]}]');\n  });\n});\n\ndescribe('interpolate - mock variable interpolation', () => {\n  it('should replace mock variables with generated values', () => {\n    const inputString = '{{$randomInt}}, {{$randomIP}}, {{$randomIPV4}}, {{$randomIPV6}}, {{$randomBoolean}}';\n\n    const result = interpolate(inputString, {});\n\n    // Validate the result using regex patterns\n    const randomIntPattern = /^(?:[0-9]{1,2}|[1-9][0-9]{2}|1000)$/;\n    const randomIPPattern = /^([\\da-f]{1,4}:){7}[\\da-f]{1,4}$|^(\\d{1,3}\\.){3}\\d{1,3}$/;\n    const randomIPV4Pattern = /^(\\d{1,3}\\.){3}\\d{1,3}$/;\n    const randomIPV6Pattern = /^([\\da-f]{1,4}:){7}[\\da-f]{1,4}$/;\n    const randomBooleanPattern = /^(true|false)$/;\n\n    const [randomInt, randomIP, randomIPV4, randomIPV6, randomBoolean] = result.split(', ');\n\n    expect(randomIntPattern.test(randomInt)).toBe(true);\n    expect(randomIPPattern.test(randomIP)).toBe(true);\n    expect(randomIPV4Pattern.test(randomIPV4)).toBe(true);\n    expect(randomIPV6Pattern.test(randomIPV6)).toBe(true);\n    expect(randomBooleanPattern.test(randomBoolean)).toBe(true);\n  });\n\n  it('should leave mock variables unchanged if no corresponding function exists', () => {\n    const inputString = 'Random number: {{$nonExistentMock}}';\n\n    const result = interpolate(inputString, {});\n\n    expect(result).toBe('Random number: {{$nonExistentMock}}');\n  });\n\n  it('should escape special characters in mock variable values and produce valid JSON when escapeJSONStrings is true', () => {\n    const inputString = '{\"escapedValue\": \"{{$randomLoremParagraphs}}\"}';\n\n    expect(() => {\n      const result = interpolate(inputString, {}, { escapeJSONStrings: true });\n      JSON.parse(result); // This should not throw an error\n    }).not.toThrow();\n  });\n\n  it('should not produce valid JSON when escapeJSONStrings is false', () => {\n    const inputString = '{\"escapedValue\": \"{{$randomLoremParagraphs}}\"}';\n\n    expect(() => {\n      const result = interpolate(inputString, {}, { escapeJSONStrings: false });\n      JSON.parse(result); // This should throw an error\n    }).toThrow();\n  });\n\n  it('should throw an error when producing invalid JSON regardless of escapeJSONStrings option', () => {\n    const inputString = '{\"escapedValue\": \"{{$randomLoremParagraphs}}\"}';\n\n    // Test without providing the options argument\n    expect(() => {\n      const result = interpolate(inputString, {});\n      JSON.parse(result); // This should throw an error\n    }).toThrow();\n\n    // Test with escapeJSONStrings explicitly set to false\n    expect(() => {\n      const result = interpolate(inputString, {}, { escapeJSONStrings: false });\n      JSON.parse(result); // This should throw an error\n    }).toThrow();\n  });\n\n  it('should process mock variables in nested objects', () => {\n    const inputString = '{{user.data}}';\n    const inputObject = {\n      user: {\n        data: {\n          id: '{{$randomUUID}}',\n          timestamp: '{{$isoTimestamp}}',\n          nested: {\n            randomInt: '{{$randomInt}}'\n          }\n        }\n      }\n    };\n\n    const result = interpolate(inputString, inputObject);\n    const parsed = JSON.parse(result);\n\n    // Validate UUID format\n    const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n    expect(uuidPattern.test(parsed.id)).toBe(true);\n\n    // Validate ISO timestamp format\n    const isoTimestampPattern = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/;\n    expect(isoTimestampPattern.test(parsed.timestamp)).toBe(true);\n\n    // Validate nested randomInt\n    expect(!isNaN(Number(parsed.nested.randomInt))).toBe(true);\n    expect(Number(parsed.nested.randomInt)).toBeGreaterThanOrEqual(0);\n    expect(Number(parsed.nested.randomInt)).toBeLessThanOrEqual(1000);\n  });\n});\n\ndescribe('interpolate - Date() handling', () => {\n  it('should interpolate Date() using JSON.stringify', () => {\n    const inputString = 'Date is {{date}}';\n    const inputObject = {\n      date: new Date('2025-04-17T15:33:41.117Z')\n    };\n\n    const jsonStringifiedDate = JSON.stringify(inputObject.date);\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe('Date is \"2025-04-17T15:33:41.117Z\"');\n    expect(result).toBe(`Date is ${jsonStringifiedDate}`);\n  });\n\n  it('should interpolate Date() when its nested in an object', () => {\n    const inputString = 'Date is {{date}}';\n    const inputObject = {\n      date: {\n        now: new Date('2025-04-17T15:33:41.117Z')\n      }\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe('Date is {\"now\":\"2025-04-17T15:33:41.117Z\"}');\n  });\n});\n\ndescribe('interpolate - moment() handling', () => {\n  it('should interpolate moment() using JSON.stringify', () => {\n    const inputString = 'Date is {{date}}';\n    const inputObject = {\n      date: moment('2025-04-17T15:33:41.117Z')\n    };\n\n    const jsonStringifiedDate = JSON.stringify(inputObject.date);\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe('Date is \"2025-04-17T15:33:41.117Z\"');\n    expect(result).toBe(`Date is ${jsonStringifiedDate}`);\n  });\n\n  it('should interpolate moment() when its nested in an object', () => {\n    const inputString = 'Date is {{date}}';\n    const inputObject = {\n      date: {\n        now: moment('2025-04-17T15:33:41.117Z')\n      }\n    };\n\n    const result = interpolate(inputString, inputObject);\n\n    expect(result).toBe('Date is {\"now\":\"2025-04-17T15:33:41.117Z\"}');\n  });\n});\n\ndescribe('interpolateObject', () => {\n  it('should interpolate strings in a flat object', () => {\n    const obj = {\n      url: '{{baseUrl}}/api/users',\n      name: '{{userName}}'\n    };\n    const variables = { baseUrl: 'https://api.example.com', userName: 'Bruno' };\n\n    const result = interpolateObject(obj, variables);\n\n    expect(result).toEqual({\n      url: 'https://api.example.com/api/users',\n      name: 'Bruno'\n    });\n  });\n\n  it('should interpolate strings in nested objects', () => {\n    const obj = {\n      request: {\n        url: '{{baseUrl}}/api',\n        headers: {\n          Authorization: 'Bearer {{token}}'\n        }\n      }\n    };\n    const variables = { baseUrl: 'https://api.example.com', token: 'abc123' };\n\n    const result = interpolateObject(obj, variables);\n\n    expect(result).toEqual({\n      request: {\n        url: 'https://api.example.com/api',\n        headers: {\n          Authorization: 'Bearer abc123'\n        }\n      }\n    });\n  });\n\n  it('should interpolate strings in arrays', () => {\n    const obj = {\n      urls: ['{{baseUrl}}/one', '{{baseUrl}}/two']\n    };\n    const variables = { baseUrl: 'https://api.example.com' };\n\n    const result = interpolateObject(obj, variables);\n\n    expect(result).toEqual({\n      urls: ['https://api.example.com/one', 'https://api.example.com/two']\n    });\n  });\n\n  it('should preserve non-string values', () => {\n    const obj = {\n      name: '{{name}}',\n      age: 5,\n      active: true,\n      data: null\n    };\n    const variables = { name: 'Bruno' };\n\n    const result = interpolateObject(obj, variables);\n\n    expect(result).toEqual({\n      name: 'Bruno',\n      age: 5,\n      active: true,\n      data: null\n    });\n  });\n\n  it('should return null and undefined as-is', () => {\n    expect(interpolateObject(null, {})).toBeNull();\n    expect(interpolateObject(undefined, {})).toBeUndefined();\n  });\n\n  it('should throw on circular references', () => {\n    const obj: any = { a: 1 };\n    obj.self = obj;\n\n    expect(() => interpolateObject(obj, {})).toThrow('Circular reference detected during interpolation.');\n  });\n\n  it('should handle shared object references without throwing false positives', () => {\n    const shared = { value: '{{sharedValue}}' };\n    const obj = {\n      x: shared,\n      y: shared\n    };\n    const variables = { sharedValue: 'test' };\n\n    const result = interpolateObject(obj, variables);\n\n    expect(result).toEqual({\n      x: { value: 'test' },\n      y: { value: 'test' }\n    });\n  });\n\n  it('should handle shared object references in arrays', () => {\n    const shared = { id: '{{id}}' };\n    const obj = {\n      items: [shared, shared, shared]\n    };\n    const variables = { id: '123' };\n\n    const result = interpolateObject(obj, variables);\n\n    expect(result).toEqual({\n      items: [{ id: '123' }, { id: '123' }, { id: '123' }]\n    });\n  });\n\n  it('should handle shared object references in nested structures', () => {\n    const shared = { name: '{{name}}' };\n    const obj = {\n      user: shared,\n      profile: {\n        user: shared,\n        metadata: {\n          user: shared\n        }\n      }\n    };\n    const variables = { name: 'Bruno' };\n\n    const result = interpolateObject(obj, variables);\n\n    expect(result).toEqual({\n      user: { name: 'Bruno' },\n      profile: {\n        user: { name: 'Bruno' },\n        metadata: {\n          user: { name: 'Bruno' }\n        }\n      }\n    });\n  });\n\n  it('should handle shared array references', () => {\n    const shared = ['{{item1}}', '{{item2}}'];\n    const obj = {\n      list1: shared,\n      list2: shared\n    };\n    const variables = { item1: 'a', item2: 'b' };\n\n    const result = interpolateObject(obj, variables);\n\n    expect(result).toEqual({\n      list1: ['a', 'b'],\n      list2: ['a', 'b']\n    });\n  });\n\n  it('should still detect actual circular references', () => {\n    const obj: any = {\n      a: { value: '{{val}}' },\n      b: { value: '{{val}}' }\n    };\n    obj.a.circular = obj.a; // Circular reference\n\n    expect(() => interpolateObject(obj, { val: 'test' })).toThrow('Circular reference detected during interpolation.');\n  });\n\n  it('should handle deeply nested circular references', () => {\n    const obj: any = {\n      level1: {\n        level2: {\n          level3: {}\n        }\n      }\n    };\n    obj.level1.level2.level3.circular = obj.level1;\n\n    expect(() => interpolateObject(obj, {})).toThrow('Circular reference detected during interpolation.');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-common/src/interpolate/index.ts",
    "content": "/**\n * The interpolation function expects a string with placeholders and an object with the values to replace the placeholders.\n * The keys passed can have dot notation too.\n *\n * Ex: interpolate('Hello, my name is ${user.name} and I am ${user.age} years old', {\n *  \"user.name\": \"Bruno\",\n *  \"user\": {\n *   \"age\": 6\n *  }\n * });\n * Output: Hello, my name is Bruno and I am 6 years old\n */\n\nimport { mockDataFunctions } from '../utils/faker-functions';\nimport { get, isPlainObject, mapValues } from 'lodash-es';\n\n// regex to match {{$keyword}}\nconst MOCK_PATTERN = /\\{\\{\\$(\\w+)\\}\\}/g;\nconst JSON_SPECIAL_CHARS = /[\\\\\\n\\r\\t\\\"]/;\n\nconst escapeJSONString = (str: string): string => {\n  if (!JSON_SPECIAL_CHARS.test(str)) {\n    return str;\n  }\n\n  return str\n    .replace(/\\\\/g, '\\\\\\\\')\n    .replace(/\\n/g, '\\\\n')\n    .replace(/\\r/g, '\\\\r')\n    .replace(/\\t/g, '\\\\t')\n    .replace(/\\\"/g, '\\\\\"');\n};\n\nconst prepareMock = (str: string, escapeJSONStrings: boolean): string => {\n  return str.replace(MOCK_PATTERN, (match, keyword) => {\n    let generatedValue = mockDataFunctions[keyword as keyof typeof mockDataFunctions]?.();\n\n    if (generatedValue === undefined) {\n      return match;\n    }\n\n    generatedValue = String(generatedValue);\n\n    return escapeJSONStrings ? escapeJSONString(generatedValue) : generatedValue;\n  });\n};\n\nconst prepareMockObj = (\n  obj: Record<string, any>,\n  escapeJSONStrings: boolean\n): Record<string, any> => {\n  const processed: Record<string, any> = {};\n\n  for (const [key, value] of Object.entries(obj)) {\n    if (typeof value === 'string') {\n      processed[key] = prepareMock(value, escapeJSONStrings);\n    } else if (isPlainObject(value)) {\n      // plain object is used to skip special objects like Date, RegExp, etc.\n      processed[key] = prepareMockObj(value, escapeJSONStrings);\n    } else {\n      processed[key] = value;\n    }\n  }\n\n  return processed;\n};\n\nconst interpolate = (\n  str: string,\n  obj: Record<string, any>,\n  options: { escapeJSONStrings?: boolean } = { escapeJSONStrings: false }\n): string => {\n  if (!str || typeof str !== 'string') {\n    return str;\n  }\n\n  const { escapeJSONStrings } = options;\n\n  const preparedStr = prepareMock(str, escapeJSONStrings ?? false);\n\n  if (!obj || typeof obj !== 'object') {\n    return preparedStr;\n  }\n  // process the object with the mock data functions\n  const preparedObj = prepareMockObj(obj, escapeJSONStrings ?? false);\n  return replace(preparedStr, preparedObj);\n};\n\nconst replace = (\n  str: string,\n  obj: Record<string, any>,\n  visited = new Set<string>(),\n  results = new Map<string, string>()\n): string => {\n  let resultStr = str;\n  let matchFound = true;\n\n  while (matchFound) {\n    const patternRegex = /\\{\\{([^}]+)\\}\\}/g;\n    matchFound = false;\n    resultStr = resultStr.replace(patternRegex, (match, placeholder) => {\n      let replacement = get(obj, placeholder);\n      if (typeof replacement === 'object' && replacement !== null) {\n        replacement = JSON.stringify(replacement);\n      }\n\n      if (results.has(match)) {\n        return results.get(match);\n      }\n\n      if (patternRegex.test(replacement) && !visited.has(match)) {\n        visited.add(match);\n        const result = replace(replacement, obj, visited, results);\n        results.set(match, result);\n\n        matchFound = true;\n        return result;\n      }\n\n      visited.add(match);\n      const result = replacement !== undefined ? replacement : match;\n      results.set(match, result);\n\n      matchFound = true;\n      return result;\n    });\n  }\n\n  return resultStr;\n};\n\nexport const interpolateObject = (obj: unknown, variables: Record<string, any>): unknown => {\n  const seen = new WeakSet<object>();\n  const walk = (value: unknown): unknown => {\n    if (value == null) return value;\n    if (typeof value === 'string') {\n      return interpolate(value, variables);\n    }\n    if (typeof value === 'object') {\n      if (seen.has(value as object)) {\n        throw new Error('Circular reference detected during interpolation.');\n      }\n      seen.add(value as object);\n      try {\n        if (Array.isArray(value)) {\n          return value.map(walk);\n        }\n        if (isPlainObject(value)) {\n          return mapValues(value as Record<string, unknown>, walk);\n        }\n        return value;\n      } finally {\n        seen.delete(value as object);\n      }\n    }\n    return value;\n  };\n  return walk(obj);\n};\n\nexport default interpolate;\n"
  },
  {
    "path": "packages/bruno-common/src/runner/index.ts",
    "content": "import { generateHtmlReport } from './reports/html/generate-report';\nimport { getRunnerSummary } from './runner-summary';\n\nexport { generateHtmlReport, getRunnerSummary };\n"
  },
  {
    "path": "packages/bruno-common/src/runner/reports/html/generate-report.ts",
    "content": "import { T_RunnerResults } from '../../types';\nimport { isHtmlContentType, getContentType, redactImageData, encodeBase64 } from '../../utils';\nimport htmlTemplateString from './template';\n\nconst generateHtmlReport = ({\n  runnerResults,\n  version = '', // Default to empty string if not provided\n  environment = null, // Default environment if not provided\n  runCompletionTime = '' // Default run completion time if not provided\n}: {\n  runnerResults: T_RunnerResults[];\n  version?: string;\n  environment?: string | null;\n  runCompletionTime?: string;\n}): string => {\n  const resultsWithSummaryAndCleanData = runnerResults.map(({ iterationIndex, results, summary }) => {\n    return {\n      iterationIndex,\n      results: results.map((result) => {\n        const { request, response } = result || {};\n        const requestContentType = request?.headers ? getContentType(request?.headers) : '';\n        const responseContentType = response?.headers ? getContentType(response?.headers) : '';\n        return {\n          ...result,\n          request: {\n            ...result.request,\n            data: request?.data ? redactImageData(request?.data, requestContentType) : request?.data,\n            isHtml: isHtmlContentType(requestContentType)\n          },\n          response: {\n            ...result.response,\n            data: response?.data ? redactImageData(response?.data, responseContentType) : response?.data,\n            isHtml: isHtmlContentType(responseContentType)\n          }\n        };\n      }),\n      summary\n    };\n  });\n  const htmlString = htmlTemplateString(encodeBase64(JSON.stringify({\n    results: resultsWithSummaryAndCleanData,\n    version,\n    environment,\n    runCompletionTime\n  })));\n  return htmlString;\n};\n\nexport { generateHtmlReport };\n"
  },
  {
    "path": "packages/bruno-common/src/runner/reports/html/template.ts",
    "content": "export const htmlTemplateString = (resutsJsonString: string) => `<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <script src=\"https://unpkg.com/vue@3/dist/vue.global.js\"></script>\n    <!-- Would use latest version, you'd better specify a version -->\n    <script src=\"https://unpkg.com/naive-ui\"></script>\n\n    <title>Bruno</title>\n    <style>\n      .error > .status {\n        color: red;\n      }\n      .success > .status {\n        color: green;\n      }\n\n      .n-collapse-item.success > .n-collapse-item__header {\n        background-color: rgba(237, 247, 242, 1);\n      }\n      .n-collapse-item.error > .n-collapse-item__header {\n        background-color: rgba(251, 238, 241, 1);\n      }\n      .skipped > .status {\n        color: orange;\n      }\n\n      .min-width-150 {\n        min-width: 150px;\n      }\n\n      /* Metadata card styling - minimal custom styles */\n      .metadata-grid {\n        display: grid;\n        grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));\n        gap: 8px;\n        margin-top: 12px;\n      }\n\n      .metadata-item {\n        text-align: center;\n        padding: 6px 8px;\n        border-radius: 6px;\n        display: flex;\n        flex-direction: column;\n      }\n\n      .metadata-label {\n        font-size: 0.65rem;\n        text-transform: uppercase;\n        letter-spacing: 0.5px;\n        margin-bottom: 4px;\n        opacity: 0.7;\n      }\n\n      .metadata-value {\n        font-size: 0.8rem;\n        font-weight: normal;\n        word-wrap: break-word;\n        overflow-wrap: break-word;\n      }\n    </style>\n  </head>\n  <body>\n    <div id=\"app\">\n      <n-config-provider :theme=\"theme\">\n        <n-layout embedded position=\"absolute\" content-style=\"padding: 24px;\">\n          <n-card>\n            <n-flex>\n              <n-page-header title=\"Bruno run dashboard\">\n                <template #avatar>\n                  <n-avatar size=\"large\" style=\"background-color: transparent\">\n                    <svg id=\"emoji\" width=\"34\" viewBox=\"0 0 72 72\" xmlns=\"http://www.w3.org/2000/svg\">\n                      <g id=\"color\">\n                        <path\n                          fill=\"#F4AA41\"\n                          stroke=\"none\"\n                          d=\"M23.5,14.5855l-4.5,1.75l-7.25,8.5l-4.5,10.75l2,5.25c1.2554,3.7911,3.5231,7.1832,7.25,10l2.5-3.3333 c0,0,3.8218,7.7098,10.7384,8.9598c0,0,10.2616,1.936,15.5949-0.8765c3.4203-1.8037,4.4167-4.4167,4.4167-4.4167l3.4167-3.4167 l1.5833,2.3333l2.0833-0.0833l5.4167-7.25L64,37.3355l-0.1667-4.5l-2.3333-5.5l-4.8333-7.4167c0,0-2.6667-4.9167-8.1667-3.9167 c0,0-6.5-4.8333-11.8333-4.0833S32.0833,10.6688,23.5,14.5855z\"\n                        ></path>\n                        <polygon\n                          fill=\"#EA5A47\"\n                          stroke=\"none\"\n                          points=\"36,47.2521 32.9167,49.6688 30.4167,49.6688 30.3333,53.5021 31.0833,57.0021 32.1667,58.9188 35,60.4188 39.5833,59.8355 41.1667,58.0855 42.1667,53.8355 41.9167,49.8355 39.9167,50.0855\"\n                        ></polygon>\n                        <polygon\n                          fill=\"#3F3F3F\"\n                          stroke=\"none\"\n                          points=\"32.5,36.9188 30.9167,40.6688 33.0833,41.9188 34.3333,42.4188 38.6667,42.5855 41.5833,40.3355 39.8333,37.0855\"\n                        ></polygon>\n                      </g>\n                      <g id=\"hair\"></g>\n                      <g id=\"skin\"></g>\n                      <g id=\"skin-shadow\"></g>\n                      <g id=\"line\">\n                        <path\n                          fill=\"#000000\"\n                          stroke=\"none\"\n                          d=\"M29.5059,30.1088c0,0-1.8051,1.2424-2.7484,0.6679c-0.9434-0.5745-1.2424-1.8051-0.6679-2.7484 s1.805-1.2424,2.7484-0.6679S29.5059,30.1088,29.5059,30.1088z\"\n                        ></path>\n                        <path\n                          fill=\"none\"\n                          stroke=\"#000000\"\n                          stroke-linecap=\"round\"\n                          stroke-linejoin=\"round\"\n                          stroke-miterlimit=\"10\"\n                          stroke-width=\"2\"\n                          d=\"M33.1089,37.006h6.1457c0.4011,0,0.7634,0.2397,0.9203,0.6089l1.1579,2.7245l-2.1792,1.1456 c-0.6156,0.3236-1.3654-0.0645-1.4567-0.754\"\n                        ></path>\n                        <path\n                          fill=\"none\"\n                          stroke=\"#000000\"\n                          stroke-linecap=\"round\"\n                          stroke-linejoin=\"round\"\n                          stroke-miterlimit=\"10\"\n                          stroke-width=\"2\"\n                          d=\"M34.7606,40.763c-0.1132,0.6268-0.7757,0.9895-1.3647,0.7471l-2.3132-0.952l1.0899-2.9035 c0.1465-0.3901,0.5195-0.6486,0.9362-0.6486\"\n                        ></path>\n                        <path\n                          fill=\"none\"\n                          stroke=\"#000000\"\n                          stroke-linecap=\"round\"\n                          stroke-linejoin=\"round\"\n                          stroke-miterlimit=\"10\"\n                          stroke-width=\"2\"\n                          d=\"M30.4364,50.0268c0,0-0.7187,8.7934,3.0072,9.9375c2.6459,0.8125,5.1497,0.5324,6.0625-0.25 c0.875-0.75,2.6323-4.4741,1.8267-9.6875\"\n                        ></path>\n                        <path\n                          fill=\"#000000\"\n                          stroke=\"none\"\n                          d=\"M44.2636,30.1088c0,0,1.805,1.2424,2.7484,0.6679c0.9434-0.5745,1.2424-1.8051,0.6679-2.7484 c-0.5745-0.9434-1.805-1.2424-2.7484-0.6679C43.9881,27.9349,44.2636,30.1088,44.2636,30.1088z\"\n                        ></path>\n                        <path\n                          fill=\"none\"\n                          stroke=\"#000000\"\n                          stroke-linecap=\"round\"\n                          stroke-linejoin=\"round\"\n                          stroke-miterlimit=\"10\"\n                          stroke-width=\"2\"\n                          d=\"M25.6245,42.8393c-0.475,3.6024,2.2343,5.7505,4.2847,6.8414c1.1968,0.6367,2.6508,0.5182,3.7176-0.3181l2.581-2.0233l2.581,2.0233 c1.0669,0.8363,2.5209,0.9548,3.7176,0.3181c2.0504-1.0909,4.7597-3.239,4.2847-6.8414\"\n                        ></path>\n                        <path\n                          fill=\"none\"\n                          stroke=\"#000000\"\n                          stroke-linecap=\"round\"\n                          stroke-linejoin=\"round\"\n                          stroke-miterlimit=\"10\"\n                          stroke-width=\"2\"\n                          d=\"M19.9509,28.3572c-2.3166,5.1597-0.5084,13.0249,0.119,15.3759c0.122,0.4571,0.0755,0.9355-0.1271,1.3631l-1.9874,4.1937 c-0.623,1.3146-2.3934,1.5533-3.331,0.4409c-3.1921-3.7871-8.5584-11.3899-6.5486-16.686 c7.0625-18.6104,15.8677-18.1429,15.8677-18.1429c2.8453-1.9336,13.1042-6.9375,24.8125,0.875c0,0,8.6323-1.7175,14.9375,16.9375 c1.8036,5.3362-3.4297,12.8668-6.5506,16.6442c-0.9312,1.127-2.7162,0.8939-3.3423-0.4272l-1.9741-4.1656 c-0.2026-0.4275-0.2491-0.906-0.1271-1.3631c0.6275-2.3509,2.4356-10.2161,0.119-15.3759\"\n                        ></path>\n                        <path\n                          fill=\"none\"\n                          stroke=\"#000000\"\n                          stroke-linecap=\"round\"\n                          stroke-linejoin=\"round\"\n                          stroke-miterlimit=\"10\"\n                          stroke-width=\"2\"\n                          d=\"M52.6309,46.4628c0,0-3.0781,6.7216-7.8049,8.2712\"\n                        ></path>\n                        <path\n                          fill=\"none\"\n                          stroke=\"#000000\"\n                          stroke-linecap=\"round\"\n                          stroke-linejoin=\"round\"\n                          stroke-miterlimit=\"10\"\n                          stroke-width=\"2\"\n                          d=\"M19.437,46.969c0,0,3.0781,6.0823,7.8049,7.632\"\n                        ></path>\n                        <line\n                          x1=\"36.2078\"\n                          x2=\"36.2078\"\n                          y1=\"47.3393\"\n                          y2=\"44.3093\"\n                          fill=\"none\"\n                          stroke=\"#000000\"\n                          stroke-linecap=\"round\"\n                          stroke-linejoin=\"round\"\n                          stroke-miterlimit=\"10\"\n                          stroke-width=\"2\"\n                        ></line>\n                      </g>\n                    </svg>\n                  </n-avatar>\n                </template>\n                <template #extra>\n                  <n-flex justify=\"end\">\n                    <n-switch v-model:value=\"darkMode\" :rail-style=\"darkModeRailStyle\">\n                      <template #checked> Dark </template>\n                      <template #unchecked> Light </template>\n                    </n-switch>\n                  </n-flex>\n                </template>\n              </n-page-header>\n              <n-tabs type=\"segment\" animated v-model:value=\"currentTab\">\n                <n-tab-pane name=\"summary\" tab=\"Summary\">\n                  <n-flex justify=\"center\" vertical>\n                    <!-- Run Information Card using Naive UI components -->\n                    <n-card title=\"Run Information\" size=\"small\">\n                      <div class=\"metadata-grid\">\n                        <n-card class=\"metadata-item\" size=\"small\">\n                          <div class=\"metadata-label\">Date & Time</div>\n                          <div class=\"metadata-value\">{{ runCompletionTime }}</div>\n                        </n-card>\n                        <n-card class=\"metadata-item\" size=\"small\">\n                          <div class=\"metadata-label\">Version</div>\n                          <div class=\"metadata-value\">{{ brunoVersion }}</div>\n                        </n-card>\n                        <n-card class=\"metadata-item\" size=\"small\">\n                          <div class=\"metadata-label\">Environment</div>\n                          <div class=\"metadata-value\">{{ environment }}</div>\n                        </n-card>\n                        <n-card class=\"metadata-item\" size=\"small\">\n                          <div class=\"metadata-label\">Total run duration</div>\n                          <div class=\"metadata-value\">{{ totalDuration }}</div>\n                        </n-card>\n                        <n-card class=\"metadata-item\" size=\"small\">\n                          <div class=\"metadata-label\">Total data received</div>\n                          <div class=\"metadata-value\">{{ totalDataReceived }}</div>\n                        </n-card>\n                        <n-card class=\"metadata-item\" size=\"small\">\n                          <div class=\"metadata-label\">Average response time</div>\n                          <div class=\"metadata-value\">{{ averageResponseTime }}</div>\n                        </n-card>\n                      </div>\n                    </n-card>\n                    <x-summary v-for=\"(result, index) in res\" :res=\"result\" :key=\"index\"></x-summary>\n                  </n-flex>\n                </n-tab-pane>\n                <n-tab-pane name=\"requests\" tab=\"Requests\">\n                  <n-flex justify=\"center\" vertical>\n                    <x-requests v-for=\"(result, index) in res\" :res=\"result\" :key=\"index\"></x-requests>\n                  </n-flex>\n                </n-tab-pane>\n              </n-tabs>\n            </n-flex>\n          </n-card>\n        </n-layout>\n      </n-config-provider>\n    </div>\n    <script type=\"text/x-template\" id=\"summary-component\">\n      <n-flex vertical style=\"margin-bottom: 50px;\">\n        <n-card>\n          <template #header>\n            <span style=\"font-size: 24px;\">{{ iterationTitle }}</span>\n          </template>\n          <n-flex justify=\"center\">\n            <n-flex justify=\"center\">\n              <n-alert type=\"success\">\n                <n-statistic\n                  label=\"Total requests\"\n                  :value=\"summaryTotalRequests\"\n                >\n                </n-statistic>\n              </n-alert>\n              <n-alert :type=\"summaryErrors ? 'error' : 'success'\">\n                <n-statistic label=\"Total errors\" :value=\"summaryErrors\">\n                </n-statistic>\n              </n-alert>\n              <n-alert type=\"success\">\n                <n-statistic\n                  label=\"Total Controls\"\n                  :value=\"summaryTotalControls\"\n                >\n                </n-statistic>\n              </n-alert>\n              <n-alert :type=\"summaryFailedControls ? 'error' : 'success'\">\n                <n-statistic\n                  label=\"Total Failed Controls\"\n                  :value=\"summaryFailedControls\"\n                >\n                </n-statistic>\n              </n-alert>\n              <n-alert type=\"warning\" v-if=\"summarySkippedRequests\">\n                <n-statistic label=\"Skipped requests\" :value=\"summarySkippedRequests\">\n                </n-statistic>\n              </n-alert>\n            </n-flex>\n          </n-flex>\n        </n-card>\n        <n-data-table :columns=\"summaryColumns\" :data=\"summaryData\" />\n      </n-flex>\n    </script>\n    <script type=\"text/x-template\" id=\"requests-component\">\n      <n-card>\n        <template #header>\n          <span style=\"font-size: 24px;\">{{ iterationTitle }}</span>\n        </template>\n        <n-flex vertical style=\"margin-bottom: 50px\">\n          <n-switch\n            v-model:value=\"onlyFailed\"\n            :rail-style=\"railStyle\"\n          >\n            <template #checked> Only Failed </template>\n            <template #unchecked> Show All </template>\n          </n-switch>\n\n          <n-collapse>\n            <x-result v-for=\"(result, index) in results\" :result=\"result\" :key=\"results.length\"></x-result>\n          </n-collapse>\n        </n-flex>\n      </n-card>\n    </script>\n    <script type=\"text/x-template\" id=\"result-component\">\n      <n-collapse-item\n        :name=\"resultTitle\"\n        arrow-placement=\"right\"\n      >\n        <template #header>\n          <n-alert\n            :type=\"getAlertType\"\n            :bordered=\"false\"\n          >\n            <template #header>\n              {{result.path}} - {{result.response.status === 'skipped' ? 'Request Skipped' : (totalPassed + '/' + total + ' Passed')}} {{hasError && result.response.status !== 'skipped' ? \" - (request failed)\" : \"\" }}\n            </template>\n          </n-alert>\n        </template>\n        <n-flex vertical>\n          <n-grid x-gap=\"12\" :cols=\"2\">\n            <n-gi>\n              <n-card title=\"REQUEST INFORMATION\">\n                <n-list>\n                  <n-list-item>\n                    <n-thing\n                      title=\"File\"\n                      :description=\"result.path\"\n                    />\n                  </n-list-item>\n                  <n-list-item>\n                    <n-thing\n                      title=\"Request Method\"\n                      :description=\"result.request.method\"\n                    />\n                  </n-list-item>\n                  <n-list-item>\n                    <n-thing\n                      title=\"Request URL\"\n                      :description=\"result.request.url\"\n                    />\n                  </n-list-item>\n                </n-list>\n              </n-card>\n            </n-gi>\n            <n-gi>\n              <n-card title=\"RESPONSE INFORMATION\">\n                <n-list>\n                  <n-list-item>\n                    <n-thing\n                      title=\"Response Code\"\n                      :description=\"'' + result.response.status\"\n                    />\n                  </n-list-item>\n                  <n-list-item>\n                    <n-thing\n                      title=\"Response time\"\n                      :description=\"result.response.responseTime + ' ms'\"\n                    />\n                  </n-list-item>\n                  <n-list-item>\n                    <n-thing\n                      title=\"Test duration\"\n                      :description=\"testDuration\"\n                    />\n                  </n-list-item>\n                </n-list>\n              </n-card>\n            </n-gi>\n          </n-grid>\n          <n-alert v-if=\"hasError || (result.response.status === 'skipped' && result.error)\" title=\"Error\" type=\"error\">\n            {{result.error}}\n          </n-alert>\n          <n-card title=\"REQUEST HEADERS\">\n            <n-data-table\n              :columns=\"headerColumns\"\n              :data=\"headerDataRequest\"\n            />\n          </n-card>\n          <n-card\n            v-if=\"result.request.data\"\n            title=\"REQUEST BODY\"\n          >\n          <iframe\n            v-if=\"result.request.isHtml\"\n            :srcdoc=\"result.request.data\"\n            style=\"width: 100%; height: 400px; border: none;\"\n          ></iframe>\n\n          <pre v-else>{{ result.request.data }}</pre>\n          </n-card>\n          <n-card title=\"RESPONSE HEADERS\">\n            <n-data-table\n              :columns=\"headerColumns\"\n              :data=\"headerDataResponse\"\n            />\n          </n-card>\n          <n-card\n            v-if=\"result.response.data\"\n            title=\"RESPONSE BODY\"\n          >\n          <iframe\n            v-if=\"result.response.isHtml\"\n            :srcdoc=\"result.response.data\"\n            style=\"width: 100%; height: 400px; border: none;\"\n          ></iframe>\n\n          <pre v-else>{{ result.response.data }}</pre>          </n-card>\n          <n-card title=\"ASSERTIONS INFORMATION\">\n            <n-data-table\n              :columns=\"assertionsColumns\"\n              :data=\"result.assertionResults\"\n              :row-class-name=\"assertionsRowClassName\"\n            />\n          </n-card>\n          <n-card title=\"TESTS INFORMATION\">\n            <n-data-table\n              :columns=\"testsColumns\"\n              :data=\"result.testResults\"\n              :row-class-name=\"testsRowClassName\"\n            />\n          </n-card>\n        </n-flex>\n      </n-collapse-item>\n    </script>\n    <script>\n      const { createApp, ref, computed, onMounted } = Vue;\n\n      function mergeTests(runnerResults) {\n        if (!Array.isArray(runnerResults)) return runnerResults; \n\n        runnerResults.forEach(iteration => {\n          const { totalTests, passedTests, failedTests, totalPreRequestTests, passedPreRequestTests, failedPreRequestTests, totalPostResponseTests, passedPostResponseTests, failedPostResponseTests } = iteration.summary;\n          \n          // Merge summary test counts\n          iteration.summary.totalTests = totalTests + totalPreRequestTests + totalPostResponseTests;\n          iteration.summary.passedTests = passedTests + passedPreRequestTests + passedPostResponseTests;\n          iteration.summary.failedTests = failedTests + failedPreRequestTests + failedPostResponseTests;\n          \n          // Merge individual result test arrays\n          iteration.results.forEach(result => {\n            result.testResults = [\n              ...(result.preRequestTestResults || []),\n              ...(result.postResponseTestResults || []),\n              ...(result.testResults || [])\n            ];\n          }); \n        });\n        \n        return runnerResults;\n      }\n\n      const App = {\n        setup() {\n          function decodeBase64(base64) {\n            const binary = atob(base64);\n            const bytes = Uint8Array.from(binary, c => c.charCodeAt(0));\n            return new TextDecoder().decode(bytes);\n          }\n          const rawResults = JSON.parse(decodeBase64('${resutsJsonString}'));\n\n          const res = computed(() => {\n            return mergeTests(rawResults.results);\n          });\n\n          const brunoVersion = computed(() => {\n            return rawResults.version || '-';\n          });\n\n          const environment = computed(() => {\n            return rawResults.environment || '-';\n          });\n\n          const runCompletionTime = computed(() => {\n            if (rawResults.runCompletionTime) {\n              return new Date(rawResults.runCompletionTime).toLocaleString();\n            }\n            return '-';\n          });\n\n          const currentTab = ref('summary');\n\n          const getTabFromQueryParam = () => {\n            const urlParams = new URLSearchParams(window.location.search);\n            const tab = urlParams.get('tab');\n            return tab && ['summary', 'requests'].includes(tab) ? tab : 'summary';\n          };\n\n          onMounted(() => {\n            currentTab.value = getTabFromQueryParam();\n          });\n\n          const darkMode = ref(false);\n          const theme = computed(() => {\n            return darkMode.value ? naive.darkTheme : null;\n          });\n\n          const totalDuration = computed(() => {\n            const total = res.value.reduce((totalTime, iteration) => {\n              return totalTime + iteration.results.reduce((sum, result) => sum + (result.runDuration || 0), 0);\n            }, 0);\n            return total > 0 ? Math.round(total * 1000) / 1000 + 's' : '-';\n          });\n\n          const totalDataReceived = computed(() => {\n            const bytes = res.value.reduce((total, iteration) => {\n              return total + iteration.results.reduce((sum, result) => {\n                const responseData = result.response?.data;\n                if (typeof responseData === 'string') {\n                  return sum + new Blob([responseData]).size;\n                }\n                return sum + (JSON.stringify(responseData || {}).length || 0);\n              }, 0);\n            }, 0);\n            \n            if (bytes === 0) return '-';\n            if (bytes < 1024) return bytes + 'B';\n            if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + 'KB';\n            return (bytes / (1024 * 1024)).toFixed(2) + 'MB';\n          });\n\n          const averageResponseTime = computed(() => {\n            let totalTime = 0;\n            let count = 0;\n            \n            res.value.forEach(iteration => {\n              iteration.results.forEach(result => {\n                if (result.response?.responseTime) {\n                  totalTime += result.response.responseTime;\n                  count++;\n                }\n              });\n            });\n            \n            return count > 0 ? Math.round(totalTime / count) + 'ms' : '-';\n          });\n\n          if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {\n            darkMode.value = true;\n          }\n          // To watch for os theme changes\n          window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (event) => {\n            darkMode.value = event.matches;\n          });\n          return {\n            res,\n            theme,\n            darkMode,\n            darkModeRailStyle: () => ({ background: 'var(--n-rail-color)' }),\n            currentTab,\n            brunoVersion,\n            environment,\n            totalDuration,\n            totalDataReceived,\n            averageResponseTime,\n            runCompletionTime\n          };\n        }\n      };\n      const app = Vue.createApp(App);\n\n      app.component('x-summary', {\n        template: '#summary-component',\n        props: ['res'],\n        setup(props) {\n          const summaryColumns = [\n            {\n              title: 'SUMMARY ITEM',\n              key: 'title'\n            },\n            {\n              title: 'TOTAL',\n              key: 'total'\n            },\n            {\n              title: 'PASSED',\n              key: 'passed'\n            },\n            {\n              title: 'FAILED',\n              key: 'failed'\n            },\n            {\n              title: 'SKIPPED',\n              key: 'skipped'\n            },\n            {\n              title: 'ERROR',\n              key: 'error'\n            }\n          ];\n          const summaryData = computed(() => [\n            {\n              title: 'Requests',\n              total: props.res.summary.totalRequests,\n              passed: props.res.summary.passedRequests,\n              failed: props.res.summary.failedRequests,\n              skipped: props.res.summary.skippedRequests,\n              error: props.res.summary.errorRequests\n            },\n            {\n              title: 'Assertions',\n              total: props.res.summary.totalAssertions,\n              passed: props.res.summary.passedAssertions,\n              failed: props.res.summary.failedAssertions,\n              skipped: '-',\n              error: '-'\n            },\n            {\n              title: 'Tests',\n              total: props.res.summary.totalTests,\n              passed: props.res.summary.passedTests,\n              failed: props.res.summary.failedTests,\n              skipped: '-',\n              error: '-'\n            }\n          ]);\n          const summaryTotalRequests = computed(() => {\n            return props.res.summary.totalRequests;\n          });\n          const summaryTotalControls = computed(() => {\n            return props.res.summary.totalTests + props.res.summary.totalAssertions;\n          });\n          const summaryFailedControls = computed(\n            () => props.res.summary.failedTests + props.res.summary.failedAssertions\n          );\n          const summarySkippedRequests = computed(() => props?.res?.summary?.skippedRequests || 0);\n          const summaryErrors = computed(() => props?.res?.results?.filter((r) => r.error || r.status === 'error').length) || 0;\n          const totalRunDuration = computed(() => props.res?.results?.reduce((total, result) => result.runDuration + total, 0));\n          const iterationIndex = Number(props.res.iterationIndex) + 1;\n          return {\n            summaryColumns,\n            summaryData,\n            summaryTotalControls,\n            summaryTotalRequests,\n            summaryFailedControls,\n            summarySkippedRequests,\n            summaryErrors,\n            totalRunDuration,\n            iterationTitle: 'Iteration ' + iterationIndex\n          };\n        }\n      });\n\n      app.component('x-requests', {\n        template: '#requests-component',\n        props: ['res'],\n        setup(props) {\n          const onlyFailed = ref(false);\n          const filteredResults = computed(() => {\n            if (onlyFailed.value) {\n              return props?.res?.results?.filter(\n                (r) =>\n                  r.status === 'error' ||\n                  !!r?.testResults?.find((t) => t.status !== 'pass') ||\n                  !!r?.assertionResults?.find((t) => t.status !== 'pass')\n              );\n            }\n            return props.res.results;\n          });\n          const iterationIndex = Number(props.res.iterationIndex) + 1;\n          return {\n            onlyFailed,\n            results: filteredResults,\n            railStyle: ({ checked }) => {\n              const style = {};\n              if (checked) {\n                style.background = '#d03050';\n              }\n              return style;\n            },\n            iterationTitle: 'Iteration ' + iterationIndex\n          };\n        }\n      });\n\n      app.component('x-result', {\n        template: '#result-component',\n        props: ['result'],\n        setup(props) {\n          const headerColumns = [\n            {\n              title: 'Header Name',\n              key: 'name',\n              className: 'min-width-150'\n            },\n            {\n              title: 'Header Value',\n              key: 'value'\n            }\n          ];\n          const assertionsColumns = [\n            {\n              title: 'Expression',\n              key: 'lhsExpr'\n            },\n            {\n              title: 'Operator',\n              key: 'operator'\n            },\n            {\n              title: 'Operand',\n              key: 'rhsOperand'\n            },\n            {\n              title: 'Status',\n              key: 'status',\n              className: 'status'\n            },\n            {\n              title: 'Error',\n              key: 'error'\n            }\n          ];\n          const assertionsRowClassName = (row) => {\n            return row.status === 'fail' ? 'error' : 'success';\n          };\n          const testsRowClassName = (row) => {\n            if (row.status === 'skipped') return 'skipped';\n            return row.status === 'fail' ? 'error' : 'success';\n          };\n          const testsColumns = [\n            {\n              title: 'Description',\n              key: 'description'\n            },\n            {\n              title: 'Status',\n              key: 'status',\n              className: 'status'\n            },\n            {\n              title: 'Error',\n              key: 'error'\n            }\n          ];\n\n          function mapHeaderToTableData(headers) {\n            if (!headers) {\n              return [];\n            }\n            return Object.keys(headers).map((name) => ({\n              name,\n              value: headers[name]\n            }));\n          }\n          const headerDataRequest = computed(() => {\n            return mapHeaderToTableData(props?.result?.request?.headers);\n          });\n          const headerDataResponse = computed(() => {\n            return mapHeaderToTableData(props?.result?.response?.headers);\n          });\n          const totalPassed = computed(() => {\n            return (\n              (props?.result?.testResults?.filter((t) => t.status === 'pass').length || 0) +\n              (props?.result?.assertionResults?.filter((t) => t.status === 'pass').length || 0)\n            );\n          });\n          const total = computed(() => {\n            return (props?.result?.testResults?.length || 0) + (props?.result?.assertionResults?.length || 0);\n          });\n\n          const hasError = computed(() => !!props?.result?.error || props?.result?.status === 'error' || (props?.result?.response?.status === 'skipped' && props?.result?.error));\n          const hasFailure = computed(() => total.value !== totalPassed.value);\n          const testDuration = computed(() => Math.round(props?.result?.runDuration * 1000) + ' ms');\n          const resultTitle = computed(() => props?.result?.path + ' ' + props?.result?.response?.status + ' ' + props?.result?.response?.statusText);\n          const getAlertType = computed(() => {\n            if (props.result.response.status === 'skipped') {\n              return 'warning';\n            }\n            return hasError.value || hasFailure.value ? 'error' : 'success';\n          });\n          return {\n            headerColumns,\n            headerDataRequest,\n            headerDataResponse,\n            assertionsColumns,\n            assertionsRowClassName,\n            testsRowClassName,\n            totalPassed,\n            total,\n            hasFailure,\n            hasError,\n            testsColumns,\n            result: props.result,\n            testDuration,\n            resultTitle,\n            getAlertType,\n            iterationIndex: props?.result?.iterationIndex\n          };\n        }\n      });\n\n      app.use(naive);\n      app.mount('#app');\n    </script>\n  </body>\n</html>\n`;\n\nexport default htmlTemplateString;\n"
  },
  {
    "path": "packages/bruno-common/src/runner/runner-summary.ts",
    "content": "import { T_RunnerRequestExecutionResult, T_RunSummary } from './types';\n\n// todo: this is generic, not specific to html, can be moved out of the report/html sub-package\nexport const getRunnerSummary = (results: T_RunnerRequestExecutionResult[]): T_RunSummary => {\n  let totalRequests = 0;\n  let passedRequests = 0;\n  let failedRequests = 0;\n  let errorRequests = 0;\n  let skippedRequests = 0;\n  let totalAssertions = 0;\n  let passedAssertions = 0;\n  let failedAssertions = 0;\n  let totalTests = 0;\n  let passedTests = 0;\n  let failedTests = 0;\n  let totalPreRequestTests = 0;\n  let passedPreRequestTests = 0;\n  let failedPreRequestTests = 0;\n  let totalPostResponseTests = 0;\n  let passedPostResponseTests = 0;\n  let failedPostResponseTests = 0;\n\n  for (const result of results || []) {\n    const { status, testResults, assertionResults, preRequestTestResults, postResponseTestResults } = result;\n    totalRequests += 1;\n    totalTests += Number(testResults?.filter((r) => !r.isScriptError).length) || 0;\n    totalAssertions += Number(assertionResults?.length) || 0;\n    totalPreRequestTests += Number(preRequestTestResults?.filter((r) => !r.isScriptError).length) || 0;\n    totalPostResponseTests += Number(postResponseTestResults?.filter((r) => !r.isScriptError).length) || 0;\n\n    if (status === 'skipped') {\n      skippedRequests += 1;\n      continue;\n    }\n\n    let anyFailed = false;\n    for (const testResult of testResults || []) {\n      if (testResult.isScriptError) {\n        anyFailed = true;\n        continue;\n      }\n      if (testResult.status === 'pass') {\n        passedTests += 1;\n      } else {\n        anyFailed = true;\n        failedTests += 1;\n      }\n    }\n    for (const assertionResult of assertionResults || []) {\n      if (assertionResult.status === 'pass') {\n        passedAssertions += 1;\n      } else {\n        anyFailed = true;\n        failedAssertions += 1;\n      }\n    }\n    for (const preRequestTestResult of preRequestTestResults || []) {\n      if (preRequestTestResult.isScriptError) {\n        anyFailed = true;\n        continue;\n      }\n      if (preRequestTestResult.status === 'pass') {\n        passedPreRequestTests += 1;\n      } else {\n        anyFailed = true;\n        failedPreRequestTests += 1;\n      }\n    }\n    for (const postResponseTestResult of postResponseTestResults || []) {\n      if (postResponseTestResult.isScriptError) {\n        anyFailed = true;\n        continue;\n      }\n      if (postResponseTestResult.status === 'pass') {\n        passedPostResponseTests += 1;\n      } else {\n        anyFailed = true;\n        failedPostResponseTests += 1;\n      }\n    }\n\n    if (!anyFailed && status !== 'error') {\n      passedRequests += 1;\n    } else if (anyFailed) {\n      failedRequests += 1;\n    } else {\n      errorRequests += 1;\n    }\n  }\n\n  return {\n    totalRequests,\n    passedRequests,\n    failedRequests,\n    errorRequests,\n    skippedRequests,\n    totalAssertions,\n    passedAssertions,\n    failedAssertions,\n    totalTests,\n    passedTests,\n    failedTests,\n    totalPreRequestTests,\n    passedPreRequestTests,\n    failedPreRequestTests,\n    totalPostResponseTests,\n    passedPostResponseTests,\n    failedPostResponseTests\n  };\n};\n"
  },
  {
    "path": "packages/bruno-common/src/runner/types/index.ts",
    "content": "// assertion results types\ntype T_AssertionPassResult = {\n  lhsExpr: string;\n  rhsExpr: string;\n  rhsOperand: string;\n  operator: string;\n  status: string;\n};\n\ntype T_AssertionFailResult = {\n  lhsExpr: string;\n  rhsExpr: string;\n  rhsOperand: string;\n  operator: string;\n  status: string;\n  error: string;\n};\n\ntype T_AssertionResult = T_AssertionPassResult | T_AssertionFailResult;\n\n// test results types\ntype T_TestPassResult = {\n  status: string;\n  description: string;\n  uid?: string;\n  isScriptError?: boolean;\n};\n\ntype T_TestFailResult = {\n  status: string;\n  description: string;\n  error: string;\n  uid?: string;\n  isScriptError?: boolean;\n};\n\ntype T_TestResult = T_TestPassResult | T_TestFailResult;\n\ntype T_EmptyRequest = {\n  method?: null | undefined;\n  url?: null | undefined;\n  headers?: null | undefined;\n  data?: null | undefined;\n  isHtml?: boolean | undefined;\n};\n\n// request types\ntype T_Request = {\n  method: string;\n  url: string;\n  headers: Record<string, string | number | undefined>;\n  data: string | object | null | boolean | number;\n  isHtml?: boolean;\n};\n\ntype T_EmptyResponse = {\n  status?: null | undefined;\n  statusText?: null | undefined;\n  headers?: null | undefined;\n  data?: null | undefined;\n  responseTime?: number | undefined;\n  isHtml?: boolean | undefined;\n};\n\ntype T_SkippedResponse = {\n  status?: string | null | undefined;\n  statusText?: string | null | undefined;\n  headers?: null | undefined;\n  data?: null | undefined;\n  responseTime?: number | undefined;\n  isHtml?: boolean | undefined;\n};\n\n// response types\ntype T_Response = {\n  status: number | string;\n  statusText: string;\n  headers: Record<string, string | number | undefined>;\n  data: string | object | null | boolean | number;\n  isHtml?: boolean;\n};\n\n// result type\nexport type T_RunnerRequestExecutionResult = {\n  iterationIndex: number;\n  name: string;\n  path: string;\n  request: T_EmptyRequest | T_Request;\n  response: T_EmptyResponse | T_Response | T_SkippedResponse;\n  status: null | undefined | string;\n  error: null | undefined | string;\n  assertionResults?: T_AssertionResult[];\n  testResults?: T_TestResult[];\n  preRequestTestResults?: T_TestResult[];\n  postResponseTestResults?: T_TestResult[];\n  runDuration: number;\n};\n\nexport type T_RunnerResults = {\n  iterationIndex: number;\n  iterationData?: any; // todo - csv/json row data\n  results: T_RunnerRequestExecutionResult[];\n  summary: T_RunSummary;\n};\n\n// run summary type\nexport type T_RunSummary = {\n  totalRequests: number;\n  passedRequests: number;\n  failedRequests: number;\n  errorRequests: number;\n  skippedRequests: number;\n  totalAssertions: number;\n  passedAssertions: number;\n  failedAssertions: number;\n  totalTests: number;\n  passedTests: number;\n  failedTests: number;\n  totalPreRequestTests: number;\n  passedPreRequestTests: number;\n  failedPreRequestTests: number;\n  totalPostResponseTests: number;\n  passedPostResponseTests: number;\n  failedPostResponseTests: number;\n};\n"
  },
  {
    "path": "packages/bruno-common/src/runner/utils/index.ts",
    "content": "export const encodeBase64 = (str: string) => {\n  const bytes = new TextEncoder().encode(str);\n  const binary = bytes.reduce((acc, byte) => acc + String.fromCharCode(byte), '');\n  return btoa(binary);\n};\n\nexport const decodeBase64 = (base64: string) => {\n  const binary = atob(base64);\n  const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));\n  return new TextDecoder().decode(bytes);\n};\n\nexport const getContentType = (headers: Record<string, string | number | undefined>): string => {\n  if (!headers || typeof headers !== 'object') {\n    return '';\n  }\n  const contentType = Object.entries(headers)\n    .find(([key]) => key.toLowerCase() === 'content-type')?.[1];\n  return typeof contentType === 'string' ? contentType : '';\n};\n\nexport const isHtmlContentType = (contentType: string) => {\n  return contentType?.includes('html');\n};\n\nexport const redactImageData = (data: string | object | number | boolean, contentType: string) => {\n  if (contentType?.includes('image')) {\n    return 'Response content redacted (image data)';\n  }\n  return data;\n};\n"
  },
  {
    "path": "packages/bruno-common/src/tags/index.spec.ts",
    "content": "import isRequestTagsIncluded from './index';\n\ndescribe('isRequestTagsIncluded', () => {\n  it('should include request when it has an included tag', () => {\n    const requestTags = ['tag1', 'tag2'];\n    const includeTags = ['tag1'];\n    const excludeTags: string[] = [];\n    const result = isRequestTagsIncluded(requestTags, includeTags, excludeTags);\n    expect(result).toBe(true);\n  });\n\n  it('should include request when included tags is empty', () => {\n    const requestTags = ['tag1', 'tag2'];\n    const includeTags: string[] = [];\n    const excludeTags: string[] = [];\n    const result = isRequestTagsIncluded(requestTags, includeTags, excludeTags);\n    expect(result).toBe(true);\n  });\n\n  it('should exclude request when it does not have an included tag', () => {\n    const requestTags = ['tag1'];\n    const includeTags = ['tag2'];\n    const excludeTags: string[] = [];\n    const result = isRequestTagsIncluded(requestTags, includeTags, excludeTags);\n    expect(result).toBe(false);\n  });\n\n  it('should exclude request when it has an excluded tag', () => {\n    const requestTags = ['tag1'];\n    const includeTags: string[] = [];\n    const excludeTags = ['tag1'];\n    const result = isRequestTagsIncluded(requestTags, includeTags, excludeTags);\n    expect(result).toBe(false);\n  });\n\n  it('should exclude request when it has both included and excluded tag', () => {\n    const requestTags = ['tag1', 'tag2'];\n    const includeTags: string[] = ['tag2'];\n    const excludeTags = ['tag1'];\n    const result = isRequestTagsIncluded(requestTags, includeTags, excludeTags);\n    expect(result).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-common/src/tags/index.ts",
    "content": "/**\n * A request should be included if it has at least one tag that is included and no tags that are excluded\n * @param requestTags Tags of the request\n * @param includeTags Tags to include\n * @param excludeTags Tags to exclude\n */\nexport const isRequestTagsIncluded = (requestTags: string[], includeTags: string[], excludeTags: string[]) => {\n  const shouldInclude = includeTags.length === 0 || requestTags.some((tag) => includeTags.includes(tag));\n  const shouldExclude = excludeTags.length > 0 && requestTags.some((tag) => excludeTags.includes(tag));\n  return shouldInclude && !shouldExclude;\n};\n\nexport default isRequestTagsIncluded;\n"
  },
  {
    "path": "packages/bruno-common/src/utils/faker-functions.spec.ts",
    "content": "import { mockDataFunctions } from './faker-functions';\n\ndescribe('mockDataFunctions Regex Validation', () => {\n  beforeAll(() => {\n    jest.useFakeTimers();\n    jest.setSystemTime(new Date('2024-01-01T00:00:00.000Z'));\n  });\n\n  afterAll(() => {\n    jest.useRealTimers();\n  });\n\n  test('timestamp and isoTimestamp should return mocked time values', () => {\n    const expectedTimestamp = '1704067200';\n    const expectedIsoTimestamp = '2024-01-01T00:00:00.000Z';\n\n    expect(mockDataFunctions.timestamp()).toBe(expectedTimestamp);\n    expect(mockDataFunctions.isoTimestamp()).toBe(expectedIsoTimestamp);\n  });\n\n  test('all values should match their expected patterns', () => {\n    const patterns: Record<string, RegExp> = {\n      guid: /^[\\da-f]{8}-[\\da-f]{4}-[\\da-f]{4}-[\\da-f]{4}-[\\da-f]{12}$/,\n      randomUUID: /^[\\da-f]{8}-[\\da-f]{4}-[\\da-f]{4}-[\\da-f]{4}-[\\da-f]{12}$/,\n      randomNanoId: /^[\\w-]{21,}$/,\n      randomAlphaNumeric: /^[\\w]$/,\n      randomBoolean: /^(true|false)$/,\n      randomInt: /^(?:[0-9]{1,2}|[1-9][0-9]{2}|1000)$/,\n      randomColor: /^[\\w\\s]+$/,\n      randomHexColor: /^#[\\da-f]{6}$/,\n      randomAbbreviation: /^\\w{2,6}$/,\n      randomIP: /^([\\da-f]{1,4}:){7}[\\da-f]{1,4}$|^(\\d{1,3}\\.){3}\\d{1,3}$/,\n      randomIPV4: /^(\\d{1,3}\\.){3}\\d{1,3}$/,\n      randomIPV6: /^([\\da-f]{1,4}:){7}[\\da-f]{1,4}$/,\n      randomMACAddress: /^([\\da-f]{2}:){5}[\\da-f]{2}$/,\n      randomPassword: /^[\\w\\d]{8,}$/,\n      randomLocale: /^[A-Z]{2}$/,\n      randomUserAgent: /^[\\w\\/\\.\\s\\(\\)\\+\\-;:_,]+$/,\n      randomProtocol: /^(http|https|ftp)s?$/,\n      randomSemver: /^\\d+\\.\\d+\\.\\d+$/,\n      randomFirstName: /^[\\s\\S]+$/,\n      randomLastName: /^[\\s\\S]+$/,\n      randomFullName: /^[\\s\\S]+$/,\n      randomNamePrefix: /^[\\s\\S]+$/,\n      randomNameSuffix: /^[\\s\\S]+$/,\n      randomJobArea: /^[\\s\\S]+$/,\n      randomJobDescriptor: /^[\\s\\S]+$/,\n      randomJobTitle: /^[\\s\\S]+$/,\n      randomJobType: /^[\\s\\S]+$/,\n      randomPhoneNumber: /^[\\s\\S]+$/,\n      randomPhoneNumberExt: /^[\\s\\S]+$/,\n      randomCity: /^[\\s\\S]+$/,\n      randomStreetName: /^[\\s\\S]+$/,\n      randomStreetAddress: /^[\\s\\S]+$/,\n      randomCountry: /^[\\s\\S]+$/,\n      randomCountryCode: /^[\\s\\S]+$/,\n      randomLatitude: /^[\\s\\S]+$/,\n      randomLongitude: /^[\\s\\S]+$/,\n      randomAvatarImage: /^[\\s\\S]+$/,\n      randomImageUrl: /^[\\s\\S]+$/,\n      randomAbstractImage: /^[\\s\\S]+$/,\n      randomAnimalsImage: /^[\\s\\S]+$/,\n      randomBusinessImage: /^[\\s\\S]+$/,\n      randomCatsImage: /^[\\s\\S]+$/,\n      randomCityImage: /^[\\s\\S]+$/,\n      randomFoodImage: /^[\\s\\S]+$/,\n      randomNightlifeImage: /^[\\s\\S]+$/,\n      randomFashionImage: /^[\\s\\S]+$/,\n      randomPeopleImage: /^[\\s\\S]+$/,\n      randomNatureImage: /^[\\s\\S]+$/,\n      randomSportsImage: /^[\\s\\S]+$/,\n      randomTransportImage: /^[\\s\\S]+$/,\n      randomImageDataUri: /^[\\s\\S]+$/,\n      randomBankAccount: /^[\\s\\S]+$/,\n      randomBankAccountName: /^[\\s\\S]+$/,\n      randomCreditCardMask: /^[\\s\\S]+$/,\n      randomBankAccountBic: /^[\\s\\S]+$/,\n      randomBankAccountIban: /^[\\s\\S]+$/,\n      randomTransactionType: /^[\\s\\S]+$/,\n      randomCurrencyCode: /^[\\s\\S]+$/,\n      randomCurrencyName: /^[\\s\\S]+$/,\n      randomCurrencySymbol: /^[\\s\\S]+$/,\n      randomBitcoin: /^[\\s\\S]+$/,\n      randomCompanyName: /^[\\s\\S]+$/,\n      randomCompanySuffix: /^[\\s\\S]+$/,\n      randomBs: /^[\\s\\S]+$/,\n      randomBsAdjective: /^[\\s\\S]+$/,\n      randomBsBuzz: /^[\\s\\S]+$/,\n      randomBsNoun: /^[\\s\\S]+$/,\n      randomCatchPhrase: /^[\\s\\S]+$/,\n      randomCatchPhraseAdjective: /^[\\s\\S]+$/,\n      randomCatchPhraseDescriptor: /^[\\s\\S]+$/,\n      randomCatchPhraseNoun: /^[\\s\\S]+$/,\n      randomDatabaseColumn: /^[\\s\\S]+$/,\n      randomDatabaseType: /^[\\s\\S]+$/,\n      randomDatabaseCollation: /^[\\s\\S]+$/,\n      randomDatabaseEngine: /^[\\s\\S]+$/,\n      randomDateFuture: /^[\\s\\S]+$/,\n      randomDatePast: /^[\\s\\S]+$/,\n      randomDateRecent: /^[\\s\\S]+$/,\n      randomWeekday: /^[\\s\\S]+$/,\n      randomMonth: /^[\\s\\S]+$/,\n      randomDomainName: /^[\\s\\S]+$/,\n      randomDomainSuffix: /^[\\s\\S]+$/,\n      randomDomainWord: /^[\\s\\S]+$/,\n      randomEmail: /^[\\w_.\\-]+@[\\w]+\\.[a-z]+$/,\n      randomExampleEmail: /^[\\w\\.-]+@example\\.[a-z]+$/,\n      randomUserName: /^[\\w.\\-]+$/,\n      randomUrl: /^https:\\/\\/[\\w\\-]+\\.[a-z]+\\/?$/,\n      randomFileName: /^[\\w\\_]+\\.[\\w\\d]+$/,\n      randomFileType: /^[\\w]+$/,\n      randomFileExt: /^[\\w\\d]+$/,\n      randomCommonFileName: /^[\\w\\_]+\\.[\\w\\d]+$/,\n      randomCommonFileType: /^[\\w]+$/,\n      randomCommonFileExt: /^[\\w\\d]+$/,\n      randomFilePath: /^[\\s\\S]+$/,\n      randomDirectoryPath: /^\\/[-\\w\\+\\/]+$/,\n      randomMimeType: /^[\\w]+\\/[\\w\\d\\-\\+\\.]+$/,\n      randomPrice: /^\\d+\\.\\d{2}$/,\n      randomProduct: /^[\\s\\S]+$/,\n      randomProductAdjective: /^[\\s\\S]+$/,\n      randomProductMaterial: /^[\\s\\S]+$/,\n      randomProductName: /^[\\s\\S]+$/,\n      randomDepartment: /^[\\s\\S]+$/,\n      randomNoun: /^[\\s\\S]+$/,\n      randomVerb: /^[\\s\\S]+$/,\n      randomIngverb: /^[\\s\\S]+$/,\n      randomAdjective: /^[\\s\\S]+$/,\n      randomWord: /^[\\s\\S]+$/,\n      randomWords: /^[\\s\\S]+$/,\n      randomPhrase: /^[\\s\\S]+$/,\n      randomLoremWord: /^[\\s\\S]+$/,\n      randomLoremWords: /^[\\s\\S]+$/,\n      randomLoremSentence: /^[\\s\\S]+$/,\n      randomLoremSentences: /^[\\s\\S]+$/,\n      randomLoremParagraph: /^[\\s\\S]+$/,\n      randomLoremParagraphs: /^[\\s\\S]+$/,\n      randomLoremText: /^[\\s\\S]+$/,\n      randomLoremSlug: /^[\\s\\S]+$/,\n      randomLoremLines: /^[\\s\\S]+$/\n    };\n\n    const errors: string[] = [];\n\n    Object.entries(mockDataFunctions).forEach(([key, func]) => {\n      const pattern = patterns[key];\n      const value = String(func());\n      if (!value.match(pattern)) {\n        errors.push(`Pattern mismatch for ${key}: expected ${pattern}, received ${value}`);\n      }\n    });\n\n    if (errors.length > 0) {\n      throw new Error(errors.join('\\n'));\n    }\n  });\n});\n"
  },
  {
    "path": "packages/bruno-common/src/utils/faker-functions.ts",
    "content": "import { faker } from '@faker-js/faker';\n\nexport const timeBasedDynamicVars = new Set(['timestamp', 'isoTimestamp']);\n\nexport const mockDataFunctions = {\n  guid: () => faker.string.uuid(),\n  timestamp: () => Math.floor(Date.now() / 1000).toString(),\n  isoTimestamp: () => new Date().toISOString(),\n  randomUUID: () => faker.string.uuid(),\n  randomNanoId: () => faker.string.nanoid(),\n  randomAlphaNumeric: () => faker.string.alphanumeric(),\n  randomBoolean: () => faker.datatype.boolean(),\n  randomInt: () => faker.number.int(1000),\n  randomColor: () => faker.color.human(),\n  randomHexColor: () => faker.color.rgb(),\n  randomAbbreviation: () => faker.hacker.abbreviation(),\n  randomIP: () => faker.internet.ip(),\n  randomIPV4: () => faker.internet.ipv4(),\n  randomIPV6: () => faker.internet.ipv6(),\n  randomMACAddress: () => faker.internet.mac(),\n  randomPassword: () => faker.internet.password(),\n  randomLocale: () => faker.location.countryCode(),\n  randomUserAgent: () => faker.internet.userAgent(),\n  randomProtocol: () => faker.internet.protocol(),\n  randomSemver: () => faker.system.semver(),\n  randomFirstName: () => faker.person.firstName(),\n  randomLastName: () => faker.person.lastName(),\n  randomFullName: () => faker.person.fullName(),\n  randomNamePrefix: () => faker.person.prefix(),\n  randomNameSuffix: () => faker.person.suffix(),\n  randomJobArea: () => faker.person.jobArea(),\n  randomJobDescriptor: () => faker.person.jobDescriptor(),\n  randomJobTitle: () => faker.person.jobTitle(),\n  randomJobType: () => faker.person.jobType(),\n  randomPhoneNumber: () => faker.phone.number({ style: 'national' }),\n  randomPhoneNumberExt: () => `${faker.phone.number({ style: 'national' })} x${faker.string.numeric(3)}`,\n  randomCity: () => faker.location.city(),\n  randomStreetName: () => faker.location.street(),\n  randomStreetAddress: () => faker.location.streetAddress(),\n  randomCountry: () => faker.location.country(),\n  randomCountryCode: () => faker.location.countryCode(),\n  randomLatitude: () => faker.location.latitude(),\n  randomLongitude: () => faker.location.longitude(),\n  randomAvatarImage: () => faker.image.avatar(),\n  randomImageUrl: () => faker.image.url(),\n  randomAbstractImage: () => faker.image.urlLoremFlickr({ category: 'abstract' }),\n  randomAnimalsImage: () => faker.image.urlLoremFlickr({ category: 'animals' }),\n  randomBusinessImage: () => faker.image.urlLoremFlickr({ category: 'business' }),\n  randomCatsImage: () => faker.image.urlLoremFlickr({ category: 'cats' }),\n  randomCityImage: () => faker.image.urlLoremFlickr({ category: 'city' }),\n  randomFoodImage: () => faker.image.urlLoremFlickr({ category: 'food' }),\n  randomNightlifeImage: () => faker.image.urlLoremFlickr({ category: 'nightlife' }),\n  randomFashionImage: () => faker.image.urlLoremFlickr({ category: 'fashion' }),\n  randomPeopleImage: () => faker.image.urlLoremFlickr({ category: 'people' }),\n  randomNatureImage: () => faker.image.urlLoremFlickr({ category: 'nature' }),\n  randomSportsImage: () => faker.image.urlLoremFlickr({ category: 'sports' }),\n  randomTransportImage: () => faker.image.urlLoremFlickr({ category: 'transport' }),\n  randomImageDataUri: () => faker.image.dataUri(),\n  randomBankAccount: () => faker.finance.accountNumber(),\n  randomBankAccountName: () => faker.finance.accountName(),\n  randomCreditCardMask: () => faker.finance.iban().replace(/(?<=.{4})\\w(?=.{2})/g, '*'),\n  randomBankAccountBic: () => faker.finance.bic(),\n  randomBankAccountIban: () => faker.finance.iban(),\n  randomTransactionType: () => faker.finance.transactionType(),\n  randomCurrencyCode: () => faker.finance.currencyCode(),\n  randomCurrencyName: () => faker.finance.currencyName(),\n  randomCurrencySymbol: () => faker.finance.currencySymbol(),\n  randomBitcoin: () => faker.finance.bitcoinAddress(),\n  randomCompanyName: () => faker.company.name(),\n  randomCompanySuffix: () => faker.company.name(),\n  randomBs: () => faker.company.buzzPhrase(),\n  randomBsAdjective: () => faker.company.buzzAdjective(),\n  randomBsBuzz: () => faker.company.buzzVerb(),\n  randomBsNoun: () => faker.company.buzzNoun(),\n  randomCatchPhrase: () => faker.company.catchPhrase(),\n  randomCatchPhraseAdjective: () => faker.company.catchPhraseAdjective(),\n  randomCatchPhraseDescriptor: () => faker.company.catchPhraseDescriptor(),\n  randomCatchPhraseNoun: () => faker.company.catchPhraseNoun(),\n  randomDatabaseColumn: () => faker.database.column(),\n  randomDatabaseType: () => faker.database.type(),\n  randomDatabaseCollation: () => faker.database.collation(),\n  randomDatabaseEngine: () => faker.database.engine(),\n  randomDateFuture: () => faker.date.future().toISOString(),\n  randomDatePast: () => faker.date.past().toISOString(),\n  randomDateRecent: () => faker.date.recent().toISOString(),\n  randomWeekday: () => faker.date.weekday(),\n  randomMonth: () => faker.date.month(),\n  randomDomainName: () => faker.internet.domainName(),\n  randomDomainSuffix: () => faker.internet.domainSuffix(),\n  randomDomainWord: () => faker.internet.domainWord(),\n  randomEmail: () => faker.internet.email(),\n  randomExampleEmail: () => faker.internet.exampleEmail(),\n  randomUserName: () => faker.internet.username(),\n  randomUrl: () => faker.internet.url(),\n  randomFileName: () => faker.system.fileName(),\n  randomFileType: () => faker.system.fileType(),\n  randomFileExt: () => faker.system.fileExt(),\n  randomCommonFileName: () => faker.system.commonFileName(),\n  randomCommonFileType: () => faker.system.commonFileType(),\n  randomCommonFileExt: () => faker.system.commonFileExt(),\n  randomFilePath: () => faker.system.filePath(),\n  randomDirectoryPath: () => faker.system.directoryPath(),\n  randomMimeType: () => faker.system.mimeType(),\n  randomPrice: () => faker.commerce.price(),\n  randomProduct: () => faker.commerce.product(),\n  randomProductAdjective: () => faker.commerce.productAdjective(),\n  randomProductMaterial: () => faker.commerce.productMaterial(),\n  randomProductName: () => faker.commerce.productName(),\n  randomDepartment: () => faker.commerce.department(),\n  randomNoun: () => faker.hacker.noun(),\n  randomVerb: () => faker.hacker.verb(),\n  randomIngverb: () => faker.hacker.ingverb(),\n  randomAdjective: () => faker.hacker.adjective(),\n  randomWord: () => faker.hacker.noun(),\n  randomWords: () => faker.lorem.words(),\n  randomPhrase: () => faker.hacker.phrase(),\n  randomLoremWord: () => faker.lorem.word(),\n  randomLoremWords: () => faker.lorem.words(),\n  randomLoremSentence: () => faker.lorem.sentence(),\n  randomLoremSentences: () => faker.lorem.sentences(),\n  randomLoremParagraph: () => faker.lorem.paragraph(),\n  randomLoremParagraphs: () => faker.lorem.paragraphs(),\n  randomLoremText: () => faker.lorem.text(),\n  randomLoremSlug: () => faker.lorem.slug(),\n  randomLoremLines: () => faker.lorem.lines()\n};\n"
  },
  {
    "path": "packages/bruno-common/src/utils/form-data.spec.ts",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport { buildFormUrlEncodedPayload, isFormData } from './form-data';\nimport FormData from 'form-data';\n\ndescribe('buildFormUrlEncodedPayload', () => {\n  it('should handle single key-value pair', () => {\n    const requestObj = [{ name: 'item', value: 2 }];\n    const expected = 'item=2';\n    const result = buildFormUrlEncodedPayload(requestObj);\n    expect(result).toEqual(expected);\n  });\n\n  it('should handle multiple key-value pairs with unique keys', () => {\n    const requestObj = [\n      { name: 'item1', value: 2 },\n      { name: 'item2', value: 3 }\n    ];\n    const expected = 'item1=2&item2=3';\n    const result = buildFormUrlEncodedPayload(requestObj);\n    expect(result).toEqual(expected);\n  });\n\n  it('should handle multiple key-value pairs with the same key', () => {\n    const requestObj = [\n      { name: 'item', value: 2 },\n      { name: 'item', value: 3 }\n    ];\n    const expected = 'item=2&item=3';\n    const result = buildFormUrlEncodedPayload(requestObj);\n    expect(result).toEqual(expected);\n  });\n\n  it('should handle mixed key-value pairs with unique and duplicate keys', () => {\n    const requestObj = [\n      { name: 'item1', value: 2 },\n      { name: 'item2', value: 3 },\n      { name: 'item1', value: 4 }\n    ];\n    const expected = 'item1=2&item2=3&item1=4';\n    const result = buildFormUrlEncodedPayload(requestObj);\n    expect(result).toEqual(expected);\n  });\n\n  it('should handle empty array', () => {\n    const result = buildFormUrlEncodedPayload([]);\n    expect(result).toEqual('');\n  });\n\n  it('should handle array with undefined and null values', () => {\n    const requestObj = [\n      { name: 'item1', value: undefined },\n      { name: 'item2', value: null as any },\n      { name: 'item3', value: '' },\n      { name: 'item4', value: 0 }\n    ];\n    const expected = 'item1=&item2=&item3=&item4=0';\n    const result = buildFormUrlEncodedPayload(requestObj);\n    expect(result).toEqual(expected);\n  });\n\n  it('should handle array with special characters in names and values', () => {\n    const requestObj = [\n      { name: 'item with spaces', value: 'value with spaces' },\n      { name: 'item&special', value: 'value&special' },\n      { name: 'item=equals', value: 'value=equals' },\n      { name: 'item%percent', value: 'value%percent' }\n    ];\n    const expected = 'item+with+spaces=value+with+spaces&item%26special=value%26special&item%3Dequals=value%3Dequals&item%25percent=value%25percent';\n    const result = buildFormUrlEncodedPayload(requestObj);\n    expect(result).toEqual(expected);\n  });\n\n  it('should handle array with numeric and boolean values', () => {\n    const requestObj = [\n      { name: 'number', value: 42 },\n      { name: 'float', value: 3.14 },\n      { name: 'boolean_true', value: true },\n      { name: 'boolean_false', value: false }\n    ];\n    const expected = 'number=42&float=3.14&boolean_true=true&boolean_false=false';\n    const result = buildFormUrlEncodedPayload(requestObj);\n    expect(result).toEqual(expected);\n  });\n\n  it('should preserve parameter order in array format', () => {\n    const requestObj = [\n      { name: 'z', value: '1' },\n      { name: 'a', value: '2' },\n      { name: 'm', value: '3' }\n    ];\n    const expected = 'z=1&a=2&m=3';\n    const result = buildFormUrlEncodedPayload(requestObj);\n    expect(result).toEqual(expected);\n  });\n\n  it('should ignore invalid items inside params array', () => {\n    const requestObj: any[] = [\n      { name: 'item1', value: 'a' },\n      'not-an-object',\n      { value: 'missingName' },\n      42,\n      { name: 'item2', value: 'b' },\n      { name: 'item3' }, // missing value should default to empty string\n      null,\n      undefined,\n      { name: '', value: 'empty_name' }, // empty name should still work\n      { name: 'valid', value: 'c' }\n    ];\n    const expected = 'item1=a&item2=b&item3=&=empty_name&valid=c';\n    const result = buildFormUrlEncodedPayload(requestObj);\n    expect(result).toEqual(expected);\n  });\n});\n\ndescribe('isFormData', () => {\n  it('should return true for objects with FormData constructor name', () => {\n    const mockFormData = {\n      constructor: { name: 'FormData' }\n    };\n    expect(isFormData(mockFormData)).toBe(true);\n  });\n\n  it('should return false for null', () => {\n    expect(isFormData(null)).toBe(false);\n  });\n\n  it('should return false for undefined', () => {\n    expect(isFormData(undefined)).toBe(false);\n  });\n\n  it('should return false for plain objects', () => {\n    expect(isFormData({})).toBe(false);\n    expect(isFormData({ key: 'value' })).toBe(false);\n  });\n\n  it('should return false for arrays', () => {\n    expect(isFormData([])).toBe(false);\n    expect(isFormData([1, 2, 3])).toBe(false);\n  });\n\n  it('should return false for primitives', () => {\n    expect(isFormData('string')).toBe(false);\n    expect(isFormData(123)).toBe(false);\n    expect(isFormData(true)).toBe(false);\n  });\n\n  it('should return false for objects with different constructor names', () => {\n    class CustomClass {}\n    const customObj = new CustomClass();\n    expect(isFormData(customObj)).toBe(false);\n  });\n\n  it('should return false for objects without constructor', () => {\n    const obj = Object.create(null);\n    expect(isFormData(obj)).toBe(false);\n  });\n\n  it('should return true for actual FormData instance from form-data library', () => {\n    const formData = new FormData();\n    formData.append('key', 'value');\n    expect(isFormData(formData)).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-common/src/utils/form-data.ts",
    "content": "/**\n * Builds a URL-encoded payload from various data formats\n *\n * This function handles multiple input formats:\n * - Array of objects with 'name' and 'value' properties (preserves order)\n *\n * @param data The request body data\n * @returns URL-encoded string suitable for application/x-www-form-urlencoded content type\n *\n * @example\n * // Array format (preserves order)\n * buildFormUrlEncodedPayload([{name: 'a', value: '1'}, {name: 'b', value: '2'}])\n * // Returns: 'a=1&b=2'\n */\nexport const buildFormUrlEncodedPayload = (params: Array<{ name: string; value: string | number | boolean | undefined }>): string => {\n  // Ensure params is iterable (array)\n  if (!Array.isArray(params)) {\n    return '';\n  }\n\n  const resultParams = new URLSearchParams();\n\n  for (const param of params) {\n    // Invalid items are ignored\n    if (typeof param !== 'object' || param === null) continue;\n    if (!('name' in param)) continue;\n\n    // Append parameter with value (default to empty string if undefined/null)\n    resultParams.append(param.name, String(param.value ?? ''));\n  }\n\n  return resultParams.toString();\n};\n\n/**\n * Determines if the given object is a FormData instance.\n * Supports native FormData (Node 18+, browser) and the 'form-data' npm package.\n * @param obj - Object to check.\n * @returns True if obj is a FormData instance, false otherwise.\n */\nexport const isFormData = (obj: unknown): boolean => {\n  // Check constructor name (works for both native FormData and form-data npm package)\n  // todo: checking constructor.name can produce false positives for objects that have a constructor.name property set to 'FormData', but this is rare.\n  return obj?.constructor?.name === 'FormData';\n};\n"
  },
  {
    "path": "packages/bruno-common/src/utils/index.ts",
    "content": "export {\n  encodeUrl,\n  parseQueryParams,\n  buildQueryString,\n  stripOrigin\n} from './url';\n\nexport {\n  buildFormUrlEncodedPayload,\n  isFormData\n} from './form-data';\n\nexport {\n  patternHasher\n} from './template-hasher';\n\nexport {\n  PROMPT_VARIABLE_TEXT_PATTERN,\n  PROMPT_VARIABLE_TEMPLATE_PATTERN,\n  extractPromptVariables,\n  extractPromptVariablesFromString\n} from './prompt-variables';\n"
  },
  {
    "path": "packages/bruno-common/src/utils/prompt-variables.spec.ts",
    "content": "import { describe, expect, it } from '@jest/globals';\n\nimport { extractPromptVariables, extractPromptVariablesFromString } from './prompt-variables';\n\ndescribe('prompt variable utils', () => {\n  describe('extractPromptVariablesFromString', () => {\n    it('should extract prompt variables', () => {\n      expect(extractPromptVariablesFromString('Hello {{?world}}')).toEqual(['world']);\n      expect(extractPromptVariablesFromString('No prompts here')).toEqual([]);\n      expect(extractPromptVariablesFromString('Multiple {{?prompts}} in {{?one}} string')).toEqual(['prompts', 'one']);\n    });\n\n    it('should deduplicate prompt variables', () => {\n      // Strings\n      expect(extractPromptVariables('{{?world}} prompt here Hello {{?world}}')).toEqual(['world']);\n      expect(extractPromptVariables('Multiple {{?prompts}} in {{?one}} string plus another {{?one}}')).toEqual(['prompts', 'one']);\n    });\n  });\n\n  describe('extractPromptVariables', () => {\n    it('should extract prompt variables from strings', () => {\n      expect(extractPromptVariables('Hello {{?world}}')).toEqual(['world']);\n      expect(extractPromptVariables('No prompts here')).toEqual([]);\n      expect(extractPromptVariables('Multiple {{?prompts}} in {{?one}} string')).toEqual(['prompts', 'one']);\n    });\n\n    it('should extract prompt variables from objects', () => {\n      expect(extractPromptVariables({ text: 'Hello {{?world}}' })).toEqual(['world']);\n      expect(extractPromptVariables({ noPrompt: 'No prompt here' })).toEqual([]);\n      expect(extractPromptVariables({ prompt1: 'Hello {{?world}}', prompt2: 'Another {{?test}}' })).toEqual(['world', 'test']);\n    });\n\n    it('should extract prompt variables from arrays', () => {\n      // Strings\n      expect(extractPromptVariables(['No prompts here', 'Hello {{?world}}'])).toEqual(['world']);\n      expect(extractPromptVariables(['Multiple {{?prompts}} in {{?one}} string', 'Another {{?test}} string'])).toEqual(['prompts', 'one', 'test']);\n\n      // Objects\n      expect(extractPromptVariables([{ prompt: 'Hello {{?world}}', noprompt: 'No prompt here' }, { noprompt: '' }])).toEqual(['world']);\n\n      // Nested arrays\n      expect(extractPromptVariables(['Prompt {{?here}}', ['Hello {{?world}}', 'Another {{?test}} string']])).toEqual(['here', 'world', 'test']);\n\n      // Mixed data types\n      expect(extractPromptVariables([{ text: 'Multiple {{?prompts}} in {{?one}} string', noPrompt: 'No prompt here' }, ['Another {{?test}} string', { prompt: '{{?nested}}', no: 'prompt' }]])).toEqual(['prompts', 'one', 'test', 'nested']);\n    });\n\n    it('should not extract prompt variables from invalid template patterns', () => {\n      expect(extractPromptVariables('Prompt with valid {{?inner space}}')).toEqual(['inner space']);\n      expect(extractPromptVariables('Prompt with invalid {{? leading space}}')).toEqual([]);\n      expect(extractPromptVariables('Prompt with invalid {{?trailing space }}')).toEqual([]);\n      expect(extractPromptVariables('Prompt with invalid {{?{curly brace}}')).toEqual([]);\n      expect(extractPromptVariables('Prompt with invalid {{?}curly brace}}')).toEqual([]);\n      expect(extractPromptVariables('Prompt with invalid {{?{curly brace}}}')).toEqual([]);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-common/src/utils/prompt-variables.ts",
    "content": "/**\n * Inner regex pattern for prompt variable names (without braces or `?` prefix)\n *\n * Pattern: /[^{}\\s](?:[^{}]*[^{}\\s])?/\n *\n * Breakdown:\n * | Part           | Meaning                                                    |\n * | -------------- | ---------------------------------------------------------- |\n * | `[^\\s{}]`      | First character: not whitespace, `{`, or `}`               |\n * | `(?:...)?`     | Optional non-capturing group (allows single-char names)    |\n * | `[^{}]*`       | Middle characters: any except `{` or `}` (spaces allowed)  |\n * | `[^\\s{}]`      | Last character: not whitespace, `{`, or `}`                |\n *\n * This inner pattern is reused in:\n * - PROMPT_VARIABLE_TEXT_PATTERN: Matches \"?Name\" format (with anchors)\n * - PROMPT_VARIABLE_PATTERN: Matches \"{{?Name}}\" format (in templates)\n *\n * Valid examples: \"Name\", \"Prompt Var\", \"x\"\n * Invalid examples: \" Name\", \"Name \", \"{Name}\", \"Na{me}\"\n */\nconst PROMPT_VARIABLE_PATTERN = /[^{}\\s](?:[^{}]*[^{}\\s])?/;\n\n/**\n * Valid examples: \"?Name\", \"?Prompt Var\", \"?x\"\n * Invalid examples: \"? Name\", \"?Name \", \"?{{Name}}\", \"?{Name}\"\n */\nexport const PROMPT_VARIABLE_TEXT_PATTERN = new RegExp(`^\\\\?(${PROMPT_VARIABLE_PATTERN.source})$`);\n\n/**\n * Valid matches: \"{{?Name}}\", \"{{?Prompt Var}}\", \"{{?x}}\"\n * Invalid: \"{{? Name}}\", \"{{?Name }}\", \"{{?{Name}}}\"\n */\nexport const PROMPT_VARIABLE_TEMPLATE_PATTERN = new RegExp(`{{\\\\?(${PROMPT_VARIABLE_PATTERN.source})}}`, 'g');\n\n/**\n * Extract prompt variables matching {{?<Prompt Text>}} from a string.\n * @param {string} str - The input string.\n * @returns {string[]} - An array of extracted prompt variables.\n */\nexport const extractPromptVariablesFromString = (str: string): string[] => {\n  const prompts = new Set<string>();\n  let match;\n  while ((match = PROMPT_VARIABLE_TEMPLATE_PATTERN.exec(str)) !== null) {\n    prompts.add(match[1]);\n  }\n  return Array.from(prompts);\n};\n\n/**\n * Extract prompt variables from an object.\n * @param {*} obj - The input object.\n * @returns {string[]} - An array of extracted prompt variables.\n */\nexport function extractPromptVariables(obj: any): string[] {\n  const prompts = new Set<string>();\n  try {\n    if (typeof obj === 'string') {\n      // Extract prompt variables from strings\n      const extracted = extractPromptVariablesFromString(obj);\n      extracted.forEach((prompt) => prompts.add(prompt));\n    } else if (Array.isArray(obj)) {\n      // Recursively extract from array elements\n      for (const item of obj) {\n        const extracted = extractPromptVariables(item);\n        extracted.forEach((prompt) => prompts.add(prompt));\n      }\n    } else if (typeof obj === 'object' && obj !== null) {\n      // Recursively extract from object properties\n      for (const key in obj) {\n        const extracted = extractPromptVariables(obj[key]);\n        extracted.forEach((prompt) => prompts.add(prompt));\n      }\n    }\n  } catch (error) {\n    console.error('Error extracting prompt variables:', error);\n  }\n  return Array.from(prompts);\n}\n"
  },
  {
    "path": "packages/bruno-common/src/utils/template-hasher.spec.ts",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport { patternHasher } from './template-hasher';\n\ndescribe('patternHasher', () => {\n  it('hashes and restore are mathematically reproducible', () => {\n    const originalUrl = '{{host}}.example.com';\n    const { hashed, restore } = patternHasher(originalUrl);\n    expect(hashed).toMatchInlineSnapshot(`\"bruno-var-hash--163450413.example.com\"`);\n    expect(restore(hashed)).toEqual(originalUrl);\n  });\n\n  it('hashes more than once', () => {\n    const originalUrl = '{{host}}.example.{{new}}';\n    const { hashed, restore } = patternHasher(originalUrl);\n    expect(hashed).toMatchInlineSnapshot(`\"bruno-var-hash--163450413.example.bruno-var-hash-652560383\"`);\n    expect(restore(hashed)).toEqual(originalUrl);\n  });\n\n  it('allows custom matchers', () => {\n    const originalUrl = '$name.example.com';\n    const { hashed, restore } = patternHasher(originalUrl, /\\$(\\w+)/);\n    expect(hashed).toMatchInlineSnapshot(`\"bruno-var-hash-180907786.example.com\"`);\n    expect(restore(hashed)).toEqual(originalUrl);\n  });\n\n  it('ignore unless matched', () => {\n    const originalUrl = '$name.example.com';\n    const { hashed, restore } = patternHasher(originalUrl);\n    expect(hashed).toMatchInlineSnapshot(`\"$name.example.com\"`);\n    expect(restore(hashed)).toEqual(originalUrl);\n  });\n\n  it('verify restoring duplicate hashes', () => {\n    const originalJSON = `{\"name\":\"{{name}}\",\"x\":\"{{name}}\", \"y\":\"{{name}}\"}`;\n    const { hashed, restore } = patternHasher(originalJSON);\n    expect(restore(hashed)).toEqual(originalJSON);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-common/src/utils/template-hasher.ts",
    "content": "const VARIABLE_REGEX = /\\{\\{([^}]+)\\}\\}/g;\n\n/**\n * Was implemented specifically for request.url where the url might have variables\n * that might need to be sanitised before being passed to a URL validator that doesn't\n * allow special characters that bruno uses as variables (`{{var_name}}`)\n *\n * The function replaces the input string with a unique hash that can be restored\n * later by the helper returned by this function\n */\nexport function patternHasher(input: string, pattern: string | RegExp = VARIABLE_REGEX) {\n  const usableRegex = new RegExp(pattern, 'g');\n  function hash(toHash: string) {\n    let hash = 5381;\n    let c;\n    for (let i = 0; i < toHash.length; i++) {\n      c = toHash.charCodeAt(i);\n      hash = ((hash << 5) + hash + c) | 0;\n    }\n    return '' + hash;\n  }\n\n  const prefix = `bruno-var-hash-`;\n  const hashToOriginal: Record<string, string> = {};\n  let result = input;\n  let hashed = false;\n  if (usableRegex.test(input)) {\n    hashed = true;\n    result = input.replace(usableRegex, function (matchedVar) {\n      const hashedValue = `${prefix}${hash(matchedVar)}`;\n      hashToOriginal[hashedValue] = matchedVar;\n      return hashedValue;\n    });\n  }\n  return {\n    hashed: result,\n    restore(current: string) {\n      if (!hashed) {\n        return current;\n      }\n      let clone = current;\n      for (const hash in hashToOriginal) {\n        const value = hashToOriginal[hash];\n        clone = clone.replaceAll(hash, value);\n      }\n      return clone;\n    }\n  };\n}\n"
  },
  {
    "path": "packages/bruno-common/src/utils/url/index.spec.ts",
    "content": "import { encodeUrl, parseQueryParams, buildQueryString } from './index';\n\ndescribe('encodeUrl', () => {\n  describe('basic functionality', () => {\n    it('should return the original URL when query string is empty', () => {\n      const url = 'https://example.com/path?';\n      expect(encodeUrl(url)).toBe(url);\n    });\n\n    it('should preserve URLs without query parameters', () => {\n      const url = 'https://api.example.com/v1/users';\n      expect(encodeUrl(url)).toBe(url);\n    });\n  });\n\n  describe('query parameter encoding', () => {\n    it('should handle a single query parameter', () => {\n      const url = 'https://example.com/api?name=john';\n      const expected = 'https://example.com/api?name=john';\n      expect(encodeUrl(url)).toBe(expected);\n    });\n\n    it('should handle simple query parameters', () => {\n      const url = 'https://example.com/api?name=john&age=25';\n      const expected = 'https://example.com/api?name=john&age=25';\n      expect(encodeUrl(url)).toBe(expected);\n    });\n\n    it('should encode query parameters with special characters', () => {\n      const url = 'https://example.com/api?name=john doe&email=john@example.com';\n      const expected = 'https://example.com/api?name=john%20doe&email=john%40example.com';\n      expect(encodeUrl(url)).toBe(expected);\n    });\n\n    it('should encode query parameters with special URL characters', () => {\n      const url = 'https://example.com/api?path=/users/123&redirect=https://other.com';\n      const expected = 'https://example.com/api?path=%2Fusers%2F123&redirect=https%3A%2F%2Fother.com';\n      expect(encodeUrl(url)).toBe(expected);\n    });\n\n    it('should encode query parameters with unicode characters', () => {\n      const url = 'https://example.com/api?name=José&city=München';\n      const expected = 'https://example.com/api?name=Jos%C3%A9&city=M%C3%BCnchen';\n      expect(encodeUrl(url)).toBe(expected);\n    });\n\n    it('should handle query parameters with empty values', () => {\n      const url = 'https://example.com/api?name=&age=25&active=';\n      const expected = 'https://example.com/api?name=&age=25&active=';\n      expect(encodeUrl(url)).toBe(expected);\n    });\n\n    it('should encode query parameters with pipe operator', () => {\n      const url = 'https://example.com/api?filter=status|active&sort=name|asc&tags=frontend|backend|api';\n      const expected = 'https://example.com/api?filter=status%7Cactive&sort=name%7Casc&tags=frontend%7Cbackend%7Capi';\n      expect(encodeUrl(url)).toBe(expected);\n    });\n\n    it('should encode query parameters with pipe operator and spaces', () => {\n      const url = 'https://example.com/api?categories=web development|mobile apps|data science&status=in progress|completed';\n      const expected = 'https://example.com/api?categories=web%20development%7Cmobile%20apps%7Cdata%20science&status=in%20progress%7Ccompleted';\n      expect(encodeUrl(url)).toBe(expected);\n    });\n  });\n\n  describe('hash fragment handling', () => {\n    it('should preserve hash fragments with encoded query parameters', () => {\n      const url = 'https://example.com/api?name=john doe#section1';\n      const expected = 'https://example.com/api?name=john%20doe#section1';\n      expect(encodeUrl(url)).toBe(expected);\n    });\n\n    it('should preserve hash fragments with pipe operator in query', () => {\n      const url = 'https://example.com/api?filter=status|active#results';\n      const expected = 'https://example.com/api?filter=status%7Cactive#results';\n      expect(encodeUrl(url)).toBe(expected);\n    });\n  });\n\n  describe('edge cases', () => {\n    it('should handle invalid input gracefully', () => {\n      expect(encodeUrl('')).toBe('');\n      expect(encodeUrl(null as any)).toBe(null);\n      expect(encodeUrl(undefined as any)).toBe(undefined);\n      expect(encodeUrl(123 as any)).toBe(123);\n    });\n\n    it('should handle URLs with multiple question marks', () => {\n      const url = 'https://example.com/api?name=john?age=25';\n      const expected = 'https://example.com/api?name=john%3Fage%3D25';\n      expect(encodeUrl(url)).toBe(expected);\n    });\n\n    it('should handle complex query parameters with multiple special characters', () => {\n      const url = 'https://example.com/api?search=hello world!@#$%^&*()&filter=active&sort=name asc';\n      const expected = 'https://example.com/api?search=hello%20world!%40#$%^&*()&filter=active&sort=name asc';\n      expect(encodeUrl(url)).toBe(expected);\n    });\n\n    it('should handle already encoded URLs', () => {\n      const url = 'https://example.com/api?name=john%20doe&email=john%40example.com';\n      const expected = 'https://example.com/api?name=john%2520doe&email=john%2540example.com';\n      expect(encodeUrl(url)).toBe(expected);\n    });\n\n    it('should handle pipe operator in already encoded URLs', () => {\n      const url = 'https://example.com/api?filter=status%7Cactive&sort=name%7Casc';\n      const expected = 'https://example.com/api?filter=status%257Cactive&sort=name%257Casc';\n      expect(encodeUrl(url)).toBe(expected);\n    });\n  });\n\n  describe('real-world scenarios', () => {\n    it('should handle API URLs with complex query parameters', () => {\n      const url = 'https://api.github.com/search/repositories?q=language:javascript&sort=stars&order=desc&per_page=10';\n      const expected = 'https://api.github.com/search/repositories?q=language%3Ajavascript&sort=stars&order=desc&per_page=10';\n      expect(encodeUrl(url)).toBe(expected);\n    });\n\n    it('should handle OAuth callback URLs', () => {\n      const url = 'https://myapp.com/callback?code=abc123&state=xyz789&redirect_uri=https://myapp.com/dashboard';\n      const expected = 'https://myapp.com/callback?code=abc123&state=xyz789&redirect_uri=https%3A%2F%2Fmyapp.com%2Fdashboard';\n      expect(encodeUrl(url)).toBe(expected);\n    });\n\n    it('should handle GraphQL queries with pipe operator', () => {\n      const url = 'https://api.example.com/graphql?query=query{users(status:active|pending){id,name}}&variables={\"filter\":\"status|active\"}';\n      const expected = 'https://api.example.com/graphql?query=query%7Busers(status%3Aactive%7Cpending)%7Bid%2Cname%7D%7D&variables=%7B%22filter%22%3A%22status%7Cactive%22%7D';\n      expect(encodeUrl(url)).toBe(expected);\n    });\n\n    it('should handle search APIs with complex queries', () => {\n      const url = 'https://api.example.com/search?q=react typescript tutorial&type=article,code&language=en&date_range=2023-01-01:2023-12-31&sort=relevance:desc';\n      const expected = 'https://api.example.com/search?q=react%20typescript%20tutorial&type=article%2Ccode&language=en&date_range=2023-01-01%3A2023-12-31&sort=relevance%3Adesc';\n      expect(encodeUrl(url)).toBe(expected);\n    });\n\n    it('should handle e-commerce API filters', () => {\n      const url = 'https://api.shop.com/products?category=electronics&brand=apple|samsung|google&price_range=100:1000&rating=4.5:5.0&availability=in_stock&sort=price:asc&limit=50';\n      const expected = 'https://api.shop.com/products?category=electronics&brand=apple%7Csamsung%7Cgoogle&price_range=100%3A1000&rating=4.5%3A5.0&availability=in_stock&sort=price%3Aasc&limit=50';\n      expect(encodeUrl(url)).toBe(expected);\n    });\n  });\n});\n\ndescribe('parseQueryParams', () => {\n  it('should extract query parameters correctly', () => {\n    const queryString = 'name=john&age=25&active=true';\n    const result = parseQueryParams(queryString);\n    expect(result).toEqual([\n      { name: 'name', value: 'john' },\n      { name: 'age', value: '25' },\n      { name: 'active', value: 'true' }\n    ]);\n  });\n\n  it('should handle empty query string', () => {\n    const result = parseQueryParams('');\n    expect(result).toEqual([]);\n  });\n\n  it('should handle query parameters with empty values', () => {\n    const queryString = 'name=&age=25&active=';\n    const result = parseQueryParams(queryString);\n    expect(result).toEqual([\n      { name: 'name', value: '' },\n      { name: 'age', value: '25' },\n      { name: 'active', value: '' }\n    ]);\n  });\n\n  it('should extract query parameters with pipe operator', () => {\n    const queryString = 'filter=status|active&sort=name|asc&tags=frontend|backend';\n    const result = parseQueryParams(queryString);\n    expect(result).toEqual([\n      { name: 'filter', value: 'status|active' },\n      { name: 'sort', value: 'name|asc' },\n      { name: 'tags', value: 'frontend|backend' }\n    ]);\n  });\n});\n\ndescribe('buildQueryString', () => {\n  it('should build query string correctly', () => {\n    const params = [\n      { name: 'name', value: 'john' },\n      { name: 'age', value: '25' },\n      { name: 'active', value: 'true' }\n    ];\n    const result = buildQueryString(params);\n    expect(result).toBe('name=john&age=25&active=true');\n  });\n\n  it('should encode parameters by default', () => {\n    const params = [\n      { name: 'name', value: 'john doe' },\n      { name: 'email', value: 'john@example.com' }\n    ];\n    const result = buildQueryString(params, { encode: true });\n    expect(result).toBe('name=john%20doe&email=john%40example.com');\n  });\n\n  it('should encode pipe operator in parameters', () => {\n    const params = [\n      { name: 'filter', value: 'status|active' },\n      { name: 'sort', value: 'name|asc' },\n      { name: 'tags', value: 'frontend|backend|api' }\n    ];\n    const result = buildQueryString(params, { encode: true });\n    expect(result).toBe('filter=status%7Cactive&sort=name%7Casc&tags=frontend%7Cbackend%7Capi');\n  });\n\n  it('should not encode parameters when encode is false', () => {\n    const params = [\n      { name: 'filter', value: 'status|active' },\n      { name: 'sort', value: 'name|asc' }\n    ];\n    const result = buildQueryString(params, { encode: false });\n    expect(result).toBe('filter=status|active&sort=name|asc');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-common/src/utils/url/index.ts",
    "content": "interface QueryParam {\n  name: string;\n  value?: string;\n}\n\ninterface BuildQueryStringOptions {\n  encode?: boolean;\n}\n\ninterface ExtractQueryParamsOptions {\n  decode?: boolean;\n}\n\nfunction buildQueryString(paramsArray: QueryParam[], { encode = false }: BuildQueryStringOptions = {}): string {\n  return paramsArray\n    .filter(({ name }) => typeof name === 'string' && name.trim().length > 0)\n    .map(({ name, value }) => {\n      const finalName = encode ? encodeURIComponent(name) : name;\n      const finalValue = encode ? encodeURIComponent(value ?? '') : (value ?? '');\n\n      return `${finalName}=${finalValue}`;\n    })\n    .join('&');\n}\n\nfunction parseQueryParams(query: string, { decode = false }: ExtractQueryParamsOptions = {}): QueryParam[] {\n  if (!query || !query.length) {\n    return [];\n  }\n\n  try {\n    const [queryString, ...hashParts] = query.split('#');\n    const pairs = queryString.split('&');\n\n    const params = pairs.map((pair) => {\n      const [name, ...valueParts] = pair.split('=');\n\n      if (!name) {\n        return null;\n      }\n\n      return {\n        name: decode ? decodeURIComponent(name) : name,\n        value: decode ? decodeURIComponent(valueParts.join('=')) : valueParts.join('=')\n      };\n    }).filter((param): param is NonNullable<typeof param> => param !== null);\n\n    return params;\n  } catch (error) {\n    console.error('Error parsing query params:', error);\n    return [];\n  }\n}\n\nconst encodeUrl = (url: string): string => {\n  // Early return for invalid input\n  if (!url || typeof url !== 'string') {\n    return url;\n  }\n\n  const [urlWithoutHash, ...hashFragments] = url.split('#');\n  const [basePath, ...queryString] = urlWithoutHash.split('?');\n\n  // If no query parameters exist, return original URL\n  if (!queryString || queryString.length === 0) {\n    return url;\n  }\n\n  const queryParams = parseQueryParams(queryString.join('?'), { decode: false });\n  // Parse and re-encode query parameters\n  const encodedQueryString = buildQueryString(queryParams, { encode: true });\n\n  // Reconstruct URL with encoded query parameters\n  const encodedUrl = `${basePath}?${encodedQueryString}${hashFragments.length > 0 ? `#${hashFragments.join('#')}` : ''}`;\n\n  return encodedUrl;\n};\n\n/**\n * Strip the origin (scheme + authority) from a URL, returning the path, query, and fragment.\n * Returns '/' if the URL has no path component.\n *\n * @example\n * stripOrigin('https://example.com/api/users?name=foo') // '/api/users?name=foo'\n * stripOrigin('http://localhost:3000')                   // '/'\n */\nconst stripOrigin = (url: string): string => {\n  return url.replace(/^https?:\\/\\/[^/?#]*/, '') || '/';\n};\n\nexport {\n  encodeUrl,\n  parseQueryParams,\n  buildQueryString,\n  stripOrigin,\n  type QueryParam,\n  type BuildQueryStringOptions,\n  type ExtractQueryParamsOptions\n};\n"
  },
  {
    "path": "packages/bruno-common/src/zoom/index.ts",
    "content": "// Convert percentage to zoom level (Electron uses logarithmic scale)\n// Formula: percentage = 100 * 1.2^level\nexport const percentageToZoomLevel = (percentage: number): number => {\n  return Math.log(percentage / 100) / Math.log(1.2);\n};\n"
  },
  {
    "path": "packages/bruno-common/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES6\",\n    \"esModuleInterop\": true,\n    \"strict\": true,\n    \"lib\": [\"es2021\"],\n    \"skipLibCheck\": true,\n    \"jsx\": \"react\",\n    \"module\": \"ESNext\",\n    \"sourceMap\": true,\n    \"outDir\": \"dist\",\n    \"moduleResolution\": \"node\",\n    \"allowSyntheticDefaultImports\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"allowJs\": true,\n    \"checkJs\": false\n  },\n  \"include\": [\"src/**/*.ts\", \"src/**/*.js\"],\n  \"exclude\": [\"dist\", \"node_modules\", \"tests\"]\n}\n"
  },
  {
    "path": "packages/bruno-converters/.gitignore",
    "content": "# dependencies\nnode_modules\nyarn.lock\npnpm-lock.yaml\npackage-lock.json\n.pnp\n.pnp.js\n\n# testing\ncoverage\n\n# production\ndist\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "packages/bruno-converters/babel.config.js",
    "content": "module.exports = {\n  presets: [['@babel/preset-env', { targets: { node: 'current' } }]]\n};\n"
  },
  {
    "path": "packages/bruno-converters/jest.config.js",
    "content": "module.exports = {\n  transform: {\n    '^.+\\\\.js$': 'babel-jest'\n  },\n  setupFiles: ['<rootDir>/jest.setup.js'],\n  transformIgnorePatterns: [\n    'node_modules/(?!(nanoid)/)'\n  ],\n  testEnvironment: 'node',\n  moduleNameMapper: {\n    '^nanoid(/(.*)|$)': 'nanoid$1'\n  }\n};\n"
  },
  {
    "path": "packages/bruno-converters/jest.setup.js",
    "content": "// Mock the uuid function\njest.mock('./src/common', () => {\n  // Import the original module to keep other functions intact\n  const originalModule = jest.requireActual('./src/common');\n\n  return {\n    __esModule: true, // Use this property to indicate it's an ES module\n    ...originalModule,\n    uuid: jest.fn(() => 'mockeduuidvalue123456') // Mock uuid to return a fixed value\n  };\n});\n"
  },
  {
    "path": "packages/bruno-converters/license.md",
    "content": "MIT License\n\nCopyright (c) 2025 Anoop M D, Anusree P S and Contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/bruno-converters/package.json",
    "content": "{\n  \"name\": \"@usebruno/converters\",\n  \"version\": \"0.1.0\",\n  \"license\": \"MIT\",\n  \"main\": \"dist/cjs/index.js\",\n  \"module\": \"dist/esm/index.js\",\n  \"files\": [\n    \"dist\",\n    \"src\",\n    \"package.json\"\n  ],\n  \"scripts\": {\n    \"clean\": \"rimraf dist\",\n    \"test\": \"node --experimental-vm-modules $(npx which jest) --colors --collectCoverage\",\n    \"prebuild\": \"npm run clean\",\n    \"build\": \"rollup -c\",\n    \"watch\": \"rollup -c -w\",\n    \"prepack\": \"npm run test && npm run build\"\n  },\n  \"dependencies\": {\n    \"@usebruno/schema\": \"^0.7.0\",\n    \"js-yaml\": \"^4.1.1\",\n    \"jscodeshift\": \"^17.3.0\",\n    \"lodash\": \"^4.17.21\",\n    \"nanoid\": \"3.3.8\",\n    \"xml2js\": \"^0.6.2\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.25.2\",\n    \"@babel/preset-env\": \"^7.25.4\",\n    \"@opencollection/types\": \"~0.8.0\",\n    \"@rollup/plugin-alias\": \"^5.1.0\",\n    \"@rollup/plugin-commonjs\": \"^23.0.2\",\n    \"@rollup/plugin-node-resolve\": \"^15.0.1\",\n    \"@rollup/plugin-typescript\": \"^9.0.2\",\n    \"@usebruno/schema-types\": \"0.0.1\",\n    \"@web/rollup-plugin-copy\": \"^0.5.1\",\n    \"babel-jest\": \"^29.7.0\",\n    \"rimraf\": \"^5.0.7\",\n    \"rollup\": \"3.29.5\",\n    \"rollup-plugin-dts\": \"^5.0.0\",\n    \"rollup-plugin-peer-deps-external\": \"^2.2.4\",\n    \"rollup-plugin-terser\": \"^7.0.2\",\n    \"typescript\": \"^4.8.4\"\n  },\n  \"overrides\": {\n    \"rollup\": \"3.29.5\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-converters/readme.md",
    "content": "# bruno-converters\n\nThe converters package is responsible for converting collections from one format to a Bruno collection.\nIt can be used as a standalone package or as a part of the Bruno framework.\n\n## Installation\n\n```bash\nnpm install @usebruno/converters\n```\n\n## Usage\n\n### Convert Postman collection to Bruno collection\n\n```javascript\nconst { postmanToBruno } = require('@usebruno/converters');\n\n// Convert Postman collection to Bruno collection\nconst brunoCollection = postmanToBruno(postmanCollection);\n```\n\n### Convert Postman Environment to Bruno Environment\n\n```javascript\nconst { postmanToBrunoEnvironment } = require('@usebruno/converters');\n\nconst brunoEnvironment = postmanToBrunoEnvironment(postmanEnvironment);\n```\n\n### Convert Insomnia collection to Bruno collection\n\n```javascript\nconst { insomniaToBruno } = require('@usebruno/converters');\n\nconst brunoCollection = insomniaToBruno(insomniaCollection);\n```\n\n### Convert OpenAPI specification to Bruno collection\n\n```javascript\nconst { openApiToBruno } = require('@usebruno/converters');\n\nconst brunoCollection = openApiToBruno(openApiSpecification);\n```\n\n### Convert WSDL file to Bruno collection\n\n```javascript\nimport { wsdlToBruno } from '@usebruno/converters';\n\nconst brunoCollection = await wsdlToBruno(wsdlContent);\n```\n\n## Example \n\n```javascript\n\nconst { postmanToBruno } = require('@usebruno/converters');\nconst fs = require('fs/promises');\nconst path = require('path');\n\nasync function convertPostmanToBruno(inputFile, outputFile) {\n  try {\n    // Read Postman collection file\n    const inputData = await fs.readFile(inputFile, 'utf8');\n    \n    // Convert to Bruno collection\n    const brunoCollection = await postmanToBruno(JSON.parse(inputData));\n    \n    // Save Bruno collection\n    await fs.writeFile(outputFile, JSON.stringify(brunoCollection, null, 2));\n    \n    console.log('Conversion successful!');\n  } catch (error) {\n    console.error('Error during conversion:', error);\n  }\n}\n\n// Usage\nconst inputFilePath = path.resolve(__dirname, 'demo_collection.postman_collection.json');\nconst outputFilePath = path.resolve(__dirname, 'bruno-collection.json');\n\nconvertPostmanToBruno(inputFilePath, outputFilePath);\n\n``` \n\n## WSDL Import Features\n\nThe WSDL importer supports the following features:\n\n- **Service Discovery**: Automatically extracts service endpoints from WSDL definitions\n- **Operation Mapping**: Converts WSDL operations to Bruno HTTP requests\n- **SOAP Envelope Generation**: Creates proper SOAP envelopes for each operation\n- **Header Configuration**: Sets up appropriate Content-Type and SOAPAction headers\n- **Environment Variables**: Creates environment variables for service base URLs\n- **Folder Organization**: Groups operations by port type for better organization\n\n### WSDL Import Example\n\n```javascript\nimport { wsdlToBruno } from '@usebruno/converters';\nimport fs from 'fs/promises';\n\nasync function importWSDL() {\n  try {\n    // Read WSDL file\n    const wsdlContent = await fs.readFile('service.wsdl', 'utf8');\n    \n    // Convert to Bruno collection\n    const brunoCollection = await wsdlToBruno(wsdlContent);\n    \n    // Save Bruno collection\n    await fs.writeFile('soap-collection.json', JSON.stringify(brunoCollection, null, 2));\n    \n    console.log('WSDL import successful!');\n  } catch (error) {\n    console.error('Error during WSDL import:', error);\n  }\n}\n\nimportWSDL();\n```\n\n### CLI Usage\n\nYou can also use the Bruno CLI to import WSDL files:\n\n```bash\n# Import WSDL file to a directory\nbruno import wsdl --source service.wsdl --output ~/Desktop/soap-collection --collection-name \"SOAP Service\"\n\n# Import WSDL from URL\nbruno import wsdl --source https://example.com/service.wsdl --output ~/Desktop --collection-name \"Remote SOAP Service\"\n\n# Import WSDL and save as JSON file\nbruno import wsdl --source service.wsdl --output-file ~/Desktop/soap-collection.json --collection-name \"SOAP Service\"\n```\n\n## Supported Formats\n\n- **Postman Collections** (v2.1)\n- **Insomnia Collections** (v4 and v5)\n- **OpenAPI Specifications** (v3.0)\n- **WSDL Files** (Web Services Description Language)\n\n## Dependencies\n\n- `lodash` - Utility functions\n- `nanoid` - UUID generation\n- `js-yaml` - YAML parsing\n- `xml2js` - XML parsing for WSDL\n- `@usebruno/schema` - Schema validation\n"
  },
  {
    "path": "packages/bruno-converters/rollup.config.js",
    "content": "const { nodeResolve } = require('@rollup/plugin-node-resolve');\nconst commonjs = require('@rollup/plugin-commonjs');\nconst typescript = require('@rollup/plugin-typescript');\nconst { terser } = require('rollup-plugin-terser');\nconst peerDepsExternal = require('rollup-plugin-peer-deps-external');\nconst { copy } = require('@web/rollup-plugin-copy');\nconst path = require('path');\n\nconst packageJson = require('./package.json');\n\nconst externalDeps = [\n  '@usebruno/schema',\n  '@usebruno/schema-types',\n  /@usebruno\\/schema-types\\/.*/,\n  '@opencollection/types',\n  /@opencollection\\/types\\/.*/,\n  // Runtime dependencies\n  'lodash',\n  'lodash/each',\n  'lodash/get',\n  'lodash/cloneDeep',\n  'lodash/map',\n  'js-yaml',\n  'jscodeshift',\n  'nanoid',\n  'xml2js',\n  // Node built-ins\n  'path',\n  'fs'\n];\n\nmodule.exports = [\n  {\n    input: 'src/index.js',\n    output: {\n      dir: path.dirname(packageJson.main),\n      format: 'cjs',\n      sourcemap: true,\n      exports: 'named',\n      entryFileNames: 'index.js'\n    },\n    plugins: [\n      peerDepsExternal(),\n      nodeResolve({\n        extensions: ['.js', '.ts', '.tsx', '.json']\n      }),\n      commonjs(),\n      typescript({\n        tsconfig: './tsconfig.json',\n        sourceMap: true,\n        outDir: path.dirname(packageJson.main)\n      }),\n      terser(),\n      copy({\n        patterns: 'src/workers/scripts/**/*',\n        rootDir: '.'\n      })\n    ],\n    external: externalDeps\n  },\n  {\n    input: 'src/index.js',\n    output: {\n      dir: path.dirname(packageJson.module),\n      format: 'esm',\n      sourcemap: true,\n      exports: 'named',\n      entryFileNames: 'index.js'\n    },\n    plugins: [\n      peerDepsExternal(),\n      nodeResolve({\n        extensions: ['.js', '.ts', '.tsx', '.json']\n      }),\n      commonjs(),\n      typescript({\n        tsconfig: './tsconfig.json',\n        sourceMap: true,\n        outDir: path.dirname(packageJson.module)\n      }),\n      terser(),\n      copy({\n        patterns: 'src/workers/scripts/**/*',\n        rootDir: '.'\n      })\n    ],\n    external: externalDeps\n  }\n];\n"
  },
  {
    "path": "packages/bruno-converters/src/common/index.js",
    "content": "import each from 'lodash/each';\nimport get from 'lodash/get';\nimport { customAlphabet } from 'nanoid';\nimport cloneDeep from 'lodash/cloneDeep';\nimport { collectionSchema } from '@usebruno/schema';\n\nexport const safeParseJSON = (str) => {\n  if (!str || !str.length || typeof str !== 'string') {\n    return str;\n  }\n  try {\n    return JSON.parse(str);\n  } catch (e) {\n    return str;\n  }\n};\n\nexport const safeStringifyJSON = (obj, indent = false) => {\n  if (obj === undefined) {\n    return obj;\n  }\n  try {\n    if (indent) {\n      return JSON.stringify(obj, null, 2);\n    }\n    return JSON.stringify(obj);\n  } catch (e) {\n    return obj;\n  }\n};\n\nexport const isItemARequest = (item) => {\n  return item.hasOwnProperty('request') && ['http-request', 'graphql-request'].includes(item.type) && !item.items;\n};\n\n// a customized version of nanoid without using _ and -\nexport const uuid = () => {\n  // https://github.com/ai/nanoid/blob/main/url-alphabet/index.js\n  const urlAlphabet = 'useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict';\n  const customNanoId = customAlphabet(urlAlphabet, 21);\n\n  return customNanoId();\n};\n\n/**\n * Sanitizes a tag name for BRU format compatibility.\n * BRU format only supports tag names containing alphanumeric characters,\n * hyphens (-), and underscores (_). Spaces are replaced with underscores.\n *\n * @param {string} tag - The tag to sanitize\n * @param {Object} options - Sanitization options\n * @param {string} options.collectionFormat - The collection format ('yml' for OpenCollection YAML)\n * @returns {string|null} - The sanitized tag, or null if the result is empty\n */\nexport const sanitizeTag = (tag, options = {}) => {\n  const typeofTag = typeof tag;\n  if (!tag || !['string', 'object'].includes(typeofTag)) {\n    return null;\n  }\n\n  let usableTagString = typeof tag == 'string' ? tag : 'name' in tag ? tag.name : '';\n\n  let sanitized = usableTagString.trim();\n\n  // BRU format only supports alphanumeric, hyphens, and underscores in tags\n  // The BRU grammar defines listitem as: (alnum | \"_\" | \"-\")+\n  // Spaces are NOT allowed, so we replace them with underscores\n\n  // Replace spaces with underscores first\n  sanitized = sanitized.replace(/\\s+/g, '_');\n\n  // Replace any character that's NOT alphanumeric, hyphen, or underscore with underscore\n  sanitized = sanitized.replace(/[^\\p{L}\\p{N}\\-_]/gu, '_');\n\n  // Collapse multiple consecutive underscores into one\n  sanitized = sanitized.replace(/_+/g, '_');\n\n  // Remove leading characters that aren't alphanumeric\n  sanitized = sanitized.replace(/^[^\\p{L}\\p{N}]+/gu, '');\n\n  // Remove trailing characters that aren't alphanumeric\n  sanitized = sanitized.replace(/[^\\p{L}\\p{N}]+$/gu, '');\n\n  // Return null if the result is empty\n  return sanitized || null;\n};\n\n/**\n * Sanitizes an array of tags, removing duplicates and null values.\n *\n * @param {string[]} tags - Array of tags to sanitize\n * @param {Object} options - Sanitization options\n * @returns {string[]} - Array of unique sanitized tags\n */\nexport const sanitizeTags = (tags, options = {}) => {\n  if (!Array.isArray(tags)) {\n    return [];\n  }\n\n  return [...new Set(\n    tags\n      .map((tag) => sanitizeTag(tag, options))\n      .filter((tag) => tag !== null)\n  )];\n};\n\nexport const validateSchema = (collection = {}) => {\n  try {\n    collectionSchema.validateSync(collection);\n    return collection;\n  } catch (err) {\n    console.log('Error validating schema', err);\n    throw new Error('The Collection has an invalid schema');\n  }\n};\n\nexport const updateUidsInCollection = (_collection) => {\n  const collection = cloneDeep(_collection);\n\n  collection.uid = uuid();\n\n  const updateItemUids = (items = []) => {\n    each(items, (item) => {\n      item.uid = uuid();\n\n      each(get(item, 'request.headers'), (header) => (header.uid = uuid()));\n      each(get(item, 'request.params'), (param) => (param.uid = uuid()));\n      each(get(item, 'request.vars.req'), (v) => (v.uid = uuid()));\n      each(get(item, 'request.vars.res'), (v) => (v.uid = uuid()));\n      each(get(item, 'request.assertions'), (a) => (a.uid = uuid()));\n      each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid()));\n      each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid()));\n      each(get(item, 'request.body.file'), (param) => (param.uid = uuid()));\n\n      if (item.items && item.items.length) {\n        updateItemUids(item.items);\n      }\n    });\n  };\n  updateItemUids(collection.items);\n\n  const updateEnvUids = (envs = []) => {\n    each(envs, (env) => {\n      env.uid = uuid();\n      each(env.variables, (variable) => (variable.uid = uuid()));\n    });\n  };\n  updateEnvUids(collection.environments);\n\n  return collection;\n};\n\n// todo\n// need to eventually get rid of supporting old collection app models\n// 1. start with making request type a constant fetched from a single place\n// 2. move references of param and replace it with query inside the app\nexport const transformItemsInCollection = (collection) => {\n  const transformItems = (items = []) => {\n    each(items, (item) => {\n      if (['http', 'graphql'].includes(item.type)) {\n        item.type = `${item.type}-request`;\n\n        if (item.request.query) {\n          item.request.params = item.request.query.map((queryItem) => ({\n            ...queryItem,\n            type: 'query',\n            uid: queryItem.uid || uuid()\n          }));\n        }\n\n        delete item.request.query;\n\n        // from 5 feb 2024, multipartFormData needs to have a type\n        // this was introduced when we added support for file uploads\n        // below logic is to make older collection exports backward compatible\n        let multipartFormData = get(item, 'request.body.multipartForm');\n        if (multipartFormData) {\n          each(multipartFormData, (form) => {\n            if (!form.type) {\n              form.type = 'text';\n            }\n          });\n        }\n      }\n\n      if (item.items && item.items.length) {\n        transformItems(item.items);\n      }\n    });\n  };\n\n  transformItems(collection.items);\n\n  return collection;\n};\n\nexport const hydrateSeqInCollection = (collection) => {\n  const hydrateSeq = (items = []) => {\n    let index = 1;\n    each(items, (item) => {\n      if (isItemARequest(item) && !item.seq) {\n        item.seq = index;\n        index++;\n      }\n      if (item.items && item.items.length) {\n        hydrateSeq(item.items);\n      }\n    });\n  };\n  hydrateSeq(collection.items);\n\n  return collection;\n};\n\nexport const deleteUidsInItems = (items) => {\n  each(items, (item) => {\n    delete item.uid;\n\n    if (['http-request', 'graphql-request', 'grpc-request'].includes(item.type)) {\n      each(get(item, 'request.headers'), (header) => delete header.uid);\n      each(get(item, 'request.params'), (param) => delete param.uid);\n      each(get(item, 'request.vars.req'), (v) => delete v.uid);\n      each(get(item, 'request.vars.res'), (v) => delete v.uid);\n      each(get(item, 'request.vars.assertions'), (a) => delete a.uid);\n      each(get(item, 'request.body.multipartForm'), (param) => delete param.uid);\n      each(get(item, 'request.body.formUrlEncoded'), (param) => delete param.uid);\n      each(get(item, 'request.body.file'), (param) => delete param.uid);\n    }\n\n    if (item.items && item.items.length) {\n      deleteUidsInItems(item.items);\n    }\n  });\n};\n\n/**\n * Some of the models in the app are not consistent with the Collection Json format\n * This function is used to transform the models to the Collection Json format\n */\nexport const transformItem = (items = []) => {\n  each(items, (item) => {\n    if (['http-request', 'graphql-request'].includes(item.type)) {\n      if (item.type === 'graphql-request') {\n        item.type = 'graphql';\n      }\n\n      if (item.type === 'http-request') {\n        item.type = 'http';\n      }\n    }\n\n    if (item.items && item.items.length) {\n      transformItem(item.items);\n    }\n  });\n};\n\nexport const deleteUidsInEnvs = (envs) => {\n  each(envs, (env) => {\n    delete env.uid;\n    each(env.variables, (variable) => delete variable.uid);\n  });\n};\n\nexport const deleteSecretsInEnvs = (envs) => {\n  each(envs, (env) => {\n    each(env.variables, (variable) => {\n      if (variable.secret) {\n        variable.value = '';\n      }\n    });\n  });\n};\n"
  },
  {
    "path": "packages/bruno-converters/src/constants/index.js",
    "content": "import { invalidVariableCharacterRegex } from './regex';\n\nexport { invalidVariableCharacterRegex };\n"
  },
  {
    "path": "packages/bruno-converters/src/constants/regex.js",
    "content": "export const invalidVariableCharacterRegex = /[^\\w-.]/g;\n"
  },
  {
    "path": "packages/bruno-converters/src/index.js",
    "content": "export { default as postmanToBruno } from './postman/postman-to-bruno.js';\nexport { default as postmanToBrunoEnvironment } from './postman/postman-env-to-bruno-env.js';\nexport { default as brunoToPostman } from './postman/bruno-to-postman.js';\nexport { default as openApiToBruno } from './openapi/openapi-to-bruno.js';\nexport { default as insomniaToBruno } from './insomnia/insomnia-to-bruno.js';\nexport { default as wsdlToBruno } from './wsdl/wsdl-to-bruno.js';\nexport { default as postmanTranslation } from './postman/postman-translations.js';\nexport { openCollectionToBruno } from './opencollection/opencollection-to-bruno.js';\nexport { brunoToOpenCollection } from './opencollection/bruno-to-opencollection.js';\n"
  },
  {
    "path": "packages/bruno-converters/src/insomnia/env-utils.js",
    "content": "import { uuid } from '../common';\nimport { flattenObject } from '../utils/flatten';\n\n/**\n * Converts an Insomnia environment node into a Bruno environment using JSON-path-like keys.\n * - Flattens env.data to dot-notation keys; values are converted to strings.\n */\nexport const toBrunoEnv = (env, index = 0) => {\n  const variables = [];\n  const flatEnvData = flattenObject(env?.data || {});\n  Object.entries(flatEnvData).forEach(([name, value]) => {\n    variables.push({\n      uid: uuid(),\n      name,\n      value: String(value),\n      type: 'text',\n      enabled: true,\n      secret: false\n    });\n  });\n\n  return {\n    uid: uuid(),\n    name: (env?.name && String(env.name).trim()) || `Environment ${index + 1}`,\n    variables\n  };\n};\n\n/**\n * Shallowly merges two flattened env data objects.\n * - Keys in override replace keys in base.\n * - No recursive merging.\n */\nconst shallowMergeFlat = (baseFlat = {}, overrideFlat = {}) => ({ ...baseFlat, ...overrideFlat });\n\n/**\n * Builds Bruno environments from Insomnia v5 environments.\n * - Expects a single object (base env) with optional subEnvironments.\n * - Creates one env for base and one env per sub using flattened, shallow-merged keys.\n */\nexport const buildV5Environments = (baseEnv) => {\n  if (!baseEnv || typeof baseEnv !== 'object') return [];\n\n  const result = [];\n\n  // include base as standalone\n  result.push(toBrunoEnv(baseEnv));\n\n  const subs = Array.isArray(baseEnv.subEnvironments) ? baseEnv.subEnvironments : [];\n  const baseFlat = flattenObject(baseEnv?.data || {});\n  subs.forEach((sub, i) => {\n    const subFlat = flattenObject(sub?.data || {});\n    const mergedFlat = shallowMergeFlat(baseFlat, subFlat);\n    result.push(toBrunoEnv({ name: sub?.name, data: mergedFlat }, i + 1));\n  });\n  return result;\n};\n\n/**\n * Builds Bruno environments from Insomnia v4 resources.\n * - Base env: parentId equals workspaceId; included as-is (flattened).\n * - Sub envs: merge base (flattened) with sub (flattened) and import.\n *\n * Note: Insomnia supports only ONE base environment per workspace.\n */\nexport const buildV4Environments = (resources, workspaceId) => {\n  const allEnvResources = resources.filter((r) => r._type === 'environment') || [];\n  const envById = {};\n  allEnvResources.forEach((e) => (envById[e._id] = e));\n\n  const isBaseEnv = (env) => env.parentId === workspaceId;\n\n  const result = [];\n\n  const baseEnv = allEnvResources.find(isBaseEnv);\n  if (baseEnv) {\n    result.push(toBrunoEnv(baseEnv));\n  }\n\n  // sub envs - all inherit from the single base environment\n  const subEnvs = allEnvResources.filter((e) => !isBaseEnv(e));\n  const baseFlat = flattenObject(baseEnv?.data || {});\n  subEnvs.forEach((sub, idx) => {\n    const subFlat = flattenObject(sub.data || {});\n    const mergedFlat = shallowMergeFlat(baseFlat, subFlat);\n    result.push(toBrunoEnv({ name: sub.name, data: mergedFlat }, idx + 1));\n  });\n\n  return result;\n};\n"
  },
  {
    "path": "packages/bruno-converters/src/insomnia/insomnia-to-bruno.js",
    "content": "import each from 'lodash/each';\nimport get from 'lodash/get';\nimport jsyaml from 'js-yaml';\nimport { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common';\nimport { buildV5Environments, buildV4Environments } from './env-utils';\n\nconst parseGraphQL = (text) => {\n  try {\n    const graphql = JSON.parse(text);\n\n    return {\n      query: normalizeVariables(graphql.query),\n      variables: JSON.stringify(graphql.variables, null, 2)\n    };\n  } catch (e) {\n    return {\n      query: '',\n      variables: ''\n    };\n  }\n};\n\nconst addSuffixToDuplicateName = (item, index, allItems) => {\n  // Check if the request name already exist and if so add a number suffix\n  const nameSuffix = allItems.reduce((nameSuffix, otherItem, otherIndex) => {\n    if (otherItem.name === item.name && otherIndex < index) {\n      nameSuffix++;\n    }\n    return nameSuffix;\n  }, 0);\n  return nameSuffix !== 0 ? `${item.name}_${nameSuffix}` : item.name;\n};\n\nconst regexVariable = new RegExp('{{.*?}}', 'g');\n\nconst normalizeVariables = (value) => {\n  value = value || '';\n  const variables = value.match(regexVariable) || [];\n  each(variables, (variable) => {\n    value = value.replace(variable, variable.replace('_.', '').replaceAll(' ', ''));\n  });\n  return value;\n};\n\nconst transformInsomniaRequestItem = (request, index, allRequests) => {\n  const name = addSuffixToDuplicateName(request, index, allRequests);\n\n  const brunoRequestItem = {\n    uid: uuid(),\n    name,\n    type: 'http-request',\n    request: {\n      url: normalizeVariables(request.url),\n      method: request.method,\n      auth: {\n        mode: 'none',\n        basic: null,\n        bearer: null,\n        digest: null\n      },\n      headers: [],\n      params: [],\n      body: {\n        mode: 'none',\n        json: null,\n        text: null,\n        xml: null,\n        formUrlEncoded: [],\n        multipartForm: []\n      }\n    }\n  };\n\n  each(request.headers, (header) => {\n    brunoRequestItem.request.headers.push({\n      uid: uuid(),\n      name: header.name,\n      value: normalizeVariables(header.value),\n      description: header.description,\n      enabled: !header.disabled\n    });\n  });\n\n  each(request.parameters, (param) => {\n    brunoRequestItem.request.params.push({\n      uid: uuid(),\n      name: param.name,\n      value: normalizeVariables(param.value),\n      description: param.description,\n      type: 'query',\n      enabled: !param.disabled\n    });\n  });\n\n  each(request.pathParameters, (param) => {\n    brunoRequestItem.request.params.push({\n      uid: uuid(),\n      name: param.name,\n      value: normalizeVariables(param.value),\n      description: '',\n      type: 'path',\n      enabled: true\n    });\n  });\n\n  const authType = get(request, 'authentication.type', '');\n\n  if (authType === 'basic') {\n    brunoRequestItem.request.auth.mode = 'basic';\n    brunoRequestItem.request.auth.basic = {\n      username: normalizeVariables(get(request, 'authentication.username', '')),\n      password: normalizeVariables(get(request, 'authentication.password', ''))\n    };\n  } else if (authType === 'bearer') {\n    brunoRequestItem.request.auth.mode = 'bearer';\n    brunoRequestItem.request.auth.bearer = {\n      token: normalizeVariables(get(request, 'authentication.token', ''))\n    };\n  }\n\n  const mimeType = get(request, 'body.mimeType', '').split(';')[0];\n\n  if (mimeType === 'application/json') {\n    brunoRequestItem.request.body.mode = 'json';\n    brunoRequestItem.request.body.json = normalizeVariables(request.body.text);\n  } else if (mimeType === 'application/x-www-form-urlencoded') {\n    brunoRequestItem.request.body.mode = 'formUrlEncoded';\n    each(request.body.params, (param) => {\n      brunoRequestItem.request.body.formUrlEncoded.push({\n        uid: uuid(),\n        name: param.name,\n        value: normalizeVariables(param.value),\n        description: param.description,\n        enabled: !param.disabled\n      });\n    });\n  } else if (mimeType === 'multipart/form-data') {\n    brunoRequestItem.request.body.mode = 'multipartForm';\n    each(request.body.params, (param) => {\n      brunoRequestItem.request.body.multipartForm.push({\n        uid: uuid(),\n        type: 'text',\n        name: param.name,\n        value: normalizeVariables(param.value),\n        description: param.description,\n        enabled: !param.disabled\n      });\n    });\n  } else if (mimeType === 'text/plain') {\n    brunoRequestItem.request.body.mode = 'text';\n    brunoRequestItem.request.body.text = normalizeVariables(request.body.text);\n  } else if (mimeType === 'text/xml' || mimeType === 'application/xml') {\n    brunoRequestItem.request.body.mode = 'xml';\n    brunoRequestItem.request.body.xml = normalizeVariables(request.body.text);\n  } else if (mimeType === 'application/graphql') {\n    brunoRequestItem.type = 'graphql-request';\n    brunoRequestItem.request.body.mode = 'graphql';\n    brunoRequestItem.request.body.graphql = parseGraphQL(request.body.text);\n  }\n\n  const settings = {\n    encodeUrl: request.settings?.encodeUrl !== false && request.settingEncodeUrl !== false // handles v4 and v5 import\n  };\n\n  brunoRequestItem.settings = settings;\n\n  return brunoRequestItem;\n};\n\nconst isInsomniaV5Export = (data) => {\n  // V5 format has a type property at the root level\n  if (data.type && data.type.startsWith('collection.insomnia.rest/5')) {\n    return true;\n  }\n  return false;\n};\n\nconst parseInsomniaV5Collection = (data) => {\n  const brunoCollection = {\n    name: data.name || 'Untitled Collection',\n    uid: uuid(),\n    version: '1',\n    items: [],\n    environments: []\n  };\n\n  try {\n    // Parse the collection items\n    const parseCollectionItems = (items, allItems = []) => {\n      if (!Array.isArray(items)) {\n        throw new Error('Invalid items format: expected array');\n      }\n\n      return items.map((item, index) => {\n        if (!item) {\n          return null;\n        }\n\n        // In v5, requests might be defined with method property or meta.type\n        if (item.method && item.url) {\n          const request = {\n            _id: item.meta?.id || uuid(),\n            name: item.name || 'Untitled Request',\n            url: item.url || '',\n            method: item.method || '',\n            headers: item.headers || [],\n            parameters: item.parameters || [],\n            pathParameters: item.pathParameters || [],\n            authentication: item.authentication || {},\n            body: item.body || {},\n            settings: item.settings || {}\n          };\n          return transformInsomniaRequestItem(request, index, allItems);\n        } else if (item.children && Array.isArray(item.children)) {\n          // Process folder\n          return {\n            uid: uuid(),\n            name: item.name || 'Untitled Folder',\n            type: 'folder',\n            items: parseCollectionItems(item.children, item.children)\n          };\n        }\n        return null;\n      }).filter(Boolean);\n    };\n\n    if (data.collection && Array.isArray(data.collection)) {\n      brunoCollection.items = parseCollectionItems(data.collection, data.collection);\n    }\n\n    // Parse environments if available\n    if (data.environments) {\n      brunoCollection.environments = buildV5Environments(data.environments);\n    }\n\n    return brunoCollection;\n  } catch (err) {\n    console.error('Error parsing collection:', err);\n    throw new Error('An error occurred while parsing the Insomnia v5 collection: ' + err.message);\n  }\n};\n\nconst parseInsomniaCollection = (data) => {\n  const brunoCollection = {\n    name: '',\n    uid: uuid(),\n    version: '1',\n    items: [],\n    environments: []\n  };\n\n  try {\n    const insomniaResources = get(data, 'resources', []);\n    const insomniaCollection = insomniaResources.find((resource) => resource._type === 'workspace');\n\n    if (!insomniaCollection) {\n      throw new Error('Collection not found inside Insomnia export');\n    }\n\n    brunoCollection.name = insomniaCollection.name;\n\n    const requestsAndFolders\n      = insomniaResources.filter((resource) => resource._type === 'request' || resource._type === 'request_group')\n        || [];\n\n    function createFolderStructure(resources, parentId = null) {\n      const requestGroups\n        = resources.filter((resource) => resource._type === 'request_group' && resource.parentId === parentId) || [];\n      const requests = resources.filter((resource) => resource._type === 'request' && resource.parentId === parentId);\n\n      const folders = requestGroups.map((folder, index, allFolder) => {\n        const name = addSuffixToDuplicateName(folder, index, allFolder);\n        const requests = resources.filter(\n          (resource) => resource._type === 'request' && resource.parentId === folder._id\n        );\n\n        return {\n          uid: uuid(),\n          name,\n          type: 'folder',\n          items: createFolderStructure(resources, folder._id).concat(\n            requests.filter((r) => r.parentId === folder._id).map(transformInsomniaRequestItem)\n          )\n        };\n      });\n\n      return folders.concat(requests.map(transformInsomniaRequestItem));\n    }\n\n    brunoCollection.items = createFolderStructure(requestsAndFolders, insomniaCollection._id);\n\n    // Build environments from resources\n    brunoCollection.environments = buildV4Environments(insomniaResources, insomniaCollection._id);\n    return brunoCollection;\n  } catch (err) {\n    console.error('Error parsing collection:', err);\n    throw new Error('An error occurred while parsing the Insomnia collection: ' + err.message);\n  }\n};\n\nexport const insomniaToBruno = (insomniaCollection) => {\n  try {\n    if (typeof insomniaCollection !== 'object') {\n      insomniaCollection = jsyaml.load(insomniaCollection, { schema: jsyaml.JSON_SCHEMA });\n    }\n    let collection;\n    if (isInsomniaV5Export(insomniaCollection)) {\n      collection = parseInsomniaV5Collection(insomniaCollection);\n    } else {\n      collection = parseInsomniaCollection(insomniaCollection);\n    }\n\n    const transformedCollection = transformItemsInCollection(collection);\n    const hydratedCollection = hydrateSeqInCollection(transformedCollection);\n    const validatedCollection = validateSchema(hydratedCollection);\n    return validatedCollection;\n  } catch (err) {\n    console.error(err);\n    throw new Error('Import collection failed: ' + err.message);\n  }\n};\n\nexport default insomniaToBruno;\n"
  },
  {
    "path": "packages/bruno-converters/src/openapi/openapi-to-bruno.js",
    "content": "import each from 'lodash/each';\nimport get from 'lodash/get';\nimport jsyaml from 'js-yaml';\nimport { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid, sanitizeTag, sanitizeTags } from '../common';\n\n// Content type patterns for matching MIME type variants\n// These patterns handle structured types with many variants (e.g., application/ld+json, application/vnd.api+json)\n// MIME types can contain: letters, numbers, hyphens, dots, and plus signs\nconst CONTENT_TYPE_PATTERNS = {\n  // Matches: application/json, application/ld+json, application/vnd.api+json, text/json, etc.\n  // Pattern: type/([base]+)?suffix where suffix is json\n  JSON: /^[\\w\\-.+]+\\/([\\w\\-.+]+\\+)?json$/,\n  // Matches: application/xml, text/xml, application/atom+xml, application/rss+xml, application/xhtml+xml, etc.\n  // Pattern: type/([base]+)?suffix where suffix is xml\n  XML: /^[\\w\\-.+]+\\/([\\w\\-.+]+\\+)?xml$/,\n  // Matches: text/html\n  // Pattern: type/([base]+)?suffix where suffix is html\n  HTML: /^[\\w\\-.+]+\\/([\\w\\-.+]+\\+)?html$/\n};\n\nconst ensureUrl = (url) => {\n  // removing multiple slashes after the protocol if it exists, or after the beginning of the string otherwise\n  return url.replace(/([^:])\\/{2,}/g, '$1/');\n};\n\nconst getStatusText = (statusCode) => {\n  const statusTexts = {\n    100: 'Continue',\n    101: 'Switching Protocols',\n    102: 'Processing',\n    103: 'Early Hints',\n    200: 'OK',\n    201: 'Created',\n    202: 'Accepted',\n    203: 'Non-Authoritative Information',\n    204: 'No Content',\n    205: 'Reset Content',\n    206: 'Partial Content',\n    207: 'Multi-Status',\n    208: 'Already Reported',\n    226: 'IM Used',\n    300: 'Multiple Choice',\n    301: 'Moved Permanently',\n    302: 'Found',\n    303: 'See Other',\n    304: 'Not Modified',\n    305: 'Use Proxy',\n    306: 'unused',\n    307: 'Temporary Redirect',\n    308: 'Permanent Redirect',\n    400: 'Bad Request',\n    401: 'Unauthorized',\n    402: 'Payment Required',\n    403: 'Forbidden',\n    404: 'Not Found',\n    405: 'Method Not Allowed',\n    406: 'Not Acceptable',\n    407: 'Proxy Authentication Required',\n    408: 'Request Timeout',\n    409: 'Conflict',\n    410: 'Gone',\n    411: 'Length Required',\n    412: 'Precondition Failed',\n    413: 'Payload Too Large',\n    414: 'URI Too Long',\n    415: 'Unsupported Media Type',\n    416: 'Range Not Satisfiable',\n    417: 'Expectation Failed',\n    418: 'I\\'m a teapot',\n    421: 'Misdirected Request',\n    422: 'Unprocessable Entity',\n    423: 'Locked',\n    424: 'Failed Dependency',\n    425: 'Too Early',\n    426: 'Upgrade Required',\n    428: 'Precondition Required',\n    429: 'Too Many Requests',\n    431: 'Request Header Fields Too Large',\n    451: 'Unavailable For Legal Reasons',\n    500: 'Internal Server Error',\n    501: 'Not Implemented',\n    502: 'Bad Gateway',\n    503: 'Service Unavailable',\n    504: 'Gateway Timeout',\n    505: 'HTTP Version Not Supported',\n    506: 'Variant Also Negotiates',\n    507: 'Insufficient Storage',\n    508: 'Loop Detected',\n    510: 'Not Extended',\n    511: 'Network Authentication Required'\n  };\n  return statusTexts[statusCode] || 'Unknown';\n};\n\n/**\n * Determines the body type based on content-type from OpenAPI spec\n * Uses pattern matching to handle various MIME type variants (e.g., application/ld+json, application/vnd.api+json)\n * @param {string} contentType - The content-type from OpenAPI spec (object key, e.g., \"application/json\")\n * @returns {string} - The body type (json, xml, html, text)\n */\nconst getBodyTypeFromContentType = (contentType) => {\n  if (!contentType || typeof contentType !== 'string') {\n    return 'text';\n  }\n\n  // Normalize: lowercase (object keys may vary in case, but shouldn't have parameters or whitespace)\n  const normalizedContentType = contentType.toLowerCase();\n\n  if (CONTENT_TYPE_PATTERNS.JSON.test(normalizedContentType)) {\n    return 'json';\n  } else if (CONTENT_TYPE_PATTERNS.XML.test(normalizedContentType)) {\n    return 'xml';\n  } else if (CONTENT_TYPE_PATTERNS.HTML.test(normalizedContentType)) {\n    return 'html';\n  }\n\n  return 'text';\n};\n\n/**\n * Gets a default value for a schema based on its type, format, and constraints\n * Prioritizes: explicit example > enum first value > format-specific example > type default\n * @param {Object} schema - The OpenAPI schema object\n * @param {Map} visited - Map to track circular references\n * @returns {*} - The default value for the schema\n */\nconst getDefaultValueForSchema = (schema, visited = new Map()) => {\n  // Check for explicit example first\n  if (schema.example !== undefined) {\n    return schema.example;\n  }\n\n  // Check for enum and use first value\n  if (schema.enum && schema.enum.length > 0) {\n    return schema.enum[0];\n  }\n\n  // Handle different types\n  if (schema.type === 'object' || schema.properties) {\n    return buildEmptyJsonBody(schema, visited);\n  }\n\n  if (schema.type === 'array') {\n    // Check for array-level example\n    if (schema.example !== undefined) {\n      return schema.example;\n    }\n\n    if (schema.items) {\n      if (schema.items.type === 'object' || schema.items.properties) {\n        return [buildEmptyJsonBody(schema.items, visited)];\n      }\n      // For primitive arrays, get example from items\n      if (schema.items.example !== undefined) {\n        return Array.isArray(schema.items.example) ? schema.items.example : [schema.items.example];\n      }\n      // Return array with a single default primitive value\n      const itemDefault = getDefaultValueForSchema(schema.items, visited);\n      if (itemDefault !== '' && itemDefault !== 0 && itemDefault !== false) {\n        return [itemDefault];\n      }\n    }\n    return [];\n  }\n\n  if (schema.type === 'integer' || schema.type === 'number') {\n    return 0;\n  }\n\n  if (schema.type === 'boolean') {\n    return false;\n  }\n\n  // Default for strings and other types\n  return '';\n};\n\n/**\n * Builds XML string from OpenAPI schema\n * @param {Object} bodySchema - The OpenAPI schema object\n * @returns {string} - XML string\n */\nconst buildXmlBody = (bodySchema) => {\n  if (!bodySchema) return '';\n\n  // String example = raw XML, return as-is\n  if (typeof bodySchema.example === 'string') {\n    return bodySchema.example;\n  }\n\n  const exampleValues = typeof bodySchema.example === 'object' ? bodySchema.example : null;\n\n  if (!bodySchema.properties && !exampleValues) return '';\n\n  const rootName = bodySchema.xml?.name || 'root';\n\n  // Build a single XML element\n  const buildElement = (name, prop = {}, value, indent = '  ') => {\n    const xmlName = prop.xml?.name || name;\n\n    if (prop.xml?.attribute) return null;\n\n    // Nested object - recurse into children\n    if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\n      const children = Object.entries(value)\n        .map(([k, v]) => buildElement(k, prop.properties?.[k] || {}, v, indent + '  '))\n        .filter(Boolean);\n      return `${indent}<${xmlName}>${children.length ? '\\n' + children.join('\\n') + '\\n' + indent : ''}</${xmlName}>`;\n    }\n\n    // Object schema without value - build empty structure from schema\n    if (prop.type === 'object' || prop.properties) {\n      const children = Object.entries(prop.properties || {})\n        .map(([k, p]) => buildElement(k, p, undefined, indent + '  '))\n        .filter(Boolean);\n      return `${indent}<${xmlName}>${children.length ? '\\n' + children.join('\\n') + '\\n' + indent : ''}</${xmlName}>`;\n    }\n\n    // Primitive value\n    const content = value != null ? String(value) : '';\n    return `${indent}<${xmlName}>${content}</${xmlName}>`;\n  };\n\n  // Collect attributes\n  const attributes = Object.entries(bodySchema.properties || {})\n    .filter(([, p]) => p.xml?.attribute)\n    .map(([name, p]) => `${p.xml?.name || name}=\"${exampleValues?.[name] ?? ''}\"`);\n\n  // Build child elements\n  const entries = bodySchema.properties\n    ? Object.entries(bodySchema.properties).map(([k, p]) => [k, p, exampleValues?.[k]])\n    : Object.entries(exampleValues || {}).map(([k, v]) => [k, {}, v]);\n\n  const children = entries\n    .map(([name, prop, value]) => buildElement(name, prop, value))\n    .filter(Boolean);\n\n  const attrStr = attributes.length ? ' ' + attributes.join(' ') : '';\n  const childrenStr = children.length ? '\\n' + children.join('\\n') + '\\n' : '';\n\n  return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n<${rootName}${attrStr}>${childrenStr}</${rootName}>`;\n};\n\nconst buildEmptyJsonBody = (bodySchema, visited = new Map()) => {\n  // Check for circular references\n  if (visited.has(bodySchema)) {\n    return {};\n  }\n\n  // Add this schema to visited map\n  visited.set(bodySchema, true);\n\n  let _jsonBody = {};\n  each(bodySchema.properties || {}, (prop, name) => {\n    _jsonBody[name] = getDefaultValueForSchema(prop, visited);\n  });\n  return _jsonBody;\n};\n\n/**\n * Body type handlers for different content types\n * Each handler has:\n * - match: function to test if this handler should process the mime type\n * - mode: the Bruno body mode to set\n * - handle: function to populate the body content\n */\nconst BODY_TYPE_HANDLERS = [\n  {\n    match: (mimeType) => CONTENT_TYPE_PATTERNS.JSON.test(mimeType),\n    mode: 'json',\n    handle: (body, bodySchema) => {\n      if (bodySchema) {\n        if (bodySchema.example !== undefined) {\n          body.json = JSON.stringify(bodySchema.example, null, 2);\n        } else if (bodySchema.type === 'array') {\n          body.json = JSON.stringify(bodySchema.items ? [buildEmptyJsonBody(bodySchema.items)] : [], null, 2);\n        } else {\n          body.json = JSON.stringify(buildEmptyJsonBody(bodySchema), null, 2);\n        }\n      }\n    }\n  },\n  {\n    match: (mimeType) => mimeType === 'application/x-www-form-urlencoded',\n    mode: 'formUrlEncoded',\n    handle: (body, bodySchema) => {\n      if (!bodySchema) return;\n      const fields = bodySchema.example || bodySchema.properties || {};\n      const isExample = !!bodySchema.example;\n\n      each(fields, (prop, name) => {\n        const value = isExample ? prop : (prop.example ?? prop.default ?? '');\n        body.formUrlEncoded.push({\n          uid: uuid(),\n          name,\n          value: value !== undefined ? String(value) : '',\n          description: prop.description || '',\n          enabled: true\n        });\n      });\n    }\n  },\n  {\n    match: (mimeType) => mimeType === 'multipart/form-data',\n    mode: 'multipartForm',\n    handle: (body, bodySchema) => {\n      if (!bodySchema) return;\n      const fields = bodySchema.example || bodySchema.properties || {};\n      const isExample = !!bodySchema.example;\n\n      each(fields, (prop, name) => {\n        const isFileField = !isExample && prop.type === 'string' && prop.format === 'binary';\n        const value = isFileField ? [] : isExample ? prop : (prop.example ?? prop.default ?? '');\n        body.multipartForm.push({\n          uid: uuid(),\n          type: isFileField ? 'file' : 'text',\n          name,\n          value: isFileField ? [] : (value !== undefined ? String(value) : ''),\n          description: prop.description || '',\n          enabled: true\n        });\n      });\n    }\n  },\n  {\n    match: (mimeType) => CONTENT_TYPE_PATTERNS.XML.test(mimeType) || mimeType === 'application/xml',\n    mode: 'xml',\n    handle: (body, bodySchema) => {\n      body.xml = buildXmlBody(bodySchema);\n    }\n  },\n  {\n    match: (mimeType) => mimeType === 'application/sparql-query',\n    mode: 'sparql',\n    handle: (body, bodySchema) => {\n      // Use example from schema if available\n      body.sparql = bodySchema?.example !== undefined ? String(bodySchema.example) : '';\n    }\n  },\n  {\n    match: (mimeType) => ['text/plain', 'application/octet-stream', '*/*'].includes(mimeType),\n    mode: 'text',\n    handle: (body, bodySchema) => {\n      // Use example from schema if available\n      body.text = bodySchema?.example !== undefined ? String(bodySchema.example) : '';\n    }\n  }\n];\n\nconst getContentLevelExample = (bodyContent) => {\n  if (bodyContent.example !== undefined) return bodyContent.example;\n  const firstExample = Object.values(bodyContent.examples ?? {})[0];\n  return firstExample?.value;\n};\n\n/**\n * Extracts or generates an example value from an OpenAPI schema\n * Handles objects, arrays, primitives, and explicit examples\n * @param {Object} schema - The OpenAPI schema object\n * @returns {*} - The example value (object, array, or primitive)\n */\nconst getExampleFromSchema = (schema) => {\n  // Check for explicit example first\n  if (schema.example !== undefined) {\n    return schema.example;\n  }\n\n  // Handle different schema types\n  if (schema.type === 'object' || (schema.properties && !schema.type)) {\n    // Handle object type or schema with properties (even if type is not explicitly set)\n    return buildEmptyJsonBody(schema);\n  } else if (schema.type === 'array') {\n    if (schema.items) {\n      // If items are objects (either by type or by having properties), create array with one example object\n      if (schema.items.type === 'object' || schema.items.properties) {\n        return [buildEmptyJsonBody(schema.items)];\n      }\n      // For primitive array items, return array with default value\n      if (schema.items.type === 'integer' || schema.items.type === 'number') {\n        return [0];\n      } else if (schema.items.type === 'boolean') {\n        return [false];\n      } else if (schema.items.type === 'string') {\n        return [''];\n      }\n    }\n    return [];\n  } else {\n    // For primitive types, use default values\n    if (schema.type === 'integer' || schema.type === 'number') {\n      return 0;\n    } else if (schema.type === 'boolean') {\n      return false;\n    }\n    return '';\n  }\n};\n\n/**\n * Populates request body in Bruno example from schema\n * Reuses BODY_TYPE_HANDLERS for consistent body generation\n * @param {Object} params - Parameters object\n * @param {Object} params.body - The Bruno request body object to populate\n * @param {Object} params.bodySchema - The OpenAPI schema for the request body\n * @param {string} params.contentType - Content type (e.g., 'application/json', 'application/ld+json')\n */\nconst populateRequestBody = ({ body, bodySchema, contentType }) => {\n  if (!contentType || typeof contentType !== 'string') return;\n\n  // Normalize: lowercase (content types from OpenAPI spec object keys may vary in case)\n  const normalizedContentType = contentType.toLowerCase();\n\n  // Find matching handler and use it (same as main request body)\n  const handler = BODY_TYPE_HANDLERS.find((h) => h.match(normalizedContentType));\n  if (handler) {\n    body.mode = handler.mode;\n\n    // Clear arrays for form-based content types to avoid duplicates\n    // (since the body was deep-copied from the main request)\n    if (normalizedContentType === 'application/x-www-form-urlencoded') {\n      body.formUrlEncoded = [];\n    } else if (normalizedContentType === 'multipart/form-data') {\n      body.multipartForm = [];\n    }\n\n    handler.handle(body, bodySchema);\n  }\n};\n\n/**\n * Creates a Bruno example from OpenAPI example data\n * @param {Object} params - Parameters object\n * @param {Object} params.brunoRequestItem - The base Bruno request item\n * @param {*} params.exampleValue - The example value (object, array, or primitive)\n * @param {string} params.exampleName - Name of the example\n * @param {string} params.exampleDescription - Description of the example\n * @param {number} params.statusCode - HTTP status code (for response examples)\n * @param {string} params.contentType - Content type (e.g., 'application/json')\n * @param {Object} [params.requestBodySchema] - Optional request body schema to populate in the example\n * @param {string} [params.requestBodyContentType] - Optional request body content type\n * @returns {Object} Bruno example object\n */\n\nconst createBrunoExample = ({ brunoRequestItem, exampleValue, exampleName, exampleDescription, statusCode, contentType, requestBodySchema = null, requestBodyContentType = null }) => {\n  const sanitized = String(exampleName ?? '').replace(/\\r?\\n/g, ' ').trim();\n  const name = sanitized || `${statusCode} Response`;\n  const numericStatus = Number(statusCode);\n  const safeStatus = Number.isFinite(numericStatus) ? numericStatus : null;\n  // Deep copy the body to avoid shared references\n  const bodyCopy = {\n    mode: brunoRequestItem.request.body.mode,\n    json: brunoRequestItem.request.body.json,\n    text: brunoRequestItem.request.body.text,\n    xml: brunoRequestItem.request.body.xml,\n    sparql: brunoRequestItem.request.body.sparql || null,\n    formUrlEncoded: [...(brunoRequestItem.request.body.formUrlEncoded || [])],\n    multipartForm: [...(brunoRequestItem.request.body.multipartForm || [])]\n  };\n\n  const brunoExample = {\n    uid: uuid(),\n    itemUid: brunoRequestItem.uid,\n    name,\n    description: exampleDescription,\n    type: 'http-request',\n    request: {\n      url: brunoRequestItem.request.url,\n      method: brunoRequestItem.request.method,\n      headers: [...brunoRequestItem.request.headers],\n      params: [...brunoRequestItem.request.params],\n      body: bodyCopy\n    },\n    response: {\n      status: safeStatus,\n      statusText: safeStatus ? getStatusText(safeStatus) : null,\n      headers: contentType ? [\n        {\n          uid: uuid(),\n          name: 'Content-Type',\n          value: contentType,\n          description: '',\n          enabled: true\n        }\n      ] : [],\n      body: {\n        type: getBodyTypeFromContentType(contentType),\n        content: typeof exampleValue === 'object' ? JSON.stringify(exampleValue, null, 2) : exampleValue\n      }\n    }\n  };\n\n  // Populate request body from schema if provided (reuses BODY_TYPE_HANDLERS)\n  if (requestBodySchema !== null) {\n    populateRequestBody({ body: brunoExample.request.body, bodySchema: requestBodySchema, contentType: requestBodyContentType });\n  }\n\n  return brunoExample;\n};\n\n// Extract a representative value from a schema property (used for request body properties)\n// Priority: prop.example > parentExample[propName] > prop.default > prop.enum[0] > ''\nconst getSchemaPropertyExampleValue = (prop, propName, parentExample = {}) => {\n  if (prop.example !== undefined) return String(prop.example);\n  if (parentExample[propName] !== undefined) return String(parentExample[propName]);\n  if (prop.default !== undefined) return String(prop.default);\n  if (prop.enum && prop.enum.length > 0) return String(prop.enum[0]);\n  return '';\n};\n\n/**\n * Extracts parameter entries based on OpenAPI parameter schema\n * For enum parameters, creates multiple entries (one per enum value)\n * Handles enum, default, constant, nullable, and array types per Swagger spec\n * @param {Object} param - The OpenAPI parameter object\n * @returns {Array} - Array of objects with value and enabled properties\n */\nconst getParameterEntries = (param) => {\n  const schema = param.schema || {};\n  const entries = [];\n\n  // Handle enum parameters - create entry for each enum value\n  if (schema.enum && Array.isArray(schema.enum) && schema.enum.length > 0) {\n    const defaultValue = schema.default !== undefined ? String(schema.default) : null;\n\n    schema.enum.forEach((enumValue) => {\n      const valueStr = String(enumValue);\n      // Enable only if it matches the default value, or if it's the first value and required\n      const isDefault = defaultValue !== null && valueStr === defaultValue;\n      const enabled = isDefault || (defaultValue === null && schema.enum.indexOf(enumValue) === 0 && !!param.required);\n\n      entries.push({\n        value: valueStr,\n        enabled: enabled\n      });\n    });\n\n    return entries;\n  }\n\n  // Handle array type with items schema that has enum\n  if (schema.type === 'array' && schema.items && schema.items.enum && Array.isArray(schema.items.enum) && schema.items.enum.length > 0) {\n    const defaultValue = schema.items.default !== undefined ? String(schema.items.default) : null;\n    const arrayDefault = schema.default !== undefined && Array.isArray(schema.default) ? schema.default : null;\n\n    // If there's a default at array level, use it\n    if (arrayDefault) {\n      entries.push({\n        value: JSON.stringify(arrayDefault),\n        enabled: true\n      });\n      return entries;\n    }\n\n    // Otherwise, create entries for each enum value in items\n    schema.items.enum.forEach((enumValue) => {\n      const valueStr = String(enumValue);\n      const isDefault = defaultValue !== null && valueStr === defaultValue;\n      const enabled = isDefault || (defaultValue === null && schema.items.enum.indexOf(enumValue) === 0 && !!param.required);\n\n      entries.push({\n        value: valueStr,\n        enabled: enabled\n      });\n    });\n\n    return entries;\n  }\n\n  // For non-enum cases, return single entry with comprehensive value extraction\n  // Merges HEAD's detailed handling with MERGE_HEAD's broader example sources\n  let value = '';\n  let enabled = param.required || false;\n\n  // Priority 1: Top-level param examples (from upstream, mutually exclusive per spec)\n  if (param.example !== undefined) {\n    value = String(param.example);\n    enabled = true;\n  } else if (param.examples) {\n    const firstExample = Object.values(param.examples)[0];\n    if (firstExample?.value !== undefined) {\n      value = String(firstExample.value);\n      enabled = true;\n    }\n  }\n\n  // Priority 2: schema.default (from HEAD, handles array defaults with JSON.stringify)\n  if (value === '' && schema.default !== undefined) {\n    if (schema.type === 'array' && Array.isArray(schema.default)) {\n      value = JSON.stringify(schema.default);\n    } else {\n      value = String(schema.default);\n    }\n    enabled = true;\n  }\n\n  // Priority 3: schema.example (from upstream)\n  if (value === '' && schema.example !== undefined) {\n    value = String(schema.example);\n    enabled = true;\n  }\n\n  // Priority 4: Array type handling (merged from both sides)\n  if (value === '' && schema.type === 'array' && schema.items) {\n    if (schema.items.example !== undefined) {\n      value = String(schema.items.example);\n    } else if (schema.items.enum && schema.items.enum.length > 0) {\n      value = String(schema.items.enum[0]);\n    } else if (schema.items.default !== undefined) {\n      value = String(schema.items.default);\n    } else {\n      value = '[]';\n    }\n    enabled = param.required || false;\n  }\n\n  // Priority 5: schema.examples (OAS 3.1+, from upstream)\n  if (value === '' && Array.isArray(schema.examples) && schema.examples.length > 0) {\n    value = String(schema.examples[0]);\n    enabled = true;\n  }\n\n  // Priority 6: schema.minimum fallback for numeric types (from upstream)\n  if (value === '' && schema.minimum !== undefined) {\n    value = String(schema.minimum);\n    enabled = param.required || false;\n  }\n\n  // Priority 7: Edge cases (from HEAD)\n  if (value === '') {\n    if (schema.nullable === true && !param.required) {\n      enabled = false;\n    } else if (param.allowEmptyValue === true && !param.required) {\n      enabled = false;\n    }\n  }\n\n  return [{ value, enabled }];\n};\n\nconst transformOpenapiRequestItem = (request, usedNames = new Set(), options = {}) => {\n  let _operationObject = request.operationObject;\n\n  let operationName = _operationObject.summary || _operationObject.operationId || _operationObject.description;\n  if (!operationName) {\n    operationName = `${request.method} ${request.path}`;\n  }\n\n  // Sanitize operation name to prevent Bruno parsing issues\n  if (operationName) {\n    // Replace line breaks and normalize whitespace\n    operationName = operationName.replace(/[\\r\\n\\s]+/g, ' ').trim();\n  }\n  if (usedNames.has(operationName)) {\n    // Make name unique to prevent filename collisions\n    // Try adding method info first\n    let uniqueName = `${operationName} (${request.method.toUpperCase()})`;\n\n    // If still not unique, add counter\n    let counter = 1;\n    while (usedNames.has(uniqueName)) {\n      uniqueName = `${operationName} (${counter})`;\n      counter++;\n    }\n\n    operationName = uniqueName;\n  }\n  usedNames.add(operationName);\n\n  // replace OpenAPI links in path by Bruno variables\n  let path = request.path.replace(/{([a-zA-Z]+)}/g, `{{${_operationObject.operationId}_$1}}`);\n\n  const brunoRequestItem = {\n    uid: uuid(),\n    name: operationName,\n    type: 'http-request',\n    tags: sanitizeTags(request.operationObject.tags || [], options),\n    request: {\n      docs: _operationObject.description,\n      url: ensureUrl(request.global.server + path),\n      method: request.method.toUpperCase(),\n      auth: {\n        mode: 'inherit',\n        basic: null,\n        bearer: null,\n        digest: null,\n        apikey: null,\n        oauth2: null\n      },\n      headers: [],\n      params: [],\n      body: {\n        mode: 'none',\n        json: null,\n        text: null,\n        xml: null,\n        formUrlEncoded: [],\n        multipartForm: []\n      },\n      script: {\n        res: null\n      }\n    }\n  };\n\n  // If the operation has its own servers, override baseUrl via request vars\n  // Only the first server is used; Bruno supports a single baseUrl per request\n  if (request.servers && request.servers.length > 0) {\n    const serverVarPairs = extractServerVars(request.servers[0]);\n    brunoRequestItem.request.vars = {\n      req: serverVarPairs.map((sv) => ({\n        uid: uuid(),\n        name: sv.name,\n        value: sv.value,\n        enabled: true,\n        local: false\n      })),\n      res: []\n    };\n  }\n\n  each(_operationObject.parameters || [], (param) => {\n    // Check if parameter schema is an object type with properties\n    // If so, expand the properties into individual parameters\n    const isObjectSchema = param.schema && param.schema.properties;\n\n    if (isObjectSchema) {\n      // Expand object schema properties into individual parameters\n      const schemaExample = param.schema.example || {};\n\n      each(param.schema.properties, (prop, propName) => {\n        const isRequired = Array.isArray(param.schema.required) && param.schema.required.includes(propName);\n\n        // Create a temporary parameter object for getParameterEntries\n        // Enrich property with parent example context if property lacks its own example\n        // Use child-level example only; drop parent-level example/examples to avoid\n        // object-level values leaking into scalar child parameters\n        const propSchema = (prop.example === undefined && schemaExample[propName] !== undefined)\n          ? { ...prop, example: schemaExample[propName] }\n          : prop;\n        const tempParam = { ...param, example: undefined, examples: undefined, name: propName, schema: propSchema, required: isRequired };\n        const entries = getParameterEntries(tempParam);\n\n        entries.forEach((entry) => {\n          if (param.in === 'query' || param.in === 'querystring') {\n            brunoRequestItem.request.params.push({\n              uid: uuid(),\n              name: propName,\n              value: entry.value,\n              description: prop.description || '',\n              enabled: entry.enabled,\n              type: 'query'\n            });\n          } else if (param.in === 'path') {\n            brunoRequestItem.request.params.push({\n              uid: uuid(),\n              name: propName,\n              value: entry.value,\n              description: prop.description || '',\n              enabled: entry.enabled,\n              type: 'path'\n            });\n          } else if (param.in === 'header') {\n            brunoRequestItem.request.headers.push({\n              uid: uuid(),\n              name: propName,\n              value: entry.value,\n              description: prop.description || '',\n              enabled: entry.enabled\n            });\n          }\n        });\n      });\n    } else {\n      const entries = getParameterEntries(param);\n\n      entries.forEach((entry) => {\n        if (param.in === 'query' || param.in === 'querystring') {\n          brunoRequestItem.request.params.push({\n            uid: uuid(),\n            name: param.name,\n            value: entry.value,\n            description: param.description || '',\n            enabled: entry.enabled,\n            type: 'query'\n          });\n        } else if (param.in === 'path') {\n          brunoRequestItem.request.params.push({\n            uid: uuid(),\n            name: param.name,\n            value: entry.value,\n            description: param.description || '',\n            enabled: entry.enabled,\n            type: 'path'\n          });\n        } else if (param.in === 'header') {\n          brunoRequestItem.request.headers.push({\n            uid: uuid(),\n            name: param.name,\n            value: entry.value,\n            description: param.description || '',\n            enabled: entry.enabled\n          });\n        }\n      });\n    }\n  });\n\n  // Handle explicit no-auth case where security: [] on the operation\n  if (Array.isArray(_operationObject.security) && _operationObject.security.length === 0) {\n    brunoRequestItem.request.auth.mode = 'inherit';\n    return brunoRequestItem;\n  }\n\n  let auth = null;\n  if (_operationObject.security && _operationObject.security.length > 0) {\n    const schemeName = Object.keys(_operationObject.security[0])[0];\n    auth = request.global.security.getScheme(schemeName);\n  }\n\n  if (auth) {\n    if (auth.type === 'http' && auth.scheme === 'basic') {\n      brunoRequestItem.request.auth.mode = 'basic';\n      brunoRequestItem.request.auth.basic = {\n        username: '{{username}}',\n        password: '{{password}}'\n      };\n    } else if (auth.type === 'http' && auth.scheme === 'bearer') {\n      brunoRequestItem.request.auth.mode = 'bearer';\n      brunoRequestItem.request.auth.bearer = {\n        token: '{{token}}'\n      };\n    } else if (auth.type === 'http' && auth.scheme === 'digest') {\n      brunoRequestItem.request.auth.mode = 'digest';\n      brunoRequestItem.request.auth.digest = {\n        username: '{{username}}',\n        password: '{{password}}'\n      };\n    } else if (auth.type === 'apiKey') {\n      const apikeyConfig = {\n        key: auth.name,\n        value: '{{apiKey}}',\n        placement: auth.in === 'query' ? 'queryparams' : 'header'\n      };\n      brunoRequestItem.request.auth.mode = 'apikey';\n      brunoRequestItem.request.auth.apikey = apikeyConfig;\n\n      if (auth.in === 'header' || auth.in === 'cookie') {\n        brunoRequestItem.request.headers.push({\n          uid: uuid(),\n          name: auth.name,\n          value: '{{apiKey}}',\n          description: auth.description || '',\n          enabled: true\n        });\n      } else if (auth.in === 'query') {\n        brunoRequestItem.request.params.push({\n          uid: uuid(),\n          name: auth.name,\n          value: '{{apiKey}}',\n          description: auth.description || '',\n          enabled: true,\n          type: 'query'\n        });\n      }\n    } else if (auth.type === 'oauth2') {\n      // Determine flow (grant type)\n      let flows = auth.flows || {};\n      let grantType = 'client_credentials';\n      if (flows.authorizationCode) {\n        grantType = 'authorization_code';\n      } else if (flows.implicit) {\n        grantType = 'implicit';\n      } else if (flows.password) {\n        grantType = 'password';\n      } else if (flows.clientCredentials) {\n        grantType = 'client_credentials';\n      }\n\n      let flowConfig = {};\n      switch (grantType) {\n        case 'authorization_code':\n          flowConfig = flows.authorizationCode || {};\n          break;\n        case 'implicit':\n          flowConfig = flows.implicit || {};\n          break;\n        case 'password':\n          flowConfig = flows.password || {};\n          break;\n        case 'client_credentials':\n        default:\n          flowConfig = flows.clientCredentials || {};\n          break;\n      }\n\n      brunoRequestItem.request.auth.mode = 'oauth2';\n      brunoRequestItem.request.auth.oauth2 = {\n        grantType: grantType,\n        authorizationUrl: flowConfig.authorizationUrl || '{{oauth_authorize_url}}',\n        accessTokenUrl: flowConfig.tokenUrl || '{{oauth_token_url}}',\n        refreshTokenUrl: flowConfig.refreshUrl || '{{oauth_refresh_url}}',\n        callbackUrl: '{{oauth_callback_url}}',\n        clientId: '{{oauth_client_id}}',\n        clientSecret: '{{oauth_client_secret}}',\n        scope: Array.isArray(flowConfig.scopes) ? flowConfig.scopes.join(' ') : Object.keys(flowConfig.scopes || {}).join(' '),\n        state: '{{oauth_state}}',\n        credentialsPlacement: 'header',\n        tokenPlacement: 'header',\n        tokenHeaderPrefix: 'Bearer',\n        autoFetchToken: false,\n        autoRefreshToken: true\n      };\n    }\n  }\n\n  // TODO: handle allOf/anyOf/oneOf\n  if (_operationObject.requestBody) {\n    const content = get(_operationObject, 'requestBody.content', {});\n    const mimeType = Object.keys(content)[0];\n    const bodyContent = content[mimeType] || {};\n    let bodySchema = bodyContent.schema;\n\n    if (bodySchema?.example === undefined) {\n      const contentExample = getContentLevelExample(bodyContent);\n      if (contentExample !== undefined) {\n        bodySchema = { ...bodySchema, example: contentExample };\n      }\n    }\n\n    // Normalize: lowercase (object keys may vary in case)\n    const normalizedMimeType = typeof mimeType === 'string' ? mimeType.toLowerCase() : '';\n\n    // Find matching handler for this content type\n    const handler = BODY_TYPE_HANDLERS.find((h) => h.match(normalizedMimeType));\n    if (handler) {\n      brunoRequestItem.request.body.mode = handler.mode;\n      handler.handle(brunoRequestItem.request.body, bodySchema);\n    }\n  }\n\n  // build the extraction scripts from responses that have links\n  // https://swagger.io/docs/specification/links/\n  let script = [];\n  each(_operationObject.responses || [], (response, responseStatus) => {\n    if (Object.hasOwn(response, 'links')) {\n      // only extract if the status code matches the response\n      script.push(`if (res.status === ${responseStatus}) {`);\n      each(response.links, (link) => {\n        each(link.parameters || [], (expression, parameter) => {\n          let value = openAPIRuntimeExpressionToScript(expression);\n          script.push(`  bru.setVar('${link.operationId}_${parameter}', ${value});`);\n        });\n      });\n      script.push(`}`);\n    }\n  });\n  if (script.length > 0) {\n    brunoRequestItem.request.script.res = script.join('\\n');\n  }\n\n  // Handle OpenAPI examples from responses and request body\n  if (_operationObject.responses) {\n    const examples = [];\n\n    // Extract request body examples if they exist\n    // Unified structure: all request body data is stored as examples with contentType\n    const requestBodyExamples = [];\n\n    /**\n     * Helper function to create examples with appropriate request body handling\n     * @param {Object} params - Parameters object\n     * @param {*} params.responseExampleValue - The response example value\n     * @param {string} params.exampleName - Name of the example\n     * @param {string} params.exampleDescription - Description of the example\n     * @param {number} params.statusCode - HTTP status code\n     * @param {string} params.responseContentType - Response content type\n     * @param {string} [params.responseExampleKey] - Optional response example key for matching\n     */\n    const createExamplesWithRequestBody = ({ responseExampleValue, exampleName, exampleDescription, statusCode, responseContentType, responseExampleKey = null }) => {\n      const requestBodyExamplesWithKeys = requestBodyExamples.filter((rb) => rb.key !== null);\n      const requestBodyExamplesWithoutKeys = requestBodyExamples.filter((rb) => rb.key === null);\n\n      // Check if there's a matching request body example by key\n      const matchingRequestBodyExample = responseExampleKey\n        ? requestBodyExamplesWithKeys.find((rb) => rb.key === responseExampleKey)\n        : null;\n\n      if (matchingRequestBodyExample) {\n        // Use the matching request body example\n        examples.push(createBrunoExample({\n          brunoRequestItem,\n          exampleValue: responseExampleValue,\n          exampleName,\n          exampleDescription,\n          statusCode,\n          contentType: responseContentType,\n          requestBodySchema: matchingRequestBodyExample.schema,\n          requestBodyContentType: matchingRequestBodyExample.contentType\n        }));\n      } else if (requestBodyExamplesWithKeys.length > 0) {\n        // No match found, create all combinations with request body examples that have keys\n        requestBodyExamplesWithKeys.forEach((rbExample) => {\n          const combinedExampleName = `${exampleName} (${rbExample.summary || rbExample.key})`;\n          const combinedExampleDescription = exampleDescription || rbExample.description || '';\n          examples.push(createBrunoExample({\n            brunoRequestItem,\n            exampleValue: responseExampleValue,\n            exampleName: combinedExampleName,\n            exampleDescription: combinedExampleDescription,\n            statusCode,\n            contentType: responseContentType,\n            requestBodySchema: rbExample.schema,\n            requestBodyContentType: rbExample.contentType\n          }));\n        });\n      } else if (requestBodyExamplesWithoutKeys.length > 0) {\n        // Single example or schema - use the first one for all response examples\n        const rbExample = requestBodyExamplesWithoutKeys[0];\n        examples.push(createBrunoExample({\n          brunoRequestItem,\n          exampleValue: responseExampleValue,\n          exampleName,\n          exampleDescription,\n          statusCode,\n          contentType: responseContentType,\n          requestBodySchema: rbExample.schema,\n          requestBodyContentType: rbExample.contentType\n        }));\n      } else {\n        // No request body, create example without request body\n        examples.push(createBrunoExample({\n          brunoRequestItem,\n          exampleValue: responseExampleValue,\n          exampleName,\n          exampleDescription,\n          statusCode,\n          contentType: responseContentType\n        }));\n      }\n    };\n\n    if (_operationObject.requestBody && _operationObject.requestBody.content) {\n      Object.entries(_operationObject.requestBody.content).forEach(([contentType, content]) => {\n        if (content.examples) {\n          // Multiple request body examples\n          Object.entries(content.examples).forEach(([exampleKey, example]) => {\n            const exampleValue = example.value !== undefined ? example.value : example;\n            requestBodyExamples.push({\n              key: exampleKey,\n              schema: { example: exampleValue }, // Wrap in schema format for BODY_TYPE_HANDLERS\n              summary: example.summary,\n              description: example.description,\n              contentType: contentType\n            });\n          });\n        } else if (content.example !== undefined) {\n          // Single request body example - wrap in schema-like object\n          requestBodyExamples.push({\n            key: null,\n            schema: { example: content.example }, // Wrap in schema format for BODY_TYPE_HANDLERS\n            summary: null,\n            description: null,\n            contentType: contentType\n          });\n        } else if (content.schema) {\n          // Schema-based request body - pass schema directly\n          requestBodyExamples.push({\n            key: null,\n            schema: content.schema,\n            summary: null,\n            description: null,\n            contentType: contentType\n          });\n        }\n      });\n    }\n\n    // Handle response examples\n    if (_operationObject.responses) {\n      Object.entries(_operationObject.responses).forEach(([statusCode, response]) => {\n        if (response.content) {\n          Object.entries(response.content).forEach(([contentType, content]) => {\n            // Handle examples (plural) - multiple named examples\n            if (content.examples) {\n              Object.entries(content.examples).forEach(([exampleKey, example]) => {\n                const exampleName = example.summary || exampleKey || `${statusCode} Response`;\n                const exampleDescription = example.description || '';\n                const exampleValue = example.value !== undefined ? example.value : example;\n\n                createExamplesWithRequestBody({\n                  responseExampleValue: exampleValue,\n                  exampleName,\n                  exampleDescription,\n                  statusCode,\n                  responseContentType: contentType,\n                  responseExampleKey: exampleKey\n                });\n              });\n            } else if (content.example !== undefined) {\n              // Handle example (singular) at content level\n              const exampleName = `${statusCode} Response`;\n              const exampleDescription = response.description || '';\n\n              createExamplesWithRequestBody({\n                responseExampleValue: content.example,\n                exampleName,\n                exampleDescription,\n                statusCode,\n                responseContentType: contentType\n              });\n            } else if (content.schema) {\n              // Handle schema - extract or generate example from schema\n              const exampleValue = getExampleFromSchema(content.schema);\n              const exampleName = `${statusCode} Response`;\n              const exampleDescription = response.description || '';\n\n              createExamplesWithRequestBody({\n                responseExampleValue: exampleValue,\n                exampleName,\n                exampleDescription,\n                statusCode,\n                responseContentType: contentType\n              });\n            }\n          });\n        } else {\n          // Handle responses without content (e.g., 204 No Content)\n          const exampleName = `${statusCode} Response`;\n          const exampleDescription = response.description || '';\n\n          createExamplesWithRequestBody({\n            responseExampleValue: '',\n            exampleName,\n            exampleDescription,\n            statusCode,\n            responseContentType: null\n          });\n        }\n      });\n    }\n\n    // Only add examples array if there are examples\n    if (examples.length > 0) {\n      brunoRequestItem.examples = examples;\n    }\n  }\n\n  return brunoRequestItem;\n};\n\n// Helper function to validate $ref\nconst isValidRef = (ref) => {\n  if (typeof ref !== 'string') {\n    return false;\n  }\n\n  return ref.startsWith('#/components/');\n};\n\nconst resolveRefs = (spec, components = spec?.components, cache = new Map()) => {\n  if (!spec || typeof spec !== 'object') {\n    return spec;\n  }\n\n  if (cache.has(spec)) {\n    return cache.get(spec);\n  }\n\n  if (Array.isArray(spec)) {\n    return spec.map((item) => resolveRefs(item, components, cache));\n  }\n\n  // Only treat as a JSON reference if it passes all validation checks\n  const isRef = isValidRef(spec.$ref);\n\n  if (isRef) {\n    const refPath = spec.$ref;\n\n    if (cache.has(refPath)) {\n      return cache.get(refPath);\n    }\n\n    if (refPath.startsWith('#/components/')) {\n      const refKeys = refPath.replace('#/components/', '').split('/');\n      let ref = components;\n\n      for (const key of refKeys) {\n        if (ref && ref[key]) {\n          ref = ref[key];\n        } else {\n          return spec;\n        }\n      }\n\n      cache.set(refPath, {});\n      const resolved = resolveRefs(ref, components, cache);\n      cache.set(refPath, resolved);\n      return resolved;\n    }\n    return spec;\n  }\n\n  const resolved = {};\n  cache.set(spec, resolved);\n\n  for (const [key, value] of Object.entries(spec)) {\n    resolved[key] = resolveRefs(value, components, cache);\n  }\n\n  return resolved;\n};\n\nconst groupRequestsByTags = (requests, options = {}) => {\n  let _groups = {};\n  let ungrouped = [];\n  each(requests, (request) => {\n    let tags = request.operationObject.tags || [];\n    if (tags.length > 0) {\n      let tag = sanitizeTag(tags[0].trim()); // take first tag, trim whitespace, and sanitize\n\n      if (tag) {\n        if (!_groups[tag]) {\n          _groups[tag] = [];\n        }\n        _groups[tag].push(request);\n      } else {\n        ungrouped.push(request);\n      }\n    } else {\n      ungrouped.push(request);\n    }\n  });\n\n  let groups = Object.keys(_groups).map((groupName) => {\n    return {\n      name: groupName,\n      requests: _groups[groupName]\n    };\n  });\n\n  return [groups, ungrouped];\n};\n\nconst groupRequestsByPath = (requests, options = {}) => {\n  const pathGroups = {};\n\n  // Group requests by their path segments\n  requests.forEach((request) => {\n    // Use original path for grouping to preserve {id} format\n    const pathToUse = request.originalPath || request.path;\n    const pathSegments = pathToUse.split('/').filter((segment) => segment !== '');\n\n    if (pathSegments.length === 0) {\n      // Handle root path or paths with only parameters\n      const groupName = 'Root';\n      if (!pathGroups[groupName]) {\n        pathGroups[groupName] = {\n          name: groupName,\n          requests: [],\n          subGroups: {}\n        };\n      }\n      pathGroups[groupName].requests.push(request);\n      return;\n    }\n\n    // Use the first segment as the main group\n    let groupName = pathSegments[0];\n\n    if (!pathGroups[groupName]) {\n      pathGroups[groupName] = {\n        name: groupName,\n        requests: [],\n        subGroups: {}\n      };\n    }\n\n    // If there's only one meaningful segment, add to main group\n    if (pathSegments.length <= 1) {\n      pathGroups[groupName].requests.push(request);\n    } else {\n      // For deeper paths, create sub-groups\n      let currentGroup = pathGroups[groupName];\n      for (let i = 1; i < pathSegments.length; i++) {\n        let subGroupName = pathSegments[i];\n\n        if (!currentGroup.subGroups[subGroupName]) {\n          currentGroup.subGroups[subGroupName] = {\n            name: subGroupName,\n            requests: [],\n            subGroups: {}\n          };\n        }\n        currentGroup = currentGroup.subGroups[subGroupName];\n      }\n      currentGroup.requests.push(request);\n    }\n  });\n\n  // Convert the nested structure to Bruno folder format\n  const buildFolderStructure = (group) => {\n    // Create a new usedNames set for each folder/subfolder scope\n    const localUsedNames = new Set();\n    const items = group.requests.map((req) => transformOpenapiRequestItem(req, localUsedNames, options));\n\n    // Add sub-folders\n    const subFolders = [];\n    Object.values(group.subGroups).forEach((subGroup) => {\n      const subFolderItems = buildFolderStructure(subGroup);\n      if (subFolderItems.length > 0) {\n        subFolders.push({\n          uid: uuid(),\n          name: subGroup.name,\n          type: 'folder',\n          items: subFolderItems\n        });\n      }\n    });\n\n    return [...items, ...subFolders];\n  };\n\n  const folders = Object.values(pathGroups).map((group) => ({\n    uid: uuid(),\n    name: group.name,\n    type: 'folder',\n    items: buildFolderStructure(group)\n  }));\n\n  return folders;\n};\n\nconst getDefaultUrl = (serverObject) => {\n  let url = serverObject.url;\n  if (serverObject.variables) {\n    each(serverObject.variables, (variable, variableName) => {\n      let sub = variable.default !== undefined ? variable.default : (variable.enum ? variable.enum[0] : `{{${variableName}}}`);\n      url = url.replaceAll(`{${variableName}}`, sub);\n    });\n  }\n  return url.endsWith('/') ? url.slice(0, -1) : url;\n};\n\n// Extract { name, value } pairs from an OpenAPI server object.\n// Converts {varName} to {{varName}} for template URLs and includes variable defaults.\nconst extractServerVars = (server) => {\n  const vars = [];\n  if (server.variables && Object.keys(server.variables).length > 0) {\n    let baseUrlTemplate = server.url;\n    each(server.variables, (variable, variableName) => {\n      baseUrlTemplate = baseUrlTemplate.replaceAll(`{${variableName}}`, `{{${variableName}}}`);\n    });\n    baseUrlTemplate = baseUrlTemplate.endsWith('/') ? baseUrlTemplate.slice(0, -1) : baseUrlTemplate;\n    vars.push({ name: 'baseUrl', value: baseUrlTemplate });\n    each(server.variables, (variable, variableName) => {\n      let value = variable.default !== undefined ? variable.default : (variable.enum ? variable.enum[0] : '');\n      vars.push({ name: variableName, value: String(value) });\n    });\n  } else {\n    vars.push({ name: 'baseUrl', value: getDefaultUrl(server) });\n  }\n  return vars;\n};\n\nconst getSecurity = (apiSpec) => {\n  let defaultSchemes = apiSpec.security || [];\n  let securitySchemes = get(apiSpec, 'components.securitySchemes', {});\n\n  const hasSchemes = Object.keys(securitySchemes).length > 0;\n\n  return {\n    supported: hasSchemes\n      ? defaultSchemes\n          .map((scheme) => securitySchemes[Object.keys(scheme)[0]])\n          .filter(Boolean)\n      : [],\n    schemes: securitySchemes,\n    getScheme: (schemeName) => securitySchemes[schemeName]\n  };\n};\n\nconst openAPIRuntimeExpressionToScript = (expression) => {\n  // see https://swagger.io/docs/specification/links/#runtime-expressions\n  if (expression === '$response.body') {\n    return 'res.body';\n  } else if (expression.startsWith('$response.body#')) {\n    let pointer = expression.substring(15);\n    // could use https://www.npmjs.com/package/json-pointer for better support\n    return `res.body${pointer.replace('/', '.')}`;\n  }\n  return expression;\n};\n\nexport const parseOpenApiCollection = (data, options = {}) => {\n  const usedNames = new Set();\n  const brunoCollection = {\n    name: '',\n    uid: uuid(),\n    version: '1',\n    items: [],\n    environments: []\n  };\n  try {\n    const collectionData = resolveRefs(data);\n    if (!collectionData) {\n      throw new Error('Invalid OpenAPI collection. Failed to resolve refs.');\n      return;\n    }\n\n    // Currently parsing of openapi spec is \"do your best\", that is\n    // allows \"invalid\" openapi spec\n\n    // Assumes v3 if not defined. v2 is not supported yet\n    if (collectionData.openapi && !collectionData.openapi.startsWith('3')) {\n      throw new Error('Only OpenAPI v3 is supported currently.');\n      return;\n    }\n\n    brunoCollection.name = collectionData.info?.title?.trim() || 'Untitled Collection';\n\n    let servers = collectionData.servers || [];\n\n    // Create environments based on the servers\n    servers.forEach((server, index) => {\n      let environmentName = server.name || server.description || `Environment ${index + 1}`;\n      const serverVars = extractServerVars(server);\n      const variables = serverVars.map((sv) => ({\n        uid: uuid(),\n        name: sv.name,\n        value: sv.value,\n        type: 'text',\n        enabled: true,\n        secret: false\n      }));\n\n      brunoCollection.environments.push({\n        uid: uuid(),\n        name: environmentName,\n        variables\n      });\n    });\n\n    let securityConfig = getSecurity(collectionData);\n\n    // Merge path-item parameters with operation parameters.\n    // Operation parameters override path-item parameters with the same name+in combination.\n    const mergeParams = (pathParams, operationParams) => {\n      const overrides = new Set(operationParams.map((p) => `${p.name}:${p.in}`));\n      const inheritedParams = pathParams.filter((p) => !overrides.has(`${p.name}:${p.in}`));\n      return [...inheritedParams, ...operationParams];\n    };\n\n    let allRequests = Object.entries(collectionData.paths)\n      .map(([path, pathItemObject]) => {\n        // Extract path-item level parameters (per OpenAPI spec, these apply to all operations under this path)\n        const pathItemParams = pathItemObject.parameters || [];\n\n        return Object.entries(pathItemObject)\n          .filter(([method, op]) => {\n            return ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'].includes(\n              method.toLowerCase()\n            );\n          })\n          .map(([method, operationObject]) => {\n            const mergedParams = mergeParams(pathItemParams, operationObject.parameters || []);\n\n            return {\n              method: method,\n              path: path.replace(/{([^}]+)}/g, ':$1'), // Replace placeholders enclosed in curly braces with colons\n              originalPath: path, // Keep original path for grouping\n              operationObject: { ...operationObject, parameters: mergedParams },\n              global: {\n                server: '{{baseUrl}}',\n                security: securityConfig\n              },\n              servers: operationObject.servers || pathItemObject.servers || null\n            };\n          });\n      })\n      .reduce((acc, val) => acc.concat(val), []); // flatten\n\n    // Support both tag-based and path-based grouping\n    const groupingType = options.groupBy || 'tags';\n\n    if (groupingType === 'path') {\n      brunoCollection.items = groupRequestsByPath(allRequests, options);\n    } else {\n      // Default tag-based grouping\n      let [groups, ungroupedRequests] = groupRequestsByTags(allRequests, options);\n      let brunoFolders = groups.map((group) => {\n        return {\n          uid: uuid(),\n          name: group.name,\n          type: 'folder',\n          root: {\n            request: {\n              auth: {\n                mode: 'inherit',\n                basic: null,\n                bearer: null,\n                digest: null,\n                apikey: null,\n                oauth2: null\n              }\n            },\n            meta: {\n              name: group.name\n            }\n          },\n          items: group.requests.map((req) => transformOpenapiRequestItem(req, usedNames, options))\n        };\n      });\n\n      let ungroupedItems = ungroupedRequests.map((req) => transformOpenapiRequestItem(req, usedNames, options));\n      let brunoCollectionItems = brunoFolders.concat(ungroupedItems);\n      brunoCollection.items = brunoCollectionItems;\n    }\n\n    // Determine collection-level authentication based on global security requirements\n    const buildCollectionAuth = (scheme) => {\n      const authTemplate = {\n        mode: 'none',\n        basic: null,\n        bearer: null,\n        digest: null,\n        apikey: null,\n        oauth2: null\n      };\n\n      if (!scheme) return authTemplate;\n\n      if (scheme.type === 'http' && scheme.scheme === 'basic') {\n        return {\n          ...authTemplate,\n          mode: 'basic',\n          basic: {\n            username: '{{username}}',\n            password: '{{password}}'\n          }\n        };\n      } else if (scheme.type === 'http' && scheme.scheme === 'bearer') {\n        return {\n          ...authTemplate,\n          mode: 'bearer',\n          bearer: {\n            token: '{{token}}'\n          }\n        };\n      } else if (scheme.type === 'http' && scheme.scheme === 'digest') {\n        return {\n          ...authTemplate,\n          mode: 'digest',\n          digest: {\n            username: '{{username}}',\n            password: '{{password}}'\n          }\n        };\n      } else if (scheme.type === 'apiKey') {\n        return {\n          ...authTemplate,\n          mode: 'apikey',\n          apikey: {\n            key: scheme.name,\n            value: '{{apiKey}}',\n            placement: scheme.in === 'query' ? 'queryparams' : 'header'\n          }\n        };\n      } else if (scheme.type === 'oauth2') {\n        let flows = scheme.flows || {};\n        let grantType = 'client_credentials';\n        if (flows.authorizationCode) {\n          grantType = 'authorization_code';\n        } else if (flows.implicit) {\n          grantType = 'implicit';\n        } else if (flows.password) {\n          grantType = 'password';\n        }\n        const flowConfig = grantType === 'authorization_code' ? flows.authorizationCode || {} : grantType === 'implicit' ? flows.implicit || {} : grantType === 'password' ? flows.password || {} : flows.clientCredentials || {};\n\n        return {\n          ...authTemplate,\n          mode: 'oauth2',\n          oauth2: {\n            grantType,\n            authorizationUrl: flowConfig.authorizationUrl || '{{oauth_authorize_url}}',\n            accessTokenUrl: flowConfig.tokenUrl || '{{oauth_token_url}}',\n            refreshTokenUrl: flowConfig.refreshUrl || '{{oauth_refresh_url}}',\n            callbackUrl: '{{oauth_callback_url}}',\n            clientId: '{{oauth_client_id}}',\n            clientSecret: '{{oauth_client_secret}}',\n            scope: Array.isArray(flowConfig.scopes) ? flowConfig.scopes.join(' ') : Object.keys(flowConfig.scopes || {}).join(' '),\n            state: '{{oauth_state}}',\n            credentialsPlacement: 'header',\n            tokenPlacement: 'header',\n            tokenHeaderPrefix: 'Bearer',\n            autoFetchToken: false,\n            autoRefreshToken: true\n          }\n        };\n      }\n      return authTemplate;\n    };\n\n    let collectionAuth = buildCollectionAuth(securityConfig.supported[0]);\n\n    brunoCollection.root = {\n      request: {\n        auth: collectionAuth\n      },\n      meta: {\n        name: brunoCollection.name\n      }\n    };\n\n    return brunoCollection;\n  } catch (err) {\n    if (!(err instanceof Error)) {\n      throw new Error('Unknown error');\n    }\n    throw err;\n  }\n};\n\nexport const openApiToBruno = (openApiSpecification, options = {}) => {\n  try {\n    if (typeof openApiSpecification !== 'object') {\n      openApiSpecification = jsyaml.load(openApiSpecification);\n    }\n\n    const collection = parseOpenApiCollection(openApiSpecification, options);\n\n    const transformedCollection = transformItemsInCollection(collection);\n\n    const hydratedCollection = hydrateSeqInCollection(transformedCollection);\n    const validatedCollection = validateSchema(hydratedCollection);\n\n    return validatedCollection;\n  } catch (err) {\n    console.error('Error converting OpenAPI to Bruno:', err);\n    if (!(err instanceof Error)) {\n      throw new Error('Unknown error');\n    }\n    throw err;\n  }\n};\n\nexport default openApiToBruno;\n"
  },
  {
    "path": "packages/bruno-converters/src/opencollection/bruno-to-opencollection.ts",
    "content": "import { toOpenCollectionAuth, toOpenCollectionHeaders, toOpenCollectionScripts, toOpenCollectionVariables } from \"./common\";\nimport { toOpenCollectionEnvironments } from \"./environment\";\nimport { toOpenCollectionFolder } from \"./folder\";\nimport { toOpenCollectionItems } from \"./items\";\nimport { BrunoCollection, BrunoCollectionRoot, BrunoConfig, ClientCertificate, CollectionConfig, OpenCollection, PemCertificate, Pkcs12Certificate, Protobuf } from \"./types\";\n\nconst toOpenCollectionConfig = (brunoConfig: BrunoConfig | undefined): CollectionConfig | undefined => {\n  if (!brunoConfig) {\n    return undefined;\n  }\n\n  const config: CollectionConfig = {};\n\n  if (brunoConfig.protobuf?.protoFiles?.length || brunoConfig.protobuf?.importPaths?.length) {\n    config.protobuf = {} as Protobuf;\n\n    if (brunoConfig.protobuf.protoFiles?.length) {\n      config.protobuf.protoFiles = brunoConfig.protobuf.protoFiles.map((f) => ({\n        type: 'file' as const,\n        path: f.path\n      }));\n    }\n\n    if (brunoConfig.protobuf.importPaths?.length) {\n      config.protobuf.importPaths = brunoConfig.protobuf.importPaths.map((p) => {\n        const importPath: { path: string; disabled?: boolean } = { path: p.path };\n        if (p.enabled === false) {\n          importPath.disabled = true;\n        }\n        return importPath;\n      });\n    }\n  }\n\n  if (brunoConfig.proxy) {\n    config.proxy = {\n      disabled: brunoConfig.proxy.disabled,\n      inherit: brunoConfig.proxy.inherit,\n      config: brunoConfig.proxy.config\n    };\n  }\n\n  if (brunoConfig.clientCertificates?.certs?.length) {\n    config.clientCertificates = brunoConfig.clientCertificates.certs\n      .map((cert): ClientCertificate | null => {\n        if (cert.type === 'pem') {\n          const pemCert: PemCertificate = {\n            domain: cert.domain || '',\n            type: 'pem',\n            certificateFilePath: cert.certFilePath || '',\n            privateKeyFilePath: cert.keyFilePath || ''\n          };\n          if (cert.passphrase) {\n            pemCert.passphrase = cert.passphrase;\n          }\n          return pemCert;\n        } else if (cert.type === 'pkcs12') {\n          const pkcs12Cert: Pkcs12Certificate = {\n            domain: cert.domain || '',\n            type: 'pkcs12',\n            pkcs12FilePath: cert.pfxFilePath || ''\n          };\n          if (cert.passphrase) {\n            pkcs12Cert.passphrase = cert.passphrase;\n          }\n          return pkcs12Cert;\n        }\n        return null;\n      })\n      .filter((cert): cert is ClientCertificate => cert !== null);\n  }\n\n  return Object.keys(config).length > 0 ? config : undefined;\n};\n\nconst hasRequestDefaults = (root: BrunoCollectionRoot | undefined): boolean => {\n  const request = root?.request;\n  return Boolean(\n    request?.headers?.length ||\n    request?.vars?.req?.length ||\n    request?.script?.req ||\n    request?.script?.res ||\n    request?.tests ||\n    (request?.auth && request.auth.mode !== 'none')\n  );\n};\n\nexport const brunoToOpenCollection = (collection: BrunoCollection): OpenCollection => {\n  const openCollection: OpenCollection = {\n    opencollection: '1.0.0',\n    info: {\n      name: collection.name || 'Untitled Collection'\n    }\n  };\n\n  const config = toOpenCollectionConfig(collection.brunoConfig as BrunoConfig);\n  if (config) {\n    openCollection.config = config;\n  }\n\n  const environments = toOpenCollectionEnvironments(collection.environments ?? undefined);\n  if (environments?.length) {\n    if (!openCollection.config) {\n      openCollection.config = {};\n    }\n    openCollection.config.environments = environments;\n  }\n\n  const items = toOpenCollectionItems(collection.items, toOpenCollectionFolder);\n  if (items.length) {\n    openCollection.items = items as OpenCollection['items'];\n  }\n\n  if (hasRequestDefaults(collection.root as BrunoCollectionRoot)) {\n    const request = (collection.root as BrunoCollectionRoot)?.request;\n    openCollection.request = {};\n\n    const headers = toOpenCollectionHeaders(request?.headers);\n    if (headers) {\n      openCollection.request.headers = headers;\n    }\n\n    const auth = toOpenCollectionAuth(request?.auth);\n    if (auth) {\n      openCollection.request.auth = auth;\n    }\n\n    const variables = toOpenCollectionVariables(request?.vars);\n    if (variables) {\n      openCollection.request.variables = variables;\n    }\n\n    const scripts = toOpenCollectionScripts(request as any);\n    if (scripts) {\n      openCollection.request.scripts = scripts;\n    }\n  }\n\n  if ((collection.root as BrunoCollectionRoot)?.docs) {\n    openCollection.docs = {\n      content: (collection.root as BrunoCollectionRoot).docs!,\n      type: 'text/markdown'\n    };\n  }\n\n  openCollection.bundled = true;\n\n  const brunoExtension: {\n    ignore?: string[];\n    presets?: {\n      requestType?: string;\n      requestUrl?: string;\n    };\n  } = {};\n\n  if ((collection.brunoConfig as BrunoConfig)?.ignore?.length) {\n    brunoExtension.ignore = (collection.brunoConfig as BrunoConfig).ignore;\n  }\n\n  const presets = (collection.brunoConfig as BrunoConfig)?.presets;\n  if (presets?.requestType || presets?.requestUrl) {\n    brunoExtension.presets = {};\n    if (presets.requestType) {\n      brunoExtension.presets.requestType = presets.requestType;\n    }\n    if (presets.requestUrl) {\n      brunoExtension.presets.requestUrl = presets.requestUrl;\n    }\n  }\n\n  if (Object.keys(brunoExtension).length > 0) {\n    openCollection.extensions = {\n      bruno: brunoExtension\n    };\n  }\n\n  return openCollection;\n};"
  },
  {
    "path": "packages/bruno-converters/src/opencollection/common/actions.ts",
    "content": "import { uuid } from '../../common/index.js';\nimport type {\n  Action,\n  ActionSetVariable,\n  ActionVariableScope,\n  BrunoVariable,\n  BrunoVariables\n} from '../types';\n\n/**\n * Convert Bruno post-response variables to OpenCollection actions.\n * Post-response variables in Bruno are converted to 'set-variable' actions\n * with phase 'after-response'.\n */\nexport const toOpenCollectionActions = (resVariables: BrunoVariables | null | undefined): Action[] | undefined => {\n  if (!resVariables?.length) {\n    return undefined;\n  }\n\n  const actions: Action[] = resVariables.map((v: BrunoVariable): ActionSetVariable => {\n    const action: ActionSetVariable = {\n      type: 'set-variable',\n      phase: 'after-response',\n      selector: {\n        expression: v.value || '',\n        method: 'jsonq'\n      },\n      variable: {\n        name: v.name || '',\n        scope: v.local ? 'request' : 'runtime' as ActionVariableScope\n      }\n    };\n\n    if (v.description && typeof v.description === 'string' && v.description.trim().length) {\n      action.description = v.description;\n    }\n\n    if (v.enabled === false) {\n      action.disabled = true;\n    }\n\n    return action;\n  });\n\n  return actions.length > 0 ? actions : undefined;\n};\n\n/**\n * Convert OpenCollection actions to Bruno post-response variables.\n * Only 'set-variable' actions with phase 'after-response' are converted.\n */\nexport const fromOpenCollectionActions = (actions: Action[] | null | undefined): BrunoVariables => {\n  if (!actions?.length) {\n    return [];\n  }\n\n  const resVars: BrunoVariables = [];\n\n  actions.forEach((action: Action) => {\n    // Only process 'set-variable' actions with 'after-response' phase\n    if (action.type === 'set-variable' && action.phase === 'after-response') {\n      const setVarAction = action as ActionSetVariable;\n\n      const variable: BrunoVariable = {\n        uid: uuid(),\n        name: setVarAction.variable?.name || '',\n        value: setVarAction.selector?.expression || '',\n        enabled: setVarAction.disabled !== true,\n        local: setVarAction.variable?.scope === 'request'\n      };\n\n      if (setVarAction.description) {\n        variable.description = typeof setVarAction.description === 'string'\n          ? setVarAction.description\n          : (setVarAction.description as { content?: string })?.content || '';\n      }\n\n      resVars.push(variable);\n    }\n  });\n\n  return resVars;\n};\n"
  },
  {
    "path": "packages/bruno-converters/src/opencollection/common/assertions.ts",
    "content": "import { uuid } from '../../common/index.js';\nimport type {\n  Assertion,\n  BrunoKeyValue\n} from '../types';\n\nexport const fromOpenCollectionAssertions = (assertions: Assertion[] | undefined): BrunoKeyValue[] => {\n  if (!assertions?.length) {\n    return [];\n  }\n\n  return assertions.map((a): BrunoKeyValue => ({\n    uid: uuid(),\n    name: a.expression || '',\n    value: `${a.operator || 'eq'} ${a.value || ''}`.trim(),\n    description: typeof a.description === 'string' ? a.description : a.description?.content || null,\n    enabled: a.disabled !== true\n  }));\n};\n\nexport const toOpenCollectionAssertions = (assertions: BrunoKeyValue[] | null | undefined): Assertion[] | undefined => {\n  if (!assertions?.length) {\n    return undefined;\n  }\n\n  return assertions.map((a): Assertion => {\n    const valueStr = a.value || '';\n    const parts = valueStr.split(' ');\n    const operator = parts[0] || 'eq';\n    const value = parts.slice(1).join(' ');\n\n    const ocAssertion: Assertion = {\n      expression: a.name || '',\n      operator,\n      value\n    };\n\n    if (a.enabled === false) {\n      ocAssertion.disabled = true;\n    }\n\n    if (a.description && typeof a.description === 'string' && a.description.trim().length) {\n      ocAssertion.description = a.description;\n    }\n\n    return ocAssertion;\n  });\n};\n"
  },
  {
    "path": "packages/bruno-converters/src/opencollection/common/auth.ts",
    "content": "import type {\n  Auth,\n  AuthBasic,\n  AuthBearer,\n  AuthDigest,\n  AuthNTLM,\n  AuthAwsV4,\n  AuthApiKey,\n  AuthWsse,\n  AuthOAuth2,\n  BrunoAuth,\n  BrunoOAuth2\n} from '../types';\n\nconst fromOpenCollectionOAuth2 = (auth: AuthOAuth2): BrunoAuth => {\n  const getTokenPlacement = (tokenConfig: AuthOAuth2['tokenConfig']): string => {\n    if (tokenConfig?.placement && 'query' in tokenConfig.placement) {\n      return 'query';\n    }\n    return 'header';\n  };\n\n  const getTokenHeaderPrefix = (tokenConfig: AuthOAuth2['tokenConfig']): string => {\n    if (tokenConfig?.placement && 'header' in tokenConfig.placement) {\n      return tokenConfig.placement.header;\n    }\n    return 'Bearer';\n  };\n\n  const getTokenQueryKey = (tokenConfig: AuthOAuth2['tokenConfig']): string => {\n    if (tokenConfig?.placement && 'query' in tokenConfig.placement) {\n      return tokenConfig.placement.query;\n    }\n    return 'access_token';\n  };\n\n  const getCredentialsPlacement = (credentials: AuthOAuth2['credentials']): 'body' | 'basic_auth_header' => {\n    if (credentials && 'placement' in credentials && credentials.placement === 'basic_auth_header') {\n      return 'basic_auth_header';\n    }\n    return 'body';\n  };\n\n  const buildOAuth2Config = (base: Partial<BrunoOAuth2>): BrunoAuth => {\n    const brunoAuth: BrunoAuth = {\n      mode: 'oauth2',\n      awsv4: null,\n      basic: null,\n      bearer: null,\n      digest: null,\n      ntlm: null,\n      oauth2: {\n        grantType: base.grantType || 'client_credentials',\n        username: base.username || null,\n        password: base.password || null,\n        callbackUrl: base.callbackUrl || null,\n        authorizationUrl: base.authorizationUrl || null,\n        accessTokenUrl: base.accessTokenUrl || null,\n        clientId: base.clientId || null,\n        clientSecret: base.clientSecret || null,\n        scope: base.scope || null,\n        state: base.state || null,\n        pkce: base.pkce ?? false,\n        credentialsPlacement: base.credentialsPlacement || null,\n        credentialsId: base.credentialsId || null,\n        tokenPlacement: base.tokenPlacement || null,\n        tokenHeaderPrefix: base.tokenHeaderPrefix || null,\n        tokenQueryKey: base.tokenQueryKey || null,\n        refreshTokenUrl: base.refreshTokenUrl || null,\n        autoRefreshToken: base.autoRefreshToken ?? false,\n        autoFetchToken: base.autoFetchToken ?? false,\n        additionalParameters: {}\n      },\n      wsse: null,\n      apikey: null\n    };\n    return brunoAuth;\n  };\n\n  switch (auth.flow) {\n    case 'client_credentials':\n      return buildOAuth2Config({\n        grantType: 'client_credentials',\n        accessTokenUrl: auth.accessTokenUrl || null,\n        refreshTokenUrl: auth.refreshTokenUrl || null,\n        clientId: auth.credentials?.clientId || null,\n        clientSecret: auth.credentials?.clientSecret || null,\n        scope: auth.scope || null,\n        credentialsPlacement: getCredentialsPlacement(auth.credentials),\n        credentialsId: auth.tokenConfig?.id || 'credentials',\n        tokenPlacement: getTokenPlacement(auth.tokenConfig),\n        tokenHeaderPrefix: getTokenHeaderPrefix(auth.tokenConfig),\n        tokenQueryKey: getTokenQueryKey(auth.tokenConfig),\n        autoFetchToken: auth.settings?.autoFetchToken !== false,\n        autoRefreshToken: auth.settings?.autoRefreshToken !== false\n      });\n\n    case 'resource_owner_password_credentials':\n      return buildOAuth2Config({\n        grantType: 'password',\n        accessTokenUrl: auth.accessTokenUrl || null,\n        refreshTokenUrl: auth.refreshTokenUrl || null,\n        clientId: auth.credentials?.clientId || null,\n        clientSecret: auth.credentials?.clientSecret || null,\n        username: auth.resourceOwner?.username || null,\n        password: auth.resourceOwner?.password || null,\n        scope: auth.scope || null,\n        credentialsPlacement: getCredentialsPlacement(auth.credentials),\n        credentialsId: auth.tokenConfig?.id || 'credentials',\n        tokenPlacement: getTokenPlacement(auth.tokenConfig),\n        tokenHeaderPrefix: getTokenHeaderPrefix(auth.tokenConfig),\n        tokenQueryKey: getTokenQueryKey(auth.tokenConfig),\n        autoFetchToken: auth.settings?.autoFetchToken !== false,\n        autoRefreshToken: auth.settings?.autoRefreshToken !== false\n      });\n\n    case 'authorization_code':\n      return buildOAuth2Config({\n        grantType: 'authorization_code',\n        authorizationUrl: auth.authorizationUrl || null,\n        accessTokenUrl: auth.accessTokenUrl || null,\n        refreshTokenUrl: auth.refreshTokenUrl || null,\n        callbackUrl: auth.callbackUrl || null,\n        clientId: auth.credentials?.clientId || null,\n        clientSecret: auth.credentials?.clientSecret || null,\n        scope: auth.scope || null,\n        pkce: (auth.pkce && !auth.pkce.disabled) || null,\n        credentialsPlacement: getCredentialsPlacement(auth.credentials),\n        credentialsId: auth.tokenConfig?.id || 'credentials',\n        tokenPlacement: getTokenPlacement(auth.tokenConfig),\n        tokenHeaderPrefix: getTokenHeaderPrefix(auth.tokenConfig),\n        tokenQueryKey: getTokenQueryKey(auth.tokenConfig),\n        autoFetchToken: auth.settings?.autoFetchToken !== false,\n        autoRefreshToken: auth.settings?.autoRefreshToken !== false\n      });\n\n    case 'implicit':\n      return buildOAuth2Config({\n        grantType: 'implicit',\n        authorizationUrl: auth.authorizationUrl || null,\n        callbackUrl: auth.callbackUrl || null,\n        clientId: auth.credentials?.clientId || null,\n        scope: auth.scope || null,\n        state: auth.state || null,\n        credentialsId: auth.tokenConfig?.id || 'credentials',\n        tokenPlacement: getTokenPlacement(auth.tokenConfig),\n        tokenHeaderPrefix: getTokenHeaderPrefix(auth.tokenConfig),\n        tokenQueryKey: getTokenQueryKey(auth.tokenConfig),\n        autoFetchToken: auth.settings?.autoFetchToken !== false\n      });\n\n    default:\n      return {\n        mode: 'none',\n        awsv4: null,\n        basic: null,\n        bearer: null,\n        digest: null,\n        ntlm: null,\n        oauth2: null,\n        wsse: null,\n        apikey: null\n      };\n  }\n};\n\nexport const fromOpenCollectionAuth = (auth: Auth | undefined): BrunoAuth => {\n  const defaultAuth: BrunoAuth = {\n    mode: 'none',\n    awsv4: null,\n    basic: null,\n    bearer: null,\n    digest: null,\n    ntlm: null,\n    oauth2: null,\n    wsse: null,\n    apikey: null\n  };\n\n  if (!auth) {\n    return defaultAuth;\n  }\n\n  if (auth === 'inherit') {\n    return { ...defaultAuth, mode: 'inherit' };\n  }\n\n  switch (auth.type) {\n    case 'basic': {\n      const basicAuth = auth as AuthBasic;\n      return {\n        ...defaultAuth,\n        mode: 'basic',\n        basic: {\n          username: basicAuth.username || null,\n          password: basicAuth.password || null\n        }\n      };\n    }\n\n    case 'bearer': {\n      const bearerAuth = auth as AuthBearer;\n      return {\n        ...defaultAuth,\n        mode: 'bearer',\n        bearer: {\n          token: bearerAuth.token || null\n        }\n      };\n    }\n\n    case 'digest': {\n      const digestAuth = auth as AuthDigest;\n      return {\n        ...defaultAuth,\n        mode: 'digest',\n        digest: {\n          username: digestAuth.username || null,\n          password: digestAuth.password || null\n        }\n      };\n    }\n\n    case 'ntlm': {\n      const ntlmAuth = auth as AuthNTLM;\n      return {\n        ...defaultAuth,\n        mode: 'ntlm',\n        ntlm: {\n          username: ntlmAuth.username || null,\n          password: ntlmAuth.password || null,\n          domain: ntlmAuth.domain || null\n        }\n      };\n    }\n\n    case 'awsv4': {\n      const awsAuth = auth as AuthAwsV4;\n      return {\n        ...defaultAuth,\n        mode: 'awsv4',\n        awsv4: {\n          accessKeyId: awsAuth.accessKeyId || null,\n          secretAccessKey: awsAuth.secretAccessKey || null,\n          sessionToken: awsAuth.sessionToken || null,\n          service: awsAuth.service || null,\n          region: awsAuth.region || null,\n          profileName: awsAuth.profileName || null\n        }\n      };\n    }\n\n    case 'apikey': {\n      const apiKeyAuth = auth as AuthApiKey;\n      return {\n        ...defaultAuth,\n        mode: 'apikey',\n        apikey: {\n          key: apiKeyAuth.key || null,\n          value: apiKeyAuth.value || null,\n          placement: apiKeyAuth.placement === 'query' ? 'queryparams' : (apiKeyAuth.placement === 'header' ? 'header' : null)\n        }\n      };\n    }\n\n    case 'wsse': {\n      const wsseAuth = auth as AuthWsse;\n      return {\n        ...defaultAuth,\n        mode: 'wsse',\n        wsse: {\n          username: wsseAuth.username || null,\n          password: wsseAuth.password || null\n        }\n      };\n    }\n\n    case 'oauth2':\n      return fromOpenCollectionOAuth2(auth as AuthOAuth2);\n\n    default:\n      return defaultAuth;\n  }\n};\n\nconst toOpenCollectionOAuth2 = (oauth2: BrunoOAuth2 | null | undefined): AuthOAuth2 | undefined => {\n  if (!oauth2) {\n    return undefined;\n  }\n\n  const base = { type: 'oauth2' as const };\n\n  switch (oauth2.grantType) {\n    case 'client_credentials':\n      return {\n        ...base,\n        flow: 'client_credentials',\n        accessTokenUrl: oauth2.accessTokenUrl || '',\n        refreshTokenUrl: oauth2.refreshTokenUrl || '',\n        credentials: {\n          clientId: oauth2.clientId || '',\n          clientSecret: oauth2.clientSecret || '',\n          placement: oauth2.credentialsPlacement === 'basic_auth_header' ? 'basic_auth_header' : 'body'\n        },\n        scope: oauth2.scope || '',\n        tokenConfig: {\n          id: oauth2.credentialsId || 'credentials',\n          placement: oauth2.tokenPlacement === 'query'\n            ? { query: oauth2.tokenQueryKey || 'access_token' }\n            : { header: oauth2.tokenHeaderPrefix || 'Bearer' }\n        },\n        settings: {\n          autoFetchToken: oauth2.autoFetchToken !== false,\n          autoRefreshToken: oauth2.autoRefreshToken !== false\n        }\n      };\n\n    case 'password':\n      return {\n        ...base,\n        flow: 'resource_owner_password_credentials',\n        accessTokenUrl: oauth2.accessTokenUrl || '',\n        refreshTokenUrl: oauth2.refreshTokenUrl || '',\n        credentials: {\n          clientId: oauth2.clientId || '',\n          clientSecret: oauth2.clientSecret || '',\n          placement: oauth2.credentialsPlacement === 'basic_auth_header' ? 'basic_auth_header' : 'body'\n        },\n        resourceOwner: {\n          username: oauth2.username || '',\n          password: oauth2.password || ''\n        },\n        scope: oauth2.scope || '',\n        tokenConfig: {\n          id: oauth2.credentialsId || 'credentials',\n          placement: oauth2.tokenPlacement === 'query'\n            ? { query: oauth2.tokenQueryKey || 'access_token' }\n            : { header: oauth2.tokenHeaderPrefix || 'Bearer' }\n        },\n        settings: {\n          autoFetchToken: oauth2.autoFetchToken !== false,\n          autoRefreshToken: oauth2.autoRefreshToken !== false\n        }\n      };\n\n    case 'authorization_code':\n      return {\n        ...base,\n        flow: 'authorization_code',\n        authorizationUrl: oauth2.authorizationUrl || '',\n        accessTokenUrl: oauth2.accessTokenUrl || '',\n        refreshTokenUrl: oauth2.refreshTokenUrl || '',\n        callbackUrl: oauth2.callbackUrl || '',\n        credentials: {\n          clientId: oauth2.clientId || '',\n          clientSecret: oauth2.clientSecret || '',\n          placement: oauth2.credentialsPlacement === 'basic_auth_header' ? 'basic_auth_header' : 'body'\n        },\n        scope: oauth2.scope || '',\n        pkce: oauth2.pkce ? { method: 'S256' } : undefined,\n        tokenConfig: {\n          id: oauth2.credentialsId || 'credentials',\n          placement: oauth2.tokenPlacement === 'query'\n            ? { query: oauth2.tokenQueryKey || 'access_token' }\n            : { header: oauth2.tokenHeaderPrefix || 'Bearer' }\n        },\n        settings: {\n          autoFetchToken: oauth2.autoFetchToken !== false,\n          autoRefreshToken: oauth2.autoRefreshToken !== false\n        }\n      };\n\n    case 'implicit':\n      return {\n        ...base,\n        flow: 'implicit',\n        authorizationUrl: oauth2.authorizationUrl || '',\n        callbackUrl: oauth2.callbackUrl || '',\n        credentials: {\n          clientId: oauth2.clientId || ''\n        },\n        scope: oauth2.scope || '',\n        state: oauth2.state || '',\n        tokenConfig: {\n          id: oauth2.credentialsId || 'credentials',\n          placement: oauth2.tokenPlacement === 'query'\n            ? { query: oauth2.tokenQueryKey || 'access_token' }\n            : { header: oauth2.tokenHeaderPrefix || 'Bearer' }\n        },\n        settings: {\n          autoFetchToken: oauth2.autoFetchToken !== false\n        }\n      };\n\n    default:\n      return undefined;\n  }\n};\n\nexport const toOpenCollectionAuth = (auth: BrunoAuth | null | undefined): Auth | undefined => {\n  if (!auth || auth.mode === 'none') {\n    return undefined;\n  }\n\n  if (auth.mode === 'inherit') {\n    return 'inherit';\n  }\n\n  switch (auth.mode) {\n    case 'basic':\n      return {\n        type: 'basic',\n        username: auth.basic?.username || '',\n        password: auth.basic?.password || ''\n      };\n\n    case 'bearer':\n      return {\n        type: 'bearer',\n        token: auth.bearer?.token || ''\n      };\n\n    case 'digest':\n      return {\n        type: 'digest',\n        username: auth.digest?.username || '',\n        password: auth.digest?.password || ''\n      };\n\n    case 'ntlm':\n      return {\n        type: 'ntlm',\n        username: auth.ntlm?.username || '',\n        password: auth.ntlm?.password || '',\n        domain: auth.ntlm?.domain || ''\n      };\n\n    case 'awsv4':\n      return {\n        type: 'awsv4',\n        accessKeyId: auth.awsv4?.accessKeyId || '',\n        secretAccessKey: auth.awsv4?.secretAccessKey || '',\n        sessionToken: auth.awsv4?.sessionToken || '',\n        service: auth.awsv4?.service || '',\n        region: auth.awsv4?.region || '',\n        profileName: auth.awsv4?.profileName || ''\n      };\n\n    case 'apikey':\n      return {\n        type: 'apikey',\n        key: auth.apikey?.key || '',\n        value: auth.apikey?.value || '',\n        placement: auth.apikey?.placement === 'queryparams' ? 'query' : 'header'\n      };\n\n    case 'wsse':\n      return {\n        type: 'wsse',\n        username: auth.wsse?.username || '',\n        password: auth.wsse?.password || ''\n      };\n\n    case 'oauth2':\n      return toOpenCollectionOAuth2(auth.oauth2);\n\n    default:\n      return undefined;\n  }\n};\n"
  },
  {
    "path": "packages/bruno-converters/src/opencollection/common/body.ts",
    "content": "import { uuid } from '../../common/index.js';\nimport type {\n  HttpRequestBody,\n  RawBody,\n  FormUrlEncodedBody,\n  FormUrlEncodedEntry,\n  MultipartFormBody,\n  MultipartFormEntry,\n  FileBody,\n  FileBodyVariant,\n  GraphQLBody,\n  BrunoHttpRequestBody,\n  BrunoKeyValue,\n  BrunoMultipartFormEntry,\n  BrunoFileEntry,\n  BrunoGraphqlBody\n} from '../types';\n\nexport const fromOpenCollectionBody = (body: HttpRequestBody | GraphQLBody | undefined, requestType: string = 'http'): BrunoHttpRequestBody => {\n  const defaultBody: BrunoHttpRequestBody = {\n    mode: 'none',\n    json: null,\n    text: null,\n    xml: null,\n    sparql: null,\n    formUrlEncoded: [],\n    multipartForm: [],\n    graphql: null,\n    file: []\n  };\n\n  if (!body) {\n    return defaultBody;\n  }\n\n  if (requestType === 'graphql') {\n    const gqlBody = body as GraphQLBody;\n    return {\n      ...defaultBody,\n      mode: 'graphql',\n      graphql: {\n        query: gqlBody.query || '',\n        variables: gqlBody.variables || ''\n      }\n    };\n  }\n\n  const httpBody = body as HttpRequestBody;\n\n  if ('type' in httpBody) {\n    switch (httpBody.type) {\n      case 'json': {\n        const rawBody = httpBody as RawBody;\n        return {\n          ...defaultBody,\n          mode: 'json',\n          json: rawBody.data || ''\n        };\n      }\n\n      case 'text': {\n        const rawBody = httpBody as RawBody;\n        return {\n          ...defaultBody,\n          mode: 'text',\n          text: rawBody.data || ''\n        };\n      }\n\n      case 'xml': {\n        const rawBody = httpBody as RawBody;\n        return {\n          ...defaultBody,\n          mode: 'xml',\n          xml: rawBody.data || ''\n        };\n      }\n\n      case 'sparql': {\n        const rawBody = httpBody as RawBody;\n        return {\n          ...defaultBody,\n          mode: 'sparql',\n          sparql: rawBody.data || ''\n        };\n      }\n\n      case 'form-urlencoded': {\n        const formBody = httpBody as FormUrlEncodedBody;\n        return {\n          ...defaultBody,\n          mode: 'formUrlEncoded',\n          formUrlEncoded: (formBody.data || []).map((field): BrunoKeyValue => ({\n            uid: uuid(),\n            name: field.name || '',\n            value: field.value || '',\n            description: typeof field.description === 'string' ? field.description : field.description?.content || null,\n            enabled: field.disabled !== true\n          }))\n        };\n      }\n\n      case 'multipart-form': {\n        const multipartBody = httpBody as MultipartFormBody;\n        return {\n          ...defaultBody,\n          mode: 'multipartForm',\n          multipartForm: (multipartBody.data || []).map((field): BrunoMultipartFormEntry => ({\n            uid: uuid(),\n            type: field.type || 'text',\n            name: field.name || '',\n            value: Array.isArray(field.value) ? field.value : (field.value || ''),\n            description: typeof field.description === 'string' ? field.description : field.description?.content || null,\n            contentType: null,\n            enabled: field.disabled !== true\n          }))\n        };\n      }\n\n      case 'file': {\n        const fileBody = httpBody as FileBody;\n        return {\n          ...defaultBody,\n          mode: 'file',\n          file: (fileBody.data || []).map((file): BrunoFileEntry => ({\n            uid: uuid(),\n            filePath: file.filePath || '',\n            contentType: file.contentType || '',\n            selected: file.selected !== false\n          }))\n        };\n      }\n    }\n  }\n\n  return defaultBody;\n};\n\nexport const toOpenCollectionBody = (body: BrunoHttpRequestBody | null | undefined): HttpRequestBody | undefined => {\n  if (!body || body.mode === 'none') {\n    return undefined;\n  }\n\n  switch (body.mode) {\n    case 'json':\n      return { type: 'json', data: body.json || '' };\n\n    case 'text':\n      return { type: 'text', data: body.text || '' };\n\n    case 'xml':\n      return { type: 'xml', data: body.xml || '' };\n\n    case 'sparql':\n      return { type: 'sparql', data: body.sparql || '' };\n\n    case 'formUrlEncoded': {\n      const formData: FormUrlEncodedEntry[] = (body.formUrlEncoded || []).map((field): FormUrlEncodedEntry => {\n        const entry: FormUrlEncodedEntry = {\n          name: field.name || '',\n          value: field.value || ''\n        };\n\n        if (field.description && typeof field.description === 'string' && field.description.trim().length) {\n          entry.description = field.description;\n        }\n\n        if (field.enabled === false) {\n          entry.disabled = true;\n        }\n\n        return entry;\n      });\n\n      return { type: 'form-urlencoded', data: formData };\n    }\n\n    case 'multipartForm': {\n      const multipartData: MultipartFormEntry[] = (body.multipartForm || []).map((field): MultipartFormEntry => {\n        const entry: MultipartFormEntry = {\n          name: field.name || '',\n          type: field.type || 'text',\n          value: field.value || ''\n        };\n\n        if (field.description && typeof field.description === 'string' && field.description.trim().length) {\n          entry.description = field.description;\n        }\n\n        if (field.enabled === false) {\n          entry.disabled = true;\n        }\n\n        return entry;\n      });\n\n      return { type: 'multipart-form', data: multipartData };\n    }\n\n    case 'file': {\n      const fileData: FileBodyVariant[] = (body.file || []).map((file): FileBodyVariant => ({\n        filePath: file.filePath || '',\n        contentType: file.contentType || '',\n        selected: file.selected !== false\n      }));\n\n      return { type: 'file', data: fileData };\n    }\n\n    case 'graphql':\n      return undefined;\n\n    default:\n      return undefined;\n  }\n};\n\nexport const toOpenCollectionGraphqlBody = (body: BrunoHttpRequestBody | null | undefined): GraphQLBody | undefined => {\n  if (!body || body.mode !== 'graphql' || !body.graphql) {\n    return undefined;\n  }\n\n  return {\n    query: body.graphql.query || '',\n    variables: body.graphql.variables || ''\n  };\n};\n"
  },
  {
    "path": "packages/bruno-converters/src/opencollection/common/headers.ts",
    "content": "import { uuid } from '../../common/index.js';\nimport type {\n  HttpRequestHeader,\n  BrunoKeyValue\n} from '../types';\n\nexport const fromOpenCollectionHeaders = (headers: HttpRequestHeader[] | undefined): BrunoKeyValue[] => {\n  if (!headers?.length) {\n    return [];\n  }\n\n  return headers.map((header): BrunoKeyValue => ({\n    uid: uuid(),\n    name: header.name || '',\n    value: header.value || '',\n    description: typeof header.description === 'string' ? header.description : header.description?.content || null,\n    enabled: header.disabled !== true\n  }));\n};\n\nexport const toOpenCollectionHeaders = (headers: BrunoKeyValue[] | null | undefined): HttpRequestHeader[] | undefined => {\n  if (!headers?.length) {\n    return undefined;\n  }\n\n  const ocHeaders = headers.map((header): HttpRequestHeader => {\n    const httpHeader: HttpRequestHeader = {\n      name: header.name || '',\n      value: header.value || ''\n    };\n\n    if (header.description && typeof header.description === 'string' && header.description.trim().length) {\n      httpHeader.description = header.description;\n    }\n\n    if (header.enabled === false) {\n      httpHeader.disabled = true;\n    }\n\n    return httpHeader;\n  });\n\n  return ocHeaders.length ? ocHeaders : undefined;\n};\n"
  },
  {
    "path": "packages/bruno-converters/src/opencollection/common/index.ts",
    "content": "export { fromOpenCollectionAuth, toOpenCollectionAuth } from './auth';\nexport { fromOpenCollectionHeaders, toOpenCollectionHeaders } from './headers';\nexport { fromOpenCollectionParams, toOpenCollectionParams } from './params';\nexport { fromOpenCollectionBody, toOpenCollectionBody, toOpenCollectionGraphqlBody } from './body';\nexport { fromOpenCollectionVariables, toOpenCollectionVariables } from './variables';\nexport { fromOpenCollectionActions, toOpenCollectionActions } from './actions';\nexport { fromOpenCollectionScripts, toOpenCollectionScripts } from './scripts';\nexport { fromOpenCollectionAssertions, toOpenCollectionAssertions } from './assertions';\n"
  },
  {
    "path": "packages/bruno-converters/src/opencollection/common/params.ts",
    "content": "import { uuid } from '../../common/index.js';\nimport type {\n  HttpRequestParam,\n  BrunoHttpRequestParam,\n  BrunoHttpRequestParamType\n} from '../types';\n\nexport const fromOpenCollectionParams = (params: HttpRequestParam[] | undefined): BrunoHttpRequestParam[] => {\n  if (!params?.length) {\n    return [];\n  }\n\n  return params.map((param): BrunoHttpRequestParam => ({\n    uid: uuid(),\n    name: param.name || '',\n    value: param.value || '',\n    description: typeof param.description === 'string' ? param.description : param.description?.content || null,\n    type: (param.type || 'query') as BrunoHttpRequestParamType,\n    enabled: param.disabled !== true\n  }));\n};\n\nexport const toOpenCollectionParams = (params: BrunoHttpRequestParam[] | null | undefined): HttpRequestParam[] | undefined => {\n  if (!params?.length) {\n    return undefined;\n  }\n\n  const ocParams = params.map((param): HttpRequestParam => {\n    const httpParam: HttpRequestParam = {\n      name: param.name || '',\n      value: param.value || '',\n      type: param.type || 'query'\n    };\n\n    if (param.description && typeof param.description === 'string' && param.description.trim().length) {\n      httpParam.description = param.description;\n    }\n\n    if (param.enabled === false) {\n      httpParam.disabled = true;\n    }\n\n    return httpParam;\n  });\n\n  return ocParams.length ? ocParams : undefined;\n};\n"
  },
  {
    "path": "packages/bruno-converters/src/opencollection/common/scripts.ts",
    "content": "import type { Scripts, Script } from '@opencollection/types/common/scripts';\nimport type { FolderRequest as BrunoFolderRequest } from '@usebruno/schema-types/collection/folder';\nimport type { HttpRequest as BrunoHttpRequest } from '@usebruno/schema-types/requests/http';\nimport type { WebSocketRequest as BrunoWebSocketRequest } from '@usebruno/schema-types/requests/websocket';\nimport type { GrpcRequest as BrunoGrpcRequest } from '@usebruno/schema-types/requests/grpc';\n\nexport const toOpenCollectionScripts = (request: BrunoFolderRequest | BrunoHttpRequest | BrunoWebSocketRequest | BrunoGrpcRequest | null | undefined): Scripts | undefined => {\n  const ocScripts: Scripts = [];\n\n  if (request?.script?.req?.trim().length) {\n    ocScripts.push({\n      type: 'before-request',\n      code: request.script.req.trim()\n    });\n  }\n  if (request?.script?.res?.trim().length) {\n    ocScripts.push({\n      type: 'after-response',\n      code: request.script.res.trim()\n    });\n  }\n  if (request?.tests?.trim().length) {\n    ocScripts.push({\n      type: 'tests',\n      code: request.tests.trim()\n    });\n  }\n\n  return ocScripts.length > 0 ? ocScripts : undefined;\n};\n\nexport const fromOpenCollectionScripts = (scripts: Scripts | null | undefined): {\n  script?: { req?: string | null; res?: string | null };\n  tests?: string | null;\n} | undefined => {\n  if (!scripts || !Array.isArray(scripts) || scripts.length === 0) {\n    return undefined;\n  }\n\n  const brunoScripts: {\n    script?: { req?: string | null; res?: string | null };\n    tests?: string | null;\n  } = {};\n\n  for (const script of scripts) {\n    if (script.type === 'before-request' && script.code) {\n      if (!brunoScripts.script) {\n        brunoScripts.script = {};\n      }\n      brunoScripts.script.req = script.code;\n    }\n    if (script.type === 'after-response' && script.code) {\n      if (!brunoScripts.script) {\n        brunoScripts.script = {};\n      }\n      brunoScripts.script.res = script.code;\n    }\n    if (script.type === 'tests' && script.code) {\n      brunoScripts.tests = script.code;\n    }\n  }\n\n  return Object.keys(brunoScripts).length > 0 ? brunoScripts : undefined;\n};\n"
  },
  {
    "path": "packages/bruno-converters/src/opencollection/common/variables.ts",
    "content": "import { uuid } from '../../common/index.js';\nimport type {\n  Variable,\n  BrunoVariable,\n  BrunoVariables\n} from '../types';\n\ninterface BrunoVars {\n  req: BrunoVariables;\n  res: BrunoVariables;\n}\n\nexport const fromOpenCollectionVariables = (variables: Variable[] | undefined): BrunoVars => {\n  if (!variables?.length) {\n    return { req: [], res: [] };\n  }\n\n  const reqVars: BrunoVariable[] = [];\n\n  variables.forEach((v: Variable) => {\n    let value = '';\n    if (typeof v.value === 'string') {\n      value = v.value;\n    } else if (v.value && typeof v.value === 'object' && 'data' in v.value) {\n      value = (v.value as { data: string }).data || '';\n    }\n\n    const variable: BrunoVariable = {\n      uid: uuid(),\n      name: v.name || '',\n      value,\n      enabled: v.disabled !== true,\n      local: false\n    };\n\n    if (v.description) {\n      variable.description = typeof v.description === 'string' ? v.description : (v.description as { content?: string })?.content || '';\n    }\n\n    reqVars.push(variable);\n  });\n\n  return { req: reqVars, res: [] };\n};\n\nexport const toOpenCollectionVariables = (vars: BrunoVars | { req?: BrunoVariables; res?: BrunoVariables } | null | undefined): Variable[] | undefined => {\n  // Handle folder variables (has req/res structure) - only use req vars\n  const hasReqRes = vars && 'req' in vars;\n  const reqVars = hasReqRes ? vars.req : vars as BrunoVariables;\n\n  const reqVarsArray = Array.isArray(reqVars) ? reqVars : [];\n\n  if (!reqVarsArray.length) {\n    return undefined;\n  }\n\n  const ocVariables: Variable[] = reqVarsArray.map((v: BrunoVariable): Variable => {\n    const variable: Variable = {\n      name: v.name || '',\n      value: v.value || ''\n    };\n\n    if (v.description && typeof v.description === 'string' && v.description.trim().length) {\n      variable.description = v.description;\n    }\n\n    if (v.enabled === false) {\n      variable.disabled = true;\n    }\n\n    return variable;\n  });\n\n  return ocVariables.length > 0 ? ocVariables : undefined;\n};\n"
  },
  {
    "path": "packages/bruno-converters/src/opencollection/environment.ts",
    "content": "import { uuid } from '../common/index.js';\nimport type {\n  Environment,\n  Variable,\n  BrunoEnvironment,\n  BrunoEnvironmentVariable\n} from './types';\n\ninterface OCVariable extends Omit<Variable, 'value'> {\n  name: string;\n  value?: string | { data: string };\n  secret?: boolean;\n  disabled?: boolean;\n}\n\nexport const fromOpenCollectionEnvironments = (environments: Environment[] | undefined): BrunoEnvironment[] => {\n  if (!environments?.length) {\n    return [];\n  }\n\n  return environments.map((env): BrunoEnvironment => ({\n    uid: uuid(),\n    name: env.name || 'Untitled Environment',\n    variables: (env.variables || []).map((v): BrunoEnvironmentVariable => {\n      const variable = v as OCVariable;\n      const isSecret = variable.secret === true;\n\n      let value = '';\n      if (!isSecret && variable.value !== undefined) {\n        if (typeof variable.value === 'string') {\n          value = variable.value;\n        } else if (variable.value && typeof variable.value === 'object' && 'data' in variable.value) {\n          value = variable.value.data;\n        }\n      }\n\n      return {\n        uid: uuid(),\n        name: variable.name || '',\n        value,\n        type: 'text',\n        enabled: variable.disabled !== true,\n        secret: isSecret\n      };\n    }),\n    color: env.color || null\n  }));\n};\n\nexport const toOpenCollectionEnvironments = (environments: BrunoEnvironment[] | undefined): Environment[] | undefined => {\n  if (!environments?.length) {\n    return undefined;\n  }\n\n  return environments.map((env): Environment => {\n    const ocEnv: Environment = {\n      name: env.name || 'Untitled Environment',\n      color: env.color ?? undefined,\n      variables: (env.variables || []).map((v): OCVariable => {\n        const ocVar: OCVariable = {\n          name: v.name || '',\n          value: typeof v.value === 'string' ? v.value : String(v.value ?? '')\n        };\n\n        if (v.secret) {\n          ocVar.secret = true;\n          // Secret variables don't include the value in export\n          delete ocVar.value;\n        }\n\n        if (v.enabled === false) {\n          ocVar.disabled = true;\n        }\n\n        return ocVar;\n      }) as Variable[]\n    };\n\n    return ocEnv;\n  });\n};\n"
  },
  {
    "path": "packages/bruno-converters/src/opencollection/folder.ts",
    "content": "import { uuid } from '../common/index.js';\nimport {\n  fromOpenCollectionHeaders,\n  toOpenCollectionHeaders,\n  fromOpenCollectionAuth,\n  toOpenCollectionAuth,\n  fromOpenCollectionScripts,\n  toOpenCollectionScripts,\n  fromOpenCollectionVariables,\n  toOpenCollectionVariables\n} from './common';\nimport { fromOpenCollectionItems, toOpenCollectionItems } from './items';\nimport type {\n  Folder,\n  FolderInfo,\n  RequestDefaults,\n  Auth,\n  BrunoItem,\n  BrunoFolderRoot,\n  BrunoKeyValue\n} from './types';\n\nexport const fromOpenCollectionFolder = (folder: Folder): BrunoItem => {\n  const info = folder.info || {};\n\n  const brunoFolder: BrunoItem = {\n    uid: uuid(),\n    type: 'folder',\n    name: info.name || 'Untitled Folder',\n    seq: info.seq || 1\n  };\n\n  if (folder.request || folder.docs) {\n    const root: BrunoFolderRoot = {};\n\n    if (folder.request) {\n      const scripts = fromOpenCollectionScripts(folder.request.scripts);\n      root.request = {\n        headers: fromOpenCollectionHeaders(folder.request.headers),\n        auth: fromOpenCollectionAuth(folder.request.auth as Auth),\n        script: scripts?.script,\n        vars: fromOpenCollectionVariables(folder.request.variables),\n        tests: scripts?.tests\n      };\n    }\n\n    if (folder.docs) {\n      if (typeof folder.docs === 'string') {\n        root.docs = folder.docs;\n      } else if (folder.docs && typeof folder.docs === 'object' && 'content' in folder.docs) {\n        root.docs = folder.docs.content || '';\n      }\n    }\n\n    root.meta = {\n      name: info.name || 'Untitled Folder',\n      seq: info.seq || 1\n    };\n\n    brunoFolder.root = root;\n  }\n\n  if (info.tags?.length) {\n    brunoFolder.tags = info.tags;\n  }\n\n  if (folder.items?.length) {\n    brunoFolder.items = fromOpenCollectionItems(folder.items, fromOpenCollectionFolder as (f: unknown) => BrunoItem);\n  }\n\n  return brunoFolder;\n};\n\nexport const toOpenCollectionFolder = (folder: BrunoItem): Folder => {\n  const info: FolderInfo = {\n    name: folder.name || 'Untitled Folder',\n    type: 'folder'\n  };\n\n  if (folder.seq) {\n    info.seq = folder.seq;\n  }\n\n  if (folder.tags?.length) {\n    info.tags = folder.tags;\n  }\n\n  const ocFolder: Folder = {\n    info\n  };\n\n  if (folder.root) {\n    const folderRequest = folder.root.request || {};\n\n    const headers = toOpenCollectionHeaders(folderRequest.headers as BrunoKeyValue[]);\n    const auth = toOpenCollectionAuth(folderRequest.auth);\n    const scripts = toOpenCollectionScripts(folderRequest as { script?: { req: string | null; res: string | null } | null; tests?: string | null });\n    const variables = toOpenCollectionVariables(folderRequest.vars);\n\n    if (headers || auth || scripts || variables) {\n      const request: RequestDefaults = {};\n\n      if (headers) {\n        request.headers = headers;\n      }\n\n      if (auth) {\n        request.auth = auth;\n      }\n\n      if (scripts) {\n        request.scripts = scripts;\n      }\n\n      if (variables) {\n        request.variables = variables;\n      }\n\n      ocFolder.request = request;\n    }\n\n    if (folder.root.docs) {\n      ocFolder.docs = {\n        content: folder.root.docs,\n        type: 'text/markdown'\n      };\n    }\n  }\n\n  if (folder.items?.length) {\n    ocFolder.items = toOpenCollectionItems(folder.items, toOpenCollectionFolder as (f: BrunoItem) => unknown) as Folder['items'];\n  }\n\n  return ocFolder;\n};\n"
  },
  {
    "path": "packages/bruno-converters/src/opencollection/index.ts",
    "content": "export { openCollectionToBruno } from './opencollection-to-bruno';\nexport { brunoToOpenCollection } from './bruno-to-opencollection';\nexport { fromOpenCollectionFolder, toOpenCollectionFolder } from './folder';\nexport { fromOpenCollectionEnvironments, toOpenCollectionEnvironments } from './environment';\n\nexport {\n  fromOpenCollectionItem,\n  toOpenCollectionItem,\n  fromOpenCollectionItems,\n  toOpenCollectionItems,\n  fromOpenCollectionHttpItem,\n  toOpenCollectionHttpItem,\n  fromOpenCollectionGraphqlItem,\n  toOpenCollectionGraphqlItem,\n  fromOpenCollectionGrpcItem,\n  toOpenCollectionGrpcItem,\n  fromOpenCollectionWebsocketItem,\n  toOpenCollectionWebsocketItem\n} from './items';\n\nexport {\n  fromOpenCollectionAuth,\n  toOpenCollectionAuth,\n  fromOpenCollectionHeaders,\n  toOpenCollectionHeaders,\n  fromOpenCollectionParams,\n  toOpenCollectionParams,\n  fromOpenCollectionBody,\n  toOpenCollectionBody,\n  toOpenCollectionGraphqlBody,\n  fromOpenCollectionVariables,\n  toOpenCollectionVariables,\n  fromOpenCollectionScripts,\n  toOpenCollectionScripts,\n  fromOpenCollectionAssertions,\n  toOpenCollectionAssertions\n} from './common';\n"
  },
  {
    "path": "packages/bruno-converters/src/opencollection/items/graphql.ts",
    "content": "import { uuid } from '../../common/index.js';\nimport {\n  fromOpenCollectionHeaders,\n  toOpenCollectionHeaders,\n  fromOpenCollectionParams,\n  toOpenCollectionParams,\n  fromOpenCollectionBody,\n  toOpenCollectionGraphqlBody,\n  fromOpenCollectionAuth,\n  toOpenCollectionAuth,\n  fromOpenCollectionScripts,\n  toOpenCollectionScripts,\n  fromOpenCollectionVariables,\n  toOpenCollectionVariables,\n  fromOpenCollectionActions,\n  toOpenCollectionActions,\n  fromOpenCollectionAssertions,\n  toOpenCollectionAssertions\n} from '../common';\nimport type {\n  GraphQLRequest,\n  GraphQLRequestInfo,\n  GraphQLRequestDetails,\n  GraphQLRequestRuntime,\n  GraphQLRequestSettings,\n  GraphQLBody,\n  GraphQLBodyVariant,\n  Auth,\n  BrunoItem,\n  BrunoKeyValue,\n  BrunoHttpRequestParam\n} from '../types';\n\nconst getGraphqlBody = (body: GraphQLBody | GraphQLBodyVariant[] | undefined): GraphQLBody | undefined => {\n  if (!body) return undefined;\n  if (Array.isArray(body)) {\n    const selected = body.find((v) => v.selected);\n    return selected?.body || body[0]?.body;\n  }\n  return body;\n};\n\nexport const fromOpenCollectionGraphqlItem = (item: GraphQLRequest): BrunoItem => {\n  const info = item.info || {};\n  const graphql = item.graphql || {};\n  const runtime = item.runtime || {};\n\n  const scripts = fromOpenCollectionScripts(runtime.scripts);\n  const graphqlBody = getGraphqlBody(graphql.body);\n\n  // variables (pre-request from variables, post-response from actions)\n  const variables = fromOpenCollectionVariables(runtime.variables);\n  const postResponseVars = fromOpenCollectionActions(runtime.actions);\n\n  const brunoItem: BrunoItem = {\n    uid: uuid(),\n    type: 'graphql-request',\n    name: info.name || 'Untitled Request',\n    seq: info.seq || 1,\n    request: {\n      url: graphql.url || '',\n      method: graphql.method || 'POST',\n      headers: fromOpenCollectionHeaders(graphql.headers),\n      params: fromOpenCollectionParams(graphql.params),\n      body: fromOpenCollectionBody(graphqlBody, 'graphql'),\n      auth: fromOpenCollectionAuth(graphql.auth as Auth),\n      script: scripts?.script,\n      vars: {\n        req: variables.req,\n        res: postResponseVars\n      },\n      assertions: fromOpenCollectionAssertions(runtime.assertions),\n      tests: scripts?.tests,\n      docs: item.docs || ''\n    }\n  };\n\n  const settings = item.settings;\n  if (settings) {\n    brunoItem.settings = {};\n    if (settings.encodeUrl !== undefined) {\n      (brunoItem.settings as Record<string, unknown>).encodeUrl = settings.encodeUrl;\n    }\n    if (settings.timeout !== undefined) {\n      (brunoItem.settings as Record<string, unknown>).timeout = settings.timeout;\n    }\n    if (settings.followRedirects !== undefined) {\n      (brunoItem.settings as Record<string, unknown>).followRedirects = settings.followRedirects;\n    }\n    if (settings.maxRedirects !== undefined) {\n      (brunoItem.settings as Record<string, unknown>).maxRedirects = settings.maxRedirects;\n    }\n  }\n\n  if (info.tags?.length) {\n    brunoItem.tags = info.tags;\n  }\n\n  return brunoItem;\n};\n\nexport const toOpenCollectionGraphqlItem = (item: BrunoItem): GraphQLRequest => {\n  const request = (item.request || {}) as Record<string, unknown>;\n  const brunoSettings = (item.settings || {}) as Record<string, unknown>;\n\n  const info: GraphQLRequestInfo = {\n    name: item.name || 'Untitled Request',\n    type: 'graphql'\n  };\n\n  if (item.seq) {\n    info.seq = item.seq;\n  }\n\n  if (item.tags?.length) {\n    info.tags = item.tags;\n  }\n\n  const graphql: GraphQLRequestDetails = {\n    url: request.url as string || '',\n    method: request.method as string || 'POST'\n  };\n\n  const headers = toOpenCollectionHeaders(request.headers as BrunoKeyValue[]);\n  if (headers) {\n    graphql.headers = headers;\n  }\n\n  const params = toOpenCollectionParams(request.params as BrunoHttpRequestParam[]);\n  if (params) {\n    graphql.params = params;\n  }\n\n  const body = toOpenCollectionGraphqlBody(request.body as Parameters<typeof toOpenCollectionGraphqlBody>[0]);\n  if (body) {\n    graphql.body = body;\n  }\n\n  // auth\n  const auth = toOpenCollectionAuth(request.auth as Parameters<typeof toOpenCollectionAuth>[0]);\n  if (auth) {\n    graphql.auth = auth;\n  }\n\n  const ocRequest: GraphQLRequest = {\n    info,\n    graphql\n  };\n\n  const scripts = toOpenCollectionScripts(request as Parameters<typeof toOpenCollectionScripts>[0]);\n  const variables = toOpenCollectionVariables(request.vars as Parameters<typeof toOpenCollectionVariables>[0]);\n  const assertions = toOpenCollectionAssertions(request.assertions as BrunoKeyValue[]);\n\n  // actions (from post-response variables)\n  const vars = request.vars as { req?: unknown[]; res?: unknown[] } | undefined;\n  const actions = toOpenCollectionActions(vars?.res as Parameters<typeof toOpenCollectionActions>[0]);\n\n  if (scripts || variables || assertions || actions) {\n    const runtime: GraphQLRequestRuntime = {};\n\n    if (scripts) {\n      runtime.scripts = scripts;\n    }\n\n    if (variables) {\n      runtime.variables = variables;\n    }\n\n    if (assertions) {\n      runtime.assertions = assertions;\n    }\n\n    if (actions) {\n      runtime.actions = actions;\n    }\n\n    ocRequest.runtime = runtime;\n  }\n\n  const settings: GraphQLRequestSettings = {\n    encodeUrl: typeof brunoSettings.encodeUrl === 'boolean' ? brunoSettings.encodeUrl : true,\n    timeout: typeof brunoSettings.timeout === 'number' ? brunoSettings.timeout : 0,\n    followRedirects: typeof brunoSettings.followRedirects === 'boolean' ? brunoSettings.followRedirects : true,\n    maxRedirects: typeof brunoSettings.maxRedirects === 'number' ? brunoSettings.maxRedirects : 5\n  };\n  ocRequest.settings = settings;\n\n  if (request.docs) {\n    ocRequest.docs = request.docs as string;\n  }\n\n  return ocRequest;\n};\n"
  },
  {
    "path": "packages/bruno-converters/src/opencollection/items/grpc.ts",
    "content": "import { uuid } from '../../common/index.js';\nimport {\n  fromOpenCollectionAuth,\n  toOpenCollectionAuth,\n  fromOpenCollectionScripts,\n  toOpenCollectionScripts,\n  fromOpenCollectionVariables,\n  toOpenCollectionVariables,\n  fromOpenCollectionActions,\n  toOpenCollectionActions,\n  fromOpenCollectionAssertions,\n  toOpenCollectionAssertions\n} from '../common';\nimport type {\n  GrpcRequest,\n  GrpcRequestInfo,\n  GrpcRequestDetails,\n  GrpcRequestRuntime,\n  GrpcMetadata,\n  GrpcMessageVariant,\n  GrpcMessagePayload,\n  Auth,\n  BrunoItem,\n  BrunoGrpcMessage,\n  BrunoKeyValue\n} from '../types';\n\nconst fromGrpcMetadata = (metadata: GrpcMetadata[] | undefined): BrunoKeyValue[] => {\n  if (!metadata?.length) {\n    return [];\n  }\n\n  return metadata.map((m): BrunoKeyValue => ({\n    uid: uuid(),\n    name: m.name || '',\n    value: m.value || '',\n    description: typeof m.description === 'string' ? m.description : (m.description as { content?: string } | undefined)?.content || null,\n    enabled: m.disabled !== true\n  }));\n};\n\nexport const fromOpenCollectionGrpcItem = (item: GrpcRequest): BrunoItem => {\n  const info = item.info || {};\n  const grpc = item.grpc || {};\n  const runtime = item.runtime || {};\n\n  const grpcMessages: BrunoGrpcMessage[] = [];\n\n  if (grpc.message) {\n    if (typeof grpc.message === 'string') {\n      grpcMessages.push({ name: 'message 1', content: grpc.message });\n    } else if (Array.isArray(grpc.message)) {\n      grpc.message.forEach((msg, index) => {\n        grpcMessages.push({\n          name: msg.title || `message ${index + 1}`,\n          content: typeof msg.message === 'string' ? msg.message : ''\n        });\n      });\n    }\n  }\n\n  const scripts = fromOpenCollectionScripts(runtime.scripts);\n\n  // variables (pre-request from variables, post-response from actions)\n  const variables = fromOpenCollectionVariables(runtime.variables);\n  const postResponseVars = fromOpenCollectionActions((runtime as { actions?: Parameters<typeof fromOpenCollectionActions>[0] }).actions);\n\n  const brunoItem: BrunoItem = {\n    uid: uuid(),\n    type: 'grpc-request',\n    name: info.name || 'Untitled Request',\n    seq: info.seq || 1,\n    request: {\n      url: grpc.url || '',\n      method: grpc.method || '',\n      headers: fromGrpcMetadata(grpc.metadata),\n      body: {\n        mode: 'grpc',\n        grpc: grpcMessages\n      },\n      auth: fromOpenCollectionAuth(grpc.auth as Auth),\n      script: scripts?.script,\n      vars: {\n        req: variables.req,\n        res: postResponseVars\n      },\n      assertions: fromOpenCollectionAssertions(runtime.assertions),\n      tests: scripts?.tests,\n      docs: ''\n    }\n  };\n\n  // Add grpc-specific properties\n  if (grpc.methodType) {\n    (brunoItem.request as unknown as Record<string, unknown>).methodType = grpc.methodType;\n  }\n  if (grpc.protoFilePath) {\n    (brunoItem.request as unknown as Record<string, unknown>).protoPath = grpc.protoFilePath;\n  }\n\n  if (info.tags?.length) {\n    brunoItem.tags = info.tags;\n  }\n\n  return brunoItem;\n};\n\nexport const toOpenCollectionGrpcItem = (item: BrunoItem): GrpcRequest => {\n  const request = (item.request || {}) as Record<string, unknown>;\n\n  const info: GrpcRequestInfo = {\n    name: item.name || 'Untitled Request',\n    type: 'grpc'\n  };\n\n  if (item.seq) {\n    info.seq = item.seq;\n  }\n\n  if (item.tags?.length) {\n    info.tags = item.tags;\n  }\n\n  const grpc: GrpcRequestDetails = {\n    url: request.url as string || '',\n    method: request.method as string || ''\n  };\n\n  if (request.methodType) {\n    grpc.methodType = request.methodType as GrpcRequestDetails['methodType'];\n  }\n  if (request.protoPath) {\n    grpc.protoFilePath = request.protoPath as string;\n  }\n\n  const headers = request.headers as BrunoKeyValue[] | undefined;\n  if (headers?.length) {\n    grpc.metadata = headers.map((h): GrpcMetadata => {\n      const metadata: GrpcMetadata = {\n        name: h.name || '',\n        value: h.value || ''\n      };\n\n      if (h.description && typeof h.description === 'string' && h.description.trim().length) {\n        metadata.description = h.description;\n      }\n\n      if (h.enabled === false) {\n        metadata.disabled = true;\n      }\n\n      return metadata;\n    });\n  }\n\n  const body = request.body as { grpc?: BrunoGrpcMessage[] } | undefined;\n  if (body?.grpc?.length) {\n    const messages = body.grpc;\n    if (messages.length === 1) {\n      grpc.message = messages[0].content || '';\n    } else {\n      grpc.message = messages.map((msg): GrpcMessageVariant => ({\n        title: msg.name || 'Untitled',\n        message: msg.content || ''\n      }));\n    }\n  }\n\n  // auth\n  const auth = toOpenCollectionAuth(request.auth as Parameters<typeof toOpenCollectionAuth>[0]);\n  if (auth) {\n    grpc.auth = auth;\n  }\n\n  const ocRequest: GrpcRequest = {\n    info,\n    grpc\n  };\n\n  const scripts = toOpenCollectionScripts(request as Parameters<typeof toOpenCollectionScripts>[0]);\n  const variables = toOpenCollectionVariables(request.vars as Parameters<typeof toOpenCollectionVariables>[0]);\n  const assertions = toOpenCollectionAssertions(request.assertions as BrunoKeyValue[]);\n\n  // actions (from post-response variables)\n  const vars = request.vars as { req?: unknown[]; res?: unknown[] } | undefined;\n  const actions = toOpenCollectionActions(vars?.res as Parameters<typeof toOpenCollectionActions>[0]);\n\n  if (scripts || variables || assertions || actions) {\n    const runtime: GrpcRequestRuntime = {};\n\n    if (scripts) {\n      runtime.scripts = scripts;\n    }\n\n    if (variables) {\n      runtime.variables = variables;\n    }\n\n    if (assertions) {\n      runtime.assertions = assertions;\n    }\n\n    if (actions) {\n      (runtime as { actions?: typeof actions }).actions = actions;\n    }\n\n    ocRequest.runtime = runtime;\n  }\n\n  if (request.docs) {\n    ocRequest.docs = request.docs as string;\n  }\n\n  return ocRequest;\n};\n"
  },
  {
    "path": "packages/bruno-converters/src/opencollection/items/http.ts",
    "content": "import { uuid } from '../../common/index.js';\nimport {\n  fromOpenCollectionHeaders,\n  toOpenCollectionHeaders,\n  fromOpenCollectionParams,\n  toOpenCollectionParams,\n  fromOpenCollectionBody,\n  toOpenCollectionBody,\n  fromOpenCollectionAuth,\n  toOpenCollectionAuth,\n  fromOpenCollectionScripts,\n  toOpenCollectionScripts,\n  fromOpenCollectionVariables,\n  toOpenCollectionVariables,\n  fromOpenCollectionActions,\n  toOpenCollectionActions,\n  fromOpenCollectionAssertions,\n  toOpenCollectionAssertions\n} from '../common';\nimport type {\n  HttpRequest,\n  HttpRequestSettings,\n  HttpRequestExample,\n  HttpRequestInfo,\n  HttpRequestDetails,\n  HttpRequestRuntime,\n  HttpRequestHeader,\n  HttpRequestBody,\n  Auth,\n  BrunoItem,\n  BrunoKeyValue,\n  BrunoHttpRequestParam,\n  BrunoExample,\n  BrunoHttpRequest\n} from '../types';\nimport type { HttpItemSettings as BrunoHttpItemSettings } from '@usebruno/schema-types/collection/item';\n\nconst getHttpBody = (body: HttpRequestBody | Array<{ title: string; selected?: boolean; body: HttpRequestBody }> | undefined): HttpRequestBody | undefined => {\n  if (!body) return undefined;\n  if (Array.isArray(body)) {\n    const selected = body.find((v) => v.selected);\n    return selected?.body || body[0]?.body;\n  }\n  return body;\n};\n\nexport const fromOpenCollectionHttpItem = (ocRequest: HttpRequest): BrunoItem => {\n  const info = ocRequest.info;\n  const http = ocRequest.http;\n  const runtime = ocRequest.runtime;\n\n  const scripts = fromOpenCollectionScripts(runtime?.scripts);\n  const httpBody = getHttpBody(http?.body as HttpRequestBody);\n\n  // variables (pre-request from variables, post-response from actions)\n  const variables = fromOpenCollectionVariables(runtime?.variables);\n  const postResponseVars = fromOpenCollectionActions(runtime?.actions);\n\n  const brunoRequest: BrunoHttpRequest = {\n    url: http?.url || '',\n    method: http?.method || 'GET',\n    headers: fromOpenCollectionHeaders(http?.headers) || [],\n    params: fromOpenCollectionParams(http?.params) || [],\n    body: fromOpenCollectionBody(httpBody) || {\n      mode: 'none',\n      json: null,\n      text: null,\n      xml: null,\n      sparql: null,\n      formUrlEncoded: [],\n      multipartForm: [],\n      graphql: null,\n      file: []\n    },\n    auth: fromOpenCollectionAuth(http?.auth as Auth),\n    script: {\n      req: scripts?.script?.req || null,\n      res: scripts?.script?.res || null\n    },\n    vars: {\n      req: variables.req,\n      res: postResponseVars\n    },\n    assertions: fromOpenCollectionAssertions(runtime?.assertions) || [],\n    tests: scripts?.tests || null,\n    docs: ocRequest.docs || null\n  };\n\n  const brunoItem: BrunoItem = {\n    uid: uuid(),\n    type: 'http-request',\n    seq: info?.seq || 1,\n    name: info?.name || 'Untitled Request',\n    tags: info?.tags || [],\n    request: brunoRequest,\n    settings: null,\n    fileContent: null,\n    root: null,\n    items: [],\n    examples: [],\n    filename: null,\n    pathname: null\n  };\n\n  if (ocRequest.settings) {\n    const settings: BrunoHttpItemSettings = {\n      encodeUrl: typeof ocRequest.settings.encodeUrl === 'boolean' ? ocRequest.settings.encodeUrl : true,\n      timeout: typeof ocRequest.settings.timeout === 'number' ? ocRequest.settings.timeout : 0,\n      followRedirects: typeof ocRequest.settings.followRedirects === 'boolean' ? ocRequest.settings.followRedirects : true,\n      maxRedirects: typeof ocRequest.settings.maxRedirects === 'number' ? ocRequest.settings.maxRedirects : 5\n    };\n    brunoItem.settings = settings;\n  }\n\n  if (ocRequest.examples?.length) {\n    brunoItem.examples = ocRequest.examples.map((example): BrunoExample => ({\n      uid: uuid(),\n      itemUid: brunoItem.uid,\n      name: example.name || 'Untitled Example',\n      description: typeof example.description === 'string' ? example.description : (example.description as { content?: string })?.content || null,\n      type: 'http-request',\n      request: {\n        url: example.request?.url || '',\n        method: example.request?.method || 'GET',\n        headers: fromOpenCollectionHeaders(example.request?.headers) || [],\n        params: fromOpenCollectionParams(example.request?.params) || [],\n        body: fromOpenCollectionBody(example.request?.body) || null\n      },\n      response: example.response ? {\n        status: example.response.status || 200,\n        statusText: example.response.statusText || 'OK',\n        headers: fromOpenCollectionHeaders(example.response.headers as HttpRequestHeader[]) || [],\n        body: example.response.body ? {\n          type: example.response.body.type || 'text',\n          content: example.response.body.data || ''\n        } : null\n      } : null\n    }));\n  }\n\n  return brunoItem;\n};\n\nexport const toOpenCollectionHttpItem = (item: BrunoItem): HttpRequest => {\n  const ocRequest: HttpRequest = {};\n  const brunoRequest = item.request as BrunoHttpRequest;\n  const brunoSettings = item.settings as BrunoHttpItemSettings | undefined;\n\n  const info: HttpRequestInfo = {\n    name: item.name || 'Untitled Request',\n    type: 'http'\n  };\n  if (item.seq) {\n    info.seq = item.seq;\n  }\n  if (item.tags?.length) {\n    info.tags = item.tags;\n  }\n  ocRequest.info = info;\n\n  const http: HttpRequestDetails = {\n    method: brunoRequest?.method || 'GET',\n    url: brunoRequest?.url || ''\n  };\n\n  const headers = toOpenCollectionHeaders(brunoRequest?.headers as BrunoKeyValue[]);\n  if (headers) {\n    http.headers = headers;\n  }\n\n  const params = toOpenCollectionParams(brunoRequest?.params as BrunoHttpRequestParam[]);\n  if (params) {\n    http.params = params;\n  }\n\n  const body = toOpenCollectionBody(brunoRequest?.body);\n  if (body) {\n    http.body = body;\n  }\n\n  // auth\n  const auth = toOpenCollectionAuth(brunoRequest?.auth);\n  if (auth) {\n    http.auth = auth;\n  }\n\n  ocRequest.http = http;\n\n  const runtime: HttpRequestRuntime = {};\n  let hasRuntime = false;\n\n  const variables = toOpenCollectionVariables(brunoRequest?.vars);\n  if (variables) {\n    runtime.variables = variables;\n    hasRuntime = true;\n  }\n\n  const scripts = toOpenCollectionScripts(brunoRequest);\n  if (scripts) {\n    runtime.scripts = scripts;\n    hasRuntime = true;\n  }\n\n  const assertions = toOpenCollectionAssertions(brunoRequest?.assertions as BrunoKeyValue[]);\n  if (assertions) {\n    runtime.assertions = assertions;\n    hasRuntime = true;\n  }\n\n  // actions (from post-response variables)\n  const resVars = brunoRequest?.vars?.res;\n  const actions = toOpenCollectionActions(resVars);\n  if (actions) {\n    runtime.actions = actions;\n    hasRuntime = true;\n  }\n\n  if (hasRuntime) {\n    ocRequest.runtime = runtime;\n  }\n\n  const settings: HttpRequestSettings = {\n    encodeUrl: typeof brunoSettings?.encodeUrl === 'boolean' ? brunoSettings.encodeUrl : true,\n    timeout: typeof brunoSettings?.timeout === 'number' ? brunoSettings.timeout : 0,\n    followRedirects: typeof brunoSettings?.followRedirects === 'boolean' ? brunoSettings.followRedirects : true,\n    maxRedirects: typeof brunoSettings?.maxRedirects === 'number' ? brunoSettings.maxRedirects : 5\n  };\n  ocRequest.settings = settings;\n\n  if (brunoRequest?.docs) {\n    ocRequest.docs = brunoRequest.docs;\n  }\n\n  if (item.examples?.length) {\n    ocRequest.examples = item.examples.map((example): HttpRequestExample => {\n      const ocExample: HttpRequestExample = {\n        name: example.name || 'Untitled Example'\n      };\n\n      if (example.description) {\n        ocExample.description = example.description;\n      }\n\n      if (example.request) {\n        ocExample.request = {\n          url: example.request.url || '',\n          method: example.request.method || 'GET'\n        };\n\n        const exampleHeaders = toOpenCollectionHeaders(example.request.headers as BrunoKeyValue[]);\n        if (exampleHeaders) {\n          ocExample.request.headers = exampleHeaders;\n        }\n\n        const exampleParams = toOpenCollectionParams(example.request.params as BrunoHttpRequestParam[]);\n        if (exampleParams) {\n          ocExample.request.params = exampleParams;\n        }\n\n        const exampleBody = toOpenCollectionBody(example.request.body);\n        if (exampleBody) {\n          ocExample.request.body = exampleBody;\n        }\n      }\n\n      if (example.response) {\n        ocExample.response = {};\n\n        if (example.response.status !== undefined) {\n          ocExample.response.status = Number(example.response.status);\n        }\n\n        if (example.response.statusText) {\n          ocExample.response.statusText = example.response.statusText;\n        }\n\n        const responseHeaders = toOpenCollectionHeaders(example.response.headers as BrunoKeyValue[]);\n        if (responseHeaders) {\n          ocExample.response.headers = responseHeaders;\n        }\n\n        if (example.response.body) {\n          ocExample.response.body = {\n            type: (example.response.body.type as 'json' | 'text' | 'xml' | 'html' | 'binary') || 'text',\n            data: String(example.response.body.content || '')\n          };\n        }\n      }\n\n      return ocExample;\n    });\n  }\n\n  return ocRequest;\n};\n"
  },
  {
    "path": "packages/bruno-converters/src/opencollection/items/index.ts",
    "content": "import { uuid } from '../../common/index.js';\nimport { fromOpenCollectionHttpItem, toOpenCollectionHttpItem } from './http';\nimport { fromOpenCollectionGraphqlItem, toOpenCollectionGraphqlItem } from './graphql';\nimport { fromOpenCollectionGrpcItem, toOpenCollectionGrpcItem } from './grpc';\nimport { fromOpenCollectionWebsocketItem, toOpenCollectionWebsocketItem } from './websocket';\nimport type {\n  BrunoItem\n} from '../types';\n\ninterface OCItem {\n  info?: {\n    type?: string;\n    name?: string;\n    seq?: number;\n  };\n  http?: unknown;\n  graphql?: unknown;\n  grpc?: unknown;\n  websocket?: unknown;\n  items?: unknown[];\n  script?: string;\n}\n\nconst getItemType = (item: OCItem): string => {\n  if (item.info?.type) {\n    return item.info.type;\n  }\n\n  if ('items' in item && item.items) {\n    return 'folder';\n  }\n\n  if ('http' in item && item.http) {\n    return 'http';\n  }\n\n  if ('graphql' in item && item.graphql) {\n    return 'graphql';\n  }\n\n  if ('grpc' in item && item.grpc) {\n    return 'grpc';\n  }\n\n  if ('websocket' in item && item.websocket) {\n    return 'websocket';\n  }\n\n  if ('script' in item && typeof item.script === 'string') {\n    return 'script';\n  }\n\n  return 'unknown';\n};\n\nexport const fromOpenCollectionItem = (item: unknown, parseFolder: (folder: unknown) => BrunoItem): BrunoItem | null => {\n  const ocItem = item as OCItem;\n  const itemType = getItemType(ocItem);\n\n  switch (itemType) {\n    case 'http':\n      return fromOpenCollectionHttpItem(item as Parameters<typeof fromOpenCollectionHttpItem>[0]);\n    case 'graphql':\n      return fromOpenCollectionGraphqlItem(item as Parameters<typeof fromOpenCollectionGraphqlItem>[0]);\n    case 'grpc':\n      return fromOpenCollectionGrpcItem(item as Parameters<typeof fromOpenCollectionGrpcItem>[0]);\n    case 'websocket':\n      return fromOpenCollectionWebsocketItem(item as Parameters<typeof fromOpenCollectionWebsocketItem>[0]);\n    case 'folder':\n      return parseFolder(item);\n    case 'script': {\n      const scriptItem = item as { script?: string; info?: { name?: string } };\n      return {\n        uid: uuid(),\n        type: 'js',\n        name: scriptItem.info?.name || 'script.js',\n        fileContent: scriptItem.script || ''\n      };\n    }\n    default:\n      return null;\n  }\n};\n\nexport const toOpenCollectionItem = (item: BrunoItem, stringifyFolder: (folder: BrunoItem) => unknown): unknown | null => {\n  switch (item.type) {\n    case 'http-request':\n      return toOpenCollectionHttpItem(item);\n    case 'graphql-request':\n      return toOpenCollectionGraphqlItem(item);\n    case 'grpc-request':\n      return toOpenCollectionGrpcItem(item);\n    case 'ws-request':\n      return toOpenCollectionWebsocketItem(item);\n    case 'folder':\n      return stringifyFolder(item);\n    case 'js':\n      return {\n        info: {\n          name: item.name || 'script.js',\n          type: 'script'\n        },\n        script: item.fileContent || ''\n      };\n    default:\n      return null;\n  }\n};\n\nexport const fromOpenCollectionItems = (items: unknown[] | undefined, parseFolder: (folder: unknown) => BrunoItem): BrunoItem[] => {\n  return (items || [])\n    .map((item) => fromOpenCollectionItem(item, parseFolder))\n    .filter((item): item is BrunoItem => item !== null);\n};\n\nexport const toOpenCollectionItems = (items: BrunoItem[] | undefined | null, stringifyFolder: (folder: BrunoItem) => unknown): unknown[] => {\n  return (items || [])\n    .map((item) => toOpenCollectionItem(item, stringifyFolder))\n    .filter((item): item is unknown => item !== null);\n};\n\nexport { fromOpenCollectionHttpItem, toOpenCollectionHttpItem } from './http';\nexport { fromOpenCollectionGraphqlItem, toOpenCollectionGraphqlItem } from './graphql';\nexport { fromOpenCollectionGrpcItem, toOpenCollectionGrpcItem } from './grpc';\nexport { fromOpenCollectionWebsocketItem, toOpenCollectionWebsocketItem } from './websocket';\n"
  },
  {
    "path": "packages/bruno-converters/src/opencollection/items/websocket.ts",
    "content": "import { uuid } from '../../common/index.js';\nimport {\n  fromOpenCollectionHeaders,\n  toOpenCollectionHeaders,\n  fromOpenCollectionAuth,\n  toOpenCollectionAuth,\n  fromOpenCollectionScripts,\n  toOpenCollectionScripts,\n  fromOpenCollectionVariables,\n  toOpenCollectionVariables,\n  fromOpenCollectionActions,\n  toOpenCollectionActions\n} from '../common';\nimport type {\n  WebSocketRequest,\n  WebSocketRequestInfo,\n  WebSocketRequestDetails,\n  WebSocketRequestRuntime,\n  WebSocketMessage,\n  WebSocketMessageVariant,\n  Auth,\n  BrunoItem,\n  BrunoWsMessage,\n  BrunoKeyValue,\n  BrunoWebSocketRequestBody\n} from '../types';\n\nexport const fromOpenCollectionWebsocketItem = (item: WebSocketRequest): BrunoItem => {\n  const info = item.info || {};\n  const websocket = item.websocket || {};\n  const runtime = item.runtime || {};\n\n  const wsMessages: BrunoWsMessage[] = [];\n\n  if (websocket.message) {\n    if ('type' in websocket.message && 'data' in websocket.message) {\n      const msg = websocket.message as WebSocketMessage;\n      wsMessages.push({\n        name: 'message 1',\n        type: msg.type || 'json',\n        content: msg.data || ''\n      });\n    } else if (Array.isArray(websocket.message)) {\n      websocket.message.forEach((m, index) => {\n        wsMessages.push({\n          name: m.title || `message ${index + 1}`,\n          type: m.message?.type || 'json',\n          content: m.message?.data || ''\n        });\n      });\n    }\n  }\n\n  const scripts = fromOpenCollectionScripts(runtime.scripts);\n\n  // variables (pre-request from variables, post-response from actions)\n  const variables = fromOpenCollectionVariables(runtime.variables);\n  const postResponseVars = fromOpenCollectionActions((runtime as { actions?: Parameters<typeof fromOpenCollectionActions>[0] }).actions);\n\n  const wsBody: BrunoWebSocketRequestBody = {\n    mode: 'ws',\n    ws: wsMessages\n  };\n\n  const brunoItem: BrunoItem = {\n    uid: uuid(),\n    type: 'ws-request',\n    name: info.name || 'Untitled Request',\n    seq: info.seq || 1,\n    request: {\n      url: websocket.url || '',\n      headers: fromOpenCollectionHeaders(websocket.headers),\n      body: wsBody,\n      auth: fromOpenCollectionAuth(websocket.auth as Auth),\n      script: scripts?.script,\n      vars: {\n        req: variables.req,\n        res: postResponseVars\n      },\n      tests: scripts?.tests,\n      docs: item.docs || ''\n    }\n  };\n\n  if (info.tags?.length) {\n    brunoItem.tags = info.tags;\n  }\n\n  return brunoItem;\n};\n\nexport const toOpenCollectionWebsocketItem = (item: BrunoItem): WebSocketRequest => {\n  const request = (item.request || {}) as Record<string, unknown>;\n\n  const info: WebSocketRequestInfo = {\n    name: item.name || 'Untitled Request',\n    type: 'websocket'\n  };\n\n  if (item.seq) {\n    info.seq = item.seq;\n  }\n\n  if (item.tags?.length) {\n    info.tags = item.tags;\n  }\n\n  const websocket: WebSocketRequestDetails = {\n    url: request.url as string || ''\n  };\n\n  const headers = toOpenCollectionHeaders(request.headers as BrunoKeyValue[]);\n  if (headers) {\n    websocket.headers = headers;\n  }\n\n  const body = request.body as { ws?: BrunoWsMessage[] } | undefined;\n  if (body?.ws?.length) {\n    const messages = body.ws;\n    if (messages.length === 1) {\n      websocket.message = {\n        type: (messages[0].type as WebSocketMessage['type']) || 'json',\n        data: messages[0].content || ''\n      };\n    } else {\n      websocket.message = messages.map((msg): WebSocketMessageVariant => ({\n        title: msg.name || 'Untitled',\n        message: {\n          type: (msg.type as WebSocketMessage['type']) || 'json',\n          data: msg.content || ''\n        }\n      }));\n    }\n  }\n\n  // auth\n  const auth = toOpenCollectionAuth(request.auth as Parameters<typeof toOpenCollectionAuth>[0]);\n  if (auth) {\n    websocket.auth = auth;\n  }\n\n  const ocRequest: WebSocketRequest = {\n    info,\n    websocket\n  };\n\n  const scripts = toOpenCollectionScripts(request as Parameters<typeof toOpenCollectionScripts>[0]);\n  const variables = toOpenCollectionVariables(request.vars as Parameters<typeof toOpenCollectionVariables>[0]);\n\n  // actions (from post-response variables)\n  const vars = request.vars as { req?: unknown[]; res?: unknown[] } | undefined;\n  const actions = toOpenCollectionActions(vars?.res as Parameters<typeof toOpenCollectionActions>[0]);\n\n  if (scripts || variables || actions) {\n    const runtime: WebSocketRequestRuntime = {};\n\n    if (scripts) {\n      runtime.scripts = scripts;\n    }\n\n    if (variables) {\n      runtime.variables = variables;\n    }\n\n    if (actions) {\n      (runtime as { actions?: typeof actions }).actions = actions;\n    }\n\n    ocRequest.runtime = runtime;\n  }\n\n  if (request.docs) {\n    ocRequest.docs = request.docs as string;\n  }\n\n  return ocRequest;\n};\n"
  },
  {
    "path": "packages/bruno-converters/src/opencollection/opencollection-to-bruno.ts",
    "content": "import { OpenCollection } from \"@opencollection/types\";\nimport { BrunoCollection, BrunoCollectionRoot, BrunoConfig, PemCertificate, Pkcs12Certificate } from \"./types\";\nimport { fromOpenCollectionAuth, fromOpenCollectionHeaders, fromOpenCollectionScripts, fromOpenCollectionVariables } from \"./common\";\nimport { uuid } from \"../common\";\nimport { fromOpenCollectionItems } from \"./items\";\nimport { fromOpenCollectionFolder } from \"./folder\";\nimport { fromOpenCollectionEnvironments } from \"./environment\";\n\nconst fromOpenCollectionConfig = (oc: OpenCollection): BrunoConfig => {\n  const brunoExtension = oc.extensions?.bruno as {\n    ignore?: string[];\n    presets?: {\n      requestType?: string;\n      requestUrl?: string;\n    };\n  } | undefined;\n\n  const ignoreList = brunoExtension && Array.isArray(brunoExtension.ignore)\n    ? brunoExtension.ignore\n    : ['node_modules', '.git'];\n\n  const brunoConfig: BrunoConfig = {\n    version: '1',\n    name: oc.info?.name || 'Untitled Collection',\n    type: 'collection',\n    ignore: ignoreList\n  };\n\n  if (brunoExtension?.presets?.requestType || brunoExtension?.presets?.requestUrl) {\n    brunoConfig.presets = {};\n    if (brunoExtension.presets.requestType) {\n      brunoConfig.presets.requestType = brunoExtension.presets.requestType;\n    }\n    if (brunoExtension.presets.requestUrl) {\n      brunoConfig.presets.requestUrl = brunoExtension.presets.requestUrl;\n    }\n  }\n\n  const config = oc.config;\n  if (!config) {\n    return brunoConfig;\n  }\n\n  if (config.protobuf) {\n    brunoConfig.protobuf = {\n      protoFiles: config.protobuf.protoFiles?.map((f) => ({\n        path: f.path\n      })),\n      importPaths: config.protobuf.importPaths?.map((p) => ({\n        path: p.path,\n        enabled: p.disabled !== true\n      }))\n    };\n  }\n\n  if (config.proxy) {\n    brunoConfig.proxy = {\n      disabled: config.proxy.disabled,\n      inherit: config.proxy.inherit,\n      config: config.proxy.config\n    };\n  }\n\n  if (config.clientCertificates?.length) {\n    brunoConfig.clientCertificates = {\n      certs: config.clientCertificates.map((cert) => {\n        if (cert.type === 'pem') {\n          const pemCert = cert as PemCertificate;\n          return {\n            domain: pemCert.domain || '',\n            type: 'pem' as const,\n            certFilePath: pemCert.certificateFilePath || '',\n            keyFilePath: pemCert.privateKeyFilePath || '',\n            passphrase: pemCert.passphrase || ''\n          };\n        } else if (cert.type === 'pkcs12') {\n          const pkcs12Cert = cert as Pkcs12Certificate;\n          return {\n            domain: pkcs12Cert.domain || '',\n            type: 'pkcs12' as const,\n            pfxFilePath: pkcs12Cert.pkcs12FilePath || '',\n            passphrase: pkcs12Cert.passphrase || ''\n          };\n        }\n        return null;\n      }).filter((cert): cert is NonNullable<typeof cert> => cert !== null)\n    };\n  }\n\n  return brunoConfig;\n};\n\nconst fromOpenCollectionRoot = (oc: OpenCollection): BrunoCollectionRoot => {\n  const root: BrunoCollectionRoot = {};\n\n  if (oc.request) {\n    const scripts = fromOpenCollectionScripts(oc.request.scripts);\n    root.request = {\n      headers: fromOpenCollectionHeaders(oc.request.headers),\n      auth: fromOpenCollectionAuth(oc.request.auth),\n      script: scripts?.script,\n      vars: fromOpenCollectionVariables(oc.request.variables),\n      tests: scripts?.tests\n    };\n  }\n\n  if (oc.docs) {\n    root.docs = typeof oc.docs === 'string'\n      ? oc.docs\n      : oc.docs.content || '';\n  }\n\n  root.meta = {\n    name: oc.info?.name || 'Untitled Collection'\n  };\n\n  return root;\n};\n\nexport const openCollectionToBruno = (openCollection: OpenCollection): BrunoCollection => {\n  const brunoCollection: BrunoCollection = {\n    uid: uuid(),\n    name: openCollection.info?.name || 'Untitled Collection',\n    version: '1',\n    items: fromOpenCollectionItems(openCollection.items, (folder: unknown) => fromOpenCollectionFolder(folder as Parameters<typeof fromOpenCollectionFolder>[0])),\n    environments: fromOpenCollectionEnvironments(openCollection.config?.environments),\n    brunoConfig: fromOpenCollectionConfig(openCollection) as Record<string, unknown>,\n    root: fromOpenCollectionRoot(openCollection)\n  };\n\n  return brunoCollection;\n};"
  },
  {
    "path": "packages/bruno-converters/src/opencollection/types.ts",
    "content": "// OpenCollection types - main module\nexport type { OpenCollection, Extensions } from '@opencollection/types';\n\n// OpenCollection collection/item types\nexport type { Item, Folder, FolderInfo } from '@opencollection/types/collection/item';\n\n// OpenCollection HTTP request types\nexport type {\n  HttpRequest,\n  HttpRequestHeader,\n  HttpResponseHeader,\n  HttpRequestParam,\n  HttpRequestBody,\n  HttpRequestSettings,\n  HttpRequestExample,\n  HttpRequestExampleRequest,\n  HttpRequestExampleResponse,\n  HttpRequestExampleResponseBody,\n  HttpRequestBodyVariant,\n  HttpRequestInfo,\n  HttpRequestDetails,\n  HttpRequestRuntime,\n  RawBody,\n  FormUrlEncodedBody,\n  FormUrlEncodedEntry,\n  MultipartFormBody,\n  MultipartFormEntry,\n  FileBody,\n  FileBodyVariant\n} from '@opencollection/types/requests/http';\n\n// OpenCollection GraphQL request types\nexport type {\n  GraphQLRequest,\n  GraphQLRequestSettings,\n  GraphQLRequestInfo,\n  GraphQLRequestDetails,\n  GraphQLRequestRuntime,\n  GraphQLBody,\n  GraphQLBodyVariant\n} from '@opencollection/types/requests/graphql';\n\n// OpenCollection gRPC request types\nexport type {\n  GrpcRequest,\n  GrpcRequestInfo,\n  GrpcRequestDetails,\n  GrpcRequestRuntime,\n  GrpcMetadata,\n  GrpcMessage,\n  GrpcMessageVariant,\n  GrpcMessagePayload,\n  GrpcMethodType\n} from '@opencollection/types/requests/grpc';\n\n// OpenCollection WebSocket request types\nexport type {\n  WebSocketRequest,\n  WebSocketRequestInfo,\n  WebSocketRequestDetails,\n  WebSocketRequestRuntime,\n  WebSocketMessage,\n  WebSocketMessageVariant,\n  WebSocketPayload,\n  WebSocketMessageType\n} from '@opencollection/types/requests/websocket';\n\n// OpenCollection config types\nexport type { Environment } from '@opencollection/types/config/environments';\nexport type { CollectionConfig } from '@opencollection/types/config/collection';\nexport type { Protobuf, ProtoFileItem, ProtoFileImportPath } from '@opencollection/types/config/protobuf';\nexport type { Proxy, ProxyConnectionConfig, ProxyConnectionAuth } from '@opencollection/types/config/proxy';\nexport type { ClientCertificate, PemCertificate, Pkcs12Certificate } from '@opencollection/types/config/certificates';\n\n// OpenCollection common types\nexport type { RequestDefaults, RequestSettings } from '@opencollection/types/common/request-defaults';\nexport type { Documentation } from '@opencollection/types/common/documentation';\nexport type { Description } from '@opencollection/types/common/description';\nexport type { Info, Author } from '@opencollection/types/common/info';\nexport type {\n  Variable,\n  VariableValueVariant\n} from '@opencollection/types/common/variables';\nexport type { Scripts } from '@opencollection/types/common/scripts';\nexport type { Assertion } from '@opencollection/types/common/assertions';\nexport type { Tag } from '@opencollection/types/common/tags';\nexport type {\n  Action,\n  ActionSetVariable,\n  ActionPhase,\n  ActionVariableScope,\n  SetVariableActionSelector,\n  SetVariableActionTarget\n} from '@opencollection/types/common/actions';\n\n// OpenCollection auth types\nexport type {\n  Auth,\n  AuthBasic,\n  AuthBearer,\n  AuthDigest,\n  AuthNTLM,\n  AuthAwsV4,\n  AuthApiKey,\n  AuthWsse\n} from '@opencollection/types/common/auth';\n\nexport type { AuthOAuth2 } from '@opencollection/types/common/auth-oauth2';\n\n// Bruno types - collection\nexport type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';\nexport type {\n  FolderRoot as BrunoFolderRoot,\n  FolderRequest as BrunoFolderRequest,\n  FolderMeta as BrunoFolderMeta\n} from '@usebruno/schema-types/collection/folder';\nexport type { Collection as BrunoCollection } from '@usebruno/schema-types/collection/collection';\nexport type {\n  Environment as BrunoEnvironment,\n  EnvironmentVariable as BrunoEnvironmentVariable\n} from '@usebruno/schema-types/collection/environment';\nexport type {\n  Example as BrunoExample,\n  ExampleRequest as BrunoExampleRequest,\n  ExampleResponse as BrunoExampleResponse,\n  ExampleResponseBody as BrunoExampleResponseBody\n} from '@usebruno/schema-types/collection/examples';\n\n// Bruno types - common\nexport type { KeyValue as BrunoKeyValue } from '@usebruno/schema-types/common/key-value';\nexport type { Variable as BrunoVariable, Variables as BrunoVariables } from '@usebruno/schema-types/common/variables';\nexport type { Script as BrunoScript } from '@usebruno/schema-types/common/scripts';\nexport type {\n  Auth as BrunoAuth,\n  AuthMode as BrunoAuthMode,\n  AuthAwsV4 as BrunoAuthAwsV4,\n  AuthBasic as BrunoAuthBasic,\n  AuthBearer as BrunoAuthBearer,\n  AuthDigest as BrunoAuthDigest,\n  AuthNTLM as BrunoAuthNTLM,\n  AuthWsse as BrunoAuthWsse,\n  AuthApiKey as BrunoAuthApiKey,\n  OAuth2 as BrunoOAuth2\n} from '@usebruno/schema-types/common/auth';\nexport type { MultipartFormEntry as BrunoMultipartFormEntry, MultipartForm as BrunoMultipartForm } from '@usebruno/schema-types/common/multipart-form';\nexport type { FileEntry as BrunoFileEntry, FileList as BrunoFileList } from '@usebruno/schema-types/common/file';\nexport type { GraphqlBody as BrunoGraphqlBody } from '@usebruno/schema-types/common/graphql';\n\n// Bruno types - requests\nexport type {\n  HttpRequest as BrunoHttpRequest,\n  HttpRequestBody as BrunoHttpRequestBody,\n  HttpRequestBodyMode as BrunoHttpRequestBodyMode,\n  HttpRequestParam as BrunoHttpRequestParam,\n  HttpRequestParamType as BrunoHttpRequestParamType\n} from '@usebruno/schema-types/requests/http';\nexport type {\n  GrpcRequest as BrunoGrpcRequest,\n  GrpcRequestBody as BrunoGrpcRequestBody,\n  GrpcMessage as BrunoGrpcMessage,\n  GrpcMethodType as BrunoGrpcMethodType\n} from '@usebruno/schema-types/requests/grpc';\nexport type {\n  WebSocketRequest as BrunoWebSocketRequest,\n  WebSocketRequestBody as BrunoWebSocketRequestBody,\n  WebSocketMessage as BrunoWsMessage\n} from '@usebruno/schema-types/requests/websocket';\n\nexport interface BrunoConfig {\n  version?: string;\n  name?: string;\n  type?: string;\n  ignore?: string[];\n  presets?: {\n    requestType?: string;\n    requestUrl?: string;\n  };\n  protobuf?: {\n    protoFiles?: { path: string }[];\n    importPaths?: { path: string; enabled?: boolean }[];\n  };\n  proxy?: {\n    disabled?: boolean;\n    inherit?: boolean;\n    config?: {\n      protocol?: string;\n      hostname?: string;\n      port?: number;\n      auth?: {\n        disabled?: boolean;\n        username?: string;\n        password?: string;\n      };\n      bypassProxy?: string;\n    };\n  };\n  clientCertificates?: {\n    certs?: Array<{\n      domain?: string;\n      type?: 'pem' | 'pkcs12';\n      certFilePath?: string;\n      keyFilePath?: string;\n      pfxFilePath?: string;\n      passphrase?: string;\n    }>;\n  };\n  scripts?: {\n    additionalContextRoots?: string[];\n  };\n  openapi?: Array<{\n    sourceUrl: string;\n    groupBy?: 'tags' | 'path';\n    lastSyncDate?: string;\n    specHash?: string;\n    autoCheck?: boolean;\n    autoCheckInterval?: number;\n  }>;\n}\n\nexport interface BrunoCollectionRoot {\n  request?: any;\n  docs?: string;\n  meta?: {\n    name?: string;\n    seq?: number;\n  };\n}"
  },
  {
    "path": "packages/bruno-converters/src/postman/bruno-to-postman.js",
    "content": "import map from 'lodash/map';\nimport { deleteSecretsInEnvs, deleteUidsInEnvs, deleteUidsInItems, isItemARequest } from '../common';\nimport translateBruToPostman from '../utils/bruno-to-postman-translator';\n\n/**\n * Transforms a given URL string into an object representing the protocol, host, path, query, and variables.\n *\n * @param {string} url - The raw URL to be transformed.\n * @param {Object} params - The params object.\n * @returns {Object|null} An object containing the URL's protocol, host, path, query, and variables, or {} if an error occurs.\n */\nexport const transformUrl = (url, params) => {\n  if (typeof url !== 'string' || !url.trim()) {\n    url = '';\n    console.error('Invalid URL input:', url);\n  }\n\n  const urlRegexPatterns = {\n    protocolAndRestSeparator: /:\\/\\//,\n    hostAndPathSeparator: /\\/(.+)/,\n    domainSegmentSeparator: /\\./,\n    pathSegmentSeparator: /\\//,\n    queryStringSeparator: /\\?/\n  };\n\n  const postmanUrl = { raw: url };\n\n  /**\n   * Splits a URL into its protocol, host and path.\n   *\n   * @param {string} url - The URL to be split.\n   * @returns {Object} An object containing the protocol and the raw host/path string.\n   */\n  const splitUrl = (url) => {\n    const urlParts = url.split(urlRegexPatterns.protocolAndRestSeparator);\n    if (urlParts.length === 1) {\n      return { protocol: '', rawHostAndPath: urlParts[0] };\n    } else if (urlParts.length === 2) {\n      const [hostAndPath, _] = urlParts[1].split(urlRegexPatterns.queryStringSeparator);\n      return { protocol: urlParts[0], rawHostAndPath: hostAndPath };\n    } else {\n      throw new Error(`Invalid URL format: ${url}`);\n    }\n  };\n\n  /**\n   * Splits the host and path from a raw host/path string.\n   *\n   * @param {string} rawHostAndPath - The raw host and path string to be split.\n   * @returns {Object} An object containing the host and path.\n   */\n  const splitHostAndPath = (rawHostAndPath) => {\n    const [host, path = ''] = rawHostAndPath.split(urlRegexPatterns.hostAndPathSeparator);\n    return { host, path };\n  };\n\n  try {\n    const { protocol, rawHostAndPath } = splitUrl(url);\n    postmanUrl.protocol = protocol;\n\n    const { host, path } = splitHostAndPath(rawHostAndPath);\n    postmanUrl.host = host ? host.split(urlRegexPatterns.domainSegmentSeparator) : [];\n    postmanUrl.path = path ? path.split(urlRegexPatterns.pathSegmentSeparator) : [];\n  } catch (error) {\n    console.error(error.message);\n    return {};\n  }\n\n  // Construct query params.\n  postmanUrl.query = params\n    .filter((param) => param.type === 'query')\n    .map(({ name, value, description }) => ({ key: name, value, description }));\n\n  // Construct path params.\n  postmanUrl.variable = params\n    .filter((param) => param.type === 'path')\n    .map(({ name, value, description }) => ({ key: name, value, description }));\n\n  return postmanUrl;\n};\n\n/**\n * Collapses multiple consecutive slashes (`//`) into a single slash, while skipping the protocol (e.g., `http://` or `https://`).\n *\n * @param {String} url - A URL string\n * @returns {String} The sanitized URL\n *\n */\nconst collapseDuplicateSlashes = (url) => {\n  return url.replace(/(?<!:)\\/{2,}/g, '/');\n};\n\n/**\n * Replaces all `\\\\` (backslashes) with `//` (forward slashes) and collapses multiple slashes into one.\n *\n * @param {string} url - The URL to sanitize.\n * @returns {string} The sanitized URL.\n *\n */\nexport const sanitizeUrl = (url) => {\n  let sanitizedUrl = collapseDuplicateSlashes(url.replace(/\\\\/g, '//'));\n  return sanitizedUrl;\n};\n\nexport const brunoToPostman = (collection) => {\n  delete collection.uid;\n  delete collection.processEnvVariables;\n  deleteUidsInItems(collection.items);\n  deleteUidsInEnvs(collection.environments);\n  deleteSecretsInEnvs(collection.environments);\n\n  const generateInfoSection = () => {\n    return {\n      name: collection.name,\n      description: collection.root?.docs,\n      schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n    };\n  };\n\n  const generateCollectionVars = (collection) => {\n    const pattern = /{{[^{}]+}}/g;\n    let collectionVars = [];\n\n    const findOccurrences = (obj, results) => {\n      if (typeof obj === 'object') {\n        if (Array.isArray(obj)) {\n          obj.forEach((item) => findOccurrences(item, results));\n        } else {\n          for (const key in obj) {\n            findOccurrences(obj[key], results);\n          }\n        }\n      } else if (typeof obj === 'string') {\n        obj.replace(pattern, (match) => {\n          const varKey = match.replace(/{{|}}/g, '');\n          results.push({\n            key: varKey,\n            value: '',\n            type: 'default'\n          });\n        });\n      }\n    };\n\n    findOccurrences(collection, collectionVars);\n\n    // Add request and response vars\n    let reqVars = (collection.root?.request?.vars?.req || []).map((v) => ({\n      key: v.name,\n      value: v.value,\n      type: 'default'\n    }));\n\n    let resVars = (collection.root?.request?.vars?.res || []).map((v) => ({\n      key: v.name,\n      value: v.value,\n      type: 'default'\n    }));\n\n    // Merge and deduplicate final result\n    const allVars = [...reqVars, ...resVars, ...collectionVars];\n    const finalVarsMap = new Map();\n    allVars.forEach((v) => {\n      if (!finalVarsMap.has(v.key)) {\n        finalVarsMap.set(v.key, v);\n      }\n    });\n\n    return Array.from(finalVarsMap.values());\n  };\n  const translateScriptSafely = (script = '') => {\n    try {\n      return translateBruToPostman(script);\n    } catch (err) {\n      console.warn('Bru→Postman script translation failed, leaving script as-is', err);\n      return script;\n    }\n  };\n\n  const generateEventSection = (item) => {\n    const eventArray = [];\n    // Request: item.script, Folder: item.root.request.script, Collection: item.request.script\n    // Tests: item.tests, Folder: item.root.request.tests, Collection: item.request.tests\n    const scriptBlock = item?.script || item?.root?.request?.script || item?.request?.script || {};\n    const testsBlock = item?.tests || item?.root?.request?.tests || item?.request?.tests;\n\n    if (scriptBlock.req && typeof scriptBlock.req === 'string') {\n      const translated = translateScriptSafely(scriptBlock.req);\n      eventArray.push({\n        listen: 'prerequest',\n        script: {\n          type: 'text/javascript',\n          packages: {},\n          requests: {},\n          exec: translated.split('\\n')\n        }\n      });\n    }\n    // testsBlock is added in the post response script since postman only supports tests in the post response script\n    if (scriptBlock.res || testsBlock) {\n      const exec = [];\n      if (scriptBlock.res && typeof scriptBlock.res === 'string') {\n        const translated = translateScriptSafely(scriptBlock.res);\n        exec.push(...translated.split('\\n'));\n      }\n      if (testsBlock && typeof testsBlock === 'string') {\n        const translatedTests = translateScriptSafely(testsBlock);\n        if (exec.length > 0) {\n          exec.push('');\n        }\n        exec.push('// Tests');\n        exec.push(...translatedTests.split('\\n'));\n      }\n\n      // Only push the event if exec has content\n      if (exec.length > 0) {\n        eventArray.push({\n          listen: 'test',\n          script: {\n            type: 'text/javascript',\n            packages: {},\n            requests: {},\n            exec: exec\n          }\n        });\n      }\n    }\n    return eventArray;\n  };\n\n  const generateHeaders = (headersArray) => {\n    if (!headersArray || !Array.isArray(headersArray)) {\n      return [];\n    }\n    return map(headersArray, (item) => {\n      return {\n        key: item.name || '',\n        value: item.value || '',\n        disabled: !item.enabled,\n        type: 'default'\n      };\n    });\n  };\n\n  const generateBody = (body) => {\n    if (!body || !body.mode) {\n      return {\n        mode: 'raw',\n        raw: ''\n      };\n    }\n\n    switch (body.mode) {\n      case 'formUrlEncoded':\n        return {\n          mode: 'urlencoded',\n          urlencoded: map(body.formUrlEncoded || [], (bodyItem) => {\n            return {\n              key: bodyItem.name || '',\n              value: bodyItem.value || '',\n              disabled: !bodyItem.enabled,\n              type: 'default'\n            };\n          })\n        };\n      case 'multipartForm':\n        return {\n          mode: 'formdata',\n          formdata: map(body.multipartForm || [], (bodyItem) => {\n            const isFile = bodyItem.type === 'file';\n\n            const getSrc = () => {\n              if (!bodyItem.value) return null;\n              if (Array.isArray(bodyItem.value)) {\n                if (bodyItem.value.length === 0) return null;\n                if (bodyItem.value.length === 1) return bodyItem.value[0];\n                return bodyItem.value;\n              }\n              return bodyItem.value;\n            };\n            return {\n              key: bodyItem.name || '',\n              disabled: !bodyItem.enabled,\n              type: isFile ? 'file' : 'text',\n              ...(isFile ? { src: getSrc() } : { value: bodyItem.value || '' }),\n              ...(bodyItem.contentType && { contentType: bodyItem.contentType })\n            };\n          })\n        };\n      case 'json':\n        return {\n          mode: 'raw',\n          raw: body.json || '',\n          options: {\n            raw: {\n              language: 'json'\n            }\n          }\n        };\n      case 'xml':\n        return {\n          mode: 'raw',\n          raw: body.xml || '',\n          options: {\n            raw: {\n              language: 'xml'\n            }\n          }\n        };\n      case 'text':\n        return {\n          mode: 'raw',\n          raw: body.text || '',\n          options: {\n            raw: {\n              language: 'text'\n            }\n          }\n        };\n      case 'graphql':\n        return {\n          mode: 'graphql',\n          graphql: body.graphql || {}\n        };\n      default:\n        return {\n          mode: 'raw',\n          raw: ''\n        };\n    }\n  };\n\n  const generateAuth = (itemAuth) => {\n    switch (itemAuth?.mode) {\n      case 'bearer':\n        return {\n          type: 'bearer',\n          bearer: {\n            key: 'token',\n            value: itemAuth.bearer?.token || '',\n            type: 'string'\n          }\n        };\n      case 'basic': {\n        return {\n          type: 'basic',\n          basic: [\n            {\n              key: 'password',\n              value: itemAuth.basic?.password || '',\n              type: 'string'\n            },\n            {\n              key: 'username',\n              value: itemAuth.basic?.username || '',\n              type: 'string'\n            }\n          ]\n        };\n      }\n      case 'apikey': {\n        return {\n          type: 'apikey',\n          apikey: [\n            {\n              key: 'key',\n              value: itemAuth.apikey?.key || '',\n              type: 'string'\n            },\n            {\n              key: 'value',\n              value: itemAuth.apikey?.value || '',\n              type: 'string'\n            }\n          ]\n        };\n      }\n      default: {\n        return {\n          type: 'noauth'\n        };\n      }\n    }\n  };\n\n  const generateRequestSection = (itemRequest) => {\n    if (!itemRequest) {\n      return {};\n    }\n\n    const requestObject = {\n      method: itemRequest.method || 'GET',\n      header: generateHeaders(itemRequest.headers),\n      auth: generateAuth(itemRequest.auth),\n      description: itemRequest.docs || '',\n      // We sanitize the URL to make sure it's in the right format before passing it to the transformUrl func. This means changing backslashes to forward slashes and reducing multiple slashes to a single one, except in the protocol part.\n      url: transformUrl(sanitizeUrl(itemRequest.url || ''), itemRequest.params || [])\n    };\n\n    if (itemRequest.body && itemRequest.body.mode !== 'none') {\n      requestObject.body = generateBody(itemRequest.body);\n    }\n    return requestObject;\n  };\n\n  const generateResponseExamples = (examples) => {\n    if (!examples || !Array.isArray(examples)) {\n      return [];\n    }\n\n    return map(examples, (example) => {\n      if (!example) {\n        return null;\n      }\n\n      const postmanResponse = {\n        name: example.name || 'Example Response',\n        originalRequest: generateOriginalRequest(example.request),\n        status: example.response?.statusText || 'OK',\n        code: parseInt(example.response?.status) || 200,\n        header: generateResponseHeaders(example.response?.headers),\n        cookie: [],\n        body: example.response?.body?.content || ''\n      };\n\n      // Add preview language based on content type\n      const contentType = getContentTypeFromHeaders(example.response?.headers);\n      if (contentType) {\n        if (contentType.includes('application/json')) {\n          postmanResponse._postman_previewlanguage = 'json';\n        } else if (contentType.includes('application/xml') || contentType.includes('text/xml')) {\n          postmanResponse._postman_previewlanguage = 'xml';\n        } else if (contentType.includes('text/html')) {\n          postmanResponse._postman_previewlanguage = 'html';\n        } else if (contentType.includes('text/plain')) {\n          postmanResponse._postman_previewlanguage = 'text';\n        }\n      }\n\n      return postmanResponse;\n    }).filter(Boolean); // Remove null entries\n  };\n\n  const generateOriginalRequest = (request) => {\n    if (!request) {\n      return {\n        method: 'GET',\n        header: [],\n        url: { raw: '', protocol: 'https', host: [], path: [] }\n      };\n    }\n\n    const originalRequestObject = {\n      method: request.method || 'GET',\n      header: generateHeaders(request.headers),\n      // We sanitize the URL to make sure it's in the right format before passing it to the transformUrl func. This means changing backslashes to forward slashes and reducing multiple slashes to a single one, except in the protocol part.\n      url: transformUrl(sanitizeUrl(request.url || ''), request.params || [])\n    };\n\n    // Add body if it exists and is not 'none' mode\n    if (request.body && request.body.mode !== 'none') {\n      originalRequestObject.body = generateBody(request.body);\n    }\n\n    return originalRequestObject;\n  };\n\n  const generateResponseHeaders = (headers) => {\n    if (!headers || !Array.isArray(headers)) {\n      return [];\n    }\n\n    return map(headers, (header) => {\n      return {\n        key: header.name || '',\n        value: header.value || '',\n        name: header.name || '',\n        description: header.description || '',\n        type: 'text'\n      };\n    });\n  };\n\n  const getContentTypeFromHeaders = (headers) => {\n    if (!headers || !Array.isArray(headers)) {\n      return null;\n    }\n\n    const contentTypeHeader = headers.find((header) =>\n      header.name && header.name.toLowerCase() === 'content-type');\n\n    return contentTypeHeader ? contentTypeHeader.value : null;\n  };\n\n  const generateItemSection = (itemsArray) => {\n    if (!itemsArray || !Array.isArray(itemsArray)) {\n      return [];\n    }\n\n    return map(itemsArray, (item) => {\n      if (!item) {\n        return null;\n      }\n\n      if (item.type === 'grpc-request') {\n        return null;\n      }\n\n      if (item.type === 'folder') {\n        const folderEvents = generateEventSection(item);\n        return {\n          name: item.name || 'Untitled Folder',\n          item: generateItemSection(item.items),\n          ...(folderEvents.length ? { event: folderEvents } : {})\n        };\n      } else if (isItemARequest(item)) {\n        const requestEvents = generateEventSection(item.request);\n        const method = (item.request?.method || 'GET').toUpperCase();\n        const hasBody = item.request?.body && item.request.body.mode !== 'none';\n\n        const methodsWithoutBody = ['GET', 'HEAD', 'OPTIONS'];\n        const needsBodyPruningDisabled = hasBody && methodsWithoutBody.includes(method);\n\n        const postmanItem = {\n          name: item.name || 'Untitled Request',\n          ...(needsBodyPruningDisabled ? { protocolProfileBehavior: { disableBodyPruning: true } } : {}),\n          request: generateRequestSection(item.request),\n          ...(requestEvents.length ? { event: requestEvents } : {})\n        };\n\n        // Add examples (responses) if they exist\n        if (item.examples && Array.isArray(item.examples) && item.examples.length > 0) {\n          postmanItem.response = generateResponseExamples(item.examples);\n        }\n\n        return postmanItem;\n      }\n      return null;\n    }).filter(Boolean);\n  };\n  const collectionToExport = {};\n  collectionToExport.info = generateInfoSection();\n  collectionToExport.item = generateItemSection(collection.items);\n  collectionToExport.variable = generateCollectionVars(collection);\n  const collectionEvents = generateEventSection(collection.root);\n  if (collectionEvents.length) {\n    collectionToExport.event = collectionEvents;\n  }\n  return collectionToExport;\n};\n\nexport default brunoToPostman;\n"
  },
  {
    "path": "packages/bruno-converters/src/postman/postman-env-to-bruno-env.js",
    "content": "import each from 'lodash/each';\nimport { invalidVariableCharacterRegex } from '../constants';\nimport { uuid } from '../common';\n\nconst isSecret = (type) => {\n  return type === 'secret';\n};\n\nconst importPostmanEnvironmentVariables = (brunoEnvironment, values = []) => {\n  brunoEnvironment.variables = brunoEnvironment.variables || [];\n\n  each(values.filter((i) => !(i.key == null && i.value == null)), (i) => {\n    const brunoEnvironmentVariable = {\n      uid: uuid(),\n      name: (i.key ?? '').replace(invalidVariableCharacterRegex, '_'),\n      value: i.value ?? '',\n      enabled: i.enabled,\n      type: 'text',\n      secret: isSecret(i.type)\n    };\n\n    brunoEnvironment.variables.push(brunoEnvironmentVariable);\n  });\n};\n\nconst importPostmanEnvironment = (environment) => {\n  const brunoEnvironment = {\n    name: environment.name,\n    variables: []\n  };\n\n  importPostmanEnvironmentVariables(brunoEnvironment, environment.values);\n  return brunoEnvironment;\n};\n\nexport const postmanToBrunoEnvironment = (postmanEnvironment) => {\n  try {\n    return importPostmanEnvironment(postmanEnvironment);\n  } catch (err) {\n    console.log(err);\n    throw new Error('Unable to parse the postman environment json file');\n  }\n};\n\nexport default postmanToBrunoEnvironment;\n"
  },
  {
    "path": "packages/bruno-converters/src/postman/postman-to-bruno.js",
    "content": "import get from 'lodash/get';\nimport { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common';\nimport { transformExampleStatusInCollection } from '@usebruno/common';\nimport each from 'lodash/each';\nimport postmanTranslation from './postman-translations';\nimport { invalidVariableCharacterRegex } from '../constants/index';\n\nconst AUTH_TYPES = Object.freeze({\n  BASIC: 'basic',\n  BEARER: 'bearer',\n  AWSV4: 'awsv4',\n  APIKEY: 'apikey',\n  DIGEST: 'digest',\n  OAUTH2: 'oauth2',\n  NOAUTH: 'noauth',\n  NONE: 'none'\n});\n\nconst parseGraphQLRequest = (graphqlSource) => {\n  try {\n    let queryResultObject = {\n      query: '',\n      variables: ''\n    };\n\n    if (typeof graphqlSource === 'string') {\n      graphqlSource = JSON.parse(graphqlSource);\n    }\n\n    if (graphqlSource.hasOwnProperty('variables') && graphqlSource.variables !== '') {\n      queryResultObject.variables = graphqlSource.variables;\n    }\n\n    if (graphqlSource.hasOwnProperty('query') && graphqlSource.query !== '') {\n      queryResultObject.query = graphqlSource.query;\n    }\n\n    return queryResultObject;\n  } catch (e) {\n    return {\n      query: '',\n      variables: ''\n    };\n  }\n};\n\n/**\n * Transforms Postman descriptions to handle both legacy and new formats.\n *\n * Postman changed their description format:\n * - Legacy format: description was a simple string\n * - New format: description is an object with 'content' and 'type' properties\n *\n * This function handles both formats to ensure backward compatibility.\n */\nconst transformDescription = (description) => {\n  if (!description) {\n    return '';\n  }\n\n  if (typeof description === 'string') {\n    return description;\n  }\n\n  if (typeof description === 'object' && description.hasOwnProperty('content')) {\n    return description.content;\n  }\n\n  return '';\n};\n\nconst isItemAFolder = (item) => {\n  return !item.request;\n};\n\nconst convertV21Auth = (array) => {\n  return array.reduce((accumulator, currentValue) => {\n    accumulator[currentValue.key] = currentValue.value;\n    return accumulator;\n  }, {});\n};\n\nconst constructUrlFromParts = (url) => {\n  if (!url) return '';\n\n  const { protocol = 'http', host, path, port, query, hash } = url || {};\n  const hostStr = Array.isArray(host) ? host.filter(Boolean).join('.') : host || '';\n  const pathStr = Array.isArray(path) ? path.join('/') : path || '';\n  const portStr = port ? `:${port}` : '';\n  const queryStr\n    = query && Array.isArray(query) && query.length > 0\n      ? `?${query\n        .filter((q) => q && q.key)\n        .map((q) => `${q.key}=${q.value || ''}`)\n        .join('&')}`\n      : '';\n  const urlStr = `${protocol}://${hostStr}${portStr}${pathStr ? `/${pathStr}` : ''}${queryStr}`;\n  return urlStr;\n};\n\nconst constructUrl = (url) => {\n  if (!url) return '';\n\n  if (typeof url === 'string') {\n    return url;\n  }\n\n  if (typeof url === 'object') {\n    const { raw } = url;\n\n    if (raw && typeof raw === 'string') {\n      // If the raw URL contains url-fragments remove it\n      if (raw.includes('#')) {\n        return raw.split('#')[0]; // Returns the part of raw URL without the url-fragment part.\n      }\n      return raw;\n    }\n\n    // If no raw value exists, construct the URL from parts\n    return constructUrlFromParts(url);\n  }\n\n  return '';\n};\n\nconst importScriptsFromEvents = (events, requestObject) => {\n  events.forEach((event) => {\n    if (event.script && event.script.exec) {\n      if (event.listen === 'prerequest') {\n        if (!requestObject.script) {\n          requestObject.script = {};\n        }\n\n        if (event.script.exec && event.script.exec.length > 0) {\n          requestObject.script.req = postmanTranslation(event.script.exec);\n        } else {\n          requestObject.script.req = '';\n          console.warn('Unexpected event.script.exec type', typeof event.script.exec);\n        }\n      }\n\n      if (event.listen === 'test') {\n        if (!requestObject.script) {\n          requestObject.script = {};\n        }\n\n        if (event.script.exec && event.script.exec.length > 0) {\n          requestObject.script.res = postmanTranslation(event.script.exec);\n        } else {\n          requestObject.script.res = '';\n          console.warn('Unexpected event.script.exec type', typeof event.script.exec);\n        }\n      }\n    }\n  });\n};\n\nconst importCollectionLevelVariables = (variables, requestObject) => {\n  const vars = variables.filter((v) => !(v.key == null && v.value == null)).map((v) => ({\n    uid: uuid(),\n    name: (v.key ?? '').replace(invalidVariableCharacterRegex, '_'),\n    value: v.value == null ? '' : typeof v.value === 'string' ? v.value : JSON.stringify(v.value),\n    enabled: true\n  }));\n\n  requestObject.vars.req = vars;\n};\n\nexport const processAuth = (auth, requestObject, isCollection = false) => {\n  // As of 14/05/2025\n  // When collections are set to \"No Auth\" in Postman, the auth object is null.\n  // When folders and requests are set to \"Inherit\" in Postman, the auth object is null.\n  // When folders and requests are set to \"No Auth\" in Postman, the auth object is present.\n\n  // Handle collection-specific \"No Auth\"\n  if (isCollection && !auth) return; // Return as requestObject is a collection and has a default mode = none\n\n  // Handle folder/request specific \"Inherit\"\n  if (!auth) return; // Return as requestObject is a folder/request and has a default mode = inherit\n\n  // Handle folder/request specific \"No Auth\"\n  if (auth.type === AUTH_TYPES.NOAUTH) {\n    requestObject.auth.mode = AUTH_TYPES.NONE; // Set the mode to none\n    return; // No further processing needed\n  }\n\n  let authValues = auth[auth.type] ?? [];\n  if (Array.isArray(authValues)) {\n    authValues = convertV21Auth(authValues);\n  }\n\n  requestObject.auth.mode = auth.type; // Set the mode based on Postman's auth type\n\n  switch (auth.type) {\n    case AUTH_TYPES.BASIC:\n      requestObject.auth.basic = {\n        username: authValues.username || '',\n        password: authValues.password || ''\n      };\n      break;\n    case AUTH_TYPES.BEARER:\n      requestObject.auth.bearer = {\n        token: authValues.token || ''\n      };\n      break;\n    case AUTH_TYPES.AWSV4:\n      requestObject.auth.awsv4 = {\n        accessKeyId: authValues.accessKey || '',\n        secretAccessKey: authValues.secretKey || '',\n        sessionToken: authValues.sessionToken || '',\n        service: authValues.service || '',\n        region: authValues.region || '',\n        profileName: ''\n      };\n      break;\n    case AUTH_TYPES.APIKEY:\n      requestObject.auth.apikey = {\n        key: authValues.key || '',\n        value: authValues.value?.toString() || '', // Convert the value to a string as Postman's schema does not rigidly define the type of it,\n        placement: 'header' // By default we are placing the apikey values in headers!\n      };\n      break;\n    case AUTH_TYPES.DIGEST:\n      requestObject.auth.digest = {\n        username: authValues.username || '',\n        password: authValues.password || ''\n      };\n      break;\n    case AUTH_TYPES.OAUTH2:\n      const findValueUsingKey = (key) => authValues[key] || '';\n\n      // Maps Postman's grant_type to the Bruno's grantType string expected in the target object\n      const oauth2GrantTypeMaps = {\n        authorization_code_with_pkce: 'authorization_code',\n        authorization_code: 'authorization_code',\n        client_credentials: 'client_credentials',\n        password_credentials: 'password'\n      };\n\n      const postmanGrantType = findValueUsingKey('grant_type');\n      const targetGrantType = oauth2GrantTypeMaps[postmanGrantType] || 'client_credentials'; // Default\n\n      // Common properties for all OAuth2 grant types\n      const baseOAuth2Config = {\n        grantType: targetGrantType,\n        accessTokenUrl: findValueUsingKey('accessTokenUrl'),\n        refreshTokenUrl: findValueUsingKey('refreshTokenUrl'),\n        clientId: findValueUsingKey('clientId'),\n        clientSecret: findValueUsingKey('clientSecret'),\n        scope: findValueUsingKey('scope'),\n        state: findValueUsingKey('state'),\n        tokenPlacement: findValueUsingKey('addTokenTo') === 'header' ? 'header' : 'url',\n        credentialsPlacement: findValueUsingKey('client_authentication') === 'body' ? 'body' : 'basic_auth_header'\n      };\n\n      switch (postmanGrantType) {\n        case 'authorization_code':\n          requestObject.auth.oauth2 = {\n            ...baseOAuth2Config,\n            authorizationUrl: findValueUsingKey('authUrl'),\n            callbackUrl: findValueUsingKey('redirect_uri'),\n            pkce: false // PKCE is not used for standard authorization_code\n          };\n          break;\n        case 'authorization_code_with_pkce':\n          requestObject.auth.oauth2 = {\n            ...baseOAuth2Config,\n            authorizationUrl: findValueUsingKey('authUrl'),\n            callbackUrl: findValueUsingKey('redirect_uri'),\n            pkce: true // Explicitly set pkce to true for this grant type\n          };\n          break;\n        case 'password_credentials':\n          requestObject.auth.oauth2 = {\n            ...baseOAuth2Config,\n            username: findValueUsingKey('username'),\n            password: findValueUsingKey('password')\n          };\n          break;\n        case 'client_credentials':\n          requestObject.auth.oauth2 = baseOAuth2Config;\n          break;\n        default:\n          console.warn('Unexpected OAuth2 grant type after mapping:', targetGrantType);\n          requestObject.auth.oauth2 = baseOAuth2Config; // Fallback to default which is Client Credentials\n          break;\n      }\n      break;\n    default:\n      requestObject.auth.mode = AUTH_TYPES.NONE;\n      console.warn('Unexpected auth.type:', auth.type, '- Mode set, but no specific config generated.');\n      break;\n  }\n};\n\nconst importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false } = {}, scriptMap) => {\n  brunoParent.items = brunoParent.items || [];\n  const folderMap = {};\n  const requestMap = {};\n\n  item.forEach((i, index) => {\n    if (isItemAFolder(i)) {\n      const baseFolderName = i.name || 'Untitled Folder';\n      let folderName = baseFolderName;\n      let count = 1;\n\n      while (folderMap[folderName]) {\n        folderName = `${baseFolderName}_${count}`;\n        count++;\n      }\n\n      const brunoFolderItem = {\n        uid: uuid(),\n        name: folderName,\n        type: 'folder',\n        items: [],\n        seq: index + 1,\n        root: {\n          docs: transformDescription(i.description),\n          meta: {\n            name: folderName\n          },\n          request: {\n            auth: {\n              mode: 'inherit',\n              basic: null,\n              bearer: null,\n              awsv4: null,\n              apikey: null,\n              oauth2: null,\n              digest: null\n            },\n            headers: [],\n            script: {},\n            tests: '',\n            vars: {}\n          }\n        }\n      };\n\n      brunoParent.items.push(brunoFolderItem);\n\n      // Folder level auth\n      processAuth(i.auth, brunoFolderItem.root.request);\n\n      if (i.item && i.item.length) {\n        importPostmanV2CollectionItem(brunoFolderItem, i.item, { useWorkers }, scriptMap);\n      }\n\n      if (i.event) {\n        if (useWorkers) {\n          scriptMap.set(brunoFolderItem.uid, {\n            events: i.event,\n            request: brunoFolderItem.root.request\n          });\n        } else {\n          importScriptsFromEvents(i.event, brunoFolderItem.root.request);\n        }\n      }\n\n      folderMap[folderName] = brunoFolderItem;\n    } else if (i.request) {\n      const method = i?.request?.method?.toUpperCase();\n      if (!method || typeof method !== 'string' || !method.trim()) {\n        console.warn('Missing or invalid request.method', method);\n        return;\n      }\n\n      const baseRequestName = i.name || 'Untitled Request';\n      let requestName = baseRequestName;\n      let count = 1;\n\n      while (requestMap[requestName]) {\n        requestName = `${baseRequestName}_${count}`;\n        count++;\n      }\n\n      const url = constructUrl(i.request.url);\n\n      const brunoRequestItem = {\n        uid: uuid(),\n        name: requestName,\n        type: 'http-request',\n        seq: index + 1,\n        request: {\n          url: url,\n          method: method,\n          auth: {\n            mode: 'inherit',\n            basic: null,\n            bearer: null,\n            awsv4: null,\n            apikey: null,\n            oauth2: null,\n            digest: null\n          },\n          headers: [],\n          params: [],\n          body: {\n            mode: 'none',\n            json: null,\n            text: null,\n            xml: null,\n            formUrlEncoded: [],\n            multipartForm: []\n          },\n          docs: transformDescription(i.request.description)\n        }\n      };\n\n      const settings = {\n        encodeUrl: i.protocolProfileBehavior?.disableUrlEncoding !== true\n      };\n\n      // Handle followRedirects setting\n      if (i.protocolProfileBehavior?.followRedirects !== undefined) {\n        settings.followRedirects = i.protocolProfileBehavior.followRedirects;\n      }\n\n      // Handle maxRedirects setting\n      if (i.protocolProfileBehavior?.maxRedirects !== undefined) {\n        settings.maxRedirects = i.protocolProfileBehavior.maxRedirects;\n      }\n\n      brunoRequestItem.settings = settings;\n\n      brunoParent.items.push(brunoRequestItem);\n\n      if (i.event) {\n        if (useWorkers) {\n          scriptMap.set(brunoRequestItem.uid, {\n            events: i.event,\n            request: brunoRequestItem.request\n          });\n        } else {\n          i.event.forEach((event) => {\n            if (event.listen === 'prerequest' && event.script && event.script.exec) {\n              if (!brunoRequestItem.request?.script) {\n                brunoRequestItem.request.script = {};\n              }\n              if (event.script.exec && event.script.exec.length > 0) {\n                brunoRequestItem.request.script.req = postmanTranslation(event.script.exec);\n              } else {\n                brunoRequestItem.request.script.req = '';\n                console.warn('Unexpected event.script.exec type', typeof event.script.exec);\n              }\n            }\n            if (event.listen === 'test' && event.script && event.script.exec) {\n              if (!brunoRequestItem.request?.script) {\n                brunoRequestItem.request.script = {};\n              }\n              if (event.script.exec && event.script.exec.length > 0) {\n                brunoRequestItem.request.script.res = postmanTranslation(event.script.exec);\n              } else {\n                brunoRequestItem.request.script.res = '';\n                console.warn('Unexpected event.script.exec type', typeof event.script.exec);\n              }\n            }\n          });\n        }\n      }\n\n      const bodyMode = get(i, 'request.body.mode');\n      if (bodyMode) {\n        if (bodyMode === 'formdata') {\n          brunoRequestItem.request.body.mode = 'multipartForm';\n\n          each(i.request.body.formdata, (param) => {\n            if (param.key == null && param.value == null) return;\n            const isFile = param.type === 'file' || (param.type === 'default' && param.src);\n            const value = isFile\n              ? (Array.isArray(param.src) ? param.src : param.src ? [param.src] : [])\n              : (Array.isArray(param.value) ? param.value.join('') : param.value ?? '');\n\n            brunoRequestItem.request.body.multipartForm.push({\n              uid: uuid(),\n              type: isFile ? 'file' : 'text',\n              name: param.key ?? '',\n              value,\n              description: transformDescription(param.description),\n              enabled: !param.disabled,\n              ...(param.contentType && { contentType: param.contentType })\n            });\n          });\n        }\n\n        if (bodyMode === 'urlencoded') {\n          brunoRequestItem.request.body.mode = 'formUrlEncoded';\n          each(i.request.body.urlencoded, (param) => {\n            if (param.key == null && param.value == null) return;\n            brunoRequestItem.request.body.formUrlEncoded.push({\n              uid: uuid(),\n              name: param.key ?? '',\n              value: param.value ?? '',\n              description: transformDescription(param.description),\n              enabled: !param.disabled\n            });\n          });\n        }\n\n        if (bodyMode === 'raw') {\n          let language = get(i, 'request.body.options.raw.language');\n          if (!language) {\n            language = searchLanguageByHeader(i.request.header);\n          }\n          if (language === 'json') {\n            brunoRequestItem.request.body.mode = 'json';\n            brunoRequestItem.request.body.json = i.request.body.raw;\n          } else if (language === 'xml') {\n            brunoRequestItem.request.body.mode = 'xml';\n            brunoRequestItem.request.body.xml = i.request.body.raw;\n          } else {\n            brunoRequestItem.request.body.mode = 'text';\n            brunoRequestItem.request.body.text = i.request.body.raw;\n          }\n        }\n      }\n\n      if (bodyMode === 'graphql') {\n        brunoRequestItem.type = 'graphql-request';\n        brunoRequestItem.request.body.mode = 'graphql';\n        brunoRequestItem.request.body.graphql = parseGraphQLRequest(i.request.body.graphql);\n      }\n\n      each(i.request.header, (header) => {\n        if (header.key == null && header.value == null) return;\n        brunoRequestItem.request.headers.push({\n          uid: uuid(),\n          name: header.key ?? '',\n          value: header.value ?? '',\n          description: transformDescription(header.description),\n          enabled: !header.disabled\n        });\n      });\n\n      // Request-level auth\n      processAuth(i.request.auth, brunoRequestItem.request);\n\n      each(get(i, 'request.url.query'), (param) => {\n        if (param.key == null && param.value == null) {\n          return;\n        }\n        brunoRequestItem.request.params.push({\n          uid: uuid(),\n          name: param.key ?? '',\n          value: param.value ?? '',\n          description: transformDescription(param.description),\n          type: 'query',\n          enabled: !param.disabled\n        });\n      });\n\n      each(get(i, 'request.url.variable', []), (param) => {\n        if (!param.key) {\n          // If no key, skip this iteration and discard the param\n          return;\n        }\n\n        brunoRequestItem.request.params.push({\n          uid: uuid(),\n          name: param.key,\n          value: param.value ?? '',\n          description: transformDescription(param.description),\n          type: 'path',\n          enabled: true\n        });\n      });\n\n      // Handle Postman examples (responses)\n      if (i.response && Array.isArray(i.response)) {\n        brunoRequestItem.examples = [];\n\n        i.response.forEach((response, responseIndex) => {\n          const sanitized = String(response.name ?? '').replace(/\\r?\\n/g, ' ').trim();\n          const exampleName = sanitized || `Example ${responseIndex + 1}`;\n\n          // Convert originalRequest to Bruno request format\n          const originalRequest = response.originalRequest || {};\n          const exampleUrl = constructUrl(originalRequest.url);\n          const exampleMethod = originalRequest.method?.toUpperCase() || method;\n\n          const example = {\n            uid: uuid(),\n            itemUid: brunoRequestItem.uid,\n            name: exampleName,\n            description: '',\n            type: 'http-request',\n            request: {\n              url: exampleUrl,\n              method: exampleMethod,\n              headers: [],\n              params: [],\n              body: {\n                mode: 'none',\n                json: null,\n                text: null,\n                xml: null,\n                formUrlEncoded: [],\n                multipartForm: []\n              }\n            },\n            response: {\n              status: response.code || null,\n              statusText: response.status || '',\n              headers: [],\n              body: {\n                type: getBodyTypeFromContentTypeHeader(response.header),\n                content: response.body || ''\n              }\n            }\n          };\n\n          // Convert original request headers\n          if (originalRequest.header && Array.isArray(originalRequest.header)) {\n            originalRequest.header.forEach((header) => {\n              if (header.key == null && header.value == null) return;\n              example.request.headers.push({\n                uid: uuid(),\n                name: header.key ?? '',\n                value: header.value ?? '',\n                description: transformDescription(header.description),\n                enabled: !header.disabled\n              });\n            });\n          }\n\n          // Convert original request query parameters\n          if (originalRequest.url && originalRequest.url.query && Array.isArray(originalRequest.url.query)) {\n            originalRequest.url.query.forEach((param) => {\n              if (param.key == null && param.value == null) {\n                return;\n              }\n              example.request.params.push({\n                uid: uuid(),\n                name: param.key ?? '',\n                value: param.value ?? '',\n                description: transformDescription(param.description),\n                type: 'query',\n                enabled: !param.disabled\n              });\n            });\n          }\n\n          if (originalRequest.url && originalRequest.url.variable && Array.isArray(originalRequest.url.variable)) {\n            originalRequest.url.variable.forEach((param) => {\n              if (!param.key) return;\n              example.request.params.push({\n                uid: uuid(),\n                name: param.key,\n                value: param.value ?? '',\n                description: transformDescription(param.description),\n                type: 'path',\n                enabled: true\n              });\n            });\n          }\n\n          // Convert original request body\n          if (originalRequest.body) {\n            const bodyMode = originalRequest.body.mode;\n            if (bodyMode === 'formdata') {\n              example.request.body.mode = 'multipartForm';\n              if (originalRequest.body.formdata && Array.isArray(originalRequest.body.formdata)) {\n                originalRequest.body.formdata.forEach((param) => {\n                  if (param.key == null && param.value == null) return;\n                  const isFile = param.type === 'file' || (param.type === 'default' && param.src);\n                  const value = isFile\n                    ? (Array.isArray(param.src) ? param.src : param.src ? [param.src] : [])\n                    : (Array.isArray(param.value) ? param.value.join('') : param.value ?? '');\n\n                  example.request.body.multipartForm.push({\n                    uid: uuid(),\n                    type: isFile ? 'file' : 'text',\n                    name: param.key ?? '',\n                    value,\n                    description: transformDescription(param.description),\n                    enabled: !param.disabled,\n                    ...(param.contentType && { contentType: param.contentType })\n                  });\n                });\n              }\n            } else if (bodyMode === 'urlencoded') {\n              example.request.body.mode = 'formUrlEncoded';\n              if (originalRequest.body.urlencoded && Array.isArray(originalRequest.body.urlencoded)) {\n                originalRequest.body.urlencoded.forEach((param) => {\n                  if (param.key == null && param.value == null) return;\n                  example.request.body.formUrlEncoded.push({\n                    uid: uuid(),\n                    name: param.key ?? '',\n                    value: param.value ?? '',\n                    description: transformDescription(param.description),\n                    enabled: !param.disabled\n                  });\n                });\n              }\n            } else if (bodyMode === 'raw') {\n              let language = get(originalRequest, 'body.options.raw.language');\n              if (!language) {\n                language = searchLanguageByHeader(originalRequest.header || []);\n              }\n              if (language === 'json') {\n                example.request.body.mode = 'json';\n                example.request.body.json = originalRequest.body.raw;\n              } else if (language === 'xml') {\n                example.request.body.mode = 'xml';\n                example.request.body.xml = originalRequest.body.raw;\n              } else {\n                example.request.body.mode = 'text';\n                example.request.body.text = originalRequest.body.raw;\n              }\n            }\n          }\n\n          // Convert response headers\n          if (response.header && Array.isArray(response.header)) {\n            response.header.forEach((header) => {\n              if (header.key == null && header.value == null) return;\n              example.response.headers.push({\n                uid: uuid(),\n                name: header.key ?? '',\n                value: header.value ?? '',\n                description: transformDescription(header.description),\n                enabled: true\n              });\n            });\n          }\n\n          brunoRequestItem.examples.push(example);\n        });\n      }\n\n      requestMap[requestName] = brunoRequestItem;\n    }\n  });\n};\n\nconst searchLanguageByHeader = (headers) => {\n  let contentType;\n  each(headers, (header) => {\n    if (header.key.toLowerCase() === 'content-type' && !header.disabled) {\n      if (typeof header.value == 'string' && /^[\\w\\-]+\\/([\\w\\-]+\\+)?json/.test(header.value)) {\n        contentType = 'json';\n      } else if (typeof header.value == 'string' && /^[\\w\\-]+\\/([\\w\\-]+\\+)?xml/.test(header.value)) {\n        contentType = 'xml';\n      }\n      return false;\n    }\n  });\n  return contentType;\n};\n\nconst getBodyTypeFromContentTypeHeader = (headers) => {\n  // Check if headers is null, undefined, or not an array\n  if (!headers || !Array.isArray(headers)) {\n    return 'text';\n  }\n\n  const contentTypeHeader = headers.find((header) => header.key.toLowerCase() === 'content-type');\n  if (contentTypeHeader) {\n    const contentType = contentTypeHeader.value?.toLowerCase();\n    if (contentType?.includes('application/json')) {\n      return 'json';\n    } else if (contentType?.includes('application/xml') || contentType?.includes('text/xml')) {\n      return 'xml';\n    } else if (contentType?.includes('text/html')) {\n      return 'html';\n    }\n  }\n  return 'text';\n};\n\nconst importPostmanV2Collection = async (collection, { useWorkers = false }) => {\n  const brunoCollection = {\n    name: collection.info.name || 'Untitled Collection',\n    uid: uuid(),\n    version: '1',\n    items: [],\n    environments: [],\n    root: {\n      docs: transformDescription(collection.info.description),\n      meta: {\n        name: collection.info.name || 'Untitled Collection'\n      },\n      request: {\n        auth: {\n          mode: 'none',\n          basic: null,\n          bearer: null,\n          awsv4: null,\n          apikey: null,\n          oauth2: null,\n          digest: null\n        },\n        headers: [],\n        script: {},\n        tests: '',\n        vars: {}\n      }\n    }\n  };\n\n  if (collection.event) {\n    importScriptsFromEvents(collection.event, brunoCollection.root.request);\n  }\n\n  if (collection?.variable) {\n    importCollectionLevelVariables(collection.variable, brunoCollection.root.request);\n  }\n\n  // Collection level auth\n  processAuth(collection.auth, brunoCollection.root.request, true);\n\n  // Create a single scriptMap for all items\n  const scriptMap = useWorkers ? new Map() : null;\n\n  importPostmanV2CollectionItem(brunoCollection, collection.item, { useWorkers }, scriptMap);\n\n  // Process all scripts in a single call at the top level\n  if (useWorkers && scriptMap && scriptMap.size > 0) {\n    try {\n      const { default: scriptTranslationWorker } = await import('../workers/postman-translator-worker');\n      const translatedScripts = await scriptTranslationWorker(scriptMap);\n\n      // Apply translated scripts to all items in the collection\n      const applyScriptsToItems = (items) => {\n        items.forEach((item) => {\n          if (item.type === 'folder') {\n            // Apply scripts to the folder\n            if (translatedScripts.has(item.uid)) {\n              if (!item.root.request.script) {\n                item.root.request.script = {};\n              }\n              if (!item.root.request.tests) {\n                item.root.request.tests = '';\n              }\n\n              const script = translatedScripts.get(item.uid).request?.script?.req;\n              const tests = translatedScripts.get(item.uid).request?.script?.res;\n\n              item.root.request.script.req = script && script.length > 0 ? script : '';\n              item.root.request.script.res = tests && tests.length > 0 ? tests : '';\n            }\n\n            // Recursively apply to nested items\n            if (item.items && item.items.length > 0) {\n              applyScriptsToItems(item.items);\n            }\n          } else {\n            if (translatedScripts.has(item.uid)) {\n              if (!item.request.script) {\n                item.request.script = {};\n              }\n              if (!item.request.tests) {\n                item.request.tests = '';\n              }\n\n              const script = translatedScripts.get(item.uid).request?.script?.req;\n              const tests = translatedScripts.get(item.uid).request?.script?.res;\n\n              item.request.script.req = script && script.length > 0 ? script : '';\n              item.request.script.res = tests && tests.length > 0 ? tests : '';\n            }\n          }\n        });\n      };\n\n      applyScriptsToItems(brunoCollection.items);\n    } catch (error) {\n      console.error('Error in script translation worker:', error);\n    } finally {\n      scriptMap.clear();\n    }\n  }\n\n  return brunoCollection;\n};\n\nconst parsePostmanCollection = async (collection, { useWorkers = false }) => {\n  try {\n    let schema = get(collection, 'info.schema');\n\n    let v2Schemas = [\n      'https://schema.getpostman.com/json/collection/v2.0.0/collection.json',\n      'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',\n      'https://schema.postman.com/json/collection/v2.0.0/collection.json',\n      'https://schema.postman.com/json/collection/v2.1.0/collection.json'\n    ];\n\n    if (v2Schemas.includes(schema)) {\n      return await importPostmanV2Collection(collection, { useWorkers });\n    }\n\n    throw new Error('Unsupported Postman schema version. Only Postman Collection v2.0 and v2.1 are supported.');\n  } catch (err) {\n    console.log(err);\n    if (err instanceof Error) {\n      throw err;\n    }\n\n    throw new Error('Invalid Postman collection format. Please check your JSON file.');\n  }\n};\n\nconst postmanToBruno = async (postmanCollection, { useWorkers = false } = {}) => {\n  try {\n    const parsedPostmanCollection = await parsePostmanCollection(postmanCollection, { useWorkers });\n    const transformedCollection = transformItemsInCollection(parsedPostmanCollection);\n    const hydratedCollection = hydrateSeqInCollection(transformedCollection);\n    // Apply backward compatibility transformation for string status to number\n    const statusTransformedCollection = transformExampleStatusInCollection(hydratedCollection);\n    const validatedCollection = validateSchema(statusTransformedCollection);\n    return validatedCollection;\n  } catch (err) {\n    console.log(err);\n    throw new Error(`Import collection failed: ${err.message}`);\n  }\n};\n\nexport default postmanToBruno;\n"
  },
  {
    "path": "packages/bruno-converters/src/postman/postman-translations.js",
    "content": "import translateCode from '../utils/postman-to-bruno-translator';\n\n// TODO: Restore the commented-out translations once the UI update fixes are live.\n// Currently these APIs only work within the request lifecycle but fail to update the UI tables.\n// e.g., setCollectionVar only sets the variable in the request lifecycle, fails to update the table in the UI.\nconst replacements = {\n  'pm\\\\.environment\\\\.get\\\\(': 'bru.getEnvVar(',\n  'pm\\\\.environment\\\\.set\\\\(': 'bru.setEnvVar(',\n  'pm\\\\.variables\\\\.get\\\\(': 'bru.getVar(',\n  'pm\\\\.variables\\\\.set\\\\(': 'bru.setVar(',\n  'pm\\\\.variables\\\\.replaceIn\\\\(': 'bru.interpolate(',\n  'pm\\\\.collectionVariables\\\\.get\\\\(': 'bru.getCollectionVar(',\n  // 'pm\\\\.collectionVariables\\\\.set\\\\(': 'bru.setCollectionVar(',\n  'pm\\\\.collectionVariables\\\\.has\\\\(': 'bru.hasCollectionVar(',\n  // 'pm\\\\.collectionVariables\\\\.unset\\\\(': 'bru.deleteCollectionVar(',\n  // 'pm\\\\.collectionVariables\\\\.clear\\\\(': 'bru.deleteAllCollectionVars(',\n  // 'pm\\\\.collectionVariables\\\\.toObject\\\\(': 'bru.getAllCollectionVars(',\n  'pm\\\\.setNextRequest\\\\(': 'bru.setNextRequest(',\n  'pm\\\\.test\\\\(': 'test(',\n  'pm.response.to.have\\\\.status\\\\(': 'expect(res.getStatus()).to.equal(',\n  'pm\\\\.response\\\\.to\\\\.have\\\\.status\\\\(': 'expect(res.getStatus()).to.equal(',\n  'pm\\\\.response\\\\.json\\\\(': 'res.getBody(',\n  'pm\\\\.expect\\\\(': 'expect(',\n  'pm\\\\.environment\\\\.has\\\\(([^)]+)\\\\)': 'bru.getEnvVar($1) !== undefined && bru.getEnvVar($1) !== null',\n  'pm\\\\.response\\\\.code': 'res.getStatus()',\n  'pm\\\\.response\\\\.text\\\\(\\\\)': 'JSON.stringify(res.getBody())',\n  'pm\\\\.expect\\\\.fail\\\\(': 'expect.fail(',\n  'pm\\\\.response\\\\.responseTime': 'res.getResponseTime()',\n  'pm\\\\.globals\\\\.set\\\\(': 'bru.setGlobalEnvVar(',\n  'pm\\\\.globals\\\\.get\\\\(': 'bru.getGlobalEnvVar(',\n  // 'pm\\\\.globals\\\\.unset\\\\(': 'bru.deleteGlobalEnvVar(',\n  'pm\\\\.globals\\\\.toObject\\\\(': 'bru.getAllGlobalEnvVars(',\n  // 'pm\\\\.globals\\\\.clear\\\\(': 'bru.deleteAllGlobalEnvVars(',\n  'pm\\\\.environment\\\\.toObject\\\\(': 'bru.getAllEnvVars(',\n  'pm\\\\.environment\\\\.clear\\\\(': 'bru.deleteAllEnvVars(',\n  'pm\\\\.variables\\\\.toObject\\\\(': 'bru.getAllVars(',\n  'pm\\\\.request\\\\.headers\\\\.remove\\\\(': 'req.deleteHeader(',\n  'pm\\\\.response\\\\.headers\\\\.get\\\\(': 'res.getHeader(',\n  'pm\\\\.response\\\\.to\\\\.have\\\\.body\\\\(': 'expect(res.getBody()).to.equal(',\n  'pm\\\\.response\\\\.to\\\\.have\\\\.header\\\\(': 'expect(res.getHeaders()).to.have.property(',\n  'pm\\\\.response\\\\.size\\\\(\\\\)': 'res.getSize()',\n  'pm\\\\.response\\\\.size\\\\(\\\\)\\\\.body': 'res.getSize().body',\n  'pm\\\\.response\\\\.responseSize': 'res.getSize().body',\n  'pm\\\\.response\\\\.size\\\\(\\\\)\\\\.header': 'res.getSize().header',\n  'pm\\\\.response\\\\.size\\\\(\\\\)\\\\.total': 'res.getSize().total',\n  'pm\\\\.environment\\\\.name': 'bru.getEnvName()',\n  'pm\\\\.response\\\\.status': 'res.statusText',\n  'pm\\\\.response\\\\.headers': 'res.getHeaders()',\n  'tests\\\\[\\'([^\\']+)\\'\\\\]\\\\s*=\\\\s*([^;]+);': 'test(\"$1\", function() { expect(Boolean($2)).to.be.true; });',\n\n  // Supported Postman request translations:\n  // - pm.request.url / request.url     -> req.getUrl()\n  // - pm.request.url.getHost() -> req.getHost()\n  // - pm.request.url.getPath() -> req.getPath()\n  // - pm.request.url.getQueryString() -> req.getQueryString()\n  // - pm.request.url.variables -> req.getPathParams()\n  // - pm.request.method / request.method -> req.getMethod()\n  // - pm.request.headers / request.headers -> req.getHeaders()\n  // - pm.request.body / request.body   -> req.getBody()\n  // - pm.info.requestName / request.name -> req.getName()\n  'pm\\\\.request\\\\.url\\\\.getHost\\\\(\\\\)': 'req.getHost()',\n  'pm\\\\.request\\\\.url\\\\.getPath\\\\(\\\\)': 'req.getPath()',\n  'pm\\\\.request\\\\.url\\\\.getQueryString\\\\(\\\\)': 'req.getQueryString()',\n  'pm\\\\.request\\\\.url\\\\.variables': 'req.getPathParams()',\n  'pm\\\\.request\\\\.url': 'req.getUrl()',\n  'pm\\\\.request\\\\.method': 'req.getMethod()',\n  'pm\\\\.request\\\\.headers': 'req.getHeaders()',\n  'pm\\\\.request\\\\.body': 'req.getBody()',\n  'pm\\\\.info\\\\.requestName': 'req.getName()',\n  'request\\\\.url': 'req.getUrl()',\n  'request\\\\.method': 'req.getMethod()',\n  'request\\\\.headers': 'req.getHeaders()',\n  'request\\\\.body': 'req.getBody()',\n  'request\\\\.name': 'req.getName()',\n  // deprecated translations\n  'postman\\\\.setEnvironmentVariable\\\\(': 'bru.setEnvVar(',\n  'postman\\\\.getEnvironmentVariable\\\\(': 'bru.getEnvVar(',\n  'postman\\\\.clearEnvironmentVariable\\\\(': 'bru.deleteEnvVar(',\n  'pm\\\\.execution\\\\.skipRequest\\\\(\\\\)': 'bru.runner.skipRequest()',\n  'pm\\\\.execution\\\\.skipRequest': 'bru.runner.skipRequest',\n  'pm\\\\.execution\\\\.setNextRequest\\\\(null\\\\)': 'bru.runner.stopExecution()',\n  'pm\\\\.execution\\\\.setNextRequest\\\\(\\'null\\'\\\\)': 'bru.runner.stopExecution()',\n  // Direct cookie access translations (pm.cookies.has/get/toObject)\n  'pm\\\\.cookies\\\\.has\\\\(([^)]+)\\\\)': 'await bru.cookies.jar().hasCookie(req.getUrl(), $1)',\n  'pm\\\\.cookies\\\\.get\\\\(([^)]+)\\\\)': '(await bru.cookies.jar().getCookie(req.getUrl(), $1))?.value',\n  'pm\\\\.cookies\\\\.toObject\\\\(\\\\)': '(await bru.cookies.jar().getCookies(req.getUrl())).reduce((obj, c) => ({...obj, [c.key]: c.value}), {})',\n  // Cookie jar translations\n  'pm\\\\.cookies\\\\.jar\\\\(\\\\)': 'bru.cookies.jar()',\n  'pm\\\\.cookies\\\\.jar\\\\(\\\\)\\\\.get\\\\(': 'bru.cookies.jar().getCookie(',\n  'pm\\\\.cookies\\\\.jar\\\\(\\\\)\\\\.set\\\\(': 'bru.cookies.jar().setCookie(',\n  'pm\\\\.cookies\\\\.jar\\\\(\\\\)\\\\.unset\\\\(': 'bru.cookies.jar().deleteCookie(',\n  'pm\\\\.cookies\\\\.jar\\\\(\\\\)\\\\.clear\\\\(': 'bru.cookies.jar().deleteCookies(',\n  'pm\\\\.cookies\\\\.jar\\\\(\\\\)\\\\.getAll\\\\(': 'bru.cookies.jar().getCookies('\n};\n\nconst extendedReplacements = Object.keys(replacements).reduce((acc, key) => {\n  const newKey = key.replace(/^pm\\\\\\./, 'postman\\\\.');\n  acc[key] = replacements[key];\n  acc[newKey] = replacements[key];\n  return acc;\n}, {});\n\nconst compiledReplacements = Object.entries(extendedReplacements).map(([pattern, replacement]) => ({\n  regex: new RegExp(pattern, 'g'),\n  replacement\n}));\n\nconst processRegexReplacement = (code) => {\n  for (const { regex, replacement } of compiledReplacements) {\n    if (regex.test(code)) {\n      code = code.replace(regex, replacement);\n    }\n  }\n  if ((code.includes('pm.') || code.includes('postman.'))) {\n    code = code.replace(/^(.*(pm\\.|postman\\.).*)$/gm, '// $1');\n  }\n  return code;\n};\n\nconst postmanTranslation = (script, options = {}) => {\n  let modifiedScript = Array.isArray(script) ? script.join('\\n') : script;\n\n  try {\n    let translatedCode = translateCode(modifiedScript);\n    if ((translatedCode.includes('pm.') || translatedCode.includes('postman.'))) {\n      translatedCode = translatedCode.replace(/^(.*(pm\\.|postman\\.).*)$/gm, '// $1');\n    }\n    return translatedCode;\n  } catch (e) {\n    console.warn('Error in postman translation:', e);\n\n    try {\n      return processRegexReplacement(modifiedScript);\n    } catch (e) {\n      console.warn('Error in postman translation:', e);\n      return modifiedScript;\n    }\n  }\n};\n\nexport default postmanTranslation;\n"
  },
  {
    "path": "packages/bruno-converters/src/utils/ast-utils.js",
    "content": "const j = require('jscodeshift');\n\n/**\n * Efficiently builds a string representation of a member expression\n * without using toSource() for better performance.\n *\n * @param {Object} node - The member expression node from the AST\n * @returns {string} - String representation of the member expression (e.g., \"pm.environment.get\")\n *\n * @example\n * // For AST node representing `pm.environment.get`\n * getMemberExpressionString(node) // returns \"pm.environment.get\"\n *\n * // For AST node representing `obj[\"prop\"]`\n * getMemberExpressionString(node) // returns \"obj.prop\"\n *\n * // For AST node representing `bru.cookies.jar()`\n * getMemberExpressionString(node) // returns \"bru.cookies.jar()\"\n */\nexport function getMemberExpressionString(node) {\n  if (node.type === 'Identifier') {\n    return node.name;\n  }\n\n  if (node.type === 'CallExpression') {\n    const calleeStr = getMemberExpressionString(node.callee);\n    return `${calleeStr}()`;\n  }\n\n  if (node.type === 'MemberExpression') {\n    const objectStr = getMemberExpressionString(node.object);\n\n    // For computed properties like obj[prop]\n    if (node.computed) {\n      // For string literals like obj[\"prop\"], include them in the string\n      if (node.property.type === 'Literal' && typeof node.property.value === 'string') {\n        return `${objectStr}.${node.property.value}`;\n      }\n      // For other computed properties, we can't reliably represent them\n      return `${objectStr}.[computed]`;\n    }\n\n    // For regular property access like obj.prop\n    if (node.property.type === 'Identifier') {\n      return `${objectStr}.${node.property.name}`;\n    }\n  }\n\n  return '[unsupported]';\n}\n\n/**\n * Builds a member expression AST node from a dotted string path.\n *\n * @param {string} str - Dotted path string (e.g., \"pm.variables.get\")\n * @returns {Object} - jscodeshift MemberExpression or Identifier node\n *\n * @example\n * buildMemberExpressionFromString(\"pm.variables.get\")\n * // Returns AST for: pm.variables.get\n *\n * buildMemberExpressionFromString(\"pm\")\n * // Returns AST for: pm (just an Identifier)\n */\nexport function buildMemberExpressionFromString(str) {\n  const parts = str.split('.');\n  let expr = j.identifier(parts[0]);\n  for (let i = 1; i < parts.length; i += 1) {\n    expr = j.memberExpression(expr, j.identifier(parts[i]));\n  }\n  return expr;\n}\n\n/**\n * Checks if a node is an identifier with a specific name.\n *\n * @param {Object} node - The AST node to check\n * @param {string} name - The expected identifier name\n * @returns {boolean} - True if node is an identifier with the given name\n */\nexport function isIdentifierNamed(node, name) {\n  return node && node.type === 'Identifier' && node.name === name;\n}\n\n/**\n * Checks if a node is the null literal.\n *\n * @param {Object} node - The AST node to check\n * @returns {boolean} - True if node is a null literal\n */\nexport function isNullLiteral(node) {\n  return node && node.type === 'Literal' && node.value === null;\n}\n"
  },
  {
    "path": "packages/bruno-converters/src/utils/bruno-send-request-transformer.js",
    "content": "const j = require('jscodeshift');\n\n/**\n * Content-Type constants for body mode detection\n * @readonly\n */\nconst CONTENT_TYPES = Object.freeze({\n  URLENCODED: 'application/x-www-form-urlencoded',\n  FORMDATA: 'multipart/form-data'\n});\n\n/**\n * Body mode constants\n * @readonly\n */\nconst BODY_MODES = Object.freeze({\n  RAW: 'raw',\n  URLENCODED: 'urlencoded',\n  FORMDATA: 'formdata'\n});\n\n/**\n * Convert Bruno object format to Postman array format for body\n * @param {Object} objectValue - Object expression with key-value pairs\n * @returns {Object} - Array expression of key-value pair objects\n */\nconst convertObjectToArray = (objectValue) => {\n  const arr = j.arrayExpression([]);\n\n  if (objectValue.type === 'ObjectExpression') {\n    objectValue.properties.forEach((prop) => {\n      // Handle spread operators (e.g., ...rest)\n      if (prop.type === 'SpreadElement' || prop.type === 'SpreadProperty') {\n        // For spread operators, we need to spread the array at runtime\n        // Convert the spread expression to spread the result of Object.entries().map()\n        // This preserves the spread behavior in Postman format\n        // Object.entries(rest).map(([key, value]) => ({key, value}))\n        arr.elements.push(\n          j.spreadElement(\n            j.callExpression(\n              j.memberExpression(\n                j.callExpression(\n                  j.memberExpression(j.identifier('Object'), j.identifier('entries')),\n                  [prop.argument]\n                ),\n                j.identifier('map')\n              ),\n              [\n                j.arrowFunctionExpression(\n                  [j.arrayPattern([j.identifier('key'), j.identifier('value')])],\n                  j.objectExpression([\n                    j.property('init', j.identifier('key'), j.identifier('key')),\n                    j.property('init', j.identifier('value'), j.identifier('value'))\n                  ])\n                )\n              ]\n            )\n          )\n        );\n      } else {\n        // Handle regular key-value properties\n        // Skip if prop doesn't have a key (shouldn't happen, but defensive)\n        if (!prop.key) return;\n\n        const keyValue = prop.key.type === 'Literal' ? prop.key.value : prop.key.name;\n\n        arr.elements.push(\n          j.objectExpression([\n            j.property('init', j.identifier('key'), j.literal(keyValue)),\n            j.property('init', j.identifier('value'), prop.value)\n          ])\n        );\n      }\n    });\n  }\n\n  return arr;\n};\n\n/**\n * Get Content-Type from headers object\n * @param {Object} requestOptions - Request options object\n * @returns {string|null} - Content-Type value or null if not found\n */\nconst getContentType = (requestOptions) => {\n  if (requestOptions.type !== 'ObjectExpression') return null;\n\n  const headersProp = requestOptions.properties.find((p) =>\n    (p.key.name === 'headers' || p.key.value === 'headers')\n  );\n\n  if (!headersProp || headersProp.value.type !== 'ObjectExpression') return null;\n\n  const contentTypeProp = headersProp.value.properties.find((p) => {\n    const keyName = p.key.type === 'Literal' ? p.key.value : p.key.name;\n    return keyName && keyName.toLowerCase() === 'content-type';\n  });\n\n  if (contentTypeProp && contentTypeProp.value.type === 'Literal') {\n    return contentTypeProp.value.value;\n  }\n\n  return null;\n};\n\n/**\n * Transform headers property from Bruno format to Postman format\n * Rename 'headers' to 'header'\n * @param {Object} requestOptions - Request options object\n */\nconst transformHeaders = (requestOptions) => {\n  if (requestOptions.type !== 'ObjectExpression') return;\n\n  requestOptions.properties.forEach((prop) => {\n    // Find and rename 'headers' property to 'header'\n    if (prop.key.name === 'headers' || prop.key.value === 'headers') {\n      prop.key = j.identifier('header');\n    }\n  });\n};\n\n/**\n * Create a raw body object expression\n * @param {Object} dataValue - The data value to wrap\n * @returns {Object} - Object expression with raw mode\n */\nconst createRawBody = (dataValue) => {\n  return j.objectExpression([\n    j.property('init', j.identifier('mode'), j.literal(BODY_MODES.RAW)),\n    j.property('init', j.identifier('raw'), dataValue)\n  ]);\n};\n\n/**\n * Determine body mode based on Content-Type header\n * @param {string|null} contentType - Content-Type header value\n * @returns {string} - Body mode: 'urlencoded', 'formdata', or 'raw'\n */\nconst determineBodyMode = (contentType) => {\n  if (!contentType) return BODY_MODES.RAW;\n\n  const normalizedContentType = contentType.toLowerCase();\n  if (normalizedContentType.includes(CONTENT_TYPES.URLENCODED)) {\n    return BODY_MODES.URLENCODED;\n  }\n  if (normalizedContentType.includes(CONTENT_TYPES.FORMDATA)) {\n    return BODY_MODES.FORMDATA;\n  }\n  return BODY_MODES.RAW;\n};\n\n/**\n * Transform body/data property from Bruno format to Postman format\n * @param {Object} requestOptions - Request options object\n * @param {string|null} contentType - Content-Type header value (passed in because headers may be renamed)\n */\nconst transformBody = (requestOptions, contentType) => {\n  if (requestOptions.type !== 'ObjectExpression') return;\n\n  requestOptions.properties.forEach((prop) => {\n    if (prop.key.name === 'data' || prop.key.value === 'data') {\n      const dataValue = prop.value;\n      const bodyMode = determineBodyMode(contentType);\n\n      // Rename 'data' to 'body'\n      prop.key = j.identifier('body');\n\n      // Convert to Postman body format based on mode\n      if (bodyMode === BODY_MODES.URLENCODED && dataValue.type === 'ObjectExpression') {\n        prop.value = j.objectExpression([\n          j.property('init', j.identifier('mode'), j.literal(BODY_MODES.URLENCODED)),\n          j.property('init', j.identifier('urlencoded'), convertObjectToArray(dataValue))\n        ]);\n      } else if (bodyMode === BODY_MODES.FORMDATA && dataValue.type === 'ObjectExpression') {\n        prop.value = j.objectExpression([\n          j.property('init', j.identifier('mode'), j.literal(BODY_MODES.FORMDATA)),\n          j.property('init', j.identifier('formdata'), convertObjectToArray(dataValue))\n        ]);\n      } else {\n        // Default to raw mode (for non-object values or unrecognized Content-Type)\n        prop.value = createRawBody(dataValue);\n      }\n    }\n  });\n};\n\n/**\n * Transform callback function to Postman format\n * @param {Object} callback - Callback function expression\n * @returns {Object} - Transformed callback function\n */\nconst transformCallback = (callback) => {\n  if (!callback || (callback.type !== 'FunctionExpression' && callback.type !== 'ArrowFunctionExpression')) return null;\n\n  const params = callback.params;\n  const callbackBody = callback.body;\n\n  // Get the response parameter name (typically the second param)\n  let responseVarName = 'response'; // Default if not found\n  if (params.length >= 2 && params[1].type === 'Identifier') {\n    responseVarName = params[1].name;\n  }\n\n  let errorVarName = 'error'; // Default if not found\n  if (params.length >= 1 && params[0].type === 'Identifier') {\n    errorVarName = params[0].name;\n  }\n\n  // Define translations for callback response properties (Bruno -> Postman)\n  const responsePropertyMap = {\n    data: 'json', // response.data -> response.json()\n    status: 'code', // response.status -> response.code\n    statusText: 'status' // response.statusText -> response.status\n  };\n\n  // Process the callback body to transform response property references\n  j(callbackBody).find(j.MemberExpression, {\n    object: {\n      type: 'Identifier',\n      name: responseVarName\n    }\n  }).forEach((memberPath) => {\n    const property = memberPath.node.property;\n\n    // Handle property access\n    if (property.type === 'Identifier' && responsePropertyMap[property.name]) {\n      const pmProperty = responsePropertyMap[property.name];\n\n      if (property.name === 'data') {\n        // response.data -> response.json() (convert to method call)\n        j(memberPath).replaceWith(\n          j.callExpression(\n            j.memberExpression(\n              j.identifier(responseVarName),\n              j.identifier(pmProperty)\n            ),\n            []\n          )\n        );\n      } else {\n        // Regular property replacement (status -> code, statusText -> status)\n        j(memberPath).replaceWith(\n          j.memberExpression(\n            j.identifier(responseVarName),\n            j.identifier(pmProperty)\n          )\n        );\n      }\n    }\n  });\n\n  // Create the callback - Postman uses regular functions\n  const bodyStatements = callbackBody.type === 'BlockStatement' ? callbackBody.body : [j.returnStatement(callbackBody)];\n  const functionExpr = j.functionExpression(\n    null,\n    [j.identifier(errorVarName), j.identifier(responseVarName)],\n    j.blockStatement(bodyStatements)\n  );\n\n  functionExpr.async = callback.async;\n\n  return functionExpr;\n};\n\n/**\n * Find and transform variable declaration for request config\n * @param {Object} root - Root AST node (jscodeshift collection)\n * @param {string} variableName - Name of the variable to find\n * @param {Set} visited - Set of visited variable names to prevent infinite loops\n * @returns {Object|null} - Transformed object expression or null if not found\n */\nconst findAndTransformVariableDeclaration = (root, variableName, visited = new Set()) => {\n  // Prevent infinite loops from circular references\n  if (visited.has(variableName)) {\n    return null;\n  }\n  visited.add(variableName);\n\n  let transformedConfig = null;\n\n  // Find the variable declaration\n  root.find(j.VariableDeclarator, {\n    id: { name: variableName }\n  }).forEach((declaratorPath) => {\n    const init = declaratorPath.value.init;\n\n    if (init && init.type === 'ObjectExpression') {\n      // Found the actual object expression - transform it in place\n      // Get Content-Type BEFORE transforming headers (since we rename headers to header)\n      const contentType = getContentType(init);\n      transformHeaders(init);\n      transformBody(init, contentType);\n\n      transformedConfig = init;\n    } else if (init && init.type === 'Identifier') {\n      // This variable references another variable - follow the chain\n      const referencedVariableName = init.name;\n      transformedConfig = findAndTransformVariableDeclaration(root, referencedVariableName, visited);\n    }\n  });\n\n  return transformedConfig;\n};\n\n/**\n * Build pm.sendRequest member expression\n * @returns {Object} - MemberExpression AST node\n */\nconst buildPmSendRequest = () => {\n  return j.memberExpression(\n    j.identifier('pm'),\n    j.identifier('sendRequest')\n  );\n};\n\n/**\n * Main transformer for bru.sendRequest -> pm.sendRequest\n * @param {Object} path - AST path to the CallExpression\n * @returns {Object|null} - Transformed call expression or null\n */\nconst bruSendRequestTransformer = (path) => {\n  const callExpr = path.value;\n  if (callExpr.type !== 'CallExpression') return null;\n\n  // Clone the arguments for modification\n  const args = [...callExpr.arguments];\n  if (!args.length) {\n    // No arguments, just replace the callee\n    return j.callExpression(buildPmSendRequest(), []);\n  }\n\n  const requestOptions = args[0];\n  const callback = args[1];\n\n  // Transform the request config options\n  if (requestOptions.type === 'ObjectExpression') {\n    // Get Content-Type BEFORE transforming headers (since we rename headers to header)\n    const contentType = getContentType(requestOptions);\n    // Transform headers\n    transformHeaders(requestOptions);\n    // Transform body\n    transformBody(requestOptions, contentType);\n  } else if (requestOptions.type === 'Identifier') {\n    // Handle case where requestOptions is a variable reference\n    const variableName = requestOptions.name;\n\n    // Find the root of the current file/program\n    const root = j(path).closest(j.Program);\n\n    // Find and transform the variable declaration\n    findAndTransformVariableDeclaration(root, variableName);\n  }\n\n  // Transform callback if present\n  let transformedArgs = [requestOptions];\n  if (callback) {\n    const transformedCallback = transformCallback(callback);\n    if (transformedCallback) {\n      transformedArgs.push(transformedCallback);\n    } else {\n      transformedArgs.push(callback);\n    }\n  }\n\n  // Create pm.sendRequest call\n  return j.callExpression(buildPmSendRequest(), transformedArgs);\n};\n\nexport default bruSendRequestTransformer;\n"
  },
  {
    "path": "packages/bruno-converters/src/utils/bruno-to-postman-translator.js",
    "content": "import {\n  getMemberExpressionString,\n  buildMemberExpressionFromString\n} from './ast-utils';\nimport brunoSendRequestTransformer from './bruno-send-request-transformer';\nconst j = require('jscodeshift');\n\n// =============================================================================\n// SIMPLE TRANSLATIONS\n// =============================================================================\n\n/**\n * Simple 1:1 translations from Bruno helpers to Postman helpers.\n * These are direct member expression replacements.\n */\n// TODO: Restore the commented-out translations once the UI update fixes are live.\n// Currently these APIs only work within the request lifecycle but fail to update the UI tables.\n// e.g., setCollectionVar only sets the variable in the request lifecycle, fails to update the table in the UI.\nconst simpleTranslations = {\n  // Global variables\n  'bru.getGlobalEnvVar': 'pm.globals.get',\n  'bru.setGlobalEnvVar': 'pm.globals.set',\n  // 'bru.deleteGlobalEnvVar': 'pm.globals.unset',\n  'bru.getAllGlobalEnvVars': 'pm.globals.toObject',\n  // 'bru.deleteAllGlobalEnvVars': 'pm.globals.clear',\n\n  // Environment variables\n  'bru.getEnvVar': 'pm.environment.get',\n  'bru.setEnvVar': 'pm.environment.set',\n  'bru.hasEnvVar': 'pm.environment.has',\n  'bru.deleteEnvVar': 'pm.environment.unset',\n  'bru.getAllEnvVars': 'pm.environment.toObject',\n  'bru.deleteAllEnvVars': 'pm.environment.clear',\n  // Note: bru.getEnvName() is handled in complexTransformations because it's a function -> property conversion\n\n  // Runtime variables\n  'bru.getVar': 'pm.variables.get',\n  'bru.setVar': 'pm.variables.set',\n  'bru.hasVar': 'pm.variables.has',\n  'bru.deleteVar': 'pm.variables.unset',\n  'bru.getAllVars': 'pm.variables.toObject',\n  // 'bru.deleteAllVars':  Postman does not have a way to delete all variables\n\n  // Collection variables\n  'bru.getCollectionVar': 'pm.collectionVariables.get',\n  // 'bru.setCollectionVar': 'pm.collectionVariables.set',\n  'bru.hasCollectionVar': 'pm.collectionVariables.has',\n  // 'bru.deleteCollectionVar': 'pm.collectionVariables.unset',\n  // 'bru.getAllCollectionVars': 'pm.collectionVariables.toObject',\n  // 'bru.deleteAllCollectionVars': 'pm.collectionVariables.clear',\n\n  // Folder variables\n  'bru.getFolderVar': 'pm.variables.get',\n  /* Bruno does not have a way to set, has or delete folder variables */\n\n  // Request variables (map to pm.variables.*)\n  'bru.getRequestVar': 'pm.variables.get',\n  /* Bruno does not have a way to set, has or delete request variables */\n\n  // Interpolation\n  'bru.interpolate': 'pm.variables.replaceIn',\n\n  // Execution control\n  'bru.setNextRequest': 'pm.execution.setNextRequest',\n  'bru.runner.skipRequest': 'pm.execution.skipRequest',\n  'bru.runner.setNextRequest': 'pm.execution.setNextRequest',\n\n  // Request helpers\n  // Note: req.getUrl(), req.getMethod(), req.getHeaders(), req.getBody(), req.getName() are handled\n  // in complexTransformations because they're function -> property conversions\n  'req.url': 'pm.request.url',\n  'req.method': 'pm.request.method',\n  'req.headers': 'pm.request.headers',\n  'req.body': 'pm.request.body',\n  'req.getHeader': 'pm.request.headers.get',\n  // Note: req.setHeader is handled in complexTransformations because it needs arg restructuring (two args -> object)\n  'req.deleteHeader': 'pm.request.headers.remove',\n\n  // URL helper methods\n  'req.getHost': 'pm.request.url.getHost',\n  'req.getPath': 'pm.request.url.getPath',\n  'req.getQueryString': 'pm.request.url.getQueryString',\n\n  // Response helpers\n  // Note: res.getStatus(), res.getResponseTime(), res.getHeaders(), res.getUrl() are handled\n  // in complexTransformations because they're function -> property conversions\n  'res.status': 'pm.response.code',\n  'res.statusText': 'pm.response.status',\n  'res.body': 'pm.response.body',\n  'res.url': 'pm.response.url',\n  'res.responseTime': 'pm.response.responseTime',\n  'res.headers': 'pm.response.headers',\n  'res.getBody': 'pm.response.json',\n  'res.getHeader': 'pm.response.headers.get',\n  'res.getSize': 'pm.response.size',\n\n  // Cookies jar\n  'bru.cookies.jar': 'pm.cookies.jar',\n\n  // Testing\n  'expect.fail': 'pm.expect.fail'\n};\n\n// =============================================================================\n// UNSUPPORTED BRUNO APIs (No Postman Equivalent)\n// =============================================================================\n\n/**\n * UNSUPPORTED BRUNO APIs (No Postman Equivalent)\n *\n * These Bruno APIs have no direct Postman equivalent and will be left unchanged\n * in the translated code. Users should be aware that these calls will not work\n * in Postman:\n *\n * Request APIs:\n * - req.getTags() - Postman doesn't have tags\n * - req.setMaxRedirects() - Postman doesn't expose redirect settings\n * - req.getTimeout() / req.setTimeout() - Postman doesn't expose timeout settings\n * - req.getExecutionMode() / req.getExecutionPlatform() - Bruno-specific\n * - req.onFail() - Postman doesn't support error handlers\n *\n * Response APIs:\n * - res.setBody() - Postman response is read-only\n *\n * Bru APIs:\n * - bru.runRequest() - Postman doesn't support nested request execution\n * - bru.sleep() - Postman doesn't have sleep (use setTimeout workaround)\n * - bru.getProcessEnv() - Postman doesn't expose process env vars\n * - bru.getOauth2CredentialVar() - Bruno-specific\n * - bru.getCollectionName() - pm.info doesn't expose collection name\n * - bru.disableParsingResponseJson() - Bruno-specific\n * - bru.cwd() - Bruno-specific\n * - bru.getAssertionResults() / bru.getTestResults() - Bruno-specific\n */\n\n// =============================================================================\n// COMPLEX TRANSFORMATIONS\n// =============================================================================\n\n/**\n * Complex transformations that require custom handling beyond simple replacements.\n * Each transformation has a pattern to match and a transform function.\n *\n * Note: These are processed in order, so more specific patterns should come first.\n */\nconst complexTransformations = [\n  // bru.sendRequest transformation\n  {\n    pattern: 'bru.sendRequest',\n    transform: brunoSendRequestTransformer\n  },\n\n  // bru.runner.stopExecution() -> pm.execution.setNextRequest(null)\n  {\n    pattern: 'bru.runner.stopExecution',\n    transform: (path) => {\n      return j.callExpression(\n        buildMemberExpressionFromString('pm.execution.setNextRequest'),\n        [j.literal(null)]\n      );\n    }\n  },\n\n  // JSON.stringify(res.getBody()) -> pm.response.text()\n  {\n    pattern: 'JSON.stringify',\n    condition: (path) => {\n      const args = path.value.arguments;\n      if (args.length !== 1) return false;\n\n      const arg = args[0];\n      if (arg.type !== 'CallExpression' || arg.callee.type !== 'MemberExpression') return false;\n\n      return getMemberExpressionString(arg.callee) === 'res.getBody';\n    },\n    transform: () => {\n      return j.callExpression(\n        buildMemberExpressionFromString('pm.response.text'),\n        []\n      );\n    }\n  },\n\n  // bru.getEnvName() -> pm.environment.name (function to property)\n  {\n    pattern: 'bru.getEnvName',\n    transform: () => {\n      // Replace the entire call expression with just the member expression (property access)\n      return buildMemberExpressionFromString('pm.environment.name');\n    }\n  },\n\n  // Request helpers: function -> property conversions\n  // req.getUrl() -> pm.request.url\n  {\n    pattern: 'req.getUrl',\n    transform: () => buildMemberExpressionFromString('pm.request.url')\n  },\n  // req.getMethod() -> pm.request.method\n  {\n    pattern: 'req.getMethod',\n    transform: () => buildMemberExpressionFromString('pm.request.method')\n  },\n  // req.getHeaders() -> pm.request.headers\n  {\n    pattern: 'req.getHeaders',\n    transform: () => buildMemberExpressionFromString('pm.request.headers')\n  },\n  // req.getBody() -> pm.request.body\n  {\n    pattern: 'req.getBody',\n    transform: () => buildMemberExpressionFromString('pm.request.body')\n  },\n  // req.getName() -> pm.info.requestName\n  {\n    pattern: 'req.getName',\n    transform: () => buildMemberExpressionFromString('pm.info.requestName')\n  },\n  // req.getAuthMode() -> pm.request.auth.type\n  {\n    pattern: 'req.getAuthMode',\n    transform: () => buildMemberExpressionFromString('pm.request.auth.type')\n  },\n  // req.getPathParams() -> pm.request.url.variables\n  {\n    pattern: 'req.getPathParams',\n    transform: () => buildMemberExpressionFromString('pm.request.url.variables')\n  },\n\n  // Response helpers: function -> property conversions\n  // res.getStatus() -> pm.response.code\n  {\n    pattern: 'res.getStatus',\n    transform: () => buildMemberExpressionFromString('pm.response.code')\n  },\n  // res.getStatusText() -> pm.response.status\n  {\n    pattern: 'res.getStatusText',\n    transform: () => buildMemberExpressionFromString('pm.response.status')\n  },\n  // res.getResponseTime() -> pm.response.responseTime\n  {\n    pattern: 'res.getResponseTime',\n    transform: () => buildMemberExpressionFromString('pm.response.responseTime')\n  },\n  // res.getHeaders() -> pm.response.headers\n  {\n    pattern: 'res.getHeaders',\n    transform: () => buildMemberExpressionFromString('pm.response.headers')\n  },\n  // res.getUrl() -> pm.response.url\n  {\n    pattern: 'res.getUrl',\n    transform: () => buildMemberExpressionFromString('pm.response.url')\n  },\n\n  // Request modifiers: function calls -> assignments\n  // req.setUrl(url) -> pm.request.url = url\n  {\n    pattern: 'req.setUrl',\n    transform: (path) => {\n      const callExpr = path.value;\n      const args = callExpr.arguments;\n      if (!args || args.length === 0) {\n        // No arguments, return the property access\n        return buildMemberExpressionFromString('pm.request.url');\n      }\n      // Transform req.setUrl(url) to pm.request.url = url\n      return j.assignmentExpression(\n        '=',\n        buildMemberExpressionFromString('pm.request.url'),\n        args[0]\n      );\n    }\n  },\n  // req.setMethod(method) -> pm.request.method = method\n  {\n    pattern: 'req.setMethod',\n    transform: (path) => {\n      const callExpr = path.value;\n      const args = callExpr.arguments;\n      if (!args || args.length === 0) {\n        // No arguments, return the property access\n        return buildMemberExpressionFromString('pm.request.method');\n      }\n      // Transform req.setMethod(method) to pm.request.method = method\n      return j.assignmentExpression(\n        '=',\n        buildMemberExpressionFromString('pm.request.method'),\n        args[0]\n      );\n    }\n  },\n  // req.setBody(data) -> pm.request.body.update({mode: \"raw\", raw: JSON.stringify(data)})\n  {\n    pattern: 'req.setBody',\n    transform: (path) => {\n      const callExpr = path.value;\n      const args = callExpr.arguments;\n      if (!args || args.length === 0) {\n        // No arguments, return the property access\n        return buildMemberExpressionFromString('pm.request.body');\n      }\n      // Transform req.setBody(data) to pm.request.body.update({mode: \"raw\", raw: JSON.stringify(data)})\n      const bodyArg = args[0];\n      const updateCall = j.callExpression(\n        j.memberExpression(\n          buildMemberExpressionFromString('pm.request.body'),\n          j.identifier('update')\n        ),\n        [\n          j.objectExpression([\n            j.property('init', j.identifier('mode'), j.literal('raw')),\n            j.property('init', j.identifier('raw'), j.callExpression(\n              j.identifier('JSON.stringify'),\n              [bodyArg]\n            ))\n          ])\n        ]\n      );\n      return updateCall;\n    }\n  },\n  // req.setHeader(key, value) -> pm.request.headers.upsert({key: key, value: value})\n  {\n    pattern: 'req.setHeader',\n    transform: (path) => {\n      const args = path.value.arguments;\n      if (!args || args.length < 2) {\n        return j.callExpression(\n          buildMemberExpressionFromString('pm.request.headers.upsert'),\n          args || []\n        );\n      }\n      return j.callExpression(\n        buildMemberExpressionFromString('pm.request.headers.upsert'),\n        [\n          j.objectExpression([\n            j.property('init', j.identifier('key'), args[0]),\n            j.property('init', j.identifier('value'), args[1])\n          ])\n        ]\n      );\n    }\n  },\n  // req.setHeaders(headers) -> loop calling pm.request.headers.upsert() for each header\n  {\n    pattern: 'req.setHeaders',\n    transform: (path) => {\n      const callExpr = path.value;\n      const args = callExpr.arguments;\n      if (!args || args.length === 0) {\n        // No arguments, return the property access\n        return buildMemberExpressionFromString('pm.request.headers');\n      }\n      const headersArg = args[0];\n\n      // Transform req.setHeaders(obj) to a for...in loop that calls upsert for each property\n      // Generate: for (const key in headersObj) { pm.request.headers.upsert({key: key, value: headersObj[key]}); }\n      const headersVar = j.identifier('_headers');\n      const keyVar = j.identifier('key');\n\n      // Create: for (const key in _headers) { pm.request.headers.upsert({key: key, value: _headers[key]}); }\n      const forLoop = j.forInStatement(\n        j.variableDeclaration('const', [j.variableDeclarator(keyVar)]),\n        headersVar,\n        j.blockStatement([\n          j.expressionStatement(\n            j.callExpression(\n              j.memberExpression(\n                buildMemberExpressionFromString('pm.request.headers'),\n                j.identifier('upsert')\n              ),\n              [\n                j.objectExpression([\n                  j.property('init', j.identifier('key'), keyVar),\n                  j.property('init', j.identifier('value'), j.memberExpression(headersVar, keyVar, true))\n                ])\n              ]\n            )\n          )\n        ])\n      );\n\n      // We need to replace the call expression with a block that includes the variable declaration and loop\n      // But the current architecture only replaces the call expression itself\n      // So we'll create an IIFE (Immediately Invoked Function Expression) that contains both\n      const iife = j.callExpression(\n        j.functionExpression(\n          null,\n          [],\n          j.blockStatement([\n            j.variableDeclaration('const', [\n              j.variableDeclarator(headersVar, headersArg)\n            ]),\n            forLoop\n          ])\n        ),\n        []\n      );\n\n      return iife;\n    }\n  }\n];\n\n// Create a map for O(1) lookups of complex transformations\nconst complexTransformationsMap = new Map();\ncomplexTransformations.forEach((t) => {\n  complexTransformationsMap.set(t.pattern, t);\n});\n\n// Cookie jar method mappings (Bruno -> PM)\n// Note: Bruno's setCookie with cookie object form is not supported (Postman only accepts url, name, value, callback?)\n// Note: getCookies(url, callback?) -> getAll(url, options?, callback?)\n//       PM docs treat callback as 2nd arg, likely handled internally to detect function vs options object\nconst cookieMethodMapping = {\n  getCookie: 'get', // (url, name, callback?) -> (url, name, callback?)\n  getCookies: 'getAll', // (url, callback?) -> (url, callback?) - PM handles internally\n  setCookie: 'set', // (url, name, value, callback?) -> (url, name, value?, callback?)\n  deleteCookie: 'unset', // (url, name, callback?) -> (url, name, callback?)\n  deleteCookies: 'clear' // (url, callback?) -> (url, callback?)\n};\n\n// =============================================================================\n// TRANSFORMATION FUNCTIONS\n// =============================================================================\n\n/**\n * Process simple member expression translations (bru.* -> pm.*)\n * and complex transformations in a single pass.\n *\n * @param {Object} ast - jscodeshift AST\n */\nfunction processAllTransformations(ast) {\n  // First handle CallExpressions for complex transformations\n  ast.find(j.CallExpression).forEach((path) => {\n    const { callee } = path.value;\n    if (callee.type !== 'MemberExpression') return;\n\n    const memberExprStr = getMemberExpressionString(callee);\n    const transform = complexTransformationsMap.get(memberExprStr);\n\n    if (transform) {\n      // Check condition if present\n      if (transform.condition && !transform.condition(path)) return;\n\n      const replacement = transform.transform(path);\n      if (replacement !== null) {\n        j(path).replaceWith(replacement);\n      }\n    }\n  });\n\n  // Then handle simple member expression translations\n  ast.find(j.MemberExpression).forEach((path) => {\n    const memberExprStr = getMemberExpressionString(path.value);\n\n    if (!Object.prototype.hasOwnProperty.call(simpleTranslations, memberExprStr)) return;\n\n    const replacement = simpleTranslations[memberExprStr];\n    j(path).replaceWith(buildMemberExpressionFromString(replacement));\n  });\n}\n\n/**\n * Transform cookie jar method calls.\n * Handles both direct calls and variables assigned to cookie jars.\n *\n * @param {Object} ast - jscodeshift AST\n */\nfunction transformCookieJarMethods(ast) {\n  // Track variables assigned to cookie jar instances\n  const cookieJarVars = new Set();\n\n  // Find variables assigned to cookie jar\n  ast.find(j.VariableDeclarator).forEach((path) => {\n    if (path.value.init?.type === 'CallExpression' && path.value.init.callee.type === 'MemberExpression') {\n      const calleeStr = getMemberExpressionString(path.value.init.callee);\n      if (calleeStr === 'bru.cookies.jar' || calleeStr === 'pm.cookies.jar') {\n        if (path.value.id.type === 'Identifier') {\n          cookieJarVars.add(path.value.id.name);\n        }\n      }\n    }\n  });\n\n  // Transform method calls on cookie jars\n  ast.find(j.CallExpression).forEach((path) => {\n    const { callee } = path.value;\n    if (callee.type !== 'MemberExpression' || callee.property.type !== 'Identifier') return;\n\n    const methodName = callee.property.name;\n    if (!cookieMethodMapping[methodName]) return;\n\n    // Check if object is a direct jar() call or a jar variable\n    const isDirectJarCall = callee.object.type === 'CallExpression'\n      && callee.object.callee.type === 'MemberExpression'\n      && ['bru.cookies.jar', 'pm.cookies.jar'].includes(getMemberExpressionString(callee.object.callee));\n\n    const isJarVariable = callee.object.type === 'Identifier' && cookieJarVars.has(callee.object.name);\n\n    if (isDirectJarCall || isJarVariable) {\n      path.value.callee.property.name = cookieMethodMapping[methodName];\n    }\n  });\n}\n\n/**\n * Transform test() -> pm.test() and expect() -> pm.expect()\n *\n * @param {Object} ast - jscodeshift AST\n */\nfunction transformTestsAndExpect(ast) {\n  // Transform test(...) -> pm.test(...)\n  ast.find(j.CallExpression, { callee: { type: 'Identifier', name: 'test' } })\n    .forEach((path) => {\n      j(path.get('callee')).replaceWith(\n        j.memberExpression(j.identifier('pm'), j.identifier('test'))\n      );\n    });\n\n  // Transform expect(...) -> pm.expect(...)\n  ast.find(j.CallExpression, { callee: { type: 'Identifier', name: 'expect' } })\n    .forEach((path) => {\n      j(path.get('callee')).replaceWith(\n        j.memberExpression(j.identifier('pm'), j.identifier('expect'))\n      );\n    });\n}\n\n// =============================================================================\n// MAIN EXPORT\n// =============================================================================\n\n/**\n * Translate Bruno scripts back to Postman-compatible scripts.\n *\n * This function transforms Bruno API calls (bru.*, req.*, res.*, test(), expect())\n * back to their Postman equivalents (pm.*, pm.request.*, pm.response.*, pm.test(), pm.expect()).\n *\n * @param {string} code - Bruno script string\n * @returns {string} - Postman-compatible script string\n *\n * @example\n * translateBruToPostman('bru.getEnvVar(\"test\");')\n * // Returns: 'pm.environment.get(\"test\");'\n *\n * @example\n * translateBruToPostman('const data = res.getBody();')\n * // Returns: 'const data = pm.response.json();'\n */\nfunction translateBruToPostman(code) {\n  if (!code || typeof code !== 'string') {\n    return '';\n  }\n\n  try {\n    const ast = j(code);\n\n    processAllTransformations(ast);\n    transformCookieJarMethods(ast);\n    transformTestsAndExpect(ast);\n\n    return ast.toSource();\n  } catch (e) {\n    console.warn('Error in Bruno to Postman translation:', e);\n    return code;\n  }\n}\n\nexport default translateBruToPostman;\n"
  },
  {
    "path": "packages/bruno-converters/src/utils/flatten.js",
    "content": "// Adapted from flat library by Hugh Kennedy (https://github.com/hughsk/flat)\n// MIT License\n\n/**\n * Recursively flattens a nested object or array into a flat object with JavaScript-style keys.\n * Arrays use square bracket notation (e.g., items[0].id).\n * Only primitives and null are included as values.\n *\n * @param {object|array} obj - The object or array to flatten.\n * @param {string} [prefix] - Used internally for recursion to build the path.\n * @returns {object} A flat object with JavaScript-style keys.\n */\nfunction flattenObject(obj, prefix = '') {\n  // Store the final flat result\n  const result = {};\n\n  /**\n   * Internal recursive function to process each value.\n   * @param {*} value - The current value (can be object, array, primitive, or null)\n   * @param {string} path - The JavaScript-style key up to this point\n   */\n  function step(value, path) {\n    // If value is a primitive (string, number, boolean) or null, add it to the result\n    if (value === null || typeof value !== 'object') {\n      result[path] = value;\n      return;\n    }\n\n    // If value is an array, iterate over each item by index\n    if (Array.isArray(value)) {\n      value.forEach((item, idx) => {\n        // Build the next path with array index using square brackets (e.g. \"items[0]\")\n        step(item, path ? `${path}[${idx}]` : `[${idx}]`);\n      });\n    } else {\n      // If value is an object, iterate over its keys\n      Object.entries(value).forEach(([key, val]) => {\n        // Build the next path with object key (e.g. \"user.name\")\n        step(val, path ? `${path}.${key}` : key);\n      });\n    }\n  }\n\n  // Start recursive flattening from the root object\n  step(obj, prefix);\n\n  // Return the flat result object\n  return result;\n}\n\nexport { flattenObject };\n"
  },
  {
    "path": "packages/bruno-converters/src/utils/postman-to-bruno-translator.js",
    "content": "import sendRequestTransformer from './send-request-transformer';\nimport { getMemberExpressionString } from './ast-utils';\nconst j = require('jscodeshift');\nconst cloneDeep = require('lodash/cloneDeep');\n\n// Simple 1:1 translations for straightforward replacements\n// TODO: Restore the commented-out translations once the UI update fixes are live.\n// Currently these APIs only work within the request lifecycle but fail to update the UI tables.\n// e.g., setCollectionVar only sets the variable in the request lifecycle, fails to update the table in the UI.\nconst simpleTranslations = {\n  // Global Variables\n  'pm.globals.get': 'bru.getGlobalEnvVar',\n  'pm.globals.set': 'bru.setGlobalEnvVar',\n  'pm.globals.replaceIn': 'bru.interpolate',\n  // 'pm.globals.unset': 'bru.deleteGlobalEnvVar',\n  'pm.globals.toObject': 'bru.getAllGlobalEnvVars',\n  // 'pm.globals.clear': 'bru.deleteAllGlobalEnvVars',\n\n  // Environment variables\n  'pm.environment.get': 'bru.getEnvVar',\n  'pm.environment.set': 'bru.setEnvVar',\n  'pm.environment.name': 'bru.getEnvName()',\n  'pm.environment.unset': 'bru.deleteEnvVar',\n  'pm.environment.replaceIn': 'bru.interpolate',\n  'pm.environment.toObject': 'bru.getAllEnvVars',\n  'pm.environment.clear': 'bru.deleteAllEnvVars',\n\n  // Variables\n  'pm.variables.get': 'bru.getVar',\n  'pm.variables.set': 'bru.setVar',\n  'pm.variables.has': 'bru.hasVar',\n  'pm.variables.toObject': 'bru.getAllVars',\n  'pm.variables.replaceIn': 'bru.interpolate',\n  // Collection variables\n  'pm.collectionVariables.get': 'bru.getCollectionVar',\n  // 'pm.collectionVariables.set': 'bru.setCollectionVar',\n  'pm.collectionVariables.has': 'bru.hasCollectionVar',\n  // 'pm.collectionVariables.unset': 'bru.deleteCollectionVar',\n  'pm.collectionVariables.replaceIn': 'bru.interpolate',\n  // 'pm.collectionVariables.clear': 'bru.deleteAllCollectionVars',\n  // 'pm.collectionVariables.toObject': 'bru.getAllCollectionVars',\n\n  // Request flow control\n  'pm.setNextRequest': 'bru.setNextRequest',\n\n  // Testing\n  'pm.test': 'test',\n  'pm.expect': 'expect',\n  'pm.expect.fail': 'expect.fail',\n\n  // Info\n  'pm.info.requestName': 'req.getName()',\n\n  // Request headers\n  'pm.request.headers.remove': 'req.deleteHeader',\n\n  // Request properties (pm.request.*)\n  'pm.request.url.getHost': 'req.getHost',\n  'pm.request.url.getPath': 'req.getPath',\n  'pm.request.url.getQueryString': 'req.getQueryString',\n  'pm.request.url.variables': 'req.getPathParams()',\n  'pm.request.url': 'req.getUrl()',\n  'pm.request.method': 'req.getMethod()',\n  'pm.request.headers': 'req.getHeaders()',\n  'pm.request.body': 'req.getBody()',\n\n  // Legacy/global request object (request.*)\n  'request.url': 'req.getUrl()',\n  'request.method': 'req.getMethod()',\n  'request.headers': 'req.getHeaders()',\n  'request.body': 'req.getBody()',\n  'request.name': 'req.getName()',\n\n  // Response properties\n  'pm.response.json': 'res.getBody',\n  'pm.response.code': 'res.getStatus()',\n  'pm.response.status': 'res.statusText',\n  'pm.response.responseTime': 'res.getResponseTime()',\n  'pm.response.statusText': 'res.statusText',\n  'pm.response.headers': 'res.getHeaders()',\n  'pm.response.size': 'res.getSize',\n  'pm.response.responseSize': 'res.getSize().body',\n  'pm.response.size().body': 'res.getSize().body',\n  'pm.response.size().header': 'res.getSize().header',\n  'pm.response.size().total': 'res.getSize().total',\n  'pm.cookies.jar': 'bru.cookies.jar',\n\n  'pm.cookies.jar().get': 'bru.cookies.jar().getCookie',\n  'pm.cookies.jar().getAll': 'bru.cookies.jar().getCookies',\n  'pm.cookies.jar().set': 'bru.cookies.jar().setCookie',\n  'pm.cookies.jar().unset': 'bru.cookies.jar().deleteCookie',\n  'pm.cookies.jar().clear': 'bru.cookies.jar().deleteCookies',\n\n  // Execution control\n  'pm.execution.skipRequest': 'bru.runner.skipRequest',\n\n  // Legacy Postman API (deprecated) (we can use pm instead of postman, as we are converting all postman references to pm in the code as the part of pre-processing)\n  'pm.setEnvironmentVariable': 'bru.setEnvVar',\n  'pm.getEnvironmentVariable': 'bru.getEnvVar',\n  'pm.clearEnvironmentVariable': 'bru.deleteEnvVar',\n\n  // Legacy response properties\n  'responseCode.code': 'res.getStatus()',\n  'responseCode.name': 'res.statusText'\n};\n\n/* Complex transformations that need custom handling\n* Note: Transform functions can return either a single node or an array of nodes.\n* When returning an array of nodes, each node in the array will be inserted\n* as a separate statement, which allows a single Postman expression to be\n* transformed into multiple Bruno statements (e.g. for complex assertions).\n*/\n\nconst complexTransformations = [\n  // pm.sendRequest transformation\n  {\n    pattern: 'pm.sendRequest',\n    transform: sendRequestTransformer\n  },\n\n  // pm.environment.has requires special handling\n  {\n    pattern: 'pm.environment.has',\n    transform: (path, j) => {\n      const callExpr = path.parent.value;\n\n      const args = callExpr.arguments;\n\n      // Create: bru.getEnvVar(arg) !== undefined && bru.getEnvVar(arg) !== null\n      return j.logicalExpression(\n        '&&',\n        j.binaryExpression(\n          '!==',\n          j.callExpression(j.identifier('bru.getEnvVar'), args),\n          j.identifier('undefined')\n        ),\n        j.binaryExpression(\n          '!==',\n          j.callExpression(j.identifier('bru.getEnvVar'), args),\n          j.identifier('null')\n        )\n      );\n    }\n  },\n\n  {\n    pattern: 'pm.response.text',\n    transform: (_, j) => {\n      return j.callExpression(j.identifier('JSON.stringify'), [j.identifier('res.getBody()')]);\n    }\n  },\n  {\n    pattern: 'pm.response.headers.get',\n    transform: (path, j) => {\n      return j.callExpression(j.identifier('res.getHeader'), path.parent.value.arguments);\n    }\n  },\n  // Handle pm.response.to.have.status\n  {\n    pattern: 'pm.response.to.have.status',\n    transform: (path, j) => {\n      const callExpr = path.parent.value;\n\n      const args = callExpr.arguments;\n\n      // Create: expect(res.getStatus()).to.equal(arg)\n      return j.callExpression(\n        j.memberExpression(\n          j.callExpression(\n            j.identifier('expect'),\n            [\n              j.callExpression(\n                j.identifier('res.getStatus'),\n                []\n              )\n            ]\n          ),\n          j.identifier('to.equal')\n        ),\n        args\n      );\n    }\n  },\n\n  // handle 'pm.response.to.have.header' to expect(res.getHeaders()).to.have.property(args)\n  {\n    pattern: 'pm.response.to.have.header',\n    transform: (path, j) => {\n      const callExpr = path.parent.value;\n\n      const args = callExpr.arguments;\n\n      if (args.length > 0) {\n        // Apply toLowerCase() to the first argument\n        args[0] = j.callExpression(\n          j.memberExpression(\n            args[0],\n            j.identifier('toLowerCase')\n          ),\n          []\n        );\n      }\n\n      // Create: expect(res.getHeaders()).to.have.property(args)\n      return j.callExpression(\n        j.memberExpression(\n          j.callExpression(\n            j.identifier('expect'),\n            [\n              j.callExpression(\n                j.identifier('res.getHeaders'),\n                []\n              )\n            ]\n          ),\n          j.identifier('to.have.property')\n        ),\n        args\n      );\n    }\n  },\n  // handle pm.response.to.have.body to expect(res.getBody()).to.equal(arg)\n  {\n    pattern: 'pm.response.to.have.body',\n    transform: (path, j) => {\n      const callExpr = path.parent.value;\n\n      const args = callExpr.arguments;\n\n      return j.callExpression(\n        j.memberExpression(\n          j.callExpression(j.identifier('expect'), [j.identifier('res.getBody()')]),\n          j.identifier('to.equal')\n        ),\n        args\n      );\n    }\n  },\n\n  // Handle pm.execution.setNextRequest(null)\n  {\n    pattern: 'pm.execution.setNextRequest',\n    transform: (path, j) => {\n      const callExpr = path.parent.value;\n\n      const args = callExpr.arguments;\n\n      // If argument is null or 'null', transform to bru.runner.stopExecution()\n      if (\n        args[0].type === 'Literal' && (args[0].value === null || args[0].value === 'null')\n      ) {\n        return j.callExpression(\n          j.identifier('bru.runner.stopExecution'),\n          []\n        );\n      }\n\n      // Otherwise, keep as bru.runner.setNextRequest with the same argument\n      return j.callExpression(\n        j.identifier('bru.runner.setNextRequest'),\n        args\n      );\n    }\n  },\n\n  // pm.cookies.has(name) → await bru.cookies.jar().hasCookie(req.getUrl(), name)\n  {\n    pattern: 'pm.cookies.has',\n    transform: (path, j) => {\n      const callExpr = path.parent.value;\n      const args = callExpr.arguments;\n\n      const hasCookieCall = j.callExpression(\n        j.identifier('bru.cookies.jar().hasCookie'),\n        [j.identifier('req.getUrl()'), ...args]\n      );\n\n      return j.awaitExpression(hasCookieCall);\n    }\n  },\n\n  // pm.cookies.get(name) → (await bru.cookies.jar().getCookie(req.getUrl(), name))?.value\n  {\n    pattern: 'pm.cookies.get',\n    transform: (path, j) => {\n      const callExpr = path.parent.value;\n      const args = callExpr.arguments;\n\n      const getCookieCall = j.callExpression(\n        j.identifier('bru.cookies.jar().getCookie'),\n        [j.identifier('req.getUrl()'), ...args]\n      );\n\n      const awaitExpr = j.awaitExpression(getCookieCall);\n      const parenAwait = j.parenthesizedExpression\n        ? j.parenthesizedExpression(awaitExpr)\n        : awaitExpr;\n\n      return j.optionalMemberExpression(\n        parenAwait,\n        j.identifier('value'),\n        false,\n        true\n      );\n    }\n  },\n\n  // pm.cookies.toObject() → (await bru.cookies.jar().getCookies(req.getUrl())).reduce((obj, c) => ({...obj, [c.key]: c.value}), {})\n  {\n    pattern: 'pm.cookies.toObject',\n    transform: (path, j) => {\n      const getCookiesCall = j.callExpression(\n        j.identifier('bru.cookies.jar().getCookies'),\n        [j.identifier('req.getUrl()')]\n      );\n\n      const awaitExpr = j.awaitExpression(getCookiesCall);\n\n      // Build the reduce callback: (obj, c) => ({...obj, [c.key]: c.value})\n      const objParam = j.identifier('obj');\n      const cParam = j.identifier('c');\n\n      const spreadElement = j.spreadElement(objParam);\n      const computedProp = j.property(\n        'init',\n        j.memberExpression(cParam, j.identifier('key')),\n        j.memberExpression(cParam, j.identifier('value'))\n      );\n      computedProp.computed = true;\n\n      const objectExpr = j.objectExpression([spreadElement, computedProp]);\n\n      const arrowBody = j.parenthesizedExpression\n        ? j.parenthesizedExpression(objectExpr)\n        : objectExpr;\n\n      const reduceFn = j.arrowFunctionExpression(\n        [objParam, cParam],\n        arrowBody\n      );\n      reduceFn.expression = true;\n\n      // Build: (await ...).reduce(fn, {})\n      return j.callExpression(\n        j.memberExpression(\n          awaitExpr,\n          j.identifier('reduce')\n        ),\n        [reduceFn, j.objectExpression([])]\n      );\n    }\n  },\n\n  // pm.globals.has requires special handling\n  {\n    pattern: 'pm.globals.has',\n    transform: (path, j) => {\n      const callExpr = path.parent.value;\n      const args = callExpr.arguments;\n\n      // Create: bru.getGlobalEnvVar(arg) !== undefined && bru.getGlobalEnvVar(arg) !== null\n      return j.logicalExpression(\n        '&&',\n        j.binaryExpression(\n          '!==',\n          j.callExpression(j.identifier('bru.getGlobalEnvVar'), args),\n          j.identifier('undefined')\n        ),\n        j.binaryExpression(\n          '!==',\n          j.callExpression(j.identifier('bru.getGlobalEnvVar'), args),\n          j.identifier('null')\n        )\n      );\n    }\n  },\n\n  // pm.request.headers.add({key, value}) -> req.setHeader(key, value)\n  {\n    pattern: 'pm.request.headers.add',\n    transform: (path, j) => {\n      const callExpr = path.parent.value;\n      const args = callExpr.arguments;\n\n      // Check if the argument is an object with key and value properties\n      if (args.length > 0 && args[0].type === 'ObjectExpression') {\n        const obj = args[0];\n        let keyProp = null;\n        let valueProp = null;\n\n        obj.properties.forEach((prop) => {\n          if (prop.key.name === 'key' || prop.key.value === 'key') {\n            keyProp = prop.value;\n          }\n          if (prop.key.name === 'value' || prop.key.value === 'value') {\n            valueProp = prop.value;\n          }\n        });\n\n        if (keyProp && valueProp) {\n          return j.callExpression(\n            j.identifier('req.setHeader'),\n            [keyProp, valueProp]\n          );\n        }\n      }\n\n      // Fallback: keep original args\n      return j.callExpression(j.identifier('req.setHeader'), args);\n    }\n  },\n\n  // pm.request.headers.upsert({key, value}) -> req.setHeader(key, value)\n  {\n    pattern: 'pm.request.headers.upsert',\n    transform: (path, j) => {\n      const callExpr = path.parent.value;\n      const args = callExpr.arguments;\n\n      // Check if the argument is an object with key and value properties\n      if (args.length > 0 && args[0].type === 'ObjectExpression') {\n        const obj = args[0];\n        let keyProp = null;\n        let valueProp = null;\n\n        obj.properties.forEach((prop) => {\n          if (prop.key.name === 'key' || prop.key.value === 'key') {\n            keyProp = prop.value;\n          }\n          if (prop.key.name === 'value' || prop.key.value === 'value') {\n            valueProp = prop.value;\n          }\n        });\n\n        if (keyProp && valueProp) {\n          return j.callExpression(\n            j.identifier('req.setHeader'),\n            [keyProp, valueProp]\n          );\n        }\n      }\n\n      // Fallback: keep original args\n      return j.callExpression(j.identifier('req.setHeader'), args);\n    }\n  },\n\n  // pm.response.to.be.ok -> expect(res.getStatus()).to.be.within(200, 299)\n  {\n    pattern: 'pm.response.to.be.ok',\n    transform: (path, j) => {\n      return j.callExpression(\n        j.memberExpression(\n          j.callExpression(j.identifier('expect'), [j.callExpression(j.identifier('res.getStatus'), [])]),\n          j.identifier('to.be.within')\n        ),\n        [j.literal(200), j.literal(299)]\n      );\n    }\n  },\n\n  // pm.response.to.be.success -> expect(res.getStatus()).to.be.within(200, 299)\n  {\n    pattern: 'pm.response.to.be.success',\n    transform: (path, j) => {\n      return j.callExpression(\n        j.memberExpression(\n          j.callExpression(j.identifier('expect'), [j.callExpression(j.identifier('res.getStatus'), [])]),\n          j.identifier('to.be.within')\n        ),\n        [j.literal(200), j.literal(299)]\n      );\n    }\n  },\n\n  // pm.response.to.be.redirection -> expect(res.getStatus()).to.be.within(300, 399)\n  {\n    pattern: 'pm.response.to.be.redirection',\n    transform: (path, j) => {\n      return j.callExpression(\n        j.memberExpression(\n          j.callExpression(j.identifier('expect'), [j.callExpression(j.identifier('res.getStatus'), [])]),\n          j.identifier('to.be.within')\n        ),\n        [j.literal(300), j.literal(399)]\n      );\n    }\n  },\n\n  // pm.response.to.be.clientError -> expect(res.getStatus()).to.be.within(400, 499)\n  {\n    pattern: 'pm.response.to.be.clientError',\n    transform: (path, j) => {\n      return j.callExpression(\n        j.memberExpression(\n          j.callExpression(j.identifier('expect'), [j.callExpression(j.identifier('res.getStatus'), [])]),\n          j.identifier('to.be.within')\n        ),\n        [j.literal(400), j.literal(499)]\n      );\n    }\n  },\n\n  // pm.response.to.be.serverError -> expect(res.getStatus()).to.be.within(500, 599)\n  {\n    pattern: 'pm.response.to.be.serverError',\n    transform: (path, j) => {\n      return j.callExpression(\n        j.memberExpression(\n          j.callExpression(j.identifier('expect'), [j.callExpression(j.identifier('res.getStatus'), [])]),\n          j.identifier('to.be.within')\n        ),\n        [j.literal(500), j.literal(599)]\n      );\n    }\n  },\n\n  // pm.response.to.be.error -> expect(res.getStatus()).to.be.at.least(400)\n  {\n    pattern: 'pm.response.to.be.error',\n    transform: (path, j) => {\n      return j.callExpression(\n        j.memberExpression(\n          j.callExpression(j.identifier('expect'), [j.callExpression(j.identifier('res.getStatus'), [])]),\n          j.identifier('to.be.at.least')\n        ),\n        [j.literal(400)]\n      );\n    }\n  },\n\n  // pm.response.to.have.jsonBody(path) -> expect(res.getBody()).to.have.nested.property(path)\n  {\n    pattern: 'pm.response.to.have.jsonBody',\n    transform: (path, j) => {\n      const callExpr = path.parent.value;\n      const args = callExpr.arguments;\n\n      if (args.length === 0) {\n        // No path provided, just check that body exists\n        return j.memberExpression(\n          j.callExpression(j.identifier('expect'), [j.callExpression(j.identifier('res.getBody'), [])]),\n          j.identifier('to.exist')\n        );\n      } else if (args.length === 1) {\n        // Path provided, check property exists\n        return j.callExpression(\n          j.memberExpression(\n            j.callExpression(j.identifier('expect'), [j.callExpression(j.identifier('res.getBody'), [])]),\n            j.identifier('to.have.nested.property')\n          ),\n          args\n        );\n      } else {\n        // Path and value provided, check property equals value\n        return j.callExpression(\n          j.memberExpression(\n            j.callExpression(j.identifier('expect'), [j.callExpression(j.identifier('res.getBody'), [])]),\n            j.identifier('to.have.nested.property')\n          ),\n          args\n        );\n      }\n    }\n  },\n\n  // Legacy postman.getResponseHeader(name) -> res.getHeader(name)\n  {\n    pattern: 'pm.getResponseHeader',\n    transform: (path, j) => {\n      const callExpr = path.parent.value;\n      const args = callExpr.arguments;\n      return j.callExpression(j.identifier('res.getHeader'), args);\n    }\n  }\n];\n\n// Create a map for complex transformations to enable O(1) lookups\nconst complexTransformationsMap = {};\ncomplexTransformations.forEach((transform) => {\n  complexTransformationsMap[transform.pattern] = transform;\n});\n\nconst varInitsToReplace = new Set(['pm', 'postman', 'pm.request', 'pm.response', 'pm.test', 'pm.expect', 'pm.environment', 'pm.variables', 'pm.collectionVariables', 'pm.execution', 'pm.globals', 'pm.cookies']);\n\n/**\n * Process all transformations (both simple and complex) in the AST in a single pass\n * @param {Object} ast - jscodeshift AST\n * @param {Set} transformedNodes - Set of already transformed nodes\n */\nfunction processTransformations(ast, transformedNodes) {\n  ast.find(j.MemberExpression).forEach((path) => {\n    if (transformedNodes.has(path.node)) return;\n\n    // Get string representation using our utility function\n    const memberExprStr = getMemberExpressionString(path.value);\n\n    // First check for simple transformations (O(1))\n    if (simpleTranslations.hasOwnProperty(memberExprStr)) {\n      const replacement = simpleTranslations[memberExprStr];\n      j(path).replaceWith(j.identifier(replacement));\n      transformedNodes.add(path.node);\n      return; // Skip complex transformation check if simple transformation applied\n    }\n\n    // Then check for complex transformations (O(1))\n    if (complexTransformationsMap.hasOwnProperty(memberExprStr)) {\n      const parentType = path.parent.value.type;\n\n      // Call-based patterns (e.g., pm.response.to.have.jsonBody(\"path\"))\n      if (parentType === 'CallExpression') {\n        const transform = complexTransformationsMap[memberExprStr];\n        const replacement = transform.transform(path, j);\n        if (Array.isArray(replacement)) {\n          // Capture stable references before mutating the AST\n          const parentPath = path.parent;\n          const grandParentPath = parentPath.parent;\n\n          // Replace the original CallExpression with the first node\n          j(parentPath).replaceWith(replacement[0]);\n          transformedNodes.add(replacement[0]);\n          transformedNodes.add(parentPath.node);\n\n          // Insert remaining nodes after the grandparent in reverse order\n          // so that repeated insertAfter on the same anchor yields correct sequence\n          for (let i = replacement.length - 1; i >= 1; i--) {\n            j(grandParentPath).insertAfter(replacement[i]);\n            transformedNodes.add(replacement[i]);\n          }\n        } else {\n          j(path.parent).replaceWith(replacement);\n          transformedNodes.add(path.node);\n          transformedNodes.add(path.parent.node);\n        }\n      } else if (parentType === 'ExpressionStatement') {\n        // Property-access patterns used as statements (e.g., pm.response.to.be.ok;)\n        const transform = complexTransformationsMap[memberExprStr];\n        const replacement = transform.transform(path, j);\n        j(path).replaceWith(replacement);\n        transformedNodes.add(path.node);\n      }\n    }\n  });\n}\n\n/**\n * Translates Postman script code to Bruno script code\n * @param {string} code - The Postman script code to translate\n * @returns {string} The translated Bruno script code\n */\nfunction translateCode(code) {\n  // Replace 'postman' with 'pm' using regex before creating the AST\n  // This is more efficient than an AST traversal\n  code = code.replace(/\\bpostman\\b/g, 'pm');\n\n  const ast = j(code);\n\n  // Keep track of transformed nodes to avoid double-processing\n  const transformedNodes = new Set();\n\n  // Preprocess the code to resolve all aliases\n  preprocessAliases(ast);\n\n  // Handle cookie jar variable assignments and method renaming\n  processCookieJarVariables(ast);\n\n  // Process all transformations in a single pass\n  processTransformations(ast, transformedNodes);\n\n  // Handle legacy Postman global APIs\n  handleLegacyGlobalAPIs(ast, transformedNodes, code);\n\n  // Handle special Postman syntax patterns\n  handleTestsBracketNotation(ast);\n\n  return ast.toSource();\n}\n\n/**\n * Preprocess all variable aliases in the AST to simplify later transformations\n * @param {Object} ast - jscodeshift AST\n */\nfunction preprocessAliases(ast) {\n  // Create a symbol table to track what each variable references\n  const symbolTable = new Map();\n  const MAX_ITERATIONS = 5;\n  let iterations = 0;\n\n  // Keep preprocessing until no more changes can be made\n  let changesMade;\n  do {\n    changesMade = false;\n\n    // First pass: Identify all variables\n    findVariableDefinitions(ast, symbolTable);\n\n    // Second pass: Replace all variable references with their resolved values\n    changesMade = resolveVariableReferences(ast, symbolTable) || false;\n\n    // Third pass: Clean up variable declarations that are no longer needed\n    changesMade = removeResolvedDeclarations(ast, symbolTable) || false;\n\n    iterations++;\n  } while (changesMade && iterations < MAX_ITERATIONS);\n}\n\n/**\n * Find all variable definitions and track what they reference\n * @param {Object} ast - jscodeshift AST\n * @param {Map} symbolTable - Map to track variable references\n */\nfunction findVariableDefinitions(ast, symbolTable) {\n  // Use a single traversal to handle both direct assignments and object destructuring\n  ast.find(j.VariableDeclarator).forEach((path) => {\n    // Only process nodes that have an initializer\n    if (!path.value.init) return;\n\n    // Handle direct assignments: const response = pm.response\n    if (path.value.id.type === 'Identifier') {\n      const varName = path.value.id.name;\n\n      // If it's a direct identifier, just map it\n      if (path.value.init.type === 'Identifier') {\n        symbolTable.set(varName, {\n          type: 'identifier',\n          value: path.value.init.name\n        });\n      } else if (path.value.init.type === 'MemberExpression') {\n        // If it's a member expression, store both parts\n        const sourceCode = getMemberExpressionString(path.value.init);\n        symbolTable.set(varName, {\n          type: 'memberExpression',\n          value: sourceCode,\n          node: path.value.init\n        });\n      }\n    } else if (path.value.id.type === 'ObjectPattern' && path.value.init.type === 'Identifier') {\n      // Handle object destructuring: const { response } = pm\n      const source = path.value.init.name;\n\n      path.value.id.properties.forEach((prop) => {\n        if (prop.key.name && prop.value.type === 'Identifier') {\n          const destVarName = prop.value.name;\n          symbolTable.set(destVarName, {\n            type: 'memberExpression',\n            value: `${source}.${prop.key.name}`,\n            node: j.memberExpression(\n              j.identifier(source),\n              j.identifier(prop.key.name)\n            )\n          });\n        }\n      });\n    }\n  });\n}\n\n/**\n * Resolve variable references by replacing them with their original values\n * @param {Object} ast - jscodeshift AST\n * @param {Map} symbolTable - Map of variable references\n * @returns {boolean} Whether any changes were made\n */\nfunction resolveVariableReferences(ast, symbolTable) {\n  let changesMade = false;\n\n  /**\n   * Example of what this function does:\n   *\n   * Input Postman code:\n   *   const response = pm.response;\n   *   const jsonData = response.json();  // response is a reference to pm.response\n   *\n   * After resolution:\n   *   const response = pm.response;\n   *   const jsonData = pm.response.json();  // response reference is replaced with pm.response\n   *\n   * Then in the next preprocessing phase, unnecessary variables like 'response' will be removed.\n   */\n\n  // Replace all identifier references with their resolved values\n  ast.find(j.Identifier).forEach((path) => {\n    const varName = path.value.name;\n\n    /**\n     * Skip specific types of identifiers that shouldn't be replaced:\n     *\n     * Case 1: Variable definitions (left side of declarations)\n     * -----------------------------------------------------\n     * In code like:\n     *   const response = pm.response;\n     *           ^\n     * We shouldn't replace 'response' on the left side with pm.response,\n     * which would result in: const pm.response = pm.response; (invalid syntax)\n     *\n     * Case 2: Property names in member expressions\n     * -----------------------------------------------------\n     * In code like:\n     *   console.log(response.status);\n     *                       ^\n     * We shouldn't replace the 'status' property name with anything,\n     * only the 'response' object reference should be replaced.\n     *\n     * We only want to replace identifiers that are being used as references,\n     * not the ones being defined or used as property names.\n     */\n\n    // Skip if this is a variable definition or property name\n    if (path.parent.value.type === 'VariableDeclarator' && path.parent.value.id === path.value) {\n      return;\n    }\n    if (path.parent.value.type === 'MemberExpression' && path.parent.value.property === path.value && !path.parent.value.computed) {\n      return;\n    }\n\n    // Only replace if this is a known variable\n    if (!symbolTable.has(varName)) return;\n\n    const symbolInfo = symbolTable.get(varName);\n    if (!varInitsToReplace.has(symbolInfo.value)) {\n      return;\n    }\n    const newNode = cloneDeep(symbolInfo.node);\n    j(path).replaceWith(newNode);\n    symbolTable.set(varName, {\n      type: 'memberExpression',\n      value: symbolInfo.value,\n      node: newNode\n    });\n    changesMade = true;\n  });\n\n  return changesMade;\n}\n\n/**\n * Remove variable declarations that have been resolved\n * @param {Object} ast - jscodeshift AST\n * @param {Map} symbolTable - Map of variable references\n * @returns {boolean} Whether any changes were made\n */\nfunction removeResolvedDeclarations(ast, symbolTable) {\n  let changesMade = false;\n\n  /**\n   * Example of what this function does:\n   *\n   * Original Postman code:\n   *   const response = pm.response;\n   *   const jsonData = response.json();\n   *   console.log(jsonData.name);\n   *\n   * After variable resolution:\n   *   const response = pm.response;        // This declaration is now redundant\n   *   const jsonData = pm.response.json(); // This value has been resolved\n   *   console.log(jsonData.name);          // This still references jsonData\n   *\n   * Final code after this cleanup step:\n   *   const jsonData = pm.response.json(); // response variable declaration is removed\n   *   console.log(jsonData.name);          // jsonData is kept since it's still referenced\n   *\n   * We only remove declarations that:\n   * 1. Have been fully resolved (references to pm.* objects)\n   * 2. No longer provide any value (since all references were replaced with resolved values)\n   */\n\n  // Use a single traversal to handle both regular variable declarations and destructuring\n  ast.find(j.VariableDeclarator).forEach((path) => {\n    // Case 1: Handle regular variable declarations\n    if (path.value.id.type === 'Identifier') {\n      const varName = path.value.id.name;\n      const replacement = symbolTable.get(varName);\n      if (!replacement || !varInitsToReplace.has(replacement.value)) return;\n\n      /**\n       * This code differentiates between two types of variable declarations:\n       *\n       * Example 1: Single variable declaration\n       * -----------------------------------\n       * Input:   const response = pm.response;\n       * Action:  The entire statement can be removed\n       * Output:  [statement removed]\n       *\n       * Example 2: Multiple variables in one declaration\n       * -----------------------------------\n       * Input:   const response = pm.response, unrelated = 5;\n       * Action:  Only remove the 'response' declarator, keep the others\n       * Output:  const unrelated = 5;\n       *\n       * We need this distinction to ensure we don't accidentally remove\n       * unrelated variables that happen to be declared in the same statement.\n       */\n      const declarationPath = j(path).closest(j.VariableDeclaration);\n      if (declarationPath.get().value.declarations.length === 1) {\n        declarationPath.remove();\n      } else {\n        // Otherwise just remove this declarator\n        j(path).remove();\n      }\n\n      changesMade = true;\n    } else if (path.value.id.type === 'ObjectPattern'\n      // Case 2: Handle destructuring of pm\n      && path.value.init\n      && path.value.init.type === 'Identifier'\n      && path.value.init.name === 'pm') {\n      /**\n       * Example of destructuring removal:\n       *\n       * Original Postman code:\n       *   const { response, environment } = pm;\n       *   console.log(response.json().name);\n       *   console.log(environment.get(\"variable\"));\n       *\n       * After variable resolution steps:\n       *   const { response, environment } = pm;  // This destructuring is now redundant\n       *   console.log(pm.response.json().name); // 'response' references already replaced with pm.response\n       *   console.log(pm.environment.get(\"variable\")); // 'environment' references replaced\n       *\n       * Final code after this cleanup step:\n       *   console.log(pm.response.json().name); // Destructuring declaration is completely removed\n       *   console.log(pm.environment.get(\"variable\"));\n       *\n       * This step specifically targets the Postman pattern of destructuring the pm object,\n       * which is common in Postman scripts but needs to be removed in the Bruno conversion.\n       */\n\n      const declarationPath = j(path).closest(j.VariableDeclaration);\n      if (declarationPath.get().value.declarations.length === 1) {\n        declarationPath.remove();\n      } else {\n        j(path).remove();\n      }\n\n      changesMade = true;\n    }\n  });\n\n  return changesMade;\n}\n\n/**\n * Process cookie jar variable assignments and rename methods on those variables\n * @param {Object} ast - jscodeshift AST\n */\nfunction processCookieJarVariables(ast) {\n  // Map of Postman cookie jar method names to Bruno equivalents\n  const cookieMethodMapping = {\n    get: 'getCookie',\n    getAll: 'getCookies',\n    set: 'setCookie',\n    unset: 'deleteCookie',\n    clear: 'deleteCookies'\n  };\n\n  // Track variables that are assigned to cookie jar instances\n  const cookieJarVariables = new Set();\n\n  // First pass: Find all variables assigned to cookie jar instances\n  ast.find(j.VariableDeclarator).forEach((path) => {\n    if (path.value.init && path.value.init.type === 'CallExpression') {\n      const initCall = path.value.init;\n\n      // Check if this is a cookie jar assignment\n      if (initCall.callee.type === 'MemberExpression') {\n        const calleeStr = getMemberExpressionString(initCall.callee);\n\n        if (calleeStr === 'pm.cookies.jar' || calleeStr === 'bru.cookies.jar') {\n          if (path.value.id.type === 'Identifier') {\n            cookieJarVariables.add(path.value.id.name);\n          }\n        }\n      }\n    }\n  });\n\n  // Second pass: Rename method calls on cookie jar variables\n  ast.find(j.CallExpression).forEach((path) => {\n    if (path.value.callee.type === 'MemberExpression'\n      && path.value.callee.object.type === 'Identifier'\n      && path.value.callee.property.type === 'Identifier') {\n      const varName = path.value.callee.object.name;\n      const methodName = path.value.callee.property.name;\n\n      // If this is a method call on a cookie jar variable\n      if (cookieJarVariables.has(varName) && cookieMethodMapping[methodName]) {\n        const newMethodName = cookieMethodMapping[methodName];\n        path.value.callee.property.name = newMethodName;\n      }\n    }\n  });\n}\n\n/**\n * Handle Postman's tests[\"...\"] = ... syntax\n * @param {Object} ast - jscodeshift AST\n */\nfunction handleTestsBracketNotation(ast) {\n  // Find the ExpressionStatement that contains the assignment\n  ast.find(j.ExpressionStatement, {\n    expression: {\n      type: 'AssignmentExpression',\n      left: {\n        type: 'MemberExpression',\n        object: { name: 'tests' },\n        computed: true,\n        property: {} // Accept any property type\n      }\n    }\n  }).forEach((path) => {\n    // Get the assignment expression\n    const assignment = path.value.expression;\n    const left = assignment.left;\n\n    // Verify it's a valid tests[] expression\n    if (left.object.type === 'Identifier'\n      && left.object.name === 'tests'\n      && left.computed === true) {\n      const property = left.property;\n      const rightSide = assignment.right;\n\n      // Handle string literals\n      if (property.type === 'Literal' && typeof property.value === 'string') {\n        const testName = property.value;\n\n        // Replace with test() function call\n        j(path).replaceWith(\n          j.expressionStatement(\n            j.callExpression(\n              j.identifier('test'),\n              [\n                j.literal(testName),\n                j.functionExpression(\n                  null,\n                  [],\n                  j.blockStatement([\n                    j.expressionStatement(\n                      j.memberExpression(\n                        j.callExpression(\n                          j.identifier('expect'),\n                          [\n                            j.callExpression(\n                              j.identifier('Boolean'),\n                              [rightSide]\n                            )\n                          ]\n                        ),\n                        j.identifier('to.be.true')\n                      )\n                    )\n                  ])\n                )\n              ]\n            )\n          )\n        );\n      } else if (property.type === 'TemplateLiteral') {\n        // Handle template literals\n        // Create a template literal with the same quasi and expressions\n        const templateLiteral = j.templateLiteral(\n          property.quasis,\n          property.expressions\n        );\n\n        // Replace with test() function call using template literal\n        j(path).replaceWith(\n          j.expressionStatement(\n            j.callExpression(\n              j.identifier('test'),\n              [\n                templateLiteral,\n                j.functionExpression(\n                  null,\n                  [],\n                  j.blockStatement([\n                    j.expressionStatement(\n                      j.memberExpression(\n                        j.callExpression(\n                          j.identifier('expect'),\n                          [\n                            j.callExpression(\n                              j.identifier('Boolean'),\n                              [rightSide]\n                            )\n                          ]\n                        ),\n                        j.identifier('to.be.true')\n                      )\n                    )\n                  ])\n                )\n              ]\n            )\n          )\n        );\n      }\n    }\n  });\n}\n\n/**\n * Handle legacy Postman global API transformations\n * This function processes legacy Postman globals like responseBody, responseHeaders, responseTime\n * while preserving user-defined variables with the same names\n *\n * @param {Object} ast - jscodeshift AST\n * @param {Set} transformedNodes - Set of already transformed nodes\n * @param {string} code - The original Postman script code\n */\nfunction handleLegacyGlobalAPIs(ast, transformedNodes, code) {\n  // regex check before the ast traversal\n  const legacyGlobalRegex = /responseBody|responseHeaders|responseTime/;\n\n  if (!legacyGlobalRegex.test(code)) {\n    return;\n  }\n\n  // Check for variable declarations with legacy global names - track which ones have conflicts\n  const conflictingNames = new Set();\n\n  // Check variable declarations\n  ast.find(j.VariableDeclarator).forEach((path) => {\n    if (path.value.id.type === 'Identifier') {\n      const varName = path.value.id.name;\n      if (legacyGlobalRegex.test(varName)) {\n        conflictingNames.add(varName);\n      }\n    }\n  });\n\n  // Handle JSON.parse(responseBody) → res.getBody()\n  // Only transform if responseBody doesn't have a user variable conflict\n  if (!conflictingNames.has('responseBody')) {\n    ast.find(j.CallExpression).forEach((path) => {\n      if (transformedNodes.has(path.node)) return;\n\n      const callExpr = path.value;\n      if (callExpr.callee.type === 'MemberExpression' && callExpr.callee.object.name === 'JSON' && callExpr.callee.property.name === 'parse') {\n        const args = callExpr.arguments;\n\n        // Check if the argument is 'responseBody'\n        if (args.length > 0 && args[0].type === 'Identifier' && args[0].name === 'responseBody') {\n          // Replace JSON.parse(responseBody) with res.getBody()\n          j(path).replaceWith(j.identifier('res.getBody()'));\n          transformedNodes.add(path.node);\n        }\n      }\n    });\n  }\n\n  // Handle standalone legacy Postman global variables\n  const legacyGlobals = [\n    { name: 'responseBody', replacement: 'res.getBody()' },\n    { name: 'responseHeaders', replacement: 'res.getHeaders()' },\n    { name: 'responseTime', replacement: 'res.getResponseTime()' }\n  ];\n\n  legacyGlobals.forEach(({ name, replacement }) => {\n    // Skip transformation if this name has a user variable conflict\n    if (conflictingNames.has(name)) {\n      return;\n    }\n\n    ast.find(j.Identifier, { name }).forEach((path) => {\n      if (transformedNodes.has(path.node)) return;\n\n      // Only transform identifiers that are being used as values, not as variable names\n      const parent = path.parent.value;\n\n      // Skip if this is part of a variable declaration (const responseBody = ...)\n      if (parent.type === 'VariableDeclarator' && parent.id === path.node) {\n        return; // Keep unchanged\n      }\n\n      // Skip if this is part of an assignment (responseBody = ...)\n      if (parent.type === 'AssignmentExpression' && parent.left === path.node) {\n        return; // Keep unchanged\n      }\n\n      // Skip if this is part of a function parameter\n      if (parent.type === 'FunctionDeclaration' || parent.type === 'FunctionExpression') {\n        return; // Keep unchanged\n      }\n\n      // Skip if this is part of an object property\n      if (parent.type === 'Property' && (parent.key === path.node || parent.value === path.node)) {\n        return; // Keep unchanged\n      }\n\n      // Transform all other references (including function call arguments)\n      // This will transform console.log(responseBody) → console.log(res.getBody())\n      j(path).replaceWith(j.identifier(replacement));\n      transformedNodes.add(path.node);\n    });\n  });\n}\n\nexport { getMemberExpressionString };\nexport default translateCode;\n"
  },
  {
    "path": "packages/bruno-converters/src/utils/send-request-transformer.js",
    "content": "/**\n * Convert Postman header array format to Bruno headers object\n * @param {Object} j - jscodeshift API\n * @param {Object} arrayValue - Array expression of key-value pair objects\n * @returns {Object} - Object expression with key-value pairs\n */\nconst convertArrayToObject = (j, arrayValue) => {\n  const obj = j.objectExpression([]);\n\n  if (arrayValue.type === 'ArrayExpression') {\n    arrayValue.elements.forEach((elem) => {\n      if (elem.type === 'ObjectExpression') {\n        const keyProp = elem.properties.find((p) => (p.key.name === 'key' || p.key.value === 'key'));\n        const valueProp = elem.properties.find((p) => (p.key.name === 'value' || p.key.value === 'value'));\n\n        if (keyProp && valueProp) {\n          obj.properties.push(\n            j.property(\n              'init',\n              j.literal(keyProp.value.value),\n              valueProp.value\n            )\n          );\n        }\n      }\n    });\n  }\n\n  return obj;\n};\n\n/**\n * Add or update a specific header in the request options\n * @param {Object} j - jscodeshift API\n * @param {Object} requestOptions - Request options object\n * @param {string} headerName - Header name to add/update\n * @param {string} headerValue - Header value\n */\nconst addOrUpdateHeader = (j, requestOptions, headerName, headerValue) => {\n  let headersProp = requestOptions.properties.find((p) => (p.key.name === 'headers' || p.key.value === 'headers'));\n\n  if (!headersProp) {\n    headersProp = j.property('init', j.identifier('headers'), j.objectExpression([]));\n    requestOptions.properties.push(headersProp);\n  } else if (headersProp.value.type !== 'ObjectExpression') {\n    headersProp.value = j.objectExpression([]);\n  }\n\n  // filter out existing header with same name (case-insensitive)\n  headersProp.value.properties = headersProp.value.properties.filter((p) =>\n    p.key.type !== 'Literal'\n    || p.key.value.toLowerCase() !== headerName.toLowerCase()\n  );\n\n  headersProp.value.properties.push(\n    j.property(\n      'init',\n      j.literal(headerName),\n      j.literal(headerValue)\n    )\n  );\n};\n\n/**\n * Transform headers property from array to object format\n * @param {Object} j - jscodeshift API\n * @param {Object} requestOptions - Request options object\n */\nconst transformHeaders = (j, requestOptions) => {\n  if (requestOptions.type !== 'ObjectExpression') return;\n\n  requestOptions.properties.forEach((prop) => {\n    // find and rename 'header' property to 'headers'\n    if (prop.key.name === 'header' || prop.key.value === 'header') {\n      prop.key.name = 'headers';\n      prop.key.value = 'headers';\n\n      // Handle array of header objects\n      if (prop.value.type === 'ArrayExpression') {\n        prop.value = convertArrayToObject(j, prop.value);\n      }\n    }\n  });\n};\n\n/**\n * Transform body property based on body mode\n * @param {Object} j - jscodeshift API\n * @param {Object} requestOptions - Request options object\n * @returns {Array|null} - Array of statements if formdata is used, null otherwise\n */\nconst transformBody = (j, requestOptions) => {\n  if (requestOptions.type !== 'ObjectExpression') return null;\n\n  requestOptions.properties.forEach((prop) => {\n    if (prop.key.name === 'body' || prop.key.value === 'body') {\n      if (prop.value.type === 'ObjectExpression') {\n        const bodyProps = prop.value.properties;\n        const modeProp = bodyProps.find((p) => (p.key.name === 'mode' || p.key.value === 'mode'));\n\n        if (modeProp && modeProp.value.type === 'Literal') {\n          const bodyMode = modeProp.value.value;\n\n          // Handle raw mode (text, json, xml, etc.)\n          if (bodyMode === 'raw') {\n            const rawProp = bodyProps.find((p) => (p.key.name === 'raw' || p.key.value === 'raw'));\n\n            if (rawProp) {\n              // Replace body with data\n              prop.key.name = 'data';\n              prop.key.value = 'data';\n              prop.value = rawProp.value;\n            }\n          } else if (bodyMode === 'urlencoded') {\n            // Handle urlencoded mode\n            const urlencodedProp = bodyProps.find((p) => (p.key.name === 'urlencoded' || p.key.value === 'urlencoded') && p.value.type === 'ArrayExpression');\n\n            if (urlencodedProp) {\n              // Replace the body property with a 'data' property\n              prop.key.name = 'data';\n              prop.key.value = 'data';\n\n              // Transform the urlencoded array to an object\n              prop.value = convertArrayToObject(j, urlencodedProp.value);\n\n              // Add Content-Type header for urlencoded\n              addOrUpdateHeader(j, requestOptions, 'Content-Type', 'application/x-www-form-urlencoded');\n            }\n          } else if (bodyMode === 'formdata') {\n            // Handle formdata mode\n            const formdataProp = bodyProps.find((p) => (p.key.name === 'formdata' || p.key.value === 'formdata') && p.value.type === 'ArrayExpression');\n\n            if (formdataProp) {\n              // Replace the body property with a 'data' property\n              prop.key.name = 'data';\n              prop.key.value = 'data';\n\n              // Transform the urlencoded array to an object\n              prop.value = convertArrayToObject(j, formdataProp.value);\n\n              // Add Content-Type header for urlencoded\n              addOrUpdateHeader(j, requestOptions, 'Content-Type', 'multipart/form-data');\n            }\n          }\n        }\n      }\n    }\n  });\n};\n\n/**\n * Transform callback function to Bruno format\n * @param {Object} j - jscodeshift API\n * @param {Object} callback - Callback function expression\n * @returns {Object} - Transformed callback function\n */\nconst transformCallback = (j, callback) => {\n  if (!callback || (callback.type !== 'FunctionExpression' && callback.type !== 'ArrowFunctionExpression')) return null;\n\n  const params = callback.params;\n  const callbackBody = callback.body;\n\n  // Get the response parameter name (typically the second param)\n  let responseVarName = 'response'; // Default if not found\n  if (params.length >= 2 && params[1].type === 'Identifier') {\n    responseVarName = params[1].name;\n  }\n\n  let errorVarName = 'error'; // Default if not found\n  if (params.length >= 1 && params[0].type === 'Identifier') {\n    errorVarName = params[0].name;\n  }\n\n  // Define translations for callback response properties\n  const responsePropertyMap = {\n    json: 'data',\n    text: 'data',\n    code: 'status',\n    status: 'statusText'\n  };\n\n  // Process the callback body to transform response property references\n  j(callbackBody).find(j.MemberExpression, {\n    object: {\n      type: 'Identifier',\n      name: responseVarName\n    }\n  }).forEach((memberPath) => {\n    const property = memberPath.node.property;\n\n    // Handle property access\n    if (property.type === 'Identifier' && responsePropertyMap[property.name]) {\n      const bruProperty = responsePropertyMap[property.name];\n      if (bruProperty) {\n        // Check if memberPath is part of a CallExpression\n        const parentPath = memberPath.parent;\n        if (parentPath && parentPath.node.type === 'CallExpression') {\n          // Replace the entire CallExpression with a property access\n          j(parentPath).replaceWith(\n            j.memberExpression(\n              j.identifier(responseVarName),\n              j.identifier(bruProperty)\n            )\n          );\n        } else {\n          // Regular property access replacement\n          j(memberPath).replaceWith(\n            j.memberExpression(\n              j.identifier(responseVarName),\n              j.identifier(bruProperty)\n            )\n          );\n        }\n      }\n    }\n  });\n\n  // Create the callback block\n  return j.functionExpression(\n    null,\n    [j.identifier(errorVarName), j.identifier(responseVarName)],\n    j.blockStatement(callbackBody.body)\n  );\n};\n\n/**\n * Find and transform variable declaration for request config\n * @param {Object} j - jscodeshift API\n * @param {Object} root - Root AST node\n * @param {string} variableName - Name of the variable to find\n * @param {Set} visited - Set of visited variable names to prevent infinite loops\n * @returns {Object|null} - Transformed object expression or null if not found\n */\nconst findAndTransformVariableDeclaration = (j, root, variableName, visited = new Set()) => {\n  // Prevent infinite loops from circular references\n  if (visited.has(variableName)) {\n    return null;\n  }\n  visited.add(variableName);\n\n  let transformedConfig = null;\n\n  // Find the variable declaration\n  root.find(j.VariableDeclarator, {\n    id: { name: variableName }\n  }).forEach((declaratorPath) => {\n    const init = declaratorPath.value.init;\n\n    if (init && init.type === 'ObjectExpression') {\n      // Found the actual object expression - clone and transform it\n      const configClone = j(init).at(0).get().value;\n\n      // Transform headers and body\n      transformHeaders(j, configClone);\n      transformBody(j, configClone);\n\n      transformedConfig = configClone;\n    } else if (init && init.type === 'Identifier') {\n      // This variable references another variable - follow the chain\n      const referencedVariableName = init.name;\n      transformedConfig = findAndTransformVariableDeclaration(j, root, referencedVariableName, visited);\n    }\n  });\n\n  return transformedConfig;\n};\n\nconst sendRequestTransformer = (path, j) => {\n  const callExpr = path.parent.value;\n  if (callExpr.type !== 'CallExpression') return;\n\n  // Clone the argument object for modification\n  const args = [...callExpr.arguments];\n  if (!args.length) return;\n\n  const requestOptions = args[0];\n  const callback = args[1];\n\n  // Check if original call was awaited\n  const wasAwaited = path.parent.parent.value.type === 'AwaitExpression';\n\n  // transform the request config options\n  if (requestOptions.type === 'ObjectExpression') {\n    // Transform headers\n    transformHeaders(j, requestOptions);\n    // Transform body\n    transformBody(j, requestOptions);\n  } else if (requestOptions.type === 'Identifier') {\n    // Handle case where requestOptions is a variable reference\n    const variableName = requestOptions.name;\n\n    // Find the root of the current file/program\n    const root = j(path).closest(j.Program);\n\n    // Find and transform the variable declaration\n    findAndTransformVariableDeclaration(j, root, variableName);\n  }\n\n  // Create the callback block and promise chain if there's a callback\n  if (callback) {\n    const transformedCallback = transformCallback(j, callback);\n\n    // Add async keyword to the callback function\n    if (transformedCallback && (transformedCallback.type === 'FunctionExpression' || transformedCallback.type === 'ArrowFunctionExpression')) {\n      transformedCallback.async = true;\n    }\n\n    // Create expression: await bru.sendRequest(requestConfig, callback);\n    const sendRequestCall = j.callExpression(\n      j.identifier('bru.sendRequest'),\n      transformedCallback ? [requestOptions, transformedCallback] : [requestOptions]\n    );\n\n    return wasAwaited ? sendRequestCall : j.awaitExpression(sendRequestCall);\n  }\n\n  // If there's no callback, just transform to await bru.sendRequest\n  const sendRequestCall = j.callExpression(\n    j.identifier('bru.sendRequest'),\n    [requestOptions]\n  );\n\n  return wasAwaited ? sendRequestCall : j.awaitExpression(sendRequestCall);\n};\n\nexport default sendRequestTransformer;\n"
  },
  {
    "path": "packages/bruno-converters/src/workers/postman-translator-worker.js",
    "content": "const { Worker } = require('node:worker_threads');\nconst path = require('node:path');\nconst os = require('node:os');\n\nfunction getMaxWorkers() {\n  return Math.max(os.availableParallelism(), 1);\n}\n\nclass WorkerPool {\n  constructor(scriptPath, size) {\n    this.workers = [];\n    this.idle = [];\n    this.queue = [];\n    this.scriptPath = scriptPath;\n    this.size = size;\n  }\n\n  // Initialize the worker pool\n  initialize() {\n    for (let i = 0; i < this.size; i++) {\n      const worker = new Worker(this.scriptPath);\n      this.workers.push(worker);\n      this.idle.push(i);\n    }\n  }\n\n  // Run a task on a worker\n  runTask(data) {\n    return new Promise((resolve, reject) => {\n      const task = { data, resolve, reject };\n\n      if (this.idle.length > 0) {\n        this._runTaskOnWorker(this.idle.shift(), task);\n      } else {\n        this.queue.push(task);\n      }\n    });\n  }\n\n  // Run a task on a specific worker\n  _runTaskOnWorker(workerId, task) {\n    const worker = this.workers[workerId];\n\n    const messageHandler = (result) => {\n      // Cleanup listeners\n      worker.removeListener('message', messageHandler);\n      worker.removeListener('error', errorHandler);\n\n      // Mark worker as idle\n      this.idle.push(workerId);\n\n      // Process queue if tasks are waiting\n      if (this.queue.length > 0) {\n        this._runTaskOnWorker(workerId, this.queue.shift());\n      }\n\n      // Resolve the task\n      task.resolve(result);\n    };\n\n    const errorHandler = (err) => {\n      worker.removeListener('message', messageHandler);\n      worker.removeListener('error', errorHandler);\n\n      this.idle.push(workerId);\n\n      if (this.queue.length > 0) {\n        this._runTaskOnWorker(workerId, this.queue.shift());\n      }\n\n      task.reject(err);\n    };\n\n    worker.on('message', messageHandler);\n    worker.on('error', errorHandler);\n    worker.postMessage(task.data);\n  }\n\n  // Terminate all workers\n  terminate() {\n    for (const worker of this.workers) {\n      worker.terminate();\n    }\n    this.workers = [];\n    this.idle = [];\n  }\n}\n\n// Helper function to count lines in a script\nfunction countScriptLines(script) {\n  if (!script) return 0;\n  return Array.isArray(script) ? script.length : script.split('\\n').length;\n}\n\n// Calculate complexity of a script entry\nfunction calculateScriptComplexity([uid, entry]) {\n  let totalLines = 0;\n  const { events } = entry;\n\n  if (events && Array.isArray(events)) {\n    events.forEach(({ script }) => {\n      if (script && script.exec) {\n        totalLines += countScriptLines(script.exec);\n      }\n    });\n  }\n\n  return { uid, entry, complexity: totalLines || 1 }; // Minimum complexity of 1\n}\n\n// Create balanced batches based on script complexity\nfunction createBalancedBatches(scriptEntries, workerCount) {\n  // Calculate complexity for each script\n  const scriptsWithComplexity = scriptEntries.map(calculateScriptComplexity);\n\n  // Sort scripts by complexity (descending)\n  scriptsWithComplexity.sort((a, b) => b.complexity - a.complexity);\n\n  // Initialize batches\n  const batches = Array.from({ length: workerCount }, () => ({\n    entries: [],\n    totalComplexity: 0\n  }));\n\n  // Algorithm: Greedy load balancing\n  // 1. Process scripts in descending order of complexity\n  // 2. Always assign each script to the batch with lowest current load\n  // 3. This minimizes the maximum workload across all workers\n  for (const { uid, entry, complexity } of scriptsWithComplexity) {\n    const batchWithLowestComplexity = batches.reduce(\n      (target, current) => current.totalComplexity < target.totalComplexity ? current : target\n    );\n\n    // Add the script to this batch\n    batchWithLowestComplexity.entries.push({ uid, entry });\n    batchWithLowestComplexity.totalComplexity += complexity;\n  }\n\n  return batches.map((batch) =>\n    batch.entries.map(({ uid, entry }) => [uid, entry])\n  ).filter((batch) => batch.length > 0);\n}\n\nconst scriptTranslationWorker = async (scriptMap) => {\n  // Convert the Map to an array of entries\n  const scriptEntries = Array.from(scriptMap.entries());\n  const maxWorkers = getMaxWorkers();\n\n  // For very small collections, don't parallelize\n  if (scriptEntries.length <= 50) {\n    const workerPool = new WorkerPool(path.join(__dirname, './src/workers/scripts/translate-postman-scripts.js'), 1);\n    workerPool.initialize();\n\n    try {\n      const translatedScripts = new Map();\n      const result = await workerPool.runTask({ scripts: scriptEntries });\n\n      if (result.error) {\n        console.error('Error in script translation worker:', result.error);\n        throw new Error(result.error);\n      }\n\n      result.forEach(([uid, { request }]) => {\n        translatedScripts.set(uid, { request });\n      });\n\n      return translatedScripts;\n    } finally {\n      workerPool.terminate();\n    }\n  }\n\n  const workerCount = Math.min(maxWorkers, 4);\n\n  // Create balanced batches based on script complexity\n  const batches = createBalancedBatches(scriptEntries, workerCount);\n\n  const translatedScripts = new Map();\n\n  // Create worker pool with optimal size\n  const workerPool = new WorkerPool(path.join(__dirname, './src/workers/scripts/translate-postman-scripts.js'), workerCount);\n  workerPool.initialize();\n\n  // Process all batches in parallel using worker pool\n  const batchPromises = batches.map((batch) => {\n    return workerPool.runTask({ scripts: batch })\n      .then((modScripts) => {\n        modScripts.forEach(([name, { request }]) => {\n          translatedScripts.set(name, { request });\n        });\n      })\n      .catch((err) => {\n        console.error('Error in script translation worker:', err);\n        throw new Error(err);\n      });\n  });\n\n  // Wait for all batches to complete\n  try {\n    await Promise.allSettled(batchPromises);\n  } finally {\n    // Clean up worker pool\n    workerPool.terminate();\n  }\n\n  return translatedScripts;\n};\n\nexport default scriptTranslationWorker;\n"
  },
  {
    "path": "packages/bruno-converters/src/workers/scripts/translate-postman-scripts.js",
    "content": "const { parentPort } = require('node:worker_threads');\nconst { postmanTranslation } = require('@usebruno/converters');\n\nparentPort.on('message', (workerData) => {\n  try {\n    const { scripts } = workerData;\n    const modScripts = scripts.map(([uid, { events }]) => {\n      const requestObject = {\n        script: {}\n      };\n\n      if (events && Array.isArray(events)) {\n        events.forEach((event) => {\n          if (event?.script && event.script.exec) {\n            if (event.listen === 'prerequest') {\n              if (event.script.exec && event.script.exec.length > 0) {\n                requestObject.script.req = postmanTranslation(event.script.exec);\n              } else {\n                requestObject.script.req = '';\n              }\n            }\n\n            if (event.listen === 'test') {\n              if (event.script.exec && event.script.exec.length > 0) {\n                requestObject.script.res = postmanTranslation(event.script.exec);\n              } else {\n                requestObject.script.res = '';\n              }\n            }\n          }\n        });\n      }\n\n      return [uid, { request: requestObject }];\n    });\n\n    parentPort.postMessage(modScripts);\n  } catch (error) {\n    console.error(error);\n    parentPort.postMessage({ error: error?.message });\n  }\n});\n"
  },
  {
    "path": "packages/bruno-converters/src/wsdl/wsdl-to-bruno.js",
    "content": "// Custom UID generator for alphanumeric IDs (no hyphens)\nconst generateUID = () => {\n  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n  let result = '';\n  for (let i = 0; i < 21; i++) {\n    result += chars.charAt(Math.floor(Math.random() * chars.length));\n  }\n  return result;\n};\n\nimport { get, each } from 'lodash';\nimport { collectionSchema } from '@usebruno/schema';\n\n// --- Inlined from src/common/index.js ---\nexport const validateSchema = (collection = {}) => {\n  try {\n    collectionSchema.validateSync(collection);\n    return collection;\n  } catch (err) {\n    throw new Error('The Collection has an invalid schema: ' + err.message);\n  }\n};\n\nexport const transformItemsInCollection = (collection) => {\n  const transformItems = (items = []) => {\n    each(items, (item) => {\n      if (['http', 'graphql'].includes(item.type)) {\n        item.type = `${item.type}-request`;\n        if (item.request.query) {\n          item.request.params = item.request.query.map((queryItem) => ({\n            ...queryItem,\n            type: 'query',\n            uid: queryItem.uid || generateUID()\n          }));\n        }\n        delete item.request.query;\n        let multipartFormData = get(item, 'request.body.multipartForm');\n        if (multipartFormData) {\n          each(multipartFormData, (form) => {\n            if (!form.type) {\n              form.type = 'text';\n            }\n          });\n        }\n      }\n      // Handle already transformed types\n      if (['http-request', 'graphql-request'].includes(item.type)) {\n        if (item.request.query) {\n          item.request.params = item.request.query.map((queryItem) => ({\n            ...queryItem,\n            type: 'query',\n            uid: queryItem.uid || generateUID()\n          }));\n        }\n        delete item.request.query;\n        let multipartFormData = get(item, 'request.body.multipartForm');\n        if (multipartFormData) {\n          each(multipartFormData, (form) => {\n            if (!form.type) {\n              form.type = 'text';\n            }\n          });\n        }\n      }\n      if (item.items && item.items.length) {\n        transformItems(item.items);\n      }\n    });\n  };\n  transformItems(collection.items);\n  return collection;\n};\n\nconst isItemARequest = (item) => {\n  return ['http-request', 'graphql-request'].includes(item.type);\n};\n\nexport const hydrateSeqInCollection = (collection) => {\n  const hydrateSeq = (items = []) => {\n    let index = 1;\n    each(items, (item) => {\n      if (isItemARequest(item) && !item.seq) {\n        item.seq = index;\n        index++;\n      }\n      if (item.items && item.items.length) {\n        hydrateSeq(item.items);\n      }\n    });\n  };\n  hydrateSeq(collection.items);\n  return collection;\n};\n// --- End inlined ---\n\n// Use a simple XML parser for Node.js environment\nconst parseXML = (xmlString) => {\n  const parser = new (require('xml2js')).Parser({\n    explicitArray: false,\n    ignoreAttrs: false,\n    mergeAttrs: true,\n    xmlns: false\n  });\n\n  return new Promise((resolve, reject) => {\n    parser.parseString(xmlString, (err, result) => {\n      if (err) {\n        reject(err);\n      } else {\n        resolve(result);\n      }\n    });\n  });\n};\n\nconst addSuffixToDuplicateName = (item, index, allItems) => {\n  // Check if the request name already exist and if so add a number suffix\n  const nameSuffix = allItems.reduce((nameSuffix, otherItem, otherIndex) => {\n    if (otherItem.name === item.name && otherIndex < index) {\n      nameSuffix++;\n    }\n    return nameSuffix;\n  }, 0);\n  return nameSuffix !== 0 ? `${item.name}_${nameSuffix}` : item.name;\n};\n\n/**\n * Enhanced WSDL Parser based on wizdler approach\n */\nclass WSDLParser {\n  constructor() {\n    this.types = new Map();\n    this.elements = new Map();\n    this.complexTypes = new Map();\n    this.simpleTypes = new Map();\n    this.messages = new Map();\n    this.portTypes = new Map();\n    this.bindings = new Map();\n    this.services = new Map();\n    this.namespaces = new Map();\n  }\n\n  /**\n   * Parse WSDL content and extract all components\n   */\n  async parse(wsdlContent) {\n    const result = await parseXML(wsdlContent);\n    const definitions = result['wsdl:definitions'] || result.definitions;\n\n    if (!definitions) {\n      throw new Error('No definitions found in WSDL');\n    }\n\n    // Extract namespaces\n    this.extractNamespaces(definitions);\n\n    // Parse types (XSD schemas)\n    if (definitions['wsdl:types'] || definitions.types) {\n      this.parseTypes(definitions['wsdl:types'] || definitions.types);\n    }\n\n    // Parse messages\n    this.parseMessages(definitions);\n\n    // Parse port types\n    this.parsePortTypes(definitions);\n\n    // Parse bindings\n    this.parseBindings(definitions);\n\n    // Parse services\n    this.parseServices(definitions);\n\n    return {\n      targetNamespace: definitions.targetNamespace || '',\n      name: definitions.name || 'WSDL Service',\n      types: this.types,\n      elements: this.elements,\n      complexTypes: this.complexTypes,\n      simpleTypes: this.simpleTypes,\n      messages: this.messages,\n      portTypes: this.portTypes,\n      bindings: this.bindings,\n      services: this.services,\n      namespaces: this.namespaces\n    };\n  }\n\n  /**\n   * Extract all namespaces from WSDL\n   */\n  extractNamespaces(definitions) {\n    // Extract from xmlns attributes\n    for (const [key, value] of Object.entries(definitions)) {\n      if (key.startsWith('xmlns:')) {\n        const prefix = key.substring(6);\n        this.namespaces.set(prefix, value);\n      } else if (key === 'xmlns') {\n        this.namespaces.set('', value);\n      }\n    }\n  }\n\n  /**\n   * Parse WSDL types section (XSD schemas)\n   */\n  parseTypes(typesNode) {\n    if (!typesNode) return;\n\n    const schemas = this.getArray(typesNode['xsd:schema'] || typesNode.schema);\n\n    for (const schema of schemas) {\n      const targetNamespace = schema.targetNamespace || '';\n\n      // Parse complex types FIRST (so they can be referenced by elements)\n      const complexTypes = this.getArray(schema['xsd:complexType'] || schema.complexType);\n      for (const complexType of complexTypes) {\n        this.parseComplexType(complexType, targetNamespace);\n      }\n\n      // Parse simple types\n      const simpleTypes = this.getArray(schema['xsd:simpleType'] || schema.simpleType);\n      for (const simpleType of simpleTypes) {\n        this.parseSimpleType(simpleType, targetNamespace);\n      }\n\n      // Parse elements LAST (so they can reference complex types)\n      const elements = this.getArray(schema['xsd:element'] || schema.element);\n      for (const element of elements) {\n        this.parseElement(element, targetNamespace);\n      }\n    }\n  }\n\n  /**\n   * Parse an XSD element\n   */\n  parseElement(element, namespace) {\n    const key = `${namespace}:${element.name}`;\n    const parsedElement = {\n      name: element.name,\n      namespace: namespace,\n      type: element.type,\n      minOccurs: element.minOccurs,\n      maxOccurs: element.maxOccurs,\n      nillable: element.nillable,\n      form: element.form,\n      attributes: [],\n      elements: []\n    };\n\n    // Handle inline complex type (recursively parse children)\n    if (element['xsd:complexType'] || element.complexType) {\n      const complexType = element['xsd:complexType'] || element.complexType;\n      // Recursively parse sequence/choice/all children as elements\n      if (complexType['xsd:sequence'] || complexType.sequence) {\n        const sequence = complexType['xsd:sequence'] || complexType.sequence;\n        const children = this.getArray(sequence['xsd:element'] || sequence.element);\n        for (const child of children) {\n          // Recursively parse child element\n          parsedElement.elements.push(this.parseElementInline(child, namespace));\n        }\n      }\n      if (complexType['xsd:choice'] || complexType.choice) {\n        const choice = complexType['xsd:choice'] || complexType.choice;\n        const children = this.getArray(choice['xsd:element'] || choice.element);\n        for (const child of children) {\n          parsedElement.elements.push(this.parseElementInline(child, namespace));\n        }\n      }\n      if (complexType['xsd:all'] || complexType.all) {\n        const all = complexType['xsd:all'] || complexType.all;\n        const children = this.getArray(all['xsd:element'] || all.element);\n        for (const child of children) {\n          parsedElement.elements.push(this.parseElementInline(child, namespace));\n        }\n      }\n      // Parse attributes\n      if (complexType['xsd:attribute'] || complexType.attribute) {\n        const attributes = this.getArray(complexType['xsd:attribute'] || complexType.attribute);\n        for (const attr of attributes) {\n          parsedElement.attributes.push({\n            name: attr.name,\n            type: attr.type,\n            use: attr.use,\n            default: attr.default,\n            fixed: attr.fixed,\n            form: attr.form\n          });\n        }\n      }\n    }\n\n    // Handle inline simple type\n    if (element['xsd:simpleType'] || element.simpleType) {\n      const simpleType = element['xsd:simpleType'] || element.simpleType;\n      parsedElement.simpleType = this.parseSimpleTypeContent(simpleType);\n    }\n\n    // Handle referenced complex type - resolve it immediately\n    if (element.type && !element['xsd:complexType'] && !element['xsd:simpleType']) {\n      const typeName = element.type.replace(/^.*:/, '');\n      const complexType = this.findComplexTypeByName(typeName, namespace);\n      if (complexType) {\n        parsedElement.elements = complexType.elements || [];\n        parsedElement.attributes = complexType.attributes || [];\n      }\n    }\n\n    this.elements.set(key, parsedElement);\n    return parsedElement; // for inline recursion\n  }\n\n  /**\n   * Helper for parsing inline child elements (does not add to elements map)\n   */\n  parseElementInline(element, namespace) {\n    const parsedElement = {\n      name: element.name,\n      namespace: namespace,\n      type: element.type,\n      minOccurs: element.minOccurs,\n      maxOccurs: element.maxOccurs,\n      nillable: element.nillable,\n      form: element.form,\n      attributes: [],\n      elements: []\n    };\n    // Inline complex type\n    if (element['xsd:complexType'] || element.complexType) {\n      const complexType = element['xsd:complexType'] || element.complexType;\n      if (complexType['xsd:sequence'] || complexType.sequence) {\n        const sequence = complexType['xsd:sequence'] || complexType.sequence;\n        const children = this.getArray(sequence['xsd:element'] || sequence.element);\n        for (const child of children) {\n          parsedElement.elements.push(this.parseElementInline(child, namespace));\n        }\n      }\n      if (complexType['xsd:choice'] || complexType.choice) {\n        const choice = complexType['xsd:choice'] || complexType.choice;\n        const children = this.getArray(choice['xsd:element'] || choice.element);\n        for (const child of children) {\n          parsedElement.elements.push(this.parseElementInline(child, namespace));\n        }\n      }\n      if (complexType['xsd:all'] || complexType.all) {\n        const all = complexType['xsd:all'] || complexType.all;\n        const children = this.getArray(all['xsd:element'] || all.element);\n        for (const child of children) {\n          parsedElement.elements.push(this.parseElementInline(child, namespace));\n        }\n      }\n      // Parse attributes\n      if (complexType['xsd:attribute'] || complexType.attribute) {\n        const attributes = this.getArray(complexType['xsd:attribute'] || complexType.attribute);\n        for (const attr of attributes) {\n          parsedElement.attributes.push({\n            name: attr.name,\n            type: attr.type,\n            use: attr.use,\n            default: attr.default,\n            fixed: attr.fixed,\n            form: attr.form\n          });\n        }\n      }\n    }\n    // Inline simple type\n    if (element['xsd:simpleType'] || element.simpleType) {\n      const simpleType = element['xsd:simpleType'] || element.simpleType;\n      parsedElement.simpleType = this.parseSimpleTypeContent(simpleType);\n    }\n    // Referenced complex type\n    if (element.type && !element['xsd:complexType'] && !element['xsd:simpleType']) {\n      const typeName = element.type.replace(/^.*:/, '');\n      const complexType = this.findComplexTypeByName(typeName, namespace);\n      if (complexType) {\n        parsedElement.elements = complexType.elements || [];\n        parsedElement.attributes = complexType.attributes || [];\n      }\n    }\n    return parsedElement;\n  }\n\n  /**\n   * Find complex type by name and namespace\n   */\n  findComplexTypeByName(typeName, namespace) {\n    // Try with namespace\n    const key = `${namespace}:${typeName}`;\n    if (this.complexTypes.has(key)) {\n      return this.complexTypes.get(key);\n    }\n\n    // Try without namespace\n    for (const [key, complexType] of this.complexTypes) {\n      if (complexType.name === typeName) {\n        return complexType;\n      }\n    }\n\n    return null;\n  }\n\n  /**\n   * Parse an XSD complex type\n   */\n  parseComplexType(complexType, namespace) {\n    const key = `${namespace}:${complexType.name}`;\n    const parsedComplexType = {\n      name: complexType.name,\n      namespace: namespace,\n      attributes: [],\n      elements: [],\n      mixed: complexType.mixed,\n      abstract: complexType.abstract\n    };\n\n    this.parseComplexTypeContent(complexType, parsedComplexType);\n    this.complexTypes.set(key, parsedComplexType);\n  }\n\n  /**\n   * Parse complex type content (sequence, choice, all, attributes)\n   */\n  parseComplexTypeContent(complexType, target) {\n    // Parse sequence\n    if (complexType['xsd:sequence'] || complexType.sequence) {\n      const sequence = complexType['xsd:sequence'] || complexType.sequence;\n      this.parseSequence(sequence, target);\n    }\n\n    // Parse choice\n    if (complexType['xsd:choice'] || complexType.choice) {\n      const choice = complexType['xsd:choice'] || complexType.choice;\n      this.parseChoice(choice, target);\n    }\n\n    // Parse all\n    if (complexType['xsd:all'] || complexType.all) {\n      const all = complexType['xsd:all'] || complexType.all;\n      this.parseAll(all, target);\n    }\n\n    // Parse attributes\n    if (complexType['xsd:attribute'] || complexType.attribute) {\n      const attributes = this.getArray(complexType['xsd:attribute'] || complexType.attribute);\n      for (const attr of attributes) {\n        target.attributes.push({\n          name: attr.name,\n          type: attr.type,\n          use: attr.use,\n          default: attr.default,\n          fixed: attr.fixed,\n          form: attr.form\n        });\n      }\n    }\n\n    // Handle simple content with extension\n    if (complexType['xsd:simpleContent'] || complexType.simpleContent) {\n      const simpleContent = complexType['xsd:simpleContent'] || complexType.simpleContent;\n      if (simpleContent['xsd:extension'] || simpleContent.extension) {\n        const extension = simpleContent['xsd:extension'] || simpleContent.extension;\n        target.baseType = extension.base;\n\n        // Parse attributes from extension\n        if (extension['xsd:attribute'] || extension.attribute) {\n          const attributes = this.getArray(extension['xsd:attribute'] || extension.attribute);\n          for (const attr of attributes) {\n            target.attributes.push({\n              name: attr.name,\n              type: attr.type,\n              use: attr.use,\n              default: attr.default,\n              fixed: attr.fixed,\n              form: attr.form\n            });\n          }\n        }\n      }\n    }\n\n    // Handle complex content with extension\n    if (complexType['xsd:complexContent'] || complexType.complexContent) {\n      const complexContent = complexType['xsd:complexContent'] || complexType.complexContent;\n      if (complexContent['xsd:extension'] || complexContent.extension) {\n        const extension = complexContent['xsd:extension'] || complexContent.extension;\n        target.baseType = extension.base;\n\n        // Parse content from extension\n        this.parseComplexTypeContent(extension, target);\n      }\n    }\n  }\n\n  /**\n   * Parse sequence content\n   */\n  parseSequence(sequence, target) {\n    const elements = this.getArray(sequence['xsd:element'] || sequence.element);\n    for (const element of elements) {\n      // Use parseElementInline to properly handle inline complex types and attributes\n      const parsedElement = this.parseElementInline(element, target.namespace || '');\n      target.elements.push(parsedElement);\n    }\n  }\n\n  /**\n   * Parse choice content\n   */\n  parseChoice(choice, target) {\n    const elements = this.getArray(choice['xsd:element'] || choice.element);\n    for (const element of elements) {\n      // Use parseElementInline to properly handle inline complex types and attributes\n      const parsedElement = this.parseElementInline(element, target.namespace || '');\n      parsedElement.choice = true;\n      target.elements.push(parsedElement);\n    }\n  }\n\n  /**\n   * Parse all content\n   */\n  parseAll(all, target) {\n    const elements = this.getArray(all['xsd:element'] || all.element);\n    for (const element of elements) {\n      // Use parseElementInline to properly handle inline complex types and attributes\n      const parsedElement = this.parseElementInline(element, target.namespace || '');\n      parsedElement.all = true;\n      target.elements.push(parsedElement);\n    }\n  }\n\n  /**\n   * Parse simple type\n   */\n  parseSimpleType(simpleType, namespace) {\n    const key = `${namespace}:${simpleType.name}`;\n    const parsedSimpleType = {\n      name: simpleType.name,\n      namespace: namespace,\n      ...this.parseSimpleTypeContent(simpleType)\n    };\n    this.simpleTypes.set(key, parsedSimpleType);\n  }\n\n  /**\n   * Parse simple type content\n   */\n  parseSimpleTypeContent(simpleType) {\n    if (simpleType['xsd:restriction'] || simpleType.restriction) {\n      const restriction = simpleType['xsd:restriction'] || simpleType.restriction;\n      return {\n        base: restriction.base,\n        enumeration: this.getArray(restriction['xsd:enumeration'] || restriction.enumeration),\n        pattern: restriction['xsd:pattern'] || restriction.pattern,\n        minLength: restriction['xsd:minLength'] || restriction.minLength,\n        maxLength: restriction['xsd:maxLength'] || restriction.maxLength\n      };\n    }\n    return {};\n  }\n\n  /**\n   * Parse WSDL messages\n   */\n  parseMessages(definitions) {\n    const messages = this.getArray(definitions['wsdl:message'] || definitions.message);\n    for (const message of messages) {\n      const parts = this.getArray(message['wsdl:part'] || message.part);\n      this.messages.set(message.name, {\n        name: message.name,\n        parts: parts.map((part) => ({\n          name: part.name,\n          type: part.type,\n          element: part.element\n        }))\n      });\n    }\n  }\n\n  /**\n   * Parse WSDL port types\n   */\n  parsePortTypes(definitions) {\n    const portTypes = this.getArray(definitions['wsdl:portType'] || definitions.portType);\n    for (const portType of portTypes) {\n      const operations = this.getArray(portType['wsdl:operation'] || portType.operation);\n      this.portTypes.set(portType.name, {\n        name: portType.name,\n        operations: operations.map((op) => ({\n          name: op.name,\n          input: op['wsdl:input'] || op.input,\n          output: op['wsdl:output'] || op.output,\n          fault: this.getArray(op['wsdl:fault'] || op.fault)\n        }))\n      });\n    }\n  }\n\n  /**\n   * Parse WSDL bindings\n   */\n  parseBindings(definitions) {\n    const bindings = this.getArray(definitions['wsdl:binding'] || definitions.binding);\n    for (const binding of bindings) {\n      const operations = this.getArray(binding['wsdl:operation'] || binding.operation);\n      this.bindings.set(binding.name, {\n        name: binding.name,\n        type: binding.type,\n        operations: operations.map((op) => {\n          // Robustly extract soapAction from any soap:operation child element\n          let soapAction = '';\n          for (const key of Object.keys(op)) {\n            if (key.endsWith(':operation')) {\n              const soapOp = op[key];\n              if (Array.isArray(soapOp)) {\n                if (soapOp[0] && soapOp[0].soapAction) {\n                  soapAction = soapOp[0].soapAction;\n                  break;\n                }\n              } else if (soapOp && soapOp.soapAction) {\n                soapAction = soapOp.soapAction;\n                break;\n              }\n            }\n          }\n          return {\n            name: op.name,\n            input: op['wsdl:input'] || op.input,\n            output: op['wsdl:output'] || op.output,\n            fault: this.getArray(op['wsdl:fault'] || op.fault),\n            soapAction: soapAction\n          };\n        })\n      });\n    }\n  }\n\n  /**\n   * Parse WSDL services\n   */\n  parseServices(definitions) {\n    const services = this.getArray(definitions['wsdl:service'] || definitions.service);\n    for (const service of services) {\n      const ports = this.getArray(service['wsdl:port'] || service.port);\n      this.services.set(service.name, {\n        name: service.name,\n        ports: ports.map((port) => ({\n          name: port.name,\n          binding: port.binding,\n          address: this.extractAddress(port)\n        }))\n      });\n    }\n  }\n\n  /**\n   * Extract service address from port\n   */\n  extractAddress(port) {\n    // Try different address formats\n    const address = port['soap:address'] || port['wsdl:address'] || port.address;\n    if (address && address.location) {\n      return address.location;\n    }\n    return '';\n  }\n\n  /**\n   * Helper to ensure array\n   */\n  getArray(item) {\n    if (!item) return [];\n    return Array.isArray(item) ? item : [item];\n  }\n}\n\n/**\n * Enhanced XML Sample Generator based on wizdler approach\n */\nclass XMLSampleGenerator {\n  constructor(wsdlData) {\n    this.wsdlData = wsdlData;\n    this.visitedTypes = new Set();\n  }\n\n  /**\n   * Generate XML sample for an element\n   */\n  generateSample(elementName, namespace = '') {\n    const element = this.findElement(elementName, namespace);\n    if (!element) {\n      return `<!-- Element ${elementName} not found -->`;\n    }\n\n    return this.generateElementSample(element, 0);\n  }\n\n  /**\n   * Find element by name and namespace\n   */\n  findElement(elementName, namespace) {\n    // Try with namespace\n    if (namespace) {\n      const key = `${namespace}:${elementName}`;\n      if (this.wsdlData.elements.has(key)) {\n        return this.wsdlData.elements.get(key);\n      }\n    }\n\n    // Try without namespace\n    for (const [key, element] of this.wsdlData.elements) {\n      if (element.name === elementName) {\n        return element;\n      }\n    }\n\n    return null;\n  }\n\n  /**\n   * Generate sample for an element\n   */\n  generateElementSample(element) {\n    let xml = '';\n\n    // Add comments for optional/repetition elements\n    const minOccurs = parseInt(element.minOccurs) || 1;\n    const maxOccurs = element.maxOccurs || '1';\n\n    if (minOccurs === 0) {\n      xml += `<!--Optional:-->`;\n    }\n\n    if (maxOccurs === 'unbounded' || (typeof maxOccurs === 'number' && maxOccurs > 1)) {\n      xml += `<!--${this.getRepetitionText(minOccurs, maxOccurs)}-->`;\n    }\n\n    // Generate attributes\n    const attributes = this.generateAttributes(element);\n\n    // Generate element content\n    if (this.isSimpleType(element)) {\n      xml += `<${element.name}${attributes}>${this.getSampleValue(element)}</${element.name}>`;\n    } else {\n      xml += `<${element.name}${attributes}>`;\n      xml += this.generateComplexContent(element);\n      xml += `</${element.name}>`;\n    }\n\n    return xml;\n  }\n\n  /**\n   * Recursively collect all attributes from a complex type and its base types\n   */\n  collectAllAttributes(complexType) {\n    let attributes = [];\n    if (complexType && complexType.attributes) {\n      attributes = attributes.concat(complexType.attributes);\n    }\n    // Recursively collect from base type if present\n    if (complexType && complexType.baseType) {\n      const baseTypeName = complexType.baseType.replace(/^.*:/, '');\n      const baseType = this.findComplexType(baseTypeName);\n      if (baseType) {\n        attributes = attributes.concat(this.collectAllAttributes(baseType));\n      }\n    }\n    return attributes;\n  }\n\n  /**\n   * Generate attributes string\n   */\n  generateAttributes(element) {\n    let attributes = [];\n\n    // Add attributes from the element itself\n    if (element.attributes && element.attributes.length > 0) {\n      attributes = attributes.concat(element.attributes);\n    }\n\n    // Add attributes from the referenced complex type (if any, recursively)\n    if (element.type) {\n      const complexType = this.findComplexType(element.type);\n      if (complexType) {\n        const allTypeAttrs = this.collectAllAttributes(complexType);\n        // Avoid duplicates by attribute name\n        const existingNames = new Set(attributes.map((a) => a.name));\n        for (const attr of allTypeAttrs) {\n          if (!existingNames.has(attr.name)) {\n            attributes.push(attr);\n          }\n        }\n      }\n    }\n\n    if (attributes.length > 0) {\n      return ' ' + attributes.map((attr) => `${attr.name}=\"?\"`).join(' ');\n    }\n    return '';\n  }\n\n  /**\n   * Check if element is simple type\n   */\n  isSimpleType(element) {\n    if (element.simpleType) return true;\n\n    const type = element.type;\n    if (!type) return false;\n\n    // Check if it's a built-in simple type\n    const simpleTypes = [\n      'string', 'int', 'integer', 'long', 'short', 'byte', 'boolean', 'float', 'double', 'decimal',\n      'date', 'dateTime', 'time', 'duration', 'gYear', 'gYearMonth', 'gMonth', 'gMonthDay', 'gDay',\n      'hexBinary', 'base64Binary', 'anyURI', 'QName', 'NOTATION', 'normalizedString', 'token',\n      'language', 'Name', 'NCName', 'ID', 'IDREF', 'IDREFS', 'ENTITY', 'ENTITIES', 'NMTOKEN', 'NMTOKENS'\n    ];\n\n    const typeName = type.replace(/^.*:/, '');\n    return simpleTypes.includes(typeName);\n  }\n\n  /**\n   * Get sample value for simple type\n   */\n  getSampleValue(element) {\n    if (element.simpleType && element.simpleType.enumeration && element.simpleType.enumeration.length > 0) {\n      return element.simpleType.enumeration[0].value || '?';\n    }\n\n    const type = element.type;\n    if (!type) return '?';\n\n    const typeName = type.replace(/^.*:/, '');\n\n    switch (typeName) {\n      case 'string': return 'string';\n      case 'int':\n      case 'integer':\n      case 'long':\n      case 'short':\n      case 'byte': return '0';\n      case 'boolean': return 'true';\n      case 'float':\n      case 'double':\n      case 'decimal': return '0.0';\n      case 'date': return '2024-01-01';\n      case 'dateTime': return '2024-01-01T00:00:00Z';\n      case 'time': return '00:00:00';\n      default: return '?';\n    }\n  }\n\n  /**\n   * Generate complex content\n   */\n  generateComplexContent(element) {\n    let xml = '';\n\n    // Handle inline complex type (elements already parsed)\n    if (element.elements && element.elements.length > 0) {\n      for (const child of element.elements) {\n        xml += this.generateElementSample(child);\n      }\n    }\n\n    // Handle referenced complex type - this is the key fix\n    if (element.type) {\n      const complexType = this.findComplexType(element.type);\n      if (complexType) {\n        xml += this.generateComplexTypeSample(complexType);\n      } else {\n        // If we can't find the complex type, try to find it as an element\n        const elementType = this.findElement(element.type.replace(/^.*:/, ''), '');\n        if (elementType) {\n          xml += this.generateElementSample(elementType);\n        }\n      }\n    }\n\n    return xml;\n  }\n\n  /**\n   * Find complex type by name\n   */\n  findComplexType(typeName) {\n    const cleanTypeName = typeName.replace(/^.*:/, '');\n\n    // First try exact match\n    for (const [key, complexType] of this.wsdlData.complexTypes) {\n      if (complexType.name === cleanTypeName) {\n        return complexType;\n      }\n    }\n\n    // Try with namespace prefix\n    for (const [key, complexType] of this.wsdlData.complexTypes) {\n      if (key.endsWith(`:${cleanTypeName}`) || key === cleanTypeName) {\n        return complexType;\n      }\n    }\n\n    return null;\n  }\n\n  /**\n   * Generate sample for complex type\n   */\n  generateComplexTypeSample(complexType) {\n    if (this.visitedTypes.has(complexType.name)) {\n      return '<!-- Recursive type detected -->';\n    }\n\n    this.visitedTypes.add(complexType.name);\n    let xml = '';\n\n    if (complexType.elements && complexType.elements.length > 0) {\n      for (const element of complexType.elements) {\n        xml += this.generateElementSample(element);\n      }\n    }\n\n    this.visitedTypes.delete(complexType.name);\n    return xml;\n  }\n\n  /**\n   * Get repetition text\n   */\n  getRepetitionText(minOccurs, maxOccurs) {\n    if (minOccurs === 0 && maxOccurs === 'unbounded') {\n      return '0 or more repetitions';\n    } else if (minOccurs === 1 && maxOccurs === 'unbounded') {\n      return '1 or more repetitions';\n    } else if (typeof maxOccurs === 'number') {\n      return `${minOccurs} to ${maxOccurs} repetitions:`;\n    } else {\n      return '0 or more repetitions';\n    }\n  }\n}\n\n/**\n * Generate SOAP envelope with example payload\n */\nconst generateSOAPEnvelope = (operation, wsdlData) => {\n  const inputMessage = operation.input?.message || '';\n  const inputMessageName = typeof inputMessage === 'string' && inputMessage.includes(':') ? inputMessage.split(':')[1] : inputMessage;\n\n  // Find the message definition\n  const message = wsdlData.messages.get(inputMessageName);\n  if (!message || !message.parts || message.parts.length === 0) {\n    return '<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"><soap:Body><!-- No message parts found --></soap:Body></soap:Envelope>';\n  }\n\n  const part = message.parts[0];\n  const elementName = part.element || part.type || '';\n\n  if (!elementName) {\n    return '<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"><soap:Body><!-- No element found --></soap:Body></soap:Envelope>';\n  }\n\n  // Extract element name and namespace\n  let name, namespace;\n  if (elementName.includes(':')) {\n    [namespace, name] = elementName.split(':');\n  } else {\n    name = elementName;\n    namespace = '';\n  }\n\n  // Generate XML sample\n  const generator = new XMLSampleGenerator(wsdlData);\n  const xmlSample = generator.generateSample(name, namespace);\n\n  return `<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"><soap:Body>${xmlSample}</soap:Body></soap:Envelope>`;\n};\n\n/**\n * Transform WSDL operation to Bruno request item\n */\nconst transformWSDLOperation = (operation, wsdlData, serviceLocation, index, allOperations, bindingOperation = null) => {\n  // Create a temporary object with the name property for duplicate checking\n  const tempItem = { name: operation.name };\n  const name = addSuffixToDuplicateName(tempItem, index, allOperations);\n  const soapEnvelope = generateSOAPEnvelope(operation, wsdlData);\n\n  // Use soapAction from binding operation if available, otherwise fallback to constructed value\n  let soapAction = '';\n  if (bindingOperation && bindingOperation.soapAction) {\n    soapAction = bindingOperation.soapAction;\n  } else {\n    // Fallback to constructed value\n    soapAction = `\"${wsdlData.targetNamespace || ''}${operation.name}\"`;\n  }\n\n  const brunoRequestItem = {\n    uid: generateUID(),\n    name,\n    type: 'http-request',\n    request: {\n      url: serviceLocation || '',\n      method: 'POST',\n      auth: {\n        mode: 'none',\n        basic: null,\n        bearer: null,\n        digest: null\n      },\n      headers: [\n        {\n          uid: generateUID(),\n          name: 'Content-Type',\n          value: 'text/xml; charset=utf-8',\n          description: '',\n          enabled: true\n        },\n        {\n          uid: generateUID(),\n          name: 'SOAPAction',\n          value: soapAction,\n          description: '',\n          enabled: true\n        }\n      ],\n      params: [],\n      body: {\n        mode: 'xml',\n        json: null,\n        text: null,\n        xml: soapEnvelope,\n        formUrlEncoded: [],\n        multipartForm: []\n      },\n      script: {\n        res: null\n      }\n    }\n  };\n\n  return brunoRequestItem;\n};\n\n/**\n * Parse WSDL collection and transform to Bruno format\n */\nconst parseWSDLCollection = (wsdlData) => {\n  const collection = {\n    uid: generateUID(),\n    version: '1',\n    name: wsdlData.name,\n    items: []\n  };\n\n  // Flatten the structure to avoid duplicate folder names\n  // Group operations by service and port, but create a single folder per service\n  for (const [serviceName, service] of wsdlData.services) {\n    const serviceFolder = {\n      uid: generateUID(),\n      name: serviceName,\n      type: 'folder',\n      items: []\n    };\n\n    // Collect all operations from all ports in this service\n    const allOperations = [];\n\n    for (const port of service.ports) {\n      // Find operations for this port\n      const bindingName = port.binding && typeof port.binding === 'string' && port.binding.includes(':') ? port.binding.split(':')[1] : port.binding;\n      const binding = wsdlData.bindings.get(bindingName);\n\n      if (binding) {\n        const bindingType = binding.type && typeof binding.type === 'string' && binding.type.includes(':') ? binding.type.split(':')[1] : binding.type;\n        const portType = wsdlData.portTypes.get(bindingType);\n\n        if (portType) {\n          for (const portTypeOp of portType.operations) {\n            // Find the corresponding binding operation by name\n            const bindingOp = binding.operations.find((bop) => bop.name === portTypeOp.name);\n            if (bindingOp) {\n              const request = transformWSDLOperation(portTypeOp, wsdlData, port.address, allOperations.length, binding.operations, bindingOp);\n              allOperations.push(request);\n            }\n          }\n        }\n      }\n    }\n\n    // Add all operations directly to the service folder\n    serviceFolder.items = allOperations;\n\n    if (serviceFolder.items.length > 0) {\n      collection.items.push(serviceFolder);\n    }\n  }\n\n  return collection;\n};\n\n/**\n * Convert WSDL content to Bruno collection\n */\nexport const wsdlToBruno = async (wsdlContent) => {\n  try {\n    if (typeof wsdlContent !== 'string') {\n      throw new Error('WSDL content must be a string');\n    }\n\n    // Parse WSDL using enhanced parser\n    const parser = new WSDLParser();\n    const wsdlData = await parser.parse(wsdlContent);\n\n    const collection = parseWSDLCollection(wsdlData);\n    const transformedCollection = transformItemsInCollection(collection);\n    const hydratedCollection = hydrateSeqInCollection(transformedCollection);\n    const validatedCollection = validateSchema(hydratedCollection);\n\n    return validatedCollection;\n  } catch (err) {\n    console.error(err);\n    throw new Error('Import WSDL collection failed: ' + err.message);\n  }\n};\n\nexport { WSDLParser, XMLSampleGenerator };\nexport default wsdlToBruno;\n"
  },
  {
    "path": "packages/bruno-converters/tests/bruno/bruno-to-postman-translations/cookies.test.js",
    "content": "import translateBruToPostman from '../../../src/utils/bruno-to-postman-translator';\n\ndescribe('Bruno to Postman Cookies Translation', () => {\n  // Cookie jar translation\n  it('should translate bru.cookies.jar to pm.cookies.jar', () => {\n    const code = 'const jar = bru.cookies.jar();';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const jar = pm.cookies.jar();');\n  });\n\n  // Cookie method translations with direct jar call chaining\n  it('should translate getCookie to get (direct chaining)', () => {\n    const code = 'const sessionId = bru.cookies.jar().getCookie(\"https://example.com\", \"sessionId\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const sessionId = pm.cookies.jar().get(\"https://example.com\", \"sessionId\");');\n  });\n\n  it('should translate getCookies to getAll (direct chaining)', () => {\n    const code = 'const allCookies = bru.cookies.jar().getCookies(\"https://example.com\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const allCookies = pm.cookies.jar().getAll(\"https://example.com\");');\n  });\n\n  it('should translate setCookie to set (direct chaining)', () => {\n    const code = 'bru.cookies.jar().setCookie(\"https://example.com\", \"token\", \"abc123\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.cookies.jar().set(\"https://example.com\", \"token\", \"abc123\");');\n  });\n\n  it('should translate deleteCookie to unset (direct chaining)', () => {\n    const code = 'bru.cookies.jar().deleteCookie(\"https://example.com\", \"sessionId\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.cookies.jar().unset(\"https://example.com\", \"sessionId\");');\n  });\n\n  it('should translate deleteCookies to clear (direct chaining)', () => {\n    const code = 'bru.cookies.jar().deleteCookies(\"https://example.com\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.cookies.jar().clear(\"https://example.com\");');\n  });\n\n  // Cookie method translations with jar variable\n  it('should translate getCookie to get (jar variable)', () => {\n    const code = `\nconst jar = bru.cookies.jar();\nconst sessionId = jar.getCookie(\"https://example.com\", \"sessionId\");\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('const jar = pm.cookies.jar();');\n    expect(translatedCode).toContain('const sessionId = jar.get(\"https://example.com\", \"sessionId\");');\n  });\n\n  it('should translate getCookies to getAll (jar variable)', () => {\n    const code = `\nconst jar = bru.cookies.jar();\nconst allCookies = jar.getCookies(\"https://example.com\");\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('const jar = pm.cookies.jar();');\n    expect(translatedCode).toContain('const allCookies = jar.getAll(\"https://example.com\");');\n  });\n\n  it('should translate setCookie to set (jar variable)', () => {\n    const code = `\nconst jar = bru.cookies.jar();\njar.setCookie(\"https://example.com\", \"token\", \"abc123\");\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('const jar = pm.cookies.jar();');\n    expect(translatedCode).toContain('jar.set(\"https://example.com\", \"token\", \"abc123\");');\n  });\n\n  it('should translate deleteCookie to unset (jar variable)', () => {\n    const code = `\nconst jar = bru.cookies.jar();\njar.deleteCookie(\"https://example.com\", \"sessionId\");\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('const jar = pm.cookies.jar();');\n    expect(translatedCode).toContain('jar.unset(\"https://example.com\", \"sessionId\");');\n  });\n\n  it('should translate deleteCookies to clear (jar variable)', () => {\n    const code = `\nconst jar = bru.cookies.jar();\njar.deleteCookies(\"https://example.com\");\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('const jar = pm.cookies.jar();');\n    expect(translatedCode).toContain('jar.clear(\"https://example.com\");');\n  });\n\n  // Complex cookie scenarios\n  it('should handle multiple cookie operations together', () => {\n    const code = `\nconst jar = bru.cookies.jar();\nconst domain = \"https://api.example.com\";\n\n// Check existing cookie\nconst existingToken = jar.getCookie(domain, \"authToken\");\n\nif (!existingToken) {\n    // Set new cookie\n    jar.setCookie(domain, \"authToken\", bru.getEnvVar(\"token\"));\n}\n\n// Get all cookies for logging\nconst allCookies = jar.getCookies(domain);\nconsole.log(\"Current cookies:\", allCookies);\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('const jar = pm.cookies.jar();');\n    expect(translatedCode).toContain('const existingToken = jar.get(domain, \"authToken\");');\n    expect(translatedCode).toContain('jar.set(domain, \"authToken\", pm.environment.get(\"token\"));');\n    expect(translatedCode).toContain('const allCookies = jar.getAll(domain);');\n  });\n\n  it('should handle cookie cleanup scenario', () => {\n    const code = `\nconst jar = bru.cookies.jar();\nconst domain = bru.getEnvVar(\"apiDomain\");\n\n// Clear specific cookies\njar.deleteCookie(domain, \"session\");\njar.deleteCookie(domain, \"tempToken\");\n\n// Or clear all cookies\nif (bru.getVar(\"clearAll\") === \"true\") {\n    jar.deleteCookies(domain);\n}\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('const jar = pm.cookies.jar();');\n    expect(translatedCode).toContain('const domain = pm.environment.get(\"apiDomain\");');\n    expect(translatedCode).toContain('jar.unset(domain, \"session\");');\n    expect(translatedCode).toContain('jar.unset(domain, \"tempToken\");');\n    expect(translatedCode).toContain('if (pm.variables.get(\"clearAll\") === \"true\") {');\n    expect(translatedCode).toContain('jar.clear(domain);');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/bruno/bruno-to-postman-translations/environment.test.js",
    "content": "import translateBruToPostman from '../../../src/utils/bruno-to-postman-translator';\n\ndescribe('Bruno to Postman Environment Variable Translation', () => {\n  it('should translate bru.getEnvVar', () => {\n    const code = 'bru.getEnvVar(\"test\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.environment.get(\"test\");');\n  });\n\n  it('should translate bru.setEnvVar', () => {\n    const code = 'bru.setEnvVar(\"test\", \"value\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.environment.set(\"test\", \"value\");');\n  });\n\n  it('should translate bru.deleteEnvVar', () => {\n    const code = 'bru.deleteEnvVar(\"test\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.environment.unset(\"test\");');\n  });\n\n  it('should translate bru.hasEnvVar', () => {\n    const code = 'bru.hasEnvVar(\"apiKey\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.environment.has(\"apiKey\");');\n  });\n\n  it('should translate bru.getEnvName() to pm.environment.name (function to property)', () => {\n    const code = 'const envName = bru.getEnvName();';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const envName = pm.environment.name;');\n  });\n\n  it('should handle nested Postman API calls with environment', () => {\n    const code = 'bru.setEnvVar(\"computed\", bru.getVar(\"base\") + \"-suffix\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.environment.set(\"computed\", pm.variables.get(\"base\") + \"-suffix\");');\n  });\n\n  it('should handle JSON operations with environment variables', () => {\n    const code = 'bru.setEnvVar(\"user\", JSON.stringify({ id: 123, name: \"John\" }));';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.environment.set(\"user\", JSON.stringify({ id: 123, name: \"John\" }));');\n  });\n\n  it('should handle JSON.parse with environment variables', () => {\n    const code = 'const userData = JSON.parse(bru.getEnvVar(\"user\"));';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const userData = JSON.parse(pm.environment.get(\"user\"));');\n  });\n\n  it('should handle all environment variable methods together', () => {\n    const code = `\n// All environment variable methods\nconst token = bru.getEnvVar(\"token\");\nbru.setEnvVar(\"timestamp\", new Date().toISOString());\n\nconsole.log(\\`Token: \\${token}\\`);\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('const token = pm.environment.get(\"token\");');\n    expect(translatedCode).toContain('pm.environment.set(\"timestamp\", new Date().toISOString());');\n  });\n\n  it('should handle environment variables with computed property names', () => {\n    const code = `\nconst prefix = \"api\";\nconst suffix = \"Key\";\nbru.setEnvVar(prefix + \"_\" + suffix, \"abc123\");\nconst computedValue = bru.getEnvVar(prefix + \"_\" + suffix);\n`;\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toContain('pm.environment.set(prefix + \"_\" + suffix, \"abc123\");');\n    expect(translatedCode).toContain('const computedValue = pm.environment.get(prefix + \"_\" + suffix);');\n  });\n\n  it('should handle environment variables in complex object structures', () => {\n    const code = `\nconst config = {\n    baseUrl: bru.getEnvVar(\"apiUrl\"),\n    headers: {\n        \"Authorization\": \"Bearer \" + bru.getEnvVar(\"token\"),\n        \"X-Api-Key\": bru.getEnvVar(\"apiKey\") || \"default-key\"\n    },\n    timeout: parseInt(bru.getEnvVar(\"timeout\") || \"5000\")\n};\n`;\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toContain('baseUrl: pm.environment.get(\"apiUrl\"),');\n    expect(translatedCode).toContain('\"Authorization\": \"Bearer \" + pm.environment.get(\"token\"),');\n    expect(translatedCode).toContain('\"X-Api-Key\": pm.environment.get(\"apiKey\") || \"default-key\"');\n    expect(translatedCode).toContain('timeout: parseInt(pm.environment.get(\"timeout\") || \"5000\")');\n  });\n\n  it('should handle environment variables in try-catch blocks', () => {\n    const code = `\ntry {\n    const configStr = bru.getEnvVar(\"config\");\n    const config = JSON.parse(configStr);\n    console.log(\"Config loaded:\", config.version);\n} catch (error) {\n    console.error(\"Failed to parse config\");\n    bru.setEnvVar(\"configError\", error.message);\n}\n`;\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toContain('const configStr = pm.environment.get(\"config\");');\n    expect(translatedCode).toContain('pm.environment.set(\"configError\", error.message);');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/bruno/bruno-to-postman-translations/execution.test.js",
    "content": "import translateBruToPostman from '../../../src/utils/bruno-to-postman-translator';\n\ndescribe('Bruno to Postman Execution Control Translation', () => {\n  // setNextRequest translations\n  it('should translate bru.setNextRequest', () => {\n    const code = 'bru.setNextRequest(\"Get User Details\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.execution.setNextRequest(\"Get User Details\");');\n  });\n\n  it('should translate bru.runner.setNextRequest', () => {\n    const code = 'bru.runner.setNextRequest(\"Create Order\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.execution.setNextRequest(\"Create Order\");');\n  });\n\n  // skipRequest translation\n  it('should translate bru.runner.skipRequest', () => {\n    const code = 'bru.runner.skipRequest();';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.execution.skipRequest();');\n  });\n\n  // stopExecution translation\n  it('should translate bru.runner.stopExecution() to pm.execution.setNextRequest(null)', () => {\n    const code = 'bru.runner.stopExecution();';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.execution.setNextRequest(null);');\n  });\n\n  // Conditional execution control\n  it('should handle setNextRequest in conditionals', () => {\n    const code = `\nif (res.getStatus() === 401) {\n    bru.setNextRequest(\"Refresh Token\");\n} else if (res.getStatus() === 200) {\n    bru.setNextRequest(\"Process Data\");\n}\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('if (pm.response.code === 401) {');\n    expect(translatedCode).toContain('pm.execution.setNextRequest(\"Refresh Token\");');\n    expect(translatedCode).toContain('} else if (pm.response.code === 200) {');\n    expect(translatedCode).toContain('pm.execution.setNextRequest(\"Process Data\");');\n  });\n\n  it('should handle stopExecution in error handling', () => {\n    const code = `\nif (res.getStatus() >= 500) {\n    console.error(\"Server error, stopping execution\");\n    bru.runner.stopExecution();\n}\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('if (pm.response.code >= 500) {');\n    expect(translatedCode).toContain('console.error(\"Server error, stopping execution\");');\n    expect(translatedCode).toContain('pm.execution.setNextRequest(null);');\n  });\n\n  it('should handle skipRequest with condition', () => {\n    const code = `\nconst shouldSkip = bru.getEnvVar(\"skipNextRequest\") === \"true\";\nif (shouldSkip) {\n    bru.runner.skipRequest();\n}\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('const shouldSkip = pm.environment.get(\"skipNextRequest\") === \"true\";');\n    expect(translatedCode).toContain('if (shouldSkip) {');\n    expect(translatedCode).toContain('pm.execution.skipRequest();');\n  });\n\n  it('should handle all execution control methods together', () => {\n    const code = `\nconst status = res.getStatus();\nconst data = res.getBody();\n\nif (status === 200 && data.hasMore) {\n    bru.setNextRequest(\"Fetch Next Page\");\n} else if (status === 429) {\n    console.log(\"Rate limited, skipping\");\n    bru.runner.skipRequest();\n} else if (status >= 500) {\n    bru.runner.stopExecution();\n}\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('const status = pm.response.code;');\n    expect(translatedCode).toContain('const data = pm.response.json();');\n    expect(translatedCode).toContain('pm.execution.setNextRequest(\"Fetch Next Page\");');\n    expect(translatedCode).toContain('pm.execution.skipRequest();');\n    expect(translatedCode).toContain('pm.execution.setNextRequest(null);');\n  });\n\n  it('should handle dynamic request names in setNextRequest', () => {\n    const code = `\nconst nextRequest = bru.getVar(\"nextRequestName\");\nbru.setNextRequest(nextRequest);\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('const nextRequest = pm.variables.get(\"nextRequestName\");');\n    expect(translatedCode).toContain('pm.execution.setNextRequest(nextRequest);');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/bruno/bruno-to-postman-translations/request.test.js",
    "content": "import translateBruToPostman from '../../../src/utils/bruno-to-postman-translator';\n\ndescribe('Bruno to Postman Request Translation', () => {\n  it('should translate req.getUrl() to pm.request.url (function to property)', () => {\n    const code = 'const url = req.getUrl();';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const url = pm.request.url;');\n  });\n\n  it('should translate req.url to pm.request.url (property to property)', () => {\n    const code = 'const url = req.url;';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const url = pm.request.url;');\n  });\n\n  it('should translate req.method to pm.request.method (property to property)', () => {\n    const code = 'const method = req.method;';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const method = pm.request.method;');\n  });\n\n  it('should translate req.headers to pm.request.headers (property to property)', () => {\n    const code = 'const headers = req.headers;';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const headers = pm.request.headers;');\n  });\n\n  it('should translate req.body to pm.request.body (property to property)', () => {\n    const code = 'const body = req.body;';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const body = pm.request.body;');\n  });\n\n  it('should translate req.getMethod() to pm.request.method (function to property)', () => {\n    const code = 'const method = req.getMethod();';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const method = pm.request.method;');\n  });\n\n  it('should translate req.getHeaders() to pm.request.headers (function to property)', () => {\n    const code = 'const headers = req.getHeaders();';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const headers = pm.request.headers;');\n  });\n\n  it('should translate req.getBody() to pm.request.body (function to property)', () => {\n    const code = 'const body = req.getBody();';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const body = pm.request.body;');\n  });\n\n  it('should translate req.getName() to pm.info.requestName (function to property)', () => {\n    const code = 'const name = req.getName();';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const name = pm.info.requestName;');\n  });\n\n  it('should translate req.getAuthMode() to pm.request.auth.type (function to property)', () => {\n    const code = 'const authMode = req.getAuthMode();';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const authMode = pm.request.auth.type;');\n  });\n\n  it('should handle req.getAuthMode() in conditionals', () => {\n    const code = 'if (req.getAuthMode() === \"oauth2\") { console.log(\"OAuth2 auth\"); }';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('if (pm.request.auth.type === \"oauth2\") { console.log(\"OAuth2 auth\"); }');\n  });\n\n  it('should translate req.getHeader() to pm.request.headers.get()', () => {\n    const code = 'const contentType = req.getHeader(\"Content-Type\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const contentType = pm.request.headers.get(\"Content-Type\");');\n  });\n\n  it('should translate req.setHeader() to pm.request.headers.upsert() with object arg', () => {\n    const code = 'req.setHeader(\"Authorization\", \"Bearer token123\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toContain('pm.request.headers.upsert({\\n  key: \"Authorization\",\\n  value: \"Bearer token123\"\\n})');\n  });\n\n  it('should translate req.deleteHeader() to pm.request.headers.remove()', () => {\n    const code = 'req.deleteHeader(\"Authorization\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.request.headers.remove(\"Authorization\");');\n  });\n\n  it('should handle req.deleteHeader() with a variable argument', () => {\n    const code = 'const headerName = \"X-Custom\"; req.deleteHeader(headerName);';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const headerName = \"X-Custom\"; pm.request.headers.remove(headerName);');\n  });\n\n  it('should handle all request properties together', () => {\n    const code = `\n// All request properties\nconst url = req.getUrl();\nconst method = req.getMethod();\nconst headers = req.getHeaders();\nconst body = req.getBody();\nconst name = req.getName();\n\nconsole.log(\\`Request: \\${method} \\${url} - \\${name}\\`);\n`;\n    const translatedCode = translateBruToPostman(code);\n    const expected = `\n// All request properties\nconst url = pm.request.url;\nconst method = pm.request.method;\nconst headers = pm.request.headers;\nconst body = pm.request.body;\nconst name = pm.info.requestName;\n\nconsole.log(\\`Request: \\${method} \\${url} - \\${name}\\`);\n`;\n    expect(translatedCode.trim()).toBe(expected.trim());\n  });\n\n  it('should handle request properties in conditionals', () => {\n    const code = `\nif (req.getMethod() === 'POST' || req.getMethod() === 'PUT') {\n    const body = req.getBody();\n    console.log(\"Request body:\", body);\n}\n`;\n    const translatedCode = translateBruToPostman(code);\n    const expected = `\nif (pm.request.method === 'POST' || pm.request.method === 'PUT') {\n    const body = pm.request.body;\n    console.log(\"Request body:\", body);\n}\n`;\n    expect(translatedCode.trim()).toBe(expected.trim());\n  });\n\n  it('should handle request logging', () => {\n    const code = `\nconsole.log(\"Making request to:\", req.getUrl());\nconsole.log(\"Method:\", req.getMethod());\nconsole.log(\"Headers:\", JSON.stringify(req.getHeaders()));\n`;\n    const translatedCode = translateBruToPostman(code);\n    const expected = `\nconsole.log(\"Making request to:\", pm.request.url);\nconsole.log(\"Method:\", pm.request.method);\nconsole.log(\"Headers:\", JSON.stringify(pm.request.headers));\n`;\n    expect(translatedCode.trim()).toBe(expected.trim());\n  });\n\n  it('should translate req.setUrl() to pm.request.url assignment', () => {\n    const code = 'req.setUrl(\"https://api.example.com/users\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.request.url = \"https://api.example.com/users\";');\n  });\n\n  it('should translate req.setMethod() to pm.request.method assignment', () => {\n    const code = 'req.setMethod(\"POST\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.request.method = \"POST\";');\n  });\n\n  it('should translate req.setBody() to pm.request.body.update()', () => {\n    const code = 'req.setBody({name: \"John\", age: 30});';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.request.body.update({\\n  mode: \"raw\",\\n  raw: JSON.stringify({name: \"John\", age: 30})\\n});');\n  });\n\n  it('should translate req.setHeaders() to pm.request.headers.upsert() calls', () => {\n    const code = 'req.setHeaders({\"Content-Type\": \"application/json\", \"Authorization\": \"Bearer token\"});';\n    const translatedCode = translateBruToPostman(code);\n    // Should generate an IIFE with a for...in loop that calls upsert for each header\n    expect(translatedCode).toBe('(function() {\\n  const _headers = {\"Content-Type\": \"application/json\", \"Authorization\": \"Bearer token\"};\\n\\n  for (const key in _headers) {\\n    pm.request.headers.upsert({\\n      key: key,\\n      value: _headers[key]\\n    });\\n  }\\n})();');\n  });\n\n  it('should handle req.setUrl() with variable', () => {\n    const code = 'const newUrl = \"https://api.example.com\"; req.setUrl(newUrl);';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const newUrl = \"https://api.example.com\"; pm.request.url = newUrl;');\n  });\n\n  it('should handle req.setMethod() with variable', () => {\n    const code = 'const method = \"PUT\"; req.setMethod(method);';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const method = \"PUT\"; pm.request.method = method;');\n  });\n\n  it('should handle req.setBody() with variable', () => {\n    const code = 'const body = {id: 1}; req.setBody(body);';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const body = {id: 1}; pm.request.body.update({\\n  mode: \"raw\",\\n  raw: JSON.stringify(body)\\n});');\n  });\n\n  // URL helper methods tests\n  it('should translate req.getHost() to pm.request.url.getHost()', () => {\n    const code = 'const host = req.getHost();';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const host = pm.request.url.getHost();');\n  });\n\n  it('should translate req.getPath() to pm.request.url.getPath()', () => {\n    const code = 'const path = req.getPath();';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const path = pm.request.url.getPath();');\n  });\n\n  it('should translate req.getQueryString() to pm.request.url.getQueryString()', () => {\n    const code = 'const queryString = req.getQueryString();';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const queryString = pm.request.url.getQueryString();');\n  });\n\n  it('should translate req.getPathParams() to pm.request.url.variables (function to property)', () => {\n    const code = 'const pathParams = req.getPathParams();';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const pathParams = pm.request.url.variables;');\n  });\n\n  it('should handle URL methods in complex expressions', () => {\n    const code = 'const fullUrl = req.getHost() + req.getPath() + \"?\" + req.getQueryString();';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toContain('pm.request.url.getHost()');\n    expect(translatedCode).toContain('pm.request.url.getPath()');\n    expect(translatedCode).toContain('pm.request.url.getQueryString()');\n  });\n\n  it('should handle req.getPathParams() in conditional', () => {\n    const code = 'if (req.getPathParams().id) { console.log(\"Has ID\"); }';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toContain('pm.request.url.variables.id');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/bruno/bruno-to-postman-translations/response.test.js",
    "content": "import translateBruToPostman from '../../../src/utils/bruno-to-postman-translator';\n\ndescribe('Bruno to Postman Response Translation', () => {\n  // Basic response property tests\n  it('should translate res.getBody()', () => {\n    const code = 'const jsonData = res.getBody();';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const jsonData = pm.response.json();');\n  });\n\n  it('should translate res.getStatus() to pm.response.code (function to property)', () => {\n    const code = 'if (res.getStatus() === 200) { console.log(\"Success\"); }';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('if (pm.response.code === 200) { console.log(\"Success\"); }');\n  });\n\n  it('should translate JSON.stringify(res.getBody()) to pm.response.text()', () => {\n    const code = 'const responseText = JSON.stringify(res.getBody());';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const responseText = pm.response.text();');\n  });\n\n  it('should translate res.getResponseTime() to pm.response.responseTime (function to property)', () => {\n    const code = 'console.log(\"Response time:\", res.getResponseTime());';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('console.log(\"Response time:\", pm.response.responseTime);');\n  });\n\n  it('should translate res.statusText to pm.response.status (property to property)', () => {\n    const code = 'console.log(\"Status text:\", res.statusText);';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('console.log(\"Status text:\", pm.response.status);');\n  });\n\n  it('should translate res.status to pm.response.code (property to property)', () => {\n    const code = 'const code = res.status;';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const code = pm.response.code;');\n  });\n\n  it('should translate res.getStatusText() to pm.response.status (function to property)', () => {\n    const code = 'const statusText = res.getStatusText();';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const statusText = pm.response.status;');\n  });\n\n  it('should translate res.getHeaders() to pm.response.headers (function to property)', () => {\n    const code = 'console.log(\"Headers:\", res.getHeaders());';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('console.log(\"Headers:\", pm.response.headers);');\n  });\n\n  it('should translate res.getUrl() to pm.response.url (function to property)', () => {\n    const code = 'const url = res.getUrl();';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const url = pm.response.url;');\n  });\n\n  it('should translate res.url to pm.response.url (property to property)', () => {\n    const code = 'const url = res.url;';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const url = pm.response.url;');\n  });\n\n  it('should translate res.getHeader()', () => {\n    const code = 'const contentType = res.getHeader(\"Content-Type\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const contentType = pm.response.headers.get(\"Content-Type\");');\n  });\n\n  // Response assertions - translated to pm.expect with response properties\n  it('should transform expect(res.getStatus()).to.equal() to pm.expect(pm.response.code).to.equal()', () => {\n    const code = 'expect(res.getStatus()).to.equal(201);';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.expect(pm.response.code).to.equal(201);');\n  });\n\n  it('should transform expect(res.getHeaders()).to.have.property() to pm.expect(pm.response.headers).to.have.property()', () => {\n    const code = 'expect(res.getHeaders()).to.have.property(\"Content-Type\".toLowerCase());';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.expect(pm.response.headers).to.have.property(\"Content-Type\".toLowerCase());');\n  });\n\n  it('should transform expect(res.getBody()).to.equal() to pm.expect(pm.response.json()).to.equal()', () => {\n    const code = 'expect(res.getBody()).to.equal(\"Expected response body\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.expect(pm.response.json()).to.equal(\"Expected response body\");');\n  });\n\n  // getSize translations\n  it('should translate res.getSize()', () => {\n    const code = 'const size = res.getSize();';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const size = pm.response.size();');\n  });\n\n  it('should translate res.getSize().body', () => {\n    const code = 'const bodySize = res.getSize().body;';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const bodySize = pm.response.size().body;');\n  });\n\n  it('should translate res.getSize().header', () => {\n    const code = 'const headerSize = res.getSize().header;';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const headerSize = pm.response.size().header;');\n  });\n\n  it('should translate res.getSize().total', () => {\n    const code = 'const totalSize = res.getSize().total;';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const totalSize = pm.response.size().total;');\n  });\n\n  // Complex response handling\n  it('should handle response data with destructuring', () => {\n    const code = `\nconst { id, name, items } = res.getBody();\nconst [first, second] = items;\nbru.setEnvVar(\"userId\", id);\n`;\n    const translatedCode = translateBruToPostman(code);\n    const expected = `\nconst { id, name, items } = pm.response.json();\nconst [first, second] = items;\npm.environment.set(\"userId\", id);\n`;\n    expect(translatedCode.trim()).toBe(expected.trim());\n  });\n\n  it('should handle response JSON with optional chaining', () => {\n    const code = `\nconst userId = res.getBody()?.user?.id ?? \"anonymous\";\nconst items = res.getBody()?.data?.items || [];\n`;\n    const translatedCode = translateBruToPostman(code);\n    const expected = `\nconst userId = pm.response.json()?.user?.id ?? \"anonymous\";\nconst items = pm.response.json()?.data?.items || [];\n`;\n    expect(translatedCode.trim()).toBe(expected.trim());\n  });\n\n  it('should handle response in complex conditionals', () => {\n    const code = `\nif (res.getStatus() >= 200 && res.getStatus() < 300) {\n    if (res.getHeader('Content-Type').includes('application/json')) {\n        const data = res.getBody();\n\n        if (data.success === true && data.token) {\n            bru.setEnvVar(\"authToken\", data.token);\n        } else if (data.error) {\n            console.error(\"API error:\", data.error);\n        }\n    }\n} else if (res.getStatus() === 404) {\n    console.log(\"Resource not found\");\n} else {\n    console.error(\"Request failed with status:\", res.getStatus());\n}\n`;\n    const translatedCode = translateBruToPostman(code);\n    const expected = `\nif (pm.response.code >= 200 && pm.response.code < 300) {\n    if (pm.response.headers.get('Content-Type').includes('application/json')) {\n        const data = pm.response.json();\n\n        if (data.success === true && data.token) {\n            pm.environment.set(\"authToken\", data.token);\n        } else if (data.error) {\n            console.error(\"API error:\", data.error);\n        }\n    }\n} else if (pm.response.code === 404) {\n    console.log(\"Resource not found\");\n} else {\n    console.error(\"Request failed with status:\", pm.response.code);\n}\n`;\n    expect(translatedCode.trim()).toBe(expected.trim());\n  });\n\n  it('should handle all response property methods together', () => {\n    const code = `\n// All response property methods\nconst statusCode = res.getStatus();\nconst responseBody = res.getBody();\nconst statusText = res.statusText;\nconst responseTime = res.getResponseTime();\n`;\n    const translatedCode = translateBruToPostman(code);\n    const expected = `\n// All response property methods\nconst statusCode = pm.response.code;\nconst responseBody = pm.response.json();\nconst statusText = pm.response.status;\nconst responseTime = pm.response.responseTime;\n`;\n    expect(translatedCode.trim()).toBe(expected.trim());\n  });\n\n  it('should handle response processing in arrow functions', () => {\n    const code = `\nconst processItems = () => {\n    const items = res.getBody().items;\n    return items.map(item => item.id);\n};\n\nconst itemIds = processItems();\nbru.setEnvVar(\"itemIds\", JSON.stringify(itemIds));\n`;\n    const translatedCode = translateBruToPostman(code);\n    const expected = `\nconst processItems = () => {\n    const items = pm.response.json().items;\n    return items.map(item => item.id);\n};\n\nconst itemIds = processItems();\npm.environment.set(\"itemIds\", JSON.stringify(itemIds));\n`;\n    expect(translatedCode.trim()).toBe(expected.trim());\n  });\n\n  it('should translate res.responseTime property to pm.response.responseTime', () => {\n    const code = 'const time = res.responseTime;';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const time = pm.response.responseTime;');\n  });\n\n  it('should translate res.headers property to pm.response.headers', () => {\n    const code = 'const headers = res.headers;';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const headers = pm.response.headers;');\n  });\n\n  it('should handle res.responseTime in conditionals', () => {\n    const code = 'if (res.responseTime > 1000) { console.log(\"Slow response\"); }';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('if (pm.response.responseTime > 1000) { console.log(\"Slow response\"); }');\n  });\n\n  it('should handle res.headers property access', () => {\n    const code = 'const contentType = res.headers[\"Content-Type\"];';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const contentType = pm.response.headers[\"Content-Type\"];');\n  });\n\n  it('should handle both res.responseTime property and res.getResponseTime() method', () => {\n    const code = `\nconst time1 = res.responseTime;\nconst time2 = res.getResponseTime();\n`;\n    const translatedCode = translateBruToPostman(code);\n    const expected = `\nconst time1 = pm.response.responseTime;\nconst time2 = pm.response.responseTime;\n`;\n    expect(translatedCode.trim()).toBe(expected.trim());\n  });\n\n  it('should handle both res.headers property and res.getHeaders() method', () => {\n    const code = `\nconst headers1 = res.headers;\nconst headers2 = res.getHeaders();\n`;\n    const translatedCode = translateBruToPostman(code);\n    const expected = `\nconst headers1 = pm.response.headers;\nconst headers2 = pm.response.headers;\n`;\n    expect(translatedCode.trim()).toBe(expected.trim());\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/bruno/bruno-to-postman-translations/send-request.test.js",
    "content": "import translateBruToPostman from '../../../src/utils/bruno-to-postman-translator';\n\ndescribe('Bruno to Postman Send Request Translation', () => {\n  describe('Raw Body Mode', () => {\n    it('should transform raw JSON body to Postman format', () => {\n      const code = `\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Accept': 'application/json',\n                'Content-Type': 'application/json',\n            },\n            data: JSON.stringify({\n                \"x\": 1\n            })\n        }, function (error, response) {\n            if (error) {\n                const errorCode = error.code;\n                console.log(errorCode);\n            }\n            if (response) {\n                const response_body = response.data;\n                const response_headers = response.headers;\n                console.log(response_body, response_headers);\n            }\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Accept': 'application/json',\n                'Content-Type': 'application/json',\n            },\n            body: {\n                mode: \"raw\",\n\n                raw: JSON.stringify({\n                    \"x\": 1\n                })\n            }\n        }, function(error, response) {\n            if (error) {\n                const errorCode = error.code;\n                console.log(errorCode);\n            }\n            if (response) {\n                const response_body = response.json();\n                const response_headers = response.headers;\n                console.log(response_body, response_headers);\n            }\n        });\n      `);\n    });\n\n    it('should transform raw text body', () => {\n      const code = `\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Content-Type': 'text/plain',\n            },\n            data: 'Hello World'\n        }, function (error, response) {\n            console.log(response.data);\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Content-Type': 'text/plain',\n            },\n            body: {\n                mode: \"raw\",\n                raw: 'Hello World'\n            }\n        }, function(error, response) {\n            console.log(response.json());\n        });\n      `);\n    });\n\n    it('should transform raw JSON object body', () => {\n      const code = `\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Accept': 'application/json',\n                'Content-Type': 'application/json',\n            },\n            data: {\n                \"x\": 1\n            }\n        }, function (error, response) {\n            if (error) {\n                const errorCode = error.code;\n                console.log(errorCode);\n            }\n            if (response) {\n                const response_body = response.data;\n                const response_headers = response.headers;\n                console.log(response_body, response_headers);\n            }\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Accept': 'application/json',\n                'Content-Type': 'application/json',\n            },\n            body: {\n                mode: \"raw\",\n\n                raw: {\n                    \"x\": 1\n                }\n            }\n        }, function(error, response) {\n            if (error) {\n                const errorCode = error.code;\n                console.log(errorCode);\n            }\n            if (response) {\n                const response_body = response.json();\n                const response_headers = response.headers;\n                console.log(response_body, response_headers);\n            }\n        });\n      `);\n    });\n\n    it('should transform raw body with spread operator (preserved as-is)', () => {\n      const code = `\n        const additionalData = { \"y\": 2, \"z\": 3 };\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/json',\n            },\n            data: {\n                \"x\": 1,\n                ...additionalData\n            }\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      // In raw mode, spread operators are preserved as-is in the object\n      expect(translatedCode).toBe(`\n        const additionalData = { \"y\": 2, \"z\": 3 };\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Content-Type': 'application/json',\n            },\n            body: {\n                mode: \"raw\",\n\n                raw: {\n                    \"x\": 1,\n                    ...additionalData\n                }\n            }\n        });\n      `);\n    });\n  });\n\n  describe('URL-encoded Body Mode', () => {\n    it('should transform urlencoded body with single key-value pair', () => {\n      const code = `\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Accept': 'application/json',\n                'Content-Type': 'application/x-www-form-urlencoded',\n            },\n            data: {\n                \"key\": \"value\"\n            }\n        }, function (error, response) {\n            if (response) {\n                const response_body = response.data;\n                console.log(response_body);\n            }\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Accept': 'application/json',\n                'Content-Type': 'application/x-www-form-urlencoded',\n            },\n            body: {\n                mode: \"urlencoded\",\n\n                urlencoded: [{\n                    key: \"key\",\n                    value: \"value\"\n                }]\n            }\n        }, function(error, response) {\n            if (response) {\n                const response_body = response.json();\n                console.log(response_body);\n            }\n        });\n      `);\n    });\n\n    it('should transform urlencoded body with multiple key-value pairs', () => {\n      const code = `\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/x-www-form-urlencoded',\n            },\n            data: {\n                \"firstName\": \"John\",\n                \"lastName\": \"Doe\",\n                \"email\": \"john.doe@example.com\"\n            }\n        }, function (error, response) {\n            console.log(response.data);\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Content-Type': 'application/x-www-form-urlencoded',\n            },\n            body: {\n                mode: \"urlencoded\",\n\n                urlencoded: [{\n                    key: \"firstName\",\n                    value: \"John\"\n                }, {\n                    key: \"lastName\",\n                    value: \"Doe\"\n                }, {\n                    key: \"email\",\n                    value: \"john.doe@example.com\"\n                }]\n            }\n        }, function(error, response) {\n            console.log(response.json());\n        });\n      `);\n    });\n\n    it('should transform urlencoded body when no Content-Type header exists', () => {\n      const code = `\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            data: {\n                \"key1\": \"value1\",\n                \"key2\": \"value2\"\n            }\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      // Without Content-Type header, defaults to raw mode\n      expect(translatedCode).toBe(`\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            body: {\n                mode: \"raw\",\n\n                raw: {\n                    \"key1\": \"value1\",\n                    \"key2\": \"value2\"\n                }\n            }\n        });\n      `);\n    });\n\n    it('should transform urlencoded body with incorrect Content-Type header', () => {\n      const code = `\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Content-Type': 'text/plain',\n            },\n            data: {\n                \"key1\": \"value1\",\n                \"key2\": \"value2\"\n            }\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      // With text/plain Content-Type, defaults to raw mode\n      expect(translatedCode).toBe(`\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Content-Type': 'text/plain',\n            },\n            body: {\n                mode: \"raw\",\n\n                raw: {\n                    \"key1\": \"value1\",\n                    \"key2\": \"value2\"\n                }\n            }\n        });\n      `);\n    });\n\n    it('should transform urlencoded body with spread operator', () => {\n      const code = `\n        const rest = { \"key3\": \"value3\", \"key4\": \"value4\" };\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/x-www-form-urlencoded',\n            },\n            data: {\n                \"key1\": \"value1\",\n                \"key2\": \"value2\",\n                ...rest\n            }\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        const rest = { \"key3\": \"value3\", \"key4\": \"value4\" };\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Content-Type': 'application/x-www-form-urlencoded',\n            },\n            body: {\n                mode: \"urlencoded\",\n\n                urlencoded: [{\n                    key: \"key1\",\n                    value: \"value1\"\n                }, {\n                    key: \"key2\",\n                    value: \"value2\"\n                }, ...Object.entries(rest).map(([key, value]) => ({\n                    key: key,\n                    value: value\n                }))]\n            }\n        });\n      `);\n    });\n\n    it('should transform urlencoded body with only spread operator', () => {\n      const code = `\n        const rest = { \"key1\": \"value1\", \"key2\": \"value2\" };\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/x-www-form-urlencoded',\n            },\n            data: {\n                ...rest\n            }\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        const rest = { \"key1\": \"value1\", \"key2\": \"value2\" };\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Content-Type': 'application/x-www-form-urlencoded',\n            },\n            body: {\n                mode: \"urlencoded\",\n\n                urlencoded: [...Object.entries(rest).map(([key, value]) => ({\n                    key: key,\n                    value: value\n                }))]\n            }\n        });\n      `);\n    });\n\n    it('should transform urlencoded body with multiple spread operators', () => {\n      const code = `\n        const rest1 = { \"key1\": \"value1\" };\n        const rest2 = { \"key2\": \"value2\" };\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/x-www-form-urlencoded',\n            },\n            data: {\n                ...rest1,\n                \"key3\": \"value3\",\n                ...rest2\n            }\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        const rest1 = { \"key1\": \"value1\" };\n        const rest2 = { \"key2\": \"value2\" };\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Content-Type': 'application/x-www-form-urlencoded',\n            },\n            body: {\n                mode: \"urlencoded\",\n\n                urlencoded: [...Object.entries(rest1).map(([key, value]) => ({\n                    key: key,\n                    value: value\n                })), {\n                    key: \"key3\",\n                    value: \"value3\"\n                }, ...Object.entries(rest2).map(([key, value]) => ({\n                    key: key,\n                    value: value\n                }))]\n            }\n        });\n      `);\n    });\n  });\n\n  describe('Multi-part Form Data Body Mode', () => {\n    it('should transform formdata body with single key-value pair', () => {\n      const code = `\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Content-Type': 'multipart/form-data',\n            },\n            data: {\n                \"key\": \"value\"\n            }\n        }, function (error, response) {\n            if (response) {\n                const response_body = response.data;\n                console.log(response_body);\n            }\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Content-Type': 'multipart/form-data',\n            },\n            body: {\n                mode: \"formdata\",\n\n                formdata: [{\n                    key: \"key\",\n                    value: \"value\"\n                }]\n            }\n        }, function(error, response) {\n            if (response) {\n                const response_body = response.json();\n                console.log(response_body);\n            }\n        });\n      `);\n    });\n\n    it('should transform formdata body with multiple key-value pairs', () => {\n      const code = `\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Content-Type': 'multipart/form-data',\n            },\n            data: {\n                \"firstName\": \"John\",\n                \"lastName\": \"Doe\",\n                \"email\": \"john.doe@example.com\"\n            }\n        }, function (error, response) {\n            if (response) {\n                const response_body = response.data;\n                console.log(response_body);\n            }\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Content-Type': 'multipart/form-data',\n            },\n            body: {\n                mode: \"formdata\",\n\n                formdata: [{\n                    key: \"firstName\",\n                    value: \"John\"\n                }, {\n                    key: \"lastName\",\n                    value: \"Doe\"\n                }, {\n                    key: \"email\",\n                    value: \"john.doe@example.com\"\n                }]\n            }\n        }, function(error, response) {\n            if (response) {\n                const response_body = response.json();\n                console.log(response_body);\n            }\n        });\n      `);\n    });\n\n    it('should transform formdata body when no Content-Type header exists', () => {\n      const code = `\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            data: {\n                \"firstName\": \"John\",\n                \"lastName\": \"Doe\"\n            }\n        }, function (error, response) {\n            if (response) {\n                const response_body = response.data;\n                console.log(response_body);\n            }\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      // Without Content-Type header, defaults to raw mode\n      expect(translatedCode).toBe(`\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            body: {\n                mode: \"raw\",\n\n                raw: {\n                    \"firstName\": \"John\",\n                    \"lastName\": \"Doe\"\n                }\n            }\n        }, function(error, response) {\n            if (response) {\n                const response_body = response.json();\n                console.log(response_body);\n            }\n        });\n      `);\n    });\n\n    it('should transform formdata body with incorrect Content-Type header', () => {\n      const code = `\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Content-Type': 'text/plain',\n            },\n            data: {\n                \"firstName\": \"John\",\n                \"lastName\": \"Doe\"\n            }\n        }, function (error, response) {\n            if (response) {\n                const response_body = response.data;\n                console.log(response_body);\n            }\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      // With text/plain Content-Type, defaults to raw mode\n      expect(translatedCode).toBe(`\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Content-Type': 'text/plain',\n            },\n            body: {\n                mode: \"raw\",\n\n                raw: {\n                    \"firstName\": \"John\",\n                    \"lastName\": \"Doe\"\n                }\n            }\n        }, function(error, response) {\n            if (response) {\n                const response_body = response.json();\n                console.log(response_body);\n            }\n        });\n      `);\n    });\n\n    it('should transform formdata body with spread operator', () => {\n      const code = `\n        const additionalFields = { \"email\": \"john@example.com\", \"phone\": \"123-456-7890\" };\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Content-Type': 'multipart/form-data',\n            },\n            data: {\n                \"firstName\": \"John\",\n                \"lastName\": \"Doe\",\n                ...additionalFields\n            }\n        }, function (error, response) {\n            if (response) {\n                const response_body = response.data;\n                console.log(response_body);\n            }\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        const additionalFields = { \"email\": \"john@example.com\", \"phone\": \"123-456-7890\" };\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Content-Type': 'multipart/form-data',\n            },\n            body: {\n                mode: \"formdata\",\n\n                formdata: [{\n                    key: \"firstName\",\n                    value: \"John\"\n                }, {\n                    key: \"lastName\",\n                    value: \"Doe\"\n                }, ...Object.entries(additionalFields).map(([key, value]) => ({\n                    key: key,\n                    value: value\n                }))]\n            }\n        }, function(error, response) {\n            if (response) {\n                const response_body = response.json();\n                console.log(response_body);\n            }\n        });\n      `);\n    });\n\n    it('should transform formdata body with only spread operator', () => {\n      const code = `\n        const formData = { \"name\": \"John\", \"age\": \"30\" };\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Content-Type': 'multipart/form-data',\n            },\n            data: {\n                ...formData\n            }\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        const formData = { \"name\": \"John\", \"age\": \"30\" };\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Content-Type': 'multipart/form-data',\n            },\n            body: {\n                mode: \"formdata\",\n\n                formdata: [...Object.entries(formData).map(([key, value]) => ({\n                    key: key,\n                    value: value\n                }))]\n            }\n        });\n      `);\n    });\n\n    it('should transform formdata body with spread operator using computed property', () => {\n      const code = `\n        const dynamicKey = \"dynamicField\";\n        const rest = { [dynamicKey]: \"dynamicValue\", \"staticField\": \"staticValue\" };\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Content-Type': 'multipart/form-data',\n            },\n            data: {\n                \"key1\": \"value1\",\n                ...rest\n            }\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        const dynamicKey = \"dynamicField\";\n        const rest = { [dynamicKey]: \"dynamicValue\", \"staticField\": \"staticValue\" };\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Content-Type': 'multipart/form-data',\n            },\n            body: {\n                mode: \"formdata\",\n\n                formdata: [{\n                    key: \"key1\",\n                    value: \"value1\"\n                }, ...Object.entries(rest).map(([key, value]) => ({\n                    key: key,\n                    value: value\n                }))]\n            }\n        });\n      `);\n    });\n  });\n\n  describe('Headers Handling', () => {\n    it('should rename headers property to header', () => {\n      const code = `\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'GET',\n            headers: {\n                'X-Custom-Header': 'custom-value',\n                'Authorization': 'Bearer token'\n            }\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'GET',\n            header: {\n                'X-Custom-Header': 'custom-value',\n                'Authorization': 'Bearer token'\n            }\n        });\n      `);\n    });\n  });\n\n  describe('Response Handling', () => {\n    it('should transform response property access', () => {\n      const code = `\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'GET'\n        }, function (error, response) {\n            const status = response.status;\n            const statusText = response.statusText;\n            const headers = response.headers;\n            const body = response.data;\n\n            if (status === 200) {\n                console.log('Success!');\n            }\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'GET'\n        }, function(error, response) {\n            const status = response.code;\n            const statusText = response.status;\n            const headers = response.headers;\n            const body = response.json();\n\n            if (status === 200) {\n                console.log('Success!');\n            }\n        });\n      `);\n    });\n  });\n\n  describe('Callback Handling', () => {\n    it('should transform callback function', () => {\n      const code = `\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'GET'\n        }, function (error, response) {\n            console.log(response.data);\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'GET'\n        }, function(error, response) {\n            console.log(response.json());\n        });\n      `);\n    });\n\n    it('should handle arrow function callbacks', () => {\n      const code = `\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'GET'\n        }, (error, response) => {\n            console.log(response.data);\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'GET'\n        }, function(error, response) {\n            console.log(response.json());\n        });\n      `);\n    });\n\n    it('should handle async arrow function callbacks', () => {\n      const code = `\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'GET'\n        }, async (error, response) => {\n            await new Promise(resolve => {\n                setTimeout(() => {\n                    resolve();\n                }, 1000)\n            });\n            console.log(response.data);\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'GET'\n        }, async function(error, response) {\n            await new Promise(resolve => {\n                setTimeout(() => {\n                    resolve();\n                }, 1000)\n            });\n            console.log(response.json());\n        });\n      `);\n    });\n  });\n\n  describe('Request Config Variables', () => {\n    it('should transform requestConfig passed as a variable', () => {\n      const code = `\n        const requestConfig = {\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Accept': 'application/json',\n                'Content-Type': 'application/json',\n            },\n            data: JSON.stringify({\n                \"x\": 1\n            })\n        };\n        bru.sendRequest(requestConfig, function (error, response) {\n            if (response) {\n                const response_body = response.data;\n                console.log(response_body);\n            }\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        const requestConfig = {\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Accept': 'application/json',\n                'Content-Type': 'application/json',\n            },\n            body: {\n                mode: \"raw\",\n\n                raw: JSON.stringify({\n                    \"x\": 1\n                })\n            }\n        };\n        pm.sendRequest(requestConfig, function(error, response) {\n            if (response) {\n                const response_body = response.json();\n                console.log(response_body);\n            }\n        });\n      `);\n    });\n\n    it('should transform requestConfig with multi-level variable references', () => {\n      const code = `\n        const requestConfig = {\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/json',\n            },\n            data: { \"x\": 1 }\n        };\n        const requestConfig1 = requestConfig;\n        const requestConfig2 = requestConfig1;\n        bru.sendRequest(requestConfig2, function (error, response) {\n            console.log(response.data);\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        const requestConfig = {\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Content-Type': 'application/json',\n            },\n            body: {\n                mode: \"raw\",\n                raw: { \"x\": 1 }\n            }\n        };\n        const requestConfig1 = requestConfig;\n        const requestConfig2 = requestConfig1;\n        pm.sendRequest(requestConfig2, function(error, response) {\n            console.log(response.json());\n        });\n      `);\n    });\n\n    it('should transform urlencoded body mode with variable config', () => {\n      const code = `\n        const requestConfig = {\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/x-www-form-urlencoded',\n            },\n            data: {\n                \"firstName\": \"John\",\n                \"lastName\": \"Doe\"\n            }\n        };\n        bru.sendRequest(requestConfig, function (error, response) {\n            console.log(response.data);\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        const requestConfig = {\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Content-Type': 'application/x-www-form-urlencoded',\n            },\n            body: {\n                mode: \"urlencoded\",\n\n                urlencoded: [{\n                    key: \"firstName\",\n                    value: \"John\"\n                }, {\n                    key: \"lastName\",\n                    value: \"Doe\"\n                }]\n            }\n        };\n        pm.sendRequest(requestConfig, function(error, response) {\n            console.log(response.json());\n        });\n      `);\n    });\n\n    it('should transform formdata body mode with variable config', () => {\n      const code = `\n        const requestConfig = {\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Content-Type': 'multipart/form-data',\n            },\n            data: {\n                \"firstName\": \"John\",\n                \"lastName\": \"Doe\"\n            }\n        };\n        bru.sendRequest(requestConfig, function (error, response) {\n            console.log(response.data);\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        const requestConfig = {\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Content-Type': 'multipart/form-data',\n            },\n            body: {\n                mode: \"formdata\",\n\n                formdata: [{\n                    key: \"firstName\",\n                    value: \"John\"\n                }, {\n                    key: \"lastName\",\n                    value: \"Doe\"\n                }]\n            }\n        };\n        pm.sendRequest(requestConfig, function(error, response) {\n            console.log(response.json());\n        });\n      `);\n    });\n\n    it('should transform variable config without callback', () => {\n      const code = `\n        const requestConfig = {\n            url: 'https://echo.usebruno.com',\n            method: 'GET',\n            headers: {\n                'Accept': 'application/json'\n            }\n        };\n        bru.sendRequest(requestConfig);\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        const requestConfig = {\n            url: 'https://echo.usebruno.com',\n            method: 'GET',\n            header: {\n                'Accept': 'application/json'\n            }\n        };\n        pm.sendRequest(requestConfig);\n      `);\n    });\n\n    it('should transform variable config with raw text body', () => {\n      const code = `\n        const requestConfig = {\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Content-Type': 'text/plain',\n            },\n            data: 'Hello World'\n        };\n        bru.sendRequest(requestConfig, function (error, response) {\n            console.log(response.data);\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        const requestConfig = {\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Content-Type': 'text/plain',\n            },\n            body: {\n                mode: \"raw\",\n                raw: 'Hello World'\n            }\n        };\n        pm.sendRequest(requestConfig, function(error, response) {\n            console.log(response.json());\n        });\n      `);\n    });\n\n    it('should transform variable config with arrow function callback', () => {\n      const code = `\n        const requestConfig = {\n            url: 'https://echo.usebruno.com',\n            method: 'GET',\n            headers: {\n                'Accept': 'application/json'\n            }\n        };\n        bru.sendRequest(requestConfig, (error, response) => {\n            console.log(response.data);\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        const requestConfig = {\n            url: 'https://echo.usebruno.com',\n            method: 'GET',\n            header: {\n                'Accept': 'application/json'\n            }\n        };\n        pm.sendRequest(requestConfig, function(error, response) {\n            console.log(response.json());\n        });\n      `);\n    });\n\n    it('should transform variable config with async arrow function callback', () => {\n      const code = `\n        const requestConfig = {\n            url: 'https://echo.usebruno.com',\n            method: 'GET',\n            headers: {\n                'Accept': 'application/json'\n            }\n        };\n        bru.sendRequest(requestConfig, async (error, response) => {\n            await new Promise(resolve => setTimeout(resolve, 100));\n            console.log(response.data);\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        const requestConfig = {\n            url: 'https://echo.usebruno.com',\n            method: 'GET',\n            header: {\n                'Accept': 'application/json'\n            }\n        };\n        pm.sendRequest(requestConfig, async function(error, response) {\n            await new Promise(resolve => setTimeout(resolve, 100));\n            console.log(response.json());\n        });\n      `);\n    });\n  });\n\n  describe('Without Callback', () => {\n    it('should transform sendRequest without callback', () => {\n      const code = `\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'GET',\n            headers: {\n                'Accept': 'application/json'\n            }\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'GET',\n            header: {\n                'Accept': 'application/json'\n            }\n        });\n      `);\n    });\n  });\n\n  describe('Simple URL Request', () => {\n    it('should handle string URL argument', () => {\n      const code = `\n        bru.sendRequest('https://echo.usebruno.com');\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        pm.sendRequest('https://echo.usebruno.com');\n      `);\n    });\n  });\n\n  describe('Complex Scenarios', () => {\n    it('should handle sendRequest inside try-catch', () => {\n      const code = `\n        try {\n            bru.sendRequest({\n                url: 'https://echo.usebruno.com',\n                method: 'GET'\n            }, function (error, response) {\n                if (error) {\n                    console.error(error);\n                }\n                console.log(response.data);\n            });\n        } catch (err) {\n            console.error(err);\n        }\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        try {\n            pm.sendRequest({\n                url: 'https://echo.usebruno.com',\n                method: 'GET'\n            }, function(error, response) {\n                if (error) {\n                    console.error(error);\n                }\n                console.log(response.json());\n            });\n        } catch (err) {\n            console.error(err);\n        }\n      `);\n    });\n\n    it('should handle sendRequest with conditional logic in callback', () => {\n      const code = `\n        bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'GET'\n        }, function (error, response) {\n            if (response.status === 200) {\n                const data = response.data;\n                console.log('Success:', data);\n            } else {\n                console.log('Error:', response.statusText);\n            }\n        });\n      `;\n      const translatedCode = translateBruToPostman(code);\n      expect(translatedCode).toBe(`\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'GET'\n        }, function(error, response) {\n            if (response.code === 200) {\n                const data = response.json();\n                console.log('Success:', data);\n            } else {\n                console.log('Error:', response.status);\n            }\n        });\n      `);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/bruno/bruno-to-postman-translations/testing-framework.test.js",
    "content": "import translateBruToPostman from '../../../src/utils/bruno-to-postman-translator';\n\ndescribe('Bruno to Postman Testing Framework Translation', () => {\n  // Basic testing framework translations\n  it('should translate test() to pm.test()', () => {\n    const code = 'test(\"Status code is 200\", function() { expect(res.getStatus()).to.equal(200); });';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.test(\"Status code is 200\", function() { pm.expect(pm.response.code).to.equal(200); });');\n  });\n\n  it('should translate expect() to pm.expect()', () => {\n    const code = 'expect(jsonData.success).to.be.true;';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.expect(jsonData.success).to.be.true;');\n  });\n\n  it('should translate expect.fail() to pm.expect.fail()', () => {\n    const code = 'if (!isValid) expect.fail(\"Data is invalid\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('if (!isValid) pm.expect.fail(\"Data is invalid\");');\n  });\n\n  // Tests with response assertions\n  it('should translate test with status check', () => {\n    const code = `\ntest(\"Check environment and call successful\", function () {\n    expect(bru.getEnvName()).to.equal(\"ENVIRONMENT_NAME\");\n    expect(res.getStatus()).to.equal(200);\n});`;\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe(`\npm.test(\"Check environment and call successful\", function () {\n    pm.expect(pm.environment.name).to.equal(\"ENVIRONMENT_NAME\");\n    pm.expect(pm.response.code).to.equal(200);\n});`);\n  });\n\n  // Test with arrow functions\n  it('should translate test with arrow functions', () => {\n    const code = `\ntest(\"Status code is 200\", () => {\n    expect(res.getStatus()).to.equal(200);\n});\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('pm.test(\"Status code is 200\", () => {');\n    expect(translatedCode).toContain('pm.expect(pm.response.code).to.equal(200);');\n  });\n\n  it('should handle multiple test assertions in one function', () => {\n    const code = `\ntest(\"The response has all properties\", () => {\n    const responseJson = res.getBody();\n    expect(responseJson.type).to.eql('vip');\n    expect(responseJson.name).to.be.a('string');\n    expect(responseJson.id).to.have.lengthOf(1);\n});\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('pm.test(\"The response has all properties\", () => {');\n    expect(translatedCode).toContain('const responseJson = pm.response.json();');\n    expect(translatedCode).toContain('pm.expect(responseJson.type).to.eql(\\'vip\\');');\n    expect(translatedCode).toContain('pm.expect(responseJson.name).to.be.a(\\'string\\');');\n    expect(translatedCode).toContain('pm.expect(responseJson.id).to.have.lengthOf(1);');\n  });\n\n  // Tests inside different code structures\n  it('should translate test commands inside tests with nested functions', () => {\n    const code = `\ntest(\"Auth flow works\", function() {\n    const response = res.getBody();\n    expect(response.authenticated).to.be.true;\n    bru.setEnvVar(\"userId\", response.user.id);\n    bru.setVar(\"sessionId\", response.session.id);\n});\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('pm.test(\"Auth flow works\", function() {');\n    expect(translatedCode).toContain('const response = pm.response.json();');\n    expect(translatedCode).toContain('pm.expect(response.authenticated).to.be.true;');\n    expect(translatedCode).toContain('pm.environment.set(\"userId\", response.user.id);');\n    expect(translatedCode).toContain('pm.variables.set(\"sessionId\", response.session.id);');\n  });\n\n  it('should handle nested test functions', () => {\n    const code = `\ntest(\"Main test group\", function() {\n    const responseJson = res.getBody();\n\n    test(\"User data validation\", function() {\n        expect(responseJson.user).to.be.an('object');\n        expect(responseJson.user.id).to.be.a('string');\n    });\n\n    test(\"Settings validation\", function() {\n        expect(responseJson.settings).to.be.an('object');\n        expect(responseJson.settings.notifications).to.be.a('boolean');\n    });\n});\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('pm.test(\"Main test group\", function() {');\n    expect(translatedCode).toContain('const responseJson = pm.response.json();');\n    expect(translatedCode).toContain('pm.test(\"User data validation\", function() {');\n    expect(translatedCode).toContain('pm.expect(responseJson.user).to.be.an(\\'object\\');');\n    expect(translatedCode).toContain('pm.test(\"Settings validation\", function() {');\n    expect(translatedCode).toContain('pm.expect(responseJson.settings.notifications).to.be.a(\\'boolean\\');');\n  });\n\n  it('should handle test with dynamic test names', () => {\n    const code = `\nconst endpoint = bru.getVar(\"currentEndpoint\");\n\ntest(\\`\\${endpoint} returns correct data\\`, function() {\n    const responseJson = res.getBody();\n    expect(responseJson).to.be.an('object');\n});\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('const endpoint = pm.variables.get(\"currentEndpoint\");');\n    expect(translatedCode).toContain('pm.test(`${endpoint} returns correct data`, function() {');\n    expect(translatedCode).toContain('const responseJson = pm.response.json();');\n    expect(translatedCode).toContain('pm.expect(responseJson).to.be.an(\\'object\\');');\n  });\n\n  it('should handle test with conditional execution', () => {\n    const code = `\nconst responseJson = res.getBody();\n\nif (responseJson.type === 'user') {\n    test(\"User validation\", function() {\n        expect(responseJson.name).to.be.a('string');\n        expect(responseJson.email).to.be.a('string');\n    });\n} else if (responseJson.type === 'admin') {\n    test(\"Admin validation\", function() {\n        expect(responseJson.accessLevel).to.be.above(5);\n        expect(responseJson.permissions).to.be.an('array');\n    });\n}\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('const responseJson = pm.response.json();');\n    expect(translatedCode).toContain('if (responseJson.type === \\'user\\') {');\n    expect(translatedCode).toContain('pm.test(\"User validation\", function() {');\n    expect(translatedCode).toContain('pm.expect(responseJson.name).to.be.a(\\'string\\');');\n    expect(translatedCode).toContain('} else if (responseJson.type === \\'admin\\') {');\n    expect(translatedCode).toContain('pm.test(\"Admin validation\", function() {');\n    expect(translatedCode).toContain('pm.expect(responseJson.accessLevel).to.be.above(5);');\n  });\n\n  it('should handle assertions with logical operators', () => {\n    const code = `\ntest(\"Response has valid structure\", function() {\n    const data = res.getBody();\n\n    expect(data.id && data.name).to.be.ok;\n    expect(data.active || data.pending).to.be.true;\n    expect(!data.deleted).to.be.true;\n});\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('pm.test(\"Response has valid structure\", function() {');\n    expect(translatedCode).toContain('const data = pm.response.json();');\n    expect(translatedCode).toContain('pm.expect(data.id && data.name).to.be.ok;');\n    expect(translatedCode).toContain('pm.expect(data.active || data.pending).to.be.true;');\n    expect(translatedCode).toContain('pm.expect(!data.deleted).to.be.true;');\n  });\n\n  it('should handle array and object assertions', () => {\n    const code = `\ntest(\"Array and object validations\", function() {\n    const data = res.getBody();\n\n    // Array validations\n    expect(data.items).to.be.an('array');\n    expect(data.items).to.have.lengthOf.at.least(1);\n    expect(data.items[0]).to.have.property('id');\n\n    // Object validations\n    expect(data.user).to.be.an('object');\n    expect(data.user).to.have.all.keys('id', 'name', 'email');\n    expect(data.user).to.include({active: true});\n});\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('pm.test(\"Array and object validations\", function() {');\n    expect(translatedCode).toContain('const data = pm.response.json();');\n    expect(translatedCode).toContain('pm.expect(data.items).to.be.an(\\'array\\');');\n    expect(translatedCode).toContain('pm.expect(data.items).to.have.lengthOf.at.least(1);');\n    expect(translatedCode).toContain('pm.expect(data.items[0]).to.have.property(\\'id\\');');\n    expect(translatedCode).toContain('pm.expect(data.user).to.be.an(\\'object\\');');\n    expect(translatedCode).toContain('pm.expect(data.user).to.have.all.keys(\\'id\\', \\'name\\', \\'email\\');');\n    expect(translatedCode).toContain('pm.expect(data.user).to.include({active: true});');\n  });\n\n  it('should handle expect.fail with conditions', () => {\n    const code = `\ntest(\"Validate critical fields\", function() {\n    const data = res.getBody();\n\n    if (!data.id) {\n        expect.fail(\"Missing ID field\");\n    }\n\n    if (data.status !== 'active' && data.status !== 'pending') {\n        expect.fail(\"Invalid status: \" + data.status);\n    }\n\n    // Continue with normal assertions\n    expect(data.name).to.be.a('string');\n});\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('pm.test(\"Validate critical fields\", function() {');\n    expect(translatedCode).toContain('const data = pm.response.json();');\n    expect(translatedCode).toContain('if (!data.id) {');\n    expect(translatedCode).toContain('pm.expect.fail(\"Missing ID field\");');\n    expect(translatedCode).toContain('if (data.status !== \\'active\\' && data.status !== \\'pending\\') {');\n    expect(translatedCode).toContain('pm.expect.fail(\"Invalid status: \" + data.status);');\n    expect(translatedCode).toContain('pm.expect(data.name).to.be.a(\\'string\\');');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/bruno/bruno-to-postman-translations/variables.test.js",
    "content": "import translateBruToPostman from '../../../src/utils/bruno-to-postman-translator';\n\ndescribe('Bruno to Postman Variables Translation', () => {\n  // Regular variables tests\n  it('should translate bru.getVar', () => {\n    const code = 'bru.getVar(\"test\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.variables.get(\"test\");');\n  });\n\n  it('should translate bru.setVar', () => {\n    const code = 'bru.setVar(\"test\", \"value\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.variables.set(\"test\", \"value\");');\n  });\n\n  it('should translate bru.hasVar', () => {\n    const code = 'bru.hasVar(\"userId\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.variables.has(\"userId\");');\n  });\n\n  it('should translate bru.deleteVar', () => {\n    const code = 'bru.deleteVar(\"tempVar\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.variables.unset(\"tempVar\");');\n  });\n\n  it('should translate bru.interpolate', () => {\n    const code = 'bru.interpolate(\"Hello {{name}}\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.variables.replaceIn(\"Hello {{name}}\");');\n  });\n\n  it('should translate bru.interpolate with complex template', () => {\n    const code = 'const greeting = bru.interpolate(\"Hello {{name}}, your user id is {{userId}}\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const greeting = pm.variables.replaceIn(\"Hello {{name}}, your user id is {{userId}}\");');\n  });\n\n  // Global variables tests\n  it('should translate bru.getGlobalEnvVar', () => {\n    const code = 'bru.getGlobalEnvVar(\"test\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.globals.get(\"test\");');\n  });\n\n  it('should translate bru.setGlobalEnvVar', () => {\n    const code = 'bru.setGlobalEnvVar(\"test\", \"value\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.globals.set(\"test\", \"value\");');\n  });\n\n  // Collection variables tests\n  it('should translate bru.getCollectionVar', () => {\n    const code = 'bru.getCollectionVar(\"baseUrl\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.collectionVariables.get(\"baseUrl\");');\n  });\n\n  // TODO: Restore once UI update fixes are live for setCollectionVar\n  it.skip('should translate bru.setCollectionVar', () => {\n    const code = 'bru.setCollectionVar(\"baseUrl\", \"https://api.example.com\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.collectionVariables.set(\"baseUrl\", \"https://api.example.com\");');\n  });\n\n  it('should translate bru.hasCollectionVar', () => {\n    const code = 'bru.hasCollectionVar(\"baseUrl\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.collectionVariables.has(\"baseUrl\");');\n  });\n\n  // TODO: Restore once UI update fixes are live for deleteCollectionVar\n  it.skip('should translate bru.deleteCollectionVar', () => {\n    const code = 'bru.deleteCollectionVar(\"baseUrl\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.collectionVariables.unset(\"baseUrl\");');\n  });\n\n  // TODO: Restore once UI update fixes are live for getAllCollectionVars\n  it.skip('should translate bru.getAllCollectionVars', () => {\n    const code = 'const vars = bru.getAllCollectionVars();';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const vars = pm.collectionVariables.toObject();');\n  });\n\n  // TODO: Restore once UI update fixes are live for deleteAllCollectionVars\n  it.skip('should translate bru.deleteAllCollectionVars', () => {\n    const code = 'bru.deleteAllCollectionVars();';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.collectionVariables.clear();');\n  });\n\n  // Folder variables tests\n  it('should translate bru.getFolderVar', () => {\n    const code = 'bru.getFolderVar(\"folderToken\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.variables.get(\"folderToken\");');\n  });\n\n  // Request variables tests\n  it('should translate bru.getRequestVar', () => {\n    const code = 'bru.getRequestVar(\"requestId\");';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.variables.get(\"requestId\");');\n  });\n\n  // Combined tests\n  it('should handle conditional expressions with variable calls', () => {\n    const code = 'const userStatus = bru.hasVar(\"userId\") ? \"logged-in\" : \"guest\";';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('const userStatus = pm.variables.has(\"userId\") ? \"logged-in\" : \"guest\";');\n  });\n\n  it('should handle all variable methods together', () => {\n    const code = `\n// All variable methods\nconst hasUserId = bru.hasVar(\"userId\");\nconst userId = bru.getVar(\"userId\");\nbru.setVar(\"requestTime\", new Date().toISOString());\n\nconsole.log(\\`Has userId: \\${hasUserId}, User ID: \\${userId}\\`);\n`;\n    const translatedCode = translateBruToPostman(code);\n\n    expect(translatedCode).toContain('const hasUserId = pm.variables.has(\"userId\");');\n    expect(translatedCode).toContain('const userId = pm.variables.get(\"userId\");');\n    expect(translatedCode).toContain('pm.variables.set(\"requestTime\", new Date().toISOString());');\n  });\n\n  it('should handle nested expressions with variables', () => {\n    const code = 'bru.setVar(\"fullPath\", bru.getEnvVar(\"baseUrl\") + bru.getVar(\"endpoint\"));';\n    const translatedCode = translateBruToPostman(code);\n    expect(translatedCode).toBe('pm.variables.set(\"fullPath\", pm.environment.get(\"baseUrl\") + pm.variables.get(\"endpoint\"));');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/bruno/bruno-to-postman-with-tests.spec.js",
    "content": "import { brunoToPostman } from '../../src/postman/bruno-to-postman';\n\ndescribe('Bruno to Postman Converter with Tests and Scripts', () => {\n  const brunoCollection = {\n    name: 'Script and Tests Collection',\n    version: '1',\n    items: [\n      {\n        name: 'Request With Scripts and Tests',\n        type: 'http-request',\n        filename: 'request-with-scripts.bru',\n        seq: 1,\n        settings: {\n          encodeUrl: true,\n          timeout: 0\n        },\n        tags: [],\n        examples: [],\n        request: {\n          url: 'https://echo.usebruno.com',\n          method: 'POST',\n          headers: [],\n          params: [],\n          body: {\n            mode: 'json',\n            json: '{\\n  \"location\": \"root-request\"\\n}',\n            formUrlEncoded: [],\n            multipartForm: [],\n            file: []\n          },\n          script: {\n            req: 'console.log(\"root-request script line 1\");\\nconsole.log(\"root-request script line 2\")',\n            res: 'console.log(\"root-request script line 1\");\\nconsole.log(\"root-request script line 2\")'\n          },\n          vars: {},\n          assertions: [],\n          tests: 'test(\"Status code is 200\", () => {\\n    expect(res.status).to.eql(200);\\n});\\ntest(\"Body is not empty\", () => {\\n    expect(res.body).not.to.eql(\"\");\\n});',\n          docs: '',\n          auth: {\n            mode: 'none'\n          }\n        }\n      },\n      {\n        type: 'folder',\n        name: 'Scripts Folder',\n        filename: 'scripts-folder',\n        seq: 2,\n        examples: [],\n        root: {\n          request: {\n            auth: {\n              mode: 'none'\n            },\n            script: {\n              req: 'console.log(\"scripts-folder script line 1\");\\nconsole.log(\"scripts-folder script line 2\")',\n              res: 'console.log(\"scripts-folder script line 1\");\\nconsole.log(\"scripts-folder script line 2\")'\n            },\n            tests: 'test(\"Status code is 200\", () => {\\n    expect(res.status).to.eql(200);\\n});\\ntest(\"Body is not empty\", () => {\\n    expect(res.body).not.to.eql(\"\");\\n});'\n          },\n          meta: {\n            name: 'Scripts Folder',\n            seq: 2\n          }\n        },\n        items: [\n          {\n            type: 'http',\n            name: 'Request In Scripts Folder',\n            filename: 'scripts-folder-echo.bru',\n            seq: 1,\n            settings: {\n              encodeUrl: true,\n              timeout: 0\n            },\n            tags: [],\n            examples: [],\n            request: {\n              url: 'https://echo.usebruno.com',\n              method: 'POST',\n              headers: [],\n              params: [],\n              body: {\n                mode: 'json',\n                json: '{\\n  \"location\": \"folder-request\"\\n}',\n                formUrlEncoded: [],\n                multipartForm: [],\n                file: []\n              },\n              script: {\n                req: 'console.log(\"scripts-folder-request script line 1\");\\nconsole.log(\"scripts-folder-request script line 2\")',\n                res: 'console.log(\"scripts-folder-request script line 1\");\\nconsole.log(\"scripts-folder-request script line 2\")'\n              },\n              vars: {},\n              assertions: [],\n              tests: 'test(\"Status code is 200\", () => {\\n    expect(res.status).to.eql(200);\\n});\\ntest(\"Body is not empty\", () => {\\n    expect(res.body).not.to.eql(\"\");\\n});',\n              docs: '',\n              auth: {\n                mode: 'none'\n              }\n            }\n          },\n          {\n            type: 'folder',\n            name: 'Scripts Inner Folder',\n            filename: 'scripts-inner-folder',\n            seq: 2,\n            examples: [],\n            root: {\n              request: {\n                auth: {\n                  mode: 'none'\n                },\n                script: {\n                  req: 'console.log(\"scripts-inner-folder script line 1\");\\nconsole.log(\"scripts-inner-folder script line 2\")',\n                  res: 'console.log(\"scripts-inner-folder script line 1\");\\nconsole.log(\"scripts-inner-folder script line 2\")'\n                },\n                tests: 'test(\"Status code is 200\", () => {\\n    expect(res.status).to.eql(200);\\n});\\ntest(\"Body is not empty\", () => {\\n    expect(res.body).not.to.eql(\"\");\\n});'\n              },\n              meta: {\n                name: 'Scripts Inner Folder',\n                seq: 2\n              }\n            },\n            items: [\n              {\n                type: 'http',\n                name: 'Request In Scripts Inner Folder',\n                filename: 'scripts-inner-folder-echo.bru',\n                seq: 2,\n                settings: {\n                  encodeUrl: true,\n                  timeout: 0\n                },\n                tags: [],\n                examples: [],\n                request: {\n                  url: 'https://echo.usebruno.com',\n                  method: 'POST',\n                  headers: [],\n                  params: [],\n                  body: {\n                    mode: 'json',\n                    json: '{\\n  \"location\": \"inner-folder-request\"\\n}',\n                    formUrlEncoded: [],\n                    multipartForm: [],\n                    file: []\n                  },\n                  script: {\n                    req: 'console.log(\"scripts-inner-folder-request script line 1\");\\nconsole.log(\"scripts-inner-folder-request script line 2\")',\n                    res: 'console.log(\"scripts-inner-folder-request script line 1\");\\nconsole.log(\"scripts-inner-folder-request script line 2\")'\n                  },\n                  vars: {},\n                  assertions: [],\n                  tests: 'test(\"Status code is 200\", () => {\\n    expect(res.status).to.eql(200);\\n});\\ntest(\"Body is not empty\", () => {\\n    expect(res.body).not.to.eql(\"\");\\n});',\n                  docs: '',\n                  auth: {\n                    mode: 'none'\n                  }\n                }\n              }\n            ]\n          }\n        ]\n      }\n    ],\n    environments: [],\n    root: {\n      request: {\n        script: {\n          req: 'console.log(\"root-request script line 1\");\\nconsole.log(\"root-request script line 2\")',\n          res: 'console.log(\"root-request script line 1\");\\nconsole.log(\"root-request script line 2\")'\n        },\n        tests: 'test(\"Status code is 200\", () => {\\n    expect(res.status).to.eql(200);\\n});\\ntest(\"Body is not empty\", () => {\\n    expect(res.body).not.to.eql(\"\");\\n});'\n      }\n    },\n    brunoConfig: {\n      version: '1',\n      name: 'Script and Tests Collection',\n      type: 'collection',\n      ignore: [\n        'node_modules',\n        '.git'\n      ],\n      size: 0.0020351409912109375,\n      filesCount: 6\n    }\n  };\n\n  it('should convert Bruno request scripts and tests to Postman event scripts', () => {\n    const postmanCollection = brunoToPostman(brunoCollection);\n    // Root request events\n    const rootRequest = postmanCollection.item.find((i) => i.name === 'Request With Scripts and Tests');\n    const rootPre = rootRequest.event.find((e) => e.listen === 'prerequest');\n    const rootTest = rootRequest.event.find((e) => e.listen === 'test');\n    expect(rootPre).toBeDefined();\n    expect(rootTest).toBeDefined();\n    expect(rootPre.script.exec).toEqual([\n      'console.log(\"root-request script line 1\");',\n      'console.log(\"root-request script line 2\")'\n    ]);\n    expect(rootTest.script.exec).toEqual([\n      'console.log(\"root-request script line 1\");',\n      'console.log(\"root-request script line 2\")',\n      '',\n      '// Tests',\n      'pm.test(\"Status code is 200\", () => {',\n      '    pm.expect(pm.response.code).to.eql(200);',\n      '});',\n      'pm.test(\"Body is not empty\", () => {',\n      '    pm.expect(pm.response.body).not.to.eql(\"\");',\n      '});'\n    ]);\n  });\n\n  it('should convert Bruno folder scripts and tests to Postman event scripts', () => {\n    const postmanCollection = brunoToPostman(brunoCollection);\n    // Folder events\n    const folder = postmanCollection.item.find((i) => i.name === 'Scripts Folder');\n    const folderPre = folder.event.find((e) => e.listen === 'prerequest');\n    const folderTest = folder.event.find((e) => e.listen === 'test');\n    expect(folderPre).toBeDefined();\n    expect(folderTest).toBeDefined();\n    expect(folderPre.script.exec).toEqual([\n      'console.log(\"scripts-folder script line 1\");',\n      'console.log(\"scripts-folder script line 2\")'\n    ]);\n    expect(folderTest.script.exec).toEqual([\n      'console.log(\"scripts-folder script line 1\");',\n      'console.log(\"scripts-folder script line 2\")',\n      '',\n      '// Tests',\n      'pm.test(\"Status code is 200\", () => {',\n      '    pm.expect(pm.response.code).to.eql(200);',\n      '});',\n      'pm.test(\"Body is not empty\", () => {',\n      '    pm.expect(pm.response.body).not.to.eql(\"\");',\n      '});'\n    ]);\n  });\n\n  it('should convert Bruno inner folder scripts and tests to Postman event scripts', () => {\n    const postmanCollection = brunoToPostman(brunoCollection);\n    const folder = postmanCollection.item.find((i) => i.name === 'Scripts Folder');\n    // Inner folder events\n    const innerFolder = folder.item.find((i) => i.name === 'Scripts Inner Folder');\n    const innerFolderPre = innerFolder.event.find((e) => e.listen === 'prerequest');\n    const innerFolderTest = innerFolder.event.find((e) => e.listen === 'test');\n    expect(innerFolderPre).toBeDefined();\n    expect(innerFolderTest).toBeDefined();\n    expect(innerFolderPre.script.exec).toEqual([\n      'console.log(\"scripts-inner-folder script line 1\");',\n      'console.log(\"scripts-inner-folder script line 2\")'\n    ]);\n    expect(innerFolderTest.script.exec).toEqual([\n      'console.log(\"scripts-inner-folder script line 1\");',\n      'console.log(\"scripts-inner-folder script line 2\")',\n      '',\n      '// Tests',\n      'pm.test(\"Status code is 200\", () => {',\n      '    pm.expect(pm.response.code).to.eql(200);',\n      '});',\n      'pm.test(\"Body is not empty\", () => {',\n      '    pm.expect(pm.response.body).not.to.eql(\"\");',\n      '});'\n    ]);\n  });\n\n  it('should convert Bruno collection scripts and tests to Postman event scripts', () => {\n    const postmanCollection = brunoToPostman(brunoCollection);\n    // Collection events\n    const collectionPre = postmanCollection.event.find((e) => e.listen === 'prerequest');\n    const collectionTest = postmanCollection.event.find((e) => e.listen === 'test');\n    expect(collectionPre).toBeDefined();\n    expect(collectionTest).toBeDefined();\n    expect(collectionPre.script.exec).toEqual([\n      'console.log(\"root-request script line 1\");',\n      'console.log(\"root-request script line 2\")'\n    ]);\n    expect(collectionTest.script.exec).toEqual([\n      'console.log(\"root-request script line 1\");',\n      'console.log(\"root-request script line 2\")',\n      '',\n      '// Tests',\n      'pm.test(\"Status code is 200\", () => {',\n      '    pm.expect(pm.response.code).to.eql(200);',\n      '});',\n      'pm.test(\"Body is not empty\", () => {',\n      '    pm.expect(pm.response.body).not.to.eql(\"\");',\n      '});'\n    ]);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/common/sanitizeTag.spec.js",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport { sanitizeTag, sanitizeTags } from '../../src/common/index.js';\n\ndescribe('sanitizeTag', () => {\n  describe('basic functionality', () => {\n    it('should return null for null input', () => {\n      expect(sanitizeTag(null)).toBeNull();\n    });\n\n    it('should return null for undefined input', () => {\n      expect(sanitizeTag(undefined)).toBeNull();\n    });\n\n    it('should return null for non-string input', () => {\n      expect(sanitizeTag(123)).toBeNull();\n      expect(sanitizeTag({})).toBeNull();\n      expect(sanitizeTag([])).toBeNull();\n    });\n\n    it('should return null for empty string', () => {\n      expect(sanitizeTag('')).toBeNull();\n    });\n\n    it('should return null for whitespace-only string', () => {\n      expect(sanitizeTag('   ')).toBeNull();\n      expect(sanitizeTag('\\t\\n')).toBeNull();\n    });\n  });\n\n  describe('valid tags', () => {\n    it('should preserve alphanumeric tags', () => {\n      expect(sanitizeTag('valid')).toBe('valid');\n      expect(sanitizeTag('ValidTag')).toBe('ValidTag');\n      expect(sanitizeTag('tag123')).toBe('tag123');\n    });\n\n    it('should preserve tags with hyphens', () => {\n      expect(sanitizeTag('valid-tag')).toBe('valid-tag');\n      expect(sanitizeTag('my-api-endpoint')).toBe('my-api-endpoint');\n    });\n\n    it('should preserve tags with underscores', () => {\n      expect(sanitizeTag('valid_tag')).toBe('valid_tag');\n      expect(sanitizeTag('my_api_endpoint')).toBe('my_api_endpoint');\n    });\n\n    it('should replace spaces with underscores', () => {\n      expect(sanitizeTag('User Management')).toBe('User_Management');\n      expect(sanitizeTag('API v1')).toBe('API_v1');\n    });\n\n    it('should preserve tags with mixed valid characters (spaces become underscores)', () => {\n      expect(sanitizeTag('valid-tag_name')).toBe('valid-tag_name');\n      expect(sanitizeTag('API v1-endpoint')).toBe('API_v1-endpoint');\n      expect(sanitizeTag('User Management API')).toBe('User_Management_API');\n    });\n  });\n\n  describe('space handling', () => {\n    it('should replace spaces with underscores in the middle of tags', () => {\n      expect(sanitizeTag('User Management')).toBe('User_Management');\n      expect(sanitizeTag('API v1')).toBe('API_v1');\n    });\n\n    it('should collapse multiple spaces into a single underscore', () => {\n      expect(sanitizeTag('User  Management')).toBe('User_Management');\n      expect(sanitizeTag('API   v1')).toBe('API_v1');\n    });\n\n    it('should trim leading and trailing spaces', () => {\n      expect(sanitizeTag('  tag  ')).toBe('tag');\n      expect(sanitizeTag('\\ttag\\n')).toBe('tag');\n    });\n\n    it('should remove leading/trailing spaces and replace internal spaces with underscores', () => {\n      expect(sanitizeTag('  User Management  ')).toBe('User_Management');\n    });\n  });\n\n  describe('special character handling', () => {\n    it('should replace dots with underscores', () => {\n      expect(sanitizeTag('api.v1')).toBe('api_v1');\n      expect(sanitizeTag('api.v1.0')).toBe('api_v1_0');\n    });\n\n    it('should replace colons with underscores', () => {\n      expect(sanitizeTag('api:v1')).toBe('api_v1');\n    });\n\n    it('should replace slashes with underscores', () => {\n      expect(sanitizeTag('api/v1')).toBe('api_v1');\n      expect(sanitizeTag('api/v1/users')).toBe('api_v1_users');\n    });\n\n    it('should replace parentheses with underscores', () => {\n      // 'API (v1)' has space before parenthesis, both become underscores\n      expect(sanitizeTag('API (v1)')).toBe('API_v1');\n      // 'API(v1)' has no space, so it becomes 'API_v1'\n      expect(sanitizeTag('API(v1)')).toBe('API_v1');\n    });\n\n    it('should replace multiple special characters', () => {\n      // 'API v1.0 (beta)' - spaces, dots, parentheses all become underscores\n      // Result: 'API_v1_0_beta' (collapsed to single underscores)\n      expect(sanitizeTag('API v1.0 (beta)')).toBe('API_v1_0_beta');\n      expect(sanitizeTag('api.v1:beta')).toBe('api_v1_beta');\n    });\n\n    it('should handle special characters at start and end', () => {\n      expect(sanitizeTag('.api')).toBe('api');\n      expect(sanitizeTag('api.')).toBe('api');\n      expect(sanitizeTag('-api')).toBe('api');\n      expect(sanitizeTag('api-')).toBe('api');\n      expect(sanitizeTag('_api')).toBe('api');\n      expect(sanitizeTag('api_')).toBe('api');\n      expect(sanitizeTag(' api')).toBe('api');\n      expect(sanitizeTag('api ')).toBe('api');\n    });\n\n    it('should return null when result is only special characters', () => {\n      expect(sanitizeTag('...')).toBeNull();\n      expect(sanitizeTag('---')).toBeNull();\n      expect(sanitizeTag('___')).toBeNull();\n      expect(sanitizeTag('.-_')).toBeNull();\n    });\n  });\n\n  describe('options handling', () => {\n    it('should ignore collectionFormat option and always sanitize', () => {\n      // The collectionFormat option is no longer used - always sanitize\n      // Spaces are replaced with underscores for BRU format compatibility\n      expect(sanitizeTag('User Management', { collectionFormat: 'yml' })).toBe('User_Management');\n      expect(sanitizeTag('api.v1', { collectionFormat: 'yml' })).toBe('api_v1');\n      // 'API (v1)' becomes 'API_v1' (space and parentheses become underscores)\n      expect(sanitizeTag('API (v1)', { collectionFormat: 'yml' })).toBe('API_v1');\n    });\n  });\n});\n\ndescribe('sanitizeTags', () => {\n  it('should return empty array for null input', () => {\n    expect(sanitizeTags(null)).toEqual([]);\n  });\n\n  it('should return empty array for undefined input', () => {\n    expect(sanitizeTags(undefined)).toEqual([]);\n  });\n\n  it('should return empty array for non-array input', () => {\n    expect(sanitizeTags('string')).toEqual([]);\n    expect(sanitizeTags(123)).toEqual([]);\n    expect(sanitizeTags({})).toEqual([]);\n  });\n\n  it('should return empty array for empty array input', () => {\n    expect(sanitizeTags([])).toEqual([]);\n  });\n\n  it('should sanitize all tags in array', () => {\n    // Spaces are replaced with underscores\n    expect(sanitizeTags(['User Management', 'API v1'])).toEqual(['User_Management', 'API_v1']);\n  });\n\n  it('should remove null values from result', () => {\n    expect(sanitizeTags(['valid', '...', 'also-valid'])).toEqual(['valid', 'also-valid']);\n  });\n\n  it('should remove duplicates from result', () => {\n    expect(sanitizeTags(['User Management', 'User Management'])).toEqual(['User_Management']);\n    expect(sanitizeTags(['api.v1', 'api_v1'])).toEqual(['api_v1']);\n  });\n\n  it('should preserve order of first occurrence', () => {\n    expect(sanitizeTags(['tag1', 'tag2', 'tag1'])).toEqual(['tag1', 'tag2']);\n  });\n\n  it('should handle mixed valid and invalid tags', () => {\n    // Spaces are replaced with underscores\n    expect(sanitizeTags(['valid-tag', 'invalid.tag', 'another valid'])).toEqual(['valid-tag', 'invalid_tag', 'another_valid']);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/insomnia/env-utils.spec.js",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport { buildV5Environments, buildV4Environments } from '../../src/insomnia/env-utils';\n\nconst getVar = (env, name) => {\n  return env.variables.find((v) => v.name === name);\n};\n\ndescribe('env-utils', () => {\n  describe('buildV5Environments', () => {\n    it('creates base and sub environments with flattened keys and shallow overrides', () => {\n      const environmentsNode = {\n        name: 'Base',\n        data: {\n          baseurl: 'https://api.example.com',\n          nested: { name: 'alice', roles: ['admin'] },\n          numbers: [1, 2]\n        },\n        subEnvironments: [\n          {\n            name: 'Staging',\n            data: {\n              baseurl: 'https://staging.example.com',\n              nested: { name: 'bob' }\n            }\n          },\n          { name: 'Dev', data: {} }\n        ]\n      };\n\n      const envs = buildV5Environments(environmentsNode);\n      expect(envs.length).toBe(3);\n\n      const base = envs[0];\n      const staging = envs[1];\n      const dev = envs[2];\n\n      expect(base.name).toBe('Base');\n      expect(getVar(base, 'baseurl')?.value).toBe('https://api.example.com');\n      expect(getVar(base, 'nested.name')?.value).toBe('alice');\n      expect(getVar(base, 'nested.roles[0]')?.value).toBe('admin');\n      expect(getVar(base, 'numbers[1]')?.value).toBe('2');\n\n      expect(staging.name).toBe('Staging');\n      // baseurl overridden in sub\n      expect(getVar(staging, 'baseurl')?.value).toBe('https://staging.example.com');\n      // nested.name overridden, nested array preserved from base\n      expect(getVar(staging, 'nested.name')?.value).toBe('bob');\n      expect(getVar(staging, 'nested.roles[0]')?.value).toBe('admin');\n\n      expect(dev.name).toBe('Dev');\n      // no sub data => inherits base\n      expect(getVar(dev, 'baseurl')?.value).toBe('https://api.example.com');\n      expect(getVar(dev, 'nested.name')?.value).toBe('alice');\n    });\n  });\n\n  describe('buildV4Environments', () => {\n    it('merges nearest base and sub env data (flattened) into standalone Bruno envs', () => {\n      const workspaceId = 'wrk_1';\n      const resources = [\n        { _id: workspaceId, _type: 'workspace', name: 'WS' },\n        {\n          _id: 'env_base',\n          _type: 'environment',\n          parentId: workspaceId,\n          name: 'Base',\n          data: {\n            baseurl: 'https://api.example.com',\n            user: { name: 'alice' },\n            arr: [{ id: 1 }]\n          }\n        },\n        {\n          _id: 'env_sub',\n          _type: 'environment',\n          parentId: 'env_base',\n          name: 'Sub',\n          data: {\n            user: { name: 'bob' }\n          }\n        }\n      ];\n\n      const envs = buildV4Environments(resources, workspaceId);\n      expect(envs.length).toBe(2);\n\n      const base = envs.find((e) => e.name === 'Base');\n      const sub = envs.find((e) => e.name === 'Sub');\n\n      expect(getVar(base, 'baseurl')?.value).toBe('https://api.example.com');\n      expect(getVar(base, 'user.name')?.value).toBe('alice');\n      expect(getVar(base, 'arr[0].id')?.value).toBe('1');\n\n      // sub should inherit base, override user.name\n      expect(getVar(sub, 'baseurl')?.value).toBe('https://api.example.com');\n      expect(getVar(sub, 'user.name')?.value).toBe('bob');\n      expect(getVar(sub, 'arr[0].id')?.value).toBe('1');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/insomnia/insomnia-collection-v5.spec.js",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport insomniaToBruno from '../../src/insomnia/insomnia-to-bruno';\n\ndescribe('insomnia-collection', () => {\n  it('should correctly import a valid Insomnia v5 collection file', async () => {\n    const brunoCollection = insomniaToBruno(insomniaCollection);\n\n    expect(brunoCollection).toMatchObject(expectedOutput);\n  });\n});\n\nconst insomniaCollection = `\ntype: collection.insomnia.rest/5.0\nname: Hello World Workspace Insomnia\nmeta:\n  id: wrk_9381cf78cb0a4eaaab1d571f29f928dc\n  created: 1744194421962\n  modified: 1744194421962\ncollection:\n  - name: Folder1\n    meta:\n      id: fld_6beacec0bd2f4370be98169217e82a2c\n      created: 1744194421968\n      modified: 1744194421968\n      sortKey: -1744194421968\n    children:\n      - url: https://testbench-sanity.usebruno.com/ping\n        name: Request1\n        meta:\n          id: req_e9fbdc9c88984068a04f442e052d4ff1\n          created: 1744194421965\n          modified: 1744194421965\n          isPrivate: false\n          sortKey: -1744194421965\n        method: GET\n        parameters:\n          - name: date\n            value: 2022-10-28\n        settings:\n          renderRequestBody: true\n          encodeUrl: true\n          followRedirects: global\n          cookies:\n            send: true\n            store: true\n          rebuildPath: true\n  - name: Folder2\n    meta:\n      id: fld_96508d79bf06420a853b07482ab280d7\n      created: 1744194421969\n      modified: 1744194421969\n      sortKey: -1744194421969\n    children:\n      - url: https://testbench-sanity.usebruno.com/ping\n        name: Request2\n        meta:\n          id: req_3c572aa26a964f1f800bfa5c53cacb75\n          created: 1744194421967\n          modified: 1744194421967\n          isPrivate: false\n          sortKey: -1744194421968\n        method: GET\n        settings:\n          renderRequestBody: true\n          encodeUrl: false\n          followRedirects: global\n          cookies:\n            send: true\n            store: true\n          rebuildPath: true\ncookieJar:\n  name: Default Jar\n  meta:\n    id: jar_9ecb97079037c7d5bb888f0bfdec9b0e1275c6d1\n    created: 1744194421971\n    modified: 1744194421971\nenvironments:\n  name: Imported Environment\n  meta:\n    id: env_a8a9a8ff952d4d079edf53f8ee22a423\n    created: 1744194421970\n    modified: 1744194421970\n    isPrivate: false\n  data:\n    var1: value1\n    var2: value2\n`;\n\nconst expectedOutput = {\n  environments: [\n    {\n      name: 'Imported Environment',\n      variables: [\n        {\n          name: 'var1',\n          value: 'value1',\n          type: 'text',\n          enabled: true,\n          secret: false\n        },\n        {\n          name: 'var2',\n          value: 'value2',\n          type: 'text',\n          enabled: true,\n          secret: false\n        }\n      ]\n    }\n  ],\n  items: [\n    {\n      items: [\n        {\n          name: 'Request1',\n          request: {\n            auth: {\n              basic: null,\n              bearer: null,\n              digest: null,\n              mode: 'none'\n            },\n            body: {\n              formUrlEncoded: [],\n              json: null,\n              mode: 'none',\n              multipartForm: [],\n              text: null,\n              xml: null\n            },\n            headers: [],\n            method: 'GET',\n            params: [\n              {\n                enabled: true,\n                name: 'date',\n                type: 'query',\n                value: '2022-10-28'\n              }\n            ],\n            url: 'https://testbench-sanity.usebruno.com/ping'\n          },\n          seq: 1,\n          type: 'http-request',\n          uid: 'mockeduuidvalue123456',\n          settings: {\n            encodeUrl: true\n          }\n        }\n      ],\n      name: 'Folder1',\n      type: 'folder',\n      uid: 'mockeduuidvalue123456'\n    },\n    {\n      items: [\n        {\n          name: 'Request2',\n          request: {\n            auth: {\n              basic: null,\n              bearer: null,\n              digest: null,\n              mode: 'none'\n            },\n            body: {\n              formUrlEncoded: [],\n              json: null,\n              mode: 'none',\n              multipartForm: [],\n              text: null,\n              xml: null\n            },\n            headers: [],\n            method: 'GET',\n            params: [],\n            url: 'https://testbench-sanity.usebruno.com/ping'\n          },\n          seq: 1,\n          type: 'http-request',\n          uid: 'mockeduuidvalue123456',\n          settings: {\n            encodeUrl: false\n          }\n        }\n      ],\n      name: 'Folder2',\n      type: 'folder',\n      uid: 'mockeduuidvalue123456'\n    }\n  ],\n  name: 'Hello World Workspace Insomnia',\n  uid: 'mockeduuidvalue123456',\n  version: '1'\n};\n"
  },
  {
    "path": "packages/bruno-converters/tests/insomnia/insomnia-collection.spec.js",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport insomniaToBruno from '../../src/insomnia/insomnia-to-bruno';\n\ndescribe('insomnia-collection', () => {\n  it('should correctly import a valid Insomnia collection file', async () => {\n    const brunoCollection = insomniaToBruno(insomniaCollection);\n\n    expect(brunoCollection).toMatchObject(expectedOutput);\n  });\n});\n\nconst insomniaCollection = {\n  _type: 'export',\n  __export_format: 4,\n  __export_date: '2024-05-20T10:02:44.123Z',\n  __export_source: 'insomnia.desktop.app:v2021.5.2',\n  resources: [\n    {\n      _id: 'req_1',\n      _type: 'request',\n      parentId: 'fld_1',\n      name: 'Request1',\n      method: 'GET',\n      url: 'https://testbench-sanity.usebruno.com/ping',\n      settingEncodeUrl: false,\n      parameters: []\n    },\n    {\n      _id: 'req_2',\n      _type: 'request',\n      parentId: 'fld_2',\n      name: 'Request2',\n      method: 'GET',\n      url: 'https://testbench-sanity.usebruno.com/ping',\n      settingEncodeUrl: true,\n      parameters: []\n    },\n    {\n      _id: 'fld_1',\n      _type: 'request_group',\n      parentId: 'wrk_1',\n      name: 'Folder1'\n    },\n    {\n      _id: 'fld_2',\n      _type: 'request_group',\n      parentId: 'wrk_1',\n      name: 'Folder2'\n    },\n    {\n      _id: 'wrk_1',\n      _type: 'workspace',\n      name: 'Hello World Workspace Insomnia'\n    },\n    {\n      _id: 'env_1',\n      _type: 'environment',\n      parentId: 'wrk_1',\n      data: {\n        var1: 'value1',\n        var2: 'value2'\n      }\n    }\n  ]\n};\n\nconst expectedOutput = {\n  environments: [\n    {\n      name: 'Environment 1',\n      variables: [\n        {\n          name: 'var1',\n          value: 'value1',\n          type: 'text',\n          enabled: true,\n          secret: false\n        },\n        {\n          name: 'var2',\n          value: 'value2',\n          type: 'text',\n          enabled: true,\n          secret: false\n        }\n      ]\n    }\n  ],\n  items: [\n    {\n      items: [\n        {\n          name: 'Request1',\n          request: {\n            auth: {\n              basic: null,\n              bearer: null,\n              digest: null,\n              mode: 'none'\n            },\n            body: {\n              formUrlEncoded: [],\n              json: null,\n              mode: 'none',\n              multipartForm: [],\n              text: null,\n              xml: null\n            },\n            headers: [],\n            method: 'GET',\n            params: [],\n            url: 'https://testbench-sanity.usebruno.com/ping'\n          },\n          seq: 1,\n          type: 'http-request',\n          uid: 'mockeduuidvalue123456',\n          settings: {\n            encodeUrl: false\n          }\n        },\n        {\n          name: 'Request1',\n          request: {\n            auth: {\n              basic: null,\n              bearer: null,\n              digest: null,\n              mode: 'none'\n            },\n            body: {\n              formUrlEncoded: [],\n              json: null,\n              mode: 'none',\n              multipartForm: [],\n              text: null,\n              xml: null\n            },\n            headers: [],\n            method: 'GET',\n            params: [],\n            url: 'https://testbench-sanity.usebruno.com/ping'\n          },\n          seq: 2,\n          type: 'http-request',\n          uid: 'mockeduuidvalue123456',\n          settings: {\n            encodeUrl: false\n          }\n        }\n      ],\n      name: 'Folder1',\n      type: 'folder',\n      uid: 'mockeduuidvalue123456'\n    },\n    {\n      items: [\n        {\n          name: 'Request2',\n          request: {\n            auth: {\n              basic: null,\n              bearer: null,\n              digest: null,\n              mode: 'none'\n            },\n            body: {\n              formUrlEncoded: [],\n              json: null,\n              mode: 'none',\n              multipartForm: [],\n              text: null,\n              xml: null\n            },\n            headers: [],\n            method: 'GET',\n            params: [],\n            url: 'https://testbench-sanity.usebruno.com/ping'\n          },\n          seq: 1,\n          type: 'http-request',\n          uid: 'mockeduuidvalue123456',\n          settings: {\n            encodeUrl: true\n          }\n        },\n        {\n          name: 'Request2',\n          request: {\n            auth: {\n              basic: null,\n              bearer: null,\n              digest: null,\n              mode: 'none'\n            },\n            body: {\n              formUrlEncoded: [],\n              json: null,\n              mode: 'none',\n              multipartForm: [],\n              text: null,\n              xml: null\n            },\n            headers: [],\n            method: 'GET',\n            params: [],\n            url: 'https://testbench-sanity.usebruno.com/ping'\n          },\n          seq: 2,\n          type: 'http-request',\n          uid: 'mockeduuidvalue123456',\n          settings: {\n            encodeUrl: true\n          }\n        }\n      ],\n      name: 'Folder2',\n      type: 'folder',\n      uid: 'mockeduuidvalue123456'\n    }\n  ],\n  name: 'Hello World Workspace Insomnia',\n  uid: 'mockeduuidvalue123456',\n  version: '1'\n};\n"
  },
  {
    "path": "packages/bruno-converters/tests/openapi/openapi-to-bruno/openapi-auth.spec.js",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport openApiToBruno from '../../../src/openapi/openapi-to-bruno';\n\ndescribe('openapi-to-bruno auth enhancements', () => {\n  it('maps HTTP Digest scheme to digest auth on the request', () => {\n    const spec = `\nopenapi: 3.0.3\ninfo:\n  title: Digest API\n  version: '1.0'\ncomponents:\n  securitySchemes:\n    DigestAuth:\n      type: http\n      scheme: digest\npaths:\n  /secure:\n    get:\n      security:\n        - DigestAuth: []\n      responses:\n        '200': { description: OK }\nservers:\n  - url: https://example.com\n`;\n    const collection = openApiToBruno(spec);\n    const req = collection.items[0];\n    expect(req.request.auth.mode).toBe('digest');\n    expect(req.request.auth.digest).toEqual({ username: '{{username}}', password: '{{password}}' });\n  });\n\n  it('maps apiKey in query and injects query param', () => {\n    const spec = `\nopenapi: 3.0.3\ninfo:\n  title: Query API-Key\n  version: '1.0'\ncomponents:\n  securitySchemes:\n    ApiKeyQuery:\n      type: apiKey\n      in: query\n      name: api_key\npaths:\n  /search:\n    get:\n      security:\n        - ApiKeyQuery: []\n      parameters:\n        - in: query\n          name: q\n          schema: { type: string }\n      responses:\n        '200': { description: OK }\nservers:\n  - url: https://example.com\n`;\n    const collection = openApiToBruno(spec);\n    const req = collection.items[0];\n    expect(req.request.auth.mode).toBe('apikey');\n    expect(req.request.auth.apikey.placement).toBe('queryparams');\n    const hasQueryParam = req.request.params.some((p) => p.name === 'api_key' && p.type === 'query');\n    expect(hasQueryParam).toBe(true);\n  });\n\n  it('maps apiKey in cookie and treats it as a header', () => {\n    const spec = `\nopenapi: 3.0.3\ninfo:\n  title: Cookie API-Key\n  version: '1.0'\ncomponents:\n  securitySchemes:\n    ApiKeyCookie:\n      type: apiKey\n      in: cookie\n      name: DEMO_API_KEY\npaths:\n  /favorites:\n    get:\n      security:\n        - ApiKeyCookie: []\n      responses:\n        '200': { description: OK }\nservers:\n  - url: https://example.com\n`;\n    const { items: [req] } = openApiToBruno(spec);\n    expect(req.request.auth.mode).toBe('apikey');\n    expect(req.request.auth.apikey.placement).toBe('header');\n    const apiKeyHeader = req.request.headers.find((h) => h.name === 'DEMO_API_KEY');\n    expect(apiKeyHeader).toBeDefined();\n    expect(apiKeyHeader.value).toBe('{{apiKey}}');\n  });\n\n  it('maps OAuth2 authorizationCode flow to oauth2 grantType authorization_code', () => {\n    const spec = `\nopenapi: 3.0.3\ninfo:\n  title: OAuth2 AuthCode\n  version: '1.0'\ncomponents:\n  securitySchemes:\n    OAuthAuthCode:\n      type: oauth2\n      flows:\n        authorizationCode:\n          authorizationUrl: https://auth.example.com/authorize\n          tokenUrl: https://auth.example.com/token\npaths:\n  /orders:\n    get:\n      security:\n        - OAuthAuthCode: []\n      responses:\n        '200': { description: OK }\nservers:\n  - url: https://example.com\n`;\n    const { items: [req] } = openApiToBruno(spec);\n    expect(req.request.auth.mode).toBe('oauth2');\n    expect(req.request.auth.oauth2.grantType).toBe('authorization_code');\n  });\n\n  it('sets auth mode to inherit when operation security is explicitly empty', () => {\n    const spec = `\nopenapi: 3.0.3\ninfo:\n  title: Public Endpoint\n  version: '1.0'\npaths:\n  /public:\n    get:\n      security: []\n      responses:\n        '200': { description: OK }\nservers:\n  - url: https://example.com\n`;\n    const { items: [req] } = openApiToBruno(spec);\n    expect(req.request.auth.mode).toBe('inherit');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/openapi/openapi-to-bruno/openapi-body.spec.js",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport openApiToBruno from '../../../src/openapi/openapi-to-bruno';\n\ndescribe('openapi requestBody with $ref', () => {\n  it('should import body fields when requestBody uses $ref to components/requestBodies with inline schema (no explicit type: object)', () => {\n    const openApiSpec = `\nopenapi: \"3.0.0\"\ninfo:\n  version: \"1.0.0\"\n  title: \"RequestBody Ref Inline Schema Test\"\nservers:\n  - url: \"https://api.example.com\"\npaths:\n  /salesInvoices:\n    post:\n      summary: \"Creates a salesInvoice\"\n      operationId: \"postSalesInvoice\"\n      requestBody:\n        $ref: '#/components/requestBodies/salesInvoice'\n      responses:\n        '201':\n          description: \"A new salesInvoice has been successfully created\"\ncomponents:\n  requestBodies:\n    salesInvoice:\n      required: true\n      content:\n        application/json:\n          schema:\n            properties:\n              id:\n                type: string\n                format: uuid\n              number:\n                type: string\n                maxLength: 20\n              externalDocumentNumber:\n                type: string\n                maxLength: 35\n              invoiceDate:\n                type: string\n                format: date-time\n              dueDate:\n                type: string\n                format: date-time\n              fees:\n                type: array\n                items:\n                  $ref: '#/components/requestBodies/fees'\n    fees:\n      properties:\n        id:\n          type: string\n          format: uuid\n        name:\n          type: string\n          maxLength: 50\n        amount:\n          type: number\n      required:\n       - id\n       - amount\n`;\n\n    const result = openApiToBruno(openApiSpec);\n\n    // Should have one request item\n    expect(result.items.length).toBe(1);\n    const request = result.items[0];\n\n    // Body mode should be json\n    expect(request.request.body.mode).toBe('json');\n\n    // Body should contain the properties from the schema\n    expect(request.request.body.json).not.toBeNull();\n\n    const bodyJson = JSON.parse(request.request.body.json);\n    expect(bodyJson).toHaveProperty('id');\n    expect(bodyJson).toHaveProperty('number');\n    expect(bodyJson).toHaveProperty('externalDocumentNumber');\n    expect(bodyJson).toHaveProperty('invoiceDate');\n    expect(bodyJson).toHaveProperty('dueDate');\n    expect(bodyJson).toHaveProperty('fees');\n    expect(bodyJson['fees'][0]).toHaveProperty('id');\n  });\n\n  it('should import formUrlEncoded body when requestBody uses $ref with inline schema', () => {\n    const openApiSpec = `\nopenapi: \"3.0.0\"\ninfo:\n  version: \"1.0.0\"\n  title: \"Form URL Encoded Ref Test\"\nservers:\n  - url: \"https://api.example.com\"\npaths:\n  /login:\n    post:\n      summary: \"Login\"\n      operationId: \"login\"\n      requestBody:\n        $ref: '#/components/requestBodies/loginForm'\n      responses:\n        '200':\n          description: \"Login successful\"\ncomponents:\n  requestBodies:\n    loginForm:\n      required: true\n      content:\n        application/x-www-form-urlencoded:\n          schema:\n            properties:\n              username:\n                type: string\n              password:\n                type: string\n`;\n\n    const result = openApiToBruno(openApiSpec);\n\n    expect(result.items.length).toBe(1);\n    const request = result.items[0];\n\n    expect(request.request.body.mode).toBe('formUrlEncoded');\n    expect(request.request.body.formUrlEncoded.length).toBe(2);\n\n    const fieldNames = request.request.body.formUrlEncoded.map((f) => f.name);\n    expect(fieldNames).toContain('username');\n    expect(fieldNames).toContain('password');\n  });\n\n  it('should import multipartForm body when requestBody uses $ref with inline schema', () => {\n    const openApiSpec = `\nopenapi: \"3.0.0\"\ninfo:\n  version: \"1.0.0\"\n  title: \"Multipart Form Ref Test\"\nservers:\n  - url: \"https://api.example.com\"\npaths:\n  /upload:\n    post:\n      summary: \"Upload file\"\n      operationId: \"uploadFile\"\n      requestBody:\n        $ref: '#/components/requestBodies/fileUpload'\n      responses:\n        '200':\n          description: \"Upload successful\"\ncomponents:\n  requestBodies:\n    fileUpload:\n      required: true\n      content:\n        multipart/form-data:\n          schema:\n            properties:\n              file:\n                type: string\n                format: binary\n              description:\n                type: string\n`;\n\n    const result = openApiToBruno(openApiSpec);\n\n    expect(result.items.length).toBe(1);\n    const request = result.items[0];\n\n    expect(request.request.body.mode).toBe('multipartForm');\n    expect(request.request.body.multipartForm.length).toBe(2);\n\n    const fieldNames = request.request.body.multipartForm.map((f) => f.name);\n    expect(fieldNames).toContain('file');\n    expect(fieldNames).toContain('description');\n  });\n\n  it('should handle number and integer types with correct default values', () => {\n    const openApiSpec = `\nopenapi: \"3.0.0\"\ninfo:\n  version: \"1.0.0\"\n  title: \"Number Type Test\"\nservers:\n  - url: \"https://api.example.com\"\npaths:\n  /orders:\n    post:\n      summary: \"Create order\"\n      operationId: \"createOrder\"\n      requestBody:\n        content:\n          application/json:\n            schema:\n              properties:\n                quantity:\n                  type: integer\n                price:\n                  type: number\n                discount:\n                  type: number\n                name:\n                  type: string\n                active:\n                  type: boolean\n      responses:\n        '201':\n          description: \"Order created\"\n`;\n\n    const result = openApiToBruno(openApiSpec);\n\n    expect(result.items.length).toBe(1);\n    const request = result.items[0];\n\n    expect(request.request.body.mode).toBe('json');\n    expect(request.request.body.json).not.toBeNull();\n\n    const bodyJson = JSON.parse(request.request.body.json);\n\n    // integer type should be 0\n    expect(bodyJson.quantity).toBe(0);\n\n    // number type should be 0 (not empty string)\n    expect(bodyJson.price).toBe(0);\n    expect(bodyJson.discount).toBe(0);\n\n    // string type should be empty string\n    expect(bodyJson.name).toBe('');\n\n    // boolean type should be false\n    expect(bodyJson.active).toBe(false);\n  });\n});\n\ndescribe('openapi requestBody content types', () => {\n  it('should handle raw body with */* content type as text', () => {\n    const openApiSpec = `\nopenapi: \"3.0.0\"\ninfo:\n  version: \"1.0.0\"\n  title: \"Raw Body Test\"\nservers:\n  - url: \"https://api.example.com\"\npaths:\n  /raw:\n    post:\n      summary: \"Raw body endpoint\"\n      operationId: \"postRaw\"\n      requestBody:\n        required: true\n        content:\n          \"*/*\":\n            schema:\n              type: string\n      responses:\n        '200':\n          description: \"Success\"\n`;\n\n    const result = openApiToBruno(openApiSpec);\n\n    expect(result.items.length).toBe(1);\n    const request = result.items[0];\n\n    expect(request.request.body.mode).toBe('text');\n    expect(request.request.body.text).toBe('');\n  });\n\n  it('should handle binary body with application/octet-stream as text', () => {\n    const openApiSpec = `\nopenapi: \"3.0.0\"\ninfo:\n  version: \"1.0.0\"\n  title: \"Binary Body Test\"\nservers:\n  - url: \"https://api.example.com\"\npaths:\n  /binary:\n    post:\n      summary: \"Binary body endpoint\"\n      operationId: \"postBinary\"\n      requestBody:\n        required: true\n        content:\n          application/octet-stream:\n            schema:\n              type: string\n              format: binary\n      responses:\n        '200':\n          description: \"Success\"\n`;\n\n    const result = openApiToBruno(openApiSpec);\n\n    expect(result.items.length).toBe(1);\n    const request = result.items[0];\n\n    expect(request.request.body.mode).toBe('text');\n    expect(request.request.body.text).toBe('');\n  });\n\n  it('should handle XML body with application/xml content type', () => {\n    const openApiSpec = `\nopenapi: \"3.0.0\"\ninfo:\n  version: \"1.0.0\"\n  title: \"XML Body Test\"\nservers:\n  - url: \"https://api.example.com\"\npaths:\n  /xml:\n    post:\n      summary: \"XML body endpoint\"\n      operationId: \"postXml\"\n      requestBody:\n        required: true\n        content:\n          application/xml:\n            schema:\n              type: object\n              properties:\n                name:\n                  type: string\n      responses:\n        '200':\n          description: \"Success\"\n`;\n\n    const result = openApiToBruno(openApiSpec);\n\n    expect(result.items.length).toBe(1);\n    const request = result.items[0];\n\n    expect(request.request.body.mode).toBe('xml');\n    expect(request.request.body.xml).toContain('<?xml version=\"1.0\" encoding=\"UTF-8\"?>');\n    expect(request.request.body.xml).toContain('<name></name>');\n  });\n\n  it('should handle XML body with text/xml content type', () => {\n    const openApiSpec = `\nopenapi: \"3.0.0\"\ninfo:\n  version: \"1.0.0\"\n  title: \"Text XML Body Test\"\nservers:\n  - url: \"https://api.example.com\"\npaths:\n  /xml:\n    post:\n      summary: \"XML body endpoint\"\n      operationId: \"postXml\"\n      requestBody:\n        required: true\n        content:\n          text/xml:\n            schema:\n              type: object\n      responses:\n        '200':\n          description: \"Success\"\n`;\n\n    const result = openApiToBruno(openApiSpec);\n\n    expect(result.items.length).toBe(1);\n    const request = result.items[0];\n\n    expect(request.request.body.mode).toBe('xml');\n  });\n\n  it('should handle SPARQL query with application/sparql-query content type', () => {\n    const openApiSpec = `\nopenapi: \"3.0.0\"\ninfo:\n  version: \"1.0.0\"\n  title: \"SPARQL Query Test\"\nservers:\n  - url: \"https://api.example.com\"\npaths:\n  /sparql:\n    post:\n      summary: \"SPARQL query endpoint\"\n      operationId: \"postSparql\"\n      requestBody:\n        required: true\n        content:\n          application/sparql-query:\n            schema:\n              type: string\n      responses:\n        '200':\n          description: \"Success\"\n`;\n\n    const result = openApiToBruno(openApiSpec);\n\n    expect(result.items.length).toBe(1);\n    const request = result.items[0];\n\n    expect(request.request.body.mode).toBe('sparql');\n    expect(request.request.body.sparql).toBe('');\n  });\n\n  it('should handle text/plain content type', () => {\n    const openApiSpec = `\nopenapi: \"3.0.0\"\ninfo:\n  version: \"1.0.0\"\n  title: \"Text Plain Test\"\nservers:\n  - url: \"https://api.example.com\"\npaths:\n  /text:\n    post:\n      summary: \"Text body endpoint\"\n      operationId: \"postText\"\n      requestBody:\n        required: true\n        content:\n          text/plain:\n            schema:\n              type: string\n      responses:\n        '200':\n          description: \"Success\"\n`;\n\n    const result = openApiToBruno(openApiSpec);\n\n    expect(result.items.length).toBe(1);\n    const request = result.items[0];\n\n    expect(request.request.body.mode).toBe('text');\n    expect(request.request.body.text).toBe('');\n  });\n\n  it('should detect file fields in multipart/form-data based on format: binary', () => {\n    const openApiSpec = `\nopenapi: \"3.0.0\"\ninfo:\n  version: \"1.0.0\"\n  title: \"Multipart File Detection Test\"\nservers:\n  - url: \"https://api.example.com\"\npaths:\n  /upload:\n    post:\n      summary: \"Upload with file and text fields\"\n      operationId: \"uploadFile\"\n      requestBody:\n        required: true\n        content:\n          multipart/form-data:\n            schema:\n              type: object\n              properties:\n                file:\n                  type: string\n                  format: binary\n                  description: \"The file to upload\"\n                description:\n                  type: string\n                  description: \"File description\"\n                userId:\n                  type: integer\n                  description: \"User ID\"\n      responses:\n        '200':\n          description: \"Success\"\n`;\n\n    const result = openApiToBruno(openApiSpec);\n\n    expect(result.items.length).toBe(1);\n    const request = result.items[0];\n\n    expect(request.request.body.mode).toBe('multipartForm');\n    expect(request.request.body.multipartForm.length).toBe(3);\n\n    // Find the file field\n    const fileField = request.request.body.multipartForm.find((f) => f.name === 'file');\n    expect(fileField).toBeDefined();\n    expect(fileField.type).toBe('file');\n    expect(fileField.value).toEqual([]); // File fields should have array value\n\n    // Find the text fields\n    const descField = request.request.body.multipartForm.find((f) => f.name === 'description');\n    expect(descField).toBeDefined();\n    expect(descField.type).toBe('text');\n    expect(descField.value).toBe(''); // Text fields should have string value\n\n    const userIdField = request.request.body.multipartForm.find((f) => f.name === 'userId');\n    expect(userIdField).toBeDefined();\n    expect(userIdField.type).toBe('text');\n    expect(userIdField.value).toBe('');\n  });\n\n  it('should handle JSON variants like application/ld+json', () => {\n    const openApiSpec = `\nopenapi: \"3.0.0\"\ninfo:\n  version: \"1.0.0\"\n  title: \"JSON-LD Test\"\nservers:\n  - url: \"https://api.example.com\"\npaths:\n  /jsonld:\n    post:\n      summary: \"JSON-LD endpoint\"\n      operationId: \"postJsonLd\"\n      requestBody:\n        required: true\n        content:\n          application/ld+json:\n            schema:\n              type: object\n              properties:\n                \"@context\":\n                  type: string\n                name:\n                  type: string\n      responses:\n        '200':\n          description: \"Success\"\n`;\n\n    const result = openApiToBruno(openApiSpec);\n\n    expect(result.items.length).toBe(1);\n    const request = result.items[0];\n\n    expect(request.request.body.mode).toBe('json');\n    expect(request.request.body.json).not.toBeNull();\n  });\n\n  it('should handle XML variants like application/atom+xml', () => {\n    const openApiSpec = `\nopenapi: \"3.0.0\"\ninfo:\n  version: \"1.0.0\"\n  title: \"Atom XML Test\"\nservers:\n  - url: \"https://api.example.com\"\npaths:\n  /feed:\n    post:\n      summary: \"Atom feed endpoint\"\n      operationId: \"postFeed\"\n      requestBody:\n        required: true\n        content:\n          application/atom+xml:\n            schema:\n              type: object\n      responses:\n        '200':\n          description: \"Success\"\n`;\n\n    const result = openApiToBruno(openApiSpec);\n\n    expect(result.items.length).toBe(1);\n    const request = result.items[0];\n\n    expect(request.request.body.mode).toBe('xml');\n  });\n});\n\ndescribe('openapi example request body - should match main request body handling', () => {\n  const bodyTypesOpenApiSpec = `\nopenapi: 3.1.0\ninfo:\n  title: Body Types Demo API\n  version: 1.0.0\nservers:\n  - url: https://api.example.com\npaths:\n  /raw-body:\n    post:\n      summary: Raw body\n      requestBody:\n        content:\n          \"*/*\":\n            schema:\n              type: string\n      responses:\n        \"200\":\n          description: Success\n  /json-body:\n    post:\n      summary: JSON body\n      requestBody:\n        content:\n          application/json:\n            schema:\n              type: object\n              properties:\n                name:\n                  type: string\n                  example: \"John\"\n      responses:\n        \"200\":\n          description: Success\n          content:\n            application/json:\n              schema:\n                type: object\n  /xml-body:\n    post:\n      summary: XML body\n      requestBody:\n        content:\n          application/xml:\n            schema:\n              type: object\n              xml:\n                name: Root\n              properties:\n                name:\n                  type: string\n      responses:\n        \"200\":\n          description: Success\n  /multipart-body:\n    post:\n      summary: Multipart body\n      requestBody:\n        content:\n          multipart/form-data:\n            schema:\n              type: object\n              properties:\n                file:\n                  type: string\n                  format: binary\n                desc:\n                  type: string\n      responses:\n        \"200\":\n          description: Success\n  /form-body:\n    post:\n      summary: Form body\n      requestBody:\n        content:\n          application/x-www-form-urlencoded:\n            schema:\n              type: object\n              properties:\n                query:\n                  type: string\n                page:\n                  type: integer\n                  default: 1\n      responses:\n        \"200\":\n          description: Success\n  /sparql-body:\n    post:\n      summary: SPARQL body\n      requestBody:\n        content:\n          application/sparql-query:\n            schema:\n              type: string\n              example: \"SELECT * WHERE { ?s ?p ?o }\"\n      responses:\n        \"200\":\n          description: Success\n`;\n\n  it('should match body mode between request and example for all content types', () => {\n    const result = openApiToBruno(bodyTypesOpenApiSpec);\n    const tests = [\n      { name: 'Raw body', mode: 'text' },\n      { name: 'JSON body', mode: 'json' },\n      { name: 'XML body', mode: 'xml' },\n      { name: 'Multipart body', mode: 'multipartForm' },\n      { name: 'Form body', mode: 'formUrlEncoded' },\n      { name: 'SPARQL body', mode: 'sparql' }\n    ];\n\n    tests.forEach(({ name, mode }) => {\n      const request = result.items.find((item) => item.name === name);\n      expect(request.request.body.mode).toBe(mode);\n      expect(request.examples[0].request.body.mode).toBe(mode);\n    });\n  });\n\n  it('should generate proper XML in example (not JSON)', () => {\n    const result = openApiToBruno(bodyTypesOpenApiSpec);\n    const xmlRequest = result.items.find((item) => item.name === 'XML body');\n\n    expect(xmlRequest.examples[0].request.body.xml).toContain('<?xml');\n    expect(xmlRequest.examples[0].request.body.xml).toContain('<Root');\n    expect(xmlRequest.examples[0].request.body.xml).not.toContain('{');\n  });\n\n  it('should detect file fields in multipart example', () => {\n    const result = openApiToBruno(bodyTypesOpenApiSpec);\n    const multipartRequest = result.items.find((item) => item.name === 'Multipart body');\n    const fileField = multipartRequest.examples[0].request.body.multipartForm.find((f) => f.name === 'file');\n    expect(fileField.type).toBe('file');\n  });\n\n  it('should use default values in form example', () => {\n    const result = openApiToBruno(bodyTypesOpenApiSpec);\n    const formRequest = result.items.find((item) => item.name === 'Form body');\n    const pageField = formRequest.examples[0].request.body.formUrlEncoded.find((f) => f.name === 'page');\n    expect(pageField.value).toBe('1');\n  });\n\n  it('should use example and enum values from schema in request body', () => {\n    const openApiSpec = `\nopenapi: \"3.0.0\"\ninfo:\n  version: \"1.0.0\"\n  title: \"Test\"\nservers:\n  - url: \"https://api.example.com\"\npaths:\n  /test:\n    post:\n      summary: \"Test\"\n      requestBody:\n        content:\n          application/json:\n            schema:\n              type: object\n              properties:\n                name:\n                  type: string\n                  example: \"John\"\n                status:\n                  type: string\n                  enum: [active, inactive]\n      responses:\n        \"200\":\n          description: \"OK\"\n`;\n    const result = openApiToBruno(openApiSpec);\n    const bodyJson = JSON.parse(result.items[0].request.body.json);\n    expect(bodyJson.name).toBe('John');\n    expect(bodyJson.status).toBe('active');\n  });\n\n  it('should use schema example values in main request body (not just examples)', () => {\n    const openApiSpec = `\nopenapi: \"3.0.0\"\ninfo:\n  version: \"1.0.0\"\n  title: \"Schema Example Values Test\"\nservers:\n  - url: \"https://api.example.com\"\npaths:\n  /users:\n    post:\n      summary: \"Create user\"\n      requestBody:\n        content:\n          application/json:\n            schema:\n              type: object\n              properties:\n                id:\n                  type: integer\n                  example: 9007199254740991\n                name:\n                  type: string\n                  example: \"example string\"\n                email:\n                  type: string\n                  format: email\n                  example: \"user@example.com\"\n                status:\n                  type: string\n                  enum: [pending, active, inactive]\n                createdDate:\n                  type: string\n                  format: date\n                  example: \"2025-01-01\"\n                score:\n                  type: number\n                  example: 3.1415926535\n      responses:\n        \"201\":\n          description: \"Created\"\n`;\n    const result = openApiToBruno(openApiSpec);\n    const request = result.items[0];\n\n    // Main request body should use example values from schema\n    const bodyJson = JSON.parse(request.request.body.json);\n    expect(bodyJson.id).toBe(9007199254740991);\n    expect(bodyJson.name).toBe('example string');\n    expect(bodyJson.email).toBe('user@example.com');\n    expect(bodyJson.status).toBe('pending'); // first enum value\n    expect(bodyJson.createdDate).toBe('2025-01-01');\n    expect(bodyJson.score).toBe(3.1415926535);\n  });\n\n  it('should handle XML body with object example (not produce [object Object])', () => {\n    const openApiSpec = `\nopenapi: \"3.0.0\"\ninfo:\n  version: \"1.0.0\"\n  title: \"XML Object Example Test\"\nservers:\n  - url: \"https://api.example.com\"\npaths:\n  /user:\n    post:\n      summary: \"Create user\"\n      operationId: \"createUser\"\n      requestBody:\n        required: true\n        content:\n          application/xml:\n            schema:\n              type: object\n              example:\n                name: \"John\"\n                age: 30\n              properties:\n                name:\n                  type: string\n                age:\n                  type: integer\n      responses:\n        \"201\":\n          description: \"Created\"\n`;\n    const result = openApiToBruno(openApiSpec);\n    const request = result.items[0];\n\n    expect(request.request.body.mode).toBe('xml');\n    // Should NOT contain [object Object]\n    expect(request.request.body.xml).not.toContain('[object Object]');\n    // Should contain the example values\n    expect(request.request.body.xml).toContain('<name>John</name>');\n    expect(request.request.body.xml).toContain('<age>30</age>');\n  });\n\n  it('should handle XML body with string example (raw XML)', () => {\n    const openApiSpec = `\nopenapi: \"3.0.0\"\ninfo:\n  version: \"1.0.0\"\n  title: \"XML String Example Test\"\nservers:\n  - url: \"https://api.example.com\"\npaths:\n  /user:\n    post:\n      summary: \"Create user\"\n      operationId: \"createUser\"\n      requestBody:\n        required: true\n        content:\n          application/xml:\n            schema:\n              type: string\n              example: '<user><name>John</name></user>'\n      responses:\n        \"201\":\n          description: \"Created\"\n`;\n    const result = openApiToBruno(openApiSpec);\n    const request = result.items[0];\n\n    expect(request.request.body.mode).toBe('xml');\n    // Should preserve the raw XML string\n    expect(request.request.body.xml).toBe('<user><name>John</name></user>');\n  });\n\n  it('should not crash when array schema has no items defined', () => {\n    const openApiSpec = `\nopenapi: \"3.0.0\"\ninfo:\n  version: \"1.0.0\"\n  title: \"Array Without Items Test\"\nservers:\n  - url: \"https://api.example.com\"\npaths:\n  /items:\n    post:\n      summary: \"Create items\"\n      operationId: \"createItems\"\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              type: array\n      responses:\n        \"201\":\n          description: \"Created\"\n`;\n    // Should not throw an error\n    expect(() => openApiToBruno(openApiSpec)).not.toThrow();\n\n    const result = openApiToBruno(openApiSpec);\n    const request = result.items[0];\n\n    expect(request.request.body.mode).toBe('json');\n    // Should produce an empty array\n    expect(request.request.body.json).toBe('[]');\n  });\n});\n\ndescribe('content-level example vs examples priority', () => {\n  it('should prefer singular example over examples (plural) and fall back to examples when example is absent', () => {\n    const spec = `\nopenapi: \"3.1.0\"\ninfo:\n  version: \"1.0.0\"\n  title: \"Test\"\nservers:\n  - url: \"https://api.example.com\"\npaths:\n  /both:\n    post:\n      summary: \"Both example and examples\"\n      requestBody:\n        content:\n          application/json:\n            schema:\n              type: object\n              properties:\n                name:\n                  type: string\n            example:\n              name: \"from singular\"\n            examples:\n              first:\n                value:\n                  name: \"from plural\"\n      responses:\n        \"200\":\n          description: \"OK\"\n  /only-examples:\n    post:\n      summary: \"Only examples plural\"\n      requestBody:\n        content:\n          application/json:\n            schema:\n              type: object\n              properties:\n                name:\n                  type: string\n            examples:\n              first:\n                value:\n                  name: \"from plural\"\n      responses:\n        \"200\":\n          description: \"OK\"\n  /schema-wins:\n    post:\n      summary: \"Schema example wins over all\"\n      requestBody:\n        content:\n          application/json:\n            schema:\n              type: object\n              example:\n                name: \"from schema\"\n            example:\n              name: \"from content\"\n            examples:\n              first:\n                value:\n                  name: \"from plural\"\n      responses:\n        \"200\":\n          description: \"OK\"\n`;\n    const result = openApiToBruno(spec);\n\n    // When both example and examples exist, singular example wins\n    const bothBody = JSON.parse(result.items.find((i) => i.name === 'Both example and examples').request.body.json);\n    expect(bothBody.name).toBe('from singular');\n\n    // When only examples exists, it is used as fallback\n    const pluralBody = JSON.parse(result.items.find((i) => i.name === 'Only examples plural').request.body.json);\n    expect(pluralBody.name).toBe('from plural');\n\n    // schema.example priority over both content-level example and examples\n    const schemaBody = JSON.parse(result.items.find((i) => i.name === 'Schema example wins over all').request.body.json);\n    expect(schemaBody.name).toBe('from schema');\n  });\n});\n\ndescribe('content-level example values for each body type', () => {\n  const spec = `\nopenapi: \"3.1.0\"\ninfo:\n  version: \"1.0.0\"\n  title: \"Test\"\nservers:\n  - url: \"https://api.example.com\"\npaths:\n  /json:\n    post:\n      summary: \"JSON body\"\n      requestBody:\n        content:\n          application/json:\n            schema:\n              type: object\n              properties:\n                name:\n                  type: string\n            example:\n              name: \"json example\"\n      responses:\n        \"200\":\n          description: \"OK\"\n  /xml:\n    post:\n      summary: \"XML body\"\n      requestBody:\n        content:\n          application/xml:\n            schema:\n              type: object\n              properties:\n                name:\n                  type: string\n            example:\n              name: \"xml example\"\n      responses:\n        \"200\":\n          description: \"OK\"\n  /text:\n    post:\n      summary: \"Text body\"\n      requestBody:\n        content:\n          text/plain:\n            schema:\n              type: string\n            example: \"plain text example\"\n      responses:\n        \"200\":\n          description: \"OK\"\n  /form:\n    post:\n      summary: \"Form body\"\n      requestBody:\n        content:\n          application/x-www-form-urlencoded:\n            schema:\n              type: object\n              properties:\n                username:\n                  type: string\n            example:\n              username: \"form_user\"\n      responses:\n        \"200\":\n          description: \"OK\"\n  /multipart:\n    post:\n      summary: \"Multipart body\"\n      requestBody:\n        content:\n          multipart/form-data:\n            schema:\n              type: object\n              properties:\n                desc:\n                  type: string\n            example:\n              desc: \"multipart desc\"\n      responses:\n        \"200\":\n          description: \"OK\"\n  /sparql:\n    post:\n      summary: \"SPARQL body\"\n      requestBody:\n        content:\n          application/sparql-query:\n            schema:\n              type: string\n            example: \"SELECT * WHERE { ?s ?p ?o }\"\n      responses:\n        \"200\":\n          description: \"OK\"\n`;\n\n  it('should import content-level example for JSON body', () => {\n    const result = openApiToBruno(spec);\n    const body = JSON.parse(result.items.find((i) => i.name === 'JSON body').request.body.json);\n    expect(body.name).toBe('json example');\n  });\n\n  it('should import content-level example for XML body', () => {\n    const result = openApiToBruno(spec);\n    const xml = result.items.find((i) => i.name === 'XML body').request.body.xml;\n    expect(xml).toContain('<name>xml example</name>');\n  });\n\n  it('should import content-level example for text/plain body', () => {\n    const result = openApiToBruno(spec);\n    const text = result.items.find((i) => i.name === 'Text body').request.body.text;\n    expect(text).toBe('plain text example');\n  });\n\n  it('should import content-level example for form-urlencoded body', () => {\n    const result = openApiToBruno(spec);\n    const field = result.items.find((i) => i.name === 'Form body').request.body.formUrlEncoded.find((f) => f.name === 'username');\n    expect(field.value).toBe('form_user');\n  });\n\n  it('should import content-level example for multipart body', () => {\n    const result = openApiToBruno(spec);\n    const field = result.items.find((i) => i.name === 'Multipart body').request.body.multipartForm.find((f) => f.name === 'desc');\n    expect(field.value).toBe('multipart desc');\n  });\n\n  it('should import content-level example for SPARQL body', () => {\n    const result = openApiToBruno(spec);\n    const sparql = result.items.find((i) => i.name === 'SPARQL body').request.body.sparql;\n    expect(sparql).toBe('SELECT * WHERE { ?s ?p ?o }');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/openapi/openapi-to-bruno/openapi-circular-references.spec.js",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport openApiToBruno from '../../../src/openapi/openapi-to-bruno';\n\ndescribe('openapi-circular-references', () => {\n  it('should handle simple circular references in schema correctly', async () => {\n    const brunoCollection = openApiToBruno(circularRefsData);\n\n    expect(brunoCollection).toMatchObject(circularRefsOutput);\n  });\n\n  it('should handle complex circular reference chains correctly', async () => {\n    const brunoCollection = openApiToBruno(complexCircularRefsData);\n\n    expect(brunoCollection).toMatchObject(circularRefsOutput);\n  });\n});\n\nconst circularRefsData = {\n  components: {\n    schemas: {\n      schema_1: {\n        additionalProperties: false,\n        description: 'schema_1',\n        properties: {\n          conditions: {\n            $ref: '#/components/schemas/schema_1'\n          }\n        },\n        type: 'object'\n      },\n      schema_2: {\n        additionalProperties: false,\n        description: 'schema_2',\n        properties: {\n          conditionGroup: {\n            description: 'nested schema_1',\n            items: { $ref: '#/components/schemas/schema_1' },\n            type: 'array'\n          },\n          operation: {\n            description: 'operation',\n            enum: ['ANY', 'ALL'],\n            type: 'string'\n          }\n        },\n        type: 'object'\n      }\n    }\n  },\n  info: {\n    description: 'circular reference openapi sample json spec',\n    title: 'circular reference openapi sample json spec',\n    version: '0.1'\n  },\n  openapi: '3.0.1',\n  paths: {\n    '/': {\n      post: {\n        deprecated: false,\n        description: 'echo ping api',\n        operationId: 'echo ping',\n        parameters: [],\n        requestBody: {\n          content: {\n            'application/json': {\n              schema: {\n                $ref: '#/components/schemas/schema_1'\n              }\n            }\n          },\n          description: 'echo ping api',\n          required: true\n        },\n        responses: {\n          200: {\n            content: {\n              'application/json': {\n                example: 'ping'\n              }\n            },\n            description: 'Returned if the request is successful.'\n          }\n        }\n      }\n    }\n  },\n  servers: [{ url: 'https://echo.usebruno.com' }]\n};\n\n// More complex circular reference test with a longer chain\nconst complexCircularRefsData = {\n  components: {\n    schemas: {\n      schema_1: {\n        additionalProperties: false,\n        description: 'schema_1',\n        properties: {\n          conditionGroup: {\n            description: 'nested schema_1',\n            items: { $ref: '#/components/schemas/schema_2' },\n            type: 'array'\n          }\n        },\n        type: 'object'\n      },\n      schema_2: {\n        additionalProperties: false,\n        description: 'schema_2',\n        properties: {\n          conditionGroup: {\n            description: 'nested schema_2',\n            items: { $ref: '#/components/schemas/schema_3' },\n            type: 'array'\n          }\n        },\n        type: 'object'\n      },\n      schema_3: {\n        additionalProperties: false,\n        description: 'schema_3',\n        properties: {\n          conditionGroup: {\n            description: 'nested schema_3',\n            items: { $ref: '#/components/schemas/schema_4' },\n            type: 'array'\n          }\n        },\n        type: 'object'\n      },\n      schema_4: {\n        additionalProperties: false,\n        description: 'schema_4',\n        properties: {\n          conditionGroup: {\n            description: 'nested schema_4',\n            items: { $ref: '#/components/schemas/schema_5' },\n            type: 'array'\n          }\n        },\n        type: 'object'\n      },\n      schema_5: {\n        additionalProperties: false,\n        description: 'schema_4',\n        properties: {\n          conditionGroup: {\n            description: 'nested schema_5',\n            items: { $ref: '#/components/schemas/schema_1' },\n            type: 'array'\n          }\n        },\n        type: 'object'\n      },\n      schema_6: {\n        additionalProperties: false,\n        description: 'schema_3',\n        properties: {\n          conditionGroup: {\n            description: 'nested schema_3',\n            items: { $ref: '#/components/schemas/schema_1' },\n            type: 'array'\n          },\n          operation: {\n            description: 'operation',\n            enum: ['ANY', 'ALL'],\n            type: 'string'\n          }\n        },\n        type: 'object'\n      }\n    }\n  },\n  info: {\n    description: 'circular reference openapi sample json spec',\n    title: 'circular reference openapi sample json spec',\n    version: '0.1'\n  },\n  openapi: '3.0.1',\n  paths: {\n    '/': {\n      post: {\n        deprecated: false,\n        description: 'echo ping api',\n        operationId: 'echo ping',\n        parameters: [],\n        requestBody: {\n          content: {\n            'application/json': {\n              schema: {\n                $ref: '#/components/schemas/schema_1'\n              }\n            }\n          },\n          description: 'echo ping api',\n          required: true\n        },\n        responses: {\n          200: {\n            content: {\n              'application/json': {\n                example: 'ping'\n              }\n            },\n            description: 'Returned if the request is successful.'\n          }\n        }\n      }\n    }\n  },\n  servers: [{ url: 'https://echo.usebruno.com' }]\n};\n\nconst circularRefsOutput = {\n  environments: [\n    {\n      name: 'Environment 1',\n      variables: [\n        {\n          enabled: true,\n          name: 'baseUrl',\n          secret: false,\n          type: 'text',\n          value: 'https://echo.usebruno.com'\n        }\n      ]\n    }\n  ],\n  items: [\n    {\n      name: 'echo ping',\n      type: 'http-request',\n      request: {\n        url: '{{baseUrl}}/',\n        method: 'POST',\n        auth: {\n          mode: 'inherit'\n        },\n        headers: [],\n        params: [],\n        body: {\n          mode: 'json'\n        }\n      }\n    }\n  ],\n  name: 'circular reference openapi sample json spec',\n  version: '1'\n};\n"
  },
  {
    "path": "packages/bruno-converters/tests/openapi/openapi-to-bruno/openapi-import-grouping.spec.js",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport openApiToBruno from '../../../src/openapi/openapi-to-bruno';\n\nconst openApiSpec = {\n  openapi: '3.0.0',\n  info: { title: 'Parameter API', version: '1.0.0' },\n  servers: [{ url: 'https://api.example.com' }],\n  paths: {\n    '/{id}': {\n      get: {\n        summary: 'Get by ID',\n        operationId: 'getById',\n        responses: { 200: { description: 'OK' } }\n      }\n    },\n    '/{id}/{subId}': {\n      get: {\n        summary: 'Get by ID and sub ID',\n        operationId: 'getByIdAndSubId',\n        responses: { 200: { description: 'OK' } }\n      }\n    }\n  }\n};\n\ndescribe('openapi-import-grouping', () => {\n  it('should handle path based grouping', () => {\n    const result = openApiToBruno(openApiSpec, { groupBy: 'path' });\n\n    // Should have one folder containing both requests\n    expect(result.items).toHaveLength(1);\n\n    const folder = result.items[0];\n    expect(folder.name).toBe('{id}');\n    expect(folder.type).toBe('folder');\n\n    // Folder should contain one request and one subfolder\n    expect(folder.items).toHaveLength(2);\n\n    const requests = folder.items.filter((item) => item.type === 'http-request');\n    const subfolders = folder.items.filter((item) => item.type === 'folder');\n\n    expect(requests).toHaveLength(1);\n    expect(subfolders).toHaveLength(1);\n\n    // Check request name\n    expect(requests[0].name).toBe('Get by ID');\n    expect(requests[0].request.url).toBe('{{baseUrl}}/:id');\n\n    // Check subfolder\n    expect(subfolders[0].name).toBe('{subId}');\n    expect(subfolders[0].type).toBe('folder');\n    expect(subfolders[0].items).toHaveLength(1);\n    expect(subfolders[0].items[0].name).toBe('Get by ID and sub ID');\n    expect(subfolders[0].items[0].request.url).toBe('{{baseUrl}}/:id/:subId');\n  });\n\n  it('should handle tag based grouping', () => {\n    const result = openApiToBruno(openApiSpec, { groupBy: 'tags' });\n\n    // With tags grouping, requests without tags should be ungrouped\n    expect(result.items).toHaveLength(2);\n\n    // Both should be individual requests (not in folders)\n    result.items.forEach((item) => {\n      expect(item.type).toBe('http-request');\n    });\n\n    // Check request names\n    const requestNames = result.items.map((req) => req.name);\n    expect(requestNames).toContain('Get by ID');\n    expect(requestNames).toContain('Get by ID and sub ID');\n\n    // Check request URLs\n    const urls = result.items.map((req) => req.request.url);\n    expect(urls).toContain('{{baseUrl}}/:id');\n    expect(urls).toContain('{{baseUrl}}/:id/:subId');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/openapi/openapi-to-bruno/openapi-path-parameters.spec.js",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport openApiToBruno from '../../../src/openapi/openapi-to-bruno';\n\ndescribe('openapi path-item level parameters', () => {\n  it('should apply path-item parameters to all operations when no operation params exist', () => {\n    const spec = `\nopenapi: '3.0.0'\ninfo:\n  title: 'Path Params API'\n  version: '1.0.0'\nservers:\n  - url: 'https://api.example.com'\npaths:\n  /items/{itemId}:\n    parameters:\n      - name: itemId\n        in: path\n        required: true\n        schema:\n          type: string\n        description: 'The item ID'\n    get:\n      summary: 'Get item'\n      operationId: 'getItem'\n      responses:\n        '200':\n          description: 'OK'\n    put:\n      summary: 'Update item'\n      operationId: 'updateItem'\n      responses:\n        '200':\n          description: 'OK'\n`;\n    const result = openApiToBruno(spec);\n\n    // Both GET and PUT should have the itemId path parameter\n    const getItem = result.items.find((i) => i.name === 'Get item');\n    const putItem = result.items.find((i) => i.name === 'Update item');\n\n    expect(getItem.request.params).toEqual(\n      expect.arrayContaining([expect.objectContaining({ name: 'itemId', type: 'path', enabled: true })])\n    );\n    expect(putItem.request.params).toEqual(\n      expect.arrayContaining([expect.objectContaining({ name: 'itemId', type: 'path', enabled: true })])\n    );\n  });\n\n  it('should preserve operation-only parameters unchanged', () => {\n    const spec = `\nopenapi: '3.0.0'\ninfo:\n  title: 'Op Only Params API'\n  version: '1.0.0'\nservers:\n  - url: 'https://api.example.com'\npaths:\n  /search:\n    get:\n      summary: 'Search'\n      operationId: 'search'\n      parameters:\n        - name: q\n          in: query\n          required: true\n          schema:\n            type: string\n          description: 'Search query'\n      responses:\n        '200':\n          description: 'OK'\n`;\n    const result = openApiToBruno(spec);\n    const search = result.items.find((i) => i.name === 'Search');\n    const queryParams = search.request.params.filter((p) => p.type === 'query');\n    expect(queryParams).toHaveLength(1);\n    expect(queryParams[0].name).toBe('q');\n  });\n\n  it('should merge path-item and operation params with no overlap', () => {\n    const spec = `\nopenapi: '3.0.0'\ninfo:\n  title: 'Merge No Overlap API'\n  version: '1.0.0'\nservers:\n  - url: 'https://api.example.com'\npaths:\n  /items/{itemId}:\n    parameters:\n      - name: itemId\n        in: path\n        required: true\n        schema:\n          type: string\n    get:\n      summary: 'Get item'\n      operationId: 'getItem'\n      parameters:\n        - name: fields\n          in: query\n          required: false\n          schema:\n            type: string\n          description: 'Fields to include'\n      responses:\n        '200':\n          description: 'OK'\n`;\n    const result = openApiToBruno(spec);\n    const getItem = result.items.find((i) => i.name === 'Get item');\n\n    // Should have both the path param from path-item and the query param from operation\n    const pathParams = getItem.request.params.filter((p) => p.type === 'path');\n    const queryParams = getItem.request.params.filter((p) => p.type === 'query');\n    expect(pathParams).toHaveLength(1);\n    expect(pathParams[0].name).toBe('itemId');\n    expect(queryParams).toHaveLength(1);\n    expect(queryParams[0].name).toBe('fields');\n  });\n\n  it('should let operation param override path-item param with same name and in', () => {\n    const spec = `\nopenapi: '3.0.0'\ninfo:\n  title: 'Override API'\n  version: '1.0.0'\nservers:\n  - url: 'https://api.example.com'\npaths:\n  /items:\n    parameters:\n      - name: limit\n        in: query\n        required: false\n        schema:\n          type: integer\n        description: 'Default limit from path-item'\n    get:\n      summary: 'List items'\n      operationId: 'listItems'\n      parameters:\n        - name: limit\n          in: query\n          required: true\n          schema:\n            type: integer\n            maximum: 50\n          description: 'Override limit for list operation'\n      responses:\n        '200':\n          description: 'OK'\n`;\n    const result = openApiToBruno(spec);\n    const listItems = result.items.find((i) => i.name === 'List items');\n\n    // Should have exactly one 'limit' query param -- the operation-level one\n    const limitParams = listItems.request.params.filter((p) => p.name === 'limit');\n    expect(limitParams).toHaveLength(1);\n    expect(limitParams[0].description).toBe('Override limit for list operation');\n    expect(limitParams[0].enabled).toBe(true); // required=true from operation\n  });\n\n  it('should handle path-item params with different in values (query, path, header)', () => {\n    const spec = `\nopenapi: '3.0.0'\ninfo:\n  title: 'Mixed In Values API'\n  version: '1.0.0'\nservers:\n  - url: 'https://api.example.com'\npaths:\n  /resources/{resourceId}:\n    parameters:\n      - name: resourceId\n        in: path\n        required: true\n        schema:\n          type: string\n      - name: format\n        in: query\n        required: false\n        schema:\n          type: string\n        description: 'Response format'\n      - name: X-Request-ID\n        in: header\n        required: false\n        schema:\n          type: string\n        description: 'Request tracking ID'\n    get:\n      summary: 'Get resource'\n      operationId: 'getResource'\n      responses:\n        '200':\n          description: 'OK'\n`;\n    const result = openApiToBruno(spec);\n    const getResource = result.items.find((i) => i.name === 'Get resource');\n\n    const pathParams = getResource.request.params.filter((p) => p.type === 'path');\n    const queryParams = getResource.request.params.filter((p) => p.type === 'query');\n    const headers = getResource.request.headers;\n\n    expect(pathParams).toHaveLength(1);\n    expect(pathParams[0].name).toBe('resourceId');\n\n    expect(queryParams).toHaveLength(1);\n    expect(queryParams[0].name).toBe('format');\n\n    // Header params end up in request.headers, not request.params\n    const trackingHeader = headers.find((h) => h.name === 'X-Request-ID');\n    expect(trackingHeader).toBeDefined();\n    expect(trackingHeader.description).toBe('Request tracking ID');\n  });\n});\n\ndescribe('openapi parameter default and example values', () => {\n  it('should use param.example as the value when present', () => {\n    const spec = `\nopenapi: '3.0.0'\ninfo:\n  title: 'Param Example API'\n  version: '1.0.0'\nservers:\n  - url: 'https://api.example.com'\npaths:\n  /search:\n    get:\n      summary: 'Search'\n      operationId: 'search'\n      parameters:\n        - name: q\n          in: query\n          required: true\n          example: 'hello world'\n          schema:\n            type: string\n      responses:\n        '200':\n          description: 'OK'\n`;\n    const result = openApiToBruno(spec);\n    const search = result.items.find((i) => i.name === 'Search');\n    const qParam = search.request.params.find((p) => p.name === 'q');\n    expect(qParam.value).toBe('hello world');\n  });\n\n  it('should use schema.default when param.example is not present', () => {\n    const spec = `\nopenapi: '3.0.0'\ninfo:\n  title: 'Schema Default API'\n  version: '1.0.0'\nservers:\n  - url: 'https://api.example.com'\npaths:\n  /items:\n    get:\n      summary: 'List items'\n      operationId: 'listItems'\n      parameters:\n        - name: limit\n          in: query\n          required: false\n          schema:\n            type: integer\n            default: 20\n      responses:\n        '200':\n          description: 'OK'\n`;\n    const result = openApiToBruno(spec);\n    const listItems = result.items.find((i) => i.name === 'List items');\n    const limitParam = listItems.request.params.find((p) => p.name === 'limit');\n    expect(limitParam.value).toBe('20');\n  });\n\n  it('should use schema.example when no param.example or schema.default exists', () => {\n    const spec = `\nopenapi: '3.0.0'\ninfo:\n  title: 'Schema Example API'\n  version: '1.0.0'\nservers:\n  - url: 'https://api.example.com'\npaths:\n  /users/{userId}:\n    get:\n      summary: 'Get user'\n      operationId: 'getUser'\n      parameters:\n        - name: userId\n          in: path\n          required: true\n          schema:\n            type: string\n            example: 'user-123'\n      responses:\n        '200':\n          description: 'OK'\n`;\n    const result = openApiToBruno(spec);\n    const getUser = result.items.find((i) => i.name === 'Get user');\n    const userIdParam = getUser.request.params.find((p) => p.name === 'userId');\n    expect(userIdParam.value).toBe('user-123');\n  });\n\n  it('should fall back to empty string when no example or default is present', () => {\n    const spec = `\nopenapi: '3.0.0'\ninfo:\n  title: 'No Default API'\n  version: '1.0.0'\nservers:\n  - url: 'https://api.example.com'\npaths:\n  /items:\n    get:\n      summary: 'List items'\n      operationId: 'listItems'\n      parameters:\n        - name: filter\n          in: query\n          required: false\n          schema:\n            type: string\n      responses:\n        '200':\n          description: 'OK'\n`;\n    const result = openApiToBruno(spec);\n    const listItems = result.items.find((i) => i.name === 'List items');\n    const filterParam = listItems.request.params.find((p) => p.name === 'filter');\n    expect(filterParam.value).toBe('');\n  });\n\n  it('should use property example/default for object schema expansion', () => {\n    const spec = {\n      openapi: '3.0.0',\n      info: { title: 'Object Schema API', version: '1.0.0' },\n      servers: [{ url: 'https://api.example.com' }],\n      paths: {\n        '/items': {\n          get: {\n            summary: 'List items',\n            operationId: 'listItems',\n            parameters: [\n              {\n                name: 'pagination',\n                in: 'query',\n                schema: {\n                  type: 'object',\n                  properties: {\n                    page: { type: 'integer', example: 1 },\n                    size: { type: 'integer', default: 25 },\n                    sort: { type: 'string' }\n                  },\n                  required: ['page']\n                }\n              }\n            ],\n            responses: { 200: { description: 'OK' } }\n          }\n        }\n      }\n    };\n    const result = openApiToBruno(spec);\n    const listItems = result.items.find((i) => i.name === 'List items');\n    const queryParams = listItems.request.params.filter((p) => p.type === 'query');\n\n    const pageParam = queryParams.find((p) => p.name === 'page');\n    expect(pageParam.value).toBe('1'); // from prop.example\n\n    const sizeParam = queryParams.find((p) => p.name === 'size');\n    expect(sizeParam.value).toBe('25'); // from prop.default\n\n    const sortParam = queryParams.find((p) => p.name === 'sort');\n    expect(sortParam.value).toBe(''); // no example or default\n  });\n\n  it('should use top-level schema.example object for property values when no prop-level example', () => {\n    const spec = {\n      openapi: '3.0.0',\n      info: { title: 'Schema Example API', version: '1.0.0' },\n      servers: [{ url: 'https://api.example.com' }],\n      paths: {\n        '/books': {\n          get: {\n            summary: 'List books',\n            operationId: 'listBooks',\n            parameters: [\n              {\n                name: 'filter',\n                in: 'query',\n                schema: {\n                  type: 'object',\n                  example: {\n                    title: 'The Great Gatsby',\n                    author: 'F. Scott Fitzgerald'\n                  },\n                  properties: {\n                    title: { type: 'string' },\n                    author: { type: 'string' },\n                    genre: { type: 'string' }\n                  }\n                }\n              }\n            ],\n            responses: { 200: { description: 'OK' } }\n          }\n        }\n      }\n    };\n    const result = openApiToBruno(spec);\n    const listBooks = result.items.find((i) => i.name === 'List books');\n    const queryParams = listBooks.request.params.filter((p) => p.type === 'query');\n\n    const titleParam = queryParams.find((p) => p.name === 'title');\n    expect(titleParam.value).toBe('The Great Gatsby'); // from schema.example.title\n\n    const authorParam = queryParams.find((p) => p.name === 'author');\n    expect(authorParam.value).toBe('F. Scott Fitzgerald'); // from schema.example.author\n\n    const genreParam = queryParams.find((p) => p.name === 'genre');\n    expect(genreParam.value).toBe(''); // not in schema.example, no prop example/default\n  });\n\n  it('should use first enum value as fallback when no example or default', () => {\n    const spec = {\n      openapi: '3.0.0',\n      info: { title: 'Enum Fallback API', version: '1.0.0' },\n      servers: [{ url: 'https://api.example.com' }],\n      paths: {\n        '/books': {\n          get: {\n            summary: 'List books',\n            operationId: 'listBooks',\n            parameters: [\n              {\n                name: 'filter',\n                in: 'query',\n                schema: {\n                  type: 'object',\n                  properties: {\n                    genre: { type: 'string', enum: ['Fiction', 'Non-Fiction', 'Science'] },\n                    format: { type: 'string', enum: ['hardcover', 'paperback'] },\n                    sort: { type: 'string' }\n                  }\n                }\n              }\n            ],\n            responses: { 200: { description: 'OK' } }\n          }\n        }\n      }\n    };\n    const result = openApiToBruno(spec);\n    const listBooks = result.items.find((i) => i.name === 'List books');\n    const queryParams = listBooks.request.params.filter((p) => p.type === 'query');\n\n    const genreParam = queryParams.find((p) => p.name === 'genre');\n    expect(genreParam.value).toBe('Fiction'); // first enum value\n\n    const formatParam = queryParams.find((p) => p.name === 'format');\n    expect(formatParam.value).toBe('hardcover'); // first enum value\n\n    const sortParam = queryParams.find((p) => p.name === 'sort');\n    expect(sortParam.value).toBe(''); // no enum, no example, no default\n  });\n\n  it('should prefer prop.example over schema.example[propName]', () => {\n    const spec = {\n      openapi: '3.0.0',\n      info: { title: 'Priority API', version: '1.0.0' },\n      servers: [{ url: 'https://api.example.com' }],\n      paths: {\n        '/books': {\n          get: {\n            summary: 'List books',\n            operationId: 'listBooks',\n            parameters: [\n              {\n                name: 'filter',\n                in: 'query',\n                schema: {\n                  type: 'object',\n                  example: {\n                    title: 'Schema-level title',\n                    author: 'Schema-level author'\n                  },\n                  properties: {\n                    title: { type: 'string', example: 'Property-level title' },\n                    author: { type: 'string' }\n                  }\n                }\n              }\n            ],\n            responses: { 200: { description: 'OK' } }\n          }\n        }\n      }\n    };\n    const result = openApiToBruno(spec);\n    const listBooks = result.items.find((i) => i.name === 'List books');\n    const queryParams = listBooks.request.params.filter((p) => p.type === 'query');\n\n    const titleParam = queryParams.find((p) => p.name === 'title');\n    expect(titleParam.value).toBe('Property-level title'); // prop.example wins over schema.example\n\n    const authorParam = queryParams.find((p) => p.name === 'author');\n    expect(authorParam.value).toBe('Schema-level author'); // falls back to schema.example\n  });\n\n  it('should prefer param.example over schema.default', () => {\n    const spec = `\nopenapi: '3.0.0'\ninfo:\n  title: 'Priority API'\n  version: '1.0.0'\nservers:\n  - url: 'https://api.example.com'\npaths:\n  /items:\n    get:\n      summary: 'List items'\n      operationId: 'listItems'\n      parameters:\n        - name: limit\n          in: query\n          required: false\n          example: 50\n          schema:\n            type: integer\n            default: 20\n            example: 10\n      responses:\n        '200':\n          description: 'OK'\n`;\n    const result = openApiToBruno(spec);\n    const listItems = result.items.find((i) => i.name === 'List items');\n    const limitParam = listItems.request.params.find((p) => p.name === 'limit');\n    expect(limitParam.value).toBe('50'); // param.example wins over schema.default and schema.example\n  });\n\n  it('should use schema.examples (plural) when no other example or default exists', () => {\n    const spec = {\n      openapi: '3.0.0',\n      info: { title: 'Schema Examples API', version: '1.0.0' },\n      servers: [{ url: 'https://api.example.com' }],\n      paths: {\n        '/items': {\n          get: {\n            summary: 'List items',\n            operationId: 'listItems',\n            parameters: [\n              {\n                name: 'status',\n                in: 'query',\n                schema: {\n                  type: 'string',\n                  examples: ['active', 'archived']\n                }\n              }\n            ],\n            responses: { 200: { description: 'OK' } }\n          }\n        }\n      }\n    };\n    const result = openApiToBruno(spec);\n    const listItems = result.items.find((i) => i.name === 'List items');\n    const statusParam = listItems.request.params.find((p) => p.name === 'status');\n    expect(statusParam.value).toBe('active'); // first schema.examples value\n  });\n\n  it('should use schema.minimum as fallback when no example, default, or enum exists', () => {\n    const spec = {\n      openapi: '3.0.0',\n      info: { title: 'Minimum API', version: '1.0.0' },\n      servers: [{ url: 'https://api.example.com' }],\n      paths: {\n        '/items': {\n          get: {\n            summary: 'List items',\n            operationId: 'listItems',\n            parameters: [\n              {\n                name: 'page',\n                in: 'query',\n                schema: {\n                  type: 'integer',\n                  minimum: 1,\n                  maximum: 100\n                }\n              }\n            ],\n            responses: { 200: { description: 'OK' } }\n          }\n        }\n      }\n    };\n    const result = openApiToBruno(spec);\n    const listItems = result.items.find((i) => i.name === 'List items');\n    const pageParam = listItems.request.params.find((p) => p.name === 'page');\n    expect(pageParam.value).toBe('1'); // schema.minimum as fallback\n  });\n});\n\n// Tests backward-compat handling of non-standard in: 'querystring' (some importers emit this instead of 'query')\ndescribe('openapi querystring parameter location', () => {\n  it('should map in: \"querystring\" to query type', () => {\n    const spec = {\n      openapi: '3.0.0',\n      info: { title: 'Querystring API', version: '1.0.0' },\n      servers: [{ url: 'https://api.example.com' }],\n      paths: {\n        '/search': {\n          get: {\n            summary: 'Search',\n            operationId: 'search',\n            parameters: [\n              {\n                name: 'q',\n                in: 'querystring',\n                required: true,\n                schema: { type: 'string' }\n              }\n            ],\n            responses: { 200: { description: 'OK' } }\n          }\n        }\n      }\n    };\n    const result = openApiToBruno(spec);\n    const search = result.items.find((i) => i.name === 'Search');\n    const queryParams = search.request.params.filter((p) => p.type === 'query');\n    expect(queryParams).toHaveLength(1);\n    expect(queryParams[0].name).toBe('q');\n    expect(queryParams[0].enabled).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/openapi/openapi-to-bruno/openapi-server-variables.spec.js",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport openApiToBruno from '../../../src/openapi/openapi-to-bruno';\n\ndescribe('openapi server variables to environment variables', () => {\n  it('should create individual environment variables from server variables', () => {\n    const spec = `\nopenapi: '3.0.0'\ninfo:\n  title: 'Server Variables API'\n  version: '1.0.0'\npaths:\n  /test:\n    get:\n      summary: 'Test'\n      responses:\n        '200':\n          description: 'OK'\nservers:\n  - url: '{protocol}://{host}:{port}/v1'\n    description: 'Main Server'\n    variables:\n      protocol:\n        default: 'https'\n      host:\n        default: 'api.example.com'\n      port:\n        default: '443'\n`;\n    const result = openApiToBruno(spec);\n    const env = result.environments[0];\n    expect(env.name).toBe('Main Server');\n    expect(env.variables).toHaveLength(4); // baseUrl + 3 variables\n\n    const baseUrl = env.variables.find((v) => v.name === 'baseUrl');\n    expect(baseUrl.value).toBe('{{protocol}}://{{host}}:{{port}}/v1');\n    expect(baseUrl.enabled).toBe(true);\n    expect(baseUrl.type).toBe('text');\n\n    const protocol = env.variables.find((v) => v.name === 'protocol');\n    expect(protocol.value).toBe('https');\n    expect(protocol.enabled).toBe(true);\n    expect(protocol.type).toBe('text');\n\n    const host = env.variables.find((v) => v.name === 'host');\n    expect(host.value).toBe('api.example.com');\n\n    const port = env.variables.find((v) => v.name === 'port');\n    expect(port.value).toBe('443');\n  });\n\n  it('should use plain resolved URL when server has no variables', () => {\n    const spec = `\nopenapi: '3.0.0'\ninfo:\n  title: 'No Variables API'\n  version: '1.0.0'\npaths:\n  /test:\n    get:\n      summary: 'Test'\n      responses:\n        '200':\n          description: 'OK'\nservers:\n  - url: 'https://api.example.com/v1'\n`;\n    const result = openApiToBruno(spec);\n    const env = result.environments[0];\n    expect(env.variables).toHaveLength(1);\n    expect(env.variables[0].name).toBe('baseUrl');\n    expect(env.variables[0].value).toBe('https://api.example.com/v1');\n  });\n\n  it('should handle multiple servers with different variables', () => {\n    const spec = `\nopenapi: '3.0.0'\ninfo:\n  title: 'Multi Server API'\n  version: '1.0.0'\npaths:\n  /test:\n    get:\n      summary: 'Test'\n      responses:\n        '200':\n          description: 'OK'\nservers:\n  - url: '{protocol}://{host}/v1'\n    description: 'Production'\n    variables:\n      protocol:\n        default: 'https'\n      host:\n        default: 'api.prod.com'\n  - url: 'https://staging.example.com/v1'\n    description: 'Staging'\n`;\n    const result = openApiToBruno(spec);\n\n    // Production env: template + variables\n    const prodEnv = result.environments[0];\n    expect(prodEnv.name).toBe('Production');\n    expect(prodEnv.variables).toHaveLength(3); // baseUrl + protocol + host\n    expect(prodEnv.variables.find((v) => v.name === 'baseUrl').value).toBe('{{protocol}}://{{host}}/v1');\n    expect(prodEnv.variables.find((v) => v.name === 'protocol').value).toBe('https');\n    expect(prodEnv.variables.find((v) => v.name === 'host').value).toBe('api.prod.com');\n\n    // Staging env: plain URL, no extra variables\n    const stagingEnv = result.environments[1];\n    expect(stagingEnv.name).toBe('Staging');\n    expect(stagingEnv.variables).toHaveLength(1);\n    expect(stagingEnv.variables[0].value).toBe('https://staging.example.com/v1');\n  });\n\n  it('should use first enum value when no default is provided', () => {\n    const spec = `\nopenapi: '3.0.0'\ninfo:\n  title: 'Enum Only API'\n  version: '1.0.0'\npaths:\n  /test:\n    get:\n      summary: 'Test'\n      responses:\n        '200':\n          description: 'OK'\nservers:\n  - url: '{protocol}://example.com'\n    variables:\n      protocol:\n        enum:\n          - https\n          - http\n`;\n    const result = openApiToBruno(spec);\n    const env = result.environments[0];\n    const protocolVar = env.variables.find((v) => v.name === 'protocol');\n    expect(protocolVar.value).toBe('https');\n  });\n\n  it('should use empty string when variable has neither default nor enum', () => {\n    const spec = `\nopenapi: '3.0.0'\ninfo:\n  title: 'No Default No Enum API'\n  version: '1.0.0'\npaths:\n  /test:\n    get:\n      summary: 'Test'\n      responses:\n        '200':\n          description: 'OK'\nservers:\n  - url: '{basePath}/v1'\n    variables:\n      basePath: {}\n`;\n    const result = openApiToBruno(spec);\n    const env = result.environments[0];\n    const basePathVar = env.variables.find((v) => v.name === 'basePath');\n    expect(basePathVar.value).toBe('');\n  });\n\n  it('should strip trailing slash from template baseUrl', () => {\n    const spec = `\nopenapi: '3.0.0'\ninfo:\n  title: 'Trailing Slash API'\n  version: '1.0.0'\npaths:\n  /test:\n    get:\n      summary: 'Test'\n      responses:\n        '200':\n          description: 'OK'\nservers:\n  - url: '{protocol}://{host}/'\n    variables:\n      protocol:\n        default: 'https'\n      host:\n        default: 'api.example.com'\n`;\n    const result = openApiToBruno(spec);\n    const env = result.environments[0];\n    const baseUrl = env.variables.find((v) => v.name === 'baseUrl');\n    expect(baseUrl.value).toBe('{{protocol}}://{{host}}');\n  });\n\n  it('should use server.name for environment name when present', () => {\n    const spec = {\n      openapi: '3.0.0',\n      info: { title: 'Named Server API', version: '1.0.0' },\n      paths: {\n        '/test': {\n          get: {\n            summary: 'Test',\n            responses: { 200: { description: 'OK' } }\n          }\n        }\n      },\n      servers: [\n        {\n          url: 'https://api.example.com',\n          name: 'Production',\n          description: 'Production server'\n        },\n        {\n          url: 'https://staging.example.com',\n          description: 'Staging server'\n        },\n        {\n          url: 'https://dev.example.com'\n        }\n      ]\n    };\n    const result = openApiToBruno(spec);\n    expect(result.environments[0].name).toBe('Production'); // prefers name over description\n    expect(result.environments[1].name).toBe('Staging server'); // falls back to description\n    expect(result.environments[2].name).toBe('Environment 3'); // falls back to index\n  });\n});\n\ndescribe('operation-level servers to request vars', () => {\n  it('should set request vars.req with baseUrl when operation has its own servers', () => {\n    const spec = {\n      openapi: '3.0.0',\n      info: { title: 'Op Server API', version: '1.0.0' },\n      servers: [{ url: 'https://api.example.com' }],\n      paths: {\n        '/files': {\n          get: {\n            summary: 'Get files',\n            operationId: 'getFiles',\n            servers: [{ url: 'https://files.example.com' }],\n            responses: { 200: { description: 'OK' } }\n          }\n        }\n      }\n    };\n    const result = openApiToBruno(spec);\n    const getFiles = result.items.find((i) => i.name === 'Get files');\n\n    expect(getFiles.request.vars).toBeDefined();\n    expect(getFiles.request.vars.req).toHaveLength(1);\n    expect(getFiles.request.vars.req[0]).toMatchObject({\n      name: 'baseUrl',\n      value: 'https://files.example.com',\n      enabled: true\n    });\n    expect(getFiles.request.vars.res).toEqual([]);\n  });\n\n  it('should create template baseUrl and variable entries for server with variables', () => {\n    const spec = {\n      openapi: '3.0.0',\n      info: { title: 'Var Server API', version: '1.0.0' },\n      servers: [{ url: 'https://api.example.com' }],\n      paths: {\n        '/regional': {\n          get: {\n            summary: 'Regional data',\n            servers: [{\n              url: '{protocol}://{region}.example.com/v2',\n              variables: {\n                protocol: { default: 'https' },\n                region: { default: 'us-east' }\n              }\n            }],\n            responses: { 200: { description: 'OK' } }\n          }\n        }\n      }\n    };\n    const result = openApiToBruno(spec);\n    const regional = result.items.find((i) => i.name === 'Regional data');\n\n    expect(regional.request.vars.req).toHaveLength(3); // baseUrl + protocol + region\n\n    const baseUrlVar = regional.request.vars.req.find((v) => v.name === 'baseUrl');\n    expect(baseUrlVar.value).toBe('{{protocol}}://{{region}}.example.com/v2');\n\n    const protocolVar = regional.request.vars.req.find((v) => v.name === 'protocol');\n    expect(protocolVar.value).toBe('https');\n\n    const regionVar = regional.request.vars.req.find((v) => v.name === 'region');\n    expect(regionVar.value).toBe('us-east');\n  });\n\n  it('should NOT set request vars when no operation servers exist', () => {\n    const spec = {\n      openapi: '3.0.0',\n      info: { title: 'No Override API', version: '1.0.0' },\n      servers: [{ url: 'https://api.example.com' }],\n      paths: {\n        '/test': {\n          get: {\n            summary: 'Test',\n            responses: { 200: { description: 'OK' } }\n          }\n        }\n      }\n    };\n    const result = openApiToBruno(spec);\n    const test = result.items.find((i) => i.name === 'Test');\n\n    expect(test.request.vars).toBeUndefined();\n  });\n\n  it('should only set vars on the operation that defines servers', () => {\n    const spec = {\n      openapi: '3.0.0',\n      info: { title: 'Selective API', version: '1.0.0' },\n      servers: [{ url: 'https://api.example.com' }],\n      paths: {\n        '/data': {\n          get: {\n            summary: 'Get data',\n            servers: [{ url: 'https://data-server.example.com' }],\n            responses: { 200: { description: 'OK' } }\n          },\n          post: {\n            summary: 'Post data',\n            responses: { 200: { description: 'OK' } }\n          }\n        }\n      }\n    };\n    const result = openApiToBruno(spec);\n    const getData = result.items.find((i) => i.name === 'Get data');\n    const postData = result.items.find((i) => i.name === 'Post data');\n\n    // GET has operation-level servers — should have vars\n    expect(getData.request.vars).toBeDefined();\n    expect(getData.request.vars.req[0].value).toBe('https://data-server.example.com');\n\n    // POST has no operation-level servers — should NOT have vars\n    expect(postData.request.vars).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/openapi/openapi-to-bruno/openapi-tags.spec.js",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport openApiToBruno from '../../../src/openapi/openapi-to-bruno';\n\n/**\n * Helper function to find a request by name in the collection.\n * Searches recursively through folders since requests with tags\n * are grouped into folders.\n */\nconst findRequestByName = (items, name) => {\n  for (const item of items) {\n    if (item.type === 'http-request' && item.name === name) {\n      return item;\n    }\n    if (item.type === 'folder' && item.items) {\n      const found = findRequestByName(item.items, name);\n      if (found) return found;\n    }\n  }\n  return undefined;\n};\n\n/**\n * Helper function to find a folder by name in the collection.\n */\nconst findFolderByName = (items, name) => {\n  for (const item of items) {\n    if (item.type === 'folder' && item.name === name) {\n      return item;\n    }\n    if (item.type === 'folder' && item.items) {\n      const found = findFolderByName(item.items, name);\n      if (found) return found;\n    }\n  }\n  return undefined;\n};\n\ndescribe('OpenAPI Import - Tag Sanitization', () => {\n  it('should replace spaces with underscores in tags', () => {\n    const openApiSpec = {\n      openapi: '3.0.0',\n      info: {\n        title: 'Test API',\n        version: '1.0.0'\n      },\n      paths: {\n        '/users': {\n          get: {\n            operationId: 'getUsers',\n            summary: 'Get users',\n            tags: ['User Management'],\n            responses: {\n              200: {\n                description: 'Success'\n              }\n            }\n          }\n        }\n      }\n    };\n\n    const result = openApiToBruno(JSON.stringify(openApiSpec));\n    const request = findRequestByName(result.items, 'Get users');\n    expect(request).toBeDefined();\n    // Spaces are replaced with underscores for BRU format compatibility\n    expect(request.tags).toEqual(['User_Management']);\n  });\n\n  it('should sanitize tags with dots', () => {\n    const openApiSpec = {\n      openapi: '3.0.0',\n      info: {\n        title: 'Test API',\n        version: '1.0.0'\n      },\n      paths: {\n        '/users': {\n          get: {\n            operationId: 'getUsers',\n            summary: 'Get users',\n            tags: ['api.v1', 'user.service'],\n            responses: {\n              200: {\n                description: 'Success'\n              }\n            }\n          }\n        }\n      }\n    };\n\n    const result = openApiToBruno(JSON.stringify(openApiSpec));\n    const request = findRequestByName(result.items, 'Get users');\n    expect(request).toBeDefined();\n    // Dots should be replaced with underscores\n    expect(request.tags).toEqual(['api_v1', 'user_service']);\n  });\n\n  it('should sanitize tags with special characters', () => {\n    const openApiSpec = {\n      openapi: '3.0.0',\n      info: {\n        title: 'Test API',\n        version: '1.0.0'\n      },\n      paths: {\n        '/users': {\n          get: {\n            operationId: 'getUsers',\n            summary: 'Get users',\n            tags: ['API (v1)', 'user-service:v2'],\n            responses: {\n              200: {\n                description: 'Success'\n              }\n            }\n          }\n        }\n      }\n    };\n\n    const result = openApiToBruno(JSON.stringify(openApiSpec));\n    const request = findRequestByName(result.items, 'Get users');\n    expect(request).toBeDefined();\n    // Parentheses, colons, and spaces should be replaced with underscores\n    // 'API (v1)' becomes 'API_v1' (space and parentheses become underscores, collapsed)\n    expect(request.tags).toEqual(['API_v1', 'user-service_v2']);\n  });\n\n  it('should preserve valid tags', () => {\n    const openApiSpec = {\n      openapi: '3.0.0',\n      info: {\n        title: 'Test API',\n        version: '1.0.0'\n      },\n      paths: {\n        '/users': {\n          get: {\n            operationId: 'getUsers',\n            summary: 'Get users',\n            tags: ['users', 'api-v1', 'user_service'],\n            responses: {\n              200: {\n                description: 'Success'\n              }\n            }\n          }\n        }\n      }\n    };\n\n    const result = openApiToBruno(JSON.stringify(openApiSpec));\n    const request = findRequestByName(result.items, 'Get users');\n    expect(request).toBeDefined();\n    expect(request.tags).toEqual(['users', 'api-v1', 'user_service']);\n  });\n\n  it('should handle empty tags array', () => {\n    const openApiSpec = {\n      openapi: '3.0.0',\n      info: {\n        title: 'Test API',\n        version: '1.0.0'\n      },\n      paths: {\n        '/users': {\n          get: {\n            operationId: 'getUsers',\n            summary: 'Get users',\n            tags: [],\n            responses: {\n              200: {\n                description: 'Success'\n              }\n            }\n          }\n        }\n      }\n    };\n\n    const result = openApiToBruno(JSON.stringify(openApiSpec));\n    const request = findRequestByName(result.items, 'Get users');\n    expect(request).toBeDefined();\n    expect(request.tags).toEqual([]);\n  });\n\n  it('should handle missing tags property', () => {\n    const openApiSpec = {\n      openapi: '3.0.0',\n      info: {\n        title: 'Test API',\n        version: '1.0.0'\n      },\n      paths: {\n        '/users': {\n          get: {\n            operationId: 'getUsers',\n            summary: 'Get users',\n            responses: {\n              200: {\n                description: 'Success'\n              }\n            }\n          }\n        }\n      }\n    };\n\n    const result = openApiToBruno(JSON.stringify(openApiSpec));\n    const request = findRequestByName(result.items, 'Get users');\n    expect(request).toBeDefined();\n    expect(request.tags).toEqual([]);\n  });\n\n  it('should remove duplicate tags after sanitization', () => {\n    const openApiSpec = {\n      openapi: '3.0.0',\n      info: {\n        title: 'Test API',\n        version: '1.0.0'\n      },\n      paths: {\n        '/users': {\n          get: {\n            operationId: 'getUsers',\n            summary: 'Get users',\n            tags: ['User Management', 'User Management', 'user-management'],\n            responses: {\n              200: {\n                description: 'Success'\n              }\n            }\n          }\n        }\n      }\n    };\n\n    const result = openApiToBruno(JSON.stringify(openApiSpec));\n    const request = findRequestByName(result.items, 'Get users');\n    expect(request).toBeDefined();\n    // 'User Management' becomes 'User_Management', which is different from 'user-management'\n    expect(request.tags).toEqual(['User_Management', 'user-management']);\n  });\n\n  it('should filter out tags that become empty after sanitization', () => {\n    const openApiSpec = {\n      openapi: '3.0.0',\n      info: {\n        title: 'Test API',\n        version: '1.0.0'\n      },\n      paths: {\n        '/users': {\n          get: {\n            operationId: 'getUsers',\n            summary: 'Get users',\n            tags: ['...', 'valid-tag', '---'],\n            responses: {\n              200: {\n                description: 'Success'\n              }\n            }\n          }\n        }\n      }\n    };\n\n    const result = openApiToBruno(JSON.stringify(openApiSpec));\n    const request = findRequestByName(result.items, 'Get users');\n    expect(request).toBeDefined();\n    expect(request.tags).toEqual(['valid-tag']);\n  });\n\n  it('should use sanitized tag names for folder grouping', () => {\n    const openApiSpec = {\n      openapi: '3.0.0',\n      info: {\n        title: 'Test API',\n        version: '1.0.0'\n      },\n      paths: {\n        '/users': {\n          get: {\n            operationId: 'getUsers',\n            summary: 'Get users',\n            tags: ['User Management'],\n            responses: {\n              200: {\n                description: 'Success'\n              }\n            }\n          }\n        },\n        '/posts': {\n          get: {\n            operationId: 'getPosts',\n            summary: 'Get posts',\n            tags: ['User Management'],\n            responses: {\n              200: {\n                description: 'Success'\n              }\n            }\n          }\n        }\n      }\n    };\n\n    const result = openApiToBruno(JSON.stringify(openApiSpec));\n    // Find the folder created from the tag - spaces replaced with underscores\n    const folder = findFolderByName(result.items, 'User_Management');\n    expect(folder).toBeDefined();\n    expect(folder.name).toBe('User_Management');\n    expect(folder.items).toHaveLength(2);\n  });\n\n  it('should sanitize folder names from tags with dots', () => {\n    const openApiSpec = {\n      openapi: '3.0.0',\n      info: {\n        title: 'Test API',\n        version: '1.0.0'\n      },\n      paths: {\n        '/users': {\n          get: {\n            operationId: 'getUsers',\n            summary: 'Get users',\n            tags: ['api.v1'],\n            responses: {\n              200: {\n                description: 'Success'\n              }\n            }\n          }\n        }\n      }\n    };\n\n    const result = openApiToBruno(JSON.stringify(openApiSpec));\n    // Find the folder created from the tag - dots should be replaced\n    const folder = findFolderByName(result.items, 'api_v1');\n    expect(folder).toBeDefined();\n    expect(folder.name).toBe('api_v1');\n  });\n\n  it('should handle utf characters as well', () => {\n    const openApiSpec = {\n      openapi: '3.0.1',\n      info: {\n        title: 'CBC-MODEL3D-API',\n        description: 'POWER BY WARE4U',\n        termsOfService: 'http://swagger.io/terms/',\n        contact: {\n          name: '陈洪',\n          email: 'sendreams@hotmail.com'\n        },\n        license: {\n          name: 'Apache 2.0',\n          url: 'http://springdoc.org'\n        },\n        version: '1.0.0'\n      },\n      tags: [\n        {\n          name: '模型管理',\n          description: '发布和管理3d模型'\n        },\n        {\n          name: '模型集市',\n          description: '模型查询、评价、下单等'\n        }\n      ],\n      paths: {\n        '/users': {\n          get: {\n            operationId: 'getUsers',\n            summary: 'Get users',\n            tags: ['模型管理'],\n            responses: {\n              200: {\n                description: 'Success'\n              }\n            }\n          }\n        }\n      }\n    };\n\n    const result = openApiToBruno(JSON.stringify(openApiSpec));\n    const folder = findFolderByName(result.items, '模型管理');\n    expect(folder).toBeDefined();\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/openapi/openapi-to-bruno/openapi-to-bruno.spec.js",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport openApiToBruno from '../../../src/openapi/openapi-to-bruno';\n\ndescribe('openapi-collection', () => {\n  it('should correctly import a valid OpenAPI file', async () => {\n    const brunoCollection = openApiToBruno(openApiCollectionString);\n\n    expect(brunoCollection).toMatchObject(expectedOutput);\n  });\n\n  it('should set auth mode to inherit when no security is defined in the collection', () => {\n    const brunoCollection = openApiToBruno(openApiCollectionString);\n\n    // The openApiCollectionString has no security defined, so auth mode should be 'inherit'\n    expect(brunoCollection.items[0].items[0].request.auth.mode).toBe('inherit');\n  });\n\n  it('trims whitespace from info.title and uses the trimmed value as the collection name', () => {\n    const openApiWithTitle = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: '  My API  '\npaths:\n  /get:\n    get:\n      summary: 'Request'\n      operationId: 'getRequest'\n      responses:\n        '200':\n          description: 'OK'\nservers:\n  - url: 'https://example.com'\n`;\n    const result = openApiToBruno(openApiWithTitle);\n    expect(result.name).toBe('My API');\n  });\n\n  it('defaults to Untitled Collection if info.title is only whitespace', () => {\n    const openApiWithTitle = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: '   '\npaths:\n  /get:\n    get:\n      summary: 'Request'\n      operationId: 'getRequest'\n      responses:\n        '200':\n          description: 'OK'\nservers:\n  - url: 'https://example.com'\n`;\n    const result = openApiToBruno(openApiWithTitle);\n    expect(result.name).toBe('Untitled Collection');\n  });\n\n  it('defaults to Untitled Collection if info.title is an empty string', () => {\n    const openApiWithEmptyTitle = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: ''\npaths:\n  /get:\n    get:\n      summary: 'Request'\n      operationId: 'getRequest'\n      responses:\n        '200':\n          description: 'OK'\nservers:\n  - url: 'https://example.com'\n`;\n    const result = openApiToBruno(openApiWithEmptyTitle);\n    expect(result.name).toBe('Untitled Collection');\n  });\n\n  it('defaults to Untitled Collection if info.title is missing', () => {\n    const openApiWithoutTitle = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\npaths:\n  /get:\n    get:\n      summary: 'Request'\n      operationId: 'getRequest'\n      responses:\n        '200':\n          description: 'OK'\nservers:\n  - url: 'https://example.com'\n`;\n    const result = openApiToBruno(openApiWithoutTitle);\n    expect(result.name).toBe('Untitled Collection');\n  });\n\n  it('defaults to Untitled Collection if info is missing entirely', () => {\n    const openApiWithMissingInfo = `\nopenapi: '3.0.0'\npaths:\n  /get:\n    get:\n      summary: 'Request'\n      operationId: 'getRequest'\n      responses:\n        '200':\n          description: 'OK'\nservers:\n  - url: 'https://example.com'\n`;\n    const result = openApiToBruno(openApiWithMissingInfo);\n    expect(result.name).toBe('Untitled Collection');\n  });\n\n  describe('authentication inheritance', () => {\n    it('should set auth mode to inherit when no security is defined', () => {\n      const openApiWithoutSecurity = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: 'API without security'\npaths:\n  /test:\n    get:\n      summary: 'Test endpoint'\n      operationId: 'testEndpoint'\n      responses:\n        '200':\n          description: 'OK'\nservers:\n  - url: 'https://example.com'\n`;\n      const result = openApiToBruno(openApiWithoutSecurity);\n      expect(result.items[0].request.auth.mode).toBe('inherit');\n    });\n\n    it('should set auth mode to inherit when no global security schemes exist', () => {\n      const openApiWithEmptySecurity = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: 'API with empty security'\nsecurity: []\npaths:\n  /test:\n    get:\n      summary: 'Test endpoint'\n      operationId: 'testEndpoint'\n      responses:\n        '200':\n          description: 'OK'\nservers:\n  - url: 'https://example.com'\n`;\n      const result = openApiToBruno(openApiWithEmptySecurity);\n      expect(result.items[0].request.auth.mode).toBe('inherit');\n    });\n\n    it('should set auth mode to inherit when components.securitySchemes is empty', () => {\n      const openApiWithEmptyComponents = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: 'API with empty components'\ncomponents:\n  securitySchemes: {}\npaths:\n  /test:\n    get:\n      summary: 'Test endpoint'\n      operationId: 'testEndpoint'\n      responses:\n        '200':\n          description: 'OK'\nservers:\n  - url: 'https://example.com'\n`;\n      const result = openApiToBruno(openApiWithEmptyComponents);\n      expect(result.items[0].request.auth.mode).toBe('inherit');\n    });\n\n    it('should set auth mode to inherit when operation has empty security array', () => {\n      const openApiWithEmptyOperationSecurity = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: 'API with empty operation security'\ncomponents:\n  securitySchemes:\n    basicAuth:\n      type: http\n      scheme: basic\npaths:\n  /test:\n    get:\n      summary: 'Test endpoint'\n      operationId: 'testEndpoint'\n      security: []\n      responses:\n        '200':\n          description: 'OK'\nservers:\n  - url: 'https://example.com'\n`;\n      const result = openApiToBruno(openApiWithEmptyOperationSecurity);\n      expect(result.items[0].request.auth.mode).toBe('inherit');\n    });\n\n    it('should set auth mode to inherit for folder root when no security is defined', () => {\n      const openApiWithTags = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: 'API with tags'\npaths:\n  /test:\n    get:\n      tags:\n        - TestGroup\n      summary: 'Test endpoint'\n      operationId: 'testEndpoint'\n      responses:\n        '200':\n          description: 'OK'\nservers:\n  - url: 'https://example.com'\n`;\n      const result = openApiToBruno(openApiWithTags);\n      expect(result.items[0].type).toBe('folder');\n      expect(result.items[0].root.request.auth.mode).toBe('inherit');\n    });\n  });\n\n  it('should handle requestBody with empty content object (undefined mimeType)', () => {\n    const openApiWithEmptyContent = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: 'API with empty requestBody content'\npaths:\n  /test:\n    post:\n      summary: 'Test endpoint with empty content'\n      operationId: 'testEndpoint'\n      requestBody:\n        content: {}\n      responses:\n        '200':\n          description: 'OK'\nservers:\n  - url: 'https://example.com'\n`;\n    const result = openApiToBruno(openApiWithEmptyContent);\n    expect(result.items[0].request.body.mode).toBe('none');\n    expect(result.items[0].request.body.json).toBe(null);\n    expect(result.items[0].request.body.text).toBe(null);\n    expect(result.items[0].request.body.xml).toBe(null);\n  });\n});\n\nconst openApiCollectionString = `\nopenapi: \"3.0.0\"\ninfo:\n  version: \"1.0.0\"\n  title: \"Hello World OpenAPI\"\npaths:\n  /get:\n    get:\n      tags:\n        - Folder1\n        - Folder2\n      summary: \"Request1 and Request2\"\n      operationId: \"getRequests\"\n      responses:\n        '200':\n          description: \"Successful response\"\ncomponents:\n  parameters:\n    var1:\n      in: \"query\"\n      name: \"var1\"\n      required: true\n      schema:\n        type: \"string\"\n        default: \"value1\"\n    var2:\n      in: \"query\"\n      name: \"var2\"\n      required: true\n      schema:\n        type: \"string\"\n        default: \"value2\"\nservers:\n  - url: \"https://echo.usebruno.com\"\n`;\n\nconst expectedOutput = {\n  environments: [\n    {\n      name: 'Environment 1',\n      uid: 'mockeduuidvalue123456',\n      variables: [\n        {\n          enabled: true,\n          name: 'baseUrl',\n          secret: false,\n          type: 'text',\n          uid: 'mockeduuidvalue123456',\n          value: 'https://echo.usebruno.com'\n        }\n      ]\n    }\n  ],\n  items: [\n    {\n      items: [\n        {\n          name: 'Request1 and Request2',\n          request: {\n            auth: {\n              basic: null,\n              bearer: null,\n              digest: null,\n              mode: 'inherit'\n            },\n            body: {\n              formUrlEncoded: [],\n              json: null,\n              mode: 'none',\n              multipartForm: [],\n              text: null,\n              xml: null\n            },\n            headers: [],\n            method: 'GET',\n            params: [],\n            script: {\n              res: null\n            },\n            url: '{{baseUrl}}/get'\n          },\n          seq: 1,\n          type: 'http-request',\n          uid: 'mockeduuidvalue123456'\n        }\n      ],\n      name: 'Folder1',\n      type: 'folder',\n      uid: 'mockeduuidvalue123456'\n    }\n  ],\n  name: 'Hello World OpenAPI',\n  uid: 'mockeduuidvalue123456',\n  version: '1'\n};\n\ndescribe('openapi-collection: object schema parameters', () => {\n  it('should expand object schema query parameters with $ref into individual properties', () => {\n    const openApiSpec = `\nopenapi: '3.0.3'\ninfo:\n  title: 'Test API for Object Schema Parameters'\n  version: '1.0.0'\nservers:\n  - url: 'https://api.example.com/v1'\npaths:\n  /items:\n    get:\n      summary: 'Get items with pagination'\n      operationId: 'getItems'\n      parameters:\n        - name: date\n          in: query\n          required: true\n          schema:\n            type: string\n            format: date\n          description: 'Filter by date'\n        - name: paginationParams\n          in: query\n          required: true\n          schema:\n            $ref: '#/components/schemas/PaginationParams'\n      responses:\n        '200':\n          description: 'Successful response'\ncomponents:\n  schemas:\n    PaginationParams:\n      type: object\n      properties:\n        page:\n          type: integer\n          format: int32\n          minimum: 0\n          description: 'Page number'\n        size:\n          type: integer\n          format: int32\n          maximum: 100\n          minimum: 1\n          description: 'Page size'\n      required:\n        - page\n        - size\n`;\n\n    const result = openApiToBruno(openApiSpec);\n\n    // Find the request item\n    const requestItem = result.items[0];\n\n    // Verify that we have 3 query parameters: date, page, size\n    const queryParams = requestItem.request.params.filter((p) => p.type === 'query');\n    expect(queryParams.length).toBe(3);\n\n    // Check that 'date' parameter exists\n    const dateParam = queryParams.find((p) => p.name === 'date');\n    expect(dateParam).toBeDefined();\n    expect(dateParam.description).toBe('Filter by date');\n    expect(dateParam.enabled).toBe(true);\n\n    // Check that 'page' parameter exists (expanded from PaginationParams)\n    const pageParam = queryParams.find((p) => p.name === 'page');\n    expect(pageParam).toBeDefined();\n    expect(pageParam.description).toBe('Page number');\n    expect(pageParam.enabled).toBe(true); // required in schema\n\n    // Check that 'size' parameter exists (expanded from PaginationParams)\n    const sizeParam = queryParams.find((p) => p.name === 'size');\n    expect(sizeParam).toBeDefined();\n    expect(sizeParam.description).toBe('Page size');\n    expect(sizeParam.enabled).toBe(true); // required in schema\n\n    // Verify that 'paginationParams' does NOT exist as a parameter\n    const paginationParam = queryParams.find((p) => p.name === 'paginationParams');\n    expect(paginationParam).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/openapi/openapi-to-bruno/path-based-grouping-duplicate-names.spec.js",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport openApiToBruno from '../../../src/openapi/openapi-to-bruno';\n\ndescribe('OpenAPI Path-Based Grouping - Duplicate Names', () => {\n  it('should not add suffixes to duplicate operation names in different folders', () => {\n    const openApiSpec = {\n      openapi: '3.0.0',\n      info: {\n        title: 'Duplicate Names Test API',\n        version: '1.0.0'\n      },\n      servers: [\n        {\n          url: 'https://api.example.com/v1'\n        }\n      ],\n      paths: {\n        // Users folder - should have \"Get User Details\" operation\n        '/users/{id}': {\n          get: {\n            summary: 'Get User Details',\n            operationId: 'getUserDetails',\n            responses: {\n              200: {\n                description: 'User details'\n              }\n            }\n          }\n        },\n        // Products folder - should also have \"Get User Details\" operation (different context)\n        '/products/{id}/owner': {\n          get: {\n            summary: 'Get User Details',\n            operationId: 'getProductOwnerDetails',\n            responses: {\n              200: {\n                description: 'Product owner details'\n              }\n            }\n          }\n        },\n        // Orders folder - should also have \"Get User Details\" operation (different context)\n        '/orders/{orderId}/customer': {\n          get: {\n            summary: 'Get User Details',\n            operationId: 'getOrderCustomerDetails',\n            responses: {\n              200: {\n                description: 'Order customer details'\n              }\n            }\n          }\n        }\n      }\n    };\n\n    const result = openApiToBruno(openApiSpec, { groupBy: 'path' });\n\n    // Find the folders\n    const usersFolder = result.items.find((item) => item.name === 'users');\n    const productsFolder = result.items.find((item) => item.name === 'products');\n    const ordersFolder = result.items.find((item) => item.name === 'orders');\n\n    // Find requests in each folder\n    // Users folder: /users/{id} -> should have request directly in users folder\n    const usersIdFolder = usersFolder.items.find((item) => item.name === '{id}');\n    expect(usersIdFolder).toBeDefined();\n    const getUserDetailsRequest = usersIdFolder.items.find((item) => item.type === 'http-request');\n\n    // Products folder: /products/{id}/owner -> should have request in products/{id}/owner\n    const productsIdFolder = productsFolder.items.find((item) => item.name === '{id}');\n    expect(productsIdFolder).toBeDefined();\n    const productsOwnerFolder = productsIdFolder.items.find((item) => item.name === 'owner');\n    expect(productsOwnerFolder).toBeDefined();\n    const getProductOwnerRequest = productsOwnerFolder.items.find((item) => item.type === 'http-request');\n\n    // Orders folder: /orders/{orderId}/customer -> should have request in orders/{orderId}/customer\n    const ordersIdFolder = ordersFolder.items.find((item) => item.name === '{orderId}');\n    expect(ordersIdFolder).toBeDefined();\n    const ordersCustomerFolder = ordersIdFolder.items.find((item) => item.name === 'customer');\n    expect(ordersCustomerFolder).toBeDefined();\n    const getOrderCustomerRequest = ordersCustomerFolder.items.find((item) => item.type === 'http-request');\n\n    expect(getUserDetailsRequest).toBeDefined();\n    expect(getProductOwnerRequest).toBeDefined();\n    expect(getOrderCustomerRequest).toBeDefined();\n\n    // CRITICAL ASSERTIONS: Names should NOT have suffixes\n    // Each folder should have its own namespace, so duplicate names across folders should be allowed\n\n    // All requests should have clean names (NO suffixes like \"(GET)\" or \"(1)\")\n    expect(getUserDetailsRequest.name).toBe('Get User Details');\n    expect(getProductOwnerRequest.name).toBe('Get User Details');\n    expect(getOrderCustomerRequest.name).toBe('Get User Details');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/openapi/openapi-with-examples.spec.js",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport openApiToBruno from '../../src/openapi/openapi-to-bruno';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\ndescribe('OpenAPI with Examples', () => {\n  const openApiWithExamples = fs.readFileSync(path.resolve(__dirname, '../../../../tests/import/openapi/fixtures/openapi-with-examples.yaml'),\n    'utf8');\n\n  it('should import OpenAPI collection with response examples', () => {\n    const brunoCollection = openApiToBruno(openApiWithExamples);\n\n    expect(brunoCollection).toBeDefined();\n    expect(brunoCollection.name).toBe('API with Examples');\n    expect(brunoCollection.items).toHaveLength(3); // Three separate requests\n\n    // Test GET /users endpoint\n    const getUsersRequest = brunoCollection.items.find((item) => item.name === 'Get all users');\n    expect(getUsersRequest).toBeDefined();\n    expect(getUsersRequest.examples).toBeDefined();\n    expect(getUsersRequest.examples).toHaveLength(4);\n\n    // Check specific examples\n    const successExample = getUsersRequest.examples.find((ex) => ex.name === 'Success Response');\n    expect(successExample).toBeDefined();\n    expect(successExample.response.status).toEqual(200);\n    expect(successExample.response.statusText).toBe('OK');\n    expect(successExample.response.headers).toHaveLength(1);\n    expect(successExample.response.headers[0].name).toBe('Content-Type');\n    expect(successExample.response.headers[0].value).toBe('application/json');\n    expect(JSON.parse(successExample.response.body.content)).toEqual({\n      users: [\n        { id: 1, name: 'John Doe', email: 'john@example.com' },\n        { id: 2, name: 'Jane Smith', email: 'jane@example.com' }\n      ]\n    });\n\n    const emptyExample = getUsersRequest.examples.find((ex) => ex.name === 'Empty Response');\n    expect(emptyExample.response.status).toEqual(200);\n    expect(JSON.parse(emptyExample.response.body.content)).toEqual({ users: [] });\n\n    const validationErrorExample = getUsersRequest.examples.find((ex) => ex.name === 'Validation Error');\n    expect(validationErrorExample).toBeDefined();\n    expect(validationErrorExample.response.status).toEqual(400);\n    expect(validationErrorExample.response.statusText).toBe('Bad Request');\n\n    const serverErrorExample = getUsersRequest.examples.find((ex) => ex.name === 'Server Error');\n    expect(serverErrorExample).toBeDefined();\n    expect(serverErrorExample.response.status).toEqual(500);\n    expect(serverErrorExample.response.statusText).toBe('Internal Server Error');\n\n    // Test POST /users endpoint\n    const createUserRequest = brunoCollection.items.find((item) => item.name === 'Create a new user');\n    expect(createUserRequest).toBeDefined();\n    expect(createUserRequest.examples).toBeDefined();\n    expect(createUserRequest.examples).toHaveLength(4);\n\n    // Check response examples\n    const createdExample = createUserRequest.examples.find((ex) => ex.name === 'User Created (Valid User)');\n    expect(createdExample).toBeDefined();\n    expect(createdExample.response.status).toEqual(201);\n    expect(createdExample.response.statusText).toBe('Created');\n    expect(JSON.parse(createdExample.response.body.content)).toEqual({\n      id: 123,\n      name: 'John Doe',\n      email: 'john@example.com',\n      created_at: '2023-01-01T00:00:00Z'\n    });\n  });\n\n  it('should handle OpenAPI examples with different content types', () => {\n    const openApiWithDifferentContentTypes = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: 'API with Different Content Types'\npaths:\n  /data:\n    get:\n      summary: 'Get data'\n      operationId: 'getData'\n      responses:\n        '200':\n          description: 'Successful response'\n          content:\n            application/json:\n              examples:\n                json_response:\n                  summary: 'JSON Response'\n                  value:\n                    message: 'Hello World'\n            text/plain:\n              examples:\n                text_response:\n                  summary: 'Text Response'\n                  value: 'Hello World'\nservers:\n  - url: 'https://api.example.com'\n`;\n\n    const brunoCollection = openApiToBruno(openApiWithDifferentContentTypes);\n    const request = brunoCollection.items[0];\n\n    expect(request.examples).toHaveLength(2);\n\n    const jsonExample = request.examples.find((ex) => ex.name === 'JSON Response');\n    expect(jsonExample).toBeDefined();\n    expect(jsonExample.response.headers[0].value).toBe('application/json');\n\n    const textExample = request.examples.find((ex) => ex.name === 'Text Response');\n    expect(textExample).toBeDefined();\n    expect(textExample.response.headers[0].value).toBe('text/plain');\n    expect(textExample.response.body.content).toBe('Hello World');\n    expect(textExample.response.body.type).toBe('text');\n  });\n\n  it('should handle OpenAPI examples without summary or description', () => {\n    const openApiWithMinimalExamples = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: 'API with Minimal Examples'\npaths:\n  /test:\n    get:\n      summary: 'Test endpoint'\n      operationId: 'test'\n      responses:\n        '200':\n          description: 'OK'\n          content:\n            application/json:\n              examples:\n                example1:\n                  value:\n                    message: 'test'\nservers:\n  - url: 'https://api.example.com'\n`;\n\n    const brunoCollection = openApiToBruno(openApiWithMinimalExamples);\n    const request = brunoCollection.items[0];\n\n    expect(request.examples).toHaveLength(1);\n    const example = request.examples[0];\n    expect(example.name).toBe('example1');\n    expect(example.description).toBe('');\n    expect(example.response.body.type).toBe('json');\n    expect(JSON.parse(example.response.body.content)).toEqual({ message: 'test' });\n  });\n\n  it('should create examples without specified request body, when response is present', () => {\n    const openApiWithoutExamples = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: 'API without Examples'\npaths:\n  /test:\n    get:\n      summary: 'Test endpoint'\n      operationId: 'test'\n      responses:\n        '200':\n          description: 'OK'\n          content:\n            application/json:\n              schema:\n                type: object\nservers:\n  - url: 'https://api.example.com'\n`;\n\n    const brunoCollection = openApiToBruno(openApiWithoutExamples);\n    const request = brunoCollection.items[0];\n\n    expect(request.examples).toHaveLength(1);\n    const example = request.examples[0];\n    expect(example.name).toBe('200 Response');\n    expect(example.description).toBe('OK');\n    expect(example.response.body.type).toBe('json');\n  });\n\n  it('should support path-based grouping when specified', () => {\n    const openApiWithPathGrouping = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: 'API with Path Grouping'\npaths:\n  /users:\n    get:\n      summary: 'Get all users'\n      operationId: 'getUsers'\n      responses:\n        '200':\n          description: 'OK'\n          content:\n            application/json:\n              examples:\n                success:\n                  summary: 'Success Response'\n                  value:\n                    users: []\n    post:\n      summary: 'Create user'\n      operationId: 'createUser'\n      responses:\n        '201':\n          description: 'Created'\n          content:\n            application/json:\n              examples:\n                created:\n                  summary: 'User Created'\n                  value:\n                    id: 123\n  /products:\n    get:\n      summary: 'Get all products'\n      operationId: 'getProducts'\n      responses:\n        '200':\n          description: 'OK'\n          content:\n            application/json:\n              examples:\n                success:\n                  summary: 'Products Response'\n                  value:\n                    products: []\nservers:\n  - url: 'https://api.example.com'\n`;\n\n    // Test with path-based grouping\n    const brunoCollection = openApiToBruno(openApiWithPathGrouping, { groupBy: 'path' });\n\n    expect(brunoCollection).toBeDefined();\n    expect(brunoCollection.name).toBe('API with Path Grouping');\n\n    // Should have 2 folders: users and products (without leading slash)\n    expect(brunoCollection.items).toHaveLength(2);\n\n    const usersFolder = brunoCollection.items.find((item) => item.name === 'users');\n    expect(usersFolder).toBeDefined();\n    expect(usersFolder.type).toBe('folder');\n    expect(usersFolder.items).toHaveLength(2); // GET and POST /users\n\n    const productsFolder = brunoCollection.items.find((item) => item.name === 'products');\n    expect(productsFolder).toBeDefined();\n    expect(productsFolder.type).toBe('folder');\n    expect(productsFolder.items).toHaveLength(1); // GET /products\n\n    // Verify examples are preserved in path-based grouping\n    const getUsersRequest = usersFolder.items.find((item) => item.name === 'Get all users');\n    expect(getUsersRequest.examples).toBeDefined();\n    expect(getUsersRequest.examples).toHaveLength(1);\n    expect(getUsersRequest.examples[0].name).toBe('Success Response');\n  });\n\n  it('should default to tag-based grouping when no groupBy option is specified', () => {\n    const openApiWithTags = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: 'API with Tags'\npaths:\n  /users:\n    get:\n      summary: 'Get all users'\n      operationId: 'getUsers'\n      tags: ['Users']\n      responses:\n        '200':\n          description: 'OK'\n  /products:\n    get:\n      summary: 'Get all products'\n      operationId: 'getProducts'\n      tags: ['Products']\n      responses:\n        '200':\n          description: 'OK'\nservers:\n  - url: 'https://api.example.com'\n`;\n\n    // Test with default grouping (tags)\n    const brunoCollection = openApiToBruno(openApiWithTags);\n\n    expect(brunoCollection).toBeDefined();\n    expect(brunoCollection.name).toBe('API with Tags');\n\n    // Should have 2 folders based on tags: Users and Products\n    expect(brunoCollection.items).toHaveLength(2);\n\n    const usersFolder = brunoCollection.items.find((item) => item.name === 'Users');\n    expect(usersFolder).toBeDefined();\n    expect(usersFolder.type).toBe('folder');\n    expect(usersFolder.items).toHaveLength(1); // GET /users\n\n    const productsFolder = brunoCollection.items.find((item) => item.name === 'Products');\n    expect(productsFolder).toBeDefined();\n    expect(productsFolder.type).toBe('folder');\n    expect(productsFolder.items).toHaveLength(1); // GET /products\n  });\n\n  describe('Request Body Examples', () => {\n    it('should match request body examples by key when response example key matches', () => {\n      const openApiWithMatchingKeys = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: 'API with Matching Keys'\npaths:\n  /users:\n    post:\n      summary: 'Create user'\n      operationId: 'createUser'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            examples:\n              valid_user:\n                summary: 'Valid User'\n                value:\n                  name: 'John Doe'\n                  email: 'john@example.com'\n              invalid_user:\n                summary: 'Invalid User'\n                value:\n                  name: ''\n                  email: 'invalid'\n      responses:\n        '201':\n          description: 'Created'\n          content:\n            application/json:\n              examples:\n                valid_user:\n                  summary: 'User Created'\n                  value:\n                    id: 123\n                    name: 'John Doe'\n                invalid_user:\n                  summary: 'Validation Error'\n                  value:\n                    error: 'Invalid input'\nservers:\n  - url: 'https://api.example.com'\n`;\n\n      const brunoCollection = openApiToBruno(openApiWithMatchingKeys);\n      const request = brunoCollection.items[0];\n\n      expect(request.examples).toBeDefined();\n      expect(request.examples).toHaveLength(2);\n\n      // Check that matching keys are used\n      const validUserExample = request.examples.find((ex) => ex.name === 'User Created');\n      expect(validUserExample).toBeDefined();\n      expect(validUserExample.request.body.mode).toBe('json');\n      expect(JSON.parse(validUserExample.request.body.json)).toEqual({\n        name: 'John Doe',\n        email: 'john@example.com'\n      });\n      expect(JSON.parse(validUserExample.response.body.content)).toEqual({\n        id: 123,\n        name: 'John Doe'\n      });\n\n      const invalidUserExample = request.examples.find((ex) => ex.name === 'Validation Error');\n      expect(invalidUserExample).toBeDefined();\n      expect(JSON.parse(invalidUserExample.request.body.json)).toEqual({\n        name: '',\n        email: 'invalid'\n      });\n      expect(JSON.parse(invalidUserExample.response.body.content)).toEqual({\n        error: 'Invalid input'\n      });\n    });\n\n    it('should create all combinations when response example keys do not match request body examples', () => {\n      const openApiWithNonMatchingKeys = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: 'API with Non-Matching Keys'\npaths:\n  /users:\n    post:\n      summary: 'Create user'\n      operationId: 'createUser'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            examples:\n              valid_user:\n                summary: 'Valid User'\n                value:\n                  name: 'John Doe'\n                  email: 'john@example.com'\n              invalid_user:\n                summary: 'Invalid User'\n                value:\n                  name: ''\n                  email: 'invalid'\n      responses:\n        '201':\n          description: 'Created'\n          content:\n            application/json:\n              examples:\n                created:\n                  summary: 'User Created'\n                  value:\n                    id: 123\n        400:\n          description: 'Bad Request'\n          content:\n            application/json:\n              examples:\n                error:\n                  summary: 'Validation Error'\n                  value:\n                    error: 'Invalid input'\nservers:\n  - url: 'https://api.example.com'\n`;\n\n      const brunoCollection = openApiToBruno(openApiWithNonMatchingKeys);\n      const request = brunoCollection.items[0];\n\n      expect(request.examples).toBeDefined();\n      // Should have 4 examples: 2 response examples × 2 request body examples\n      expect(request.examples).toHaveLength(4);\n\n      // Check combinations for 201 response\n      const createdWithValid = request.examples.find((ex) => ex.name === 'User Created (Valid User)');\n      expect(createdWithValid).toBeDefined();\n      expect(createdWithValid.response.status).toEqual(201);\n      expect(JSON.parse(createdWithValid.request.body.json)).toEqual({\n        name: 'John Doe',\n        email: 'john@example.com'\n      });\n\n      const createdWithInvalid = request.examples.find((ex) => ex.name === 'User Created (Invalid User)');\n      expect(createdWithInvalid).toBeDefined();\n      expect(createdWithInvalid.response.status).toEqual(201);\n      expect(JSON.parse(createdWithInvalid.request.body.json)).toEqual({\n        name: '',\n        email: 'invalid'\n      });\n\n      // Check combinations for 400 response\n      const errorWithValid = request.examples.find((ex) => ex.name === 'Validation Error (Valid User)');\n      expect(errorWithValid).toBeDefined();\n      expect(errorWithValid.response.status).toEqual(400);\n\n      const errorWithInvalid = request.examples.find((ex) => ex.name === 'Validation Error (Invalid User)');\n      expect(errorWithInvalid).toBeDefined();\n      expect(errorWithInvalid.response.status).toEqual(400);\n    });\n\n    it('should use single request body example for all response examples', () => {\n      const openApiWithSingleRequestBody = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: 'API with Single Request Body'\npaths:\n  /users:\n    post:\n      summary: 'Create user'\n      operationId: 'createUser'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            example:\n              name: 'John Doe'\n              email: 'john@example.com'\n      responses:\n        '201':\n          description: 'Created'\n          content:\n            application/json:\n              examples:\n                created:\n                  summary: 'User Created'\n                  value:\n                    id: 123\n                duplicate:\n                  summary: 'Duplicate User'\n                  value:\n                    error: 'User already exists'\nservers:\n  - url: 'https://api.example.com'\n`;\n\n      const brunoCollection = openApiToBruno(openApiWithSingleRequestBody);\n      const request = brunoCollection.items[0];\n\n      expect(request.examples).toBeDefined();\n      expect(request.examples).toHaveLength(2);\n\n      // Both examples should have the same request body\n      const createdExample = request.examples.find((ex) => ex.name === 'User Created');\n      expect(createdExample).toBeDefined();\n      expect(createdExample.request.body.mode).toBe('json');\n      expect(JSON.parse(createdExample.request.body.json)).toEqual({\n        name: 'John Doe',\n        email: 'john@example.com'\n      });\n\n      const duplicateExample = request.examples.find((ex) => ex.name === 'Duplicate User');\n      expect(duplicateExample).toBeDefined();\n      expect(JSON.parse(duplicateExample.request.body.json)).toEqual({\n        name: 'John Doe',\n        email: 'john@example.com'\n      });\n    });\n\n    it('should use schema-based request body for all response examples', () => {\n      const openApiWithSchemaRequestBody = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: 'API with Schema Request Body'\npaths:\n  /users:\n    post:\n      summary: 'Create user'\n      operationId: 'createUser'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              type: object\n              required:\n                - name\n                - email\n              properties:\n                name:\n                  type: string\n                  example: 'John Doe'\n                email:\n                  type: string\n                  format: email\n                  example: 'john@example.com'\n      responses:\n        '201':\n          description: 'Created'\n          content:\n            application/json:\n              examples:\n                created:\n                  summary: 'User Created'\n                  value:\n                    id: 123\n                error:\n                  summary: 'Error Response'\n                  value:\n                    error: 'Something went wrong'\nservers:\n  - url: 'https://api.example.com'\n`;\n\n      const brunoCollection = openApiToBruno(openApiWithSchemaRequestBody);\n      const request = brunoCollection.items[0];\n\n      expect(request.examples).toBeDefined();\n      expect(request.examples).toHaveLength(2);\n\n      // Both examples should have request body generated from schema\n      const createdExample = request.examples.find((ex) => ex.name === 'User Created');\n      expect(createdExample).toBeDefined();\n      expect(createdExample.request.body.mode).toBe('json');\n      const requestBody = JSON.parse(createdExample.request.body.json);\n      expect(requestBody).toHaveProperty('name');\n      expect(requestBody).toHaveProperty('email');\n\n      const errorExample = request.examples.find((ex) => ex.name === 'Error Response');\n      expect(errorExample).toBeDefined();\n      expect(JSON.parse(errorExample.request.body.json)).toEqual(requestBody);\n    });\n\n    it('should handle request body examples with different content types', () => {\n      const openApiWithDifferentRequestBodyTypes = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: 'API with Different Request Body Types'\npaths:\n  /data:\n    post:\n      summary: 'Post data'\n      operationId: 'postData'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            examples:\n              json_data:\n                summary: 'JSON Data'\n                value:\n                  message: 'Hello'\n          text/plain:\n            examples:\n              text_data:\n                summary: 'Text Data'\n                value: 'Hello World'\n      responses:\n        '200':\n          description: 'OK'\n          content:\n            application/json:\n              examples:\n                success:\n                  summary: 'Success'\n                  value:\n                    status: 'ok'\nservers:\n  - url: 'https://api.example.com'\n`;\n\n      const brunoCollection = openApiToBruno(openApiWithDifferentRequestBodyTypes);\n      const request = brunoCollection.items[0];\n\n      expect(request.examples).toBeDefined();\n      // Should create combinations: 1 response × 2 request body examples = 2 examples\n      expect(request.examples).toHaveLength(2);\n\n      const jsonExample = request.examples.find((ex) => ex.name === 'Success (JSON Data)');\n      expect(jsonExample).toBeDefined();\n      expect(jsonExample.request.body.mode).toBe('json');\n      expect(JSON.parse(jsonExample.request.body.json)).toEqual({ message: 'Hello' });\n\n      const textExample = request.examples.find((ex) => ex.name === 'Success (Text Data)');\n      expect(textExample).toBeDefined();\n      expect(textExample.request.body.mode).toBe('text');\n      expect(textExample.request.body.text).toBe('Hello World');\n    });\n\n    it('should handle mixed matching and non-matching request body examples', () => {\n      const openApiWithMixedMatching = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: 'API with Mixed Matching'\npaths:\n  /users:\n    post:\n      summary: 'Create user'\n      operationId: 'createUser'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            examples:\n              valid_user:\n                summary: 'Valid User'\n                value:\n                  name: 'John Doe'\n                  email: 'john@example.com'\n              invalid_user:\n                summary: 'Invalid User'\n                value:\n                  name: ''\n                  email: 'invalid'\n      responses:\n        '201':\n          description: 'Created'\n          content:\n            application/json:\n              examples:\n                valid_user:\n                  summary: 'User Created'\n                  value:\n                    id: 123\n                unmatched:\n                  summary: 'Unmatched Response'\n                  value:\n                    id: 456\nservers:\n  - url: 'https://api.example.com'\n`;\n\n      const brunoCollection = openApiToBruno(openApiWithMixedMatching);\n      const request = brunoCollection.items[0];\n\n      expect(request.examples).toBeDefined();\n      // Should have: 1 matched (valid_user) + 2 combinations for unmatched (unmatched × 2 request body examples) = 3\n      expect(request.examples).toHaveLength(3);\n\n      // Matched example\n      const matchedExample = request.examples.find((ex) => ex.name === 'User Created');\n      expect(matchedExample).toBeDefined();\n      expect(JSON.parse(matchedExample.request.body.json)).toEqual({\n        name: 'John Doe',\n        email: 'john@example.com'\n      });\n\n      // Unmatched combinations\n      const unmatchedWithValid = request.examples.find((ex) => ex.name === 'Unmatched Response (Valid User)');\n      expect(unmatchedWithValid).toBeDefined();\n\n      const unmatchedWithInvalid = request.examples.find((ex) => ex.name === 'Unmatched Response (Invalid User)');\n      expect(unmatchedWithInvalid).toBeDefined();\n    });\n\n    it('should not create request body when no request body is defined', () => {\n      const openApiWithoutRequestBody = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: 'API without Request Body'\npaths:\n  /users:\n    get:\n      summary: 'Get users'\n      operationId: 'getUsers'\n      responses:\n        '200':\n          description: 'OK'\n          content:\n            application/json:\n              examples:\n                success:\n                  summary: 'Success'\n                  value:\n                    users: []\nservers:\n  - url: 'https://api.example.com'\n`;\n\n      const brunoCollection = openApiToBruno(openApiWithoutRequestBody);\n      const request = brunoCollection.items[0];\n\n      expect(request.examples).toBeDefined();\n      expect(request.examples).toHaveLength(1);\n\n      const example = request.examples[0];\n      expect(example.request.body.mode).toBe('none');\n      expect(example.request.body.json).toBeNull();\n    });\n\n    it('should handle request body with singular example and multiple response examples', () => {\n      const openApiWithSingularExample = `\nopenapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: 'API with Singular Example'\npaths:\n  /users:\n    post:\n      summary: 'Create user'\n      operationId: 'createUser'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            example:\n              name: 'Jane Doe'\n              email: 'jane@example.com'\n      responses:\n        '201':\n          description: 'Created'\n          content:\n            application/json:\n              examples:\n                created:\n                  summary: 'User Created'\n                  value:\n                    id: 1\n                duplicate:\n                  summary: 'Duplicate'\n                  value:\n                    id: 2\n        400:\n          description: 'Bad Request'\n          content:\n            application/json:\n              examples:\n                error:\n                  summary: 'Error'\n                  value:\n                    error: 'Bad request'\nservers:\n  - url: 'https://api.example.com'\n`;\n\n      const brunoCollection = openApiToBruno(openApiWithSingularExample);\n      const request = brunoCollection.items[0];\n\n      expect(request.examples).toBeDefined();\n      expect(request.examples).toHaveLength(3);\n\n      // All examples should have the same request body\n      const requestBodyValue = { name: 'Jane Doe', email: 'jane@example.com' };\n      request.examples.forEach((example) => {\n        expect(example.request.body.mode).toBe('json');\n        expect(JSON.parse(example.request.body.json)).toEqual(requestBodyValue);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/bruno-to-postman-with-examples.spec.js",
    "content": "import { brunoToPostman } from '../../src/postman/bruno-to-postman';\n\ndescribe('Bruno to Postman Converter with Examples', () => {\n  it('should export Bruno collection with examples to Postman format', () => {\n    const brunoCollection = {\n      name: 'Test Collection with Examples',\n      items: [\n        {\n          name: 'Get Users',\n          type: 'http-request',\n          request: {\n            method: 'GET',\n            url: 'https://api.example.com/users',\n            headers: [\n              {\n                name: 'Accept',\n                value: 'application/json',\n                enabled: true\n              }\n            ],\n            params: [],\n            body: {\n              mode: 'none'\n            }\n          },\n          examples: [\n            {\n              name: 'Success Response',\n              description: 'Successful response with user data',\n              type: 'http-request',\n              request: {\n                method: 'GET',\n                url: 'https://api.example.com/users',\n                headers: [\n                  {\n                    name: 'Accept',\n                    value: 'application/json',\n                    enabled: true\n                  }\n                ],\n                params: [],\n                body: {\n                  mode: 'none'\n                }\n              },\n              response: {\n                status: 200,\n                statusText: 'OK',\n                headers: [\n                  {\n                    name: 'Content-Type',\n                    value: 'application/json',\n                    enabled: true\n                  }\n                ],\n                body: {\n                  type: 'json',\n                  content: JSON.stringify({\n                    users: [\n                      { id: 1, name: 'John Doe', email: 'john@example.com' },\n                      { id: 2, name: 'Jane Smith', email: 'jane@example.com' }\n                    ]\n                  })\n                }\n              }\n            },\n            {\n              name: 'Error Response',\n              description: 'Error response when server fails',\n              type: 'http-request',\n              request: {\n                method: 'GET',\n                url: 'https://api.example.com/users',\n                headers: [\n                  {\n                    name: 'Accept',\n                    value: 'application/json',\n                    enabled: true\n                  }\n                ],\n                params: [],\n                body: {\n                  mode: 'none'\n                }\n              },\n              response: {\n                status: 500,\n                statusText: 'Internal Server Error',\n                headers: [\n                  {\n                    name: 'Content-Type',\n                    value: 'application/json',\n                    enabled: true\n                  }\n                ],\n                body: {\n                  type: 'json',\n                  content: JSON.stringify({\n                    error: 'Internal Server Error',\n                    message: 'Something went wrong'\n                  })\n                }\n              }\n            }\n          ]\n        },\n        {\n          name: 'Create User',\n          type: 'http-request',\n          request: {\n            method: 'POST',\n            url: 'https://api.example.com/users',\n            headers: [\n              {\n                name: 'Content-Type',\n                value: 'application/json',\n                enabled: true\n              }\n            ],\n            params: [],\n            body: {\n              mode: 'json',\n              json: JSON.stringify({\n                name: 'New User',\n                email: 'newuser@example.com'\n              })\n            }\n          },\n          examples: [\n            {\n              name: 'User Created',\n              description: 'Successfully created user',\n              type: 'http-request',\n              request: {\n                method: 'POST',\n                url: 'https://api.example.com/users',\n                headers: [\n                  {\n                    name: 'Content-Type',\n                    value: 'application/json',\n                    enabled: true\n                  }\n                ],\n                params: [],\n                body: {\n                  mode: 'json',\n                  json: JSON.stringify({\n                    name: 'New User',\n                    email: 'newuser@example.com'\n                  })\n                }\n              },\n              response: {\n                status: 201,\n                statusText: 'Created',\n                headers: [\n                  {\n                    name: 'Content-Type',\n                    value: 'application/json',\n                    enabled: true\n                  }\n                ],\n                body: {\n                  type: 'json',\n                  content: JSON.stringify({\n                    id: 123,\n                    name: 'New User',\n                    email: 'newuser@example.com',\n                    createdAt: '2023-01-01T00:00:00Z'\n                  })\n                }\n              }\n            }\n          ]\n        }\n      ]\n    };\n\n    const postmanCollection = brunoToPostman(brunoCollection);\n\n    // Verify basic collection structure\n    expect(postmanCollection).toBeDefined();\n    expect(postmanCollection.info.name).toBe('Test Collection with Examples');\n    expect(postmanCollection.item).toHaveLength(2);\n\n    // Test first request with examples\n    const getUsersRequest = postmanCollection.item[0];\n    expect(getUsersRequest.name).toBe('Get Users');\n    expect(getUsersRequest.request.method).toBe('GET');\n    expect(getUsersRequest.request.url.raw).toBe('https://api.example.com/users');\n\n    // Verify examples are converted to responses\n    expect(getUsersRequest.response).toBeDefined();\n    expect(getUsersRequest.response).toHaveLength(2);\n\n    // Test first example (Success Response)\n    const successResponse = getUsersRequest.response[0];\n    expect(successResponse.name).toBe('Success Response');\n    expect(successResponse.status).toBe('OK');\n    expect(successResponse.code).toEqual(200);\n    expect(successResponse._postman_previewlanguage).toBe('json');\n    expect(successResponse.header).toHaveLength(1);\n    expect(successResponse.header[0].key).toBe('Content-Type');\n    expect(successResponse.header[0].value).toBe('application/json');\n    expect(JSON.parse(successResponse.body)).toEqual({\n      users: [\n        { id: 1, name: 'John Doe', email: 'john@example.com' },\n        { id: 2, name: 'Jane Smith', email: 'jane@example.com' }\n      ]\n    });\n\n    // Verify originalRequest is properly generated\n    expect(successResponse.originalRequest).toBeDefined();\n    expect(successResponse.originalRequest.method).toBe('GET');\n    expect(successResponse.originalRequest.url.raw).toBe('https://api.example.com/users');\n    expect(successResponse.originalRequest.header).toHaveLength(1);\n    expect(successResponse.originalRequest.header[0].key).toBe('Accept');\n    expect(successResponse.originalRequest.header[0].value).toBe('application/json');\n    // GET request with mode 'none' should not have body\n    expect(successResponse.originalRequest.body).toBeUndefined();\n\n    // Test second example (Error Response)\n    const errorResponse = getUsersRequest.response[1];\n    expect(errorResponse.name).toBe('Error Response');\n    expect(errorResponse.status).toBe('Internal Server Error');\n    expect(errorResponse.code).toBe(500);\n    expect(JSON.parse(errorResponse.body)).toEqual({\n      error: 'Internal Server Error',\n      message: 'Something went wrong'\n    });\n\n    // Test second request with examples\n    const createUserRequest = postmanCollection.item[1];\n    expect(createUserRequest.name).toBe('Create User');\n    expect(createUserRequest.request.method).toBe('POST');\n    expect(createUserRequest.response).toBeDefined();\n    expect(createUserRequest.response).toHaveLength(1);\n\n    const createdResponse = createUserRequest.response[0];\n    expect(createdResponse.name).toBe('User Created');\n    expect(createdResponse.status).toBe('Created');\n    expect(createdResponse.code).toEqual(201);\n    expect(JSON.parse(createdResponse.body)).toEqual({\n      id: 123,\n      name: 'New User',\n      email: 'newuser@example.com',\n      createdAt: '2023-01-01T00:00:00Z'\n    });\n\n    // Verify originalRequest includes body when present\n    expect(createdResponse.originalRequest).toBeDefined();\n    expect(createdResponse.originalRequest.method).toBe('POST');\n    expect(createdResponse.originalRequest.body).toBeDefined();\n    expect(createdResponse.originalRequest.body.mode).toBe('raw');\n    expect(createdResponse.originalRequest.body.options.raw.language).toBe('json');\n    expect(JSON.parse(createdResponse.originalRequest.body.raw)).toEqual({\n      name: 'New User',\n      email: 'newuser@example.com'\n    });\n  });\n\n  it('should handle requests without examples', () => {\n    const brunoCollection = {\n      name: 'Collection without Examples',\n      items: [\n        {\n          name: 'Simple Request',\n          type: 'http-request',\n          request: {\n            method: 'GET',\n            url: 'https://api.example.com/test',\n            headers: [],\n            params: [],\n            body: { mode: 'none' }\n          }\n          // No examples\n        }\n      ]\n    };\n\n    const postmanCollection = brunoToPostman(brunoCollection);\n\n    expect(postmanCollection).toBeDefined();\n    expect(postmanCollection.item).toHaveLength(1);\n\n    const request = postmanCollection.item[0];\n    expect(request.name).toBe('Simple Request');\n    expect(request.response).toBeUndefined(); // No examples, so no response array\n  });\n\n  it('should handle empty examples array', () => {\n    const brunoCollection = {\n      name: 'Collection with Empty Examples',\n      items: [\n        {\n          name: 'Request with Empty Examples',\n          type: 'http-request',\n          request: {\n            method: 'GET',\n            url: 'https://api.example.com/test',\n            headers: [],\n            params: [],\n            body: { mode: 'none' }\n          },\n          examples: [] // Empty array\n        }\n      ]\n    };\n\n    const postmanCollection = brunoToPostman(brunoCollection);\n\n    expect(postmanCollection).toBeDefined();\n    expect(postmanCollection.item).toHaveLength(1);\n\n    const request = postmanCollection.item[0];\n    expect(request.name).toBe('Request with Empty Examples');\n    expect(request.response).toBeUndefined(); // Empty examples array, so no response array\n  });\n\n  it('should handle different content types in examples', () => {\n    const brunoCollection = {\n      name: 'Collection with Different Content Types',\n      items: [\n        {\n          name: 'XML Response',\n          type: 'http-request',\n          request: {\n            method: 'GET',\n            url: 'https://api.example.com/xml',\n            headers: [],\n            params: [],\n            body: { mode: 'none' }\n          },\n          examples: [\n            {\n              name: 'XML Example',\n              type: 'http-request',\n              request: {\n                method: 'GET',\n                url: 'https://api.example.com/xml',\n                headers: [],\n                params: [],\n                body: { mode: 'none' }\n              },\n              response: {\n                status: 200,\n                statusText: 'OK',\n                headers: [\n                  {\n                    name: 'Content-Type',\n                    value: 'application/xml',\n                    enabled: true\n                  }\n                ],\n                body: {\n                  type: 'xml',\n                  content: '<users><user><id>1</id><name>John</name></user></users>'\n                }\n              }\n            },\n            {\n              name: 'HTML Example',\n              type: 'http-request',\n              request: {\n                method: 'GET',\n                url: 'https://api.example.com/html',\n                headers: [],\n                params: [],\n                body: { mode: 'none' }\n              },\n              response: {\n                status: 200,\n                statusText: 'OK',\n                headers: [\n                  {\n                    name: 'Content-Type',\n                    value: 'text/html',\n                    enabled: true\n                  }\n                ],\n                body: {\n                  type: 'html',\n                  content: '<html><body><h1>Hello World</h1></body></html>'\n                }\n              }\n            }\n          ]\n        }\n      ]\n    };\n\n    const postmanCollection = brunoToPostman(brunoCollection);\n\n    expect(postmanCollection).toBeDefined();\n    expect(postmanCollection.item).toHaveLength(1);\n\n    const request = postmanCollection.item[0];\n    expect(request.response).toHaveLength(2);\n\n    // Test XML response\n    const xmlResponse = request.response[0];\n    expect(xmlResponse.name).toBe('XML Example');\n    expect(xmlResponse._postman_previewlanguage).toBe('xml');\n    expect(xmlResponse.body).toBe('<users><user><id>1</id><name>John</name></user></users>');\n\n    // Test HTML response\n    const htmlResponse = request.response[1];\n    expect(htmlResponse.name).toBe('HTML Example');\n    expect(htmlResponse._postman_previewlanguage).toBe('html');\n    expect(htmlResponse.body).toBe('<html><body><h1>Hello World</h1></body></html>');\n  });\n\n  it('should handle folders with examples', () => {\n    const brunoCollection = {\n      name: 'Collection with Folders and Examples',\n      items: [\n        {\n          name: 'Users API',\n          type: 'folder',\n          items: [\n            {\n              name: 'Get User',\n              type: 'http-request',\n              request: {\n                method: 'GET',\n                url: 'https://api.example.com/users/1',\n                headers: [],\n                params: [],\n                body: { mode: 'none' }\n              },\n              examples: [\n                {\n                  name: 'User Found',\n                  type: 'http-request',\n                  request: {\n                    method: 'GET',\n                    url: 'https://api.example.com/users/1',\n                    headers: [],\n                    params: [],\n                    body: { mode: 'none' }\n                  },\n                  response: {\n                    status: 200,\n                    statusText: 'OK',\n                    headers: [\n                      {\n                        name: 'Content-Type',\n                        value: 'application/json',\n                        enabled: true\n                      }\n                    ],\n                    body: {\n                      type: 'json',\n                      content: JSON.stringify({ id: 1, name: 'John Doe' })\n                    }\n                  }\n                }\n              ]\n            }\n          ]\n        }\n      ]\n    };\n\n    const postmanCollection = brunoToPostman(brunoCollection);\n\n    expect(postmanCollection).toBeDefined();\n    expect(postmanCollection.item).toHaveLength(1);\n\n    const folder = postmanCollection.item[0];\n    expect(folder.name).toBe('Users API');\n    expect(folder.item).toHaveLength(1);\n\n    const request = folder.item[0];\n    expect(request.name).toBe('Get User');\n    expect(request.response).toBeDefined();\n    expect(request.response).toHaveLength(1);\n    expect(request.response[0].name).toBe('User Found');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/bruno-to-postman.spec.js",
    "content": "import { sanitizeUrl, transformUrl, brunoToPostman } from '../../src/postman/bruno-to-postman';\n\ndescribe('transformUrl', () => {\n  it('should handle basic URL with path variables', () => {\n    const url = 'https://example.com/{{username}}/api/resource/:id';\n    const params = [\n      { name: 'id', value: '123', type: 'path' }\n    ];\n\n    const result = transformUrl(url, params);\n\n    expect(result).toEqual({\n      raw: 'https://example.com/{{username}}/api/resource/:id',\n      protocol: 'https',\n      host: ['example', 'com'],\n      path: ['{{username}}', 'api', 'resource', ':id'],\n      query: [],\n      variable: [\n        { key: 'id', value: '123' }\n      ]\n    });\n  });\n\n  it('should handle URL with query parameters', () => {\n    const url = 'https://example.com/api/resource?limit=10&offset=20';\n    const params = [\n      { name: 'limit', value: '10', type: 'query' },\n      { name: 'offset', value: '20', type: 'query' }\n    ];\n\n    const result = transformUrl(url, params);\n\n    expect(result).toEqual({\n      raw: 'https://example.com/api/resource?limit=10&offset=20',\n      protocol: 'https',\n      host: ['example', 'com'],\n      path: ['api', 'resource'],\n      query: [\n        { key: 'limit', value: '10' },\n        { key: 'offset', value: '20' }\n      ],\n      variable: []\n    });\n  });\n\n  it('should handle URL without protocol', () => {\n    const url = 'example.com/api/resource';\n    const params = [];\n\n    const result = transformUrl(url, params);\n\n    expect(result).toEqual({\n      raw: 'example.com/api/resource',\n      protocol: '',\n      host: ['example', 'com'],\n      path: ['api', 'resource'],\n      query: [],\n      variable: []\n    });\n  });\n});\n\ndescribe('sanitizeUrl', () => {\n  it('should replace backslashes with slashes', () => {\n    const input = 'http:\\\\\\\\example.com\\\\path\\\\to\\\\file';\n    const expected = 'http://example.com/path/to/file';\n    expect(sanitizeUrl(input)).toBe(expected);\n  });\n\n  it('should collapse multiple slashes into a single slash', () => {\n    const input = 'http://example.com//path///to////file';\n    const expected = 'http://example.com/path/to/file';\n    expect(sanitizeUrl(input)).toBe(expected);\n  });\n\n  it('should handle URLs with mixed slashes', () => {\n    const input = 'http:\\\\example.com//path\\\\to//file';\n    const expected = 'http://example.com/path/to/file';\n    expect(sanitizeUrl(input)).toBe(expected);\n  });\n});\n\ndescribe('brunoToPostman null checks and fallbacks', () => {\n  it('should handle null or undefined headers', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'Test Request',\n          type: 'http-request',\n          request: {\n            method: 'GET',\n            url: 'https://example.com',\n            headers: null\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].request.header).toEqual([]);\n  });\n\n  it('should handle null or undefined items in headers', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'Test Request',\n          type: 'http-request',\n          request: {\n            method: 'GET',\n            url: 'https://example.com',\n            headers: [\n              { name: null, value: 'test-value', enabled: true },\n              { name: 'Content-Type', value: null, enabled: true }\n            ]\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].request.header).toEqual([\n      { key: '', value: 'test-value', disabled: false, type: 'default' },\n      { key: 'Content-Type', value: '', disabled: false, type: 'default' }\n    ]);\n  });\n\n  it('should handle null or undefined body', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'Test Request',\n          type: 'http-request',\n          request: {\n            method: 'GET',\n            url: 'https://example.com',\n            body: null\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    // Should not have body property since we're checking for body before adding it\n    expect(result.item[0].request.body).toBeUndefined();\n  });\n\n  it('should handle null or undefined body mode', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'Test Request',\n          type: 'http-request',\n          request: {\n            method: 'GET',\n            url: 'https://example.com',\n            body: {}\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    // Should use default raw mode for undefined body mode\n    expect(result.item[0].request.body).toEqual({\n      mode: 'raw',\n      raw: ''\n    });\n  });\n\n  it('should handle null or undefined formUrlEncoded array', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'Test Request',\n          type: 'http-request',\n          request: {\n            method: 'POST',\n            url: 'https://example.com',\n            body: {\n              mode: 'formUrlEncoded',\n              formUrlEncoded: null\n            }\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].request.body).toEqual({\n      mode: 'urlencoded',\n      urlencoded: []\n    });\n  });\n\n  it('should handle null or undefined multipartForm array', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'Test Request',\n          type: 'http-request',\n          request: {\n            method: 'POST',\n            url: 'https://example.com',\n            body: {\n              mode: 'multipartForm',\n              multipartForm: null\n            }\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].request.body).toEqual({\n      mode: 'formdata',\n      formdata: []\n    });\n  });\n\n  it('should handle null or undefined items in form data', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'Test Request',\n          type: 'http-request',\n          request: {\n            method: 'POST',\n            url: 'https://example.com',\n            body: {\n              mode: 'formUrlEncoded',\n              formUrlEncoded: [\n                { name: null, value: 'test-value', enabled: true },\n                { name: 'field', value: null, enabled: true }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].request.body.urlencoded).toEqual([\n      { key: '', value: 'test-value', disabled: false, type: 'default' },\n      { key: 'field', value: '', disabled: false, type: 'default' }\n    ]);\n  });\n\n  it('should handle null or undefined method', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'Test Request',\n          type: 'http-request',\n          request: {\n            url: 'https://example.com',\n            method: null\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].request.method).toBe('GET');\n  });\n\n  it('should handle null or undefined url', () => {\n    // Mock console.error to prevent it from logging during test\n    const originalConsoleError = console.error;\n    console.error = jest.fn();\n\n    const simpleCollection = {\n      items: [\n        {\n          name: 'Test Request',\n          type: 'http-request',\n          request: {\n            method: 'GET',\n            url: null\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].request.url.raw).toBe('');\n  });\n\n  it('should handle null or undefined params', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'Test Request',\n          type: 'http-request',\n          request: {\n            method: 'GET',\n            url: 'https://example.com',\n            params: null\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].request.url.variable).toEqual([]);\n  });\n\n  it('should handle null or undefined docs', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'Test Request',\n          type: 'http-request',\n          request: {\n            method: 'GET',\n            url: 'https://example.com',\n            docs: null\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].request.description).toBe('');\n  });\n\n  it('should handle null or undefined folder name', () => {\n    const simpleCollection = {\n      items: [\n        {\n          type: 'folder',\n          name: null,\n          items: []\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].name).toBe('Untitled Folder');\n  });\n\n  it('should handle null or undefined request name', () => {\n    const simpleCollection = {\n      items: [\n        {\n          type: 'http-request',\n          name: null,\n          request: {\n            method: 'GET',\n            url: 'https://example.com'\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].name).toBe('Untitled Request');\n  });\n\n  it('should handle null or undefined folder items', () => {\n    const simpleCollection = {\n      items: [\n        {\n          type: 'folder',\n          name: 'Test Folder',\n          items: null\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].item).toEqual([]);\n  });\n\n  it('should handle null or undefined auth object', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'Test Request',\n          type: 'http-request',\n          request: {\n            method: 'GET',\n            url: 'https://example.com',\n            auth: null\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].request.auth).toEqual({ type: 'noauth' });\n  });\n\n  it('should handle missing token in bearer auth', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'Test Request',\n          type: 'http-request',\n          request: {\n            method: 'GET',\n            url: 'https://example.com',\n            auth: {\n              mode: 'bearer',\n              bearer: { token: null }\n            }\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].request.auth).toEqual({\n      type: 'bearer',\n      bearer: {\n        key: 'token',\n        value: '',\n        type: 'string'\n      }\n    });\n  });\n\n  it('should handle missing username/password in basic auth', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'Test Request',\n          type: 'http-request',\n          request: {\n            method: 'GET',\n            url: 'https://example.com',\n            auth: {\n              mode: 'basic',\n              basic: { username: null, password: undefined }\n            }\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].request.auth).toEqual({\n      type: 'basic',\n      basic: [\n        {\n          key: 'password',\n          value: '',\n          type: 'string'\n        },\n        {\n          key: 'username',\n          value: '',\n          type: 'string'\n        }\n      ]\n    });\n  });\n\n  it('should handle missing key/value in apikey auth', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'Test Request',\n          type: 'http-request',\n          request: {\n            method: 'GET',\n            url: 'https://example.com',\n            auth: {\n              mode: 'apikey',\n              apikey: { key: null, value: undefined }\n            }\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].request.auth).toEqual({\n      type: 'apikey',\n      apikey: [\n        {\n          key: 'key',\n          value: '',\n          type: 'string'\n        },\n        {\n          key: 'value',\n          value: '',\n          type: 'string'\n        }\n      ]\n    });\n  });\n});\n\ndescribe('brunoToPostman multipartForm handling', () => {\n  it('should export file type with type: file and src field', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'Test Request',\n          type: 'http-request',\n          request: {\n            method: 'POST',\n            url: 'https://example.com',\n            body: {\n              mode: 'multipartForm',\n              multipartForm: [\n                {\n                  name: 'myFile',\n                  value: ['/path/to/file1.txt', '/path/to/file2.txt'],\n                  type: 'file',\n                  enabled: true\n                }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].request.body).toEqual({\n      mode: 'formdata',\n      formdata: [\n        {\n          key: 'myFile',\n          src: ['/path/to/file1.txt', '/path/to/file2.txt'],\n          disabled: false,\n          type: 'file'\n        }\n      ]\n    });\n  });\n\n  it('should export text type with type: text and value field', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'Test Request',\n          type: 'http-request',\n          request: {\n            method: 'POST',\n            url: 'https://example.com',\n            body: {\n              mode: 'multipartForm',\n              multipartForm: [\n                {\n                  name: 'myField',\n                  value: 'some text value',\n                  type: 'text',\n                  enabled: true\n                }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].request.body).toEqual({\n      mode: 'formdata',\n      formdata: [\n        {\n          key: 'myField',\n          value: 'some text value',\n          disabled: false,\n          type: 'text'\n        }\n      ]\n    });\n  });\n\n  it('should export contentType when specified', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'Test Request',\n          type: 'http-request',\n          request: {\n            method: 'POST',\n            url: 'https://example.com',\n            body: {\n              mode: 'multipartForm',\n              multipartForm: [\n                {\n                  name: 'myFile',\n                  value: ['/path/to/file.json'],\n                  type: 'file',\n                  contentType: 'application/json',\n                  enabled: true\n                }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].request.body).toEqual({\n      mode: 'formdata',\n      formdata: [\n        {\n          key: 'myFile',\n          src: '/path/to/file.json',\n          disabled: false,\n          type: 'file',\n          contentType: 'application/json'\n        }\n      ]\n    });\n  });\n\n  it('should handle mixed file and text fields', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'Test Request',\n          type: 'http-request',\n          request: {\n            method: 'POST',\n            url: 'https://example.com',\n            body: {\n              mode: 'multipartForm',\n              multipartForm: [\n                {\n                  name: 'textField',\n                  value: 'hello',\n                  type: 'text',\n                  enabled: true\n                },\n                {\n                  name: 'fileField',\n                  value: ['/path/to/file.txt'],\n                  type: 'file',\n                  enabled: false\n                }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].request.body).toEqual({\n      mode: 'formdata',\n      formdata: [\n        {\n          key: 'textField',\n          value: 'hello',\n          disabled: false,\n          type: 'text'\n        },\n        {\n          key: 'fileField',\n          src: '/path/to/file.txt',\n          disabled: true,\n          type: 'file'\n        }\n      ]\n    });\n  });\n\n  it('should handle file type with string value (not array)', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'Test Request',\n          type: 'http-request',\n          request: {\n            method: 'POST',\n            url: 'https://example.com',\n            body: {\n              mode: 'multipartForm',\n              multipartForm: [\n                {\n                  name: 'myFile',\n                  value: '/single/file/path.txt',\n                  type: 'file',\n                  enabled: true\n                }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].request.body.formdata[0]).toEqual({\n      key: 'myFile',\n      src: '/single/file/path.txt',\n      disabled: false,\n      type: 'file'\n    });\n  });\n\n  it('should handle file type with empty value', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'Test Request',\n          type: 'http-request',\n          request: {\n            method: 'POST',\n            url: 'https://example.com',\n            body: {\n              mode: 'multipartForm',\n              multipartForm: [\n                {\n                  name: 'myFile',\n                  value: '',\n                  type: 'file',\n                  enabled: true\n                }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].request.body.formdata[0]).toEqual({\n      key: 'myFile',\n      src: null,\n      disabled: false,\n      type: 'file'\n    });\n  });\n});\n\ndescribe('brunoToPostman protocolProfileBehavior handling', () => {\n  it('should add disableBodyPruning for GET requests with body', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'GET with body',\n          type: 'http-request',\n          request: {\n            method: 'GET',\n            url: 'https://example.com',\n            body: {\n              mode: 'multipartForm',\n              multipartForm: [\n                {\n                  name: 'file',\n                  value: '/path/to/file.txt',\n                  type: 'file',\n                  enabled: true\n                }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].protocolProfileBehavior).toEqual({\n      disableBodyPruning: true\n    });\n  });\n\n  it('should not add protocolProfileBehavior for POST requests with body', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'POST with body',\n          type: 'http-request',\n          request: {\n            method: 'POST',\n            url: 'https://example.com',\n            body: {\n              mode: 'multipartForm',\n              multipartForm: [\n                {\n                  name: 'file',\n                  value: '/path/to/file.txt',\n                  type: 'file',\n                  enabled: true\n                }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].protocolProfileBehavior).toBeUndefined();\n  });\n\n  it('should not add protocolProfileBehavior for GET requests without body', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'GET without body',\n          type: 'http-request',\n          request: {\n            method: 'GET',\n            url: 'https://example.com'\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].protocolProfileBehavior).toBeUndefined();\n  });\n\n  it('should add disableBodyPruning for HEAD requests with body', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'HEAD with body',\n          type: 'http-request',\n          request: {\n            method: 'HEAD',\n            url: 'https://example.com',\n            body: {\n              mode: 'json',\n              json: '{\"test\": true}'\n            }\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.item[0].protocolProfileBehavior).toEqual({\n      disableBodyPruning: true\n    });\n  });\n});\n\ndescribe('brunoToPostman event handling', () => {\n  it('should generate events for request scripts (req/res)', () => {\n    const simpleCollection = {\n      items: [\n        {\n          name: 'Test Request',\n          type: 'http-request',\n          request: {\n            method: 'GET',\n            url: 'https://example.com',\n            script: {\n              req: 'console.log(\"pre\");',\n              res: 'console.log(\"post\");'\n            }\n          }\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    const events = result.item[0].event;\n\n    expect(events).toHaveLength(2);\n    expect(events[0]).toMatchObject({ listen: 'prerequest', script: { exec: ['console.log(\"pre\");'] } });\n    expect(events[1]).toMatchObject({ listen: 'test', script: { exec: ['console.log(\"post\");'] } });\n  });\n\n  it('should generate events for folder scripts', () => {\n    const simpleCollection = {\n      items: [\n        {\n          type: 'folder',\n          name: 'Test Folder',\n          script: {\n            req: 'console.log(\"folder pre\");',\n            res: 'console.log(\"folder post\");'\n          },\n          items: []\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    const folder = result.item[0];\n\n    expect(folder.name).toBe('Test Folder');\n    expect(folder.event).toHaveLength(2);\n    expect(folder.event[0].listen).toBe('prerequest');\n    expect(folder.event[1].listen).toBe('test');\n  });\n\n  it('should generate collection-level events from root', () => {\n    const simpleCollection = {\n      root: {\n        script: {\n          req: 'console.log(\"collection pre\");',\n          res: 'console.log(\"collection post\");'\n        }\n      },\n      items: []\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    expect(result.event).toHaveLength(2);\n    expect(result.event[0].listen).toBe('prerequest');\n    expect(result.event[1].listen).toBe('test');\n  });\n\n  it('should handle nested folders and requests with scripts', () => {\n    const simpleCollection = {\n      items: [\n        {\n          type: 'folder',\n          name: 'Parent Folder',\n          items: [\n            {\n              type: 'http-request',\n              name: 'Nested Request',\n              request: {\n                method: 'GET',\n                url: 'https://example.com',\n                script: { req: 'console.log(\"nested pre\");' }\n              }\n            }\n          ]\n        }\n      ]\n    };\n\n    const result = brunoToPostman(simpleCollection);\n    const folder = result.item[0];\n    const nestedRequest = folder.item[0];\n\n    expect(folder.name).toBe('Parent Folder');\n    expect(nestedRequest.name).toBe('Nested Request');\n    expect(nestedRequest.event).toHaveLength(1);\n    expect(nestedRequest.event[0].listen).toBe('prerequest');\n    expect(nestedRequest.event[0].script.exec).toEqual(['console.log(\"nested pre\");']);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-env-to-bruno-env.spec.js",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport postmanToBrunoEnvironment from '../../src/postman/postman-env-to-bruno-env';\n\ndescribe('postmanToBrunoEnvironment Function', () => {\n  it('should correctly import a valid Postman environment file', async () => {\n    const postmanEnvironment = {\n      id: 'some-id',\n      name: 'My Environment',\n      values: [\n        {\n          key: 'var1',\n          value: 'value1',\n          enabled: true,\n          type: 'text'\n        },\n        {\n          key: 'var2',\n          value: 'value2',\n          enabled: false,\n          type: 'secret'\n        }\n      ]\n    };\n\n    const brunoEnvironment = await postmanToBrunoEnvironment(postmanEnvironment);\n\n    const expectedEnvironment = {\n      name: 'My Environment',\n      variables: [\n        {\n          name: 'var1',\n          value: 'value1',\n          enabled: true,\n          secret: false,\n          type: 'text',\n          uid: 'mockeduuidvalue123456'\n        },\n        {\n          name: 'var2',\n          value: 'value2',\n          enabled: false,\n          secret: true,\n          type: 'text',\n          uid: 'mockeduuidvalue123456'\n        }\n      ]\n    };\n\n    expect(brunoEnvironment).toEqual(expectedEnvironment);\n  });\n\n  it('should handle falsy values in environment variables', async () => {\n    const postmanEnvironment = {\n      id: 'some-id',\n      name: 'My Environment',\n      values: [\n        {\n          enabled: true,\n          type: 'text'\n        },\n        {\n          value: '',\n          enabled: true,\n          type: 'text'\n        },\n        {\n          key: '',\n          enabled: true,\n          type: 'text'\n        },\n        {\n          key: '',\n          value: '',\n          enabled: true,\n          type: 'text'\n        }\n      ]\n    };\n\n    const brunoEnvironment = await postmanToBrunoEnvironment(postmanEnvironment);\n\n    const expectedEnvironment = {\n      name: 'My Environment',\n      variables: [\n        {\n          name: '',\n          value: '',\n          enabled: true,\n          secret: false,\n          type: 'text',\n          uid: 'mockeduuidvalue123456'\n        },\n        {\n          name: '',\n          value: '',\n          enabled: true,\n          secret: false,\n          type: 'text',\n          uid: 'mockeduuidvalue123456'\n        },\n        {\n          name: '',\n          value: '',\n          enabled: true,\n          secret: false,\n          type: 'text',\n          uid: 'mockeduuidvalue123456'\n        }\n      ]\n    };\n\n    expect(brunoEnvironment).toEqual(expectedEnvironment);\n  });\n\n  it.skip('should throw Error when JSON parsing fails', async () => {\n    const invalidBrunoEnvironment = {\n      id: 'some-id',\n      name: 'My Environment',\n      values: [\n        {\n          key: 'var1',\n          value: 'value1',\n          enabled: true,\n          type: 'text'\n        }\n      ]\n    };\n\n    await expect(postmanToBrunoEnvironment(invalidBrunoEnvironment)).rejects.toThrow(Error);\n    await expect(postmanToBrunoEnvironment(invalidBrunoEnvironment)).rejects.toThrow(\n      'Unable to parse the postman environment json file'\n    );\n  });\n\n  it('should handle empty variables', async () => {\n    const collectionWithEmptyVars = {\n      name: 'My Environment',\n      values: []\n    };\n\n    const brunoCollection = await postmanToBrunoEnvironment(collectionWithEmptyVars);\n    expect(brunoCollection.variables).toEqual([]);\n  });\n\n  it('should handle undefined variables', async () => {\n    const collectionWithUndefinedVars = {\n      name: 'My Environment'\n    };\n\n    const brunoCollection = await postmanToBrunoEnvironment(collectionWithUndefinedVars);\n    expect(brunoCollection.variables).toEqual([]);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-to-bruno/collection-auth.spec.js",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport postmanToBruno from '../../../src/postman/postman-to-bruno';\n\ndescribe('Collection Authentication', () => {\n  it('should handle no auth at collection level (when auth property is absent)', async () => {\n    const postmanCollection = {\n      info: {\n        name: 'Collection level no auth',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [],\n      event: [\n        {\n          listen: 'prerequest',\n          script: {\n            type: 'text/javascript',\n            packages: {},\n            exec: ['']\n          }\n        },\n        {\n          listen: 'test',\n          script: {\n            type: 'text/javascript',\n            packages: {},\n            exec: ['']\n          }\n        }\n      ]\n    };\n\n    const result = await postmanToBruno(postmanCollection);\n    // console.log('result', JSON.stringify(result, null, 2));\n\n    expect(result.root.request.auth).toEqual({\n      mode: 'none',\n      basic: null,\n      bearer: null,\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n  });\n\n  it('should handle basic auth at collection level', async () => {\n    const postmanCollection = {\n      info: {\n        name: 'Collection level basic auth',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [],\n      auth: {\n        type: 'basic',\n        basic: [\n          {\n            key: 'password',\n            value: 'testpass',\n            type: 'string'\n          },\n          {\n            key: 'username',\n            value: 'testuser',\n            type: 'string'\n          }\n        ]\n      },\n      event: [\n        {\n          listen: 'prerequest',\n          script: {\n            type: 'text/javascript',\n            packages: {},\n            exec: ['']\n          }\n        },\n        {\n          listen: 'test',\n          script: {\n            type: 'text/javascript',\n            packages: {},\n            exec: ['']\n          }\n        }\n      ]\n    };\n\n    const result = await postmanToBruno(postmanCollection);\n    // console.log('result', JSON.stringify(result, null, 2));\n\n    expect(result.root.request.auth).toEqual({\n      mode: 'basic',\n      basic: {\n        username: 'testuser',\n        password: 'testpass'\n      },\n      bearer: null,\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n  });\n\n  it('should handle bearer token auth at collection level', async () => {\n    const postmanCollection = {\n      info: {\n        name: 'Collection level bearer token',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [],\n      auth: {\n        type: 'bearer',\n        bearer: [\n          {\n            key: 'token',\n            value: 'token',\n            type: 'string'\n          }\n        ]\n      },\n      event: [\n        {\n          listen: 'prerequest',\n          script: {\n            type: 'text/javascript',\n            packages: {},\n            exec: ['']\n          }\n        },\n        {\n          listen: 'test',\n          script: {\n            type: 'text/javascript',\n            packages: {},\n            exec: ['']\n          }\n        }\n      ]\n    };\n\n    const result = await postmanToBruno(postmanCollection);\n    // console.log('result', JSON.stringify(result, null, 2));\n\n    expect(result.root.request.auth).toEqual({\n      mode: 'bearer',\n      basic: null,\n      bearer: {\n        token: 'token'\n      },\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n  });\n\n  it('should handle API key auth at collection level', async () => {\n    const postmanCollection = {\n      info: {\n        name: 'Collection level api key',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [],\n      auth: {\n        type: 'apikey',\n        apikey: [\n          {\n            key: 'value',\n            value: 'apikey',\n            type: 'string'\n          },\n          {\n            key: 'key',\n            value: 'apikey',\n            type: 'string'\n          }\n        ]\n      },\n      event: [\n        {\n          listen: 'prerequest',\n          script: {\n            type: 'text/javascript',\n            packages: {},\n            exec: ['']\n          }\n        },\n        {\n          listen: 'test',\n          script: {\n            type: 'text/javascript',\n            packages: {},\n            exec: ['']\n          }\n        }\n      ]\n    };\n\n    const result = await postmanToBruno(postmanCollection);\n\n    expect(result.root.request.auth).toEqual({\n      mode: 'apikey',\n      basic: null,\n      bearer: null,\n      awsv4: null,\n      apikey: {\n        key: 'apikey',\n        value: 'apikey',\n        placement: 'header'\n      },\n      oauth2: null,\n      digest: null\n    });\n  });\n\n  it('should handle digest auth at collection level', async () => {\n    const postmanCollection = {\n      info: {\n        name: 'Collection level digest auth',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [],\n      auth: {\n        type: 'digest',\n        digest: [\n          {\n            key: 'password',\n            value: 'digest auth',\n            type: 'string'\n          },\n          {\n            key: 'username',\n            value: 'digest auth',\n            type: 'string'\n          },\n          {\n            key: 'algorithm',\n            value: 'MD5',\n            type: 'string'\n          }\n        ]\n      },\n      event: [\n        {\n          listen: 'prerequest',\n          script: {\n            type: 'text/javascript',\n            packages: {},\n            exec: ['']\n          }\n        },\n        {\n          listen: 'test',\n          script: {\n            type: 'text/javascript',\n            packages: {},\n            exec: ['']\n          }\n        }\n      ]\n    };\n\n    const result = await postmanToBruno(postmanCollection);\n\n    expect(result.root.request.auth).toEqual({\n      mode: 'digest',\n      basic: null,\n      bearer: null,\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: {\n        username: 'digest auth',\n        password: 'digest auth'\n      }\n    });\n  });\n  it('should handle missing auth values when auth.type exists', async () => {\n    const postmanCollection = {\n      info: {\n        name: 'Collection with missing auth values',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [],\n      auth: {\n        type: 'basic'\n        // Missing basic auth values\n      },\n      event: [\n        {\n          listen: 'prerequest',\n          script: {\n            type: 'text/javascript',\n            packages: {},\n            exec: ['']\n          }\n        },\n        {\n          listen: 'test',\n          script: {\n            type: 'text/javascript',\n            packages: {},\n            exec: ['']\n          }\n        }\n      ]\n    };\n\n    const result = await postmanToBruno(postmanCollection);\n\n    expect(result.root.request.auth).toEqual({\n      mode: 'basic',\n      basic: {\n        username: '',\n        password: ''\n      },\n      bearer: null,\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n  });\n\n  it('should handle missing auth values for different auth types', async () => {\n    const postmanCollection = {\n      info: {\n        name: 'Collection with missing auth values for different types',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [],\n      auth: {\n        type: 'bearer'\n        // Missing bearer token\n      },\n      event: [\n        {\n          listen: 'prerequest',\n          script: {\n            type: 'text/javascript',\n            packages: {},\n            exec: ['']\n          }\n        },\n        {\n          listen: 'test',\n          script: {\n            type: 'text/javascript',\n            packages: {},\n            exec: ['']\n          }\n        }\n      ]\n    };\n\n    const result = await postmanToBruno(postmanCollection);\n\n    expect(result.root.request.auth).toEqual({\n      mode: 'bearer',\n      basic: null,\n      bearer: {\n        token: ''\n      },\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-to-bruno/folder-auth.spec.js",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport postmanToBruno from '../../../src/postman/postman-to-bruno';\n\ndescribe('Folder Authentication', () => {\n  it('should handle \"Inherit Auth\" at folder level (auth property absent)', async () => {\n    const postmanCollection = {\n      info: {\n        name: 'Folder Inherit Auth',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'Inheriting Folder',\n          items: []\n        }\n      ],\n      auth: {\n        type: 'basic',\n        basic: [\n          {\n            key: 'password',\n            value: 'testpass',\n            type: 'string'\n          },\n          {\n            key: 'username',\n            value: 'testuser',\n            type: 'string'\n          }\n        ]\n      },\n      event: [\n        {\n          listen: 'prerequest',\n          script: {\n            type: 'text/javascript',\n            packages: {},\n            exec: ['']\n          }\n        },\n        {\n          listen: 'test',\n          script: {\n            type: 'text/javascript',\n            packages: {},\n            exec: ['']\n          }\n        }\n      ]\n    };\n\n    const result = await postmanToBruno(postmanCollection);\n\n    expect(result.items[0].root.request.auth).toEqual({\n      mode: 'inherit',\n      basic: null,\n      bearer: null,\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n  });\n\n  it('should handle explicit \"No Auth\" at folder level', async () => {\n    const postmanCollection = {\n      info: {\n        name: 'Folder No Auth',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'No Auth Folder',\n          item: [],\n          auth: {\n            type: 'noauth'\n          }\n        }\n      ],\n      auth: {\n        type: 'basic',\n        basic: [\n          {\n            key: 'password',\n            value: 'testpass',\n            type: 'string'\n          },\n          {\n            key: 'username',\n            value: 'testuser',\n            type: 'string'\n          }\n        ]\n      },\n      event: [\n        {\n          listen: 'prerequest',\n          script: {\n            type: 'text/javascript',\n            packages: {},\n            exec: ['']\n          }\n        },\n        {\n          listen: 'test',\n          script: {\n            type: 'text/javascript',\n            packages: {},\n            exec: ['']\n          }\n        }\n      ]\n    };\n\n    const result = await postmanToBruno(postmanCollection);\n\n    expect(result.items[0].root.request.auth).toEqual({\n      mode: 'none',\n      basic: null,\n      bearer: null,\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n  });\n\n  it('should handle basic auth at folder level', async () => {\n    const postmanCollection = {\n      info: {\n        name: 'Folder level basic auth',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'folder',\n          item: [],\n          auth: {\n            type: 'basic',\n            basic: [\n              {\n                key: 'password',\n                value: 'testpass',\n                type: 'string'\n              },\n              {\n                key: 'username',\n                value: 'testuser',\n                type: 'string'\n              }\n            ]\n          },\n          event: [\n            {\n              listen: 'prerequest',\n              script: {\n                type: 'text/javascript',\n                packages: {},\n                exec: ['']\n              }\n            },\n            {\n              listen: 'test',\n              script: {\n                type: 'text/javascript',\n                packages: {},\n                exec: ['']\n              }\n            }\n          ]\n        }\n      ]\n    };\n\n    const result = await postmanToBruno(postmanCollection);\n\n    expect(result.items[0].root.request.auth).toEqual({\n      mode: 'basic',\n      basic: {\n        username: 'testuser',\n        password: 'testpass'\n      },\n      bearer: null,\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n  });\n\n  it('should handle bearer token auth at folder level', async () => {\n    const postmanCollection = {\n      info: {\n        name: 'Folder level bearer token',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'folder',\n          item: [],\n          auth: {\n            type: 'bearer',\n            bearer: [\n              {\n                key: 'token',\n                value: 'token',\n                type: 'string'\n              }\n            ]\n          },\n          event: [\n            {\n              listen: 'prerequest',\n              script: {\n                type: 'text/javascript',\n                packages: {},\n                exec: ['']\n              }\n            },\n            {\n              listen: 'test',\n              script: {\n                type: 'text/javascript',\n                packages: {},\n                exec: ['']\n              }\n            }\n          ]\n        }\n      ]\n    };\n\n    const result = await postmanToBruno(postmanCollection);\n\n    expect(result.items[0].root.request.auth).toEqual({\n      mode: 'bearer',\n      basic: null,\n      bearer: { token: 'token' },\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n  });\n\n  it('should handle API key auth at folder level', async () => {\n    const postmanCollection = {\n      info: {\n        name: 'Folder level API key',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'folder',\n          item: [],\n          auth: {\n            type: 'apikey',\n            apikey: [\n              {\n                key: 'value',\n                value: 'apikey',\n                type: 'string'\n              },\n              {\n                key: 'key',\n                value: 'apikey',\n                type: 'string'\n              }\n            ]\n          },\n          event: [\n            {\n              listen: 'prerequest',\n              script: {\n                type: 'text/javascript',\n                packages: {},\n                exec: ['']\n              }\n            },\n            {\n              listen: 'test',\n              script: {\n                type: 'text/javascript',\n                packages: {},\n                exec: ['']\n              }\n            }\n          ]\n        }\n      ]\n    };\n\n    const result = await postmanToBruno(postmanCollection);\n\n    expect(result.items[0].root.request.auth).toEqual({\n      mode: 'apikey',\n      basic: null,\n      bearer: null,\n      awsv4: null,\n      apikey: { key: 'apikey', value: 'apikey', placement: 'header' },\n      oauth2: null,\n      digest: null\n    });\n  });\n\n  it('should handle digest auth at folder level', async () => {\n    const postmanCollection = {\n      info: {\n        name: 'Folder level digest auth',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'folder',\n          item: [],\n          auth: {\n            type: 'digest',\n            digest: [\n              {\n                key: 'password',\n                value: 'digest pass',\n                type: 'string'\n              },\n              {\n                key: 'username',\n                value: 'digest user',\n                type: 'string'\n              },\n              {\n                key: 'algorithm',\n                value: 'MD5',\n                type: 'string'\n              }\n            ]\n          },\n          event: [\n            {\n              listen: 'prerequest',\n              script: {\n                type: 'text/javascript',\n                packages: {},\n                exec: ['']\n              }\n            },\n            {\n              listen: 'test',\n              script: {\n                type: 'text/javascript',\n                packages: {},\n                exec: ['']\n              }\n            }\n          ]\n        }\n      ]\n    };\n\n    const result = await postmanToBruno(postmanCollection);\n\n    expect(result.items[0].root.request.auth).toEqual({\n      mode: 'digest',\n      basic: null,\n      bearer: null,\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: { username: 'digest user', password: 'digest pass' }\n    });\n  });\n\n  it('should handle missing auth values in folder level auth', async () => {\n    const postmanCollection = {\n      info: {\n        name: 'Folder with missing auth values',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'folder',\n          item: [],\n          auth: {\n            type: 'basic'\n            // Missing basic values\n          },\n          event: [\n            {\n              listen: 'prerequest',\n              script: {\n                type: 'text/javascript',\n                packages: {},\n                exec: ['']\n              }\n            },\n            {\n              listen: 'test',\n              script: {\n                type: 'text/javascript',\n                packages: {},\n                exec: ['']\n              }\n            }\n          ]\n        }\n      ]\n    };\n\n    const result = await postmanToBruno(postmanCollection);\n\n    expect(result.items[0].root.request.auth).toEqual({\n      mode: 'basic',\n      basic: {\n        username: '',\n        password: ''\n      },\n      bearer: null,\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport postmanToBruno from '../../../src/postman/postman-to-bruno';\nimport { invalidVariableCharacterRegex } from '../../../src/constants';\n\ndescribe('postman-collection', () => {\n  it('should correctly import a valid Postman collection file', async () => {\n    const brunoCollection = await postmanToBruno(postmanCollection);\n    expect(brunoCollection).toMatchObject(expectedOutput);\n  });\n\n  it('should replace invalid variable characters with underscores', () => {\n    const variables = [\n      { key: 'validKey', value: 'value1' },\n      { key: 'invalid key', value: 'value2' },\n      { key: 'another@invalid#key$', value: 'value3' }\n    ];\n\n    const processedVariables = variables.map((v) => ({\n      name: v.key.replace(invalidVariableCharacterRegex, '_'),\n      value: v.value\n    }));\n\n    expect(processedVariables).toEqual([\n      { name: 'validKey', value: 'value1' },\n      { name: 'invalid_key', value: 'value2' },\n      { name: 'another_invalid_key_', value: 'value3' }\n    ]);\n  });\n\n  it('should handle falsy values in collection variables', async () => {\n    const collectionWithFalsyVars = {\n      info: {\n        _postman_id: '7f91bbd8-cb97-41ac-8d0b-e1fcd8bb4ce9',\n        name: 'collection with falsy vars',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      variable: [\n        {\n          type: 'string'\n        },\n        {\n          key: '',\n          type: 'string'\n        },\n        {\n          value: '',\n          type: 'string'\n        },\n        {\n          key: '',\n          value: '',\n          type: 'string'\n        }\n      ],\n      item: []\n    };\n\n    const brunoCollection = await postmanToBruno(collectionWithFalsyVars);\n\n    expect(brunoCollection.root.request.vars.req).toEqual([\n      {\n        uid: 'mockeduuidvalue123456',\n        name: '',\n        value: '',\n        enabled: true\n      },\n      {\n        uid: 'mockeduuidvalue123456',\n        name: '',\n        value: '',\n        enabled: true\n      },\n      {\n        uid: 'mockeduuidvalue123456',\n        name: '',\n        value: '',\n        enabled: true\n      }\n    ]);\n  });\n\n  it('should successfully translate a URL path array with no empty elements', async () => {\n    const collectionWithFalsyVars = {\n      info: {\n        _postman_id: '7f91bbd8-cb97-41ac-8d0b-e1fcd8bb4ce9',\n        name: 'collection with falsy vars',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      variable: [\n        {\n          type: 'string'\n        },\n        {\n          key: '',\n          type: 'string'\n        },\n        {\n          value: '',\n          type: 'string'\n        },\n        {\n          key: '',\n          value: '',\n          type: 'string'\n        }\n      ],\n      item: [\n        {\n          name: 'Request with all settings',\n          protocolProfileBehavior: {\n            maxRedirects: 10,\n            followRedirects: false,\n            disableUrlEncoding: true\n          },\n          request: {\n            method: 'GET',\n            header: [],\n            url: {\n              protocol: 'https',\n              host: ['httpbin', 'org'],\n              path: ['api', 'v1', 'resource']\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collectionWithFalsyVars);\n\n    expect(brunoCollection.items.map((item) => item.request.url)).toEqual([\n      'https://httpbin.org/api/v1/resource'\n    ]);\n  });\n\n  it('should not mutate a URL path with an empty element representing a trailing slash', async () => {\n    const collectionWithFalsyVars = {\n      info: {\n        _postman_id: '7f91bbd8-cb97-41ac-8d0b-e1fcd8bb4ce9',\n        name: 'collection with falsy vars',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      variable: [\n        {\n          type: 'string'\n        },\n        {\n          key: '',\n          type: 'string'\n        },\n        {\n          value: '',\n          type: 'string'\n        },\n        {\n          key: '',\n          value: '',\n          type: 'string'\n        }\n      ],\n      item: [\n        {\n          name: 'Request with all settings',\n          protocolProfileBehavior: {\n            maxRedirects: 10,\n            followRedirects: false,\n            disableUrlEncoding: true\n          },\n          request: {\n            method: 'GET',\n            header: [],\n            url: {\n              protocol: 'https',\n              host: ['httpbin', 'org'],\n              path: ['api', 'v1', 'resource', '']\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collectionWithFalsyVars);\n\n    expect(brunoCollection.items.map((item) => item.request.url)).toEqual([\n      'https://httpbin.org/api/v1/resource/'\n    ]);\n  });\n\n  it('should not mutate a URL path with an empty element representing a trailing slash', async () => {\n    const collectionWithFalsyVars = {\n      info: {\n        _postman_id: '7f91bbd8-cb97-41ac-8d0b-e1fcd8bb4ce9',\n        name: 'collection with falsy vars',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      variable: [\n        {\n          type: 'string'\n        },\n        {\n          key: '',\n          type: 'string'\n        },\n        {\n          value: '',\n          type: 'string'\n        },\n        {\n          key: '',\n          value: '',\n          type: 'string'\n        }\n      ],\n      item: [\n        {\n          name: 'Request with all settings',\n          protocolProfileBehavior: {\n            maxRedirects: 10,\n            followRedirects: false,\n            disableUrlEncoding: true\n          },\n          request: {\n            method: 'GET',\n            header: [],\n            url: {\n              protocol: 'https',\n              host: ['httpbin', 'org'],\n              path: ['api', '', 'resource']\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collectionWithFalsyVars);\n\n    expect(brunoCollection.items.map((item) => item.request.url)).toEqual([\n      'https://httpbin.org/api//resource'\n    ]);\n  });\n\n  it('should convert non-string variable values to strings', async () => {\n    const collectionWithNonStringVars = {\n      info: {\n        name: 'Non-String Variable Demo',\n        _postman_id: 'abcd1234-5678-90ef-ghij-1234567890ab',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      variable: [\n        { key: 'timeout', value: 5000 },\n        { key: 'enabled', value: true },\n        { key: 'user', value: { id: 1, name: 'Alice' } }\n      ],\n      item: [\n        {\n          name: 'Sample Request',\n          request: {\n            method: 'GET',\n            url: {\n              raw: 'https://postman-echo.com/get',\n              protocol: 'https',\n              host: ['postman-echo', 'com'],\n              path: ['get']\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collectionWithNonStringVars);\n    const vars = brunoCollection.root.request.vars.req;\n\n    expect(vars).toHaveLength(3);\n    expect(vars[0]).toMatchObject({ name: 'timeout', value: '5000' });\n    expect(vars[1]).toMatchObject({ name: 'enabled', value: 'true' });\n    expect(vars[2]).toMatchObject({ name: 'user', value: '{\"id\":1,\"name\":\"Alice\"}' });\n  });\n\n  it('should handle empty variables', async () => {\n    const collectionWithEmptyVars = {\n      info: {\n        _postman_id: '7f91bbd8-cb97-41ac-8d0b-e1fcd8bb4ce9',\n        name: 'collection with falsy vars',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      variable: [],\n      item: []\n    };\n\n    const brunoCollection = await postmanToBruno(collectionWithEmptyVars);\n    expect(brunoCollection.root.request.vars.req).toEqual([]);\n  });\n\n  it('should correctly import protocolProfileBehavior settings from Postman requests', async () => {\n    const collectionWithSettings = {\n      info: {\n        _postman_id: 'test-settings-id',\n        name: 'Collection with Settings',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'Request with all settings',\n          protocolProfileBehavior: {\n            maxRedirects: 10,\n            followRedirects: false,\n            disableUrlEncoding: true\n          },\n          request: {\n            method: 'GET',\n            header: [],\n            url: {\n              raw: 'https://echo.usebruno.com/get',\n              protocol: 'https',\n              host: ['echo', 'usebruno', 'com'],\n              path: ['get']\n            }\n          }\n        },\n        {\n          name: 'Request with partial settings',\n          protocolProfileBehavior: {\n            followRedirects: true\n          },\n          request: {\n            method: 'POST',\n            header: [],\n            url: {\n              raw: 'https://echo.usebruno.com/post',\n              protocol: 'https',\n              host: ['echo', 'usebruno', 'com'],\n              path: ['post']\n            }\n          }\n        },\n        {\n          name: 'Request without settings',\n          request: {\n            method: 'PUT',\n            header: [],\n            url: {\n              raw: 'https://echo.usebruno.com/put',\n              protocol: 'https',\n              host: ['echo', 'usebruno', 'com'],\n              path: ['put']\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collectionWithSettings);\n\n    // Test request with all settings\n    const requestWithAllSettings = brunoCollection.items[0];\n    expect(requestWithAllSettings.settings).toEqual({\n      encodeUrl: false,\n      followRedirects: false,\n      maxRedirects: 10\n    });\n\n    // Test request with partial settings\n    const requestWithPartialSettings = brunoCollection.items[1];\n    expect(requestWithPartialSettings.settings).toEqual({\n      encodeUrl: true,\n      followRedirects: true\n    });\n\n    // Test request without settings\n    const requestWithoutSettings = brunoCollection.items[2];\n    expect(requestWithoutSettings.settings).toEqual({\n      encodeUrl: true\n    });\n  });\n\n  it('should handle collection with auth object having undefined type', async () => {\n    const collectionWithUndefinedAuthType = {\n      info: {\n        _postman_id: '7f91bbd8-cb97-41ac-8d0b-e1fcd8bb4ce9',\n        name: 'collection with undefined auth type',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      auth: {\n        basic: [\n          { key: 'username', value: 'testuser', type: 'string' },\n          { key: 'password', value: 'testpass', type: 'string' }\n        ]\n      },\n      item: [\n        {\n          name: 'request',\n          request: {\n            method: 'GET',\n            header: [],\n            url: {\n              raw: 'https://api.example.com/test',\n              protocol: 'https',\n              host: ['api', 'example', 'com'],\n              path: ['test']\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collectionWithUndefinedAuthType);\n\n    // Collection level auth should default to 'none'\n    expect(brunoCollection.root.request.auth).toEqual({\n      mode: 'none',\n      basic: null,\n      bearer: null,\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n\n    // Request should inherit auth mode\n    expect(brunoCollection.items[0].request.auth).toEqual({\n      mode: 'inherit',\n      basic: null,\n      bearer: null,\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n  });\n\n  it('should handle collection with auth object having null type', async () => {\n    const collectionWithNullAuthType = {\n      info: {\n        _postman_id: '7f91bbd8-cb97-41ac-8d0b-e1fcd8bb4ce9',\n        name: 'collection with null auth type',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      auth: {\n        type: null,\n        bearer: {\n          token: 'test-token'\n        }\n      },\n      item: [\n        {\n          name: 'request',\n          request: {\n            method: 'GET',\n            header: [],\n            url: {\n              raw: 'https://api.example.com/test',\n              protocol: 'https',\n              host: ['api', 'example', 'com'],\n              path: ['test']\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collectionWithNullAuthType);\n\n    // Collection level auth should default to 'none'\n    expect(brunoCollection.root.request.auth).toEqual({\n      mode: 'none',\n      basic: null,\n      bearer: null,\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n  });\n\n  it('should handle collection with auth object having unexpected type value', async () => {\n    const collectionWithUnexpectedAuthType = {\n      info: {\n        _postman_id: '7f91bbd8-cb97-41ac-8d0b-e1fcd8bb4ce9',\n        name: 'collection with unexpected auth type',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      auth: {\n        type: 'unexpected_auth_type',\n        basic: [\n          { key: 'username', value: 'testuser', type: 'string' },\n          { key: 'password', value: 'testpass', type: 'string' }\n        ]\n      },\n      item: [\n        {\n          name: 'request',\n          request: {\n            method: 'GET',\n            header: [],\n            url: {\n              raw: 'https://api.example.com/test',\n              protocol: 'https',\n              host: ['api', 'example', 'com'],\n              path: ['test']\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collectionWithUnexpectedAuthType);\n\n    // Collection level auth should default to 'none'\n    expect(brunoCollection.root.request.auth).toEqual({\n      mode: 'none',\n      basic: null,\n      bearer: null,\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n\n    // Request should inherit auth mode\n    expect(brunoCollection.items[0].request.auth).toEqual({\n      mode: 'inherit',\n      basic: null,\n      bearer: null,\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n  });\n\n  it('should handle request with auth object having undefined type', async () => {\n    const collectionWithRequestUndefinedAuthType = {\n      info: {\n        _postman_id: '7f91bbd8-cb97-41ac-8d0b-e1fcd8bb4ce9',\n        name: 'collection with request undefined auth type',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'request',\n          request: {\n            method: 'GET',\n            header: [],\n            url: {\n              raw: 'https://api.example.com/test',\n              protocol: 'https',\n              host: ['api', 'example', 'com'],\n              path: ['test']\n            },\n            auth: {\n              basic: [\n                { key: 'username', value: 'testuser', type: 'string' },\n                { key: 'password', value: 'testpass', type: 'string' }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collectionWithRequestUndefinedAuthType);\n\n    // Collection level auth should default to 'none'\n    expect(brunoCollection.root.request.auth).toEqual({\n      mode: 'none',\n      basic: null,\n      bearer: null,\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n\n    // Request auth should default to 'none'\n    expect(brunoCollection.items[0].request.auth).toEqual({\n      mode: 'none',\n      basic: null,\n      bearer: null,\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n  });\n\n  it('should handle folder with auth object having unexpected type', async () => {\n    const collectionWithFolderUnexpectedAuthType = {\n      info: {\n        _postman_id: '7f91bbd8-cb97-41ac-8d0b-e1fcd8bb4ce9',\n        name: 'collection with folder unexpected auth type',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'folder',\n          auth: {\n            type: 'unexpected_folder_auth_type',\n            bearer: {\n              token: 'folder-token'\n            }\n          },\n          item: [\n            {\n              name: 'request',\n              request: {\n                method: 'GET',\n                header: [],\n                url: {\n                  raw: 'https://api.example.com/test',\n                  protocol: 'https',\n                  host: ['api', 'example', 'com'],\n                  path: ['test']\n                }\n              }\n            }\n          ]\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collectionWithFolderUnexpectedAuthType);\n\n    // Folder auth should default to 'none'\n    expect(brunoCollection.items[0].root.request.auth).toEqual({\n      mode: 'none',\n      basic: null,\n      bearer: null,\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n\n    // Request should inherit auth mode\n    expect(brunoCollection.items[0].items[0].request.auth).toEqual({\n      mode: 'inherit',\n      basic: null,\n      bearer: null,\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n  });\n\n  it('should skip headers where both key and value are null, and coalesce partial nulls', async () => {\n    const collectionWithNullHeaders = {\n      info: {\n        _postman_id: 'test-null-headers',\n        name: 'collection with null headers',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'request with null headers',\n          request: {\n            method: 'GET',\n            header: [\n              { key: 'Content-Type', value: 'application/json' },\n              { key: null, value: null },\n              { key: null, value: 'somevalue' },\n              { key: 'X-Custom', value: null }\n            ],\n            url: { raw: 'https://example.com/api' }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collectionWithNullHeaders);\n    const headers = brunoCollection.items[0].request.headers;\n\n    expect(headers).toHaveLength(3);\n    expect(headers[0].name).toBe('Content-Type');\n    expect(headers[0].value).toBe('application/json');\n    expect(headers[1].name).toBe('');\n    expect(headers[1].value).toBe('somevalue');\n    expect(headers[2].name).toBe('X-Custom');\n    expect(headers[2].value).toBe('');\n  });\n\n  it('should skip urlencoded params where both key and value are null', async () => {\n    const collectionWithNullUrlencoded = {\n      info: {\n        _postman_id: 'test-null-urlencoded',\n        name: 'collection with null urlencoded',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'request with null urlencoded',\n          request: {\n            method: 'POST',\n            header: [],\n            url: { raw: 'https://example.com/api' },\n            body: {\n              mode: 'urlencoded',\n              urlencoded: [\n                { key: 'field1', value: 'value1' },\n                { key: null, value: null },\n                { key: null, value: 'partialvalue' },\n                { key: 'field2', value: null }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collectionWithNullUrlencoded);\n    const formUrlEncoded = brunoCollection.items[0].request.body.formUrlEncoded;\n\n    expect(formUrlEncoded).toHaveLength(3);\n    expect(formUrlEncoded[0].name).toBe('field1');\n    expect(formUrlEncoded[0].value).toBe('value1');\n    expect(formUrlEncoded[1].name).toBe('');\n    expect(formUrlEncoded[1].value).toBe('partialvalue');\n    expect(formUrlEncoded[2].name).toBe('field2');\n    expect(formUrlEncoded[2].value).toBe('');\n  });\n\n  it('should skip formdata params where both key and value are null', async () => {\n    const collectionWithNullFormdata = {\n      info: {\n        _postman_id: 'test-null-formdata',\n        name: 'collection with null formdata',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'request with null formdata',\n          request: {\n            method: 'POST',\n            header: [],\n            url: { raw: 'https://example.com/api' },\n            body: {\n              mode: 'formdata',\n              formdata: [\n                { key: 'field1', value: 'value1', type: 'text' },\n                { key: null, value: null, type: 'text' },\n                { key: 'field2', value: null, type: 'text' }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collectionWithNullFormdata);\n    const multipartForm = brunoCollection.items[0].request.body.multipartForm;\n\n    expect(multipartForm).toHaveLength(2);\n    expect(multipartForm[0].name).toBe('field1');\n    expect(multipartForm[0].value).toBe('value1');\n    expect(multipartForm[1].name).toBe('field2');\n    expect(multipartForm[1].value).toBe('');\n  });\n\n  it('should skip query params where both key and value are null', async () => {\n    const collectionWithNullQueryParams = {\n      info: {\n        _postman_id: 'test-null-query-params',\n        name: 'collection with null query params',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'request with null query params',\n          request: {\n            method: 'GET',\n            header: [],\n            url: {\n              raw: 'https://example.com/api?search=test',\n              protocol: 'https',\n              host: ['example', 'com'],\n              path: ['api'],\n              query: [\n                { key: 'search', value: 'test' },\n                { key: null, value: null },\n                { key: null, value: 'somevalue' },\n                { key: 'emptyval', value: null }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collectionWithNullQueryParams);\n    const params = brunoCollection.items[0].request.params;\n\n    // Fully-null entry should be skipped\n    expect(params).toHaveLength(3);\n\n    // Normal param preserved as-is\n    expect(params[0].name).toBe('search');\n    expect(params[0].value).toBe('test');\n    expect(params[0].type).toBe('query');\n\n    // Null key normalized to empty string, value preserved\n    expect(params[1].name).toBe('');\n    expect(params[1].value).toBe('somevalue');\n    expect(params[1].type).toBe('query');\n\n    // Key preserved, null value normalized to empty string\n    expect(params[2].name).toBe('emptyval');\n    expect(params[2].value).toBe('');\n    expect(params[2].type).toBe('query');\n  });\n});\n\n// Simple Collection (postman)\n// ├── folder\n// │   └── request (GET)\n// └── request (GET)\n\nconst postmanCollection = {\n  info: {\n    _postman_id: '7f91bbd8-cb97-41ac-8d0b-e1fcd8bb4ce9',\n    name: 'simple collection',\n    schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',\n    _exporter_id: '21992467',\n    _collection_link: 'https://random-user-007.postman.co/workspace/testing~7523f559-3d5f-4c30-8315-3cb3c3ff98b7/collection/21992467-7f91bbd8-cb97-41ac-8d0b-e1fcd8bb4ce9?action=share&source=collection_link&creator=007'\n  },\n  item: [\n    {\n      name: 'folder',\n      item: [\n        {\n          name: 'request',\n          request: {\n            method: 'GET',\n            header: [],\n            url: {\n              raw: 'https://usebruno.com',\n              protocol: 'https',\n              host: [\n                'usebruno',\n                'com'\n              ]\n            }\n          },\n          response: []\n        }\n      ]\n    },\n    {\n      name: 'request',\n      request: {\n        method: 'GET',\n        header: [],\n        url: {\n          raw: 'https://usebruno.com',\n          protocol: 'https',\n          host: [\n            'usebruno',\n            'com'\n          ]\n        }\n      },\n      response: []\n    }\n  ]\n};\n\n// Simple Collection (bruno)\n// ├── folder\n// │   └── request (GET)\n// └── request (GET)\n\ndescribe('postman-collection formdata import', () => {\n  it('should import formdata with type: file correctly', async () => {\n    const collectionWithFileFormdata = {\n      info: {\n        _postman_id: 'test-id',\n        name: 'collection with file formdata',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'request with file',\n          request: {\n            method: 'POST',\n            header: [],\n            url: { raw: 'https://example.com/upload' },\n            body: {\n              mode: 'formdata',\n              formdata: [\n                {\n                  key: 'myFile',\n                  type: 'file',\n                  src: ['/path/to/file1.txt', '/path/to/file2.txt'],\n                  disabled: false\n                }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collectionWithFileFormdata);\n    const multipartForm = brunoCollection.items[0].request.body.multipartForm;\n\n    expect(multipartForm).toHaveLength(1);\n    expect(multipartForm[0].type).toBe('file');\n    expect(multipartForm[0].name).toBe('myFile');\n    expect(multipartForm[0].value).toEqual(['/path/to/file1.txt', '/path/to/file2.txt']);\n    expect(multipartForm[0].enabled).toBe(true);\n  });\n\n  it('should import formdata with type: default and src field as file', async () => {\n    const collectionWithDefaultTypeAndSrc = {\n      info: {\n        _postman_id: 'test-id',\n        name: 'collection with default type formdata',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'request with default type',\n          request: {\n            method: 'POST',\n            header: [],\n            url: { raw: 'https://example.com/upload' },\n            body: {\n              mode: 'formdata',\n              formdata: [\n                {\n                  key: 'myFile',\n                  type: 'default',\n                  src: '/path/to/file.txt',\n                  disabled: false\n                }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collectionWithDefaultTypeAndSrc);\n    const multipartForm = brunoCollection.items[0].request.body.multipartForm;\n\n    expect(multipartForm).toHaveLength(1);\n    expect(multipartForm[0].type).toBe('file');\n    expect(multipartForm[0].name).toBe('myFile');\n    expect(multipartForm[0].value).toEqual(['/path/to/file.txt']);\n    expect(multipartForm[0].enabled).toBe(true);\n  });\n\n  it('should import formdata with type: default and value array as text', async () => {\n    const collectionWithDefaultTypeAndValueArray = {\n      info: {\n        _postman_id: 'test-id',\n        name: 'collection with default type and value array',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'request with default type',\n          request: {\n            method: 'POST',\n            header: [],\n            url: { raw: 'https://example.com/upload' },\n            body: {\n              mode: 'formdata',\n              formdata: [\n                {\n                  key: 'myField',\n                  type: 'default',\n                  value: ['some', 'text'],\n                  disabled: false\n                }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collectionWithDefaultTypeAndValueArray);\n    const multipartForm = brunoCollection.items[0].request.body.multipartForm;\n\n    expect(multipartForm).toHaveLength(1);\n    expect(multipartForm[0].type).toBe('text');\n    expect(multipartForm[0].name).toBe('myField');\n    expect(multipartForm[0].value).toBe('sometext');\n    expect(multipartForm[0].enabled).toBe(true);\n  });\n\n  it('should preserve contentType when importing formdata', async () => {\n    const collectionWithContentType = {\n      info: {\n        _postman_id: 'test-id',\n        name: 'collection with contentType',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'request with contentType',\n          request: {\n            method: 'POST',\n            header: [],\n            url: { raw: 'https://example.com/upload' },\n            body: {\n              mode: 'formdata',\n              formdata: [\n                {\n                  key: 'myFile',\n                  type: 'file',\n                  src: '/path/to/file.json',\n                  contentType: 'application/json',\n                  disabled: false\n                }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collectionWithContentType);\n    const multipartForm = brunoCollection.items[0].request.body.multipartForm;\n\n    expect(multipartForm).toHaveLength(1);\n    expect(multipartForm[0].type).toBe('file');\n    expect(multipartForm[0].contentType).toBe('application/json');\n  });\n\n  it('should handle mixed file and text fields in formdata', async () => {\n    const collectionWithMixedFormdata = {\n      info: {\n        _postman_id: 'test-id',\n        name: 'collection with mixed formdata',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'request with mixed fields',\n          request: {\n            method: 'POST',\n            header: [],\n            url: { raw: 'https://example.com/upload' },\n            body: {\n              mode: 'formdata',\n              formdata: [\n                {\n                  key: 'textField',\n                  type: 'text',\n                  value: 'hello world',\n                  disabled: false\n                },\n                {\n                  key: 'fileField',\n                  type: 'file',\n                  src: '/path/to/file.txt',\n                  disabled: true\n                }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collectionWithMixedFormdata);\n    const multipartForm = brunoCollection.items[0].request.body.multipartForm;\n\n    expect(multipartForm).toHaveLength(2);\n    expect(multipartForm[0].type).toBe('text');\n    expect(multipartForm[0].value).toBe('hello world');\n    expect(multipartForm[0].enabled).toBe(true);\n    expect(multipartForm[1].type).toBe('file');\n    expect(multipartForm[1].value).toEqual(['/path/to/file.txt']);\n    expect(multipartForm[1].enabled).toBe(false);\n  });\n});\n\nconst expectedOutput = {\n  name: 'simple collection',\n  uid: 'mockeduuidvalue123456',\n  version: '1',\n  items: [\n    {\n      uid: 'mockeduuidvalue123456',\n      name: 'folder',\n      type: 'folder',\n      seq: 1,\n      items: [\n        {\n          uid: 'mockeduuidvalue123456',\n          name: 'request',\n          type: 'http-request',\n          seq: 1,\n          request: {\n            url: 'https://usebruno.com',\n            method: 'GET',\n            auth: {\n              mode: 'inherit',\n              basic: null,\n              bearer: null,\n              awsv4: null,\n              apikey: null,\n              oauth2: null,\n              digest: null\n            },\n            headers: [],\n            params: [],\n            body: {\n              mode: 'none',\n              json: null,\n              text: null,\n              xml: null,\n              formUrlEncoded: [],\n              multipartForm: []\n            },\n            docs: ''\n          }\n        }\n      ],\n      root: {\n        docs: '',\n        meta: {\n          name: 'folder'\n        },\n        request: {\n          auth: {\n            mode: 'inherit',\n            basic: null,\n            bearer: null,\n            awsv4: null,\n            apikey: null,\n            oauth2: null,\n            digest: null\n          },\n          headers: [],\n          script: {},\n          tests: '',\n          vars: {}\n        }\n      }\n    },\n    {\n      uid: 'mockeduuidvalue123456',\n      name: 'request',\n      type: 'http-request',\n      seq: 2,\n      request: {\n        url: 'https://usebruno.com',\n        method: 'GET',\n        auth: {\n          mode: 'inherit',\n          basic: null,\n          bearer: null,\n          awsv4: null,\n          apikey: null,\n          oauth2: null,\n          digest: null\n        },\n        headers: [],\n        params: [],\n        body: {\n          mode: 'none',\n          json: null,\n          text: null,\n          xml: null,\n          formUrlEncoded: [],\n          multipartForm: []\n        },\n        docs: ''\n      }\n    }\n  ],\n  environments: [],\n  root: {\n    docs: '',\n    meta: {\n      name: 'simple collection'\n    },\n    request: {\n      auth: {\n        mode: 'none',\n        basic: null,\n        bearer: null,\n        awsv4: null,\n        apikey: null,\n        oauth2: null,\n        digest: null\n      },\n      headers: [],\n      script: {},\n      tests: '',\n      vars: {}\n    }\n  }\n};\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-to-bruno/postman-translations/postman-request.spec.js",
    "content": "const { default: postmanTranslation } = require('../../../../src/postman/postman-translations');\n\ndescribe('postmanTranslations - request commands', () => {\n  test('should handle request commands', () => {\n    const inputScript = `\n      const requestUrl = pm.request.url;\n      const requestMethod = pm.request.method;\n      const requestHeaders = pm.request.headers;\n      const requestBody = pm.request.body;\n      const requestName = pm.info.requestName;\n\n      pm.test('Request method is POST', function() {\n        pm.expect(pm.request.method).to.equal('POST');\n      });\n    `;\n    const expectedOutput = `\n      const requestUrl = req.getUrl();\n      const requestMethod = req.getMethod();\n      const requestHeaders = req.getHeaders();\n      const requestBody = req.getBody();\n      const requestName = req.getName();\n\n      test('Request method is POST', function() {\n        expect(req.getMethod()).to.equal('POST');\n      });\n    `;\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should handle legacy request object without pm prefix', () => {\n    const inputScript = `\n      const url = request.url;\n      const method = request.method;\n      const body = request.body;\n      const name = request.name;\n    `;\n    const expectedOutput = `\n      const url = req.getUrl();\n      const method = req.getMethod();\n      const body = req.getBody();\n      const name = req.getName();\n    `;\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should handle pm.request.url helper methods', () => {\n    const inputScript = `\n      const host = pm.request.url.getHost();\n      const path = pm.request.url.getPath();\n      const queryString = pm.request.url.getQueryString();\n      const pathVariables = pm.request.url.variables;\n    `;\n    const expectedOutput = `\n      const host = req.getHost();\n      const path = req.getPath();\n      const queryString = req.getQueryString();\n      const pathVariables = req.getPathParams();\n    `;\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-to-bruno/postman-translations/postman-response.spec.js",
    "content": "const { default: postmanTranslation } = require('../../../../src/postman/postman-translations');\n\ndescribe('postmanTranslations - response commands', () => {\n  test('should handle response commands', () => {\n    const inputScript = `\n      const responseTime = pm.response.responseTime;\n      const responseCode = pm.response.code;\n      const responseText = pm.response.text();\n      const responseJson = pm.response.json();\n      const responseStatus = pm.response.status;\n      const responseHeaders = pm.response.headers;\n      const responseSize = pm.response.size();\n      const responseSizeBody = pm.response.size().body;\n      const responseSizeHeader = pm.response.size().header;\n      const responseSizeTotal = pm.response.size().total;\n      const responseSizeBody2 = pm.response.responseSize;\n\n      pm.test('Status code is 200', function() {\n        pm.response.to.have.status(200);\n      });\n    `;\n    const expectedOutput = `\n      const responseTime = res.getResponseTime();\n      const responseCode = res.getStatus();\n      const responseText = JSON.stringify(res.getBody());\n      const responseJson = res.getBody();\n      const responseStatus = res.statusText;\n      const responseHeaders = res.getHeaders();\n      const responseSize = res.getSize();\n      const responseSizeBody = res.getSize().body;\n      const responseSizeHeader = res.getSize().header;\n      const responseSizeTotal = res.getSize().total;\n      const responseSizeBody2 = res.getSize().body;\n\n      test('Status code is 200', function() {\n        expect(res.getStatus()).to.equal(200);\n      });\n    `;\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-to-bruno/process-auth.spec.js",
    "content": "const { processAuth } = require('../../../src/postman/postman-to-bruno');\n\ndescribe('processAuth', () => {\n  let requestObject;\n\n  beforeEach(() => {\n    requestObject = {\n      auth: {\n        mode: 'none',\n        basic: null,\n        bearer: null,\n        awsv4: null,\n        apikey: null,\n        oauth2: null,\n        digest: null\n      }\n    };\n  });\n\n  it('should handle no auth', () => {\n    processAuth(null, requestObject);\n    expect(requestObject.auth.mode).toBe('none');\n  });\n\n  it('should handle noauth type', () => {\n    processAuth({ type: 'noauth' }, requestObject);\n    expect(requestObject.auth.mode).toBe('none');\n  });\n\n  it('should handle basic auth', () => {\n    const auth = {\n      type: 'basic',\n      basic: [\n        { key: 'username', value: 'testuser', type: 'string' },\n        { key: 'password', value: 'testpass', type: 'string' }\n      ]\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('basic');\n    expect(requestObject.auth.basic).toEqual({\n      username: 'testuser',\n      password: 'testpass'\n    });\n  });\n\n  it('should handle basic auth with missing values', () => {\n    const auth = {\n      type: 'basic',\n      basic: {}\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('basic');\n    expect(requestObject.auth.basic).toEqual({\n      username: '',\n      password: ''\n    });\n  });\n\n  it('should handle basic auth with missing basic key', () => {\n    const auth = {\n      type: 'basic'\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('basic');\n    expect(requestObject.auth.basic).toEqual({\n      username: '',\n      password: ''\n    });\n  });\n\n  it('should handle bearer auth', () => {\n    const auth = {\n      type: 'bearer',\n      bearer: {\n        token: 'test-token'\n      }\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('bearer');\n    expect(requestObject.auth.bearer).toEqual({\n      token: 'test-token'\n    });\n  });\n\n  it('should handle bearer auth with missing values', () => {\n    const auth = {\n      type: 'bearer',\n      bearer: {}\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('bearer');\n    expect(requestObject.auth.bearer).toEqual({\n      token: ''\n    });\n  });\n\n  it('should handle bearer auth with missing bearer key', () => {\n    const auth = {\n      type: 'bearer'\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('bearer');\n    expect(requestObject.auth.bearer).toEqual({\n      token: ''\n    });\n  });\n\n  it('should handle awsv4 auth', () => {\n    const auth = {\n      type: 'awsv4',\n      awsv4: {\n        accessKey: 'test-access-key',\n        secretKey: 'test-secret-key',\n        sessionToken: 'test-session-token',\n        service: 'test-service',\n        region: 'test-region'\n      }\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('awsv4');\n    expect(requestObject.auth.awsv4).toEqual({\n      accessKeyId: 'test-access-key',\n      secretAccessKey: 'test-secret-key',\n      sessionToken: 'test-session-token',\n      service: 'test-service',\n      region: 'test-region',\n      profileName: ''\n    });\n  });\n\n  it('should handle awsv4 auth with missing values', () => {\n    const auth = {\n      type: 'awsv4',\n      awsv4: {}\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('awsv4');\n    expect(requestObject.auth.awsv4).toEqual({\n      accessKeyId: '',\n      secretAccessKey: '',\n      sessionToken: '',\n      service: '',\n      region: '',\n      profileName: ''\n    });\n  });\n\n  it('should handle awsv4 auth with missing awsv4 key', () => {\n    const auth = {\n      type: 'awsv4'\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('awsv4');\n    expect(requestObject.auth.awsv4).toEqual({\n      accessKeyId: '',\n      secretAccessKey: '',\n      sessionToken: '',\n      service: '',\n      region: '',\n      profileName: ''\n    });\n  });\n\n  it('should handle apikey auth', () => {\n    const auth = {\n      type: 'apikey',\n      apikey: {\n        key: 'test-key',\n        value: 'test-value'\n      }\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('apikey');\n    expect(requestObject.auth.apikey).toEqual({\n      key: 'test-key',\n      value: 'test-value',\n      placement: 'header'\n    });\n  });\n\n  it('should handle apikey auth with missing values', () => {\n    const auth = {\n      type: 'apikey',\n      apikey: {}\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('apikey');\n    expect(requestObject.auth.apikey).toEqual({\n      key: '',\n      value: '',\n      placement: 'header'\n    });\n  });\n\n  it('should handle apikey auth with missing apikey key', () => {\n    const auth = {\n      type: 'apikey'\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('apikey');\n    expect(requestObject.auth.apikey).toEqual({\n      key: '',\n      value: '',\n      placement: 'header'\n    });\n  });\n\n  it('should handle digest auth', () => {\n    const auth = {\n      type: 'digest',\n      digest: {\n        username: 'testuser',\n        password: 'testpass'\n      }\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('digest');\n    expect(requestObject.auth.digest).toEqual({\n      username: 'testuser',\n      password: 'testpass'\n    });\n  });\n\n  it('should handle digest auth with missing values', () => {\n    const auth = {\n      type: 'digest',\n      digest: {}\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('digest');\n    expect(requestObject.auth.digest).toEqual({\n      username: '',\n      password: ''\n    });\n  });\n\n  it('should handle digest auth with missing digest key', () => {\n    const auth = {\n      type: 'digest'\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('digest');\n    expect(requestObject.auth.digest).toEqual({\n      username: '',\n      password: ''\n    });\n  });\n\n  it('should handle oauth2 auth with authorization_code grant type', () => {\n    const auth = {\n      type: 'oauth2',\n      oauth2: {\n        grant_type: 'authorization_code',\n        authUrl: 'https://auth.example.com',\n        redirect_uri: 'https://callback.example.com',\n        accessTokenUrl: 'https://token.example.com',\n        refreshTokenUrl: 'https://refresh.example.com',\n        clientId: 'test-client-id',\n        clientSecret: 'test-client-secret',\n        scope: 'test-scope',\n        state: 'test-state',\n        addTokenTo: 'header',\n        client_authentication: 'body'\n      }\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('oauth2');\n    expect(requestObject.auth.oauth2).toEqual({\n      grantType: 'authorization_code',\n      authorizationUrl: 'https://auth.example.com',\n      callbackUrl: 'https://callback.example.com',\n      accessTokenUrl: 'https://token.example.com',\n      refreshTokenUrl: 'https://refresh.example.com',\n      clientId: 'test-client-id',\n      clientSecret: 'test-client-secret',\n      scope: 'test-scope',\n      state: 'test-state',\n      pkce: false,\n      tokenPlacement: 'header',\n      credentialsPlacement: 'body'\n    });\n  });\n\n  it('should handle oauth2 auth with password_credentials grant type', () => {\n    const auth = {\n      type: 'oauth2',\n      oauth2: {\n        grant_type: 'password_credentials',\n        accessTokenUrl: 'https://token.example.com',\n        refreshTokenUrl: 'https://refresh.example.com',\n        username: 'testuser',\n        password: 'testpass',\n        clientId: 'test-client-id',\n        clientSecret: 'test-client-secret',\n        scope: 'test-scope',\n        state: 'test-state',\n        addTokenTo: 'header',\n        client_authentication: 'body'\n      }\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('oauth2');\n    expect(requestObject.auth.oauth2).toEqual({\n      grantType: 'password',\n      accessTokenUrl: 'https://token.example.com',\n      refreshTokenUrl: 'https://refresh.example.com',\n      username: 'testuser',\n      password: 'testpass',\n      clientId: 'test-client-id',\n      clientSecret: 'test-client-secret',\n      scope: 'test-scope',\n      state: 'test-state',\n      tokenPlacement: 'header',\n      credentialsPlacement: 'body'\n    });\n  });\n\n  it('should handle oauth2 auth with client_credentials grant type', () => {\n    const auth = {\n      type: 'oauth2',\n      oauth2: {\n        grant_type: 'client_credentials',\n        accessTokenUrl: 'https://token.example.com',\n        refreshTokenUrl: 'https://refresh.example.com',\n        clientId: 'test-client-id',\n        clientSecret: 'test-client-secret',\n        scope: 'test-scope',\n        state: 'test-state',\n        addTokenTo: 'header',\n        client_authentication: 'body'\n      }\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('oauth2');\n    expect(requestObject.auth.oauth2).toEqual({\n      grantType: 'client_credentials',\n      accessTokenUrl: 'https://token.example.com',\n      refreshTokenUrl: 'https://refresh.example.com',\n      clientId: 'test-client-id',\n      clientSecret: 'test-client-secret',\n      scope: 'test-scope',\n      state: 'test-state',\n      tokenPlacement: 'header',\n      credentialsPlacement: 'body'\n    });\n  });\n\n  it('should handle oauth2 auth with missing values', () => {\n    const auth = {\n      type: 'oauth2',\n      oauth2: {}\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('oauth2');\n    expect(requestObject.auth.oauth2).toEqual({\n      grantType: 'client_credentials',\n      accessTokenUrl: '',\n      refreshTokenUrl: '',\n      clientId: '',\n      clientSecret: '',\n      scope: '',\n      state: '',\n      tokenPlacement: 'url',\n      credentialsPlacement: 'basic_auth_header'\n    });\n  });\n\n  it('should handle oauth2 auth with missing oauth2 key', () => {\n    const auth = {\n      type: 'oauth2'\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('oauth2');\n    expect(requestObject.auth.oauth2).toEqual({\n      grantType: 'client_credentials',\n      accessTokenUrl: '',\n      refreshTokenUrl: '',\n      clientId: '',\n      clientSecret: '',\n      scope: '',\n      state: '',\n      tokenPlacement: 'url',\n      credentialsPlacement: 'basic_auth_header'\n    });\n  });\n\n  it('should handle oauth2 auth with authorization_code_with_pkce grant type', () => {\n    const auth = {\n      type: 'oauth2',\n      oauth2: {\n        grant_type: 'authorization_code_with_pkce',\n        authUrl: 'https://auth.example.com',\n        redirect_uri: 'https://callback.example.com',\n        accessTokenUrl: 'https://token.example.com',\n        refreshTokenUrl: 'https://refresh.example.com',\n        clientId: 'test-client-id',\n        clientSecret: 'test-client-secret',\n        scope: 'test-scope',\n        state: 'test-state',\n        addTokenTo: 'header',\n        client_authentication: 'body'\n      }\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('oauth2');\n    expect(requestObject.auth.oauth2).toEqual({\n      grantType: 'authorization_code',\n      authorizationUrl: 'https://auth.example.com',\n      callbackUrl: 'https://callback.example.com',\n      accessTokenUrl: 'https://token.example.com',\n      refreshTokenUrl: 'https://refresh.example.com',\n      clientId: 'test-client-id',\n      clientSecret: 'test-client-secret',\n      scope: 'test-scope',\n      state: 'test-state',\n      pkce: true,\n      tokenPlacement: 'header',\n      credentialsPlacement: 'body'\n    });\n  });\n\n  it('should handle auth object with undefined type', () => {\n    const auth = {};\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('none');\n    expect(requestObject.auth.basic).toBe(null);\n    expect(requestObject.auth.bearer).toBe(null);\n    expect(requestObject.auth.awsv4).toBe(null);\n    expect(requestObject.auth.apikey).toBe(null);\n    expect(requestObject.auth.oauth2).toBe(null);\n    expect(requestObject.auth.digest).toBe(null);\n  });\n\n  it('should handle type as null and auth as null', () => {\n    const auth = {\n      type: null,\n      auth: null\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('none');\n    expect(requestObject.auth.basic).toBe(null);\n    expect(requestObject.auth.bearer).toBe(null);\n    expect(requestObject.auth.awsv4).toBe(null);\n    expect(requestObject.auth.apikey).toBe(null);\n    expect(requestObject.auth.oauth2).toBe(null);\n    expect(requestObject.auth.digest).toBe(null);\n  });\n\n  it('should handle auth object with undefined type, but basic auth', () => {\n    const auth = {\n      basic: [\n        { key: 'username', value: 'testuser', type: 'string' },\n        { key: 'password', value: 'testpass', type: 'string' }\n      ]\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('none');\n    expect(requestObject.auth.basic).toBe(null);\n    expect(requestObject.auth.bearer).toBe(null);\n    expect(requestObject.auth.awsv4).toBe(null);\n    expect(requestObject.auth.apikey).toBe(null);\n    expect(requestObject.auth.oauth2).toBe(null);\n    expect(requestObject.auth.digest).toBe(null);\n  });\n\n  it('should handle auth object with null type', () => {\n    const auth = {\n      type: null\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('none');\n    expect(requestObject.auth.basic).toBe(null);\n    expect(requestObject.auth.bearer).toBe(null);\n    expect(requestObject.auth.awsv4).toBe(null);\n    expect(requestObject.auth.apikey).toBe(null);\n    expect(requestObject.auth.oauth2).toBe(null);\n    expect(requestObject.auth.digest).toBe(null);\n  });\n\n  it('should handle auth object with empty string type', () => {\n    const auth = {\n      type: null,\n      basic: {\n        username: 'testuser',\n        password: 'testpass'\n      }\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('none');\n    expect(requestObject.auth.basic).toBe(null);\n    expect(requestObject.auth.bearer).toBe(null);\n    expect(requestObject.auth.awsv4).toBe(null);\n    expect(requestObject.auth.apikey).toBe(null);\n    expect(requestObject.auth.oauth2).toBe(null);\n    expect(requestObject.auth.digest).toBe(null);\n  });\n\n  it('should handle auth object with boolean type value', () => {\n    const auth = {\n      type: 'unknown_auth_type',\n      unknown_auth_type: {\n        accessKey: 'test-access-key',\n        secretKey: 'test-secret-key'\n      }\n    };\n    processAuth(auth, requestObject);\n    expect(requestObject.auth.mode).toBe('none');\n    expect(requestObject.auth.basic).toBe(null);\n    expect(requestObject.auth.bearer).toBe(null);\n    expect(requestObject.auth.awsv4).toBe(null);\n    expect(requestObject.auth.apikey).toBe(null);\n    expect(requestObject.auth.oauth2).toBe(null);\n    expect(requestObject.auth.digest).toBe(null);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-to-bruno/request-auth.spec.js",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport postmanToBruno from '../../../src/postman/postman-to-bruno';\n\ndescribe('Request Authentication', () => {\n  it('should handle basic auth at request level', async () => {\n    const postmanCollection = {\n      info: {\n        name: 'Request Auth Collection',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'Basic Auth Request',\n          request: {\n            method: 'GET',\n            url: 'https://api.example.com/test',\n            auth: {\n              type: 'basic',\n              basic: [\n                { key: 'username', value: 'requestuser' },\n                { key: 'password', value: 'requestpass' }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const result = await postmanToBruno(postmanCollection);\n\n    expect(result.items[0].request.auth).toEqual({\n      mode: 'basic',\n      basic: {\n        username: 'requestuser',\n        password: 'requestpass'\n      },\n      bearer: null,\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n  });\n\n  it('should inherit folder auth when request has no auth', async () => {\n    const postmanCollection = {\n      info: {\n        name: 'Inherit Request Auth Collection',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'Auth Folder',\n          auth: {\n            type: 'bearer',\n            bearer: [{ key: 'token', value: 'foldertoken' }]\n          },\n          item: [\n            {\n              name: 'Inherit Auth Request',\n              request: {\n                method: 'GET',\n                url: 'https://api.example.com/test'\n              }\n            }\n          ]\n        }\n      ]\n    };\n\n    const result = await postmanToBruno(postmanCollection);\n\n    expect(result.items[0].items[0].request.auth).toEqual({\n      mode: 'inherit',\n      basic: null,\n      bearer: null,\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n  });\n\n  it('should handle \"Inherit Auth\" for request (auth property absent, inherits from folder)', async () => {\n    const postmanCollection = {\n      info: {\n        name: 'Request Inherit Auth from Folder',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'Auth Folder',\n          auth: { // Folder has auth\n            type: 'bearer',\n            bearer: [{ key: 'token', value: 'foldertoken' }]\n          },\n          item: [\n            {\n              name: 'Inheriting Request',\n              request: {\n                method: 'GET',\n                url: 'https://api.example.com/test'\n                // auth property is ABSENT for this request, meaning \"Inherit auth from parent\"\n              }\n            }\n          ]\n        }\n      ]\n    };\n\n    const result = await postmanToBruno(postmanCollection);\n\n    expect(result.items[0].items[0].request.auth).toEqual({\n      mode: 'inherit',\n      basic: null,\n      bearer: null, // It should NOT have the folder's token directly here after import\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n  });\n\n  it('should handle \"Inherit Auth\" for request (auth property absent, inherits from collection if folder also inherits)', async () => {\n    const postmanCollection = {\n      info: {\n        name: 'Request Inherit Auth from Collection',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      auth: { // Collection has auth\n        type: 'basic',\n        basic: [\n\n          { key: 'username', value: 'requestuser' },\n          { key: 'password', value: 'requestpass' }\n        ]\n      },\n      item: [\n        {\n          name: 'Inheriting Folder',\n          // auth property is ABSENT for this folder\n          item: [\n            {\n              name: 'Inheriting Request',\n              request: {\n                method: 'GET',\n                url: 'https://api.example.com/test'\n                // auth property is ABSENT for this request\n              }\n            }\n          ]\n        }\n      ]\n    };\n\n    const result = await postmanToBruno(postmanCollection);\n\n    // Check folder first\n    expect(result.items[0].root.request.auth).toEqual({\n      mode: 'inherit',\n      basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null\n    });\n    // Then check request\n    expect(result.items[0].items[0].request.auth).toEqual({\n      mode: 'inherit',\n      basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null\n    });\n  });\n\n  it('should handle explicit \"No Auth\" at request level', async () => {\n    const postmanCollection = {\n      info: {\n        name: 'Request No Auth',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'Auth Folder', // Parent folder might have auth\n          auth: {\n            type: 'bearer',\n            bearer: [{ key: 'token', value: 'foldertoken' }]\n          },\n          item: [\n            {\n              name: 'Explicit No Auth Request',\n              request: {\n                method: 'GET',\n                url: 'https://api.example.com/test',\n                auth: { // Request explicitly set to \"No Auth\"\n                  type: 'noauth'\n                }\n              }\n            }\n          ]\n        }\n      ]\n    };\n\n    const result = await postmanToBruno(postmanCollection);\n\n    expect(result.items[0].items[0].request.auth).toEqual({\n      mode: 'none', // <<<< KEY CHECK\n      basic: null,\n      bearer: null,\n      awsv4: null,\n      apikey: null,\n      oauth2: null,\n      digest: null\n    });\n  });\n\n  it('should handle \"Inherit Auth\" for a request nested under multiple inheriting folders', async () => {\n    const postmanCollection = {\n      info: {\n        name: 'Multi-Level Inherit Auth',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      auth: { // Collection level auth\n        type: 'basic',\n        basic: [\n          { key: 'username', value: 'collectionUser' },\n          { key: 'password', value: 'collectionPass' }\n        ]\n      },\n      item: [\n        {\n          name: 'Folder Level 1 (Inherit)',\n          // auth property is ABSENT for this folder, meaning \"Inherit\"\n          item: [\n            {\n              name: 'Folder Level 2 (Inherit)',\n              // auth property is ABSENT for this folder, meaning \"Inherit\"\n              item: [\n                {\n                  name: 'Deeply Nested Request (Inherit)',\n                  request: {\n                    method: 'GET',\n                    url: 'https://api.example.com/deep'\n                    // auth property is ABSENT for this request, meaning \"Inherit\"\n                  }\n                }\n              ]\n            }\n          ]\n        }\n      ]\n    };\n\n    const result = await postmanToBruno(postmanCollection);\n\n    // Check Folder Level 1\n    expect(result.items[0].root.request.auth).toEqual({\n      mode: 'inherit',\n      basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null\n    });\n\n    // Check Folder Level 2\n    expect(result.items[0].items[0].root.request.auth).toEqual({\n      mode: 'inherit',\n      basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null\n    });\n\n    // Check the Request\n    expect(result.items[0].items[0].items[0].request.auth).toEqual({\n      mode: 'inherit',\n      basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null\n    });\n  });\n\n  it('should handle \"Inherit Auth\" where an intermediate folder has explicit auth', async () => {\n    const postmanCollection = {\n      info: {\n        name: 'Multi-Level Inherit with Override',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      auth: { // Collection level auth\n        type: 'basic',\n        basic: [\n          { key: 'username', value: 'collectionUser' },\n          { key: 'password', value: 'collectionPass' }\n        ]\n      },\n      item: [\n        {\n          name: 'Folder Level 1 (Explicit Bearer)',\n          auth: { // This folder has its own auth\n            type: 'bearer',\n            bearer: [{ key: 'token', value: 'folder1Token' }]\n          },\n          item: [\n            {\n              name: 'Folder Level 2 (Inherit from Folder 1)',\n              // auth property is ABSENT for this folder, meaning \"Inherit\"\n              item: [\n                {\n                  name: 'Deeply Nested Request (Inherit from Folder 1 via Folder 2)',\n                  request: {\n                    method: 'GET',\n                    url: 'https://api.example.com/deep_override'\n                    // auth property is ABSENT for this request, meaning \"Inherit\"\n                  }\n                }\n              ]\n            }\n          ]\n        }\n      ]\n    };\n\n    const result = await postmanToBruno(postmanCollection);\n\n    // Check Folder Level 1\n    expect(result.items[0].root.request.auth).toEqual({\n      mode: 'bearer',\n      basic: null,\n      bearer: { token: 'folder1Token' }, // Explicitly set\n      awsv4: null, apikey: null, oauth2: null, digest: null\n    });\n\n    // Check Folder Level 2\n    expect(result.items[0].items[0].root.request.auth).toEqual({\n      mode: 'inherit', // Inherits from Folder 1\n      basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null\n    });\n\n    // Check the Request\n    expect(result.items[0].items[0].items[0].request.auth).toEqual({\n      mode: 'inherit', // Inherits from Folder 1\n      basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null\n    });\n  });\n\n  it('should handle \"Inherit Auth\" where an intermediate folder has explicit \"No Auth\"', async () => {\n    const postmanCollection = {\n      info: {\n        name: 'Multi-Level Inherit with No Auth Stop',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      auth: { // Collection level auth\n        type: 'basic',\n        basic: [\n          { key: 'username', value: 'collectionUser' },\n          { key: 'password', value: 'collectionPass' }\n        ]\n      },\n      item: [\n        {\n          name: 'Folder Level 1 (Explicit No Auth)',\n          auth: { // This folder is explicitly \"No Auth\"\n            type: 'noauth'\n          },\n          item: [\n            {\n              name: 'Folder Level 2 (Inherit from Folder 1 - so No Auth)',\n              // auth property is ABSENT for this folder, meaning \"Inherit\"\n              item: [\n                {\n                  name: 'Deeply Nested Request (Inherit from Folder 1 via Folder 2 - so No Auth)',\n                  request: {\n                    method: 'GET',\n                    url: 'https://api.example.com/deep_no_auth_stop'\n                    // auth property is ABSENT for this request, meaning \"Inherit\"\n                  }\n                }\n              ]\n            }\n          ]\n        }\n      ]\n    };\n\n    const result = await postmanToBruno(postmanCollection);\n\n    // Check Folder Level 1\n    expect(result.items[0].root.request.auth).toEqual({\n      mode: 'none', // Explicitly \"No Auth\"\n      basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null\n    });\n\n    // Check Folder Level 2\n    expect(result.items[0].items[0].root.request.auth).toEqual({\n      mode: 'inherit', // Inherits from Folder 1\n      basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null\n    });\n\n    // Check the Request\n    expect(result.items[0].items[0].items[0].request.auth).toEqual({\n      mode: 'inherit', // Inherits from Folder 1\n      basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-to-bruno/transform-description.spec.js",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport postmanToBruno from '../../../src/postman/postman-to-bruno';\n\ndescribe('transformDescription function', () => {\n  it('should handle null and undefined descriptions', async () => {\n    const collection = {\n      info: {\n        name: 'Test Collection',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',\n        description: null\n      },\n      item: []\n    };\n\n    const brunoCollection = await postmanToBruno(collection);\n    expect(brunoCollection.root.docs).toBe('');\n  });\n\n  it('should handle string descriptions (legacy format)', async () => {\n    const collection = {\n      info: {\n        name: 'Test Collection',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',\n        description: 'This is a string description'\n      },\n      item: []\n    };\n\n    const brunoCollection = await postmanToBruno(collection);\n    expect(brunoCollection.root.docs).toBe('This is a string description');\n  });\n\n  it('should handle object descriptions with content property (new Postman format)', async () => {\n    const collection = {\n      info: {\n        name: 'Test Collection',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',\n        description: {\n          content: 'This is the content from the new Postman format',\n          type: 'text/plain'\n        }\n      },\n      item: []\n    };\n\n    const brunoCollection = await postmanToBruno(collection);\n    expect(brunoCollection.root.docs).toBe('This is the content from the new Postman format');\n  });\n\n  it('should handle object descriptions without content property', async () => {\n    const collection = {\n      info: {\n        name: 'Test Collection',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',\n        description: {\n          type: 'text/plain'\n        }\n      },\n      item: []\n    };\n\n    const brunoCollection = await postmanToBruno(collection);\n    expect(brunoCollection.root.docs).toBe('');\n  });\n\n  it('should handle request descriptions with new format', async () => {\n    const collection = {\n      info: {\n        name: 'Test Collection',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'Test Request',\n          request: {\n            method: 'GET',\n            url: 'https://api.example.com/test',\n            description: {\n              content: 'This is a request description in new format',\n              type: 'text/plain'\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collection);\n    expect(brunoCollection.items[0].request.docs).toBe('This is a request description in new format');\n  });\n\n  it('should handle folder descriptions with new format', async () => {\n    const collection = {\n      info: {\n        name: 'Test Collection',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'Test Folder',\n          description: {\n            content: 'This is a folder description in new format',\n            type: 'text/plain'\n          },\n          item: []\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collection);\n    expect(brunoCollection.items[0].root.docs).toBe('This is a folder description in new format');\n  });\n\n  it('should handle header descriptions with new format', async () => {\n    const collection = {\n      info: {\n        name: 'Test Collection',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'Test Request',\n          request: {\n            method: 'GET',\n            url: 'https://api.example.com/test',\n            header: [\n              {\n                key: 'Authorization',\n                value: 'Bearer token',\n                description: {\n                  content: 'Authorization header description',\n                  type: 'text/plain'\n                }\n              }\n            ]\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collection);\n    expect(brunoCollection.items[0].request.headers[0].description).toBe('Authorization header description');\n  });\n\n  it('should handle query parameter descriptions with new format', async () => {\n    const collection = {\n      info: {\n        name: 'Test Collection',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'Test Request',\n          request: {\n            method: 'GET',\n            url: {\n              raw: 'https://api.example.com/test?param=value',\n              host: ['api', 'example', 'com'],\n              path: ['test'],\n              query: [\n                {\n                  key: 'param',\n                  value: 'value',\n                  description: {\n                    content: 'Query parameter description',\n                    type: 'text/plain'\n                  }\n                }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collection);\n    expect(brunoCollection.items[0].request.params[0].description).toBe('Query parameter description');\n  });\n\n  it('should handle path variable descriptions with new format', async () => {\n    const collection = {\n      info: {\n        name: 'Test Collection',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'Test Request',\n          request: {\n            method: 'GET',\n            url: {\n              raw: 'https://api.example.com/users/:id',\n              host: ['api', 'example', 'com'],\n              path: ['users', ':id'],\n              variable: [\n                {\n                  key: 'id',\n                  value: '123',\n                  description: {\n                    content: 'User ID path variable',\n                    type: 'text/plain'\n                  }\n                }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collection);\n    expect(brunoCollection.items[0].request.params[0].description).toBe('User ID path variable');\n  });\n\n  it('should handle form data descriptions with new format', async () => {\n    const collection = {\n      info: {\n        name: 'Test Collection',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'Test Request',\n          request: {\n            method: 'POST',\n            url: 'https://api.example.com/test',\n            body: {\n              mode: 'formdata',\n              formdata: [\n                {\n                  key: 'field1',\n                  value: 'value1',\n                  description: {\n                    content: 'Form field description',\n                    type: 'text/plain'\n                  }\n                }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collection);\n    expect(brunoCollection.items[0].request.body.multipartForm[0].description).toBe('Form field description');\n  });\n\n  it('should handle urlencoded form descriptions with new format', async () => {\n    const collection = {\n      info: {\n        name: 'Test Collection',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n      },\n      item: [\n        {\n          name: 'Test Request',\n          request: {\n            method: 'POST',\n            url: 'https://api.example.com/test',\n            body: {\n              mode: 'urlencoded',\n              urlencoded: [\n                {\n                  key: 'field1',\n                  value: 'value1',\n                  description: {\n                    content: 'URL encoded field description',\n                    type: 'text/plain'\n                  }\n                }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collection);\n    expect(brunoCollection.items[0].request.body.formUrlEncoded[0].description).toBe('URL encoded field description');\n  });\n\n  it('should handle mixed description formats in the same collection', async () => {\n    const collection = {\n      info: {\n        name: 'Test Collection',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',\n        description: 'Collection with string description'\n      },\n      item: [\n        {\n          name: 'Test Folder',\n          description: {\n            content: 'Folder with object description',\n            type: 'text/plain'\n          },\n          item: [\n            {\n              name: 'Test Request',\n              request: {\n                method: 'GET',\n                url: 'https://api.example.com/test',\n                description: 'Request with string description',\n                header: [\n                  {\n                    key: 'Content-Type',\n                    value: 'application/json',\n                    description: {\n                      content: 'Header with object description',\n                      type: 'text/plain'\n                    }\n                  }\n                ]\n              }\n            }\n          ]\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collection);\n\n    // Collection description (string)\n    expect(brunoCollection.root.docs).toBe('Collection with string description');\n\n    // Folder description (object)\n    expect(brunoCollection.items[0].root.docs).toBe('Folder with object description');\n\n    // Request description (string)\n    expect(brunoCollection.items[0].items[0].request.docs).toBe('Request with string description');\n\n    // Header description (object)\n    expect(brunoCollection.items[0].items[0].request.headers[0].description).toBe('Header with object description');\n  });\n\n  it('should handle edge cases like empty strings and special characters', async () => {\n    const collection = {\n      info: {\n        name: 'Test Collection',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',\n        description: {\n          content: '',\n          type: 'text/plain'\n        }\n      },\n      item: [\n        {\n          name: 'Test Request',\n          request: {\n            method: 'GET',\n            url: 'https://api.example.com/test',\n            description: {\n              content: 'Description with special chars: !@#$%^&*()',\n              type: 'text/plain'\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(collection);\n    expect(brunoCollection.root.docs).toBe('');\n    expect(brunoCollection.items[0].request.docs).toBe('Description with special chars: !@#$%^&*()');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-translations/postman-comments.spec.js",
    "content": "import postmanTranslation from '../../../src/postman/postman-translations';\n\ndescribe('postmanTranslations - comment handling', () => {\n  test('should not translate non-pm commands', () => {\n    const inputScript = `\n      console.log('This script does not contain pm commands.');\n      const data = pm.environment.get('key');\n      pm.collectionVariables.set('key', data);\n    `;\n    const result = postmanTranslation(inputScript);\n    expect(result).toContain('console.log(\\'This script does not contain pm commands.\\');');\n    expect(result).toContain('const data = bru.getEnvVar(\\'key\\');');\n  });\n\n  // TODO: Restore once UI update fixes are live for setCollectionVar\n  test.skip('should translate pm.collectionVariables.set to bru.setCollectionVar', () => {\n    const inputScript = 'pm.collectionVariables.set(\\'key\\', data);';\n    const expectedOutput = 'bru.setCollectionVar(\\'key\\', data);';\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should comment non-translated pm commands', () => {\n    const inputScript = 'pm.test(\\'random test\\', () => pm.vault.get(secretPath));';\n    const expectedOutput = '// test(\\'random test\\', () => pm.vault.get(secretPath));';\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should handle multiple pm commands on the same line', () => {\n    const inputScript = 'pm.environment.get(\\'key\\'); pm.environment.set(\\'key\\', \\'value\\');';\n    const expectedOutput = 'bru.getEnvVar(\\'key\\'); bru.setEnvVar(\\'key\\', \\'value\\');';\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should handle comments and other JavaScript code', () => {\n    const inputScript = `\n      // This is a comment\n      const value = 'test';\n      pm.environment.set('key', value);\n      /*\n        Multi-line comment\n      */\n      const result = pm.environment.get('key');\n      console.log('Result:', result);\n    `;\n    const expectedOutput = `\n      // This is a comment\n      const value = 'test';\n      bru.setEnvVar('key', value);\n      /*\n        Multi-line comment\n      */\n      const result = bru.getEnvVar('key');\n      console.log('Result:', result);\n    `;\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-translations/postman-cookie-conversions.spec.js",
    "content": "import postmanTranslation from '../../../src/postman/postman-translations';\n\ndescribe('postmanTranslations - cookie API conversions', () => {\n  test('should convert pm.cookies.jar().get to bru.cookies.jar().getCookie', () => {\n    const inputScript = `pm.cookies.jar().get('https://example.com', 'sessionId', (err, cookie) => {\n      console.log(cookie);\n    });`;\n\n    const expectedOutput = `bru.cookies.jar().getCookie('https://example.com', 'sessionId', (err, cookie) => {\n      console.log(cookie);\n    });`;\n\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should convert pm.cookies.jar().getAll to bru.cookies.jar().getCookies', () => {\n    const inputScript = `pm.cookies.jar().getAll('https://example.com', (err, cookies) => {\n      console.log(cookies);\n    });`;\n\n    const expectedOutput = `bru.cookies.jar().getCookies('https://example.com', (err, cookies) => {\n      console.log(cookies);\n    });`;\n\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should convert pm.cookies.jar().set to bru.cookies.jar().setCookie', () => {\n    const inputScript = `pm.cookies.jar().set('https://example.com', 'sessionId', 'abc123', (err) => {\n      if (err) console.error(err);\n    });`;\n\n    const expectedOutput = `bru.cookies.jar().setCookie('https://example.com', 'sessionId', 'abc123', (err) => {\n      if (err) console.error(err);\n    });`;\n\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should convert pm.cookies.jar().unset to bru.cookies.jar().deleteCookie', () => {\n    const inputScript = `pm.cookies.jar().unset('https://example.com', 'sessionId', (err) => {\n      if (err) console.error(err);\n    });`;\n\n    const expectedOutput = `bru.cookies.jar().deleteCookie('https://example.com', 'sessionId', (err) => {\n      if (err) console.error(err);\n    });`;\n\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should convert pm.cookies.jar().clear to bru.cookies.jar().deleteCookies (behavior difference)', () => {\n    const inputScript = `pm.cookies.jar().clear('https://example.com', (err) => {\n      if (err) console.error(err);\n    });`;\n\n    const expectedOutput = `bru.cookies.jar().deleteCookies('https://example.com', (err) => {\n      if (err) console.error(err);\n    });`;\n\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should handle multiple cookie operations in one script', () => {\n    const inputScript = `\n      pm.cookies.jar().set('https://api.example.com', 'auth', 'token123');\n      const cookie = pm.cookies.jar().get('https://api.example.com', 'auth');\n      pm.cookies.jar().getAll('https://api.example.com', (err, cookies) => {\n        console.log('All cookies:', cookies);\n      });\n      pm.cookies.jar().unset('https://api.example.com', 'temp');\n      pm.cookies.jar().clear('https://api.example.com');\n    `;\n\n    const expectedOutput = `\n      bru.cookies.jar().setCookie('https://api.example.com', 'auth', 'token123');\n      const cookie = bru.cookies.jar().getCookie('https://api.example.com', 'auth');\n      bru.cookies.jar().getCookies('https://api.example.com', (err, cookies) => {\n        console.log('All cookies:', cookies);\n      });\n      bru.cookies.jar().deleteCookie('https://api.example.com', 'temp');\n      bru.cookies.jar().deleteCookies('https://api.example.com');\n    `;\n\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should convert variable assignment and method calls on cookie jar variables', () => {\n    const inputScript = `\n      const jar = pm.cookies.jar();\n      jar.set('https://example.com', 'user', 'john');\n      const userCookie = jar.get('https://example.com', 'user');\n    `;\n\n    const expectedOutput = `\n      const jar = bru.cookies.jar();\n      jar.setCookie('https://example.com', 'user', 'john');\n      const userCookie = jar.getCookie('https://example.com', 'user');\n    `;\n\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should convert jar.get to jar.getCookie with callback', () => {\n    const inputScript = `\n      const jar = pm.cookies.jar();\n      jar.get('https://api.example.com', 'authToken', (error, cookie) => {\n        if (error) {\n          console.error('Error getting cookie:', error);\n        } else {\n          console.log('Retrieved cookie:', cookie);\n        }\n      });\n    `;\n\n    const expectedOutput = `\n      const jar = bru.cookies.jar();\n      jar.getCookie('https://api.example.com', 'authToken', (error, cookie) => {\n        if (error) {\n          console.error('Error getting cookie:', error);\n        } else {\n          console.log('Retrieved cookie:', cookie);\n        }\n      });\n    `;\n\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should convert jar.getAll to jar.getCookies with callback', () => {\n    const inputScript = `\n      const jar = pm.cookies.jar();\n      jar.getAll('https://api.example.com', (error, cookies) => {\n        if (error) {\n          console.error('Error getting cookies:', error);\n        } else {\n          console.log('All cookies:', cookies);\n        }\n      });\n    `;\n\n    const expectedOutput = `\n      const jar = bru.cookies.jar();\n      jar.getCookies('https://api.example.com', (error, cookies) => {\n        if (error) {\n          console.error('Error getting cookies:', error);\n        } else {\n          console.log('All cookies:', cookies);\n        }\n      });\n    `;\n\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should convert jar.set to jar.setCookie with cookie object', () => {\n    const inputScript = `\n      const jar = pm.cookies.jar();\n      jar.set('https://api.example.com', {\n        key: 'sessionId',\n        value: 'abc123',\n        path: '/api',\n        httpOnly: true,\n        secure: true\n      }, (error) => {\n        if (error) console.error(error);\n      });\n    `;\n\n    const expectedOutput = `\n      const jar = bru.cookies.jar();\n      jar.setCookie('https://api.example.com', {\n        key: 'sessionId',\n        value: 'abc123',\n        path: '/api',\n        httpOnly: true,\n        secure: true\n      }, (error) => {\n        if (error) console.error(error);\n      });\n    `;\n\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should convert jar.unset to jar.deleteCookie', () => {\n    const inputScript = `\n      const jar = pm.cookies.jar();\n      jar.unset('https://api.example.com', 'tempCookie', (error) => {\n        if (error) {\n          console.error('Failed to delete cookie:', error);\n        } else {\n          console.log('Cookie deleted successfully');\n        }\n      });\n    `;\n\n    const expectedOutput = `\n      const jar = bru.cookies.jar();\n      jar.deleteCookie('https://api.example.com', 'tempCookie', (error) => {\n        if (error) {\n          console.error('Failed to delete cookie:', error);\n        } else {\n          console.log('Cookie deleted successfully');\n        }\n      });\n    `;\n\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should convert jar.clear to jar.deleteCookies', () => {\n    const inputScript = `\n      const jar = pm.cookies.jar();\n      jar.clear('https://api.example.com', (error) => {\n        if (error) {\n          console.error('Failed to clear cookies:', error);\n        } else {\n          console.log('All cookies cleared for domain');\n        }\n      });\n    `;\n\n    const expectedOutput = `\n      const jar = bru.cookies.jar();\n      jar.deleteCookies('https://api.example.com', (error) => {\n        if (error) {\n          console.error('Failed to clear cookies:', error);\n        } else {\n          console.log('All cookies cleared for domain');\n        }\n      });\n    `;\n\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should handle complex cookie workflow with jar variable', () => {\n    const inputScript = `\n      const cookieJar = pm.cookies.jar();\n      \n      // Set multiple cookies\n      cookieJar.set('https://example.com', 'auth', 'token123');\n      cookieJar.set('https://example.com', {\n        key: 'preferences',\n        value: JSON.stringify({theme: 'dark'}),\n        path: '/'\n      });\n      \n      // Get specific cookie\n      cookieJar.get('https://example.com', 'auth', (err, authCookie) => {\n        console.log('Auth cookie:', authCookie);\n      });\n      \n      // Get all cookies\n      cookieJar.getAll('https://example.com', (err, allCookies) => {\n        console.log('Total cookies:', allCookies.length);\n      });\n      \n      // Clean up\n      cookieJar.unset('https://example.com', 'temp');\n      cookieJar.clear('https://example.com');\n    `;\n\n    const expectedOutput = `\n      const cookieJar = bru.cookies.jar();\n      \n      // Set multiple cookies\n      cookieJar.setCookie('https://example.com', 'auth', 'token123');\n      cookieJar.setCookie('https://example.com', {\n        key: 'preferences',\n        value: JSON.stringify({theme: 'dark'}),\n        path: '/'\n      });\n      \n      // Get specific cookie\n      cookieJar.getCookie('https://example.com', 'auth', (err, authCookie) => {\n        console.log('Auth cookie:', authCookie);\n      });\n      \n      // Get all cookies\n      cookieJar.getCookies('https://example.com', (err, allCookies) => {\n        console.log('Total cookies:', allCookies.length);\n      });\n      \n      // Clean up\n      cookieJar.deleteCookie('https://example.com', 'temp');\n      cookieJar.deleteCookies('https://example.com');\n    `;\n\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should handle mixed jar variable and direct calls', () => {\n    const inputScript = `\n      const jar = pm.cookies.jar();\n      jar.get('https://api.com', 'session');\n      \n      pm.cookies.jar().set('https://other.com', 'temp', 'value');\n      \n      jar.getAll('https://api.com', (err, cookies) => {\n        console.log(cookies);\n      });\n    `;\n\n    const expectedOutput = `\n      const jar = bru.cookies.jar();\n      jar.getCookie('https://api.com', 'session');\n      \n      bru.cookies.jar().setCookie('https://other.com', 'temp', 'value');\n      \n      jar.getCookies('https://api.com', (err, cookies) => {\n        console.log(cookies);\n      });\n    `;\n\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  // Tests for pm.cookies direct access methods (has, get, toObject)\n\n  test('should convert pm.cookies.has(name) to await hasCookie', () => {\n    const inputScript = `pm.cookies.has('token')`;\n    const expectedOutput = `await bru.cookies.jar().hasCookie(req.getUrl(), 'token')`;\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should convert pm.cookies.get(name) to await getCookie?.value', () => {\n    const inputScript = `pm.cookies.get('token')`;\n    const expectedOutput = `(await bru.cookies.jar().getCookie(req.getUrl(), 'token'))?.value`;\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should convert pm.cookies.toObject() to getCookies reduce', () => {\n    const inputScript = `pm.cookies.toObject()`;\n    const expectedOutput = `(await bru.cookies.jar().getCookies(req.getUrl())).reduce((obj, c) => ({\n  ...obj,\n  [c.key]: c.value\n}), {})`;\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should convert pm.cookies.has inside an if conditional', () => {\n    const inputScript = `if (pm.cookies.has('auth')) { console.log('found'); }`;\n    const expectedOutput = `if (await bru.cookies.jar().hasCookie(req.getUrl(), 'auth')) { console.log('found'); }`;\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should convert pm.cookies.get with a variable argument', () => {\n    const inputScript = `const val = pm.cookies.get(cookieName)`;\n    const expectedOutput = `const val = (await bru.cookies.jar().getCookie(req.getUrl(), cookieName))?.value`;\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should handle mixed pm.cookies.get and pm.cookies.jar().set without conflict', () => {\n    const inputScript = `const v = pm.cookies.get('token'); pm.cookies.jar().set('https://example.com', 'a', 'b');`;\n    const expectedOutput = `const v = (await bru.cookies.jar().getCookie(req.getUrl(), 'token'))?.value; bru.cookies.jar().setCookie('https://example.com', 'a', 'b');`;\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should handle combined has + get in same script', () => {\n    const inputScript = `if (pm.cookies.has('auth')) { const token = pm.cookies.get('auth'); }`;\n    const expectedOutput = `if (await bru.cookies.jar().hasCookie(req.getUrl(), 'auth')) { const token = (await bru.cookies.jar().getCookie(req.getUrl(), 'auth'))?.value; }`;\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n\n  test('should handle aliased access: const cookies = pm.cookies', () => {\n    const inputScript = `const cookies = pm.cookies; cookies.get('token');`;\n    const expectedOutput = `(await bru.cookies.jar().getCookie(req.getUrl(), 'token'))?.value;`;\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-translations/postman-edge-cases.spec.js",
    "content": "import postmanTranslation from '../../../src/postman/postman-translations';\n\ndescribe('postmanTranslations - edge cases', () => {\n  test('should handle nested commands and edge cases', () => {\n    const inputScript = `\n      const sampleObjects = [\n        {\n          key: pm.environment.get('key'),\n          value: pm.variables.get('value')\n        },\n        {\n          key: pm.collectionVariables.get('key'),\n          value: pm.collectionVariables.get('value')\n        }\n      ];\n      const dataTesting = Object.entries(sampleObjects || {}).reduce((acc, [key, value]) => {\n        // this is a comment\n        acc[key] = pm.collectionVariables.get(pm.environment.get(value));\n        return acc; // Return the accumulator\n      }, {});\n      Object.values(dataTesting).forEach((data) => {\n        pm.environment.set(data.key, pm.variables.get(data.value));\n      });\n    `;\n    const expectedOutput = `\n      const sampleObjects = [\n        {\n          key: bru.getEnvVar('key'),\n          value: bru.getVar('value')\n        },\n        {\n          key: bru.getCollectionVar('key'),\n          value: bru.getCollectionVar('value')\n        }\n      ];\n      const dataTesting = Object.entries(sampleObjects || {}).reduce((acc, [key, value]) => {\n        // this is a comment\n        acc[key] = bru.getCollectionVar(bru.getEnvVar(value));\n        return acc; // Return the accumulator\n      }, {});\n      Object.values(dataTesting).forEach((data) => {\n        bru.setEnvVar(data.key, bru.getVar(data.value));\n      });\n    `;\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-translations/postman-test-commands.spec.js",
    "content": "import postmanTranslation from '../../../src/postman/postman-translations';\n\ndescribe('postmanTranslations - test commands', () => {\n  test('should handle test commands', () => {\n    const inputScript = `\n      pm.test('Status code is 200', () => {\n        pm.response.to.have.status(200);\n      });\n      pm.test('this test will fail', () => {\n        return false\n      });\n    `;\n    const expectedOutput = `\n      test('Status code is 200', () => {\n        expect(res.getStatus()).to.equal(200);\n      });\n      test('this test will fail', () => {\n        return false\n      });\n    `;\n    expect(postmanTranslation(inputScript)).toBe(expectedOutput);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-translations/postman-variables.spec.js",
    "content": "import postmanTranslation from '../../../src/postman/postman-translations';\n\ndescribe('postmanTranslations - variables commands', () => {\n  test('should translate environment variable commands', () => {\n    const inputScript = `\n      pm.environment.get('key');\n      pm.environment.set('key', 'value');\n    `;\n    const result = postmanTranslation(inputScript);\n    expect(result).toContain('bru.getEnvVar(\\'key\\')');\n    expect(result).toContain('bru.setEnvVar(\\'key\\', \\'value\\')');\n  });\n\n  test('should translate runtime variable commands', () => {\n    const inputScript = `\n      pm.variables.get('key');\n      pm.variables.set('key', 'value');\n    `;\n    const result = postmanTranslation(inputScript);\n    expect(result).toContain('bru.getVar(\\'key\\')');\n    expect(result).toContain('bru.setVar(\\'key\\', \\'value\\')');\n  });\n\n  test('should translate pm.collectionVariables.get', () => {\n    const inputScript = 'pm.collectionVariables.get(\\'key\\');';\n    const result = postmanTranslation(inputScript);\n    expect(result).toContain('bru.getCollectionVar(\\'key\\')');\n  });\n\n  test('should translate pm.expect with pm.environment.has', () => {\n    const inputScript = 'pm.expect(pm.environment.has(\\'key\\')).to.be.true;';\n    const result = postmanTranslation(inputScript);\n    expect(result).toContain('bru.getEnvVar(\\'key\\') !== undefined && bru.getEnvVar(\\'key\\') !== null');\n    expect(result).toContain('.to.be.true');\n  });\n\n  // TODO: Restore once UI update fixes are live for setCollectionVar\n  test.skip('should translate pm.collectionVariables.set to bru.setCollectionVar', () => {\n    const inputScript = 'pm.collectionVariables.set(\\'key\\', \\'value\\');';\n    expect(postmanTranslation(inputScript)).toBe('bru.setCollectionVar(\\'key\\', \\'value\\');');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/combined.test.js",
    "content": "import translateCode from '../../../../src/utils/postman-to-bruno-translator';\n\ndescribe('Combined API Features Translation', () => {\n  // Basic translation test\n  it('should translate code', () => {\n    const code = 'console.log(\"Hello, world!\");';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(code);\n  });\n\n  // Preserving comments\n  it('should preserve comments', () => {\n    const code = '// This is a comment';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('// This is a comment');\n  });\n\n  it('should preserve comments inside functions', () => {\n    const code = `\n        function getUserDetails() {\n            // Get user details from API\n            const response = pm.response.json();\n        }\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        function getUserDetails() {\n            // Get user details from API\n            const response = res.getBody();\n        }\n        `);\n  });\n\n  it('should preserve comments inside if statements', () => {\n    const code = `\n        if (pm.response.code === 200) {\n            // Success\n            console.log(\"Success\");\n        }\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        if (res.getStatus() === 200) {\n            // Success\n            console.log(\"Success\");\n        }\n        `);\n  });\n\n  it('should preserve multiline comments', () => {\n    const code = `\n        /*\n        This is a multiline comment\n        */\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        /*\n        This is a multiline comment\n        */\n        `);\n  });\n\n  it('should preserve comments inside for loops', () => {\n    const code = `\n        for (let i = 0; i < 10; i++) {\n            // Loop iteration\n            console.log(pm.response.json()[i]);\n        }\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        for (let i = 0; i < 10; i++) {\n            // Loop iteration\n            console.log(res.getBody()[i]);\n        }\n        `);\n  });\n\n  // Multiple transformations in the same code block\n  it('should handle multiple translations in the same code block', () => {\n    const code = `\n        const token = pm.environment.get(\"authToken\");\n        pm.test(\"Auth flow works\", function() {\n            const response = pm.response.json();\n            pm.expect(response.authenticated).to.be.true;\n            pm.environment.set(\"userId\", response.user.id);\n            pm.collectionVariables.set(\"sessionId\", response.session.id);\n        });\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).not.toContain('pm.test(\"Auth flow works\", function() {');\n    expect(translatedCode).not.toContain('pm.expect(response.authenticated).to.be.true;');\n    expect(translatedCode).not.toContain('pm.environment.set(\"userId\", response.user.id);');\n    expect(translatedCode).toContain('const token = bru.getEnvVar(\"authToken\");');\n    expect(translatedCode).toContain('test(\"Auth flow works\", function() {');\n    expect(translatedCode).toContain('const response = res.getBody();');\n    expect(translatedCode).toContain('expect(response.authenticated).to.be.true;');\n    expect(translatedCode).toContain('bru.setEnvVar(\"userId\", response.user.id);');\n  });\n\n  // TODO: Restore once UI update fixes are live for setCollectionVar\n  it.skip('should translate pm.collectionVariables.set in a combined code block', () => {\n    const code = 'pm.collectionVariables.set(\"sessionId\", response.session.id);';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('bru.setCollectionVar(\"sessionId\", response.session.id);');\n  });\n\n  // Nested expressions\n  it('should handle nested Postman API calls', () => {\n    const code = 'pm.environment.set(\"computed\", pm.variables.get(\"base\") + \"-suffix\");';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('bru.setEnvVar(\"computed\", bru.getVar(\"base\") + \"-suffix\");');\n  });\n\n  // TODO: Restore once UI update fixes are live for setCollectionVar\n  it.skip('should handle more complex nested expressions', () => {\n    const code = 'pm.collectionVariables.set(\"fullPath\", pm.environment.get(\"baseUrl\") + pm.variables.get(\"endpoint\"));';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('bru.setCollectionVar(\"fullPath\", bru.getEnvVar(\"baseUrl\") + bru.getVar(\"endpoint\"));');\n  });\n\n  // Unrelated code\n  it('should leave unrelated code untouched', () => {\n    const code = `\n        function calculateTotal(items) {\n            return items.reduce((sum, item) => sum + item.price, 0);\n        }\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(code);\n  });\n\n  it('should handle Postman API calls within JavaScript methods', () => {\n    const code = `\n        const helpers = {\n            getAuthHeader: function() {\n                return \"Bearer \" + pm.environment.get(\"token\");\n            }\n        };\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('return \"Bearer \" + bru.getEnvVar(\"token\");');\n  });\n\n  it('should handle aliases with object destructuring', () => {\n    const code = `\n        const { environment, variables } = pm;\n        environment.set(\"token\", \"abc123\");\n        variables.get(\"userId\");\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe(`\n        bru.setEnvVar(\"token\", \"abc123\");\n        bru.getVar(\"userId\");\n        `);\n  });\n\n  // Code context tests\n  it('should translate pm commands inside functions', () => {\n    const code = `\n        function getAuthHeader() {\n            return \"Bearer \" + pm.environment.get(\"token\");\n        }\n        `;\n\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        function getAuthHeader() {\n            return \"Bearer \" + bru.getEnvVar(\"token\");\n        }\n        `);\n  });\n\n  it('should translate pm commands inside if statements', () => {\n    const code = `\n        if (pm.response.code === 200) {\n            console.log(\"Success\");\n        }\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        if (res.getStatus() === 200) {\n            console.log(\"Success\");\n        }\n        `);\n  });\n\n  it('should translate pm commands inside if statements', () => {\n    const code = `\n        const json = pm.response.json();\n        if (json.code === 200) {\n            console.log(\"Success\");\n        }\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        const json = res.getBody();\n        if (json.code === 200) {\n            console.log(\"Success\");\n        }\n        `);\n  });\n\n  it('should translate pm commands inside else statements', () => {\n    const code = `\n        if (pm.response.code === 200) {\n            console.log(\"Success\");\n            pm.response.to.have.status(200);\n        } else {\n            console.log(\"Failure\");\n            expect(res.getStatus()).to.equal(400);\n        }\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        if (res.getStatus() === 200) {\n            console.log(\"Success\");\n            expect(res.getStatus()).to.equal(200);\n        } else {\n            console.log(\"Failure\");\n            expect(res.getStatus()).to.equal(400);\n        }\n        `);\n  });\n\n  it('should translate pm commands inside for loops', () => {\n    const code = `\n        for (let i = 0; i < pm.response.json().length; i++) {\n            console.log(pm.response.json()[i]);\n        }\n        `;\n\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        for (let i = 0; i < res.getBody().length; i++) {\n            console.log(res.getBody()[i]);\n        }\n        `);\n  });\n\n  it('should translate pm commands inside while loops', () => {\n    const code = `\n        while (pm.response.code === 200) {\n            console.log(\"Success\");\n        }\n        `;\n\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        while (res.getStatus() === 200) {\n            console.log(\"Success\");\n        }\n        `);\n  });\n\n  it('should translate pm commands inside switch statements', () => {\n    const code = `\n        switch (pm.response.code) {\n            case 200:\n                console.log(\"Success\");\n                break;\n        }\n        `;\n\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        switch (res.getStatus()) {\n            case 200:\n                console.log(\"Success\");\n                break;\n        }\n        `);\n  });\n\n  it('should translate pm commands inside try catch statements', () => {\n    const code = `\n        try {\n            pm.response.to.have.status(200);\n        } catch (error) {\n            console.log(\"Failure\");\n            expect(res.getStatus()).to.equal(400);\n        }\n        `;\n\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        try {\n            expect(res.getStatus()).to.equal(200);\n        } catch (error) {\n            console.log(\"Failure\");\n            expect(res.getStatus()).to.equal(400);\n        }\n        `);\n  });\n\n  it('should translate aliases within if statements block', () => {\n    const code = `\n        const env = pm.environment;\n        const vars = pm.variables;\n        const collVars = pm.collectionVariables;\n        const test = pm.test;\n        const expect = pm.expect;\n        const response = pm.response;\n        \n        function processResponse() {\n          if(response.code === 200) {\n            console.log(\"Success\");\n          } else if(response.code === 400) {\n            console.log(\"Failure\");\n            expect(response.code).to.equal(400);\n          } else {\n            console.log(\"Unknown status code\");\n            expect(response.code).to.equal(500);\n          }\n        }\n        `;\n\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        function processResponse() {\n          if(res.getStatus() === 200) {\n            console.log(\"Success\");\n          } else if(res.getStatus() === 400) {\n            console.log(\"Failure\");\n            expect(res.getStatus()).to.equal(400);\n          } else {\n            console.log(\"Unknown status code\");\n            expect(res.getStatus()).to.equal(500);\n          }\n        }\n        `);\n  });\n\n  it('should handle pm aliases inside functions', () => {\n    const code = `\n        const tempRes = pm.response;\n        const tempTest = pm.test;\n        const tempExpect = pm.expect;\n        const tempEnv = pm.environment;\n        const tempVars = pm.variables;\n        const tempCollVars = pm.collectionVariables;\n\n        function processResponse() {\n            tempTest(\"Status code is 200\", function() { expect(tempRes.code).to.equal(200); });\n            tempEnv.set(\"userId\", tempRes.json().userId);\n            tempVars.set(\"token\", tempRes.json().token);\n            tempCollVars.set(\"sessionId\", tempRes.json().sessionId);\n        }\n        `;\n\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('test(\"Status code is 200\", function() { expect(res.getStatus()).to.equal(200); });');\n    expect(translatedCode).toContain('bru.setEnvVar(\"userId\", res.getBody().userId);');\n    expect(translatedCode).toContain('bru.setVar(\"token\", res.getBody().token);');\n  });\n\n  // TODO: Restore once UI update fixes are live for setCollectionVar\n  it.skip('should translate pm.collectionVariables alias set inside functions', () => {\n    const code = `\n        const tempCollVars = pm.collectionVariables;\n        tempCollVars.set(\"sessionId\", \"value\");\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('bru.setCollectionVar(\"sessionId\", \"value\");');\n  });\n\n  it('should nested pm commands', () => {\n    const code = `\n        pm.collectionVariables.get(pm.environment.get('key'))\n        pm.test(\"Status code is 200\", function() {\n            pm.response.to.have.status(200);\n        });\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        bru.getCollectionVar(bru.getEnvVar('key'))\n        test(\"Status code is 200\", function() {\n            expect(res.getStatus()).to.equal(200);\n        });\n        `);\n  });\n\n  it('should handle pm objects in template literals', () => {\n    const code = `\n        const baseUrl = pm.environment.get(\"baseUrl\");\n        const endpoint = pm.variables.get(\"endpoint\");\n        const url = \\`\\${baseUrl}/api/\\${endpoint}\\`;\n        console.log(\\`Response status: \\${pm.response.code}\\`);\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('const baseUrl = bru.getEnvVar(\"baseUrl\");');\n    expect(translatedCode).toContain('const endpoint = bru.getVar(\"endpoint\");');\n    expect(translatedCode).toContain('const url = `${baseUrl}/api/${endpoint}`;');\n    expect(translatedCode).toContain('console.log(`Response status: ${res.getStatus()}`);');\n  });\n\n  it('should handle pm objects in arrow functions', () => {\n    const code = `\n        const getAuthHeader = () => \"Bearer \" + pm.environment.get(\"token\");\n        const processItems = items => items.forEach(item => {\n            pm.variables.set(item.key, item.value);\n        });\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('const getAuthHeader = () => \"Bearer \" + bru.getEnvVar(\"token\");');\n    expect(translatedCode).toContain('const processItems = items => items.forEach(item => {');\n    expect(translatedCode).toContain('bru.setVar(item.key, item.value);');\n  });\n\n  it('test', () => {\n    const code = `\n        const globals = pm.globals;\n        const key = globals.get(\"key\");\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        const key = bru.getGlobalEnvVar(\"key\");\n        `);\n  });\n\n  it('should handle pm.response.to.have.body integrated with other assertions', () => {\n    const code = `\n        pm.test(\"Response validation\", function() {\n            pm.response.to.have.status(200);\n            pm.response.to.have.body({\"success\": true});\n            pm.response.to.have.header(\"Content-Type\", \"application/json\");\n        });\n        `;\n    const translatedCode = translateCode(code);\n\n    const expectedOutput = `\n        test(\"Response validation\", function() {\n            expect(res.getStatus()).to.equal(200);\n            expect(res.getBody()).to.equal({\"success\": true});\n            expect(res.getHeaders()).to.have.property(\"Content-Type\".toLowerCase(), \"application/json\");\n        });\n        `;\n    expect(translatedCode).toBe(expectedOutput);\n  });\n\n  it('should handle pm.response.to.have.body with dynamic content', () => {\n    const code = `\n        const expectedResponse = {\n            id: pm.environment.get(\"userId\"),\n            token: pm.variables.get(\"authToken\"),\n            timestamp: new Date().getTime()\n        };\n        \n        pm.test(\"Dynamic response validation\", function() {\n            pm.response.to.have.body(expectedResponse);\n        });\n        `;\n    const translatedCode = translateCode(code);\n\n    const expectedOutput = `\n        const expectedResponse = {\n            id: bru.getEnvVar(\"userId\"),\n            token: bru.getVar(\"authToken\"),\n            timestamp: new Date().getTime()\n        };\n        \n        test(\"Dynamic response validation\", function() {\n            expect(res.getBody()).to.equal(expectedResponse);\n        });\n        `;\n    expect(translatedCode).toBe(expectedOutput);\n  });\n\n  it('should handle pm.response.to.have.body in control structures', () => {\n    const code = `\n        const jsonData = pm.response.json();\n        \n        if (jsonData.status === \"success\") {\n            pm.response.to.have.body({\n                status: \"success\",\n                data: jsonData.data\n            });\n        } else {\n            pm.expect(jsonData.error).to.exist;\n        }\n        `;\n    const translatedCode = translateCode(code);\n\n    const expectedOutput = `\n        const jsonData = res.getBody();\n        \n        if (jsonData.status === \"success\") {\n            expect(res.getBody()).to.equal({\n                status: \"success\",\n                data: jsonData.data\n            });\n        } else {\n            expect(jsonData.error).to.exist;\n        }\n        `;\n    expect(translatedCode).toBe(expectedOutput);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/environment.test.js",
    "content": "import translateCode from '../../../../src/utils/postman-to-bruno-translator';\n\ndescribe('Environment Variable Translation', () => {\n  it('should translate pm.environment.get', () => {\n    const code = 'pm.environment.get(\"test\");';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('bru.getEnvVar(\"test\");');\n  });\n\n  it('should translate pm.environment.set', () => {\n    const code = 'pm.environment.set(\"test\", \"value\");';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('bru.setEnvVar(\"test\", \"value\");');\n  });\n\n  it('should translate pm.environment.has', () => {\n    const code = 'pm.environment.has(\"test\")';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('bru.getEnvVar(\"test\") !== undefined && bru.getEnvVar(\"test\") !== null');\n  });\n\n  it('should translate pm.environment.unset', () => {\n    const code = 'pm.environment.unset(\"test\");';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('bru.deleteEnvVar(\"test\");');\n  });\n\n  it('should translate pm.environment.name', () => {\n    const code = 'pm.environment.name;';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('bru.getEnvName();');\n  });\n\n  it('should handle nested Postman API calls with environment', () => {\n    const code = 'pm.environment.set(\"computed\", pm.variables.get(\"base\") + \"-suffix\");';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('bru.setEnvVar(\"computed\", bru.getVar(\"base\") + \"-suffix\");');\n  });\n\n  it('should handle JSON operations with environment variables', () => {\n    const code = 'pm.environment.set(\"user\", JSON.stringify({ id: 123, name: \"John\" }));';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('bru.setEnvVar(\"user\", JSON.stringify({ id: 123, name: \"John\" }));');\n  });\n\n  it('should handle JSON.parse with environment variables', () => {\n    const code = 'const userData = JSON.parse(pm.environment.get(\"user\"));';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('const userData = JSON.parse(bru.getEnvVar(\"user\"));');\n  });\n\n  it('should translate pm.environment.name with different access patterns', () => {\n    const code = `\n        const envName1 = pm.environment.name;\n        const env = pm.environment;\n        const envName2 = env.name;\n        console.log(pm.environment.name);\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        const envName1 = bru.getEnvName();\n        const envName2 = bru.getEnvName();\n        console.log(bru.getEnvName());\n        `);\n  });\n\n  it('should handle environment aliases', () => {\n    const code = `\n        const env = pm.environment;\n        const name = env.name;\n        const has = env.has(\"test\");\n        const set = env.set(\"test\", \"value\");\n        const get = env.get(\"test\");\n        const unset = env.unset(\"test\");\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        const name = bru.getEnvName();\n        const has = bru.getEnvVar(\"test\") !== undefined && bru.getEnvVar(\"test\") !== null;\n        const set = bru.setEnvVar(\"test\", \"value\");\n        const get = bru.getEnvVar(\"test\");\n        const unset = bru.deleteEnvVar(\"test\");\n        `);\n  });\n\n  // Legacy API (postman.) tests related to environment\n  it('should translate postman.setEnvironmentVariable', () => {\n    const code = 'postman.setEnvironmentVariable(\"apiKey\", \"abc123\");';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('bru.setEnvVar(\"apiKey\", \"abc123\");');\n  });\n\n  it('should translate postman.getEnvironmentVariable', () => {\n    const code = 'const baseUrl = postman.getEnvironmentVariable(\"baseUrl\");';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('const baseUrl = bru.getEnvVar(\"baseUrl\");');\n  });\n\n  it('should translate postman.clearEnvironmentVariable', () => {\n    const code = 'postman.clearEnvironmentVariable(\"tempToken\");';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('bru.deleteEnvVar(\"tempToken\");');\n  });\n\n  it('should handle all environment variable methods together', () => {\n    const code = `\n        // All environment variable methods\n        const envName = pm.environment.name;\n        const hasToken = pm.environment.has(\"token\");\n        const token = pm.environment.get(\"token\");\n        pm.environment.set(\"timestamp\", new Date().toISOString());\n        \n        console.log(\\`Environment: \\${envName}, Has token: \\${hasToken}, Token: \\${token}\\`);\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('const envName = bru.getEnvName();');\n    expect(translatedCode).toContain('const hasToken = bru.getEnvVar(\"token\") !== undefined && bru.getEnvVar(\"token\") !== null;');\n    expect(translatedCode).toContain('const token = bru.getEnvVar(\"token\");');\n    expect(translatedCode).toContain('bru.setEnvVar(\"timestamp\", new Date().toISOString());');\n  });\n\n  // Additional robust tests for environment variables\n  it('should handle environment variables with computed property names', () => {\n    const code = `\n        const prefix = \"api\";\n        const suffix = \"Key\";\n        pm.environment.set(prefix + \"_\" + suffix, \"abc123\");\n        const computedValue = pm.environment.get(prefix + \"_\" + suffix);\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('bru.setEnvVar(prefix + \"_\" + suffix, \"abc123\");');\n    expect(translatedCode).toContain('const computedValue = bru.getEnvVar(prefix + \"_\" + suffix);');\n  });\n\n  it('should handle environment variables in complex object structures', () => {\n    const code = `\n        const config = {\n            baseUrl: pm.environment.get(\"apiUrl\"),\n            headers: {\n                \"Authorization\": \"Bearer \" + pm.environment.get(\"token\"),\n                \"X-Api-Key\": pm.environment.get(\"apiKey\") || \"default-key\"\n            },\n            timeout: parseInt(pm.environment.get(\"timeout\") || \"5000\"),\n            validate: pm.environment.has(\"validateResponses\")\n        };\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('baseUrl: bru.getEnvVar(\"apiUrl\"),');\n    expect(translatedCode).toContain('\"Authorization\": \"Bearer \" + bru.getEnvVar(\"token\"),');\n    expect(translatedCode).toContain('\"X-Api-Key\": bru.getEnvVar(\"apiKey\") || \"default-key\"');\n    expect(translatedCode).toContain('timeout: parseInt(bru.getEnvVar(\"timeout\") || \"5000\"),');\n    expect(translatedCode).toContain('validate: bru.getEnvVar(\"validateResponses\") !== undefined && bru.getEnvVar(\"validateResponses\") !== null');\n  });\n\n  it('should handle environment variables in conditionals correctly', () => {\n    const code = `\n        if (pm.environment.has(\"apiKey\")) {\n            if (pm.environment.get(\"apiKey\").length > 0) {\n                console.log(\"Valid API key exists\");\n            } else {\n                console.log(\"API key is empty\");\n            }\n        } else {\n            console.log(\"No API key defined\");\n        }\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('if (bru.getEnvVar(\"apiKey\") !== undefined && bru.getEnvVar(\"apiKey\") !== null) {');\n    expect(translatedCode).toContain('if (bru.getEnvVar(\"apiKey\").length > 0) {');\n  });\n\n  it('should handle multiple levels of environment variable aliasing', () => {\n    const code = `\n        const env = pm.environment;\n        \n        env.set(\"key\", \"value\");\n        const value = env.get(\"key\");\n        const exists = env.has(\"key\");\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        bru.setEnvVar(\"key\", \"value\");\n        const value = bru.getEnvVar(\"key\");\n        const exists = bru.getEnvVar(\"key\") !== undefined && bru.getEnvVar(\"key\") !== null;\n        `);\n  });\n\n  it('should handle environment variables with dynamic values', () => {\n    const code = `\n        // Generate a timestamp for this request\n        const timestamp = new Date().toISOString();\n        pm.environment.set(\"requestTimestamp\", timestamp);\n        \n        // Generate a unique ID\n        const uniqueId = \"req_\" + Math.random().toString(36).substring(2, 15);\n        pm.environment.set(\"requestId\", uniqueId);\n        \n        // Calculate an expiry time (30 minutes from now)\n        const expiryTime = new Date();\n        expiryTime.setMinutes(expiryTime.getMinutes() + 30);\n        pm.environment.set(\"tokenExpiry\", expiryTime.getTime());\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('bru.setEnvVar(\"requestTimestamp\", timestamp);');\n    expect(translatedCode).toContain('bru.setEnvVar(\"requestId\", uniqueId);');\n    expect(translatedCode).toContain('bru.setEnvVar(\"tokenExpiry\", expiryTime.getTime());');\n  });\n\n  it('should handle environment variables in try-catch blocks', () => {\n    const code = `\n        try {\n            const configStr = pm.environment.get(\"config\");\n            const config = JSON.parse(configStr);\n            console.log(\"Config loaded:\", config.version);\n        } catch (error) {\n            console.error(\"Failed to parse config\");\n            pm.environment.set(\"configError\", error.message);\n        }\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('const configStr = bru.getEnvVar(\"config\");');\n    expect(translatedCode).toContain('bru.setEnvVar(\"configError\", error.message);');\n  });\n\n  it('should handle legacy environment and pm.setEnvironmentVariable together', () => {\n    const code = `\n        // Legacy style\n        postman.setEnvironmentVariable(\"legacyKey\", \"legacyValue\");\n        \n        // Mixed with newer style\n        const value = pm.environment.get(\"anotherKey\");\n        \n        // Another legacy form\n        pm.setEnvironmentVariable(\"thirdKey\", \"thirdValue\");\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('bru.setEnvVar(\"legacyKey\", \"legacyValue\");');\n    expect(translatedCode).toContain('const value = bru.getEnvVar(\"anotherKey\");');\n    expect(translatedCode).toContain('bru.setEnvVar(\"thirdKey\", \"thirdValue\");');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/exec-flow.test.js",
    "content": "import translateCode from '../../../../src/utils/postman-to-bruno-translator';\n\ndescribe('Execution Flow Translation', () => {\n  // Request flow control\n  it('should translate pm.setNextRequest', () => {\n    const code = 'pm.setNextRequest(\"Get User Details\");';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('bru.setNextRequest(\"Get User Details\");');\n  });\n\n  it('should translate pm.execution.skipRequest', () => {\n    const code = 'if (condition) pm.execution.skipRequest();';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('if (condition) bru.runner.skipRequest();');\n  });\n\n  it('should translate pm.execution.setNextRequest(null)', () => {\n    const code = 'pm.execution.setNextRequest(null);';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('bru.runner.stopExecution();');\n  });\n\n  it('should translate pm.execution.setNextRequest(\"null\")', () => {\n    const code = 'pm.execution.setNextRequest(\"null\");';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('bru.runner.stopExecution();');\n  });\n\n  it('should handle pm.execution.setNextRequest with non-null parameters', () => {\n    const code = `\n        // Continue normal flow\n        pm.execution.setNextRequest(\"Get user details\");\n        \n        // With variable\n        const nextReq = \"Update profile\";\n        pm.execution.setNextRequest(nextReq);\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('bru.runner.setNextRequest(\"Get user details\");');\n    expect(translatedCode).toContain('bru.runner.setNextRequest(nextReq);');\n  });\n\n  it('should handle all execution control methods together', () => {\n    const code = `\n        // All execution control methods\n        if (pm.response.code === 401) {\n            pm.execution.skipRequest();\n        } else if (pm.response.code === 500) {\n            pm.execution.setNextRequest(null);\n        } else {\n            pm.setNextRequest(\"Get User Details\");\n        }\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('if (res.getStatus() === 401) {');\n    expect(translatedCode).toContain('bru.runner.skipRequest();');\n    expect(translatedCode).toContain('} else if (res.getStatus() === 500) {');\n    expect(translatedCode).toContain('bru.runner.stopExecution();');\n    expect(translatedCode).toContain('} else {');\n    expect(translatedCode).toContain('bru.setNextRequest(\"Get User Details\");');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/legacy-global-apis.test.js",
    "content": "import translateCode from '../../../../src/utils/postman-to-bruno-translator';\n\ndescribe('Legacy Postman API Translation', () => {\n  describe('handleLegacyGlobalAPIs - No Conflicts', () => {\n    test('should translate responseBody when no user variables exist', () => {\n      const input = `\n        const data = JSON.parse(responseBody);\n`;\n\n      const result = translateCode(input);\n      const expected = `\n        const data = res.getBody();\n`;\n\n      expect(result).toEqual(expected);\n    });\n\n    test('should translate responseHeaders when no user variables exist', () => {\n      const input = `\n        console.log(responseHeaders);\n        const headers = responseHeaders;\n      `;\n\n      const result = translateCode(input);\n\n      expect(result).toContain('res.getHeaders()');\n      expect(result).not.toContain('responseHeaders');\n    });\n\n    test('should translate responseTime when no user variables exist', () => {\n      const input = `\n        console.log(responseTime);\n        const time = responseTime;\n      `;\n\n      const result = translateCode(input);\n\n      expect(result).toContain('res.getResponseTime()');\n      expect(result).not.toContain('responseTime');\n    });\n\n    test('should translate JSON.parse(responseBody) when no user variables exist', () => {\n      const input = `\n        const data = JSON.parse(responseBody);\n        console.log(data);\n      `;\n\n      const result = translateCode(input);\n\n      expect(result).toContain('res.getBody()');\n      expect(result).not.toContain('JSON.parse(responseBody)');\n      expect(result).not.toContain('responseBody');\n    });\n\n    test('should translate JSON.parse(responseBody) usage without assignment when no user variables exist', () => {\n      const input = `\n        console.log(JSON.parse(responseBody));\n      `;\n\n      const result = translateCode(input);\n      const expected = `\n        console.log(res.getBody());\n      `;\n\n      expect(result).toContain(expected);\n    });\n\n    test('should translate all legacy APIs when no conflicts exist', () => {\n      const input = `\n        const data = JSON.parse(responseBody);\n        const headers = responseHeaders;\n        const time = responseTime;\n        \n        console.log(data, headers, time);\n      `;\n\n      const result = translateCode(input);\n\n      expect(result).toContain('res.getBody()');\n      expect(result).toContain('res.getHeaders()');\n      expect(result).toContain('res.getResponseTime()');\n      expect(result).not.toContain('responseBody');\n      expect(result).not.toContain('responseHeaders');\n      expect(result).not.toContain('responseTime');\n    });\n  });\n\n  describe('handleLegacyGlobalAPIs - With Conflicts', () => {\n    test('should NOT translate responseBody when user variable exists', () => {\n      const input = `\n        const responseBody = pm.response.json();\n        console.log(responseBody);\n      `;\n\n      const result = translateCode(input);\n      const expected = `\n        const responseBody = res.getBody();\n        console.log(responseBody);\n      `;\n\n      // pm.response.json() should be transformed to res.getBody() (Postman API transformation)\n      expect(result).toEqual(expected);\n    });\n\n    test('should NOT translate responseHeaders when user variable exists', () => {\n      const input = `\n        const responseHeaders = pm.response.headers;\n        console.log(responseHeaders);\n      `;\n\n      const result = translateCode(input);\n      const expected = `\n        const responseHeaders = res.getHeaders();\n        console.log(responseHeaders);\n      `;\n\n      expect(result).toEqual(expected);\n    });\n\n    test('should NOT translate responseTime when user variable exists', () => {\n      const input = `\n        const responseTime = pm.response.responseTime;\n        console.log(responseTime);\n      `;\n\n      const result = translateCode(input);\n      const expected = `\n        const responseTime = res.getResponseTime();\n        console.log(responseTime);\n      `;\n\n      expect(result).toEqual(expected);\n    });\n\n    test('should NOT translate JSON.parse(responseBody) when user variable exists', () => {\n      const input = `\n        const responseBody = pm.response.json();\n        const data = JSON.parse(responseBody);\n        console.log(data);\n      `;\n\n      const result = translateCode(input);\n      const expected = `\n        const responseBody = res.getBody();\n        const data = JSON.parse(responseBody);\n        console.log(data);\n      `;\n\n      expect(result).toEqual(expected);\n    });\n  });\n\n  describe('handleLegacyGlobalAPIs - Partial Conflicts', () => {\n    test('should translate non-conflicting APIs when some conflicts exist', () => {\n      const input = `\n        const responseBody = pm.response.json();\n        console.log(responseBody);\n        console.log(responseHeaders);\n        console.log(responseTime);\n      `;\n\n      const result = translateCode(input);\n      const expected = `\n        const responseBody = res.getBody();\n        console.log(responseBody);\n        console.log(res.getHeaders());\n        console.log(res.getResponseTime());\n      `;\n\n      expect(result).toEqual(expected);\n    });\n\n    test('should translate JSON.parse(responseBody) only when no conflict exists', () => {\n      const input = `\n        const responseHeaders = pm.response.headers;\n        const data = JSON.parse(responseBody);\n        console.log(responseHeaders);\n      `;\n\n      const result = translateCode(input);\n      const expected = `\n        const responseHeaders = res.getHeaders();\n        const data = res.getBody();\n        console.log(responseHeaders);\n      `;\n\n      expect(result).toEqual(expected);\n    });\n  });\n\n  describe('handleLegacyGlobalAPIs - Edge Cases', () => {\n    test.skip('should handle function parameters with legacy names', () => {\n      const input = `\n        function test(responseBody) {\n          console.log(responseBody);\n          console.log(responseHeaders);\n        }\n      `;\n\n      const result = translateCode(input);\n      const expected = `\n        function test(responseBody) {\n          console.log(responseBody);\n          console.log(res.getHeaders());\n        }\n      `;\n\n      expect(result).toEqual(expected);\n    });\n\n    test('should handle object properties with legacy names', () => {\n      const input = `\n        const config = {\n          responseBody: 'custom',\n          responseHeaders: 'custom'\n        };\n        console.log(responseTime);\n      `;\n\n      const result = translateCode(input);\n\n      const expected = `\n        const config = {\n          responseBody: 'custom',\n          responseHeaders: 'custom'\n        };\n        console.log(res.getResponseTime());\n      `;\n\n      expect(result).toEqual(expected);\n    });\n\n    test('should handle assignments with legacy names', () => {\n      const input = `\n        responseBody = 'new value';\n        responseHeaders = 'new headers';\n        console.log(responseTime);\n      `;\n\n      const result = translateCode(input);\n\n      const expected = `\n        responseBody = 'new value';\n        responseHeaders = 'new headers';\n        console.log(res.getResponseTime());\n      `;\n\n      expect(result).toEqual(expected);\n    });\n\n    test('should handle mixed usage patterns', () => {\n      const input = `\n        const responseBody = pm.response.json();\n        const data = JSON.parse(responseBody);\n        console.log(responseHeaders);\n        console.log(responseTime);\n        \n        function test(data) {\n          console.log(responseBody);\n          console.log(responseHeaders);\n        }\n      `;\n\n      const result = translateCode(input);\n\n      const expected = `\n        const responseBody = res.getBody();\n        const data = JSON.parse(responseBody);\n        console.log(res.getHeaders());\n        console.log(res.getResponseTime());\n        \n        function test(data) {\n          console.log(responseBody);\n          console.log(res.getHeaders());\n        }\n      `;\n\n      expect(result).toEqual(expected);\n    });\n  });\n\n  describe('handleLegacyGlobalAPIs - No Legacy APIs', () => {\n    test('should not modify code when no legacy APIs are present', () => {\n      const input = `\n        const data = { name: 'test' };\n        console.log(data.name);\n      `;\n\n      const result = translateCode(input);\n      const expected = `\n        const data = { name: 'test' };\n        console.log(data.name);\n      `;\n\n      expect(result).toEqual(expected);\n    });\n  });\n\n  describe('responseCode translations', () => {\n    test('should translate responseCode.code', () => {\n      const input = 'const status = responseCode.code;';\n      const result = translateCode(input);\n      expect(result).toBe('const status = res.getStatus();');\n    });\n\n    test('should translate responseCode.name', () => {\n      const input = 'const statusName = responseCode.name;';\n      const result = translateCode(input);\n      expect(result).toBe('const statusName = res.statusText;');\n    });\n\n    test('should translate responseCode.code in conditional', () => {\n      const input = 'if (responseCode.code === 200) { console.log(\"Success\"); }';\n      const result = translateCode(input);\n      expect(result).toBe('if (res.getStatus() === 200) { console.log(\"Success\"); }');\n    });\n\n    test('should translate both responseCode.code and responseCode.name together', () => {\n      const input = `\n        const code = responseCode.code;\n        const name = responseCode.name;\n        console.log(code, name);\n      `;\n      const result = translateCode(input);\n      expect(result).toContain('const code = res.getStatus();');\n      expect(result).toContain('const name = res.statusText;');\n    });\n  });\n\n  describe('postman.getResponseHeader translations', () => {\n    test('should translate postman.getResponseHeader', () => {\n      const input = 'postman.getResponseHeader(\"Content-Type\");';\n      const result = translateCode(input);\n      expect(result).toBe('res.getHeader(\"Content-Type\");');\n    });\n\n    test('should translate postman.getResponseHeader in assignment', () => {\n      const input = 'const contentType = postman.getResponseHeader(\"Content-Type\");';\n      const result = translateCode(input);\n      expect(result).toBe('const contentType = res.getHeader(\"Content-Type\");');\n    });\n\n    test('should translate postman.getResponseHeader with variable argument', () => {\n      const input = 'const headerName = \"Authorization\"; const value = postman.getResponseHeader(headerName);';\n      const result = translateCode(input);\n      expect(result).toContain('res.getHeader(headerName)');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/legacy-tests-syntax.test.js",
    "content": "import translateCode from '../../../../src/utils/postman-to-bruno-translator';\n\ndescribe('Legacy Tests[] Syntax Translation', () => {\n  it('should handle tests[] commands', () => {\n    const code = `\n        tests[\"Status code is 200\"] = pm.response.code === 200;`;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        test(\"Status code is 200\", function() {\n                expect(Boolean(res.getStatus() === 200)).to.be.true;\n        });`);\n  });\n\n  it('should handle tests[] with complex expressions', () => {\n    const code = `\n        tests[\"Response has valid data\"] = pm.response.json().data && pm.response.json().data.length > 0;`;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        test(\"Response has valid data\", function() {\n                expect(Boolean(res.getBody().data && res.getBody().data.length > 0)).to.be.true;\n        });`);\n  });\n\n  it('should handle tests[] with string equality', () => {\n    const code = `\n        tests[\"Content-Type is application/json\"] = pm.response.headers.get(\"Content-Type\") === \"application/json\";`;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        test(\"Content-Type is application/json\", function() {\n                expect(Boolean(res.getHeader(\"Content-Type\") === \"application/json\")).to.be.true;\n        });`);\n  });\n\n  it('should handle tests[] with function calls', () => {\n    const code = `\n        tests[\"Response time is acceptable\"] = pm.response.responseTime < 500;`;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        test(\"Response time is acceptable\", function() {\n                expect(Boolean(res.getResponseTime() < 500)).to.be.true;\n        });`);\n  });\n\n  it('should handle tests[] with variable references', () => {\n    const code = `\n        const expectedStatus = 201;\n        tests[\"Status code is correct\"] = pm.response.code === expectedStatus;`;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        const expectedStatus = 201;\n        test(\"Status code is correct\", function() {\n                expect(Boolean(res.getStatus() === expectedStatus)).to.be.true;\n        });`);\n  });\n\n  it('should handle multiple tests[] statements', () => {\n    const code = `\n        tests[\"Status code is 200\"] = pm.response.code === 200;\n        tests[\"Response has data\"] = pm.response.json().hasOwnProperty(\"data\");`;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        test(\"Status code is 200\", function() {\n                expect(Boolean(res.getStatus() === 200)).to.be.true;\n        });\n        test(\"Response has data\", function() {\n                expect(Boolean(res.getBody().hasOwnProperty(\"data\"))).to.be.true;\n        });`);\n  });\n\n  it('should handle tests[] with special characters in name', () => {\n    const code = `\n        tests[\"Special characters: !@#$%^&*()\"] = true;`;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        test(\"Special characters: !@#$%^&*()\", function() {\n                expect(Boolean(true)).to.be.true;\n        });`);\n  });\n\n  it('should handle tests[] with pm.environment variables', () => {\n    const code = `\n        tests[\"Response matches environment variable\"] = pm.response.json().id === pm.environment.get(\"expectedId\");`;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        test(\"Response matches environment variable\", function() {\n                expect(Boolean(res.getBody().id === bru.getEnvVar(\"expectedId\"))).to.be.true;\n        });`);\n  });\n\n  it('should handle nested pm objects in tests[] assignments', () => {\n    const code = `\n        tests[\"Authentication header is present\"] = pm.request.headers.has(\"Authorization\");\n        tests[\"Data count is correct\"] = pm.response.json().items.length === pm.variables.get(\"expectedCount\");\n        `;\n    const translatedCode = translateCode(code);\n\n    // The exact translation might vary depending on implementation details,\n    // but we can check for key transformations\n    expect(translatedCode).toContain('test(\"Authentication header is present\"');\n    expect(translatedCode).toContain('test(\"Data count is correct\"');\n    expect(translatedCode).toContain('res.getBody().items.length === bru.getVar(\"expectedCount\")');\n  });\n\n  // Additional robust tests for legacy tests[] syntax\n  it('should handle tests[] with complex boolean expressions', () => {\n    const code = `\n        tests[\"Complex validation\"] = (pm.response.code >= 200 && pm.response.code < 300) || \n                                     (pm.response.json().success === true && pm.response.json().data !== null);`;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('test(\"Complex validation\", function() {');\n    expect(translatedCode).toContain('expect(Boolean((res.getStatus() >= 200 && res.getStatus() < 300) ||');\n    expect(translatedCode).toContain('(res.getBody().success === true && res.getBody().data !== null))).to.be.true;');\n  });\n\n  it('should handle tests[] with array methods', () => {\n    const code = `\n        tests[\"All items have an ID\"] = pm.response.json().items.every(item => item.hasOwnProperty('id'));\n        tests[\"Has premium item\"] = pm.response.json().items.some(item => item.type === 'premium');`;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('test(\"All items have an ID\", function() {');\n    expect(translatedCode).toContain('expect(Boolean(res.getBody().items.every(item => item.hasOwnProperty(\\'id\\')))).to.be.true;');\n    expect(translatedCode).toContain('test(\"Has premium item\", function() {');\n    expect(translatedCode).toContain('expect(Boolean(res.getBody().items.some(item => item.type === \\'premium\\'))).to.be.true;');\n  });\n\n  it('should handle tests[] with template literals in the name', () => {\n    const code = `\n        const endpoint = \"users\";\n        tests[\\`Endpoint \\${endpoint} returns valid response\\`] = pm.response.code === 200;`;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('const endpoint = \"users\";');\n    expect(translatedCode).toContain('test(`Endpoint ${endpoint} returns valid response`, function() {');\n    expect(translatedCode).toContain('expect(Boolean(res.getStatus() === 200)).to.be.true;');\n  });\n\n  it('should handle tests[] with deep property access', () => {\n    const code = `\n        tests[\"User has admin role\"] = pm.response.json().user && \n                                     pm.response.json().user.roles && \n                                     pm.response.json().user.roles.includes('admin');`;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('test(\"User has admin role\", function() {');\n    expect(translatedCode).toContain('expect(Boolean(res.getBody().user &&');\n    expect(translatedCode).toContain('res.getBody().user.roles &&');\n    expect(translatedCode).toContain('res.getBody().user.roles.includes(\\'admin\\'))).to.be.true;');\n  });\n\n  it('should handle tests[] with JSON schema validation patterns', () => {\n    const code = `\n        const schema = {\n            type: \"object\",\n            required: [\"id\", \"name\"],\n            properties: {\n                id: { type: \"string\" },\n                name: { type: \"string\" }\n            }\n        };\n        \n        const data = pm.response.json();\n        \n        // Basic schema validation patterns\n        tests[\"Has required fields\"] = data.hasOwnProperty('id') && data.hasOwnProperty('name');\n        tests[\"ID is string\"] = typeof data.id === 'string';\n        tests[\"Name is string\"] = typeof data.name === 'string';`;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('const schema = {');\n    expect(translatedCode).toContain('type: \"object\",');\n    expect(translatedCode).toContain('required: [\"id\", \"name\"],');\n    expect(translatedCode).toContain('const data = res.getBody();');\n    expect(translatedCode).toContain('test(\"Has required fields\", function() {');\n    expect(translatedCode).toContain('expect(Boolean(data.hasOwnProperty(\\'id\\') && data.hasOwnProperty(\\'name\\'))).to.be.true;');\n    expect(translatedCode).toContain('test(\"ID is string\", function() {');\n    expect(translatedCode).toContain('expect(Boolean(typeof data.id === \\'string\\')).to.be.true;');\n  });\n\n  it('should handle tests[] within conditional blocks', () => {\n    const code = `\n        const data = pm.response.json();\n        \n        if (pm.response.code === 200) {\n            tests[\"Success response has data\"] = data.hasOwnProperty('items');\n            \n            if (data.items.length > 0) {\n                tests[\"First item has ID\"] = data.items[0].hasOwnProperty('id');\n            }\n        } else {\n            tests[\"Error response has message\"] = data.hasOwnProperty('message');\n        }`;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('const data = res.getBody();');\n    expect(translatedCode).toContain('if (res.getStatus() === 200) {');\n    expect(translatedCode).toContain('test(\"Success response has data\", function() {');\n    expect(translatedCode).toContain('expect(Boolean(data.hasOwnProperty(\\'items\\'))).to.be.true;');\n    expect(translatedCode).toContain('if (data.items.length > 0) {');\n    expect(translatedCode).toContain('test(\"First item has ID\", function() {');\n    expect(translatedCode).toContain('expect(Boolean(data.items[0].hasOwnProperty(\\'id\\'))).to.be.true;');\n    expect(translatedCode).toContain('} else {');\n    expect(translatedCode).toContain('test(\"Error response has message\", function() {');\n    expect(translatedCode).toContain('expect(Boolean(data.hasOwnProperty(\\'message\\'))).to.be.true;');\n  });\n\n  it('should handle tests[] with combination of legacy and modern styles', () => {\n    const code = `\n        // Legacy style\n        tests[\"Status code is 200\"] = pm.response.code === 200;\n        \n        // Modern style\n        pm.test(\"Response has valid data\", function() {\n            const json = pm.response.json();\n            pm.expect(json).to.be.an('object');\n            pm.expect(json.items).to.be.an('array');\n            \n            // Mix by using tests[] inside pm.test\n            tests[\"All items have price\"] = json.items.every(item => item.hasOwnProperty('price'));\n        });`;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('test(\"Status code is 200\", function() {');\n    expect(translatedCode).toContain('expect(Boolean(res.getStatus() === 200)).to.be.true;');\n    expect(translatedCode).toContain('test(\"Response has valid data\", function() {');\n    expect(translatedCode).toContain('const json = res.getBody();');\n    expect(translatedCode).toContain('expect(json).to.be.an(\\'object\\');');\n    expect(translatedCode).toContain('expect(json.items).to.be.an(\\'array\\');');\n    expect(translatedCode).toContain('test(\"All items have price\", function() {');\n    expect(translatedCode).toContain('expect(Boolean(json.items.every(item => item.hasOwnProperty(\\'price\\')))).to.be.true;');\n  });\n\n  it('should handle complex real-world tests[] example', () => {\n    const code = `\n        // Parse response\n        const response = pm.response.json();\n        \n        // Basic response validation\n        tests[\"Status code is 200\"] = pm.response.code === 200;\n        tests[\"Response is valid JSON\"] = response !== null && typeof response === 'object';\n        \n        // Check headers\n        tests[\"Has content-type header\"] = pm.response.headers.has(\"Content-Type\");\n        tests[\"Content-Type is JSON\"] = pm.response.headers.get(\"Content-Type\").includes(\"application/json\");\n        \n        // Validate against expected values\n        const expectedItems = parseInt(pm.environment.get(\"expectedItemCount\"));\n        tests[\"Has correct number of items\"] = response.items.length === expectedItems;\n        \n        // Check for required fields on all items\n        const requiredFields = [\"id\", \"name\", \"price\", \"category\"];\n        tests[\"All items have required fields\"] = response.items.every(item => {\n            return requiredFields.every(field => item.hasOwnProperty(field));\n        });\n        \n        // Validate specific business rules\n        tests[\"No items with zero price\"] = response.items.every(item => parseFloat(item.price) > 0);\n        tests[\"Has at least one featured item\"] = response.items.some(item => item.featured === true);\n        \n        // If we find a specific item we're looking for, save its ID for later\n        const targetItem = response.items.find(item => item.name === pm.variables.get(\"targetItemName\"));\n        if (targetItem) {\n            pm.environment.set(\"targetItemId\", targetItem.id);\n            tests[\"Found target item\"] = true;\n        }`;\n    const translatedCode = translateCode(code);\n\n    // Check key transformations\n    expect(translatedCode).toContain('const response = res.getBody();');\n    expect(translatedCode).toContain('test(\"Status code is 200\", function() {');\n    expect(translatedCode).toContain('expect(Boolean(res.getStatus() === 200)).to.be.true;');\n    expect(translatedCode).toContain('test(\"Has content-type header\", function() {');\n    expect(translatedCode).toContain('expect(Boolean(res.getHeaders().has(\"Content-Type\"))).to.be.true;');\n    expect(translatedCode).toContain('test(\"Content-Type is JSON\", function() {');\n    expect(translatedCode).toContain('expect(Boolean(res.getHeader(\"Content-Type\").includes(\"application/json\"))).to.be.true;');\n    expect(translatedCode).toContain('const expectedItems = parseInt(bru.getEnvVar(\"expectedItemCount\"));');\n    expect(translatedCode).toContain('test(\"Has correct number of items\", function() {');\n    expect(translatedCode).toContain('expect(Boolean(response.items.length === expectedItems)).to.be.true;');\n    expect(translatedCode).toContain('const targetItem = response.items.find(item => item.name === bru.getVar(\"targetItemName\"));');\n    expect(translatedCode).toContain('bru.setEnvVar(\"targetItemId\", targetItem.id);');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/multiline-syntax.test.js",
    "content": "import translateCode from '../../../../src/utils/postman-to-bruno-translator';\n\ndescribe('Multiline Syntax Handling', () => {\n  it('should handle basic multiline variable syntax with indentation', () => {\n    const code = `\n    const userId = pm.variables\n                            .get(\"userId\");\n    pm.variables\n                            .set(\"timestamp\", new Date().toISOString());\n    const hasToken = pm.variables\n                            .has(\"token\");\n    `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n    const userId = bru.getVar(\"userId\");\n    bru.setVar(\"timestamp\", new Date().toISOString());\n    const hasToken = bru.hasVar(\"token\");\n    `);\n  });\n\n  it('should handle multiline environment variable syntax', () => {\n    const code = `\n    const baseUrl = pm\n                .environment\n                .get(\"baseUrl\");\n    pm\n                .environment\n                .set(\"requestTime\", Date.now());\n    `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n    const baseUrl = bru.getEnvVar(\"baseUrl\");\n    bru.setEnvVar(\"requestTime\", Date.now());\n    `);\n  });\n\n  it('should handle multiline collection variable get syntax', () => {\n    const code = `\n    const apiKey = pm.collectionVariables\n                            .get(\"apiKey\");\n    `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('const apiKey = bru.getCollectionVar(\"apiKey\")');\n  });\n\n  // TODO: Restore once UI update fixes are live for setCollectionVar\n  it.skip('should handle multiline collection variable set syntax', () => {\n    const code = `\n    pm.collectionVariables\n                            .set(\"lastRun\", new Date().toISOString());\n    `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('bru.setCollectionVar(\"lastRun\", new Date().toISOString())');\n  });\n\n  it('should handle complex environment.has transformation with multiline syntax', () => {\n    const code = `\n    if (pm.environment\n                    .has(\"apiKey\")) {\n      console.log(\"API Key exists\");\n    }\n    `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n    if (bru.getEnvVar(\"apiKey\") !== undefined && bru.getEnvVar(\"apiKey\") !== null) {\n      console.log(\"API Key exists\");\n    }\n    `);\n  });\n\n  it('should handle response.to.have.status with multiline formatting', () => {\n    const code = `\n    pm.test(\"Status code is correct\", function() {\n      pm\n        .response\n          .to\n            .have\n              .status(200);\n    });\n    `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200)');\n  });\n\n  it('should handle response.to.have.header with multiline formatting', () => {\n    const code = `\n    pm.test(\"Content type is present\", function() {\n      pm\n        .response\n          .to\n            .have\n              .header(\"content-type\");\n    });\n    `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(\"content-type\".toLowerCase())');\n  });\n\n  it('should handle response properties with multiline syntax', () => {\n    const code = `\n    const responseBody = pm\n                          .response\n                            .json();\n    const responseText = pm\n                          .response\n                            .text;\n    const responseTime = pm\n                          .response\n                            .responseTime;\n    `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('const responseBody = res.getBody()');\n    expect(translatedCode).toContain('const responseText = ');\n    expect(translatedCode).toContain('const responseTime = res.getResponseTime()');\n  });\n\n  it('should handle execution flow control with multiline syntax', () => {\n    const code = `\n    // Stop execution\n    pm\n      .execution\n        .setNextRequest(null);\n    \n    // Continue to next request\n    pm\n      .execution\n        .setNextRequest(\"Next API Call\");\n    `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('// Stop execution');\n    expect(translatedCode).toContain('// Continue to next request');\n    expect(translatedCode).toContain('bru.runner.stopExecution()');\n    expect(translatedCode).toContain('bru.runner.setNextRequest(\"Next API Call\")');\n  });\n\n  it('should handle mixed normal and multiline syntax in the same code', () => {\n    const code = `\n    // Normal syntax\n    const normalVar = pm.variables.get(\"normal\");\n    \n    // Multiline syntax\n    const multilineVar = pm.variables\n                            .get(\"multiline\");\n    \n    // Normal syntax again\n    pm.variables.set(\"normalSet\", \"value\");\n    \n    // Multiline syntax again\n    pm.variables\n                            .set(\"multilineSet\", \"value\");\n    `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n    // Normal syntax\n    const normalVar = bru.getVar(\"normal\");\n    \n    // Multiline syntax\n    const multilineVar = bru.getVar(\"multiline\");\n    \n    // Normal syntax again\n    bru.setVar(\"normalSet\", \"value\");\n    \n    // Multiline syntax again\n    bru.setVar(\"multilineSet\", \"value\");\n    `);\n  });\n\n  it('should handle complex multiline method chaining', () => {\n    const code = `\n    pm\n      .test(\"Test with chaining\", function() {\n        pm\n          .response\n            .to\n              .have\n                .status(200);\n        \n        const body = pm\n                      .response\n                        .json();\n        \n        pm\n          .expect(body)\n            .to\n              .have\n                .property('success')\n                  .equal(true);\n      });\n    `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('test(\"Test with chaining\", function() {');\n    expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200)');\n    expect(translatedCode).toContain('const body = res.getBody()');\n    expect(translatedCode).toContain('.property(\\'success\\')');\n    expect(translatedCode).toContain('.equal(true)');\n  });\n\n  it('should handle a comprehensive script with various multiline formats', () => {\n    const code = `\n    // This comprehensive script tests different multiline styles and whitespace variations\n\n    // Environment variables with different formatting styles\n    const baseUrl = pm.environment.get(\"baseUrl\");\n    const apiKey = pm\n      .environment\n        .get(\"apiKey\");\n    const userId = pm.environment\n                      .get(\"userId\");\n\n    // Mix of variable styles\n    pm.variables.set(\"testId\", \"test-\" + Date.now());\n    pm\n      .variables\n        .set(\"timestamp\", new Date().toISOString());\n\n    // Collection variables with inconsistent spacing\n    pm.collectionVariables\n      .set(\"lastRun\", new Date());\n\n    // Complex conditionals with multiline expressions\n    if (pm\n          .environment\n            .has(\"apiKey\") &&\n       pm.variables.has(\"testId\")) {\n\n      // Testing response with mixed syntax styles\n      pm.test(\"Response validation\", function() {\n        // Normal style\n        pm.response.to.have.status(200);\n\n        // Multiline with different indentation\n        pm\n          .response\n            .to\n              .have\n                .header(\"content-type\");\n\n        pm.response\n                .to.have\n                      .jsonBody(\"success\", true);\n\n        // Extreme indentation\n        pm\n                                .response\n                                              .to\n                                                        .not\n                                                                  .have\n                                                                            .jsonBody(\"error\");\n      });\n\n      // Flow control with mixed styles\n      if (pm.response.code === 401) {\n        pm.execution.setNextRequest(null);\n      } else {\n        pm\n          .execution\n            .setNextRequest(\"Next API Call\");\n      }\n    }\n    `;\n\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('const baseUrl = bru.getEnvVar(\"baseUrl\")');\n    expect(translatedCode).toContain('const apiKey = bru.getEnvVar(\"apiKey\")');\n    expect(translatedCode).toContain('const userId = bru.getEnvVar(\"userId\")');\n\n    // Check variables translations\n    expect(translatedCode).toContain('bru.setVar(\"testId\", \"test-\" + Date.now())');\n    expect(translatedCode).toContain('bru.setVar(\"timestamp\", new Date().toISOString())');\n\n    // Check complex conditionals\n    expect(translatedCode).toContain('if (bru.getEnvVar(\"apiKey\") !== undefined && bru.getEnvVar(\"apiKey\") !== null &&');\n    expect(translatedCode).toContain('bru.hasVar(\"testId\"))');\n\n    // Check response testing\n    expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200)');\n    expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(\"content-type\".toLowerCase())');\n\n    // Check flow control\n    expect(translatedCode).toContain('if (res.getStatus() === 401)');\n    expect(translatedCode).toContain('bru.runner.stopExecution()');\n    expect(translatedCode).toContain('bru.runner.setNextRequest(\"Next API Call\")');\n  });\n\n  // TODO: Restore once UI update fixes are live for setCollectionVar\n  it.skip('should translate multiline pm.collectionVariables.set in comprehensive script', () => {\n    const code = `\n    pm.collectionVariables\n      .set(\"lastRun\", new Date());\n    `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('bru.setCollectionVar(\"lastRun\", new Date())');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/postman-references.test.js",
    "content": "import translateCode from '../../../../src/utils/postman-to-bruno-translator';\n\ndescribe('Postman to PM References Conversion', () => {\n  // Basic conversions\n  it('should convert basic postman references to pm', () => {\n    const code = 'postman.setEnvironmentVariable(\"key\", \"value\");';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('bru.setEnvVar(\"key\", \"value\");');\n    // The key part is that it should convert postman.* to pm.* internally before\n    // translating to bru.* APIs\n  });\n\n  it('should convert postman variable access to pm', () => {\n    const code = 'const value = postman.getEnvironmentVariable(\"key\");';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('const value = bru.getEnvVar(\"key\");');\n  });\n\n  it('should handle postman variable assignments', () => {\n    const code = `\n    const envVar = postman.environment.get(\"apiKey\");\n    const baseUrl = postman.environment.get(\"baseUrl\");\n    `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('const envVar = bru.getEnvVar(\"apiKey\");');\n    expect(translatedCode).toContain('const baseUrl = bru.getEnvVar(\"baseUrl\");');\n  });\n\n  // More complex patterns\n  it('should handle mixed postman and pm references in the same code', () => {\n    const code = `\n    // Using both postman and pm APIs\n    const apiKey = postman.environment.get(\"apiKey\");\n    const baseUrl = pm.environment.get(\"baseUrl\");\n    \n    // Using both formats in a test\n    postman.test(\"Status code is 200\", function() {\n        pm.expect(pm.response.code).to.equal(200);\n    });\n    `;\n\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('const apiKey = bru.getEnvVar(\"apiKey\");');\n    expect(translatedCode).toContain('const baseUrl = bru.getEnvVar(\"baseUrl\");');\n    expect(translatedCode).toContain('test(\"Status code is 200\", function() {');\n    expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);');\n  });\n\n  it('should handle postman references in object destructuring', () => {\n    const code = `\n    const { environment } = postman;\n    environment.set(\"key\", \"value\");\n    `;\n\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('bru.setEnvVar(\"key\", \"value\");');\n  });\n\n  // Complex control flows\n  it('should handle postman references in control flow statements', () => {\n    const code = `\n    if (postman.environment.get(\"isProduction\") === \"true\") {\n        const apiUrl = postman.environment.get(\"prodUrl\");\n        postman.setNextRequest(\"Production Flow\");\n    } else {\n        const apiUrl = postman.environment.get(\"devUrl\");\n        postman.setNextRequest(\"Development Flow\");\n    }\n    `;\n\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('if (bru.getEnvVar(\"isProduction\") === \"true\") {');\n    expect(translatedCode).toContain('const apiUrl = bru.getEnvVar(\"prodUrl\");');\n    expect(translatedCode).toContain('bru.setNextRequest(\"Production Flow\");');\n    expect(translatedCode).toContain('const apiUrl = bru.getEnvVar(\"devUrl\");');\n    expect(translatedCode).toContain('bru.setNextRequest(\"Development Flow\");');\n  });\n\n  // Legacy response handling\n  it('should handle legacy postman response methods', () => {\n    const code = `\n    // Using legacy response handling\n    const responseCode = postman.response.code;\n    const responseBody = postman.response.json();\n    \n    // Set environment variables with response data\n    postman.setEnvironmentVariable(\"lastResponseCode\", responseCode);\n    `;\n\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('const responseCode = res.getStatus();');\n    expect(translatedCode).toContain('const responseBody = res.getBody();');\n    expect(translatedCode).toContain('bru.setEnvVar(\"lastResponseCode\", responseCode);');\n  });\n\n  // Postman in string literals should be untouched\n  it('should not convert postman references in string literals', () => {\n    const code = `\n    console.log(\"This is a pm script\");\n    const message = \"We're using pm to test our API\";\n    `;\n\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('console.log(\"This is a pm script\");');\n    expect(translatedCode).toContain('const message = \"We\\'re using pm to test our API\";');\n  });\n\n  // Complex example with aliasing\n  it('should handle complex postman reference patterns with aliasing', () => {\n    const code = `\n    // Aliasing the postman object\n    const env = postman.environment;\n    const code = postman.code;\n    \n    // Using the alias\n    const apiKey = env.get(\"apiKey\");\n    const userId = env.get(\"userId\");\n    \n    // Using alias in tests\n    postman.test(\"Response is valid\", function() {\n        postman.expect(code).to.equal(200);\n    });\n    `;\n\n    const translatedCode = translateCode(code);\n    // Should handle the aliases properly\n    expect(translatedCode).toContain('const apiKey = bru.getEnvVar(\"apiKey\");');\n    expect(translatedCode).toContain('const userId = bru.getEnvVar(\"userId\");');\n    expect(translatedCode).toContain('test(\"Response is valid\", function() {');\n    expect(translatedCode).toContain('expect(code).to.equal(200);');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/request.test.js",
    "content": "import translateCode from '../../../../src/utils/postman-to-bruno-translator';\n\ndescribe('Request Translation', () => {\n  it('should translate pm.request.url', () => {\n    const code = 'const requestUrl = pm.request.url;';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('const requestUrl = req.getUrl();');\n  });\n\n  it('should translate pm.request.method', () => {\n    const code = 'const method = pm.request.method;';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('const method = req.getMethod();');\n  });\n\n  it('should translate pm.request.headers', () => {\n    const code = 'const headers = pm.request.headers;';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('const headers = req.getHeaders();');\n  });\n\n  it('should translate pm.request.body', () => {\n    const code = 'const body = pm.request.body;';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('const body = req.getBody();');\n  });\n\n  it('should translate pm.response.statusText', () => {\n    const code = 'const statusText = pm.response.statusText;';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('const statusText = res.statusText;');\n  });\n\n  it('should translate multiple request methods in one block', () => {\n    const code = `\n        const url = pm.request.url;\n        const method = pm.request.method;\n        const headers = pm.request.headers;\n        const body = pm.request.body;\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        const url = req.getUrl();\n        const method = req.getMethod();\n        const headers = req.getHeaders();\n        const body = req.getBody();\n        `);\n  });\n\n  it('should handle request and response properties together', () => {\n    const code = `\n        // Get request data\n        const url = pm.request.url;\n        const method = pm.request.method;\n        \n        // Get response data\n        const statusCode = pm.response.code;\n        const statusText = pm.response.statusText;\n        \n        // Verify expectations\n        pm.test(\"Request was made correctly\", function() {\n            pm.expect(method).to.equal(\"POST\");\n            pm.expect(url).to.include(\"/api/items\");\n        });\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('const url = req.getUrl();');\n    expect(translatedCode).toContain('const method = req.getMethod();');\n    expect(translatedCode).toContain('const statusCode = res.getStatus();');\n    expect(translatedCode).toContain('const statusText = res.statusText;');\n    expect(translatedCode).toContain('test(\"Request was made correctly\", function() {');\n    expect(translatedCode).toContain('expect(method).to.equal(\"POST\");');\n    expect(translatedCode).toContain('expect(url).to.include(\"/api/items\");');\n  });\n\n  it('should handle request properties in conditional blocks', () => {\n    const code = `\n        if (pm.request.method === \"POST\") {\n            console.log(\"This is a POST request to \" + pm.request.url);\n            pm.test(\"Request has correct content-type\", function() {\n                pm.expect(pm.request.headers.has(\"Content-Type\")).to.be.true;\n            });\n        }\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('if (req.getMethod() === \"POST\") {');\n    expect(translatedCode).toContain('console.log(\"This is a POST request to \" + req.getUrl());');\n    expect(translatedCode).toContain('test(\"Request has correct content-type\", function() {');\n    // Note: The expectation for headers.has might be transformed differently\n    // depending on how complex transformations are handled\n  });\n\n  it('should handle request data extraction and variable setting', () => {\n    const code = `\n        // Extract request data\n        const requestData = pm.request.body;\n        const contentType = pm.request.headers.get(\"Content-Type\");\n        \n        // Save for later use\n        pm.variables.set(\"lastRequestBody\", JSON.stringify(requestData));\n        pm.environment.set(\"lastContentType\", contentType);\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('const requestData = req.getBody();');\n    expect(translatedCode).toContain('bru.setVar(\"lastRequestBody\", JSON.stringify(requestData));');\n    expect(translatedCode).toContain('bru.setEnvVar(\"lastContentType\", contentType);');\n  });\n\n  it('should translate legacy request.* properties', () => {\n    const code = `\n        const url = request.url;\n        const method = request.method;\n        const body = request.body;\n        const name = request.name;\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        const url = req.getUrl();\n        const method = req.getMethod();\n        const body = req.getBody();\n        const name = req.getName();\n        `);\n  });\n\n  // --- pm.request.headers.add and upsert ---------------------------\n  it('should translate pm.request.headers.add with object argument', () => {\n    const code = 'pm.request.headers.add({key: \"Authorization\", value: \"Bearer token\"});';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('req.setHeader(\"Authorization\", \"Bearer token\");');\n  });\n\n  it('should translate pm.request.headers.upsert with object argument', () => {\n    const code = 'pm.request.headers.upsert({key: \"Content-Type\", value: \"application/json\"});';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('req.setHeader(\"Content-Type\", \"application/json\");');\n  });\n\n  it('should translate pm.request.headers.add with quoted key property', () => {\n    const code = 'pm.request.headers.add({\"key\": \"X-Custom-Header\", \"value\": \"custom-value\"});';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('req.setHeader(\"X-Custom-Header\", \"custom-value\");');\n  });\n\n  it('should translate pm.request.headers.upsert with variable values', () => {\n    const code = 'const headerName = \"Authorization\"; const headerValue = \"Bearer \" + token; pm.request.headers.upsert({key: headerName, value: headerValue});';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('req.setHeader(headerName, headerValue)');\n  });\n\n  it('should translate multiple headers.add calls', () => {\n    const code = `\n        pm.request.headers.add({key: \"Authorization\", value: \"Bearer token\"});\n        pm.request.headers.add({key: \"Content-Type\", value: \"application/json\"});\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('req.setHeader(\"Authorization\", \"Bearer token\")');\n    expect(translatedCode).toContain('req.setHeader(\"Content-Type\", \"application/json\")');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/response.test.js",
    "content": "import translateCode from '../../../../src/utils/postman-to-bruno-translator';\n\ndescribe('Response Translation', () => {\n  // Basic response property tests\n  it('should translate pm.response.json', () => {\n    const code = 'const jsonData = pm.response.json();';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('const jsonData = res.getBody();');\n  });\n\n  it('should translate pm.response.code', () => {\n    const code = 'if (pm.response.code === 200) { console.log(\"Success\"); }';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('if (res.getStatus() === 200) { console.log(\"Success\"); }');\n  });\n\n  it('should translate pm.response.text', () => {\n    const code = 'const responseText = pm.response.text();';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('const responseText = JSON.stringify(res.getBody());');\n  });\n\n  it('should translate pm.response.responseTime', () => {\n    const code = 'console.log(\"Response time:\", pm.response.responseTime);';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('console.log(\"Response time:\", res.getResponseTime());');\n  });\n\n  it('should translate pm.response.statusText', () => {\n    const code = 'console.log(\"Status text:\", pm.response.statusText);';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('console.log(\"Status text:\", res.statusText);');\n  });\n\n  it('should translate pm.response.headers', () => {\n    const code = 'console.log(\"Headers:\", pm.response.headers);';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('console.log(\"Headers:\", res.getHeaders());');\n  });\n\n  // Complex response transformations\n  it('should transform pm.response.to.have.status', () => {\n    const code = 'pm.response.to.have.status(201);';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('expect(res.getStatus()).to.equal(201);');\n  });\n\n  it('should transform pm.response.to.have.header with single argument', () => {\n    const code = 'pm.response.to.have.header(\"Content-Type\");';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('expect(res.getHeaders()).to.have.property(\"Content-Type\".toLowerCase());');\n  });\n\n  it('should transform multiple pm.response.to.have.header statements', () => {\n    const code = `\n        pm.response.to.have.header(\"Content-Type\", \"application/json\");\n        pm.response.to.have.header(\"Cache-Control\", \"no-cache\");\n        `;\n    const translatedCode = translateCode(code);\n\n    // Check for the existence of all four assertions (two pairs)\n    expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(\"Content-Type\".toLowerCase(), \"application/json\");');\n    expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(\"Cache-Control\".toLowerCase(), \"no-cache\");');\n  });\n\n  it('should transform pm.response.to.have.header inside control structures', () => {\n    const code = `\n        if (pm.response.code === 200) {\n            pm.response.to.have.header(\"Content-Type\", \"application/json\");\n        }\n        `;\n    const translatedCode = translateCode(code);\n\n    // The assertions should be inside the if block\n    expect(translatedCode).toContain('if (res.getStatus() === 200) {');\n    expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(\"Content-Type\".toLowerCase(), \"application/json\");');\n  });\n\n  it('should transform pm.response.to.have.header with variable parameters', () => {\n    const code = `\n        const headerName = \"Content-Type\";\n        const expectedValue = \"application/json\";\n        pm.response.to.have.header(headerName, expectedValue);\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('const headerName = \"Content-Type\";');\n    expect(translatedCode).toContain('const expectedValue = \"application/json\";');\n    expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(headerName.toLowerCase(), expectedValue);');\n  });\n\n  // Response aliases tests\n  it('should handle response aliases', () => {\n    const code = `\n        const response = pm.response;\n        const status = response.status;\n        const body = response.json();\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        const status = res.statusText;\n        const body = res.getBody();\n        `);\n  });\n\n  // Response to.have.status with different formats\n  it('should handle pm.response.to.have.status with different status codes', () => {\n    const code = `\n        // Test different status codes\n        pm.response.to.have.status(200); // OK\n        pm.response.to.have.status(201); // Created\n        pm.response.to.have.status(400); // Bad Request\n        pm.response.to.have.status(404); // Not Found\n        pm.response.to.have.status(500); // Server Error\n        \n        // With variables\n        const expectedStatus = 200;\n        pm.response.to.have.status(expectedStatus);\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);');\n    expect(translatedCode).toContain('expect(res.getStatus()).to.equal(201);');\n    expect(translatedCode).toContain('expect(res.getStatus()).to.equal(400);');\n    expect(translatedCode).toContain('expect(res.getStatus()).to.equal(404);');\n    expect(translatedCode).toContain('expect(res.getStatus()).to.equal(500);');\n    expect(translatedCode).toContain('expect(res.getStatus()).to.equal(expectedStatus);');\n  });\n\n  // Alias for pm.response.to.have.status\n  it('should handle pm.response.to.have.status alias', () => {\n    const code = `\n        const resp = pm.response;\n        resp.to.have.status(200);\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        expect(res.getStatus()).to.equal(200);\n        `);\n  });\n\n  it('should handle pm.response.to.have.header alias', () => {\n    const code = `\n        const resp = pm.response;\n        resp.to.have.header(\"Content-Type\");\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        expect(res.getHeaders()).to.have.property(\"Content-Type\".toLowerCase());\n        `);\n  });\n\n  it('should handle pm.response.to.have.header alias with value check', () => {\n    const code = `\n        const resp = pm.response;\n        resp.to.have.header(\"Content-Type\", \"application/json\");\n        `;\n    const translatedCode = translateCode(code);\n\n    // Check for both assertions when using an alias\n    expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(\"Content-Type\".toLowerCase(), \"application/json\");');\n  });\n\n  it('should translate response.status', () => {\n    const code = `\n        const resp = pm.response;\n        const statusCode = resp.status;\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        const statusCode = res.statusText;\n        `);\n  });\n\n  it('should translate response.body', () => {\n    const code = `\n        const resp = pm.response;\n        const responseBody = resp.json();\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        const responseBody = res.getBody();\n        `);\n  });\n\n  it('should translate response.headers', () => {\n    const code = `\n        const resp = pm.response;\n        const headers = resp.headers;\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        const headers = res.getHeaders();\n        `);\n  });\n\n  it('should translate pm.response.statusText', () => {\n    const code = `\n        const resp = pm.response;\n        const statusText = resp.statusText;\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        const statusText = res.statusText;\n        `);\n  });\n\n  it('should translate multiple response methods in one block', () => {\n    const code = `\n        const resp = pm.response;\n        const statusCode = resp.code;\n        const statusText = resp.statusText;\n        const jsonData = resp.json();\n        const responseText = resp.text();\n        const time = resp.responseTime;\n        resp.to.have.status(200);\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        const statusCode = res.getStatus();\n        const statusText = res.statusText;\n        const jsonData = res.getBody();\n        const responseText = JSON.stringify(res.getBody());\n        const time = res.getResponseTime();\n        expect(res.getStatus()).to.equal(200);\n        `);\n  });\n\n  it('should handle accessing nested properties on response objects', () => {\n    const code = `\n        const resp = pm.response;\n        const data = resp.json();\n        if (data && data.user && data.user.id) {\n            pm.environment.set(\"userId\", data.user.id);\n        }\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).not.toContain('const resp = pm.response;');\n    expect(translatedCode).toContain('const data = res.getBody();');\n    expect(translatedCode).toContain('bru.setEnvVar(\"userId\", data.user.id);');\n  });\n\n  it('should handle all response property methods together', () => {\n    const code = `\n        // All response property methods\n        const statusCode = pm.response.code;\n        const responseBody = pm.response.json();\n        const responseText = pm.response.text();\n        const statusText = pm.response.statusText;\n        const responseTime = pm.response.responseTime;\n        \n        pm.test(\"Response is valid\", function() {\n            pm.response.to.have.status(200);\n            pm.expect(responseBody).to.be.an('object');\n            pm.expect(responseTime).to.be.below(1000);\n            pm.expect(statusText).to.equal('OK');\n        });\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('const statusCode = res.getStatus();');\n    expect(translatedCode).toContain('const responseBody = res.getBody();');\n    expect(translatedCode).toContain('const responseText = JSON.stringify(res.getBody());');\n    expect(translatedCode).toContain('const responseTime = res.getResponseTime();');\n    expect(translatedCode).toContain('const statusText = res.statusText;');\n    expect(translatedCode).toContain('test(\"Response is valid\", function() {');\n    expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);');\n    expect(translatedCode).toContain('expect(responseBody).to.be.an(\\'object\\');');\n    expect(translatedCode).toContain('expect(responseTime).to.be.below(1000);');\n    expect(translatedCode).toContain('expect(statusText).to.equal(\\'OK\\');');\n  });\n\n  it('should handle pm objects with array access on response', () => {\n    const code = `\n        const items = pm.response.json().items;\n        for (let i = 0; i < items.length; i++) {\n            pm.collectionVariables.set(\"item_\" + i, items[i].id);\n        }\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('const items = res.getBody().items;');\n  });\n\n  // TODO: Restore once UI update fixes are live for setCollectionVar\n  it.skip('should translate pm.collectionVariables.set with array access pattern', () => {\n    const code = 'pm.collectionVariables.set(\"item_\" + i, items[i].id);';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('bru.setCollectionVar(\"item_\" + i, items[i].id);');\n  });\n\n  it('should handle response JSON with optional chaining and nullish coalescing', () => {\n    const code = `\n        const userId = pm.response.json()?.user?.id ?? \"anonymous\";\n        const items = pm.response.json()?.data?.items || [];\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('const userId = res.getBody()?.user?.id ?? \"anonymous\";');\n    expect(translatedCode).toContain('const items = res.getBody()?.data?.items || [];');\n  });\n\n  it('should handle response headers with different access patterns', () => {\n    // will need to handle get, set methods, bruno does not support this yet\n    const code = `\n        const contentType = pm.response.headers.get('Content-Type');\n        const contentLength = pm.response.headers.get('Content-Length');\n        console.log(\"contentType\", contentType);\n        console.log(\"contentLength\", contentLength);\n        \n        pm.test(\"Headers are correct\", function() {\n            pm.response.to.have.header('Content-Type');\n            pm.response.to.have.header('Content-Length');\n            pm.expect(contentType).to.include('application/json');\n        });\n        `;\n    const translatedCode = translateCode(code);\n\n    // Check how header access is translated\n    expect(translatedCode).toContain('const contentType = res.getHeader(\\'Content-Type\\');');\n    expect(translatedCode).toContain('const contentLength = res.getHeader(\\'Content-Length\\');');\n    expect(translatedCode).toContain('console.log(\"contentType\", contentType);');\n    expect(translatedCode).toContain('console.log(\"contentLength\", contentLength);');\n    expect(translatedCode).not.toContain('pm.test');\n    expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(\\'Content-Type\\'.toLowerCase())');\n    expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(\\'Content-Length\\'.toLowerCase())');\n    expect(translatedCode).toContain('expect(contentType).to.include(\\'application/json\\')');\n  });\n\n  it('should transform response data with array destructuring', () => {\n    const code = `\n        const { id, name, items } = pm.response.json();\n        const [first, second] = items;\n        pm.environment.set(\"userId\", id);\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('const { id, name, items } = res.getBody();');\n    expect(translatedCode).toContain('const [first, second] = items;');\n    expect(translatedCode).toContain('bru.setEnvVar(\"userId\", id);');\n  });\n\n  it('should handle response in complex conditionals', () => {\n    const code = `\n        if (pm.response.code >= 200 && pm.response.code < 300) {\n            if (pm.response.headers.get('Content-Type').includes('application/json')) {\n                const data = pm.response.json();\n                \n                if (data.success === true && data.token) {\n                    pm.environment.set(\"authToken\", data.token);\n                } else if (data.error) {\n                    console.error(\"API error:\", data.error);\n                }\n            }\n        } else if (pm.response.code === 404) {\n            console.log(\"Resource not found\");\n        } else {\n            console.error(\"Request failed with status:\", pm.response.code);\n        }\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('if (res.getStatus() >= 200 && res.getStatus() < 300) {');\n    expect(translatedCode).toContain('if (res.getHeader(\\'Content-Type\\').includes(\\'application/json\\')) {');\n    expect(translatedCode).toContain('const data = res.getBody();');\n    expect(translatedCode).toContain('bru.setEnvVar(\"authToken\", data.token);');\n    expect(translatedCode).toContain('} else if (res.getStatus() === 404) {');\n    expect(translatedCode).toContain('console.error(\"Request failed with status:\", res.getStatus());');\n  });\n\n  it('should handle response processing with try-catch', () => {\n    const code = `\n        try {\n            const data = pm.response.json();\n            pm.environment.set(\"userData\", JSON.stringify(data.user));\n        } catch (error) {\n            console.error(\"Failed to parse response:\", error);\n            const text = pm.response.text();\n            pm.environment.set(\"rawResponse\", text);\n        }\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('const data = res.getBody();');\n    expect(translatedCode).toContain('bru.setEnvVar(\"userData\", JSON.stringify(data.user));');\n    expect(translatedCode).toContain('const text = JSON.stringify(res.getBody());');\n    expect(translatedCode).toContain('bru.setEnvVar(\"rawResponse\", text);');\n  });\n\n  it('should handle JSON path style access to response data', () => {\n    const code = `\n        const data = pm.response.json();\n        const userId = data.user.id;\n        const userEmail = data.user.contact.email;\n        const firstItem = data.items[0];\n        \n        pm.environment.set(\"userId\", userId);\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('const data = res.getBody();');\n    expect(translatedCode).toContain('const userId = data.user.id;');\n    expect(translatedCode).toContain('const userEmail = data.user.contact.email;');\n    expect(translatedCode).toContain('const firstItem = data.items[0];');\n    expect(translatedCode).toContain('bru.setEnvVar(\"userId\", userId);');\n  });\n\n  it('should handle template literals with response data', () => {\n    const code = `\n        const data = pm.response.json();\n        const welcomeMessage = \\`Hello, \\${data.user.name}! Your ID is \\${data.user.id}.\\`;\n        \n        pm.environment.set(\"welcomeMessage\", welcomeMessage);\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('const data = res.getBody();');\n    expect(translatedCode).toContain('const welcomeMessage = `Hello, ${data.user.name}! Your ID is ${data.user.id}.`;');\n    expect(translatedCode).toContain('bru.setEnvVar(\"welcomeMessage\", welcomeMessage);');\n  });\n\n  it('should handle response processing in arrow functions', () => {\n    const code = `\n        const processItems = () => {\n            const items = pm.response.json().items;\n            return items.map(item => item.id);\n        };\n        \n        const itemIds = processItems();\n        pm.environment.set(\"itemIds\", JSON.stringify(itemIds));\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('const items = res.getBody().items;');\n    expect(translatedCode).toContain('return items.map(item => item.id);');\n    expect(translatedCode).toContain('const itemIds = processItems();');\n    expect(translatedCode).toContain('bru.setEnvVar(\"itemIds\", JSON.stringify(itemIds));');\n  });\n\n  it('should handle complex inline operations with response data', () => {\n    const code = `\n        const items = pm.response.json().items;\n        const totalValue = items.reduce((sum, item) => sum + item.price, 0);\n        const highValueItems = items.filter(item => item.price > 100);\n        const itemNames = items.map(item => item.name);\n        \n        pm.environment.set(\"totalValue\", totalValue);\n        pm.environment.set(\"highValueItemCount\", highValueItems.length);\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('const items = res.getBody().items;');\n    expect(translatedCode).toContain('const totalValue = items.reduce((sum, item) => sum + item.price, 0);');\n    expect(translatedCode).toContain('const highValueItems = items.filter(item => item.price > 100);');\n    expect(translatedCode).toContain('const itemNames = items.map(item => item.name);');\n    expect(translatedCode).toContain('bru.setEnvVar(\"totalValue\", totalValue);');\n    expect(translatedCode).toContain('bru.setEnvVar(\"highValueItemCount\", highValueItems.length);');\n  });\n\n  it('should handle complex test structure with pm.response.to.have.header', () => {\n    const code = `\n        pm.test(\"Response headers validation\", function() {\n            pm.response.to.have.header(\"Content-Type\", \"application/json\");\n            pm.response.to.have.header(\"Cache-Control\");\n            \n            const responseTime = pm.response.responseTime;\n            pm.expect(responseTime).to.be.below(1000);\n        });\n        `;\n    const translatedCode = translateCode(code);\n\n    // Check for test function conversion\n    expect(translatedCode).toContain('test(\"Response headers validation\", function() {');\n\n    // Check for header assertions inside the test callback\n    expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(\"Content-Type\".toLowerCase(), \"application/json\");');\n    expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(\"Cache-Control\".toLowerCase())');\n\n    // Check that other test assertions are preserved\n    expect(translatedCode).toContain('const responseTime = res.getResponseTime();');\n    expect(translatedCode).toContain('expect(responseTime).to.be.below(1000);');\n  });\n\n  it('should handle dynamic header names in pm.response.to.have.header', () => {\n    const code = `\n        function checkHeaderPresent(headerName) {\n            pm.response.to.have.header(headerName);\n        }\n        \n        function validateHeader(headerName, expectedValue) {\n            pm.response.to.have.header(headerName, expectedValue);\n        }\n        \n        checkHeaderPresent(\"Authorization\");\n        validateHeader(\"Content-Type\", \"application/json\");\n        `;\n    const translatedCode = translateCode(code);\n\n    // Check function transformations\n    expect(translatedCode).toContain('function checkHeaderPresent(headerName) {');\n    expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(headerName.toLowerCase())');\n\n    expect(translatedCode).toContain('function validateHeader(headerName, expectedValue) {');\n    expect(translatedCode).toContain('expect(res.getHeaders()).to.have.property(headerName.toLowerCase(), expectedValue);');\n\n    // Check function calls\n    expect(translatedCode).toContain('checkHeaderPresent(\"Authorization\");');\n    expect(translatedCode).toContain('validateHeader(\"Content-Type\", \"application/json\");');\n  });\n\n  it('should transform pm.response.to.have.body with string literal', () => {\n    const code = 'pm.response.to.have.body(\"Expected response body\");';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('expect(res.getBody()).to.equal(\"Expected response body\");');\n  });\n\n  it('should transform pm.response.to.have.body with variable parameter', () => {\n    const code = `\n        const expectedBody = {\"status\": \"success\", \"data\": [1, 2, 3]};\n        pm.response.to.have.body(expectedBody);\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('const expectedBody = {\"status\": \"success\", \"data\": [1, 2, 3]};');\n    expect(translatedCode).toContain('expect(res.getBody()).to.equal(expectedBody);');\n  });\n\n  it('should transform pm.response.to.have.body with JSON object', () => {\n    const code = `pm.response.to.have.body({\"status\": \"success\", \"message\": \"Operation completed\"});`;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('expect(res.getBody()).to.equal({\"status\": \"success\", \"message\": \"Operation completed\"});');\n  });\n\n  it('should transform pm.response.to.have.body inside test function', () => {\n    const code = `\n        pm.test(\"Response body validation\", function() {\n            const expectedResponse = {\"result\": true};\n            pm.response.to.have.body(expectedResponse);\n        });\n        `;\n    const translatedCode = translateCode(code);\n    const expectedOutput = `\n        test(\"Response body validation\", function() {\n            const expectedResponse = {\"result\": true};\n            expect(res.getBody()).to.equal(expectedResponse);\n        });\n        `;\n    expect(translatedCode).toBe(expectedOutput);\n  });\n\n  it('should transform pm.response.to.have.body with response alias', () => {\n    const code = `\n        const resp = pm.response;\n        resp.to.have.body({\"status\": \"ok\"});\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        expect(res.getBody()).to.equal({\"status\": \"ok\"});\n        `);\n  });\n\n  // --- getSize translations ---------------------------\n  it('should translate pm.response.size()', () => {\n    const code = 'const size = pm.response.size();';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('const size = res.getSize();');\n  });\n\n  it('should translate pm.response.size().body', () => {\n    const code = 'const bodySize = pm.response.size().body;';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('const bodySize = res.getSize().body;');\n  });\n\n  it('should translate pm.response.size().header', () => {\n    const code = 'const headerSize = pm.response.size().header;';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('const headerSize = res.getSize().header;');\n  });\n\n  it('should translate pm.response.size().total', () => {\n    const code = 'const totalSize = pm.response.size().total;';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('const totalSize = res.getSize().total;');\n  });\n\n  it('should translate pm.response.responseSize alias', () => {\n    const code = 'const responseSize = pm.response.responseSize;';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('const responseSize = res.getSize().body;');\n  });\n\n  // --- BDD-style response assertions ---------------------------\n\n  it('should translate pm.response.to.be.ok', () => {\n    const code = 'pm.response.to.be.ok;';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('expect(res.getStatus()).to.be.within(200, 299)');\n  });\n\n  it('should translate pm.response.to.be.success', () => {\n    const code = 'pm.response.to.be.success;';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('expect(res.getStatus()).to.be.within(200, 299)');\n  });\n\n  it('should translate pm.response.to.be.redirection', () => {\n    const code = 'pm.response.to.be.redirection;';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('expect(res.getStatus()).to.be.within(300, 399)');\n  });\n\n  it('should translate pm.response.to.be.clientError', () => {\n    const code = 'pm.response.to.be.clientError;';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('expect(res.getStatus()).to.be.within(400, 499)');\n  });\n\n  it('should translate pm.response.to.be.serverError', () => {\n    const code = 'pm.response.to.be.serverError;';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('expect(res.getStatus()).to.be.within(500, 599)');\n  });\n\n  it('should translate pm.response.to.be.error', () => {\n    const code = 'pm.response.to.be.error;';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('expect(res.getStatus()).to.be.at.least(400)');\n  });\n\n  it('should handle BDD-style assertions inside test blocks', () => {\n    const code = `\n        pm.test(\"Status check\", function() {\n            pm.response.to.be.ok;\n            pm.response.to.be.success;\n        });\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('test(\"Status check\", function() {');\n    expect(translatedCode).toContain('expect(res.getStatus()).to.be.within(200, 299)');\n  });\n\n  it('should translate pm.response.to.have.jsonBody with path', () => {\n    const code = 'pm.response.to.have.jsonBody(\"user.id\");';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('expect(res.getBody()).to.have.nested.property(\"user.id\")');\n  });\n\n  it('should translate pm.response.to.have.jsonBody with path and value', () => {\n    const code = 'pm.response.to.have.jsonBody(\"status\", \"success\");';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('expect(res.getBody()).to.have.nested.property(\"status\", \"success\")');\n  });\n\n  it('should translate pm.response.to.have.jsonBody without arguments', () => {\n    const code = 'pm.response.to.have.jsonBody();';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('expect(res.getBody()).to.exist');\n  });\n\n  it('should handle pm.response.to.have.jsonBody inside test blocks', () => {\n    const code = `\n        pm.test(\"Response validation\", function() {\n            pm.response.to.have.jsonBody(\"data\");\n            pm.response.to.have.jsonBody(\"data.id\", 123);\n        });\n        `;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('test(\"Response validation\", function() {');\n    expect(translatedCode).toContain('expect(res.getBody()).to.have.nested.property(\"data\")');\n    expect(translatedCode).toContain('expect(res.getBody()).to.have.nested.property(\"data.id\", 123)');\n  });\n\n  it('should translate pm.response.to.have.jsonBody with nested path', () => {\n    const code = 'pm.response.to.have.jsonBody(\"response.data.items[0].name\");';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('expect(res.getBody()).to.have.nested.property(\"response.data.items[0].name\")');\n  });\n\n  it('should translate pm.response.to.have.jsonBody with variable path', () => {\n    const code = 'const path = \"user.id\"; pm.response.to.have.jsonBody(path);';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('expect(res.getBody()).to.have.nested.property(path)');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/scoped-variables.test.js",
    "content": "import translateCode from '../../../../src/utils/postman-to-bruno-translator';\n\ndescribe('Scoped Variables', () => {\n  it.skip('should handle scoped variables correctly', () => {\n    const code = `\n    const response = pm.response;\n    const status = response.status;\n\n    function test() {\n        const response = delta.response;\n        const status = response.status;\n        console.log(status);\n    }\n    `;\n    const result = translateCode(code);\n    console.log(result);\n    expect(result).toBe(`\n    const status = res.statusText;\n\n    function test() {\n        const response = delta.response;\n        const status = response.status;\n        console.log(status);\n    }\n    `);\n  });\n\n  it.skip('should handle scoped variables correctly', () => {\n    const code = `\n    const response = delta.response;\n    const status = response.status;\n\n    function test() {\n        const response = pm.response;\n        const status = response.status;\n        console.log(status);\n    }\n    `;\n    const result = translateCode(code);\n    console.log(result);\n    expect(result).toBe(`\n    const response = delta.response;\n    const status = response.status;\n\n    function test() {\n        const status = res.statusText;\n        console.log(status);\n    }\n    `);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/testing-framework.test.js",
    "content": "import translateCode from '../../../../src/utils/postman-to-bruno-translator';\n\ndescribe('Testing Framework Translation', () => {\n  // Basic testing framework translations\n  it('should translate pm.test', () => {\n    const code = 'pm.test(\"Status code is 200\", function() { pm.response.to.have.status(200); });';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('test(\"Status code is 200\", function() { expect(res.getStatus()).to.equal(200); });');\n  });\n\n  it('should translate pm.expect', () => {\n    const code = 'pm.expect(jsonData.success).to.be.true;';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('expect(jsonData.success).to.be.true;');\n  });\n\n  it('should translate pm.expect.fail', () => {\n    const code = 'if (!isValid) pm.expect.fail(\"Data is invalid\");';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe('if (!isValid) expect.fail(\"Data is invalid\");');\n  });\n\n  // Tests with response assertions\n  it('should translate pm.response.to.have.status in tests', () => {\n    const code = `\n        pm.test(\"Check environment and call successful\", function () {\n            pm.expect(pm.environment.name).to.equal(\"ENVIRONMENT_NAME\");\n            pm.response.to.have.status(200);\n        });`;\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toBe(`\n        test(\"Check environment and call successful\", function () {\n            expect(bru.getEnvName()).to.equal(\"ENVIRONMENT_NAME\");\n            expect(res.getStatus()).to.equal(200);\n        });`);\n  });\n\n  // Test aliases\n  it('should handle test aliases', () => {\n    const code = `\n        const { test, expect } = pm;\n        \n        test(\"Status code is 200\", function () {\n            expect(pm.response.code).to.equal(200);\n        });\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).not.toContain('const { test, expect } = pm');\n    expect(translatedCode).toContain('test(\"Status code is 200\", function () {');\n    expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);');\n  });\n\n  // Tests inside different code structures\n  it('should translate pm commands inside tests with nested functions', () => {\n    const code = `\n        pm.test(\"Auth flow works\", function() {\n            const response = pm.response.json();\n            pm.expect(response.authenticated).to.be.true;\n            pm.environment.set(\"userId\", response.user.id);\n            pm.collectionVariables.set(\"sessionId\", response.session.id);\n        });\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('test(\"Auth flow works\", function() {');\n    expect(translatedCode).toContain('const response = res.getBody();');\n    expect(translatedCode).toContain('expect(response.authenticated).to.be.true;');\n    expect(translatedCode).toContain('bru.setEnvVar(\"userId\", response.user.id);');\n  });\n\n  // TODO: Restore once UI update fixes are live for setCollectionVar\n  it.skip('should translate pm.collectionVariables.set inside test functions', () => {\n    const code = 'pm.collectionVariables.set(\"sessionId\", response.session.id);';\n    const translatedCode = translateCode(code);\n    expect(translatedCode).toContain('bru.setCollectionVar(\"sessionId\", response.session.id);');\n  });\n\n  it('should translate pm.test with arrow functions', () => {\n    const code = `\n        pm.test(\"Status code is 200\", () => {\n            pm.expect(pm.response.code).to.eql(200);\n        });\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('test(\"Status code is 200\", () => {');\n    expect(translatedCode).toContain('expect(res.getStatus()).to.eql(200);');\n  });\n\n  it('should handle multiple test assertions in one function', () => {\n    const code = `\n        pm.test(\"The response has all properties\", () => {\n            const responseJson = pm.response.json();\n            pm.expect(responseJson.type).to.eql('vip');\n            pm.expect(responseJson.name).to.be.a('string');\n            pm.expect(responseJson.id).to.have.lengthOf(1);\n        });\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('test(\"The response has all properties\", () => {');\n    expect(translatedCode).toContain('const responseJson = res.getBody();');\n    expect(translatedCode).toContain('expect(responseJson.type).to.eql(\\'vip\\');');\n    expect(translatedCode).toContain('expect(responseJson.name).to.be.a(\\'string\\');');\n    expect(translatedCode).toContain('expect(responseJson.id).to.have.lengthOf(1);');\n  });\n\n  // Test with aliased variables\n  it('should translate aliases within test functions', () => {\n    const code = `\n        const tempRes = pm.response;\n        const tempTest = pm.test;\n        const tempExpect = pm.expect;\n\n        tempTest(\"Status code is 200\", function() { \n            tempExpect(tempRes.code).to.equal(200); \n        });\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).not.toContain('const tempRes = pm.response;');\n    expect(translatedCode).not.toContain('const tempTest = pm.test;');\n    expect(translatedCode).not.toContain('const tempExpect = pm.expect;');\n    expect(translatedCode).toContain('test(\"Status code is 200\", function() {');\n    expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);');\n  });\n\n  // Additional robust tests for testing framework\n  it('should handle nested test functions', () => {\n    const code = `\n        pm.test(\"Main test group\", function() {\n            const responseJson = pm.response.json();\n            \n            pm.test(\"User data validation\", function() {\n                pm.expect(responseJson.user).to.be.an('object');\n                pm.expect(responseJson.user.id).to.be.a('string');\n            });\n            \n            pm.test(\"Settings validation\", function() {\n                pm.expect(responseJson.settings).to.be.an('object');\n                pm.expect(responseJson.settings.notifications).to.be.a('boolean');\n            });\n        });\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('test(\"Main test group\", function() {');\n    expect(translatedCode).toContain('const responseJson = res.getBody();');\n    expect(translatedCode).toContain('test(\"User data validation\", function() {');\n    expect(translatedCode).toContain('expect(responseJson.user).to.be.an(\\'object\\');');\n    expect(translatedCode).toContain('test(\"Settings validation\", function() {');\n    expect(translatedCode).toContain('expect(responseJson.settings.notifications).to.be.a(\\'boolean\\');');\n  });\n\n  it('should handle test with dynamic test names', () => {\n    const code = `\n        const endpoint = pm.variables.get(\"currentEndpoint\");\n        \n        pm.test(\\`\\${endpoint} returns correct data\\`, function() {\n            const responseJson = pm.response.json();\n            pm.expect(responseJson).to.be.an('object');\n        });\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('const endpoint = bru.getVar(\"currentEndpoint\");');\n    expect(translatedCode).toContain('test(`${endpoint} returns correct data`, function() {');\n    expect(translatedCode).toContain('const responseJson = res.getBody();');\n    expect(translatedCode).toContain('expect(responseJson).to.be.an(\\'object\\');');\n  });\n\n  it('should handle test with conditional execution', () => {\n    const code = `\n        const responseJson = pm.response.json();\n        \n        if (responseJson.type === 'user') {\n            pm.test(\"User validation\", function() {\n                pm.expect(responseJson.name).to.be.a('string');\n                pm.expect(responseJson.email).to.be.a('string');\n            });\n        } else if (responseJson.type === 'admin') {\n            pm.test(\"Admin validation\", function() {\n                pm.expect(responseJson.accessLevel).to.be.above(5);\n                pm.expect(responseJson.permissions).to.be.an('array');\n            });\n        }\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('const responseJson = res.getBody();');\n    expect(translatedCode).toContain('if (responseJson.type === \\'user\\') {');\n    expect(translatedCode).toContain('test(\"User validation\", function() {');\n    expect(translatedCode).toContain('expect(responseJson.name).to.be.a(\\'string\\');');\n    expect(translatedCode).toContain('} else if (responseJson.type === \\'admin\\') {');\n    expect(translatedCode).toContain('test(\"Admin validation\", function() {');\n    expect(translatedCode).toContain('expect(responseJson.accessLevel).to.be.above(5);');\n  });\n\n  it('should handle assertions with logical operators', () => {\n    const code = `\n        pm.test(\"Response has valid structure\", function() {\n            const data = pm.response.json();\n            \n            pm.expect(data.id && data.name).to.be.ok;\n            pm.expect(data.active || data.pending).to.be.true;\n            pm.expect(!data.deleted).to.be.true;\n        });\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('test(\"Response has valid structure\", function() {');\n    expect(translatedCode).toContain('const data = res.getBody();');\n    expect(translatedCode).toContain('expect(data.id && data.name).to.be.ok;');\n    expect(translatedCode).toContain('expect(data.active || data.pending).to.be.true;');\n    expect(translatedCode).toContain('expect(!data.deleted).to.be.true;');\n  });\n\n  it('should handle array and object assertions', () => {\n    const code = `\n        pm.test(\"Array and object validations\", function() {\n            const data = pm.response.json();\n            \n            // Array validations\n            pm.expect(data.items).to.be.an('array');\n            pm.expect(data.items).to.have.lengthOf.at.least(1);\n            pm.expect(data.items[0]).to.have.property('id');\n            \n            // Object validations\n            pm.expect(data.user).to.be.an('object');\n            pm.expect(data.user).to.have.all.keys('id', 'name', 'email');\n            pm.expect(data.user).to.include({active: true});\n        });\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('test(\"Array and object validations\", function() {');\n    expect(translatedCode).toContain('const data = res.getBody();');\n    expect(translatedCode).toContain('expect(data.items).to.be.an(\\'array\\');');\n    expect(translatedCode).toContain('expect(data.items).to.have.lengthOf.at.least(1);');\n    expect(translatedCode).toContain('expect(data.items[0]).to.have.property(\\'id\\');');\n    expect(translatedCode).toContain('expect(data.user).to.be.an(\\'object\\');');\n    expect(translatedCode).toContain('expect(data.user).to.have.all.keys(\\'id\\', \\'name\\', \\'email\\');');\n    expect(translatedCode).toContain('expect(data.user).to.include({active: true});');\n  });\n\n  it('should handle chai assertions with deep equality', () => {\n    const code = `\n        pm.test(\"Deep equality checks\", function() {\n            const data = pm.response.json();\n            \n            pm.expect(data.config).to.deep.equal({\n                version: \"1.0\",\n                active: true,\n                features: [\"search\", \"export\"]\n            });\n            \n            pm.expect(data.tags).to.have.members(['api', 'test']);\n            pm.expect(data.meta).to.deep.include({format: 'json'});\n        });\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('test(\"Deep equality checks\", function() {');\n    expect(translatedCode).toContain('const data = res.getBody();');\n    expect(translatedCode).toContain('expect(data.config).to.deep.equal({');\n    expect(translatedCode).toContain('version: \"1.0\",');\n    expect(translatedCode).toContain('active: true,');\n    expect(translatedCode).toContain('features: [\"search\", \"export\"]');\n    expect(translatedCode).toContain('expect(data.tags).to.have.members([\\'api\\', \\'test\\']);');\n    expect(translatedCode).toContain('expect(data.meta).to.deep.include({format: \\'json\\'});');\n  });\n\n  it('should handle chai assertions with string comparisons', () => {\n    const code = `\n        pm.test(\"String validations\", function() {\n            const data = pm.response.json();\n            \n            pm.expect(data.id).to.be.a('string');\n            pm.expect(data.name).to.match(/^[A-Za-z\\\\s]+$/);\n            pm.expect(data.description).to.include('API');\n            pm.expect(data.url).to.have.string('api/v1');\n            pm.expect(data.code).to.have.lengthOf(8);\n        });\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('test(\"String validations\", function() {');\n    expect(translatedCode).toContain('const data = res.getBody();');\n    expect(translatedCode).toContain('expect(data.id).to.be.a(\\'string\\');');\n    expect(translatedCode).toContain('expect(data.name).to.match(/^[A-Za-z\\\\s]+$/);');\n    expect(translatedCode).toContain('expect(data.description).to.include(\\'API\\');');\n    expect(translatedCode).toContain('expect(data.url).to.have.string(\\'api/v1\\');');\n    expect(translatedCode).toContain('expect(data.code).to.have.lengthOf(8);');\n  });\n\n  it('should handle assertions with numeric comparisons', () => {\n    const code = `\n        pm.test(\"Numeric validations\", function() {\n            const data = pm.response.json();\n            \n            pm.expect(data.count).to.be.a('number');\n            pm.expect(data.count).to.be.above(0);\n            pm.expect(data.price).to.be.within(10, 100);\n            pm.expect(data.discount).to.be.at.most(25);\n            pm.expect(data.quantity * data.price).to.equal(data.total);\n        });\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('test(\"Numeric validations\", function() {');\n    expect(translatedCode).toContain('const data = res.getBody();');\n    expect(translatedCode).toContain('expect(data.count).to.be.a(\\'number\\');');\n    expect(translatedCode).toContain('expect(data.count).to.be.above(0);');\n    expect(translatedCode).toContain('expect(data.price).to.be.within(10, 100);');\n    expect(translatedCode).toContain('expect(data.discount).to.be.at.most(25);');\n    expect(translatedCode).toContain('expect(data.quantity * data.price).to.equal(data.total);');\n  });\n\n  it('should handle pm.expect.fail with conditions', () => {\n    const code = `\n        pm.test(\"Validate critical fields\", function() {\n            const data = pm.response.json();\n            \n            if (!data.id) {\n                pm.expect.fail(\"Missing ID field\");\n            }\n            \n            if (data.status !== 'active' && data.status !== 'pending') {\n                pm.expect.fail(\"Invalid status: \" + data.status);\n            }\n            \n            // Continue with normal assertions\n            pm.expect(data.name).to.be.a('string');\n        });\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('test(\"Validate critical fields\", function() {');\n    expect(translatedCode).toContain('const data = res.getBody();');\n    expect(translatedCode).toContain('if (!data.id) {');\n    expect(translatedCode).toContain('expect.fail(\"Missing ID field\");');\n    expect(translatedCode).toContain('if (data.status !== \\'active\\' && data.status !== \\'pending\\') {');\n    expect(translatedCode).toContain('expect.fail(\"Invalid status: \" + data.status);');\n    expect(translatedCode).toContain('expect(data.name).to.be.a(\\'string\\');');\n  });\n\n  it('should handle complex test compositions', () => {\n    const code = `\n        // Helper function\n        function validateUserObject(user) {\n            pm.expect(user).to.be.an('object');\n            pm.expect(user.id).to.be.a('string');\n            pm.expect(user.name).to.be.a('string');\n            return user.id && user.name;\n        }\n        \n        pm.test(\"Response validation\", function() {\n            const response = pm.response.json();\n            const validUsers = [];\n            \n            // Test status code\n            pm.response.to.have.status(200);\n            \n            // Test main user\n            if (response.user) {\n                const isValid = validateUserObject(response.user);\n                if (isValid) {\n                    validUsers.push(response.user);\n                }\n            }\n            \n            // Test related users\n            if (response.relatedUsers && Array.isArray(response.relatedUsers)) {\n                pm.test(\"Related users validation\", function() {\n                    response.relatedUsers.forEach((user, index) => {\n                        pm.test(\\`User at index \\${index}\\`, function() {\n                            const isValid = validateUserObject(user);\n                            if (isValid) {\n                                validUsers.push(user);\n                            }\n                        });\n                    });\n                });\n            }\n            \n            // Set the valid users for later use\n            if (validUsers.length > 0) {\n                pm.environment.set(\"validUsers\", JSON.stringify(validUsers));\n            }\n        });\n        `;\n    const translatedCode = translateCode(code);\n\n    // Test key transformations\n    expect(translatedCode).toContain('function validateUserObject(user) {');\n    expect(translatedCode).toContain('expect(user).to.be.an(\\'object\\');');\n    expect(translatedCode).toContain('test(\"Response validation\", function() {');\n    expect(translatedCode).toContain('const response = res.getBody();');\n    expect(translatedCode).toContain('expect(res.getStatus()).to.equal(200);');\n    expect(translatedCode).toContain('test(\"Related users validation\", function() {');\n    expect(translatedCode).toContain('test(`User at index ${index}`, function() {');\n    expect(translatedCode).toContain('bru.setEnvVar(\"validUsers\", JSON.stringify(validUsers));');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/transformers/send-request.test.js",
    "content": "import translateCode from '../../../../../src/utils/postman-to-bruno-translator';\n\ndescribe('Send Request Translation', () => {\n  describe('Raw Body Mode', () => {\n    it('should transform raw JSON string body', () => {\n      const code = `\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Accept': 'application/json',\n                'Content-Type': 'application/json',\n            },\n            body: {\n                mode: 'raw',\n                raw: JSON.stringify({\n                    \"x\": 1\n                })\n            }\n        }, async function (error, response) {\n            if (error) {\n                const errorCode = error.code;\n                console.log(errorCode);\n            }\n            if (response) {\n                const response_body = response.json();\n                const response_headers = response.headers;\n                console.log(response_body, response_headers);\n            }\n        });\n      `;\n      const translatedCode = translateCode(code);\n      expect(translatedCode).toBe(`\n        await bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Accept': 'application/json',\n                'Content-Type': 'application/json',\n            },\n            data: JSON.stringify({\n                \"x\": 1\n            })\n        }, async function(error, response) {\n            if (error) {\n                const errorCode = error.code;\n                console.log(errorCode);\n            }\n            if (response) {\n                const response_body = response.data;\n                const response_headers = response.headers;\n                console.log(response_body, response_headers);\n            }\n        });\n      `);\n    });\n\n    it('should transform raw JSON object body', () => {\n      const code = `\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Accept': 'application/json',\n                'Content-Type': 'application/json',\n            },\n            body: {\n                mode: 'raw',\n                raw: {\n                    \"x\": 1\n                }\n            }\n        }, function (error, response) {\n            if (error) {\n                const errorCode = error.code;\n                console.log(errorCode);\n            }\n            if (response) {\n                const response_body = response.json();\n                const response_headers = response.headers;\n                console.log(response_body, response_headers);\n            }\n        });\n      `;\n      const translatedCode = translateCode(code);\n      expect(translatedCode).toBe(`\n        await bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Accept': 'application/json',\n                'Content-Type': 'application/json',\n            },\n            data: {\n                \"x\": 1\n            }\n        }, async function(error, response) {\n            if (error) {\n                const errorCode = error.code;\n                console.log(errorCode);\n            }\n            if (response) {\n                const response_body = response.data;\n                const response_headers = response.headers;\n                console.log(response_body, response_headers);\n            }\n        });\n      `);\n    });\n\n    it('should transform raw text body', () => {\n      const code = `\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Content-Type': 'text/plain',\n            },\n            body: {\n                mode: 'raw',\n                raw: 'Hello World'\n            }\n        }, function (error, response) {\n            console.log(response.text());\n        });\n      `;\n      const translatedCode = translateCode(code);\n      expect(translatedCode).toBe(`\n        await bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Content-Type': 'text/plain',\n            },\n            data: 'Hello World'\n        }, async function(error, response) {\n            console.log(response.data);\n        });\n      `);\n    });\n  });\n\n  describe('URL-encoded Body Mode', () => {\n    it('should transform urlencoded body with single key-value pair', () => {\n      const code = `\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Accept': 'application/json',\n                'Content-Type': 'application/x-www-form-urlencoded',\n            },\n            body: {\n                mode: 'urlencoded',\n                urlencoded: [\n                    { key: \"key\", value: \"value\" }\n                ]\n            }\n        }, function (error, response) {\n            if (error) {\n                const errorCode = error.code;\n                console.log(errorCode);\n            }\n            if (response) {\n                const response_body = response.json();\n                const response_headers = response.headers;\n                console.log(response_body, response_headers);\n            }\n        });\n      `;\n      const translatedCode = translateCode(code);\n      expect(translatedCode).toBe(`\n        await bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Accept': 'application/json',\n                \"Content-Type\": \"application/x-www-form-urlencoded\",\n            },\n            data: {\n                \"key\": \"value\"\n            }\n        }, async function(error, response) {\n            if (error) {\n                const errorCode = error.code;\n                console.log(errorCode);\n            }\n            if (response) {\n                const response_body = response.data;\n                const response_headers = response.headers;\n                console.log(response_body, response_headers);\n            }\n        });\n      `);\n    });\n\n    it('should transform urlencoded body with multiple key-value pairs', () => {\n      const code = `\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {},\n            body: {\n                mode: 'urlencoded',\n                urlencoded: [\n                    { key: \"firstName\", value: \"John\" },\n                    { key: \"lastName\", value: \"Doe\" },\n                    { key: \"email\", value: \"john.doe@example.com\" }\n                ]\n            }\n        }, function (error, response) {\n            console.log(response.json());\n        });\n      `;\n      const translatedCode = translateCode(code);\n      expect(translatedCode).toBe(`\n        await bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                \"Content-Type\": \"application/x-www-form-urlencoded\"\n            },\n            data: {\n                \"firstName\": \"John\",\n                \"lastName\": \"Doe\",\n                \"email\": \"john.doe@example.com\"\n            }\n        }, async function(error, response) {\n            console.log(response.data);\n        });\n      `);\n    });\n\n    it('should transform urlencoded body when no Content-Type header exists', () => {\n      const code = `\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            body: {\n                mode: 'urlencoded',\n                urlencoded: [\n                    { key: \"key1\", value: \"value1\" },\n                    { key: \"key2\", value: \"value2\" }\n                ]\n            }\n        });\n    `;\n      const translatedCode = translateCode(code);\n      expect(translatedCode).toBe(`\n        await bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n\n            data: {\n                \"key1\": \"value1\",\n                \"key2\": \"value2\"\n            },\n\n            headers: {\n                \"Content-Type\": \"application/x-www-form-urlencoded\"\n            }\n        });\n    `);\n    });\n\n    it('should transform urlencoded body with incorrect Content-Type header', () => {\n      const code = `\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                \"Content-Type\": \"text/plain\"\n            },\n            body: {\n                mode: 'urlencoded',\n                urlencoded: [\n                    { key: \"key1\", value: \"value1\" },\n                    { key: \"key2\", value: \"value2\" }\n                ]\n            }\n        });\n    `;\n      const translatedCode = translateCode(code);\n      expect(translatedCode).toBe(`\n        await bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                \"Content-Type\": \"application/x-www-form-urlencoded\"\n            },\n            data: {\n                \"key1\": \"value1\",\n                \"key2\": \"value2\"\n            }\n        });\n    `);\n    });\n  });\n\n  describe('Multi-part Form Data Body Mode', () => {\n    it('should transform formdata body with single key-value pair', () => {\n      const code = `\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Content-Type': 'multipart/form-data',\n            },\n            body: {\n                mode: 'formdata',\n                formdata: [\n                    { key: \"key\", value: \"value\" }\n                ]\n            }\n        }, function (error, response) {\n            if (error) {\n                const errorCode = error.code;\n                console.log(errorCode);\n            }\n            if (response) {\n                const response_body = response.json();\n                const response_headers = response.headers;\n                console.log(response_body, response_headers);\n            }\n        });\n      `;\n      const translatedCode = translateCode(code);\n      expect(translatedCode).toBe(`\n        await bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                \"Content-Type\": \"multipart/form-data\",\n            },\n            data: {\n                \"key\": \"value\"\n            }\n        }, async function(error, response) {\n            if (error) {\n                const errorCode = error.code;\n                console.log(errorCode);\n            }\n            if (response) {\n                const response_body = response.data;\n                const response_headers = response.headers;\n                console.log(response_body, response_headers);\n            }\n        });\n      `);\n    });\n\n    it('should transform formdata body with multiple key-value pair', () => {\n      const code = `\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Content-Type': 'multipart/form-data',\n            },\n            body: {\n                mode: 'formdata',\n                formdata: [\n                    { key: \"firstName\", value: \"John\" },\n                    { key: \"lastName\", value: \"Doe\" },\n                    { key: \"email\", value: \"john.doe@example.com\" }\n                ]\n            }\n        }, function (error, response) {\n            if (error) {\n                const errorCode = error.code;\n                console.log(errorCode);\n            }\n            if (response) {\n                const response_body = response.json();\n                const response_headers = response.headers;\n                console.log(response_body, response_headers);\n            }\n        });\n      `;\n      const translatedCode = translateCode(code);\n      expect(translatedCode).toBe(`\n        await bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                \"Content-Type\": \"multipart/form-data\",\n            },\n            data: {\n                \"firstName\": \"John\",\n                \"lastName\": \"Doe\",\n                \"email\": \"john.doe@example.com\"\n            }\n        }, async function(error, response) {\n            if (error) {\n                const errorCode = error.code;\n                console.log(errorCode);\n            }\n            if (response) {\n                const response_body = response.data;\n                const response_headers = response.headers;\n                console.log(response_body, response_headers);\n            }\n        });\n      `);\n    });\n\n    it('should transform formdata body when no Content-Type header exists', () => {\n      const code = `\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            body: {\n                mode: 'formdata',\n                formdata: [\n                    { key: \"firstName\", value: \"John\" },\n                    { key: \"lastName\", value: \"Doe\" },\n                    { key: \"email\", value: \"john.doe@example.com\" }\n                ]\n            }\n        }, function (error, response) {\n            if (error) {\n                const errorCode = error.code;\n                console.log(errorCode);\n            }\n            if (response) {\n                const response_body = response.json();\n                const response_headers = response.headers;\n                console.log(response_body, response_headers);\n            }\n        });\n      `;\n      const translatedCode = translateCode(code);\n      expect(translatedCode).toBe(`\n        await bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n\n            data: {\n                \"firstName\": \"John\",\n                \"lastName\": \"Doe\",\n                \"email\": \"john.doe@example.com\"\n            },\n\n            headers: {\n                \"Content-Type\": \"multipart/form-data\"\n            }\n        }, async function(error, response) {\n            if (error) {\n                const errorCode = error.code;\n                console.log(errorCode);\n            }\n            if (response) {\n                const response_body = response.data;\n                const response_headers = response.headers;\n                console.log(response_body, response_headers);\n            }\n        });\n      `);\n    });\n\n    it('should transform formdata body with incorrect Content-Type header', () => {\n      const code = `\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                \"Content-Type\": \"text/plain\"\n            },\n            body: {\n                mode: 'formdata',\n                formdata: [\n                    { key: \"firstName\", value: \"John\" },\n                    { key: \"lastName\", value: \"Doe\" },\n                    { key: \"email\", value: \"john.doe@example.com\" }\n                ]\n            }\n        }, function (error, response) {\n            if (error) {\n                const errorCode = error.code;\n                console.log(errorCode);\n            }\n            if (response) {\n                const response_body = response.json();\n                const response_headers = response.headers;\n                console.log(response_body, response_headers);\n            }\n        });\n      `;\n      const translatedCode = translateCode(code);\n      expect(translatedCode).toBe(`\n        await bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                \"Content-Type\": \"multipart/form-data\"\n            },\n            data: {\n                \"firstName\": \"John\",\n                \"lastName\": \"Doe\",\n                \"email\": \"john.doe@example.com\"\n            }\n        }, async function(error, response) {\n            if (error) {\n                const errorCode = error.code;\n                console.log(errorCode);\n            }\n            if (response) {\n                const response_body = response.data;\n                const response_headers = response.headers;\n                console.log(response_body, response_headers);\n            }\n        });\n      `);\n    });\n  });\n\n  describe('Headers and Content-Type Handling', () => {\n    it('should rename header property to headers', () => {\n      const code = `\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'GET',\n            header: {\n                'X-Custom-Header': 'custom-value',\n                'Authorization': 'Bearer token'\n            }\n        });\n      `;\n      const translatedCode = translateCode(code);\n      expect(translatedCode).toBe(`\n        await bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'GET',\n            headers: {\n                'X-Custom-Header': 'custom-value',\n                'Authorization': 'Bearer token'\n            }\n        });\n      `);\n    });\n\n    it('should handle header array format', () => {\n      const code = `\n        pm.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'GET',\n            header: [\n                { key: 'X-Custom-Header', value: 'custom-value' },\n                { key: 'Authorization', value: 'Bearer token' }\n            ]\n        });\n      `;\n      const translatedCode = translateCode(code);\n      expect(translatedCode).toBe(`\n        await bru.sendRequest({\n            url: 'https://echo.usebruno.com',\n            method: 'GET',\n            headers: {\n                \"X-Custom-Header\": 'custom-value',\n                \"Authorization\": 'Bearer token'\n            }\n        });\n      `);\n    });\n  });\n\n  describe('Response Handling', () => {\n    it('should transform response property access', () => {\n      const code = `\n        pm.sendRequest('https://echo.usebruno.com', function (error, response) {\n            const status = response.code;\n            const statusText = response.status;\n            const headers = response.headers;\n            const body = response.json();\n            const responseTime = response.responseTime;\n            const text = response.text();\n            \n            if (status === 200) {\n                console.log('Success!');\n            }\n        });\n      `;\n      const translatedCode = translateCode(code);\n      expect(translatedCode).toContain(`const status = response.status;\n            const statusText = response.statusText;`);\n      expect(translatedCode).toContain('const headers = response.headers');\n      expect(translatedCode).toContain('const body = response.data');\n      expect(translatedCode).toContain('const responseTime = response.responseTime');\n      expect(translatedCode).toContain('const text = response.data');\n    });\n  });\n\n  describe('Async/Await', () => {\n    it('Should not add await if already present', () => {\n      const code = `\n        try {\n            const response = await pm.sendRequest({\n                url: \"https://echo.usebruno.com\",\n                method: \"GET\"\n            });\n\n            console.log(response.json());\n        } catch (err) {\n            console.error(err);\n        }\n      `;\n      const translatedCode = translateCode(code);\n      expect(translatedCode).toBe(`\n        try {\n            const response = await bru.sendRequest({\n                url: \"https://echo.usebruno.com\",\n                method: \"GET\"\n            });\n\n            console.log(response.json());\n        } catch (err) {\n            console.error(err);\n        }\n      `);\n    });\n\n    it('Should handle arrow function callbacks', () => {\n      const code = `\n        try {\n            pm.sendRequest({\n                url: \"https://echo.usebruno.com\",\n                method: \"GET\"\n            }, (error, response) => {\n                console.log(response.json());\n            });\n        } catch (err) {\n            console.error(err);\n        }\n      `;\n      const translatedCode = translateCode(code);\n      expect(translatedCode).toBe(`\n        try {\n            await bru.sendRequest({\n                url: \"https://echo.usebruno.com\",\n                method: \"GET\"\n            }, async function(error, response) {\n                console.log(response.data);\n            });\n        } catch (err) {\n            console.error(err);\n        }\n      `);\n    });\n\n    it('Should handle async arrow function callbacks', () => {\n      const code = `\n        try {\n            pm.sendRequest({\n                url: \"https://echo.usebruno.com\",\n                method: \"GET\"\n            }, async (error, response) => {\n                await new Promise(resolve => {\n                    setTimeout(() => {\n                        resolve();\n                    }, 1000)\n                });\n                console.log(response.json());\n            });\n        } catch (err) {\n            console.error(err);\n        }\n      `;\n      const translatedCode = translateCode(code);\n      expect(translatedCode).toBe(`\n        try {\n            await bru.sendRequest({\n                url: \"https://echo.usebruno.com\",\n                method: \"GET\"\n            }, async function(error, response) {\n                await new Promise(resolve => {\n                    setTimeout(() => {\n                        resolve();\n                    }, 1000)\n                });\n                console.log(response.data);\n            });\n        } catch (err) {\n            console.error(err);\n        }\n      `);\n    });\n  });\n\n  describe('requestConfig variables', () => {\n    it('requestConfig passed as a variable', () => {\n      const code = `\n        const requestConfig = {\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Accept': 'application/json',\n                'Content-Type': 'application/json',\n            },\n            body: {\n                mode: 'raw',\n                raw: JSON.stringify({\n                    \"x\": 1\n                })\n            }\n        };\n        pm.sendRequest(requestConfig, async function (error, response) {\n            if (error) {\n                const errorCode = error.code;\n                console.log(errorCode);\n            }\n            if (response) {\n                const response_body = response.json();\n                const response_headers = response.headers;\n                console.log(response_body, response_headers);\n            }\n        });\n      `;\n      const translatedCode = translateCode(code);\n      expect(translatedCode).toBe(`\n        const requestConfig = {\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Accept': 'application/json',\n                'Content-Type': 'application/json',\n            },\n            data: JSON.stringify({\n                \"x\": 1\n            })\n        };\n        await bru.sendRequest(requestConfig, async function(error, response) {\n            if (error) {\n                const errorCode = error.code;\n                console.log(errorCode);\n            }\n            if (response) {\n                const response_body = response.data;\n                const response_headers = response.headers;\n                console.log(response_body, response_headers);\n            }\n        });\n      `);\n    });\n\n    it('requestConfig passed as a variable with multi-level references', () => {\n      const code = `\n        const requestConfig = {\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            header: {\n                'Accept': 'application/json',\n                'Content-Type': 'application/json',\n            },\n            body: {\n                mode: 'raw',\n                raw: JSON.stringify({\n                    \"x\": 1\n                })\n            }\n        };\n        const requestConfig1 = requestConfig;\n        const requestConfig2 = requestConfig1;\n        const requestConfig3 = requestConfig2;\n        pm.sendRequest(requestConfig3, async function (error, response) {\n            if (error) {\n                const errorCode = error.code;\n                console.log(errorCode);\n            }\n            if (response) {\n                const response_body = response.json();\n                const response_headers = response.headers;\n                console.log(response_body, response_headers);\n            }\n        });\n      `;\n      const translatedCode = translateCode(code);\n      expect(translatedCode).toBe(`\n        const requestConfig = {\n            url: 'https://echo.usebruno.com',\n            method: 'POST',\n            headers: {\n                'Accept': 'application/json',\n                'Content-Type': 'application/json',\n            },\n            data: JSON.stringify({\n                \"x\": 1\n            })\n        };\n        const requestConfig1 = requestConfig;\n        const requestConfig2 = requestConfig1;\n        const requestConfig3 = requestConfig2;\n        await bru.sendRequest(requestConfig3, async function(error, response) {\n            if (error) {\n                const errorCode = error.code;\n                console.log(errorCode);\n            }\n            if (response) {\n                const response_body = response.data;\n                const response_headers = response.headers;\n                console.log(response_body, response_headers);\n            }\n        });\n      `);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/variable-chaining.test.js",
    "content": "import translateCode from '../../../../src/utils/postman-to-bruno-translator';\n\ndescribe('Variable Chaining Resolution', () => {\n  test('should resolve a simple variable chain (variable pointing to another variable)', () => {\n    const code = `\n      const original = pm.response;\n      const alias = original;\n      const data = alias.json();\n    `;\n\n    const translatedCode = translateCode(code);\n\n    // Check that alias.json() was properly resolved to res.getBody()\n    expect(translatedCode).toContain('const data = res.getBody();');\n    // The original variable declarations should be removed\n    expect(translatedCode).not.toContain('const original =');\n    expect(translatedCode).not.toContain('const alias =');\n  });\n\n  test('should handle mixed variable references correctly', () => {\n    const code = `\n      const respVar = pm.response;\n      const envVar = pm.environment;\n      const respAlias = respVar;\n      \n      // These should be replaced\n      const statusCode = respAlias.code;\n      const envValue = envVar.get(\"key\");\n      \n      // This should not be replaced\n      const unrelatedVar = \"some value\";\n    `;\n\n    const translatedCode = translateCode(code);\n\n    // Check correct replacements\n    expect(translatedCode).not.toContain('const respVar');\n    expect(translatedCode).not.toContain('const envVar');\n    expect(translatedCode).toContain('const statusCode = res.getStatus();');\n    expect(translatedCode).toContain('const envValue = bru.getEnvVar(\"key\");');\n\n    // Check that unrelated variables are preserved\n    expect(translatedCode).toContain('const unrelatedVar = \"some value\";');\n  });\n\n  /**\n   * This test verifies that when multiple variables are declared in a single statement,\n   * only the ones referencing Postman objects are removed and the others are preserved.\n   *\n   * For example, in a statement like:\n   *   const response = pm.response, counter = 5, helper = \"test\";\n   *\n   * Only 'response' should be removed, resulting in:\n   *   const counter = 5, helper = \"test\";\n   */\n  test('should handle multiple variables in one declaration statement', () => {\n    const code = `\n      // Multiple variables in one declaration, with a mix of Postman objects and regular variables\n      const response = pm.response, counter = 5, helper = \"test\";\n      \n      // Using both the Postman reference (should be replaced) and regular values (should be preserved)\n      const statusCode = response.code;\n      console.log(\"Counter value:\", counter);\n      console.log(\"Helper string:\", helper);\n      \n      // Another example with different Postman object\n      let env = pm.environment, timeout = 1000, isValid = true;\n      const baseUrl = env.get(\"baseUrl\");\n    `;\n\n    const translatedCode = translateCode(code);\n\n    // Postman references should be replaced\n    expect(translatedCode).not.toContain('response = pm.response');\n    expect(translatedCode).not.toContain('env = pm.environment');\n\n    // Regular variables should be preserved\n    expect(translatedCode).toContain('const counter = 5');\n    expect(translatedCode).toContain('helper = \"test\"');\n    expect(translatedCode).toContain('timeout = 1000');\n    expect(translatedCode).toContain('isValid = true');\n\n    // References to Postman objects should be properly translated\n    expect(translatedCode).toContain('const statusCode = res.getStatus();');\n    expect(translatedCode).toContain('const baseUrl = bru.getEnvVar(\"baseUrl\");');\n\n    // Console logs with regular variables should be preserved\n    expect(translatedCode).toContain('console.log(\"Counter value:\", counter);');\n    expect(translatedCode).toContain('console.log(\"Helper string:\", helper);');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/variables.test.js",
    "content": "import translateCode from '../../../../src/utils/postman-to-bruno-translator';\n\ndescribe('Variables Translation', () => {\n  // Regular variables tests\n  it('should translate pm.variables.get', () => {\n    const code = 'pm.variables.get(\"test\");';\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe('bru.getVar(\"test\");');\n  });\n\n  it('should translate pm.variables.set', () => {\n    const code = 'pm.variables.set(\"test\", \"value\");';\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe('bru.setVar(\"test\", \"value\");');\n  });\n\n  it('should translate pm.variables.has', () => {\n    const code = 'pm.variables.has(\"userId\");';\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe('bru.hasVar(\"userId\");');\n  });\n\n  it('should translate pm.variables.replaceIn', () => {\n    const code = 'pm.variables.replaceIn(\"Hello {{name}}\");';\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe('bru.interpolate(\"Hello {{name}}\");');\n  });\n\n  it('should translate pm.variables.replaceIn with variables and expressions', () => {\n    const code = 'const greeting = pm.variables.replaceIn(\"Hello {{name}}, your user id is {{userId}}\");';\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe('const greeting = bru.interpolate(\"Hello {{name}}, your user id is {{userId}}\");');\n  });\n\n  it('should translate pm.variables.replaceIn within complex expressions', () => {\n    const code = 'const url = baseUrl + pm.variables.replaceIn(\"/users/{{userId}}/profile\");';\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe('const url = baseUrl + bru.interpolate(\"/users/{{userId}}/profile\");');\n  });\n\n  it('should translate pm.variables.replaceIn with multiple nested variable references', () => {\n    const code = 'const template = pm.variables.replaceIn(\"{{prefix}}-{{env}}-{{suffix}}\");';\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe('const template = bru.interpolate(\"{{prefix}}-{{env}}-{{suffix}}\");');\n  });\n\n  it('should translate aliased variables.replaceIn', () => {\n    const code = `\n            const variables = pm.variables;\n            const message = variables.replaceIn(\"Welcome, {{username}}!\");\n            `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe(`\n            const message = bru.interpolate(\"Welcome, {{username}}!\");\n            `);\n  });\n\n  // Collection variables tests\n  it('should translate pm.collectionVariables.get', () => {\n    const code = 'pm.collectionVariables.get(\"apiUrl\");';\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe('bru.getCollectionVar(\"apiUrl\");');\n  });\n\n  // TODO: Restore once UI update fixes are live for setCollectionVar\n  it.skip('should translate pm.collectionVariables.set', () => {\n    const code = 'pm.collectionVariables.set(\"token\", jsonData.token);';\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe('bru.setCollectionVar(\"token\", jsonData.token);');\n  });\n\n  it('should translate pm.collectionVariables.has', () => {\n    const code = 'pm.collectionVariables.has(\"authToken\");';\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe('bru.hasCollectionVar(\"authToken\");');\n  });\n\n  // TODO: Restore once UI update fixes are live for deleteCollectionVar\n  it.skip('should translate pm.collectionVariables.unset', () => {\n    const code = 'pm.collectionVariables.unset(\"tempVar\");';\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe('bru.deleteCollectionVar(\"tempVar\");');\n  });\n\n  it('should handle pm.globals.get', () => {\n    const code = 'pm.globals.get(\"test\");';\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe('bru.getGlobalEnvVar(\"test\");');\n  });\n\n  it('should handle pm.globals.set', () => {\n    const code = 'pm.globals.set(\"test\", \"value\");';\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe('bru.setGlobalEnvVar(\"test\", \"value\");');\n  });\n\n  // Alias tests for variables\n  it('should handle variables aliases', () => {\n    const code = `\n        const vars = pm.variables;\n        const has = vars.has(\"test\");\n        const set = vars.set(\"test\", \"value\");\n        const get = vars.get(\"test\");\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe(`\n        const has = bru.hasVar(\"test\");\n        const set = bru.setVar(\"test\", \"value\");\n        const get = bru.getVar(\"test\");\n        `);\n  });\n\n  // Alias tests for collection variables\n  // TODO: Restore once UI update fixes are live for setCollectionVar/deleteCollectionVar\n  it.skip('should handle collection variables aliases', () => {\n    const code = `\n        const collVars = pm.collectionVariables;\n        const has = collVars.has(\"test\");\n        const set = collVars.set(\"test\", \"value\");\n        const get = collVars.get(\"test\");\n        const unset = collVars.unset(\"test\");\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe(`\n        const has = bru.hasCollectionVar(\"test\");\n        const set = bru.setCollectionVar(\"test\", \"value\");\n        const get = bru.getCollectionVar(\"test\");\n        const unset = bru.deleteCollectionVar(\"test\");\n        `);\n  });\n\n  it('should handle pm.globals aliases', () => {\n    const code = `\n        const globals = pm.globals;\n        const get = globals.get(\"test\");\n        const set = globals.set(\"test\", \"value\");\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe(`\n        const get = bru.getGlobalEnvVar(\"test\");\n        const set = bru.setGlobalEnvVar(\"test\", \"value\");\n        `);\n  });\n\n  // Combined tests\n  it('should handle conditional expressions with variable calls', () => {\n    const code = 'const userStatus = pm.variables.has(\"userId\") ? \"logged-in\" : \"guest\";';\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe('const userStatus = bru.hasVar(\"userId\") ? \"logged-in\" : \"guest\";');\n  });\n\n  it('should handle all variable methods together', () => {\n    const code = `\n        // All variable methods\n        const hasUserId = pm.variables.has(\"userId\");\n        const userId = pm.variables.get(\"userId\");\n        pm.variables.set(\"requestTime\", new Date().toISOString());\n        \n        console.log(\\`Has userId: \\${hasUserId}, User ID: \\${userId}\\`);\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('const hasUserId = bru.hasVar(\"userId\");');\n    expect(translatedCode).toContain('const userId = bru.getVar(\"userId\");');\n    expect(translatedCode).toContain('bru.setVar(\"requestTime\", new Date().toISOString());');\n  });\n\n  // TODO: Restore once UI update fixes are live for setCollectionVar/deleteCollectionVar\n  it.skip('should handle all collection variable methods together', () => {\n    const code = `\n        // All collection variable methods\n        const hasApiUrl = pm.collectionVariables.has(\"apiUrl\");\n        const apiUrl = pm.collectionVariables.get(\"apiUrl\");\n        pm.collectionVariables.set(\"requestTime\", new Date().toISOString());\n        pm.collectionVariables.unset(\"tempVar\");\n        \n        console.log(\\`Has API URL: \\${hasApiUrl}, API URL: \\${apiUrl}\\`);\n        `;\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('const hasApiUrl = bru.hasCollectionVar(\"apiUrl\");');\n    expect(translatedCode).toContain('const apiUrl = bru.getCollectionVar(\"apiUrl\");');\n    expect(translatedCode).toContain('bru.setCollectionVar(\"requestTime\", new Date().toISOString());');\n    expect(translatedCode).toContain('bru.deleteCollectionVar(\"tempVar\");');\n  });\n\n  // TODO: Restore once UI update fixes are live for setCollectionVar\n  it.skip('should handle more complex nested expressions with variables', () => {\n    const code = 'pm.collectionVariables.set(\"fullPath\", pm.environment.get(\"baseUrl\") + pm.variables.get(\"endpoint\"));';\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe('bru.setCollectionVar(\"fullPath\", bru.getEnvVar(\"baseUrl\") + bru.getVar(\"endpoint\"));');\n  });\n\n  // replaceIn tests for different variable scopes\n  it('should translate pm.globals.replaceIn', () => {\n    const code = 'pm.globals.replaceIn(\"Hello {{name}}\");';\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe('bru.interpolate(\"Hello {{name}}\");');\n  });\n\n  it('should translate pm.environment.replaceIn', () => {\n    const code = 'pm.environment.replaceIn(\"{{baseUrl}}/api\");';\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe('bru.interpolate(\"{{baseUrl}}/api\");');\n  });\n\n  it('should translate pm.collectionVariables.replaceIn', () => {\n    const code = 'pm.collectionVariables.replaceIn(\"{{apiKey}}\");';\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe('bru.interpolate(\"{{apiKey}}\");');\n  });\n\n  it('should translate pm.globals.replaceIn in assignment', () => {\n    const code = 'const message = pm.globals.replaceIn(\"Welcome {{username}}!\");';\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toBe('const message = bru.interpolate(\"Welcome {{username}}!\");');\n  });\n\n  // pm.globals.has tests\n  it('should translate pm.globals.has', () => {\n    const code = 'pm.globals.has(\"token\");';\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('bru.getGlobalEnvVar(\"token\") !== undefined');\n    expect(translatedCode).toContain('bru.getGlobalEnvVar(\"token\") !== null');\n  });\n\n  it('should translate pm.globals.has in conditional', () => {\n    const code = 'if (pm.globals.has(\"authToken\")) { console.log(\"Token exists\"); }';\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('bru.getGlobalEnvVar(\"authToken\") !== undefined');\n    expect(translatedCode).toContain('bru.getGlobalEnvVar(\"authToken\") !== null');\n    expect(translatedCode).toContain('console.log(\"Token exists\");');\n  });\n\n  it('should translate pm.globals.has with variable assignment', () => {\n    const code = 'const hasGlobal = pm.globals.has(\"config\");';\n    const translatedCode = translateCode(code);\n\n    expect(translatedCode).toContain('const hasGlobal = bru.getGlobalEnvVar(\"config\") !== undefined && bru.getGlobalEnvVar(\"config\") !== null');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/postman-with-examples.spec.js",
    "content": "import postmanToBruno from '../src/postman/postman-to-bruno.js';\n\ndescribe('Postman to Bruno Converter with Examples', () => {\n  const postmanCollectionWithExamples = {\n    info: {\n      _postman_id: 'd7b47cc4-c3c5-4c9d-99d4-04b6025c9000',\n      name: 'collection with examples',\n      schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',\n      _exporter_id: '41238764'\n    },\n    item: [\n      {\n        name: 'New Request',\n        request: {\n          method: 'GET',\n          header: [],\n          url: {\n            raw: 'https://testbench-sanity.usebruno.com/ping',\n            protocol: 'https',\n            host: ['testbench-sanity', 'usebruno', 'com'],\n            path: ['ping']\n          }\n        },\n        response: [\n          {\n            name: 'Success Response',\n            originalRequest: {\n              method: 'GET',\n              header: [],\n              url: {\n                raw: 'https://testbench-sanity.usebruno.com/ping',\n                protocol: 'https',\n                host: ['testbench-sanity', 'usebruno', 'com'],\n                path: ['ping']\n              }\n            },\n            status: 'OK',\n            code: 200,\n            _postman_previewlanguage: 'json',\n            header: [\n              {\n                key: 'Content-Type',\n                value: 'application/json',\n                name: 'Content-Type',\n                description: '',\n                type: 'text'\n              },\n              {\n                key: 'x-powered-by',\n                value: 'Express'\n              }\n            ],\n            cookie: [],\n            body: '{\\n    \"ping\": \"pong\"\\n}'\n          },\n          {\n            name: 'Error Response',\n            originalRequest: {\n              method: 'GET',\n              header: [\n                {\n                  key: 'Content-Type',\n                  value: 'application/json',\n                  type: 'text'\n                }\n              ],\n              body: {\n                mode: 'raw',\n                raw: '{\\n    \"ping\": \"pong\"\\n}',\n                options: {\n                  raw: {\n                    language: 'json'\n                  }\n                }\n              },\n              url: {\n                raw: 'https://testbench-sanity.usebruno.com/ping',\n                protocol: 'https',\n                host: ['testbench-sanity', 'usebruno', 'com'],\n                path: ['ping']\n              }\n            },\n            status: 'Internal Server Error',\n            code: 500,\n            _postman_previewlanguage: 'json',\n            header: [\n              {\n                key: 'Content-Type',\n                value: 'application/json',\n                name: 'Content-Type',\n                description: '',\n                type: 'text'\n              }\n            ],\n            cookie: [],\n            body: '{\\n    \"error\": \"Internal Server Error\"\\n}'\n          }\n        ]\n      }\n    ]\n  };\n\n  test('should convert Postman collection with examples to Bruno format', async () => {\n    const brunoCollection = await postmanToBruno(postmanCollectionWithExamples);\n\n    expect(brunoCollection).toBeDefined();\n    expect(brunoCollection.name).toBe('collection with examples');\n    expect(brunoCollection.items).toHaveLength(1);\n\n    const request = brunoCollection.items[0];\n    expect(request.name).toBe('New Request');\n    expect(request.type).toBe('http-request');\n    expect(request.examples).toBeDefined();\n    expect(request.examples).toHaveLength(2);\n\n    // Test first example (Success Response)\n    const successExample = request.examples[0];\n    expect(successExample.name).toBe('Success Response');\n    expect(successExample.type).toBe('http-request');\n    expect(successExample.itemUid).toBe(request.uid);\n    expect(successExample.request.url).toBe('https://testbench-sanity.usebruno.com/ping');\n    expect(successExample.request.method).toBe('GET');\n    expect(successExample.response.status).toEqual(200);\n    expect(successExample.response.statusText).toBe('OK');\n    expect(successExample.response.body.content).toBe('{\\n    \"ping\": \"pong\"\\n}');\n    expect(successExample.response.body.type).toBe('json');\n    expect(successExample.response.headers).toHaveLength(2);\n    expect(successExample.response.headers[0].name).toBe('Content-Type');\n    expect(successExample.response.headers[0].value).toBe('application/json');\n    expect(successExample.response.headers[1].name).toBe('x-powered-by');\n    expect(successExample.response.headers[1].value).toBe('Express');\n\n    // Test second example (Error Response)\n    const errorExample = request.examples[1];\n    expect(errorExample.name).toBe('Error Response');\n    expect(errorExample.type).toBe('http-request');\n    expect(errorExample.itemUid).toBe(request.uid);\n    expect(errorExample.request.url).toBe('https://testbench-sanity.usebruno.com/ping');\n    expect(errorExample.request.method).toBe('GET');\n    expect(errorExample.response.status).toEqual(500);\n    expect(errorExample.response.statusText).toBe('Internal Server Error');\n    expect(errorExample.response.body.content).toBe('{\\n    \"error\": \"Internal Server Error\"\\n}');\n    expect(errorExample.response.body.type).toBe('json');\n    expect(errorExample.response.headers).toHaveLength(1);\n    expect(errorExample.response.headers[0].name).toBe('Content-Type');\n    expect(errorExample.response.headers[0].value).toBe('application/json');\n\n    // Test that the example has the original request headers from the originalRequest\n    expect(errorExample.request.headers).toHaveLength(1);\n    expect(errorExample.request.headers[0].name).toBe('Content-Type');\n    expect(errorExample.request.headers[0].value).toBe('application/json');\n\n    // Test that the example has the original request body from the originalRequest\n    expect(errorExample.request.body.mode).toBe('json');\n    expect(errorExample.request.body.json).toBe('{\\n    \"ping\": \"pong\"\\n}');\n  });\n\n  test('should handle Postman collection without examples', async () => {\n    const postmanCollectionWithoutExamples = {\n      info: {\n        _postman_id: 'd7b47cc4-c3c5-4c9d-99d4-04b6025c9000',\n        name: 'collection without examples',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',\n        _exporter_id: '41238764'\n      },\n      item: [\n        {\n          name: 'Simple Request',\n          request: {\n            method: 'GET',\n            header: [],\n            url: {\n              raw: 'https://api.example.com/test',\n              protocol: 'https',\n              host: ['api', 'example', 'com'],\n              path: ['test']\n            }\n          }\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(postmanCollectionWithoutExamples);\n\n    expect(brunoCollection).toBeDefined();\n    expect(brunoCollection.name).toBe('collection without examples');\n    expect(brunoCollection.items).toHaveLength(1);\n\n    const request = brunoCollection.items[0];\n    expect(request.name).toBe('Simple Request');\n    expect(request.type).toBe('http-request');\n    expect(request.examples).toBeUndefined();\n  });\n\n  test('should handle Postman collection with empty examples array', async () => {\n    const postmanCollectionWithEmptyExamples = {\n      info: {\n        _postman_id: 'd7b47cc4-c3c5-4c9d-99d4-04b6025c9000',\n        name: 'collection with empty examples',\n        schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',\n        _exporter_id: '41238764'\n      },\n      item: [\n        {\n          name: 'Request with Empty Examples',\n          request: {\n            method: 'GET',\n            header: [],\n            url: {\n              raw: 'https://api.example.com/test',\n              protocol: 'https',\n              host: ['api', 'example', 'com'],\n              path: ['test']\n            }\n          },\n          response: []\n        }\n      ]\n    };\n\n    const brunoCollection = await postmanToBruno(postmanCollectionWithEmptyExamples);\n\n    expect(brunoCollection).toBeDefined();\n    expect(brunoCollection.name).toBe('collection with empty examples');\n    expect(brunoCollection.items).toHaveLength(1);\n\n    const request = brunoCollection.items[0];\n    expect(request.name).toBe('Request with Empty Examples');\n    expect(request.type).toBe('http-request');\n    expect(request.examples).toBeDefined();\n    expect(request.examples).toHaveLength(0);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/utils/flatten.spec.js",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport { flattenObject } from '../../src/utils/flatten';\n\ndescribe('flattenObject', () => {\n  it('returns empty object for empty input object', () => {\n    expect(flattenObject({})).toEqual({});\n  });\n\n  it('flattens a simple nested object', () => {\n    const input = { user: { name: 'Tom', info: { id: 1 } } };\n    expect(flattenObject(input)).toEqual({\n      'user.name': 'Tom',\n      'user.info.id': 1\n    });\n  });\n\n  it('flattens arrays using JavaScript-style square bracket notation', () => {\n    const input = { tags: ['a', 'b'], nums: [1, 2] };\n    expect(flattenObject(input)).toEqual({\n      'tags[0]': 'a',\n      'tags[1]': 'b',\n      'nums[0]': 1,\n      'nums[1]': 2\n    });\n  });\n\n  it('handles null and primitive leaves correctly', () => {\n    const input = { a: null, b: true, c: 0, d: 'x' };\n    expect(flattenObject(input)).toEqual({\n      a: null,\n      b: true,\n      c: 0,\n      d: 'x'\n    });\n  });\n\n  it('flattens mixed nested objects and arrays', () => {\n    const input = {\n      user: { name: 'Tom', roles: ['admin', 'editor'] },\n      list: [{ id: 1 }, { id: 2 }]\n    };\n    expect(flattenObject(input)).toEqual({\n      'user.name': 'Tom',\n      'user.roles[0]': 'admin',\n      'user.roles[1]': 'editor',\n      'list[0].id': 1,\n      'list[1].id': 2\n    });\n  });\n\n  it('ignores empty arrays/objects (no keys produced for empty containers)', () => {\n    const input = { emptyObj: {}, emptyArr: [] };\n    expect(flattenObject(input)).toEqual({});\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/utils/getMemberExpressionString.test.js",
    "content": "const { describe, it, expect } = require('@jest/globals');\nconst { getMemberExpressionString } = require('../../src/utils/ast-utils');\nconst j = require('jscodeshift');\n\ndescribe('getMemberExpressionString', () => {\n  it('should correctly convert simple member expressions to strings', () => {\n    // Create a simple member expression: pm.environment.get\n    const memberExpr = j.memberExpression(\n      j.memberExpression(\n        j.identifier('pm'),\n        j.identifier('environment')\n      ),\n      j.identifier('get')\n    );\n\n    const result = getMemberExpressionString(memberExpr);\n    expect(result).toBe('pm.environment.get');\n  });\n\n  it('should handle computed properties with string literals', () => {\n    // Create a computed member expression: pm[\"environment\"][\"get\"]\n    const memberExpr = j.memberExpression(\n      j.memberExpression(\n        j.identifier('pm'),\n        j.literal('environment'),\n        true // computed\n      ),\n      j.literal('get'),\n      true // computed\n    );\n\n    const result = getMemberExpressionString(memberExpr);\n    expect(result).toBe('pm.environment.get');\n  });\n\n  it('should mark non-string computed properties as [computed]', () => {\n    // Create a computed member expression with variable: obj[varName]\n    const memberExpr = j.memberExpression(\n      j.identifier('obj'),\n      j.identifier('varName'),\n      true // computed\n    );\n\n    const result = getMemberExpressionString(memberExpr);\n    expect(result).toBe('obj.[computed]');\n  });\n\n  it('should handle basic identifiers', () => {\n    const identifier = j.identifier('pm');\n    const result = getMemberExpressionString(identifier);\n    expect(result).toBe('pm');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tests/wsdl/wsdl-to-bruno.spec.js",
    "content": "import { describe, it, expect } from '@jest/globals';\nimport wsdlToBruno from '../../src/wsdl/wsdl-to-bruno.js';\n\ndescribe('wsdl-to-bruno', () => {\n  it('should throw error for non-string input', async () => {\n    await expect(wsdlToBruno({})).rejects.toThrow('WSDL content must be a string');\n  });\n\n  it('should throw error for empty string input', async () => {\n    await expect(wsdlToBruno('')).rejects.toThrow('Import WSDL collection failed');\n  });\n\n  it('should throw error for invalid XML', async () => {\n    await expect(wsdlToBruno('<invalid>xml</invalid>')).rejects.toThrow('Import WSDL collection failed');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-converters/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"module\": \"ESNext\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\",\n    \"resolveJsonModule\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"moduleResolution\": \"node\",\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"allowJs\": true,\n    \"checkJs\": false,\n    \"types\": [\"node\"],\n    \"lib\": [\"ES2020\"],\n    \"typeRoots\": [\"../../node_modules/@types\", \"./node_modules/@types\"],\n    \"baseUrl\": \"../..\",\n    \"paths\": {\n      \"@usebruno/schema-types\": [\"packages/bruno-schema-types/dist/index.d.ts\"],\n      \"@usebruno/schema-types/*\": [\"packages/bruno-schema-types/dist/*\"],\n      \"@opencollection/types\": [\"node_modules/@opencollection/types/dist/opencollection.d.ts\"],\n      \"@opencollection/types/*\": [\"node_modules/@opencollection/types/dist/*\"]\n    }\n  },\n  \"include\": [\"src/**/*.ts\", \"src/**/*.tsx\", \"src/**/*.js\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "packages/bruno-converters/types/common.d.ts",
    "content": "export declare const uuid: () => string;\nexport declare const normalizeFileName: (name: string) => string;\nexport declare const validateSchema: (collection?: {}) => Promise<unknown>;\nexport declare const updateUidsInCollection: (_collection: any) => any;\nexport declare const transformItemsInCollection: (collection: any) => any;\nexport declare const hydrateSeqInCollection: (collection: any) => any;\n"
  },
  {
    "path": "packages/bruno-docs/package.json",
    "content": "{\n  \"name\": \"@usebruno/docs\",\n  \"version\": \"0.0.1\",\n  \"main\": \"src/index.js\",\n  \"files\": [\n    \"src\",\n    \"package.json\"\n  ],\n  \"dependencies\": {\n  }\n}"
  },
  {
    "path": "packages/bruno-docs/readme.md",
    "content": "# bruno-docs\n\nThis is a wip.\n\nWe have a request to generate docs in a html file that can be hosted on a server so that the visitor can view the API and make requests without downloading/installing anything."
  },
  {
    "path": "packages/bruno-electron/.gitignore",
    "content": "node_modules\nweb\nout\ndist\n.env\n\n// certs\nsectigo.*\n\npnpm-lock.yaml\npackage-lock.json\nyarn.lock\n"
  },
  {
    "path": "packages/bruno-electron/electron-builder-config.js",
    "content": "require('dotenv').config({ path: process.env.DOTENV_PATH });\n\nconst config = {\n  appId: 'com.usebruno.app',\n  productName: 'Bruno',\n  electronVersion: '37.6.1',\n  directories: {\n    buildResources: 'resources',\n    output: 'out'\n  },\n  extraResources: [\n    {\n      from: 'resources/data/sample-collection.json',\n      to: 'data/sample-collection.json'\n    }\n  ],\n  files: ['**/*'],\n  afterSign: 'notarize.js',\n  mac: {\n    artifactName: '${name}_${version}_${arch}_${os}.${ext}',\n    category: 'public.app-category.developer-tools',\n    target: [\n      {\n        target: 'dmg',\n        arch: ['x64', 'arm64']\n      },\n      {\n        target: 'zip',\n        arch: ['x64', 'arm64']\n      }\n    ],\n    icon: 'resources/icons/mac/icon.icns',\n    hardenedRuntime: true,\n    identity: 'Anoop MD (W7LPPWA48L)',\n    entitlements: 'resources/entitlements.mac.plist',\n    entitlementsInherit: 'resources/entitlements.mac.plist',\n    notarize: false,\n    protocols: [\n      {\n        name: 'Bruno',\n        schemes: [\n          'bruno'\n        ]\n      }\n    ]\n  },\n  linux: {\n    artifactName: '${name}_${version}_${arch}_${os}.${ext}',\n    icon: 'resources/icons/png',\n    target: [\n      {\n        target: 'AppImage',\n        arch: ['x64', 'arm64']\n      },\n      {\n        target: 'deb',\n        arch: ['x64', 'arm64']\n      },\n      {\n        target: 'rpm',\n        arch: ['x64', 'arm64']\n      }\n    ],\n    protocols: [\n      {\n        name: 'Bruno',\n        schemes: ['bruno']\n      }\n    ],\n    category: 'Development',\n    desktop: {\n      MimeType: 'x-scheme-handler/bruno;'\n    }\n  },\n  deb: {\n    // Docs: https://www.electron.build/configuration/linux#debian-package-options\n    depends: [\n      'libgtk-3-0',\n      'libnotify4',\n      'libnss3',\n      'libxss1',\n      'libxtst6',\n      'xdg-utils',\n      'libatspi2.0-0',\n      'libuuid1',\n      'libsecret-1-0',\n      'libasound2' // #1036\n    ]\n  },\n  win: {\n    artifactName: '${name}_${version}_${arch}_win.${ext}',\n    icon: 'resources/icons/win/icon.ico',\n    target: [\n      {\n        target: 'nsis',\n        arch: ['x64', 'arm64']\n      }\n    ],\n    sign: null,\n    publisherName: 'Bruno Software Inc'\n  },\n  nsis: {\n    oneClick: false,\n    allowToChangeInstallationDirectory: true,\n    allowElevation: true,\n    createDesktopShortcut: true,\n    createStartMenuShortcut: true\n  }\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "packages/bruno-electron/notarize.js",
    "content": "require('dotenv').config({ path: process.env.DOTENV_PATH });\nconst fs = require('fs');\nconst path = require('path');\nconst electron_notarize = require('electron-notarize');\n\nconst notarize = async function (params) {\n  if (process.platform !== 'darwin') {\n    return;\n  }\n\n  let appId = 'com.usebruno.app';\n\n  let appPath = path.join(params.appOutDir, `${params.packager.appInfo.productFilename}.app`);\n  if (!fs.existsSync(appPath)) {\n    console.error(`Cannot find application at: ${appPath}`);\n    return;\n  }\n\n  console.log(`Notarizing ${appId} found at ${appPath} using Apple ID ${process.env.APPLE_ID}`);\n\n  try {\n    await electron_notarize.notarize({\n      appBundleId: appId,\n      appPath: appPath,\n      appleId: process.env.APPLE_ID,\n      appleIdPassword: process.env.APPLE_ID_PASSWORD,\n      ascProvider: 'W7LPPWA48L'\n    });\n  } catch (error) {\n    console.error(error);\n  }\n\n  console.log(`Done notarizing ${appId}`);\n};\n\nmodule.exports = notarize;\n"
  },
  {
    "path": "packages/bruno-electron/package.json",
    "content": "{\n  \"version\": \"2.0.0\",\n  \"name\": \"bruno\",\n  \"description\": \"Opensource API Client for Exploring and Testing APIs\",\n  \"homepage\": \"https://www.usebruno.com\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/usebruno/bruno.git\"\n  },\n  \"private\": true,\n  \"main\": \"src/index.js\",\n  \"author\": \"Anoop M D <anoop.md1421@gmail.com> (https://helloanoop.com/)\",\n  \"scripts\": {\n    \"clean\": \"rimraf dist\",\n    \"dev\": \"electron .\",\n    \"debug\": \"electron . --inspect=9229\",\n    \"dist:mac\": \"electron-builder --mac --config electron-builder-config.js\",\n    \"dist:win\": \"electron-builder --win --config electron-builder-config.js\",\n    \"dist:linux\": \"electron-builder --linux AppImage --config electron-builder-config.js\",\n    \"dist:deb\": \"electron-builder --linux deb --config electron-builder-config.js\",\n    \"dist:rpm\": \"electron-builder --linux rpm --config electron-builder-config.js\",\n    \"dist:snap\": \"electron-builder --linux snap --config electron-builder-config.js\",\n    \"pack\": \"electron-builder --dir\",\n    \"test\": \"node --experimental-vm-modules $(npx which jest)\"\n  },\n  \"jest\": {\n    \"modulePaths\": [\n      \"node_modules\"\n    ]\n  },\n  \"dependencies\": {\n    \"@aws-sdk/credential-providers\": \"3.750.0\",\n    \"@grpc/grpc-js\": \"^1.13.2\",\n    \"@grpc/proto-loader\": \"^0.7.13\",\n    \"@lydell/node-pty\": \"^1.1.0\",\n    \"@usebruno/common\": \"0.1.0\",\n    \"@usebruno/converters\": \"^0.1.0\",\n    \"@usebruno/filestore\": \"^0.1.0\",\n    \"@usebruno/js\": \"0.12.0\",\n    \"@usebruno/lang\": \"0.12.0\",\n    \"@usebruno/node-machine-id\": \"^2.0.0\",\n    \"@usebruno/requests\": \"^0.1.0\",\n    \"@usebruno/schema\": \"0.7.0\",\n    \"about-window\": \"^1.15.2\",\n    \"adm-zip\": \"^0.5.16\",\n    \"archiver\": \"^7.0.1\",\n    \"aws4-axios\": \"^3.3.0\",\n    \"axios\": \"^1.8.3\",\n    \"axios-ntlm\": \"^1.4.2\",\n    \"chai\": \"^4.3.7\",\n    \"chokidar\": \"^3.5.3\",\n    \"content-disposition\": \"^0.5.4\",\n    \"decomment\": \"^0.9.5\",\n    \"diff\": \"^8.0.3\",\n    \"dotenv\": \"^16.0.3\",\n    \"electron-is-dev\": \"^2.0.0\",\n    \"electron-notarize\": \"^1.2.2\",\n    \"electron-store\": \"^8.1.0\",\n    \"electron-util\": \"^0.17.2\",\n    \"extract-zip\": \"^2.0.1\",\n    \"form-data\": \"^4.0.0\",\n    \"fs-extra\": \"^10.1.0\",\n    \"graphql\": \"^16.6.0\",\n    \"hexy\": \"^0.3.5\",\n    \"http-proxy-agent\": \"^7.0.0\",\n    \"https-proxy-agent\": \"^7.0.2\",\n    \"iconv-lite\": \"^0.6.3\",\n    \"is-valid-path\": \"^0.1.1\",\n    \"js-yaml\": \"^4.1.1\",\n    \"lodash\": \"^4.17.21\",\n    \"mime-types\": \"^2.1.35\",\n    \"nanoid\": \"3.3.8\",\n    \"qs\": \"^6.14.1\",\n    \"simple-git\": \"^3.22.0\",\n    \"socks-proxy-agent\": \"^8.0.2\",\n    \"tough-cookie\": \"^6.0.0\",\n    \"uuid\": \"^9.0.0\",\n    \"yup\": \"^0.32.11\"\n  },\n  \"optionalDependencies\": {\n    \"dmg-license\": \"^1.0.11\"\n  },\n  \"devDependencies\": {\n    \"electron\": \"~37.6.1\",\n    \"electron-builder\": \"^24.13.3\",\n    \"electron-devtools-installer\": \"^4.0.0\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-electron/readme.md",
    "content": "# bruno-electron\n\n```bash\n# electron dev\nnpm start\n\n# generate pfx file for signing windows build\nopenssl pkcs12 -export -inkey sectigo.key -in sectigo.pem -out sectigo.pfx\n```\n"
  },
  {
    "path": "packages/bruno-electron/resources/data/sample-collection.json",
    "content": "{\n    \"version\": \"1\",\n    \"uid\": \"1igyn4u00000000000232\",\n    \"name\": \"Sample API Collection\",\n    \"items\": [\n      {\n        \"uid\": \"1igyn4u00000000000001\",\n        \"type\": \"http-request\",\n        \"name\": \"Get Users\",\n        \"seq\": 1,\n        \"request\": {\n          \"url\": \"https://jsonplaceholder.typicode.com/users\",\n          \"method\": \"GET\",\n          \"headers\": [],\n          \"params\": [],\n          \"body\": {\n            \"mode\": \"none\"\n          },\n          \"auth\": {\n            \"mode\": \"none\"\n          },\n          \"script\": {\n            \"req\": \"\",\n            \"res\": \"\"\n          },\n          \"vars\": {\n            \"req\": [],\n            \"res\": []\n          },\n          \"assertions\": [],\n          \"tests\": \"\",\n          \"docs\": \"This request retrieves a list of users from the JSONPlaceholder API.\"\n        }\n      }\n    ],\n    \"environments\": [],\n    \"activeEnvironmentUid\": null,\n    \"root\": {\n      \"request\": {\n        \"headers\": [],\n        \"auth\": {\n          \"mode\": \"none\"\n        },\n        \"script\": {\n          \"req\": \"\",\n          \"res\": \"\"\n        },\n        \"vars\": {\n          \"req\": [],\n          \"res\": []\n        },\n        \"tests\": \"\"\n      }\n    }\n  }\n  "
  },
  {
    "path": "packages/bruno-electron/resources/entitlements.mac.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>com.apple.security.cs.allow-jit</key>\n    <true/>\n    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n    <true/>\n  </dict>\n</plist>"
  },
  {
    "path": "packages/bruno-electron/src/about/about.css",
    "content": ".versions {\n  -webkit-user-select: text;\n  user-select: text;\n}\n.title {\n  -webkit-user-select: text;\n  user-select: text;\n}\n"
  },
  {
    "path": "packages/bruno-electron/src/app/about-bruno.js",
    "content": "module.exports = function aboutBruno({ version }) {\n  const currentYear = new Date().getFullYear();\n  return `\n    <!DOCTYPE html>\n    <html lang=\"en\">\n    <head>\n        <meta charset=\"utf-8\">\n        <meta name=\"viewport\" content=\"width=device-width, minimum-scale=1.0, initial-scale=1, user-scalable=yes\">\n        <title>About Bruno</title>\n        <style>\n            body {\n                font-family: Arial, sans-serif;\n                text-align: center;\n                margin: 0;\n                padding: 10px;\n                background-color: #f4f4f4;\n                color: #333;\n            }\n            .logo {\n                margin-top: 0px;\n            }\n            .title {\n                font-size: 24px;\n                margin-top: 5px;\n                font-weight: bold;\n                color: #222;\n            }\n            .description {\n                font-size: 12px;\n                color: #222;\n                margin-top: 5px;\n            }\n            .buttons {\n                margin-top: 5px;\n            }\n            .footer {\n                margin-top: 5px;\n                padding: 5px;\n                font-size: 14px;\n                color: #555;\n            }\n            .link {\n                display: inline-block;\n                margin-top: 10px;\n                padding: 10px 15px;\n                background-color: #F4AA41;\n                color: white;\n                text-decoration: none;\n                border-radius: 5px;\n                cursor: pointer;\n                transition: background 0.3s;\n            }\n            .link:hover {\n                background-color: #F4AA41;\n            }\n        </style>\n    </head>\n    <body>\n      <div class=\"logo\">\n      </div>\n        <svg id=\"emoji\" width=\"100\" viewBox=\"0 0 72 72\" xmlns=\"http://www.w3.org/2000/svg\">\n          <g id=\"color\">\n            <path\n              fill=\"#F4AA41\"\n              stroke=\"none\"\n              d=\"M23.5,14.5855l-4.5,1.75l-7.25,8.5l-4.5,10.75l2,5.25c1.2554,3.7911,3.5231,7.1832,7.25,10l2.5-3.3333 c0,0,3.8218,7.7098,10.7384,8.9598c0,0,10.2616,1.936,15.5949-0.8765c3.4203-1.8037,4.4167-4.4167,4.4167-4.4167l3.4167-3.4167 l1.5833,2.3333l2.0833-0.0833l5.4167-7.25L64,37.3355l-0.1667-4.5l-2.3333-5.5l-4.8333-7.4167c0,0-2.6667-4.9167-8.1667-3.9167 c0,0-6.5-4.8333-11.8333-4.0833S32.0833,10.6688,23.5,14.5855z\"\n            />\n            <polygon\n              fill=\"#EA5A47\"\n              stroke=\"none\"\n              points=\"36,47.2521 32.9167,49.6688 30.4167,49.6688 30.3333,53.5021 31.0833,57.0021 32.1667,58.9188 35,60.4188 39.5833,59.8355 41.1667,58.0855 42.1667,53.8355 41.9167,49.8355 39.9167,50.0855\"\n            />\n            <polygon\n              fill=\"#3F3F3F\"\n              stroke=\"none\"\n              points=\"32.5,36.9188 30.9167,40.6688 33.0833,41.9188 34.3333,42.4188 38.6667,42.5855 41.5833,40.3355 39.8333,37.0855\"\n            />\n          </g>\n          <g id=\"hair\" />\n          <g id=\"skin\" />\n          <g id=\"skin-shadow\" />\n          <g id=\"line\">\n            <path\n              fill=\"#000000\"\n              stroke=\"none\"\n              d=\"M29.5059,30.1088c0,0-1.8051,1.2424-2.7484,0.6679c-0.9434-0.5745-1.2424-1.8051-0.6679-2.7484 s1.805-1.2424,2.7484-0.6679S29.5059,30.1088,29.5059,30.1088z\"\n            />\n            <path\n              fill=\"none\"\n              stroke=\"#000000\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              strokeMiterlimit=\"10\"\n              strokeWidth=\"2\"\n              d=\"M33.1089,37.006h6.1457c0.4011,0,0.7634,0.2397,0.9203,0.6089l1.1579,2.7245l-2.1792,1.1456 c-0.6156,0.3236-1.3654-0.0645-1.4567-0.754\"\n            />\n            <path\n              fill=\"none\"\n              stroke=\"#000000\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              strokeMiterlimit=\"10\"\n              strokeWidth=\"2\"\n              d=\"M34.7606,40.763c-0.1132,0.6268-0.7757,0.9895-1.3647,0.7471l-2.3132-0.952l1.0899-2.9035 c0.1465-0.3901,0.5195-0.6486,0.9362-0.6486\"\n            />\n            <path\n              fill=\"none\"\n              stroke=\"#000000\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              strokeMiterlimit=\"10\"\n              strokeWidth=\"2\"\n              d=\"M30.4364,50.0268c0,0-0.7187,8.7934,3.0072,9.9375c2.6459,0.8125,5.1497,0.5324,6.0625-0.25 c0.875-0.75,2.6323-4.4741,1.8267-9.6875\"\n            />\n            <path\n              fill=\"#000000\"\n              stroke=\"none\"\n              d=\"M44.2636,30.1088c0,0,1.805,1.2424,2.7484,0.6679c0.9434-0.5745,1.2424-1.8051,0.6679-2.7484 c-0.5745-0.9434-1.805-1.2424-2.7484-0.6679C43.9881,27.9349,44.2636,30.1088,44.2636,30.1088z\"\n            />\n            <path\n              fill=\"none\"\n              stroke=\"#000000\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              strokeMiterlimit=\"10\"\n              strokeWidth=\"2\"\n              d=\"M25.6245,42.8393c-0.475,3.6024,2.2343,5.7505,4.2847,6.8414c1.1968,0.6367,2.6508,0.5182,3.7176-0.3181l2.581-2.0233l2.581,2.0233 c1.0669,0.8363,2.5209,0.9548,3.7176,0.3181c2.0504-1.0909,4.7597-3.239,4.2847-6.8414\"\n            />\n            <path\n              fill=\"none\"\n              stroke=\"#000000\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              strokeMiterlimit=\"10\"\n              strokeWidth=\"2\"\n              d=\"M19.9509,28.3572c-2.3166,5.1597-0.5084,13.0249,0.119,15.3759c0.122,0.4571,0.0755,0.9355-0.1271,1.3631l-1.9874,4.1937 c-0.623,1.3146-2.3934,1.5533-3.331,0.4409c-3.1921-3.7871-8.5584-11.3899-6.5486-16.686 c7.0625-18.6104,15.8677-18.1429,15.8677-18.1429c2.8453-1.9336,13.1042-6.9375,24.8125,0.875c0,0,8.6323-1.7175,14.9375,16.9375 c1.8036,5.3362-3.4297,12.8668-6.5506,16.6442c-0.9312,1.127-2.7162,0.8939-3.3423-0.4272l-1.9741-4.1656 c-0.2026-0.4275-0.2491-0.906-0.1271-1.3631c0.6275-2.3509,2.4356-10.2161,0.119-15.3759\"\n            />\n            <path\n              fill=\"none\"\n              stroke=\"#000000\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              strokeMiterlimit=\"10\"\n              strokeWidth=\"2\"\n              d=\"M52.6309,46.4628c0,0-3.0781,6.7216-7.8049,8.2712\"\n            />\n            <path\n              fill=\"none\"\n              stroke=\"#000000\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              strokeMiterlimit=\"10\"\n              strokeWidth=\"2\"\n              d=\"M19.437,46.969c0,0,3.0781,6.0823,7.8049,7.632\"\n            />\n            <line\n              x1=\"36.2078\"\n              x2=\"36.2078\"\n              y1=\"47.3393\"\n              y2=\"44.3093\"\n              fill=\"none\"\n              stroke=\"#000000\"\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              strokeMiterlimit=\"10\"\n              strokeWidth=\"2\"\n            />\n          </g>\n        </svg>\n      <h2 class=\"title\">Bruno ${version}</h2>\n      <footer class=\"footer\">\n          ©${currentYear} Bruno Software Inc\n      </footer>\n    </body>\n    </html>\n  `;\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/app/apiSpecs.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst { dialog, ipcMain } = require('electron');\nconst { normalizeAndResolvePath } = require('../utils/filesystem');\nconst { generateUidBasedOnHash } = require('../utils/common');\nconst {\n  addApiSpecToWorkspace,\n  readWorkspaceConfig,\n  getWorkspaceUid\n} = require('../utils/workspace-config');\n\nconst DEFAULT_WORKSPACE_NAME = 'My Workspace';\n\nconst normalizeWorkspaceConfig = (config) => {\n  return {\n    ...config,\n    name: config.info?.name,\n    type: config.info?.type,\n    collections: config.collections || [],\n    apiSpecs: config.specs || []\n  };\n};\n\nconst prepareWorkspaceConfigForClient = (workspaceConfig, isDefault) => {\n  if (isDefault) {\n    return {\n      ...workspaceConfig,\n      name: DEFAULT_WORKSPACE_NAME,\n      type: 'default'\n    };\n  }\n  return workspaceConfig;\n};\n\nconst openApiSpecDialog = async (win, watcher, options = {}) => {\n  const { filePaths } = await dialog.showOpenDialog(win, {\n    properties: ['openFile', 'createFile']\n  });\n\n  if (filePaths && filePaths[0]) {\n    const resolvedPath = normalizeAndResolvePath(filePaths[0]);\n    try {\n      await openApiSpec(win, watcher, resolvedPath, options);\n    } catch (err) {\n      console.error(`[ERROR] Cannot open API spec: \"${resolvedPath}\"`);\n    }\n  }\n};\n\nconst openApiSpec = async (win, watcher, apiSpecPath, options = {}) => {\n  try {\n    const uid = generateUidBasedOnHash(apiSpecPath);\n\n    if (options.workspacePath) {\n      const workspaceFilePath = path.join(options.workspacePath, 'workspace.yml');\n\n      if (fs.existsSync(workspaceFilePath)) {\n        const workspaceConfig = readWorkspaceConfig(options.workspacePath);\n        const specs = workspaceConfig.specs || [];\n\n        const specName = path.basename(apiSpecPath, path.extname(apiSpecPath));\n\n        const existingSpec = specs.find((a) => {\n          if (!a.path) return false;\n          const existingPath = path.isAbsolute(a.path)\n            ? a.path\n            : path.resolve(options.workspacePath, a.path);\n          return existingPath === apiSpecPath || a.name === specName;\n        });\n\n        if (!existingSpec) {\n          await addApiSpecToWorkspace(options.workspacePath, {\n            name: specName,\n            path: apiSpecPath\n          });\n\n          const updatedConfig = readWorkspaceConfig(options.workspacePath);\n          const normalizedConfig = normalizeWorkspaceConfig(updatedConfig);\n          const workspaceUid = getWorkspaceUid(options.workspacePath);\n          const isDefault = workspaceUid === 'default';\n          const configForClient = prepareWorkspaceConfigForClient(normalizedConfig, isDefault);\n          win.webContents.send('main:workspace-config-updated', options.workspacePath, workspaceUid, configForClient);\n        }\n      }\n    }\n\n    if (!watcher.hasWatcher(apiSpecPath)) {\n      ipcMain.emit('main:apispec-opened', win, apiSpecPath, uid, options.workspacePath);\n    } else {\n      win.webContents.send('main:apispec-tree-updated', 'addFile', {\n        pathname: apiSpecPath,\n        uid: uid,\n        raw: require('fs').readFileSync(apiSpecPath, 'utf8'),\n        name: require('path').basename(apiSpecPath, require('path').extname(apiSpecPath)),\n        filename: require('path').basename(apiSpecPath),\n        json: (() => {\n          const ext = require('path').extname(apiSpecPath).toLowerCase();\n          const content = require('fs').readFileSync(apiSpecPath, 'utf8');\n          if (ext === '.yaml' || ext === '.yml') {\n            return require('js-yaml').load(content);\n          } else if (ext === '.json') {\n            return JSON.parse(content);\n          }\n          return null;\n        })()\n      });\n    }\n  } catch (err) {\n    if (!options.dontSendDisplayErrors) {\n      win.webContents.send('main:display-error', {\n        error: err.message || 'An error occurred while opening the apiSpec'\n      });\n    }\n  }\n};\n\nmodule.exports = {\n  openApiSpec,\n  openApiSpecDialog\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/app/apiSpecsWatcher.js",
    "content": "const _ = require('lodash');\nconst fs = require('fs');\nconst path = require('path');\nconst chokidar = require('chokidar');\nconst { getApiSpecUid } = require('../cache/apiSpecUids');\nconst yaml = require('js-yaml');\nconst { isDirectory } = require('../utils/filesystem');\nconst { safeParseJSON } = require('../utils/common');\n\nconst hasApiSpecExtension = (filename) => {\n  if (!filename || typeof filename !== 'string') return false;\n  return ['yaml', 'yml', 'json'].some((ext) => filename.toLowerCase().endsWith(`.${ext}`));\n};\n\nconst parseApiSpecContent = (pathname) => {\n  const extension = path.extname(pathname).toLowerCase();\n  let content = fs.readFileSync(pathname, 'utf8');\n\n  if (extension === '.yaml' || extension === '.yml') {\n    return yaml.load(content);\n  } else if (extension === '.json') {\n    return safeParseJSON(content);\n  }\n  return null;\n};\n\nconst hydrateApiSpecWithUuid = (apiSpec, pathname) => {\n  apiSpec.uid = getApiSpecUid(pathname);\n  return apiSpec;\n};\n\nconst add = async (win, pathname) => {\n  if (!hasApiSpecExtension(pathname)) return;\n  try {\n    const basename = path.basename(pathname);\n    const file = {};\n    const apiSpecContent = parseApiSpecContent(pathname);\n\n    file.raw = fs.readFileSync(pathname, 'utf8');\n    file.name = apiSpecContent?.info?.title || basename.split('.')[0];\n    file.filename = basename;\n    file.pathname = pathname;\n    file.json = apiSpecContent;\n    hydrateApiSpecWithUuid(file, pathname);\n    win.webContents.send('main:apispec-tree-updated', 'addFile', file);\n  } catch (err) {\n    console.error(err);\n  }\n};\n\nconst change = async (win, pathname) => {\n  if (!hasApiSpecExtension(pathname)) return;\n  try {\n    const basename = path.basename(pathname);\n    const file = {};\n    const apiSpecContent = parseApiSpecContent(pathname);\n\n    file.raw = fs.readFileSync(pathname, 'utf8');\n    file.name = apiSpecContent?.info?.title || basename.split('.')[0];\n    file.filename = basename;\n    file.pathname = pathname;\n    file.json = apiSpecContent;\n    hydrateApiSpecWithUuid(file, pathname);\n    win.webContents.send('main:apispec-tree-updated', 'changeFile', file);\n  } catch (err) {\n    console.error(err);\n  }\n};\n\nclass ApiSpecWatcher {\n  constructor() {\n    this.watchers = {};\n    this.watcherWorkspaces = {};\n  }\n\n  addWatcher(win, watchPath, apiSpecUid, brunoConfig, workspacePath = null) {\n    // Avoid creating watcher for directories\n    if (isDirectory(watchPath)) return;\n\n    if (this.watchers[watchPath]) {\n      this.watchers[watchPath].close();\n    }\n\n    if (workspacePath) {\n      this.watcherWorkspaces[watchPath] = workspacePath;\n    }\n\n    // Always ignore node_modules and .git, regardless of user config\n    // This prevents infinite loops with symlinked directories (e.g., npm workspaces)\n    const defaultIgnores = ['node_modules', '.git'];\n    const userIgnores = brunoConfig?.ignore || [];\n    const ignores = [...new Set([...defaultIgnores, ...userIgnores])];\n\n    const self = this;\n    setTimeout(() => {\n      const watcher = chokidar.watch(watchPath, {\n        ignoreInitial: false,\n        usePolling: watchPath.startsWith('\\\\\\\\') ? true : false,\n        ignored: (filepath) => {\n          const normalizedPath = filepath.replace(/\\\\/g, '/');\n          const relativePath = path.relative(watchPath, normalizedPath);\n\n          // Check if any path segment matches a default ignore pattern (handles symlinks)\n          const pathSegments = relativePath.split(path.sep);\n          if (pathSegments.some((segment) => defaultIgnores.includes(segment))) {\n            return true;\n          }\n\n          return ignores.some((ignorePattern) => {\n            const normalizedIgnorePattern = ignorePattern.replace(/\\\\/g, '/');\n            return relativePath === normalizedIgnorePattern || relativePath.startsWith(normalizedIgnorePattern);\n          });\n        },\n        persistent: true,\n        ignorePermissionErrors: true,\n        awaitWriteFinish: {\n          stabilityThreshold: 80,\n          pollInterval: 10\n        },\n        depth: 20\n      });\n\n      watcher\n        .on('add', (pathname) => add(win, pathname, apiSpecUid, watchPath, workspacePath))\n        .on('change', (pathname) => change(win, pathname, apiSpecUid, watchPath, workspacePath));\n\n      self.watchers[watchPath] = watcher;\n    }, 100);\n  }\n\n  hasWatcher(watchPath) {\n    return this.watchers[watchPath];\n  }\n\n  removeWatcher(watchPath, win) {\n    if (this.watchers[watchPath]) {\n      this.watchers[watchPath].close();\n      this.watchers[watchPath] = null;\n    }\n    if (this.watcherWorkspaces[watchPath]) {\n      delete this.watcherWorkspaces[watchPath];\n    }\n  }\n}\n\nmodule.exports = ApiSpecWatcher;\n"
  },
  {
    "path": "packages/bruno-electron/src/app/collection-watcher.js",
    "content": "const _ = require('lodash');\nconst fs = require('fs');\nconst path = require('path');\nconst chokidar = require('chokidar');\nconst {\n  hasRequestExtension,\n  isWSLPath,\n  normalizeAndResolvePath,\n  sizeInMB,\n  getCollectionFormat\n} = require('../utils/filesystem');\nconst {\n  parseEnvironment,\n  parseRequest,\n  parseRequestViaWorker,\n  parseCollection,\n  parseFolder\n} = require('@usebruno/filestore');\n\nconst { uuid } = require('../utils/common');\nconst { getRequestUid } = require('../cache/requestUids');\nconst { decryptStringSafe } = require('../utils/encryption');\nconst { setBrunoConfig } = require('../store/bruno-config');\nconst EnvironmentSecretsStore = require('../store/env-secrets');\nconst UiStateSnapshot = require('../store/ui-state-snapshot');\nconst { parseFileMeta, hydrateRequestWithUuid } = require('../utils/collection');\nconst { parseLargeRequestWithRedaction } = require('../utils/parse');\nconst { transformBrunoConfigAfterRead } = require('../utils/transformBrunoConfig');\nconst dotEnvWatcher = require('./dotenv-watcher');\n\nconst MAX_FILE_SIZE = 2.5 * 1024 * 1024;\n\nconst environmentSecretsStore = new EnvironmentSecretsStore();\n\nconst isBrunoConfigFile = (pathname, collectionPath) => {\n  const dirname = path.dirname(pathname);\n  const basename = path.basename(pathname);\n\n  return path.normalize(dirname) === path.normalize(collectionPath) && basename === 'bruno.json';\n};\n\nconst isEnvironmentsFolder = (pathname, collectionPath) => {\n  const dirname = path.dirname(pathname);\n  const envDirectory = path.join(collectionPath, 'environments');\n\n  return path.normalize(dirname) === path.normalize(envDirectory);\n};\n\nconst isFolderRootFile = (pathname, collectionPath) => {\n  const basename = path.basename(pathname);\n  const format = getCollectionFormat(collectionPath);\n\n  if (format === 'yml') {\n    return basename === 'folder.yml';\n  } else if (format === 'bru') {\n    return basename === 'folder.bru';\n  }\n\n  return false;\n};\n\nconst isCollectionRootFile = (pathname, collectionPath) => {\n  const dirname = path.dirname(pathname);\n  const basename = path.basename(pathname);\n\n  // return if we are not at the root of the collection\n  if (path.normalize(dirname) !== path.normalize(collectionPath)) {\n    return false;\n  }\n\n  return basename === 'collection.bru' || basename === 'opencollection.yml';\n};\n\nconst envHasSecrets = (environment = {}) => {\n  const secrets = _.filter(environment.variables, (v) => v.secret);\n\n  return secrets && secrets.length > 0;\n};\n\nconst hydrateCollectionRootWithUuid = (collectionRoot) => {\n  const params = _.get(collectionRoot, 'request.params', []);\n  const headers = _.get(collectionRoot, 'request.headers', []);\n  const requestVars = _.get(collectionRoot, 'request.vars.req', []);\n  const responseVars = _.get(collectionRoot, 'request.vars.res', []);\n\n  params.forEach((param) => (param.uid = uuid()));\n  headers.forEach((header) => (header.uid = uuid()));\n  requestVars.forEach((variable) => (variable.uid = uuid()));\n  responseVars.forEach((variable) => (variable.uid = uuid()));\n\n  return collectionRoot;\n};\n\nconst addEnvironmentFile = async (win, pathname, collectionUid, collectionPath) => {\n  try {\n    const basename = path.basename(pathname);\n    const file = {\n      meta: {\n        collectionUid,\n        pathname,\n        name: basename\n      }\n    };\n\n    const format = getCollectionFormat(collectionPath);\n    let content = fs.readFileSync(pathname, 'utf8');\n\n    file.data = await parseEnvironment(content, { format });\n\n    // Extract name by removing the extension\n    const ext = path.extname(basename);\n    file.data.name = basename.substring(0, basename.length - ext.length);\n    file.data.uid = getRequestUid(pathname);\n\n    _.each(_.get(file, 'data.variables', []), (variable) => (variable.uid = uuid()));\n\n    // hydrate environment variables with secrets\n    if (envHasSecrets(file.data)) {\n      const envSecrets = environmentSecretsStore.getEnvSecrets(collectionPath, file.data);\n      _.each(envSecrets, (secret) => {\n        const variable = _.find(file.data.variables, (v) => v.name === secret.name);\n        if (variable && secret.value) {\n          const decryptionResult = decryptStringSafe(secret.value);\n          variable.value = decryptionResult.value;\n        }\n      });\n    }\n\n    win.webContents.send('main:collection-tree-updated', 'addEnvironmentFile', file);\n  } catch (err) {\n    console.error('Error processing environment file: ', err);\n  }\n};\n\nconst changeEnvironmentFile = async (win, pathname, collectionUid, collectionPath) => {\n  try {\n    const basename = path.basename(pathname);\n    const file = {\n      meta: {\n        collectionUid,\n        pathname,\n        name: basename\n      }\n    };\n\n    const format = getCollectionFormat(collectionPath);\n    const content = fs.readFileSync(pathname, 'utf8');\n\n    file.data = await parseEnvironment(content, { format });\n\n    // Extract name by removing the extension\n    const ext = path.extname(basename);\n    file.data.name = basename.substring(0, basename.length - ext.length);\n    file.data.uid = getRequestUid(pathname);\n    _.each(_.get(file, 'data.variables', []), (variable) => (variable.uid = uuid()));\n\n    // hydrate environment variables with secrets\n    if (envHasSecrets(file.data)) {\n      const envSecrets = environmentSecretsStore.getEnvSecrets(collectionPath, file.data);\n      _.each(envSecrets, (secret) => {\n        const variable = _.find(file.data.variables, (v) => v.name === secret.name);\n        if (variable && secret.value) {\n          const decryptionResult = decryptStringSafe(secret.value);\n          variable.value = decryptionResult.value;\n        }\n      });\n    }\n\n    // we are reusing the addEnvironmentFile event itself\n    // this is because the uid of the pathname remains the same\n    // and the collection tree will be able to update the existing environment\n    win.webContents.send('main:collection-tree-updated', 'addEnvironmentFile', file);\n  } catch (err) {\n    console.error(err);\n  }\n};\n\nconst unlinkEnvironmentFile = async (win, pathname, collectionUid) => {\n  try {\n    const file = {\n      meta: {\n        collectionUid,\n        pathname,\n        name: path.basename(pathname)\n      },\n      data: {\n        uid: getRequestUid(pathname),\n        name: path.basename(pathname).substring(0, path.basename(pathname).length - 4)\n      }\n    };\n\n    win.webContents.send('main:collection-tree-updated', 'unlinkEnvironmentFile', file);\n  } catch (err) {\n    console.error(err);\n  }\n};\n\nconst add = async (win, pathname, collectionUid, collectionPath, useWorkerThread, watcher) => {\n  console.log(`watcher add: ${pathname}`);\n\n  if (isBrunoConfigFile(pathname, collectionPath)) {\n    try {\n      const content = fs.readFileSync(pathname, 'utf8');\n      let brunoConfig = JSON.parse(content);\n\n      // Transform the config to add exists metadata for protobuf files and import paths\n      brunoConfig = await transformBrunoConfigAfterRead(brunoConfig, collectionPath);\n\n      setBrunoConfig(collectionUid, brunoConfig);\n\n      const payload = {\n        collectionUid,\n        brunoConfig: brunoConfig\n      };\n\n      win.webContents.send('main:bruno-config-update', payload);\n    } catch (err) {\n      console.error(err);\n    }\n  }\n\n  if (isEnvironmentsFolder(pathname, collectionPath)) {\n    return addEnvironmentFile(win, pathname, collectionUid, collectionPath);\n  }\n\n  if (isCollectionRootFile(pathname, collectionPath)) {\n    const format = getCollectionFormat(collectionPath);\n    const file = {\n      meta: {\n        collectionUid,\n        pathname,\n        name: path.basename(pathname),\n        collectionRoot: true\n      }\n    };\n\n    try {\n      let content = fs.readFileSync(pathname, 'utf8');\n      let parsed = await parseCollection(content, { format });\n\n      let collectionRoot, brunoConfig;\n      if (format === 'yml') {\n        collectionRoot = parsed.collectionRoot;\n        brunoConfig = parsed.brunoConfig;\n      } else {\n        collectionRoot = parsed;\n        brunoConfig = undefined;\n      }\n\n      file.data = collectionRoot;\n\n      hydrateCollectionRootWithUuid(file.data);\n      win.webContents.send('main:collection-tree-updated', 'addFile', file);\n\n      // in yml format, opencollection.yml also contains the bruno config\n      if (format === 'yml') {\n        // Transform the config to add exists metadata for protobuf files and import paths\n        brunoConfig = await transformBrunoConfigAfterRead(brunoConfig, collectionPath);\n\n        setBrunoConfig(collectionUid, brunoConfig);\n\n        const payload = {\n          collectionUid,\n          brunoConfig: brunoConfig\n        };\n\n        win.webContents.send('main:bruno-config-update', payload);\n      }\n    } catch (err) {\n      console.error(err);\n    }\n\n    return;\n  }\n\n  if (isFolderRootFile(pathname, collectionPath)) {\n    const file = {\n      meta: {\n        collectionUid,\n        pathname,\n        name: path.basename(pathname),\n        folderRoot: true\n      }\n    };\n\n    try {\n      let format = getCollectionFormat(collectionPath);\n      let content = fs.readFileSync(pathname, 'utf8');\n      file.data = await parseFolder(content, { format });\n\n      hydrateCollectionRootWithUuid(file.data);\n      win.webContents.send('main:collection-tree-updated', 'addFile', file);\n      return;\n    } catch (err) {\n      console.error(err);\n      return;\n    }\n  }\n\n  const format = getCollectionFormat(collectionPath);\n  if (hasRequestExtension(pathname, format)) {\n    watcher.addFileToProcessing(collectionUid, pathname);\n\n    const file = {\n      meta: {\n        collectionUid,\n        pathname,\n        name: path.basename(pathname)\n      }\n    };\n\n    const fileStats = fs.statSync(pathname);\n    let content = fs.readFileSync(pathname, 'utf8');\n\n    // If worker thread is not used, we can directly parse the file\n    if (!useWorkerThread) {\n      try {\n        file.data = await parseRequest(content, { format });\n        file.partial = false;\n        file.loading = false;\n        file.size = sizeInMB(fileStats?.size);\n        hydrateRequestWithUuid(file.data, pathname);\n        win.webContents.send('main:collection-tree-updated', 'addFile', file);\n      } catch (error) {\n        console.error(error);\n      } finally {\n        watcher.markFileAsProcessed(win, collectionUid, pathname);\n      }\n      return;\n    }\n\n    try {\n      // we need to send a partial file info to the UI\n      // so that the UI can display the file in the collection tree\n      file.data = {\n        name: path.basename(pathname),\n        type: 'http-request'\n      };\n\n      const metaJson = parseFileMeta(content, format);\n      file.data = metaJson;\n      file.partial = true;\n      file.loading = false;\n      file.size = sizeInMB(fileStats?.size);\n      hydrateRequestWithUuid(file.data, pathname);\n      win.webContents.send('main:collection-tree-updated', 'addFile', file);\n\n      if (fileStats.size < MAX_FILE_SIZE) {\n        // This is to update the loading indicator in the UI\n        file.data = metaJson;\n        file.partial = false;\n        file.loading = true;\n        hydrateRequestWithUuid(file.data, pathname);\n        win.webContents.send('main:collection-tree-updated', 'addFile', file);\n\n        // This is to update the file info in the UI\n        file.data = await parseRequestViaWorker(content, {\n          format,\n          filename: pathname\n        });\n        file.partial = false;\n        file.loading = false;\n        hydrateRequestWithUuid(file.data, pathname);\n        win.webContents.send('main:collection-tree-updated', 'addFile', file);\n      }\n    } catch (error) {\n      file.data = {\n        name: path.basename(pathname),\n        type: 'http-request'\n      };\n      file.error = {\n        message: error?.message\n      };\n      file.partial = true;\n      file.loading = false;\n      file.size = sizeInMB(fileStats?.size);\n      hydrateRequestWithUuid(file.data, pathname);\n      win.webContents.send('main:collection-tree-updated', 'addFile', file);\n    } finally {\n      watcher.markFileAsProcessed(win, collectionUid, pathname);\n    }\n  }\n};\n\nconst addDirectory = async (win, pathname, collectionUid, collectionPath) => {\n  const envDirectory = path.join(collectionPath, 'environments');\n\n  if (path.normalize(pathname) === path.normalize(envDirectory)) {\n    return;\n  }\n\n  let name = path.basename(pathname);\n  let seq;\n\n  const format = getCollectionFormat(collectionPath);\n  const folderFilePath = path.join(pathname, `folder.${format}`);\n\n  try {\n    if (fs.existsSync(folderFilePath)) {\n      let folderFileContent = fs.readFileSync(folderFilePath, 'utf8');\n      let folderData = await parseFolder(folderFileContent, { format });\n      name = folderData?.meta?.name || name;\n      seq = folderData?.meta?.seq;\n    }\n  } catch (error) {\n    console.error(`Error occured while parsing folder.${format} file`);\n    console.error(error);\n  }\n\n  const directory = {\n    meta: {\n      collectionUid,\n      pathname,\n      name,\n      seq,\n      uid: getRequestUid(pathname)\n    }\n  };\n\n  win.webContents.send('main:collection-tree-updated', 'addDir', directory);\n};\n\nconst change = async (win, pathname, collectionUid, collectionPath) => {\n  if (isBrunoConfigFile(pathname, collectionPath)) {\n    try {\n      const content = fs.readFileSync(pathname, 'utf8');\n      let brunoConfig = JSON.parse(content);\n\n      // Transform the config to add file existence checks for protobuf files and import paths\n      brunoConfig = await transformBrunoConfigAfterRead(brunoConfig, collectionPath);\n\n      setBrunoConfig(collectionUid, brunoConfig);\n\n      const payload = {\n        collectionUid,\n        brunoConfig: brunoConfig\n      };\n\n      win.webContents.send('main:bruno-config-update', payload);\n    } catch (err) {\n      console.error(err);\n    }\n\n    return;\n  }\n\n  if (isEnvironmentsFolder(pathname, collectionPath)) {\n    return changeEnvironmentFile(win, pathname, collectionUid, collectionPath);\n  }\n\n  if (isCollectionRootFile(pathname, collectionPath)) {\n    const file = {\n      meta: {\n        collectionUid,\n        pathname,\n        name: path.basename(pathname),\n        collectionRoot: true\n      }\n    };\n\n    try {\n      let content = fs.readFileSync(pathname, 'utf8');\n      let format = getCollectionFormat(collectionPath);\n      let parsed = await parseCollection(content, { format });\n\n      let collectionRoot, brunoConfig;\n      if (format === 'yml') {\n        collectionRoot = parsed.collectionRoot;\n        brunoConfig = parsed.brunoConfig;\n      } else {\n        collectionRoot = parsed;\n        brunoConfig = undefined;\n      }\n\n      file.data = collectionRoot;\n\n      hydrateCollectionRootWithUuid(file.data);\n      win.webContents.send('main:collection-tree-updated', 'change', file);\n\n      // in yml format, opencollection.yml also contains the bruno config\n      if (format === 'yml') {\n        // Transform the config to add exists metadata for protobuf files and import paths\n        brunoConfig = await transformBrunoConfigAfterRead(brunoConfig, collectionPath);\n\n        setBrunoConfig(collectionUid, brunoConfig);\n\n        const payload = {\n          collectionUid,\n          brunoConfig: brunoConfig\n        };\n\n        win.webContents.send('main:bruno-config-update', payload);\n      }\n    } catch (err) {\n      console.error(err);\n    }\n\n    return;\n  }\n\n  if (isFolderRootFile(pathname, collectionPath)) {\n    const file = {\n      meta: {\n        collectionUid,\n        pathname,\n        name: path.basename(pathname),\n        folderRoot: true\n      }\n    };\n\n    try {\n      let format = getCollectionFormat(collectionPath);\n      let content = fs.readFileSync(pathname, 'utf8');\n      file.data = await parseFolder(content, { format });\n\n      hydrateCollectionRootWithUuid(file.data);\n      win.webContents.send('main:collection-tree-updated', 'change', file);\n      return;\n    } catch (err) {\n      console.error(err);\n      return;\n    }\n  }\n\n  const format = getCollectionFormat(collectionPath);\n  if (hasRequestExtension(pathname, format)) {\n    try {\n      const file = {\n        meta: {\n          collectionUid,\n          pathname,\n          name: path.basename(pathname)\n        }\n      };\n\n      const content = fs.readFileSync(pathname, 'utf8');\n      const fileStats = fs.statSync(pathname);\n\n      if (fileStats.size >= MAX_FILE_SIZE && format === 'bru') {\n        file.data = await parseLargeRequestWithRedaction(content, 'bru');\n      } else {\n        file.data = await parseRequest(content, { format });\n      }\n\n      file.size = sizeInMB(fileStats?.size);\n      hydrateRequestWithUuid(file.data, pathname);\n      win.webContents.send('main:collection-tree-updated', 'change', file);\n    } catch (err) {\n      console.error(err);\n    }\n  }\n};\n\nconst unlink = (win, pathname, collectionUid, collectionPath) => {\n  try {\n    if (!fs.existsSync(collectionPath)) {\n      return;\n    }\n    console.log(`watcher unlink: ${pathname}`);\n\n    if (isEnvironmentsFolder(pathname, collectionPath)) {\n      return unlinkEnvironmentFile(win, pathname, collectionUid);\n    }\n\n    let format;\n    try {\n      format = getCollectionFormat(collectionPath);\n    } catch (error) {\n      console.error(`Error getting collection format for: ${collectionPath}`, error);\n      return;\n    }\n    if (hasRequestExtension(pathname, format)) {\n      const basename = path.basename(pathname);\n      const dirname = path.dirname(pathname);\n\n      if (basename === 'opencollection.yml' && path.normalize(dirname) === path.normalize(collectionPath)) {\n        return;\n      }\n\n      const file = {\n        meta: {\n          collectionUid,\n          pathname,\n          name: basename\n        }\n      };\n      win.webContents.send('main:collection-tree-updated', 'unlink', file);\n    }\n  } catch (err) {\n    console.error(`Error processing unlink event for: ${pathname}`, err);\n  }\n};\n\nconst unlinkDir = async (win, pathname, collectionUid, collectionPath) => {\n  try {\n    if (!fs.existsSync(collectionPath)) {\n      return;\n    }\n    const envDirectory = path.join(collectionPath, 'environments');\n\n    if (path.normalize(pathname) === path.normalize(envDirectory)) {\n      return;\n    }\n\n    let format;\n    try {\n      format = getCollectionFormat(collectionPath);\n    } catch (error) {\n      console.error(`Error getting collection format for: ${collectionPath}`, error);\n      return;\n    }\n    const folderFilePath = path.join(pathname, `folder.${format}`);\n\n    let name = path.basename(pathname);\n\n    if (fs.existsSync(folderFilePath)) {\n      let folderFileContent = fs.readFileSync(folderFilePath, 'utf8');\n      let folderData = await parseFolder(folderFileContent, { format });\n      name = folderData?.meta?.name || name;\n    }\n\n    const directory = {\n      meta: {\n        collectionUid,\n        pathname,\n        name\n      }\n    };\n    win.webContents.send('main:collection-tree-updated', 'unlinkDir', directory);\n  } catch (err) {\n    console.error(`Error processing unlinkDir event for: ${pathname}`, err);\n  }\n};\n\nconst onWatcherSetupComplete = (win, watchPath, collectionUid, watcher) => {\n  // Mark discovery as complete\n  watcher.completeCollectionDiscovery(win, collectionUid);\n\n  const UiStateSnapshotStore = new UiStateSnapshot();\n  const collectionsSnapshotState = UiStateSnapshotStore.getCollections();\n  const collectionSnapshotState = collectionsSnapshotState?.find((c) => c?.pathname && path.normalize(c.pathname) === path.normalize(watchPath));\n  win.webContents.send('main:hydrate-app-with-ui-state-snapshot', collectionSnapshotState);\n};\n\nclass CollectionWatcher {\n  constructor() {\n    this.watchers = {};\n    this.loadingStates = {};\n    this.tempDirectoryMap = {};\n  }\n\n  // Initialize loading state tracking for a collection\n  initializeLoadingState(collectionUid) {\n    if (!this.loadingStates[collectionUid]) {\n      this.loadingStates[collectionUid] = {\n        isDiscovering: false, // Initial discovery phase\n        isProcessing: false, // Processing discovered files\n        pendingFiles: new Set() // Files that need processing\n      };\n    }\n  }\n\n  startCollectionDiscovery(win, collectionUid) {\n    this.initializeLoadingState(collectionUid);\n    const state = this.loadingStates[collectionUid];\n\n    state.isDiscovering = true;\n    state.pendingFiles.clear();\n\n    win.webContents.send('main:collection-loading-state-updated', {\n      collectionUid,\n      isLoading: true\n    });\n  }\n\n  addFileToProcessing(collectionUid, filepath) {\n    this.initializeLoadingState(collectionUid);\n    const state = this.loadingStates[collectionUid];\n    state.pendingFiles.add(filepath);\n  }\n\n  markFileAsProcessed(win, collectionUid, filepath) {\n    if (!this.loadingStates[collectionUid]) return;\n\n    const state = this.loadingStates[collectionUid];\n    state.pendingFiles.delete(filepath);\n\n    // If discovery is complete and no pending files, mark as not loading\n    if (!state.isDiscovering && state.pendingFiles.size === 0 && state.isProcessing) {\n      state.isProcessing = false;\n      win.webContents.send('main:collection-loading-state-updated', {\n        collectionUid,\n        isLoading: false\n      });\n    }\n  }\n\n  completeCollectionDiscovery(win, collectionUid) {\n    if (!this.loadingStates[collectionUid]) return;\n\n    const state = this.loadingStates[collectionUid];\n    state.isDiscovering = false;\n\n    // If there are pending files, start processing phase\n    if (state.pendingFiles.size > 0) {\n      state.isProcessing = true;\n    } else {\n      // No pending files, collection is fully loaded\n      win.webContents.send('main:collection-loading-state-updated', {\n        collectionUid,\n        isLoading: false\n      });\n    }\n  }\n\n  cleanupLoadingState(collectionUid) {\n    delete this.loadingStates[collectionUid];\n  }\n\n  addWatcher(win, watchPath, collectionUid, brunoConfig, forcePolling = false, useWorkerThread) {\n    if (this.watchers[watchPath]) {\n      this.watchers[watchPath].close();\n    }\n\n    this.initializeLoadingState(collectionUid);\n\n    this.startCollectionDiscovery(win, collectionUid);\n\n    // Always ignore node_modules and .git, regardless of user config\n    // This prevents infinite loops with symlinked directories (e.g., npm workspaces)\n    const defaultIgnores = ['node_modules', '.git'];\n    const userIgnores = brunoConfig?.ignore || [];\n    const ignores = [...new Set([...defaultIgnores, ...userIgnores])];\n\n    setTimeout(() => {\n      const watcher = chokidar.watch(watchPath, {\n        ignoreInitial: false,\n        usePolling: isWSLPath(watchPath) || forcePolling ? true : false,\n        ignored: (filepath) => {\n          const normalizedPath = normalizeAndResolvePath(filepath);\n          const relativePath = path.relative(watchPath, normalizedPath);\n          const basename = path.basename(filepath);\n\n          // Ignore .env files - handled by dotenv-watcher\n          if (basename === '.env' || basename.startsWith('.env.')) {\n            return true;\n          }\n\n          // Check if any path segment matches a default ignore pattern (handles symlinks)\n          const pathSegments = relativePath.split(path.sep);\n          if (pathSegments.some((segment) => defaultIgnores.includes(segment))) {\n            return true;\n          }\n\n          return ignores.some((ignorePattern) => {\n            return relativePath === ignorePattern || relativePath.startsWith(ignorePattern);\n          });\n        },\n        persistent: true,\n        ignorePermissionErrors: true,\n        awaitWriteFinish: {\n          stabilityThreshold: 80,\n          pollInterval: 10\n        },\n        depth: 20,\n        disableGlobbing: true\n      });\n\n      let startedNewWatcher = false;\n      watcher\n        .on('ready', () => onWatcherSetupComplete(win, watchPath, collectionUid, this))\n        .on('add', (pathname) => add(win, pathname, collectionUid, watchPath, useWorkerThread, this))\n        .on('addDir', (pathname) => addDirectory(win, pathname, collectionUid, watchPath))\n        .on('change', (pathname) => change(win, pathname, collectionUid, watchPath))\n        .on('unlink', (pathname) => unlink(win, pathname, collectionUid, watchPath))\n        .on('unlinkDir', (pathname) => unlinkDir(win, pathname, collectionUid, watchPath))\n        .on('error', (error) => {\n          // `EMFILE` is an error code thrown when to many files are watched at the same time see: https://github.com/usebruno/bruno/issues/627\n          // `ENOSPC` stands for \"Error No space\" but is also thrown if the file watcher limit is reached.\n          // To prevent loops `!forcePolling` is checked.\n          if ((error.code === 'ENOSPC' || error.code === 'EMFILE') && !startedNewWatcher && !forcePolling) {\n            // This callback is called for every file the watcher is trying to watch. To prevent a spam of messages and\n            // Multiple watcher being started `startedNewWatcher` is set to prevent this.\n            startedNewWatcher = true;\n            watcher.close();\n            console.error(\n              `\\nCould not start watcher for ${watchPath}:`,\n              'ENOSPC: System limit for number of file watchers reached!',\n              'Trying again with polling, this will be slower!\\n',\n              'Update your system config to allow more concurrently watched files with:',\n              '\"echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p\"'\n            );\n            this.addWatcher(win, watchPath, collectionUid, brunoConfig, true, useWorkerThread);\n          } else {\n            console.error(`An error occurred in the watcher for: ${watchPath}`, error);\n          }\n        });\n\n      this.watchers[watchPath] = watcher;\n\n      dotEnvWatcher.addCollectionWatcher(win, watchPath, collectionUid);\n    }, 100);\n  }\n\n  hasWatcher(watchPath) {\n    return this.watchers[watchPath];\n  }\n\n  removeWatcher(watchPath, win, collectionUid) {\n    if (this.watchers[watchPath]) {\n      this.watchers[watchPath].close();\n      this.watchers[watchPath] = null;\n    }\n\n    dotEnvWatcher.removeCollectionWatcher(watchPath);\n\n    const tempDirectoryPath = this.tempDirectoryMap[watchPath];\n    if (tempDirectoryPath && this.watchers[tempDirectoryPath]) {\n      this.watchers[tempDirectoryPath].close();\n      delete this.watchers[tempDirectoryPath];\n      delete this.tempDirectoryMap[watchPath];\n    }\n\n    if (collectionUid) {\n      this.cleanupLoadingState(collectionUid);\n    }\n  }\n\n  getWatcherByItemPath(itemPath) {\n    const paths = Object.keys(this.watchers);\n\n    const watcherPath = paths?.find((collectionPath) => {\n      const absCollectionPath = path.resolve(collectionPath);\n      const absItemPath = path.resolve(itemPath);\n\n      return absItemPath.startsWith(absCollectionPath);\n    });\n\n    return watcherPath ? this.watchers[watcherPath] : null;\n  }\n\n  unlinkItemPathInWatcher(itemPath) {\n    const watcher = this.getWatcherByItemPath(itemPath);\n    if (watcher) {\n      watcher.unwatch(itemPath);\n    }\n  }\n\n  addItemPathInWatcher(itemPath) {\n    const watcher = this.getWatcherByItemPath(itemPath);\n    if (watcher && !watcher?.has?.(itemPath)) {\n      watcher?.add?.(itemPath);\n    }\n  }\n\n  // Helper function to get collection path from temp directory metadata\n  getCollectionPathFromTempDirectory(tempDirectoryPath) {\n    const metadataPath = path.join(tempDirectoryPath, 'metadata.json');\n    try {\n      const metadataContent = fs.readFileSync(metadataPath, 'utf8');\n      const metadata = JSON.parse(metadataContent);\n      return metadata.collectionPath;\n    } catch (error) {\n      console.error(`Error reading metadata from temp directory ${tempDirectoryPath}:`, error);\n      return null;\n    }\n  }\n\n  // Add watcher for transient directory\n  // The tempDirectoryPath is stored in this.tempDirectoryMap[collectionPath] so removeWatcher can clean it up\n  addTempDirectoryWatcher(win, tempDirectoryPath, collectionUid, collectionPath) {\n    if (this.watchers[tempDirectoryPath]) {\n      this.watchers[tempDirectoryPath].close();\n    }\n\n    // Store the mapping from collectionPath to tempDirectoryPath for cleanup in removeWatcher\n    this.tempDirectoryMap[collectionPath] = tempDirectoryPath;\n\n    // Ignore metadata.json file\n    const ignored = (filepath) => {\n      const basename = path.basename(filepath);\n      return basename === 'metadata.json';\n    };\n\n    const watcher = chokidar.watch(tempDirectoryPath, {\n      ignoreInitial: true, // Don't process existing files\n      usePolling: isWSLPath(tempDirectoryPath) ? true : false,\n      ignored,\n      persistent: true,\n      ignorePermissionErrors: true,\n      awaitWriteFinish: {\n        stabilityThreshold: 80,\n        pollInterval: 10\n      },\n      depth: 1, // Only watch the temp directory itself, not subdirectories\n      disableGlobbing: true\n    });\n\n    // Wrapper function to handle temp directory files\n    const addTempFile = async (pathname) => {\n      // Skip metadata.json\n      if (path.basename(pathname) === 'metadata.json') {\n        return;\n      }\n\n      // Get the actual collection path from metadata\n      const actualCollectionPath = this.getCollectionPathFromTempDirectory(tempDirectoryPath);\n      if (!actualCollectionPath) {\n        console.error(`Could not determine collection path for temp directory: ${tempDirectoryPath}`);\n        return;\n      }\n\n      // Use the collection format from the actual collection\n      const format = getCollectionFormat(actualCollectionPath);\n\n      // Only process request files\n      if (hasRequestExtension(pathname, format)) {\n        // Call the regular add function with the actual collection path\n        // This will hydrate and send the file to the renderer\n        await add(win, pathname, collectionUid, actualCollectionPath, false, this);\n      }\n    };\n    const unlinkTempFile = async (pathname) => {\n      // Skip metadata.json\n      if (path.basename(pathname) === 'metadata.json') {\n        return;\n      }\n\n      // Get the actual collection path from metadata\n      const actualCollectionPath = this.getCollectionPathFromTempDirectory(tempDirectoryPath);\n      if (!actualCollectionPath) {\n        console.error(`Could not determine collection path for temp directory: ${tempDirectoryPath}`);\n        return;\n      }\n\n      // Use the collection format from the actual collection\n      const format = getCollectionFormat(actualCollectionPath);\n\n      // Only process request files\n      if (hasRequestExtension(pathname, format)) {\n        // Call the regular unlink function with the actual collection path\n        await unlink(win, pathname, collectionUid, actualCollectionPath);\n      }\n    };\n\n    watcher\n      .on('add', (pathname) => addTempFile(pathname))\n      .on('unlink', (pathname) => unlinkTempFile(pathname))\n      .on('error', (error) => {\n        console.error(`An error occurred in the temp directory watcher for: ${tempDirectoryPath}`, error);\n      });\n\n    this.watchers[tempDirectoryPath] = watcher;\n  }\n\n  getAllWatcherPaths() {\n    return Object.entries(this.watchers)\n      .filter(([path, watcher]) => !!watcher)\n      .map(([path, _watcher]) => path);\n  }\n}\n\nconst collectionWatcher = new CollectionWatcher();\n\nmodule.exports = collectionWatcher;\n"
  },
  {
    "path": "packages/bruno-electron/src/app/collections.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst { dialog, ipcMain } = require('electron');\nconst Yup = require('yup');\nconst { isDirectory, getCollectionStats, normalizeAndResolvePath } = require('../utils/filesystem');\nconst { generateUidBasedOnHash } = require('../utils/common');\nconst { transformBrunoConfigAfterRead } = require('../utils/transformBrunoConfig');\nconst { parseCollection } = require('@usebruno/filestore');\n\n// Track scratch collection paths (temp directories for workspace scratch requests)\nconst scratchCollectionPaths = new Set();\n\n// Register a scratch collection path\nconst registerScratchCollectionPath = (scratchPath) => {\n  scratchCollectionPaths.add(path.normalize(scratchPath));\n};\n\n// todo: bruno.json config schema validation errors must be propagated to the UI\nconst configSchema = Yup.object({\n  name: Yup.string().max(256, 'name must be 256 characters or less').required('name is required'),\n  type: Yup.string().oneOf(['collection']).required('type is required'),\n  // For BRU format collections\n  version: Yup.string().oneOf(['1']).notRequired(),\n  // For YAML format collections (opencollection)\n  opencollection: Yup.string().notRequired(),\n  // OpenAPI sync configuration (array, one entry per synced spec)\n  openapi: Yup.array().of(\n    Yup.object({\n      sourceUrl: Yup.string().notRequired(),\n      lastSyncDate: Yup.string().notRequired(),\n      specHash: Yup.string().notRequired(),\n      groupBy: Yup.string().oneOf(['tags', 'path']).notRequired(),\n      autoCheck: Yup.boolean().notRequired(),\n      autoCheckInterval: Yup.number().notRequired()\n    })\n  ).notRequired()\n});\n\nconst readConfigFile = async (pathname) => {\n  try {\n    const jsonData = fs.readFileSync(pathname, 'utf8');\n    return JSON.parse(jsonData);\n  } catch (err) {\n    return Promise.reject(new Error(`Unable to parse json in bruno.json in ${pathname}`));\n  }\n};\n\nconst validateSchema = async (config) => {\n  try {\n    await configSchema.validate(config);\n  } catch (err) {\n    return Promise.reject(new Error('bruno.json format is invalid in ' + config?.name));\n  }\n};\n\nconst getCollectionConfigFile = async (pathname) => {\n  // Check for opencollection.yml first\n  const ocYmlPath = path.join(pathname, 'opencollection.yml');\n  if (fs.existsSync(ocYmlPath)) {\n    try {\n      const content = fs.readFileSync(ocYmlPath, 'utf8');\n      const {\n        brunoConfig\n      } = parseCollection(content, { format: 'yml' });\n      await validateSchema(brunoConfig);\n      return brunoConfig;\n    } catch (err) {\n      throw new Error(`Unable to parse opencollection.yml: ${err.message}`);\n    }\n  }\n\n  // Fall back to bruno.json\n  const configFilePath = path.join(pathname, 'bruno.json');\n  if (!fs.existsSync(configFilePath)) {\n    throw new Error(`The collection is not valid (neither bruno.json nor opencollection.yml found)`);\n  }\n\n  const config = await readConfigFile(configFilePath);\n  await validateSchema(config);\n\n  return config;\n};\n\nconst openCollectionDialog = async (win, watcher) => {\n  const { canceled, filePaths } = await dialog.showOpenDialog(win, {\n    properties: ['openDirectory', 'createDirectory', 'multiSelections']\n  });\n\n  if (!canceled && filePaths?.length > 0) {\n    // Using Set to remove duplicates\n    const { openCollectionPromises, invalidPaths } = [...new Set(filePaths)].reduce((acc, filePath) => {\n      const resolvedPath = path.resolve(filePath);\n\n      if (isDirectory(resolvedPath)) {\n        // Open each valid collection in parallel\n        acc.openCollectionPromises.push(openCollection(win, watcher, resolvedPath).catch((err) => {\n          console.error(`[ERROR] Failed to open collection at \"${resolvedPath}\":`, err.message);\n          return { error: err, path: resolvedPath };\n        }));\n      } else {\n        acc.invalidPaths.push(resolvedPath);\n        console.error(`[ERROR] Cannot open unknown folder: \"${resolvedPath}\"`);\n      }\n\n      return acc;\n    },\n    { openCollectionPromises: [], invalidPaths: [] });\n\n    // Wait for all valid collections to be opened\n    await Promise.all(openCollectionPromises);\n\n    // Notify about any invalid paths\n    if (invalidPaths.length > 0) {\n      win.webContents.send('main:display-error', `Some selected folders could not be opened: ${invalidPaths.join(', ')}`);\n    }\n  }\n};\n\nconst openCollection = async (win, watcher, collectionPath, options = {}) => {\n  // If watcher already exists, collection is already loaded in the app\n  // Just send the collection info so frontend can add to workspace if needed\n  if (watcher.hasWatcher(collectionPath)) {\n    try {\n      let brunoConfig = await getCollectionConfigFile(collectionPath);\n      const uid = generateUidBasedOnHash(collectionPath);\n      brunoConfig = await transformBrunoConfigAfterRead(brunoConfig, collectionPath);\n      const { size, filesCount } = await getCollectionStats(collectionPath);\n      brunoConfig.size = size;\n      brunoConfig.filesCount = filesCount;\n      win.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig);\n    } catch (err) {\n      if (!options.dontSendDisplayErrors) {\n        win.webContents.send('main:display-error', {\n          message: err.message || 'An error occurred while opening the local collection'\n        });\n      }\n    }\n    return;\n  }\n\n  try {\n    let brunoConfig = await getCollectionConfigFile(collectionPath);\n    const uid = generateUidBasedOnHash(collectionPath);\n\n    // Always ensure node_modules and .git are ignored, regardless of user config\n    const defaultIgnores = ['node_modules', '.git'];\n    const userIgnores = brunoConfig.ignore || [];\n    brunoConfig.ignore = [...new Set([...defaultIgnores, ...userIgnores])];\n\n    brunoConfig = await transformBrunoConfigAfterRead(brunoConfig, collectionPath);\n\n    const { size, filesCount } = await getCollectionStats(collectionPath);\n    brunoConfig.size = size;\n    brunoConfig.filesCount = filesCount;\n\n    win.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig);\n    ipcMain.emit('main:collection-opened', win, collectionPath, uid, brunoConfig);\n  } catch (err) {\n    if (!options.dontSendDisplayErrors) {\n      win.webContents.send('main:display-error', {\n        message: err.message || 'An error occurred while opening the local collection'\n      });\n    }\n  }\n};\n\nconst openCollectionsByPathname = async (win, watcher, collectionPaths, options = {}) => {\n  const seenPaths = new Set();\n\n  for (const collectionPath of collectionPaths) {\n    const resolvedPath = path.isAbsolute(collectionPath)\n      ? collectionPath\n      : normalizeAndResolvePath(collectionPath);\n\n    const normalizedPath = path.normalize(resolvedPath);\n    if (seenPaths.has(normalizedPath)) {\n      continue;\n    }\n    seenPaths.add(normalizedPath);\n\n    if (isDirectory(resolvedPath)) {\n      await openCollection(win, watcher, resolvedPath, options);\n    } else {\n      console.error(`Cannot open unknown folder: \"${resolvedPath}\"`);\n    }\n  }\n};\n\nmodule.exports = {\n  openCollection,\n  openCollectionDialog,\n  openCollectionsByPathname,\n  registerScratchCollectionPath\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/app/dotenv-watcher.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst chokidar = require('chokidar');\nconst { parseDotEnv } = require('@usebruno/filestore');\nconst { setDotEnvVars, clearDotEnvVars, setWorkspaceDotEnvVars, clearWorkspaceDotEnvVars } = require('../store/process-env');\n\nconst isDotEnvFile = (filename) => {\n  return filename === '.env' || filename.startsWith('.env.');\n};\n\nconst parseVariablesToArray = (envObject) => {\n  return Object.entries(envObject).map(([name, value]) => ({\n    name,\n    value,\n    enabled: true,\n    secret: false\n  }));\n};\n\nconst DEFAULT_WATCHER_OPTIONS = {\n  ignoreInitial: false,\n  persistent: true,\n  ignorePermissionErrors: true,\n  depth: 0\n};\n\nconst createFileHandler = (win, options) => (pathname) => {\n  const { type, uid, uidKey, pathKey, basePath, setEnvVars } = options;\n  const filename = path.basename(pathname);\n\n  if (!isDotEnvFile(filename)) {\n    return;\n  }\n\n  try {\n    const content = fs.readFileSync(pathname, 'utf8');\n    const jsonData = parseDotEnv(content);\n\n    if (filename === '.env') {\n      setEnvVars(jsonData);\n    }\n\n    const variables = parseVariablesToArray(jsonData);\n\n    if (!win.isDestroyed()) {\n      const payload = {\n        type,\n        [uidKey]: uid,\n        filename,\n        variables,\n        exists: true,\n        processEnvVariables: { ...jsonData }\n      };\n      if (pathKey) {\n        payload[pathKey] = basePath;\n      }\n      win.webContents.send('main:dotenv-file-update', payload);\n    }\n  } catch (err) {\n    console.error(`Error processing dotenv file ${pathname}:`, err);\n  }\n};\n\nconst createUnlinkHandler = (win, options) => (pathname) => {\n  const { type, uid, uidKey, pathKey, basePath, clearEnvVars } = options;\n  const filename = path.basename(pathname);\n\n  if (!isDotEnvFile(filename)) {\n    return;\n  }\n\n  if (filename === '.env') {\n    clearEnvVars();\n  }\n\n  if (!win.isDestroyed()) {\n    const payload = {\n      type,\n      [uidKey]: uid,\n      filename,\n      variables: [],\n      exists: false,\n      processEnvVariables: {}\n    };\n    if (pathKey) {\n      payload[pathKey] = basePath;\n    }\n    win.webContents.send('main:dotenv-file-update', payload);\n  }\n};\n\nclass DotEnvWatcher {\n  constructor() {\n    this.collectionWatchers = new Map();\n    this.workspaceWatchers = new Map();\n  }\n\n  addCollectionWatcher(win, collectionPath, collectionUid) {\n    if (this.collectionWatchers.has(collectionPath)) {\n      this.collectionWatchers.get(collectionPath).close();\n    }\n\n    const watcher = chokidar.watch(collectionPath, {\n      ...DEFAULT_WATCHER_OPTIONS,\n      disableGlobbing: true,\n      awaitWriteFinish: {\n        stabilityThreshold: 80,\n        pollInterval: 100\n      }\n    });\n\n    const handlerOptions = {\n      type: 'collection',\n      uid: collectionUid,\n      uidKey: 'collectionUid',\n      basePath: collectionPath,\n      setEnvVars: (data) => setDotEnvVars(collectionUid, data),\n      clearEnvVars: () => clearDotEnvVars(collectionUid)\n    };\n\n    const handleFile = createFileHandler(win, handlerOptions);\n    const handleUnlink = createUnlinkHandler(win, handlerOptions);\n\n    watcher.on('add', handleFile);\n    watcher.on('change', handleFile);\n    watcher.on('unlink', handleUnlink);\n    watcher.on('error', (err) => {\n      console.error(`Collection watcher error for ${collectionPath}:`, err);\n    });\n\n    this.collectionWatchers.set(collectionPath, watcher);\n  }\n\n  removeCollectionWatcher(collectionPath, collectionUid) {\n    if (this.collectionWatchers.has(collectionPath)) {\n      this.collectionWatchers.get(collectionPath).close();\n      this.collectionWatchers.delete(collectionPath);\n    }\n    if (collectionUid) {\n      clearDotEnvVars(collectionUid);\n    }\n  }\n\n  hasCollectionWatcher(collectionPath) {\n    return this.collectionWatchers.has(collectionPath);\n  }\n\n  addWorkspaceWatcher(win, workspacePath, workspaceUid) {\n    if (this.workspaceWatchers.has(workspacePath)) {\n      this.workspaceWatchers.get(workspacePath).close();\n    }\n\n    const watcher = chokidar.watch(workspacePath, {\n      ...DEFAULT_WATCHER_OPTIONS,\n      disableGlobbing: true,\n      awaitWriteFinish: {\n        stabilityThreshold: 80,\n        pollInterval: 250\n      }\n    });\n\n    const handlerOptions = {\n      type: 'workspace',\n      uid: workspaceUid,\n      uidKey: 'workspaceUid',\n      pathKey: 'workspacePath',\n      basePath: workspacePath,\n      setEnvVars: (data) => setWorkspaceDotEnvVars(workspacePath, data),\n      clearEnvVars: () => clearWorkspaceDotEnvVars(workspacePath)\n    };\n\n    const handleFile = createFileHandler(win, handlerOptions);\n    const handleUnlink = createUnlinkHandler(win, handlerOptions);\n\n    watcher.on('add', handleFile);\n    watcher.on('change', handleFile);\n    watcher.on('unlink', handleUnlink);\n    watcher.on('error', (err) => {\n      console.error(`Workspace watcher error for ${workspacePath}:`, err);\n    });\n\n    this.workspaceWatchers.set(workspacePath, watcher);\n  }\n\n  removeWorkspaceWatcher(workspacePath) {\n    if (this.workspaceWatchers.has(workspacePath)) {\n      this.workspaceWatchers.get(workspacePath).close();\n      this.workspaceWatchers.delete(workspacePath);\n    }\n    clearWorkspaceDotEnvVars(workspacePath);\n  }\n\n  hasWorkspaceWatcher(workspacePath) {\n    return this.workspaceWatchers.has(workspacePath);\n  }\n\n  closeAll() {\n    for (const [path, watcher] of this.collectionWatchers) {\n      watcher.close();\n    }\n    this.collectionWatchers.clear();\n\n    for (const [path, watcher] of this.workspaceWatchers) {\n      watcher.close();\n    }\n    this.workspaceWatchers.clear();\n  }\n}\n\nconst dotEnvWatcher = new DotEnvWatcher();\n\nmodule.exports = dotEnvWatcher;\n"
  },
  {
    "path": "packages/bruno-electron/src/app/menu-template.js",
    "content": "const { ipcMain } = require('electron');\nconst os = require('os');\nconst { BrowserWindow } = require('electron');\nconst { version } = require('../../package.json');\nconst aboutBruno = require('./about-bruno');\n\nconst template = [\n  {\n    label: 'Collection',\n    submenu: [\n      {\n        label: 'Open Collection',\n        click() {\n          ipcMain.emit('main:open-collection');\n        }\n      },\n      {\n        label: 'Open Recent',\n        role: 'recentdocuments',\n        visible: os.platform() == 'darwin',\n        submenu: [\n          {\n            label: 'Clear Recent',\n            role: 'clearrecentdocuments'\n          }\n        ]\n      },\n      {\n        label: 'Preferences',\n        accelerator: 'CommandOrControl+,',\n        click() {\n          ipcMain.emit('main:open-preferences');\n        }\n      },\n      { type: 'separator' },\n      { role: 'quit' },\n      {\n        label: 'Force Quit',\n        click() {\n          process.exit();\n        }\n      }\n    ]\n  },\n  {\n    label: 'Edit',\n    submenu: [\n      { role: 'undo' },\n      { role: 'redo' },\n      { type: 'separator' },\n      { role: 'cut' },\n      { role: 'copy' },\n      { role: 'paste' },\n      { role: 'selectAll' },\n      { type: 'separator' },\n      { role: 'hide' },\n      { role: 'hideOthers' }\n    ]\n  },\n  {\n    label: 'View',\n    submenu: [\n      { role: 'toggledevtools' },\n      { type: 'separator' },\n      {\n        label: 'Actual Size',\n        accelerator: 'CommandOrControl+0',\n        click() {\n          ipcMain.emit('menu:reset-zoom');\n        }\n      },\n      {\n        label: 'Zoom In',\n        accelerator: 'CommandOrControl+Plus',\n        click() {\n          ipcMain.emit('menu:zoom-in');\n        }\n      },\n      {\n        label: 'Zoom Out',\n        accelerator: 'CommandOrControl+-',\n        click() {\n          ipcMain.emit('menu:zoom-out');\n        }\n      },\n      { type: 'separator' },\n      { role: 'togglefullscreen' }\n    ]\n  },\n  {\n    role: 'window',\n    submenu: [{ role: 'minimize' }, { role: 'close', accelerator: 'CommandOrControl+Shift+Q' }]\n  },\n  {\n    role: 'help',\n    submenu: [\n      {\n        label: 'About Bruno',\n        click: () => {\n          const aboutWindow = new BrowserWindow({\n            width: 350,\n            height: 250,\n            webPreferences: {\n              nodeIntegration: true\n            }\n          });\n          aboutWindow.removeMenu();\n          aboutWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(aboutBruno({ version }))}`);\n        }\n      },\n      { label: 'Documentation', click: () => ipcMain.emit('main:open-docs') }\n    ]\n  }\n];\n\nmodule.exports = template;\n"
  },
  {
    "path": "packages/bruno-electron/src/app/onboarding.js",
    "content": "const fs = require('node:fs');\nconst path = require('node:path');\nconst { app, ipcMain } = require('electron');\nconst { preferencesUtil, getPreferences, savePreferences } = require('../store/preferences');\nconst { importCollection, findUniqueFolderName } = require('../utils/collection-import');\nconst { resolveDefaultLocation } = require('../utils/default-location');\n\nlet pendingSampleCollection = null;\n\n// When renderer is ready, send any pending collection-opened event\n// This ensures the sample collection appears in the sidebar after onboarding\nipcMain.on('main:renderer-ready', (mainWindow) => {\n  if (pendingSampleCollection) {\n    const { mainWindow: win, collectionPath, uid, brunoConfig } = pendingSampleCollection;\n    win.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig);\n    ipcMain.emit('main:collection-opened', win, collectionPath, uid, brunoConfig);\n    pendingSampleCollection = null;\n  }\n});\n\n/**\n * Import sample collection for new users\n */\nasync function importSampleCollection(collectionLocation, mainWindow) {\n  // Handle both development and production paths\n  const sampleCollectionPath = app.isPackaged\n    ? path.join(process.resourcesPath, 'data', 'sample-collection.json')\n    : path.join(app.getAppPath(), 'resources', 'data', 'sample-collection.json');\n\n  if (!fs.existsSync(sampleCollectionPath)) {\n    throw new Error(`Sample collection file not found at: ${sampleCollectionPath}`);\n  }\n\n  const sampleCollectionData = fs.readFileSync(sampleCollectionPath, 'utf8');\n  const sampleCollection = JSON.parse(sampleCollectionData);\n\n  const collectionName = await findUniqueFolderName('Sample API Collection', collectionLocation);\n\n  const collectionToImport = {\n    ...sampleCollection,\n    name: collectionName\n  };\n\n  try {\n    const {\n      collectionPath: createdPath,\n      uid,\n      brunoConfig\n    } = await importCollection(\n      collectionToImport,\n      collectionLocation,\n      mainWindow,\n      collectionName,\n      undefined, // format - use default\n      { skipOpenEvent: true } // Don't send event yet - renderer isn't ready\n    );\n\n    return { collectionPath: createdPath, uid, brunoConfig };\n  } catch (error) {\n    console.error('Failed to import sample collection:', error);\n    throw error;\n  }\n}\n\n/**\n * Onboard new users by creating a sample collection.\n *\n * This also determines whether the welcome modal should be shown:\n * - Genuinely new users (no collections, no previous launch) → show welcome modal\n * - Existing users upgrading (have collections but no hasLaunchedBefore flag) → skip welcome modal\n *\n * Called directly from the renderer:ready IPC handler in preferences.js to ensure\n * preferences are set correctly before being sent to the renderer.\n */\nasync function onboardUser(mainWindow, lastOpenedCollections) {\n  try {\n    if (preferencesUtil.hasLaunchedBefore()) {\n      return;\n    }\n\n    // Check if user already has collections — this indicates an existing user\n    // upgrading to a version that introduced onboarding, not a genuinely new user\n    const collections = lastOpenedCollections ? lastOpenedCollections.getAll() : [];\n    const isExistingUser = collections.length > 0;\n\n    if (isExistingUser) {\n      // Existing user upgrading: mark as launched, don't show welcome modal\n      // hasSeenWelcomeModal is intentionally NOT set here — it will be absent\n      // from preferences, and the renderer defaults absent values to true (no modal)\n      await preferencesUtil.markAsLaunched();\n      return;\n    }\n\n    // Genuinely new user\n    if (process.env.DISABLE_SAMPLE_COLLECTION_IMPORT !== 'true') {\n      const collectionLocation = resolveDefaultLocation();\n      const collectionInfo = await importSampleCollection(collectionLocation, mainWindow);\n\n      // Store collection info to open after renderer is ready\n      // The main:collection-opened event is deferred until main:renderer-ready is emitted\n      pendingSampleCollection = { mainWindow, ...collectionInfo };\n    }\n\n    // Mark as launched and explicitly enable the welcome modal for new users\n    const preferences = getPreferences();\n    preferences.onboarding = {\n      ...preferences.onboarding,\n      hasLaunchedBefore: true,\n      hasSeenWelcomeModal: false\n    };\n    await savePreferences(preferences);\n  } catch (error) {\n    console.error('Failed to handle onboarding:', error);\n    // Still mark as launched to prevent retry on next startup\n    await preferencesUtil.markAsLaunched();\n  }\n}\n\nmodule.exports = onboardUser;\n"
  },
  {
    "path": "packages/bruno-electron/src/app/system-monitor.js",
    "content": "const { app } = require('electron');\n\nclass SystemMonitor {\n  constructor() {\n    this.intervalId = null;\n    this.isMonitoring = false;\n    this.startTime = null;\n  }\n\n  start(win, intervalMs = 2000) {\n    if (this.isMonitoring) {\n      return;\n    }\n\n    this.isMonitoring = true;\n\n    // Emit initial stats\n    this.emitSystemStats(win);\n\n    // Set up periodic monitoring\n    // Use setTimeout pattern instead of setInterval to avoid overlapping calls\n    this.scheduleNextEmit(win, intervalMs);\n  }\n\n  scheduleNextEmit(win, intervalMs) {\n    if (!this.isMonitoring) {\n      return;\n    }\n\n    this.intervalId = setTimeout(() => {\n      this.emitSystemStats(win);\n      this.scheduleNextEmit(win, intervalMs);\n    }, intervalMs);\n  }\n\n  stop() {\n    if (this.intervalId) {\n      clearTimeout(this.intervalId);\n      this.intervalId = null;\n    }\n    this.isMonitoring = false;\n  }\n\n  emitSystemStats(win) {\n    try {\n      const metrics = app.getAppMetrics();\n      const currentTime = new Date();\n\n      if (metrics.length === 0) {\n        throw new Error('No metrics found');\n      }\n\n      if (this.startTime == null) {\n        let creationTime = metrics[0].creationTime;\n\n        for (const metric of metrics) {\n          creationTime = Math.min(creationTime, metric.creationTime);\n        }\n\n        this.startTime = new Date(creationTime);\n      }\n\n      let totalCPU = 0;\n      let totalMemory = 0;\n\n      for (const metric of metrics) {\n        totalCPU += metric.cpu.percentCPUUsage;\n        totalMemory += metric.memory.workingSetSize * 1024;\n      }\n\n      const uptime = (currentTime - this.startTime) / 1000;\n      var processes = metrics.map((metric) => ({\n        pid: metric.pid,\n        title: metric.title,\n        memory: metric.memory.workingSetSize * 1024,\n        cpu: metric.cpu.percentCPUUsage,\n        type: metric.type || 'unknown',\n        creationTime: metric.creationTime\n      }));\n      const systemResources = {\n        cpu: totalCPU,\n        memory: totalMemory,\n        pid: process.pid,\n        uptime: uptime,\n        timestamp: currentTime.toISOString(),\n        processes: processes\n      };\n\n      if (win && !win.isDestroyed()) {\n        win.webContents.send('main:filesync-system-resources', systemResources);\n      }\n    } catch (error) {\n      console.error('Error getting system stats:', error);\n\n      const memory = process.memoryUsage();\n      const currentTime = new Date();\n\n      const uptime = !this.startTime ? 0 : (currentTime - this.startTime) / 1000;\n\n      // Fallback stats using process.memoryUsage()\n      const fallbackStats = {\n        cpu: 0,\n        memory: memory.rss,\n        pid: process.pid,\n        uptime: uptime,\n        timestamp: currentTime.toISOString(),\n        processes: []\n      };\n\n      if (win && !win.isDestroyed()) {\n        win.webContents.send('main:filesync-system-resources', fallbackStats);\n      }\n    }\n  }\n\n  isRunning() {\n    return this.isMonitoring;\n  }\n}\n\nmodule.exports = SystemMonitor;\n"
  },
  {
    "path": "packages/bruno-electron/src/app/workspace-watcher.js",
    "content": "const _ = require('lodash');\nconst fs = require('fs');\nconst path = require('path');\nconst chokidar = require('chokidar');\nconst yaml = require('js-yaml');\nconst { generateUidBasedOnHash, uuid } = require('../utils/common');\nconst { getWorkspaceUid } = require('../utils/workspace-config');\nconst { parseEnvironment } = require('@usebruno/filestore');\nconst EnvironmentSecretsStore = require('../store/env-secrets');\nconst { decryptStringSafe } = require('../utils/encryption');\nconst dotEnvWatcher = require('./dotenv-watcher');\n\nconst environmentSecretsStore = new EnvironmentSecretsStore();\n\nconst DEFAULT_WORKSPACE_NAME = 'My Workspace';\n\nconst envHasSecrets = (environment) => {\n  const secrets = _.filter(environment.variables, (v) => v.secret === true);\n  return secrets && secrets.length > 0;\n};\n\nconst normalizeWorkspaceConfig = (config) => {\n  return {\n    ...config,\n    name: config.info?.name,\n    type: config.info?.type,\n    collections: config.collections || [],\n    apiSpecs: config.specs || []\n  };\n};\n\nconst handleWorkspaceFileChange = (win, workspacePath) => {\n  try {\n    const workspaceFilePath = path.join(workspacePath, 'workspace.yml');\n\n    if (!fs.existsSync(workspaceFilePath)) {\n      return;\n    }\n\n    const yamlContent = fs.readFileSync(workspaceFilePath, 'utf8');\n    const rawConfig = yaml.load(yamlContent);\n    const workspaceConfig = normalizeWorkspaceConfig(rawConfig);\n\n    const type = workspaceConfig.info?.type || workspaceConfig.type;\n    if (type !== 'workspace') {\n      return;\n    }\n\n    const workspaceUid = getWorkspaceUid(workspacePath);\n    const isDefault = workspaceUid === 'default';\n\n    win.webContents.send('main:workspace-config-updated', workspacePath, workspaceUid, {\n      ...workspaceConfig,\n      name: isDefault ? DEFAULT_WORKSPACE_NAME : workspaceConfig.name,\n      type: isDefault ? 'default' : workspaceConfig.type\n    });\n  } catch (error) {\n    console.error('Error handling workspace file change:', error);\n  }\n};\n\nconst parseGlobalEnvironmentFile = async (pathname, workspacePath, workspaceUid) => {\n  const basename = path.basename(pathname);\n  const environmentName = basename.slice(0, -'.yml'.length);\n\n  const file = {\n    meta: {\n      workspaceUid,\n      pathname,\n      name: basename\n    }\n  };\n\n  const content = fs.readFileSync(pathname, 'utf8');\n  file.data = await parseEnvironment(content, { format: 'yml' });\n  file.data.name = environmentName;\n  file.data.uid = generateUidBasedOnHash(pathname);\n\n  _.each(_.get(file, 'data.variables', []), (variable) => {\n    if (!variable.uid) {\n      variable.uid = uuid();\n    }\n  });\n\n  if (envHasSecrets(file.data)) {\n    const envSecrets = environmentSecretsStore.getEnvSecrets(workspacePath, file.data);\n    _.each(envSecrets, (secret) => {\n      const variable = _.find(file.data.variables, (v) => v.name === secret.name);\n      if (variable && secret.value) {\n        const decryptionResult = decryptStringSafe(secret.value);\n        variable.value = decryptionResult.value;\n      }\n    });\n  }\n\n  return file;\n};\n\nconst handleGlobalEnvironmentFileAdd = async (win, pathname, workspacePath, workspaceUid) => {\n  try {\n    const file = await parseGlobalEnvironmentFile(pathname, workspacePath, workspaceUid);\n    win.webContents.send('main:global-environment-added', workspaceUid, file);\n  } catch (error) {\n    console.error('Error handling global environment file add:', error);\n  }\n};\n\nconst handleGlobalEnvironmentFileChange = async (win, pathname, workspacePath, workspaceUid) => {\n  try {\n    const file = await parseGlobalEnvironmentFile(pathname, workspacePath, workspaceUid);\n    win.webContents.send('main:global-environment-changed', workspaceUid, file);\n  } catch (error) {\n    console.error('Error handling global environment file change:', error);\n  }\n};\n\nconst handleGlobalEnvironmentFileUnlink = async (win, pathname, workspaceUid) => {\n  try {\n    const environmentUid = generateUidBasedOnHash(pathname);\n    win.webContents.send('main:global-environment-deleted', workspaceUid, environmentUid);\n  } catch (error) {\n    console.error('Error handling global environment file unlink:', error);\n  }\n};\n\nclass WorkspaceWatcher {\n  constructor() {\n    this.watchers = {};\n    this.environmentWatchers = {};\n  }\n\n  addWatcher(win, workspacePath) {\n    const workspaceFilePath = path.join(workspacePath, 'workspace.yml');\n    const environmentsDir = path.join(workspacePath, 'environments');\n    const workspaceUid = getWorkspaceUid(workspacePath);\n\n    if (this.watchers[workspacePath]) {\n      this.watchers[workspacePath].close();\n    }\n    if (this.environmentWatchers[workspacePath]) {\n      this.environmentWatchers[workspacePath].close();\n    }\n\n    const self = this;\n    setTimeout(() => {\n      if (win.isDestroyed()) {\n        return;\n      }\n\n      const watcher = chokidar.watch(workspaceFilePath, {\n        ignoreInitial: true,\n        persistent: true,\n        ignorePermissionErrors: true,\n        awaitWriteFinish: {\n          stabilityThreshold: 80,\n          pollInterval: 10\n        }\n      });\n\n      watcher.on('change', () => handleWorkspaceFileChange(win, workspacePath));\n\n      self.watchers[workspacePath] = watcher;\n\n      dotEnvWatcher.addWorkspaceWatcher(win, workspacePath, workspaceUid);\n\n      if (fs.existsSync(environmentsDir)) {\n        const envWatcher = chokidar.watch(path.join(environmentsDir, `*.yml`), {\n          ignoreInitial: true,\n          persistent: true,\n          ignorePermissionErrors: true,\n          awaitWriteFinish: {\n            stabilityThreshold: 100,\n            pollInterval: 10\n          }\n        });\n\n        envWatcher.on('add', (pathname) => {\n          handleGlobalEnvironmentFileAdd(win, pathname, workspacePath, workspaceUid);\n        });\n\n        envWatcher.on('change', (pathname) => {\n          handleGlobalEnvironmentFileChange(win, pathname, workspacePath, workspaceUid);\n        });\n\n        envWatcher.on('unlink', (pathname) => {\n          handleGlobalEnvironmentFileUnlink(win, pathname, workspaceUid);\n        });\n\n        self.environmentWatchers[workspacePath] = envWatcher;\n      } else {\n        const dirWatcher = chokidar.watch(environmentsDir, {\n          ignoreInitial: false,\n          persistent: true,\n          ignorePermissionErrors: true,\n          depth: 0\n        });\n\n        dirWatcher.on('addDir', () => {\n          dirWatcher.close();\n          self.addWatcher(win, workspacePath);\n        });\n\n        self.environmentWatchers[workspacePath] = dirWatcher;\n      }\n    }, 100);\n  }\n\n  removeWatcher(workspacePath) {\n    try {\n      if (this.watchers[workspacePath]) {\n        this.watchers[workspacePath].close();\n        delete this.watchers[workspacePath];\n      }\n      if (this.environmentWatchers[workspacePath]) {\n        this.environmentWatchers[workspacePath].close();\n        delete this.environmentWatchers[workspacePath];\n      }\n      dotEnvWatcher.removeWorkspaceWatcher(workspacePath);\n    } catch (error) {\n      console.error('Error removing workspace watcher:', error);\n    }\n  }\n\n  hasWatcher(workspacePath) {\n    return Boolean(this.watchers[workspacePath]);\n  }\n}\n\nmodule.exports = WorkspaceWatcher;\n"
  },
  {
    "path": "packages/bruno-electron/src/cache/apiSpecUids.js",
    "content": "/**\n * we maintain a cache of apiSpec uids to ensure that we\n * preserve the same uid for a apiSpec even when the apiSpec\n * moves to a different location\n *\n * In the past, we used to generate unique ids based on the\n * pathname of the apiSpec, but we faced problems when implementing\n * functionality where the user can move the apiSpec to a different\n * location. In that case, the uid would change, and the we would\n * lose the apiSpec's draft state if the user has made some changes\n */\n\nconst apiSpecUids = new Map();\nconst { uuid } = require('../utils/common');\n\nconst getApiSpecUid = (pathname) => {\n  let uid = apiSpecUids.get(pathname);\n\n  if (!uid) {\n    uid = uuid();\n    apiSpecUids.set(pathname, uid);\n  }\n\n  return uid;\n};\n\nconst moveApiSpecUid = (oldPathname, newPathname) => {\n  const uid = apiSpecUids.get(oldPathname);\n\n  if (uid) {\n    apiSpecUids.delete(oldPathname);\n    apiSpecUids.set(newPathname, uid);\n  }\n};\n\nconst removeApiSpecUid = (pathname) => {\n  apiSpecUids.delete(pathname);\n};\n\nmodule.exports = {\n  getApiSpecUid,\n  moveApiSpecUid,\n  removeApiSpecUid\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/cache/requestUids.js",
    "content": "/**\n * we maintain a cache of request uids to ensure that we\n * preserve the same uid for a request even when the request\n * moves to a different location\n *\n * In the past, we used to generate unique ids based on the\n * pathname of the request, but we faced problems when implementing\n * functionality where the user can move the request to a different\n * location. In that case, the uid would change, and we would\n * lose the request's draft state if the user has made some changes\n */\n\nconst requestUids = new Map();\nconst exampleUids = new Map();\nconst { uuid } = require('../utils/common');\n\nconst getRequestUid = (pathname) => {\n  let uid = requestUids.get(pathname);\n\n  if (!uid) {\n    uid = uuid();\n    requestUids.set(pathname, uid);\n  }\n\n  return uid;\n};\n\nconst moveRequestUid = (oldPathname, newPathname) => {\n  const uid = requestUids.get(oldPathname);\n\n  if (uid) {\n    requestUids.delete(oldPathname);\n    requestUids.set(newPathname, uid);\n  }\n};\n\nconst deleteRequestUid = (pathname) => {\n  requestUids.delete(pathname);\n};\n\nconst getExampleUid = (pathname, index) => {\n  let uid = exampleUids.get(`${pathname}-${index}`);\n\n  if (!uid) {\n    uid = uuid();\n    exampleUids.set(`${pathname}-${index}`, uid);\n  }\n\n  return uid;\n};\n\n/**\n * Syncs the example UID cache with the current state of examples being saved.\n * This ensures the cache stays consistent when examples are added, deleted, or reordered.\n *\n * @param {string} pathname - The file path of the request\n * @param {Array} examples - The examples array being saved (each with a uid property)\n */\nconst syncExampleUidsCache = (pathname, examples = []) => {\n  // Clear all existing cache entries for this pathname\n  for (const key of exampleUids.keys()) {\n    if (key.startsWith(`${pathname}-`)) {\n      exampleUids.delete(key);\n    }\n  }\n\n  // Rebuild cache with current example UIDs at their new indices\n  examples.forEach((example, index) => {\n    if (example.uid) {\n      exampleUids.set(`${pathname}-${index}`, example.uid);\n    }\n  });\n};\n\nmodule.exports = {\n  getRequestUid,\n  moveRequestUid,\n  deleteRequestUid,\n  getExampleUid,\n  syncExampleUidsCache\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/index.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('node:child_process');\nconst isDev = require('electron-is-dev');\nconst os = require('os');\nconst { initializeShellEnv } = require('@usebruno/requests');\nconst { percentageToZoomLevel } = require('@usebruno/common');\n\nif (isDev) {\n  if (!fs.existsSync(path.join(__dirname, '../../bruno-js/src/sandbox/bundle-browser-rollup.js'))) {\n    console.log('JS Sandbox libraries have not been bundled yet');\n    console.log('Please run the below command \\nnpm run sandbox:bundle-libraries --workspace=packages/bruno-js');\n    throw new Error('JS Sandbox libraries have not been bundled yet');\n  }\n}\n\nconst { format } = require('url');\nconst { BrowserWindow, app, session, Menu, globalShortcut, ipcMain, nativeTheme } = require('electron');\nconst { setContentSecurityPolicy } = require('electron-util');\n\nif (isDev && process.env.ELECTRON_USER_DATA_PATH) {\n  console.debug('`ELECTRON_USER_DATA_PATH` found, modifying `userData` path: \\n'\n    + `\\t${app.getPath('userData')} -> ${process.env.ELECTRON_USER_DATA_PATH}`);\n\n  app.setPath('userData', process.env.ELECTRON_USER_DATA_PATH);\n}\n\n// Command line switches\nif (os.platform() === 'linux') {\n  // Use portal version 4 that supports current_folder option\n  // to address https://github.com/usebruno/bruno/issues/5471\n  // Runtime sets the default version to 3, refs https://github.com/electron/electron/pull/44426\n  app.commandLine.appendSwitch('xdg-portal-required-version', '4');\n}\n\nconst menuTemplate = require('./app/menu-template');\nconst { openCollection } = require('./app/collections');\nconst registerNetworkIpc = require('./ipc/network');\nconst registerCollectionsIpc = require('./ipc/collection');\nconst registerFilesystemIpc = require('./ipc/filesystem');\nconst registerPreferencesIpc = require('./ipc/preferences');\nconst registerSystemMonitorIpc = require('./ipc/system-monitor');\nconst registerWorkspaceIpc = require('./ipc/workspace');\nconst registerApiSpecIpc = require('./ipc/apiSpec');\nconst registerGitIpc = require('./ipc/git');\nconst registerOpenAPISyncIpc = require('./ipc/openapi-sync');\nconst collectionWatcher = require('./app/collection-watcher');\nconst WorkspaceWatcher = require('./app/workspace-watcher');\nconst ApiSpecWatcher = require('./app/apiSpecsWatcher');\nconst { loadWindowState, saveBounds, saveMaximized } = require('./utils/window');\nconst { preferencesUtil, getPreferences, savePreferences } = require('./store/preferences');\nconst { globalEnvironmentsManager } = require('./store/workspace-environments');\nconst registerNotificationsIpc = require('./ipc/notifications');\nconst registerGlobalEnvironmentsIpc = require('./ipc/global-environments');\nconst TerminalManager = require('./ipc/terminal');\nconst { safeParseJSON, safeStringifyJSON } = require('./utils/common');\nconst { getDomainsWithCookies } = require('./utils/cookies');\nconst { cookiesStore } = require('./store/cookies');\nconst SystemMonitor = require('./app/system-monitor');\nconst { getIsRunningInRosetta } = require('./utils/arch');\nconst { handleAppProtocolUrl, getAppProtocolUrlFromArgv } = require('./utils/deeplink');\n\nconst systemMonitor = new SystemMonitor();\nconst terminalManager = new TerminalManager();\n\nconst workspaceWatcher = new WorkspaceWatcher();\nconst apiSpecWatcher = new ApiSpecWatcher();\n\n// Reference: https://content-security-policy.com/\nconst contentSecurityPolicy = [\n  'default-src \\'self\\'',\n  'connect-src \\'self\\' https://*.posthog.com',\n  'font-src \\'self\\' https: data:;',\n  'frame-src data:',\n  'script-src \\'self\\' data:',\n  // this has been commented out to make oauth2 work\n  // \"form-action 'none'\",\n  // we make an exception and allow http for images so that\n  // they can be used as link in the embedded markdown editors\n  'img-src \\'self\\' blob: data: http: https:',\n  'media-src \\'self\\' blob: data: https:',\n  'style-src \\'self\\' \\'unsafe-inline\\' https:'\n];\n\nsetContentSecurityPolicy(contentSecurityPolicy.join(';') + ';');\n\nconst menu = Menu.buildFromTemplate(menuTemplate);\nconst isMac = process.platform === 'darwin';\nconst isWindows = process.platform === 'win32';\nconst isLinux = process.platform === 'linux';\n\nlet mainWindow;\nlet appProtocolUrl;\n\n// Helper function to save zoom percentage to preferences and notify renderer\nconst saveZoomPreferences = async (percentage) => {\n  if (!mainWindow) return;\n\n  const clampedPercentage = Math.max(50, Math.min(150, percentage));\n\n  const prefs = getPreferences();\n  prefs.display = prefs.display || {};\n  prefs.display.zoomPercentage = clampedPercentage;\n\n  try {\n    await savePreferences(prefs);\n    // Notify renderer to update Redux state only after successful save\n    mainWindow.webContents.send('main:load-preferences', prefs);\n  } catch (err) {\n    console.error('Failed to save zoom preference:', err);\n  }\n};\n\n// Helper function to focus and restore the main window\nconst focusMainWindow = () => {\n  if (mainWindow) {\n    app.focus({ steal: true });\n    if (mainWindow.isMinimized()) {\n      mainWindow.restore();\n    }\n    mainWindow.focus();\n  }\n};\n\n// Parse protocol URL from command line arguments (if any)\nappProtocolUrl = getAppProtocolUrlFromArgv(process.argv);\n\n// Single instance lock - ensures only one instance of Bruno runs at a time (enabled by default)\nconst useSingleInstance = process.env.DISABLE_SINGLE_INSTANCE !== 'true';\nconst gotTheLock = useSingleInstance ? app.requestSingleInstanceLock() : true;\n\nif (useSingleInstance && !gotTheLock) {\n  // Another instance is already running, quit immediately\n  app.quit();\n} else {\n  // This is the primary instance (or single instance is disabled)\n\n  // Try to remove any existing registrations\n  app.removeAsDefaultProtocolClient('bruno');\n  // Register as default handler for `bruno://` protocol URLs\n  app.setAsDefaultProtocolClient('bruno');\n\n  if (isLinux) {\n    try {\n      execSync('xdg-mime default bruno.desktop x-scheme-handler/bruno');\n    } catch (err) {}\n  }\n\n  // Handle protocol URLs for MacOS\n  if (isMac) {\n    app.on('open-url', (event, url) => {\n      event.preventDefault();\n      if (url) {\n        if (mainWindow) {\n          focusMainWindow();\n          handleAppProtocolUrl(url);\n        } else {\n          // Store for handling after window is ready\n          appProtocolUrl = url;\n        }\n      }\n    });\n  }\n\n  // Handle second instance attempts - focus primary window on all platforms\n  app.on('second-instance', (event, commandLine) => {\n    focusMainWindow();\n    // Extract and handle protocol URL from the second instance attempt\n    const url = getAppProtocolUrlFromArgv(commandLine);\n    if (url) {\n      handleAppProtocolUrl(url);\n    }\n  });\n}\n\n// Prepare the renderer once the app is ready\napp.on('ready', async () => {\n  // Ensure shell environment is loaded before any operations that need it\n  await initializeShellEnv();\n\n  if (isDev) {\n    const { installExtension, REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer');\n    try {\n      const extensions = await installExtension([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS], {\n        loadExtensionOptions: { allowFileAccess: true }\n      });\n      console.log(`Added Extensions:  ${extensions.map((ext) => ext.name).join(', ')}`);\n      await require('node:timers/promises').setTimeout(1000);\n      session.defaultSession.getAllExtensions().map((ext) => {\n        console.log(`Loading Extension: ${ext.name}`);\n        session.defaultSession.loadExtension(ext.path);\n      });\n    } catch (err) {\n      console.error('An error occurred while loading extensions: ', err);\n    }\n  }\n\n  // Initialize system proxy cache early (non-blocking)\n  const { fetchSystemProxy } = require('./store/system-proxy');\n  fetchSystemProxy().catch((err) => {\n    console.warn('Failed to initialize system proxy cache:', err);\n  });\n\n  Menu.setApplicationMenu(menu);\n  const { maximized, x, y, width, height } = loadWindowState();\n\n  mainWindow = new BrowserWindow({\n    x,\n    y,\n    width,\n    height,\n    minWidth: 700,\n    minHeight: 400,\n    show: false,\n    webPreferences: {\n      nodeIntegration: true,\n      contextIsolation: true,\n      preload: path.join(__dirname, 'preload.js'),\n      webviewTag: true\n    },\n    title: 'Bruno',\n    icon: path.join(__dirname, 'about/256x256.png'),\n    titleBarStyle: isMac ? 'hiddenInset' : isWindows ? 'hidden' : undefined,\n    frame: isLinux ? false : true,\n    trafficLightPosition: isMac ? { x: 12, y: 10 } : undefined\n    // we will bring this back\n    // see https://github.com/usebruno/bruno/issues/440\n    // autoHideMenuBar: true\n  });\n\n  if (maximized) {\n    mainWindow.maximize();\n  }\n\n  ipcMain.on('renderer:window-minimize', () => {\n    if (!isWindows && !isLinux) return;\n    mainWindow.minimize();\n  });\n\n  ipcMain.on('renderer:window-maximize', () => {\n    if (!isWindows && !isLinux) return;\n    if (mainWindow.isMaximized()) {\n      mainWindow.unmaximize();\n    } else {\n      mainWindow.maximize();\n    }\n  });\n\n  ipcMain.on('renderer:window-close', () => {\n    if (!isWindows && !isLinux) return;\n    mainWindow.close();\n  });\n\n  ipcMain.handle('renderer:window-is-maximized', () => {\n    if (!isWindows && !isLinux) return false;\n    return mainWindow.isMaximized();\n  });\n\n  ipcMain.handle('renderer:open-preferences', () => {\n    ipcMain.emit('main:open-preferences');\n  });\n\n  ipcMain.handle('renderer:toggle-devtools', () => {\n    mainWindow.webContents.toggleDevTools();\n  });\n\n  ipcMain.handle('renderer:reset-zoom', () => {\n    updateZoomLevel(100);\n  });\n\n  ipcMain.handle('renderer:zoom-in', () => {\n    incrementZoomAndPersist(10);\n  });\n\n  ipcMain.handle('renderer:zoom-out', () => {\n    incrementZoomAndPersist(-10);\n  });\n\n  // Menu event handlers for zoom (from menu-template.js)\n  ipcMain.on('menu:reset-zoom', () => {\n    updateZoomLevel(100);\n  });\n\n  ipcMain.on('menu:zoom-in', () => {\n    incrementZoomAndPersist(10);\n  });\n\n  ipcMain.on('menu:zoom-out', () => {\n    incrementZoomAndPersist(-10);\n  });\n\n  ipcMain.handle('renderer:set-zoom-level', (event, zoomLevel) => {\n    mainWindow.webContents.setZoomLevel(zoomLevel);\n  });\n\n  ipcMain.handle('renderer:toggle-fullscreen', () => {\n    mainWindow.setFullScreen(!mainWindow.isFullScreen());\n  });\n\n  ipcMain.handle('renderer:open-docs', () => {\n    ipcMain.emit('main:open-docs');\n  });\n\n  ipcMain.handle('renderer:open-about', () => {\n    const { version } = require('../package.json');\n    const aboutBruno = require('./app/about-bruno');\n    const aboutWindow = new BrowserWindow({\n      width: 350,\n      height: 250,\n      webPreferences: {\n        nodeIntegration: true\n      }\n    });\n    aboutWindow.removeMenu();\n    aboutWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(aboutBruno({ version }))}`);\n  });\n\n  mainWindow.once('ready-to-show', () => {\n    // Apply saved zoom level from preferences before showing window\n    const zoomPercentage = preferencesUtil.getZoomPercentage();\n    if (zoomPercentage) {\n      const zoomLevel = percentageToZoomLevel(zoomPercentage);\n      mainWindow.webContents.setZoomLevel(zoomLevel);\n    }\n    mainWindow.show();\n  });\n  const devPort = process.env.BRUNO_DEV_PORT || 3000;\n  const url = isDev\n    ? `http://localhost:${devPort}`\n    : format({\n        pathname: path.join(__dirname, '../web/index.html'),\n        protocol: 'file:',\n        slashes: true\n      });\n\n  mainWindow.loadURL(url).catch((reason) => {\n    console.error(`Error: Failed to load URL: \"${url}\" (Electron shows a blank screen because of this).`);\n    console.error('Original message:', reason);\n    if (isDev) {\n      console.error(\n        'Could not connect to Next.Js dev server, is it running?'\n        + ' Start the dev server using \"npm run dev:web\" and restart electron'\n      );\n    } else {\n      console.error(\n        'If you are using an official production build: the above error is most likely a bug! '\n        + ' Please report this under: https://github.com/usebruno/bruno/issues'\n      );\n    }\n  });\n\n  let boundsTimeout;\n  const handleBoundsChange = () => {\n    if (!mainWindow.isMaximized()) {\n      if (boundsTimeout) {\n        clearTimeout(boundsTimeout);\n      }\n      boundsTimeout = setTimeout(() => {\n        saveBounds(mainWindow);\n      }, 100);\n    }\n  };\n\n  mainWindow.on('resize', handleBoundsChange);\n  mainWindow.on('move', handleBoundsChange);\n\n  mainWindow.on('maximize', () => {\n    saveMaximized(true);\n    mainWindow.webContents.send('main:window-maximized');\n  });\n  mainWindow.on('unmaximize', () => {\n    saveMaximized(false);\n    mainWindow.webContents.send('main:window-unmaximized');\n  });\n\n  // Full screen events for title bar padding adjustment\n  mainWindow.on('enter-full-screen', () => {\n    mainWindow.webContents.send('main:enter-full-screen');\n  });\n  mainWindow.on('leave-full-screen', () => {\n    mainWindow.webContents.send('main:leave-full-screen');\n  });\n\n  mainWindow.on('close', (e) => {\n    e.preventDefault();\n    terminalManager.cleanup(mainWindow.webContents);\n    ipcMain.emit('main:start-quit-flow');\n  });\n\n  mainWindow.webContents.on('will-redirect', (event, url) => {\n    event.preventDefault();\n    if (/^(http:\\/\\/|https:\\/\\/)/.test(url)) {\n      require('electron').shell.openExternal(url);\n    }\n  });\n\n  mainWindow.webContents.once('did-finish-load', () => {\n    if (appProtocolUrl) {\n      handleAppProtocolUrl(appProtocolUrl);\n    }\n  });\n\n  mainWindow.webContents.setWindowOpenHandler(({ url }) => {\n    try {\n      const { protocol } = new URL(url);\n      if (['https:', 'http:'].includes(protocol)) {\n        require('electron').shell.openExternal(url);\n      }\n    } catch (e) {\n      console.error(e);\n    }\n    return { action: 'deny' };\n  });\n\n  mainWindow.webContents.on('did-finish-load', async () => {\n    try {\n      let ogSend = mainWindow.webContents.send;\n      mainWindow.webContents.send = function (channel, ...args) {\n        return ogSend.apply(this, [channel, ...args.map((_) => {\n          // todo: replace this with @msgpack/msgpack encode/decode\n          return safeParseJSON(safeStringifyJSON(_));\n        })]);\n      };\n    } catch (err) {\n      console.error('Error wrapping webContents.send:', err);\n    }\n\n    // Send cookies list after renderer is ready\n    try {\n      cookiesStore.initializeCookies();\n      const cookiesList = await getDomainsWithCookies();\n      mainWindow.webContents.send('main:cookies-update', cookiesList);\n    } catch (err) {\n      console.error('Failed to load cookies for renderer', err);\n    }\n\n    mainWindow.webContents.send('main:app-loaded', {\n      isRunningInRosetta: getIsRunningInRosetta()\n    });\n  });\n\n  // register all ipc handlers\n  registerNetworkIpc(mainWindow);\n  registerGlobalEnvironmentsIpc(mainWindow, globalEnvironmentsManager);\n  registerCollectionsIpc(mainWindow, collectionWatcher);\n  registerPreferencesIpc(mainWindow, collectionWatcher);\n  registerWorkspaceIpc(mainWindow, workspaceWatcher);\n  registerApiSpecIpc(mainWindow, apiSpecWatcher);\n  registerNotificationsIpc(mainWindow, collectionWatcher);\n  registerFilesystemIpc(mainWindow);\n  registerSystemMonitorIpc(mainWindow, systemMonitor);\n  registerGitIpc(mainWindow);\n  registerOpenAPISyncIpc(mainWindow);\n});\n\n// Quit the app once all windows are closed\napp.on('before-quit', () => {\n  // Release single instance lock to allow other instances to take over\n  if (useSingleInstance && gotTheLock) {\n    app.releaseSingleInstanceLock();\n  }\n\n  try {\n    cookiesStore.saveCookieJar(true);\n  } catch (err) {\n    console.warn('Failed to flush cookies on quit', err);\n  }\n\n  // Stop system monitoring\n  systemMonitor.stop();\n\n  try {\n    terminalManager.killAll();\n  } catch (err) {\n    console.error('Failed to kill all terminals on quit', err);\n  }\n});\n\napp.on('window-all-closed', app.quit);\n\n// Open collection from Recent menu (#1521)\napp.on('open-file', (event, path) => {\n  openCollection(mainWindow, collectionWatcher, path);\n});\n\n// Register the global shortcuts\napp.on('browser-window-focus', () => {\n  // Quick fix for Electron issue #29996: https://github.com/electron/electron/issues/29996\n  globalShortcut.register('Ctrl+=', () => {\n    incrementZoomAndPersist(10);\n  });\n});\n\n// Disable global shortcuts when not focused\napp.on('browser-window-blur', () => {\n  globalShortcut.unregisterAll();\n});\n\n/**\n * @param {number} inc (+/- amount to zoom in / out);\n */\nfunction incrementZoomAndPersist(inc) {\n  const currentPercentage = preferencesUtil.getZoomPercentage();\n  const nextPercentage = Math.min(\n    Math.max(currentPercentage + inc, 50),\n    150\n  );\n  updateZoomLevel(nextPercentage);\n}\n\n/**\n * @param {number} percent percentage to increase or decrease zoom by, percentage is converted to chrome's log value internally\n */\nfunction updateZoomLevel(percent) {\n  const zoomLevel = percentageToZoomLevel(percent);\n  mainWindow.webContents.setZoomLevel(zoomLevel);\n  saveZoomPreferences(percent);\n}\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/apiSpec.js",
    "content": "const { ipcMain } = require('electron');\nconst { openApiSpecDialog, openApiSpec } = require('../app/apiSpecs');\nconst { writeFile } = require('../utils/filesystem');\nconst { removeApiSpecUid } = require('../cache/apiSpecUids');\nconst { removeApiSpecFromWorkspace } = require('../utils/workspace-config');\nconst path = require('path');\nconst fs = require('fs');\n\nconst registerRendererEventHandlers = (mainWindow, watcher, lastOpenedApiSpecs) => {\n  ipcMain.handle('renderer:open-api-spec', (event, workspacePath = null) => {\n    if (watcher && mainWindow) {\n      openApiSpecDialog(mainWindow, watcher, { workspacePath });\n    }\n  });\n\n  ipcMain.handle('renderer:open-api-spec-file', (event, apiSpecPath, workspacePath = null) => {\n    if (watcher && mainWindow) {\n      openApiSpec(mainWindow, watcher, apiSpecPath, { workspacePath });\n    }\n  });\n\n  ipcMain.handle('renderer:save-api-spec', async (event, pathname, content) => {\n    try {\n      await writeFile(pathname, content);\n      Promise.resolve();\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:create-api-spec', async (event, apiSpecName, apiSpecLocation, content = '', workspacePath = null) => {\n    try {\n      let pathname = path.join(apiSpecLocation, apiSpecName);\n      if (fs.existsSync(pathname)) {\n        throw new Error(`path: ${pathname} already exists`);\n      }\n      await writeFile(pathname, content);\n      openApiSpec(mainWindow, watcher, pathname, { workspacePath });\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:remove-api-spec', async (event, pathname, workspacePath = null) => {\n    try {\n      if (watcher && mainWindow) {\n        watcher.removeWatcher(pathname, mainWindow);\n        removeApiSpecUid(pathname);\n\n        if (workspacePath) {\n          const workspaceFilePath = path.join(workspacePath, 'workspace.yml');\n\n          if (fs.existsSync(workspaceFilePath)) {\n            await removeApiSpecFromWorkspace(workspacePath, pathname);\n          }\n        }\n      }\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:fetch-api-spec', async (event, url) => {\n    try {\n      const data = await fetch(url).then((res) => res.text());\n      return data;\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:ensure-apispec-folder', async (event, workspacePath) => {\n    try {\n      const apiSpecPath = path.join(workspacePath, 'apispec');\n      if (!fs.existsSync(apiSpecPath)) {\n        fs.mkdirSync(apiSpecPath, { recursive: true });\n      }\n      return apiSpecPath;\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n};\n\nconst registerMainEventHandlers = (mainWindow, watcher, lastOpenedApiSpecs) => {\n  ipcMain.handle('main:open-api-spec', () => {\n    if (watcher && mainWindow) {\n      openApiSpecDialog(mainWindow, watcher);\n    }\n  });\n  ipcMain.on('main:apispec-opened', (win, pathname, uid, workspacePath = null) => {\n    watcher.addWatcher(win, pathname, uid, {}, workspacePath);\n  });\n};\n\nconst registerApiSpecIpc = (mainWindow, watcher, lastOpenedApiSpecs) => {\n  registerRendererEventHandlers(mainWindow, watcher, lastOpenedApiSpecs);\n  registerMainEventHandlers(mainWindow, watcher, lastOpenedApiSpecs);\n};\n\nmodule.exports = registerApiSpecIpc;\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/collection.js",
    "content": "const _ = require('lodash');\nconst fs = require('fs');\nconst fsExtra = require('fs-extra');\nconst os = require('os');\nconst path = require('path');\nconst archiver = require('archiver');\nconst extractZip = require('extract-zip');\nconst AdmZip = require('adm-zip');\nconst { ipcMain, shell, dialog, app } = require('electron');\nconst {\n  parseRequest,\n  stringifyRequest,\n  parseRequestViaWorker,\n  stringifyRequestViaWorker,\n  parseCollection,\n  stringifyCollection,\n  parseFolder,\n  stringifyFolder,\n  stringifyEnvironment,\n  parseEnvironment,\n  DEFAULT_COLLECTION_FORMAT\n} = require('@usebruno/filestore');\nconst { dotenvToJson } = require('@usebruno/lang');\nconst brunoConverters = require('@usebruno/converters');\nconst { postmanToBruno } = brunoConverters;\nconst { cookiesStore } = require('../store/cookies');\nconst { parseLargeRequestWithRedaction } = require('../utils/parse');\nconst { wsClient } = require('../ipc/network/ws-event-handlers');\nconst { hasSubDirectories } = require('../utils/filesystem');\n\nconst {\n  DEFAULT_GITIGNORE,\n  writeFile,\n  hasBruExtension,\n  isDirectory,\n  createDirectory,\n  sanitizeName,\n  isWSLPath,\n  safeToRename,\n  isWindowsOS,\n  hasRequestExtension,\n  getCollectionFormat,\n  searchForRequestFiles,\n  validateName,\n  getCollectionStats,\n  sizeInMB,\n  safeWriteFileSync,\n  copyPath,\n  removePath,\n  getPaths,\n  generateUniqueName,\n  isDotEnvFile,\n  isValidDotEnvFilename,\n  isBrunoConfigFile,\n  isBruEnvironmentConfig,\n  isCollectionRootBruFile,\n  scanForBrunoFiles\n} = require('../utils/filesystem');\nconst { openCollectionDialog, openCollectionsByPathname, registerScratchCollectionPath } = require('../app/collections');\nconst { generateUidBasedOnHash, stringifyJson, safeStringifyJSON, safeParseJSON } = require('../utils/common');\nconst { moveRequestUid, deleteRequestUid, syncExampleUidsCache } = require('../cache/requestUids');\nconst { deleteCookiesForDomain, getDomainsWithCookies, addCookieForDomain, modifyCookieForDomain, parseCookieString, createCookieString, deleteCookie } = require('../utils/cookies');\nconst EnvironmentSecretsStore = require('../store/env-secrets');\nconst CollectionSecurityStore = require('../store/collection-security');\nconst UiStateSnapshotStore = require('../store/ui-state-snapshot');\nconst interpolateVars = require('./network/interpolate-vars');\nconst { interpolateString } = require('./network/interpolate-string');\nconst { getEnvVars, getTreePathFromCollectionToItem, mergeVars, parseBruFileMeta, hydrateRequestWithUuid, transformRequestToSaveToFilesystem } = require('../utils/collection');\nconst { getProcessEnvVars } = require('../store/process-env');\nconst { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, getOAuth2TokenUsingImplicitGrant, refreshOauth2Token } = require('../utils/oauth2');\nconst { getCertsAndProxyConfig } = require('./network/cert-utils');\nconst collectionWatcher = require('../app/collection-watcher');\nconst { transformBrunoConfigBeforeSave } = require('../utils/transformBrunoConfig');\nconst { REQUEST_TYPES } = require('../utils/constants');\nconst { cancelOAuth2AuthorizationRequest, isOauth2AuthorizationRequestInProgress } = require('../utils/oauth2-protocol-handler');\nconst { findUniqueFolderName } = require('../utils/collection-import');\nconst { saveSpecAndUpdateMetadata, cleanupSpecFilesForCollection } = require('./openapi-sync');\n\nconst environmentSecretsStore = new EnvironmentSecretsStore();\nconst collectionSecurityStore = new CollectionSecurityStore();\nconst uiStateSnapshotStore = new UiStateSnapshotStore();\n\n// size and file count limits to determine whether the bru files in the collection should be loaded asynchronously or not.\nconst MAX_COLLECTION_SIZE_IN_MB = 20;\nconst MAX_SINGLE_FILE_SIZE_IN_COLLECTION_IN_MB = 5;\nconst MAX_COLLECTION_FILES_COUNT = 2000;\n\n// Get the base directory for transient request files (stored in app data directory)\nconst getTransientDirectoryBase = () => {\n  return path.join(app.getPath('userData'), 'tmp', 'transient');\n};\n\n// Get the prefix used for transient collection directories\nconst getTransientCollectionPrefix = () => {\n  return path.join(getTransientDirectoryBase(), 'bruno-');\n};\n\n// Get the prefix used for scratch collection directories\nconst getTransientScratchPrefix = () => {\n  return path.join(getTransientDirectoryBase(), 'bruno-scratch-');\n};\n\n// Check if a path is within the transient directory\nconst isTransientPath = (filePath) => {\n  const transientBase = getTransientDirectoryBase();\n  return filePath.startsWith(transientBase + path.sep) || filePath.startsWith(transientBase);\n};\n\nconst envHasSecrets = (environment = {}) => {\n  const secrets = _.filter(environment.variables, (v) => v.secret);\n\n  return secrets && secrets.length > 0;\n};\n\nconst findCollectionPathByItemPath = (filePath) => {\n  const parts = filePath.split(path.sep);\n  const index = parts.findIndex((part) => part.startsWith('bruno-'));\n\n  if (isTransientPath(filePath) && index !== -1) {\n    const transientDirPath = parts.slice(0, index + 1).join(path.sep);\n    const metadataPath = path.join(transientDirPath, 'metadata.json');\n    try {\n      const metadataContent = fs.readFileSync(metadataPath, 'utf8');\n      const metadata = JSON.parse(metadataContent);\n\n      if (metadata.type === 'scratch') {\n        return transientDirPath;\n      }\n\n      if (metadata.collectionPath) {\n        return metadata.collectionPath;\n      }\n    } catch (error) {\n      return null;\n    }\n    return null;\n  }\n\n  const allCollectionPaths = collectionWatcher.getAllWatcherPaths();\n\n  // Find the collection path that contains this file\n  // Sort by length descending to find the most specific (deepest) match first\n  const sortedPaths = allCollectionPaths.sort((a, b) => b.length - a.length);\n\n  // Normalize the file path for comparison\n  const normalizedFilePath = path.normalize(filePath);\n\n  for (const collectionPath of sortedPaths) {\n    const normalizedCollectionPath = path.normalize(collectionPath);\n    if (normalizedFilePath.startsWith(normalizedCollectionPath + path.sep) || normalizedFilePath === normalizedCollectionPath) {\n      return collectionPath;\n    }\n  }\n\n  return null;\n};\n\nconst validatePathIsInsideCollection = (filePath) => {\n  const collectionPath = findCollectionPathByItemPath(filePath);\n\n  if (!collectionPath) {\n    throw new Error(`Path: ${filePath} should be inside a collection`);\n  }\n};\n\nconst registerRendererEventHandlers = (mainWindow, watcher) => {\n  // create collection\n  ipcMain.handle(\n    'renderer:create-collection',\n    async (event, collectionName, collectionFolderName, collectionLocation, options = {}) => {\n      try {\n        const format = options.format || DEFAULT_COLLECTION_FORMAT;\n        collectionFolderName = sanitizeName(collectionFolderName);\n        const dirPath = path.join(collectionLocation, collectionFolderName);\n        if (fs.existsSync(dirPath)) {\n          const files = fs.readdirSync(dirPath);\n\n          if (files.length > 0) {\n            throw new Error(`collection: ${dirPath} already exists and is not empty`);\n          }\n        }\n\n        if (!validateName(path.basename(dirPath))) {\n          throw new Error(`collection: invalid pathname - ${dirPath}`);\n        }\n\n        if (!fs.existsSync(dirPath)) {\n          await createDirectory(dirPath);\n        }\n\n        const uid = generateUidBasedOnHash(dirPath);\n        let brunoConfig = {\n          version: '1',\n          name: collectionName,\n          type: 'collection',\n          ignore: ['node_modules', '.git']\n        };\n\n        if (format === 'yml') {\n          const collectionRoot = {\n            meta: {\n              name: collectionName\n            }\n          };\n          // For YAML collections, set opencollection instead of version\n          brunoConfig = {\n            opencollection: '1.0.0',\n            name: collectionName,\n            type: 'collection',\n            ignore: ['node_modules', '.git']\n          };\n          const content = stringifyCollection(collectionRoot, brunoConfig, { format });\n          await writeFile(path.join(dirPath, 'opencollection.yml'), content);\n        } else if (format === 'bru') {\n          const content = await stringifyJson(brunoConfig);\n          await writeFile(path.join(dirPath, 'bruno.json'), content);\n        } else {\n          throw new Error(`Invalid format: ${format}`);\n        }\n\n        await writeFile(path.join(dirPath, '.gitignore'), DEFAULT_GITIGNORE);\n\n        const { size, filesCount } = await getCollectionStats(dirPath);\n        brunoConfig.size = size;\n        brunoConfig.filesCount = filesCount;\n\n        mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig);\n        ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid, brunoConfig);\n      } catch (error) {\n        return Promise.reject(error);\n      }\n    }\n  );\n  // clone collection\n  ipcMain.handle(\n    'renderer:clone-collection',\n    async (event, collectionName, collectionFolderName, collectionLocation, previousPath) => {\n      collectionFolderName = sanitizeName(collectionFolderName);\n      const dirPath = path.join(collectionLocation, collectionFolderName);\n      if (fs.existsSync(dirPath)) {\n        throw new Error(`collection: ${dirPath} already exists`);\n      }\n\n      if (!validateName(path.basename(dirPath))) {\n        throw new Error(`collection: invalid pathname - ${dirPath}`);\n      }\n\n      // create dir\n      await createDirectory(dirPath);\n      const uid = generateUidBasedOnHash(dirPath);\n      const format = getCollectionFormat(previousPath);\n      let brunoConfig;\n\n      if (format === 'yml') {\n        const configFilePath = path.join(previousPath, 'opencollection.yml');\n        const content = fs.readFileSync(configFilePath, 'utf8');\n        const {\n          brunoConfig: parsedBrunoConfig,\n          collectionRoot\n        } = parseCollection(content, { format });\n\n        brunoConfig = parsedBrunoConfig;\n        brunoConfig.name = collectionName;\n\n        const newContent = stringifyCollection(collectionRoot, brunoConfig, { format });\n        await writeFile(path.join(dirPath, 'opencollection.yml'), newContent);\n      } else if (format === 'bru') {\n        const configFilePath = path.join(previousPath, 'bruno.json');\n        const content = fs.readFileSync(configFilePath, 'utf8');\n        brunoConfig = JSON.parse(content);\n        brunoConfig.name = collectionName;\n        const newContent = await stringifyJson(brunoConfig);\n        await writeFile(path.join(dirPath, 'bruno.json'), newContent);\n      } else {\n        throw new Error(`Invalid collectionformat: ${format}`);\n      }\n\n      // Now copy all the files matching the collection's filetype along with the dir\n      const files = searchForRequestFiles(previousPath);\n\n      for (const sourceFilePath of files) {\n        const relativePath = path.relative(previousPath, sourceFilePath);\n        const newFilePath = path.join(dirPath, relativePath);\n\n        // skip if the file is opencollection.yml or bruno.json at the root of the collection\n        const isRootConfigFile = (path.basename(sourceFilePath) === 'opencollection.yml' || path.basename(sourceFilePath) === 'bruno.json')\n          && path.dirname(sourceFilePath) === previousPath;\n\n        if (isRootConfigFile) {\n          continue;\n        }\n\n        // handle dir of files\n        fs.mkdirSync(path.dirname(newFilePath), { recursive: true });\n        // copy each files\n        fs.copyFileSync(sourceFilePath, newFilePath);\n      }\n\n      const { size, filesCount } = await getCollectionStats(dirPath);\n      brunoConfig.size = size;\n      brunoConfig.filesCount = filesCount;\n\n      mainWindow.webContents.send('main:collection-opened', dirPath, uid, brunoConfig);\n      ipcMain.emit('main:collection-opened', mainWindow, dirPath, uid);\n    }\n  );\n  // rename collection\n  ipcMain.handle('renderer:rename-collection', async (event, newName, collectionPathname) => {\n    try {\n      const format = getCollectionFormat(collectionPathname);\n\n      if (format === 'yml') {\n        const configFilePath = path.join(collectionPathname, 'opencollection.yml');\n        const content = fs.readFileSync(configFilePath, 'utf8');\n        const {\n          brunoConfig,\n          collectionRoot\n        } = parseCollection(content, { format: 'yml' });\n\n        brunoConfig.name = newName;\n\n        const newContent = stringifyCollection(collectionRoot, brunoConfig, { format: 'yml' });\n        await writeFile(path.join(collectionPathname, 'opencollection.yml'), newContent);\n      } else if (format === 'bru') {\n        const configFilePath = path.join(collectionPathname, 'bruno.json');\n        const content = fs.readFileSync(configFilePath, 'utf8');\n        const brunoConfig = JSON.parse(content);\n        brunoConfig.name = newName;\n        const newContent = await stringifyJson(brunoConfig);\n        await writeFile(path.join(collectionPathname, 'bruno.json'), newContent);\n      } else {\n        throw new Error(`Invalid format: ${format}`);\n      }\n\n      mainWindow.webContents.send('main:collection-renamed', {\n        collectionPathname,\n        newName\n      });\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:save-folder-root', async (event, folder) => {\n    try {\n      const { name: folderName, root: folderRoot = {}, folderPathname, collectionPathname } = folder;\n\n      const format = getCollectionFormat(collectionPathname);\n      const folderFilePath = path.join(folderPathname, `folder.${format}`);\n\n      if (!folderRoot.meta) {\n        folderRoot.meta = {\n          name: folderName\n        };\n      }\n\n      const content = await stringifyFolder(folderRoot, { format });\n      await writeFile(folderFilePath, content);\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  // save collection root\n  ipcMain.handle('renderer:save-collection-root', async (event, collectionPathname, collectionRoot, brunoConfig) => {\n    try {\n      const format = getCollectionFormat(collectionPathname);\n      const filename = format === 'yml' ? 'opencollection.yml' : 'collection.bru';\n      const content = await stringifyCollection(collectionRoot, brunoConfig, { format });\n\n      await writeFile(path.join(collectionPathname, filename), content);\n    } catch (error) {\n      console.error('Error in save-collection-root:', error);\n      return Promise.reject(error);\n    }\n  });\n\n  // new request\n  ipcMain.handle('renderer:new-request', async (event, pathname, request) => {\n    try {\n      if (fs.existsSync(pathname)) {\n        throw new Error(`path: ${pathname} already exists`);\n      }\n\n      const collectionPath = findCollectionPathByItemPath(pathname);\n      if (!collectionPath) {\n        throw new Error('Collection not found for the given pathname');\n      }\n      const format = getCollectionFormat(collectionPath);\n\n      // For the actual filename part, we want to be strict\n      const baseFilename = request?.filename?.replace(`.${format}`, '');\n      if (!validateName(baseFilename)) {\n        throw new Error(`${request.filename} is not a valid filename`);\n      }\n      validatePathIsInsideCollection(pathname);\n\n      const content = await stringifyRequestViaWorker(request, { format });\n      await writeFile(pathname, content);\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  // save request\n  ipcMain.handle('renderer:save-request', async (event, pathname, request, format) => {\n    try {\n      if (!fs.existsSync(pathname)) {\n        throw new Error(`path: ${pathname} does not exist`);\n      }\n\n      // Sync example UIDs cache to maintain consistency when examples are added/deleted/reordered\n      syncExampleUidsCache(pathname, request.examples);\n\n      const content = await stringifyRequestViaWorker(request, { format });\n      await writeFile(pathname, content);\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:save-transient-request', async (event, { sourcePathname, targetDirname, targetFilename, request, format, sourceFormat }) => {\n    try {\n      if (!fs.existsSync(sourcePathname)) {\n        throw new Error(`Source path: ${sourcePathname} does not exist`);\n      }\n\n      if (!fs.existsSync(targetDirname)) {\n        throw new Error(`Target directory: ${targetDirname} does not exist`);\n      }\n\n      validatePathIsInsideCollection(targetDirname);\n\n      const collectionPath = findCollectionPathByItemPath(targetDirname);\n      if (!collectionPath) {\n        throw new Error('Could not determine collection for target directory');\n      }\n      const targetFormat = getCollectionFormat(collectionPath);\n\n      const filename = targetFilename || path.basename(sourcePathname);\n      const filenameWithoutExt = filename.replace(/\\.(bru|yml)$/, '');\n      const finalFilename = `${filenameWithoutExt}.${targetFormat}`;\n      const targetPathname = path.join(targetDirname, finalFilename);\n\n      if (fs.existsSync(targetPathname)) {\n        throw new Error(`A file with the name \"${finalFilename}\" already exists in the target location`);\n      }\n\n      const actualSourceFormat = sourceFormat || 'yml';\n      const needsConversion = actualSourceFormat !== targetFormat;\n\n      let finalContent;\n      if (needsConversion) {\n        const { parseRequest, stringifyRequest } = require('@usebruno/filestore');\n        const sourceContent = await fs.promises.readFile(sourcePathname, 'utf8');\n        const parsedRequest = parseRequest(sourceContent, { format: actualSourceFormat });\n        const mergedRequest = { ...parsedRequest, ...request };\n        syncExampleUidsCache(sourcePathname, mergedRequest.examples);\n        finalContent = stringifyRequest(mergedRequest, { format: targetFormat });\n      } else {\n        syncExampleUidsCache(sourcePathname, request.examples);\n        finalContent = await stringifyRequestViaWorker(request, { format: targetFormat });\n      }\n\n      await writeFile(targetPathname, finalContent);\n      return { newPathname: targetPathname };\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  // save multiple requests\n  ipcMain.handle('renderer:save-multiple-requests', async (event, requestsToSave) => {\n    try {\n      for (let r of requestsToSave) {\n        const request = r.item;\n        const pathname = r.pathname;\n\n        if (!fs.existsSync(pathname)) {\n          throw new Error(`path: ${pathname} does not exist`);\n        }\n\n        const content = await stringifyRequestViaWorker(request, { format: r.format });\n        await writeFile(pathname, content);\n      }\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  // Helper: Parse file content based on scope type\n  const parseFileByType = async (fileContent, scopeType, format) => {\n    switch (scopeType) {\n      case 'request':\n        return await parseRequestViaWorker(fileContent, { format });\n      case 'folder':\n        return parseFolder(fileContent, { format });\n      case 'collection':\n        return parseCollection(fileContent, { format });\n      default:\n        throw new Error(`Invalid scope type: ${scopeType}`);\n    }\n  };\n\n  const stringifyByType = async (data, scopeType, collectionRoot, format) => {\n    switch (scopeType) {\n      case 'request':\n        return await stringifyRequestViaWorker(data, { format });\n      case 'folder':\n        return stringifyFolder(data, { format });\n      case 'collection':\n        return stringifyCollection(collectionRoot, data, { format });\n      default:\n        throw new Error(`Invalid scope type: ${scopeType}`);\n    }\n  };\n\n  // Helper: Update or create variable in array\n  const updateOrCreateVariable = (variables, variable) => {\n    const existingVar = variables.find((v) => v.name === variable.name);\n\n    if (existingVar) {\n      // Update existing variable\n      return variables.map((v) => (v.name === variable.name ? variable : v));\n    }\n\n    // Create new variable\n    return [...variables, variable];\n  };\n\n  // update variable in request/folder/collection file\n  ipcMain.handle('renderer:update-variable-in-file', async (event, pathname, variable, scopeType, collectionRoot, format) => {\n    try {\n      if (!fs.existsSync(pathname)) {\n        throw new Error(`path: ${pathname} does not exist`);\n      }\n\n      // Read and parse the file\n      const fileContent = fs.readFileSync(pathname, 'utf8');\n      const parsedData = await parseFileByType(fileContent, scopeType, format);\n\n      // Update the specific variable or create it if it doesn't exist\n      const varsPath = 'request.vars.req';\n      const variables = _.get(parsedData, varsPath, []);\n      const updatedVariables = updateOrCreateVariable(variables, variable);\n\n      _.set(parsedData, varsPath, updatedVariables);\n\n      const content = await stringifyByType(parsedData, scopeType, collectionRoot, format);\n      await writeFile(pathname, content);\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  // create environment\n  ipcMain.handle('renderer:create-environment', async (event, collectionPathname, name, variables, color) => {\n    try {\n      const envDirPath = path.join(collectionPathname, 'environments');\n      if (!fs.existsSync(envDirPath)) {\n        await createDirectory(envDirPath);\n      }\n\n      const format = getCollectionFormat(collectionPathname);\n\n      // Get existing environment files to generate unique name\n      const existingFiles = fs.existsSync(envDirPath) ? fs.readdirSync(envDirPath) : [];\n      const existingEnvNames = existingFiles\n        .filter((file) => file.endsWith(`.${format}`))\n        .map((file) => path.basename(file, `.${format}`));\n\n      // Generate unique name based on existing environment files\n      const sanitizedName = sanitizeName(name);\n      const uniqueName = generateUniqueName(sanitizedName, (name) => existingEnvNames.includes(name));\n\n      const envFilePath = path.join(envDirPath, `${uniqueName}.${format}`);\n\n      const environment = {\n        name: uniqueName,\n        variables: variables || [],\n        color\n      };\n\n      if (envHasSecrets(environment)) {\n        environmentSecretsStore.storeEnvSecrets(collectionPathname, environment);\n      }\n\n      const content = await stringifyEnvironment(environment, { format });\n\n      await writeFile(envFilePath, content);\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  // save environment\n  ipcMain.handle('renderer:save-environment', async (event, collectionPathname, environment) => {\n    try {\n      const envDirPath = path.join(collectionPathname, 'environments');\n      if (!fs.existsSync(envDirPath)) {\n        await createDirectory(envDirPath);\n      }\n\n      const format = getCollectionFormat(collectionPathname);\n      // Determine filetype from collection\n      const envFilePath = path.join(envDirPath, `${environment.name}.${format}`);\n\n      if (!fs.existsSync(envFilePath)) {\n        throw new Error(`environment: ${envFilePath} does not exist`);\n      }\n\n      if (envHasSecrets(environment)) {\n        environmentSecretsStore.storeEnvSecrets(collectionPathname, environment);\n      }\n\n      const content = await stringifyEnvironment(environment, { format });\n      await writeFile(envFilePath, content);\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  // rename environment\n  ipcMain.handle('renderer:rename-environment', async (event, collectionPathname, environmentName, newName) => {\n    try {\n      const format = getCollectionFormat(collectionPathname);\n      const envDirPath = path.join(collectionPathname, 'environments');\n      const envFilePath = path.join(envDirPath, `${environmentName}.${format}`);\n\n      if (!fs.existsSync(envFilePath)) {\n        throw new Error(`environment: ${envFilePath} does not exist`);\n      }\n\n      const newEnvFilePath = path.join(envDirPath, `${newName}.${format}`);\n      if (!safeToRename(envFilePath, newEnvFilePath)) {\n        throw new Error(`environment: ${newEnvFilePath} already exists`);\n      }\n\n      moveRequestUid(envFilePath, newEnvFilePath);\n      fs.renameSync(envFilePath, newEnvFilePath);\n\n      environmentSecretsStore.renameEnvironment(collectionPathname, environmentName, newName);\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  // delete environment\n  ipcMain.handle('renderer:delete-environment', async (event, collectionPathname, environmentName) => {\n    try {\n      const format = getCollectionFormat(collectionPathname);\n      const envDirPath = path.join(collectionPathname, 'environments');\n      const envFilePath = path.join(envDirPath, `${environmentName}.${format}`);\n      if (!fs.existsSync(envFilePath)) {\n        throw new Error(`environment: ${envFilePath} does not exist`);\n      }\n\n      fs.unlinkSync(envFilePath);\n\n      environmentSecretsStore.deleteEnvironment(collectionPathname, environmentName);\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  // Save .env file variables for collection\n  ipcMain.handle('renderer:save-dotenv-variables', async (event, collectionPathname, variables, filename = '.env') => {\n    try {\n      if (!isValidDotEnvFilename(filename)) {\n        throw new Error('Invalid .env filename');\n      }\n\n      const dotEnvPath = path.join(collectionPathname, filename);\n\n      // Convert variables array to .env format\n      const content = variables\n        .filter((v) => v.name && v.name.trim() !== '')\n        .map((v) => {\n          const value = v.value || '';\n          // If value contains newlines or special characters, wrap in quotes\n          if (value.includes('\\n') || value.includes('\"') || value.includes('\\'') || value.includes('\\\\')) {\n            // Escape backslashes first, then double quotes\n            const escapedValue = value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n            return `${v.name}=\"${escapedValue}\"`;\n          }\n          return `${v.name}=${value}`;\n        })\n        .join('\\n');\n\n      await writeFile(dotEnvPath, content);\n\n      return { success: true };\n    } catch (error) {\n      console.error('Error saving .env file:', error);\n      return Promise.reject(error);\n    }\n  });\n\n  // Save .env file raw content for collection\n  ipcMain.handle('renderer:save-dotenv-raw', async (event, collectionPathname, content, filename = '.env') => {\n    try {\n      if (!isValidDotEnvFilename(filename)) {\n        throw new Error('Invalid .env filename');\n      }\n\n      const dotEnvPath = path.join(collectionPathname, filename);\n      await writeFile(dotEnvPath, content);\n      return { success: true };\n    } catch (error) {\n      console.error('Error saving .env file:', error);\n      return Promise.reject(error);\n    }\n  });\n\n  // Create .env file for collection\n  ipcMain.handle('renderer:create-dotenv-file', async (event, collectionPathname, filename = '.env') => {\n    try {\n      if (!isValidDotEnvFilename(filename)) {\n        throw new Error('Invalid .env filename');\n      }\n\n      const dotEnvPath = path.join(collectionPathname, filename);\n\n      if (fs.existsSync(dotEnvPath)) {\n        throw new Error(`${filename} file already exists`);\n      }\n\n      await writeFile(dotEnvPath, '');\n\n      return { success: true, filename };\n    } catch (error) {\n      console.error('Error creating .env file:', error);\n      return Promise.reject(error);\n    }\n  });\n\n  // Delete .env file for collection\n  ipcMain.handle('renderer:delete-dotenv-file', async (event, collectionPathname, filename = '.env') => {\n    try {\n      if (!isValidDotEnvFilename(filename)) {\n        throw new Error('Invalid .env filename');\n      }\n\n      const dotEnvPath = path.join(collectionPathname, filename);\n\n      if (!fs.existsSync(dotEnvPath)) {\n        throw new Error(`${filename} file does not exist`);\n      }\n\n      fs.unlinkSync(dotEnvPath);\n\n      return { success: true };\n    } catch (error) {\n      console.error('Error deleting .env file:', error);\n      return Promise.reject(error);\n    }\n  });\n\n  // update environment color\n  ipcMain.handle('renderer:update-environment-color', async (event, collectionPathname, environmentName, color) => {\n    try {\n      const format = getCollectionFormat(collectionPathname);\n      const envDirPath = path.join(collectionPathname, 'environments');\n      const envFilePath = path.join(envDirPath, `${environmentName}.${format}`);\n\n      if (!fs.existsSync(envFilePath)) {\n        throw new Error(`environment: ${envFilePath} does not exist`);\n      }\n\n      // Read, update color, and write back to file\n      const fileContent = fs.readFileSync(envFilePath, 'utf8');\n      const environment = parseEnvironment(fileContent, { format });\n      environment.color = color;\n      const updatedContent = stringifyEnvironment(environment, { format });\n      fs.writeFileSync(envFilePath, updatedContent, 'utf8');\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  // Generic environment export handler\n  ipcMain.handle('renderer:export-environment', async (event, { environments, environmentType, filePath, exportFormat = 'folder' }) => {\n    try {\n      const { app } = require('electron');\n      const appVersion = app?.getVersion() || '2.0.0';\n\n      // For single environments and folder exports, include info in each environment\n      const environmentWithInfo = (environment) => ({\n        name: environment.name,\n        variables: environment.variables,\n        color: environment.color ?? undefined,\n        info: {\n          type: 'bruno-environment',\n          exportedAt: new Date().toISOString(),\n          exportedUsing: `Bruno/v${appVersion}`\n        }\n      });\n\n      if (exportFormat === 'folder') {\n        // separate environment json files in folder\n        const baseFolderName = `bruno-${environmentType}-environments`;\n        const uniqueFolderName = generateUniqueName(baseFolderName, (name) => fs.existsSync(path.join(filePath, name)));\n        const exportPath = path.join(filePath, uniqueFolderName);\n\n        fs.mkdirSync(exportPath, { recursive: true });\n\n        for (const environment of environments) {\n          const baseFileName = environment.name ? `${environment.name.replace(/[^a-zA-Z0-9-_]/g, '_')}` : 'environment';\n          const uniqueFileName = generateUniqueName(baseFileName, (name) => fs.existsSync(path.join(exportPath, `${name}.json`)));\n          const fullPath = path.join(exportPath, `${uniqueFileName}.json`);\n\n          const cleanEnv = environmentWithInfo(environment);\n          const jsonContent = JSON.stringify(cleanEnv, null, 2);\n          await fs.promises.writeFile(fullPath, jsonContent, 'utf8');\n        }\n      } else if (exportFormat === 'single-file') {\n        // all environments in a single file with top-level info and environments array\n        const baseFileName = `bruno-${environmentType}-environments`;\n        const uniqueFileName = generateUniqueName(baseFileName, (name) => fs.existsSync(path.join(filePath, `${name}.json`)));\n        const fullPath = path.join(filePath, `${uniqueFileName}.json`);\n\n        const exportData = {\n          info: {\n            type: 'bruno-environment',\n            exportedAt: new Date().toISOString(),\n            exportedUsing: `Bruno/v${appVersion}`\n          },\n          environments\n        };\n\n        const jsonContent = JSON.stringify(exportData, null, 2);\n        await fs.promises.writeFile(fullPath, jsonContent, 'utf8');\n      } else if (exportFormat === 'single-object') {\n        // single environment json file\n        if (environments.length !== 1) {\n          throw new Error('Single object export requires exactly one environment');\n        }\n\n        const environment = environments[0];\n        const baseFileName = environment.name ? `${environment.name.replace(/[^a-zA-Z0-9-_]/g, '_')}` : 'environment';\n        const uniqueFileName = generateUniqueName(baseFileName, (name) => fs.existsSync(path.join(filePath, `${name}.json`)));\n        const fullPath = path.join(filePath, `${uniqueFileName}.json`);\n        const jsonContent = JSON.stringify(environmentWithInfo(environment), null, 2);\n        await fs.promises.writeFile(fullPath, jsonContent, 'utf8');\n      } else {\n        throw new Error(`Unsupported export format: ${exportFormat}`);\n      }\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  // rename item\n  ipcMain.handle('renderer:rename-item-name', async (event, { itemPath, newName, collectionPathname }) => {\n    try {\n      if (!fs.existsSync(itemPath)) {\n        throw new Error(`path: ${itemPath} does not exist`);\n      }\n\n      if (isDirectory(itemPath)) {\n        const format = getCollectionFormat(collectionPathname);\n        const folderFilePath = path.join(itemPath, `folder.${format}`);\n        let folderFileJsonContent;\n        if (fs.existsSync(folderFilePath)) {\n          const oldFolderFileContent = await fs.promises.readFile(folderFilePath, 'utf8');\n          folderFileJsonContent = await parseFolder(oldFolderFileContent, { format });\n          folderFileJsonContent.meta.name = newName;\n        } else {\n          folderFileJsonContent = {\n            meta: {\n              name: newName\n            }\n          };\n        }\n\n        const folderFileContent = await stringifyFolder(folderFileJsonContent, { format });\n        await writeFile(folderFilePath, folderFileContent);\n\n        return;\n      }\n\n      const format = getCollectionFormat(collectionPathname);\n      if (!hasRequestExtension(itemPath, format)) {\n        throw new Error(`path: ${itemPath} is not a valid request file`);\n      }\n\n      const data = fs.readFileSync(itemPath, 'utf8');\n      const jsonData = parseRequest(data, { format });\n      jsonData.name = newName;\n      const content = stringifyRequest(jsonData, { format });\n      await writeFile(itemPath, content);\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  // rename item\n  ipcMain.handle('renderer:rename-item-filename', async (event, { oldPath, newPath, newName, newFilename, collectionPathname }) => {\n    const tempDir = path.join(os.tmpdir(), `temp-folder-${Date.now()}`);\n    const isWindowsOSAndNotWSLPathAndItemHasSubDirectories = isDirectory(oldPath) && isWindowsOS() && !isWSLPath(oldPath) && hasSubDirectories(oldPath);\n    try {\n      // Check if the old path exists\n      if (!fs.existsSync(oldPath)) {\n        throw new Error(`path: ${oldPath} does not exist`);\n      }\n\n      if (!safeToRename(oldPath, newPath)) {\n        throw new Error(`path: ${newPath} already exists`);\n      }\n\n      const format = getCollectionFormat(collectionPathname);\n\n      if (isDirectory(oldPath)) {\n        const folderFilePath = path.join(oldPath, `folder.${format}`);\n        let folderFileJsonContent;\n        if (fs.existsSync(folderFilePath)) {\n          const oldFolderFileContent = await fs.promises.readFile(folderFilePath, 'utf8');\n          folderFileJsonContent = await parseFolder(oldFolderFileContent, { format });\n          folderFileJsonContent.meta.name = newName;\n        } else {\n          folderFileJsonContent = {\n            meta: {\n              name: newName\n            }\n          };\n        }\n\n        const folderFileContent = await stringifyFolder(folderFileJsonContent, { format });\n        await writeFile(folderFilePath, folderFileContent);\n\n        const requestFilesAtSource = await searchForRequestFiles(oldPath, collectionPathname);\n\n        for (let requestFile of requestFilesAtSource) {\n          const newRequestFilePath = requestFile.replace(oldPath, newPath);\n          moveRequestUid(requestFile, newRequestFilePath);\n        }\n\n        /**\n         * If it is windows OS\n         * And it is not a WSL path (meaning it is not running in WSL (linux pathtype))\n         * And it has sub directories\n         * Only then we need to use the temp dir approach to rename the folder\n         *\n         * Windows OS would sometimes throw error when renaming a folder with sub directories\n         * This is an alternative approach to avoid that error\n         */\n        if (isWindowsOSAndNotWSLPathAndItemHasSubDirectories) {\n          await fsExtra.copy(oldPath, tempDir);\n          await fsExtra.remove(oldPath);\n          await fsExtra.move(tempDir, newPath, { overwrite: true });\n          await fsExtra.remove(tempDir);\n        } else {\n          await fs.renameSync(oldPath, newPath);\n        }\n\n        return newPath;\n      }\n\n      if (!hasRequestExtension(oldPath, format)) {\n        throw new Error(`path: ${oldPath} is not a valid request file`);\n      }\n\n      if (!validateName(newFilename)) {\n        throw new Error(`path: ${newFilename} is not a valid filename`);\n      }\n\n      // update name in file and save new copy, then delete old copy\n      const data = await fs.promises.readFile(oldPath, 'utf8'); // Use async read\n      const jsonData = parseRequest(data, { format });\n      jsonData.name = newName;\n      moveRequestUid(oldPath, newPath);\n\n      const content = stringifyRequest(jsonData, { format });\n      await fs.promises.unlink(oldPath);\n      await writeFile(newPath, content);\n\n      return newPath;\n    } catch (error) {\n      // in case the rename file operations fails, and we see that the temp dir exists\n      // and the old path does not exist, we need to restore the data from the temp dir to the old path\n      if (isWindowsOSAndNotWSLPathAndItemHasSubDirectories) {\n        if (fsExtra.pathExistsSync(tempDir) && !fsExtra.pathExistsSync(oldPath)) {\n          try {\n            await fsExtra.copy(tempDir, oldPath);\n            await fsExtra.remove(tempDir);\n          } catch (err) {\n            console.error('Failed to restore data to the old path:', err);\n          }\n        }\n      }\n\n      return Promise.reject(error);\n    }\n  });\n\n  // new folder\n  ipcMain.handle('renderer:new-folder', async (event, { pathname, folderData, format }) => {\n    const resolvedFolderName = sanitizeName(path.basename(pathname));\n    pathname = path.join(path.dirname(pathname), resolvedFolderName);\n    try {\n      if (!fs.existsSync(pathname)) {\n        fs.mkdirSync(pathname);\n        const folderFilePath = path.join(pathname, `folder.${format}`);\n        const content = await stringifyFolder(folderData, { format });\n        await writeFile(folderFilePath, content);\n      } else {\n        return Promise.reject(new Error('The directory already exists'));\n      }\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  // delete file/folder\n  ipcMain.handle('renderer:delete-item', async (event, pathname, type, collectionPathname) => {\n    try {\n      if (type === 'folder') {\n        if (!fs.existsSync(pathname)) {\n          return Promise.reject(new Error('The directory does not exist'));\n        }\n\n        // delete the request uid mappings\n        const requestFilesAtSource = await searchForRequestFiles(pathname, collectionPathname);\n        for (let requestFile of requestFilesAtSource) {\n          deleteRequestUid(requestFile);\n        }\n\n        fs.rmSync(pathname, { recursive: true, force: true });\n      } else if (['http-request', 'graphql-request', 'grpc-request', 'ws-request'].includes(type)) {\n        if (!fs.existsSync(pathname)) {\n          return Promise.reject(new Error('The file does not exist'));\n        }\n\n        deleteRequestUid(pathname);\n\n        fs.unlinkSync(pathname);\n      } else {\n        return Promise.reject();\n      }\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  // Delete transient request files by their absolute paths\n  // This is a simpler handler specifically for cleaning up transient requests\n  // tempDirectory: the collection's temp directory path to validate files belong to this collection\n  ipcMain.handle('renderer:delete-transient-requests', async (event, filePaths, tempDirectory) => {\n    const brunoTempPrefix = getTransientCollectionPrefix();\n    const results = { deleted: [], skipped: [], errors: [] };\n\n    // Validate tempDirectory is within Bruno transient directory\n    const normalizedTempDir = tempDirectory ? path.normalize(tempDirectory) : null;\n    if (!normalizedTempDir || !normalizedTempDir.startsWith(brunoTempPrefix)) {\n      return { deleted: [], skipped: filePaths.map((p) => ({ path: p, reason: 'Invalid temp directory' })), errors: [] };\n    }\n\n    for (const filePath of filePaths) {\n      try {\n        // Safety check: only delete files within the collection's temp directory\n        const normalizedPath = path.normalize(filePath);\n        if (!normalizedPath.startsWith(normalizedTempDir + path.sep) && normalizedPath !== normalizedTempDir) {\n          results.skipped.push({ path: filePath, reason: 'Not in collection temp directory' });\n          continue;\n        }\n\n        // Check if file exists before trying to delete\n        if (!fs.existsSync(filePath)) {\n          results.skipped.push({ path: filePath, reason: 'File does not exist' });\n          continue;\n        }\n\n        // Delete the file and its UID mapping\n        deleteRequestUid(filePath);\n        fs.unlinkSync(filePath);\n        results.deleted.push(filePath);\n      } catch (error) {\n        results.errors.push({ path: filePath, error: error.message });\n      }\n    }\n\n    return results;\n  });\n\n  ipcMain.handle('renderer:open-collection', async () => {\n    if (watcher && mainWindow) {\n      await openCollectionDialog(mainWindow, watcher);\n    }\n  });\n\n  ipcMain.handle('renderer:open-multiple-collections', async (e, collectionPaths, options = {}) => {\n    if (watcher && mainWindow) {\n      await openCollectionsByPathname(mainWindow, watcher, collectionPaths);\n      if (options.workspacePath) {\n        const { setCollectionWorkspace } = require('../store/process-env');\n        const { generateUidBasedOnHash } = require('../utils/common');\n        for (const collectionPath of collectionPaths) {\n          const collectionUid = generateUidBasedOnHash(collectionPath);\n          setCollectionWorkspace(collectionUid, options.workspacePath);\n        }\n      }\n    }\n  });\n\n  ipcMain.handle('renderer:set-collection-workspace', (event, collectionUid, workspacePath) => {\n    if (workspacePath) {\n      const { setCollectionWorkspace } = require('../store/process-env');\n      setCollectionWorkspace(collectionUid, workspacePath);\n    }\n  });\n\n  ipcMain.handle('renderer:remove-collection', async (event, collectionPath, collectionUid, workspacePath) => {\n    if (watcher && mainWindow) {\n      watcher.removeWatcher(collectionPath, mainWindow, collectionUid);\n\n      if (wsClient) {\n        wsClient.closeForCollection(collectionUid);\n      }\n    }\n\n    // Clean up\n    const { clearCollectionWorkspace } = require('../store/process-env');\n    clearCollectionWorkspace(collectionUid);\n\n    if (workspacePath && workspacePath !== 'default') {\n      try {\n        const { removeCollectionFromWorkspace } = require('../utils/workspace-config');\n        await removeCollectionFromWorkspace(workspacePath, collectionPath);\n      } catch (error) {\n        console.error('Error removing collection from workspace.yml:', error);\n      }\n    }\n\n    // Clean up AppData spec files for this collection\n    try {\n      cleanupSpecFilesForCollection(collectionPath);\n    } catch (error) {\n      console.error('Error cleaning up spec files for removed collection:', error);\n    }\n  });\n\n  ipcMain.handle('renderer:import-collection', async (_, collection, collectionLocation, options = {}) => {\n    const format = options.format || DEFAULT_COLLECTION_FORMAT;\n    const rawOpenAPISpec = options.rawOpenAPISpec;\n    let collections = Array.isArray(collection) ? collection : [collection];\n    let completedImports = 0;\n    let failedImports = 0;\n    let successfulImports = [];\n\n    for (let coll of collections) {\n      try {\n        // Sending a \"started\" and \"ended\" event to renderer to start and stop the spinner.\n        mainWindow.webContents.send('main:collection-import-started', coll.uid);\n\n        let collectionName = sanitizeName(coll.name);\n        let collectionPath = path.join(collectionLocation, collectionName);\n\n        // Auto-rename if collection already exists\n        if (fs.existsSync(collectionPath)) {\n          const uniqueName = await findUniqueFolderName(coll.name, collectionLocation);\n          collectionName = sanitizeName(uniqueName);\n          collectionPath = path.join(collectionLocation, collectionName);\n          coll.name = uniqueName;\n        }\n\n        const getFilenameWithFormat = (item, format) => {\n          if (item?.filename) {\n            const ext = path.extname(item.filename);\n            if (ext === '.bru' || ext === '.yml') {\n              return item.filename.replace(ext, `.${format}`);\n            }\n            return item.filename;\n          }\n          return `${item.name}.${format}`;\n        };\n\n        // Recursive function to parse the collection items and create files/folders\n        const parseCollectionItems = async (items = [], currentPath) => {\n          await Promise.all(items.map(async (item) => {\n            if (['http-request', 'graphql-request', 'grpc-request', 'ws-request'].includes(item.type)) {\n              let sanitizedFilename = sanitizeName(getFilenameWithFormat(item, format));\n              const content = await stringifyRequestViaWorker(item, { format });\n              const filePath = path.join(currentPath, sanitizedFilename);\n              safeWriteFileSync(filePath, content);\n            }\n            if (item.type === 'folder') {\n              let sanitizedFolderName = sanitizeName(item?.filename || item?.name);\n              const folderPath = path.join(currentPath, sanitizedFolderName);\n              fs.mkdirSync(folderPath);\n\n              if (item?.root?.meta?.name) {\n                const folderFilePath = path.join(folderPath, `folder.${format}`);\n                item.root.meta.seq = item.seq;\n                const folderContent = await stringifyFolder(item.root, { format });\n                safeWriteFileSync(folderFilePath, folderContent);\n              }\n\n              if (item.items && item.items.length) {\n                await parseCollectionItems(item.items, folderPath);\n              }\n            }\n            // Handle items of type 'js'\n            if (item.type === 'js') {\n              let sanitizedFilename = sanitizeName(item?.filename || `${item.name}.js`);\n              const filePath = path.join(currentPath, sanitizedFilename);\n              safeWriteFileSync(filePath, item.fileContent);\n            }\n          }));\n        };\n\n        const parseEnvironments = async (environments = [], collectionPath) => {\n          const envDirPath = path.join(collectionPath, 'environments');\n          if (!fs.existsSync(envDirPath)) {\n            fs.mkdirSync(envDirPath);\n          }\n\n          await Promise.all(environments.map(async (env) => {\n            const content = await stringifyEnvironment(env, { format });\n            let sanitizedEnvFilename = sanitizeName(`${env.name}.${format}`);\n            const filePath = path.join(envDirPath, sanitizedEnvFilename);\n            safeWriteFileSync(filePath, content);\n          }));\n        };\n\n        const getBrunoJsonConfig = (collection) => {\n          let brunoConfig = collection.brunoConfig;\n\n          if (!brunoConfig) {\n            brunoConfig = {\n              version: '1',\n              name: collection.name,\n              type: 'collection',\n              ignore: ['node_modules', '.git']\n            };\n          }\n\n          return brunoConfig;\n        };\n\n        await createDirectory(collectionPath);\n\n        const uid = generateUidBasedOnHash(collectionPath);\n        const brunoConfig = getBrunoJsonConfig(coll);\n\n        // Convert absolute local file paths to collection-relative (git-shareable)\n        if (Array.isArray(brunoConfig.openapi)) {\n          for (const entry of brunoConfig.openapi) {\n            if (entry.sourceUrl && path.isAbsolute(entry.sourceUrl)) {\n              entry.sourceUrl = path.relative(collectionPath, entry.sourceUrl);\n            }\n          }\n        }\n\n        if (format === 'yml') {\n          brunoConfig.opencollection = '1.0.0';\n          const collectionContent = await stringifyCollection(coll.root, brunoConfig, { format });\n          await writeFile(path.join(collectionPath, 'opencollection.yml'), collectionContent);\n        } else if (format === 'bru') {\n          const stringifiedBrunoConfig = await stringifyJson(brunoConfig);\n          await writeFile(path.join(collectionPath, 'bruno.json'), stringifiedBrunoConfig);\n\n          const collectionContent = await stringifyCollection(coll.root, brunoConfig, { format });\n          await writeFile(path.join(collectionPath, 'collection.bru'), collectionContent);\n        } else {\n          throw new Error(`Invalid format: ${format}`);\n        }\n\n        // create folder and files based on collection\n        await parseCollectionItems(coll.items, collectionPath);\n        await parseEnvironments(coll.environments, collectionPath);\n\n        // Save OpenAPI spec file for sync support\n        if (rawOpenAPISpec && brunoConfig.openapi?.length) {\n          const specContent = typeof rawOpenAPISpec === 'string'\n            ? rawOpenAPISpec\n            : JSON.stringify(rawOpenAPISpec, null, 2);\n          await saveSpecAndUpdateMetadata({ collectionPath, specContent });\n        }\n\n        const { size, filesCount } = await getCollectionStats(collectionPath);\n        brunoConfig.size = size;\n        brunoConfig.filesCount = filesCount;\n\n        mainWindow.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig);\n        ipcMain.emit('main:collection-opened', mainWindow, collectionPath, uid, brunoConfig);\n\n        mainWindow.webContents.send('main:collection-import-ended', coll.uid);\n\n        successfulImports.push({\n          path: collectionPath,\n          name: coll.name\n        });\n        // Increment completed imports\n        completedImports++;\n      } catch (error) {\n        mainWindow.webContents.send('main:collection-import-failed', coll.uid, {\n          message: `Error ${error.message}`\n        });\n        console.error(`Failed to import collection: ${coll.name}, Error: ${error.message}`);\n\n        // Increment failed imports\n        failedImports++;\n\n        // Continue with next collection instead of breaking\n        continue;\n      }\n    }\n\n    // Send final status when all collections have been processed (either succeeded or failed)\n    if ((completedImports + failedImports) === collections.length) {\n      mainWindow.webContents.send('main:all-collections-import-ended', {\n        message: `Import completed. ${completedImports} collections imported successfully, ${failedImports} failed.`,\n        status: {\n          total: collections.length,\n          succeeded: completedImports,\n          failed: failedImports\n        }\n      });\n    }\n\n    return {\n      success: {\n        count: completedImports,\n        items: successfulImports\n      }\n    };\n  });\n\n  ipcMain.handle('renderer:clone-folder', async (event, itemFolder, collectionPath, collectionPathname) => {\n    try {\n      if (fs.existsSync(collectionPath)) {\n        throw new Error(`folder: ${collectionPath} already exists`);\n      }\n\n      const format = getCollectionFormat(collectionPathname);\n\n      // Recursive function to parse the folder and create files/folders\n      const parseCollectionItems = (items = [], currentPath) => {\n        items.forEach(async (item) => {\n          if (['http-request', 'graphql-request', 'grpc-request'].includes(item.type)) {\n            const content = await stringifyRequestViaWorker(item, { format });\n\n            // Use the correct file extension based on target format\n            const baseName = path.parse(item.filename).name;\n            const newFilename = format === 'yml' ? `${baseName}.yml` : `${baseName}.bru`;\n            const filePath = path.join(currentPath, newFilename);\n\n            safeWriteFileSync(filePath, content);\n          }\n          if (item.type === 'folder') {\n            const folderPath = path.join(currentPath, item.filename);\n            fs.mkdirSync(folderPath);\n\n            // If folder has a root element, then I should write its folder file\n            if (item.root) {\n              const folderContent = await stringifyFolder(item.root, { format });\n              folderContent.name = item.name;\n              if (folderContent) {\n                const folderFilePath = path.join(folderPath, `folder.${format}`);\n                safeWriteFileSync(folderFilePath, folderContent);\n              }\n            }\n\n            if (item.items && item.items.length) {\n              parseCollectionItems(item.items, folderPath);\n            }\n          }\n        });\n      };\n\n      await createDirectory(collectionPath);\n\n      // If initial folder has a root element, then I should write its folder file\n      if (itemFolder.root) {\n        const folderContent = await stringifyFolder(itemFolder.root, { format });\n        if (folderContent) {\n          const folderFilePath = path.join(collectionPath, `folder.${format}`);\n          safeWriteFileSync(folderFilePath, folderContent);\n        }\n      }\n\n      // create folder and files based on another folder\n      await parseCollectionItems(itemFolder.items, collectionPath);\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:resequence-items', async (event, itemsToResequence, collectionPathname) => {\n    try {\n      const format = getCollectionFormat(collectionPathname);\n\n      for (let item of itemsToResequence) {\n        if (item?.type === 'folder') {\n          const folderRootPath = path.join(item.pathname, `folder.${format}`);\n          let folderJsonData = {\n            meta: {\n              name: path.basename(item.pathname),\n              seq: item.seq\n            }\n          };\n          if (fs.existsSync(folderRootPath)) {\n            const folderContent = fs.readFileSync(folderRootPath, 'utf8');\n            folderJsonData = await parseFolder(folderContent, { format });\n            if (!folderJsonData?.meta) {\n              folderJsonData.meta = {\n                name: path.basename(item.pathname),\n                seq: item.seq\n              };\n            }\n            if (folderJsonData?.meta?.seq === item.seq) {\n              continue;\n            }\n            folderJsonData.meta.seq = item.seq;\n          }\n          const content = await stringifyFolder(folderJsonData, { format });\n          await writeFile(folderRootPath, content);\n        } else if (REQUEST_TYPES.includes(item?.type)) {\n          if (fs.existsSync(item.pathname)) {\n            const itemToSave = transformRequestToSaveToFilesystem(item);\n            const content = await stringifyRequestViaWorker(itemToSave, { format });\n            await writeFile(item.pathname, content);\n          }\n        }\n      }\n      return true;\n    } catch (error) {\n      console.error('Error in resequence-items:', error);\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:move-file-item', async (event, itemPath, destinationPath) => {\n    try {\n      const itemContent = fs.readFileSync(itemPath, 'utf8');\n      const newItemPath = path.join(destinationPath, path.basename(itemPath));\n\n      moveRequestUid(itemPath, newItemPath);\n\n      fs.unlinkSync(itemPath);\n      safeWriteFileSync(newItemPath, itemContent);\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:move-item', async (event, { targetDirname, sourcePathname }) => {\n    try {\n      if (fs.existsSync(targetDirname)) {\n        const sourceDirname = path.dirname(sourcePathname);\n        const pathnamesBefore = await getPaths(sourcePathname);\n        const pathnamesAfter = pathnamesBefore?.map((p) => p?.replace(sourceDirname, targetDirname));\n        await copyPath(sourcePathname, targetDirname);\n        await removePath(sourcePathname);\n        // move the request uids of the previous file/folders to the new file/folder items\n        pathnamesAfter?.forEach((_, index) => {\n          moveRequestUid(pathnamesBefore[index], pathnamesAfter[index]);\n        });\n      }\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:move-folder-item', async (event, folderPath, destinationPath) => {\n    try {\n      const folderName = path.basename(folderPath);\n      const newFolderPath = path.join(destinationPath, folderName);\n\n      if (!fs.existsSync(folderPath)) {\n        throw new Error(`folder: ${folderPath} does not exist`);\n      }\n\n      if (fs.existsSync(newFolderPath)) {\n        throw new Error(`folder: ${newFolderPath} already exists`);\n      }\n\n      const requestFilesAtSource = await searchForRequestFiles(folderPath);\n\n      for (let requestFile of requestFilesAtSource) {\n        const newRequestFilePath = requestFile.replace(folderPath, newFolderPath);\n        moveRequestUid(requestFile, newRequestFilePath);\n      }\n\n      fs.renameSync(folderPath, newFolderPath);\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:update-bruno-config', async (event, brunoConfig, collectionPath, collectionRoot) => {\n    try {\n      const transformedBrunoConfig = transformBrunoConfigBeforeSave(brunoConfig);\n      const format = getCollectionFormat(collectionPath);\n\n      if (format === 'bru') {\n        const brunoConfigPath = path.join(collectionPath, 'bruno.json');\n        const content = await stringifyJson(transformedBrunoConfig);\n        await writeFile(brunoConfigPath, content);\n      } else if (format === 'yml') {\n        const content = await stringifyCollection(collectionRoot, transformedBrunoConfig, { format });\n        await writeFile(path.join(collectionPath, 'opencollection.yml'), content);\n      } else {\n        throw new Error(`Invalid collection format: ${format}`);\n      }\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:open-devtools', async () => {\n    mainWindow.webContents.openDevTools();\n  });\n\n  ipcMain.handle('renderer:load-gql-schema-file', async () => {\n    try {\n      const { filePaths } = await dialog.showOpenDialog(mainWindow, {\n        properties: ['openFile']\n      });\n      if (filePaths.length === 0) {\n        return;\n      }\n\n      const jsonData = fs.readFileSync(filePaths[0], 'utf8');\n      return safeParseJSON(jsonData);\n    } catch (err) {\n      return Promise.reject(new Error('Failed to load GraphQL schema file'));\n    }\n  });\n\n  const updateCookiesAndNotify = async () => {\n    const domainsWithCookies = await getDomainsWithCookies();\n    mainWindow.webContents.send(\n      'main:cookies-update',\n      safeParseJSON(safeStringifyJSON(domainsWithCookies))\n    );\n    cookiesStore.saveCookieJar();\n  };\n\n  // Delete all cookies for a domain\n  ipcMain.handle('renderer:delete-cookies-for-domain', async (event, domain) => {\n    try {\n      await deleteCookiesForDomain(domain);\n      await updateCookiesAndNotify();\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:delete-cookie', async (event, domain, path, cookieKey) => {\n    try {\n      await deleteCookie(domain, path, cookieKey);\n      await updateCookiesAndNotify();\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  // add cookie\n  ipcMain.handle('renderer:add-cookie', async (event, domain, cookie) => {\n    try {\n      await addCookieForDomain(domain, cookie);\n      await updateCookiesAndNotify();\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  // modify cookie\n  ipcMain.handle('renderer:modify-cookie', async (event, domain, oldCookie, cookie) => {\n    try {\n      await modifyCookieForDomain(domain, oldCookie, cookie);\n      await updateCookiesAndNotify();\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:get-parsed-cookie', async (event, cookieStr) => {\n    try {\n      return parseCookieString(cookieStr);\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:create-cookie-string', async (event, cookie) => {\n    try {\n      return createCookieString(cookie);\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:save-collection-security-config', async (event, collectionPath, securityConfig) => {\n    try {\n      collectionSecurityStore.setSecurityConfigForCollection(collectionPath, {\n        jsSandboxMode: securityConfig.jsSandboxMode\n      });\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:get-collection-security-config', async (event, collectionPath) => {\n    try {\n      return collectionSecurityStore.getSecurityConfigForCollection(collectionPath);\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:update-ui-state-snapshot', (event, { type, data }) => {\n    try {\n      uiStateSnapshotStore.update({ type, data });\n    } catch (error) {\n      throw new Error(error.message);\n    }\n  });\n\n  ipcMain.handle('renderer:fetch-oauth2-credentials', async (event, { itemUid, request, collection }) => {\n    try {\n      if (request.oauth2) {\n        let requestCopy = _.cloneDeep(request);\n        const { uid: collectionUid, pathname: collectionPath, runtimeVariables, environments = [], activeEnvironmentUid } = collection;\n        const environment = _.find(environments, (e) => e.uid === activeEnvironmentUid);\n        const envVars = getEnvVars(environment);\n        const processEnvVars = getProcessEnvVars(collectionUid);\n        const partialItem = { uid: itemUid };\n        const requestTreePath = getTreePathFromCollectionToItem(collection, partialItem);\n        mergeVars(collection, requestCopy, requestTreePath);\n        const globalEnvironmentVariables = collection.globalEnvironmentVariables;\n        const promptVariables = collection.promptVariables;\n        interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);\n        const { oauth2: { grantType, accessTokenUrl, refreshTokenUrl }, collectionVariables, folderVariables, requestVariables } = requestCopy || {};\n\n        // For OAuth2 token requests, use accessTokenUrl for cert/proxy config instead of main request URL\n        let certsAndProxyConfigForTokenUrl = null;\n        let certsAndProxyConfigForRefreshUrl = null;\n\n        if (accessTokenUrl && grantType !== 'implicit') {\n          const interpolatedTokenUrl = interpolateString(accessTokenUrl, {\n            globalEnvironmentVariables,\n            collectionVariables,\n            envVars,\n            folderVariables,\n            requestVariables,\n            runtimeVariables,\n            processEnvVars,\n            promptVariables\n          });\n          let tokenRequestForConfig = { ...requestCopy, url: interpolatedTokenUrl };\n          certsAndProxyConfigForTokenUrl = await getCertsAndProxyConfig({\n            collectionUid,\n            collection,\n            request: tokenRequestForConfig,\n            envVars,\n            runtimeVariables,\n            processEnvVars,\n            collectionPath,\n            globalEnvironmentVariables\n          });\n        }\n\n        // For refresh token requests, use refreshTokenUrl if available, otherwise accessTokenUrl\n        const tokenUrlForRefresh = refreshTokenUrl || accessTokenUrl;\n        if (tokenUrlForRefresh && grantType !== 'implicit') {\n          const interpolatedRefreshUrl = interpolateString(tokenUrlForRefresh, {\n            globalEnvironmentVariables,\n            collectionVariables,\n            envVars,\n            folderVariables,\n            requestVariables,\n            runtimeVariables,\n            processEnvVars,\n            promptVariables\n          });\n          let refreshRequestForConfig = { ...requestCopy, url: interpolatedRefreshUrl };\n          certsAndProxyConfigForRefreshUrl = await getCertsAndProxyConfig({\n            collectionUid,\n            collection,\n            request: refreshRequestForConfig,\n            envVars,\n            runtimeVariables,\n            processEnvVars,\n            collectionPath,\n            globalEnvironmentVariables\n          });\n        }\n\n        const handleOAuth2Response = (response) => {\n          if (response.error && !response.debugInfo) {\n            throw new Error(response.error);\n          }\n          return response;\n        };\n\n        switch (grantType) {\n          case 'authorization_code':\n            interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);\n            return await getOAuth2TokenUsingAuthorizationCode({\n              request: requestCopy,\n              collectionUid,\n              forceFetch: true,\n              certsAndProxyConfigForTokenUrl,\n              certsAndProxyConfigForRefreshUrl\n            }).then(handleOAuth2Response);\n\n          case 'client_credentials':\n            interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);\n            return await getOAuth2TokenUsingClientCredentials({\n              request: requestCopy,\n              collectionUid,\n              forceFetch: true,\n              certsAndProxyConfigForTokenUrl,\n              certsAndProxyConfigForRefreshUrl\n            }).then(handleOAuth2Response);\n\n          case 'password':\n            interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);\n            return await getOAuth2TokenUsingPasswordCredentials({\n              request: requestCopy,\n              collectionUid,\n              forceFetch: true,\n              certsAndProxyConfigForTokenUrl,\n              certsAndProxyConfigForRefreshUrl\n            }).then(handleOAuth2Response);\n\n          case 'implicit':\n            interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);\n            return await getOAuth2TokenUsingImplicitGrant({\n              request: requestCopy,\n              collectionUid,\n              forceFetch: true\n            }).then(handleOAuth2Response);\n\n          default:\n            return {\n              error: `Unsupported grant type: ${grantType}`,\n              credentials: null,\n              url: null,\n              collectionUid,\n              credentialsId: null\n            };\n        }\n      }\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:refresh-oauth2-credentials', async (event, { itemUid, request, collection }) => {\n    try {\n      if (request.oauth2) {\n        let requestCopy = _.cloneDeep(request);\n        const { uid: collectionUid, pathname: collectionPath, runtimeVariables, environments = [], activeEnvironmentUid } = collection;\n        const environment = _.find(environments, (e) => e.uid === activeEnvironmentUid);\n        const envVars = getEnvVars(environment);\n        const processEnvVars = getProcessEnvVars(collectionUid);\n        const partialItem = { uid: itemUid };\n        const requestTreePath = getTreePathFromCollectionToItem(collection, partialItem);\n        mergeVars(collection, requestCopy, requestTreePath);\n        interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);\n        const globalEnvironmentVariables = collection.globalEnvironmentVariables;\n\n        const certsAndProxyConfig = await getCertsAndProxyConfig({\n          collectionUid,\n          collection,\n          request: requestCopy,\n          envVars,\n          runtimeVariables,\n          processEnvVars,\n          collectionPath,\n          globalEnvironmentVariables\n        });\n\n        let { credentials, url, credentialsId, debugInfo } = await refreshOauth2Token({ requestCopy, collectionUid, certsAndProxyConfig });\n        return { credentials, url, collectionUid, credentialsId, debugInfo };\n      }\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:cancel-oauth2-authorization-request', async () => {\n    try {\n      const cancelled = cancelOAuth2AuthorizationRequest();\n      return { success: true, cancelled };\n    } catch (err) {\n      return { success: false, error: err.message };\n    }\n  });\n\n  ipcMain.handle('renderer:is-oauth2-authorization-request-in-progress', () => {\n    return isOauth2AuthorizationRequestInProgress();\n  });\n\n  // todo: could be removed\n  ipcMain.handle('renderer:load-request-via-worker', async (event, { collectionUid, pathname }) => {\n    let fileStats;\n    try {\n      fileStats = fs.statSync(pathname);\n      if (hasBruExtension(pathname)) {\n        const file = {\n          meta: {\n            collectionUid,\n            pathname,\n            name: path.basename(pathname)\n          }\n        };\n        let bruContent = fs.readFileSync(pathname, 'utf8');\n        const metaJson = parseBruFileMeta(bruContent);\n        file.data = metaJson;\n        file.loading = true;\n        file.partial = true;\n        file.size = sizeInMB(fileStats?.size);\n        hydrateRequestWithUuid(file.data, pathname);\n        mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);\n        file.data = await parseRequestViaWorker(bruContent, { format: 'bru' });\n        file.partial = false;\n        file.loading = true;\n        file.size = sizeInMB(fileStats?.size);\n        hydrateRequestWithUuid(file.data, pathname);\n        mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);\n      }\n    } catch (error) {\n      if (hasBruExtension(pathname)) {\n        const file = {\n          meta: {\n            collectionUid,\n            pathname,\n            name: path.basename(pathname)\n          }\n        };\n        let bruContent = fs.readFileSync(pathname, 'utf8');\n        const metaJson = parseBruFileMeta(bruContent);\n        file.data = metaJson;\n        file.partial = true;\n        file.loading = false;\n        file.size = sizeInMB(fileStats?.size);\n        hydrateRequestWithUuid(file.data, pathname);\n        mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);\n      }\n      return Promise.reject(error);\n    }\n  });\n\n  // todo: could be removed\n  ipcMain.handle('renderer:load-request', async (event, { collectionUid, pathname }) => {\n    let fileStats;\n    try {\n      fileStats = fs.statSync(pathname);\n      if (hasRequestExtension(pathname)) {\n        const file = {\n          meta: {\n            collectionUid,\n            pathname,\n            name: path.basename(pathname)\n          }\n        };\n        let bruContent = fs.readFileSync(pathname, 'utf8');\n        const metaJson = parseBruFileMeta(bruContent);\n        file.data = metaJson;\n        file.loading = true;\n        file.partial = true;\n        file.size = sizeInMB(fileStats?.size);\n        hydrateRequestWithUuid(file.data, pathname);\n        mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);\n        file.data = parseRequest(bruContent);\n        file.partial = false;\n        file.loading = true;\n        file.size = sizeInMB(fileStats?.size);\n        hydrateRequestWithUuid(file.data, pathname);\n        mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);\n      }\n    } catch (error) {\n      if (hasRequestExtension(pathname)) {\n        const file = {\n          meta: {\n            collectionUid,\n            pathname,\n            name: path.basename(pathname)\n          }\n        };\n        let bruContent = fs.readFileSync(pathname, 'utf8');\n        const metaJson = parseBruFileMeta(bruContent);\n        file.data = metaJson;\n        file.partial = true;\n        file.loading = false;\n        file.size = sizeInMB(fileStats?.size);\n        hydrateRequestWithUuid(file.data, pathname);\n        mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);\n      }\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:load-large-request', async (event, { collectionUid, pathname }) => {\n    let fileStats;\n    if (!hasBruExtension(pathname)) {\n      return;\n    }\n\n    const file = {\n      meta: {\n        collectionUid,\n        pathname,\n        name: path.basename(pathname)\n      }\n    };\n\n    try {\n      fileStats = fs.statSync(pathname);\n\n      const bruContent = fs.readFileSync(pathname, 'utf8');\n      const metaJson = parseBruFileMeta(bruContent);\n\n      file.data = metaJson;\n      file.partial = false;\n      file.loading = true;\n      file.size = sizeInMB(fileStats?.size);\n      hydrateRequestWithUuid(file.data, pathname);\n      await mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);\n\n      try {\n        const parsedData = await parseLargeRequestWithRedaction(bruContent, 'bru');\n\n        file.data = parsedData;\n        file.loading = false;\n        file.partial = false;\n        file.size = sizeInMB(fileStats?.size);\n        hydrateRequestWithUuid(file.data, pathname);\n        await mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);\n      } catch (parseError) {\n        file.data = metaJson;\n        file.partial = true;\n        file.loading = false;\n        file.size = sizeInMB(fileStats?.size);\n        hydrateRequestWithUuid(file.data, pathname);\n        await mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);\n        throw parseError;\n      }\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:mount-collection', async (event, { collectionUid, collectionPathname, brunoConfig }) => {\n    let tempDirectoryPath = null;\n    try {\n      // Ensure the transient base directory exists\n      const transientBase = getTransientDirectoryBase();\n      if (!fs.existsSync(transientBase)) {\n        fs.mkdirSync(transientBase, { recursive: true });\n      }\n      tempDirectoryPath = fs.mkdtempSync(getTransientCollectionPrefix());\n      const metadata = {\n        collectionPath: collectionPathname\n      };\n      fs.writeFileSync(path.join(tempDirectoryPath, 'metadata.json'), JSON.stringify(metadata));\n    } catch (error) {\n      throw error;\n    }\n    const {\n      size,\n      filesCount,\n      maxFileSize\n    } = await getCollectionStats(collectionPathname);\n\n    const shouldLoadCollectionAsync\n      = (size > MAX_COLLECTION_SIZE_IN_MB)\n        || (filesCount > MAX_COLLECTION_FILES_COUNT)\n        || (maxFileSize > MAX_SINGLE_FILE_SIZE_IN_COLLECTION_IN_MB);\n\n    watcher.addWatcher(mainWindow, collectionPathname, collectionUid, brunoConfig, false, shouldLoadCollectionAsync);\n\n    // Add watcher for transient directory\n    watcher.addTempDirectoryWatcher(mainWindow, tempDirectoryPath, collectionUid, collectionPathname);\n\n    return tempDirectoryPath;\n  });\n\n  ipcMain.handle('renderer:mount-workspace-scratch', async (event, { workspaceUid, workspacePath }) => {\n    try {\n      // Ensure the transient base directory exists\n      const transientBase = getTransientDirectoryBase();\n      if (!fs.existsSync(transientBase)) {\n        fs.mkdirSync(transientBase, { recursive: true });\n      }\n      const tempDirectoryPath = fs.mkdtempSync(getTransientScratchPrefix());\n      registerScratchCollectionPath(tempDirectoryPath);\n\n      const collectionRoot = {\n        meta: {\n          name: 'Scratch'\n        }\n      };\n\n      const brunoConfig = {\n        opencollection: '1.0.0',\n        name: 'Scratch',\n        type: 'collection',\n        ignore: ['node_modules', '.git']\n      };\n\n      const content = stringifyCollection(collectionRoot, brunoConfig, { format: 'yml' });\n      await writeFile(path.join(tempDirectoryPath, 'opencollection.yml'), content);\n\n      const metadata = {\n        workspaceUid,\n        workspacePath,\n        type: 'scratch'\n      };\n      fs.writeFileSync(path.join(tempDirectoryPath, 'metadata.json'), JSON.stringify(metadata));\n\n      return tempDirectoryPath;\n    } catch (error) {\n      console.error('Error mounting workspace scratch collection:', error);\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:add-collection-watcher', async (event, { collectionPath, collectionUid, brunoConfig }) => {\n    if (!watcher || !mainWindow) {\n      throw new Error('Watcher or mainWindow not available');\n    }\n\n    try {\n      const { size, filesCount, maxFileSize } = await getCollectionStats(collectionPath);\n\n      const shouldLoadCollectionAsync\n        = (size > MAX_COLLECTION_SIZE_IN_MB)\n          || (filesCount > MAX_COLLECTION_FILES_COUNT)\n          || (maxFileSize > MAX_SINGLE_FILE_SIZE_IN_COLLECTION_IN_MB);\n\n      watcher.addWatcher(mainWindow, collectionPath, collectionUid, brunoConfig, false, shouldLoadCollectionAsync);\n\n      return { success: true };\n    } catch (error) {\n      console.error('Error adding collection watcher:', error);\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:save-scratch-request', async (event, { sourcePathname, targetDirname, targetFilename, request }) => {\n    try {\n      if (!fs.existsSync(sourcePathname)) {\n        throw new Error(`Source path: ${sourcePathname} does not exist`);\n      }\n\n      if (!fs.existsSync(targetDirname)) {\n        throw new Error(`Target directory: ${targetDirname} does not exist`);\n      }\n\n      validatePathIsInsideCollection(targetDirname);\n\n      const collectionPath = findCollectionPathByItemPath(targetDirname);\n      if (!collectionPath) {\n        throw new Error('Could not determine collection for target directory');\n      }\n      const format = getCollectionFormat(collectionPath);\n\n      const filename = targetFilename || path.basename(sourcePathname);\n      const filenameWithoutExt = filename.replace(/\\.(bru|yml)$/, '');\n      const finalFilename = `${filenameWithoutExt}.${format}`;\n      const targetPathname = path.join(targetDirname, finalFilename);\n\n      if (fs.existsSync(targetPathname)) {\n        throw new Error(`A file with the name \"${finalFilename}\" already exists in the target location`);\n      }\n\n      const content = await stringifyRequestViaWorker(request, { format });\n\n      await writeFile(targetPathname, content);\n\n      if (request.examples) {\n        syncExampleUidsCache(collectionPath, request.examples);\n      }\n\n      return { newPathname: targetPathname };\n    } catch (error) {\n      console.error('Error saving scratch request:', error);\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:show-in-folder', async (event, filePath) => {\n    try {\n      if (!filePath) {\n        throw new Error('File path is required');\n      }\n      shell.showItemInFolder(filePath);\n    } catch (error) {\n      console.error('Error in show-in-folder: ', error);\n      throw error;\n    }\n  });\n\n  // Implement the Postman to Bruno conversion handler\n  ipcMain.handle('renderer:convert-postman-to-bruno', async (event, postmanCollection) => {\n    try {\n      // Convert Postman collection to Bruno format\n      const brunoCollection = await postmanToBruno(postmanCollection, { useWorkers: true });\n\n      return brunoCollection;\n    } catch (error) {\n      console.error('Error converting Postman to Bruno:', error);\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:get-collection-json', async (event, collectionPath) => {\n    let variables = {};\n    let name = '';\n    const getBruFilesRecursively = async (dir) => {\n      const getFilesInOrder = async (dir) => {\n        let bruJsons = [];\n\n        const traverse = async (currentPath) => {\n          const filesInCurrentDir = fs.readdirSync(currentPath);\n\n          if (currentPath.includes('node_modules')) {\n            return;\n          }\n\n          for (const file of filesInCurrentDir) {\n            const filePath = path.join(currentPath, file);\n            const stats = fs.lstatSync(filePath);\n\n            if (stats.isDirectory() && !filePath.startsWith('.git') && !filePath.startsWith('node_modules')) {\n              await traverse(filePath);\n            }\n          }\n\n          const currentDirBruJsons = [];\n          for (const file of filesInCurrentDir) {\n            const filePath = path.join(currentPath, file);\n            const stats = fs.lstatSync(filePath);\n\n            if (isBrunoConfigFile(filePath, collectionPath)) {\n              try {\n                const content = fs.readFileSync(filePath, 'utf8');\n                const brunoConfig = JSON.parse(content);\n\n                name = brunoConfig?.name;\n              } catch (err) {\n                console.error(err);\n              }\n            }\n\n            if (isDotEnvFile(filePath, collectionPath)) {\n              try {\n                const content = fs.readFileSync(filePath, 'utf8');\n                const jsonData = dotenvToJson(content);\n                variables = {\n                  ...variables,\n                  processEnvVariables: {\n                    ...process.env,\n                    ...jsonData\n                  }\n                };\n                continue;\n              } catch (err) {\n                console.error(err);\n              }\n            }\n\n            if (isBruEnvironmentConfig(filePath, collectionPath)) {\n              try {\n                let bruContent = fs.readFileSync(filePath, 'utf8');\n                const environmentFilepathBasename = path.basename(filePath);\n                const environmentName = environmentFilepathBasename.substring(0, environmentFilepathBasename.length - 4);\n                let data = await parseEnvironment(bruContent);\n                variables = {\n                  ...variables,\n                  envVariables: {\n                    ...(variables?.envVariables || {}),\n                    [path.basename(filePath)]: data.variables\n                  }\n                };\n                continue;\n              } catch (err) {\n                console.error(err);\n              }\n            }\n\n            if (isCollectionRootBruFile(filePath, collectionPath)) {\n              try {\n                let bruContent = fs.readFileSync(filePath, 'utf8');\n                let data = await parseCollection(bruContent);\n                // TODO\n                continue;\n              } catch (err) {\n                console.error(err);\n              }\n            }\n            if (!stats.isDirectory() && path.extname(filePath) === '.bru' && file !== 'folder.bru') {\n              const bruContent = fs.readFileSync(filePath, 'utf8');\n              const bruJson = parseRequest(bruContent);\n\n              currentDirBruJsons.push({\n                ...bruJson\n              });\n            }\n          }\n\n          bruJsons = bruJsons.concat(currentDirBruJsons);\n        };\n\n        await traverse(dir);\n        return bruJsons;\n      };\n\n      const orderedFiles = await getFilesInOrder(dir);\n      return orderedFiles;\n    };\n\n    const files = await getBruFilesRecursively(collectionPath);\n    return { name, files, ...variables };\n  });\n\n  ipcMain.handle('renderer:export-collection-zip', async (event, collectionPath, collectionName) => {\n    try {\n      if (!collectionPath || !fs.existsSync(collectionPath)) {\n        throw new Error('Collection path does not exist');\n      }\n\n      const defaultFileName = `${sanitizeName(collectionName)}.zip`;\n      const { filePath, canceled } = await dialog.showSaveDialog(mainWindow, {\n        title: 'Export Collection as ZIP',\n        defaultPath: defaultFileName,\n        filters: [{ name: 'Zip Files', extensions: ['zip'] }]\n      });\n\n      if (canceled || !filePath) {\n        return { success: false, canceled: true };\n      }\n\n      const ignoredDirectories = ['node_modules', '.git'];\n\n      await new Promise((resolve, reject) => {\n        const output = fs.createWriteStream(filePath);\n        const archive = archiver('zip', { zlib: { level: 9 } });\n\n        output.on('close', () => {\n          resolve();\n        });\n\n        archive.on('error', (err) => {\n          reject(err);\n        });\n\n        archive.pipe(output);\n\n        const addDirectoryToArchive = (dirPath, archivePath) => {\n          const entries = fs.readdirSync(dirPath, { withFileTypes: true });\n\n          for (const entry of entries) {\n            const fullPath = path.join(dirPath, entry.name);\n            const entryArchivePath = archivePath ? path.join(archivePath, entry.name) : entry.name;\n\n            if (entry.isDirectory()) {\n              if (!ignoredDirectories.includes(entry.name)) {\n                addDirectoryToArchive(fullPath, entryArchivePath);\n              }\n            } else {\n              archive.file(fullPath, { name: entryArchivePath });\n            }\n          }\n        };\n\n        addDirectoryToArchive(collectionPath, '');\n        archive.finalize();\n      });\n\n      return { success: true, filePath };\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:is-bruno-collection-zip', async (event, zipFilePath) => {\n    try {\n      const zip = new AdmZip(zipFilePath);\n      const entries = zip.getEntries().map((e) => e.entryName);\n\n      return entries.some(\n        (name) =>\n          name === 'bruno.json'\n          || name === 'opencollection.yml'\n          || /^[^/]+\\/bruno\\.json$/.test(name)\n          || /^[^/]+\\/opencollection\\.yml$/.test(name)\n      );\n    } catch {\n      return false;\n    }\n  });\n\n  ipcMain.handle('renderer:import-collection-zip', async (event, zipFilePath, collectionLocation) => {\n    try {\n      if (!fs.existsSync(zipFilePath)) {\n        throw new Error('ZIP file does not exist');\n      }\n\n      if (!collectionLocation || !fs.existsSync(collectionLocation)) {\n        throw new Error('Collection location does not exist');\n      }\n\n      const tempDir = path.join(os.tmpdir(), `bruno_zip_import_${Date.now()}`);\n      await fsExtra.ensureDir(tempDir);\n\n      // Validates that no symlinks point outside the base directory\n      const validateNoExternalSymlinks = (dir, baseDir) => {\n        const entries = fs.readdirSync(dir, { withFileTypes: true });\n        for (const entry of entries) {\n          const fullPath = path.join(dir, entry.name);\n          const stat = fs.lstatSync(fullPath);\n\n          if (stat.isSymbolicLink()) {\n            const linkTarget = fs.readlinkSync(fullPath);\n            const resolvedTarget = path.resolve(path.dirname(fullPath), linkTarget);\n            if (!resolvedTarget.startsWith(baseDir + path.sep) && resolvedTarget !== baseDir) {\n              throw new Error(`Security error: Symlink \"${entry.name}\" points outside extraction directory`);\n            }\n          }\n\n          if (stat.isDirectory() && !stat.isSymbolicLink()) {\n            validateNoExternalSymlinks(fullPath, baseDir);\n          }\n        }\n      };\n\n      try {\n        await extractZip(zipFilePath, { dir: tempDir });\n\n        validateNoExternalSymlinks(tempDir, tempDir);\n\n        const extractedItems = fs.readdirSync(tempDir);\n        let collectionDir = tempDir;\n\n        if (extractedItems.length === 1) {\n          const singleItem = path.join(tempDir, extractedItems[0]);\n          const singleItemStat = fs.lstatSync(singleItem);\n          if (singleItemStat.isDirectory() && !singleItemStat.isSymbolicLink()) {\n            collectionDir = singleItem;\n          }\n        }\n\n        const brunoJsonPath = path.join(collectionDir, 'bruno.json');\n        const openCollectionYmlPath = path.join(collectionDir, 'opencollection.yml');\n\n        if (!fs.existsSync(brunoJsonPath) && !fs.existsSync(openCollectionYmlPath)) {\n          throw new Error('Invalid collection: Neither bruno.json nor opencollection.yml found in the ZIP file');\n        }\n\n        // Ensure config files are not symlinks\n        if (fs.existsSync(brunoJsonPath) && fs.lstatSync(brunoJsonPath).isSymbolicLink()) {\n          throw new Error('Security error: bruno.json cannot be a symbolic link');\n        }\n        if (fs.existsSync(openCollectionYmlPath) && fs.lstatSync(openCollectionYmlPath).isSymbolicLink()) {\n          throw new Error('Security error: opencollection.yml cannot be a symbolic link');\n        }\n\n        let collectionName = 'Imported Collection';\n        let brunoConfig = { name: collectionName, version: '1', type: 'collection', ignore: ['node_modules', '.git'] };\n        if (fs.existsSync(openCollectionYmlPath)) {\n          try {\n            const content = fs.readFileSync(openCollectionYmlPath, 'utf8');\n            const parsed = parseCollection(content, { format: 'yml' });\n            brunoConfig = parsed.brunoConfig || brunoConfig;\n            collectionName = brunoConfig.name || collectionName;\n          } catch (e) {\n            console.error(`Error parsing opencollection.yml at ${openCollectionYmlPath}:`, e);\n          }\n        } else if (fs.existsSync(brunoJsonPath)) {\n          try {\n            brunoConfig = JSON.parse(fs.readFileSync(brunoJsonPath, 'utf8'));\n            collectionName = brunoConfig.name || collectionName;\n          } catch (e) {\n            console.error(`Error parsing bruno.json at ${brunoJsonPath}:`, e);\n          }\n        }\n\n        let sanitizedName = sanitizeName(collectionName);\n        if (!sanitizedName) {\n          sanitizedName = `untitled-${Date.now()}`;\n        }\n        let finalCollectionPath = path.join(collectionLocation, sanitizedName);\n        let counter = 1;\n        while (fs.existsSync(finalCollectionPath)) {\n          finalCollectionPath = path.join(collectionLocation, `${sanitizedName} (${counter})`);\n          counter++;\n        }\n\n        await fsExtra.move(collectionDir, finalCollectionPath);\n        if (tempDir !== collectionDir) {\n          await fsExtra.remove(tempDir).catch(() => {});\n        }\n\n        const uid = generateUidBasedOnHash(finalCollectionPath);\n        const { size, filesCount } = await getCollectionStats(finalCollectionPath);\n        brunoConfig.size = size;\n        brunoConfig.filesCount = filesCount;\n\n        mainWindow.webContents.send('main:collection-opened', finalCollectionPath, uid, brunoConfig);\n        ipcMain.emit('main:collection-opened', mainWindow, finalCollectionPath, uid, brunoConfig);\n\n        return finalCollectionPath;\n      } catch (error) {\n        await fsExtra.remove(tempDir).catch(() => {});\n        throw error;\n      }\n    } catch (error) {\n      throw error;\n    }\n  });\n};\n\nconst registerMainEventHandlers = (mainWindow, watcher) => {\n  ipcMain.on('main:open-collection', () => {\n    if (watcher && mainWindow) {\n      openCollectionDialog(mainWindow, watcher);\n    }\n  });\n\n  ipcMain.on('main:open-docs', () => {\n    const docsURL = 'https://docs.usebruno.com';\n    shell.openExternal(docsURL);\n  });\n\n  ipcMain.on('main:collection-opened', async (win, pathname, uid, brunoConfig) => {\n    app.addRecentDocument(pathname);\n  });\n\n  ipcMain.handle('renderer:scan-for-bruno-files', (event, dir) => {\n    try {\n      return scanForBrunoFiles(dir);\n    } catch (error) {\n      throw new Error(error.message);\n    }\n  });\n\n  // The app listen for this event and allows the user to save unsaved requests before closing the app\n  ipcMain.on('main:start-quit-flow', () => {\n    mainWindow.webContents.send('main:start-quit-flow');\n  });\n\n  ipcMain.handle('main:complete-quit-flow', () => {\n    mainWindow.destroy();\n  });\n\n  ipcMain.handle('main:force-quit', () => {\n    process.exit();\n  });\n};\n\nconst registerCollectionsIpc = (mainWindow, watcher) => {\n  registerRendererEventHandlers(mainWindow, watcher);\n  registerMainEventHandlers(mainWindow, watcher);\n};\n\nmodule.exports = registerCollectionsIpc;\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/filesystem.js",
    "content": "const { ipcMain } = require('electron');\nconst path = require('node:path');\n\nconst {\n  browseDirectory,\n  browseFiles,\n  normalizeAndResolvePath,\n  isFile,\n  isDirectory\n} = require('../utils/filesystem');\nconst { findUniqueFolderName } = require('../utils/collection-import');\n\nconst registerFilesystemIpc = (mainWindow) => {\n  ipcMain.handle('renderer:browse-directory', async (event, pathname, request) => {\n    try {\n      return await browseDirectory(mainWindow);\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:browse-files', async (_, filters, properties) => {\n    try {\n      return await browseFiles(mainWindow, filters, properties);\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:exists-sync', async (_, filePath) => {\n    try {\n      const normalizedPath = normalizeAndResolvePath(filePath);\n      return isFile(normalizedPath);\n    } catch (error) {\n      return false;\n    }\n  });\n\n  ipcMain.handle('renderer:resolve-path', async (_, relativePath, basePath) => {\n    try {\n      const resolvedPath = path.resolve(basePath, relativePath);\n      return normalizeAndResolvePath(resolvedPath);\n    } catch (error) {\n      return relativePath;\n    }\n  });\n\n  ipcMain.handle('renderer:is-directory', async (_, pathname) => {\n    return isDirectory(pathname);\n  });\n\n  ipcMain.handle('renderer:find-unique-folder-name', async (_, baseName, location) => {\n    try {\n      return await findUniqueFolderName(baseName, location);\n    } catch (error) {\n      throw error;\n    }\n  });\n};\n\nmodule.exports = registerFilesystemIpc;\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/git.js",
    "content": "const { ipcMain } = require('electron');\nconst { cloneGitRepository } = require('../utils/git');\nconst { createDirectory, removeDirectory } = require('../utils/filesystem');\n\nconst registerGitIpc = (mainWindow) => {\n  ipcMain.handle('renderer:clone-git-repository', async (event, { url, path, processUid }) => {\n    let directoryCreated = false;\n    try {\n      await createDirectory(path);\n      directoryCreated = true;\n      await cloneGitRepository(mainWindow, { url, path, processUid });\n      return 'Repository cloned successfully';\n    } catch (error) {\n      if (directoryCreated) {\n        await removeDirectory(path);\n      }\n      return Promise.reject(error);\n    }\n  });\n};\n\nmodule.exports = registerGitIpc;\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/global-environments.js",
    "content": "require('dotenv').config();\nconst fs = require('fs');\nconst path = require('path');\nconst { ipcMain } = require('electron');\nconst { globalEnvironmentsStore } = require('../store/global-environments');\nconst { generateUniqueName, sanitizeName, writeFile, isValidDotEnvFilename } = require('../utils/filesystem');\n\nconst registerGlobalEnvironmentsIpc = (mainWindow, workspaceEnvironmentsManager) => {\n  ipcMain.handle('renderer:create-global-environment', async (event, { uid, name, variables, color, workspaceUid, workspacePath }) => {\n    try {\n      // If workspace path provided, use workspace environments manager\n      if (workspacePath && workspaceEnvironmentsManager) {\n        const { globalEnvironments } = await workspaceEnvironmentsManager.getGlobalEnvironmentsByPath(workspacePath);\n        const existingNames = globalEnvironments?.map((env) => env.name) || [];\n\n        const sanitizedName = sanitizeName(name);\n        const uniqueName = generateUniqueName(sanitizedName, (name) => existingNames.includes(name));\n\n        return await workspaceEnvironmentsManager.addGlobalEnvironmentByPath(workspacePath, { uid, name: uniqueName, variables, color });\n      }\n\n      const existingGlobalEnvironments = globalEnvironmentsStore.getGlobalEnvironments();\n      const existingNames = existingGlobalEnvironments?.map((env) => env.name) || [];\n\n      const sanitizedName = sanitizeName(name);\n      const uniqueName = generateUniqueName(sanitizedName, (name) => existingNames.includes(name));\n\n      globalEnvironmentsStore.addGlobalEnvironment({ uid, name: uniqueName, variables, color });\n\n      return { name: uniqueName, color };\n    } catch (error) {\n      console.error('Error in renderer:create-global-environment:', error);\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:save-global-environment', async (event, { environmentUid, variables, workspaceUid, workspacePath }) => {\n    try {\n      if (workspacePath && workspaceEnvironmentsManager) {\n        return await workspaceEnvironmentsManager.saveGlobalEnvironmentByPath(workspacePath, { environmentUid, variables });\n      }\n\n      globalEnvironmentsStore.saveGlobalEnvironment({ environmentUid, variables });\n    } catch (error) {\n      console.error('Error in renderer:save-global-environment:', error);\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:rename-global-environment', async (event, { environmentUid, name, workspaceUid, workspacePath }) => {\n    try {\n      if (workspacePath && workspaceEnvironmentsManager) {\n        return await workspaceEnvironmentsManager.renameGlobalEnvironmentByPath(workspacePath, { environmentUid, name });\n      }\n\n      globalEnvironmentsStore.renameGlobalEnvironment({ environmentUid, name });\n    } catch (error) {\n      console.error('Error in renderer:rename-global-environment:', error);\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:delete-global-environment', async (event, { environmentUid, workspaceUid, workspacePath }) => {\n    try {\n      if (workspacePath && workspaceEnvironmentsManager) {\n        return await workspaceEnvironmentsManager.deleteGlobalEnvironmentByPath(workspacePath, { environmentUid });\n      }\n\n      globalEnvironmentsStore.deleteGlobalEnvironment({ environmentUid });\n    } catch (error) {\n      console.error('Error in renderer:delete-global-environment:', error);\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:select-global-environment', async (event, { environmentUid, workspaceUid, workspacePath }) => {\n    try {\n      if (workspacePath && workspaceEnvironmentsManager) {\n        return await workspaceEnvironmentsManager.selectGlobalEnvironmentByPath(workspacePath, { environmentUid });\n      }\n\n      globalEnvironmentsStore.selectGlobalEnvironment({ environmentUid });\n    } catch (error) {\n      console.error('Error in renderer:select-global-environment:', error);\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:get-global-environments', async (event, { workspaceUid, workspacePath }) => {\n    try {\n      if (workspacePath && workspaceEnvironmentsManager) {\n        return await workspaceEnvironmentsManager.getGlobalEnvironmentsByPath(workspacePath);\n      }\n\n      return {\n        globalEnvironments: globalEnvironmentsStore.getGlobalEnvironments() || [],\n        activeGlobalEnvironmentUid: globalEnvironmentsStore.getActiveGlobalEnvironmentUid()\n      };\n    } catch (error) {\n      console.error('Error in renderer:get-global-environments:', error);\n      return Promise.reject(error);\n    }\n  });\n\n  // Save workspace .env file variables\n  ipcMain.handle('renderer:save-workspace-dotenv-variables', async (event, { workspacePath, variables, filename = '.env' }) => {\n    try {\n      if (!workspacePath) {\n        throw new Error('Workspace path is required');\n      }\n\n      if (!isValidDotEnvFilename(filename)) {\n        throw new Error('Invalid .env filename');\n      }\n\n      const dotEnvPath = path.join(workspacePath, filename);\n\n      // Convert variables array to .env format\n      const content = variables\n        .filter((v) => v.name && v.name.trim() !== '')\n        .map((v) => {\n          const value = v.value || '';\n          // If value contains newlines or special characters, wrap in quotes\n          if (value.includes('\\n') || value.includes('\"') || value.includes('\\'') || value.includes('\\\\')) {\n            // Escape backslashes first, then double quotes\n            const escapedValue = value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n            return `${v.name}=\"${escapedValue}\"`;\n          }\n          return `${v.name}=${value}`;\n        })\n        .join('\\n');\n\n      await writeFile(dotEnvPath, content);\n\n      return { success: true };\n    } catch (error) {\n      console.error('Error saving workspace .env file:', error);\n      return Promise.reject(error);\n    }\n  });\n\n  // Save workspace .env file raw content\n  ipcMain.handle('renderer:save-workspace-dotenv-raw', async (event, { workspacePath, content, filename = '.env' }) => {\n    try {\n      if (!workspacePath) {\n        throw new Error('Workspace path is required');\n      }\n\n      if (!isValidDotEnvFilename(filename)) {\n        throw new Error('Invalid .env filename');\n      }\n\n      const dotEnvPath = path.join(workspacePath, filename);\n      await writeFile(dotEnvPath, content);\n\n      return { success: true };\n    } catch (error) {\n      console.error('Error saving workspace .env file:', error);\n      return Promise.reject(error);\n    }\n  });\n\n  // Create workspace .env file\n  ipcMain.handle('renderer:create-workspace-dotenv-file', async (event, { workspacePath, filename = '.env' }) => {\n    try {\n      if (!workspacePath) {\n        throw new Error('Workspace path is required');\n      }\n\n      if (!isValidDotEnvFilename(filename)) {\n        throw new Error('Invalid .env filename');\n      }\n\n      const dotEnvPath = path.join(workspacePath, filename);\n\n      if (fs.existsSync(dotEnvPath)) {\n        throw new Error(`${filename} file already exists`);\n      }\n\n      await writeFile(dotEnvPath, '');\n\n      return { success: true };\n    } catch (error) {\n      console.error('Error creating workspace .env file:', error);\n      return Promise.reject(error);\n    }\n  });\n\n  // Delete workspace .env file\n  ipcMain.handle('renderer:delete-workspace-dotenv-file', async (event, { workspacePath, filename = '.env' }) => {\n    try {\n      if (!workspacePath) {\n        throw new Error('Workspace path is required');\n      }\n\n      if (!isValidDotEnvFilename(filename)) {\n        throw new Error('Invalid .env filename');\n      }\n\n      const dotEnvPath = path.join(workspacePath, filename);\n\n      if (!fs.existsSync(dotEnvPath)) {\n        throw new Error(`${filename} file does not exist`);\n      }\n\n      fs.unlinkSync(dotEnvPath);\n\n      return { success: true };\n    } catch (error) {\n      console.error('Error deleting workspace .env file:', error);\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:update-global-environment-color', async (event, { environmentUid, color, workspacePath }) => {\n    try {\n      if (workspacePath && workspaceEnvironmentsManager) {\n        return await workspaceEnvironmentsManager.updateGlobalEnvironmentColorByPath(workspacePath, { environmentUid, color });\n      }\n\n      globalEnvironmentsStore.updateGlobalEnvironmentColor({ environmentUid, color });\n    } catch (error) {\n      console.error('Error in renderer:update-global-environment-color:', error);\n      return Promise.reject(error);\n    }\n  });\n};\n\nmodule.exports = registerGlobalEnvironmentsIpc;\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/network/authorize-user-in-system-browser.js",
    "content": "const { shell } = require('electron');\nconst { registerOauth2AuthorizationRequest, rejectOauth2AuthorizationRequest } = require('../../utils/oauth2-protocol-handler');\n\nconst authorizeUserInSystemBrowser = ({ authorizeUrl, callbackUrl, grantType = 'authorization_code' }) => {\n  return new Promise((resolve, reject) => {\n    // Replace callback URL in authorization URL\n    const authorizationUrlObj = new URL(authorizeUrl);\n    authorizationUrlObj.searchParams.set('redirect_uri', callbackUrl);\n    const modifiedAuthorizeUrl = authorizationUrlObj.toString();\n\n    // Set timeout for the request (5 minutes)\n    const timeout = setTimeout(() => {\n      rejectOauth2AuthorizationRequest(new Error('Authorization timeout'));\n    }, 5 * 60 * 1000);\n\n    // Wrap resolve/reject to clear timeout and add debugInfo\n    const debugInfo = {\n      data: []\n    };\n\n    const authorizationRequest = {\n      request: {\n        url: modifiedAuthorizeUrl,\n        method: 'GET',\n        headers: {},\n        error: null\n      },\n      response: {\n        headers: {},\n        status: null,\n        statusText: null,\n        error: null\n      },\n      fromCache: false,\n      completed: false\n    };\n\n    debugInfo.data.push(authorizationRequest);\n\n    const wrappedResolve = (value) => {\n      clearTimeout(timeout);\n      if (grantType === 'implicit') {\n        resolve({ implicitTokens: value, debugInfo });\n      } else {\n        resolve({ authorizationCode: value, debugInfo });\n      }\n    };\n\n    const wrappedReject = (error) => {\n      clearTimeout(timeout);\n      reject(error);\n    };\n\n    registerOauth2AuthorizationRequest(wrappedResolve, wrappedReject, debugInfo);\n\n    // Open system browser\n    shell.openExternal(modifiedAuthorizeUrl).catch((error) => {\n      rejectOauth2AuthorizationRequest(error);\n    });\n  });\n};\n\nmodule.exports = { authorizeUserInSystemBrowser };\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/network/authorize-user-in-window.js",
    "content": "const { BrowserWindow } = require('electron');\nconst { preferencesUtil } = require('../../store/preferences');\n\nconst matchesCallbackUrl = (url, callbackUrl) => {\n  if (!url) return false;\n  // Match the callback URL and require an OAuth2 response indicator\n  // (code query params for authorization code flow, or hash fragment for implicit flow).\n  // This prevents false matches on intermediate pages (e.g. /auth/login) when the\n  // callback URL is a root path like https://hostname/.\n  return url.href.startsWith(callbackUrl.href)\n    && (url.searchParams.has('code') || url.hash.length > 1);\n};\n\nconst authorizeUserInWindow = ({ authorizeUrl, callbackUrl, session, additionalHeaders = {}, grantType = 'authorization_code' }) => {\n  return new Promise(async (resolve, reject) => {\n    let finalUrl = null;\n    let debugInfo = {\n      data: []\n    };\n    let currentMainRequest = null;\n\n    let allOpenWindows = BrowserWindow.getAllWindows();\n\n    // Close all windows except the main window (assumed to have id 1)\n    let windowsExcludingMain = allOpenWindows.filter((w) => w.id !== 1);\n    windowsExcludingMain.forEach((w) => {\n      w.close();\n    });\n\n    const window = new BrowserWindow({\n      webPreferences: {\n        nodeIntegration: false,\n        partition: session\n      },\n      show: false\n    });\n    window.on('ready-to-show', window.show.bind(window));\n\n    // Ensure the browser window complies with \"SSL/TLS Certificate Verification\" preference\n    window.webContents.on('certificate-error', (event, url, error, certificate, callback) => {\n      event.preventDefault();\n      const shouldAllow = !preferencesUtil.shouldVerifyTls();\n      if (!shouldAllow) {\n        console.error(`Bruno OAuth: SSL Certificate verification failed for ${url}. Error: ${error}`);\n        console.error('Bruno OAuth: Disable \"SSL/TLS Certificate Verification\" in settings to proceed with OAuth flows that use self-signed certificates.');\n      }\n      callback(shouldAllow);\n    });\n\n    const { session: webSession } = window.webContents;\n\n    // Intercept request events and gather data\n    webSession.webRequest.onBeforeRequest((details, callback) => {\n      const { id: requestId, url, method, resourceType, frameId } = details;\n      if (resourceType === 'mainFrame') {\n        // This is a main frame request\n        currentMainRequest = {\n          requestId,\n          resourceType,\n          frameId,\n          request: {\n            url,\n            method,\n            headers: {},\n            error: null\n          },\n          response: {\n            headers: {},\n            status: null,\n            statusText: null,\n            error: null\n          },\n          fromCache: false,\n          completed: true,\n          requests: [] // No sub-requests in this context\n        };\n        // Add to mainRequests\n\n        // pushing the currentMainRequest to debugInfo\n        // the currentMainRequest will be further updated by object reference\n        debugInfo.data.push(currentMainRequest);\n      }\n\n      callback({ cancel: false });\n    });\n\n    webSession.webRequest.onBeforeSendHeaders((details, callback) => {\n      const { id: requestId, requestHeaders, method, url } = details;\n\n      if (details.resourceType === 'mainFrame' && Object.keys(additionalHeaders).length > 0) {\n        // Add our custom headers\n        for (const [name, value] of Object.entries(additionalHeaders)) {\n          requestHeaders[name] = value;\n        }\n      }\n\n      if (currentMainRequest?.requestId === requestId) {\n        currentMainRequest.request = {\n          url,\n          headers: requestHeaders,\n          method\n        };\n      }\n      callback({ cancel: false, requestHeaders });\n    });\n\n    webSession.webRequest.onHeadersReceived((details, callback) => {\n      const { id: requestId, url, statusCode, responseHeaders, method } = details;\n      if (currentMainRequest?.requestId === requestId) {\n        currentMainRequest.response = {\n          url,\n          method,\n          status: statusCode,\n          headers: responseHeaders\n        };\n      }\n      callback({ cancel: false, responseHeaders });\n    });\n\n    webSession.webRequest.onCompleted((details) => {\n      const { id: requestId, fromCache } = details;\n      if (currentMainRequest?.requestId === requestId) {\n        currentMainRequest.completed = true;\n        currentMainRequest.fromCache = fromCache;\n      }\n    });\n\n    webSession.webRequest.onErrorOccurred((details) => {\n      const { id: requestId, error } = details;\n      if (currentMainRequest?.requestId === requestId) {\n        currentMainRequest.response.error = error;\n      }\n    });\n\n    function onWindowRedirect(url) {\n      // Handle redirects as needed\n      let urlObj;\n      let callbackUrlObj;\n\n      try {\n        urlObj = new URL(url);\n      } catch (e) {\n        // Invalid redirect URL, skip processing\n        return;\n      }\n\n      try {\n        callbackUrlObj = new URL(callbackUrl);\n      } catch (e) {\n        // Invalid callback URL, skip matching but still check for errors below\n        callbackUrlObj = null;\n      }\n\n      // Handle OAuth error responses first, so we reject with\n      // a descriptive error instead of resolving with a null authorization code\n      if (urlObj.searchParams.has('error')) {\n        const error = urlObj.searchParams.get('error');\n        const errorDescription = urlObj.searchParams.get('error_description');\n        const errorUri = urlObj.searchParams.get('error_uri');\n        let errorData = {\n          message: 'Authorization Failed!',\n          error,\n          errorDescription,\n          errorUri\n        };\n        reject(new Error(JSON.stringify(errorData)));\n        window.close();\n        return;\n      }\n\n      if (callbackUrlObj && matchesCallbackUrl(urlObj, callbackUrlObj)) {\n        finalUrl = url;\n        window.close();\n        return;\n      }\n    }\n\n    // Update currentMainRequest when navigation occurs\n    window.webContents.on('did-start-navigation', (event, url, isInPlace, isMainFrame) => {\n      if (isMainFrame) {\n        // Reset currentMainRequest since a new navigation is starting\n        currentMainRequest = null;\n      }\n    });\n\n    window.webContents.on('did-navigate', (event, url) => {\n      onWindowRedirect(url);\n    });\n\n    window.webContents.on('will-redirect', (event, url) => {\n      onWindowRedirect(url);\n    });\n\n    window.on('close', () => {\n      // Clean up listeners to prevent memory leaks\n      window.webContents.removeAllListeners();\n      webSession.webRequest.onBeforeRequest(null);\n      webSession.webRequest.onBeforeSendHeaders(null);\n      webSession.webRequest.onHeadersReceived(null);\n      webSession.webRequest.onCompleted(null);\n      webSession.webRequest.onErrorOccurred(null);\n\n      if (finalUrl) {\n        try {\n          // Handle different grant types differently\n          if (grantType === 'implicit') {\n            // For implicit flow, tokens are in the URL hash fragment\n            const urlWithHash = new URL(finalUrl);\n            const hash = urlWithHash.hash.substring(1); // Remove the leading #\n            const hashParams = new URLSearchParams(hash);\n\n            // Extract tokens from hash fragment\n            const implicitTokens = {\n              access_token: hashParams.get('access_token'),\n              token_type: hashParams.get('token_type'),\n              expires_in: hashParams.get('expires_in'),\n              state: hashParams.get('state'),\n              scope: hashParams.get('scope')\n            };\n\n            return resolve({ implicitTokens, debugInfo });\n          } else {\n            // Default case - authorization code flow\n            const callbackUrlWithCode = new URL(finalUrl);\n            const authorizationCode = callbackUrlWithCode.searchParams.get('code');\n            return resolve({ authorizationCode, debugInfo });\n          }\n        } catch (error) {\n          return reject(error);\n        }\n      } else {\n        return reject(new Error('Authorization window closed'));\n      }\n    });\n\n    try {\n      await window.loadURL(authorizeUrl);\n    } catch (error) {\n      // Ignore ERR_ABORTED errors that occur during redirects\n      if (error.code === 'ERR_ABORTED') {\n        console.debug('Ignoring ERR_ABORTED during authorizeUserInWindow');\n        return;\n      }\n      reject(error);\n      window.close();\n    }\n  });\n};\n\nmodule.exports = { authorizeUserInWindow, matchesCallbackUrl };\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/network/awsv4auth-helper.js",
    "content": "const { fromIni } = require('@aws-sdk/credential-providers');\nconst { aws4Interceptor } = require('aws4-axios');\n\nfunction isStrPresent(str) {\n  return str && str !== '' && str !== 'undefined';\n}\n\nasync function resolveAwsV4Credentials(request) {\n  const awsv4 = request.awsv4config;\n  if (isStrPresent(awsv4.profileName)) {\n    try {\n      const credentialsProvider = fromIni({\n        profile: awsv4.profileName,\n        ignoreCache: true\n      });\n      const credentials = await credentialsProvider();\n      awsv4.accessKeyId = credentials.accessKeyId;\n      awsv4.secretAccessKey = credentials.secretAccessKey;\n      awsv4.sessionToken = credentials.sessionToken;\n    } catch {\n      console.error('Failed to fetch credentials from AWS profile.');\n    }\n  }\n  return awsv4;\n}\n\nfunction addAwsV4Interceptor(axiosInstance, request) {\n  if (!request.awsv4config) {\n    console.warn('No Auth Config found!');\n    return;\n  }\n\n  const awsv4 = request.awsv4config;\n  if (!isStrPresent(awsv4.accessKeyId) || !isStrPresent(awsv4.secretAccessKey)) {\n    console.warn('Required Auth Fields are not present');\n    return;\n  }\n\n  const interceptor = aws4Interceptor({\n    options: {\n      region: awsv4.region,\n      service: awsv4.service\n    },\n    credentials: {\n      accessKeyId: awsv4.accessKeyId,\n      secretAccessKey: awsv4.secretAccessKey,\n      sessionToken: awsv4.sessionToken\n    }\n  });\n\n  axiosInstance.interceptors.request.use(interceptor);\n}\n\nmodule.exports = {\n  addAwsV4Interceptor,\n  resolveAwsV4Credentials\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/network/axios-instance.js",
    "content": "const URL = require('url');\nconst Socket = require('net').Socket;\nconst axios = require('axios');\nconst connectionCache = new Map(); // Cache to store checkConnection() results\nconst electronApp = require('electron');\nconst { setupProxyAgents } = require('../../utils/proxy-util');\nconst { addCookieToJar, getCookieStringForUrl } = require('../../utils/cookies');\nconst { preferencesUtil } = require('../../store/preferences');\nconst { safeStringifyJSON } = require('../../utils/common');\nconst { createFormData } = require('../../utils/form-data');\n\nconst LOCAL_IPV6 = '::1';\nconst LOCAL_IPV4 = '127.0.0.1';\nconst LOCALHOST = 'localhost';\nconst version = electronApp?.app?.getVersion() ?? '';\nconst redirectResponseCodes = [301, 302, 303, 307, 308];\n\nconst saveCookies = (url, headers) => {\n  if (preferencesUtil.shouldStoreCookies()) {\n    let setCookieHeaders = [];\n    if (headers['set-cookie']) {\n      setCookieHeaders = Array.isArray(headers['set-cookie'])\n        ? headers['set-cookie']\n        : [headers['set-cookie']];\n      for (let setCookieHeader of setCookieHeaders) {\n        if (typeof setCookieHeader === 'string' && setCookieHeader.length) {\n          addCookieToJar(setCookieHeader, url);\n        }\n      }\n    }\n  }\n};\n\nconst getTld = (hostname) => {\n  if (!hostname) {\n    return '';\n  }\n\n  return hostname.substring(hostname.lastIndexOf('.') + 1);\n};\n\nconst checkConnection = (host, port) =>\n  new Promise((resolve) => {\n    const key = `${host}:${port}`;\n    const cachedResult = connectionCache.get(key);\n\n    if (cachedResult !== undefined) {\n      resolve(cachedResult);\n    } else {\n      const socket = new Socket();\n\n      socket.once('connect', () => {\n        socket.end();\n        connectionCache.set(key, true); // Cache successful connection\n        resolve(true);\n      });\n\n      socket.once('error', () => {\n        connectionCache.set(key, false); // Cache failed connection\n        resolve(false);\n      });\n\n      // Try to connect to the host and port\n      socket.connect(port, host);\n    }\n  });\n\n/**\n * Function that configures axios with timing interceptors\n * Important to note here that the timings are not completely accurate.\n * @see https://github.com/axios/axios/issues/695\n * @returns {axios.AxiosInstance}\n */\nfunction makeAxiosInstance({\n  proxyMode = 'off',\n  proxyConfig = {},\n  requestMaxRedirects = 5,\n  httpsAgentRequestFields = {},\n  interpolationOptions = {},\n  followRedirects = true\n} = {}) {\n  /** @type {axios.AxiosInstance} */\n  const instance = axios.create({\n    transformRequest: function (data, headers) {\n      const contentType = headers?.['Content-Type'] || headers?.['content-type'] || '';\n      const hasJSONContentType = contentType.includes('json');\n      if (typeof data === 'string' && hasJSONContentType) {\n        return data;\n      }\n\n      axios.defaults.transformRequest.forEach(function (tr) {\n        data = tr.call(this, data, headers);\n      }, this);\n      return data;\n    },\n    proxy: false,\n    maxRedirects: 0,\n    headers: {}\n  });\n\n  // Set User-Agent manually (using transformRequest to delete headers instead)\n  instance.defaults.headers.common = {\n    'User-Agent': `bruno-runtime/${version}`\n  };\n\n  instance.interceptors.request.use(async (config) => {\n    const url = URL.parse(config.url);\n    config.metadata = config.metadata || {};\n    config.metadata.startTime = new Date().getTime();\n    const timeline = config.metadata.timeline || [];\n    // Add initial request details to the timeline\n    timeline.push({\n      timestamp: new Date(),\n      type: 'separator'\n    });\n    timeline.push({\n      timestamp: new Date(),\n      type: 'info',\n      message: `Preparing request to ${config.url}`\n    });\n    timeline.push({\n      timestamp: new Date(),\n      type: 'info',\n      message: `Current time is ${new Date().toISOString()}`\n    });\n\n    // Add request method line\n    timeline.push({\n      timestamp: new Date(),\n      type: 'request',\n      message: `${config.method.toUpperCase()} ${config.url}`\n    });\n\n    // Add request data if available\n    if (config.data) {\n      let requestData = typeof config.data === 'string' ? config.data : JSON.stringify(config.data, null, 2);\n      timeline.push({\n        timestamp: new Date(),\n        type: 'requestData',\n        message: requestData\n      });\n    }\n\n    // Resolve all *.localhost to localhost and check if it should use IPv6 or IPv4\n    // RFC: 6761 section 6.3 (https://tools.ietf.org/html/rfc6761#section-6.3)\n    // @see https://github.com/usebruno/bruno/issues/124\n    if (getTld(url.hostname) === LOCALHOST || url.hostname === LOCAL_IPV4 || url.hostname === LOCAL_IPV6) {\n      // use custom DNS lookup for localhost\n      config.lookup = (hostname, options, callback) => {\n        const portNumber = Number(url.port) || (url.protocol.includes('https') ? 443 : 80);\n        checkConnection(LOCAL_IPV6, portNumber).then((useIpv6) => {\n          const ip = useIpv6 ? LOCAL_IPV6 : LOCAL_IPV4;\n          callback(null, ip, useIpv6 ? 6 : 4);\n        });\n      };\n    }\n\n    config.headers['request-start-time'] = Date.now();\n\n    /**\n      Apply header deletions requested via req.deleteHeader() in pre-request scripts.\n      Using set(name, null) rather than delete(): the axios http adapter guards its\n      own defaults (User-Agent, Accept-Encoding) with set(..., false) which only\n      skips writing when the key already exists. delete() removes the key entirely,\n      so the guard misses and the adapter re-adds the default. null keeps the key\n      present (blocking the guard) while toJSON() omits null values from the wire.\n     */\n    const headersToDelete = config.__headersToDelete;\n    let deleteConnection = false;\n\n    if (headersToDelete && Array.isArray(headersToDelete)) {\n      headersToDelete.forEach((headerName) => {\n        const lower = headerName.toLowerCase();\n        if (lower === 'host') return;\n        if (lower === 'connection') {\n          // Handled after setupProxyAgents to avoid being overwritten by keepAlive:true.\n          deleteConnection = true;\n          return;\n        }\n        config.headers.set(headerName, null);\n      });\n      delete config.__headersToDelete;\n    }\n\n    // Log request headers AFTER deletion so the timeline reflects what is actually sent.\n    // Skip null values (headers marked for deletion) and false values (e.g. content-type\n    // suppressed for no-body requests — see https://github.com/usebruno/bruno/issues/1693).\n    Object.entries(config.headers).forEach(([key, value]) => {\n      if (value === null || value === false) return;\n      timeline.push({\n        timestamp: new Date(),\n        type: 'requestHeader',\n        message: `${key}: ${value}`\n      });\n    });\n\n    const agentOptions = {\n      ...httpsAgentRequestFields,\n      keepAlive: true\n    };\n\n    try {\n      // Now call setupProxyAgents and pass the timeline\n      setupProxyAgents({\n        requestConfig: config,\n        proxyMode: proxyMode, // 'on', 'off', or 'system', depending on your settings\n        proxyConfig: proxyConfig,\n        httpsAgentRequestFields: agentOptions,\n        interpolationOptions: interpolationOptions, // Provide your interpolation options\n        timeline\n      });\n    } catch (err) {\n      if (err.timeline) {\n        timeline = err.timeline;\n      }\n      timeline.push({\n        timestamp: new Date(),\n        type: 'error',\n        message: `Error setting up proxy agents: ${err?.message}`\n      });\n    }\n\n    config.metadata.timeline = timeline;\n    return config;\n  });\n\n  let redirectCount = 0;\n\n  instance.interceptors.response.use(\n    (response) => {\n      let timeline;\n      const end = Date.now();\n      const start = response.config.headers['request-start-time'];\n      response.headers['request-duration'] = end - start;\n      redirectCount = 0;\n\n      const config = response.config;\n      timeline = config?.metadata?.timeline || [];\n      const duration = end - config?.metadata.startTime;\n\n      const httpVersion = response?.request?.res?.httpVersion || response?.httpVersion;\n      if (httpVersion?.startsWith('2')) {\n        timeline.push({\n          timestamp: new Date(),\n          type: 'info',\n          message: `Using HTTP/2, server supports multiplexing`\n        });\n      }\n      timeline.push({\n        timestamp: new Date(),\n        type: 'response',\n        message: `HTTP/${httpVersion || '1.1'} ${response.status} ${response.statusText}`\n      });\n\n      Object.entries(response.headers).forEach(([key, value]) => {\n        timeline.push({\n          timestamp: new Date(),\n          type: 'responseHeader',\n          message: `${key}: ${value}`\n        });\n      });\n\n      timeline.push({\n        timestamp: new Date(),\n        type: 'info',\n        message: `Request completed in ${duration} ms`\n      });\n      response.timeline = timeline;\n      return response;\n    },\n    (error) => {\n      const config = error.config;\n      const timeline = config?.metadata?.timeline || [];\n      timeline?.push({\n        timestamp: new Date(),\n        type: 'error',\n        message: 'there was an error executing the request!'\n      });\n      if (error.response) {\n        const end = Date.now();\n        const start = error.config.headers['request-start-time'];\n        error.response.headers['request-duration'] = end - start;\n        const duration = end - config?.metadata?.startTime;\n        if (error.response && redirectResponseCodes.includes(error.response.status)) {\n          timeline.push({\n            timestamp: new Date(),\n            type: 'response',\n            message: `HTTP/${error.response.httpVersion || '1.1'} ${error.response.status} ${error.response.statusText}`\n          });\n          Object.entries(error.response.headers).forEach(([key, value]) => {\n            timeline.push({\n              timestamp: new Date(),\n              type: 'responseHeader',\n              message: `${key}: ${value}`\n            });\n          });\n          timeline.push({\n            timestamp: new Date(),\n            type: 'info',\n            message: `Request completed in ${duration} ms`\n          });\n\n          // Attach the timeline to the response\n          error.response.timeline = timeline;\n\n          if (!followRedirects) {\n            if (preferencesUtil.shouldStoreCookies()) {\n              saveCookies(error.config.url, error.response.headers);\n            }\n\n            return Promise.reject(error);\n          }\n\n          if (redirectCount >= requestMaxRedirects) {\n            const errorResponseData = error.response.data;\n            timeline?.push({\n              timestamp: new Date(),\n              type: 'error',\n              message: safeStringifyJSON(errorResponseData?.toString?.())\n            });\n            return Promise.reject(error);\n          }\n\n          // Increase redirect count\n          redirectCount++;\n\n          const locationHeader = error.response.headers.location;\n          let redirectUrl = locationHeader;\n\n          // Handle relative URLs by resolving them against the original request URL\n          if (locationHeader && !locationHeader.match(/^https?:\\/\\//i)) {\n            // It's a relative URL, resolve it against the original URL\n            redirectUrl = URL.resolve(error.config.url, locationHeader);\n\n            timeline.push({\n              timestamp: new Date(),\n              type: 'info',\n              message: `Resolving relative redirect URL: ${locationHeader} → ${redirectUrl}`\n            });\n          }\n\n          if (preferencesUtil.shouldStoreCookies()) {\n            saveCookies(error.config.url, error.response.headers);\n          }\n\n          // Create a new request config for the redirect\n          const requestConfig = {\n            ...error.config,\n            url: redirectUrl,\n            headers: {\n              ...error.config.headers\n            }\n          };\n\n          // Apply proper HTTP redirect behavior based on status code\n          const statusCode = error.response.status;\n          const originalMethod = (error.config.method || 'get').toLowerCase();\n\n          // For 301, 302, 303: change method to GET unless it was HEAD\n          if ([301, 302, 303].includes(statusCode) && originalMethod !== 'head') {\n            requestConfig.method = 'get';\n            requestConfig.data = undefined;\n            delete requestConfig.headers['content-length'];\n            delete requestConfig.headers['Content-Length'];\n\n            delete requestConfig.headers['content-type'];\n            delete requestConfig.headers['Content-Type'];\n\n            timeline.push({\n              timestamp: new Date(),\n              type: 'info',\n              message: `Changed method from ${originalMethod.toUpperCase()} to GET for ${statusCode} redirect and removed request body`\n            });\n          } else {\n            // For 307, 308 and other status codes: preserve method and body\n            if (requestConfig.data && typeof requestConfig.data === 'object'\n              && requestConfig.data.constructor && requestConfig.data.constructor.name === 'FormData') {\n              const formData = requestConfig.data;\n              if (formData._released || (formData._streams && formData._streams.length === 0)) {\n                if (error.config._originalMultipartData && error.config.collectionPath) {\n                  timeline.push({\n                    timestamp: new Date(),\n                    type: 'info',\n                    message: `Recreating consumed FormData for ${statusCode} redirect`\n                  });\n\n                  const recreatedForm = createFormData(error.config._originalMultipartData, error.config.collectionPath);\n                  requestConfig.data = recreatedForm;\n\n                  const formHeaders = recreatedForm.getHeaders();\n                  Object.assign(requestConfig.headers, formHeaders);\n\n                  // preserve the original data for potential future redirects\n                  requestConfig._originalMultipartData = error.config._originalMultipartData;\n                  requestConfig.collectionPath = error.config.collectionPath;\n                } else {\n                  timeline.push({\n                    timestamp: new Date(),\n                    type: 'info',\n                    message: `FormData consumed but no original data available for ${statusCode} redirect`\n                  });\n                }\n              } else {\n                requestConfig._originalMultipartData = error.config._originalMultipartData;\n                requestConfig.collectionPath = error.config.collectionPath;\n              }\n            }\n          }\n\n          if (preferencesUtil.shouldSendCookies()) {\n            const cookieString = getCookieStringForUrl(redirectUrl);\n            if (cookieString && typeof cookieString === 'string' && cookieString.length) {\n              requestConfig.headers['cookie'] = cookieString;\n            }\n          }\n\n          try {\n            setupProxyAgents({\n              requestConfig,\n              proxyMode,\n              proxyConfig,\n              httpsAgentRequestFields,\n              interpolationOptions,\n              timeline\n            });\n          } catch (err) {\n            if (err.timeline) {\n              timeline = err.timeline;\n            }\n            timeline.push({\n              timestamp: new Date(),\n              type: 'error',\n              message: `Error setting up proxy agents: ${err?.message}`\n            });\n          }\n\n          requestConfig.metadata.timeline = timeline;\n          // Make the redirected request\n          return instance(requestConfig);\n        } else {\n          const errorResponseData = error.response.data;\n          timeline.push({\n            timestamp: new Date(),\n            type: 'response',\n            message: `HTTP/${error.response.httpVersion || '1.1'} ${error.response.status} ${error.response.statusText}`\n          });\n          Object.entries(error?.response?.headers || {}).forEach(([key, value]) => {\n            timeline.push({\n              timestamp: new Date(),\n              type: 'responseHeader',\n              message: `${key}: ${value}`\n            });\n          });\n          timeline?.push({\n            timestamp: new Date(),\n            type: 'error',\n            message: safeStringifyJSON(errorResponseData?.toString?.())\n          });\n          error?.cause && timeline?.push({\n            timestamp: new Date(),\n            type: 'error',\n            message: safeStringifyJSON(error?.cause)\n          });\n          error?.errors && timeline?.push({\n            timestamp: new Date(),\n            type: 'error',\n            message: safeStringifyJSON(error?.errors)\n          });\n          error.response.timeline = timeline;\n          return Promise.reject(error);\n        }\n      } else if (error?.code) {\n        Object.entries(error?.response?.headers || {}).forEach(([key, value]) => {\n          timeline.push({\n            timestamp: new Date(),\n            type: 'responseHeader',\n            message: `${key}: ${value}`\n          });\n        });\n        timeline?.push({\n          timestamp: new Date(),\n          type: 'error',\n          message: safeStringifyJSON(error?.cause)\n        });\n        timeline?.push({\n          timestamp: new Date(),\n          type: 'error',\n          message: safeStringifyJSON(error?.errors)\n        });\n        error.timeline = timeline;\n        error.statusText = error.code;\n        return Promise.reject(error);\n      }\n      return Promise.reject(error);\n    }\n  );\n\n  return instance;\n}\n\nmodule.exports = {\n  makeAxiosInstance\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/network/cert-utils.js",
    "content": "const fs = require('node:fs');\nconst path = require('path');\nconst { get } = require('lodash');\nconst { getCACertificates } = require('@usebruno/requests');\nconst { preferencesUtil } = require('../../store/preferences');\nconst { getBrunoConfig } = require('../../store/bruno-config');\nconst { getCachedSystemProxy } = require('../../store/system-proxy');\nconst { interpolateString, interpolateObject } = require('./interpolate-string');\n\n/**\n * Gets certificates and proxy configuration for a request\n */\nconst getCertsAndProxyConfig = async ({\n  collectionUid,\n  collection,\n  request,\n  envVars,\n  runtimeVariables,\n  processEnvVars,\n  collectionPath,\n  globalEnvironmentVariables\n}) => {\n  /**\n   * @see https://github.com/usebruno/bruno/issues/211 set keepAlive to true, this should fix socket hang up errors\n   * @see https://github.com/nodejs/node/pull/43522 keepAlive was changed to true globally on Node v19+\n   */\n  const httpsAgentRequestFields = { keepAlive: true };\n  if (!preferencesUtil.shouldVerifyTls()) {\n    httpsAgentRequestFields['rejectUnauthorized'] = false;\n  }\n\n  let caCertificates = '';\n  let caCertificatesCount = { system: 0, root: 0, custom: 0, extra: 0 };\n\n  // Only load CA certificates if SSL validation is enabled (otherwise they're unused)\n  if (preferencesUtil.shouldVerifyTls()) {\n    let caCertFilePath = preferencesUtil.shouldUseCustomCaCertificate() && preferencesUtil.getCustomCaCertificateFilePath();\n    let caCertificatesData = getCACertificates({\n      caCertFilePath,\n      shouldKeepDefaultCerts: preferencesUtil.shouldKeepDefaultCaCertificates()\n    });\n\n    caCertificates = caCertificatesData.caCertificates;\n    caCertificatesCount = caCertificatesData.caCertificatesCount;\n  }\n\n  // configure HTTPS agent with aggregated CA certificates\n  httpsAgentRequestFields['caCertificatesCount'] = caCertificatesCount;\n  httpsAgentRequestFields['ca'] = caCertificates || [];\n\n  const { promptVariables } = collection;\n  const collectionVariables = request.collectionVariables || {};\n  const folderVariables = request.folderVariables || {};\n  const requestVariables = request.requestVariables || {};\n\n  const brunoConfig = getBrunoConfig(collectionUid, collection);\n  const interpolationOptions = {\n    globalEnvironmentVariables,\n    collectionVariables,\n    envVars,\n    folderVariables,\n    requestVariables,\n    runtimeVariables,\n    promptVariables,\n    processEnvVars\n  };\n\n  // client certificate config\n  const clientCertConfig = get(brunoConfig, 'clientCertificates.certs', []);\n\n  for (let clientCert of clientCertConfig) {\n    const domain = interpolateString(clientCert?.domain, interpolationOptions);\n    const type = clientCert?.type || 'cert';\n    if (domain) {\n      const hostRegex = '^(https:\\\\/\\\\/|grpc:\\\\/\\\\/|grpcs:\\\\/\\\\/|ws:\\\\/\\\\/|wss:\\\\/\\\\/)?'\n        + domain.replaceAll('.', '\\\\.').replaceAll('*', '.*');\n      const requestUrl = interpolateString(request.url, interpolationOptions);\n      if (requestUrl && requestUrl.match(hostRegex)) {\n        if (type === 'cert') {\n          try {\n            let certFilePath = interpolateString(clientCert?.certFilePath, interpolationOptions);\n            certFilePath = path.isAbsolute(certFilePath) ? certFilePath : path.join(collectionPath, certFilePath);\n            let keyFilePath = interpolateString(clientCert?.keyFilePath, interpolationOptions);\n            keyFilePath = path.isAbsolute(keyFilePath) ? keyFilePath : path.join(collectionPath, keyFilePath);\n\n            httpsAgentRequestFields['cert'] = fs.readFileSync(certFilePath);\n            httpsAgentRequestFields['key'] = fs.readFileSync(keyFilePath);\n          } catch (err) {\n            console.error('Error reading cert/key file', err);\n            throw new Error('Error reading cert/key file' + err);\n          }\n        } else if (type === 'pfx') {\n          try {\n            let pfxFilePath = interpolateString(clientCert?.pfxFilePath, interpolationOptions);\n            pfxFilePath = path.isAbsolute(pfxFilePath) ? pfxFilePath : path.join(collectionPath, pfxFilePath);\n            httpsAgentRequestFields['pfx'] = fs.readFileSync(pfxFilePath);\n          } catch (err) {\n            console.error('Error reading pfx file', err);\n            throw new Error('Error reading pfx file' + err);\n          }\n        }\n        httpsAgentRequestFields['passphrase'] = interpolateString(clientCert.passphrase, interpolationOptions);\n        break;\n      }\n    }\n  }\n\n  /**\n   * Proxy configuration\n   *\n   * New format:\n   * - disabled: boolean (optional, defaults to false)\n   * - inherit: boolean (required)\n   * - config: { protocol, hostname, port, auth, bypassProxy }\n   *\n   * When collection proxy has inherit=false and disabled=false, use collection-specific proxy\n   * When collection proxy has inherit=true, inherit from global preferences\n   * When disabled=true, proxy is disabled\n   *\n   * Below logic calculates the proxyMode and proxyConfig to be used for the request\n   */\n  let proxyMode = 'off';\n  let proxyConfig = {};\n\n  const collectionProxyConfig = get(brunoConfig, 'proxy', {});\n  const collectionProxyDisabled = get(collectionProxyConfig, 'disabled', false);\n  const collectionProxyInherit = get(collectionProxyConfig, 'inherit', true);\n  const collectionProxyConfigData = get(collectionProxyConfig, 'config', collectionProxyConfig);\n\n  if (!collectionProxyDisabled && !collectionProxyInherit) {\n    // Use collection-specific proxy\n    proxyConfig = collectionProxyConfigData;\n    proxyMode = 'on';\n  } else if (!collectionProxyDisabled && collectionProxyInherit) {\n    // Inherit from global preferences\n    const globalProxy = preferencesUtil.getGlobalProxyConfig();\n    const globalDisabled = get(globalProxy, 'disabled', false);\n    const globalInherit = get(globalProxy, 'inherit', false);\n    const globalProxyConfigData = get(globalProxy, 'config', globalProxy);\n\n    if (!globalDisabled && !globalInherit) {\n      // Use global custom proxy\n      proxyConfig = globalProxyConfigData;\n      proxyMode = 'on';\n    } else if (!globalDisabled && globalInherit) {\n      // Use system proxy (cached at app startup)\n      proxyMode = 'system';\n      const systemProxyConfig = await getCachedSystemProxy();\n      proxyConfig = systemProxyConfig || { http_proxy: null, https_proxy: null, no_proxy: null, source: 'cache-miss' };\n    }\n    // else: global proxy is disabled, proxyMode stays 'off'\n  }\n  // else: collection proxy is disabled, proxyMode stays 'off'\n\n  return { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions };\n};\n\n/**\n * Builds the certsAndProxyConfig object for bru.sendRequest\n * This allows bru.sendRequest to use the same proxy/certs config as the main request\n */\nconst buildCertsAndProxyConfig = async ({\n  collectionUid,\n  collection,\n  collectionPath,\n  envVars,\n  runtimeVariables,\n  processEnvVars,\n  request\n}) => {\n  const brunoConfig = getBrunoConfig(collectionUid, collection);\n\n  // Build interpolation options (same pattern as getCertsAndProxyConfig)\n  const globalEnvironmentVariables = collection.globalEnvironmentVariables || {};\n  const { promptVariables } = collection;\n  const collectionVariables = request?.collectionVariables || {};\n  const folderVariables = request?.folderVariables || {};\n  const requestVariables = request?.requestVariables || {};\n\n  const interpolationOptions = {\n    globalEnvironmentVariables,\n    collectionVariables,\n    envVars,\n    folderVariables,\n    requestVariables,\n    runtimeVariables,\n    promptVariables,\n    processEnvVars\n  };\n\n  // Build options for getHttpHttpsAgents\n  const options = {\n    noproxy: false,\n    shouldVerifyTls: preferencesUtil.shouldVerifyTls(),\n    shouldUseCustomCaCertificate: preferencesUtil.shouldUseCustomCaCertificate(),\n    customCaCertificateFilePath: preferencesUtil.getCustomCaCertificateFilePath(),\n    shouldKeepDefaultCaCertificates: preferencesUtil.shouldKeepDefaultCaCertificates(),\n    cacheSslSession: preferencesUtil.isSslSessionCachingEnabled()\n  };\n\n  // Get client certificates from bruno config and interpolate\n  const rawClientCertificates = get(brunoConfig, 'clientCertificates');\n  const clientCertificates = rawClientCertificates\n    ? interpolateObject(rawClientCertificates, interpolationOptions)\n    : undefined;\n\n  // Get proxy config from bruno config and interpolate\n  const collectionProxyConfig = get(brunoConfig, 'proxy', {});\n  const collectionLevelProxy = interpolateObject(collectionProxyConfig, interpolationOptions);\n\n  // Get app-level proxy config from global preferences\n  const appLevelProxyConfig = preferencesUtil.getGlobalProxyConfig();\n\n  // Get system proxy config\n  const systemProxyConfig = await getCachedSystemProxy();\n\n  return {\n    collectionPath,\n    options,\n    clientCertificates,\n    collectionLevelProxy,\n    appLevelProxyConfig,\n    systemProxyConfig\n  };\n};\n\nmodule.exports = { getCertsAndProxyConfig, buildCertsAndProxyConfig };\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/network/grpc-event-handlers.js",
    "content": "// To implement grpc event handlers\nconst { ipcMain, app } = require('electron');\nconst { GrpcClient } = require('@usebruno/requests');\nconst { safeParseJSON, safeStringifyJSON } = require('../../utils/common');\nconst { cloneDeep, get } = require('lodash');\nconst { preferencesUtil } = require('../../store/preferences');\nconst { getCertsAndProxyConfig } = require('./cert-utils');\nconst { interpolateString } = require('./interpolate-string');\nconst path = require('node:path');\nconst prepareGrpcRequest = require('./prepare-grpc-request');\nconst { normalizeAndResolvePath } = require('../../utils/filesystem');\nconst { configureRequest } = require('./prepare-grpc-request');\n\n// Creating grpcClient at module level so it can be accessed from window-all-closed event\nlet grpcClient;\n\n/**\n * Extract protobuf include directories from collection config\n * @param {Object} collection - The collection object\n * @returns {string[]} Array of resolved include directory paths\n */\nconst getProtobufIncludeDirs = (collection) => {\n  if (!collection) {\n    return [];\n  }\n\n  const brunoConfig = collection.draft?.brunoConfig || collection.brunoConfig;\n  const importPaths = brunoConfig?.protobuf?.importPaths ?? [];\n\n  return importPaths\n    .filter(({ enabled }) => Boolean(enabled))\n    .map(({ path: relativePath }) => normalizeAndResolvePath(path.resolve(collection.pathname, relativePath)));\n};\n\n/**\n * Register IPC handlers for gRPC\n */\nconst registerGrpcEventHandlers = (window) => {\n  const sendEvent = (eventName, ...args) => {\n    if (window && !window.isDestroyed() && window.webContents && !window.webContents.isDestroyed()) {\n      window.webContents.send(eventName, ...args);\n    } else {\n      console.warn(`Unable to send message \"${eventName}\": Window not available`);\n    }\n  };\n\n  grpcClient = new GrpcClient(sendEvent);\n\n  ipcMain.handle('connections-changed', (event) => {\n    sendEvent('grpc:connections-changed', event);\n  });\n\n  // Start a new gRPC connection\n  ipcMain.handle('grpc:start-connection', async (event, { request, collection, environment, runtimeVariables }) => {\n    try {\n      const requestCopy = cloneDeep(request);\n      const preparedRequest = await prepareGrpcRequest(requestCopy, collection, environment, runtimeVariables, {});\n\n      const protocolRegex = /^([-+\\w]{1,25})(:?\\/\\/|:)/;\n      if (!protocolRegex.test(preparedRequest.url)) {\n        preparedRequest.url = `http://${preparedRequest.url}`;\n      }\n\n      // Get certificates and proxy configuration\n      const certsAndProxyConfig = await getCertsAndProxyConfig({\n        collectionUid: collection.uid,\n        collection,\n        request: preparedRequest,\n        envVars: preparedRequest.envVars,\n        runtimeVariables,\n        processEnvVars: preparedRequest.processEnvVars,\n        collectionPath: collection.pathname,\n        globalEnvironmentVariables: collection.globalEnvironmentVariables\n      });\n\n      await configureRequest(\n        preparedRequest,\n        requestCopy,\n        collection,\n        preparedRequest.envVars,\n        runtimeVariables,\n        preparedRequest.processEnvVars,\n        preparedRequest.promptVariables,\n        certsAndProxyConfig\n      );\n\n      // Extract certificate information from the config\n      const { httpsAgentRequestFields } = certsAndProxyConfig;\n\n      // Configure verify options\n      const verifyOptions = {\n        rejectUnauthorized: preferencesUtil.shouldVerifyTls()\n      };\n\n      // Extract certificate information\n      const rootCertificate = httpsAgentRequestFields.ca;\n      const privateKey = httpsAgentRequestFields.key;\n      const certificateChain = httpsAgentRequestFields.cert;\n      const passphrase = httpsAgentRequestFields.passphrase;\n      const pfx = httpsAgentRequestFields.pfx;\n\n      const requestSent = {\n        type: 'request',\n        url: preparedRequest.url,\n        method: preparedRequest.method,\n        methodType: preparedRequest.methodType,\n        headers: preparedRequest.headers,\n        body: preparedRequest.body,\n        timestamp: Date.now()\n      };\n\n      const includeDirs = getProtobufIncludeDirs(collection);\n\n      // Start gRPC connection with the processed request and certificates\n      await grpcClient.startConnection({\n        request: preparedRequest,\n        collection,\n        rootCertificate,\n        privateKey,\n        certificateChain,\n        passphrase,\n        pfx,\n        verifyOptions,\n        includeDirs\n      });\n\n      sendEvent('grpc:request', preparedRequest.uid, collection.uid, requestSent);\n\n      // Send OAuth credentials update if available\n      if (preparedRequest?.oauth2Credentials) {\n        window.webContents.send('main:credentials-update', {\n          credentials: preparedRequest.oauth2Credentials?.credentials,\n          url: preparedRequest.oauth2Credentials?.url,\n          collectionUid: collection.uid,\n          credentialsId: preparedRequest.oauth2Credentials?.credentialsId,\n          ...(preparedRequest.oauth2Credentials?.folderUid ? { folderUid: preparedRequest.oauth2Credentials.folderUid } : { itemUid: preparedRequest.uid }),\n          debugInfo: preparedRequest.oauth2Credentials.debugInfo\n        });\n      }\n\n      return { success: true };\n    } catch (error) {\n      console.error('Error starting gRPC connection:', error);\n      if (error instanceof Error) {\n        throw error;\n      }\n      sendEvent('grpc:error', request.uid, collection.uid, { error: error.message });\n      return { success: false, error: error.message };\n    }\n  });\n\n  // Get all active connection IDs\n  ipcMain.handle('grpc:get-active-connections', (event) => {\n    try {\n      const activeConnectionIds = grpcClient.getActiveConnectionIds();\n      return { success: true, activeConnectionIds };\n    } catch (error) {\n      console.error('Error getting active connections:', error);\n      return { success: false, error: error.message, activeConnectionIds: [] };\n    }\n  });\n\n  // Send a message to an existing stream\n  ipcMain.handle('grpc:send-message', (event, requestId, collectionUid, message) => {\n    try {\n      grpcClient.sendMessage(requestId, collectionUid, message);\n      sendEvent('grpc:message', requestId, collectionUid, message);\n      return { success: true };\n    } catch (error) {\n      console.error('Error sending gRPC message:', error);\n      return { success: false, error: error.message };\n    }\n  });\n\n  // End a streaming request\n  ipcMain.handle('grpc:end-request', (event, params) => {\n    try {\n      const { requestId } = params || {};\n      if (!requestId) {\n        throw new Error('Request ID is required');\n      }\n      grpcClient.end(requestId);\n      return { success: true };\n    } catch (error) {\n      console.error('Error ending gRPC stream:', error);\n      return { success: false, error: error.message };\n    }\n  });\n\n  // Cancel a request\n  ipcMain.handle('grpc:cancel-request', (event, params) => {\n    try {\n      const { requestId } = params || {};\n      if (!requestId) {\n        throw new Error('Request ID is required');\n      }\n      grpcClient.cancel(requestId);\n      return { success: true };\n    } catch (error) {\n      console.error('Error cancelling gRPC request:', error);\n      return { success: false, error: error.message };\n    }\n  });\n\n  // Load methods from server reflection\n  ipcMain.handle('grpc:load-methods-reflection', async (event, { request, collection, environment, runtimeVariables }) => {\n    try {\n      const requestCopy = cloneDeep(request);\n      const preparedRequest = await prepareGrpcRequest(requestCopy, collection, environment, runtimeVariables);\n\n      const protocolRegex = /^([-+\\w]{1,25})(:?\\/\\/|:)/;\n      if (!protocolRegex.test(preparedRequest.url)) {\n        preparedRequest.url = `http://${preparedRequest.url}`;\n      }\n\n      // Get certificates and proxy configuration\n      const certsAndProxyConfig = await getCertsAndProxyConfig({\n        collectionUid: collection.uid,\n        collection,\n        request: preparedRequest,\n        envVars: preparedRequest.envVars,\n        runtimeVariables,\n        processEnvVars: preparedRequest.processEnvVars,\n        collectionPath: collection.pathname,\n        globalEnvironmentVariables: collection.globalEnvironmentVariables\n      });\n\n      await configureRequest(\n        preparedRequest,\n        requestCopy,\n        collection,\n        preparedRequest.envVars,\n        runtimeVariables,\n        preparedRequest.processEnvVars,\n        preparedRequest.promptVariables,\n        certsAndProxyConfig\n      );\n\n      // Extract certificate information from the config\n      const { httpsAgentRequestFields } = certsAndProxyConfig;\n\n      // Configure verify options\n      const verifyOptions = {\n        rejectUnauthorized: preferencesUtil.shouldVerifyTls()\n      };\n\n      // Extract certificate information\n      const rootCertificate = httpsAgentRequestFields.ca;\n      const privateKey = httpsAgentRequestFields.key;\n      const certificateChain = httpsAgentRequestFields.cert;\n      const passphrase = httpsAgentRequestFields.passphrase;\n      const pfx = httpsAgentRequestFields.pfx;\n\n      // Send OAuth credentials update if available\n      if (preparedRequest?.oauth2Credentials) {\n        window.webContents.send('main:credentials-update', {\n          credentials: preparedRequest.oauth2Credentials?.credentials,\n          url: preparedRequest.oauth2Credentials?.url,\n          collectionUid: collection.uid,\n          credentialsId: preparedRequest.oauth2Credentials?.credentialsId,\n          ...(preparedRequest.oauth2Credentials?.folderUid ? { folderUid: preparedRequest.oauth2Credentials.folderUid } : { itemUid: preparedRequest.uid }),\n          debugInfo: preparedRequest.oauth2Credentials.debugInfo\n        });\n      }\n\n      const methods = await grpcClient.loadMethodsFromReflection({\n        request: preparedRequest,\n        collectionUid: collection.uid,\n        rootCertificate,\n        privateKey,\n        certificateChain,\n        passphrase,\n        pfx,\n        verifyOptions,\n        sendEvent\n      });\n\n      return { success: true, methods: safeParseJSON(safeStringifyJSON(methods)) };\n    } catch (error) {\n      console.error('Error loading gRPC methods from reflection:', error);\n      return { success: false, error: error.message };\n    }\n  });\n\n  // Load methods from proto file\n  ipcMain.handle('grpc:load-methods-proto', async (event, { filePath, collection }) => {\n    try {\n      const includeDirs = getProtobufIncludeDirs(collection);\n\n      const methods = await grpcClient.loadMethodsFromProtoFile(filePath, includeDirs);\n      return { success: true, methods: safeParseJSON(safeStringifyJSON(methods)) };\n    } catch (error) {\n      console.error('Error loading gRPC methods from proto file:', error);\n      return { success: false, error: error.message };\n    }\n  });\n\n  // Generate a sample gRPC message based on method path\n  ipcMain.handle('grpc:generate-sample-message', async (event, { methodPath, existingMessage, options = {} }) => {\n    try {\n      // Generate the sample message\n      const result = grpcClient.generateSampleMessage(methodPath, {\n        ...options,\n        // Parse existing message if provided\n        existingMessage: existingMessage ? safeParseJSON(existingMessage) : null\n      });\n\n      if (!result.success) {\n        return {\n          success: false,\n          error: result.error || 'Failed to generate sample message'\n        };\n      }\n\n      // Convert the message to a JSON string for safe transfer through IPC\n      return {\n        success: true,\n        message: JSON.stringify(result.message, null, 2)\n      };\n    } catch (error) {\n      console.error('Error generating gRPC sample message:', error);\n      return {\n        success: false,\n        error: error.message || 'Failed to generate sample message'\n      };\n    }\n  });\n\n  // Generate grpcurl command for a request\n  ipcMain.handle('grpc:generate-grpcurl', async (event, { request, collection, environment, runtimeVariables }) => {\n    try {\n      const requestCopy = cloneDeep(request);\n      const preparedRequest = await prepareGrpcRequest(requestCopy, collection, environment, runtimeVariables, {});\n\n      const protocolRegex = /^([-+\\w]{1,25})(:?\\/\\/|:)/;\n      if (!protocolRegex.test(preparedRequest.url)) {\n        preparedRequest.url = `http://${preparedRequest.url}`;\n      }\n\n      const interpolationOptions = {\n        envVars: preparedRequest.envVars,\n        runtimeVariables,\n        processEnvVars: preparedRequest.processEnvVars\n      };\n      let caCertFilePath, certFilePath, keyFilePath;\n\n      if (preferencesUtil.shouldUseCustomCaCertificate()) {\n        caCertFilePath = preferencesUtil.getCustomCaCertificateFilePath();\n      }\n\n      const clientCertConfig = collection.draft?.brunoConfig ? get(collection, 'draft.brunoConfig.clientCertificates.certs', []) : get(collection, 'brunoConfig.clientCertificates.certs', []);\n\n      for (let clientCert of clientCertConfig) {\n        const domain = interpolateString(clientCert?.domain, interpolationOptions);\n        const type = clientCert?.type || 'cert';\n        if (domain) {\n          const hostRegex = '^(https:\\\\/\\\\/|grpc:\\\\/\\\\/|grpcs:\\\\/\\\\/)' + domain.replaceAll('.', '\\\\.').replaceAll('*', '.*');\n          const requestUrl = interpolateString(preparedRequest.url, interpolationOptions);\n          if (requestUrl.match(hostRegex)) {\n            if (type === 'cert') {\n              certFilePath = interpolateString(clientCert?.certFilePath, interpolationOptions);\n              certFilePath = path.isAbsolute(certFilePath) ? certFilePath : path.join(collection.pathname, certFilePath);\n              keyFilePath = interpolateString(clientCert?.keyFilePath, interpolationOptions);\n              keyFilePath = path.isAbsolute(keyFilePath) ? keyFilePath : path.join(collection.pathname, keyFilePath);\n            }\n          }\n        }\n      }\n      // Generate the grpcurl command\n      const command = grpcClient.generateGrpcurlCommand({\n        request: preparedRequest,\n        collectionPath: collection.pathname,\n        certificates: {\n          ca: caCertFilePath,\n          cert: certFilePath,\n          key: keyFilePath\n        }\n      });\n\n      return { success: true, command };\n    } catch (error) {\n      console.error('Error generating grpcurl command:', error);\n      return { success: false, error: error.message };\n    }\n  });\n};\n\n// Clean up gRPC connections when all windows are closed\nif (app && typeof app.on === 'function') {\n  app.on('window-all-closed', () => {\n    if (grpcClient && typeof grpcClient.clearAllConnections === 'function') {\n      try {\n        grpcClient.clearAllConnections();\n      } catch (error) {\n        console.error('Error clearing gRPC connections:', error);\n      }\n    }\n  });\n}\n\nmodule.exports = registerGrpcEventHandlers;\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/network/index.js",
    "content": "const https = require('https');\nconst axios = require('axios');\nconst path = require('path');\nconst qs = require('qs');\nconst decomment = require('decomment');\nconst contentDispositionParser = require('content-disposition');\nconst mime = require('mime-types');\nconst { ipcMain } = require('electron');\nconst { each, get, extend, cloneDeep, merge } = require('lodash');\nconst { NtlmClient } = require('axios-ntlm');\nconst { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js');\nconst { encodeUrl } = require('@usebruno/common').utils;\nconst { extractPromptVariables } = require('@usebruno/common').utils;\nconst { interpolateString } = require('./interpolate-string');\nconst { resolveAwsV4Credentials, addAwsV4Interceptor } = require('./awsv4auth-helper');\nconst { addDigestInterceptor } = require('@usebruno/requests');\nconst prepareGqlIntrospectionRequest = require('./prepare-gql-introspection-request');\nconst { prepareRequest } = require('./prepare-request');\nconst interpolateVars = require('./interpolate-vars');\nconst { makeAxiosInstance } = require('./axios-instance');\nconst { resolveInheritedSettings } = require('../../utils/collection');\nconst { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token');\nconst { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse, parseDataFromRequest } = require('../../utils/common');\nconst { chooseFileToSave, writeFile, getCollectionFormat, hasRequestExtension } = require('../../utils/filesystem');\nconst { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies');\nconst { createFormData } = require('../../utils/form-data');\nconst { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars, getTreePathFromCollectionToItem, mergeVars, sortByNameThenSequence } = require('../../utils/collection');\nconst { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, getOAuth2TokenUsingImplicitGrant, updateCollectionOauth2Credentials, clearOauth2CredentialsByCredentialsId } = require('../../utils/oauth2');\nconst { preferencesUtil } = require('../../store/preferences');\nconst { getProcessEnvVars } = require('../../store/process-env');\nconst { getBrunoConfig } = require('../../store/bruno-config');\nconst Oauth2Store = require('../../store/oauth2');\nconst { isRequestTagsIncluded } = require('@usebruno/common');\nconst { cookiesStore } = require('../../store/cookies');\nconst registerGrpcEventHandlers = require('./grpc-event-handlers');\nconst { registerWsEventHandlers } = require('./ws-event-handlers');\nconst { getCertsAndProxyConfig, buildCertsAndProxyConfig } = require('./cert-utils');\nconst { buildFormUrlEncodedPayload, isFormData } = require('@usebruno/common').utils;\n\nconst ERROR_OCCURRED_WHILE_EXECUTING_REQUEST = 'Error occurred while executing the request!';\n\nconst saveCookies = (url, headers) => {\n  if (preferencesUtil.shouldStoreCookies()) {\n    let setCookieHeaders = [];\n    if (headers['set-cookie']) {\n      setCookieHeaders = Array.isArray(headers['set-cookie'])\n        ? headers['set-cookie']\n        : [headers['set-cookie']];\n      for (let setCookieHeader of setCookieHeaders) {\n        if (typeof setCookieHeader === 'string' && setCookieHeader.length) {\n          addCookieToJar(setCookieHeader, url);\n        }\n      }\n    }\n  }\n};\n\nconst getJsSandboxRuntime = (collection) => {\n  const securityConfig = get(collection, 'securityConfig', {});\n\n  if (securityConfig.jsSandboxMode === 'developer') {\n    return 'nodevm';\n  }\n\n  // default runtime is `quickjs`\n  return 'quickjs';\n};\n\nconst hasStreamHeaders = (headers) => {\n  const headerSplit = (headers.get('content-type') ?? '').split(';').map((d) => d.trim());\n  return headerSplit.indexOf('text/event-stream') > -1;\n};\n\nconst promisifyStream = async (stream, abortController, closeOnFirst) => {\n  const chunks = [];\n\n  return new Promise((resolve, reject) => {\n    const doResolve = () => {\n      const fullBuffer = Buffer.concat(chunks);\n      resolve(fullBuffer.buffer.slice(fullBuffer.byteOffset, fullBuffer.byteOffset + fullBuffer.byteLength));\n    };\n\n    stream.on('data', (chunk) => {\n      chunks.push(chunk);\n\n      if (closeOnFirst) {\n        doResolve();\n\n        if (abortController) {\n          abortController.abort();\n        }\n      }\n    });\n\n    stream.on('close', doResolve);\n    stream.on('error', (err) => reject(err));\n  });\n};\n\nconst configureRequest = async (\n  collectionUid,\n  collection,\n  request,\n  envVars,\n  runtimeVariables,\n  processEnvVars,\n  collectionPath,\n  globalEnvironmentVariables\n) => {\n  const protocolRegex = /^([-+\\w]{1,25})(:?\\/\\/|:)/;\n  const hasVariables = request.url.startsWith('{{');\n  if (!hasVariables && !protocolRegex.test(request.url)) {\n    request.url = `http://${request.url}`;\n  }\n\n  const certsAndProxyConfig = await getCertsAndProxyConfig({\n    collectionUid,\n    collection,\n    request,\n    envVars,\n    runtimeVariables,\n    processEnvVars,\n    collectionPath,\n    globalEnvironmentVariables\n  });\n\n  // Get followRedirects setting, default to true for backward compatibility\n  const followRedirects = request.settings?.followRedirects ?? true;\n\n  // Get maxRedirects from request settings, fallback to request.maxRedirects, then default to 5\n  let requestMaxRedirects = request.settings?.maxRedirects ?? request.maxRedirects ?? 5;\n\n  // Ensure it's a valid number\n  if (typeof requestMaxRedirects !== 'number' || requestMaxRedirects < 0) {\n    requestMaxRedirects = 5; // Default to 5 redirects\n  }\n\n  // If followRedirects is disabled, set maxRedirects to 0 to disable all redirects\n  if (!followRedirects) {\n    requestMaxRedirects = 0;\n  }\n\n  request.maxRedirects = 0;\n\n  const { promptVariables = {} } = collection;\n  let { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions } = certsAndProxyConfig;\n  let axiosInstance = makeAxiosInstance({\n    proxyMode,\n    proxyConfig,\n    requestMaxRedirects,\n    httpsAgentRequestFields,\n    interpolationOptions,\n    followRedirects\n  });\n\n  if (request.ntlmConfig) {\n    axiosInstance = NtlmClient(request.ntlmConfig, axiosInstance.defaults);\n    delete request.ntlmConfig;\n  }\n\n  if (request.oauth2) {\n    let requestCopy = cloneDeep(request);\n    const { oauth2: { grantType, tokenPlacement, tokenHeaderPrefix, tokenQueryKey, tokenSource, accessTokenUrl, refreshTokenUrl } = {}, collectionVariables, folderVariables, requestVariables } = requestCopy || {};\n\n    // Get cert/proxy configs for token and refresh URLs\n    let certsAndProxyConfigForTokenUrl = certsAndProxyConfig;\n    let certsAndProxyConfigForRefreshUrl = certsAndProxyConfig;\n\n    if (accessTokenUrl && grantType !== 'implicit') {\n      const interpolatedTokenUrl = interpolateString(accessTokenUrl, {\n        globalEnvironmentVariables,\n        collectionVariables,\n        envVars,\n        folderVariables,\n        requestVariables,\n        runtimeVariables,\n        processEnvVars,\n        promptVariables\n      });\n      const tokenRequestForConfig = { ...requestCopy, url: interpolatedTokenUrl };\n      certsAndProxyConfigForTokenUrl = await getCertsAndProxyConfig({\n        collectionUid,\n        collection,\n        request: tokenRequestForConfig,\n        envVars,\n        runtimeVariables,\n        processEnvVars,\n        collectionPath,\n        globalEnvironmentVariables\n      });\n    }\n\n    const tokenUrlForRefresh = refreshTokenUrl || accessTokenUrl;\n    if (tokenUrlForRefresh && grantType !== 'implicit') {\n      const interpolatedRefreshUrl = interpolateString(tokenUrlForRefresh, {\n        globalEnvironmentVariables,\n        collectionVariables,\n        envVars,\n        folderVariables,\n        requestVariables,\n        runtimeVariables,\n        processEnvVars,\n        promptVariables\n      });\n      const refreshRequestForConfig = { ...requestCopy, url: interpolatedRefreshUrl };\n      certsAndProxyConfigForRefreshUrl = await getCertsAndProxyConfig({\n        collectionUid,\n        collection,\n        request: refreshRequestForConfig,\n        envVars,\n        runtimeVariables,\n        processEnvVars,\n        collectionPath,\n        globalEnvironmentVariables\n      });\n    }\n\n    let credentials, credentialsId, oauth2Url, debugInfo;\n    switch (grantType) {\n      case 'authorization_code':\n        interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars, promptVariables);\n        ({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingAuthorizationCode({ request: requestCopy, collectionUid, certsAndProxyConfigForTokenUrl, certsAndProxyConfigForRefreshUrl }));\n        request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };\n        {\n          const tokenValue = tokenSource === 'id_token' ? credentials?.id_token : credentials?.access_token;\n          if (tokenPlacement == 'header' && tokenValue) {\n            request.headers['Authorization'] = `${tokenHeaderPrefix} ${tokenValue}`.trim();\n          } else if (tokenValue) {\n            try {\n              const url = new URL(request.url);\n              url.searchParams.set(tokenQueryKey, tokenValue);\n              request.url = url.toString();\n            } catch (error) {}\n          }\n        }\n        break;\n      case 'implicit':\n        interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars, promptVariables);\n        ({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingImplicitGrant({ request: requestCopy, collectionUid }));\n        request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };\n        {\n          const tokenValue = tokenSource === 'id_token' ? credentials?.id_token : credentials?.access_token;\n          if (tokenPlacement == 'header' && tokenValue) {\n            request.headers['Authorization'] = `${tokenHeaderPrefix} ${tokenValue}`.trim();\n          } else if (tokenValue) {\n            try {\n              const url = new URL(request.url);\n              url.searchParams.set(tokenQueryKey, tokenValue);\n              request.url = url.toString();\n            } catch (error) {}\n          }\n        }\n        break;\n      case 'client_credentials':\n        interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars, promptVariables);\n        ({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingClientCredentials({ request: requestCopy, collectionUid, certsAndProxyConfigForTokenUrl, certsAndProxyConfigForRefreshUrl }));\n        request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };\n        {\n          const tokenValue = tokenSource === 'id_token' ? credentials?.id_token : credentials?.access_token;\n          if (tokenPlacement == 'header' && tokenValue) {\n            request.headers['Authorization'] = `${tokenHeaderPrefix} ${tokenValue}`.trim();\n          } else if (tokenValue) {\n            try {\n              const url = new URL(request.url);\n              url.searchParams.set(tokenQueryKey, tokenValue);\n              request.url = url.toString();\n            } catch (error) {}\n          }\n        }\n        break;\n      case 'password':\n        interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars, promptVariables);\n        ({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingPasswordCredentials({ request: requestCopy, collectionUid, certsAndProxyConfigForTokenUrl, certsAndProxyConfigForRefreshUrl }));\n        request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };\n        {\n          const tokenValue = tokenSource === 'id_token' ? credentials?.id_token : credentials?.access_token;\n          if (tokenPlacement == 'header' && tokenValue) {\n            request.headers['Authorization'] = `${tokenHeaderPrefix} ${tokenValue}`.trim();\n          } else if (tokenValue) {\n            try {\n              const url = new URL(request.url);\n              url.searchParams.set(tokenQueryKey, tokenValue);\n              request.url = url.toString();\n            } catch (error) {}\n          }\n        }\n        break;\n    }\n  }\n\n  if (request.awsv4config) {\n    request.awsv4config = await resolveAwsV4Credentials(request);\n    addAwsV4Interceptor(axiosInstance, request);\n    delete request.awsv4config;\n  }\n\n  if (request.digestConfig) {\n    addDigestInterceptor(axiosInstance, request);\n  }\n\n  // Get timeout from request settings, fallback to global preference\n  const resolvedSettings = resolveInheritedSettings(request.settings || {});\n  request.timeout = resolvedSettings.timeout;\n\n  // add cookies to request\n  if (preferencesUtil.shouldSendCookies()) {\n    const cookieString = getCookieStringForUrl(request.url);\n    if (cookieString && typeof cookieString === 'string' && cookieString.length) {\n      const existingCookieHeaderName = Object.keys(request.headers).find(\n        (name) => name.toLowerCase() === 'cookie'\n      );\n      const existingCookieString = existingCookieHeaderName ? request.headers[existingCookieHeaderName] : '';\n\n      // Helper function to parse cookies into an object\n      const parseCookies = (str) => str.split(';').reduce((cookies, cookie) => {\n        const [name, ...rest] = cookie.split('=');\n        if (name && name.trim()) {\n          cookies[name.trim()] = rest.join('=').trim();\n        }\n        return cookies;\n      }, {});\n\n      const mergedCookies = {\n        ...parseCookies(existingCookieString),\n        ...parseCookies(cookieString)\n      };\n\n      const combinedCookieString = Object.entries(mergedCookies)\n        .map(([name, value]) => `${name}=${value}`)\n        .join('; ');\n\n      request.headers[existingCookieHeaderName || 'Cookie'] = combinedCookieString;\n    }\n  }\n\n  // Add API key to the URL\n  if (request.apiKeyAuthValueForQueryParams && request.apiKeyAuthValueForQueryParams.placement === 'queryparams') {\n    const urlObj = new URL(request.url);\n\n    // Interpolate key and value as they can be variables before adding to the URL.\n    const key = interpolateString(request.apiKeyAuthValueForQueryParams.key, interpolationOptions);\n    const value = interpolateString(request.apiKeyAuthValueForQueryParams.value, interpolationOptions);\n\n    urlObj.searchParams.set(key, value);\n    request.url = urlObj.toString();\n  }\n\n  // Remove apiKeyAuthValueForQueryParams, already interpolated and added to URL\n  delete request.apiKeyAuthValueForQueryParams;\n\n  return axiosInstance;\n};\n\nconst fetchGqlSchemaHandler = async (event, endpoint, environment, _request, collection) => {\n  try {\n    const requestTreePath = getTreePathFromCollectionToItem(collection, _request);\n    // Create a clone of the request to avoid mutating the original\n    const resolvedRequest = cloneDeep(_request);\n    // mergeVars modifies the request in place, but we'll assign it to ensure consistency\n    mergeVars(collection, resolvedRequest, requestTreePath);\n    const envVars = getEnvVars(environment);\n\n    const globalEnvironmentVars = collection.globalEnvironmentVariables;\n    const folderVars = resolvedRequest.folderVariables;\n    const requestVariables = resolvedRequest.requestVariables;\n    const collectionVariables = resolvedRequest.collectionVariables;\n    const runtimeVars = collection.runtimeVariables;\n\n    // Precedence: runtimeVars > requestVariables > folderVars > envVars > collectionVariables > globalEnvironmentVars\n    const processEnvVars = getProcessEnvVars(collection.uid);\n    const resolvedVars = merge(\n      {},\n      globalEnvironmentVars,\n      collectionVariables,\n      envVars,\n      folderVars,\n      requestVariables,\n      runtimeVars,\n      {\n        process: {\n          env: {\n            ...processEnvVars\n          }\n        }\n      }\n    );\n\n    const collectionRoot = collection?.draft?.root || collection?.root || {};\n    const request = prepareGqlIntrospectionRequest(endpoint, resolvedVars, _request, collectionRoot);\n\n    // Get timeout from request settings, resolve inheritance if needed\n    const resolvedSettings = resolveInheritedSettings(request.settings || {});\n    request.timeout = resolvedSettings.timeout;\n\n    if (!preferencesUtil.shouldVerifyTls()) {\n      request.httpsAgent = new https.Agent({\n        rejectUnauthorized: false\n      });\n    }\n\n    const collectionPath = collection.pathname;\n\n    const axiosInstance = await configureRequest(\n      collection.uid,\n      collection,\n      request,\n      envVars,\n      collection.runtimeVariables,\n      processEnvVars,\n      collectionPath,\n      collection.globalEnvironmentVariables\n    );\n\n    const response = await axiosInstance(request);\n\n    return {\n      status: response.status,\n      statusText: response.statusText,\n      headers: response.headers,\n      data: response.data\n    };\n  } catch (error) {\n    if (error.response) {\n      return {\n        status: error.response.status,\n        statusText: error.response.statusText,\n        headers: error.response.headers,\n        data: error.response.data\n      };\n    }\n\n    return Promise.reject(error);\n  }\n};\n\nconst registerNetworkIpc = (mainWindow) => {\n  const onConsoleLog = (type, args) => {\n    console[type](...args);\n\n    mainWindow.webContents.send('main:console-log', {\n      type,\n      args\n    });\n  };\n\n  const notifyScriptExecution = ({\n    channel, // 'main:run-request-event' | 'main:run-folder-event'\n    basePayload, // request-level or runner-level identifiers\n    scriptType, // 'pre-request' | 'post-response' | 'test'\n    error // optional Error\n  }) => {\n    mainWindow.webContents.send(channel, {\n      type: `${scriptType}-script-execution`,\n      ...basePayload,\n      errorMessage: error ? (error.message || `An error occurred in ${scriptType.replace('-', ' ')} script`) : null\n    });\n  };\n\n  const appendScriptErrorResult = (scriptType, scriptResult, error) => {\n    if (!error) {\n      return scriptResult;\n    }\n\n    const descriptionMap = {\n      'test': 'Test Script Error',\n      'post-response': 'Post-Response Script Error',\n      'pre-request': 'Pre-Request Script Error'\n    };\n\n    const messageMap = {\n      'test': 'An error occurred while executing the test script.',\n      'post-response': 'An error occurred while executing the post-response script.',\n      'pre-request': 'An error occurred while executing the pre-request script.'\n    };\n\n    const results = [\n      ...(scriptResult?.results || []),\n      {\n        status: 'fail',\n        description: descriptionMap[scriptType] || 'Script Error',\n        error: error.message || messageMap[scriptType] || 'An error occurred while executing the script.',\n        isScriptError: true\n      }\n    ];\n\n    return {\n      ...(scriptResult || {}),\n      results\n    };\n  };\n\n  const resetOauth2Credentials = ({ oauth2CredentialsToReset, request, collectionUid }) => {\n    if (!oauth2CredentialsToReset?.length) return;\n    for (const credentialId of oauth2CredentialsToReset) {\n      clearOauth2CredentialsByCredentialsId({ collectionUid, credentialsId: credentialId });\n      if (request?.oauth2Credentials?.credentialsId === credentialId) {\n        request.oauth2Credentials = null;\n      }\n      const prefix = `$oauth2.${credentialId}.`;\n      if (request.oauth2CredentialVariables) {\n        for (const key of Object.keys(request.oauth2CredentialVariables)) {\n          if (key.startsWith(prefix)) {\n            delete request.oauth2CredentialVariables[key];\n          }\n        }\n      }\n      mainWindow.webContents.send('main:credentials-clear', { collectionUid, credentialsId: credentialId });\n    }\n  };\n\n  const runPreRequest = async (\n    request,\n    requestUid,\n    envVars,\n    collectionPath,\n    collection,\n    collectionUid,\n    runtimeVariables,\n    processEnvVars,\n    scriptingConfig,\n    runRequestByItemPathname\n  ) => {\n    // run pre-request script\n    let scriptResult;\n    const { promptVariables = {}, name: collectionName } = collection;\n\n    const requestScript = get(request, 'script.req');\n    if (requestScript?.length) {\n      const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });\n      scriptResult = await scriptRuntime.runRequestScript(\n        decomment(requestScript, { space: true }),\n        request,\n        envVars,\n        runtimeVariables,\n        collectionPath,\n        onConsoleLog,\n        processEnvVars,\n        scriptingConfig,\n        runRequestByItemPathname,\n        collectionName\n      );\n\n      mainWindow.webContents.send('main:script-environment-update', {\n        envVariables: scriptResult.envVariables,\n        runtimeVariables: scriptResult.runtimeVariables,\n        persistentEnvVariables: scriptResult.persistentEnvVariables,\n        requestUid,\n        collectionUid\n      });\n\n      mainWindow.webContents.send('main:persistent-env-variables-update', {\n        persistentEnvVariables: scriptResult.persistentEnvVariables,\n        collectionUid\n      });\n\n      mainWindow.webContents.send('main:global-environment-variables-update', {\n        globalEnvironmentVariables: scriptResult.globalEnvironmentVariables\n      });\n\n      collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables;\n\n      resetOauth2Credentials({ oauth2CredentialsToReset: scriptResult.oauth2CredentialsToReset, request, collectionUid });\n\n      const domainsWithCookies = await getDomainsWithCookies();\n      mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));\n    }\n\n    // interpolate variables inside request\n    interpolateVars(request, envVars, runtimeVariables, processEnvVars, promptVariables);\n\n    if (request.settings?.encodeUrl) {\n      request.url = encodeUrl(request.url);\n    }\n\n    // if this is a graphql request, parse the variables, only after interpolation\n    // https://github.com/usebruno/bruno/issues/884\n    if (request.mode === 'graphql' && typeof request.data?.variables === 'string') {\n      try {\n        request.data.variables = JSON.parse(request.data.variables);\n      } catch (err) {\n        throw new Error(`Failed to parse GraphQL variables: ${err.message}`);\n      }\n    }\n\n    // stringify the request url encoded params\n    const contentTypeHeader = Object.keys(request.headers).find((name) => name.toLowerCase() === 'content-type');\n\n    if (contentTypeHeader && request.headers[contentTypeHeader] === 'application/x-www-form-urlencoded') {\n      if (Array.isArray(request.data)) {\n        request.data = buildFormUrlEncodedPayload(request.data);\n      } else if (typeof request.data !== 'string') {\n        request.data = qs.stringify(request.data, { arrayFormat: 'repeat' });\n      }\n      // if `data` is of string type - return as-is (assumes already encoded)\n    }\n\n    const contentType = contentTypeHeader ? request.headers[contentTypeHeader] : '';\n    if (typeof contentType === 'string' && contentType.startsWith('multipart/')) {\n      if (!isFormData(request.data)) {\n        request._originalMultipartData = request.data;\n        request.collectionPath = collectionPath;\n        let form = createFormData(request.data, collectionPath);\n        request.data = form;\n        if (contentType !== 'multipart/form-data') {\n          // Patch: Axios leverages getHeaders method to get the headers so FormData should be monkey patched\n          const formHeaders = form.getHeaders();\n          formHeaders['content-type'] = `${contentType}; boundary=${form.getBoundary()}`;\n          form.getHeaders = function () {\n            return formHeaders;\n          };\n        }\n\n        extend(request.headers, form.getHeaders());\n      }\n    }\n\n    return scriptResult;\n  };\n\n  const runPostResponse = async (\n    request,\n    response,\n    requestUid,\n    envVars,\n    collectionPath,\n    collection,\n    collectionUid,\n    runtimeVariables,\n    processEnvVars,\n    scriptingConfig,\n    runRequestByItemPathname\n  ) => {\n    // run post-response vars\n    const postResponseVars = get(request, 'vars.res', []);\n    if (postResponseVars?.length) {\n      const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });\n      const result = varsRuntime.runPostResponseVars(\n        postResponseVars,\n        request,\n        response,\n        envVars,\n        runtimeVariables,\n        collectionPath,\n        processEnvVars\n      );\n\n      if (result) {\n        mainWindow.webContents.send('main:script-environment-update', {\n          envVariables: result.envVariables,\n          runtimeVariables: result.runtimeVariables,\n          persistentEnvVariables: result.persistentEnvVariables,\n          requestUid,\n          collectionUid\n        });\n\n        mainWindow.webContents.send('main:persistent-env-variables-update', {\n          persistentEnvVariables: result.persistentEnvVariables,\n          collectionUid\n        });\n\n        mainWindow.webContents.send('main:global-environment-variables-update', {\n          globalEnvironmentVariables: result.globalEnvironmentVariables\n        });\n\n        collection.globalEnvironmentVariables = result.globalEnvironmentVariables;\n      }\n\n      if (result?.error) {\n        mainWindow.webContents.send('main:display-error', result.error);\n      }\n    }\n\n    // run post-response script\n    const responseScript = get(request, 'script.res');\n    let scriptResult;\n    const collectionName = collection?.name;\n    if (responseScript?.length) {\n      const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });\n      scriptResult = await scriptRuntime.runResponseScript(\n        decomment(responseScript, { space: true }),\n        request,\n        response,\n        envVars,\n        runtimeVariables,\n        collectionPath,\n        onConsoleLog,\n        processEnvVars,\n        scriptingConfig,\n        runRequestByItemPathname,\n        collectionName\n      );\n\n      mainWindow.webContents.send('main:script-environment-update', {\n        envVariables: scriptResult.envVariables,\n        runtimeVariables: scriptResult.runtimeVariables,\n        persistentEnvVariables: scriptResult.persistentEnvVariables,\n        requestUid,\n        collectionUid\n      });\n\n      mainWindow.webContents.send('main:persistent-env-variables-update', {\n        persistentEnvVariables: scriptResult.persistentEnvVariables,\n        collectionUid\n      });\n\n      mainWindow.webContents.send('main:global-environment-variables-update', {\n        globalEnvironmentVariables: scriptResult.globalEnvironmentVariables\n      });\n\n      collection.globalEnvironmentVariables = scriptResult.globalEnvironmentVariables;\n\n      resetOauth2Credentials({ oauth2CredentialsToReset: scriptResult.oauth2CredentialsToReset, request, collectionUid });\n\n      const domainsWithCookiesPost = await getDomainsWithCookies();\n      mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesPost)));\n    }\n    return scriptResult;\n  };\n\n  const runRequest = async ({ item, collection, envVars, processEnvVars, runtimeVariables, runInBackground = false }) => {\n    const collectionUid = collection.uid;\n    const collectionPath = collection.pathname;\n    const cancelTokenUid = uuid();\n    // requestUid is passed when a request is triggered; defaults to uuid() if not provided (e.g., bru.runRequest())\n    const requestUid = item.requestUid || uuid();\n\n    const runRequestByItemPathname = async (relativeItemPathname) => {\n      return new Promise(async (resolve, reject) => {\n        const format = getCollectionFormat(collection.pathname);\n        let itemPathname = path.join(collection.pathname, relativeItemPathname);\n        if (itemPathname && !hasRequestExtension(itemPathname, format)) {\n          itemPathname = `${itemPathname}.${format}`;\n        }\n        const _item = cloneDeep(findItemInCollectionByPathname(collection, itemPathname));\n        if (_item) {\n          const res = await runRequest({ item: _item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: true });\n          resolve(res);\n        }\n        reject(`bru.runRequest: invalid request path - ${itemPathname}`);\n      });\n    };\n\n    !runInBackground && mainWindow.webContents.send('main:run-request-event', {\n      type: 'request-queued',\n      requestUid,\n      collectionUid,\n      itemUid: item.uid,\n      cancelTokenUid\n    });\n\n    const abortController = new AbortController();\n    const request = await prepareRequest(item, collection, abortController);\n    request.__bruno__executionMode = 'standalone';\n    request.responseType = 'stream';\n    // flag to see if the stream needs to be handled as an actual stream or\n    // is it just a data stream from axios\n    let isResponseStream = false;\n    const brunoConfig = getBrunoConfig(collectionUid, collection);\n    const scriptingConfig = get(brunoConfig, 'scripts', {});\n    scriptingConfig.runtime = getJsSandboxRuntime(collection);\n\n    try {\n      request.signal = abortController.signal;\n      saveCancelToken(cancelTokenUid, abortController);\n\n      // Build certsAndProxyConfig for bru.sendRequest\n      const certsAndProxyConfig = await buildCertsAndProxyConfig({\n        collectionUid,\n        collection,\n        collectionPath,\n        envVars,\n        runtimeVariables,\n        processEnvVars,\n        request\n      });\n\n      // Add certsAndProxyConfig to request object for bru.sendRequest\n      request.certsAndProxyConfig = certsAndProxyConfig;\n\n      let preRequestScriptResult = null;\n      let preRequestError = null;\n      try {\n        preRequestScriptResult = await runPreRequest(\n          request,\n          requestUid,\n          envVars,\n          collectionPath,\n          collection,\n          collectionUid,\n          runtimeVariables,\n          processEnvVars,\n          scriptingConfig,\n          runRequestByItemPathname\n        );\n      } catch (error) {\n        preRequestError = error;\n      }\n\n      if (preRequestError?.partialResults) {\n        preRequestScriptResult = preRequestError.partialResults;\n      }\n\n      preRequestScriptResult = appendScriptErrorResult('pre-request', preRequestScriptResult, preRequestError);\n\n      if (preRequestScriptResult?.results) {\n        mainWindow.webContents.send('main:run-request-event', {\n          type: 'test-results-pre-request',\n          results: preRequestScriptResult.results,\n          itemUid: item.uid,\n          requestUid,\n          collectionUid\n        });\n      }\n\n      !runInBackground && notifyScriptExecution({\n        channel: 'main:run-request-event',\n        basePayload: { requestUid, collectionUid, itemUid: item.uid },\n        scriptType: 'pre-request',\n        error: preRequestError\n      });\n\n      if (preRequestError) {\n        return Promise.reject(preRequestError);\n      }\n      const axiosInstance = await configureRequest(\n        collectionUid,\n        collection,\n        request,\n        envVars,\n        runtimeVariables,\n        processEnvVars,\n        collectionPath,\n        collection.globalEnvironmentVariables\n      );\n\n      const { data: requestData, dataBuffer: requestDataBuffer } = parseDataFromRequest(request);\n\n      // Remove false Content-Type header (used to stop axios from auto-setting it); no Content-Type was actually set or sent.\n      const headersSent = { ...request.headers };\n      Object.keys(headersSent).forEach((key) => {\n        if (key.toLowerCase() === 'content-type' && headersSent[key] === false) {\n          delete headersSent[key];\n        }\n      });\n\n      let requestSent = {\n        url: request.url,\n        method: request.method,\n        headers: headersSent,\n        data: requestData,\n        dataBuffer: requestDataBuffer,\n        timestamp: Date.now()\n      };\n\n      !runInBackground && mainWindow.webContents.send('main:run-request-event', {\n        type: 'request-sent',\n        requestSent,\n        collectionUid,\n        itemUid: item.uid,\n        requestUid,\n        cancelTokenUid\n      });\n\n      if (request.oauth2Credentials?.credentials && request.oauth2Credentials?.credentialsId) {\n        mainWindow.webContents.send('main:credentials-update', {\n          credentials: request?.oauth2Credentials?.credentials,\n          url: request?.oauth2Credentials?.url,\n          collectionUid,\n          credentialsId: request?.oauth2Credentials?.credentialsId,\n          ...(request?.oauth2Credentials?.folderUid ? { folderUid: request.oauth2Credentials.folderUid } : { itemUid: item.uid }),\n          debugInfo: request?.oauth2Credentials?.debugInfo\n        });\n\n        const { credentialsId, credentials } = request.oauth2Credentials;\n        request.oauth2CredentialVariables = request.oauth2CredentialVariables || {};\n        Object.entries(credentials).forEach(([key, value]) => {\n          request.oauth2CredentialVariables[`$oauth2.${credentialsId}.${key}`] = value;\n        });\n      }\n\n      let response, responseTime, axiosDataStream;\n      try {\n        /** @type {import('axios').AxiosResponse} */\n        response = await axiosInstance(request);\n        isResponseStream = hasStreamHeaders(response.headers);\n\n        if (!isResponseStream) {\n          response.data = await promisifyStream(response.data);\n        }\n\n        // Prevents the duration on leaking to the actual result\n        responseTime = response.headers.get('request-duration');\n        response.headers.delete('request-duration');\n      } catch (error) {\n        deleteCancelToken(cancelTokenUid);\n\n        // if it's a cancel request, don't continue\n        if (axios.isCancel(error)) {\n          // we are not rejecting the promise here and instead returning a response object with `error` which is handled in the `send-http-request` invocation\n          // timeline prop won't be accessible in the usual way in the renderer process if we reject the promise\n          return {\n            statusText: 'REQUEST_CANCELLED',\n            isCancel: true,\n            error: 'REQUEST_CANCELLED',\n            timeline: error.timeline\n          };\n        }\n        if (error?.response) {\n          response = error.response;\n\n          // Prevents the duration on leaking to the actual result\n          responseTime = response.headers.get('request-duration');\n          response.headers.delete('request-duration');\n          isResponseStream = hasStreamHeaders(response.headers);\n          if (!isResponseStream) {\n            response.data = await promisifyStream(response.data);\n          }\n        } else {\n          await executeRequestOnFailHandler(request, error);\n\n          // if it's not a network error, don't continue\n          // we are not rejecting the promise here and instead returning a response object with `error` which is handled in the `send-http-request` invocation\n          // timeline prop won't be accessible in the usual way in the renderer process if we reject the promise\n          return {\n            statusText: error.statusText,\n            error: error.message || ERROR_OCCURRED_WHILE_EXECUTING_REQUEST,\n            timeline: error.timeline\n          };\n        }\n      }\n\n      // Continue with the rest of the request lifecycle - post response vars, script, assertions, tests\n      if (isResponseStream) {\n        axiosDataStream = response.data;\n      }\n\n      const { data, dataBuffer } = isResponseStream\n        ? { data: '', dataBuffer: Buffer.alloc(0) }\n        : parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);\n      response.data = data;\n      response.dataBuffer = dataBuffer;\n\n      response.responseTime = responseTime;\n\n      // save cookies\n      if (preferencesUtil.shouldStoreCookies()) {\n        saveCookies(request.url, response.headers);\n      }\n\n      // send domain cookies to renderer\n      const domainsWithCookies = await getDomainsWithCookies();\n\n      mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));\n      cookiesStore.saveCookieJar();\n\n      const runPostScripts = async () => {\n        let postResponseScriptResult = null;\n        let postResponseError = null;\n        try {\n          postResponseScriptResult = await runPostResponse(request,\n            response,\n            requestUid,\n            envVars,\n            collectionPath,\n            collection,\n            collectionUid,\n            runtimeVariables,\n            processEnvVars,\n            scriptingConfig,\n            runRequestByItemPathname);\n        } catch (error) {\n          console.error('Post-response script error:', error);\n          postResponseError = error;\n        }\n\n        // Extract partial results from error if available\n        // This preserves any test() calls that passed before the script errored\n        // (e.g., if 2 tests pass then script throws, we still want to show those 2 passing tests)\n        if (postResponseError?.partialResults) {\n          postResponseScriptResult = postResponseError.partialResults;\n        }\n\n        postResponseScriptResult = appendScriptErrorResult('post-response', postResponseScriptResult, postResponseError);\n\n        if (postResponseScriptResult?.results) {\n          mainWindow.webContents.send('main:run-request-event', {\n            type: 'test-results-post-response',\n            results: postResponseScriptResult.results,\n            itemUid: item.uid,\n            requestUid,\n            collectionUid\n          });\n        }\n\n        !runInBackground && notifyScriptExecution({\n          channel: 'main:run-request-event',\n          basePayload: { requestUid, collectionUid, itemUid: item.uid },\n          scriptType: 'post-response',\n          error: postResponseError\n        });\n\n        // run assertions\n        const assertions = get(request, 'assertions');\n        if (assertions) {\n          const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime });\n          const results = assertRuntime.runAssertions(assertions,\n            request,\n            response,\n            envVars,\n            runtimeVariables,\n            processEnvVars);\n\n          !runInBackground && mainWindow.webContents.send('main:run-request-event', {\n            type: 'assertion-results',\n            results: results,\n            itemUid: item.uid,\n            requestUid,\n            collectionUid\n          });\n        }\n\n        const testFile = get(request, 'tests');\n        const collectionName = collection?.name;\n        if (typeof testFile === 'string') {\n          const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });\n          let testResults = null;\n          let testError = null;\n\n          try {\n            testResults = await testRuntime.runTests(decomment(testFile, { space: true }),\n              request,\n              response,\n              envVars,\n              runtimeVariables,\n              collectionPath,\n              onConsoleLog,\n              processEnvVars,\n              scriptingConfig,\n              runRequestByItemPathname,\n              collectionName);\n          } catch (error) {\n            testError = error;\n\n            if (error.partialResults) {\n              testResults = error.partialResults;\n            } else {\n              testResults = {\n                request,\n                envVariables: envVars,\n                runtimeVariables,\n                globalEnvironmentVariables: request?.globalEnvironmentVariables || {},\n                results: [],\n                nextRequestName: null\n              };\n            }\n          }\n\n          testResults = appendScriptErrorResult('test', testResults, testError);\n\n          !runInBackground && mainWindow.webContents.send('main:run-request-event', {\n            type: 'test-results',\n            results: testResults.results,\n            itemUid: item.uid,\n            requestUid,\n            collectionUid\n          });\n\n          mainWindow.webContents.send('main:script-environment-update', {\n            envVariables: testResults.envVariables,\n            runtimeVariables: testResults.runtimeVariables,\n            requestUid,\n            collectionUid\n          });\n\n          mainWindow.webContents.send('main:persistent-env-variables-update', {\n            persistentEnvVariables: testResults.persistentEnvVariables,\n            collectionUid\n          });\n\n          mainWindow.webContents.send('main:global-environment-variables-update', {\n            globalEnvironmentVariables: testResults.globalEnvironmentVariables\n          });\n\n          collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables;\n\n          resetOauth2Credentials({ oauth2CredentialsToReset: testResults.oauth2CredentialsToReset, request, collectionUid });\n\n          !runInBackground && notifyScriptExecution({\n            channel: 'main:run-request-event',\n            basePayload: { requestUid, collectionUid, itemUid: item.uid },\n            scriptType: 'test',\n            error: testError\n          });\n\n          const domainsWithCookiesTest = await getDomainsWithCookies();\n          mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesTest)));\n          cookiesStore.saveCookieJar();\n        }\n      };\n      if (isResponseStream) {\n        axiosDataStream.on('close', () => runPostScripts().then());\n      } else {\n        await runPostScripts();\n      }\n\n      return {\n        status: response.status,\n        statusText: response.statusText,\n        headers: response.headers,\n        data: response.data,\n        stream: isResponseStream ? axiosDataStream : null,\n        cancelTokenUid: cancelTokenUid,\n        dataBuffer: response.dataBuffer.toString('base64'),\n        size: Buffer.byteLength(response.dataBuffer),\n        duration: responseTime ?? 0,\n        url: response.request ? response.request.protocol + '//' + response.request.host + response.request.path : null,\n        timeline: response.timeline\n      };\n    } catch (error) {\n      deleteCancelToken(cancelTokenUid);\n\n      // we are not rejecting the promise here and instead returning a response object with `error` which is handled in the `send-http-request` invocation\n      // timeline prop won't be accessible in the usual way in the renderer process if we reject the promise\n      return {\n        status: error?.status,\n        error: error?.message || ERROR_OCCURRED_WHILE_EXECUTING_REQUEST,\n        timeline: error?.timeline\n      };\n    }\n  };\n\n  /**\n   * Extract prompt variables from a request\n   * Tries to respect the hierarchy of the variables and avoid unnecessary prompts as much as possible\n   * Note: TO BE CALLED ONLY AFTER THE PREPARE REQUEST\n   *\n   * @param {*} request - request object built by prepareRequest\n   * @returns {string[]} An array of extracted prompt variables\n   */\n  const extractPromptVariablesForRequest = async ({ request, collection, envVars: collectionEnvironmentVars, runtimeVariables, processEnvVars }) => {\n    const { globalEnvironmentVariables, collectionVariables, folderVariables, requestVariables, ...requestObj } = request;\n\n    const allVariables = {\n      ...globalEnvironmentVariables,\n      ...collectionEnvironmentVars,\n      ...collectionVariables,\n      ...folderVariables,\n      ...requestVariables,\n      ...runtimeVariables,\n      process: {\n        env: {\n          ...processEnvVars\n        }\n      }\n    };\n\n    const { interpolationOptions, ...certsAndProxyConfig } = await getCertsAndProxyConfig({\n      collectionUid: collection.uid,\n      collection,\n      request,\n      envVars: collectionEnvironmentVars,\n      runtimeVariables,\n      processEnvVars,\n      collectionPath: collection.pathname,\n      globalEnvironmentVariables\n    });\n\n    const prompts = extractPromptVariables(requestObj);\n    prompts.push(...extractPromptVariables(allVariables));\n    prompts.push(...extractPromptVariables(certsAndProxyConfig));\n\n    // return unique prompt variables\n    return Array.from(new Set(prompts));\n  };\n\n  // handler for sending http request\n  ipcMain.handle('send-http-request', async (event, item, collection, environment, runtimeVariables) => {\n    let seq = 0;\n    const collectionUid = collection.uid;\n    const envVars = getEnvVars(environment);\n    const processEnvVars = getProcessEnvVars(collectionUid);\n    const response = await runRequest({ item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: false });\n    if (response.stream) {\n      const stream = response.stream;\n      response.stream = { running: response.status >= 200 && response.status < 300 };\n\n      stream.on('data', (newData) => {\n        seq += 1;\n\n        const parsed = parseDataFromResponse({ data: newData, headers: {} });\n\n        mainWindow.webContents.send('main:http-stream-new-data', {\n          collectionUid,\n          itemUid: item.uid,\n          seq,\n          timestamp: Date.now(),\n          data: parsed\n        });\n      });\n\n      stream.on('close', () => {\n        if (!cancelTokens[response.cancelTokenUid]) return;\n\n        mainWindow.webContents.send('main:http-stream-end', {\n          collectionUid,\n          itemUid: item.uid,\n          seq: seq + 1,\n          timestamp: Date.now()\n        });\n\n        deleteCancelToken(response.cancelTokenUid);\n      });\n    }\n    return response;\n  });\n\n  ipcMain.handle('clear-oauth2-cache', async (event, uid, url, credentialsId) => {\n    return new Promise((resolve, reject) => {\n      try {\n        const oauth2Store = new Oauth2Store();\n        oauth2Store.clearSessionIdOfCollection({ collectionUid: uid, url, credentialsId });\n        resolve();\n      } catch (err) {\n        reject(new Error('Could not clear oauth2 cache'));\n      }\n    });\n  });\n\n  ipcMain.handle('cancel-http-request', async (event, cancelTokenUid) => {\n    return new Promise((resolve, reject) => {\n      if (cancelTokenUid && cancelTokens[cancelTokenUid]) {\n        const abortController = cancelTokens[cancelTokenUid];\n        deleteCancelToken(cancelTokenUid);\n        abortController.abort();\n        resolve();\n      } else {\n        reject(new Error('cancel token not found'));\n      }\n    });\n  });\n\n  // handler for fetch-gql-schema\n  ipcMain.handle('fetch-gql-schema', fetchGqlSchemaHandler);\n\n  ipcMain.handle(\n    'renderer:run-collection-folder', async (event, folder, collection, environment, runtimeVariables, recursive, delay, tags, selectedRequestUids) => {\n      const collectionUid = collection.uid;\n      const collectionPath = collection.pathname;\n      const folderUid = folder ? folder.uid : null;\n      const cancelTokenUid = uuid();\n      const brunoConfig = getBrunoConfig(collectionUid, collection);\n      const scriptingConfig = get(brunoConfig, 'scripts', {});\n      scriptingConfig.runtime = getJsSandboxRuntime(collection);\n      const envVars = getEnvVars(environment);\n      const processEnvVars = getProcessEnvVars(collectionUid);\n      let stopRunnerExecution = false;\n      let currentAbortController;\n\n      const abortController = new AbortController();\n      saveCancelToken(cancelTokenUid, abortController);\n\n      abortController.signal.addEventListener('abort', () => {\n        if (currentAbortController) {\n          currentAbortController.abort();\n        }\n      });\n\n      const runRequestByItemPathname = async (relativeItemPathname) => {\n        return new Promise(async (resolve, reject) => {\n          const format = getCollectionFormat(collection.pathname);\n          let itemPathname = path.join(collection.pathname, relativeItemPathname);\n          if (itemPathname && !hasRequestExtension(itemPathname, format)) {\n            itemPathname = `${itemPathname}.${format}`;\n          }\n          const _item = cloneDeep(findItemInCollectionByPathname(collection, itemPathname));\n          if (_item) {\n            const res = await runRequest({ item: _item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: true });\n            resolve(res);\n          }\n          reject(`bru.runRequest: invalid request path - ${itemPathname}`);\n        });\n      };\n\n      if (!folder) {\n        folder = collection;\n      }\n\n      mainWindow.webContents.send('main:run-folder-event', {\n        type: 'testrun-started',\n        isRecursive: recursive,\n        collectionUid,\n        folderUid,\n        cancelTokenUid\n      });\n\n      try {\n        let folderRequests = [];\n\n        if (recursive) {\n          let sortedFolder = sortFolder(folder);\n          folderRequests = getAllRequestsInFolderRecursively(sortedFolder);\n        } else {\n          each(folder.items, (item) => {\n            // Skip transient requests\n            if (item.request && !item.isTransient) {\n              folderRequests.push(item);\n            }\n          });\n\n          // sort requests by seq property\n          folderRequests = sortByNameThenSequence(folderRequests);\n        }\n\n        // Filter requests based on tags\n        if (tags && tags.include && tags.exclude) {\n          const includeTags = tags.include ? tags.include : [];\n          const excludeTags = tags.exclude ? tags.exclude : [];\n          folderRequests = folderRequests.filter(({ tags: requestTags = [], draft }) => {\n            requestTags = draft?.tags || requestTags || [];\n            return isRequestTagsIncluded(requestTags, includeTags, excludeTags);\n          });\n        }\n\n        // Filter requests based on selectedRequestUids (for \"Configure requests to run\")\n        if (selectedRequestUids && selectedRequestUids.length > 0) {\n          const uidIndexMap = new Map();\n          selectedRequestUids.forEach((uid, index) => {\n            uidIndexMap.set(uid, index);\n          });\n\n          folderRequests = folderRequests\n            .filter((request) => uidIndexMap.has(request.uid))\n            .sort((a, b) => {\n              const indexA = uidIndexMap.get(a.uid);\n              const indexB = uidIndexMap.get(b.uid);\n              return indexA - indexB;\n            });\n        }\n\n        let currentRequestIndex = 0;\n        let nJumps = 0; // count the number of jumps to avoid infinite loops\n        while (currentRequestIndex < folderRequests.length) {\n          // user requested to cancel runner\n          if (abortController.signal.aborted) {\n            let error = new Error('Runner execution cancelled');\n            error.isCancel = true;\n            throw error;\n          }\n\n          stopRunnerExecution = false;\n\n          const item = cloneDeep(folderRequests[currentRequestIndex]);\n          let nextRequestName;\n          const itemUid = item.uid;\n          const eventData = {\n            collectionUid,\n            folderUid,\n            itemUid\n          };\n\n          let timeStart;\n          let timeEnd;\n\n          mainWindow.webContents.send('main:run-folder-event', {\n            type: 'request-queued',\n            ...eventData\n          });\n\n          // Skip gRPC requests\n          if (item.type === 'grpc-request') {\n            mainWindow.webContents.send('main:run-folder-event', {\n              type: 'runner-request-skipped',\n              error: 'gRPC requests are skipped in folder/collection runs',\n              responseReceived: {\n                status: 'skipped',\n                statusText: 'gRPC request skipped',\n                data: null,\n                responseTime: 0,\n                headers: null\n              },\n              ...eventData\n            });\n            currentRequestIndex++;\n            continue;\n          }\n\n          const request = await prepareRequest(item, collection, abortController);\n          request.__bruno__executionMode = 'runner';\n\n          const requestUid = uuid();\n\n          const promptVars = await extractPromptVariablesForRequest({ request, collection, envVars, runtimeVariables, processEnvVars });\n\n          if (promptVars.length > 0) {\n            mainWindow.webContents.send('main:run-folder-event', {\n              type: 'runner-request-skipped',\n              error: 'Request has been skipped due to containing prompt variables',\n              responseReceived: {\n                status: 'skipped',\n                statusText: `Prompt variables detected in request. Runner execution is not supported for requests with prompt variables. \\n Promps: ${promptVars.join(', ')}`,\n                data: null,\n                responseTime: 0,\n                headers: null\n              },\n              ...eventData\n            });\n\n            currentRequestIndex++;\n\n            continue;\n          }\n\n          try {\n            // Build certsAndProxyConfig for bru.sendRequest\n            const certsAndProxyConfig = await buildCertsAndProxyConfig({\n              collectionUid,\n              collection,\n              collectionPath,\n              envVars,\n              runtimeVariables,\n              processEnvVars,\n              request\n            });\n\n            // Add certsAndProxyConfig to request object for bru.sendRequest\n            request.certsAndProxyConfig = certsAndProxyConfig;\n\n            let preRequestScriptResult;\n            let preRequestError = null;\n            try {\n              preRequestScriptResult = await runPreRequest(\n                request,\n                requestUid,\n                envVars,\n                collectionPath,\n                collection,\n                collectionUid,\n                runtimeVariables,\n                processEnvVars,\n                scriptingConfig,\n                runRequestByItemPathname\n              );\n            } catch (error) {\n              console.error('Pre-request script error:', error);\n              preRequestError = error;\n            }\n\n            if (preRequestError?.partialResults) {\n              preRequestScriptResult = preRequestError.partialResults;\n            }\n\n            preRequestScriptResult = appendScriptErrorResult('pre-request', preRequestScriptResult, preRequestError);\n\n            if (preRequestScriptResult?.results) {\n              mainWindow.webContents.send('main:run-folder-event', {\n                type: 'test-results-pre-request',\n                preRequestTestResults: preRequestScriptResult.results,\n                ...eventData\n              });\n            }\n\n            notifyScriptExecution({\n              channel: 'main:run-folder-event',\n              basePayload: eventData,\n              scriptType: 'pre-request',\n              error: preRequestError\n            });\n\n            const domainsWithCookiesPreRequest = await getDomainsWithCookies();\n            mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesPreRequest)));\n\n            if (preRequestError) {\n              throw preRequestError;\n            }\n\n            if (preRequestScriptResult?.nextRequestName !== undefined) {\n              nextRequestName = preRequestScriptResult.nextRequestName;\n            }\n\n            if (preRequestScriptResult?.stopExecution) {\n              stopRunnerExecution = true;\n            }\n\n            if (preRequestScriptResult?.skipRequest) {\n              mainWindow.webContents.send('main:run-folder-event', {\n                type: 'runner-request-skipped',\n                error: 'Request has been skipped from pre-request script',\n                responseReceived: {\n                  status: 'skipped',\n                  statusText: 'request skipped via pre-request script',\n                  data: null,\n                  responseTime: 0,\n                  headers: null\n                },\n                ...eventData\n              });\n              currentRequestIndex++;\n              continue;\n            }\n\n            const { data: requestData, dataBuffer: requestDataBuffer } = parseDataFromRequest(request);\n\n            // Remove false Content-Type header (used to stop axios from auto-setting it); no Content-Type was actually set or sent.\n            const headersSent = { ...request.headers };\n            Object.keys(headersSent).forEach((key) => {\n              if (key.toLowerCase() === 'content-type' && headersSent[key] === false) {\n                delete headersSent[key];\n              }\n            });\n\n            let requestSent = {\n              url: request.url,\n              method: request.method,\n              headers: headersSent,\n              data: requestData,\n              dataBuffer: requestDataBuffer,\n              timestamp: Date.now()\n            };\n\n            // todo:\n            // i have no clue why electron can't send the request object\n            // without safeParseJSON(safeStringifyJSON(request.data))\n            mainWindow.webContents.send('main:run-folder-event', {\n              type: 'request-sent',\n              requestSent,\n              ...eventData\n            });\n\n            currentAbortController = new AbortController();\n            request.signal = currentAbortController.signal;\n            request.responseType = 'stream';\n            const axiosInstance = await configureRequest(\n              collectionUid,\n              collection,\n              request,\n              envVars,\n              runtimeVariables,\n              processEnvVars,\n              collectionPath,\n              collection.globalEnvironmentVariables\n            );\n\n            if (request.oauth2Credentials?.credentials && request.oauth2Credentials?.credentialsId) {\n              mainWindow.webContents.send('main:credentials-update', {\n                credentials: request?.oauth2Credentials?.credentials,\n                url: request?.oauth2Credentials?.url,\n                collectionUid,\n                credentialsId: request?.oauth2Credentials?.credentialsId,\n                ...(request?.oauth2Credentials?.folderUid ? { folderUid: request.oauth2Credentials.folderUid } : { itemUid: item.uid }),\n                debugInfo: request?.oauth2Credentials?.debugInfo\n              });\n\n              const { credentialsId, credentials } = request.oauth2Credentials;\n              request.oauth2CredentialVariables = request.oauth2CredentialVariables || {};\n              Object.entries(credentials).forEach(([key, value]) => {\n                request.oauth2CredentialVariables[`$oauth2.${credentialsId}.${key}`] = value;\n              });\n\n              collection.oauth2Credentials = updateCollectionOauth2Credentials({\n                itemUid: item.uid,\n                collectionUid,\n                collectionOauth2Credentials: collection.oauth2Credentials,\n                requestOauth2Credentials: request.oauth2Credentials\n              });\n            }\n\n            timeStart = Date.now();\n            let response, responseTime;\n            try {\n              if (delay && !Number.isNaN(delay) && delay > 0) {\n                const delayPromise = new Promise((resolve) => setTimeout(resolve, delay));\n\n                const cancellationPromise = new Promise((_, reject) => {\n                  abortController.signal.addEventListener('abort', () => {\n                    reject(new Error('Cancelled'));\n                  });\n                });\n\n                await Promise.race([delayPromise, cancellationPromise]);\n              }\n\n              /** @type {import('axios').AxiosResponse} */\n              response = await axiosInstance(request);\n              response.data = await promisifyStream(response.data, currentAbortController, false);\n              timeEnd = Date.now();\n\n              const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);\n              response.data = data;\n              response.dataBuffer = dataBuffer;\n              response.responseTime = response.headers.get('request-duration');\n              response.headers.delete('request-duration');\n\n              // save cookies\n              if (preferencesUtil.shouldStoreCookies()) {\n                saveCookies(request.url, response.headers);\n              }\n\n              // send domain cookies to renderer\n              const domainsWithCookies = await getDomainsWithCookies();\n\n              mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));\n\n              mainWindow.webContents.send('main:run-folder-event', {\n                type: 'response-received',\n                responseReceived: {\n                  status: response.status,\n                  statusText: response.statusText,\n                  headers: response.headers,\n                  duration: timeEnd - timeStart,\n                  dataBuffer: dataBuffer.toString('base64'),\n                  size: Buffer.byteLength(dataBuffer),\n                  data: response.data,\n                  responseTime: response.responseTime,\n                  timeline: response.timeline,\n                  url: response.request ? response.request.protocol + '//' + response.request.host + response.request.path : null\n                },\n                ...eventData\n              });\n            } catch (error) {\n              // Skip further processing if request was cancelled\n              if (axios.isCancel(error)) {\n                throw error;\n              }\n\n              if (error?.response) {\n                error.response.data = await promisifyStream(error.response.data, currentAbortController, false);\n                const { data, dataBuffer } = parseDataFromResponse(error.response);\n                error.response.responseTime = error.response.headers.get('request-duration');\n                error.response.headers.delete('request-duration');\n                error.response.data = data;\n                error.response.dataBuffer = dataBuffer;\n\n                timeEnd = Date.now();\n                response = {\n                  status: error.response.status,\n                  statusText: error.response.statusText,\n                  headers: error.response.headers,\n                  duration: timeEnd - timeStart,\n                  dataBuffer: dataBuffer.toString('base64'),\n                  size: Buffer.byteLength(dataBuffer),\n                  data: error.response.data,\n                  responseTime: error.response.responseTime,\n                  timeline: error.response.timeline\n                };\n\n                // if we get a response from the server, we consider it as a success\n                mainWindow.webContents.send('main:run-folder-event', {\n                  type: 'response-received',\n                  error: error ? error.message : 'An error occurred while running the request',\n                  responseReceived: response,\n                  ...eventData\n                });\n              } else {\n                await executeRequestOnFailHandler(request, error);\n\n                // if it's not a network error, don't continue\n                throw error;\n              }\n            }\n\n            let postResponseScriptResult;\n            let postResponseError = null;\n            try {\n              postResponseScriptResult = await runPostResponse(\n                request,\n                response,\n                requestUid,\n                envVars,\n                collectionPath,\n                collection,\n                collectionUid,\n                runtimeVariables,\n                processEnvVars,\n                scriptingConfig,\n                runRequestByItemPathname\n              );\n            } catch (error) {\n              console.error('Post-response script error:', error);\n              postResponseError = error;\n            }\n\n            // Extract partial results from error if available\n            // (e.g., if 2 tests pass then script throws, we still want to show those 2 passing tests)\n            if (postResponseError?.partialResults) {\n              postResponseScriptResult = postResponseError.partialResults;\n            }\n\n            postResponseScriptResult = appendScriptErrorResult('post-response', postResponseScriptResult, postResponseError);\n\n            notifyScriptExecution({\n              channel: 'main:run-folder-event',\n              basePayload: eventData,\n              scriptType: 'post-response',\n              error: postResponseError\n            });\n\n            const domainsWithCookiesPostResponse = await getDomainsWithCookies();\n            mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesPostResponse)));\n\n            if (postResponseScriptResult?.nextRequestName !== undefined) {\n              nextRequestName = postResponseScriptResult.nextRequestName;\n            }\n\n            if (postResponseScriptResult?.stopExecution) {\n              stopRunnerExecution = true;\n            }\n\n            // Send post-response test results if available\n            if (postResponseScriptResult?.results) {\n              mainWindow.webContents.send('main:run-folder-event', {\n                type: 'test-results-post-response',\n                postResponseTestResults: postResponseScriptResult.results,\n                ...eventData\n              });\n            }\n\n            // run assertions\n            const assertions = get(item, 'request.assertions');\n            if (assertions) {\n              const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime });\n              const results = assertRuntime.runAssertions(\n                assertions,\n                request,\n                response,\n                envVars,\n                runtimeVariables,\n                processEnvVars\n              );\n\n              mainWindow.webContents.send('main:run-folder-event', {\n                type: 'assertion-results',\n                assertionResults: results,\n                itemUid: item.uid,\n                collectionUid\n              });\n            }\n\n            const testFile = get(request, 'tests');\n            const collectionName = collection?.name;\n            if (typeof testFile === 'string') {\n              let testResults = null;\n              let testError = null;\n\n              try {\n                const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });\n                testResults = await testRuntime.runTests(\n                  decomment(testFile, { space: true }),\n                  request,\n                  response,\n                  envVars,\n                  runtimeVariables,\n                  collectionPath,\n                  onConsoleLog,\n                  processEnvVars,\n                  scriptingConfig,\n                  runRequestByItemPathname,\n                  collectionName\n                );\n              } catch (error) {\n                testError = error;\n\n                if (error.partialResults) {\n                  testResults = error.partialResults;\n                } else {\n                  testResults = {\n                    request,\n                    envVariables: envVars,\n                    runtimeVariables,\n                    globalEnvironmentVariables: request?.globalEnvironmentVariables || {},\n                    results: [],\n                    nextRequestName: null\n                  };\n                }\n              }\n\n              testResults = appendScriptErrorResult('test', testResults, testError);\n\n              if (testResults?.nextRequestName !== undefined) {\n                nextRequestName = testResults.nextRequestName;\n              }\n\n              mainWindow.webContents.send('main:run-folder-event', {\n                type: 'test-results',\n                testResults: testResults.results,\n                ...eventData\n              });\n\n              mainWindow.webContents.send('main:script-environment-update', {\n                envVariables: testResults.envVariables,\n                runtimeVariables: testResults.runtimeVariables,\n                collectionUid\n              });\n\n              mainWindow.webContents.send('main:global-environment-variables-update', {\n                globalEnvironmentVariables: testResults.globalEnvironmentVariables\n              });\n\n              collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables;\n\n              resetOauth2Credentials({ oauth2CredentialsToReset: testResults.oauth2CredentialsToReset, request, collectionUid });\n\n              notifyScriptExecution({\n                channel: 'main:run-folder-event',\n                basePayload: eventData,\n                scriptType: 'test',\n                error: testError\n              });\n\n              const domainsWithCookiesTest = await getDomainsWithCookies();\n              mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesTest)));\n            }\n          } catch (error) {\n            mainWindow.webContents.send('main:run-folder-event', {\n              type: 'error',\n              error: error ? error.message : 'An error occurred while running the request',\n              responseReceived: {},\n              ...eventData\n            });\n          }\n\n          if (stopRunnerExecution) {\n            deleteCancelToken(cancelTokenUid);\n            mainWindow.webContents.send('main:run-folder-event', {\n              type: 'testrun-ended',\n              collectionUid,\n              folderUid,\n              statusText: 'collection run was terminated!',\n              runCompletionTime: new Date().toISOString()\n            });\n            break;\n          }\n\n          if (nextRequestName !== undefined) {\n            nJumps++;\n            if (nJumps > 10000) {\n              throw new Error('Too many jumps, possible infinite loop');\n            }\n            if (nextRequestName === null) {\n              break;\n            }\n            const nextRequestIdx = folderRequests.findIndex((request) => request.name === nextRequestName);\n            if (nextRequestIdx >= 0) {\n              currentRequestIndex = nextRequestIdx;\n            } else {\n              console.error('Could not find request with name \\'' + nextRequestName + '\\'');\n              currentRequestIndex++;\n            }\n          } else {\n            currentRequestIndex++;\n          }\n        }\n\n        deleteCancelToken(cancelTokenUid);\n        mainWindow.webContents.send('main:run-folder-event', {\n          type: 'testrun-ended',\n          collectionUid,\n          folderUid,\n          runCompletionTime: new Date().toISOString()\n        });\n      } catch (error) {\n        console.log('error', error);\n        deleteCancelToken(cancelTokenUid);\n        mainWindow.webContents.send('main:run-folder-event', {\n          type: 'testrun-ended',\n          collectionUid,\n          folderUid,\n          runCompletionTime: new Date().toISOString(),\n          error: error && !error.isCancel ? error : null\n        });\n      }\n    }\n  );\n\n  // save response to file\n  ipcMain.handle('renderer:save-response-to-file', async (event, response, url, pathname) => {\n    try {\n      const getHeaderValue = (headerName) => {\n        const headersArray = typeof response.headers === 'object' ? Object.entries(response.headers) : [];\n\n        if (headersArray.length > 0) {\n          const header = headersArray.find((header) => header[0] === headerName);\n          if (header && header.length > 1) {\n            return header[1];\n          }\n        }\n      };\n\n      const getFileNameFromContentDispositionHeader = () => {\n        const contentDisposition = getHeaderValue('content-disposition');\n        try {\n          const disposition = contentDispositionParser.parse(contentDisposition);\n          return disposition && disposition.parameters['filename'];\n        } catch (error) { }\n      };\n\n      const getFileNameFromUrlPath = () => {\n        const lastPathLevel = new URL(url).pathname.split('/').pop();\n        if (lastPathLevel && /\\..+/.exec(lastPathLevel)) {\n          return lastPathLevel;\n        }\n      };\n\n      const getFileNameBasedOnContentTypeHeader = () => {\n        const contentType = getHeaderValue('content-type');\n        const extension = (contentType && mime.extension(contentType)) || 'txt';\n        return `response.${extension}`;\n      };\n\n      const getEncodingFormat = () => {\n        const contentType = getHeaderValue('content-type');\n        const extension = mime.extension(contentType) || 'txt';\n        return ['json', 'xml', 'html', 'yml', 'yaml', 'txt'].includes(extension) ? 'utf-8' : 'base64';\n      };\n\n      const determineFileName = () => {\n        return (\n          getFileNameFromContentDispositionHeader() || getFileNameFromUrlPath() || getFileNameBasedOnContentTypeHeader()\n        );\n      };\n\n      const dirPath = path.dirname(pathname);\n      const fileName = determineFileName();\n      const filePath = await chooseFileToSave(mainWindow, path.join(dirPath, fileName));\n      if (filePath) {\n        const encoding = getEncodingFormat();\n        const data = Buffer.from(response.dataBuffer, 'base64');\n        if (encoding === 'utf-8') {\n          await writeFile(filePath, data);\n        } else {\n          await writeFile(filePath, data, true);\n        }\n        return { success: true, filePath };\n      }\n      return { success: false, cancelled: true };\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n};\n\n/**\n * Executes the custom error handler if it exists on the request\n * @param {Object} request - The request object that may contain an onFailHandler\n * @param {Error} error - The error that occurred\n */\nconst executeRequestOnFailHandler = async (request, error) => {\n  if (!request || typeof request.onFailHandler !== 'function') {\n    return;\n  }\n\n  try {\n    await request.onFailHandler(error);\n  } catch (handlerError) {\n    console.error('Error executing onFail handler', handlerError);\n    // @TODO: This is a temporary solution to display the error message in the response pane. Revisit and handle properly.\n    error.message = `1. Request failed: ${error.message || ERROR_OCCURRED_WHILE_EXECUTING_REQUEST}\\n2. Error executing onFail handler: ${handlerError.message || 'Unknown error'}`;\n  }\n};\n\nconst registerAllNetworkIpc = (mainWindow) => {\n  registerNetworkIpc(mainWindow);\n  registerGrpcEventHandlers(mainWindow);\n  registerWsEventHandlers(mainWindow);\n};\n\nmodule.exports = registerAllNetworkIpc;\nmodule.exports.configureRequest = configureRequest;\nmodule.exports.getCertsAndProxyConfig = getCertsAndProxyConfig;\nmodule.exports.fetchGqlSchemaHandler = fetchGqlSchemaHandler;\nmodule.exports.executeRequestOnFailHandler = executeRequestOnFailHandler;\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/network/interpolate-string.js",
    "content": "const { forOwn, cloneDeep } = require('lodash');\nconst { interpolate, interpolateObject: interpolateObjectCommon } = require('@usebruno/common');\n\nconst buildCombinedVars = ({\n  globalEnvironmentVariables,\n  collectionVariables,\n  envVars,\n  folderVariables,\n  requestVariables,\n  runtimeVariables,\n  processEnvVars,\n  promptVariables\n}) => {\n  processEnvVars = processEnvVars || {};\n  runtimeVariables = runtimeVariables || {};\n  globalEnvironmentVariables = globalEnvironmentVariables || {};\n  collectionVariables = collectionVariables || {};\n  folderVariables = folderVariables || {};\n  requestVariables = requestVariables || {};\n  promptVariables = promptVariables || {};\n\n  // we clone envVars because we don't want to modify the original object\n  envVars = envVars ? cloneDeep(envVars) : {};\n\n  // envVars can inturn have values as {{process.env.VAR_NAME}}\n  // so we need to interpolate envVars first with processEnvVars\n  forOwn(envVars, (value, key) => {\n    envVars[key] = interpolate(value, {\n      process: {\n        env: {\n          ...processEnvVars\n        }\n      }\n    });\n  });\n\n  return {\n    ...globalEnvironmentVariables,\n    ...collectionVariables,\n    ...envVars,\n    ...folderVariables,\n    ...requestVariables,\n    ...runtimeVariables,\n    ...promptVariables,\n    process: {\n      env: {\n        ...processEnvVars\n      }\n    }\n  };\n};\n\nconst interpolateString = (str, interpolationOptions) => {\n  if (!str || !str.length || typeof str !== 'string') {\n    return str;\n  }\n\n  const combinedVars = buildCombinedVars(interpolationOptions);\n  return interpolate(str, combinedVars);\n};\n\n/**\n * Recursively interpolates all string values in an object\n */\nconst interpolateObject = (obj, interpolationOptions) => {\n  const combinedVars = buildCombinedVars(interpolationOptions);\n  return interpolateObjectCommon(obj, combinedVars);\n};\n\nmodule.exports = {\n  interpolateString,\n  interpolateObject\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/network/interpolate-vars.js",
    "content": "const { interpolate } = require('@usebruno/common');\nconst { each, forOwn, cloneDeep } = require('lodash');\nconst { isFormData } = require('@usebruno/common').utils;\n\nconst getContentType = (headers = {}) => {\n  let contentType = '';\n  forOwn(headers, (value, key) => {\n    if (key && key.toLowerCase() === 'content-type') {\n      contentType = value;\n    }\n  });\n\n  return contentType;\n};\n\nconst getRawQueryString = (url) => {\n  const queryIndex = url.indexOf('?');\n  return queryIndex !== -1 ? url.slice(queryIndex) : '';\n};\n\nconst interpolateVars = (request, envVariables = {}, runtimeVariables = {}, processEnvVars = {}, promptVariables = {}) => {\n  const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};\n  const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};\n  const collectionVariables = request?.collectionVariables || {};\n  const folderVariables = request?.folderVariables || {};\n  const requestVariables = request?.requestVariables || {};\n  // we clone envVars because we don't want to modify the original object\n  envVariables = cloneDeep(envVariables);\n\n  // envVars can inturn have values as {{process.env.VAR_NAME}}\n  // so we need to interpolate envVars first with processEnvVars\n  forOwn(envVariables, (value, key) => {\n    envVariables[key] = interpolate(value, {\n      process: {\n        env: {\n        }\n      }\n    });\n  });\n\n  const _interpolate = (str, { escapeJSONStrings } = {}) => {\n    if (!str || !str.length || typeof str !== 'string') {\n      return str;\n    }\n\n    // runtimeVariables take precedence over envVars\n    const combinedVars = {\n      ...globalEnvironmentVariables,\n      ...collectionVariables,\n      ...envVariables,\n      ...folderVariables,\n      ...requestVariables,\n      ...oauth2CredentialVariables,\n      ...runtimeVariables,\n      ...promptVariables,\n      process: {\n        env: {\n          ...processEnvVars\n        }\n      }\n    };\n\n    return interpolate(str, combinedVars, {\n      escapeJSONStrings\n    });\n  };\n\n  request.url = _interpolate(request.url);\n  const isGrpcRequest = request.mode === 'grpc';\n\n  forOwn(request.headers, (value, key) => {\n    delete request.headers[key];\n    request.headers[_interpolate(key)] = _interpolate(value);\n  });\n\n  const contentType = getContentType(request.headers);\n  const isGraphqlRequest = request.mode === 'graphql';\n\n  // gRPC: interpolate entire body (JSON message template and any other keys).\n  if (isGrpcRequest && request.body) {\n    const jsonDoc = JSON.stringify(request.body);\n    const parsed = _interpolate(jsonDoc, { escapeJSONStrings: true });\n    request.body = JSON.parse(parsed);\n  }\n  // Interpolate WebSocket message body\n  const isWsRequest = request.mode === 'ws';\n  if (isWsRequest && request.body && request.body.ws && Array.isArray(request.body.ws)) {\n    request.body.ws.forEach((message) => {\n      if (message && message.content) {\n        let isJson = false;\n        try {\n          JSON.parse(message.content);\n          isJson = true;\n        } catch (e) {}\n        message.content = _interpolate(message.content, {\n          escapeJSONStrings: isJson\n        });\n      }\n    });\n  }\n\n  // GraphQL: interpolate query and variables in place. We do not stringify the whole body and interpolate that, because variables is a JSON string. Full-body stringify would nest it and double-escape any {{var}} inside.\n  if (isGraphqlRequest && request.data && typeof request.data === 'object') {\n    request.data.query = _interpolate(request.data.query, { escapeJSONStrings: true });\n    request.data.variables = _interpolate(request.data.variables, { escapeJSONStrings: true });\n  }\n\n  if (typeof contentType === 'string' && !isGraphqlRequest) {\n    /*\n      We explicitly avoid interpolating buffer values because the file content is read as a buffer object in raw body mode.\n      Even if the selected file's content type is JSON, this prevents the buffer object from being interpolated.\n    */\n    if (contentType.includes('json') && !Buffer.isBuffer(request.data)) {\n      if (typeof request.data === 'string') {\n        if (request.data.length) {\n          request.data = _interpolate(request.data, {\n            escapeJSONStrings: true\n          });\n        }\n      } else if (typeof request.data === 'object') {\n        try {\n          const jsonDoc = JSON.stringify(request.data);\n          const parsed = _interpolate(jsonDoc, {\n            escapeJSONStrings: true\n          });\n          request.data = JSON.parse(parsed);\n        } catch (err) {}\n      }\n    } else if (contentType === 'application/x-www-form-urlencoded') {\n      if (request.data && Array.isArray(request.data)) {\n        request.data = request.data.map((d) => ({\n          ...d,\n          value: _interpolate(d?.value)\n        }));\n      }\n    } else if (contentType.startsWith('multipart/')) {\n      if (Array.isArray(request?.data) && !isFormData(request.data)) {\n        try {\n          request.data = request?.data?.map((d) => ({\n            ...d,\n            value: _interpolate(d?.value)\n          }));\n        } catch (err) {}\n      }\n    } else {\n      request.data = _interpolate(request.data);\n    }\n  }\n\n  each(request.pathParams, (param) => {\n    param.value = _interpolate(param.value);\n  });\n\n  if (request?.pathParams?.length) {\n    let url = request.url;\n    const urlSearchRaw = getRawQueryString(request.url);\n    if (!url.startsWith('http://') && !url.startsWith('https://')) {\n      url = `http://${url}`;\n    }\n\n    try {\n      url = new URL(url);\n    } catch (e) {\n      throw { message: 'Invalid URL format', originalError: e.message };\n    }\n\n    const urlPathnameInterpolatedWithPathParams = url.pathname\n      .split('/')\n      .filter((path) => path !== '')\n      .map((path) => {\n        // traditional path parameters\n        if (path.startsWith(':')) {\n          const paramName = path.slice(1);\n          const existingPathParam = request.pathParams.find((param) => param.name === paramName);\n          if (!existingPathParam) {\n            return '/' + path;\n          }\n          return '/' + existingPathParam.value;\n        }\n\n        // for OData-style parameters (parameters inside parentheses)\n        // Check if path matches valid OData syntax:\n        // 1. EntitySet('key') or EntitySet(key)\n        // 2. EntitySet(Key1=value1,Key2=value2)\n        // 3. Function(param=value)\n        if (/^[A-Za-z0-9_.-]+\\([^)]*\\)$/.test(path)) {\n          const paramRegex = /[:](\\w+)/g;\n          let match;\n          let result = path;\n          while ((match = paramRegex.exec(path))) {\n            if (match[1]) {\n              let name = match[1].replace(/[')\"`]+$/, '');\n              name = name.replace(/^[('\"`]+/, '');\n              if (name) {\n                const existingPathParam = request.pathParams.find((param) => param.name === name);\n                if (existingPathParam) {\n                  result = result.replace(':' + match[1], existingPathParam.value);\n                }\n              }\n            }\n          }\n          return '/' + result;\n        }\n        return '/' + path;\n      })\n      .join('');\n\n    const trailingSlash = url.pathname.endsWith('/') ? '/' : '';\n    request.url = url.origin + urlPathnameInterpolatedWithPathParams + trailingSlash + urlSearchRaw;\n  }\n\n  if (request.proxy) {\n    request.proxy.protocol = _interpolate(request.proxy.protocol);\n    request.proxy.hostname = _interpolate(request.proxy.hostname);\n    request.proxy.port = _interpolate(request.proxy.port);\n\n    if (request.proxy.auth) {\n      request.proxy.auth.username = _interpolate(request.proxy.auth.username);\n      request.proxy.auth.password = _interpolate(request.proxy.auth.password);\n    }\n  }\n\n  // todo: we have things happening in two places w.r.t basic auth\n  // need to refactor this in the future\n  // the request.auth (basic auth) object gets set inside the prepare-request.js file\n  if (request.basicAuth) {\n    const username = _interpolate(request.basicAuth.username) || '';\n    const password = _interpolate(request.basicAuth.password) || '';\n    // use auth header based approach and delete the request.auth object\n    request.headers['Authorization'] = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;\n    delete request.basicAuth;\n  }\n\n  if (request?.oauth2?.grantType) {\n    let username, password, scope, clientId, clientSecret;\n    switch (request.oauth2.grantType) {\n      case 'password':\n        request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';\n        request.oauth2.refreshTokenUrl = _interpolate(request.oauth2.refreshTokenUrl) || '';\n        request.oauth2.username = _interpolate(request.oauth2.username) || '';\n        request.oauth2.password = _interpolate(request.oauth2.password) || '';\n        request.oauth2.clientId = _interpolate(request.oauth2.clientId) || '';\n        request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';\n        request.oauth2.scope = _interpolate(request.oauth2.scope) || '';\n        request.oauth2.credentialsPlacement = _interpolate(request.oauth2.credentialsPlacement) || '';\n        request.oauth2.credentialsId = _interpolate(request.oauth2.credentialsId) || '';\n        request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';\n        request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';\n        request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';\n        request.oauth2.autoFetchToken = _interpolate(request.oauth2.autoFetchToken);\n        request.oauth2.autoRefreshToken = _interpolate(request.oauth2.autoRefreshToken);\n        break;\n      case 'implicit':\n        request.oauth2.callbackUrl = _interpolate(request.oauth2.callbackUrl) || '';\n        request.oauth2.authorizationUrl = _interpolate(request.oauth2.authorizationUrl) || '';\n        request.oauth2.clientId = _interpolate(request.oauth2.clientId) || '';\n        request.oauth2.scope = _interpolate(request.oauth2.scope) || '';\n        request.oauth2.state = _interpolate(request.oauth2.state) || '';\n        request.oauth2.credentialsId = _interpolate(request.oauth2.credentialsId) || '';\n        request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';\n        request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';\n        request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';\n        request.oauth2.autoFetchToken = _interpolate(request.oauth2.autoFetchToken);\n        break;\n      case 'authorization_code':\n        request.oauth2.callbackUrl = _interpolate(request.oauth2.callbackUrl) || '';\n        request.oauth2.authorizationUrl = _interpolate(request.oauth2.authorizationUrl) || '';\n        request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';\n        request.oauth2.refreshTokenUrl = _interpolate(request.oauth2.refreshTokenUrl) || '';\n        request.oauth2.clientId = _interpolate(request.oauth2.clientId) || '';\n        request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';\n        request.oauth2.scope = _interpolate(request.oauth2.scope) || '';\n        request.oauth2.state = _interpolate(request.oauth2.state) || '';\n        request.oauth2.pkce = _interpolate(request.oauth2.pkce) || false;\n        request.oauth2.credentialsPlacement = _interpolate(request.oauth2.credentialsPlacement) || '';\n        request.oauth2.credentialsId = _interpolate(request.oauth2.credentialsId) || '';\n        request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';\n        request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';\n        request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';\n        request.oauth2.autoFetchToken = _interpolate(request.oauth2.autoFetchToken);\n        request.oauth2.autoRefreshToken = _interpolate(request.oauth2.autoRefreshToken);\n        break;\n      case 'client_credentials':\n        request.oauth2.accessTokenUrl = _interpolate(request.oauth2.accessTokenUrl) || '';\n        request.oauth2.refreshTokenUrl = _interpolate(request.oauth2.refreshTokenUrl) || '';\n        request.oauth2.clientId = _interpolate(request.oauth2.clientId) || '';\n        request.oauth2.clientSecret = _interpolate(request.oauth2.clientSecret) || '';\n        request.oauth2.scope = _interpolate(request.oauth2.scope) || '';\n        request.oauth2.credentialsPlacement = _interpolate(request.oauth2.credentialsPlacement) || '';\n        request.oauth2.credentialsId = _interpolate(request.oauth2.credentialsId) || '';\n        request.oauth2.tokenPlacement = _interpolate(request.oauth2.tokenPlacement) || '';\n        request.oauth2.tokenHeaderPrefix = _interpolate(request.oauth2.tokenHeaderPrefix) || '';\n        request.oauth2.tokenQueryKey = _interpolate(request.oauth2.tokenQueryKey) || '';\n        request.oauth2.autoFetchToken = _interpolate(request.oauth2.autoFetchToken);\n        request.oauth2.autoRefreshToken = _interpolate(request.oauth2.autoRefreshToken);\n        break;\n      default:\n        break;\n    }\n\n    // Interpolate additional parameters for all OAuth2 grant types\n    if (request.oauth2.additionalParameters) {\n      // Interpolate authorization parameters\n      if (Array.isArray(request.oauth2.additionalParameters.authorization)) {\n        request.oauth2.additionalParameters.authorization.forEach((param) => {\n          if (param && param.enabled !== false) {\n            param.name = _interpolate(param.name) || '';\n            param.value = _interpolate(param.value) || '';\n          }\n        });\n      }\n\n      // Interpolate token parameters\n      if (Array.isArray(request.oauth2.additionalParameters.token)) {\n        request.oauth2.additionalParameters.token.forEach((param) => {\n          if (param && param.enabled !== false) {\n            param.name = _interpolate(param.name) || '';\n            param.value = _interpolate(param.value) || '';\n          }\n        });\n      }\n\n      // Interpolate refresh parameters\n      if (Array.isArray(request.oauth2.additionalParameters.refresh)) {\n        request.oauth2.additionalParameters.refresh.forEach((param) => {\n          if (param && param.enabled !== false) {\n            param.name = _interpolate(param.name) || '';\n            param.value = _interpolate(param.value) || '';\n          }\n        });\n      }\n    }\n  }\n\n  // interpolate vars for aws sigv4 auth\n  if (request.awsv4config) {\n    request.awsv4config.accessKeyId = _interpolate(request.awsv4config.accessKeyId) || '';\n    request.awsv4config.secretAccessKey = _interpolate(request.awsv4config.secretAccessKey) || '';\n    request.awsv4config.sessionToken = _interpolate(request.awsv4config.sessionToken) || '';\n    request.awsv4config.service = _interpolate(request.awsv4config.service) || '';\n    request.awsv4config.region = _interpolate(request.awsv4config.region) || '';\n    request.awsv4config.profileName = _interpolate(request.awsv4config.profileName) || '';\n  }\n\n  // interpolate vars for digest auth\n  if (request.digestConfig) {\n    request.digestConfig.username = _interpolate(request.digestConfig.username) || '';\n    request.digestConfig.password = _interpolate(request.digestConfig.password) || '';\n  }\n\n  // interpolate vars for wsse auth\n  if (request.wsse) {\n    request.wsse.username = _interpolate(request.wsse.username) || '';\n    request.wsse.password = _interpolate(request.wsse.password) || '';\n  }\n\n  // interpolate vars for ntlmConfig auth\n  if (request.ntlmConfig) {\n    request.ntlmConfig.username = _interpolate(request.ntlmConfig.username) || '';\n    request.ntlmConfig.password = _interpolate(request.ntlmConfig.password) || '';\n    request.ntlmConfig.domain = _interpolate(request.ntlmConfig.domain) || '';\n  }\n\n  if (request?.auth) delete request.auth;\n\n  return request;\n};\n\nmodule.exports = interpolateVars;\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js",
    "content": "const { get, each } = require('lodash');\nconst { interpolate } = require('@usebruno/common');\nconst { getIntrospectionQuery } = require('graphql');\nconst { setAuthHeaders } = require('./prepare-request');\n\nconst prepareGqlIntrospectionRequest = (endpoint, resolvedVars, request, collectionRoot) => {\n  if (endpoint && endpoint.length) {\n    endpoint = interpolate(endpoint, resolvedVars);\n  }\n\n  const queryParams = {\n    query: getIntrospectionQuery()\n  };\n\n  let axiosRequest = {\n    method: 'POST',\n    url: endpoint,\n    headers: {\n      ...mapHeaders(request.headers, get(collectionRoot, 'request.headers', []), resolvedVars),\n      'Accept': 'application/json',\n      'Content-Type': 'application/json'\n    },\n    data: JSON.stringify(queryParams)\n  };\n\n  return setAuthHeaders(axiosRequest, request, collectionRoot);\n};\n\nconst mapHeaders = (requestHeaders, collectionHeaders, resolvedVars) => {\n  const headers = {};\n\n  // Add collection headers first\n  each(collectionHeaders, (h) => {\n    if (h.enabled) {\n      headers[h.name] = interpolate(h.value, resolvedVars);\n    }\n  });\n\n  // Then add request headers, which will overwrite if names overlap\n  each(requestHeaders, (h) => {\n    if (h.enabled) {\n      headers[h.name] = interpolate(h.value, resolvedVars);\n    }\n  });\n\n  return headers;\n};\n\nmodule.exports = prepareGqlIntrospectionRequest;\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/network/prepare-grpc-request.js",
    "content": "const { cloneDeep, each, get } = require('lodash');\nconst interpolateVars = require('./interpolate-vars');\nconst { getEnvVars, getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars, mergeAuth, getFormattedCollectionOauth2Credentials } = require('../../utils/collection');\nconst { getProcessEnvVars } = require('../../store/process-env');\nconst { getOAuth2TokenUsingPasswordCredentials, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingAuthorizationCode } = require('../../utils/oauth2');\nconst { setAuthHeaders } = require('./prepare-request');\nconst { getCertsAndProxyConfig } = require('./cert-utils');\nconst { interpolateString } = require('./interpolate-string');\n\nconst processHeaders = (headers) => {\n  Object.entries(headers).forEach(([key, value]) => {\n    if (key?.toLowerCase().endsWith('-bin')) {\n      headers[key] = Buffer.from(value, 'base64');\n    }\n  });\n};\n\nconst placeOAuth2Token = (grpcRequest, credentials, tokenPlacement, tokenHeaderPrefix, tokenQueryKey) => {\n  if (tokenPlacement === 'header') {\n    grpcRequest.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`;\n  } else {\n    try {\n      const url = new URL(grpcRequest.url);\n      url?.searchParams?.set(tokenQueryKey, credentials?.access_token);\n      grpcRequest.url = url?.toString();\n    } catch (error) {\n      console.error('Failed to parse URL for OAuth2 token placement:', error);\n    }\n  }\n};\n\nconst configureRequest = async (grpcRequest, request, collection, envVars, runtimeVariables, processEnvVars, promptVariables, certsAndProxyConfig) => {\n  if (grpcRequest.oauth2) {\n    let requestCopy = cloneDeep(grpcRequest);\n    const { uid: collectionUid, pathname: collectionPath, globalEnvironmentVariables } = collection;\n    const { oauth2: { grantType, tokenPlacement, tokenHeaderPrefix, tokenQueryKey, accessTokenUrl, refreshTokenUrl } = {}, collectionVariables, folderVariables, requestVariables } = requestCopy || {};\n    let credentials, credentialsId, oauth2Url, debugInfo;\n\n    // Get cert/proxy configs for token and refresh URLs\n    let certsAndProxyConfigForTokenUrl = certsAndProxyConfig;\n    let certsAndProxyConfigForRefreshUrl = certsAndProxyConfig;\n\n    if (accessTokenUrl && grantType !== 'implicit') {\n      const interpolatedTokenUrl = interpolateString(accessTokenUrl, {\n        globalEnvironmentVariables,\n        collectionVariables,\n        envVars,\n        folderVariables,\n        requestVariables,\n        runtimeVariables,\n        processEnvVars,\n        promptVariables\n      });\n      const tokenRequestForConfig = { ...requestCopy, url: interpolatedTokenUrl };\n      certsAndProxyConfigForTokenUrl = await getCertsAndProxyConfig({\n        collectionUid,\n        collection,\n        request: tokenRequestForConfig,\n        envVars,\n        runtimeVariables,\n        processEnvVars,\n        collectionPath,\n        globalEnvironmentVariables\n      });\n    }\n\n    const tokenUrlForRefresh = refreshTokenUrl || accessTokenUrl;\n    if (tokenUrlForRefresh && grantType !== 'implicit') {\n      const interpolatedRefreshUrl = interpolateString(tokenUrlForRefresh, {\n        globalEnvironmentVariables,\n        collectionVariables,\n        envVars,\n        folderVariables,\n        requestVariables,\n        runtimeVariables,\n        processEnvVars,\n        promptVariables\n      });\n      const refreshRequestForConfig = { ...requestCopy, url: interpolatedRefreshUrl };\n      certsAndProxyConfigForRefreshUrl = await getCertsAndProxyConfig({\n        collectionUid,\n        collection,\n        request: refreshRequestForConfig,\n        envVars,\n        runtimeVariables,\n        processEnvVars,\n        collectionPath,\n        globalEnvironmentVariables\n      });\n    }\n\n    try {\n      switch (grantType) {\n        case 'authorization_code':\n          interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars, promptVariables);\n          ({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingAuthorizationCode({ request: requestCopy, collectionUid: collection.uid, certsAndProxyConfigForTokenUrl, certsAndProxyConfigForRefreshUrl }));\n          grpcRequest.oauth2Credentials = { credentials, url: oauth2Url, collectionUid: collection.uid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };\n          placeOAuth2Token(grpcRequest, credentials, tokenPlacement, tokenHeaderPrefix, tokenQueryKey);\n          break;\n        case 'client_credentials':\n          interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars, promptVariables);\n          ({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingClientCredentials({ request: requestCopy, collectionUid: collection.uid, certsAndProxyConfigForTokenUrl, certsAndProxyConfigForRefreshUrl }));\n          grpcRequest.oauth2Credentials = { credentials, url: oauth2Url, collectionUid: collection.uid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };\n          placeOAuth2Token(grpcRequest, credentials, tokenPlacement, tokenHeaderPrefix, tokenQueryKey);\n          break;\n        case 'password':\n          interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars, promptVariables);\n          ({ credentials, url: oauth2Url, credentialsId, debugInfo } = await getOAuth2TokenUsingPasswordCredentials({ request: requestCopy, collectionUid: collection.uid, certsAndProxyConfigForTokenUrl, certsAndProxyConfigForRefreshUrl }));\n          grpcRequest.oauth2Credentials = { credentials, url: oauth2Url, collectionUid: collection.uid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };\n          placeOAuth2Token(grpcRequest, credentials, tokenPlacement, tokenHeaderPrefix, tokenQueryKey);\n          break;\n      }\n    } catch (error) {\n      console.error('Failed to configure OAuth2 request:', error);\n      throw error;\n    }\n  }\n};\n\nconst prepareGrpcRequest = async (item, collection, environment, runtimeVariables) => {\n  const request = item.draft ? item.draft.request : item.request;\n  const collectionRoot = collection?.draft?.root ? get(collection, 'draft.root', {}) : get(collection, 'root', {});\n  const headers = {};\n  const url = request.url;\n  const { promptVariables = {} } = collection;\n\n  const scriptFlow = collection?.brunoConfig?.scripts?.flow ?? 'sandwich';\n  const requestTreePath = getTreePathFromCollectionToItem(collection, item);\n  if (requestTreePath && requestTreePath.length > 0) {\n    mergeAuth(collection, request, requestTreePath);\n    mergeHeaders(collection, request, requestTreePath);\n    mergeScripts(collection, request, requestTreePath, scriptFlow);\n    mergeVars(collection, request, requestTreePath);\n    request.globalEnvironmentVariables = collection?.globalEnvironmentVariables;\n    request.oauth2CredentialVariables = getFormattedCollectionOauth2Credentials({ oauth2Credentials: collection?.oauth2Credentials });\n    request.promptVariables = promptVariables;\n  }\n\n  each(get(request, 'headers', []), (h) => {\n    if (h.enabled && h.name.length > 0) {\n      headers[h.name] = h.value;\n    }\n  });\n\n  const processEnvVars = getProcessEnvVars(collection.uid);\n  const envVars = getEnvVars(environment);\n\n  let grpcRequest = {\n    uid: item.uid,\n    mode: request.body.mode,\n    method: request.method,\n    methodType: request.methodType,\n    url,\n    headers,\n    processEnvVars,\n    envVars,\n    runtimeVariables,\n    promptVariables,\n    body: request.body,\n    protoPath: request.protoPath,\n    // Add variable properties for interpolation\n    vars: request.vars,\n    collectionVariables: request.collectionVariables,\n    folderVariables: request.folderVariables,\n    requestVariables: request.requestVariables,\n    globalEnvironmentVariables: request.globalEnvironmentVariables,\n    oauth2CredentialVariables: request.oauth2CredentialVariables\n  };\n\n  grpcRequest = setAuthHeaders(grpcRequest, request, collectionRoot);\n\n  interpolateVars(grpcRequest, envVars, runtimeVariables, processEnvVars, promptVariables);\n  processHeaders(grpcRequest.headers);\n\n  return grpcRequest;\n};\n\nmodule.exports = prepareGrpcRequest;\nmodule.exports.configureRequest = configureRequest;\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/network/prepare-request.js",
    "content": "const { get, each, filter, find } = require('lodash');\nconst decomment = require('decomment');\nconst crypto = require('node:crypto');\nconst fs = require('node:fs');\nconst { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars, getFormattedCollectionOauth2Credentials, mergeAuth } = require('../../utils/collection');\nconst path = require('node:path');\nconst { isLargeFile } = require('../../utils/filesystem');\n\nconst STREAMING_FILE_SIZE_THRESHOLD = 20 * 1024 * 1024; // 20MB\n\nconst setAuthHeaders = (axiosRequest, request, collectionRoot) => {\n  const collectionAuth = get(collectionRoot, 'request.auth');\n  if (collectionAuth && request.auth.mode === 'inherit') {\n    switch (collectionAuth.mode) {\n      case 'awsv4':\n        axiosRequest.awsv4config = {\n          accessKeyId: get(collectionAuth, 'awsv4.accessKeyId'),\n          secretAccessKey: get(collectionAuth, 'awsv4.secretAccessKey'),\n          sessionToken: get(collectionAuth, 'awsv4.sessionToken'),\n          service: get(collectionAuth, 'awsv4.service'),\n          region: get(collectionAuth, 'awsv4.region'),\n          profileName: get(collectionAuth, 'awsv4.profileName')\n        };\n        break;\n      case 'basic':\n        axiosRequest.basicAuth = {\n          username: get(collectionAuth, 'basic.username'),\n          password: get(collectionAuth, 'basic.password')\n        };\n        break;\n      case 'bearer':\n        axiosRequest.headers['Authorization'] = `Bearer ${get(collectionAuth, 'bearer.token', '')}`;\n        break;\n      case 'digest':\n        axiosRequest.digestConfig = {\n          username: get(collectionAuth, 'digest.username'),\n          password: get(collectionAuth, 'digest.password')\n        };\n        break;\n      case 'ntlm':\n        axiosRequest.ntlmConfig = {\n          username: get(collectionAuth, 'ntlm.username'),\n          password: get(collectionAuth, 'ntlm.password'),\n          domain: get(collectionAuth, 'ntlm.domain')\n        };\n        break;\n      case 'wsse':\n        const username = get(collectionAuth, 'wsse.username', '');\n        const password = get(collectionAuth, 'wsse.password', '');\n\n        const ts = new Date().toISOString();\n        const nonce = crypto.randomBytes(16).toString('hex');\n\n        // Create the password digest using SHA-1 as required for WSSE\n        const hash = crypto.createHash('sha1');\n        hash.update(nonce + ts + password);\n        const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64');\n\n        // Construct the WSSE header\n        axiosRequest.headers[\n          'X-WSSE'\n        ] = `UsernameToken Username=\"${username}\", PasswordDigest=\"${digest}\", Nonce=\"${nonce}\", Created=\"${ts}\"`;\n        break;\n      case 'apikey':\n        const apiKeyAuth = get(collectionAuth, 'apikey');\n        if (apiKeyAuth.key.length === 0) break;\n        if (apiKeyAuth.placement === 'header') {\n          axiosRequest.headers[apiKeyAuth.key] = apiKeyAuth.value;\n        } else if (apiKeyAuth.placement === 'queryparams') {\n          // If the API key authentication is set and its placement is 'queryparams', add it to the axios request object. This will be used in the configureRequest function to append the API key to the query parameters of the request URL.\n          axiosRequest.apiKeyAuthValueForQueryParams = apiKeyAuth;\n        }\n        break;\n      case 'oauth2':\n        const grantType = get(collectionAuth, 'oauth2.grantType');\n        switch (grantType) {\n          case 'password':\n            axiosRequest.oauth2 = {\n              grantType: grantType,\n              accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),\n              refreshTokenUrl: get(collectionAuth, 'oauth2.refreshTokenUrl'),\n              username: get(collectionAuth, 'oauth2.username'),\n              password: get(collectionAuth, 'oauth2.password'),\n              clientId: get(collectionAuth, 'oauth2.clientId'),\n              clientSecret: get(collectionAuth, 'oauth2.clientSecret'),\n              scope: get(collectionAuth, 'oauth2.scope'),\n              credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),\n              credentialsId: get(collectionAuth, 'oauth2.credentialsId'),\n              tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),\n              tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),\n              tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),\n              tokenSource: get(collectionAuth, 'oauth2.tokenSource'),\n              autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),\n              autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken'),\n              additionalParameters: get(collectionAuth, 'oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })\n            };\n            break;\n          case 'authorization_code':\n            axiosRequest.oauth2 = {\n              grantType: grantType,\n              callbackUrl: get(collectionAuth, 'oauth2.callbackUrl'),\n              authorizationUrl: get(collectionAuth, 'oauth2.authorizationUrl'),\n              accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),\n              refreshTokenUrl: get(collectionAuth, 'oauth2.refreshTokenUrl'),\n              clientId: get(collectionAuth, 'oauth2.clientId'),\n              clientSecret: get(collectionAuth, 'oauth2.clientSecret'),\n              scope: get(collectionAuth, 'oauth2.scope'),\n              state: get(collectionAuth, 'oauth2.state'),\n              pkce: get(collectionAuth, 'oauth2.pkce'),\n              credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),\n              credentialsId: get(collectionAuth, 'oauth2.credentialsId'),\n              tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),\n              tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),\n              tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),\n              tokenSource: get(collectionAuth, 'oauth2.tokenSource'),\n              autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),\n              autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken'),\n              additionalParameters: get(collectionAuth, 'oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })\n            };\n            break;\n          case 'implicit':\n            axiosRequest.oauth2 = {\n              grantType: grantType,\n              callbackUrl: get(collectionAuth, 'oauth2.callbackUrl'),\n              authorizationUrl: get(collectionAuth, 'oauth2.authorizationUrl'),\n              clientId: get(collectionAuth, 'oauth2.clientId'),\n              scope: get(collectionAuth, 'oauth2.scope'),\n              state: get(collectionAuth, 'oauth2.state'),\n              credentialsId: get(collectionAuth, 'oauth2.credentialsId'),\n              tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),\n              tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),\n              tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),\n              tokenSource: get(collectionAuth, 'oauth2.tokenSource'),\n              autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),\n              additionalParameters: get(collectionAuth, 'oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })\n            };\n            break;\n          case 'client_credentials':\n            axiosRequest.oauth2 = {\n              grantType: grantType,\n              accessTokenUrl: get(collectionAuth, 'oauth2.accessTokenUrl'),\n              refreshTokenUrl: get(collectionAuth, 'oauth2.refreshTokenUrl'),\n              clientId: get(collectionAuth, 'oauth2.clientId'),\n              clientSecret: get(collectionAuth, 'oauth2.clientSecret'),\n              scope: get(collectionAuth, 'oauth2.scope'),\n              credentialsPlacement: get(collectionAuth, 'oauth2.credentialsPlacement'),\n              credentialsId: get(collectionAuth, 'oauth2.credentialsId'),\n              tokenPlacement: get(collectionAuth, 'oauth2.tokenPlacement'),\n              tokenHeaderPrefix: get(collectionAuth, 'oauth2.tokenHeaderPrefix'),\n              tokenQueryKey: get(collectionAuth, 'oauth2.tokenQueryKey'),\n              tokenSource: get(collectionAuth, 'oauth2.tokenSource'),\n              autoFetchToken: get(collectionAuth, 'oauth2.autoFetchToken'),\n              autoRefreshToken: get(collectionAuth, 'oauth2.autoRefreshToken'),\n              additionalParameters: get(collectionAuth, 'oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })\n            };\n            break;\n        }\n        break;\n    }\n  }\n\n  if (request.auth) {\n    switch (request.auth.mode) {\n      case 'awsv4':\n        axiosRequest.awsv4config = {\n          accessKeyId: get(request, 'auth.awsv4.accessKeyId'),\n          secretAccessKey: get(request, 'auth.awsv4.secretAccessKey'),\n          sessionToken: get(request, 'auth.awsv4.sessionToken'),\n          service: get(request, 'auth.awsv4.service'),\n          region: get(request, 'auth.awsv4.region'),\n          profileName: get(request, 'auth.awsv4.profileName')\n        };\n        break;\n      case 'basic':\n        axiosRequest.basicAuth = {\n          username: get(request, 'auth.basic.username'),\n          password: get(request, 'auth.basic.password')\n        };\n        break;\n      case 'bearer':\n        axiosRequest.headers['Authorization'] = `Bearer ${get(request, 'auth.bearer.token', '')}`;\n        break;\n      case 'digest':\n        axiosRequest.digestConfig = {\n          username: get(request, 'auth.digest.username'),\n          password: get(request, 'auth.digest.password')\n        };\n        break;\n      case 'ntlm':\n        axiosRequest.ntlmConfig = {\n          username: get(request, 'auth.ntlm.username'),\n          password: get(request, 'auth.ntlm.password'),\n          domain: get(request, 'auth.ntlm.domain')\n        };\n      case 'oauth2':\n        const grantType = get(request, 'auth.oauth2.grantType');\n        switch (grantType) {\n          case 'password':\n            axiosRequest.oauth2 = {\n              grantType: grantType,\n              accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),\n              refreshTokenUrl: get(request, 'auth.oauth2.refreshTokenUrl'),\n              username: get(request, 'auth.oauth2.username'),\n              password: get(request, 'auth.oauth2.password'),\n              clientId: get(request, 'auth.oauth2.clientId'),\n              clientSecret: get(request, 'auth.oauth2.clientSecret'),\n              scope: get(request, 'auth.oauth2.scope'),\n              credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),\n              credentialsId: get(request, 'auth.oauth2.credentialsId'),\n              tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),\n              tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),\n              tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),\n              tokenSource: get(request, 'auth.oauth2.tokenSource'),\n              autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),\n              autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken'),\n              additionalParameters: get(request, 'auth.oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })\n            };\n            break;\n          case 'authorization_code':\n            axiosRequest.oauth2 = {\n              grantType: grantType,\n              callbackUrl: get(request, 'auth.oauth2.callbackUrl'),\n              authorizationUrl: get(request, 'auth.oauth2.authorizationUrl'),\n              accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),\n              refreshTokenUrl: get(request, 'auth.oauth2.refreshTokenUrl'),\n              clientId: get(request, 'auth.oauth2.clientId'),\n              clientSecret: get(request, 'auth.oauth2.clientSecret'),\n              scope: get(request, 'auth.oauth2.scope'),\n              state: get(request, 'auth.oauth2.state'),\n              pkce: get(request, 'auth.oauth2.pkce'),\n              credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),\n              credentialsId: get(request, 'auth.oauth2.credentialsId'),\n              tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),\n              tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),\n              tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),\n              tokenSource: get(request, 'auth.oauth2.tokenSource'),\n              autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),\n              autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken'),\n              additionalParameters: get(request, 'auth.oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })\n            };\n            break;\n          case 'implicit':\n            axiosRequest.oauth2 = {\n              grantType: grantType,\n              callbackUrl: get(request, 'auth.oauth2.callbackUrl'),\n              authorizationUrl: get(request, 'auth.oauth2.authorizationUrl'),\n              clientId: get(request, 'auth.oauth2.clientId'),\n              scope: get(request, 'auth.oauth2.scope'),\n              state: get(request, 'auth.oauth2.state'),\n              credentialsId: get(request, 'auth.oauth2.credentialsId'),\n              tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),\n              tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),\n              tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),\n              tokenSource: get(request, 'auth.oauth2.tokenSource'),\n              autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),\n              additionalParameters: get(request, 'auth.oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })\n            };\n            break;\n          case 'client_credentials':\n            axiosRequest.oauth2 = {\n              grantType: grantType,\n              accessTokenUrl: get(request, 'auth.oauth2.accessTokenUrl'),\n              refreshTokenUrl: get(request, 'auth.oauth2.refreshTokenUrl'),\n              clientId: get(request, 'auth.oauth2.clientId'),\n              clientSecret: get(request, 'auth.oauth2.clientSecret'),\n              scope: get(request, 'auth.oauth2.scope'),\n              credentialsPlacement: get(request, 'auth.oauth2.credentialsPlacement'),\n              credentialsId: get(request, 'auth.oauth2.credentialsId'),\n              tokenPlacement: get(request, 'auth.oauth2.tokenPlacement'),\n              tokenHeaderPrefix: get(request, 'auth.oauth2.tokenHeaderPrefix'),\n              tokenQueryKey: get(request, 'auth.oauth2.tokenQueryKey'),\n              tokenSource: get(request, 'auth.oauth2.tokenSource'),\n              autoFetchToken: get(request, 'auth.oauth2.autoFetchToken'),\n              autoRefreshToken: get(request, 'auth.oauth2.autoRefreshToken'),\n              additionalParameters: get(request, 'auth.oauth2.additionalParameters', { authorization: [], token: [], refresh: [] })\n            };\n            break;\n        }\n        break;\n      case 'wsse':\n        const username = get(request, 'auth.wsse.username', '');\n        const password = get(request, 'auth.wsse.password', '');\n\n        const ts = new Date().toISOString();\n        const nonce = crypto.randomBytes(16).toString('hex');\n\n        // Create the password digest using SHA-1 as required for WSSE\n        const hash = crypto.createHash('sha1');\n        hash.update(nonce + ts + password);\n        const digest = Buffer.from(hash.digest('hex').toString('utf8')).toString('base64');\n\n        // Construct the WSSE header\n        axiosRequest.headers[\n          'X-WSSE'\n        ] = `UsernameToken Username=\"${username}\", PasswordDigest=\"${digest}\", Nonce=\"${nonce}\", Created=\"${ts}\"`;\n        break;\n      case 'apikey':\n        const apiKeyAuth = get(request, 'auth.apikey');\n        if (apiKeyAuth.key.length === 0) break;\n        if (apiKeyAuth.placement === 'header') {\n          axiosRequest.headers[apiKeyAuth.key] = apiKeyAuth.value;\n        } else if (apiKeyAuth.placement === 'queryparams') {\n          // If the API key authentication is set and its placement is 'queryparams', add it to the axios request object. This will be used in the configureRequest function to append the API key to the query parameters of the request URL.\n          axiosRequest.apiKeyAuthValueForQueryParams = apiKeyAuth;\n        }\n        break;\n    }\n  }\n\n  return axiosRequest;\n};\n\nconst prepareRequest = async (item, collection = {}, abortController) => {\n  const request = item.draft ? item.draft.request : item.request;\n  const settings = item.draft?.settings ?? item.settings;\n  const collectionRoot = collection?.draft?.root ? get(collection, 'draft.root', {}) : get(collection, 'root', {});\n  const collectionPath = collection?.pathname;\n  const headers = {};\n  let contentTypeDefined = false;\n  let url = request.url;\n\n  each(get(collectionRoot, 'request.headers', []), (h) => {\n    if (h.enabled && h.name?.toLowerCase() === 'content-type') {\n      contentTypeDefined = true;\n      return false;\n    }\n  });\n\n  const scriptFlow = collection?.brunoConfig?.scripts?.flow ?? 'sandwich';\n  const requestTreePath = getTreePathFromCollectionToItem(collection, item);\n  if (requestTreePath && requestTreePath.length > 0) {\n    mergeHeaders(collection, request, requestTreePath);\n    mergeScripts(collection, request, requestTreePath, scriptFlow);\n    mergeVars(collection, request, requestTreePath);\n    mergeAuth(collection, request, requestTreePath);\n    request.globalEnvironmentVariables = collection?.globalEnvironmentVariables;\n    request.oauth2CredentialVariables = getFormattedCollectionOauth2Credentials({ oauth2Credentials: collection?.oauth2Credentials });\n    request.promptVariables = collection?.promptVariables || {};\n  }\n\n  each(get(request, 'headers', []), (h) => {\n    if (h.enabled && h.name.length > 0) {\n      headers[h.name] = h.value;\n      if (h.name.toLowerCase() === 'content-type') {\n        contentTypeDefined = true;\n      }\n    }\n  });\n\n  let axiosRequest = {\n    mode: request.body.mode,\n    method: request.method,\n    url,\n    headers,\n    name: item.name,\n    pathname: item.pathname,\n    tags: item.tags || [],\n    pathParams: request.params?.filter((param) => param.type === 'path'),\n    settings,\n    responseType: 'arraybuffer'\n  };\n\n  axiosRequest = setAuthHeaders(axiosRequest, request, collectionRoot);\n\n  if (request.body.mode === 'json') {\n    if (!contentTypeDefined) {\n      axiosRequest.headers['content-type'] = 'application/json';\n    }\n    try {\n      axiosRequest.data = decomment(request?.body?.json);\n    } catch (error) {\n      axiosRequest.data = request?.body?.json;\n    }\n  }\n\n  if (request.body.mode === 'text') {\n    if (!contentTypeDefined) {\n      axiosRequest.headers['content-type'] = 'text/plain';\n    }\n    axiosRequest.data = request.body.text;\n  }\n\n  if (request.body.mode === 'xml') {\n    if (!contentTypeDefined) {\n      axiosRequest.headers['content-type'] = 'application/xml';\n    }\n    axiosRequest.data = request.body.xml;\n  }\n\n  if (request.body.mode === 'sparql') {\n    if (!contentTypeDefined) {\n      axiosRequest.headers['content-type'] = 'application/sparql-query';\n    }\n    axiosRequest.data = request.body.sparql;\n  }\n\n  if (request.body.mode === 'file') {\n    if (!contentTypeDefined) {\n      axiosRequest.headers['content-type'] = 'application/octet-stream'; // Default headers for binary file uploads\n    }\n\n    const bodyFile = find(request.body.file, (param) => param.selected);\n    if (bodyFile) {\n      let { filePath, contentType } = bodyFile;\n\n      axiosRequest.headers['content-type'] = contentType;\n      if (filePath) {\n        if (!path.isAbsolute(filePath)) {\n          filePath = path.join(collectionPath, filePath);\n        }\n\n        try {\n          // Large files can cause \"JavaScript heap out of memory\" errors when loaded entirely into memory.\n          if (isLargeFile(filePath, STREAMING_FILE_SIZE_THRESHOLD)) {\n            // For large files: Use streaming to avoid memory issues\n            axiosRequest.data = fs.createReadStream(filePath);\n          } else {\n            // For smaller files: Use synchronous read for better performance\n            axiosRequest.data = fs.readFileSync(filePath);\n          }\n        } catch (error) {\n          console.error('Error reading file:', error);\n        }\n      }\n    }\n  }\n\n  if (request.body.mode === 'formUrlEncoded') {\n    if (!contentTypeDefined) {\n      axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded';\n    }\n    const enabledParams = filter(request.body.formUrlEncoded, (p) => p.enabled);\n    axiosRequest.data = enabledParams;\n  }\n\n  if (request.body.mode === 'multipartForm') {\n    if (!contentTypeDefined) {\n      axiosRequest.headers['content-type'] = 'multipart/form-data';\n    }\n    const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);\n    axiosRequest.data = enabledParams;\n  }\n\n  if (request.body.mode === 'graphql') {\n    const graphqlQuery = {\n      query: get(request, 'body.graphql.query'),\n      // https://github.com/usebruno/bruno/issues/884 - we must only parse the variables after the variable interpolation\n      variables: decomment(get(request, 'body.graphql.variables') || '{}')\n    };\n    if (!contentTypeDefined) {\n      axiosRequest.headers['content-type'] = 'application/json';\n    }\n    axiosRequest.data = graphqlQuery;\n  }\n\n  // if the mode is 'none' then set the content-type header to false. #1693\n  if (request.body.mode === 'none' && request.auth.mode !== 'awsv4') {\n    if (!contentTypeDefined) {\n      axiosRequest.headers['content-type'] = false;\n    }\n  }\n\n  if (request.script) {\n    axiosRequest.script = request.script;\n  }\n\n  if (request.tests) {\n    axiosRequest.tests = request.tests;\n  }\n\n  axiosRequest.vars = request.vars;\n  axiosRequest.collectionVariables = request.collectionVariables;\n  axiosRequest.folderVariables = request.folderVariables;\n  axiosRequest.requestVariables = request.requestVariables;\n  axiosRequest.promptVariables = request.promptVariables;\n  axiosRequest.globalEnvironmentVariables = request.globalEnvironmentVariables;\n  axiosRequest.oauth2CredentialVariables = request.oauth2CredentialVariables;\n  axiosRequest.assertions = request.assertions;\n  axiosRequest.oauth2Credentials = request.oauth2Credentials;\n\n  return axiosRequest;\n};\n\nmodule.exports = {\n  prepareRequest,\n  setAuthHeaders\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/network/ws-event-handlers.js",
    "content": "const { ipcMain, app } = require('electron');\nconst { WsClient } = require('@usebruno/requests');\nconst { safeParseJSON, safeStringifyJSON } = require('../../utils/common');\nconst { cloneDeep, each, get } = require('lodash');\nconst interpolateVars = require('./interpolate-vars');\nconst { preferencesUtil } = require('../../store/preferences');\nconst { getCertsAndProxyConfig } = require('./cert-utils');\nconst {\n  getEnvVars,\n  getTreePathFromCollectionToItem,\n  mergeHeaders,\n  mergeScripts,\n  mergeVars,\n  mergeAuth,\n  getFormattedCollectionOauth2Credentials\n} = require('../../utils/collection');\nconst { getProcessEnvVars } = require('../../store/process-env');\nconst {\n  getOAuth2TokenUsingPasswordCredentials,\n  getOAuth2TokenUsingClientCredentials,\n  getOAuth2TokenUsingAuthorizationCode\n} = require('../../utils/oauth2');\nconst { interpolateString } = require('./interpolate-string');\nconst path = require('node:path');\nconst { setAuthHeaders } = require('./prepare-request');\n\nconst prepareWsRequest = async (item, collection, environment, runtimeVariables, certsAndProxyConfig = {}) => {\n  const request = item.draft ? item.draft.request : item.request;\n  const collectionRoot = collection?.draft?.root ? get(collection, 'draft.root', {}) : get(collection, 'root', {});\n  const brunoConfig = collection.draft?.brunoConfig\n    ? get(collection, 'draft.brunoConfig', {})\n    : get(collection, 'brunoConfig', {});\n  const rawHeaders = cloneDeep(request.headers ?? []);\n  const headers = {};\n\n  const scriptFlow = brunoConfig?.scripts?.flow ?? 'sandwich';\n  const requestTreePath = getTreePathFromCollectionToItem(collection, item);\n  if (requestTreePath && requestTreePath.length > 0) {\n    mergeHeaders(collection, request, requestTreePath);\n    mergeScripts(collection, request, requestTreePath, scriptFlow);\n    mergeVars(collection, request, requestTreePath);\n    mergeAuth(collection, request, requestTreePath);\n    request.globalEnvironmentVariables = collection?.globalEnvironmentVariables;\n    request.oauth2CredentialVariables = getFormattedCollectionOauth2Credentials({\n      oauth2Credentials: collection?.oauth2Credentials\n    });\n  }\n\n  each(get(collectionRoot, 'request.headers', []), (h) => {\n    if (h.enabled && h.name?.toLowerCase() === 'content-type') {\n      return false;\n    }\n  });\n\n  each(get(request, 'headers', []), (h) => {\n    if (h.enabled) {\n      headers[h.name] = h.value;\n    }\n  });\n\n  const socketProtocols = rawHeaders\n    .filter((header) => {\n      return header.name && header.name.toLowerCase() === 'sec-websocket-protocol' && header.enabled;\n    })\n    .map((d) => d.value.trim())\n    .join(',');\n\n  if (socketProtocols.length > 0) {\n    headers['Sec-WebSocket-Protocol'] = socketProtocols;\n  }\n\n  const envVars = getEnvVars(environment);\n  const processEnvVars = getProcessEnvVars(collection.uid);\n  const { promptVariables = {} } = collection;\n\n  let wsRequest = {\n    uid: item.uid,\n    mode: request.body.mode,\n    url: request.url,\n    headers,\n    processEnvVars,\n    envVars,\n    runtimeVariables,\n    body: request.body,\n    // Add variable properties for interpolation\n    vars: request.vars,\n    collectionVariables: request.collectionVariables,\n    folderVariables: request.folderVariables,\n    requestVariables: request.requestVariables,\n    globalEnvironmentVariables: request.globalEnvironmentVariables,\n    oauth2CredentialVariables: request.oauth2CredentialVariables\n  };\n\n  wsRequest = setAuthHeaders(wsRequest, request, collection);\n\n  if (wsRequest.oauth2) {\n    let requestCopy = cloneDeep(wsRequest);\n    const { oauth2: { grantType, tokenPlacement, tokenHeaderPrefix, tokenQueryKey, accessTokenUrl, refreshTokenUrl } = {}, collectionVariables, folderVariables, requestVariables } = requestCopy || {};\n\n    // Get cert/proxy configs for token and refresh URLs\n    let certsAndProxyConfigForTokenUrl = certsAndProxyConfig;\n    let certsAndProxyConfigForRefreshUrl = certsAndProxyConfig;\n\n    if (accessTokenUrl && grantType !== 'implicit') {\n      const interpolatedTokenUrl = interpolateString(accessTokenUrl, {\n        globalEnvironmentVariables: request.globalEnvironmentVariables,\n        collectionVariables,\n        envVars,\n        folderVariables,\n        requestVariables,\n        runtimeVariables,\n        processEnvVars,\n        promptVariables\n      });\n      const tokenRequestForConfig = { ...requestCopy, url: interpolatedTokenUrl };\n      certsAndProxyConfigForTokenUrl = await getCertsAndProxyConfig({\n        collectionUid: collection.uid,\n        collection,\n        request: tokenRequestForConfig,\n        envVars,\n        runtimeVariables,\n        processEnvVars,\n        collectionPath: collection.pathname,\n        globalEnvironmentVariables: request.globalEnvironmentVariables\n      });\n    }\n\n    const tokenUrlForRefresh = refreshTokenUrl || accessTokenUrl;\n    if (tokenUrlForRefresh && grantType !== 'implicit') {\n      const interpolatedRefreshUrl = interpolateString(tokenUrlForRefresh, {\n        globalEnvironmentVariables: request.globalEnvironmentVariables,\n        collectionVariables,\n        envVars,\n        folderVariables,\n        requestVariables,\n        runtimeVariables,\n        processEnvVars,\n        promptVariables\n      });\n      const refreshRequestForConfig = { ...requestCopy, url: interpolatedRefreshUrl };\n      certsAndProxyConfigForRefreshUrl = await getCertsAndProxyConfig({\n        collectionUid: collection.uid,\n        collection,\n        request: refreshRequestForConfig,\n        envVars,\n        runtimeVariables,\n        processEnvVars,\n        collectionPath: collection.pathname,\n        globalEnvironmentVariables: request.globalEnvironmentVariables\n      });\n    }\n\n    let credentials, credentialsId, oauth2Url, debugInfo;\n\n    switch (grantType) {\n      case 'authorization_code':\n        interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);\n        ({\n          credentials,\n          url: oauth2Url,\n          credentialsId,\n          debugInfo\n        } = await getOAuth2TokenUsingAuthorizationCode({\n          request: requestCopy,\n          collectionUid: collection.uid,\n          certsAndProxyConfigForTokenUrl,\n          certsAndProxyConfigForRefreshUrl\n        }));\n        wsRequest.oauth2Credentials = {\n          credentials,\n          url: oauth2Url,\n          collectionUid: collection.uid,\n          credentialsId,\n          debugInfo,\n          folderUid: request.oauth2Credentials?.folderUid\n        };\n        if (tokenPlacement == 'header') {\n          wsRequest.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`;\n        } else {\n          try {\n            const url = new URL(request.url);\n            url?.searchParams?.set(tokenQueryKey, credentials?.access_token);\n            request.url = url?.toString();\n          } catch (error) {}\n        }\n        break;\n      case 'client_credentials':\n        interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);\n        ({\n          credentials,\n          url: oauth2Url,\n          credentialsId,\n          debugInfo\n        } = await getOAuth2TokenUsingClientCredentials({\n          request: requestCopy,\n          collectionUid: collection.uid,\n          certsAndProxyConfigForTokenUrl,\n          certsAndProxyConfigForRefreshUrl\n        }));\n        wsRequest.oauth2Credentials = {\n          credentials,\n          url: oauth2Url,\n          collectionUid: collection.uid,\n          credentialsId,\n          debugInfo,\n          folderUid: request.oauth2Credentials?.folderUid\n        };\n        if (tokenPlacement == 'header') {\n          wsRequest.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`;\n        } else {\n          try {\n            const url = new URL(request.url);\n            url?.searchParams?.set(tokenQueryKey, credentials?.access_token);\n            request.url = url?.toString();\n          } catch (error) {}\n        }\n        break;\n      case 'password':\n        interpolateVars(requestCopy, envVars, runtimeVariables, processEnvVars);\n        ({\n          credentials,\n          url: oauth2Url,\n          credentialsId,\n          debugInfo\n        } = await getOAuth2TokenUsingPasswordCredentials({\n          request: requestCopy,\n          collectionUid: collection.uid,\n          certsAndProxyConfigForTokenUrl,\n          certsAndProxyConfigForRefreshUrl\n        }));\n        wsRequest.oauth2Credentials = {\n          credentials,\n          url: oauth2Url,\n          collectionUid: collection.uid,\n          credentialsId,\n          debugInfo,\n          folderUid: request.oauth2Credentials?.folderUid\n        };\n        if (tokenPlacement == 'header') {\n          wsRequest.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`;\n        } else {\n          try {\n            const url = new URL(request.url);\n            url?.searchParams?.set(tokenQueryKey, credentials?.access_token);\n            request.url = url?.toString();\n          } catch (error) {}\n        }\n        break;\n    }\n  }\n\n  // Add API key to the URL if placement is queryparams\n  if (wsRequest.apiKeyAuthValueForQueryParams && wsRequest.apiKeyAuthValueForQueryParams.placement === 'queryparams') {\n    try {\n      const urlObj = new URL(wsRequest.url);\n\n      const globalEnvironmentVariables = request.globalEnvironmentVariables;\n      const promptVariables = collection?.promptVariables || {};\n\n      const interpolationOptions = {\n        globalEnvironmentVariables,\n        envVars,\n        runtimeVariables,\n        promptVariables,\n        processEnvVars\n      };\n\n      const key = interpolateString(wsRequest.apiKeyAuthValueForQueryParams.key, interpolationOptions);\n      const value = interpolateString(wsRequest.apiKeyAuthValueForQueryParams.value, interpolationOptions);\n\n      urlObj.searchParams.set(key, value);\n      wsRequest.url = urlObj.toString();\n    } catch (error) {\n      console.error('Error adding API key to WebSocket URL:', error);\n    }\n  }\n\n  delete wsRequest.apiKeyAuthValueForQueryParams;\n\n  interpolateVars(wsRequest, envVars, runtimeVariables, processEnvVars);\n\n  return wsRequest;\n};\n\n// Creating wsClient at module level so it can be accessed from window-all-closed event\nlet wsClient;\n\n/**\n * Register IPC handlers for WebSocket\n */\nconst registerWsEventHandlers = (window) => {\n  const sendEvent = (eventName, ...args) => {\n    if (window && !window.isDestroyed() && window.webContents && !window.webContents.isDestroyed()) {\n      window.webContents.send(eventName, ...args);\n    } else {\n      console.warn(`Unable to send message \"${eventName}\": Window not available`);\n    }\n  };\n\n  wsClient = new WsClient(sendEvent);\n\n  // Start a new WebSocket connection\n  ipcMain.handle(\n    'renderer:ws:start-connection',\n    async (event, { request, collection, environment, runtimeVariables, settings, options = {} }) => {\n      try {\n        const requestCopy = cloneDeep(request);\n        const preparedRequest = await prepareWsRequest(requestCopy, collection, environment, runtimeVariables, {});\n        const connectOnly = options?.connectOnly ?? false;\n        const requestSent = {\n          type: 'request',\n          url: preparedRequest.url,\n          headers: preparedRequest.headers,\n          body: preparedRequest.body,\n          timestamp: Date.now()\n        };\n\n        if (!connectOnly) {\n          const hasMessages = preparedRequest.body.ws.some((msg) => msg.content.length);\n          if (hasMessages) {\n            preparedRequest.body.ws.forEach((message) => {\n              wsClient.queueMessage(preparedRequest.uid, collection.uid, message.content);\n            });\n          }\n        }\n\n        // Get certificates and proxy configuration\n        const certsAndProxyConfig = await getCertsAndProxyConfig({\n          collectionUid: collection.uid,\n          collection,\n          request: requestCopy.request,\n          envVars: preparedRequest.envVars,\n          runtimeVariables,\n          processEnvVars: preparedRequest.processEnvVars,\n          collectionPath: collection.pathname,\n          globalEnvironmentVariables: collection.globalEnvironmentVariables\n        });\n\n        const { httpsAgentRequestFields } = certsAndProxyConfig;\n\n        const sslOptions = {\n          rejectUnauthorized: preferencesUtil.shouldVerifyTls(),\n          ca: httpsAgentRequestFields.ca,\n          cert: httpsAgentRequestFields.cert,\n          key: httpsAgentRequestFields.key,\n          pfx: httpsAgentRequestFields.pfx,\n          passphrase: httpsAgentRequestFields.passphrase\n        };\n\n        // Start WebSocket connection\n        await wsClient.startConnection({\n          request: preparedRequest,\n          collection,\n          options: {\n            timeout: settings.timeout,\n            keepAlive: settings.keepAliveInterval > 0 ? true : false,\n            keepAliveInterval: settings.keepAliveInterval,\n            sslOptions\n          }\n        });\n\n        sendEvent('main:ws:request', preparedRequest.uid, collection.uid, requestSent);\n\n        // Send OAuth credentials update if available\n        if (preparedRequest?.oauth2Credentials) {\n          window.webContents.send('main:credentials-update', {\n            credentials: preparedRequest.oauth2Credentials?.credentials,\n            url: preparedRequest.oauth2Credentials?.url,\n            collectionUid: collection.uid,\n            credentialsId: preparedRequest.oauth2Credentials?.credentialsId,\n            ...(preparedRequest.oauth2Credentials?.folderUid\n              ? { folderUid: preparedRequest.oauth2Credentials.folderUid }\n              : { itemUid: preparedRequest.uid }),\n            debugInfo: preparedRequest.oauth2Credentials.debugInfo\n          });\n        }\n\n        return { success: true };\n      } catch (error) {\n        console.error('Error starting WebSocket connection:', error);\n        if (error instanceof Error) {\n          throw error;\n        }\n        sendEvent('main:ws:error', request.uid, collection.uid, { error: error.message });\n        return { success: false, error: error.message };\n      }\n    }\n  );\n\n  // Get all active connection IDs\n  ipcMain.handle('renderer:ws:get-active-connections', (event) => {\n    try {\n      const activeConnectionIds = wsClient.getActiveConnectionIds();\n      return { success: true, activeConnectionIds };\n    } catch (error) {\n      console.error('Error getting active connections:', error);\n      return { success: false, error: error.message, activeConnectionIds: [] };\n    }\n  });\n\n  ipcMain.handle(\n    'renderer:ws:queue-message',\n    async (event, { item, collection, environment, runtimeVariables, messageContent }) => {\n      try {\n        const itemCopy = cloneDeep(item);\n        const preparedRequest = await prepareWsRequest(itemCopy, collection, environment, runtimeVariables, {});\n\n        // If messageContent is provided, find and queue that specific message (interpolated)\n        // Otherwise, queue all messages\n        if (messageContent !== undefined && messageContent !== null) {\n          // Find the message index in the original request\n          const originalMessages = itemCopy.draft?.request?.body?.ws || itemCopy.request?.body?.ws || [];\n          const messageIndex = originalMessages.findIndex((msg) => msg.content === messageContent);\n\n          if (messageIndex >= 0 && preparedRequest.body?.ws?.[messageIndex]) {\n            // Queue the interpolated version of the specific message\n            const message = preparedRequest.body.ws[messageIndex];\n            wsClient.queueMessage(preparedRequest.uid, collection.uid, message.content, message.type);\n          } else {\n            // Message not found in request body, queue as-is (shouldn't happen in normal flow)\n            wsClient.queueMessage(preparedRequest.uid, collection.uid, messageContent);\n          }\n        } else {\n          // Queue all messages (they are already interpolated by prepareWsRequest -> interpolateVars)\n          if (preparedRequest.body && preparedRequest.body.ws && Array.isArray(preparedRequest.body.ws)) {\n            preparedRequest.body.ws\n              .filter((message) => message && message.content)\n              .forEach((message) => {\n                wsClient.queueMessage(preparedRequest.uid, collection.uid, message.content, message.type);\n              });\n          }\n        }\n\n        return { success: true };\n      } catch (error) {\n        console.error('Error queuing WebSocket message:', error);\n        return { success: false, error: error.message };\n      }\n    }\n  );\n\n  // Send a message to an existing WebSocket connection\n  ipcMain.handle('renderer:ws:send-message', (event, requestId, collectionUid, message) => {\n    try {\n      wsClient.sendMessage(requestId, collectionUid, message);\n      return { success: true };\n    } catch (error) {\n      console.error('Error sending WebSocket message:', error);\n      return { success: false, error: error.message };\n    }\n  });\n\n  // Close a WebSocket connection\n  ipcMain.handle('renderer:ws:close-connection', (event, requestId, code, reason) => {\n    try {\n      wsClient.close(requestId, code, reason);\n      return { success: true };\n    } catch (error) {\n      console.error('Error closing WebSocket connection:', error);\n      return { success: false, error: error.message };\n    }\n  });\n\n  // Check if a WebSocket connection is active\n  ipcMain.handle('renderer:ws:is-connection-active', (event, requestId) => {\n    try {\n      const isActive = wsClient.isConnectionActive(requestId);\n      return { success: true, isActive };\n    } catch (error) {\n      console.error('Error checking WebSocket connection status:', error);\n      return { success: false, error: error.message, isActive: false };\n    }\n  });\n\n  /**\n   * Get the connection status of a connection\n   * @param {string} requestId - The request ID to get the connection status of\n   * @returns {string} - The connection status\n   */\n  ipcMain.handle('renderer:ws:connection-status', (event, requestId) => {\n    try {\n      const status = wsClient.connectionStatus(requestId);\n      return { success: true, status };\n    } catch (error) {\n      console.error('Error getting WebSocket connection status:', error);\n      return { success: false, error: error.message, status: 'disconnected' };\n    }\n  });\n};\n\nmodule.exports = {\n  registerWsEventHandlers,\n  wsClient,\n  prepareWsRequest\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/notifications.js",
    "content": "require('dotenv').config();\nconst { ipcMain } = require('electron');\nconst fetch = require('node-fetch');\n\nconst registerNotificationsIpc = (mainWindow, watcher) => {\n  ipcMain.handle('renderer:fetch-notifications', async () => {\n    try {\n      const notifications = await fetchNotifications();\n      return Promise.resolve(notifications);\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n};\n\nmodule.exports = registerNotificationsIpc;\n\nconst fetchNotifications = async () => {\n  try {\n    let url = process.env.BRUNO_INFO_ENDPOINT || 'https://appinfo.usebruno.com';\n    const data = await fetch(url).then((res) => res.json());\n\n    return data?.notifications || [];\n  } catch (error) {\n    return Promise.reject('Error while fetching notifications!', error);\n  }\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/openapi-sync.js",
    "content": "const _ = require('lodash');\nconst fs = require('fs');\nconst fsExtra = require('fs-extra');\nconst path = require('path');\nconst crypto = require('crypto');\nconst { ipcMain, app } = require('electron');\nconst {\n  parseRequest,\n  stringifyRequestViaWorker,\n  parseCollection,\n  stringifyCollection,\n  stringifyFolder\n} = require('@usebruno/filestore');\nconst { openApiToBruno } = require('@usebruno/converters');\nconst { writeFile, sanitizeName, getCollectionFormat, posixifyPath } = require('../utils/filesystem');\nconst { getEnvVars } = require('../utils/collection');\nconst { getProcessEnvVars } = require('../store/process-env');\nconst { getCertsAndProxyConfig } = require('./network/cert-utils');\nconst { makeAxiosInstance } = require('./network/axios-instance');\nconst jsyaml = require('js-yaml');\n\n/**\n * Detect if a string content is YAML (not JSON).\n * Attempts JSON.parse first for a definitive check rather than relying on heuristics.\n */\nconst isYamlContent = (content) => {\n  if (!content || typeof content !== 'string') return false;\n  try {\n    JSON.parse(content);\n    return false; // Valid JSON — not YAML\n  } catch {\n    // Not JSON — verify it's actually parseable as YAML and produces an object\n    try {\n      const result = jsyaml.load(content);\n      return result && typeof result === 'object';\n    } catch {\n      return false;\n    }\n  }\n};\n\n/**\n * Pretty-print JSON content for readable diffs. YAML content is returned as-is.\n */\nconst prettyPrintSpec = (content) => {\n  if (!content) return '';\n  try {\n    const parsed = JSON.parse(content);\n    return JSON.stringify(parsed, null, 2);\n  } catch {\n    return content;\n  }\n};\n\n/**\n * Generate an MD5 hash of a parsed OpenAPI spec for quick change detection.\n */\nconst generateSpecHash = (spec) => {\n  if (!spec) return null;\n  return crypto.createHash('md5').update(JSON.stringify(spec)).digest('hex');\n};\n\n/**\n * Validate that a target path is inside the collection directory.\n * Prevents path traversal attacks via ../../ in user-supplied paths.\n */\nconst isPathInsideCollection = (targetPath, collectionPath) => {\n  const resolvedTarget = path.resolve(targetPath);\n  const resolvedCollection = path.resolve(collectionPath);\n  return resolvedTarget.startsWith(resolvedCollection + path.sep) || resolvedTarget === resolvedCollection;\n};\n\n/**\n * Validate that a URL uses http or https scheme only.\n */\nconst isValidHttpUrl = (urlString) => {\n  try {\n    const url = new URL(urlString);\n    return url.protocol === 'http:' || url.protocol === 'https:';\n  } catch {\n    return false;\n  }\n};\n\nconst isLocalFilePath = (str) => !isValidHttpUrl(str) && typeof str === 'string' && str.length > 0;\n\nconst resolveSourceUrl = (collectionPath, sourceUrl) => {\n  if (!sourceUrl || isValidHttpUrl(sourceUrl)) return sourceUrl;\n  return path.resolve(collectionPath, sourceUrl);\n};\n\n/**\n * Get the directory where OpenAPI spec files are stored in AppData.\n */\nconst getSpecsDir = () => path.join(app.getPath('userData'), 'specs');\n\n/**\n * Load the spec metadata file from AppData.\n * Returns an object mapping collectionPath → array of { filename, sourceUrl } entries.\n */\nconst loadSpecMetadata = () => {\n  const metadataPath = path.join(getSpecsDir(), 'metadata.json');\n  try {\n    if (fs.existsSync(metadataPath)) {\n      return JSON.parse(fs.readFileSync(metadataPath, 'utf8'));\n    }\n  } catch {\n    // ignore parse errors, return empty\n  }\n  return {};\n};\n\n/**\n * Save the spec metadata file to AppData.\n */\nconst saveSpecMetadata = (metadata) => {\n  const specsDir = getSpecsDir();\n  fsExtra.ensureDirSync(specsDir);\n  const metadataPath = path.join(specsDir, 'metadata.json');\n  const tmpPath = metadataPath + '.tmp';\n  fs.writeFileSync(tmpPath, JSON.stringify(metadata, null, 2), 'utf8');\n  fs.renameSync(tmpPath, metadataPath);\n};\n\n/**\n * Get all spec entries for a collection.\n */\nconst getSpecEntriesForCollection = (collectionPath) => {\n  return loadSpecMetadata()[collectionPath] || [];\n};\n\n/**\n * Get the spec entry for a specific sourceUrl within a collection.\n */\nconst getSpecEntryForUrl = (collectionPath) => {\n  return getSpecEntriesForCollection(collectionPath)[0] || null;\n};\n\n/**\n * Parse a spec string (JSON or YAML) into an object.\n */\nconst parseSpec = (content) => {\n  try {\n    return JSON.parse(content);\n  } catch {\n    return jsyaml.load(content);\n  }\n};\n\n/**\n * Validate that a parsed spec object is a valid OpenAPI 3.x document.\n * Swagger 2.0 is not supported — the converter only handles OpenAPI 3.x.\n */\nconst isValidOpenApiSpec = (spec) => {\n  if (!spec || typeof spec !== 'object') return false;\n  if (spec.swagger) return false;\n  if (spec.openapi && typeof spec.openapi === 'string' && spec.openapi.startsWith('3.')) {\n    return spec.paths && typeof spec.paths === 'object';\n  }\n  return false;\n};\n\n/**\n * Fetch OpenAPI spec content from a remote URL or local file path.\n * Handles proxy/cert resolution for remote URLs.\n * Returns { content, spec } on success, or { error, errorCode? } on failure.\n */\nconst fetchSpecFromSource = async ({ collectionUid, collectionPath, sourceUrl, environmentContext = {} }) => {\n  const { activeEnvironmentUid, environments = [], runtimeVariables = {}, globalEnvironmentVariables = {} } = environmentContext;\n\n  if (!isValidHttpUrl(sourceUrl) && !isLocalFilePath(sourceUrl)) {\n    return { error: 'Invalid source: only http/https URLs and local file paths are allowed' };\n  }\n\n  let content;\n\n  if (isLocalFilePath(sourceUrl)) {\n    const resolvedPath = collectionPath ? path.resolve(collectionPath, sourceUrl) : sourceUrl;\n    if (!fs.existsSync(resolvedPath)) {\n      return { error: `Spec file not found at: ${sourceUrl}`, errorCode: 'SOURCE_FILE_NOT_FOUND' };\n    }\n    content = fs.readFileSync(resolvedPath, 'utf8');\n  } else {\n    const cacheBustUrl = sourceUrl.includes('?')\n      ? `${sourceUrl}&_=${Date.now()}`\n      : `${sourceUrl}?_=${Date.now()}`;\n\n    const environment = _.find(environments, (e) => e.uid === activeEnvironmentUid);\n    const envVars = getEnvVars(environment);\n    const processEnvVars = getProcessEnvVars(collectionUid);\n    const { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions } = await getCertsAndProxyConfig({\n      collectionUid,\n      collection: { promptVariables: {} },\n      request: {},\n      envVars,\n      runtimeVariables,\n      processEnvVars,\n      collectionPath,\n      globalEnvironmentVariables\n    });\n    const axiosInstance = makeAxiosInstance({ proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions });\n\n    try {\n      const response = await axiosInstance.get(cacheBustUrl, {\n        headers: {\n          'Cache-Control': 'no-cache, no-store, must-revalidate',\n          'Pragma': 'no-cache'\n        },\n        timeout: 30000,\n        transformResponse: [(data) => data]\n      });\n      content = response.data;\n    } catch (fetchErr) {\n      if (fetchErr.response) {\n        return { error: `Failed to fetch spec: ${fetchErr.response.status} ${fetchErr.response.statusText}` };\n      }\n      const reason = fetchErr.code || fetchErr.cause?.code || fetchErr.name || 'unknown';\n      return { error: `Could not reach ${sourceUrl} (${reason})` };\n    }\n  }\n\n  const spec = parseSpec(content);\n  return { content, spec };\n};\n\n/**\n * Normalize a Bruno request URL down to a comparable path.\n * Strips template variables ({{baseUrl}}), protocol/host, query params,\n * converts {param} to :param, collapses slashes, removes trailing slash.\n */\nconst normalizeUrlPath = (urlStr) => {\n  if (!urlStr) return '';\n  return urlStr\n    .replace(/\\{\\{[^}]+\\}\\}/g, '')\n    .replace(/^https?:\\/\\/[^/]+/, '')\n    .replace(/\\?.*$/, '')\n    .replace(/{([^}]+)}/g, ':$1')\n    .replace(/\\/+/g, '/')\n    .replace(/\\/$/, '');\n};\n\n/**\n * Load bruno config from disk. Returns { format, brunoConfig, collectionRoot }.\n * collectionRoot is only set for yml format collections.\n */\nconst loadBrunoConfig = (collectionPath) => {\n  const format = getCollectionFormat(collectionPath);\n  let brunoConfig;\n  let collectionRoot;\n\n  if (format === 'yml') {\n    const configFilePath = path.join(collectionPath, 'opencollection.yml');\n    if (!fs.existsSync(configFilePath)) {\n      throw new Error('opencollection.yml not found');\n    }\n    const content = fs.readFileSync(configFilePath, 'utf8');\n    const parsed = parseCollection(content, { format });\n    brunoConfig = parsed.brunoConfig;\n    collectionRoot = parsed.collectionRoot;\n  } else {\n    const brunoJsonPath = path.join(collectionPath, 'bruno.json');\n    if (!fs.existsSync(brunoJsonPath)) {\n      throw new Error('bruno.json not found');\n    }\n    brunoConfig = JSON.parse(fs.readFileSync(brunoJsonPath, 'utf8'));\n  }\n\n  // Resolve relative openapi sourceUrls to absolute so all callers get consistent paths\n  if (Array.isArray(brunoConfig?.openapi)) {\n    brunoConfig.openapi = brunoConfig.openapi.map((entry) => ({\n      ...entry,\n      sourceUrl: resolveSourceUrl(collectionPath, entry.sourceUrl)\n    }));\n  }\n\n  return { format, brunoConfig, collectionRoot };\n};\n\n/**\n * Save bruno config to disk (bruno.json or opencollection.yml).\n */\nconst saveBrunoConfig = async (collectionPath, format, brunoConfig, collectionRoot) => {\n  // Convert absolute openapi sourceUrls back to collection-relative for git-shareability\n  const configToSave = { ...brunoConfig };\n  if (Array.isArray(configToSave?.openapi)) {\n    configToSave.openapi = configToSave.openapi.map((entry) => ({\n      ...entry,\n      sourceUrl: (entry.sourceUrl && !isValidHttpUrl(entry.sourceUrl))\n        ? posixifyPath(path.relative(collectionPath, entry.sourceUrl))\n        : entry.sourceUrl\n    }));\n  }\n\n  if (format === 'yml') {\n    const content = await stringifyCollection(collectionRoot, configToSave, { format });\n    await writeFile(path.join(collectionPath, 'opencollection.yml'), content);\n  } else {\n    const brunoJsonPath = path.join(collectionPath, 'bruno.json');\n    await writeFile(brunoJsonPath, JSON.stringify(configToSave, null, 2));\n  }\n};\n\n/**\n * Find a spec item in a Bruno collection tree by HTTP method and path.\n * Returns { item, folderName } or null.\n */\nconst findItemInCollection = (items, method, targetPath, currentFolderName = null) => {\n  const normalizedTarget = normalizeUrlPath(targetPath);\n  for (const item of items) {\n    if (item.type === 'folder' && item.items) {\n      const found = findItemInCollection(item.items, method, targetPath, item.name);\n      if (found) return found;\n    }\n    if (item.request?.method?.toLowerCase() === method.toLowerCase()) {\n      if (normalizeUrlPath(item.request.url) === normalizedTarget) {\n        return { item, folderName: currentFolderName };\n      }\n    }\n  }\n  return null;\n};\n\n/**\n * Find an existing request file on disk by HTTP method and normalized path.\n * Scans .bru/.yml files in the collection directory recursively.\n * Returns { filePath, request, content, fileFormat } or null.\n */\nconst findRequestFileOnDisk = (dirPath, method, urlPath) => {\n  if (!fs.existsSync(dirPath)) return null;\n  const files = fs.readdirSync(dirPath);\n  for (const file of files) {\n    const filePath = path.join(dirPath, file);\n    const stats = fs.statSync(filePath);\n    if (stats.isDirectory() && !['node_modules', '.git', 'environments'].includes(file)) {\n      const found = findRequestFileOnDisk(filePath, method, urlPath);\n      if (found) return found;\n    } else if (file.endsWith('.bru') || file.endsWith('.yml') || file.endsWith('.yaml')) {\n      if (file.startsWith('folder.') || file.startsWith('collection.')) continue;\n      try {\n        const content = fs.readFileSync(filePath, 'utf8');\n        const fileFormat = file.endsWith('.yml') || file.endsWith('.yaml') ? 'yml' : 'bru';\n        const request = parseRequest(content, { format: fileFormat });\n        if (request?.request) {\n          const reqMethod = request.request.method?.toUpperCase();\n          const reqPath = normalizeUrlPath(request.request.url);\n          if (reqMethod === method && reqPath === urlPath) {\n            return { filePath, request, content, fileFormat };\n          }\n        }\n      } catch (err) {\n        // Skip files that can't be parsed\n      }\n    }\n  }\n  return null;\n};\n\n/**\n * Save an OpenAPI spec file to AppData specs directory.\n * - Detects format (JSON/YAML) from the content and uses the correct file extension.\n * - Reuses an existing UUID filename if one exists for this sourceUrl, otherwise creates a new one.\n * - Updates metadata.json with the filename → sourceUrl mapping.\n *\n * @param {Object} params\n * @param {string} params.collectionPath - Path to the collection directory.\n * @param {string} params.content - The spec content string to save (JSON or YAML).\n * @param {string} params.sourceUrl - The source URL identifying which spec entry to update.\n */\nconst saveOpenApiSpecFile = async ({ collectionPath, content, sourceUrl }) => {\n  const specsDir = getSpecsDir();\n  await fsExtra.ensureDir(specsDir);\n\n  const resolvedUrl = resolveSourceUrl(collectionPath, sourceUrl);\n  const meta = loadSpecMetadata();\n  const existingEntry = (meta[collectionPath] || [])[0];\n\n  let filename;\n  if (existingEntry) {\n    // Reuse existing UUID filename\n    filename = existingEntry.filename;\n  } else {\n    // Generate a new UUID filename based on content type\n    const ext = isYamlContent(content) ? 'yaml' : 'json';\n    filename = `${crypto.randomUUID()}.${ext}`;\n  }\n\n  // Always replace with a single entry (one spec per collection for now)\n  meta[collectionPath] = [{ filename, sourceUrl: resolvedUrl }];\n  saveSpecMetadata(meta);\n\n  await writeFile(path.join(specsDir, filename), content);\n};\n\n/**\n * Save an OpenAPI spec file and update sync metadata (lastSyncDate, specHash) in brunoConfig.\n * Shared by both the IPC handler (connect flow) and the import flow.\n */\nconst saveSpecAndUpdateMetadata = async ({ collectionPath, specContent }) => {\n  const { format, brunoConfig, collectionRoot } = loadBrunoConfig(collectionPath);\n  const sourceUrl = brunoConfig?.openapi?.[0]?.sourceUrl;\n\n  await saveOpenApiSpecFile({ collectionPath, content: specContent, sourceUrl });\n\n  let parsedSpec;\n  try {\n    parsedSpec = JSON.parse(specContent);\n  } catch {\n    parsedSpec = jsyaml.load(specContent);\n  }\n\n  const specHash = generateSpecHash(parsedSpec);\n  const lastSyncDate = new Date().toISOString();\n  if (brunoConfig.openapi?.[0]) {\n    brunoConfig.openapi[0] = { ...brunoConfig.openapi[0], lastSyncDate, specHash };\n  };\n\n  await saveBrunoConfig(collectionPath, format, brunoConfig, collectionRoot);\n};\n\n/**\n * Clean up stored spec files and metadata for a collection (called when a collection is removed).\n */\nconst cleanupSpecFilesForCollection = (collectionPath) => {\n  const meta = loadSpecMetadata();\n  const entries = meta[collectionPath] || [];\n  for (const entry of entries) {\n    const specPath = path.join(getSpecsDir(), entry.filename);\n    if (fs.existsSync(specPath)) fs.unlinkSync(specPath);\n  }\n  if (entries.length > 0) {\n    delete meta[collectionPath];\n    saveSpecMetadata(meta);\n  }\n};\n\n/**\n * Merge spec params/headers with existing user values.\n * Matches by name + value to correctly handle enum-expanded params (multiple entries with same name).\n * Only preserves the user's enabled state; values come from the spec.\n */\nconst mergeWithUserValues = (specItems, existingItems) => {\n  return (specItems || []).map((specItem) => {\n    const existing = (existingItems || []).find(\n      (e) => e.name === specItem.name && e.value === specItem.value\n    );\n    return existing ? { ...specItem, enabled: existing.enabled } : specItem;\n  });\n};\n\n/**\n * Merge a spec item into an existing request, preserving collection-specific data\n * (tests, scripts, assertions) and user values for matching params/headers.\n *\n * fullReset: true = spec replaces entire request section (reset mode)\n *            false = only override url/body/auth from spec (sync mode)\n */\nconst mergeSpecIntoRequest = (existingRequest, specItem, { fullReset = false } = {}) => {\n  const mergedParams = mergeWithUserValues(specItem.request.params, existingRequest.request?.params);\n  const mergedHeaders = mergeWithUserValues(specItem.request.headers, existingRequest.request?.headers);\n\n  if (fullReset) {\n    return {\n      ...existingRequest,\n      request: {\n        ...existingRequest.request,\n        url: specItem.request.url,\n        method: specItem.request.method,\n        body: specItem.request.body,\n        auth: specItem.request.auth,\n        docs: specItem.request.docs,\n        params: mergedParams || [],\n        headers: mergedHeaders || []\n      }\n    };\n  }\n\n  return {\n    ...existingRequest,\n    request: {\n      ...existingRequest.request,\n      url: specItem.request.url,\n      body: specItem.request.body,\n      auth: specItem.request.auth,\n      params: mergedParams || existingRequest.request?.params || [],\n      headers: mergedHeaders || existingRequest.request?.headers || []\n    }\n  };\n};\n\n/**\n * Ensure a tag-based folder exists in the collection directory.\n * Creates the folder and its folder.bru/folder.yml file if missing.\n * Returns the resolved target folder path (falls back to collectionPath on reserved/traversal names).\n */\nconst RESERVED_FOLDER_NAMES = ['node_modules', '.git', 'environments'];\n\nconst ensureTagFolder = async (collectionPath, folderName, format) => {\n  const safeFolderName = sanitizeName(folderName);\n  if (RESERVED_FOLDER_NAMES.some((r) => r.toLowerCase() === safeFolderName.toLowerCase())) {\n    console.warn(`[OpenAPI Sync] Tag \"${folderName}\" sanitizes to reserved folder name \"${safeFolderName}\", placing requests in collection root`);\n    return collectionPath;\n  }\n  const targetFolder = path.join(collectionPath, safeFolderName);\n  if (!isPathInsideCollection(targetFolder, collectionPath)) {\n    console.error(`[OpenAPI Sync] Path traversal blocked in folder name: ${folderName}`);\n    return collectionPath;\n  }\n  if (!fs.existsSync(targetFolder)) {\n    fs.mkdirSync(targetFolder, { recursive: true });\n    const folderBruPath = path.join(targetFolder, `folder.${format}`);\n    const folderContent = await stringifyFolder({ meta: { name: safeFolderName } }, { format });\n    await writeFile(folderBruPath, folderContent);\n  }\n  return targetFolder;\n};\n\n/**\n * Flatten a Bruno collection's items into a Map keyed by endpoint ID (METHOD:normalizedPath).\n * Each value includes the original item plus the parent folderName.\n */\nconst buildSpecItemsMap = (collectionItems) => {\n  const map = new Map();\n  const flatten = (items, parentFolder = null) => {\n    for (const item of items) {\n      if (item.type === 'folder' && item.items) {\n        flatten(item.items, item.name);\n      } else if (item.request) {\n        const method = item.request.method?.toUpperCase() || 'GET';\n        const urlPath = normalizeUrlPath(item.request.url);\n        const id = `${method}:${urlPath}`;\n        map.set(id, { ...item, folderName: parentFolder });\n      }\n    }\n  };\n  flatten(collectionItems);\n  return map;\n};\n\n/**\n * Recursively extracts all key paths from a parsed JSON value (dot-notation).\n * Used to compare JSON body structure/schema without comparing values.\n */\nconst extractJsonKeys = (obj, prefix = '') => {\n  const keys = [];\n  if (obj && typeof obj === 'object' && !Array.isArray(obj)) {\n    for (const key of Object.keys(obj)) {\n      const fullKey = prefix ? `${prefix}.${key}` : key;\n      keys.push(fullKey);\n      keys.push(...extractJsonKeys(obj[key], fullKey));\n    }\n  } else if (Array.isArray(obj) && obj.length > 0) {\n    // Only inspect first element (spec arrays always have one template item)\n    keys.push(...extractJsonKeys(obj[0], `${prefix}[]`));\n  }\n  return keys;\n};\n\n/**\n * Compare two Bruno-format requests field-by-field.\n * Returns { hasDiff, changes } where changes is an array of human-readable strings.\n */\nconst compareRequestFields = (specRequest, actualRequest) => {\n  // Compare parameters by name:type pairs (catches query<->path type changes)\n  const specParamKeys = (specRequest.params || []).map((p) => `${p.name}:${p.type || 'query'}`).sort();\n  const actualParamKeys = (actualRequest.params || []).map((p) => `${p.name}:${p.type || 'query'}`).sort();\n\n  // Compare headers (by name)\n  const specHeaderNames = (specRequest.headers || []).map((h) => h.name).sort();\n  const actualHeaderNames = (actualRequest.headers || []).map((h) => h.name).sort();\n\n  // Check for differences\n  const paramsDiff = JSON.stringify(specParamKeys) !== JSON.stringify(actualParamKeys);\n  const headersDiff = JSON.stringify(specHeaderNames) !== JSON.stringify(actualHeaderNames);\n\n  // Check body mode difference\n  const specBodyMode = specRequest.body?.mode || 'none';\n  const actualBodyMode = actualRequest.body?.mode || 'none';\n  const bodyDiff = specBodyMode !== actualBodyMode;\n\n  // Check auth mode difference\n  const specAuthMode = specRequest.auth?.mode || 'none';\n  const actualAuthMode = actualRequest.auth?.mode || 'none';\n  const authDiff = specAuthMode !== actualAuthMode;\n\n  // Check auth config differences when auth modes match\n  let authConfigDiff = false;\n  if (!authDiff && specAuthMode !== 'none' && specAuthMode !== 'inherit') {\n    if (specAuthMode === 'apikey') {\n      const specApikey = specRequest.auth?.apikey || {};\n      const actualApikey = actualRequest.auth?.apikey || {};\n      authConfigDiff = specApikey.key !== actualApikey.key || specApikey.placement !== actualApikey.placement;\n    } else if (specAuthMode === 'oauth2') {\n      const specOauth2 = specRequest.auth?.oauth2 || {};\n      const actualOauth2 = actualRequest.auth?.oauth2 || {};\n      const grantType = specOauth2.grantType || actualOauth2.grantType;\n      const commonFields = ['grantType', 'scope'];\n      const grantTypeFields = {\n        authorization_code: [...commonFields, 'authorizationUrl', 'accessTokenUrl'],\n        implicit: [...commonFields, 'authorizationUrl'],\n        password: [...commonFields, 'accessTokenUrl'],\n        client_credentials: [...commonFields, 'accessTokenUrl']\n      };\n      const fields = grantTypeFields[grantType] || commonFields;\n      authConfigDiff = fields.some((field) => specOauth2[field] !== actualOauth2[field]);\n    }\n  }\n\n  // Check form field names when body modes match and mode is form-based\n  let formFieldsDiff = false;\n  let specFormFieldNames = [];\n  let actualFormFieldNames = [];\n  if (!bodyDiff && (specBodyMode === 'formUrlEncoded' || specBodyMode === 'multipartForm')) {\n    if (specBodyMode === 'multipartForm') {\n      specFormFieldNames = (specRequest.body?.multipartForm || []).map((f) => `${f.name}:${f.type || 'text'}`).sort();\n      actualFormFieldNames = (actualRequest.body?.multipartForm || []).map((f) => `${f.name}:${f.type || 'text'}`).sort();\n    } else {\n      specFormFieldNames = (specRequest.body?.formUrlEncoded || []).map((f) => f.name).sort();\n      actualFormFieldNames = (actualRequest.body?.formUrlEncoded || []).map((f) => f.name).sort();\n    }\n    formFieldsDiff = JSON.stringify(specFormFieldNames) !== JSON.stringify(actualFormFieldNames);\n  }\n\n  // Check JSON body structure when both sides use json mode\n  let jsonBodyDiff = false;\n  if (!bodyDiff && specBodyMode === 'json') {\n    try {\n      const specJson = specRequest.body?.json ? JSON.parse(specRequest.body.json) : null;\n      const actualJson = actualRequest.body?.json ? JSON.parse(actualRequest.body.json) : null;\n      if (specJson !== null && actualJson !== null) {\n        const specKeys = extractJsonKeys(specJson).sort();\n        const actualKeys = extractJsonKeys(actualJson).sort();\n        jsonBodyDiff = JSON.stringify(specKeys) !== JSON.stringify(actualKeys);\n      } else if ((specJson === null) !== (actualJson === null)) {\n        jsonBodyDiff = true;\n      }\n    } catch (e) {\n      // Malformed JSON — skip structural comparison\n    }\n  }\n\n  const hasDiff = paramsDiff || headersDiff || bodyDiff || authDiff || authConfigDiff || formFieldsDiff || jsonBodyDiff;\n\n  const changes = [];\n  if (hasDiff) {\n    if (paramsDiff) {\n      const addedParams = actualParamKeys.filter((p) => !specParamKeys.includes(p));\n      const removedParams = specParamKeys.filter((p) => !actualParamKeys.includes(p));\n      if (addedParams.length) changes.push(`+${addedParams.length} params`);\n      if (removedParams.length) changes.push(`-${removedParams.length} params`);\n    }\n    if (headersDiff) {\n      const addedHeaders = actualHeaderNames.filter((h) => !specHeaderNames.includes(h));\n      const removedHeaders = specHeaderNames.filter((h) => !actualHeaderNames.includes(h));\n      if (addedHeaders.length) changes.push(`+${addedHeaders.length} headers`);\n      if (removedHeaders.length) changes.push(`-${removedHeaders.length} headers`);\n    }\n    if (bodyDiff) changes.push(`body: ${actualBodyMode}`);\n    if (authDiff) changes.push(`auth: ${actualAuthMode}`);\n    if (authConfigDiff) changes.push('auth config');\n    if (formFieldsDiff) {\n      const addedFields = actualFormFieldNames.filter((f) => !specFormFieldNames.includes(f));\n      const removedFields = specFormFieldNames.filter((f) => !actualFormFieldNames.includes(f));\n      if (addedFields.length) changes.push(`+${addedFields.length} form fields`);\n      if (removedFields.length) changes.push(`-${removedFields.length} form fields`);\n    }\n    if (jsonBodyDiff) changes.push('body schema');\n  }\n\n  return { hasDiff, changes };\n};\n\n/**\n * Load the stored spec for a collection and convert it to Bruno collection format.\n * Throws if no stored spec file exists.\n */\nconst loadStoredSpecCollection = (collectionPath, brunoConfig) => {\n  const sourceUrl = brunoConfig?.openapi?.[0]?.sourceUrl;\n  const specEntry = sourceUrl ? getSpecEntryForUrl(collectionPath) : null;\n  const specPath = specEntry ? path.join(getSpecsDir(), specEntry.filename) : null;\n\n  if (!specPath || !fs.existsSync(specPath)) {\n    throw new Error('No stored spec file found. Please sync with remote spec first.');\n  }\n\n  const specRaw = fs.readFileSync(specPath, 'utf8');\n  const storedSpec = parseSpec(specRaw);\n  const groupBy = brunoConfig?.openapi?.[0]?.groupBy || 'tags';\n  return openApiToBruno(storedSpec, { groupBy });\n};\n\nconst registerOpenAPISyncIpc = (mainWindow) => {\n  ipcMain.handle('renderer:check-openapi-updates', async (event, {\n    collectionUid, collectionPath, sourceUrl, storedSpecHash, environmentContext\n  }) => {\n    try {\n      const result = await fetchSpecFromSource({ collectionUid, collectionPath, sourceUrl, environmentContext });\n      if (result.error) {\n        return { hasUpdates: false, error: result.error, errorCode: result.errorCode };\n      }\n      const remoteSpecHash = generateSpecHash(result.spec);\n      return { hasUpdates: storedSpecHash !== remoteSpecHash, remoteSpecHash };\n    } catch (error) {\n      console.error('[OpenAPI Sync] Lightweight check error:', error.message);\n      return { hasUpdates: false, error: error.message };\n    }\n  });\n\n  ipcMain.handle('renderer:compare-openapi-specs', async (event, {\n    collectionUid, collectionPath, sourceUrl, environmentContext\n  }) => {\n    try {\n      // Compare two OpenAPI specs by converting both to Bruno format and using field-level comparison.\n      // This ensures specDrift uses the same comparison sensitivity as collectionDrift/remoteDrift.\n      const compareSpecs = (oldSpec, newSpec, groupBy) => {\n        // Convert both specs to Bruno collection format\n        const oldBruno = oldSpec ? openApiToBruno(oldSpec, { groupBy }) : { items: [] };\n        const newBruno = newSpec ? openApiToBruno(newSpec, { groupBy }) : { items: [] };\n\n        // Build endpoint maps keyed by METHOD:normalizedPath\n        const oldItems = buildSpecItemsMap(oldBruno.items || []);\n        const newItems = buildSpecItemsMap(newBruno.items || []);\n\n        const added = [];\n        const removed = [];\n        const modified = [];\n        const unchanged = [];\n\n        for (const [id, newItem] of newItems) {\n          const colonIndex = id.indexOf(':');\n          const method = id.substring(0, colonIndex);\n          const urlPath = id.substring(colonIndex + 1);\n\n          if (!oldItems.has(id)) {\n            added.push({ id, method, path: urlPath, name: newItem.name });\n          } else {\n            const oldItem = oldItems.get(id);\n            const { hasDiff, changes } = compareRequestFields(oldItem.request, newItem.request);\n            if (hasDiff) {\n              modified.push({ id, method, path: urlPath, name: newItem.name, changes: changes.join(', ') });\n            } else {\n              unchanged.push({ id, method, path: urlPath, name: newItem.name });\n            }\n          }\n        }\n\n        for (const [id] of oldItems) {\n          if (!newItems.has(id)) {\n            const colonIndex = id.indexOf(':');\n            const method = id.substring(0, colonIndex);\n            const urlPath = id.substring(colonIndex + 1);\n            const oldItem = oldItems.get(id);\n            removed.push({ id, method, path: urlPath, name: oldItem.name });\n          }\n        }\n\n        // Compare metadata (title, version, description)\n        const oldTitle = oldSpec?.info?.title || null;\n        const newTitle = newSpec?.info?.title || null;\n        const titleChanged = oldTitle !== newTitle;\n\n        const oldVersion = oldSpec?.info?.version || null;\n        const newVersion = newSpec?.info?.version || null;\n        const versionChanged = oldVersion !== newVersion;\n\n        const oldDescription = oldSpec?.info?.description || null;\n        const newDescription = newSpec?.info?.description || null;\n        const descriptionChanged = oldDescription !== newDescription;\n\n        const metadataChanged = titleChanged || versionChanged || descriptionChanged;\n\n        return {\n          added,\n          removed,\n          modified,\n          unchanged,\n          // Metadata changes\n          titleChanged,\n          storedTitle: oldTitle,\n          newTitle,\n          versionChanged,\n          storedVersion: oldVersion,\n          newVersion,\n          descriptionChanged,\n          storedDescription: oldDescription,\n          newDescription,\n          metadataChanged,\n          hasChanges: added.length > 0 || removed.length > 0 || modified.length > 0 || metadataChanged\n        };\n      };\n\n      const specEntry = getSpecEntryForUrl(collectionPath);\n      const storedSpecPath = specEntry ? path.join(getSpecsDir(), specEntry.filename) : null;\n\n      let storedSpec = null;\n      let storedContent = '';\n      const storedSpecMissing = !storedSpecPath || !fs.existsSync(storedSpecPath);\n      if (!storedSpecMissing) {\n        storedContent = fs.readFileSync(storedSpecPath, 'utf8');\n        storedSpec = parseSpec(storedContent);\n      }\n\n      const fetchResult = await fetchSpecFromSource({ collectionUid, collectionPath, sourceUrl, environmentContext });\n      if (fetchResult.error) {\n        return {\n          isValid: false,\n          error: fetchResult.error,\n          errorCode: fetchResult.errorCode,\n          storedSpec,\n          storedSpecMissing\n        };\n      }\n\n      const newSpecContent = fetchResult.content;\n      const newSpec = fetchResult.spec;\n\n      if (!isValidOpenApiSpec(newSpec)) {\n        const error = newSpec?.swagger\n          ? 'Swagger 2.0 is not supported. Please convert your spec to OpenAPI 3.x.'\n          : 'The source does not contain a valid OpenAPI 3.x specification';\n        return {\n          isValid: false,\n          error,\n          added: [],\n          removed: [],\n          unchanged: [],\n          hasChanges: false\n        };\n      }\n\n      // Check for title/name changes\n      const storedTitle = storedSpec?.info?.title || null;\n      const newTitle = newSpec?.info?.title || null;\n      const titleChanged = storedSpec && storedTitle && newTitle && storedTitle !== newTitle;\n\n      // Generate hashes for quick change detection\n      const storedSpecHash = generateSpecHash(storedSpec);\n      const remoteSpecHash = generateSpecHash(newSpec);\n      const hasRemoteChanges = storedSpecHash !== remoteSpecHash;\n\n      // Read groupBy from brunoConfig for consistent spec conversion\n      let groupBy = 'tags';\n      try {\n        const { brunoConfig } = loadBrunoConfig(collectionPath);\n        groupBy = brunoConfig?.openapi?.[0]?.groupBy || 'tags';\n      } catch (e) {\n        // Default to 'tags' if brunoConfig is not available\n      }\n\n      const diff = compareSpecs(storedSpec, newSpec, groupBy);\n\n      // Detect remote spec format and determine correct filename\n      const remoteIsYaml = isYamlContent(newSpecContent);\n      const correctSpecFilename = remoteIsYaml ? 'openapi.yaml' : 'openapi.json';\n\n      // Generate unified diff for text diff view\n      const { createTwoFilesPatch } = require('diff');\n      const prettyStored = prettyPrintSpec(storedContent);\n      const prettyNew = prettyPrintSpec(newSpecContent);\n      const totalLines = Math.max(\n        prettyStored.split('\\n').length,\n        prettyNew.split('\\n').length\n      );\n      const unifiedDiff = createTwoFilesPatch(\n        correctSpecFilename, correctSpecFilename,\n        prettyStored, prettyNew,\n        'Current Spec', 'New Spec',\n        { context: totalLines }\n      );\n\n      return {\n        ...diff,\n        isValid: true,\n        storedSpec,\n        newSpec,\n        newSpecContent,\n        specFilename: correctSpecFilename,\n        // Hash comparison for quick change detection\n        hasRemoteChanges,\n        storedSpecHash,\n        remoteSpecHash,\n        storedSpecMissing,\n        // Metadata\n        titleChanged,\n        storedTitle,\n        newTitle,\n        // Text diff\n        unifiedDiff\n      };\n    } catch (error) {\n      console.error('Error comparing OpenAPI specs:', error);\n      throw error;\n    }\n  });\n\n  // Collection Drift Detection - compare stored spec (converted to bru) vs actual .bru files\n  ipcMain.handle('renderer:get-collection-drift', async (event, { collectionPath, compareSpec }) => {\n    try {\n      let brunoConfig;\n      try {\n        ({ brunoConfig } = loadBrunoConfig(collectionPath));\n      } catch (err) {\n        return { error: err.message };\n      }\n\n      // Load spec to compare against — use compareSpec if provided, otherwise read stored spec from disk\n      let specToCompare;\n      const groupBy = brunoConfig?.openapi?.[0]?.groupBy || 'tags';\n\n      if (compareSpec) {\n        specToCompare = compareSpec;\n      } else {\n        const driftSourceUrl = brunoConfig?.openapi?.[0]?.sourceUrl;\n        const driftSpecEntry = driftSourceUrl ? getSpecEntryForUrl(collectionPath) : null;\n        const storedSpecPath = driftSpecEntry ? path.join(getSpecsDir(), driftSpecEntry.filename) : null;\n\n        if (!storedSpecPath || !fs.existsSync(storedSpecPath)) {\n          return {\n            error: null,\n            noStoredSpec: true,\n            inSync: [],\n            modified: [],\n            localOnly: [],\n            missing: [],\n            specEndpointCount: 0,\n            collectionEndpointCount: 0\n          };\n        }\n\n        const storedContent = fs.readFileSync(storedSpecPath, 'utf8');\n        specToCompare = parseSpec(storedContent);\n      }\n\n      // Convert spec to Bruno collection format\n      const specAsCollection = openApiToBruno(specToCompare, { groupBy });\n\n      // Build map of expected items by endpoint ID (method:path)\n      const specItems = buildSpecItemsMap(specAsCollection.items || []);\n\n      // Scan and parse collection endpoints from disk\n      const scanCollectionFiles = (dirPath, relativePath = '') => {\n        const files = [];\n        if (!fs.existsSync(dirPath)) return files;\n        const entries = fs.readdirSync(dirPath);\n        for (const entry of entries) {\n          const fullPath = path.join(dirPath, entry);\n          const relPath = relativePath ? path.join(relativePath, entry) : entry;\n          if (['node_modules', '.git', 'environments'].includes(entry)) continue;\n          const stats = fs.statSync(fullPath);\n          if (stats.isDirectory()) {\n            files.push(...scanCollectionFiles(fullPath, relPath));\n          } else if ((entry.endsWith('.bru') || entry.endsWith('.yml') || entry.endsWith('.yaml'))\n            && !entry.startsWith('folder.') && !entry.startsWith('collection.') && !entry.startsWith('opencollection.')) {\n            files.push({ fullPath, relativePath: relPath });\n          }\n        }\n        return files;\n      };\n\n      const collectionFiles = scanCollectionFiles(collectionPath);\n      const collectionEndpoints = [];\n      for (const { fullPath, relativePath } of collectionFiles) {\n        try {\n          const content = fs.readFileSync(fullPath, 'utf8');\n          const fileFormat = fullPath.endsWith('.yml') || fullPath.endsWith('.yaml') ? 'yml' : 'bru';\n          const parsed = parseRequest(content, { format: fileFormat });\n          if (!parsed?.request) continue;\n          collectionEndpoints.push({\n            fullPath,\n            relativePath,\n            request: parsed.request,\n            name: parsed.meta?.name || parsed.name || path.basename(fullPath)\n          });\n        } catch (err) {\n          console.error(`[Collection Drift] Error parsing ${fullPath}:`, err.message);\n        }\n      }\n\n      // Compare each collection endpoint against spec\n      const result = {\n        inSync: [],\n        modified: [],\n        localOnly: [],\n        missing: []\n      };\n\n      const foundEndpointIds = new Set();\n\n      for (const { fullPath, relativePath, request: actualRequest, name: itemName } of collectionEndpoints) {\n        const method = actualRequest.method?.toUpperCase() || 'GET';\n        const urlPath = normalizeUrlPath(actualRequest.url);\n        const id = `${method}:${urlPath}`;\n\n        foundEndpointIds.add(id);\n\n        const specItem = specItems.get(id);\n        if (!specItem) {\n          // Endpoint exists in collection but not in spec\n          result.localOnly.push({\n            id,\n            method,\n            path: urlPath,\n            filePath: relativePath,\n            pathname: fullPath,\n            name: itemName\n          });\n        } else {\n          // Compare key fields to detect drift\n          const { hasDiff, changes } = compareRequestFields(specItem.request, actualRequest);\n\n          if (hasDiff) {\n            result.modified.push({\n              id,\n              method,\n              path: urlPath,\n              filePath: relativePath,\n              pathname: fullPath,\n              name: itemName,\n              changes: changes.join(', '),\n              actualRequest: { request: actualRequest },\n              specItem\n            });\n          } else {\n            result.inSync.push({\n              id,\n              method,\n              path: urlPath,\n              filePath: relativePath,\n              pathname: fullPath,\n              name: itemName\n            });\n          }\n        }\n      }\n\n      // Find endpoints in spec but missing from collection\n      for (const [id, specItem] of specItems) {\n        if (!foundEndpointIds.has(id)) {\n          // Split only on first colon to preserve :param in paths\n          const colonIndex = id.indexOf(':');\n          const method = id.substring(0, colonIndex);\n          const urlPath = id.substring(colonIndex + 1);\n          result.missing.push({\n            id,\n            method,\n            path: urlPath,\n            name: specItem.name || specItem.request?.url || id\n          });\n        }\n      }\n\n      return {\n        error: null,\n        noStoredSpec: false,\n        ...result,\n        specEndpointCount: specItems.size,\n        collectionEndpointCount: collectionEndpoints.length\n      };\n    } catch (error) {\n      console.error('Error getting collection drift:', error);\n      throw error;\n    }\n  });\n\n  // Get endpoint diff data for visual comparison (spec vs collection)\n  ipcMain.handle('renderer:get-endpoint-diff-data', async (event, { collectionPath, endpointId, newSpec }) => {\n    try {\n      let brunoConfig;\n      try {\n        ({ brunoConfig } = loadBrunoConfig(collectionPath));\n      } catch (err) {\n        return { error: err.message };\n      }\n\n      // Parse endpoint ID (format: \"METHOD:path\")\n      const [method, ...pathParts] = endpointId.split(':');\n      const endpointPath = pathParts.join(':'); // Rejoin in case path contains ':'\n\n      // Get spec to use (new spec if provided, otherwise stored spec)\n      let specToUse = newSpec;\n      if (!specToUse) {\n        const diffSourceUrl = brunoConfig?.openapi?.[0]?.sourceUrl;\n        const diffSpecEntry = diffSourceUrl ? getSpecEntryForUrl(collectionPath) : null;\n        const storedSpecPath = diffSpecEntry ? path.join(getSpecsDir(), diffSpecEntry.filename) : null;\n        if (storedSpecPath && fs.existsSync(storedSpecPath)) {\n          const content = fs.readFileSync(storedSpecPath, 'utf8');\n          specToUse = parseSpec(content);\n        }\n      }\n\n      if (!specToUse) {\n        return { error: 'No spec available' };\n      }\n\n      // Convert spec to Bruno collection format\n      const groupBy = brunoConfig?.openapi?.[0]?.groupBy || 'tags';\n      const specAsCollection = openApiToBruno(specToUse, { groupBy });\n\n      // Find the spec item for this endpoint\n      const specItem = findItemInCollection(specAsCollection.items || [], method, endpointPath)?.item || null;\n\n      // Find the actual collection file for this endpoint\n      const actualFile = findRequestFileOnDisk(collectionPath, method.toUpperCase(), endpointPath);\n      const actualRequest = actualFile?.request || null;\n\n      // Transform to visual diff format (matching what VisualDiffViewer rendering components expect)\n      // Components like VisualDiffUrlBar, VisualDiffParams, etc. read from data.request.*\n      const transformToVisualFormat = (item) => {\n        if (!item) return null;\n        const req = item.request || item;\n        // Strip query string from URL - params are shown in the separate Parameters section\n        const urlWithoutQuery = (req.url || '').split('?')[0];\n\n        // Normalize params/headers to only include fields relevant for comparison.\n        // Different sources (openApiToBruno vs parseRequest) include different metadata\n        // fields (uid, description) which cause false positives in isEqual comparisons.\n        const normalizeParams = (params) => (params || []).map((p) => ({\n          name: p.name,\n          value: p.value,\n          enabled: p.enabled !== false,\n          type: p.type\n        }));\n        const normalizeHeaders = (headers) => (headers || []).map((h) => ({\n          name: h.name,\n          value: h.value,\n          enabled: h.enabled !== false\n        }));\n\n        return {\n          name: item.name || item.meta?.name,\n          type: item.type,\n          request: {\n            method: req.method,\n            url: urlWithoutQuery,\n            params: normalizeParams(req.params),\n            headers: normalizeHeaders(req.headers),\n            body: req.body || {},\n            auth: req.auth || {},\n            vars: item.vars || req.vars || {},\n            assertions: item.assertions || req.assertions || [],\n            script: item.script || req.script || {},\n            tests: item.tests || req.tests || '',\n            docs: item.docs || req.docs || ''\n          }\n        };\n      };\n\n      return {\n        error: null,\n        // oldData = current collection state, newData = expected from spec\n        oldData: transformToVisualFormat(actualRequest),\n        newData: transformToVisualFormat(specItem)\n      };\n    } catch (error) {\n      console.error('Error getting endpoint diff data:', error);\n      return { error: error.message };\n    }\n  });\n\n  // Sync modes: 'spec-only' | 'reset' | 'sync' (default)\n  ipcMain.handle('renderer:apply-openapi-sync', async (event, { collectionPath, addNewRequests, removeDeletedRequests, diff, localOnlyToRemove = [], driftedToReset = [], mode = 'sync', endpointDecisions = {} }) => {\n    try {\n      const { format, brunoConfig, collectionRoot } = loadBrunoConfig(collectionPath);\n      const sourceUrl = brunoConfig?.openapi?.[0]?.sourceUrl;\n\n      // Mode: spec-only - Just save the spec, don't touch collection\n      if (mode === 'spec-only') {\n        if (diff.newSpec && typeof diff.newSpec === 'object') {\n          const specContent = diff.newSpecContent || JSON.stringify(diff.newSpec, null, 2);\n          await saveOpenApiSpecFile({ collectionPath, content: specContent, sourceUrl });\n        }\n\n        // Update sync metadata\n        if (brunoConfig.openapi?.[0]) {\n          brunoConfig.openapi[0] = {\n            ...brunoConfig.openapi[0],\n            lastSyncDate: new Date().toISOString(),\n            specHash: generateSpecHash(diff.newSpec)\n          };\n        }\n\n        await saveBrunoConfig(collectionPath, format, brunoConfig, collectionRoot);\n\n        return { success: true, mode: 'spec-only' };\n      }\n\n      // Mode: reset - Save spec and reset all endpoints to spec (preserve tests/scripts)\n      if (mode === 'reset' && diff.newSpec) {\n        const groupBy = brunoConfig?.openapi?.[0]?.groupBy || 'tags';\n        const newCollection = openApiToBruno(diff.newSpec, { groupBy });\n\n        // Build map of spec items by endpoint ID\n        const specItemsMap = buildSpecItemsMap(newCollection.items || []);\n\n        // Find and update existing .bru files\n        const findAndResetRequest = async (dirPath) => {\n          if (!fs.existsSync(dirPath)) return;\n\n          const files = fs.readdirSync(dirPath);\n          for (const file of files) {\n            const filePath = path.join(dirPath, file);\n            const stats = fs.statSync(filePath);\n\n            if (stats.isDirectory() && !['node_modules', '.git', 'environments'].includes(file)) {\n              await findAndResetRequest(filePath);\n            } else if ((file.endsWith('.bru') || file.endsWith('.yml') || file.endsWith('.yaml'))\n              && !file.startsWith('folder.') && !file.startsWith('collection.')) {\n              try {\n                const content = fs.readFileSync(filePath, 'utf8');\n                const fileFormat = file.endsWith('.yml') || file.endsWith('.yaml') ? 'yml' : 'bru';\n                const existingRequest = parseRequest(content, { format: fileFormat });\n\n                if (existingRequest?.request) {\n                  const method = existingRequest.request.method?.toUpperCase() || 'GET';\n                  const urlPath = normalizeUrlPath(existingRequest.request.url);\n                  const id = `${method}:${urlPath}`;\n\n                  const specItem = specItemsMap.get(id);\n                  if (specItem) {\n                    const mergedRequest = mergeSpecIntoRequest(existingRequest, specItem, { fullReset: true });\n                    const newContent = await stringifyRequestViaWorker(mergedRequest, { format: fileFormat });\n                    await writeFile(filePath, newContent);\n\n                    // Mark as processed\n                    specItemsMap.delete(id);\n                  }\n                }\n              } catch (err) {\n                console.error(`Error resetting file ${filePath}:`, err);\n              }\n            }\n          }\n        };\n\n        await findAndResetRequest(collectionPath);\n\n        // Create missing endpoints from spec\n        for (const [, specItem] of specItemsMap) {\n          let targetFolder = collectionPath;\n          if (specItem.folderName && groupBy === 'tags') {\n            targetFolder = await ensureTagFolder(collectionPath, specItem.folderName, format);\n          }\n\n          const requestContent = await stringifyRequestViaWorker(specItem, { format });\n          const sanitizedFilename = `${sanitizeName(specItem.name || path.basename(specItem.filename || '', `.${format}`))}.${format}`;\n          await writeFile(path.join(targetFolder, sanitizedFilename), requestContent);\n        }\n\n        // Save spec in original format\n        const specContent = diff.newSpecContent || JSON.stringify(diff.newSpec, null, 2);\n        await saveOpenApiSpecFile({ collectionPath, content: specContent, sourceUrl });\n\n        // Update sync metadata\n        if (brunoConfig.openapi?.[0]) {\n          brunoConfig.openapi[0] = {\n            ...brunoConfig.openapi[0],\n            lastSyncDate: new Date().toISOString(),\n            specHash: generateSpecHash(diff.newSpec)\n          };\n        }\n\n        await saveBrunoConfig(collectionPath, format, brunoConfig, collectionRoot);\n\n        return { success: true, mode: 'reset' };\n      }\n\n      // Mode: sync (default) — compute shared values once\n      const groupBy = brunoConfig?.openapi?.[0]?.groupBy || 'tags';\n      let newCollection;\n      if (diff.newSpec) {\n        try {\n          newCollection = openApiToBruno(diff.newSpec, { groupBy });\n        } catch (err) {\n          console.error('[OpenAPI Sync] Error converting spec:', err);\n        }\n      }\n\n      // Remove endpoints before adding new ones to avoid filename collisions\n      // (e.g., when a path is renamed but the summary stays the same, both generate the same filename)\n      if (removeDeletedRequests && diff.removed?.length > 0) {\n        const findAndRemoveRequest = (dirPath) => {\n          if (!fs.existsSync(dirPath)) return;\n\n          const files = fs.readdirSync(dirPath);\n          for (const file of files) {\n            const filePath = path.join(dirPath, file);\n            const stats = fs.statSync(filePath);\n\n            if (stats.isDirectory() && !['node_modules', '.git', 'environments'].includes(file)) {\n              findAndRemoveRequest(filePath);\n            } else if ((file.endsWith('.bru') || file.endsWith('.yml') || file.endsWith('.yaml'))\n              && !file.startsWith('folder.') && !file.startsWith('collection.')) {\n              try {\n                const content = fs.readFileSync(filePath, 'utf8');\n                const request = parseRequest(content, { format: file.endsWith('.yml') || file.endsWith('.yaml') ? 'yml' : 'bru' });\n\n                if (request?.request) {\n                  const method = request.request.method?.toUpperCase();\n                  const url = normalizeUrlPath(request.request.url);\n\n                  if (!isPathInsideCollection(filePath, collectionPath)) {\n                    console.error(`[OpenAPI Sync] Path traversal blocked: ${filePath}`);\n                  } else {\n                    for (const removed of diff.removed) {\n                      const removedPath = normalizeUrlPath(removed.path);\n                      if (method === removed.method.toUpperCase() && url === removedPath) {\n                        fs.unlinkSync(filePath);\n                        break;\n                      }\n                    }\n                  }\n                }\n              } catch (err) {\n                console.error(`Error parsing file ${filePath}:`, err);\n              }\n            }\n          }\n        };\n\n        findAndRemoveRequest(collectionPath);\n      }\n\n      // Remove local-only endpoints (endpoints in collection but not in spec)\n      // Verify file content before deleting — the file may have been modified by the user\n      // between the drift scan and sync execution, making the pre-computed filePath stale.\n      if (localOnlyToRemove?.length > 0) {\n        for (const endpoint of localOnlyToRemove) {\n          if (endpoint.filePath) {\n            const fullPath = path.resolve(collectionPath, endpoint.filePath);\n            if (!isPathInsideCollection(fullPath, collectionPath)) {\n              console.error(`[OpenAPI Sync] Path traversal blocked in localOnlyToRemove: ${endpoint.filePath}`);\n              continue;\n            }\n            if (fs.existsSync(fullPath)) {\n              try {\n                const fileFormat = fullPath.endsWith('.yml') || fullPath.endsWith('.yaml') ? 'yml' : 'bru';\n                const content = fs.readFileSync(fullPath, 'utf8');\n                const parsed = parseRequest(content, { format: fileFormat });\n                if (parsed?.request) {\n                  const fileMethod = parsed.request.method?.toUpperCase();\n                  const fileUrlPath = normalizeUrlPath(parsed.request.url);\n                  if (fileMethod === endpoint.method && fileUrlPath === endpoint.path) {\n                    fs.unlinkSync(fullPath);\n                  }\n                }\n              } catch (err) {\n                console.error(`[OpenAPI Sync] Error verifying file before removal ${endpoint.filePath}:`, err);\n              }\n            }\n          }\n        }\n      }\n\n      if (addNewRequests && diff.added?.length > 0 && newCollection) {\n        for (const endpoint of diff.added) {\n          const normalizedPath = normalizeUrlPath(endpoint.path);\n          const result = findItemInCollection(newCollection.items, endpoint.method, endpoint.path);\n          const newItem = result?.item;\n\n          if (newItem) {\n            // Check if endpoint already exists in collection (prevents overwriting user customizations)\n            const existingFile = findRequestFileOnDisk(collectionPath, endpoint.method.toUpperCase(), normalizedPath);\n\n            if (existingFile) {\n              const mergedRequest = mergeSpecIntoRequest(existingFile.request, newItem);\n              const content = await stringifyRequestViaWorker(mergedRequest, { format: existingFile.fileFormat });\n              await writeFile(existingFile.filePath, content);\n            } else {\n              // Truly new — create file in the appropriate folder\n              let targetFolder = collectionPath;\n              if (result.folderName && groupBy === 'tags') {\n                targetFolder = await ensureTagFolder(collectionPath, result.folderName, format);\n              }\n\n              const requestContent = await stringifyRequestViaWorker(newItem, { format });\n              const sanitizedFilename = `${sanitizeName(newItem.name || path.basename(newItem.filename || '', `.${format}`))}.${format}`;\n              await writeFile(path.join(targetFolder, sanitizedFilename), requestContent);\n            }\n          }\n        }\n      }\n\n      // Handle modified endpoints with conflict resolutions\n      // endpointDecisions: { endpointId: 'keep-mine' | 'accept-incoming' }\n      // Only apply changes for endpoints marked as 'accept-incoming' or not in decisions (default: apply)\n      if (diff.modified?.length > 0 && newCollection) {\n        for (const endpoint of diff.modified) {\n          // Check if user chose to keep their version\n          const endpointId = endpoint.id || `${endpoint.method.toUpperCase()}:${normalizeUrlPath(endpoint.path)}`;\n          const decision = endpointDecisions[endpointId];\n          if (decision === 'keep-mine') {\n            continue;\n          }\n\n          // Apply incoming changes for this endpoint\n          const normalizedPath = normalizeUrlPath(endpoint.path);\n          const result = findItemInCollection(newCollection.items, endpoint.method, endpoint.path);\n          const newItem = result?.item;\n          const existingFile = findRequestFileOnDisk(collectionPath, endpoint.method.toUpperCase(), normalizedPath);\n\n          if (newItem && existingFile) {\n            const mergedRequest = mergeSpecIntoRequest(existingFile.request, newItem);\n            const content = await stringifyRequestViaWorker(mergedRequest, { format: existingFile.fileFormat });\n            await writeFile(existingFile.filePath, content);\n          }\n        }\n      }\n\n      // Handle drifted endpoints to reset (collection differs from stored spec)\n      // These are endpoints where user chose 'accept-incoming' to reset to spec\n      if (driftedToReset?.length > 0) {\n        // Reuse newCollection if available, otherwise fall back to stored spec\n        let driftCollection = newCollection;\n        if (!driftCollection) {\n          const applySpecEntry = getSpecEntryForUrl(collectionPath);\n          const storedSpecPath = applySpecEntry ? path.join(getSpecsDir(), applySpecEntry.filename) : null;\n          if (storedSpecPath && fs.existsSync(storedSpecPath)) {\n            try {\n              driftCollection = openApiToBruno(parseSpec(fs.readFileSync(storedSpecPath, 'utf8')), { groupBy });\n            } catch (err) {\n              console.error('[OpenAPI Sync] Error converting stored spec for drift reset:', err);\n            }\n          }\n        }\n\n        if (driftCollection) {\n          const specItemsMap = buildSpecItemsMap(driftCollection.items || []);\n\n          for (const endpoint of driftedToReset) {\n            const specItem = specItemsMap.get(endpoint.id);\n            if (!specItem) {\n              continue;\n            }\n\n            if (endpoint.filePath) {\n              const fullPath = path.resolve(collectionPath, endpoint.filePath);\n              if (!isPathInsideCollection(fullPath, collectionPath)) {\n                console.error(`[OpenAPI Sync] Path traversal blocked in driftedToReset: ${endpoint.filePath}`);\n                continue;\n              }\n              if (fs.existsSync(fullPath)) {\n                try {\n                  const fileFormat = fullPath.endsWith('.yml') || fullPath.endsWith('.yaml') ? 'yml' : 'bru';\n                  const existingContent = fs.readFileSync(fullPath, 'utf8');\n                  const existingRequest = parseRequest(existingContent, { format: fileFormat });\n                  const mergedRequest = mergeSpecIntoRequest(existingRequest, specItem, { fullReset: true });\n                  const content = await stringifyRequestViaWorker(mergedRequest, { format: fileFormat });\n                  await writeFile(fullPath, content);\n                } catch (err) {\n                  console.error(`[OpenAPI Sync] Error resetting drifted endpoint ${endpoint.id}:`, err);\n                }\n              }\n            }\n          }\n        }\n      }\n\n      // Save spec only if we have a valid spec\n      if (diff.newSpec && typeof diff.newSpec === 'object') {\n        const specContent = diff.newSpecContent || JSON.stringify(diff.newSpec, null, 2);\n        await saveOpenApiSpecFile({ collectionPath, content: specContent, sourceUrl });\n      }\n\n      if (brunoConfig.openapi?.[0]) {\n        const updated = {\n          ...brunoConfig.openapi[0],\n          lastSyncDate: new Date().toISOString()\n        };\n        // Only update specHash when we have a valid newSpec, otherwise preserve existing hash\n        if (diff.newSpec) {\n          updated.specHash = generateSpecHash(diff.newSpec);\n        }\n        brunoConfig.openapi[0] = updated;\n      }\n\n      await saveBrunoConfig(collectionPath, format, brunoConfig, collectionRoot);\n\n      return { success: true };\n    } catch (error) {\n      console.error('Error applying OpenAPI sync:', error);\n      throw error;\n    }\n  });\n\n  // Update OpenAPI sync configuration (e.g., source URL)\n  ipcMain.handle('renderer:update-openapi-sync-config', async (event, { collectionPath, config }) => {\n    try {\n      const { format, brunoConfig, collectionRoot } = loadBrunoConfig(collectionPath);\n\n      // Merge new config into existing entry (allowlist keys only)\n      const allowedKeys = ['sourceUrl', 'groupBy', 'lastSyncDate', 'specHash', 'autoCheck', 'autoCheckInterval'];\n      const sanitizedConfig = {};\n      for (const key of allowedKeys) {\n        if (key in config) {\n          sanitizedConfig[key] = config[key];\n        }\n      }\n\n      // sourceUrl is required — it identifies which entry to create/update\n      if (!sanitizedConfig.sourceUrl) {\n        throw new Error('sourceUrl is required to update openapi sync config');\n      }\n\n      // Validate sourceUrl — reject protocol-based non-http(s) URLs (e.g. ftp://, file://)\n      if (sanitizedConfig.sourceUrl.includes('://') && !isValidHttpUrl(sanitizedConfig.sourceUrl)) {\n        throw new Error('Invalid URL: only http and https URLs are allowed');\n      }\n\n      // Resolve to absolute for consistent internal handling (saveBrunoConfig converts back to relative)\n      sanitizedConfig.sourceUrl = resolveSourceUrl(collectionPath, sanitizedConfig.sourceUrl);\n\n      // Update or create the single openapi entry\n      const existingEntry = brunoConfig.openapi?.[0];\n      if (existingEntry) {\n        brunoConfig.openapi = [{ ...existingEntry, ...sanitizedConfig }];\n      } else {\n        if (!('autoCheck' in sanitizedConfig)) sanitizedConfig.autoCheck = true;\n        if (!('autoCheckInterval' in sanitizedConfig)) sanitizedConfig.autoCheckInterval = 5;\n        brunoConfig.openapi = [sanitizedConfig];\n      }\n\n      // Save updated config\n      await saveBrunoConfig(collectionPath, format, brunoConfig, collectionRoot);\n\n      return { success: true };\n    } catch (error) {\n      console.error('Error updating OpenAPI sync config:', error);\n      throw error;\n    }\n  });\n\n  // Save OpenAPI spec file and update sync metadata (used by both connect and import flows)\n  ipcMain.handle('renderer:save-openapi-spec', async (event, { collectionPath, specContent }) => {\n    try {\n      await saveSpecAndUpdateMetadata({ collectionPath, specContent });\n      return { success: true };\n    } catch (error) {\n      console.error('Error saving OpenAPI spec file:', error);\n      throw error;\n    }\n  });\n\n  // Fetch OpenAPI spec content from a remote URL or local file path\n  ipcMain.handle('renderer:fetch-openapi-spec', async (event, {\n    collectionUid, collectionPath, sourceUrl, environmentContext\n  }) => {\n    try {\n      const result = await fetchSpecFromSource({ collectionUid, collectionPath, sourceUrl, environmentContext });\n      if (result.error) return { error: result.error, errorCode: result.errorCode };\n      if (!isValidOpenApiSpec(result.spec)) {\n        const error = result.spec?.swagger\n          ? 'Swagger 2.0 is not supported. Please convert your spec to OpenAPI 3.x.'\n          : 'The source does not contain a valid OpenAPI 3.x specification';\n        return { error };\n      }\n      return { content: result.content };\n    } catch (error) {\n      return { error: error.message || 'Failed to fetch spec' };\n    }\n  });\n\n  // Read stored OpenAPI spec file from AppData\n  ipcMain.handle('renderer:read-openapi-spec', async (event, { collectionPath }) => {\n    try {\n      const entry = getSpecEntryForUrl(collectionPath);\n      if (!entry) return { error: 'Spec file not found' };\n      const specPath = path.join(getSpecsDir(), entry.filename);\n      if (!fs.existsSync(specPath)) return { error: 'Spec file not found' };\n      return { content: fs.readFileSync(specPath, 'utf8') };\n    } catch (error) {\n      return { error: error.message || 'Failed to read spec file' };\n    }\n  });\n\n  // Remove OpenAPI sync configuration (disconnect sync)\n  ipcMain.handle('renderer:remove-openapi-sync-config', async (event, { collectionPath, deleteSpecFile = false }) => {\n    try {\n      const { format, brunoConfig, collectionRoot } = loadBrunoConfig(collectionPath);\n\n      // Remove openapi config\n      delete brunoConfig.openapi;\n      await saveBrunoConfig(collectionPath, format, brunoConfig, collectionRoot);\n\n      // Remove spec file and metadata for this collection\n      const meta = loadSpecMetadata();\n      const entry = (meta[collectionPath] || [])[0];\n      if (entry && deleteSpecFile) {\n        const specPath = path.join(getSpecsDir(), entry.filename);\n        if (fs.existsSync(specPath)) fs.unlinkSync(specPath);\n      }\n      delete meta[collectionPath];\n      saveSpecMetadata(meta);\n\n      return { success: true };\n    } catch (error) {\n      console.error('Error removing OpenAPI sync config:', error);\n      throw error;\n    }\n  });\n\n  // Add missing endpoints to collection (from stored spec)\n  ipcMain.handle('renderer:add-missing-endpoints', async (event, { collectionPath, endpoints }) => {\n    try {\n      const { format, brunoConfig } = loadBrunoConfig(collectionPath);\n      const groupBy = brunoConfig?.openapi?.[0]?.groupBy || 'tags';\n      const specCollection = loadStoredSpecCollection(collectionPath, brunoConfig);\n\n      let addedCount = 0;\n      for (const endpoint of endpoints) {\n        const result = findItemInCollection(specCollection.items, endpoint.method, endpoint.path);\n\n        if (result) {\n          const { item: specItem, folderName } = result;\n          let targetFolder = collectionPath;\n\n          // Use folder name from spec collection structure\n          if (folderName && groupBy === 'tags') {\n            targetFolder = await ensureTagFolder(collectionPath, folderName, format);\n          }\n\n          const requestContent = await stringifyRequestViaWorker(specItem, { format });\n          const sanitizedFilename = `${sanitizeName(specItem.name || path.basename(specItem.filename || '', `.${format}`))}.${format}`;\n          await writeFile(path.join(targetFolder, sanitizedFilename), requestContent);\n          addedCount++;\n        }\n      }\n\n      return { success: true, addedCount };\n    } catch (error) {\n      console.error('Error adding missing endpoints:', error);\n      throw error;\n    }\n  });\n\n  // Reset modified endpoints to match the spec\n  ipcMain.handle('renderer:reset-endpoints-to-spec', async (event, { collectionPath, endpoints }) => {\n    try {\n      const { brunoConfig } = loadBrunoConfig(collectionPath);\n      const specCollection = loadStoredSpecCollection(collectionPath, brunoConfig);\n\n      let resetCount = 0;\n      for (const endpoint of endpoints) {\n        // Find the spec version of this endpoint\n        const specItem = findItemInCollection(specCollection.items, endpoint.method, endpoint.path)?.item;\n\n        if (specItem && endpoint.pathname) {\n          if (!isPathInsideCollection(endpoint.pathname, collectionPath)) {\n            console.error(`[OpenAPI Sync] Path traversal blocked in reset-endpoints: ${endpoint.pathname}`);\n            continue;\n          }\n\n          try {\n            const fileFormat = endpoint.pathname.endsWith('.yml') || endpoint.pathname.endsWith('.yaml') ? 'yml' : 'bru';\n            const existingContent = fs.readFileSync(endpoint.pathname, 'utf8');\n            const existingRequest = parseRequest(existingContent, { format: fileFormat });\n            const mergedRequest = mergeSpecIntoRequest(existingRequest, specItem, { fullReset: true });\n            const requestContent = await stringifyRequestViaWorker(mergedRequest, { format: fileFormat });\n            await writeFile(endpoint.pathname, requestContent);\n            resetCount++;\n          } catch (err) {\n            console.error(`[OpenAPI Sync] Error resetting endpoint ${endpoint.pathname}:`, err);\n          }\n        }\n      }\n\n      return { success: true, resetCount };\n    } catch (error) {\n      console.error('Error resetting endpoints to spec:', error);\n      throw error;\n    }\n  });\n\n  // Delete endpoints from collection\n  ipcMain.handle('renderer:delete-endpoints', async (event, { collectionPath, endpoints }) => {\n    try {\n      let deletedCount = 0;\n\n      for (const endpoint of endpoints) {\n        if (endpoint.pathname && fs.existsSync(endpoint.pathname)) {\n          if (!isPathInsideCollection(endpoint.pathname, collectionPath)) {\n            console.error(`[OpenAPI Sync] Path traversal blocked in delete-endpoints: ${endpoint.pathname}`);\n            continue;\n          }\n          fs.unlinkSync(endpoint.pathname);\n          deletedCount++;\n        }\n      }\n\n      return { success: true, deletedCount };\n    } catch (error) {\n      console.error('Error deleting endpoints:', error);\n      throw error;\n    }\n  });\n};\n\nmodule.exports = registerOpenAPISyncIpc;\nmodule.exports.saveSpecAndUpdateMetadata = saveSpecAndUpdateMetadata;\nmodule.exports.cleanupSpecFilesForCollection = cleanupSpecFilesForCollection;\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/preferences.js",
    "content": "const fs = require('fs');\nconst { ipcMain, nativeTheme } = require('electron');\nconst { getPreferences, savePreferences } = require('../store/preferences');\nconst { getGitVersion } = require('../utils/git');\nconst { globalEnvironmentsStore } = require('../store/global-environments');\nconst { getCachedSystemProxy, fetchSystemProxy } = require('../store/system-proxy');\nconst { resolveDefaultLocation } = require('../utils/default-location');\nconst onboardUser = require('../app/onboarding');\nconst LastOpenedCollections = require('../store/last-opened-collections');\nconst { clearAgentCache } = require('@usebruno/requests');\n\nconst registerPreferencesIpc = (mainWindow) => {\n  const lastOpenedCollections = new LastOpenedCollections();\n\n  const onboardingPromise = onboardUser(mainWindow, lastOpenedCollections);\n\n  ipcMain.handle('renderer:ready', async (event) => {\n    await onboardingPromise;\n\n    // load preferences\n    const preferences = getPreferences();\n\n    // Set the default location if it hasn't been set by the user\n    if (!preferences.general?.defaultLocation || !fs.existsSync(preferences.general.defaultLocation)) {\n      preferences.general ??= {};\n      preferences.general.defaultLocation = resolveDefaultLocation();\n      await savePreferences(preferences);\n    }\n\n    mainWindow.webContents.send('main:load-preferences', preferences);\n\n    try {\n      // load global environments\n      const globalEnvironments = globalEnvironmentsStore.getGlobalEnvironments();\n      let activeGlobalEnvironmentUid = globalEnvironmentsStore.getActiveGlobalEnvironmentUid();\n      activeGlobalEnvironmentUid = globalEnvironments?.find((env) => env?.uid == activeGlobalEnvironmentUid) ? activeGlobalEnvironmentUid : null;\n      mainWindow.webContents.send('main:load-global-environments', { globalEnvironments, activeGlobalEnvironmentUid });\n    } catch (error) {\n      console.error('Error occured while fetching global environements!');\n      console.error(error);\n    }\n\n    const gitVersion = await getGitVersion();\n    mainWindow.webContents.send('main:git-version', gitVersion);\n\n    ipcMain.emit('main:renderer-ready', mainWindow);\n  });\n\n  ipcMain.on('main:open-preferences', () => {\n    mainWindow.webContents.send('main:open-preferences');\n  });\n\n  ipcMain.handle('renderer:save-preferences', async (event, preferences) => {\n    try {\n      await savePreferences(preferences);\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.handle('renderer:clear-http-https-agent-cache', async () => {\n    try {\n      clearAgentCache();\n    } catch (error) {\n      return Promise.reject(error);\n    }\n  });\n\n  ipcMain.on('renderer:theme-change', (event, theme) => {\n    nativeTheme.themeSource = theme;\n  });\n\n  ipcMain.handle('renderer:get-system-proxy-variables', async () => {\n    return await getCachedSystemProxy();\n  });\n\n  ipcMain.handle('renderer:refresh-system-proxy', async () => {\n    return await fetchSystemProxy({ refresh: true });\n  });\n};\n\nmodule.exports = registerPreferencesIpc;\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/system-monitor.js",
    "content": "const { ipcMain } = require('electron');\n\nconst registerSystemMonitorIpc = (mainWindow, systemMonitor) => {\n  ipcMain.handle('renderer:start-system-monitoring', (event, intervalMs = 2000) => {\n    try {\n      systemMonitor.start(mainWindow, intervalMs);\n      return { success: true };\n    } catch (error) {\n      console.error('Error starting system monitoring:', error);\n      return { success: false, error: error.message };\n    }\n  });\n\n  ipcMain.handle('renderer:stop-system-monitoring', (event) => {\n    try {\n      systemMonitor.stop();\n      return { success: true };\n    } catch (error) {\n      console.error('Error stopping system monitoring:', error);\n      return { success: false, error: error.message };\n    }\n  });\n\n  ipcMain.handle('renderer:is-system-monitoring-active', (event) => {\n    try {\n      const isActive = systemMonitor.isRunning();\n      return { success: true, isActive };\n    } catch (error) {\n      console.error('Error checking system monitoring status:', error);\n      return { success: false, error: error.message, isActive: false };\n    }\n  });\n};\n\nmodule.exports = registerSystemMonitorIpc;\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/terminal.js",
    "content": "const { ipcMain } = require('electron');\nconst pty = require('@lydell/node-pty');\nconst os = require('os');\nconst path = require('path');\nconst isDev = require('electron-is-dev');\n\nclass TerminalManager {\n  constructor() {\n    this.terminals = new Map();\n    this.setupIpcHandlers();\n  }\n\n  setupIpcHandlers() {\n    // Create a new terminal session\n    ipcMain.handle('terminal:create', (event, options = {}) => {\n      try {\n        const sessionId = this.generateSessionId();\n        const shell = this.getDefaultShell();\n        // Use provided cwd or default to home directory\n        const cwd = options.cwd || this.getDefaultCwd();\n\n        if (isDev) {\n          console.log(`Creating new terminal session: ${sessionId} at ${cwd}`);\n        }\n\n        const ptyProcess = pty.spawn(shell, [], {\n          name: 'xterm-color',\n          cols: 80,\n          rows: 24,\n          cwd: cwd,\n          env: process.env\n        });\n\n        // Store terminal session\n        this.terminals.set(sessionId, {\n          pty: ptyProcess,\n          webContents: event.sender,\n          cwd: cwd // Store initial cwd\n        });\n\n        // Handle terminal output\n        ptyProcess.onData((data) => {\n          try {\n            if (data && event.sender && !event.sender.isDestroyed()) {\n              event.sender.send(`terminal:data:${sessionId}`, data);\n            }\n          } catch (error) {\n            console.warn('Failed to send terminal data:', error);\n          }\n        });\n\n        // Handle terminal exit\n        ptyProcess.onExit(({ exitCode, signal }) => {\n          try {\n            this.terminals.delete(sessionId);\n            if (event.sender && !event.sender.isDestroyed()) {\n              event.sender.send(`terminal:exit:${sessionId}`, { exitCode, signal });\n            }\n          } catch (error) {\n            console.warn('Failed to handle terminal exit:', error);\n          }\n        });\n\n        return sessionId;\n      } catch (error) {\n        console.error('Failed to create terminal session:', error);\n        return null;\n      }\n    });\n\n    // Send input to terminal\n    ipcMain.on('terminal:input', (event, sessionId, data) => {\n      try {\n        const terminal = this.terminals.get(sessionId);\n        if (terminal && terminal.pty && data) {\n          terminal.pty.write(data);\n        }\n      } catch (error) {\n        console.warn('Failed to send input to terminal:', error);\n      }\n    });\n\n    // Resize terminal\n    ipcMain.on('terminal:resize', (event, sessionId, { cols, rows }) => {\n      try {\n        const terminal = this.terminals.get(sessionId);\n        if (terminal && terminal.pty && cols > 0 && rows > 0) {\n          terminal.pty.resize(cols, rows);\n        }\n      } catch (error) {\n        console.warn('Failed to resize terminal:', error);\n      }\n    });\n\n    // Kill terminal session\n    ipcMain.on('terminal:kill', (event, sessionId) => {\n      const terminal = this.terminals.get(sessionId);\n      if (terminal && terminal.pty) {\n        try {\n          terminal.pty.kill();\n          this.terminals.delete(sessionId);\n        } catch (error) {\n          console.error('Failed to kill terminal:', error);\n        }\n      }\n    });\n\n    // Get list of all active terminal sessions\n    ipcMain.handle('terminal:list-sessions', (event) => {\n      try {\n        const sessions = [];\n        for (const [sessionId, terminal] of this.terminals.entries()) {\n          if (terminal && terminal.pty) {\n            // Check if process is still alive\n            try {\n              const pid = terminal.pty.pid;\n              process.kill(pid, 0); // Signal 0 just checks if process exists\n              sessions.push({\n                sessionId,\n                cwd: terminal.cwd || '', // Use stored cwd\n                pid: pid\n              });\n            } catch (error) {\n              // Process doesn't exist, remove it\n              this.terminals.delete(sessionId);\n            }\n          }\n        }\n        return sessions;\n      } catch (error) {\n        console.error('Failed to list terminal sessions:', error);\n        return [];\n      }\n    });\n  }\n\n  getDefaultShell() {\n    if (process.platform === 'win32') {\n      // Try PowerShell Core first (pwsh.exe), then fall back to Windows PowerShell (powershell.exe)\n      return process.env.PWSH || 'powershell.exe';\n    } else {\n      return process.env.SHELL || '/bin/bash';\n    }\n  }\n\n  getDefaultCwd() {\n    // Try to use user's home directory as default\n    return process.env.HOME || process.env.USERPROFILE || os.homedir();\n  }\n\n  generateSessionId() {\n    return `terminal_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;\n  }\n\n  // Clean up terminals when window closes\n  cleanup(webContents) {\n    for (const [sessionId, terminal] of this.terminals.entries()) {\n      if (terminal.webContents === webContents) {\n        try {\n          terminal.pty.kill();\n          this.terminals.delete(sessionId);\n        } catch (error) {\n          console.error('Failed to cleanup terminal:', error);\n        }\n      }\n    }\n  }\n\n  // Kill all terminals\n  killAll() {\n    for (const [sessionId, terminal] of this.terminals.entries()) {\n      try {\n        terminal.pty.kill();\n      } catch (error) {\n        console.error('Failed to kill terminal:', error);\n      }\n    }\n    this.terminals.clear();\n  }\n}\n\nmodule.exports = TerminalManager;\n"
  },
  {
    "path": "packages/bruno-electron/src/ipc/workspace.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst fsExtra = require('fs-extra');\nconst archiver = require('archiver');\nconst extractZip = require('extract-zip');\nconst { ipcMain, dialog } = require('electron');\nconst isDev = require('electron-is-dev');\nconst { createDirectory, sanitizeName, writeFile, DEFAULT_GITIGNORE } = require('../utils/filesystem');\nconst yaml = require('js-yaml');\nconst LastOpenedWorkspaces = require('../store/last-opened-workspaces');\nconst { defaultWorkspaceManager } = require('../store/default-workspace');\nconst { globalEnvironmentsManager } = require('../store/workspace-environments');\n\nconst {\n  createWorkspaceConfig,\n  readWorkspaceConfig,\n  writeWorkspaceConfig,\n  validateWorkspaceConfig,\n  updateWorkspaceName,\n  updateWorkspaceDocs,\n  addCollectionToWorkspace,\n  removeCollectionFromWorkspace,\n  reorderWorkspaceCollections,\n  getWorkspaceCollections,\n  normalizeCollectionEntry,\n  validateWorkspacePath,\n  validateWorkspaceDirectory,\n  getWorkspaceUid\n} = require('../utils/workspace-config');\n\nconst { isValidCollectionDirectory } = require('../utils/filesystem');\n\nconst DEFAULT_WORKSPACE_NAME = 'My Workspace';\n\nconst prepareWorkspaceConfigForClient = (workspaceConfig, workspacePath, isDefault) => {\n  const collections = workspaceConfig.collections || [];\n  const filteredCollections = collections\n    .map((collection) => {\n      if (collection.path && !path.isAbsolute(collection.path)) {\n        return { ...collection, path: path.resolve(workspacePath, collection.path) };\n      }\n      return collection;\n    })\n    .filter((collection) => collection.path && isValidCollectionDirectory(collection.path));\n\n  const config = {\n    ...workspaceConfig,\n    collections: filteredCollections\n  };\n\n  if (isDefault) {\n    return {\n      ...config,\n      name: DEFAULT_WORKSPACE_NAME,\n      type: 'default'\n    };\n  }\n  return config;\n};\n\nconst registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {\n  const lastOpenedWorkspaces = new LastOpenedWorkspaces();\n\n  ipcMain.handle('renderer:create-workspace',\n    async (event, workspaceName, workspaceFolderName, workspaceLocation) => {\n      try {\n        workspaceFolderName = sanitizeName(workspaceFolderName);\n        const dirPath = path.join(workspaceLocation, workspaceFolderName);\n\n        if (fs.existsSync(dirPath)) {\n          const files = fs.readdirSync(dirPath);\n          if (files.length > 0) {\n            throw new Error(`workspace: ${dirPath} already exists and is not empty`);\n          }\n        }\n\n        validateWorkspaceDirectory(dirPath);\n\n        if (!fs.existsSync(dirPath)) {\n          await createDirectory(dirPath);\n        }\n\n        await createDirectory(path.join(dirPath, 'collections'));\n\n        const workspaceUid = getWorkspaceUid(dirPath);\n        const isDefault = workspaceUid === 'default';\n        const workspaceConfig = createWorkspaceConfig(workspaceName);\n\n        await writeWorkspaceConfig(dirPath, workspaceConfig);\n        await writeFile(path.join(dirPath, '.gitignore'), DEFAULT_GITIGNORE);\n\n        lastOpenedWorkspaces.add(dirPath);\n\n        const configForClient = prepareWorkspaceConfigForClient(workspaceConfig, dirPath, isDefault);\n\n        mainWindow.webContents.send('main:workspace-opened', dirPath, workspaceUid, configForClient);\n\n        if (workspaceWatcher) {\n          workspaceWatcher.addWatcher(mainWindow, dirPath);\n        }\n\n        return {\n          workspaceConfig: configForClient,\n          workspaceUid,\n          workspacePath: dirPath\n        };\n      } catch (error) {\n        throw error;\n      }\n    });\n\n  ipcMain.handle('renderer:open-workspace', async (event, workspacePath) => {\n    try {\n      validateWorkspacePath(workspacePath);\n\n      const workspaceConfig = readWorkspaceConfig(workspacePath);\n      validateWorkspaceConfig(workspaceConfig);\n\n      const workspaceUid = getWorkspaceUid(workspacePath);\n      const isDefault = workspaceUid === 'default';\n      const configForClient = prepareWorkspaceConfigForClient(workspaceConfig, workspacePath, isDefault);\n\n      lastOpenedWorkspaces.add(workspacePath);\n\n      mainWindow.webContents.send('main:workspace-opened', workspacePath, workspaceUid, configForClient);\n\n      if (workspaceWatcher) {\n        workspaceWatcher.addWatcher(mainWindow, workspacePath);\n      }\n\n      return {\n        workspaceConfig: configForClient,\n        workspaceUid,\n        workspacePath\n      };\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:open-workspace-dialog', async (event) => {\n    try {\n      const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {\n        properties: ['openDirectory'],\n        title: 'Open Workspace',\n        buttonLabel: 'Open Workspace'\n      });\n\n      if (canceled || filePaths.length === 0) {\n        return null;\n      }\n\n      const workspacePath = filePaths[0];\n      validateWorkspacePath(workspacePath);\n\n      const workspaceConfig = readWorkspaceConfig(workspacePath);\n      validateWorkspaceConfig(workspaceConfig);\n\n      const workspaceUid = getWorkspaceUid(workspacePath);\n      const isDefault = workspaceUid === 'default';\n      const configForClient = prepareWorkspaceConfigForClient(workspaceConfig, workspacePath, isDefault);\n\n      lastOpenedWorkspaces.add(workspacePath);\n\n      mainWindow.webContents.send('main:workspace-opened', workspacePath, workspaceUid, configForClient);\n\n      if (workspaceWatcher) {\n        workspaceWatcher.addWatcher(mainWindow, workspacePath);\n      }\n\n      return {\n        workspaceConfig: configForClient,\n        workspaceUid,\n        workspacePath\n      };\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:load-workspace-collections', async (event, workspacePath) => {\n    try {\n      if (!workspacePath) {\n        throw new Error('Workspace path is undefined');\n      }\n\n      validateWorkspacePath(workspacePath);\n      return getWorkspaceCollections(workspacePath);\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:reorder-workspace-collections', async (event, workspacePath, collectionPaths) => {\n    try {\n      if (!workspacePath) {\n        throw new Error('Workspace path is undefined');\n      }\n      validateWorkspacePath(workspacePath);\n      await reorderWorkspaceCollections(workspacePath, collectionPaths);\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:load-workspace-apispecs', async (event, workspacePath) => {\n    try {\n      if (!workspacePath) {\n        throw new Error('Workspace path is undefined');\n      }\n\n      const workspaceFilePath = path.join(workspacePath, 'workspace.yml');\n\n      if (!fs.existsSync(workspaceFilePath)) {\n        throw new Error('Invalid workspace: workspace.yml not found');\n      }\n\n      const yamlContent = fs.readFileSync(workspaceFilePath, 'utf8');\n      const workspaceConfig = yaml.load(yamlContent);\n\n      if (!workspaceConfig || typeof workspaceConfig !== 'object') {\n        return [];\n      }\n\n      const specs = workspaceConfig.specs || [];\n\n      const resolvedSpecs = specs\n        .map((spec) => {\n          if (spec.path && !path.isAbsolute(spec.path)) {\n            return {\n              ...spec,\n              path: path.join(workspacePath, spec.path)\n            };\n          }\n          return spec;\n        })\n        .filter((spec) => spec.path && fs.existsSync(spec.path));\n\n      return resolvedSpecs;\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:get-last-opened-workspaces', async () => {\n    try {\n      const workspacePaths = lastOpenedWorkspaces.getAll();\n      const validWorkspaces = [];\n      const invalidPaths = [];\n\n      for (const workspacePath of workspacePaths) {\n        const workspaceYmlPath = path.join(workspacePath, 'workspace.yml');\n\n        if (fs.existsSync(workspaceYmlPath)) {\n          validWorkspaces.push(workspacePath);\n        } else {\n          invalidPaths.push(workspacePath);\n        }\n      }\n\n      for (const invalidPath of invalidPaths) {\n        lastOpenedWorkspaces.remove(invalidPath);\n      }\n\n      return validWorkspaces;\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:rename-workspace', async (event, workspacePath, newName) => {\n    try {\n      await updateWorkspaceName(workspacePath, newName);\n      return { success: true };\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:close-workspace', async (event, workspacePath) => {\n    try {\n      lastOpenedWorkspaces.remove(workspacePath);\n\n      if (workspaceWatcher) {\n        workspaceWatcher.removeWatcher(workspacePath);\n      }\n\n      return { success: true };\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:export-workspace', async (event, workspacePath, workspaceName) => {\n    try {\n      if (!workspacePath || !fs.existsSync(workspacePath)) {\n        throw new Error('Workspace path does not exist');\n      }\n\n      const defaultFileName = `${sanitizeName(workspaceName)}.zip`;\n      const { filePath, canceled } = await dialog.showSaveDialog(mainWindow, {\n        title: 'Export Workspace',\n        defaultPath: defaultFileName,\n        filters: [{ name: 'Zip Files', extensions: ['zip'] }]\n      });\n\n      if (canceled || !filePath) {\n        return { success: false, canceled: true };\n      }\n\n      const ignoredDirectories = ['node_modules', '.git'];\n\n      await new Promise((resolve, reject) => {\n        const output = fs.createWriteStream(filePath);\n        const archive = archiver('zip', { zlib: { level: 9 } });\n\n        output.on('close', () => {\n          resolve();\n        });\n\n        archive.on('error', (err) => {\n          reject(err);\n        });\n\n        archive.pipe(output);\n\n        const addDirectoryToArchive = (dirPath, archivePath) => {\n          const entries = fs.readdirSync(dirPath, { withFileTypes: true });\n\n          for (const entry of entries) {\n            const fullPath = path.join(dirPath, entry.name);\n            const entryArchivePath = archivePath ? path.join(archivePath, entry.name) : entry.name;\n\n            if (entry.isDirectory()) {\n              if (!ignoredDirectories.includes(entry.name)) {\n                addDirectoryToArchive(fullPath, entryArchivePath);\n              }\n            } else {\n              archive.file(fullPath, { name: entryArchivePath });\n            }\n          }\n        };\n\n        addDirectoryToArchive(workspacePath, '');\n        archive.finalize();\n      });\n\n      return { success: true, filePath };\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:import-workspace', async (event, zipFilePath, extractLocation) => {\n    try {\n      if (!zipFilePath || !fs.existsSync(zipFilePath)) {\n        throw new Error('Zip file does not exist');\n      }\n\n      if (!extractLocation || !fs.existsSync(extractLocation)) {\n        throw new Error('Extract location does not exist');\n      }\n\n      const tempDir = path.join(extractLocation, `_bruno_temp_${Date.now()}`);\n      await fsExtra.ensureDir(tempDir);\n\n      try {\n        await extractZip(zipFilePath, { dir: tempDir });\n\n        const extractedItems = fs.readdirSync(tempDir);\n        let workspaceDir = tempDir;\n\n        if (extractedItems.length === 1) {\n          const singleItem = path.join(tempDir, extractedItems[0]);\n          if (fs.statSync(singleItem).isDirectory()) {\n            workspaceDir = singleItem;\n          }\n        }\n\n        const workspaceYmlPath = path.join(workspaceDir, 'workspace.yml');\n        if (!fs.existsSync(workspaceYmlPath)) {\n          throw new Error('Invalid workspace: workspace.yml not found in the zip file');\n        }\n\n        const workspaceConfig = yaml.load(fs.readFileSync(workspaceYmlPath, 'utf8'));\n        const workspaceName = workspaceConfig.info.name || 'Imported Workspace';\n        const sanitizedName = sanitizeName(workspaceName);\n\n        let finalWorkspacePath = path.join(extractLocation, sanitizedName);\n        let counter = 1;\n        while (fs.existsSync(finalWorkspacePath)) {\n          finalWorkspacePath = path.join(extractLocation, `${sanitizedName} (${counter})`);\n          counter++;\n        }\n\n        if (workspaceDir !== tempDir) {\n          await fsExtra.move(workspaceDir, finalWorkspacePath);\n          await fsExtra.remove(tempDir);\n        } else {\n          await fsExtra.move(tempDir, finalWorkspacePath);\n        }\n\n        validateWorkspacePath(finalWorkspacePath);\n\n        const finalConfig = readWorkspaceConfig(finalWorkspacePath);\n        validateWorkspaceConfig(finalConfig);\n\n        const workspaceUid = getWorkspaceUid(finalWorkspacePath);\n        const isDefault = workspaceUid === 'default';\n        const configForClient = prepareWorkspaceConfigForClient(finalConfig, finalWorkspacePath, isDefault);\n\n        lastOpenedWorkspaces.add(finalWorkspacePath);\n\n        mainWindow.webContents.send('main:workspace-opened', finalWorkspacePath, workspaceUid, configForClient);\n\n        if (workspaceWatcher) {\n          workspaceWatcher.addWatcher(mainWindow, finalWorkspacePath);\n        }\n\n        return {\n          success: true,\n          workspaceConfig: configForClient,\n          workspaceUid,\n          workspacePath: finalWorkspacePath\n        };\n      } catch (error) {\n        await fsExtra.remove(tempDir).catch(() => {});\n        throw error;\n      }\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:save-workspace-docs', async (event, workspacePath, docs) => {\n    try {\n      return await updateWorkspaceDocs(workspacePath, docs);\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:load-workspace-environments', async (event, workspacePath) => {\n    try {\n      const result = await globalEnvironmentsManager.getGlobalEnvironments(workspacePath);\n      return result.globalEnvironments;\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:create-workspace-environment', async (event, workspacePath, environmentName) => {\n    try {\n      return await globalEnvironmentsManager.createGlobalEnvironment(workspacePath, {\n        name: environmentName,\n        variables: []\n      });\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:delete-workspace-environment', async (event, workspacePath, environmentUid) => {\n    try {\n      return await globalEnvironmentsManager.deleteGlobalEnvironment(workspacePath, { environmentUid });\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:select-workspace-environment', async (event, workspacePath, environmentUid) => {\n    try {\n      return await globalEnvironmentsManager.selectGlobalEnvironment(workspacePath, { environmentUid });\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:import-workspace-environment', async (event, workspacePath, environmentData) => {\n    try {\n      return await globalEnvironmentsManager.createGlobalEnvironment(workspacePath, {\n        name: environmentData.name || 'Imported Environment',\n        variables: environmentData.variables || []\n      });\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:update-workspace-environment', async (event, workspacePath, environmentUid, environmentData) => {\n    try {\n      return await globalEnvironmentsManager.saveGlobalEnvironment(workspacePath, {\n        environmentUid,\n        variables: environmentData.variables || []\n      });\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:rename-workspace-environment', async (event, workspacePath, environmentUid, newName) => {\n    try {\n      return await globalEnvironmentsManager.renameGlobalEnvironment(workspacePath, {\n        environmentUid,\n        name: newName\n      });\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:copy-workspace-environment', async (event, workspacePath, environmentUid, newName) => {\n    try {\n      const result = await globalEnvironmentsManager.getGlobalEnvironments(workspacePath);\n      const sourceEnv = result.globalEnvironments.find((env) => env.uid === environmentUid);\n\n      if (!sourceEnv) {\n        throw new Error('Source environment not found');\n      }\n\n      // Create new environment with copied variables\n      return await globalEnvironmentsManager.createGlobalEnvironment(workspacePath, {\n        name: newName,\n        variables: sourceEnv.variables || []\n      });\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:add-collection-to-workspace', async (event, workspacePath, collection) => {\n    try {\n      const normalizedCollection = normalizeCollectionEntry(workspacePath, collection);\n      const updatedCollections = await addCollectionToWorkspace(workspacePath, normalizedCollection);\n\n      const workspaceConfig = readWorkspaceConfig(workspacePath);\n      const workspaceUid = getWorkspaceUid(workspacePath);\n      const isDefault = workspaceUid === 'default';\n      const configForClient = prepareWorkspaceConfigForClient(workspaceConfig, workspacePath, isDefault);\n      mainWindow.webContents.send('main:workspace-config-updated', workspacePath, workspaceUid, configForClient);\n\n      return updatedCollections;\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:ensure-collections-folder', async (event, workspacePath) => {\n    try {\n      const collectionsPath = path.join(workspacePath, 'collections');\n      if (!fs.existsSync(collectionsPath)) {\n        await createDirectory(collectionsPath);\n      }\n      return collectionsPath;\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:start-workspace-watcher', async (event, workspacePath) => {\n    try {\n      if (workspaceWatcher) {\n        workspaceWatcher.addWatcher(mainWindow, workspacePath);\n      }\n      return true;\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:remove-collection-from-workspace', async (event, workspaceUid, workspacePath, collectionPath, options = {}) => {\n    try {\n      const { deleteFiles = false } = options;\n      const result = await removeCollectionFromWorkspace(workspacePath, collectionPath);\n\n      if (deleteFiles && result.removedCollection && fs.existsSync(collectionPath)) {\n        await fsExtra.remove(collectionPath);\n      }\n\n      const correctWorkspaceUid = getWorkspaceUid(workspacePath);\n      const isDefault = correctWorkspaceUid === 'default';\n      const configForClient = prepareWorkspaceConfigForClient(result.updatedConfig, workspacePath, isDefault);\n      mainWindow.webContents.send('main:workspace-config-updated', workspacePath, correctWorkspaceUid, configForClient);\n\n      return true;\n    } catch (error) {\n      throw error;\n    }\n  });\n\n  ipcMain.handle('renderer:get-collection-workspaces', async (event, collectionPath) => {\n    try {\n      const workspacePaths = lastOpenedWorkspaces.getAll();\n      const workspacesWithCollection = [];\n\n      for (const workspacePath of workspacePaths) {\n        try {\n          const workspaceYmlPath = path.join(workspacePath, 'workspace.yml');\n          if (fs.existsSync(workspaceYmlPath)) {\n            const workspaceConfig = yaml.load(fs.readFileSync(workspaceYmlPath, 'utf8')) || {};\n            const collections = workspaceConfig.collections || [];\n\n            const hasCollection = collections.some((c) => {\n              const resolvedPath = path.isAbsolute(c.path)\n                ? c.path\n                : path.resolve(workspacePath, c.path);\n              return resolvedPath === collectionPath;\n            });\n\n            if (hasCollection) {\n              workspacesWithCollection.push(workspacePath);\n            }\n          }\n        } catch (error) {\n          console.warn('Failed to check workspace collection:', error.message);\n        }\n      }\n\n      return workspacesWithCollection;\n    } catch (error) {\n      return [];\n    }\n  });\n\n  ipcMain.handle('renderer:get-default-workspace', async (event) => {\n    try {\n      const result = await defaultWorkspaceManager.ensureDefaultWorkspaceExists();\n      if (!result) {\n        return null;\n      }\n\n      const { workspacePath, workspaceUid } = result;\n      const workspaceConfig = readWorkspaceConfig(workspacePath);\n      const configForClient = prepareWorkspaceConfigForClient(workspaceConfig, workspacePath, true);\n\n      return {\n        workspaceConfig: configForClient,\n        workspaceUid,\n        workspacePath\n      };\n    } catch (error) {\n      console.error('Error getting default workspace:', error);\n      return null;\n    }\n  });\n\n  // Guard to prevent main:renderer-ready from running multiple times (only needed in dev mode due to strict mode)\n  let rendererReadyProcessed = false;\n\n  ipcMain.on('main:renderer-ready', async (win) => {\n    if (isDev && rendererReadyProcessed) {\n      return;\n    }\n    rendererReadyProcessed = true;\n\n    try {\n      let defaultWorkspacePath = null;\n\n      const defaultResult = await defaultWorkspaceManager.ensureDefaultWorkspaceExists();\n      if (defaultResult) {\n        const { workspacePath, workspaceUid } = defaultResult;\n        defaultWorkspacePath = workspacePath;\n        const workspaceConfig = readWorkspaceConfig(workspacePath);\n        const configForClient = prepareWorkspaceConfigForClient(workspaceConfig, workspacePath, true);\n\n        win.webContents.send('main:workspace-opened', workspacePath, workspaceUid, configForClient);\n\n        if (workspaceWatcher) {\n          workspaceWatcher.addWatcher(win, workspacePath);\n        }\n      }\n\n      const workspacePaths = lastOpenedWorkspaces.getAll();\n      const invalidPaths = [];\n\n      for (const workspacePath of workspacePaths) {\n        if (defaultWorkspacePath && workspacePath === defaultWorkspacePath) {\n          continue;\n        }\n\n        const workspaceYmlPath = path.join(workspacePath, 'workspace.yml');\n\n        if (fs.existsSync(workspaceYmlPath)) {\n          try {\n            const workspaceConfig = readWorkspaceConfig(workspacePath);\n            validateWorkspaceConfig(workspaceConfig);\n            const workspaceUid = getWorkspaceUid(workspacePath);\n            const isDefault = workspaceUid === 'default';\n            const configForClient = prepareWorkspaceConfigForClient(workspaceConfig, workspacePath, isDefault);\n\n            win.webContents.send('main:workspace-opened', workspacePath, workspaceUid, configForClient);\n\n            if (workspaceWatcher) {\n              workspaceWatcher.addWatcher(win, workspacePath);\n            }\n          } catch (error) {\n            console.error(`Error loading workspace ${workspacePath}:`, error);\n            invalidPaths.push(workspacePath);\n          }\n        } else {\n          invalidPaths.push(workspacePath);\n        }\n      }\n\n      for (const invalidPath of invalidPaths) {\n        lastOpenedWorkspaces.remove(invalidPath);\n      }\n    } catch (error) {\n      console.error('Error initializing workspaces:', error);\n    }\n  });\n};\n\nmodule.exports = registerWorkspaceIpc;\n"
  },
  {
    "path": "packages/bruno-electron/src/preload.js",
    "content": "const { ipcRenderer, contextBridge, webUtils, shell } = require('electron');\n\ncontextBridge.exposeInMainWorld('isPlaywright', process.env.PLAYWRIGHT === 'true');\n\ncontextBridge.exposeInMainWorld('ipcRenderer', {\n  invoke: (channel, ...args) => ipcRenderer.invoke(channel, ...args),\n  send: (channel, ...args) => ipcRenderer.send(channel, ...args),\n  on: (channel, handler) => {\n    // Deliberately strip event as it includes `sender`\n    const subscription = (event, ...args) => {\n      // Ensure args is always an array to prevent undefined errors\n      const safeArgs = args && args.length ? args : [];\n      handler(...safeArgs);\n    };\n    ipcRenderer.on(channel, subscription);\n\n    return () => {\n      ipcRenderer.removeListener(channel, subscription);\n    };\n  },\n  getFilePath(file) {\n    const path = webUtils.getPathForFile(file);\n    return path;\n  },\n  openExternal: (url) => shell.openExternal(url)\n});\n"
  },
  {
    "path": "packages/bruno-electron/src/store/bruno-config.js",
    "content": "/**\n * This modules stores the configs loaded from bruno.json\n */\n\nconst config = {};\n\n// collectionUid is a hash based on the collection path\nconst getBrunoConfig = (collectionUid, collection) => {\n  if (collection?.draft?.brunoConfig) {\n    return collection.draft.brunoConfig;\n  }\n  return config[collectionUid] || {};\n};\n\nconst setBrunoConfig = (collectionUid, brunoConfig) => {\n  config[collectionUid] = brunoConfig;\n};\n\nmodule.exports = {\n  getBrunoConfig,\n  setBrunoConfig\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/store/collection-security.js",
    "content": "const _ = require('lodash');\nconst Store = require('electron-store');\n\nclass CollectionSecurityStore {\n  constructor() {\n    this.store = new Store({\n      name: 'collection-security',\n      clearInvalidConfig: true\n    });\n  }\n\n  setSecurityConfigForCollection(collectionPathname, securityConfig) {\n    const collections = this.store.get('collections') || [];\n    const collection = _.find(collections, (c) => c.path === collectionPathname);\n\n    if (!collection) {\n      collections.push({\n        path: collectionPathname,\n        securityConfig: {\n          jsSandboxMode: securityConfig.jsSandboxMode\n        }\n      });\n\n      this.store.set('collections', collections);\n      return;\n    }\n\n    collection.securityConfig = securityConfig || {};\n    this.store.set('collections', collections);\n  }\n\n  getSecurityConfigForCollection(collectionPathname) {\n    const collections = this.store.get('collections') || [];\n    const collection = _.find(collections, (c) => c.path === collectionPathname);\n    return collection?.securityConfig || {};\n  }\n}\n\nmodule.exports = CollectionSecurityStore;\n"
  },
  {
    "path": "packages/bruno-electron/src/store/cookies.js",
    "content": "const Store = require('electron-store');\nconst { cookies: cookiesModule } = require('@usebruno/requests');\nconst { cookieJar } = cookiesModule;\nconst { Cookie } = require('tough-cookie');\nconst { createCookieString } = cookiesModule;\nconst crypto = require('crypto');\nconst { encryptString, decryptString } = require('../utils/encryption');\n\nconst DEBOUNCE_MS = 5000; // Debounce duration (ms) for persisting cookie jar\nclass CookiesStore {\n  #saveTimerId = null;\n  #debounceStart = null; // Track first debounce time\n  #passkey = null;\n\n  constructor() {\n    this.store = new Store({\n      name: 'cookies',\n      clearInvalidConfig: true,\n      defaults: {\n        encryptedPasskey: null,\n        cookies: {}\n      }\n    });\n  }\n\n  #generatePasskey() {\n    // Generate 32 bytes (256 bits) of random data and convert to hex\n    return crypto.randomBytes(32).toString('hex');\n  }\n\n  initializeEncryption() {\n    try {\n      let encryptedPasskey = this.store.get('encryptedPasskey');\n      if (!encryptedPasskey) {\n        // Generate cryptographically secure random passkey\n        const passkey = this.#generatePasskey();\n        encryptedPasskey = encryptString(passkey);\n        if (!encryptedPasskey) {\n          console.warn('Failed to encrypt new passkey, falling back to unencrypted cookies');\n          this.#passkey = null;\n          return;\n        }\n        this.store.set('encryptedPasskey', encryptedPasskey);\n      }\n      this.#passkey = decryptString(encryptedPasskey);\n      if (!this.#passkey) {\n        console.warn('Failed to decrypt passkey, falling back to unencrypted cookies');\n      }\n    } catch (err) {\n      console.warn('Failed to initialize encryption, falling back to unencrypted cookies:', err);\n      this.#passkey = null;\n    }\n  }\n\n  getCookies() {\n    const cookieStore = this.store.get('cookies', {});\n    const decryptedCookies = [];\n\n    // Filter and decrypt cookies\n    Object.values(cookieStore).forEach((domainCookies) => {\n      if (!Array.isArray(domainCookies)) return;\n\n      domainCookies.forEach((cookie) => {\n        try {\n          // Create cookie with decrypted value\n          const decryptedCookie = {\n            ...cookie,\n            value: decryptString(cookie.value, this.#passkey)\n          };\n          decryptedCookies.push(decryptedCookie);\n        } catch (err) {\n          console.warn('Failed to process cookie:', cookie?.key, err);\n          // Still add the cookie but with empty value if processing fails\n          decryptedCookies.push({\n            ...cookie,\n            value: ''\n          });\n        }\n      });\n    });\n\n    return decryptedCookies;\n  }\n\n  setCookies(cookies) {\n    try {\n      // Organize cookies by domain\n      const cookiesByDomain = {};\n      cookies.cookies.forEach((cookie) => {\n        try {\n          if (!cookiesByDomain[cookie.domain]) {\n            cookiesByDomain[cookie.domain] = [];\n          }\n\n          cookiesByDomain[cookie.domain].push({\n            ...cookie,\n            value: encryptString(cookie.value, this.#passkey)\n          });\n        } catch (err) {\n          console.warn('Failed to process cookie for storage:', cookie?.key, err);\n          // Still store the cookie but with original value if encryption fails\n          if (!cookiesByDomain[cookie.domain]) {\n            cookiesByDomain[cookie.domain] = [];\n          }\n          cookiesByDomain[cookie.domain].push(cookie);\n        }\n      });\n\n      return this.store.set('cookies', cookiesByDomain);\n    } catch (err) {\n      console.warn('Failed to set cookies:', err);\n    }\n  }\n\n  // Initialize cookies from store into cookie jar\n  initializeCookies() {\n    if (this.#passkey === null) {\n      this.initializeEncryption();\n    }\n    try {\n      const storedCookies = this.getCookies();\n\n      if (Array.isArray(storedCookies) && storedCookies.length) {\n        storedCookies.forEach((cookie) => this.loadCookieIntoJar(cookie));\n      }\n    } catch (err) {\n      console.warn('Failed to initialize cookies:', err);\n    }\n  }\n\n  // Load a single cookie into the cookie jar\n  loadCookieIntoJar(rawCookie) {\n    try {\n      const cookie = Cookie.fromJSON(rawCookie);\n      if (!cookie) return;\n\n      // Re-assemble request URL for tough-cookie\n      const protocol = cookie.secure ? 'https' : 'http';\n      const domain = cookie.domain.startsWith('.') ? cookie.domain.slice(1) : cookie.domain;\n      const url = `${protocol}://${domain}${cookie.path || '/'}`;\n      const setCookieHeader = createCookieString(cookie);\n\n      cookieJar.setCookieSync(setCookieHeader, url, { ignoreError: true });\n    } catch (err) {\n      console.warn('Failed to load cookie:', rawCookie?.key, err?.message);\n    }\n  }\n\n  // Save current cookie jar state to store with debouncing\n  writeCookieJar() {\n    try {\n      const serialized = cookieJar.serializeSync();\n      this.setCookies(serialized);\n    } catch (err) {\n      console.warn('Failed to save cookie jar:', err);\n    } finally {\n      this.#debounceStart = null;\n    }\n  }\n\n  saveCookieJar(immediate = false) {\n    // Debounced write to avoid excessive disk I/O during rapid request bursts\n    if (immediate) {\n      if (this.#saveTimerId) {\n        clearTimeout(this.#saveTimerId);\n        this.#saveTimerId = null;\n      }\n      return this.writeCookieJar();\n    }\n\n    if (!this.#debounceStart) {\n      this.#debounceStart = Date.now();\n    }\n\n    if (this.#saveTimerId) {\n      clearTimeout(this.#saveTimerId);\n    }\n    this.#saveTimerId = setTimeout(() => {\n      this.writeCookieJar();\n      this.#saveTimerId = null;\n    }, DEBOUNCE_MS);\n  }\n}\n\n// Create singleton instance\nconst cookiesStore = new CookiesStore();\n\nmodule.exports = {\n  cookiesStore,\n  CookiesStore\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/store/default-workspace.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst { app } = require('electron');\nconst { generateUidBasedOnHash } = require('../utils/common');\nconst { writeFile, isValidCollectionDirectory } = require('../utils/filesystem');\nconst { getPreferences, savePreferences } = require('./preferences');\nconst { globalEnvironmentsStore } = require('./global-environments');\nconst {\n  generateYamlContent,\n  readWorkspaceConfig,\n  validateWorkspaceConfig,\n  isValidCollectionEntry\n} = require('../utils/workspace-config');\n\nconst OPENCOLLECTION_VERSION = '1.0.0';\nconst WORKSPACE_TYPE = 'workspace';\nconst DEFAULT_WORKSPACE_UID = 'default';\nconst MAX_WORKSPACE_CREATION_ATTEMPTS = 20;\nconst GLOBAL_ENV_BACKUP_FILE = 'global-environments-backup.json';\n\nclass DefaultWorkspaceManager {\n  constructor() {\n    this.defaultWorkspacePath = null;\n    this.initializationPromise = null;\n  }\n\n  /**\n   * Finds all existing default workspace directories sorted by number (latest first)\n   */\n  findExistingDefaultWorkspaces() {\n    const configDir = app.getPath('userData');\n    const baseWorkspacePath = path.join(configDir, 'default-workspace');\n    const workspaces = [];\n\n    // Check base path\n    if (fs.existsSync(baseWorkspacePath)) {\n      workspaces.push({ path: baseWorkspacePath, index: 0 });\n    }\n\n    // Check numbered paths\n    for (let i = 1; i < MAX_WORKSPACE_CREATION_ATTEMPTS; i++) {\n      const numberedPath = `${baseWorkspacePath}-${i}`;\n      if (fs.existsSync(numberedPath)) {\n        workspaces.push({ path: numberedPath, index: i });\n      }\n    }\n\n    // Sort by index descending (latest first)\n    return workspaces.sort((a, b) => b.index - a.index).map((w) => w.path);\n  }\n\n  /**\n   * Finds the latest valid default workspace from existing directories\n   */\n  findLatestValidWorkspace() {\n    const workspaces = this.findExistingDefaultWorkspaces();\n    for (const workspacePath of workspaces) {\n      if (this.isValidDefaultWorkspace(workspacePath)) {\n        return workspacePath;\n      }\n    }\n    return null;\n  }\n\n  /**\n   * Recovers collections and environments from an existing workspace directory\n   */\n  recoverDataFromWorkspace(workspacePath) {\n    const recovered = { collections: [], environments: [], activeEnvironmentUid: null };\n\n    try {\n      // Try to read workspace config for collections\n      const config = readWorkspaceConfig(workspacePath);\n      if (config.collections && Array.isArray(config.collections)) {\n        recovered.collections = config.collections.filter((c) => {\n          if (!isValidCollectionEntry(c)) return false;\n          const collectionPath = path.isAbsolute(c.path) ? c.path : path.resolve(workspacePath, c.path);\n          return isValidCollectionDirectory(collectionPath);\n        });\n      }\n      if (config.activeEnvironmentUid) {\n        recovered.activeEnvironmentUid = config.activeEnvironmentUid;\n      }\n    } catch (error) {\n      console.error('Failed to read workspace config during recovery:', error);\n    }\n\n    // Try to read environments from workspace environments directory\n    const envDir = path.join(workspacePath, 'environments');\n    if (fs.existsSync(envDir)) {\n      try {\n        const envFiles = fs.readdirSync(envDir).filter((f) => f.endsWith('.yml'));\n        for (const file of envFiles) {\n          const envPath = path.join(envDir, file);\n          recovered.environments.push({ path: envPath, name: path.basename(file, '.yml') });\n        }\n      } catch (error) {\n        console.error('Failed to read environments during recovery:', error);\n      }\n    }\n\n    return recovered;\n  }\n\n  /**\n   * Backs up global environments to filesystem\n   */\n  backupGlobalEnvironments() {\n    try {\n      const globalEnvironments = globalEnvironmentsStore.getGlobalEnvironments();\n      const activeUid = globalEnvironmentsStore.getActiveGlobalEnvironmentUid();\n\n      if (globalEnvironments && globalEnvironments.length > 0) {\n        const configDir = app.getPath('userData');\n        const backupPath = path.join(configDir, GLOBAL_ENV_BACKUP_FILE);\n        const backup = {\n          environments: globalEnvironments,\n          activeGlobalEnvironmentUid: activeUid,\n          backupDate: new Date().toISOString()\n        };\n        fs.writeFileSync(backupPath, JSON.stringify(backup, null, 2), 'utf8');\n      }\n    } catch (error) {\n      console.error('Failed to backup global environments:', error);\n    }\n  }\n\n  getDefaultWorkspacePath() {\n    if (this.defaultWorkspacePath) {\n      return this.defaultWorkspacePath;\n    }\n\n    const preferences = getPreferences();\n    this.defaultWorkspacePath = preferences?.general?.defaultWorkspacePath;\n    return this.defaultWorkspacePath;\n  }\n\n  getDefaultWorkspaceUid() {\n    return DEFAULT_WORKSPACE_UID;\n  }\n\n  async setDefaultWorkspacePath(workspacePath) {\n    const preferences = getPreferences();\n    if (!preferences.general) {\n      preferences.general = {};\n    }\n    preferences.general.defaultWorkspacePath = workspacePath;\n    try {\n      await savePreferences(preferences);\n    } catch (error) {\n      console.error('Failed to save preferences:', error);\n    }\n\n    this.defaultWorkspacePath = workspacePath;\n\n    return workspacePath;\n  }\n\n  isValidDefaultWorkspace(workspacePath) {\n    if (!workspacePath || !fs.existsSync(workspacePath)) {\n      return false;\n    }\n\n    const workspaceYmlPath = path.join(workspacePath, 'workspace.yml');\n    if (!fs.existsSync(workspaceYmlPath)) {\n      return false;\n    }\n\n    try {\n      const config = readWorkspaceConfig(workspacePath);\n      validateWorkspaceConfig(config);\n      return true;\n    } catch (error) {\n      return false;\n    }\n  }\n\n  async ensureDefaultWorkspaceExists() {\n    if (this.initializationPromise) {\n      return this.initializationPromise;\n    }\n\n    const existingPath = this.getDefaultWorkspacePath();\n\n    // Case 1: Valid workspace exists at stored path\n    if (this.isValidDefaultWorkspace(existingPath)) {\n      this.defaultWorkspacePath = existingPath;\n      return {\n        workspacePath: existingPath,\n        workspaceUid: this.getDefaultWorkspaceUid()\n      };\n    }\n\n    this.initializationPromise = (async () => {\n      try {\n        // Case 2: No path in preferences - check for existing default workspaces\n        if (!existingPath) {\n          const latestValid = this.findLatestValidWorkspace();\n          if (latestValid) {\n            await this.setDefaultWorkspacePath(latestValid);\n            return { workspacePath: latestValid, workspaceUid: this.getDefaultWorkspaceUid() };\n          }\n        }\n\n        // Case 3: Path exists but workspace is broken - try recovery\n        const hasExistingPath = existingPath && fs.existsSync(existingPath);\n        const recoverySource = hasExistingPath ? existingPath : this.findExistingDefaultWorkspaces()[0];\n        const recoveredData = recoverySource ? this.recoverDataFromWorkspace(recoverySource) : null;\n\n        const shouldMigrate = this.needsMigration();\n        const newWorkspacePath = await this.initializeDefaultWorkspace({\n          migrateFromPreferences: shouldMigrate,\n          recoveredData\n        });\n\n        return {\n          workspacePath: newWorkspacePath,\n          workspaceUid: this.getDefaultWorkspaceUid()\n        };\n      } catch (error) {\n        console.error('Failed to initialize default workspace:', error);\n        return null;\n      } finally {\n        this.initializationPromise = null;\n      }\n    })();\n\n    return this.initializationPromise;\n  }\n\n  async initializeDefaultWorkspace(options = {}) {\n    const { migrateFromPreferences = true, recoveredData = null } = options;\n\n    const configDir = app.getPath('userData');\n    const baseWorkspacePath = path.join(configDir, 'default-workspace');\n\n    let workspacePath = baseWorkspacePath;\n    let counter = 1;\n    while (fs.existsSync(workspacePath) && counter < MAX_WORKSPACE_CREATION_ATTEMPTS) {\n      workspacePath = `${baseWorkspacePath}-${counter}`;\n      counter++;\n    }\n\n    if (counter >= MAX_WORKSPACE_CREATION_ATTEMPTS) {\n      throw new Error('Unable to create default workspace: too many existing workspace directories');\n    }\n\n    fs.mkdirSync(workspacePath, { recursive: true });\n    fs.mkdirSync(path.join(workspacePath, 'collections'), { recursive: true });\n    fs.mkdirSync(path.join(workspacePath, 'environments'), { recursive: true });\n\n    const workspaceConfig = {\n      opencollection: OPENCOLLECTION_VERSION,\n      info: {\n        name: 'My Workspace',\n        type: WORKSPACE_TYPE\n      },\n      collections: [],\n      specs: [],\n      docs: ''\n    };\n\n    // Copy recovered environments to new workspace\n    if (recoveredData?.environments?.length > 0) {\n      const envDir = path.join(workspacePath, 'environments');\n      for (const env of recoveredData.environments) {\n        try {\n          const destPath = path.join(envDir, `${env.name}.yml`);\n          if (fs.existsSync(env.path)) {\n            fs.copyFileSync(env.path, destPath);\n          }\n        } catch (error) {\n          console.error('Failed to copy environment:', env.name, error);\n        }\n      }\n      if (recoveredData.activeEnvironmentUid) {\n        workspaceConfig.activeEnvironmentUid = recoveredData.activeEnvironmentUid;\n      }\n    }\n\n    // Apply recovered collections first (lower priority)\n    if (recoveredData?.collections?.length > 0) {\n      workspaceConfig.collections = recoveredData.collections;\n    }\n\n    if (migrateFromPreferences) {\n      await this.migrateFromPreferences(workspacePath, workspaceConfig);\n    }\n\n    const yamlContent = generateYamlContent(workspaceConfig);\n    await writeFile(path.join(workspacePath, 'workspace.yml'), yamlContent);\n\n    await this.setDefaultWorkspacePath(workspacePath);\n\n    return workspacePath;\n  }\n\n  async migrateFromPreferences(workspacePath, workspaceConfig) {\n    const Store = require('electron-store');\n    const preferencesStore = new Store({ name: 'preferences' });\n\n    try {\n      const lastOpenedCollections = preferencesStore.get('lastOpenedCollections', []);\n\n      if (lastOpenedCollections && lastOpenedCollections.length > 0) {\n        // Build set of existing paths from recovered collections\n        const existingPaths = new Set(\n          (workspaceConfig.collections || []).map((c) => {\n            const collPath = path.isAbsolute(c.path) ? c.path : path.resolve(workspacePath, c.path);\n            return path.normalize(collPath);\n          })\n        );\n\n        const collections = lastOpenedCollections\n          .map((collectionPath) => {\n            if (!collectionPath || typeof collectionPath !== 'string') {\n              return null;\n            }\n            const absolutePath = path.resolve(collectionPath);\n            const normalizedPath = path.normalize(absolutePath);\n\n            if (existingPaths.has(normalizedPath)) {\n              return null;\n            }\n            existingPaths.add(normalizedPath);\n\n            if (!isValidCollectionDirectory(absolutePath)) {\n              return null;\n            }\n\n            return { path: absolutePath, name: path.basename(absolutePath) };\n          })\n          .filter((collection) => isValidCollectionEntry(collection));\n\n        // Merge: preference collections come after recovered ones\n        workspaceConfig.collections = [...(workspaceConfig.collections || []), ...collections];\n      }\n\n      // Backup global environments before migrating\n      this.backupGlobalEnvironments();\n\n      const globalEnvironments = globalEnvironmentsStore.getGlobalEnvironments();\n      const activeGlobalEnvironmentUid = globalEnvironmentsStore.getActiveGlobalEnvironmentUid();\n\n      if (globalEnvironments && globalEnvironments.length > 0) {\n        const { stringifyEnvironment } = require('@usebruno/filestore');\n        const environmentsDir = path.join(workspacePath, 'environments');\n\n        // Get existing environment names to avoid overwriting recovered ones\n        let existingEnvNames = [];\n        if (fs.existsSync(environmentsDir)) {\n          try {\n            existingEnvNames = fs.readdirSync(environmentsDir)\n              .filter((f) => f.endsWith('.yml'))\n              .map((f) => f.replace('.yml', ''));\n          } catch (error) {\n            console.error('Failed to read environments directory:', error);\n          }\n        }\n        const existingEnvs = new Set(existingEnvNames);\n\n        for (const env of globalEnvironments) {\n          if (!env || !env.name || typeof env.name !== 'string') {\n            continue;\n          }\n\n          // Skip if environment already exists from recovery\n          if (existingEnvs.has(env.name)) {\n            continue;\n          }\n\n          const envFilePath = path.join(environmentsDir, `${env.name}.yml`);\n          const environment = { name: env.name, variables: env.variables || [] };\n          const content = stringifyEnvironment(environment, { format: 'yml' });\n          await writeFile(envFilePath, content);\n\n          if (env.uid === activeGlobalEnvironmentUid && !workspaceConfig.activeEnvironmentUid) {\n            workspaceConfig.activeEnvironmentUid = generateUidBasedOnHash(envFilePath);\n          }\n        }\n      }\n\n      const defaultWorkspaceDocs = preferencesStore.get('preferences.defaultWorkspaceDocs', '');\n      if (defaultWorkspaceDocs && !workspaceConfig.docs) {\n        workspaceConfig.docs = defaultWorkspaceDocs;\n      }\n    } catch (error) {\n      console.error('Failed to migrate from preferences:', error);\n    }\n  }\n\n  needsMigration() {\n    const workspacePath = this.getDefaultWorkspacePath();\n    // Only skip migration if workspace is valid, not just if it exists\n    if (workspacePath && this.isValidDefaultWorkspace(workspacePath)) {\n      return false;\n    }\n\n    const Store = require('electron-store');\n    const preferencesStore = new Store({ name: 'preferences' });\n    const lastOpenedCollections = preferencesStore.get('lastOpenedCollections', []);\n    const globalEnvironments = globalEnvironmentsStore.getGlobalEnvironments();\n\n    return lastOpenedCollections.length > 0 || globalEnvironments.length > 0;\n  }\n}\n\nconst defaultWorkspaceManager = new DefaultWorkspaceManager();\n\nmodule.exports = {\n  defaultWorkspaceManager,\n  DefaultWorkspaceManager\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/store/env-secrets.js",
    "content": "const _ = require('lodash');\nconst Store = require('electron-store');\nconst { encryptStringSafe } = require('../utils/encryption');\n\nconst posixifyPath = (p) => (p ? p.replace(/\\\\/g, '/') : p);\n\n/**\n * Sample secrets store file\n *\n * {\n *   \"collections\": [{\n *     \"path\": \"/Users/anoop/Code/acme-acpi-collection\",\n *     \"environments\" : [{\n *       \"name\": \"Local\",\n *       \"secrets\": [{\n *         \"name\": \"token\",\n *         \"value\": \"abracadabra\"\n *       }]\n *     }]\n *   }]\n * }\n */\n\nclass EnvironmentSecretsStore {\n  constructor() {\n    this.store = new Store({\n      name: 'secrets',\n      clearInvalidConfig: true\n    });\n  }\n\n  storeEnvSecrets(collectionPathname, environment) {\n    const normalizedPathname = posixifyPath(collectionPathname);\n    const envVars = [];\n    _.each(environment.variables, (v) => {\n      if (v.secret) {\n        envVars.push({\n          name: v.name,\n          value: encryptStringSafe(v.value).value\n        });\n      }\n    });\n\n    const collections = this.store.get('collections') || [];\n    const collection = _.find(collections, (c) => posixifyPath(c.path) === normalizedPathname);\n\n    // if collection doesn't exist, create it, add the environment and save\n    if (!collection) {\n      collections.push({\n        path: normalizedPathname,\n        environments: [\n          {\n            name: environment.name,\n            secrets: envVars\n          }\n        ]\n      });\n\n      this.store.set('collections', collections);\n      return;\n    }\n\n    collection.path = normalizedPathname;\n\n    // if collection exists, check if environment exists\n    // if environment doesn't exist, add the environment and save\n    collection.environments = collection.environments || [];\n    const env = _.find(collection.environments, (e) => e.name === environment.name);\n    if (!env) {\n      collection.environments.push({\n        name: environment.name,\n        secrets: envVars\n      });\n\n      this.store.set('collections', collections);\n      return;\n    }\n\n    // if environment exists, update the secrets and save\n    env.secrets = envVars;\n    this.store.set('collections', collections);\n  }\n\n  getEnvSecrets(collectionPathname, environment) {\n    const normalizedPathname = posixifyPath(collectionPathname);\n    const collections = this.store.get('collections') || [];\n    const collection = _.find(collections, (c) => posixifyPath(c.path) === normalizedPathname);\n    if (!collection) {\n      return [];\n    }\n\n    const env = _.find(collection.environments, (e) => e.name === environment.name);\n    if (!env) {\n      return [];\n    }\n\n    return env.secrets || [];\n  }\n\n  renameEnvironment(collectionPathname, oldName, newName) {\n    const normalizedPathname = posixifyPath(collectionPathname);\n    const collections = this.store.get('collections') || [];\n    const collection = _.find(collections, (c) => posixifyPath(c.path) === normalizedPathname);\n    if (!collection) {\n      return;\n    }\n\n    const env = _.find(collection.environments, (e) => e.name === oldName);\n    if (!env) {\n      return;\n    }\n\n    env.name = newName;\n    this.store.set('collections', collections);\n  }\n\n  deleteEnvironment(collectionPathname, environmentName) {\n    const normalizedPathname = posixifyPath(collectionPathname);\n    const collections = this.store.get('collections') || [];\n    const collection = _.find(collections, (c) => posixifyPath(c.path) === normalizedPathname);\n    if (!collection) {\n      return;\n    }\n\n    _.remove(collection.environments, (e) => e.name === environmentName);\n    this.store.set('collections', collections);\n  }\n}\n\nmodule.exports = EnvironmentSecretsStore;\n"
  },
  {
    "path": "packages/bruno-electron/src/store/global-environments.js",
    "content": "const _ = require('lodash');\nconst Store = require('electron-store');\nconst { encryptStringSafe, decryptStringSafe } = require('../utils/encryption');\nconst { environmentSchema } = require('@usebruno/schema');\n\nclass GlobalEnvironmentsStore {\n  constructor() {\n    this.store = new Store({\n      name: 'global-environments',\n      clearInvalidConfig: true\n    });\n  }\n\n  /**\n   * Validates and filters environments array, removing invalid entries\n   * @param {Array} environments - Array of environment objects to validate\n   * @returns {Array} - Array of valid environments\n   */\n  filterValidEnvironments(environments) {\n    if (!Array.isArray(environments)) {\n      return [];\n    }\n\n    return environments.filter((env) => {\n      try {\n        environmentSchema.validateSync(env);\n        return true;\n      } catch (error) {\n        console.error('Invalid environment:', env);\n        console.error(error);\n        return false;\n      }\n    });\n  }\n\n  encryptGlobalEnvironmentVariables({ globalEnvironments }) {\n    return globalEnvironments?.map((env) => {\n      const variables = env.variables?.map((v) => ({\n        ...v,\n        value: v?.secret ? encryptStringSafe(v.value).value : v?.value\n      })) || [];\n\n      return {\n        ...env,\n        variables\n      };\n    });\n  }\n\n  decryptGlobalEnvironmentVariables({ globalEnvironments }) {\n    return globalEnvironments?.map((env) => {\n      const variables = env.variables?.map((v) => ({\n        ...v,\n        value: v?.secret ? decryptStringSafe(v.value).value : v?.value\n      })) || [];\n\n      return {\n        ...env,\n        variables\n      };\n    });\n  }\n\n  getGlobalEnvironments() {\n    let globalEnvironments = this.store.get('environments', []);\n\n    // Previously, a bug caused environment variables to be saved without a type.\n    // Since that issue is now fixed, this code ensures that anyone who imported\n    // data before the fix will have the missing types added retroactively.\n    globalEnvironments?.forEach((env) => {\n      env?.variables?.forEach((v) => {\n        if (!v.type) {\n          v.type = 'text';\n        }\n      });\n    });\n\n    globalEnvironments = this.filterValidEnvironments(globalEnvironments);\n\n    globalEnvironments = this.decryptGlobalEnvironmentVariables({ globalEnvironments });\n\n    return globalEnvironments;\n  }\n\n  getActiveGlobalEnvironmentUid() {\n    return this.store.get('activeGlobalEnvironmentUid', null);\n  }\n\n  setGlobalEnvironments(globalEnvironments) {\n    globalEnvironments = this.filterValidEnvironments(globalEnvironments);\n\n    globalEnvironments = this.encryptGlobalEnvironmentVariables({ globalEnvironments });\n    return this.store.set('environments', globalEnvironments);\n  }\n\n  setActiveGlobalEnvironmentUid(uid) {\n    return this.store.set('activeGlobalEnvironmentUid', uid);\n  }\n\n  addGlobalEnvironment({ uid, name, variables = [], color }) {\n    let globalEnvironments = this.getGlobalEnvironments();\n    const existingEnvironment = globalEnvironments.find((env) => env?.name == name);\n    if (existingEnvironment) {\n      throw new Error('Environment with the same name already exists');\n    }\n    globalEnvironments.push({\n      uid,\n      name,\n      variables,\n      color\n    });\n    this.setGlobalEnvironments(globalEnvironments);\n  }\n\n  saveGlobalEnvironment({ environmentUid: globalEnvironmentUid, variables }) {\n    let globalEnvironments = this.getGlobalEnvironments();\n    const environment = globalEnvironments.find((env) => env?.uid == globalEnvironmentUid);\n    globalEnvironments = globalEnvironments.filter((env) => env?.uid !== globalEnvironmentUid);\n    if (environment) {\n      environment.variables = variables;\n    }\n    globalEnvironments.push(environment);\n    this.setGlobalEnvironments(globalEnvironments);\n  }\n\n  renameGlobalEnvironment({ environmentUid: globalEnvironmentUid, name }) {\n    let globalEnvironments = this.getGlobalEnvironments();\n    const environment = globalEnvironments.find((env) => env?.uid == globalEnvironmentUid);\n    globalEnvironments = globalEnvironments.filter((env) => env?.uid !== globalEnvironmentUid);\n    if (environment) {\n      environment.name = name;\n    }\n    globalEnvironments.push(environment);\n    this.setGlobalEnvironments(globalEnvironments);\n  }\n\n  copyGlobalEnvironment({ uid, name, variables }) {\n    let globalEnvironments = this.getGlobalEnvironments();\n    globalEnvironments.push({\n      uid,\n      name,\n      variables\n    });\n    this.setGlobalEnvironments(globalEnvironments);\n  }\n\n  selectGlobalEnvironment({ environmentUid: globalEnvironmentUid }) {\n    let globalEnvironments = this.getGlobalEnvironments();\n    const environment = globalEnvironments.find((env) => env?.uid == globalEnvironmentUid);\n    if (environment) {\n      this.setActiveGlobalEnvironmentUid(globalEnvironmentUid);\n    } else {\n      this.setActiveGlobalEnvironmentUid(null);\n    }\n  }\n\n  deleteGlobalEnvironment({ environmentUid }) {\n    let globalEnvironments = this.getGlobalEnvironments();\n    let activeGlobalEnvironmentUid = this.getActiveGlobalEnvironmentUid();\n    globalEnvironments = globalEnvironments.filter((env) => env?.uid !== environmentUid);\n    if (environmentUid == activeGlobalEnvironmentUid) {\n      this.setActiveGlobalEnvironmentUid(null);\n    }\n    this.setGlobalEnvironments(globalEnvironments);\n  }\n\n  updateGlobalEnvironmentColor({ environmentUid, color }) {\n    let globalEnvironments = this.getGlobalEnvironments();\n    const environment = globalEnvironments.find((env) => env?.uid == environmentUid);\n    if (environment) {\n      environment.color = color;\n    }\n    this.setGlobalEnvironments(globalEnvironments);\n  }\n}\n\nconst globalEnvironmentsStore = new GlobalEnvironmentsStore();\n\nmodule.exports = {\n  globalEnvironmentsStore\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/store/last-opened-collections.js",
    "content": "const path = require('node:path');\nconst _ = require('lodash');\nconst Store = require('electron-store');\nconst { isDirectory } = require('../utils/filesystem');\n\nclass LastOpenedCollections {\n  constructor() {\n    this.store = new Store({\n      name: 'preferences',\n      clearInvalidConfig: true\n    });\n    console.log(`Preferences file is located at: ${this.store.path}`);\n  }\n\n  getAll() {\n    let collections = this.store.get('lastOpenedCollections') || [];\n    collections = collections.map((collection) => path.resolve(collection));\n    return collections;\n  }\n\n  add(collectionPath) {\n    const collections = this.getAll();\n\n    if (isDirectory(collectionPath) && !collections.includes(collectionPath)) {\n      collections.push(collectionPath);\n      this.store.set('lastOpenedCollections', collections);\n    }\n  }\n\n  update(collectionPaths) {\n    this.store.set('lastOpenedCollections', collectionPaths);\n  }\n\n  remove(collectionPath) {\n    let collections = this.getAll();\n\n    if (collections.includes(collectionPath)) {\n      collections = _.filter(collections, (c) => c !== collectionPath);\n      this.store.set('lastOpenedCollections', collections);\n    }\n  }\n\n  removeAll() {\n    this.store.set('lastOpenedCollections', []);\n  }\n}\n\nmodule.exports = LastOpenedCollections;\n"
  },
  {
    "path": "packages/bruno-electron/src/store/last-opened-workspaces.js",
    "content": "const Store = require('electron-store');\n\nclass LastOpenedWorkspaces {\n  constructor() {\n    this.store = new Store({\n      name: 'preferences',\n      defaults: {}\n    });\n  }\n\n  getAll() {\n    return this.store.get('workspaces.lastOpenedWorkspaces', []);\n  }\n\n  add(workspacePath) {\n    const workspaces = this.getAll();\n\n    if (workspaces.includes(workspacePath)) {\n      return workspaces;\n    }\n\n    workspaces.unshift(workspacePath);\n    this.store.set('workspaces.lastOpenedWorkspaces', workspaces);\n    return workspaces;\n  }\n\n  remove(workspacePath) {\n    const workspaces = this.getAll();\n    const filteredWorkspaces = workspaces.filter((w) => w !== workspacePath);\n    this.store.set('workspaces.lastOpenedWorkspaces', filteredWorkspaces);\n    return filteredWorkspaces;\n  }\n}\n\nmodule.exports = LastOpenedWorkspaces;\n"
  },
  {
    "path": "packages/bruno-electron/src/store/oauth2.js",
    "content": "const _ = require('lodash');\nconst Store = require('electron-store');\nconst { uuid, safeStringifyJSON, safeParseJSON } = require('../utils/common');\nconst { encryptStringSafe, decryptStringSafe } = require('../utils/encryption');\n\n/**\n * Sample secrets store file\n *\n * {\n *   \"collections\": [{\n *     \"path\": \"/Users/anoop/Code/acme-acpi-collection\",\n *     \"environments\" : [{\n *       \"name\": \"Local\",\n *       \"secrets\": [{\n *         \"name\": \"token\",\n *         \"value\": \"abracadabra\"\n *       }]\n *     }]\n *   }]\n * }\n */\n\nclass Oauth2Store {\n  constructor() {\n    this.store = new Store({\n      name: 'oauth2',\n      clearInvalidConfig: true\n    });\n  }\n\n  // Get oauth2 data for all collections\n  getAllOauth2Data() {\n    let oauth2Data = this.store.get('collections');\n    if (!Array.isArray(oauth2Data)) oauth2Data = [];\n    return oauth2Data;\n  }\n\n  // Get oauth2 data for a collection\n  getOauth2DataOfCollection({ collectionUid, url }) {\n    let oauth2Data = this.getAllOauth2Data();\n    let oauth2DataForCollection = oauth2Data.find((d) => d?.collectionUid == collectionUid);\n\n    // If oauth2 data is not present for the collection, add it to the store\n    if (!oauth2DataForCollection) {\n      let newOauth2DataForCollection = {\n        collectionUid\n      };\n      let updatedOauth2Data = [...oauth2Data, newOauth2DataForCollection];\n      this.store.set('collections', updatedOauth2Data);\n\n      return newOauth2DataForCollection;\n    }\n\n    return oauth2DataForCollection;\n  }\n\n  // Update oauth2 data of a collection\n  updateOauth2DataOfCollection({ collectionUid, url, data }) {\n    let oauth2Data = this.getAllOauth2Data();\n\n    let updatedOauth2Data = oauth2Data.filter((d) => d.collectionUid !== collectionUid);\n    updatedOauth2Data.push({ ...data });\n\n    this.store.set('collections', updatedOauth2Data);\n  }\n\n  // Create a new oauth2 Session Id for a collection\n  createNewOauth2SessionIdForCollection({ collectionUid, url }) {\n    let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url });\n\n    let newSessionId = uuid();\n\n    let newOauth2DataForCollection = {\n      ...oauth2DataForCollection,\n      sessionId: newSessionId\n    };\n\n    this.updateOauth2DataOfCollection({ collectionUid, data: newOauth2DataForCollection });\n\n    return newOauth2DataForCollection;\n  }\n\n  // Get session id of a collection\n  getSessionIdOfCollection({ collectionUid, url }) {\n    try {\n      let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url });\n\n      if (oauth2DataForCollection?.sessionId && typeof oauth2DataForCollection.sessionId === 'string') {\n        return oauth2DataForCollection.sessionId;\n      }\n\n      let newOauth2DataForCollection = this.createNewOauth2SessionIdForCollection({ collectionUid, url });\n      return newOauth2DataForCollection?.sessionId;\n    } catch (err) {\n      console.log('error retrieving session id from cache', err);\n    }\n  }\n\n  // clear session id of a collection\n  clearSessionIdOfCollection({ collectionUid, url }) {\n    try {\n      let oauth2Data = this.getAllOauth2Data();\n\n      let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url });\n      delete oauth2DataForCollection.sessionId;\n      delete oauth2DataForCollection.credentials;\n\n      let updatedOauth2Data = oauth2Data.filter((d) => d.collectionUid !== collectionUid);\n      updatedOauth2Data.push({ ...oauth2DataForCollection });\n\n      this.store.set('collections', updatedOauth2Data);\n    } catch (err) {\n      console.log('error while clearing the oauth2 session cache', err);\n    }\n  }\n\n  getCredentialsForCollection({ collectionUid, url, credentialsId }) {\n    try {\n      let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url });\n      let credentials = oauth2DataForCollection?.credentials?.find((c) => (c?.url == url) && (c?.credentialsId == credentialsId));\n      if (!credentials?.data) return null;\n      const decryptionResult = decryptStringSafe(credentials?.data);\n      const decryptedCredentialsData = safeParseJSON(decryptionResult.value);\n      return decryptedCredentialsData;\n    } catch (err) {\n      console.log('error retrieving oauth2 credentials from cache', err);\n    }\n  }\n\n  updateCredentialsForCollection({ collectionUid, url, credentialsId, credentials = {} }) {\n    try {\n      const encryptionResult = encryptStringSafe(safeStringifyJSON(credentials));\n      const encryptedCredentialsData = encryptionResult.value;\n      let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url });\n      let filteredCredentials = oauth2DataForCollection?.credentials?.filter((c) => (c?.url !== url) || (c?.credentialsId !== credentialsId));\n      if (!filteredCredentials) filteredCredentials = [];\n      filteredCredentials.push({\n        url,\n        data: encryptedCredentialsData,\n        credentialsId\n      });\n      let newOauth2DataForCollection = {\n        ...oauth2DataForCollection,\n        credentials: filteredCredentials\n      };\n      this.updateOauth2DataOfCollection({ collectionUid, data: newOauth2DataForCollection });\n      return newOauth2DataForCollection;\n    } catch (err) {\n      console.log('error updating oauth2 credentials from cache', err);\n    }\n  }\n\n  clearCredentialsByCredentialsId({ collectionUid, credentialsId }) {\n    try {\n      let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid });\n      let filteredCredentials = oauth2DataForCollection?.credentials?.filter(\n        (c) => c?.credentialsId !== credentialsId\n      );\n      let newOauth2DataForCollection = {\n        ...oauth2DataForCollection,\n        credentials: filteredCredentials\n      };\n      this.updateOauth2DataOfCollection({ collectionUid, data: newOauth2DataForCollection });\n      return newOauth2DataForCollection;\n    } catch (err) {\n      console.log('error clearing oauth2 credentials by credentialsId from cache', err);\n    }\n  }\n\n  clearCredentialsForCollection({ collectionUid, url, credentialsId }) {\n    try {\n      let oauth2DataForCollection = this.getOauth2DataOfCollection({ collectionUid, url });\n      let filteredCredentials = oauth2DataForCollection?.credentials?.filter((c) => (c?.url !== url) || (c?.credentialsId !== credentialsId));\n      let newOauth2DataForCollection = {\n        ...oauth2DataForCollection,\n        credentials: filteredCredentials\n      };\n      this.updateOauth2DataOfCollection({ collectionUid, data: newOauth2DataForCollection });\n      return newOauth2DataForCollection;\n    } catch (err) {\n      console.log('error clearing oauth2 credentials from cache', err);\n    }\n  }\n}\n\nmodule.exports = Oauth2Store;\n"
  },
  {
    "path": "packages/bruno-electron/src/store/preferences.js",
    "content": "const Yup = require('yup');\nconst Store = require('electron-store');\nconst { get, merge } = require('lodash');\n\n/**\n * The preferences are stored in the electron store 'preferences.json'.\n * The electron process uses this module to get the preferences.\n *\n */\n\nconst defaultPreferences = {\n  request: {\n    sslVerification: true,\n    customCaCertificate: {\n      enabled: false,\n      filePath: null\n    },\n    keepDefaultCaCertificates: {\n      enabled: true\n    },\n    storeCookies: true,\n    sendCookies: true,\n    timeout: 0,\n    oauth2: {\n      useSystemBrowser: false\n    }\n  },\n  font: {\n    codeFont: 'default',\n    codeFontSize: 13\n  },\n  proxy: {\n    inherit: true,\n    config: {\n      protocol: 'http',\n      hostname: '',\n      port: null,\n      auth: {\n        username: '',\n        password: ''\n      },\n      bypassProxy: ''\n    }\n  },\n  layout: {\n    responsePaneOrientation: 'horizontal'\n  },\n  beta: {\n    'openapi-sync': false\n  },\n  onboarding: {\n    hasLaunchedBefore: false,\n    hasSeenWelcomeModal: true\n  },\n  general: {\n    defaultLocation: '',\n    defaultWorkspacePath: ''\n  },\n  autoSave: {\n    enabled: false,\n    interval: 1000\n  },\n  display: {\n    zoomPercentage: 100\n  },\n  cache: {\n    sslSession: {\n      enabled: false\n    }\n  }\n};\n\nconst preferencesSchema = Yup.object().shape({\n  request: Yup.object().shape({\n    sslVerification: Yup.boolean(),\n    customCaCertificate: Yup.object({\n      enabled: Yup.boolean(),\n      filePath: Yup.string().nullable()\n    }),\n    keepDefaultCaCertificates: Yup.object({\n      enabled: Yup.boolean()\n    }),\n    storeCookies: Yup.boolean(),\n    sendCookies: Yup.boolean(),\n    timeout: Yup.number(),\n    oauth2: Yup.object({\n      useSystemBrowser: Yup.boolean()\n    })\n  }),\n  font: Yup.object().shape({\n    codeFont: Yup.string().nullable(),\n    codeFontSize: Yup.number().min(1).max(32).nullable()\n  }),\n  proxy: Yup.object({\n    disabled: Yup.boolean().optional(),\n    inherit: Yup.boolean().required(),\n    config: Yup.object({\n      protocol: Yup.string().oneOf(['http', 'https', 'socks4', 'socks5']),\n      hostname: Yup.string().max(1024),\n      port: Yup.number().min(1).max(65535).nullable(),\n      auth: Yup.object({\n        disabled: Yup.boolean().optional(),\n        username: Yup.string().max(1024),\n        password: Yup.string().max(1024)\n      }).optional(),\n      bypassProxy: Yup.string().optional().max(1024)\n    }).required()\n  }),\n  layout: Yup.object({\n    responsePaneOrientation: Yup.string().oneOf(['horizontal', 'vertical'])\n  }),\n  beta: Yup.object({\n    'openapi-sync': Yup.boolean()\n  }),\n  onboarding: Yup.object({\n    hasLaunchedBefore: Yup.boolean(),\n    hasSeenWelcomeModal: Yup.boolean()\n  }),\n  general: Yup.object({\n    defaultLocation: Yup.string().max(1024).nullable(),\n    defaultWorkspacePath: Yup.string().max(1024).nullable()\n  }),\n  autoSave: Yup.object({\n    enabled: Yup.boolean(),\n    interval: Yup.number().min(100)\n  }),\n  display: Yup.object({\n    zoomPercentage: Yup.number().min(50).max(150)\n  }),\n  cache: Yup.object({\n    sslSession: Yup.object({\n      enabled: Yup.boolean()\n    })\n  }).optional()\n});\n\nclass PreferencesStore {\n  constructor() {\n    this.store = new Store({\n      name: 'preferences',\n      clearInvalidConfig: true\n    });\n  }\n\n  getPreferences() {\n    let preferences = this.store.get('preferences', {});\n\n    // Handle existing users without proxy settings\n    // They should get disabled proxy by default, not inherit from system\n    // New users (empty preferences) will get defaultPreferences.proxy via merge\n    if (Object.keys(preferences).length > 0 && !preferences.proxy) {\n      preferences.proxy = {\n        inherit: false,\n        disabled: true,\n        config: {\n          protocol: 'http',\n          hostname: '',\n          port: null,\n          auth: {\n            username: '',\n            password: ''\n          },\n          bypassProxy: ''\n        }\n      };\n    }\n\n    if (preferences?.proxy) {\n      const proxy = preferences.proxy || {};\n\n      // Check if this is an old format that needs migration\n      const hasOldFormat = proxy.hasOwnProperty('enabled') || proxy.hasOwnProperty('mode');\n\n      if (hasOldFormat) {\n        let newProxy = {\n          inherit: true,\n          config: {\n            protocol: proxy.protocol || 'http',\n            hostname: proxy.hostname || '',\n            port: proxy.port || null,\n            auth: {\n              username: get(proxy, 'auth.username', ''),\n              password: get(proxy, 'auth.password', '')\n            },\n            bypassProxy: proxy.bypassProxy || ''\n          }\n        };\n\n        // Handle old format 1: enabled (boolean)\n        if (proxy.hasOwnProperty('enabled') && typeof proxy.enabled === 'boolean') {\n          newProxy.disabled = !proxy.enabled;\n          newProxy.inherit = false;\n        } else if (proxy.hasOwnProperty('mode')) {\n          // Handle old format 2: mode ('off' | 'on' | 'system')\n          if (proxy.mode === 'off') {\n            newProxy.disabled = true;\n            newProxy.inherit = false;\n          } else if (proxy.mode === 'on') {\n            newProxy.disabled = false;\n            newProxy.inherit = false;\n          } else if (proxy.mode === 'system') {\n            newProxy.disabled = false;\n            newProxy.inherit = true;\n          }\n        }\n\n        // Migrate auth.enabled to auth.disabled\n        if (get(proxy, 'auth.enabled') === false) {\n          newProxy.config.auth.disabled = true;\n        }\n        // If auth.enabled is true or undefined, omit disabled (defaults to false)\n\n        // Omit disabled: false at top level (optional field)\n        if (newProxy.disabled === false) {\n          delete newProxy.disabled;\n        }\n        // Omit auth.disabled: false (optional field)\n        if (newProxy.config.auth.disabled === false) {\n          delete newProxy.config.auth.disabled;\n        }\n\n        preferences.proxy = newProxy;\n      }\n    }\n\n    // Migrate font size from 14px to 13px for existing users\n    // Only migrate once if codeFont is 'default' (or not set) and codeFontSize is 14\n    // This ensures the migration only happens once and doesn't override user's explicit choices\n    // If user explicitly sets it to 14px after migration, it won't be migrated again\n    const fontSizeMigrated = get(preferences, '_migrations.codeFontSize14to13', false);\n    if (!fontSizeMigrated) {\n      const codeFont = get(preferences, 'font.codeFont', 'default');\n      const codeFontSize = get(preferences, 'font.codeFontSize');\n\n      // Only migrate if it's the old default combination (codeFont is default and size is 14)\n      if (codeFont === 'default' && codeFontSize === 14) {\n        preferences.font.codeFontSize = 13;\n        // Mark migration as complete\n        if (!preferences._migrations) {\n          preferences._migrations = {};\n        }\n        preferences._migrations.codeFontSize14to13 = true;\n        // Save the migrated preferences back to the store\n        this.store.set('preferences', preferences);\n      }\n    }\n\n    // Migrate from defaultCollectionLocation to defaultLocation\n    if (preferences.general?.defaultCollectionLocation !== undefined\n      && preferences.general?.defaultLocation === undefined) {\n      preferences.general.defaultLocation = preferences.general.defaultCollectionLocation;\n      delete preferences.general.defaultCollectionLocation;\n      this.store.set('preferences', preferences);\n    }\n\n    return merge({}, defaultPreferences, preferences);\n  }\n\n  savePreferences(newPreferences) {\n    return this.store.set('preferences', newPreferences);\n  }\n}\n\nconst preferencesStore = new PreferencesStore();\n\nconst getPreferences = () => {\n  return preferencesStore.getPreferences();\n};\n\nconst savePreferences = async (newPreferences) => {\n  return new Promise((resolve, reject) => {\n    preferencesSchema\n      .validate(newPreferences, { abortEarly: true })\n      .then((validatedPreferences) => {\n        preferencesStore.savePreferences(validatedPreferences);\n        resolve();\n      })\n      .catch((error) => {\n        reject(error);\n      });\n  });\n};\n\nconst preferencesUtil = {\n  shouldVerifyTls: () => {\n    return get(getPreferences(), 'request.sslVerification', true);\n  },\n  shouldUseCustomCaCertificate: () => {\n    return get(getPreferences(), 'request.customCaCertificate.enabled', false);\n  },\n  shouldKeepDefaultCaCertificates: () => {\n    return get(getPreferences(), 'request.keepDefaultCaCertificates.enabled', true);\n  },\n  getCustomCaCertificateFilePath: () => {\n    return get(getPreferences(), 'request.customCaCertificate.filePath', null);\n  },\n  getRequestTimeout: () => {\n    return get(getPreferences(), 'request.timeout', 0);\n  },\n  getGlobalProxyConfig: () => {\n    return get(getPreferences(), 'proxy', defaultPreferences.proxy);\n  },\n  shouldStoreCookies: () => {\n    return get(getPreferences(), 'request.storeCookies', true);\n  },\n  shouldSendCookies: () => {\n    return get(getPreferences(), 'request.sendCookies', true);\n  },\n  shouldUseSystemBrowser: () => {\n    return get(getPreferences(), 'request.oauth2.useSystemBrowser', false);\n  },\n  getResponsePaneOrientation: () => {\n    return get(getPreferences(), 'layout.responsePaneOrientation', 'horizontal');\n  },\n  isBetaFeatureEnabled: (featureName) => {\n    return get(getPreferences(), `beta.${featureName}`, false);\n  },\n  getZoomPercentage: () => {\n    return get(getPreferences(), 'display.zoomPercentage', 100);\n  },\n  isSslSessionCachingEnabled: () => {\n    return get(getPreferences(), 'cache.sslSession.enabled', false);\n  },\n  hasLaunchedBefore: () => {\n    return get(getPreferences(), 'onboarding.hasLaunchedBefore', false);\n  },\n  markAsLaunched: async () => {\n    const preferences = getPreferences();\n    preferences.onboarding.hasLaunchedBefore = true;\n\n    try {\n      await savePreferences(preferences);\n    } catch (err) {\n      console.error('Failed to save preferences in markAsLaunched:', err);\n    }\n  }\n};\n\nmodule.exports = {\n  getPreferences,\n  savePreferences,\n  preferencesUtil\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/store/process-env.js",
    "content": "/**\n * This file stores all the process.env variables under collection and workspace scope\n *\n * process.env variables are sourced from 3 places:\n * 1. .env file in the workspace root\n * 2. .env file in the collection root\n * 3. process.env variables set in the OS\n *\n * Priority (highest to lowest): collection .env > workspace .env > OS process.env\n *\n * Multiple collections can be opened in the same electron app.\n * Each collection's .env file can have different values for the same process.env variable.\n */\n\nconst dotEnvVars = {};\nconst workspaceDotEnvVars = {};\nconst collectionWorkspaceMap = {};\n\n// collectionUid is a hash based on the collection path\nconst getProcessEnvVars = (collectionUid) => {\n  const workspacePath = collectionWorkspaceMap[collectionUid];\n  const workspaceEnvVars = workspacePath ? workspaceDotEnvVars[workspacePath] : {};\n\n  return {\n    ...process.env,\n    ...workspaceEnvVars,\n    ...dotEnvVars[collectionUid]\n  };\n};\n\nconst setDotEnvVars = (collectionUid, envVars) => {\n  dotEnvVars[collectionUid] = envVars;\n};\n\nconst clearDotEnvVars = (collectionUid) => {\n  delete dotEnvVars[collectionUid];\n};\n\nconst setWorkspaceDotEnvVars = (workspacePath, envVars) => {\n  workspaceDotEnvVars[workspacePath] = envVars;\n};\n\nconst clearWorkspaceDotEnvVars = (workspacePath) => {\n  delete workspaceDotEnvVars[workspacePath];\n};\n\nconst setCollectionWorkspace = (collectionUid, workspacePath) => {\n  collectionWorkspaceMap[collectionUid] = workspacePath;\n};\n\nconst clearCollectionWorkspace = (collectionUid) => {\n  delete collectionWorkspaceMap[collectionUid];\n};\n\nmodule.exports = {\n  getProcessEnvVars,\n  setDotEnvVars,\n  clearDotEnvVars,\n  setWorkspaceDotEnvVars,\n  clearWorkspaceDotEnvVars,\n  setCollectionWorkspace,\n  clearCollectionWorkspace\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/store/system-proxy.js",
    "content": "const { getSystemProxy } = require('@usebruno/requests');\n\nlet cachedSystemProxy;\nlet systemProxyPromise;\n\nconst loadSystemProxy = async () => {\n  try {\n    cachedSystemProxy = await getSystemProxy();\n  } catch (error) {\n    console.error('Failed to initialize system proxy:', error);\n    cachedSystemProxy = {\n      http_proxy: null,\n      https_proxy: null,\n      no_proxy: null,\n      source: 'error'\n    };\n  }\n  return cachedSystemProxy;\n};\n\nconst fetchSystemProxy = ({ refresh = false } = {}) => {\n  if (refresh || !systemProxyPromise) {\n    systemProxyPromise = loadSystemProxy();\n  }\n  return systemProxyPromise;\n};\n\nconst getCachedSystemProxy = async () => {\n  if (!systemProxyPromise) {\n    await fetchSystemProxy();\n  } else {\n    await systemProxyPromise;\n  }\n  return cachedSystemProxy;\n};\n\nmodule.exports = {\n  fetchSystemProxy,\n  getCachedSystemProxy\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/store/ui-state-snapshot.js",
    "content": "const Store = require('electron-store');\n\nclass UiStateSnapshotStore {\n  constructor() {\n    this.store = new Store({\n      name: 'ui-state-snapshot',\n      clearInvalidConfig: true\n    });\n  }\n\n  getCollections() {\n    return this.store.get('collections') || [];\n  }\n\n  saveCollections(collections) {\n    this.store.set('collections', collections);\n  }\n\n  getCollectionByPathname({ pathname }) {\n    let collections = this.getCollections();\n\n    let collection = collections.find((c) => c?.pathname === pathname);\n    if (!collection) {\n      collection = { pathname };\n      collections.push(collection);\n      this.saveCollections(collections);\n    }\n\n    return collection;\n  }\n\n  setCollectionByPathname({ collection }) {\n    let collections = this.getCollections();\n\n    collections = collections.filter((c) => c?.pathname !== collection.pathname);\n    collections.push({ ...collection });\n    this.saveCollections(collections);\n\n    return collection;\n  }\n\n  updateCollectionEnvironment({ collectionPath, environmentName }) {\n    const collection = this.getCollectionByPathname({ pathname: collectionPath });\n    collection.selectedEnvironment = environmentName;\n    this.setCollectionByPathname({ collection });\n  }\n\n  update({ type, data }) {\n    switch (type) {\n      case 'COLLECTION_ENVIRONMENT':\n        const { collectionPath, environmentName } = data;\n        this.updateCollectionEnvironment({ collectionPath, environmentName });\n        break;\n      default:\n        break;\n    }\n  }\n}\n\nmodule.exports = UiStateSnapshotStore;\n"
  },
  {
    "path": "packages/bruno-electron/src/store/window-state.js",
    "content": "const Store = require('electron-store');\n\nconst DEFAULT_WINDOW_WIDTH = 1280;\nconst DEFAULT_WINDOW_HEIGHT = 768;\n\nconst DEFAULT_MAXIMIZED = false;\n\nclass WindowStateStore {\n  constructor() {\n    this.store = new Store({\n      name: 'preferences',\n      clearInvalidConfig: true\n    });\n  }\n\n  getBounds() {\n    return (\n      this.store.get('window-bounds') || {\n        x: 0,\n        y: 0,\n        width: DEFAULT_WINDOW_WIDTH,\n        height: DEFAULT_WINDOW_HEIGHT\n      }\n    );\n  }\n\n  setBounds(bounds) {\n    this.store.set('window-bounds', bounds);\n  }\n\n  getMaximized() {\n    return this.store.get('maximized') || DEFAULT_MAXIMIZED;\n  }\n\n  setMaximized(isMaximized) {\n    this.store.set('maximized', isMaximized);\n  }\n}\n\nmodule.exports = WindowStateStore;\n"
  },
  {
    "path": "packages/bruno-electron/src/store/workspace-environments.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst _ = require('lodash');\nconst yaml = require('js-yaml');\nconst { parseEnvironment, stringifyEnvironment } = require('@usebruno/filestore');\nconst { writeFile, createDirectory } = require('../utils/filesystem');\nconst { generateUidBasedOnHash, uuid } = require('../utils/common');\nconst { decryptStringSafe } = require('../utils/encryption');\nconst EnvironmentSecretsStore = require('./env-secrets');\nconst {\n  readWorkspaceConfig,\n  generateYamlContent,\n  writeWorkspaceFileAtomic\n} = require('../utils/workspace-config');\nconst { withLock, getWorkspaceLockKey } = require('../utils/workspace-lock');\n\nconst environmentSecretsStore = new EnvironmentSecretsStore();\n\nconst ENV_FILE_EXTENSION = '.yml';\n\nclass GlobalEnvironmentsManager {\n  constructor() {}\n\n  envHasSecrets(environment) {\n    const secrets = _.filter(environment.variables, (v) => v.secret === true);\n    return secrets && secrets.length > 0;\n  }\n\n  getEnvironmentsDir(workspacePath) {\n    return path.join(workspacePath, 'environments');\n  }\n\n  getEnvironmentFilePath(workspacePath, environmentName) {\n    return path.join(this.getEnvironmentsDir(workspacePath), `${environmentName}${ENV_FILE_EXTENSION}`);\n  }\n\n  findEnvironmentFileByUid(workspacePath, environmentUid) {\n    const environmentsDir = this.getEnvironmentsDir(workspacePath);\n\n    if (!fs.existsSync(environmentsDir)) {\n      return null;\n    }\n\n    const files = fs.readdirSync(environmentsDir);\n\n    for (const file of files) {\n      if (file.endsWith(ENV_FILE_EXTENSION)) {\n        const filePath = path.join(environmentsDir, file);\n        const fileUid = generateUidBasedOnHash(filePath);\n        if (fileUid === environmentUid) {\n          return {\n            filePath,\n            fileName: file,\n            name: file.slice(0, -ENV_FILE_EXTENSION.length)\n          };\n        }\n      }\n    }\n\n    return null;\n  }\n\n  async parseEnvironmentFile(filePath, workspacePath) {\n    const content = fs.readFileSync(filePath, 'utf8');\n    const environment = await parseEnvironment(content, { format: 'yml' });\n\n    const fileName = path.basename(filePath);\n    environment.name = fileName.slice(0, -ENV_FILE_EXTENSION.length);\n    environment.uid = generateUidBasedOnHash(filePath);\n\n    _.each(environment.variables, (variable) => {\n      if (!variable.uid) {\n        variable.uid = uuid();\n      }\n    });\n\n    if (this.envHasSecrets(environment)) {\n      const envSecrets = environmentSecretsStore.getEnvSecrets(workspacePath, environment);\n      _.each(envSecrets, (secret) => {\n        const variable = _.find(environment.variables, (v) => v.name === secret.name);\n        if (variable && secret.value) {\n          const decryptionResult = decryptStringSafe(secret.value);\n          variable.value = decryptionResult.value;\n        }\n      });\n    }\n\n    return environment;\n  }\n\n  async getGlobalEnvironments(workspacePath) {\n    try {\n      if (!workspacePath) {\n        throw new Error('Workspace path is required');\n      }\n\n      const environmentsDir = this.getEnvironmentsDir(workspacePath);\n\n      if (!fs.existsSync(environmentsDir)) {\n        return {\n          globalEnvironments: [],\n          activeGlobalEnvironmentUid: null\n        };\n      }\n\n      const files = fs.readdirSync(environmentsDir);\n      const environments = [];\n\n      for (const file of files) {\n        if (file.endsWith(ENV_FILE_EXTENSION)) {\n          const filePath = path.join(environmentsDir, file);\n\n          try {\n            const environment = await this.parseEnvironmentFile(filePath, workspacePath);\n            environments.push(environment);\n          } catch (parseError) {\n            console.error(`Failed to parse environment file ${file}:`, parseError);\n          }\n        }\n      }\n\n      const activeGlobalEnvironmentUid = await this.getActiveGlobalEnvironmentUid(workspacePath);\n\n      return {\n        globalEnvironments: environments,\n        activeGlobalEnvironmentUid\n      };\n    } catch (error) {\n      throw error;\n    }\n  }\n\n  async getActiveGlobalEnvironmentUid(workspacePath) {\n    try {\n      if (!workspacePath) {\n        return null;\n      }\n\n      const workspaceFilePath = path.join(workspacePath, 'workspace.yml');\n\n      if (!fs.existsSync(workspaceFilePath)) {\n        return null;\n      }\n\n      const yamlContent = fs.readFileSync(workspaceFilePath, 'utf8');\n      const workspaceConfig = yaml.load(yamlContent);\n\n      return workspaceConfig.activeEnvironmentUid || null;\n    } catch (error) {\n      return null;\n    }\n  }\n\n  async setActiveGlobalEnvironmentUid(workspacePath, environmentUid) {\n    if (!workspacePath) {\n      throw new Error('Workspace path is required');\n    }\n\n    const workspaceFilePath = path.join(workspacePath, 'workspace.yml');\n\n    if (!fs.existsSync(workspaceFilePath)) {\n      throw new Error('Invalid workspace: workspace.yml not found');\n    }\n\n    return withLock(getWorkspaceLockKey(workspacePath), async () => {\n      const workspaceConfig = readWorkspaceConfig(workspacePath);\n      workspaceConfig.activeEnvironmentUid = environmentUid;\n      const yamlOutput = generateYamlContent(workspaceConfig);\n      await writeWorkspaceFileAtomic(workspacePath, yamlOutput);\n      return true;\n    });\n  }\n\n  async createGlobalEnvironment(workspacePath, { uid, name, variables, color }) {\n    try {\n      if (!workspacePath) {\n        throw new Error('Workspace path is required');\n      }\n\n      const environmentsDir = this.getEnvironmentsDir(workspacePath);\n\n      if (!fs.existsSync(environmentsDir)) {\n        await createDirectory(environmentsDir);\n      }\n\n      const environmentFilePath = this.getEnvironmentFilePath(workspacePath, name);\n\n      if (fs.existsSync(environmentFilePath)) {\n        throw new Error(`Environment \"${name}\" already exists`);\n      }\n\n      const environment = {\n        name: name,\n        variables: variables || [],\n        color\n      };\n\n      if (this.envHasSecrets(environment)) {\n        environmentSecretsStore.storeEnvSecrets(workspacePath, environment);\n      }\n\n      const content = await stringifyEnvironment(environment, { format: 'yml' });\n      await writeFile(environmentFilePath, content);\n\n      return {\n        uid: generateUidBasedOnHash(environmentFilePath),\n        name,\n        variables,\n        color\n      };\n    } catch (error) {\n      throw error;\n    }\n  }\n\n  async saveGlobalEnvironment(workspacePath, { environmentUid, variables }) {\n    try {\n      if (!workspacePath) {\n        throw new Error('Workspace path is required');\n      }\n\n      const envFile = this.findEnvironmentFileByUid(workspacePath, environmentUid);\n\n      if (!envFile) {\n        throw new Error(`Environment file not found for uid: ${environmentUid}`);\n      }\n\n      const environment = {\n        name: envFile.name,\n        variables: variables\n      };\n\n      if (this.envHasSecrets(environment)) {\n        environmentSecretsStore.storeEnvSecrets(workspacePath, environment);\n      }\n\n      const content = await stringifyEnvironment(environment, { format: 'yml' });\n      await writeFile(envFile.filePath, content);\n\n      return true;\n    } catch (error) {\n      throw error;\n    }\n  }\n\n  async renameGlobalEnvironment(workspacePath, { environmentUid, name: newName }) {\n    try {\n      if (!workspacePath) {\n        throw new Error('Workspace path is required');\n      }\n\n      const envFile = this.findEnvironmentFileByUid(workspacePath, environmentUid);\n\n      if (!envFile) {\n        throw new Error(`Environment file not found for uid: ${environmentUid}`);\n      }\n\n      const newFilePath = this.getEnvironmentFilePath(workspacePath, newName);\n\n      if (fs.existsSync(newFilePath) && newFilePath !== envFile.filePath) {\n        throw new Error(`Environment \"${newName}\" already exists`);\n      }\n\n      const environment = await this.parseEnvironmentFile(envFile.filePath, workspacePath);\n      const oldName = environment.name;\n      environment.name = newName;\n\n      const content = await stringifyEnvironment(environment, { format: 'yml' });\n      await writeFile(newFilePath, content);\n\n      if (this.envHasSecrets(environment)) {\n        const oldEnv = { name: oldName };\n        const secrets = environmentSecretsStore.getEnvSecrets(workspacePath, oldEnv);\n\n        if (secrets && secrets.length > 0) {\n          const newEnv = { name: newName, variables: environment.variables };\n          environmentSecretsStore.storeEnvSecrets(workspacePath, newEnv);\n        }\n      }\n\n      if (envFile.filePath !== newFilePath) {\n        fs.unlinkSync(envFile.filePath);\n      }\n\n      const newUid = generateUidBasedOnHash(newFilePath);\n      return { uid: newUid, name: newName };\n    } catch (error) {\n      throw error;\n    }\n  }\n\n  async deleteGlobalEnvironment(workspacePath, { environmentUid }) {\n    try {\n      if (!workspacePath) {\n        throw new Error('Workspace path is required');\n      }\n\n      const envFile = this.findEnvironmentFileByUid(workspacePath, environmentUid);\n\n      if (!envFile) {\n        throw new Error(`Environment file not found for uid: ${environmentUid}`);\n      }\n\n      fs.unlinkSync(envFile.filePath);\n\n      const activeGlobalEnvironmentUid = await this.getActiveGlobalEnvironmentUid(workspacePath);\n      if (activeGlobalEnvironmentUid === environmentUid) {\n        await this.setActiveGlobalEnvironmentUid(workspacePath, null);\n      }\n\n      return true;\n    } catch (error) {\n      throw error;\n    }\n  }\n\n  async selectGlobalEnvironment(workspacePath, { environmentUid }) {\n    try {\n      if (!workspacePath) {\n        throw new Error('Workspace path is required');\n      }\n\n      await this.setActiveGlobalEnvironmentUid(workspacePath, environmentUid);\n      return true;\n    } catch (error) {\n      throw error;\n    }\n  }\n\n  async updateGlobalEnvironmentColor(workspacePath, environmentUid, color) {\n    try {\n      if (!workspacePath) {\n        throw new Error('Workspace path is required');\n      }\n\n      const envFile = this.findEnvironmentFileByUid(workspacePath, environmentUid);\n\n      if (!envFile) {\n        throw new Error(`Environment file not found for uid: ${environmentUid}`);\n      }\n\n      const environment = await this.parseEnvironmentFile(envFile.filePath, workspacePath);\n      environment.color = color;\n\n      const content = stringifyEnvironment(environment, { format: 'yml' });\n      await writeFile(envFile.filePath, content);\n\n      return true;\n    } catch (error) {\n      throw error;\n    }\n  }\n\n  async getGlobalEnvironmentsByPath(workspacePath) {\n    return this.getGlobalEnvironments(workspacePath);\n  }\n\n  async addGlobalEnvironmentByPath(workspacePath, params) {\n    return this.createGlobalEnvironment(workspacePath, params);\n  }\n\n  async saveGlobalEnvironmentByPath(workspacePath, params) {\n    return this.saveGlobalEnvironment(workspacePath, params);\n  }\n\n  async renameGlobalEnvironmentByPath(workspacePath, params) {\n    return this.renameGlobalEnvironment(workspacePath, params);\n  }\n\n  async deleteGlobalEnvironmentByPath(workspacePath, params) {\n    return this.deleteGlobalEnvironment(workspacePath, params);\n  }\n\n  async selectGlobalEnvironmentByPath(workspacePath, params) {\n    return this.selectGlobalEnvironment(workspacePath, params);\n  }\n\n  async updateGlobalEnvironmentColorByPath(workspacePath, { environmentUid, color }) {\n    return this.updateGlobalEnvironmentColor(workspacePath, environmentUid, color);\n  }\n}\n\nconst globalEnvironmentsManager = new GlobalEnvironmentsManager();\n\nmodule.exports = {\n  globalEnvironmentsManager,\n  GlobalEnvironmentsManager,\n  ENV_FILE_EXTENSION\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/arch.js",
    "content": "const getIsRunningInRosetta = () => {\n  const isMac = process.platform === 'darwin';\n  const isArm64 = process.arch === 'arm64';\n\n  if (!isMac) return false;\n  if (isArm64) return false;\n\n  const os = require('os');\n  const isRunningOnSilicon = os.cpus().find((d) => d.model.includes('Apple'));\n  if (!isRunningOnSilicon) {\n    return false;\n  }\n  return true;\n};\n\nmodule.exports = {\n  getIsRunningInRosetta\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/cancel-token.js",
    "content": "const cancelTokens = {};\n\nconst saveCancelToken = (uid, abortController) => {\n  cancelTokens[uid] = abortController;\n};\n\nconst deleteCancelToken = (uid) => {\n  delete cancelTokens[uid];\n};\n\nmodule.exports = {\n  cancelTokens,\n  saveCancelToken,\n  deleteCancelToken\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/collection-import.js",
    "content": "const fs = require('node:fs');\nconst path = require('node:path');\nconst { ipcMain } = require('electron');\nconst { sanitizeName, createDirectory, writeFile, safeWriteFileSync, getCollectionStats } = require('./filesystem');\nconst { generateUidBasedOnHash, stringifyJson } = require('./common');\nconst { stringifyRequestViaWorker, stringifyCollection, stringifyEnvironment, stringifyFolder, DEFAULT_COLLECTION_FORMAT } = require('@usebruno/filestore');\n\n/**\n * Recursively find a unique folder name by appending incremental numbers\n */\nasync function findUniqueFolderName(baseName, collectionLocation, counter = 0) {\n  const folderName = counter === 0 ? baseName : `${baseName} - ${counter}`;\n  const collectionPath = path.join(collectionLocation, sanitizeName(folderName));\n\n  if (fs.existsSync(collectionPath)) {\n    return findUniqueFolderName(baseName, collectionLocation, counter + 1);\n  }\n\n  return folderName;\n}\n\n/**\n * Import a collection - shared logic used by both IPC handler and onboarding service\n * @param {Object} options - Optional settings\n * @param {boolean} options.skipOpenEvent - If true, don't send main:collection-opened event (caller will handle it)\n */\nasync function importCollection(collection, collectionLocation, mainWindow, uniqueFolderName = null, format = DEFAULT_COLLECTION_FORMAT, options = {}) {\n  // Use provided unique folder name or use collection name\n  let folderName = uniqueFolderName ? sanitizeName(uniqueFolderName) : sanitizeName(collection.name);\n  let collectionPath = path.join(collectionLocation, folderName);\n\n  if (fs.existsSync(collectionPath)) {\n    throw new Error(`collection: ${collectionPath} already exists`);\n  }\n\n  // Recursive function to parse the collection items and create files/folders\n  const parseCollectionItems = async (items = [], currentPath) => {\n    for (const item of items) {\n      if (['http-request', 'graphql-request', 'grpc-request'].includes(item.type)) {\n        let sanitizedFilename = sanitizeName(item.filename || `${item.name}.${format}`);\n        const content = await stringifyRequestViaWorker(item, { format });\n        const filePath = path.join(currentPath, sanitizedFilename);\n        safeWriteFileSync(filePath, content);\n      }\n      if (item.type === 'folder') {\n        let sanitizedFolderName = sanitizeName(item.filename || item.name);\n        const folderPath = path.join(currentPath, sanitizedFolderName);\n        fs.mkdirSync(folderPath);\n\n        if (item.root?.meta?.name) {\n          const folderFilePath = path.join(folderPath, `folder.${format}`);\n          item.root.meta.seq = item.seq;\n          const folderContent = await stringifyFolder(item.root, { format });\n          safeWriteFileSync(folderFilePath, folderContent);\n        }\n\n        if (item.items && item.items.length) {\n          await parseCollectionItems(item.items, folderPath);\n        }\n      }\n      // Handle items of type 'js'\n      if (item.type === 'js') {\n        let sanitizedFilename = sanitizeName(item.filename || `${item.name}.js`);\n        const filePath = path.join(currentPath, sanitizedFilename);\n        safeWriteFileSync(filePath, item.fileContent);\n      }\n    }\n  };\n\n  const parseEnvironments = async (environments = [], collectionPath) => {\n    const envDirPath = path.join(collectionPath, 'environments');\n    if (!fs.existsSync(envDirPath)) {\n      fs.mkdirSync(envDirPath);\n    }\n\n    for (const env of environments) {\n      const content = await stringifyEnvironment(env, { format });\n      let sanitizedEnvFilename = sanitizeName(`${env.name}.${format}`);\n      const filePath = path.join(envDirPath, sanitizedEnvFilename);\n      safeWriteFileSync(filePath, content);\n    }\n  };\n\n  const getBrunoJsonConfig = (collection) => {\n    let brunoConfig = collection.brunoConfig;\n\n    if (!brunoConfig) {\n      brunoConfig = {\n        version: '1',\n        name: collection.name,\n        type: 'collection',\n        ignore: ['node_modules', '.git']\n      };\n    }\n\n    return brunoConfig;\n  };\n\n  await createDirectory(collectionPath);\n\n  const uid = generateUidBasedOnHash(collectionPath);\n  let brunoConfig = getBrunoJsonConfig(collection);\n\n  if (format === 'yml') {\n    const collectionContent = await stringifyCollection(collection.root, brunoConfig, { format });\n    await writeFile(path.join(collectionPath, 'opencollection.yml'), collectionContent);\n  } else if (format === 'bru') {\n    const stringifiedBrunoConfig = await stringifyJson(brunoConfig);\n    await writeFile(path.join(collectionPath, 'bruno.json'), stringifiedBrunoConfig);\n\n    const collectionContent = await stringifyCollection(collection.root, brunoConfig, { format });\n    await writeFile(path.join(collectionPath, 'collection.bru'), collectionContent);\n  } else {\n    throw new Error(`Invalid format: ${format}`);\n  }\n\n  const { size, filesCount } = await getCollectionStats(collectionPath);\n  brunoConfig.size = size;\n  brunoConfig.filesCount = filesCount;\n\n  // Send collection-opened event unless caller wants to handle it themselves (e.g., during onboarding)\n  if (!options.skipOpenEvent) {\n    mainWindow.webContents.send('main:collection-opened', collectionPath, uid, brunoConfig);\n    ipcMain.emit('main:collection-opened', mainWindow, collectionPath, uid, brunoConfig);\n  }\n\n  // create folder and files based on collection\n  await parseCollectionItems(collection.items, collectionPath);\n  await parseEnvironments(collection.environments, collectionPath);\n\n  return { collectionPath, uid, brunoConfig };\n}\n\nmodule.exports = {\n  importCollection,\n  findUniqueFolderName\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/collection.js",
    "content": "const { get, each, find, compact, isString, filter } = require('lodash');\nconst fs = require('fs');\nconst { getRequestUid, getExampleUid } = require('../cache/requestUids');\nconst { uuid } = require('./common');\nconst os = require('os');\nconst { preferencesUtil } = require('../store/preferences');\nconst path = require('path');\nconst { DEFAULT_COLLECTION_FORMAT } = require('@usebruno/filestore');\n\nconst mergeHeaders = (collection, request, requestTreePath) => {\n  let headers = new Map();\n\n  let collectionHeaders = collection?.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []);\n  collectionHeaders.forEach((header) => {\n    if (header.enabled) {\n      if (header?.name?.toLowerCase?.() === 'content-type') {\n        headers.set('content-type', header.value);\n      } else {\n        headers.set(header.name, header.value);\n      }\n    }\n  });\n\n  for (let i of requestTreePath) {\n    if (i.type === 'folder') {\n      const folderRoot = i?.draft || i?.root;\n      let _headers = get(folderRoot, 'request.headers', []);\n      _headers.forEach((header) => {\n        if (header.enabled) {\n          if (header.name.toLowerCase() === 'content-type') {\n            headers.set('content-type', header.value);\n          } else {\n            headers.set(header.name, header.value);\n          }\n        }\n      });\n    } else {\n      const _headers = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'request.headers', []);\n      _headers.forEach((header) => {\n        if (header.enabled) {\n          if (header.name.toLowerCase() === 'content-type') {\n            headers.set('content-type', header.value);\n          } else {\n            headers.set(header.name, header.value);\n          }\n        }\n      });\n    }\n  }\n\n  request.headers = Array.from(headers, ([name, value]) => ({ name, value, enabled: true }));\n};\n\nconst mergeVars = (collection, request, requestTreePath = []) => {\n  let reqVars = new Map();\n  const collectionRoot = collection?.draft?.root || collection?.root || {};\n  let collectionRequestVars = get(collectionRoot, 'request.vars.req', []);\n  let collectionVariables = {};\n  collectionRequestVars.forEach((_var) => {\n    if (_var.enabled) {\n      reqVars.set(_var.name, _var.value);\n      collectionVariables[_var.name] = _var.value;\n    }\n  });\n  let folderVariables = {};\n  let requestVariables = {};\n  for (let i of requestTreePath) {\n    if (i.type === 'folder') {\n      const folderRoot = i?.draft || i?.root;\n      let vars = get(folderRoot, 'request.vars.req', []);\n      vars.forEach((_var) => {\n        if (_var.enabled) {\n          reqVars.set(_var.name, _var.value);\n          folderVariables[_var.name] = _var.value;\n        }\n      });\n    } else {\n      const vars = i?.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []);\n      vars.forEach((_var) => {\n        if (_var.enabled) {\n          reqVars.set(_var.name, _var.value);\n          requestVariables[_var.name] = _var.value;\n        }\n      });\n    }\n  }\n\n  request.collectionVariables = collectionVariables;\n  request.folderVariables = folderVariables;\n  request.requestVariables = requestVariables;\n\n  if (request?.vars) {\n    request.vars.req = Array.from(reqVars, ([name, value]) => ({\n      name,\n      value,\n      enabled: true,\n      type: 'request'\n    }));\n  }\n\n  let resVars = new Map();\n  let collectionResponseVars = get(collectionRoot, 'request.vars.res', []);\n  collectionResponseVars.forEach((_var) => {\n    if (_var.enabled) {\n      resVars.set(_var.name, _var.value);\n    }\n  });\n  for (let i of requestTreePath) {\n    if (i.type === 'folder') {\n      const folderRoot = i?.draft || i?.root;\n      let vars = get(folderRoot, 'request.vars.res', []);\n      vars.forEach((_var) => {\n        if (_var.enabled) {\n          resVars.set(_var.name, _var.value);\n        }\n      });\n    } else {\n      const vars = i?.draft ? get(i, 'draft.request.vars.res', []) : get(i, 'request.vars.res', []);\n      vars.forEach((_var) => {\n        if (_var.enabled) {\n          resVars.set(_var.name, _var.value);\n        }\n      });\n    }\n  }\n\n  if (request?.vars) {\n    request.vars.res = Array.from(resVars, ([name, value]) => ({\n      name,\n      value,\n      enabled: true,\n      type: 'response'\n    }));\n  }\n};\n\n/**\n * Wraps a script in an IIFE closure to isolate its scope\n * @param {string} script - The script code to wrap\n * @returns {string} The wrapped script\n */\nconst wrapScriptInClosure = (script) => {\n  if (!script || script.trim() === '') {\n    return '';\n  }\n\n  // Wrap script in async IIFE to create isolated scope\n  // This prevents variable re-declaration errors and allows early returns\n  // to only affect the current script segment\n  return `await (async () => {\n${script}\n})();`;\n};\n\nconst mergeScripts = (collection, request, requestTreePath, scriptFlow) => {\n  const collectionRoot = collection?.draft?.root || collection?.root || {};\n  let collectionPreReqScript = get(collectionRoot, 'request.script.req', '');\n  let collectionPostResScript = get(collectionRoot, 'request.script.res', '');\n  let collectionTests = get(collectionRoot, 'request.tests', '');\n\n  let combinedPreReqScript = [];\n  let combinedPostResScript = [];\n  let combinedTests = [];\n  for (let i of requestTreePath) {\n    if (i.type === 'folder') {\n      const folderRoot = i?.draft || i?.root;\n      let preReqScript = get(folderRoot, 'request.script.req', '');\n      if (preReqScript && preReqScript.trim() !== '') {\n        combinedPreReqScript.push(preReqScript);\n      }\n\n      let postResScript = get(folderRoot, 'request.script.res', '');\n      if (postResScript && postResScript.trim() !== '') {\n        combinedPostResScript.push(postResScript);\n      }\n\n      let tests = get(folderRoot, 'request.tests', '');\n      if (tests && tests?.trim?.() !== '') {\n        combinedTests.push(tests);\n      }\n    }\n  }\n\n  // Wrap each script segment in its own closure and join them\n  // This allows each script to run separately with its own scope,\n  // preventing variable re-declaration errors and allowing early returns\n  // to only affect that specific script segment\n  const preReqScripts = [\n    collectionPreReqScript,\n    ...combinedPreReqScript,\n    request?.script?.req || ''\n  ];\n  request.script.req = compact(preReqScripts.map(wrapScriptInClosure)).join(os.EOL + os.EOL);\n\n  // Handle post-response scripts based on scriptFlow\n  if (scriptFlow === 'sequential') {\n    const postResScripts = [\n      collectionPostResScript,\n      ...combinedPostResScript,\n      request?.script?.res || ''\n    ];\n    request.script.res = compact(postResScripts.map(wrapScriptInClosure)).join(os.EOL + os.EOL);\n  } else {\n    // Reverse order for non-sequential flow\n    const postResScripts = [\n      request?.script?.res || '',\n      ...[...combinedPostResScript].reverse(),\n      collectionPostResScript\n    ];\n    request.script.res = compact(postResScripts.map(wrapScriptInClosure)).join(os.EOL + os.EOL);\n  }\n\n  // Handle tests based on scriptFlow\n  if (scriptFlow === 'sequential') {\n    const testScripts = [\n      collectionTests,\n      ...combinedTests,\n      request?.tests || ''\n    ];\n    request.tests = compact(testScripts.map(wrapScriptInClosure)).join(os.EOL + os.EOL);\n  } else {\n    // Reverse order for non-sequential flow\n    const testScripts = [\n      request?.tests || '',\n      ...[...combinedTests].reverse(),\n      collectionTests\n    ];\n    request.tests = compact(testScripts.map(wrapScriptInClosure)).join(os.EOL + os.EOL);\n  }\n};\n\nconst flattenItems = (items = []) => {\n  const flattenedItems = [];\n\n  const flatten = (itms, flattened) => {\n    each(itms, (i) => {\n      flattened.push(i);\n\n      if (i.items && i.items.length) {\n        flatten(i.items, flattened);\n      }\n    });\n  };\n\n  flatten(items, flattenedItems);\n\n  return flattenedItems;\n};\n\nconst findItem = (items = [], itemUid) => {\n  return find(items, (i) => i.uid === itemUid);\n};\n\nconst findItemInCollection = (collection, itemUid) => {\n  let flattenedItems = flattenItems(collection.items);\n\n  return findItem(flattenedItems, itemUid);\n};\n\nconst findParentItemInCollection = (collection, itemUid) => {\n  let flattenedItems = flattenItems(collection.items);\n\n  return find(flattenedItems, (item) => {\n    return item.items && find(item.items, (i) => i.uid === itemUid);\n  });\n};\n\nconst findParentItemInCollectionByPathname = (collection, pathname) => {\n  let flattenedItems = flattenItems(collection.items);\n\n  return find(flattenedItems, (item) => {\n    return item.items && find(item.items, (i) => i.pathname === pathname);\n  });\n};\n\nconst getTreePathFromCollectionToItem = (collection, _item) => {\n  let path = [];\n  let item = findItemInCollection(collection, _item.uid);\n  while (item) {\n    path.unshift(item);\n    item = findParentItemInCollection(collection, item.uid);\n  }\n  return path;\n};\n\nconst parseBruFileMeta = (data) => {\n  try {\n    const metaRegex = /meta\\s*{\\s*([\\s\\S]*?)\\s*}/;\n    const match = data?.match?.(metaRegex);\n    if (match) {\n      const metaContent = match[1].trim();\n      const lines = metaContent.replace(/\\r\\n/g, '\\n').split('\\n');\n      const metaJson = {};\n      lines.forEach((line) => {\n        const [key, value] = line.split(':').map((str) => str.trim());\n        if (key && value) {\n          metaJson[key] = isNaN(value) ? value : Number(value);\n        }\n      });\n\n      // Transform to the format expected by bruno-app\n      let requestType = metaJson.type;\n      if (requestType === 'http') {\n        requestType = 'http-request';\n      } else if (requestType === 'graphql') {\n        requestType = 'graphql-request';\n      } else {\n        requestType = 'http-request';\n      }\n\n      const sequence = metaJson.seq;\n      const transformedJson = {\n        type: requestType,\n        name: metaJson.name,\n        seq: !isNaN(sequence) ? Number(sequence) : 1,\n        settings: {},\n        tags: metaJson.tags || [],\n        request: {\n          method: '',\n          url: '',\n          params: [],\n          headers: [],\n          auth: { mode: 'none' },\n          body: { mode: 'none' },\n          script: {},\n          vars: {},\n          assertions: [],\n          tests: '',\n          docs: ''\n        }\n      };\n\n      return transformedJson;\n    } else {\n      console.log('No \"meta\" block found in the file.');\n      return null;\n    }\n  } catch (err) {\n    console.error('Error reading file:', err);\n    return null;\n  }\n};\n\n// Parse YML file meta information\nconst parseYmlFileMeta = (data) => {\n  try {\n    const yaml = require('js-yaml');\n    const parsed = yaml.load(data);\n\n    if (!parsed || !parsed.meta) {\n      console.log('No \"meta\" section found in YAML file.');\n      return null;\n    }\n\n    const metaJson = parsed.meta;\n\n    // Transform to the format expected by bruno-app\n    let requestType = metaJson.type;\n    const typeMap = {\n      http: 'http-request',\n      graphql: 'graphql-request',\n      grpc: 'grpc-request',\n      ws: 'ws-request'\n    };\n    requestType = typeMap[requestType] || 'http-request';\n\n    const sequence = metaJson.seq;\n    const transformedJson = {\n      type: requestType,\n      name: metaJson.name,\n      seq: !isNaN(sequence) ? Number(sequence) : 1,\n      settings: {},\n      tags: metaJson.tags || [],\n      request: {\n        method: '',\n        url: '',\n        params: [],\n        headers: [],\n        auth: { mode: 'none' },\n        body: { mode: 'none' },\n        script: {},\n        vars: {},\n        assertions: [],\n        tests: '',\n        docs: ''\n      }\n    };\n\n    return transformedJson;\n  } catch (err) {\n    console.error('Error parsing YAML file meta:', err);\n    return null;\n  }\n};\n\n// Format-aware meta parsing function\nconst parseFileMeta = (data, format = DEFAULT_COLLECTION_FORMAT) => {\n  if (format === 'yml') {\n    return parseYmlFileMeta(data);\n  } else {\n    return parseBruFileMeta(data);\n  }\n};\n\nconst hydrateRequestWithUuid = (request, pathname) => {\n  request.uid = getRequestUid(pathname);\n  const prefix = path.join(os.tmpdir(), 'bruno-');\n  request.isTransient = pathname.startsWith(prefix);\n\n  const params = get(request, 'request.params', []);\n  const headers = get(request, 'request.headers', []);\n  const requestVars = get(request, 'request.vars.req', []);\n  const responseVars = get(request, 'request.vars.res', []);\n  const assertions = get(request, 'request.assertions', []);\n  const bodyFormUrlEncoded = get(request, 'request.body.formUrlEncoded', []);\n  const bodyMultipartForm = get(request, 'request.body.multipartForm', []);\n  const file = get(request, 'request.body.file', []);\n  const examples = get(request, 'examples', []);\n\n  params.forEach((param) => (param.uid = uuid()));\n  headers.forEach((header) => (header.uid = uuid()));\n  requestVars.forEach((variable) => (variable.uid = uuid()));\n  responseVars.forEach((variable) => (variable.uid = uuid()));\n  assertions.forEach((assertion) => (assertion.uid = uuid()));\n  bodyFormUrlEncoded.forEach((param) => (param.uid = uuid()));\n  bodyMultipartForm.forEach((param) => (param.uid = uuid()));\n  file.forEach((param) => (param.uid = uuid()));\n  examples.forEach((example, eIndex) => {\n    example.uid = getExampleUid(pathname, eIndex);\n    example.itemUid = request.uid;\n    const params = get(example, 'request.params', []);\n    const headers = get(example, 'request.headers', []);\n    const responseHeaders = get(example, 'response.headers', []);\n    const bodyMultipartForm = get(example, 'request.body.multipartForm', []);\n    const bodyFormUrlEncoded = get(example, 'request.body.formUrlEncoded', []);\n    const file = get(example, 'request.body.file', []);\n    params.forEach((param) => (param.uid = uuid()));\n    headers.forEach((header) => (header.uid = uuid()));\n    responseHeaders.forEach((header) => (header.uid = uuid()));\n    bodyMultipartForm.forEach((param) => (param.uid = uuid()));\n    bodyFormUrlEncoded.forEach((param) => (param.uid = uuid()));\n    file.forEach((param) => (param.uid = uuid()));\n  });\n\n  return request;\n};\n\nconst findItemByPathname = (items = [], pathname) => {\n  return find(items, (i) => i.pathname === pathname);\n};\n\nconst findItemInCollectionByPathname = (collection, pathname) => {\n  let flattenedItems = flattenItems(collection.items);\n\n  return findItemByPathname(flattenedItems, pathname);\n};\n\nconst replaceTabsWithSpaces = (str, numSpaces = 2) => {\n  if (!str || !str.length || !isString(str)) {\n    return '';\n  }\n\n  return str.replaceAll('\\t', ' '.repeat(numSpaces));\n};\n\nconst transformRequestToSaveToFilesystem = (item) => {\n  const _item = item.draft ? item.draft : item;\n  const itemToSave = {\n    uid: _item.uid,\n    type: _item.type,\n    name: _item.name,\n    seq: _item.seq,\n    settings: _item.settings,\n    tags: _item.tags,\n    examples: _item.examples || [],\n    request: {\n      method: _item.request.method,\n      url: _item.request.url,\n      params: [],\n      headers: [],\n      auth: _item.request.auth,\n      body: _item.request.body,\n      script: _item.request.script,\n      vars: _item.request.vars,\n      assertions: _item.request.assertions,\n      tests: _item.request.tests,\n      docs: _item.request.docs\n    }\n  };\n\n  if (_item.type === 'grpc-request') {\n    itemToSave.request.methodType = _item.request.methodType;\n    itemToSave.request.protoPath = _item.request.protoPath;\n    delete itemToSave.request.params;\n  }\n\n  // Only process params for non-gRPC requests\n  if (_item.type !== 'grpc-request') {\n    each(_item.request.params, (param) => {\n      itemToSave.request.params.push({\n        uid: param.uid,\n        name: param.name,\n        value: param.value,\n        description: param.description,\n        type: param.type,\n        enabled: param.enabled\n      });\n    });\n  }\n\n  each(_item.request.headers, (header) => {\n    itemToSave.request.headers.push({\n      uid: header.uid,\n      name: header.name,\n      value: header.value,\n      description: header.description,\n      enabled: header.enabled\n    });\n  });\n\n  if (itemToSave.request.body.mode === 'json') {\n    itemToSave.request.body = {\n      ...itemToSave.request.body,\n      json: replaceTabsWithSpaces(itemToSave.request.body.json)\n    };\n  }\n\n  if (itemToSave.request.body.mode === 'grpc') {\n    itemToSave.request.body = {\n      ...itemToSave.request.body,\n      grpc: itemToSave.request.body.grpc.map(({ name, content }, index) => ({\n        name: name ? name : `message ${index + 1}`,\n        content: replaceTabsWithSpaces(content)\n      }))\n    };\n  }\n\n  return itemToSave;\n};\n\nconst sortCollection = (collection) => {\n  const items = collection.items || [];\n  let folderItems = filter(items, (item) => item.type === 'folder');\n  let requestItems = filter(items, (item) => item.type !== 'folder');\n\n  folderItems = sortByNameThenSequence(folderItems);\n  requestItems = requestItems.sort((a, b) => a.seq - b.seq);\n\n  collection.items = folderItems.concat(requestItems);\n\n  each(folderItems, (item) => {\n    sortCollection(item);\n  });\n};\n\nconst sortFolder = (folder = {}) => {\n  const items = folder.items || [];\n  let folderItems = filter(items, (item) => item.type === 'folder');\n  let requestItems = filter(items, (item) => item.type !== 'folder');\n\n  folderItems = sortByNameThenSequence(folderItems);\n  requestItems = requestItems.sort((a, b) => a.seq - b.seq);\n\n  folder.items = folderItems.concat(requestItems);\n\n  each(folderItems, (item) => {\n    sortFolder(item);\n  });\n\n  return folder;\n};\n\nconst getAllRequestsInFolderRecursively = (folder = {}) => {\n  let requests = [];\n\n  if (folder.items && folder.items.length) {\n    folder.items.forEach((item) => {\n      // Skip transient requests\n      if (item.isTransient) {\n        return;\n      }\n      if (item.type !== 'folder') {\n        requests.push(item);\n      } else {\n        requests = requests.concat(getAllRequestsInFolderRecursively(item));\n      }\n    });\n  }\n\n  return requests;\n};\n\nconst getEnvVars = (environment = {}) => {\n  const variables = environment.variables;\n  if (!variables || !variables.length) {\n    return {\n      __name__: environment.name\n    };\n  }\n\n  const envVars = {};\n  each(variables, (variable) => {\n    if (variable.enabled) {\n      envVars[variable.name] = variable.value;\n    }\n  });\n\n  return {\n    ...envVars,\n    __name__: environment.name\n  };\n};\n\nconst getFormattedCollectionOauth2Credentials = ({ oauth2Credentials = [] }) => {\n  let credentialsVariables = {};\n  oauth2Credentials.forEach(({ credentialsId, credentials }) => {\n    if (credentials) {\n      Object.entries(credentials).forEach(([key, value]) => {\n        credentialsVariables[`$oauth2.${credentialsId}.${key}`] = value;\n      });\n    }\n  });\n  return credentialsVariables;\n};\n\nconst mergeAuth = (collection, request, requestTreePath) => {\n  // Start with collection level auth (always consider collection auth as base)\n  const collectionRoot = collection?.draft?.root || collection?.root || {};\n  let collectionAuth = get(collectionRoot, 'request.auth', { mode: 'none' });\n  let effectiveAuth = collectionAuth;\n  let lastFolderWithAuth = null;\n\n  // Traverse through the path to find the closest auth configuration\n  for (let i of requestTreePath) {\n    if (i.type === 'folder') {\n      const folderRoot = i?.draft || i?.root;\n      const folderAuth = get(folderRoot, 'request.auth');\n      // Only consider folders that have a valid auth mode\n      if (folderAuth && folderAuth.mode && folderAuth.mode !== 'none' && folderAuth.mode !== 'inherit') {\n        effectiveAuth = folderAuth;\n        lastFolderWithAuth = i;\n      }\n    }\n  }\n\n  // If request is set to inherit, use the effective auth from collection/folders\n  if (request.auth.mode === 'inherit') {\n    request.auth = effectiveAuth;\n\n    // For OAuth2, we need to handle credentials properly\n    if (effectiveAuth.mode === 'oauth2') {\n      if (lastFolderWithAuth) {\n        // If auth is from folder, add folderUid and clear itemUid\n        request.oauth2Credentials = {\n          ...request.oauth2Credentials,\n          folderUid: lastFolderWithAuth.uid,\n          itemUid: null,\n          mode: request.auth.mode\n        };\n      } else {\n        // If auth is from collection, ensure no folderUid and no itemUid\n        request.oauth2Credentials = {\n          ...request.oauth2Credentials,\n          folderUid: null,\n          itemUid: null,\n          mode: request.auth.mode\n        };\n      }\n    }\n  }\n};\n\nconst resolveInheritedSettings = (settings) => {\n  const resolvedSettings = {};\n\n  // Resolve each setting individually\n  Object.keys(settings).forEach((settingKey) => {\n    const currentValue = settings[settingKey];\n\n    // If setting is inherited, fallback to preferences only for timeout setting\n    if (currentValue === 'inherit' || currentValue === undefined || currentValue === null) {\n      if (settingKey === 'timeout') {\n        resolvedSettings[settingKey] = preferencesUtil.getRequestTimeout();\n      }\n    } else {\n      // Use the current value as-is\n      resolvedSettings[settingKey] = currentValue;\n    }\n  });\n\n  // Handle missing timeout setting - if timeout is not in settings, treat it as inherited\n  if (!settings.hasOwnProperty('timeout')) {\n    resolvedSettings.timeout = preferencesUtil.getRequestTimeout();\n  }\n\n  return resolvedSettings;\n};\n\nconst sortByNameThenSequence = (items) => {\n  const isSeqValid = (seq) => Number.isFinite(seq) && Number.isInteger(seq) && seq > 0;\n\n  // Sort folders alphabetically by name\n  const alphabeticallySorted = [...items].sort((a, b) => a.name && b.name && a.name.localeCompare(b.name));\n\n  // Extract folders without 'seq'\n  const withoutSeq = alphabeticallySorted.filter((f) => !isSeqValid(f['seq']));\n\n  // Extract folders with 'seq' and sort them by 'seq'\n  const withSeq = alphabeticallySorted.filter((f) => isSeqValid(f['seq'])).sort((a, b) => a.seq - b.seq);\n\n  const sortedItems = withoutSeq;\n\n  // Insert folders with 'seq' at their specified positions\n  withSeq.forEach((item) => {\n    const position = item.seq - 1;\n    const existingItem = withoutSeq[position];\n\n    // Check if there's already an item with the same sequence number\n    const hasItemWithSameSeq = Array.isArray(existingItem)\n      ? existingItem?.[0]?.seq === item.seq\n      : existingItem?.seq === item.seq;\n\n    if (hasItemWithSameSeq) {\n      // If there's a conflict, group items with same sequence together\n      const newGroup = Array.isArray(existingItem)\n        ? [...existingItem, item]\n        : [existingItem, item];\n\n      withoutSeq.splice(position, 1, newGroup);\n    } else {\n      // Insert item at the specified position\n      withoutSeq.splice(position, 0, item);\n    }\n  });\n\n  // return flattened sortedItems\n  return sortedItems.flat();\n};\n\nmodule.exports = {\n  mergeHeaders,\n  mergeVars,\n  mergeScripts,\n  mergeAuth,\n  getTreePathFromCollectionToItem,\n  flattenItems,\n  findItem,\n  findItemInCollection,\n  findItemByPathname,\n  findItemInCollectionByPathname,\n  findParentItemInCollection,\n  findParentItemInCollectionByPathname,\n  parseBruFileMeta,\n  parseFileMeta,\n  hydrateRequestWithUuid,\n  transformRequestToSaveToFilesystem,\n  sortCollection,\n  sortFolder,\n  getAllRequestsInFolderRecursively,\n  getEnvVars,\n  getFormattedCollectionOauth2Credentials,\n  sortByNameThenSequence,\n  resolveInheritedSettings\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/common.js",
    "content": "const { customAlphabet } = require('nanoid');\nconst iconv = require('iconv-lite');\nconst { cloneDeep } = require('lodash');\nconst { formatMultipartData } = require('./form-data');\nconst { isFormData } = require('@usebruno/common').utils;\n\n// a customized version of nanoid without using _ and -\nconst uuid = () => {\n  // https://github.com/ai/nanoid/blob/main/url-alphabet/index.js\n  const urlAlphabet = 'useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict';\n  const customNanoId = customAlphabet(urlAlphabet, 21);\n\n  return customNanoId();\n};\n\nconst stringifyJson = async (str) => {\n  try {\n    return JSON.stringify(str, null, 2);\n  } catch (err) {\n    return Promise.reject(err);\n  }\n};\n\nconst parseJson = async (obj) => {\n  try {\n    return JSON.parse(obj);\n  } catch (err) {\n    return Promise.reject(err);\n  }\n};\n\nconst getCircularReplacer = () => {\n  const seen = new WeakSet();\n  return (key, value) => {\n    if (typeof value === 'object' && value !== null) {\n      if (seen.has(value)) return '[Circular]';\n      seen.add(value);\n    }\n    return value;\n  };\n};\n\nconst safeStringifyJSON = (data, indent = null) => {\n  if (data === undefined) return undefined;\n  try {\n    // getCircularReplacer - removes circular references that cause an error when stringifying\n    return JSON.stringify(data, getCircularReplacer(), indent);\n  } catch (e) {\n    console.warn('Failed to stringify data:', e.message);\n    return data;\n  }\n};\n\nconst safeParseJSON = (data) => {\n  try {\n    return JSON.parse(data);\n  } catch (e) {\n    return data;\n  }\n};\n\nconst simpleHash = (str) => {\n  let hash = 0;\n  for (let i = 0; i < str.length; i++) {\n    const char = str.charCodeAt(i);\n    hash = (hash << 5) - hash + char;\n    hash &= hash; // Convert to 32bit integer\n  }\n  return new Uint32Array([hash])[0].toString(36);\n};\n\nconst generateUidBasedOnHash = (str) => {\n  const hash = simpleHash(str);\n\n  return `${hash}`.padEnd(21, '0');\n};\n\nconst flattenDataForDotNotation = (data) => {\n  var result = {};\n  function recurse(current, prop) {\n    if (Object(current) !== current) {\n      result[prop] = current;\n    } else if (Array.isArray(current)) {\n      for (var i = 0, l = current.length; i < l; i++) {\n        recurse(current[i], prop + '[' + i + ']');\n      }\n      if (l == 0) {\n        result[prop] = [];\n      }\n    } else {\n      var isEmpty = true;\n      for (var p in current) {\n        isEmpty = false;\n        recurse(current[p], prop ? prop + '.' + p : p);\n      }\n      if (isEmpty && prop) {\n        result[prop] = {};\n      }\n    }\n  }\n\n  recurse(data, '');\n  return result;\n};\n\nconst parseDataFromResponse = (response, disableParsingResponseJson = false) => {\n  // Parse the charset from content type: https://stackoverflow.com/a/33192813\n  const charsetMatch = /charset=([^()<>@,;:\"/[\\]?.=\\s]*)/i.exec(response.headers['content-type'] || '');\n  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#using_exec_with_regexp_literals\n  const charsetValue = charsetMatch?.[1];\n  const dataBuffer = Buffer.from(response.data);\n  // Overwrite the original data for backwards compatibility\n  let data;\n  if (iconv.encodingExists(charsetValue)) {\n    data = iconv.decode(dataBuffer, charsetValue);\n  } else {\n    data = iconv.decode(dataBuffer, 'utf-8');\n  }\n  // Try to parse response to JSON, this can quietly fail\n  try {\n    // Filter out ZWNBSP character\n    // https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d\n    data = data.replace(/^\\uFEFF/, '');\n    if (!disableParsingResponseJson) {\n      data = JSON.parse(data);\n    }\n  } catch { }\n\n  return { data, dataBuffer };\n};\n\nconst parseDataFromRequest = (request) => {\n  let requestDataString;\n\n  // File uploads are redacted, multipart FormData is formatted from original data for readability, and other types are stringified as-is.\n  if (request.mode === 'file') {\n    requestDataString = '<request body redacted>';\n  } else if (isFormData(request?.data) && Array.isArray(request._originalMultipartData)) {\n    const boundary = request.data._boundary || 'boundary';\n    requestDataString = formatMultipartData(request._originalMultipartData, boundary);\n  } else {\n    requestDataString = typeof request?.data === 'string' ? request?.data : safeStringifyJSON(request?.data);\n  }\n\n  const requestCopy = cloneDeep(request);\n  if (!requestCopy.data) {\n    return { data: null, dataBuffer: null };\n  }\n  requestCopy.data = requestDataString;\n  return parseDataFromResponse(requestCopy);\n};\n\nmodule.exports = {\n  uuid,\n  stringifyJson,\n  parseJson,\n  safeStringifyJSON,\n  safeParseJSON,\n  simpleHash,\n  generateUidBasedOnHash,\n  flattenDataForDotNotation,\n  parseDataFromResponse,\n  parseDataFromRequest\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/constants.js",
    "content": "export const REQUEST_TYPES = ['http-request', 'graphql-request', 'grpc-request', 'ws-request'];\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/cookies.js",
    "content": "module.exports = require('@usebruno/requests').cookies;\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/deeplink.js",
    "content": "const { handleOauth2ProtocolUrl } = require('./oauth2-protocol-handler');\n\n// Store appProtocolUrl - will be handled in the `did-finish-load` event handler\nconst getAppProtocolUrlFromArgv = (argv) => {\n  return argv.find((arg) => arg.startsWith('bruno://'));\n};\n\n// Handle app protocol URLs\nconst handleAppProtocolUrl = (url) => {\n  // Handle OAuth2 callback URLs - `bruno://app/oauth2/callback`\n  if (isOauth2Url(url)) {\n    handleOauth2ProtocolUrl(url);\n  }\n  return;\n};\n\nconst isOauth2Url = (url) => {\n  try {\n    const urlObj = new URL(url);\n\n    if (urlObj.pathname === '/oauth2/callback') {\n      return true;\n    }\n  } catch (error) {\n    console.error('[Protocol Handler] Error handling protocol URL:', error);\n  }\n  return false;\n};\n\nmodule.exports = { handleAppProtocolUrl, getAppProtocolUrlFromArgv };\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/default-location.js",
    "content": "const fs = require('node:fs');\nconst path = require('node:path');\nconst { app } = require('electron');\n\nconst BRUNO_DIR_NAME = 'bruno';\n\n/**\n * Returns the default location where new workspaces and collections are stored.\n * Checks ~/Documents/bruno if available, otherwise falls back to the app's data directory\n */\nfunction resolveDefaultLocation() {\n  const defaultPaths = [\n    path.join(app.getPath('documents'), BRUNO_DIR_NAME),\n    app.getPath('userData')\n  ];\n\n  for (const dirPath of defaultPaths) {\n    try {\n      fs.mkdirSync(dirPath, { recursive: true });\n      return dirPath;\n    } catch (error) {\n      console.warn(`Failed to create directory at ${dirPath}:`, error.message);\n    }\n  }\n\n  throw new Error('Failed to create default location');\n}\n\nmodule.exports = { resolveDefaultLocation };\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/encryption.js",
    "content": "const crypto = require('crypto');\nconst { machineIdSync } = require('@usebruno/node-machine-id');\nconst { safeStorage } = require('electron');\n\n// Constants for algorithm identification\nconst ELECTRONSAFESTORAGE_ALGO = '00';\nconst AES256_ALGO = '01';\n\nfunction deriveKeyAndIv(password, keyLength, ivLength) {\n  const key = Buffer.alloc(keyLength);\n  const iv = Buffer.alloc(ivLength);\n  const derivedBytes = [];\n  let lastHash = null;\n\n  while (Buffer.concat(derivedBytes).length < keyLength + ivLength) {\n    const hash = crypto.createHash('md5');\n    if (lastHash) {\n      hash.update(lastHash);\n    }\n    hash.update(Buffer.from(password, 'utf8'));\n    lastHash = hash.digest();\n    derivedBytes.push(lastHash);\n  }\n\n  const concatenatedBytes = Buffer.concat(derivedBytes);\n  concatenatedBytes.copy(key, 0, 0, keyLength);\n  concatenatedBytes.copy(iv, 0, keyLength, keyLength + ivLength);\n\n  return { key, iv };\n}\n\nfunction aes256Encrypt(data, passkey = null) {\n  const rawKey = passkey || machineIdSync();\n  const iv = Buffer.alloc(16, 0); // Default IV for new encryption\n  const key = crypto.createHash('sha256').update(rawKey).digest(); // Derive a 32-byte key\n  const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);\n  let encrypted = cipher.update(data, 'utf8', 'hex');\n  encrypted += cipher.final('hex');\n\n  return encrypted;\n}\n\nfunction aes256Decrypt(data, passkey = null) {\n  const rawKey = passkey || machineIdSync();\n\n  // Attempt to decrypt using new method first\n  const iv = Buffer.alloc(16, 0); // Default IV for new encryption\n  const key = crypto.createHash('sha256').update(rawKey).digest(); // Derive a 32-byte key\n\n  try {\n    const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);\n    let decrypted = decipher.update(data, 'hex', 'utf8');\n    decrypted += decipher.final('utf8');\n    return decrypted;\n  } catch (err) {\n    // If decryption fails, fall back to old key derivation\n    try {\n      const { key: oldKey, iv: oldIv } = deriveKeyAndIv(rawKey, 32, 16);\n      const decipher = crypto.createDecipheriv('aes-256-cbc', oldKey, oldIv);\n      const decrypted = decipher.update(data, 'hex', 'utf8');\n      decrypted += decipher.final('utf8');\n      return decrypted;\n    } catch (fallbackErr) {\n      console.error('AES256 decryption failed with both methods:', err, fallbackErr);\n      throw new Error('AES256 decryption failed: ' + fallbackErr.message);\n    }\n  }\n}\n\n// electron safe storage encryption and decryption functions\nfunction safeStorageEncrypt(str) {\n  let encryptedStringBuffer = safeStorage.encryptString(str);\n\n  // Convert the encrypted buffer to a hexadecimal string\n  const encryptedString = encryptedStringBuffer.toString('hex');\n\n  return encryptedString;\n}\nfunction safeStorageDecrypt(str) {\n  try {\n    // Convert the hexadecimal string to a buffer\n    const encryptedStringBuffer = Buffer.from(str, 'hex');\n\n    // Decrypt the buffer\n    const decryptedStringBuffer = safeStorage.decryptString(encryptedStringBuffer);\n\n    // Convert the decrypted buffer to a string\n    const decryptedString = decryptedStringBuffer.toString();\n\n    return decryptedString;\n  } catch (err) {\n    console.error('SafeStorage decryption failed:', err);\n    throw new Error('SafeStorage decryption failed: ' + err.message);\n  }\n}\n\nfunction encryptString(str, passkey = null) {\n  if (typeof str !== 'string') {\n    throw new Error('Encrypt failed: invalid string');\n  }\n  if (str.length === 0) {\n    return '';\n  }\n\n  // If a passkey is provided (from cookies store), we must use it for encryption.\n  if (passkey !== null && passkey !== undefined) {\n    if (typeof passkey !== 'string' || passkey.length === 0) {\n      // Corrupted / empty passkey -> do not encrypt, return empty value\n      return '';\n    }\n    try {\n      const encryptedString = aes256Encrypt(str, passkey);\n      return `$${AES256_ALGO}:${encryptedString}`;\n    } catch (err) {\n      // Any error indicates the passkey is unusable; return empty string\n      return '';\n    }\n  }\n\n  if (safeStorage && safeStorage.isEncryptionAvailable()) {\n    const encryptedString = safeStorageEncrypt(str);\n    return `$${ELECTRONSAFESTORAGE_ALGO}:${encryptedString}`;\n  }\n\n  const encryptedString = aes256Encrypt(str);\n  return `$${AES256_ALGO}:${encryptedString}`;\n}\n\nfunction decryptString(str, passkey = null) {\n  if (typeof str !== 'string') {\n    throw new Error('Decrypt failed: unrecognized string format');\n  }\n  if (str.length === 0) {\n    return '';\n  }\n\n  // Find the index of the first colon\n  const colonIndex = str.indexOf(':');\n\n  if (colonIndex === -1) {\n    throw new Error('Decrypt failed: unrecognized string format');\n  }\n\n  // Extract algo and encryptedString based on the colon index\n  const algo = str.substring(1, colonIndex);\n  const encryptedString = str.substring(colonIndex + 1);\n\n  if ([ELECTRONSAFESTORAGE_ALGO, AES256_ALGO].indexOf(algo) === -1) {\n    throw new Error('Decrypt failed: Invalid algo');\n  }\n\n  if (algo === ELECTRONSAFESTORAGE_ALGO) {\n    if (safeStorage && safeStorage.isEncryptionAvailable()) {\n      return safeStorageDecrypt(encryptedString);\n    } else {\n      return '';\n    }\n  }\n\n  if (algo === AES256_ALGO) {\n    return aes256Decrypt(encryptedString, passkey || null);\n  }\n  throw new Error('Decrypt failed: Invalid algo');\n}\n\nfunction decryptStringSafe(str) {\n  try {\n    const result = decryptString(str);\n    return { success: true, value: result };\n  } catch (err) {\n    console.error('Decryption failed:', err.message);\n    return { success: false, error: err.message, value: '' };\n  }\n}\n\nfunction encryptStringSafe(str) {\n  try {\n    const result = encryptString(str);\n    return { success: true, value: result };\n  } catch (err) {\n    console.error('Encryption failed:', err.message);\n    return { success: false, error: err.message, value: '' };\n  }\n}\n\nmodule.exports = {\n  encryptString,\n  encryptStringSafe,\n  decryptString,\n  decryptStringSafe\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/filesystem.js",
    "content": "const path = require('path');\nconst fs = require('fs-extra');\nconst fsPromises = require('fs/promises');\nconst { dialog } = require('electron');\nconst isValidPathname = require('is-valid-path');\nconst os = require('os');\n\nconst DEFAULT_GITIGNORE = [\n  '# Secrets',\n  '.env*',\n  '',\n  '# Dependencies',\n  'node_modules',\n  '',\n  '# OS files',\n  '.DS_Store',\n  'Thumbs.db'\n].join('\\n');\n\nconst exists = async (p) => {\n  try {\n    await fsPromises.access(p);\n    return true;\n  } catch (_) {\n    return false;\n  }\n};\n\nconst isSymbolicLink = (filepath) => {\n  try {\n    return fs.existsSync(filepath) && fs.lstatSync(filepath).isSymbolicLink();\n  } catch (_) {\n    return false;\n  }\n};\n\nconst isFile = (filepath) => {\n  try {\n    return fs.existsSync(filepath) && fs.lstatSync(filepath).isFile();\n  } catch (_) {\n    return false;\n  }\n};\n\nconst isDirectory = (dirPath) => {\n  try {\n    return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory();\n  } catch (_) {\n    return false;\n  }\n};\n\nconst isValidCollectionDirectory = (dirPath) => {\n  if (!isDirectory(dirPath)) {\n    return false;\n  }\n  const brunoJsonPath = path.join(dirPath, 'bruno.json');\n  const opencollectionYmlPath = path.join(dirPath, 'opencollection.yml');\n  return fs.existsSync(brunoJsonPath) || fs.existsSync(opencollectionYmlPath);\n};\n\nconst hasSubDirectories = (dir) => {\n  const files = fs.readdirSync(dir);\n  return files.some((file) => fs.statSync(path.join(dir, file)).isDirectory());\n};\n\nconst normalizeAndResolvePath = (pathname) => {\n  if (isWSLPath(pathname)) {\n    return normalizeWSLPath(pathname);\n  }\n\n  if (isSymbolicLink(pathname)) {\n    const absPath = path.dirname(pathname);\n    const targetPath = path.resolve(absPath, fs.readlinkSync(pathname));\n    if (isFile(targetPath) || isDirectory(targetPath)) {\n      return path.resolve(targetPath);\n    }\n    console.error(`Cannot resolve link target \"${pathname}\" (${targetPath}).`);\n    return '';\n  }\n  return path.resolve(pathname);\n};\n\nfunction isWSLPath(pathname) {\n  // Check if the path starts with the WSL prefix\n  // eg. \"\\\\wsl.localhost\\Ubuntu\\home\\user\\bruno\\collection\\scripting\\api\\req\\getHeaders.bru\"\n  return pathname.startsWith('\\\\\\\\') || pathname.startsWith('//') || pathname.startsWith('/wsl.localhost/') || pathname.startsWith('\\\\wsl.localhost');\n}\n\nfunction normalizeWSLPath(pathname) {\n  // Replace the WSL path prefix and convert forward slashes to backslashes\n  // This is done to achieve WSL paths (linux style) to Windows UNC equivalent (Universal Naming Conversion)\n  return pathname.replace(/^\\/wsl.localhost/, '\\\\\\\\wsl.localhost').replace(/\\//g, '\\\\');\n}\n\nconst writeFile = async (pathname, content, isBinary = false) => {\n  try {\n    await safeWriteFile(pathname, content, {\n      encoding: !isBinary ? 'utf-8' : null\n    });\n  } catch (err) {\n    console.error(`Error writing file at ${pathname}:`, err);\n    return Promise.reject(err);\n  }\n};\n\nconst hasJsonExtension = (filename) => {\n  if (!filename || typeof filename !== 'string') return false;\n  return ['json'].some((ext) => filename.toLowerCase().endsWith(`.${ext}`));\n};\n\nconst hasBruExtension = (filename) => {\n  if (!filename || typeof filename !== 'string') return false;\n  return ['bru'].some((ext) => filename.toLowerCase().endsWith(`.${ext}`));\n};\n\nconst hasRequestExtension = (filename, format = null) => {\n  if (!filename || typeof filename !== 'string') return false;\n\n  if (format) {\n    const ext = format === 'yml' ? 'yml' : 'bru';\n    return filename.toLowerCase().endsWith(`.${ext}`);\n  }\n\n  return ['bru', 'yml'].some((ext) => filename.toLowerCase().endsWith(`.${ext}`));\n};\n\nconst createDirectory = async (dir) => {\n  if (!dir) {\n    throw new Error(`directory: path is null`);\n  }\n\n  if (fs.existsSync(dir)) {\n    throw new Error(`directory: ${dir} already exists`);\n  }\n\n  return fs.mkdirSync(dir);\n};\n\nconst browseDirectory = async (win) => {\n  const { filePaths } = await dialog.showOpenDialog(win, {\n    properties: ['openDirectory', 'createDirectory']\n  });\n\n  if (!filePaths || !filePaths[0]) {\n    return false;\n  }\n\n  const resolvedPath = path.resolve(filePaths[0]);\n  return isDirectory(resolvedPath) ? resolvedPath : false;\n};\n\nconst browseFiles = async (win, filters = [], properties = []) => {\n  const { filePaths } = await dialog.showOpenDialog(win, {\n    properties: ['openFile', ...properties],\n    filters\n  });\n\n  if (!filePaths) {\n    return [];\n  }\n\n  return filePaths.map((filePath) => path.resolve(filePath)).filter((filePath) => isFile(filePath));\n};\n\nconst chooseFileToSave = async (win, preferredFileName = '') => {\n  const { filePath } = await dialog.showSaveDialog(win, {\n    defaultPath: preferredFileName\n  });\n\n  return filePath;\n};\n\nconst searchForFiles = (dir, extension) => {\n  let results = [];\n  const files = fs.readdirSync(dir);\n  for (const file of files) {\n    const filePath = path.join(dir, file);\n    const stat = fs.statSync(filePath);\n    if (stat.isDirectory()) {\n      results = results.concat(searchForFiles(filePath, extension));\n    } else if (path.extname(file) === extension) {\n      results.push(filePath);\n    }\n  }\n  return results;\n};\n\n// Search for request files based on collection filetype by reading config\nconst searchForRequestFiles = (dir, collectionPath = null) => {\n  const format = getCollectionFormat(collectionPath || dir);\n  if (format === 'yml') {\n    return searchForFiles(dir, '.yml');\n  } else if (format === 'bru') {\n    return searchForFiles(dir, '.bru');\n  } else {\n    throw new Error(`Invalid format: ${format}`);\n  }\n};\n\nconst sanitizeName = (name) => {\n  const invalidCharacters = /[<>:\"/\\\\|?*\\x00-\\x1F]/g;\n  name = name\n    .replace(invalidCharacters, '-') // replace invalid characters with hyphens\n    .replace(/^[\\s\\-]+/, '') // remove leading spaces and hyphens\n    .replace(/[.\\s]+$/, ''); // remove trailing dots and spaces\n  return name;\n};\n\nconst isWindowsOS = () => {\n  return os.platform() === 'win32';\n};\n\n/**\n * Generate a unique name by adding a \"copy\" suffix if needed\n *\n * @param {string} baseName - The base name\n * @param {Function} checkExists - Function that takes a name and returns true if it exists\n * @returns {string} - A unique name\n */\nconst generateUniqueName = (baseName, checkExists) => {\n  if (!checkExists(baseName)) {\n    return baseName;\n  }\n\n  let counter = 1;\n  let uniqueName = `${baseName} copy`;\n\n  while (checkExists(uniqueName)) {\n    counter++;\n    uniqueName = `${baseName} copy ${counter}`;\n  }\n  return uniqueName;\n};\n\nconst getCollectionFormat = (collectionPath) => {\n  const ocYmlPath = path.join(collectionPath, 'opencollection.yml');\n  if (fs.existsSync(ocYmlPath)) {\n    return 'yml';\n  }\n\n  const brunoJsonPath = path.join(collectionPath, 'bruno.json');\n  if (fs.existsSync(brunoJsonPath)) {\n    return 'bru';\n  }\n\n  throw new Error(`No collection configuration found at: ${collectionPath}`);\n};\n\nconst validateName = (name) => {\n  const invalidCharacters = /[<>:\"/\\\\|?*\\x00-\\x1F]/g; // keeping this for informational purpose\n  const reservedDeviceNames = /^(CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])$/i;\n  const firstCharacter = /^[^\\s\\-<>:\"/\\\\|?*\\x00-\\x1F]/; // no space, hyphen and `invalidCharacters`\n  const middleCharacters = /^[^<>:\"/\\\\|?*\\x00-\\x1F]*$/; // no `invalidCharacters`\n  const lastCharacter = /[^.\\s<>:\"/\\\\|?*\\x00-\\x1F]$/; // no dot, space and `invalidCharacters`\n  if (name.length > 255) return false; // max name length\n\n  if (reservedDeviceNames.test(name)) return false; // windows reserved names\n\n  return (\n    firstCharacter.test(name)\n    && middleCharacters.test(name)\n    && lastCharacter.test(name)\n  );\n};\n\nconst safeToRename = (oldPath, newPath) => {\n  try {\n    // If the new path doesn't exist, it's safe to rename\n    if (!fs.existsSync(newPath)) {\n      return true;\n    }\n\n    const oldStat = fs.statSync(oldPath);\n    const newStat = fs.statSync(newPath);\n\n    if (isWindowsOS()) {\n      // Windows-specific comparison:\n      // Check if both files have the same birth time, size (Since, Win FAT-32 doesn't use inodes)\n\n      return oldStat.birthtimeMs === newStat.birthtimeMs && oldStat.size === newStat.size;\n    }\n    // Unix/Linux/MacOS: Check inode to see if they are the same file\n    return oldStat.ino === newStat.ino;\n  } catch (error) {\n    console.error(`Error checking file rename safety for ${oldPath} and ${newPath}:`, error);\n    return false;\n  }\n};\n\nconst getCollectionStats = async (directoryPath) => {\n  let size = 0;\n  let filesCount = 0;\n  let maxFileSize = 0;\n\n  async function calculateStats(directory) {\n    const entries = await fsPromises.readdir(directory, { withFileTypes: true });\n\n    const tasks = entries.map(async (entry) => {\n      const fullPath = path.join(directory, entry.name);\n\n      if (entry.isDirectory()) {\n        if (['node_modules', '.git'].includes(entry.name)) {\n          return;\n        }\n\n        await calculateStats(fullPath);\n      }\n\n      if (path.extname(fullPath) === '.bru') {\n        const stats = await fsPromises.stat(fullPath);\n        size += stats?.size;\n        if (maxFileSize < stats?.size) {\n          maxFileSize = stats?.size;\n        }\n        filesCount += 1;\n      }\n    });\n\n    await Promise.all(tasks);\n  }\n\n  await calculateStats(directoryPath);\n\n  size = sizeInMB(size);\n  maxFileSize = sizeInMB(maxFileSize);\n\n  return { size, filesCount, maxFileSize };\n};\n\nconst sizeInMB = (size) => {\n  return size / (1024 * 1024);\n};\n\nconst getSafePathToWrite = (filePath) => {\n  const MAX_FILENAME_LENGTH = 255; // Common limit on most filesystems\n  let dir = path.dirname(filePath);\n  let ext = path.extname(filePath);\n  let base = path.basename(filePath, ext);\n  if (base.length + ext.length > MAX_FILENAME_LENGTH) {\n    base = sanitizeName(base);\n    base = base.slice(0, MAX_FILENAME_LENGTH - ext.length);\n  }\n  let safePath = path.join(dir, base + ext);\n  return safePath;\n};\n\nasync function safeWriteFile(filePath, data, options) {\n  const safePath = getSafePathToWrite(filePath);\n\n  try {\n    const fsExtra = require('fs-extra');\n    fsExtra.outputFileSync(safePath, data, options);\n  } catch (err) {\n    console.error(`Error writing file at ${safePath}:`, err);\n    return Promise.reject(err);\n  }\n}\n\nfunction safeWriteFileSync(filePath, data) {\n  const safePath = getSafePathToWrite(filePath);\n  fs.writeFileSync(safePath, data);\n}\n\n// Recursively copies a source <file/directory> to a destination <directory>.\nconst copyPath = async (source, destination) => {\n  let targetPath = `${destination}/${path.basename(source)}`;\n\n  const targetPathExists = await fsPromises.access(targetPath).then(() => true).catch(() => false);\n  if (targetPathExists) {\n    throw new Error(`Cannot copy, ${path.basename(source)} already exists in ${path.basename(destination)}`);\n  }\n\n  const copy = async (source, destination) => {\n    const stat = await fsPromises.lstat(source);\n    if (stat.isDirectory()) {\n      await fsPromises.mkdir(destination, { recursive: true });\n      const entries = await fsPromises.readdir(source);\n      for (const entry of entries) {\n        const srcPath = path.join(source, entry);\n        const destPath = path.join(destination, entry);\n        await copy(srcPath, destPath);\n      }\n    } else {\n      await fsPromises.copyFile(source, destination);\n    }\n  };\n\n  await copy(source, targetPath);\n};\n\n// Recursively removes a source <file/directory>.\nconst removePath = async (source) => {\n  const stat = await fsPromises.lstat(source);\n  if (stat.isDirectory()) {\n    const entries = await fsPromises.readdir(source);\n    for (const entry of entries) {\n      const entryPath = path.join(source, entry);\n      await removePath(entryPath);\n    }\n    await fsPromises.rmdir(source);\n  } else {\n    await fsPromises.unlink(source);\n  }\n};\n\n// Recursively gets paths.\nconst getPaths = async (source) => {\n  let paths = [];\n  const _getPaths = async (source) => {\n    const stat = await fsPromises.lstat(source);\n    paths.push(source);\n    if (stat.isDirectory()) {\n      const entries = await fsPromises.readdir(source);\n      for (const entry of entries) {\n        const entryPath = path.join(source, entry);\n        await _getPaths(entryPath);\n      }\n    }\n  };\n  await _getPaths(source);\n  return paths;\n};\n\n/**\n * Checks if a file is larger than a given threshold.\n * @param {string} filePath - The path to the file.\n * @param {number} threshold - The threshold in bytes. Default is 10MB.\n * @returns {boolean} True if the file is larger than the threshold, false otherwise.\n */\nconst isLargeFile = (filePath, threshold = 10 * 1024 * 1024) => {\n  if (!isFile(filePath)) {\n    throw new Error(`File ${filePath} is not a file`);\n  }\n\n  const size = fs.statSync(filePath).size;\n\n  return size > threshold;\n};\n\nconst isDotEnvFile = (pathname, collectionPath) => {\n  const dirname = path.dirname(pathname);\n  const basename = path.basename(pathname);\n\n  return path.normalize(dirname) === path.normalize(collectionPath) && basename === '.env';\n};\n\nconst isValidDotEnvFilename = (filename) => {\n  if (!filename || typeof filename !== 'string') return false;\n  const basename = path.basename(filename);\n  if (basename !== filename) return false;\n  return basename === '.env' || (basename.startsWith('.env.') && /^\\.env\\.[a-zA-Z0-9._-]+$/.test(basename));\n};\n\nconst isBrunoConfigFile = (pathname, collectionPath) => {\n  const dirname = path.dirname(pathname);\n  const basename = path.basename(pathname);\n\n  return path.normalize(dirname) === path.normalize(collectionPath) && basename === 'bruno.json';\n};\n\nconst isBruEnvironmentConfig = (pathname, collectionPath) => {\n  const dirname = path.dirname(pathname);\n  const envDirectory = path.join(collectionPath, 'environments');\n  const basename = path.basename(pathname);\n\n  return path.normalize(dirname) === path.normalize(envDirectory) && hasBruExtension(basename);\n};\n\nconst isCollectionRootBruFile = (pathname, collectionPath) => {\n  const dirname = path.dirname(pathname);\n  const basename = path.basename(pathname);\n\n  return path.normalize(dirname) === path.normalize(collectionPath) && basename === 'collection.bru';\n};\n\nconst scanForBrunoFiles = async (dir) => {\n  const brunoFolders = [];\n\n  const scanDir = (currentDir) => {\n    const files = fs.readdirSync(currentDir);\n\n    if (files && files.length) {\n      files.forEach((file) => {\n        const fullPath = path.join(currentDir, file);\n        const stat = fs.statSync(fullPath);\n\n        if (stat.isDirectory()) {\n          if (['node_modules', '.git'].includes(file)) {\n            return;\n          }\n          scanDir(fullPath);\n        } else if (file === 'bruno.json') {\n          brunoFolders.push(currentDir);\n        }\n      });\n    }\n  };\n\n  scanDir(dir);\n  return brunoFolders;\n};\n\nconst posixifyPath = (p) => (p ? p.replace(/\\\\/g, '/') : p);\n\nmodule.exports = {\n  DEFAULT_GITIGNORE,\n  posixifyPath,\n  isValidPathname,\n  exists,\n  isSymbolicLink,\n  isFile,\n  isDirectory,\n  isValidCollectionDirectory,\n  normalizeAndResolvePath,\n  isWSLPath,\n  normalizeWSLPath,\n  writeFile,\n  hasJsonExtension,\n  hasBruExtension,\n  hasRequestExtension,\n  createDirectory,\n  browseDirectory,\n  browseFiles,\n  chooseFileToSave,\n  searchForFiles,\n  searchForRequestFiles,\n  sanitizeName,\n  isWindowsOS,\n  safeToRename,\n  validateName,\n  hasSubDirectories,\n  getCollectionStats,\n  sizeInMB,\n  safeWriteFile,\n  safeWriteFileSync,\n  copyPath,\n  removePath,\n  getPaths,\n  isLargeFile,\n  generateUniqueName,\n  getCollectionFormat,\n  isDotEnvFile,\n  isValidDotEnvFilename,\n  isBrunoConfigFile,\n  isBruEnvironmentConfig,\n  isCollectionRootBruFile,\n  scanForBrunoFiles\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/filesystem.test.js",
    "content": "const { sanitizeName, isWSLPath, normalizeWSLPath, normalizeAndResolvePath, isLargeFile } = require('./filesystem.js');\nconst fs = require('fs-extra');\n\ndescribe('sanitizeName', () => {\n  it('should replace invalid characters with hyphens', () => {\n    expect(sanitizeName(\n      'valid<>:\"/\\|?*\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x09\\x0A\\x0B\\x0C\\x0D\\x0E\\x0F\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1A\\x1B\\x1C\\x1D\\x1E\\x1F'\n    )).toEqual('valid----------------------------------------');\n\n    expect(sanitizeName(\n      '<>:\"/\\|?*\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x09\\x0A\\x0B\\x0C\\x0D\\x0E\\x0F\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1A\\x1B\\x1C\\x1D\\x1E\\x1Fvalid<>:\"/\\|?*\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x09\\x0A\\x0B\\x0C\\x0D\\x0E\\x0F\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1A\\x1B\\x1C\\x1D\\x1E\\x1F'\n    )).toEqual('valid----------------------------------------');\n  });\n\n  it('should not modify valid directory names', () => {\n    const input = 'my-directory';\n    expect(sanitizeName(input)).toEqual(input);\n  });\n\n  it('should replace multiple invalid characters with a single hyphen', () => {\n    const input = 'my<>invalid?directory';\n    const expectedOutput = 'my--invalid-directory';\n    expect(sanitizeName(input)).toEqual(expectedOutput);\n  });\n\n  it('should handle names with slashes', () => {\n    const input = 'my/invalid/directory';\n    const expectedOutput = 'my-invalid-directory';\n    expect(sanitizeName(input)).toEqual(expectedOutput);\n  });\n});\n\ndescribe('isLargeFile', () => {\n  let existsSyncSpy;\n  let lstatSyncSpy;\n  let statSyncSpy;\n\n  beforeEach(() => {\n    existsSyncSpy = jest.spyOn(fs, 'existsSync');\n    lstatSyncSpy = jest.spyOn(fs, 'lstatSync');\n    statSyncSpy = jest.spyOn(fs, 'statSync');\n  });\n\n  afterEach(() => {\n    jest.restoreAllMocks();\n  });\n\n  it('should return false when file size is below default threshold (10MB)', () => {\n    existsSyncSpy.mockReturnValue(true);\n    lstatSyncSpy.mockReturnValue({ isFile: () => true });\n    statSyncSpy.mockReturnValue({ size: 5 * 1024 * 1024 }); // 5MB\n\n    expect(isLargeFile('/path/small.bin')).toBe(false);\n  });\n\n  it('should return true when file size is above default threshold (10MB)', () => {\n    existsSyncSpy.mockReturnValue(true);\n    lstatSyncSpy.mockReturnValue({ isFile: () => true });\n    statSyncSpy.mockReturnValue({ size: 15 * 1024 * 1024 }); // 15MB\n\n    expect(isLargeFile('/path/large.bin')).toBe(true);\n  });\n\n  it('should respect custom threshold (args true or false)', () => {\n    existsSyncSpy.mockReturnValue(true);\n    lstatSyncSpy.mockReturnValue({ isFile: () => true });\n    statSyncSpy.mockReturnValue({ size: 50 });\n\n    expect(isLargeFile('/path/file.bin', 100)).toBe(false); // 50 < 100\n    expect(isLargeFile('/path/file.bin', 10)).toBe(true); // 50 > 10\n  });\n\n  it('should throw on invalid values (not a file)', () => {\n    existsSyncSpy.mockReturnValue(false);\n    lstatSyncSpy.mockReturnValue({ isFile: () => false });\n\n    expect(() => isLargeFile('/path/not-a-file.bin')).toThrow('File /path/not-a-file.bin is not a file');\n  });\n});\n\ndescribe('WSL Path Utilities', () => {\n  describe('isWSLPath', () => {\n    it('should identify WSL paths starting with double backslash', () => {\n      expect(isWSLPath('\\\\\\\\wsl.localhost\\\\Ubuntu\\\\home\\\\user')).toBe(true);\n    });\n\n    it('should identify WSL paths starting with double forward slash', () => {\n      expect(isWSLPath('//wsl.localhost/Ubuntu/home/user')).toBe(true);\n    });\n\n    it('should identify WSL paths starting with /wsl.localhost/', () => {\n      expect(isWSLPath('/wsl.localhost/Ubuntu/home/user')).toBe(true);\n    });\n\n    it('should identify WSL paths starting with \\\\wsl.localhost', () => {\n      expect(isWSLPath('\\\\wsl.localhost\\\\Ubuntu\\\\home\\\\user')).toBe(true);\n    });\n\n    it('should return false for non-WSL paths', () => {\n      expect(isWSLPath('C:\\\\Users\\\\user\\\\Documents')).toBe(false);\n      expect(isWSLPath('/home/user/documents')).toBe(false);\n      expect(isWSLPath('relative/path')).toBe(false);\n    });\n  });\n\n  describe('normalizeWSLPath', () => {\n    it('should convert forward slash WSL paths to backslash format', () => {\n      const input = '/wsl.localhost/Ubuntu/home/user/file.txt';\n      const expected = '\\\\\\\\wsl.localhost\\\\Ubuntu\\\\home\\\\user\\\\file.txt';\n      expect(normalizeWSLPath(input)).toBe(expected);\n    });\n\n    it('should handle paths already in backslash format', () => {\n      const input = '\\\\\\\\wsl.localhost\\\\Ubuntu\\\\home\\\\user\\\\file.txt';\n      expect(normalizeWSLPath(input)).toBe(input);\n    });\n\n    it('should convert mixed slash formats to backslash format', () => {\n      const input = '/wsl.localhost\\\\Ubuntu/home\\\\user/file.txt';\n      const expected = '\\\\\\\\wsl.localhost\\\\Ubuntu\\\\home\\\\user\\\\file.txt';\n      expect(normalizeWSLPath(input)).toBe(expected);\n    });\n  });\n\n  describe('normalizeAndResolvePath with WSL paths', () => {\n    it('should normalize WSL paths', () => {\n      const input = '/wsl.localhost/Ubuntu/home/user/file.txt';\n      const expected = '\\\\\\\\wsl.localhost\\\\Ubuntu\\\\home\\\\user\\\\file.txt';\n      expect(normalizeAndResolvePath(input)).toBe(expected);\n    });\n\n    it('should handle already normalized WSL paths', () => {\n      const input = '\\\\\\\\wsl.localhost\\\\Ubuntu\\\\home\\\\user\\\\file.txt';\n      expect(normalizeAndResolvePath(input)).toBe(input);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/form-data.js",
    "content": "const { forEach } = require('lodash');\nconst FormData = require('form-data');\nconst fs = require('fs');\nconst path = require('path');\n\nconst formatMultipartData = (multipartData, boundary) => {\n  if (!Array.isArray(multipartData) || multipartData.length === 0) {\n    return '';\n  }\n\n  const normalizeBoundary = (b) => {\n    const value = b || 'boundary';\n    return value.replace(/^--+/, '').replace(/--+$/, '');\n  };\n\n  const getFileName = (filePath) => {\n    if (typeof filePath === 'string' && filePath.trim()) {\n      return path.basename(filePath) || 'file';\n    }\n    return 'file';\n  };\n\n  const formatValue = (value) => {\n    if (Array.isArray(value)) {\n      return value.map((v) => String(v ?? '')).join(', ');\n    }\n    return String(value ?? '');\n  };\n\n  const boundaryValue = normalizeBoundary(boundary);\n  const parts = [];\n\n  multipartData.forEach((field) => {\n    if (!field || !field.name) return;\n\n    parts.push(`----${boundaryValue}`);\n    parts.push('Content-Disposition: form-data');\n\n    if (field.type === 'file') {\n      const filePaths = Array.isArray(field.value) ? field.value : (field.value ? [field.value] : ['']);\n      filePaths.forEach((filePath) => {\n        parts.push(`----${boundaryValue}`);\n        parts.push('Content-Disposition: form-data');\n        const fileName = getFileName(filePath);\n        parts.push(`name: ${field.name}`);\n        parts.push(`value: [File: ${fileName}]`);\n        parts.push('');\n      });\n    } else {\n      const value = formatValue(field.value);\n      parts.push(`name: ${field.name}`);\n      parts.push(`value: ${value}`);\n      parts.push('');\n    }\n  });\n\n  parts.push(`----${boundaryValue}--`);\n  return parts.join('\\n');\n};\n\nconst createFormData = (data, collectionPath) => {\n  // make axios work in node using form data\n  // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427\n  const form = new FormData();\n  forEach(data, (datum) => {\n    const { name, type, value, contentType } = datum;\n    let options = {};\n    if (contentType) {\n      options.contentType = contentType;\n    }\n    if (type === 'text') {\n      if (Array.isArray(value)) {\n        value.forEach((val) => form.append(name, val, options));\n      } else {\n        form.append(name, value, options);\n      }\n      return;\n    }\n\n    if (type === 'file') {\n      const filePaths = value || [];\n      filePaths.forEach((filePath) => {\n        let trimmedFilePath = filePath.trim();\n        if (!path.isAbsolute(trimmedFilePath)) {\n          trimmedFilePath = path.join(collectionPath, trimmedFilePath);\n        }\n        options.filename = path.basename(trimmedFilePath);\n        form.append(name, fs.createReadStream(trimmedFilePath), options);\n      });\n    }\n  });\n  return form;\n};\n\nmodule.exports = {\n  createFormData,\n  formatMultipartData\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/git.js",
    "content": "const simpleGit = require('simple-git');\nconst fs = require('fs');\nconst path = require('path');\nconst { exec } = require('child_process');\nconst { parseRequest } = require('@usebruno/filestore');\n\nlet collectionPathToGitRootPathMap = new Map();\n\nconst simpleGitInstances = new Map();\n\nconst getGitVersion = () => {\n  return new Promise((resolve, reject) => {\n    exec('git --version', (error, stdout, stderr) => {\n      if (error) {\n        return resolve(null);\n      }\n      const gitVersion = stdout.trim();\n      return resolve(gitVersion);\n    });\n  });\n};\n\nconst getSimpleGitInstanceForPath = (gitRootPath) => {\n  let git = simpleGitInstances.get(gitRootPath);\n  if (!git) {\n    git = simpleGit(gitRootPath);\n    simpleGitInstances.set(gitRootPath, git);\n  }\n  return git;\n};\n\nconst handleGitOutput = ({ win, processUid, sendStdout = false }) => (command, stdout, stderr) => {\n  const sendProgressUpdate = (data) => {\n    win.webContents.send('main:update-git-operation-progress', {\n      uid: processUid,\n      data: data.toString()\n    });\n  };\n\n  stderr.on('data', sendProgressUpdate);\n\n  if (sendStdout) {\n    stdout.on('data', sendProgressUpdate);\n  }\n};\n\nconst findGitRootPath = (collectionPath) => {\n  const gitPath = path.join(collectionPath, '.git');\n  try {\n    if (fs.existsSync(gitPath)) {\n      return gitPath?.split('.git')?.[0];\n    } else {\n      const parentDir = path.dirname(collectionPath);\n      if (parentDir === collectionPath) {\n        return null;\n      } else {\n        return findGitRootPath(parentDir);\n      }\n    }\n  } catch (err) {\n    console.error('Error finding .git path:', err);\n    return null;\n  }\n};\n\nconst getCollectionGitRootPath = (collectionPath) => {\n  let savedGitRootPath = collectionPathToGitRootPathMap.get(collectionPath);\n  if (savedGitRootPath) {\n    return savedGitRootPath;\n  }\n  let gitRootPath = findGitRootPath(collectionPath);\n  collectionPathToGitRootPathMap.set(collectionPath, gitRootPath);\n  return gitRootPath;\n};\n\nconst getCollectionGitRepoUrl = async (gitRootPath) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    git.listRemote(['--get-url', 'origin'], (err, data) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n      resolve(data.trim());\n    });\n  });\n};\n\nconst initGit = async (gitRootPath) => {\n  const git = getSimpleGitInstanceForPath(gitRootPath);\n  await git.init();\n  // Create and checkout main branch -> This is specific for use with Bruno\n  return await git.raw(['branch', '-M', 'main']);\n};\n\nconst stageChanges = async (gitRootPath, files) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    git.add(files, (err, res) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n      resolve(res);\n    });\n  });\n};\n\nconst unstageChanges = async (gitRootPath, files) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n\n    // First check the status to see which files are actually staged\n    git.status(['--porcelain'], (err, status) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n\n      // Filter files to only include those that are actually staged\n      const stagedFiles = files.filter((fullPath) => {\n        const relativePath = path.relative(gitRootPath, fullPath);\n        // Normalize path separators for cross-platform compatibility\n        const normalizedPath = relativePath.replace(/\\\\/g, '/');\n        return status.files.some((file) =>\n          file.path === normalizedPath\n          && (file.index === 'M' || file.index === 'A' || file.index === 'D')\n        );\n      });\n\n      // If no files are actually staged, just resolve\n      if (stagedFiles.length === 0) {\n        resolve();\n        return;\n      }\n\n      // Unstage only the files that are actually staged\n      git.reset(['HEAD', '--', ...stagedFiles], (err, res) => {\n        if (err) {\n          reject(err);\n          return;\n        }\n        resolve(res);\n      });\n    });\n  });\n};\n\nconst discardChanges = async (gitRootPath, filePaths) => {\n  return new Promise(async (resolve, reject) => {\n    try {\n      const git = getSimpleGitInstanceForPath(gitRootPath);\n\n      // Get current git status to categorize files\n      git.status(['--porcelain'], async (err, status) => {\n        if (err) {\n          reject(err);\n          return;\n        }\n\n        // Create a map of file paths to their status\n        const fileStatusMap = {};\n        status.files.forEach((file) => {\n          fileStatusMap[file.path] = file;\n        });\n\n        // Categorize files based on their git status\n        const trackedFiles = [];\n        const untrackedFiles = [];\n\n        filePaths.forEach((filePath) => {\n          // Normalize paths for comparison\n          const relativePath = filePath.startsWith(gitRootPath)\n            ? path.relative(gitRootPath, filePath) : filePath;\n\n          // Normalize path separators for cross-platform compatibility\n          const normalizedPath = relativePath.replace(/\\\\/g, '/');\n          const fileStatus = fileStatusMap[normalizedPath];\n\n          // If the file is untracked, we need to delete it from the filesystem\n          // ? means untracked\n          if (fileStatus && fileStatus.working_dir === '?') {\n            // Untracked file - needs to be deleted from filesystem\n            untrackedFiles.push(filePath);\n          } else if (fileStatus) {\n            // Tracked file - can be discarded with git checkout\n            trackedFiles.push(filePath);\n          } else {\n            // File not in status - might be already deleted, renamed, or doesn't exist\n            console.warn(`File not found in git status: ${relativePath}. File may have been already deleted or moved.`);\n\n            // Check if it's an absolute path that needs to be treated as untracked\n            if (filePath.startsWith(gitRootPath) && fs.existsSync(filePath)) {\n              console.log(`Treating unknown file as untracked: ${relativePath}`);\n              untrackedFiles.push(filePath);\n            }\n          }\n        });\n\n        // Handle tracked and untracked files sequentially\n        try {\n          // Handle tracked files with git checkout\n          if (trackedFiles.length > 0) {\n            await new Promise((checkoutResolve, checkoutReject) => {\n              git.checkout(trackedFiles, (err, res) => {\n                if (err) {\n                  console.error('Error discarding tracked files:', err);\n                  checkoutReject(err);\n                } else {\n                  console.log(`Discarded ${trackedFiles.length} tracked files`);\n                  checkoutResolve(res);\n                }\n              });\n            });\n          }\n\n          // Handle untracked files by deleting them from filesystem\n          if (untrackedFiles.length > 0) {\n            for (const filePath of untrackedFiles) {\n              const fullPath = filePath.startsWith(gitRootPath) ? filePath : path.join(gitRootPath, filePath);\n\n              // Check if file exists before trying to delete\n              if (fs.existsSync(fullPath)) {\n                await fs.promises.unlink(fullPath);\n                console.log(`Deleted untracked file: ${fullPath}`);\n              }\n            }\n          }\n\n          resolve({\n            trackedFilesDiscarded: trackedFiles.length,\n            untrackedFilesDeleted: untrackedFiles.length\n          });\n        } catch (discardError) {\n          console.error('Error during discard operation:', discardError);\n          reject(discardError);\n        }\n      });\n    } catch (gitStatusError) {\n      reject(gitStatusError);\n    }\n  });\n};\n\nconst commitChanges = async (gitRootPath, message) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    git.commit(message, (err, res) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n      resolve(res);\n    });\n  });\n};\n\nconst getStagedFileDiff = async (gitRootPath, filePath) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    git.diff(['--no-prefix', '--staged', '--', filePath], (err, stagedChanges) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n      resolve(stagedChanges);\n    });\n  });\n};\n\nconst getRenamedFileDiff = async (gitRootPath, file) => {\n  return new Promise((resolve, reject) => {\n    const git = simpleGit(gitRootPath);\n    git.diff(['--staged', '--', file.from, file.to], (err, stagedChanges) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n      resolve(stagedChanges);\n    });\n  });\n};\n\nconst getUnstagedFileDiff = async (gitRootPath, filePath) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n\n    git.status((err, status) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n\n      const isFileTracked = status.files.some((file) => {\n        const statusFilePath = path.join(gitRootPath, file.path);\n        return filePath === statusFilePath && file.index !== '?' && file.working_dir !== '?';\n      });\n\n      if (isFileTracked) {\n        git.diff(['--no-prefix', '--diff-filter=ACMD', '--', filePath], (err, tracked) => {\n          if (err) {\n            reject(err);\n            return;\n          }\n          resolve(tracked);\n        });\n      } else {\n        fs.readFile(filePath, 'utf8', (err, content) => {\n          if (err) {\n            reject(err);\n            return;\n          }\n\n          const prefixedLines = content\n            .split('\\n')\n            .map((line) => `+${line}`);\n          const lineCount = prefixedLines.length;\n          const lines = prefixedLines.join('\\n');\n\n          let diff\n            = [\n              `diff --git a/${filePath} b/${filePath}`,\n              `new file mode 100644`,\n              `--- a/${filePath}`,\n              `+++ b/${filePath}`,\n              `@@ -0,0 +1,${lineCount} @@`,\n              `${lines}`\n            ].join('\\n') + '\\n';\n\n          resolve(diff);\n        });\n      }\n    });\n  });\n};\n\nconst getCollectionGitBranches = async (gitRootPath) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    git.branchLocal((err, branches) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n      resolve(branches.all);\n    });\n  });\n};\n\nconst getCurrentGitBranch = async (gitRootPath) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    git.branchLocal((err, branches) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n      resolve(branches.current);\n    });\n  });\n};\n\nconst getDefaultGitBranch = async (gitRootPath) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    git.raw(['symbolic-ref', '--short', 'HEAD'], (err, branch) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n      resolve(branch.trim());\n    });\n  });\n};\n\nconst checkoutGitBranch = async (win, { gitRootPath, branchName, processUid, shouldCreate = false }) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    git.outputHandler(handleGitOutput({ win, processUid }));\n\n    const checkoutArgs = shouldCreate ? ['-b', branchName, '--progress'] : branchName;\n    git.checkout(checkoutArgs, (err, res) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n      resolve(res);\n    });\n  });\n};\n\nconst checkoutRemoteGitBranch = async (win, { gitRootPath, remoteName, branchName, processUid }) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    git.outputHandler(handleGitOutput({ win, processUid }));\n\n    const remoteBranchName = `${remoteName}/${branchName}`;\n\n    // Check if the remote branch exists\n    git.branch(['-r'], async (err, branches) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n\n      const remoteBranches = branches.all.map((branch) => branch.trim());\n      const remoteBranchExists = remoteBranches.includes(remoteBranchName);\n\n      if (remoteBranchExists) {\n        try {\n          const localBranches = await getCollectionGitBranches(gitRootPath);\n          const localBranchExists = localBranches.includes(branchName);\n          if (localBranchExists) {\n            // Set the local branch to track the remote branch\n            git.branch(['--set-upstream-to', remoteBranchName, branchName], async (err, res) => {\n              if (err) {\n                reject(err);\n              } else {\n                git.checkout(branchName, (err, res) => {\n                  if (err) {\n                    reject(err);\n                  } else {\n                    resolve(res);\n                  }\n                });\n              }\n            });\n          } else {\n            git.checkout(['-b', branchName, '--track', remoteBranchName, '--progress'], (err, res) => {\n              if (err) {\n                reject(err);\n              } else {\n                resolve(res);\n              }\n            });\n          }\n        } catch (err) {\n          reject(err);\n        }\n      } else {\n        reject(new Error(`Remote branch ${remoteBranchName} does not exist`));\n      }\n    });\n  });\n};\n\nconst getCollectionGitLogs = async (gitRootPath) => {\n  const git = getSimpleGitInstanceForPath(gitRootPath);\n\n  try {\n    // Get logs with shortstat for file change info\n    const result = await git.raw([\n      'log',\n      '--format=%H|%s|%an|%aI',\n      '--shortstat',\n      '-n', '500'\n    ]);\n\n    if (!result || !result.trim()) {\n      return [];\n    }\n\n    const commits = [];\n    const lines = result.split('\\n');\n    let currentCommit = null;\n\n    for (const line of lines) {\n      const trimmedLine = line.trim();\n      if (!trimmedLine) continue;\n\n      // Check if this is a commit line (contains | separators)\n      if (trimmedLine.includes('|')) {\n        // If we have a pending commit, push it\n        if (currentCommit) {\n          commits.push(currentCommit);\n        }\n\n        const parts = trimmedLine.split('|');\n        if (parts.length >= 4) {\n          currentCommit = {\n            hash: parts[0],\n            message: parts[1],\n            author_name: parts[2],\n            date: parts[3],\n            filesChanged: 0,\n            insertions: 0,\n            deletions: 0\n          };\n        }\n      } else if (currentCommit && trimmedLine.includes('changed')) {\n        // This is a shortstat line, parse it\n        // Format: \" 3 files changed, 45 insertions(+), 12 deletions(-)\"\n        const filesMatch = trimmedLine.match(/(\\d+) files? changed/);\n        const insertionsMatch = trimmedLine.match(/(\\d+) insertions?\\(\\+\\)/);\n        const deletionsMatch = trimmedLine.match(/(\\d+) deletions?\\(-\\)/);\n\n        if (filesMatch) currentCommit.filesChanged = parseInt(filesMatch[1], 10);\n        if (insertionsMatch) currentCommit.insertions = parseInt(insertionsMatch[1], 10);\n        if (deletionsMatch) currentCommit.deletions = parseInt(deletionsMatch[1], 10);\n      }\n    }\n\n    // Push the last commit if exists\n    if (currentCommit) {\n      commits.push(currentCommit);\n    }\n\n    return commits;\n  } catch (err) {\n    console.error('Error getting git logs:', err);\n    return [];\n  }\n};\n\nconst getCollectionGitTagsWithDetails = (gitRootPath) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    git\n      .tags(['-l', '--format=%(refname:short)||%(creatordate)||%(creator)'])\n      .then((tags) => {\n        const tagDetails = tags.all?.map((tag) => {\n          const [name, date, author] = tag.split('||');\n          return { name, date, author };\n        });\n        resolve(tagDetails);\n      })\n      .catch(reject);\n  });\n};\n\nconst canPush = async (gitRootPath) => {\n  const git = getSimpleGitInstanceForPath(gitRootPath);\n  const branch = await git.revparse(['--abbrev-ref', 'HEAD']);\n  const remote = await git.listRemote(['--get-url', 'origin']);\n\n  if (!remote) {\n    throw new Error('Remote not configured');\n  }\n\n  const remoteInfo = await git.lsRemote(['--refs', remote]);\n  const logs = await git.log({ maxCount: 1 });\n  const localHead = logs.latest.hash;\n  const remoteRefs = remoteInfo.split('\\n');\n  const remoteHeads = remoteRefs.reduce((acc, ref) => {\n    const [hash, refName] = ref.split('\\t');\n    acc[refName.replace('refs/heads/', '')] = hash;\n    return acc;\n  }, {});\n  const remoteHead = remoteHeads[branch];\n\n  if (localHead === remoteHead) {\n    return false;\n  }\n\n  return true;\n};\n\nconst pushGitChanges = async (win, { gitRootPath, processUid, remote, remoteBranch }) => {\n  return new Promise(async (resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    git.outputHandler(handleGitOutput({ win, processUid, sendStdout: true }));\n\n    try {\n      // Check if the local branch is tracking a remote branch\n      git.branch((err, branchSummary) => {\n        if (err) {\n          reject(err);\n          return;\n        }\n\n        const currentBranch = branchSummary.branches[remoteBranch];\n\n        if (!currentBranch) {\n          reject(new Error(`Branch ${remoteBranch} does not exist.`));\n          return;\n        }\n\n        const trackingBranch = currentBranch.tracking;\n\n        if (!trackingBranch) {\n          // Set the upstream tracking branch\n          git.push(['--set-upstream', remote, remoteBranch], (err, res) => {\n            if (err) {\n              reject(err);\n            } else {\n              resolve(res);\n            }\n          });\n        } else {\n          // Push the local branch to the remote\n          git.push(remote, remoteBranch, (err, res) => {\n            if (err) {\n              reject(err);\n            } else {\n              resolve(res);\n            }\n          });\n        }\n      });\n    } catch (error) {\n      reject(error);\n    }\n  });\n};\n\nconst pullGitChanges = async (win, data) => {\n  const { gitRootPath, processUid, remote, remoteBranch, strategy } = data;\n  if (strategy !== '--no-rebase' && strategy !== '--ff-only') {\n    throw new Error('Invalid strategy');\n  }\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    git.outputHandler(handleGitOutput({ win, processUid, sendStdout: true })).pull(remote, remoteBranch, [strategy], (err, res) => {\n      if (err) {\n        reject(err);\n      } else {\n        resolve(res);\n      }\n    });\n  });\n};\n\nasync function getChangedFilesInCollectionGit(_gitRootPath, _collectionPath) {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(_gitRootPath);\n    git.status(['--porcelain', _gitRootPath], async (err, status) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n\n      const totalFiles = status?.files?.length || 0;\n      if (totalFiles > 5000) {\n        return resolve({\n          staged: [],\n          unstaged: [],\n          totalFiles,\n          tooManyFiles: true\n        });\n      }\n\n      const unstaged = await Promise.all(\n        status.files\n          .filter(\n            (file) => file.index === '?' || file.index === ' ' || file.working_dir === '?' || file.working_dir === 'M'\n          )\n          .map(async (file) => {\n            return { path: file.path, type: 'unstaged', fileIndex: file.index, working_dir: file.working_dir };\n          })\n      );\n\n      const renamed = await Promise.all(\n        status.renamed.map(async (file) => {\n          return { path: file.to, to: file.to, from: file.from, type: 'renamed', fileIndex: 'R', working_dir: '' };\n        })\n      );\n\n      const staged = await Promise.all(\n        status.files\n          .filter(\n            (file) =>\n              (file.index === 'M' || file.index === 'A' || file.index === 'D')\n              && (file.working_dir === 'M' || file.working_dir === ' ')\n          )\n          .map(async (file) => {\n            return { path: file.path, type: 'staged', fileIndex: file.index, working_dir: file.working_dir };\n          })\n      );\n\n      const conflicted = await Promise.all(\n        status.files.filter((file) => file.index === 'U' || file.working_dir === 'U').map(async (file) => {\n          return { path: file.path, type: 'conflicted', fileIndex: file.index, working_dir: file.working_dir };\n        }) || []\n      );\n\n      resolve({\n        staged: [...staged, ...renamed],\n        unstaged,\n        totalFiles,\n        tooManyFiles: false,\n        conflicted\n      });\n    });\n  });\n}\n\nconst getCollectionGitData = async (gitRootPath, collectionPath) => {\n  if (!gitRootPath) return {};\n  const [branches, currentGitBranch, defaultGitBranch, gitRepoUrl] = await Promise.all([\n    getCollectionGitBranches(gitRootPath),\n    getCurrentGitBranch(gitRootPath),\n    getDefaultGitBranch(gitRootPath),\n    getCollectionGitRepoUrl(gitRootPath)\n  ]);\n\n  const logs = branches.length ? await getCollectionGitLogs(gitRootPath) : [];\n\n  return {\n    gitRootPath,\n    gitRepoUrl,\n    branches,\n    currentGitBranch,\n    defaultGitBranch,\n    logs\n  };\n};\n\nconst cloneGitRepository = async (win, data) => {\n  return new Promise((resolve, reject) => {\n    const { url, path, processUid } = data;\n    const git = getSimpleGitInstanceForPath(path);\n\n    git.outputHandler(handleGitOutput({ win, processUid, sendStdout: true }));\n    git.clone(url, path, ['--progress'], (err, res) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n      resolve(res);\n    });\n  });\n};\n\nconst fetchRemotes = (gitRootPath) => {\n  return new Promise((resolve, reject) => {\n    if (!gitRootPath) return resolve([]);\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    git.getRemotes(true)\n      .then((remoteList) => {\n        resolve(remoteList);\n      })\n      .catch((err) => {\n        console.error('Error fetching remotes:', err);\n        reject(err);\n      });\n  });\n};\n\nconst fetchChanges = (gitRootPath, remote = 'origin') => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    git.fetch(remote, (err, res) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n      resolve(res);\n    });\n  });\n};\n\nconst fetchRemoteBranches = ({ gitRootPath, remote }) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    git.branch(['-r'], (err, branches) => {\n      if (err) {\n        reject(err);\n      } else {\n        const branchNames = branches?.all\n          .filter((branch) => branch.startsWith(`${remote}/`))\n          .map((branch) => branch.slice(remote.length + 1));\n        resolve(branchNames);\n      }\n    });\n  });\n};\n\nconst addRemote = ({ gitRootPath, remoteName, remoteUrl }) => {\n  return new Promise(async (resolve, reject) => {\n    try {\n      const git = getSimpleGitInstanceForPath(gitRootPath);\n      console.log('Adding remote:', { gitRootPath, remoteName, remoteUrl });\n      await git.addRemote(remoteName, remoteUrl);\n      const remotes = await fetchRemotes(gitRootPath);\n      resolve(remotes);\n    } catch (err) {\n      console.error('Error adding remote:', err);\n      reject(err);\n    }\n  });\n};\n\nconst removeRemote = ({ gitRootPath, remoteName }) => {\n  return new Promise(async (resolve, reject) => {\n    try {\n      const git = getSimpleGitInstanceForPath(gitRootPath);\n      console.log('Removing remote:', { gitRootPath, remoteName });\n      await git.removeRemote(remoteName);\n      const remotes = await fetchRemotes(gitRootPath);\n      resolve(remotes);\n    } catch (err) {\n      console.error('Error removing remote:', err);\n      reject(err);\n    }\n  });\n};\n\nconst getBehindCount = async (gitRootPath) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n\n    // First try to get status which includes tracking info and counts\n    git.status((err, status) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n\n      // Check if we have tracking branch information\n      const trackingBranch = status.tracking;\n      if (!trackingBranch) {\n        // No tracking branch set\n        resolve({\n          behind: 0,\n          commits: []\n        });\n        return;\n      }\n\n      // Use status.behind if available, otherwise calculate manually\n      const behindCount = status.behind || 0;\n\n      if (behindCount === 0) {\n        resolve({\n          behind: 0,\n          commits: []\n        });\n        return;\n      }\n\n      // Get the actual commits that are behind\n      git.log(['HEAD..' + trackingBranch], (err, log) => {\n        if (err) {\n          // If log fails, return the count from status but empty commits\n          resolve({\n            behind: behindCount,\n            commits: []\n          });\n          return;\n        }\n\n        const commits = log.all.map((commit) => ({\n          hash: commit.hash,\n          message: commit.message,\n          author: commit.author_name,\n          time: new Date(commit.date).toLocaleString()\n        }));\n\n        resolve({\n          behind: behindCount,\n          commits\n        });\n      });\n    });\n  });\n};\n\nconst getAheadCount = async (gitRootPath) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n\n    // First try to get status which includes tracking info and counts\n    git.status((err, status) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n\n      // Check if we have tracking branch information\n      const trackingBranch = status.tracking;\n      if (!trackingBranch) {\n        // No tracking branch set - get all local commits as \"ahead\"\n        git.log(['HEAD'], (err, allLog) => {\n          if (err) {\n            resolve({\n              ahead: 0,\n              commits: []\n            });\n            return;\n          }\n\n          const commits = allLog.all.map((commit) => ({\n            hash: commit.hash,\n            message: commit.message,\n            author: commit.author_name,\n            time: new Date(commit.date).toLocaleString()\n          }));\n\n          resolve({\n            ahead: commits.length,\n            commits\n          });\n        });\n        return;\n      }\n\n      // Use status.ahead if available, otherwise calculate manually\n      const aheadCount = status.ahead || 0;\n\n      if (aheadCount === 0) {\n        resolve({\n          ahead: 0,\n          commits: []\n        });\n        return;\n      }\n\n      // Get commits that are ahead (in local but not on remote)\n      git.log([trackingBranch + '..HEAD'], (err, log) => {\n        if (err) {\n          // If remote doesn't exist, get all local commits (they're all \"ahead\")\n          git.log(['HEAD'], (err, allLog) => {\n            if (err) {\n              resolve({\n                ahead: aheadCount,\n                commits: []\n              });\n              return;\n            }\n\n            const commits = allLog.all.map((commit) => ({\n              hash: commit.hash,\n              message: commit.message,\n              author: commit.author_name,\n              time: new Date(commit.date).toLocaleString()\n            }));\n\n            resolve({\n              ahead: aheadCount,\n              commits\n            });\n          });\n          return;\n        }\n\n        const commits = log.all.map((commit) => ({\n          hash: commit.hash,\n          message: commit.message,\n          author: commit.author_name,\n          time: new Date(commit.date).toLocaleString()\n        }));\n\n        resolve({\n          ahead: aheadCount,\n          commits\n        });\n      });\n    });\n  });\n};\n\nconst getAheadBehindCount = async (gitRootPath) => {\n  try {\n    const [behindStatus, aheadStatus] = await Promise.all([\n      getBehindCount(gitRootPath),\n      getAheadCount(gitRootPath)\n    ]);\n\n    return {\n      behind: behindStatus.behind,\n      ahead: aheadStatus.ahead,\n      behindCommits: behindStatus.commits,\n      aheadCommits: aheadStatus.commits\n    };\n  } catch (error) {\n    console.error('Error getting ahead/behind count:', error);\n    // Return safe defaults\n    return {\n      behind: 0,\n      ahead: 0,\n      behindCommits: [],\n      aheadCommits: []\n    };\n  }\n};\n\nconst abortConflictResolution = async (gitRootPath) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    if (fs.existsSync(path.join(gitRootPath, '.git', 'MERGE_HEAD'))) {\n      git.raw(['merge', '--abort'], (err, res) => {\n        if (err) {\n          reject(err);\n        } else {\n          resolve(res);\n        }\n      });\n    } else {\n      reject(new Error('No merge in progress'));\n    }\n  });\n};\n\nconst continueMerge = async (gitRootPath, conflictedFiles, commitMessage) => {\n  return new Promise(async (resolve, reject) => {\n    try {\n      const fsPromises = require('fs/promises');\n\n      // Step 1: Write all conflicted files' final state to disk\n      for (const file of conflictedFiles) {\n        // file.path is relative to gitRootPath, convert to absolute path\n        const fullFilePath = path.join(gitRootPath, file.path);\n\n        // Ensure directory exists\n        const dir = path.dirname(fullFilePath);\n        await fsPromises.mkdir(dir, { recursive: true });\n\n        // Write the resolved content\n        await fsPromises.writeFile(fullFilePath, file.content, 'utf8');\n      }\n\n      // Step 2: Stage the conflicted files\n      const filePaths = conflictedFiles.map((f) => f.path);\n      const fullPaths = filePaths.map((p) => path.join(gitRootPath, p));\n      await stageChanges(gitRootPath, fullPaths);\n\n      // Step 3: Write commit message to .git/MERGE_MSG\n      const mergeMsgPath = path.join(gitRootPath, '.git', 'MERGE_MSG');\n      await fsPromises.writeFile(mergeMsgPath, commitMessage, 'utf8');\n\n      // Step 4: Call git merge --continue\n      exec('git -c core.editor=: merge --continue', { cwd: gitRootPath }, (err, stdout) => {\n        if (err) {\n          reject(err);\n          return;\n        }\n        resolve(stdout);\n      });\n    } catch (error) {\n      reject(error);\n    }\n  });\n};\n\nconst getCommitFiles = async (gitRootPath, commitHash) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    // Get the list of files changed in this commit with stats\n    git.raw(['show', '--stat', '--name-status', '--format=', commitHash], (err, result) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n\n      const lines = result.trim().split('\\n').filter((line) => line.trim());\n      const files = [];\n\n      for (const line of lines) {\n        // Parse name-status format: M<tab>filename or A<tab>filename or D<tab>filename\n        const match = line.match(/^([AMDRC])\\t(.+)$/);\n        if (match) {\n          const [, status, filePath] = match;\n          files.push({\n            path: filePath,\n            status: status === 'A' ? 'added' : status === 'D' ? 'deleted' : status === 'M' ? 'modified' : status === 'R' ? 'renamed' : 'changed'\n          });\n        }\n      }\n\n      resolve(files);\n    });\n  });\n};\n\nconst getCommitFileDiff = async (gitRootPath, commitHash, filePath) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    // Get the diff for a specific file in a commit (compare with parent)\n    git.raw(['show', '--no-prefix', '-p', commitHash, '--', filePath], (err, diff) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n      resolve(diff);\n    });\n  });\n};\n\n/**\n * Get the list of files changed between two commits\n * @param {string} gitRootPath - Path to git repository\n * @param {string} fromCommit - Base commit hash (older)\n * @param {string} toCommit - Target commit hash (newer)\n * @returns {Promise<Array>} List of changed files with status\n */\nconst getCommitCompareFiles = async (gitRootPath, fromCommit, toCommit) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    // Get the list of files changed between two commits\n    git.raw(['diff', '--name-status', fromCommit, toCommit], (err, result) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n\n      const lines = result.trim().split('\\n').filter((line) => line.trim());\n      const files = [];\n\n      for (const line of lines) {\n        // Parse name-status format: M<tab>filename or A<tab>filename or D<tab>filename\n        const match = line.match(/^([AMDRC])\\t(.+)$/);\n        if (match) {\n          const [, status, filePath] = match;\n          files.push({\n            path: filePath,\n            status: status === 'A' ? 'added' : status === 'D' ? 'deleted' : status === 'M' ? 'modified' : status === 'R' ? 'renamed' : 'changed'\n          });\n        }\n      }\n\n      resolve(files);\n    });\n  });\n};\n\n/**\n * Get the diff for a specific file between two commits\n * @param {string} gitRootPath - Path to git repository\n * @param {string} fromCommit - Base commit hash (older)\n * @param {string} toCommit - Target commit hash (newer)\n * @param {string} filePath - Path to the file\n * @returns {Promise<string>} Diff string\n */\nconst getCommitCompareFileDiff = async (gitRootPath, fromCommit, toCommit, filePath) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    // Get the diff for a specific file between two commits\n    git.raw(['diff', '--no-prefix', fromCommit, toCommit, '--', filePath], (err, diff) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n      resolve(diff);\n    });\n  });\n};\n\n/**\n * Get git history for a specific file\n * @param {string} gitRootPath - Path to git repository\n * @param {string} filePath - Path to the file (relative to git root)\n * @returns {Promise<Array>} List of commits that touched this file\n */\nconst getFileGitHistory = async (gitRootPath, filePath) => {\n  const git = getSimpleGitInstanceForPath(gitRootPath);\n\n  try {\n    const result = await git.raw([\n      'log',\n      '--format=%H|%s|%an|%aI',\n      '--follow',\n      '-n', '100',\n      '--', filePath\n    ]);\n\n    if (!result || !result.trim()) {\n      return [];\n    }\n\n    const commits = [];\n    const lines = result.trim().split('\\n');\n\n    for (const line of lines) {\n      if (line.includes('|')) {\n        const [hash, message, author, date] = line.split('|');\n        commits.push({\n          hash,\n          message,\n          author_name: author,\n          date\n        });\n      }\n    }\n\n    return commits;\n  } catch (err) {\n    console.error('Error getting file git history:', err);\n    return [];\n  }\n};\n\n/**\n * Get git graph data for visualization\n * Gets commits from branch with parent info in a single git log call\n * Only includes branch commits that fall within the time range of the main line\n */\n/**\n * Create a new stash with a message\n * @param {string} gitRootPath - Path to git repository\n * @param {string} message - Stash message/identifier\n * @returns {Promise<void>}\n */\nconst createStash = async (gitRootPath, message) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    // Use --include-untracked to stash untracked files as well\n    git.stash(['push', '--include-untracked', '-m', message], (err, result) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n      resolve(result);\n    });\n  });\n};\n\n/**\n * Get stash diff stats (files changed, insertions, deletions)\n * Includes both tracked and untracked files\n * @param {object} git - simple-git instance\n * @param {number} stashIndex - Index of the stash\n * @returns {Promise<object>} Object with filesChanged, insertions, deletions\n */\nconst getStashStats = async (git, stashIndex) => {\n  let filesChanged = 0;\n  let insertions = 0;\n  let deletions = 0;\n\n  try {\n    // Get stats for tracked files\n    const trackedResult = await git.raw(['stash', 'show', '--numstat', `stash@{${stashIndex}}`]);\n\n    if (trackedResult) {\n      const lines = trackedResult.trim().split('\\n').filter((line) => line.trim());\n      filesChanged += lines.length;\n\n      lines.forEach((line) => {\n        const parts = line.split('\\t');\n        if (parts.length >= 2) {\n          const added = parseInt(parts[0], 10) || 0;\n          const removed = parseInt(parts[1], 10) || 0;\n          insertions += added;\n          deletions += removed;\n        }\n      });\n    }\n  } catch (err) {\n    // No tracked changes or error, continue\n  }\n\n  try {\n    // Get stats for untracked files (stored in stash^3)\n    // First check if the third parent exists (untracked files commit)\n    const untrackedResult = await git.raw(['diff', '--numstat', '4b825dc642cb6eb9a060e54bf8d69288fbee4904', `stash@{${stashIndex}}^3`]);\n\n    if (untrackedResult) {\n      const lines = untrackedResult.trim().split('\\n').filter((line) => line.trim());\n      filesChanged += lines.length;\n\n      lines.forEach((line) => {\n        const parts = line.split('\\t');\n        if (parts.length >= 2) {\n          const added = parseInt(parts[0], 10) || 0;\n          // Untracked files are all additions\n          insertions += added;\n        }\n      });\n    }\n  } catch (err) {\n    // No untracked files in stash or stash^3 doesn't exist, that's fine\n  }\n\n  return { filesChanged, insertions, deletions };\n};\n\n/**\n * List all stashes\n * @param {string} gitRootPath - Path to git repository\n * @returns {Promise<Array>} List of stashes with index, message, date, and diff stats\n */\nconst listStashes = async (gitRootPath) => {\n  const git = getSimpleGitInstanceForPath(gitRootPath);\n\n  try {\n    const stashList = await git.stashList();\n    const stashEntries = stashList.all || [];\n\n    // Fetch stats for each stash in parallel\n    const stashesWithStats = await Promise.all(\n      stashEntries.map(async (stash, index) => {\n        const stats = await getStashStats(git, index);\n        return {\n          index: index,\n          hash: stash.hash,\n          message: stash.message,\n          date: stash.date,\n          filesChanged: stats.filesChanged,\n          insertions: stats.insertions,\n          deletions: stats.deletions\n        };\n      })\n    );\n\n    return stashesWithStats;\n  } catch (err) {\n    throw err;\n  }\n};\n\n/**\n * Apply a stash by index (restores changes but keeps stash)\n * @param {string} gitRootPath - Path to git repository\n * @param {number} stashIndex - Index of the stash to apply\n * @returns {Promise<void>}\n */\nconst applyStash = async (gitRootPath, stashIndex) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    git.stash(['apply', `stash@{${stashIndex}}`], (err, result) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n      resolve(result);\n    });\n  });\n};\n\n/**\n * Drop (delete) a stash by index\n * @param {string} gitRootPath - Path to git repository\n * @param {number} stashIndex - Index of the stash to drop\n * @returns {Promise<void>}\n */\nconst dropStash = async (gitRootPath, stashIndex) => {\n  return new Promise((resolve, reject) => {\n    const git = getSimpleGitInstanceForPath(gitRootPath);\n    git.stash(['drop', `stash@{${stashIndex}}`], (err, result) => {\n      if (err) {\n        reject(err);\n        return;\n      }\n      resolve(result);\n    });\n  });\n};\n\n/**\n * Get list of files in a stash (both tracked and untracked)\n * @param {string} gitRootPath - Path to git repository\n * @param {number} stashIndex - Index of the stash\n * @returns {Promise<Array>} List of files with status\n */\nconst getStashFiles = async (gitRootPath, stashIndex) => {\n  const git = getSimpleGitInstanceForPath(gitRootPath);\n  const files = [];\n\n  try {\n    // Get tracked files from stash\n    const trackedResult = await git.raw(['stash', 'show', '--name-status', `stash@{${stashIndex}}`]);\n\n    if (trackedResult) {\n      const lines = trackedResult.trim().split('\\n').filter((line) => line.trim());\n      for (const line of lines) {\n        const match = line.match(/^([AMDRC])\\t(.+)$/);\n        if (match) {\n          const [, status, filePath] = match;\n          files.push({\n            path: filePath,\n            status: status === 'A' ? 'added' : status === 'D' ? 'deleted' : status === 'M' ? 'modified' : status === 'R' ? 'renamed' : 'changed',\n            isUntracked: false\n          });\n        }\n      }\n    }\n  } catch (err) {\n    // No tracked files or error\n  }\n\n  try {\n    // Get untracked files from stash^3\n    const untrackedResult = await git.raw(['ls-tree', '-r', '--name-only', `stash@{${stashIndex}}^3`]);\n\n    if (untrackedResult) {\n      const lines = untrackedResult.trim().split('\\n').filter((line) => line.trim());\n      for (const filePath of lines) {\n        files.push({\n          path: filePath,\n          status: 'added',\n          isUntracked: true\n        });\n      }\n    }\n  } catch (err) {\n    // No untracked files in stash\n  }\n\n  return files;\n};\n\n/**\n * Get diff for a specific file in a stash\n * @param {string} gitRootPath - Path to git repository\n * @param {number} stashIndex - Index of the stash\n * @param {string} filePath - Path to the file\n * @param {boolean} isUntracked - Whether the file is untracked\n * @returns {Promise<string>} Diff string\n */\nconst getStashFileDiff = async (gitRootPath, stashIndex, filePath, isUntracked = false) => {\n  const git = getSimpleGitInstanceForPath(gitRootPath);\n\n  try {\n    if (isUntracked) {\n      // For untracked files, show the full content as a diff against empty\n      const content = await git.raw(['show', `stash@{${stashIndex}}^3:${filePath}`]);\n      // Format as a unified diff showing all lines as additions\n      const lines = content.split('\\n');\n      const diffLines = [\n        `diff --git a/${filePath} b/${filePath}`,\n        'new file mode 100644',\n        '--- /dev/null',\n        `+++ b/${filePath}`,\n        `@@ -0,0 +1,${lines.length} @@`,\n        ...lines.map((line) => `+${line}`)\n      ];\n      return diffLines.join('\\n');\n    } else {\n      // For tracked files, use git diff to compare stash against its parent\n      // stash@{n}^ is the parent commit, stash@{n} is the stash commit\n      const diff = await git.raw(['diff', `stash@{${stashIndex}}^`, `stash@{${stashIndex}}`, '--', filePath]);\n      return diff;\n    }\n  } catch (err) {\n    throw err;\n  }\n};\n\n/**\n * Get file content at a specific commit\n * @param {string} gitRootPath - Path to git repository\n * @param {string} commitHash - Commit hash\n * @param {string} filePath - Path to the file\n * @returns {Promise<string>} File content\n */\nconst getFileContentAtCommit = async (gitRootPath, commitHash, filePath) => {\n  const git = getSimpleGitInstanceForPath(gitRootPath);\n  try {\n    const content = await git.raw(['show', `${commitHash}:${filePath}`]);\n    return content;\n  } catch (err) {\n    // File might not exist at this commit (e.g., newly added file)\n    return null;\n  }\n};\n\n/**\n * Check if file supports visual diff\n * @param {string} filePath - Path to the file\n * @returns {boolean} True if file supports visual diff\n */\nconst supportsVisualDiff = (filePath) => {\n  if (!filePath) return false;\n\n  const fileName = filePath.split('/').pop();\n  const excludedFiles = ['folder.yml', 'folder.bru', 'opencollection.yml', 'collection.bru'];\n  if (excludedFiles.includes(fileName)) {\n    return false;\n  }\n\n  return filePath.endsWith('.bru') || filePath.endsWith('.yml');\n};\n\n/**\n * Parse content for visual diff viewer\n * Uses parseRequest to get consistent BrunoItem structure for both .bru and .yml files\n * @param {string} content - Raw file content\n * @param {string} filePath - Path to the file\n * @returns {object|null} Parsed BrunoItem or null if parsing fails\n */\nconst parseContentForVisualDiff = (content, filePath) => {\n  if (!content) return null;\n  try {\n    if (filePath?.endsWith('.bru')) {\n      return parseRequest(content, { format: 'bru' });\n    } else if (filePath?.endsWith('.yml')) {\n      return parseRequest(content, { format: 'yml' });\n    }\n    return null;\n  } catch (err) {\n    console.error('Error parsing content for visual diff:', err);\n    return null;\n  }\n};\n\n/**\n * Get old and new file content for visual diff\n * @param {string} gitRootPath - Path to git repository\n * @param {string} commitHash - Commit hash\n * @param {string} filePath - Path to the file\n * @returns {Promise<{oldContent: string|null, newContent: string|null, oldParsed: object|null, newParsed: object|null}>} Old and new file content\n */\nconst getFileContentForVisualDiff = async (gitRootPath, commitHash, filePath) => {\n  const git = getSimpleGitInstanceForPath(gitRootPath);\n  const canParseVisualDiff = supportsVisualDiff(filePath);\n\n  try {\n    // Get the new content (at the commit)\n    let newContent = null;\n    try {\n      newContent = await git.raw(['show', `${commitHash}:${filePath}`]);\n    } catch (err) {\n      // File might be deleted in this commit\n      newContent = null;\n    }\n\n    // Get the old content (at the parent commit)\n    let oldContent = null;\n    try {\n      oldContent = await git.raw(['show', `${commitHash}^:${filePath}`]);\n    } catch (err) {\n      // File might not exist in parent (newly added)\n      oldContent = null;\n    }\n\n    // Parse content if applicable\n    let oldParsed = null;\n    let newParsed = null;\n\n    if (canParseVisualDiff) {\n      oldParsed = parseContentForVisualDiff(oldContent, filePath);\n      newParsed = parseContentForVisualDiff(newContent, filePath);\n    }\n\n    return { oldContent, newContent, oldParsed, newParsed };\n  } catch (err) {\n    throw err;\n  }\n};\n\n/**\n * Get old and new file content for visual diff (for staged/unstaged changes)\n * @param {string} gitRootPath - Path to git repository\n * @param {string} filePath - Path to the file (relative to git root)\n * @param {string} type - Type of change: 'staged', 'unstaged', or 'renamed'\n * @returns {Promise<{oldContent: string|null, newContent: string|null, oldParsed: object|null, newParsed: object|null}>} Old and new file content\n */\nconst getWorkingFileContentForVisualDiff = async (gitRootPath, filePath, type) => {\n  const git = getSimpleGitInstanceForPath(gitRootPath);\n  const fullPath = path.join(gitRootPath, filePath);\n  const canParseVisualDiff = supportsVisualDiff(filePath);\n\n  try {\n    let oldContent = null;\n    let newContent = null;\n\n    if (type === 'staged') {\n      // For staged changes: old = HEAD, new = index (staged)\n      try {\n        oldContent = await git.raw(['show', `HEAD:${filePath}`]);\n      } catch (err) {\n        // File might be newly added\n        oldContent = null;\n      }\n\n      try {\n        newContent = await git.raw(['show', `:${filePath}`]);\n      } catch (err) {\n        // File might be deleted\n        newContent = null;\n      }\n    } else if (type === 'unstaged') {\n      // For unstaged changes: old = index or HEAD, new = working directory\n      try {\n        // Try to get from index first (if there are staged changes)\n        oldContent = await git.raw(['show', `:${filePath}`]);\n      } catch (err) {\n        try {\n          // Fall back to HEAD\n          oldContent = await git.raw(['show', `HEAD:${filePath}`]);\n        } catch (err2) {\n          // File might be untracked\n          oldContent = null;\n        }\n      }\n\n      // Read working directory content\n      try {\n        newContent = fs.readFileSync(fullPath, 'utf8');\n      } catch (err) {\n        // File might be deleted\n        newContent = null;\n      }\n    }\n\n    // Parse content if applicable\n    let oldParsed = null;\n    let newParsed = null;\n\n    if (canParseVisualDiff) {\n      oldParsed = parseContentForVisualDiff(oldContent, filePath);\n      newParsed = parseContentForVisualDiff(newContent, filePath);\n    }\n\n    return { oldContent, newContent, oldParsed, newParsed };\n  } catch (err) {\n    throw err;\n  }\n};\n\n/**\n * Get old and new file content for visual diff (for stash)\n * @param {string} gitRootPath - Path to git repository\n * @param {number} stashIndex - Index of the stash\n * @param {string} filePath - Path to the file\n * @param {boolean} isUntracked - Whether the file is untracked\n * @returns {Promise<{oldContent: string|null, newContent: string|null, oldParsed: object|null, newParsed: object|null}>} Old and new file content\n */\nconst getStashFileContentForVisualDiff = async (gitRootPath, stashIndex, filePath, isUntracked = false) => {\n  const git = getSimpleGitInstanceForPath(gitRootPath);\n  const canParseVisualDiff = supportsVisualDiff(filePath);\n\n  try {\n    let oldContent = null;\n    let newContent = null;\n\n    if (isUntracked) {\n      // For untracked files in stash, old is null (didn't exist)\n      oldContent = null;\n      try {\n        newContent = await git.raw(['show', `stash@{${stashIndex}}^3:${filePath}`]);\n      } catch (err) {\n        newContent = null;\n      }\n    } else {\n      // For tracked files: old = stash parent, new = stash content\n      try {\n        oldContent = await git.raw(['show', `stash@{${stashIndex}}^:${filePath}`]);\n      } catch (err) {\n        oldContent = null;\n      }\n\n      try {\n        newContent = await git.raw(['show', `stash@{${stashIndex}}:${filePath}`]);\n      } catch (err) {\n        newContent = null;\n      }\n    }\n\n    // Parse content if applicable\n    let oldParsed = null;\n    let newParsed = null;\n\n    if (canParseVisualDiff) {\n      oldParsed = parseContentForVisualDiff(oldContent, filePath);\n      newParsed = parseContentForVisualDiff(newContent, filePath);\n    }\n\n    return { oldContent, newContent, oldParsed, newParsed };\n  } catch (err) {\n    throw err;\n  }\n};\n\nconst getGitGraph = async (gitRootPath, branchName, limit = 50) => {\n  const git = getSimpleGitInstanceForPath(gitRootPath);\n\n  try {\n    // Get commits with parent info, request one extra to check if there are more\n    const result = await git.raw([\n      'log',\n      '--format=%H|%P|%s|%an|%aI',\n      '--first-parent',\n      '-n', String(limit + 1), // Request one extra to check hasMore\n      branchName || 'HEAD'\n    ]);\n\n    if (!result || !result.trim()) {\n      return { commits: [], branches: [], hasMore: false };\n    }\n\n    const lines = result.trim().split('\\n');\n\n    // Check if there are more commits beyond this page\n    const hasMore = lines.length > limit;\n    const commitLines = hasMore ? lines.slice(0, limit) : lines;\n\n    const commits = [];\n    const mainLineHashes = new Set();\n\n    // Parse main line commits\n    for (const line of commitLines) {\n      const [hash, parents, message, author, date] = line.split('|');\n      const parentList = parents ? parents.split(' ').filter((p) => p) : [];\n      const isMerge = parentList.length > 1;\n\n      commits.push({\n        hash,\n        message,\n        author_name: author,\n        date,\n        isMerge,\n        parents: parentList\n      });\n      mainLineHashes.add(hash);\n    }\n\n    // Get the time range of the main line commits\n    // First commit is newest, last commit is oldest\n    const oldestMainCommitDate = commits.length > 0 ? commits[commits.length - 1].date : null;\n\n    // For merge commits, get branch commits (only within main line time range)\n    const branches = [];\n\n    for (const commit of commits) {\n      if (commit.isMerge && commit.parents[1]) {\n        try {\n          // Build git log command with --since to limit to main line time range\n          const logArgs = [\n            'log',\n            '--format=%H|%P|%s|%an|%aI',\n            '--first-parent',\n            '-n', '50'\n          ];\n\n          // Only include commits since the oldest main line commit\n          if (oldestMainCommitDate) {\n            logArgs.push('--since=' + oldestMainCommitDate);\n          }\n\n          logArgs.push(commit.parents[1]);\n\n          const branchResult = await git.raw(logArgs);\n\n          if (branchResult && branchResult.trim()) {\n            const branchLines = branchResult.trim().split('\\n');\n            const branchCommits = [];\n\n            for (const bLine of branchLines) {\n              const [bHash, bParents, bMessage, bAuthor, bDate] = bLine.split('|');\n\n              // Stop if we hit main line\n              if (mainLineHashes.has(bHash)) break;\n\n              branchCommits.push({\n                hash: bHash,\n                message: bMessage,\n                author_name: bAuthor,\n                date: bDate,\n                parents: bParents ? bParents.split(' ').filter((p) => p) : []\n              });\n            }\n\n            if (branchCommits.length > 0) {\n              branches.push({\n                mergeCommitHash: commit.hash,\n                commits: branchCommits\n              });\n            }\n          }\n        } catch (err) {\n          // Ignore errors for individual branch traversal\n        }\n      }\n    }\n\n    return { commits, branches, hasMore };\n  } catch (err) {\n    console.error('Error getting git graph:', err);\n    return { commits: [], branches: [], hasMore: false };\n  }\n};\n\nmodule.exports = {\n  getCollectionGitRootPath,\n  getCollectionGitRepoUrl,\n  stageChanges,\n  unstageChanges,\n  discardChanges,\n  commitChanges,\n  getChangedFilesInCollectionGit,\n  getCollectionGitBranches,\n  getDefaultGitBranch,\n  getCurrentGitBranch,\n  checkoutGitBranch,\n  getCollectionGitLogs,\n  getCollectionGitTagsWithDetails,\n  canPush,\n  pushGitChanges,\n  pullGitChanges,\n  initGit,\n  getCollectionGitData,\n  getStagedFileDiff,\n  getUnstagedFileDiff,\n  getRenamedFileDiff,\n  cloneGitRepository,\n  fetchChanges,\n  fetchRemotes,\n  fetchRemoteBranches,\n  checkoutRemoteGitBranch,\n  getGitVersion,\n  addRemote,\n  removeRemote,\n  getAheadBehindCount,\n  getAheadCount,\n  getBehindCount,\n  abortConflictResolution,\n  continueMerge,\n  getCommitFiles,\n  getCommitFileDiff,\n  getCommitCompareFiles,\n  getCommitCompareFileDiff,\n  getFileGitHistory,\n  getGitGraph,\n  createStash,\n  listStashes,\n  applyStash,\n  dropStash,\n  getStashFiles,\n  getStashFileDiff,\n  getFileContentAtCommit,\n  getFileContentForVisualDiff,\n  getWorkingFileContentForVisualDiff,\n  getStashFileContentForVisualDiff\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/oauth2-protocol-handler.js",
    "content": "let oauth2AuthorizationRequest = null;\n\nconst registerOauth2AuthorizationRequest = (resolve, reject, debugInfo = null) => {\n  // Cancel any existing pending request\n  if (oauth2AuthorizationRequest) {\n    oauth2AuthorizationRequest.reject(new Error('Authorization cancelled: new request started'));\n  }\n\n  oauth2AuthorizationRequest = {\n    resolve,\n    reject,\n    debugInfo,\n    timestamp: Date.now()\n  };\n};\n\nconst isOauth2AuthorizationRequestInProgress = () => {\n  return oauth2AuthorizationRequest !== null;\n};\n\nconst resolveOauth2AuthorizationRequest = (data) => {\n  if (oauth2AuthorizationRequest) {\n    oauth2AuthorizationRequest.resolve(data);\n    oauth2AuthorizationRequest = null;\n    return true;\n  }\n  return false;\n};\n\nconst rejectOauth2AuthorizationRequest = (error) => {\n  if (oauth2AuthorizationRequest) {\n    oauth2AuthorizationRequest.reject(error);\n    oauth2AuthorizationRequest = null;\n    return true;\n  }\n  return false;\n};\n\nconst cancelOAuth2AuthorizationRequest = () => {\n  return rejectOauth2AuthorizationRequest(new Error('Authorization cancelled by user'));\n};\n\nconst handleOauth2ProtocolUrl = (url) => {\n  try {\n    const urlObj = new URL(url);\n\n    // Add callback URL details to debugInfo if available\n    if (oauth2AuthorizationRequest?.debugInfo) {\n      const callbackRequest = {\n        request: {\n          url: url,\n          method: '',\n          headers: {},\n          error: null\n        },\n        response: {\n          url: url,\n          headers: {},\n          status: '',\n          statusText: 'BRUNO_OAUTH2_PROTOCOL',\n          error: null\n        },\n        fromCache: false,\n        completed: true\n      };\n      oauth2AuthorizationRequest.debugInfo.data.push(callbackRequest);\n    }\n\n    // Check for errors in query params (authorization code flow) or hash (implicit flow)\n    const error = urlObj.searchParams.get('error') || (urlObj.hash ? new URLSearchParams(urlObj.hash.substring(1)).get('error') : null);\n    const errorDescription = urlObj.searchParams.get('error_description') || (urlObj.hash ? new URLSearchParams(urlObj.hash.substring(1)).get('error_description') : null);\n\n    if (error) {\n      const errorData = {\n        message: 'Authorization Failed!',\n        error,\n        errorDescription\n      };\n      rejectOauth2AuthorizationRequest(new Error(JSON.stringify(errorData)));\n      return;\n    }\n\n    // Check if this is an implicit grant (tokens in hash fragment)\n    if (urlObj.hash) {\n      const hash = urlObj.hash.substring(1); // Remove the leading #\n      const hashParams = new URLSearchParams(hash);\n      const accessToken = hashParams.get('access_token');\n\n      if (accessToken) {\n        // Extract tokens from hash fragment for implicit grant\n        const implicitTokens = {\n          access_token: accessToken,\n          token_type: hashParams.get('token_type'),\n          expires_in: hashParams.get('expires_in'),\n          state: hashParams.get('state'),\n          scope: hashParams.get('scope')\n        };\n        resolveOauth2AuthorizationRequest(implicitTokens);\n        return;\n      }\n    }\n\n    // Check for authorization code in query params (authorization code flow)\n    const code = urlObj.searchParams.get('code');\n    if (code) {\n      resolveOauth2AuthorizationRequest(code);\n      return;\n    }\n\n    // No code or access_token found - reject with error\n    rejectOauth2AuthorizationRequest(new Error('Invalid OAuth2 callback: missing code or access_token'));\n  } catch (err) {\n    console.error('Error handling protocol URL:', err);\n    rejectOauth2AuthorizationRequest(err);\n  }\n};\n\nmodule.exports = {\n  registerOauth2AuthorizationRequest,\n  rejectOauth2AuthorizationRequest,\n  cancelOAuth2AuthorizationRequest,\n  isOauth2AuthorizationRequestInProgress,\n  handleOauth2ProtocolUrl\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/oauth2.js",
    "content": "const { get, cloneDeep, filter } = require('lodash');\nconst crypto = require('crypto');\nconst { authorizeUserInWindow } = require('../ipc/network/authorize-user-in-window');\nconst { authorizeUserInSystemBrowser } = require('../ipc/network/authorize-user-in-system-browser');\nconst Oauth2Store = require('../store/oauth2');\nconst { makeAxiosInstance } = require('../ipc/network/axios-instance');\nconst { safeParseJSON, safeStringifyJSON } = require('./common');\nconst { preferencesUtil } = require('../store/preferences');\nconst qs = require('qs');\n\nconst BRUNO_OAUTH2_CALLBACK_URL = 'https://oauth.usebruno.com/callback';\n\nconst oauth2Store = new Oauth2Store();\n\nconst persistOauth2Credentials = ({ collectionUid, url, credentials, credentialsId }) => {\n  if (credentials?.error || !credentials?.access_token) return;\n  const enhancedCredentials = {\n    ...credentials,\n    created_at: Date.now()\n  };\n  oauth2Store.updateCredentialsForCollection({ collectionUid, url, credentials: enhancedCredentials, credentialsId });\n};\n\nconst clearOauth2Credentials = ({ collectionUid, url, credentialsId }) => {\n  oauth2Store.clearCredentialsForCollection({ collectionUid, url, credentialsId });\n};\n\nconst clearOauth2CredentialsByCredentialsId = ({ collectionUid, credentialsId }) => {\n  oauth2Store.clearCredentialsByCredentialsId({ collectionUid, credentialsId });\n};\n\nconst getStoredOauth2Credentials = ({ collectionUid, url, credentialsId }) => {\n  try {\n    const credentials = oauth2Store.getCredentialsForCollection({ collectionUid, url, credentialsId });\n    return credentials;\n  } catch (error) {\n    return null;\n  }\n};\n\nconst isTokenExpired = (credentials) => {\n  if (!credentials?.access_token) {\n    return true;\n  }\n  if (!credentials?.expires_in || !credentials.created_at) {\n    return false;\n  }\n  const expiryTime = credentials.created_at + credentials.expires_in * 1000;\n  return Date.now() > expiryTime;\n};\n\nconst safeParseJSONBuffer = (data) => {\n  return safeParseJSON(Buffer.isBuffer(data) ? data.toString() : data);\n};\n\nconst getCredentialsFromTokenUrl = async ({ requestConfig, certsAndProxyConfig }) => {\n  const { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions } = certsAndProxyConfig;\n  const axiosInstance = makeAxiosInstance({ proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions });\n  let requestDetails = { request: {}, response: {} }, parsedResponseData;\n  try {\n    const response = await axiosInstance(requestConfig);\n    const { url: responseUrl, headers: responseHeaders, status: responseStatus, statusText: responseStatusText, data: responseData, timeline, config } = response || {};\n    const { url: requestUrl, headers: requestHeaders, data: requestData } = config || {};\n    parsedResponseData = safeParseJSONBuffer(responseData);\n    requestDetails = {\n      request: {\n        url: requestUrl,\n        headers: requestHeaders,\n        data: requestData,\n        method: 'POST'\n      },\n      response: {\n        url: responseUrl,\n        headers: responseHeaders,\n        data: parsedResponseData,\n        status: responseStatus,\n        statusText: responseStatusText,\n        timeline\n      }\n    };\n  } catch (error) {\n    if (error.response) {\n      const { response, config } = error;\n      const { url: responseUrl, headers: responseHeaders, status: responseStatus, statusText: responseStatusText, data: responseData, timeline } = response || {};\n      const { url: requestUrl, headers: requestHeaders, data: requestData } = config || {};\n      const errorResponseData = safeStringifyJSON(safeParseJSONBuffer(responseData));\n      requestDetails = {\n        request: {\n          url: requestUrl,\n          headers: requestHeaders,\n          data: requestData,\n          method: 'POST'\n        },\n        response: {\n          url: responseUrl,\n          headers: responseHeaders,\n          data: errorResponseData,\n          status: responseStatus,\n          statusText: responseStatusText,\n          timeline,\n          error: errorResponseData,\n          timestamp: Date.now()\n        }\n      };\n    } else if (error?.code) {\n      // error.config is not available here\n      const { url: requestUrl, headers: requestHeaders, data: requestData } = requestConfig;\n      requestDetails = {\n        request: {\n          url: requestUrl,\n          headers: requestHeaders,\n          data: requestData\n        },\n        response: {\n          status: '-',\n          statusText: error?.code,\n          headers: {},\n          data: safeStringifyJSON(error?.errors),\n          timeline: error?.timeline\n        }\n      };\n    }\n  }\n\n  // Add the axios request and response info as a main request in debugInfo\n  requestDetails = {\n    ...requestDetails,\n    requestId: Date.now().toString(),\n    fromCache: false,\n    completed: true,\n    requests: [] // No sub-requests in this context\n  };\n\n  return { credentials: parsedResponseData, requestDetails };\n};\n\n// AUTHORIZATION CODE\n\nconst getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, forceFetch = false, certsAndProxyConfigForTokenUrl, certsAndProxyConfigForRefreshUrl }) => {\n  let codeVerifier = generateCodeVerifier();\n  let codeChallenge = generateCodeChallenge(codeVerifier);\n\n  let requestCopy = cloneDeep(request);\n  const oAuth = get(requestCopy, 'oauth2', {});\n  const {\n    clientId,\n    clientSecret,\n    callbackUrl,\n    scope,\n    pkce,\n    credentialsPlacement,\n    authorizationUrl,\n    credentialsId,\n    autoRefreshToken,\n    autoFetchToken,\n    additionalParameters\n  } = oAuth;\n  const effectiveCallbackUrl = callbackUrl && callbackUrl.length ? callbackUrl : BRUNO_OAUTH2_CALLBACK_URL;\n  const url = requestCopy?.oauth2?.accessTokenUrl;\n\n  // Validate required fields\n  if (!authorizationUrl) {\n    return {\n      error: 'Authorization URL is required for OAuth2 authorization code flow',\n      credentials: null,\n      url,\n      credentialsId\n    };\n  }\n\n  if (!url) {\n    return {\n      error: 'Access Token URL is required for OAuth2 authorization code flow',\n      credentials: null,\n      url: authorizationUrl,\n      credentialsId\n    };\n  }\n\n  if (!effectiveCallbackUrl) {\n    return {\n      error: 'Callback URL is required for OAuth2 authorization code flow',\n      credentials: null,\n      url,\n      credentialsId\n    };\n  }\n\n  if (!clientId) {\n    return {\n      error: 'Client ID is required for OAuth2 authorization code flow',\n      credentials: null,\n      url,\n      credentialsId\n    };\n  }\n\n  if (!forceFetch) {\n    const storedCredentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId });\n\n    if (storedCredentials) {\n      // Token exists\n      if (!isTokenExpired(storedCredentials)) {\n        // Token is valid, use it\n        return { collectionUid, url, credentials: storedCredentials, credentialsId };\n      } else {\n        // Token is expired\n        if (autoRefreshToken && storedCredentials.refresh_token) {\n          // Try to refresh token\n          try {\n            const refreshedCredentialsData = await refreshOauth2Token({ requestCopy, collectionUid, certsAndProxyConfig: certsAndProxyConfigForRefreshUrl });\n            return { collectionUid, url, credentials: refreshedCredentialsData.credentials, credentialsId };\n          } catch (error) {\n            // Refresh failed\n            clearOauth2Credentials({ collectionUid, url, credentialsId });\n            if (autoFetchToken) {\n              // Proceed to fetch new token\n            } else {\n              // Proceed with expired token\n              return { collectionUid, url, credentials: storedCredentials, credentialsId };\n            }\n          }\n        } else if (autoRefreshToken && !storedCredentials.refresh_token) {\n          // Cannot refresh; try autoFetchToken\n          if (autoFetchToken) {\n            // Proceed to fetch new token\n            clearOauth2Credentials({ collectionUid, url, credentialsId });\n          } else {\n            // Proceed with expired token\n            return { collectionUid, url, credentials: storedCredentials, credentialsId };\n          }\n        } else if (!autoRefreshToken && autoFetchToken) {\n          // Proceed to fetch new token\n          clearOauth2Credentials({ collectionUid, url, credentialsId });\n        } else {\n          // Proceed with expired token\n          return { collectionUid, url, credentials: storedCredentials, credentialsId };\n        }\n      }\n    } else {\n      // No stored credentials\n      if (autoFetchToken && !storedCredentials) {\n        // Proceed to fetch new token\n      } else {\n        // Proceed without token\n        return { collectionUid, url, credentials: storedCredentials, credentialsId };\n      }\n    }\n  }\n\n  // Fetch new token process\n  let { authorizationCode, debugInfo } = await getOAuth2AuthorizationCode(requestCopy, codeChallenge, collectionUid);\n\n  let axiosRequestConfig = {};\n  axiosRequestConfig.method = 'POST';\n  axiosRequestConfig.headers = {\n    'content-type': 'application/x-www-form-urlencoded',\n    'Accept': 'application/json'\n  };\n  if (credentialsPlacement === 'basic_auth_header') {\n    const secret = clientSecret ?? '';\n    axiosRequestConfig.headers['Authorization'] = `Basic ${Buffer.from(`${clientId}:${secret}`).toString('base64')}`;\n  }\n\n  const data = {\n    grant_type: 'authorization_code',\n    code: authorizationCode,\n    redirect_uri: effectiveCallbackUrl\n  };\n  if (credentialsPlacement !== 'basic_auth_header') {\n    data.client_id = clientId;\n  }\n  if (clientSecret && clientSecret.trim() !== '' && credentialsPlacement !== 'basic_auth_header') {\n    data.client_secret = clientSecret;\n  }\n  if (pkce) {\n    data['code_verifier'] = codeVerifier;\n  }\n\n  axiosRequestConfig.url = url;\n  axiosRequestConfig.responseType = 'arraybuffer';\n  // Apply additional parameters to token request\n  if (additionalParameters?.token?.length) {\n    applyAdditionalParameters(axiosRequestConfig, data, additionalParameters.token);\n  }\n  axiosRequestConfig.data = qs.stringify(data);\n  try {\n    const { credentials, requestDetails } = await getCredentialsFromTokenUrl({ requestConfig: axiosRequestConfig, certsAndProxyConfig: certsAndProxyConfigForTokenUrl });\n\n    // Ensure debugInfo.data is initialized\n    if (!debugInfo) {\n      debugInfo = { data: [] };\n    } else if (!debugInfo.data) {\n      debugInfo.data = [];\n    }\n\n    debugInfo.data.push(requestDetails);\n    credentials && persistOauth2Credentials({ collectionUid, url, credentials, credentialsId });\n    return { collectionUid, url, credentials, credentialsId, debugInfo };\n  } catch (error) {\n    return Promise.reject(error);\n  }\n};\n\nconst getOAuth2AuthorizationCode = (request, codeChallenge, collectionUid) => {\n  return new Promise(async (resolve, reject) => {\n    const { oauth2 } = request;\n    const { callbackUrl, clientId, authorizationUrl, scope, state, pkce, accessTokenUrl, additionalParameters } = oauth2;\n    const useSystemBrowser = preferencesUtil.shouldUseSystemBrowser();\n    const effectiveCallbackUrl = callbackUrl && callbackUrl.length ? callbackUrl : BRUNO_OAUTH2_CALLBACK_URL;\n\n    const authorizationUrlWithQueryParams = new URL(authorizationUrl);\n    authorizationUrlWithQueryParams.searchParams.append('response_type', 'code');\n    authorizationUrlWithQueryParams.searchParams.append('client_id', clientId);\n\n    if (effectiveCallbackUrl) {\n      authorizationUrlWithQueryParams.searchParams.append('redirect_uri', effectiveCallbackUrl);\n    }\n\n    if (scope) {\n      authorizationUrlWithQueryParams.searchParams.append('scope', scope);\n    }\n    if (pkce) {\n      authorizationUrlWithQueryParams.searchParams.append('code_challenge', codeChallenge);\n      authorizationUrlWithQueryParams.searchParams.append('code_challenge_method', 'S256');\n    }\n    if (state) {\n      authorizationUrlWithQueryParams.searchParams.append('state', state);\n    }\n    if (additionalParameters?.authorization?.length) {\n      additionalParameters.authorization.forEach((param) => {\n        if (param.enabled && param.name) {\n          if (param.sendIn === 'queryparams') {\n            authorizationUrlWithQueryParams.searchParams.append(param.name, param.value || '');\n          }\n        }\n      });\n    }\n\n    try {\n      const authorizeUrl = authorizationUrlWithQueryParams.toString();\n      const authorizeFunction = useSystemBrowser ? authorizeUserInSystemBrowser : authorizeUserInWindow;\n      const { authorizationCode, debugInfo } = await authorizeFunction({\n        authorizeUrl,\n        callbackUrl: effectiveCallbackUrl,\n        session: oauth2Store.getSessionIdOfCollection({ collectionUid, url: accessTokenUrl }),\n        additionalHeaders: getAdditionalHeaders(additionalParameters?.authorization)\n      });\n      resolve({ authorizationCode, debugInfo });\n    } catch (err) {\n      reject(err);\n    }\n  });\n};\n\nconst getAdditionalHeaders = (params) => {\n  if (!params || !params.length) {\n    return {};\n  }\n\n  const headers = {};\n  params.forEach((param) => {\n    if (param.enabled && param.name && param.sendIn === 'headers') {\n      headers[param.name] = param.value || '';\n    }\n  });\n\n  return headers;\n};\n\n// CLIENT CREDENTIALS\n\nconst getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, forceFetch = false, certsAndProxyConfigForTokenUrl, certsAndProxyConfigForRefreshUrl }) => {\n  let requestCopy = cloneDeep(request);\n  const oAuth = get(requestCopy, 'oauth2', {});\n  const {\n    clientId,\n    clientSecret,\n    scope,\n    credentialsPlacement,\n    credentialsId,\n    autoRefreshToken,\n    autoFetchToken,\n    additionalParameters\n  } = oAuth;\n\n  const url = requestCopy?.oauth2?.accessTokenUrl;\n\n  // Validate required fields\n  if (!url) {\n    return {\n      error: 'Access Token URL is required for OAuth2 client credentials flow',\n      credentials: null,\n      url,\n      credentialsId\n    };\n  }\n\n  if (!clientId) {\n    return {\n      error: 'Client ID is required for OAuth2 client credentials flow',\n      credentials: null,\n      url,\n      credentialsId\n    };\n  }\n\n  if (!forceFetch) {\n    const storedCredentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId });\n\n    if (storedCredentials) {\n      // Token exists\n      if (!isTokenExpired(storedCredentials)) {\n        // Token is valid, use it\n        return { collectionUid, url, credentials: storedCredentials, credentialsId };\n      } else {\n        // Token is expired\n        if (autoRefreshToken && storedCredentials.refresh_token) {\n          // Try to refresh token\n          try {\n            const refreshedCredentialsData = await refreshOauth2Token({ requestCopy, collectionUid, certsAndProxyConfig: certsAndProxyConfigForRefreshUrl });\n            return { collectionUid, url, credentials: refreshedCredentialsData.credentials, credentialsId };\n          } catch (error) {\n            clearOauth2Credentials({ collectionUid, url, credentialsId });\n            if (autoFetchToken) {\n              // Proceed to fetch new token\n            } else {\n              // Proceed with expired token\n              return { collectionUid, url, credentials: storedCredentials, credentialsId };\n            }\n          }\n        } else if (autoRefreshToken && !storedCredentials.refresh_token) {\n          if (autoFetchToken) {\n            // Proceed to fetch new token\n            clearOauth2Credentials({ collectionUid, url, credentialsId });\n          } else {\n            // Proceed with expired token\n            return { collectionUid, url, credentials: storedCredentials, credentialsId };\n          }\n        } else if (!autoRefreshToken && autoFetchToken) {\n          // Proceed to fetch new token\n          clearOauth2Credentials({ collectionUid, url, credentialsId });\n        } else {\n          // Proceed with expired token\n          return { collectionUid, url, credentials: storedCredentials, credentialsId };\n        }\n      }\n    } else {\n      // No stored credentials\n      if (autoFetchToken && !storedCredentials) {\n        // Proceed to fetch new token\n      } else {\n        // Proceed without token\n        return { collectionUid, url, credentials: storedCredentials, credentialsId };\n      }\n    }\n  }\n\n  // Fetch new token process\n  let axiosRequestConfig = {};\n  axiosRequestConfig.method = 'POST';\n  axiosRequestConfig.headers = {\n    'content-type': 'application/x-www-form-urlencoded',\n    'Accept': 'application/json'\n  };\n  if (credentialsPlacement === 'basic_auth_header') {\n    const secret = clientSecret ?? '';\n    axiosRequestConfig.headers['Authorization'] = `Basic ${Buffer.from(`${clientId}:${secret}`).toString('base64')}`;\n  }\n  const data = {\n    grant_type: 'client_credentials'\n  };\n  if (credentialsPlacement !== 'basic_auth_header') {\n    data.client_id = clientId;\n  }\n  if (clientSecret && clientSecret.trim() !== '' && credentialsPlacement !== 'basic_auth_header') {\n    data.client_secret = clientSecret;\n  }\n  if (scope && scope.trim() !== '') {\n    data.scope = scope;\n  }\n  axiosRequestConfig.url = url;\n  axiosRequestConfig.responseType = 'arraybuffer';\n  if (additionalParameters?.token?.length) {\n    applyAdditionalParameters(axiosRequestConfig, data, additionalParameters.token);\n  }\n  axiosRequestConfig.data = qs.stringify(data);\n  let debugInfo = { data: [] };\n  try {\n    const { credentials, requestDetails } = await getCredentialsFromTokenUrl({ requestConfig: axiosRequestConfig, certsAndProxyConfig: certsAndProxyConfigForTokenUrl });\n    debugInfo.data.push(requestDetails);\n    credentials && persistOauth2Credentials({ collectionUid, url, credentials, credentialsId });\n    return { collectionUid, url, credentials, credentialsId, debugInfo };\n  } catch (error) {\n    return Promise.reject(safeStringifyJSON(error?.response?.data));\n  }\n};\n\n// PASSWORD CREDENTIALS\n\nconst getOAuth2TokenUsingPasswordCredentials = async ({ request, collectionUid, forceFetch = false, certsAndProxyConfigForTokenUrl, certsAndProxyConfigForRefreshUrl }) => {\n  let requestCopy = cloneDeep(request);\n  const oAuth = get(requestCopy, 'oauth2', {});\n  const {\n    username,\n    password,\n    clientId,\n    clientSecret,\n    scope,\n    credentialsPlacement,\n    credentialsId,\n    autoRefreshToken,\n    autoFetchToken,\n    additionalParameters\n  } = oAuth;\n  const url = requestCopy?.oauth2?.accessTokenUrl;\n\n  // Validate required fields\n  if (!url) {\n    return {\n      error: 'Access Token URL is required for OAuth2 password credentials flow',\n      credentials: null,\n      url,\n      credentialsId\n    };\n  }\n\n  if (!username) {\n    return {\n      error: 'Username is required for OAuth2 password credentials flow',\n      credentials: null,\n      url,\n      credentialsId\n    };\n  }\n\n  if (!password) {\n    return {\n      error: 'Password is required for OAuth2 password credentials flow',\n      credentials: null,\n      url,\n      credentialsId\n    };\n  }\n\n  if (!clientId) {\n    return {\n      error: 'Client ID is required for OAuth2 password credentials flow',\n      credentials: null,\n      url,\n      credentialsId\n    };\n  }\n\n  if (!forceFetch) {\n    const storedCredentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId });\n\n    if (storedCredentials) {\n      // Token exists\n      if (!isTokenExpired(storedCredentials)) {\n        // Token is valid, use it\n        return { collectionUid, url, credentials: storedCredentials, credentialsId };\n      } else {\n        // Token is expired\n        if (autoRefreshToken && storedCredentials.refresh_token) {\n          // Try to refresh token\n          try {\n            const refreshedCredentialsData = await refreshOauth2Token({ requestCopy, collectionUid, certsAndProxyConfig: certsAndProxyConfigForRefreshUrl });\n            return { collectionUid, url, credentials: refreshedCredentialsData.credentials, credentialsId };\n          } catch (error) {\n            clearOauth2Credentials({ collectionUid, url, credentialsId });\n            if (autoFetchToken) {\n              // Proceed to fetch new token\n            } else {\n              // Proceed with expired token\n              return { collectionUid, url, credentials: storedCredentials, credentialsId };\n            }\n          }\n        } else if (autoRefreshToken && !storedCredentials.refresh_token) {\n          // Cannot refresh; try autoFetchToken\n          if (autoFetchToken) {\n            // Proceed to fetch new token\n            clearOauth2Credentials({ collectionUid, url, credentialsId });\n          } else {\n            // Proceed with expired token\n            return { collectionUid, url, credentials: storedCredentials, credentialsId };\n          }\n        } else if (!autoRefreshToken && autoFetchToken) {\n          // Proceed to fetch new token\n          clearOauth2Credentials({ collectionUid, url, credentialsId });\n        } else {\n          // Proceed with expired token\n          return { collectionUid, url, credentials: storedCredentials, credentialsId };\n        }\n      }\n    } else {\n      // No stored credentials\n      if (autoFetchToken && !storedCredentials) {\n        // Proceed to fetch new token\n      } else {\n        // Proceed without token\n        return { collectionUid, url, credentials: storedCredentials, credentialsId };\n      }\n    }\n  }\n\n  // Fetch new token process\n  let axiosRequestConfig = {};\n  axiosRequestConfig.method = 'POST';\n  axiosRequestConfig.headers = {\n    'content-type': 'application/x-www-form-urlencoded',\n    'Accept': 'application/json'\n  };\n  if (credentialsPlacement === 'basic_auth_header') {\n    const secret = clientSecret ?? '';\n    axiosRequestConfig.headers['Authorization'] = `Basic ${Buffer.from(`${clientId}:${secret}`).toString('base64')}`;\n  }\n  const data = {\n    grant_type: 'password',\n    username,\n    password\n  };\n  if (credentialsPlacement !== 'basic_auth_header') {\n    data.client_id = clientId;\n  }\n  if (clientSecret && clientSecret.trim() !== '' && credentialsPlacement !== 'basic_auth_header') {\n    data.client_secret = clientSecret;\n  }\n  if (scope && scope.trim() !== '') {\n    data.scope = scope;\n  }\n  axiosRequestConfig.url = url;\n  axiosRequestConfig.responseType = 'arraybuffer';\n  if (additionalParameters?.token?.length) {\n    applyAdditionalParameters(axiosRequestConfig, data, additionalParameters.token);\n  }\n  axiosRequestConfig.data = qs.stringify(data);\n  let debugInfo = { data: [] };\n  try {\n    const { credentials, requestDetails } = await getCredentialsFromTokenUrl({ requestConfig: axiosRequestConfig, certsAndProxyConfig: certsAndProxyConfigForTokenUrl });\n    debugInfo.data.push(requestDetails);\n    credentials && persistOauth2Credentials({ collectionUid, url, credentials, credentialsId });\n    return { collectionUid, url, credentials, credentialsId, debugInfo };\n  } catch (error) {\n    return Promise.reject(safeStringifyJSON(error?.response?.data));\n  }\n};\n\nconst refreshOauth2Token = async ({ requestCopy, collectionUid, certsAndProxyConfig }) => {\n  const oAuth = get(requestCopy, 'oauth2', {});\n  const { clientId, clientSecret, credentialsId, credentialsPlacement, additionalParameters } = oAuth;\n  const url = oAuth.refreshTokenUrl ? oAuth.refreshTokenUrl : oAuth.accessTokenUrl;\n\n  const credentials = getStoredOauth2Credentials({ collectionUid, url, credentialsId });\n  if (!credentials?.refresh_token) {\n    clearOauth2Credentials({ collectionUid, url, credentialsId });\n    // Proceed without token\n    return { collectionUid, url, credentials: null, credentialsId };\n  } else {\n    const data = {\n      grant_type: 'refresh_token',\n      refresh_token: credentials.refresh_token\n    };\n    if (credentialsPlacement !== 'basic_auth_header') {\n      data.client_id = clientId;\n    }\n    if (clientSecret && clientSecret.trim() !== '' && credentialsPlacement !== 'basic_auth_header') {\n      data.client_secret = clientSecret;\n    }\n    let axiosRequestConfig = {};\n    axiosRequestConfig.method = 'POST';\n    axiosRequestConfig.headers = {\n      'content-type': 'application/x-www-form-urlencoded',\n      'Accept': 'application/json'\n    };\n    if (credentialsPlacement === 'basic_auth_header') {\n      const secret = clientSecret ?? '';\n      axiosRequestConfig.headers['Authorization'] = `Basic ${Buffer.from(`${clientId}:${secret}`).toString('base64')}`;\n    }\n    axiosRequestConfig.url = url;\n    axiosRequestConfig.responseType = 'arraybuffer';\n    if (additionalParameters?.refresh?.length) {\n      applyAdditionalParameters(axiosRequestConfig, data, additionalParameters.refresh);\n    }\n    axiosRequestConfig.data = qs.stringify(data);\n    let debugInfo = { data: [] };\n    try {\n      const { credentials, requestDetails } = await getCredentialsFromTokenUrl({ requestConfig: axiosRequestConfig, certsAndProxyConfig });\n      debugInfo.data.push(requestDetails);\n      if (!credentials || credentials?.error) {\n        clearOauth2Credentials({ collectionUid, url, credentialsId });\n        return { collectionUid, url, credentials: null, credentialsId, debugInfo };\n      }\n      credentials && persistOauth2Credentials({ collectionUid, url, credentials, credentialsId });\n      return { collectionUid, url, credentials, credentialsId, debugInfo };\n    } catch (error) {\n      clearOauth2Credentials({ collectionUid, url, credentialsId });\n      // Proceed without token\n      return { collectionUid, url, credentials: null, credentialsId, debugInfo };\n    }\n  }\n};\n\n// HELPER FUNCTIONS\n\nconst generateCodeVerifier = () => {\n  return crypto.randomBytes(22).toString('hex');\n};\n\nconst generateCodeChallenge = (codeVerifier) => {\n  const hash = crypto.createHash('sha256');\n  hash.update(codeVerifier);\n  const base64Hash = hash.digest('base64')\n    .replace(/\\+/g, '-')\n    .replace(/\\//g, '_')\n    .replace(/=/g, '');\n  return base64Hash;\n};\n\n// Apply additional parameters to a request\nconst applyAdditionalParameters = (requestCopy, data, params = []) => {\n  params.forEach((param) => {\n    if (!param.enabled || !param.name) {\n      return;\n    }\n\n    switch (param.sendIn) {\n      case 'headers':\n        requestCopy.headers[param.name] = param.value || '';\n        break;\n      case 'queryparams':\n        // For query params, add to URL\n        try {\n          let url = new URL(requestCopy.url);\n          url.searchParams.append(param.name, param.value || '');\n          requestCopy.url = url.href;\n        } catch (error) {\n          console.error('invalid token/refresh url', requestCopy.url);\n        }\n        break;\n      case 'body':\n        // For body, add to data object\n        data[param.name] = param.value || '';\n        break;\n    }\n  });\n};\n\nconst getOAuth2TokenUsingImplicitGrant = async ({ request, collectionUid, forceFetch = false }) => {\n  const { oauth2 = {} } = request;\n  const {\n    authorizationUrl,\n    clientId,\n    scope,\n    state = '',\n    callbackUrl,\n    credentialsId = 'credentials',\n    autoFetchToken = true,\n    additionalParameters\n  } = oauth2;\n  const useSystemBrowser = preferencesUtil.shouldUseSystemBrowser();\n  const effectiveCallbackUrl = callbackUrl && callbackUrl.length ? callbackUrl : BRUNO_OAUTH2_CALLBACK_URL;\n\n  // Validate required fields\n  if (!authorizationUrl) {\n    return {\n      error: 'Authorization URL is required for OAuth2 implicit flow',\n      credentials: null,\n      url: authorizationUrl,\n      credentialsId\n    };\n  }\n\n  if (!effectiveCallbackUrl) {\n    return {\n      error: 'Callback URL is required for OAuth2 implicit flow',\n      credentials: null,\n      url: authorizationUrl,\n      credentialsId\n    };\n  }\n\n  // Check if we already have valid credentials\n  if (!forceFetch) {\n    try {\n      const storedCredentials = getStoredOauth2Credentials({\n        collectionUid,\n        url: authorizationUrl,\n        credentialsId\n      });\n\n      if (storedCredentials) {\n        // Token exists\n        if (!isTokenExpired(storedCredentials)) {\n          // Token is valid, use it\n          return {\n            collectionUid,\n            credentials: storedCredentials,\n            url: authorizationUrl,\n            credentialsId\n          };\n        } else {\n          // Token is expired - unlike other grant types, implicit flow doesn't support refresh tokens\n          if (autoFetchToken) {\n            // Proceed to fetch new token\n            clearOauth2Credentials({ collectionUid, url: authorizationUrl, credentialsId });\n          } else {\n            // Proceed with expired token\n            return {\n              collectionUid,\n              credentials: storedCredentials,\n              url: authorizationUrl,\n              credentialsId\n            };\n          }\n        }\n      } else {\n        // No stored credentials\n        if (!autoFetchToken) {\n          // Don't fetch token if autoFetchToken is disabled\n          return {\n            collectionUid,\n            credentials: null,\n            url: authorizationUrl,\n            credentialsId\n          };\n        }\n        // Otherwise proceed to fetch new token\n      }\n    } catch (error) {\n      console.error('Error retrieving oauth2 credentials from cache', error);\n      clearOauth2Credentials({ collectionUid, url: authorizationUrl, credentialsId });\n    }\n  }\n\n  const authorizationUrlWithQueryParams = new URL(authorizationUrl);\n  authorizationUrlWithQueryParams.searchParams.append('response_type', 'token');\n  authorizationUrlWithQueryParams.searchParams.append('client_id', clientId);\n\n  if (effectiveCallbackUrl) {\n    authorizationUrlWithQueryParams.searchParams.append('redirect_uri', effectiveCallbackUrl);\n  }\n\n  if (scope) {\n    authorizationUrlWithQueryParams.searchParams.append('scope', scope);\n  }\n  if (state) {\n    authorizationUrlWithQueryParams.searchParams.append('state', state);\n  }\n  if (additionalParameters?.authorization?.length) {\n    additionalParameters.authorization.forEach((param) => {\n      if (param.enabled && param.name) {\n        if (param.sendIn === 'queryparams') {\n          authorizationUrlWithQueryParams.searchParams.append(param.name, param.value || '');\n        }\n      }\n    });\n  }\n\n  const authorizeUrl = authorizationUrlWithQueryParams.toString();\n\n  try {\n    const authorizeFunction = useSystemBrowser ? authorizeUserInSystemBrowser : authorizeUserInWindow;\n    const result = await authorizeFunction({\n      authorizeUrl,\n      callbackUrl: effectiveCallbackUrl,\n      session: oauth2Store.getSessionIdOfCollection({ collectionUid, url: authorizationUrl }),\n      grantType: 'implicit',\n      additionalHeaders: getAdditionalHeaders(additionalParameters?.authorization)\n    });\n\n    const { implicitTokens, debugInfo } = result;\n\n    if (!implicitTokens || !implicitTokens.access_token) {\n      return {\n        error: 'No access token received from authorization server',\n        credentials: null,\n        url: authorizationUrl,\n        credentialsId,\n        debugInfo\n      };\n    }\n\n    const credentials = {\n      access_token: implicitTokens.access_token,\n      token_type: implicitTokens.token_type || 'Bearer',\n      state: implicitTokens.state || '',\n      ...(implicitTokens.expires_in ? { expires_in: parseInt(implicitTokens.expires_in) } : {}),\n      created_at: Date.now()\n    };\n\n    if (implicitTokens.scope) {\n      credentials.scope = implicitTokens.scope;\n    }\n\n    // Store the credentials\n    persistOauth2Credentials({\n      collectionUid,\n      url: authorizationUrl,\n      credentials,\n      credentialsId\n    });\n\n    return {\n      collectionUid,\n      credentials,\n      url: authorizationUrl,\n      credentialsId,\n      debugInfo\n    };\n  } catch (error) {\n    return {\n      error: error.message || 'Failed to obtain token',\n      credentials: null,\n      url: authorizationUrl,\n      credentialsId\n    };\n  }\n};\n\nconst updateCollectionOauth2Credentials = ({ collectionUid, itemUid, collectionOauth2Credentials = [], requestOauth2Credentials = {} }) => {\n  const { url, credentialsId, folderUid, credentials, debugInfo } = requestOauth2Credentials;\n\n  // Remove existing credentials for the same combination\n  const filteredOauth2Credentials = filter(cloneDeep(collectionOauth2Credentials),\n    (creds) =>\n      !(creds.url === url\n        && creds.collectionUid === collectionUid\n        && creds.credentialsId === credentialsId));\n\n  // Add the new credential with folderUid and itemUid\n  filteredOauth2Credentials.push({\n    collectionUid,\n    folderUid: folderUid,\n    itemUid: folderUid ? null : itemUid,\n    url,\n    credentials,\n    credentialsId,\n    debugInfo\n  });\n\n  return filteredOauth2Credentials;\n};\n\nmodule.exports = {\n  persistOauth2Credentials,\n  clearOauth2Credentials,\n  clearOauth2CredentialsByCredentialsId,\n  getStoredOauth2Credentials,\n  getOAuth2TokenUsingAuthorizationCode,\n  getOAuth2TokenUsingClientCredentials,\n  getOAuth2TokenUsingPasswordCredentials,\n  getOAuth2TokenUsingImplicitGrant,\n  refreshOauth2Token,\n  generateCodeVerifier,\n  generateCodeChallenge,\n  updateCollectionOauth2Credentials\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/parse.js",
    "content": "const { parseRequestAndRedactBody, parseRequestViaWorker, DEFAULT_COLLECTION_FORMAT } = require('@usebruno/filestore');\n\n/**\n * Parses a large BRU request string by redacting body blocks, parsing the remainder,\n * and then reinserting extracted body content into the parsed structure.\n * @param {string} bruContent\n * @param {string} format - Collection format, defaults to 'bru'\n * @returns {Promise<any>} parsed request JSON\n */\nasync function parseLargeRequestWithRedaction(bruContent, format = DEFAULT_COLLECTION_FORMAT) {\n  const { bruFileStringWithRedactedBody, extractedBodyContent } = parseRequestAndRedactBody(bruContent, { format });\n  const parsedData = await parseRequestViaWorker(bruFileStringWithRedactedBody, { format });\n\n  if (!parsedData.request) {\n    parsedData.request = {};\n  }\n  if (!parsedData.request.body) {\n    parsedData.request.body = {};\n  }\n\n  if (extractedBodyContent.json) {\n    parsedData.request.body.json = extractedBodyContent.json;\n  }\n  if (extractedBodyContent.text) {\n    parsedData.request.body.text = extractedBodyContent.text;\n  }\n  if (extractedBodyContent.xml) {\n    parsedData.request.body.xml = extractedBodyContent.xml;\n  }\n  if (extractedBodyContent.sparql) {\n    parsedData.request.body.sparql = extractedBodyContent.sparql;\n  }\n  if (extractedBodyContent.graphql) {\n    if (!parsedData.request.body.graphql) {\n      parsedData.request.body.graphql = {};\n    }\n    parsedData.request.body.graphql.query = extractedBodyContent.graphql;\n  }\n\n  return parsedData;\n}\n\nmodule.exports = { parseLargeRequestWithRedaction };\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/proxy-util.js",
    "content": "const parseUrl = require('url').parse;\nconst https = require('node:https');\nconst http = require('node:http');\nconst { HttpsProxyAgent } = require('https-proxy-agent');\nconst { interpolateString } = require('../ipc/network/interpolate-string');\nconst { SocksProxyAgent } = require('socks-proxy-agent');\nconst { HttpProxyAgent } = require('http-proxy-agent');\nconst { isEmpty, get, isUndefined, isNull } = require('lodash');\nconst { getOrCreateHttpsAgent, getOrCreateHttpAgent } = require('@usebruno/requests');\nconst { preferencesUtil } = require('../store/preferences');\n\nconst DEFAULT_PORTS = {\n  ftp: 21,\n  gopher: 70,\n  http: 80,\n  https: 443,\n  ws: 80,\n  wss: 443\n};\n/**\n * check for proxy bypass, copied form 'proxy-from-env'\n */\nconst shouldUseProxy = (url, proxyBypass) => {\n  if (proxyBypass === '*') {\n    return false; // Never proxy if wildcard is set.\n  }\n\n  // use proxy if no proxyBypass is set\n  if (!proxyBypass || typeof proxyBypass !== 'string' || isEmpty(proxyBypass.trim())) {\n    return true;\n  }\n\n  const parsedUrl = typeof url === 'string' ? parseUrl(url) : url || {};\n  let proto = parsedUrl.protocol;\n  let hostname = parsedUrl.host;\n  let port = parsedUrl.port;\n  if (typeof hostname !== 'string' || !hostname || typeof proto !== 'string') {\n    return false; // Don't proxy URLs without a valid scheme or host.\n  }\n\n  proto = proto.split(':', 1)[0];\n  // Stripping ports in this way instead of using parsedUrl.hostname to make\n  // sure that the brackets around IPv6 addresses are kept.\n  hostname = hostname.replace(/:\\d*$/, '');\n  port = parseInt(port) || DEFAULT_PORTS[proto] || 0;\n\n  return proxyBypass.split(/[,;\\s]/).every(function (dontProxyFor) {\n    if (!dontProxyFor) {\n      return true; // Skip zero-length hosts.\n    }\n    const parsedProxy = dontProxyFor.match(/^(.+):(\\d+)$/);\n    let parsedProxyHostname = parsedProxy ? parsedProxy[1] : dontProxyFor;\n    const parsedProxyPort = parsedProxy ? parseInt(parsedProxy[2]) : 0;\n    if (parsedProxyPort && parsedProxyPort !== port) {\n      return true; // Skip if ports don't match.\n    }\n\n    if (!/^[.*]/.test(parsedProxyHostname)) {\n      // No wildcards, so stop proxying if there is an exact match.\n      return hostname !== parsedProxyHostname;\n    }\n\n    if (parsedProxyHostname.charAt(0) === '*') {\n      // Remove leading wildcard.\n      parsedProxyHostname = parsedProxyHostname.slice(1);\n    }\n    // Stop proxying if the hostname ends with the no_proxy host.\n    return !hostname.endsWith(parsedProxyHostname);\n  });\n};\n\n/**\n * Options that should be forwarded from the constructor to the target TLS upgrade.\n */\nconst TARGET_TLS_OPTIONS = ['cert', 'key', 'pfx', 'passphrase', 'rejectUnauthorized', 'secureContext'];\n\n/**\n * Patched version of HttpsProxyAgent that correctly handles TLS options for\n * both the proxy connection and the target server connection.\n *\n * The upstream HttpsProxyAgent (https://github.com/TooTallNate/proxy-agents/issues/194)\n * ignores constructor options when upgrading the tunneled socket to TLS for the\n * target server. This patch forwards the relevant TLS options to the target upgrade.\n */\nclass PatchedHttpsProxyAgent extends HttpsProxyAgent {\n  constructor(proxy, opts) {\n    super(proxy, opts);\n    this.constructorOpts = opts;\n  }\n\n  async connect(req, opts) {\n    const targetOpts = { ...opts };\n\n    if (this.constructorOpts) {\n      for (const key of TARGET_TLS_OPTIONS) {\n        if (key in this.constructorOpts) {\n          targetOpts[key] = this.constructorOpts[key];\n        }\n      }\n    }\n\n    return super.connect(req, targetOpts);\n  }\n}\n\nfunction setupProxyAgents({\n  requestConfig,\n  proxyMode = 'off',\n  proxyConfig,\n  httpsAgentRequestFields,\n  interpolationOptions,\n  timeline\n}) {\n  const disableCache = !preferencesUtil.isSslSessionCachingEnabled();\n\n  // Ensure TLS options are properly set\n  const tlsOptions = {\n    ...httpsAgentRequestFields,\n    // Enable all secure protocols by default\n    secureProtocol: undefined,\n    // Allow Node.js to choose the protocol\n    minVersion: 'TLSv1',\n    rejectUnauthorized: httpsAgentRequestFields.rejectUnauthorized !== undefined ? httpsAgentRequestFields.rejectUnauthorized : true,\n    // Enable keepAlive for connection reuse\n    keepAlive: true\n  };\n\n  const parsedUrl = parseUrl(requestConfig.url);\n  const isHttpsRequest = parsedUrl.protocol === 'https:';\n  const hostname = parsedUrl.hostname || null;\n\n  if (proxyMode === 'on') {\n    const shouldProxy = shouldUseProxy(requestConfig.url, get(proxyConfig, 'bypassProxy', ''));\n    if (shouldProxy) {\n      const proxyProtocol = interpolateString(get(proxyConfig, 'protocol'), interpolationOptions);\n      const proxyHostname = interpolateString(get(proxyConfig, 'hostname'), interpolationOptions);\n      const proxyPort = interpolateString(get(proxyConfig, 'port'), interpolationOptions);\n      const proxyAuthEnabled = !get(proxyConfig, 'auth.disabled', false);\n      const socksEnabled = proxyProtocol.includes('socks');\n\n      let uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;\n      let proxyUri;\n      if (proxyAuthEnabled) {\n        const proxyAuthUsername = encodeURIComponent(interpolateString(get(proxyConfig, 'auth.username'), interpolationOptions));\n        const proxyAuthPassword = encodeURIComponent(interpolateString(get(proxyConfig, 'auth.password'), interpolationOptions));\n        proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;\n      } else {\n        proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;\n      }\n\n      if (timeline) {\n        timeline.push({\n          timestamp: new Date(),\n          type: 'info',\n          message: `Using proxy: ${proxyProtocol}://${proxyHostname}${uriPort}`\n        });\n      }\n\n      // When the proxy itself uses HTTPS, the agent connecting to it needs TLS options\n      // (e.g., ca certs) even for plain HTTP requests\n      const isHttpsProxy = proxyProtocol === 'https';\n      const httpProxyAgentOptions = isHttpsProxy ? { keepAlive: true, ...tlsOptions } : { keepAlive: true };\n\n      // Only set the agent needed for the request protocol\n      if (socksEnabled) {\n        if (isHttpsRequest) {\n          requestConfig.httpsAgent = getOrCreateHttpsAgent({ AgentClass: SocksProxyAgent, options: tlsOptions, proxyUri, timeline, disableCache, hostname });\n        } else {\n          requestConfig.httpAgent = getOrCreateHttpAgent({ AgentClass: SocksProxyAgent, options: httpProxyAgentOptions, proxyUri, timeline, disableCache, hostname });\n        }\n      } else {\n        if (isHttpsRequest) {\n          requestConfig.httpsAgent = getOrCreateHttpsAgent({ AgentClass: PatchedHttpsProxyAgent, options: tlsOptions, proxyUri, timeline, disableCache, hostname });\n        } else {\n          requestConfig.httpAgent = getOrCreateHttpAgent({ AgentClass: HttpProxyAgent, options: httpProxyAgentOptions, proxyUri, timeline, disableCache, hostname });\n        }\n      }\n    }\n  } else if (proxyMode === 'system') {\n    const { http_proxy, https_proxy, no_proxy } = proxyConfig || {};\n    const shouldUseSystemProxy = shouldUseProxy(requestConfig.url, no_proxy || '');\n    if (shouldUseSystemProxy) {\n      try {\n        if (http_proxy?.length && !isHttpsRequest) {\n          const parsedHttpProxy = new URL(http_proxy);\n          const isHttpsSystemProxy = parsedHttpProxy.protocol === 'https:';\n          const systemHttpProxyAgentOptions = isHttpsSystemProxy ? { keepAlive: true, ...tlsOptions } : { keepAlive: true };\n          if (timeline) {\n            timeline.push({\n              timestamp: new Date(),\n              type: 'info',\n              message: `Using system proxy: ${http_proxy}`\n            });\n          }\n          requestConfig.httpAgent = getOrCreateHttpAgent({ AgentClass: HttpProxyAgent, options: systemHttpProxyAgentOptions, proxyUri: http_proxy, timeline, disableCache, hostname });\n        }\n      } catch (error) {\n        throw new Error(`Invalid system http_proxy \"${http_proxy}\": ${error.message}`);\n      }\n      try {\n        if (https_proxy?.length && isHttpsRequest) {\n          new URL(https_proxy);\n          if (timeline) {\n            timeline.push({\n              timestamp: new Date(),\n              type: 'info',\n              message: `Using system proxy: ${https_proxy}`\n            });\n          }\n          requestConfig.httpsAgent = getOrCreateHttpsAgent({ AgentClass: PatchedHttpsProxyAgent, options: tlsOptions, proxyUri: https_proxy, timeline, disableCache, hostname });\n        }\n      } catch (error) {\n        throw new Error(`Invalid system https_proxy \"${https_proxy}\": ${error.message}`);\n      }\n    }\n  }\n\n  if (!requestConfig.httpAgent && !requestConfig.httpsAgent) {\n    if (isHttpsRequest) {\n      requestConfig.httpsAgent = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: tlsOptions, proxyUri: null, timeline, disableCache, hostname });\n    } else {\n      requestConfig.httpAgent = getOrCreateHttpAgent({ AgentClass: http.Agent, options: { keepAlive: true }, proxyUri: null, timeline, disableCache, hostname });\n    }\n  }\n}\n\nmodule.exports = {\n  shouldUseProxy,\n  PatchedHttpsProxyAgent,\n  setupProxyAgents\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/tests/collection-utils.spec.js",
    "content": "const { transformRequestToSaveToFilesystem } = require('../collection');\n\ndescribe('transformRequestToSaveToFilesystem', () => {\n  it('should preserve all relevant fields when transforming request', () => {\n    const testItem = {\n      uid: 'test-uid-123',\n      type: 'http-request',\n      name: 'Test Request',\n      seq: 1,\n      settings: {\n        enableEncodeUrl: true\n      },\n      tags: ['smoke', 'regression', 'api'],\n      request: {\n        method: 'POST',\n        url: 'https://api.example.com/test',\n        params: [\n          {\n            uid: 'param-uid-1',\n            name: 'param1',\n            value: 'value1',\n            description: 'Test parameter',\n            type: 'text',\n            enabled: true\n          }\n        ],\n        headers: [\n          {\n            uid: 'header-uid-1',\n            name: 'Content-Type',\n            value: 'application/json',\n            description: 'Request content type',\n            enabled: true\n          }\n        ],\n        auth: {\n          type: 'bearer',\n          token: 'test-token'\n        },\n        body: {\n          mode: 'json',\n          json: '{\"test\": \"data\"}'\n        },\n        script: {\n          req: 'console.log(\"request script\");',\n          res: 'console.log(\"response script\");'\n        },\n        vars: {\n          preRequest: 'const testVar = \"value\";',\n          postResponse: 'console.log(testVar);'\n        },\n        assertions: [\n          {\n            uid: 'assert-uid-1',\n            name: 'Status Code',\n            operator: 'equals',\n            expected: 200\n          }\n        ],\n        tests: [\n          {\n            uid: 'test-uid-1',\n            name: 'Test Response',\n            code: 'expect(response.status).toEqual(200);'\n          }\n        ],\n        docs: 'This is a test request documentation'\n      }\n    };\n\n    // Transform the request\n    const result = transformRequestToSaveToFilesystem(testItem);\n\n    // Verify all top-level fields are preserved\n    expect(result.uid).toBe(testItem.uid);\n    expect(result.type).toBe(testItem.type);\n    expect(result.name).toBe(testItem.name);\n    expect(result.seq).toBe(testItem.seq);\n    expect(result.settings).toEqual(testItem.settings);\n\n    // Verify tags are preserved (this is the main focus)\n    expect(result.tags).toEqual(['smoke', 'regression', 'api']);\n    expect(result.tags).toHaveLength(3);\n\n    // Verify request object structure\n    expect(result.request).toBeDefined();\n    expect(result.request.method).toBe(testItem.request.method);\n    expect(result.request.url).toBe(testItem.request.url);\n    expect(result.request.auth).toEqual(testItem.request.auth);\n    expect(result.request.body).toEqual(testItem.request.body);\n    expect(result.request.script).toEqual(testItem.request.script);\n    expect(result.request.vars).toEqual(testItem.request.vars);\n    expect(result.request.assertions).toEqual(testItem.request.assertions);\n    expect(result.request.tests).toEqual(testItem.request.tests);\n    expect(result.request.docs).toBe(testItem.request.docs);\n\n    // Verify params are processed correctly\n    expect(result.request.params).toHaveLength(1);\n    expect(result.request.params[0]).toEqual({\n      uid: 'param-uid-1',\n      name: 'param1',\n      value: 'value1',\n      description: 'Test parameter',\n      type: 'text',\n      enabled: true\n    });\n\n    // Verify headers are processed correctly\n    expect(result.request.headers).toHaveLength(1);\n    expect(result.request.headers[0]).toEqual({\n      uid: 'header-uid-1',\n      name: 'Content-Type',\n      value: 'application/json',\n      description: 'Request content type',\n      enabled: true\n    });\n  });\n\n  it('should handle draft items correctly', () => {\n    const testItem = {\n      uid: 'test-uid-456',\n      type: 'http-request',\n      name: 'Draft Request',\n      seq: 2,\n      settings: {},\n      tags: ['draft', 'wip'],\n      request: {\n        method: 'GET',\n        url: 'https://api.example.com/draft',\n        params: [],\n        headers: [],\n        auth: {},\n        body: { mode: 'none' },\n        script: { req: '', res: '' },\n        vars: { preRequest: '', postResponse: '' },\n        assertions: [],\n        tests: [],\n        docs: ''\n      },\n      draft: {\n        uid: 'draft-uid-789',\n        type: 'http-request',\n        name: 'Draft Request Modified',\n        seq: 2,\n        settings: { enableEncodeUrl: true },\n        tags: ['draft', 'wip', 'modified'],\n        request: {\n          method: 'PUT',\n          url: 'https://api.example.com/draft-modified',\n          params: [],\n          headers: [],\n          auth: {},\n          body: { mode: 'none' },\n          script: { req: '', res: '' },\n          vars: { preRequest: '', postResponse: '' },\n          assertions: [],\n          tests: [],\n          docs: ''\n        }\n      }\n    };\n\n    const result = transformRequestToSaveToFilesystem(testItem);\n\n    // Should use draft data when available\n    expect(result.uid).toBe('draft-uid-789');\n    expect(result.name).toBe('Draft Request Modified');\n    expect(result.settings).toEqual({ enableEncodeUrl: true });\n\n    // Verify draft tags are preserved\n    expect(result.tags).toEqual(['draft', 'wip', 'modified']);\n    expect(result.tags).toContain('modified');\n    expect(result.tags).toHaveLength(3);\n  });\n\n  it('should handle gRPC requests', () => {\n    const testItem = {\n      uid: 'grpc-uid-123',\n      type: 'grpc-request',\n      name: 'gRPC Test Request',\n      seq: 3,\n      settings: {},\n      tags: ['grpc', 'microservice'],\n      request: {\n        method: 'unary',\n        methodType: 'unary',\n        protoPath: '/path/to/proto',\n        url: 'grpc://localhost:50051',\n        params: [], // gRPC requests don't use params\n        headers: [],\n        auth: {},\n        body: { mode: 'grpc', grpc: [{ name: 'message1', content: 'test content' }] },\n        script: { req: '', res: '' },\n        vars: { preRequest: '', postResponse: '' },\n        assertions: [],\n        tests: [],\n        docs: 'gRPC test documentation'\n      }\n    };\n\n    const result = transformRequestToSaveToFilesystem(testItem);\n\n    // Verify gRPC-specific fields\n    expect(result.type).toBe('grpc-request');\n    expect(result.request.methodType).toBe('unary');\n    expect(result.request.protoPath).toBe('/path/to/proto');\n    expect(result.request.params).toBeUndefined(); // Should be deleted for gRPC\n\n    // Verify tags are preserved for gRPC requests\n    expect(result.tags).toEqual(['grpc', 'microservice']);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/tests/filesystem/index.spec.js",
    "content": "const path = require('path');\nconst fs = require('fs/promises');\nconst os = require('os');\nconst { copyPath, removePath } = require('../../filesystem');\nconst { initialCollectionStructure, finalCollectionStructure } = require('../fixtures/filesystem/copypath-removepath');\n\ndescribe('File System Operations', () => {\n  let tempDir;\n\n  beforeAll(async () => {\n    // Create a temporary directory for each test\n    tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bruno-test-'));\n    await createFilesAndFolders(tempDir, initialCollectionStructure);\n    const result = await verifyFilesAndFolders(tempDir, initialCollectionStructure);\n    expect(result).toBe(true);\n  });\n\n  afterAll(async () => {\n    // clean up after each test\n    await fs.rm(tempDir, { recursive: true, force: true });\n    // confirm the temp directory is deleted\n    expect(await fs.access(tempDir).then(() => true).catch(() => false)).toBe(false);\n  });\n\n  describe('copyPath and removePath', () => {\n    it('should move files and folder items multiple times', async () => {\n      {\n        const sourcePath = path.join(tempDir, 'folder_1', 'file_2.bru');\n        const destDir = path.join(tempDir, 'folder_1', 'folder_1_1');\n        await copyPath(sourcePath, destDir);\n        await removePath(sourcePath);\n      }\n\n      {\n        const sourcePath = path.join(tempDir, 'folder_2');\n        const destDir = path.join(tempDir, 'folder_1', 'folder_1_1');\n        await copyPath(sourcePath, destDir);\n        await removePath(sourcePath);\n      }\n\n      {\n        const sourcePath = path.join(tempDir, 'folder_1', 'folder_1_1', 'folder_2', 'file_2_2.bru');\n        const destDir = path.join(tempDir, 'folder_1');\n        await copyPath(sourcePath, destDir);\n        await removePath(sourcePath);\n      }\n\n      {\n        const sourcePath = path.join(tempDir, 'folder_1', 'folder_1_1', 'folder_2', 'folder_2_1');\n        const destDir = path.join(tempDir);\n        await copyPath(sourcePath, destDir);\n        await removePath(sourcePath);\n      }\n\n      const result = await verifyFilesAndFolders(tempDir, finalCollectionStructure);\n      expect(result).toBe(true);\n    });\n\n    it('should throw an error move file/folder if the destination has the same filename', async () => {\n      {\n        const sourcePath = path.join(tempDir, 'folder_1', 'file_dup.bru');\n        const destDir = path.join(tempDir, 'folder_1');\n        await expect(copyPath(sourcePath, destDir)).rejects.toThrow();\n      }\n    });\n  });\n});\n\n// create folders and files recursively based on the defined json structure\nconst createFilesAndFolders = async (dir, filesAndFolders) => {\n  for (const item of filesAndFolders) {\n    const itemPath = path.join(dir, item.name);\n    if (item.type === 'folder') {\n      await fs.mkdir(itemPath, { recursive: true });\n      await createFilesAndFolders(itemPath, item.files);\n    } else {\n      await fs.writeFile(itemPath, item.content);\n    }\n  }\n};\n\n// if a file/folder doesnt exist, return false\n// should only contain files and folders that are defined in the json structure\nconst verifyFilesAndFolders = async (dir, filesAndFolders) => {\n  const verify = async (dir, filesAndFolders) => {\n    const files = await fs.readdir(dir);\n    if (files.length !== filesAndFolders.length) {\n      return false;\n    }\n    for (const file of files) {\n      const itemPath = path.join(dir, file);\n      const item = filesAndFolders.find((f) => f.name === file);\n      if (!item) {\n        return false;\n      }\n      if (item.type === 'folder') {\n        return await verify(itemPath, item.files);\n      } else {\n        return await fs.readFile(itemPath, 'utf8').then((content) => content === item.content);\n      }\n    }\n    return true;\n  };\n\n  try {\n    const verified = await verify(dir, filesAndFolders);\n    return verified;\n  } catch (error) {\n    console.error(error);\n    return false;\n  }\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/tests/fixtures/filesystem/copypath-removepath.js",
    "content": "const initialCollectionStructure = [\n  {\n    name: 'folder_1',\n    type: 'folder',\n    files: [\n      {\n        name: 'file_1.bru',\n        type: 'file',\n        content: 'file_1_content'\n      },\n      {\n        name: 'file_2.bru',\n        type: 'file',\n        content: 'file_2_content'\n      },\n      {\n        name: 'folder_1_1',\n        type: 'folder',\n        files: [\n          {\n            name: 'file_1_1.bru',\n            type: 'file',\n            content: 'file_1_1_content'\n          },\n          {\n            name: 'file_1_2.bru',\n            type: 'file',\n            content: 'file_1_2_content'\n          }\n        ]\n      },\n      {\n        name: 'file_1_3.bru',\n        type: 'file',\n        content: 'file_1_3_content'\n      },\n      {\n        name: 'file_dup.bru',\n        type: 'file',\n        content: 'file_dup_content'\n      }\n    ]\n  },\n  {\n    name: 'folder_2',\n    type: 'folder',\n    files: [\n      {\n        name: 'file_2_1.bru',\n        type: 'file',\n        content: 'file_2_1_content'\n      },\n      {\n        name: 'file_2_2.bru',\n        type: 'file',\n        content: 'file_2_2_content'\n      },\n      {\n        name: 'folder_2_1',\n        type: 'folder',\n        files: [\n          {\n            name: 'file_2_1_1.bru',\n            type: 'file',\n            content: 'file_2_1_1_content'\n          }\n        ]\n      }\n    ]\n  },\n  {\n    name: 'file_dup.bru',\n    type: 'file',\n    content: 'file_dup_content'\n  }\n];\n\nconst finalCollectionStructure = [\n  {\n    name: 'folder_1',\n    type: 'folder',\n    files: [\n      {\n        name: 'file_1.bru',\n        type: 'file',\n        content: 'file_1_content'\n      },\n      {\n        name: 'folder_1_1',\n        type: 'folder',\n        files: [\n          {\n            name: 'file_1_1.bru',\n            type: 'file',\n            content: 'file_1_1_content'\n          },\n          {\n            name: 'file_1_2.bru',\n            type: 'file',\n            content: 'file_1_2_content'\n          },\n          {\n            name: 'file_2.bru',\n            type: 'file',\n            content: 'file_2_content'\n          },\n          {\n            name: 'folder_2',\n            type: 'folder',\n            files: [\n              {\n                name: 'file_2_1.bru',\n                type: 'file',\n                content: 'file_2_1_content'\n              }\n            ]\n          }\n        ]\n      },\n      {\n        name: 'file_1_3.bru',\n        type: 'file',\n        content: 'file_1_3_content'\n      },\n      {\n        name: 'file_2_2.bru',\n        type: 'file',\n        content: 'file_2_2_content'\n      },\n      {\n        name: 'file_dup.bru',\n        type: 'file',\n        content: 'file_dup_content'\n      }\n    ]\n  },\n  {\n    name: 'folder_2_1',\n    type: 'folder',\n    files: [\n      {\n        name: 'file_2_1_1.bru',\n        type: 'file',\n        content: 'file_2_1_1_content'\n      }\n    ]\n  },\n  {\n    name: 'file_dup.bru',\n    type: 'file',\n    content: 'file_dup_content'\n  }\n];\n\nmodule.exports = { initialCollectionStructure, finalCollectionStructure };\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/transformBrunoConfig.js",
    "content": "const path = require('path');\nconst { isFile, isDirectory } = require('./filesystem');\nconst { transformProxyConfig } = require('@usebruno/requests');\n\nfunction transformBrunoConfigBeforeSave(brunoConfig) {\n  // remove exists from importPaths and protoFiles\n  if (brunoConfig.protobuf?.importPaths) {\n    brunoConfig.protobuf.importPaths = brunoConfig.protobuf.importPaths.map((importPath) => {\n      delete importPath.exists;\n      return importPath;\n    });\n  }\n  if (brunoConfig.protobuf?.protoFiles) {\n    brunoConfig.protobuf.protoFiles = brunoConfig.protobuf.protoFiles.map((protoFile) => {\n      delete protoFile.exists;\n      return protoFile;\n    });\n  }\n\n  // Clean up proxy config before saving\n  if (brunoConfig.proxy) {\n    // Remove disabled: false (optional field)\n    if (brunoConfig.proxy.disabled === false) {\n      delete brunoConfig.proxy.disabled;\n    }\n    // Remove auth.disabled: false (optional field)\n    if (brunoConfig.proxy.config?.auth?.disabled === false) {\n      delete brunoConfig.proxy.config.auth.disabled;\n    }\n  }\n\n  return brunoConfig;\n}\n\nasync function transformBrunoConfigAfterRead(brunoConfig, collectionPathname) {\n  // add exists to importPaths and protoFiles by checking actual file/directory existence\n  if (brunoConfig.protobuf?.importPaths) {\n    brunoConfig.protobuf.importPaths = await Promise.all(brunoConfig.protobuf.importPaths.map(async (importPath) => {\n      try {\n        // Resolve the relative path against the collection pathname\n        const absolutePath = path.resolve(collectionPathname, importPath.path);\n        // Check if it's a directory\n        const exists = isDirectory(absolutePath);\n        return {\n          ...importPath,\n          exists\n        };\n      } catch (error) {\n        return {\n          ...importPath,\n          exists: false\n        };\n      }\n    }));\n  }\n\n  if (brunoConfig.protobuf?.protoFiles) {\n    brunoConfig.protobuf.protoFiles = await Promise.all(brunoConfig.protobuf.protoFiles.map(async (protoFile) => {\n      try {\n        // Resolve the relative path against the collection pathname\n        const absolutePath = path.resolve(collectionPathname, protoFile.path);\n        // Check if it's a file\n        const exists = isFile(absolutePath);\n        return {\n          ...protoFile,\n          exists\n        };\n      } catch (error) {\n        return {\n          ...protoFile,\n          exists: false\n        };\n      }\n    }));\n  }\n\n  // Migrate proxy configuration from old format to new format\n  if (brunoConfig.proxy) {\n    brunoConfig.proxy = transformProxyConfig(brunoConfig.proxy);\n  }\n\n  return brunoConfig;\n}\n\nmodule.exports = {\n  transformBrunoConfigBeforeSave,\n  transformBrunoConfigAfterRead\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/window.js",
    "content": "const { screen } = require('electron');\nconst WindowStateStore = require('../store/window-state');\n\nconst windowStateStore = new WindowStateStore();\n\nconst DEFAULT_WINDOW_WIDTH = 1280;\nconst DEFAULT_WINDOW_HEIGHT = 768;\n\nconst loadWindowState = () => {\n  const maximized = windowStateStore.getMaximized();\n  const bounds = windowStateStore.getBounds();\n\n  const positionValid = isPositionValid(bounds);\n  const sizeValid = isSizeValid(bounds);\n\n  return {\n    maximized,\n    x: bounds.x && positionValid ? bounds.x : undefined,\n    y: bounds.y && positionValid ? bounds.y : undefined,\n    width: bounds.width && sizeValid ? bounds.width : DEFAULT_WINDOW_WIDTH,\n    height: bounds.height && sizeValid ? bounds.height : DEFAULT_WINDOW_HEIGHT\n  };\n};\n\nconst saveBounds = (window) => {\n  const bounds = window.getBounds();\n\n  windowStateStore.setBounds(bounds);\n};\n\nconst saveMaximized = (isMaximized) => {\n  windowStateStore.setMaximized(isMaximized);\n};\n\nconst isPositionValid = (bounds) => {\n  const area = getArea(bounds);\n\n  return (\n    bounds.x >= area.x\n    && bounds.y >= area.y\n    && bounds.x + bounds.width <= area.x + area.width\n    && bounds.y + bounds.height <= area.y + area.height\n  );\n};\n\nconst isSizeValid = (bounds) => {\n  const area = getArea(bounds);\n\n  return bounds.width <= area.width && bounds.height <= area.height;\n};\n\nconst getArea = (bounds) => {\n  return screen.getDisplayMatching(bounds).workArea;\n};\n\nmodule.exports = {\n  loadWindowState,\n  saveBounds,\n  saveMaximized\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/workspace-config.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst yaml = require('js-yaml');\nconst { writeFile, validateName, isValidCollectionDirectory } = require('./filesystem');\nconst { generateUidBasedOnHash } = require('./common');\nconst { withLock, getWorkspaceLockKey } = require('./workspace-lock');\n\n// Normalize Windows backslash paths to forward slashes for cross-platform compatibility.\nconst posixifyPath = (p) => (p ? p.replace(/\\\\/g, '/') : p);\n\nconst WORKSPACE_TYPE = 'workspace';\nconst OPENCOLLECTION_VERSION = '1.0.0';\n\nconst quoteYamlValue = (value) => {\n  if (typeof value !== 'string') {\n    return `\"${String(value)}\"`;\n  }\n\n  if (value === '') {\n    return '\"\"';\n  }\n\n  const escaped = value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n  return `\"${escaped}\"`;\n};\n\nconst writeWorkspaceFileAtomic = async (workspacePath, content) => {\n  const workspaceFilePath = path.join(workspacePath, 'workspace.yml');\n  await writeFile(workspaceFilePath, content);\n\n  // Previous atomic write implementation commented out due to permission issues on Linux\n  // when temp directory is on a different filesystem (cross-device link error)\n\n  // const tempFilePath = path.join(os.tmpdir(), `workspace-${Date.now()}-${crypto.randomBytes(16).toString('hex')}.yml`);\n\n  // try {\n  //   await writeFile(tempFilePath, content);\n\n  //   if (fs.existsSync(workspaceFilePath)) {\n  //     fs.unlinkSync(workspaceFilePath);\n  //   }\n\n  //   fs.renameSync(tempFilePath, workspaceFilePath);\n  // } catch (error) {\n  //   if (fs.existsSync(tempFilePath)) {\n  //     try {\n  //       fs.unlinkSync(tempFilePath);\n  //     } catch (_) {}\n  //   }\n  //   throw error;\n  // }\n};\n\nconst isValidCollectionEntry = (collection) => {\n  if (!collection || typeof collection !== 'object') {\n    return false;\n  }\n\n  if (!collection.name || typeof collection.name !== 'string' || collection.name.trim() === '') {\n    return false;\n  }\n\n  if (!collection.path || typeof collection.path !== 'string' || collection.path.trim() === '') {\n    return false;\n  }\n\n  return true;\n};\n\nconst isValidSpecEntry = (spec) => {\n  if (!spec || typeof spec !== 'object') {\n    return false;\n  }\n\n  if (!spec.name || typeof spec.name !== 'string' || spec.name.trim() === '') {\n    return false;\n  }\n\n  if (!spec.path || typeof spec.path !== 'string' || spec.path.trim() === '') {\n    return false;\n  }\n\n  return true;\n};\n\nconst sanitizeCollections = (collections) => {\n  if (!Array.isArray(collections)) {\n    return [];\n  }\n\n  return collections.filter((collection) => {\n    if (!isValidCollectionEntry(collection)) {\n      console.error('Skipping invalid collection entry:', collection);\n      return false;\n    }\n    return true;\n  }).map((collection) => {\n    const sanitized = {\n      name: collection.name.trim(),\n      path: posixifyPath(collection.path.trim())\n    };\n\n    if (collection.remote && typeof collection.remote === 'string') {\n      sanitized.remote = collection.remote.trim();\n    }\n\n    return sanitized;\n  });\n};\n\nconst sanitizeSpecs = (specs) => {\n  if (!Array.isArray(specs)) {\n    return [];\n  }\n\n  return specs.filter((spec) => {\n    if (!isValidSpecEntry(spec)) {\n      console.error('Skipping invalid spec entry:', spec);\n      return false;\n    }\n    return true;\n  }).map((spec) => ({\n    name: spec.name.trim(),\n    path: posixifyPath(spec.path.trim())\n  }));\n};\n\nconst makeRelativePath = (workspacePath, absolutePath) => {\n  if (!path.isAbsolute(absolutePath)) {\n    return posixifyPath(absolutePath);\n  }\n\n  try {\n    const relativePath = path.relative(workspacePath, absolutePath);\n    if (relativePath.startsWith('..') && relativePath.split(path.sep).filter((s) => s === '..').length > 2) {\n      return posixifyPath(absolutePath);\n    }\n    return posixifyPath(relativePath);\n  } catch (error) {\n    return posixifyPath(absolutePath);\n  }\n};\n\nconst getNormalizedAbsoluteCollectionPath = (workspacePath, collection) => {\n  if (!collection?.path) return null;\n  const resolved = path.isAbsolute(collection.path) ? collection.path : path.resolve(workspacePath, collection.path);\n  return path.normalize(resolved);\n};\n\nconst normalizeCollectionEntry = (workspacePath, collection) => {\n  const relativePath = makeRelativePath(workspacePath, collection.path);\n\n  const normalizedCollection = {\n    name: collection.name,\n    path: relativePath\n  };\n\n  if (collection.remote) {\n    normalizedCollection.remote = collection.remote;\n  }\n\n  return normalizedCollection;\n};\n\nconst validateWorkspacePath = (workspacePath) => {\n  if (!workspacePath) {\n    throw new Error('Workspace path is required');\n  }\n\n  if (!fs.existsSync(workspacePath)) {\n    throw new Error(`Workspace path does not exist: ${workspacePath}`);\n  }\n\n  const workspaceFilePath = path.join(workspacePath, 'workspace.yml');\n  if (!fs.existsSync(workspaceFilePath)) {\n    throw new Error('Invalid workspace: workspace.yml not found');\n  }\n\n  return true;\n};\n\nconst validateWorkspaceDirectory = (dirPath) => {\n  if (!validateName(path.basename(dirPath))) {\n    throw new Error(`Invalid workspace directory name: ${dirPath}`);\n  }\n  return true;\n};\n\nconst createWorkspaceConfig = (workspaceName) => ({\n  opencollection: OPENCOLLECTION_VERSION,\n  info: {\n    name: workspaceName,\n    type: WORKSPACE_TYPE\n  },\n  collections: [],\n  specs: [],\n  docs: ''\n});\n\nconst normalizeWorkspaceConfig = (config) => {\n  return {\n    ...config,\n    name: config.info?.name,\n    type: config.info?.type,\n    collections: config.collections || [],\n    apiSpecs: config.specs || []\n  };\n};\n\nconst readWorkspaceConfig = (workspacePath) => {\n  const workspaceFilePath = path.join(workspacePath, 'workspace.yml');\n\n  if (!fs.existsSync(workspaceFilePath)) {\n    throw new Error('Invalid workspace: workspace.yml not found');\n  }\n\n  const yamlContent = fs.readFileSync(workspaceFilePath, 'utf8');\n  const workspaceConfig = yaml.load(yamlContent);\n\n  if (!workspaceConfig || typeof workspaceConfig !== 'object') {\n    throw new Error('Invalid workspace: workspace.yml is malformed');\n  }\n\n  return normalizeWorkspaceConfig(workspaceConfig);\n};\n\nconst generateYamlContent = (config) => {\n  const yamlLines = [];\n  const workspaceName = config.info?.name || config.name || 'Untitled Workspace';\n  const workspaceType = config.info?.type || config.type || WORKSPACE_TYPE;\n\n  yamlLines.push(`opencollection: ${config.opencollection || OPENCOLLECTION_VERSION}`);\n  yamlLines.push('info:');\n  yamlLines.push(`  name: ${quoteYamlValue(workspaceName)}`);\n  yamlLines.push(`  type: ${workspaceType}`);\n  yamlLines.push('');\n\n  const collections = sanitizeCollections(config.collections);\n  if (collections.length > 0) {\n    yamlLines.push('collections:');\n    for (const collection of collections) {\n      yamlLines.push(`  - name: ${quoteYamlValue(collection.name)}`);\n      yamlLines.push(`    path: ${quoteYamlValue(collection.path)}`);\n      if (collection.remote) {\n        yamlLines.push(`    remote: ${quoteYamlValue(collection.remote)}`);\n      }\n    }\n  } else {\n    yamlLines.push('collections:');\n  }\n  yamlLines.push('');\n\n  const specs = sanitizeSpecs(config.specs);\n  if (specs.length > 0) {\n    yamlLines.push('specs:');\n    for (const spec of specs) {\n      yamlLines.push(`  - name: ${quoteYamlValue(spec.name)}`);\n      yamlLines.push(`    path: ${quoteYamlValue(spec.path)}`);\n    }\n  } else {\n    yamlLines.push('specs:');\n  }\n  yamlLines.push('');\n\n  const docs = config.docs || '';\n  if (docs) {\n    const escapedDocs = docs.includes('\\n')\n      ? `|-\\n  ${docs.split('\\n').join('\\n  ')}`\n      : quoteYamlValue(docs);\n    yamlLines.push(`docs: ${escapedDocs}`);\n  } else {\n    yamlLines.push('docs: \\'\\'');\n  }\n\n  if (config.activeEnvironmentUid && typeof config.activeEnvironmentUid === 'string') {\n    yamlLines.push('');\n    yamlLines.push(`activeEnvironmentUid: ${config.activeEnvironmentUid}`);\n  }\n\n  yamlLines.push('');\n\n  return yamlLines.join('\\n');\n};\n\nconst writeWorkspaceConfig = async (workspacePath, config) => {\n  return withLock(getWorkspaceLockKey(workspacePath), async () => {\n    const yamlContent = generateYamlContent(config);\n    await writeWorkspaceFileAtomic(workspacePath, yamlContent);\n  });\n};\n\nconst validateWorkspaceConfig = (config) => {\n  if (!config || typeof config !== 'object') {\n    throw new Error('Workspace configuration must be an object');\n  }\n\n  const type = config.info?.type || config.type;\n  if (type !== WORKSPACE_TYPE) {\n    throw new Error('Invalid workspace: not a bruno workspace');\n  }\n\n  const name = config.info?.name || config.name;\n  if (!name || typeof name !== 'string') {\n    throw new Error('Workspace must have a valid name');\n  }\n\n  return true;\n};\n\nconst updateWorkspaceName = async (workspacePath, newName) => {\n  return withLock(getWorkspaceLockKey(workspacePath), async () => {\n    const config = readWorkspaceConfig(workspacePath);\n    config.name = newName;\n    if (config.info) {\n      config.info.name = newName;\n    }\n    const yamlContent = generateYamlContent(config);\n    await writeWorkspaceFileAtomic(workspacePath, yamlContent);\n    return config;\n  });\n};\n\nconst updateWorkspaceDocs = async (workspacePath, docs) => {\n  return withLock(getWorkspaceLockKey(workspacePath), async () => {\n    const config = readWorkspaceConfig(workspacePath);\n    config.docs = docs;\n    const yamlContent = generateYamlContent(config);\n    await writeWorkspaceFileAtomic(workspacePath, yamlContent);\n    return docs;\n  });\n};\n\nconst addCollectionToWorkspace = async (workspacePath, collection) => {\n  if (!isValidCollectionEntry(collection)) {\n    throw new Error('Invalid collection: name and path are required');\n  }\n\n  return withLock(getWorkspaceLockKey(workspacePath), async () => {\n    const config = readWorkspaceConfig(workspacePath);\n\n    if (!config.collections) {\n      config.collections = [];\n    }\n\n    const normalizedCollection = {\n      name: collection.name.trim(),\n      path: posixifyPath(collection.path.trim())\n    };\n\n    if (collection.remote && typeof collection.remote === 'string') {\n      normalizedCollection.remote = collection.remote.trim();\n    }\n\n    const existingIndex = config.collections.findIndex((c) => c.path && posixifyPath(c.path) === normalizedCollection.path);\n\n    if (existingIndex >= 0) {\n      config.collections[existingIndex] = normalizedCollection;\n    } else {\n      config.collections.push(normalizedCollection);\n    }\n\n    const yamlContent = generateYamlContent(config);\n    await writeWorkspaceFileAtomic(workspacePath, yamlContent);\n    return config.collections;\n  });\n};\n\nconst removeCollectionFromWorkspace = async (workspacePath, collectionPath) => {\n  return withLock(getWorkspaceLockKey(workspacePath), async () => {\n    const config = readWorkspaceConfig(workspacePath);\n\n    let removedCollection = null;\n\n    config.collections = (config.collections || []).filter((c) => {\n      const collectionPathFromYml = c.path ? posixifyPath(c.path) : c.path;\n\n      if (!collectionPathFromYml) {\n        return true;\n      }\n\n      const absoluteCollectionPath = path.isAbsolute(collectionPathFromYml)\n        ? collectionPathFromYml\n        : path.resolve(workspacePath, collectionPathFromYml);\n\n      if (path.normalize(absoluteCollectionPath) === path.normalize(collectionPath)) {\n        removedCollection = c;\n        return false;\n      }\n\n      return true;\n    });\n\n    const yamlContent = generateYamlContent(config);\n    await writeWorkspaceFileAtomic(workspacePath, yamlContent);\n\n    return {\n      removedCollection,\n      updatedConfig: config\n    };\n  });\n};\n\n/**\n * Reorders the collections array in the workspace's workspace.yml to match the given path list.\n * Entries not in the list are appended at the end.\n * @param {string} workspacePath - Absolute path to the workspace directory\n * @param {string[]} collectionPaths - Absolute collection pathnames in the desired order\n */\nconst reorderWorkspaceCollections = async (workspacePath, collectionPaths) => {\n  if (!Array.isArray(collectionPaths)) {\n    throw new Error('collectionPaths must be an array');\n  }\n\n  return withLock(getWorkspaceLockKey(workspacePath), async () => {\n    const config = readWorkspaceConfig(workspacePath);\n    const existing = config.collections || [];\n\n    const inNewOrder = [];\n    const matched = new Set();\n\n    for (const absolutePath of collectionPaths) {\n      const targetPath = posixifyPath(path.normalize(absolutePath));\n      const entry = existing.find(\n        (c) => posixifyPath(getNormalizedAbsoluteCollectionPath(workspacePath, c)) === targetPath\n      );\n      if (entry && !matched.has(entry)) {\n        inNewOrder.push(entry);\n        matched.add(entry);\n      }\n    }\n\n    const notInList = existing.filter((c) => !matched.has(c));\n    config.collections = [...inNewOrder, ...notInList];\n\n    const yamlContent = generateYamlContent(config);\n    await writeWorkspaceFileAtomic(workspacePath, yamlContent);\n  });\n};\n\nconst getWorkspaceCollections = (workspacePath) => {\n  const config = readWorkspaceConfig(workspacePath);\n  const collections = config.collections || [];\n\n  const seenPaths = new Set();\n  return collections\n    .map((collection) => {\n      const collectionPath = collection.path ? posixifyPath(collection.path) : collection.path;\n      if (collectionPath && !path.isAbsolute(collectionPath)) {\n        return {\n          ...collection,\n          path: path.resolve(workspacePath, collectionPath)\n        };\n      }\n      return { ...collection, path: collectionPath };\n    })\n    .filter((collection) => {\n      if (!collection.path) {\n        return false;\n      }\n      const normalizedPath = path.normalize(collection.path);\n      if (seenPaths.has(normalizedPath)) {\n        return false;\n      }\n      seenPaths.add(normalizedPath);\n      if (!isValidCollectionDirectory(collection.path)) {\n        return false;\n      }\n      return true;\n    });\n};\n\nconst getWorkspaceApiSpecs = (workspacePath) => {\n  const config = readWorkspaceConfig(workspacePath);\n  const specs = config.specs || [];\n\n  return specs.map((spec) => {\n    const specPath = spec.path ? posixifyPath(spec.path) : spec.path;\n    if (specPath && !path.isAbsolute(specPath)) {\n      return {\n        ...spec,\n        path: path.join(workspacePath, specPath)\n      };\n    }\n    return { ...spec, path: specPath };\n  });\n};\n\nconst addApiSpecToWorkspace = async (workspacePath, apiSpec) => {\n  if (!isValidSpecEntry(apiSpec)) {\n    throw new Error('Invalid API spec: name and path are required');\n  }\n\n  return withLock(getWorkspaceLockKey(workspacePath), async () => {\n    const config = readWorkspaceConfig(workspacePath);\n\n    if (!config.specs) {\n      config.specs = [];\n    }\n\n    const normalizedSpec = {\n      name: apiSpec.name.trim(),\n      path: makeRelativePath(workspacePath, apiSpec.path).trim()\n    };\n\n    const existingIndex = config.specs.findIndex(\n      (a) => a.name === normalizedSpec.name || (a.path && posixifyPath(a.path) === normalizedSpec.path)\n    );\n\n    if (existingIndex >= 0) {\n      config.specs[existingIndex] = normalizedSpec;\n    } else {\n      config.specs.push(normalizedSpec);\n    }\n\n    const yamlContent = generateYamlContent(config);\n    await writeWorkspaceFileAtomic(workspacePath, yamlContent);\n    return config.specs;\n  });\n};\n\nconst removeApiSpecFromWorkspace = async (workspacePath, apiSpecPath) => {\n  return withLock(getWorkspaceLockKey(workspacePath), async () => {\n    const config = readWorkspaceConfig(workspacePath);\n\n    if (!config.specs) {\n      return { removedApiSpec: null, updatedConfig: config };\n    }\n\n    let removedApiSpec = null;\n\n    config.specs = config.specs.filter((a) => {\n      const specPathFromYml = a.path ? posixifyPath(a.path) : a.path;\n      if (!specPathFromYml) return true;\n\n      const absoluteSpecPath = path.isAbsolute(specPathFromYml)\n        ? specPathFromYml\n        : path.resolve(workspacePath, specPathFromYml);\n\n      if (path.normalize(absoluteSpecPath) === path.normalize(apiSpecPath)) {\n        removedApiSpec = a;\n        return false;\n      }\n\n      return true;\n    });\n\n    const yamlContent = generateYamlContent(config);\n    await writeWorkspaceFileAtomic(workspacePath, yamlContent);\n\n    return {\n      removedApiSpec,\n      updatedConfig: config\n    };\n  });\n};\n\nconst getWorkspaceUid = (workspacePath) => {\n  const { defaultWorkspaceManager } = require('../store/default-workspace');\n  const defaultWorkspacePath = defaultWorkspaceManager.getDefaultWorkspacePath();\n  if (defaultWorkspacePath && path.normalize(workspacePath) === path.normalize(defaultWorkspacePath)) {\n    return defaultWorkspaceManager.getDefaultWorkspaceUid();\n  }\n  return generateUidBasedOnHash(workspacePath);\n};\n\nmodule.exports = {\n  makeRelativePath,\n  normalizeCollectionEntry,\n  validateWorkspacePath,\n  validateWorkspaceDirectory,\n  createWorkspaceConfig,\n  readWorkspaceConfig,\n  writeWorkspaceConfig,\n  validateWorkspaceConfig,\n  updateWorkspaceName,\n  updateWorkspaceDocs,\n  addCollectionToWorkspace,\n  removeCollectionFromWorkspace,\n  reorderWorkspaceCollections,\n  getWorkspaceCollections,\n  getWorkspaceApiSpecs,\n  addApiSpecToWorkspace,\n  removeApiSpecFromWorkspace,\n  generateYamlContent,\n  getWorkspaceUid,\n  writeWorkspaceFileAtomic,\n  isValidCollectionEntry,\n  isValidSpecEntry\n};\n"
  },
  {
    "path": "packages/bruno-electron/src/utils/workspace-lock.js",
    "content": "const locks = new Map();\n\nconst acquireLock = async (key, timeout = 10000) => {\n  const startTime = Date.now();\n\n  while (locks.has(key)) {\n    if (Date.now() - startTime > timeout) {\n      throw new Error(`Lock acquisition timeout for: ${key}`);\n    }\n    await new Promise((resolve) => setTimeout(resolve, 50));\n  }\n\n  let releaseFn;\n  const releasePromise = new Promise((resolve) => {\n    releaseFn = resolve;\n  });\n\n  locks.set(key, releasePromise);\n\n  return () => {\n    locks.delete(key);\n    releaseFn();\n  };\n};\n\nconst withLock = async (key, fn) => {\n  const release = await acquireLock(key);\n  try {\n    return await fn();\n  } finally {\n    release();\n  }\n};\n\nconst getWorkspaceLockKey = (workspacePath) => {\n  return `workspace:${workspacePath}`;\n};\n\nmodule.exports = {\n  acquireLock,\n  withLock,\n  getWorkspaceLockKey\n};\n"
  },
  {
    "path": "packages/bruno-electron/tests/cookies-store.test.js",
    "content": "const path = require('path');\n\nconst mockEncrypt = (str) => (str.length ? `$enc:${str}` : '');\nconst mockDecrypt = (str) => str.replace(/^\\$enc:/, '');\n\njest.mock('../src/utils/encryption', () => ({\n  encryptString: jest.fn(mockEncrypt),\n  decryptString: jest.fn(mockDecrypt)\n}));\n\njest.mock('electron-store', () => {\n  return jest.fn().mockImplementation((opts = {}) => {\n    const data = { ...(opts.defaults || {}) };\n    return {\n      get: (key, fallback) => (key in data ? data[key] : fallback),\n      set: (key, value) => {\n        data[key] = value;\n      }\n    };\n  });\n});\n\nconst { CookiesStore } = require(path.join('..', 'src', 'store', 'cookies'));\n\nfunction createFreshStore() {\n  return new CookiesStore();\n}\n\ndescribe('CookiesStore', () => {\n  test('setCookies encrypts values and getCookies returns decrypted values', () => {\n    const store = createFreshStore();\n\n    const cookie = {\n      domain: 'example.com',\n      key: 'auth',\n      value: 'token',\n      path: '/',\n      secure: true,\n      httpOnly: true\n    };\n\n    store.setCookies({ cookies: [cookie] });\n\n    // Raw persisted value should be encrypted\n    const raw = store.store.get('cookies');\n    expect(raw['example.com'][0].value).toBe(`$enc:${cookie.value}`);\n\n    // API should return decrypted value\n    const retrieved = store.getCookies();\n    expect(retrieved[0].value).toBe(cookie.value);\n  });\n\n  test('getCookies leaves plain-text cookie values untouched', () => {\n    const store = createFreshStore();\n\n    const plainCookie = {\n      domain: 'example.com',\n      key: 'sid',\n      value: 'plaintext',\n      path: '/',\n      secure: false,\n      httpOnly: false\n    };\n\n    // Manually inject to the underlying store to simulate legacy/plain data\n    store.store.set('cookies', { 'example.com': [plainCookie] });\n\n    const cookies = store.getCookies();\n    expect(cookies[0].value).toBe('plaintext');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-electron/tests/network/authorize-user.spec.js",
    "content": "const { matchesCallbackUrl } = require('../../src/ipc/network/authorize-user-in-window');\n\ndescribe('matchesCallbackUrl', () => {\n  const testCases = [\n    { url: 'https://random-url/endpoint', expected: false },\n    { url: 'https://random-url/endpoint?code=abcd', expected: false },\n    { url: 'https://callback.url/endpoint?code=abcd', expected: true },\n    { url: 'https://callback.url/endpoint/?code=abcd', expected: true },\n    { url: 'https://callback.url/random-endpoint/?code=abcd', expected: false }\n  ];\n\n  it.each(testCases)('$url - should be $expected', ({ url, expected }) => {\n    let callBackUrl = 'https://callback.url/endpoint';\n\n    let actual = matchesCallbackUrl(new URL(url), new URL(callBackUrl));\n\n    expect(actual).toBe(expected);\n  });\n\n  describe('root path callback URL', () => {\n    const rootPathCases = [\n      { url: 'https://hostname/auth/login', expected: false, desc: 'intermediate login page without code' },\n      { url: 'https://hostname/consent', expected: false, desc: 'intermediate consent page without code' },\n      { url: 'https://hostname/?code=abcd', expected: true, desc: 'root callback with authorization code' },\n      { url: 'https://hostname/?error=access_denied', expected: false, desc: 'root callback with error (handled separately by onWindowRedirect)' },\n      { url: 'https://hostname/#access_token=xyz', expected: true, desc: 'root callback with implicit flow hash' },\n      { url: 'https://hostname/', expected: false, desc: 'root path without any OAuth2 params' },\n      { url: 'https://other-host/?code=abcd', expected: false, desc: 'different host with code param' }\n    ];\n\n    it.each(rootPathCases)('$desc ($url) - should be $expected', ({ url, expected }) => {\n      let callBackUrl = 'https://hostname/';\n\n      let actual = matchesCallbackUrl(new URL(url), new URL(callBackUrl));\n\n      expect(actual).toBe(expected);\n    });\n  });\n\n  describe('implicit flow with hash fragments', () => {\n    const implicitCases = [\n      { url: 'https://callback.url/endpoint#access_token=xyz&token_type=bearer', expected: true, desc: 'callback with hash fragment' },\n      { url: 'https://callback.url/endpoint#', expected: false, desc: 'callback with empty hash' },\n      { url: 'https://callback.url/endpoint', expected: false, desc: 'callback without hash or code' }\n    ];\n\n    it.each(implicitCases)('$desc ($url) - should be $expected', ({ url, expected }) => {\n      let callBackUrl = 'https://callback.url/endpoint';\n\n      let actual = matchesCallbackUrl(new URL(url), new URL(callBackUrl));\n\n      expect(actual).toBe(expected);\n    });\n  });\n\n  it('should return false for null url', () => {\n    let callBackUrl = 'https://callback.url/endpoint';\n    expect(matchesCallbackUrl(null, new URL(callBackUrl))).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-electron/tests/network/execute-request-error-handler.spec.js",
    "content": "const { executeRequestOnFailHandler } = require('../../src/ipc/network/index');\nconst axios = require('axios');\n\ndescribe('executeRequestOnFailHandler', () => {\n  let consoleSpy;\n\n  beforeEach(() => {\n    consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});\n  });\n\n  afterEach(() => {\n    consoleSpy.mockRestore();\n  });\n\n  it('should do nothing when request is null', async () => {\n    const error = new Error('Test error');\n\n    await executeRequestOnFailHandler(null, error);\n\n    expect(consoleSpy).not.toHaveBeenCalled();\n  });\n\n  it('should do nothing when request is undefined', async () => {\n    const error = new Error('Test error');\n\n    await executeRequestOnFailHandler(undefined, error);\n\n    expect(consoleSpy).not.toHaveBeenCalled();\n  });\n\n  it('should do nothing when onFailHandler is not a function', async () => {\n    const request = { onFailHandler: 'not a function' };\n    const error = new Error('Test error');\n\n    await executeRequestOnFailHandler(request, error);\n\n    expect(consoleSpy).not.toHaveBeenCalled();\n  });\n\n  it('should call onFailHandler when it exists and is a function', async () => {\n    const mockHandler = jest.fn();\n    const request = { onFailHandler: mockHandler };\n    const error = new Error('Test error');\n\n    await executeRequestOnFailHandler(request, error);\n\n    expect(mockHandler).toHaveBeenCalledWith(error);\n    expect(mockHandler).toHaveBeenCalledTimes(1);\n    expect(consoleSpy).not.toHaveBeenCalled();\n  });\n\n  it('should handle errors when onFailHandler fails by mutating the error message', async () => {\n    const handlerError = new Error('Handler failed');\n    const mockHandler = jest.fn(() => {\n      throw handlerError;\n    });\n    const request = { onFailHandler: mockHandler };\n    const error = new Error('Original error');\n\n    await executeRequestOnFailHandler(request, error);\n\n    expect(mockHandler).toHaveBeenCalledWith(error);\n    expect(error.message).toContain('1. Request failed: Original error');\n    expect(error.message).toContain('2. Error executing onFail handler: Handler failed');\n  });\n\n  it('should pass the correct hard error object to the handler for DNS failure', async () => {\n    const mockHandler = jest.fn();\n    const request = { onFailHandler: mockHandler };\n\n    let error;\n    try {\n      await axios.get('https://this-domain-definitely-does-not-exist-12345.com/api/test', {\n        timeout: 5000\n      });\n    } catch (err) {\n      error = err;\n    }\n\n    // Verify this is actually a hard error (no response)\n    expect(error.response).toBeUndefined();\n\n    await executeRequestOnFailHandler(request, error);\n\n    expect(mockHandler).toHaveBeenCalledWith(error);\n    expect(error.message).toContain('ENOTFOUND'); // DNS resolution failed\n  });\n\n  it('should pass the correct hard error object to the handler for connection timeout', async () => {\n    const mockHandler = jest.fn();\n    const request = { onFailHandler: mockHandler };\n\n    let error;\n    try {\n      await axios.get('http://192.168.255.255:9999/api/test', {\n        timeout: 100\n      });\n    } catch (err) {\n      error = err;\n    }\n\n    // Verify this is actually a hard error (no response)\n    expect(error.response).toBeUndefined();\n\n    await executeRequestOnFailHandler(request, error);\n\n    expect(mockHandler).toHaveBeenCalledWith(error);\n    const passedError = mockHandler.mock.calls[0][0];\n    expect(passedError.response).toBeUndefined(); // Should be undefined for hard errors\n    expect(passedError.code).toBe('ECONNABORTED'); // Connection aborted due to timeout\n  });\n});\n"
  },
  {
    "path": "packages/bruno-electron/tests/network/fetch-gql-schema-handler.spec.js",
    "content": "const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request');\nconst { fetchGqlSchemaHandler } = require('../../src/ipc/network');\n\n// Mock only the prepare-gql-introspection-request to avoid network calls\njest.mock('../../src/ipc/network/prepare-gql-introspection-request', () => {\n  return jest.fn().mockImplementation((endpoint, vars, request, root) => {\n    return {\n      url: endpoint,\n      method: 'POST',\n      headers: request?.headers || {},\n      data: {\n        query: '{ __schema { types { name } } }'\n      }\n    };\n  });\n});\n\ndescribe('fetchGqlSchemaHandler - variable precedence', () => {\n  beforeEach(() => {\n    jest.clearAllMocks();\n  });\n\n  afterEach(() => {\n    jest.restoreAllMocks();\n  });\n\n  it('should override global environment variables with environment variables', async () => {\n    const endpoint = 'https://example.com/';\n    const environment = {\n      variables: [\n        { name: 'SHARED_VAR', value: 'env-value', enabled: true }\n      ]\n    };\n    const request = {\n      uid: 'test-request',\n      vars: {\n        req: [] // No request variables\n      }\n    };\n    const collection = {\n      uid: 'test-collection',\n      pathname: '/test',\n      runtimeVariables: {},\n      globalEnvironmentVariables: {\n        SHARED_VAR: 'global-value'\n      },\n      items: [\n        {\n          uid: 'test-request',\n          request: {\n            vars: {\n              req: [] // No request variables\n            }\n          }\n        }\n      ],\n      root: {\n        request: {\n          headers: [],\n          vars: {\n            req: [] // No collection variables\n          }\n        }\n      }\n    };\n\n    await fetchGqlSchemaHandler(null, endpoint, environment, request, collection);\n\n    expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith(\n      endpoint,\n      expect.objectContaining({\n        SHARED_VAR: 'env-value'\n      }),\n      request,\n      collection.root\n    );\n  });\n\n  it('should override environment variables with folder-level variables', async () => {\n    const endpoint = 'https://example.com/';\n    const environment = {\n      variables: [\n        { name: 'SHARED_VAR', value: 'env-value', enabled: true }\n      ]\n    };\n    const request = {\n      uid: 'test-request',\n      vars: {\n        req: [] // No request variables\n      }\n    };\n    const collection = {\n      uid: 'test-collection',\n      pathname: '/test',\n      runtimeVariables: {},\n      globalEnvironmentVariables: {},\n      items: [\n        {\n          uid: 'test-folder',\n          type: 'folder',\n          root: {\n            request: {\n              vars: {\n                req: [\n                  { name: 'SHARED_VAR', value: 'folder-value', enabled: true }\n                ]\n              }\n            }\n          },\n          items: [\n            {\n              uid: 'test-request',\n              request: {\n                vars: {\n                  req: [] // No request variables\n                }\n              }\n            }\n          ]\n        }\n      ],\n      root: {\n        request: {\n          headers: [],\n          vars: {\n            req: [] // No collection variables\n          }\n        }\n      }\n    };\n\n    await fetchGqlSchemaHandler(null, endpoint, environment, request, collection);\n\n    expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith(\n      endpoint,\n      expect.objectContaining({\n        SHARED_VAR: 'folder-value'\n      }),\n      request,\n      collection.root\n    );\n  });\n\n  it('should override folder-level variables with request variables', async () => {\n    const endpoint = 'https://example.com/';\n    const environment = {\n      variables: []\n    };\n    const request = {\n      uid: 'test-request',\n      vars: {\n        req: [\n          { name: 'SHARED_VAR', value: 'request-value', enabled: true }\n        ]\n      }\n    };\n    const collection = {\n      uid: 'test-collection',\n      pathname: '/test',\n      runtimeVariables: {},\n      globalEnvironmentVariables: {},\n      items: [\n        {\n          uid: 'test-folder',\n          type: 'folder',\n          root: {\n            request: {\n              vars: {\n                req: [\n                  { name: 'SHARED_VAR', value: 'folder-value', enabled: true }\n                ]\n              }\n            }\n          },\n          items: [\n            {\n              uid: 'test-request',\n              request: {\n                vars: {\n                  req: [\n                    { name: 'SHARED_VAR', value: 'request-value', enabled: true }\n                  ]\n                }\n              }\n            }\n          ]\n        }\n      ],\n      root: {\n        request: {\n          headers: [],\n          vars: {\n            req: [] // No collection variables\n          }\n        }\n      }\n    };\n\n    await fetchGqlSchemaHandler(null, endpoint, environment, request, collection);\n\n    expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith(\n      endpoint,\n      expect.objectContaining({\n        SHARED_VAR: 'request-value'\n      }),\n      request,\n      collection.root\n    );\n  });\n\n  it('should override global environment variables with collection variables', async () => {\n    const endpoint = 'https://example.com/';\n    const environment = {\n      variables: []\n    };\n    const request = {\n      uid: 'test-request',\n      vars: {\n        req: [] // No request variables\n      }\n    };\n    const collection = {\n      uid: 'test-collection',\n      pathname: '/test',\n      runtimeVariables: {},\n      globalEnvironmentVariables: {\n        SHARED_VAR: 'global-value'\n      },\n      items: [\n        {\n          uid: 'test-request',\n          request: {\n            vars: {\n              req: [] // No request variables\n            }\n          }\n        }\n      ],\n      root: {\n        request: {\n          headers: [],\n          vars: {\n            req: [\n              { name: 'SHARED_VAR', value: 'collection-value', enabled: true }\n            ]\n          }\n        }\n      }\n    };\n\n    await fetchGqlSchemaHandler(null, endpoint, environment, request, collection);\n\n    expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith(\n      endpoint,\n      expect.objectContaining({\n        SHARED_VAR: 'collection-value'\n      }),\n      request,\n      collection.root\n    );\n  });\n\n  it('should override collection variables with environment variables', async () => {\n    const endpoint = 'https://example.com/';\n    const environment = {\n      variables: [\n        { name: 'SHARED_VAR', value: 'env-value', enabled: true }\n      ]\n    };\n    const request = {\n      uid: 'test-request',\n      vars: {\n        req: [] // No request variables\n      }\n    };\n    const collection = {\n      uid: 'test-collection',\n      pathname: '/test',\n      runtimeVariables: {},\n      globalEnvironmentVariables: {},\n      items: [\n        {\n          uid: 'test-request',\n          request: {\n            vars: {\n              req: [] // No request variables\n            }\n          }\n        }\n      ],\n      root: {\n        request: {\n          headers: [],\n          vars: {\n            req: [\n              { name: 'SHARED_VAR', value: 'collection-value', enabled: true }\n            ]\n          }\n        }\n      }\n    };\n\n    await fetchGqlSchemaHandler(null, endpoint, environment, request, collection);\n\n    expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith(\n      endpoint,\n      expect.objectContaining({\n        SHARED_VAR: 'env-value'\n      }),\n      request,\n      collection.root\n    );\n  });\n\n  it('should override request variables with runtime variables', async () => {\n    const endpoint = 'https://example.com/';\n    const environment = {\n      variables: []\n    };\n\n    const request = {\n      uid: 'test-request',\n      vars: {\n        req: [\n          { name: 'SHARED_VAR', value: 'request-value', enabled: true }\n        ]\n      }\n    };\n\n    const collection = {\n      uid: 'test-collection',\n      pathname: '/test',\n      runtimeVariables: {\n        SHARED_VAR: 'runtime-value'\n      },\n      items: [\n        {\n          uid: 'test-request',\n          request: {\n            vars: {\n              req: [\n                { name: 'SHARED_VAR', value: 'request-value', enabled: true }\n              ]\n            }\n          }\n        }\n      ],\n      root: {\n        request: {\n          headers: [],\n          vars: {\n            req: [] // No collection variables\n          }\n        }\n      }\n    };\n\n    await fetchGqlSchemaHandler(null, endpoint, environment, request, collection);\n\n    expect(prepareGqlIntrospectionRequest).toHaveBeenCalledWith(\n      endpoint,\n      expect.objectContaining({\n        SHARED_VAR: 'runtime-value'\n      }),\n      request,\n      collection.root\n    );\n  });\n});\n"
  },
  {
    "path": "packages/bruno-electron/tests/network/index.spec.js",
    "content": "const { configureRequest } = require('../../src/ipc/network/index');\n\ndescribe('index: configureRequest', () => {\n  it('Should add \\'http://\\' to the URL if no protocol is specified', async () => {\n    const request = { method: 'GET', url: 'test-domain', body: {} };\n    await configureRequest(null, {}, request, null, null, null, null);\n    expect(request.url).toEqual('http://test-domain');\n  });\n\n  it('Should NOT add \\'http://\\' to the URL if a protocol is specified', async () => {\n    const request = { method: 'GET', url: 'ftp://test-domain', body: {} };\n    await configureRequest(null, {}, request, null, null, null, null);\n    expect(request.url).toEqual('ftp://test-domain');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-electron/tests/network/interpolate-vars.spec.js",
    "content": "const interpolateVars = require('../../src/ipc/network/interpolate-vars');\n\ndescribe('interpolate-vars: interpolateVars', () => {\n  describe('Interpolates string', () => {\n    describe('With environment variables', () => {\n      it('If there\\'s a var with only alphanumeric characters in its name', async () => {\n        const request = { method: 'GET', url: '{{testUrl1}}' };\n\n        const result = interpolateVars(request, { testUrl1: 'test.com' }, null, null);\n        expect(result.url).toEqual('test.com');\n      });\n\n      it('If there\\'s a var with a \\'.\\' in its name', async () => {\n        const request = { method: 'GET', url: '{{test.url}}' };\n\n        const result = interpolateVars(request, { 'test.url': 'test.com' }, null, null);\n        expect(result.url).toEqual('test.com');\n      });\n\n      it('If there\\'s a var with a \\'-\\' in its name', async () => {\n        const request = { method: 'GET', url: '{{test-url}}' };\n\n        const result = interpolateVars(request, { 'test-url': 'test.com' }, null, null);\n        expect(result.url).toEqual('test.com');\n      });\n\n      it('If there\\'s a var with a \\'_\\' in its name', async () => {\n        const request = { method: 'GET', url: '{{test_url}}' };\n\n        const result = interpolateVars(request, { test_url: 'test.com' }, null, null);\n        expect(result.url).toEqual('test.com');\n      });\n\n      it('If there are multiple variables', async () => {\n        const body\n          = '{\\n  \"firstElem\": {{body-var-1}},\\n  \"secondElem\": [{{body.var.2}}],\\n  \"thirdElem\": {\\n    \"fourthElem\": {{body_var_3}},\\n    \"{{varAsKey}}\": {{valueForKey}} }}';\n        const expectedBody\n          = '{\\n  \"firstElem\": Test1,\\n  \"secondElem\": [Test2],\\n  \"thirdElem\": {\\n    \"fourthElem\": Test3,\\n    \"TestKey\": TestValueForKey }}';\n\n        const request = { method: 'POST', url: 'test', data: body, headers: { 'content-type': 'json' } };\n        const result = interpolateVars(\n          request,\n          {\n            'body-var-1': 'Test1',\n            'body.var.2': 'Test2',\n            'body_var_3': 'Test3',\n            'varAsKey': 'TestKey',\n            'valueForKey': 'TestValueForKey'\n          },\n          null,\n          null\n        );\n        expect(result.data).toEqual(expectedBody);\n      });\n    });\n\n    describe('With path params', () => {\n      it('keeps the original url search params as is', async () => {\n        const request = {\n          method: 'GET',\n          url: 'http://example.com/:param/?search=hello world',\n          pathParams: [\n            {\n              type: 'path',\n              name: 'param',\n              value: 'foobar'\n            }\n          ]\n        };\n\n        const result = interpolateVars(request, null, null, null);\n        expect(result.url).toBe('http://example.com/foobar/?search=hello world');\n      });\n\n      it('keeps the original url search params as is even when url might not have protocl ', async () => {\n        const request = {\n          method: 'GET',\n          url: 'example.com/:param/?search=hello world',\n          pathParams: [\n            {\n              type: 'path',\n              name: 'param',\n              value: 'foobar'\n            }\n          ]\n        };\n\n        const result = interpolateVars(request, null, null, null);\n        expect(result.url).toBe('http://example.com/foobar/?search=hello world');\n      });\n\n      it('keeps the original url search params as is even when encoded', async () => {\n        const request = {\n          method: 'GET',\n          url: 'http://example.com/:param?search=hello%20world',\n          pathParams: [\n            {\n              type: 'path',\n              name: 'param',\n              value: 'foobar'\n            }\n          ]\n        };\n\n        const result = interpolateVars(request, null, null, null);\n        expect(result.url).toBe('http://example.com/foobar?search=hello%20world');\n      });\n\n      it('keeps the original url search params as is with edge cases', async () => {\n        const requestOne = {\n          method: 'GET',\n          url: 'https://example.com/:param?x=1#section',\n          pathParams: [\n            {\n              type: 'path',\n              name: 'param',\n              value: 'foobar'\n            }\n          ]\n        };\n\n        const requestTwo = {\n          method: 'GET',\n          url: 'https://example.com/:param?x?y=2',\n          pathParams: [\n            {\n              type: 'path',\n              name: 'param',\n              value: 'foobar'\n            }\n          ]\n        };\n\n        const resultOne = interpolateVars(requestOne, null, null, null);\n        expect(resultOne.url).toBe('https://example.com/foobar?x=1#section');\n\n        const resultTwo = interpolateVars(requestTwo, null, null, null);\n        expect(resultTwo.url).toBe('https://example.com/foobar?x?y=2');\n      });\n\n      it('keeps the original url even without search', async () => {\n        const request = {\n          method: 'GET',\n          url: 'http://example.com/:param',\n          pathParams: [\n            {\n              type: 'path',\n              name: 'param',\n              value: 'foobar'\n            }\n          ]\n        };\n\n        const result = interpolateVars(request, null, null, null);\n        expect(result.url).toBe('http://example.com/foobar');\n      });\n\n      it('updates the path with odata style params | smoke', async () => {\n        const request = {\n          method: 'GET',\n          url: 'http://example.com/Category(\\':CategoryID\\')/Item(:ItemId)/:xpath/Tags(\"tag test\")',\n          pathParams: [\n            {\n              type: 'path',\n              name: 'CategoryID',\n              value: 'foobar'\n            },\n            {\n              type: 'path',\n              name: 'ItemId',\n              value: 1\n            },\n            {\n              type: 'path',\n              name: 'xpath',\n              value: 'foobar'\n            }\n          ]\n        };\n\n        const result = interpolateVars(request, null, null, null);\n        expect(result.url).toBe('http://example.com/Category(\\'foobar\\')/Item(1)/foobar/Tags(%22tag%20test%22)');\n      });\n    });\n\n    describe('With process environment variables', () => {\n      /*\n       * It should NOT turn process env vars into literal segments.\n       * Otherwise, Handlebars will try to access the var literally\n       */\n      it('If there\\'s a var that starts with \\'process.env.\\'', async () => {\n        const request = { method: 'GET', url: '{{process.env.TEST_VAR}}' };\n\n        const result = interpolateVars(request, null, null, { TEST_VAR: 'test.com' });\n        expect(result.url).toEqual('test.com');\n      });\n    });\n\n    describe('With gRPC requests and all variable types', () => {\n      it('Should interpolate collection variables, global environment variables, etc. in gRPC requests', async () => {\n        const request = {\n          method: '/random.Service/randomMethod',\n          url: '{{baseUrl}}/{{service}}/{{method}}',\n          mode: 'grpc',\n          body: {\n            json: '{\"message\": \"{{message}}\", \"id\": {{id}}}'\n          },\n          // Set variable properties on the request object\n          globalEnvironmentVariables: {},\n          collectionVariables: { service: 'greeter' },\n          folderVariables: { method: 'SayHello' },\n          requestVariables: { message: 'Hello World' },\n          oauth2CredentialVariables: {}\n        };\n\n        const result = interpolateVars(\n          request,\n          { baseUrl: 'grpc://localhost:50051' }, // envVars\n          { id: 123 }, // runtimeVariables\n          {} // processEnvVars\n        );\n\n        expect(result.url).toEqual('grpc://localhost:50051/greeter/SayHello');\n        expect(result.body.json).toEqual('{\"message\": \"Hello World\", \"id\": 123}');\n      });\n\n      it('Should handle gRPC requests with global environment variables', async () => {\n        const request = {\n          method: '/random.Service/randomMethod',\n          url: '{{globalBaseUrl}}/{{service}}',\n          mode: 'grpc',\n          body: {\n            json: '{\"token\": \"{{globalToken}}\"}'\n          },\n          // Set variable properties on the request object\n          globalEnvironmentVariables: { globalBaseUrl: 'grpcs://api.example.com', globalToken: 'abc123' },\n          collectionVariables: { service: 'auth' },\n          folderVariables: {},\n          requestVariables: {},\n          oauth2CredentialVariables: {}\n        };\n\n        const result = interpolateVars(\n          request,\n          {}, // envVars\n          {}, // runtimeVariables\n          {} // processEnvVars\n        );\n\n        expect(result.url).toEqual('grpcs://api.example.com/auth');\n        expect(result.body.json).toEqual('{\"token\": \"abc123\"}');\n      });\n    });\n  });\n\n  describe('Does NOT interpolate string', () => {\n    describe('With environment variables', () => {\n      it('If it\\'s not a var (no braces)', async () => {\n        const request = { method: 'GET', url: 'test' };\n\n        const result = interpolateVars(request, { 'test.url': 'test.com' }, null, null);\n        expect(result.url).toEqual('test');\n      });\n\n      it('If it\\'s not a var (only 1 set of braces)', async () => {\n        const request = { method: 'GET', url: '{test.url}' };\n\n        const result = interpolateVars(request, { 'test.url': 'test.com' }, null, null);\n        expect(result.url).toEqual('{test.url}');\n      });\n\n      it('If it\\'s not a var (1 opening & 2 closing braces)', async () => {\n        const request = { method: 'GET', url: '{test.url}}' };\n\n        const result = interpolateVars(request, { 'test.url': 'test.com' }, null, null);\n        expect(result.url).toEqual('{test.url}}');\n      });\n\n      it('If there are no variables (multiple)', async () => {\n        let gqlBody = `{\"query\":\"mutation {\\\\n  test(input: { native: { firstElem: \\\\\"{should-not-get-interpolated}\\\\\", secondElem: \\\\\"{should-not-get-interpolated}}\"}}) {\\\\n    __typename\\\\n    ... on TestType {\\\\n      id\\\\n      identifier\\\\n    }\\\\n  }\\\\n}\",\"variables\":\"{}\"}`;\n\n        const request = { method: 'POST', url: 'test', data: gqlBody };\n        const result = interpolateVars(request, { 'should-not-get-interpolated': 'ERROR' }, null, null);\n        expect(result.data).toEqual(gqlBody);\n      });\n    });\n  });\n\n  describe('Handles content-type header set to false', () => {\n    it('Should result empty data', async () => {\n      const request = { method: 'POST', url: 'test', data: undefined, headers: { 'content-type': false } };\n\n      const result = interpolateVars(request, { 'test.url': 'test.com' }, null, null);\n      expect(result.data).toEqual(undefined);\n    });\n  });\n\n  describe('Multipart body (multipart/form-data and multipart/mixed)', () => {\n    it('interpolates value in each part when Content-Type is multipart/form-data', () => {\n      const request = {\n        method: 'POST',\n        url: 'http://api.example/upload',\n        headers: { 'Content-Type': 'multipart/form-data; boundary=----boundary' },\n        data: [\n          { name: 'field1', value: '{{token}}', type: 'text' },\n          { name: 'field2', value: 'static', type: 'text' },\n          { name: 'field3', value: '{{prefix}}-suffix', type: 'text' }\n        ]\n      };\n\n      const result = interpolateVars(\n        request,\n        { token: 'secret123', prefix: 'my' },\n        null,\n        null\n      );\n\n      expect(result.data).toEqual([\n        { name: 'field1', value: 'secret123', type: 'text' },\n        { name: 'field2', value: 'static', type: 'text' },\n        { name: 'field3', value: 'my-suffix', type: 'text' }\n      ]);\n    });\n\n    it('interpolates value in each part when Content-Type is multipart/mixed', () => {\n      const request = {\n        method: 'POST',\n        url: 'http://api.example/send',\n        headers: { 'Content-Type': 'multipart/mixed; boundary=----mixed' },\n        data: [\n          { name: 'part1', value: '{{envVar}}', type: 'text' },\n          { name: 'part2', value: '{{another}}', type: 'text' }\n        ]\n      };\n\n      const result = interpolateVars(\n        request,\n        { envVar: 'first', another: 'second' },\n        null,\n        null\n      );\n\n      expect(result.data).toEqual([\n        { name: 'part1', value: 'first', type: 'text' },\n        { name: 'part2', value: 'second', type: 'text' }\n      ]);\n    });\n\n    it('leaves part keys (name, type, etc.) intact and only interpolates value', () => {\n      const request = {\n        method: 'POST',\n        url: 'http://api.example/upload',\n        headers: { 'Content-Type': 'multipart/form-data' },\n        data: [\n          { name: 'file', value: '{{path}}', type: 'file', fileName: 'doc.pdf' }\n        ]\n      };\n\n      const result = interpolateVars(request, { path: '/tmp/doc.pdf' }, null, null);\n\n      expect(result.data).toHaveLength(1);\n      expect(result.data[0].name).toBe('file');\n      expect(result.data[0].type).toBe('file');\n      expect(result.data[0].fileName).toBe('doc.pdf');\n      expect(result.data[0].value).toBe('/tmp/doc.pdf');\n    });\n\n    it('handles empty multipart array', () => {\n      const request = {\n        method: 'POST',\n        url: 'http://api.example/upload',\n        headers: { 'Content-Type': 'multipart/form-data' },\n        data: []\n      };\n\n      const result = interpolateVars(request, { x: 'y' }, null, null);\n\n      expect(result.data).toEqual([]);\n    });\n\n    it('handles part with missing or undefined value', () => {\n      const request = {\n        method: 'POST',\n        url: 'http://api.example/upload',\n        headers: { 'Content-Type': 'multipart/form-data' },\n        data: [\n          { name: 'a', value: '{{present}}' },\n          { name: 'b' },\n          { name: 'c', value: undefined }\n        ]\n      };\n\n      const result = interpolateVars(request, { present: 'ok' }, null, null);\n\n      expect(result.data[0].value).toBe('ok');\n      expect(result.data[1].value).toBeUndefined();\n      expect(result.data[2].value).toBeUndefined();\n    });\n\n    it('preserves raw string body when Content-Type is multipart/mixed (manually constructed multipart)', () => {\n      // Equivalent to: curl -X POST https://httpbin.dev/post \\\n      //   -H 'content-type: multipart/mixed; boundary=TestBoundary123' \\\n      //   --data '--TestBoundary123\\r\\nContent-Type: application/json\\r\\n\\r\\n{\"test\": true}\\r\\n--TestBoundary123--\\r\\n'\n      const rawMultipartBody = [\n        '--TestBoundary123',\n        'Content-Type: application/json',\n        '',\n        '{\"test\": true}',\n        '--TestBoundary123--',\n        ''\n      ].join('\\r\\n');\n\n      const request = {\n        method: 'POST',\n        url: 'https://httpbin.dev/post',\n        headers: { 'content-type': 'multipart/mixed; boundary=TestBoundary123' },\n        data: rawMultipartBody\n      };\n\n      const result = interpolateVars(request, {}, null, null);\n\n      expect(result.data).toBe(rawMultipartBody);\n      expect(result.data).toContain('--TestBoundary123');\n      expect(result.data).toContain('Content-Type: application/json');\n      expect(result.data).toContain('{\"test\": true}');\n      expect(result.data).toContain('--TestBoundary123--');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-electron/tests/network/prepare-gql-introspection-request.spec.js",
    "content": "const prepareGqlIntrospectionRequest = require('../../src/ipc/network/prepare-gql-introspection-request');\n\ndescribe('prepareGqlIntrospectionRequest', () => {\n  const createBasicSetup = () => ({\n    endpoint: 'https://example.com/',\n    request: {\n      headers: []\n    },\n    collectionRoot: {\n      request: {\n        headers: []\n      }\n    }\n  });\n\n  it('should handle environment variables in headers', () => {\n    const setup = createBasicSetup();\n    setup.request.headers = [\n      { name: 'Authorization', value: 'Bearer {{AUTH_TOKEN}}', enabled: true }\n    ];\n    const vars = {\n      AUTH_TOKEN: 'token-value'\n    };\n\n    const result = prepareGqlIntrospectionRequest(setup.endpoint, vars, setup.request, setup.collectionRoot);\n\n    expect(result.headers['Authorization']).toBe('Bearer token-value');\n    expect(result.method).toBe('POST');\n    expect(result.url).toBe(setup.endpoint);\n  });\n\n  it('should override collection headers with request headers', () => {\n    const setup = createBasicSetup();\n    setup.collectionRoot.request.headers = [\n      { name: 'X-Header', value: 'collection-value', enabled: true }\n    ];\n    setup.request.headers = [\n      { name: 'X-Header', value: 'request-value', enabled: true }\n    ];\n\n    const result = prepareGqlIntrospectionRequest(setup.endpoint, {}, setup.request, setup.collectionRoot);\n\n    expect(result.headers['X-Header']).toBe('request-value');\n  });\n\n  it('should handle enabled and disabled headers', () => {\n    const setup = createBasicSetup();\n    setup.request.headers = [\n      { name: 'X-Enabled', value: 'enabled', enabled: true },\n      { name: 'X-Disabled', value: 'disabled', enabled: false }\n    ];\n\n    const result = prepareGqlIntrospectionRequest(setup.endpoint, {}, setup.request, setup.collectionRoot);\n\n    expect(result.headers['X-Enabled']).toBe('enabled');\n    expect(result.headers['X-Disabled']).toBeUndefined();\n  });\n\n  it('should always include required GraphQL headers', () => {\n    const setup = createBasicSetup();\n    const result = prepareGqlIntrospectionRequest(setup.endpoint, {}, setup.request, setup.collectionRoot);\n    expect(result.headers['Accept']).toBe('application/json');\n    expect(result.headers['Content-Type']).toBe('application/json');\n  });\n\n  it('should handle process.env variables in endpoint URL', () => {\n    const setup = createBasicSetup();\n    setup.endpoint = 'https://{{process.env.API_HOST}}/graphql';\n    const vars = {\n      process: {\n        env: {\n          API_HOST: 'api.example.com'\n        }\n      }\n    };\n\n    const result = prepareGqlIntrospectionRequest(setup.endpoint, vars, setup.request, setup.collectionRoot);\n\n    expect(result.url).toBe('https://api.example.com/graphql');\n    expect(result.method).toBe('POST');\n  });\n\n  it('should handle missing process.env variables gracefully', () => {\n    const setup = createBasicSetup();\n    setup.request.headers = [\n      { name: 'X-API-Key', value: '{{process.env.MISSING_VAR}}', enabled: true }\n    ];\n\n    const result = prepareGqlIntrospectionRequest(setup.endpoint, {}, setup.request, setup.collectionRoot);\n\n    expect(result.headers['X-API-Key']).toBe('{{process.env.MISSING_VAR}}');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-electron/tests/network/prepare-grpc-request.spec.js",
    "content": "const { describe, it, expect, beforeEach } = require('@jest/globals');\n\n// Mock dependencies\njest.mock('../../src/ipc/network/interpolate-vars');\njest.mock('../../src/utils/collection');\njest.mock('../../src/store/process-env');\njest.mock('../../src/utils/oauth2');\njest.mock('../../src/ipc/network/prepare-request');\n\nconst prepareGrpcRequest = require('../../src/ipc/network/prepare-grpc-request');\nconst interpolateVars = require('../../src/ipc/network/interpolate-vars');\nconst { getEnvVars, getTreePathFromCollectionToItem } = require('../../src/utils/collection');\nconst { getProcessEnvVars } = require('../../src/store/process-env');\nconst { setAuthHeaders } = require('../../src/ipc/network/prepare-request');\n\ndescribe('prepare-grpc-request: prepareGrpcRequest', () => {\n  let mockItem;\n  let mockCollection;\n  let mockEnvironment;\n  let mockRuntimeVariables;\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n\n    getEnvVars.mockReturnValue({});\n    getTreePathFromCollectionToItem.mockReturnValue([]);\n    getProcessEnvVars.mockReturnValue({});\n    setAuthHeaders.mockImplementation((request) => request);\n    interpolateVars.mockImplementation((request) => request);\n\n    mockItem = {\n      uid: 'test-item-uid',\n      request: {\n        method: 'POST',\n        methodType: 'unary',\n        url: 'grpc://localhost:50051',\n        headers: [],\n        body: {\n          mode: 'json',\n          json: '{\"test\": \"data\"}'\n        },\n        protoPath: '/path/to/proto.proto',\n        auth: { mode: 'none' }\n      }\n    };\n\n    mockCollection = {\n      uid: 'test-collection-uid',\n      root: {\n        request: {\n          headers: []\n        }\n      },\n      brunoConfig: {\n        scripts: {\n          flow: 'sandwich'\n        }\n      }\n    };\n\n    mockEnvironment = {};\n    mockRuntimeVariables = {};\n  });\n\n  describe('Header processing', () => {\n    it('should keep regular headers as strings', async () => {\n      mockItem.request.headers = [\n        { name: 'content-type', value: 'application/grpc', enabled: true },\n        { name: 'authorization', value: 'Bearer token123', enabled: true },\n        { name: 'user-agent', value: 'bruno-client', enabled: true }\n      ];\n\n      const result = await prepareGrpcRequest(mockItem, mockCollection, mockEnvironment, mockRuntimeVariables);\n\n      expect(result.headers['content-type']).toBe('application/grpc');\n      expect(result.headers['authorization']).toBe('Bearer token123');\n      expect(result.headers['user-agent']).toBe('bruno-client');\n      expect(typeof result.headers['content-type']).toBe('string');\n      expect(typeof result.headers['authorization']).toBe('string');\n      expect(typeof result.headers['user-agent']).toBe('string');\n    });\n\n    it('should skip disabled headers', async () => {\n      mockItem.request.headers = [\n        { name: 'content-type', value: 'application/grpc', enabled: false },\n        { name: 'authorization', value: 'Bearer token123', enabled: false }\n      ];\n\n      const result = await prepareGrpcRequest(mockItem, mockCollection, mockEnvironment, mockRuntimeVariables);\n\n      expect(result.headers).toEqual({});\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-electron/tests/network/prepare-request.spec.js",
    "content": "const { describe, it, expect } = require('@jest/globals');\n\nconst { prepareRequest } = require('../../src/ipc/network/prepare-request');\n\ndescribe('prepare-request: prepareRequest', () => {\n  describe('Decomments request body', () => {\n    it('If request body is valid JSON', async () => {\n      const body = { mode: 'json', json: '{\\n\"test\": \"{{someVar}}\" // comment\\n}' };\n      const expected = `{\n\\\"test\\\": \\\"{{someVar}}\\\" \n}`;\n      const result = await prepareRequest({ request: { body }, collection: { pathname: '' } });\n      expect(result.data).toEqual(expected);\n    });\n\n    it('If request body is not valid JSON', async () => {\n      const body = { mode: 'json', json: '{\\n\"test\": {{someVar}} // comment\\n}' };\n      const expected = '{\\n\"test\": {{someVar}} \\n}';\n      const result = await prepareRequest({ request: { body }, collection: { pathname: '' } });\n      expect(result.data).toEqual(expected);\n    });\n  });\n\n  describe.each(['POST', 'PUT', 'PATCH'])('POST request with no body', (method) => {\n    it('Should set content-type header to false if method is ' + method + ' and there is no data in the body', async () => {\n      const request = { method: method, url: 'test-domain', body: { mode: 'none' }, auth: { mode: 'none' } };\n      const result = await prepareRequest({ request, collection: { pathname: '' } });\n      expect(result.headers['content-type']).toEqual(false);\n    });\n    it('Should respect the content-type header if explicitly set', async () => {\n      const request = {\n        method: method,\n        url: 'test-domain',\n        body: { mode: 'none' },\n        headers: [{ name: 'content-type', value: 'application/json', enabled: true }],\n        auth: { mode: 'none' }\n      };\n      const result = await prepareRequest({ request, collection: { pathname: '' } });\n      expect(result.headers['content-type']).toEqual('application/json');\n    });\n  });\n\n  describe('GraphQL request', () => {\n    it('keeps variables as string for interpolation', async () => {\n      const item = {\n        request: {\n          method: 'POST',\n          headers: [],\n          params: [],\n          url: 'https://example.com',\n          body: {\n            mode: 'graphql',\n            graphql: {\n              query: 'query { x }',\n              variables: '{\"apiPermissions\": {{permissionsJSON}}}'\n            }\n          }\n        }\n      };\n      const result = await prepareRequest(item);\n      expect(result.mode).toBe('graphql');\n      expect(result.data).toMatchObject({ query: 'query { x }' });\n      expect(typeof result.data.variables).toBe('string');\n      expect(result.data.variables).toBe('{\"apiPermissions\": {{permissionsJSON}}}');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-electron/tests/network/prepare-ws-request.spec.js",
    "content": "// Mock dependencies before requiring the module\nconst { prepareWsRequest } = require('../../src/ipc/network/ws-event-handlers');\n\ndescribe('prepareWsRequest: API Key Query Params', () => {\n  const createMockItem = (authConfig = {}) => ({\n    uid: 'test-item-uid',\n    request: {\n      url: 'ws://localhost:3001',\n      headers: [],\n      body: {\n        mode: 'raw',\n        ws: []\n      },\n      auth: authConfig,\n      vars: { req: [], res: [] },\n      script: { req: '', res: '' }\n    }\n  });\n\n  const createMockCollection = (collectionAuth = null) => ({\n    uid: 'test-collection-uid',\n    pathname: '/test/path',\n    root: {\n      request: {\n        headers: [],\n        auth: collectionAuth || { mode: 'none' }\n      }\n    },\n    brunoConfig: {},\n    globalEnvironmentVariables: {},\n    promptVariables: {},\n    items: []\n  });\n\n  describe('API Key with Query Params placement', () => {\n    it('should append API key to URL when placement is queryparams', async () => {\n      const item = createMockItem({\n        mode: 'apikey',\n        apikey: {\n          key: 'apiKey',\n          value: 'test-api-key-123',\n          placement: 'queryparams'\n        }\n      });\n      const collection = createMockCollection();\n      const environment = { variables: [] };\n      const runtimeVariables = {};\n\n      const result = await prepareWsRequest(item, collection, environment, runtimeVariables);\n\n      expect(result.url).toContain('apiKey=test-api-key-123');\n      expect(result.url).toBe('ws://localhost:3001/?apiKey=test-api-key-123');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-electron/tests/prepare-request.test.js",
    "content": "const crypto = require('node:crypto');\n\n// Mock crypto.randomBytes to return predictable values for testing\njest.mock('node:crypto', () => ({\n  ...jest.requireActual('node:crypto'),\n  randomBytes: jest.fn(() => Buffer.from('1234567890abcdef', 'hex'))\n}));\n\n// Mock the lodash get function with a more sophisticated mock\nconst mockGet = jest.fn();\njest.mock('lodash', () => ({\n  get: mockGet,\n  each: jest.fn(),\n  filter: jest.fn(),\n  find: jest.fn()\n}));\n\n// Import the function to test\nconst { setAuthHeaders } = require('../src/ipc/network/prepare-request');\n\ndescribe('setAuthHeaders', () => {\n  let mockAxiosRequest;\n  let mockRequest;\n  let mockCollectionRoot;\n\n  beforeEach(() => {\n    // Reset all mocks\n    jest.clearAllMocks();\n\n    // Reset crypto mock to return predictable values\n    crypto.randomBytes.mockReturnValue(Buffer.from('1234567890abcdef', 'hex'));\n\n    // Setup default mock objects\n    mockAxiosRequest = {\n      headers: {}\n    };\n\n    mockRequest = {\n      auth: {\n        mode: 'none'\n      }\n    };\n\n    mockCollectionRoot = {\n      request: {\n        auth: null\n      }\n    };\n\n    // Setup a more sophisticated mock for lodash get function\n    mockGet.mockImplementation((obj, path, defaultValue) => {\n      if (!obj) return defaultValue;\n\n      const keys = path.split('.');\n      let current = obj;\n\n      for (const key of keys) {\n        if (current && typeof current === 'object' && key in current) {\n          current = current[key];\n        } else {\n          return defaultValue;\n        }\n      }\n\n      return current;\n    });\n  });\n\n  describe('Collection-level authentication inheritance', () => {\n    test('should inherit AWS v4 authentication from collection', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'awsv4',\n        awsv4: {\n          accessKeyId: 'test-access-key',\n          secretAccessKey: 'test-secret-key',\n          sessionToken: 'test-session-token',\n          service: 's3',\n          region: 'us-east-1',\n          profileName: 'default'\n        }\n      };\n\n      mockRequest.auth.mode = 'inherit';\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.awsv4config).toEqual({\n        accessKeyId: 'test-access-key',\n        secretAccessKey: 'test-secret-key',\n        sessionToken: 'test-session-token',\n        service: 's3',\n        region: 'us-east-1',\n        profileName: 'default'\n      });\n    });\n\n    test('should inherit basic authentication from collection', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'basic',\n        basic: {\n          username: 'testuser',\n          password: 'testpass'\n        }\n      };\n\n      mockRequest.auth.mode = 'inherit';\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.basicAuth).toEqual({\n        username: 'testuser',\n        password: 'testpass'\n      });\n    });\n\n    test('should inherit bearer authentication from collection', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'bearer',\n        bearer: {\n          token: 'test-token'\n        }\n      };\n\n      mockRequest.auth.mode = 'inherit';\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.headers['Authorization']).toBe('Bearer test-token');\n    });\n\n    test('should inherit digest authentication from collection', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'digest',\n        digest: {\n          username: 'testuser',\n          password: 'testpass'\n        }\n      };\n\n      mockRequest.auth.mode = 'inherit';\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.digestConfig).toEqual({\n        username: 'testuser',\n        password: 'testpass'\n      });\n    });\n\n    test('should inherit NTLM authentication from collection', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'ntlm',\n        ntlm: {\n          username: 'testuser',\n          password: 'testpass',\n          domain: 'testdomain'\n        }\n      };\n\n      mockRequest.auth.mode = 'inherit';\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.ntlmConfig).toEqual({\n        username: 'testuser',\n        password: 'testpass',\n        domain: 'testdomain'\n      });\n    });\n\n    test('should inherit WSSE authentication from collection', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'wsse',\n        wsse: {\n          username: 'testuser',\n          password: 'testpass'\n        }\n      };\n\n      mockRequest.auth.mode = 'inherit';\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.headers['X-WSSE']).toMatch(/UsernameToken Username=\"testuser\", PasswordDigest=\"[^\"]+\", Nonce=\"1234567890abcdef\", Created=\"[^\"]+\"/);\n    });\n\n    test('should inherit API key authentication from collection (header placement)', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'apikey',\n        apikey: {\n          key: 'X-API-Key',\n          value: 'test-api-key',\n          placement: 'header'\n        }\n      };\n\n      mockRequest.auth.mode = 'inherit';\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.headers['X-API-Key']).toBe('test-api-key');\n    });\n\n    test('should inherit API key authentication from collection (query params placement)', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'apikey',\n        apikey: {\n          key: 'api_key',\n          value: 'test-api-key',\n          placement: 'queryparams'\n        }\n      };\n\n      mockRequest.auth.mode = 'inherit';\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.apiKeyAuthValueForQueryParams).toEqual({\n        key: 'api_key',\n        value: 'test-api-key',\n        placement: 'queryparams'\n      });\n    });\n\n    test('should skip API key authentication when key is empty', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'apikey',\n        apikey: {\n          key: '',\n          value: 'test-api-key',\n          placement: 'header'\n        }\n      };\n\n      mockRequest.auth.mode = 'inherit';\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.headers['']).toBeUndefined();\n    });\n  });\n\n  describe('OAuth2 authentication inheritance', () => {\n    test('should inherit OAuth2 password grant from collection', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'oauth2',\n        oauth2: {\n          grantType: 'password',\n          accessTokenUrl: 'https://example.com/token',\n          refreshTokenUrl: 'https://example.com/refresh',\n          username: 'testuser',\n          password: 'testpass',\n          clientId: 'test-client',\n          clientSecret: 'test-secret',\n          scope: 'read write',\n          credentialsPlacement: 'body',\n          credentialsId: 'test-credentials',\n          tokenPlacement: 'header',\n          tokenHeaderPrefix: 'Bearer',\n          tokenQueryKey: 'access_token',\n          autoFetchToken: true,\n          autoRefreshToken: true,\n          additionalParameters: { authorization: [], token: [], refresh: [] }\n        }\n      };\n\n      mockRequest.auth.mode = 'inherit';\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.oauth2).toEqual({\n        grantType: 'password',\n        accessTokenUrl: 'https://example.com/token',\n        refreshTokenUrl: 'https://example.com/refresh',\n        username: 'testuser',\n        password: 'testpass',\n        clientId: 'test-client',\n        clientSecret: 'test-secret',\n        scope: 'read write',\n        credentialsPlacement: 'body',\n        credentialsId: 'test-credentials',\n        tokenPlacement: 'header',\n        tokenHeaderPrefix: 'Bearer',\n        tokenQueryKey: 'access_token',\n        autoFetchToken: true,\n        autoRefreshToken: true,\n        additionalParameters: { authorization: [], token: [], refresh: [] }\n      });\n    });\n\n    test('should inherit OAuth2 authorization_code grant from collection', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'oauth2',\n        oauth2: {\n          grantType: 'authorization_code',\n          callbackUrl: 'https://example.com/callback',\n          authorizationUrl: 'https://example.com/auth',\n          accessTokenUrl: 'https://example.com/token',\n          refreshTokenUrl: 'https://example.com/refresh',\n          clientId: 'test-client',\n          clientSecret: 'test-secret',\n          scope: 'read write',\n          state: 'random-state',\n          pkce: true,\n          credentialsPlacement: 'body',\n          credentialsId: 'test-credentials',\n          tokenPlacement: 'header',\n          tokenHeaderPrefix: 'Bearer',\n          tokenQueryKey: 'access_token',\n          autoFetchToken: true,\n          autoRefreshToken: true,\n          additionalParameters: { authorization: [], token: [], refresh: [] }\n        }\n      };\n\n      mockRequest.auth.mode = 'inherit';\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.oauth2).toEqual({\n        grantType: 'authorization_code',\n        callbackUrl: 'https://example.com/callback',\n        authorizationUrl: 'https://example.com/auth',\n        accessTokenUrl: 'https://example.com/token',\n        refreshTokenUrl: 'https://example.com/refresh',\n        clientId: 'test-client',\n        scope: 'read write',\n        state: 'random-state',\n        pkce: true,\n        credentialsPlacement: 'body',\n        clientSecret: 'test-secret',\n        credentialsId: 'test-credentials',\n        tokenPlacement: 'header',\n        tokenHeaderPrefix: 'Bearer',\n        tokenQueryKey: 'access_token',\n        autoFetchToken: true,\n        autoRefreshToken: true,\n        additionalParameters: { authorization: [], token: [], refresh: [] }\n      });\n    });\n\n    test('should inherit OAuth2 implicit grant from collection', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'oauth2',\n        oauth2: {\n          grantType: 'implicit',\n          callbackUrl: 'https://example.com/callback',\n          authorizationUrl: 'https://example.com/auth',\n          clientId: 'test-client',\n          scope: 'read write',\n          state: 'random-state',\n          credentialsId: 'test-credentials',\n          tokenPlacement: 'header',\n          tokenHeaderPrefix: 'Bearer',\n          tokenQueryKey: 'access_token',\n          autoFetchToken: true,\n          additionalParameters: { authorization: [], token: [], refresh: [] }\n        }\n      };\n\n      mockRequest.auth.mode = 'inherit';\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.oauth2).toEqual({\n        grantType: 'implicit',\n        callbackUrl: 'https://example.com/callback',\n        authorizationUrl: 'https://example.com/auth',\n        clientId: 'test-client',\n        scope: 'read write',\n        state: 'random-state',\n        credentialsId: 'test-credentials',\n        tokenPlacement: 'header',\n        tokenHeaderPrefix: 'Bearer',\n        tokenQueryKey: 'access_token',\n        autoFetchToken: true,\n        additionalParameters: { authorization: [], token: [], refresh: [] }\n      });\n    });\n\n    test('should inherit OAuth2 client_credentials grant from collection', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'oauth2',\n        oauth2: {\n          grantType: 'client_credentials',\n          accessTokenUrl: 'https://example.com/token',\n          refreshTokenUrl: 'https://example.com/refresh',\n          clientId: 'test-client',\n          clientSecret: 'test-secret',\n          scope: 'read write',\n          credentialsPlacement: 'body',\n          credentialsId: 'test-credentials',\n          tokenPlacement: 'header',\n          tokenHeaderPrefix: 'Bearer',\n          tokenQueryKey: 'access_token',\n          autoFetchToken: true,\n          autoRefreshToken: true,\n          additionalParameters: { authorization: [], token: [], refresh: [] }\n        }\n      };\n\n      mockRequest.auth.mode = 'inherit';\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.oauth2).toEqual({\n        grantType: 'client_credentials',\n        accessTokenUrl: 'https://example.com/token',\n        refreshTokenUrl: 'https://example.com/refresh',\n        clientId: 'test-client',\n        clientSecret: 'test-secret',\n        scope: 'read write',\n        credentialsPlacement: 'body',\n        credentialsId: 'test-credentials',\n        tokenPlacement: 'header',\n        tokenHeaderPrefix: 'Bearer',\n        tokenQueryKey: 'access_token',\n        autoFetchToken: true,\n        autoRefreshToken: true,\n        additionalParameters: { authorization: [], token: [], refresh: [] }\n      });\n    });\n  });\n\n  describe('Request-level authentication (overrides collection)', () => {\n    test('should set AWS v4 authentication at request level', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'awsv4',\n        awsv4: {\n          accessKeyId: 'test-access-key',\n          secretAccessKey: 'test-secret-key',\n          sessionToken: 'test-session-token',\n          service: 's3',\n          region: 'us-east-1',\n          profileName: 'default'\n        }\n      };\n      mockRequest.auth = {\n        mode: 'awsv4',\n        awsv4: {\n          accessKeyId: 'request-access-key',\n          secretAccessKey: 'request-secret-key',\n          sessionToken: 'request-session-token',\n          service: 's3',\n          region: 'us-west-2',\n          profileName: 'production'\n        }\n      };\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.awsv4config).toEqual({\n        accessKeyId: 'request-access-key',\n        secretAccessKey: 'request-secret-key',\n        sessionToken: 'request-session-token',\n        service: 's3',\n        region: 'us-west-2',\n        profileName: 'production'\n      });\n    });\n\n    test('should set basic authentication at request level', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'basic',\n        basic: {\n          username: 'testuser',\n          password: 'testpass'\n        }\n      };\n      mockRequest.auth = {\n        mode: 'basic',\n        basic: {\n          username: 'requestuser',\n          password: 'requestpass'\n        }\n      };\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.basicAuth).toEqual({\n        username: 'requestuser',\n        password: 'requestpass'\n      });\n    });\n\n    test('should set bearer authentication at request level', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'bearer',\n        bearer: {\n          token: 'test-token'\n        }\n      };\n      mockRequest.auth = {\n        mode: 'bearer',\n        bearer: {\n          token: 'request-token'\n        }\n      };\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.headers['Authorization']).toBe('Bearer request-token');\n    });\n\n    test('should set digest authentication at request level', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'digest',\n        digest: {\n          username: 'testuser',\n          password: 'testpass'\n        }\n      };\n      mockRequest.auth = {\n        mode: 'digest',\n        digest: {\n          username: 'requestuser',\n          password: 'requestpass'\n        }\n      };\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.digestConfig).toEqual({\n        username: 'requestuser',\n        password: 'requestpass'\n      });\n    });\n\n    test('should set NTLM authentication at request level', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'ntlm',\n        ntlm: {\n          username: 'testuser',\n          password: 'testpass',\n          domain: 'testdomain'\n        }\n      };\n      mockRequest.auth = {\n        mode: 'ntlm',\n        ntlm: {\n          username: 'requestuser',\n          password: 'requestpass',\n          domain: 'requestdomain'\n        }\n      };\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.ntlmConfig).toEqual({\n        username: 'requestuser',\n        password: 'requestpass',\n        domain: 'requestdomain'\n      });\n    });\n\n    test('should set WSSE authentication at request level', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'wsse',\n        wsse: {\n          username: 'testuser',\n          password: 'testpass'\n        }\n      };\n      mockRequest.auth = {\n        mode: 'wsse',\n        wsse: {\n          username: 'requestuser',\n          password: 'requestpass'\n        }\n      };\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.headers['X-WSSE']).toMatch(/UsernameToken Username=\"requestuser\", PasswordDigest=\"[^\"]+\", Nonce=\"1234567890abcdef\", Created=\"[^\"]+\"/);\n    });\n\n    test('should set API key authentication at request level (header placement)', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'apikey',\n        apikey: {\n          key: 'X-Request-API-Key',\n          value: 'test-api-key',\n          placement: 'header'\n        }\n      };\n      mockRequest.auth = {\n        mode: 'apikey',\n        apikey: {\n          key: 'X-Request-API-Key',\n          value: 'request-api-key',\n          placement: 'header'\n        }\n      };\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.headers['X-Request-API-Key']).toBe('request-api-key');\n    });\n\n    test('should set API key authentication at request level (query params placement)', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'apikey',\n        apikey: {\n          key: 'X-Request-API-Key',\n          value: 'test-api-key',\n          placement: 'header'\n        }\n      };\n      mockRequest.auth = {\n        mode: 'apikey',\n        apikey: {\n          key: 'request_api_key',\n          value: 'request-api-key',\n          placement: 'queryparams'\n        }\n      };\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.apiKeyAuthValueForQueryParams).toEqual({\n        key: 'request_api_key',\n        value: 'request-api-key',\n        placement: 'queryparams'\n      });\n    });\n\n    test('should set OAuth2 password grant at request level', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'oauth2',\n        oauth2: {\n          grantType: 'password',\n          accessTokenUrl: 'https://collection.com/token',\n          refreshTokenUrl: 'https://collection.com/refresh',\n          username: 'collectionuser',\n          password: 'collectionpass',\n          clientId: 'collection-client',\n          clientSecret: 'collection-secret',\n          scope: 'read',\n          credentialsPlacement: 'header',\n          credentialsId: 'collection-credentials',\n          tokenPlacement: 'query',\n          tokenHeaderPrefix: 'Token',\n          tokenQueryKey: 'token',\n          autoFetchToken: false,\n          autoRefreshToken: false,\n          additionalParameters: { authorization: [], token: [], refresh: [] }\n        }\n      };\n      mockRequest.auth = {\n        mode: 'oauth2',\n        oauth2: {\n          grantType: 'password',\n          accessTokenUrl: 'https://request.com/token',\n          refreshTokenUrl: 'https://request.com/refresh',\n          username: 'requestuser',\n          password: 'requestpass',\n          clientId: 'request-client',\n          clientSecret: 'request-secret',\n          scope: 'read',\n          credentialsPlacement: 'header',\n          credentialsId: 'request-credentials',\n          tokenPlacement: 'query',\n          tokenHeaderPrefix: 'Token',\n          tokenQueryKey: 'token',\n          autoFetchToken: false,\n          autoRefreshToken: false,\n          additionalParameters: { authorization: [], token: [], refresh: [] }\n        }\n      };\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.oauth2).toEqual({\n        grantType: 'password',\n        accessTokenUrl: 'https://request.com/token',\n        refreshTokenUrl: 'https://request.com/refresh',\n        username: 'requestuser',\n        password: 'requestpass',\n        clientId: 'request-client',\n        clientSecret: 'request-secret',\n        scope: 'read',\n        credentialsPlacement: 'header',\n        credentialsId: 'request-credentials',\n        tokenPlacement: 'query',\n        tokenHeaderPrefix: 'Token',\n        tokenQueryKey: 'token',\n        autoFetchToken: false,\n        autoRefreshToken: false,\n        additionalParameters: { authorization: [], token: [], refresh: [] }\n      });\n    });\n\n    test('should set OAuth2 authorization_code grant at request level', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'oauth2',\n        oauth2: {\n          grantType: 'password',\n          callbackUrl: 'https://collection.com/callback',\n          authorizationUrl: 'https://collection.com/auth',\n          accessTokenUrl: 'https://collection.com/token',\n          refreshTokenUrl: 'https://collection.com/refresh',\n          username: 'collectionuser',\n          password: 'collectionpass',\n          clientId: 'collection-client',\n          clientSecret: 'collection-secret'\n        }\n      };\n      mockRequest.auth = {\n        mode: 'oauth2',\n        oauth2: {\n          grantType: 'authorization_code',\n          callbackUrl: 'https://request.com/callback',\n          authorizationUrl: 'https://request.com/auth',\n          accessTokenUrl: 'https://request.com/token',\n          refreshTokenUrl: 'https://request.com/refresh',\n          clientId: 'request-client',\n          clientSecret: 'request-secret',\n          scope: 'read',\n          state: 'request-state',\n          pkce: false,\n          credentialsPlacement: 'body',\n          credentialsId: 'request-credentials',\n          tokenPlacement: 'header',\n          tokenHeaderPrefix: 'Bearer',\n          tokenQueryKey: 'access_token',\n          autoFetchToken: true,\n          autoRefreshToken: true,\n          additionalParameters: { authorization: [], token: [], refresh: [] }\n        }\n      };\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.oauth2).toEqual({\n        grantType: 'authorization_code',\n        callbackUrl: 'https://request.com/callback',\n        authorizationUrl: 'https://request.com/auth',\n        accessTokenUrl: 'https://request.com/token',\n        refreshTokenUrl: 'https://request.com/refresh',\n        clientId: 'request-client',\n        clientSecret: 'request-secret',\n        scope: 'read',\n        state: 'request-state',\n        pkce: false,\n        credentialsPlacement: 'body',\n        credentialsId: 'request-credentials',\n        tokenPlacement: 'header',\n        tokenHeaderPrefix: 'Bearer',\n        tokenQueryKey: 'access_token',\n        autoFetchToken: true,\n        autoRefreshToken: true,\n        additionalParameters: { authorization: [], token: [], refresh: [] }\n      });\n    });\n\n    test('should set OAuth2 implicit grant at request level', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'oauth2',\n        oauth2: {\n          grantType: 'implicit',\n          callbackUrl: 'https://collection.com/callback',\n          authorizationUrl: 'https://collection.com/auth',\n          clientId: 'collection-client',\n          scope: 'read',\n          state: 'collection-state',\n          credentialsId: 'collection-credentials',\n          tokenPlacement: 'header',\n          tokenHeaderPrefix: 'Bearer',\n          tokenQueryKey: 'access_token',\n          autoFetchToken: true,\n          additionalParameters: { authorization: [], token: [], refresh: [] }\n        }\n      };\n      mockRequest.auth = {\n        mode: 'oauth2',\n        oauth2: {\n          grantType: 'implicit',\n          callbackUrl: 'https://request.com/callback',\n          authorizationUrl: 'https://request.com/auth',\n          clientId: 'request-client',\n          scope: 'read',\n          state: 'request-state',\n          credentialsId: 'request-credentials',\n          tokenPlacement: 'query',\n          tokenHeaderPrefix: 'Token',\n          tokenQueryKey: 'token',\n          autoFetchToken: false,\n          additionalParameters: { authorization: [], token: [], refresh: [] }\n        }\n      };\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.oauth2).toEqual({\n        grantType: 'implicit',\n        callbackUrl: 'https://request.com/callback',\n        authorizationUrl: 'https://request.com/auth',\n        clientId: 'request-client',\n        credentialsId: 'request-credentials',\n        scope: 'read',\n        state: 'request-state',\n        tokenPlacement: 'query',\n        tokenHeaderPrefix: 'Token',\n        tokenQueryKey: 'token',\n        autoFetchToken: false,\n        additionalParameters: { authorization: [], token: [], refresh: [] }\n      });\n    });\n\n    test('should set OAuth2 client_credentials grant at request level', () => {\n      mockCollectionRoot.request.auth = {\n        mode: 'oauth2',\n        oauth2: {\n          grantType: 'client_credentials',\n          accessTokenUrl: 'https://collection.com/token',\n          refreshTokenUrl: 'https://collection.com/refresh',\n          clientId: 'collection-client',\n          clientSecret: 'collection-secret',\n          scope: 'read',\n          credentialsPlacement: 'body',\n          credentialsId: 'collection-credentials',\n          tokenPlacement: 'header',\n          tokenHeaderPrefix: 'Bearer',\n          tokenQueryKey: 'access_token',\n          autoFetchToken: true,\n          autoRefreshToken: true,\n          additionalParameters: { authorization: [], token: [], refresh: [] }\n        }\n      };\n      mockRequest.auth = {\n        mode: 'oauth2',\n        oauth2: {\n          grantType: 'client_credentials',\n          accessTokenUrl: 'https://request.com/token',\n          refreshTokenUrl: 'https://request.com/refresh',\n          clientId: 'request-client',\n          clientSecret: 'request-secret',\n          scope: 'read',\n          credentialsPlacement: 'body',\n          credentialsId: 'request-credentials',\n          tokenPlacement: 'header',\n          tokenHeaderPrefix: 'Bearer',\n          tokenQueryKey: 'access_token',\n          autoFetchToken: true,\n          autoRefreshToken: true,\n          additionalParameters: { authorization: [], token: [], refresh: [] }\n        }\n      };\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.oauth2).toEqual({\n        grantType: 'client_credentials',\n        accessTokenUrl: 'https://request.com/token',\n        refreshTokenUrl: 'https://request.com/refresh',\n        clientId: 'request-client',\n        clientSecret: 'request-secret',\n        scope: 'read',\n        credentialsPlacement: 'body',\n        credentialsId: 'request-credentials',\n        tokenPlacement: 'header',\n        tokenHeaderPrefix: 'Bearer',\n        tokenQueryKey: 'access_token',\n        autoFetchToken: true,\n        autoRefreshToken: true,\n        additionalParameters: { authorization: [], token: [], refresh: [] }\n      });\n    });\n  });\n\n  describe('Edge cases and error handling', () => {\n    test('should handle missing collection auth gracefully', () => {\n      mockCollectionRoot.request.auth = null;\n      mockRequest.auth = {\n        mode: 'basic',\n        basic: {\n          username: 'testuser',\n          password: 'testpass'\n        }\n      };\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result.basicAuth).toEqual({\n        username: 'testuser',\n        password: 'testpass'\n      });\n    });\n\n    test('should handle missing request auth gracefully', () => {\n      mockRequest.auth = null;\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result).toBe(mockAxiosRequest);\n      expect(result.headers).toEqual({});\n    });\n\n    test('should handle missing auth mode gracefully', () => {\n      mockRequest.auth = {};\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result).toBe(mockAxiosRequest);\n      expect(result.headers).toEqual({});\n    });\n\n    test('should handle unknown auth mode gracefully', () => {\n      mockRequest.auth = {\n        mode: 'unknown'\n      };\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result).toBe(mockAxiosRequest);\n      expect(result.headers).toEqual({});\n    });\n\n    test('should handle missing OAuth2 grant type gracefully', () => {\n      mockRequest.auth = {\n        mode: 'oauth2',\n        oauth2: {}\n      };\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result).toBe(mockAxiosRequest);\n      expect(result.oauth2).toBeUndefined();\n    });\n\n    test('should handle unknown OAuth2 grant type gracefully', () => {\n      mockRequest.auth = {\n        mode: 'oauth2',\n        oauth2: {\n          grantType: 'unknown_grant'\n        }\n      };\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result).toBe(mockAxiosRequest);\n      expect(result.oauth2).toBeUndefined();\n    });\n\n    test('should return the modified axiosRequest object', () => {\n      mockRequest.auth = {\n        mode: 'bearer',\n        bearer: {\n          token: 'test-token'\n        }\n      };\n\n      const result = setAuthHeaders(mockAxiosRequest, mockRequest, mockCollectionRoot);\n\n      expect(result).toBe(mockAxiosRequest);\n      expect(result.headers['Authorization']).toBe('Bearer test-token');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-electron/tests/store/default-location-migration.spec.js",
    "content": "let mockStoreData = {};\n\njest.mock('electron-store', () => {\n  return jest.fn().mockImplementation((opts = {}) => {\n    return {\n      get: (key, fallback) => (key in mockStoreData ? mockStoreData[key] : fallback),\n      set: (key, value) => {\n        mockStoreData[key] = value;\n      }\n    };\n  });\n});\n\nconst { getPreferences } = require('../../src/store/preferences');\n\ndescribe('Default Location Migration', () => {\n  beforeEach(() => {\n    // Reset mock store data before each test\n    mockStoreData = {};\n  });\n\n  it('should migrate defaultCollectionLocation to defaultLocation', () => {\n    mockStoreData['preferences'] = {\n      general: {\n        defaultCollectionLocation: '/home/user/collections'\n      }\n    };\n\n    const preferences = getPreferences();\n\n    expect(preferences.general.defaultLocation).toBe('/home/user/collections');\n    expect(mockStoreData['preferences'].general.defaultCollectionLocation).toBeUndefined();\n    expect(mockStoreData['preferences'].general.defaultLocation).toBe('/home/user/collections');\n  });\n\n  it('should not migrate if defaultLocation already exists', () => {\n    mockStoreData['preferences'] = {\n      general: {\n        defaultCollectionLocation: '/old/path',\n        defaultLocation: '/new/path'\n      }\n    };\n\n    const preferences = getPreferences();\n\n    expect(preferences.general.defaultLocation).toBe('/new/path');\n    // Old key is left untouched\n    expect(mockStoreData['preferences'].general.defaultCollectionLocation).toBe('/old/path');\n  });\n\n  it('should return default empty string when neither key exists', () => {\n    mockStoreData['preferences'] = {};\n\n    const preferences = getPreferences();\n\n    expect(preferences.general.defaultLocation).toBe('');\n    // No migration occurred — store unchanged\n    expect(mockStoreData['preferences'].general).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/bruno-electron/tests/store/global-environments.test.js",
    "content": "const { globalEnvironmentsStore } = require('../../src/store/global-environments');\n\n// Previously, a bug caused environment variables to be saved without a type.\n// Since that issue is now fixed, this code ensures that anyone who imported\n// data before the fix will have the missing types added retroactively.\ndescribe('global environment variable type backward compatibility', () => {\n  beforeEach(() => {\n    globalEnvironmentsStore.store.clear();\n  });\n\n  it('should add type field for existing global environments without type', () => {\n    // Mock global environments without type field\n    const mockGlobalEnvironments = [\n      {\n        uid: 'yDlwWe3qgimPG20G7AbF7',\n        name: 'Test Environment',\n        variables: [\n          {\n            uid: 'b6BIHGaCrm4m97YA2dIdx',\n            name: 'regular_var',\n            value: 'regular_value',\n            enabled: true,\n            secret: false\n            // Missing: type field\n          },\n          {\n            uid: 'yQTqanPoMdRjKnHyIOZNc',\n            name: 'secret_var',\n            value: 'secret_value',\n            enabled: true,\n            secret: true\n            // Missing: type field\n          }\n        ]\n      }\n    ];\n\n    globalEnvironmentsStore.store.set('environments', mockGlobalEnvironments);\n\n    const processedEnvironments = globalEnvironmentsStore.getGlobalEnvironments();\n\n    expect(processedEnvironments).toHaveLength(1);\n    expect(processedEnvironments[0].variables).toHaveLength(2);\n\n    const regularVar = processedEnvironments[0].variables.find((v) => v.name === 'regular_var');\n    const secretVar = processedEnvironments[0].variables.find((v) => v.name === 'secret_var');\n\n    expect(regularVar.name).toBe('regular_var');\n    expect(regularVar.type).toBe('text');\n\n    expect(secretVar.name).toBe('secret_var');\n    expect(secretVar.type).toBe('text');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-electron/tests/store/proxy-preferences.spec.js",
    "content": "let mockStoreData = {};\n\njest.mock('electron-store', () => {\n  return jest.fn().mockImplementation((opts = {}) => {\n    return {\n      get: (key, fallback) => (key in mockStoreData ? mockStoreData[key] : fallback),\n      set: (key, value) => {\n        mockStoreData[key] = value;\n      }\n    };\n  });\n});\n\nconst { getPreferences, savePreferences } = require('../../src/store/preferences');\n\ndescribe('Proxy Preferences Migration', () => {\n  beforeEach(() => {\n    // Reset mock store data before each test\n    mockStoreData = {};\n  });\n\n  describe('Default Proxy Settings', () => {\n    it('should default to inherit: true for new users (empty preferences)', () => {\n      // New user - no preferences.json exists, store returns empty object\n      mockStoreData['preferences'] = {};\n\n      const preferences = getPreferences();\n\n      // New users get the default proxy settings with inherit: true\n      expect(preferences.proxy.inherit).toBe(true);\n      expect(preferences.proxy.disabled).toBeUndefined();\n      expect(preferences.proxy.config).toBeDefined();\n      expect(preferences.proxy.config.protocol).toBe('http');\n      expect(preferences.proxy.config.hostname).toBe('');\n      expect(preferences.proxy.config.port).toBeNull();\n    });\n\n    it('should default to disabled: true, inherit: false for existing users without proxy settings', () => {\n      // Existing user - has preferences but no proxy property\n      mockStoreData['preferences'] = {\n        request: {\n          sslVerification: true\n        },\n        font: {\n          codeFont: 'default',\n          codeFontSize: 13\n        }\n      };\n\n      const preferences = getPreferences();\n\n      // Existing users without proxy get disabled proxy by default\n      expect(preferences.proxy.disabled).toBe(true);\n      expect(preferences.proxy.inherit).toBe(false);\n      expect(preferences.proxy.config).toBeDefined();\n      expect(preferences.proxy.config.protocol).toBe('http');\n      expect(preferences.proxy.config.hostname).toBe('');\n      expect(preferences.proxy.config.port).toBeNull();\n      expect(preferences.proxy.config.auth.username).toBe('');\n      expect(preferences.proxy.config.auth.password).toBe('');\n      expect(preferences.proxy.config.bypassProxy).toBe('');\n    });\n  });\n\n  describe('New Format (no migration needed)', () => {\n    it('should handle new format with inherit: false', () => {\n      const newFormatProxy = {\n        proxy: {\n          inherit: false,\n          config: {\n            protocol: 'http',\n            hostname: 'proxy.example.com',\n            port: 8080,\n            auth: {\n              username: 'user',\n              password: 'pass'\n            },\n            bypassProxy: 'localhost'\n          }\n        }\n      };\n\n      mockStoreData['preferences'] = newFormatProxy;\n\n      const preferences = getPreferences();\n\n      // Verify key fields are preserved from stored preferences\n      expect(preferences.proxy.inherit).toBe(false);\n      expect(preferences.proxy.config.protocol).toBe('http');\n      expect(preferences.proxy.config.hostname).toBe('proxy.example.com');\n      expect(preferences.proxy.config.port).toBe(8080);\n      expect(preferences.proxy.config.auth.username).toBe('user');\n      expect(preferences.proxy.config.auth.password).toBe('pass');\n      expect(preferences.proxy.config.bypassProxy).toBe('localhost');\n    });\n\n    it('should handle new format with inherit: true', () => {\n      const newFormatProxy = {\n        proxy: {\n          inherit: true,\n          config: {\n            protocol: 'http',\n            hostname: '',\n            port: null,\n            auth: {\n              username: '',\n              password: ''\n            },\n            bypassProxy: ''\n          }\n        }\n      };\n\n      mockStoreData['preferences'] = newFormatProxy;\n\n      const preferences = getPreferences();\n\n      expect(preferences.proxy.inherit).toBe(true);\n      expect(preferences.proxy.config).toBeDefined();\n    });\n\n    it('should handle new format with disabled: true', () => {\n      const newFormatProxy = {\n        proxy: {\n          disabled: true,\n          inherit: false,\n          config: {\n            protocol: 'http',\n            hostname: '',\n            port: null,\n            auth: {\n              username: '',\n              password: ''\n            },\n            bypassProxy: ''\n          }\n        }\n      };\n\n      mockStoreData['preferences'] = newFormatProxy;\n\n      const preferences = getPreferences();\n\n      // disabled: true is preserved from stored preferences\n      expect(preferences.proxy.disabled).toBe(true);\n      expect(preferences.proxy.inherit).toBe(false);\n      expect(preferences.proxy.config).toBeDefined();\n    });\n\n    it('should handle new format with auth.disabled: true', () => {\n      const newFormatProxy = {\n        proxy: {\n          inherit: false,\n          config: {\n            protocol: 'http',\n            hostname: 'proxy.example.com',\n            port: 8080,\n            auth: {\n              disabled: true,\n              username: 'user',\n              password: 'pass'\n            },\n            bypassProxy: ''\n          }\n        }\n      };\n\n      mockStoreData['preferences'] = newFormatProxy;\n\n      const preferences = getPreferences();\n\n      // auth.disabled: true is preserved from stored preferences\n      expect(preferences.proxy.config.auth.disabled).toBe(true);\n      expect(preferences.proxy.config.auth.username).toBe('user');\n      expect(preferences.proxy.config.auth.password).toBe('pass');\n    });\n  });\n\n  describe('Old Format 1: enabled (boolean)', () => {\n    it('should migrate enabled: true to disabled: false, inherit: false', () => {\n      const oldFormatProxy = {\n        proxy: {\n          enabled: true,\n          protocol: 'http',\n          hostname: 'proxy.example.com',\n          port: 8080,\n          auth: {\n            enabled: true,\n            username: 'user',\n            password: 'pass'\n          },\n          bypassProxy: 'localhost'\n        }\n      };\n\n      mockStoreData['preferences'] = oldFormatProxy;\n\n      const preferences = getPreferences();\n\n      // After migration, inherit should be false (old enabled: true maps to inherit: false)\n      expect(preferences.proxy.inherit).toBe(false);\n      // Values are preserved from stored preferences\n      expect(preferences.proxy.config.protocol).toBe('http');\n      expect(preferences.proxy.config.hostname).toBe('proxy.example.com');\n      expect(preferences.proxy.config.port).toBe(8080);\n      expect(preferences.proxy.config.auth.username).toBe('user');\n      expect(preferences.proxy.config.auth.password).toBe('pass');\n      expect(preferences.proxy.config.bypassProxy).toBe('localhost');\n      expect(preferences.proxy.disabled).toBeUndefined(); // disabled: false is omitted\n    });\n\n    it('should migrate enabled: false to disabled: true, inherit: false', () => {\n      const oldFormatProxy = {\n        proxy: {\n          enabled: false,\n          protocol: 'http',\n          hostname: 'proxy.example.com',\n          port: 8080,\n          auth: {\n            enabled: false,\n            username: '',\n            password: ''\n          },\n          bypassProxy: ''\n        }\n      };\n\n      mockStoreData['preferences'] = oldFormatProxy;\n\n      const preferences = getPreferences();\n\n      // After migration, enabled: false becomes disabled: true, inherit: false\n      expect(preferences.proxy.disabled).toBe(true);\n      expect(preferences.proxy.inherit).toBe(false);\n    });\n\n    it('should migrate auth.enabled: false to auth.disabled: true', () => {\n      const oldFormatProxy = {\n        proxy: {\n          enabled: true,\n          protocol: 'http',\n          hostname: 'proxy.example.com',\n          port: 8080,\n          auth: {\n            enabled: false,\n            username: 'user',\n            password: 'pass'\n          },\n          bypassProxy: ''\n        }\n      };\n\n      mockStoreData['preferences'] = oldFormatProxy;\n\n      const preferences = getPreferences();\n\n      // auth.disabled: true is preserved from stored preferences\n      expect(preferences.proxy.config.auth.disabled).toBe(true);\n    });\n  });\n\n  describe('Old Format 2: mode (string)', () => {\n    it('should migrate mode: \"off\" to disabled: true, inherit: false', () => {\n      const oldFormatProxy = {\n        proxy: {\n          mode: 'off',\n          protocol: 'http',\n          hostname: '',\n          port: null,\n          auth: {\n            enabled: false,\n            username: '',\n            password: ''\n          },\n          bypassProxy: ''\n        }\n      };\n\n      mockStoreData['preferences'] = oldFormatProxy;\n\n      const preferences = getPreferences();\n\n      // disabled: true is preserved from migration\n      expect(preferences.proxy.disabled).toBe(true);\n      expect(preferences.proxy.inherit).toBe(false);\n    });\n\n    it('should migrate mode: \"on\" to disabled: false, inherit: false', () => {\n      const oldFormatProxy = {\n        proxy: {\n          mode: 'on',\n          protocol: 'https',\n          hostname: 'proxy.example.com',\n          port: 8443,\n          auth: {\n            enabled: true,\n            username: 'user',\n            password: 'pass'\n          },\n          bypassProxy: '*.local'\n        }\n      };\n\n      mockStoreData['preferences'] = oldFormatProxy;\n\n      const preferences = getPreferences();\n\n      expect(preferences.proxy.disabled).toBeUndefined(); // disabled: false is omitted\n      expect(preferences.proxy.inherit).toBe(false);\n      // Values are preserved from stored preferences\n      expect(preferences.proxy.config.protocol).toBe('https');\n      expect(preferences.proxy.config.hostname).toBe('proxy.example.com');\n      expect(preferences.proxy.config.port).toBe(8443);\n    });\n\n    it('should migrate mode: \"system\" to disabled: false, inherit: true', () => {\n      const oldFormatProxy = {\n        proxy: {\n          mode: 'system',\n          protocol: 'http',\n          hostname: '',\n          port: null,\n          auth: {\n            enabled: false,\n            username: '',\n            password: ''\n          },\n          bypassProxy: ''\n        }\n      };\n\n      mockStoreData['preferences'] = oldFormatProxy;\n\n      const preferences = getPreferences();\n\n      expect(preferences.proxy.disabled).toBeUndefined(); // disabled: false is omitted\n      expect(preferences.proxy.inherit).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-electron/tests/utils/collection.spec.js",
    "content": "const { parseBruFileMeta } = require('../../src/utils/collection');\n\ndescribe('parseBruFileMeta', () => {\n  test('parses valid meta block correctly', () => {\n    const data = `meta {\n      name: 0.2_mb\n      type: http\n      seq: 1\n    }`;\n\n    const result = parseBruFileMeta(data);\n\n    expect(result).toEqual({\n      type: 'http-request',\n      name: '0.2_mb',\n      seq: 1,\n      settings: {},\n      tags: [],\n      request: {\n        method: '',\n        url: '',\n        params: [],\n        headers: [],\n        auth: { mode: 'none' },\n        body: { mode: 'none' },\n        script: {},\n        vars: {},\n        assertions: [],\n        tests: '',\n        docs: ''\n      }\n    });\n  });\n\n  test('returns null for missing meta block', () => {\n    const data = `someOtherBlock {\n      key: value\n    }`;\n\n    const result = parseBruFileMeta(data);\n\n    expect(result).toBeNull();\n  });\n\n  test('handles empty meta block gracefully', () => {\n    const data = `meta {}`;\n\n    const result = parseBruFileMeta(data);\n\n    expect(result).toEqual({\n      type: 'http-request',\n      name: undefined,\n      seq: 1,\n      settings: {},\n      tags: [],\n      request: {\n        method: '',\n        url: '',\n        params: [],\n        headers: [],\n        auth: { mode: 'none' },\n        body: { mode: 'none' },\n        script: {},\n        vars: {},\n        assertions: [],\n        tests: '',\n        docs: ''\n      }\n    });\n  });\n\n  test('ignores invalid lines in meta block', () => {\n    const data = `meta {\n      name: 0.2_mb\n      invalidLine\n      seq: 1\n    }`;\n\n    const result = parseBruFileMeta(data);\n\n    expect(result).toEqual({\n      type: 'http-request',\n      name: '0.2_mb',\n      seq: 1,\n      settings: {},\n      tags: [],\n      request: {\n        method: '',\n        url: '',\n        params: [],\n        headers: [],\n        auth: { mode: 'none' },\n        body: { mode: 'none' },\n        script: {},\n        vars: {},\n        assertions: [],\n        tests: '',\n        docs: ''\n      }\n    });\n  });\n\n  test('handles unexpected input gracefully', () => {\n    const data = null;\n\n    const result = parseBruFileMeta(data);\n\n    expect(result).toBeNull();\n  });\n\n  test('handles missing colon gracefully', () => {\n    const data = `meta {\n      name 0.2_mb\n      seq: 1\n    }`;\n\n    const result = parseBruFileMeta(data);\n\n    expect(result).toEqual({\n      type: 'http-request',\n      name: undefined,\n      seq: 1,\n      settings: {},\n      tags: [],\n      request: {\n        method: '',\n        url: '',\n        params: [],\n        headers: [],\n        auth: { mode: 'none' },\n        body: { mode: 'none' },\n        script: {},\n        vars: {},\n        assertions: [],\n        tests: '',\n        docs: ''\n      }\n    });\n  });\n\n  test('parses numeric values correctly', () => {\n    const data = `meta {\n      numValue: 1234\n      floatValue: 12.34\n      strValue: some_text\n      seq: 5\n    }`;\n\n    const result = parseBruFileMeta(data);\n\n    expect(result).toEqual({\n      type: 'http-request',\n      name: undefined,\n      seq: 5,\n      settings: {},\n      tags: [],\n      request: {\n        method: '',\n        url: '',\n        params: [],\n        headers: [],\n        auth: { mode: 'none' },\n        body: { mode: 'none' },\n        script: {},\n        vars: {},\n        assertions: [],\n        tests: '',\n        docs: ''\n      }\n    });\n  });\n\n  test('handles syntax error in meta block 1', () => {\n    const data = `meta \n      name: 0.2_mb\n      type: http\n      seq: 1\n    }`;\n\n    const result = parseBruFileMeta(data);\n\n    expect(result).toBeNull();\n  });\n\n  test('handles syntax error in meta block 2', () => {\n    const data = `meta {\n      name: 0.2_mb\n      type: http\n      seq: 1\n    `;\n\n    const result = parseBruFileMeta(data);\n\n    expect(result).toBeNull();\n  });\n\n  test('handles graphql type correctly', () => {\n    const data = `meta {\n      name: graphql_query\n      type: graphql\n      seq: 2\n    }`;\n\n    const result = parseBruFileMeta(data);\n\n    expect(result).toEqual({\n      type: 'graphql-request',\n      name: 'graphql_query',\n      seq: 2,\n      settings: {},\n      tags: [],\n      request: {\n        method: '',\n        url: '',\n        params: [],\n        headers: [],\n        auth: { mode: 'none' },\n        body: { mode: 'none' },\n        script: {},\n        vars: {},\n        assertions: [],\n        tests: '',\n        docs: ''\n      }\n    });\n  });\n\n  test('handles unknown type correctly', () => {\n    const data = `meta {\n      name: unknown_request\n      type: unknown\n      seq: 3\n    }`;\n\n    const result = parseBruFileMeta(data);\n\n    expect(result).toEqual({\n      type: 'http-request',\n      name: 'unknown_request',\n      seq: 3,\n      settings: {},\n      tags: [],\n      request: {\n        method: '',\n        url: '',\n        params: [],\n        headers: [],\n        auth: { mode: 'none' },\n        body: { mode: 'none' },\n        script: {},\n        vars: {},\n        assertions: [],\n        tests: '',\n        docs: ''\n      }\n    });\n  });\n\n  test('handles missing seq gracefully', () => {\n    const data = `meta {\n      name: no_seq_request\n      type: http\n    }`;\n\n    const result = parseBruFileMeta(data);\n\n    expect(result).toEqual({\n      type: 'http-request',\n      name: 'no_seq_request',\n      seq: 1, // Default fallback\n      settings: {},\n      tags: [],\n      request: {\n        method: '',\n        url: '',\n        params: [],\n        headers: [],\n        auth: { mode: 'none' },\n        body: { mode: 'none' },\n        script: {},\n        vars: {},\n        assertions: [],\n        tests: '',\n        docs: ''\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-electron/tests/utils/common.spec.js",
    "content": "const { flattenDataForDotNotation, parseDataFromRequest } = require('../../src/utils/common');\nconst FormData = require('form-data');\n\ndescribe('utils: flattenDataForDotNotation', () => {\n  test('Flatten a simple object with dot notation', () => {\n    const input = {\n      person: {\n        name: 'John',\n        age: 30\n      }\n    };\n\n    const expectedOutput = {\n      'person.name': 'John',\n      'person.age': 30\n    };\n\n    expect(flattenDataForDotNotation(input)).toEqual(expectedOutput);\n  });\n\n  test('Flatten an object with nested arrays', () => {\n    const input = {\n      users: [\n        { name: 'Alice', age: 25 },\n        { name: 'Bob', age: 28 }\n      ]\n    };\n\n    const expectedOutput = {\n      'users[0].name': 'Alice',\n      'users[0].age': 25,\n      'users[1].name': 'Bob',\n      'users[1].age': 28\n    };\n\n    expect(flattenDataForDotNotation(input)).toEqual(expectedOutput);\n  });\n\n  test('Flatten an empty object', () => {\n    const input = {};\n\n    const expectedOutput = {};\n\n    expect(flattenDataForDotNotation(input)).toEqual(expectedOutput);\n  });\n\n  test('Flatten an object with nested objects', () => {\n    const input = {\n      person: {\n        name: 'Alice',\n        address: {\n          city: 'New York',\n          zipcode: '10001'\n        }\n      }\n    };\n\n    const expectedOutput = {\n      'person.name': 'Alice',\n      'person.address.city': 'New York',\n      'person.address.zipcode': '10001'\n    };\n\n    expect(flattenDataForDotNotation(input)).toEqual(expectedOutput);\n  });\n\n  test('Flatten an object with arrays of objects', () => {\n    const input = {\n      teams: [\n        { name: 'Team A', members: ['Alice', 'Bob'] },\n        { name: 'Team B', members: ['Charlie', 'David'] }\n      ]\n    };\n\n    const expectedOutput = {\n      'teams[0].name': 'Team A',\n      'teams[0].members[0]': 'Alice',\n      'teams[0].members[1]': 'Bob',\n      'teams[1].name': 'Team B',\n      'teams[1].members[0]': 'Charlie',\n      'teams[1].members[1]': 'David'\n    };\n\n    expect(flattenDataForDotNotation(input)).toEqual(expectedOutput);\n  });\n});\n\ndescribe('utils: parseDataFromRequest', () => {\n  test('should format multipart FormData', () => {\n    const formData = new FormData();\n    formData._boundary = 'boundary123';\n    const request = {\n      data: formData,\n      _originalMultipartData: [\n        { name: 'description', type: 'text', value: 'dfv' },\n        { name: 'file', type: 'file', value: ['Dumy.xml'] }\n      ],\n      headers: {}\n    };\n\n    const result = parseDataFromRequest(request);\n    expect(result.data).toContain('name: description');\n    expect(result.data).toContain('value: dfv');\n    expect(result.data).toContain('value: [File: Dumy.xml]');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-electron/tests/utils/encryption.spec.js",
    "content": "const { encryptString, decryptString, encryptStringSafe, decryptStringSafe } = require('../../src/utils/encryption');\n\n// We can only unit test aes 256 fallback as safeStorage is only available\n// in the main process\n\ndescribe('Encryption and Decryption Tests', () => {\n  it('should encrypt and decrypt using AES-256', () => {\n    const plaintext = 'bruno is awesome';\n    const encrypted = encryptString(plaintext);\n    const decrypted = decryptString(encrypted);\n\n    expect(decrypted).toBe(plaintext);\n  });\n\n  it('should handle empty strings in encryptString', () => {\n    const result = encryptString('');\n    expect(result).toBe('');\n  });\n\n  it('should handle empty strings in decryptString', () => {\n    const result = decryptString('');\n    expect(result).toBe('');\n  });\n\n  it('encrypt should throw an error for invalid string', () => {\n    expect(() => encryptString(null)).toThrow('Encrypt failed: invalid string');\n    expect(() => encryptString(undefined)).toThrow('Encrypt failed: invalid string');\n  });\n\n  it('decrypt should throw an error for invalid string', () => {\n    expect(() => decryptString(null)).toThrow('Decrypt failed: unrecognized string format');\n    expect(() => decryptString('garbage')).toThrow('Decrypt failed: unrecognized string format');\n  });\n\n  it.skip('string encrypted using createCipher (< node 20) should be decrypted properly', () => {\n    const encryptedString = '$01:2738e0e6a38bcde5fd80141ceadc9b67bc7b1fca7e398c552c1ca2bace28eb57';\n    const decryptedValue = decryptString(encryptedString);\n\n    expect(decryptedValue).toBe('bruno is awesome');\n  });\n\n  it('decrypt should throw an error for invalid algorithm', () => {\n    const invalidAlgo = '$99:abcdefg';\n\n    expect(() => decryptString(invalidAlgo)).toThrow('Decrypt failed: Invalid algo');\n  });\n});\n\ndescribe('Safe Encryption and Decryption Tests', () => {\n  it('should encrypt and decrypt successfully using encryptStringSafe and decryptStringSafe', () => {\n    const plaintext = 'bruno is awesome';\n    const encryptionResult = encryptStringSafe(plaintext);\n    const decryptionResult = decryptStringSafe(encryptionResult.value);\n\n    expect(encryptionResult.success).toBe(true);\n    expect(decryptionResult.success).toBe(true);\n    expect(decryptionResult.value).toBe(plaintext);\n  });\n\n  it('should handle empty strings in encryptStringSafe', () => {\n    const result = encryptStringSafe('');\n    expect(result.success).toBe(true);\n    expect(result.value).toBe('');\n  });\n\n  it('should handle empty strings in decryptStringSafe', () => {\n    const result = decryptStringSafe('');\n    expect(result.success).toBe(true);\n    expect(result.value).toBe('');\n  });\n\n  it('should handle invalid string format in decryptStringSafe', () => {\n    const result = decryptStringSafe('garbage');\n    expect(result.success).toBe(false);\n    expect(result.error).toBe('Decrypt failed: unrecognized string format');\n    expect(result.value).toBe('');\n  });\n\n  it('should handle invalid algorithm in decryptStringSafe', () => {\n    const invalidAlgo = '$99:abcdefg';\n    const result = decryptStringSafe(invalidAlgo);\n    expect(result.success).toBe(false);\n    expect(result.error).toBe('Decrypt failed: Invalid algo');\n    expect(result.value).toBe('');\n  });\n\n  it('should handle malformed encrypted string in decryptStringSafe', () => {\n    const malformedString = '$01:invalid-hex-string';\n    const result = decryptStringSafe(malformedString);\n    expect(result.success).toBe(false);\n    expect(result.error).toContain('AES256 decryption failed');\n    expect(result.value).toBe('');\n  });\n\n  it('should handle special characters in encryptStringSafe and decryptStringSafe', () => {\n    const specialText = 'bruno@#$%^&*()_+-=[]{}|;:,.<>?';\n    const encryptionResult = encryptStringSafe(specialText);\n    const decryptionResult = decryptStringSafe(encryptionResult.value);\n\n    expect(encryptionResult.success).toBe(true);\n    expect(decryptionResult.success).toBe(true);\n    expect(decryptionResult.value).toBe(specialText);\n  });\n\n  it('decrypt-safe should not throw error for invalid inputs', () => {\n    expect(() => decryptStringSafe(null)).not.toThrow();\n    expect(() => decryptStringSafe(undefined)).not.toThrow();\n    expect(() => decryptStringSafe('garbage')).not.toThrow();\n    expect(() => decryptStringSafe(123456789)).not.toThrow();\n    expect(() => decryptStringSafe('aes256:')).not.toThrow();\n    expect(() => decryptStringSafe('aes256:invalid_base64')).not.toThrow();\n  });\n});\n"
  },
  {
    "path": "packages/bruno-electron/tests/utils/form-data.spec.js",
    "content": "const { formatMultipartData } = require('../../src/utils/form-data');\n\ndescribe('utils: formatMultipartData', () => {\n  test('should format text field', () => {\n    const data = [{ name: 'description', type: 'text', value: 'dfv' }];\n    const result = formatMultipartData(data, 'boundary');\n\n    expect(result).toContain('----boundary');\n    expect(result).toContain('Content-Disposition: form-data');\n    expect(result).toContain('name: description');\n    expect(result).toContain('value: dfv');\n    expect(result).toContain('----boundary--');\n  });\n\n  test('should format file field', () => {\n    const data = [{ name: 'file', type: 'file', value: ['Dumy.xml'] }];\n    const result = formatMultipartData(data, 'boundary');\n\n    expect(result).toContain('name: file');\n    expect(result).toContain('value: [File: Dumy.xml]');\n  });\n\n  test('should format multiple fields', () => {\n    const data = [\n      { name: 'description', type: 'text', value: 'dfv' },\n      { name: 'file', type: 'file', value: ['Dumy.xml'] }\n    ];\n    const result = formatMultipartData(data, 'boundary');\n\n    expect(result).toContain('name: description');\n    expect(result).toContain('value: dfv');\n    expect(result).toContain('name: file');\n    expect(result).toContain('value: [File: Dumy.xml]');\n  });\n\n  test('should return empty string for invalid input', () => {\n    expect(formatMultipartData([], 'boundary')).toBe('');\n    expect(formatMultipartData(null, 'boundary')).toBe('');\n  });\n\n  test('should normalize boundary', () => {\n    const data = [{ name: 'field', type: 'text', value: 'value' }];\n    expect(formatMultipartData(data, '--boundary')).toContain('----boundary');\n    expect(formatMultipartData(data, 'boundary--')).toContain('----boundary');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-electron/tests/utils/proxy-util.spec.js",
    "content": "const { shouldUseProxy } = require('../../src/utils/proxy-util');\n\ntest('no proxy necessary - star', () => {\n  const url = 'http://wwww.example.org/test';\n  const noProxy = '*';\n\n  expect(shouldUseProxy(url, noProxy)).toEqual(false);\n});\n\ntest('no proxy necessary - no noProxy bypass', () => {\n  const url = 'http://wwww.example.org/test';\n  const noProxy = '';\n\n  expect(shouldUseProxy(url, noProxy)).toEqual(true);\n});\n\ntest('no proxy necessary - wildcard match', () => {\n  const url = 'http://wwww.example.org/test';\n  const noProxy = '*example.org';\n\n  expect(shouldUseProxy(url, noProxy)).toEqual(false);\n});\n\ntest('no proxy necessary - direct proxy', () => {\n  const url = 'http://wwww.example.org/test';\n  const noProxy = 'wwww.example.org';\n\n  expect(shouldUseProxy(url, noProxy)).toEqual(false);\n});\n\ntest('no proxy necessary - multiple proxy', () => {\n  const url = 'http://wwww.example.org/test';\n  const noProxy = 'www.example.com,wwww.example.org';\n\n  expect(shouldUseProxy(url, noProxy)).toEqual(false);\n});\n\ntest('proxy necessary - no proxy match multiple', () => {\n  const url = 'https://wwww.example.test/test';\n  const noProxy = 'www.example.com,wwww.example.org';\n\n  expect(shouldUseProxy(url, noProxy)).toEqual(true);\n});\n\ntest('proxy necessary - no proxy match', () => {\n  const url = 'https://wwww.example.test/test';\n  const noProxy = 'www.example.com';\n\n  expect(shouldUseProxy(url, noProxy)).toEqual(true);\n});\n"
  },
  {
    "path": "packages/bruno-electron/tests/utils/transform-bruno-config.spec.js",
    "content": "const { transformBrunoConfigBeforeSave, transformBrunoConfigAfterRead } = require('../../src/utils/transformBrunoConfig');\n\ndescribe('BrunoConfig Proxy Transform', () => {\n  describe('transformBrunoConfigAfterRead - Migration from old to new format', () => {\n    describe('Old Format: enabled (true | false | \"global\")', () => {\n      test('should migrate enabled: true to disabled: false, inherit: false', async () => {\n        const oldConfig = {\n          name: 'Test Collection',\n          proxy: {\n            enabled: true,\n            protocol: 'http',\n            hostname: 'proxy.example.com',\n            port: 8080,\n            auth: {\n              enabled: true,\n              username: 'user',\n              password: 'pass'\n            },\n            bypassProxy: 'localhost'\n          }\n        };\n\n        const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path');\n\n        expect(result.proxy).toEqual({\n          inherit: false,\n          config: {\n            protocol: 'http',\n            hostname: 'proxy.example.com',\n            port: 8080,\n            auth: {\n              username: 'user',\n              password: 'pass'\n            },\n            bypassProxy: 'localhost'\n          }\n        });\n        expect(result.proxy.disabled).toBeUndefined(); // disabled: false is omitted\n      });\n\n      test('should migrate enabled: false to disabled: true, inherit: false', async () => {\n        const oldConfig = {\n          name: 'Test Collection',\n          proxy: {\n            enabled: false,\n            protocol: 'http',\n            hostname: 'proxy.example.com',\n            port: 8080,\n            auth: {\n              enabled: false,\n              username: '',\n              password: ''\n            },\n            bypassProxy: ''\n          }\n        };\n\n        const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path');\n\n        expect(result.proxy.disabled).toBe(true);\n        expect(result.proxy.inherit).toBe(false);\n      });\n\n      test('should migrate enabled: \"global\" to disabled: false, inherit: true', async () => {\n        const oldConfig = {\n          name: 'Test Collection',\n          proxy: {\n            enabled: 'global',\n            protocol: 'http',\n            hostname: '',\n            port: null,\n            auth: {\n              enabled: false,\n              username: '',\n              password: ''\n            },\n            bypassProxy: ''\n          }\n        };\n\n        const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path');\n\n        expect(result.proxy.disabled).toBeUndefined(); // disabled: false is omitted\n        expect(result.proxy.inherit).toBe(true);\n      });\n\n      test('should migrate auth.enabled: false to auth.disabled: true', async () => {\n        const oldConfig = {\n          name: 'Test Collection',\n          proxy: {\n            enabled: true,\n            protocol: 'http',\n            hostname: 'proxy.example.com',\n            port: 8080,\n            auth: {\n              enabled: false,\n              username: 'user',\n              password: 'pass'\n            },\n            bypassProxy: ''\n          }\n        };\n\n        const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path');\n\n        expect(result.proxy.config.auth.disabled).toBe(true);\n        expect(result.proxy.config.auth.username).toBe('user');\n        expect(result.proxy.config.auth.password).toBe('pass');\n      });\n\n      test('should omit auth.disabled when auth.enabled: true', async () => {\n        const oldConfig = {\n          name: 'Test Collection',\n          proxy: {\n            enabled: true,\n            protocol: 'http',\n            hostname: 'proxy.example.com',\n            port: 8080,\n            auth: {\n              enabled: true,\n              username: 'user',\n              password: 'pass'\n            },\n            bypassProxy: ''\n          }\n        };\n\n        const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path');\n\n        expect(result.proxy.config.auth.disabled).toBeUndefined();\n        expect(result.proxy.config.auth.username).toBe('user');\n        expect(result.proxy.config.auth.password).toBe('pass');\n      });\n    });\n\n    describe('New Format (no migration)', () => {\n      test('should not modify new format with inherit: false', async () => {\n        const newConfig = {\n          name: 'Test Collection',\n          proxy: {\n            inherit: false,\n            config: {\n              protocol: 'https',\n              hostname: 'proxy.example.com',\n              port: 8443,\n              auth: {\n                username: 'user',\n                password: 'pass'\n              },\n              bypassProxy: '*.local'\n            }\n          }\n        };\n\n        const result = await transformBrunoConfigAfterRead(newConfig, '/test/path');\n\n        expect(result.proxy).toEqual(newConfig.proxy);\n      });\n\n      test('should not modify new format with inherit: true', async () => {\n        const newConfig = {\n          name: 'Test Collection',\n          proxy: {\n            inherit: true,\n            config: {\n              protocol: 'http',\n              hostname: '',\n              port: null,\n              auth: {\n                username: '',\n                password: ''\n              },\n              bypassProxy: ''\n            }\n          }\n        };\n\n        const result = await transformBrunoConfigAfterRead(newConfig, '/test/path');\n\n        expect(result.proxy).toEqual(newConfig.proxy);\n      });\n\n      test('should not modify new format with disabled: true', async () => {\n        const newConfig = {\n          name: 'Test Collection',\n          proxy: {\n            disabled: true,\n            inherit: false,\n            config: {\n              protocol: 'http',\n              hostname: '',\n              port: null,\n              auth: {\n                username: '',\n                password: ''\n              },\n              bypassProxy: ''\n            }\n          }\n        };\n\n        const result = await transformBrunoConfigAfterRead(newConfig, '/test/path');\n\n        expect(result.proxy).toEqual(newConfig.proxy);\n      });\n\n      test('should not modify new format with auth.disabled: true', async () => {\n        const newConfig = {\n          name: 'Test Collection',\n          proxy: {\n            inherit: false,\n            config: {\n              protocol: 'http',\n              hostname: 'proxy.example.com',\n              port: 8080,\n              auth: {\n                disabled: true,\n                username: 'user',\n                password: 'pass'\n              },\n              bypassProxy: ''\n            }\n          }\n        };\n\n        const result = await transformBrunoConfigAfterRead(newConfig, '/test/path');\n\n        expect(result.proxy).toEqual(newConfig.proxy);\n      });\n    });\n\n    describe('Edge Cases', () => {\n      test('should handle missing proxy config', async () => {\n        const config = {\n          name: 'Test Collection'\n        };\n\n        const result = await transformBrunoConfigAfterRead(config, '/test/path');\n\n        expect(result.proxy).toBeUndefined();\n      });\n\n      test('should handle null port values', async () => {\n        const oldConfig = {\n          name: 'Test Collection',\n          proxy: {\n            enabled: true,\n            protocol: 'http',\n            hostname: 'proxy.example.com',\n            port: null,\n            auth: {\n              enabled: false,\n              username: '',\n              password: ''\n            },\n            bypassProxy: ''\n          }\n        };\n\n        const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path');\n\n        expect(result.proxy.config.port).toBeNull();\n      });\n\n      test('should handle SOCKS protocols', async () => {\n        const oldConfig = {\n          name: 'Test Collection',\n          proxy: {\n            enabled: true,\n            protocol: 'socks5',\n            hostname: 'socks.example.com',\n            port: 1080,\n            auth: {\n              enabled: true,\n              username: 'socksuser',\n              password: 'sockspass'\n            },\n            bypassProxy: ''\n          }\n        };\n\n        const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path');\n\n        expect(result.proxy.config.protocol).toBe('socks5');\n        expect(result.proxy.config.hostname).toBe('socks.example.com');\n        expect(result.proxy.config.port).toBe(1080);\n      });\n\n      test('should handle missing auth object', async () => {\n        const oldConfig = {\n          name: 'Test Collection',\n          proxy: {\n            enabled: true,\n            protocol: 'http',\n            hostname: 'proxy.example.com',\n            port: 8080,\n            bypassProxy: ''\n          }\n        };\n\n        const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path');\n\n        expect(result.proxy.config.auth).toEqual({\n          username: '',\n          password: ''\n        });\n      });\n\n      test('should preserve protobuf config during proxy migration', async () => {\n        const oldConfig = {\n          name: 'Test Collection',\n          protobuf: {\n            protoFiles: [{ path: 'test.proto' }],\n            importPaths: [{ path: 'imports/' }]\n          },\n          proxy: {\n            enabled: 'global',\n            protocol: 'http',\n            hostname: '',\n            port: null,\n            auth: {\n              enabled: false,\n              username: '',\n              password: ''\n            },\n            bypassProxy: ''\n          }\n        };\n\n        const result = await transformBrunoConfigAfterRead(oldConfig, '/test/path');\n\n        expect(result.protobuf).toBeDefined();\n        expect(result.protobuf.protoFiles).toHaveLength(1);\n        expect(result.proxy.inherit).toBe(true);\n      });\n    });\n  });\n\n  describe('transformBrunoConfigBeforeSave - Cleanup optional fields', () => {\n    test('should remove disabled: false from proxy config', () => {\n      const config = {\n        name: 'Test Collection',\n        proxy: {\n          disabled: false,\n          inherit: false,\n          config: {\n            protocol: 'http',\n            hostname: 'proxy.example.com',\n            port: 8080,\n            auth: {\n              username: 'user',\n              password: 'pass'\n            },\n            bypassProxy: ''\n          }\n        }\n      };\n\n      const result = transformBrunoConfigBeforeSave(config);\n\n      expect(result.proxy.disabled).toBeUndefined();\n      expect(result.proxy.inherit).toBe(false);\n    });\n\n    test('should keep disabled: true in proxy config', () => {\n      const config = {\n        name: 'Test Collection',\n        proxy: {\n          disabled: true,\n          inherit: false,\n          config: {\n            protocol: 'http',\n            hostname: '',\n            port: null,\n            auth: {\n              username: '',\n              password: ''\n            },\n            bypassProxy: ''\n          }\n        }\n      };\n\n      const result = transformBrunoConfigBeforeSave(config);\n\n      expect(result.proxy.disabled).toBe(true);\n    });\n\n    test('should remove auth.disabled: false from proxy config', () => {\n      const config = {\n        name: 'Test Collection',\n        proxy: {\n          inherit: false,\n          config: {\n            protocol: 'http',\n            hostname: 'proxy.example.com',\n            port: 8080,\n            auth: {\n              disabled: false,\n              username: 'user',\n              password: 'pass'\n            },\n            bypassProxy: ''\n          }\n        }\n      };\n\n      const result = transformBrunoConfigBeforeSave(config);\n\n      expect(result.proxy.config.auth.disabled).toBeUndefined();\n      expect(result.proxy.config.auth.username).toBe('user');\n    });\n\n    test('should keep auth.disabled: true in proxy config', () => {\n      const config = {\n        name: 'Test Collection',\n        proxy: {\n          inherit: false,\n          config: {\n            protocol: 'http',\n            hostname: 'proxy.example.com',\n            port: 8080,\n            auth: {\n              disabled: true,\n              username: 'user',\n              password: 'pass'\n            },\n            bypassProxy: ''\n          }\n        }\n      };\n\n      const result = transformBrunoConfigBeforeSave(config);\n\n      expect(result.proxy.config.auth.disabled).toBe(true);\n    });\n\n    test('should handle missing proxy config', () => {\n      const config = {\n        name: 'Test Collection'\n      };\n\n      const result = transformBrunoConfigBeforeSave(config);\n\n      expect(result.proxy).toBeUndefined();\n    });\n\n    test('should preserve protobuf config cleanup', () => {\n      const config = {\n        name: 'Test Collection',\n        protobuf: {\n          protoFiles: [{ path: 'test.proto', exists: true }],\n          importPaths: [{ path: 'imports/', exists: false }]\n        },\n        proxy: {\n          disabled: false,\n          inherit: false,\n          config: {\n            protocol: 'http',\n            hostname: 'proxy.example.com',\n            port: 8080,\n            auth: {\n              disabled: false,\n              username: 'user',\n              password: 'pass'\n            },\n            bypassProxy: ''\n          }\n        }\n      };\n\n      const result = transformBrunoConfigBeforeSave(config);\n\n      // Protobuf exists fields should be removed\n      expect(result.protobuf.protoFiles[0].exists).toBeUndefined();\n      expect(result.protobuf.importPaths[0].exists).toBeUndefined();\n\n      // Proxy optional fields should be removed\n      expect(result.proxy.disabled).toBeUndefined();\n      expect(result.proxy.config.auth.disabled).toBeUndefined();\n    });\n\n    test('should not modify config without optional fields', () => {\n      const config = {\n        name: 'Test Collection',\n        proxy: {\n          inherit: true,\n          config: {\n            protocol: 'http',\n            hostname: '',\n            port: null,\n            auth: {\n              username: '',\n              password: ''\n            },\n            bypassProxy: ''\n          }\n        }\n      };\n\n      const result = transformBrunoConfigBeforeSave(config);\n\n      expect(result.proxy).toEqual(config.proxy);\n    });\n  });\n\n  describe('Round-trip transformation', () => {\n    test('should handle read -> save -> read cycle for old format', async () => {\n      const oldConfig = {\n        name: 'Test Collection',\n        proxy: {\n          enabled: true,\n          protocol: 'http',\n          hostname: 'proxy.example.com',\n          port: 8080,\n          auth: {\n            enabled: true,\n            username: 'user',\n            password: 'pass'\n          },\n          bypassProxy: 'localhost'\n        }\n      };\n\n      // Read (migrate)\n      const afterRead = await transformBrunoConfigAfterRead(oldConfig, '/test/path');\n\n      // Save (cleanup)\n      const beforeSave = transformBrunoConfigBeforeSave(afterRead);\n\n      // Read again (should not change)\n      const afterSecondRead = await transformBrunoConfigAfterRead(beforeSave, '/test/path');\n\n      expect(afterSecondRead.proxy).toEqual(beforeSave.proxy);\n      expect(afterSecondRead.proxy.inherit).toBe(false);\n      expect(afterSecondRead.proxy.disabled).toBeUndefined();\n      expect(afterSecondRead.proxy.config.auth.disabled).toBeUndefined();\n    });\n\n    test('should handle read -> save -> read cycle for new format', async () => {\n      const newConfig = {\n        name: 'Test Collection',\n        proxy: {\n          inherit: false,\n          config: {\n            protocol: 'https',\n            hostname: 'proxy.example.com',\n            port: 8443,\n            auth: {\n              username: 'user',\n              password: 'pass'\n            },\n            bypassProxy: '*.local'\n          }\n        }\n      };\n\n      // Read (no migration)\n      const afterRead = await transformBrunoConfigAfterRead(newConfig, '/test/path');\n\n      // Save (cleanup)\n      const beforeSave = transformBrunoConfigBeforeSave(afterRead);\n\n      // Read again (should not change)\n      const afterSecondRead = await transformBrunoConfigAfterRead(beforeSave, '/test/path');\n\n      expect(afterSecondRead.proxy).toEqual(newConfig.proxy);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-electron/tests/utils/workspace-config.spec.js",
    "content": "const path = require('path');\nconst fs = require('fs');\nconst os = require('os');\nconst yaml = require('js-yaml');\nconst { reorderWorkspaceCollections } = require('../../src/utils/workspace-config');\n\nconst collection = (name, pathSegment) => ({ name, path: pathSegment });\n\ndescribe('reorderWorkspaceCollections', () => {\n  let workspacePath;\n\n  /** Writes workspace.yml with the given collections (relative paths). */\n  const writeWorkspaceYml = (collections) => {\n    const content = [\n      'opencollection: 1.0.0',\n      'info:',\n      '  name: Test',\n      '  type: workspace',\n      'collections:',\n      ...collections.flatMap((c) => [`  - name: ${c.name}`, `    path: ${c.path}`]),\n      'specs: []',\n      'docs: \\'\\''\n    ].join('\\n');\n    fs.writeFileSync(path.join(workspacePath, 'workspace.yml'), content);\n  };\n\n  /** Returns collection paths (relative) in order as stored in workspace.yml. */\n  const getCollectionPathsFromYml = () => {\n    const raw = fs.readFileSync(path.join(workspacePath, 'workspace.yml'), 'utf8');\n    const config = yaml.load(raw);\n    return (config.collections || []).map((c) => c.path);\n  };\n\n  /** Resolves a relative collection path segment to an absolute path under the current workspace. */\n  const absPath = (relativePath) => path.resolve(workspacePath, relativePath);\n\n  beforeEach(() => {\n    workspacePath = fs.mkdtempSync(path.join(os.tmpdir(), 'bruno-ws-'));\n  });\n\n  afterEach(() => {\n    fs.rmSync(workspacePath, { recursive: true, force: true });\n  });\n\n  test('reorders collections to match given path list', async () => {\n    writeWorkspaceYml([\n      collection('API', 'collections/api'),\n      collection('Backend', 'collections/backend'),\n      collection('Frontend', 'collections/frontend')\n    ]);\n\n    await reorderWorkspaceCollections(workspacePath, [\n      absPath('collections/frontend'),\n      absPath('collections/api'),\n      absPath('collections/backend')\n    ]);\n\n    expect(getCollectionPathsFromYml()).toEqual(['collections/frontend', 'collections/api', 'collections/backend']);\n  });\n\n  test('deduplicates when reorder list contains duplicate paths', async () => {\n    writeWorkspaceYml([\n      collection('API', 'collections/api'),\n      collection('Backend', 'collections/backend')\n    ]);\n\n    await reorderWorkspaceCollections(workspacePath, [\n      absPath('collections/api'),\n      absPath('collections/backend'),\n      absPath('collections/api'),\n      absPath('collections/api')\n    ]);\n\n    expect(getCollectionPathsFromYml()).toEqual(['collections/api', 'collections/backend']);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-filestore/.gitignore",
    "content": "node_modules\n.DS_Store\n*.log\ndist\ncoverage "
  },
  {
    "path": "packages/bruno-filestore/LICENSE.md",
    "content": "\nMIT License\n\nCopyright (c) 2022 Anoop M D, Anusree P S and Contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "packages/bruno-filestore/README.md",
    "content": "# Bruno Filestore\n\nA generic file storage and parsing package for Bruno API client.\n\n## Purpose\n\nThis package abstracts the file format operations for Bruno, providing a clean interface for parsing and stringifying Bruno requests, collections, folders, and environments.\n\n## Features\n\n- Format-agnostic APIs for file operations\n- Currently supports Bruno's custom `.bru` format\n- Designed for future extensibility to support YAML and other formats\n\n## Usage\n\n```javascript\nconst {\n  parseRequest,\n  stringifyRequest,\n  parseCollection,\n  stringifyCollection,\n  parseEnvironment,\n  stringifyEnvironment,\n  parseDotEnv\n} = require('@usebruno/filestore');\n\n// Parse a .bru request file\nconst requestData = parseRequest(bruContent);\n\n// Stringify request data to .bru format\nconst bruContent = stringifyRequest(requestData);\n\n// Example with future format support (not yet implemented)\nconst requestData = parseRequest(yamlContent, { format: 'yaml' });\n```\n\n## API\n\nThe package provides the following functions:\n\n- `parseRequest(content, options = { format: 'bru' })`: Parse request file content\n- `stringifyRequest(requestObj, options = { format: 'bru' })`: Convert request object to file content\n- `parseCollection(content, options = { format: 'bru' })`: Parse collection file content\n- `stringifyCollection(collectionObj, options = { format: 'bru' })`: Convert collection object to file content\n- `parseFolder(content, options = { format: 'bru' })`: Parse folder file content\n- `stringifyFolder(folderObj, options = { format: 'bru' })`: Convert folder object to file content\n- `parseEnvironment(content, options = { format: 'bru' })`: Parse environment file content\n- `stringifyEnvironment(envObj, options = { format: 'bru' })`: Convert environment object to file content\n- `parseDotEnv(content)`: Parse .env file content "
  },
  {
    "path": "packages/bruno-filestore/babel.config.js",
    "content": "module.exports = {\n  presets: [\n    ['@babel/preset-env', { targets: { node: 'current' } }],\n    '@babel/preset-typescript',\n  ],\n}; "
  },
  {
    "path": "packages/bruno-filestore/jest.config.js",
    "content": "module.exports = {\n  testEnvironment: 'node',\n  transform: {\n    '^.+\\\\.(js|ts)$': 'babel-jest',\n  },\n  moduleFileExtensions: ['js', 'ts'],\n  testMatch: ['**/__tests__/**/*.(js|ts)', '**/*.(test|spec).(js|ts)'],\n  collectCoverageFrom: [\n    'src/**/*.(js|ts)',\n    '!src/**/*.d.ts',\n  ],\n  setupFilesAfterEnv: [],\n}; "
  },
  {
    "path": "packages/bruno-filestore/package.json",
    "content": "{\n  \"name\": \"@usebruno/filestore\",\n  \"version\": \"0.1.0\",\n  \"license\": \"MIT\",\n  \"main\": \"dist/cjs/index.js\",\n  \"module\": \"dist/esm/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"files\": [\n    \"dist\",\n    \"src\",\n    \"package.json\"\n  ],\n  \"scripts\": {\n    \"clean\": \"rimraf dist\",\n    \"prebuild\": \"npm run clean\",\n    \"build\": \"rollup -c && tsc --emitDeclarationOnly\",\n    \"watch\": \"rollup -c -w\",\n    \"test\": \"jest\",\n    \"test:watch\": \"jest --watch\",\n    \"prepack\": \"npm run test && npm run build\"\n  },\n  \"devDependencies\": {\n    \"@babel/preset-env\": \"^7.22.0\",\n    \"@babel/preset-typescript\": \"^7.22.0\",\n    \"@rollup/plugin-commonjs\": \"^23.0.2\",\n    \"@rollup/plugin-json\": \"^6.1.0\",\n    \"@rollup/plugin-node-resolve\": \"^15.0.1\",\n    \"@rollup/plugin-typescript\": \"^12.1.2\",\n    \"@types/jest\": \"^29.5.11\",\n    \"@types/lodash\": \"^4.14.191\",\n    \"@types/node\": \"^24.1.0\",\n    \"@usebruno/schema-types\": \"0.0.1\",\n    \"babel-jest\": \"^29.7.0\",\n    \"jest\": \"^29.2.0\",\n    \"nanoid\": \"3.3.8\",\n    \"rimraf\": \"^3.0.2\",\n    \"rollup\": \"3.29.5\",\n    \"rollup-plugin-dts\": \"^5.0.0\",\n    \"rollup-plugin-peer-deps-external\": \"^2.2.4\",\n    \"rollup-plugin-terser\": \"^7.0.2\",\n    \"typescript\": \"^4.8.4\"\n  },\n  \"overrides\": {\n    \"rollup\": \"3.29.5\"\n  },\n  \"dependencies\": {\n    \"@types/nanoid\": \"^2.1.0\",\n    \"@usebruno/lang\": \"0.12.0\",\n    \"ajv\": \"^8.17.1\",\n    \"lodash\": \"^4.17.21\",\n    \"yaml\": \"^2.3.4\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-filestore/rollup.config.js",
    "content": "const { nodeResolve } = require('@rollup/plugin-node-resolve');\nconst commonjs = require('@rollup/plugin-commonjs');\nconst typescript = require('@rollup/plugin-typescript');\nconst json = require('@rollup/plugin-json');\nconst { terser } = require('rollup-plugin-terser');\nconst peerDepsExternal = require('rollup-plugin-peer-deps-external');\n\nconst packageJson = require('./package.json');\n\nconst externalDeps = [\n  '@usebruno/lang',\n  '@usebruno/schema-types',\n  /@usebruno\\/schema-types\\/.*/,\n  '@opencollection/types',\n  /@opencollection\\/types\\/.*/,\n  // Runtime dependencies\n  'lodash',\n  'yaml',\n  'ajv',\n  // Node built-ins\n  'worker_threads',\n  'path',\n  'fs'\n];\n\nconst commonPlugins = [\n  peerDepsExternal(),\n  nodeResolve({\n    extensions: ['.js', '.ts', '.tsx', '.json']\n  }),\n  json(),\n  commonjs(),\n  typescript({\n    tsconfig: './tsconfig.json',\n    declaration: false,\n    declarationMap: false\n  }),\n  terser()\n];\n\nmodule.exports = [\n  {\n    input: 'src/index.ts',\n    output: [\n      {\n        file: packageJson.main,\n        format: 'cjs',\n        sourcemap: true,\n        exports: 'named'\n      },\n      {\n        file: packageJson.module,\n        format: 'esm',\n        sourcemap: true,\n        exports: 'named'\n      }\n    ],\n    plugins: commonPlugins,\n    external: externalDeps\n  },\n  {\n    input: 'src/workers/worker-script.ts',\n    output: [\n      {\n        file: 'dist/cjs/workers/worker-script.js',\n        format: 'cjs',\n        sourcemap: true\n      },\n      {\n        file: 'dist/esm/workers/worker-script.js',\n        format: 'cjs',\n        sourcemap: true\n      }\n    ],\n    plugins: commonPlugins,\n    external: externalDeps\n  }\n];\n"
  },
  {
    "path": "packages/bruno-filestore/src/constants.ts",
    "content": "import type { CollectionFormat } from './types';\n\nexport const DEFAULT_COLLECTION_FORMAT: CollectionFormat = 'yml';\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/bru/index.ts",
    "content": "import * as _ from 'lodash';\nimport {\n  bruToJsonV2,\n  jsonToBruV2,\n  bruToEnvJsonV2,\n  envJsonToBruV2,\n  collectionBruToJson as _collectionBruToJson,\n  jsonToCollectionBru as _jsonToCollectionBru\n} from '@usebruno/lang';\nimport { getOauth2AdditionalParameters } from './utils/oauth2-additional-params';\n\nexport const parseBruRequest = (data: string | any, parsed: boolean = false): any => {\n  try {\n    const json = parsed ? data : bruToJsonV2(data);\n\n    let requestType = _.get(json, 'meta.type');\n    switch (requestType) {\n      case 'http':\n        requestType = 'http-request';\n        break;\n      case 'graphql':\n        requestType = 'graphql-request';\n        break;\n      case 'grpc':\n        requestType = 'grpc-request';\n        break;\n      case 'ws':\n        requestType = 'ws-request';\n        break;\n      default:\n        requestType = 'http-request';\n    }\n\n    const sequence = _.get(json, 'meta.seq');\n    const urlPath: Record<typeof requestType, string> = {\n      'grpc-request': 'grpc.url',\n      'ws-request': 'ws.url',\n      'default': 'http.url'\n    };\n    const transformedJson = {\n      type: requestType,\n      name: _.get(json, 'meta.name'),\n      seq: !_.isNaN(sequence) ? Number(sequence) : 1,\n      settings: _.get(json, 'settings', {}),\n      tags: _.get(json, 'meta.tags', []),\n      request: {\n        // Preserving special characters in custom methods. Using _.upperCase strips special characters.\n        method:\n          requestType === 'grpc-request'\n            ? _.get(json, 'grpc.method', '')\n            : String(_.get(json, 'http.method') ?? '').toUpperCase(),\n        url: _.get(json, urlPath[requestType], _.get(json, urlPath.default)),\n        headers: requestType === 'grpc-request' ? _.get(json, 'metadata', []) : _.get(json, 'headers', []),\n        auth: _.get(json, 'auth', {}),\n        body: _.get(json, 'body', {}),\n        script: _.get(json, 'script', {}),\n        vars: _.get(json, 'vars', {}),\n        assertions: _.get(json, 'assertions', []),\n        tests: _.get(json, 'tests', ''),\n        docs: _.get(json, 'docs', '')\n      },\n      examples: _.get(json, 'examples', []).map((e: any) => {\n        return bruExampleToJson(e, true, requestType, _.get(json, 'http.method'));\n      })\n    } as any;\n\n    // Add request type specific fields\n    if (requestType === 'grpc-request') {\n      const selectedMethodType = _.get(json, 'grpc.methodType');\n      selectedMethodType && ((transformedJson.request as any).methodType = selectedMethodType);\n      const protoPath = _.get(json, 'grpc.protoPath');\n      protoPath && ((transformedJson.request as any).protoPath = protoPath);\n      transformedJson.request.auth.mode = _.get(json, 'grpc.auth', 'none');\n      transformedJson.request.body = _.get(json, 'body', {\n        mode: 'grpc',\n        grpc: _.get(json, 'body.grpc', [\n          {\n            name: 'message 1',\n            content: '{}'\n          }\n        ])\n      });\n    } else if (requestType === 'ws-request') {\n      transformedJson.request.auth.mode = _.get(json, 'ws.auth', 'none');\n      transformedJson.request.body = _.get(json, 'body', {\n        mode: 'ws',\n        ws: _.get(json, 'body.ws', [\n          {\n            name: 'message 1',\n            content: '{}'\n          }\n        ])\n      });\n    } else {\n      // For HTTP and GraphQL\n      (transformedJson.request as any).params = _.get(json, 'params', []);\n      transformedJson.request.auth.mode = _.get(json, 'http.auth', 'none');\n      transformedJson.request.body.mode = _.get(json, 'http.body', 'none');\n    }\n\n    // add oauth2 additional parameters if they exist\n    const hasOauth2GrantType = json?.auth?.oauth2?.grantType;\n    if (hasOauth2GrantType) {\n      const additionalParameters = getOauth2AdditionalParameters(json);\n      const hasAdditionalParameters = Object.keys(additionalParameters || {}).length > 0;\n      if (hasAdditionalParameters) {\n        transformedJson.request.auth.oauth2.additionalParameters = additionalParameters;\n      }\n    }\n    return transformedJson;\n  } catch (error) {\n    console.log('parseBruRequest error', error);\n    throw error;\n  }\n};\n\nexport const stringifyBruRequest = (json: any): string => {\n  try {\n    let type = _.get(json, 'type');\n    switch (type) {\n      case 'http-request':\n        type = 'http';\n        break;\n      case 'graphql-request':\n        type = 'graphql';\n        break;\n      case 'grpc-request':\n        type = 'grpc';\n        break;\n      case 'ws-request':\n        type = 'ws';\n        break;\n      default:\n        type = 'http';\n    }\n\n    const sequence = _.get(json, 'seq');\n\n    // Start with the common meta section\n    const bruJson = {\n      meta: {\n        name: _.get(json, 'name'),\n        type: type,\n        seq: !_.isNaN(sequence) ? Number(sequence) : 1,\n        tags: _.get(json, 'tags', [])\n      }\n    } as any;\n\n    // For HTTP and GraphQL requests, maintain the current structure\n    if (type === 'http' || type === 'graphql') {\n      bruJson.http = {\n        // Preserve special characters in custom request methods. Avoid _.lowerCase which strips symbols.\n        method: String(_.get(json, 'request.method') ?? '').toLowerCase(),\n        url: _.get(json, 'request.url'),\n        auth: _.get(json, 'request.auth.mode', 'none'),\n        body: _.get(json, 'request.body.mode', 'none')\n      };\n      bruJson.params = _.get(json, 'request.params', []);\n      bruJson.body = _.get(json, 'request.body', {\n        mode: 'json',\n        json: '{}'\n      });\n    } else if (type === 'grpc') {\n      // For gRPC, add gRPC-specific structure but maintain field names\n      bruJson.grpc = {\n        url: _.get(json, 'request.url'),\n        auth: _.get(json, 'request.auth.mode', 'none'),\n        body: _.get(json, 'request.body.mode', 'grpc')\n      };\n      // Only add method if it exists\n      const method = _.get(json, 'request.method');\n      const methodType = _.get(json, 'request.methodType');\n      const protoPath = _.get(json, 'request.protoPath');\n      if (method) bruJson.grpc.method = method;\n      if (methodType) bruJson.grpc.methodType = methodType;\n      if (protoPath) bruJson.grpc.protoPath = protoPath;\n      bruJson.body = _.get(json, 'request.body', {\n        mode: 'grpc',\n        grpc: _.get(json, 'request.body.grpc', [\n          {\n            name: 'message 1',\n            content: '{}'\n          }\n        ])\n      });\n    } else if (type === 'ws') {\n      bruJson.ws = {\n        url: _.get(json, 'request.url'),\n        auth: _.get(json, 'request.auth.mode', 'none'),\n        body: _.get(json, 'request.body.mode', 'ws')\n      };\n\n      bruJson.body = _.get(json, 'request.body', {\n        mode: 'ws',\n        ws: _.get(json, 'request.body.ws', [\n          {\n            name: 'message 1',\n            content: '{}'\n          }\n        ])\n      });\n    }\n\n    // Common fields for all request types\n    if (type === 'grpc') {\n      bruJson.metadata = _.get(json, 'request.headers', []); // Use metadata for gRPC\n    } else {\n      bruJson.headers = _.get(json, 'request.headers', []); // Use headers for HTTP/GraphQL\n    }\n    bruJson.auth = _.get(json, 'request.auth', {});\n    bruJson.script = _.get(json, 'request.script', {});\n    bruJson.vars = {\n      req: _.get(json, 'request.vars.req', []),\n      res: _.get(json, 'request.vars.res', [])\n    };\n    // should we add assertions and tests for grpc requests?\n    bruJson.assertions = _.get(json, 'request.assertions', []);\n    bruJson.tests = _.get(json, 'request.tests', '');\n    bruJson.settings = _.get(json, 'settings', {});\n    bruJson.docs = _.get(json, 'request.docs', '');\n    bruJson.examples = _.get(json, 'examples', []).map((e: any) => jsonExampleToBru(e));\n\n    const bru = jsonToBruV2(bruJson);\n    return bru;\n  } catch (error) {\n    throw error;\n  }\n};\n\nexport const parseBruCollection = (data: string | any, parsed: boolean = false): any => {\n  try {\n    const json = parsed ? data : _collectionBruToJson(data);\n\n    const transformedJson: any = {\n      request: {\n        headers: _.get(json, 'headers', []),\n        auth: _.get(json, 'auth', {}),\n        script: _.get(json, 'script', {}),\n        vars: _.get(json, 'vars', {}),\n        tests: _.get(json, 'tests', '')\n      },\n      settings: _.get(json, 'settings', {}),\n      docs: _.get(json, 'docs', '')\n    };\n\n    // add meta if it exists\n    // this is only for folder bru file\n    if (json.meta) {\n      transformedJson.meta = {\n        name: json.meta.name\n      };\n\n      // Include seq if it exists\n      if (json.meta.seq !== undefined) {\n        const sequence = json.meta.seq;\n        transformedJson.meta.seq = !isNaN(sequence) ? Number(sequence) : 1;\n      }\n    }\n\n    // add oauth2 additional parameters if they exist\n    const hasOauth2GrantType = json?.auth?.oauth2?.grantType;\n    if (hasOauth2GrantType) {\n      const additionalParameters = getOauth2AdditionalParameters(json);\n      const hasAdditionalParameters = Object.keys(additionalParameters).length > 0;\n      if (hasAdditionalParameters) {\n        transformedJson.request.auth.oauth2.additionalParameters = additionalParameters;\n      }\n    }\n\n    return transformedJson;\n  } catch (error) {\n    return Promise.reject(error);\n  }\n};\n\nexport const stringifyBruCollection = (json: any, isFolder?: boolean): string => {\n  try {\n    const collectionBruJson: any = {\n      headers: _.get(json, 'request.headers', []),\n      script: {\n        req: _.get(json, 'request.script.req', ''),\n        res: _.get(json, 'request.script.res', '')\n      },\n      vars: {\n        req: _.get(json, 'request.vars.req', []),\n        res: _.get(json, 'request.vars.res', [])\n      },\n      tests: _.get(json, 'request.tests', ''),\n      auth: _.get(json, 'request.auth', {}),\n      docs: _.get(json, 'docs', '')\n    };\n\n    // add meta if it exists\n    // this is only for folder bru file\n    if (json?.meta) {\n      collectionBruJson.meta = {\n        name: json.meta.name\n      };\n\n      // Include seq if it exists\n      if (json.meta.seq !== undefined) {\n        const sequence = json.meta.seq;\n        collectionBruJson.meta.seq = !isNaN(sequence) ? Number(sequence) : 1;\n      }\n    }\n\n    if (!isFolder) {\n      collectionBruJson.auth = _.get(json, 'request.auth', {});\n    }\n\n    return _jsonToCollectionBru(collectionBruJson);\n  } catch (error) {\n    throw error;\n  }\n};\n\nexport const parseBruEnvironment = (bru: string): any => {\n  try {\n    const json = bruToEnvJsonV2(bru);\n\n    // the app env format requires each variable to have a type\n    // this need to be evaluated and safely removed\n    // i don't see it being used in schema validation\n    if (json && json.variables && json.variables.length) {\n      _.each(json.variables, (v: any) => (v.type = 'text'));\n    }\n\n    return json;\n  } catch (error) {\n    return Promise.reject(error);\n  }\n};\n\nexport const stringifyBruEnvironment = (json: any): string => {\n  try {\n    const bru = envJsonToBruV2(json);\n    return bru;\n  } catch (error) {\n    throw error;\n  }\n};\n\n// New functions for example handling\nexport const bruExampleToJson = (data: string | any, parsed: boolean = false, parentType?: string, parentMethod?: string): any => {\n  try {\n    const json = parsed ? data : bruToJsonV2(data);\n\n    // Use parent request's type and method if provided\n    const requestType = parentType || _.get(json, 'meta.type', 'http');\n    const requestMethod = parentMethod || _.get(json, 'http.method', 'GET');\n\n    let transformedType = requestType;\n    switch (requestType) {\n      case 'http':\n        transformedType = 'http-request';\n        break;\n      case 'graphql':\n        transformedType = 'graphql-request';\n        break;\n      case 'grpc':\n        transformedType = 'grpc-request';\n        break;\n      case 'ws':\n        transformedType = 'ws-request';\n        break;\n      default:\n        transformedType = 'http-request';\n    }\n\n    // Follow the same structure as the main request, but with missing fields for examples\n    const transformedJson = {\n      type: transformedType,\n      name: _.get(json, 'name'),\n      description: _.get(json, 'description', ''),\n      // Examples don't have seq, settings, tags\n      request: {\n        method: _.get(json, 'request.method') || requestMethod,\n        url: _.get(json, 'request.url'),\n        headers: _.get(json, 'request.headers', []),\n        body: _.get(json, 'request.body', {\n          mode: 'none'\n        }),\n        // Examples don't have script, vars, assertions, tests, docs\n        params: _.get(json, 'request.params', [])\n      },\n      response: {\n        headers: _.get(json, 'response.headers', []).map((header: any) => ({\n          name: header.name,\n          value: header.value\n        })),\n        status: String(_.get(json, 'response.status', '200')),\n        statusText: _.get(json, 'response.statusText', 'OK'),\n        body: {\n          type: _.get(json, 'response.body.type', 'json'),\n          content: _.get(json, 'response.body.content', '')\n        }\n      }\n    } as any;\n\n    return transformedJson;\n  } catch (error) {\n    console.log('bruExampleToJson error', error);\n    throw error;\n  }\n};\n\nexport const jsonExampleToBru = (json: any) => {\n  try {\n    // Transform the JSON to match the same structure as main request, but with missing fields\n    const exampleJson = {\n      name: _.get(json, 'name'),\n      description: _.get(json, 'description', ''),\n      // Examples don't have seq, settings, tags\n      request: {\n        method: _.get(json, 'request.method'),\n        url: _.get(json, 'request.url'),\n        headers: _.get(json, 'request.headers', []),\n        body: _.get(json, 'request.body', {}),\n        // Examples don't have script, vars, assertions, tests, docs\n        params: _.get(json, 'request.params', [])\n      },\n      response: _.get(json, 'response', {})\n    };\n\n    return exampleJson;\n  } catch (error) {\n    throw error;\n  }\n};\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/bru/tests/fixtures/oauth2-additional-params.js",
    "content": "const getBruJsonWithAdditionalParams = (grantType) => ({\n  \"meta\": {\n    \"name\": \"OAuth2 Additional Params Test\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"http\": {\n    \"method\": \"get\",\n    \"url\": \"https://api.usebruno.com/protected\"\n  },\n  \"auth\": {\n    \"oauth2\": {\n      \"grantType\": grantType,\n    },\n  },\n  \"oauth2_additional_parameters_auth_req_headers\": [\n    {\n      \"name\": \"auth-header\",\n      \"value\": \"auth-header-value\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"disabled-auth-header\",\n      \"value\": \"disabled-auth-header-value\",\n      \"enabled\": false\n    }\n  ],\n  \"oauth2_additional_parameters_auth_req_queryparams\": [\n    {\n      \"name\": \"auth-query-param\",\n      \"value\": \"auth-query-param-value\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"disabled-auth-query-param\",\n      \"value\": \"disabled-auth-query-param-value\",\n      \"enabled\": false\n    }\n  ],\n  \"oauth2_additional_parameters_access_token_req_headers\": [\n    {\n      \"name\": \"token-header\",\n      \"value\": \"token-header-value\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"disabled-token-header\",\n      \"value\": \"disabled-token-header-value\",\n      \"enabled\": false\n    }\n  ],\n  \"oauth2_additional_parameters_access_token_req_queryparams\": [\n    {\n      \"name\": \"token-query-param\",\n      \"value\": \"token-query-param-value\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"disabled-token-query-param\",\n      \"value\": \"disabled-token-query-param-value\",\n      \"enabled\": false\n    }\n  ],\n  \"oauth2_additional_parameters_access_token_req_bodyvalues\": [\n    {\n      \"name\": \"token-body\",\n      \"value\": \"token-body-value\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"disabled-token-body\",\n      \"value\": \"disabled-token-body-value\",\n      \"enabled\": false\n    }\n  ],\n  \"oauth2_additional_parameters_refresh_token_req_headers\": [\n    {\n      \"name\": \"refresh-header\",\n      \"value\": \"refresh-header-value\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"disabled-refresh-header\",\n      \"value\": \"disabled-refresh-header-value\",\n      \"enabled\": false\n    }\n  ],\n  \"oauth2_additional_parameters_refresh_token_req_queryparams\": [\n    {\n      \"name\": \"refresh-query-param\",\n      \"value\": \"refresh-query-param-value\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"disabled-refresh-query-param\",\n      \"value\": \"disabled-refresh-query-param-value\",\n      \"enabled\": false\n    }\n  ],\n  \"oauth2_additional_parameters_refresh_token_req_bodyvalues\": [\n    {\n      \"name\": \"refresh-body\",\n      \"value\": \"refresh-body-value\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"disabled-refresh-body\",\n      \"value\": \"disabled-refresh-body-value\",\n      \"enabled\": false\n    }\n  ]\n})\n\nexport {\n  getBruJsonWithAdditionalParams\n};\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/bru/tests/fixtures/request-parse-and-redact-body-data/input.bru",
    "content": "meta {\n  name: echo request\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://echo.usebruno.com\n  body: json\n  auth: none\n}\nbody:json {\n  {\n    \"hello\": \"world\"\n  }\n}\n\nbody:text {\n  This is a text body\r\n}\r\nbody:xml {\n  <xml>\n    <name>John</name>\n    <age>30</age>\n  </xml>\n}\n\nbody:sparql {\n  SELECT * WHERE {\n    ?subject ?predicate ?object .\n  }\n  LIMIT 10\r\n}\r\nbody:graphql {\n  {\n    launchesPast {\n      launch_site {\n        site_name\n      }\n      launch_success\n    }\n  }\n}\n\nbody:form-urlencoded {\n  apikey: secret\n  numbers: +91998877665\n  ~message: hello\n}\n\nbody:multipart-form {\n  apikey: secret\n  numbers: +91998877665\n  ~message: hello\n}\nbody:file {\n  file: @file(path/to/file.json) @contentType(application/json)\n  file: @file(path/to/file.json) @contentType(application/json)\n  ~file: @file(path/to/file2.json) @contentType(application/json)\n}\n\nbody:graphql:vars {\n  {\n    \"limit\": 5\n  }\n}\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/bru/tests/fixtures/request-parse-and-redact-body-data/output.bru",
    "content": "meta {\n  name: echo request\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://echo.usebruno.com\n  body: json\n  auth: none\n}\n\nbody:form-urlencoded {\n  apikey: secret\n  numbers: +91998877665\n  ~message: hello\n}\n\nbody:multipart-form {\n  apikey: secret\n  numbers: +91998877665\n  ~message: hello\n}\n\nbody:file {\n  file: @file(path/to/file.json) @contentType(application/json)\n  file: @file(path/to/file.json) @contentType(application/json)\n  ~file: @file(path/to/file2.json) @contentType(application/json)\n}\n\nbody:graphql:vars {\n  {\n    \"limit\": 5\n  }\n}\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/bru/tests/oauth2-additional-params.spec.js",
    "content": "const { getOauth2AdditionalParameters } = require('../utils/oauth2-additional-params');\nconst { parseBruRequest, parseBruCollection } = require('../index');\nconst { getBruJsonWithAdditionalParams } = require('./fixtures/oauth2-additional-params');\n\ndescribe('getOauth2AdditionalParameters', () => {\n  it('authorization_code', () => {\n    const additionalParameters = getOauth2AdditionalParameters(getBruJsonWithAdditionalParams('authorization_code'));\n    expect(additionalParameters.authorization).toHaveLength(4);\n    expect(additionalParameters.token).toHaveLength(6);\n    expect(additionalParameters.refresh).toHaveLength(6);\n\n    expect(additionalParameters.authorization.map(p => p.sendIn).sort()).toEqual(['headers', 'headers', 'queryparams', 'queryparams']);\n    expect(additionalParameters.token.map(p => p.sendIn).sort()).toEqual(['body', 'body', 'headers', 'headers', 'queryparams', 'queryparams']);\n    expect(additionalParameters.refresh.map(p => p.sendIn).sort()).toEqual(['body', 'body', 'headers', 'headers', 'queryparams', 'queryparams']);\n  });\n\n  it('client_credentials', () => {\n    const additionalParameters = getOauth2AdditionalParameters(getBruJsonWithAdditionalParams('client_credentials'));\n    expect(additionalParameters.authorization).toBeUndefined();\n    expect(additionalParameters.token).toHaveLength(6);\n    expect(additionalParameters.refresh).toHaveLength(6);\n\n    expect(additionalParameters.token.map(p => p.sendIn).sort()).toEqual(['body', 'body', 'headers', 'headers', 'queryparams', 'queryparams']);\n    expect(additionalParameters.refresh.map(p => p.sendIn).sort()).toEqual(['body', 'body', 'headers', 'headers', 'queryparams', 'queryparams']);\n  });\n\n  it('password', () => {\n    const additionalParameters = getOauth2AdditionalParameters(getBruJsonWithAdditionalParams('password'));\n    expect(additionalParameters.authorization).toBeUndefined();\n    expect(additionalParameters.token).toHaveLength(6);\n    expect(additionalParameters.refresh).toHaveLength(6);\n\n    expect(additionalParameters.token.map(p => p.sendIn).sort()).toEqual(['body', 'body', 'headers', 'headers', 'queryparams', 'queryparams']);\n    expect(additionalParameters.refresh.map(p => p.sendIn).sort()).toEqual(['body', 'body', 'headers', 'headers', 'queryparams', 'queryparams']);\n  });\n\n  it('implicit', () => {\n    const additionalParameters = getOauth2AdditionalParameters(getBruJsonWithAdditionalParams('implicit'));\n    expect(additionalParameters.authorization).toHaveLength(4);\n    expect(additionalParameters.token).toBeUndefined();\n    expect(additionalParameters.refresh).toBeUndefined();\n\n    expect(additionalParameters.authorization.map(p => p.sendIn).sort()).toEqual(['headers', 'headers', 'queryparams', 'queryparams']);\n  });\n});"
  },
  {
    "path": "packages/bruno-filestore/src/formats/bru/tests/request-parse-and-redact-body-data.spec.js",
    "content": "const fs = require('node:fs');\nconst path = require('node:path');\nconst { bruRequestParseAndRedactBodyData } = require(\"../utils/request-parse-and-redact-body-data\");\n\ndescribe(\"parse and redact body data\", () => {\n  it(\"should redact body blocks from the bru file string\", () => {\n    const fixturesPath = `/fixtures/request-parse-and-redact-body-data`;\n    const inputBruString = fs.readFileSync(path.join(__dirname, fixturesPath, './input.bru'), 'utf8');\n    const expectedOutputBruString = fs.readFileSync(path.join(__dirname, fixturesPath, './output.bru'), 'utf8');\n\n    const res = bruRequestParseAndRedactBodyData(inputBruString);\n    expect(res.bruFileStringWithRedactedBody).toBe(expectedOutputBruString);\n    expect(res.extractedBodyContent).toEqual({\n      graphql: `\n{\n  launchesPast {\n    launch_site {\n      site_name\n    }\n    launch_success\n  }\n}\n      `.trim(),\n      json: `\n{\n  \"hello\": \"world\"\n}     \n      `.trim(),\n      sparql: `\nSELECT * WHERE {\n  ?subject ?predicate ?object .\n}\nLIMIT 10\n      `.trim(),\n      text: `This is a text body`, \n      xml: `\n<xml>\n  <name>John</name>\n  <age>30</age>\n</xml>\n      `.trim()\n    })\n  });\n});"
  },
  {
    "path": "packages/bruno-filestore/src/formats/bru/utils/oauth2-additional-params.ts",
    "content": "type T_Oauth2ParameterType = 'authorization' | 'token' | 'refresh';\ntype T_Oauth2ParameterSendInType = 'headers' | 'queryparams' | 'body';\n\nexport interface T_OAuth2AdditionalParam {\n  name: string;\n  value: string;\n  enabled: boolean;\n  sendIn: T_Oauth2ParameterSendInType;\n}\n\nexport interface T_OAuth2AdditionalParameters {\n  authorization?: T_OAuth2AdditionalParam[];\n  token?: T_OAuth2AdditionalParam[];\n  refresh?: T_OAuth2AdditionalParam[];\n}\n\nexport interface T_Oauth2Auth {\n  grantType: string;\n  additionalParameters?: T_OAuth2AdditionalParameters;\n}\n\nexport interface T_BruJson {\n  auth: {\n    oauth2: T_Oauth2Auth;\n  };\n  oauth2_additional_parameters_auth_req_headers?: any[];\n  oauth2_additional_parameters_auth_req_queryparams?: any[];\n  oauth2_additional_parameters_access_token_req_headers?: any[];\n  oauth2_additional_parameters_access_token_req_queryparams?: any[];\n  oauth2_additional_parameters_access_token_req_bodyvalues?: any[];\n  oauth2_additional_parameters_refresh_token_req_headers?: any[];\n  oauth2_additional_parameters_refresh_token_req_queryparams?: any[];\n  oauth2_additional_parameters_refresh_token_req_bodyvalues?: any[];\n}\n\ninterface T_Oauth2ParameterMapping {\n  type: T_Oauth2ParameterType;\n  sendIn: T_Oauth2ParameterSendInType;\n  source: keyof T_BruJson;\n}\n\nconst PARAMETER_MAPPINGS: T_Oauth2ParameterMapping[] = [\n  // Authorization parameters (only for authorization_code grant type)\n  { type: 'authorization', sendIn: 'headers', source: 'oauth2_additional_parameters_auth_req_headers' },\n  { type: 'authorization', sendIn: 'queryparams', source: 'oauth2_additional_parameters_auth_req_queryparams' },\n\n  // Token parameters (for all grant types)\n  { type: 'token', sendIn: 'headers', source: 'oauth2_additional_parameters_access_token_req_headers' },\n  { type: 'token', sendIn: 'queryparams', source: 'oauth2_additional_parameters_access_token_req_queryparams' },\n  { type: 'token', sendIn: 'body', source: 'oauth2_additional_parameters_access_token_req_bodyvalues' },\n\n  // Refresh parameters (for grant types that support refresh)\n  { type: 'refresh', sendIn: 'headers', source: 'oauth2_additional_parameters_refresh_token_req_headers' },\n  { type: 'refresh', sendIn: 'queryparams', source: 'oauth2_additional_parameters_refresh_token_req_queryparams' },\n  { type: 'refresh', sendIn: 'body', source: 'oauth2_additional_parameters_refresh_token_req_bodyvalues' }\n];\n\n/**\n * Maps source parameters to T_OAuth2AdditionalParam format\n */\nconst mapParametersFromSource = (sourceParams: any[], sendIn: T_Oauth2ParameterSendInType): T_OAuth2AdditionalParam[] => {\n  if (!sourceParams?.length) {\n    return [];\n  }\n\n  return sourceParams.map((param) => ({\n    ...param,\n    sendIn\n  }));\n};\n\n/**\n * Checks if a parameter type should be included based on grant type\n */\nconst shouldIncludeParameterType = (type: T_Oauth2ParameterType, grantType: string): boolean => {\n  // Authorization parameters are only valid for authorization_code grant type\n  if (type === 'authorization') {\n    return grantType === 'authorization_code' || grantType === 'implicit';\n  }\n\n  if (type === 'token' || type === 'refresh') {\n    return grantType !== 'implicit';\n  }\n\n  // Token and refresh parameters are valid for all grant types\n  return true;\n};\n\n/**\n * Collects all parameters for a specific type (authorization, token, or refresh)\n */\nconst collectParametersForType = (\n  json: T_BruJson,\n  type: T_Oauth2ParameterType,\n  grantType: string\n): T_OAuth2AdditionalParam[] => {\n  if (!shouldIncludeParameterType(type, grantType)) {\n    return [];\n  }\n\n  const relevantMappings = PARAMETER_MAPPINGS.filter((mapping) => mapping.type === type);\n  const allParams: T_OAuth2AdditionalParam[] = [];\n\n  for (const mapping of relevantMappings) {\n    const sourceParams = json[mapping.source] as any[];\n    const mappedParams = mapParametersFromSource(sourceParams, mapping.sendIn);\n    allParams.push(...mappedParams);\n  }\n\n  return allParams;\n};\n\n/**\n * This function extracts OAuth2 additional parameters from various sources in the bru json data and organizes\n * them into a structured format based on their usage context (authorization, token, refresh).\n *\n * @param json - json object containing OAuth2 configuration and additional parameters\n * @returns OAuth2 additional parameters\n */\nexport const getOauth2AdditionalParameters = (json: T_BruJson): T_OAuth2AdditionalParameters => {\n  const grantType = json.auth.oauth2.grantType;\n  const additionalParameters: T_OAuth2AdditionalParameters = {};\n\n  try {\n    // Collect parameters for each type\n    const parameterTypes: T_Oauth2ParameterType[] = ['authorization', 'token', 'refresh'];\n\n    for (const type of parameterTypes) {\n      const params = collectParametersForType(json, type, grantType);\n      if (params.length > 0) {\n        additionalParameters[type] = params;\n      }\n    }\n  } catch (error) {\n    console.error(error);\n    console.error('Error while getting the oauth2 additional parameters!');\n  }\n\n  return additionalParameters;\n};\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/bru/utils/request-parse-and-redact-body-data.ts",
    "content": "/**\n * Parses a .bru file and extracts body content while redacting it from the main content\n * @param {string} bruFileContent - The raw content of the .bru file\n * @returns {Object} Object containing redacted file content and extracted body data\n */\nexport const bruRequestParseAndRedactBodyData = (bruFileContent: string) => {\n  try {\n    // Define the patterns that indicate the start of different body types\n    const bodyTypePatterns = [\n      'body:json {',\n      'body:text {',\n      'body:xml {',\n      'body:sparql {',\n      'body:graphql {'\n    ];\n\n    // Normalize line endings to LF\n    bruFileContent = (bruFileContent || '').replace(/\\r\\n/g, '\\n');\n\n    const EOL = `\\n`;\n\n    /**\n     * Removes the leading 2-space indentation from each line of a string\n     * @param {string} indentedString - The string with leading spaces to remove\n     * @returns {string} The string with indentation removed\n     */\n    const removeLeadingIndentation = (indentedString: string) => {\n      if (!indentedString || !indentedString.length) {\n        return indentedString || '';\n      }\n\n      return indentedString\n        .split(EOL)\n        .map((line) => line.replace(/^  /, ''))\n        .join(EOL);\n    };\n\n    // Split the file content into blocks\n    let fileContentBlocks = bruFileContent.split(`${EOL}}${EOL}`);\n    fileContentBlocks = fileContentBlocks.filter(Boolean).map((_) => _.trim());\n\n    // Extract body blocks and their content\n    const extractedBodyBlocks = fileContentBlocks\n      .filter((block) => bodyTypePatterns.some((pattern) => block.startsWith(pattern)))\n      .reduce((bodyContentMap: Record<string, string>, bodyBlock) => {\n        // Extract the body type (json, text, xml, etc.) from the first line\n        const firstLine = bodyBlock.split(EOL)[0];\n        const bodyType = firstLine.split(`body:`)[1].split(/\\s/)[0];\n\n        // Extract the body content (everything between the opening and closing braces)\n        const bodyContentLines = bodyBlock.split(EOL).slice(1);\n        const rawBodyContent = bodyContentLines.join(EOL);\n\n        // Remove indentation from the body content\n        const cleanBodyContent = removeLeadingIndentation(rawBodyContent);\n\n        bodyContentMap[bodyType] = cleanBodyContent;\n        return bodyContentMap;\n      }, {});\n\n    // Filter out body blocks to get the remaining file content\n    const fileContentWithoutBodyBlocks = fileContentBlocks.filter((block) =>\n      !bodyTypePatterns.some((pattern) => block.startsWith(pattern))\n    );\n\n    return {\n      bruFileStringWithRedactedBody: fileContentWithoutBodyBlocks.join(`${EOL}}${EOL}${EOL}`).concat(`${EOL}}${EOL}`),\n      extractedBodyContent: extractedBodyBlocks\n    };\n  } catch (error) {\n    console.error('Error parsing and redacting body data:', error);\n    return {\n      bruFileStringWithRedactedBody: bruFileContent,\n      extractedBodyContent: {}\n    };\n  }\n};\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/common/actions.ts",
    "content": "import type { Action, ActionSetVariable, ActionVariableScope } from '@opencollection/types/common/actions';\nimport type { Variable as BrunoVariable, Variables as BrunoVariables } from '@usebruno/schema-types/common/variables';\nimport { uuid, ensureString } from '../../../utils';\n\n/**\n * Convert Bruno post-response variables to OpenCollection actions.\n * Post-response variables in Bruno are converted to 'set-variable' actions\n * with phase 'after-response'.\n */\nexport const toOpenCollectionActions = (resVariables: BrunoVariables | null | undefined): Action[] | undefined => {\n  if (!resVariables?.length) {\n    return undefined;\n  }\n\n  const actions: Action[] = resVariables.map((v: BrunoVariable): ActionSetVariable => {\n    const action: ActionSetVariable = {\n      type: 'set-variable',\n      phase: 'after-response',\n      selector: {\n        expression: v.value || '',\n        method: 'jsonq'\n      },\n      variable: {\n        name: v.name || '',\n        scope: v.local ? 'request' : 'runtime' as ActionVariableScope\n      }\n    };\n\n    if (v.description?.trim().length) {\n      action.description = v.description;\n    }\n\n    if (v.enabled === false) {\n      action.disabled = true;\n    }\n\n    return action;\n  });\n\n  return actions.length > 0 ? actions : undefined;\n};\n\n/**\n * Convert OpenCollection actions to Bruno post-response variables.\n * Only 'set-variable' actions with phase 'after-response' are converted.\n */\nexport const toBrunoPostResponseVariables = (actions: Action[] | null | undefined): BrunoVariables => {\n  if (!actions?.length) {\n    return [];\n  }\n\n  const resVars: BrunoVariables = [];\n\n  actions.forEach((action: Action) => {\n    // Only process 'set-variable' actions with 'after-response' phase\n    if (action.type === 'set-variable' && action.phase === 'after-response') {\n      const setVarAction = action as ActionSetVariable;\n\n      const variable: BrunoVariable = {\n        uid: uuid(),\n        name: ensureString(setVarAction.variable?.name),\n        value: ensureString(setVarAction.selector?.expression),\n        enabled: setVarAction.disabled !== true,\n        local: false\n      };\n\n      if (setVarAction.description) {\n        variable.description = typeof setVarAction.description === 'string'\n          ? setVarAction.description\n          : (setVarAction.description as any)?.content || '';\n      }\n\n      resVars.push(variable);\n    }\n  });\n\n  return resVars;\n};\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/common/assertions.ts",
    "content": "import type { KeyValue as BrunoKeyValue } from '@usebruno/schema-types/common/key-value';\nimport type { Assertion } from '@opencollection/types/common/assertions';\nimport { uuid, ensureString } from '../../../utils';\n\nconst OPERATORS = [\n  'eq',\n  'neq',\n  'gt',\n  'gte',\n  'lt',\n  'lte',\n  'in',\n  'notIn',\n  'contains',\n  'notContains',\n  'length',\n  'matches',\n  'notMatches',\n  'startsWith',\n  'endsWith',\n  'between',\n  'isEmpty',\n  'isNotEmpty',\n  'isNull',\n  'isUndefined',\n  'isDefined',\n  'isTruthy',\n  'isFalsy',\n  'isJson',\n  'isNumber',\n  'isString',\n  'isBoolean',\n  'isArray'\n] as const;\n\nconst UNARY_OPERATORS = [\n  'isEmpty',\n  'isNotEmpty',\n  'isNull',\n  'isUndefined',\n  'isDefined',\n  'isTruthy',\n  'isFalsy',\n  'isJson',\n  'isNumber',\n  'isString',\n  'isBoolean',\n  'isArray'\n] as const;\n\ntype Operator = typeof OPERATORS[number];\n\nconst parseAssertionOperator = (str: string = ''): { operator: Operator; value: string | undefined } => {\n  if (!str || typeof str !== 'string' || !str.length) {\n    return {\n      operator: 'eq',\n      value: str\n    };\n  }\n\n  const [firstWord, ...rest] = str.trim().split(' ');\n  const remainingValue = rest.join(' ');\n\n  // Check if first word is a unary operator\n  if (UNARY_OPERATORS.includes(firstWord as any)) {\n    return {\n      operator: firstWord as Operator,\n      value: undefined\n    };\n  }\n\n  // Check if first word is any recognized operator\n  if (OPERATORS.includes(firstWord as any)) {\n    return {\n      operator: firstWord as Operator,\n      value: remainingValue\n    };\n  }\n\n  // If not a recognized operator, treat the whole string as value with 'eq' operator\n  return {\n    operator: 'eq',\n    value: str\n  };\n};\n\nexport const toOpenCollectionAssertions = (assertions: BrunoKeyValue[] | null | undefined): Assertion[] | undefined => {\n  if (!assertions?.length) {\n    return undefined;\n  }\n\n  const ocAssertions: Assertion[] = assertions.map((assertion: BrunoKeyValue): Assertion => {\n    const { operator, value } = parseAssertionOperator(assertion.value || '');\n\n    const ocAssertion: Assertion = {\n      expression: assertion.name || '',\n      operator,\n      ...(value !== undefined && { value })\n    };\n\n    if (assertion?.description?.trim().length) {\n      ocAssertion.description = assertion.description;\n    }\n\n    if (assertion.enabled === false) {\n      ocAssertion.disabled = true;\n    }\n\n    return ocAssertion;\n  });\n\n  return ocAssertions.length > 0 ? ocAssertions : undefined;\n};\n\nexport const toBrunoAssertions = (assertions: Assertion[] | null | undefined): BrunoKeyValue[] | undefined => {\n  if (!assertions?.length) {\n    return undefined;\n  }\n\n  const brunoAssertions: BrunoKeyValue[] = assertions.map((assertion: Assertion): BrunoKeyValue => {\n    // Reconstruct the \"operator value\" format that Bruno uses\n    let valueString = ensureString(assertion.operator);\n    if (assertion.value !== undefined && assertion.value !== null) {\n      valueString = `${assertion.operator} ${ensureString(assertion.value)}`;\n    }\n\n    const brunoAssertion: BrunoKeyValue = {\n      uid: uuid(),\n      name: ensureString(assertion.expression),\n      value: valueString,\n      enabled: assertion.disabled !== true\n    };\n\n    if (assertion.description) {\n      if (typeof assertion.description === 'string' && assertion.description.trim().length) {\n        brunoAssertion.description = assertion.description;\n      } else if (typeof assertion.description === 'object' && assertion.description.content?.trim().length) {\n        brunoAssertion.description = assertion.description.content;\n      }\n    }\n\n    return brunoAssertion;\n  });\n\n  return brunoAssertions.length > 0 ? brunoAssertions : undefined;\n};\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/common/auth-oauth2.ts",
    "content": "import type {\n  AuthOAuth2,\n  OAuth2AdditionalParameter,\n  OAuth2AuthorizationCodeFlow,\n  OAuth2ClientCredentials,\n  OAuth2ClientCredentialsFlow,\n  OAuth2ImplicitFlow,\n  OAuth2PKCE,\n  OAuth2ResourceOwner,\n  OAuth2ResourceOwnerPasswordFlow,\n  OAuth2Settings,\n  OAuth2TokenConfig\n} from '@opencollection/types/common/auth';\nimport type {\n  OAuth2 as BrunoOAuth2,\n  OAuthAdditionalParameter as BrunoOAuthAdditionalParameter\n} from '@usebruno/schema-types/common/auth';\nimport { isString, isNonEmptyString } from '../../../utils';\n\nconst normalizeBoolean = (value?: boolean | null): boolean | undefined =>\n  typeof value === 'boolean' ? value : undefined;\n\nconst mapSendIn = (sendIn?: string | null): OAuth2AdditionalParameter['placement'] | undefined => {\n  if (!isString(sendIn)) {\n    return undefined;\n  }\n\n  switch (sendIn.trim().toLowerCase()) {\n    case 'headers':\n      return 'header';\n    case 'queryparams':\n      return 'query';\n    case 'body':\n      return 'body';\n    default:\n      return undefined;\n  }\n};\n\nconst mapAdditionalParameters = (params?: BrunoOAuthAdditionalParameter[] | null): OAuth2AdditionalParameter[] | undefined => {\n  if (!Array.isArray(params) || params.length === 0) {\n    return undefined;\n  }\n\n  const mapped = params\n    .filter((param) => param && isNonEmptyString(param.name))\n    .map((param) => {\n      const placement = mapSendIn(param!.sendIn);\n      if (!placement) {\n        return undefined;\n      }\n\n      const mappedParam: OAuth2AdditionalParameter = {\n        name: param!.name!.trim(),\n        placement\n      };\n\n      isNonEmptyString(param!.value) && (mappedParam.value = param.value);\n\n      return mappedParam;\n    })\n    .filter((param): param is OAuth2AdditionalParameter => Boolean(param));\n\n  return mapped.length > 0 ? mapped : undefined;\n};\n\nconst buildClientCredentials = (oauth: BrunoOAuth2): OAuth2ClientCredentials | undefined => {\n  const credentials: OAuth2ClientCredentials = {};\n\n  isNonEmptyString(oauth.clientId) && (credentials.clientId = oauth.clientId);\n  isNonEmptyString(oauth.clientSecret) && (credentials.clientSecret = oauth.clientSecret);\n  isNonEmptyString(oauth.credentialsPlacement) && (credentials.placement = oauth.credentialsPlacement);\n\n  return Object.keys(credentials).length > 0 ? credentials : undefined;\n};\n\nconst buildResourceOwner = (oauth: BrunoOAuth2): OAuth2ResourceOwner | undefined => {\n  const resourceOwner: OAuth2ResourceOwner = {};\n\n  isNonEmptyString(oauth.username) && (resourceOwner.username = oauth.username);\n  isNonEmptyString(oauth.password) && (resourceOwner.password = oauth.password);\n\n  return Object.keys(resourceOwner).length > 0 ? resourceOwner : undefined;\n};\n\nconst buildPkce = (pkce?: boolean | null): OAuth2PKCE | undefined => {\n  if (pkce === null || pkce === undefined) {\n    return undefined;\n  }\n\n  // If pkce is false, set disabled: true; if true, return empty object (enabled by default)\n  return pkce ? {} : { disabled: true };\n};\n\nconst buildTokenConfig = (oauth: BrunoOAuth2): OAuth2TokenConfig | undefined => {\n  const tokenConfig: OAuth2TokenConfig = {};\n\n  isNonEmptyString(oauth.credentialsId) && (tokenConfig.id = oauth.credentialsId);\n\n  if (!isNonEmptyString(oauth.tokenPlacement)) {\n    // default to header\n    tokenConfig.placement = { header: '' };\n  }\n\n  if (oauth.tokenPlacement === 'header') {\n    tokenConfig.placement = {\n      header: oauth.tokenHeaderPrefix as string\n    };\n  }\n\n  if (oauth.tokenPlacement === 'url') {\n    tokenConfig.placement = {\n      query: oauth.tokenQueryKey as string\n    };\n  }\n\n  tokenConfig.source = oauth.tokenSource || 'access_token';\n\n  return Object.keys(tokenConfig).length > 0 ? tokenConfig : undefined;\n};\n\nconst buildSettings = (oauth: BrunoOAuth2): OAuth2Settings | undefined => {\n  const autoFetchToken = normalizeBoolean(oauth.autoFetchToken);\n  const autoRefreshToken = normalizeBoolean(oauth.autoRefreshToken);\n\n  const settings: OAuth2Settings = {};\n  if (autoFetchToken !== undefined) settings.autoFetchToken = autoFetchToken;\n  if (autoRefreshToken !== undefined) settings.autoRefreshToken = autoRefreshToken;\n\n  return Object.keys(settings).length > 0 ? settings : undefined;\n};\n\nconst buildClientCredentialsFlow = (oauth: BrunoOAuth2): OAuth2ClientCredentialsFlow => {\n  const flow: OAuth2ClientCredentialsFlow = {\n    type: 'oauth2',\n    flow: 'client_credentials'\n  };\n\n  isNonEmptyString(oauth.accessTokenUrl) && (flow.accessTokenUrl = oauth.accessTokenUrl);\n  isNonEmptyString(oauth.refreshTokenUrl) && (flow.refreshTokenUrl = oauth.refreshTokenUrl);\n\n  const credentials = buildClientCredentials(oauth);\n  if (credentials) flow.credentials = credentials;\n\n  isNonEmptyString(oauth.scope) && (flow.scope = oauth.scope);\n\n  const accessTokenRequest = mapAdditionalParameters(oauth.additionalParameters?.token);\n  const refreshTokenRequest = mapAdditionalParameters(oauth.additionalParameters?.refresh);\n\n  if (accessTokenRequest || refreshTokenRequest) {\n    flow.additionalParameters = {};\n    if (accessTokenRequest) {\n      flow.additionalParameters.accessTokenRequest = accessTokenRequest;\n    }\n    if (refreshTokenRequest) {\n      flow.additionalParameters.refreshTokenRequest = refreshTokenRequest;\n    }\n  }\n\n  const tokenConfig = buildTokenConfig(oauth);\n  if (tokenConfig) flow.tokenConfig = tokenConfig;\n\n  const settings = buildSettings(oauth);\n  if (settings) flow.settings = settings;\n\n  return flow;\n};\n\nconst buildResourceOwnerPasswordFlow = (oauth: BrunoOAuth2): OAuth2ResourceOwnerPasswordFlow => {\n  const flow: OAuth2ResourceOwnerPasswordFlow = {\n    type: 'oauth2',\n    flow: 'resource_owner_password_credentials'\n  };\n\n  isNonEmptyString(oauth.accessTokenUrl) && (flow.accessTokenUrl = oauth.accessTokenUrl);\n  isNonEmptyString(oauth.refreshTokenUrl) && (flow.refreshTokenUrl = oauth.refreshTokenUrl);\n\n  const credentials = buildClientCredentials(oauth);\n  if (credentials) flow.credentials = credentials;\n\n  const resourceOwner = buildResourceOwner(oauth);\n  if (resourceOwner) flow.resourceOwner = resourceOwner;\n\n  isNonEmptyString(oauth.scope) && (flow.scope = oauth.scope);\n\n  const accessTokenRequest = mapAdditionalParameters(oauth.additionalParameters?.token);\n  const refreshTokenRequest = mapAdditionalParameters(oauth.additionalParameters?.refresh);\n\n  if (accessTokenRequest || refreshTokenRequest) {\n    flow.additionalParameters = {};\n    if (accessTokenRequest) {\n      flow.additionalParameters.accessTokenRequest = accessTokenRequest;\n    }\n    if (refreshTokenRequest) {\n      flow.additionalParameters.refreshTokenRequest = refreshTokenRequest;\n    }\n  }\n\n  const tokenConfig = buildTokenConfig(oauth);\n  if (tokenConfig) flow.tokenConfig = tokenConfig;\n\n  const settings = buildSettings(oauth);\n  if (settings) flow.settings = settings;\n\n  return flow;\n};\n\nconst buildAuthorizationCodeFlow = (oauth: BrunoOAuth2): OAuth2AuthorizationCodeFlow => {\n  const flow: OAuth2AuthorizationCodeFlow = {\n    type: 'oauth2',\n    flow: 'authorization_code'\n  };\n\n  isNonEmptyString(oauth.authorizationUrl) && (flow.authorizationUrl = oauth.authorizationUrl);\n  isNonEmptyString(oauth.accessTokenUrl) && (flow.accessTokenUrl = oauth.accessTokenUrl);\n  isNonEmptyString(oauth.refreshTokenUrl) && (flow.refreshTokenUrl = oauth.refreshTokenUrl);\n  isNonEmptyString(oauth.callbackUrl) && (flow.callbackUrl = oauth.callbackUrl);\n\n  const credentials = buildClientCredentials(oauth);\n  if (credentials) flow.credentials = credentials;\n\n  const authorizationRequest = mapAdditionalParameters(oauth.additionalParameters?.authorization);\n  const accessTokenRequest = mapAdditionalParameters(oauth.additionalParameters?.token);\n  const refreshTokenRequest = mapAdditionalParameters(oauth.additionalParameters?.refresh);\n\n  if (authorizationRequest || accessTokenRequest || refreshTokenRequest) {\n    flow.additionalParameters = {};\n    if (authorizationRequest) {\n      flow.additionalParameters.authorizationRequest = authorizationRequest;\n    }\n    if (accessTokenRequest) {\n      flow.additionalParameters.accessTokenRequest = accessTokenRequest;\n    }\n    if (refreshTokenRequest) {\n      flow.additionalParameters.refreshTokenRequest = refreshTokenRequest;\n    }\n  }\n\n  isNonEmptyString(oauth.scope) && (flow.scope = oauth.scope);\n  isNonEmptyString(oauth.state) && (flow.state = oauth.state);\n\n  const pkce = buildPkce(oauth.pkce);\n  if (pkce) flow.pkce = pkce;\n\n  const tokenConfig = buildTokenConfig(oauth);\n  if (tokenConfig) flow.tokenConfig = tokenConfig;\n\n  const settings = buildSettings(oauth);\n  if (settings) flow.settings = settings;\n\n  return flow;\n};\n\nconst buildImplicitFlow = (oauth: BrunoOAuth2): OAuth2ImplicitFlow => {\n  const flow: OAuth2ImplicitFlow = {\n    type: 'oauth2',\n    flow: 'implicit'\n  };\n\n  isNonEmptyString(oauth.authorizationUrl) && (flow.authorizationUrl = oauth.authorizationUrl);\n  isNonEmptyString(oauth.callbackUrl) && (flow.callbackUrl = oauth.callbackUrl);\n  isNonEmptyString(oauth.clientId) && (flow.credentials = { clientId: oauth.clientId });\n  isNonEmptyString(oauth.scope) && (flow.scope = oauth.scope);\n  isNonEmptyString(oauth.state) && (flow.state = oauth.state);\n\n  const authorizationRequest = mapAdditionalParameters(oauth.additionalParameters?.authorization);\n  if (authorizationRequest) {\n    flow.additionalParameters = { authorizationRequest };\n  }\n\n  const tokenConfig = buildTokenConfig(oauth);\n  if (tokenConfig) flow.tokenConfig = tokenConfig;\n\n  const settings = buildSettings(oauth);\n  if (settings) flow.settings = settings;\n\n  return flow;\n};\n\nexport const toOpenCollectionOAuth2 = (oauth?: BrunoOAuth2 | null): AuthOAuth2 | undefined => {\n  if (!oauth) {\n    return undefined;\n  }\n\n  switch (oauth.grantType) {\n    case 'client_credentials':\n      return buildClientCredentialsFlow(oauth);\n    case 'password':\n      return buildResourceOwnerPasswordFlow(oauth);\n    case 'authorization_code':\n      return buildAuthorizationCodeFlow(oauth);\n    case 'implicit':\n      return buildImplicitFlow(oauth);\n    default:\n      console.warn(`toOpenCollectionOAuth2: Unsupported OAuth2 grant type \"${oauth.grantType}\".`);\n      return undefined;\n  }\n};\n\nconst reversePlacementMapping = (placement?: OAuth2AdditionalParameter['placement']): 'headers' | 'queryparams' | 'body' | null => {\n  if (!placement) {\n    return null;\n  }\n\n  switch (placement) {\n    case 'header':\n      return 'headers';\n    case 'query':\n      return 'queryparams';\n    case 'body':\n      return 'body';\n    default:\n      return null;\n  }\n};\n\nconst reverseAdditionalParameters = (params?: OAuth2AdditionalParameter[]): BrunoOAuthAdditionalParameter[] | null => {\n  if (!Array.isArray(params) || params.length === 0) {\n    return null;\n  }\n\n  const mapped = params.map((param): BrunoOAuthAdditionalParameter => {\n    const sendIn = reversePlacementMapping(param.placement);\n\n    return {\n      name: param.name || null,\n      value: param.value || null,\n      sendIn: sendIn || 'headers',\n      enabled: true\n    };\n  });\n\n  return mapped.length > 0 ? mapped : null;\n};\n\nexport const toBrunoOAuth2 = (oauth: AuthOAuth2 | null | undefined): BrunoOAuth2 | null => {\n  if (!oauth) {\n    return null;\n  }\n\n  const brunoOAuth: BrunoOAuth2 = {\n    grantType: 'authorization_code',\n    username: null,\n    password: null,\n    callbackUrl: null,\n    authorizationUrl: null,\n    accessTokenUrl: null,\n    clientId: null,\n    clientSecret: null,\n    scope: null,\n    state: null,\n    pkce: false, // Default to false for all grant types\n    credentialsPlacement: null,\n    credentialsId: null,\n    tokenPlacement: null,\n    tokenHeaderPrefix: null,\n    tokenQueryKey: null,\n    tokenSource: 'access_token',\n    refreshTokenUrl: null,\n    autoRefreshToken: false, // Default to false\n    autoFetchToken: true, // Default to true\n    additionalParameters: null\n  };\n\n  switch (oauth.flow) {\n    case 'client_credentials':\n      brunoOAuth.grantType = 'client_credentials';\n      if (oauth.accessTokenUrl) brunoOAuth.accessTokenUrl = oauth.accessTokenUrl;\n      if (oauth.refreshTokenUrl) brunoOAuth.refreshTokenUrl = oauth.refreshTokenUrl;\n      if (oauth.credentials?.clientId) brunoOAuth.clientId = oauth.credentials.clientId;\n      if (oauth.credentials?.clientSecret) brunoOAuth.clientSecret = oauth.credentials.clientSecret;\n      if (oauth.credentials?.placement) brunoOAuth.credentialsPlacement = oauth.credentials.placement;\n      if (oauth.scope) brunoOAuth.scope = oauth.scope;\n\n      // token config\n      if (oauth.tokenConfig?.id) brunoOAuth.credentialsId = oauth.tokenConfig.id;\n      if (oauth.tokenConfig?.source) brunoOAuth.tokenSource = oauth.tokenConfig.source || 'access_token';\n      if (oauth.tokenConfig?.placement) {\n        if ('header' in oauth.tokenConfig.placement) {\n          brunoOAuth.tokenPlacement = 'header';\n          brunoOAuth.tokenHeaderPrefix = oauth.tokenConfig.placement.header || '';\n        } else if ('query' in oauth.tokenConfig.placement) {\n          brunoOAuth.tokenPlacement = 'url';\n          brunoOAuth.tokenQueryKey = oauth.tokenConfig.placement.query || '';\n        }\n      }\n\n      // additional parameters\n      if (oauth.additionalParameters) {\n        const tempParams: Record<string, any> = {};\n        if (oauth.additionalParameters.accessTokenRequest) {\n          const tokenParams = reverseAdditionalParameters(oauth.additionalParameters.accessTokenRequest);\n          if (tokenParams) {\n            tempParams.token = tokenParams;\n          }\n        }\n        if (oauth.additionalParameters.refreshTokenRequest) {\n          const refreshParams = reverseAdditionalParameters(oauth.additionalParameters.refreshTokenRequest);\n          if (refreshParams) {\n            tempParams.refresh = refreshParams;\n          }\n        }\n        // Only set additionalParameters if there are actual parameters\n        if (Object.keys(tempParams).length > 0) {\n          brunoOAuth.additionalParameters = tempParams;\n        }\n      }\n      break;\n\n    case 'resource_owner_password_credentials':\n      brunoOAuth.grantType = 'password';\n      if (oauth.accessTokenUrl) brunoOAuth.accessTokenUrl = oauth.accessTokenUrl;\n      if (oauth.refreshTokenUrl) brunoOAuth.refreshTokenUrl = oauth.refreshTokenUrl;\n      if (oauth.credentials?.clientId) brunoOAuth.clientId = oauth.credentials.clientId;\n      if (oauth.credentials?.clientSecret) brunoOAuth.clientSecret = oauth.credentials.clientSecret;\n      if (oauth.credentials?.placement) brunoOAuth.credentialsPlacement = oauth.credentials.placement;\n      if (oauth.resourceOwner?.username) brunoOAuth.username = oauth.resourceOwner.username;\n      if (oauth.resourceOwner?.password) brunoOAuth.password = oauth.resourceOwner.password;\n      if (oauth.scope) brunoOAuth.scope = oauth.scope;\n\n      // token config\n      if (oauth.tokenConfig?.id) brunoOAuth.credentialsId = oauth.tokenConfig.id;\n      if (oauth.tokenConfig?.source) brunoOAuth.tokenSource = oauth.tokenConfig.source || 'access_token';\n      if (oauth.tokenConfig?.placement) {\n        if ('header' in oauth.tokenConfig.placement) {\n          brunoOAuth.tokenPlacement = 'header';\n          brunoOAuth.tokenHeaderPrefix = oauth.tokenConfig.placement.header || '';\n        } else if ('query' in oauth.tokenConfig.placement) {\n          brunoOAuth.tokenPlacement = 'url';\n          brunoOAuth.tokenQueryKey = oauth.tokenConfig.placement.query || '';\n        }\n      }\n\n      // additional parameters\n      if (oauth.additionalParameters) {\n        const tempParams: Record<string, any> = {};\n        if (oauth.additionalParameters.accessTokenRequest) {\n          const tokenParams = reverseAdditionalParameters(oauth.additionalParameters.accessTokenRequest);\n          if (tokenParams) {\n            tempParams.token = tokenParams;\n          }\n        }\n        if (oauth.additionalParameters.refreshTokenRequest) {\n          const refreshParams = reverseAdditionalParameters(oauth.additionalParameters.refreshTokenRequest);\n          if (refreshParams) {\n            tempParams.refresh = refreshParams;\n          }\n        }\n        // Only set additionalParameters if there are actual parameters\n        if (Object.keys(tempParams).length > 0) {\n          brunoOAuth.additionalParameters = tempParams;\n        }\n      }\n      break;\n\n    case 'authorization_code':\n      brunoOAuth.grantType = 'authorization_code';\n      if (oauth.authorizationUrl) brunoOAuth.authorizationUrl = oauth.authorizationUrl;\n      if (oauth.accessTokenUrl) brunoOAuth.accessTokenUrl = oauth.accessTokenUrl;\n      if (oauth.refreshTokenUrl) brunoOAuth.refreshTokenUrl = oauth.refreshTokenUrl;\n      if (oauth.callbackUrl) brunoOAuth.callbackUrl = oauth.callbackUrl;\n      if (oauth.credentials?.clientId) brunoOAuth.clientId = oauth.credentials.clientId;\n      if (oauth.credentials?.clientSecret) brunoOAuth.clientSecret = oauth.credentials.clientSecret;\n      if (oauth.credentials?.placement) brunoOAuth.credentialsPlacement = oauth.credentials.placement;\n      if (oauth.scope) brunoOAuth.scope = oauth.scope;\n      if (oauth.state) brunoOAuth.state = oauth.state;\n\n      // token config\n      if (oauth.tokenConfig?.id) brunoOAuth.credentialsId = oauth.tokenConfig.id;\n      if (oauth.tokenConfig?.source) brunoOAuth.tokenSource = oauth.tokenConfig.source || 'access_token';\n      if (oauth.tokenConfig?.placement) {\n        if ('header' in oauth.tokenConfig.placement) {\n          brunoOAuth.tokenPlacement = 'header';\n          brunoOAuth.tokenHeaderPrefix = oauth.tokenConfig.placement.header || '';\n        } else if ('query' in oauth.tokenConfig.placement) {\n          brunoOAuth.tokenPlacement = 'url';\n          brunoOAuth.tokenQueryKey = oauth.tokenConfig.placement.query || '';\n        }\n      }\n\n      // additional parameters\n      if (oauth.additionalParameters) {\n        const tempParams: Record<string, any> = {};\n        if (oauth.additionalParameters.authorizationRequest) {\n          const authParams = reverseAdditionalParameters(oauth.additionalParameters.authorizationRequest);\n          if (authParams) {\n            tempParams.authorization = authParams;\n          }\n        }\n        if (oauth.additionalParameters.accessTokenRequest) {\n          const tokenParams = reverseAdditionalParameters(oauth.additionalParameters.accessTokenRequest);\n          if (tokenParams) {\n            tempParams.token = tokenParams;\n          }\n        }\n        if (oauth.additionalParameters.refreshTokenRequest) {\n          const refreshParams = reverseAdditionalParameters(oauth.additionalParameters.refreshTokenRequest);\n          if (refreshParams) {\n            tempParams.refresh = refreshParams;\n          }\n        }\n        // Only set additionalParameters if there are actual parameters\n        if (Object.keys(tempParams).length > 0) {\n          brunoOAuth.additionalParameters = tempParams;\n        }\n      }\n      break;\n\n    case 'implicit':\n      brunoOAuth.grantType = 'implicit';\n      if (oauth.authorizationUrl) brunoOAuth.authorizationUrl = oauth.authorizationUrl;\n      if (oauth.callbackUrl) brunoOAuth.callbackUrl = oauth.callbackUrl;\n      if (oauth.credentials?.clientId) brunoOAuth.clientId = oauth.credentials.clientId;\n      if (oauth.scope) brunoOAuth.scope = oauth.scope;\n      if (oauth.state) brunoOAuth.state = oauth.state;\n\n      // token config\n      if (oauth.tokenConfig?.id) brunoOAuth.credentialsId = oauth.tokenConfig.id;\n      if (oauth.tokenConfig?.source) brunoOAuth.tokenSource = oauth.tokenConfig.source || 'access_token';\n      if (oauth.tokenConfig?.placement) {\n        if ('header' in oauth.tokenConfig.placement) {\n          brunoOAuth.tokenPlacement = 'header';\n          brunoOAuth.tokenHeaderPrefix = oauth.tokenConfig.placement.header || '';\n        } else if ('query' in oauth.tokenConfig.placement) {\n          brunoOAuth.tokenPlacement = 'url';\n          brunoOAuth.tokenQueryKey = oauth.tokenConfig.placement.query || '';\n        }\n      }\n\n      // additional parameters\n      if (oauth.additionalParameters) {\n        const tempParams: Record<string, any> = {};\n        if (oauth.additionalParameters.authorizationRequest) {\n          const authParams = reverseAdditionalParameters(oauth.additionalParameters.authorizationRequest);\n          if (authParams) {\n            tempParams.authorization = authParams;\n          }\n        }\n        // Only set additionalParameters if there are actual parameters\n        if (Object.keys(tempParams).length > 0) {\n          brunoOAuth.additionalParameters = tempParams;\n        }\n      }\n      break;\n\n    default:\n      return null;\n  }\n\n  if (oauth.settings?.autoFetchToken !== undefined) {\n    brunoOAuth.autoFetchToken = oauth.settings.autoFetchToken;\n  }\n  if (oauth.settings?.autoRefreshToken !== undefined) {\n    brunoOAuth.autoRefreshToken = oauth.settings.autoRefreshToken;\n  }\n\n  if (brunoOAuth.grantType === 'authorization_code' && oauth.flow === 'authorization_code') {\n    const authCodeFlow = oauth as OAuth2AuthorizationCodeFlow;\n    if (authCodeFlow.pkce !== undefined) {\n      // If pkce.disabled is true, set pkce to false; otherwise set to true\n      brunoOAuth.pkce = !authCodeFlow.pkce.disabled;\n    }\n  }\n\n  if (brunoOAuth.additionalParameters === null) {\n    delete brunoOAuth.additionalParameters;\n  }\n\n  return brunoOAuth;\n};\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/common/auth.ts",
    "content": "import type {\n  Auth,\n  AuthApiKey,\n  AuthAwsV4,\n  AuthBasic,\n  AuthBearer,\n  AuthDigest,\n  AuthNTLM,\n  AuthWsse\n} from '@opencollection/types/common/auth';\nimport type { Auth as BrunoAuth } from '@usebruno/schema-types/common/auth';\nimport { isString } from '../../../utils';\nimport { toOpenCollectionOAuth2, toBrunoOAuth2 } from './auth-oauth2';\n\nconst buildAwsV4Auth = (config?: BrunoAuth['awsv4']): AuthAwsV4 => {\n  const auth: AuthAwsV4 = { type: 'awsv4' };\n\n  if (!config) {\n    return auth;\n  }\n\n  if (isString(config.accessKeyId)) auth.accessKeyId = config.accessKeyId;\n  if (isString(config.secretAccessKey)) auth.secretAccessKey = config.secretAccessKey;\n  if (isString(config.sessionToken)) auth.sessionToken = config.sessionToken;\n  if (isString(config.service)) auth.service = config.service;\n  if (isString(config.region)) auth.region = config.region;\n  if (isString(config.profileName)) auth.profileName = config.profileName;\n\n  return auth;\n};\n\nconst buildBasicAuth = (config?: BrunoAuth['basic']): AuthBasic => {\n  const auth: AuthBasic = { type: 'basic' };\n\n  if (!config) {\n    return auth;\n  }\n\n  if (isString(config.username)) auth.username = config.username;\n  if (isString(config.password)) auth.password = config.password;\n\n  return auth;\n};\n\nconst buildBearerAuth = (config?: BrunoAuth['bearer']): AuthBearer => {\n  const auth: AuthBearer = { type: 'bearer' };\n\n  if (!config) {\n    return auth;\n  }\n\n  if (isString(config.token)) auth.token = config.token;\n\n  return auth;\n};\n\nconst buildDigestAuth = (config?: BrunoAuth['digest']): AuthDigest => {\n  const auth: AuthDigest = { type: 'digest' };\n\n  if (!config) {\n    return auth;\n  }\n\n  if (isString(config.username)) auth.username = config.username;\n  if (isString(config.password)) auth.password = config.password;\n\n  return auth;\n};\n\nconst buildNtlmAuth = (config?: BrunoAuth['ntlm']): AuthNTLM => {\n  const auth: AuthNTLM = { type: 'ntlm' };\n\n  if (!config) {\n    return auth;\n  }\n\n  if (isString(config.username)) auth.username = config.username;\n  if (isString(config.password)) auth.password = config.password;\n  if (isString(config.domain)) auth.domain = config.domain;\n\n  return auth;\n};\n\nconst buildWsseAuth = (config?: BrunoAuth['wsse']): AuthWsse => {\n  const auth: AuthWsse = { type: 'wsse' };\n\n  if (!config) {\n    return auth;\n  }\n\n  if (isString(config.username)) auth.username = config.username;\n  if (isString(config.password)) auth.password = config.password;\n\n  return auth;\n};\n\nconst buildApiKeyAuth = (config?: BrunoAuth['apikey']): AuthApiKey => {\n  const auth: AuthApiKey = { type: 'apikey' };\n\n  if (!config) {\n    return auth;\n  }\n\n  if (isString(config.key)) auth.key = config.key;\n  if (isString(config.value)) auth.value = config.value;\n\n  if (isString(config.placement)) {\n    if (config.placement === 'header') {\n      auth.placement = 'header';\n    } else if (config.placement === 'queryparams') {\n      auth.placement = 'query';\n    }\n  }\n\n  return auth;\n};\n\nexport const toOpenCollectionAuth = (auth?: BrunoAuth | null): Auth | undefined => {\n  if (!auth || auth.mode === 'none') {\n    return undefined;\n  }\n\n  if (auth.mode === 'inherit') {\n    return 'inherit';\n  }\n\n  switch (auth.mode) {\n    case 'awsv4':\n      return buildAwsV4Auth(auth.awsv4);\n    case 'basic':\n      return buildBasicAuth(auth.basic);\n    case 'bearer':\n      return buildBearerAuth(auth.bearer);\n    case 'digest':\n      return buildDigestAuth(auth.digest);\n    case 'ntlm':\n      return buildNtlmAuth(auth.ntlm);\n    case 'wsse':\n      return buildWsseAuth(auth.wsse);\n    case 'apikey':\n      return buildApiKeyAuth(auth.apikey);\n    case 'oauth2':\n      return toOpenCollectionOAuth2(auth.oauth2);\n    default:\n      console.warn(`toOpenCollectionAuth failed: Unsupported auth mode \"${auth.mode}\".`);\n      return undefined;\n  }\n};\n\nexport const toBrunoAuth = (auth: Auth | null | undefined): BrunoAuth | null => {\n  const brunoAuth: BrunoAuth = {\n    mode: 'none',\n    awsv4: null,\n    basic: null,\n    bearer: null,\n    digest: null,\n    ntlm: null,\n    oauth2: null,\n    wsse: null,\n    apikey: null\n  };\n\n  if (!auth) {\n    return brunoAuth;\n  }\n\n  if (auth === 'inherit') {\n    brunoAuth.mode = 'inherit';\n    return brunoAuth;\n  }\n\n  switch (auth.type) {\n    case 'awsv4':\n      brunoAuth.mode = 'awsv4';\n      brunoAuth.awsv4 = {\n        accessKeyId: auth.accessKeyId || null,\n        secretAccessKey: auth.secretAccessKey || null,\n        sessionToken: auth.sessionToken || null,\n        service: auth.service || null,\n        region: auth.region || null,\n        profileName: auth.profileName || null\n      };\n      break;\n\n    case 'basic':\n      brunoAuth.mode = 'basic';\n      brunoAuth.basic = {\n        username: auth.username || null,\n        password: auth.password || null\n      };\n      break;\n\n    case 'bearer':\n      brunoAuth.mode = 'bearer';\n      brunoAuth.bearer = {\n        token: auth.token || ''\n      };\n      break;\n\n    case 'digest':\n      brunoAuth.mode = 'digest';\n      brunoAuth.digest = {\n        username: auth.username || null,\n        password: auth.password || null\n      };\n      break;\n\n    case 'ntlm':\n      brunoAuth.mode = 'ntlm';\n      brunoAuth.ntlm = {\n        username: auth.username || null,\n        password: auth.password || null,\n        domain: auth.domain || null\n      };\n      break;\n\n    case 'wsse':\n      brunoAuth.mode = 'wsse';\n      brunoAuth.wsse = {\n        username: auth.username || null,\n        password: auth.password || null\n      };\n      break;\n\n    case 'apikey':\n      brunoAuth.mode = 'apikey';\n      brunoAuth.apikey = {\n        key: auth.key || null,\n        value: auth.value || null,\n        placement: auth.placement === 'query' ? 'queryparams' : (auth.placement === 'header' ? 'header' : null)\n      };\n      break;\n\n    case 'oauth2':\n      brunoAuth.mode = 'oauth2';\n      brunoAuth.oauth2 = toBrunoOAuth2(auth);\n      break;\n\n    default:\n      console.warn('toBrunoAuth failed: Unsupported auth type');\n      break;\n  }\n\n  return brunoAuth;\n};\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/common/body.ts",
    "content": "import type { HttpRequestBody as BrunoHttpRequestBody } from '@usebruno/schema-types/requests/http';\nimport type {\n  HttpRequestBody,\n  RawBody,\n  FormUrlEncodedBody,\n  FormUrlEncodedEntry,\n  MultipartFormBody,\n  MultipartFormEntry,\n  FileBody,\n  FileBodyEntry\n} from '@opencollection/types/requests/http';\nimport type { KeyValue as BrunoKeyValue } from '@usebruno/schema-types/common/key-value';\nimport { uuid, ensureString } from '../../../utils';\n\nexport const toOpenCollectionBody = (body: BrunoHttpRequestBody | null | undefined): HttpRequestBody | undefined => {\n  if (!body) {\n    return undefined;\n  }\n\n  switch (body.mode) {\n    case 'none':\n      return undefined;\n\n    case 'json':\n      const rawBody: RawBody = {\n        type: 'json',\n        data: body.json || ''\n      };\n      return rawBody;\n\n    case 'text':\n      const textBody: RawBody = {\n        type: 'text',\n        data: body.text || ''\n      };\n      return textBody;\n\n    case 'xml':\n      const xmlBody: RawBody = {\n        type: 'xml',\n        data: body.xml || ''\n      };\n      return xmlBody;\n\n    case 'sparql':\n      const sparqlBody: RawBody = {\n        type: 'sparql',\n        data: body.sparql || ''\n      };\n      return sparqlBody;\n\n    case 'formUrlEncoded':\n      const formEntries: FormUrlEncodedEntry[] = body.formUrlEncoded?.map((entry: BrunoKeyValue): FormUrlEncodedEntry => {\n        const formEntry: FormUrlEncodedEntry = {\n          name: entry.name || '',\n          value: entry.value || ''\n        };\n\n        if (entry?.description?.trim().length) {\n          formEntry.description = entry.description;\n        }\n\n        if (entry.enabled === false) {\n          formEntry.disabled = true;\n        }\n\n        return formEntry;\n      }) || [];\n\n      const formBody: FormUrlEncodedBody = {\n        type: 'form-urlencoded',\n        ...(formEntries.length > 0 && { data: formEntries })\n      } as FormUrlEncodedBody;\n      return formBody;\n\n    case 'multipartForm':\n      const multipartEntries: MultipartFormEntry[] = body.multipartForm?.map((entry): MultipartFormEntry => {\n        const multipartEntry: MultipartFormEntry = {\n          name: entry.name || '',\n          type: entry.type,\n          value: entry.value || (entry.type === 'file' ? [] : '')\n        };\n\n        if (entry?.contentType?.trim().length) {\n          multipartEntry.contentType = entry.contentType;\n        }\n\n        if (entry?.description?.trim().length) {\n          multipartEntry.description = entry.description;\n        }\n\n        if (entry.enabled === false) {\n          multipartEntry.disabled = true;\n        }\n\n        return multipartEntry;\n      }) || [];\n\n      const multipartBody: MultipartFormBody = {\n        type: 'multipart-form',\n        ...(multipartEntries.length > 0 && { data: multipartEntries })\n      } as MultipartFormBody;\n      return multipartBody;\n\n    case 'file':\n      const fileEntries: FileBodyEntry[] = body.file?.map((file): FileBodyEntry => {\n        return {\n          filePath: file.filePath || '',\n          contentType: file.contentType || '',\n          selected: file.selected ?? false\n        };\n      }) || [];\n\n      const fileBody: FileBody = {\n        type: 'file',\n        ...(fileEntries.length > 0 && { data: fileEntries })\n      } as FileBody;\n      return fileBody;\n\n    case 'graphql':\n      // GraphQL body is handled separately in GraphQL request stringify\n      return undefined;\n\n    default:\n      return undefined;\n  }\n};\n\nexport const toBrunoBody = (body: HttpRequestBody | null | undefined): BrunoHttpRequestBody | undefined => {\n  if (!body) {\n    return {\n      mode: 'none',\n      json: null,\n      text: null,\n      xml: null,\n      sparql: null,\n      formUrlEncoded: [],\n      multipartForm: [],\n      graphql: null,\n      file: []\n    };\n  }\n\n  const brunoBody: BrunoHttpRequestBody = {\n    mode: 'none',\n    json: null,\n    text: null,\n    xml: null,\n    sparql: null,\n    formUrlEncoded: [],\n    multipartForm: [],\n    graphql: null,\n    file: []\n  };\n\n  switch (body.type) {\n    case 'json':\n      brunoBody.mode = 'json';\n      brunoBody.json = body.data || '';\n      break;\n\n    case 'text':\n      brunoBody.mode = 'text';\n      brunoBody.text = body.data || '';\n      break;\n\n    case 'xml':\n      brunoBody.mode = 'xml';\n      brunoBody.xml = body.data || '';\n      break;\n\n    case 'sparql':\n      brunoBody.mode = 'sparql';\n      brunoBody.sparql = body.data || '';\n      break;\n\n    case 'form-urlencoded':\n      brunoBody.mode = 'formUrlEncoded';\n      brunoBody.formUrlEncoded = body.data?.map((entry): BrunoKeyValue => {\n        const formEntry: BrunoKeyValue = {\n          uid: uuid(),\n          name: ensureString(entry.name),\n          value: ensureString(entry.value),\n          enabled: entry.disabled !== true\n        };\n\n        if (entry.description) {\n          if (typeof entry.description === 'string' && entry.description.trim().length) {\n            formEntry.description = entry.description;\n          } else if (typeof entry.description === 'object' && entry.description.content?.trim().length) {\n            formEntry.description = entry.description.content;\n          }\n        }\n\n        return formEntry;\n      }) || [];\n      break;\n\n    case 'multipart-form':\n      brunoBody.mode = 'multipartForm';\n      brunoBody.multipartForm = body.data?.map((entry): any => {\n        const multipartEntry: any = {\n          uid: uuid(),\n          type: entry.type,\n          name: ensureString(entry.name),\n          value: entry.type === 'file' ? (entry.value || []) : ensureString(entry.value),\n          contentType: entry.contentType || null,\n          enabled: entry.disabled !== true\n        };\n\n        if (entry.description) {\n          if (typeof entry.description === 'string' && entry.description.trim().length) {\n            multipartEntry.description = entry.description;\n          } else if (typeof entry.description === 'object' && entry.description.content?.trim().length) {\n            multipartEntry.description = entry.description.content;\n          }\n        }\n\n        return multipartEntry;\n      }) || [];\n      break;\n\n    case 'file':\n      brunoBody.mode = 'file';\n      brunoBody.file = body.data?.map((file): any => ({\n        uid: uuid(),\n        filePath: file.filePath || '',\n        contentType: file.contentType || '',\n        selected: file.selected ?? false\n      })) || [];\n      break;\n\n    default:\n      break;\n  }\n\n  return brunoBody;\n};\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/common/headers.ts",
    "content": "import type { FolderRequest as BrunoFolderRequest } from '@usebruno/schema-types/collection/folder';\nimport type { KeyValue as BrunoKeyValue } from '@usebruno/schema-types/common/key-value';\nimport type { HttpRequestHeader, HttpResponseHeader } from '@opencollection/types/requests/http';\nimport { uuid, ensureString } from '../../../utils';\n\nexport const toOpenCollectionHttpHeaders = (headers: BrunoFolderRequest['headers']): HttpRequestHeader[] | undefined => {\n  if (!headers?.length) {\n    return undefined;\n  }\n\n  const ocHeaders = headers.map((header: BrunoKeyValue): HttpRequestHeader => {\n    const httpHeader: HttpRequestHeader = {\n      name: header.name || '',\n      value: header.value || ''\n    };\n    if (header?.description?.trim().length) {\n      httpHeader.description = header.description;\n    }\n    if (header.enabled === false) {\n      httpHeader.disabled = true;\n    }\n    return httpHeader;\n  });\n\n  return ocHeaders.length ? ocHeaders : undefined;\n};\n\nexport const toOpenCollectionResponseHeaders = (headers: BrunoFolderRequest['headers']): HttpResponseHeader[] | undefined => {\n  if (!headers?.length) {\n    return undefined;\n  }\n\n  const ocHeaders = headers.map((header: BrunoKeyValue): HttpResponseHeader => ({\n    name: header.name || '',\n    value: header.value || ''\n  }));\n\n  return ocHeaders.length ? ocHeaders : undefined;\n};\n\nexport const toBrunoHttpHeaders = (headers: HttpRequestHeader[] | HttpResponseHeader[] | null | undefined): BrunoKeyValue[] | undefined => {\n  if (!headers?.length) {\n    return undefined;\n  }\n\n  const brunoHeaders = headers.map((header): BrunoKeyValue => {\n    const brunoHeader: BrunoKeyValue = {\n      uid: uuid(),\n      name: ensureString(header.name),\n      value: ensureString(header.value),\n      enabled: ('disabled' in header) ? header.disabled !== true : true\n    };\n\n    return brunoHeader;\n  });\n\n  return brunoHeaders.length ? brunoHeaders : undefined;\n};\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/common/params.ts",
    "content": "import type { HttpRequestParam as BrunoHttpRequestParam } from '@usebruno/schema-types/requests/http';\nimport type { HttpRequestParam } from '@opencollection/types/requests/http';\nimport { uuid, ensureString } from '../../../utils';\n\nexport const toOpenCollectionParams = (params: BrunoHttpRequestParam[] | null | undefined): HttpRequestParam[] | undefined => {\n  if (!params?.length) {\n    return undefined;\n  }\n\n  const ocParams = params.map((param: BrunoHttpRequestParam): HttpRequestParam => {\n    const ocParam: HttpRequestParam = {\n      name: param.name || '',\n      value: param.value || '',\n      type: param.type\n    };\n\n    if (param?.description?.trim().length) {\n      ocParam.description = param.description;\n    }\n\n    if (param.enabled === false) {\n      ocParam.disabled = true;\n    }\n\n    return ocParam;\n  });\n\n  return ocParams.length ? ocParams : undefined;\n};\n\nexport const toBrunoParams = (params: HttpRequestParam[] | null | undefined): BrunoHttpRequestParam[] | undefined => {\n  if (!params?.length) {\n    return undefined;\n  }\n\n  const brunoParams = params.map((param: HttpRequestParam): BrunoHttpRequestParam => {\n    const brunoParam: BrunoHttpRequestParam = {\n      uid: uuid(),\n      name: ensureString(param.name),\n      value: ensureString(param.value),\n      type: param.type,\n      enabled: param.disabled !== true\n    };\n\n    if (param.description) {\n      if (typeof param.description === 'string' && param.description.trim().length) {\n        brunoParam.description = param.description;\n      } else if (typeof param.description === 'object' && param.description.content?.trim().length) {\n        brunoParam.description = param.description.content;\n      }\n    }\n\n    return brunoParam;\n  });\n\n  return brunoParams.length ? brunoParams : undefined;\n};\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/common/scripts.ts",
    "content": "import type { Scripts, Script } from '@opencollection/types/common/scripts';\nimport type { FolderRequest as BrunoFolderRequest } from '@usebruno/schema-types/collection/folder';\nimport type { HttpRequest as BrunoHttpRequest } from '@usebruno/schema-types/requests/http';\nimport type { WebSocketRequest as BrunoWebSocketRequest } from '@usebruno/schema-types/requests/websocket';\nimport type { GrpcRequest as BrunoGrpcRequest } from '@usebruno/schema-types/requests/grpc';\n\nexport const toOpenCollectionScripts = (request: BrunoFolderRequest | BrunoHttpRequest | BrunoWebSocketRequest | BrunoGrpcRequest | null | undefined): Scripts | undefined => {\n  const ocScripts: Scripts = [];\n\n  if (request?.script?.req?.trim().length) {\n    ocScripts.push({\n      type: 'before-request',\n      code: request.script.req.trim()\n    });\n  }\n  if (request?.script?.res?.trim().length) {\n    ocScripts.push({\n      type: 'after-response',\n      code: request.script.res.trim()\n    });\n  }\n  if (request?.tests?.trim().length) {\n    ocScripts.push({\n      type: 'tests',\n      code: request.tests.trim()\n    });\n  }\n\n  return ocScripts.length > 0 ? ocScripts : undefined;\n};\n\nexport const toBrunoScripts = (scripts: Scripts | null | undefined): {\n  script?: { req?: string; res?: string };\n  tests?: string;\n} | undefined => {\n  if (!scripts || !Array.isArray(scripts) || scripts.length === 0) {\n    return undefined;\n  }\n\n  const brunoScripts: {\n    script?: { req?: string; res?: string };\n    tests?: string;\n  } = {};\n\n  for (const script of scripts) {\n    if (script.type === 'before-request' && script.code) {\n      if (!brunoScripts.script) {\n        brunoScripts.script = {};\n      }\n      brunoScripts.script.req = script.code;\n    }\n    if (script.type === 'after-response' && script.code) {\n      if (!brunoScripts.script) {\n        brunoScripts.script = {};\n      }\n      brunoScripts.script.res = script.code;\n    }\n    if (script.type === 'tests' && script.code) {\n      brunoScripts.tests = script.code;\n    }\n  }\n\n  return Object.keys(brunoScripts).length > 0 ? brunoScripts : undefined;\n};\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/common/variables.ts",
    "content": "import { Variable } from '@opencollection/types/common/variables';\nimport { FolderRequest as BrunoFolderRequest } from '@usebruno/schema-types/collection/folder';\nimport { Variable as BrunoVariable, Variables as BrunoVariables } from '@usebruno/schema-types/common/variables';\nimport { uuid, ensureString } from '../../../utils';\n\n/**\n * Convert Bruno pre-request variables to OpenCollection variables format.\n * Note: Post-response variables are now converted to actions (see actions.ts).\n */\nexport const toOpenCollectionVariables = (variables: BrunoFolderRequest['vars'] | BrunoVariables | null | undefined): Variable[] | undefined => {\n  // Handle folder variables (has req/res structure) - only use req vars\n  const hasReqRes = variables && 'req' in variables;\n  const reqVars = hasReqRes ? variables.req : variables as BrunoVariables;\n\n  const reqVarsArray = Array.isArray(reqVars) ? reqVars : [];\n\n  if (!reqVarsArray.length) {\n    return undefined;\n  }\n\n  const ocVariables: Variable[] = reqVarsArray.map((v: BrunoVariable): Variable => {\n    const variable: Variable = {\n      name: v.name || '',\n      value: v.value || ''\n    };\n\n    if (v?.description?.trim().length) {\n      variable.description = v.description;\n    }\n\n    if (v.enabled === false) {\n      variable.disabled = true;\n    }\n    return variable;\n  });\n\n  return ocVariables.length > 0 ? ocVariables : undefined;\n};\n\n/**\n * Convert OpenCollection variables to Bruno pre-request variables format.\n * Note: Post-response variables come from actions (see actions.ts).\n */\nexport const toBrunoVariables = (variables: Variable[] | null | undefined): { req: BrunoVariables; res: BrunoVariables } => {\n  if (!variables?.length) {\n    return { req: [], res: [] };\n  }\n\n  const reqVars: BrunoVariables = [];\n\n  variables.forEach((v: Variable) => {\n    const variable: BrunoVariable = {\n      uid: uuid(),\n      name: ensureString(v.name),\n      value: ensureString(v.value),\n      enabled: v.disabled !== true,\n      local: false\n    };\n\n    if (v.description) {\n      variable.description = typeof v.description === 'string' ? v.description : (v.description as any)?.content || '';\n    }\n\n    reqVars.push(variable);\n  });\n\n  return { req: reqVars, res: [] };\n};\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/index.ts",
    "content": "import parseYmlItem from './parseItem';\nimport parseYmlFolder from './parseFolder';\nimport parseYmlCollection from './parseCollection';\nimport parseYmlEnvironment from './parseEnvironment';\n\nimport stringifyYmlItem from './stringifyItem';\nimport stringifyYmlFolder from './stringifyFolder';\nimport stringifyYmlCollection from './stringifyCollection';\nimport stringifyYmlEnvironment from './stringifyEnvironment';\n\nexport {\n  parseYmlItem,\n  parseYmlFolder,\n  parseYmlCollection,\n  parseYmlEnvironment,\n  stringifyYmlItem,\n  stringifyYmlFolder,\n  stringifyYmlCollection,\n  stringifyYmlEnvironment\n};\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/items/parseGraphQLRequest.ts",
    "content": "import type { Item as BrunoItem, HttpItemSettings as BrunoHttpItemSettings } from '@usebruno/schema-types/collection/item';\nimport type { HttpRequest as BrunoHttpRequest } from '@usebruno/schema-types/requests/http';\nimport type { GraphQLRequest, GraphQLRequestSettings, GraphQLBody } from '@opencollection/types/requests/graphql';\nimport { toBrunoAuth } from '../common/auth';\nimport { toBrunoHttpHeaders } from '../common/headers';\nimport { toBrunoParams } from '../common/params';\nimport { toBrunoVariables } from '../common/variables';\nimport { toBrunoPostResponseVariables } from '../common/actions';\nimport { toBrunoScripts } from '../common/scripts';\nimport { toBrunoAssertions } from '../common/assertions';\nimport { uuid, ensureString } from '../../../utils';\n\nconst parseGraphQLRequest = (ocRequest: GraphQLRequest): BrunoItem => {\n  const info = ocRequest.info;\n  const graphql = ocRequest.graphql;\n  const runtime = ocRequest.runtime;\n\n  const brunoRequest: BrunoHttpRequest = {\n    url: ensureString(graphql?.url),\n    method: ensureString(graphql?.method, 'POST'),\n    headers: toBrunoHttpHeaders(graphql?.headers) || [],\n    params: toBrunoParams(graphql?.params) || [],\n    auth: toBrunoAuth(graphql?.auth),\n    body: {\n      mode: 'graphql',\n      json: null,\n      text: null,\n      xml: null,\n      sparql: null,\n      formUrlEncoded: [],\n      multipartForm: [],\n      graphql: {\n        query: (graphql?.body as GraphQLBody)?.query || '',\n        variables: (graphql?.body as GraphQLBody)?.variables || ''\n      },\n      file: []\n    },\n    script: {\n      req: null,\n      res: null\n    },\n    vars: {\n      req: [],\n      res: []\n    },\n    assertions: [],\n    tests: null,\n    docs: null\n  };\n\n  // scripts\n  const scripts = toBrunoScripts(runtime?.scripts);\n  if (scripts?.script && brunoRequest.script) {\n    if (scripts.script.req) {\n      brunoRequest.script.req = scripts.script.req;\n    }\n    if (scripts.script.res) {\n      brunoRequest.script.res = scripts.script.res;\n    }\n  }\n  if (scripts?.tests) {\n    brunoRequest.tests = scripts.tests;\n  }\n\n  // variables (pre-request from variables, post-response from actions)\n  const variables = toBrunoVariables(runtime?.variables);\n  const postResponseVars = toBrunoPostResponseVariables(runtime?.actions);\n  brunoRequest.vars = {\n    req: variables.req,\n    res: postResponseVars\n  };\n\n  // assertions\n  const assertions = toBrunoAssertions(runtime?.assertions);\n  if (assertions) {\n    brunoRequest.assertions = assertions;\n  }\n\n  // docs\n  if (ocRequest.docs) {\n    brunoRequest.docs = ocRequest.docs;\n  }\n\n  // bruno item\n  const brunoItem: BrunoItem = {\n    uid: uuid(),\n    type: 'graphql-request',\n    seq: info?.seq || 1,\n    name: ensureString(info?.name, 'Untitled Request'),\n    tags: info?.tags || [],\n    request: brunoRequest,\n    settings: null,\n    fileContent: null,\n    root: null,\n    items: [],\n    examples: [],\n    filename: null,\n    pathname: null\n  };\n\n  // settings\n  if (ocRequest.settings) {\n    const settings: BrunoHttpItemSettings = {};\n\n    if (typeof ocRequest.settings.encodeUrl === 'boolean') {\n      settings.encodeUrl = ocRequest.settings.encodeUrl;\n    } else {\n      settings.encodeUrl = true;\n    }\n\n    if (typeof ocRequest.settings.timeout === 'number') {\n      settings.timeout = ocRequest.settings.timeout;\n    } else if (ocRequest.settings.timeout === 'inherit') {\n      settings.timeout = 'inherit';\n    } else {\n      settings.timeout = 0;\n    }\n\n    if (typeof ocRequest.settings.followRedirects === 'boolean') {\n      settings.followRedirects = ocRequest.settings.followRedirects;\n    } else {\n      settings.followRedirects = true;\n    }\n\n    if (typeof ocRequest.settings.maxRedirects === 'number') {\n      settings.maxRedirects = ocRequest.settings.maxRedirects;\n    } else {\n      settings.maxRedirects = 5;\n    }\n\n    brunoItem.settings = settings;\n  }\n\n  return brunoItem;\n};\n\nexport default parseGraphQLRequest;\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/items/parseGrpcRequest.ts",
    "content": "import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';\nimport type { GrpcRequest as BrunoGrpcRequest } from '@usebruno/schema-types/requests/grpc';\nimport type { GrpcRequest, GrpcMetadata } from '@opencollection/types/requests/grpc';\nimport type { KeyValue as BrunoKeyValue } from '@usebruno/schema-types/common/key-value';\nimport { toBrunoAuth } from '../common/auth';\nimport { toBrunoVariables } from '../common/variables';\nimport { toBrunoScripts } from '../common/scripts';\nimport { toBrunoAssertions } from '../common/assertions';\nimport { isNonEmptyString, uuid, ensureString } from '../../../utils';\n\nconst toBrunoGrpcMetadata = (metadata: GrpcMetadata[] | null | undefined): BrunoKeyValue[] | undefined => {\n  if (!metadata?.length) {\n    return undefined;\n  }\n\n  const brunoMetadata = metadata.map((meta: GrpcMetadata): BrunoKeyValue => {\n    const brunoMeta: BrunoKeyValue = {\n      uid: uuid(),\n      name: ensureString(meta.name),\n      value: ensureString(meta.value),\n      enabled: meta.disabled !== true\n    };\n\n    return brunoMeta;\n  });\n\n  return brunoMetadata.length ? brunoMetadata : undefined;\n};\n\nconst parseGrpcRequest = (ocRequest: GrpcRequest): BrunoItem => {\n  const info = ocRequest.info;\n  const grpc = ocRequest.grpc;\n  const runtime = ocRequest.runtime;\n\n  const brunoRequest: BrunoGrpcRequest = {\n    url: ensureString(grpc?.url),\n    method: ensureString(grpc?.method),\n    methodType: grpc?.methodType || '',\n    protoPath: grpc?.protoFilePath || null,\n    headers: toBrunoGrpcMetadata(grpc?.metadata) || [],\n    auth: toBrunoAuth(grpc?.auth),\n    body: {\n      mode: 'grpc',\n      grpc: []\n    },\n    script: {\n      req: null,\n      res: null\n    },\n    vars: {\n      req: [],\n      res: []\n    },\n    assertions: [],\n    tests: null,\n    docs: null\n  };\n\n  // message\n  if (isNonEmptyString(grpc?.message)) {\n    brunoRequest.body.grpc = [{\n      name: '',\n      content: grpc?.message as string\n    }];\n  }\n\n  // scripts\n  const scripts = toBrunoScripts(runtime?.scripts);\n  if (scripts?.script && brunoRequest.script) {\n    if (scripts.script.req) {\n      brunoRequest.script.req = scripts.script.req;\n    }\n    if (scripts.script.res) {\n      brunoRequest.script.res = scripts.script.res;\n    }\n  }\n  if (scripts?.tests) {\n    brunoRequest.tests = scripts.tests;\n  }\n\n  // variables\n  const variables = toBrunoVariables(runtime?.variables);\n  brunoRequest.vars = variables;\n\n  // assertions\n  const assertions = toBrunoAssertions(runtime?.assertions);\n  if (assertions) {\n    brunoRequest.assertions = assertions;\n  }\n\n  // docs\n  if (ocRequest.docs) {\n    brunoRequest.docs = ocRequest.docs;\n  }\n\n  // bruno item\n  const brunoItem: BrunoItem = {\n    uid: uuid(),\n    type: 'grpc-request',\n    seq: info?.seq || 1,\n    name: ensureString(info?.name, 'Untitled Request'),\n    tags: info?.tags || [],\n    request: brunoRequest,\n    settings: {},\n    fileContent: null,\n    root: null,\n    items: [],\n    examples: [],\n    filename: null,\n    pathname: null\n  };\n\n  return brunoItem;\n};\n\nexport default parseGrpcRequest;\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/items/parseHttpRequest.ts",
    "content": "import type { Item as BrunoItem, HttpItemSettings as BrunoHttpItemSettings } from '@usebruno/schema-types/collection/item';\nimport type { HttpRequest as BrunoHttpRequest } from '@usebruno/schema-types/requests/http';\nimport type { HttpRequest, HttpRequestBody } from '@opencollection/types/requests/http';\nimport { toBrunoAuth } from '../common/auth';\nimport { toBrunoHttpHeaders } from '../common/headers';\nimport { toBrunoParams } from '../common/params';\nimport { toBrunoBody } from '../common/body';\nimport { toBrunoVariables } from '../common/variables';\nimport { toBrunoPostResponseVariables } from '../common/actions';\nimport { toBrunoScripts } from '../common/scripts';\nimport { toBrunoAssertions } from '../common/assertions';\nimport { uuid, ensureString } from '../../../utils';\n\nconst parseHttpRequest = (ocRequest: HttpRequest): BrunoItem => {\n  const info = ocRequest.info;\n  const http = ocRequest.http;\n  const runtime = ocRequest.runtime;\n\n  const brunoRequest: BrunoHttpRequest = {\n    url: ensureString(http?.url),\n    method: ensureString(http?.method, 'GET'),\n    headers: toBrunoHttpHeaders(http?.headers) || [],\n    params: toBrunoParams(http?.params) || [],\n    auth: toBrunoAuth(http?.auth),\n    body: toBrunoBody(http?.body as HttpRequestBody) || {\n      mode: 'none',\n      json: null,\n      text: null,\n      xml: null,\n      sparql: null,\n      formUrlEncoded: [],\n      multipartForm: [],\n      graphql: null,\n      file: []\n    },\n    script: {\n      req: null,\n      res: null\n    },\n    vars: {\n      req: [],\n      res: []\n    },\n    assertions: [],\n    tests: null,\n    docs: null\n  };\n\n  // scripts\n  const scripts = toBrunoScripts(runtime?.scripts);\n  if (scripts?.script && brunoRequest.script) {\n    if (scripts.script.req) {\n      brunoRequest.script.req = scripts.script.req;\n    }\n    if (scripts.script.res) {\n      brunoRequest.script.res = scripts.script.res;\n    }\n  }\n  if (scripts?.tests) {\n    brunoRequest.tests = scripts.tests;\n  }\n\n  // variables (pre-request from variables, post-response from actions)\n  const variables = toBrunoVariables(runtime?.variables);\n  const postResponseVars = toBrunoPostResponseVariables(runtime?.actions);\n  brunoRequest.vars = {\n    req: variables.req,\n    res: postResponseVars\n  };\n\n  // assertions\n  const assertions = toBrunoAssertions(runtime?.assertions);\n  if (assertions) {\n    brunoRequest.assertions = assertions;\n  }\n\n  // docs\n  if (ocRequest.docs) {\n    brunoRequest.docs = ocRequest.docs;\n  }\n\n  // bruno item\n  const brunoItem: BrunoItem = {\n    uid: uuid(),\n    type: 'http-request',\n    seq: info?.seq || 1,\n    name: ensureString(info?.name, 'Untitled Request'),\n    tags: info?.tags || [],\n    request: brunoRequest,\n    settings: null,\n    fileContent: null,\n    root: null,\n    items: [],\n    examples: [],\n    filename: null,\n    pathname: null\n  };\n\n  // settings\n  if (ocRequest.settings) {\n    const settings: BrunoHttpItemSettings = {};\n\n    if (typeof ocRequest.settings.encodeUrl === 'boolean') {\n      settings.encodeUrl = ocRequest.settings.encodeUrl;\n    } else {\n      settings.encodeUrl = true;\n    }\n\n    if (typeof ocRequest.settings.timeout === 'number') {\n      settings.timeout = ocRequest.settings.timeout;\n    } else if (ocRequest.settings.timeout === 'inherit') {\n      settings.timeout = 'inherit';\n    } else {\n      settings.timeout = 0;\n    }\n\n    if (typeof ocRequest.settings.followRedirects === 'boolean') {\n      settings.followRedirects = ocRequest.settings.followRedirects;\n    } else {\n      settings.followRedirects = true;\n    }\n\n    if (typeof ocRequest.settings.maxRedirects === 'number') {\n      settings.maxRedirects = ocRequest.settings.maxRedirects;\n    } else {\n      settings.maxRedirects = 5;\n    }\n\n    brunoItem.settings = settings;\n  }\n\n  // examples\n  if (ocRequest.examples?.length) {\n    brunoItem.examples = ocRequest.examples.map((example) => {\n      const brunoExample: any = {\n        uid: uuid(),\n        itemUid: uuid(),\n        name: ensureString(example.name, 'Untitled Example'),\n        type: 'http-request',\n        request: null,\n        response: null\n      };\n\n      if (example.description) {\n        if (typeof example.description === 'string' && example.description.trim().length) {\n          brunoExample.description = example.description;\n        } else if (typeof example.description === 'object' && example.description.content?.trim().length) {\n          brunoExample.description = example.description.content;\n        }\n      }\n\n      if (example.request) {\n        brunoExample.request = {\n          url: ensureString(example.request.url),\n          method: ensureString(example.request.method, 'GET'),\n          headers: toBrunoHttpHeaders(example.request.headers) || [],\n          params: toBrunoParams(example.request.params) || [],\n          body: toBrunoBody(example.request.body) || {\n            mode: 'none',\n            json: null,\n            text: null,\n            xml: null,\n            sparql: null,\n            formUrlEncoded: null,\n            multipartForm: null,\n            graphql: null,\n            file: null\n          }\n        };\n      }\n\n      if (example.response) {\n        brunoExample.response = {\n          status: typeof example.response.status === 'number' ? example.response.status\n            : example.response.status !== undefined ? Number(example.response.status) : null,\n          statusText: example.response.statusText || null,\n          headers: toBrunoHttpHeaders(example.response.headers) || [],\n          body: null\n        };\n\n        if (example.response.body) {\n          brunoExample.response.body = {\n            type: example.response.body.type || 'text',\n            content: example.response.body.data || ''\n          };\n        }\n      }\n\n      return brunoExample;\n    });\n  }\n\n  return brunoItem;\n};\n\nexport default parseHttpRequest;\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/items/parseScript.ts",
    "content": "import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';\nimport type { ScriptFile } from '@opencollection/types/collection/item';\nimport { uuid } from '../../../utils';\n\nconst parseScript = (ocScript: ScriptFile): BrunoItem => {\n  const brunoItem: BrunoItem = {\n    uid: uuid(),\n    type: 'js',\n    seq: 1,\n    name: 'Script',\n    tags: [],\n    request: null,\n    settings: null,\n    fileContent: ocScript.script || '',\n    root: null,\n    items: [],\n    examples: [],\n    filename: null,\n    pathname: null\n  };\n\n  return brunoItem;\n};\n\nexport default parseScript;\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/items/parseWebsocketRequest.ts",
    "content": "import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';\nimport type { WebSocketRequest as BrunoWebSocketRequest } from '@usebruno/schema-types/requests/websocket';\nimport type { WebSocketRequest, WebSocketMessage } from '@opencollection/types/requests/websocket';\nimport { toBrunoAuth } from '../common/auth';\nimport { toBrunoHttpHeaders } from '../common/headers';\nimport { toBrunoVariables } from '../common/variables';\nimport { toBrunoScripts } from '../common/scripts';\nimport { uuid, ensureString } from '../../../utils';\n\nconst parseWebsocketRequest = (ocRequest: WebSocketRequest): BrunoItem => {\n  const info = ocRequest.info;\n  const websocket = ocRequest.websocket;\n  const runtime = ocRequest.runtime;\n\n  const brunoRequest: BrunoWebSocketRequest = {\n    url: ensureString(websocket?.url),\n    headers: toBrunoHttpHeaders(websocket?.headers) || [],\n    auth: toBrunoAuth(websocket?.auth),\n    body: {\n      mode: 'ws',\n      ws: []\n    },\n    script: {\n      req: null,\n      res: null\n    },\n    vars: {\n      req: [],\n      res: []\n    },\n    assertions: [],\n    tests: null,\n    docs: null\n  };\n\n  // message\n  if (websocket?.message) {\n    const message = websocket.message as WebSocketMessage;\n    const messageData = ensureString(message.data);\n    if (messageData.trim().length) {\n      brunoRequest.body.ws = [{\n        name: '',\n        type: message.type || 'text',\n        content: messageData\n      }];\n    }\n  }\n\n  // scripts\n  const scripts = toBrunoScripts(runtime?.scripts);\n  if (scripts?.script && brunoRequest.script) {\n    if (scripts.script.req) {\n      brunoRequest.script.req = scripts.script.req;\n    }\n    if (scripts.script.res) {\n      brunoRequest.script.res = scripts.script.res;\n    }\n  }\n  if (scripts?.tests) {\n    brunoRequest.tests = scripts.tests;\n  }\n\n  // variables\n  const variables = toBrunoVariables(runtime?.variables);\n  brunoRequest.vars = variables;\n\n  // docs\n  if (ocRequest.docs) {\n    brunoRequest.docs = ocRequest.docs;\n  }\n\n  // settings\n  const wsSettings: Record<string, number> = {\n    timeout: 0,\n    keepAliveInterval: 0\n  };\n\n  if (ocRequest.settings) {\n    if (typeof ocRequest.settings.timeout === 'number') {\n      wsSettings.timeout = ocRequest.settings.timeout;\n    }\n    if (typeof ocRequest.settings.keepAliveInterval === 'number') {\n      wsSettings.keepAliveInterval = ocRequest.settings.keepAliveInterval;\n    }\n  }\n\n  // bruno item\n  const brunoItem: BrunoItem = {\n    uid: uuid(),\n    type: 'ws-request',\n    seq: info?.seq || 1,\n    name: ensureString(info?.name, 'Untitled Request'),\n    tags: info?.tags || [],\n    request: brunoRequest,\n    settings: wsSettings as any,\n    fileContent: null,\n    root: null,\n    items: [],\n    examples: [],\n    filename: null,\n    pathname: null\n  };\n\n  return brunoItem;\n};\n\nexport default parseWebsocketRequest;\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/items/stringifyGraphQLRequest.ts",
    "content": "import type { Item as BrunoItem, HttpItemSettings as BrunoHttpItemSettings } from '@usebruno/schema-types/collection/item';\nimport type { HttpRequest as BrunoHttpRequest } from '@usebruno/schema-types/requests/http';\nimport type { GraphQLRequest, GraphQLRequestSettings, GraphQLBody, GraphQLRequestInfo, GraphQLRequestDetails, GraphQLRequestRuntime } from '@opencollection/types/requests/graphql';\nimport type { Auth } from '@opencollection/types/common/auth';\nimport type { Scripts } from '@opencollection/types/common/scripts';\nimport type { Variable } from '@opencollection/types/common/variables';\nimport type { Assertion } from '@opencollection/types/common/assertions';\nimport type { Action } from '@opencollection/types/common/actions';\nimport type { HttpRequestParam, HttpRequestHeader } from '@opencollection/types/requests/http';\nimport { stringifyYml } from '../utils';\nimport { isNonEmptyString, isNumber } from '../../../utils';\nimport { toOpenCollectionAuth } from '../common/auth';\nimport { toOpenCollectionHttpHeaders } from '../common/headers';\nimport { toOpenCollectionParams } from '../common/params';\nimport { toOpenCollectionVariables } from '../common/variables';\nimport { toOpenCollectionActions } from '../common/actions';\nimport { toOpenCollectionScripts } from '../common/scripts';\nimport { toOpenCollectionAssertions } from '../common/assertions';\n\nconst stringifyGraphQLRequest = (item: BrunoItem): string => {\n  try {\n    const ocRequest: GraphQLRequest = {};\n    const brunoRequest = item.request as BrunoHttpRequest;\n\n    // info block\n    const info: GraphQLRequestInfo = {\n      name: isNonEmptyString(item.name) ? item.name : 'Untitled Request',\n      type: 'graphql'\n    };\n    if (item.seq) {\n      info.seq = item.seq;\n    }\n    if (item.tags?.length) {\n      info.tags = item.tags;\n    }\n    ocRequest.info = info;\n\n    // graphql block\n    const graphql: GraphQLRequestDetails = {\n      method: isNonEmptyString(brunoRequest.method) ? brunoRequest.method : 'POST',\n      url: isNonEmptyString(brunoRequest.url) ? brunoRequest.url : ''\n    };\n\n    // headers\n    const headers: HttpRequestHeader[] | undefined = toOpenCollectionHttpHeaders(brunoRequest.headers);\n    if (headers) {\n      graphql.headers = headers;\n    }\n\n    // params\n    const params: HttpRequestParam[] | undefined = toOpenCollectionParams(brunoRequest.params);\n    if (params) {\n      graphql.params = params;\n    }\n\n    // body\n    if (brunoRequest.body?.mode === 'graphql' && brunoRequest.body.graphql) {\n      const graphqlBody: GraphQLBody = {};\n      let hasBody = false;\n\n      if (isNonEmptyString(brunoRequest.body.graphql.query)) {\n        graphqlBody.query = brunoRequest.body.graphql.query;\n        hasBody = true;\n      }\n\n      if (isNonEmptyString(brunoRequest.body.graphql.variables)) {\n        graphqlBody.variables = brunoRequest.body.graphql.variables;\n        hasBody = true;\n      }\n\n      if (hasBody) {\n        graphql.body = graphqlBody;\n      }\n    }\n\n    // auth (in graphql block, not runtime)\n    const auth: Auth | undefined = toOpenCollectionAuth(brunoRequest.auth);\n    if (auth) {\n      graphql.auth = auth;\n    }\n\n    ocRequest.graphql = graphql;\n\n    // runtime block\n    const runtime: GraphQLRequestRuntime = {};\n    let hasRuntime = false;\n\n    // variables\n    const variables: Variable[] | undefined = toOpenCollectionVariables(brunoRequest.vars);\n    if (variables) {\n      runtime.variables = variables;\n      hasRuntime = true;\n    }\n\n    // scripts\n    const scripts: Scripts | undefined = toOpenCollectionScripts(brunoRequest);\n    if (scripts) {\n      runtime.scripts = scripts;\n      hasRuntime = true;\n    }\n\n    // assertions\n    const assertions: Assertion[] | undefined = toOpenCollectionAssertions(brunoRequest.assertions);\n    if (assertions) {\n      runtime.assertions = assertions;\n      hasRuntime = true;\n    }\n\n    // actions (from post-response variables)\n    const resVars = brunoRequest.vars?.res;\n    const actions: Action[] | undefined = toOpenCollectionActions(resVars);\n    if (actions) {\n      runtime.actions = actions;\n      hasRuntime = true;\n    }\n\n    if (hasRuntime) {\n      ocRequest.runtime = runtime;\n    }\n\n    // settings\n    const httpSettings = item.settings as BrunoHttpItemSettings | undefined;\n    const settings: GraphQLRequestSettings = {};\n    if (httpSettings?.encodeUrl === true) {\n      settings.encodeUrl = true;\n    } else if (httpSettings?.encodeUrl === false) {\n      settings.encodeUrl = false;\n    } else {\n      settings.encodeUrl = true;\n    }\n\n    const timeout = httpSettings?.timeout;\n    if (isNumber(timeout)) {\n      settings.timeout = timeout;\n    } else {\n      settings.timeout = 0;\n    }\n\n    if (httpSettings?.followRedirects === true) {\n      settings.followRedirects = true;\n    } else if (httpSettings?.followRedirects === false) {\n      settings.followRedirects = false;\n    } else {\n      settings.followRedirects = true;\n    }\n\n    const maxRedirects = httpSettings?.maxRedirects;\n    if (isNumber(maxRedirects)) {\n      settings.maxRedirects = maxRedirects;\n    } else {\n      settings.maxRedirects = 5;\n    }\n\n    ocRequest.settings = settings;\n\n    // docs\n    if (isNonEmptyString(brunoRequest.docs)) {\n      ocRequest.docs = brunoRequest.docs;\n    }\n\n    return stringifyYml(ocRequest);\n  } catch (error) {\n    console.error('Error stringifying GraphQL request:', error);\n    throw error;\n  }\n};\n\nexport default stringifyGraphQLRequest;\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/items/stringifyGrpcRequest.ts",
    "content": "import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';\nimport type { KeyValue as BrunoKeyValue } from '@usebruno/schema-types/common/key-value';\nimport type { GrpcRequest as BrunoGrpcRequest } from '@usebruno/schema-types/requests/grpc';\nimport type { GrpcRequest, GrpcMetadata, GrpcMessage, GrpcRequestInfo, GrpcRequestDetails, GrpcRequestRuntime } from '@opencollection/types/requests/grpc';\nimport type { Auth } from '@opencollection/types/common/auth';\nimport type { Scripts } from '@opencollection/types/common/scripts';\nimport type { Variable } from '@opencollection/types/common/variables';\nimport type { Assertion } from '@opencollection/types/common/assertions';\nimport { stringifyYml } from '../utils';\nimport { isNonEmptyString } from '../../../utils';\nimport { toOpenCollectionAuth } from '../common/auth';\nimport { toOpenCollectionVariables } from '../common/variables';\nimport { toOpenCollectionScripts } from '../common/scripts';\nimport { toOpenCollectionAssertions } from '../common/assertions';\n\nconst stringifyGrpcRequest = (item: BrunoItem): string => {\n  try {\n    const ocRequest: GrpcRequest = {};\n    const brunoRequest = item.request as BrunoGrpcRequest;\n\n    // info block\n    const info: GrpcRequestInfo = {\n      name: isNonEmptyString(item.name) ? item.name : 'Untitled Request',\n      type: 'grpc'\n    };\n    if (item.seq) {\n      info.seq = item.seq;\n    }\n    if (item.tags?.length) {\n      info.tags = item.tags;\n    }\n    ocRequest.info = info;\n\n    // grpc block\n    const grpc: GrpcRequestDetails = {\n      url: isNonEmptyString(brunoRequest.url) ? brunoRequest.url : '',\n      method: isNonEmptyString(brunoRequest.method) ? brunoRequest.method : ''\n    };\n\n    // method type\n    if (brunoRequest.methodType) {\n      grpc.methodType = brunoRequest.methodType;\n    }\n\n    // proto file path\n    if (isNonEmptyString(brunoRequest.protoPath)) {\n      grpc.protoFilePath = brunoRequest.protoPath;\n    }\n\n    // metadata\n    if (brunoRequest.headers?.length) {\n      const metadata: GrpcMetadata[] = brunoRequest.headers.map((header: BrunoKeyValue) => {\n        const metadataItem: GrpcMetadata = {\n          name: header.name || '',\n          value: header.value || ''\n        };\n\n        if (header?.description?.trim().length) {\n          metadataItem.description = header.description;\n        }\n\n        if (header.enabled === false) {\n          metadataItem.disabled = true;\n        }\n\n        return metadataItem;\n      });\n\n      if (metadata.length) {\n        grpc.metadata = metadata;\n      }\n    }\n\n    // message\n    if (brunoRequest.body?.mode === 'grpc' && brunoRequest.body.grpc?.length) {\n      const messages = brunoRequest.body.grpc;\n\n      // todo: bruno app supports only one message for now\n      // update this when bruno app supports multiple messages\n      if (messages.length) {\n        const message: GrpcMessage = messages[0].content || '';\n        if (message.trim().length) {\n          grpc.message = message;\n        }\n      }\n    }\n\n    // auth\n    const auth: Auth | undefined = toOpenCollectionAuth(brunoRequest.auth);\n    if (auth) {\n      grpc.auth = auth;\n    }\n\n    ocRequest.grpc = grpc;\n\n    // runtime block\n    const runtime: GrpcRequestRuntime = {};\n    let hasRuntime = false;\n\n    // variables\n    const variables: Variable[] | undefined = toOpenCollectionVariables(brunoRequest.vars);\n    if (variables) {\n      runtime.variables = variables;\n      hasRuntime = true;\n    }\n\n    // scripts\n    const scripts: Scripts | undefined = toOpenCollectionScripts(brunoRequest);\n    if (scripts) {\n      runtime.scripts = scripts;\n      hasRuntime = true;\n    }\n\n    // assertions\n    const assertions: Assertion[] | undefined = toOpenCollectionAssertions(brunoRequest.assertions);\n    if (assertions) {\n      runtime.assertions = assertions;\n      hasRuntime = true;\n    }\n\n    if (hasRuntime) {\n      ocRequest.runtime = runtime;\n    }\n\n    // docs\n    if (isNonEmptyString(brunoRequest.docs)) {\n      ocRequest.docs = brunoRequest.docs;\n    }\n\n    return stringifyYml(ocRequest);\n  } catch (error) {\n    console.error('Error stringifying gRPC request:', error);\n    throw error;\n  }\n};\n\nexport default stringifyGrpcRequest;\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/items/stringifyHttpRequest.ts",
    "content": "import type { Item as BrunoItem, HttpItemSettings as BrunoHttpItemSettings } from '@usebruno/schema-types/collection/item';\nimport type { HttpRequest as BrunoHttpRequest } from '@usebruno/schema-types/requests/http';\nimport type { HttpRequest, HttpRequestSettings, HttpRequestExample, HttpRequestInfo, HttpRequestDetails, HttpRequestRuntime, HttpRequestHeader } from '@opencollection/types/requests/http';\nimport type { Auth } from '@opencollection/types/common/auth';\nimport type { Scripts } from '@opencollection/types/common/scripts';\nimport type { Variable } from '@opencollection/types/common/variables';\nimport type { Assertion } from '@opencollection/types/common/assertions';\nimport type { Action } from '@opencollection/types/common/actions';\nimport type { HttpRequestParam, HttpRequestBody } from '@opencollection/types/requests/http';\nimport { stringifyYml } from '../utils';\nimport { toOpenCollectionAuth } from '../common/auth';\nimport { toOpenCollectionHttpHeaders, toOpenCollectionResponseHeaders } from '../common/headers';\nimport { toOpenCollectionParams } from '../common/params';\nimport { toOpenCollectionBody } from '../common/body';\nimport { toOpenCollectionVariables } from '../common/variables';\nimport { toOpenCollectionActions } from '../common/actions';\nimport { toOpenCollectionScripts } from '../common/scripts';\nimport { toOpenCollectionAssertions } from '../common/assertions';\nimport { isNumber, isNonEmptyString } from '../../../utils';\n\nconst stringifyHttpRequest = (item: BrunoItem): string => {\n  try {\n    const ocRequest: HttpRequest = {};\n    const brunoRequest = item.request as BrunoHttpRequest;\n\n    // info block\n    const info: HttpRequestInfo = {\n      name: isNonEmptyString(item.name) ? item.name : 'Untitled Request',\n      type: 'http'\n    };\n    if (item.seq) {\n      info.seq = item.seq;\n    }\n    if (item.tags?.length) {\n      info.tags = item.tags;\n    }\n    ocRequest.info = info;\n\n    // http block\n    const http: HttpRequestDetails = {\n      method: isNonEmptyString(brunoRequest.method) ? brunoRequest.method : 'GET',\n      url: isNonEmptyString(brunoRequest.url) ? brunoRequest.url : ''\n    };\n\n    // headers\n    const headers: HttpRequestHeader[] | undefined = toOpenCollectionHttpHeaders(brunoRequest.headers);\n    if (headers) {\n      http.headers = headers;\n    }\n\n    // params\n    const params: HttpRequestParam[] | undefined = toOpenCollectionParams(brunoRequest.params);\n    if (params) {\n      http.params = params;\n    }\n\n    // body\n    const body: HttpRequestBody | undefined = toOpenCollectionBody(brunoRequest.body);\n    if (body) {\n      http.body = body;\n    }\n\n    // auth\n    const auth: Auth | undefined = toOpenCollectionAuth(brunoRequest.auth);\n    if (auth) {\n      http.auth = auth;\n    }\n\n    ocRequest.http = http;\n\n    // runtime block\n    const runtime: HttpRequestRuntime = {};\n    let hasRuntime = false;\n\n    // variables\n    const variables: Variable[] | undefined = toOpenCollectionVariables(brunoRequest.vars);\n    if (variables) {\n      runtime.variables = variables;\n      hasRuntime = true;\n    }\n\n    // scripts\n    const scripts: Scripts | undefined = toOpenCollectionScripts(brunoRequest);\n    if (scripts) {\n      runtime.scripts = scripts;\n      hasRuntime = true;\n    }\n\n    // assertions\n    const assertions: Assertion[] | undefined = toOpenCollectionAssertions(brunoRequest.assertions);\n    if (assertions) {\n      runtime.assertions = assertions;\n      hasRuntime = true;\n    }\n\n    // actions (from post-response variables)\n    const resVars = brunoRequest.vars?.res;\n    const actions: Action[] | undefined = toOpenCollectionActions(resVars);\n    if (actions) {\n      runtime.actions = actions;\n      hasRuntime = true;\n    }\n\n    if (hasRuntime) {\n      ocRequest.runtime = runtime;\n    }\n\n    // settings\n    const httpSettings = item.settings as BrunoHttpItemSettings | undefined;\n    const settings: HttpRequestSettings = {};\n    if (httpSettings?.encodeUrl === true) {\n      settings.encodeUrl = true;\n    } else if (httpSettings?.encodeUrl === false) {\n      settings.encodeUrl = false;\n    } else {\n      settings.encodeUrl = true;\n    }\n\n    const timeout = httpSettings?.timeout;\n    if (isNumber(timeout)) {\n      settings.timeout = timeout;\n    } else {\n      settings.timeout = 0;\n    }\n\n    if (httpSettings?.followRedirects === true) {\n      settings.followRedirects = true;\n    } else if (httpSettings?.followRedirects === false) {\n      settings.followRedirects = false;\n    } else {\n      settings.followRedirects = true;\n    }\n\n    const maxRedirects = httpSettings?.maxRedirects;\n    if (isNumber(maxRedirects)) {\n      settings.maxRedirects = maxRedirects;\n    } else {\n      settings.maxRedirects = 5;\n    }\n\n    ocRequest.settings = settings;\n\n    // examples\n    if (item.examples?.length) {\n      const examples: HttpRequestExample[] = item.examples.map((example) => {\n        const ocExample: HttpRequestExample = {};\n        ocExample.name = example?.name || 'Untitled Example';\n\n        if (isNonEmptyString(example.description)) {\n          ocExample.description = example.description;\n        }\n\n        if (example.request) {\n          ocExample.request = {};\n          ocExample.request.url = example.request.url || '';\n          ocExample.request.method = example.request.method || 'GET';\n\n          const exampleHeaders = toOpenCollectionHttpHeaders(example.request.headers);\n          if (exampleHeaders) {\n            ocExample.request.headers = exampleHeaders;\n          }\n\n          const exampleParams = toOpenCollectionParams(example.request.params);\n          if (exampleParams) {\n            ocExample.request.params = exampleParams;\n          }\n\n          const exampleBody = toOpenCollectionBody(example.request.body);\n          if (exampleBody !== undefined) {\n            ocExample.request.body = exampleBody;\n          }\n        }\n\n        if (example.response) {\n          ocExample.response = {};\n\n          const statusNum = Number(example.response.status);\n          if (Number.isInteger(statusNum) && statusNum > 0) {\n            ocExample.response.status = statusNum;\n          }\n\n          if (isNonEmptyString(example.response.statusText)) {\n            ocExample.response.statusText = example.response.statusText;\n          }\n\n          const responseHeaders = toOpenCollectionResponseHeaders(example.response.headers);\n          if (responseHeaders) {\n            ocExample.response.headers = responseHeaders;\n          }\n\n          if (example.response.body && example.response.body.type && example.response.body.content !== undefined) {\n            const content = example.response.body.content;\n            const contentString = typeof content === 'string' ? content : JSON.stringify(content, null, 2);\n\n            ocExample.response.body = {\n              type: example.response.body.type as 'json' | 'text' | 'xml' | 'html' | 'binary',\n              data: contentString\n            };\n          }\n        }\n\n        return ocExample;\n      });\n\n      if (examples?.length) {\n        ocRequest.examples = examples;\n      }\n    }\n\n    // docs\n    if (isNonEmptyString(brunoRequest.docs)) {\n      ocRequest.docs = brunoRequest.docs;\n    }\n\n    return stringifyYml(ocRequest);\n  } catch (error) {\n    console.error('Error stringifying HTTP request:', error);\n    throw error;\n  }\n};\n\nexport default stringifyHttpRequest;\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/items/stringifyScript.ts",
    "content": "import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';\nimport type { ScriptFile } from '@opencollection/types/collection/item';\nimport { stringifyYml } from '../utils';\n\nconst stringifyScript = (item: BrunoItem): string => {\n  try {\n    const ocScript: ScriptFile = {\n      type: 'script'\n    };\n\n    if (item.fileContent?.trim().length) {\n      ocScript.script = item.fileContent;\n    }\n\n    return stringifyYml(ocScript);\n  } catch (error) {\n    console.error('Error stringifying script:', error);\n    throw error;\n  }\n};\n\nexport default stringifyScript;\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/items/stringifyWebsocketRequest.ts",
    "content": "import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';\nimport type { WebSocketRequest as BrunoWebSocketRequest } from '@usebruno/schema-types/requests/websocket';\nimport type { WebSocketRequest, WebSocketMessage, WebSocketRequestInfo, WebSocketRequestDetails, WebSocketRequestRuntime } from '@opencollection/types/requests/websocket';\nimport type { Auth } from '@opencollection/types/common/auth';\nimport type { Scripts } from '@opencollection/types/common/scripts';\nimport type { Variable } from '@opencollection/types/common/variables';\nimport type { HttpRequestHeader } from '@opencollection/types/requests/http';\nimport { stringifyYml } from '../utils';\nimport { isNonEmptyString } from '../../../utils';\nimport { toOpenCollectionAuth } from '../common/auth';\nimport { toOpenCollectionHttpHeaders } from '../common/headers';\nimport { toOpenCollectionVariables } from '../common/variables';\nimport { toOpenCollectionScripts } from '../common/scripts';\n\nconst stringifyWebsocketRequest = (item: BrunoItem): string => {\n  try {\n    const ocRequest: WebSocketRequest = {};\n    const brunoRequest = item.request as BrunoWebSocketRequest;\n\n    // info block\n    const info: WebSocketRequestInfo = {\n      name: isNonEmptyString(item.name) ? item.name : 'Untitled Request',\n      type: 'websocket'\n    };\n    if (item.seq) {\n      info.seq = item.seq;\n    }\n    if (item.tags?.length) {\n      info.tags = item.tags;\n    }\n    ocRequest.info = info;\n\n    // websocket block\n    const websocket: WebSocketRequestDetails = {\n      url: isNonEmptyString(brunoRequest.url) ? brunoRequest.url : ''\n    };\n\n    // headers\n    const headers: HttpRequestHeader[] | undefined = toOpenCollectionHttpHeaders(brunoRequest.headers);\n    if (headers) {\n      websocket.headers = headers;\n    }\n\n    // message\n    if (brunoRequest.body?.mode === 'ws' && brunoRequest.body.ws?.length) {\n      const messages = brunoRequest.body.ws;\n\n      // todo: bruno app supports only one message for now\n      // update this when bruno app supports multiple messages\n      if (messages.length) {\n        const msg = messages[0];\n        const message: WebSocketMessage = {\n          type: (msg.type as 'text' | 'json' | 'xml' | 'binary') || 'text',\n          data: msg.content || ''\n        };\n        if (message.data.trim().length) {\n          websocket.message = message;\n        }\n      }\n    }\n\n    // auth\n    const auth: Auth | undefined = toOpenCollectionAuth(brunoRequest.auth);\n    if (auth) {\n      websocket.auth = auth;\n    }\n\n    ocRequest.websocket = websocket;\n\n    // runtime block\n    const runtime: WebSocketRequestRuntime = {};\n    let hasRuntime = false;\n\n    // variables\n    const variables: Variable[] | undefined = toOpenCollectionVariables(brunoRequest.vars);\n    if (variables) {\n      runtime.variables = variables;\n      hasRuntime = true;\n    }\n\n    // scripts\n    const scripts: Scripts | undefined = toOpenCollectionScripts(brunoRequest);\n    if (scripts) {\n      runtime.scripts = scripts;\n      hasRuntime = true;\n    }\n\n    if (hasRuntime) {\n      ocRequest.runtime = runtime;\n    }\n\n    // settings\n    const wsSettings = item.settings as Record<string, number | string | undefined> | undefined;\n    if (wsSettings) {\n      ocRequest.settings = {};\n      const timeout = Number(wsSettings.timeout);\n      ocRequest.settings.timeout = !isNaN(timeout) ? timeout : 0;\n      const keepAliveInterval = Number(wsSettings.keepAliveInterval);\n      ocRequest.settings.keepAliveInterval = !isNaN(keepAliveInterval) ? keepAliveInterval : 0;\n    }\n\n    // docs\n    if (isNonEmptyString(brunoRequest.docs)) {\n      ocRequest.docs = brunoRequest.docs;\n    }\n\n    return stringifyYml(ocRequest);\n  } catch (error) {\n    console.error('Error stringifying WebSocket request:', error);\n    throw error;\n  }\n};\n\nexport default stringifyWebsocketRequest;\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/parseCollection.ts",
    "content": "import type { OpenCollection } from '@opencollection/types';\nimport type { FolderRoot } from '@usebruno/schema-types/collection/folder';\nimport { parseYml } from './utils';\nimport { toBrunoAuth } from './common/auth';\nimport { toBrunoHttpHeaders } from './common/headers';\nimport { toBrunoVariables } from './common/variables';\nimport { toBrunoPostResponseVariables } from './common/actions';\nimport { toBrunoScripts } from './common/scripts';\nimport { ensureString } from '../../utils';\n\ninterface ParsedCollection {\n  collectionRoot: FolderRoot;\n  brunoConfig: Record<string, any>;\n}\n\nconst parseCollection = (ymlString: string): ParsedCollection => {\n  try {\n    const oc: OpenCollection = parseYml(ymlString);\n\n    // bruno config\n    const brunoConfig: Record<string, any> = {\n      opencollection: oc.opencollection || '1.0.0',\n      name: ensureString(oc.info?.name, 'Untitled Collection'),\n      type: 'collection',\n      ignore: []\n    };\n\n    const brunoExtension = (oc.extensions as any)?.bruno;\n    if (brunoExtension?.ignore && Array.isArray(brunoExtension.ignore)) {\n      brunoConfig.ignore = brunoExtension.ignore;\n    }\n\n    // presets\n    if (brunoExtension?.presets) {\n      const presets = brunoExtension.presets as any;\n      if (presets.request) {\n        brunoConfig.presets = {\n          requestType: presets.request.type || [],\n          requestUrl: presets.request.url || []\n        };\n      }\n    }\n\n    // bruno-specific extensions\n    const brunoExtensions = oc.extensions?.bruno as any;\n    if (Array.isArray(brunoExtensions?.scripts?.additionalContextRoots)) {\n      const sanitizedRoots = brunoExtensions.scripts.additionalContextRoots\n        .filter((item: any) => typeof item === 'string');\n\n      if (sanitizedRoots.length > 0) {\n        brunoConfig.scripts = {\n          ...brunoConfig.scripts,\n          additionalContextRoots: sanitizedRoots\n        };\n      }\n    }\n    if (Array.isArray(brunoExtensions?.openapi) && brunoExtensions.openapi.length > 0) {\n      brunoConfig.openapi = brunoExtensions.openapi.map((entry: any) => ({\n        sourceUrl: entry.sourceUrl,\n        groupBy: entry.groupBy,\n        ...(entry.lastSyncDate && { lastSyncDate: entry.lastSyncDate }),\n        ...(entry.specHash && { specHash: entry.specHash }),\n        autoCheck: entry.autoCheck !== false,\n        autoCheckInterval: entry.autoCheckInterval || 5\n      }));\n    }\n\n    // protobuf\n    if (oc.config?.protobuf) {\n      brunoConfig.protobuf = {\n        protoFiles: oc.config.protobuf.protoFiles?.map((protoFile: any) => ({\n          path: protoFile.path\n        })) || [],\n        importPaths: oc.config.protobuf.importPaths?.map((importPath: any) => ({\n          path: importPath.path,\n          enabled: importPath.disabled !== true\n        })) || []\n      };\n    }\n\n    // proxy - only support newer format\n    // Default: inherit from global preferences\n    brunoConfig.proxy = {\n      inherit: true,\n      config: {\n        protocol: 'http',\n        hostname: '',\n        port: '',\n        auth: {\n          username: '',\n          password: ''\n        },\n        bypassProxy: ''\n      }\n    };\n\n    if (oc.config?.proxy && typeof oc.config.proxy === 'object') {\n      // Validate newer format: must have 'inherit' and 'config' properties\n      const proxyConfig = oc.config.proxy as any;\n\n      if ('inherit' in proxyConfig && typeof proxyConfig.inherit === 'boolean' && proxyConfig.config && typeof proxyConfig.config === 'object') {\n        // Valid newer format\n        brunoConfig.proxy = {\n          inherit: proxyConfig.inherit,\n          config: {\n            protocol: proxyConfig.config.protocol || 'http',\n            hostname: proxyConfig.config.hostname || '',\n            port: proxyConfig.config.port || '',\n            auth: {\n              username: proxyConfig.config.auth?.username || '',\n              password: proxyConfig.config.auth?.password || ''\n            },\n            bypassProxy: proxyConfig.config.bypassProxy || ''\n          }\n        };\n\n        // Handle optional disabled field\n        if (proxyConfig.disabled === true) {\n          brunoConfig.proxy.disabled = true;\n        }\n\n        // Handle optional auth.disabled field\n        if (proxyConfig.config.auth?.disabled === true) {\n          brunoConfig.proxy.config.auth.disabled = true;\n        }\n      }\n      // If not in newer format, use default (inherit: true)\n    }\n\n    // client certificates\n    if (oc.config?.clientCertificates?.length) {\n      brunoConfig.clientCertificates = {\n        certs: oc.config.clientCertificates.map((cert: any) => {\n          if (cert.type === 'pem') {\n            return {\n              domain: cert.domain,\n              type: 'cert',\n              certFilePath: cert.certificateFilePath,\n              keyFilePath: cert.privateKeyFilePath,\n              passphrase: cert.passphrase || ''\n            };\n          } else if (cert.type === 'pkcs12') {\n            return {\n              domain: cert.domain,\n              type: 'pfx',\n              pfxFilePath: cert.pkcs12FilePath,\n              passphrase: cert.passphrase || ''\n            };\n          }\n          return null;\n        }).filter((cert: any) => cert !== null)\n      };\n    }\n\n    // collection root\n    const collectionRoot: FolderRoot = {\n      meta: null,\n      request: null,\n      docs: null\n    };\n\n    // request defaults\n    if (oc.request) {\n      collectionRoot.request = {\n        headers: null,\n        auth: null,\n        script: {\n          req: null,\n          res: null\n        },\n        vars: {\n          req: [],\n          res: []\n        },\n        tests: null\n      };\n\n      // headers\n      const headers = toBrunoHttpHeaders(oc.request.headers);\n      collectionRoot.request.headers = headers || [];\n\n      // auth\n      const auth = toBrunoAuth(oc.request.auth);\n      if (auth) {\n        collectionRoot.request.auth = auth;\n      }\n\n      // variables\n      const variables = toBrunoVariables(oc.request.variables);\n      const postResponseVars = toBrunoPostResponseVariables((oc.request as any).actions);\n      collectionRoot.request.vars = {\n        req: variables.req,\n        res: postResponseVars\n      };\n\n      // scripts\n      const scripts = toBrunoScripts(oc.request.scripts);\n      if (scripts?.script && collectionRoot.request.script) {\n        if (scripts.script.req) {\n          collectionRoot.request.script.req = scripts.script.req;\n        }\n        if (scripts.script.res) {\n          collectionRoot.request.script.res = scripts.script.res;\n        }\n      }\n      if (scripts?.tests) {\n        collectionRoot.request.tests = scripts.tests;\n      }\n    }\n\n    // docs\n    if (oc.docs) {\n      if (typeof oc.docs === 'string') {\n        collectionRoot.docs = oc.docs;\n      } else if (typeof oc.docs === 'object' && oc.docs.content) {\n        collectionRoot.docs = oc.docs.content;\n      }\n    }\n\n    return {\n      collectionRoot,\n      brunoConfig\n    };\n  } catch (error) {\n    console.error('Error parsing collection:', error);\n    throw error;\n  }\n};\n\nexport default parseCollection;\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/parseEnvironment.ts",
    "content": "import type { Environment as BrunoEnvironment, EnvironmentVariable as BrunoEnvironmentVariable } from '@usebruno/schema-types/collection/environment';\nimport type { Environment } from '@opencollection/types/config/environments';\nimport type { Variable, SecretVariable } from '@opencollection/types/common/variables';\nimport { parseYml } from './utils';\nimport { uuid, ensureString } from '../../utils';\n\nconst isSecretVariable = (v: Variable | SecretVariable): v is SecretVariable => {\n  return 'secret' in v && v.secret === true;\n};\n\nconst toBrunoEnvironmentVariables = (variables: (Variable | SecretVariable)[] | null | undefined): BrunoEnvironmentVariable[] => {\n  if (!variables?.length) {\n    return [];\n  }\n\n  return variables.map((v): BrunoEnvironmentVariable => {\n    if (isSecretVariable(v)) {\n      return {\n        uid: uuid(),\n        name: ensureString(v.name),\n        value: '',\n        type: 'text',\n        enabled: v.disabled !== true,\n        secret: true\n      };\n    }\n    const variable: BrunoEnvironmentVariable = {\n      uid: uuid(),\n      name: ensureString(v.name),\n      value: ensureString(v.value),\n      type: 'text',\n      enabled: v.disabled !== true,\n      secret: false\n    };\n    return variable;\n  });\n};\n\nconst parseEnvironment = (ymlString: string): BrunoEnvironment => {\n  try {\n    const ocEnvironment: Environment = parseYml(ymlString);\n\n    const brunoEnvironment: BrunoEnvironment = {\n      uid: uuid(),\n      name: ensureString(ocEnvironment.name, 'Untitled Environment'),\n      variables: toBrunoEnvironmentVariables(ocEnvironment.variables),\n      color: ocEnvironment.color || null\n    };\n\n    return brunoEnvironment;\n  } catch (error) {\n    console.error('Error parsing environment:', error);\n    throw error;\n  }\n};\n\nexport default parseEnvironment;\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/parseFolder.ts",
    "content": "import type { FolderRoot } from '@usebruno/schema-types/collection/folder';\nimport type { Folder } from '@opencollection/types/collection/item';\nimport { parseYml } from './utils';\nimport { toBrunoAuth } from './common/auth';\nimport { toBrunoHttpHeaders } from './common/headers';\nimport { toBrunoVariables } from './common/variables';\nimport { toBrunoPostResponseVariables } from './common/actions';\nimport { toBrunoScripts } from './common/scripts';\nimport { ensureString } from '../../utils';\n\nconst parseFolder = (ymlString: string): FolderRoot => {\n  try {\n    const ocFolder: Folder = parseYml(ymlString);\n\n    const info = ocFolder.info;\n\n    const folderRoot: FolderRoot = {\n      meta: {\n        name: ensureString(info?.name, 'Untitled Folder'),\n        seq: info?.seq || 1\n      },\n      request: null,\n      docs: null\n    };\n\n    // request defaults\n    if (ocFolder.request) {\n      folderRoot.request = {\n        headers: [],\n        auth: null,\n        script: {\n          req: null,\n          res: null\n        },\n        vars: {\n          req: [],\n          res: []\n        },\n        tests: null\n      };\n\n      // headers\n      const headers = toBrunoHttpHeaders(ocFolder.request.headers);\n      if (headers) {\n        folderRoot.request.headers = headers;\n      }\n\n      // auth\n      const auth = toBrunoAuth(ocFolder.request.auth);\n      if (auth) {\n        folderRoot.request.auth = auth;\n      }\n\n      // variables\n      const variables = toBrunoVariables(ocFolder.request.variables);\n      const postResponseVars = toBrunoPostResponseVariables((ocFolder.request as any).actions);\n      folderRoot.request.vars = {\n        req: variables.req,\n        res: postResponseVars\n      };\n\n      // scripts\n      const scripts = toBrunoScripts(ocFolder.request.scripts);\n      if (scripts?.script && folderRoot.request.script) {\n        if (scripts.script.req) {\n          folderRoot.request.script.req = scripts.script.req;\n        }\n        if (scripts.script.res) {\n          folderRoot.request.script.res = scripts.script.res;\n        }\n      }\n      if (scripts?.tests) {\n        folderRoot.request.tests = scripts.tests;\n      }\n    }\n\n    // docs (now at root level)\n    if (ocFolder.docs) {\n      if (typeof ocFolder.docs === 'string' && ocFolder.docs.trim().length) {\n        folderRoot.docs = ocFolder.docs;\n      } else if (typeof ocFolder.docs === 'object' && ocFolder.docs.content?.trim().length) {\n        folderRoot.docs = ocFolder.docs.content;\n      }\n    }\n\n    return folderRoot;\n  } catch (error) {\n    console.error('Error parsing folder:', error);\n    throw error;\n  }\n};\n\nexport default parseFolder;\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/parseItem.ts",
    "content": "import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';\nimport type { Item, ScriptFile } from '@opencollection/types/collection/item';\nimport type { HttpRequest } from '@opencollection/types/requests/http';\nimport type { GraphQLRequest } from '@opencollection/types/requests/graphql';\nimport type { GrpcRequest } from '@opencollection/types/requests/grpc';\nimport type { WebSocketRequest } from '@opencollection/types/requests/websocket';\nimport { parseYml } from './utils';\nimport parseHttpRequest from './items/parseHttpRequest';\nimport parseGraphQLRequest from './items/parseGraphQLRequest';\nimport parseGrpcRequest from './items/parseGrpcRequest';\nimport parseWebsocketRequest from './items/parseWebsocketRequest';\nimport parseScript from './items/parseScript';\n\n// Helper to get the type from an item (now in info block)\nconst getItemType = (item: Item): string | undefined => {\n  if ('info' in item && item.info && 'type' in item.info) {\n    return item.info.type;\n  }\n  // For ScriptFile which still has type at root\n  if ('type' in item) {\n    return (item as ScriptFile).type;\n  }\n  return undefined;\n};\n\n/**\n * In v3.0.0-rc1 release auth was present under runtime property for all requests\n * This has now been moved to the respective request properties\n * This backward compatibility has been put in place for folks who tried out our early preview\n * Should be safe to remove this in 3 months. Delete after 5 Apr 2026\n */\nconst ensureAuthV3Rc1BackwardsCompatibility = (parsedItemYml: any): any => {\n  const itemType = parsedItemYml?.info?.type;\n\n  switch (itemType) {\n    case 'http':\n      if (parsedItemYml.runtime?.auth && !parsedItemYml.http?.auth) {\n        parsedItemYml.http.auth = parsedItemYml.runtime.auth;\n      }\n      break;\n    case 'graphql':\n      if (parsedItemYml.runtime?.auth && !parsedItemYml.graphql?.auth) {\n        parsedItemYml.graphql.auth = parsedItemYml.runtime.auth;\n      }\n      break;\n    case 'grpc':\n      if (parsedItemYml.runtime?.auth && !parsedItemYml.grpc?.auth) {\n        parsedItemYml.grpc.auth = parsedItemYml.runtime.auth;\n      }\n      break;\n    case 'websocket':\n      if (parsedItemYml.runtime?.auth && !parsedItemYml.websocket?.auth) {\n        parsedItemYml.websocket.auth = parsedItemYml.runtime.auth;\n      }\n      break;\n    default:\n      break;\n  }\n\n  return parsedItemYml;\n};\n\nconst parseItem = (ymlString: string): BrunoItem => {\n  try {\n    const parsedYml = parseYml(ymlString);\n\n    const ocItem: Item = ensureAuthV3Rc1BackwardsCompatibility(parsedYml);\n    const itemType = getItemType(ocItem);\n\n    if (!ocItem || !itemType) {\n      throw new Error('Invalid item: missing type');\n    }\n\n    switch (itemType) {\n      case 'http':\n        return parseHttpRequest(ocItem as HttpRequest);\n\n      case 'graphql':\n        return parseGraphQLRequest(ocItem as GraphQLRequest);\n\n      case 'grpc':\n        return parseGrpcRequest(ocItem as GrpcRequest);\n\n      case 'websocket':\n        return parseWebsocketRequest(ocItem as WebSocketRequest);\n\n      case 'script':\n        return parseScript(ocItem as ScriptFile);\n\n      case 'folder':\n        throw new Error('Folder items should be handled separately using parseFolder');\n\n      default:\n        throw new Error(`Unsupported item type: ${itemType}`);\n    }\n  } catch (error) {\n    console.error('Error parsing item:', error);\n    throw error;\n  }\n};\n\nexport default parseItem;\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/stringifyCollection.ts",
    "content": "import type { OpenCollection } from '@opencollection/types';\nimport type { ProtoFileItem, ProtoFileImportPath } from '@opencollection/types/config/protobuf';\nimport type { HttpRequestHeader } from '@opencollection/types/requests/http';\nimport type { ClientCertificate, PemCertificate, Pkcs12Certificate } from '@opencollection/types/config/certificates';\nimport type { Variable } from '@opencollection/types/common/variables';\nimport type { Action } from '@opencollection/types/common/actions';\nimport type { Scripts } from '@opencollection/types/common/scripts';\nimport { stringifyYml } from './utils';\nimport { toOpenCollectionAuth } from './common/auth';\nimport { toOpenCollectionHttpHeaders } from './common/headers';\nimport { toOpenCollectionVariables } from './common/variables';\nimport { toOpenCollectionActions } from './common/actions';\nimport { toOpenCollectionScripts } from './common/scripts';\nimport type { Auth } from '@opencollection/types/common/auth';\n\nconst hasCollectionConfig = (brunoConfig: any): boolean => {\n  // protobuf\n  const hasProtobuf = (\n    brunoConfig.protobuf?.protoFiles?.length > 0\n    || brunoConfig.protobuf?.importPaths?.length > 0\n  );\n\n  // proxy - check if proxy is configured in newer format\n  // Valid newer format: has 'inherit' property and 'config' object\n  const isValidProxyFormat = brunoConfig.proxy\n    && typeof brunoConfig.proxy === 'object'\n    && 'inherit' in brunoConfig.proxy\n    && brunoConfig.proxy.config\n    && typeof brunoConfig.proxy.config === 'object';\n\n  const hasProxy = isValidProxyFormat;\n\n  // client certificates\n  const hasClientCertificates = brunoConfig.clientCertificates?.certs?.length > 0;\n\n  return hasProtobuf || hasProxy || hasClientCertificates;\n};\n\nconst hasRequestDefaults = (collectionRoot: any): boolean => {\n  const requestRoot = collectionRoot?.request;\n\n  return Boolean((requestRoot?.headers?.length)\n    || (requestRoot?.vars?.req?.length)\n    || (requestRoot?.vars?.res?.length)\n    || hasRequestScripts(collectionRoot)\n    || hasRequestAuth(collectionRoot));\n};\n\nconst hasRequestAuth = (collectionRoot: any): boolean => {\n  const reqAuthMode = collectionRoot?.request?.auth?.mode;\n  return Boolean(reqAuthMode && reqAuthMode !== 'none');\n};\n\nconst hasRequestScripts = (collectionRoot: any): boolean => {\n  if (!collectionRoot?.request) return false;\n\n  return (collectionRoot.request.script?.req)\n    || (collectionRoot.request.script?.res)\n    || (collectionRoot.request.tests);\n};\n\nconst hasPresets = (brunoConfig: any): boolean => {\n  return brunoConfig?.presets?.requestType?.length\n    || brunoConfig?.presets?.requestUrl?.length;\n};\n\nconst stringifyCollection = (collectionRoot: any, brunoConfig: any): string => {\n  try {\n    const oc: OpenCollection = {};\n\n    oc.opencollection = '1.0.0';\n    oc.info = {\n      name: brunoConfig.name || 'Untitled Collection'\n    };\n\n    // collection config\n    if (hasCollectionConfig(brunoConfig)) {\n      oc.config = {};\n\n      if (brunoConfig.protobuf?.protoFiles?.length || brunoConfig.protobuf?.importPaths?.length) {\n        oc.config.protobuf = {};\n\n        if (brunoConfig.protobuf.protoFiles?.length) {\n          oc.config.protobuf.protoFiles = brunoConfig.protobuf.protoFiles.map((protoFile: any): ProtoFileItem => ({\n            type: 'file' as const,\n            path: protoFile.path\n          }));\n        }\n\n        if (brunoConfig.protobuf.importPaths?.length) {\n          oc.config.protobuf.importPaths = brunoConfig.protobuf.importPaths.map((importPath: any): ProtoFileImportPath => {\n            const item: ProtoFileImportPath = { path: importPath.path };\n            if (importPath.enabled === false) {\n              item.disabled = true;\n            }\n            return item;\n          });\n        }\n      }\n\n      // proxy - only write newer format\n      // Validate that brunoConfig.proxy is in newer format before writing\n      const isValidProxyFormat = brunoConfig.proxy\n        && typeof brunoConfig.proxy === 'object'\n        && 'inherit' in brunoConfig.proxy\n        && brunoConfig.proxy.config\n        && typeof brunoConfig.proxy.config === 'object';\n\n      if (isValidProxyFormat) {\n        oc.config.proxy = {\n          inherit: brunoConfig.proxy.inherit,\n          config: {\n            protocol: brunoConfig.proxy.config.protocol || 'http',\n            hostname: brunoConfig.proxy.config.hostname || '',\n            port: brunoConfig.proxy.config.port || '',\n            auth: {\n              username: brunoConfig.proxy.config.auth?.username || '',\n              password: brunoConfig.proxy.config.auth?.password || ''\n            },\n            bypassProxy: brunoConfig.proxy.config.bypassProxy || ''\n          }\n        };\n\n        // Add optional disabled field if true\n        if (brunoConfig.proxy.disabled === true) {\n          oc.config.proxy.disabled = true;\n        }\n\n        // Add optional auth.disabled field if true\n        if (brunoConfig.proxy.config?.auth?.disabled === true) {\n          if (oc.config.proxy.config && oc.config.proxy.config.auth) {\n            oc.config.proxy.config.auth.disabled = true;\n          }\n        }\n      }\n\n      // client certificates\n      if (brunoConfig.clientCertificates?.certs?.length) {\n        oc.config.clientCertificates = brunoConfig.clientCertificates.certs\n          .map((cert: any): ClientCertificate | null => {\n            if (cert.type === 'cert') {\n              const pemCert: PemCertificate = {\n                domain: cert.domain,\n                type: 'pem',\n                certificateFilePath: cert.certFilePath,\n                privateKeyFilePath: cert.keyFilePath,\n                ...(cert.passphrase && { passphrase: cert.passphrase })\n              };\n              return pemCert;\n            } else if (cert.type === 'pfx') {\n              const pkcs12Cert: Pkcs12Certificate = {\n                domain: cert.domain,\n                type: 'pkcs12',\n                pkcs12FilePath: cert.pfxFilePath,\n                ...(cert.passphrase && { passphrase: cert.passphrase })\n              };\n              return pkcs12Cert;\n            } else {\n              // Unsupported certificate type - ignore silently\n              return null;\n            }\n          })\n          .filter((cert: ClientCertificate | null): cert is ClientCertificate => cert !== null);\n      }\n    }\n\n    // request defaults\n    if (hasRequestDefaults(collectionRoot)) {\n      oc.request = {};\n\n      // headers\n      if (collectionRoot.request?.headers?.length) {\n        const ocHeaders: HttpRequestHeader[] | undefined = toOpenCollectionHttpHeaders(collectionRoot.request?.headers);\n        if (ocHeaders) {\n          oc.request.headers = ocHeaders;\n        }\n      }\n\n      // auth\n      if (hasRequestAuth(collectionRoot)) {\n        const ocAuth: Auth | undefined = toOpenCollectionAuth(collectionRoot.request?.auth);\n        if (ocAuth) {\n          oc.request.auth = ocAuth;\n        }\n      }\n\n      // variables\n      if (collectionRoot.request?.vars?.req?.length) {\n        const ocVariables: Variable[] | undefined = toOpenCollectionVariables(collectionRoot.request?.vars);\n        if (ocVariables) {\n          oc.request.variables = ocVariables;\n        }\n      }\n\n      // actions (post-response variables)\n      if (collectionRoot.request?.vars?.res?.length) {\n        const ocActions: Action[] | undefined = toOpenCollectionActions(collectionRoot.request?.vars?.res);\n        if (ocActions) {\n          (oc.request as any).actions = ocActions;\n        }\n      }\n\n      // scripts\n      if (hasRequestScripts(collectionRoot)) {\n        const ocScripts: Scripts | undefined = toOpenCollectionScripts(collectionRoot.request);\n        if (ocScripts) {\n          oc.request.scripts = ocScripts;\n        }\n      }\n    }\n\n    // docs\n    if (collectionRoot?.docs?.trim().length) {\n      oc.docs = {\n        content: collectionRoot.docs,\n        type: 'text/markdown'\n      };\n    }\n\n    // bundled\n    oc.bundled = false;\n\n    // extensions\n    oc.extensions = {};\n\n    const hasBrunoExtensions = brunoConfig.ignore?.length || hasPresets(brunoConfig);\n\n    if (hasBrunoExtensions) {\n      const brunoExtension: any = {};\n\n      if (brunoConfig.ignore?.length) {\n        const ignoreList: string[] = [];\n        brunoConfig.ignore.forEach((ignore: string) => {\n          ignoreList.push(ignore);\n        });\n        brunoExtension.ignore = ignoreList;\n      }\n\n      if (hasPresets(brunoConfig)) {\n        const presetsRequest: any = {};\n        if (brunoConfig.presets.requestType?.length) {\n          presetsRequest.type = brunoConfig.presets.requestType;\n        }\n        if (brunoConfig.presets.requestUrl?.length) {\n          presetsRequest.url = brunoConfig.presets.requestUrl;\n        }\n        brunoExtension.presets = {\n          request: presetsRequest\n        };\n      }\n\n      oc.extensions.bruno = brunoExtension;\n    }\n\n    // bruno-specific script extensions\n    if (brunoConfig.scripts?.additionalContextRoots?.length) {\n      if (!oc.extensions.bruno) {\n        oc.extensions.bruno = {};\n      }\n      (oc.extensions.bruno as any).scripts = {\n        additionalContextRoots: brunoConfig.scripts.additionalContextRoots\n      };\n    }\n\n    // bruno-specific extensions\n    if (Array.isArray(brunoConfig.openapi) && brunoConfig.openapi.length > 0) {\n      if (!oc.extensions.bruno) {\n        oc.extensions.bruno = {};\n      }\n      (oc.extensions.bruno as any).openapi = brunoConfig.openapi.map((entry: any) => ({\n        sourceUrl: entry.sourceUrl,\n        groupBy: entry.groupBy,\n        ...(entry.lastSyncDate && { lastSyncDate: entry.lastSyncDate }),\n        ...(entry.specHash && { specHash: entry.specHash }),\n        autoCheck: entry.autoCheck !== false,\n        autoCheckInterval: entry.autoCheckInterval || 5\n      }));\n    }\n\n    return stringifyYml(oc);\n  } catch (error) {\n    console.error('Error stringifying opencollection.yml:', error);\n    throw error;\n  }\n};\n\nexport default stringifyCollection;\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/stringifyEnvironment.ts",
    "content": "import type { Environment as BrunoEnvironment, EnvironmentVariable as BrunoEnvironmentVariable } from '@usebruno/schema-types/collection/environment';\nimport type { Environment } from '@opencollection/types/config/environments';\nimport type { Variable, SecretVariable } from '@opencollection/types/common/variables';\nimport { stringifyYml } from './utils';\n\nconst toOpenCollectionEnvironmentVariables = (variables: BrunoEnvironmentVariable[]): (Variable | SecretVariable)[] | undefined => {\n  if (!variables?.length) {\n    return undefined;\n  }\n\n  const ocVariables: (Variable | SecretVariable)[] = variables\n    .filter((v: BrunoEnvironmentVariable) => {\n      // todo: currently neither bru lang nor bruno app supports non-string values\n      // update this when bruno app supports non-string values\n      return typeof v.value === 'string';\n    })\n    .map((v: BrunoEnvironmentVariable): Variable | SecretVariable => {\n      if (v.secret === true) {\n        const secretVar: SecretVariable = {\n          secret: true,\n          name: v.name || ''\n        };\n        if (v.enabled === false) {\n          secretVar.disabled = true;\n        }\n        return secretVar;\n      }\n\n      const variable: Variable = {\n        name: v.name || '',\n        value: v.value as string\n      };\n\n      if (v.enabled === false) {\n        variable.disabled = true;\n      }\n\n      return variable;\n    });\n\n  return ocVariables.length > 0 ? ocVariables : undefined;\n};\n\nconst stringifyEnvironment = (environment: BrunoEnvironment): string => {\n  try {\n    const ocEnvironment: Environment = {\n      name: environment.name\n    };\n\n    if (environment.color) {\n      ocEnvironment.color = environment.color;\n    }\n\n    if (environment.variables?.length) {\n      const ocVariables = toOpenCollectionEnvironmentVariables(environment.variables);\n      if (ocVariables) {\n        ocEnvironment.variables = ocVariables;\n      }\n    }\n\n    return stringifyYml(ocEnvironment);\n  } catch (error) {\n    console.error('Error stringifying environment:', error);\n    throw error;\n  }\n};\nexport default stringifyEnvironment;\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/stringifyFolder.ts",
    "content": "import type { FolderRoot } from '@usebruno/schema-types/collection/folder';\nimport type { Folder, FolderInfo } from '@opencollection/types/collection/item';\nimport type { Variable } from '@opencollection/types/common/variables';\nimport type { Action } from '@opencollection/types/common/actions';\nimport type { Scripts } from '@opencollection/types/common/scripts';\nimport type { Auth } from '@opencollection/types/common/auth';\nimport type { HttpRequestHeader } from '@opencollection/types/requests/http';\nimport type { RequestDefaults } from '@opencollection/types/common/request-defaults';\nimport { toOpenCollectionAuth } from './common/auth';\nimport { toOpenCollectionHttpHeaders } from './common/headers';\nimport { toOpenCollectionVariables } from './common/variables';\nimport { toOpenCollectionActions } from './common/actions';\nimport { toOpenCollectionScripts } from './common/scripts';\nimport { stringifyYml } from './utils';\n\nconst hasRequestDefaults = (folderRoot: FolderRoot): boolean => {\n  const requestDefaults = folderRoot?.request;\n\n  return Boolean((requestDefaults?.headers?.length)\n    || (requestDefaults?.vars?.req?.length)\n    || (requestDefaults?.vars?.res?.length)\n    || hasRequestScripts(folderRoot)\n    || hasRequestAuth(folderRoot));\n};\n\nconst hasRequestAuth = (folderRoot: FolderRoot): boolean => {\n  return Boolean((folderRoot.request?.auth?.mode !== 'none'));\n};\n\nconst hasRequestScripts = (folderRoot: FolderRoot): boolean => {\n  return Boolean((folderRoot.request?.script?.req)\n    || (folderRoot.request?.script?.res)\n    || (folderRoot.request?.tests));\n};\n\nconst stringifyFolder = (folderRoot: FolderRoot): string => {\n  try {\n    const ocFolder: Folder = {};\n\n    // info block\n    const info: FolderInfo = {\n      name: folderRoot.meta?.name || 'Untitled Folder',\n      type: 'folder',\n      seq: folderRoot.meta?.seq || 1\n    };\n    ocFolder.info = info;\n\n    // request defaults\n    if (hasRequestDefaults(folderRoot)) {\n      ocFolder.request = {} as RequestDefaults;\n\n      // headers\n      if (folderRoot.request?.headers?.length) {\n        const ocHeaders: HttpRequestHeader[] | undefined = toOpenCollectionHttpHeaders(folderRoot.request?.headers);\n        if (ocHeaders) {\n          ocFolder.request.headers = ocHeaders;\n        }\n      }\n\n      // auth\n      if (hasRequestAuth(folderRoot)) {\n        const ocAuth: Auth | undefined = toOpenCollectionAuth(folderRoot.request?.auth);\n        if (ocAuth) {\n          ocFolder.request.auth = ocAuth;\n        }\n      }\n\n      // variables\n      if (folderRoot.request?.vars?.req?.length) {\n        const ocVariables: Variable[] | undefined = toOpenCollectionVariables(folderRoot.request?.vars);\n        if (ocVariables) {\n          ocFolder.request.variables = ocVariables;\n        }\n      }\n\n      // actions (post-response variables)\n      if (folderRoot.request?.vars?.res?.length) {\n        const ocActions: Action[] | undefined = toOpenCollectionActions(folderRoot.request?.vars?.res);\n        if (ocActions) {\n          (ocFolder.request as any).actions = ocActions;\n        }\n      }\n\n      // scripts\n      if (hasRequestScripts(folderRoot)) {\n        const ocScripts: Scripts | undefined = toOpenCollectionScripts(folderRoot?.request);\n        if (ocScripts) {\n          ocFolder.request.scripts = ocScripts;\n        }\n      }\n    }\n\n    // docs\n    if (folderRoot.docs?.trim().length) {\n      ocFolder.docs = {\n        content: folderRoot.docs,\n        type: 'text/markdown'\n      };\n    }\n\n    return stringifyYml(ocFolder);\n  } catch (error) {\n    console.error('Error stringifying folder.yml:', error);\n    throw error;\n  }\n};\nexport default stringifyFolder;\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/stringifyItem.ts",
    "content": "import type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';\nimport stringifyHttpRequest from './items/stringifyHttpRequest';\nimport stringifyGraphqlRequest from './items/stringifyGraphQLRequest';\nimport stringifyGrpcRequest from './items/stringifyGrpcRequest';\nimport stringifyWebsocketRequest from './items/stringifyWebsocketRequest';\nimport stringifyScript from './items/stringifyScript';\n\nconst stringifyItem = (item: BrunoItem): string => {\n  try {\n    switch (item.type) {\n      case 'http-request':\n        return stringifyHttpRequest(item);\n\n      case 'graphql-request':\n        return stringifyGraphqlRequest(item);\n\n      case 'grpc-request':\n        return stringifyGrpcRequest(item);\n\n      case 'ws-request':\n        return stringifyWebsocketRequest(item);\n\n      case 'js':\n        return stringifyScript(item);\n\n      case 'folder':\n        throw new Error('Folder items should be handled separately using stringifyFolder');\n\n      default:\n        throw new Error(`Unsupported item type: ${item.type}`);\n    }\n  } catch (error) {\n    console.error('Error stringifying item:', error);\n    throw error;\n  }\n};\nexport default stringifyItem;\n"
  },
  {
    "path": "packages/bruno-filestore/src/formats/yml/utils.ts",
    "content": "import * as YAML from 'yaml';\n\n// Top-level keys that should have a blank line before them\nconst BLOCK_KEYS = ['info', 'http', 'graphql', 'grpc', 'websocket', 'runtime', 'settings', 'examples', 'docs', 'items', 'request'];\n\nexport const stringifyYml = (obj: any): string => {\n  const yamlStr = YAML.stringify(obj, {\n    lineWidth: 0,\n    indent: 2,\n    minContentWidth: 0,\n    defaultStringType: 'PLAIN'\n  });\n\n  // Add blank lines before major blocks (only at the top level, not indented)\n  const lines = yamlStr.split('\\n');\n  const result: string[] = [];\n\n  for (let i = 0; i < lines.length; i++) {\n    const line = lines[i];\n    // Check if this is a top-level key (no leading whitespace) that should have a blank line\n    if (i > 0 && !line.startsWith(' ') && !line.startsWith('\\t')) {\n      const key = line.split(':')[0];\n      if (BLOCK_KEYS.includes(key)) {\n        // Add blank line before this block (if previous line isn't already blank)\n        if (result.length > 0 && result[result.length - 1].trim() !== '') {\n          result.push('');\n        }\n      }\n    }\n    result.push(line);\n  }\n\n  return result.join('\\n');\n};\n\nexport const parseYml = (ymlString: string): any => {\n  return YAML.parse(ymlString);\n};\n"
  },
  {
    "path": "packages/bruno-filestore/src/index.ts",
    "content": "import type { BrunoCollection, BrunoItem, BrunoEnvironment } from '@usebruno/schema-types';\n\nimport {\n  parseBruRequest,\n  parseBruCollection,\n  parseBruEnvironment,\n  stringifyBruRequest,\n  stringifyBruCollection,\n  stringifyBruEnvironment\n} from './formats/bru';\nimport {\n  parseYmlItem,\n  parseYmlCollection,\n  parseYmlFolder,\n  parseYmlEnvironment,\n  stringifyYmlItem,\n  stringifyYmlFolder,\n  stringifyYmlCollection,\n  stringifyYmlEnvironment\n} from './formats/yml';\nimport { dotenvToJson } from '@usebruno/lang';\nimport BruParserWorker from './workers';\nimport {\n  ParseOptions,\n  StringifyOptions,\n  CollectionFormat\n} from './types';\nimport { DEFAULT_COLLECTION_FORMAT } from './constants';\nimport { bruRequestParseAndRedactBodyData } from './formats/bru/utils/request-parse-and-redact-body-data';\n\n// request\nexport const parseRequest = (content: string, options: ParseOptions = { format: DEFAULT_COLLECTION_FORMAT }): any => {\n  if (options.format === 'bru') {\n    return parseBruRequest(content);\n  } else if (options.format === 'yml') {\n    return parseYmlItem(content);\n  }\n  throw new Error(`Unsupported format: ${options.format}`);\n};\n\nexport const parseRequestAndRedactBody = (content: string, options: ParseOptions = { format: 'bru' }): any => {\n  if (options.format === 'bru') {\n    return bruRequestParseAndRedactBodyData(content);\n  }\n  throw new Error(`Unsupported format: ${options.format}`);\n};\n\nexport const stringifyRequest = (requestObj: BrunoItem, options: StringifyOptions = { format: DEFAULT_COLLECTION_FORMAT }): string => {\n  if (options.format === 'bru') {\n    return stringifyBruRequest(requestObj);\n  } else if (options.format === 'yml') {\n    return stringifyYmlItem(requestObj);\n  }\n  throw new Error(`Unsupported format: ${options.format}`);\n};\n\n// request via worker\nlet globalWorkerInstance: BruParserWorker | null = null;\nconst getWorkerInstance = (): BruParserWorker => {\n  if (!globalWorkerInstance) {\n    globalWorkerInstance = new BruParserWorker();\n  }\n  return globalWorkerInstance;\n};\n\nexport const parseRequestViaWorker = async (content: string, options: { format: CollectionFormat; filename?: string }): Promise<any> => {\n  const fileParserWorker = getWorkerInstance();\n\n  return await fileParserWorker.parseRequest(content, options.format);\n};\n\nexport const stringifyRequestViaWorker = async (requestObj: any, options: { format: CollectionFormat }): Promise<string> => {\n  const fileParserWorker = getWorkerInstance();\n  return await fileParserWorker.stringifyRequest(requestObj, options.format);\n};\n\n// collection\nexport const parseCollection = (content: string, options: ParseOptions = { format: DEFAULT_COLLECTION_FORMAT }): any => {\n  if (options.format === 'bru') {\n    return parseBruCollection(content);\n  } else if (options.format === 'yml') {\n    return parseYmlCollection(content);\n  }\n  throw new Error(`Unsupported format: ${options.format}`);\n};\n\nexport const stringifyCollection = (collectionObj: BrunoCollection, brunoConfig: any, options: StringifyOptions = { format: DEFAULT_COLLECTION_FORMAT }): string => {\n  if (options.format === 'bru') {\n    return stringifyBruCollection(collectionObj, false);\n  } else if (options.format === 'yml') {\n    return stringifyYmlCollection(collectionObj, brunoConfig);\n  }\n  throw new Error(`Unsupported format: ${options.format}`);\n};\n\n// folder\nexport const parseFolder = (content: string, options: ParseOptions = { format: DEFAULT_COLLECTION_FORMAT }): any => {\n  if (options.format === 'bru') {\n    return parseBruCollection(content);\n  } else if (options.format === 'yml') {\n    return parseYmlFolder(content);\n  }\n  throw new Error(`Unsupported format: ${options.format}`);\n};\n\nexport const stringifyFolder = (folderObj: any, options: StringifyOptions = { format: DEFAULT_COLLECTION_FORMAT }): string => {\n  if (options.format === 'bru') {\n    return stringifyBruCollection(folderObj, true);\n  } else if (options.format === 'yml') {\n    return stringifyYmlFolder(folderObj);\n  }\n  throw new Error(`Unsupported format: ${options.format}`);\n};\n\n// environment\nexport const parseEnvironment = (content: string, options: ParseOptions = { format: DEFAULT_COLLECTION_FORMAT }): any => {\n  if (options.format === 'bru') {\n    return parseBruEnvironment(content);\n  } else if (options.format === 'yml') {\n    return parseYmlEnvironment(content);\n  }\n  throw new Error(`Unsupported format: ${options.format}`);\n};\n\nexport const stringifyEnvironment = (envObj: BrunoEnvironment, options: StringifyOptions = { format: DEFAULT_COLLECTION_FORMAT }): string => {\n  if (options.format === 'bru') {\n    return stringifyBruEnvironment(envObj);\n  } else if (options.format === 'yml') {\n    return stringifyYmlEnvironment(envObj);\n  }\n  throw new Error(`Unsupported format: ${options.format}`);\n};\n\nexport const parseDotEnv = (content: string): Record<string, string> => {\n  return dotenvToJson(content);\n};\n\nexport { BruParserWorker };\nexport * from './types';\nexport * from './constants';\n"
  },
  {
    "path": "packages/bruno-filestore/src/types/bruno-lang.d.ts",
    "content": "declare module '@usebruno/lang' {\n  export function bruToJsonV2(bruContent: string): any;\n  export function jsonToBruV2(jsonData: any): string;\n  export function bruToEnvJsonV2(bruContent: string): any;\n  export function envJsonToBruV2(jsonData: any): string;\n  export function collectionBruToJson(bruContent: string): any;\n  export function jsonToCollectionBru(jsonData: any): string;\n  export function dotenvToJson(envContent: string): Record<string, string>;\n}\n"
  },
  {
    "path": "packages/bruno-filestore/src/types.ts",
    "content": "export type CollectionFormat = 'bru' | 'yml';\n\nexport interface ParseOptions {\n  format?: CollectionFormat;\n}\n\nexport interface StringifyOptions {\n  format?: CollectionFormat;\n}\n\nexport interface WorkerTask {\n  data: any;\n  priority: number;\n  scriptPath: string;\n  taskType?: 'parse' | 'stringify';\n  resolve?: (value: any) => void;\n  reject?: (reason?: any) => void;\n}\n\nexport interface Lane {\n  maxSize: number;\n}\n"
  },
  {
    "path": "packages/bruno-filestore/src/utils/index.ts",
    "content": "const { customAlphabet } = require('nanoid');\n\nexport const isString = (value: unknown): value is string => typeof value === 'string';\n\nexport const isNumber = (value: unknown): value is number => typeof value === 'number';\n\nexport const isNonEmptyString = (value: unknown): value is string => isString(value) && value.trim().length > 0;\n\nexport const ensureString = (value: unknown, fallback: string = ''): string => {\n  if (value === null || value === undefined) {\n    return fallback;\n  }\n  if (typeof value === 'string') {\n    return value;\n  }\n  return String(value);\n};\n\nexport const uuid = () => {\n  // https://github.com/ai/nanoid/blob/main/url-alphabet/index.js\n  const urlAlphabet = 'useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict';\n  const customNanoId = customAlphabet(urlAlphabet, 21);\n\n  return customNanoId();\n};\n"
  },
  {
    "path": "packages/bruno-filestore/src/workers/WorkerQueue/index.ts",
    "content": "import { Worker } from 'node:worker_threads';\n\ninterface QueuedTask {\n  priority: number;\n  scriptPath: string;\n  data: any;\n  taskType: 'parse' | 'stringify';\n  resolve?: (value: any) => void;\n  reject?: (reason?: any) => void;\n}\n\nclass WorkerQueue {\n  private queue: QueuedTask[];\n  private isProcessing: boolean;\n  private workers: Record<string, Worker>;\n\n  constructor() {\n    this.queue = [];\n    this.isProcessing = false;\n    this.workers = {};\n  }\n\n  async getWorkerForScriptPath(scriptPath: string) {\n    if (!this.workers) this.workers = {};\n    let worker = this.workers[scriptPath];\n    if (!worker || worker.threadId === -1) {\n      this.workers[scriptPath] = worker = new Worker(scriptPath);\n    }\n    return worker;\n  }\n\n  async enqueue(task: QueuedTask) {\n    const { priority, scriptPath, data, taskType } = task;\n\n    return new Promise((resolve, reject) => {\n      this.queue.push({ priority, scriptPath, data, taskType, resolve, reject });\n      this.queue?.sort((taskX, taskY) => taskX?.priority - taskY?.priority);\n      this.processQueue();\n    });\n  }\n\n  async processQueue() {\n    if (this.isProcessing || this.queue.length === 0) {\n      return;\n    }\n\n    this.isProcessing = true;\n    const { scriptPath, data, taskType, resolve, reject } = this.queue.shift() as QueuedTask;\n\n    try {\n      const result = await this.runWorker({ scriptPath, data, taskType });\n      resolve?.(result);\n    } catch (error) {\n      reject?.(error);\n    } finally {\n      this.isProcessing = false;\n      this.processQueue();\n    }\n  }\n\n  async runWorker({ scriptPath, data, taskType }: { scriptPath: string; data: any; taskType: 'parse' | 'stringify' }) {\n    return new Promise(async (resolve, reject) => {\n      let worker = await this.getWorkerForScriptPath(scriptPath);\n\n      const messageHandler = (data: any) => {\n        worker.off('message', messageHandler);\n        worker.off('error', errorHandler);\n        worker.off('exit', exitHandler);\n\n        if (data?.error) {\n          reject(new Error(data?.error));\n        } else {\n          resolve(data);\n        }\n      };\n\n      const errorHandler = (error: Error) => {\n        worker.off('message', messageHandler);\n        worker.off('error', errorHandler);\n        worker.off('exit', exitHandler);\n        reject(error);\n      };\n\n      const exitHandler = (code: number) => {\n        worker.off('message', messageHandler);\n        worker.off('error', errorHandler);\n        worker.off('exit', exitHandler);\n        // Remove dead worker from cache\n        delete this.workers[scriptPath];\n        reject(new Error(`Worker stopped with exit code ${code}`));\n      };\n\n      worker.on('message', messageHandler);\n      worker.on('error', errorHandler);\n      worker.on('exit', exitHandler);\n\n      worker.postMessage({ taskType, data });\n    });\n  }\n\n  async cleanup() {\n    const promises = Object.values(this.workers).map((worker) => {\n      if (worker.threadId !== -1) {\n        return worker.terminate();\n      }\n      return Promise.resolve();\n    });\n\n    await Promise.allSettled(promises);\n    this.workers = {};\n  }\n}\n\nexport default WorkerQueue;\n"
  },
  {
    "path": "packages/bruno-filestore/src/workers/index.ts",
    "content": "import WorkerQueue from './WorkerQueue';\nimport { Lane, CollectionFormat } from '../types';\nimport { DEFAULT_COLLECTION_FORMAT } from '../constants';\nimport path from 'node:path';\n\nconst sizeInMB = (size: number): number => {\n  return size / (1024 * 1024);\n};\n\nconst getSize = (data: any): number => {\n  return sizeInMB(typeof data === 'string' ? Buffer.byteLength(data, 'utf8') : Buffer.byteLength(JSON.stringify(data), 'utf8'));\n};\n\n/**\n * Lanes are used to determine which worker queue to use based on the size of the data.\n *\n * The first lane is for smaller files (<0.1MB), the second lane is for larger files (>=0.1MB).\n * This helps with parsing performance.\n */\nconst LANES: Lane[] = [{\n  maxSize: 0.005\n}, {\n  maxSize: 0.1\n}, {\n  maxSize: 1\n}, {\n  maxSize: 10\n}, {\n  maxSize: 100\n}];\n\ninterface WorkerQueueWithSize {\n  maxSize: number;\n  workerQueue: WorkerQueue;\n\n}\n\nclass BruParserWorker {\n  private workerQueues: WorkerQueueWithSize[];\n\n  constructor() {\n    this.workerQueues = LANES?.map((lane) => ({\n      maxSize: lane?.maxSize,\n      workerQueue: new WorkerQueue()\n    }));\n  }\n\n  private getWorkerQueue(size: number): WorkerQueue {\n    // Find the first queue that can handle the given size\n    // or fallback to the last queue for largest files\n    const queueForSize = this.workerQueues.find((queue) =>\n      queue.maxSize >= size\n    );\n\n    return queueForSize?.workerQueue ?? this.workerQueues[this.workerQueues.length - 1].workerQueue;\n  }\n\n  private async enqueueTask({ data, taskType, format = DEFAULT_COLLECTION_FORMAT }: { data: any; taskType: 'parse' | 'stringify'; format?: CollectionFormat }): Promise<any> {\n    const size = getSize(data);\n    const workerQueue = this.getWorkerQueue(size);\n    const workerScriptPath = path.join(__dirname, './workers/worker-script.js');\n\n    return workerQueue.enqueue({\n      data: { data, format },\n      priority: size,\n      scriptPath: workerScriptPath,\n      taskType\n    });\n  }\n\n  async parseRequest(data: any, format: CollectionFormat = DEFAULT_COLLECTION_FORMAT): Promise<any> {\n    return this.enqueueTask({ data, taskType: 'parse', format });\n  }\n\n  async stringifyRequest(data: any, format: CollectionFormat = DEFAULT_COLLECTION_FORMAT): Promise<any> {\n    return this.enqueueTask({ data, taskType: 'stringify', format });\n  }\n\n  async cleanup(): Promise<void> {\n    const cleanupPromises = this.workerQueues.map(({ workerQueue }) =>\n      workerQueue.cleanup()\n    );\n    await Promise.allSettled(cleanupPromises);\n  }\n}\n\nexport default BruParserWorker;\n"
  },
  {
    "path": "packages/bruno-filestore/src/workers/worker-script.ts",
    "content": "import { parentPort } from 'node:worker_threads';\nimport { parseBruRequest, stringifyBruRequest } from '../formats/bru';\nimport { parseYmlItem, stringifyYmlItem } from '../formats/yml';\nimport { CollectionFormat } from '../types';\nimport { DEFAULT_COLLECTION_FORMAT } from '../constants';\n\ninterface WorkerMessage {\n  taskType: 'parse' | 'stringify';\n  data: {\n    data: any;\n    format?: CollectionFormat;\n  };\n}\n\nparentPort?.on('message', async (message: WorkerMessage) => {\n  try {\n    const { taskType, data: messageData } = message;\n    const { data, format = DEFAULT_COLLECTION_FORMAT } = messageData;\n    let result: any;\n\n    if (taskType === 'parse') {\n      if (format === 'yml') {\n        result = parseYmlItem(data);\n      } else {\n        result = parseBruRequest(data);\n      }\n    } else if (taskType === 'stringify') {\n      if (format === 'yml') {\n        result = stringifyYmlItem(data);\n      } else {\n        result = stringifyBruRequest(data);\n      }\n    } else {\n      throw new Error(`Unknown task type: ${taskType}`);\n    }\n\n    parentPort?.postMessage(result);\n  } catch (error: any) {\n    console.error('Worker error:', error);\n    parentPort?.postMessage({ error: error?.message });\n  }\n});\n"
  },
  {
    "path": "packages/bruno-filestore/test-results/.last-run.json",
    "content": "{\n  \"status\": \"failed\",\n  \"failedTests\": []\n}"
  },
  {
    "path": "packages/bruno-filestore/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"module\": \"ESNext\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\",\n    \"resolveJsonModule\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"moduleResolution\": \"node\",\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"allowJs\": true,\n    \"checkJs\": false,\n    \"types\": [\"node\"],\n    \"lib\": [\"ES2020\"],\n    \"typeRoots\": [\"../../node_modules/@types\", \"./node_modules/@types\", \"./src/types\"],\n    \"baseUrl\": \"../..\",\n    \"paths\": {\n      \"@usebruno/schema-types\": [\"packages/bruno-schema-types/dist/index.d.ts\"],\n      \"@usebruno/schema-types/*\": [\"packages/bruno-schema-types/dist/*\"],\n      \"@opencollection/types\": [\"node_modules/@opencollection/types/dist/opencollection.d.ts\"],\n      \"@opencollection/types/*\": [\"node_modules/@opencollection/types/dist/*\"]\n    }\n  },\n  \"include\": [\"src/**/*.ts\", \"src/**/*.tsx\", \"src/**/*.js\", \"src/**/*.d.ts\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}"
  },
  {
    "path": "packages/bruno-graphql-docs/.gitignore",
    "content": "# dependencies\nnode_modules\nyarn.lock\npnpm-lock.yaml\npackage-lock.json\n.pnp\n.pnp.js\n\n# testing\ncoverage\n\n# production\ndist\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "packages/bruno-graphql-docs/license.md",
    "content": "\nMIT License\n\nCopyright (c) 2022 Anoop M D, Anusree P S and Contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "packages/bruno-graphql-docs/package.json",
    "content": "{\n  \"name\": \"@usebruno/graphql-docs\",\n  \"version\": \"0.1.0\",\n  \"license\" : \"MIT\",\n  \"main\": \"dist/cjs/index.js\",\n  \"module\": \"dist/esm/index.js\",\n  \"files\": [\n    \"dist\",\n    \"package.json\"\n  ],\n  \"scripts\": {\n    \"build\": \"rollup -c\",\n    \"watch\": \"rollup -c -w\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-commonjs\": \"^23.0.2\",\n    \"@rollup/plugin-node-resolve\": \"^15.0.1\",\n    \"@rollup/plugin-typescript\": \"^9.0.2\",\n    \"@types/markdown-it\": \"^12.2.3\",\n    \"@types/react\": \"^18.0.25\",\n    \"graphql\": \"^16.6.0\",\n    \"markdown-it\": \"^13.0.1\",\n    \"postcss\": \"8.4.47\",\n    \"react\": \"19.0.0\",\n    \"react-dom\": \"18.2.0\",\n    \"rollup\":\"3.29.5\",\n    \"rollup-plugin-dts\": \"^5.0.0\",\n    \"rollup-plugin-peer-deps-external\": \"^2.2.4\",\n    \"rollup-plugin-postcss\": \"^4.0.2\",\n    \"rollup-plugin-terser\": \"^7.0.2\",\n    \"typescript\": \"^4.8.4\"\n  },\n  \"peerDependencies\": {\n    \"graphql\": \"^16.6.0\",\n    \"markdown-it\": \"^13.0.1\"\n  },\n  \"overrides\": {\n    \"rollup\":\"3.29.5\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-graphql-docs/readme.md",
    "content": "# bruno-graphql-docs\n\nStandalone graphql docs explorer module forked from [graphiql](https://github.com/graphql/graphiql)\n\n### Publish to Npm Registry\n```bash\nnpm publish --access=public\n```"
  },
  {
    "path": "packages/bruno-graphql-docs/rollup.config.js",
    "content": "const { nodeResolve } = require('@rollup/plugin-node-resolve');\nconst commonjs = require('@rollup/plugin-commonjs');\nconst typescript = require('@rollup/plugin-typescript');\nconst dts = require('rollup-plugin-dts');\nconst postcss = require('rollup-plugin-postcss');\nconst { terser } = require('rollup-plugin-terser');\nconst peerDepsExternal = require('rollup-plugin-peer-deps-external');\n\nconst packageJson = require('./package.json');\n\nmodule.exports = [\n  {\n    input: 'src/index.ts',\n    output: [\n      {\n        file: packageJson.main,\n        format: 'cjs',\n        sourcemap: true\n      },\n      {\n        file: packageJson.module,\n        format: 'esm',\n        sourcemap: true\n      }\n    ],\n    plugins: [\n      postcss({\n        minimize: true,\n        extensions: ['.css'],\n        extract: true\n      }),\n      peerDepsExternal(),\n      nodeResolve({\n        extensions: ['.css']\n      }),\n      commonjs(),\n      typescript({ tsconfig: './tsconfig.json' }),\n      terser()\n    ],\n    external: ['react', 'react-dom', 'index.css']\n  },\n  {\n    input: 'dist/esm/index.d.ts',\n    external: [/\\.css$/],\n    output: [{ file: 'dist/index.d.ts', format: 'esm' }],\n    plugins: [dts.default()]\n  }\n];\n"
  },
  {
    "path": "packages/bruno-graphql-docs/src/components/DocExplorer/Argument.tsx",
    "content": "/**\n *  Copyright (c) 2021 GraphQL Contributors.\n *\n *  This source code is licensed under the MIT license found in the\n *  LICENSE file in the root directory of this source tree.\n */\n\nimport React from 'react';\nimport { GraphQLArgument } from 'graphql';\nimport TypeLink from './TypeLink';\nimport DefaultValue from './DefaultValue';\nimport { OnClickTypeFunction } from './types';\n\ntype ArgumentProps = {\n  arg: GraphQLArgument;\n  onClickType: OnClickTypeFunction;\n  showDefaultValue?: boolean;\n};\n\nexport default function Argument({ arg, onClickType, showDefaultValue }: ArgumentProps) {\n  return (\n    <span className=\"arg\">\n      <span className=\"arg-name\">{arg.name}</span>\n      {': '}\n      <TypeLink type={arg.type} onClick={onClickType} />\n      {showDefaultValue !== false && <DefaultValue field={arg} />}\n    </span>\n  );\n}\n"
  },
  {
    "path": "packages/bruno-graphql-docs/src/components/DocExplorer/DefaultValue.tsx",
    "content": "/**\n *  Copyright (c) 2021 GraphQL Contributors.\n *\n *  This source code is licensed under the MIT license found in the\n *  LICENSE file in the root directory of this source tree.\n */\n\nimport React from 'react';\nimport { astFromValue, print, ValueNode } from 'graphql';\nimport { FieldType } from './types';\n\nconst printDefault = (ast?: ValueNode | null): string => {\n  if (!ast) {\n    return '';\n  }\n  return print(ast);\n};\n\ntype DefaultValueProps = {\n  field: FieldType;\n};\n\nexport default function DefaultValue({ field }: DefaultValueProps) {\n  // field.defaultValue could be null or false, so be careful here!\n  if ('defaultValue' in field && field.defaultValue !== undefined) {\n    return (\n      <span>\n        {' = '}\n        <span className=\"arg-default-value\">{printDefault(astFromValue(field.defaultValue, field.type))}</span>\n      </span>\n    );\n  }\n\n  return null;\n}\n"
  },
  {
    "path": "packages/bruno-graphql-docs/src/components/DocExplorer/Directive.tsx",
    "content": "/**\n *  Copyright (c) 2021 GraphQL Contributors.\n *\n *  This source code is licensed under the MIT license found in the\n *  LICENSE file in the root directory of this source tree.\n */\n\nimport React from 'react';\nimport { DirectiveNode } from 'graphql';\n\ntype DirectiveProps = {\n  directive: DirectiveNode;\n};\n\nexport default function Directive({ directive }: DirectiveProps) {\n  return (\n    <span className=\"doc-category-item\" id={directive.name.value}>\n      {'@'}\n      {directive.name.value}\n    </span>\n  );\n}\n"
  },
  {
    "path": "packages/bruno-graphql-docs/src/components/DocExplorer/FieldDoc.tsx",
    "content": "/**\n *  Copyright (c) 2021 GraphQL Contributors.\n *\n *  This source code is licensed under the MIT license found in the\n *  LICENSE file in the root directory of this source tree.\n */\n\nimport React from 'react';\nimport Argument from './Argument';\nimport Directive from './Directive';\nimport MarkdownContent from './MarkdownContent';\nimport TypeLink from './TypeLink';\nimport { GraphQLArgument, DirectiveNode } from 'graphql';\nimport { OnClickTypeFunction, FieldType } from './types';\n\ntype FieldDocProps = {\n  field?: FieldType;\n  onClickType: OnClickTypeFunction;\n};\n\nexport default function FieldDoc({ field, onClickType }: FieldDocProps) {\n  const [showDeprecated, handleShowDeprecated] = React.useState(false);\n  let argsDef;\n  let deprecatedArgsDef;\n  if (field && 'args' in field && field.args.length > 0) {\n    argsDef = (\n      <div id=\"doc-args\" className=\"doc-category\">\n        <div className=\"doc-category-title\">{'arguments'}</div>\n        {field.args\n          .filter((arg) => !arg.deprecationReason)\n          .map((arg: GraphQLArgument) => (\n            <div key={arg.name} className=\"doc-category-item\">\n              <div>\n                <Argument arg={arg} onClickType={onClickType} />\n              </div>\n              <MarkdownContent className=\"doc-value-description\" markdown={arg.description} />\n              {arg && 'deprecationReason' in arg && (\n                <MarkdownContent className=\"doc-deprecation\" markdown={arg?.deprecationReason} />\n              )}\n            </div>\n          ))}\n      </div>\n    );\n    const deprecatedArgs = field.args.filter((arg) => Boolean(arg.deprecationReason));\n    if (deprecatedArgs.length > 0) {\n      deprecatedArgsDef = (\n        <div id=\"doc-deprecated-args\" className=\"doc-category\">\n          <div className=\"doc-category-title\">{'deprecated arguments'}</div>\n          {!showDeprecated ? (\n            <button className=\"show-btn\" onClick={() => handleShowDeprecated(!showDeprecated)}>\n              {'Show deprecated arguments...'}\n            </button>\n          ) : (\n            deprecatedArgs.map((arg, i) => (\n              <div key={i}>\n                <div>\n                  <Argument arg={arg} onClickType={onClickType} />\n                </div>\n                <MarkdownContent className=\"doc-value-description\" markdown={arg.description} />\n                {arg && 'deprecationReason' in arg && (\n                  <MarkdownContent className=\"doc-deprecation\" markdown={arg?.deprecationReason} />\n                )}\n              </div>\n            ))\n          )}\n        </div>\n      );\n    }\n  }\n\n  let directivesDef;\n  if (field && field.astNode && field.astNode.directives && field.astNode.directives.length > 0) {\n    directivesDef = (\n      <div id=\"doc-directives\" className=\"doc-category\">\n        <div className=\"doc-category-title\">{'directives'}</div>\n        {field.astNode.directives.map((directive: DirectiveNode) => (\n          <div key={directive.name.value} className=\"doc-category-item\">\n            <div>\n              <Directive directive={directive} />\n            </div>\n          </div>\n        ))}\n      </div>\n    );\n  }\n\n  return (\n    <div>\n      <MarkdownContent className=\"doc-type-description\" markdown={field?.description || 'No Description'} />\n      {field && 'deprecationReason' in field && (\n        <MarkdownContent className=\"doc-deprecation\" markdown={field?.deprecationReason} />\n      )}\n      <div className=\"doc-category\">\n        <div className=\"doc-category-title\">{'type'}</div>\n        <TypeLink type={field?.type} onClick={onClickType} />\n      </div>\n      {argsDef}\n      {directivesDef}\n      {deprecatedArgsDef}\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/bruno-graphql-docs/src/components/DocExplorer/MarkdownContent.tsx",
    "content": "/**\n *  Copyright (c) 2021 GraphQL Contributors.\n *\n *  This source code is licensed under the MIT license found in the\n *  LICENSE file in the root directory of this source tree.\n */\n\nimport React from 'react';\nimport MD from 'markdown-it';\n\ntype Maybe<T> = T | null | undefined;\n\nconst md = new MD({\n  // render urls as links, à la github-flavored markdown\n  linkify: true\n});\n\ntype MarkdownContentProps = {\n  markdown?: Maybe<string>;\n  className?: string;\n};\n\nexport default function MarkdownContent({ markdown, className }: MarkdownContentProps) {\n  if (!markdown) {\n    return <div />;\n  }\n\n  return <div className={className} dangerouslySetInnerHTML={{ __html: md.render(markdown) }} />;\n}\n"
  },
  {
    "path": "packages/bruno-graphql-docs/src/components/DocExplorer/SchemaDoc.tsx",
    "content": "/**\n *  Copyright (c) 2021 GraphQL Contributors.\n *\n *  This source code is licensed under the MIT license found in the\n *  LICENSE file in the root directory of this source tree.\n */\n\nimport React from 'react';\nimport TypeLink from './TypeLink';\nimport MarkdownContent from './MarkdownContent';\nimport { GraphQLSchema } from 'graphql';\nimport { OnClickTypeFunction } from './types';\n\ntype SchemaDocProps = {\n  schema: GraphQLSchema;\n  onClickType: OnClickTypeFunction;\n};\n\n// Render the top level Schema\nexport default function SchemaDoc({ schema, onClickType }: SchemaDocProps) {\n  const queryType = schema.getQueryType();\n  const mutationType = schema.getMutationType && schema.getMutationType();\n  const subscriptionType = schema.getSubscriptionType && schema.getSubscriptionType();\n\n  return (\n    <div>\n      <MarkdownContent\n        className=\"doc-type-description\"\n        markdown={schema.description || 'A GraphQL schema provides a root type for each kind of operation.'}\n      />\n      <div className=\"doc-category\">\n        <div className=\"doc-category-title\">{'root types'}</div>\n        <div className=\"doc-category-item\">\n          <span className=\"keyword\">{'query'}</span>\n          {': '}\n          <TypeLink type={queryType} onClick={onClickType} />\n        </div>\n        {mutationType && (\n          <div className=\"doc-category-item\">\n            <span className=\"keyword\">{'mutation'}</span>\n            {': '}\n            <TypeLink type={mutationType} onClick={onClickType} />\n          </div>\n        )}\n        {subscriptionType && (\n          <div className=\"doc-category-item\">\n            <span className=\"keyword\">{'subscription'}</span>\n            {': '}\n            <TypeLink type={subscriptionType} onClick={onClickType} />\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/bruno-graphql-docs/src/components/DocExplorer/SearchBox.tsx",
    "content": "/**\n *  Copyright (c) 2021 GraphQL Contributors.\n *\n *  This source code is licensed under the MIT license found in the\n *  LICENSE file in the root directory of this source tree.\n */\n\nimport React, { ChangeEventHandler } from 'react';\n\nimport debounce from '../../utility/debounce';\n\ntype OnSearchFn = (value: string) => void;\n\ntype SearchBoxProps = {\n  value?: string;\n  placeholder: string;\n  onSearch: OnSearchFn;\n};\n\ntype SearchBoxState = {\n  value: string;\n};\n\nexport default class SearchBox extends React.Component<SearchBoxProps, SearchBoxState> {\n  debouncedOnSearch: OnSearchFn;\n\n  constructor(props: SearchBoxProps) {\n    super(props);\n    this.state = { value: props.value || '' };\n    this.debouncedOnSearch = debounce(200, this.props.onSearch);\n  }\n\n  render() {\n    return (\n      <label className=\"search-box\">\n        <div className=\"search-box-icon\" aria-hidden=\"true\">\n          {'\\u26b2'}\n        </div>\n        <input\n          value={this.state.value}\n          onChange={this.handleChange}\n          type=\"text\"\n          placeholder={this.props.placeholder}\n          aria-label={this.props.placeholder}\n        />\n        {this.state.value && (\n          <button className=\"search-box-clear\" onClick={this.handleClear} aria-label=\"Clear search input\">\n            {'\\u2715'}\n          </button>\n        )}\n      </label>\n    );\n  }\n\n  handleChange: ChangeEventHandler<HTMLInputElement> = (event) => {\n    const value = event.currentTarget.value;\n    this.setState({ value });\n    this.debouncedOnSearch(value);\n  };\n\n  handleClear = () => {\n    this.setState({ value: '' });\n    this.props.onSearch('');\n  };\n}\n"
  },
  {
    "path": "packages/bruno-graphql-docs/src/components/DocExplorer/SearchResults.tsx",
    "content": "/**\n *  Copyright (c) 2021 GraphQL Contributors.\n *\n *  This source code is licensed under the MIT license found in the\n *  LICENSE file in the root directory of this source tree.\n */\n\nimport React, { ReactNode } from 'react';\nimport { GraphQLSchema, GraphQLNamedType } from 'graphql';\n\nimport Argument from './Argument';\nimport TypeLink from './TypeLink';\nimport { OnClickFieldFunction, OnClickTypeFunction } from './types';\n\ntype SearchResultsProps = {\n  schema: GraphQLSchema;\n  withinType?: GraphQLNamedType;\n  searchValue: string;\n  onClickType: OnClickTypeFunction;\n  onClickField: OnClickFieldFunction;\n};\n\nexport default class SearchResults extends React.Component<SearchResultsProps, {}> {\n  shouldComponentUpdate(nextProps: SearchResultsProps) {\n    return this.props.schema !== nextProps.schema || this.props.searchValue !== nextProps.searchValue;\n  }\n\n  render() {\n    const searchValue = this.props.searchValue;\n    const withinType = this.props.withinType;\n    const schema = this.props.schema;\n    const onClickType = this.props.onClickType;\n    const onClickField = this.props.onClickField;\n\n    const matchedWithin: ReactNode[] = [];\n    const matchedTypes: ReactNode[] = [];\n    const matchedFields: ReactNode[] = [];\n\n    const typeMap = schema.getTypeMap();\n    let typeNames = Object.keys(typeMap);\n\n    // Move the within type name to be the first searched.\n    if (withinType) {\n      typeNames = typeNames.filter((n) => n !== withinType.name);\n      typeNames.unshift(withinType.name);\n    }\n\n    for (const typeName of typeNames) {\n      if (matchedWithin.length + matchedTypes.length + matchedFields.length >= 100) {\n        break;\n      }\n\n      const type = typeMap[typeName];\n      if (withinType !== type && isMatch(typeName, searchValue)) {\n        matchedTypes.push(\n          <div className=\"doc-category-item\" key={typeName}>\n            <TypeLink type={type} onClick={onClickType} />\n          </div>\n        );\n      }\n\n      if (type && 'getFields' in type) {\n        const fields = type.getFields();\n        Object.keys(fields).forEach((fieldName) => {\n          const field = fields[fieldName];\n          let matchingArgs;\n\n          if (!isMatch(fieldName, searchValue)) {\n            if ('args' in field && field.args.length) {\n              matchingArgs = field.args.filter((arg) => isMatch(arg.name, searchValue));\n              if (matchingArgs.length === 0) {\n                return;\n              }\n            } else {\n              return;\n            }\n          }\n\n          const match = (\n            <div className=\"doc-category-item\" key={typeName + '.' + fieldName}>\n              {withinType !== type && [<TypeLink key=\"type\" type={type} onClick={onClickType} />, '.']}\n              <a className=\"field-name\" onClick={(event) => onClickField(field, type, event)}>\n                {field.name}\n              </a>\n              {matchingArgs && [\n                '(',\n                <span key=\"args\">\n                  {matchingArgs.map((arg) => (\n                    <Argument key={arg.name} arg={arg} onClickType={onClickType} showDefaultValue={false} />\n                  ))}\n                </span>,\n                ')'\n              ]}\n            </div>\n          );\n\n          if (withinType === type) {\n            matchedWithin.push(match);\n          } else {\n            matchedFields.push(match);\n          }\n        });\n      }\n    }\n\n    if (matchedWithin.length + matchedTypes.length + matchedFields.length === 0) {\n      return <span className=\"doc-alert-text\">{'No results found.'}</span>;\n    }\n\n    if (withinType && matchedTypes.length + matchedFields.length > 0) {\n      return (\n        <div>\n          {matchedWithin}\n          <div className=\"doc-category\">\n            <div className=\"doc-category-title\">{'other results'}</div>\n            {matchedTypes}\n            {matchedFields}\n          </div>\n        </div>\n      );\n    }\n\n    return (\n      <div className=\"doc-search-items\">\n        {matchedWithin}\n        {matchedTypes}\n        {matchedFields}\n      </div>\n    );\n  }\n}\n\nfunction isMatch(sourceText: string, searchValue: string) {\n  try {\n    const escaped = searchValue.replace(/[^_0-9A-Za-z]/g, (ch) => '\\\\' + ch);\n    return sourceText.search(new RegExp(escaped, 'i')) !== -1;\n  } catch (e) {\n    return sourceText.toLowerCase().indexOf(searchValue.toLowerCase()) !== -1;\n  }\n}\n"
  },
  {
    "path": "packages/bruno-graphql-docs/src/components/DocExplorer/TypeDoc.tsx",
    "content": "/**\n *  Copyright (c) 2021 GraphQL Contributors.\n *\n *  This source code is licensed under the MIT license found in the\n *  LICENSE file in the root directory of this source tree.\n */\n\nimport React, { ReactNode } from 'react';\nimport {\n  GraphQLSchema,\n  GraphQLObjectType,\n  GraphQLInterfaceType,\n  GraphQLUnionType,\n  GraphQLEnumType,\n  GraphQLType,\n  GraphQLEnumValue\n} from 'graphql';\n\nimport Argument from './Argument';\nimport MarkdownContent from './MarkdownContent';\nimport TypeLink from './TypeLink';\nimport DefaultValue from './DefaultValue';\nimport { FieldType, OnClickTypeFunction, OnClickFieldFunction } from './types';\n\ntype TypeDocProps = {\n  schema: GraphQLSchema;\n  type: GraphQLType;\n  onClickType: OnClickTypeFunction;\n  onClickField: OnClickFieldFunction;\n};\n\ntype TypeDocState = {\n  showDeprecated: boolean;\n};\n\nexport default class TypeDoc extends React.Component<TypeDocProps, TypeDocState> {\n  constructor(props: TypeDocProps) {\n    super(props);\n    this.state = { showDeprecated: false };\n  }\n\n  shouldComponentUpdate(nextProps: TypeDocProps, nextState: TypeDocState) {\n    return (\n      this.props.type !== nextProps.type ||\n      this.props.schema !== nextProps.schema ||\n      this.state.showDeprecated !== nextState.showDeprecated\n    );\n  }\n\n  render() {\n    const schema = this.props.schema;\n    const type = this.props.type;\n    const onClickType = this.props.onClickType;\n    const onClickField = this.props.onClickField;\n\n    let typesTitle: string | null = null;\n    let types: readonly (GraphQLObjectType | GraphQLInterfaceType)[] = [];\n    if (type instanceof GraphQLUnionType) {\n      typesTitle = 'possible types';\n      types = schema.getPossibleTypes(type);\n    } else if (type instanceof GraphQLInterfaceType) {\n      typesTitle = 'implementations';\n      types = schema.getPossibleTypes(type);\n    } else if (type instanceof GraphQLObjectType) {\n      typesTitle = 'implements';\n      types = type.getInterfaces();\n    }\n\n    let typesDef;\n    if (types && types.length > 0) {\n      typesDef = (\n        <div id=\"doc-types\" className=\"doc-category\">\n          <div className=\"doc-category-title\">{typesTitle}</div>\n          {types.map((subtype) => (\n            <div key={subtype.name} className=\"doc-category-item\">\n              <TypeLink type={subtype} onClick={onClickType} />\n            </div>\n          ))}\n        </div>\n      );\n    }\n\n    // InputObject and Object\n    let fieldsDef;\n    let deprecatedFieldsDef;\n    if (type && 'getFields' in type) {\n      const fieldMap = type.getFields();\n      const fields = Object.keys(fieldMap).map((name) => fieldMap[name]);\n      fieldsDef = (\n        <div id=\"doc-fields\" className=\"doc-category\">\n          <div className=\"doc-category-title\">{'fields'}</div>\n          {fields\n            .filter((field) => !field.deprecationReason)\n            .map((field) => (\n              <Field key={field.name} type={type} field={field} onClickType={onClickType} onClickField={onClickField} />\n            ))}\n        </div>\n      );\n\n      const deprecatedFields = fields.filter((field) => Boolean(field.deprecationReason));\n      if (deprecatedFields.length > 0) {\n        deprecatedFieldsDef = (\n          <div id=\"doc-deprecated-fields\" className=\"doc-category\">\n            <div className=\"doc-category-title\">{'deprecated fields'}</div>\n            {!this.state.showDeprecated ? (\n              <button className=\"show-btn\" onClick={this.handleShowDeprecated}>\n                {'Show deprecated fields...'}\n              </button>\n            ) : (\n              deprecatedFields.map((field) => (\n                <Field\n                  key={field.name}\n                  type={type}\n                  field={field}\n                  onClickType={onClickType}\n                  onClickField={onClickField}\n                />\n              ))\n            )}\n          </div>\n        );\n      }\n    }\n\n    let valuesDef: ReactNode;\n    let deprecatedValuesDef: ReactNode;\n    if (type instanceof GraphQLEnumType) {\n      const values = type.getValues();\n      valuesDef = (\n        <div className=\"doc-category\">\n          <div className=\"doc-category-title\">{'values'}</div>\n          {values\n            .filter((value) => Boolean(!value.deprecationReason))\n            .map((value) => (\n              <EnumValue key={value.name} value={value} />\n            ))}\n        </div>\n      );\n\n      const deprecatedValues = values.filter((value) => Boolean(value.deprecationReason));\n      if (deprecatedValues.length > 0) {\n        deprecatedValuesDef = (\n          <div className=\"doc-category\">\n            <div className=\"doc-category-title\">{'deprecated values'}</div>\n            {!this.state.showDeprecated ? (\n              <button className=\"show-btn\" onClick={this.handleShowDeprecated}>\n                {'Show deprecated values...'}\n              </button>\n            ) : (\n              deprecatedValues.map((value) => <EnumValue key={value.name} value={value} />)\n            )}\n          </div>\n        );\n      }\n    }\n\n    return (\n      <div>\n        <MarkdownContent\n          className=\"doc-type-description\"\n          markdown={('description' in type && type.description) || 'No Description'}\n        />\n        {type instanceof GraphQLObjectType && typesDef}\n        {fieldsDef}\n        {deprecatedFieldsDef}\n        {valuesDef}\n        {deprecatedValuesDef}\n        {!(type instanceof GraphQLObjectType) && typesDef}\n      </div>\n    );\n  }\n\n  handleShowDeprecated = () => this.setState({ showDeprecated: true });\n}\n\ntype FieldProps = {\n  type: GraphQLType;\n  field: FieldType;\n  onClickType: OnClickTypeFunction;\n  onClickField: OnClickFieldFunction;\n};\n\nfunction Field({ type, field, onClickType, onClickField }: FieldProps) {\n  return (\n    <div className=\"doc-category-item\">\n      <a className=\"field-name\" onClick={(event) => onClickField(field, type, event)}>\n        {field.name}\n      </a>\n      {'args' in field &&\n        field.args &&\n        field.args.length > 0 && [\n          '(',\n          <span key=\"args\">\n            {field.args\n              .filter((arg) => !arg.deprecationReason)\n              .map((arg) => (\n                <Argument key={arg.name} arg={arg} onClickType={onClickType} />\n              ))}\n          </span>,\n          ')'\n        ]}\n      {': '}\n      <TypeLink type={field.type} onClick={onClickType} />\n      <DefaultValue field={field} />\n      {field.description && <MarkdownContent className=\"field-short-description\" markdown={field.description} />}\n      {'deprecationReason' in field && field.deprecationReason && (\n        <MarkdownContent className=\"doc-deprecation\" markdown={field.deprecationReason} />\n      )}\n    </div>\n  );\n}\n\ntype EnumValue = {\n  value: GraphQLEnumValue;\n};\n\nfunction EnumValue({ value }: EnumValue) {\n  return (\n    <div className=\"doc-category-item\">\n      <div className=\"enum-value\">{value.name}</div>\n      <MarkdownContent className=\"doc-value-description\" markdown={value.description} />\n      {value.deprecationReason && <MarkdownContent className=\"doc-deprecation\" markdown={value.deprecationReason} />}\n    </div>\n  );\n}\n"
  },
  {
    "path": "packages/bruno-graphql-docs/src/components/DocExplorer/TypeLink.tsx",
    "content": "/**\n *  Copyright (c) 2021 GraphQL Contributors.\n *\n *  This source code is licensed under the MIT license found in the\n *  LICENSE file in the root directory of this source tree.\n */\n\nimport React from 'react';\nimport { GraphQLList, GraphQLNonNull, GraphQLType, GraphQLNamedType } from 'graphql';\nimport { OnClickTypeFunction } from './types';\n\ntype Maybe<T> = T | null | undefined;\n\ntype TypeLinkProps = {\n  type?: Maybe<GraphQLType>;\n  onClick?: OnClickTypeFunction;\n};\n\nexport default function TypeLink(props: TypeLinkProps) {\n  const onClick = props.onClick ? props.onClick : () => null;\n  return renderType(props.type, onClick);\n}\n\nfunction renderType(type: Maybe<GraphQLType>, onClick: OnClickTypeFunction) {\n  if (type instanceof GraphQLNonNull) {\n    return (\n      <span>\n        {renderType(type.ofType, onClick)}\n        {'!'}\n      </span>\n    );\n  }\n  if (type instanceof GraphQLList) {\n    return (\n      <span>\n        {'['}\n        {renderType(type.ofType, onClick)}\n        {']'}\n      </span>\n    );\n  }\n  return (\n    <a\n      className=\"type-name\"\n      onClick={(event) => {\n        event.preventDefault();\n        onClick(type as GraphQLNamedType, event);\n      }}\n      href=\"#\"\n    >\n      {type?.name}\n    </a>\n  );\n}\n"
  },
  {
    "path": "packages/bruno-graphql-docs/src/components/DocExplorer/types.ts",
    "content": "import { MouseEvent } from 'react';\nimport {\n  GraphQLField,\n  GraphQLInputField,\n  GraphQLArgument,\n  GraphQLObjectType,\n  GraphQLInterfaceType,\n  GraphQLInputObjectType,\n  GraphQLType,\n  GraphQLNamedType\n} from 'graphql';\n\nexport type FieldType = GraphQLField<{}, {}, {}> | GraphQLInputField | GraphQLArgument;\n\nexport type OnClickFieldFunction = (\n  field: FieldType,\n  type?: GraphQLObjectType | GraphQLInterfaceType | GraphQLInputObjectType | GraphQLType,\n  event?: MouseEvent\n) => void;\n\nexport type OnClickTypeFunction = (type: GraphQLNamedType, event?: MouseEvent<HTMLAnchorElement>) => void;\n\nexport type OnClickFieldOrTypeFunction = OnClickFieldFunction | OnClickTypeFunction;\n"
  },
  {
    "path": "packages/bruno-graphql-docs/src/components/DocExplorer.tsx",
    "content": "/**\n *  Copyright (c) 2021 GraphQL Contributors.\n *\n *  This source code is licensed under the MIT license found in the\n *  LICENSE file in the root directory of this source tree.\n */\n\nimport React, { ReactNode } from 'react';\nimport { GraphQLSchema, isType, GraphQLNamedType, GraphQLError } from 'graphql';\nimport { FieldType } from './DocExplorer/types';\n\nimport FieldDoc from './DocExplorer/FieldDoc';\nimport SchemaDoc from './DocExplorer/SchemaDoc';\nimport SearchBox from './DocExplorer/SearchBox';\nimport SearchResults from './DocExplorer/SearchResults';\nimport TypeDoc from './DocExplorer/TypeDoc';\n\ntype NavStackItem = {\n  name: string;\n  title?: string;\n  search?: string;\n  def?: GraphQLNamedType | FieldType;\n};\n\nconst initialNav: NavStackItem = {\n  name: 'Schema',\n  title: 'Documentation Explorer'\n};\n\ntype DocExplorerProps = {\n  schema?: GraphQLSchema | null;\n  schemaErrors?: readonly GraphQLError[];\n  children?: ReactNode | null;\n};\n\ntype DocExplorerState = {\n  navStack: NavStackItem[];\n};\n\n/**\n * DocExplorer\n *\n * Shows documentations for GraphQL definitions from the schema.\n *\n * Props:\n *\n *   - schema: A required GraphQLSchema instance that provides GraphQL document\n *     definitions.\n *\n * Children:\n *\n *   - Any provided children will be positioned in the right-hand-side of the\n *     top bar. Typically this will be a \"close\" button for temporary explorer.\n *\n */\nexport class DocExplorer extends React.Component<DocExplorerProps, DocExplorerState> {\n  // handleClickTypeOrField: OnClickTypeFunction | OnClickFieldFunction\n  constructor(props: DocExplorerProps) {\n    super(props);\n\n    this.state = { navStack: [initialNav] };\n  }\n\n  shouldComponentUpdate(nextProps: DocExplorerProps, nextState: DocExplorerState) {\n    return (\n      this.props.schema !== nextProps.schema ||\n      this.state.navStack !== nextState.navStack ||\n      this.props.schemaErrors !== nextProps.schemaErrors\n    );\n  }\n\n  render() {\n    const { schema, schemaErrors } = this.props;\n    const navStack = this.state.navStack;\n    const navItem = navStack[navStack.length - 1];\n\n    let content;\n    if (schemaErrors) {\n      content = <div className=\"error-container\">{'Error fetching schema'}</div>;\n    } else if (schema === undefined) {\n      // Schema is undefined when it is being loaded via introspection.\n      content = (\n        <div className=\"spinner-container\">\n          <div className=\"spinner\" />\n        </div>\n      );\n    } else if (!schema) {\n      // Schema is null when it explicitly does not exist, typically due to\n      // an error during introspection.\n      content = <div className=\"error-container\">{'No Schema Available'}</div>;\n    } else if (navItem.search) {\n      content = (\n        <SearchResults\n          searchValue={navItem.search}\n          withinType={navItem.def as GraphQLNamedType}\n          schema={schema}\n          onClickType={this.handleClickType}\n          onClickField={this.handleClickField}\n        />\n      );\n    } else if (navStack.length === 1) {\n      content = <SchemaDoc schema={schema} onClickType={this.handleClickType} />;\n    } else if (isType(navItem.def)) {\n      content = (\n        <TypeDoc\n          schema={schema}\n          type={navItem.def}\n          onClickType={this.handleClickType}\n          onClickField={this.handleClickField}\n        />\n      );\n    } else {\n      content = <FieldDoc field={navItem.def as FieldType} onClickType={this.handleClickType} />;\n    }\n\n    const shouldSearchBoxAppear = navStack.length === 1 || (isType(navItem.def) && 'getFields' in navItem.def);\n\n    let prevName;\n    if (navStack.length > 1) {\n      prevName = navStack[navStack.length - 2].name;\n    }\n\n    return (\n      <div className=\"graphql-docs-container\">\n        <section className=\"doc-explorer\" key={navItem.name} aria-label=\"Documentation Explorer\">\n          <div className=\"doc-explorer-title-bar\">\n            {prevName && (\n              <button\n                className=\"doc-explorer-back\"\n                onClick={this.handleNavBackClick}\n                aria-label={`Go back to ${prevName}`}\n              >\n                {prevName}\n              </button>\n            )}\n            <div className=\"doc-explorer-title\">{navItem.title || navItem.name}</div>\n            <div className=\"doc-explorer-rhs\">{this.props.children}</div>\n          </div>\n          <div className=\"doc-explorer-contents\">\n            {shouldSearchBoxAppear && (\n              <SearchBox\n                value={navItem.search}\n                placeholder={`Search ${navItem.name}...`}\n                onSearch={this.handleSearch}\n              />\n            )}\n            {content}\n          </div>\n        </section>\n      </div>\n    );\n  }\n\n  // Public API\n  showDoc(typeOrField: GraphQLNamedType | FieldType) {\n    const navStack = this.state.navStack;\n    const topNav = navStack[navStack.length - 1];\n    if (topNav.def !== typeOrField) {\n      this.setState({\n        navStack: navStack.concat([\n          {\n            name: typeOrField.name,\n            def: typeOrField\n          }\n        ])\n      });\n    }\n  }\n\n  // Public API\n  showDocForReference(reference: any) {\n    if (reference && reference.kind === 'Type') {\n      this.showDoc(reference.type);\n    } else if (reference.kind === 'Field') {\n      this.showDoc(reference.field);\n    } else if (reference.kind === 'Argument' && reference.field) {\n      this.showDoc(reference.field);\n    } else if (reference.kind === 'EnumValue' && reference.type) {\n      this.showDoc(reference.type);\n    }\n  }\n\n  // Public API\n  showSearch(search: string) {\n    const navStack = this.state.navStack.slice();\n    const topNav = navStack[navStack.length - 1];\n    navStack[navStack.length - 1] = { ...topNav, search };\n    this.setState({ navStack });\n  }\n\n  reset() {\n    this.setState({ navStack: [initialNav] });\n  }\n\n  handleNavBackClick = () => {\n    if (this.state.navStack.length > 1) {\n      this.setState({ navStack: this.state.navStack.slice(0, -1) });\n    }\n  };\n\n  handleClickType = (type: GraphQLNamedType) => {\n    this.showDoc(type);\n  };\n\n  handleClickField = (field: FieldType) => {\n    this.showDoc(field);\n  };\n\n  handleSearch = (value: string) => {\n    this.showSearch(value);\n  };\n}\n"
  },
  {
    "path": "packages/bruno-graphql-docs/src/index.css",
    "content": ".graphql-docs-container .doc-explorer {\n  background: white;\n}\n\n.graphql-docs-container .doc-explorer-title-bar,\n.graphql-docs-container .history-title-bar {\n  cursor: default;\n  display: flex;\n  line-height: 14px;\n  padding: 8px 8px 5px;\n  position: relative;\n  user-select: none;\n}\n\n.graphql-docs-container .doc-explorer-title,\n.graphql-docs-container .history-title {\n  flex: 1;\n  font-weight: bold;\n  overflow-x: hidden;\n  padding: 10px 0 10px 10px;\n  text-align: center;\n  text-overflow: ellipsis;\n  user-select: text;\n  white-space: nowrap;\n}\n\n.graphql-docs-container .doc-explorer-back {\n  color: #3b5998;\n  cursor: pointer;\n  margin: -7px 0 -6px -8px;\n  overflow-x: hidden;\n  padding: 17px 12px 16px 16px;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  background: 0;\n  border: 0;\n  line-height: 14px;\n}\n\n.doc-explorer-narrow .doc-explorer-back {\n  width: 0;\n}\n\n.graphql-docs-container .doc-explorer-back:before {\n  border-left: 2px solid #3b5998;\n  border-top: 2px solid #3b5998;\n  content: '';\n  display: inline-block;\n  height: 9px;\n  margin: 0 3px -1px 0;\n  position: relative;\n  transform: rotate(-45deg);\n  width: 9px;\n}\n\n.graphql-docs-container .doc-explorer-rhs {\n  position: relative;\n}\n\n.graphql-docs-container .doc-explorer-contents,\n.graphql-docs-container .history-contents {\n  background-color: #ffffff;\n  border-top: 1px solid #d6d6d6;\n  bottom: 0;\n  left: 0;\n  overflow-y: auto;\n  padding: 20px 15px;\n  position: absolute;\n  right: 0;\n  top: 47px;\n}\n\n.graphql-docs-container .doc-explorer-contents {\n  min-width: 300px;\n}\n\n.graphql-docs-container .doc-type-description p:first-child,\n.graphql-docs-container .doc-type-description blockquote:first-child {\n  margin-top: 0;\n}\n\n.graphql-docs-container .doc-explorer-contents a {\n  cursor: pointer;\n  text-decoration: none;\n}\n\n.graphql-docs-container .doc-explorer-contents a:hover {\n  text-decoration: underline;\n}\n\n.graphql-docs-container .doc-value-description > :first-child {\n  margin-top: 4px;\n}\n\n.graphql-docs-container .doc-value-description > :last-child {\n  margin-bottom: 4px;\n}\n\n.graphql-docs-container .doc-type-description code,\n.graphql-docs-container .doc-type-description pre,\n.graphql-docs-container .doc-category code,\n.graphql-docs-container .doc-category pre {\n  --saf-0: rgba(var(--sk_foreground_low, 29, 28, 29), 0.13);\n  font-size: 12px;\n  line-height: 1.50001;\n  font-variant-ligatures: none;\n  white-space: pre;\n  white-space: pre-wrap;\n  word-wrap: break-word;\n  word-break: normal;\n  -webkit-tab-size: 4;\n  -moz-tab-size: 4;\n  tab-size: 4;\n}\n\n.graphql-docs-container .doc-type-description code,\n.graphql-docs-container .doc-category code {\n  padding: 2px 3px 1px;\n  border: 1px solid var(--saf-0);\n  border-radius: 3px;\n  background-color: rgba(var(--sk_foreground_min, 29, 28, 29), 0.04);\n  color: #e01e5a;\n  background-color: white;\n}\n\n.graphql-docs-container .doc-category {\n  margin: 20px 0;\n}\n\n.graphql-docs-container .doc-category-title {\n  border-bottom: 1px solid #e0e0e0;\n  color: #777;\n  cursor: default;\n  font-size: 14px;\n  font-variant: small-caps;\n  font-weight: bold;\n  letter-spacing: 1px;\n  margin: 0 -15px 10px 0;\n  padding: 10px 0;\n  user-select: none;\n}\n\n.graphql-docs-container .doc-category-item {\n  margin: 12px 0;\n  color: #555;\n}\n\n.graphql-docs-container .keyword {\n  color: #b11a04;\n}\n\n.graphql-docs-container .type-name {\n  color: #ca9800;\n}\n\n.graphql-docs-container .field-name {\n  color: #1f61a0;\n}\n\n.graphql-docs-container .field-short-description {\n  color: #666;\n  margin-left: 5px;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n.graphql-docs-container .enum-value {\n  color: #0b7fc7;\n}\n\n.graphql-docs-container .arg-name {\n  color: #8b2bb9;\n}\n\n.graphql-docs-container .arg {\n  display: block;\n  margin-left: 1em;\n}\n\n.graphql-docs-container .arg:first-child:last-child,\n.graphql-docs-container .arg:first-child:nth-last-child(2),\n.graphql-docs-container .arg:first-child:nth-last-child(2) ~ .arg {\n  display: inherit;\n  margin: inherit;\n}\n\n.graphql-docs-container .arg:first-child:nth-last-child(2):after {\n  content: ', ';\n}\n\n.graphql-docs-container .arg-default-value {\n  color: #43a047;\n}\n\n.graphql-docs-container .doc-deprecation {\n  background: #fffae8;\n  box-shadow: inset 0 0 1px #bfb063;\n  color: #867f70;\n  line-height: 16px;\n  margin: 8px -8px;\n  max-height: 80px;\n  overflow: hidden;\n  padding: 8px;\n  border-radius: 3px;\n}\n\n.graphql-docs-container .doc-deprecation:before {\n  content: 'Deprecated:';\n  color: #c79b2e;\n  cursor: default;\n  display: block;\n  font-size: 9px;\n  font-weight: bold;\n  letter-spacing: 1px;\n  line-height: 1;\n  padding-bottom: 5px;\n  text-transform: uppercase;\n  user-select: none;\n}\n\n.graphql-docs-container .doc-deprecation > :first-child {\n  margin-top: 0;\n}\n\n.graphql-docs-container .doc-deprecation > :last-child {\n  margin-bottom: 0;\n}\n\n.graphql-docs-container .show-btn {\n  -webkit-appearance: initial;\n  display: block;\n  border-radius: 3px;\n  border: solid 1px #ccc;\n  text-align: center;\n  padding: 8px 12px 10px;\n  width: 100%;\n  box-sizing: border-box;\n  background: #fbfcfc;\n  color: #555;\n  cursor: pointer;\n}\n\n.graphql-docs-container .search-box {\n  border-bottom: 1px solid #d3d6db;\n  display: flex;\n  align-items: center;\n  font-size: 14px;\n  margin: -15px -15px 12px 0;\n  position: relative;\n}\n\n.graphql-docs-container .search-box-icon {\n  cursor: pointer;\n  display: block;\n  font-size: 24px;\n  transform: rotate(-45deg);\n  user-select: none;\n}\n\n.graphql-docs-container .search-box .search-box-clear {\n  background-color: #d0d0d0;\n  border-radius: 12px;\n  color: #fff;\n  cursor: pointer;\n  font-size: 11px;\n  padding: 1px 5px 2px;\n  position: absolute;\n  right: 3px;\n  user-select: none;\n  border: 0;\n}\n\n.graphql-docs-container .search-box .search-box-clear:hover {\n  background-color: #b9b9b9;\n}\n\n.graphql-docs-container .search-box > input {\n  border: none;\n  box-sizing: border-box;\n  font-size: 14px;\n  outline: none;\n  padding: 6px 24px 8px 20px;\n  width: 100%;\n}\n\n.graphql-docs-container .error-container {\n  font-weight: bold;\n  left: 0;\n  letter-spacing: 1px;\n  opacity: 0.5;\n  position: absolute;\n  right: 0;\n  text-align: center;\n  text-transform: uppercase;\n  top: 50%;\n  transform: translate(0, -50%);\n}\n"
  },
  {
    "path": "packages/bruno-graphql-docs/src/index.ts",
    "content": "import { DocExplorer } from './components/DocExplorer';\n\nimport './index.css';\n\nexport { DocExplorer };\n"
  },
  {
    "path": "packages/bruno-graphql-docs/src/utility/debounce.ts",
    "content": "/**\n *  Copyright (c) 2021 GraphQL Contributors.\n *\n *  This source code is licensed under the MIT license found in the\n *  LICENSE file in the root directory of this source tree.\n */\n\n/**\n * Provided a duration and a function, returns a new function which is called\n * `duration` milliseconds after the last call.\n */\nexport default function debounce<F extends (...args: any[]) => any>(duration: number, fn: F) {\n  let timeout: number | null;\n  return function (this: any, ...args: Parameters<F>) {\n    if (timeout) {\n      window.clearTimeout(timeout);\n    }\n    timeout = window.setTimeout(() => {\n      timeout = null;\n      fn.apply(this, args);\n    }, duration);\n  };\n}\n"
  },
  {
    "path": "packages/bruno-graphql-docs/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"esModuleInterop\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"jsx\": \"react\",\n    \"module\": \"ESNext\",  \n    \"declaration\": true,\n    \"declarationDir\": \"types\",\n    \"sourceMap\": true,\n    \"outDir\": \"dist\",\n    \"moduleResolution\": \"node\",\n    \"emitDeclarationOnly\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"forceConsistentCasingInFileNames\": true\n  },\n  \"exclude\": [\n    \"dist\",\n    \"node_modules\",\n    \"src/**/*.test.tsx\"\n  ],\n}"
  },
  {
    "path": "packages/bruno-js/.gitignore",
    "content": "src/sandbox/bundle-browser-rollup.js"
  },
  {
    "path": "packages/bruno-js/license.md",
    "content": "\nMIT License\n\nCopyright (c) 2022 Anoop M D, Anusree P S and Contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "packages/bruno-js/package.json",
    "content": "{\n  \"name\": \"@usebruno/js\",\n  \"version\": \"0.12.0\",\n  \"license\": \"MIT\",\n  \"main\": \"src/index.js\",\n  \"files\": [\n    \"src\",\n    \"package.json\"\n  ],\n  \"scripts\": {\n    \"test\": \"node --experimental-vm-modules $(npx which jest) --testPathIgnorePatterns test.js\",\n    \"sandbox:bundle-libraries\": \"node ./src/sandbox/bundle-libraries.js\"\n  },\n  \"dependencies\": {\n    \"@usebruno/common\": \"0.1.0\",\n    \"@usebruno/query\": \"0.1.0\",\n    \"ajv\": \"^8.12.0\",\n    \"ajv-formats\": \"^2.1.1\",\n    \"atob\": \"^2.1.2\",\n    \"axios\": \"^1.8.3\",\n    \"btoa\": \"^1.2.1\",\n    \"chai\": \"^4.3.7\",\n    \"chai-string\": \"^1.5.0\",\n    \"cheerio\": \"^1.0.0\",\n    \"crypto-js\": \"^4.2.0\",\n    \"json-query\": \"^2.2.2\",\n    \"jsonwebtoken\": \"^9.0.3\",\n    \"lodash\": \"^4.17.21\",\n    \"moment\": \"^2.29.4\",\n    \"nanoid\": \"3.3.8\",\n    \"node-fetch\": \"^2.7.0\",\n    \"path\": \"^0.12.7\",\n    \"quickjs-emscripten\": \"^0.29.2\",\n    \"tv4\": \"^1.3.0\",\n    \"uuid\": \"^9.0.0\",\n    \"xml-formatter\": \"^3.5.0\",\n    \"xml2js\": \"^0.6.2\",\n    \"yaml\": \"^2.3.4\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-commonjs\": \"^23.0.2\",\n    \"@rollup/plugin-node-resolve\": \"^15.0.1\",\n    \"rollup\": \"3.29.5\",\n    \"rollup-plugin-terser\": \"^7.0.2\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-js/readme.md",
    "content": "# bruno-js\n\nProvides the script, test, vars and assert runtimes.\n\n### Publish to Npm Registry\n```bash\nnpm publish --access=public\n```"
  },
  {
    "path": "packages/bruno-js/src/bru.js",
    "content": "const { cloneDeep } = require('lodash');\nconst xmlFormat = require('xml-formatter');\nconst { interpolate: _interpolate } = require('@usebruno/common');\nconst { sendRequest, createSendRequest } = require('@usebruno/requests').scripting;\nconst { jar: createCookieJar } = require('@usebruno/requests').cookies;\n\nconst variableNameRegex = /^[\\w-.]*$/;\n\nclass Bru {\n  /**\n   * @param {string} runtime - The runtime environment ('quickjs' or 'nodevm')\n   * @param {object} envVariables - Environment variables\n   * @param {object} runtimeVariables - Runtime variables\n   * @param {object} processEnvVars - Process environment variables\n   * @param {string} collectionPath - Path to the collection\n   * @param {object} collectionVariables - Collection-level variables\n   * @param {object} folderVariables - Folder-level variables\n   * @param {object} requestVariables - Request-level variables\n   * @param {object} globalEnvironmentVariables - Global environment variables\n   * @param {object} oauth2CredentialVariables - OAuth2 credential variables\n   * @param {string} collectionName - Name of the collection\n   * @param {object} promptVariables - Prompt variables\n   * @param {object} certsAndProxyConfig - Configuration for bru.sendRequest (proxy, certs, TLS)\n   * @param {string} certsAndProxyConfig.collectionPath - Path to the collection\n   * @param {object} certsAndProxyConfig.options - TLS and proxy options\n   * @param {object} [certsAndProxyConfig.clientCertificates] - Client certificate configuration\n   * @param {object} [certsAndProxyConfig.collectionLevelProxy] - Collection-level proxy settings\n   * @param {object} [certsAndProxyConfig.systemProxyConfig] - System proxy configuration\n   */\n  constructor(runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables, certsAndProxyConfig) {\n    this.envVariables = envVariables || {};\n    this.runtimeVariables = runtimeVariables || {};\n    this.promptVariables = promptVariables || {};\n    this.processEnvVars = cloneDeep(processEnvVars || {});\n    this.collectionVariables = collectionVariables || {};\n    this.folderVariables = folderVariables || {};\n    this.requestVariables = requestVariables || {};\n    this.globalEnvironmentVariables = globalEnvironmentVariables || {};\n    this.oauth2CredentialVariables = oauth2CredentialVariables || {};\n    this.collectionPath = collectionPath;\n    this.collectionName = collectionName;\n    // Use createSendRequest with config if provided, otherwise use default sendRequest\n    this.sendRequest = certsAndProxyConfig ? createSendRequest(certsAndProxyConfig) : sendRequest;\n    this.runtime = runtime;\n    this.cookies = {\n      jar: () => {\n        const cookieJar = createCookieJar();\n\n        return {\n          getCookie: (url, cookieName, callback) => {\n            const interpolatedUrl = this.interpolate(url);\n            return cookieJar.getCookie(interpolatedUrl, cookieName, callback);\n          },\n\n          getCookies: (url, callback) => {\n            const interpolatedUrl = this.interpolate(url);\n            return cookieJar.getCookies(interpolatedUrl, callback);\n          },\n\n          setCookie: (url, nameOrCookieObj, valueOrCallback, maybeCallback) => {\n            const interpolatedUrl = this.interpolate(url);\n            return cookieJar.setCookie(interpolatedUrl, nameOrCookieObj, valueOrCallback, maybeCallback);\n          },\n\n          setCookies: (url, cookiesArray, callback) => {\n            const interpolatedUrl = this.interpolate(url);\n            return cookieJar.setCookies(interpolatedUrl, cookiesArray, callback);\n          },\n\n          // Clear entire cookie jar\n          clear: (callback) => {\n            return cookieJar.clear(callback);\n          },\n\n          // Delete cookies for a specific URL/domain\n          deleteCookies: (url, callback) => {\n            const interpolatedUrl = this.interpolate(url);\n            return cookieJar.deleteCookies(interpolatedUrl, callback);\n          },\n\n          deleteCookie: (url, cookieName, callback) => {\n            const interpolatedUrl = this.interpolate(url);\n            return cookieJar.deleteCookie(interpolatedUrl, cookieName, callback);\n          },\n\n          hasCookie: (url, cookieName, callback) => {\n            const interpolatedUrl = this.interpolate(url);\n            return cookieJar.hasCookie(interpolatedUrl, cookieName, callback);\n          }\n        };\n      }\n    };\n    // Holds variables that are marked as persistent by scripts\n    this.persistentEnvVariables = {};\n    // Holds credential IDs to be reset after script execution\n    this.oauth2CredentialsToReset = [];\n    this.runner = {\n      skipRequest: () => {\n        this.skipRequest = true;\n      },\n      stopExecution: () => {\n        this.stopExecution = true;\n      },\n      setNextRequest: (nextRequest) => {\n        this.nextRequest = nextRequest;\n      }\n    };\n\n    this.utils = {\n      minifyJson: (json) => {\n        if (json === null || json === undefined) {\n          throw new Error('Failed to minify');\n        }\n\n        if (typeof json === 'object') {\n          try {\n            return JSON.stringify(json);\n          } catch (err) {\n            throw new Error(`Failed to minify: ${err?.message || err}`);\n          }\n        }\n\n        if (typeof json === 'string') {\n          const trimmed = json.trim();\n          if (trimmed === '') return trimmed;\n          try {\n            return JSON.stringify(JSON.parse(trimmed));\n          } catch (err) {\n            throw new Error(`Failed to minify: ${err?.message || err}`);\n          }\n        }\n\n        throw new TypeError('minifyJson expects a string or object');\n      },\n\n      minifyXml: (xml) => {\n        if (xml === null || xml === undefined) {\n          throw new Error('Failed to minify');\n        }\n\n        if (typeof xml === 'string') {\n          try {\n            return xmlFormat(xml, { collapseContent: false, indentation: '', lineSeparator: '' });\n          } catch (err) {\n            throw new Error(`Failed to minify: ${err?.message || err}`);\n          }\n        }\n\n        throw new TypeError('minifyXml expects a string');\n      }\n    };\n  }\n\n  interpolate = (strOrObj) => {\n    if (!strOrObj) return strOrObj;\n    const isObj = typeof strOrObj === 'object';\n    const strToInterpolate = isObj ? JSON.stringify(strOrObj) : strOrObj;\n\n    const combinedVars = {\n      ...this.globalEnvironmentVariables,\n      ...this.collectionVariables,\n      ...this.envVariables,\n      ...this.folderVariables,\n      ...this.requestVariables,\n      ...this.oauth2CredentialVariables,\n      ...this.runtimeVariables,\n      ...this.promptVariables,\n      process: {\n        env: {\n          ...this.processEnvVars\n        }\n      }\n    };\n\n    const interpolatedStr = _interpolate(strToInterpolate, combinedVars);\n    return isObj ? JSON.parse(interpolatedStr) : interpolatedStr;\n  };\n\n  cwd() {\n    return this.collectionPath;\n  }\n\n  getEnvName() {\n    return this.envVariables.__name__;\n  }\n\n  getProcessEnv(key) {\n    return this.processEnvVars[key];\n  }\n\n  hasEnvVar(key) {\n    return Object.hasOwn(this.envVariables, key);\n  }\n\n  getEnvVar(key) {\n    return this.interpolate(this.envVariables[key]);\n  }\n\n  setEnvVar(key, value, options = {}) {\n    if (!key) {\n      throw new Error('Creating a env variable without specifying a name is not allowed.');\n    }\n\n    if (variableNameRegex.test(key) === false) {\n      throw new Error(\n        `Variable name: \"${key}\" contains invalid characters! Names must only contain alpha-numeric characters, \"-\", \"_\", \".\"`\n      );\n    }\n\n    // When persist is true, only string values are allowed\n    if (options?.persist && typeof value !== 'string') {\n      throw new Error(`Persistent environment variables must be strings. Received ${typeof value} for key \"${key}\".`);\n    }\n\n    this.envVariables[key] = value;\n\n    if (options?.persist) {\n      this.persistentEnvVariables[key] = value;\n    } else {\n      if (this.persistentEnvVariables[key]) {\n        delete this.persistentEnvVariables[key];\n      }\n    }\n  }\n\n  deleteEnvVar(key) {\n    delete this.envVariables[key];\n  }\n\n  getAllEnvVars() {\n    const vars = Object.assign({}, this.envVariables);\n    delete vars.__name__;\n    return vars;\n  }\n\n  deleteAllEnvVars() {\n    const envName = this.envVariables.__name__;\n    for (let key in this.envVariables) {\n      if (this.envVariables.hasOwnProperty(key)) {\n        delete this.envVariables[key];\n      }\n    }\n    if (envName !== undefined) {\n      this.envVariables.__name__ = envName;\n    }\n  }\n\n  getGlobalEnvVar(key) {\n    return this.interpolate(this.globalEnvironmentVariables[key]);\n  }\n\n  setGlobalEnvVar(key, value) {\n    if (!key) {\n      throw new Error('Creating a env variable without specifying a name is not allowed.');\n    }\n\n    this.globalEnvironmentVariables[key] = value;\n  }\n\n  // TODO: deleteGlobalEnvVar works in the request lifecycle but does not update the UI.\n  // Re-enable once the UI sync issue is resolved.\n  // deleteGlobalEnvVar(key) {\n  //   delete this.globalEnvironmentVariables[key];\n  // }\n\n  getAllGlobalEnvVars() {\n    return Object.assign({}, this.globalEnvironmentVariables);\n  }\n\n  // TODO: deleteAllGlobalEnvVars works in the request lifecycle but does not update the UI.\n  // Re-enable once the UI sync issue is resolved.\n  // deleteAllGlobalEnvVars() {\n  //   for (let key in this.globalEnvironmentVariables) {\n  //     if (this.globalEnvironmentVariables.hasOwnProperty(key)) {\n  //       delete this.globalEnvironmentVariables[key];\n  //     }\n  //   }\n  // }\n\n  getOauth2CredentialVar(key) {\n    return this.interpolate(this.oauth2CredentialVariables[key]);\n  }\n\n  resetOauth2Credential(credentialId) {\n    if (!credentialId || typeof credentialId !== 'string') {\n      throw new Error('credentialId must be a non-empty string');\n    }\n\n    if (!this.oauth2CredentialsToReset.includes(credentialId)) {\n      this.oauth2CredentialsToReset.push(credentialId);\n    }\n\n    // Remove matching credential variables so subsequent getOauth2CredentialVar() calls return undefined\n    const prefix = `$oauth2.${credentialId}.`;\n    for (const key of Object.keys(this.oauth2CredentialVariables)) {\n      if (key.startsWith(prefix)) {\n        delete this.oauth2CredentialVariables[key];\n      }\n    }\n  }\n\n  hasVar(key) {\n    return Object.hasOwn(this.runtimeVariables, key);\n  }\n\n  setVar(key, value) {\n    if (!key) {\n      throw new Error('Creating a variable without specifying a name is not allowed.');\n    }\n\n    if (variableNameRegex.test(key) === false) {\n      throw new Error(\n        `Variable name: \"${key}\" contains invalid characters!`\n        + ' Names must only contain alpha-numeric characters, \"-\", \"_\", \".\"'\n      );\n    }\n\n    this.runtimeVariables[key] = value;\n  }\n\n  getVar(key) {\n    if (variableNameRegex.test(key) === false) {\n      throw new Error(\n        `Variable name: \"${key}\" contains invalid characters!`\n        + ' Names must only contain alpha-numeric characters, \"-\", \"_\", \".\"'\n      );\n    }\n\n    return this.interpolate(this.runtimeVariables[key]);\n  }\n\n  deleteVar(key) {\n    delete this.runtimeVariables[key];\n  }\n\n  deleteAllVars() {\n    for (let key in this.runtimeVariables) {\n      if (this.runtimeVariables.hasOwnProperty(key)) {\n        delete this.runtimeVariables[key];\n      }\n    }\n  }\n\n  getAllVars() {\n    return Object.assign({}, this.runtimeVariables);\n  }\n\n  getCollectionVar(key) {\n    return this.interpolate(this.collectionVariables[key]);\n  }\n\n  // TODO: setCollectionVar works in the request lifecycle but does not update the UI.\n  // Re-enable once the UI sync issue is resolved.\n  // setCollectionVar(key, value) {\n  //   if (!key) {\n  //     throw new Error('Creating a variable without specifying a name is not allowed.');\n  //   }\n  //\n  //   if (variableNameRegex.test(key) === false) {\n  //     throw new Error(\n  //       `Variable name: \"${key}\" contains invalid characters!`\n  //       + ' Names must only contain alpha-numeric characters, \"-\", \"_\", \".\"'\n  //     );\n  //   }\n  //\n  //   this.collectionVariables[key] = value;\n  // }\n\n  hasCollectionVar(key) {\n    return Object.hasOwn(this.collectionVariables, key);\n  }\n\n  // TODO: deleteCollectionVar works in the request lifecycle but does not update the UI.\n  // Re-enable once the UI sync issue is resolved.\n  // deleteCollectionVar(key) {\n  //   delete this.collectionVariables[key];\n  // }\n\n  // TODO: deleteAllCollectionVars works in the request lifecycle but does not update the UI.\n  // Re-enable once the UI sync issue is resolved.\n  // deleteAllCollectionVars() {\n  //   for (let key in this.collectionVariables) {\n  //     if (this.collectionVariables.hasOwnProperty(key)) {\n  //       delete this.collectionVariables[key];\n  //     }\n  //   }\n  // }\n\n  // TODO: getAllCollectionVars works in the request lifecycle but does not update the UI.\n  // Re-enable once the UI sync issue is resolved.\n  // getAllCollectionVars() {\n  //   return Object.assign({}, this.collectionVariables);\n  // }\n\n  getFolderVar(key) {\n    return this.interpolate(this.folderVariables[key]);\n  }\n\n  getRequestVar(key) {\n    return this.interpolate(this.requestVariables[key]);\n  }\n\n  setNextRequest(nextRequest) {\n    this.nextRequest = nextRequest;\n  }\n\n  sleep(ms) {\n    return new Promise((resolve) => setTimeout(resolve, ms));\n  }\n\n  getCollectionName() {\n    return this.collectionName;\n  }\n\n  isSafeMode() {\n    return this.runtime === 'quickjs';\n  }\n}\n\nmodule.exports = Bru;\n"
  },
  {
    "path": "packages/bruno-js/src/bruno-request.js",
    "content": "class BrunoRequest {\n  /**\n   * The following properties are available as shorthand:\n   * - req.url\n   * - req.method\n   * - req.headers\n   * - req.timeout\n   * - req.body\n   *\n   * Above shorthands are useful for accessing the request properties directly in the scripts\n   * It must be noted that the user cannot set these properties directly.\n   * They should use the respective setter methods to set these properties.\n   */\n  constructor(req) {\n    this.req = req;\n    this.url = req.url;\n    this.method = req.method;\n    this.headers = req.headers;\n    this.timeout = req.timeout;\n    this.name = req.name;\n    this.pathParams = req.pathParams;\n    this.tags = req.tags || [];\n    /**\n     * We automatically parse the JSON body if the content type is JSON\n     * This is to make it easier for the user to access the body directly\n     *\n     * It must be noted that the request data is always a string and is what gets sent over the network\n     * If the user wants to access the raw data, they can use getBody({raw: true}) method\n     */\n    const isJson = this.hasJSONContentType(this.req.headers);\n    if (isJson) {\n      this.body = this.__safeParseJSON(req.data);\n    }\n  }\n\n  getUrl() {\n    return this.req.url;\n  }\n\n  setUrl(url) {\n    this.url = url;\n    this.req.url = url;\n  }\n\n  getHost() {\n    try {\n      const url = new URL(this.req.url);\n      return url.host;\n    } catch (e) {\n      return '';\n    }\n  }\n\n  getPath() {\n    try {\n      const url = new URL(this.req.url);\n      let pathname = url.pathname;\n\n      // If path params exist, interpolate them into the pathname\n      if (this.req.pathParams && Array.isArray(this.req.pathParams)) {\n        pathname = pathname\n          .split('/')\n          .map((segment) => {\n            if (segment.startsWith(':')) {\n              const paramName = segment.slice(1);\n              const pathParam = this.req.pathParams.find((param) => param.name === paramName);\n              if (pathParam && pathParam.value) {\n                return pathParam.value;\n              }\n            }\n            return segment;\n          })\n          .join('/');\n      }\n\n      return pathname;\n    } catch (e) {\n      return '';\n    }\n  }\n\n  getQueryString() {\n    try {\n      const url = new URL(this.req.url);\n      // Return query string without the leading '?'\n      return url.search ? url.search.substring(1) : '';\n    } catch (e) {\n      return '';\n    }\n  }\n\n  getMethod() {\n    return this.req.method;\n  }\n\n  getAuthMode() {\n    if (this.req?.oauth2) {\n      return 'oauth2';\n    } else if (this.headers?.['Authorization']?.startsWith('Bearer')) {\n      return 'bearer';\n    } else if (this.headers?.['Authorization']?.startsWith('Basic') || this.req?.auth?.username) {\n      return 'basic';\n    } else if (this.req?.awsv4) {\n      return 'awsv4';\n    } else if (this.req?.digestConfig) {\n      return 'digest';\n    } else if (this.headers?.['X-WSSE'] || this.req?.auth?.username) {\n      return 'wsse';\n    } else {\n      return 'none';\n    }\n  }\n\n  setMethod(method) {\n    this.method = method;\n    this.req.method = method;\n  }\n\n  getHeaders() {\n    return this.req.headers;\n  }\n\n  setHeaders(headers) {\n    this.headers = headers;\n    this.req.headers = headers;\n  }\n\n  deleteHeaders(headers) {\n    headers.forEach((name) => this.deleteHeader(name));\n  }\n\n  getHeader(name) {\n    return this.req.headers[name];\n  }\n\n  setHeader(name, value) {\n    this.headers[name] = value;\n    this.req.headers[name] = value;\n  }\n\n  deleteHeader(name) {\n    delete this.headers[name];\n    delete this.req.headers[name];\n\n    /**\n      Store header name to be applied in the axios request interceptor.\n      Default headers (user-agent, accept, accept-encoding, etc.) are added after\n      the pre-request script runs, so we track them here and delete them later.\n    */\n    if (!this.req.__headersToDelete) {\n      this.req.__headersToDelete = [];\n    }\n    if (!this.req.__headersToDelete.includes(name)) {\n      this.req.__headersToDelete.push(name);\n    }\n  }\n\n  hasJSONContentType(headers) {\n    const contentType = headers?.['Content-Type'] || headers?.['content-type'] || '';\n    return contentType.includes('json');\n  }\n\n  /**\n   * Get the body of the request\n   *\n   * We automatically parse and return the JSON body if the content type is JSON\n   * If the user wants the raw body, they can pass the raw option as true\n   */\n  getBody(options = {}) {\n    if (options.raw) {\n      return this.req.data;\n    }\n\n    const isJson = this.hasJSONContentType(this.req.headers);\n    if (isJson) {\n      return this.__safeParseJSON(this.req.data);\n    }\n\n    return this.req.data;\n  }\n\n  /**\n   * If the content type is JSON and if the data is an object\n   *  - We set the body property as the object itself\n   *  - We set the request data as the stringified JSON as it is what gets sent over the network\n   * Otherwise\n   *  - We set the request data as the data itself\n   *  - We set the body property as the data itself\n   *\n   * If the user wants to override this behavior, they can pass the raw option as true\n   */\n  setBody(data, options = {}) {\n    if (options.raw) {\n      this.req.data = data;\n      this.body = data;\n      return;\n    }\n\n    const isJson = this.hasJSONContentType(this.req.headers);\n    if (isJson && this.__isObject(data)) {\n      this.body = data;\n      this.req.data = this.__safeStringifyJSON(data);\n      return;\n    }\n\n    this.req.data = data;\n    this.body = data;\n  }\n\n  setMaxRedirects(maxRedirects) {\n    this.req.maxRedirects = maxRedirects;\n  }\n\n  getTimeout() {\n    return this.req.timeout;\n  }\n\n  setTimeout(timeout) {\n    this.timeout = timeout;\n    this.req.timeout = timeout;\n  }\n\n  onFail(callback) {\n    if (typeof callback === 'function') {\n      this.req.onFailHandler = callback;\n    } else if (callback) {\n      throw new Error(`${callback} is not a function`);\n    }\n  }\n\n  __safeParseJSON(str) {\n    try {\n      return JSON.parse(str);\n    } catch (e) {\n      return str;\n    }\n  }\n\n  __safeStringifyJSON(obj) {\n    try {\n      return JSON.stringify(obj);\n    } catch (e) {\n      return obj;\n    }\n  }\n\n  __isObject(obj) {\n    return obj !== null && typeof obj === 'object';\n  }\n\n  disableParsingResponseJson() {\n    this.req.__brunoDisableParsingResponseJson = true;\n  }\n\n  getExecutionMode() {\n    return this.req.__bruno__executionMode;\n  }\n\n  getName() {\n    return this.req.name;\n  }\n\n  getPathParams() {\n    const params = Array.isArray(this.req.pathParams) ? this.req.pathParams : [];\n\n    return params.map((param) => ({\n      name: param.name,\n      value: param.value,\n      type: param.type\n    }));\n  }\n\n  /**\n   * Get the tags associated with this request\n   * @returns {Array<string>} Array of tag strings\n   */\n  getTags() {\n    return this.req.tags || [];\n  }\n}\n\nmodule.exports = BrunoRequest;\n"
  },
  {
    "path": "packages/bruno-js/src/bruno-response.js",
    "content": "const { get } = require('@usebruno/query');\nconst _ = require('lodash');\n\nclass BrunoResponse {\n  constructor(res) {\n    this.res = res;\n    this.status = res ? res.status : null;\n    this.statusText = res ? res.statusText : null;\n    this.headers = res ? res.headers : null;\n    this.body = res ? res.data : null;\n    this.responseTime = res ? res.responseTime : null;\n    this.url = res?.request ? res.request.protocol + '//' + res.request.host + res.request.path : null;\n\n    // Make the instance callable\n    const callable = (...args) => get(this.body, ...args);\n    Object.setPrototypeOf(callable, this.constructor.prototype);\n    Object.assign(callable, this);\n\n    return callable;\n  }\n\n  getStatus() {\n    return this.res ? this.res.status : null;\n  }\n\n  getStatusText() {\n    return this.res ? this.res.statusText : null;\n  }\n\n  getHeader(name) {\n    return this.res && this.res.headers ? this.res.headers[name] : null;\n  }\n\n  getHeaders() {\n    return this.res ? this.res.headers : null;\n  }\n\n  getBody() {\n    return this.res ? this.res.data : null;\n  }\n\n  getResponseTime() {\n    return this.res ? this.res.responseTime : null;\n  }\n\n  getUrl() {\n    return this.res ? this.url : null;\n  }\n\n  setBody(data) {\n    if (!this.res) {\n      return;\n    }\n\n    const clonedData = _.cloneDeep(data);\n    this.res.data = clonedData;\n    this.body = clonedData;\n\n    // Update dataBuffer to match the modified body\n    if (clonedData === null || clonedData === undefined) {\n      this.res.dataBuffer = Buffer.from('');\n    } else if (typeof clonedData === 'string') {\n      this.res.dataBuffer = Buffer.from(clonedData);\n    } else {\n      // For objects, stringify them\n      try {\n        this.res.dataBuffer = Buffer.from(JSON.stringify(clonedData));\n      } catch (e) {\n        this.res.dataBuffer = Buffer.from('');\n      }\n    }\n  }\n\n  // TODO: Refactor: dataBuffer size calculation should be handled in a shared utility so it can be passed and reused across the application\n  getSize() {\n    if (!this.res) {\n      return { header: 0, body: 0, total: 0 };\n    }\n\n    const { data, dataBuffer, headers } = this.res;\n    let bodySize = 0;\n\n    // Use raw received bytes\n    if (Buffer.isBuffer(dataBuffer)) {\n      bodySize = dataBuffer.length;\n    } else {\n      // Use server-reported Content-Length\n      const contentLength = headers && (headers['content-length'] || headers['Content-Length']);\n      if (contentLength && !isNaN(contentLength)) {\n        bodySize = parseInt(contentLength, 10);\n      } else if (data != null) {\n        // Manual calculation\n        const raw = typeof data === 'string' ? data : JSON.stringify(data);\n        bodySize = Buffer.byteLength(raw);\n      }\n    }\n\n    const headerLines = [\n      `HTTP/1.1 ${this.res.status} ${this.res.statusText}`,\n      ...Object.entries(this.res.headers || {}).flatMap(([key, value]) =>\n        Array.isArray(value)\n          ? value.map((v) => `${key}: ${v}`)\n          : [`${key}: ${value}`]\n      ),\n      '',\n      ''\n    ];\n    const headerSize = Buffer.byteLength(headerLines.join('\\r\\n'));\n\n    return { header: headerSize, body: bodySize, total: headerSize + bodySize };\n  }\n\n  getDataBuffer() {\n    return this.res ? this.res.dataBuffer : null;\n  }\n}\n\nmodule.exports = BrunoResponse;\n"
  },
  {
    "path": "packages/bruno-js/src/index.js",
    "content": "const ScriptRuntime = require('./runtime/script-runtime');\nconst TestRuntime = require('./runtime/test-runtime');\nconst VarsRuntime = require('./runtime/vars-runtime');\nconst AssertRuntime = require('./runtime/assert-runtime');\nconst { runScriptInNodeVm } = require('./sandbox/node-vm');\nconst { formatErrorWithContext, SCRIPT_TYPES } = require('./utils/error-formatter');\n\nmodule.exports = {\n  ScriptRuntime,\n  TestRuntime,\n  VarsRuntime,\n  AssertRuntime,\n  runScriptInNodeVm,\n  formatErrorWithContext,\n  SCRIPT_TYPES\n};\n"
  },
  {
    "path": "packages/bruno-js/src/interpolate-string.js",
    "content": "const { interpolate } = require('@usebruno/common');\n\nconst interpolateString = (\n  str,\n  { envVariables = {}, runtimeVariables = {}, processEnvVars = {}, collectionVariables = {}, folderVariables = {}, requestVariables = {}, globalEnvironmentVariables = {} }\n) => {\n  if (!str || !str.length || typeof str !== 'string') {\n    return str;\n  }\n\n  const combinedVars = {\n    ...globalEnvironmentVariables,\n    ...collectionVariables,\n    ...envVariables,\n    ...folderVariables,\n    ...requestVariables,\n    ...runtimeVariables,\n    process: {\n      env: {\n        ...processEnvVars\n      }\n    }\n  };\n\n  return interpolate(str, combinedVars);\n};\n\nmodule.exports = {\n  interpolateString\n};\n"
  },
  {
    "path": "packages/bruno-js/src/runtime/assert-runtime.js",
    "content": "const _ = require('lodash');\nconst chai = require('chai');\nconst { nanoid } = require('nanoid');\nconst Bru = require('../bru');\nconst BrunoRequest = require('../bruno-request');\nconst { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');\nconst { interpolateString } = require('../interpolate-string');\nconst { executeQuickJsVm } = require('../sandbox/quickjs');\n\nconst { expect } = chai;\nchai.use(require('chai-string'));\nchai.use(function (chai, utils) {\n  // Custom assertion for checking if a variable is JSON\n  chai.Assertion.addProperty('json', function () {\n    const obj = this._obj;\n    // Use Object.prototype.toString instead of constructor check for cross-realm compatibility.\n    // Objects created inside Node's vm.createContext() have a different Object constructor,\n    // so obj.constructor === Object fails for objects passed via res.setBody() from scripts.\n    // Note: toString check is more permissive than constructor check — custom class instances\n    const isJson = typeof obj === 'object' && obj !== null && !Array.isArray(obj)\n      && Object.prototype.toString.call(obj) === '[object Object]';\n\n    this.assert(isJson, `expected ${utils.inspect(obj)} to be JSON`, `expected ${utils.inspect(obj)} not to be JSON`);\n  });\n});\n\n// Custom assertion for matching regex\nchai.use(function (chai, utils) {\n  chai.Assertion.addMethod('match', function (regex) {\n    const obj = this._obj;\n    let match = false;\n    if (obj === undefined) {\n      match = false;\n    } else {\n      match = regex.test(obj);\n    }\n\n    this.assert(\n      match,\n      `expected ${utils.inspect(obj)} to match ${regex}`,\n      `expected ${utils.inspect(obj)} not to match ${regex}`\n    );\n  });\n});\n\n/**\n * Assertion operators\n *\n * eq          : equal to\n * neq         : not equal to\n * gt          : greater than\n * gte         : greater than or equal to\n * lt          : less than\n * lte         : less than or equal to\n * in          : in\n * notIn       : not in\n * contains    : contains\n * notContains : not contains\n * length      : length\n * matches     : matches\n * notMatches  : not matches\n * startsWith  : starts with\n * endsWith    : ends with\n * between     : between\n * isEmpty     : is empty\n * isNotEmpty  : is not empty\n * isNull      : is null\n * isUndefined : is undefined\n * isDefined   : is defined\n * isTruthy    : is truthy\n * isFalsy     : is falsy\n * isJson      : is json\n * isNumber    : is number\n * isString    : is string\n * isBoolean   : is boolean\n * isArray     : is array\n */\nconst parseAssertionOperator = (str = '') => {\n  if (!str || typeof str !== 'string' || !str.length) {\n    return {\n      operator: 'eq',\n      value: str\n    };\n  }\n\n  const operators = [\n    'eq',\n    'neq',\n    'gt',\n    'gte',\n    'lt',\n    'lte',\n    'in',\n    'notIn',\n    'contains',\n    'notContains',\n    'length',\n    'matches',\n    'notMatches',\n    'startsWith',\n    'endsWith',\n    'between',\n    'isEmpty',\n    'isNotEmpty',\n    'isNull',\n    'isUndefined',\n    'isDefined',\n    'isTruthy',\n    'isFalsy',\n    'isJson',\n    'isNumber',\n    'isString',\n    'isBoolean',\n    'isArray'\n  ];\n\n  const unaryOperators = [\n    'isEmpty',\n    'isNotEmpty',\n    'isNull',\n    'isUndefined',\n    'isDefined',\n    'isTruthy',\n    'isFalsy',\n    'isJson',\n    'isNumber',\n    'isString',\n    'isBoolean',\n    'isArray'\n  ];\n\n  const [operator, ...rest] = str.trim().split(' ');\n  const value = rest.join(' ');\n\n  if (unaryOperators.includes(operator)) {\n    return {\n      operator,\n      value: ''\n    };\n  }\n\n  if (operators.includes(operator)) {\n    return {\n      operator,\n      value\n    };\n  }\n\n  return {\n    operator: 'eq',\n    value: str\n  };\n};\n\nconst isUnaryOperator = (operator) => {\n  const unaryOperators = [\n    'isEmpty',\n    'isNotEmpty',\n    'isNull',\n    'isUndefined',\n    'isDefined',\n    'isTruthy',\n    'isFalsy',\n    'isJson',\n    'isNumber',\n    'isString',\n    'isBoolean',\n    'isArray'\n  ];\n\n  return unaryOperators.includes(operator);\n};\n\nconst evaluateJsTemplateLiteralBasedOnRuntime = (literal, context, runtime) => {\n  if (runtime === 'quickjs') {\n    return executeQuickJsVm({\n      script: literal,\n      context,\n      scriptType: 'template-literal'\n    });\n  }\n\n  return evaluateJsTemplateLiteral(literal, context);\n};\n\nconst evaluateJsExpressionBasedOnRuntime = (expr, context, runtime) => {\n  if (runtime === 'quickjs') {\n    return executeQuickJsVm({\n      script: expr,\n      context,\n      scriptType: 'expression'\n    });\n  }\n\n  return evaluateJsExpression(expr, context);\n};\n\nconst evaluateRhsOperand = (rhsOperand, operator, context, runtime) => {\n  if (isUnaryOperator(operator)) {\n    return;\n  }\n\n  const interpolationContext = {\n    globalEnvironmentVariables: context.bru.globalEnvironmentVariables,\n    collectionVariables: context.bru.collectionVariables,\n    folderVariables: context.bru.folderVariables,\n    requestVariables: context.bru.requestVariables,\n    runtimeVariables: context.bru.runtimeVariables,\n    envVariables: context.bru.envVariables,\n    processEnvVars: context.bru.processEnvVars\n  };\n\n  // gracefully allow both a,b as well as [a, b]\n  if (operator === 'in' || operator === 'notIn') {\n    if (rhsOperand.startsWith('[') && rhsOperand.endsWith(']')) {\n      rhsOperand = rhsOperand.substring(1, rhsOperand.length - 1);\n    }\n\n    return rhsOperand\n      .split(',')\n      .map((v) =>\n        evaluateJsTemplateLiteralBasedOnRuntime(interpolateString(v.trim(), interpolationContext), context, runtime)\n      );\n  }\n\n  if (operator === 'between') {\n    const [lhs, rhs] = rhsOperand\n      .split(',')\n      .map((v) =>\n        evaluateJsTemplateLiteralBasedOnRuntime(interpolateString(v.trim(), interpolationContext), context, runtime)\n      );\n    return [lhs, rhs];\n  }\n\n  // gracefully allow both ^[a-Z] as well as /^[a-Z]/\n  if (operator === 'matches' || operator === 'notMatches') {\n    if (rhsOperand.startsWith('/') && rhsOperand.endsWith('/')) {\n      rhsOperand = rhsOperand.substring(1, rhsOperand.length - 1);\n    }\n\n    return interpolateString(rhsOperand, interpolationContext);\n  }\n\n  return evaluateJsTemplateLiteralBasedOnRuntime(interpolateString(rhsOperand, interpolationContext), context, runtime);\n};\n\nclass AssertRuntime {\n  constructor(props) {\n    this.runtime = props?.runtime || 'quickjs';\n  }\n\n  runAssertions(assertions, request, response, envVariables, runtimeVariables, processEnvVars) {\n    const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};\n    const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};\n    const collectionVariables = request?.collectionVariables || {};\n    const folderVariables = request?.folderVariables || {};\n    const requestVariables = request?.requestVariables || {};\n    const enabledAssertions = _.filter(assertions, (a) => a.enabled);\n    if (!enabledAssertions.length) {\n      return [];\n    }\n\n    const promptVariables = request?.promptVariables || {};\n    const certsAndProxyConfig = request?.certsAndProxyConfig;\n    const bru = new Bru(\n      this.runtime,\n      envVariables,\n      runtimeVariables,\n      processEnvVars,\n      undefined,\n      collectionVariables,\n      folderVariables,\n      requestVariables,\n      globalEnvironmentVariables,\n      {},\n      undefined,\n      promptVariables,\n      certsAndProxyConfig\n    );\n    const req = new BrunoRequest(request);\n    const res = createResponseParser(response);\n\n    const bruContext = {\n      bru,\n      req,\n      res\n    };\n\n    const context = {\n      ...globalEnvironmentVariables,\n      ...collectionVariables,\n      ...envVariables,\n      ...folderVariables,\n      ...requestVariables,\n      ...oauth2CredentialVariables,\n      ...runtimeVariables,\n      ...processEnvVars,\n      ...bruContext\n    };\n\n    const assertionResults = [];\n\n    // parse assertion operators\n    for (const v of enabledAssertions) {\n      const lhsExpr = v.name;\n      const rhsExpr = v.value;\n      const { operator, value: rhsOperand } = parseAssertionOperator(rhsExpr);\n\n      try {\n        const lhs = evaluateJsExpressionBasedOnRuntime(lhsExpr, context, this.runtime);\n        const rhs = evaluateRhsOperand(rhsOperand, operator, context, this.runtime);\n\n        switch (operator) {\n          case 'eq':\n            expect(lhs).to.equal(rhs);\n            break;\n          case 'neq':\n            expect(lhs).to.not.equal(rhs);\n            break;\n          case 'gt':\n            expect(lhs).to.be.greaterThan(rhs);\n            break;\n          case 'gte':\n            expect(lhs).to.be.greaterThanOrEqual(rhs);\n            break;\n          case 'lt':\n            expect(lhs).to.be.lessThan(rhs);\n            break;\n          case 'lte':\n            expect(lhs).to.be.lessThanOrEqual(rhs);\n            break;\n          case 'in':\n            expect(lhs).to.be.oneOf(rhs);\n            break;\n          case 'notIn':\n            expect(lhs).to.not.be.oneOf(rhs);\n            break;\n          case 'contains':\n            expect(lhs).to.include(rhs);\n            break;\n          case 'notContains':\n            expect(lhs).to.not.include(rhs);\n            break;\n          case 'length':\n            expect(lhs).to.have.lengthOf(rhs);\n            break;\n          case 'matches':\n            expect(lhs).to.match(new RegExp(rhs));\n            break;\n          case 'notMatches':\n            expect(lhs).to.not.match(new RegExp(rhs));\n            break;\n          case 'startsWith':\n            expect(lhs).to.startWith(rhs);\n            break;\n          case 'endsWith':\n            expect(lhs).to.endWith(rhs);\n            break;\n          case 'between':\n            const [min, max] = rhs;\n            expect(lhs).to.be.within(min, max);\n            break;\n          case 'isEmpty':\n            expect(lhs).to.be.empty;\n            break;\n          case 'isNotEmpty':\n            expect(lhs).to.not.be.empty;\n            break;\n          case 'isNull':\n            expect(lhs).to.be.null;\n            break;\n          case 'isUndefined':\n            expect(lhs).to.be.undefined;\n            break;\n          case 'isDefined':\n            expect(lhs).to.not.be.undefined;\n            break;\n          case 'isTruthy':\n            expect(lhs).to.be.true;\n            break;\n          case 'isFalsy':\n            expect(lhs).to.be.false;\n            break;\n          case 'isJson':\n            expect(lhs).to.be.json;\n            break;\n          case 'isNumber':\n            expect(lhs).to.be.a('number');\n            break;\n          case 'isString':\n            expect(lhs).to.be.a('string');\n            break;\n          case 'isBoolean':\n            expect(lhs).to.be.a('boolean');\n            break;\n          case 'isArray':\n            expect(lhs).to.be.a('array');\n            break;\n          default:\n            expect(lhs).to.equal(rhs);\n            break;\n        }\n\n        assertionResults.push({\n          uid: nanoid(),\n          lhsExpr,\n          rhsExpr,\n          rhsOperand,\n          operator,\n          status: 'pass'\n        });\n      } catch (err) {\n        assertionResults.push({\n          uid: nanoid(),\n          lhsExpr,\n          rhsExpr,\n          rhsOperand,\n          operator,\n          status: 'fail',\n          error: err.message\n        });\n      }\n    }\n\n    request.assertionResults = assertionResults;\n\n    return assertionResults;\n  }\n}\n\nmodule.exports = AssertRuntime;\n"
  },
  {
    "path": "packages/bruno-js/src/runtime/script-runtime.js",
    "content": "const chai = require('chai');\nconst Bru = require('../bru');\nconst BrunoRequest = require('../bruno-request');\nconst BrunoResponse = require('../bruno-response');\nconst { cleanJson } = require('../utils');\nconst { createBruTestResultMethods } = require('../utils/results');\nconst { runScriptInNodeVm } = require('../sandbox/node-vm');\nconst { executeQuickJsVmAsync } = require('../sandbox/quickjs');\nconst { SANDBOX } = require('../utils/sandbox');\n\nclass ScriptRuntime {\n  constructor(props) {\n    this.runtime = props?.runtime || 'quickjs';\n  }\n\n  // This approach is getting out of hand\n  // Need to refactor this to use a single arg (object) instead of 7\n  async runRequestScript(\n    script,\n    request,\n    envVariables,\n    runtimeVariables,\n    collectionPath,\n    onConsoleLog,\n    processEnvVars,\n    scriptingConfig,\n    runRequestByItemPathname,\n    collectionName\n  ) {\n    const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};\n    const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};\n    const collectionVariables = request?.collectionVariables || {};\n    const folderVariables = request?.folderVariables || {};\n    const requestVariables = request?.requestVariables || {};\n    const promptVariables = request?.promptVariables || {};\n    const assertionResults = request?.assertionResults || [];\n    const certsAndProxyConfig = request?.certsAndProxyConfig;\n    const scriptPath = request?.pathname;\n    const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables, certsAndProxyConfig);\n    const req = new BrunoRequest(request);\n\n    // extend bru with result getter methods\n    const { __brunoTestResults, test } = createBruTestResultMethods(bru, assertionResults, chai);\n\n    const context = {\n      bru,\n      req,\n      test,\n      expect: chai.expect,\n      assert: chai.assert,\n      __brunoTestResults: __brunoTestResults\n    };\n\n    if (onConsoleLog && typeof onConsoleLog === 'function') {\n      const customLogger = (type) => {\n        return (...args) => {\n          onConsoleLog(type, cleanJson(args));\n        };\n      };\n      context.console = {\n        log: customLogger('log'),\n        debug: customLogger('debug'),\n        info: customLogger('info'),\n        warn: customLogger('warn'),\n        error: customLogger('error')\n      };\n    }\n\n    if (runRequestByItemPathname) {\n      context.bru.runRequest = runRequestByItemPathname;\n    }\n\n    // Helper to build the result object for pre-request scripts\n    // Extracted to avoid duplication across runtime branches\n    const buildRequestScriptResult = () => ({\n      request,\n      envVariables: cleanJson(envVariables),\n      runtimeVariables: cleanJson(runtimeVariables),\n      persistentEnvVariables: bru.persistentEnvVariables,\n      globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),\n      oauth2CredentialsToReset: bru.oauth2CredentialsToReset,\n      results: cleanJson(__brunoTestResults.getResults()),\n      nextRequestName: bru.nextRequest,\n      skipRequest: bru.skipRequest,\n      stopExecution: bru.stopExecution\n    });\n\n    // Track script errors to attach partial results before re-throwing\n    // This ensures that any test() calls that passed before the error are preserved\n    // Similar pattern to test-runtime.js which already handles this correctly\n    let scriptError = null;\n\n    if (this.runtime === SANDBOX.NODEVM) {\n      try {\n        await runScriptInNodeVm({\n          script,\n          context,\n          collectionPath,\n          scriptingConfig,\n          scriptPath\n        });\n      } catch (error) {\n        scriptError = error;\n      }\n\n      // If script errored, attach partial results so callers can display passed tests\n      // before the error occurred (e.g., 2 tests pass, then script throws)\n      if (scriptError) {\n        scriptError.partialResults = buildRequestScriptResult();\n        throw scriptError;\n      }\n\n      return buildRequestScriptResult();\n    }\n\n    // default runtime is `quickjs`\n    try {\n      await executeQuickJsVmAsync({\n        script: script,\n        context: context,\n        collectionPath,\n        scriptPath\n      });\n    } catch (error) {\n      scriptError = error;\n    }\n\n    if (scriptError) {\n      scriptError.partialResults = buildRequestScriptResult();\n      throw scriptError;\n    }\n\n    return buildRequestScriptResult();\n  }\n\n  async runResponseScript(\n    script,\n    request,\n    response,\n    envVariables,\n    runtimeVariables,\n    collectionPath,\n    onConsoleLog,\n    processEnvVars,\n    scriptingConfig,\n    runRequestByItemPathname,\n    collectionName\n  ) {\n    const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};\n    const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};\n    const collectionVariables = request?.collectionVariables || {};\n    const folderVariables = request?.folderVariables || {};\n    const requestVariables = request?.requestVariables || {};\n    const promptVariables = request?.promptVariables || {};\n    const assertionResults = request?.assertionResults || {};\n    const certsAndProxyConfig = request?.certsAndProxyConfig;\n    const scriptPath = request?.pathname;\n    const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables, certsAndProxyConfig);\n    const req = new BrunoRequest(request);\n    const res = new BrunoResponse(response);\n\n    // extend bru with result getter methods\n    const { __brunoTestResults, test } = createBruTestResultMethods(bru, assertionResults, chai);\n\n    const context = {\n      bru,\n      req,\n      res,\n      test,\n      expect: chai.expect,\n      assert: chai.assert,\n      __brunoTestResults: __brunoTestResults\n    };\n\n    if (onConsoleLog && typeof onConsoleLog === 'function') {\n      const customLogger = (type) => {\n        return (...args) => {\n          onConsoleLog(type, cleanJson(args));\n        };\n      };\n      context.console = {\n        log: customLogger('log'),\n        info: customLogger('info'),\n        warn: customLogger('warn'),\n        error: customLogger('error'),\n        debug: customLogger('debug')\n      };\n    }\n\n    if (runRequestByItemPathname) {\n      context.bru.runRequest = runRequestByItemPathname;\n    }\n\n    // Helper to build the result object for post-response scripts\n    // Extracted to avoid duplication across runtime branches\n    const buildResponseScriptResult = () => ({\n      response,\n      envVariables: cleanJson(envVariables),\n      persistentEnvVariables: cleanJson(bru.persistentEnvVariables),\n      runtimeVariables: cleanJson(runtimeVariables),\n      globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),\n      oauth2CredentialsToReset: bru.oauth2CredentialsToReset,\n      results: cleanJson(__brunoTestResults.getResults()),\n      nextRequestName: bru.nextRequest,\n      skipRequest: bru.skipRequest,\n      stopExecution: bru.stopExecution\n    });\n\n    // Track script errors to attach partial results before re-throwing\n    // This ensures that any test() calls that passed before the error are preserved\n    // Similar pattern to test-runtime.js which already handles this correctly\n    let scriptError = null;\n\n    if (this.runtime === SANDBOX.NODEVM) {\n      try {\n        await runScriptInNodeVm({\n          script,\n          context,\n          collectionPath,\n          scriptingConfig,\n          scriptPath\n        });\n      } catch (error) {\n        scriptError = error;\n      }\n\n      // If script errored, attach partial results so callers can display passed tests\n      // before the error occurred (e.g., 2 tests pass, then script throws)\n      if (scriptError) {\n        scriptError.partialResults = buildResponseScriptResult();\n        throw scriptError;\n      }\n\n      return buildResponseScriptResult();\n    }\n\n    // default runtime is `quickjs`\n    try {\n      await executeQuickJsVmAsync({\n        script: script,\n        context: context,\n        collectionPath,\n        scriptPath\n      });\n    } catch (error) {\n      scriptError = error;\n    }\n\n    if (scriptError) {\n      scriptError.partialResults = buildResponseScriptResult();\n      throw scriptError;\n    }\n\n    return buildResponseScriptResult();\n  }\n}\n\nmodule.exports = ScriptRuntime;\n"
  },
  {
    "path": "packages/bruno-js/src/runtime/test-runtime.js",
    "content": "const chai = require('chai');\nconst Bru = require('../bru');\nconst BrunoRequest = require('../bruno-request');\nconst BrunoResponse = require('../bruno-response');\nconst { cleanJson } = require('../utils');\nconst { createBruTestResultMethods } = require('../utils/results');\nconst { runScriptInNodeVm } = require('../sandbox/node-vm');\nconst jsonwebtoken = require('jsonwebtoken');\nconst { executeQuickJsVmAsync } = require('../sandbox/quickjs');\nconst { SANDBOX } = require('../utils/sandbox');\n\nclass TestRuntime {\n  constructor(props) {\n    this.runtime = props?.runtime || 'quickjs';\n  }\n\n  async runTests(\n    testsFile,\n    request,\n    response,\n    envVariables,\n    runtimeVariables,\n    collectionPath,\n    onConsoleLog,\n    processEnvVars,\n    scriptingConfig,\n    runRequestByItemPathname,\n    collectionName\n  ) {\n    const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};\n    const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};\n    const collectionVariables = request?.collectionVariables || {};\n    const folderVariables = request?.folderVariables || {};\n    const requestVariables = request?.requestVariables || {};\n    const promptVariables = request?.promptVariables || {};\n    const assertionResults = request?.assertionResults || [];\n    const certsAndProxyConfig = request?.certsAndProxyConfig;\n    const scriptPath = request?.pathname;\n    const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, collectionName, promptVariables, certsAndProxyConfig);\n    const req = new BrunoRequest(request);\n    const res = new BrunoResponse(response);\n\n    // extend bru with result getter methods\n    const { __brunoTestResults, test } = createBruTestResultMethods(bru, assertionResults, chai);\n\n    if (!testsFile || !testsFile.length) {\n      return {\n        request,\n        envVariables,\n        runtimeVariables,\n        globalEnvironmentVariables,\n        results: __brunoTestResults.getResults(),\n        nextRequestName: bru.nextRequest\n      };\n    }\n\n    const context = {\n      test,\n      bru,\n      req,\n      res,\n      expect: chai.expect,\n      assert: chai.assert,\n      __brunoTestResults: __brunoTestResults,\n      jwt: jsonwebtoken\n    };\n\n    if (onConsoleLog && typeof onConsoleLog === 'function') {\n      const customLogger = (type) => {\n        return (...args) => {\n          onConsoleLog(type, cleanJson(args));\n        };\n      };\n      context.console = {\n        log: customLogger('log'),\n        info: customLogger('info'),\n        warn: customLogger('warn'),\n        debug: customLogger('debug'),\n        error: customLogger('error')\n      };\n    }\n\n    if (runRequestByItemPathname) {\n      context.bru.runRequest = runRequestByItemPathname;\n    }\n\n    let scriptError = null;\n\n    try {\n      if (this.runtime === SANDBOX.NODEVM) {\n        await runScriptInNodeVm({\n          script: testsFile,\n          context,\n          collectionPath,\n          scriptingConfig,\n          scriptPath\n        });\n      } else {\n        // default runtime is `quickjs`\n        await executeQuickJsVmAsync({\n          script: testsFile,\n          context: context,\n          collectionPath,\n          scriptPath\n        });\n      }\n    } catch (error) {\n      scriptError = error;\n    }\n\n    const result = {\n      request,\n      envVariables: cleanJson(envVariables),\n      runtimeVariables: cleanJson(runtimeVariables),\n      globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),\n      persistentEnvVariables: cleanJson(bru.persistentEnvVariables),\n      oauth2CredentialsToReset: bru.oauth2CredentialsToReset,\n      results: cleanJson(__brunoTestResults.getResults()),\n      nextRequestName: bru.nextRequest\n    };\n\n    if (scriptError) {\n      scriptError.partialResults = result;\n      throw scriptError;\n    }\n\n    return result;\n  }\n}\n\nmodule.exports = TestRuntime;\n"
  },
  {
    "path": "packages/bruno-js/src/runtime/vars-runtime.js",
    "content": "const _ = require('lodash');\nconst Bru = require('../bru');\nconst BrunoRequest = require('../bruno-request');\nconst { evaluateJsExpression, createResponseParser } = require('../utils');\nconst { cleanJson } = require('../utils');\n\nconst { executeQuickJsVm } = require('../sandbox/quickjs');\n\nconst evaluateJsExpressionBasedOnRuntime = (expr, context, runtime, mode) => {\n  if (runtime === 'quickjs') {\n    return executeQuickJsVm({\n      script: expr,\n      context,\n      scriptType: 'expression'\n    });\n  }\n\n  return evaluateJsExpression(expr, context);\n};\n\nclass VarsRuntime {\n  constructor(props) {\n    this.runtime = props?.runtime || 'quickjs';\n    this.mode = props?.mode || 'developer';\n  }\n\n  runPostResponseVars(vars, request, response, envVariables, runtimeVariables, collectionPath, processEnvVars) {\n    const requestVariables = request?.requestVariables || {};\n    const globalEnvironmentVariables = request?.globalEnvironmentVariables || {};\n    const oauth2CredentialVariables = request?.oauth2CredentialVariables || {};\n    const collectionVariables = request?.collectionVariables || {};\n    const folderVariables = request?.folderVariables || {};\n    const enabledVars = _.filter(vars, (v) => v.enabled);\n    if (!enabledVars.length) {\n      return;\n    }\n\n    const promptVariables = request?.promptVariables || {};\n    const certsAndProxyConfig = request?.certsAndProxyConfig;\n    const bru = new Bru(this.runtime, envVariables, runtimeVariables, processEnvVars, undefined, collectionVariables, folderVariables, requestVariables, globalEnvironmentVariables, oauth2CredentialVariables, undefined, promptVariables, certsAndProxyConfig);\n    const req = new BrunoRequest(request);\n    const res = createResponseParser(response);\n\n    const bruContext = {\n      bru,\n      req,\n      res\n    };\n\n    const context = {\n      ...envVariables,\n      ...runtimeVariables,\n      ...bruContext\n    };\n\n    const errors = new Map();\n    _.each(enabledVars, (v) => {\n      try {\n        const value = evaluateJsExpressionBasedOnRuntime(v.value, context, this.runtime);\n        if (v.name) {\n          bru.setVar(v.name, value);\n        }\n      } catch (error) {\n        errors.set(v.name, error);\n      }\n    });\n\n    let error = null;\n    if (errors.size > 0) {\n      // Format all errors as a single string to be displayed in a toast\n      const errorMessage = [...errors.entries()].map(([name, err]) => `${name}: ${err.message ?? err}`).join('\\n');\n      error = `${errors.size} error${errors.size === 1 ? '' : 's'} in post response variables: \\n${errorMessage}`;\n    }\n\n    return {\n      envVariables,\n      runtimeVariables,\n      globalEnvironmentVariables: cleanJson(globalEnvironmentVariables),\n      persistentEnvVariables: cleanJson(bru.persistentEnvVariables),\n      error\n    };\n  }\n}\n\nmodule.exports = VarsRuntime;\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/bundle-libraries.js",
    "content": "const rollup = require('rollup');\nconst { nodeResolve } = require('@rollup/plugin-node-resolve');\nconst commonjs = require('@rollup/plugin-commonjs');\nconst fs = require('fs');\nconst { terser } = require('rollup-plugin-terser');\n\nconst bundleLibraries = async () => {\n  const codeScript = `\n    import { expect, assert } from 'chai';\n    import { Buffer } from \"buffer\";\n    import moment from \"moment\";\n    import btoa from \"btoa\";\n    import atob from \"atob\";\n    import * as cryptoJs from 'crypto-js';\n    import tv4 from \"tv4\";\n    globalThis.expect = expect;\n    globalThis.assert = assert;\n    globalThis.moment = moment;\n    globalThis.btoa = btoa;\n    globalThis.atob = atob;\n    globalThis.Buffer = Buffer;\n    globalThis.tv4 = tv4;\n    globalThis.requireObject = {\n      ...(globalThis.requireObject || {}),\n      'chai': { expect, assert },\n      'moment': moment,\n      'buffer': { Buffer },\n      'btoa': btoa,\n      'atob': atob,\n      'crypto-js': cryptoJs,\n      'tv4': tv4\n    };\n`;\n\n  const config = {\n    input: {\n      input: 'inline-code',\n      plugins: [\n        {\n          name: 'inline-code-plugin',\n          resolveId(id) {\n            if (id === 'inline-code') {\n              return id;\n            }\n            return null;\n          },\n          load(id) {\n            if (id === 'inline-code') {\n              return codeScript;\n            }\n            return null;\n          }\n        },\n        nodeResolve({\n          preferBuiltins: false,\n          browser: false\n        }),\n        commonjs(),\n        terser()\n      ]\n    },\n    output: {\n      file: './src/sandbox/bundle-browser-rollup.js',\n      format: 'iife',\n      name: 'MyBundle'\n    }\n  };\n\n  try {\n    const bundle = await rollup.rollup(config.input);\n    const { output } = await bundle.generate(config.output);\n    fs.writeFileSync(\n      './src/sandbox/bundle-browser-rollup.js',\n      `\n      const getBundledCode = () => {\n        return function(){\n          ${output?.map((o) => o.code).join('\\n')}\n        }()\n      }\n      module.exports = getBundledCode;\n    `\n    );\n  } catch (error) {\n    console.error('Error while bundling:', error);\n  }\n};\n\nbundleLibraries();\n\nmodule.exports = bundleLibraries;\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/mixins/typed-arrays.js",
    "content": "exports.mixinTypedArrays = (obj) => {\n  Object.assign(obj, {\n    Int8Array: Int8Array,\n    Uint8Array: Uint8Array,\n    Uint8ClampedArray: Uint8ClampedArray,\n    Int16Array: Int16Array,\n    Uint16Array: Uint16Array,\n    Int32Array: Int32Array,\n    Uint32Array: Uint32Array,\n    Float32Array: Float32Array,\n    Float64Array: Float64Array,\n    BigInt64Array: BigInt64Array,\n    BigUint64Array: BigUint64Array\n  });\n};\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/node-vm/cjs-loader.js",
    "content": "const vm = require('node:vm');\nconst fs = require('node:fs');\nconst path = require('node:path');\nconst nodeModule = require('node:module');\n\nconst { isBuiltinModule, isPathWithinAllowedRoots } = require('./utils');\n\n/**\n * Resolve a local module path, handling files and directories\n * Follows Node.js resolution algorithm:\n * 1. Exact path (with extension)\n * 2. Path + .js extension\n * 3. Directory with package.json (main field)\n * 4. Directory with index.js\n * @param {string} fromDir - Directory to resolve from\n * @param {string} moduleName - Module name/path\n * @returns {string} Resolved absolute path\n */\nfunction resolveLocalModulePath(fromDir, moduleName) {\n  const basePath = path.resolve(fromDir, moduleName);\n\n  // 1. If has extension, use as-is\n  if (path.extname(moduleName)) {\n    return path.normalize(basePath);\n  }\n\n  // 2. Try with .js extension\n  const withJs = basePath + '.js';\n  if (fs.existsSync(withJs)) {\n    return path.normalize(withJs);\n  }\n\n  // 3. Check if it's a directory\n  if (fs.existsSync(basePath) && fs.statSync(basePath).isDirectory()) {\n    // 3a. Check for package.json with main field\n    const pkgPath = path.join(basePath, 'package.json');\n    if (fs.existsSync(pkgPath)) {\n      try {\n        const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));\n        if (pkg.main) {\n          const mainPath = path.resolve(basePath, pkg.main);\n          if (fs.existsSync(mainPath)) {\n            return path.normalize(mainPath);\n          }\n        }\n      } catch {\n        // Ignore JSON parse errors, fall through to index.js\n      }\n    }\n\n    // 3b. Check for index.js\n    const indexPath = path.join(basePath, 'index.js');\n    if (fs.existsSync(indexPath)) {\n      return path.normalize(indexPath);\n    }\n  }\n\n  // 4. Fall back to original path (will likely fail with file not found)\n  return path.normalize(basePath);\n}\n\n/**\n * Creates a custom require function with enhanced security and local module support\n * @param {Object} options - Configuration options\n * @param {string} options.collectionPath - Path to the collection directory\n * @param {Object} options.isolatedContext - The VM isolated context created with vm.createContext()\n * @param {string} options.currentModuleDir - Current module directory for resolving relative paths\n * @param {Map} options.localModuleCache - Cache for loaded modules\n * @param {string[]} options.additionalContextRootsAbsolute - Additional allowed root paths\n * @returns {Function} Custom require function\n */\nfunction createCustomRequire({\n  collectionPath,\n  isolatedContext,\n  currentModuleDir = collectionPath,\n  localModuleCache = new Map(),\n  additionalContextRootsAbsolute = []\n}) {\n  return (moduleName) => {\n    const normalizedModuleName = moduleName.replace(/\\\\/g, '/');\n\n    // 1. Handle local modules (./path, ../path)\n    if (normalizedModuleName.startsWith('./') || normalizedModuleName.startsWith('../')) {\n      return loadLocalModule({\n        moduleName: normalizedModuleName,\n        collectionPath,\n        isolatedContext,\n        localModuleCache,\n        currentModuleDir,\n        additionalContextRootsAbsolute\n      });\n    }\n\n    // 2. Handle absolute paths - route through local module security checks\n    // This prevents bypassing additionalContextRoots by using absolute paths\n    if (path.isAbsolute(normalizedModuleName)) {\n      return loadLocalModule({\n        moduleName: normalizedModuleName,\n        collectionPath,\n        isolatedContext,\n        localModuleCache,\n        currentModuleDir,\n        additionalContextRootsAbsolute\n      });\n    }\n\n    // 3. Handle Node.js builtin modules\n    // Note: Builtins are loaded via native require, bypassing VM isolation.\n    // This is intentional - [`developer` mode] node-vm isolation need not be strict for builtins.\n    if (isBuiltinModule(moduleName)) {\n      return require(moduleName);\n    }\n\n    // 4. Handle npm modules - load INTO vm context\n    return loadNpmModule({\n      moduleName,\n      collectionPath,\n      isolatedContext,\n      localModuleCache\n    });\n  };\n}\n\n/**\n * Loads a local module from the filesystem with security checks and caching\n * @param {Object} options - Configuration options\n * @returns {*} The exported content of the loaded module\n * @throws {Error} When module is outside collection path or cannot be loaded\n */\nfunction loadLocalModule({\n  moduleName,\n  collectionPath,\n  isolatedContext,\n  localModuleCache,\n  currentModuleDir,\n  additionalContextRootsAbsolute = []\n}) {\n  // Validate the raw module name doesn't try to escape allowed roots\n  const preliminaryPath = path.resolve(currentModuleDir, moduleName);\n  if (!isPathWithinAllowedRoots(path.normalize(preliminaryPath), additionalContextRootsAbsolute)) {\n    const allowedRootsDisplay = additionalContextRootsAbsolute.map((root) => `  - ${root}`).join('\\n');\n    throw new Error(\n      `Access to files outside of the allowed context roots is not allowed: ${moduleName}\\n\\n`\n      + `Allowed context roots:\\n${allowedRootsDisplay}`\n    );\n  }\n\n  // Resolve the module path, handling files and directories\n  const normalizedFilePath = resolveLocalModulePath(currentModuleDir, moduleName);\n\n  // Final security check after resolution\n  if (!isPathWithinAllowedRoots(normalizedFilePath, additionalContextRootsAbsolute)) {\n    const allowedRootsDisplay = additionalContextRootsAbsolute.map((root) => `  - ${root}`).join('\\n');\n    throw new Error(\n      `Access to files outside of the allowed context roots is not allowed: ${moduleName}\\n\\n`\n      + `Allowed context roots:\\n${allowedRootsDisplay}`\n    );\n  }\n\n  // Check cache - we cache moduleObj, return its exports\n  if (localModuleCache.has(normalizedFilePath)) {\n    return localModuleCache.get(normalizedFilePath).exports;\n  }\n\n  if (!fs.existsSync(normalizedFilePath)) {\n    throw new Error(`Cannot find module ${moduleName}`);\n  }\n\n  const moduleCode = fs.readFileSync(normalizedFilePath, 'utf8');\n  const moduleObj = { exports: {} };\n  const moduleDir = path.dirname(normalizedFilePath);\n\n  // Pre-populate cache with moduleObj BEFORE execution to handle circular dependencies\n  // This allows re-entrant requires to get partial exports (Node.js behavior)\n  // We cache moduleObj (not moduleObj.exports) so that module.exports reassignment works\n  localModuleCache.set(normalizedFilePath, moduleObj);\n\n  // Create require function for nested imports\n  const moduleRequire = createCustomRequire({\n    collectionPath,\n    isolatedContext,\n    currentModuleDir: moduleDir,\n    localModuleCache,\n    additionalContextRootsAbsolute\n  });\n\n  try {\n    // Wrap module code in a function that receives CJS parameters\n    const wrappedCode = `(function(module, exports, require, __filename, __dirname) {\\n${moduleCode}\\n})`;\n    const compiledScript = new vm.Script(wrappedCode, { filename: normalizedFilePath });\n    const moduleFunction = compiledScript.runInContext(isolatedContext);\n    moduleFunction(moduleObj, moduleObj.exports, moduleRequire, normalizedFilePath, moduleDir);\n    return moduleObj.exports;\n  } catch (error) {\n    // Remove failed module from cache to allow retry\n    localModuleCache.delete(normalizedFilePath);\n    throw new Error(`Error loading local module ${moduleName}: ${error.message}`);\n  }\n}\n\n/**\n * Executes a module in the VM context with caching and special file handling\n * @param {Object} options - Configuration options\n * @returns {*} The exported content of the loaded module\n * @throws {Error} When module cannot be loaded\n */\nfunction executeModuleInVmContext({\n  resolvedPath,\n  moduleName,\n  isolatedContext,\n  collectionPath,\n  localModuleCache\n}) {\n  // Check cache - we cache moduleObj, return its exports\n  if (localModuleCache.has(resolvedPath)) {\n    return localModuleCache.get(resolvedPath).exports;\n  }\n\n  // Native modules (.node files) - fall back to host require\n  // Note: This bypasses VM isolation for native addons.\n  // This is intentional - [`developer` mode] node-vm isolation need not be strict for native modules.\n  if (resolvedPath.endsWith('.node')) {\n    const result = require(resolvedPath);\n    // Wrap in moduleObj format for consistent cache retrieval\n    localModuleCache.set(resolvedPath, { exports: result });\n    return result;\n  }\n\n  // JSON files - parse directly\n  if (resolvedPath.endsWith('.json')) {\n    const jsonContent = fs.readFileSync(resolvedPath, 'utf8');\n    const result = JSON.parse(jsonContent);\n    // Wrap in moduleObj format for consistent cache retrieval\n    localModuleCache.set(resolvedPath, { exports: result });\n    return result;\n  }\n\n  // JavaScript files\n  const moduleSource = fs.readFileSync(resolvedPath, 'utf8');\n  const moduleDir = path.dirname(resolvedPath);\n  const moduleObj = { exports: {} };\n\n  // Pre-populate cache with moduleObj BEFORE execution to handle circular dependencies\n  // This allows re-entrant requires to get partial exports (Node.js behavior)\n  // We cache moduleObj (not moduleObj.exports) so that module.exports reassignment works\n  localModuleCache.set(resolvedPath, moduleObj);\n\n  const moduleRequire = createNpmModuleRequire({\n    collectionPath,\n    isolatedContext,\n    currentModuleDir: moduleDir,\n    localModuleCache\n  });\n\n  try {\n    // Wrap module code in a function that receives CJS parameters\n    const wrappedCode = `(function(module, exports, require, __filename, __dirname) {\\n${moduleSource}\\n})`;\n    const compiledScript = new vm.Script(wrappedCode, { filename: resolvedPath });\n    const moduleFunction = compiledScript.runInContext(isolatedContext);\n    moduleFunction(moduleObj, moduleObj.exports, moduleRequire, resolvedPath, moduleDir);\n  } catch (error) {\n    // Remove failed module from cache to allow retry\n    localModuleCache.delete(resolvedPath);\n    const stack = error.stack || '';\n    throw new Error(`Error loading module ${moduleName}: ${error.message}\\nStack: ${stack}`);\n  }\n\n  return moduleObj.exports;\n}\n\n/**\n * Loads an npm module into the vm context\n * @param {Object} options - Configuration options\n * @returns {*} The exported content of the loaded module\n * @throws {Error} When module cannot be resolved or loaded\n */\nfunction loadNpmModule({\n  moduleName,\n  collectionPath,\n  isolatedContext,\n  localModuleCache\n}) {\n  let resolvedPath;\n\n  // Module resolution order:\n  // 1. Collection's node_modules (user-installed packages for their collection)\n  // 2. Bruno's node_modules (fallback for built-in dependencies)\n  //\n  // This order ensures user packages take precedence, allowing users to:\n  // - Override Bruno's bundled package versions\n  // - Install collection-specific dependencies\n  if (collectionPath) {\n    try {\n      const collectionRequire = nodeModule.createRequire(path.join(collectionPath, 'package.json'));\n      resolvedPath = collectionRequire.resolve(moduleName);\n    } catch {\n      // Module not found in collection, continue to fallback\n    }\n  }\n\n  // Fall back to Bruno's node_modules\n  if (!resolvedPath) {\n    try {\n      resolvedPath = require.resolve(moduleName, { paths: module.paths });\n    } catch (mainError) {\n      throw new Error(\n        `Could not resolve module \"${moduleName}\": ${mainError.message}\\n\\n`\n        + `Install it with: npm install ${moduleName}`\n      );\n    }\n  }\n\n  return executeModuleInVmContext({\n    resolvedPath,\n    moduleName,\n    isolatedContext,\n    collectionPath,\n    localModuleCache\n  });\n}\n\n/**\n * Creates require function for npm module dependencies\n * @param {Object} options - Configuration options\n * @returns {Function} Custom require function for npm module dependencies\n */\nfunction createNpmModuleRequire({\n  collectionPath,\n  isolatedContext,\n  currentModuleDir,\n  localModuleCache\n}) {\n  const moduleRequire = nodeModule.createRequire(path.join(currentModuleDir, 'index.js'));\n\n  return (moduleName) => {\n    // Handle relative imports within npm module\n    if (moduleName.startsWith('./') || moduleName.startsWith('../')) {\n      const resolvedPath = moduleRequire.resolve(moduleName);\n      return executeModuleInVmContext({\n        resolvedPath,\n        moduleName,\n        isolatedContext,\n        collectionPath,\n        localModuleCache\n      });\n    }\n\n    // Handle builtins\n    // Note: Builtins are loaded via native require, bypassing VM isolation.\n    // This is intentional - [`developer` mode] node-vm isolation need not be strict for builtins.\n    if (isBuiltinModule(moduleName)) {\n      return require(moduleName);\n    }\n\n    // Handle npm dependencies - resolve from current module's directory\n    const resolvedPath = moduleRequire.resolve(moduleName);\n    return executeModuleInVmContext({\n      resolvedPath,\n      moduleName,\n      isolatedContext,\n      collectionPath,\n      localModuleCache\n    });\n  };\n}\n\nmodule.exports = {\n  createCustomRequire\n};\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/node-vm/console.js",
    "content": "/**\n * Gets the type tag of a value using Object.prototype.toString\n * This works across VM context boundaries unlike instanceof\n * @param {*} value - The value to check\n * @returns {string} The type tag (e.g., 'Set', 'Map', 'Array', 'Object')\n */\nfunction getTypeTag(value) {\n  return Object.prototype.toString.call(value).slice(8, -1);\n}\n\n/**\n * Transforms a value, converting Set and Map to a special format for display\n * Uses Object.prototype.toString for cross-context type detection\n * @param {*} value - The value to transform\n * @param {WeakSet} seen - Set of already visited objects for circular ref detection\n * @returns {*} Transformed value with Set/Map converted to __brunoType format\n */\nfunction transformValue(value, seen = new WeakSet()) {\n  // Return primitives as-is\n  if (value === null || value === undefined || typeof value !== 'object' && typeof value !== 'function') {\n    return value;\n  }\n\n  // Circular reference check for objects\n  if (typeof value === 'object') {\n    if (seen.has(value)) {\n      return '[Circular]';\n    }\n    seen.add(value);\n  }\n\n  const typeTag = getTypeTag(value);\n\n  if (typeTag === 'Set') {\n    return {\n      __brunoType: 'Set',\n      __brunoValue: Array.from(value).map((item) => transformValue(item, seen))\n    };\n  }\n\n  if (typeTag === 'Map') {\n    return {\n      __brunoType: 'Map',\n      __brunoValue: Array.from(value.entries()).map(([k, v]) => [\n        transformValue(k, seen),\n        transformValue(v, seen)\n      ])\n    };\n  }\n\n  if (typeTag === 'Array') {\n    return value.map((item) => transformValue(item, seen));\n  }\n\n  if (typeTag === 'Object') {\n    const transformed = {};\n    for (const [key, val] of Object.entries(value)) {\n      transformed[key] = transformValue(val, seen);\n    }\n    return transformed;\n  }\n\n  // Handle functions - show clean wrapper\n  if (typeTag === 'Function' || typeof value === 'function') {\n    const name = value.name || 'anonymous';\n    return `function ${name}() {\\n    [native code]\\n}`;\n  }\n\n  // Handle other built-in types (Date, RegExp, Error, etc.) - convert to string representation\n  try {\n    return value?.toString?.() ?? String(value);\n  } catch {\n    return `[${typeTag}]`;\n  }\n}\n\n/**\n * Wraps a console object to add Set/Map support for logging\n * @param {Object} originalConsole - The original console object\n * @returns {Object} Wrapped console with Set/Map transformation\n */\nfunction wrapConsoleWithSerializers(originalConsole) {\n  if (!originalConsole) return originalConsole;\n\n  const methodsToWrap = ['log', 'debug', 'info', 'warn', 'error'];\n  const wrappedConsole = { ...originalConsole };\n\n  for (const method of methodsToWrap) {\n    if (typeof originalConsole[method] === 'function') {\n      wrappedConsole[method] = (...args) => {\n        const transformedArgs = args.map((arg) => transformValue(arg));\n        originalConsole[method](...transformedArgs);\n      };\n    }\n  }\n\n  return wrappedConsole;\n}\n\nmodule.exports = {\n  wrapConsoleWithSerializers\n};\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/node-vm/constants.js",
    "content": "/**\n * Constants for the Node.js VM sandbox.\n *\n * ECMAScript built-ins (Object, Array, Function, etc.)\n * are NOT passed from the host. The VM provides its own versions, ensuring\n * consistent prototype chains for libraries that use introspection.\n *\n * Handled separately in index.js:\n * - global/globalThis: Points to isolated context (not host)\n * - require: createCustomRequire() (custom module loader)\n */\n\n/**\n * Safe globals to pass from host to VM context.\n *\n * ECMAScript built-ins (Object, Array, Function, String, Number,\n * Boolean, Symbol, Date, RegExp, Map, Set, Promise, JSON, Math,\n * parseInt, etc.) are intentionally NOT included here.\n *\n * The VM context provides its own versions of these, which ensures consistent\n * prototype chains. Passing host versions causes prototype mismatches.\n *\n * Only Node.js-specific and Web APIs that the VM doesn't provide are listed.\n */\nconst safeGlobals = [\n  'process',\n\n  // Node.js timers (not part of ECMAScript)\n  'setTimeout',\n  'setInterval',\n  'clearTimeout',\n  'clearInterval',\n  'setImmediate',\n  'clearImmediate',\n  'queueMicrotask',\n\n  // Node.js globals\n  'Buffer',\n\n  // Error types - needed for instanceof checks with errors from host APIs/modules\n  'Error',\n  'TypeError',\n  'ReferenceError',\n  'SyntaxError',\n  'RangeError',\n  'URIError',\n  'EvalError',\n  'AggregateError',\n\n  // URL APIs (WHATWG - not ECMAScript)\n  'URL',\n  'URLSearchParams',\n\n  // Encoding APIs\n  'TextEncoder',\n  'TextDecoder',\n  'atob',\n  'btoa',\n\n  // Fetch API (Node 18+)\n  'fetch',\n  'Request',\n  'Response',\n  'Headers',\n  'FormData',\n  'AbortController',\n  'AbortSignal',\n  'Blob',\n\n  // Streams API\n  'ReadableStream',\n  'WritableStream',\n  'TransformStream',\n\n  // Internationalization (needs host's locale data)\n  'Intl',\n\n  // Web Crypto API\n  'crypto',\n\n  // WebAssembly\n  'WebAssembly',\n\n  // Performance API\n  'performance',\n\n  // Events API\n  'Event',\n  'EventTarget',\n  'CustomEvent',\n\n  // Message passing\n  'MessageChannel',\n  'MessagePort'\n];\n\nmodule.exports = {\n  safeGlobals\n};\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/node-vm/index.js",
    "content": "const vm = require('node:vm');\nconst path = require('node:path');\nconst { get } = require('lodash');\nconst lodash = require('lodash');\nconst { wrapConsoleWithSerializers } = require('./console');\nconst { ScriptError, resolveVmFilename } = require('./utils');\nconst { createCustomRequire } = require('./cjs-loader');\nconst { safeGlobals } = require('./constants');\nconst { mixinTypedArrays } = require('../mixins/typed-arrays');\nconst { wrapScriptInClosure, SANDBOX } = require('../../utils/sandbox');\n\n/**\n * Executes a script in a Node.js VM context with enhanced security and module loading\n *\n * @param {Object} options - Configuration options\n * @param {string} options.script - The script code to execute\n * @param {Object} options.context - The execution context with Bruno objects\n * @param {string} options.collectionPath - Path to the collection directory\n * @param {Object} options.scriptingConfig - Scripting configuration options\n * @param {string} [options.scriptPath] - Path to the source file for accurate stack traces\n * @returns {Promise<Object>} Execution results including variables and test results\n * @throws {ScriptError} When script execution fails\n */\nasync function runScriptInNodeVm({\n  script,\n  context,\n  collectionPath,\n  scriptingConfig,\n  scriptPath\n}) {\n  if (script.trim().length === 0) {\n    return;\n  }\n\n  try {\n    // Compute allowed context roots for security validation\n    const additionalContextRoots = get(scriptingConfig, 'additionalContextRoots', []);\n    const additionalContextRootsAbsolute = lodash\n      .chain(additionalContextRoots)\n      .map((acr) => (path.isAbsolute(acr) ? acr : path.join(collectionPath, acr)))\n      .map((acr) => path.normalize(acr))\n      .value();\n    additionalContextRootsAbsolute.push(path.normalize(collectionPath));\n\n    // Build the script context with Bruno objects and globals\n    const scriptContext = buildScriptContext(context, scriptingConfig);\n\n    // Create truly isolated context - scriptContext becomes the global object\n    // Scripts can ONLY access what's explicitly in scriptContext\n    const isolatedContext = vm.createContext(scriptContext);\n\n    // Add global/globalThis pointing to the isolated context (not host global)\n    // This allows libraries that reference 'global' to work while maintaining isolation\n    scriptContext.global = scriptContext;\n    scriptContext.globalThis = scriptContext;\n\n    // Create module cache for CJS modules\n    const localModuleCache = new Map();\n\n    // Add require() function for CJS module loading\n    scriptContext.require = createCustomRequire({\n      collectionPath,\n      isolatedContext,\n      currentModuleDir: collectionPath,\n      localModuleCache,\n      additionalContextRootsAbsolute\n    });\n\n    const vmFilename = resolveVmFilename(scriptPath, collectionPath);\n\n    // Execute the script in the isolated context\n    const wrappedScript = wrapScriptInClosure(script, SANDBOX.NODEVM);\n    let compiledScript;\n    try {\n      compiledScript = new vm.Script(wrappedScript, {\n        filename: vmFilename\n      });\n    } catch (error) {\n      // V8 puts \"filename:line\" as the first line of syntax error stacks.\n      // Parse it so the error formatter can map to the correct source location.\n      const firstLine = error.stack?.split('\\n')[0];\n      const match = firstLine?.match(/^(.+):(\\d+)$/);\n      if (match && match[1] === vmFilename) {\n        error.__callSites = [{\n          filePath: vmFilename,\n          line: parseInt(match[2], 10),\n          column: null,\n          functionName: null\n        }];\n      }\n      throw error;\n    }\n\n    // Capture structured call sites for error-formatter line mapping\n    const originalPrepareStackTrace = Error.prepareStackTrace;\n    Error.prepareStackTrace = (error, callSites) => {\n      error.__callSites = callSites\n        .filter((site) => site.getFileName() === vmFilename)\n        .map((site) => ({\n          filePath: site.getFileName(),\n          line: site.getLineNumber(),\n          column: site.getColumnNumber(),\n          functionName: site.getFunctionName() || null\n        }));\n\n      return error.toString() + '\\n' + callSites\n        .map((site) => `    at ${site}`)\n        .join('\\n');\n    };\n\n    try {\n      await compiledScript.runInContext(isolatedContext, {\n        displayErrors: true\n      });\n    } catch (error) {\n      // V8 invokes prepareStackTrace lazily on first .stack access.\n      // Reading .stack here so custom handler runs and populates error.__callSites\n      // (used later by the error formatter to map stack frames to the .bru/.yml script)\n      void error.stack;\n      throw error;\n    } finally {\n      Error.prepareStackTrace = originalPrepareStackTrace;\n    }\n  } catch (error) {\n    throw new ScriptError(error, script);\n  }\n}\n\n/**\n * Build the script context with Bruno objects and necessary globals\n * @param {Object} context - Bruno context (bru, req, res, etc.)\n * @param {Object} scriptingConfig - Scripting configuration\n * @returns {Object} Script context object\n */\nfunction buildScriptContext(context, scriptingConfig) {\n  const scriptContext = {\n    ...context,\n\n    // Bruno context (wrap console with Set/Map support)\n    console: wrapConsoleWithSerializers(context.console),\n\n    // Configuration for nested module loading\n    scriptingConfig: scriptingConfig,\n\n    // Safe globals from allowlist (Node.js/Web APIs only, not ECMAScript built-ins)\n    ...Object.fromEntries(\n      safeGlobals\n        .filter((key) => global[key] !== undefined)\n        .map((key) => [key, global[key]])\n    )\n  };\n\n  // Add TypedArrays from host for compatibility with host APIs (TextEncoder, crypto, etc.)\n  mixinTypedArrays(scriptContext);\n\n  return scriptContext;\n}\n\nmodule.exports = {\n  runScriptInNodeVm\n};\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/node-vm/index.spec.js",
    "content": "const { describe, it, expect, beforeEach, afterEach } = require('@jest/globals');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst { runScriptInNodeVm } = require('./index');\n\ndescribe('node-vm sandbox', () => {\n  let testDir;\n  let collectionPath;\n\n  beforeEach(() => {\n    // Create a temporary test directory\n    testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bruno-test-'));\n    collectionPath = path.join(testDir, 'collection');\n    fs.mkdirSync(collectionPath);\n  });\n\n  afterEach(() => {\n    // Clean up test directory\n    fs.rmSync(testDir, { recursive: true, force: true });\n  });\n\n  describe('createCustomRequire - local modules', () => {\n    it('should load local module with ./ path', async () => {\n      // Create a local module\n      fs.writeFileSync(\n        path.join(collectionPath, 'helper.js'),\n        'module.exports = { value: 42 };'\n      );\n\n      const script = `\n        const helper = require('./helper');\n        bru.setVar('result', helper.value);\n      `;\n\n      const context = {\n        bru: { setVar: jest.fn() },\n        console: console\n      };\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', 42);\n    });\n\n    it('should load local module with ../ path', async () => {\n      // Create a subdirectory and modules\n      const subDir = path.join(collectionPath, 'subdir');\n      fs.mkdirSync(subDir);\n      fs.writeFileSync(\n        path.join(collectionPath, 'parent.js'),\n        'module.exports = { name: \"parent\" };'\n      );\n      fs.writeFileSync(\n        path.join(subDir, 'child.js'),\n        'const parent = require(\"../parent\"); module.exports = parent;'\n      );\n\n      const script = `\n        const child = require('./subdir/child');\n        bru.setVar('result', child.name);\n      `;\n\n      const context = {\n        bru: { setVar: jest.fn() },\n        console: console\n      };\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', 'parent');\n    });\n\n    it('should handle backslashes on Windows', async () => {\n      const subDir = path.join(collectionPath, 'utils');\n      fs.mkdirSync(subDir);\n      fs.writeFileSync(\n        path.join(subDir, 'module.js'),\n        'module.exports = { platform: \"cross-platform\" };'\n      );\n\n      // Simulate Windows-style path with backslashes\n      const script = `\n        const mod = require('.\\\\\\\\utils\\\\\\\\module');\n        bru.setVar('result', mod.platform);\n      `;\n\n      const context = {\n        bru: { setVar: jest.fn() },\n        console: console\n      };\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', 'cross-platform');\n    });\n\n    it('should block access outside collection path', async () => {\n      const script = `\n        const outside = require('../../outside');\n      `;\n\n      const context = { console: console };\n\n      await expect(\n        runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} })\n      ).rejects.toThrow('Access to files outside of the allowed context roots is not allowed');\n    });\n\n    it('should block absolute paths outside allowed roots', async () => {\n      // Try to require an absolute path outside the collection\n      const script = `\n        const secret = require('/etc/passwd');\n      `;\n\n      const context = { console: console };\n\n      await expect(\n        runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} })\n      ).rejects.toThrow('Access to files outside of the allowed context roots is not allowed');\n    });\n\n    it('should allow absolute paths within allowed roots', async () => {\n      // Create a module in the collection\n      fs.writeFileSync(\n        path.join(collectionPath, 'absolute-test.js'),\n        'module.exports = { loaded: true };'\n      );\n\n      // Use absolute path to require it\n      const absolutePath = path.join(collectionPath, 'absolute-test.js');\n      const script = `\n        const mod = require('${absolutePath.replace(/\\\\/g, '\\\\\\\\')}');\n        bru.setVar('result', mod.loaded);\n      `;\n\n      const context = {\n        bru: { setVar: jest.fn() },\n        console: console\n      };\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', true);\n    });\n  });\n\n  describe('createCustomRequire - additionalContextRoots', () => {\n    it('should allow module access from additionalContextRoots', async () => {\n      // Create an additional context root at same level as collection\n      const additionalRoot = path.join(testDir, 'shared');\n      fs.mkdirSync(additionalRoot);\n      fs.writeFileSync(\n        path.join(additionalRoot, 'shared.js'),\n        'module.exports = { shared: true };'\n      );\n\n      // From collection, traverse up to testDir, then into shared directory\n      const script = `\n        const shared = require('../shared/shared');\n        bru.setVar('result', shared.shared);\n      `;\n\n      const context = {\n        bru: { setVar: jest.fn() },\n        console: console\n      };\n\n      const scriptingConfig = {\n        additionalContextRoots: [additionalRoot]\n      };\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });\n\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', true);\n    });\n\n    it('should handle relative additionalContextRoots path', async () => {\n      // Create a sibling directory to collection\n      const libsDir = path.join(testDir, 'libs');\n      fs.mkdirSync(libsDir);\n      fs.writeFileSync(\n        path.join(libsDir, 'lib.js'),\n        'module.exports = { fromLib: \"yes\" };'\n      );\n\n      const script = `\n        const lib = require('../libs/lib');\n        bru.setVar('result', lib.fromLib);\n      `;\n\n      const context = {\n        bru: { setVar: jest.fn() },\n        console: console\n      };\n\n      const scriptingConfig = {\n        additionalContextRoots: ['../libs']\n      };\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });\n\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', 'yes');\n    });\n\n    it('should handle nested additional context roots modules', async () => {\n      // Create an additional context root\n      const additionalRoot = path.join(testDir, 'shared');\n      fs.mkdirSync(additionalRoot);\n      fs.writeFileSync(\n        path.join(additionalRoot, 'allowed.js'),\n        'module.exports = { allowed: true };'\n      );\n\n      // Create a nested module that tries to require from additional root\n      fs.writeFileSync(\n        path.join(collectionPath, 'parent.js'),\n        `\n          const allowed = require('../shared/allowed');\n          module.exports = { nestedAccess: allowed.allowed };\n          `\n      );\n\n      const script = `\n          const parent = require('./parent');\n          bru.setVar('result', parent.nestedAccess);\n        `;\n\n      const context = {\n        bru: { setVar: jest.fn() },\n        console: console\n      };\n\n      const scriptingConfig = {\n        additionalContextRoots: [additionalRoot]\n      };\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });\n\n      // Nested module should successfully access the additional root\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', true);\n    });\n  });\n\n  describe('createCustomRequire - npm modules', () => {\n    it('should load npm module', async () => {\n      const script = `\n        const lodash = require('lodash');\n        bru.setVar('result', typeof lodash.get);\n      `;\n\n      const context = {\n        bru: { setVar: jest.fn() },\n        console: console\n      };\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', 'function');\n    });\n  });\n\n  describe('createCustomRequire - module caching', () => {\n    it('should cache loaded modules', async () => {\n      // Module increments a counter each time it's executed\n      // If caching works, counter should only be 1 after multiple requires\n      fs.writeFileSync(\n        path.join(collectionPath, 'cached.js'),\n        `\n        if (!global._cacheTestCount) global._cacheTestCount = 0;\n        global._cacheTestCount++;\n        module.exports = { id: Date.now() };\n        `\n      );\n\n      const script = `\n        const mod1 = require('./cached');\n        const mod2 = require('./cached');\n        const mod3 = require('./cached');\n        bru.setVar('sameInstance', mod1 === mod2 && mod2 === mod3);\n        bru.setVar('loadCount', global._cacheTestCount);\n      `;\n\n      const context = {\n        bru: { setVar: jest.fn() },\n        console: console\n      };\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      // All requires should return the same cached instance\n      expect(context.bru.setVar).toHaveBeenCalledWith('sameInstance', true);\n      // Module should only be executed once\n      expect(context.bru.setVar).toHaveBeenCalledWith('loadCount', 1);\n    });\n\n    it('should handle circular dependencies', async () => {\n      // Create two modules that require each other\n      fs.writeFileSync(\n        path.join(collectionPath, 'circularA.js'),\n        `\n        exports.name = 'A';\n        const B = require('./circularB');\n        exports.fromB = B.name;\n        `\n      );\n      fs.writeFileSync(\n        path.join(collectionPath, 'circularB.js'),\n        `\n        exports.name = 'B';\n        const A = require('./circularA');\n        exports.fromA = A.name;\n        `\n      );\n\n      const script = `\n        const A = require('./circularA');\n        // A loads first, sets exports.name='A', then requires B\n        // B loads, sets exports.name='B', requires A (gets partial: {name:'A'})\n        // B finishes with {name:'B', fromA:'A'}\n        // A finishes with {name:'A', fromB:'B'}\n        bru.setVar('result', A.name + '-' + A.fromB);\n      `;\n\n      const context = {\n        bru: { setVar: jest.fn() },\n        console: console\n      };\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', 'A-B');\n    });\n  });\n\n  describe('createCustomRequire - Node.js builtin modules', () => {\n    it('should load builtin modules (crypto)', async () => {\n      const script = `\n        const crypto = require('crypto');\n        bru.setVar('result', typeof crypto.createHash);\n      `;\n\n      const context = {\n        bru: { setVar: jest.fn() },\n        console: console\n      };\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', 'function');\n    });\n\n    it('should support node: prefix syntax', async () => {\n      const script = `\n        const path = require('node:path');\n        bru.setVar('result', typeof path.join);\n      `;\n\n      const context = {\n        bru: { setVar: jest.fn() },\n        console: console\n      };\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', 'function');\n    });\n\n    it('should allow all builtin modules including fs', async () => {\n      const script = `\n        const fs = require('fs');\n        bru.setVar('result', typeof fs.readFileSync);\n      `;\n\n      const context = {\n        bru: { setVar: jest.fn() },\n        console: console\n      };\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', 'function');\n    });\n\n    it('should load multiple builtins', async () => {\n      const script = `\n        const url = require('url');\n        const util = require('util');\n        const buffer = require('buffer');\n        const fs = require('fs');\n        bru.setVar('result', typeof url.parse + '-' + typeof util.format + '-' + typeof buffer.Buffer + '-' + typeof fs.readFileSync);\n      `;\n\n      const context = {\n        bru: { setVar: jest.fn() },\n        console: console\n      };\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', 'function-function-function-function');\n    });\n  });\n\n  describe('createCustomRequire - npm modules in vm context', () => {\n    it('should load npm modules from collection into vm context', async () => {\n      // Create a mock npm module in collection's node_modules\n      const nodeModulesDir = path.join(collectionPath, 'node_modules', 'test-module');\n      fs.mkdirSync(nodeModulesDir, { recursive: true });\n      fs.writeFileSync(\n        path.join(nodeModulesDir, 'index.js'),\n        'module.exports = { name: \"test-module\", value: 123 };'\n      );\n\n      const script = `\n        const testMod = require('test-module');\n        bru.setVar('result', testMod.name + '-' + testMod.value);\n      `;\n\n      const context = {\n        bru: { setVar: jest.fn() },\n        console: console\n      };\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', 'test-module-123');\n    });\n\n    it('should handle npm module with dependencies', async () => {\n      // Create a mock npm module with internal dependencies\n      const nodeModulesDir = path.join(collectionPath, 'node_modules', 'parent-module');\n      fs.mkdirSync(nodeModulesDir, { recursive: true });\n      fs.writeFileSync(\n        path.join(nodeModulesDir, 'helper.js'),\n        'module.exports = { helper: true };'\n      );\n      fs.writeFileSync(\n        path.join(nodeModulesDir, 'index.js'),\n        'const helper = require(\"./helper\"); module.exports = { hasHelper: helper.helper };'\n      );\n\n      const script = `\n        const parentMod = require('parent-module');\n        bru.setVar('result', parentMod.hasHelper);\n      `;\n\n      const context = {\n        bru: { setVar: jest.fn() },\n        console: console\n      };\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', true);\n    });\n\n    it('should provide bru object to npm modules', async () => {\n      const nodeModulesDir = path.join(collectionPath, 'node_modules', 'bru-access-module');\n      fs.mkdirSync(nodeModulesDir, { recursive: true });\n      fs.writeFileSync(\n        path.join(nodeModulesDir, 'index.js'),\n        `module.exports = {\n          getEnvVar: function(name) { return bru.getEnvVar(name); },\n          setVar: function(name, value) { bru.setVar(name, value); }\n        };`\n      );\n\n      const script = `\n        const bruModule = require('bru-access-module');\n        const envValue = bruModule.getEnvVar('TEST_VAR');\n        bruModule.setVar('result', envValue);\n      `;\n\n      const getEnvVarMock = jest.fn().mockReturnValue('test-value');\n      const setVarMock = jest.fn();\n      const context = {\n        bru: {\n          getEnvVar: getEnvVarMock,\n          setVar: setVarMock\n        },\n        console: console\n      };\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(getEnvVarMock).toHaveBeenCalledWith('TEST_VAR');\n      expect(setVarMock).toHaveBeenCalledWith('result', 'test-value');\n    });\n\n    it('should provide req object to npm modules', async () => {\n      const nodeModulesDir = path.join(collectionPath, 'node_modules', 'req-access-module');\n      fs.mkdirSync(nodeModulesDir, { recursive: true });\n      fs.writeFileSync(\n        path.join(nodeModulesDir, 'index.js'),\n        `module.exports = {\n          getUrl: function() { return req.getUrl(); },\n          getMethod: function() { return req.getMethod(); },\n          setHeader: function(name, value) { req.setHeader(name, value); }\n        };`\n      );\n\n      const script = `\n        const reqModule = require('req-access-module');\n        const url = reqModule.getUrl();\n        const method = reqModule.getMethod();\n        reqModule.setHeader('X-Custom', 'value');\n        bru.setVar('result', method + ':' + url);\n      `;\n\n      const setVarMock = jest.fn();\n      const getUrlMock = jest.fn().mockReturnValue('https://api.example.com');\n      const getMethodMock = jest.fn().mockReturnValue('POST');\n      const setHeaderMock = jest.fn();\n      const context = {\n        bru: { setVar: setVarMock },\n        req: {\n          getUrl: getUrlMock,\n          getMethod: getMethodMock,\n          setHeader: setHeaderMock\n        },\n        console: console\n      };\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(getUrlMock).toHaveBeenCalled();\n      expect(getMethodMock).toHaveBeenCalled();\n      expect(setHeaderMock).toHaveBeenCalledWith('X-Custom', 'value');\n      expect(setVarMock).toHaveBeenCalledWith('result', 'POST:https://api.example.com');\n    });\n\n    it('should provide res object to npm modules', async () => {\n      const nodeModulesDir = path.join(collectionPath, 'node_modules', 'res-access-module');\n      fs.mkdirSync(nodeModulesDir, { recursive: true });\n      fs.writeFileSync(\n        path.join(nodeModulesDir, 'index.js'),\n        `module.exports = {\n          getStatus: function() { return res.getStatus(); },\n          getBody: function() { return res.getBody(); },\n          getHeader: function(name) { return res.getHeader(name); }\n        };`\n      );\n\n      const script = `\n        const resModule = require('res-access-module');\n        const status = resModule.getStatus();\n        const body = resModule.getBody();\n        const contentType = resModule.getHeader('content-type');\n        bru.setVar('result', status + ':' + contentType + ':' + body.message);\n      `;\n\n      const context = {\n        bru: { setVar: jest.fn() },\n        res: {\n          getStatus: jest.fn().mockReturnValue(200),\n          getBody: jest.fn().mockReturnValue({ message: 'success' }),\n          getHeader: jest.fn().mockReturnValue('application/json')\n        },\n        console: console\n      };\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(context.res.getStatus).toHaveBeenCalled();\n      expect(context.res.getBody).toHaveBeenCalled();\n      expect(context.res.getHeader).toHaveBeenCalledWith('content-type');\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', '200:application/json:success');\n    });\n\n    it('should provide bru, req, res to nested npm module dependencies', async () => {\n      // Create parent module\n      const parentDir = path.join(collectionPath, 'node_modules', 'parent-ctx-module');\n      fs.mkdirSync(parentDir, { recursive: true });\n      fs.writeFileSync(\n        path.join(parentDir, 'index.js'),\n        `const child = require('./child');\n        module.exports = { childResult: child.getData() };`\n      );\n      // Create child module that accesses context\n      fs.writeFileSync(\n        path.join(parentDir, 'child.js'),\n        `module.exports = {\n          getData: function() {\n            return {\n              envVar: bru.getEnvVar('NESTED_VAR'),\n              reqUrl: req.getUrl(),\n              resStatus: res.getStatus()\n            };\n          }\n        };`\n      );\n\n      const script = `\n        const parent = require('parent-ctx-module');\n        const data = parent.childResult;\n        bru.setVar('result', data.envVar + '|' + data.reqUrl + '|' + data.resStatus);\n      `;\n\n      const getEnvVarMock = jest.fn().mockReturnValue('nested-value');\n      const setVarMock = jest.fn();\n      const getUrlMock = jest.fn().mockReturnValue('https://nested.example.com');\n      const getStatusMock = jest.fn().mockReturnValue(201);\n      const context = {\n        bru: {\n          getEnvVar: getEnvVarMock,\n          setVar: setVarMock\n        },\n        req: {\n          getUrl: getUrlMock\n        },\n        res: {\n          getStatus: getStatusMock\n        },\n        console: console\n      };\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(getEnvVarMock).toHaveBeenCalledWith('NESTED_VAR');\n      expect(getUrlMock).toHaveBeenCalled();\n      expect(getStatusMock).toHaveBeenCalled();\n      expect(setVarMock).toHaveBeenCalledWith('result', 'nested-value|https://nested.example.com|201');\n    });\n\n    describe('CommonJS module patterns', () => {\n      it('should handle module.exports = object pattern', async () => {\n        const nodeModulesDir = path.join(collectionPath, 'node_modules', 'cjs-object');\n        fs.mkdirSync(nodeModulesDir, { recursive: true });\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'index.js'),\n          'module.exports = { foo: \"bar\", num: 42 };'\n        );\n\n        const script = `\n          const mod = require('cjs-object');\n          bru.setVar('result', mod.foo + '-' + mod.num);\n        `;\n\n        const context = {\n          bru: { setVar: jest.fn() },\n          console: console\n        };\n\n        await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n        expect(context.bru.setVar).toHaveBeenCalledWith('result', 'bar-42');\n      });\n\n      it('should handle module.exports = function pattern', async () => {\n        const nodeModulesDir = path.join(collectionPath, 'node_modules', 'cjs-function');\n        fs.mkdirSync(nodeModulesDir, { recursive: true });\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'index.js'),\n          'module.exports = function(x) { return x * 2; };'\n        );\n\n        const script = `\n          const double = require('cjs-function');\n          bru.setVar('result', double(21));\n        `;\n\n        const context = {\n          bru: { setVar: jest.fn() },\n          console: console\n        };\n\n        await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n        expect(context.bru.setVar).toHaveBeenCalledWith('result', 42);\n      });\n\n      it('should handle module.exports = class pattern', async () => {\n        const nodeModulesDir = path.join(collectionPath, 'node_modules', 'cjs-class');\n        fs.mkdirSync(nodeModulesDir, { recursive: true });\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'index.js'),\n          `class Calculator {\n            constructor(val) { this.val = val; }\n            add(x) { return this.val + x; }\n          }\n          module.exports = Calculator;`\n        );\n\n        const script = `\n          const Calculator = require('cjs-class');\n          const calc = new Calculator(10);\n          bru.setVar('result', calc.add(5));\n        `;\n\n        const context = {\n          bru: { setVar: jest.fn() },\n          console: console\n        };\n\n        await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n        expect(context.bru.setVar).toHaveBeenCalledWith('result', 15);\n      });\n\n      it('should handle exports.property pattern', async () => {\n        const nodeModulesDir = path.join(collectionPath, 'node_modules', 'cjs-exports');\n        fs.mkdirSync(nodeModulesDir, { recursive: true });\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'index.js'),\n          `exports.add = function(a, b) { return a + b; };\n          exports.multiply = function(a, b) { return a * b; };\n          exports.VERSION = '1.0.0';`\n        );\n\n        const script = `\n          const math = require('cjs-exports');\n          bru.setVar('result', math.add(2, 3) + '-' + math.multiply(4, 5) + '-' + math.VERSION);\n        `;\n\n        const context = {\n          bru: { setVar: jest.fn() },\n          console: console\n        };\n\n        await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n        expect(context.bru.setVar).toHaveBeenCalledWith('result', '5-20-1.0.0');\n      });\n\n      it('should handle mixed module.exports and exports pattern', async () => {\n        const nodeModulesDir = path.join(collectionPath, 'node_modules', 'cjs-mixed');\n        fs.mkdirSync(nodeModulesDir, { recursive: true });\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'index.js'),\n          `// module.exports takes precedence\n          exports.ignored = 'this will be ignored';\n          module.exports = { actual: 'value' };`\n        );\n\n        const script = `\n          const mod = require('cjs-mixed');\n          bru.setVar('result', mod.actual + '-' + (mod.ignored || 'undefined'));\n        `;\n\n        const context = {\n          bru: { setVar: jest.fn() },\n          console: console\n        };\n\n        await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n        expect(context.bru.setVar).toHaveBeenCalledWith('result', 'value-undefined');\n      });\n    });\n\n    describe('File extension handling', () => {\n      it('should load .cjs files as CommonJS', async () => {\n        const nodeModulesDir = path.join(collectionPath, 'node_modules', 'cjs-ext-module');\n        fs.mkdirSync(nodeModulesDir, { recursive: true });\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'package.json'),\n          '{\"name\": \"cjs-ext-module\", \"main\": \"index.cjs\"}'\n        );\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'index.cjs'),\n          'module.exports = { format: \"cjs\", value: 100 };'\n        );\n\n        const script = `\n          const mod = require('cjs-ext-module');\n          bru.setVar('result', mod.format + '-' + mod.value);\n        `;\n\n        const context = {\n          bru: { setVar: jest.fn() },\n          console: console\n        };\n\n        await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n        expect(context.bru.setVar).toHaveBeenCalledWith('result', 'cjs-100');\n      });\n\n      it('should fail when loading .mjs files (ES modules)', async () => {\n        const nodeModulesDir = path.join(collectionPath, 'node_modules', 'mjs-ext-module');\n        fs.mkdirSync(nodeModulesDir, { recursive: true });\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'package.json'),\n          '{\"name\": \"mjs-ext-module\", \"main\": \"index.mjs\"}'\n        );\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'index.mjs'),\n          'export default { format: \"esm\" };'\n        );\n\n        const script = `\n          const mod = require('mjs-ext-module');\n        `;\n\n        const context = { console: console };\n\n        await expect(\n          runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} })\n        ).rejects.toThrow();\n      });\n\n      it('should load module with package.json main field', async () => {\n        const nodeModulesDir = path.join(collectionPath, 'node_modules', 'custom-main');\n        fs.mkdirSync(path.join(nodeModulesDir, 'lib'), { recursive: true });\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'package.json'),\n          '{\"name\": \"custom-main\", \"main\": \"lib/entry.js\"}'\n        );\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'lib', 'entry.js'),\n          'module.exports = { entry: \"custom-main-lib\" };'\n        );\n\n        const script = `\n          const mod = require('custom-main');\n          bru.setVar('result', mod.entry);\n        `;\n\n        const context = {\n          bru: { setVar: jest.fn() },\n          console: console\n        };\n\n        await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n        expect(context.bru.setVar).toHaveBeenCalledWith('result', 'custom-main-lib');\n      });\n\n      it('should require relative .cjs files within npm module', async () => {\n        const nodeModulesDir = path.join(collectionPath, 'node_modules', 'cjs-relative');\n        fs.mkdirSync(nodeModulesDir, { recursive: true });\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'helper.cjs'),\n          'module.exports = { helperValue: \"from-cjs\" };'\n        );\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'index.js'),\n          'const helper = require(\"./helper.cjs\"); module.exports = helper;'\n        );\n\n        const script = `\n          const mod = require('cjs-relative');\n          bru.setVar('result', mod.helperValue);\n        `;\n\n        const context = {\n          bru: { setVar: jest.fn() },\n          console: console\n        };\n\n        await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n        expect(context.bru.setVar).toHaveBeenCalledWith('result', 'from-cjs');\n      });\n\n      it('should load .json files directly', async () => {\n        const nodeModulesDir = path.join(collectionPath, 'node_modules', 'json-direct');\n        fs.mkdirSync(nodeModulesDir, { recursive: true });\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'package.json'),\n          '{\"name\": \"json-direct\", \"main\": \"data.json\"}'\n        );\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'data.json'),\n          '{\"type\": \"json-main\", \"count\": 42}'\n        );\n\n        const script = `\n          const data = require('json-direct');\n          bru.setVar('result', data.type + '-' + data.count);\n        `;\n\n        const context = {\n          bru: { setVar: jest.fn() },\n          console: console\n        };\n\n        await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n        expect(context.bru.setVar).toHaveBeenCalledWith('result', 'json-main-42');\n      });\n    });\n\n    describe('JSON file handling', () => {\n      it('should load JSON files from npm modules', async () => {\n        const nodeModulesDir = path.join(collectionPath, 'node_modules', 'json-module');\n        fs.mkdirSync(nodeModulesDir, { recursive: true });\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'config.json'),\n          '{\"name\": \"test-config\", \"version\": \"1.0.0\", \"enabled\": true}'\n        );\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'index.js'),\n          'const config = require(\"./config.json\"); module.exports = config;'\n        );\n\n        const script = `\n          const config = require('json-module');\n          bru.setVar('result', config.name + '-' + config.version + '-' + config.enabled);\n        `;\n\n        const context = {\n          bru: { setVar: jest.fn() },\n          console: console\n        };\n\n        await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n        expect(context.bru.setVar).toHaveBeenCalledWith('result', 'test-config-1.0.0-true');\n      });\n\n      it('should handle nested JSON requires', async () => {\n        const nodeModulesDir = path.join(collectionPath, 'node_modules', 'nested-json');\n        fs.mkdirSync(path.join(nodeModulesDir, 'data'), { recursive: true });\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'data', 'schema.json'),\n          '{\"type\": \"object\", \"properties\": {\"id\": {\"type\": \"number\"}}}'\n        );\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'index.js'),\n          'const schema = require(\"./data/schema.json\"); module.exports = { schema };'\n        );\n\n        const script = `\n          const mod = require('nested-json');\n          bru.setVar('result', mod.schema.type + '-' + mod.schema.properties.id.type);\n        `;\n\n        const context = {\n          bru: { setVar: jest.fn() },\n          console: console\n        };\n\n        await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n        expect(context.bru.setVar).toHaveBeenCalledWith('result', 'object-number');\n      });\n    });\n\n    describe('Node.js globals in npm modules', () => {\n      it('should have access to Buffer', async () => {\n        const nodeModulesDir = path.join(collectionPath, 'node_modules', 'buffer-module');\n        fs.mkdirSync(nodeModulesDir, { recursive: true });\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'index.js'),\n          `module.exports = {\n            encode: function(str) { return Buffer.from(str).toString('base64'); },\n            decode: function(b64) { return Buffer.from(b64, 'base64').toString('utf8'); }\n          };`\n        );\n\n        const script = `\n          const bufMod = require('buffer-module');\n          const encoded = bufMod.encode('hello');\n          const decoded = bufMod.decode(encoded);\n          bru.setVar('result', encoded + '-' + decoded);\n        `;\n\n        const context = {\n          bru: { setVar: jest.fn() },\n          console: console\n        };\n\n        await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n        expect(context.bru.setVar).toHaveBeenCalledWith('result', 'aGVsbG8=-hello');\n      });\n\n      it('should have access to URL and URLSearchParams', async () => {\n        const nodeModulesDir = path.join(collectionPath, 'node_modules', 'url-module');\n        fs.mkdirSync(nodeModulesDir, { recursive: true });\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'index.js'),\n          `module.exports = {\n            parseUrl: function(urlStr) {\n              const url = new URL(urlStr);\n              return url.hostname;\n            },\n            buildQuery: function(params) {\n              const search = new URLSearchParams(params);\n              return search.toString();\n            }\n          };`\n        );\n\n        const script = `\n          const urlMod = require('url-module');\n          bru.setVar('result', urlMod.parseUrl('https://example.com/path'));\n        `;\n\n        const context = {\n          bru: { setVar: jest.fn() },\n          console: console\n        };\n\n        await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n        expect(context.bru.setVar).toHaveBeenCalledWith('result', 'example.com');\n      });\n\n      it('should have access to setTimeout/clearTimeout', async () => {\n        const nodeModulesDir = path.join(collectionPath, 'node_modules', 'timer-module');\n        fs.mkdirSync(nodeModulesDir, { recursive: true });\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'index.js'),\n          `module.exports = {\n            hasTimers: function() {\n              return typeof setTimeout === 'function' && typeof clearTimeout === 'function';\n            }\n          };`\n        );\n\n        const script = `\n          const timerMod = require('timer-module');\n          bru.setVar('result', timerMod.hasTimers());\n        `;\n\n        const context = {\n          bru: { setVar: jest.fn() },\n          console: console\n        };\n\n        await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n        expect(context.bru.setVar).toHaveBeenCalledWith('result', true);\n      });\n    });\n\n    describe('Error handling', () => {\n      it('should throw error for non-existent module', async () => {\n        const script = `\n          const mod = require('non-existent-module-xyz');\n        `;\n\n        const context = { console: console };\n\n        await expect(\n          runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} })\n        ).rejects.toThrow('Could not resolve module');\n      });\n\n      it('should throw error for module with syntax error', async () => {\n        const nodeModulesDir = path.join(collectionPath, 'node_modules', 'syntax-error-module');\n        fs.mkdirSync(nodeModulesDir, { recursive: true });\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'index.js'),\n          'module.exports = { invalid syntax here'\n        );\n\n        const script = `\n          const mod = require('syntax-error-module');\n        `;\n\n        const context = { console: console };\n\n        await expect(\n          runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} })\n        ).rejects.toThrow();\n      });\n\n      it('should throw error for module with runtime error', async () => {\n        const nodeModulesDir = path.join(collectionPath, 'node_modules', 'runtime-error-module');\n        fs.mkdirSync(nodeModulesDir, { recursive: true });\n        fs.writeFileSync(\n          path.join(nodeModulesDir, 'index.js'),\n          'throw new Error(\"Module initialization failed\");'\n        );\n\n        const script = `\n          const mod = require('runtime-error-module');\n        `;\n\n        const context = { console: console };\n\n        await expect(\n          runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} })\n        ).rejects.toThrow('Module initialization failed');\n      });\n    });\n  });\n\n  describe('context isolation', () => {\n    it('should have global pointing to isolated context (not host)', async () => {\n      const context = {\n        bru: { setVar: jest.fn() },\n        console: console\n      };\n\n      // global exists but points to isolated context, so global.bru should exist\n      // process is a sanitized object in the isolated context\n      const script = `bru.setVar('result', typeof global.bru === 'object' && typeof global.process === 'object')`;\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', true);\n    });\n\n    it('should not have access to host fs module via globalThis', async () => {\n      const context = {\n        bru: { setVar: jest.fn() },\n        console: console\n      };\n\n      const script = `bru.setVar('result', typeof globalThis.fs)`;\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', 'undefined');\n    });\n\n    it('should throw ReferenceError for undeclared variables', async () => {\n      const context = { console: console };\n\n      const script = `const x = someUndeclaredVar`;\n\n      await expect(\n        runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} })\n      ).rejects.toThrow('someUndeclaredVar is not defined');\n    });\n\n    it('should have access to context objects via globalThis', async () => {\n      const context = {\n        bru: { setVar: jest.fn() },\n        req: { url: 'http://test.com' },\n        console: console\n      };\n\n      const script = `bru.setVar('result', typeof globalThis.req)`;\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', 'object');\n    });\n\n    it('should have access to allowed globals like Buffer', async () => {\n      const context = {\n        bru: { setVar: jest.fn() },\n        console: console\n      };\n\n      const script = `bru.setVar('result', typeof globalThis.Buffer)`;\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', 'function');\n    });\n\n    it('should have access to process object with nextTick', async () => {\n      const context = {\n        bru: { setVar: jest.fn() },\n        console: console\n      };\n\n      const script = `\n        const hasSafeProps = typeof process.version === 'string' && typeof process.platform === 'string';\n        const hasNextTick = typeof process.nextTick === 'function';\n        bru.setVar('result', hasSafeProps && hasNextTick);\n      `;\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', true);\n    });\n\n    it('should work with Array.isArray across context boundaries', async () => {\n      const context = {\n        bru: { setVar: jest.fn() },\n        console: console\n      };\n\n      const script = `\n        const arr = [1, 2, 3];\n        bru.setVar('result', Array.isArray(arr));\n      `;\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', true);\n    });\n\n    it('should have working Object methods', async () => {\n      const context = {\n        bru: { setVar: jest.fn() },\n        console: console\n      };\n\n      const script = `\n        const obj = { a: 1, b: 2 };\n        bru.setVar('result', Object.keys(obj).join(','));\n      `;\n\n      await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });\n\n      expect(context.bru.setVar).toHaveBeenCalledWith('result', 'a,b');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/node-vm/utils.js",
    "content": "const path = require('node:path');\nconst nodeModule = require('node:module');\n\n/**\n * Check if a module is a Node.js builtin\n * @param {string} moduleName - Module name to check\n * @returns {boolean} True if module is a builtin\n */\nfunction isBuiltinModule(moduleName) {\n  const normalized = moduleName.startsWith('node:') ? moduleName.slice(5) : moduleName;\n  return nodeModule.builtinModules.includes(normalized);\n}\n\n/**\n * Validate that a path is within allowed context roots\n * @param {string} normalizedPath - Normalized file path\n * @param {Array<string>} additionalContextRootsAbsolute - Allowed roots\n * @returns {boolean} True if path is within allowed roots\n */\nfunction isPathWithinAllowedRoots(normalizedPath, additionalContextRootsAbsolute) {\n  return additionalContextRootsAbsolute.some((allowedRoot) => {\n    const normalizedAllowedRoot = path.normalize(allowedRoot);\n    const relativePath = path.relative(normalizedAllowedRoot, normalizedPath);\n    return !relativePath.startsWith('..') && !path.isAbsolute(relativePath);\n  });\n}\n\n/**\n * Resolve the VM filename for the script\n * @param {string|null} scriptPath - Path to the source file\n * @param {string} collectionPath - Path to the collection directory\n * @returns {string} Absolute path to use as the VM filename\n */\nfunction resolveVmFilename(scriptPath, collectionPath) {\n  if (scriptPath) {\n    return path.isAbsolute(scriptPath) ? scriptPath : path.join(collectionPath, scriptPath);\n  }\n  return path.join(collectionPath, 'script.js');\n}\n\nclass ScriptError extends Error {\n  constructor(error, script) {\n    super(error.message);\n    this.name = 'ScriptError';\n    this.originalError = error;\n    this.script = script;\n    this.stack = error.stack;\n    this.__callSites = error.__callSites || null;\n  }\n}\n\nmodule.exports = {\n  isBuiltinModule,\n  isPathWithinAllowedRoots,\n  resolveVmFilename,\n  ScriptError\n};\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/quickjs/index.js",
    "content": "const addBruShimToContext = require('./shims/bru');\nconst addBrunoRequestShimToContext = require('./shims/bruno-request');\nconst addConsoleShimToContext = require('./shims/console');\nconst addBrunoResponseShimToContext = require('./shims/bruno-response');\nconst addTestShimToContext = require('./shims/test');\nconst addLibraryShimsToContext = require('./shims/lib');\nconst addLocalModuleLoaderShimToContext = require('./shims/local-module');\nconst { newQuickJSWASMModule, memoizePromiseFactory } = require('quickjs-emscripten');\n\n// execute `npm run sandbox:bundle-libraries` if the below file doesn't exist\nconst getBundledCode = require('../bundle-browser-rollup');\nconst addPathShimToContext = require('./shims/lib/path');\nconst { marshallToVm } = require('./utils');\nconst addCryptoUtilsShimToContext = require('./shims/lib/crypto-utils');\nconst { wrapScriptInClosure, SANDBOX } = require('../../utils/sandbox');\n\nlet QuickJSSyncContext;\nconst loader = memoizePromiseFactory(() => newQuickJSWASMModule());\nconst getContext = (opts) => loader().then((mod) => (QuickJSSyncContext = mod.newContext(opts)));\ngetContext();\n\nconst toNumber = (value) => {\n  const num = Number(value);\n  return Number.isInteger(num) ? parseInt(value, 10) : parseFloat(value);\n};\n\nconst removeQuotes = (str) => {\n  if ((str.startsWith('\"') && str.endsWith('\"')) || (str.startsWith('\\'') && str.endsWith('\\''))) {\n    return str.slice(1, -1);\n  }\n  return str;\n};\n\nconst executeQuickJsVm = ({ script: externalScript, context: externalContext, scriptType = 'template-literal' }) => {\n  if (!externalScript?.length || typeof externalScript !== 'string') {\n    return externalScript;\n  }\n  externalScript = externalScript?.trim();\n\n  if (scriptType === 'template-literal') {\n    if (!isNaN(Number(externalScript))) {\n      const number = Number(externalScript);\n\n      // Check if the number is too high. Too high number might get altered, see #1000\n      if (number > Number.MAX_SAFE_INTEGER) {\n        return externalScript;\n      }\n\n      return toNumber(externalScript);\n    }\n\n    if (externalScript === 'true') return true;\n    if (externalScript === 'false') return false;\n    if (externalScript === 'null') return null;\n    if (externalScript === 'undefined') return undefined;\n\n    externalScript = removeQuotes(externalScript);\n  }\n\n  const vm = QuickJSSyncContext;\n\n  try {\n    const { bru, req, res, ...variables } = externalContext;\n\n    bru && addBruShimToContext(vm, bru);\n    req && addBrunoRequestShimToContext(vm, req);\n    res && addBrunoResponseShimToContext(vm, res);\n\n    Object.entries(variables)?.forEach(([key, value]) => {\n      vm.setProp(vm.global, key, marshallToVm(value, vm));\n    });\n\n    const templateLiteralText = `\\`${externalScript}\\``;\n    const jsExpressionText = `${externalScript}`;\n\n    let scriptText = scriptType === 'template-literal' ? templateLiteralText : jsExpressionText;\n\n    const result = vm.evalCode(scriptText);\n    if (result.error) {\n      let e = vm.dump(result.error);\n      result.error.dispose();\n      return e;\n    } else {\n      let v = vm.dump(result.value);\n      result.value.dispose();\n      return v;\n    }\n  } catch (error) {\n    console.error('Error executing the script!', error);\n  }\n};\n\nconst executeQuickJsVmAsync = async ({ script: externalScript, context: externalContext, collectionPath, scriptPath }) => {\n  if (!externalScript?.length || typeof externalScript !== 'string') {\n    return externalScript;\n  }\n  externalScript = externalScript?.trim();\n\n  try {\n    const module = await newQuickJSWASMModule();\n    const vm = module.newContext();\n\n    // add crypto utilities required by the crypto-js library in bundledCode\n    await addCryptoUtilsShimToContext(vm);\n\n    const bundledCode = getBundledCode?.toString() || '';\n    const moduleLoaderCode = function () {\n      return `\n        globalThis.require = (mod) => {\n          let lib = globalThis.requireObject[mod];\n          let isModuleAPath = (module) => (module?.startsWith('.') || module?.startsWith?.(bru.cwd()))\n          if (lib) {\n            return lib;\n          }\n          else if (isModuleAPath(mod)) {\n            // fetch local module\n            let localModuleCode = globalThis.__brunoLoadLocalModule(mod);\n\n            // compile local module as iife\n            (function (){\n              const initModuleExportsCode = \"const module = { exports: {} };\"\n              const copyModuleExportsCode = \"\\\\n;globalThis.requireObject[mod] = module.exports;\";\n              const patchedRequire = ${`\n                \"\\\\n;\" +\n                \"let require = (subModule) => isModuleAPath(subModule) ? globalThis.require(path.resolve(bru.cwd(), mod, '..', subModule)) : globalThis.require(subModule)\" +\n                \"\\\\n;\" \n              `}\n              eval(initModuleExportsCode + patchedRequire + localModuleCode + copyModuleExportsCode);\n            })();\n\n            // resolve module\n            return globalThis.requireObject[mod];\n          }\n          else {\n            throw new Error(\"Cannot find module \" + mod);\n          }\n        }\n      `;\n    };\n\n    vm.evalCode(\n      `\n        (${bundledCode})()\n        ${moduleLoaderCode()}\n      `\n    );\n\n    const { bru, req, res, test, __brunoTestResults, console: consoleFn } = externalContext;\n\n    consoleFn && addConsoleShimToContext(vm, consoleFn);\n    bru && addBruShimToContext(vm, bru);\n    req && addBrunoRequestShimToContext(vm, req);\n    res && addBrunoResponseShimToContext(vm, res);\n    addLocalModuleLoaderShimToContext(vm, collectionPath);\n    addPathShimToContext(vm);\n\n    await addLibraryShimsToContext(vm);\n\n    test && __brunoTestResults && addTestShimToContext(vm, __brunoTestResults);\n\n    const script = wrapScriptInClosure(externalScript, SANDBOX.QUICKJS);\n\n    const result = vm.evalCode(script, scriptPath);\n    const promiseHandle = vm.unwrapResult(result);\n    const resolvedResult = await vm.resolvePromise(promiseHandle);\n    promiseHandle.dispose();\n    const resolvedHandle = vm.unwrapResult(resolvedResult);\n    resolvedHandle.dispose();\n    // vm.dispose();\n    return;\n  } catch (error) {\n    error.__isQuickJS = true;\n    throw error;\n  }\n};\n\nmodule.exports = {\n  executeQuickJsVm,\n  executeQuickJsVmAsync\n};\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/quickjs/shims/bru.js",
    "content": "const { cleanJson, cleanCircularJson } = require('../../../utils');\nconst { marshallToVm } = require('../utils');\n\nconst addBruShimToContext = (vm, bru) => {\n  const bruObject = vm.newObject();\n  const bruRunnerObject = vm.newObject();\n\n  let cwd = vm.newFunction('cwd', function () {\n    return marshallToVm(bru.cwd(), vm);\n  });\n  vm.setProp(bruObject, 'cwd', cwd);\n  cwd.dispose();\n\n  let getEnvName = vm.newFunction('getEnvName', function () {\n    return marshallToVm(bru.getEnvName(), vm);\n  });\n  vm.setProp(bruObject, 'getEnvName', getEnvName);\n  getEnvName.dispose();\n\n  let getCollectionName = vm.newFunction('getCollectionName', function () {\n    return marshallToVm(bru.getCollectionName(), vm);\n  });\n  vm.setProp(bruObject, 'getCollectionName', getCollectionName);\n  getCollectionName.dispose();\n\n  let isSafeMode = vm.newFunction('isSafeMode', function () {\n    return marshallToVm(bru.isSafeMode(), vm);\n  });\n  vm.setProp(bruObject, 'isSafeMode', isSafeMode);\n  isSafeMode.dispose();\n\n  let getProcessEnv = vm.newFunction('getProcessEnv', function (key) {\n    return marshallToVm(bru.getProcessEnv(vm.dump(key)), vm);\n  });\n  vm.setProp(bruObject, 'getProcessEnv', getProcessEnv);\n  getProcessEnv.dispose();\n\n  let interpolate = vm.newFunction('interpolate', function (str) {\n    return marshallToVm(bru.interpolate(vm.dump(str)), vm);\n  });\n  vm.setProp(bruObject, 'interpolate', interpolate);\n  interpolate.dispose();\n\n  let hasEnvVar = vm.newFunction('hasEnvVar', function (key) {\n    return marshallToVm(bru.hasEnvVar(vm.dump(key)), vm);\n  });\n  vm.setProp(bruObject, 'hasEnvVar', hasEnvVar);\n  hasEnvVar.dispose();\n\n  let getEnvVar = vm.newFunction('getEnvVar', function (key) {\n    return marshallToVm(bru.getEnvVar(vm.dump(key)), vm);\n  });\n  vm.setProp(bruObject, 'getEnvVar', getEnvVar);\n  getEnvVar.dispose();\n\n  let setEnvVar = vm.newFunction('setEnvVar', function (key, value, options = {}) {\n    bru.setEnvVar(vm.dump(key), vm.dump(value), vm.dump(options));\n  });\n  vm.setProp(bruObject, 'setEnvVar', setEnvVar);\n  setEnvVar.dispose();\n\n  let deleteEnvVar = vm.newFunction('deleteEnvVar', function (key) {\n    bru.deleteEnvVar(vm.dump(key));\n  });\n  vm.setProp(bruObject, 'deleteEnvVar', deleteEnvVar);\n  deleteEnvVar.dispose();\n\n  let getAllEnvVars = vm.newFunction('getAllEnvVars', function () {\n    return marshallToVm(bru.getAllEnvVars(), vm);\n  });\n  vm.setProp(bruObject, 'getAllEnvVars', getAllEnvVars);\n  getAllEnvVars.dispose();\n\n  let deleteAllEnvVars = vm.newFunction('deleteAllEnvVars', function () {\n    bru.deleteAllEnvVars();\n  });\n  vm.setProp(bruObject, 'deleteAllEnvVars', deleteAllEnvVars);\n  deleteAllEnvVars.dispose();\n\n  let getGlobalEnvVar = vm.newFunction('getGlobalEnvVar', function (key) {\n    return marshallToVm(bru.getGlobalEnvVar(vm.dump(key)), vm);\n  });\n  vm.setProp(bruObject, 'getGlobalEnvVar', getGlobalEnvVar);\n  getGlobalEnvVar.dispose();\n\n  let getOauth2CredentialVar = vm.newFunction('getOauth2CredentialVar', function (key) {\n    return marshallToVm(bru.getOauth2CredentialVar(vm.dump(key)), vm);\n  });\n  vm.setProp(bruObject, 'getOauth2CredentialVar', getOauth2CredentialVar);\n  getOauth2CredentialVar.dispose();\n\n  let resetOauth2Credential = vm.newFunction('resetOauth2Credential', function (credentialId) {\n    bru.resetOauth2Credential(vm.dump(credentialId));\n  });\n  vm.setProp(bruObject, 'resetOauth2Credential', resetOauth2Credential);\n  resetOauth2Credential.dispose();\n\n  let setGlobalEnvVar = vm.newFunction('setGlobalEnvVar', function (key, value) {\n    bru.setGlobalEnvVar(vm.dump(key), vm.dump(value));\n  });\n  vm.setProp(bruObject, 'setGlobalEnvVar', setGlobalEnvVar);\n  setGlobalEnvVar.dispose();\n\n  // TODO: deleteGlobalEnvVar works in the request lifecycle but does not update the UI.\n  // Re-enable once the UI sync issue is resolved.\n  // let deleteGlobalEnvVar = vm.newFunction('deleteGlobalEnvVar', function (key) {\n  //   bru.deleteGlobalEnvVar(vm.dump(key));\n  // });\n  // vm.setProp(bruObject, 'deleteGlobalEnvVar', deleteGlobalEnvVar);\n  // deleteGlobalEnvVar.dispose();\n\n  let getAllGlobalEnvVars = vm.newFunction('getAllGlobalEnvVars', function () {\n    return marshallToVm(bru.getAllGlobalEnvVars(), vm);\n  });\n  vm.setProp(bruObject, 'getAllGlobalEnvVars', getAllGlobalEnvVars);\n  getAllGlobalEnvVars.dispose();\n\n  // TODO: deleteAllGlobalEnvVars works in the request lifecycle but does not update the UI.\n  // Re-enable once the UI sync issue is resolved.\n  // let deleteAllGlobalEnvVars = vm.newFunction('deleteAllGlobalEnvVars', function () {\n  //   bru.deleteAllGlobalEnvVars();\n  // });\n  // vm.setProp(bruObject, 'deleteAllGlobalEnvVars', deleteAllGlobalEnvVars);\n  // deleteAllGlobalEnvVars.dispose();\n\n  let hasVar = vm.newFunction('hasVar', function (key) {\n    return marshallToVm(bru.hasVar(vm.dump(key)), vm);\n  });\n  vm.setProp(bruObject, 'hasVar', hasVar);\n  hasVar.dispose();\n\n  let getVar = vm.newFunction('getVar', function (key) {\n    return marshallToVm(bru.getVar(vm.dump(key)), vm);\n  });\n  vm.setProp(bruObject, 'getVar', getVar);\n  getVar.dispose();\n\n  let setVar = vm.newFunction('setVar', function (key, value) {\n    bru.setVar(vm.dump(key), vm.dump(value));\n  });\n  vm.setProp(bruObject, 'setVar', setVar);\n  setVar.dispose();\n\n  let deleteVar = vm.newFunction('deleteVar', function (key) {\n    bru.deleteVar(vm.dump(key));\n  });\n  vm.setProp(bruObject, 'deleteVar', deleteVar);\n  deleteVar.dispose();\n\n  let deleteAllVars = vm.newFunction('deleteAllVars', function () {\n    bru.deleteAllVars();\n  });\n  vm.setProp(bruObject, 'deleteAllVars', deleteAllVars);\n  deleteAllVars.dispose();\n\n  let getAllVars = vm.newFunction('getAllVars', function () {\n    return marshallToVm(bru.getAllVars(), vm);\n  });\n  vm.setProp(bruObject, 'getAllVars', getAllVars);\n  getAllVars.dispose();\n\n  let setNextRequest = vm.newFunction('setNextRequest', function (nextRequest) {\n    bru.setNextRequest(vm.dump(nextRequest));\n  });\n  vm.setProp(bruObject, 'setNextRequest', setNextRequest);\n  setNextRequest.dispose();\n\n  let runnerSkipRequest = vm.newFunction('skipRequest', function () {\n    bru?.runner?.skipRequest();\n  });\n  vm.setProp(bruRunnerObject, 'skipRequest', runnerSkipRequest);\n  runnerSkipRequest.dispose();\n\n  let runnerStopExecution = vm.newFunction('stopExecution', function () {\n    bru?.runner?.stopExecution();\n  });\n  vm.setProp(bruRunnerObject, 'stopExecution', runnerStopExecution);\n  runnerStopExecution.dispose();\n\n  let runnerSetNextRequest = vm.newFunction('setNextRequest', function (nextRequest) {\n    bru?.runner?.setNextRequest(vm.dump(nextRequest));\n  });\n  vm.setProp(bruRunnerObject, 'setNextRequest', runnerSetNextRequest);\n  runnerSetNextRequest.dispose();\n\n  let visualize = vm.newFunction('visualize', function (htmlString) {\n    bru.visualize(vm.dump(htmlString));\n  });\n  vm.setProp(bruObject, 'visualize', visualize);\n  visualize.dispose();\n\n  let getSecretVar = vm.newFunction('getSecretVar', function (key) {\n    return marshallToVm(bru.getSecretVar(vm.dump(key)), vm);\n  });\n  vm.setProp(bruObject, 'getSecretVar', getSecretVar);\n  getSecretVar.dispose();\n\n  let getRequestVar = vm.newFunction('getRequestVar', function (key) {\n    return marshallToVm(bru.getRequestVar(vm.dump(key)), vm);\n  });\n  vm.setProp(bruObject, 'getRequestVar', getRequestVar);\n  getRequestVar.dispose();\n\n  let getFolderVar = vm.newFunction('getFolderVar', function (key) {\n    return marshallToVm(bru.getFolderVar(vm.dump(key)), vm);\n  });\n  vm.setProp(bruObject, 'getFolderVar', getFolderVar);\n  getFolderVar.dispose();\n\n  let getCollectionVar = vm.newFunction('getCollectionVar', function (key) {\n    return marshallToVm(bru.getCollectionVar(vm.dump(key)), vm);\n  });\n  vm.setProp(bruObject, 'getCollectionVar', getCollectionVar);\n  getCollectionVar.dispose();\n\n  // TODO: setCollectionVar works in the request lifecycle but does not update the UI.\n  // Re-enable once the UI sync issue is resolved.\n  // let setCollectionVar = vm.newFunction('setCollectionVar', function (key, value) {\n  //   bru.setCollectionVar(vm.dump(key), vm.dump(value));\n  // });\n  // vm.setProp(bruObject, 'setCollectionVar', setCollectionVar);\n  // setCollectionVar.dispose();\n\n  let hasCollectionVar = vm.newFunction('hasCollectionVar', function (key) {\n    return marshallToVm(bru.hasCollectionVar(vm.dump(key)), vm);\n  });\n  vm.setProp(bruObject, 'hasCollectionVar', hasCollectionVar);\n  hasCollectionVar.dispose();\n\n  // TODO: deleteCollectionVar works in the request lifecycle but does not update the UI.\n  // Re-enable once the UI sync issue is resolved.\n  // let deleteCollectionVar = vm.newFunction('deleteCollectionVar', function (key) {\n  //   bru.deleteCollectionVar(vm.dump(key));\n  // });\n  // vm.setProp(bruObject, 'deleteCollectionVar', deleteCollectionVar);\n  // deleteCollectionVar.dispose();\n\n  // TODO: deleteAllCollectionVars works in the request lifecycle but does not update the UI.\n  // Re-enable once the UI sync issue is resolved.\n  // let deleteAllCollectionVars = vm.newFunction('deleteAllCollectionVars', function () {\n  //   bru.deleteAllCollectionVars();\n  // });\n  // vm.setProp(bruObject, 'deleteAllCollectionVars', deleteAllCollectionVars);\n  // deleteAllCollectionVars.dispose();\n\n  // TODO: getAllCollectionVars works in the request lifecycle but does not update the UI.\n  // Re-enable once the UI sync issue is resolved.\n  // let getAllCollectionVars = vm.newFunction('getAllCollectionVars', function () {\n  //   return marshallToVm(bru.getAllCollectionVars(), vm);\n  // });\n  // vm.setProp(bruObject, 'getAllCollectionVars', getAllCollectionVars);\n  // getAllCollectionVars.dispose();\n\n  let getTestResults = vm.newFunction('getTestResults', () => {\n    const promise = vm.newPromise();\n    bru\n      .getTestResults()\n      .then((results) => {\n        promise.resolve(marshallToVm(cleanJson(results), vm));\n      })\n      .catch((err) => {\n        promise.resolve(\n          marshallToVm(\n            cleanJson({\n              message: err.message\n            }),\n            vm\n          )\n        );\n      });\n    promise.settled.then(vm.runtime.executePendingJobs);\n    return promise.handle;\n  });\n  getTestResults.consume((handle) => vm.setProp(bruObject, 'getTestResults', handle));\n\n  let getAssertionResults = vm.newFunction('getAssertionResults', () => {\n    const promise = vm.newPromise();\n    bru\n      .getAssertionResults()\n      .then((results) => {\n        promise.resolve(marshallToVm(cleanJson(results), vm));\n      })\n      .catch((err) => {\n        promise.resolve(\n          marshallToVm(\n            cleanJson({\n              message: err.message\n            }),\n            vm\n          )\n        );\n      });\n    promise.settled.then(vm.runtime.executePendingJobs);\n    return promise.handle;\n  });\n  getAssertionResults.consume((handle) => vm.setProp(bruObject, 'getAssertionResults', handle));\n\n  let runRequestHandle = vm.newFunction('runRequest', (args) => {\n    const promise = vm.newPromise();\n    bru\n      .runRequest(vm.dump(args))\n      .then((response) => {\n        promise.resolve(marshallToVm(cleanCircularJson(response), vm));\n      })\n      .catch((err) => {\n        promise.resolve(\n          marshallToVm(\n            cleanJson({\n              message: err.message\n            }),\n            vm\n          )\n        );\n      });\n    promise.settled.then(vm.runtime.executePendingJobs);\n    return promise.handle;\n  });\n  runRequestHandle.consume((handle) => vm.setProp(bruObject, 'runRequest', handle));\n\n  let sendRequestHandle = vm.newFunction('_sendRequest', (args) => {\n    const promise = vm.newPromise();\n    bru\n      .sendRequest(vm.dump(args))\n      .then((response) => {\n        promise.resolve(marshallToVm(cleanCircularJson(response), vm));\n      })\n      .catch((err) => {\n        promise.reject(\n          marshallToVm(\n            cleanJson(err),\n            vm\n          )\n        );\n      });\n    promise.settled.then(vm.runtime.executePendingJobs);\n    return promise.handle;\n  });\n  sendRequestHandle.consume((handle) => vm.setProp(bruObject, '_sendRequest', handle));\n\n  const sleep = vm.newFunction('sleep', (timer) => {\n    const t = vm.getString(timer);\n    const promise = vm.newPromise();\n    setTimeout(() => {\n      promise.resolve(vm.newString('slept'));\n    }, t);\n    promise.settled.then(vm.runtime.executePendingJobs);\n    return promise.handle;\n  });\n  sleep.consume((handle) => vm.setProp(bruObject, 'sleep', handle));\n\n  let bruCookiesObject = vm.newObject();\n\n  const _jarFn = vm.newFunction('_jar', () => {\n    const nativeJar = bru.cookies.jar();\n    const jarObj = vm.newObject();\n\n    const _getCookieFn = vm.newFunction('_getCookie', (url, cookieName) => {\n      const promise = vm.newPromise();\n      nativeJar.getCookie(vm.dump(url), vm.dump(cookieName), (err, cookie) => {\n        if (err) {\n          promise.reject(marshallToVm(cleanJson(err), vm));\n        } else {\n          promise.resolve(marshallToVm(cleanCircularJson(cookie), vm));\n        }\n      });\n      promise.settled.then(vm.runtime.executePendingJobs);\n      return promise.handle;\n    });\n    _getCookieFn.consume((handle) => vm.setProp(jarObj, '_getCookie', handle));\n\n    const _getCookiesFn = vm.newFunction('_getCookies', (url) => {\n      const promise = vm.newPromise();\n      nativeJar.getCookies(vm.dump(url), (err, cookies) => {\n        if (err) {\n          promise.reject(marshallToVm(cleanJson(err), vm));\n        } else {\n          promise.resolve(marshallToVm(cleanCircularJson(cookies), vm));\n        }\n      });\n      promise.settled.then(vm.runtime.executePendingJobs);\n      return promise.handle;\n    });\n    _getCookiesFn.consume((handle) => vm.setProp(jarObj, '_getCookies', handle));\n\n    const _setCookieFn = vm.newFunction('_setCookie', (url, nameOrCookieObj, value) => {\n      const promise = vm.newPromise();\n      const dumpedUrl = vm.dump(url);\n      const dumpedNameOrObj = vm.dump(nameOrCookieObj);\n\n      // Check if the second argument is an object (cookie object case)\n      if (typeof dumpedNameOrObj === 'object' && dumpedNameOrObj !== null) {\n        // Cookie object case: setCookie(url, cookieObject, callback)\n        nativeJar.setCookie(dumpedUrl, dumpedNameOrObj, (err) => {\n          if (err) {\n            promise.reject(marshallToVm(cleanJson(err), vm));\n          } else {\n            promise.resolve(vm.undefined);\n          }\n        });\n      } else {\n        // Name/value case: setCookie(url, name, value, callback)\n        const dumpedValue = value ? vm.dump(value) : '';\n        nativeJar.setCookie(dumpedUrl, dumpedNameOrObj, dumpedValue, (err) => {\n          if (err) {\n            promise.reject(marshallToVm(cleanJson(err), vm));\n          } else {\n            promise.resolve(vm.undefined);\n          }\n        });\n      }\n\n      promise.settled.then(vm.runtime.executePendingJobs);\n      return promise.handle;\n    });\n    _setCookieFn.consume((handle) => vm.setProp(jarObj, '_setCookie', handle));\n\n    const _setCookiesFn = vm.newFunction('_setCookies', (url, cookiesArray) => {\n      const promise = vm.newPromise();\n\n      nativeJar.setCookies(vm.dump(url), vm.dump(cookiesArray), (err) => {\n        if (err) {\n          promise.reject(marshallToVm(cleanJson(err), vm));\n        } else {\n          promise.resolve(vm.undefined);\n        }\n      });\n      promise.settled.then(vm.runtime.executePendingJobs);\n      return promise.handle;\n    });\n    _setCookiesFn.consume((handle) => vm.setProp(jarObj, '_setCookies', handle));\n\n    const _clearFn = vm.newFunction('_clear', () => {\n      const promise = vm.newPromise();\n      nativeJar.clear((err) => {\n        if (err) {\n          promise.reject(marshallToVm(cleanJson(err), vm));\n        } else {\n          promise.resolve(vm.undefined);\n        }\n      });\n      promise.settled.then(vm.runtime.executePendingJobs);\n      return promise.handle;\n    });\n    _clearFn.consume((handle) => vm.setProp(jarObj, '_clear', handle));\n\n    const _deleteCookiesFn = vm.newFunction('_deleteCookies', (url) => {\n      const promise = vm.newPromise();\n      nativeJar.deleteCookies(vm.dump(url), (err) => {\n        if (err) {\n          promise.reject(marshallToVm(cleanJson(err), vm));\n        } else {\n          promise.resolve(vm.undefined);\n        }\n      });\n      promise.settled.then(vm.runtime.executePendingJobs);\n      return promise.handle;\n    });\n    _deleteCookiesFn.consume((handle) => vm.setProp(jarObj, '_deleteCookies', handle));\n\n    const _deleteCookieFn = vm.newFunction('_deleteCookie', (url, cookieName) => {\n      const promise = vm.newPromise();\n      nativeJar.deleteCookie(vm.dump(url), vm.dump(cookieName), (err) => {\n        if (err) {\n          promise.reject(marshallToVm(cleanJson(err), vm));\n        } else {\n          promise.resolve(vm.undefined);\n        }\n      });\n      promise.settled.then(vm.runtime.executePendingJobs);\n      return promise.handle;\n    });\n    _deleteCookieFn.consume((handle) => vm.setProp(jarObj, '_deleteCookie', handle));\n\n    const _hasCookieFn = vm.newFunction('_hasCookie', (url, cookieName) => {\n      const promise = vm.newPromise();\n      nativeJar.hasCookie(vm.dump(url), vm.dump(cookieName), (err, exists) => {\n        if (err) {\n          promise.reject(marshallToVm(cleanJson(err), vm));\n        } else {\n          promise.resolve(marshallToVm(exists, vm));\n        }\n      });\n      promise.settled.then(vm.runtime.executePendingJobs);\n      return promise.handle;\n    });\n    _hasCookieFn.consume((handle) => vm.setProp(jarObj, '_hasCookie', handle));\n\n    return jarObj;\n  });\n  _jarFn.consume((handle) => vm.setProp(bruCookiesObject, '_jar', handle));\n\n  vm.setProp(bruObject, 'cookies', bruCookiesObject);\n  bruCookiesObject.dispose();\n\n  vm.setProp(bruObject, 'runner', bruRunnerObject);\n  vm.setProp(vm.global, 'bru', bruObject);\n  bruObject.dispose();\n\n  vm.evalCode(`\n    // sendRequest with callback: normalize error.status (axios uses error.response.status) so\n    // tests like expect(error.status).to.eql(404) pass in safe sandbox; return response after\n    // success callback for consistent promise resolution.\n    globalThis.bru.sendRequest = async (requestConfig, callback) => {\n      if (!callback) return await globalThis.bru._sendRequest(requestConfig);\n      try {\n        const response = await globalThis.bru._sendRequest(requestConfig);\n        try {\n          await callback(null, response);\n          return response;\n        }\n        catch(error) {\n          return Promise.reject(error);\n        }\n      }\n      catch(error) {\n        const errObj = JSON.parse(JSON.stringify(error));\n        if (errObj && errObj.response && typeof errObj.response.status === 'number') errObj.status = errObj.response.status;\n        try {\n          await callback(errObj, null);\n        }\n        catch(err) {\n          return Promise.reject(err);\n        }\n      }\n    };\n\n    globalThis.bru.cookies.jar = () => {\n      const _jar = globalThis.bru.cookies._jar();\n\n      const callWithCallback = async (promiseFn, callback) => {\n        if (!callback) return await promiseFn();\n        try {\n          const result = await promiseFn();\n          try { await callback(null, result); } catch(cbErr) { return Promise.reject(cbErr); }\n        } catch(err) {\n          try { await callback(err, null); } catch(cbErr) { return Promise.reject(cbErr); }\n        }\n      };\n\n      return {\n        getCookie: (url, name, cb) => callWithCallback(() => _jar._getCookie(url, name), cb),\n        getCookies: (url, cb) => callWithCallback(() => _jar._getCookies(url), cb),\n        setCookie: (url, nameOrCookieObj, valueOrCallback, maybeCallback) => {\n          if (typeof nameOrCookieObj === 'object' && nameOrCookieObj !== null) {\n            const callback = typeof valueOrCallback === 'function' ? valueOrCallback : undefined;\n            return callWithCallback(() => _jar._setCookie(url, nameOrCookieObj), callback);\n          } else {\n            const value = typeof valueOrCallback === 'string' ? valueOrCallback : '';\n            const callback = typeof maybeCallback === 'function' ? maybeCallback : \n                           (typeof valueOrCallback === 'function' ? valueOrCallback : undefined);\n            return callWithCallback(() => _jar._setCookie(url, nameOrCookieObj, value), callback);\n          }\n        },\n        setCookies: (url, cookiesArray, cb) => callWithCallback(() => _jar._setCookies(url, cookiesArray), cb),\n        clear: (cb) => callWithCallback(() => _jar._clear(), cb),\n        deleteCookies: (url, cb) => callWithCallback(() => _jar._deleteCookies(url), cb),\n        deleteCookie: (url, name, cb) => callWithCallback(() => _jar._deleteCookie(url, name), cb),\n        hasCookie: (url, name, cb) => callWithCallback(() => _jar._hasCookie(url, name), cb)\n      };\n    };\n  `);\n};\n\nmodule.exports = addBruShimToContext;\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/quickjs/shims/bruno-request.js",
    "content": "const { marshallToVm } = require('../utils');\n\nconst addBrunoRequestShimToContext = (vm, req) => {\n  const reqObject = vm.newObject();\n\n  const url = marshallToVm(req.getUrl(), vm);\n  const method = marshallToVm(req.getMethod(), vm);\n  const headers = marshallToVm(req.getHeaders(), vm);\n  const body = marshallToVm(req.getBody(), vm);\n  const timeout = marshallToVm(req.getTimeout(), vm);\n  const name = marshallToVm(req.getName(), vm);\n  const pathParams = marshallToVm(req.getPathParams(), vm);\n  const tags = marshallToVm(req.getTags(), vm);\n\n  vm.setProp(reqObject, 'url', url);\n  vm.setProp(reqObject, 'method', method);\n  vm.setProp(reqObject, 'headers', headers);\n  vm.setProp(reqObject, 'body', body);\n  vm.setProp(reqObject, 'timeout', timeout);\n  vm.setProp(reqObject, 'name', name);\n  vm.setProp(reqObject, 'pathParams', pathParams);\n  vm.setProp(reqObject, 'tags', tags);\n\n  url.dispose();\n  method.dispose();\n  headers.dispose();\n  body.dispose();\n  timeout.dispose();\n  name.dispose();\n  pathParams.dispose();\n  tags.dispose();\n\n  let getUrl = vm.newFunction('getUrl', function () {\n    return marshallToVm(req.getUrl(), vm);\n  });\n  vm.setProp(reqObject, 'getUrl', getUrl);\n  getUrl.dispose();\n\n  let setUrl = vm.newFunction('setUrl', function (url) {\n    req.setUrl(vm.dump(url));\n  });\n  vm.setProp(reqObject, 'setUrl', setUrl);\n  setUrl.dispose();\n\n  let getHost = vm.newFunction('getHost', function () {\n    return marshallToVm(req.getHost(), vm);\n  });\n  vm.setProp(reqObject, 'getHost', getHost);\n  getHost.dispose();\n\n  let getPath = vm.newFunction('getPath', function () {\n    return marshallToVm(req.getPath(), vm);\n  });\n  vm.setProp(reqObject, 'getPath', getPath);\n  getPath.dispose();\n\n  let getQueryString = vm.newFunction('getQueryString', function () {\n    return marshallToVm(req.getQueryString(), vm);\n  });\n  vm.setProp(reqObject, 'getQueryString', getQueryString);\n  getQueryString.dispose();\n\n  let getMethod = vm.newFunction('getMethod', function () {\n    return marshallToVm(req.getMethod(), vm);\n  });\n  vm.setProp(reqObject, 'getMethod', getMethod);\n  getMethod.dispose();\n\n  let getAuthMode = vm.newFunction('getAuthMode', function () {\n    return marshallToVm(req.getAuthMode(), vm);\n  });\n  vm.setProp(reqObject, 'getAuthMode', getAuthMode);\n  getAuthMode.dispose();\n\n  let getName = vm.newFunction('getName', function () {\n    return marshallToVm(req.getName(), vm);\n  });\n  vm.setProp(reqObject, 'getName', getName);\n  getName.dispose();\n\n  let getPathParams = vm.newFunction('getPathParams', function () {\n    return marshallToVm(req.getPathParams(), vm);\n  });\n  vm.setProp(reqObject, 'getPathParams', getPathParams);\n  getPathParams.dispose();\n\n  let setMethod = vm.newFunction('setMethod', function (method) {\n    req.setMethod(vm.dump(method));\n  });\n  vm.setProp(reqObject, 'setMethod', setMethod);\n  setMethod.dispose();\n\n  let getHeaders = vm.newFunction('getHeaders', function () {\n    return marshallToVm(req.getHeaders(), vm);\n  });\n  vm.setProp(reqObject, 'getHeaders', getHeaders);\n  getHeaders.dispose();\n\n  let setHeaders = vm.newFunction('setHeaders', function (headers) {\n    req.setHeaders(vm.dump(headers));\n  });\n  vm.setProp(reqObject, 'setHeaders', setHeaders);\n  setHeaders.dispose();\n\n  let deleteHeaders = vm.newFunction('deleteHeaders', function (headers) {\n    req.deleteHeaders(vm.dump(headers));\n  });\n  vm.setProp(reqObject, 'deleteHeaders', deleteHeaders);\n  deleteHeaders.dispose();\n\n  let getHeader = vm.newFunction('getHeader', function (name) {\n    return marshallToVm(req.getHeader(vm.dump(name)), vm);\n  });\n  vm.setProp(reqObject, 'getHeader', getHeader);\n  getHeader.dispose();\n\n  let setHeader = vm.newFunction('setHeader', function (name, value) {\n    req.setHeader(vm.dump(name), vm.dump(value));\n  });\n  vm.setProp(reqObject, 'setHeader', setHeader);\n  setHeader.dispose();\n\n  let deleteHeader = vm.newFunction('deleteHeader', function (header) {\n    req.deleteHeader(vm.dump(header));\n  });\n  vm.setProp(reqObject, 'deleteHeader', deleteHeader);\n  deleteHeader.dispose();\n\n  let getBody = vm.newFunction('getBody', function (options = {}) {\n    return marshallToVm(req.getBody(vm.dump(options)), vm);\n  });\n\n  vm.setProp(reqObject, 'getBody', getBody);\n  getBody.dispose();\n\n  let setBody = vm.newFunction('setBody', function (data, options = {}) {\n    req.setBody(vm.dump(data), vm.dump(options));\n  });\n  vm.setProp(reqObject, 'setBody', setBody);\n  setBody.dispose();\n\n  let setMaxRedirects = vm.newFunction('setMaxRedirects', function (maxRedirects) {\n    req.setMaxRedirects(vm.dump(maxRedirects));\n  });\n  vm.setProp(reqObject, 'setMaxRedirects', setMaxRedirects);\n  setMaxRedirects.dispose();\n\n  let getTimeout = vm.newFunction('getTimeout', function () {\n    return marshallToVm(req.getTimeout(), vm);\n  });\n  vm.setProp(reqObject, 'getTimeout', getTimeout);\n  getTimeout.dispose();\n\n  let setTimeout = vm.newFunction('setTimeout', function (timeout) {\n    req.setTimeout(vm.dump(timeout));\n  });\n  vm.setProp(reqObject, 'setTimeout', setTimeout);\n  setTimeout.dispose();\n\n  let disableParsingResponseJson = vm.newFunction('disableParsingResponseJson', function () {\n    req.disableParsingResponseJson();\n  });\n  vm.setProp(reqObject, 'disableParsingResponseJson', disableParsingResponseJson);\n  disableParsingResponseJson.dispose();\n\n  let getExecutionMode = vm.newFunction('getExecutionMode', function () {\n    return marshallToVm(req.getExecutionMode(), vm);\n  });\n  vm.setProp(reqObject, 'getExecutionMode', getExecutionMode);\n  getExecutionMode.dispose();\n\n  let getTags = vm.newFunction('getTags', function () {\n    return marshallToVm(req.getTags(), vm);\n  });\n  vm.setProp(reqObject, 'getTags', getTags);\n  getTags.dispose();\n\n  vm.setProp(vm.global, 'req', reqObject);\n  reqObject.dispose();\n};\n\nmodule.exports = addBrunoRequestShimToContext;\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/quickjs/shims/bruno-response.js",
    "content": "const { marshallToVm } = require('../utils');\n\nconst addBrunoResponseShimToContext = (vm, res) => {\n  let resFn = vm.newFunction('res', function (exprStr) {\n    return marshallToVm(res(vm.dump(exprStr)), vm);\n  });\n\n  const status = marshallToVm(res?.status, vm);\n  const statusText = marshallToVm(res?.statusText, vm);\n  const headers = marshallToVm(res?.headers, vm);\n  const body = marshallToVm(res?.body, vm);\n  const responseTime = marshallToVm(res?.responseTime, vm);\n  const url = marshallToVm(res?.url, vm);\n\n  vm.setProp(resFn, 'status', status);\n  vm.setProp(resFn, 'statusText', statusText);\n  vm.setProp(resFn, 'headers', headers);\n  vm.setProp(resFn, 'body', body);\n  vm.setProp(resFn, 'responseTime', responseTime);\n  vm.setProp(resFn, 'url', url);\n\n  status.dispose();\n  headers.dispose();\n  body.dispose();\n  responseTime.dispose();\n  url.dispose();\n  statusText.dispose();\n\n  let getStatusText = vm.newFunction('getStatusText', function () {\n    return marshallToVm(res.getStatusText(), vm);\n  });\n  vm.setProp(resFn, 'getStatusText', getStatusText);\n  getStatusText.dispose();\n\n  let getStatus = vm.newFunction('getStatus', function () {\n    return marshallToVm(res.getStatus(), vm);\n  });\n  vm.setProp(resFn, 'getStatus', getStatus);\n  getStatus.dispose();\n\n  let getHeader = vm.newFunction('getHeader', function (name) {\n    return marshallToVm(res.getHeader(vm.dump(name)), vm);\n  });\n  vm.setProp(resFn, 'getHeader', getHeader);\n  getHeader.dispose();\n\n  let getHeaders = vm.newFunction('getHeaders', function () {\n    return marshallToVm(res.getHeaders(), vm);\n  });\n  vm.setProp(resFn, 'getHeaders', getHeaders);\n  getHeaders.dispose();\n\n  let getBody = vm.newFunction('getBody', function () {\n    return marshallToVm(res.getBody(), vm);\n  });\n  vm.setProp(resFn, 'getBody', getBody);\n  getBody.dispose();\n\n  let getResponseTime = vm.newFunction('getResponseTime', function () {\n    return marshallToVm(res.getResponseTime(), vm);\n  });\n  vm.setProp(resFn, 'getResponseTime', getResponseTime);\n  getResponseTime.dispose();\n\n  let getUrl = vm.newFunction('getUrl', function () {\n    return marshallToVm(res.getUrl(), vm);\n  });\n  vm.setProp(resFn, 'getUrl', getUrl);\n  getUrl.dispose();\n\n  let setBody = vm.newFunction('setBody', function (data) {\n    res.setBody(vm.dump(data));\n  });\n  vm.setProp(resFn, 'setBody', setBody);\n  setBody.dispose();\n\n  let getSize = vm.newFunction('getSize', function () {\n    return marshallToVm(res.getSize(), vm);\n  });\n  vm.setProp(resFn, 'getSize', getSize);\n  getSize.dispose();\n\n  vm.setProp(vm.global, 'res', resFn);\n  resFn.dispose();\n};\n\nmodule.exports = addBrunoResponseShimToContext;\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/quickjs/shims/console.js",
    "content": "const addConsoleShimToContext = (vm, console) => {\n  if (!console) return;\n\n  // Helper function to convert QuickJS values to native values with Set/Map support\n  const dumpWithSerializers = (arg) => {\n    // Track all handles for centralized disposal\n    let nameProp, constructorProp, constructorNameProp, toStringFn, toStringResult;\n    let arrayFn, fromFn, arrayResult;\n\n    try {\n      const argType = vm.typeof(arg);\n\n      // Early return for primitives (string, number, boolean, undefined, null)\n      if (arg == null || arg === vm.null || arg === vm.undefined) {\n        return vm.dump(arg);\n      }\n\n      if (argType !== 'object' && argType !== 'function') {\n        return vm.dump(arg);\n      }\n\n      // Handle functions - show clean wrapper\n      if (argType === 'function') {\n        nameProp = vm.getProp(arg, 'name');\n        const name = nameProp ? vm.dump(nameProp) || 'anonymous' : 'anonymous';\n        return `function ${name}() {\\n    [native code]\\n}`;\n      }\n\n      // Try to get the constructor name to detect Set/Map\n      constructorProp = vm.getProp(arg, 'constructor');\n      if (!constructorProp) {\n        return vm.dump(arg);\n      }\n\n      let constructorName = null;\n      constructorNameProp = vm.getProp(constructorProp, 'name');\n      if (constructorNameProp) {\n        constructorName = vm.dump(constructorNameProp);\n      }\n\n      // Handle Date, RegExp, Error - call toString()\n      if (constructorName === 'Date' || constructorName === 'RegExp' || constructorName?.endsWith?.('Error')) {\n        toStringFn = vm.getProp(arg, 'toString');\n        if (toStringFn) {\n          toStringResult = vm.callFunction(toStringFn, arg);\n          if (toStringResult.error) {\n            return vm.dump(arg);\n          }\n          return vm.dump(toStringResult.value);\n        }\n      }\n\n      // If not a Set or Map, use standard dump\n      if (constructorName !== 'Set' && constructorName !== 'Map') {\n        return vm.dump(arg);\n      }\n\n      // Convert Set or Map to array via Array.from\n      arrayFn = vm.getProp(vm.global, 'Array');\n      if (!arrayFn) {\n        return vm.dump(arg);\n      }\n\n      fromFn = vm.getProp(arrayFn, 'from');\n      if (!fromFn) {\n        return vm.dump(arg);\n      }\n\n      arrayResult = vm.callFunction(fromFn, arrayFn, arg);\n      if (arrayResult.error) {\n        return vm.dump(arg);\n      }\n\n      return {\n        __brunoType: constructorName,\n        __brunoValue: vm.dump(arrayResult.value)\n      };\n    } catch (e) {\n      // Fallback to normal dump\n      return vm.dump(arg);\n    } finally {\n      // Centralized handle disposal - dispose all handles regardless of success or error\n      nameProp?.dispose();\n      constructorProp?.dispose();\n      constructorNameProp?.dispose();\n      toStringFn?.dispose();\n      toStringResult?.value?.dispose();\n      toStringResult?.error?.dispose();\n      arrayFn?.dispose();\n      fromFn?.dispose();\n      arrayResult?.value?.dispose();\n      arrayResult?.error?.dispose();\n    }\n  };\n\n  const consoleHandle = vm.newObject();\n\n  const logHandle = vm.newFunction('log', (...args) => {\n    const nativeArgs = args.map(dumpWithSerializers);\n    console?.log?.(...nativeArgs);\n  });\n\n  const debugHandle = vm.newFunction('debug', (...args) => {\n    const nativeArgs = args.map(dumpWithSerializers);\n    console?.debug?.(...nativeArgs);\n  });\n\n  const infoHandle = vm.newFunction('info', (...args) => {\n    const nativeArgs = args.map(dumpWithSerializers);\n    console?.info?.(...nativeArgs);\n  });\n\n  const warnHandle = vm.newFunction('warn', (...args) => {\n    const nativeArgs = args.map(dumpWithSerializers);\n    console?.warn?.(...nativeArgs);\n  });\n\n  const errorHandle = vm.newFunction('error', (...args) => {\n    const nativeArgs = args.map(dumpWithSerializers);\n    console?.error?.(...nativeArgs);\n  });\n\n  vm.setProp(consoleHandle, 'log', logHandle);\n  vm.setProp(consoleHandle, 'debug', debugHandle);\n  vm.setProp(consoleHandle, 'info', infoHandle);\n  vm.setProp(consoleHandle, 'warn', warnHandle);\n  vm.setProp(consoleHandle, 'error', errorHandle);\n\n  vm.setProp(vm.global, 'console', consoleHandle);\n  consoleHandle.dispose();\n  logHandle.dispose();\n  debugHandle.dispose();\n  infoHandle.dispose();\n  warnHandle.dispose();\n  errorHandle.dispose();\n};\n\nmodule.exports = addConsoleShimToContext;\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/quickjs/shims/lib/axios.js",
    "content": "const axios = require('axios');\nconst { cleanJson } = require('../../../../utils');\nconst { marshallToVm } = require('../../utils');\n\nconst methods = ['get', 'post', 'put', 'patch', 'delete'];\n\nconst buildAxiosErrorData = (err) => {\n  return {\n    message: err.message,\n    code: err.code,\n    isAxiosError: err.isAxiosError,\n    ...(err.response && {\n      response: {\n        status: err.response.status,\n        statusText: err.response.statusText,\n        headers: err.response.headers,\n        data: err.response.data\n      }\n    }),\n    ...(err.config && {\n      config: {\n        url: err.config.url,\n        method: err.config.method,\n        headers: err.config.headers,\n        data: err.config.data\n      }\n    })\n  };\n};\n\nconst addAxiosShimToContext = async (vm) => {\n  methods?.forEach((method) => {\n    const axiosHandle = vm.newFunction(method, (...args) => {\n      const nativeArgs = args.map(vm.dump);\n      const promise = vm.newPromise();\n      axios[method](...nativeArgs)\n        .then((response) => {\n          const { status, headers, data } = response || {};\n          promise.resolve(marshallToVm(cleanJson({ status, headers, data }), vm));\n        })\n        .catch((err) => {\n          promise.reject(marshallToVm(cleanJson(buildAxiosErrorData(err)), vm));\n        });\n      promise.settled.then(vm.runtime.executePendingJobs);\n      return promise.handle;\n    });\n    axiosHandle.consume((handle) => vm.setProp(vm.global, `__bruno__axios__${method}`, handle));\n  });\n\n  const axiosHandle = vm.newFunction('axios', (...args) => {\n    const nativeArgs = args.map(vm.dump);\n    const promise = vm.newPromise();\n    axios(...nativeArgs)\n      .then((response) => {\n        const { status, headers, data } = response || {};\n        promise.resolve(marshallToVm(cleanJson({ status, headers, data }), vm));\n      })\n      .catch((err) => {\n        promise.reject(marshallToVm(cleanJson(buildAxiosErrorData(err)), vm));\n      });\n    promise.settled.then(vm.runtime.executePendingJobs);\n    return promise.handle;\n  });\n  axiosHandle.consume((handle) => vm.setProp(vm.global, `__bruno__axios`, handle));\n\n  vm.evalCode(\n    `\n        globalThis.axios = __bruno__axios;\n        ${methods\n          ?.map((method) => {\n            return `globalThis.axios.${method} = __bruno__axios__${method};`;\n          })\n          ?.join('\\n')}\n        globalThis.requireObject = {\n          ...globalThis.requireObject,\n          axios: globalThis.axios,\n        }\n    `\n  );\n};\n\nmodule.exports = addAxiosShimToContext;\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/quickjs/shims/lib/axios.spec.js",
    "content": "const { describe, it, expect, beforeAll, beforeEach, afterEach, afterAll } = require('@jest/globals');\nconst { newQuickJSWASMModule } = require('quickjs-emscripten');\nconst addAxiosShimToContext = require('./axios');\n\n// Mock axios\njest.mock('axios');\nconst axios = require('axios');\n\ndescribe('axios shim tests', () => {\n  let vm, module;\n\n  beforeAll(async () => {\n    module = await newQuickJSWASMModule();\n  });\n\n  beforeEach(async () => {\n    vm = module.newContext();\n    await addAxiosShimToContext(vm);\n    jest.clearAllMocks();\n  });\n\n  afterEach(() => {\n    if (vm) {\n      try {\n        vm.dispose();\n      } catch (err) {\n        console.error('Error disposing vm', err);\n      }\n      vm = null;\n    }\n  });\n\n  afterAll(() => {\n    if (module) {\n      try {\n        module.dispose();\n      } catch (err) {\n        console.error('Error disposing module', err);\n      }\n      module = null;\n    }\n  });\n\n  describe('successful requests', () => {\n    it('should resolve axios.get with response data', async () => {\n      const mockResponse = {\n        status: 200,\n        headers: { 'content-type': 'application/json' },\n        data: { message: 'success' }\n      };\n      axios.get.mockResolvedValue(mockResponse);\n\n      const result = vm.evalCode(`\n        (async () => {\n          const response = await axios.get('https://api.example.com/data');\n          return response;\n        })()\n      `);\n      const promiseHandle = vm.unwrapResult(result);\n      const resolvedResult = await vm.resolvePromise(promiseHandle);\n      const resolvedHandle = vm.unwrapResult(resolvedResult);\n      const responseData = vm.dump(resolvedHandle);\n\n      resolvedHandle.dispose();\n      promiseHandle.dispose();\n\n      expect(responseData).toEqual({\n        status: 200,\n        headers: { 'content-type': 'application/json' },\n        data: { message: 'success' }\n      });\n    });\n\n    it('should resolve axios.post with response data', async () => {\n      const mockResponse = {\n        status: 201,\n        headers: { 'content-type': 'application/json' },\n        data: { id: 123, created: true }\n      };\n      axios.post.mockResolvedValue(mockResponse);\n\n      const result = vm.evalCode(`\n        (async () => {\n          const response = await axios.post('https://api.example.com/users', { name: 'test' });\n          return response;\n        })()\n      `);\n      const promiseHandle = vm.unwrapResult(result);\n      const resolvedResult = await vm.resolvePromise(promiseHandle);\n      const resolvedHandle = vm.unwrapResult(resolvedResult);\n      const responseData = vm.dump(resolvedHandle);\n\n      resolvedHandle.dispose();\n      promiseHandle.dispose();\n\n      expect(responseData.status).toBe(201);\n      expect(responseData.data).toEqual({ id: 123, created: true });\n    });\n\n    it('should resolve all HTTP methods', async () => {\n      const mockResponse = {\n        status: 200,\n        headers: {},\n        data: { success: true }\n      };\n\n      const methods = ['get', 'post', 'put', 'patch', 'delete'];\n\n      for (const method of methods) {\n        axios[method].mockResolvedValue(mockResponse);\n\n        const result = vm.evalCode(`\n          (async () => {\n            const response = await axios.${method}('https://api.example.com/endpoint');\n            return response.status;\n          })()\n        `);\n        const promiseHandle = vm.unwrapResult(result);\n        const resolvedResult = await vm.resolvePromise(promiseHandle);\n        const resolvedHandle = vm.unwrapResult(resolvedResult);\n        const status = vm.dump(resolvedHandle);\n\n        resolvedHandle.dispose();\n        promiseHandle.dispose();\n\n        expect(status).toBe(200);\n      }\n    });\n  });\n\n  describe('error handling - 4xx/5xx responses', () => {\n    it('should reject on 404 error with full error information', async () => {\n      const mockError = {\n        message: 'Request failed with status code 404',\n        response: {\n          status: 404,\n          statusText: 'Not Found',\n          headers: { 'content-type': 'application/json' },\n          data: { error: 'Resource not found' }\n        },\n        config: {\n          url: 'https://api.example.com/users/999',\n          method: 'get',\n          headers: { Accept: 'application/json' },\n          data: undefined\n        }\n      };\n      axios.get.mockRejectedValue(mockError);\n\n      const result = vm.evalCode(`\n        (async () => {\n          try {\n            await axios.get('https://api.example.com/users/999');\n            return { caught: false };\n          } catch (error) {\n            return {\n              caught: true,\n              message: error.message,\n              status: error.response?.status,\n              statusText: error.response?.statusText,\n              responseData: error.response?.data,\n              configUrl: error.config?.url,\n              configMethod: error.config?.method\n            };\n          }\n        })()\n      `);\n      const promiseHandle = vm.unwrapResult(result);\n      const resolvedResult = await vm.resolvePromise(promiseHandle);\n      const resolvedHandle = vm.unwrapResult(resolvedResult);\n      const errorData = vm.dump(resolvedHandle);\n\n      resolvedHandle.dispose();\n      promiseHandle.dispose();\n\n      expect(errorData.caught).toBe(true);\n      expect(errorData.message).toBe('Request failed with status code 404');\n      expect(errorData.status).toBe(404);\n      expect(errorData.statusText).toBe('Not Found');\n      expect(errorData.responseData).toEqual({ error: 'Resource not found' });\n      expect(errorData.configUrl).toBe('https://api.example.com/users/999');\n      expect(errorData.configMethod).toBe('get');\n    });\n\n    it('should reject on 500 error', async () => {\n      const mockError = {\n        message: 'Request failed with status code 500',\n        response: {\n          status: 500,\n          statusText: 'Internal Server Error',\n          headers: {},\n          data: { error: 'Server error' }\n        },\n        config: {\n          url: 'https://api.example.com/endpoint',\n          method: 'post',\n          headers: {},\n          data: { test: 'data' }\n        }\n      };\n      axios.post.mockRejectedValue(mockError);\n\n      const result = vm.evalCode(`\n        (async () => {\n          try {\n            await axios.post('https://api.example.com/endpoint', { test: 'data' });\n            return { caught: false };\n          } catch (error) {\n            return {\n              caught: true,\n              status: error.response?.status,\n              message: error.message\n            };\n          }\n        })()\n      `);\n      const promiseHandle = vm.unwrapResult(result);\n      const resolvedResult = await vm.resolvePromise(promiseHandle);\n      const resolvedHandle = vm.unwrapResult(resolvedResult);\n      const errorData = vm.dump(resolvedHandle);\n\n      resolvedHandle.dispose();\n      promiseHandle.dispose();\n\n      expect(errorData.caught).toBe(true);\n      expect(errorData.status).toBe(500);\n      expect(errorData.message).toBe('Request failed with status code 500');\n    });\n\n    it('should reject on 401 unauthorized error', async () => {\n      const mockError = {\n        message: 'Request failed with status code 401',\n        response: {\n          status: 401,\n          statusText: 'Unauthorized',\n          headers: { 'www-authenticate': 'Bearer' },\n          data: { error: 'Invalid token' }\n        },\n        config: {\n          url: 'https://api.example.com/protected',\n          method: 'get',\n          headers: { Authorization: 'Bearer invalid' },\n          data: undefined\n        }\n      };\n      axios.get.mockRejectedValue(mockError);\n\n      const result = vm.evalCode(`\n        (async () => {\n          try {\n            await axios.get('https://api.example.com/protected');\n            return { caught: false };\n          } catch (error) {\n            return {\n              caught: true,\n              status: error.response?.status,\n              responseData: error.response?.data\n            };\n          }\n        })()\n      `);\n      const promiseHandle = vm.unwrapResult(result);\n      const resolvedResult = await vm.resolvePromise(promiseHandle);\n      const resolvedHandle = vm.unwrapResult(resolvedResult);\n      const errorData = vm.dump(resolvedHandle);\n\n      resolvedHandle.dispose();\n      promiseHandle.dispose();\n\n      expect(errorData.caught).toBe(true);\n      expect(errorData.status).toBe(401);\n      expect(errorData.responseData).toEqual({ error: 'Invalid token' });\n    });\n  });\n\n  describe('error handling - network errors', () => {\n    it('should reject on network error without response', async () => {\n      const mockError = {\n        message: 'Network Error',\n        config: {\n          url: 'https://api.example.com/endpoint',\n          method: 'get',\n          headers: {},\n          data: undefined\n        }\n      };\n      axios.get.mockRejectedValue(mockError);\n\n      const result = vm.evalCode(`\n        (async () => {\n          try {\n            await axios.get('https://api.example.com/endpoint');\n            return { caught: false };\n          } catch (error) {\n            return {\n              caught: true,\n              message: error.message,\n              hasResponse: !!error.response,\n              configUrl: error.config?.url\n            };\n          }\n        })()\n      `);\n      const promiseHandle = vm.unwrapResult(result);\n      const resolvedResult = await vm.resolvePromise(promiseHandle);\n      const resolvedHandle = vm.unwrapResult(resolvedResult);\n      const errorData = vm.dump(resolvedHandle);\n\n      resolvedHandle.dispose();\n      promiseHandle.dispose();\n\n      expect(errorData.caught).toBe(true);\n      expect(errorData.message).toBe('Network Error');\n      expect(errorData.hasResponse).toBe(false);\n      expect(errorData.configUrl).toBe('https://api.example.com/endpoint');\n    });\n\n    it('should reject on timeout error', async () => {\n      const mockError = {\n        message: 'timeout of 1000ms exceeded',\n        config: {\n          url: 'https://api.example.com/slow',\n          method: 'get',\n          headers: {},\n          data: undefined\n        }\n      };\n      axios.get.mockRejectedValue(mockError);\n\n      const result = vm.evalCode(`\n        (async () => {\n          try {\n            await axios.get('https://api.example.com/slow');\n            return { caught: false };\n          } catch (error) {\n            return {\n              caught: true,\n              message: error.message\n            };\n          }\n        })()\n      `);\n      const promiseHandle = vm.unwrapResult(result);\n      const resolvedResult = await vm.resolvePromise(promiseHandle);\n      const resolvedHandle = vm.unwrapResult(resolvedResult);\n      const errorData = vm.dump(resolvedHandle);\n\n      resolvedHandle.dispose();\n      promiseHandle.dispose();\n\n      expect(errorData.caught).toBe(true);\n      expect(errorData.message).toBe('timeout of 1000ms exceeded');\n    });\n  });\n\n  describe('base axios function', () => {\n    it('should work with axios() base function', async () => {\n      const mockResponse = {\n        status: 200,\n        headers: {},\n        data: { success: true }\n      };\n      axios.mockResolvedValue(mockResponse);\n\n      const result = vm.evalCode(`\n        (async () => {\n          const response = await axios({\n            method: 'GET',\n            url: 'https://api.example.com/data'\n          });\n          return response;\n        })()\n      `);\n      const promiseHandle = vm.unwrapResult(result);\n      const resolvedResult = await vm.resolvePromise(promiseHandle);\n      const resolvedHandle = vm.unwrapResult(resolvedResult);\n      const responseData = vm.dump(resolvedHandle);\n\n      resolvedHandle.dispose();\n      promiseHandle.dispose();\n\n      expect(responseData.status).toBe(200);\n      expect(responseData.data).toEqual({ success: true });\n    });\n\n    it('should reject on error with axios() base function', async () => {\n      const mockError = {\n        message: 'Request failed with status code 403',\n        response: {\n          status: 403,\n          statusText: 'Forbidden',\n          headers: {},\n          data: { error: 'Access denied' }\n        },\n        config: {\n          url: 'https://api.example.com/forbidden',\n          method: 'get',\n          headers: {},\n          data: undefined\n        }\n      };\n      axios.mockRejectedValue(mockError);\n\n      const result = vm.evalCode(`\n        (async () => {\n          try {\n            await axios({\n              method: 'GET',\n              url: 'https://api.example.com/forbidden'\n            });\n            return { caught: false };\n          } catch (error) {\n            return {\n              caught: true,\n              status: error.response?.status,\n              message: error.message\n            };\n          }\n        })()\n      `);\n      const promiseHandle = vm.unwrapResult(result);\n      const resolvedResult = await vm.resolvePromise(promiseHandle);\n      const resolvedHandle = vm.unwrapResult(resolvedResult);\n      const errorData = vm.dump(resolvedHandle);\n\n      resolvedHandle.dispose();\n      promiseHandle.dispose();\n\n      expect(errorData.caught).toBe(true);\n      expect(errorData.status).toBe(403);\n      expect(errorData.message).toBe('Request failed with status code 403');\n    });\n  });\n\n  describe('real-world use case from issue #6342', () => {\n    it('should properly handle token refresh error with full error info', async () => {\n      const mockError = {\n        message: 'Request failed with status code 404',\n        response: {\n          status: 404,\n          statusText: 'Not Found',\n          headers: { 'content-type': 'application/json' },\n          data: { error: 'Realm not found' }\n        },\n        config: {\n          url: 'https://keycloak.example.com/auth/realms/test/protocol/openid-connect/token',\n          method: 'post',\n          headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n          data: 'grant_type=password&client_id=test&username=user&password=pass&scope=openid'\n        }\n      };\n      axios.post.mockRejectedValue(mockError);\n\n      const result = vm.evalCode(`\n        (async () => {\n          const url = 'https://keycloak.example.com/auth/realms/test/protocol/openid-connect/token';\n          const data = 'grant_type=password&client_id=test&username=user&password=pass&scope=openid';\n\n          try {\n            const response = await axios.post(url, data, {\n              headers: { 'Content-Type': 'application/x-www-form-urlencoded' }\n            });\n            return { success: true, token: response.data?.access_token };\n          } catch (error) {\n            return {\n              success: false,\n              errorMessage: error.message,\n              hasConfig: !!error.config,\n              configUrl: error.config?.url,\n              configMethod: error.config?.method,\n              configData: error.config?.data,\n              responseStatus: error.response?.status,\n              responseData: error.response?.data\n            };\n          }\n        })()\n      `);\n      const promiseHandle = vm.unwrapResult(result);\n      const resolvedResult = await vm.resolvePromise(promiseHandle);\n      const resolvedHandle = vm.unwrapResult(resolvedResult);\n      const result_data = vm.dump(resolvedHandle);\n\n      resolvedHandle.dispose();\n      promiseHandle.dispose();\n\n      expect(result_data.success).toBe(false);\n      expect(result_data.errorMessage).toBe('Request failed with status code 404');\n      expect(result_data.hasConfig).toBe(true);\n      expect(result_data.configUrl).toBe('https://keycloak.example.com/auth/realms/test/protocol/openid-connect/token');\n      expect(result_data.configMethod).toBe('post');\n      expect(result_data.responseStatus).toBe(404);\n      expect(result_data.responseData).toEqual({ error: 'Realm not found' });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/quickjs/shims/lib/crypto-utils.js",
    "content": "const crypto = require('node:crypto');\nconst { marshallToVm } = require('../../utils');\nconst { serializeTypedArray, deserializeTypedArray } = require('./utils');\n\n/**\n * Node.js crypto module shim for QuickJS sandbox\n * Implements crypto.randomBytes and crypto.getRandomValues functions\n */\nconst addCryptoUtilsShimToContext = async (vm) => {\n  let randomBytesHandle = vm.newFunction('randomBytes', function (sizeHandle) {\n    try {\n      let size = vm.dump(sizeHandle);\n\n      if (typeof size !== 'number') {\n        throw new TypeError('The \"size\" argument must be of type number');\n      }\n\n      size = Math.trunc(size);\n\n      if (size < 0) {\n        throw new RangeError('The \"size\" argument must be >= 0');\n      }\n\n      if (size > 65536) { // 2^31 - 1 (max safe integer for practical use)\n        throw new RangeError('The \"size\" argument is too large');\n      }\n\n      if (size === 0) {\n        return marshallToVm([], vm);\n      }\n\n      const buffer = crypto.randomBytes(size);\n\n      const byteArray = Array.from(buffer);\n\n      return marshallToVm(byteArray, vm);\n    } catch (error) {\n      const vmError = vm.newError(error.message);\n      vm.setProp(vmError, 'name', vm.newString(error.name));\n\n      throw vmError;\n    }\n  });\n\n  let getRandomValuesHandle = vm.newFunction('getRandomValues', function (arrayHandle) {\n    try {\n      // Receive the serialized array data directly\n      const serializedArray = vm.dump(arrayHandle);\n      const typedArray = deserializeTypedArray(serializedArray);\n\n      if (typedArray.length === 0) {\n        return marshallToVm([], vm);\n      }\n\n      if (typedArray.length > 65536) {\n        throw new Error('getRandomValues: ArrayBufferView byte length exceeds 65536');\n      }\n\n      crypto.getRandomValues(typedArray);\n\n      const byteArray = Array.from(typedArray);\n\n      return marshallToVm(byteArray, vm);\n    } catch (error) {\n      const vmError = vm.newError(error.message);\n      vm.setProp(vmError, 'name', vm.newString(error.name));\n\n      throw vmError;\n    }\n  });\n\n  // Set the functions in global context\n  vm.setProp(vm.global, '__bruno__crypto__randomBytes', randomBytesHandle);\n  vm.setProp(vm.global, '__bruno__crypto__getRandomValues', getRandomValuesHandle);\n  randomBytesHandle.dispose();\n  getRandomValuesHandle.dispose();\n\n  vm.evalCode(`\n    // Helper function for typed array serialization\n    ${serializeTypedArray.toString()}\n    \n    // Create crypto module object following Node.js specifications\n    const cryptoModule = {\n      // node.js crypto.randomBytes API\n      randomBytes: function(size) {\n        const byteArray = globalThis.__bruno__crypto__randomBytes(size);\n        return Buffer.from(Array.from(byteArray));\n      },\n      // node.js crypto.getRandomValues API\n      getRandomValues: function(typedArray) {\n        const serializedTypedArray = serializeTypedArray(typedArray);\n        typedArray.set(globalThis.__bruno__crypto__getRandomValues(serializedTypedArray));\n        return typedArray;\n      },\n    };\n    \n    // Make crypto available globally\n    globalThis.crypto = cryptoModule;\n  `);\n};\n\nmodule.exports = addCryptoUtilsShimToContext;\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/quickjs/shims/lib/crypto-utils.spec.js",
    "content": "const { describe, it, expect } = require('@jest/globals');\nconst { newQuickJSWASMModule } = require('quickjs-emscripten');\nconst addCryptoUtilsShimToContext = require('./crypto-utils');\nconst getBundledCode = require('../../../bundle-browser-rollup');\n\ndescribe('crypto-utils shims tests', () => {\n  let vm, module;\n\n  beforeAll(async () => {\n    module = await newQuickJSWASMModule();\n  });\n\n  beforeEach(async () => {\n    vm = module.newContext();\n    await addCryptoUtilsShimToContext(vm);\n    // required for `Buffer` library usage\n    const bundledCode = getBundledCode?.toString() || '';\n    vm.evalCode(\n      `\n        (${bundledCode})()\n      `\n    );\n  });\n\n  it('should provide crypto.randomBytes function', async () => {\n    const result = vm.evalCode('typeof crypto.randomBytes');\n    const handle = vm.unwrapResult(result);\n    const type = vm.dump(handle);\n    handle.dispose();\n\n    expect(type).toBe('function');\n  });\n\n  it('should provide crypto.getRandomValues function', async () => {\n    const result = vm.evalCode('typeof crypto.getRandomValues');\n    const handle = vm.unwrapResult(result);\n    const type = vm.dump(handle);\n    handle.dispose();\n\n    expect(type).toBe('function');\n  });\n\n  it('should generate random bytes with correct length', async () => {\n    const result = vm.evalCode('crypto.randomBytes(8).length');\n    const handle = vm.unwrapResult(result);\n    const length = vm.dump(handle);\n    handle.dispose();\n\n    expect(length).toBe(8);\n  });\n\n  it('should convert random bytes to hex string', async () => {\n    const result = vm.evalCode('crypto.randomBytes(4).toString(\"hex\").length');\n    const handle = vm.unwrapResult(result);\n    const hexLength = vm.dump(handle);\n    handle.dispose();\n\n    expect(hexLength).toBe(8); // 4 bytes = 8 hex chars\n  });\n\n  it('should fill Uint8Array with getRandomValues', async () => {\n    const result = vm.evalCode(`\n      const arr = new Uint8Array(5);\n      crypto.getRandomValues(arr);\n      arr.length;\n    `);\n    const handle = vm.unwrapResult(result);\n    const length = vm.dump(handle);\n    handle.dispose();\n\n    expect(length).toBe(5);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/quickjs/shims/lib/index.js",
    "content": "const addAxiosShimToContext = require('./axios');\nconst addNanoidShimToContext = require('./nanoid');\nconst addPathShimToContext = require('./path');\nconst addUuidShimToContext = require('./uuid');\nconst addJwtShimToContext = require('./jwt');\n\nconst addLibraryShimsToContext = async (vm) => {\n  await addNanoidShimToContext(vm);\n  await addAxiosShimToContext(vm);\n  await addUuidShimToContext(vm);\n  await addPathShimToContext(vm);\n  await addJwtShimToContext(vm);\n};\n\nmodule.exports = addLibraryShimsToContext;\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/quickjs/shims/lib/jwt.js",
    "content": "const jwt = require('jsonwebtoken');\nconst { marshallToVm, invokeFunction } = require('../../utils');\n\nconst addJwtShimToContext = async (vm) => {\n  // --- sign ---\n  const _jwtSign = vm.newFunction('sign', function (payload, secret, options, callback) {\n    const nativePayload = vm.dump(payload);\n    const nativeSecret = vm.dump(secret);\n\n    let nativeOptions;\n    let callbackHandle = callback;\n    const optionsType = options === undefined ? 'undefined' : vm.typeof(options);\n    if (optionsType === 'function') {\n      callbackHandle = options;\n      nativeOptions = undefined;\n    } else if (optionsType === 'object' && options !== null) {\n      nativeOptions = vm.dump(options);\n    }\n\n    // If a callback is provided\n    if (callbackHandle && vm.typeof(callbackHandle) === 'function') {\n      let tokenResult;\n      let hostError;\n      try {\n        tokenResult = nativeOptions\n          ? jwt.sign(nativePayload, nativeSecret, nativeOptions)\n          : jwt.sign(nativePayload, nativeSecret);\n      } catch (err) {\n        hostError = err;\n      }\n\n      try {\n        if (hostError) {\n          const errVm = vm.newError(hostError.message || String(hostError));\n          invokeFunction(vm, callbackHandle, [errVm, vm.undefined])\n            .catch((e) => {\n              console.warn('[JWT SHIM][sign.cb] callback invocation error:', e);\n            })\n            .finally(() => {\n              errVm.dispose();\n              callbackHandle.dispose();\n            });\n        } else {\n          const tokenVm = marshallToVm(String(tokenResult), vm);\n          invokeFunction(vm, callbackHandle, [vm.null, tokenVm])\n            .catch((e) => {\n              console.warn('[JWT SHIM][sign.cb] callback invocation error:', e);\n            })\n            .finally(() => {\n              tokenVm.dispose();\n              callbackHandle.dispose();\n            });\n        }\n      } catch (e) {\n        console.warn('[JWT SHIM][sign.cb] unexpected error:', e);\n        callbackHandle.dispose();\n      }\n\n      return vm.undefined;\n    }\n\n    try {\n      const token = nativeOptions\n        ? jwt.sign(nativePayload, nativeSecret, nativeOptions)\n        : jwt.sign(nativePayload, nativeSecret);\n      return marshallToVm(token, vm);\n    } catch (err) {\n      throw vm.newError(err.message || String(err));\n    }\n  });\n\n  vm.setProp(vm.global, '__bruno__jwt__sign', _jwtSign);\n  _jwtSign.dispose();\n\n  // --- verify ---\n  const _jwtVerify = vm.newFunction('verify', function (token, secret, options, callback) {\n    const nativeToken = vm.dump(token);\n    const nativeSecret = vm.dump(secret);\n\n    let nativeOptions;\n    let actualCallback = callback;\n\n    const optionsType = options === undefined ? 'undefined' : vm.typeof(options);\n    if (optionsType === 'function') {\n      actualCallback = options;\n      nativeOptions = undefined;\n    } else if (optionsType === 'object' && options !== null) {\n      nativeOptions = vm.dump(options);\n    }\n\n    if (actualCallback && vm.typeof(actualCallback) === 'function') {\n      let decodedResult;\n      let hostError;\n      try {\n        decodedResult = nativeOptions\n          ? jwt.verify(nativeToken, nativeSecret, nativeOptions)\n          : jwt.verify(nativeToken, nativeSecret);\n      } catch (err) {\n        hostError = err;\n      }\n\n      try {\n        if (hostError) {\n          const vmErr = vm.newError(hostError.message || String(hostError));\n          invokeFunction(vm, actualCallback, [vmErr, vm.undefined])\n            .catch((e) => {\n              console.warn('[JWT SHIM][verify.cb] callback invocation error:', e);\n            })\n            .finally(() => {\n              vmErr.dispose();\n              actualCallback.dispose();\n            });\n        } else {\n          const vmNull = vm.null;\n          const vmDecoded = marshallToVm(decodedResult, vm);\n          invokeFunction(vm, actualCallback, [vmNull, vmDecoded])\n            .catch((e) => {\n              console.warn('[JWT SHIM][verify.cb] callback invocation error:', e);\n            })\n            .finally(() => {\n              vmDecoded.dispose();\n              actualCallback.dispose();\n            });\n        }\n      } catch (e) {\n        console.warn('[JWT SHIM][verify.cb] unexpected error:', e);\n        actualCallback.dispose();\n      }\n\n      return vm.undefined;\n    }\n\n    try {\n      const decoded = nativeOptions\n        ? jwt.verify(nativeToken, nativeSecret, nativeOptions)\n        : jwt.verify(nativeToken, nativeSecret);\n      return marshallToVm(decoded, vm);\n    } catch (err) {\n      throw vm.newError(err.message || String(err));\n    }\n  });\n\n  vm.setProp(vm.global, '__bruno__jwt__verify', _jwtVerify);\n  _jwtVerify.dispose();\n\n  // --- decode ---\n  const _jwtDecode = vm.newFunction('decode', function (token, options) {\n    const nativeToken = vm.dump(token);\n\n    let nativeOptions;\n    const optionsType = options === undefined ? 'undefined' : vm.typeof(options);\n    if (optionsType === 'object' && options !== null) {\n      nativeOptions = vm.dump(options);\n    }\n\n    try {\n      const decoded = nativeOptions\n        ? jwt.decode(nativeToken, nativeOptions)\n        : jwt.decode(nativeToken);\n      return marshallToVm(decoded, vm);\n    } catch (err) {\n      throw vm.newError(err.message || String(err));\n    }\n  });\n\n  vm.setProp(vm.global, '__bruno__jwt__decode', _jwtDecode);\n  _jwtDecode.dispose();\n\n  vm.evalCode(`\n    globalThis.jwt = {};\n    globalThis.jwt.sign = globalThis.__bruno__jwt__sign;\n    globalThis.jwt.verify = globalThis.__bruno__jwt__verify;\n    globalThis.jwt.decode = globalThis.__bruno__jwt__decode;\n    globalThis.requireObject = {\n      ...globalThis.requireObject,\n      'jsonwebtoken': globalThis.jwt,\n    };\n  `);\n};\n\nmodule.exports = addJwtShimToContext;\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/quickjs/shims/lib/nanoid.js",
    "content": "const { nanoid } = require('nanoid');\nconst { marshallToVm } = require('../../utils');\n\nconst addNanoidShimToContext = async (vm) => {\n  let _nanoid = vm.newFunction('nanoid', function () {\n    let v = nanoid();\n    return marshallToVm(v, vm);\n  });\n  vm.setProp(vm.global, '__bruno__nanoid', _nanoid);\n  _nanoid.dispose();\n\n  vm.evalCode(\n    `\n      globalThis.nanoid = {};\n      globalThis.nanoid.nanoid = globalThis.__bruno__nanoid;\n      globalThis.requireObject = {\n          ...globalThis.requireObject,\n          'nanoid': globalThis.nanoid\n      }\n    `\n  );\n};\n\nmodule.exports = addNanoidShimToContext;\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/quickjs/shims/lib/path.js",
    "content": "const path = require('path');\nconst { marshallToVm } = require('../../utils');\n\nconst fns = ['resolve'];\n\nconst addPathShimToContext = async (vm) => {\n  fns.forEach((fn) => {\n    let fnHandle = vm.newFunction(fn, function (...args) {\n      const nativeArgs = args.map(vm.dump);\n      return marshallToVm(path[fn](...nativeArgs), vm);\n    });\n    vm.setProp(vm.global, `__bruno__path__${fn}`, fnHandle);\n    fnHandle.dispose();\n  });\n\n  vm.evalCode(\n    `\n        globalThis.path = {};\n        ${fns?.map((fn, idx) => `globalThis.path.${fn} = __bruno__path__${fn}`).join('\\n')}\n        globalThis.requireObject = {\n            ...(globalThis.requireObject || {}),\n            path: globalThis.path,\n        }\n    `\n  );\n};\n\nmodule.exports = addPathShimToContext;\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/quickjs/shims/lib/utils.js",
    "content": "function serializeTypedArray(ta) {\n  return {\n    type: ta.constructor.name,\n    array: Array.from(ta),\n    length: ta.length\n  };\n}\n\nfunction deserializeTypedArray(obj) {\n  // Allowed typed array constructors for crypto operations\n  const allowedConstructors = new Set([\n    'Int8Array',\n    'Uint8Array',\n    'Uint8ClampedArray',\n    'Int16Array',\n    'Uint16Array',\n    'Int32Array',\n    'Uint32Array',\n    'Float32Array',\n    'Float64Array',\n    'BigInt64Array',\n    'BigUint64Array'\n  ]);\n\n  if (!obj || typeof obj !== 'object') {\n    throw new TypeError('getRandomValues: Invalid typed array object');\n  }\n\n  if (typeof obj.type !== 'string' || !allowedConstructors.has(obj.type)) {\n    throw new TypeError(`getRandomValues: Invalid or unsupported typed array type: ${obj.type}`);\n  }\n\n  if (!obj.array || typeof obj.length !== 'number') {\n    throw new TypeError('getRandomValues: Invalid typed array properties');\n  }\n\n  const ctor = globalThis[obj.type];\n  if (typeof ctor !== 'function') {\n    throw new TypeError(`getRandomValues: Constructor ${obj.type} is not available`);\n  }\n\n  return new ctor(obj.array, 0, obj.length);\n}\n\nmodule.exports = {\n  serializeTypedArray,\n  deserializeTypedArray\n};\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/quickjs/shims/lib/uuid.js",
    "content": "const uuid = require('uuid');\nconst { marshallToVm } = require('../../utils');\n\nconst fns = ['version', 'parse', 'stringify', 'v1', 'v1ToV6', 'v3', 'v4', 'v5', 'v6', 'v6ToV1', 'v7', 'validate'];\n\nconst addUuidShimToContext = async (vm) => {\n  fns.forEach((fn) => {\n    let fnHandle = vm.newFunction(fn, function (...args) {\n      const nativeArgs = args.map(vm.dump);\n      return marshallToVm(uuid[fn](...nativeArgs), vm);\n    });\n    vm.setProp(vm.global, `__bruno__uuid__${fn}`, fnHandle);\n    fnHandle.dispose();\n  });\n\n  vm.evalCode(\n    `\n        globalThis.uuid = {};\n        ${['version', 'parse', 'stringify', 'v1', 'v1ToV6', 'v3', 'v4', 'v5', 'v6', 'v6ToV1', 'v7', 'validate']\n          ?.map((fn, idx) => `globalThis.uuid.${fn} = __bruno__uuid__${fn}`)\n          .join('\\n')}\n        globalThis.requireObject = {\n            ...globalThis.requireObject,\n            uuid: globalThis.uuid,\n        }\n    `\n  );\n};\n\nmodule.exports = addUuidShimToContext;\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/quickjs/shims/local-module.js",
    "content": "const path = require('path');\nconst fs = require('fs');\nconst { marshallToVm } = require('../utils');\n\nconst addLocalModuleLoaderShimToContext = (vm, collectionPath) => {\n  let loadLocalModuleHandle = vm.newFunction('loadLocalModule', function (module) {\n    const filename = vm.dump(module);\n\n    // Check if the filename has an extension\n    const hasExtension = path.extname(filename) !== '';\n    const resolvedFilename = hasExtension ? filename : `${filename}.js`;\n\n    // Resolve the file path and check if it's within the collectionPath\n    const filePath = path.resolve(collectionPath, resolvedFilename);\n    const relativePath = path.relative(collectionPath, filePath);\n\n    // Ensure the resolved file path is inside the collectionPath\n    if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {\n      throw new Error('Access to files outside of the collectionPath is not allowed.');\n    }\n\n    if (!fs.existsSync(filePath)) {\n      throw new Error(`Cannot find module ${filename}`);\n    }\n\n    let code = fs.readFileSync(filePath).toString();\n\n    return marshallToVm(code, vm);\n  });\n\n  vm.setProp(vm.global, '__brunoLoadLocalModule', loadLocalModuleHandle);\n  loadLocalModuleHandle.dispose();\n};\n\nmodule.exports = addLocalModuleLoaderShimToContext;\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/quickjs/shims/test.js",
    "content": "const { marshallToVm } = require('../utils');\n\nconst addBruShimToContext = (vm, __brunoTestResults) => {\n  let addResult = vm.newFunction('addResult', function (v) {\n    __brunoTestResults.addResult(vm.dump(v));\n  });\n  vm.setProp(vm.global, '__bruno__addResult', addResult);\n  addResult.dispose();\n\n  let getResults = vm.newFunction('getResults', function () {\n    return marshallToVm(__brunoTestResults.getResults(), vm);\n  });\n  vm.setProp(vm.global, '__bruno__getResults', getResults);\n  getResults.dispose();\n\n  vm.evalCode(\n    `\n      globalThis.expect = require('chai').expect;\n      globalThis.assert = require('chai').assert;\n\n      globalThis.__brunoTestResults = {\n        addResult: globalThis.__bruno__addResult,\n        getResults: globalThis.__bruno__getResults,\n      }\n\n      globalThis.DummyChaiAssertionError = class DummyChaiAssertionError extends Error {\n        constructor(message, props, ssf) {\n          super(message);\n          this.name = \"AssertionError\";\n          Object.assign(this, props);\n        }\n      }\n\n      globalThis.Test = (__brunoTestResults) => async (description, callback) => {\n        try {\n          await callback();\n          __brunoTestResults.addResult({ description, status: \"pass\" });\n        } catch (error) {\n          if (error instanceof DummyChaiAssertionError) {\n            const { message, actual, expected } = error;\n            __brunoTestResults.addResult({\n              description,\n              status: \"fail\",\n              error: message,\n              actual,\n              expected,\n            });\n          } else {\n            globalThis.__bruno__addResult({\n              description,\n              status: \"fail\",\n              error: error.message || \"An unexpected error occurred.\",\n            });\n          }\n        }\n      };\n\n      globalThis.test = Test(__brunoTestResults);\n    `\n  );\n\n  // Register custom chai assertion for isJson (expect(...).to.be.json)\n  // The bundled chai only exposes { expect, assert } — no Assertion class.\n  // Access the prototype through an expect() instance instead.\n  vm.evalCode(\n    `\n      (function() {\n        var proto = Object.getPrototypeOf(expect(null));\n        Object.defineProperty(proto, 'json', {\n          get: function () {\n            var obj = this._obj;\n            var isJson = typeof obj === 'object' && obj !== null && !Array.isArray(obj) &&\n              Object.prototype.toString.call(obj) === '[object Object]';\n            this.assert(isJson, 'expected #{this} to be JSON', 'expected #{this} not to be JSON');\n            return this;\n          },\n          configurable: true\n        });\n      })();\n    `\n  );\n};\n\nmodule.exports = addBruShimToContext;\n"
  },
  {
    "path": "packages/bruno-js/src/sandbox/quickjs/utils/index.js",
    "content": "const marshallToVm = (value, vm) => {\n  if (value === undefined) {\n    return vm.undefined;\n  }\n  if (value === null) {\n    return vm.null;\n  }\n  if (typeof value === 'string') {\n    return vm.newString(value);\n  } else if (typeof value === 'number') {\n    return vm.newNumber(value);\n  } else if (typeof value === 'boolean') {\n    return value ? vm.true : vm.false;\n  } else if (typeof value === 'object') {\n    if (Array.isArray(value)) {\n      const arr = vm.newArray();\n      for (let i = 0; i < value.length; i++) {\n        vm.setProp(arr, i, marshallToVm(value[i], vm));\n      }\n      return arr;\n    } else {\n      const obj = vm.newObject();\n      for (const key in value) {\n        vm.setProp(obj, key, marshallToVm(value[key], vm));\n      }\n      return obj;\n    }\n  } else if (typeof value === 'function') {\n    return vm.newString('[Function (anonymous)]');\n  }\n};\n\n/**\n * Invokes a QuickJS function handle.\n * - Returns a Promise\n *\n * @param {Object} vm - QuickJS VM instance\n * @param {QuickJSHandle} quickFn - A QuickJS function handle\n * @param {Array} args - Arguments to pass to the function\n * @returns {Promise<any>} - The result as a Promise\n */\nasync function invokeFunction(vm, quickFn, args = []) {\n  if (vm.typeof(quickFn) !== 'function') {\n    throw new TypeError('Target is not a QuickJS function');\n  }\n\n  const result = vm.callFunction(quickFn, vm.global, ...args);\n\n  if (result.error) {\n    const error = vm.dump(result.error);\n    result.error.dispose();\n    throw error;\n  }\n\n  // Check if the result is a QuickJS Promise handle (async functions)\n  if (vm.typeof(result.value) === 'object' && result.value.constructor && vm.typeof(result.value.constructor) === 'function') {\n    try {\n      const promiseHandle = vm.unwrapResult(result);\n      const resolvedResult = await vm.resolvePromise(promiseHandle);\n      promiseHandle.dispose();\n      const resolvedHandle = vm.unwrapResult(resolvedResult);\n      const value = vm.dump(resolvedHandle);\n      resolvedHandle.dispose();\n      return Promise.resolve(value);\n    } catch (promiseError) {\n      // If it's not a valid Promise, throw an error\n      result.value.dispose();\n      throw new Error(`Invalid Promise handle: ${promiseError.message}`);\n    }\n  }\n\n  const value = vm.dump(result.value);\n  result.value.dispose();\n\n  return (value && typeof value.then === 'function')\n    ? value\n    : Promise.resolve(value);\n}\n\nmodule.exports = {\n  marshallToVm,\n  invokeFunction\n};\n"
  },
  {
    "path": "packages/bruno-js/src/test-results.js",
    "content": "const { nanoid } = require('nanoid');\n\nclass TestResults {\n  constructor() {\n    this.results = [];\n  }\n\n  addResult(result) {\n    result.uid = nanoid();\n    this.results.push(result);\n  }\n\n  getResults() {\n    return this.results;\n  }\n}\n\nmodule.exports = TestResults;\n"
  },
  {
    "path": "packages/bruno-js/src/test.js",
    "content": "const Test = (__brunoTestResults, chai) => async (description, callback) => {\n  try {\n    await callback();\n    __brunoTestResults.addResult({ description, status: 'pass' });\n  } catch (error) {\n    if (error instanceof chai.AssertionError) {\n      const { message, actual, expected } = error;\n      __brunoTestResults.addResult({\n        description,\n        status: 'fail',\n        error: message,\n        actual,\n        expected,\n        stack: error.stack || null,\n        errorName: error.name || 'AssertionError'\n      });\n    } else {\n      __brunoTestResults.addResult({\n        description,\n        status: 'fail',\n        error: error.message || 'An unexpected error occurred.',\n        stack: error.stack || null,\n        errorName: error.name || 'Error'\n      });\n    }\n  }\n};\n\nmodule.exports = Test;\n"
  },
  {
    "path": "packages/bruno-js/src/utils/error-formatter.js",
    "content": "const fs = require('fs');\nconst YAML = require('yaml');\nconst { NODEVM_SCRIPT_WRAPPER_OFFSET, QUICKJS_SCRIPT_WRAPPER_OFFSET } = require('./sandbox');\n\nconst DEFAULT_CONTEXT_LINES = 5;\nconst ALLOWED_SOURCE_EXTENSIONS = ['.bru', '.yml', '.yaml'];\n\nconst isAllowedSourceFile = (filePath) =>\n  typeof filePath === 'string' && ALLOWED_SOURCE_EXTENSIONS.some((ext) => filePath.endsWith(ext));\n\nconst SCRIPT_TYPES = Object.freeze({\n  PRE_REQUEST: 'pre-request',\n  POST_RESPONSE: 'post-response',\n  TEST: 'test'\n});\n\n// Bruno script types → OpenCollection YAML script types\nconst SCRIPT_TYPE_TO_YML = {\n  [SCRIPT_TYPES.PRE_REQUEST]: 'before-request',\n  [SCRIPT_TYPES.POST_RESPONSE]: 'after-response',\n  [SCRIPT_TYPES.TEST]: 'tests'\n};\n\nconst readFile = (filePath, cache = null) => {\n  if (cache?.has(filePath)) return cache.get(filePath);\n  try {\n    const content = fs.readFileSync(filePath, 'utf-8').replace(/\\r\\n/g, '\\n');\n    if (cache) cache.set(filePath, content);\n    return content;\n  } catch {\n    return null;\n  }\n};\n\nconst BLOCK_PATTERNS = {\n  [SCRIPT_TYPES.PRE_REQUEST]: /^script:pre-request\\s*\\{/,\n  [SCRIPT_TYPES.POST_RESPONSE]: /^script:post-response\\s*\\{/,\n  [SCRIPT_TYPES.TEST]: /^tests\\s*\\{/\n};\n\n/** Find the 1-indexed line where a script block's content starts in a .bru file */\nconst findScriptBlockStartLine = (filePath, scriptType, cache = null) => {\n  if (!filePath.endsWith('.bru')) return null;\n\n  const cacheKey = `bru:${filePath}:${scriptType}`;\n  if (cache?.has(cacheKey)) return cache.get(cacheKey);\n\n  const content = readFile(filePath, cache);\n  if (!content) return null;\n\n  const pattern = BLOCK_PATTERNS[scriptType];\n  if (!pattern) return null;\n\n  const lines = content.split('\\n');\n  let result = null;\n  for (let i = 0; i < lines.length; i++) {\n    if (pattern.test(lines[i])) {\n      result = i + 2; // +1 for 1-indexing, +1 for line after opening brace\n      break;\n    }\n  }\n\n  if (cache) cache.set(cacheKey, result);\n  return result;\n};\n\n/** Find the 1-indexed line where a script block's content starts in a .yml file */\nconst findYmlScriptBlockStartLine = (filePath, scriptType, cache = null) => {\n  if (!filePath.endsWith('.yml') && !filePath.endsWith('.yaml')) return null;\n\n  const cacheKey = `yml:${filePath}:${scriptType}`;\n  if (cache?.has(cacheKey)) return cache.get(cacheKey);\n\n  const content = readFile(filePath, cache);\n  if (!content) return null;\n\n  const ymlType = SCRIPT_TYPE_TO_YML[scriptType];\n  if (!ymlType) return null;\n\n  let result = null;\n  try {\n    const lineCounter = new YAML.LineCounter();\n    const doc = YAML.parseDocument(content, { lineCounter });\n\n    // Request yml files use runtime.scripts, collection/folder yml files use request.scripts\n    const scriptPaths = [['runtime', 'scripts'], ['request', 'scripts']];\n    for (const scriptPath of scriptPaths) {\n      const scripts = doc.getIn(scriptPath, true);\n      if (YAML.isSeq(scripts)) {\n        for (const item of scripts.items) {\n          if (!YAML.isMap(item)) continue;\n          if (item.get('type') === ymlType) {\n            const codeNode = item.get('code', true);\n            if (codeNode && codeNode.range) {\n              result = lineCounter.linePos(codeNode.range[0]).line + 1;\n              break;\n            }\n          }\n        }\n        if (result) break;\n      }\n    }\n  } catch {\n    // invalid YAML\n  }\n\n  if (cache) cache.set(cacheKey, result);\n  return result;\n};\n\n/** Adjust a runtime-reported line number to the actual line in the .bru/.yml file */\nconst adjustLineNumber = (filePath, reportedLine, isQuickJS, scriptType = null, cache = null, scriptMetadata = null) => {\n  const isBruFile = filePath.endsWith('.bru');\n  const isYmlFile = filePath.endsWith('.yml') || filePath.endsWith('.yaml');\n\n  if (!isBruFile && !isYmlFile) {\n    return reportedLine;\n  }\n\n  const wrapperOffset = isQuickJS ? QUICKJS_SCRIPT_WRAPPER_OFFSET : NODEVM_SCRIPT_WRAPPER_OFFSET;\n  const scriptRelativeLine = reportedLine - wrapperOffset;\n\n  if (scriptRelativeLine < 1) return reportedLine;\n\n  // Use metadata if available to correctly map line numbers in combined scripts\n  if (scriptType && scriptMetadata) {\n    const { requestStartLine, requestEndLine } = scriptMetadata;\n    if (requestStartLine != null && requestEndLine != null) {\n      if (scriptRelativeLine >= requestStartLine && scriptRelativeLine <= requestEndLine) {\n        // Error is within the request script segment\n        const blockStartLine = isBruFile\n          ? findScriptBlockStartLine(filePath, scriptType, cache)\n          : findYmlScriptBlockStartLine(filePath, scriptType, cache);\n\n        if (blockStartLine) {\n          return blockStartLine + (scriptRelativeLine - requestStartLine) - 1;\n        }\n      } else {\n        // Error is in a collection/folder-level script\n        // Cannot map to the request .bru/.yml file, return null to skip source context.\n        return null;\n      }\n    }\n  }\n\n  // No segment metadata, map script-relative line to file line via block start.\n  if (scriptType) {\n    const blockStartLine = isBruFile\n      ? findScriptBlockStartLine(filePath, scriptType, cache)\n      : findYmlScriptBlockStartLine(filePath, scriptType, cache);\n\n    if (blockStartLine) {\n      return blockStartLine + scriptRelativeLine - 1;\n    }\n  }\n\n  return scriptRelativeLine;\n};\n\n/**\n * Resolve an error in a collection/folder script segment to its source file and line.\n * Uses the segments array in metadata to find which segment the error falls in,\n * then maps to the actual line in that segment's source file.\n */\nconst resolveSegmentError = (parsed, metadata, scriptType, cache) => {\n  if (!metadata?.segments?.length || !parsed) return null;\n\n  const wrapperOffset = parsed.isQuickJS ? QUICKJS_SCRIPT_WRAPPER_OFFSET : NODEVM_SCRIPT_WRAPPER_OFFSET;\n  const scriptRelativeLine = parsed.line - wrapperOffset;\n  if (scriptRelativeLine < 1) return null;\n\n  for (const segment of metadata.segments) {\n    if (scriptRelativeLine >= segment.startLine && scriptRelativeLine <= segment.endLine) {\n      const isBru = segment.filePath.endsWith('.bru');\n      const isYml = segment.filePath.endsWith('.yml') || segment.filePath.endsWith('.yaml');\n      if (!isBru && !isYml) return null;\n\n      const blockStartLine = isBru\n        ? findScriptBlockStartLine(segment.filePath, scriptType, cache)\n        : findYmlScriptBlockStartLine(segment.filePath, scriptType, cache);\n      if (!blockStartLine) return null;\n\n      return {\n        line: blockStartLine + (scriptRelativeLine - segment.startLine) - 1,\n        filePath: segment.filePath,\n        displayPath: segment.displayPath\n      };\n    }\n  }\n  return null;\n};\n\n/** Extract file path, line, column, and runtime type from a single stack trace line */\nconst matchStackFrame = (line) => {\n  // QuickJS: \"at (/path/to/file.bru:11)\" or \"at <anonymous> (/path/to/file.bru:11)\"\n  const quickjsMatch = line.match(/at (?:<[^>]+>\\s*)?\\(((?:[A-Za-z]:)?[^:]+):(\\d+)(?::(\\d+))?\\)/);\n  if (quickjsMatch && (quickjsMatch[1].includes('/') || quickjsMatch[1].includes('\\\\'))) {\n    return {\n      filePath: quickjsMatch[1],\n      line: parseInt(quickjsMatch[2], 10),\n      column: quickjsMatch[3] ? parseInt(quickjsMatch[3], 10) : null,\n      isQuickJS: true\n    };\n  }\n\n  // Node VM: \"at /path/to/file.bru:11:5\" or \"at Object.<anonymous> (/path/to/file.bru:11:5)\"\n  const nodeMatch = line.match(/at (?:.*?\\()?((?:[A-Za-z]:)?[^:]+):(\\d+)(?::(\\d+))?\\)?/);\n  if (nodeMatch && (nodeMatch[1].includes('/') || nodeMatch[1].includes('\\\\'))) {\n    return {\n      filePath: nodeMatch[1],\n      line: parseInt(nodeMatch[2], 10),\n      column: nodeMatch[3] ? parseInt(nodeMatch[3], 10) : null,\n      isQuickJS: false\n    };\n  }\n\n  return null;\n};\n\n/** Parse the first stack frame to extract file path, line, and column */\nconst parseStackTrace = (stack) => {\n  if (!stack) return null;\n\n  for (const line of stack.split('\\n')) {\n    const match = matchStackFrame(line);\n    if (match) return match;\n  }\n\n  return null;\n};\n\nconst parseErrorLocation = (error) => {\n  if (error.__callSites?.length > 0) {\n    const first = error.__callSites[0];\n    return {\n      filePath: first.filePath,\n      line: first.line,\n      column: first.column,\n      isQuickJS: false\n    };\n  }\n\n  /* falls back to string parsing */\n  const parsed = parseStackTrace(error.stack);\n  if (parsed && error.__isQuickJS) {\n    parsed.isQuickJS = true;\n  }\n  return parsed;\n};\n\n/** Read source file and extract context lines around the error location */\nconst getSourceContext = (filePath, errorLine, contextLines = DEFAULT_CONTEXT_LINES, cache = null) => {\n  const content = readFile(filePath, cache);\n  if (!content) return null;\n\n  const lines = content.split('\\n');\n  const startLine = Math.max(1, errorLine - contextLines);\n  const endLine = Math.min(lines.length, errorLine + contextLines);\n\n  const contextLinesArray = [];\n  for (let i = startLine; i <= endLine; i++) {\n    contextLinesArray.push({\n      lineNumber: i,\n      content: lines[i - 1],\n      isError: i === errorLine\n    });\n  }\n\n  return { lines: contextLinesArray, startLine, errorLine };\n};\n\n/** Build adjusted stack trace string from structured CallSite data */\nconst buildStackFromCallSites = (callSites, scriptType = null, cache = null, scriptMetadata = null) => {\n  return callSites.map((site) => {\n    const adjusted = adjustLineNumber(site.filePath, site.line, false, scriptType, cache, scriptMetadata);\n    let fileToUse = site.filePath;\n    let lineToUse = adjusted !== null ? adjusted : site.line;\n\n    // Try segment resolution for collection/folder frames\n    if (adjusted === null && scriptMetadata?.segments) {\n      const parsed = { line: site.line, isQuickJS: false };\n      const resolved = resolveSegmentError(parsed, scriptMetadata, scriptType, cache);\n      if (resolved) {\n        fileToUse = resolved.filePath;\n        lineToUse = resolved.line;\n      }\n    }\n\n    const loc = site.column ? `${fileToUse}:${lineToUse}:${site.column}` : `${fileToUse}:${lineToUse}`;\n    const name = site.functionName ? `${site.functionName} (${loc})` : loc;\n    return `    at ${name}`;\n  }).join('\\n');\n};\n\n/** Adjust all line numbers in a stack trace string */\nconst adjustStackTrace = (stack, scriptType = null, cache = null, scriptMetadata = null, forceQuickJS = false) => {\n  if (!stack) return stack;\n\n  return stack.split('\\n').map((line) => {\n    const match = matchStackFrame(line);\n    if (!match) return line;\n\n    const isQuickJS = forceQuickJS || match.isQuickJS;\n    const adjusted = adjustLineNumber(match.filePath, match.line, isQuickJS, scriptType, cache, scriptMetadata);\n\n    // Try segment resolution for collection/folder frames\n    if (adjusted === null && scriptMetadata?.segments) {\n      const parsed = { line: match.line, isQuickJS };\n      const resolved = resolveSegmentError(parsed, scriptMetadata, scriptType, cache);\n      if (resolved) {\n        const suffix = match.isQuickJS ? ')' : '';\n        return match.column !== null\n          ? line.replace(`${match.filePath}:${match.line}:${match.column}${suffix}`, `${resolved.filePath}:${resolved.line}:${match.column}${suffix}`)\n          : line.replace(`${match.filePath}:${match.line}${suffix}`, `${resolved.filePath}:${resolved.line}${suffix}`);\n      }\n      return line;\n    }\n\n    if (adjusted === null || adjusted === match.line) return line;\n\n    const suffix = match.isQuickJS ? ')' : '';\n    return match.column !== null\n      ? line.replace(`:${match.line}:${match.column}${suffix}`, `:${adjusted}:${match.column}${suffix}`)\n      : line.replace(`:${match.line}${suffix}`, `:${adjusted}${suffix}`);\n  }).join('\\n');\n};\n\n/** Resolve original error name from wrapped errors (QuickJS cause / Node VM ScriptError) */\nconst getErrorTypeName = (error) => {\n  return error.cause?.name || error.originalError?.name || error.name || error.constructor?.name || 'Error';\n};\n\n/** Format an error with source context and adjusted line numbers */\nconst formatErrorWithContext = (error, relativeFilePath = null, scriptType = null, contextLines = DEFAULT_CONTEXT_LINES, scriptMetadata = null) => {\n  if (!error) return '';\n\n  const cache = new Map();\n  // Use metadata from error object if available, otherwise use passed parameter\n  const metadata = error.scriptMetadata || scriptMetadata;\n\n  const parsed = parseErrorLocation(error);\n  if (!parsed) {\n    return `${error.message}\\n${error.stack || ''}`;\n  }\n\n  const { filePath } = parsed;\n  const adjustedLine = adjustLineNumber(filePath, parsed.line, parsed.isQuickJS, scriptType, cache, metadata);\n\n  // adjustedLine === null means the error is in a collection/folder script\n  // resolve to the collection/folder source file using segment metadata\n  let segmentResult = null;\n  if (adjustedLine === null) {\n    segmentResult = resolveSegmentError(parsed, metadata, scriptType, cache);\n    if (!segmentResult) {\n      // Fallback: no segment resolution possible, show message + stack only\n      const errorType = getErrorTypeName(error);\n      const parts = [`${errorType}: ${error.message}`];\n      if (error.__callSites?.length > 0) {\n        parts.push(buildStackFromCallSites(error.__callSites, scriptType, cache, metadata));\n      } else if (error.stack) {\n        const stackLines = error.stack.split('\\n').slice(1);\n        for (const stackLine of stackLines) {\n          parts.push(`    ${stackLine.trim()}`);\n        }\n      }\n      return parts.join('\\n');\n    }\n  }\n\n  const sourceFile = segmentResult ? segmentResult.filePath : filePath;\n  const sourceLine = segmentResult ? segmentResult.line : adjustedLine;\n  const context = isAllowedSourceFile(sourceFile) ? getSourceContext(sourceFile, sourceLine, contextLines, cache) : null;\n\n  if (!context) {\n    return `${error.message}\\n${error.stack || ''}`;\n  }\n\n  const displayPath = segmentResult ? segmentResult.displayPath : (relativeFilePath || filePath);\n  const lines = [];\n\n  lines.push(`File: ${displayPath}`);\n  lines.push('');\n\n  const maxLineNumber = context.lines[context.lines.length - 1].lineNumber;\n  const lineNumberWidth = String(maxLineNumber).length;\n\n  for (const lineInfo of context.lines) {\n    const lineNum = String(lineInfo.lineNumber).padStart(lineNumberWidth, ' ');\n    const prefix = lineInfo.isError ? '>' : ' ';\n\n    lines.push(`${prefix} ${lineNum} |  ${lineInfo.content}`);\n  }\n\n  lines.push('');\n\n  const errorType = getErrorTypeName(error);\n  lines.push(`${errorType}: ${error.message}`);\n\n  if (error.__callSites?.length > 0) {\n    lines.push(buildStackFromCallSites(error.__callSites, scriptType, cache, metadata));\n  } else {\n    const stackToDisplay = adjustStackTrace(error.stack, scriptType, cache, metadata, parsed.isQuickJS);\n    const userStackLines = stackToDisplay.split('\\n').slice(1);\n    for (const stackLine of userStackLines) {\n      lines.push(`    ${stackLine.trim()}`);\n    }\n  }\n\n  return lines.join('\\n');\n};\n\nmodule.exports = {\n  SCRIPT_TYPES,\n  DEFAULT_CONTEXT_LINES,\n  parseStackTrace,\n  parseErrorLocation,\n  buildStackFromCallSites,\n  getSourceContext,\n  formatErrorWithContext,\n  adjustLineNumber,\n  resolveSegmentError,\n  findScriptBlockStartLine,\n  findYmlScriptBlockStartLine,\n  adjustStackTrace,\n  getErrorTypeName\n};\n"
  },
  {
    "path": "packages/bruno-js/src/utils/error-formatter.spec.js",
    "content": "const { describe, it, expect, beforeEach, afterEach } = require('@jest/globals');\nconst {\n  formatErrorWithContext,\n  findScriptBlockStartLine,\n  findYmlScriptBlockStartLine,\n  adjustLineNumber,\n  parseStackTrace,\n  parseErrorLocation\n} = require('./error-formatter');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\n\n// Line numbers annotated for reference:\n// 13: script:pre-request {        → blockStartLine = 14\n// 14:   const token = ...          → script line 1\n// 18: script:post-response {      → blockStartLine = 19\n// 19:   const data = res.body;     → script line 1\n// 20:   bru.setVar(...)            → script line 2\n// 24: tests {                     → blockStartLine = 25\n// 25:   test(\"status is 200\"...)   → script line 1\nconst MULTI_BLOCK_BRU = `meta {\n  name: multi-block-test\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://example.com\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  const token = bru.getEnvVar('token');\n  req.setHeader('Authorization', token);\n}\n\nscript:post-response {\n  const data = res.body;\n  bru.setVar('userId', data.id);\n  console.log(data);\n}\n\ntests {\n  test(\"status is 200\", function() {\n    expect(res.status).to.equal(200);\n  });\n  test(\"has body\", function() {\n    expect(res.body).to.not.be.null;\n  });\n}`;\n\n// Fixture with JS comments to verify line mapping when comments are present.\n// 11: script:post-response {      → blockStartLine = 12\n// 12:   // This is a comment       → script line 1\n// 13:   const data = res.body;     → script line 2\n// 14:   // Another comment         → script line 3\n// 15:   bru.setVar('userId', ...); → script line 4\nconst BRU_WITH_COMMENTS = `meta {\n  name: comment-test\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://example.com\n}\n\nscript:post-response {\n  // This is a comment\n  const data = res.body;\n  // Another comment\n  bru.setVar('userId', data.id);\n}`;\n\n// YML fixture: blockStartLine = 8 (pre-request), 12 (post-response), 16 (tests)\nconst MULTI_BLOCK_YML = [\n  'info:',\n  '  name: yaml-test',\n  '  version: \"1\"',\n  'runtime:',\n  '  scripts:',\n  '    - type: before-request',\n  '      code: |-',\n  '        const token = bru.getEnvVar(\\'token\\');',\n  '        req.setHeader(\\'Authorization\\', token);',\n  '    - type: after-response',\n  '      code: |-',\n  '        const data = res.body;',\n  '        bru.setVar(\\'userId\\', data.id);',\n  '    - type: tests',\n  '      code: |-',\n  '        test(\"status is 200\", function() {',\n  '          expect(res.status).to.equal(200);',\n  '        });'\n].join('\\n');\n\n// Collection/folder yml : scripts at request.scripts\n// blockStartLine: before-request = 5, tests = 9\nconst COLLECTION_YML = [\n  'info:',\n  '  name: test-collection',\n  'request:',\n  '  scripts:',\n  '    - type: before-request',\n  '      code: |-',\n  '        const abc = fc()',\n  '        const x = bru.getVar(\\'x\\');',\n  '    - type: tests',\n  '      code: |-',\n  '        test(\"example\", function() {',\n  '          expect(true).to.be.true;',\n  '        });'\n].join('\\n');\n\n// Wrapper offsets: QuickJS = 9 (script line 1 = VM line 10), NodeVM = 2 (script line 1 = VM line 3)\n\ndescribe('Error Formatter', () => {\n  let testDir;\n  let bruFilePath;\n  let ymlFilePath;\n  let bruWithCommentsPath;\n  let collectionYmlPath;\n\n  beforeEach(() => {\n    testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bruno-test-'));\n    bruFilePath = path.join(testDir, 'test.bru');\n    ymlFilePath = path.join(testDir, 'test.yml');\n    bruWithCommentsPath = path.join(testDir, 'comments.bru');\n    collectionYmlPath = path.join(testDir, 'opencollection.yml');\n    fs.writeFileSync(bruFilePath, MULTI_BLOCK_BRU);\n    fs.writeFileSync(ymlFilePath, MULTI_BLOCK_YML);\n    fs.writeFileSync(bruWithCommentsPath, BRU_WITH_COMMENTS);\n    fs.writeFileSync(collectionYmlPath, COLLECTION_YML);\n  });\n\n  afterEach(() => {\n    fs.rmSync(testDir, { recursive: true, force: true });\n  });\n\n  describe('findScriptBlockStartLine', () => {\n    it('should find each block type in .bru files', () => {\n      expect(findScriptBlockStartLine(bruFilePath, 'pre-request')).toBe(14);\n      expect(findScriptBlockStartLine(bruFilePath, 'post-response')).toBe(19);\n      expect(findScriptBlockStartLine(bruFilePath, 'test')).toBe(25);\n    });\n\n    it('should return null for missing block or non-.bru files', () => {\n      const noBlockPath = path.join(testDir, 'no-block.bru');\n      fs.writeFileSync(noBlockPath, 'meta {\\n  name: test\\n}');\n      expect(findScriptBlockStartLine(noBlockPath, 'post-response')).toBeNull();\n      expect(findScriptBlockStartLine('/some/file.js', 'post-response')).toBeNull();\n    });\n  });\n\n  describe('findYmlScriptBlockStartLine', () => {\n    it('should find each block type in .yml files', () => {\n      expect(findYmlScriptBlockStartLine(ymlFilePath, 'pre-request')).toBe(8);\n      expect(findYmlScriptBlockStartLine(ymlFilePath, 'post-response')).toBe(12);\n      expect(findYmlScriptBlockStartLine(ymlFilePath, 'test')).toBe(16);\n    });\n\n    it('should find script blocks in collection/folder yml files (request.scripts path)', () => {\n      expect(findYmlScriptBlockStartLine(collectionYmlPath, 'pre-request')).toBe(7);\n      expect(findYmlScriptBlockStartLine(collectionYmlPath, 'test')).toBe(11);\n    });\n\n    it('should return null for missing block or non-.yml files', () => {\n      const noRuntimePath = path.join(testDir, 'no-runtime.yml');\n      fs.writeFileSync(noRuntimePath, 'info:\\n  name: simple\\n  version: \"1\"\\n');\n      expect(findYmlScriptBlockStartLine(noRuntimePath, 'pre-request')).toBeNull();\n    });\n  });\n\n  describe('adjustLineNumber', () => {\n    it('should adjust QuickJS lines for .bru files', () => {\n      // VM line - offset(9) = scriptLine → blockStart + scriptLine - 1\n      expect(adjustLineNumber(bruFilePath, 10, true, 'pre-request')).toBe(14);\n      expect(adjustLineNumber(bruFilePath, 11, true, 'post-response')).toBe(20);\n      expect(adjustLineNumber(bruFilePath, 10, true, 'test')).toBe(25);\n    });\n\n    it('should adjust NodeVM lines for .bru files', () => {\n      // VM line 4 - offset(2) = scriptLine 2 → blockStart(19) + 2 - 1 = 20\n      expect(adjustLineNumber(bruFilePath, 4, false, 'post-response')).toBe(20);\n    });\n\n    it('should adjust lines for .yml files', () => {\n      expect(adjustLineNumber(ymlFilePath, 10, true, 'pre-request')).toBe(8);\n      expect(adjustLineNumber(ymlFilePath, 11, true, 'post-response')).toBe(13);\n      expect(adjustLineNumber(ymlFilePath, 4, false, 'post-response')).toBe(13);\n    });\n\n    it('should adjust lines correctly when script has comments', () => {\n      // VM line 12 - offset(9) = scriptLine 3 → blockStart(12) + 3 - 1 = 14\n      expect(adjustLineNumber(bruWithCommentsPath, 12, true, 'post-response')).toBe(14);\n    });\n\n    it('should return reportedLine for non-.bru/.yml files or invalid offset', () => {\n      expect(adjustLineNumber('/some/file.js', 10, true, 'post-response')).toBe(10);\n      // VM line 5 - offset(9) = -4, which is < 1\n      expect(adjustLineNumber(bruFilePath, 5, true, 'post-response')).toBe(5);\n    });\n\n    it('should use metadata for combined scripts', () => {\n      // scriptLine 5 within request range [5, 7] → blockStart(19) + (5-5) - 1 = 18\n      const metadata = { requestStartLine: 5, requestEndLine: 7 };\n      expect(adjustLineNumber(bruFilePath, 14, true, 'post-response', null, metadata)).toBe(18);\n    });\n\n    it('should return null for collection/folder segment errors', () => {\n      // scriptLine 3 is before requestStartLine(10) → cannot map to request file\n      const metadata = { requestStartLine: 10, requestEndLine: 15 };\n      expect(adjustLineNumber(bruFilePath, 12, true, 'post-response', null, metadata)).toBeNull();\n    });\n\n    it('should return null when request segment is empty', () => {\n      // requestStartLine: 0 indicates the request segment was empty\n      const metadata = { requestStartLine: 0, requestEndLine: 0 };\n      expect(adjustLineNumber(bruFilePath, 12, true, 'post-response', null, metadata)).toBeNull();\n    });\n  });\n\n  describe('parseStackTrace', () => {\n    it('should detect QuickJS stack frame formats', () => {\n      expect(parseStackTrace('Error: test\\n    at (/path/file.bru:11)'))\n        .toMatchObject({ filePath: '/path/file.bru', line: 11, isQuickJS: true });\n      expect(parseStackTrace('Error: test\\n    at <anonymous> (/path/file.bru:11)'))\n        .toMatchObject({ filePath: '/path/file.bru', line: 11, isQuickJS: true });\n      expect(parseStackTrace('Error: test\\n    at <eval> (/path/file.bru:11:5)'))\n        .toMatchObject({ filePath: '/path/file.bru', line: 11, column: 5, isQuickJS: true });\n    });\n\n    it('should detect NodeVM stack frame formats', () => {\n      expect(parseStackTrace('Error: test\\n    at /path/file.js:10:5'))\n        .toMatchObject({ filePath: '/path/file.js', line: 10, column: 5, isQuickJS: false });\n      expect(parseStackTrace('Error: test\\n    at Object.<anonymous> (/path/file.js:10:5)'))\n        .toMatchObject({ filePath: '/path/file.js', line: 10, column: 5, isQuickJS: false });\n    });\n\n    it('should return null for unparseable or null input', () => {\n      expect(parseStackTrace('just a plain string')).toBeNull();\n      expect(parseStackTrace(null)).toBeNull();\n    });\n  });\n\n  describe('formatErrorWithContext', () => {\n    it('should format error with arrow pointing at the correct line', () => {\n      const error = new Error('data is not defined');\n      error.name = 'ReferenceError';\n      error.stack = `ReferenceError: data is not defined\\n    at (${bruFilePath}:10)`;\n\n      const formatted = formatErrorWithContext(error, 'test.bru', 'post-response');\n      expect(formatted).toContain('ReferenceError: data is not defined');\n\n      const arrowLine = formatted.split('\\n').find((l) => l.startsWith('>'));\n      expect(arrowLine).toContain('const data = res.body;');\n    });\n\n    it('should show original error type for wrapped QuickJS errors', () => {\n      const error = new Error('x is not defined');\n      error.name = 'QuickJSUnwrapError';\n      error.cause = { name: 'ReferenceError', message: 'x is not defined' };\n      error.stack = `QuickJSUnwrapError: x is not defined\\n    at (${bruFilePath}:10)`;\n\n      const formatted = formatErrorWithContext(error, 'test.bru', 'post-response');\n      expect(formatted).toContain('ReferenceError:');\n      expect(formatted).not.toContain('QuickJSUnwrapError');\n    });\n\n    it('should use __callSites and adjust line numbers in stack', () => {\n      const error = new Error('data is not defined');\n      error.name = 'ReferenceError';\n      error.stack = `ReferenceError: data is not defined\\n    at ${bruFilePath}:4:5`;\n      error.__callSites = [{ filePath: bruFilePath, line: 4, column: 5, functionName: null }];\n\n      const formatted = formatErrorWithContext(error, 'test.bru', 'post-response');\n      // VM line 4 → file line 20\n      expect(formatted).toContain(`${bruFilePath}:20:5`);\n\n      const arrowLine = formatted.split('\\n').find((l) => l.startsWith('>'));\n      expect(arrowLine).toContain('bru.setVar');\n    });\n\n    it('should show message-only output for collection/folder script errors', () => {\n      const error = new Error('x is not defined');\n      error.name = 'ReferenceError';\n      // scriptLine 3 (VM 12 - offset 9) is before requestStartLine(10)\n      error.stack = `ReferenceError: x is not defined\\n    at (${bruFilePath}:12)`;\n\n      const metadata = { requestStartLine: 10, requestEndLine: 15 };\n      const formatted = formatErrorWithContext(error, 'test.bru', 'post-response', 5, metadata);\n\n      expect(formatted).toContain('ReferenceError: x is not defined');\n      // Should NOT show source context from the request file\n      expect(formatted).not.toContain('File:');\n      expect(formatted).not.toContain('meta {');\n      expect(formatted).not.toContain('>');\n    });\n\n    it('should show source context from collection.bru when segments are provided', () => {\n      const collectionBruPath = path.join(testDir, 'collection.bru');\n      fs.writeFileSync(collectionBruPath, 'meta {\\n  name: My Collection\\n}\\n\\nscript:pre-request {\\n  const x = undefined;\\n  x.foo();\\n}');\n\n      const error = new Error('Cannot read properties of undefined');\n      error.name = 'TypeError';\n      // NodeVM offset=2, scriptRelativeLine = 5-2 = 3 → line 3 of wrapped segment = x.foo()\n      error.stack = `TypeError: Cannot read properties of undefined\\n    at ${bruFilePath}:5:5`;\n\n      // Collection segment is lines 1-4 in combined script (3-line wrap of 2-line script)\n      const metadata = {\n        requestStartLine: 0,\n        requestEndLine: 0,\n        segments: [\n          { startLine: 1, endLine: 4, filePath: collectionBruPath, displayPath: 'collection.bru' }\n        ]\n      };\n\n      const formatted = formatErrorWithContext(error, 'test.bru', 'pre-request', 5, metadata);\n      expect(formatted).toContain('File: collection.bru');\n      expect(formatted).toContain('x.foo()');\n      expect(formatted).toContain('TypeError: Cannot read properties of undefined');\n      const arrowLine = formatted.split('\\n').find((l) => l.startsWith('>'));\n      expect(arrowLine).toContain('x.foo()');\n    });\n\n    it('should resolve error to correct folder when multiple segments exist', () => {\n      const folder1Dir = path.join(testDir, 'folder1');\n      const folder2Dir = path.join(testDir, 'folder2');\n      fs.mkdirSync(folder1Dir);\n      fs.mkdirSync(folder2Dir);\n\n      const folder1Bru = path.join(folder1Dir, 'folder.bru');\n      const folder2Bru = path.join(folder2Dir, 'folder.bru');\n      fs.writeFileSync(folder1Bru, 'meta {\\n  name: Folder1\\n}\\n\\nscript:pre-request {\\n  let a = 1;\\n}');\n      fs.writeFileSync(folder2Bru, 'meta {\\n  name: Folder2\\n}\\n\\nscript:pre-request {\\n  let b = undefined;\\n  b.pop();\\n}');\n\n      const error = new Error('Cannot read properties of undefined');\n      error.name = 'TypeError';\n      // NodeVM offset=2, scriptRelativeLine = 9-2 = 7, falls in folder2 segment [5,7]\n      error.stack = `TypeError: Cannot read properties of undefined\\n    at ${bruFilePath}:9:5`;\n\n      const metadata = {\n        requestStartLine: 0,\n        requestEndLine: 0,\n        segments: [\n          { startLine: 1, endLine: 3, filePath: folder1Bru, displayPath: 'folder1/folder.bru' },\n          { startLine: 5, endLine: 7, filePath: folder2Bru, displayPath: 'folder2/folder.bru' }\n        ]\n      };\n\n      const formatted = formatErrorWithContext(error, 'test.bru', 'pre-request', 5, metadata);\n      expect(formatted).toContain('File: folder2/folder.bru');\n      expect(formatted).toContain('b.pop()');\n    });\n\n    it('should resolve collection yml segment errors to opencollection.yml', () => {\n      const error = new Error('\\'fc\\' is not defined');\n      error.name = 'ReferenceError';\n      error.__isQuickJS = true;\n      // QuickJS offset=9, scriptRelativeLine = 11-9 = 2 → falls in collection segment [1,4]\n      error.stack = `ReferenceError: 'fc' is not defined\\n    at <anonymous> (${ymlFilePath}:11)`;\n\n      const metadata = {\n        requestStartLine: 6,\n        requestEndLine: 8,\n        segments: [\n          { startLine: 1, endLine: 4, filePath: collectionYmlPath, displayPath: 'opencollection.yml' }\n        ]\n      };\n\n      const formatted = formatErrorWithContext(error, 'test.yml', 'pre-request', 5, metadata);\n      expect(formatted).toContain('File: opencollection.yml');\n      expect(formatted).toContain('\\'fc\\' is not defined');\n      const arrowLine = formatted.split('\\n').find((l) => l.startsWith('>'));\n      expect(arrowLine).toContain('fc()');\n    });\n\n    it('should handle edge cases gracefully', () => {\n      expect(formatErrorWithContext(null)).toBe('');\n\n      const error = new Error('Test error');\n      error.stack = 'Invalid stack trace';\n      expect(formatErrorWithContext(error)).toContain('Test error');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-js/src/utils/results.js",
    "content": "const TestResults = require('../test-results');\nconst Test = require('../test');\n\n// Calculate summary statistics for test results\nconst getResultsSummary = (results) => {\n  const summary = {\n    total: results.length,\n    passed: 0,\n    failed: 0,\n    skipped: 0\n  };\n\n  results.forEach((r) => {\n    const passed = r.status === 'pass';\n    if (passed) summary.passed += 1;\n    else if (r.status === 'fail') summary.failed += 1;\n    else summary.skipped += 1;\n  });\n\n  return summary;\n};\n\nconst createBruTestResultMethods = (bru, assertionResults, chai) => {\n  const __brunoTestResults = new TestResults();\n  const test = Test(__brunoTestResults, chai);\n  setupBruTestMethods(bru, __brunoTestResults, assertionResults);\n\n  return { __brunoTestResults, test };\n};\n\nconst setupBruTestMethods = (bru, __brunoTestResults, assertionResults) => {\n  const getTestResults = async () => {\n    let results = await __brunoTestResults.getResults();\n    const summary = getResultsSummary(results);\n    return {\n      summary,\n      results: results.map((r) => ({\n        status: r.status,\n        description: r.description,\n        expected: r.expected,\n        actual: r.actual,\n        error: r.error\n      }))\n    };\n  };\n\n  const getAssertionResults = async () => {\n    let results = assertionResults;\n    const summary = getResultsSummary(results);\n    return {\n      summary,\n      results: results.map((r) => ({\n        status: r.status,\n        lhsExpr: r.lhsExpr,\n        rhsExpr: r.rhsExpr,\n        operator: r.operator,\n        rhsOperand: r.rhsOperand,\n        error: r.error\n      }))\n    };\n  };\n\n  // Set methods on bru object if provided\n  if (bru) {\n    bru.getTestResults = getTestResults;\n    bru.getAssertionResults = getAssertionResults;\n  }\n\n  // Also return the methods for direct use\n  return {\n    getTestResults,\n    getAssertionResults\n  };\n};\n\nmodule.exports = {\n  getResultsSummary,\n  createBruTestResultMethods,\n  setupBruTestMethods\n};\n"
  },
  {
    "path": "packages/bruno-js/src/utils/sandbox.js",
    "content": "// Sandbox script wrapping utilities for Node VM and QuickJS.\n// Line offsets are computed from the prefix strings so error-formatter.js can map\n// VM-reported line numbers back to the original .bru/.yml source lines.\n\nconst SANDBOX = Object.freeze({\n  NODEVM: 'nodevm',\n  QUICKJS: 'quickjs'\n});\n\n// -- Node VM --\n\nconst NODEVM_SCRIPT_PREFIX = `\n        (async function(){\n          `;\n\nconst NODEVM_SCRIPT_SUFFIX = `\n        })();\n      `;\n\n// -- QuickJS --\n\nconst QUICKJS_SCRIPT_PREFIX = `\n      (async () => {\n        const setTimeout = async(fn, timer) => {\n          v = await bru.sleep(timer);\n          fn.apply();\n        }\n\n        await bru.sleep(0);\n        try {\n          `;\n\nconst QUICKJS_SCRIPT_SUFFIX = `\n        }\n        catch(error) {\n          throw error;\n        }\n        return 'done';\n      })()\n    `;\n\n// Computed offsets — number of newlines before user script in each wrapper\nconst NODEVM_SCRIPT_WRAPPER_OFFSET = NODEVM_SCRIPT_PREFIX.split('\\n').length - 1;\nconst QUICKJS_SCRIPT_WRAPPER_OFFSET = QUICKJS_SCRIPT_PREFIX.split('\\n').length - 1;\n\n/**\n * Wraps a script in the appropriate sandbox closure.\n * @param {string} script - The script code to wrap\n * @param {'nodevm'|'quickjs'} sandbox - The sandbox runtime to wrap for\n * @returns {string} The wrapped script\n */\nconst wrapScriptInClosure = (script, sandbox) => {\n  if (sandbox === SANDBOX.QUICKJS) {\n    return QUICKJS_SCRIPT_PREFIX + script + QUICKJS_SCRIPT_SUFFIX;\n  }\n  return NODEVM_SCRIPT_PREFIX + script + NODEVM_SCRIPT_SUFFIX;\n};\n\nmodule.exports = {\n  SANDBOX,\n  wrapScriptInClosure,\n  NODEVM_SCRIPT_WRAPPER_OFFSET,\n  QUICKJS_SCRIPT_WRAPPER_OFFSET\n};\n"
  },
  {
    "path": "packages/bruno-js/src/utils.js",
    "content": "const jsonQuery = require('json-query');\nconst { get } = require('@usebruno/query');\n\nconst JS_KEYWORDS = `\n  break case catch class const continue debugger default delete do\n  else export extends false finally for function if import in instanceof\n  new null return super switch this throw true try typeof var void while with\n  undefined let static yield arguments of\n`\n  .split(/\\s+/)\n  .filter((word) => word.length > 0);\n\n/**\n * Creates a function from a JavaScript expression\n *\n * When the function is called, the variables used in this expression are picked up from the context\n *\n * ```js\n * res.data.pets.map(pet => pet.name.toUpperCase())\n *\n * function(__bruno__functionInnerContext) {\n *   const { res, pet } = __bruno__functionInnerContext;\n *   return res.data.pets.map(pet => pet.name.toUpperCase())\n * }\n * ```\n */\nconst compileJsExpression = (expr) => {\n  // get all dotted identifiers (foo, bar.baz, .baz)\n  const matches = expr.match(/([\\w\\.$]+)/g) ?? [];\n\n  // get valid js identifiers (foo, bar)\n  const vars = new Set(\n    matches\n      .filter((match) => /^[a-zA-Z$_]/.test(match)) // starts with valid js identifier (foo.bar)\n      .map((match) => match.split('.')[0]) // top level identifier (foo)\n      .filter((name) => !JS_KEYWORDS.includes(name)) // exclude js keywords\n  );\n\n  // globals such as Math\n  const globals = [...vars].filter((name) => name in globalThis);\n\n  const code = {\n    vars: [...vars].join(', '),\n    // pick global from context or globalThis\n    globals: globals.map((name) => ` ${name} = ${name} ?? globalThis.${name};`).join('')\n  };\n\n  // param name that is unlikely to show up as a var in an expression\n  const param = `__bruno__functionInnerContext`;\n  const body = `let { ${code.vars} } = ${param}; ${code.globals}; return ${expr}`;\n\n  return new Function(param, body);\n};\n\nconst internalExpressionCache = new Map();\n\nconst evaluateJsExpression = (expression, context) => {\n  let fn = internalExpressionCache.get(expression);\n  if (fn == null) {\n    internalExpressionCache.set(expression, (fn = compileJsExpression(expression)));\n  }\n  return fn(context);\n};\n\nconst evaluateJsTemplateLiteral = (templateLiteral, context) => {\n  if (!templateLiteral || !templateLiteral.length || typeof templateLiteral !== 'string') {\n    return templateLiteral;\n  }\n\n  templateLiteral = templateLiteral.trim();\n\n  if (templateLiteral === 'true') {\n    return true;\n  }\n\n  if (templateLiteral === 'false') {\n    return false;\n  }\n\n  if (templateLiteral === 'null') {\n    return null;\n  }\n\n  if (templateLiteral === 'undefined') {\n    return undefined;\n  }\n\n  if (templateLiteral.startsWith('\"') && templateLiteral.endsWith('\"')) {\n    return templateLiteral.slice(1, -1);\n  }\n\n  if (templateLiteral.startsWith('\\'') && templateLiteral.endsWith('\\'')) {\n    return templateLiteral.slice(1, -1);\n  }\n\n  if (!isNaN(templateLiteral)) {\n    const number = Number(templateLiteral);\n    // Check if the number is too high. Too high number might get altered, see #1000\n    if (number > Number.MAX_SAFE_INTEGER) {\n      return templateLiteral;\n    }\n    return number;\n  }\n\n  templateLiteral = '`' + templateLiteral + '`';\n\n  return evaluateJsExpression(templateLiteral, context);\n};\n\nconst createResponseParser = (response = {}) => {\n  const res = (expr, ...fns) => {\n    return get(response.data, expr, ...fns);\n  };\n\n  res.status = response.status;\n  res.statusText = response.statusText;\n  res.headers = response.headers;\n  res.body = response.data;\n  res.responseTime = response.responseTime;\n  res.url = response.request ? response.request.protocol + '//' + response.request.host + response.request.path : null;\n\n  res.jq = (expr) => {\n    const output = jsonQuery(expr, { data: response.data });\n    return output ? output.value : null;\n  };\n\n  return res;\n};\n\n/**\n * Objects that are created inside developer mode execution context result in an serialization error when sent to the renderer process\n * Error sending from webFrameMain:  Error: Failed to serialize arguments\n *    at s.send (node:electron/js2c/browser_init:169:631)\n *    at g.send (node:electron/js2c/browser_init:165:2156)\n * How to reproduce\n *    Remove the cleanJson fix and execute the below post response script\n *    bru.setVar(\"a\", {b:3});\n * Todo: Find a better fix\n *\n * serializes typedArrays by using Buffer to handle most binary cases\n * // TODO: reaper, replace with `devalue` after evaluating all cases, current setup is\n * more of a hotfix\n */\nconst cleanJson = (data) => {\n  const typedArrays = [\n    // Baseline typed arrays\n    Int8Array,\n    Uint8Array,\n    Uint8ClampedArray,\n    Int16Array,\n    Uint16Array,\n    Int32Array,\n    Uint32Array,\n    Float32Array,\n    Float64Array,\n    BigInt64Array,\n    BigUint64Array,\n\n    // Baseline 2025 Newly available\n    'Float16Array' in globalThis ? Float16Array : null\n  ].filter(Boolean);\n  const binaryNames = typedArrays.map((d) => d.name);\n\n  const seen = new WeakSet();\n\n  const replacer = (key, value) => {\n    if (typeof value === 'object' && value !== null) {\n      if (seen.has(value)) {\n        return '[Circular Reference]';\n      }\n      seen.add(value);\n\n      // instanceof + [[Class]] cover same-realm; duck-type fallback for cross-realm/cross-context Error-like objects\n      if (value instanceof Error || Object.prototype.toString.call(value) === '[object Error]' || (typeof value.message === 'string' && typeof value.stack === 'string')) {\n        const error = {};\n        // name/message are often on prototype; ensure they're in the output\n        error.name = value.name;\n        error.message = value.message;\n        Object.getOwnPropertyNames(value).forEach((prop) => {\n          error[prop] = value[prop];\n        });\n        return error;\n      }\n\n      const isBinary = typedArrays.find((d) => value instanceof d);\n      if (isBinary) {\n        return {\n          __cleanJSONType: isBinary.name,\n          __cleanJSONValue: Buffer.from(value.buffer).toJSON()\n        };\n      }\n    }\n    return value;\n  };\n\n  const reviver = (key, value) => {\n    if (typeof value !== 'object' || value === null) {\n      return value;\n    }\n    if ('__cleanJSONType' in value && '__cleanJSONValue' in value) {\n      const matchedName = binaryNames.find((d) => value.__cleanJSONType === d);\n      if (!matchedName) return value;\n      const binConstructor = typedArrays.find((d) => d.name === matchedName);\n\n      return binConstructor.from(Buffer.from(value.__cleanJSONValue));\n    }\n    return value;\n  };\n\n  try {\n    return JSON.parse(JSON.stringify(data, replacer), reviver);\n  } catch (e) {\n    return data;\n  }\n};\n\nconst cleanCircularJson = (data) => {\n  try {\n    // Handle circular references by keeping track of seen objects\n    const seen = new WeakSet();\n\n    const replacer = (key, value) => {\n      // Skip non-objects and null\n      if (typeof value !== 'object' || value === null) {\n        return value;\n      }\n\n      // Detect circular reference\n      if (seen.has(value)) {\n        return '[Circular Reference]';\n      }\n\n      seen.add(value);\n      return value;\n    };\n\n    return JSON.parse(JSON.stringify(data, replacer));\n  } catch (e) {\n    return data;\n  }\n};\n\nmodule.exports = {\n  evaluateJsExpression,\n  evaluateJsTemplateLiteral,\n  createResponseParser,\n  internalExpressionCache,\n  cleanJson,\n  cleanCircularJson\n};\n"
  },
  {
    "path": "packages/bruno-js/tests/bruno-request-delete-header.spec.js",
    "content": "const { describe, it, expect, beforeEach } = require('@jest/globals');\nconst BrunoRequest = require('../src/bruno-request');\n\nconst makeReq = (overrides = {}) => ({\n  url: 'http://localhost:5000/api',\n  method: 'GET',\n  headers: {\n    'Content-Type': 'application/json',\n    ...overrides.headers\n  },\n  data: undefined,\n  ...overrides\n});\n\ndescribe('BrunoRequest - header deletion', () => {\n  describe('deleteHeader()', () => {\n    it('removes a user-set header from req.headers', () => {\n      const rawReq = makeReq({ headers: { 'X-Custom': 'value' } });\n      const req = new BrunoRequest(rawReq);\n\n      req.deleteHeader('X-Custom');\n\n      expect(rawReq.headers['X-Custom']).toBeUndefined();\n    });\n\n    it('adds the header name to __headersToDelete on the req object', () => {\n      const rawReq = makeReq();\n      const req = new BrunoRequest(rawReq);\n\n      req.deleteHeader('user-agent');\n\n      expect(rawReq.__headersToDelete).toEqual(['user-agent']);\n    });\n\n    it('tracks multiple deleteHeader calls without duplicates', () => {\n      const rawReq = makeReq();\n      const req = new BrunoRequest(rawReq);\n\n      req.deleteHeader('user-agent');\n      req.deleteHeader('accept');\n      req.deleteHeader('user-agent'); // duplicate – should not be added again\n\n      expect(rawReq.__headersToDelete).toEqual(['user-agent', 'accept']);\n    });\n\n    it('does NOT attach a non-enumerable __headersToDelete to req.headers', () => {\n      const rawReq = makeReq();\n      const req = new BrunoRequest(rawReq);\n\n      req.deleteHeader('accept-encoding');\n\n      // The non-enumerable approach was removed; __headersToDelete must NOT be on headers\n      expect(rawReq.headers.__headersToDelete).toBeUndefined();\n      // But it must be on the req config object itself\n      expect(rawReq.__headersToDelete).toContain('accept-encoding');\n    });\n  });\n\n  describe('deleteHeaders()', () => {\n    it('removes multiple user-set headers from req.headers', () => {\n      const rawReq = makeReq({ headers: { 'X-A': '1', 'X-B': '2', 'X-C': '3' } });\n      const req = new BrunoRequest(rawReq);\n\n      req.deleteHeaders(['X-A', 'X-C']);\n\n      expect(rawReq.headers['X-A']).toBeUndefined();\n      expect(rawReq.headers['X-C']).toBeUndefined();\n      expect(rawReq.headers['X-B']).toBe('2');\n    });\n\n    it('adds all names to __headersToDelete so default headers can be suppressed', () => {\n      const rawReq = makeReq();\n      const req = new BrunoRequest(rawReq);\n\n      req.deleteHeaders(['user-agent', 'accept', 'accept-encoding']);\n\n      expect(rawReq.__headersToDelete).toEqual(['user-agent', 'accept', 'accept-encoding']);\n    });\n\n    it('does not duplicate entries when deleteHeaders is called with the same name twice', () => {\n      const rawReq = makeReq();\n      const req = new BrunoRequest(rawReq);\n\n      req.deleteHeaders(['user-agent', 'accept']);\n      req.deleteHeaders(['user-agent']); // duplicate\n\n      expect(rawReq.__headersToDelete).toEqual(['user-agent', 'accept']);\n    });\n\n    it('delegates to deleteHeader so tracking is consistent', () => {\n      const rawReq = makeReq({ headers: { 'X-Test': 'hello' } });\n      const req = new BrunoRequest(rawReq);\n\n      req.deleteHeaders(['X-Test', 'connection']);\n\n      // User-set header removed immediately\n      expect(rawReq.headers['X-Test']).toBeUndefined();\n      // Both tracked for interceptor\n      expect(rawReq.__headersToDelete).toContain('X-Test');\n      expect(rawReq.__headersToDelete).toContain('connection');\n    });\n  });\n\n  describe('host header protection', () => {\n    it('still tracks host in __headersToDelete even though the interceptor will ignore it', () => {\n      // The protection lives in the axios interceptor, not in BrunoRequest itself.\n      // BrunoRequest just tracks whatever the user asks to delete.\n      const rawReq = makeReq();\n      const req = new BrunoRequest(rawReq);\n\n      req.deleteHeader('host');\n\n      expect(rawReq.__headersToDelete).toContain('host');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-js/tests/runtime.spec.js",
    "content": "const { describe, it, expect } = require('@jest/globals');\nconst TestRuntime = require('../src/runtime/test-runtime');\nconst ScriptRuntime = require('../src/runtime/script-runtime');\nconst AssertRuntime = require('../src/runtime/assert-runtime');\nconst Bru = require('../src/bru');\nconst VarsRuntime = require('../src/runtime/vars-runtime');\n\ndescribe('runtime', () => {\n  describe('test-runtime', () => {\n    const baseRequest = {\n      method: 'GET',\n      url: 'http://localhost:3000/',\n      headers: {},\n      data: undefined\n    };\n    const baseResponse = {\n      status: 200,\n      statusText: 'OK',\n      data: [\n        {\n          id: 1\n        },\n        {\n          id: 2\n        },\n        {\n          id: 3\n        }\n      ]\n    };\n\n    it('should wait async tests', async () => {\n      const testFile = `\n                await test('async test', ()=> {\n                    return new Promise((resolve)=> {\n                        setTimeout(()=> {resolve()},200)\n                    })\n                })\n            `;\n\n      const runtime = new TestRuntime({ runtime: 'nodevm' });\n      const result = await runtime.runTests(\n        testFile,\n        { ...baseRequest },\n        { ...baseResponse },\n        {},\n        {},\n        '.',\n        null,\n        process.env\n      );\n      expect(result.results.map((el) => ({ description: el.description, status: el.status }))).toEqual([\n        { description: 'async test', status: 'pass' }\n      ]);\n    });\n\n    it('should have ajv and ajv-formats dependencies available', async () => {\n      const testFile = `\n                const Ajv = require('ajv');\n                const addFormats = require(\"ajv-formats\");\n                const ajv = new Ajv();\n                addFormats(ajv);\n                \n                const schema = {\n                  type: 'string',\n                  format: 'date-time'\n                };\n                \n                const validate = ajv.compile(schema)\n                \n                test('format valid', () => {\n                  const valid = validate(new Date().toISOString())\n                  expect(valid).to.be.true;\n                })\n            `;\n\n      const runtime = new TestRuntime({ runtime: 'nodevm' });\n      const result = await runtime.runTests(\n        testFile,\n        { ...baseRequest },\n        { ...baseResponse },\n        {},\n        {},\n        '.',\n        null,\n        process.env\n      );\n      expect(result.results.map((el) => ({ description: el.description, status: el.status }))).toEqual([\n        { description: 'format valid', status: 'pass' }\n      ]);\n    });\n  });\n\n  describe('script-runtime', () => {\n    describe('run-request-script', () => {\n      const baseRequest = {\n        method: 'GET',\n        url: 'http://localhost:3000/',\n        headers: {},\n        data: undefined\n      };\n\n      it('should have ajv and ajv-formats dependencies available', async () => {\n        const script = `\n                  const Ajv = require('ajv');\n                  const addFormats = require(\"ajv-formats\");\n                  const ajv = new Ajv();\n                  addFormats(ajv);\n                  \n                  const schema = {\n                    type: 'string',\n                    format: 'date-time'\n                  };\n                  \n                  const validate = ajv.compile(schema)\n                  \n                  bru.setVar('validation', validate(new Date().toISOString()))\n              `;\n\n        const runtime = new ScriptRuntime({ runtime: 'nodevm' });\n        const result = await runtime.runRequestScript(script, { ...baseRequest }, {}, {}, '.', null, process.env);\n        expect(result.runtimeVariables.validation).toBeTruthy();\n      });\n    });\n\n    describe('run-response-script', () => {\n      const baseRequest = {\n        method: 'GET',\n        url: 'http://localhost:3000/',\n        headers: {},\n        data: undefined\n      };\n      const baseResponse = {\n        status: 200,\n        statusText: 'OK',\n        data: [\n          {\n            id: 1\n          },\n          {\n            id: 2\n          },\n          {\n            id: 3\n          }\n        ]\n      };\n\n      it('should have ajv and ajv-formats dependencies available', async () => {\n        const script = `\n                  const Ajv = require('ajv');\n                  const addFormats = require(\"ajv-formats\");\n                  const ajv = new Ajv();\n                  addFormats(ajv);\n                  \n                  const schema = {\n                    type: 'string',\n                    format: 'date-time'\n                  };\n                  \n                  const validate = ajv.compile(schema)\n                  \n                  bru.setVar('validation', validate(new Date().toISOString()))\n              `;\n\n        const runtime = new ScriptRuntime({ runtime: 'nodevm' });\n        const result = await runtime.runResponseScript(\n          script,\n          { ...baseRequest },\n          { ...baseResponse },\n          {},\n          {},\n          '.',\n          null,\n          process.env\n        );\n        expect(result.runtimeVariables.validation).toBeTruthy();\n      });\n    });\n  });\n\n  describe('persistent environment variables validation', () => {\n    it('should throw error when trying to persist non-string values', async () => {\n      const script = `bru.setEnvVar('number', 42, {persist: true});`;\n      const runtime = new ScriptRuntime({ runtime: 'nodevm' });\n\n      await expect(runtime.runRequestScript(script, {}, {}, {}, '.', null, process.env))\n        .rejects.toThrow('Persistent environment variables must be strings. Received number for key \"number\".');\n    });\n\n    it('should throw error when trying to persist boolean values', async () => {\n      const script = `bru.setEnvVar('isActive', true, {persist: true});`;\n      const runtime = new ScriptRuntime({ runtime: 'nodevm' });\n\n      await expect(runtime.runRequestScript(script, {}, {}, {}, '.', null, process.env))\n        .rejects.toThrow('Persistent environment variables must be strings. Received boolean for key \"isActive\".');\n    });\n\n    it('should throw error when trying to persist object values', async () => {\n      const script = `bru.setEnvVar('config', {port: 3000}, {persist: true});`;\n      const runtime = new ScriptRuntime({ runtime: 'nodevm' });\n\n      await expect(runtime.runRequestScript(script, {}, {}, {}, '.', null, process.env))\n        .rejects.toThrow('Persistent environment variables must be strings. Received object for key \"config\".');\n    });\n\n    it('should throw error when trying to persist array values', async () => {\n      const script = `bru.setEnvVar('items', ['item1', 'item2'], {persist: true});`;\n      const runtime = new ScriptRuntime({ runtime: 'nodevm' });\n\n      await expect(runtime.runRequestScript(script, {}, {}, {}, '.', null, process.env))\n        .rejects.toThrow('Persistent environment variables must be strings. Received object for key \"items\".');\n    });\n\n    it('should allow string values when persist is true', async () => {\n      const script = `bru.setEnvVar('api_key', 'abc123', {persist: true});`;\n      const runtime = new ScriptRuntime({ runtime: 'nodevm' });\n\n      const result = await runtime.runRequestScript(script, {}, {}, {}, '.', null, process.env);\n\n      expect(result.envVariables.api_key).toBe('abc123');\n    });\n\n    it('should allow non-string values when persist is false', async () => {\n      const script = `\n        bru.setEnvVar('number', 42, {persist: false});\n        bru.setEnvVar('boolean', true, {persist: false});\n        bru.setEnvVar('object', {key: 'value'}, {persist: false});\n        bru.setEnvVar('array', [1, 2, 3], {persist: false});\n      `;\n      const runtime = new ScriptRuntime({ runtime: 'nodevm' });\n\n      const result = await runtime.runRequestScript(script, {}, {}, {}, '.', null, process.env);\n\n      expect(result.envVariables.number).toBe(42);\n      expect(result.envVariables.boolean).toBe(true);\n      expect(result.envVariables.object).toEqual({ key: 'value' });\n      expect(result.envVariables.array).toEqual([1, 2, 3]);\n    });\n\n    it('should allow non-string values when persist is not specified', async () => {\n      const script = `bru.setEnvVar('number', 42);`;\n      const runtime = new ScriptRuntime({ runtime: 'nodevm' });\n\n      const result = await runtime.runRequestScript(script, {}, {}, {}, '.', null, process.env);\n\n      expect(result.envVariables.number).toBe(42);\n    });\n  });\n\n  describe('bru.setVar random variable', () => {\n    it('should be able to set random variables as values', async () => {\n      const script = `bru.setVar('title', '{{$randomFirstName}}')`;\n\n      const runtime = new ScriptRuntime({ runtime: 'nodevm' });\n\n      const result = await runtime.runRequestScript(script, {}, {}, {}, '.', null, process.env);\n\n      expect(result.runtimeVariables.title).toBe('{{$randomFirstName}}');\n    });\n  });\n\n  describe('assert-runtime', () => {\n    const baseRequest = {\n      method: 'GET',\n      url: 'http://localhost:3000/',\n      headers: {},\n      data: undefined\n    };\n\n    const makeResponse = (data) => ({\n      status: 200,\n      statusText: 'OK',\n      data,\n      headers: {}\n    });\n\n    const runAssertions = (assertions, response, runtime = 'nodevm') => {\n      const assertRuntime = new AssertRuntime({ runtime });\n      return assertRuntime.runAssertions(assertions, { ...baseRequest }, response, {}, {}, process.env);\n    };\n\n    describe('isJson', () => {\n      it('should pass for a plain object', () => {\n        const results = runAssertions(\n          [{ name: 'res.body', value: 'isJson', enabled: true }],\n          makeResponse({ id: 1, name: 'test' })\n        );\n        expect(results[0].status).toBe('pass');\n      });\n\n      it('should pass for a nested object', () => {\n        const results = runAssertions(\n          [{ name: 'res.body', value: 'isJson', enabled: true }],\n          makeResponse({ user: { id: 1, tags: ['a', 'b'] } })\n        );\n        expect(results[0].status).toBe('pass');\n      });\n\n      it('should pass for objects from a different realm (e.g. after res.setBody in node-vm)', async () => {\n        const response = makeResponse({ id: 1, name: 'original' });\n\n        // res.setBody() inside node-vm creates a cross-realm object whose\n        // constructor is the VM's Object, not the host's Object\n        const scriptRuntime = new ScriptRuntime({ runtime: 'nodevm' });\n        await scriptRuntime.runResponseScript(\n          `res.setBody({ id: 2, name: 'updated' });`,\n          { ...baseRequest },\n          response,\n          {}, {}, '.', null, process.env\n        );\n\n        const results = runAssertions(\n          [{ name: 'res.body', value: 'isJson', enabled: true }],\n          response\n        );\n        expect(results[0].status).toBe('pass');\n      });\n\n      it('should fail for an array', () => {\n        const results = runAssertions(\n          [{ name: 'res.body', value: 'isJson', enabled: true }],\n          makeResponse([1, 2, 3])\n        );\n        expect(results[0].status).toBe('fail');\n      });\n\n      it('should fail for a string', () => {\n        const results = runAssertions(\n          [{ name: 'res.body', value: 'isJson', enabled: true }],\n          makeResponse('hello')\n        );\n        expect(results[0].status).toBe('fail');\n      });\n\n      it('should fail for null', () => {\n        const results = runAssertions(\n          [{ name: 'res.body', value: 'isJson', enabled: true }],\n          makeResponse(null)\n        );\n        expect(results[0].status).toBe('fail');\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-js/tests/setEnvVar.spec.js",
    "content": "const Bru = require('../src/bru');\n\ndescribe('Bru.setEnvVar', () => {\n  const makeBru = () =>\n    new Bru(\n      /* runtime */ 'quickjs',\n      /* envVariables */ {},\n      /* runtimeVariables */ {},\n      /* processEnvVars */ {},\n      /* collectionPath */ '/',\n      /* historyLogger */ undefined,\n      /* setVisualizations */ undefined,\n      /* secretVariables */ {},\n      /* collectionVariables */ {},\n      /* folderVariables */ {},\n      /* requestVariables */ {},\n      /* globalEnvironmentVariables */ {},\n      /* oauth2CredentialVariables */ {},\n      /* iterationDetails */ {},\n      /* collectionName */ 'Test'\n    );\n\n  test('updates envVariables and does not mark persistent when persist=false', () => {\n    const bru = makeBru();\n    bru.setEnvVar('non_persist', 'value', { persist: false });\n    expect(bru.envVariables.non_persist).toBe('value');\n    expect(bru.persistentEnvVariables.non_persist).toBeUndefined();\n  });\n\n  test('updates envVariables and tracks persistent when persist=true (string only)', () => {\n    const bru = makeBru();\n    bru.setEnvVar('persist_me', 'value', { persist: true });\n    expect(bru.envVariables.persist_me).toBe('value');\n    expect(bru.persistentEnvVariables.persist_me).toBe('value');\n  });\n\n  test('updates envVariables when options are omitted (defaults to non-persistent)', () => {\n    const bru = makeBru();\n    bru.setEnvVar('no_options', 'value');\n    expect(bru.envVariables.no_options).toBe('value');\n    expect(bru.persistentEnvVariables.no_options).toBeUndefined();\n  });\n\n  test('throws when persist=true but value is not a string', () => {\n    const bru = makeBru();\n    expect(() => bru.setEnvVar('persist_me', 123, { persist: true })).toThrow(\n      /Persistent environment variables must be strings/\n    );\n  });\n\n  test('changing existing key to non-persistent removes prior persisted entry', () => {\n    const bru = makeBru();\n    bru.setEnvVar('same_key', 'old', { persist: true });\n    expect(bru.persistentEnvVariables.same_key).toBe('old');\n\n    bru.setEnvVar('same_key', 'new');\n    expect(bru.envVariables.same_key).toBe('new');\n    expect(bru.persistentEnvVariables.same_key).toBeUndefined();\n  });\n\n  test('changing existing key to persistent updates persisted value', () => {\n    const bru = makeBru();\n    bru.setEnvVar('same_key', 'old');\n    expect(bru.persistentEnvVariables.same_key).toBeUndefined();\n\n    bru.setEnvVar('same_key', 'new', { persist: true });\n    expect(bru.envVariables.same_key).toBe('new');\n    expect(bru.persistentEnvVariables.same_key).toBe('new');\n  });\n\n  test('validates key name - invalid characters are rejected', () => {\n    const bru = makeBru();\n    expect(() => bru.setEnvVar('invalid key', 'v')).toThrow(/contains invalid characters/);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-js/tests/utils.spec.js",
    "content": "const { describe, it, expect } = require('@jest/globals');\nconst {\n  evaluateJsExpression,\n  evaluateJsTemplateLiteral,\n  internalExpressionCache: cache,\n  createResponseParser,\n  cleanJson,\n  cleanCircularJson\n} = require('../src/utils');\n\ndescribe('utils', () => {\n  describe('expression evaluation', () => {\n    const context = {\n      res: {\n        data: { pets: ['bruno', 'max'] },\n        context: 'testContext',\n        __bruno__functionInnerContext: 0\n      }\n    };\n\n    beforeEach(() => cache.clear());\n    afterEach(() => cache.clear());\n\n    it('should evaluate expression', () => {\n      let result;\n\n      result = evaluateJsExpression('res.data.pets', context);\n      expect(result).toEqual(['bruno', 'max']);\n\n      result = evaluateJsExpression('res.data.pets[0].toUpperCase()', context);\n      expect(result).toEqual('BRUNO');\n    });\n\n    it('should cache expression', () => {\n      expect(cache.size).toBe(0);\n      evaluateJsExpression('res.data.pets', context);\n      expect(cache.size).toBe(1);\n    });\n\n    it('should use cached expression', () => {\n      const expr = 'res.data.pets';\n\n      evaluateJsExpression(expr, context);\n\n      const fn = cache.get(expr);\n      expect(fn).toBeDefined();\n\n      evaluateJsExpression(expr, context);\n\n      // cache should not be overwritten\n      expect(cache.get(expr)).toBe(fn);\n    });\n\n    it('should identify top level variables', () => {\n      const expr = 'res.data.pets[0].toUpperCase()';\n      evaluateJsExpression(expr, context);\n      expect(cache.get(expr).toString()).toContain('let { res } = __bruno__functionInnerContext;');\n    });\n\n    it('should not duplicate variables', () => {\n      const expr = 'res.data.pets[0] + res.data.pets[1]';\n      evaluateJsExpression(expr, context);\n      expect(cache.get(expr).toString()).toContain('let { res } = __bruno__functionInnerContext;');\n    });\n\n    it('should exclude js keywords like true false from vars', () => {\n      const expr = 'res.data.pets.length > 0 ? true : false';\n      evaluateJsExpression(expr, context);\n      expect(cache.get(expr).toString()).toContain('let { res } = __bruno__functionInnerContext;');\n    });\n\n    it('should exclude numbers from vars', () => {\n      const expr = 'res.data.pets.length + 10';\n      evaluateJsExpression(expr, context);\n      expect(cache.get(expr).toString()).toContain('let { res } = __bruno__functionInnerContext;');\n    });\n\n    it('should pick variables from complex expressions', () => {\n      const expr = 'res.data.pets.map(pet => pet.length)';\n      const result = evaluateJsExpression(expr, context);\n      expect(result).toEqual([5, 3]);\n      expect(cache.get(expr).toString()).toContain('let { res, pet } = __bruno__functionInnerContext;');\n    });\n\n    it('should be ok picking extra vars from strings', () => {\n      const expr = '\\'hello\\' + \\' \\' + res.data.pets[0]';\n      const result = evaluateJsExpression(expr, context);\n      expect(result).toBe('hello bruno');\n      // extra var hello is harmless\n      expect(cache.get(expr).toString()).toContain('let { hello, res } = __bruno__functionInnerContext;');\n    });\n\n    it('should evaluate expressions referencing globals', () => {\n      const startTime = new Date('2022-02-01').getTime();\n      const currentTime = new Date('2022-02-02').getTime();\n\n      jest.useFakeTimers({ now: currentTime });\n\n      const expr = 'Math.max(Date.now(), startTime)';\n      const result = evaluateJsExpression(expr, { startTime });\n\n      expect(result).toBe(currentTime);\n\n      expect(cache.get(expr).toString()).toContain('Math = Math ?? globalThis.Math;');\n      expect(cache.get(expr).toString()).toContain('Date = Date ?? globalThis.Date;');\n    });\n\n    it('should use global overridden in context', () => {\n      const startTime = new Date('2022-02-01').getTime();\n      const currentTime = new Date('2022-02-02').getTime();\n\n      jest.useFakeTimers({ now: currentTime });\n\n      const context = {\n        Date: { now: () => new Date('2022-01-31').getTime() },\n        startTime\n      };\n\n      const expr = 'Math.max(Date.now(), startTime)';\n      const result = evaluateJsExpression(expr, context);\n\n      expect(result).toBe(startTime);\n    });\n\n    it('should allow \"context\" as a var name', () => {\n      const expr = 'res[\"context\"].toUpperCase()';\n      evaluateJsExpression(expr, context);\n      expect(cache.get(expr).toString()).toContain('let { res, context } = __bruno__functionInnerContext;');\n    });\n\n    it('should throw an error when we use \"__bruno__functionInnerContext\" as a var name', () => {\n      const expr = 'res[\"__bruno__functionInnerContext\"].toUpperCase()';\n      expect(() => evaluateJsExpression(expr, context)).toThrow(SyntaxError);\n      expect(() => evaluateJsExpression(expr, context)).toThrow(\n        'Identifier \\'__bruno__functionInnerContext\\' has already been declared'\n      );\n    });\n  });\n\n  describe('response parser', () => {\n    const res = createResponseParser({\n      status: 200,\n      data: {\n        order: {\n          items: [\n            { id: 1, amount: 10 },\n            { id: 2, amount: 20 }\n          ]\n        }\n      }\n    });\n\n    it('should default to bruno query', () => {\n      const value = res('..items[?].amount[0]', (i) => i.amount > 10);\n      expect(value).toBe(20);\n    });\n\n    it('should allow json-query', () => {\n      const value = res.jq('order.items[amount > 10].amount');\n      expect(value).toBe(20);\n    });\n  });\n\n  describe('cleanJson', () => {\n    it('primitives should be kept as is', () => {\n      const input = {\n        number: 1,\n        string: 'hello world',\n        booleanFalse: false,\n        booleanTrue: true,\n        float: 2.1,\n        floatDeep: 2.2222222\n      };\n      expect(cleanJson(input)).toEqual(input);\n    });\n\n    it('functions are lost', () => {\n      const func = function (x, y) {\n        return x + y;\n      };\n\n      const input = {\n        func,\n        number: 1\n      };\n\n      expect(cleanJson(input)).toEqual({\n        number: 1\n      });\n    });\n\n    it('dates are serialized', () => {\n      const date = new Date();\n      const str = date.toISOString();\n\n      const input = {\n        date\n      };\n\n      expect(cleanJson(input)).toEqual({\n        date: str\n      });\n    });\n\n    it('typed arrays should be kept as is', () => {\n      const input = {\n        Int8Array: Int8Array.from(Buffer.from('hello world').toString()),\n        Uint8Array: Uint8Array.from(Buffer.from('hello world').toString()),\n        Uint8ClampedArray: Uint8ClampedArray.from(Buffer.from('hello world').toString()),\n        Int16Array: Int16Array.from(Buffer.from('hello world').toString()),\n        Uint16Array: Uint16Array.from(Buffer.from('hello world').toString()),\n        Int32Array: Int32Array.from(Buffer.from('hello world').toString()),\n        Uint32Array: Uint32Array.from(Buffer.from('hello world').toString()),\n        Float32Array: Float32Array.from(Buffer.from('hello world').toString()),\n        Float64Array: Float64Array.from(Buffer.from('hello world').toString()),\n        BigInt64Array: BigInt64Array.from(Buffer.from('123').toString()),\n        BigUint64Array: BigUint64Array.from(Buffer.from('234').toString())\n      };\n\n      expect(cleanJson(input)).toEqual(input);\n    });\n\n    it('replaces circular references with [Circular Reference]', () => {\n      const obj = { a: 1 };\n      obj.self = obj;\n      expect(cleanJson(obj)).toEqual({ a: 1, self: '[Circular Reference]' });\n    });\n\n    it('serializes Error instances with all own properties', () => {\n      const err = new Error('oops');\n      const out = cleanJson(err);\n      expect(out).toMatchObject({ message: 'oops', name: 'Error' });\n      expect(typeof out.stack).toBe('string');\n    });\n\n    it('serializes Error with extra own properties (code, cause)', () => {\n      const err = new Error('failed');\n      err.code = 'ERR_FAILED';\n      err.cause = new Error('root cause');\n      const out = cleanJson(err);\n      expect(out.message).toBe('failed');\n      expect(out.code).toBe('ERR_FAILED');\n      expect(out.cause).toMatchObject({ message: 'root cause', name: 'Error' });\n      expect(typeof out.cause.stack).toBe('string');\n    });\n\n    it('serializes Error subclasses', () => {\n      const err = new TypeError('type oops');\n      const out = cleanJson(err);\n      expect(out).toMatchObject({ message: 'type oops', name: 'TypeError' });\n      expect(typeof out.stack).toBe('string');\n    });\n\n    it('serializes duck-typed error-like objects (message + stack strings)', () => {\n      const fake = { message: 'fake', stack: 'at line 1' };\n      const out = cleanJson(fake);\n      expect(out).toEqual(fake);\n    });\n\n    it('does not treat plain objects with non-string message/stack as errors', () => {\n      const notError = { message: 123, stack: 'at line 1' };\n      const out = cleanJson(notError);\n      expect(out).toEqual(notError);\n      const notError2 = { message: 'ok', stack: 456 };\n      const out2 = cleanJson(notError2);\n      expect(out2).toEqual(notError2);\n    });\n\n    it('serializes nested Error inside object', () => {\n      const input = { err: new Error('nested'), id: 1 };\n      const out = cleanJson(input);\n      expect(out.id).toBe(1);\n      expect(out.err).toMatchObject({ message: 'nested', name: 'Error' });\n      expect(typeof out.err.stack).toBe('string');\n    });\n\n    it('handles circular ref and Error in same structure', () => {\n      const err = new Error('cycle');\n      const obj = { err, ref: null };\n      obj.ref = obj;\n      const out = cleanJson(obj);\n      expect(out.err).toMatchObject({ message: 'cycle' });\n      expect(out.ref).toBe('[Circular Reference]');\n    });\n  });\n\n  describe('cleanCircularJson', () => {\n    it('returns primitives and plain objects as-is', () => {\n      expect(cleanCircularJson(1)).toBe(1);\n      expect(cleanCircularJson('x')).toBe('x');\n      expect(cleanCircularJson({ a: 1 })).toEqual({ a: 1 });\n    });\n\n    it('replaces circular references with [Circular Reference]', () => {\n      const obj = { a: 1 };\n      obj.self = obj;\n      expect(cleanCircularJson(obj)).toEqual({ a: 1, self: '[Circular Reference]' });\n    });\n\n    it('handles deeply nested circular ref', () => {\n      const obj = { level: 1, child: null };\n      obj.child = { level: 2, back: obj };\n      const out = cleanCircularJson(obj);\n      expect(out.level).toBe(1);\n      expect(out.child.level).toBe(2);\n      expect(out.child.back).toBe('[Circular Reference]');\n    });\n  });\n\n  describe('evaluateJsTemplateLiteral', () => {\n    it('returns non-string or empty input as-is', () => {\n      expect(evaluateJsTemplateLiteral(null)).toBe(null);\n      expect(evaluateJsTemplateLiteral('')).toBe('');\n      expect(evaluateJsTemplateLiteral(42)).toBe(42);\n    });\n\n    it('parses boolean and null literals', () => {\n      expect(evaluateJsTemplateLiteral('true')).toBe(true);\n      expect(evaluateJsTemplateLiteral('false')).toBe(false);\n      expect(evaluateJsTemplateLiteral('null')).toBe(null);\n      expect(evaluateJsTemplateLiteral('undefined')).toBe(undefined);\n    });\n\n    it('parses quoted strings', () => {\n      expect(evaluateJsTemplateLiteral('\"hello\"')).toBe('hello');\n      expect(evaluateJsTemplateLiteral('\\'world\\'')).toBe('world');\n    });\n\n    it('parses numbers', () => {\n      expect(evaluateJsTemplateLiteral('42')).toBe(42);\n      expect(evaluateJsTemplateLiteral('3.14')).toBe(3.14);\n    });\n\n    it('evaluates template literal with context', () => {\n      const context = { res: { data: { name: 'Bruno' } } };\n      expect(evaluateJsTemplateLiteral('${res.data.name}', context)).toBe('Bruno');\n    });\n\n    it('keeps large numbers as string (safe integer limit)', () => {\n      const big = '9007199254740993';\n      expect(evaluateJsTemplateLiteral(big)).toBe(big);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/.gitignore",
    "content": "node_modules\nweb\nout\n\npnpm-lock.yaml\npackage-lock.json\nyarn.lock\n"
  },
  {
    "path": "packages/bruno-lang/example/request.bru",
    "content": "type http-request\nname Send Bulk SMS\nmethod GET\nurl https://api.textlocal.in/bulk_json?apiKey=secret=&numbers=919988776655&message=hello&sender=600010\nbody-mode json\nseq 1\n\nparams\n  1 apiKey secret\n  1 numbers 998877665\n  1 message hello\n/params\n\nheaders\n  1 content-type application/json\n  1 accept-language en-US,en;q=0.9,hi;q=0.8\n  0 transaction-id {{transactionId}}\n/headers\n\nbody(type=json)\n  {\n    \"apikey\": \"secret\",\n    \"numbers\": \"+91998877665\",\n    \"data\": {\n      \"sender\": \"TXTLCL\",\n      \"messages\": [{\n        \"numbers\": \"+91998877665\",\n        \"message\": \"Hello World\"\n      }]\n    }\n  }\n/body\n\nbody(type=graphql)\n  {\n    launchesPast {\n      launch_site {\n        site_name\n      }\n      launch_success\n    }\n  }\n/body\n\nscript\n  let user = 'John Doe';\n\n  function onRequest(request) {\n    request.body.user = user;\n  }\n\n  function onResponse(request, response) {\n    expect(response.status).to.equal(200);\n  }\n/script\n"
  },
  {
    "path": "packages/bruno-lang/example/request.json",
    "content": "{\n  \"type\": \"http-request\",\n  \"name\": \"Send Bulk SMS\",\n  \"request\": {\n    \"method\": \"GET\",\n    \"url\": \"https://api.textlocal.in/bulk_json?apiKey=secret=&numbers=998877665&message=hello&sender=600010\",\n    \"params\": [\n      {\n        \"name\": \"apiKey\",\n        \"value\": \"secret\",\n        \"enabled\": true\n      },\n      {\n        \"name\": \"numbers\",\n        \"value\": \"998877665\",\n        \"enabled\": true\n      },\n      {\n        \"name\": \"message\",\n        \"value\": \"hello\",\n        \"enabled\": true\n      },\n      {\n        \"name\": \"sender\",\n        \"value\": \"600010\",\n        \"enabled\": true\n      }\n    ],\n    \"headers\": [],\n    \"body\": {\n      \"mode\": \"json\",\n      \"json\": \"{\\n  apikey: \\\"secret\\\",\\n  numbers: \\\"+919988776655\\\",\\n  data: {\\n    sender: \\\"TXTLCL\\\",\\n    messages: [{\\n      numbers: \\\"+919988776655\\\",\\n      message: \\\"Hello World\\\"\\n    }]\\n  }\\n}\",\n      \"text\": null,\n      \"xml\": null,\n      \"multipartForm\": null,\n      \"formUrlEncoded\": null\n    }\n  }\n}"
  },
  {
    "path": "packages/bruno-lang/example/request.next.bru",
    "content": "type http-request\nname Send Bulk SMS\nmethod GET\nurl https://api.textlocal.in/bulk_json?apiKey=secret=&numbers=919988776655&message=hello&sender=600010\nbody-mode json\nseq 1\n\nparams\n  1 apiKey secret\n  1 numbers 998877665\n  1 message hello\n/params\n\nheaders\n  1 content-type application/json\n  1 accept-language en-US,en;q=0.9,hi;q=0.8\n  0 transaction-id {{transactionId}}\n/headers\n\nbody(type=json)\n  {\n    apikey: \"secret\",\n    numbers: \"+91998877665\",\n    data: {\n      sender: \"TXTLCL\",\n      messages: [{\n        numbers: \"+91998877665\",\n        message: \"Hello World\"\n      }]\n    }\n  }\n/body\n\nbody(type=graphql)\n  {\n    launchesPast {\n      launch_site {\n        site_name\n      }\n      launch_success\n    }\n  }\n/body\n\nscript\n  let user = 'John Doe';\n\n  function onRequest(request) {\n    request.body.user = user;\n  }\n\n  function onResponse(request, response) {\n    expect(response.status).to.equal(200);\n  }\n/script\n\nassert\n  1 $res.data.order.items.length 1\n  1 $res.data.orderNumber.isDefined true\n/assert\n\ndocs\n  Documentation about the request\n/docs\n\nresponse-example(name=\"Created\", status=201)\n  headers\n    1 content-type  application/json\n    1 accept-language en-US,en;q=0.9,hi;q=0.8\n    0 transaction-id {{transactionId}}\n  /headers\n\n  body\n    {\n      \"data\": {\n        \"launchesPast\": [\n          {\n            \"launch_site\": {\n              \"site_name\": \"CCAFS SLC 40\"\n            },\n            \"launch_success\": true\n          },\n          {\n            \"launch_site\": {\n              \"site_name\": \"VAFB SLC 4E\"\n            },\n            \"launch_success\": true\n          }\n        ]\n      }\n    }\n  /body\n/response-example"
  },
  {
    "path": "packages/bruno-lang/license.md",
    "content": "\nMIT License\n\nCopyright (c) 2022 Anoop M D, Anusree P S and Contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "packages/bruno-lang/package.json",
    "content": "{\n  \"name\": \"@usebruno/lang\",\n  \"version\": \"0.12.0\",\n  \"license\": \"MIT\",\n  \"main\": \"src/index.js\",\n  \"files\": [\n    \"src\",\n    \"v1\",\n    \"v2\",\n    \"package.json\"\n  ],\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"dependencies\": {\n    \"arcsecond\": \"^5.0.0\",\n    \"dotenv\": \"^16.3.1\",\n    \"lodash\": \"^4.17.21\",\n    \"ohm-js\": \"^16.6.0\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-lang/readme.md",
    "content": "# bruno-lang\n\nThe language utils for working with `.bru` files\n\n### Publish to Npm Registry\n```bash\nnpm publish --access=public\n```"
  },
  {
    "path": "packages/bruno-lang/src/index.js",
    "content": "const bruToJsonV2 = require('../v2/src/bruToJson');\nconst jsonToBruV2 = require('../v2/src/jsonToBru');\nconst bruToEnvJsonV2 = require('../v2/src/envToJson');\nconst envJsonToBruV2 = require('../v2/src/jsonToEnv');\nconst dotenvToJson = require('../v2/src/dotenvToJson');\n\nconst collectionBruToJson = require('../v2/src/collectionBruToJson');\nconst jsonToCollectionBru = require('../v2/src/jsonToCollectionBru');\n\n// Todo: remove V2 suffixes\n// Changes will have to be made to the CLI and GUI\n\nmodule.exports = {\n  bruToJsonV2,\n  jsonToBruV2,\n  bruToEnvJsonV2,\n  envJsonToBruV2,\n\n  collectionBruToJson,\n  jsonToCollectionBru,\n\n  dotenvToJson\n};\n"
  },
  {
    "path": "packages/bruno-lang/v1/src/body-tag.js",
    "content": "const { between, regex, everyCharUntil } = require('arcsecond');\nconst keyvalLines = require('./key-val-lines');\n\n// body(type=json)\nconst bodyJsonBegin = regex(/^body\\s*\\(\\s*type\\s*=\\s*json\\s*\\)\\s*\\r?\\n/);\n\n// body(type=graphql)\nconst bodyGraphqlBegin = regex(/^body\\s*\\(\\s*type\\s*=\\s*graphql\\s*\\)\\s*\\r?\\n/);\n\n// body(type=graphql-vars)\nconst bodyGraphqlVarsBegin = regex(/^body\\s*\\(\\s*type\\s*=\\s*graphql-vars\\s*\\)\\s*\\r?\\n/);\n\n// body(type=text)\nconst bodyTextBegin = regex(/^body\\s*\\(\\s*type\\s*=\\s*text\\s*\\)\\s*\\r?\\n/);\n\n// body(type=xml)\nconst bodyXmlBegin = regex(/^body\\s*\\(\\s*type\\s*=\\s*xml\\s*\\)\\s*\\r?\\n/);\n\nconst bodyEnd = regex(/^[\\r?\\n]+\\/body\\s*[\\r?\\n]*/);\n\nconst bodyJsonTag = between(bodyJsonBegin)(bodyEnd)(everyCharUntil(bodyEnd)).map((bodyJson) => {\n  return {\n    body: {\n      json: bodyJson\n    }\n  };\n});\n\nconst bodyGraphqlTag = between(bodyGraphqlBegin)(bodyEnd)(everyCharUntil(bodyEnd)).map((bodyGraphql) => {\n  return {\n    body: {\n      graphql: {\n        query: bodyGraphql\n      }\n    }\n  };\n});\n\nconst bodyGraphqlVarsTag = between(bodyGraphqlVarsBegin)(bodyEnd)(everyCharUntil(bodyEnd)).map((varsGraphql) => {\n  return {\n    body: {\n      graphql: {\n        variables: varsGraphql\n      }\n    }\n  };\n});\n\nconst bodyTextTag = between(bodyTextBegin)(bodyEnd)(everyCharUntil(bodyEnd)).map((bodyText) => {\n  return {\n    body: {\n      text: bodyText\n    }\n  };\n});\n\nconst bodyXmlTag = between(bodyXmlBegin)(bodyEnd)(everyCharUntil(bodyEnd)).map((bodyXml) => {\n  return {\n    body: {\n      xml: bodyXml\n    }\n  };\n});\n\n/**\n * We have deprecated form-url-encoded type in body tag, it was a misspelling on my part\n * The new type is form-urlencoded\n *\n * Very few people would have used this. I launched this to the public on 22 Jan 2023\n * And I am making the change on 23 Jan 2023\n *\n * This deprecated tag can be removed on 1 April 2023\n */\n\n// body(type=form-url-encoded)\nconst bodyFormUrlEncodedDeprecated = regex(/^body\\s*\\(\\s*type\\s*=\\s*form-url-encoded\\s*\\)\\s*\\r?\\n/);\n\n// body(type=form-urlencoded)\nconst bodyFormUrlEncoded = regex(/^body\\s*\\(\\s*type\\s*=\\s*form-urlencoded\\s*\\)\\s*\\r?\\n/);\n\n// body(type=multipart-form)\nconst bodyMultipartForm = regex(/^body\\s*\\(\\s*type\\s*=\\s*multipart-form\\s*\\)\\s*\\r?\\n/);\n\n// this regex allows the body end tag to start without a newline\n// currently the line parser consumes the last newline\n// todo: fix this\nconst bodyEndRelaxed = regex(/^[\\r?\\n]*\\/body\\s*[\\r?\\n]*/);\n\nconst bodyFormUrlEncodedTagDeprecated = between(bodyFormUrlEncodedDeprecated)(bodyEndRelaxed)(keyvalLines).map(\n  ([result]) => {\n    return {\n      body: {\n        formUrlEncoded: result\n      }\n    };\n  }\n);\n\nconst bodyFormUrlEncodedTag = between(bodyFormUrlEncoded)(bodyEndRelaxed)(keyvalLines).map(([result]) => {\n  return {\n    body: {\n      formUrlEncoded: result\n    }\n  };\n});\n\nconst bodyMultipartFormTag = between(bodyMultipartForm)(bodyEndRelaxed)(keyvalLines).map(([result]) => {\n  return {\n    body: {\n      multipartForm: result\n    }\n  };\n});\n\nmodule.exports = {\n  bodyJsonTag,\n  bodyGraphqlTag,\n  bodyGraphqlVarsTag,\n  bodyTextTag,\n  bodyXmlTag,\n  bodyFormUrlEncodedTagDeprecated,\n  bodyFormUrlEncodedTag,\n  bodyMultipartFormTag\n};\n"
  },
  {
    "path": "packages/bruno-lang/v1/src/env-vars-tag.js",
    "content": "const { between, regex } = require('arcsecond');\nconst { each } = require('lodash');\nconst keyValLines = require('./key-val-lines');\n\nconst begin = regex(/^vars\\s*\\r?\\n/);\nconst end = regex(/^[\\r?\\n]*\\/vars\\s*[\\r?\\n]*/);\n\nconst envVarsTag = between(begin)(end)(keyValLines).map(([variables]) => {\n  each(variables, (variable) => {\n    variable.type = 'text';\n  });\n\n  return {\n    variables\n  };\n});\n\nmodule.exports = envVarsTag;\n"
  },
  {
    "path": "packages/bruno-lang/v1/src/headers-tag.js",
    "content": "const { between, regex } = require('arcsecond');\nconst keyValLines = require('./key-val-lines');\n\nconst begin = regex(/^headers\\s*\\r?\\n/);\nconst end = regex(/^[\\r?\\n]*\\/headers\\s*[\\r?\\n]*/);\n\nconst headersTag = between(begin)(end)(keyValLines).map(([headers]) => {\n  return {\n    headers\n  };\n});\n\nmodule.exports = headersTag;\n"
  },
  {
    "path": "packages/bruno-lang/v1/src/index.js",
    "content": "const { many, choice, anyChar } = require('arcsecond');\nconst _ = require('lodash');\nconst { indentString, outdentString } = require('./utils');\n\nconst inlineTag = require('./inline-tag');\nconst paramsTag = require('./params-tag');\nconst headersTag = require('./headers-tag');\nconst {\n  bodyJsonTag,\n  bodyGraphqlTag,\n  bodyGraphqlVarsTag,\n  bodyTextTag,\n  bodyXmlTag,\n  bodyFormUrlEncodedTagDeprecated,\n  bodyFormUrlEncodedTag,\n  bodyMultipartFormTag\n} = require('./body-tag');\nconst scriptTag = require('./script-tag');\nconst testsTag = require('./tests-tag');\n\nconst bruToJson = (fileContents) => {\n  const parser = many(\n    choice([\n      inlineTag,\n      paramsTag,\n      headersTag,\n      bodyJsonTag,\n      bodyGraphqlTag,\n      bodyGraphqlVarsTag,\n      bodyTextTag,\n      bodyXmlTag,\n      bodyFormUrlEncodedTagDeprecated,\n      bodyFormUrlEncodedTag,\n      bodyMultipartFormTag,\n      scriptTag,\n      testsTag,\n      anyChar\n    ])\n  );\n\n  const parsed = parser.run(fileContents).result.reduce((acc, item) => _.merge(acc, item), {});\n\n  const json = {\n    type: parsed.type || '',\n    name: parsed.name || '',\n    seq: parsed.seq ? Number(parsed.seq) : 1,\n    request: {\n      method: parsed.method || '',\n      url: parsed.url || '',\n      params: parsed.params || [],\n      headers: parsed.headers || [],\n      body: parsed.body || { mode: 'none' },\n      script: parsed.script ? outdentString(parsed.script) : '',\n      tests: parsed.tests ? outdentString(parsed.tests) : ''\n    }\n  };\n\n  const body = _.get(json, 'request.body');\n\n  if (body && body.text) {\n    body.text = outdentString(body.text);\n  }\n\n  if (body && body.json) {\n    body.json = outdentString(body.json);\n  }\n\n  if (body && body.xml) {\n    body.xml = outdentString(body.xml);\n  }\n\n  if (body && body.graphql && body.graphql.query) {\n    body.graphql.query = outdentString(body.graphql.query);\n  }\n\n  if (body && body.graphql && body.graphql.variables) {\n    body.graphql.variables = outdentString(body.graphql.variables);\n  }\n\n  return json;\n};\n\nconst jsonToBru = (json) => {\n  const {\n    type,\n    name,\n    seq,\n    request: { method, url, params, headers, body, script, tests }\n  } = json;\n\n  let bru = `name ${name}\nmethod ${method}\nurl ${url}\ntype ${type}\nbody-mode ${body ? body.mode : 'none'}\nseq ${seq ? seq : 1}\n`;\n\n  if (params && params.length) {\n    bru += `\nparams\n${params.map((param) => `  ${param.enabled ? 1 : 0} ${param.name} ${param.value}`).join('\\n')}\n/params\n`;\n  }\n\n  if (headers && headers.length) {\n    bru += `\nheaders\n${headers.map((header) => `  ${header.enabled ? 1 : 0} ${header.name} ${header.value}`).join('\\n')}\n/headers\n`;\n  }\n\n  if (body && body.json && body.json.length) {\n    bru += `\nbody(type=json)\n${indentString(body.json)}\n/body\n`;\n  }\n\n  if (body && body.graphql && body.graphql.query) {\n    bru += `\nbody(type=graphql)\n${indentString(body.graphql.query)}\n/body\n`;\n  }\n\n  if (body && body.graphql && body.graphql.variables) {\n    bru += `\nbody(type=graphql-vars)\n${indentString(body.graphql.variables)}\n/body\n`;\n  }\n\n  if (body && body.text && body.text.length) {\n    bru += `\nbody(type=text)\n${indentString(body.text)}\n/body\n`;\n  }\n\n  if (body && body.xml && body.xml.length) {\n    bru += `\nbody(type=xml)\n${indentString(body.xml)}\n/body\n`;\n  }\n\n  if (body && body.formUrlEncoded && body.formUrlEncoded.length) {\n    bru += `\nbody(type=form-urlencoded)\n${body.formUrlEncoded.map((item) => `  ${item.enabled ? 1 : 0} ${item.name} ${item.value}`).join('\\n')}\n/body\n`;\n  }\n\n  if (body && body.multipartForm && body.multipartForm.length) {\n    bru += `\nbody(type=multipart-form)\n${body.multipartForm.map((item) => `  ${item.enabled ? 1 : 0} ${item.name} ${item.value}`).join('\\n')}\n/body\n`;\n  }\n\n  if (script && script.length) {\n    bru += `\nscript\n${indentString(script)}\n/script\n`;\n  }\n\n  if (tests && tests.length) {\n    bru += `\ntests\n${indentString(tests)}\n/tests\n`;\n  }\n\n  return bru;\n};\n\n// env\nconst envVarsTag = require('./env-vars-tag');\n\nconst bruToEnvJson = (fileContents) => {\n  const parser = many(choice([envVarsTag, anyChar]));\n\n  const parsed = parser.run(fileContents).result.reduce((acc, item) => _.merge(acc, item), {});\n\n  const json = {\n    variables: parsed.variables || []\n  };\n\n  return json;\n};\n\nconst envJsonToBru = (json) => {\n  const { variables } = json;\n\n  let bru = '';\n\n  if (variables && variables.length) {\n    bru += `vars\n${variables.map((item) => `  ${item.enabled ? 1 : 0} ${item.name} ${item.value}`).join('\\n')}\n/vars\n`;\n  }\n\n  return bru;\n};\n\nmodule.exports = {\n  bruToJson,\n  jsonToBru,\n  bruToEnvJson,\n  envJsonToBru\n};\n"
  },
  {
    "path": "packages/bruno-lang/v1/src/inline-tag.js",
    "content": "const { sequenceOf, str, regex, choice, endOfInput, everyCharUntil } = require('arcsecond');\n\nconst whitespace = regex(/^[ \\t]*/);\nconst newline = regex(/^\\r?\\n/);\nconst newLineOrEndOfInput = choice([endOfInput, newline]);\n\nconst inlineTag = sequenceOf([\n  choice([str('type'), str('name'), str('method'), str('url'), str('seq'), str('body-mode')]),\n  whitespace,\n  choice([newline, everyCharUntil(newLineOrEndOfInput)])\n]).map(([key, _, val]) => {\n  if (val === '\\n' || val === '\\r\\n') {\n    val = '';\n  }\n\n  if (key === 'body-mode') {\n    return {\n      body: {\n        mode: val\n      }\n    };\n  }\n\n  return { [key]: val };\n});\n\nmodule.exports = inlineTag;\n"
  },
  {
    "path": "packages/bruno-lang/v1/src/key-val-lines.js",
    "content": "const { sequenceOf, whitespace, optionalWhitespace, choice, digit, many, regex, sepBy } = require('arcsecond');\n\nconst newline = regex(/^\\r?\\n/);\nconst wordWithoutWhitespace = regex(/^[^\\s\\r?\\t\\n]+/g);\nconst wordWithWhitespace = regex(/^[^\\r?\\n]+/g);\n\n// matching lines like: 1 key value\nconst line = sequenceOf([\n  optionalWhitespace,\n  digit,\n  whitespace,\n  wordWithoutWhitespace,\n  whitespace,\n  wordWithWhitespace\n]).map(([_, enabled, __, key, ___, value]) => {\n  return {\n    enabled: Number(enabled) ? true : false,\n    name: key ? key.trim() : '',\n    value: value ? value.trim() : ''\n  };\n});\n\n// matching lines like: 1 key follows by [whitespaces] and a newline\nconst line2 = sequenceOf([optionalWhitespace, digit, whitespace, wordWithoutWhitespace, regex(/^\\s*\\r?\\n/)]).map(\n  ([_, enabled, __, key]) => {\n    return {\n      enabled: Number(enabled) ? true : false,\n      name: key,\n      value: ''\n    };\n  }\n);\n\n// matching lines like: 1 followed by [whitespaces] and a newline\nconst line3 = sequenceOf([optionalWhitespace, digit, regex(/^\\s*\\r?\\n/)]).map(([_, enabled]) => {\n  return {\n    enabled: Number(enabled) ? true : false,\n    name: '',\n    value: ''\n  };\n});\n\nconst lines = many(choice([line3, line2, line]));\n\nconst keyValLines = sepBy(newline)(lines);\n\nmodule.exports = keyValLines;\n"
  },
  {
    "path": "packages/bruno-lang/v1/src/params-tag.js",
    "content": "const { between, regex } = require('arcsecond');\nconst keyValLines = require('./key-val-lines');\n\nconst begin = regex(/^params\\s*\\r?\\n/);\nconst end = regex(/^[\\r?\\n]*\\/params\\s*[\\r?\\n]*/);\n\nconst paramsTag = between(begin)(end)(keyValLines).map(([params]) => {\n  return {\n    params\n  };\n});\n\nmodule.exports = paramsTag;\n"
  },
  {
    "path": "packages/bruno-lang/v1/src/script-tag.js",
    "content": "const { between, regex, everyCharUntil } = require('arcsecond');\n\nconst scriptBegin = regex(/^script\\s*\\r?\\n/);\nconst scriptEnd = regex(/^[\\r?\\n]+\\/script[\\s\\r?\\n]*/);\n\nconst scriptTag = between(scriptBegin)(scriptEnd)(everyCharUntil(scriptEnd)).map((script) => {\n  return {\n    script: script\n  };\n});\n\nmodule.exports = scriptTag;\n"
  },
  {
    "path": "packages/bruno-lang/v1/src/tests-tag.js",
    "content": "const { between, regex, everyCharUntil } = require('arcsecond');\n\nconst testsBegin = regex(/^tests\\s*\\r?\\n/);\nconst testsEnd = regex(/^[\\r?\\n]+\\/tests[\\s\\r?\\n]*/);\n\nconst testsTag = between(testsBegin)(testsEnd)(everyCharUntil(testsEnd)).map((tests) => {\n  return {\n    tests: tests\n  };\n});\n\nmodule.exports = testsTag;\n"
  },
  {
    "path": "packages/bruno-lang/v1/src/utils.js",
    "content": "// safely parse json\nconst safeParseJson = (json) => {\n  try {\n    return JSON.parse(json);\n  } catch (e) {\n    return null;\n  }\n};\n\nconst indentString = (str) => {\n  if (!str || !str.length) {\n    return str || '';\n  }\n\n  return str\n    .split('\\n')\n    .map((line) => '  ' + line)\n    .join('\\n');\n};\n\nconst outdentString = (str) => {\n  if (!str || !str.length) {\n    return str || '';\n  }\n\n  return str\n    .split('\\n')\n    .map((line) => line.replace(/^  /, ''))\n    .join('\\n');\n};\n\nmodule.exports = {\n  safeParseJson,\n  indentString,\n  outdentString\n};\n"
  },
  {
    "path": "packages/bruno-lang/v1/tests/body-tag.spec.js",
    "content": "const { bodyJsonTag } = require('../src/body-tag');\n\ndescribe('bodyJsonTag', () => {\n  const testbodyJson = (input, expected) => {\n    const result = bodyJsonTag.run(input);\n    expect(result.isError).toBe(false);\n    expect(result.result.body.json).toEqual('{ \"foo\": \"bar\" }');\n  };\n\n  // simple case\n  it('should parse json body tag - 1', () => {\n    const input = 'body(type=json)\\n{ \"foo\": \"bar\" }\\n/body';\n    testbodyJson(input, '{ \"foo\": \"bar\" }\\n');\n  });\n\n  // space between body and args\n  it('should parse json body tag - 2', () => {\n    const input = 'body (type = json)\\n{ \"foo\": \"bar\" }\\n/body';\n    testbodyJson(input, '{ \"foo\": \"bar\" }\\n');\n  });\n\n  // space after body tag\n  it('should parse json body tag - 3', () => {\n    const input = 'body (type = json)  \\n{ \"foo\": \"bar\" }\\n/body';\n    testbodyJson(input, '{ \"foo\": \"bar\" }\\n');\n  });\n\n  // space after body tag\n  it('should parse json body tag - 4', () => {\n    const input = 'body (type = json)  \\n{ \"foo\": \"bar\" }\\n/body ';\n    testbodyJson(input, '{ \"foo\": \"bar\" }\\n');\n  });\n\n  it('should fail to parse when body tag is missing', () => {\n    const input = '{ \"foo\": \"bar\" }\\n/body';\n    const result = bodyJsonTag.run(input);\n    expect(result.isError).toBe(true);\n  });\n\n  it('should fail to parse when body end tag is missing', () => {\n    const input = 'body (type = json)\\n{ \"foo\": \"bar\" }';\n    const result = bodyJsonTag.run(input);\n    expect(result.isError).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v1/tests/bru-to-env-json.spec.js",
    "content": "const fs = require('fs');\nconst path = require('path');\n\nconst { bruToEnvJson } = require('../src');\n\ndescribe('bruToEnvJson', () => {\n  it('should parse .bru file contents', () => {\n    const requestFile = fs.readFileSync(path.join(__dirname, 'fixtures', 'env.bru'), 'utf8');\n    const result = bruToEnvJson(requestFile);\n\n    expect(result).toEqual({\n      variables: [\n        {\n          enabled: true,\n          name: 'host',\n          value: 'https://www.google.com',\n          type: 'text'\n        },\n        {\n          enabled: true,\n          name: 'jwt',\n          value: 'secret',\n          type: 'text'\n        },\n        {\n          enabled: false,\n          name: 'Content-type',\n          value: 'application/json',\n          type: 'text'\n        }\n      ]\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v1/tests/bru-to-json.spec.js",
    "content": "const fs = require('fs');\nconst path = require('path');\n\nconst { bruToJson } = require('../src');\n\ndescribe('bruToJson', () => {\n  it('should parse .bru file contents', () => {\n    const requestFile = fs.readFileSync(path.join(__dirname, 'fixtures', 'request.bru'), 'utf8');\n    const result = bruToJson(requestFile);\n\n    expect(result).toEqual({\n      type: 'http-request',\n      name: 'Send Bulk SMS',\n      seq: 1,\n      request: {\n        method: 'GET',\n        url: 'https://api.textlocal.in/bulk_json?apiKey=secret=&numbers=919988776655&message=hello&sender=600010',\n        params: [\n          {\n            enabled: true,\n            name: 'apiKey',\n            value: 'secret'\n          },\n          {\n            enabled: true,\n            name: 'numbers',\n            value: '998877665'\n          },\n          {\n            enabled: true,\n            name: 'message',\n            value: 'hello'\n          }\n        ],\n        headers: [\n          {\n            enabled: true,\n            name: 'content-type',\n            value: 'application/json'\n          },\n          {\n            enabled: true,\n            name: 'accept-language',\n            value: 'en-US,en;q=0.9,hi;q=0.8'\n          },\n          {\n            enabled: false,\n            name: 'transaction-id',\n            value: '{{transactionId}}'\n          }\n        ],\n        body: {\n          mode: 'json',\n          json: '{\\n  \"apikey\": \"secret\",\\n  \"numbers\": \"+91998877665\"\\n}',\n          graphql: {\n            query: '{\\n  launchesPast {\\n    launch_success\\n  }\\n}'\n          },\n          text: 'Hello, there. You must be from the past',\n          xml: '<body>back to the ice age</body>',\n          formUrlEncoded: [\n            {\n              enabled: true,\n              name: 'username',\n              value: 'john'\n            },\n            {\n              enabled: false,\n              name: 'password',\n              value: '{{password}}'\n            }\n          ],\n          multipartForm: [\n            {\n              enabled: true,\n              name: 'username',\n              value: 'nash'\n            },\n            {\n              enabled: false,\n              name: 'password',\n              value: 'governingdynamics'\n            }\n          ]\n        },\n        script: 'const foo=\\'bar\\';',\n        tests: 'bruno.test(\\'200 ok\\', () => {});'\n      }\n    });\n  });\n});\n\ndescribe('jsonToBru - should parse bru file having empty url', () => {\n  const requestFile = `name Send Bulk SMS\nmethod GET\nurl \ntype http-request\nbody-mode none\nseq 1\n`;\n\n  it('should parse .bru file having empty url', () => {\n    const result = bruToJson(requestFile);\n\n    expect(result).toEqual({\n      type: 'http-request',\n      name: 'Send Bulk SMS',\n      seq: 1,\n      request: {\n        method: 'GET',\n        url: '',\n        params: [],\n        headers: [],\n        body: { mode: 'none' },\n        script: '',\n        tests: ''\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v1/tests/env-json-to-bru.spec.js",
    "content": "const fs = require('fs');\nconst path = require('path');\n\nconst { envJsonToBru } = require('../src');\n\ndescribe('envJsonToBru', () => {\n  it('should convert json file into .bru file', () => {\n    const env = {\n      variables: [\n        {\n          enabled: true,\n          name: 'host',\n          value: 'https://www.google.com',\n          type: 'text'\n        },\n        {\n          enabled: true,\n          name: 'jwt',\n          value: 'secret',\n          type: 'text'\n        },\n        {\n          enabled: false,\n          name: 'Content-type',\n          value: 'application/json',\n          type: 'text'\n        }\n      ]\n    };\n\n    const expectedBruFile = fs.readFileSync(path.join(__dirname, 'fixtures', 'env.bru'), 'utf8');\n    const actualBruFile = envJsonToBru(env);\n\n    expect(expectedBruFile).toEqual(actualBruFile);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v1/tests/fixtures/env.bru",
    "content": "vars\n  1 host https://www.google.com\n  1 jwt secret\n  0 Content-type application/json\n/vars\n"
  },
  {
    "path": "packages/bruno-lang/v1/tests/fixtures/request.bru",
    "content": "name Send Bulk SMS\nmethod GET\nurl https://api.textlocal.in/bulk_json?apiKey=secret=&numbers=919988776655&message=hello&sender=600010\ntype http-request\nbody-mode json\nseq 1\n\nparams\n  1 apiKey secret\n  1 numbers 998877665\n  1 message hello\n/params\n\nheaders\n  1 content-type application/json\n  1 accept-language en-US,en;q=0.9,hi;q=0.8\n  0 transaction-id {{transactionId}}\n/headers\n\nbody(type=json)\n  {\n    \"apikey\": \"secret\",\n    \"numbers\": \"+91998877665\"\n  }\n/body\n\nbody(type=graphql)\n  {\n    launchesPast {\n      launch_success\n    }\n  }\n/body\n\nbody(type=text)\n  Hello, there. You must be from the past\n/body\n\nbody(type=xml)\n  <body>back to the ice age</body>\n/body\n\nbody(type=form-urlencoded)\n  1 username john\n  0 password {{password}}\n/body\n\nbody(type=multipart-form)\n  1 username nash\n  0 password governingdynamics\n/body\n\nscript\n  const foo='bar';\n/script\n\ntests\n  bruno.test('200 ok', () => {});\n/tests\n"
  },
  {
    "path": "packages/bruno-lang/v1/tests/inline-tag.spec.js",
    "content": "const inlineTag = require('../src/inline-tag');\nconst { sepBy, char, many } = require('arcsecond');\n\ndescribe('type', () => {\n  it('should parse the type', () => {\n    const input = 'type http-request';\n    const result = inlineTag.run(input);\n    expect(result.isError).toBe(false);\n    expect(result.result).toEqual({ type: 'http-request' });\n  });\n\n  it('should allow whitespaces while parsing the type', () => {\n    const input = 'type    http-request';\n    const result = inlineTag.run(input);\n    expect(result.isError).toBe(false);\n    expect(result.result).toEqual({ type: 'http-request' });\n  });\n\n  it('should fail to parse when type is missing', () => {\n    const input = 'type';\n    const result = inlineTag.run(input);\n    expect(result.isError).toBe(true);\n  });\n});\n\ndescribe('multiple inline tags', () => {\n  it('should parse the multiple inline tags', () => {\n    const input = `\ntype http-request\nname Send Bulk SMS\nmethod GET\nurl https://api.textlocal.in/bulk_json?apiKey=secret=&numbers=919988776655&message=hello&sender=600010\nbody-mode json\n    `;\n\n    const newline = char('\\n');\n    const line = inlineTag;\n    const lines = many(line);\n    const parser = sepBy(newline)(lines);\n\n    const result = parser.run(input);\n\n    expect(result.isError).toBe(false);\n    expect(result.result).toEqual([\n      [],\n      [{ type: 'http-request' }],\n      [{ name: 'Send Bulk SMS' }],\n      [{ method: 'GET' }],\n      [{ url: 'https://api.textlocal.in/bulk_json?apiKey=secret=&numbers=919988776655&message=hello&sender=600010' }],\n      [{ body: { mode: 'json' } }],\n      []\n    ]);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v1/tests/json-to-bru.spec.js",
    "content": "const fs = require('fs');\nconst path = require('path');\n\nconst { jsonToBru } = require('../src');\n\ndescribe('bruToJson', () => {\n  it('should convert json file into .bru file', () => {\n    const request = {\n      type: 'http-request',\n      name: 'Send Bulk SMS',\n      seq: 1,\n      request: {\n        method: 'GET',\n        url: 'https://api.textlocal.in/bulk_json?apiKey=secret=&numbers=919988776655&message=hello&sender=600010',\n        params: [\n          {\n            enabled: true,\n            name: 'apiKey',\n            value: 'secret'\n          },\n          {\n            enabled: true,\n            name: 'numbers',\n            value: '998877665'\n          },\n          {\n            enabled: true,\n            name: 'message',\n            value: 'hello'\n          }\n        ],\n        headers: [\n          {\n            enabled: true,\n            name: 'content-type',\n            value: 'application/json'\n          },\n          {\n            enabled: true,\n            name: 'accept-language',\n            value: 'en-US,en;q=0.9,hi;q=0.8'\n          },\n          {\n            enabled: false,\n            name: 'transaction-id',\n            value: '{{transactionId}}'\n          }\n        ],\n        body: {\n          mode: 'json',\n          json: '{\\n  \"apikey\": \"secret\",\\n  \"numbers\": \"+91998877665\"\\n}',\n          graphql: {\n            query: '{\\n  launchesPast {\\n    launch_success\\n  }\\n}'\n          },\n          text: 'Hello, there. You must be from the past',\n          xml: '<body>back to the ice age</body>',\n          formUrlEncoded: [\n            {\n              enabled: true,\n              name: 'username',\n              value: 'john'\n            },\n            {\n              enabled: false,\n              name: 'password',\n              value: '{{password}}'\n            }\n          ],\n          multipartForm: [\n            {\n              enabled: true,\n              name: 'username',\n              value: 'nash'\n            },\n            {\n              enabled: false,\n              name: 'password',\n              value: 'governingdynamics'\n            }\n          ]\n        },\n        script: 'const foo=\\'bar\\';',\n        tests: 'bruno.test(\\'200 ok\\', () => {});'\n      }\n    };\n\n    const expectedBruFile = fs.readFileSync(path.join(__dirname, 'fixtures', 'request.bru'), 'utf8');\n    const actualBruFile = jsonToBru(request);\n\n    expect(expectedBruFile).toEqual(actualBruFile);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v1/tests/key-val-lines.spec.js",
    "content": "const { between, regex, anyChar, many, choice } = require('arcsecond');\nconst _ = require('lodash');\n\nconst keyValLines = require('../src/key-val-lines');\n\nconst begin = regex(/^vars\\s*\\r?\\n/);\nconst end = regex(/^[\\r?\\n]*\\/vars\\s*[\\r?\\n]*/);\n\nconst varsTag = between(begin)(end)(keyValLines).map(([variables]) => {\n  return {\n    variables\n  };\n});\n\nconst toJson = (fileContents) => {\n  const parser = many(choice([varsTag, anyChar]));\n\n  const parsed = parser.run(fileContents).result.reduce((acc, item) => _.merge(acc, item), {});\n\n  const json = {\n    variables: parsed.variables || []\n  };\n\n  return json;\n};\n\ndescribe('bool-key-val', () => {\n  it('should parse bool-key-val - case 1', () => {\n    const file = `\nvars\n  1 host https://www.google.com\n/vars\n`;\n\n    const result = toJson(file);\n    expect(result).toEqual({\n      variables: [\n        {\n          enabled: true,\n          name: 'host',\n          value: 'https://www.google.com'\n        }\n      ]\n    });\n  });\n\n  it('should parse bool-key-val - case 2', () => {\n    const file = `\nvars\n  1 host https://www.google.com\n  1 auth jwt secret\n/vars\n`;\n\n    const result = toJson(file);\n    expect(result).toEqual({\n      variables: [\n        {\n          enabled: true,\n          name: 'host',\n          value: 'https://www.google.com'\n        },\n        {\n          enabled: true,\n          name: 'auth',\n          value: 'jwt secret'\n        }\n      ]\n    });\n  });\n\n  // following test cases are for edge cases\n\n  // one line with just enabled flag\n  it('should parse bool-key-val - case 3', () => {\n    const file = `\nvars\n  1\n/vars\n`;\n    const result = toJson(file);\n    expect(result).toEqual({\n      variables: [\n        {\n          enabled: true,\n          name: '',\n          value: ''\n        }\n      ]\n    });\n  });\n\n  // one line with just enabled flag and a space\n  it('should parse bool-key-val - case 4', () => {\n    const file = `\nvars\n  1 \n/vars\n`;\n    const result = toJson(file);\n    expect(result).toEqual({\n      variables: [\n        {\n          enabled: true,\n          name: '',\n          value: ''\n        }\n      ]\n    });\n  });\n\n  // one line with just enabled flag and a space and a name\n  it('should parse bool-key-val - case 5', () => {\n    const file = `\nvars\n  1 host\n/vars\n`;\n    const result = toJson(file);\n    expect(result).toEqual({\n      variables: [\n        {\n          enabled: true,\n          name: 'host',\n          value: ''\n        }\n      ]\n    });\n  });\n\n  // one line with just enabled flag and a space and a name and a space\n  it('should parse bool-key-val - case 6', () => {\n    const file = `\nvars\n  1 host \n/vars\n`;\n    const result = toJson(file);\n    expect(result).toEqual({\n      variables: [\n        {\n          enabled: true,\n          name: 'host',\n          value: ''\n        }\n      ]\n    });\n  });\n\n  // three lines, second line with just enabled flag\n  it('should parse bool-key-val - case 7', () => {\n    const file = `\nvars\n  1 host https://www.google.com\n  1\n  0 Content-type application/json\n/vars\n`;\n    const result = toJson(file);\n    expect(result).toEqual({\n      variables: [\n        {\n          enabled: true,\n          name: 'host',\n          value: 'https://www.google.com'\n        },\n        {\n          enabled: true,\n          name: '',\n          value: ''\n        },\n        {\n          enabled: false,\n          name: 'Content-type',\n          value: 'application/json'\n        }\n      ]\n    });\n  });\n\n  // three lines, second line with just enabled flag and a space\n  it('should parse bool-key-val - case 8', () => {\n    const file = `\nvars\n  1 host https://www.google.com\n  1 \n  0 Content-type application/json\n/vars\n`;\n    const result = toJson(file);\n    expect(result).toEqual({\n      variables: [\n        {\n          enabled: true,\n          name: 'host',\n          value: 'https://www.google.com'\n        },\n        {\n          enabled: true,\n          name: '',\n          value: ''\n        },\n        {\n          enabled: false,\n          name: 'Content-type',\n          value: 'application/json'\n        }\n      ]\n    });\n  });\n\n  // three lines, second line with just enabled flag and a space and a name\n  it('should parse bool-key-val - case 9', () => {\n    const file = `\nvars\n  1 host https://www.google.com\n  1 auth\n  0 Content-type application/json\n/vars\n`;\n    const result = toJson(file);\n    expect(result).toEqual({\n      variables: [\n        {\n          enabled: true,\n          name: 'host',\n          value: 'https://www.google.com'\n        },\n        {\n          enabled: true,\n          name: 'auth',\n          value: ''\n        },\n        {\n          enabled: false,\n          name: 'Content-type',\n          value: 'application/json'\n        }\n      ]\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v1/tests/script-tag.spec.js",
    "content": "const scriptTag = require('../src/script-tag');\n\ndescribe('scriptTag', () => {\n  // simple case\n  it('should parse script contents - 1', () => {\n    const input = 'script\\n  const foo = \"bar\";\\n/script';\n    const result = scriptTag.run(input);\n    expect(result.isError).toBe(false);\n    expect(result.result.script).toEqual('  const foo = \"bar\";');\n  });\n\n  // simple case with extra spaces\n  it('should parse script contents - 2', () => {\n    const input = 'script  \\n  const foo = \"bar\";\\n/script';\n    const result = scriptTag.run(input);\n    expect(result.isError).toBe(false);\n    expect(result.result.script).toEqual('  const foo = \"bar\";');\n  });\n\n  // simple case with extra spaces\n  it('should parse script contents - 3', () => {\n    const input = 'script  \\n  const foo = \"bar\";\\n/script  ';\n    const result = scriptTag.run(input);\n    expect(result.isError).toBe(false);\n    expect(result.result.script).toEqual('  const foo = \"bar\";');\n  });\n\n  // simple case with extra spaces\n  it('should parse script contents - 4', () => {\n    const input = 'script  \\n  const foo = \"bar\";\\n/script  \\n';\n    const result = scriptTag.run(input);\n    expect(result.isError).toBe(false);\n    expect(result.result.script).toEqual('  const foo = \"bar\";');\n  });\n\n  // simple case with extra spaces\n  it('should parse script contents - 5', () => {\n    const input = 'script  \\n  const foo = \"bar\";\\n/script  \\n  ';\n    const result = scriptTag.run(input);\n    expect(result.isError).toBe(false);\n    expect(result.result.script).toEqual('  const foo = \"bar\";');\n  });\n\n  // simple case with extra spaces\n  it('should parse script contents - 6', () => {\n    const input = 'script  \\n  const foo = \"bar\";\\n/script  \\n  \\n';\n    const result = scriptTag.run(input);\n    expect(result.isError).toBe(false);\n    expect(result.result.script).toEqual('  const foo = \"bar\";');\n  });\n\n  // error case - missing script start tag\n  it('should fail to parse when script start tag is missing', () => {\n    const input = '  const foo = \"bar\";\\n/script';\n    const result = scriptTag.run(input);\n    expect(result.isError).toBe(true);\n  });\n\n  // error case - missing script end tag\n  it('should fail to parse when script end tag is missing', () => {\n    const input = 'script\\n  const foo = \"bar\";';\n    const result = scriptTag.run(input);\n    expect(result.isError).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v1/tests/tests-tag.spec.js",
    "content": "const testsTag = require('../src/tests-tag');\n\ndescribe('testsTag', () => {\n  // simple case\n  it('should parse tests contents - 1', () => {\n    const input = 'tests\\n  bruno.test(\"200 ok\", () => {});\\n/tests';\n    const result = testsTag.run(input);\n    expect(result.isError).toBe(false);\n    expect(result.result.tests).toEqual('  bruno.test(\"200 ok\", () => {});');\n  });\n\n  // simple case with extra spaces\n  it('should parse tests contents - 2', () => {\n    const input = 'tests  \\n  bruno.test(\"200 ok\", () => {});\\n/tests';\n    const result = testsTag.run(input);\n    expect(result.isError).toBe(false);\n    expect(result.result.tests).toEqual('  bruno.test(\"200 ok\", () => {});');\n  });\n\n  // simple case with extra spaces\n  it('should parse tests contents - 3', () => {\n    const input = 'tests  \\n  bruno.test(\"200 ok\", () => {});\\n/tests  ';\n    const result = testsTag.run(input);\n    expect(result.isError).toBe(false);\n    expect(result.result.tests).toEqual('  bruno.test(\"200 ok\", () => {});');\n  });\n\n  // simple case with extra spaces\n  it('should parse tests contents - 4', () => {\n    const input = 'tests  \\n  bruno.test(\"200 ok\", () => {});\\n/tests  \\n';\n    const result = testsTag.run(input);\n    expect(result.isError).toBe(false);\n    expect(result.result.tests).toEqual('  bruno.test(\"200 ok\", () => {});');\n  });\n\n  // simple case with extra spaces\n  it('should parse tests contents - 5', () => {\n    const input = 'tests  \\n  bruno.test(\"200 ok\", () => {});\\n/tests  \\n  ';\n    const result = testsTag.run(input);\n    expect(result.isError).toBe(false);\n    expect(result.result.tests).toEqual('  bruno.test(\"200 ok\", () => {});');\n  });\n\n  // simple case with extra spaces\n  it('should parse tests contents - 6', () => {\n    const input = 'tests  \\n  bruno.test(\"200 ok\", () => {});\\n/tests  \\n  \\n';\n    const result = testsTag.run(input);\n    expect(result.isError).toBe(false);\n    expect(result.result.tests).toEqual('  bruno.test(\"200 ok\", () => {});');\n  });\n\n  // error case - missing tests start tag\n  it('should fail to parse when tests start tag is missing', () => {\n    const input = '  bruno.test(\"200 ok\", () => {});\\n/tests';\n    const result = testsTag.run(input);\n    expect(result.isError).toBe(true);\n  });\n\n  // error case - missing tests end tag\n  it('should fail to parse when tests end tag is missing', () => {\n    const input = 'tests\\n  bruno.test(\"200 ok\", () => {});';\n    const result = testsTag.run(input);\n    expect(result.isError).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v1/tests/utils.spec.js",
    "content": "const { safeParseJson, indentString, outdentString, get } = require('../src/utils');\n\ndescribe('utils', () => {\n  describe('safeParseJson', () => {\n    it('should parse valid json', () => {\n      const input = '{\"a\": 1}';\n      const result = safeParseJson(input);\n      expect(result).toEqual({ a: 1 });\n    });\n\n    it('should return null for invalid json', () => {\n      const input = '{\"a\": 1';\n      const result = safeParseJson(input);\n      expect(result).toBeNull();\n    });\n  });\n\n  describe('indentString', () => {\n    it('correctly indents a multiline string', () => {\n      const input = 'line1\\nline2\\nline3';\n      const expectedOutput = '  line1\\n  line2\\n  line3';\n      expect(indentString(input)).toBe(expectedOutput);\n    });\n  });\n\n  describe('outdentString', () => {\n    it('correctly outdents a multiline string', () => {\n      const input = '  line1\\n  line2\\n  line3';\n      const expectedOutput = 'line1\\nline2\\nline3';\n      expect(outdentString(input)).toBe(expectedOutput);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v2/src/bruToJson.js",
    "content": "const ohm = require('ohm-js');\nconst _ = require('lodash');\nconst { safeParseJson, outdentString } = require('./utils');\nconst parseExample = require('./example/bruToJson');\n\n/**\n * A Bru file is made up of blocks.\n * There are three types of blocks\n *\n * 1. Dictionary Blocks - These are blocks that have key value pairs\n * ex:\n *  headers {\n *   content-type: application/json\n *  }\n *\n * 2. Text Blocks - These are blocks that have text\n * ex:\n * body:json {\n *  {\n *   \"username\": \"John Nash\",\n *   \"password\": \"governingdynamics\n *  }\n\n * 3. List Blocks - These are blocks that have a list of items\n * ex:\n *  tags [\n *   regression\n *   smoke-test\n *  ]\n *\n */\nconst grammar = ohm.grammar(`Bru {\n  BruFile = (meta | http | grpc | ws | query | params | headers | metadata | auths | bodies | varsandassert | script | tests | settings | docs | example)*\n  auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth2 | authwsse | authapikey | authOauth2Configs\n  bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body | bodygrpc | bodyws\n  bodyforms = bodyformurlencoded | bodymultipart | bodyfile\n  params = paramspath | paramsquery\n  \n  // Oauth2 additional parameters\n  authOauth2Configs = oauth2AuthReqConfig | oauth2AccessTokenReqConfig | oauth2RefreshTokenReqConfig\n  oauth2AuthReqConfig = oauth2AuthReqHeaders | oauth2AuthReqQueryParams \n  oauth2AccessTokenReqConfig = oauth2AccessTokenReqHeaders | oauth2AccessTokenReqQueryParams | oauth2AccessTokenReqBody\n  oauth2RefreshTokenReqConfig = oauth2RefreshTokenReqHeaders | oauth2RefreshTokenReqQueryParams | oauth2RefreshTokenReqBody\n \n  nl = \"\\\\r\"? \"\\\\n\"\n  st = \" \" | \"\\\\t\"\n  stnl = st | nl\n  tagend = nl \"}\"\n  optionalnl = ~tagend nl\n  keychar = ~(tagend | st | nl | \":\") any\n  valuechar = ~(nl | tagend) any\n\n   // Multiline text block surrounded by '''\n  multilinetextblockdelimiter = \"'''\"\n  multilinetextblock = multilinetextblockdelimiter (~multilinetextblockdelimiter any)* multilinetextblockdelimiter st* contenttypeannotation?\n  contenttypeannotation = \"@contentType(\" (~\")\" any)* \")\"\n\n  // Dictionary Blocks\n  dictionary = st* \"{\" pairlist? tagend\n  pairlist = optionalnl* pair (~tagend stnl* pair)* (~tagend space)*\n  pair = st* (quoted_key | key) st* \":\" st* value st*\n  disable_char = \"~\"\n  quote_char = \"\\\\\"\"\n  esc_char = \"\\\\\\\\\"\n  esc_quote_char = esc_char quote_char\n  quoted_key_char = ~(quote_char | esc_quote_char | nl) any\n  quoted_key = disable_char? quote_char (esc_quote_char | quoted_key_char)* quote_char\n  key = keychar*\n  value = list | multilinetextblock | singlelinevalue\n  singlelinevalue = valuechar*\n\n  // Dictionary for Assert Block\n  assertdictionary = st* \"{\" assertpairlist? tagend\n  assertpairlist = optionalnl* assertpair (~tagend stnl* assertpair)* (~tagend space)*\n  assertpair = st* assertkey st* \":\" st* value st*\n  assertkey = ~tagend assertkeychar*\n  assertkeychar = ~(tagend | nl | \":\") any\n\n  // Text Blocks\n  textblock = textline (~tagend nl textline)*\n  textline = textchar*\n  textchar = ~nl any\n\n  // List\n  list = st* \"[\" nl+ listitems? st* nl+ st* \"]\"\n  listitems = listitem (nl+ listitem)*\n  listitem = st+ (alnum | \"_\" | \"-\")+ st*\n\n  meta = \"meta\" dictionary\n  settings = \"settings\" dictionary\n\n  http = get | post | put | delete | patch | options | head | connect | trace | httpcustom\n  grpc = \"grpc\" dictionary\n  ws = \"ws\" dictionary\n  get = \"get\" dictionary\n  post = \"post\" dictionary\n  put = \"put\" dictionary\n  delete = \"delete\" dictionary\n  patch = \"patch\" dictionary\n  options = \"options\" dictionary\n  head = \"head\" dictionary\n  connect = \"connect\" dictionary\n  trace = \"trace\" dictionary\n  httpcustom = \"http\" dictionary\n\n\n  headers = \"headers\" dictionary\n  metadata = \"metadata\" dictionary\n\n  query = \"query\" dictionary\n  paramspath = \"params:path\" dictionary\n  paramsquery = \"params:query\" dictionary\n\n  varsandassert = varsreq | varsres | assert\n  varsreq = \"vars:pre-request\" dictionary\n  varsres = \"vars:post-response\" dictionary\n  assert = \"assert\" assertdictionary\n\n  authawsv4 = \"auth:awsv4\" dictionary\n  authbasic = \"auth:basic\" dictionary\n  authbearer = \"auth:bearer\" dictionary\n  authdigest = \"auth:digest\" dictionary\n  authNTLM = \"auth:ntlm\" dictionary\n  authOAuth2 = \"auth:oauth2\" dictionary\n  authwsse = \"auth:wsse\" dictionary\n  authapikey = \"auth:apikey\" dictionary\n\n  oauth2AuthReqHeaders = \"auth:oauth2:additional_params:auth_req:headers\" dictionary\n  oauth2AuthReqQueryParams = \"auth:oauth2:additional_params:auth_req:queryparams\" dictionary\n  oauth2AccessTokenReqHeaders = \"auth:oauth2:additional_params:access_token_req:headers\" dictionary\n  oauth2AccessTokenReqQueryParams = \"auth:oauth2:additional_params:access_token_req:queryparams\" dictionary\n  oauth2AccessTokenReqBody = \"auth:oauth2:additional_params:access_token_req:body\" dictionary\n  oauth2RefreshTokenReqHeaders = \"auth:oauth2:additional_params:refresh_token_req:headers\" dictionary\n  oauth2RefreshTokenReqQueryParams = \"auth:oauth2:additional_params:refresh_token_req:queryparams\" dictionary\n  oauth2RefreshTokenReqBody = \"auth:oauth2:additional_params:refresh_token_req:body\" dictionary\n\n  body = \"body\" st* \"{\" nl* textblock tagend\n  bodyjson = \"body:json\" st* \"{\" nl* textblock tagend\n  bodytext = \"body:text\" st* \"{\" nl* textblock tagend\n  bodyxml = \"body:xml\" st* \"{\" nl* textblock tagend\n  bodysparql = \"body:sparql\" st* \"{\" nl* textblock tagend\n  bodygraphql = \"body:graphql\" st* \"{\" nl* textblock tagend\n  bodygraphqlvars = \"body:graphql:vars\" st* \"{\" nl* textblock tagend\n  bodygrpc = \"body:grpc\" dictionary\n  bodyws = \"body:ws\" dictionary\n\n  bodyformurlencoded = \"body:form-urlencoded\" dictionary\n  bodymultipart = \"body:multipart-form\" dictionary\n  bodyfile = \"body:file\" dictionary\n\n\n  // Examples - multiple example blocks\n  example = \"example\" st* \"{\" nl* examplecontent tagend\n  examplecontent = (~tagend any)*\n  \n  script = scriptreq | scriptres\n  scriptreq = \"script:pre-request\" st* \"{\" nl* textblock tagend\n  scriptres = \"script:post-response\" st* \"{\" nl* textblock tagend\n  tests = \"tests\" st* \"{\" nl* textblock tagend\n  docs = \"docs\" st* \"{\" nl* textblock tagend\n}`);\n\nconst mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => {\n  if (!pairList.length) {\n    return [];\n  }\n  return _.map(pairList[0], (pair) => {\n    let name = _.keys(pair)[0];\n    let value = pair[name];\n\n    if (!parseEnabled) {\n      return {\n        name,\n        value\n      };\n    }\n\n    let enabled = true;\n    if (name && name.length && name.charAt(0) === '~') {\n      name = name.slice(1);\n      enabled = false;\n    }\n\n    return {\n      name,\n      value,\n      enabled\n    };\n  });\n};\n\nconst mapRequestParams = (pairList = [], type) => {\n  if (!pairList.length) {\n    return [];\n  }\n  return _.map(pairList[0], (pair) => {\n    let name = _.keys(pair)[0];\n    let value = pair[name];\n    let enabled = true;\n    if (name && name.length && name.charAt(0) === '~') {\n      name = name.slice(1);\n      enabled = false;\n    }\n\n    return {\n      name,\n      value,\n      enabled,\n      type\n    };\n  });\n};\n\nconst multipartExtractContentType = (pair) => {\n  if (_.isString(pair.value)) {\n    const match = pair.value.match(/^(.*?)\\s*@contentType\\((.*?)\\)\\s*$/s);\n    if (match != null && match.length > 2) {\n      pair.value = match[1];\n      pair.contentType = match[2];\n    } else {\n      pair.contentType = '';\n    }\n  }\n};\n\nconst fileExtractContentType = (pair) => {\n  if (_.isString(pair.value)) {\n    const match = pair.value.match(/^(.*?)\\s*@contentType\\((.*?)\\)\\s*$/s);\n    if (match && match.length > 2) {\n      pair.value = match[1].trim();\n      pair.contentType = match[2].trim();\n    } else {\n      pair.contentType = '';\n    }\n  }\n};\n\nconst mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) => {\n  const pairs = mapPairListToKeyValPairs(pairList, parseEnabled);\n\n  return pairs.map((pair) => {\n    pair.type = 'text';\n    multipartExtractContentType(pair);\n\n    if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) {\n      let filestr = pair.value.replace(/^@file\\(/, '').replace(/\\)$/, '');\n      pair.type = 'file';\n      pair.value = filestr.split('|');\n    }\n\n    return pair;\n  });\n};\n\nconst mapPairListToKeyValPairsFile = (pairList = [], parseEnabled = true) => {\n  const pairs = mapPairListToKeyValPairs(pairList, parseEnabled);\n  return pairs.map((pair) => {\n    fileExtractContentType(pair);\n\n    if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) {\n      let filePath = pair.value.replace(/^@file\\(/, '').replace(/\\)$/, '');\n      pair.filePath = filePath;\n      pair.selected = pair.enabled;\n\n      // Remove pair.value as it only contains the file path reference\n      delete pair.value;\n      // Remove pair.name as it is auto-generated (e.g., file1, file2, file3, etc.)\n      delete pair.name;\n      delete pair.enabled;\n    }\n\n    return pair;\n  });\n};\n\nconst concatArrays = (objValue, srcValue) => {\n  if (_.isArray(objValue) && _.isArray(srcValue)) {\n    return objValue.concat(srcValue);\n  }\n};\n\nconst mapPairListToKeyValPair = (pairList = []) => {\n  if (!pairList || !pairList.length) {\n    return {};\n  }\n\n  return _.merge({}, ...pairList[0]);\n};\n\n/**\n * @param {Record<unknown,unknown>} obj\n * @returns {(key:string, opts?:{fallback: number })=>number|undefined}\n */\nconst createGetNumFromRecord = (obj) => (key, { fallback } = {}) => {\n  if (!(key in obj)) return fallback;\n  const asNumber = typeof obj[key] === 'number' ? obj[key] : Number(obj[key]);\n  if (isNaN(asNumber)) {\n    return fallback;\n  }\n  return asNumber;\n};\n\n// Parse example content using dedicated example parser\nconst parseExampleContent = (content) => {\n  try {\n    // Unindent the content by removing leading whitespace from each line\n    const lines = content.split('\\n');\n\n    // Find the minimum indentation (excluding empty lines)\n    let minIndent = Infinity;\n    lines.forEach((line) => {\n      if (line.trim() !== '') {\n        const indent = line.match(/^[ \\t]*/)[0].length;\n        minIndent = Math.min(minIndent, indent);\n      }\n    });\n\n    // Remove the minimum indentation from all lines\n    const unindentedLines = lines.map((line) => {\n      if (line.trim() === '') return line; // Keep empty lines as is\n      return line.substring(minIndent);\n    });\n\n    const unindentedContent = unindentedLines.join('\\n').trim();\n\n    // Parse the unindented content using the dedicated example parser\n    return parseExample(unindentedContent);\n  } catch (error) {\n    console.error('Error parsing example content:', error);\n    return { error: error.message };\n  }\n};\n\nconst sem = grammar.createSemantics().addAttribute('ast', {\n  BruFile(tags) {\n    if (!tags || !tags.ast || !tags.ast.length) {\n      return {};\n    }\n\n    return _.reduce(\n      tags.ast,\n      (result, item) => {\n        return _.mergeWith(result, item, concatArrays);\n      },\n      {}\n    );\n  },\n  dictionary(_1, _2, pairlist, _3) {\n    return pairlist.ast;\n  },\n  pairlist(_1, pair, _2, rest, _3) {\n    return [pair.ast, ...rest.ast];\n  },\n  pair(_1, key, _2, _3, _4, value, _5) {\n    let res = {};\n    if (Array.isArray(value.ast)) {\n      res[key.ast] = value.ast;\n      return res;\n    }\n    res[key.ast] = value.ast ? value.ast.trim() : '';\n    return res;\n  },\n  esc_quote_char(_1, quote) {\n    // unescape\n    return quote.sourceString;\n  },\n  quoted_key(disabled, _1, chars, _2) {\n    // unquote\n    return (disabled ? disabled.sourceString : '') + chars.ast.join('');\n  },\n  key(chars) {\n    return chars.sourceString ? chars.sourceString.trim() : '';\n  },\n  assertdictionary(_1, _2, pairlist, _3) {\n    return pairlist.ast;\n  },\n  assertpairlist(_1, pair, _2, rest, _3) {\n    return [pair.ast, ...rest.ast];\n  },\n  assertpair(_1, key, _2, _3, _4, value, _5) {\n    let res = {};\n    res[key.ast] = value.ast ? value.ast.trim() : '';\n    return res;\n  },\n  assertkey(chars) {\n    return chars.sourceString ? chars.sourceString.trim() : '';\n  },\n  list(_1, _2, _3, listitems, _4, _5, _6, _7) {\n    return listitems.ast.flat();\n  },\n  listitems(listitem, _1, rest) {\n    return [listitem.ast, ...rest.ast];\n  },\n  listitem(_1, textchar, _2) {\n    return textchar.sourceString;\n  },\n  textblock(line, _1, rest) {\n    return [line.ast, ...rest.ast].join('\\n');\n  },\n  textline(chars) {\n    return chars.sourceString;\n  },\n  textchar(char) {\n    return char.sourceString;\n  },\n  nl(_1, _2) {\n    return '';\n  },\n  st(_) {\n    return '';\n  },\n  tagend(_1, _2) {\n    return '';\n  },\n  _terminal() {\n    return this.sourceString;\n  },\n  multilinetextblockdelimiter(_) {\n    return '';\n  },\n  multilinetextblock(_1, content, _2, _3, contentType) {\n    const multilineString = content.sourceString\n      .split('\\n')\n      .map((line) => line.slice(4))\n      .join('\\n');\n\n    if (!contentType.sourceString) {\n      return multilineString;\n    }\n    return `${multilineString} ${contentType.sourceString}`;\n  },\n  singlelinevalue(chars) {\n    return chars.sourceString?.trim() || '';\n  },\n  _iter(...elements) {\n    return elements.map((e) => e.ast);\n  },\n  meta(_1, dictionary) {\n    let meta = mapPairListToKeyValPair(dictionary.ast);\n\n    if (!meta.seq) {\n      meta.seq = 1;\n    }\n\n    if (!meta.type) {\n      meta.type = 'http';\n    }\n\n    return {\n      meta\n    };\n  },\n  settings(_1, dictionary) {\n    let settings = mapPairListToKeyValPair(dictionary.ast);\n    const getNumFromRecord = createGetNumFromRecord(settings);\n\n    const keepAliveInterval = getNumFromRecord('keepAliveInterval');\n\n    const parsedSettings = {};\n    if (settings.followRedirects !== undefined) {\n      parsedSettings.followRedirects = typeof settings.followRedirects === 'boolean' ? settings.followRedirects : settings.followRedirects === 'true';\n    }\n\n    // Parse maxRedirects as number\n    if (settings.maxRedirects !== undefined) {\n      const maxRedirects = parseInt(settings.maxRedirects, 10);\n      if (!isNaN(maxRedirects)) {\n        parsedSettings.maxRedirects = maxRedirects;\n      }\n    }\n\n    // Parse timeout as number or inherit\n    if (settings.timeout !== undefined) {\n      if (settings.timeout === 'inherit') {\n        parsedSettings.timeout = 'inherit';\n      } else {\n        const timeout = parseInt(settings.timeout, 10);\n        if (!isNaN(timeout)) {\n          parsedSettings.timeout = timeout;\n        }\n      }\n    }\n\n    const _settings = {\n      encodeUrl: typeof settings.encodeUrl === 'boolean' ? settings.encodeUrl : settings.encodeUrl === 'true',\n      timeout: parsedSettings.timeout !== undefined ? parsedSettings.timeout : 0\n    };\n\n    if (parsedSettings.followRedirects !== undefined) {\n      _settings.followRedirects = parsedSettings.followRedirects;\n    }\n\n    if (parsedSettings.maxRedirects !== undefined) {\n      _settings.maxRedirects = parsedSettings.maxRedirects;\n    }\n\n    if (keepAliveInterval) {\n      _settings.keepAliveInterval = keepAliveInterval;\n    }\n\n    return {\n      settings: _settings\n    };\n  },\n  grpc(_1, dictionary) {\n    return {\n      grpc: mapPairListToKeyValPair(dictionary.ast)\n    };\n  },\n  ws(_1, dictionary) {\n    return {\n      ws: mapPairListToKeyValPair(dictionary.ast)\n    };\n  },\n  get(_1, dictionary) {\n    return {\n      http: {\n        method: 'get',\n        ...mapPairListToKeyValPair(dictionary.ast)\n      }\n    };\n  },\n  post(_1, dictionary) {\n    return {\n      http: {\n        method: 'post',\n        ...mapPairListToKeyValPair(dictionary.ast)\n      }\n    };\n  },\n  put(_1, dictionary) {\n    return {\n      http: {\n        method: 'put',\n        ...mapPairListToKeyValPair(dictionary.ast)\n      }\n    };\n  },\n  delete(_1, dictionary) {\n    return {\n      http: {\n        method: 'delete',\n        ...mapPairListToKeyValPair(dictionary.ast)\n      }\n    };\n  },\n  patch(_1, dictionary) {\n    return {\n      http: {\n        method: 'patch',\n        ...mapPairListToKeyValPair(dictionary.ast)\n      }\n    };\n  },\n  options(_1, dictionary) {\n    return {\n      http: {\n        method: 'options',\n        ...mapPairListToKeyValPair(dictionary.ast)\n      }\n    };\n  },\n  head(_1, dictionary) {\n    return {\n      http: {\n        method: 'head',\n        ...mapPairListToKeyValPair(dictionary.ast)\n      }\n    };\n  },\n  connect(_1, dictionary) {\n    return {\n      http: {\n        method: 'connect',\n        ...mapPairListToKeyValPair(dictionary.ast)\n      }\n    };\n  },\n  trace(_1, dictionary) {\n    return {\n      http: {\n        method: 'trace',\n        ...mapPairListToKeyValPair(dictionary.ast)\n      }\n    };\n  },\n  httpcustom(_1, dictionary) {\n    const dict = mapPairListToKeyValPair(dictionary.ast);\n    const method = dict.method;\n    const rest = { ...dict };\n    delete rest.method;\n    return {\n      http: {\n        method,\n        ...rest\n      }\n    };\n  },\n  query(_1, dictionary) {\n    return {\n      params: mapRequestParams(dictionary.ast, 'query')\n    };\n  },\n  paramspath(_1, dictionary) {\n    return {\n      params: mapRequestParams(dictionary.ast, 'path')\n    };\n  },\n  paramsquery(_1, dictionary) {\n    return {\n      params: mapRequestParams(dictionary.ast, 'query')\n    };\n  },\n  headers(_1, dictionary) {\n    return {\n      headers: mapPairListToKeyValPairs(dictionary.ast)\n    };\n  },\n  metadata(_1, dictionary) {\n    return {\n      metadata: mapPairListToKeyValPairs(dictionary.ast)\n    };\n  },\n  authawsv4(_1, dictionary) {\n    const auth = mapPairListToKeyValPairs(dictionary.ast, false);\n    const accessKeyIdKey = _.find(auth, { name: 'accessKeyId' });\n    const secretAccessKeyKey = _.find(auth, { name: 'secretAccessKey' });\n    const sessionTokenKey = _.find(auth, { name: 'sessionToken' });\n    const serviceKey = _.find(auth, { name: 'service' });\n    const regionKey = _.find(auth, { name: 'region' });\n    const profileNameKey = _.find(auth, { name: 'profileName' });\n    const accessKeyId = accessKeyIdKey ? accessKeyIdKey.value : '';\n    const secretAccessKey = secretAccessKeyKey ? secretAccessKeyKey.value : '';\n    const sessionToken = sessionTokenKey ? sessionTokenKey.value : '';\n    const service = serviceKey ? serviceKey.value : '';\n    const region = regionKey ? regionKey.value : '';\n    const profileName = profileNameKey ? profileNameKey.value : '';\n    return {\n      auth: {\n        awsv4: {\n          accessKeyId,\n          secretAccessKey,\n          sessionToken,\n          service,\n          region,\n          profileName\n        }\n      }\n    };\n  },\n  authbasic(_1, dictionary) {\n    const auth = mapPairListToKeyValPairs(dictionary.ast, false);\n    const usernameKey = _.find(auth, { name: 'username' });\n    const passwordKey = _.find(auth, { name: 'password' });\n    const username = usernameKey ? usernameKey.value : '';\n    const password = passwordKey ? passwordKey.value : '';\n    return {\n      auth: {\n        basic: {\n          username,\n          password\n        }\n      }\n    };\n  },\n  authbearer(_1, dictionary) {\n    const auth = mapPairListToKeyValPairs(dictionary.ast, false);\n    const tokenKey = _.find(auth, { name: 'token' });\n    const token = tokenKey ? tokenKey.value : '';\n    return {\n      auth: {\n        bearer: {\n          token\n        }\n      }\n    };\n  },\n  authdigest(_1, dictionary) {\n    const auth = mapPairListToKeyValPairs(dictionary.ast, false);\n    const usernameKey = _.find(auth, { name: 'username' });\n    const passwordKey = _.find(auth, { name: 'password' });\n    const username = usernameKey ? usernameKey.value : '';\n    const password = passwordKey ? passwordKey.value : '';\n    return {\n      auth: {\n        digest: {\n          username,\n          password\n        }\n      }\n    };\n  },\n  authNTLM(_1, dictionary) {\n    const auth = mapPairListToKeyValPairs(dictionary.ast, false);\n    const usernameKey = _.find(auth, { name: 'username' });\n    const passwordKey = _.find(auth, { name: 'password' });\n    const domainKey = _.find(auth, { name: 'domain' });\n\n    const username = usernameKey ? usernameKey.value : '';\n    const password = passwordKey ? passwordKey.value : '';\n    const domain = passwordKey ? domainKey.value : '';\n\n    return {\n      auth: {\n        ntlm: {\n          username,\n          password,\n          domain\n        }\n      }\n    };\n  },\n  authOAuth2(_1, dictionary) {\n    const auth = mapPairListToKeyValPairs(dictionary.ast, false);\n    const grantTypeKey = _.find(auth, { name: 'grant_type' });\n    const usernameKey = _.find(auth, { name: 'username' });\n    const passwordKey = _.find(auth, { name: 'password' });\n    const callbackUrlKey = _.find(auth, { name: 'callback_url' });\n    const authorizationUrlKey = _.find(auth, { name: 'authorization_url' });\n    const accessTokenUrlKey = _.find(auth, { name: 'access_token_url' });\n    const refreshTokenUrlKey = _.find(auth, { name: 'refresh_token_url' });\n    const clientIdKey = _.find(auth, { name: 'client_id' });\n    const clientSecretKey = _.find(auth, { name: 'client_secret' });\n    const scopeKey = _.find(auth, { name: 'scope' });\n    const stateKey = _.find(auth, { name: 'state' });\n    const pkceKey = _.find(auth, { name: 'pkce' });\n    const credentialsPlacementKey = _.find(auth, { name: 'credentials_placement' });\n    const credentialsIdKey = _.find(auth, { name: 'credentials_id' });\n    const tokenPlacementKey = _.find(auth, { name: 'token_placement' });\n    const tokenHeaderPrefixKey = _.find(auth, { name: 'token_header_prefix' });\n    const tokenQueryKeyKey = _.find(auth, { name: 'token_query_key' });\n    const autoFetchTokenKey = _.find(auth, { name: 'auto_fetch_token' });\n    const autoRefreshTokenKey = _.find(auth, { name: 'auto_refresh_token' });\n    const tokenSourceKey = _.find(auth, { name: 'token_source' });\n    return {\n      auth: {\n        oauth2:\n          grantTypeKey?.value && grantTypeKey?.value == 'password'\n            ? {\n                grantType: grantTypeKey ? grantTypeKey.value : '',\n                accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',\n                refreshTokenUrl: refreshTokenUrlKey ? refreshTokenUrlKey.value : '',\n                username: usernameKey ? usernameKey.value : '',\n                password: passwordKey ? passwordKey.value : '',\n                clientId: clientIdKey ? clientIdKey.value : '',\n                clientSecret: clientSecretKey ? clientSecretKey.value : '',\n                scope: scopeKey ? scopeKey.value : '',\n                credentialsPlacement: credentialsPlacementKey?.value ? credentialsPlacementKey.value : 'body',\n                credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials',\n                tokenSource: tokenSourceKey?.value ? tokenSourceKey.value : 'access_token',\n                tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',\n                tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : '',\n                tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',\n                autoFetchToken: autoFetchTokenKey ? safeParseJson(autoFetchTokenKey?.value) ?? true : true,\n                autoRefreshToken: autoRefreshTokenKey ? safeParseJson(autoRefreshTokenKey?.value) ?? false : false\n              }\n            : grantTypeKey?.value && grantTypeKey?.value == 'authorization_code'\n              ? {\n                  grantType: grantTypeKey ? grantTypeKey.value : '',\n                  callbackUrl: callbackUrlKey ? callbackUrlKey.value : '',\n                  authorizationUrl: authorizationUrlKey ? authorizationUrlKey.value : '',\n                  accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',\n                  refreshTokenUrl: refreshTokenUrlKey ? refreshTokenUrlKey.value : '',\n                  clientId: clientIdKey ? clientIdKey.value : '',\n                  clientSecret: clientSecretKey ? clientSecretKey.value : '',\n                  scope: scopeKey ? scopeKey.value : '',\n                  state: stateKey ? stateKey.value : '',\n                  pkce: pkceKey ? safeParseJson(pkceKey?.value) ?? false : false,\n                  credentialsPlacement: credentialsPlacementKey?.value ? credentialsPlacementKey.value : 'body',\n                  credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials',\n                  tokenSource: tokenSourceKey?.value ? tokenSourceKey.value : 'access_token',\n                  tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',\n                  tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : '',\n                  tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',\n                  autoFetchToken: autoFetchTokenKey ? safeParseJson(autoFetchTokenKey?.value) ?? true : true,\n                  autoRefreshToken: autoRefreshTokenKey ? safeParseJson(autoRefreshTokenKey?.value) ?? false : false\n                }\n              : grantTypeKey?.value && grantTypeKey?.value == 'client_credentials'\n                ? {\n                    grantType: grantTypeKey ? grantTypeKey.value : '',\n                    accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',\n                    refreshTokenUrl: refreshTokenUrlKey ? refreshTokenUrlKey.value : '',\n                    clientId: clientIdKey ? clientIdKey.value : '',\n                    clientSecret: clientSecretKey ? clientSecretKey.value : '',\n                    scope: scopeKey ? scopeKey.value : '',\n                    credentialsPlacement: credentialsPlacementKey?.value ? credentialsPlacementKey.value : 'body',\n                    credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials',\n                    tokenSource: tokenSourceKey?.value ? tokenSourceKey.value : 'access_token',\n                    tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',\n                    tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : '',\n                    tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',\n                    autoFetchToken: autoFetchTokenKey ? safeParseJson(autoFetchTokenKey?.value) ?? true : true,\n                    autoRefreshToken: autoRefreshTokenKey ? safeParseJson(autoRefreshTokenKey?.value) ?? false : false\n                  }\n                : grantTypeKey?.value && grantTypeKey?.value == 'implicit'\n                  ? {\n                      grantType: grantTypeKey ? grantTypeKey.value : '',\n                      callbackUrl: callbackUrlKey ? callbackUrlKey.value : '',\n                      authorizationUrl: authorizationUrlKey ? authorizationUrlKey.value : '',\n                      clientId: clientIdKey ? clientIdKey.value : '',\n                      scope: scopeKey ? scopeKey.value : '',\n                      state: stateKey ? stateKey.value : '',\n                      credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials',\n                      tokenSource: tokenSourceKey?.value ? tokenSourceKey.value : 'access_token',\n                      tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',\n                      tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : '',\n                      tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',\n                      autoFetchToken: autoFetchTokenKey ? safeParseJson(autoFetchTokenKey?.value) ?? true : true\n                    }\n                  : {}\n      }\n    };\n  },\n  oauth2AuthReqHeaders(_1, dictionary) {\n    return {\n      oauth2_additional_parameters_auth_req_headers: mapPairListToKeyValPairs(dictionary.ast)\n    };\n  },\n  oauth2AuthReqQueryParams(_1, dictionary) {\n    return {\n      oauth2_additional_parameters_auth_req_queryparams: mapPairListToKeyValPairs(dictionary.ast)\n    };\n  },\n  oauth2AccessTokenReqHeaders(_1, dictionary) {\n    return {\n      oauth2_additional_parameters_access_token_req_headers: mapPairListToKeyValPairs(dictionary.ast)\n    };\n  },\n  oauth2AccessTokenReqQueryParams(_1, dictionary) {\n    return {\n      oauth2_additional_parameters_access_token_req_queryparams: mapPairListToKeyValPairs(dictionary.ast)\n    };\n  },\n  oauth2AccessTokenReqBody(_1, dictionary) {\n    return {\n      oauth2_additional_parameters_access_token_req_bodyvalues: mapPairListToKeyValPairs(dictionary.ast)\n    };\n  },\n  oauth2RefreshTokenReqHeaders(_1, dictionary) {\n    return {\n      oauth2_additional_parameters_refresh_token_req_headers: mapPairListToKeyValPairs(dictionary.ast)\n    };\n  },\n  oauth2RefreshTokenReqQueryParams(_1, dictionary) {\n    return {\n      oauth2_additional_parameters_refresh_token_req_queryparams: mapPairListToKeyValPairs(dictionary.ast)\n    };\n  },\n  oauth2RefreshTokenReqBody(_1, dictionary) {\n    return {\n      oauth2_additional_parameters_refresh_token_req_bodyvalues: mapPairListToKeyValPairs(dictionary.ast)\n    };\n  },\n  authwsse(_1, dictionary) {\n    const auth = mapPairListToKeyValPairs(dictionary.ast, false);\n\n    const userKey = _.find(auth, { name: 'username' });\n    const secretKey = _.find(auth, { name: 'password' });\n    const username = userKey ? userKey.value : '';\n    const password = secretKey ? secretKey.value : '';\n\n    return {\n      auth: {\n        wsse: {\n          username,\n          password\n        }\n      }\n    };\n  },\n  authapikey(_1, dictionary) {\n    const auth = mapPairListToKeyValPairs(dictionary.ast, false);\n\n    const findValueByName = (name) => {\n      const item = _.find(auth, { name });\n      return item ? item.value : '';\n    };\n\n    const key = findValueByName('key');\n    const value = findValueByName('value');\n    const placement = findValueByName('placement');\n\n    return {\n      auth: {\n        apikey: {\n          key,\n          value,\n          placement\n        }\n      }\n    };\n  },\n  bodyformurlencoded(_1, dictionary) {\n    return {\n      body: {\n        formUrlEncoded: mapPairListToKeyValPairs(dictionary.ast)\n      }\n    };\n  },\n  bodymultipart(_1, dictionary) {\n    return {\n      body: {\n        multipartForm: mapPairListToKeyValPairsMultipart(dictionary.ast)\n      }\n    };\n  },\n  bodyfile(_1, dictionary) {\n    return {\n      body: {\n        file: mapPairListToKeyValPairsFile(dictionary.ast)\n      }\n    };\n  },\n  body(_1, _2, _3, _4, textblock, _5) {\n    return {\n      http: {\n        body: 'json'\n      },\n      body: {\n        json: outdentString(textblock.sourceString)\n      }\n    };\n  },\n  bodyjson(_1, _2, _3, _4, textblock, _5) {\n    return {\n      body: {\n        json: outdentString(textblock.sourceString)\n      }\n    };\n  },\n  bodytext(_1, _2, _3, _4, textblock, _5) {\n    return {\n      body: {\n        text: outdentString(textblock.sourceString)\n      }\n    };\n  },\n  bodyxml(_1, _2, _3, _4, textblock, _5) {\n    return {\n      body: {\n        xml: outdentString(textblock.sourceString)\n      }\n    };\n  },\n  bodysparql(_1, _2, _3, _4, textblock, _5) {\n    return {\n      body: {\n        sparql: outdentString(textblock.sourceString)\n      }\n    };\n  },\n  bodygraphql(_1, _2, _3, _4, textblock, _5) {\n    return {\n      body: {\n        graphql: {\n          query: outdentString(textblock.sourceString)\n        }\n      }\n    };\n  },\n  bodygraphqlvars(_1, _2, _3, _4, textblock, _5) {\n    return {\n      body: {\n        graphql: {\n          variables: outdentString(textblock.sourceString)\n        }\n      }\n    };\n  },\n  varsreq(_1, dictionary) {\n    const vars = mapPairListToKeyValPairs(dictionary.ast);\n    _.each(vars, (v) => {\n      let name = v.name;\n      if (name && name.length && name.charAt(0) === '@') {\n        v.name = name.slice(1);\n        v.local = true;\n      } else {\n        v.local = false;\n      }\n    });\n\n    return {\n      vars: {\n        req: vars\n      }\n    };\n  },\n  varsres(_1, dictionary) {\n    const vars = mapPairListToKeyValPairs(dictionary.ast);\n    _.each(vars, (v) => {\n      let name = v.name;\n      if (name && name.length && name.charAt(0) === '@') {\n        v.name = name.slice(1);\n        v.local = true;\n      } else {\n        v.local = false;\n      }\n    });\n\n    return {\n      vars: {\n        res: vars\n      }\n    };\n  },\n  assert(_1, dictionary) {\n    return {\n      assertions: mapPairListToKeyValPairs(dictionary.ast)\n    };\n  },\n  scriptreq(_1, _2, _3, _4, textblock, _5) {\n    return {\n      script: {\n        req: outdentString(textblock.sourceString)\n      }\n    };\n  },\n  scriptres(_1, _2, _3, _4, textblock, _5) {\n    return {\n      script: {\n        res: outdentString(textblock.sourceString)\n      }\n    };\n  },\n  tests(_1, _2, _3, _4, textblock, _5) {\n    return {\n      tests: outdentString(textblock.sourceString)\n    };\n  },\n  docs(_1, _2, _3, _4, textblock, _5) {\n    return {\n      docs: outdentString(textblock.sourceString)\n    };\n  },\n  bodygrpc(_1, dictionary) {\n    const pairs = mapPairListToKeyValPairs(dictionary.ast, false);\n    const namePair = _.find(pairs, { name: 'name' });\n    const contentPair = _.find(pairs, { name: 'content' });\n\n    const messageName = namePair ? namePair.value : '';\n    const messageContent = contentPair ? contentPair.value : '';\n\n    return {\n      body: {\n        mode: 'grpc',\n        grpc: [{\n          name: messageName,\n          content: messageContent\n        }]\n      }\n    };\n  },\n  bodyws(_1, dictionary) {\n    const pairs = mapPairListToKeyValPairs(dictionary.ast, false);\n    const namePair = _.find(pairs, { name: 'name' });\n    const contentPair = _.find(pairs, { name: 'content' });\n    const typePair = _.find(pairs, { name: 'type' });\n\n    const messageName = namePair ? namePair.value : '';\n    const messageContent = contentPair ? contentPair.value : '';\n    const messageTypeContent = typePair ? typePair.value : '';\n\n    return {\n      body: {\n        mode: 'ws',\n        ws: [\n          {\n            name: messageName,\n            type: messageTypeContent,\n            content: messageContent\n          }\n        ]\n      }\n    };\n  },\n  example(_1, _2, _3, _4, examplecontent, _5) {\n    const content = examplecontent.sourceString;\n    const parsedExample = parseExampleContent(content);\n    return {\n      examples: [parsedExample]\n    };\n  },\n  examplecontent(chars) {\n    return outdentString(chars.sourceString);\n  }\n});\n\nconst parser = (input) => {\n  const match = grammar.match(input);\n\n  if (match.succeeded()) {\n    let ast = sem(match).ast;\n\n    return ast;\n  } else {\n    throw new Error(match.message);\n  }\n};\n\nmodule.exports = parser;\n"
  },
  {
    "path": "packages/bruno-lang/v2/src/collectionBruToJson.js",
    "content": "const ohm = require('ohm-js');\nconst _ = require('lodash');\nconst { safeParseJson, outdentString } = require('./utils');\n\nconst grammar = ohm.grammar(`Bru {\n  BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)*\n  auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM |authOAuth2 | authwsse | authapikey | authOauth2Configs\n\n  // Oauth2 additional parameters\n  authOauth2Configs = oauth2AuthReqConfig | oauth2AccessTokenReqConfig | oauth2RefreshTokenReqConfig\n  oauth2AuthReqConfig = oauth2AuthReqHeaders | oauth2AuthReqQueryParams \n  oauth2AccessTokenReqConfig = oauth2AccessTokenReqHeaders | oauth2AccessTokenReqQueryParams | oauth2AccessTokenReqBody\n  oauth2RefreshTokenReqConfig = oauth2RefreshTokenReqHeaders | oauth2RefreshTokenReqQueryParams | oauth2RefreshTokenReqBody\n\n  nl = \"\\\\r\"? \"\\\\n\"\n  st = \" \" | \"\\\\t\"\n  stnl = st | nl\n  tagend = nl \"}\"\n  optionalnl = ~tagend nl\n  keychar = ~(tagend | st | nl | \":\") any\n  valuechar = ~(nl | tagend) any\n\n  // Multiline text block surrounded by '''\n  multilinetextblockdelimiter = \"'''\"\n  multilinetextblock = multilinetextblockdelimiter (~multilinetextblockdelimiter any)* multilinetextblockdelimiter\n\n  // Dictionary Blocks\n  dictionary = st* \"{\" pairlist? tagend\n  pairlist = optionalnl* pair (~tagend stnl* pair)* (~tagend space)*\n  pair = st* (quoted_key | key) st* \":\" st* value st*\n  disable_char = \"~\"\n  quote_char = \"\\\\\"\"\n  esc_char = \"\\\\\\\\\"\n  esc_quote_char = esc_char quote_char\n  quoted_key_char = ~(quote_char | esc_quote_char | nl) any\n  quoted_key = disable_char? quote_char (esc_quote_char | quoted_key_char)* quote_char\n  key = keychar*\n  value = multilinetextblock | valuechar*\n\n  // Text Blocks\n  textblock = textline (~tagend nl textline)*\n  textline = textchar*\n  textchar = ~nl any\n  \n  meta = \"meta\" dictionary\n\n  auth = \"auth\" dictionary\n\n  oauth2AuthReqHeaders = \"auth:oauth2:additional_params:auth_req:headers\" dictionary\n  oauth2AuthReqQueryParams = \"auth:oauth2:additional_params:auth_req:queryparams\" dictionary\n  oauth2AccessTokenReqHeaders = \"auth:oauth2:additional_params:access_token_req:headers\" dictionary\n  oauth2AccessTokenReqQueryParams = \"auth:oauth2:additional_params:access_token_req:queryparams\" dictionary\n  oauth2AccessTokenReqBody = \"auth:oauth2:additional_params:access_token_req:body\" dictionary\n  oauth2RefreshTokenReqHeaders = \"auth:oauth2:additional_params:refresh_token_req:headers\" dictionary\n  oauth2RefreshTokenReqQueryParams = \"auth:oauth2:additional_params:refresh_token_req:queryparams\" dictionary\n  oauth2RefreshTokenReqBody = \"auth:oauth2:additional_params:refresh_token_req:body\" dictionary\n\n  headers = \"headers\" dictionary\n\n  query = \"query\" dictionary\n\n  vars = varsreq | varsres\n  varsreq = \"vars:pre-request\" dictionary\n  varsres = \"vars:post-response\" dictionary\n\n  authawsv4 = \"auth:awsv4\" dictionary\n  authbasic = \"auth:basic\" dictionary\n  authbearer = \"auth:bearer\" dictionary\n  authdigest = \"auth:digest\" dictionary\n  authNTLM = \"auth:ntlm\" dictionary\n  authOAuth2 = \"auth:oauth2\" dictionary\n  authwsse = \"auth:wsse\" dictionary\n  authapikey = \"auth:apikey\" dictionary\n\n  script = scriptreq | scriptres\n  scriptreq = \"script:pre-request\" st* \"{\" nl* textblock tagend\n  scriptres = \"script:post-response\" st* \"{\" nl* textblock tagend\n  tests = \"tests\" st* \"{\" nl* textblock tagend\n  docs = \"docs\" st* \"{\" nl* textblock tagend\n}`);\n\nconst mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => {\n  if (!pairList.length) {\n    return [];\n  }\n  return _.map(pairList[0], (pair) => {\n    let name = _.keys(pair)[0];\n    let value = pair[name];\n\n    if (!parseEnabled) {\n      return {\n        name,\n        value\n      };\n    }\n\n    let enabled = true;\n    if (name && name.length && name.charAt(0) === '~') {\n      name = name.slice(1);\n      enabled = false;\n    }\n\n    return {\n      name,\n      value,\n      enabled\n    };\n  });\n};\n\nconst concatArrays = (objValue, srcValue) => {\n  if (_.isArray(objValue) && _.isArray(srcValue)) {\n    return objValue.concat(srcValue);\n  }\n};\n\nconst mapPairListToKeyValPair = (pairList = []) => {\n  if (!pairList || !pairList.length) {\n    return {};\n  }\n\n  return _.merge({}, ...pairList[0]);\n};\n\nconst sem = grammar.createSemantics().addAttribute('ast', {\n  BruFile(tags) {\n    if (!tags || !tags.ast || !tags.ast.length) {\n      return {};\n    }\n\n    return _.reduce(\n      tags.ast,\n      (result, item) => {\n        return _.mergeWith(result, item, concatArrays);\n      },\n      {}\n    );\n  },\n  dictionary(_1, _2, pairlist, _3) {\n    return pairlist.ast;\n  },\n  pairlist(_1, pair, _2, rest, _3) {\n    return [pair.ast, ...rest.ast];\n  },\n  pair(_1, key, _2, _3, _4, value, _5) {\n    let res = {};\n    res[key.ast] = value.ast ? value.ast.trim() : '';\n    return res;\n  },\n  quoted_key(disabled, _1, chars, _2) {\n    // unquote and handle disabled prefix\n    return (disabled ? disabled.sourceString : '') + chars.ast.join('');\n  },\n  esc_quote_char(_1, quote) {\n    // unescape\n    return quote.sourceString;\n  },\n  quoted_key_char(char) {\n    // return the character itself\n    return char.sourceString;\n  },\n  key(chars) {\n    return chars.sourceString ? chars.sourceString.trim() : '';\n  },\n  value(chars) {\n    if (chars.ctorName === 'list') {\n      return chars.ast;\n    }\n    try {\n      let isMultiline = chars.sourceString?.startsWith(`'''`) && chars.sourceString?.endsWith(`'''`);\n      if (isMultiline) {\n        const multilineString = chars.sourceString?.replace(/^'''|'''$/g, '');\n        return multilineString\n          .split('\\n')\n          .map((line) => line.slice(4))\n          .join('\\n');\n      }\n      return chars.sourceString ? chars.sourceString.trim() : '';\n    } catch (err) {\n      console.error(err);\n    }\n    return chars.sourceString ? chars.sourceString.trim() : '';\n  },\n  textblock(line, _1, rest) {\n    return [line.ast, ...rest.ast].join('\\n');\n  },\n  textline(chars) {\n    return chars.sourceString;\n  },\n  textchar(char) {\n    return char.sourceString;\n  },\n  multilinetextblock(_1, content, _2) {\n    // Join all the content between the triple quotes and trim it\n    return content.sourceString.trim();\n  },\n  nl(_1, _2) {\n    return '';\n  },\n  st(_) {\n    return '';\n  },\n  tagend(_1, _2) {\n    return '';\n  },\n  _iter(...elements) {\n    return elements.map((e) => e.ast);\n  },\n  meta(_1, dictionary) {\n    let meta = mapPairListToKeyValPair(dictionary.ast) || {};\n\n    meta.type = 'collection';\n\n    return {\n      meta\n    };\n  },\n  auth(_1, dictionary) {\n    let auth = mapPairListToKeyValPair(dictionary.ast) || {};\n\n    return {\n      auth: {\n        mode: auth?.mode || 'none'\n      }\n    };\n  },\n  query(_1, dictionary) {\n    return {\n      query: mapPairListToKeyValPairs(dictionary.ast)\n    };\n  },\n  headers(_1, dictionary) {\n    return {\n      headers: mapPairListToKeyValPairs(dictionary.ast)\n    };\n  },\n  authawsv4(_1, dictionary) {\n    const auth = mapPairListToKeyValPairs(dictionary.ast, false);\n    const accessKeyIdKey = _.find(auth, { name: 'accessKeyId' });\n    const secretAccessKeyKey = _.find(auth, { name: 'secretAccessKey' });\n    const sessionTokenKey = _.find(auth, { name: 'sessionToken' });\n    const serviceKey = _.find(auth, { name: 'service' });\n    const regionKey = _.find(auth, { name: 'region' });\n    const profileNameKey = _.find(auth, { name: 'profileName' });\n    const accessKeyId = accessKeyIdKey ? accessKeyIdKey.value : '';\n    const secretAccessKey = secretAccessKeyKey ? secretAccessKeyKey.value : '';\n    const sessionToken = sessionTokenKey ? sessionTokenKey.value : '';\n    const service = serviceKey ? serviceKey.value : '';\n    const region = regionKey ? regionKey.value : '';\n    const profileName = profileNameKey ? profileNameKey.value : '';\n    return {\n      auth: {\n        awsv4: {\n          accessKeyId,\n          secretAccessKey,\n          sessionToken,\n          service,\n          region,\n          profileName\n        }\n      }\n    };\n  },\n  authbasic(_1, dictionary) {\n    const auth = mapPairListToKeyValPairs(dictionary.ast, false);\n    const usernameKey = _.find(auth, { name: 'username' });\n    const passwordKey = _.find(auth, { name: 'password' });\n    const username = usernameKey ? usernameKey.value : '';\n    const password = passwordKey ? passwordKey.value : '';\n    return {\n      auth: {\n        basic: {\n          username,\n          password\n        }\n      }\n    };\n  },\n  authbearer(_1, dictionary) {\n    const auth = mapPairListToKeyValPairs(dictionary.ast, false);\n    const tokenKey = _.find(auth, { name: 'token' });\n    const token = tokenKey ? tokenKey.value : '';\n    return {\n      auth: {\n        bearer: {\n          token\n        }\n      }\n    };\n  },\n  authdigest(_1, dictionary) {\n    const auth = mapPairListToKeyValPairs(dictionary.ast, false);\n    const usernameKey = _.find(auth, { name: 'username' });\n    const passwordKey = _.find(auth, { name: 'password' });\n    const username = usernameKey ? usernameKey.value : '';\n    const password = passwordKey ? passwordKey.value : '';\n    return {\n      auth: {\n        digest: {\n          username,\n          password\n        }\n      }\n    };\n  },\n  authNTLM(_1, dictionary) {\n    const auth = mapPairListToKeyValPairs(dictionary.ast, false);\n    const usernameKey = _.find(auth, { name: 'username' });\n    const passwordKey = _.find(auth, { name: 'password' });\n    const domainKey = _.find(auth, { name: 'domain' });\n\n    const username = usernameKey ? usernameKey.value : '';\n    const password = passwordKey ? passwordKey.value : '';\n    const domain = domainKey ? domainKey.value : '';\n\n    return {\n      auth: {\n        ntlm: {\n          username,\n          password,\n          domain\n        }\n      }\n    };\n  },\n  authOAuth2(_1, dictionary) {\n    const auth = mapPairListToKeyValPairs(dictionary.ast, false);\n    const grantTypeKey = _.find(auth, { name: 'grant_type' });\n    const usernameKey = _.find(auth, { name: 'username' });\n    const passwordKey = _.find(auth, { name: 'password' });\n    const callbackUrlKey = _.find(auth, { name: 'callback_url' });\n    const authorizationUrlKey = _.find(auth, { name: 'authorization_url' });\n    const accessTokenUrlKey = _.find(auth, { name: 'access_token_url' });\n    const refreshTokenUrlKey = _.find(auth, { name: 'refresh_token_url' });\n    const clientIdKey = _.find(auth, { name: 'client_id' });\n    const clientSecretKey = _.find(auth, { name: 'client_secret' });\n    const scopeKey = _.find(auth, { name: 'scope' });\n    const stateKey = _.find(auth, { name: 'state' });\n    const pkceKey = _.find(auth, { name: 'pkce' });\n    const credentialsPlacementKey = _.find(auth, { name: 'credentials_placement' });\n    const credentialsIdKey = _.find(auth, { name: 'credentials_id' });\n    const tokenPlacementKey = _.find(auth, { name: 'token_placement' });\n    const tokenHeaderPrefixKey = _.find(auth, { name: 'token_header_prefix' });\n    const tokenQueryKeyKey = _.find(auth, { name: 'token_query_key' });\n    const autoFetchTokenKey = _.find(auth, { name: 'auto_fetch_token' });\n    const autoRefreshTokenKey = _.find(auth, { name: 'auto_refresh_token' });\n    const tokenSourceKey = _.find(auth, { name: 'token_source' });\n    return {\n      auth: {\n        oauth2:\n          grantTypeKey?.value && grantTypeKey?.value == 'password'\n            ? {\n                grantType: grantTypeKey ? grantTypeKey.value : '',\n                accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',\n                refreshTokenUrl: refreshTokenUrlKey ? refreshTokenUrlKey.value : '',\n                username: usernameKey ? usernameKey.value : '',\n                password: passwordKey ? passwordKey.value : '',\n                clientId: clientIdKey ? clientIdKey.value : '',\n                clientSecret: clientSecretKey ? clientSecretKey.value : '',\n                scope: scopeKey ? scopeKey.value : '',\n                credentialsPlacement: credentialsPlacementKey?.value ? credentialsPlacementKey.value : 'body',\n                credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials',\n                tokenSource: tokenSourceKey?.value ? tokenSourceKey.value : 'access_token',\n                tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',\n                tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : '',\n                tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',\n                autoFetchToken: autoFetchTokenKey ? safeParseJson(autoFetchTokenKey?.value) ?? true : true,\n                autoRefreshToken: autoRefreshTokenKey ? safeParseJson(autoRefreshTokenKey?.value) ?? false : false\n              }\n            : grantTypeKey?.value && grantTypeKey?.value == 'authorization_code'\n              ? {\n                  grantType: grantTypeKey ? grantTypeKey.value : '',\n                  callbackUrl: callbackUrlKey ? callbackUrlKey.value : '',\n                  authorizationUrl: authorizationUrlKey ? authorizationUrlKey.value : '',\n                  accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',\n                  refreshTokenUrl: refreshTokenUrlKey ? refreshTokenUrlKey.value : '',\n                  clientId: clientIdKey ? clientIdKey.value : '',\n                  clientSecret: clientSecretKey ? clientSecretKey.value : '',\n                  scope: scopeKey ? scopeKey.value : '',\n                  state: stateKey ? stateKey.value : '',\n                  pkce: pkceKey ? safeParseJson(pkceKey?.value) ?? false : false,\n                  credentialsPlacement: credentialsPlacementKey?.value ? credentialsPlacementKey.value : 'body',\n                  credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials',\n                  tokenSource: tokenSourceKey?.value ? tokenSourceKey.value : 'access_token',\n                  tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',\n                  tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : '',\n                  tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',\n                  autoFetchToken: autoFetchTokenKey ? safeParseJson(autoFetchTokenKey?.value) ?? true : true,\n                  autoRefreshToken: autoRefreshTokenKey ? safeParseJson(autoRefreshTokenKey?.value) ?? false : false\n                }\n              : grantTypeKey?.value && grantTypeKey?.value == 'implicit'\n                ? {\n                    grantType: grantTypeKey ? grantTypeKey.value : '',\n                    callbackUrl: callbackUrlKey ? callbackUrlKey.value : '',\n                    authorizationUrl: authorizationUrlKey ? authorizationUrlKey.value : '',\n                    clientId: clientIdKey ? clientIdKey.value : '',\n                    scope: scopeKey ? scopeKey.value : '',\n                    state: stateKey ? stateKey.value : '',\n                    credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials',\n                    tokenSource: tokenSourceKey?.value ? tokenSourceKey.value : 'access_token',\n                    tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',\n                    tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : '',\n                    tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',\n                    autoFetchToken: autoFetchTokenKey ? safeParseJson(autoFetchTokenKey?.value) ?? true : true\n                  }\n                : grantTypeKey?.value && grantTypeKey?.value == 'client_credentials'\n                  ? {\n                      grantType: grantTypeKey ? grantTypeKey.value : '',\n                      accessTokenUrl: accessTokenUrlKey ? accessTokenUrlKey.value : '',\n                      refreshTokenUrl: refreshTokenUrlKey ? refreshTokenUrlKey.value : '',\n                      clientId: clientIdKey ? clientIdKey.value : '',\n                      clientSecret: clientSecretKey ? clientSecretKey.value : '',\n                      scope: scopeKey ? scopeKey.value : '',\n                      credentialsPlacement: credentialsPlacementKey?.value ? credentialsPlacementKey.value : 'body',\n                      credentialsId: credentialsIdKey?.value ? credentialsIdKey.value : 'credentials',\n                      tokenSource: tokenSourceKey?.value ? tokenSourceKey.value : 'access_token',\n                      tokenPlacement: tokenPlacementKey?.value ? tokenPlacementKey.value : 'header',\n                      tokenHeaderPrefix: tokenHeaderPrefixKey?.value ? tokenHeaderPrefixKey.value : '',\n                      tokenQueryKey: tokenQueryKeyKey?.value ? tokenQueryKeyKey.value : 'access_token',\n                      autoFetchToken: autoFetchTokenKey ? safeParseJson(autoFetchTokenKey?.value) ?? true : true,\n                      autoRefreshToken: autoRefreshTokenKey ? safeParseJson(autoRefreshTokenKey?.value) ?? false : false\n                    }\n                  : {}\n      }\n    };\n  },\n  oauth2AuthReqHeaders(_1, dictionary) {\n    return {\n      oauth2_additional_parameters_auth_req_headers: mapPairListToKeyValPairs(dictionary.ast)\n    };\n  },\n  oauth2AuthReqQueryParams(_1, dictionary) {\n    return {\n      oauth2_additional_parameters_auth_req_queryparams: mapPairListToKeyValPairs(dictionary.ast)\n    };\n  },\n  oauth2AccessTokenReqHeaders(_1, dictionary) {\n    return {\n      oauth2_additional_parameters_access_token_req_headers: mapPairListToKeyValPairs(dictionary.ast)\n    };\n  },\n  oauth2AccessTokenReqQueryParams(_1, dictionary) {\n    return {\n      oauth2_additional_parameters_access_token_req_queryparams: mapPairListToKeyValPairs(dictionary.ast)\n    };\n  },\n  oauth2AccessTokenReqBody(_1, dictionary) {\n    return {\n      oauth2_additional_parameters_access_token_req_bodyvalues: mapPairListToKeyValPairs(dictionary.ast)\n    };\n  },\n  oauth2RefreshTokenReqHeaders(_1, dictionary) {\n    return {\n      oauth2_additional_parameters_refresh_token_req_headers: mapPairListToKeyValPairs(dictionary.ast)\n    };\n  },\n  oauth2RefreshTokenReqQueryParams(_1, dictionary) {\n    return {\n      oauth2_additional_parameters_refresh_token_req_queryparams: mapPairListToKeyValPairs(dictionary.ast)\n    };\n  },\n  oauth2RefreshTokenReqBody(_1, dictionary) {\n    return {\n      oauth2_additional_parameters_refresh_token_req_bodyvalues: mapPairListToKeyValPairs(dictionary.ast)\n    };\n  },\n  authwsse(_1, dictionary) {\n    const auth = mapPairListToKeyValPairs(dictionary.ast, false);\n    const userKey = _.find(auth, { name: 'username' });\n    const secretKey = _.find(auth, { name: 'password' });\n    const username = userKey ? userKey.value : '';\n    const password = secretKey ? secretKey.value : '';\n    return {\n      auth: {\n        wsse: {\n          username,\n          password\n        }\n      }\n    };\n  },\n  authapikey(_1, dictionary) {\n    const auth = mapPairListToKeyValPairs(dictionary.ast, false);\n\n    const findValueByName = (name) => {\n      const item = _.find(auth, { name });\n      return item ? item.value : '';\n    };\n\n    const key = findValueByName('key');\n    const value = findValueByName('value');\n    const placement = findValueByName('placement');\n\n    return {\n      auth: {\n        apikey: {\n          key,\n          value,\n          placement\n        }\n      }\n    };\n  },\n  varsreq(_1, dictionary) {\n    const vars = mapPairListToKeyValPairs(dictionary.ast);\n    _.each(vars, (v) => {\n      let name = v.name;\n      if (name && name.length && name.charAt(0) === '@') {\n        v.name = name.slice(1);\n        v.local = true;\n      } else {\n        v.local = false;\n      }\n    });\n\n    return {\n      vars: {\n        req: vars\n      }\n    };\n  },\n  varsres(_1, dictionary) {\n    const vars = mapPairListToKeyValPairs(dictionary.ast);\n    _.each(vars, (v) => {\n      let name = v.name;\n      if (name && name.length && name.charAt(0) === '@') {\n        v.name = name.slice(1);\n        v.local = true;\n      } else {\n        v.local = false;\n      }\n    });\n\n    return {\n      vars: {\n        res: vars\n      }\n    };\n  },\n  scriptreq(_1, _2, _3, _4, textblock, _5) {\n    return {\n      script: {\n        req: outdentString(textblock.sourceString)\n      }\n    };\n  },\n  scriptres(_1, _2, _3, _4, textblock, _5) {\n    return {\n      script: {\n        res: outdentString(textblock.sourceString)\n      }\n    };\n  },\n  tests(_1, _2, _3, _4, textblock, _5) {\n    return {\n      tests: outdentString(textblock.sourceString)\n    };\n  },\n  docs(_1, _2, _3, _4, textblock, _5) {\n    return {\n      docs: outdentString(textblock.sourceString)\n    };\n  }\n});\n\nconst parser = (input) => {\n  const match = grammar.match(input);\n\n  if (match.succeeded()) {\n    let ast = sem(match).ast;\n\n    return ast;\n  } else {\n    throw new Error(match.message);\n  }\n};\n\nmodule.exports = parser;\n"
  },
  {
    "path": "packages/bruno-lang/v2/src/common/attributes.js",
    "content": "/**\n * Base AST attributes common to all grammar parsers in examples\n * These attributes handle common grammar constructs like dictionaries, pairs, text blocks, etc.\n */\nconst astBaseAttribute = {\n  dictionary(_1, _2, pairlist, _3) {\n    return pairlist.ast;\n  },\n  pairlist(_1, pair, _2, rest) {\n    return [pair.ast, ...rest.ast];\n  },\n  pair(_1, key, _2, _3, _4, value, _5) {\n    let res = {};\n    if (Array.isArray(value.ast)) {\n      res[key.ast] = value.ast;\n      return res;\n    }\n    res[key.ast] = value.ast ? value.ast.trim() : '';\n    return res;\n  },\n  esc_quote_char(_1, quote) {\n    return quote.sourceString;\n  },\n  quoted_key(disabled, _1, chars, _2) {\n    return (disabled ? disabled.sourceString : '') + chars.ast.join('');\n  },\n  key(chars) {\n    return chars.sourceString ? chars.sourceString.trim() : '';\n  },\n  textblock(line, _1, rest) {\n    return [line.ast, ...rest.ast].join('\\n');\n  },\n  textline(chars) {\n    return chars.sourceString;\n  },\n  textchar(char) {\n    return char.sourceString;\n  },\n  nl(_1, _2) {\n    return '';\n  },\n  st(_) {\n    return '';\n  },\n  tagend(_1, _2) {\n    return '';\n  },\n  _terminal() {\n    return this.sourceString;\n  },\n  multilinetextblockdelimiter(_) {\n    return '';\n  },\n  multilinetextblock(_1, content, _2, _3, contentType) {\n    const multilineString = content.sourceString\n      .split('\\n')\n      .map((line) => line.slice(4))\n      .join('\\n');\n\n    if (!contentType.sourceString) {\n      return multilineString;\n    }\n    return `${multilineString} ${contentType.sourceString}`;\n  },\n  singlelinevalue(chars) {\n    return chars.sourceString?.trim() || '';\n  },\n  _iter(...elements) {\n    return elements.map((e) => e.ast);\n  }\n};\n\nmodule.exports = astBaseAttribute;\n"
  },
  {
    "path": "packages/bruno-lang/v2/src/common/semantic-utils.js",
    "content": "const _ = require('lodash');\n\n/**\n * Maps a pair list to an array of key-value pairs\n * @param {Array} pairList - The pair list from the AST\n * @param {boolean} parseEnabled - Whether to parse the enabled/disabled state from the name\n * @returns {Array} Array of objects with name, value, and optionally enabled properties\n */\nconst mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => {\n  if (!pairList.length) {\n    return [];\n  }\n  return _.map(pairList[0], (pair) => {\n    let name = _.keys(pair)[0];\n    let value = pair[name];\n\n    if (!parseEnabled) {\n      return {\n        name,\n        value\n      };\n    }\n\n    let enabled = true;\n    if (name && name.length && name.charAt(0) === '~') {\n      name = name.slice(1);\n      enabled = false;\n    }\n\n    return {\n      name,\n      value,\n      enabled\n    };\n  });\n};\n\n/**\n * Maps a pair list to an array of request parameters with type\n * @param {Array} pairList - The pair list from the AST\n * @param {string} type - The type of parameter (e.g., 'path', 'query')\n * @returns {Array} Array of objects with name, value, enabled, and type properties\n */\nconst mapRequestParams = (pairList = [], type) => {\n  if (!pairList.length) {\n    return [];\n  }\n  return _.map(pairList[0], (pair) => {\n    let name = _.keys(pair)[0];\n    let value = pair[name];\n    let enabled = true;\n    if (name && name.length && name.charAt(0) === '~') {\n      name = name.slice(1);\n      enabled = false;\n    }\n\n    return {\n      name,\n      value,\n      enabled,\n      type\n    };\n  });\n};\n\n/**\n * Extracts content type from multipart form pair value\n * @param {Object} pair - The pair object to extract content type from\n */\nconst multipartExtractContentType = (pair) => {\n  if (_.isString(pair.value)) {\n    const match = pair.value.match(/^(.*?)\\s*@contentType\\((.*?)\\)\\s*$/s);\n    if (match != null && match.length > 2) {\n      pair.value = match[1];\n      pair.contentType = match[2];\n    } else {\n      pair.contentType = '';\n    }\n  }\n};\n\n/**\n * Extracts content type from file pair value\n * @param {Object} pair - The pair object to extract content type from\n */\nconst fileExtractContentType = (pair) => {\n  if (_.isString(pair.value)) {\n    const match = pair.value.match(/^(.*?)\\s*@contentType\\((.*?)\\)\\s*$/s);\n    if (match && match.length > 2) {\n      pair.value = match[1].trim();\n      pair.contentType = match[2].trim();\n    } else {\n      pair.contentType = '';\n    }\n  }\n};\n\n/**\n * Maps a pair list to multipart form key-value pairs\n * @param {Array} pairList - The pair list from the AST\n * @param {boolean} parseEnabled - Whether to parse the enabled/disabled state from the name\n * @returns {Array} Array of objects with name, value, enabled, type, and contentType properties\n */\nconst mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) => {\n  const pairs = mapPairListToKeyValPairs(pairList, parseEnabled);\n\n  return pairs.map((pair) => {\n    pair.type = 'text';\n    multipartExtractContentType(pair);\n\n    if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) {\n      let filestr = pair.value.replace(/^@file\\(/, '').replace(/\\)$/, '');\n      pair.type = 'file';\n      pair.value = filestr.split('|');\n    }\n\n    return pair;\n  });\n};\n\n/**\n * Maps a pair list to file key-value pairs\n * @param {Array} pairList - The pair list from the AST\n * @param {boolean} parseEnabled - Whether to parse the enabled/disabled state from the name\n * @returns {Array} Array of objects with filePath, selected, and contentType properties\n */\nconst mapPairListToKeyValPairsFile = (pairList = [], parseEnabled = true) => {\n  const pairs = mapPairListToKeyValPairs(pairList, parseEnabled);\n  return pairs.map((pair) => {\n    fileExtractContentType(pair);\n\n    if (pair.value.startsWith('@file(') && pair.value.endsWith(')')) {\n      let filePath = pair.value.replace(/^@file\\(/, '').replace(/\\)$/, '');\n      pair.filePath = filePath;\n      pair.selected = pair.enabled;\n\n      // Remove pair.value as it only contains the file path reference\n      delete pair.value;\n      // Remove pair.name as it is auto-generated (e.g., file1, file2, file3, etc.)\n      delete pair.name;\n      delete pair.enabled;\n    }\n\n    return pair;\n  });\n};\n\n/**\n * Concatenates arrays when merging objects (used with lodash mergeWith)\n * @param {*} objValue - The existing value\n * @param {*} srcValue - The source value\n * @returns {Array|undefined} Concatenated array if both values are arrays, undefined otherwise\n */\nconst concatArrays = (objValue, srcValue) => {\n  if (_.isArray(objValue) && _.isArray(srcValue)) {\n    return objValue.concat(srcValue);\n  }\n};\n\nmodule.exports = {\n  mapPairListToKeyValPairs,\n  mapRequestParams,\n  multipartExtractContentType,\n  fileExtractContentType,\n  mapPairListToKeyValPairsMultipart,\n  mapPairListToKeyValPairsFile,\n  concatArrays\n};\n"
  },
  {
    "path": "packages/bruno-lang/v2/src/dotenvToJson.js",
    "content": "const dotenv = require('dotenv');\n\nconst parser = (input) => {\n  const buf = Buffer.from(input);\n  const parsed = dotenv.parse(buf);\n  return parsed;\n};\n\nmodule.exports = parser;\n"
  },
  {
    "path": "packages/bruno-lang/v2/src/envToJson.js",
    "content": "const ohm = require('ohm-js');\nconst _ = require('lodash');\n\n// Env files use 4-space indentation for multiline content\n// vars {\n//   API_KEY: '''\n//     -----BEGIN PUBLIC KEY-----\n//     MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8\n//     HMR5LXFFrwXQFE6xUVhXrxUpx1TtfoGkRcU7LEWV\n//     -----END PUBLIC KEY-----\n//   '''\n// }\nconst indentLevel = 4;\nconst grammar = ohm.grammar(`Bru {\n  BruEnvFile = (vars | secretvars | color)*\n\n  nl = \"\\\\r\"? \"\\\\n\"\n  st = \" \" | \"\\\\t\"\n  stnl = st | nl\n  tagend = nl \"}\"\n  optionalnl = ~tagend nl\n  keychar = ~(tagend | st | nl | \":\") any\n  valuechar = ~(nl | tagend | multilinetextblockstart) any\n\n  multilinetextblockdelimiter = \"'''\"\n  multilinetextblockstart = \"'''\" nl\n  multilinetextblockend = nl st* \"'''\"\n  multilinetextblock = multilinetextblockstart multilinetextblockcontent multilinetextblockend\n  multilinetextblockcontent = (~multilinetextblockend any)*\n\n  // Dictionary Blocks\n  dictionary = st* \"{\" pairlist? tagend\n  pairlist = optionalnl* pair (~tagend stnl* pair)* (~tagend space)*\n  pair = st* key st* \":\" st* value st*\n  key = keychar*\n  value = multilinetextblock | valuechar*\n\n  // Array Blocks\n  array = st* \"[\" stnl* valuelist stnl* \"]\"\n  valuelist = stnl* arrayvalue stnl* (\",\" stnl* arrayvalue)*\n  arrayvalue = arrayvaluechar*\n  arrayvaluechar = ~(nl | st | \"[\" | \"]\" | \",\") any\n\n  secretvars = \"vars:secret\" array\n  vars = \"vars\" dictionary\n  color = \"color:\" any*\n}`);\n\nconst mapPairListToKeyValPairs = (pairList = []) => {\n  if (!pairList.length) {\n    return [];\n  }\n\n  return _.map(pairList[0], (pair) => {\n    let name = _.keys(pair)[0];\n    let value = pair[name];\n    let enabled = true;\n    if (name && name.length && name.charAt(0) === '~') {\n      name = name.slice(1);\n      enabled = false;\n    }\n\n    return {\n      name,\n      value,\n      enabled\n    };\n  });\n};\n\nconst mapArrayListToKeyValPairs = (arrayList = []) => {\n  arrayList = arrayList.filter((v) => v && v.length);\n\n  if (!arrayList.length) {\n    return [];\n  }\n\n  return _.map(arrayList, (value) => {\n    let name = value;\n    let enabled = true;\n    if (name && name.length && name.charAt(0) === '~') {\n      name = name.slice(1);\n      enabled = false;\n    }\n\n    return {\n      name,\n      value: '',\n      enabled\n    };\n  });\n};\n\nconst concatArrays = (objValue, srcValue) => {\n  if (_.isArray(objValue) && _.isArray(srcValue)) {\n    return objValue.concat(srcValue);\n  }\n};\n\nconst sem = grammar.createSemantics().addAttribute('ast', {\n  BruEnvFile(tags) {\n    if (!tags || !tags.ast || !tags.ast.length) {\n      return {\n        variables: []\n      };\n    }\n\n    return _.reduce(\n      tags.ast,\n      (result, item) => {\n        return _.mergeWith(result, item, concatArrays);\n      },\n      {}\n    );\n  },\n  array(_1, _2, _3, valuelist, _4, _5) {\n    return valuelist.ast;\n  },\n  arrayvalue(chars) {\n    return chars.sourceString ? chars.sourceString.trim() : '';\n  },\n  valuelist(_1, value, _2, _3, _4, rest) {\n    return [value.ast, ...rest.ast];\n  },\n  dictionary(_1, _2, pairlist, _3) {\n    return pairlist.ast;\n  },\n  pairlist(_1, pair, _2, rest, _3) {\n    return [pair.ast, ...rest.ast];\n  },\n  pair(_1, key, _2, _3, _4, value, _5) {\n    let res = {};\n    res[key.ast] = value.ast ? value.ast.trim() : '';\n    return res;\n  },\n  key(chars) {\n    return chars.sourceString ? chars.sourceString.trim() : '';\n  },\n  value(chars) {\n    // .ctorName provides the name of the rule that matched the input\n    if (chars.ctorName === 'multilinetextblock') {\n      return chars.ast;\n    }\n    return chars.sourceString ? chars.sourceString.trim() : '';\n  },\n  multilinetextblockstart(_1, _2) {\n    return '';\n  },\n  multilinetextblockend(_1, _2, _3) {\n    return '';\n  },\n  multilinetextblockdelimiter(_) {\n    return '';\n  },\n  multilinetextblock(_1, content, _2) {\n    return content.ast\n      .split(/\\r\\n|\\r|\\n/)\n      .map((line) => line.slice(indentLevel)) // Remove 4-space indentation\n      .join('\\n')\n      .trim();\n  },\n  multilinetextblockcontent(chars) {\n    return chars.sourceString;\n  },\n  nl(_1, _2) {\n    return '';\n  },\n  st(_) {\n    return '';\n  },\n  tagend(_1, _2) {\n    return '';\n  },\n  _iter(...elements) {\n    return elements.map((e) => e.ast);\n  },\n  vars(_1, dictionary) {\n    const vars = mapPairListToKeyValPairs(dictionary.ast);\n    _.each(vars, (v) => {\n      v.secret = false;\n    });\n    return {\n      variables: vars\n    };\n  },\n  secretvars: (_1, array) => {\n    const vars = mapArrayListToKeyValPairs(array.ast);\n    _.each(vars, (v) => {\n      v.secret = true;\n    });\n    return {\n      variables: vars\n    };\n  },\n  color: (_1, anystring) => {\n    return {\n      color: anystring.sourceString.trim()\n    };\n  }\n});\n\nconst parser = (input) => {\n  const match = grammar.match(input);\n\n  if (match.succeeded()) {\n    return sem(match).ast;\n  } else {\n    throw new Error(match.message);\n  }\n};\n\nmodule.exports = parser;\n"
  },
  {
    "path": "packages/bruno-lang/v2/src/example/bruToJson.js",
    "content": "const ohm = require('ohm-js');\nconst _ = require('lodash');\nconst { safeParseJson, outdentString } = require('../utils');\nconst parseRequest = require('./request/bruToJson');\nconst parseResponse = require('./response/bruToJson');\nconst astBaseAttribute = require('../common/attributes');\n\n/**\n * Example Grammar for Bruno\n *\n * Examples follow a simplified grammar with root-level properties and proper colon syntax.\n * No meta block - everything is at root level: name, description, type, url, etc.\n * Supports all body types from request side but response body stays as simple text.\n */\n\nconst exampleGrammar = ohm.grammar(`Example {\n  ExampleFile = (name | description | request | response)*\n  \n  nl = \"\\\\r\"? \"\\\\n\"\n  st = \" \" | \"\\\\t\"\n  stnl = st | nl\n  tagend = nl \"}\"\n  optionalnl = ~tagend nl\n  keychar = ~(tagend | st | nl | \":\") any\n  valuechar = ~(nl | tagend) any\n\n  // Multiline text block surrounded by '''\n  multilinetextblockdelimiter = \"'''\"\n  multilinetextblock = multilinetextblockdelimiter (~multilinetextblockdelimiter any)* multilinetextblockdelimiter st* contenttypeannotation?\n  contenttypeannotation = \"@contentType(\" (~\")\" any)* \")\"\n\n  // Dictionary Blocks\n  dictionary = st* \"{\" pairlist? tagend\n  pairlist = optionalnl* pair (~tagend nl pair)*\n  pair = st* (quoted_key | key) st* \":\" st* value st*\n  disable_char = \"~\"\n  quote_char = \"\\\\\"\"\n  esc_char = \"\\\\\\\\\"\n  esc_quote_char = esc_char quote_char\n  quoted_key_char = ~(quote_char | esc_quote_char | nl) any\n  quoted_key = disable_char? quote_char (esc_quote_char | quoted_key_char)* quote_char\n  key = keychar*\n  value = list | multilinetextblock | singlelinevalue\n  singlelinevalue = valuechar*\n  \n  // List\n  list = st* \"[\" nl+ listitems? st* nl+ st* \"]\"\n  listitems = listitem (nl+ listitem)*\n  listitem = st+ (alnum | \"_\" | \"-\")+ st*\n  \n  // Text Blocks\n  textblock = textline (~tagend nl textline)*\n  textline = textchar*\n  textchar = ~nl any\n  textvalue = multilinetextblock | singlelinevalue\n\n  // Root level properties\n  name =  \"name\" st* \":\" st* valuechar* st*\n  description = \"description\" st* \":\" st* textvalue st*\n\n  // Request block\n  request = nl* \"request\" st* \":\" st* \"{\" nl* requestcontent+ nl* \"}\" nl*\n  requestcontent = (~tagend any)+\n\n  // Response block\n  response =  \"response\" st* \":\" st* \"{\" nl* responsecontent nl* \"}\" nl*\n  responsecontent = (~tagend any)+\n}`);\n\nconst astExampleAttribute = {\n  ExampleFile(tags) {\n    if (!tags || !tags.ast || !tags.ast.length) {\n      return {};\n    }\n\n    const result = _.reduce(tags.ast, (acc, item) => {\n      return _.merge(acc, item);\n    }, {});\n\n    return result;\n  },\n  // Root level properties\n  name(_1, _2, _3, _4, value, _6) {\n    return {\n      name: value.sourceString ? value.sourceString.trim() : ''\n    };\n  },\n  description(_1, _2, _3, _4, value, _6) {\n    return {\n      description: value.ast ? value.ast.trim() : ''\n    };\n  },\n  textvalue(content) {\n    return content.ast;\n  },\n  multilinetextblock(_1, content, _2, _3, contentType) {\n    const multilineString = outdentString(content.sourceString);\n\n    if (!contentType.sourceString) {\n      return multilineString;\n    }\n    return `${multilineString} ${contentType.sourceString}`;\n  },\n  request(_1, _2, _3, _4, _5, _6, _7, requestcontent, _8, _9, _10) {\n    if (!requestcontent || !requestcontent.ast || !requestcontent.ast.length) {\n      return {};\n    }\n\n    const outdentedContent = outdentString(requestcontent.sourceString);\n    const parsedRequest = parseRequest(outdentedContent);\n\n    return {\n      request: parsedRequest\n    };\n  },\n  requestcontent(chars) {\n    return chars.sourceString;\n  },\n  response(_1, _2, _3, _4, _5, _6, content, _7, _8, _9) {\n    const outdentedContent = outdentString(content.sourceString);\n    const parsedResponse = parseResponse(outdentedContent);\n\n    return { response: parsedResponse };\n  },\n  responsecontent(chars) {\n    return chars.sourceString;\n  }\n};\n\nconst grammarSemantics = exampleGrammar.createSemantics();\ngrammarSemantics.addAttribute('ast', { ...astBaseAttribute, ...astExampleAttribute });\n\nconst parseExample = (input) => {\n  const match = exampleGrammar.match(input);\n\n  if (match.succeeded()) {\n    let ast = grammarSemantics(match).ast;\n    return ast;\n  } else {\n    console.log('match failed', match);\n    throw new Error(match.message);\n  }\n};\n\nmodule.exports = parseExample;\n"
  },
  {
    "path": "packages/bruno-lang/v2/src/example/jsonToBru.js",
    "content": "const { indentString, getValueString } = require('../utils');\n\n// remove the last line if two new lines are found\nconst stripLastLine = (text) => {\n  if (!text || !text.length) return text;\n\n  return text.replace(/(\\r?\\n)$/, '');\n};\n\nconst quoteKey = (key) => {\n  const quotableChars = [':', '\"', '{', '}', ' '];\n  return quotableChars.some((char) => key.includes(char)) ? ('\"' + key.replaceAll('\"', '\\\\\"') + '\"') : key;\n};\n\n// Custom indentation function for proper spacing\nconst indentStringCustom = (str, spaces = 4) => {\n  if (!str || !str.length) {\n    return str || '';\n  }\n\n  const indent = ' '.repeat(spaces);\n  return str\n    .split(/\\r\\n|\\r|\\n/)\n    .map((line) => indent + line)\n    .join('\\n');\n};\n\n// Convert JSON to example BRU format with proper colon syntax\nconst jsonToExampleBru = (json) => {\n  const { name, description, request, response } = json;\n  const { url, method, params, headers, body } = request || {};\n  const { headers: responseHeaders, status: responseStatus, statusText: responseStatusText, body: responseBody } = response || {};\n\n  let bru = '';\n\n  if (name) {\n    bru += `name: ${name}\\n`;\n  }\n\n  if (description) {\n    const descriptionValue = getValueString(description);\n    bru += `description: ${descriptionValue}\\n`;\n  }\n\n  // Request block\n  bru += '\\nrequest: {\\n';\n\n  bru += `  url: ${url}\\n`;\n\n  bru += `  method: ${method}\\n`;\n\n  // Add mode field inside request block, right after method\n  if (request && request.body && request.body.mode) {\n    bru += `  mode: ${request.body.mode}\\n`;\n  }\n\n  if (params && params.length) {\n    const queryParams = params.filter((param) => param.type === 'query');\n    const pathParams = params.filter((param) => param.type === 'path');\n\n    if (queryParams.length) {\n      bru += '  params:query: {\\n';\n      bru += `${indentStringCustom(queryParams\n        .map((item) => `${item.enabled ? '' : '~'}${quoteKey(item.name)}: ${item.value}`)\n        .join('\\n'), 4)}`;\n      bru += '\\n  }\\n\\n';\n    }\n\n    if (pathParams.length) {\n      bru += '  params:path: {\\n';\n      bru += `${indentStringCustom(pathParams\n        .map((item) => `${item.enabled ? '' : '~'}${quoteKey(item.name)}: ${item.value}`)\n        .join('\\n'), 4)}`;\n      bru += '\\n  }\\n\\n';\n    }\n  }\n\n  if (headers && headers.length) {\n    bru += '  headers: {\\n';\n    bru += `${indentStringCustom(headers\n      .map((item) => `${item.enabled ? '' : '~'}${quoteKey(item.name)}: ${item.value}`)\n      .join('\\n'), 4)}`;\n    bru += '\\n  }\\n\\n';\n  }\n\n  // All body types from request side\n  if (body && body.json) {\n    bru += `  body:json: {\\n${indentStringCustom(body.json, 4)}\\n  }\\n\\n`;\n  }\n\n  if (body && body.text) {\n    bru += `  body:text: {\\n${indentStringCustom(body.text, 4)}\\n  }\\n\\n`;\n  }\n\n  if (body && body.xml) {\n    bru += `  body:xml: {\\n${indentStringCustom(body.xml, 4)}\\n  }\\n\\n`;\n  }\n\n  if (body && body.sparql) {\n    bru += `  body:sparql: {\\n${indentStringCustom(body.sparql, 4)}\\n  }\\n\\n`;\n  }\n\n  if (body && body.graphql && body.graphql.query) {\n    bru += `  body:graphql: {\\n${indentStringCustom(body.graphql.query, 4)}\\n  }\\n\\n`;\n  }\n\n  if (body && body.graphql && body.graphql.variables) {\n    bru += `  body:graphql:vars: {\\n${indentStringCustom(body.graphql.variables, 4)}\\n  }\\n\\n`;\n  }\n\n  if (body && body.formUrlEncoded && body.formUrlEncoded.length) {\n    bru += `  body:form-urlencoded: {\\n`;\n    const enabledValues = body.formUrlEncoded\n      .filter((item) => item.enabled)\n      .map((item) => `${quoteKey(item.name)}: ${item.value}`)\n      .join('\\n');\n    const disabledValues = body.formUrlEncoded\n      .filter((item) => !item.enabled)\n      .map((item) => `~${quoteKey(item.name)}: ${item.value}`)\n      .join('\\n');\n\n    if (enabledValues) {\n      bru += `${indentStringCustom(enabledValues, 4)}\\n`;\n    }\n    if (disabledValues) {\n      bru += `${indentStringCustom(disabledValues, 4)}\\n`;\n    }\n    bru += '  }\\n\\n';\n  }\n\n  if (body && body.multipartForm && body.multipartForm.length) {\n    bru += `  body:multipart-form: {\\n`;\n    const multipartForms = body.multipartForm;\n    if (multipartForms.length) {\n      bru += `${indentStringCustom(multipartForms\n        .map((item) => {\n          const enabled = item.enabled ? '' : '~';\n          const contentType\n            = item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : '';\n\n          if (item.type === 'text') {\n            // Use getValueString to wrap multiline values with triple quotes\n            const valueString = getValueString(item.value);\n            return `${enabled}${quoteKey(item.name)}: ${valueString}${contentType}`;\n          }\n\n          if (item.type === 'file') {\n            const filepaths = Array.isArray(item.value) ? item.value : [];\n            const filestr = filepaths.join('|');\n            const value = `@file(${filestr})`;\n            return `${enabled}${quoteKey(item.name)}: ${value}${contentType}`;\n          }\n        })\n        .join('\\n'), 4)}\\n`;\n    }\n    bru += '  }\\n\\n';\n  }\n\n  if (body && body.file && body.file.length) {\n    bru += `  body:file: {\\n`;\n    const files = body.file;\n    if (files.length) {\n      bru += `${indentStringCustom(files\n        .map((item) => {\n          const selected = item.selected ? '' : '~';\n          const contentType\n            = item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : '';\n          const filePath = item.filePath || '';\n          const value = `@file(${filePath})`;\n          const itemName = 'file';\n          return `${selected}${quoteKey(itemName)}: ${value}${contentType}`;\n        })\n        .join('\\n'), 4)}\\n`;\n    }\n    bru += '  }\\n\\n';\n  }\n  /**\n   * Only remove the last line if there are two new lines at the end\n   * else the stripLastLine function will remove the last line and the curly braces move to the end of last line\n   */\n  if (bru.endsWith('\\n\\n')) {\n    bru = stripLastLine(bru);\n  }\n\n  bru += '}\\n\\n';\n\n  // Response block\n  if (response) {\n    bru += 'response: {\\n';\n\n    // Response headers\n    if (responseHeaders && responseHeaders.length) {\n      bru += '  headers: {\\n';\n      bru += `${indentStringCustom(responseHeaders\n        .map((item) => `${quoteKey(item.name)}: ${item.value}`)\n        .join('\\n'), 4)}`;\n      bru += '\\n  }\\n\\n';\n    }\n\n    // Response status\n    if (responseStatus || responseStatusText) {\n      bru += '  status: {\\n';\n      if (responseStatus !== undefined) {\n        bru += `    code: ${responseStatus}\\n`;\n      }\n      if (responseStatusText !== undefined) {\n        bru += `    text: ${responseStatusText}\\n`;\n      }\n      bru += '  }\\n\\n';\n    }\n\n    // Response body with type and content\n    if (responseBody) {\n      bru += '  body: {\\n';\n\n      if (responseBody.type) {\n        bru += `    type: ${responseBody.type}\\n`;\n      }\n\n      if (responseBody.content !== undefined) {\n        let contentString = typeof responseBody.content === 'string' ? responseBody.content : JSON.stringify(responseBody.content, null, 2);\n        bru += `    content: '''\\n${indentStringCustom(contentString, 6)}\\n    '''\\n`;\n      }\n\n      bru += '  }\\n\\n';\n    }\n\n    bru = stripLastLine(bru);\n    bru += '}';\n  }\n\n  /**\n   * Only remove the last line if there are two new lines at the end\n   * else the stripLastLine function will remove the last line and the curly braces move to the end of last line\n   */\n  while (bru.endsWith('\\n')) {\n    bru = stripLastLine(bru);\n  }\n\n  return bru;\n};\n\nmodule.exports = jsonToExampleBru;\n"
  },
  {
    "path": "packages/bruno-lang/v2/src/example/request/bruToJson.js",
    "content": "const ohm = require('ohm-js');\nconst _ = require('lodash');\nconst { safeParseJson, outdentString } = require('../../utils');\nconst astBaseAttribute = require('../../common/attributes');\nconst {\n  mapPairListToKeyValPairs,\n  mapRequestParams,\n  mapPairListToKeyValPairsMultipart,\n  mapPairListToKeyValPairsFile,\n  concatArrays\n} = require('../../common/semantic-utils');\n\n/**\n * Request Block Grammar for Bruno Examples\n *\n * Handles parsing of request blocks within example files.\n * Supports all body types: json, text, xml, sparql, graphql, form-urlencoded, multipart-form, file\n */\nconst requestGrammar = ohm.grammar(`Request {\n  RequestFile = requestcontent*\n  \n  nl = \"\\\\r\"? \"\\\\n\"\n  st = \" \" | \"\\\\t\"\n  stnl = st | nl\n  tagend = nl \"}\"\n  optionalnl = ~tagend nl\n  keychar = ~(tagend | st | nl | \":\") any\n  valuechar = ~(nl | tagend) any\n\n  // Multiline text block surrounded by '''\n  multilinetextblockdelimiter = \"'''\"\n  multilinetextblock = multilinetextblockdelimiter (~multilinetextblockdelimiter any)* multilinetextblockdelimiter st* contenttypeannotation?\n  contenttypeannotation = \"@contentType(\" (~\")\" any)* \")\"\n\n  // Dictionary Blocks\n  dictionary = st* \"{\" pairlist? tagend\n  pairlist = optionalnl* pair (~tagend nl pair)*\n  pair = st* (quoted_key | key) st* \":\" st* value st*\n  disable_char = \"~\"\n  quote_char = \"\\\\\"\"\n  esc_char = \"\\\\\\\\\"\n  esc_quote_char = esc_char quote_char\n  quoted_key_char = ~(quote_char | esc_quote_char | nl) any\n  quoted_key = disable_char? quote_char (esc_quote_char | quoted_key_char)* quote_char\n  key = keychar*\n  value = list | multilinetextblock | singlelinevalue\n  singlelinevalue = valuechar*\n\n  // List\n  list = st* \"[\" nl+ listitems? st* nl+ st* \"]\"\n  listitems = listitem (nl+ listitem)*\n  listitem = st+ (alnum | \"_\" | \"-\")+ st*\n\n  // Text Blocks\n  textblock = textline (~tagend nl textline)*\n  textline = textchar*\n  textchar = ~nl any\n\n  // Request content\n  requestcontent = requesturl | requestmethod | requestmode | requestparamspath | requestparamsquery | requestheaders | requestbodies\n  requesturl = \"url\" st* \":\" st* valuechar*\n  requestmethod = \"method\" st* \":\" st* valuechar*\n  requestmode = \"mode\" st* \":\" st* valuechar*\n  requestparamspath = \"params:path\" st* \":\" st* dictionary\n  requestparamsquery = \"params:query\" st* \":\" st* dictionary\n  requestheaders = \"headers\" st* \":\" st* dictionary\n  requestbodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyformurlencoded | bodymultipart | bodyfile\n\n  // All body types from request side\n  bodyjson = \"body:json\" st* \":\" st* \"{\" nl* textblock tagend\n  bodytext = \"body:text\" st* \":\" st* \"{\" nl* textblock tagend\n  bodyxml = \"body:xml\" st* \":\" st* \"{\" nl* textblock tagend\n  bodysparql = \"body:sparql\" st* \":\" st* \"{\" nl* textblock tagend\n  bodygraphql = \"body:graphql\" st* \":\" st* \"{\" nl* textblock tagend\n  bodygraphqlvars = \"body:graphql:vars\" st* \":\" st* \"{\" nl* textblock tagend\n  bodyformurlencoded = \"body:form-urlencoded\" st* \":\" st* dictionary\n  bodymultipart = \"body:multipart-form\" st* \":\" st* dictionary\n  bodyfile = \"body:file\" st* \":\" st* dictionary\n}`);\n\nconst astRequestAttribute = {\n  RequestFile(tags) {\n    if (!tags || !tags.ast || !tags.ast.length) {\n      return {};\n    }\n\n    return _.reduce(tags.ast,\n      (result, item) => {\n        return _.mergeWith(result, item, concatArrays);\n      },\n      {});\n  },\n  requesturl(_1, _2, _3, _4, value) {\n    return {\n      url: value.sourceString ? value.sourceString.trim() : ''\n    };\n  },\n  requestmethod(_1, _2, _3, _4, value) {\n    return {\n      method: value.sourceString ? value.sourceString.trim() : ''\n    };\n  },\n  requestmode(_1, _2, _3, _4, value) {\n    const modeValue = value.sourceString ? value.sourceString.trim() : '';\n    // Return body with the mode set\n    return {\n      body: {\n        mode: modeValue || 'none'\n      }\n    };\n  },\n  requestparamspath(_1, _2, _3, _4, dictionary) {\n    return {\n      params: mapRequestParams(dictionary.ast, 'path')\n    };\n  },\n  requestparamsquery(_1, _2, _3, _4, dictionary) {\n    return {\n      params: mapRequestParams(dictionary.ast, 'query')\n    };\n  },\n  requestheaders(_1, _2, _3, _4, dictionary) {\n    return {\n      headers: mapPairListToKeyValPairs(dictionary.ast)\n    };\n  },\n\n  // All body types from request side\n  bodyjson(_1, _2, _3, _4, _5, _6, textblock, _8) {\n    return {\n      body: {\n        mode: 'json',\n        json: outdentString(textblock.sourceString)\n      }\n    };\n  },\n  bodytext(_1, _2, _3, _4, _5, _6, textblock, _8) {\n    return {\n      body: {\n        mode: 'text',\n        text: outdentString(textblock.sourceString)\n      }\n    };\n  },\n  bodyxml(_1, _2, _3, _4, _5, _6, textblock, _8) {\n    return {\n      body: {\n        mode: 'xml',\n        xml: outdentString(textblock.sourceString)\n      }\n    };\n  },\n  bodysparql(_1, _2, _3, _4, _5, _6, textblock, _8) {\n    return {\n      body: {\n        mode: 'sparql',\n        sparql: outdentString(textblock.sourceString)\n      }\n    };\n  },\n  bodygraphql(_1, _2, _3, _4, _5, _6, textblock, _8) {\n    return {\n      body: {\n        mode: 'graphql',\n        graphql: {\n          query: outdentString(textblock.sourceString)\n        }\n      }\n    };\n  },\n  bodygraphqlvars(_1, _2, _3, _4, _5, _6, textblock, _8) {\n    return {\n      body: {\n        mode: 'graphql',\n        graphql: {\n          variables: outdentString(textblock.sourceString)\n        }\n      }\n    };\n  },\n  bodyformurlencoded(_1, _2, _3, _4, dictionary) {\n    return {\n      body: {\n        mode: 'formUrlEncoded',\n        formUrlEncoded: mapPairListToKeyValPairs(dictionary.ast)\n      }\n    };\n  },\n  bodymultipart(_1, _2, _3, _4, dictionary) {\n    return {\n      body: {\n        mode: 'multipartForm',\n        multipartForm: mapPairListToKeyValPairsMultipart(dictionary.ast)\n      }\n    };\n  },\n  bodyfile(_1, _2, _3, _4, dictionary) {\n    return {\n      body: {\n        mode: 'file',\n        file: mapPairListToKeyValPairsFile(dictionary.ast)\n      }\n    };\n  }\n};\n\nconst grammarSemantics = requestGrammar.createSemantics();\ngrammarSemantics.addAttribute('ast', { ...astBaseAttribute, ...astRequestAttribute });\n\nconst parseRequest = (input) => {\n  const match = requestGrammar.match(input);\n\n  if (match.succeeded()) {\n    let ast = grammarSemantics(match).ast;\n    return ast;\n  } else {\n    console.log('match failed', match);\n    throw new Error(match.message);\n  }\n};\n\nmodule.exports = parseRequest;\n"
  },
  {
    "path": "packages/bruno-lang/v2/src/example/response/bruToJson.js",
    "content": "const ohm = require('ohm-js');\nconst _ = require('lodash');\nconst { safeParseJson, outdentString } = require('../../utils');\nconst astBaseAttribute = require('../../common/attributes');\nconst { mapPairListToKeyValPairs } = require('../../common/semantic-utils');\n\n/**\n * Response Block Grammar for Bruno Examples\n *\n * Handles parsing of response blocks within example files.\n * Supports headers, status, and body parsing.\n */\nconst responseGrammar = ohm.grammar(`Response {\n  ResponseFile = responsecontent*\n  \n  nl = \"\\\\r\"? \"\\\\n\"\n  st = \" \" | \"\\\\t\"\n  stnl = st | nl\n  tagend = nl \"}\"\n  optionalnl = ~tagend nl\n  keychar = ~(tagend | st | nl | \":\") any\n  valuechar = ~(nl | tagend) any\n\n  // Multiline text block surrounded by '''\n  multilinetextblockdelimiter = \"'''\"\n  multilinetextblock = multilinetextblockdelimiter (~multilinetextblockdelimiter any)* multilinetextblockdelimiter st* contenttypeannotation?\n  contenttypeannotation = \"@contentType(\" (~\")\" any)* \")\"\n\n  // Dictionary Blocks\n  dictionary = st* \"{\" pairlist? tagend\n  pairlist = optionalnl* pair (~tagend nl pair)*\n  pair = st* (quoted_key | key) st* \":\" st* value st*\n  disable_char = \"~\"\n  quote_char = \"\\\\\"\"\n  esc_char = \"\\\\\\\\\"\n  esc_quote_char = esc_char quote_char\n  quoted_key_char = ~(quote_char | esc_quote_char | nl) any\n  quoted_key = disable_char? quote_char (esc_quote_char | quoted_key_char)* quote_char\n  key = keychar*\n  value = list | multilinetextblock | singlelinevalue\n  singlelinevalue = valuechar*\n\n  // List\n  list = st* \"[\" nl+ listitems? st* nl+ st* \"]\"\n  listitems = listitem (nl+ listitem)*\n  listitem = st+ (alnum | \"_\" | \"-\")+ st*\n\n  // Text Blocks\n  textblock = textline (~tagend nl textline)*\n  textline = textchar*\n  textchar = ~nl any\n\n  // Response content\n  responsecontent = responseheaders | responsestatus | responsebodyblock\n  responseheaders = \"headers\" st* \":\" st* dictionary nl*\n  responsestatus = \"status\" st* \":\" st* dictionary nl*\n  responsebodyblock = \"body\" st* \":\" st* \"{\" nl* responsebodyfields tagend\n  responsebodyfields = (responsebodytype | responsebodycontentvalue)*\n  responsebodytype = st* \"type\" st* \":\" st* valuechar* nl*\n  responsebodycontentvalue = st* \"content\" st* \":\" st* multilinetextblock\n}`);\n\nconst astResponseAttribute = {\n  ResponseFile(tags) {\n    if (!tags || !tags.ast || !tags.ast.length) {\n      return {};\n    }\n    // Filter out empty items and merge the results\n    const validItems = tags.ast.filter((item) => item && Object.keys(item).length > 0);\n    return _.reduce(validItems, (result, item) => {\n      return _.merge(result, item);\n    }, {});\n  },\n  responsecontent(content) {\n    return content.ast;\n  },\n  responseheaders(_1, _2, _3, _4, dictionary, _6) {\n    return { headers: mapPairListToKeyValPairs(dictionary.ast) };\n  },\n  responsestatus(_1, _2, _3, _4, dictionary, _6) {\n    const statusPairs = mapPairListToKeyValPairs(dictionary.ast, false);\n    return {\n      status: statusPairs.find((p) => p.name === 'code')?.value || 200,\n      statusText: statusPairs.find((p) => p.name === 'text')?.value || 'OK'\n    };\n  },\n  responsebodyblock(_1, _2, _3, _4, _5, _6, responsebodyfields, _8) {\n    // Extract type and content from the array structure\n    // responsebodyfields.ast is an array like [{ type: 'json' }, { content: \"'''...\" }]\n    const bodyData = {};\n\n    if (Array.isArray(responsebodyfields.ast)) {\n      responsebodyfields.ast.forEach((field) => {\n        if (field && typeof field === 'object') {\n          if (field.type !== undefined) {\n            bodyData.type = field.type;\n          }\n          if (field.content !== undefined) {\n            bodyData.content = field.content;\n          }\n        }\n      });\n    }\n\n    return {\n      body: bodyData\n    };\n  },\n  responsebodytype(_1, _2, _3, _4, _5, value, _7) {\n    return {\n      type: value.sourceString ? value.sourceString.trim() : ''\n    };\n  },\n  responsebodycontentvalue(_1, _2, _3, _4, _5, multilinetextblock) {\n    const multilineString = multilinetextblock.sourceString?.replace(/^'''|'''$/g, '').replace(/  $/g, '').replace(/^\\n|\\n$/g, '');\n\n    return {\n      content: outdentString(multilineString ?? '', 4)\n    };\n  }\n};\n\nconst grammarSemantics = responseGrammar.createSemantics();\ngrammarSemantics.addAttribute('ast', { ...astBaseAttribute, ...astResponseAttribute });\n\nconst parseResponse = (input) => {\n  const match = responseGrammar.match(input);\n\n  if (match.succeeded()) {\n    let ast = grammarSemantics(match).ast;\n    return ast;\n  } else {\n    console.log('match failed', match);\n    throw new Error(match.message);\n  }\n};\n\nmodule.exports = parseResponse;\n"
  },
  {
    "path": "packages/bruno-lang/v2/src/jsonToBru.js",
    "content": "const _ = require('lodash');\n\nconst { indentString, getValueString, getKeyString, getValueUrl } = require('./utils');\nconst jsonToExampleBru = require('./example/jsonToBru');\n\nconst enabled = (items = [], key = 'enabled') => items.filter((item) => item[key]);\nconst disabled = (items = [], key = 'enabled') => items.filter((item) => !item[key]);\n\n// remove the last line if two new lines are found\nconst stripLastLine = (text) => {\n  if (!text || !text.length) return text;\n\n  return text.replace(/(\\r?\\n)$/, '');\n};\n\nconst jsonToBru = (json) => {\n  const { meta, http, grpc, ws, params, headers, metadata, auth, body, script, tests, vars, assertions, settings, docs, examples } = json;\n\n  let bru = '';\n\n  if (meta) {\n    bru += 'meta {\\n';\n\n    const tags = meta.tags;\n    delete meta.tags;\n\n    for (const key in meta) {\n      bru += `  ${key}: ${meta[key]}\\n`;\n    }\n\n    if (tags && tags.length) {\n      bru += `  tags: [\\n`;\n      for (const tag of tags) {\n        bru += `    ${tag}\\n`;\n      }\n      bru += `  ]\\n`;\n    }\n\n    bru += '}\\n\\n';\n  }\n\n  if (http?.method) {\n    const { method, url, body, auth } = http;\n    const standardMethods = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace', 'connect']);\n\n    const isStandard = standardMethods.has(method);\n\n    bru += isStandard ? `${method} {` : `http {\\n  method: ${method}`;\n    bru += `\\n  url: ${getValueUrl(url)}`;\n\n    if (body?.length) {\n      bru += `\\n  body: ${body}`;\n    }\n\n    if (auth?.length) {\n      bru += `\\n  auth: ${auth}`;\n    }\n\n    bru += `\\n}\\n\\n`;\n  }\n\n  if (grpc && grpc.url) {\n    bru += `grpc {\n  url: ${grpc.url}`;\n\n    if (grpc.method && grpc.method.length) {\n      bru += `\n  method: ${grpc.method}`;\n    }\n\n    if (grpc.body && grpc.body.length) {\n      bru += `\n  body: ${grpc.body}`;\n    }\n\n    if (grpc.protoPath && grpc.protoPath.length) {\n      bru += `\n  protoPath: ${grpc.protoPath}`;\n    }\n\n    if (grpc.auth && grpc.auth.length) {\n      bru += `\n  auth: ${grpc.auth}`;\n    }\n\n    if (grpc.methodType && grpc.methodType.length) {\n      bru += `\n  methodType: ${grpc.methodType}`;\n    }\n\n    bru += `\n}\n\n`;\n  }\n\n  if (ws && ws.url) {\n    bru += `ws {\n  url: ${ws.url}`;\n\n    if (ws.body && ws.body.length) {\n      bru += `\n  body: ${ws.body}`;\n    }\n\n    if (ws.auth && ws.auth.length) {\n      bru += `\n  auth: ${ws.auth}`;\n    }\n\n    if (ws.methodType && ws.methodType.length) {\n      bru += `\n  methodType: ${ws.methodType}`;\n    }\n\n    bru += `\n}\n\n`;\n  }\n\n  if (params && params.length) {\n    const queryParams = params.filter((param) => param.type === 'query');\n    const pathParams = params.filter((param) => param.type === 'path');\n\n    if (queryParams.length) {\n      bru += 'params:query {';\n      if (enabled(queryParams).length) {\n        bru += `\\n${indentString(\n          enabled(queryParams)\n            .map((item) => `${getKeyString(item.name)}: ${getValueString(item.value)}`)\n            .join('\\n')\n        )}`;\n      }\n\n      if (disabled(queryParams).length) {\n        bru += `\\n${indentString(\n          disabled(queryParams)\n            .map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)\n            .join('\\n')\n        )}`;\n      }\n\n      bru += '\\n}\\n\\n';\n    }\n\n    if (pathParams.length) {\n      bru += 'params:path {';\n\n      bru += `\\n${indentString(pathParams.map((item) => `${item.name}: ${getValueString(item.value)}`).join('\\n'))}`;\n\n      bru += '\\n}\\n\\n';\n    }\n  }\n\n  if (headers && headers.length) {\n    bru += 'headers {';\n    if (enabled(headers).length) {\n      bru += `\\n${indentString(\n        enabled(headers)\n          .map((item) => `${getKeyString(item.name)}: ${getValueString(item.value)}`)\n          .join('\\n')\n      )}`;\n    }\n\n    if (disabled(headers).length) {\n      bru += `\\n${indentString(\n        disabled(headers)\n          .map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)\n          .join('\\n')\n      )}`;\n    }\n\n    bru += '\\n}\\n\\n';\n  }\n\n  if (metadata && metadata.length) {\n    bru += 'metadata {';\n    if (enabled(metadata).length) {\n      bru += `\\n${indentString(\n        enabled(metadata)\n          .map((item) => `${item.name}: ${getValueString(item.value)}`)\n          .join('\\n')\n      )}`;\n    }\n\n    if (disabled(metadata).length) {\n      bru += `\\n${indentString(\n        disabled(metadata)\n          .map((item) => `~${item.name}: ${getValueString(item.value)}`)\n          .join('\\n')\n      )}`;\n    }\n\n    bru += '\\n}\\n\\n';\n  }\n\n  if (auth && auth.awsv4) {\n    bru += `auth:awsv4 {\n${indentString(`accessKeyId: ${auth?.awsv4?.accessKeyId || ''}`)}\n${indentString(`secretAccessKey: ${auth?.awsv4?.secretAccessKey || ''}`)}\n${indentString(`sessionToken: ${auth?.awsv4?.sessionToken || ''}`)}\n${indentString(`service: ${auth?.awsv4?.service || ''}`)}\n${indentString(`region: ${auth?.awsv4?.region || ''}`)}\n${indentString(`profileName: ${auth?.awsv4?.profileName || ''}`)}\n}\n\n`;\n  }\n\n  if (auth && auth.basic) {\n    bru += `auth:basic {\n${indentString(`username: ${auth?.basic?.username || ''}`)}\n${indentString(`password: ${auth?.basic?.password || ''}`)}\n}\n\n`;\n  }\n\n  if (auth && auth.wsse) {\n    bru += `auth:wsse {\n${indentString(`username: ${auth?.wsse?.username || ''}`)}\n${indentString(`password: ${auth?.wsse?.password || ''}`)}\n}\n\n`;\n  }\n\n  if (auth && auth.bearer) {\n    bru += `auth:bearer {\n${indentString(`token: ${auth?.bearer?.token || ''}`)}\n}\n\n`;\n  }\n\n  if (auth && auth.digest) {\n    bru += `auth:digest {\n${indentString(`username: ${auth?.digest?.username || ''}`)}\n${indentString(`password: ${auth?.digest?.password || ''}`)}\n}\n\n`;\n  }\n\n  if (auth && auth.ntlm) {\n    bru += `auth:ntlm {\n${indentString(`username: ${auth?.ntlm?.username || ''}`)}\n${indentString(`password: ${auth?.ntlm?.password || ''}`)}\n${indentString(`domain: ${auth?.ntlm?.domain || ''}`)}\n\n}\n\n`;\n  }\n\n  if (auth && auth.oauth2) {\n    switch (auth?.oauth2?.grantType) {\n      case 'password':\n        bru += `auth:oauth2 {\n${indentString(`grant_type: password`)}\n${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}\n${indentString(`refresh_token_url: ${auth?.oauth2?.refreshTokenUrl || ''}`)}\n${indentString(`username: ${auth?.oauth2?.username || ''}`)}\n${indentString(`password: ${auth?.oauth2?.password || ''}`)}\n${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}\n${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}\n${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}\n${indentString(`credentials_placement: ${auth?.oauth2?.credentialsPlacement || ''}`)}\n${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}\n${indentString(`token_source: ${auth?.oauth2?.tokenSource || 'access_token'}`)}\n${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${\n  auth?.oauth2?.tokenPlacement == 'header' ? '\\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''\n}${\n  auth?.oauth2?.tokenPlacement !== 'header' ? '\\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''\n}\n${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toString()}`)}\n${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).toString()}`)}\n}\n\n`;\n        break;\n      case 'authorization_code':\n        bru += `auth:oauth2 {\n${indentString(`grant_type: authorization_code`)}\n${indentString(`callback_url: ${auth?.oauth2?.callbackUrl || ''}`)}\n${indentString(`authorization_url: ${auth?.oauth2?.authorizationUrl || ''}`)}\n${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}\n${indentString(`refresh_token_url: ${auth?.oauth2?.refreshTokenUrl || ''}`)}\n${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}\n${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}\n${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}\n${indentString(`state: ${auth?.oauth2?.state || ''}`)}\n${indentString(`pkce: ${(auth?.oauth2?.pkce || false).toString()}`)}\n${indentString(`credentials_placement: ${auth?.oauth2?.credentialsPlacement || ''}`)}\n${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}\n${indentString(`token_source: ${auth?.oauth2?.tokenSource || 'access_token'}`)}\n${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${\n  auth?.oauth2?.tokenPlacement == 'header' ? '\\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''\n}${\n  auth?.oauth2?.tokenPlacement !== 'header' ? '\\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''\n}\n${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toString()}`)}\n${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).toString()}`)}\n}\n\n`;\n        break;\n      case 'client_credentials':\n        bru += `auth:oauth2 {\n${indentString(`grant_type: client_credentials`)}\n${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}\n${indentString(`refresh_token_url: ${auth?.oauth2?.refreshTokenUrl || ''}`)}\n${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}\n${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}\n${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}\n${indentString(`credentials_placement: ${auth?.oauth2?.credentialsPlacement || ''}`)}\n${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}\n${indentString(`token_source: ${auth?.oauth2?.tokenSource || 'access_token'}`)}\n${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${\n  auth?.oauth2?.tokenPlacement == 'header' ? '\\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''\n}${\n  auth?.oauth2?.tokenPlacement !== 'header' ? '\\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''\n}\n${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toString()}`)}\n${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).toString()}`)}\n}\n\n`;\n        break;\n      case 'implicit':\n        bru += `auth:oauth2 {\n${indentString(`grant_type: implicit`)}\n${indentString(`callback_url: ${auth?.oauth2?.callbackUrl || ''}`)}\n${indentString(`authorization_url: ${auth?.oauth2?.authorizationUrl || ''}`)}\n${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}\n${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}\n${indentString(`state: ${auth?.oauth2?.state || ''}`)}\n${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}\n${indentString(`token_source: ${auth?.oauth2?.tokenSource || 'access_token'}`)}\n${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${\n  auth?.oauth2?.tokenPlacement == 'header' ? '\\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''\n}${\n  auth?.oauth2?.tokenPlacement !== 'header' ? '\\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''\n}\n${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toString()}`)}\n}\n\n`;\n        break;\n    }\n\n    if (auth?.oauth2?.additionalParameters) {\n      const { authorization: authorizationParams, token: tokenParams, refresh: refreshParams } = auth?.oauth2?.additionalParameters;\n      const authorizationHeaders = authorizationParams?.filter((p) => p?.sendIn == 'headers');\n      if (authorizationHeaders?.length) {\n        bru += `auth:oauth2:additional_params:auth_req:headers {\n${indentString(\n  authorizationHeaders\n    .filter((item) => item?.name?.length)\n    .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)\n    .join('\\n')\n)}\n}\n\n`;\n      }\n      const authorizationQueryParams = authorizationParams?.filter((p) => p?.sendIn == 'queryparams');\n      if (authorizationQueryParams?.length) {\n        bru += `auth:oauth2:additional_params:auth_req:queryparams {\n${indentString(\n  authorizationQueryParams\n    .filter((item) => item?.name?.length)\n    .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)\n    .join('\\n')\n)}\n}\n\n`;\n      }\n      const tokenHeaders = tokenParams?.filter((p) => p?.sendIn == 'headers');\n      if (tokenHeaders?.length) {\n        bru += `auth:oauth2:additional_params:access_token_req:headers {\n${indentString(\n  tokenHeaders\n    .filter((item) => item?.name?.length)\n    .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)\n    .join('\\n')\n)}\n}\n\n`;\n      }\n      const tokenQueryParams = tokenParams?.filter((p) => p?.sendIn == 'queryparams');\n      if (tokenQueryParams?.length) {\n        bru += `auth:oauth2:additional_params:access_token_req:queryparams {\n${indentString(\n  tokenQueryParams\n    .filter((item) => item?.name?.length)\n    .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)\n    .join('\\n')\n)}\n}\n\n`;\n      }\n      const tokenBodyValues = tokenParams?.filter((p) => p?.sendIn == 'body');\n      if (tokenBodyValues?.length) {\n        bru += `auth:oauth2:additional_params:access_token_req:body {\n${indentString(\n  tokenBodyValues\n    .filter((item) => item?.name?.length)\n    .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)\n    .join('\\n')\n)}\n}\n\n`;\n      }\n      const refreshHeaders = refreshParams?.filter((p) => p?.sendIn == 'headers');\n      if (refreshHeaders?.length) {\n        bru += `auth:oauth2:additional_params:refresh_token_req:headers {\n${indentString(\n  refreshHeaders\n    .filter((item) => item?.name?.length)\n    .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)\n    .join('\\n')\n)}\n}\n\n`;\n      }\n      const refreshQueryParams = refreshParams?.filter((p) => p?.sendIn == 'queryparams');\n      if (refreshQueryParams?.length) {\n        bru += `auth:oauth2:additional_params:refresh_token_req:queryparams {\n${indentString(\n  refreshQueryParams\n    .filter((item) => item?.name?.length)\n    .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)\n    .join('\\n')\n)}\n}\n\n`;\n      }\n      const refreshBodyValues = refreshParams?.filter((p) => p?.sendIn == 'body');\n      if (refreshBodyValues?.length) {\n        bru += `auth:oauth2:additional_params:refresh_token_req:body {\n${indentString(\n  refreshBodyValues\n    .filter((item) => item?.name?.length)\n    .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)\n    .join('\\n')\n)}\n}\n\n`;\n      }\n    }\n  }\n\n  if (auth && auth.apikey) {\n    bru += `auth:apikey {\n${indentString(`key: ${auth?.apikey?.key || ''}`)}\n${indentString(`value: ${auth?.apikey?.value || ''}`)}\n${indentString(`placement: ${auth?.apikey?.placement || ''}`)}\n}\n\n`;\n  }\n\n  if (body && body.json && body.json.length) {\n    bru += `body:json {\n${indentString(body.json)}\n}\n\n`;\n  }\n\n  if (body && body.text && body.text.length) {\n    bru += `body:text {\n${indentString(body.text)}\n}\n\n`;\n  }\n\n  if (body && body.xml && body.xml.length) {\n    bru += `body:xml {\n${indentString(body.xml)}\n}\n\n`;\n  }\n\n  if (body && body.sparql && body.sparql.length) {\n    bru += `body:sparql {\n${indentString(body.sparql)}\n}\n\n`;\n  }\n\n  if (body && body.formUrlEncoded && body.formUrlEncoded.length) {\n    bru += `body:form-urlencoded {\\n`;\n\n    if (enabled(body.formUrlEncoded).length) {\n      const enabledValues = enabled(body.formUrlEncoded)\n        .map((item) => `${getKeyString(item.name)}: ${getValueString(item.value)}`)\n        .join('\\n');\n      bru += `${indentString(enabledValues)}\\n`;\n    }\n\n    if (disabled(body.formUrlEncoded).length) {\n      const disabledValues = disabled(body.formUrlEncoded)\n        .map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)\n        .join('\\n');\n      bru += `${indentString(disabledValues)}\\n`;\n    }\n\n    bru += '}\\n\\n';\n  }\n\n  if (body && body.multipartForm && body.multipartForm.length) {\n    bru += `body:multipart-form {`;\n    const multipartForms = enabled(body.multipartForm).concat(disabled(body.multipartForm));\n\n    if (multipartForms.length) {\n      bru += `\\n${indentString(\n        multipartForms\n          .map((item) => {\n            const enabled = item.enabled ? '' : '~';\n            const contentType\n              = item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : '';\n\n            if (item.type === 'text') {\n              return `${enabled}${getKeyString(item.name)}: ${getValueString(item.value)}${contentType}`;\n            }\n\n            if (item.type === 'file') {\n              const filepaths = Array.isArray(item.value) ? item.value : [];\n              const filestr = filepaths.join('|');\n\n              const value = `@file(${filestr})`;\n              return `${enabled}${getKeyString(item.name)}: ${value}${contentType}`;\n            }\n          })\n          .join('\\n')\n      )}`;\n    }\n\n    bru += '\\n}\\n\\n';\n  }\n\n  if (body && body.file && body.file.length) {\n    bru += `body:file {`;\n    const files = enabled(body.file, 'selected').concat(disabled(body.file, 'selected'));\n\n    if (files.length) {\n      bru += `\\n${indentString(\n        files\n          .map((item) => {\n            const selected = item.selected ? '' : '~';\n            const contentType\n              = item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : '';\n            const filePath = item.filePath || '';\n            const value = `@file(${filePath})`;\n            const itemName = 'file';\n            return `${selected}${itemName}: ${value}${contentType}`;\n          })\n          .join('\\n')\n      )}`;\n    }\n\n    bru += '\\n}\\n\\n';\n  }\n\n  if (body && body.graphql && body.graphql.query) {\n    bru += `body:graphql {\\n`;\n    bru += `${indentString(body.graphql.query)}`;\n    bru += '\\n}\\n\\n';\n  }\n\n  if (body && body.graphql && body.graphql.variables) {\n    bru += `body:graphql:vars {\\n`;\n    bru += `${indentString(body.graphql.variables)}`;\n    bru += '\\n}\\n\\n';\n  }\n\n  if (body && body.grpc) {\n    // Convert each gRPC message to a separate body:grpc block\n    if (Array.isArray(body.grpc)) {\n      body.grpc.forEach((m) => {\n        const { name, content } = m;\n\n        bru += `body:grpc {\\n`;\n\n        bru += `${indentString(`name: ${getValueString(name)}`)}\\n`;\n\n        // Convert content to JSON string if it's an object\n        let jsonValue = typeof content === 'object' ? JSON.stringify(content, null, 2) : content || '{}';\n\n        // Wrap content with triple quotes for multiline support, without extra indentation\n        bru += `${indentString(`content: '''\\n${indentString(jsonValue)}\\n'''`)}\\n`;\n        bru += '}\\n\\n';\n      });\n    }\n  }\n\n  if (body && body.ws) {\n    // Convert each ws message to a separate body:ws block\n    if (Array.isArray(body.ws)) {\n      body.ws.forEach((message) => {\n        const { name, content, type = '' } = message;\n\n        bru += `body:ws {\\n`;\n\n        bru += `${indentString(`name: ${getValueString(name)}`)}\\n`;\n        if (type.length) {\n          bru += `${indentString(`type: ${getValueString(type)}`)}\\n`;\n        }\n\n        // Convert content to JSON string if it's an object\n        let contentValue = typeof content === 'object' ? JSON.stringify(content, null, 2) : content || '{}';\n\n        // Wrap content with triple quotes for multiline support, without extra indentation\n        bru += `${indentString(`content: '''\\n${indentString(contentValue)}\\n'''`)}\\n`;\n        bru += '}\\n\\n';\n      });\n    }\n  }\n\n  let reqvars = _.get(vars, 'req');\n  let resvars = _.get(vars, 'res');\n  if (reqvars && reqvars.length) {\n    const varsEnabled = _.filter(reqvars, (v) => v.enabled && !v.local);\n    const varsDisabled = _.filter(reqvars, (v) => !v.enabled && !v.local);\n    const varsLocalEnabled = _.filter(reqvars, (v) => v.enabled && v.local);\n    const varsLocalDisabled = _.filter(reqvars, (v) => !v.enabled && v.local);\n\n    bru += `vars:pre-request {`;\n\n    if (varsEnabled.length) {\n      bru += `\\n${indentString(varsEnabled.map((item) => `${item.name}: ${getValueString(item.value)}`).join('\\n'))}`;\n    }\n\n    if (varsLocalEnabled.length) {\n      bru += `\\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${getValueString(item.value)}`).join('\\n'))}`;\n    }\n\n    if (varsDisabled.length) {\n      bru += `\\n${indentString(varsDisabled.map((item) => `~${item.name}: ${getValueString(item.value)}`).join('\\n'))}`;\n    }\n\n    if (varsLocalDisabled.length) {\n      bru += `\\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${getValueString(item.value)}`).join('\\n'))}`;\n    }\n\n    bru += '\\n}\\n\\n';\n  }\n  if (resvars && resvars.length) {\n    const varsEnabled = _.filter(resvars, (v) => v.enabled && !v.local);\n    const varsDisabled = _.filter(resvars, (v) => !v.enabled && !v.local);\n    const varsLocalEnabled = _.filter(resvars, (v) => v.enabled && v.local);\n    const varsLocalDisabled = _.filter(resvars, (v) => !v.enabled && v.local);\n\n    bru += `vars:post-response {`;\n\n    if (varsEnabled.length) {\n      bru += `\\n${indentString(varsEnabled.map((item) => `${item.name}: ${getValueString(item.value)}`).join('\\n'))}`;\n    }\n\n    if (varsLocalEnabled.length) {\n      bru += `\\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${getValueString(item.value)}`).join('\\n'))}`;\n    }\n\n    if (varsDisabled.length) {\n      bru += `\\n${indentString(varsDisabled.map((item) => `~${item.name}: ${getValueString(item.value)}`).join('\\n'))}`;\n    }\n\n    if (varsLocalDisabled.length) {\n      bru += `\\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${getValueString(item.value)}`).join('\\n'))}`;\n    }\n\n    bru += '\\n}\\n\\n';\n  }\n\n  if (assertions && assertions.length) {\n    bru += `assert {`;\n\n    if (enabled(assertions).length) {\n      bru += `\\n${indentString(\n        enabled(assertions)\n          .map((item) => `${item.name}: ${getValueString(item.value)}`)\n          .join('\\n')\n      )}`;\n    }\n\n    if (disabled(assertions).length) {\n      bru += `\\n${indentString(\n        disabled(assertions)\n          .map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)\n          .join('\\n')\n      )}`;\n    }\n\n    bru += '\\n}\\n\\n';\n  }\n\n  if (script && script.req && script.req.length) {\n    bru += `script:pre-request {\n${indentString(script.req)}\n}\n\n`;\n  }\n\n  if (script && script.res && script.res.length) {\n    bru += `script:post-response {\n${indentString(script.res)}\n}\n\n`;\n  }\n\n  if (tests && tests.length) {\n    bru += `tests {\n${indentString(tests)}\n}\n\n`;\n  }\n\n  if (settings && Object.keys(settings).length) {\n    bru += 'settings {\\n';\n    for (const key in settings) {\n      bru += `  ${key}: ${settings[key]}\\n`;\n    }\n    bru += '}\\n\\n';\n  }\n\n  if (docs && docs.length) {\n    bru += `docs {\n${indentString(docs)}\n}\n\n`;\n  }\n\n  if (examples && examples.length) {\n    examples.forEach((example) => {\n      const bruExample = jsonToExampleBru(example);\n      bru += `example {\\n${indentString(bruExample)}\\n}\\n\\n`;\n    });\n  }\n\n  return stripLastLine(bru);\n};\n\nmodule.exports = jsonToBru;\nmodule.exports.jsonToExampleBru = jsonToExampleBru;\n\n// alternative to writing the below code to avoid undefined\n"
  },
  {
    "path": "packages/bruno-lang/v2/src/jsonToCollectionBru.js",
    "content": "const _ = require('lodash');\n\nconst { indentString, getValueString, getKeyString } = require('./utils');\n\nconst enabled = (items = []) => items.filter((item) => item.enabled);\nconst disabled = (items = []) => items.filter((item) => !item.enabled);\n\n// remove the last line if two new lines are found\nconst stripLastLine = (text) => {\n  if (!text || !text.length) return text;\n\n  return text.replace(/(\\r?\\n)$/, '');\n};\n\nconst jsonToCollectionBru = (json) => {\n  const { meta, query, headers, auth, script, tests, vars, docs } = json;\n\n  let bru = '';\n\n  if (meta) {\n    bru += 'meta {\\n';\n    for (const key in meta) {\n      bru += `  ${key}: ${meta[key]}\\n`;\n    }\n    bru += '}\\n\\n';\n  }\n\n  if (query && query.length) {\n    bru += 'query {';\n    if (enabled(query).length) {\n      bru += `\\n${indentString(\n        enabled(query)\n          .map((item) => `${getKeyString(item.name)}: ${getValueString(item.value)}`)\n          .join('\\n')\n      )}`;\n    }\n\n    if (disabled(query).length) {\n      bru += `\\n${indentString(\n        disabled(query)\n          .map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)\n          .join('\\n')\n      )}`;\n    }\n\n    bru += '\\n}\\n\\n';\n  }\n\n  if (headers && headers.length) {\n    bru += 'headers {';\n    if (enabled(headers).length) {\n      bru += `\\n${indentString(\n        enabled(headers)\n          .map((item) => `${getKeyString(item.name)}: ${getValueString(item.value)}`)\n          .join('\\n')\n      )}`;\n    }\n\n    if (disabled(headers).length) {\n      bru += `\\n${indentString(\n        disabled(headers)\n          .map((item) => `~${getKeyString(item.name)}: ${getValueString(item.value)}`)\n          .join('\\n')\n      )}`;\n    }\n\n    bru += '\\n}\\n\\n';\n  }\n\n  if (auth && auth.mode) {\n    bru += `auth {\n${indentString(`mode: ${auth.mode}`)}\n}\n\n`;\n  }\n\n  if (auth && auth.awsv4) {\n    bru += `auth:awsv4 {\n${indentString(`accessKeyId: ${auth.awsv4.accessKeyId}`)}\n${indentString(`secretAccessKey: ${auth.awsv4.secretAccessKey}`)}\n${indentString(`sessionToken: ${auth.awsv4.sessionToken}`)}\n${indentString(`service: ${auth.awsv4.service}`)}\n${indentString(`region: ${auth.awsv4.region}`)}\n${indentString(`profileName: ${auth.awsv4.profileName}`)}\n}\n\n`;\n  }\n\n  if (auth && auth.basic) {\n    bru += `auth:basic {\n${indentString(`username: ${auth.basic.username}`)}\n${indentString(`password: ${auth.basic.password}`)}\n}\n\n`;\n  }\n\n  if (auth && auth.wsse) {\n    bru += `auth:wsse {\n${indentString(`username: ${auth.wsse.username}`)}\n${indentString(`password: ${auth.wsse.password}`)}\n}\n\n`;\n  }\n\n  if (auth && auth.bearer) {\n    bru += `auth:bearer {\n${indentString(`token: ${auth.bearer.token}`)}\n}\n\n`;\n  }\n\n  if (auth && auth.digest) {\n    bru += `auth:digest {\n${indentString(`username: ${auth.digest.username}`)}\n${indentString(`password: ${auth.digest.password}`)}\n}\n\n`;\n  }\n\n  if (auth && auth.ntlm) {\n    bru += `auth:ntlm {\n${indentString(`username: ${auth.ntlm.username}`)}\n${indentString(`password: ${auth.ntlm.password}`)}\n${indentString(`domain: ${auth.ntlm.domain}`)}\n\n}\n\n`;\n  }\n\n  if (auth && auth.apikey) {\n    bru += `auth:apikey {\n${indentString(`key: ${auth?.apikey?.key || ''}`)}\n${indentString(`value: ${auth?.apikey?.value || ''}`)}\n${indentString(`placement: ${auth?.apikey?.placement || ''}`)}\n}\n`;\n  }\n\n  if (auth && auth.oauth2) {\n    switch (auth?.oauth2?.grantType) {\n      case 'password':\n        bru += `auth:oauth2 {\n${indentString(`grant_type: password`)}\n${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}\n${indentString(`refresh_token_url: ${auth?.oauth2?.refreshTokenUrl || ''}`)}\n${indentString(`username: ${auth?.oauth2?.username || ''}`)}\n${indentString(`password: ${auth?.oauth2?.password || ''}`)}\n${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}\n${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}\n${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}\n${indentString(`credentials_placement: ${auth?.oauth2?.credentialsPlacement || ''}`)}\n${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}\n${indentString(`token_source: ${auth?.oauth2?.tokenSource || 'access_token'}`)}\n${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${\n  auth?.oauth2?.tokenPlacement == 'header' ? '\\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''\n}${\n  auth?.oauth2?.tokenPlacement !== 'header' ? '\\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''\n}\n${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toString()}`)}\n${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).toString()}`)}\n}\n\n`;\n        break;\n      case 'authorization_code':\n        bru += `auth:oauth2 {\n${indentString(`grant_type: authorization_code`)}\n${indentString(`callback_url: ${auth?.oauth2?.callbackUrl || ''}`)}\n${indentString(`authorization_url: ${auth?.oauth2?.authorizationUrl || ''}`)}\n${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}\n${indentString(`refresh_token_url: ${auth?.oauth2?.refreshTokenUrl || ''}`)}\n${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}\n${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}\n${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}\n${indentString(`state: ${auth?.oauth2?.state || ''}`)}\n${indentString(`pkce: ${(auth?.oauth2?.pkce || false).toString()}`)}\n${indentString(`credentials_placement: ${auth?.oauth2?.credentialsPlacement || ''}`)}\n${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}\n${indentString(`token_source: ${auth?.oauth2?.tokenSource || 'access_token'}`)}\n${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${\n  auth?.oauth2?.tokenPlacement == 'header' ? '\\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''\n}${\n  auth?.oauth2?.tokenPlacement !== 'header' ? '\\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''\n}\n${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toString()}`)}\n${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).toString()}`)}\n}\n\n`;\n        break;\n      case 'implicit':\n        bru += `auth:oauth2 {\n${indentString(`grant_type: implicit`)}\n${indentString(`callback_url: ${auth?.oauth2?.callbackUrl || ''}`)}\n${indentString(`authorization_url: ${auth?.oauth2?.authorizationUrl || ''}`)}\n${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}\n${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}\n${indentString(`state: ${auth?.oauth2?.state || ''}`)}\n${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}\n${indentString(`token_source: ${auth?.oauth2?.tokenSource || 'access_token'}`)}\n${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${\n  auth?.oauth2?.tokenPlacement == 'header' ? '\\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''\n}${\n  auth?.oauth2?.tokenPlacement !== 'header' ? '\\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''\n}\n${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toString()}`)}\n}\n\n`;\n        break;\n      case 'client_credentials':\n        bru += `auth:oauth2 {\n${indentString(`grant_type: client_credentials`)}\n${indentString(`access_token_url: ${auth?.oauth2?.accessTokenUrl || ''}`)}\n${indentString(`refresh_token_url: ${auth?.oauth2?.refreshTokenUrl || ''}`)}\n${indentString(`client_id: ${auth?.oauth2?.clientId || ''}`)}\n${indentString(`client_secret: ${auth?.oauth2?.clientSecret || ''}`)}\n${indentString(`scope: ${auth?.oauth2?.scope || ''}`)}\n${indentString(`credentials_placement: ${auth?.oauth2?.credentialsPlacement || ''}`)}\n${indentString(`credentials_id: ${auth?.oauth2?.credentialsId || ''}`)}\n${indentString(`token_source: ${auth?.oauth2?.tokenSource || 'access_token'}`)}\n${indentString(`token_placement: ${auth?.oauth2?.tokenPlacement || ''}`)}${\n  auth?.oauth2?.tokenPlacement == 'header' ? '\\n' + indentString(`token_header_prefix: ${auth?.oauth2?.tokenHeaderPrefix || ''}`) : ''\n}${\n  auth?.oauth2?.tokenPlacement !== 'header' ? '\\n' + indentString(`token_query_key: ${auth?.oauth2?.tokenQueryKey || ''}`) : ''\n}\n${indentString(`auto_fetch_token: ${(auth?.oauth2?.autoFetchToken ?? true).toString()}`)}\n${indentString(`auto_refresh_token: ${(auth?.oauth2?.autoRefreshToken ?? false).toString()}`)}\n}\n\n`;\n        break;\n    }\n\n    if (auth?.oauth2?.additionalParameters) {\n      const { authorization: authorizationParams, token: tokenParams, refresh: refreshParams } = auth?.oauth2?.additionalParameters;\n      const authorizationHeaders = authorizationParams?.filter((p) => p?.sendIn == 'headers');\n      if (authorizationHeaders?.length) {\n        bru += `auth:oauth2:additional_params:auth_req:headers {\n${indentString(\n  authorizationHeaders\n    .filter((item) => item?.name?.length)\n    .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)\n    .join('\\n')\n)}\n}\n\n`;\n      }\n      const authorizationQueryParams = authorizationParams?.filter((p) => p?.sendIn == 'queryparams');\n      if (authorizationQueryParams?.length) {\n        bru += `auth:oauth2:additional_params:auth_req:queryparams {\n${indentString(\n  authorizationQueryParams\n    .filter((item) => item?.name?.length)\n    .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)\n    .join('\\n')\n)}\n}\n\n`;\n      }\n      const tokenHeaders = tokenParams?.filter((p) => p?.sendIn == 'headers');\n      if (tokenHeaders?.length) {\n        bru += `auth:oauth2:additional_params:access_token_req:headers {\n${indentString(\n  tokenHeaders\n    .filter((item) => item?.name?.length)\n    .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)\n    .join('\\n')\n)}\n}\n\n`;\n      }\n      const tokenQueryParams = tokenParams?.filter((p) => p?.sendIn == 'queryparams');\n      if (tokenQueryParams?.length) {\n        bru += `auth:oauth2:additional_params:access_token_req:queryparams {\n${indentString(\n  tokenQueryParams\n    .filter((item) => item?.name?.length)\n    .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)\n    .join('\\n'))}\n}\n\n`;\n      }\n      const tokenBodyValues = tokenParams?.filter((p) => p?.sendIn == 'body');\n      if (tokenBodyValues?.length) {\n        bru += `auth:oauth2:additional_params:access_token_req:body {\n${indentString(\n  tokenBodyValues\n    .filter((item) => item?.name?.length)\n    .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)\n    .join('\\n')\n)}\n}\n\n`;\n      }\n      const refreshHeaders = refreshParams?.filter((p) => p?.sendIn == 'headers');\n      if (refreshHeaders?.length) {\n        bru += `auth:oauth2:additional_params:refresh_token_req:headers {\n${indentString(\n  refreshHeaders\n    .filter((item) => item?.name?.length)\n    .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)\n    .join('\\n')\n)}\n}\n\n`;\n      }\n      const refreshQueryParams = refreshParams?.filter((p) => p?.sendIn == 'queryparams');\n      if (refreshQueryParams?.length) {\n        bru += `auth:oauth2:additional_params:refresh_token_req:queryparams {\n${indentString(\n  refreshQueryParams\n    .filter((item) => item?.name?.length)\n    .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)\n    .join('\\n')\n)}\n}\n\n`;\n      }\n      const refreshBodyValues = refreshParams?.filter((p) => p?.sendIn == 'body');\n      if (refreshBodyValues?.length) {\n        bru += `auth:oauth2:additional_params:refresh_token_req:body {\n${indentString(\n  refreshBodyValues\n    .filter((item) => item?.name?.length)\n    .map((item) => `${item.enabled ? '' : '~'}${getKeyString(item.name)}: ${getValueString(item.value)}`)\n    .join('\\n')\n)}\n}\n\n`;\n      }\n    }\n  }\n\n  let reqvars = _.get(vars, 'req');\n  let resvars = _.get(vars, 'res');\n  if (reqvars && reqvars.length) {\n    const varsEnabled = _.filter(reqvars, (v) => v.enabled && !v.local);\n    const varsDisabled = _.filter(reqvars, (v) => !v.enabled && !v.local);\n    const varsLocalEnabled = _.filter(reqvars, (v) => v.enabled && v.local);\n    const varsLocalDisabled = _.filter(reqvars, (v) => !v.enabled && v.local);\n\n    bru += `vars:pre-request {`;\n\n    if (varsEnabled.length) {\n      bru += `\\n${indentString(varsEnabled.map((item) => `${item.name}: ${getValueString(item.value)}`).join('\\n'))}`;\n    }\n\n    if (varsLocalEnabled.length) {\n      bru += `\\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${getValueString(item.value)}`).join('\\n'))}`;\n    }\n\n    if (varsDisabled.length) {\n      bru += `\\n${indentString(varsDisabled.map((item) => `~${item.name}: ${getValueString(item.value)}`).join('\\n'))}`;\n    }\n\n    if (varsLocalDisabled.length) {\n      bru += `\\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${getValueString(item.value)}`).join('\\n'))}`;\n    }\n\n    bru += '\\n}\\n\\n';\n  }\n  if (resvars && resvars.length) {\n    const varsEnabled = _.filter(resvars, (v) => v.enabled && !v.local);\n    const varsDisabled = _.filter(resvars, (v) => !v.enabled && !v.local);\n    const varsLocalEnabled = _.filter(resvars, (v) => v.enabled && v.local);\n    const varsLocalDisabled = _.filter(resvars, (v) => !v.enabled && v.local);\n\n    bru += `vars:post-response {`;\n\n    if (varsEnabled.length) {\n      bru += `\\n${indentString(varsEnabled.map((item) => `${item.name}: ${getValueString(item.value)}`).join('\\n'))}`;\n    }\n\n    if (varsLocalEnabled.length) {\n      bru += `\\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${getValueString(item.value)}`).join('\\n'))}`;\n    }\n\n    if (varsDisabled.length) {\n      bru += `\\n${indentString(varsDisabled.map((item) => `~${item.name}: ${getValueString(item.value)}`).join('\\n'))}`;\n    }\n\n    if (varsLocalDisabled.length) {\n      bru += `\\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${getValueString(item.value)}`).join('\\n'))}`;\n    }\n\n    bru += '\\n}\\n\\n';\n  }\n\n  if (script && script.req && script.req.length) {\n    bru += `script:pre-request {\n${indentString(script.req)}\n}\n\n`;\n  }\n\n  if (script && script.res && script.res.length) {\n    bru += `script:post-response {\n${indentString(script.res)}\n}\n\n`;\n  }\n\n  if (tests && tests.length) {\n    bru += `tests {\n${indentString(tests)}\n}\n\n`;\n  }\n\n  if (docs && docs.length) {\n    bru += `docs {\n${indentString(docs)}\n}\n\n`;\n  }\n\n  return stripLastLine(bru);\n};\n\nmodule.exports = jsonToCollectionBru;\n"
  },
  {
    "path": "packages/bruno-lang/v2/src/jsonToEnv.js",
    "content": "const _ = require('lodash');\nconst { getValueString, indentString } = require('./utils');\n\nconst envToJson = (json) => {\n  const variables = _.get(json, 'variables', []);\n  const color = _.get(json, 'color', null);\n\n  const vars = variables\n    .filter((variable) => !variable.secret)\n    .map((variable) => {\n      const { name, value, enabled } = variable;\n      const prefix = enabled ? '' : '~';\n\n      return indentString(`${prefix}${name}: ${getValueString(value)}`);\n    });\n\n  const secretVars = variables\n    .filter((variable) => variable.secret)\n    .map((variable) => {\n      const { name, enabled } = variable;\n      const prefix = enabled ? '' : '~';\n      return indentString(`${prefix}${name}`);\n    });\n\n  let output = '';\n\n  if (!variables || !variables.length) {\n    output += `vars {\n}\n`;\n  }\n\n  if (vars.length) {\n    output += `vars {\n${vars.join('\\n')}\n}\n`;\n  }\n\n  if (secretVars.length) {\n    output += `vars:secret [\n${secretVars.join(',\\n')}\n]\n`;\n  }\n  if (color) {\n    output += `color: ${color}\n`;\n  }\n\n  return output;\n};\n\nmodule.exports = envToJson;\n"
  },
  {
    "path": "packages/bruno-lang/v2/src/utils.js",
    "content": "// safely parse json\nconst safeParseJson = (json) => {\n  try {\n    return JSON.parse(json);\n  } catch (e) {\n    return null;\n  }\n};\n\nconst indentString = (str, levels = 1) => {\n  if (!str || !str.length) {\n    return str || '';\n  }\n\n  const indent = '  '.repeat(levels);\n  return str\n    .split(/\\r\\n|\\r|\\n/)\n    .map((line) => indent + line)\n    .join('\\n');\n};\n\nconst outdentString = (str, spaces = 2) => {\n  if (!str || !str.length) {\n    return str || '';\n  }\n\n  const spacesRegex = new RegExp(`^ {${spaces}}`);\n  return str\n    .split(/\\r\\n|\\r|\\n/)\n    .map((line) => line.replace(spacesRegex, ''))\n    .join('\\n');\n};\n\nconst getValueString = (value) => {\n  // Handle null, undefined, and empty strings\n  if (!value) {\n    return '';\n  }\n\n  const hasNewLines = value.includes('\\n') || value.includes('\\r');\n\n  if (!hasNewLines) {\n    return value;\n  }\n\n  // Wrap multiline values in triple quotes with 2-space indentation\n  return `'''\\n${indentString(value)}\\n'''`;\n};\n\nconst getKeyString = (key) => {\n  const quotableChars = [':', '\"', '{', '}', ' '];\n  return quotableChars.some((char) => key.includes(char)) ? ('\"' + key.replaceAll('\"', '\\\\\"') + '\"') : key;\n};\n\nconst getValueUrl = (url) => {\n  // Handle null, undefined, and empty strings\n  if (!url) {\n    return '';\n  }\n\n  const hasNewLines = url.includes('\\n') || url.includes('\\r');\n\n  if (!hasNewLines) {\n    return url;\n  }\n\n  // Wrap multiline values in triple quotes with 4-space indentation (2 levels)\n  return `'''\\n${indentString(url, 2)}\\n'''`;\n};\n\nmodule.exports = {\n  safeParseJson,\n  indentString,\n  outdentString,\n  getValueString,\n  getKeyString,\n  getValueUrl\n};\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/assert.spec.js",
    "content": "/**\n * This test file is used to test the text parser.\n */\nconst parser = require('../src/bruToJson');\n\ndescribe('assert parser', () => {\n  it('should parse assert statement', () => {\n    const input = `\nassert {\n  res(\"data.airports\").filter(a => a.code ===\"BLR\").name: \"Bangalore International Airport\"\n}\n`;\n\n    const output = parser(input);\n    const expected = {\n      assertions: [\n        {\n          name: 'res(\"data.airports\").filter(a => a.code ===\"BLR\").name',\n          value: '\"Bangalore International Airport\"',\n          enabled: true\n        }\n      ]\n    };\n    expect(output).toEqual(expected);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/bruToJson.spec.js",
    "content": "const parser = require('../src/bruToJson');\n\ndescribe('bruToJson parser', () => {\n  describe('body:ws', () => {\n    it('infers message and settings | smoke', () => {\n      const input = `\nbody:ws {\n    type: json\n    name: message 1\n    content: '''\n      {\"foo\":\"bar\"}\n    ''' \n}\n\nsettings {\n      timeout: 30\n}\n`;\n\n      const expected = {\n        body: {\n          mode: 'ws',\n          ws: [\n            {\n              content: '{\"foo\":\"bar\"}',\n              name: 'message 1',\n              type: 'json'\n            }\n          ]\n        },\n        settings: {\n          encodeUrl: false,\n          timeout: 30\n        }\n      };\n\n      const output = parser(input);\n      expect(output).toEqual(expected);\n    });\n  });\n\n  describe('body:grpc', () => {\n    it('parses message content with name and content', () => {\n      const input = `\nbody:grpc {\n    name: message 1\n    content: '''\n      {\"foo\":\"bar\"}\n    ''' \n}\n`;\n\n      const expected = {\n        body: {\n          mode: 'grpc',\n          grpc: [\n            {\n              content: '{\"foo\":\"bar\"}',\n              name: 'message 1'\n            }\n          ]\n        }\n      };\n\n      const output = parser(input);\n      expect(output).toEqual(expected);\n    });\n\n    it('parses message with variables in content', () => {\n      const input = `\nbody:grpc {\n    name: message 1\n    content: '''\n      {\"id\":{{userId}},\"name\":\"{{userName}}\"}\n    ''' \n}\n`;\n\n      const expected = {\n        body: {\n          mode: 'grpc',\n          grpc: [\n            {\n              content: '{\"id\":{{userId}},\"name\":\"{{userName}}\"}',\n              name: 'message 1'\n            }\n          ]\n        }\n      };\n\n      const output = parser(input);\n      expect(output).toEqual(expected);\n    });\n  });\n\n  describe('multi-line values', () => {\n    it('parses multi-line values in URL, headers, params, and vars', () => {\n      const input = `\nmeta {\n  name: new-line\n  type: http\n  seq: 1\n}\n\nget {\n  url: '''\n    https://httpbin.io/anything?foo=hello\n    world\n'''\n  body: none\n  auth: oauth2\n}\n\nparams:query {\n  foo: '''\n    hello\n    world\n  '''\n}\n\nheaders {\n  \"test header\": '''\n    t1\n    t2\n  '''\n}\n\nvars:pre-request {\n  test-var: '''\n    t1\n    t2\n  '''\n}\n`;\n\n      const expected = {\n        meta: {\n          name: 'new-line',\n          type: 'http',\n          seq: '1'\n        },\n        http: {\n          method: 'get',\n          url: 'https://httpbin.io/anything?foo=hello\\nworld',\n          body: 'none',\n          auth: 'oauth2'\n        },\n        params: [\n          {\n            name: 'foo',\n            value: 'hello\\nworld',\n            enabled: true,\n            type: 'query'\n          }\n        ],\n        headers: [\n          {\n            name: 'test header',\n            value: 't1\\nt2',\n            enabled: true\n          }\n        ],\n        vars: {\n          req: [\n            {\n              name: 'test-var',\n              value: 't1\\nt2',\n              enabled: true,\n              local: false\n            }\n          ]\n        }\n      };\n\n      const output = parser(input);\n      expect(output).toEqual(expected);\n    });\n\n    it('parses multiline body parts with content type annotation', () => {\n      const input = `\nbody:multipart-form {\n  filePart: '''\n    Line1\n    Line2\n  ''' @contentType(text/plain)\n}\n`;\n\n      const expected = {\n        body: {\n          multipartForm: [\n            {\n              name: 'filePart',\n              value: 'Line1\\nLine2',\n              enabled: true,\n              type: 'text',\n              contentType: 'text/plain'\n            }\n          ]\n        }\n      };\n\n      const output = parser(input);\n      expect(output).toEqual(expected);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/collection.spec.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst collectionBruToJson = require('../src/collectionBruToJson');\nconst jsonToCollectionBru = require('../src/jsonToCollectionBru');\n\ndescribe('collectionBruToJson', () => {\n  it('should parse the collection bru file', () => {\n    const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'collection.bru'), 'utf8');\n    const expected = require('./fixtures/collection.json');\n    const output = collectionBruToJson(input);\n\n    expect(output).toEqual(expected);\n  });\n});\n\ndescribe('jsonToCollectionBru', () => {\n  it('should convert the collection json to bru', () => {\n    const input = require('./fixtures/collection.json');\n    const expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'collection.bru'), 'utf8');\n    const output = jsonToCollectionBru(input);\n\n    expect(output).toEqual(expected);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/custom-methods/custom-method.spec.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst bruToJson = require('../../src/bruToJson');\nconst jsonToBru = require('../../src/jsonToBru');\n\ndescribe('Custom Method Conversion Tests', () => {\n  const fixturesDir = path.join(__dirname, 'fixtures');\n\n  describe('parse (BRU to JSON)', () => {\n    it('should parse FETCH custom method from BRU to JSON', () => {\n      const input = fs.readFileSync(path.join(fixturesDir, 'custom-method.bru'), 'utf8');\n      const expected = require(path.join(fixturesDir, 'custom-method.json'));\n      const output = bruToJson(input);\n\n      expect(output).toEqual(expected);\n    });\n\n    it('should parse X-CUSTOM method from BRU to JSON', () => {\n      const input = fs.readFileSync(path.join(fixturesDir, 'custom-method-x-custom.bru'), 'utf8');\n      const expected = require(path.join(fixturesDir, 'custom-method-x-custom.json'));\n      const output = bruToJson(input);\n\n      expect(output).toEqual(expected);\n    });\n\n    it('should parse custom method with special characters from BRU to JSON', () => {\n      const input = fs.readFileSync(path.join(fixturesDir, 'custom-method-with-special-chars.bru'), 'utf8');\n      const expected = require(path.join(fixturesDir, 'custom-method-with-special-chars.json'));\n      const output = bruToJson(input);\n\n      expect(output).toEqual(expected);\n    });\n  });\n\n  describe('stringify (JSON to BRU)', () => {\n    it('should stringify FETCH custom method from JSON to BRU', () => {\n      const input = require(path.join(fixturesDir, 'custom-method.json'));\n      const expected = fs.readFileSync(path.join(fixturesDir, 'custom-method.bru'), 'utf8');\n      const output = jsonToBru(input);\n\n      expect(output).toEqual(expected);\n    });\n\n    it('should stringify X-CUSTOM method from JSON to BRU', () => {\n      const input = require(path.join(fixturesDir, 'custom-method-x-custom.json'));\n      const expected = fs.readFileSync(path.join(fixturesDir, 'custom-method-x-custom.bru'), 'utf8');\n      const output = jsonToBru(input);\n\n      expect(output).toEqual(expected);\n    });\n\n    it('should stringify custom method with special characters from JSON to BRU', () => {\n      const input = require(path.join(fixturesDir, 'custom-method-with-special-chars.json'));\n      const expected = fs.readFileSync(path.join(fixturesDir, 'custom-method-with-special-chars.bru'), 'utf8');\n      const output = jsonToBru(input);\n\n      expect(output).toEqual(expected);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/custom-methods/fixtures/custom-method-with-special-chars.bru",
    "content": "meta {\n  name: Custom Method with Special Characters\n  type: http\n  seq: 3\n}\n\nhttp {\n  method: CUSTOM@METHOD\n  url: https://api.example.com/special-method\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/custom-methods/fixtures/custom-method-with-special-chars.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Custom Method with Special Characters\",\n    \"type\": \"http\",\n    \"seq\": \"3\"\n  },\n  \"http\": {\n    \"method\": \"CUSTOM@METHOD\",\n    \"url\": \"https://api.example.com/special-method\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/custom-methods/fixtures/custom-method-x-custom.bru",
    "content": "meta {\n  name: Custom Method X-CUSTOM\n  type: http\n  seq: 2\n}\n\nhttp {\n  method: X-CUSTOM\n  url: https://api.example.com/x-custom\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/custom-methods/fixtures/custom-method-x-custom.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Custom Method X-CUSTOM\",\n    \"type\": \"http\",\n    \"seq\": \"2\"\n  },\n  \"http\": {\n    \"method\": \"X-CUSTOM\",\n    \"url\": \"https://api.example.com/x-custom\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/custom-methods/fixtures/custom-method.bru",
    "content": "meta {\n  name: Custom Method FETCH\n  type: http\n  seq: 1\n}\n\nhttp {\n  method: FETCH\n  url: https://api.example.com/custom\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/custom-methods/fixtures/custom-method.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Custom Method FETCH\",\n    \"type\": \"http\",\n    \"seq\": \"1\"\n  },\n  \"http\": {\n    \"method\": \"FETCH\",\n    \"url\": \"https://api.example.com/custom\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/defaults.spec.js",
    "content": "const bruToJson = require('../src/bruToJson');\n\ndescribe('defaults', () => {\n  it('should parse the default type and seq', () => {\n    const input = `\nmeta {\n  name: Create user\n}\n\npost {\n  url: /users\n}\n`;\n    const expected = {\n      meta: {\n        name: 'Create user',\n        seq: 1,\n        type: 'http'\n      },\n      http: {\n        method: 'post',\n        url: '/users'\n      }\n    };\n    const output = bruToJson(input);\n    expect(output).toEqual(expected);\n  });\n\n  it('should parse the default body mode as json if the body is found', () => {\n    const input = `\nmeta {\n  name: Create user\n}\n\npost {\n  url: /users\n}\n\nbody {\n  {\n    name: John\n    age: 30\n  }\n}\n`;\n\n    const expected = {\n      meta: {\n        name: 'Create user',\n        seq: 1,\n        type: 'http'\n      },\n      http: {\n        method: 'post',\n        url: '/users',\n        body: 'json'\n      },\n      body: {\n        json: '{\\n  name: John\\n  age: 30\\n}'\n      }\n    };\n\n    const output = bruToJson(input);\n    expect(output).toEqual(expected);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/dictionary.spec.js",
    "content": "/**\n * This test file is used to test the dictionary parser.\n */\n\nconst parser = require('../src/bruToJson');\n\nconst assertSingleHeader = (input) => {\n  const output = parser(input);\n\n  const expected = {\n    headers: [\n      {\n        name: 'hello',\n        value: 'world',\n        enabled: true\n      }\n    ]\n  };\n  expect(output).toEqual(expected);\n};\n\ndescribe('headers parser', () => {\n  it('should parse empty header', () => {\n    const input = `\nheaders {\n}`;\n\n    const output = parser(input);\n    const expected = {\n      headers: []\n    };\n    expect(output).toEqual(expected);\n  });\n\n  it('should parse single header', () => {\n    const input = `\nheaders {\n  hello: world\n}`;\n\n    assertSingleHeader(input);\n  });\n\n  it('should parse single header with spaces', () => {\n    const input = `\nheaders {\n      hello: world   \n}`;\n\n    assertSingleHeader(input);\n  });\n\n  it('should parse single header with spaces and newlines', () => {\n    const input = `\nheaders {\n\n      hello: world   \n  \n\n}`;\n\n    assertSingleHeader(input);\n  });\n\n  it('should parse single header with empty value', () => {\n    const input = `\nheaders {\n  hello:\n}`;\n\n    const output = parser(input);\n    const expected = {\n      headers: [\n        {\n          name: 'hello',\n          value: '',\n          enabled: true\n        }\n      ]\n    };\n    expect(output).toEqual(expected);\n  });\n\n  it('should parse single header with empty key', () => {\n    const input = `\nheaders {\n  : world\n}`;\n\n    const output = parser(input);\n    const expected = {\n      headers: [\n        {\n          name: '',\n          value: 'world',\n          enabled: true\n        }\n      ]\n    };\n    expect(output).toEqual(expected);\n  });\n\n  it('should parse multi headers', () => {\n    const input = `\nheaders {\n  content-type: application/json\n    \n  Authorization: JWT secret\n}`;\n\n    const output = parser(input);\n    const expected = {\n      headers: [\n        {\n          name: 'content-type',\n          value: 'application/json',\n          enabled: true\n        },\n        {\n          name: 'Authorization',\n          value: 'JWT secret',\n          enabled: true\n        }\n      ]\n    };\n    expect(output).toEqual(expected);\n  });\n\n  it('should parse disabled headers', () => {\n    const input = `\nheaders {\n  ~content-type: application/json\n}`;\n\n    const output = parser(input);\n    const expected = {\n      headers: [\n        {\n          name: 'content-type',\n          value: 'application/json',\n          enabled: false\n        }\n      ]\n    };\n    expect(output).toEqual(expected);\n  });\n\n  it('should parse empty url', () => {\n    const input = `\nget {\n  url: \n  body: json\n}`;\n\n    const output = parser(input);\n    const expected = {\n      http: {\n        url: '',\n        method: 'get',\n        body: 'json'\n      }\n    };\n    expect(output).toEqual(expected);\n  });\n\n  it('should throw error on invalid header', () => {\n    const input = `\nheaders {\n  hello: world\n  foo\n}`;\n\n    expect(() => parser(input)).toThrow();\n  });\n\n  it('should throw error on invalid header', () => {\n    const input = `\nheaders {\n  hello: world\n  foo: bar}`;\n\n    expect(() => parser(input)).toThrow();\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/dotenvToJson.spec.js",
    "content": "const parser = require('../src/dotenvToJson');\n\ndescribe('DotEnv File Parser', () => {\n  test('it should parse a simple key-value pair', () => {\n    const input = `FOO=bar`;\n    const expected = { FOO: 'bar' };\n    const output = parser(input);\n    expect(output).toEqual(expected);\n  });\n\n  test('it should parse a simple key-value pair with empty lines', () => {\n    const input = `\nFOO=bar\n\n`;\n    const expected = { FOO: 'bar' };\n    const output = parser(input);\n    expect(output).toEqual(expected);\n  });\n\n  test('it should parse multiple key-value pairs', () => {\n    const input = `\nFOO=bar\nBAZ=2\nBEEP=false\n`;\n    const expected = {\n      FOO: 'bar',\n      BAZ: '2',\n      BEEP: 'false'\n    };\n    const output = parser(input);\n    expect(output).toEqual(expected);\n  });\n\n  test('it should not strip leading and trailing whitespace when using quotes', () => {\n    const input = `\nSPACE=\"  value  \"\n`;\n    const expected = { SPACE: '  value  ' };\n    const output = parser(input);\n    expect(output).toEqual(expected);\n  });\n\n  test('it should strip leading and trailing whitespace when NOT using quotes', () => {\n    const input = `\nSPACE=  value  \n`;\n    const expected = { SPACE: 'value' };\n    const output = parser(input);\n    expect(output).toEqual(expected);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/envToJson.spec.js",
    "content": "const parser = require('../src/envToJson');\n\ndescribe('env parser', () => {\n  it('should parse empty vars', () => {\n    const input = `\nvars {\n}`;\n\n    const output = parser(input);\n    const expected = {\n      variables: []\n    };\n\n    expect(output).toEqual(expected);\n  });\n\n  it('should parse single var line', () => {\n    const input = `\nvars {\n  url: http://localhost:3000\n}`;\n\n    const output = parser(input);\n    const expected = {\n      variables: [\n        {\n          name: 'url',\n          value: 'http://localhost:3000',\n          enabled: true,\n          secret: false\n        }\n      ]\n    };\n\n    expect(output).toEqual(expected);\n  });\n\n  it('should parse multiple var lines', () => {\n    const input = `\nvars {\n  url: http://localhost:3000\n  port: 3000\n  ~token: secret\n}`;\n\n    const output = parser(input);\n    const expected = {\n      variables: [\n        {\n          name: 'url',\n          value: 'http://localhost:3000',\n          enabled: true,\n          secret: false\n        },\n        {\n          name: 'port',\n          value: '3000',\n          enabled: true,\n          secret: false\n        },\n        {\n          name: 'token',\n          value: 'secret',\n          enabled: false,\n          secret: false\n        }\n      ]\n    };\n\n    expect(output).toEqual(expected);\n  });\n\n  it('should gracefully handle empty lines and spaces', () => {\n    const input = `\n\nvars {\n      url:     http://localhost:3000   \n  port: 3000\n}\n\n`;\n\n    const output = parser(input);\n    const expected = {\n      variables: [\n        {\n          name: 'url',\n          value: 'http://localhost:3000',\n          enabled: true,\n          secret: false\n        },\n        {\n          name: 'port',\n          value: '3000',\n          enabled: true,\n          secret: false\n        }\n      ]\n    };\n\n    expect(output).toEqual(expected);\n  });\n\n  it('should parse vars with empty values', () => {\n    const input = `\nvars {\n  url: \n  phone: \n  api-key:\n}\n`;\n\n    const output = parser(input);\n    const expected = {\n      variables: [\n        {\n          name: 'url',\n          value: '',\n          enabled: true,\n          secret: false\n        },\n        {\n          name: 'phone',\n          value: '',\n          enabled: true,\n          secret: false\n        },\n        {\n          name: 'api-key',\n          value: '',\n          enabled: true,\n          secret: false\n        }\n      ]\n    };\n\n    expect(output).toEqual(expected);\n  });\n\n  it('should parse empty secret vars', () => {\n    const input = `\nvars {\n  url: http://localhost:3000\n}\n\nvars:secret [\n\n]\n`;\n\n    const output = parser(input);\n    const expected = {\n      variables: [\n        {\n          name: 'url',\n          value: 'http://localhost:3000',\n          enabled: true,\n          secret: false\n        }\n      ]\n    };\n\n    expect(output).toEqual(expected);\n  });\n\n  it('should parse secret vars', () => {\n    const input = `\nvars {\n  url: http://localhost:3000\n}\n\nvars:secret [\n  token\n]\n`;\n\n    const output = parser(input);\n    const expected = {\n      variables: [\n        {\n          name: 'url',\n          value: 'http://localhost:3000',\n          enabled: true,\n          secret: false\n        },\n        {\n          name: 'token',\n          value: '',\n          enabled: true,\n          secret: true\n        }\n      ]\n    };\n\n    expect(output).toEqual(expected);\n  });\n\n  it('should parse multiline secret vars', () => {\n    const input = `\nvars {\n  url: http://localhost:3000\n}\n\nvars:secret [\n  access_token,\n  access_secret,\n\n  ~access_password\n]\n`;\n\n    const output = parser(input);\n    const expected = {\n      variables: [\n        {\n          name: 'url',\n          value: 'http://localhost:3000',\n          enabled: true,\n          secret: false\n        },\n        {\n          name: 'access_token',\n          value: '',\n          enabled: true,\n          secret: true\n        },\n        {\n          name: 'access_secret',\n          value: '',\n          enabled: true,\n          secret: true\n        },\n        {\n          name: 'access_password',\n          value: '',\n          enabled: false,\n          secret: true\n        }\n      ]\n    };\n\n    expect(output).toEqual(expected);\n  });\n\n  it('should parse inline secret vars', () => {\n    const input = `\nvars {\n  url: http://localhost:3000\n}\n\nvars:secret [access_key]\n`;\n\n    const output = parser(input);\n    const expected = {\n      variables: [\n        {\n          name: 'url',\n          value: 'http://localhost:3000',\n          enabled: true,\n          secret: false\n        },\n        {\n          name: 'access_key',\n          value: '',\n          enabled: true,\n          secret: true\n        }\n      ]\n    };\n\n    expect(output).toEqual(expected);\n  });\n\n  it('should parse inline multiple secret vars', () => {\n    const input = `\nvars {\n  url: http://localhost:3000\n}\n\nvars:secret [access_key,access_secret,    access_password  ]\n`;\n\n    const output = parser(input);\n    const expected = {\n      variables: [\n        {\n          name: 'url',\n          value: 'http://localhost:3000',\n          enabled: true,\n          secret: false\n        },\n        {\n          name: 'access_key',\n          value: '',\n          enabled: true,\n          secret: true\n        },\n        {\n          name: 'access_secret',\n          value: '',\n          enabled: true,\n          secret: true\n        },\n        {\n          name: 'access_password',\n          value: '',\n          enabled: true,\n          secret: true\n        }\n      ]\n    };\n\n    expect(output).toEqual(expected);\n  });\n\n  it('should parse multiline variable values', () => {\n    const input = `\nvars {\n  json_data: '''\n    {\n      \"name\": \"test\",\n      \"value\": 123\n    }\n  '''\n}`;\n\n    const output = parser(input);\n    const expected = {\n      variables: [\n        {\n          name: 'json_data',\n          value: '{\\n  \"name\": \"test\",\\n  \"value\": 123\\n}',\n          enabled: true,\n          secret: false\n        }\n      ]\n    };\n\n    expect(output).toEqual(expected);\n  });\n\n  it('should parse multiline variable that has indentation', () => {\n    const input = `\nvars {\n  script: '''\n    function test() {\n      console.log(\"hello\");\n      return true;\n    }\n  '''\n}`;\n\n    const output = parser(input);\n    const expected = {\n      variables: [\n        {\n          name: 'script',\n          value: 'function test() {\\n  console.log(\"hello\");\\n  return true;\\n}',\n          enabled: true,\n          secret: false\n        }\n      ]\n    };\n\n    expect(output).toEqual(expected);\n  });\n\n  it('should parse disabled multiline variable', () => {\n    const input = `\nvars {\n  ~disabled_multiline: '''\n    line 1\n    line 2\n    line 3\n  '''\n}`;\n\n    const output = parser(input);\n    const expected = {\n      variables: [\n        {\n          name: 'disabled_multiline',\n          value: 'line 1\\nline 2\\nline 3',\n          enabled: false,\n          secret: false\n        }\n      ]\n    };\n\n    expect(output).toEqual(expected);\n  });\n\n  it('should parse multiple multiline variables', () => {\n    const input = `\nvars {\n  config: '''\n    debug=true\n    port=3000\n  '''\n  template: '''\n    <html>\n      <body>Hello World</body>\n    </html>\n  '''\n}`;\n\n    const output = parser(input);\n    const expected = {\n      variables: [\n        {\n          name: 'config',\n          value: 'debug=true\\nport=3000',\n          enabled: true,\n          secret: false\n        },\n        {\n          name: 'template',\n          value: '<html>\\n  <body>Hello World</body>\\n</html>',\n          enabled: true,\n          secret: false\n        }\n      ]\n    };\n\n    expect(output).toEqual(expected);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/examples.spec.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst bruToJson = require('../../src/bruToJson');\nconst jsonToBru = require('../../src/jsonToBru');\n\ndescribe('Examples functionality', () => {\n  describe('Fixture-based tests', () => {\n    it('should parse examples-simple.bru correctly', () => {\n      const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'examples-simple.bru'), 'utf8');\n      const expected = require('./fixtures/json/examples-simple.json');\n      const output = bruToJson(input);\n\n      expect(output).toEqual(expected);\n    });\n\n    it('should parse examples-complex.bru correctly', () => {\n      const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'examples-complex.bru'), 'utf8');\n      const output = bruToJson(input);\n\n      // Basic structure validation\n      expect(output.meta).toBeDefined();\n      expect(output.http).toBeDefined();\n      expect(output.examples).toBeDefined();\n      expect(Array.isArray(output.examples)).toBe(true);\n      expect(output.examples).toHaveLength(3);\n\n      // Check each example has the expected structure\n      output.examples.forEach((example, index) => {\n        expect(example.name).toBeDefined();\n        expect(example.description).toBeDefined();\n        expect(example.request).toBeDefined();\n        expect(example.request.url).toBeDefined();\n        if (example.response) {\n          expect(example.response.status).toBeDefined();\n          expect(example.response.body).toBeDefined();\n        }\n      });\n\n      // Check specific examples\n      const jsonExample = output.examples[0];\n      expect(jsonExample.name).toBe('JSON API Example');\n      expect(jsonExample.request.url).toBeDefined();\n      if (jsonExample.request.body && jsonExample.request.body.json) {\n        expect(jsonExample.request.body.json).toContain('\"format\": \"json\"');\n      }\n\n      const xmlExample = output.examples[1];\n      expect(xmlExample.name).toBe('XML API Example');\n      if (xmlExample.request.body && xmlExample.request.body.xml) {\n        expect(xmlExample.request.body.xml).toContain('<format>xml</format>');\n      }\n\n      const textExample = output.examples[2];\n      expect(textExample.name).toBe('Text API Example');\n    });\n  });\n\n  describe('Basic examples parsing', () => {\n    it('should parse a single example block', () => {\n      const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'bruToJson-single-example.bru'), 'utf8');\n      const expected = require('./fixtures/json/bruToJson-single-example.json');\n      const output = bruToJson(input);\n\n      expect(output).toEqual(expected);\n    });\n\n    it('should parse multiple example blocks', () => {\n      const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'bruToJson-multiple-examples.bru'), 'utf8');\n      const expected = require('./fixtures/json/bruToJson-multiple-examples.json');\n      const output = bruToJson(input);\n\n      expect(output).toEqual(expected);\n    });\n\n    it('should handle examples with response blocks', () => {\n      const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'bruToJson-response-example.bru'), 'utf8');\n      const expected = require('./fixtures/json/bruToJson-response-example.json');\n      const output = bruToJson(input);\n      expect(output).toEqual(expected);\n    });\n  });\n\n  describe('Examples with different body types', () => {\n    it('should handle examples with JSON body', () => {\n      const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'bruToJson-json-body.bru'), 'utf8');\n      const expected = require('./fixtures/json/bruToJson-json-body.json');\n      const output = bruToJson(input);\n\n      expect(output).toEqual(expected);\n    });\n\n    it('should handle examples with XML body', () => {\n      const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'bruToJson-xml-body.bru'), 'utf8');\n      const expected = require('./fixtures/json/bruToJson-xml-body.json');\n      const output = bruToJson(input);\n\n      expect(output).toEqual(expected);\n    });\n\n    it('should handle examples with text body', () => {\n      const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'bruToJson-text-body.bru'), 'utf8');\n      const expected = require('./fixtures/json/bruToJson-text-body.json');\n      const output = bruToJson(input);\n\n      expect(output).toEqual(expected);\n    });\n  });\n\n  describe('Edge cases', () => {\n    it('should handle empty example blocks', () => {\n      const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'bruToJson-empty-example.bru'), 'utf8');\n      const expected = require('./fixtures/json/bruToJson-empty-example.json');\n      const output = bruToJson(input);\n\n      expect(output).toEqual(expected);\n    });\n\n    it('should work without any examples', () => {\n      const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'bruToJson-no-examples.bru'), 'utf8');\n      const expected = require('./fixtures/json/bruToJson-no-examples.json');\n      const output = bruToJson(input);\n\n      expect(output).toEqual(expected);\n    });\n  });\n\n  describe('jsonToBru conversion', () => {\n    it('should convert JSON with examples to BRU format', () => {\n      const jsonInput = require('./fixtures/json/jsonToBru-simple.json');\n      const expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'jsonToBru-simple.bru'), 'utf8');\n      const output = jsonToBru(jsonInput);\n      expect(output).toEqual(expected);\n    });\n\n    it('should handle multiple examples correctly', () => {\n      const jsonInput = require('./fixtures/json/jsonToBru-multiple.json');\n      const expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'jsonToBru-multiple.bru'), 'utf8');\n      const output = jsonToBru(jsonInput);\n\n      expect(output).toEqual(expected);\n    });\n\n    it('should handle examples with response blocks', () => {\n      const jsonInput = require('./fixtures/json/jsonToBru-response.json');\n      const expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'jsonToBru-response.bru'), 'utf8');\n      const output = jsonToBru(jsonInput);\n      expect(output).toEqual(expected);\n    });\n\n    it('should handle examples with different body types', () => {\n      const jsonInput = require('./fixtures/json/jsonToBru-bodytypes.json');\n      const expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'jsonToBru-bodytypes.bru'), 'utf8');\n      const output = jsonToBru(jsonInput);\n\n      expect(output).toEqual(expected);\n    });\n\n    it('should handle round-trip conversion correctly', () => {\n      const originalBru = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'examples-simple.bru'), 'utf8');\n      const jsonFromBru = bruToJson(originalBru);\n      const bruFromJson = jsonToBru(jsonFromBru);\n      const jsonFromBruAgain = bruToJson(bruFromJson);\n\n      // The examples should be preserved through the round-trip\n      expect(jsonFromBruAgain.examples).toBeDefined();\n      expect(Array.isArray(jsonFromBruAgain.examples)).toBe(true);\n      expect(jsonFromBruAgain.examples).toHaveLength(2);\n      expect(jsonFromBruAgain.examples[0].name).toBe('Get User by ID');\n      expect(jsonFromBruAgain.examples[1].name).toBe('Create New User');\n    });\n\n    it('should handle empty examples array', () => {\n      const jsonInput = {\n        meta: {\n          name: 'No Examples API',\n          type: 'http'\n        },\n        http: {\n          method: 'get',\n          url: 'https://api.example.com/test'\n        },\n        examples: []\n      };\n\n      const expected = `meta {\n  name: No Examples API\n  type: http\n}\n\nget {\n  url: https://api.example.com/test\n}\n`;\n\n      const output = jsonToBru(jsonInput);\n\n      expect(output).toEqual(expected);\n    });\n\n    it('should handle examples with minimal structure', () => {\n      const jsonInput = {\n        meta: {\n          name: 'Test API',\n          type: 'http'\n        },\n        http: {\n          url: 'https://api.example.com/test',\n          method: 'get'\n        },\n        examples: [\n          {\n            name: 'Example Request',\n            description: 'A simple example',\n            request: {\n              url: 'https://api.example.com/example',\n              method: 'get'\n            }\n          }\n        ]\n      };\n\n      const expected = `meta {\n  name: Test API\n  type: http\n}\n\nget {\n  url: https://api.example.com/test\n}\n\nexample {\n  name: Example Request\n  description: A simple example\n  \n  request: {\n    url: https://api.example.com/example\n    method: get\n  }\n}\n`;\n\n      const output = jsonToBru(jsonInput);\n\n      expect(output).toEqual(expected);\n    });\n\n    it('should handle examples without description', () => {\n      const jsonInput = {\n        meta: {\n          name: 'Test API',\n          type: 'http'\n        },\n        http: {\n          url: 'https://api.example.com/test',\n          method: 'get'\n        },\n        examples: [\n          {\n            name: 'Example Request',\n            request: {\n              url: 'https://api.example.com/example',\n              method: 'get'\n            }\n          }\n        ]\n      };\n\n      const expected = `meta {\n  name: Test API\n  type: http\n}\n\nget {\n  url: https://api.example.com/test\n}\n\nexample {\n  name: Example Request\n  \n  request: {\n    url: https://api.example.com/example\n    method: get\n  }\n}\n`;\n\n      const output = jsonToBru(jsonInput);\n\n      expect(output).toEqual(expected);\n    });\n  });\n\n  describe('Complex examples with auth', () => {\n    it('should parse complex-with-auth.bru correctly', () => {\n      const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'complex-with-auth.bru'), 'utf8');\n      const expected = require('./fixtures/json/complex-with-auth.json');\n      const output = bruToJson(input);\n\n      expect(output).toEqual(expected);\n    });\n\n    it('should parse form-data-complex.bru correctly', () => {\n      const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'form-data-complex.bru'), 'utf8');\n      const expected = require('./fixtures/json/form-data-complex.json');\n      const output = bruToJson(input);\n\n      expect(output).toEqual(expected);\n    });\n\n    it('should parse multiple-examples-variations.bru correctly', () => {\n      const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'multiple-examples-variations.bru'), 'utf8');\n      const expected = require('./fixtures/json/multiple-examples-variations.json');\n      const output = bruToJson(input);\n\n      expect(output).toEqual(expected);\n    });\n\n    it('should parse oauth2-examples.bru correctly', () => {\n      const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'oauth2-examples.bru'), 'utf8');\n      const expected = require('./fixtures/json/oauth2-examples.json');\n      const output = bruToJson(input);\n\n      expect(output).toEqual(expected);\n    });\n\n    describe('jsonToBru conversion for complex fixtures', () => {\n      it('should convert complex-with-auth.json to BRU format and preserve examples', () => {\n        const jsonInput = require('./fixtures/json/complex-with-auth.json');\n        const expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'complex-with-auth.bru'), 'utf8');\n        const output = jsonToBru(jsonInput);\n        expect(output).toEqual(expected);\n      });\n\n      it('should convert form-data-complex.json to BRU format and preserve examples', () => {\n        const jsonInput = require('./fixtures/json/form-data-complex.json');\n        const expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'form-data-complex.bru'), 'utf8');\n        const output = jsonToBru(jsonInput);\n        expect(output).toEqual(expected);\n      });\n\n      it('should convert multiple-examples-variations.json to BRU format and preserve examples', () => {\n        const jsonInput = require('./fixtures/json/multiple-examples-variations.json');\n        const expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'multiple-examples-variations.bru'), 'utf8');\n        const output = jsonToBru(jsonInput);\n        expect(output).toEqual(expected);\n      });\n\n      it('should convert oauth2-examples.json to BRU format and preserve examples', () => {\n        const jsonInput = require('./fixtures/json/oauth2-examples.json');\n        const expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'oauth2-examples.bru'), 'utf8');\n        const output = jsonToBru(jsonInput);\n        expect(output).toEqual(expected);\n      });\n    });\n  });\n\n  describe('Examples with multiline descriptions', () => {\n    it('should parse examples with multiline descriptions', () => {\n      const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'examples-multiline-description.bru'), 'utf8');\n      const expected = require('./fixtures/json/examples-multiline-description.json');\n      const output = bruToJson(input);\n\n      expect(output).toEqual(expected);\n    });\n\n    it('should convert examples with multiline descriptions to BRU format', () => {\n      const jsonInput = require('./fixtures/json/examples-multiline-description.json');\n      const expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'examples-multiline-description.bru'), 'utf8');\n      const output = jsonToBru(jsonInput);\n\n      expect(output).toEqual(expected);\n    });\n\n    it('should parse example without description field', () => {\n      const bruInput = `meta {\n  name: Test API\n  type: http\n}\n\nexample {\n  name: Example Request\n  \n  request: {\n    url: https://api.example.com/example\n    method: get\n  }\n}\n`;\n\n      const output = bruToJson(bruInput);\n\n      expect(output.examples).toBeDefined();\n      expect(output.examples).toHaveLength(1);\n      expect(output.examples[0].name).toBe('Example Request');\n      expect(output.examples[0].description).toBeUndefined();\n    });\n  });\n\n  describe('Examples with multiline strings and contentType', () => {\n    it('should parse examples with multiline strings and @contentType annotations', () => {\n      const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'examples-multiline-contenttype.bru'), 'utf8');\n      const expected = require('./fixtures/json/examples-multiline-contenttype.json');\n      const output = bruToJson(input);\n\n      expect(output).toEqual(expected);\n    });\n\n    it('should correctly extract contentType from multiline values', () => {\n      const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'examples-multiline-contenttype.bru'), 'utf8');\n      const output = bruToJson(input);\n\n      const example = output.examples[0];\n      const multipartForm = example.request.body.multipartForm;\n\n      // Check that multiline values with @contentType are parsed correctly\n      const testField = multipartForm.find((f) => f.name === 'test');\n      expect(testField).toBeDefined();\n      expect(testField.value).toContain('\"hello\"');\n      expect(testField.contentType).toBe('application/json');\n\n      // Check single-line value with @contentType\n      const simpleField = multipartForm.find((f) => f.name === 'simple');\n      expect(simpleField).toBeDefined();\n      expect(simpleField.value).toBe('cat and mouse');\n      expect(simpleField.contentType).toBe('text/plain');\n\n      // Check multiline value without @contentType\n      const arrayField = multipartForm.find((f) => f.name === 'array');\n      expect(arrayField).toBeDefined();\n      expect(arrayField.value).toContain('\"coolade\"');\n      expect(arrayField.contentType).toBe('');\n\n      // Check complex multiline JSON with @contentType\n      const jsonValueField = multipartForm.find((f) => f.name === 'jsonValue');\n      expect(jsonValueField).toBeDefined();\n      expect(jsonValueField.value).toContain('\"key\": \"value\"');\n      expect(jsonValueField.contentType).toBe('application/json');\n    });\n\n    it('should handle round-trip conversion for multiline strings with contentType', () => {\n      const originalBru = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'examples-multiline-contenttype.bru'), 'utf8');\n      const jsonFromBru = bruToJson(originalBru);\n      const bruFromJson = jsonToBru(jsonFromBru);\n      const jsonFromBruAgain = bruToJson(bruFromJson);\n\n      // The examples should be preserved through the round-trip\n      expect(jsonFromBruAgain.examples).toBeDefined();\n      expect(Array.isArray(jsonFromBruAgain.examples)).toBe(true);\n      expect(jsonFromBruAgain.examples).toHaveLength(1);\n\n      const example = jsonFromBruAgain.examples[0];\n      const multipartForm = example.request.body.multipartForm;\n\n      // Verify contentType is preserved\n      const testField = multipartForm.find((f) => f.name === 'test');\n      expect(testField.contentType).toBe('application/json');\n      expect(testField.value).toContain('\"hello\"');\n\n      const jsonValueField = multipartForm.find((f) => f.name === 'jsonValue');\n      expect(jsonValueField.contentType).toBe('application/json');\n      expect(jsonValueField.value).toContain('\"key\": \"value\"');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/bru/bruToJson-empty-example.bru",
    "content": "meta {\n  name: Test API\n  type: http\n}\n\nexample {\n  \n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/bru/bruToJson-json-body.bru",
    "content": "meta {\n  name: Test API\n  type: http\n}\n\nexample {\n  name: JSON Example\n  description: An example with JSON body\n  \n  request: {\n    url: https://api.example.com/data\n    method: post\n    mode: json\n    body:json: {\n      {\n        \"name\": \"Test\",\n        \"value\": 123\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/bru/bruToJson-multiple-examples.bru",
    "content": "meta {\n  name: Test API\n  type: http\n}\n\nget {\n  url: https://api.example.com/test\n}\n\nexample {\n  name: Example 1\n  description: First example\n\n  request: {\n    url: https://api.example.com/example1\n    method: get\n    mode: none\n  }\n}\n\nexample {\n  name: Example 2\n  description: Second example with JSON body\n\n  request: {\n    url: https://api.example.com/example2\n    method: get\n    mode: json\n    body:json: {\n      {\n        \"data\": \"test\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/bru/bruToJson-no-examples.bru",
    "content": "meta {\n  name: Test API\n  type: http\n}\n\nget {\n  url: https://api.example.com/test\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/bru/bruToJson-response-example.bru",
    "content": "meta {\n  name: Test API\n  type: http\n}\n\nget {\n  url: https://api.example.com/test\n}\n\nexample {\n  name: Example with Response\n  description: An example with response data\n  \n  request: {\n    url: https://api.example.com/users/123\n    method: get\n    mode: none\n  }\n\n  response: {\n    headers: {\n      content-type: application/json\n    }\n\n    status: {\n      code: 200\n      text: OK\n    }\n    \n    body: {\n      type: json\n      content: '''\n        {\n          \"id\": 123,\n          \"name\": \"John Doe\"\n        }\n      '''\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/bru/bruToJson-single-example.bru",
    "content": "meta {\n  name: Test API\n  type: http\n}\n\nget {\n  url: https://api.example.com/test\n}\n\nexample {\n  name: Example Request\n  description: A simple example request\n  \n  request: {\n    url: https://api.example.com/example\n    method: get\n    mode: none\n  }\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/bru/bruToJson-text-body.bru",
    "content": "meta {\n  name: Test API\n  type: http\n}\n\nexample {\n  name: Text Example\n  description: An example with text body\n  \n  request: {\n    url: https://api.example.com/data\n    method: post\n    mode: text\n    body:text: {\n      Plain text data\n      with multiple lines\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/bru/bruToJson-xml-body.bru",
    "content": "meta {\n  name: Test API\n  type: http\n}\n\nexample {\n  name: XML Example\n  description: An example with XML body\n\n  request: {\n    url: https://api.example.com/data\n    method: post\n    mode: xml\n    body:xml: {\n      <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n      <data>\n        <name>Test</name>\n        <value>123</value>\n      </data>\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/bru/complex-with-auth.bru",
    "content": "meta {\n  name: Complex API with Auth\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://api.example.com/users\n  body: json\n  auth: bearer\n}\n\nparams:query {\n  include: details\n  format: json\n}\n\nparams:path {\n  id: 123\n  status: active\n}\n\nheaders {\n  content-type: application/json\n  x-api-key: my-secret-key\n  x-request-id: {{$uuid}}\n}\n\nauth:basic {\n  username: admin\n  password: secret123\n}\n\nauth:bearer {\n  token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHluKJe2vncyz4h4SH6FSS7YOKIx\n}\n\nauth:apikey {\n  key: x-api-key\n  value: api-secret-key-12345\n  placement: header\n}\n\nbody:json {\n  {\n    \"username\": \"johndoe\",\n    \"email\": \"john@example.com\",\n    \"profile\": {\n      \"firstName\": \"John\",\n      \"lastName\": \"Doe\",\n      \"age\": 30\n    },\n    \"preferences\": {\n      \"theme\": \"dark\",\n      \"notifications\": true\n    },\n    \"tags\": [\"admin\", \"user\", \"verified\"]\n  }\n}\n\nvars:pre-request {\n  user_id: 12345\n  environment: production\n}\n\nvars:post-response {\n  response_id: {{res.id}}\n  processed_at: {{$timestamp}}\n}\n\nscript:pre-request {\n  const timestamp = Date.now();\n  bru.setVar(\"request_timestamp\", timestamp);\n  bru.setVar(\"signature\", crypto.createHash('md5').update(timestamp.toString()).digest('hex'));\n}\n\ntests {\n  test(\"Response should be 201\", function() {\n    expect(res.getStatus()).to.eql(201);\n  });\n  \n  test(\"Response should have user data\", function() {\n    const body = res.getBody();\n    expect(body.username).to.be.ok;\n    expect(body.email).to.be.ok;\n  });\n}\n\ndocs {\n  This endpoint creates a new user account.\n  Requires authentication via Bearer token or API key.\n  Supports nested JSON structures with arrays.\n}\n\nexample {\n  name: Basic User Creation\n  description: Creates a new user with minimal required fields\n  \n  request: {\n    url: https://api.example.com/users\n    method: post\n    mode: json\n    headers: {\n      authorization: \"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\",\n      content-type: \"application/json\"\n    }\n  \n    body:json: {\n      {\n        \"username\": \"newuser\",\n        \"email\": \"newuser@example.com\",\n        \"password\": \"SecurePass123!\"\n      }\n    }\n  }\n  \n  response: {\n    status: {\n      code: 201\n      text: OK\n    }\n  \n    body: {\n      type: json\n      content: '''\n        {\n          \"id\": 100,\n          \"username\": \"newuser\",\n          \"email\": \"newuser@example.com\",\n          \"created_at\": \"2024-01-15T10:30:00Z\",\n          \"status\": \"active\"\n        }\n      '''\n    }\n  }\n}\n\nexample {\n  name: Advanced User with Profile\n  description: Creates a user with complete profile information and nested data\n  \n  request: {\n    url: https://api.example.com/users\n    method: post\n    mode: json\n    headers: {\n      authorization: \"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\",\n      content-type: \"application/json\",\n      x-api-key: \"advanced-api-key-45678\"\n    }\n  \n    body:json: {\n      {\n        \"username\": \"developer\",\n        \"email\": \"dev@example.com\",\n        \"password\": \"DevPass2024!\",\n        \"profile\": {\n          \"firstName\": \"Jane\",\n          \"lastName\": \"Developer\",\n          \"bio\": \"Software engineer passionate about APIs\",\n          \"location\": {\n            \"city\": \"San Francisco\",\n            \"country\": \"USA\",\n            \"timezone\": \"America/Los_Angeles\"\n          },\n          \"social\": {\n            \"github\": \"jandeveloper\",\n            \"twitter\": \"@jane_dev\"\n          }\n        },\n        \"skills\": [\"JavaScript\", \"Node.js\", \"React\", \"API Design\"],\n        \"experience\": 5,\n        \"preferences\": {\n          \"theme\": \"dark\",\n          \"language\": \"en\",\n          \"notifications\": {\n            \"email\": true,\n            \"push\": true,\n            \"sms\": false\n          }\n        }\n      }\n    }\n  }\n  \n  response: {\n    status: {\n      code: 201\n      text: OK\n    }\n  \n    body: {\n      type: json\n      content: '''\n        {\n          \"id\": 101,\n          \"username\": \"developer\",\n          \"email\": \"dev@example.com\",\n          \"profile\": {\n            \"firstName\": \"Jane\",\n            \"lastName\": \"Developer\",\n            \"bio\": \"Software engineer passionate about APIs\",\n            \"location\": {\n              \"city\": \"San Francisco\",\n              \"country\": \"USA\",\n              \"timezone\": \"America/Los_Angeles\"\n            }\n          },\n          \"skills\": [\"JavaScript\", \"Node.js\", \"React\", \"API Design\"],\n          \"experience\": 5,\n          \"created_at\": \"2024-01-15T12:45:30Z\",\n          \"status\": \"active\",\n          \"verified\": false\n        }\n      '''\n    }\n  }\n}\n\nexample {\n  name: XML Data Example\n  description: Example with XML format and complex structure\n  \n  request: {\n    url: https://api.example.com/users/xml\n    method: post\n    mode: xml\n    headers: {\n      content-type: \"application/xml\",\n      authorization: \"Basic YWRtaW46cGFzc3dvcmQ=\"\n    }\n  \n    body:xml: {\n      <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n      <user>\n        <username>xmluser</username>\n        <email>xml@example.com</email>\n        <profile>\n          <firstName>XML</firstName>\n          <lastName>User</lastName>\n        </profile>\n      </user>\n    }\n  }\n  \n  response: {\n    status: {\n      code: 201\n      text: OK\n    }\n  \n    body: {\n      type: xml\n      content: '''\n        <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n        <response>\n          <id>102</id>\n          <status>created</status>\n          <user>\n            <username>xmluser</username>\n            <email>xml@example.com</email>\n          </user>\n        </response>\n      '''\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/bru/examples-complex.bru",
    "content": "meta {\n  name: Multi-format API\n  type: http\n}\n\nget {\n  url: https://api.example.com/data\n}\n\nheaders {\n  content-type: application/json\n}\n\nbody:json {\n  {\n    \"message\": \"API supports multiple formats\"\n  }\n}\n\nexample {\n  name: JSON API Example\n  description: An example using JSON format\n\n  request: {\n    url: https://api.example.com/json\n    method: post\n    mode: json\n    body:json: {\n      {\n        \"format\": \"json\",\n        \"data\": {\n          \"name\": \"JSON Example\",\n          \"value\": 123\n        }\n      }\n    }\n  }\n\n  response: {\n    status: {\n      code: 201\n    }\n\n    body: {\n      type: json\n      content: '''\n        {\n          \"id\": 1,\n          \"format\": \"json\",\n          \"created\": true\n        }\n      '''\n    }\n  }\n}\n\nexample {\n  name: XML API Example\n  description: An example using XML format\n\n  request: {\n    url: https://api.example.com/xml\n    method: post\n    mode: xml\n    body:xml: {\n      <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n      <data>\n        <format>xml</format>\n        <name>XML Example</name>\n        <value>456</value>\n      </data>\n    }\n  }\n  response: {\n    status: {\n      code: 201\n    }\n\n    body: {\n      type: xml\n      content: '''\n        <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n        <response>\n          <id>2</id>\n          <format>xml</format>\n          <created>true</created>\n        </response>\n      '''\n    }\n  }\n}\n\nexample {\n  name: Text API Example\n  description: An example using text format\n\n  request: {\n    url: https://api.example.com/text\n    method: post\n    mode: text\n    body:text: {\n      Plain text data\n      Format: text\n      Name: Text Example\n      Value: 789\n    }\n  }\n\n  response: {\n    status: {\n      code: 201\n    }\n    \n    body: {\n      type: text\n      content: '''\n        Success: true\n        Format: text\n        ID: 3\n      '''\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/bru/examples-multiline-contenttype.bru",
    "content": "meta {\n  name: Multiline ContentType Test\n  type: http\n  seq: 1\n}\n\npost {\n  url: {{host}}/test\n  body: multipartForm\n}\n\nexample {\n  name: Example with multiline strings and contentType\n  \n  request: {\n    url: {{host}}/test\n    method: POST\n    mode: multipartForm\n    body:multipart-form: {\n      test: '''\n        {\n        \"hello\" : \"there\"\n        }\n      ''' @contentType(application/json)\n      simple: cat and mouse @contentType(text/plain)\n      array: '''\n        [\n        \"coolade\", \n        \"blast\"\n        ]\n      '''\n      jsonValue: '''\n        {\n          \"key\": \"value\",\n          \"nested\": {\n            \"data\": 123\n          }\n        }\n      ''' @contentType(application/json)\n      textValue: '''\n        This is a\n        multiline text\n        value\n      ''' @contentType(text/plain)\n    }\n  }\n  \n  response: {\n    status: {\n      code: 200\n      text: OK\n    }\n    \n    body: {\n      type: json\n      content: '''\n        {\n          \"status\": \"success\",\n          \"message\": \"Data received\"\n        }\n      '''\n    }\n  }\n}\n\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/bru/examples-multiline-description.bru",
    "content": "meta {\n  name: Multiline Description Test\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://api.example.com/test\n}\n\nexample {\n  name: Test Example\n  description: '''\n    This is a multiline description.\n    It spans multiple lines.\n    And should be parsed correctly.\n  '''\n  \n  request: {\n    url: https://api.example.com/test\n    method: get\n  }\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/bru/examples-simple.bru",
    "content": "meta {\n  name: User API Documentation\n  type: http\n}\n\nget {\n  url: https://api.example.com/users\n}\n\nexample {\n  name: Get User by ID\n  description: Example of getting a user by ID\n  \n  request: {\n    url: https://api.example.com/users/123\n    method: get\n    mode: none\n  }\n\n  response: {\n    status: {\n      code: 200\n    }\n\n    body: {\n      type: json\n      content: '''\n        {\n          \"id\": 123,\n          \"name\": \"John Doe\",\n          \"email\": \"john@example.com\"\n        }\n      '''\n    }\n  }\n}\n\nexample {\n  name: Create New User\n  description: Example of creating a new user\n  \n  request: {\n    url: https://api.example.com/users\n    method: post\n    mode: json\n    body:json: {\n      {\n        \"name\": \"New User\",\n        \"email\": \"newuser@example.com\"\n      }\n    }\n  }\n\n  response: {\n    status: {\n      code: 201\n    }\n    \n    body: {\n      type: json\n      content: '''\n        {\n          \"id\": 456,\n          \"name\": \"New User\",\n          \"email\": \"newuser@example.com\",\n          \"created_at\": \"2023-01-15T10:30:00Z\"\n        }\n      '''\n    }\n  }\n}"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/bru/form-data-complex.bru",
    "content": "meta {\n  name: Form Data Complex\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://api.example.com/upload\n  body: multipart-form\n  auth: bearer\n}\n\nheaders {\n  content-type: multipart/form-data\n  x-upload-version: v2\n}\n\nauth:basic {\n  username: uploader\n  password: upload-secure-pass\n}\n\nauth:bearer {\n  token: upload-token-abc123xyz\n}\n\nbody:multipart-form {\n  document: @file(/path/to/file.pdf)\n  title: Quarterly Report 2024\n  description: Detailed quarterly financial analysis\n  tags: finance,q4,2024\n}\n\nscript:pre-request {\n  const file = bru.readFile(\"/path/to/file.pdf\");\n  bru.setVar(\"file_size\", file.length);\n  bru.setVar(\"file_name\", \"document.pdf\");\n}\n\nexample {\n  name: File Upload with Metadata\n  description: Upload a file with comprehensive metadata\n  \n  request: {\n    url: https://api.example.com/upload\n    method: post\n    mode: multipartForm\n    headers: {\n      authorization: \"Bearer upload-token-abc123xyz\",\n      x-upload-client: \"bruno\"\n    }\n  \n    body:multipart-form: {\n      document: @file(examples/sample.pdf)\n      title: Sample Document\n      description: This is a sample document for testing\n      category: documents\n      tags: sample,test,documents\n      metadata: {\"author\":\"John Doe\",\"version\":\"1.0\",\"date\":\"2024-01-15\"}\n    }\n  }\n  \n  response: {\n    status: {\n      code: 200\n      text: OK\n    }\n  \n    body: {\n      type: json\n      content: '''\n        {\n          \"id\": \"file-12345\",\n          \"filename\": \"sample.pdf\",\n          \"size\": 245760,\n          \"uploaded_at\": \"2024-01-15T14:30:00Z\",\n          \"status\": \"completed\",\n          \"url\": \"https://cdn.example.com/files/sample.pdf\",\n          \"metadata\": {\n            \"title\": \"Sample Document\",\n            \"description\": \"This is a sample document for testing\",\n            \"category\": \"documents\",\n            \"tags\": [\"sample\", \"test\", \"documents\"]\n          }\n        }\n      '''\n    }\n  }\n}\n\nexample {\n  name: Form URL Encoded Data\n  description: Example with form-urlencoded body type\n  \n  request: {\n    url: https://api.example.com/submit\n    method: post\n    mode: formUrlEncoded\n    headers: {\n      content-type: \"application/x-www-form-urlencoded\",\n      authorization: \"Basic dGVzdDp0ZXN0\"\n    }\n  \n    body:form-urlencoded: {\n      username: testuser\n      password: testpass123\n      remember: true\n    }\n  }\n  \n  response: {\n    status: {\n      code: 200\n      text: OK\n    }\n  \n    body: {\n      type: json\n      content: '''\n        {\n          \"success\": true,\n          \"message\": \"Form submitted successfully\",\n          \"session_id\": \"sess-abc123def456\",\n          \"user\": {\n            \"username\": \"testuser\",\n            \"authenticated\": true\n          }\n        }\n      '''\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/bru/jsonToBru-bodytypes.bru",
    "content": "meta {\n  name: Body Types API\n  type: http\n}\n\nget {\n  url: https://api.example.com/test\n}\n\nexample {\n  name: JSON Body Example\n  description: An example with JSON body\n  \n  request: {\n    url: https://api.example.com/json\n    method: post\n    mode: json\n    body:json: {\n      {\n        \"name\": \"Test\",\n        \"value\": 123\n      }\n    }\n  }\n}\n\nexample {\n  name: XML Body Example\n  description: An example with XML body\n  \n  request: {\n    url: https://api.example.com/xml\n    method: post\n    mode: xml\n    body:xml: {\n      <?xml version=\"1.0\"?>\n      <data>\n        <name>Test</name>\n        <value>123</value>\n      </data>\n    }\n  }\n}\n\nexample {\n  name: Text Body Example\n  description: An example with text body\n  \n  request: {\n    url: https://api.example.com/text\n    method: post\n    mode: text\n    body:text: {\n      Plain text data\n      with multiple lines\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/bru/jsonToBru-multiple.bru",
    "content": "meta {\n  name: Multi-Example API\n  type: http\n}\n\nget {\n  url: https://api.example.com/test\n}\n\nexample {\n  name: Example 1\n  description: First example\n  \n  request: {\n    url: https://api.example.com/example1\n    method: get\n    mode: none\n  }\n}\n\nexample {\n  name: Example 2\n  description: Second example with JSON body\n  \n  request: {\n    url: https://api.example.com/example2\n    method: get\n    mode: json\n    body:json: {\n      {\n        \"data\": \"test2\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/bru/jsonToBru-response.bru",
    "content": "meta {\n  name: Response Example API\n  type: http\n}\n\nget {\n  url: https://api.example.com/test\n}\n\nexample {\n  name: Response Example\n  description: An example with response data\n  \n  request: {\n    url: https://api.example.com/users/123\n    method: get\n    mode: none\n  }\n  \n  response: {\n    headers: {\n      content-type: application/json\n    }\n  \n    status: {\n      code: 200\n      text: OK\n    }\n  \n    body: {\n      type: json\n      content: '''\n        {\n          \"id\": 123,\n          \"name\": \"John Doe\"\n        }\n      '''\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/bru/jsonToBru-simple.bru",
    "content": "meta {\n  name: Test API\n  type: http\n}\n\nget {\n  url: https://api.example.com/test\n}\n\nexample {\n  name: Example Request\n  description: A simple example request\n  \n  request: {\n    url: https://api.example.com/example\n    method: post\n    mode: json\n    body:json: {\n      {\n        \"data\": \"test\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/bru/multiple-examples-variations.bru",
    "content": "meta {\n  name: Multiple Examples Variations\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://api.example.com/data\n}\n\nparams:query {\n  page: 1\n  limit: 10\n  sort: desc\n}\n\nheaders {\n  accept: application/json\n  x-client: bruno\n}\n\nauth:bearer {\n  token: token-for-multiple-examples\n}\n\nbody:json {\n  {\n    \"query\": \"search\",\n    \"filters\": []\n  }\n}\n\nexample {\n  name: Simple GET Request\n  description: Basic GET request with minimal data\n  \n  request: {\n    url: https://api.example.com/data?id=123\n    method: get\n    mode: none\n    headers: {\n      accept: \"application/json\"\n    }\n  }\n  \n  response: {\n    status: {\n      code: 200\n      text: OK\n    }\n  \n    body: {\n      type: json\n      content: '''\n        {\n          \"id\": 123,\n          \"name\": \"Item\",\n          \"value\": 42\n        }\n      '''\n    }\n  }\n}\n\nexample {\n  name: Complex Search Query\n  description: GET request with complex query parameters\n  \n  request: {\n    url: https://api.example.com/data?page=1&limit=20&sort=name&order=asc&filter[status]=active&filter[type]=premium\n    method: get\n    mode: none\n    headers: {\n      accept: \"application/json\",\n      x-api-key: \"search-key-789\"\n    }\n  }\n  \n  response: {\n    status: {\n      code: 200\n      text: OK\n    }\n  \n    body: {\n      type: json\n      content: '''\n        {\n          \"data\": [\n            {\n              \"id\": 1,\n              \"name\": \"Premium Item 1\",\n              \"status\": \"active\",\n              \"type\": \"premium\"\n            },\n            {\n              \"id\": 2,\n              \"name\": \"Premium Item 2\",  \n              \"status\": \"active\",\n              \"type\": \"premium\"\n            }\n          ],\n          \"pagination\": {\n            \"page\": 1,\n            \"limit\": 20,\n            \"total\": 2\n          }\n        }\n      '''\n    }\n  }\n}\n\nexample {\n  name: Text Response Example\n  description: Request returning plain text response\n  \n  request: {\n    url: https://api.example.com/data/text\n    method: get\n    mode: none\n    headers: {\n      accept: \"text/plain\"\n    }\n  }\n  \n  response: {\n    status: {\n      code: 200\n      text: OK\n    }\n  \n    body: {\n      type: text\n      content: '''\n        This is a plain text response\n        Multiple lines of content\n        No JSON formatting needed\n      '''\n    }\n  }\n}\n\nexample {\n  name: XML Response with Nested Structure\n  description: Example with complex XML response\n  \n  request: {\n    url: https://api.example.com/data/xml\n    method: get\n    mode: none\n    headers: {\n      accept: \"application/xml\"\n    }\n  }\n  \n  response: {\n    status: {\n      code: 200\n      text: OK\n    }\n  \n    body: {\n      type: xml\n      content: '''\n        <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n        <response>\n          <metadata>\n            <version>1.0</version>\n            <timestamp>2024-01-15T15:00:00Z</timestamp>\n          </metadata>\n          <data>\n            <item id=\"1\">\n              <name>Item One</name>\n              <value>100</value>\n              <nested>\n                <attribute key=\"type\">primary</attribute>\n              </nested>\n            </item>\n            <item id=\"2\">\n              <name>Item Two</name>\n              <value>200</value>\n              <nested>\n                <attribute key=\"type\">secondary</attribute>\n              </nested>\n            </item>\n          </data>\n        </response>\n      '''\n    }\n  }\n}\n\nexample {\n  name: GraphQL Query Example\n  description: Example with GraphQL query body\n  \n  request: {\n    url: https://api.example.com/graphql\n    method: post\n    mode: graphql\n    headers: {\n      content-type: \"application/json\",\n      authorization: \"Bearer graphql-token-xyz\"\n    }\n  \n    body:graphql: {\n      query {\n        user(id: \"123\") {\n          id\n          name\n          email\n          posts {\n            title\n            content\n          }\n        }\n      }\n    }\n  }\n  \n  response: {\n    status: {\n      code: 200\n      text: OK\n    }\n  \n    body: {\n      type: json\n      content: '''\n        {\n          \"data\": {\n            \"user\": {\n              \"id\": \"123\",\n              \"name\": \"John Doe\",\n              \"email\": \"john@example.com\",\n              \"posts\": [\n                {\n                  \"title\": \"First Post\",\n                  \"content\": \"This is my first blog post\"\n                }\n              ]\n            }\n          }\n        }\n      '''\n    }\n  }\n}\n\nexample {\n  name: SPARQL Query Example\n  description: Example with SPARQL query\n  \n  request: {\n    url: https://sparql.example.com/query\n    method: post\n    mode: sparql\n    headers: {\n      content-type: \"application/sparql-query\"\n    }\n  \n    body:sparql: {\n      SELECT ?name ?email WHERE {\n        ?person <http://example.org/name> ?name .\n        ?person <http://example.org/email> ?email .\n        ?person <http://example.org/age> ?age .\n        FILTER(?age > 18)\n      }\n    }\n  }\n  \n  response: {\n    status: {\n      code: 200\n      text: OK\n    }\n  \n    body: {\n      type: json\n      content: '''\n        {\n          \"results\": {\n            \"bindings\": [\n              {\n                \"name\": {\n                  \"value\": \"John Doe\"\n                },\n                \"email\": {\n                  \"value\": \"john@example.com\"\n                }\n              }\n            ]\n          }\n        }\n      '''\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/bru/oauth2-examples.bru",
    "content": "meta {\n  name: OAuth2 Examples API\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://api.example.com/oauth/protected\n  body: json\n  auth: oauth2\n}\n\nheaders {\n  content-type: application/json\n}\n\nauth:oauth2 {\n  grant_type: authorization_code\n  callback_url: https://api.example.com/oauth/callback\n  authorization_url: https://oauth.example.com/authorize\n  access_token_url: https://oauth.example.com/token\n  refresh_token_url: https://oauth.example.com/token\n  client_id: my-client-id\n  client_secret: my-client-secret\n  scope: read write\n  state: \n  pkce: true\n  credentials_placement: header\n  credentials_id: authorization\n  token_source: access_token\n  token_placement: header\n  token_header_prefix: Bearer\n  auto_fetch_token: true\n  auto_refresh_token: true\n}\n\nbody:json {\n  {\n    \"action\": \"test\",\n    \"data\": {\n      \"message\": \"Protected resource access\"\n    }\n  }\n}\n\nvars:pre-request {\n  oauth_state: {{$uuid}}\n  client_scopes: read,write,admin\n}\n\nscript:pre-request {\n  const state = crypto.randomBytes(16).toString('hex');\n  bru.setVar('oauth_state', state);\n  bru.setVar('timestamp', Date.now());\n}\n\ntests {\n  test(\"Response should be 200\", function() {\n    expect(res.getStatus()).to.eql(200);\n  });\n  \n  test(\"Should have user data in response\", function() {\n    const body = res.getBody();\n    expect(body.access_token).to.be.ok;\n  });\n}\n\ndocs {\n  This collection demonstrates OAuth2 authentication flows.\n  Supports authorization code, client credentials, and password grant types.\n  Examples show token refresh and protected resource access.\n}\n\nexample {\n  name: OAuth2 Protected Resource\n  description: Example accessing resource protected with OAuth2 authorization code flow\n  \n  request: {\n    url: https://api.example.com/oauth/protected\n    method: post\n    mode: json\n    headers: {\n      authorization: \"Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9\",\n      content-type: \"application/json\"\n    }\n  \n    body:json: {\n      {\n        \"action\": \"fetch\",\n        \"resource\": \"user_profile\"\n      }\n    }\n  }\n  \n  response: {\n    status: {\n      code: 200\n      text: OK\n    }\n  \n    body: {\n      type: json\n      content: '''\n        {\n          \"user\": {\n            \"id\": \"123\",\n            \"name\": \"John Doe\",\n            \"email\": \"john@example.com\",\n            \"scopes\": [\"read\", \"write\"]\n          },\n          \"token\": {\n            \"access_token\": \"access_token_abc123\",\n            \"expires_in\": 3600,\n            \"token_type\": \"Bearer\"\n          }\n        }\n      '''\n    }\n  }\n}\n\nexample {\n  name: OAuth2 Token Refresh\n  description: Example demonstrating OAuth2 token refresh flow\n  \n  request: {\n    url: https://api.example.com/oauth/token\n    method: post\n    mode: json\n    headers: {\n      content-type: \"application/json\",\n      accept: \"application/json\"\n    }\n  \n    body:json: {\n      {\n        \"grant_type\": \"refresh_token\",\n        \"refresh_token\": \"refresh_token_xyz789\",\n        \"client_id\": \"my-client-id\",\n        \"client_secret\": \"my-client-secret\"\n      }\n    }\n  }\n  \n  response: {\n    status: {\n      code: 200\n      text: OK\n    }\n  \n    body: {\n      type: json\n      content: '''\n        {\n          \"access_token\": \"new_access_token_def456\",\n          \"refresh_token\": \"new_refresh_token_abc789\",\n          \"expires_in\": 3600,\n          \"token_type\": \"Bearer\",\n          \"scope\": \"read write\"\n        }\n      '''\n    }\n  }\n}\n\nexample {\n  name: OAuth2 Client Credentials\n  description: Example using OAuth2 client credentials grant type\n  \n  request: {\n    url: https://api.example.com/oauth/client-credentials\n    method: post\n    mode: json\n    headers: {\n      content-type: \"application/json\"\n    }\n  \n    body:json: {\n      {\n        \"grant_type\": \"client_credentials\",\n        \"client_id\": \"service-account\",\n        \"client_secret\": \"service-secret-key\",\n        \"scope\": \"admin\"\n      }\n    }\n  }\n  \n  response: {\n    status: {\n      code: 200\n      text: OK\n    }\n  \n    body: {\n      type: json\n      content: '''\n        {\n          \"access_token\": \"service_access_token_123\",\n          \"expires_in\": 7200,\n          \"token_type\": \"Bearer\",\n          \"scope\": \"admin\"\n        }\n      '''\n    }\n  }\n}\n\nexample {\n  name: OAuth2 Password Grant\n  description: Example using OAuth2 password grant (username/password)\n  \n  request: {\n    url: https://api.example.com/oauth/password\n    method: post\n    mode: json\n    headers: {\n      content-type: \"application/json\"\n    }\n  \n    body:json: {\n      {\n        \"grant_type\": \"password\",\n        \"username\": \"user@example.com\",\n        \"password\": \"SecurePass123!\",\n        \"client_id\": \"mobile-app\",\n        \"client_secret\": \"mobile-app-secret\"\n      }\n    }\n  }\n  \n  response: {\n    status: {\n      code: 200\n      text: OK\n    }\n  \n    body: {\n      type: json\n      content: '''\n        {\n          \"access_token\": \"user_access_token_456\",\n          \"refresh_token\": \"user_refresh_token_789\",\n          \"expires_in\": 3600,\n          \"token_type\": \"Bearer\"\n        }\n      '''\n    }\n  }\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/json/bruToJson-empty-example.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Test API\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"examples\": [\n    {}\n  ]\n}"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/json/bruToJson-json-body.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Test API\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"examples\": [\n    {\n      \"name\": \"JSON Example\",\n      \"description\": \"An example with JSON body\",\n      \"request\": {\n        \"url\": \"https://api.example.com/data\",\n        \"method\": \"post\",\n        \"body\": {\n          \"mode\": \"json\",\n          \"json\": \"{\\n  \\\"name\\\": \\\"Test\\\",\\n  \\\"value\\\": 123\\n}\"\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/json/bruToJson-multiple-examples.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Test API\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"http\": {\n    \"method\": \"get\",\n    \"url\": \"https://api.example.com/test\"\n  },\n  \"examples\": [\n    {\n      \"name\": \"Example 1\",\n      \"description\": \"First example\",\n      \"request\": {\n        \"url\": \"https://api.example.com/example1\",\n        \"method\": \"get\",\n        \"body\": {\n          \"mode\": \"none\"\n        }\n      }\n    },\n    {\n      \"name\": \"Example 2\",\n      \"description\": \"Second example with JSON body\",\n      \"request\": {\n        \"url\": \"https://api.example.com/example2\",\n        \"method\": \"get\",\n        \"body\": {\n          \"mode\": \"json\",\n          \"json\": \"{\\n  \\\"data\\\": \\\"test\\\"\\n}\"\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/json/bruToJson-no-examples.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Test API\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"http\": {\n    \"method\": \"get\",\n    \"url\": \"https://api.example.com/test\"\n  }\n}"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/json/bruToJson-response-example.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Test API\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"http\": {\n    \"method\": \"get\",\n    \"url\": \"https://api.example.com/test\"\n  },\n  \"examples\": [\n    {\n      \"name\": \"Example with Response\",\n      \"description\": \"An example with response data\",\n      \"request\": {\n        \"url\": \"https://api.example.com/users/123\",\n        \"method\": \"get\",\n        \"body\": {\n          \"mode\": \"none\"\n        }\n      },\n      \"response\": {\n        \"headers\": [\n          {\n            \"name\": \"content-type\",\n            \"value\": \"application/json\",\n            \"enabled\": true\n          }\n        ],\n        \"status\": \"200\",\n        \"statusText\": \"OK\",\n        \"body\": {\n          \"type\": \"json\",\n          \"content\": \"{\\n  \\\"id\\\": 123,\\n  \\\"name\\\": \\\"John Doe\\\"\\n}\"\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/json/bruToJson-single-example.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Test API\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"http\": {\n    \"method\": \"get\",\n    \"url\": \"https://api.example.com/test\"\n  },\n  \"examples\": [\n    {\n      \"name\": \"Example Request\",\n      \"description\": \"A simple example request\",\n      \"request\": {\n        \"url\": \"https://api.example.com/example\",\n        \"method\": \"get\",\n        \"body\": {\n          \"mode\": \"none\"\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/json/bruToJson-text-body.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Test API\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"examples\": [\n    {\n      \"name\": \"Text Example\",\n      \"description\": \"An example with text body\",\n      \"request\": {\n        \"url\": \"https://api.example.com/data\",\n        \"method\": \"post\",\n        \"body\": {\n          \"mode\": \"text\",\n          \"text\": \"Plain text data\\nwith multiple lines\"\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/json/bruToJson-xml-body.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Test API\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"examples\": [\n    {\n      \"name\": \"XML Example\",\n      \"description\": \"An example with XML body\",\n      \"request\": {\n        \"url\": \"https://api.example.com/data\",\n        \"method\": \"post\",\n        \"body\": {\n          \"mode\": \"xml\",\n          \"xml\": \"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\\n<data>\\n  <name>Test</name>\\n  <value>123</value>\\n</data>\"\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/json/complex-with-auth.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Complex API with Auth\",\n    \"type\": \"http\",\n    \"seq\": \"1\"\n  },\n  \"http\": {\n    \"method\": \"post\",\n    \"url\": \"https://api.example.com/users\",\n    \"body\": \"json\",\n    \"auth\": \"bearer\"\n  },\n  \"headers\": [\n    {\n      \"name\": \"content-type\",\n      \"value\": \"application/json\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"x-api-key\",\n      \"value\": \"my-secret-key\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"x-request-id\",\n      \"value\": \"{{$uuid}}\",\n      \"enabled\": true\n    }\n  ],\n  \"params\": [\n    {\n      \"name\": \"include\",\n      \"value\": \"details\",\n      \"enabled\": true,\n      \"type\": \"query\"\n    },\n    {\n      \"name\": \"format\",\n      \"value\": \"json\",\n      \"enabled\": true,\n      \"type\": \"query\"\n    },\n    {\n      \"name\": \"id\",\n      \"value\": \"123\",\n      \"enabled\": true,\n      \"type\": \"path\"\n    },\n    {\n      \"name\": \"status\",\n      \"value\": \"active\",\n      \"enabled\": true,\n      \"type\": \"path\"\n    }\n  ],\n  \"auth\": {\n    \"basic\": {\n      \"username\": \"admin\",\n      \"password\": \"secret123\"\n    },\n    \"bearer\": {\n      \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHluKJe2vncyz4h4SH6FSS7YOKIx\"\n    },\n    \"apikey\": {\n      \"key\": \"x-api-key\",\n      \"value\": \"api-secret-key-12345\",\n      \"placement\": \"header\"\n    }\n  },\n  \"body\": {\n    \"json\": \"{\\n  \\\"username\\\": \\\"johndoe\\\",\\n  \\\"email\\\": \\\"john@example.com\\\",\\n  \\\"profile\\\": {\\n    \\\"firstName\\\": \\\"John\\\",\\n    \\\"lastName\\\": \\\"Doe\\\",\\n    \\\"age\\\": 30\\n  },\\n  \\\"preferences\\\": {\\n    \\\"theme\\\": \\\"dark\\\",\\n    \\\"notifications\\\": true\\n  },\\n  \\\"tags\\\": [\\\"admin\\\", \\\"user\\\", \\\"verified\\\"]\\n}\"\n  },\n  \"script\": {\n    \"req\": \"const timestamp = Date.now();\\nbru.setVar(\\\"request_timestamp\\\", timestamp);\\nbru.setVar(\\\"signature\\\", crypto.createHash('md5').update(timestamp.toString()).digest('hex'));\"\n  },\n  \"vars\": {\n    \"req\": [\n      {\n        \"name\": \"user_id\",\n        \"value\": \"12345\",\n        \"enabled\": true,\n        \"local\": false\n      },\n      {\n        \"name\": \"environment\",\n        \"value\": \"production\",\n        \"enabled\": true,\n        \"local\": false\n      }\n    ],\n    \"res\": [\n      {\n        \"name\": \"response_id\",\n        \"value\": \"{{res.id}}\",\n        \"enabled\": true,\n        \"local\": false\n      },\n      {\n        \"name\": \"processed_at\",\n        \"value\": \"{{$timestamp}}\",\n        \"enabled\": true,\n        \"local\": false\n      }\n    ]\n  },\n  \"tests\": \"test(\\\"Response should be 201\\\", function() {\\n  expect(res.getStatus()).to.eql(201);\\n});\\n\\ntest(\\\"Response should have user data\\\", function() {\\n  const body = res.getBody();\\n  expect(body.username).to.be.ok;\\n  expect(body.email).to.be.ok;\\n});\",\n  \"docs\": \"This endpoint creates a new user account.\\nRequires authentication via Bearer token or API key.\\nSupports nested JSON structures with arrays.\",\n  \"examples\": [\n    {\n      \"name\": \"Basic User Creation\",\n      \"description\": \"Creates a new user with minimal required fields\",\n      \"request\": {\n        \"url\": \"https://api.example.com/users\",\n        \"method\": \"post\",\n        \"body\": {\n          \"mode\": \"json\",\n          \"json\": \"{\\n  \\\"username\\\": \\\"newuser\\\",\\n  \\\"email\\\": \\\"newuser@example.com\\\",\\n  \\\"password\\\": \\\"SecurePass123!\\\"\\n}\"\n        },\n        \"headers\": [\n          {\n            \"name\": \"authorization\",\n            \"value\": \"\\\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\\\",\",\n            \"enabled\": true\n          },\n          {\n            \"name\": \"content-type\",\n            \"value\": \"\\\"application/json\\\"\",\n            \"enabled\": true\n          }\n        ]\n      },\n      \"response\": {\n        \"status\": \"201\",\n        \"statusText\": \"OK\",\n        \"body\": {\n          \"type\": \"json\",\n          \"content\": \"{\\n  \\\"id\\\": 100,\\n  \\\"username\\\": \\\"newuser\\\",\\n  \\\"email\\\": \\\"newuser@example.com\\\",\\n  \\\"created_at\\\": \\\"2024-01-15T10:30:00Z\\\",\\n  \\\"status\\\": \\\"active\\\"\\n}\"\n        }\n      }\n    },\n    {\n      \"name\": \"Advanced User with Profile\",\n      \"description\": \"Creates a user with complete profile information and nested data\",\n      \"request\": {\n        \"url\": \"https://api.example.com/users\",\n        \"method\": \"post\",\n        \"body\": {\n          \"mode\": \"json\",\n          \"json\": \"{\\n  \\\"username\\\": \\\"developer\\\",\\n  \\\"email\\\": \\\"dev@example.com\\\",\\n  \\\"password\\\": \\\"DevPass2024!\\\",\\n  \\\"profile\\\": {\\n    \\\"firstName\\\": \\\"Jane\\\",\\n    \\\"lastName\\\": \\\"Developer\\\",\\n    \\\"bio\\\": \\\"Software engineer passionate about APIs\\\",\\n    \\\"location\\\": {\\n      \\\"city\\\": \\\"San Francisco\\\",\\n      \\\"country\\\": \\\"USA\\\",\\n      \\\"timezone\\\": \\\"America/Los_Angeles\\\"\\n    },\\n    \\\"social\\\": {\\n      \\\"github\\\": \\\"jandeveloper\\\",\\n      \\\"twitter\\\": \\\"@jane_dev\\\"\\n    }\\n  },\\n  \\\"skills\\\": [\\\"JavaScript\\\", \\\"Node.js\\\", \\\"React\\\", \\\"API Design\\\"],\\n  \\\"experience\\\": 5,\\n  \\\"preferences\\\": {\\n    \\\"theme\\\": \\\"dark\\\",\\n    \\\"language\\\": \\\"en\\\",\\n    \\\"notifications\\\": {\\n      \\\"email\\\": true,\\n      \\\"push\\\": true,\\n      \\\"sms\\\": false\\n    }\\n  }\\n}\"\n        },\n        \"headers\": [\n          {\n            \"name\": \"authorization\",\n            \"value\": \"\\\"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\\\",\",\n            \"enabled\": true\n          },\n          {\n            \"name\": \"content-type\",\n            \"value\": \"\\\"application/json\\\",\",\n            \"enabled\": true\n          },\n          {\n            \"name\": \"x-api-key\",\n            \"value\": \"\\\"advanced-api-key-45678\\\"\",\n            \"enabled\": true\n          }\n        ]\n      },\n      \"response\": {\n        \"status\": \"201\",\n        \"statusText\": \"OK\",\n        \"body\": {\n          \"type\": \"json\",\n          \"content\": \"{\\n  \\\"id\\\": 101,\\n  \\\"username\\\": \\\"developer\\\",\\n  \\\"email\\\": \\\"dev@example.com\\\",\\n  \\\"profile\\\": {\\n    \\\"firstName\\\": \\\"Jane\\\",\\n    \\\"lastName\\\": \\\"Developer\\\",\\n    \\\"bio\\\": \\\"Software engineer passionate about APIs\\\",\\n    \\\"location\\\": {\\n      \\\"city\\\": \\\"San Francisco\\\",\\n      \\\"country\\\": \\\"USA\\\",\\n      \\\"timezone\\\": \\\"America/Los_Angeles\\\"\\n    }\\n  },\\n  \\\"skills\\\": [\\\"JavaScript\\\", \\\"Node.js\\\", \\\"React\\\", \\\"API Design\\\"],\\n  \\\"experience\\\": 5,\\n  \\\"created_at\\\": \\\"2024-01-15T12:45:30Z\\\",\\n  \\\"status\\\": \\\"active\\\",\\n  \\\"verified\\\": false\\n}\"\n        }\n      }\n    },\n    {\n      \"name\": \"XML Data Example\",\n      \"description\": \"Example with XML format and complex structure\",\n      \"request\": {\n        \"url\": \"https://api.example.com/users/xml\",\n        \"method\": \"post\",\n        \"body\": {\n          \"mode\": \"xml\",\n          \"xml\": \"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\\n<user>\\n  <username>xmluser</username>\\n  <email>xml@example.com</email>\\n  <profile>\\n    <firstName>XML</firstName>\\n    <lastName>User</lastName>\\n  </profile>\\n</user>\"\n        },\n        \"headers\": [\n          {\n            \"name\": \"content-type\",\n            \"value\": \"\\\"application/xml\\\",\",\n            \"enabled\": true\n          },\n          {\n            \"name\": \"authorization\",\n            \"value\": \"\\\"Basic YWRtaW46cGFzc3dvcmQ=\\\"\",\n            \"enabled\": true\n          }\n        ]\n      },\n      \"response\": {\n        \"status\": \"201\",\n        \"statusText\": \"OK\",\n        \"body\": {\n          \"type\": \"xml\",\n          \"content\": \"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\\n<response>\\n  <id>102</id>\\n  <status>created</status>\\n  <user>\\n    <username>xmluser</username>\\n    <email>xml@example.com</email>\\n  </user>\\n</response>\"\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/json/examples-complex.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Multi-format API\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"http\": {\n    \"method\": \"get\",\n    \"url\": \"https://api.example.com/data\"\n  },\n  \"headers\": [\n    {\n      \"name\": \"content-type\",\n      \"value\": \"application/json\",\n      \"enabled\": true\n    }\n  ],\n  \"body\": {\n    \"json\": \"{\\n  \\\"message\\\": \\\"API supports multiple formats\\\"\\n}\"\n  },\n  \"examples\": [\n    {\n      \"name\": \"JSON API Example\",\n      \"description\": \"An example using JSON format\",\n      \"request\": {\n        \"url\": \"https://api.example.com/json\",\n        \"method\": \"post\",\n        \"body\": {\n          \"mode\": \"json\",\n          \"json\": \"{\\n  \\\"format\\\": \\\"json\\\",\\n  \\\"data\\\": {\\n    \\\"name\\\": \\\"JSON Example\\\",\\n    \\\"value\\\": 123\\n  }\\n}\"\n        }\n      },\n      \"response\": {\n        \"status\": \"201\",\n        \"statusText\": \"Created\",\n        \"body\": {\n          \"type\": \"json\",\n          \"content\": \"{\\n  \\\"id\\\": 1,\\n  \\\"format\\\": \\\"json\\\",\\n  \\\"created\\\": true\\n}\"\n        }\n      }\n    },\n    {\n      \"name\": \"XML API Example\",\n      \"description\": \"An example using XML format\",\n      \"request\": {\n        \"url\": \"https://api.example.com/xml\",\n        \"method\": \"post\",\n        \"body\": {\n          \"mode\": \"xml\",\n          \"xml\": \"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\\n<data>\\n  <format>xml</format>\\n  <name>XML Example</name>\\n  <value>456</value>\\n</data>\"\n        }\n      },\n      \"response\": {\n        \"status\": \"201\",\n        \"statusText\": \"Created\",\n        \"body\": {\n          \"type\": \"xml\",\n          \"content\": \"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\\n<response>\\n  <id>2</id>\\n  <format>xml</format>\\n  <created>true</created>\\n</response>\"\n        }\n      }\n    },\n    {\n      \"name\": \"Text API Example\",\n      \"description\": \"An example using text format\",\n      \"request\": {\n        \"url\": \"https://api.example.com/text\",\n        \"method\": \"post\",\n        \"body\": {\n          \"mode\": \"text\",\n          \"text\": \"Plain text data\\nFormat: text\\nName: Text Example\\nValue: 789\"\n        }\n      },\n      \"response\": {\n        \"status\": \"201\",\n        \"statusText\": \"Created\",\n        \"body\": {\n          \"type\": \"text\",\n          \"content\": \"Success: true\\nFormat: text\\nID: 3\"\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/json/examples-multiline-contenttype.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Multiline ContentType Test\",\n    \"type\": \"http\",\n    \"seq\": \"1\"\n  },\n  \"http\": {\n    \"method\": \"post\",\n    \"url\": \"{{host}}/test\",\n    \"body\": \"multipartForm\"\n  },\n  \"examples\": [\n    {\n      \"name\": \"Example with multiline strings and contentType\",\n      \"request\": {\n        \"url\": \"{{host}}/test\",\n        \"method\": \"POST\",\n        \"body\": {\n          \"mode\": \"multipartForm\",\n          \"multipartForm\": [\n            {\n              \"name\": \"test\",\n              \"value\": \"{\\n\\\"hello\\\" : \\\"there\\\"\\n}\",\n              \"enabled\": true,\n              \"type\": \"text\",\n              \"contentType\": \"application/json\"\n            },\n            {\n              \"name\": \"simple\",\n              \"value\": \"cat and mouse\",\n              \"enabled\": true,\n              \"type\": \"text\",\n              \"contentType\": \"text/plain\"\n            },\n            {\n              \"name\": \"array\",\n              \"value\": \"[\\n\\\"coolade\\\", \\n\\\"blast\\\"\\n]\",\n              \"enabled\": true,\n              \"type\": \"text\",\n              \"contentType\": \"\"\n            },\n            {\n              \"name\": \"jsonValue\",\n              \"value\": \"{\\n  \\\"key\\\": \\\"value\\\",\\n  \\\"nested\\\": {\\n    \\\"data\\\": 123\\n  }\\n}\",\n              \"enabled\": true,\n              \"type\": \"text\",\n              \"contentType\": \"application/json\"\n            },\n            {\n              \"name\": \"textValue\",\n              \"value\": \"This is a\\nmultiline text\\nvalue\",\n              \"enabled\": true,\n              \"type\": \"text\",\n              \"contentType\": \"text/plain\"\n            }\n          ]\n        }\n      },\n      \"response\": {\n        \"status\": \"200\",\n        \"statusText\": \"OK\",\n        \"body\": {\n          \"type\": \"json\",\n          \"content\": \"{\\n  \\\"status\\\": \\\"success\\\",\\n  \\\"message\\\": \\\"Data received\\\"\\n}\"\n        }\n      }\n    }\n  ]\n}\n\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/json/examples-multiline-description.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Multiline Description Test\",\n    \"type\": \"http\",\n    \"seq\": \"1\"\n  },\n  \"http\": {\n    \"method\": \"get\",\n    \"url\": \"https://api.example.com/test\"\n  },\n  \"examples\": [\n    {\n      \"name\": \"Test Example\",\n      \"description\": \"This is a multiline description.\\nIt spans multiple lines.\\nAnd should be parsed correctly.\",\n      \"request\": {\n        \"url\": \"https://api.example.com/test\",\n        \"method\": \"get\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/json/examples-simple.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"User API Documentation\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"http\": {\n    \"method\": \"get\",\n    \"url\": \"https://api.example.com/users\"\n  },\n  \"examples\": [\n    {\n      \"name\": \"Get User by ID\",\n      \"description\": \"Example of getting a user by ID\",\n      \"request\": {\n        \"url\": \"https://api.example.com/users/123\",\n        \"method\": \"get\",\n        \"body\": {\n          \"mode\": \"none\"\n        }\n      },\n      \"response\": {\n        \"status\": \"200\",\n        \"statusText\": \"OK\",\n        \"body\": {\n          \"type\": \"json\",\n          \"content\": \"{\\n  \\\"id\\\": 123,\\n  \\\"name\\\": \\\"John Doe\\\",\\n  \\\"email\\\": \\\"john@example.com\\\"\\n}\"\n        }\n      }\n    },\n    {\n      \"name\": \"Create New User\",\n      \"description\": \"Example of creating a new user\",\n      \"request\": {\n        \"url\": \"https://api.example.com/users\",\n        \"method\": \"post\",\n        \"body\": {\n          \"mode\": \"json\",\n          \"json\": \"{\\n  \\\"name\\\": \\\"New User\\\",\\n  \\\"email\\\": \\\"newuser@example.com\\\"\\n}\"\n        }\n      },\n      \"response\": {\n        \"status\": \"201\",\n        \"statusText\": \"OK\",\n        \"body\": {\n          \"type\": \"json\",\n          \"content\": \"{\\n  \\\"id\\\": 456,\\n  \\\"name\\\": \\\"New User\\\",\\n  \\\"email\\\": \\\"newuser@example.com\\\",\\n  \\\"created_at\\\": \\\"2023-01-15T10:30:00Z\\\"\\n}\"\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/json/form-data-complex.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Form Data Complex\",\n    \"type\": \"http\",\n    \"seq\": \"1\"\n  },\n  \"http\": {\n    \"method\": \"post\",\n    \"url\": \"https://api.example.com/upload\",\n    \"body\": \"multipart-form\",\n    \"auth\": \"bearer\"\n  },\n  \"headers\": [\n    {\n      \"name\": \"content-type\",\n      \"value\": \"multipart/form-data\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"x-upload-version\",\n      \"value\": \"v2\",\n      \"enabled\": true\n    }\n  ],\n  \"auth\": {\n    \"basic\": {\n      \"username\": \"uploader\",\n      \"password\": \"upload-secure-pass\"\n    },\n    \"bearer\": {\n      \"token\": \"upload-token-abc123xyz\"\n    }\n  },\n  \"body\": {\n    \"multipartForm\": [\n      {\n        \"name\": \"document\",\n        \"value\": [\n          \"/path/to/file.pdf\"\n        ],\n        \"enabled\": true,\n        \"type\": \"file\",\n        \"contentType\": \"\"\n      },\n      {\n        \"name\": \"title\",\n        \"value\": \"Quarterly Report 2024\",\n        \"enabled\": true,\n        \"type\": \"text\",\n        \"contentType\": \"\"\n      },\n      {\n        \"name\": \"description\",\n        \"value\": \"Detailed quarterly financial analysis\",\n        \"enabled\": true,\n        \"type\": \"text\",\n        \"contentType\": \"\"\n      },\n      {\n        \"name\": \"tags\",\n        \"value\": \"finance,q4,2024\",\n        \"enabled\": true,\n        \"type\": \"text\",\n        \"contentType\": \"\"\n      }\n    ]\n  },\n  \"script\": {\n    \"req\": \"const file = bru.readFile(\\\"/path/to/file.pdf\\\");\\nbru.setVar(\\\"file_size\\\", file.length);\\nbru.setVar(\\\"file_name\\\", \\\"document.pdf\\\");\"\n  },\n  \"examples\": [\n    {\n      \"name\": \"File Upload with Metadata\",\n      \"description\": \"Upload a file with comprehensive metadata\",\n      \"request\": {\n        \"url\": \"https://api.example.com/upload\",\n        \"method\": \"post\",\n        \"body\": {\n          \"mode\": \"multipartForm\",\n          \"multipartForm\": [\n            {\n              \"name\": \"document\",\n              \"value\": [\n                \"examples/sample.pdf\"\n              ],\n              \"enabled\": true,\n              \"type\": \"file\",\n              \"contentType\": \"\"\n            },\n            {\n              \"name\": \"title\",\n              \"value\": \"Sample Document\",\n              \"enabled\": true,\n              \"type\": \"text\",\n              \"contentType\": \"\"\n            },\n            {\n              \"name\": \"description\",\n              \"value\": \"This is a sample document for testing\",\n              \"enabled\": true,\n              \"type\": \"text\",\n              \"contentType\": \"\"\n            },\n            {\n              \"name\": \"category\",\n              \"value\": \"documents\",\n              \"enabled\": true,\n              \"type\": \"text\",\n              \"contentType\": \"\"\n            },\n            {\n              \"name\": \"tags\",\n              \"value\": \"sample,test,documents\",\n              \"enabled\": true,\n              \"type\": \"text\",\n              \"contentType\": \"\"\n            },\n            {\n              \"name\": \"metadata\",\n              \"value\": \"{\\\"author\\\":\\\"John Doe\\\",\\\"version\\\":\\\"1.0\\\",\\\"date\\\":\\\"2024-01-15\\\"}\",\n              \"enabled\": true,\n              \"type\": \"text\",\n              \"contentType\": \"\"\n            }\n          ]\n        },\n        \"headers\": [\n          {\n            \"name\": \"authorization\",\n            \"value\": \"\\\"Bearer upload-token-abc123xyz\\\",\",\n            \"enabled\": true\n          },\n          {\n            \"name\": \"x-upload-client\",\n            \"value\": \"\\\"bruno\\\"\",\n            \"enabled\": true\n          }\n        ]\n      },\n      \"response\": {\n        \"status\": \"200\",\n        \"statusText\": \"OK\",\n        \"body\": {\n          \"type\": \"json\",\n          \"content\": \"{\\n  \\\"id\\\": \\\"file-12345\\\",\\n  \\\"filename\\\": \\\"sample.pdf\\\",\\n  \\\"size\\\": 245760,\\n  \\\"uploaded_at\\\": \\\"2024-01-15T14:30:00Z\\\",\\n  \\\"status\\\": \\\"completed\\\",\\n  \\\"url\\\": \\\"https://cdn.example.com/files/sample.pdf\\\",\\n  \\\"metadata\\\": {\\n    \\\"title\\\": \\\"Sample Document\\\",\\n    \\\"description\\\": \\\"This is a sample document for testing\\\",\\n    \\\"category\\\": \\\"documents\\\",\\n    \\\"tags\\\": [\\\"sample\\\", \\\"test\\\", \\\"documents\\\"]\\n  }\\n}\"\n        }\n      }\n    },\n    {\n      \"name\": \"Form URL Encoded Data\",\n      \"description\": \"Example with form-urlencoded body type\",\n      \"request\": {\n        \"url\": \"https://api.example.com/submit\",\n        \"method\": \"post\",\n        \"body\": {\n          \"mode\": \"formUrlEncoded\",\n          \"formUrlEncoded\": [\n            {\n              \"name\": \"username\",\n              \"value\": \"testuser\",\n              \"enabled\": true\n            },\n            {\n              \"name\": \"password\",\n              \"value\": \"testpass123\",\n              \"enabled\": true\n            },\n            {\n              \"name\": \"remember\",\n              \"value\": \"true\",\n              \"enabled\": true\n            }\n          ]\n        },\n        \"headers\": [\n          {\n            \"name\": \"content-type\",\n            \"value\": \"\\\"application/x-www-form-urlencoded\\\",\",\n            \"enabled\": true\n          },\n          {\n            \"name\": \"authorization\",\n            \"value\": \"\\\"Basic dGVzdDp0ZXN0\\\"\",\n            \"enabled\": true\n          }\n        ]\n      },\n      \"response\": {\n        \"status\": \"200\",\n        \"statusText\": \"OK\",\n        \"body\": {\n          \"type\": \"json\",\n          \"content\": \"{\\n  \\\"success\\\": true,\\n  \\\"message\\\": \\\"Form submitted successfully\\\",\\n  \\\"session_id\\\": \\\"sess-abc123def456\\\",\\n  \\\"user\\\": {\\n    \\\"username\\\": \\\"testuser\\\",\\n    \\\"authenticated\\\": true\\n  }\\n}\"\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/json/jsonToBru-bodytypes.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Body Types API\",\n    \"type\": \"http\"\n  },\n  \"http\": {\n    \"method\": \"get\",\n    \"url\": \"https://api.example.com/test\"\n  },\n  \"examples\": [\n    {\n      \"name\": \"JSON Body Example\",\n      \"description\": \"An example with JSON body\",\n      \"request\": {\n        \"url\": \"https://api.example.com/json\",\n        \"method\": \"post\",\n        \"body\": {\n          \"mode\": \"json\",\n          \"json\": \"{\\n  \\\"name\\\": \\\"Test\\\",\\n  \\\"value\\\": 123\\n}\"\n        }\n      }\n    },\n    {\n      \"name\": \"XML Body Example\",\n      \"description\": \"An example with XML body\",\n      \"request\": {\n        \"url\": \"https://api.example.com/xml\",\n        \"method\": \"post\",\n        \"body\": {\n          \"mode\": \"xml\",\n          \"xml\": \"<?xml version=\\\"1.0\\\"?>\\n<data>\\n  <name>Test</name>\\n  <value>123</value>\\n</data>\"\n        }\n      }\n    },\n    {\n      \"name\": \"Text Body Example\",\n      \"description\": \"An example with text body\",\n      \"request\": {\n        \"url\": \"https://api.example.com/text\",\n        \"method\": \"post\",\n        \"body\": {\n          \"mode\": \"text\",\n          \"text\": \"Plain text data\\nwith multiple lines\"\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/json/jsonToBru-multiple.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Multi-Example API\",\n    \"type\": \"http\"\n  },\n  \"http\": {\n    \"method\": \"get\",\n    \"url\": \"https://api.example.com/test\"\n  },\n  \"examples\": [\n    {\n      \"name\": \"Example 1\",\n      \"description\": \"First example\",\n      \"request\": {\n        \"url\": \"https://api.example.com/example1\",\n        \"method\": \"get\",\n        \"body\": {\n          \"mode\": \"none\"\n        }\n      }\n    },\n    {\n      \"name\": \"Example 2\",\n      \"description\": \"Second example with JSON body\",\n      \"request\": {\n        \"url\": \"https://api.example.com/example2\",\n        \"method\": \"get\",\n        \"body\": {\n          \"mode\": \"json\",\n          \"json\": \"{\\n  \\\"data\\\": \\\"test2\\\"\\n}\"\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/json/jsonToBru-response.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Response Example API\",\n    \"type\": \"http\"\n  },\n  \"http\": {\n    \"method\": \"get\",\n    \"url\": \"https://api.example.com/test\"\n  },\n  \"examples\": [\n    {\n      \"name\": \"Response Example\",\n      \"description\": \"An example with response data\",\n      \"request\": {\n        \"url\": \"https://api.example.com/users/123\",\n        \"method\": \"get\",\n        \"body\": {\n          \"mode\": \"none\"\n        }\n      },\n      \"response\": {\n        \"headers\": [\n          {\n            \"name\": \"content-type\",\n            \"value\": \"application/json\",\n            \"enabled\": true\n          }\n        ],\n        \"status\": \"200\",\n        \"statusText\": \"OK\",\n        \"body\": {\n          \"type\": \"json\",\n          \"content\": \"{\\n  \\\"id\\\": 123,\\n  \\\"name\\\": \\\"John Doe\\\"\\n}\"\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/json/jsonToBru-simple.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Test API\",\n    \"type\": \"http\"\n  },\n  \"http\": {\n    \"method\": \"get\",\n    \"url\": \"https://api.example.com/test\"\n  },\n  \"examples\": [\n    {\n      \"name\": \"Example Request\",\n      \"description\": \"A simple example request\",\n      \"request\": {\n        \"url\": \"https://api.example.com/example\",\n        \"method\": \"post\",\n        \"body\": {\n          \"mode\": \"json\",\n          \"json\": \"{\\n  \\\"data\\\": \\\"test\\\"\\n}\"\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/json/multiple-examples-variations.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Multiple Examples Variations\",\n    \"type\": \"http\",\n    \"seq\": \"1\"\n  },\n  \"http\": {\n    \"method\": \"get\",\n    \"url\": \"https://api.example.com/data\"\n  },\n  \"headers\": [\n    {\n      \"name\": \"accept\",\n      \"value\": \"application/json\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"x-client\",\n      \"value\": \"bruno\",\n      \"enabled\": true\n    }\n  ],\n  \"params\": [\n    {\n      \"name\": \"page\",\n      \"value\": \"1\",\n      \"enabled\": true,\n      \"type\": \"query\"\n    },\n    {\n      \"name\": \"limit\",\n      \"value\": \"10\",\n      \"enabled\": true,\n      \"type\": \"query\"\n    },\n    {\n      \"name\": \"sort\",\n      \"value\": \"desc\",\n      \"enabled\": true,\n      \"type\": \"query\"\n    }\n  ],\n  \"auth\": {\n    \"bearer\": {\n      \"token\": \"token-for-multiple-examples\"\n    }\n  },\n  \"body\": {\n    \"json\": \"{\\n  \\\"query\\\": \\\"search\\\",\\n  \\\"filters\\\": []\\n}\"\n  },\n  \"examples\": [\n    {\n      \"name\": \"Simple GET Request\",\n      \"description\": \"Basic GET request with minimal data\",\n      \"request\": {\n        \"url\": \"https://api.example.com/data?id=123\",\n        \"method\": \"get\",\n        \"body\": {\n          \"mode\": \"none\"\n        },\n        \"headers\": [\n          {\n            \"name\": \"accept\",\n            \"value\": \"\\\"application/json\\\"\",\n            \"enabled\": true\n          }\n        ]\n      },\n      \"response\": {\n        \"status\": \"200\",\n        \"statusText\": \"OK\",\n        \"body\": {\n          \"type\": \"json\",\n          \"content\": \"{\\n  \\\"id\\\": 123,\\n  \\\"name\\\": \\\"Item\\\",\\n  \\\"value\\\": 42\\n}\"\n        }\n      }\n    },\n    {\n      \"name\": \"Complex Search Query\",\n      \"description\": \"GET request with complex query parameters\",\n      \"request\": {\n        \"url\": \"https://api.example.com/data?page=1&limit=20&sort=name&order=asc&filter[status]=active&filter[type]=premium\",\n        \"method\": \"get\",\n        \"body\": {\n          \"mode\": \"none\"\n        },\n        \"headers\": [\n          {\n            \"name\": \"accept\",\n            \"value\": \"\\\"application/json\\\",\",\n            \"enabled\": true\n          },\n          {\n            \"name\": \"x-api-key\",\n            \"value\": \"\\\"search-key-789\\\"\",\n            \"enabled\": true\n          }\n        ]\n      },\n      \"response\": {\n        \"status\": \"200\",\n        \"statusText\": \"OK\",\n        \"body\": {\n          \"type\": \"json\",\n          \"content\": \"{\\n  \\\"data\\\": [\\n    {\\n      \\\"id\\\": 1,\\n      \\\"name\\\": \\\"Premium Item 1\\\",\\n      \\\"status\\\": \\\"active\\\",\\n      \\\"type\\\": \\\"premium\\\"\\n    },\\n    {\\n      \\\"id\\\": 2,\\n      \\\"name\\\": \\\"Premium Item 2\\\",  \\n      \\\"status\\\": \\\"active\\\",\\n      \\\"type\\\": \\\"premium\\\"\\n    }\\n  ],\\n  \\\"pagination\\\": {\\n    \\\"page\\\": 1,\\n    \\\"limit\\\": 20,\\n    \\\"total\\\": 2\\n  }\\n}\"\n        }\n      }\n    },\n    {\n      \"name\": \"Text Response Example\",\n      \"description\": \"Request returning plain text response\",\n      \"request\": {\n        \"url\": \"https://api.example.com/data/text\",\n        \"method\": \"get\",\n        \"body\": {\n          \"mode\": \"none\"\n        },\n        \"headers\": [\n          {\n            \"name\": \"accept\",\n            \"value\": \"\\\"text/plain\\\"\",\n            \"enabled\": true\n          }\n        ]\n      },\n      \"response\": {\n        \"status\": \"200\",\n        \"statusText\": \"OK\",\n        \"body\": {\n          \"type\": \"text\",\n          \"content\": \"This is a plain text response\\nMultiple lines of content\\nNo JSON formatting needed\"\n        }\n      }\n    },\n    {\n      \"name\": \"XML Response with Nested Structure\",\n      \"description\": \"Example with complex XML response\",\n      \"request\": {\n        \"url\": \"https://api.example.com/data/xml\",\n        \"method\": \"get\",\n        \"body\": {\n          \"mode\": \"none\"\n        },\n        \"headers\": [\n          {\n            \"name\": \"accept\",\n            \"value\": \"\\\"application/xml\\\"\",\n            \"enabled\": true\n          }\n        ]\n      },\n      \"response\": {\n        \"status\": \"200\",\n        \"statusText\": \"OK\",\n        \"body\": {\n          \"type\": \"xml\",\n          \"content\": \"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\\n<response>\\n  <metadata>\\n    <version>1.0</version>\\n    <timestamp>2024-01-15T15:00:00Z</timestamp>\\n  </metadata>\\n  <data>\\n    <item id=\\\"1\\\">\\n      <name>Item One</name>\\n      <value>100</value>\\n      <nested>\\n        <attribute key=\\\"type\\\">primary</attribute>\\n      </nested>\\n    </item>\\n    <item id=\\\"2\\\">\\n      <name>Item Two</name>\\n      <value>200</value>\\n      <nested>\\n        <attribute key=\\\"type\\\">secondary</attribute>\\n      </nested>\\n    </item>\\n  </data>\\n</response>\"\n        }\n      }\n    },\n    {\n      \"name\": \"GraphQL Query Example\",\n      \"description\": \"Example with GraphQL query body\",\n      \"request\": {\n        \"url\": \"https://api.example.com/graphql\",\n        \"method\": \"post\",\n        \"body\": {\n          \"mode\": \"graphql\",\n          \"graphql\": {\n            \"query\": \"query {\\n  user(id: \\\"123\\\") {\\n    id\\n    name\\n    email\\n    posts {\\n      title\\n      content\\n    }\\n  }\\n}\"\n          }\n        },\n        \"headers\": [\n          {\n            \"name\": \"content-type\",\n            \"value\": \"\\\"application/json\\\",\",\n            \"enabled\": true\n          },\n          {\n            \"name\": \"authorization\",\n            \"value\": \"\\\"Bearer graphql-token-xyz\\\"\",\n            \"enabled\": true\n          }\n        ]\n      },\n      \"response\": {\n        \"status\": \"200\",\n        \"statusText\": \"OK\",\n        \"body\": {\n          \"type\": \"json\",\n          \"content\": \"{\\n  \\\"data\\\": {\\n    \\\"user\\\": {\\n      \\\"id\\\": \\\"123\\\",\\n      \\\"name\\\": \\\"John Doe\\\",\\n      \\\"email\\\": \\\"john@example.com\\\",\\n      \\\"posts\\\": [\\n        {\\n          \\\"title\\\": \\\"First Post\\\",\\n          \\\"content\\\": \\\"This is my first blog post\\\"\\n        }\\n      ]\\n    }\\n  }\\n}\"\n        }\n      }\n    },\n    {\n      \"name\": \"SPARQL Query Example\",\n      \"description\": \"Example with SPARQL query\",\n      \"request\": {\n        \"url\": \"https://sparql.example.com/query\",\n        \"method\": \"post\",\n        \"body\": {\n          \"mode\": \"sparql\",\n          \"sparql\": \"SELECT ?name ?email WHERE {\\n  ?person <http://example.org/name> ?name .\\n  ?person <http://example.org/email> ?email .\\n  ?person <http://example.org/age> ?age .\\n  FILTER(?age > 18)\\n}\"\n        },\n        \"headers\": [\n          {\n            \"name\": \"content-type\",\n            \"value\": \"\\\"application/sparql-query\\\"\",\n            \"enabled\": true\n          }\n        ]\n      },\n      \"response\": {\n        \"status\": \"200\",\n        \"statusText\": \"OK\",\n        \"body\": {\n          \"type\": \"json\",\n          \"content\": \"{\\n  \\\"results\\\": {\\n    \\\"bindings\\\": [\\n      {\\n        \\\"name\\\": {\\n          \\\"value\\\": \\\"John Doe\\\"\\n        },\\n        \\\"email\\\": {\\n          \\\"value\\\": \\\"john@example.com\\\"\\n        }\\n      }\\n    ]\\n  }\\n}\"\n        }\n      }\n    }\n  ]\n}"
  },
  {
    "path": "packages/bruno-lang/v2/tests/examples/fixtures/json/oauth2-examples.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"OAuth2 Examples API\",\n    \"type\": \"http\",\n    \"seq\": \"1\"\n  },\n  \"http\": {\n    \"method\": \"post\",\n    \"url\": \"https://api.example.com/oauth/protected\",\n    \"body\": \"json\",\n    \"auth\": \"oauth2\"\n  },\n  \"headers\": [\n    {\n      \"name\": \"content-type\",\n      \"value\": \"application/json\",\n      \"enabled\": true\n    }\n  ],\n  \"auth\": {\n    \"oauth2\": {\n      \"grantType\": \"authorization_code\",\n      \"callbackUrl\": \"https://api.example.com/oauth/callback\",\n      \"authorizationUrl\": \"https://oauth.example.com/authorize\",\n      \"accessTokenUrl\": \"https://oauth.example.com/token\",\n      \"refreshTokenUrl\": \"https://oauth.example.com/token\",\n      \"clientId\": \"my-client-id\",\n      \"clientSecret\": \"my-client-secret\",\n      \"scope\": \"read write\",\n      \"state\": \"\",\n      \"pkce\": true,\n      \"credentialsPlacement\": \"header\",\n      \"credentialsId\": \"authorization\",\n      \"tokenSource\": \"access_token\",\n      \"tokenPlacement\": \"header\",\n      \"tokenHeaderPrefix\": \"Bearer\",\n      \"tokenQueryKey\": \"access_token\",\n      \"autoFetchToken\": true,\n      \"autoRefreshToken\": true\n    }\n  },\n  \"body\": {\n    \"json\": \"{\\n  \\\"action\\\": \\\"test\\\",\\n  \\\"data\\\": {\\n    \\\"message\\\": \\\"Protected resource access\\\"\\n  }\\n}\"\n  },\n  \"vars\": {\n    \"req\": [\n      {\n        \"name\": \"oauth_state\",\n        \"value\": \"{{$uuid}}\",\n        \"enabled\": true,\n        \"local\": false\n      },\n      {\n        \"name\": \"client_scopes\",\n        \"value\": \"read,write,admin\",\n        \"enabled\": true,\n        \"local\": false\n      }\n    ]\n  },\n  \"script\": {\n    \"req\": \"const state = crypto.randomBytes(16).toString('hex');\\nbru.setVar('oauth_state', state);\\nbru.setVar('timestamp', Date.now());\"\n  },\n  \"tests\": \"test(\\\"Response should be 200\\\", function() {\\n  expect(res.getStatus()).to.eql(200);\\n});\\n\\ntest(\\\"Should have user data in response\\\", function() {\\n  const body = res.getBody();\\n  expect(body.access_token).to.be.ok;\\n});\",\n  \"examples\": [\n    {\n      \"name\": \"OAuth2 Protected Resource\",\n      \"description\": \"Example accessing resource protected with OAuth2 authorization code flow\",\n      \"request\": {\n        \"url\": \"https://api.example.com/oauth/protected\",\n        \"method\": \"post\",\n        \"body\": {\n          \"mode\": \"json\",\n          \"json\": \"{\\n  \\\"action\\\": \\\"fetch\\\",\\n  \\\"resource\\\": \\\"user_profile\\\"\\n}\"\n        },\n        \"headers\": [\n          {\n            \"name\": \"authorization\",\n            \"value\": \"\\\"Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9\\\",\",\n            \"enabled\": true\n          },\n          {\n            \"name\": \"content-type\",\n            \"value\": \"\\\"application/json\\\"\",\n            \"enabled\": true\n          }\n        ]\n      },\n      \"response\": {\n        \"status\": \"200\",\n        \"statusText\": \"OK\",\n        \"body\": {\n          \"type\": \"json\",\n          \"content\": \"{\\n  \\\"user\\\": {\\n    \\\"id\\\": \\\"123\\\",\\n    \\\"name\\\": \\\"John Doe\\\",\\n    \\\"email\\\": \\\"john@example.com\\\",\\n    \\\"scopes\\\": [\\\"read\\\", \\\"write\\\"]\\n  },\\n  \\\"token\\\": {\\n    \\\"access_token\\\": \\\"access_token_abc123\\\",\\n    \\\"expires_in\\\": 3600,\\n    \\\"token_type\\\": \\\"Bearer\\\"\\n  }\\n}\"\n        }\n      }\n    },\n    {\n      \"name\": \"OAuth2 Token Refresh\",\n      \"description\": \"Example demonstrating OAuth2 token refresh flow\",\n      \"request\": {\n        \"url\": \"https://api.example.com/oauth/token\",\n        \"method\": \"post\",\n        \"body\": {\n          \"mode\": \"json\",\n          \"json\": \"{\\n  \\\"grant_type\\\": \\\"refresh_token\\\",\\n  \\\"refresh_token\\\": \\\"refresh_token_xyz789\\\",\\n  \\\"client_id\\\": \\\"my-client-id\\\",\\n  \\\"client_secret\\\": \\\"my-client-secret\\\"\\n}\"\n        },\n        \"headers\": [\n          {\n            \"name\": \"content-type\",\n            \"value\": \"\\\"application/json\\\",\",\n            \"enabled\": true\n          },\n          {\n            \"name\": \"accept\",\n            \"value\": \"\\\"application/json\\\"\",\n            \"enabled\": true\n          }\n        ]\n      },\n      \"response\": {\n        \"status\": \"200\",\n        \"statusText\": \"OK\",\n        \"body\": {\n          \"type\": \"json\",\n          \"content\": \"{\\n  \\\"access_token\\\": \\\"new_access_token_def456\\\",\\n  \\\"refresh_token\\\": \\\"new_refresh_token_abc789\\\",\\n  \\\"expires_in\\\": 3600,\\n  \\\"token_type\\\": \\\"Bearer\\\",\\n  \\\"scope\\\": \\\"read write\\\"\\n}\"\n        }\n      }\n    },\n    {\n      \"name\": \"OAuth2 Client Credentials\",\n      \"description\": \"Example using OAuth2 client credentials grant type\",\n      \"request\": {\n        \"url\": \"https://api.example.com/oauth/client-credentials\",\n        \"method\": \"post\",\n        \"body\": {\n          \"mode\": \"json\",\n          \"json\": \"{\\n  \\\"grant_type\\\": \\\"client_credentials\\\",\\n  \\\"client_id\\\": \\\"service-account\\\",\\n  \\\"client_secret\\\": \\\"service-secret-key\\\",\\n  \\\"scope\\\": \\\"admin\\\"\\n}\"\n        },\n        \"headers\": [\n          {\n            \"name\": \"content-type\",\n            \"value\": \"\\\"application/json\\\"\",\n            \"enabled\": true\n          }\n        ]\n      },\n      \"response\": {\n        \"status\": \"200\",\n        \"statusText\": \"OK\",\n        \"body\": {\n          \"type\": \"json\",\n          \"content\": \"{\\n  \\\"access_token\\\": \\\"service_access_token_123\\\",\\n  \\\"expires_in\\\": 7200,\\n  \\\"token_type\\\": \\\"Bearer\\\",\\n  \\\"scope\\\": \\\"admin\\\"\\n}\"\n        }\n      }\n    },\n    {\n      \"name\": \"OAuth2 Password Grant\",\n      \"description\": \"Example using OAuth2 password grant (username/password)\",\n      \"request\": {\n        \"url\": \"https://api.example.com/oauth/password\",\n        \"method\": \"post\",\n        \"body\": {\n          \"mode\": \"json\",\n          \"json\": \"{\\n  \\\"grant_type\\\": \\\"password\\\",\\n  \\\"username\\\": \\\"user@example.com\\\",\\n  \\\"password\\\": \\\"SecurePass123!\\\",\\n  \\\"client_id\\\": \\\"mobile-app\\\",\\n  \\\"client_secret\\\": \\\"mobile-app-secret\\\"\\n}\"\n        },\n        \"headers\": [\n          {\n            \"name\": \"content-type\",\n            \"value\": \"\\\"application/json\\\"\",\n            \"enabled\": true\n          }\n        ]\n      },\n      \"response\": {\n        \"status\": \"200\",\n        \"statusText\": \"OK\",\n        \"body\": {\n          \"type\": \"json\",\n          \"content\": \"{\\n  \\\"access_token\\\": \\\"user_access_token_456\\\",\\n  \\\"refresh_token\\\": \\\"user_refresh_token_789\\\",\\n  \\\"expires_in\\\": 3600,\\n  \\\"token_type\\\": \\\"Bearer\\\"\\n}\"\n        }\n      }\n    }\n  ],\n  \"docs\": \"This collection demonstrates OAuth2 authentication flows.\\nSupports authorization code, client credentials, and password grant types.\\nExamples show token refresh and protected resource access.\"\n}"
  },
  {
    "path": "packages/bruno-lang/v2/tests/fixtures/collection.bru",
    "content": "meta {\n  type: collection\n}\n\nheaders {\n  content-type: application/json\n  Authorization: Bearer 123\n  ~transaction-id: {{transactionId}}\n}\n\nauth {\n  mode: none\n}\n\nauth:basic {\n  username: john\n  password: secret\n}\n\nauth:wsse {\n  username: john\n  password: secret\n}\n\nauth:bearer {\n  token: 123\n}\n\nauth:digest {\n  username: john\n  password: secret\n}\n\nvars:pre-request {\n  departingDate: 2020-01-01\n  ~returningDate: 2020-01-02\n}\n\nvars:post-response {\n  ~transactionId: $res.body.transactionId\n}\n\nscript:pre-request {\n  console.log(\"In Collection pre Request Script\");\n}\n\nscript:post-response {\n  console.log(\"In Collection post Request Script\");\n}\n\ndocs {\n  This request needs auth token to be set in the headers.\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/fixtures/collection.json",
    "content": "{\n  \"meta\": {\n    \"type\": \"collection\"\n  },\n  \"headers\": [\n    {\n      \"name\": \"content-type\",\n      \"value\": \"application/json\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"Authorization\",\n      \"value\": \"Bearer 123\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"transaction-id\",\n      \"value\": \"{{transactionId}}\",\n      \"enabled\": false\n    }\n  ],\n  \"auth\": {\n    \"mode\": \"none\",\n    \"basic\": {\n      \"username\": \"john\",\n      \"password\": \"secret\"\n    },\n    \"bearer\": {\n      \"token\": \"123\"\n    },\n    \"digest\": {\n      \"username\": \"john\",\n      \"password\": \"secret\"\n    },\n    \"wsse\": {\n      \"username\": \"john\",\n      \"password\": \"secret\"\n    }\n  },\n  \"vars\": {\n    \"req\": [\n      {\n        \"name\": \"departingDate\",\n        \"value\": \"2020-01-01\",\n        \"enabled\": true,\n        \"local\": false\n      },\n      {\n        \"name\": \"returningDate\",\n        \"value\": \"2020-01-02\",\n        \"enabled\": false,\n        \"local\": false\n      }\n    ],\n    \"res\": [\n      {\n        \"name\": \"transactionId\",\n        \"value\": \"$res.body.transactionId\",\n        \"enabled\": false,\n        \"local\": false\n      }\n    ]\n  },\n  \"script\": {\n    \"req\": \"console.log(\\\"In Collection pre Request Script\\\");\",\n    \"res\": \"console.log(\\\"In Collection post Request Script\\\");\"\n  },\n  \"docs\": \"This request needs auth token to be set in the headers.\"\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/fixtures/request.bru",
    "content": "meta {\n  name: Send Bulk SMS\n  type: http\n  seq: 1\n  tags: [\n    foo\n    bar\n  ]\n}\n\nget {\n  url: https://api.textlocal.in/send/:id\n  body: json\n  auth: bearer\n}\n\nparams:query {\n  apiKey: secret\n  numbers: 998877665\n  \"key with spaces\": is allowed\n  \"colon:parameter\": is allowed\n  \"nested escaped \\\"quote\\\"\": is allowed\n  \"{braces}\": is allowed\n  ~\"disabled:colon:parameter\": is allowed\n  ~message: hello\n}\n\nparams:path {\n  id: 123\n}\n\nheaders {\n  content-type: application/json\n  Authorization: Bearer 123\n  \"key with spaces\": is allowed\n  \"colon:header\": is allowed\n  \"{braces}\": is allowed\n  \"nested escaped \\\"quote\\\"\": is allowed\n  ~\"disabled:colon:header\": is allowed\n  ~transaction-id: {{transactionId}}\n}\n\nauth:awsv4 {\n  accessKeyId: A12345678\n  secretAccessKey: thisisasecret\n  sessionToken: thisisafakesessiontoken\n  service: execute-api\n  region: us-east-1\n  profileName: test_profile\n}\n\nauth:basic {\n  username: john\n  password: secret\n}\n\nauth:wsse {\n  username: john\n  password: secret\n}\n\nauth:bearer {\n  token: 123\n}\n\nauth:digest {\n  username: john\n  password: secret\n}\n\nauth:oauth2 {\n  grant_type: authorization_code\n  callback_url: http://localhost:8080/api/auth/oauth2/authorization_code/callback\n  authorization_url: http://localhost:8080/api/auth/oauth2/authorization_code/authorize\n  access_token_url: http://localhost:8080/api/auth/oauth2/authorization_code/token\n  refresh_token_url: \n  client_id: client_id_1\n  client_secret: client_secret_1\n  scope: read write\n  state: 807061d5f0be\n  pkce: false\n  credentials_placement: body\n  credentials_id: credentials\n  token_source: access_token\n  token_placement: header\n  token_header_prefix: Bearer\n  auto_fetch_token: true\n  auto_refresh_token: true\n}\n\nbody:json {\n  {\n    \"hello\": \"world\"\n  }\n}\n\nbody:text {\n  This is a text body\n}\n\nbody:xml {\n  <xml>\n    <name>John</name>\n    <age>30</age>\n  </xml>\n}\n\nbody:sparql {\n  SELECT * WHERE {\n    ?subject ?predicate ?object .\n  }\n  LIMIT 10\n}\n\nbody:form-urlencoded {\n  apikey: secret\n  numbers: +91998877665\n  \"key with spaces\": is allowed\n  \"colon:parameter\": is allowed\n  \"nested escaped \\\"quote\\\"\": is allowed\n  \"{braces}\": is allowed\n  ~message: hello\n  ~\"disabled colon:parameter\": is allowed\n}\n\nbody:multipart-form {\n  apikey: secret\n  numbers: +91998877665\n  \"key with spaces\": is allowed\n  \"colon:part\": is allowed\n  \"nested escaped \\\"quote\\\"\": is allowed\n  \"{braces}\": is allowed\n  ~message: hello\n  ~\"disabled colon:part\": is allowed\n}\n\nbody:file {\n  file: @file(path/to/file.json) @contentType(application/json)\n  file: @file(path/to/file.json) @contentType(application/json)\n  ~file: @file(path/to/file2.json) @contentType(application/json)\n}\n\nbody:graphql {\n  {\n    launchesPast {\n      launch_site {\n        site_name\n      }\n      launch_success\n    }\n  }\n}\n\nbody:graphql:vars {\n  {\n    \"limit\": 5\n  }\n}\n\nvars:pre-request {\n  departingDate: 2020-01-01\n  ~returningDate: 2020-01-02\n}\n\nvars:post-response {\n  token: $res.body.token\n  @orderNumber: $res.body.orderNumber\n  ~petId: $res.body.id\n  ~@transactionId: $res.body.transactionId\n}\n\nassert {\n  $res.status: 200\n  ~$res.body.message: success\n}\n\nscript:pre-request {\n  const foo = 'bar';\n}\n\ntests {\n  function onResponse(request, response) {\n    expect(response.status).to.equal(200);\n  }\n}\n\ndocs {\n  This request needs auth token to be set in the headers.\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/fixtures/request.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Send Bulk SMS\",\n    \"type\": \"http\",\n    \"seq\": \"1\",\n    \"tags\": [\n      \"foo\",\n      \"bar\"\n    ]\n  },\n  \"http\": {\n    \"method\": \"get\",\n    \"url\": \"https://api.textlocal.in/send/:id\",\n    \"body\": \"json\",\n    \"auth\": \"bearer\"\n  },\n  \"params\": [\n    {\n      \"name\": \"apiKey\",\n      \"value\": \"secret\",\n      \"type\": \"query\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"numbers\",\n      \"value\": \"998877665\",\n      \"type\": \"query\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"key with spaces\",\n      \"value\": \"is allowed\",\n      \"type\": \"query\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"colon:parameter\",\n      \"value\": \"is allowed\",\n      \"type\": \"query\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"nested escaped \\\"quote\\\"\",\n      \"value\": \"is allowed\",\n      \"type\": \"query\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"{braces}\",\n      \"value\": \"is allowed\",\n      \"type\": \"query\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"disabled:colon:parameter\",\n      \"value\": \"is allowed\",\n      \"type\": \"query\",\n      \"enabled\": false\n    },\n    {\n      \"name\": \"message\",\n      \"value\": \"hello\",\n      \"type\": \"query\",\n      \"enabled\": false\n    },\n    {\n      \"name\": \"id\",\n      \"value\": \"123\",\n      \"type\": \"path\",\n      \"enabled\": true\n    }\n  ],\n  \"headers\": [\n    {\n      \"name\": \"content-type\",\n      \"value\": \"application/json\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"Authorization\",\n      \"value\": \"Bearer 123\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"key with spaces\",\n      \"value\": \"is allowed\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"colon:header\",\n      \"value\": \"is allowed\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"{braces}\",\n      \"value\": \"is allowed\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"nested escaped \\\"quote\\\"\",\n      \"value\": \"is allowed\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"disabled:colon:header\",\n      \"value\": \"is allowed\",\n      \"enabled\": false\n    },\n    {\n      \"name\": \"transaction-id\",\n      \"value\": \"{{transactionId}}\",\n      \"enabled\": false\n    }\n  ],\n  \"auth\": {\n    \"awsv4\": {\n      \"accessKeyId\": \"A12345678\",\n      \"secretAccessKey\": \"thisisasecret\",\n      \"sessionToken\": \"thisisafakesessiontoken\",\n      \"service\": \"execute-api\",\n      \"region\": \"us-east-1\",\n      \"profileName\": \"test_profile\"\n    },\n    \"basic\": {\n      \"username\": \"john\",\n      \"password\": \"secret\"\n    },\n    \"bearer\": {\n      \"token\": \"123\"\n    },\n    \"digest\": {\n      \"username\": \"john\",\n      \"password\": \"secret\"\n    },\n    \"oauth2\": {\n      \"accessTokenUrl\": \"http://localhost:8080/api/auth/oauth2/authorization_code/token\",\n      \"authorizationUrl\": \"http://localhost:8080/api/auth/oauth2/authorization_code/authorize\",\n      \"autoFetchToken\": true,\n      \"autoRefreshToken\": true,\n      \"callbackUrl\": \"http://localhost:8080/api/auth/oauth2/authorization_code/callback\",\n      \"clientId\": \"client_id_1\",\n      \"clientSecret\": \"client_secret_1\",\n      \"credentialsId\": \"credentials\",\n      \"tokenSource\": \"access_token\",\n      \"credentialsPlacement\": \"body\",\n      \"grantType\": \"authorization_code\",\n      \"pkce\": false,\n      \"refreshTokenUrl\": \"\",\n      \"scope\": \"read write\",\n      \"state\": \"807061d5f0be\",\n      \"tokenHeaderPrefix\": \"Bearer\",\n      \"tokenPlacement\": \"header\",\n      \"tokenQueryKey\": \"access_token\"\n    },\n    \"wsse\": {\n      \"username\": \"john\",\n      \"password\": \"secret\"\n    }\n  },\n  \"body\": {\n    \"json\": \"{\\n  \\\"hello\\\": \\\"world\\\"\\n}\",\n    \"text\": \"This is a text body\",\n    \"xml\": \"<xml>\\n  <name>John</name>\\n  <age>30</age>\\n</xml>\",\n    \"sparql\": \"SELECT * WHERE {\\n  ?subject ?predicate ?object .\\n}\\nLIMIT 10\",\n    \"graphql\": {\n      \"query\": \"{\\n  launchesPast {\\n    launch_site {\\n      site_name\\n    }\\n    launch_success\\n  }\\n}\",\n      \"variables\": \"{\\n  \\\"limit\\\": 5\\n}\"\n    },\n    \"formUrlEncoded\": [\n      {\n        \"name\": \"apikey\",\n        \"value\": \"secret\",\n        \"enabled\": true\n      },\n      {\n        \"name\": \"numbers\",\n        \"value\": \"+91998877665\",\n        \"enabled\": true\n      },\n      {\n        \"name\": \"key with spaces\",\n        \"value\": \"is allowed\",\n        \"enabled\": true\n      },\n      {\n        \"name\": \"colon:parameter\",\n        \"value\": \"is allowed\",\n        \"enabled\": true\n      },\n      {\n        \"name\": \"nested escaped \\\"quote\\\"\",\n        \"value\": \"is allowed\",\n        \"enabled\": true\n      },\n      {\n        \"name\": \"{braces}\",\n        \"value\": \"is allowed\",\n        \"enabled\": true\n      },\n      {\n        \"name\": \"message\",\n        \"value\": \"hello\",\n        \"enabled\": false\n      },\n      {\n        \"name\": \"disabled colon:parameter\",\n        \"value\": \"is allowed\",\n        \"enabled\": false\n      }\n    ],\n    \"multipartForm\": [\n      {\n        \"contentType\": \"\",\n        \"name\": \"apikey\",\n        \"value\": \"secret\",\n        \"enabled\": true,\n        \"type\": \"text\"\n      },\n      {\n        \"contentType\": \"\",\n        \"name\": \"numbers\",\n        \"value\": \"+91998877665\",\n        \"enabled\": true,\n        \"type\": \"text\"\n      },\n      {\n        \"contentType\": \"\",\n        \"name\": \"key with spaces\",\n        \"value\": \"is allowed\",\n        \"enabled\": true,\n        \"type\": \"text\"\n      },\n      {\n        \"contentType\": \"\",\n        \"name\": \"colon:part\",\n        \"value\": \"is allowed\",\n        \"enabled\": true,\n        \"type\": \"text\"\n      },\n      {\n        \"contentType\": \"\",\n        \"name\": \"nested escaped \\\"quote\\\"\",\n        \"value\": \"is allowed\",\n        \"enabled\": true,\n        \"type\": \"text\"\n      },\n      {\n        \"contentType\": \"\",\n        \"name\": \"{braces}\",\n        \"value\": \"is allowed\",\n        \"enabled\": true,\n        \"type\": \"text\"\n      },\n      {\n        \"contentType\": \"\",\n        \"name\": \"message\",\n        \"value\": \"hello\",\n        \"enabled\": false,\n        \"type\": \"text\"\n      },\n      {\n        \"contentType\": \"\",\n        \"name\": \"disabled colon:part\",\n        \"value\": \"is allowed\",\n        \"enabled\": false,\n        \"type\": \"text\"\n      }\n    ],\n    \"file\": [\n      {\n        \"filePath\": \"path/to/file.json\",\n        \"contentType\": \"application/json\",\n        \"selected\": true\n      },\n      {\n        \"filePath\": \"path/to/file.json\",\n        \"contentType\": \"application/json\",\n        \"selected\": true\n      },\n      {\n        \"filePath\": \"path/to/file2.json\",\n        \"contentType\": \"application/json\",\n        \"selected\": false\n      }\n    ]\n  },\n  \"vars\": {\n    \"req\": [\n      {\n        \"name\": \"departingDate\",\n        \"value\": \"2020-01-01\",\n        \"local\": false,\n        \"enabled\": true\n      },\n      {\n        \"name\": \"returningDate\",\n        \"value\": \"2020-01-02\",\n        \"local\": false,\n        \"enabled\": false\n      }\n    ],\n    \"res\": [\n      {\n        \"name\": \"token\",\n        \"value\": \"$res.body.token\",\n        \"local\": false,\n        \"enabled\": true\n      },\n      {\n        \"name\": \"orderNumber\",\n        \"value\": \"$res.body.orderNumber\",\n        \"local\": true,\n        \"enabled\": true\n      },\n      {\n        \"name\": \"petId\",\n        \"value\": \"$res.body.id\",\n        \"local\": false,\n        \"enabled\": false\n      },\n      {\n        \"name\": \"transactionId\",\n        \"value\": \"$res.body.transactionId\",\n        \"local\": true,\n        \"enabled\": false\n      }\n    ]\n  },\n  \"assertions\": [\n    {\n      \"name\": \"$res.status\",\n      \"value\": \"200\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"$res.body.message\",\n      \"value\": \"success\",\n      \"enabled\": false\n    }\n  ],\n  \"script\": {\n    \"req\": \"const foo = 'bar';\"\n  },\n  \"tests\": \"function onResponse(request, response) {\\n  expect(response.status).to.equal(200);\\n}\",\n  \"docs\": \"This request needs auth token to be set in the headers.\"\n}"
  },
  {
    "path": "packages/bruno-lang/v2/tests/getKeyString.spec.js",
    "content": "const { getKeyString } = require('../src/utils');\n\ndescribe('getKeyString', () => {\n  describe('should not quote keys without special characters', () => {\n    it('should return simple alphanumeric keys as-is', () => {\n      expect(getKeyString('hello')).toBe('hello');\n      expect(getKeyString('world123')).toBe('world123');\n      expect(getKeyString('API')).toBe('API');\n    });\n\n    it('should return keys with hyphens as-is', () => {\n      expect(getKeyString('api-key')).toBe('api-key');\n      expect(getKeyString('content-type')).toBe('content-type');\n    });\n\n    it('should return keys with underscores as-is', () => {\n      expect(getKeyString('api_key')).toBe('api_key');\n      expect(getKeyString('user_name')).toBe('user_name');\n    });\n  });\n\n  describe('should quote keys with special characters', () => {\n    it('should quote keys with colons', () => {\n      expect(getKeyString('key:value')).toBe('\"key:value\"');\n      expect(getKeyString('disabled:colon:header')).toBe('\"disabled:colon:header\"');\n      expect(getKeyString(':startsWithColon')).toBe('\":startsWithColon\"');\n      expect(getKeyString('endsWithColon:')).toBe('\"endsWithColon:\"');\n    });\n\n    it('should quote keys with spaces', () => {\n      expect(getKeyString('key with spaces')).toBe('\"key with spaces\"');\n      expect(getKeyString(' leadingSpace')).toBe('\" leadingSpace\"');\n      expect(getKeyString('trailingSpace ')).toBe('\"trailingSpace \"');\n      expect(getKeyString('multiple   spaces')).toBe('\"multiple   spaces\"');\n    });\n\n    it('should quote keys with curly braces', () => {\n      expect(getKeyString('{braces}')).toBe('\"{braces}\"');\n      expect(getKeyString('{only-open')).toBe('\"{only-open\"');\n      expect(getKeyString('only-close}')).toBe('\"only-close}\"');\n      expect(getKeyString('nested{brace}here')).toBe('\"nested{brace}here\"');\n    });\n\n    it('should quote keys with double quotes and escape them', () => {\n      expect(getKeyString('nested \"quote\"')).toBe('\"nested \\\\\"quote\\\\\"\"');\n      expect(getKeyString('\"quoted\"')).toBe('\"\\\\\"quoted\\\\\"\"');\n      expect(getKeyString('multiple \"quotes\" here \"too\"')).toBe('\"multiple \\\\\"quotes\\\\\" here \\\\\"too\\\\\"\"');\n    });\n\n    it('should quote keys with multiple special characters', () => {\n      expect(getKeyString('key: value')).toBe('\"key: value\"');\n      expect(getKeyString('{key}: \"value\"')).toBe('\"{key}: \\\\\"value\\\\\"\"');\n      expect(getKeyString('complex:key with {braces}')).toBe('\"complex:key with {braces}\"');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/index.spec.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst bruToJson = require('../src/bruToJson');\nconst jsonToBru = require('../src/jsonToBru');\n\ndescribe('bruToJson', () => {\n  it('should parse the bru file', () => {\n    const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'request.bru'), 'utf8');\n    const expected = require('./fixtures/request.json');\n    const output = bruToJson(input);\n\n    expect(output).toEqual(expected);\n  });\n});\n\ndescribe('jsonToBru', () => {\n  it('should parse the json file', () => {\n    const input = require('./fixtures/request.json');\n    const expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'request.bru'), 'utf8');\n    const output = jsonToBru(input);\n\n    expect(output).toEqual(expected);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/jsonToBru.spec.js",
    "content": "const stringify = require('../src/jsonToBru');\n\ndescribe('jsonToBru stringify', () => {\n  describe('body:ws', () => {\n    it('stringifies a valid bruno request | smoke', () => {\n      const input = {\n        ws: {\n          url: 'ws://localhost:3000',\n          body: 'ws'\n        },\n        body: {\n          mode: 'ws',\n          ws: [\n            {\n              content: '{\"foo\":\"bar\"}',\n              name: 'message 1',\n              type: 'json'\n            }\n          ]\n        },\n        settings: {\n          keepAliveInterval: 30,\n          timeout: 250\n        }\n      };\n\n      const output = stringify(input);\n\n      // generic structure snapshot\n      expect(output).toMatchInlineSnapshot(`\n        \"ws {\n          url: ws://localhost:3000\n          body: ws\n        }\n\n        body:ws {\n          name: message 1\n          type: json\n          content: '''\n            {\"foo\":\"bar\"}\n          '''\n        }\n\n        settings {\n          keepAliveInterval: 30\n          timeout: 250\n        }\n        \"\n      `);\n\n      // Hard check if the input settings were stored as is\n      expect(output).toMatch(new RegExp(`keepAliveInterval: ${input.settings.keepAliveInterval}`));\n      expect(output).toMatch(new RegExp(`timeout: ${input.settings.timeout}`));\n    });\n  });\n\n  describe('multi-line values', () => {\n    it('handles multi-line values in URL, headers, params, and vars', () => {\n      const input = {\n        meta: {\n          name: 'new-line',\n          type: 'http',\n          seq: 1\n        },\n        http: {\n          method: 'get',\n          url: 'https://httpbin.io/anything?foo=hello\\nworld',\n          body: 'none',\n          auth: 'oauth2'\n        },\n        params: [\n          {\n            name: 'foo',\n            value: 'hello\\nworld',\n            enabled: true,\n            type: 'query'\n          }\n        ],\n        headers: [\n          {\n            name: 'test header',\n            value: 't1\\nt2',\n            enabled: true\n          }\n        ],\n        vars: {\n          req: [\n            {\n              name: 'test-var',\n              value: 't1\\nt2',\n              enabled: true\n            }\n          ]\n        }\n      };\n\n      const output = stringify(input);\n\n      expect(output).toMatchInlineSnapshot(`\n        \"meta {\n          name: new-line\n          type: http\n          seq: 1\n        }\n\n        get {\n          url: '''\n            https://httpbin.io/anything?foo=hello\n            world\n        '''\n          body: none\n          auth: oauth2\n        }\n\n        params:query {\n          foo: '''\n            hello\n            world\n          '''\n        }\n\n        headers {\n          \"test header\": '''\n            t1\n            t2\n          '''\n        }\n\n        vars:pre-request {\n          test-var: '''\n            t1\n            t2\n          '''\n        }\n        \"\n      `);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/jsonToEnv.spec.js",
    "content": "const parser = require('../src/jsonToEnv');\n\ndescribe('jsonToEnv', () => {\n  it('should stringify empty vars', () => {\n    const input = {\n      variables: []\n    };\n\n    const output = parser(input);\n    const expected = `vars {\n}\n`;\n\n    expect(output).toEqual(expected);\n  });\n\n  it('should stringify single var line', () => {\n    const input = {\n      variables: [\n        {\n          name: 'url',\n          value: 'http://localhost:3000',\n          enabled: true\n        }\n      ]\n    };\n\n    const output = parser(input);\n    const expected = `vars {\n  url: http://localhost:3000\n}\n`;\n    expect(output).toEqual(expected);\n  });\n\n  it('should stringify multiple var lines', () => {\n    const input = {\n      variables: [\n        {\n          name: 'url',\n          value: 'http://localhost:3000',\n          enabled: true\n        },\n        {\n          name: 'port',\n          value: '3000',\n          enabled: false\n        }\n      ]\n    };\n\n    const expected = `vars {\n  url: http://localhost:3000\n  ~port: 3000\n}\n`;\n    const output = parser(input);\n    expect(output).toEqual(expected);\n  });\n\n  it('should stringify secret vars', () => {\n    const input = {\n      variables: [\n        {\n          name: 'url',\n          value: 'http://localhost:3000',\n          enabled: true\n        },\n        {\n          name: 'token',\n          value: 'abracadabra',\n          enabled: true,\n          secret: true\n        }\n      ]\n    };\n\n    const output = parser(input);\n    const expected = `vars {\n  url: http://localhost:3000\n}\nvars:secret [\n  token\n]\n`;\n    expect(output).toEqual(expected);\n  });\n\n  it('should stringify multiple secret vars', () => {\n    const input = {\n      variables: [\n        {\n          name: 'url',\n          value: 'http://localhost:3000',\n          enabled: true\n        },\n        {\n          name: 'access_token',\n          value: 'abracadabra',\n          enabled: true,\n          secret: true\n        },\n        {\n          name: 'access_secret',\n          value: 'abracadabra',\n          enabled: false,\n          secret: true\n        }\n      ]\n    };\n\n    const output = parser(input);\n    const expected = `vars {\n  url: http://localhost:3000\n}\nvars:secret [\n  access_token,\n  ~access_secret\n]\n`;\n    expect(output).toEqual(expected);\n  });\n\n  it('should stringify even if the only secret vars are present', () => {\n    const input = {\n      variables: [\n        {\n          name: 'token',\n          value: 'abracadabra',\n          enabled: true,\n          secret: true\n        }\n      ]\n    };\n\n    const output = parser(input);\n    const expected = `vars:secret [\n  token\n]\n`;\n    expect(output).toEqual(expected);\n  });\n\n  it('should stringify multiline variables', () => {\n    const input = {\n      variables: [\n        {\n          name: 'json_data',\n          value: '{\\n  \"name\": \"test\",\\n  \"value\": 123\\n}',\n          enabled: true\n        }\n      ]\n    };\n\n    const output = parser(input);\n    const expected = `vars {\n  json_data: '''\n    {\n      \"name\": \"test\",\n      \"value\": 123\n    }\n  '''\n}\n`;\n    expect(output).toEqual(expected);\n  });\n\n  it('should stringify multiline variables containing indentation', () => {\n    const input = {\n      variables: [\n        {\n          name: 'script',\n          value: 'function test() {\\n  console.log(\"hello\");\\n  return true;\\n}',\n          enabled: true\n        }\n      ]\n    };\n\n    const output = parser(input);\n    const expected = `vars {\n  script: '''\n    function test() {\n      console.log(\"hello\");\n      return true;\n    }\n  '''\n}\n`;\n    expect(output).toEqual(expected);\n  });\n\n  it('should stringify disabled multiline variable', () => {\n    const input = {\n      variables: [\n        {\n          name: 'disabled_multiline',\n          value: 'line 1\\nline 2\\nline 3',\n          enabled: false\n        }\n      ]\n    };\n\n    const output = parser(input);\n    const expected = `vars {\n  ~disabled_multiline: '''\n    line 1\n    line 2\n    line 3\n  '''\n}\n`;\n    expect(output).toEqual(expected);\n  });\n\n  it('should stringify multiple multiline variables', () => {\n    const input = {\n      variables: [\n        {\n          name: 'config',\n          value: 'debug=true\\nport=3000',\n          enabled: true\n        },\n        {\n          name: 'template',\n          value: '<html>\\n  <body>Hello World</body>\\n</html>',\n          enabled: true\n        }\n      ]\n    };\n\n    const output = parser(input);\n    const expected = `vars {\n  config: '''\n    debug=true\n    port=3000\n  '''\n  template: '''\n    <html>\n      <body>Hello World</body>\n    </html>\n  '''\n}\n`;\n    expect(output).toEqual(expected);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/list.spec.js",
    "content": "/**\n * This test file is used to test list parsing in various BruFile blocks.\n */\nconst parser = require('../src/bruToJson');\n\ndescribe('List Support in BruFile Blocks', () => {\n  describe('Basic List Functionality', () => {\n    describe('Valid List Syntax', () => {\n      it('should parse simple list with proper indentation', () => {\n        const input = `\nmeta {\n  tags: [\n    tag_1\n    tag_2\n  ]\n}\n`;\n        const output = parser(input);\n        const expected = {\n          meta: {\n            seq: 1,\n            tags: ['tag_1', 'tag_2'],\n            type: 'http'\n          }\n        };\n        expect(output).toEqual(expected);\n      });\n\n      it('should parse list with mixed properties', () => {\n        const input = `\nmeta {\n  name: request_name\n  tags: [\n    regression\n    smoke_test\n  ]\n  type: http\n}\n`;\n        const output = parser(input);\n        const expected = {\n          meta: {\n            seq: 1,\n            name: 'request_name',\n            tags: ['regression', 'smoke_test'],\n            type: 'http'\n          }\n        };\n        expect(output).toEqual(expected);\n      });\n\n      it('should parse list with varying indentation inside list', () => {\n        const input = `\nmeta {\n  tags: [\n  tag_1\n    tag_2\n      tag_3\n  ]\n}\n`;\n        const output = parser(input);\n        const expected = {\n          meta: {\n            seq: 1,\n            tags: ['tag_1', 'tag_2', 'tag_3'],\n            type: 'http'\n          }\n        };\n        expect(output).toEqual(expected);\n      });\n\n      it('should parse list with alphanumeric, underscore, and hyphen characters', () => {\n        const input = `\nmeta {\n  tags: [\n    tag-with-hyphens\n    tag_with_underscores\n    tag123numbers\n    CamelCaseTag\n  ]\n}\n`;\n        const output = parser(input);\n        const expected = {\n          meta: {\n            seq: 1,\n            tags: ['tag-with-hyphens', 'tag_with_underscores', 'tag123numbers', 'CamelCaseTag'],\n            type: 'http'\n          }\n        };\n        expect(output).toEqual(expected);\n      });\n    });\n\n    describe('Invalid List Syntax', () => {\n      it('should fail when list items have no indentation', () => {\n        const input = `\nmeta {\n  tags: [\n    tag_1\ntag_2\n  ]\n}\n`;\n        expect(() => parser(input)).toThrow();\n      });\n\n      it('should fail when list has empty lines between items', () => {\n        const input = `\nmeta {\n  tags: [\n    tag_1\n    \n    tag_2\n  ]\n}\n`;\n        expect(() => parser(input)).toThrow();\n      });\n\n      it('should fail when list opening bracket is on same line as first item', () => {\n        const input = `\nmeta {\n  tags: [tag_1\n  tag_2\n  ]\n}\n`;\n        expect(() => parser(input)).toThrow();\n      });\n\n      it('should fail when list closing bracket is on same line as last item', () => {\n        const input = `\nmeta {\n  tags: [\n    tag_1\n    tag_2]\n}\n`;\n        expect(() => parser(input)).toThrow();\n      });\n\n      it('should fail when list items contain invalid characters - variation 1', () => {\n        const input = `\nmeta {\n  tags: [\n    tag*1\n    tag@2\n  ]\n}\n`;\n        expect(() => parser(input)).toThrow();\n      });\n\n      it('should fail when list items contain spaces', () => {\n        const input = `\nmeta {\n  tags: [\n    tag with spaces\n    another-tag\n  ]\n}\n`;\n        expect(() => parser(input)).toThrow();\n      });\n\n      it('should fail when list items contain invalid characters - variation 2', () => {\n        const input = `\nmeta {\n  tags: [\n    tag_1,\n    tag_2\n  ]\n}\n`;\n        expect(() => parser(input)).toThrow();\n      });\n\n      it('should fail when first list item has no indentation', () => {\n        const input = `\nmeta {\n  tags: [ tag_1\n    tag_2\n  ]\n}\n`;\n        expect(() => parser(input)).toThrow();\n      });\n\n      it('should fail when list item are not seperated by atleast one newline', () => {\n        const input = `\nmeta {\n  tags: [ \n    tag_1\n    tag_2 tag_3\n  ]\n}\n`;\n        expect(() => parser(input)).toThrow();\n      });\n\n      it('should not parse empty list', () => {\n        const input = `\nmeta {\n  tags: [\n  ]\n}\n`;\n\n        expect(() => parser(input)).toThrow();\n      });\n    });\n\n    describe('String Values That Look Like Lists', () => {\n      it('should parse inline bracketed strings as regular values', () => {\n        const input = `\nmeta {\n  name: [some name]\n  tags: [\n    actual_list_item\n  ]\n}\n`;\n        const output = parser(input);\n        const expected = {\n          meta: {\n            seq: 1,\n            name: '[some name]',\n            tags: ['actual_list_item'],\n            type: 'http'\n          }\n        };\n        expect(output).toEqual(expected);\n      });\n\n      it('should parse bracketed strings with spaces as regular values', () => {\n        const input = `\nmeta {\n  name: [ this is the name ]\n  tags: [\n    tag_1\n    tag_2\n  ]\n}\n`;\n        const output = parser(input);\n        const expected = {\n          meta: {\n            seq: 1,\n            name: '[ this is the name ]',\n            tags: ['tag_1', 'tag_2'],\n            type: 'http'\n          }\n        };\n        expect(output).toEqual(expected);\n      });\n\n      it('should fail when multiline bracketed strings are malformed', () => {\n        const input = `\nmeta {\n  name: [this spans\n  multiple lines\n  ]\n}\n`;\n        expect(() => parser(input)).toThrow();\n      });\n    });\n  });\n\n  describe('Lists in Meta Block', () => {\n    it('should parse tags in meta block', () => {\n      const input = `\nmeta {\n  name: API Test\n  tags: [\n    api\n    integration\n    v1\n  ]\n}\n`;\n      const output = parser(input);\n      const expected = {\n        meta: {\n          name: 'API Test',\n          tags: ['api', 'integration', 'v1'],\n          seq: 1,\n          type: 'http'\n        }\n      };\n      expect(output).toEqual(expected);\n    });\n\n    it('should parse custom list properties in meta block', () => {\n      const input = `\nmeta {\n  categories: [\n    user-management\n    auth\n  ]\n  environments: [\n    staging\n    production\n  ]\n}\n`;\n      const output = parser(input);\n      const expected = {\n        meta: {\n          seq: 1,\n          categories: ['user-management', 'auth'],\n          environments: ['staging', 'production'],\n          type: 'http'\n        }\n      };\n      expect(output).toEqual(expected);\n    });\n  });\n\n  describe('Lists type content in Body Blocks', () => {\n    it('should parse bru file with a text body block that has list type values - variation 1', () => {\n      const input = `\nmeta {\n  name: [name]\n  tags: [\n    tag_1\n    tag_2\n  ]\n}\nbody:text {\n  meta {\n    name: [name]\n    tags: [\n      tag_1\n      tag_2\n    ]\n  }\n}\n`;\n      const output = parser(input);\n\n      const expected = {\n        meta: {\n          name: '[name]',\n          tags: [\n            'tag_1',\n            'tag_2'\n          ],\n          seq: 1,\n          type: 'http'\n        },\n        body: {\n          text: `meta {\n  name: [name]\n  tags: [\n    tag_1\n    tag_2\n  ]\n}`\n        }\n      };\n      expect(output).toEqual(expected);\n    });\n\n    it('should parse bru file with a text body block that has list type values - variation 2', () => {\n      const input = `\nmeta {\n  name: [name]\n  tags: [\n    tag_1\n    tag_2\n  ]\n}\nbody:text {\n  meta {\n    name: [name]\n    tags: [\n      tag_1\n      tag_2\n    ]\n  }\n}\n`;\n      const output = parser(input);\n\n      const expected = {\n        meta: {\n          name: '[name]',\n          tags: [\n            'tag_1',\n            'tag_2'\n          ],\n          seq: 1,\n          type: 'http'\n        },\n        body: {\n          text: `meta {\n  name: [name]\n  tags: [\n    tag_1\n    tag_2\n  ]\n}`\n        }\n      };\n      expect(output).toEqual(expected);\n    });\n\n    it('should parse bru file with a json body block that has list type values - variation 1', () => {\n      const input = `\nmeta {\n  name: [name]\n  tags: [\n    tag_1\n    tag_2\n  ]\n}\nbody:json {\n  meta {\n    name: [name]\n    tags: [\n      tag_1\n      tag_2\n    ]\n  }\n}\n`;\n      const output = parser(input);\n\n      const expected = {\n        meta: {\n          name: '[name]',\n          tags: [\n            'tag_1',\n            'tag_2'\n          ],\n          seq: 1,\n          type: 'http'\n        },\n        body: {\n          json: `meta {\n  name: [name]\n  tags: [\n    tag_1\n    tag_2\n  ]\n}`\n        }\n      };\n      expect(output).toEqual(expected);\n    });\n\n    it('should parse bru file with a json body block that has list type values - variation 2', () => {\n      const input = `\nmeta {\n  name: [name]\n  tags: [\n    tag_1\n    tag_2\n  ]\n}\nbody:json {\n  meta {\n    name: [name]\n    tags: [\n      tag_1\n      tag_2\n    ]\n  }\n}\n`;\n      const output = parser(input);\n\n      const expected = {\n        meta: {\n          name: '[name]',\n          tags: [\n            'tag_1',\n            'tag_2'\n          ],\n          seq: 1,\n          type: 'http'\n        },\n        body: {\n          json: `meta {\n  name: [name]\n  tags: [\n    tag_1\n    tag_2\n  ]\n}`\n        }\n      };\n      expect(output).toEqual(expected);\n    });\n\n    it('should parse bru file with a json body block that has array values', () => {\n      const input = `\nmeta {\n  name: [name]\n  tags: [\n    tag_1\n    tag_2\n  ]\n}\nbody:json {\n  {\n    array: [\n      \"1\",\n      \"2\",\n      \"3\"\n    ]\n  }\n}\n`;\n      const output = parser(input);\n\n      const expected = {\n        meta: {\n          name: '[name]',\n          tags: [\n            'tag_1',\n            'tag_2'\n          ],\n          seq: 1,\n          type: 'http'\n        },\n        body: {\n          json: `{\n  array: [\n    \"1\",\n    \"2\",\n    \"3\"\n  ]\n}`\n        }\n      };\n      expect(output).toEqual(expected);\n    });\n\n    it('should parse bru file with a json body block that has array of objects - variation 1', () => {\n      const input = `\nmeta {\n  name: [name]\n  tags: [\n    tag_1\n    tag_2\n  ]\n}\nbody:json {\n  {\n    array: [\n      {\n        \"id\": 1\n      },\n      {\n        \"id\": 2\n      },\n      {\n        \"id\": 3\n      }\n    ]\n  }\n}\n`;\n      const output = parser(input);\n\n      const expected = {\n        meta: {\n          name: '[name]',\n          tags: [\n            'tag_1',\n            'tag_2'\n          ],\n          seq: 1,\n          type: 'http'\n        },\n        body: {\n          json: `{\n  array: [\n    {\n      \"id\": 1\n    },\n    {\n      \"id\": 2\n    },\n    {\n      \"id\": 3\n    }\n  ]\n}`\n        }\n      };\n      expect(output).toEqual(expected);\n    });\n\n    it('should parse bru file with a json body block that has array of objects - variation 2', () => {\n      const input = `\nmeta {\n  name: [name]\n  tags: [\n    tag_1\n    tag_2\n  ]\n}\nbody:json {\n  [{\n    \"foo\": \"bar\"\n  }]\n}\n`;\n      const output = parser(input);\n\n      const expected = {\n        meta: {\n          name: '[name]',\n          tags: [\n            'tag_1',\n            'tag_2'\n          ],\n          seq: 1,\n          type: 'http'\n        },\n        body: {\n          json: `[{\n  \"foo\": \"bar\"\n}]`\n        }\n      };\n      expect(output).toEqual(expected);\n    });\n\n    it('should parse bru file with a json body block that has array of objects - variation 3', () => {\n      const input = `\nmeta {\n  name: [name]\n  tags: [\n    tag_1\n    tag_2\n  ]\n}\nbody:json {\n  [{\"foo\": \"bar\"}]\n}\n`;\n      const output = parser(input);\n\n      const expected = {\n        meta: {\n          name: '[name]',\n          tags: [\n            'tag_1',\n            'tag_2'\n          ],\n          seq: 1,\n          type: 'http'\n        },\n        body: {\n          json: `[{\"foo\": \"bar\"}]`\n        }\n      };\n      expect(output).toEqual(expected);\n    });\n\n    it('should parse bru file with a json body block that has objects and arrays - variation 1', () => {\n      const input = `\nmeta {\n  name: [name]\n  tags: [\n    tag_1\n    tag_2\n  ]\n}\nbody:json {\n  {\n    object: {\n      array: [\n        {\n          \"id\": 1\n        },\n        {\n          \"id\": 2\n        },\n        {\n          \"id\": 3\n        }\n      ]\n    }\n  }\n}\n`;\n      const output = parser(input);\n\n      const expected = {\n        meta: {\n          name: '[name]',\n          tags: [\n            'tag_1',\n            'tag_2'\n          ],\n          seq: 1,\n          type: 'http'\n        },\n        body: {\n          json: `{\n  object: {\n    array: [\n      {\n        \"id\": 1\n      },\n      {\n        \"id\": 2\n      },\n      {\n        \"id\": 3\n      }\n    ]\n  }\n}`\n        }\n      };\n      expect(output).toEqual(expected);\n    });\n\n    it('should parse bru file with a json body block with complex arrays', () => {\n      const input = `\nmeta {\n  name: [name]\n  tags: [\n    tag_1\n    tag_2\n  ]\n}\nbody:json {\n  [\n    \"string\",\n    array: [\n      \"tag_1\",\n      \"tag_2\"\n    ],\n    object: {\n      array: [\n        {\n          \"id\": 1\n        },\n        {\n          \"id\": 2\n        },\n        {\n          \"id\": 3\n        }\n      ]\n    }\n  ]\n}\n`;\n      const output = parser(input);\n\n      const expected = {\n        meta: {\n          name: '[name]',\n          tags: [\n            'tag_1',\n            'tag_2'\n          ],\n          seq: 1,\n          type: 'http'\n        },\n        body: {\n          json: `[\n  \"string\",\n  array: [\n    \"tag_1\",\n    \"tag_2\"\n  ],\n  object: {\n    array: [\n      {\n        \"id\": 1\n      },\n      {\n        \"id\": 2\n      },\n      {\n        \"id\": 3\n      }\n    ]\n  }\n]`\n        }\n      };\n      expect(output).toEqual(expected);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/oauth2-additional-params.spec.js",
    "content": "const bruToJson = require('../src/bruToJson');\nconst collectionBruToJson = require('../src/collectionBruToJson');\n\ndescribe('OAuth2 Additional Parameters - request level', () => {\n  it('should parse all oauth2 additional parameters config types together', () => {\n    const input = `\nmeta {\n  name: OAuth2 Additional Params Test\n  type: http\n}\n\nget {\n  url: https://api.usebruno.com/protected\n}\n\nauth:oauth2 {\n  grant_type: authorization_code\n  client_id: bruno-client-id\n  client_secret: bruno-client-secret\n  authorization_url: https://auth.usebruno.com/oauth/authorize\n  access_token_url: https://auth.usebruno.com/oauth/token\n}\n\nauth:oauth2:additional_params:auth_req:headers {\n  auth-header: auth-header-value\n  ~disabled-auth-header: disabled-auth-header-value\n}\n\nauth:oauth2:additional_params:auth_req:queryparams {\n  auth-query-param: auth-query-param-value\n  ~disabled-auth-query-param: disabled-auth-query-param-value\n}\n\nauth:oauth2:additional_params:access_token_req:headers {\n  token-header: token-header-value\n  ~disabled-token-header: disabled-token-header-value\n}\n\nauth:oauth2:additional_params:access_token_req:queryparams {\n  token-query-param: token-query-param-value\n  ~disabled-token-query-param: disabled-token-query-param-value\n}\n\nauth:oauth2:additional_params:access_token_req:body {\n  token-body: token-body-value\n  ~disabled-token-body: disabled-token-body-value\n}\n\nauth:oauth2:additional_params:refresh_token_req:headers {\n  refresh-header: refresh-header-value\n  ~disabled-refresh-header: disabled-refresh-header-value\n}\n\nauth:oauth2:additional_params:refresh_token_req:queryparams {\n  refresh-query-param: refresh-query-param-value\n  ~disabled-refresh-query-param: disabled-refresh-query-param-value\n}\n\nauth:oauth2:additional_params:refresh_token_req:body {\n  refresh-body: refresh-body-value\n  ~disabled-refresh-body: disabled-refresh-body-value\n}\n    `.trim();\n\n    const result = bruToJson(input);\n\n    // Verify all config types are present\n    expect(result).toHaveProperty('oauth2_additional_parameters_auth_req_headers');\n    expect(result).toHaveProperty('oauth2_additional_parameters_auth_req_queryparams');\n    expect(result).toHaveProperty('oauth2_additional_parameters_access_token_req_headers');\n    expect(result).toHaveProperty('oauth2_additional_parameters_access_token_req_queryparams');\n    expect(result).toHaveProperty('oauth2_additional_parameters_access_token_req_bodyvalues');\n    expect(result).toHaveProperty('oauth2_additional_parameters_refresh_token_req_headers');\n    expect(result).toHaveProperty('oauth2_additional_parameters_refresh_token_req_queryparams');\n    expect(result).toHaveProperty('oauth2_additional_parameters_refresh_token_req_bodyvalues');\n\n    // Verify each has exactly one parameter\n    expect(result.oauth2_additional_parameters_auth_req_headers).toHaveLength(2);\n    expect(result.oauth2_additional_parameters_auth_req_queryparams).toHaveLength(2);\n    expect(result.oauth2_additional_parameters_access_token_req_headers).toHaveLength(2);\n    expect(result.oauth2_additional_parameters_access_token_req_queryparams).toHaveLength(2);\n    expect(result.oauth2_additional_parameters_access_token_req_bodyvalues).toHaveLength(2);\n    expect(result.oauth2_additional_parameters_refresh_token_req_headers).toHaveLength(2);\n    expect(result.oauth2_additional_parameters_refresh_token_req_queryparams).toHaveLength(2);\n    expect(result.oauth2_additional_parameters_refresh_token_req_bodyvalues).toHaveLength(2);\n\n    // Verify parameter values\n    expect(result.oauth2_additional_parameters_auth_req_headers).toEqual([{\n      name: 'auth-header',\n      value: 'auth-header-value',\n      enabled: true\n    }, {\n      name: 'disabled-auth-header',\n      value: 'disabled-auth-header-value',\n      enabled: false\n    }]);\n\n    expect(result.oauth2_additional_parameters_auth_req_queryparams).toEqual([{\n      name: 'auth-query-param',\n      value: 'auth-query-param-value',\n      enabled: true\n    }, {\n      name: 'disabled-auth-query-param',\n      value: 'disabled-auth-query-param-value',\n      enabled: false\n    }]);\n\n    expect(result.oauth2_additional_parameters_access_token_req_headers).toEqual([{\n      name: 'token-header',\n      value: 'token-header-value',\n      enabled: true\n    }, {\n      name: 'disabled-token-header',\n      value: 'disabled-token-header-value',\n      enabled: false\n    }]);\n\n    expect(result.oauth2_additional_parameters_access_token_req_queryparams).toEqual([{\n      name: 'token-query-param',\n      value: 'token-query-param-value',\n      enabled: true\n    }, {\n      name: 'disabled-token-query-param',\n      value: 'disabled-token-query-param-value',\n      enabled: false\n    }]);\n\n    expect(result.oauth2_additional_parameters_access_token_req_bodyvalues).toEqual([{\n      name: 'token-body',\n      value: 'token-body-value',\n      enabled: true\n    }, {\n      name: 'disabled-token-body',\n      value: 'disabled-token-body-value',\n      enabled: false\n    }]);\n\n    expect(result.oauth2_additional_parameters_refresh_token_req_headers).toEqual([{\n      name: 'refresh-header',\n      value: 'refresh-header-value',\n      enabled: true\n    }, {\n      name: 'disabled-refresh-header',\n      value: 'disabled-refresh-header-value',\n      enabled: false\n    }]);\n\n    expect(result.oauth2_additional_parameters_refresh_token_req_queryparams).toEqual([{\n      name: 'refresh-query-param',\n      value: 'refresh-query-param-value',\n      enabled: true\n    }, {\n      name: 'disabled-refresh-query-param',\n      value: 'disabled-refresh-query-param-value',\n      enabled: false\n    }]);\n\n    expect(result.oauth2_additional_parameters_refresh_token_req_bodyvalues).toEqual([{\n      name: 'refresh-body',\n      value: 'refresh-body-value',\n      enabled: true\n    }, {\n      name: 'disabled-refresh-body',\n      value: 'disabled-refresh-body-value',\n      enabled: false\n    }]);\n  });\n});\n\ndescribe('OAuth2 Additional Parameters - collection/folder level', () => {\n  it('should parse all oauth2 additional parameters config types together', () => {\n    const input = `\nauth {\n  mode: oauth2\n}\n\nauth:oauth2 {\n  grant_type: authorization_code\n  client_id: bruno-client-id\n  client_secret: bruno-client-secret\n  authorization_url: https://auth.usebruno.com/oauth/authorize\n  access_token_url: https://auth.usebruno.com/oauth/token\n}\n\nauth:oauth2:additional_params:auth_req:headers {\n  auth-header: auth-header-value\n  ~disabled-auth-header: disabled-auth-header-value\n}\n\nauth:oauth2:additional_params:auth_req:queryparams {\n  auth-query-param: auth-query-param-value\n  ~disabled-auth-query-param: disabled-auth-query-param-value\n}\n\nauth:oauth2:additional_params:access_token_req:headers {\n  token-header: token-header-value\n  ~disabled-token-header: disabled-token-header-value\n}\n\nauth:oauth2:additional_params:access_token_req:queryparams {\n  token-query-param: token-query-param-value\n  ~disabled-token-query-param: disabled-token-query-param-value\n}\n\nauth:oauth2:additional_params:access_token_req:body {\n  token-body: token-body-value\n  ~disabled-token-body: disabled-token-body-value\n}\n\nauth:oauth2:additional_params:refresh_token_req:headers {\n  refresh-header: refresh-header-value\n  ~disabled-refresh-header: disabled-refresh-header-value\n}\n\nauth:oauth2:additional_params:refresh_token_req:queryparams {\n  refresh-query-param: refresh-query-param-value\n  ~disabled-refresh-query-param: disabled-refresh-query-param-value\n}\n\nauth:oauth2:additional_params:refresh_token_req:body {\n  refresh-body: refresh-body-value\n  ~disabled-refresh-body: disabled-refresh-body-value\n}\n   `.trim();\n\n    const result = collectionBruToJson(input);\n\n    // Verify all config types are present\n    expect(result).toHaveProperty('oauth2_additional_parameters_auth_req_headers');\n    expect(result).toHaveProperty('oauth2_additional_parameters_auth_req_queryparams');\n    expect(result).toHaveProperty('oauth2_additional_parameters_access_token_req_headers');\n    expect(result).toHaveProperty('oauth2_additional_parameters_access_token_req_queryparams');\n    expect(result).toHaveProperty('oauth2_additional_parameters_access_token_req_bodyvalues');\n    expect(result).toHaveProperty('oauth2_additional_parameters_refresh_token_req_headers');\n    expect(result).toHaveProperty('oauth2_additional_parameters_refresh_token_req_queryparams');\n    expect(result).toHaveProperty('oauth2_additional_parameters_refresh_token_req_bodyvalues');\n\n    // Verify each has exactly one parameter\n    expect(result.oauth2_additional_parameters_auth_req_headers).toHaveLength(2);\n    expect(result.oauth2_additional_parameters_auth_req_queryparams).toHaveLength(2);\n    expect(result.oauth2_additional_parameters_access_token_req_headers).toHaveLength(2);\n    expect(result.oauth2_additional_parameters_access_token_req_queryparams).toHaveLength(2);\n    expect(result.oauth2_additional_parameters_access_token_req_bodyvalues).toHaveLength(2);\n    expect(result.oauth2_additional_parameters_refresh_token_req_headers).toHaveLength(2);\n    expect(result.oauth2_additional_parameters_refresh_token_req_queryparams).toHaveLength(2);\n    expect(result.oauth2_additional_parameters_refresh_token_req_bodyvalues).toHaveLength(2);\n\n    // Verify parameter values\n    expect(result.oauth2_additional_parameters_auth_req_headers).toEqual([{\n      name: 'auth-header',\n      value: 'auth-header-value',\n      enabled: true\n    }, {\n      name: 'disabled-auth-header',\n      value: 'disabled-auth-header-value',\n      enabled: false\n    }]);\n\n    expect(result.oauth2_additional_parameters_auth_req_queryparams).toEqual([{\n      name: 'auth-query-param',\n      value: 'auth-query-param-value',\n      enabled: true\n    }, {\n      name: 'disabled-auth-query-param',\n      value: 'disabled-auth-query-param-value',\n      enabled: false\n    }]);\n\n    expect(result.oauth2_additional_parameters_access_token_req_headers).toEqual([{\n      name: 'token-header',\n      value: 'token-header-value',\n      enabled: true\n    }, {\n      name: 'disabled-token-header',\n      value: 'disabled-token-header-value',\n      enabled: false\n    }]);\n\n    expect(result.oauth2_additional_parameters_access_token_req_queryparams).toEqual([{\n      name: 'token-query-param',\n      value: 'token-query-param-value',\n      enabled: true\n    }, {\n      name: 'disabled-token-query-param',\n      value: 'disabled-token-query-param-value',\n      enabled: false\n    }]);\n\n    expect(result.oauth2_additional_parameters_access_token_req_bodyvalues).toEqual([{\n      name: 'token-body',\n      value: 'token-body-value',\n      enabled: true\n    }, {\n      name: 'disabled-token-body',\n      value: 'disabled-token-body-value',\n      enabled: false\n    }]);\n\n    expect(result.oauth2_additional_parameters_refresh_token_req_headers).toEqual([{\n      name: 'refresh-header',\n      value: 'refresh-header-value',\n      enabled: true\n    }, {\n      name: 'disabled-refresh-header',\n      value: 'disabled-refresh-header-value',\n      enabled: false\n    }]);\n\n    expect(result.oauth2_additional_parameters_refresh_token_req_queryparams).toEqual([{\n      name: 'refresh-query-param',\n      value: 'refresh-query-param-value',\n      enabled: true\n    }, {\n      name: 'disabled-refresh-query-param',\n      value: 'disabled-refresh-query-param-value',\n      enabled: false\n    }]);\n\n    expect(result.oauth2_additional_parameters_refresh_token_req_bodyvalues).toEqual([{\n      name: 'refresh-body',\n      value: 'refresh-body-value',\n      enabled: true\n    }, {\n      name: 'disabled-refresh-body',\n      value: 'disabled-refresh-body-value',\n      enabled: false\n    }]);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/script.spec.js",
    "content": "/**\n * This test file is used to test the text parser.\n */\nconst parser = require('../src/bruToJson');\n\ndescribe('script parser', () => {\n  it('should parse request script', () => {\n    const input = `\nscript:pre-request {\n  $req.setHeader('Content-Type', 'application/json');\n}\n`;\n\n    const output = parser(input);\n    const expected = {\n      script: {\n        req: '$req.setHeader(\\'Content-Type\\', \\'application/json\\');'\n      }\n    };\n    expect(output).toEqual(expected);\n  });\n\n  it('should parse response script', () => {\n    const input = `\nscript:post-response {\n  expect(response.status).to.equal(200);\n}\n`;\n\n    const output = parser(input);\n    const expected = {\n      script: {\n        res: 'expect(response.status).to.equal(200);'\n      }\n    };\n    expect(output).toEqual(expected);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/settings/fixtures/settings-all-options.bru",
    "content": "meta {\n  name: Settings All Options Test\n  type: http\n  seq: 3\n}\n\nput {\n  url: https://api.example.com/all-options\n}\n\nheaders {\n  content-type: application/json\n}\n\nbody:json {\n  {\n    \"test\": \"data\"\n  }\n}\n\nsettings {\n  encodeUrl: true\n  followRedirects: false\n  maxRedirects: 0\n  timeout: 60000\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/settings/fixtures/settings-all-options.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Settings All Options Test\",\n    \"type\": \"http\",\n    \"seq\": \"3\"\n  },\n  \"http\": {\n    \"method\": \"put\",\n    \"url\": \"https://api.example.com/all-options\"\n  },\n  \"headers\": [\n    {\n      \"name\": \"content-type\",\n      \"value\": \"application/json\",\n      \"enabled\": true\n    }\n  ],\n  \"body\": {\n    \"json\": \"{\\n  \\\"test\\\": \\\"data\\\"\\n}\"\n  },\n  \"settings\": {\n    \"encodeUrl\": true,\n    \"followRedirects\": false,\n    \"maxRedirects\": 0,\n    \"timeout\": 60000\n  }\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/settings/fixtures/settings-minimal.bru",
    "content": "meta {\n  name: Settings Minimal Test\n  type: http\n  seq: 2\n}\n\npost {\n  url: https://api.example.com/minimal\n}\n\nsettings {\n  encodeUrl: false\n  timeout: 5000\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/settings/fixtures/settings-minimal.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Settings Minimal Test\",\n    \"type\": \"http\",\n    \"seq\": \"2\"\n  },\n  \"http\": {\n    \"method\": \"post\",\n    \"url\": \"https://api.example.com/minimal\"\n  },\n  \"settings\": {\n    \"encodeUrl\": false,\n    \"timeout\": 5000\n  }\n}\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/settings/settings.spec.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst bruToJson = require('../../src/bruToJson');\nconst jsonToBru = require('../../src/jsonToBru');\n\ndescribe('Settings Conversion Tests', () => {\n  const fixturesDir = path.join(__dirname, 'fixtures');\n\n  describe('parse (BRU to JSON)', () => {\n    it('should parse minimal settings from BRU to JSON', () => {\n      const input = fs.readFileSync(path.join(fixturesDir, 'settings-minimal.bru'), 'utf8');\n      const expected = require(path.join(fixturesDir, 'settings-minimal.json'));\n      const output = bruToJson(input);\n\n      expect(output).toEqual(expected);\n    });\n\n    it('should parse all settings options from BRU to JSON', () => {\n      const input = fs.readFileSync(path.join(fixturesDir, 'settings-all-options.bru'), 'utf8');\n      const expected = require(path.join(fixturesDir, 'settings-all-options.json'));\n      const output = bruToJson(input);\n\n      expect(output).toEqual(expected);\n    });\n  });\n\n  describe('stringify (JSON to BRU)', () => {\n    it('should stringify minimal settings from JSON to BRU (with defaults)', () => {\n      const input = require(path.join(fixturesDir, 'settings-minimal.json'));\n      const expected = fs.readFileSync(path.join(fixturesDir, 'settings-minimal.bru'), 'utf8');\n      const output = jsonToBru(input);\n\n      expect(output).toEqual(expected);\n    });\n\n    it('should stringify all settings options from JSON to BRU', () => {\n      const input = require(path.join(fixturesDir, 'settings-all-options.json'));\n      const expected = fs.readFileSync(path.join(fixturesDir, 'settings-all-options.bru'), 'utf8');\n      const output = jsonToBru(input);\n\n      expect(output).toEqual(expected);\n    });\n  });\n\n  describe('round-trip conversion', () => {\n    it('should maintain data integrity through JSON -> BRU -> JSON conversion', () => {\n      const originalJson = require(path.join(fixturesDir, 'settings-all-options.json'));\n\n      // Convert JSON to BRU\n      const bru = jsonToBru(originalJson);\n\n      // Convert BRU back to JSON\n      const convertedJson = bruToJson(bru);\n\n      expect(convertedJson).toEqual(originalJson);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/tags.spec.js",
    "content": "/**\n * This test file is used to test the text parser.\n */\nconst parser = require('../src/bruToJson');\n\ndescribe('tags parser', () => {\n  it('should parse request tags', () => {\n    const input = `\nmeta {\n  name: request\n  type: http\n  seq: 1\n  tags: [\n    tag_1\n    tag_2\n    tag_3\n    tag_4\n  ]\n}\n`;\n\n    const output = parser(input);\n    const expected = {\n      meta: {\n        name: 'request',\n        type: 'http',\n        tags: ['tag_1', 'tag_2', 'tag_3', 'tag_4'],\n        seq: '1'\n      }\n    };\n    expect(output).toEqual(expected);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-lang/v2/tests/utils.spec.js",
    "content": "const { getValueString } = require('../src/utils');\n\ndescribe('getValueString', () => {\n  it('returns single line value as-is', () => {\n    expect(getValueString('hello world')).toBe('hello world');\n  });\n\n  it('wraps multiline value in triple quotes with indentation', () => {\n    expect(getValueString('line1\\nline2\\nline3')).toBe('\\'\\'\\'\\n  line1\\n  line2\\n  line3\\n\\'\\'\\'');\n  });\n\n  it('normalizes different newline types', () => {\n    expect(getValueString('line1\\r\\nline2\\rline3\\nline4')).toBe('\\'\\'\\'\\n  line1\\n  line2\\n  line3\\n  line4\\n\\'\\'\\'');\n  });\n\n  it('returns empty string for empty/null/undefined', () => {\n    expect(getValueString('')).toBe('');\n    expect(getValueString(null)).toBe('');\n    expect(getValueString(undefined)).toBe('');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-query/.gitignore",
    "content": "# dependencies\nnode_modules\nyarn.lock\npnpm-lock.yaml\npackage-lock.json\n.pnp\n.pnp.js\n\n# testing\ncoverage\n\n# production\ndist\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "packages/bruno-query/jest.config.js",
    "content": "/** @type {import('ts-jest').JestConfigWithTsJest} */\nmodule.exports = {\n  preset: 'ts-jest',\n  testEnvironment: 'node',\n};"
  },
  {
    "path": "packages/bruno-query/license.md",
    "content": "\nMIT License\n\nCopyright (c) 2022 Anoop M D, Anusree P S and Contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "packages/bruno-query/package.json",
    "content": "{\n  \"name\": \"@usebruno/query\",\n  \"version\": \"0.1.0\",\n  \"license\" : \"MIT\",\n  \"main\": \"dist/cjs/index.js\",\n  \"module\": \"dist/esm/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"files\": [\n    \"dist\",\n    \"src\",\n    \"license.md\",\n    \"readme.md\",\n    \"package.json\"\n  ],\n  \"scripts\": {\n    \"clean\": \"rimraf dist\",\n    \"test\": \"jest\",\n    \"prebuild\": \"npm run clean\",\n    \"build\": \"rollup -c\",\n    \"watch\": \"rollup -c -w\",\n    \"prepack\": \"npm run test && npm run build\"\n  },\n  \"devDependencies\": {\n    \"@rollup/plugin-commonjs\": \"^23.0.2\",\n    \"@rollup/plugin-node-resolve\": \"^15.0.1\",\n    \"@rollup/plugin-typescript\": \"^9.0.2\",\n    \"rollup\":\"3.29.5\",\n    \"rollup-plugin-dts\": \"^5.0.0\",\n    \"rollup-plugin-peer-deps-external\": \"^2.2.4\",\n    \"rollup-plugin-terser\": \"^7.0.2\",\n    \"typescript\": \"^4.8.4\"\n  },\n  \"overrides\": {\n    \"rollup\":\"3.29.5\"\n  }\n}"
  },
  {
    "path": "packages/bruno-query/readme.md",
    "content": "# bruno-query\n\nBruno query with deep navigation, filter and map support\n\nEasy array navigation\n```js\nget(data, 'customer.orders.items.amount')\n```\nDeep navigation .. double dots\n```js\nget(data, '..items.amount')\n```\nArray indexing\n```js\nget(data, '..items[0].amount')\n```\nArray filtering [?] with corresponding filter function\n```js\nget(data, '..items[?].amount', i => i.amount > 20) \n```\nArray filtering [?] with simple object predicate, same as (i => i.id === 2 && i.amount === 20)\n```js\nget(data, '..items[?]', { id: 2, amount: 20 }) \n```\nArray mapping [?] with corresponding mapper function\n```js\nget(data, '..items..amount[?]', amt => amt + 10) \n```\n\n### Publish to Npm Registry\n```bash\nnpm publish --access=public\n```\n"
  },
  {
    "path": "packages/bruno-query/rollup.config.js",
    "content": "const { nodeResolve } = require(\"@rollup/plugin-node-resolve\");\nconst commonjs = require(\"@rollup/plugin-commonjs\");\nconst typescript = require(\"@rollup/plugin-typescript\");\nconst dts = require(\"rollup-plugin-dts\");\nconst { terser } = require(\"rollup-plugin-terser\");\nconst peerDepsExternal = require('rollup-plugin-peer-deps-external');\n\nconst packageJson = require(\"./package.json\");\n\nmodule.exports = [\n  {\n    input: \"src/index.ts\",\n    output: [\n      {\n        file: packageJson.main,\n        format: \"cjs\",\n        sourcemap: true,\n      },\n      {\n        file: packageJson.module,\n        format: \"esm\",\n        sourcemap: true,\n      },\n    ],\n    plugins: [\n      peerDepsExternal(),\n      nodeResolve({\n        extensions: ['.css']\n      }),\n      commonjs(),\n      typescript({ tsconfig: \"./tsconfig.json\" }),\n      terser()\n    ]\n  },\n  {\n    input: \"dist/esm/index.d.ts\",\n    output: [{ file: \"dist/index.d.ts\", format: \"esm\" }],\n    plugins: [dts.default()],\n  }\n];"
  },
  {
    "path": "packages/bruno-query/src/index.ts",
    "content": "/**\n * If value is an array returns the deeply flattened array, otherwise value\n */\nfunction normalize(value: any) {\n  if (!Array.isArray(value)) return value;\n\n  const values = [] as any[];\n\n  value.forEach((item) => {\n    const value = normalize(item);\n    if (value != null) {\n      values.push(...(Array.isArray(value) ? value : [value]));\n    }\n  });\n\n  return values.length ? values : undefined;\n}\n\n/**\n * Gets value of a prop from source.\n *\n * If source is an array get value from each item.\n *\n * If deep is true then recursively gets values for prop in nested objects.\n *\n * Once a value is found will not recurse further into that value.\n */\nfunction getValue(source: any, prop: string, deep = false): any {\n  if (typeof source !== 'object') return;\n\n  let value;\n\n  if (Array.isArray(source)) {\n    value = source.map((item) => getValue(item, prop, deep));\n  } else {\n    value = source[prop];\n    if (deep) {\n      value = [value];\n      for (const [key, item] of Object.entries(source)) {\n        if (key !== prop && typeof item === 'object') {\n          value.push(getValue(source[key], prop, deep));\n        }\n      }\n    }\n  }\n\n  return normalize(value);\n}\n\ntype PredicateOrMapper = ((obj: any) => any) | Record<string, any>;\n\n/**\n * Make a predicate function that checks scalar properties for equality\n */\nfunction objectPredicate(obj: Record<string, any>) {\n  return (item: any) => {\n    for (const [key, value] of Object.entries(obj)) {\n      if (item[key] !== value) return false;\n    }\n    return true;\n  };\n}\n\n/**\n * Apply filter on source array or object\n *\n * If the filter returns a non boolean non null value it is treated as a mapped value\n */\nfunction filterOrMap(source: any, funOrObj: PredicateOrMapper) {\n  const fun = typeof funOrObj === 'object' ? objectPredicate(funOrObj) : funOrObj;\n  const isArray = Array.isArray(source);\n  const list = isArray ? source : [source];\n  const result = [] as any[];\n  for (const item of list) {\n    if (item == null) continue;\n    const value = fun(item);\n    if (value === true) {\n      result.push(item); // predicate\n    } else if (value != null && value !== false) {\n      result.push(value); // mapper\n    }\n  }\n  return normalize(isArray ? result : result[0]);\n}\n\n/**\n * Getter with deep navigation, filter and map support\n *\n * 1. Easy array navigation\n *    ```js\n *    get(data, 'customer.orders.items.amount')\n *    ```\n * 2. Deep navigation .. double dots\n *    ```js\n *    get(data, '..items.amount')\n *    ```\n * 3. Array indexing\n *    ```js\n *    get(data, '..items[0].amount')\n *    ```\n * 4. Array filtering [?] with corresponding filter function\n *    ```js\n *    get(data, '..items[?].amount', i => i.amount > 20)\n *    ```\n * 5. Array filtering [?] with simple object predicate, same as (i => i.id === 2 && i.amount === 20)\n *    ```js\n *    get(data, '..items[?]', { id: 2, amount: 20 })\n *    ```\n * 6. Array mapping [?] with corresponding mapper function\n *    ```js\n *    get(data, '..items[?].amount', i => i.amount + 10)\n *    ```\n */\nexport function get(source: any, path: string, ...fns: PredicateOrMapper[]) {\n  const paths = path\n    .replace(/\\s+/g, '')\n    .split(/(\\.{1,2}|\\[\\?\\]|\\[\\d+\\])/g) // [\"..\", \"items\", \"[?]\", \".\", \"amount\", \"[0]\" ]\n    .filter((s) => s.length > 0)\n    .map((str) => {\n      str = str.replace(/\\[|\\]/g, '');\n      const index = parseInt(str);\n      return isNaN(index) ? str : index;\n    });\n\n  let index = 0,\n    lookbehind = '' as string | number,\n    funIndex = 0;\n\n  while (source != null && index < paths.length) {\n    const token = paths[index++];\n\n    switch (true) {\n      case token === '..':\n      case token === '.':\n        break;\n      case token === '?':\n        const fun = fns[funIndex++];\n        if (fun == null) throw new Error(`missing function for ${lookbehind}`);\n        source = filterOrMap(source, fun);\n        break;\n      case typeof token === 'number':\n        source = normalize(source[token]);\n        break;\n      default:\n        source = getValue(source, token as string, lookbehind === '..');\n    }\n\n    lookbehind = token;\n  }\n\n  return source;\n}\n"
  },
  {
    "path": "packages/bruno-query/tests/index.spec.ts",
    "content": "import { describe, expect, it } from '@jest/globals';\n\nimport { get } from '../src/index';\n\nconst data = {\n  customer: {\n    address: {\n      city: 'bangalore'\n    },\n    orders: [\n      {\n        id: 'order-1',\n        items: [\n          { id: 1, amount: 10 },\n          { id: 2, amount: 20 }\n        ]\n      },\n      {\n        id: 'order-2',\n        items: [\n          { id: 3, amount: 30 },\n          { id: 4, amount: 40 }\n        ]\n      }\n    ]\n  }\n};\n\ndescribe('get', () => {\n  it.each([\n    ['customer.address.city', 'bangalore'],\n    ['customer.orders.items.amount', [10, 20, 30, 40]],\n    ['customer.orders.items.amount[0]', 10],\n    ['..items.amount', [10, 20, 30, 40]],\n    ['..amount', [10, 20, 30, 40]],\n    ['..items.amount[0]', 10],\n    ['..items[0].amount', 10],\n    ['..items[5].amount', undefined], // invalid index\n    ['..id', ['order-1', 1, 2, 'order-2', 3, 4]], // all ids\n    ['customer.orders.foo', undefined],\n    ['..customer.foo', undefined],\n    ['..address', [{ city: 'bangalore' }]], // .. will return array\n    ['..address[0]', { city: 'bangalore' }]\n  ])('%s should be %j', (expr, result) => {\n    expect(get(data, expr)).toEqual(result);\n  });\n\n  // filter and map\n  it.each([\n    ['..items[?].amount', [40], (i: any) => i.amount > 30], // [?] filter\n    ['..items[?].amount', [40], { id: 4, amount: 40 }], // object filter\n    ['..items[?].amount', undefined, { id: 5, amount: 40 }],\n    ['..items..amount[?][0]', 40, (amt: number) => amt > 30],\n    ['..items..amount[0][?]', undefined, (amt: number) => amt > 30], // filter on single value\n    ['..items..amount[?]', [11, 21, 31, 41], (amt: number) => amt + 1], // [?] mapper\n    ['..items..amount[0][?]', 11, (amt: number) => amt + 1] // [?] map on single value\n  ])('%s should be %j for %s', (expr, result, filter) => {\n    expect(get(data, expr, filter)).toEqual(result);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-query/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES6\",\n    \"esModuleInterop\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"jsx\": \"react\",\n    \"module\": \"ESNext\",\n    \"declaration\": true,\n    \"declarationDir\": \"types\",\n    \"sourceMap\": true,\n    \"outDir\": \"dist\",\n    \"moduleResolution\": \"node\",\n    \"emitDeclarationOnly\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"forceConsistentCasingInFileNames\": true\n  },\n  \"exclude\": [\n    \"dist\",\n    \"node_modules\",\n    \"tests\"\n  ],\n}"
  },
  {
    "path": "packages/bruno-requests/.gitignore",
    "content": "# dependencies\nnode_modules\nyarn.lock\npnpm-lock.yaml\npackage-lock.json\n.pnp\n.pnp.js\n\n# testing\ncoverage\n\n# production\ndist\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "packages/bruno-requests/babel.config.js",
    "content": "module.exports = {\n  presets: [\n    ['@babel/preset-env', { targets: { node: 'current' } }],\n    '@babel/preset-typescript'\n  ]\n};\n"
  },
  {
    "path": "packages/bruno-requests/jest.config.js",
    "content": "module.exports = {\n  transform: {\n    '^.+\\\\.(ts|js)$': 'babel-jest'\n  },\n  transformIgnorePatterns: [\n    '/node_modules/(?!(lodash-es|is-ip|ip-regex|super-regex|function-timeout|time-span|convert-hrtime|clone-regexp|is-regexp)/)'\n  ],\n  testEnvironment: 'node',\n  testMatch: [\n    '**/*.(test|spec).(ts|js)'\n  ],\n  moduleFileExtensions: ['ts', 'js', 'json']\n};\n"
  },
  {
    "path": "packages/bruno-requests/package.json",
    "content": "{\n  \"name\": \"@usebruno/requests\",\n  \"version\": \"0.1.0\",\n  \"license\": \"MIT\",\n  \"main\": \"dist/cjs/index.js\",\n  \"module\": \"dist/esm/index.js\",\n  \"types\": \"dist/index.d.js\",\n  \"files\": [\n    \"dist\",\n    \"src\",\n    \"package.json\"\n  ],\n  \"scripts\": {\n    \"clean\": \"rimraf dist\",\n    \"prebuild\": \"npm run clean\",\n    \"build\": \"rollup -c\",\n    \"watch\": \"rollup -c -w\",\n    \"test\": \"jest\",\n    \"test:watch\": \"jest --watch\",\n    \"prepack\": \"npm run test && npm run build\"\n  },\n  \"dependencies\": {\n    \"@faker-js/faker\": \"^9.7.0\",\n    \"@grpc/grpc-js\": \"^1.13.3\",\n    \"@grpc/proto-loader\": \"^0.7.15\",\n    \"@types/qs\": \"^6.9.18\",\n    \"axios\": \"^1.9.0\",\n    \"debug\": \"^4.4.3\",\n    \"google-protobuf\": \"^4.0.0\",\n    \"grpc-js-reflection-client\": \"^1.3.0\",\n    \"http-proxy-agent\": \"~7.0.2\",\n    \"https-proxy-agent\": \"~7.0.6\",\n    \"is-ip\": \"^5.0.1\",\n    \"socks-proxy-agent\": \"~8.0.5\",\n    \"system-ca\": \"^2.0.1\",\n    \"tough-cookie\": \"^6.0.0\",\n    \"ws\": \"^8.18.3\",\n    \"shell-env\": \"^4.0.1\"\n  },\n  \"devDependencies\": {\n    \"@babel/preset-env\": \"^7.22.0\",\n    \"@babel/preset-typescript\": \"^7.22.0\",\n    \"@rollup/plugin-alias\": \"^5.1.1\",\n    \"@rollup/plugin-commonjs\": \"^23.0.2\",\n    \"@rollup/plugin-json\": \"^6.1.0\",\n    \"@rollup/plugin-node-resolve\": \"^15.0.1\",\n    \"@rollup/plugin-typescript\": \"^9.0.2\",\n    \"@types/jest\": \"^29.5.11\",\n    \"babel-jest\": \"^29.7.0\",\n    \"builtin-modules\": \"^5.0.0\",\n    \"jest\": \"^29.2.0\",\n    \"rollup\": \"3.29.5\",\n    \"rollup-plugin-dts\": \"^5.0.0\",\n    \"rollup-plugin-peer-deps-external\": \"^2.2.4\",\n    \"rollup-plugin-terser\": \"^7.0.2\",\n    \"typescript\": \"^4.8.4\"\n  },\n  \"overrides\": {\n    \"rollup\": \"3.29.5\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-requests/rollup.config.js",
    "content": "const { nodeResolve } = require('@rollup/plugin-node-resolve');\nconst commonjs = require('@rollup/plugin-commonjs');\nconst typescript = require('@rollup/plugin-typescript');\nconst dts = require('rollup-plugin-dts');\nconst { terser } = require('rollup-plugin-terser');\nconst peerDepsExternal = require('rollup-plugin-peer-deps-external');\nconst json = require('@rollup/plugin-json');\nconst { isBuiltin } = require('module');\nconst packageJson = require('./package.json');\n\nmodule.exports = [\n  {\n    input: 'src/index.ts',\n    output: [\n      {\n        file: packageJson.main,\n        format: 'cjs',\n        sourcemap: true,\n        exports: 'named'\n      },\n      {\n        file: packageJson.module,\n        format: 'esm',\n        sourcemap: true,\n        exports: 'named'\n      }\n    ],\n    plugins: [\n      peerDepsExternal(),\n      nodeResolve({\n        extensions: ['.js', '.ts', '.tsx', '.json', '.css'],\n        dedupe: ['@grpc/grpc-js'],\n        preferBuiltins: true\n      }),\n      json(),\n      commonjs({\n        transformMixedEsModules: true\n      }),\n      typescript({ tsconfig: './tsconfig.json' }),\n      terser()\n    ],\n    external: (id) => isBuiltin(id) || ['axios', 'qs', 'ws', 'debug', 'shell-env'].includes(id)\n  }\n];\n"
  },
  {
    "path": "packages/bruno-requests/src/auth/digestauth-helper.js",
    "content": "const crypto = require('crypto');\nconst { URL } = require('node:url');\n\nfunction isStrPresent(str) {\n  return str && str.trim() !== '' && str.trim() !== 'undefined';\n}\n\nfunction stripQuotes(str) {\n  return str.replace(/\"/g, '');\n}\n\nfunction splitAuthHeaderKeyValue(str) {\n  const indexOfEqual = str.indexOf('=');\n  const key = str.substring(0, indexOfEqual).trim();\n  const value = str.substring(indexOfEqual + 1);\n  return [key, value];\n}\n\nfunction containsDigestHeader(response) {\n  const authHeader = response?.headers?.['www-authenticate'];\n  return authHeader ? authHeader.trim().toLowerCase().startsWith('digest') : false;\n}\n\nfunction containsAuthorizationHeader(originalRequest) {\n  return Boolean(\n    originalRequest.headers['Authorization']\n    || originalRequest.headers['authorization']\n  );\n}\n\nfunction md5(input) {\n  return crypto.createHash('md5').update(input).digest('hex');\n}\n\nexport function addDigestInterceptor(axiosInstance, request) {\n  const { username, password } = request.digestConfig;\n  console.debug('Digest Auth Interceptor Initialized');\n\n  if (!isStrPresent(username) || !isStrPresent(password)) {\n    console.warn('Required Digest Auth fields (username/password) are not present');\n    return;\n  }\n\n  axiosInstance.interceptors.response.use(\n    (response) => response,\n    (error) => {\n      const originalRequest = error.config;\n\n      // Prevent retry loops\n      if (originalRequest._retry) {\n        return Promise.reject(error);\n      }\n      originalRequest._retry = true;\n\n      if (\n        error.response?.status === 401\n        && containsDigestHeader(error.response)\n        && !containsAuthorizationHeader(originalRequest)\n      ) {\n        console.debug('Processing Digest Authentication Challenge');\n        console.debug(error.response.headers['www-authenticate']);\n\n        const authDetails = error.response.headers['www-authenticate']\n          .split(',')\n          .map((pair) => splitAuthHeaderKeyValue(pair).map((item) => item.trim()).map(stripQuotes))\n          .reduce((acc, [key, value]) => {\n            const normalizedKey = key.toLowerCase().replace('digest ', '');\n            if (normalizedKey && value !== undefined) {\n              acc[normalizedKey] = value;\n            }\n            return acc;\n          }, {});\n\n        // Validate required auth details\n        if (!authDetails.realm || !authDetails.nonce) {\n          console.warn('Missing required auth details (realm or nonce)');\n          return Promise.reject(error);\n        }\n\n        console.debug('Auth Details: \\n', authDetails);\n\n        const nonceCount = '00000001';\n        const cnonce = crypto.randomBytes(24).toString('hex');\n\n        if (authDetails.algorithm && authDetails.algorithm.toUpperCase() !== 'MD5') {\n          console.warn(`Unsupported Digest algorithm: ${authDetails.algorithm}`);\n          return Promise.reject(error);\n        } else {\n          authDetails.algorithm = 'MD5';\n        }\n\n        // Build full URL from the original request (may include query params and baseURL)\n        const resolvedUrl = new URL(\n          originalRequest.url || request.url,\n          originalRequest.baseURL || request.baseURL || 'http://localhost'\n        );\n        const uri = `${resolvedUrl.pathname}${resolvedUrl.search}`;\n        // Used 'GET' as default method to avoid missing method error\n        const method = (originalRequest.method || request.method || 'GET').toUpperCase();\n        const HA1 = md5(`${username}:${authDetails.realm}:${password}`);\n        const HA2 = md5(`${method}:${uri}`);\n        let response;\n        if (authDetails.qop && authDetails.qop.split(',').map((q) => q.trim().toLowerCase()).includes('auth')) {\n          console.debug('Using QOP \\'auth\\' for Digest Authentication');\n          response = md5(`${HA1}:${authDetails.nonce}:${nonceCount}:${cnonce}:auth:${HA2}`);\n        } else {\n          console.debug('No QOP specified, using simple digest');\n          response = md5(`${HA1}:${authDetails.nonce}:${HA2}`);\n        }\n\n        const headerFields = [\n          `username=\"${username}\"`,\n          `realm=\"${authDetails.realm}\"`,\n          `nonce=\"${authDetails.nonce}\"`,\n          `uri=\"${uri}\"`,\n          `response=\"${response}\"`\n        ];\n\n        if (authDetails.qop && authDetails.qop.split(',').map((q) => q.trim().toLowerCase()).includes('auth')) {\n          headerFields.push(`qop=\"auth\"`, `algorithm=\"${authDetails.algorithm}\"`, `nc=\"${nonceCount}\"`, `cnonce=\"${cnonce}\"`);\n        }\n\n        if (authDetails.opaque) {\n          headerFields.push(`opaque=\"${authDetails.opaque}\"`);\n        }\n\n        const authorizationHeader = `Digest ${headerFields.join(', ')}`;\n\n        // Ensure headers are initialized\n        originalRequest.headers = originalRequest.headers || {};\n        originalRequest.headers['Authorization'] = authorizationHeader;\n\n        console.debug(`Authorization: ${originalRequest.headers['Authorization']}`);\n\n        delete originalRequest.digestConfig;\n\n        return axiosInstance(originalRequest);\n      }\n\n      return Promise.reject(error);\n    }\n  );\n}\n"
  },
  {
    "path": "packages/bruno-requests/src/auth/digestauth-helper.spec.js",
    "content": "const axios = require('axios');\nconst { addDigestInterceptor } = require('./digestauth-helper');\n\ndescribe('Digest Auth with query params', () => {\n  test('uri should include path and query string', async () => {\n    const axiosInstance = axios.create();\n\n    let callCount = 0;\n    let capturedAuthorization;\n\n    // Custom adapter to simulate a 401 challenge then a 200 success\n    axiosInstance.defaults.adapter = async (config) => {\n      callCount += 1;\n      if (callCount === 1) {\n        const error = new Error('Unauthorized');\n        error.config = config;\n        error.response = {\n          status: 401,\n          headers: {\n            'www-authenticate': 'Digest realm=\"test\", nonce=\"abc\", qop=\"auth\"'\n          }\n        };\n        throw error;\n      }\n\n      // Second call should have Authorization header set by interceptor\n      capturedAuthorization = config.headers && (config.headers.Authorization || config.headers.authorization);\n      return {\n        status: 200,\n        statusText: 'OK',\n        headers: {},\n        config,\n        data: { ok: true }\n      };\n    };\n\n    const request = {\n      method: 'GET',\n      url: 'http://example.com/resource?foo=bar&baz=qux',\n      headers: {},\n      digestConfig: { username: 'user', password: 'pass' }\n    };\n\n    addDigestInterceptor(axiosInstance, request);\n\n    const res = await axiosInstance(request);\n    expect(res.status).toEqual(200);\n\n    expect(capturedAuthorization).toBeTruthy();\n    // Extract uri=\"...\" from the header\n    const uriMatch = /uri=\"([^\"]+)\"/.exec(capturedAuthorization);\n    expect(uriMatch).toBeTruthy();\n    const uri = uriMatch[1];\n\n    // Expected to include both pathname and query\n    expect(uri).toBe('/resource?foo=bar&baz=qux');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-requests/src/auth/index.ts",
    "content": "export { addDigestInterceptor } from './digestauth-helper';\nexport { getOAuth2Token } from './oauth2-helper';\n"
  },
  {
    "path": "packages/bruno-requests/src/auth/oauth2-helper.spec.ts",
    "content": "import axios from 'axios';\nimport { getOAuth2Token, TokenStore, OAuth2Config } from './oauth2-helper';\n\n/**\n * Creates a mock token store for testing purposes.\n *\n * The token store simulates credential persistence using an in-memory Map.\n * Keys are formatted as `${url}:${credentialsId}` to uniquely identify credentials.\n */\nconst createMockTokenStore = (): TokenStore & { credentials: Map<string, any> } => {\n  const credentials = new Map<string, any>();\n  return {\n    credentials,\n    async saveCredential({ url, credentialsId, credentials: creds }) {\n      credentials.set(`${url}:${credentialsId}`, creds);\n      return true;\n    },\n    async getCredential({ url, credentialsId }) {\n      return credentials.get(`${url}:${credentialsId}`) || null;\n    },\n    async deleteCredential({ url, credentialsId }) {\n      return credentials.delete(`${url}:${credentialsId}`);\n    }\n  };\n};\n\n/**\n * Creates a mock axios adapter that intercepts HTTP requests.\n *\n * This allows tests to:\n * 1. Capture the request config (headers, body, URL) for assertion\n * 2. Return a controlled response without making actual network calls\n *\n * @param responseData - The mock response data to return (defaults to a valid token response)\n * @returns An object containing the adapter and a getter for the captured request config\n */\nconst createMockAdapter = (responseData: any = { access_token: 'test-token', expires_in: 3600 }) => {\n  let capturedConfig: any = null;\n\n  const adapter = async (config: any) => {\n    capturedConfig = config;\n    return {\n      status: 200,\n      statusText: 'OK',\n      headers: { 'content-type': 'application/json' },\n      config,\n      data: Buffer.from(JSON.stringify(responseData))\n    };\n  };\n\n  return { adapter, getCapturedConfig: () => capturedConfig };\n};\n\n/**\n * OAuth2 Client Credentials Grant Tests\n *\n * These tests verify the behavior of the OAuth2 client credentials flow,\n * specifically focusing on how client credentials (clientId and clientSecret)\n * are transmitted to the authorization server.\n *\n * OAuth2 spec allows two methods for sending client credentials:\n * 1. HTTP Basic Authentication header (RFC 6749 Section 2.3.1)\n * 2. Request body parameters (RFC 6749 Section 2.3.1)\n *\n * The `credentialsPlacement` config option controls which method is used.\n */\ndescribe('OAuth2 Helper - Client Credentials Grant', () => {\n  let originalAdapter: any;\n\n  beforeEach(() => {\n    originalAdapter = axios.defaults.adapter;\n  });\n\n  afterEach(() => {\n    axios.defaults.adapter = originalAdapter;\n  });\n\n  /**\n   * Tests for `credentialsPlacement: 'basic_auth_header'`\n   *\n   * When using Basic Auth, credentials are sent as:\n   *   Authorization: Basic base64(clientId:clientSecret)\n   *\n   * Per RFC 6749, even if clientSecret is empty, the colon separator\n   * must still be present: base64(clientId:)\n   */\n  describe('when credentialsPlacement is basic_auth_header', () => {\n    /**\n     * Verifies that when clientSecret is undefined, we still send a valid\n     * Authorization header with an empty secret (clientId:)\n     *\n     * This handles cases where a public client doesn't have a secret\n     * but the server still expects Basic Auth format.\n     */\n    test('should send token request with Authorization header when clientSecret is undefined', async () => {\n      const { adapter, getCapturedConfig } = createMockAdapter();\n      axios.defaults.adapter = adapter;\n\n      const tokenStore = createMockTokenStore();\n      const config: OAuth2Config = {\n        grantType: 'client_credentials',\n        accessTokenUrl: 'https://auth.example.com/token',\n        clientId: 'my-client-id',\n        clientSecret: undefined,\n        credentialsPlacement: 'basic_auth_header'\n      };\n\n      const token = await getOAuth2Token(config, tokenStore, '');\n\n      expect(token).toBe('test-token');\n\n      const capturedConfig = getCapturedConfig();\n      expect(capturedConfig).not.toBeNull();\n\n      // Authorization header should contain base64(clientId:) with empty secret\n      // \"my-client-id:\" encodes to \"bXktY2xpZW50LWlkOg==\"\n      const expectedAuth = `Basic ${Buffer.from('my-client-id:').toString('base64')}`;\n      expect(capturedConfig.headers['Authorization']).toBe(expectedAuth);\n\n      // grant_type must always be in the request body\n      expect(capturedConfig.data).toContain('grant_type=client_credentials');\n\n      // When using basic_auth_header, client_id should NOT be duplicated in the body\n      expect(capturedConfig.data).not.toContain('client_id=');\n    });\n\n    /**\n     * Verifies that an empty string clientSecret is treated the same as undefined.\n     *\n     * The implementation uses nullish coalescing (clientSecret ?? '') so both\n     * undefined and empty string result in the same Authorization header.\n     */\n    test('should send token request with Authorization header when clientSecret is empty string', async () => {\n      const { adapter, getCapturedConfig } = createMockAdapter();\n      axios.defaults.adapter = adapter;\n\n      const tokenStore = createMockTokenStore();\n      const config: OAuth2Config = {\n        grantType: 'client_credentials',\n        accessTokenUrl: 'https://auth.example.com/token',\n        clientId: 'my-client-id',\n        clientSecret: '',\n        credentialsPlacement: 'basic_auth_header'\n      };\n\n      const token = await getOAuth2Token(config, tokenStore, '');\n\n      expect(token).toBe('test-token');\n\n      const capturedConfig = getCapturedConfig();\n      expect(capturedConfig).not.toBeNull();\n\n      // Empty string secret should produce same result as undefined\n      const expectedAuth = `Basic ${Buffer.from('my-client-id:').toString('base64')}`;\n      expect(capturedConfig.headers['Authorization']).toBe(expectedAuth);\n    });\n\n    /**\n     * Verifies that when clientSecret is provided, it's properly included\n     * in the Authorization header.\n     */\n    test('should send token request with Authorization header when clientSecret is present', async () => {\n      const { adapter, getCapturedConfig } = createMockAdapter();\n      axios.defaults.adapter = adapter;\n\n      const tokenStore = createMockTokenStore();\n      const config: OAuth2Config = {\n        grantType: 'client_credentials',\n        accessTokenUrl: 'https://auth.example.com/token',\n        clientId: 'my-client-id',\n        clientSecret: 'my-secret',\n        credentialsPlacement: 'basic_auth_header'\n      };\n\n      const token = await getOAuth2Token(config, tokenStore, '');\n\n      expect(token).toBe('test-token');\n\n      const capturedConfig = getCapturedConfig();\n      expect(capturedConfig).not.toBeNull();\n\n      // Authorization header should contain base64(clientId:clientSecret)\n      // \"my-client-id:my-secret\" encodes to \"bXktY2xpZW50LWlkOm15LXNlY3JldA==\"\n      const expectedAuth = `Basic ${Buffer.from('my-client-id:my-secret').toString('base64')}`;\n      expect(capturedConfig.headers['Authorization']).toBe(expectedAuth);\n\n      // When using basic_auth_header, client_secret should NOT be in the body\n      expect(capturedConfig.data).not.toContain('client_secret=');\n    });\n  });\n\n  /**\n   * Tests for `credentialsPlacement: 'body'`\n   *\n   * When using body placement, credentials are sent as form parameters:\n   *   client_id=xxx&client_secret=yyy\n   *\n   * No Authorization header should be present.\n   */\n  describe('when credentialsPlacement is body', () => {\n    /**\n     * Verifies that when clientSecret is empty, only client_id is sent in the body.\n     *\n     * An empty client_secret should not be sent as it may cause issues with\n     * some authorization servers that interpret it differently than omitting it.\n     */\n    test('should send client_id in body and no Authorization header when clientSecret is empty', async () => {\n      const { adapter, getCapturedConfig } = createMockAdapter();\n      axios.defaults.adapter = adapter;\n\n      const tokenStore = createMockTokenStore();\n      const config: OAuth2Config = {\n        grantType: 'client_credentials',\n        accessTokenUrl: 'https://auth.example.com/token',\n        clientId: 'my-client-id',\n        clientSecret: '',\n        credentialsPlacement: 'body'\n      };\n\n      const token = await getOAuth2Token(config, tokenStore, '');\n\n      expect(token).toBe('test-token');\n\n      const capturedConfig = getCapturedConfig();\n      expect(capturedConfig).not.toBeNull();\n\n      // No Authorization header when using body placement\n      expect(capturedConfig.headers['Authorization']).toBeUndefined();\n\n      // client_id must be in the body\n      expect(capturedConfig.data).toContain('client_id=my-client-id');\n\n      // Empty client_secret should be omitted entirely, not sent as empty value\n      expect(capturedConfig.data).not.toContain('client_secret=');\n    });\n\n    /**\n     * Verifies that when clientSecret is provided, both client_id and\n     * client_secret are sent in the request body.\n     */\n    test('should send both client_id and client_secret in body when clientSecret is present', async () => {\n      const { adapter, getCapturedConfig } = createMockAdapter();\n      axios.defaults.adapter = adapter;\n\n      const tokenStore = createMockTokenStore();\n      const config: OAuth2Config = {\n        grantType: 'client_credentials',\n        accessTokenUrl: 'https://auth.example.com/token',\n        clientId: 'my-client-id',\n        clientSecret: 'my-secret',\n        credentialsPlacement: 'body'\n      };\n\n      const token = await getOAuth2Token(config, tokenStore, '');\n\n      expect(token).toBe('test-token');\n\n      const capturedConfig = getCapturedConfig();\n      expect(capturedConfig).not.toBeNull();\n\n      // No Authorization header when using body placement\n      expect(capturedConfig.headers['Authorization']).toBeUndefined();\n\n      // Both credentials should be in the body\n      expect(capturedConfig.data).toContain('client_id=my-client-id');\n      expect(capturedConfig.data).toContain('client_secret=my-secret');\n    });\n  });\n});\n\n/**\n * OAuth2 Password Grant Tests (Resource Owner Password Credentials)\n *\n * These tests verify the password grant flow, which includes:\n * - User credentials (username, password) always sent in the body\n * - Client credentials (clientId, clientSecret) placement configurable\n *\n * Note: Password grant is considered legacy and not recommended for new apps,\n * but many existing systems still require it.\n */\ndescribe('OAuth2 Helper - Password Grant', () => {\n  let originalAdapter: any;\n\n  beforeEach(() => {\n    originalAdapter = axios.defaults.adapter;\n  });\n\n  afterEach(() => {\n    axios.defaults.adapter = originalAdapter;\n  });\n\n  /**\n   * Tests for `credentialsPlacement: 'basic_auth_header'` with password grant\n   *\n   * Client credentials go in Authorization header, while user credentials\n   * (username, password) are always in the request body.\n   */\n  describe('when credentialsPlacement is basic_auth_header', () => {\n    /**\n     * Verifies password grant with undefined clientSecret sends proper\n     * Authorization header and includes username/password in body.\n     */\n    test('should send token request with Authorization header when clientSecret is undefined', async () => {\n      const { adapter, getCapturedConfig } = createMockAdapter();\n      axios.defaults.adapter = adapter;\n\n      const tokenStore = createMockTokenStore();\n      const config: OAuth2Config = {\n        grantType: 'password',\n        accessTokenUrl: 'https://auth.example.com/token',\n        clientId: 'my-client-id',\n        clientSecret: undefined,\n        username: 'testuser',\n        password: 'testpass',\n        credentialsPlacement: 'basic_auth_header'\n      };\n\n      const token = await getOAuth2Token(config, tokenStore, '');\n\n      expect(token).toBe('test-token');\n\n      const capturedConfig = getCapturedConfig();\n      expect(capturedConfig).not.toBeNull();\n\n      // Authorization header with empty secret\n      const expectedAuth = `Basic ${Buffer.from('my-client-id:').toString('base64')}`;\n      expect(capturedConfig.headers['Authorization']).toBe(expectedAuth);\n\n      // Password grant specific: grant_type and user credentials in body\n      expect(capturedConfig.data).toContain('grant_type=password');\n      expect(capturedConfig.data).toContain('username=testuser');\n      expect(capturedConfig.data).toContain('password=testpass');\n\n      // client_id should NOT be in body when using basic_auth_header\n      expect(capturedConfig.data).not.toContain('client_id=');\n    });\n\n    /**\n     * Verifies empty string clientSecret behaves same as undefined.\n     */\n    test('should send token request with Authorization header when clientSecret is empty string', async () => {\n      const { adapter, getCapturedConfig } = createMockAdapter();\n      axios.defaults.adapter = adapter;\n\n      const tokenStore = createMockTokenStore();\n      const config: OAuth2Config = {\n        grantType: 'password',\n        accessTokenUrl: 'https://auth.example.com/token',\n        clientId: 'my-client-id',\n        clientSecret: '',\n        username: 'testuser',\n        password: 'testpass',\n        credentialsPlacement: 'basic_auth_header'\n      };\n\n      const token = await getOAuth2Token(config, tokenStore, '');\n\n      expect(token).toBe('test-token');\n\n      const capturedConfig = getCapturedConfig();\n      expect(capturedConfig).not.toBeNull();\n\n      // Empty string treated same as undefined\n      const expectedAuth = `Basic ${Buffer.from('my-client-id:').toString('base64')}`;\n      expect(capturedConfig.headers['Authorization']).toBe(expectedAuth);\n    });\n\n    /**\n     * Verifies clientSecret is properly included in Authorization header.\n     */\n    test('should send token request with Authorization header when clientSecret is present', async () => {\n      const { adapter, getCapturedConfig } = createMockAdapter();\n      axios.defaults.adapter = adapter;\n\n      const tokenStore = createMockTokenStore();\n      const config: OAuth2Config = {\n        grantType: 'password',\n        accessTokenUrl: 'https://auth.example.com/token',\n        clientId: 'my-client-id',\n        clientSecret: 'my-secret',\n        username: 'testuser',\n        password: 'testpass',\n        credentialsPlacement: 'basic_auth_header'\n      };\n\n      const token = await getOAuth2Token(config, tokenStore, '');\n\n      expect(token).toBe('test-token');\n\n      const capturedConfig = getCapturedConfig();\n      expect(capturedConfig).not.toBeNull();\n\n      // Full credentials in Authorization header\n      const expectedAuth = `Basic ${Buffer.from('my-client-id:my-secret').toString('base64')}`;\n      expect(capturedConfig.headers['Authorization']).toBe(expectedAuth);\n\n      // client_secret should NOT be duplicated in body\n      expect(capturedConfig.data).not.toContain('client_secret=');\n    });\n  });\n\n  /**\n   * Tests for `credentialsPlacement: 'body'` with password grant\n   *\n   * Both client credentials and user credentials are sent in the request body.\n   */\n  describe('when credentialsPlacement is body', () => {\n    /**\n     * Verifies password grant with empty clientSecret sends client_id\n     * but omits client_secret from the body.\n     */\n    test('should send client_id in body and no Authorization header when clientSecret is empty', async () => {\n      const { adapter, getCapturedConfig } = createMockAdapter();\n      axios.defaults.adapter = adapter;\n\n      const tokenStore = createMockTokenStore();\n      const config: OAuth2Config = {\n        grantType: 'password',\n        accessTokenUrl: 'https://auth.example.com/token',\n        clientId: 'my-client-id',\n        clientSecret: '',\n        username: 'testuser',\n        password: 'testpass',\n        credentialsPlacement: 'body'\n      };\n\n      const token = await getOAuth2Token(config, tokenStore, '');\n\n      expect(token).toBe('test-token');\n\n      const capturedConfig = getCapturedConfig();\n      expect(capturedConfig).not.toBeNull();\n\n      // No Authorization header\n      expect(capturedConfig.headers['Authorization']).toBeUndefined();\n\n      // client_id in body, but not empty client_secret\n      expect(capturedConfig.data).toContain('client_id=my-client-id');\n      expect(capturedConfig.data).not.toContain('client_secret=');\n    });\n\n    /**\n     * Verifies password grant with clientSecret sends all credentials in body.\n     */\n    test('should send both client_id and client_secret in body when clientSecret is present', async () => {\n      const { adapter, getCapturedConfig } = createMockAdapter();\n      axios.defaults.adapter = adapter;\n\n      const tokenStore = createMockTokenStore();\n      const config: OAuth2Config = {\n        grantType: 'password',\n        accessTokenUrl: 'https://auth.example.com/token',\n        clientId: 'my-client-id',\n        clientSecret: 'my-secret',\n        username: 'testuser',\n        password: 'testpass',\n        credentialsPlacement: 'body'\n      };\n\n      const token = await getOAuth2Token(config, tokenStore, '');\n\n      expect(token).toBe('test-token');\n\n      const capturedConfig = getCapturedConfig();\n      expect(capturedConfig).not.toBeNull();\n\n      // No Authorization header\n      expect(capturedConfig.headers['Authorization']).toBeUndefined();\n\n      // All credentials in body\n      expect(capturedConfig.data).toContain('client_id=my-client-id');\n      expect(capturedConfig.data).toContain('client_secret=my-secret');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-requests/src/auth/oauth2-helper.ts",
    "content": "import axios, { AxiosInstance, AxiosRequestConfig, ResponseType } from 'axios';\nimport qs from 'qs';\nimport debug from 'debug';\n\nexport interface TokenStore {\n  saveCredential({ url, credentialsId, credentials }: { url: string; credentialsId: string; credentials: any }): Promise<boolean>;\n  getCredential({ url, credentialsId }: { url: string; credentialsId: string }): Promise<any>;\n  deleteCredential({ url, credentialsId }: { url: string; credentialsId: string }): Promise<boolean>;\n}\n\nexport interface AdditionalParameter {\n  name: string;\n  value: string;\n  enabled: boolean;\n  sendIn: 'headers' | 'queryparams' | 'body';\n}\n\nexport interface OAuth2Config {\n  grantType: 'client_credentials' | 'password';\n  accessTokenUrl: string;\n  clientId?: string;\n  clientSecret?: string;\n  username?: string;\n  password?: string;\n  scope?: string;\n  credentialsPlacement?: 'basic_auth_header' | 'body';\n  credentialsId?: string;\n  autoRefreshToken?: boolean;\n  autoFetchToken?: boolean;\n  tokenSource?: 'access_token' | 'id_token';\n  additionalParameters?: {\n    token?: AdditionalParameter[];\n  };\n}\n\ninterface RequestConfig extends AxiosRequestConfig {\n  method: string;\n  url: string;\n  headers: {\n    'Content-Type': string;\n    'Authorization'?: string;\n    [key: string]: any;\n  };\n  data: string;\n  responseType: ResponseType;\n}\n\ninterface ClientCredentialsData {\n  grant_type: string;\n  scope?: string;\n  client_id?: string;\n  client_secret?: string;\n  [key: string]: any; // For additional parameters\n}\n\ninterface PasswordGrantData {\n  grant_type: string;\n  username: string;\n  password: string;\n  scope?: string;\n  client_id?: string;\n  client_secret?: string;\n  [key: string]: any; // For additional parameters\n}\n\n/**\n * Apply additional parameters to a request\n */\nconst applyAdditionalParameters = (requestConfig: RequestConfig, data: any, params: AdditionalParameter[] = []) => {\n  params.forEach((param) => {\n    if (!param.enabled || !param.name) {\n      return;\n    }\n\n    switch (param.sendIn) {\n      case 'headers':\n        requestConfig.headers[param.name] = param.value || '';\n        break;\n      case 'queryparams':\n        // For query params, add to URL\n        try {\n          const url = new URL(requestConfig.url);\n          url.searchParams.append(param.name, param.value || '');\n          requestConfig.url = url.href;\n        } catch (error) {\n          throw new Error(`Invalid token URL: ${requestConfig.url}`);\n        }\n        break;\n      case 'body':\n        // For body, add to data object\n        data[param.name] = param.value || '';\n        break;\n    }\n  });\n};\n\n/**\n * Safely parse JSON response data\n */\nconst safeParseJSONBuffer = (data: any) => {\n  try {\n    return JSON.parse(Buffer.isBuffer(data) ? data.toString() : data);\n  } catch {\n    return data;\n  }\n};\n\n/**\n * Fetches an OAuth2 token using client credentials grant\n */\nconst fetchTokenClientCredentials = async (oauth2Config: OAuth2Config, axiosInstance?: AxiosInstance) => {\n  const {\n    accessTokenUrl,\n    clientId,\n    clientSecret,\n    scope,\n    credentialsPlacement = 'basic_auth_header',\n    additionalParameters\n  } = oauth2Config;\n\n  if (!accessTokenUrl) {\n    throw new Error('Access Token URL is required for OAuth2 client credentials flow');\n  }\n\n  if (!clientId) {\n    throw new Error('Client ID is required for OAuth2 client credentials flow');\n  }\n\n  const requestConfig: RequestConfig = {\n    method: 'POST',\n    url: accessTokenUrl,\n    headers: {\n      'Content-Type': 'application/x-www-form-urlencoded',\n      'Accept': 'application/json'\n    },\n    data: '',\n    responseType: 'arraybuffer'\n  };\n\n  const data: ClientCredentialsData = {\n    grant_type: 'client_credentials'\n  };\n\n  if (scope && scope.trim() !== '') {\n    data.scope = scope;\n  }\n\n  if (credentialsPlacement === 'basic_auth_header') {\n    const secret = clientSecret ?? '';\n    requestConfig.headers['Authorization'] = `Basic ${Buffer.from(`${clientId}:${secret}`).toString('base64')}`;\n  }\n\n  if (credentialsPlacement !== 'basic_auth_header') {\n    data.client_id = clientId;\n  }\n\n  if (clientSecret && clientSecret.trim() !== '' && credentialsPlacement !== 'basic_auth_header') {\n    data.client_secret = clientSecret;\n  }\n\n  if (additionalParameters?.token?.length) {\n    applyAdditionalParameters(requestConfig, data, additionalParameters.token);\n  }\n\n  requestConfig.data = qs.stringify(data);\n\n  debug('oauth2')('> request');\n  debug('oauth2')(JSON.stringify(requestConfig, null, 2));\n\n  try {\n    const httpClient = axiosInstance || axios;\n    const response = await httpClient(requestConfig);\n    const parsedData = safeParseJSONBuffer(response.data);\n\n    if (parsedData && typeof parsedData === 'object') {\n      parsedData.created_at = Date.now();\n    }\n\n    debug('oauth2')('> response');\n    debug('oauth2')(JSON.stringify(parsedData, null, 2));\n    return parsedData;\n  } catch (err: any) {\n    if (err?.response) {\n      debug('oauth2')('< error');\n      debug('oauth2')(JSON.stringify({\n        status: err.response.status,\n        statusText: err.response.statusText,\n        data: err.response.data ? safeParseJSONBuffer(err.response.data) : null,\n        headers: err.response.headers\n      }, null, 2));\n    } else {\n      debug('oauth2')('< error');\n      debug('oauth2')(err.message || err);\n    }\n    throw err;\n  }\n};\n\n/**\n * Fetches an OAuth2 token using password grant\n */\nconst fetchTokenPassword = async (oauth2Config: OAuth2Config, axiosInstance?: AxiosInstance) => {\n  const {\n    accessTokenUrl,\n    clientId,\n    clientSecret,\n    username,\n    password,\n    scope,\n    credentialsPlacement = 'basic_auth_header',\n    additionalParameters\n  } = oauth2Config;\n\n  if (!accessTokenUrl) {\n    throw new Error('Access Token URL is required for OAuth2 password credentials flow');\n  }\n\n  if (!username) {\n    throw new Error('Username is required for OAuth2 password credentials flow');\n  }\n\n  if (!password) {\n    throw new Error('Password is required for OAuth2 password credentials flow');\n  }\n\n  if (!clientId) {\n    throw new Error('Client ID is required for OAuth2 password credentials flow');\n  }\n\n  const requestConfig: RequestConfig = {\n    method: 'POST',\n    url: accessTokenUrl,\n    headers: {\n      'Content-Type': 'application/x-www-form-urlencoded',\n      'Accept': 'application/json'\n    },\n    data: '',\n    responseType: 'arraybuffer'\n  };\n\n  const data: PasswordGrantData = {\n    grant_type: 'password',\n    username,\n    password\n  };\n\n  if (scope && scope.trim() !== '') {\n    data.scope = scope;\n  }\n\n  if (credentialsPlacement === 'basic_auth_header') {\n    const secret = clientSecret ?? '';\n    requestConfig.headers['Authorization'] = `Basic ${Buffer.from(`${clientId}:${secret}`).toString('base64')}`;\n  }\n\n  if (credentialsPlacement !== 'basic_auth_header') {\n    data.client_id = clientId;\n  }\n\n  if (clientSecret && clientSecret.trim() !== '' && credentialsPlacement !== 'basic_auth_header') {\n    data.client_secret = clientSecret;\n  }\n\n  if (additionalParameters?.token?.length) {\n    applyAdditionalParameters(requestConfig, data, additionalParameters.token);\n  }\n\n  requestConfig.data = qs.stringify(data);\n\n  debug('oauth2')('> request');\n  debug('oauth2')(JSON.stringify(requestConfig, null, 2));\n\n  try {\n    const httpClient = axiosInstance || axios;\n    const response = await httpClient(requestConfig);\n    const parsedData = safeParseJSONBuffer(response.data);\n\n    if (parsedData && typeof parsedData === 'object') {\n      parsedData.created_at = Date.now();\n    }\n\n    debug('oauth2')('< response');\n    debug('oauth2')(JSON.stringify(parsedData, null, 2));\n    return parsedData;\n  } catch (err: any) {\n    if (err?.response) {\n      debug('oauth2')('< error');\n      debug('oauth2')(JSON.stringify({\n        status: err.response.status,\n        statusText: err.response.statusText,\n        data: err.response.data ? safeParseJSONBuffer(err.response.data) : null,\n        headers: err.response.headers\n      }, null, 2));\n    } else {\n      debug('oauth2')('< error');\n      debug('oauth2')(err.message || err);\n    }\n    throw err;\n  }\n};\n\n/**\n * Check if a token is expired\n */\nconst isTokenExpired = (credentials: any): boolean => {\n  if (!credentials?.access_token) {\n    return true;\n  }\n  if (!credentials?.expires_in || !credentials.created_at) {\n    return false; // No expiration info, assume valid\n  }\n  const expiryTime = credentials.created_at + credentials.expires_in * 1000;\n  return Date.now() > expiryTime;\n};\n\n/**\n * Manages OAuth2 token retrieval and storage\n */\nexport const getOAuth2Token = async (oauth2Config: OAuth2Config, tokenStore: TokenStore, verbose: string, axiosInstance?: AxiosInstance): Promise<string | null> => {\n  const {\n    grantType,\n    accessTokenUrl,\n    credentialsId = 'default',\n    autoFetchToken = true,\n    tokenSource = 'access_token'\n  } = oauth2Config;\n\n  if (verbose) {\n    debug.enable('oauth2');\n  }\n\n  if (!grantType) {\n    throw new Error('Grant type is required for OAuth2');\n  }\n\n  if (!accessTokenUrl) {\n    throw new Error('Access token URL is required for OAuth2');\n  }\n\n  if (!['client_credentials', 'password'].includes(grantType)) {\n    throw new Error(`Unsupported grant type: ${grantType}. Supported types: client_credentials, password`);\n  }\n\n  // Check if we already have credentials stored\n  const existingToken = await tokenStore.getCredential({ url: accessTokenUrl, credentialsId });\n\n  if (existingToken) {\n    // Check if token is expired\n    if (!isTokenExpired(existingToken)) {\n      // Token is valid, use it\n      return tokenSource === 'id_token' ? existingToken.id_token : existingToken.access_token;\n    } else {\n      // Token is expired\n      if (autoFetchToken) {\n        // Clear expired token and proceed to fetch new token\n        await tokenStore.deleteCredential({ url: accessTokenUrl, credentialsId });\n      } else {\n        // Return expired token if autoFetchToken is disabled\n        return tokenSource === 'id_token' ? existingToken.id_token : existingToken.access_token;\n      }\n    }\n  } else {\n    // No stored credentials\n    if (!autoFetchToken) {\n      // Don't fetch token if autoFetchToken is disabled\n      return null;\n    }\n    // Otherwise, proceed to fetch new token\n  }\n\n  let tokenResponse;\n\n  if (grantType === 'client_credentials') {\n    tokenResponse = await fetchTokenClientCredentials(oauth2Config, axiosInstance);\n  } else if (grantType === 'password') {\n    tokenResponse = await fetchTokenPassword(oauth2Config, axiosInstance);\n  } else {\n    throw new Error(`Unsupported grant type: ${grantType}`);\n  }\n\n  if (tokenResponse.error) {\n    throw new Error(JSON.stringify(tokenResponse));\n  }\n\n  if (!tokenResponse || !tokenResponse.access_token) {\n    throw new Error('No access token received from server');\n  }\n\n  if (tokenResponse.expires_in && tokenResponse.created_at) {\n    tokenResponse.expires_at = tokenResponse.created_at + tokenResponse.expires_in * 1000;\n  }\n\n  const saved = await tokenStore.saveCredential({ url: accessTokenUrl, credentialsId, credentials: tokenResponse });\n  if (!saved) {\n    console.warn('OAuth2: Failed to save token to store, but proceeding with token');\n  }\n\n  return tokenSource === 'id_token' ? tokenResponse.id_token : tokenResponse.access_token;\n};\n"
  },
  {
    "path": "packages/bruno-requests/src/cookies/index.spec.ts",
    "content": "import cookiesModule from './index';\nimport { Cookie } from 'tough-cookie';\n\n// Provide explicit type for the cookie-jar wrapper returned by cookiesModule.jar()\ntype CookieJarWrapper = ReturnType<typeof cookiesModule.jar>;\n\nconst jarFactory = (): CookieJarWrapper => cookiesModule.jar();\n\ndescribe('Bruno Cookie Jar Wrapper - API Examples', () => {\n  let jar: CookieJarWrapper;\n  const testUrl = 'https://api.example.com';\n\n  beforeEach(() => {\n    jar = jarFactory();\n    // Clear all cookies before each test\n    jar.clear();\n  });\n\n  describe('Basic Cookie Operations', () => {\n    test('setCookie and getCookie - name/value pair', async () => {\n      const cookieName = 'authToken';\n      const cookieValue = 'jwt123';\n\n      // Set a cookie\n      await jar.setCookie(testUrl, cookieName, cookieValue);\n\n      // Get the cookie back\n      const cookie = (await jar.getCookie(testUrl, cookieName))!;\n      expect(cookie.key).toBe(cookieName);\n      expect(cookie.value).toBe(cookieValue);\n      expect(cookie.domain).toBe('api.example.com');\n    });\n\n    test('setCookie with cookie object', async () => {\n      const cookieObj = {\n        key: 'sessionId',\n        value: 'abc123',\n        path: '/api',\n        httpOnly: true,\n        secure: true\n      };\n\n      await jar.setCookie(testUrl, cookieObj);\n\n      const cookie = (await jar.getCookie(testUrl + '/api', 'sessionId'))!;\n      expect(cookie.key).toBe('sessionId');\n      expect(cookie.value).toBe('abc123');\n      expect(cookie.path).toBe('/api');\n      expect(cookie.httpOnly).toBe(true);\n      expect(cookie.secure).toBe(true);\n    });\n\n    test('getCookie returns null for non-existent cookie', async () => {\n      const cookie = await jar.getCookie(testUrl, 'nonexistent');\n      expect(cookie).toBeNull();\n    });\n  });\n\n  describe('Multiple Cookie Operations', () => {\n    test('setCookies with array of cookie objects', async () => {\n      const cookies = [\n        { key: 'cookie1', value: 'value1' },\n        { key: 'cookie2', value: 'value2' },\n        { key: 'cookie3', value: 'value3', httpOnly: true }\n      ];\n\n      await jar.setCookies(testUrl, cookies);\n\n      // Verify all cookies were set\n      const retrievedCookies = (await jar.getCookies(testUrl)) as Cookie[];\n      expect(retrievedCookies).toHaveLength(3);\n\n      const cookieNames = retrievedCookies.map((c: Cookie) => c.key);\n      expect(cookieNames).toContain('cookie1');\n      expect(cookieNames).toContain('cookie2');\n      expect(cookieNames).toContain('cookie3');\n    });\n\n    test('getCookies returns all cookies for URL', async () => {\n      // Set multiple cookies\n      await jar.setCookie(testUrl, 'auth', 'token123');\n      await jar.setCookie(testUrl, 'session', 'sess456');\n      await jar.setCookie(testUrl, 'prefs', 'theme=dark');\n\n      const cookies = (await jar.getCookies(testUrl)) as Cookie[];\n      expect(cookies).toHaveLength(3);\n\n      const cookieMap = (cookies as Cookie[]).reduce<Record<string, string>>((map, cookie: Cookie) => {\n        map[cookie.key] = cookie.value;\n        return map;\n      }, {} as Record<string, string>);\n\n      expect(cookieMap.auth).toBe('token123');\n      expect(cookieMap.session).toBe('sess456');\n      expect(cookieMap.prefs).toBe('theme=dark');\n    });\n  });\n\n  describe('Cookie Deletion', () => {\n    test('deleteCookie removes specific cookie', async () => {\n      // Set two cookies\n      await jar.setCookie(testUrl, 'keep', 'keepValue');\n      await jar.setCookie(testUrl, 'remove', 'removeValue');\n\n      // Delete one cookie\n      await jar.deleteCookie(testUrl, 'remove');\n\n      // Verify only one cookie remains\n      const cookies = (await jar.getCookies(testUrl)) as Cookie[];\n      expect(cookies).toHaveLength(1);\n      expect(cookies[0]!.key).toBe('keep');\n      expect(cookies[0]!.value).toBe('keepValue');\n    });\n\n    test('deleteCookies removes all cookies for URL', async () => {\n      // Set multiple cookies\n      await jar.setCookie(testUrl, 'cookie1', 'value1');\n      await jar.setCookie(testUrl, 'cookie2', 'value2');\n\n      // Delete all cookies for the URL\n      await jar.deleteCookies(testUrl);\n\n      // Verify no cookies remain\n      const cookies = (await jar.getCookies(testUrl)) as Cookie[];\n      expect(cookies).toHaveLength(0);\n    });\n\n    test('clear removes all cookies from jar', async () => {\n      // Set cookies for multiple URLs\n      await jar.setCookie('https://site1.com', 'cookie1', 'value1');\n      await jar.setCookie('https://site2.com', 'cookie2', 'value2');\n\n      // Clear entire jar\n      await jar.clear();\n\n      // Verify no cookies remain for any URL\n      const cookies1 = (await jar.getCookies('https://site1.com')) as Cookie[];\n      const cookies2 = (await jar.getCookies('https://site2.com')) as Cookie[];\n\n      expect(cookies1).toHaveLength(0);\n      expect(cookies2).toHaveLength(0);\n    });\n  });\n\n  describe('hasCookie', () => {\n    test('hasCookie returns true for existing cookie', async () => {\n      await jar.setCookie(testUrl, 'authToken', 'jwt123');\n      const exists = await jar.hasCookie(testUrl, 'authToken');\n      expect(exists).toBe(true);\n    });\n\n    test('hasCookie returns false for non-existent cookie', async () => {\n      const exists = await jar.hasCookie(testUrl, 'nonexistent');\n      expect(exists).toBe(false);\n    });\n  });\n\n  describe('Callback mode does not return a Promise', () => {\n    // tough-cookie's createPromiseCallback() returns a never-resolving Promise when\n    // a callback is provided. The wrapper must NOT propagate that Promise, otherwise\n    // `await jar.getCookie(url, name, cb)` hangs forever (blocks the Node VM runner).\n    test('getCookie with callback returns void, not a Promise', () => {\n      jar.setCookie(testUrl, 'x', '1');\n      const ret = jar.getCookie(testUrl, 'x', () => {});\n      expect(ret).toBeUndefined();\n    });\n\n    test('getCookies with callback returns void, not a Promise', () => {\n      const ret = jar.getCookies(testUrl, () => {});\n      expect(ret).toBeUndefined();\n    });\n\n    test('hasCookie with callback returns void, not a Promise', () => {\n      const ret = jar.hasCookie(testUrl, 'x', () => {});\n      expect(ret).toBeUndefined();\n    });\n\n    test('clear with callback returns void, not a Promise', () => {\n      const ret = jar.clear(() => {});\n      expect(ret).toBeUndefined();\n    });\n\n    test('deleteCookies with callback returns void, not a Promise', () => {\n      const ret = jar.deleteCookies(testUrl, () => {});\n      expect(ret).toBeUndefined();\n    });\n\n    test('deleteCookie with callback returns void, not a Promise', () => {\n      const ret = jar.deleteCookie(testUrl, 'x', () => {});\n      expect(ret).toBeUndefined();\n    });\n\n    test('validation-error paths with callback return void, not the callback return value', () => {\n      // If the wrapper did `return callback(error)`, the caller would receive\n      // whatever the callback returns — which could be a truthy value or a Promise.\n      // All validation-error callback paths must return void (undefined).\n      const spy = () => 'leaked!' as any;\n\n      expect(jar.getCookie('', '', spy)).toBeUndefined();\n      expect(jar.getCookies('', spy)).toBeUndefined();\n      expect(jar.hasCookie('', '', spy)).toBeUndefined();\n      expect(jar.deleteCookies('', spy)).toBeUndefined();\n      expect(jar.deleteCookie('', '', spy)).toBeUndefined();\n    });\n\n    test('getCookie with callback can be safely awaited without hanging', async () => {\n      await jar.setCookie(testUrl, 'token', 'abc');\n\n      let callbackData: any = null;\n      // This would hang forever before the fix if getCookie returned tough-cookie's Promise\n      await jar.getCookie(testUrl, 'token', (_err, cookie) => {\n        callbackData = cookie;\n      });\n\n      expect(callbackData).not.toBeNull();\n      expect(callbackData.key).toBe('token');\n      expect(callbackData.value).toBe('abc');\n    });\n  });\n\n  describe('Error Handling', () => {\n    test('setCookie handles missing URL', async () => {\n      await expect(jar.setCookie('', 'name', 'value')).rejects.toThrow('URL is required');\n    });\n\n    test('getCookie handles missing URL', async () => {\n      await expect(jar.getCookie('', 'name')).rejects.toThrow('URL and cookie name are required');\n    });\n\n    test('setCookies handles invalid input', async () => {\n      await expect(jar.setCookies(testUrl, 'not-an-array' as any)).rejects.toThrow('expects an array');\n    });\n\n    test('setCookie handles missing cookie name in object', async () => {\n      await expect(jar.setCookie(testUrl, { value: 'test' })).rejects.toThrow('key (name) is required');\n    });\n  });\n\n  describe('Real-world Usage Examples', () => {\n    test('Authentication workflow example', async () => {\n      const apiUrl = 'https://api.example.com';\n      const authToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';\n\n      // Simulate login - set auth cookie\n      await jar.setCookie(apiUrl, 'authToken', authToken);\n\n      // Later in the session - retrieve auth token\n      const cookie = (await jar.getCookie(apiUrl, 'authToken'))!;\n      expect(cookie.value).toBe(authToken);\n\n      // Simulate logout - remove auth cookie\n      await jar.deleteCookie(apiUrl, 'authToken');\n\n      // Verify cookie is gone\n      const deletedCookie = await jar.getCookie(apiUrl, 'authToken');\n      expect(deletedCookie).toBeNull();\n    });\n\n    test('Session management with multiple cookies', async () => {\n      const sessionUrl = 'https://app.example.com';\n\n      // Set session cookies\n      const sessionCookies = [\n        { key: 'sessionId', value: 'sess_123', httpOnly: true },\n        { key: 'csrfToken', value: 'csrf_456' },\n        { key: 'userPrefs', value: JSON.stringify({ theme: 'dark', lang: 'en' }) }\n      ];\n\n      await jar.setCookies(sessionUrl, sessionCookies);\n\n      // Retrieve all session cookies\n      const cookies = (await jar.getCookies(sessionUrl)) as Cookie[];\n      expect(cookies).toHaveLength(3);\n\n      // Find specific cookies\n      const sessionCookie = cookies.find((c: Cookie) => c.key === 'sessionId')!;\n      const csrfCookie = cookies.find((c: Cookie) => c.key === 'csrfToken')!;\n      const prefsCookie = cookies.find((c: Cookie) => c.key === 'userPrefs')!;\n\n      expect(sessionCookie.value).toBe('sess_123');\n      expect(sessionCookie.httpOnly).toBe(true);\n      expect(csrfCookie.value).toBe('csrf_456');\n\n      const prefs = JSON.parse(prefsCookie.value);\n      expect(prefs.theme).toBe('dark');\n      expect(prefs.lang).toBe('en');\n    });\n\n    test('Cookie path handling', async () => {\n      const baseUrl = 'https://example.com';\n\n      // Set cookies with different paths\n      await jar.setCookie(baseUrl, { key: 'global', value: 'global_val', path: '/' });\n      await jar.setCookie(baseUrl, { key: 'api', value: 'api_val', path: '/api' });\n      await jar.setCookie(baseUrl, { key: 'admin', value: 'admin_val', path: '/admin' });\n\n      const rootCookies = (await jar.getCookies(baseUrl + '/')) as Cookie[];\n      const globalCookie = rootCookies.find((c: Cookie) => c.key === 'global')!;\n      expect(globalCookie).toBeTruthy();\n      expect(globalCookie.value).toBe('global_val');\n\n      const apiCookies = (await jar.getCookies(baseUrl + '/api/users')) as Cookie[];\n      expect(apiCookies.length).toBeGreaterThanOrEqual(2);\n\n      const apiCookieNames = apiCookies.map((c: Cookie) => c.key);\n      expect(apiCookieNames).toContain('global');\n      expect(apiCookieNames).toContain('api');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-requests/src/cookies/index.ts",
    "content": "import { Cookie, CookieJar } from 'tough-cookie';\nimport each from 'lodash/each';\nimport moment from 'moment';\nimport { isPotentiallyTrustworthyOrigin } from '../utils/url-validation';\n\nconst cookieJar = new CookieJar();\n\nconst addCookieToJar = (setCookieHeader: string, requestUrl: string): void => {\n  const cookie = Cookie.parse(setCookieHeader, { loose: true });\n  if (!cookie) return;\n  cookieJar.setCookieSync(cookie, requestUrl, {\n    ignoreError: true\n  });\n};\n\nconst getCookiesForUrl = (url: string) => {\n  return cookieJar.getCookiesSync(url, {\n    secure: isPotentiallyTrustworthyOrigin(url)\n  } as any);\n};\n\nconst getCookieStringForUrl = (url: string): string => {\n  const cookies = getCookiesForUrl(url);\n  if (!Array.isArray(cookies) || !cookies.length) return '';\n\n  const validCookies = cookies.filter((cookie: any) => !cookie.expires || (cookie.expires as any) > Date.now());\n  return validCookies.map((cookie) => cookie.cookieString()).join('; ');\n};\n\nconst getDomainsWithCookies = (): Promise<Array<{ domain: string; cookies: Cookie[]; cookieString: string }>> => {\n  return new Promise((resolve, reject) => {\n    const domainCookieMap: Record<string, Cookie[]> = {};\n\n    (cookieJar as any).store.getAllCookies((err: Error, cookies: Cookie[]) => {\n      if (err) return reject(err);\n\n      cookies.forEach((cookie) => {\n        // Handle null domain by skipping the cookie\n        if (!cookie.domain) return;\n\n        if (!domainCookieMap[cookie.domain]) {\n          domainCookieMap[cookie.domain] = [cookie];\n        } else {\n          domainCookieMap[cookie.domain].push(cookie);\n        }\n      });\n\n      const domains = Object.keys(domainCookieMap);\n      const domainsWithCookies: Array<{ domain: string; cookies: Cookie[]; cookieString: string }> = [];\n\n      each(domains, (domain) => {\n        const cookiesForDomain = domainCookieMap[domain];\n        const validCookies = cookiesForDomain.filter((cookie: any) => !cookie.expires || (cookie.expires as any) > Date.now());\n\n        if (validCookies.length) {\n          domainsWithCookies.push({\n            domain,\n            cookies: validCookies,\n            cookieString: validCookies.map((cookie) => cookie.cookieString()).join('; ')\n          });\n        }\n      });\n\n      resolve(domainsWithCookies);\n    });\n  });\n};\n\nconst deleteCookie = (domain: string, path: string, cookieKey: string): Promise<void> => {\n  return new Promise((resolve, reject) => {\n    (cookieJar as any).store.removeCookie(domain, path, cookieKey, (err: Error) => {\n      if (err) return reject(err);\n      resolve();\n    });\n  });\n};\n\nconst deleteCookiesForDomain = (domain: string): Promise<void> => {\n  return new Promise((resolve, reject) => {\n    (cookieJar as any).store.removeCookies(domain, null, (err: Error) => {\n      if (err) return reject(err);\n      resolve();\n    });\n  });\n};\n\nconst updateCookieObj = (cookieObj: any, oldCookie: Cookie) => {\n  return {\n    ...cookieObj,\n    path: oldCookie.path,\n    key: oldCookie.key,\n    domain: oldCookie.domain,\n    expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity,\n    creation: oldCookie?.creation && moment(oldCookie.creation).isValid() ? new Date(oldCookie.creation) : new Date(),\n    lastAccessed:\n      oldCookie?.lastAccessed && moment(oldCookie.lastAccessed).isValid()\n        ? new Date(oldCookie.lastAccessed)\n        : new Date()\n  } as any;\n};\n\nconst createCookieObj = (cookieObj: any) => {\n  return {\n    ...cookieObj,\n    path: cookieObj.path,\n    expires: cookieObj?.expires && moment(cookieObj.expires).isValid() ? new Date(cookieObj.expires) : Infinity,\n    creation: cookieObj?.creation && moment(cookieObj.creation).isValid() ? new Date(cookieObj.creation) : new Date(),\n    lastAccessed:\n      cookieObj?.lastAccessed && moment(cookieObj.lastAccessed).isValid()\n        ? new Date(cookieObj.lastAccessed)\n        : new Date()\n  } as any;\n};\n\nconst addCookieForDomain = (domain: string, cookieObj: any): Promise<void> => {\n  return new Promise((resolve, reject) => {\n    try {\n      const cookie = new Cookie(createCookieObj(cookieObj));\n      (cookieJar as any).store.putCookie(cookie, (err: Error) => {\n        if (err) return reject(err);\n        resolve();\n      });\n    } catch (err) {\n      reject(err);\n    }\n  });\n};\n\nconst modifyCookieForDomain = (domain: string, oldCookieObj: any, cookieObj: any): Promise<void> => {\n  return new Promise((resolve, reject) => {\n    try {\n      const oldCookie = new Cookie(createCookieObj(oldCookieObj));\n      const newCookie = new Cookie(updateCookieObj(cookieObj, oldCookie));\n      (cookieJar as any).store.updateCookie(oldCookie, newCookie, (removeErr: Error) => {\n        if (removeErr) return reject(removeErr);\n        resolve();\n      });\n    } catch (err) {\n      reject(err);\n    }\n  });\n};\n\nconst parseCookieString = (cookieStr: string): any | null => {\n  try {\n    const cookie = Cookie.parse(cookieStr);\n    if (!cookie) return null;\n    return {\n      ...cookie,\n      expires: cookie.expires === 'Infinity' || (cookie.expires as any) === Infinity ? null : cookie.expires\n    };\n  } catch (err) {\n    throw err;\n  }\n};\n\nconst createCookieString = (cookieObj: any): string => {\n  const cookie = new Cookie(createCookieObj(cookieObj));\n  let cookieString = cookie.toString(); // tough-cookie omits domain\n\n  // Manually append domain if cookie is hostOnly but we still want Domain flag\n  if (cookieObj.hostOnly && !cookieString.includes('Domain=')) {\n    cookieString += `; Domain=${cookieObj.domain}`;\n  }\n  return cookieString;\n};\n\nconst saveCookies = (url: string, headers: any) => {\n  if (headers['set-cookie']) {\n    let setCookieHeaders = Array.isArray(headers['set-cookie'])\n      ? headers['set-cookie']\n      : [headers['set-cookie']];\n    for (let setCookieHeader of setCookieHeaders) {\n      if (typeof setCookieHeader === 'string' && setCookieHeader.length) {\n        addCookieToJar(setCookieHeader, url);\n      }\n    }\n  }\n};\n\nconst cookieJarWrapper = () => {\n  return {\n\n    // Get the full cookie object for the given URL & name.\n    getCookie: function (\n      url: string,\n      cookieName: string,\n      callback?: (err: Error | null | undefined, cookie?: Cookie | null) => void\n    ) {\n      if (!url || !cookieName) {\n        const error = new Error('URL and cookie name are required');\n        if (callback) {\n          callback(error); return;\n        }\n        return Promise.reject(error);\n      }\n\n      if (callback) {\n        // Callback mode – do NOT return the value from cookieJar.getCookies() because\n        // tough-cookie returns a never-resolving Promise when a callback is provided.\n        // Returning void ensures `await` on a callback-style call resolves immediately.\n        cookieJar.getCookies(url, (err: Error | null, cookies?: Cookie[]) => {\n          if (err) return callback(err);\n          const cookieList = cookies || [];\n          const cookie = cookieList.find((c) => c.key === cookieName);\n          callback(null, cookie || null);\n        });\n        return;\n      }\n\n      // Promise mode\n      return new Promise<Cookie | null>((resolve, reject) => {\n        cookieJar.getCookies(url, (err: Error | null, cookies?: Cookie[]) => {\n          if (err) return reject(err);\n          const cookieList = cookies || [];\n          const cookie = cookieList.find((c) => c.key === cookieName);\n          resolve(cookie || null);\n        });\n      });\n    },\n\n    // Check whether a cookie with the given name exists for the URL.\n    hasCookie: function (\n      url: string,\n      cookieName: string,\n      callback?: (err: Error | null | undefined, exists?: boolean) => void\n    ) {\n      if (!url || !cookieName) {\n        const error = new Error('URL and cookie name are required');\n        if (callback) {\n          callback(error); return;\n        }\n        return Promise.reject(error);\n      }\n\n      if (callback) {\n        cookieJar.getCookies(url, (err: Error | null, cookies?: Cookie[]) => {\n          if (err) return callback(err);\n          const cookieList = cookies || [];\n          callback(null, cookieList.some((c) => c.key === cookieName));\n        });\n        return;\n      }\n\n      return new Promise<boolean>((resolve, reject) => {\n        cookieJar.getCookies(url, (err: Error | null, cookies?: Cookie[]) => {\n          if (err) return reject(err);\n          const cookieList = cookies || [];\n          resolve(cookieList.some((c) => c.key === cookieName));\n        });\n      });\n    },\n\n    // Get all cookies that would be sent to the given URL.\n    getCookies: function (url: string, callback?: (err: Error | null | undefined, cookies?: Cookie[]) => void) {\n      if (!url) {\n        const error = new Error('URL is required');\n        if (callback) {\n          callback(error); return;\n        }\n        return Promise.reject(error);\n      }\n\n      if (callback) {\n        // Callback mode\n        cookieJar.getCookies(url, callback as any);\n        return;\n      }\n\n      // Promise mode\n      return new Promise<Cookie[]>((resolve, reject) => {\n        cookieJar.getCookies(url, (err: Error | null, cookies?: Cookie[]) => {\n          if (err) return reject(err);\n          resolve(cookies || []);\n        });\n      });\n    },\n\n    setCookie: function (\n      url: string,\n      nameOrCookieObj: string | Record<string, any>,\n      valueOrCallback?: string | ((err?: Error | undefined) => void),\n      maybeCallback?: (err?: Error | undefined) => void\n    ) {\n      // Determine the callback\n      let callback: ((err?: Error | undefined) => void) | undefined;\n      if (typeof maybeCallback === 'function') {\n        callback = maybeCallback;\n      } else if (typeof valueOrCallback === 'function') {\n        callback = valueOrCallback as (err?: Error | undefined) => void;\n      }\n\n      const executeSetCookie = () => {\n        if (!url) throw new Error('URL is required');\n\n        // CASE 1: name/value pair provided\n        if (typeof nameOrCookieObj === 'string') {\n          const cookieName = nameOrCookieObj;\n          const cookieValue = typeof valueOrCallback === 'string' ? valueOrCallback : '';\n\n          if (!cookieName) throw new Error('Cookie name is required');\n\n          const cookie = new Cookie({\n            key: cookieName,\n            value: cookieValue,\n            domain: new URL(url).hostname\n          });\n\n          cookieJar.setCookieSync(cookie, url, { ignoreError: true });\n          return;\n        }\n\n        // CASE 2: cookie object provided\n        if (typeof nameOrCookieObj === 'object' && nameOrCookieObj !== null) {\n          const obj = { ...(nameOrCookieObj as any) } as any;\n\n          if (!obj.key && obj.name) obj.key = obj.name;\n          if (!obj.key) throw new Error('cookieObject.key (name) is required');\n\n          const base = {\n            domain: new URL(url).hostname,\n            ...obj\n          } as any;\n\n          const processedCookie = createCookieObj(base);\n          const cookie = new Cookie(processedCookie);\n          cookieJar.setCookieSync(cookie, url, { ignoreError: true });\n          return;\n        }\n\n        // If we reach here, arguments were invalid\n        throw new Error('Invalid arguments passed to setCookie');\n      };\n\n      if (callback) {\n        // Callback mode\n        try {\n          executeSetCookie();\n          callback(undefined);\n        } catch (err) {\n          callback(err as Error);\n        }\n        return;\n      }\n\n      // Promise mode\n      return new Promise<void>((resolve, reject) => {\n        try {\n          executeSetCookie();\n          resolve();\n        } catch (err) {\n          reject(err);\n        }\n      });\n    },\n\n    setCookies: function (\n      url: string,\n      cookiesArray: any[],\n      callback?: (err?: Error | undefined) => void\n    ) {\n      const executeSetCookies = () => {\n        if (!url) throw new Error('URL is required');\n        if (!Array.isArray(cookiesArray)) {\n          throw new Error('setCookies expects an array of cookie objects');\n        }\n\n        for (const cookieObject of cookiesArray) {\n          const obj = { ...(cookieObject as any) } as any;\n\n          if (!obj.key && obj.name) obj.key = obj.name;\n          if (!obj.key) throw new Error('cookieObject.key (name) is required');\n\n          const base = {\n            domain: new URL(url).hostname,\n            ...obj\n          } as any;\n\n          const processedCookie = createCookieObj(base);\n          const cookie = new Cookie(processedCookie);\n          cookieJar.setCookieSync(cookie, url, { ignoreError: true });\n        }\n      };\n\n      if (callback) {\n        // Callback mode\n        try {\n          executeSetCookies();\n          callback(undefined);\n        } catch (err) {\n          callback(err as Error);\n        }\n        return;\n      }\n\n      // Promise mode\n      return new Promise<void>((resolve, reject) => {\n        try {\n          executeSetCookies();\n          resolve();\n        } catch (err) {\n          reject(err);\n        }\n      });\n    },\n\n    clear: function (callback?: (err?: Error | undefined) => void) {\n      if (callback) {\n        // Callback mode\n        (cookieJar as any).store.removeAllCookies(callback);\n        return;\n      }\n\n      // Promise mode\n      return new Promise<void>((resolve, reject) => {\n        (cookieJar as any).store.removeAllCookies((err?: Error) => {\n          if (err) reject(err);\n          else resolve();\n        });\n      });\n    },\n\n    deleteCookies: function (url: string, callback?: (err?: Error | undefined) => void) {\n      if (!url) {\n        const error = new Error('URL is required');\n        if (callback) {\n          callback(error); return;\n        }\n        return Promise.reject(error);\n      }\n\n      if (callback) {\n        // Callback mode\n        cookieJar.getCookies(url, (err: Error | null, cookies?: Cookie[]) => {\n          if (err) return callback(err);\n          const cookieList = cookies || [];\n          if (!cookieList.length) return callback(undefined);\n\n          let pending = cookieList.length;\n          const done = (removeErr?: Error) => {\n            if (removeErr) return callback(removeErr);\n            if (--pending === 0) {\n              callback(undefined);\n            }\n          };\n\n          cookieList.forEach((cookie) => {\n            (cookieJar as any).store.removeCookie(cookie.domain, cookie.path, cookie.key, done);\n          });\n        });\n        return;\n      }\n\n      // Promise mode\n      return new Promise<void>((resolve, reject) => {\n        cookieJar.getCookies(url, (err: Error | null, cookies?: Cookie[]) => {\n          if (err) return reject(err);\n          const cookieList = cookies || [];\n          if (!cookieList.length) return resolve();\n\n          let pending = cookieList.length;\n          const done = (removeErr?: Error) => {\n            if (removeErr) return reject(removeErr);\n            if (--pending === 0) {\n              resolve();\n            }\n          };\n\n          cookieList.forEach((cookie) => {\n            (cookieJar as any).store.removeCookie(cookie.domain, cookie.path, cookie.key, done);\n          });\n        });\n      });\n    },\n\n    deleteCookie: function (url: string, cookieName: string, callback?: (err?: Error | undefined) => void) {\n      if (!url || !cookieName) {\n        const error = new Error('URL and cookie name are required');\n        if (callback) {\n          callback(error); return;\n        }\n        return Promise.reject(error);\n      }\n\n      const executeDelete = (callback: (err?: Error) => void) => {\n        cookieJar.getCookies(url, (err: Error | null, cookies?: Cookie[]) => {\n          if (err) return callback(err);\n\n          // Filter cookies matching key\n          const cookieList = cookies || [];\n          const matchingCookies = cookieList.filter((c) => c.key === cookieName);\n          if (!matchingCookies.length) return callback(undefined);\n\n          const urlPath = new URL(url).pathname || '/';\n\n          // Prioritise a cookie whose path exactly matches the URL path\n          let cookieToDelete = matchingCookies.find((c) => c.path === urlPath);\n\n          // If not found, fall back to the first matching cookie (most specific path first)\n          if (!cookieToDelete) {\n            // tough-cookie sorts cookies by path length desc, preserve that order\n            cookieToDelete = matchingCookies[0];\n          }\n\n          (cookieJar as any).store.removeCookie(\n            cookieToDelete.domain,\n            cookieToDelete.path,\n            cookieToDelete.key,\n            callback\n          );\n        });\n      };\n\n      if (callback) {\n        // Callback mode\n        executeDelete(callback);\n        return;\n      }\n\n      // Promise mode\n      return new Promise<void>((resolve, reject) => {\n        executeDelete((err?: Error) => {\n          if (err) reject(err);\n          else resolve();\n        });\n      });\n    }\n  } as const;\n};\n\nconst cookiesModule = {\n  cookieJar,\n  addCookieToJar,\n  getCookiesForUrl,\n  getCookieStringForUrl,\n  getDomainsWithCookies,\n  deleteCookie,\n  deleteCookiesForDomain,\n  addCookieForDomain,\n  modifyCookieForDomain,\n  parseCookieString,\n  createCookieString,\n  updateCookieObj,\n  createCookieObj,\n  jar: cookieJarWrapper,\n  saveCookies\n};\n\nexport default cookiesModule;\n"
  },
  {
    "path": "packages/bruno-requests/src/grpc/grpc-client.js",
    "content": "import { makeGenericClientConstructor, ChannelCredentials, Metadata, status, credentials, CallCredentials } from '@grpc/grpc-js';\nimport { GrpcReflection } from 'grpc-js-reflection-client';\nimport * as protoLoader from '@grpc/proto-loader';\nimport { generateGrpcSampleMessage } from './grpcMessageGenerator';\nimport * as tls from 'tls';\nimport { isString } from 'lodash';\nimport * as nodePath from 'node:path';\n\n/**\n * Configuration options for loading and parsing Protocol Buffers definitions.\n * These options are passed to @grpc/proto-loader and protobufjs.\n * @type {import('@grpc/proto-loader').Options}\n */\nconst configOptions = {\n  keepCase: true,\n  alternateCommentMode: true,\n  preferTrailingComment: true,\n  /**\n   * Long conversion type.\n   * Valid values are `String` and `Number` (the global types).\n   * Defaults to copy the present value, which is a possibly unsafe number without and a {@link Long} with a long library.\n   *\n   * JavaScript's Number type can only safely represent integers up to 2^53 - 1 (Number.MAX_SAFE_INTEGER).\n   * Since gRPC's int64, uint64, sint64, and fixed64 types can exceed this limit, we convert them to strings\n   * to preserve their full precision.\n   */\n  longs: String,\n  enums: String,\n  bytes: String,\n  defaults: true,\n  arrays: false,\n  objects: false,\n  oneofs: true,\n  json: true\n};\n\nconst reflectionServices = ['grpc.reflection.v1alpha.ServerReflection', 'grpc.reflection.v1.ServerReflection'];\n\nconst replaceTabsWithSpaces = (str, numSpaces = 2) => {\n  if (!str || !str.length || !isString(str)) {\n    return '';\n  }\n\n  return str.replaceAll('\\t', ' '.repeat(numSpaces)).replaceAll('\\n', '');\n};\n\nconst ensureBuffer = (data) => {\n  if (Buffer.isBuffer(data)) {\n    return data;\n  }\n  return Buffer.from(data, 'utf-8');\n};\n\n/**\n * Safely parse JSON string with error handling\n * @param {string} jsonString - The JSON string to parse\n * @param {string} context - Context for error messages (e.g., 'message content', 'request body')\n * @returns {Object} Parsed object or throws error with context\n * @throws {Error} If JSON parsing fails\n */\nconst safeJsonParse = (jsonString, context = 'JSON string') => {\n  try {\n    return JSON.parse(jsonString);\n  } catch (error) {\n    const errorMessage = `Failed to parse ${context}: ${error.message}`;\n    console.error(errorMessage, { originalString: jsonString, parseError: error });\n    throw new Error(errorMessage);\n  }\n};\n\nconst processGrpcMetadata = (metadata) => {\n  return Object.entries(metadata).map(([name, value]) => {\n    if (Array.isArray(value)) {\n      return {\n        name,\n        value: value\n          .map((v) => {\n            if (v && typeof v === 'object' && v.type === 'Buffer' && Array.isArray(v.data)) {\n              return Buffer.from(v.data).toString('base64');\n            }\n            return v.toString();\n          })\n          .join(', ')\n      };\n    }\n    if (value && typeof value === 'object' && value.type === 'Buffer' && Array.isArray(value.data)) {\n      return { name, value: Buffer.from(value.data).toString('base64') };\n    }\n    return { name, value: value.toString() };\n  });\n};\n\n// Unix socket: unix:/path, unix:///path, unix-abstract:name\nconst isUnixSocket = (str) => {\n  if (!str) return false;\n  return str.startsWith('unix:') || str.startsWith('unix-abstract:');\n};\n\n// Windows named pipe: \\\\.\\pipe\\name or //./pipe/name\nconst isWindowsNamedPipe = (str) => {\n  if (!str) return false;\n  return str.startsWith('\\\\\\\\.\\\\pipe\\\\')\n    || str.startsWith('//./pipe/')\n    || str.toLowerCase().startsWith('\\\\\\\\.\\\\pipe\\\\')\n    || str.toLowerCase().startsWith('//./pipe/');\n};\n\nconst normalizeWindowsNamedPipe = (pipePath) => {\n  if (pipePath.startsWith('//./pipe/')) {\n    return pipePath.replace('//./pipe/', '\\\\\\\\.\\\\pipe\\\\');\n  }\n  return pipePath;\n};\n\n// Parse gRPC URL into components, handling TCP, Unix sockets, and Windows named pipes\nconst getParsedGrpcUrlObject = (url) => {\n  const addProtocolIfMissing = (str) => {\n    if (str.includes('://')) return str;\n    if (str.includes('localhost') || str.includes('127.0.0.1')) {\n      return `grpc://${str}`;\n    }\n    return `grpcs://${str}`;\n  };\n  const removeTrailingSlash = (str) => (str.endsWith('/') ? str.slice(0, -1) : str);\n\n  if (!url) return { host: '', path: '', protocol: '', isLocalTransport: false };\n\n  if (isUnixSocket(url)) {\n    return { host: url, path: '', protocol: 'unix', isLocalTransport: true };\n  }\n\n  if (isWindowsNamedPipe(url)) {\n    return { host: normalizeWindowsNamedPipe(url), path: '', protocol: 'pipe', isLocalTransport: true };\n  }\n\n  const urlObj = new URL(addProtocolIfMissing(url.toLowerCase()));\n\n  return {\n    host: urlObj.host,\n    protocol: urlObj.protocol.replace(':', ''),\n    path: removeTrailingSlash(urlObj.pathname),\n    isLocalTransport: false\n  };\n};\n\n/**\n * Handles gRPC events and forwards them using the provided callback\n * @param {Function} callback - Callback function to send events\n * @param {string} requestId - The unique ID of the request\n * @param {string} collectionUid - The collection UID\n * @param {Object} rpc - The gRPC object\n */\nconst setupGrpcEventHandlers = (callback, requestId, collectionUid, rpc) => {\n  rpc.on('status', (status, res) => {\n    const statusWithMetadata = {\n      ...status,\n      metadata: processGrpcMetadata(status.metadata.getMap ? status.metadata.getMap() : status.metadata)\n    };\n    callback('grpc:status', requestId, collectionUid, { status: statusWithMetadata, res });\n  });\n\n  rpc.on('error', (error) => {\n    const errorWithMetadata = {\n      ...error,\n      metadata: processGrpcMetadata(error.metadata.getMap ? error.metadata.getMap() : error.metadata)\n    };\n    callback('grpc:error', requestId, collectionUid, { error: errorWithMetadata });\n  });\n\n  rpc.on('end', (res) => {\n    callback('grpc:server-end-stream', requestId, collectionUid, { res });\n    const channel = rpc?.call?.channel;\n    if (channel) channel.close();\n  });\n\n  rpc.on('data', (res) => {\n    callback('grpc:response', requestId, collectionUid, { error: null, res });\n  });\n\n  rpc.on('cancel', (res) => {\n    callback('grpc:server-cancel-stream', requestId, collectionUid, { res });\n\n    const channel = rpc?.call?.channel;\n    if (channel) channel.close();\n  });\n\n  rpc.on('metadata', (metadata) => {\n    const metadataWithProcessed = processGrpcMetadata(metadata.getMap ? metadata.getMap() : metadata);\n    callback('grpc:metadata', requestId, collectionUid, { metadata: metadataWithProcessed });\n  });\n};\n\nclass GrpcClient {\n  constructor(eventCallback) {\n    this.activeConnections = new Map();\n    this.methods = new Map();\n    this.eventCallback = eventCallback;\n  }\n\n  /**\n   * Creates call options from metadata for gRPC calls\n   * @param {grpc.Metadata} metadata - metadata to be sent with calls\n   * @returns {Object} callOptions object with credentials if metadata is provided\n   */\n  #createCallOptions(metadata) {\n    if (metadata && Object.keys(metadata.getMap()).length > 0) {\n      // Create CallCredentials from metadata generator\n      const callCredentials = CallCredentials.createFromMetadataGenerator((options, callback) => {\n        callback(null, metadata);\n      });\n      return { credentials: callCredentials };\n    }\n    return {};\n  }\n\n  /**\n   * Creates a reflection client that works for v1, v1alpha, or both.\n   *\n   * @param {string} host - host:port of the gRPC server\n   * @param {grpc.ChannelCredentials} credentials - defaults to insecure\n   * @param {grpc.Metadata} metadata - metadata to be sent with reflection calls (used for insecure connections where credentials can't include metadata)\n   * @param {grpc.ChannelOptions} options - channel options\n   * @returns {Promise<{ client: GrpcReflection, services: string[], callOptions: Object }>}\n   */\n  async #getReflectionClient(host, credentials = ChannelCredentials.createInsecure(), metadata = null, options = {}) {\n    const makeClient = (version) => new GrpcReflection(host, credentials, options, version);\n    const callOptions = this.#createCallOptions(metadata);\n\n    let client;\n    let services;\n\n    try {\n      client = makeClient('v1');\n      services = await client.listServices('*', callOptions);\n      return { client, services, callOptions };\n    } catch (e) {\n      console.warn(`gRPC reflection v1 failed:`, e);\n    }\n\n    client = makeClient('v1alpha');\n    services = await client.listServices('*', callOptions);\n    return { client, services, callOptions };\n  }\n\n  /**\n   * Get method type based on streaming configuration\n   */\n  #getMethodType({ requestStream, responseStream }) {\n    if (requestStream && responseStream) return 'bidi-streaming';\n    if (requestStream) return 'client-streaming';\n    if (responseStream) return 'server-streaming';\n    return 'unary';\n  }\n\n  /**\n   * @typedef {(hostname: string, cert: Object) => Error|undefined} CheckServerIdentityCallback\n   * Callback for custom server certificate verification\n   */\n\n  /**\n   * @typedef {Object} VerifyOptions\n   * @property {CheckServerIdentityCallback} [checkServerIdentity] - If set, this callback will be\n   * invoked after the usual hostname verification has been performed on the peer certificate.\n   * @property {boolean} [rejectUnauthorized] - Whether to reject connections if the certificate validation fails\n   */\n\n  /**\n   * Return a new ChannelCredentials instance with a given set of credentials.\n   * The resulting instance can be used to construct a Channel that communicates\n   * over TLS.\n   * @param {string} url - The gRPC server URL\n   * @param {string|null} rootCertificate - The root certificate data (CA certificate)\n   * @param {string|null} privateKey - The client certificate private key, if available\n   * @param {string|null} certificateChain - The client certificate key chain, if available\n   * @param {string|null} passphrase - The passphrase for the private key, if available\n   * @param {string|null} pfx - The PFX/P12 certificate data, if available\n   * @param {VerifyOptions} verifyOptions - Additional options for verifying the server certificate\n   * @returns {import('@grpc/grpc-js').ChannelCredentials} The gRPC channel credentials\n   */\n  #getChannelCredentials({ url, rootCertificate, privateKey, certificateChain, passphrase, pfx, verifyOptions }) {\n    const securedProtocols = ['grpcs', 'https'];\n    try {\n      const { protocol, isLocalTransport } = getParsedGrpcUrlObject(url);\n\n      if (isLocalTransport) {\n        return ChannelCredentials.createInsecure();\n      }\n\n      const isSecureConnection = securedProtocols.some((sp) => protocol === sp);\n      if (!isSecureConnection) {\n        return ChannelCredentials.createInsecure();\n      }\n\n      const rootCertBuffer = rootCertificate ? ensureBuffer(rootCertificate) : null;\n      const clientCertBuffer = certificateChain ? ensureBuffer(certificateChain) : null;\n      const privateKeyBuffer = privateKey ? ensureBuffer(privateKey) : null;\n      const pfxBuffer = pfx ? ensureBuffer(pfx) : null;\n\n      // Create proper SSL credentials with correct options\n      const sslOptions = {\n        ...(verifyOptions || {}),\n        // Default to true if not specified\n        rejectUnauthorized: verifyOptions?.rejectUnauthorized !== false\n      };\n\n      const shouldUseSecureContext = pfxBuffer || passphrase;\n\n      if (shouldUseSecureContext) {\n        const secureContext = tls.createSecureContext({\n          ca: rootCertBuffer,\n          key: privateKeyBuffer,\n          cert: clientCertBuffer,\n          pfx: pfxBuffer,\n          passphrase: passphrase\n        });\n        return ChannelCredentials.createFromSecureContext(secureContext, sslOptions);\n      }\n\n      return ChannelCredentials.createSsl(rootCertBuffer, privateKeyBuffer, clientCertBuffer, sslOptions);\n    } catch (error) {\n      console.error('Error creating channel credentials:', error);\n      // Default to insecure as fallback\n      return ChannelCredentials.createInsecure();\n    }\n  }\n\n  /**\n   * Get method from the path\n   */\n  #getMethodFromPath(path) {\n    if (this.methods.has(path)) {\n      return this.methods.get(path);\n    }\n    throw new Error(`Method ${path} not found, please refresh the methods`);\n  }\n\n  /**\n   * Refresh methods using reflection or proto file as fallback\n   * @param {Object} options - Options for refreshing methods\n   * @param {string} options.url - The gRPC server URL\n   * @param {Object} options.headers - The request headers/metadata\n   * @param {string} [options.protoPath] - Path to proto file if available\n   * @param {string} [options.collectionPath] - Collection path for proto file resolution\n   * @param {string} [options.collectionUid] - Collection UID\n   * @param {Object} [options.certificates] - Certificate configuration\n   * @param {Object} [options.verifyOptions] - Additional options for verifying the server certificate\n   * @param {string[]} [options.includeDirs] - Include directories for proto file resolution\n   * @returns {Promise<boolean>} Whether methods were successfully refreshed\n   * @private\n   */\n  async #refreshMethods({ url, headers, protoPath, collectionPath, collectionUid, certificates = {}, verifyOptions, includeDirs = [] }) {\n    try {\n      // Try reflection first if no proto path is specified\n      if (!protoPath) {\n        await this.loadMethodsFromReflection({\n          request: { url, headers },\n          collectionUid,\n          rootCertificate: certificates.ca,\n          privateKey: certificates.key,\n          certificateChain: certificates.cert,\n          passphrase: certificates.passphrase,\n          pfx: certificates.pfx,\n          verifyOptions,\n          sendEvent: () => {} // No-op for refresh\n        });\n        return true;\n      }\n\n      // Try proto file if available\n      if (protoPath) {\n        const absoluteProtoPath = nodePath.resolve(collectionPath, protoPath);\n        await this.loadMethodsFromProtoFile(absoluteProtoPath, includeDirs);\n        return true;\n      }\n\n      return false;\n    } catch (error) {\n      console.error('Failed to refresh methods:', error);\n      return false;\n    }\n  }\n\n  /**\n   * Handle connection\n   * @param {Object} options - The options for the connection\n   * @param {import('@grpc/grpc-js/src/make-client').ServiceClient} options.client - The client instance\n   * @param {string} options.requestId - The request ID\n   * @param {string} options.collectionUid - The collection UID\n   * @param {string} options.requestPath - The request path\n   * @param {Object} options.method - The method object\n   * @param {Object} options.messages - The messages []\n   * @param {Object} options.metadata - The metadata object\n   */\n  #handleConnection(options) {\n    const methodType = this.#getMethodType(options.method);\n    switch (methodType) {\n      case 'unary':\n        this.#handleUnaryResponse(options);\n        break;\n      case 'client-streaming':\n        this.#handleClientStreamingResponse(options);\n        break;\n      case 'server-streaming':\n        this.#handleServerStreamingResponse(options);\n        break;\n      case 'bidi-streaming':\n        this.#handleBidiStreamingResponse(options);\n        break;\n      default:\n        throw new Error(`Unsupported method type: ${methodType}`);\n    }\n  }\n\n  /**\n   * Handle unary responses\n   */\n  #handleUnaryResponse({ client, requestId, requestPath, method, messages, metadata, collectionUid }) {\n    const rpc = client.makeUnaryRequest(\n      requestPath,\n      method.requestSerialize,\n      method.responseDeserialize,\n      messages[0],\n      metadata,\n      (error, res) => {\n        this.eventCallback('grpc:response', requestId, collectionUid, { error, res });\n      }\n    );\n\n    setupGrpcEventHandlers(this.eventCallback, requestId, collectionUid, rpc);\n  }\n\n  #handleClientStreamingResponse({ client, requestId, requestPath, method, metadata, collectionUid }) {\n    const rpc = client.makeClientStreamRequest(\n      requestPath,\n      method.requestSerialize,\n      method.responseDeserialize,\n      metadata,\n      (error, res) => {\n        this.eventCallback('grpc:response', requestId, collectionUid, { error, res });\n      }\n    );\n    this.#addConnection(requestId, rpc);\n\n    setupGrpcEventHandlers(this.eventCallback, requestId, collectionUid, rpc);\n  }\n\n  #handleServerStreamingResponse({ client, requestId, requestPath, method, messages, metadata, collectionUid }) {\n    const message = messages[0];\n    const rpc = client.makeServerStreamRequest(\n      requestPath,\n      method.requestSerialize,\n      method.responseDeserialize,\n      message,\n      metadata,\n      (error, res) => {\n        this.eventCallback('grpc:response', requestId, collectionUid, { error, res });\n      }\n    );\n    this.#addConnection(requestId, rpc);\n\n    setupGrpcEventHandlers(this.eventCallback, requestId, collectionUid, rpc);\n  }\n\n  #handleBidiStreamingResponse({ client, requestId, requestPath, method, messages, metadata, collectionUid }) {\n    const rpc = client.makeBidiStreamRequest(\n      requestPath,\n      method.requestSerialize,\n      method.responseDeserialize,\n      metadata\n    );\n    this.#addConnection(requestId, rpc);\n\n    setupGrpcEventHandlers(this.eventCallback, requestId, collectionUid, rpc);\n  }\n\n  /**\n   * Starts a gRPC connection and handles the request based on the method type.\n   * This method sets up the connection, creates the client, and initiates the appropriate\n   * request handling based on whether it's unary, server streaming, client streaming, or bidirectional streaming.\n   *\n   * @param {Object} params - The parameters for starting the connection\n   * @param {Object} params.request - The gRPC request object\n   * @param {string} params.request.url - The gRPC server URL (e.g., 'grpc://localhost:50051')\n   * @param {string} params.request.method - The full method path (e.g., '/package.Service/Method')\n   * @param {Object} params.request.body - The request body containing gRPC messages\n   * @param {Object} params.request.headers - The request headers/metadata\n   * @param {Object} params.collection - The collection object containing the request\n   * @param {string} [params.certificateChain] - The client certificate chain for TLS\n   * @param {string} [params.privateKey] - The client private key for TLS\n   * @param {string} [params.rootCertificate] - The root/CA certificate for TLS\n   * @param {string} [params.passphrase] - The passphrase for the private key if encrypted\n   * @param {string} [params.pfx] - The PFX/P12 certificate data\n   * @param {Object} [params.verifyOptions] - Additional options for verifying the server certificate\n   * @param {import('@grpc/grpc-js').ChannelOptions} [params.channelOptions] - Additional options for the gRPC channel\n   * @param {string[]} [params.includeDirs] - Include directories for proto file resolution\n   */\n  async startConnection({\n    request,\n    collection,\n    certificateChain,\n    privateKey,\n    rootCertificate,\n    passphrase,\n    pfx,\n    verifyOptions,\n    channelOptions = {},\n    includeDirs = []\n  }) {\n    const credentials = this.#getChannelCredentials({\n      url: request.url,\n      rootCertificate,\n      privateKey,\n      certificateChain,\n      passphrase,\n      pfx,\n      verifyOptions\n    });\n    const { host, path } = getParsedGrpcUrlObject(request.url);\n    const methodPath = request.method;\n\n    let method;\n    try {\n      method = this.#getMethodFromPath(methodPath);\n    } catch (error) {\n      /* Attempt to refresh methods as fallback\n      * In an ideal case, the stored metadata from local storage should be received from the client side,\n      * however, this approach causes serialization failure as the method definition loses its requestSerialize function while saving to local storage\n      * so we are using reflection as a fallback\n      */\n      const refreshSuccess = await this.#refreshMethods({\n        url: request.url,\n        headers: request.headers,\n        protoPath: request.protoPath,\n        collectionPath: collection.pathname,\n        collectionUid: collection.uid,\n        certificates: {\n          ca: rootCertificate,\n          cert: certificateChain,\n          key: privateKey,\n          passphrase,\n          pfx\n        },\n        verifyOptions,\n        includeDirs\n      });\n\n      if (!refreshSuccess) {\n        throw new Error(`Failed to refresh methods and method ${methodPath} not found`);\n      }\n\n      // Try to get the method again after refresh\n      try {\n        method = this.#getMethodFromPath(methodPath);\n      } catch (refreshError) {\n        throw refreshError;\n      }\n    }\n\n    // Extract user-agent from headers if provided (case-insensitive)\n    // Set it as grpc.primary_user_agent channel option to prepend to the default user-agent\n    const userAgentKey = Object.keys(request.headers).find(\n      (key) => key.toLowerCase() === 'user-agent'\n    );\n    const userAgentValue = userAgentKey ? request.headers[userAgentKey] : null;\n\n    const mergedChannelOptions = userAgentValue\n      ? { 'grpc.primary_user_agent': userAgentValue, ...channelOptions }\n      : channelOptions;\n\n    const Client = makeGenericClientConstructor({});\n    const client = new Client(host, credentials, mergedChannelOptions);\n    if (!client) {\n      throw new Error('Failed to create client');\n    }\n\n    let messages = request.body.grpc;\n    try {\n      messages = messages.map(({ content }) => safeJsonParse(content, 'message content'));\n    } catch (parseError) {\n      console.error('Failed to parse gRPC message content:', parseError);\n      this.eventCallback('grpc:error', request.uid, collection.uid, {\n        error: parseError\n      });\n      return; // Exit early to prevent sending invalid data\n    }\n\n    const requestPath = path + methodPath;\n    const requestId = request.uid;\n    const collectionUid = collection.uid;\n    const metadata = new Metadata();\n    Object.entries(request.headers).forEach(([name, value]) => {\n      metadata.add(name, value);\n    });\n\n    this.#handleConnection({\n      client,\n      requestId,\n      collectionUid,\n      requestPath,\n      method,\n      messages,\n      metadata\n    });\n  }\n\n  /**\n   * Send a message to an active gRPC connection\n   * @param {string} requestId - The request ID of the active connection\n   * @param {string} collectionUid - The collection UID for the request\n   * @param {Object|string} body - The message body to send, can be a JSON object or a string\n   */\n  sendMessage(requestId, collectionUid, body) {\n    const connection = this.activeConnections.get(requestId);\n\n    if (connection) {\n      let parsedBody;\n\n      // Parse the body if it's a string, with error handling\n      if (typeof body === 'string') {\n        try {\n          parsedBody = safeJsonParse(body, 'request body');\n        } catch (parseError) {\n          // Log the error and notify the client\n          console.error('Failed to parse message body:', parseError);\n          this.eventCallback('grpc:error', requestId, collectionUid, {\n            error: parseError\n          });\n          return; // Exit early to prevent sending invalid data\n        }\n      } else {\n        parsedBody = body;\n      }\n\n      connection.write(parsedBody, (error) => {\n        if (error) {\n          this.eventCallback('grpc:error', requestId, collectionUid, { error });\n        }\n      });\n    }\n  }\n\n  /**\n   * Load methods from server reflection\n   */\n  async loadMethodsFromReflection({\n    request,\n    collectionUid,\n    rootCertificate,\n    privateKey,\n    certificateChain,\n    passphrase,\n    pfx,\n    verifyOptions,\n    sendEvent,\n    channelOptions = {}\n  }) {\n    const { host, path } = getParsedGrpcUrlObject(request.url);\n\n    // Extract user-agent from headers if provided (case-insensitive)\n    // Set it as grpc.primary_user_agent channel option to prepend to the default user-agent\n    const userAgentKey = Object.keys(request.headers).find(\n      (key) => key.toLowerCase() === 'user-agent'\n    );\n    const userAgentValue = userAgentKey ? request.headers[userAgentKey] : null;\n    const mergedChannelOptions = userAgentValue ? { 'grpc.primary_user_agent': userAgentValue, ...channelOptions } : channelOptions;\n\n    const metadata = new Metadata();\n    Object.entries(request.headers).forEach(([name, value]) => {\n      metadata.add(name, value);\n    });\n    const credentials = this.#getChannelCredentials({\n      url: request.url,\n      rootCertificate,\n      privateKey,\n      certificateChain,\n      passphrase,\n      pfx,\n      verifyOptions\n    });\n\n    try {\n      const { client, services, callOptions } = await this.#getReflectionClient(host, credentials, metadata, mergedChannelOptions);\n\n      const methods = [];\n      for (const service of services) {\n        if (reflectionServices.includes(service)) {\n          continue;\n        }\n        const m = await client.listMethods(service, callOptions);\n        methods.push(...m);\n      }\n\n      const methodsWithType = methods.map((method) => {\n        const { definition, ...rest } = method;\n        const modifiedMethod = {\n          ...rest,\n          ...definition\n        };\n        modifiedMethod.type = this.#getMethodType(modifiedMethod);\n        return modifiedMethod;\n      });\n      methodsWithType.forEach((method) => {\n        this.methods.set(method.path, method);\n      });\n      return methodsWithType;\n    } catch (error) {\n      console.error('Error in gRPC reflection:', error);\n      sendEvent('grpc:error', request.uid, collectionUid, { error });\n      throw error;\n    }\n  }\n\n  async loadMethodsFromProtoFile(filePath, includeDirs = []) {\n    const protoDefinition = await protoLoader.load(filePath, { ...configOptions, includeDirs });\n    const methods = Object.values(protoDefinition)\n      .filter((definition) => !definition?.format)\n      .flatMap(Object.values);\n    const methodsWithType = methods.map((method) => ({\n      ...method,\n      type: this.#getMethodType(method)\n    }));\n    methods.forEach((method) => {\n      this.methods.set(method.path, method);\n    });\n    return methodsWithType;\n  }\n\n  end(requestId) {\n    const connection = this.activeConnections.get(requestId);\n    if (connection && typeof connection.end === 'function') {\n      connection.end();\n      this.#removeConnection(requestId);\n    }\n  }\n\n  cancel(requestId) {\n    const connection = this.activeConnections.get(requestId);\n    if (connection && typeof connection.cancel === 'function') {\n      connection.cancel();\n      this.#removeConnection(requestId);\n    }\n  }\n\n  /**\n   * Check if a connection is active\n   * @param {string} requestId - The request ID to check\n   * @returns {boolean} - Whether the connection is active\n   */\n  isConnectionActive(requestId) {\n    return this.activeConnections.has(requestId);\n  }\n\n  /**\n   * Clear all active connections\n   */\n  clearAllConnections() {\n    const connectionIds = this.getActiveConnectionIds();\n\n    this.activeConnections.forEach((connection) => {\n      if (typeof connection.cancel === 'function') {\n        connection.cancel();\n      }\n    });\n\n    this.activeConnections.clear();\n\n    // Emit an event with empty active connection IDs\n    if (connectionIds.length > 0) {\n      this.eventCallback('grpc:connections-changed', {\n        type: 'cleared',\n        activeConnectionIds: []\n      });\n    }\n  }\n\n  /**\n   * Generate a sample message for a specific method path\n   * @param {string} methodPath - The full gRPC method path\n   * @param {Object} options - Options for message generation\n   * @returns {Object} A sample message or error\n   */\n  generateSampleMessage(methodPath, options = {}) {\n    try {\n      let method;\n\n      // First, try to use the methodMetadata from options if provided\n      if (options.methodMetadata) {\n        method = options.methodMetadata;\n      } else {\n        // Fall back to checking if the method exists in the cache\n        if (!this.methods.has(methodPath)) {\n          return {\n            success: false,\n            error: `Method ${methodPath} not found in cache, please refresh the methods`\n          };\n        }\n\n        // Get the method definition from cache\n        method = this.methods.get(methodPath);\n      }\n\n      // Generate a sample message using our generator\n      const sampleMessage = generateGrpcSampleMessage(method, options);\n\n      return {\n        success: true,\n        message: sampleMessage\n      };\n    } catch (error) {\n      console.error('Error generating sample gRPC message:', error);\n      return {\n        success: false,\n        error: error.message || 'Failed to generate sample message'\n      };\n    }\n  }\n\n  /**\n   * Get all active connection IDs\n   * @returns {string[]} Array of active connection IDs\n   */\n  getActiveConnectionIds() {\n    return Array.from(this.activeConnections.keys());\n  }\n\n  /**\n   * Add a connection to the active connections map and emit an event\n   * @param {string} requestId - The request ID\n   * @param {Object} connection - The connection object\n   * @private\n   */\n  #addConnection(requestId, connection) {\n    this.activeConnections.set(requestId, connection);\n\n    // Emit an event with all active connection IDs\n    this.eventCallback('grpc:connections-changed', {\n      type: 'added',\n      requestId,\n      activeConnectionIds: this.getActiveConnectionIds()\n    });\n  }\n\n  /**\n   * Remove a connection from the active connections map and emit an event\n   * @param {string} requestId - The request ID\n   * @private\n   */\n  #removeConnection(requestId) {\n    if (this.activeConnections.has(requestId)) {\n      this.activeConnections.delete(requestId);\n\n      // Emit an event with all active connection IDs\n      this.eventCallback('grpc:connections-changed', {\n        type: 'removed',\n        requestId,\n        activeConnectionIds: this.getActiveConnectionIds()\n      });\n    }\n  }\n\n  /**\n   * Generate a grpcurl command for a gRPC request\n   * @param {Object} request -  request object\n   * @param {Object} options.certificates - Certificate configuration\n   * @returns {string} The generated grpcurl command\n   */\n  generateGrpcurlCommand({ request, collectionPath = '', shell = 'bash', certificates = {} }) {\n    const { url, method, methodType = 'unary', body, headers, protoPath } = request;\n    const useReflection = !protoPath;\n    const parts = [];\n    const { host, path, protocol } = getParsedGrpcUrlObject(url);\n    const { ca, cert, key } = certificates;\n\n    parts.push('grpcurl');\n\n    if (protocol === 'unix') {\n      parts.push('-plaintext');\n      parts.push('-unix');\n      parts.push('-authority localhost');\n    } else if (protocol === 'pipe') {\n      console.warn('Windows named pipes are not directly supported by grpcurl');\n      parts.push('-plaintext');\n    } else if (url.startsWith('grpcs://') || url.startsWith('https://')) {\n      if (ca) {\n        /**\n         * Instead of using certificate that relies on CN, use SANs\n         * CN certificates seems to cause verification errors with grpcurl\n         * https://github.com/fullstorydev/grpcurl/issues/320\n         */\n        parts.push(`-cacert ${ca}`);\n      }\n      if (cert && key) {\n        /**\n         * passphrase is not supported by grpcurl, so we need to decrypt the key first\n         * When using key that is encrypted, use the passphrase to decrypt it\n         * openssl rsa -in client.key -out client_decrypted.key\n         * it will ask for passphrase, use the passphrase to decrypt the key\n         * then use the decrypted key for making the request using grpcurl\n         */\n        parts.push(`-cert ${cert}`);\n        parts.push(`-key ${key}`);\n      }\n    } else {\n      parts.push('-plaintext');\n    }\n\n    for (const [key, value] of Object.entries(headers)) {\n      parts.push(`-H \"${key}: ${value}\"`);\n    }\n\n    if (!useReflection && protoPath) {\n      const absoluteProtoPath = collectionPath ? nodePath.resolve(collectionPath, protoPath) : protoPath;\n      const importPath = nodePath.dirname(absoluteProtoPath);\n      const protoFileName = nodePath.basename(absoluteProtoPath);\n      parts.push(`-import-path ${importPath}`);\n      parts.push(`-proto ${protoFileName}`);\n    }\n\n    const isClientStreaming = methodType === 'client-streaming' || methodType === 'bidi-streaming';\n\n    if (body.grpc.length > 0) {\n      if (isClientStreaming) {\n        parts.push(`-d @`);\n      } else {\n        // For unary and server streaming, send as a single message\n        parts.push(`-d '${replaceTabsWithSpaces(body.grpc[0].content)}'`);\n      }\n    }\n\n    if (protocol === 'unix') {\n      let socketPath = url;\n      if (url.startsWith('unix:///')) {\n        socketPath = url.slice(7);\n      } else if (url.startsWith('unix:')) {\n        socketPath = url.slice(5);\n      }\n      parts.push(socketPath);\n    } else {\n      parts.push(host);\n    }\n\n    parts.push(path.slice(1) + (path ? '/' : '') + (method.startsWith('/') ? method.slice(1) : method));\n\n    if (isClientStreaming) {\n      const messages = body.grpc.map(({ content }) => replaceTabsWithSpaces(content));\n      const stdinData = messages.join('\\n');\n      parts.push(`<< EOF\\n${stdinData}\\nEOF`);\n    }\n\n    return parts.join(' ');\n  }\n}\n\nexport { GrpcClient };\n"
  },
  {
    "path": "packages/bruno-requests/src/grpc/grpc-client.spec.js",
    "content": "/**\n * @jest-environment node\n */\n\n// Store captured channel options for assertions\nlet capturedChannelOptions = null;\n\n// Mock GrpcReflection to capture options\nconst mockListServices = jest.fn().mockResolvedValue(['test.Service']);\nconst mockListMethods = jest.fn().mockResolvedValue([\n  {\n    path: '/test.Service/TestMethod',\n    definition: {\n      requestStream: false,\n      responseStream: false\n    }\n  }\n]);\n\njest.mock('grpc-js-reflection-client', () => ({\n  GrpcReflection: jest.fn().mockImplementation((host, credentials, options) => {\n    capturedChannelOptions = options;\n    return {\n      listServices: mockListServices,\n      listMethods: mockListMethods\n    };\n  })\n}));\n\n// Mock @grpc/grpc-js\njest.mock('@grpc/grpc-js', () => {\n  const createMockMetadata = () => {\n    const map = {};\n    return {\n      add: jest.fn((key, value) => {\n        if (map[key] === undefined) {\n          map[key] = value;\n        } else if (Array.isArray(map[key])) {\n          map[key].push(value);\n        } else {\n          map[key] = [map[key], value];\n        }\n      }),\n      getMap: jest.fn(() => map)\n    };\n  };\n\n  // Create a mock RPC object with event emitter interface\n  const createMockRpc = () => {\n    const handlers = {};\n    const mockRpc = {\n      on: jest.fn((event, handler) => {\n        handlers[event] = handler;\n        return mockRpc; // Return the mock object for chaining\n      }),\n      write: jest.fn(),\n      end: jest.fn(),\n      cancel: jest.fn(),\n      call: {\n        channel: { close: jest.fn() }\n      }\n    };\n    return mockRpc;\n  };\n\n  return {\n    makeGenericClientConstructor: jest.fn(() => {\n      return jest.fn().mockImplementation((host, credentials, options) => {\n        capturedChannelOptions = options;\n        const mockRpc = createMockRpc();\n        return {\n          close: jest.fn(),\n          makeUnaryRequest: jest.fn().mockReturnValue(mockRpc),\n          makeClientStreamRequest: jest.fn().mockReturnValue(mockRpc),\n          makeServerStreamRequest: jest.fn().mockReturnValue(mockRpc),\n          makeBidiStreamRequest: jest.fn().mockReturnValue(mockRpc)\n        };\n      });\n    }),\n    ChannelCredentials: {\n      createInsecure: jest.fn().mockReturnValue('insecure-credentials'),\n      createSsl: jest.fn().mockReturnValue('ssl-credentials'),\n      createFromSecureContext: jest.fn().mockReturnValue('secure-context-credentials')\n    },\n    Metadata: jest.fn().mockImplementation(() => createMockMetadata()),\n    status: {},\n    credentials: {},\n    CallCredentials: {\n      createFromMetadataGenerator: jest.fn().mockReturnValue('call-credentials')\n    }\n  };\n});\n\n// Mock proto-loader\njest.mock('@grpc/proto-loader', () => ({\n  load: jest.fn().mockResolvedValue({})\n}));\n\nimport { GrpcClient } from './grpc-client';\n\ndescribe('GrpcClient', () => {\n  let grpcClient;\n  let mockEventCallback;\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n    capturedChannelOptions = null;\n    mockEventCallback = jest.fn();\n    grpcClient = new GrpcClient(mockEventCallback);\n  });\n\n  describe('User-Agent behavior in loadMethodsFromReflection', () => {\n    const baseRequest = {\n      url: 'grpc://localhost:50051',\n      uid: 'test-request-uid',\n      headers: {}\n    };\n\n    const baseParams = {\n      collectionUid: 'test-collection-uid',\n      sendEvent: jest.fn()\n    };\n\n    describe('case-insensitive header extraction', () => {\n      test('should extract User-Agent header (capitalized)', async () => {\n        const request = {\n          ...baseRequest,\n          headers: { 'User-Agent': 'Bruno/1.0' }\n        };\n\n        await grpcClient.loadMethodsFromReflection({\n          request,\n          ...baseParams\n        });\n\n        expect(capturedChannelOptions['grpc.primary_user_agent']).toBe('Bruno/1.0');\n      });\n\n      test('should extract user-agent header (lowercase)', async () => {\n        const request = {\n          ...baseRequest,\n          headers: { 'user-agent': 'Bruno/1.0' }\n        };\n\n        await grpcClient.loadMethodsFromReflection({\n          request,\n          ...baseParams\n        });\n\n        expect(capturedChannelOptions['grpc.primary_user_agent']).toBe('Bruno/1.0');\n      });\n\n      test('should extract USER-AGENT header (uppercase)', async () => {\n        const request = {\n          ...baseRequest,\n          headers: { 'USER-AGENT': 'Bruno/1.0' }\n        };\n\n        await grpcClient.loadMethodsFromReflection({\n          request,\n          ...baseParams\n        });\n\n        expect(capturedChannelOptions['grpc.primary_user_agent']).toBe('Bruno/1.0');\n      });\n\n      test('should extract uSeR-aGeNt header (mixed case)', async () => {\n        const request = {\n          ...baseRequest,\n          headers: { 'uSeR-aGeNt': 'Bruno/1.0' }\n        };\n\n        await grpcClient.loadMethodsFromReflection({\n          request,\n          ...baseParams\n        });\n\n        expect(capturedChannelOptions['grpc.primary_user_agent']).toBe('Bruno/1.0');\n      });\n    });\n\n    describe('channel options merging', () => {\n      test('should preserve existing channelOptions when user-agent is set', async () => {\n        const request = {\n          ...baseRequest,\n          headers: { 'User-Agent': 'Bruno/1.0' }\n        };\n\n        await grpcClient.loadMethodsFromReflection({\n          request,\n          ...baseParams,\n          channelOptions: {\n            'grpc.max_receive_message_length': 1024 * 1024,\n            'grpc.keepalive_time_ms': 30000\n          }\n        });\n\n        expect(capturedChannelOptions['grpc.primary_user_agent']).toBe('Bruno/1.0');\n        expect(capturedChannelOptions['grpc.max_receive_message_length']).toBe(1024 * 1024);\n        expect(capturedChannelOptions['grpc.keepalive_time_ms']).toBe(30000);\n      });\n\n      test('should include grpc.primary_user_agent in merged options alongside other options', async () => {\n        const request = {\n          ...baseRequest,\n          headers: { 'User-Agent': 'Bruno/1.0' }\n        };\n\n        await grpcClient.loadMethodsFromReflection({\n          request,\n          ...baseParams,\n          channelOptions: {\n            'grpc.other_option': 'value'\n          }\n        });\n\n        // Use array notation for keys containing dots to avoid Jest interpreting as nested path\n        expect(capturedChannelOptions).toHaveProperty(['grpc.primary_user_agent'], 'Bruno/1.0');\n        expect(capturedChannelOptions).toHaveProperty(['grpc.other_option'], 'value');\n      });\n\n      test('should allow channelOptions to override grpc.primary_user_agent', async () => {\n        const request = {\n          ...baseRequest,\n          headers: { 'User-Agent': 'Bruno/1.0' }\n        };\n\n        await grpcClient.loadMethodsFromReflection({\n          request,\n          ...baseParams,\n          channelOptions: {\n            'grpc.primary_user_agent': 'ExistingUA'\n          }\n        });\n\n        expect(capturedChannelOptions['grpc.primary_user_agent']).toBe('ExistingUA');\n      });\n    });\n\n    describe('missing user-agent handling', () => {\n      test('should pass channelOptions unchanged when no user-agent header', async () => {\n        const request = {\n          ...baseRequest,\n          headers: { 'Content-Type': 'application/grpc' }\n        };\n\n        const channelOptions = {\n          'grpc.max_receive_message_length': 1024 * 1024\n        };\n\n        await grpcClient.loadMethodsFromReflection({\n          request,\n          ...baseParams,\n          channelOptions\n        });\n\n        expect(capturedChannelOptions['grpc.primary_user_agent']).toBeUndefined();\n        expect(capturedChannelOptions['grpc.max_receive_message_length']).toBe(1024 * 1024);\n      });\n\n      test('should pass empty object when no user-agent and no channelOptions', async () => {\n        const request = {\n          ...baseRequest,\n          headers: {}\n        };\n\n        await grpcClient.loadMethodsFromReflection({\n          request,\n          ...baseParams\n        });\n\n        expect(capturedChannelOptions['grpc.primary_user_agent']).toBeUndefined();\n      });\n\n      test('should not add grpc.primary_user_agent when user-agent header is missing', async () => {\n        const request = {\n          ...baseRequest,\n          headers: { Authorization: 'Bearer token' }\n        };\n\n        await grpcClient.loadMethodsFromReflection({\n          request,\n          ...baseParams,\n          channelOptions: {}\n        });\n\n        expect(capturedChannelOptions['grpc.primary_user_agent']).toBeUndefined();\n        expect(Object.keys(capturedChannelOptions)).not.toContain('grpc.primary_user_agent');\n      });\n    });\n\n    describe('edge cases', () => {\n      test('should handle empty user-agent value', async () => {\n        const request = {\n          ...baseRequest,\n          headers: { 'User-Agent': '' }\n        };\n\n        await grpcClient.loadMethodsFromReflection({\n          request,\n          ...baseParams\n        });\n\n        // Empty string is falsy, so grpc.primary_user_agent should not be set\n        expect(capturedChannelOptions['grpc.primary_user_agent']).toBeUndefined();\n      });\n    });\n  });\n\n  describe('User-Agent behavior in startConnection', () => {\n    const baseRequest = {\n      url: 'grpc://localhost:50051',\n      uid: 'test-request-uid',\n      method: '/test.Service/TestMethod',\n      headers: {},\n      body: {\n        grpc: [{ content: '{}' }]\n      }\n    };\n\n    const baseCollection = {\n      uid: 'test-collection-uid',\n      pathname: '/test/path'\n    };\n\n    beforeEach(() => {\n      // Pre-register a method so startConnection can find it\n      grpcClient.methods.set('/test.Service/TestMethod', {\n        path: '/test.Service/TestMethod',\n        requestStream: false,\n        responseStream: false,\n        requestSerialize: (val) => Buffer.from(JSON.stringify(val)),\n        responseDeserialize: (val) => JSON.parse(val.toString())\n      });\n    });\n\n    describe('case-insensitive header extraction', () => {\n      test('should extract User-Agent header (capitalized)', async () => {\n        const request = {\n          ...baseRequest,\n          headers: { 'User-Agent': 'Bruno/1.0' }\n        };\n\n        await grpcClient.startConnection({\n          request,\n          collection: baseCollection\n        });\n\n        expect(capturedChannelOptions['grpc.primary_user_agent']).toBe('Bruno/1.0');\n      });\n\n      test('should extract user-agent header (lowercase)', async () => {\n        const request = {\n          ...baseRequest,\n          headers: { 'user-agent': 'Bruno/1.0' }\n        };\n\n        await grpcClient.startConnection({\n          request,\n          collection: baseCollection\n        });\n\n        expect(capturedChannelOptions['grpc.primary_user_agent']).toBe('Bruno/1.0');\n      });\n\n      test('should extract USER-AGENT header (uppercase)', async () => {\n        const request = {\n          ...baseRequest,\n          headers: { 'USER-AGENT': 'Bruno/1.0' }\n        };\n\n        await grpcClient.startConnection({\n          request,\n          collection: baseCollection\n        });\n\n        expect(capturedChannelOptions['grpc.primary_user_agent']).toBe('Bruno/1.0');\n      });\n\n      test('should extract uSeR-aGeNt header (mixed case)', async () => {\n        const request = {\n          ...baseRequest,\n          headers: { 'uSeR-aGeNt': 'Bruno/1.0' }\n        };\n\n        await grpcClient.startConnection({\n          request,\n          collection: baseCollection\n        });\n\n        expect(capturedChannelOptions['grpc.primary_user_agent']).toBe('Bruno/1.0');\n      });\n    });\n\n    describe('channel options merging', () => {\n      test('should preserve existing channelOptions when user-agent is set', async () => {\n        const request = {\n          ...baseRequest,\n          headers: { 'User-Agent': 'Bruno/1.0' }\n        };\n\n        await grpcClient.startConnection({\n          request,\n          collection: baseCollection,\n          channelOptions: {\n            'grpc.max_receive_message_length': 1024 * 1024,\n            'grpc.keepalive_time_ms': 30000\n          }\n        });\n\n        expect(capturedChannelOptions['grpc.primary_user_agent']).toBe('Bruno/1.0');\n        expect(capturedChannelOptions['grpc.max_receive_message_length']).toBe(1024 * 1024);\n        expect(capturedChannelOptions['grpc.keepalive_time_ms']).toBe(30000);\n      });\n\n      test('should include grpc.primary_user_agent in merged options alongside other options', async () => {\n        const request = {\n          ...baseRequest,\n          headers: { 'User-Agent': 'Bruno/1.0' }\n        };\n\n        await grpcClient.startConnection({\n          request,\n          collection: baseCollection,\n          channelOptions: {\n            'grpc.other_option': 'value'\n          }\n        });\n\n        // Use array notation for keys containing dots to avoid Jest interpreting as nested path\n        expect(capturedChannelOptions).toHaveProperty(['grpc.primary_user_agent'], 'Bruno/1.0');\n        expect(capturedChannelOptions).toHaveProperty(['grpc.other_option'], 'value');\n      });\n\n      test('should allow channelOptions to override grpc.primary_user_agent', async () => {\n        const request = {\n          ...baseRequest,\n          headers: { 'User-Agent': 'Bruno/1.0' }\n        };\n\n        await grpcClient.startConnection({\n          request,\n          collection: baseCollection,\n          channelOptions: {\n            'grpc.primary_user_agent': 'ExistingUA'\n          }\n        });\n\n        expect(capturedChannelOptions['grpc.primary_user_agent']).toBe('ExistingUA');\n      });\n    });\n\n    describe('missing user-agent handling', () => {\n      test('should pass channelOptions unchanged when no user-agent header', async () => {\n        const request = {\n          ...baseRequest,\n          headers: { 'Content-Type': 'application/grpc' }\n        };\n\n        const channelOptions = {\n          'grpc.max_receive_message_length': 1024 * 1024\n        };\n\n        await grpcClient.startConnection({\n          request,\n          collection: baseCollection,\n          channelOptions\n        });\n\n        expect(capturedChannelOptions['grpc.primary_user_agent']).toBeUndefined();\n        expect(capturedChannelOptions['grpc.max_receive_message_length']).toBe(1024 * 1024);\n      });\n\n      test('should not add grpc.primary_user_agent when user-agent header is missing', async () => {\n        const request = {\n          ...baseRequest,\n          headers: { Authorization: 'Bearer token' }\n        };\n\n        await grpcClient.startConnection({\n          request,\n          collection: baseCollection,\n          channelOptions: {}\n        });\n\n        expect(capturedChannelOptions['grpc.primary_user_agent']).toBeUndefined();\n        expect(Object.keys(capturedChannelOptions)).not.toContain('grpc.primary_user_agent');\n      });\n    });\n\n    describe('edge cases', () => {\n      test('should handle empty user-agent value', async () => {\n        const request = {\n          ...baseRequest,\n          headers: { 'User-Agent': '' }\n        };\n\n        await grpcClient.startConnection({\n          request,\n          collection: baseCollection\n        });\n\n        // Empty string is falsy, so grpc.primary_user_agent should not be set\n        expect(capturedChannelOptions['grpc.primary_user_agent']).toBeUndefined();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-requests/src/grpc/grpcMessageGenerator.js",
    "content": "import { faker } from '@faker-js/faker';\n\n/**\n * Generates a sample message based on method parameter fields\n * @param {Object} fields - Method parameter fields\n * @param {Object} options - Generation options\n * @returns {Object} Generated message\n */\nconst generateSampleMessageFromFields = (fields, options = {}) => {\n  const result = {};\n\n  if (!fields || !Array.isArray(fields)) {\n    return {};\n  }\n\n  fields.forEach((field) => {\n    // Generate a value based on field name and type\n    if (field.type === 'TYPE_MESSAGE') {\n      // Handle nested message\n      if (field.messageType && field.messageType.field) {\n        if (field.label === 'LABEL_REPEATED') {\n          // Generate array of nested messages\n          const count = options.arraySize || faker.number.int({ min: 1, max: 3 });\n          result[field.name] = Array.from({ length: count }, () =>\n            generateSampleMessageFromFields(field.messageType.field, options)\n          );\n        } else {\n          // Generate single nested message\n          result[field.name] = generateSampleMessageFromFields(field.messageType.field, options);\n        }\n      } else {\n        // No field info for nested message, generate a simple object\n        result[field.name] = field.label === 'LABEL_REPEATED' ? [{}] : {};\n      }\n    } else if (field.type === 'TYPE_ENUM') {\n      result[field.name] = field.label === 'LABEL_REPEATED' ? [0] : 0;\n    } else {\n      // Generate value based on primitive type and name\n      let value;\n\n      switch (field.type) {\n        case 'TYPE_DOUBLE':\n        case 'TYPE_FLOAT':\n          value = faker.number.float({ min: 0, max: 1000, precision: 0.01 });\n          break;\n        case 'TYPE_INT32':\n        case 'TYPE_INT64':\n        case 'TYPE_SINT32':\n        case 'TYPE_SINT64':\n        case 'TYPE_UINT32':\n        case 'TYPE_UINT64':\n        case 'TYPE_FIXED32':\n        case 'TYPE_FIXED64':\n          value = faker.number.int({ min: 0, max: 1000 });\n          break;\n        case 'TYPE_BOOL':\n          value = faker.datatype.boolean();\n          break;\n        case 'TYPE_STRING':\n          value = faker.lorem.word();\n          break;\n        case 'TYPE_BYTES':\n          value = Buffer.from(faker.string.alpha({ length: { min: 5, max: 10 } })).toString('base64');\n          break;\n        default:\n          value = faker.lorem.word();\n      }\n\n      if (field.label === 'LABEL_REPEATED') {\n        // Generate array of values\n        const count = options.arraySize || faker.number.int({ min: 1, max: 3 });\n        result[field.name] = Array.from({ length: count }, () => value);\n      } else {\n        result[field.name] = value;\n      }\n    }\n  });\n\n  return result;\n};\n\n/**\n * Extracts field definitions from a method's request type\n * @param {Object} method - The gRPC method\n * @returns {Array|null} Array of field definitions or null\n */\nconst getMethodRequestFields = (method) => {\n  try {\n    // Navigate through various potential property paths to find fields\n    if (method.requestType?.type?.field) {\n      return method.requestType.type.field;\n    }\n\n    if (method.requestType?.field) {\n      return method.requestType.field;\n    }\n\n    if (method.requestType?.type) {\n      return method.requestType.type;\n    }\n  } catch (error) {\n    console.error('Error extracting method request fields:', error);\n    return null;\n  }\n};\n\n/**\n * Generates a sample gRPC message based on a method definition\n * @param {Object} method - gRPC method definition\n * @param {Object} options - Generation options\n * @returns {Object} Generated message\n */\nexport const generateGrpcSampleMessage = (method, options = {}) => {\n  try {\n    if (!method) {\n      return {};\n    }\n\n    const fields = getMethodRequestFields(method);\n\n    if (fields) {\n      return generateSampleMessageFromFields(fields, options);\n    }\n\n    // If method exists but no field information could be extracted,\n    // generate a generic message that matches common patterns\n    return {};\n  } catch (error) {\n    console.error('Error generating gRPC sample message:', error);\n  }\n};\n"
  },
  {
    "path": "packages/bruno-requests/src/grpc/index.ts",
    "content": "export { GrpcClient } from './grpc-client';\nexport { generateGrpcSampleMessage } from './grpcMessageGenerator';\n"
  },
  {
    "path": "packages/bruno-requests/src/index.ts",
    "content": "export { addDigestInterceptor, getOAuth2Token } from './auth';\nexport { GrpcClient, generateGrpcSampleMessage } from './grpc';\nexport { WsClient } from './ws/ws-client';\nexport { default as cookies } from './cookies';\n\nexport { getCACertificates } from './utils/ca-cert';\nexport { transformProxyConfig } from './utils/proxy-util';\nexport { default as createVaultClient, VaultError } from './utils/node-vault';\nexport type { VaultClient, VaultConfig, VaultRequestOptions } from './utils/node-vault';\nexport { getHttpHttpsAgents } from './utils/http-https-agents';\nexport { initializeShellEnv } from './utils/shell-env';\nexport { getOrCreateHttpsAgent, getOrCreateHttpAgent, clearAgentCache, getAgentCacheSize } from './utils/agent-cache';\n\nexport * as scripting from './scripting';\n\nexport { makeAxiosInstance, getSystemProxy } from './network';\n"
  },
  {
    "path": "packages/bruno-requests/src/network/axios-instance.ts",
    "content": "import { default as axios, AxiosRequestConfig, AxiosRequestHeaders, AxiosResponse, InternalAxiosRequestConfig } from 'axios';\nimport http from 'node:http';\nimport https from 'node:https';\n\n/**\n *\n * @param {Object} customRequestConfig options - partial AxiosRequestConfig\n *\n * @returns {import('axios').AxiosInstance} Configured Axios instance\n *\n * @example\n * const instance = makeAxiosInstance({\n *   maxRedirects: 0,\n *   proxy: false,\n *   headers: {\n *       \"User-Agent\": `bruno-runtime/_version_`\n *   },\n * });\n */\n\ntype ModifiedInternalAxiosRequestConfig = InternalAxiosRequestConfig & {\n  startTime: number;\n  __headersToDelete?: string[];\n};\n\ntype ModifiedAxiosResponse = AxiosResponse & {\n  responseTime: number;\n};\n\nconst baseRequestConfig: Partial<AxiosRequestConfig> = {\n  proxy: false,\n  httpAgent: new http.Agent({ keepAlive: true }),\n  httpsAgent: new https.Agent({ keepAlive: true }),\n  transformRequest: function transformRequest(data: any, headers: AxiosRequestHeaders) {\n    const contentType = headers.getContentType() || '';\n    const hasJSONContentType = contentType.includes('json');\n    if (typeof data === 'string' && hasJSONContentType) {\n      return data;\n    }\n\n    if (Array.isArray(axios.defaults.transformRequest)) {\n      axios.defaults.transformRequest.forEach((tr) => {\n        data = tr.call(this, data, headers);\n      });\n    }\n\n    return data;\n  }\n};\n\nconst makeAxiosInstance = (customRequestConfig?: AxiosRequestConfig) => {\n  customRequestConfig = customRequestConfig || {};\n  const axiosInstance = axios.create({\n    ...baseRequestConfig,\n    ...customRequestConfig,\n    headers: {}\n  });\n\n  axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {\n    // Apply header deletions requested via req.deleteHeader() in pre-request scripts.\n    const modConfig = config as ModifiedInternalAxiosRequestConfig;\n    const headersToDelete = modConfig.__headersToDelete;\n    if (headersToDelete && Array.isArray(headersToDelete)) {\n      headersToDelete.forEach((headerName: string) => {\n        const lower = headerName.toLowerCase();\n        if (lower === 'host' || lower === 'connection') return;\n        // Using set(name, null) rather than delete(): the axios http adapter guards its\n        // own defaults (User-Agent, Accept-Encoding) with set(..., false) which only\n        // skips writing when the key already exists. delete() removes the key entirely,\n        // so the guard misses and the adapter re-adds the default. null keeps the key\n        // present (blocking the guard) while toJSON() omits null values from the wire.\n        config.headers.set(headerName, null);\n      });\n      delete modConfig.__headersToDelete;\n    }\n\n    const modifiedConfig: ModifiedInternalAxiosRequestConfig = {\n      ...config,\n      startTime: Date.now()\n    };\n    return modifiedConfig;\n  });\n\n  axiosInstance.interceptors.response.use((response: AxiosResponse) => {\n    const config = response.config as ModifiedInternalAxiosRequestConfig;\n    const startTime = config.startTime;\n    const endTime = Date.now();\n    const modifiedResponse: ModifiedAxiosResponse = {\n      ...response,\n      responseTime: endTime - startTime\n    };\n    return modifiedResponse;\n  });\n\n  return axiosInstance;\n};\n\nexport {\n  makeAxiosInstance\n};\n"
  },
  {
    "path": "packages/bruno-requests/src/network/index.ts",
    "content": "export { makeAxiosInstance } from './axios-instance';\n\nexport { getSystemProxy } from './system-proxy';\n"
  },
  {
    "path": "packages/bruno-requests/src/network/system-proxy/index.spec.js",
    "content": "const { getSystemProxy, SystemProxyResolver } = require('./index');\nconst os = require('node:os');\n\n// Mock dependencies\njest.mock('node:os');\njest.mock('./utils/windows');\njest.mock('./utils/macos');\njest.mock('./utils/linux');\n\ndescribe('SystemProxyResolver Integration', () => {\n  let detector;\n  let originalEnv;\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n    jest.useFakeTimers();\n    detector = new SystemProxyResolver();\n    originalEnv = { ...process.env };\n\n    // Clear environment variables\n    delete process.env.http_proxy;\n    delete process.env.HTTP_PROXY;\n    delete process.env.https_proxy;\n    delete process.env.HTTPS_PROXY;\n    delete process.env.no_proxy;\n    delete process.env.NO_PROXY;\n  });\n\n  afterEach(() => {\n    process.env = originalEnv;\n    jest.runOnlyPendingTimers();\n    // Clear any pending timers to prevent Jest open handles\n    jest.clearAllTimers();\n    jest.useRealTimers();\n  });\n\n  describe('Environment Variables', () => {\n    it('should prioritize lowercase over uppercase variables', () => {\n      process.env.http_proxy = 'http://proxy.usebruno.com:8080';\n      process.env.HTTP_PROXY = 'http://proxy.usebruno.com:8081';\n      process.env.https_proxy = 'https://proxy.usebruno.com:8082';\n      process.env.HTTPS_PROXY = 'https://proxy.usebruno.com:8083';\n\n      const result = detector.getEnvironmentVariables();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'https://proxy.usebruno.com:8082',\n        no_proxy: null,\n        source: 'environment'\n      });\n    });\n\n    it('should fall back to uppercase when lowercase is not set', () => {\n      process.env.HTTP_PROXY = 'http://proxy.usebruno.com:8081';\n      process.env.NO_PROXY = 'localhost,127.0.0.1';\n\n      const result = detector.getEnvironmentVariables();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8081',\n        https_proxy: null,\n        no_proxy: 'localhost,127.0.0.1',\n        source: 'environment'\n      });\n    });\n\n    it('should return null values when no environment variables are set', () => {\n      const result = detector.getEnvironmentVariables();\n\n      expect(result).toEqual({\n        http_proxy: null,\n        https_proxy: null,\n        no_proxy: null,\n        source: 'environment'\n      });\n    });\n  });\n\n  describe('Platform Routing', () => {\n    it('should route to Windows detector on win32', async () => {\n      // Create a new detector instance after mocking the platform\n      os.platform.mockReturnValue('win32');\n      const testResolver = new SystemProxyResolver();\n      const { WindowsProxyResolver } = require('./utils/windows');\n      const mockDetect = jest.fn().mockResolvedValue({ source: 'windows-system' });\n      WindowsProxyResolver.mockImplementation(() => ({ detect: mockDetect }));\n\n      await testResolver.getSystemProxy();\n      expect(mockDetect).toHaveBeenCalled();\n    });\n\n    it('should route to macOS detector on darwin', async () => {\n      // Create a new detector instance after mocking the platform\n      os.platform.mockReturnValue('darwin');\n      const testResolver = new SystemProxyResolver();\n      const { MacOSProxyResolver } = require('./utils/macos');\n      const mockDetect = jest.fn().mockResolvedValue({ source: 'macos-system' });\n      MacOSProxyResolver.mockImplementation(() => ({ detect: mockDetect }));\n\n      await testResolver.getSystemProxy();\n      expect(mockDetect).toHaveBeenCalled();\n    });\n\n    it('should route to Linux detector on linux', async () => {\n      // Create a new detector instance after mocking the platform\n      os.platform.mockReturnValue('linux');\n      const testResolver = new SystemProxyResolver();\n      const { LinuxProxyResolver } = require('./utils/linux');\n      const mockDetect = jest.fn().mockResolvedValue({ source: 'linux-system' });\n      LinuxProxyResolver.mockImplementation(() => ({ detect: mockDetect }));\n\n      await testResolver.getSystemProxy();\n      expect(mockDetect).toHaveBeenCalled();\n    });\n  });\n\n  describe('Error Handling', () => {\n    it('should throw error when platform detection fails', async () => {\n      os.platform.mockReturnValue('win32');\n      const testResolver = new SystemProxyResolver();\n      const { WindowsProxyResolver } = require('./utils/windows');\n      WindowsProxyResolver.mockImplementation(() => ({\n        detect: jest.fn().mockRejectedValue(new Error('Detection failed'))\n      }));\n\n      await expect(testResolver.getSystemProxy()).rejects.toThrow('Detection failed');\n    });\n\n    it('should throw error on timeout', async () => {\n      os.platform.mockReturnValue('win32');\n      const testResolver = new SystemProxyResolver({ commandTimeoutMs: 100 });\n      const { WindowsProxyResolver } = require('./utils/windows');\n\n      // Mock a detector that throws a timeout error\n      WindowsProxyResolver.mockImplementation(() => ({\n        detect: jest.fn().mockRejectedValue(new Error('System proxy detection timeout'))\n      }));\n\n      await expect(testResolver.getSystemProxy()).rejects.toThrow('System proxy detection timeout');\n    });\n\n    it('should throw error for unsupported platform', async () => {\n      os.platform.mockReturnValue('freebsd');\n      const testResolver = new SystemProxyResolver();\n\n      await expect(testResolver.getSystemProxy()).rejects.toThrow('Unsupported platform: freebsd');\n    });\n  });\n\n  describe('getSystemProxy function', () => {\n    beforeEach(() => {\n      // Reset modules to ensure fresh imports for each test\n      jest.resetModules();\n    });\n\n    it('should merge environment variables with system proxy', async () => {\n      // Mock os.platform before requiring the module\n      jest.doMock('node:os', () => ({\n        platform: jest.fn().mockReturnValue('win32')\n      }));\n\n      const { WindowsProxyResolver } = require('./utils/windows');\n      WindowsProxyResolver.mockImplementation(() => ({\n        detect: jest.fn().mockResolvedValue({\n          http_proxy: 'http://system-proxy.usebruno.com:8080',\n          https_proxy: 'https://system-proxy.usebruno.com:8443',\n          no_proxy: 'localhost',\n          source: 'windows-system'\n        })\n      }));\n\n      process.env.http_proxy = 'http://env-proxy.usebruno.com:9090';\n\n      // Require the module after mocking\n      const { getSystemProxy: getSystemProxyFresh } = require('./index');\n      const result = await getSystemProxyFresh();\n\n      // Environment variables take priority\n      expect(result).toEqual({\n        http_proxy: 'http://env-proxy.usebruno.com:9090',\n        https_proxy: 'https://system-proxy.usebruno.com:8443',\n        no_proxy: 'localhost',\n        source: 'windows-system + environment'\n      });\n    });\n\n    it('should return only system proxy when no environment variables are set', async () => {\n      // Mock os.platform before requiring the module\n      jest.doMock('node:os', () => ({\n        platform: jest.fn().mockReturnValue('darwin')\n      }));\n\n      const { MacOSProxyResolver } = require('./utils/macos');\n      MacOSProxyResolver.mockImplementation(() => ({\n        detect: jest.fn().mockResolvedValue({\n          http_proxy: 'http://system-proxy.usebruno.com:8080',\n          https_proxy: 'https://system-proxy.usebruno.com:8443',\n          no_proxy: 'localhost',\n          source: 'macos-system'\n        })\n      }));\n\n      // Require the module after mocking\n      const { getSystemProxy: getSystemProxyFresh } = require('./index');\n      const result = await getSystemProxyFresh();\n\n      expect(result).toEqual({\n        http_proxy: 'http://system-proxy.usebruno.com:8080',\n        https_proxy: 'https://system-proxy.usebruno.com:8443',\n        no_proxy: 'localhost',\n        source: 'macos-system'\n      });\n    });\n\n    it('should fallback to environment variables when system detection fails', async () => {\n      // Mock os.platform before requiring the module\n      jest.doMock('node:os', () => ({\n        platform: jest.fn().mockReturnValue('linux')\n      }));\n\n      const { LinuxProxyResolver } = require('./utils/linux');\n      LinuxProxyResolver.mockImplementation(() => ({\n        detect: jest.fn().mockRejectedValue(new Error('Detection failed'))\n      }));\n\n      process.env.http_proxy = 'http://proxy.usebruno.com:8080';\n      process.env.https_proxy = 'https://proxy.usebruno.com:8443';\n\n      // Require the module after mocking\n      const { getSystemProxy: getSystemProxyFresh } = require('./index');\n      const result = await getSystemProxyFresh();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'https://proxy.usebruno.com:8443',\n        no_proxy: null,\n        source: 'environment'\n      });\n    });\n\n    it('should return null values when no proxy is configured', async () => {\n      // Mock os.platform before requiring the module\n      jest.doMock('node:os', () => ({\n        platform: jest.fn().mockReturnValue('darwin')\n      }));\n\n      const { MacOSProxyResolver } = require('./utils/macos');\n      MacOSProxyResolver.mockImplementation(() => ({\n        detect: jest.fn().mockResolvedValue({\n          http_proxy: null,\n          https_proxy: null,\n          no_proxy: null,\n          source: 'macos-system'\n        })\n      }));\n\n      // Require the module after mocking\n      const { getSystemProxy: getSystemProxyFresh } = require('./index');\n      const result = await getSystemProxyFresh();\n\n      expect(result).toEqual({\n        http_proxy: null,\n        https_proxy: null,\n        no_proxy: null,\n        source: 'macos-system'\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-requests/src/network/system-proxy/index.ts",
    "content": "import { platform } from 'node:os';\nimport { ProxyConfiguration, SystemProxyResolverOptions } from './types';\nimport { WindowsProxyResolver } from './utils/windows';\nimport { MacOSProxyResolver } from './utils/macos';\nimport { LinuxProxyResolver } from './utils/linux';\nimport { normalizeNoProxy, normalizeProxyUrl } from './utils/common';\n\nexport class SystemProxyResolver {\n  private osPlatform: string;\n  private commandTimeoutMs: number = 10000;\n  private activeDetection: Promise<ProxyConfiguration> | null = null;\n\n  constructor(options: SystemProxyResolverOptions = {}) {\n    this.osPlatform = platform();\n    if (options.commandTimeoutMs) {\n      this.commandTimeoutMs = options.commandTimeoutMs;\n    }\n  }\n\n  async getSystemProxy(): Promise<ProxyConfiguration> {\n    // Return active detection if already in progress\n    if (this.activeDetection) {\n      return this.activeDetection;\n    }\n\n    // Start new detection\n    this.activeDetection = this.detectSystemProxy();\n\n    try {\n      const result = await this.activeDetection;\n      return result;\n    } finally {\n      this.activeDetection = null;\n    }\n  }\n\n  private async detectSystemProxy(): Promise<ProxyConfiguration> {\n    const startTime = Date.now();\n\n    try {\n      const result = await this.detectByPlatform();\n\n      // Log slow detections\n      const detectionTime = Date.now() - startTime;\n      if (detectionTime > 5000) {\n        console.warn(`System proxy detection took ${detectionTime}ms`);\n      }\n\n      return result;\n    } catch (error) {\n      throw error;\n    }\n  }\n\n  private async detectByPlatform(): Promise<ProxyConfiguration> {\n    switch (this.osPlatform) {\n      case 'win32':\n        return await new WindowsProxyResolver().detect({ timeoutMs: this.commandTimeoutMs });\n      case 'darwin':\n        return await new MacOSProxyResolver().detect({ timeoutMs: this.commandTimeoutMs });\n      case 'linux':\n        return await new LinuxProxyResolver().detect({ timeoutMs: this.commandTimeoutMs });\n      default:\n        throw new Error(`Unsupported platform: ${this.osPlatform}`);\n    }\n  }\n\n  getEnvironmentVariables(): ProxyConfiguration {\n    const { http_proxy, HTTP_PROXY, https_proxy, HTTPS_PROXY, no_proxy, NO_PROXY, all_proxy, ALL_PROXY } = process.env;\n\n    const httpProxy = http_proxy || HTTP_PROXY || all_proxy || ALL_PROXY || '';\n    const httpsProxy = https_proxy || HTTPS_PROXY || all_proxy || ALL_PROXY || '';\n    const noProxy = no_proxy || NO_PROXY || '';\n\n    return {\n      http_proxy: httpProxy ? normalizeProxyUrl(httpProxy) : null,\n      https_proxy: httpsProxy ? normalizeProxyUrl(httpsProxy) : null,\n      no_proxy: noProxy ? normalizeNoProxy(noProxy) : null,\n      source: 'environment'\n    };\n  }\n}\n\nconst systemProxyResolver = new SystemProxyResolver();\n\nexport async function getSystemProxy(): Promise<ProxyConfiguration> {\n  const proxyEnvironmentVariables = systemProxyResolver.getEnvironmentVariables();\n\n  const hasEnvironmentProxy = proxyEnvironmentVariables.http_proxy || proxyEnvironmentVariables.https_proxy;\n\n  try {\n    const systemProxyEnvironmentVariables = await systemProxyResolver.getSystemProxy();\n\n    return {\n      http_proxy: proxyEnvironmentVariables?.http_proxy || systemProxyEnvironmentVariables?.http_proxy,\n      https_proxy: proxyEnvironmentVariables?.https_proxy || systemProxyEnvironmentVariables?.https_proxy,\n      no_proxy: proxyEnvironmentVariables?.no_proxy || systemProxyEnvironmentVariables?.no_proxy,\n      source: hasEnvironmentProxy ? `${systemProxyEnvironmentVariables?.source} + environment` : systemProxyEnvironmentVariables?.source\n    };\n  } catch (error) {\n    return proxyEnvironmentVariables;\n  }\n}\n\nexport { ProxyConfiguration } from './types';\n"
  },
  {
    "path": "packages/bruno-requests/src/network/system-proxy/types.ts",
    "content": "export interface ProxyConfiguration {\n  http_proxy?: string | null;\n  https_proxy?: string | null;\n  no_proxy?: string | null;\n  source: string;\n};\n\nexport interface ProxyResolver {\n  detect(opts?: { timeoutMs?: number }): Promise<ProxyConfiguration>;\n}\n\nexport interface SystemProxyResolverOptions {\n  commandTimeoutMs?: number;\n}\n"
  },
  {
    "path": "packages/bruno-requests/src/network/system-proxy/utils/common.spec.ts",
    "content": "import { normalizeProxyUrl, normalizeNoProxy } from './common';\n\ndescribe('normalizeProxyUrl', () => {\n  it('should add http protocol when missing', () => {\n    expect(normalizeProxyUrl('proxy.usebruno.com:8080')).toBe('http://proxy.usebruno.com:8080');\n  });\n\n  it('should not modify URL with existing protocol', () => {\n    expect(normalizeProxyUrl('http://proxy.usebruno.com:8080')).toBe('http://proxy.usebruno.com:8080');\n    expect(normalizeProxyUrl('https://proxy.usebruno.com:8443')).toBe('https://proxy.usebruno.com:8443');\n  });\n\n  it('should handle empty string', () => {\n    expect(normalizeProxyUrl('')).toBe('');\n  });\n\n  it('should handle various protocols', () => {\n    expect(normalizeProxyUrl('socks5://proxy.usebruno.com:1080')).toBe('socks5://proxy.usebruno.com:1080');\n    expect(normalizeProxyUrl('socks4://proxy.usebruno.com:1080')).toBe('socks4://proxy.usebruno.com:1080');\n  });\n\n  it('should handle URLs without port', () => {\n    expect(normalizeProxyUrl('proxy.usebruno.com')).toBe('http://proxy.usebruno.com');\n  });\n});\n\ndescribe('normalizeNoProxy', () => {\n  it('should normalize comma-separated list', () => {\n    expect(normalizeNoProxy('localhost,127.0.0.1')).toBe('localhost,127.0.0.1');\n  });\n\n  it('should convert semicolons to commas', () => {\n    expect(normalizeNoProxy('localhost;127.0.0.1')).toBe('localhost,127.0.0.1');\n  });\n\n  it('should handle mixed delimiters', () => {\n    expect(normalizeNoProxy('localhost;127.0.0.1,*.local')).toBe('localhost,127.0.0.1,*.local');\n  });\n\n  it('should trim whitespace', () => {\n    expect(normalizeNoProxy('localhost , 127.0.0.1 ; *.local')).toBe('localhost,127.0.0.1,*.local');\n  });\n\n  it('should remove empty entries', () => {\n    expect(normalizeNoProxy('localhost,,127.0.0.1')).toBe('localhost,127.0.0.1');\n    expect(normalizeNoProxy('localhost;  ;127.0.0.1')).toBe('localhost,127.0.0.1');\n  });\n\n  it('should handle null input', () => {\n    expect(normalizeNoProxy(null)).toBeNull();\n  });\n\n  it('should handle empty string', () => {\n    expect(normalizeNoProxy('')).toBeNull();\n  });\n\n  it('should handle whitespace-only string', () => {\n    expect(normalizeNoProxy('   ')).toBeNull();\n  });\n\n  it('should handle complex patterns', () => {\n    expect(normalizeNoProxy('localhost;127.0.0.1;*.local;192.168.1.0/24;<local>')).toBe('localhost,127.0.0.1,*.local,192.168.1.0/24,<local>');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-requests/src/network/system-proxy/utils/common.ts",
    "content": "import { execFile, ExecFileOptions } from 'node:child_process';\nimport { promisify } from 'node:util';\n\nexport const execFileAsync = promisify(execFile);\n\n/**\n * Safely execute a command without shell interpretation.\n * Returns stdout on success, null on error.\n *\n * @param bin - The binary/command to execute\n * @param args - Array of arguments to pass to the command\n * @param opts - ExecFileOptions (timeout, maxBuffer, etc.)\n * @returns stdout trimmed on success, null on error\n */\nexport async function safeExec(bin: string, args: string[], opts: ExecFileOptions): Promise<string | null> {\n  try {\n    const { stdout } = await execFileAsync(bin, args, opts);\n    return stdout.trim();\n  } catch {\n    return null;\n  }\n}\n\n/**\n * Normalizes a proxy URL by ensuring it includes a protocol.\n * @param proxy - The proxy URL to normalize (e.g., \"proxy.usebruno.com:8080\").\n * @param defaultProtocol - The protocol to prepend if missing (default: \"http\").\n * @returns The normalized proxy URL (e.g., \"http://proxy.usebruno.com:8080\").\n *\n * Notes:\n * - If the URL already includes a protocol (e.g., \"https://...\"), it is returned unchanged.\n * - When system proxy settings omit the protocol,\n *   this function cannot infer the original protocol and will apply the default (\"http\").\n */\n\nexport const normalizeProxyUrl = (proxy: string, defaultProtocol: string = 'http'): string => {\n  if (!proxy) return proxy;\n\n  // Check if proxy already has a protocol (must have :// after protocol name)\n  if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(proxy)) {\n    return proxy;\n  }\n\n  // Add default protocol\n  return `${defaultProtocol}://${proxy}`;\n};\n\n/**\n * Normalizes no_proxy list to comma-separated format\n * @param noProxy - The no_proxy string (e.g., \"localhost;127.0.0.1\")\n * @returns Normalized comma-separated no_proxy list (e.g., \"localhost,127.0.0.1\")\n */\nexport const normalizeNoProxy = (noProxy: string | null): string | null => {\n  if (!noProxy) return null;\n\n  const normalized = noProxy\n    .split(/[;,\\s]+/)\n    .map((s) => s.trim())\n    .filter((s) => s.length > 0)\n    .join(',');\n\n  return normalized || null;\n};\n"
  },
  {
    "path": "packages/bruno-requests/src/network/system-proxy/utils/linux.spec.ts",
    "content": "import { LinuxProxyResolver } from './linux';\n\n// Mock the entire child_process module\njest.mock('node:child_process', () => ({\n  execFile: jest.fn()\n}));\n\n// Mock the fs/promises module\njest.mock('node:fs/promises', () => ({\n  readFile: jest.fn()\n}));\n\n// Mock the fs module\njest.mock('node:fs', () => ({\n  existsSync: jest.fn()\n}));\n\n// Mock the util module\njest.mock('node:util', () => ({\n  promisify: jest.fn((fn) => fn)\n}));\n\ndescribe('LinuxProxyResolver', () => {\n  let detector: LinuxProxyResolver;\n  let mockExecFile: jest.MockedFunction<any>;\n  let mockReadFile: jest.MockedFunction<any>;\n  let mockExistsSync: jest.MockedFunction<any>;\n\n  beforeEach(() => {\n    detector = new LinuxProxyResolver();\n    const { execFile } = require('node:child_process');\n    const { readFile } = require('node:fs/promises');\n    const { existsSync } = require('node:fs');\n    mockExecFile = execFile;\n    mockReadFile = readFile;\n    mockExistsSync = existsSync;\n    jest.clearAllMocks();\n  });\n\n  describe('gsettings proxy detection', () => {\n    it('should detect manual proxy configuration', async () => {\n      const modeOutput = '\\'manual\\'';\n      const httpHostOutput = '\\'proxy.usebruno.com\\'';\n      const httpPortOutput = '8080';\n      const httpsHostOutput = '\\'secure-proxy.usebruno.com\\'';\n      const httpsPortOutput = '8443';\n      const ignoreHostsOutput = '[\\'localhost\\', \\'127.0.0.1\\']';\n\n      mockExecFile\n        .mockResolvedValueOnce({ stdout: modeOutput, stderr: '' })\n        .mockResolvedValueOnce({ stdout: httpHostOutput, stderr: '' })\n        .mockResolvedValueOnce({ stdout: httpPortOutput, stderr: '' })\n        .mockResolvedValueOnce({ stdout: httpsHostOutput, stderr: '' })\n        .mockResolvedValueOnce({ stdout: httpsPortOutput, stderr: '' })\n        .mockResolvedValueOnce({ stdout: ignoreHostsOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'http://secure-proxy.usebruno.com:8443',\n        no_proxy: 'localhost,127.0.0.1',\n        source: 'linux-system'\n      });\n    });\n\n    it('should detect identical HTTP and HTTPS proxies', async () => {\n      const modeOutput = '\\'manual\\'';\n      const httpHostOutput = '\\'proxy.usebruno.com\\'';\n      const httpPortOutput = '8080';\n      const httpsHostOutput = '\\'proxy.usebruno.com\\'';\n      const httpsPortOutput = '8080';\n      const ignoreHostsOutput = '[]';\n\n      mockExecFile\n        .mockResolvedValueOnce({ stdout: modeOutput, stderr: '' })\n        .mockResolvedValueOnce({ stdout: httpHostOutput, stderr: '' })\n        .mockResolvedValueOnce({ stdout: httpPortOutput, stderr: '' })\n        .mockResolvedValueOnce({ stdout: httpsHostOutput, stderr: '' })\n        .mockResolvedValueOnce({ stdout: httpsPortOutput, stderr: '' })\n        .mockResolvedValueOnce({ stdout: ignoreHostsOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'http://proxy.usebruno.com:8080',\n        no_proxy: null,\n        source: 'linux-system'\n      });\n    });\n\n    it('should handle non-manual proxy mode', async () => {\n      const modeOutput = '\\'auto\\'';\n\n      mockExecFile.mockResolvedValueOnce({ stdout: modeOutput, stderr: '' });\n\n      await expect(detector.detect()).rejects.toThrow('Linux proxy detection failed');\n    });\n\n    it('should handle empty ignore hosts list', async () => {\n      const modeOutput = '\\'manual\\'';\n      const httpHostOutput = '\\'proxy.usebruno.com\\'';\n      const httpPortOutput = '8080';\n      const httpsHostOutput = '\\'\\'';\n      const httpsPortOutput = '';\n      const ignoreHostsOutput = '[]';\n\n      mockExecFile\n        .mockResolvedValueOnce({ stdout: modeOutput, stderr: '' })\n        .mockResolvedValueOnce({ stdout: httpHostOutput, stderr: '' })\n        .mockResolvedValueOnce({ stdout: httpPortOutput, stderr: '' })\n        .mockResolvedValueOnce({ stdout: httpsHostOutput, stderr: '' })\n        .mockResolvedValueOnce({ stdout: httpsPortOutput, stderr: '' })\n        .mockResolvedValueOnce({ stdout: ignoreHostsOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: null,\n        no_proxy: null,\n        source: 'linux-system'\n      });\n    });\n  });\n\n  describe('/etc/environment proxy detection', () => {\n    it('should detect proxy from /etc/environment', async () => {\n      // Mock gsettings to fail\n      mockExecFile.mockImplementation(() => {\n        throw new Error('gsettings not available');\n      });\n\n      // Mock /etc/environment file\n      mockExistsSync.mockReturnValueOnce(true);\n      mockReadFile.mockResolvedValueOnce(`\nhttp_proxy=http://proxy.usebruno.com:8080\nhttps_proxy=http://proxy.usebruno.com:8080\nno_proxy=localhost,127.0.0.1\n`);\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'http://proxy.usebruno.com:8080',\n        no_proxy: 'localhost,127.0.0.1',\n        source: 'linux-system'\n      });\n    });\n  });\n\n  describe('systemd proxy detection', () => {\n    it('should detect proxy from systemd configuration', async () => {\n      mockExecFile.mockImplementation(() => {\n        throw new Error('gsettings not available');\n      });\n\n      // Mock all previous methods to fail\n      mockExistsSync.mockReturnValue(false);\n\n      // Mock systemd conf directory to exist\n      mockExistsSync.mockReturnValueOnce(true);\n\n      // Mock systemd proxy file to exist\n      mockExistsSync.mockReturnValueOnce(true);\n      mockReadFile.mockResolvedValueOnce(`\nhttp_proxy=http://proxy.usebruno.com:8080\nhttps_proxy=http://proxy.usebruno.com:8080\nno_proxy=localhost,127.0.0.1\n`);\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'http://proxy.usebruno.com:8080',\n        no_proxy: 'localhost,127.0.0.1',\n        source: 'linux-system'\n      });\n    });\n  });\n\n  describe('Error Handling', () => {\n    it('should throw error when gsettings is not available', async () => {\n      const error = new Error('gsettings: command not found');\n\n      mockExecFile.mockImplementation(() => {\n        throw error;\n      });\n\n      // Mock all file-based methods to fail\n      mockExistsSync.mockReturnValue(false);\n\n      await expect(detector.detect()).rejects.toThrow('Linux proxy detection failed');\n    });\n\n    it('should throw error when gsettings schema is not installed', async () => {\n      const error = new Error('No such schema');\n\n      mockExecFile.mockImplementation(() => {\n        throw error;\n      });\n\n      // Mock all file-based methods to fail\n      mockExistsSync.mockReturnValue(false);\n\n      await expect(detector.detect()).rejects.toThrow('Linux proxy detection failed');\n    });\n\n    it('should throw error when no proxy configuration is found', async () => {\n      mockExecFile.mockImplementation(() => {\n        throw new Error('gsettings not available');\n      });\n\n      // Mock all file-based methods to fail\n      mockExistsSync.mockReturnValue(false);\n\n      await expect(detector.detect()).rejects.toThrow('Linux proxy detection failed');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-requests/src/network/system-proxy/utils/linux.ts",
    "content": "import { ExecFileOptions } from 'node:child_process';\nimport { readFile, readdir } from 'node:fs/promises';\nimport { existsSync } from 'node:fs';\nimport { ProxyConfiguration, ProxyResolver } from '../types';\nimport { normalizeProxyUrl, normalizeNoProxy, safeExec } from './common';\n\n// Pre-compile patterns for proxy variable detection\nconst PROXY_VAR_PATTERNS = ['http_proxy', 'https_proxy', 'no_proxy', 'all_proxy', 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'ALL_PROXY']\n  .flatMap((varName) => [\n    { varName: varName.toLowerCase(), pattern: new RegExp(`^export\\\\s+${varName}\\\\s*=\\\\s*(.+)$`, 'i') },\n    { varName: varName.toLowerCase(), pattern: new RegExp(`^${varName}\\\\s*=\\\\s*(.+)$`, 'i') }\n  ]);\n\nexport class LinuxProxyResolver implements ProxyResolver {\n  async detect(opts?: { timeoutMs?: number }): Promise<ProxyConfiguration> {\n    const timeoutMs = opts?.timeoutMs ?? 10000;\n    const execOpts: ExecFileOptions = {\n      timeout: timeoutMs,\n      maxBuffer: 1024 * 1024\n    };\n\n    try {\n      // Try different proxy detection methods in order of preference\n      const detectionMethods = [\n        () => this.getGSettingsProxy(execOpts),\n        () => this.getKDEProxy(execOpts),\n        () => this.getEnvironmentFileProxy(),\n        () => this.getSystemdProxy()\n      ];\n\n      for (const method of detectionMethods) {\n        try {\n          const proxy = await method();\n          if (proxy) {\n            return proxy;\n          }\n        } catch (error) {\n          // Continue to next method if this one fails\n          continue;\n        }\n      }\n\n      throw new Error('No Linux proxy configuration found');\n    } catch (error) {\n      throw new Error(`Linux proxy detection failed: ${error instanceof Error ? error.message : String(error)}`);\n    }\n  }\n\n  private async getGSettingsProxy(execOpts: ExecFileOptions): Promise<ProxyConfiguration | null> {\n    try {\n      const mode = await safeExec('gsettings', ['get', 'org.gnome.system.proxy', 'mode'], execOpts);\n      if (mode !== '\\'manual\\'') {\n        return null;\n      }\n\n      const httpHost = await safeExec('gsettings', ['get', 'org.gnome.system.proxy.http', 'host'], execOpts);\n      const httpPort = await safeExec('gsettings', ['get', 'org.gnome.system.proxy.http', 'port'], execOpts);\n      const httpsHost = await safeExec('gsettings', ['get', 'org.gnome.system.proxy.https', 'host'], execOpts);\n      const httpsPort = await safeExec('gsettings', ['get', 'org.gnome.system.proxy.https', 'port'], execOpts);\n      const ignoreHosts = await safeExec('gsettings', ['get', 'org.gnome.system.proxy', 'ignore-hosts'], execOpts);\n\n      const cleanHttpHost = (httpHost || '').replace(/'/g, '');\n      const cleanHttpPort = httpPort || '';\n      const cleanHttpsHost = (httpsHost || '').replace(/'/g, '');\n      const cleanHttpsPort = httpsPort || '';\n      const cleanIgnoreHosts = ignoreHosts || '';\n\n      const http_proxy = cleanHttpHost && cleanHttpPort ? normalizeProxyUrl(`${cleanHttpHost}:${cleanHttpPort}`) : null;\n      const https_proxy = cleanHttpsHost && cleanHttpsPort ? normalizeProxyUrl(`${cleanHttpsHost}:${cleanHttpsPort}`) : null;\n\n      const rawNoProxy = cleanIgnoreHosts !== '[]' ? cleanIgnoreHosts.replace(/[\\[\\]']/g, '').replace(/,\\s*/g, ',') : null;\n\n      return {\n        http_proxy,\n        https_proxy,\n        no_proxy: normalizeNoProxy(rawNoProxy),\n        source: 'linux-system'\n      };\n    } catch (error) {\n      return null;\n    }\n  }\n\n  private async getKDEProxy(execOpts: ExecFileOptions): Promise<ProxyConfiguration | null> {\n    try {\n      // Check if kreadconfig5 is available and get proxy type\n      const proxyType = await safeExec('kreadconfig5', ['--group', 'Proxy Settings', '--key', 'ProxyType'], execOpts);\n\n      // ProxyType values:\n      // 0 = No proxy\n      // 1 = Manual proxy configuration\n      // 2 = Automatic proxy configuration via URL\n      // 3 = Automatic proxy detection\n      // 4 = Use system proxy configuration (environment variables)\n\n      if (proxyType !== '1') {\n        // Only handle manual proxy configuration for now\n        return null;\n      }\n\n      const httpProxy = await safeExec('kreadconfig5', ['--group', 'Proxy Settings', '--key', 'httpProxy'], execOpts);\n      const httpsProxy = await safeExec('kreadconfig5', ['--group', 'Proxy Settings', '--key', 'httpsProxy'], execOpts);\n      const noProxy = await safeExec('kreadconfig5', ['--group', 'Proxy Settings', '--key', 'NoProxyFor'], execOpts);\n\n      const cleanHttpProxy = httpProxy || '';\n      const cleanHttpsProxy = httpsProxy || '';\n      const cleanNoProxy = noProxy || '';\n\n      const http_proxy = cleanHttpProxy ? normalizeProxyUrl(cleanHttpProxy) : null;\n      const https_proxy = cleanHttpsProxy ? normalizeProxyUrl(cleanHttpsProxy) : null;\n\n      return {\n        http_proxy,\n        https_proxy,\n        no_proxy: normalizeNoProxy(cleanNoProxy || null),\n        source: 'linux-system'\n      };\n    } catch (error) {\n      return null;\n    }\n  }\n\n  private async getEnvironmentFileProxy(): Promise<ProxyConfiguration | null> {\n    try {\n      if (!existsSync('/etc/environment')) {\n        return null;\n      }\n      const content = await readFile('/etc/environment', 'utf8');\n      return this.parseProxyFromContent(content);\n    } catch (error) {\n      return null;\n    }\n  }\n\n  private async getSystemdProxy(): Promise<ProxyConfiguration | null> {\n    try {\n      const systemdConfDir = '/etc/systemd/system.conf.d';\n      if (!existsSync(systemdConfDir)) {\n        return null;\n      }\n\n      // Look for systemd proxy configuration files\n      const files = await readdir(systemdConfDir);\n      const systemdFiles = files.filter((f) => f.endsWith('.conf'));\n      let content = '';\n\n      for (const file of systemdFiles) {\n        const filePath = `${systemdConfDir}/${file}`;\n        if (existsSync(filePath)) {\n          const fileContent = await readFile(filePath, 'utf8');\n          content += fileContent + '\\n';\n        }\n      }\n\n      if (!content) {\n        return null;\n      }\n\n      return this.parseProxyFromContent(content);\n    } catch (error) {\n      return null;\n    }\n  }\n\n  private parseProxyFromContent(content: string): ProxyConfiguration | null {\n    const proxyVars = ['http_proxy', 'https_proxy', 'no_proxy', 'all_proxy', 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'ALL_PROXY'];\n    const proxies: Record<string, string> = {};\n\n    const lines = content.split('\\n');\n    for (const line of lines) {\n      const trimmedLine = line.trim();\n\n      // Skip comments and empty lines\n      if (!trimmedLine || trimmedLine.startsWith('#')) {\n        continue;\n      }\n\n      // Handle systemd Environment= and DefaultEnvironment= directives\n      const systemdEnvMatch = trimmedLine.match(/^(Default)?Environment\\s*=\\s*(.+)$/i);\n      if (systemdEnvMatch) {\n        const envVars = systemdEnvMatch[2];\n        // Parse key=value pairs from the directive (handles quoted and unquoted values)\n        const kvPairs = envVars.match(/([A-Z_]+)=(?:\"([^\"]*)\"|'([^']*)'|(\\S+))/gi);\n        if (kvPairs) {\n          for (const pair of kvPairs) {\n            const [key, ...valueParts] = pair.split('=');\n            const value = valueParts.join('=').replace(/^[\"']|[\"']$/g, '');\n            if (proxyVars.some((v) => v.toLowerCase() === key.toLowerCase())) {\n              proxies[key.toLowerCase()] = value;\n            }\n          }\n        }\n        continue;\n      }\n\n      // Handle different formats: VAR=value, export VAR=value, VAR=\"value\", etc.\n      for (const { varName, pattern } of PROXY_VAR_PATTERNS) {\n        const match = trimmedLine.match(pattern);\n        if (match) {\n          let value = match[1].trim();\n          // Remove surrounding quotes\n          value = value.replace(/^[\"']|[\"']$/g, '');\n          proxies[varName] = value;\n          break;\n        }\n      }\n    }\n\n    // Convert to ProxyConfiguration format with ALL_PROXY fallback\n    const httpProxy = proxies.http_proxy || proxies.all_proxy || null;\n    const httpsProxy = proxies.https_proxy || proxies.all_proxy || null;\n    const http_proxy = httpProxy ? normalizeProxyUrl(httpProxy) : null;\n    const https_proxy = httpsProxy ? normalizeProxyUrl(httpsProxy) : null;\n    const no_proxy = proxies.no_proxy || null;\n\n    if (http_proxy || https_proxy) {\n      return {\n        http_proxy,\n        https_proxy,\n        no_proxy: normalizeNoProxy(no_proxy),\n        source: 'linux-system'\n      };\n    }\n\n    return null;\n  }\n}\n"
  },
  {
    "path": "packages/bruno-requests/src/network/system-proxy/utils/macos.spec.ts",
    "content": "import { MacOSProxyResolver } from './macos';\n\n// Mock the entire child_process module\njest.mock('node:child_process', () => ({\n  execFile: jest.fn()\n}));\n\n// Mock the util module\njest.mock('node:util', () => ({\n  promisify: jest.fn((fn) => fn)\n}));\n\ndescribe('MacOSProxyResolver', () => {\n  let detector: MacOSProxyResolver;\n  let mockExecFile: jest.MockedFunction<any>;\n\n  beforeEach(() => {\n    detector = new MacOSProxyResolver();\n    const { execFile } = require('node:child_process');\n    mockExecFile = execFile;\n    jest.clearAllMocks();\n  });\n\n  describe('scutil proxy detection', () => {\n    it('should detect HTTP and HTTPS proxy settings', async () => {\n      const scutilOutput = `<dictionary> {\n  HTTPEnable : 1\n  HTTPPort : 8080\n  HTTPProxy : proxy.usebruno.com\n  HTTPSEnable : 1\n  HTTPSPort : 8443\n  HTTPSProxy : secure-proxy.usebruno.com\n  ExceptionsList : <array> {\n    0 : localhost\n    1 : 127.0.0.1\n  }\n  ExcludeSimpleHostnames : 1\n}`;\n\n      mockExecFile.mockResolvedValueOnce({ stdout: scutilOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'http://secure-proxy.usebruno.com:8443',\n        no_proxy: 'localhost,127.0.0.1,<local>',\n        source: 'macos-system'\n      });\n    });\n\n    it('should handle disabled proxy settings', async () => {\n      const scutilOutput = `<dictionary> {\n  HTTPEnable : 0\n  HTTPProxy : proxy.usebruno.com\n  HTTPSEnable : 0\n  HTTPSProxy : proxy.usebruno.com\n}`;\n\n      mockExecFile.mockResolvedValueOnce({ stdout: scutilOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: null,\n        https_proxy: null,\n        no_proxy: null,\n        source: 'macos-system'\n      });\n    });\n\n    it('should use default ports when not specified', async () => {\n      const scutilOutput = `<dictionary> {\n  HTTPEnable : 1\n  HTTPProxy : proxy.usebruno.com\n  HTTPSEnable : 1\n  HTTPSProxy : secure-proxy.usebruno.com\n}`;\n\n      mockExecFile.mockResolvedValueOnce({ stdout: scutilOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result.http_proxy).toBe('http://proxy.usebruno.com:80');\n      expect(result.https_proxy).toBe('http://secure-proxy.usebruno.com:443');\n    });\n\n    it('should handle only HTTP proxy enabled', async () => {\n      const scutilOutput = `<dictionary> {\n  HTTPEnable : 1\n  HTTPPort : 8080\n  HTTPProxy : proxy.usebruno.com\n  HTTPSEnable : 0\n  HTTPSProxy : secure-proxy.usebruno.com\n}`;\n\n      mockExecFile.mockResolvedValueOnce({ stdout: scutilOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: null,\n        no_proxy: null,\n        source: 'macos-system'\n      });\n    });\n\n    it('should handle only HTTPS proxy enabled', async () => {\n      const scutilOutput = `<dictionary> {\n  HTTPEnable : 0\n  HTTPProxy : proxy.usebruno.com\n  HTTPSEnable : 1\n  HTTPSPort : 8443\n  HTTPSProxy : secure-proxy.usebruno.com\n}`;\n\n      mockExecFile.mockResolvedValueOnce({ stdout: scutilOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: null,\n        https_proxy: 'http://secure-proxy.usebruno.com:8443',\n        no_proxy: null,\n        source: 'macos-system'\n      });\n    });\n\n    it('should handle empty exceptions list', async () => {\n      const scutilOutput = `<dictionary> {\n  HTTPEnable : 1\n  HTTPPort : 8080\n  HTTPProxy : proxy.usebruno.com\n  HTTPSEnable : 1\n  HTTPSPort : 8080\n  HTTPSProxy : proxy.usebruno.com\n  ExceptionsList : <array> {\n  }\n  ExcludeSimpleHostnames : 0\n}`;\n\n      mockExecFile.mockResolvedValueOnce({ stdout: scutilOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'http://proxy.usebruno.com:8080',\n        no_proxy: null,\n        source: 'macos-system'\n      });\n    });\n\n    it('should handle ExcludeSimpleHostnames without exceptions', async () => {\n      const scutilOutput = `<dictionary> {\n  HTTPEnable : 1\n  HTTPPort : 8080\n  HTTPProxy : proxy.usebruno.com\n  HTTPSEnable : 1\n  HTTPSPort : 8080\n  HTTPSProxy : proxy.usebruno.com\n  ExcludeSimpleHostnames : 1\n}`;\n\n      mockExecFile.mockResolvedValueOnce({ stdout: scutilOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'http://proxy.usebruno.com:8080',\n        no_proxy: '<local>',\n        source: 'macos-system'\n      });\n    });\n\n    it('should handle complex exceptions list', async () => {\n      const scutilOutput = `<dictionary> {\n  HTTPEnable : 1\n  HTTPPort : 8080\n  HTTPProxy : proxy.usebruno.com\n  HTTPSEnable : 1\n  HTTPSPort : 8080\n  HTTPSProxy : proxy.usebruno.com\n  ExceptionsList : <array> {\n    0 : localhost\n    1 : 127.0.0.1\n    2 : *.local\n    3 : 192.168.1.0/24\n  }\n  ExcludeSimpleHostnames : 1\n}`;\n\n      mockExecFile.mockResolvedValueOnce({ stdout: scutilOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'http://proxy.usebruno.com:8080',\n        no_proxy: 'localhost,127.0.0.1,*.local,192.168.1.0/24,<local>',\n        source: 'macos-system'\n      });\n    });\n\n    it('should handle malformed scutil output gracefully', async () => {\n      const scutilOutput = `<dictionary> {\n  HTTPEnable : 1\n  HTTPPort : 8080\n  HTTPProxy : proxy.usebruno.com\n  HTTPSEnable : 1\n  HTTPSPort : 8080\n  HTTPSProxy proxy.usebruno.com\n}`;\n\n      mockExecFile.mockResolvedValueOnce({ stdout: scutilOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: null,\n        no_proxy: null,\n        source: 'macos-system'\n      });\n    });\n  });\n\n  describe('Error Handling', () => {\n    it('should throw error when scutil command fails', async () => {\n      mockExecFile.mockRejectedValueOnce(new Error('scutil command not found'));\n\n      await expect(detector.detect()).rejects.toThrow('macOS proxy detection failed');\n    });\n\n    it('should throw error for invalid scutil output', async () => {\n      mockExecFile.mockResolvedValueOnce({ stdout: 'Invalid output format', stderr: '' });\n\n      await expect(detector.detect()).rejects.toThrow('macOS proxy detection failed');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-requests/src/network/system-proxy/utils/macos.ts",
    "content": "import { ExecFileOptions } from 'node:child_process';\nimport { ProxyConfiguration, ProxyResolver } from '../types';\nimport { normalizeProxyUrl, normalizeNoProxy, execFileAsync } from './common';\n\nexport class MacOSProxyResolver implements ProxyResolver {\n  async detect(opts?: { timeoutMs?: number }): Promise<ProxyConfiguration> {\n    const timeoutMs = opts?.timeoutMs ?? 10000;\n    const execOpts: ExecFileOptions = {\n      timeout: timeoutMs,\n      maxBuffer: 1024 * 1024\n    };\n\n    try {\n      const { stdout } = await execFileAsync('scutil', ['--proxy'], execOpts);\n      return this.parseScutilOutput(stdout);\n    } catch (error) {\n      throw new Error(`macOS proxy detection failed: ${error instanceof Error ? error.message : String(error)}`);\n    }\n  }\n\n  private parseScutilOutput(output: string): ProxyConfiguration {\n    if (typeof output !== 'string') {\n      throw new Error('Invalid scutil --proxy output');\n    }\n\n    const cleanLines = output.split('\\n')\n      .map((line) => line.trim())\n      .filter((line) => line.length > 0);\n\n    const dictStart = cleanLines.findIndex((line) => line.includes('<dictionary>'));\n    if (dictStart === -1) {\n      throw new Error('Invalid scutil --proxy output format');\n    }\n    const config = this.parseConfiguration(cleanLines, dictStart);\n    return this.buildProxyConfiguration(config);\n  }\n\n  private parseConfiguration(lines: string[], startIndex: number): Record<string, any> {\n    const config: Record<string, any> = {};\n    let i = startIndex + 1;\n\n    while (i < lines.length && !lines[i].includes('}')) {\n      const line = lines[i];\n\n      if (!line.trim()) {\n        i++;\n        continue;\n      }\n\n      const keyValueMatch = line.match(/^([^:]+)\\s*:\\s*(.+)$/);\n      if (keyValueMatch) {\n        const key = keyValueMatch[1].trim();\n        const value = keyValueMatch[2].trim();\n\n        if (value === '<array> {') {\n          // Parse array\n          const array: string[] = [];\n          i++;\n          while (i < lines.length && !lines[i].includes('}')) {\n            const arrayLine = lines[i].trim();\n            const arrayMatch = arrayLine.match(/^\\d+\\s*:\\s*(.+)$/);\n            if (arrayMatch) {\n              array.push(arrayMatch[1].trim());\n            }\n            i++;\n          }\n          config[key] = array;\n        } else if (value.match(/^\\d+$/)) {\n          config[key] = parseInt(value, 10);\n        } else {\n          config[key] = value;\n        }\n      }\n\n      i++;\n    }\n\n    return config;\n  }\n\n  private buildProxyConfiguration(config: Record<string, any>): ProxyConfiguration {\n    let http_proxy: string | null = null;\n    let https_proxy: string | null = null;\n    let no_proxy: string | null = null;\n\n    // Check HTTP proxy\n    if (config.HTTPEnable === 1 && config.HTTPProxy) {\n      const port = config.HTTPPort || 80;\n      http_proxy = normalizeProxyUrl(`${config.HTTPProxy}:${port}`);\n    }\n\n    // Check HTTPS proxy\n    if (config.HTTPSEnable === 1 && config.HTTPSProxy) {\n      const port = config.HTTPSPort || 443;\n      https_proxy = normalizeProxyUrl(`${config.HTTPSProxy}:${port}`);\n    }\n\n    // Check bypass list\n    if (config.ExceptionsList && Array.isArray(config.ExceptionsList) && config.ExceptionsList.length > 0) {\n      no_proxy = config.ExceptionsList.join(',');\n    }\n\n    // Handle \"exclude simple hostnames\" setting\n    if (config.ExcludeSimpleHostnames === 1) {\n      no_proxy = no_proxy ? `${no_proxy},<local>` : '<local>';\n    }\n\n    return {\n      http_proxy,\n      https_proxy,\n      no_proxy: normalizeNoProxy(no_proxy),\n      source: 'macos-system'\n    };\n  }\n}\n"
  },
  {
    "path": "packages/bruno-requests/src/network/system-proxy/utils/windows.spec.ts",
    "content": "import { WindowsProxyResolver } from './windows';\n\n// Mock the entire child_process module\njest.mock('node:child_process', () => ({\n  execFile: jest.fn()\n}));\n\n// Mock the util module\njest.mock('node:util', () => ({\n  promisify: jest.fn((fn) => fn)\n}));\n\ndescribe('WindowsProxyResolver', () => {\n  let detector: WindowsProxyResolver;\n  let mockExecFile: jest.MockedFunction<any>;\n\n  beforeEach(() => {\n    detector = new WindowsProxyResolver();\n    const { execFile } = require('node:child_process');\n    mockExecFile = execFile;\n    jest.clearAllMocks();\n  });\n\n  describe('Internet Options Registry Detection', () => {\n    it('should detect single proxy configuration', async () => {\n      const regOutput = `\nHKEY_CURRENT_USER\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Internet Settings\n    ProxyEnable    REG_DWORD    0x1\n    ProxyServer    REG_SZ    proxy.usebruno.com:8080\n    ProxyOverride    REG_SZ    localhost;127.0.0.1\n`;\n\n      mockExecFile.mockResolvedValueOnce({ stdout: regOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'http://proxy.usebruno.com:8080',\n        no_proxy: 'localhost,127.0.0.1',\n        source: 'windows-system'\n      });\n    });\n\n    it('should detect protocol-specific proxy configuration', async () => {\n      const regOutput = `\nHKEY_CURRENT_USER\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Internet Settings\n    ProxyEnable    REG_DWORD    0x1\n    ProxyServer    REG_SZ    http=proxy.usebruno.com:8080;https=proxy.usebruno.com:8443\n`;\n\n      mockExecFile.mockResolvedValueOnce({ stdout: regOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'http://proxy.usebruno.com:8443',\n        no_proxy: null,\n        source: 'windows-system'\n      });\n    });\n\n    it('should fallback to WinHTTP when registry fails', async () => {\n      const winhttpOutput = `\nCurrent WinHTTP proxy settings:\n    Proxy Server(s) :  proxy.usebruno.com:8080\n    Bypass List     :  localhost\n`;\n\n      mockExecFile\n        .mockRejectedValueOnce(new Error('Registry access denied'))\n        .mockResolvedValueOnce({ stdout: winhttpOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'http://proxy.usebruno.com:8080',\n        no_proxy: 'localhost',\n        source: 'windows-system'\n      });\n    });\n  });\n\n  describe('WinHTTP Detection', () => {\n    it('should handle direct access configuration', async () => {\n      mockExecFile\n        .mockRejectedValueOnce(new Error('Registry not accessible'))\n        .mockResolvedValueOnce({ stdout: 'Direct access (no proxy server)', stderr: '' });\n\n      await expect(detector.detect()).rejects.toThrow('Windows proxy detection failed');\n    });\n\n    it('should detect single proxy from WinHTTP', async () => {\n      const winhttpOutput = `\nCurrent WinHTTP proxy settings:\n    Proxy Server(s) :  proxy.usebruno.com:8080\n    Bypass List     :  localhost;127.0.0.1\n`;\n\n      mockExecFile\n        .mockRejectedValueOnce(new Error('Registry access denied'))\n        .mockResolvedValueOnce({ stdout: winhttpOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'http://proxy.usebruno.com:8080',\n        no_proxy: 'localhost,127.0.0.1',\n        source: 'windows-system'\n      });\n    });\n\n    it('should detect protocol-specific proxy from WinHTTP', async () => {\n      const winhttpOutput = `\nCurrent WinHTTP proxy settings:\n    Proxy Server(s) :  http=proxy.usebruno.com:8080;https=proxy.usebruno.com:8443\n    Bypass List     :  localhost\n`;\n\n      mockExecFile\n        .mockRejectedValueOnce(new Error('Registry access denied'))\n        .mockResolvedValueOnce({ stdout: winhttpOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'http://proxy.usebruno.com:8443',\n        no_proxy: 'localhost',\n        source: 'windows-system'\n      });\n    });\n\n    it('should handle WinHTTP with no bypass list', async () => {\n      const winhttpOutput = `\nCurrent WinHTTP proxy settings:\n    Proxy Server(s) :  proxy.usebruno.com:8080\n    Bypass List     :  (none)\n`;\n\n      mockExecFile\n        .mockRejectedValueOnce(new Error('Registry access denied'))\n        .mockResolvedValueOnce({ stdout: winhttpOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'http://proxy.usebruno.com:8080',\n        no_proxy: null,\n        source: 'windows-system'\n      });\n    });\n  });\n\n  describe('System Proxy Environment Detection', () => {\n    it('should detect system-wide proxy environment variables', async () => {\n      const regOutput = `\nHKEY_LOCAL_MACHINE\\\\SYSTEM\\\\CurrentControlSet\\\\Control\\\\Session Manager\\\\Environment\n    HTTP_PROXY    REG_SZ    http://proxy.usebruno.com:8080\n    HTTPS_PROXY    REG_SZ    http://proxy.usebruno.com:8080\n    NO_PROXY    REG_SZ    localhost,127.0.0.1\n`;\n\n      mockExecFile\n        .mockRejectedValueOnce(new Error('Internet Options not accessible'))\n        .mockRejectedValueOnce(new Error('WinHTTP not accessible'))\n        .mockResolvedValueOnce({ stdout: regOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'http://proxy.usebruno.com:8080',\n        no_proxy: 'localhost,127.0.0.1',\n        source: 'windows-system'\n      });\n    });\n\n    it('should handle only HTTP_PROXY in system environment', async () => {\n      const regOutput = `\nHKEY_LOCAL_MACHINE\\\\SYSTEM\\\\CurrentControlSet\\\\Control\\\\Session Manager\\\\Environment\n    HTTP_PROXY    REG_SZ    http://proxy.usebruno.com:8080\n`;\n\n      mockExecFile\n        .mockRejectedValueOnce(new Error('Internet Options not accessible'))\n        .mockRejectedValueOnce(new Error('WinHTTP not accessible'))\n        .mockResolvedValueOnce({ stdout: regOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: null,\n        no_proxy: null,\n        source: 'windows-system'\n      });\n    });\n  });\n\n  describe('User Environment Proxy Detection', () => {\n    it('should detect proxy from HKCU\\\\Environment', async () => {\n      const regOutput = `\nHKEY_CURRENT_USER\\\\Environment\n    HTTP_PROXY    REG_SZ    http://proxy.usebruno.com:8080\n    HTTPS_PROXY    REG_SZ    http://proxy.usebruno.com:8080\n    NO_PROXY    REG_SZ    localhost,127.0.0.1\n`;\n\n      mockExecFile\n        .mockRejectedValueOnce(new Error('Internet Options not accessible'))\n        .mockRejectedValueOnce(new Error('WinHTTP not accessible'))\n        .mockRejectedValueOnce(new Error('System environment not accessible'))\n        .mockResolvedValueOnce({ stdout: regOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'http://proxy.usebruno.com:8080',\n        no_proxy: 'localhost,127.0.0.1',\n        source: 'windows-system'\n      });\n    });\n  });\n\n  describe('Edge Cases and Parsing', () => {\n    it('should handle proxy server with existing http:// prefix', async () => {\n      const regOutput = `\nHKEY_CURRENT_USER\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Internet Settings\n    ProxyEnable    REG_DWORD    0x1\n    ProxyServer    REG_SZ    http://proxy.usebruno.com:8080\n    ProxyOverride    REG_SZ    localhost;127.0.0.1\n`;\n\n      mockExecFile.mockResolvedValueOnce({ stdout: regOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'http://proxy.usebruno.com:8080',\n        no_proxy: 'localhost,127.0.0.1',\n        source: 'windows-system'\n      });\n    });\n\n    it('should handle protocol-specific proxies with existing prefixes', async () => {\n      const regOutput = `\nHKEY_CURRENT_USER\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Internet Settings\n    ProxyEnable    REG_DWORD    0x1\n    ProxyServer    REG_SZ    http=http://proxy.usebruno.com:8080;https=https://secure-proxy.usebruno.com:8443\n`;\n\n      mockExecFile.mockResolvedValueOnce({ stdout: regOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'https://secure-proxy.usebruno.com:8443',\n        no_proxy: null,\n        source: 'windows-system'\n      });\n    });\n\n    it('should handle empty proxy override', async () => {\n      const regOutput = `\nHKEY_CURRENT_USER\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Internet Settings\n    ProxyEnable    REG_DWORD    0x1\n    ProxyServer    REG_SZ    proxy.usebruno.com:8080\n    ProxyOverride    REG_SZ    \n`;\n\n      mockExecFile.mockResolvedValueOnce({ stdout: regOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'http://proxy.usebruno.com:8080',\n        no_proxy: null,\n        source: 'windows-system'\n      });\n    });\n\n    it('should handle proxy disabled in registry', async () => {\n      const regOutput = `\nHKEY_CURRENT_USER\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Internet Settings\n    ProxyEnable    REG_DWORD    0x0\n    ProxyServer    REG_SZ    proxy.usebruno.com:8080\n    ProxyOverride    REG_SZ    localhost;127.0.0.1\n`;\n\n      mockExecFile.mockResolvedValueOnce({ stdout: regOutput, stderr: '' });\n\n      await expect(detector.detect()).rejects.toThrow('Windows proxy detection failed');\n    });\n\n    it('should handle decimal ProxyEnable value', async () => {\n      const regOutput = `\nHKEY_CURRENT_USER\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Internet Settings\n    ProxyEnable    REG_DWORD    1\n    ProxyServer    REG_SZ    proxy.usebruno.com:8080\n    ProxyOverride    REG_SZ    localhost;127.0.0.1\n`;\n\n      mockExecFile.mockResolvedValueOnce({ stdout: regOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'http://proxy.usebruno.com:8080',\n        no_proxy: 'localhost,127.0.0.1',\n        source: 'windows-system'\n      });\n    });\n\n    it('should handle decimal ProxyEnable disabled', async () => {\n      const regOutput = `\nHKEY_CURRENT_USER\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Internet Settings\n    ProxyEnable    REG_DWORD    0\n    ProxyServer    REG_SZ    proxy.usebruno.com:8080\n    ProxyOverride    REG_SZ    localhost;127.0.0.1\n`;\n\n      mockExecFile.mockResolvedValueOnce({ stdout: regOutput, stderr: '' });\n\n      await expect(detector.detect()).rejects.toThrow('Windows proxy detection failed');\n    });\n\n    it('should handle malformed registry output gracefully', async () => {\n      const regOutput = `\nHKEY_CURRENT_USER\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Internet Settings\n    ProxyEnable    REG_DWORD    0x1\n    ProxyServer    REG_SZ    proxy.usebruno.com:8080\n    ProxyOverride    REG_SZ    localhost;127.0.0.1\n    SomeOtherValue    REG_SZ    ignored\n`;\n\n      mockExecFile.mockResolvedValueOnce({ stdout: regOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'http://proxy.usebruno.com:8080',\n        no_proxy: 'localhost,127.0.0.1',\n        source: 'windows-system'\n      });\n    });\n\n    it('should handle complex bypass list', async () => {\n      const regOutput = `\nHKEY_CURRENT_USER\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Internet Settings\n    ProxyEnable    REG_DWORD    0x1\n    ProxyServer    REG_SZ    proxy.usebruno.com:8080\n    ProxyOverride    REG_SZ    localhost;127.0.0.1;*.local;192.168.1.0/24\n`;\n\n      mockExecFile.mockResolvedValueOnce({ stdout: regOutput, stderr: '' });\n\n      const result = await detector.detect();\n\n      expect(result).toEqual({\n        http_proxy: 'http://proxy.usebruno.com:8080',\n        https_proxy: 'http://proxy.usebruno.com:8080',\n        no_proxy: 'localhost,127.0.0.1,*.local,192.168.1.0/24',\n        source: 'windows-system'\n      });\n    });\n  });\n\n  describe('Error Handling', () => {\n    it('should throw error when no proxy configuration is found', async () => {\n      mockExecFile.mockRejectedValue(new Error('Command failed'));\n\n      await expect(detector.detect()).rejects.toThrow('Windows proxy detection failed');\n    });\n\n    it('should handle registry access denied gracefully', async () => {\n      mockExecFile.mockRejectedValue(new Error('Access is denied'));\n\n      await expect(detector.detect()).rejects.toThrow('Windows proxy detection failed');\n    });\n\n    it('should handle malformed WinHTTP output', async () => {\n      mockExecFile\n        .mockRejectedValueOnce(new Error('Registry not accessible'))\n        .mockResolvedValueOnce({ stdout: 'Malformed WinHTTP output', stderr: '' });\n\n      await expect(detector.detect()).rejects.toThrow('Windows proxy detection failed');\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-requests/src/network/system-proxy/utils/windows.ts",
    "content": "import { ExecFileOptions } from 'node:child_process';\nimport { ProxyConfiguration, ProxyResolver } from '../types';\nimport { normalizeProxyUrl, normalizeNoProxy, safeExec } from './common';\n\nexport class WindowsProxyResolver implements ProxyResolver {\n  async detect(opts?: { timeoutMs?: number }): Promise<ProxyConfiguration> {\n    const timeoutMs = opts?.timeoutMs ?? 10000;\n    const execOpts: ExecFileOptions = {\n      timeout: timeoutMs,\n      windowsHide: true,\n      maxBuffer: 1024 * 1024\n    };\n\n    try {\n      // Try different detection methods in order of preference\n      const detectionMethods = [\n        () => this.getInternetOptions(execOpts),\n        () => this.getWinHttpProxy(execOpts),\n        () => this.getSystemProxyEnvironment(execOpts),\n        () => this.getUserEnvironmentProxy(execOpts)\n      ];\n\n      for (const method of detectionMethods) {\n        try {\n          const proxy = await method();\n          if (proxy) {\n            return proxy;\n          }\n        } catch (error) {\n          // Continue to next method if this one fails\n          continue;\n        }\n      }\n\n      throw new Error('No Windows proxy configuration found');\n    } catch (error) {\n      throw new Error(`Windows proxy detection failed: ${error instanceof Error ? error.message : String(error)}`);\n    }\n  }\n\n  private async getInternetOptions(execOpts: ExecFileOptions): Promise<ProxyConfiguration | null> {\n    const stdout = await safeExec('reg', ['query', 'HKCU\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Internet Settings'], execOpts);\n    if (!stdout) return null;\n\n    const lines = stdout.split('\\n');\n    let proxyEnabled = false;\n    let proxyServer: string | null = null;\n    let proxyOverride: string | null = null;\n\n    for (const line of lines) {\n      const trimmedLine = line.trim();\n\n      if (trimmedLine.includes('ProxyEnable') && trimmedLine.includes('REG_DWORD')) {\n        // Extract the value after REG_DWORD\n        const match = trimmedLine.match(/ProxyEnable\\s+REG_DWORD\\s+(0x[0-9a-fA-F]+|\\d+)/);\n        if (match) {\n          const value = match[1];\n          proxyEnabled = value === '0x1' || value === '1';\n        }\n      }\n\n      if (trimmedLine.includes('ProxyServer') && trimmedLine.includes('REG_SZ')) {\n        const match = trimmedLine.match(/ProxyServer\\s+REG_SZ\\s+(.+)/);\n        if (match) proxyServer = match[1].trim();\n      }\n\n      if (trimmedLine.includes('ProxyOverride') && trimmedLine.includes('REG_SZ')) {\n        const match = trimmedLine.match(/ProxyOverride\\s+REG_SZ\\s+(.+)/);\n        if (match) proxyOverride = match[1].trim();\n      }\n    }\n\n    if (proxyEnabled && proxyServer) {\n      return this.parseProxyString(proxyServer, proxyOverride);\n    }\n\n    return null;\n  }\n\n  private async getWinHttpProxy(execOpts: ExecFileOptions): Promise<ProxyConfiguration | null> {\n    const stdout = await safeExec('netsh', ['winhttp', 'show', 'proxy'], execOpts);\n    if (!stdout) return null;\n\n    if (stdout.includes('Direct access (no proxy server)')) {\n      return null;\n    }\n\n    const proxyServerMatch = stdout.match(/Proxy Server\\(s\\)\\s*:\\s*(.+)/);\n    const bypassListMatch = stdout.match(/Bypass List\\s*:\\s*(.+)/);\n\n    if (proxyServerMatch) {\n      const proxyServer = proxyServerMatch[1].trim();\n      const bypassList = bypassListMatch ? bypassListMatch[1].trim() : '';\n\n      return this.parseProxyString(proxyServer, bypassList);\n    }\n\n    return null;\n  }\n\n  private async getSystemProxyEnvironment(execOpts: ExecFileOptions): Promise<ProxyConfiguration | null> {\n    // Check for system-wide proxy environment variables\n    const stdout = await safeExec('reg', ['query', 'HKLM\\\\SYSTEM\\\\CurrentControlSet\\\\Control\\\\Session Manager\\\\Environment'], execOpts);\n    if (!stdout) return null;\n\n    const lines = stdout.split('\\n');\n    let http_proxy: string | null = null;\n    let https_proxy: string | null = null;\n    let no_proxy: string | null = null;\n\n    for (const line of lines) {\n      const trimmedLine = line.trim();\n\n      if (trimmedLine.toUpperCase().includes('HTTP_PROXY') && trimmedLine.includes('REG_SZ')) {\n        const match = trimmedLine.match(/HTTP_PROXY\\s+REG_SZ\\s+(.+)/i);\n        if (match) http_proxy = match[1].trim();\n      }\n\n      if (trimmedLine.toUpperCase().includes('HTTPS_PROXY') && trimmedLine.includes('REG_SZ')) {\n        const match = trimmedLine.match(/HTTPS_PROXY\\s+REG_SZ\\s+(.+)/i);\n        if (match) https_proxy = match[1].trim();\n      }\n\n      if (trimmedLine.toUpperCase().includes('NO_PROXY') && trimmedLine.includes('REG_SZ')) {\n        const match = trimmedLine.match(/NO_PROXY\\s+REG_SZ\\s+(.+)/i);\n        if (match) no_proxy = match[1].trim();\n      }\n    }\n\n    if (http_proxy || https_proxy) {\n      return {\n        http_proxy: http_proxy ? normalizeProxyUrl(http_proxy) : null,\n        https_proxy: https_proxy ? normalizeProxyUrl(https_proxy) : null,\n        no_proxy: no_proxy ? normalizeNoProxy(no_proxy) : null,\n        source: 'windows-system'\n      };\n    }\n\n    return null;\n  }\n\n  private async getUserEnvironmentProxy(execOpts: ExecFileOptions): Promise<ProxyConfiguration | null> {\n    // Check for user-specific proxy environment variables in HKCU\\Environment\n    const stdout = await safeExec('reg', ['query', 'HKCU\\\\Environment'], execOpts);\n    if (!stdout) return null;\n\n    const lines = stdout.split('\\n');\n    let http_proxy: string | null = null;\n    let https_proxy: string | null = null;\n    let no_proxy: string | null = null;\n\n    for (const line of lines) {\n      const trimmedLine = line.trim();\n\n      if (trimmedLine.toUpperCase().includes('HTTP_PROXY') && trimmedLine.includes('REG_SZ')) {\n        const match = trimmedLine.match(/HTTP_PROXY\\s+REG_SZ\\s+(.+)/i);\n        if (match) http_proxy = match[1].trim();\n      }\n\n      if (trimmedLine.toUpperCase().includes('HTTPS_PROXY') && trimmedLine.includes('REG_SZ')) {\n        const match = trimmedLine.match(/HTTPS_PROXY\\s+REG_SZ\\s+(.+)/i);\n        if (match) https_proxy = match[1].trim();\n      }\n\n      if (trimmedLine.toUpperCase().includes('NO_PROXY') && trimmedLine.includes('REG_SZ')) {\n        const match = trimmedLine.match(/NO_PROXY\\s+REG_SZ\\s+(.+)/i);\n        if (match) no_proxy = match[1].trim();\n      }\n    }\n\n    if (http_proxy || https_proxy) {\n      return {\n        http_proxy: http_proxy ? normalizeProxyUrl(http_proxy) : null,\n        https_proxy: https_proxy ? normalizeProxyUrl(https_proxy) : null,\n        no_proxy: no_proxy ? normalizeNoProxy(no_proxy) : null,\n        source: 'windows-system'\n      };\n    }\n\n    return null;\n  }\n\n  private parseProxyString(proxyServer: string, bypassList: string | null): ProxyConfiguration {\n    let http_proxy: string | null = null;\n    let https_proxy: string | null = null;\n\n    if (proxyServer.includes('=')) {\n      // Protocol-specific format: \"http=proxy1:8080;https=proxy2:8080\"\n      const protocols = proxyServer.split(';');\n      for (const protocol of protocols) {\n        const [proto, server] = protocol.split('=');\n        if (!server || !proto) continue;\n        if (proto === 'http') {\n          http_proxy = normalizeProxyUrl(server);\n        } else if (proto === 'https') {\n          https_proxy = normalizeProxyUrl(server);\n        }\n      }\n    } else {\n      // Single proxy for all protocols: \"proxy.example.com:8080\"\n      const proxy = normalizeProxyUrl(proxyServer);\n      http_proxy = proxy;\n      https_proxy = proxy;\n    }\n\n    return {\n      http_proxy,\n      https_proxy,\n      no_proxy: bypassList && bypassList !== '(none)' ? normalizeNoProxy(bypassList) : null,\n      source: 'windows-system'\n    };\n  }\n}\n"
  },
  {
    "path": "packages/bruno-requests/src/scripting/index.ts",
    "content": "export { default as sendRequest, createSendRequest } from './send-request';\n"
  },
  {
    "path": "packages/bruno-requests/src/scripting/send-request.spec.ts",
    "content": "import sendRequest, { createSendRequest } from './send-request';\n\njest.mock('../network', () => ({\n  makeAxiosInstance: jest.fn()\n}));\n\njest.mock('../utils/http-https-agents', () => ({\n  getHttpHttpsAgents: jest.fn()\n}));\n\nimport { makeAxiosInstance } from '../network';\nimport { getHttpHttpsAgents } from '../utils/http-https-agents';\n\nconst mockMakeAxiosInstance = makeAxiosInstance as jest.Mock;\nconst mockGetHttpHttpsAgents = getHttpHttpsAgents as jest.Mock;\n\ndescribe('sendRequest', () => {\n  let mockAxios: jest.Mock;\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n    mockAxios = jest.fn();\n    mockMakeAxiosInstance.mockReturnValue(mockAxios);\n    mockGetHttpHttpsAgents.mockResolvedValue({ httpAgent: null, httpsAgent: null });\n  });\n\n  describe('without callback', () => {\n    test('should return response directly', async () => {\n      const mockResponse = { data: 'test', status: 200 };\n      mockAxios.mockResolvedValue(mockResponse);\n\n      const result = await sendRequest({ url: 'http://example.com' });\n\n      expect(result).toBe(mockResponse);\n    });\n\n    test('should reject on request error', async () => {\n      const error = new Error('Network error');\n      mockAxios.mockRejectedValue(error);\n\n      await expect(sendRequest({ url: 'http://example.com' })).rejects.toThrow('Network error');\n    });\n\n    test('should handle URL string instead of config object', async () => {\n      const mockResponse = { data: 'pong', status: 200 };\n      mockAxios.mockResolvedValue(mockResponse);\n\n      const result = await sendRequest('http://example.com/ping');\n\n      expect(result).toBe(mockResponse);\n      expect(mockAxios).toHaveBeenCalledWith(\n        expect.objectContaining({\n          url: 'http://example.com/ping'\n        })\n      );\n    });\n  });\n\n  describe('with callback', () => {\n    test('should call callback with response and return response', async () => {\n      const mockResponse = { data: 'test', status: 200 };\n      mockAxios.mockResolvedValue(mockResponse);\n      const callback = jest.fn();\n\n      const result = await sendRequest({ url: 'http://example.com' }, callback);\n\n      expect(callback).toHaveBeenCalledWith(null, mockResponse);\n      expect(result).toBe(mockResponse);\n    });\n\n    test('should call callback with error on request failure', async () => {\n      const error = new Error('Network error');\n      mockAxios.mockRejectedValue(error);\n      const callback = jest.fn();\n\n      await sendRequest({ url: 'http://example.com' }, callback);\n\n      expect(callback).toHaveBeenCalledWith(error, null);\n    });\n\n    test('should reject if callback throws on success', async () => {\n      const mockResponse = { data: 'test', status: 200 };\n      mockAxios.mockResolvedValue(mockResponse);\n      const callbackError = new Error('Callback error');\n      const callback = jest.fn().mockRejectedValue(callbackError);\n\n      await expect(sendRequest({ url: 'http://example.com' }, callback)).rejects.toThrow(\n        'Callback error'\n      );\n    });\n\n    test('should reject if callback throws on error', async () => {\n      const requestError = new Error('Network error');\n      mockAxios.mockRejectedValue(requestError);\n      const callbackError = new Error('Callback error');\n      const callback = jest.fn().mockRejectedValue(callbackError);\n\n      await expect(sendRequest({ url: 'http://example.com' }, callback)).rejects.toThrow(\n        'Callback error'\n      );\n    });\n  });\n});\n\ndescribe('createSendRequest', () => {\n  let mockAxios: jest.Mock;\n\n  beforeEach(() => {\n    jest.clearAllMocks();\n    mockAxios = jest.fn();\n    mockMakeAxiosInstance.mockReturnValue(mockAxios);\n  });\n\n  test('should apply agents from config', async () => {\n    const mockHttpAgent = { name: 'httpAgent' };\n    const mockHttpsAgent = { name: 'httpsAgent' };\n    mockGetHttpHttpsAgents.mockResolvedValue({\n      httpAgent: mockHttpAgent,\n      httpsAgent: mockHttpsAgent\n    });\n    const mockResponse = { data: 'test' };\n    mockAxios.mockResolvedValue(mockResponse);\n\n    const customSendRequest = createSendRequest({ proxyConfig: {} });\n    await customSendRequest({ url: 'https://example.com' });\n\n    expect(mockGetHttpHttpsAgents).toHaveBeenCalledWith({\n      proxyConfig: {},\n      requestUrl: 'https://example.com'\n    });\n    expect(mockAxios).toHaveBeenCalledWith(\n      expect.objectContaining({\n        httpAgent: mockHttpAgent,\n        httpsAgent: mockHttpsAgent\n      })\n    );\n  });\n\n  test('should not override agents if already set in requestConfig', async () => {\n    const configHttpAgent = { name: 'configAgent' };\n    const configHttpsAgent = { name: 'configHttpsAgent' };\n    mockGetHttpHttpsAgents.mockResolvedValue({\n      httpAgent: { name: 'ignored' },\n      httpsAgent: { name: 'ignored' }\n    });\n    mockAxios.mockResolvedValue({ data: 'test' });\n\n    const customSendRequest = createSendRequest({ proxyConfig: {} });\n    await customSendRequest({\n      url: 'https://example.com',\n      httpAgent: configHttpAgent,\n      httpsAgent: configHttpsAgent\n    });\n\n    expect(mockAxios).toHaveBeenCalledWith(\n      expect.objectContaining({\n        httpAgent: configHttpAgent,\n        httpsAgent: configHttpsAgent\n      })\n    );\n  });\n\n  test('should not call getHttpHttpsAgents when no config provided', async () => {\n    mockAxios.mockResolvedValue({ data: 'test' });\n\n    const customSendRequest = createSendRequest();\n    await customSendRequest({ url: 'https://example.com' });\n\n    expect(mockGetHttpHttpsAgents).not.toHaveBeenCalled();\n  });\n\n  test('should handle URL string and apply agents from config', async () => {\n    const mockHttpAgent = { name: 'httpAgent' };\n    const mockHttpsAgent = { name: 'httpsAgent' };\n    mockGetHttpHttpsAgents.mockResolvedValue({\n      httpAgent: mockHttpAgent,\n      httpsAgent: mockHttpsAgent\n    });\n    const mockResponse = { data: 'pong' };\n    mockAxios.mockResolvedValue(mockResponse);\n\n    const customSendRequest = createSendRequest({ collectionPath: '/test' });\n    const result = await customSendRequest('https://example.com/ping');\n\n    expect(result).toBe(mockResponse);\n    expect(mockGetHttpHttpsAgents).toHaveBeenCalledWith({\n      collectionPath: '/test',\n      requestUrl: 'https://example.com/ping'\n    });\n    expect(mockAxios).toHaveBeenCalledWith(\n      expect.objectContaining({\n        url: 'https://example.com/ping',\n        httpAgent: mockHttpAgent,\n        httpsAgent: mockHttpsAgent\n      })\n    );\n  });\n});\n"
  },
  {
    "path": "packages/bruno-requests/src/scripting/send-request.ts",
    "content": "import { AxiosRequestConfig } from 'axios';\nimport { makeAxiosInstance } from '../network';\nimport { getHttpHttpsAgents } from '../utils/http-https-agents';\nimport type { GetHttpHttpsAgentsParams } from '../utils/http-https-agents';\n\ntype T_SendRequestCallback = (error: any, response: any) => void;\n\n/**\n * Configuration for creating a sendRequest function with proxy/certs support.\n * This is the same config used by getHttpHttpsAgents, minus requestUrl which is\n * extracted from the actual request.\n */\ntype SendRequestConfig = Omit<GetHttpHttpsAgentsParams, 'requestUrl'>;\n\n/**\n * Creates a sendRequest function configured with proxy and certificate settings.\n * This allows bru.sendRequest to use the same proxy/certs config as the main request.\n *\n * @param config - Configuration for proxy, certs, and TLS options (same as getHttpHttpsAgents)\n * @returns A sendRequest function that applies the config to each request\n */\nconst createSendRequest = (config?: SendRequestConfig) => {\n  return async (requestConfig: AxiosRequestConfig | string, callback?: T_SendRequestCallback) => {\n    // Handle case where requestConfig is a URL string\n    const normalizedConfig: AxiosRequestConfig = typeof requestConfig === 'string'\n      ? { url: requestConfig }\n      : { ...requestConfig };\n\n    // If config is provided, create agents with the request URL for proper proxy bypass\n    if (config) {\n      const requestUrl = normalizedConfig.url;\n\n      const { httpAgent, httpsAgent } = await getHttpHttpsAgents({\n        ...config,\n        requestUrl\n      });\n\n      // Apply agents if not explicitly set in normalizedConfig\n      if (httpAgent && !normalizedConfig.httpAgent) {\n        normalizedConfig.httpAgent = httpAgent;\n      }\n      if (httpsAgent && !normalizedConfig.httpsAgent) {\n        normalizedConfig.httpsAgent = httpsAgent;\n      }\n    }\n\n    const axiosInstance = makeAxiosInstance();\n\n    if (!callback) {\n      return await axiosInstance(normalizedConfig);\n    }\n\n    try {\n      const response = await axiosInstance(normalizedConfig);\n      try {\n        await callback(null, response);\n        return response;\n      } catch (error) {\n        return Promise.reject(error);\n      }\n    } catch (error: any) {\n      // Normalize axios error for callback: tests expect error.status (e.g. 404), but axios\n      // puts the status on error.response.status. Setting status here ensures the same\n      // behaviour in nodevm (--sandbox developer, used in CI) and in QuickJS (safe sandbox).\n      const errForCallback\n        = error && typeof error.response?.status === 'number'\n          ? { ...error, status: error.response.status }\n          : error;\n      try {\n        await callback(errForCallback, null);\n      } catch (err) {\n        return Promise.reject(err);\n      }\n    }\n  };\n};\n\n// Default sendRequest without config (for backward compatibility)\nconst sendRequest = createSendRequest();\n\nexport default sendRequest;\nexport { createSendRequest };\nexport type { SendRequestConfig };\n"
  },
  {
    "path": "packages/bruno-requests/src/utils/agent-cache.spec.ts",
    "content": "import https from 'node:https';\nimport http from 'node:http';\nimport { EventEmitter } from 'node:events';\nimport { getOrCreateHttpsAgent, getOrCreateHttpAgent, clearAgentCache, getAgentCacheSize } from './agent-cache';\n\ndescribe('Agent Cache', () => {\n  beforeEach(() => {\n    clearAgentCache();\n  });\n\n  describe('getOrCreateHttpsAgent', () => {\n    it('creates a new agent when cache is empty', () => {\n      const agent = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { rejectUnauthorized: true } });\n\n      expect(agent).toBeInstanceOf(https.Agent);\n      expect(getAgentCacheSize()).toBe(1);\n    });\n\n    it('returns cached agent for identical options', () => {\n      const options = { rejectUnauthorized: true, keepAlive: true };\n\n      const agent1 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options });\n      const agent2 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options });\n\n      expect(agent1).toBe(agent2);\n      expect(getAgentCacheSize()).toBe(1);\n    });\n\n    it('creates separate agents for different rejectUnauthorized values', () => {\n      const agent1 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { rejectUnauthorized: true } });\n      const agent2 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { rejectUnauthorized: false } });\n\n      expect(agent1).not.toBe(agent2);\n      expect(getAgentCacheSize()).toBe(2);\n    });\n\n    it('creates separate agents for different CA certificates', () => {\n      const agent1 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { ca: 'cert-a' } });\n      const agent2 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { ca: 'cert-b' } });\n\n      expect(agent1).not.toBe(agent2);\n      expect(getAgentCacheSize()).toBe(2);\n    });\n\n    it('creates separate agents for different cert values', () => {\n      const agent1 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { cert: Buffer.from('cert-a') } });\n      const agent2 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { cert: Buffer.from('cert-b') } });\n\n      expect(agent1).not.toBe(agent2);\n      expect(getAgentCacheSize()).toBe(2);\n    });\n\n    it('creates separate agents for different key values', () => {\n      const agent1 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { key: Buffer.from('key-a') } });\n      const agent2 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { key: Buffer.from('key-b') } });\n\n      expect(agent1).not.toBe(agent2);\n      expect(getAgentCacheSize()).toBe(2);\n    });\n\n    it('creates separate agents for different pfx values', () => {\n      const agent1 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { pfx: Buffer.from('pfx-a') } });\n      const agent2 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { pfx: Buffer.from('pfx-b') } });\n\n      expect(agent1).not.toBe(agent2);\n      expect(getAgentCacheSize()).toBe(2);\n    });\n\n    it('creates separate agents for different passphrase values', () => {\n      const agent1 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { passphrase: 'pass-a' } });\n      const agent2 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { passphrase: 'pass-b' } });\n\n      expect(agent1).not.toBe(agent2);\n      expect(getAgentCacheSize()).toBe(2);\n    });\n\n    it('creates separate agents for different proxy URIs', () => {\n      const options = { rejectUnauthorized: true };\n\n      const agent1 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options, proxyUri: 'http://proxy1:8080' });\n      const agent2 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options, proxyUri: 'http://proxy2:8080' });\n\n      expect(agent1).not.toBe(agent2);\n      expect(getAgentCacheSize()).toBe(2);\n    });\n\n    it('creates separate agents for different agent classes', () => {\n      const options = { keepAlive: true };\n\n      const httpsAgent = getOrCreateHttpsAgent({ AgentClass: https.Agent, options });\n      const httpAgent = getOrCreateHttpsAgent({ AgentClass: http.Agent, options });\n\n      expect(httpsAgent).not.toBe(httpAgent);\n      expect(getAgentCacheSize()).toBe(2);\n    });\n\n    it('creates separate agents for different keepAlive values', () => {\n      const agent1 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { keepAlive: true } });\n      const agent2 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { keepAlive: false } });\n\n      expect(agent1).not.toBe(agent2);\n      expect(getAgentCacheSize()).toBe(2);\n    });\n\n    it('creates separate agents for different hostnames', () => {\n      const options = { rejectUnauthorized: true };\n\n      const agent1 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options, hostname: 'api.example.com' });\n      const agent2 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options, hostname: 'auth.example.com' });\n\n      expect(agent1).not.toBe(agent2);\n      expect(getAgentCacheSize()).toBe(2);\n    });\n\n    it('returns cached agent for the same hostname', () => {\n      const options = { rejectUnauthorized: true };\n\n      const agent1 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options, hostname: 'api.example.com' });\n      const agent2 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options, hostname: 'api.example.com' });\n\n      expect(agent1).toBe(agent2);\n      expect(getAgentCacheSize()).toBe(1);\n    });\n\n    it('creates separate agents for null hostname vs explicit hostname', () => {\n      const options = { rejectUnauthorized: true };\n\n      const agent1 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options, hostname: null });\n      const agent2 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options, hostname: 'api.example.com' });\n\n      expect(agent1).not.toBe(agent2);\n      expect(getAgentCacheSize()).toBe(2);\n    });\n  });\n\n  describe('timeline support', () => {\n    it('does not add timeline when none is provided', () => {\n      const agent = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: {} }) as any;\n\n      expect(agent.timeline).toBeUndefined();\n    });\n\n    it('uses provided timeline array', () => {\n      const timeline: any[] = [];\n      const agent = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: {}, timeline }) as any;\n\n      expect(agent.timeline).toBe(timeline);\n    });\n\n    it('updates timeline reference on cached agents', () => {\n      const timeline1: any[] = [];\n      const timeline2: any[] = [];\n\n      const agent1 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: {}, timeline: timeline1 }) as any;\n      expect(agent1.timeline).toBe(timeline1);\n\n      const agent2 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: {}, timeline: timeline2 }) as any;\n      expect(agent1).toBe(agent2);\n      expect(agent2.timeline).toBe(timeline2);\n    });\n\n    it('logs when reusing a cached HTTPS agent', () => {\n      const timeline1: any[] = [];\n      const timeline2: any[] = [];\n\n      // First call creates new agent - no reuse message\n      getOrCreateHttpsAgent({ AgentClass: https.Agent, options: {}, timeline: timeline1 });\n      expect(timeline1.some((e) => e.message.includes('Reusing cached https agent'))).toBe(false);\n\n      // Second call reuses cached agent - should log reuse message with SSL session reuse\n      getOrCreateHttpsAgent({ AgentClass: https.Agent, options: {}, timeline: timeline2 });\n      expect(timeline2.some((e) => e.message.includes('Reusing cached https agent'))).toBe(true);\n    });\n\n    it('logs when reusing a cached HTTP agent', () => {\n      const timeline1: any[] = [];\n      const timeline2: any[] = [];\n\n      // First call creates new agent - no reuse message\n      getOrCreateHttpAgent({ AgentClass: http.Agent, options: { keepAlive: true }, timeline: timeline1 });\n      expect(timeline1.some((e) => e.message.includes('Reusing cached http agent'))).toBe(false);\n\n      // Second call reuses cached agent - should log reuse message with connection reuse\n      getOrCreateHttpAgent({ AgentClass: http.Agent, options: { keepAlive: true }, timeline: timeline2 });\n      expect(timeline2.some((e) => e.message.includes('Reusing cached http agent'))).toBe(true);\n    });\n\n    it('logs SSL validation status on agent creation', () => {\n      const timeline: any[] = [];\n      getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { rejectUnauthorized: true }, timeline });\n\n      const sslEntry = timeline.find((e) => e.message.includes('SSL validation'));\n      expect(sslEntry).toBeDefined();\n      expect(sslEntry.message).toContain('enabled');\n    });\n\n    it('logs SSL validation disabled when rejectUnauthorized is false', () => {\n      const timeline: any[] = [];\n      getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { rejectUnauthorized: false }, timeline });\n\n      const sslEntry = timeline.find((e) => e.message.includes('SSL validation'));\n      expect(sslEntry).toBeDefined();\n      expect(sslEntry.message).toContain('disabled');\n    });\n  });\n\n  describe('clearAgentCache', () => {\n    it('removes all cached agents', () => {\n      getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { rejectUnauthorized: true } });\n      getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { rejectUnauthorized: false } });\n      expect(getAgentCacheSize()).toBe(2);\n\n      clearAgentCache();\n      expect(getAgentCacheSize()).toBe(0);\n    });\n\n    it('destroys all agents when clearing cache', () => {\n      const destroyMocks: jest.Mock[] = [];\n\n      // Create several agents and attach mock destroy functions\n      for (let i = 0; i < 5; i++) {\n        const agent = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { ca: `cert-${i}` } }) as any;\n        const mock = jest.fn();\n        agent.destroy = mock;\n        destroyMocks.push(mock);\n      }\n\n      expect(getAgentCacheSize()).toBe(5);\n\n      clearAgentCache();\n\n      expect(getAgentCacheSize()).toBe(0);\n      // All agents should have been destroyed\n      destroyMocks.forEach((mock) => {\n        expect(mock).toHaveBeenCalled();\n      });\n    });\n  });\n\n  describe('LRU eviction', () => {\n    it('maintains cache size under limit', () => {\n      // Create many agents with different options\n      for (let i = 0; i < 150; i++) {\n        getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { ca: `cert-${i}` } });\n      }\n\n      // Cache should be capped at MAX_AGENT_CACHE_SIZE (100)\n      expect(getAgentCacheSize()).toBeLessThanOrEqual(100);\n    });\n\n    it('destroys evicted agents to prevent memory leaks', () => {\n      // Create first agent and attach a mock destroy function\n      const firstAgent = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { ca: 'cert-to-evict' } }) as any;\n      const destroyMock = jest.fn();\n      firstAgent.destroy = destroyMock;\n\n      // Fill cache to trigger eviction (100 more agents will evict the first one)\n      for (let i = 0; i < 100; i++) {\n        getOrCreateHttpsAgent({ AgentClass: https.Agent, options: { ca: `cert-${i}` } });\n      }\n\n      // First agent should have been evicted and destroyed\n      expect(destroyMock).toHaveBeenCalled();\n    });\n  });\n\n  describe('concurrent requests timeline isolation', () => {\n    it('isolates timeline events for concurrent requests using the same cached agent', () => {\n      const timeline1: any[] = [];\n      const timeline2: any[] = [];\n\n      // Get the same agent twice with different timelines (simulating concurrent requests)\n      const agent1 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: {}, timeline: timeline1 }) as any;\n      const agent2 = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: {}, timeline: timeline2 }) as any;\n\n      // Both should return the same cached agent\n      expect(agent1).toBe(agent2);\n\n      // Create mock sockets to simulate concurrent connections\n      const mockSocket1 = new EventEmitter() as any;\n      mockSocket1.remoteAddress = '1.2.3.4';\n      mockSocket1.remotePort = 443;\n      mockSocket1.getProtocol = () => 'TLSv1.3';\n      mockSocket1.getCipher = () => ({ name: 'AES-256-GCM', version: 'TLSv1.3' });\n      mockSocket1.alpnProtocol = 'h2';\n      mockSocket1.getPeerCertificate = () => ({\n        subject: { CN: 'example.com' },\n        valid_from: 'Jan 1 00:00:00 2024 GMT',\n        valid_to: 'Jan 1 00:00:00 2025 GMT'\n      });\n      mockSocket1.authorized = true;\n\n      const mockSocket2 = new EventEmitter() as any;\n      mockSocket2.remoteAddress = '5.6.7.8';\n      mockSocket2.remotePort = 443;\n      mockSocket2.getProtocol = () => 'TLSv1.3';\n      mockSocket2.getCipher = () => ({ name: 'AES-256-GCM', version: 'TLSv1.3' });\n      mockSocket2.alpnProtocol = 'http/1.1';\n      mockSocket2.getPeerCertificate = () => ({\n        subject: { CN: 'other.com' },\n        valid_from: 'Jan 1 00:00:00 2024 GMT',\n        valid_to: 'Jan 1 00:00:00 2025 GMT'\n      });\n      mockSocket2.authorized = true;\n\n      // Mock createConnection to return our mock sockets\n      const originalCreateConnection = Object.getPrototypeOf(Object.getPrototypeOf(agent1)).createConnection;\n      let callCount = 0;\n      jest.spyOn(Object.getPrototypeOf(Object.getPrototypeOf(agent1)), 'createConnection').mockImplementation(function (this: any, options: any, callback: any) {\n        callCount++;\n        return callCount === 1 ? mockSocket1 : mockSocket2;\n      });\n\n      // Simulate request 1 starting - this captures timeline1 in the closure\n      agent1.timeline = timeline1;\n      const socket1 = agent1.createConnection({ host: 'example.com', port: 443 }, () => {});\n\n      // Before request 1's events fire, request 2 starts and updates agent.timeline\n      // This simulates the race condition\n      agent1.timeline = timeline2;\n      const socket2 = agent1.createConnection({ host: 'other.com', port: 443 }, () => {});\n\n      // Now fire events for both sockets - they should go to their respective timelines\n      mockSocket1.emit('connect');\n      mockSocket1.emit('secureConnect');\n\n      mockSocket2.emit('connect');\n      mockSocket2.emit('secureConnect');\n\n      // Verify timeline1 only contains events for request 1 (example.com)\n      const timeline1Messages = timeline1.map((e) => e.message);\n      expect(timeline1Messages.some((m) => m.includes('example.com'))).toBe(true);\n      expect(timeline1Messages.some((m) => m.includes('other.com'))).toBe(false);\n\n      // Verify timeline2 only contains events for request 2 (other.com)\n      const timeline2Messages = timeline2.map((e) => e.message);\n      expect(timeline2Messages.some((m) => m.includes('other.com'))).toBe(true);\n      expect(timeline2Messages.some((m) => m.includes('example.com'))).toBe(false);\n\n      // Restore the original implementation\n      jest.restoreAllMocks();\n    });\n\n    it('logs events to captured timeline even after agent.timeline is reassigned', () => {\n      const timeline1: any[] = [];\n      const timeline2: any[] = [];\n\n      const agent = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: {}, timeline: timeline1 }) as any;\n\n      // Create a mock socket\n      const mockSocket = new EventEmitter() as any;\n      mockSocket.remoteAddress = '1.2.3.4';\n      mockSocket.remotePort = 443;\n\n      // Mock createConnection\n      jest.spyOn(Object.getPrototypeOf(Object.getPrototypeOf(agent)), 'createConnection').mockImplementation(() => mockSocket);\n\n      // Start creating connection - this captures timeline1\n      const socket = agent.createConnection({ host: 'test.com', port: 443 }, () => {});\n\n      // Reassign agent.timeline (simulating another request coming in)\n      agent.timeline = timeline2;\n\n      // Fire the connect event - this should still go to timeline1 (captured reference)\n      mockSocket.emit('connect');\n\n      // Verify event went to timeline1, not timeline2\n      expect(timeline1.some((e) => e.message.includes('Connected to test.com'))).toBe(true);\n      expect(timeline2.some((e) => e.message.includes('Connected to test.com'))).toBe(false);\n\n      jest.restoreAllMocks();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-requests/src/utils/agent-cache.ts",
    "content": "import crypto from 'node:crypto';\nimport tls from 'node:tls';\nimport type { Agent as HttpAgent } from 'node:http';\nimport type { Agent as HttpsAgent } from 'node:https';\nimport { createTimelineAgentClass, createTimelineHttpAgentClass, type TimelineEntry, type AgentOptions, type HttpAgentOptions, type AgentClass, type HttpAgentClass } from './timeline-agent';\n\n/**\n * Agent cache for SSL session reuse.\n * Agents are cached by their configuration to enable TLS session resumption,\n * which significantly reduces SSL handshake time for repeated requests.\n */\nconst agentCache = new Map<string, HttpAgent | HttpsAgent>();\n\n/**\n * Maximum number of agents to cache.\n * 100 provides a good balance between memory usage and SSL session reuse.\n * Each agent maintains persistent connections, so higher values increase memory.\n * Lower values may reduce SSL session hits for users with many different TLS configs.\n */\nconst MAX_AGENT_CACHE_SIZE = 100;\n\n/**\n * Cache for timeline-wrapped HTTPS agent classes.\n * Prevents creating new class definitions on every call.\n */\nconst timelineClassCache = new WeakMap<any, AgentClass>();\n\n/**\n * Cache for timeline-wrapped HTTP agent classes.\n * Prevents creating new class definitions on every call.\n */\nconst timelineHttpClassCache = new WeakMap<any, HttpAgentClass>();\n\n/**\n * Map to assign unique IDs to agent classes.\n * Used for cache key generation since different classes may have the same name.\n */\nconst agentClassIdMap = new WeakMap<any, number>();\nlet agentClassIdCounter = 0;\n\nfunction getAgentClassId(AgentClass: any): number {\n  if (agentClassIdMap.has(AgentClass)) {\n    return agentClassIdMap.get(AgentClass)!;\n  }\n  const id = ++agentClassIdCounter;\n  agentClassIdMap.set(AgentClass, id);\n  return id;\n}\n\n/**\n * Hash a value using SHA-256 and return a truncated hex string.\n * Truncated to 16 chars for compact cache keys while maintaining uniqueness.\n */\nfunction hashValue(value: string | Buffer | undefined): string | null {\n  if (!value) return null;\n  const data = Buffer.isBuffer(value) ? value : String(value);\n  return crypto.createHash('sha256').update(data).digest('hex').slice(0, 16);\n}\n\n/**\n * Cache for secure contexts created from CA options.\n * Keyed by the hash of the CA value to avoid creating duplicate contexts.\n */\nconst secureContextCache = new Map<string, tls.SecureContext>();\n\n/**\n * Build a TLS secure context that adds custom CAs on top of the OpenSSL defaults.\n *\n * When Node.js receives an explicit `ca` option in tls.connect() or https.Agent,\n * it replaces the default CA store entirely. This means CAs that are only in the\n * OpenSSL default trust store (e.g. /etc/ssl/cert.pem) but not in\n * tls.rootCertificates or tls.getCACertificates('system') are lost.\n *\n * This function creates a secureContext starting from the OpenSSL defaults\n * and adds custom CAs on top via addCACert(), which appends rather than replaces.\n */\nfunction buildSecureContext(ca: string | Buffer | (string | Buffer)[]): tls.SecureContext {\n  const caHash = hashCaValue(ca);\n  if (caHash && secureContextCache.has(caHash)) {\n    return secureContextCache.get(caHash)!;\n  }\n\n  const ctx = tls.createSecureContext();\n  const caList = Array.isArray(ca) ? ca : [ca];\n  for (const cert of caList) {\n    if (cert) {\n      ctx.context.addCACert(cert);\n    }\n  }\n\n  if (caHash) {\n    secureContextCache.set(caHash, ctx);\n  }\n  return ctx;\n}\n\n/**\n * Convert agent options to use a secureContext instead of raw `ca`.\n * This ensures custom CAs are added on top of the OpenSSL defaults\n * rather than replacing the default CA store.\n *\n * When client certificates (pfx/cert/key) are also present, they are loaded\n * into the secure context so they aren't silently ignored by Node.js\n * (Node.js skips pfx/cert/key/ca when a secureContext is provided).\n */\nfunction applySecureContext<T extends AgentOptions | HttpAgentOptions>(options: T): T {\n  if ('ca' in options && (options as AgentOptions).ca) {\n    const { ca, ...rest } = options as AgentOptions;\n\n    // When client certs are present alongside CA, build a combined context\n    // that includes both. This context can't be CA-cached since it's unique\n    // per client cert + CA combination.\n    const hasClientCert = rest.pfx || rest.cert || rest.key;\n    if (hasClientCert) {\n      const ctxOptions: Record<string, any> = {};\n      if (rest.pfx) ctxOptions.pfx = rest.pfx;\n      if (rest.cert) ctxOptions.cert = rest.cert;\n      if (rest.key) ctxOptions.key = rest.key;\n      if (rest.passphrase) ctxOptions.passphrase = rest.passphrase;\n\n      const ctx = tls.createSecureContext(ctxOptions);\n      const caList = Array.isArray(ca) ? ca : [ca!];\n      for (const caCert of caList) {\n        if (caCert) ctx.context.addCACert(caCert);\n      }\n\n      const { pfx: _pfx, cert: _cert, key: _key, passphrase: _pass, ...cleanRest } = rest;\n      return { ...cleanRest, secureContext: ctx } as unknown as T;\n    }\n\n    // CA-only case: use cached secure context\n    return { ...rest, secureContext: buildSecureContext(ca!) } as unknown as T;\n  }\n  return options;\n}\n\n/**\n * Hash a CA value which can be a single value or an array of certificates.\n * Node.js TLS options allow ca to be string | Buffer | (string | Buffer)[].\n */\nfunction hashCaValue(value: string | Buffer | (string | Buffer)[] | undefined): string | null {\n  if (!value) return null;\n  if (Array.isArray(value)) {\n    // Concatenate all values with separator and hash together\n    const combined = value.map((v) => (Buffer.isBuffer(v) ? v.toString('base64') : String(v))).join('|');\n    return crypto.createHash('sha256').update(combined).digest('hex').slice(0, 16);\n  }\n  const data = Buffer.isBuffer(value) ? value : String(value);\n  return crypto.createHash('sha256').update(data).digest('hex').slice(0, 16);\n}\n\n/**\n * Generate a cache key from HTTPS agent options.\n * Uses a hash of the serialized options to create a compact key.\n */\nfunction getAgentCacheKey(agentClassId: number, options: AgentOptions, proxyUri: string | null = null, hostname: string | null = null): string {\n  // Extract the TLS-relevant options for the cache key\n  const keyData = {\n    agentClassId,\n    hostname: proxyUri?.length ? null : hostname,\n    proxyUri,\n    keepAlive: options.keepAlive,\n    rejectUnauthorized: options.rejectUnauthorized,\n    // Hash certificates and passphrase instead of including full content\n    ca: hashCaValue(options.ca),\n    cert: hashValue(options.cert),\n    key: hashValue(options.key),\n    pfx: hashValue(options.pfx),\n    passphrase: hashValue(options.passphrase),\n    minVersion: options.minVersion,\n    secureProtocol: options.secureProtocol\n  };\n  return JSON.stringify(keyData);\n}\n\n/**\n * Generate a cache key from HTTP agent options.\n * Simpler than HTTPS since no TLS options are involved.\n */\nfunction getHttpAgentCacheKey(agentClassId: number, options: HttpAgentOptions, proxyUri: string | null = null, hostname: string | null = null): string {\n  const keyData = {\n    agentClassId,\n    hostname: proxyUri?.length ? null : hostname,\n    proxyUri,\n    keepAlive: options.keepAlive\n  };\n  return JSON.stringify(keyData);\n}\n\n/**\n * Get a cached timeline-wrapped HTTPS agent class.\n * Creates the wrapped class once and caches it for reuse.\n */\nfunction getTimelineAgentClass(BaseAgentClass: any): AgentClass {\n  if (timelineClassCache.has(BaseAgentClass)) {\n    return timelineClassCache.get(BaseAgentClass)!;\n  }\n  const wrappedClass = createTimelineAgentClass(BaseAgentClass);\n  timelineClassCache.set(BaseAgentClass, wrappedClass);\n  return wrappedClass;\n}\n\n/**\n * Get a cached timeline-wrapped HTTP agent class.\n * Creates the wrapped class once and caches it for reuse.\n */\nfunction getTimelineHttpAgentClass(BaseAgentClass: any): HttpAgentClass {\n  if (timelineHttpClassCache.has(BaseAgentClass)) {\n    return timelineHttpClassCache.get(BaseAgentClass)!;\n  }\n  const wrappedClass = createTimelineHttpAgentClass(BaseAgentClass);\n  timelineHttpClassCache.set(BaseAgentClass, wrappedClass);\n  return wrappedClass;\n}\n\n/**\n * Type for cache key generation functions.\n */\ntype CacheKeyFn<T> = (classId: number, options: T, proxyUri: string | null, hostname: string | null) => string;\n\n/**\n * Type for timeline class wrapper functions.\n */\ntype TimelineClassFn = (base: any) => AgentClass | HttpAgentClass;\n\n/**\n * Internal helper for agent caching with LRU eviction.\n * Shared logic for both HTTP and HTTPS agents.\n */\nfunction getOrCreateAgentInternal<TOptions extends HttpAgentOptions>(\n  BaseAgentClass: any,\n  options: TOptions,\n  proxyUri: string | null,\n  timeline: TimelineEntry[] | null,\n  getCacheKey: CacheKeyFn<TOptions>,\n  getTimelineClass: TimelineClassFn,\n  cacheHitMessage: string,\n  disableCache: boolean = false,\n  hostname: string | null = null\n): HttpAgent | HttpsAgent {\n  const agentClassId = getAgentClassId(BaseAgentClass);\n  const cacheKey = getCacheKey(agentClassId, options, proxyUri, hostname);\n\n  if (!disableCache && agentCache.has(cacheKey)) {\n    // Move to end for LRU (delete and re-add)\n    const agent = agentCache.get(cacheKey)!;\n    agentCache.delete(cacheKey);\n    agentCache.set(cacheKey, agent);\n\n    // Update timeline reference for new request\n    // The cached agent was created with a previous timeline,\n    // but we need events to go to the current request's timeline\n    if (timeline && 'timeline' in agent) {\n      (agent as any).timeline = timeline;\n    }\n\n    // Log that we're reusing a cached agent\n    if (timeline) {\n      timeline.push({\n        timestamp: new Date(),\n        type: 'info',\n        message: cacheHitMessage\n      });\n    }\n\n    return agent;\n  }\n\n  const AgentClass = timeline ? getTimelineClass(BaseAgentClass) : BaseAgentClass;\n  // Convert raw `ca` to a secureContext that adds CAs on top of OpenSSL defaults\n  const resolvedOptions = applySecureContext(options);\n\n  let agent: HttpAgent | HttpsAgent;\n  if (timeline) {\n    // Timeline-wrapped classes handle proxy internally via options.proxy\n    const agentOptions = proxyUri ? { ...resolvedOptions, proxy: proxyUri } : resolvedOptions;\n    agent = new AgentClass(agentOptions, timeline);\n  } else if (proxyUri) {\n    // Proxy agent classes expect (proxyUri, options) constructor signature\n    agent = new BaseAgentClass(proxyUri, resolvedOptions);\n  } else {\n    agent = new BaseAgentClass(resolvedOptions);\n  }\n\n  if (!disableCache) {\n    // Evict oldest entry if cache is full (LRU eviction)\n    if (agentCache.size >= MAX_AGENT_CACHE_SIZE) {\n      const firstKey = agentCache.keys().next().value;\n      if (firstKey !== undefined) {\n        const evictedAgent = agentCache.get(firstKey);\n        agentCache.delete(firstKey);\n        // Destroy the agent to release its sockets and prevent memory leaks\n        if (evictedAgent && typeof (evictedAgent as any).destroy === 'function') {\n          (evictedAgent as any).destroy();\n        }\n      }\n    }\n\n    agentCache.set(cacheKey, agent);\n  }\n\n  return agent;\n}\n\n/**\n * Get or create a cached HTTPS agent.\n * Reuses existing agents to enable SSL session caching.\n * Uses LRU-style eviction when cache exceeds MAX_AGENT_CACHE_SIZE.\n * Automatically wraps the agent class with timeline logging support.\n */\nfunction getOrCreateHttpsAgent({\n  AgentClass,\n  options,\n  proxyUri = null,\n  timeline = null,\n  disableCache = false,\n  hostname = null\n}: {\n  AgentClass: any;\n  options: AgentOptions;\n  proxyUri?: string | null;\n  timeline?: TimelineEntry[] | null;\n  disableCache?: boolean;\n  hostname?: string | null;\n}): HttpAgent | HttpsAgent {\n  return getOrCreateAgentInternal(\n    AgentClass,\n    options,\n    proxyUri,\n    timeline,\n    getAgentCacheKey,\n    getTimelineAgentClass,\n    'Reusing cached https agent',\n    disableCache,\n    hostname\n  );\n}\n\n/**\n * Get or create a cached HTTP agent.\n * Reuses existing agents to enable connection reuse.\n * Uses LRU-style eviction when cache exceeds MAX_AGENT_CACHE_SIZE.\n * Automatically wraps the agent class with timeline logging support.\n */\nfunction getOrCreateHttpAgent({\n  AgentClass,\n  options,\n  proxyUri = null,\n  timeline = null,\n  disableCache = false,\n  hostname = null\n}: {\n  AgentClass: any;\n  options: HttpAgentOptions;\n  proxyUri?: string | null;\n  timeline?: TimelineEntry[] | null;\n  disableCache?: boolean;\n  hostname?: string | null;\n}): HttpAgent {\n  return getOrCreateAgentInternal(\n    AgentClass,\n    options,\n    proxyUri,\n    timeline,\n    getHttpAgentCacheKey,\n    getTimelineHttpAgentClass,\n    'Reusing cached http agent',\n    disableCache,\n    hostname\n  ) as HttpAgent;\n}\n\n/**\n * Clear the agent cache. Useful for testing or when SSL configuration changes.\n * Destroys all cached agents to properly release their sockets.\n */\nfunction clearAgentCache(): void {\n  for (const agent of agentCache.values()) {\n    if (agent && typeof (agent as any).destroy === 'function') {\n      (agent as any).destroy();\n    }\n  }\n  agentCache.clear();\n}\n\n/**\n * Get the current size of the agent cache.\n */\nfunction getAgentCacheSize(): number {\n  return agentCache.size;\n}\n\nexport { getOrCreateHttpsAgent, getOrCreateHttpAgent, clearAgentCache, getAgentCacheSize };\n"
  },
  {
    "path": "packages/bruno-requests/src/utils/ca-cert.ts",
    "content": "import * as tls from 'node:tls';\nimport * as fs from 'node:fs';\n\ntype T_CACertificatesOptions = {\n  caCertFilePath?: string;\n  shouldKeepDefaultCerts?: boolean;\n};\n\ntype T_CACertificatesResult = {\n  caCertificates: string;\n  caCertificatesCount: {\n    system: number;\n    root: number;\n    custom: number;\n    extra: number;\n  };\n};\n\nlet systemCertsCache: string[] | undefined;\n\nfunction getSystemCerts(): string[] {\n  if (systemCertsCache) return systemCertsCache;\n\n  try {\n    systemCertsCache = tls.getCACertificates('system');\n\n    return systemCertsCache;\n  } catch (error) {\n    return [];\n  }\n}\n\nfunction certToString(cert: string | Buffer) {\n  return typeof cert === 'string'\n    ? cert\n    : Buffer.from(cert.buffer, cert.byteOffset, cert.byteLength).toString('utf8');\n}\n\nfunction mergeCA(...args: (string | string[])[]): string {\n  const ca = new Set<string>();\n  for (const item of args) {\n    if (!item) continue;\n    const caList = Array.isArray(item) ? item : [item];\n    for (const cert of caList) {\n      if (cert) {\n        ca.add(certToString(cert));\n      }\n    }\n  }\n  return [...ca].join('\\n');\n}\n\nfunction getNodeExtraCACerts(): string[] {\n  const extraCACertPath = process.env.NODE_EXTRA_CA_CERTS;\n  if (!extraCACertPath) return [];\n\n  try {\n    if (fs.existsSync(extraCACertPath)) {\n      const extraCACert = fs.readFileSync(extraCACertPath, 'utf8');\n      if (extraCACert && extraCACert.trim()) {\n        return [extraCACert];\n      }\n    }\n  } catch (err) {\n    console.error(`Failed to read NODE_EXTRA_CA_CERTS from ${extraCACertPath}:`, (err as Error).message);\n  }\n\n  return [];\n}\n\n/**\n * Get CA certificates\n *\n * Generic function to get CA certificates\n * - System CA certificates (From OS)\n * - Root CA certificates (From Node)\n * - Custom CA certificates (From user-provided file)\n * - NODE_EXTRA_CA_CERTS (From environment variable)\n *\n * If no custom CA certificate file path is provided\n *  → return system CA certificates and root certificates + NODE_EXTRA_CA_CERTS\n *\n * If custom CA certificate file path is provided\n *  → use custom CA certificate file + NODE_EXTRA_CA_CERTS\n *  → ignore system + root certificates if shouldKeepDefaultCerts is false\n *\n * @param caCertFilePath - path to custom CA certificate file\n * @param shouldKeepDefaultCerts - whether to keep default CA certificates\n * @returns {T_CACertificatesResult} - CA certificates and their count\n */\n\nconst getCACertificates = ({ caCertFilePath, shouldKeepDefaultCerts = true }: T_CACertificatesOptions): T_CACertificatesResult => {\n  try {\n    let caCertificates = '';\n    let caCertificatesCount = {\n      system: 0,\n      root: 0,\n      custom: 0,\n      extra: 0\n    };\n\n    let systemCerts: string[] = [];\n    let rootCerts: string[] = [];\n    let customCerts: string[] = [];\n    let nodeExtraCerts: string[] = [];\n\n    // handle user-provided custom CA certificate file with optional default certificates\n    if (caCertFilePath) {\n      // validate custom CA certificate file\n      if (fs.existsSync(caCertFilePath)) {\n        try {\n          const customCert = fs.readFileSync(caCertFilePath, 'utf8');\n          if (customCert && customCert.trim()) {\n            customCerts.push(customCert);\n            caCertificatesCount.custom = customCerts.length;\n          }\n        } catch (err) {\n          console.error(`Failed to read custom CA certificate from ${caCertFilePath}:`, (err as Error).message);\n          throw new Error(`Unable to load custom CA certificate: ${(err as Error).message}`);\n        }\n      } else {\n        throw new Error(`Invalid custom CA certificate path: ${caCertFilePath}`);\n      }\n\n      if (shouldKeepDefaultCerts) {\n        // get system certs\n        systemCerts = getSystemCerts();\n        caCertificatesCount.system = systemCerts.length;\n\n        // get root certs\n        rootCerts = [...tls.rootCertificates];\n        caCertificatesCount.root = rootCerts.length;\n      }\n    } else {\n      // get system certs\n      systemCerts = getSystemCerts();\n      caCertificatesCount.system = systemCerts.length;\n\n      // get root certs\n      rootCerts = [...tls.rootCertificates];\n      caCertificatesCount.root = rootCerts.length;\n    }\n\n    // get NODE_EXTRA_CA_CERTS\n    nodeExtraCerts = getNodeExtraCACerts();\n    caCertificatesCount.extra = nodeExtraCerts.length;\n\n    // merge certs\n    const mergedCerts = mergeCA(systemCerts, rootCerts, customCerts, nodeExtraCerts);\n    caCertificates = mergedCerts;\n\n    return {\n      caCertificates,\n      caCertificatesCount\n    };\n  } catch (err) {\n    console.error('Error configuring CA certificates:', (err as Error).message);\n    throw err; // Re-throw certificate loading errors as they're critical\n  }\n};\n\nexport {\n  getCACertificates\n};\n"
  },
  {
    "path": "packages/bruno-requests/src/utils/http-https-agents.ts",
    "content": "import * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport http from 'node:http';\nimport https from 'node:https';\nimport type { Agent as HttpAgent } from 'node:http';\nimport type { Agent as HttpsAgent } from 'node:https';\nimport { parse as parseUrl, type Url } from 'url';\nimport { HttpsProxyAgent } from 'https-proxy-agent';\nimport { SocksProxyAgent } from 'socks-proxy-agent';\nimport { HttpProxyAgent } from 'http-proxy-agent';\nimport { isEmpty, get, isUndefined, isNull } from 'lodash';\nimport { getCACertificates } from './ca-cert';\nimport { transformProxyConfig } from './proxy-util';\nimport { getOrCreateHttpsAgent, getOrCreateHttpAgent } from './agent-cache';\nimport type { TimelineEntry } from './timeline-agent';\n\nconst DEFAULT_PORTS: Record<string, number> = {\n  ftp: 21,\n  gopher: 70,\n  http: 80,\n  https: 443,\n  ws: 80,\n  wss: 443\n};\n\ntype ProxyMode = 'on' | 'off' | 'system';\n\ntype ProxyAuth = {\n  enabled: boolean;\n  username?: string;\n  password?: string;\n};\n\ntype ProxyConfig = {\n  enabled?: boolean | 'global';\n  protocol?: string;\n  hostname?: string;\n  port?: number | null;\n  auth?: ProxyAuth;\n  bypassProxy?: string;\n  mode?: ProxyMode;\n};\n\ntype SystemProxyConfig = {\n  http_proxy?: string;\n  https_proxy?: string;\n  no_proxy?: string;\n};\n\ntype ClientCertificate = {\n  domain?: string;\n  type?: 'cert' | 'pfx';\n  certFilePath?: string;\n  keyFilePath?: string;\n  pfxFilePath?: string;\n  passphrase?: string;\n};\n\ntype CACertificatesCount = {\n  system: number;\n  root: number;\n  custom: number;\n  extra: number;\n};\n\ntype CertsConfig = {\n  caCertificatesCount?: CACertificatesCount;\n  ca?: string | string[];\n  cert?: Buffer;\n  key?: Buffer;\n  pfx?: Buffer;\n  passphrase?: string;\n};\n\ntype HttpsAgentRequestFields = {\n  keepAlive?: boolean;\n  rejectUnauthorized?: boolean;\n  caCertificatesCount?: CACertificatesCount;\n  ca?: string | string[];\n};\n\ntype TlsOptions = HttpsAgentRequestFields & CertsConfig & {\n  secureProtocol?: string;\n  minVersion?: string;\n  ALPNProtocols?: string[];\n};\n\ntype AgentResult = {\n  httpAgent?: HttpAgent;\n  httpsAgent?: HttpsAgent | HttpsProxyAgent<any> | SocksProxyAgent;\n};\n\ntype ConfigOptions = {\n  noproxy: boolean;\n  shouldVerifyTls: boolean;\n  shouldUseCustomCaCertificate: boolean;\n  customCaCertificateFilePath?: string;\n  shouldKeepDefaultCaCertificates: boolean;\n  cacheSslSession?: boolean;\n};\n\ntype GetCertsAndProxyConfigParams = {\n  requestUrl?: string;\n  collectionPath: string;\n  options: ConfigOptions;\n  clientCertificates?: {\n    certs?: ClientCertificate[];\n  };\n  collectionLevelProxy?: ProxyConfig;\n  appLevelProxyConfig?: Record<string, any>;\n  systemProxyConfig?: SystemProxyConfig;\n};\n\ntype GetCertsAndProxyConfigResult = {\n  proxyMode: ProxyMode;\n  proxyConfig: ProxyConfig;\n  certsConfig: CertsConfig;\n};\n\ntype CreateAgentsParams = {\n  requestUrl?: string;\n  proxyMode: ProxyMode;\n  proxyConfig: ProxyConfig;\n  certsConfig: CertsConfig;\n  httpsAgentRequestFields: HttpsAgentRequestFields;\n  systemProxyConfig?: SystemProxyConfig;\n  timeline?: TimelineEntry[];\n  disableCache?: boolean;\n};\n\ntype GetHttpHttpsAgentsParams = {\n  requestUrl?: string;\n  collectionPath: string;\n  options: ConfigOptions;\n  clientCertificates?: {\n    certs?: ClientCertificate[];\n  };\n  collectionLevelProxy?: ProxyConfig;\n  appLevelProxyConfig?: Record<string, any>;\n  systemProxyConfig?: SystemProxyConfig;\n  timeline?: TimelineEntry[];\n};\n\n/**\n * check for proxy bypass, copied from 'proxy-from-env'\n */\nconst shouldUseProxy = (url: string | undefined, proxyBypass: string | undefined): boolean => {\n  if (proxyBypass === '*') {\n    return false; // Never proxy if wildcard is set.\n  }\n\n  // use proxy if no proxyBypass is set\n  if (!proxyBypass || typeof proxyBypass !== 'string' || isEmpty(proxyBypass.trim())) {\n    return true;\n  }\n\n  const parsedUrl: Url | {} = typeof url === 'string' ? parseUrl(url) : (url ? (url as unknown as Url) : {});\n  const urlObj = parsedUrl as Url;\n  let proto = urlObj.protocol;\n  let hostname = urlObj.host;\n  let port: string | null = urlObj.port;\n  if (typeof hostname !== 'string' || !hostname || typeof proto !== 'string') {\n    return false; // Don't proxy URLs without a valid scheme or host.\n  }\n\n  proto = proto.split(':', 1)[0];\n  // Stripping ports in this way instead of using parsedUrl.hostname to make\n  // sure that the brackets around IPv6 addresses are kept.\n  hostname = hostname.replace(/:\\d*$/, '');\n  const portNum = parseInt(port || '', 10) || DEFAULT_PORTS[proto] || 0;\n\n  return proxyBypass.split(/[,;\\s]/).every(function (dontProxyFor) {\n    if (!dontProxyFor) {\n      return true; // Skip zero-length hosts.\n    }\n    const parsedProxy = dontProxyFor.match(/^(.+):(\\d+)$/);\n    let parsedProxyHostname = parsedProxy ? parsedProxy[1] : dontProxyFor;\n    const parsedProxyPort = parsedProxy ? parseInt(parsedProxy[2], 10) : 0;\n    if (parsedProxyPort && parsedProxyPort !== portNum) {\n      return true; // Skip if ports don't match.\n    }\n\n    if (!/^[.*]/.test(parsedProxyHostname)) {\n      // No wildcards, so stop proxying if there is an exact match.\n      return hostname !== parsedProxyHostname;\n    }\n\n    if (parsedProxyHostname.charAt(0) === '*') {\n      // Remove leading wildcard.\n      parsedProxyHostname = parsedProxyHostname.slice(1);\n    }\n    // Stop proxying if the hostname ends with the no_proxy host.\n    return !hostname.endsWith(parsedProxyHostname);\n  });\n};\n\n/**\n * Options that should be forwarded from the constructor to the target TLS upgrade.\n * The upstream HttpsProxyAgent (https://github.com/TooTallNate/proxy-agents/issues/194)\n * ignores constructor options when upgrading the tunneled socket to TLS for the\n * target server. This list covers client certificates, verification, and secure context.\n */\nconst TARGET_TLS_OPTIONS = ['cert', 'key', 'pfx', 'passphrase', 'rejectUnauthorized', 'secureContext'] as const;\n\n/**\n * Patched version of HttpsProxyAgent that correctly handles TLS options for\n * both the proxy connection and the target server connection.\n *\n * This patch forwards client certificate options, rejectUnauthorized, and\n * secureContext to the target TLS upgrade. The agent-cache layer converts raw\n * `ca` to a secureContext (via addCACert) before construction, so custom CAs\n * are added on top of the OpenSSL defaults rather than replacing them.\n */\nclass PatchedHttpsProxyAgent extends HttpsProxyAgent<any> {\n  private constructorOpts: any;\n\n  constructor(proxy: string, opts: any) {\n    super(proxy, opts);\n    this.constructorOpts = opts;\n  }\n\n  async connect(req: any, opts: any) {\n    const targetOpts = { ...opts };\n\n    // Forward TLS options to the target TLS upgrade\n    if (this.constructorOpts) {\n      for (const key of TARGET_TLS_OPTIONS) {\n        if (key in this.constructorOpts) {\n          targetOpts[key] = this.constructorOpts[key];\n        }\n      }\n    }\n\n    return super.connect(req, targetOpts);\n  }\n}\n\nconst getCertsAndProxyConfig = ({\n  requestUrl,\n  collectionPath,\n  options,\n  clientCertificates,\n  collectionLevelProxy,\n  appLevelProxyConfig,\n  systemProxyConfig\n}: GetCertsAndProxyConfigParams): GetCertsAndProxyConfigResult => {\n  const certsConfig: CertsConfig = {};\n\n  // Only load CA certificates when TLS verification is enabled\n  if (options.shouldVerifyTls) {\n    const caCertFilePath = options.shouldUseCustomCaCertificate && options.customCaCertificateFilePath ? options.customCaCertificateFilePath : undefined;\n    const caCertificatesData = getCACertificates({\n      caCertFilePath,\n      shouldKeepDefaultCerts: options.shouldKeepDefaultCaCertificates\n    });\n\n    // configure HTTPS agent with aggregated CA certificates\n    certsConfig.caCertificatesCount = caCertificatesData.caCertificatesCount;\n    certsConfig.ca = caCertificatesData.caCertificates || [];\n  }\n\n  // client certificate config\n  const clientCertConfig = get(clientCertificates, 'certs', []) as ClientCertificate[];\n\n  for (const clientCert of clientCertConfig) {\n    const domain = clientCert?.domain;\n    const type = clientCert?.type || 'cert';\n    if (domain) {\n      const hostRegex = '^(https:\\\\/\\\\/|grpc:\\\\/\\\\/|grpcs:\\\\/\\\\/)?' + domain.replace(/\\./g, '\\\\.').replace(/\\*/g, '.*');\n      if (requestUrl && requestUrl.match(hostRegex)) {\n        if (type === 'cert') {\n          try {\n            let certFilePath = clientCert?.certFilePath;\n            if (!certFilePath) {\n              throw new Error('certFilePath is required for cert type');\n            }\n            certFilePath = path.isAbsolute(certFilePath) ? certFilePath : path.join(collectionPath, certFilePath);\n            let keyFilePath = clientCert?.keyFilePath;\n            if (!keyFilePath) {\n              throw new Error('keyFilePath is required for cert type');\n            }\n            keyFilePath = path.isAbsolute(keyFilePath) ? keyFilePath : path.join(collectionPath, keyFilePath);\n\n            certsConfig.cert = fs.readFileSync(certFilePath);\n            certsConfig.key = fs.readFileSync(keyFilePath);\n          } catch (err: any) {\n            console.error('Error reading cert/key file', err);\n            throw new Error(`Error reading cert/key file: ${err.message}`);\n          }\n        } else if (type === 'pfx') {\n          try {\n            let pfxFilePath = clientCert?.pfxFilePath;\n            if (!pfxFilePath) {\n              throw new Error('pfxFilePath is required for pfx type');\n            }\n            pfxFilePath = path.isAbsolute(pfxFilePath) ? pfxFilePath : path.join(collectionPath, pfxFilePath);\n            certsConfig.pfx = fs.readFileSync(pfxFilePath);\n          } catch (err: any) {\n            console.error('Error reading pfx file', err);\n            throw new Error(`Error reading pfx file: ${err.message}`);\n          }\n        }\n        certsConfig.passphrase = clientCert.passphrase;\n        break;\n      }\n    }\n  }\n\n  /**\n   * Proxy configuration\n   *\n   * Preferences proxyMode has three possible values: on, off, system\n   * Collection proxyMode has three possible values: true, false, global\n   *\n   * When collection proxyMode is true, it overrides the app-level proxy settings\n   * When collection proxyMode is false, it ignores the app-level proxy settings\n   * When collection proxyMode is global, it uses the app-level proxy settings\n   *\n   * Below logic calculates the proxyMode and proxyConfig to be used for the request\n   */\n  let proxyMode: ProxyMode = 'off';\n  let proxyConfig: ProxyConfig = {};\n\n  const collectionProxyConfig = transformProxyConfig(collectionLevelProxy || {}) as ProxyConfig;\n  const collectionProxyDisabled = get(collectionProxyConfig, 'disabled', false);\n  const collectionProxyInherit = get(collectionProxyConfig, 'inherit', true);\n  const collectionProxyConfigData = get(collectionProxyConfig, 'config', {});\n\n  if (options.noproxy || collectionProxyDisabled) {\n    // If noproxy flag is set or collection proxy is disabled, don't use any proxy\n    proxyMode = 'off';\n  } else if (!collectionProxyDisabled && !collectionProxyInherit) {\n    // Use collection-specific proxy\n    proxyConfig = collectionProxyConfigData;\n    proxyMode = 'on';\n  } else if (!collectionProxyDisabled && collectionProxyInherit) {\n    // Inherit from app-level proxy settings\n    if (appLevelProxyConfig) {\n      const globalDisabled = get(appLevelProxyConfig, 'disabled', false);\n      const globalInherit = get(appLevelProxyConfig, 'inherit', false);\n      const globalProxyConfigData = get(appLevelProxyConfig, 'config', appLevelProxyConfig);\n\n      if (!globalDisabled && !globalInherit) {\n        // Use app-level custom proxy\n        proxyConfig = globalProxyConfigData;\n        proxyMode = 'on';\n      } else if (!globalDisabled && globalInherit) {\n        // App-level also inherits, fall through to system proxy\n        const { http_proxy, https_proxy } = systemProxyConfig || {};\n        if (http_proxy?.length || https_proxy?.length) {\n          proxyMode = 'system';\n        }\n      }\n      // else: app-level proxy is disabled, proxyMode stays 'off'\n    } else {\n      // No app-level proxy config (e.g. CLI), fall through to system proxy\n      const { http_proxy, https_proxy } = systemProxyConfig || {};\n      if (http_proxy?.length || https_proxy?.length) {\n        proxyMode = 'system';\n      }\n    }\n  }\n  // else: collection proxy is disabled, proxyMode stays 'off'\n\n  return { proxyMode, proxyConfig, certsConfig };\n};\n\nfunction extractHostname(url: string | undefined): string | null {\n  if (!url) return null;\n  try {\n    return new URL(url).hostname || null;\n  } catch {\n    return null;\n  }\n}\n\nfunction createAgents({\n  requestUrl,\n  proxyMode,\n  proxyConfig,\n  systemProxyConfig,\n  certsConfig,\n  httpsAgentRequestFields,\n  timeline,\n  disableCache = true\n}: CreateAgentsParams): AgentResult {\n  // Ensure TLS options are properly set\n  const tlsOptions: TlsOptions = {\n    ...httpsAgentRequestFields,\n    ...certsConfig,\n    // Enable all secure protocols by default\n    secureProtocol: undefined,\n    // Allow Node.js to choose the protocol\n    minVersion: 'TLSv1',\n    rejectUnauthorized: httpsAgentRequestFields.rejectUnauthorized !== undefined ? httpsAgentRequestFields.rejectUnauthorized : true\n  };\n\n  let httpAgent: HttpAgent | undefined;\n  let httpsAgent: HttpsAgent | HttpsProxyAgent<any> | SocksProxyAgent | undefined;\n\n  // Determine if this is an HTTPS request\n  const isHttpsRequest = requestUrl ? requestUrl.startsWith('https:') : true;\n\n  // Extract hostname for per-host agent caching (enables TLS session reuse per host)\n  const hostname = extractHostname(requestUrl);\n\n  if (proxyMode === 'on') {\n    const shouldProxy = shouldUseProxy(requestUrl, get(proxyConfig, 'bypassProxy', ''));\n    if (shouldProxy) {\n      const proxyProtocol = get(proxyConfig, 'protocol');\n      const proxyHostname = get(proxyConfig, 'hostname');\n      const proxyPort = get(proxyConfig, 'port');\n      const proxyAuthEnabled = !get(proxyConfig, 'auth.disabled', false);\n      const socksEnabled = proxyProtocol && proxyProtocol.includes('socks');\n\n      if (!proxyProtocol || !proxyHostname) {\n        throw new Error('Proxy protocol and hostname are required when proxy is enabled');\n      }\n\n      const uriPort = isUndefined(proxyPort) || isNull(proxyPort) ? '' : `:${proxyPort}`;\n      let proxyUri: string;\n      if (proxyAuthEnabled) {\n        const proxyAuthUsername = encodeURIComponent(get(proxyConfig, 'auth.username', ''));\n        const proxyAuthPassword = encodeURIComponent(get(proxyConfig, 'auth.password', ''));\n        proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}${uriPort}`;\n      } else {\n        proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;\n      }\n\n      // When the proxy itself uses HTTPS, the agent connecting to it needs TLS options\n      // (e.g., ca certs) even for plain HTTP requests\n      const isHttpsProxy = proxyProtocol === 'https';\n      const httpProxyAgentOptions = isHttpsProxy ? { keepAlive: true, ...tlsOptions } : { keepAlive: true };\n\n      // Only set the agent needed for the request protocol\n      if (socksEnabled) {\n        if (isHttpsRequest) {\n          httpsAgent = getOrCreateHttpsAgent({ AgentClass: SocksProxyAgent, options: tlsOptions as any, proxyUri, timeline: timeline || null, disableCache, hostname }) as HttpsAgent;\n        } else {\n          httpAgent = getOrCreateHttpAgent({ AgentClass: SocksProxyAgent, options: httpProxyAgentOptions as any, proxyUri, timeline: timeline || null, disableCache, hostname });\n        }\n      } else {\n        if (isHttpsRequest) {\n          httpsAgent = getOrCreateHttpsAgent({ AgentClass: PatchedHttpsProxyAgent, options: tlsOptions as any, proxyUri, timeline: timeline || null, disableCache, hostname }) as HttpsAgent;\n        } else {\n          httpAgent = getOrCreateHttpAgent({ AgentClass: HttpProxyAgent, options: httpProxyAgentOptions as any, proxyUri, timeline: timeline || null, disableCache, hostname });\n        }\n      }\n    } else {\n      // If proxy should not be used, only set HTTPS agent for HTTPS requests\n      if (isHttpsRequest) {\n        httpsAgent = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: tlsOptions as any, timeline: timeline || null, disableCache, hostname }) as HttpsAgent;\n      }\n      // HTTP requests without proxy don't need a custom agent\n    }\n  } else if (proxyMode === 'system') {\n    const http_proxy = get(systemProxyConfig, 'http_proxy');\n    const https_proxy = get(systemProxyConfig, 'https_proxy');\n    const no_proxy = get(systemProxyConfig, 'no_proxy');\n    const shouldUseSystemProxy = shouldUseProxy(requestUrl, no_proxy || '');\n    if (shouldUseSystemProxy) {\n      try {\n        if (http_proxy?.length && !isHttpsRequest) {\n          const parsedHttpProxy = new URL(http_proxy);\n          const isHttpsSystemProxy = parsedHttpProxy.protocol === 'https:';\n          const systemHttpProxyAgentOptions = isHttpsSystemProxy ? { keepAlive: true, ...tlsOptions } : { keepAlive: true };\n          httpAgent = getOrCreateHttpAgent({ AgentClass: HttpProxyAgent, options: systemHttpProxyAgentOptions as any, proxyUri: http_proxy, timeline: timeline || null, disableCache, hostname });\n        }\n      } catch (error) {\n        throw new Error('Invalid system http_proxy');\n      }\n      try {\n        if (https_proxy?.length && isHttpsRequest) {\n          new URL(https_proxy);\n          httpsAgent = getOrCreateHttpsAgent({ AgentClass: PatchedHttpsProxyAgent, options: tlsOptions as any, proxyUri: https_proxy, timeline: timeline || null, disableCache, hostname }) as HttpsAgent;\n        }\n      } catch (error) {\n        throw new Error('Invalid system https_proxy');\n      }\n    }\n  }\n\n  if (!httpAgent && !httpsAgent) {\n    if (isHttpsRequest) {\n      httpsAgent = getOrCreateHttpsAgent({ AgentClass: https.Agent, options: tlsOptions as any, timeline: timeline || null, disableCache, hostname }) as HttpsAgent;\n    } else {\n      httpAgent = getOrCreateHttpAgent({ AgentClass: http.Agent, options: { keepAlive: true }, timeline: timeline || null, disableCache, hostname });\n    }\n  }\n\n  return { httpAgent, httpsAgent };\n}\n\nconst getHttpHttpsAgents = async ({\n  requestUrl,\n  collectionPath,\n  clientCertificates,\n  collectionLevelProxy,\n  appLevelProxyConfig,\n  systemProxyConfig,\n  options,\n  timeline\n}: GetHttpHttpsAgentsParams): Promise<AgentResult> => {\n  const { proxyMode, proxyConfig, certsConfig } = getCertsAndProxyConfig({\n    requestUrl,\n    collectionPath,\n    clientCertificates,\n    collectionLevelProxy,\n    appLevelProxyConfig,\n    systemProxyConfig,\n    options\n  });\n\n  /**\n   * @see https://github.com/usebruno/bruno/issues/211 set keepAlive to true, this should fix socket hang up errors\n   * @see https://github.com/nodejs/node/pull/43522 keepAlive was changed to true globally on Node v19+\n   */\n  const httpsAgentRequestFields: HttpsAgentRequestFields = { keepAlive: true };\n  if (!options.shouldVerifyTls) {\n    httpsAgentRequestFields.rejectUnauthorized = false;\n  }\n\n  const { httpAgent, httpsAgent } = createAgents({\n    requestUrl,\n    proxyMode,\n    proxyConfig,\n    systemProxyConfig,\n    certsConfig,\n    httpsAgentRequestFields,\n    timeline,\n    disableCache: !options.cacheSslSession\n  });\n\n  return { httpAgent, httpsAgent };\n};\n\nexport { getHttpHttpsAgents };\n\nexport type { GetHttpHttpsAgentsParams };\n"
  },
  {
    "path": "packages/bruno-requests/src/utils/node-vault.spec.ts",
    "content": "import axios from 'axios';\nimport createVaultClient, { VaultError, VaultClient } from './node-vault';\n\n// Mock axios\njest.mock('axios', () => {\n  const mockAxios = jest.fn();\n  (mockAxios as any).isAxiosError = jest.fn((error: any) => error.isAxiosError === true);\n  return mockAxios;\n});\n\nconst mockedAxios = axios as jest.MockedFunction<typeof axios>;\n\ndescribe('node-vault', () => {\n  beforeEach(() => {\n    jest.clearAllMocks();\n    // Clear environment variables\n    delete process.env.VAULT_ADDR;\n    delete process.env.VAULT_TOKEN;\n    delete process.env.VAULT_NAMESPACE;\n  });\n\n  describe('module', () => {\n    it('should export a function that returns a new client', () => {\n      const vault = createVaultClient();\n      expect(typeof createVaultClient).toBe('function');\n      expect(typeof vault).toBe('object');\n    });\n\n    it('should set default values for endpoint and apiVersion', () => {\n      const vault = createVaultClient();\n      expect(vault.endpoint).toBe('http://127.0.0.1:8200');\n      expect(vault.apiVersion).toBe('v1');\n    });\n\n    it('should use environment variables for defaults', () => {\n      process.env.VAULT_ADDR = 'https://vault.example.com';\n      process.env.VAULT_TOKEN = 'env-token';\n      process.env.VAULT_NAMESPACE = 'env-namespace';\n\n      const vault = createVaultClient();\n      expect(vault.endpoint).toBe('https://vault.example.com');\n      expect(vault.token).toBe('env-token');\n      expect(vault.namespace).toBe('env-namespace');\n    });\n\n    it('should allow config to override environment variables', () => {\n      process.env.VAULT_ADDR = 'https://vault.example.com';\n      process.env.VAULT_TOKEN = 'env-token';\n\n      const vault = createVaultClient({\n        endpoint: 'https://custom.vault.com',\n        token: 'config-token'\n      });\n      expect(vault.endpoint).toBe('https://custom.vault.com');\n      expect(vault.token).toBe('config-token');\n    });\n  });\n\n  describe('client properties', () => {\n    it('should allow direct assignment of endpoint', () => {\n      const vault = createVaultClient();\n      vault.endpoint = 'https://new-vault.example.com';\n      expect(vault.endpoint).toBe('https://new-vault.example.com');\n    });\n\n    it('should allow direct assignment of token', () => {\n      const vault = createVaultClient();\n      vault.token = 'new-token';\n      expect(vault.token).toBe('new-token');\n    });\n\n    it('should allow direct assignment of namespace', () => {\n      const vault = createVaultClient();\n      vault.namespace = 'my-namespace';\n      expect(vault.namespace).toBe('my-namespace');\n    });\n\n    it('should allow direct assignment of apiVersion', () => {\n      const vault = createVaultClient();\n      vault.apiVersion = 'v2';\n      expect(vault.apiVersion).toBe('v2');\n    });\n  });\n\n  describe('read(path, requestOptions)', () => {\n    let vault: VaultClient;\n\n    beforeEach(() => {\n      vault = createVaultClient({\n        endpoint: 'http://localhost:8200',\n        token: 'test-token'\n      });\n    });\n\n    it('should read data from path', async () => {\n      const responseData = { data: { value: 'secret-value' } };\n      mockedAxios.mockResolvedValueOnce({\n        status: 200,\n        data: responseData\n      });\n\n      const result = await vault.read('secret/data/hello');\n\n      expect(mockedAxios).toHaveBeenCalledTimes(1);\n      expect(mockedAxios).toHaveBeenCalledWith(\n        expect.objectContaining({\n          method: 'GET',\n          url: 'http://localhost:8200/v1/secret/data/hello',\n          headers: expect.objectContaining({\n            'X-Vault-Token': 'test-token'\n          })\n        })\n      );\n      expect(result).toEqual(responseData);\n    });\n\n    it('should include namespace header when set', async () => {\n      vault.namespace = 'my-namespace';\n      mockedAxios.mockResolvedValueOnce({\n        status: 200,\n        data: { data: {} }\n      });\n\n      await vault.read('secret/data/hello');\n\n      expect(mockedAxios).toHaveBeenCalledWith(\n        expect.objectContaining({\n          headers: expect.objectContaining({\n            'X-Vault-Token': 'test-token',\n            'X-Vault-Namespace': 'my-namespace'\n          })\n        })\n      );\n    });\n\n    it('should use updated endpoint after assignment', async () => {\n      vault.endpoint = 'https://new-vault.com';\n      mockedAxios.mockResolvedValueOnce({\n        status: 200,\n        data: { data: {} }\n      });\n\n      await vault.read('secret/data/hello');\n\n      expect(mockedAxios).toHaveBeenCalledWith(\n        expect.objectContaining({\n          url: 'https://new-vault.com/v1/secret/data/hello'\n        })\n      );\n    });\n\n    it('should handle 404 errors', async () => {\n      mockedAxios.mockResolvedValueOnce({\n        status: 404,\n        data: { errors: ['no secrets found'] }\n      });\n\n      await expect(vault.read('secret/data/nonexistent')).rejects.toThrow('no secrets found');\n    });\n\n    it('should handle 204 no content response', async () => {\n      mockedAxios.mockResolvedValueOnce({\n        status: 204,\n        data: null\n      });\n\n      const result = await vault.read('secret/data/empty');\n      expect(result).toBeNull();\n    });\n\n    it('should handle paths with leading slash without creating double slashes', async () => {\n      const responseData = { data: { value: 'secret-value' } };\n      mockedAxios.mockResolvedValueOnce({\n        status: 200,\n        data: responseData\n      });\n\n      const result = await vault.read('/secret/data/hello');\n\n      expect(mockedAxios).toHaveBeenCalledTimes(1);\n      expect(mockedAxios).toHaveBeenCalledWith(\n        expect.objectContaining({\n          method: 'GET',\n          url: 'http://localhost:8200/v1/secret/data/hello',\n          headers: expect.objectContaining({\n            'X-Vault-Token': 'test-token'\n          })\n        })\n      );\n      expect(result).toEqual(responseData);\n    });\n\n    it('should handle endpoint with trailing slash', async () => {\n      vault.endpoint = 'http://localhost:8200/';\n      const responseData = { data: { value: 'secret-value' } };\n      mockedAxios.mockResolvedValueOnce({\n        status: 200,\n        data: responseData\n      });\n\n      const result = await vault.read('secret/data/hello');\n\n      expect(mockedAxios).toHaveBeenCalledTimes(1);\n      expect(mockedAxios).toHaveBeenCalledWith(\n        expect.objectContaining({\n          method: 'GET',\n          url: 'http://localhost:8200/v1/secret/data/hello'\n        })\n      );\n      expect(result).toEqual(responseData);\n    });\n  });\n\n  describe('write(path, data, requestOptions)', () => {\n    let vault: VaultClient;\n\n    beforeEach(() => {\n      vault = createVaultClient({\n        endpoint: 'http://localhost:8200',\n        token: 'test-token'\n      });\n    });\n\n    it('should write data to path', async () => {\n      const writeData = { value: 'world' };\n      const responseData = { data: { created_time: '2024-01-01T00:00:00Z' } };\n      mockedAxios.mockResolvedValueOnce({\n        status: 200,\n        data: responseData\n      });\n\n      const result = await vault.write('secret/data/hello', writeData);\n\n      expect(mockedAxios).toHaveBeenCalledTimes(1);\n      expect(mockedAxios).toHaveBeenCalledWith(\n        expect.objectContaining({\n          method: 'POST',\n          url: 'http://localhost:8200/v1/secret/data/hello',\n          data: writeData,\n          headers: expect.objectContaining({\n            'X-Vault-Token': 'test-token',\n            'Content-Type': 'application/json'\n          })\n        })\n      );\n      expect(result).toEqual(responseData);\n    });\n\n    it('should handle LDAP login write', async () => {\n      const loginData = { password: 'my-password' };\n      const responseData = {\n        auth: {\n          client_token: 'ldap-token',\n          renewable: true,\n          lease_duration: 3600\n        }\n      };\n      mockedAxios.mockResolvedValueOnce({\n        status: 200,\n        data: responseData\n      });\n\n      const result = await vault.write('auth/ldap/login/myuser', loginData);\n\n      expect(mockedAxios).toHaveBeenCalledWith(\n        expect.objectContaining({\n          method: 'POST',\n          url: 'http://localhost:8200/v1/auth/ldap/login/myuser',\n          data: loginData\n        })\n      );\n      expect(result.auth.client_token).toBe('ldap-token');\n    });\n  });\n\n  describe('approleLogin(args)', () => {\n    let vault: VaultClient;\n\n    beforeEach(() => {\n      vault = createVaultClient({\n        endpoint: 'http://localhost:8200'\n      });\n    });\n\n    it('should login with role_id and secret_id', async () => {\n      const responseData = {\n        auth: {\n          client_token: 'approle-token',\n          renewable: true,\n          lease_duration: 3600,\n          policies: ['default', 'my-policy']\n        }\n      };\n      mockedAxios.mockResolvedValueOnce({\n        status: 200,\n        data: responseData\n      });\n\n      const result = await vault.approleLogin({\n        role_id: 'my-role-id',\n        secret_id: 'my-secret-id'\n      });\n\n      expect(mockedAxios).toHaveBeenCalledWith(\n        expect.objectContaining({\n          method: 'POST',\n          url: 'http://localhost:8200/v1/auth/approle/login',\n          data: {\n            role_id: 'my-role-id',\n            secret_id: 'my-secret-id'\n          }\n        })\n      );\n      expect(result.auth.client_token).toBe('approle-token');\n    });\n\n    it('should login with only role_id when secret_id is not required', async () => {\n      const responseData = {\n        auth: { client_token: 'approle-token' }\n      };\n      mockedAxios.mockResolvedValueOnce({\n        status: 200,\n        data: responseData\n      });\n\n      await vault.approleLogin({\n        role_id: 'my-role-id'\n      });\n\n      expect(mockedAxios).toHaveBeenCalledWith(\n        expect.objectContaining({\n          data: {\n            role_id: 'my-role-id'\n          }\n        })\n      );\n    });\n\n    it('should use custom mount_point', async () => {\n      mockedAxios.mockResolvedValueOnce({\n        status: 200,\n        data: { auth: { client_token: 'token' } }\n      });\n\n      await vault.approleLogin({\n        role_id: 'my-role-id',\n        secret_id: 'my-secret-id',\n        mount_point: 'custom-approle'\n      });\n\n      expect(mockedAxios).toHaveBeenCalledWith(\n        expect.objectContaining({\n          url: 'http://localhost:8200/v1/auth/custom-approle/login'\n        })\n      );\n    });\n\n    it('should handle authentication errors', async () => {\n      mockedAxios.mockResolvedValueOnce({\n        status: 400,\n        data: { errors: ['invalid role or secret ID'] }\n      });\n\n      await expect(vault.approleLogin({\n        role_id: 'bad-role-id',\n        secret_id: 'bad-secret-id'\n      })).rejects.toThrow('invalid role or secret ID');\n    });\n  });\n\n  describe('tokenLookupSelf()', () => {\n    let vault: VaultClient;\n\n    beforeEach(() => {\n      vault = createVaultClient({\n        endpoint: 'http://localhost:8200',\n        token: 'my-token'\n      });\n    });\n\n    it('should lookup current token', async () => {\n      const responseData = {\n        data: {\n          id: 'my-token',\n          ttl: 3600,\n          renewable: true,\n          policies: ['default']\n        }\n      };\n      mockedAxios.mockResolvedValueOnce({\n        status: 200,\n        data: responseData\n      });\n\n      const result = await vault.tokenLookupSelf();\n\n      expect(mockedAxios).toHaveBeenCalledWith(\n        expect.objectContaining({\n          method: 'GET',\n          url: 'http://localhost:8200/v1/auth/token/lookup-self',\n          headers: expect.objectContaining({\n            'X-Vault-Token': 'my-token'\n          })\n        })\n      );\n      expect(result.data.ttl).toBe(3600);\n    });\n\n    it('should handle expired token error', async () => {\n      mockedAxios.mockResolvedValueOnce({\n        status: 403,\n        data: { errors: ['permission denied'] }\n      });\n\n      await expect(vault.tokenLookupSelf()).rejects.toThrow('permission denied');\n    });\n  });\n\n  describe('tokenRenewSelf(args)', () => {\n    let vault: VaultClient;\n\n    beforeEach(() => {\n      vault = createVaultClient({\n        endpoint: 'http://localhost:8200',\n        token: 'my-token'\n      });\n    });\n\n    it('should renew current token', async () => {\n      const responseData = {\n        auth: {\n          client_token: 'my-token',\n          renewable: true,\n          lease_duration: 7200\n        }\n      };\n      mockedAxios.mockResolvedValueOnce({\n        status: 200,\n        data: responseData\n      });\n\n      const result = await vault.tokenRenewSelf();\n\n      expect(mockedAxios).toHaveBeenCalledWith(\n        expect.objectContaining({\n          method: 'POST',\n          url: 'http://localhost:8200/v1/auth/token/renew-self',\n          headers: expect.objectContaining({\n            'X-Vault-Token': 'my-token'\n          })\n        })\n      );\n      expect(result.auth.lease_duration).toBe(7200);\n    });\n\n    it('should pass increment when provided', async () => {\n      mockedAxios.mockResolvedValueOnce({\n        status: 200,\n        data: { auth: { lease_duration: 3600 } }\n      });\n\n      await vault.tokenRenewSelf({ increment: 3600 });\n\n      expect(mockedAxios).toHaveBeenCalledWith(\n        expect.objectContaining({\n          data: { increment: 3600 }\n        })\n      );\n    });\n\n    it('should handle non-renewable token error', async () => {\n      mockedAxios.mockResolvedValueOnce({\n        status: 400,\n        data: { errors: ['lease is not renewable'] }\n      });\n\n      await expect(vault.tokenRenewSelf()).rejects.toThrow('lease is not renewable');\n    });\n  });\n\n  describe('error handling', () => {\n    let vault: VaultClient;\n\n    beforeEach(() => {\n      vault = createVaultClient({\n        endpoint: 'http://localhost:8200',\n        token: 'test-token'\n      });\n    });\n\n    it('should throw VaultError with response structure', async () => {\n      mockedAxios.mockResolvedValueOnce({\n        status: 500,\n        data: { errors: ['internal server error'] }\n      });\n\n      try {\n        await vault.read('secret/data/hello');\n      } catch (error) {\n        expect(error).toBeInstanceOf(VaultError);\n        expect((error as VaultError).message).toBe('internal server error');\n        expect((error as VaultError).response).toEqual({\n          statusCode: 500,\n          status: 500,\n          body: { errors: ['internal server error'] }\n        });\n      }\n    });\n\n    it('should handle error without errors array', async () => {\n      mockedAxios.mockResolvedValueOnce({\n        status: 503,\n        data: {}\n      });\n\n      await expect(vault.read('secret/data/hello')).rejects.toThrow('Status 503');\n    });\n\n    it('should handle network errors', async () => {\n      const networkError = new Error('Network Error');\n      (networkError as any).isAxiosError = true;\n      (networkError as any).code = 'ECONNREFUSED';\n      mockedAxios.mockRejectedValueOnce(networkError);\n\n      try {\n        await vault.read('secret/data/hello');\n      } catch (error) {\n        expect((error as any).message).toBe('Network Error');\n        expect((error as any).code).toBe('ECONNREFUSED');\n      }\n    });\n\n    it('should handle axios error with response', async () => {\n      const axiosError = new Error('Request failed');\n      (axiosError as any).isAxiosError = true;\n      (axiosError as any).response = {\n        status: 401,\n        data: { errors: ['permission denied'] }\n      };\n      mockedAxios.mockRejectedValueOnce(axiosError);\n\n      await expect(vault.read('secret/data/hello')).rejects.toThrow('permission denied');\n    });\n\n    it('should pass through non-axios errors', async () => {\n      const genericError = new Error('Unknown error');\n      mockedAxios.mockRejectedValueOnce(genericError);\n\n      await expect(vault.read('secret/data/hello')).rejects.toThrow('Unknown error');\n    });\n  });\n\n  describe('requestOptions', () => {\n    it('should pass strictSSL to https agent', async () => {\n      const vault = createVaultClient({\n        endpoint: 'https://vault.example.com',\n        token: 'test-token',\n        requestOptions: {\n          strictSSL: false\n        }\n      });\n\n      mockedAxios.mockResolvedValueOnce({\n        status: 200,\n        data: {}\n      });\n\n      await vault.read('secret/data/hello');\n\n      expect(mockedAxios).toHaveBeenCalledWith(\n        expect.objectContaining({\n          httpsAgent: expect.any(Object)\n        })\n      );\n    });\n\n    it('should not set httpsAgent for http endpoints', async () => {\n      const vault = createVaultClient({\n        endpoint: 'http://localhost:8200',\n        token: 'test-token',\n        requestOptions: {\n          strictSSL: false\n        }\n      });\n\n      mockedAxios.mockResolvedValueOnce({\n        status: 200,\n        data: {}\n      });\n\n      await vault.read('secret/data/hello');\n\n      const callArgs = mockedAxios.mock.calls[0][0] as any;\n      expect(callArgs.httpsAgent).toBeUndefined();\n    });\n\n    it('should configure proxy when provided', async () => {\n      const vault = createVaultClient({\n        endpoint: 'http://localhost:8200',\n        token: 'test-token',\n        requestOptions: {\n          proxy: 'http://proxy.example.com:8080'\n        }\n      });\n\n      mockedAxios.mockResolvedValueOnce({\n        status: 200,\n        data: {}\n      });\n\n      await vault.read('secret/data/hello');\n\n      expect(mockedAxios).toHaveBeenCalledWith(\n        expect.objectContaining({\n          proxy: expect.objectContaining({\n            host: 'proxy.example.com',\n            port: 8080\n          })\n        })\n      );\n    });\n\n    it('should configure proxy with authentication', async () => {\n      const vault = createVaultClient({\n        endpoint: 'http://localhost:8200',\n        token: 'test-token',\n        requestOptions: {\n          proxy: 'http://user:pass@proxy.example.com:8080'\n        }\n      });\n\n      mockedAxios.mockResolvedValueOnce({\n        status: 200,\n        data: {}\n      });\n\n      await vault.read('secret/data/hello');\n\n      expect(mockedAxios).toHaveBeenCalledWith(\n        expect.objectContaining({\n          proxy: expect.objectContaining({\n            host: 'proxy.example.com',\n            port: 8080,\n            auth: {\n              username: 'user',\n              password: 'pass'\n            }\n          })\n        })\n      );\n    });\n  });\n\n  describe('URL construction', () => {\n    it('should construct URL with apiVersion', async () => {\n      const vault = createVaultClient({\n        endpoint: 'http://localhost:8200',\n        apiVersion: 'v2'\n      });\n\n      mockedAxios.mockResolvedValueOnce({\n        status: 200,\n        data: {}\n      });\n\n      await vault.read('secret/data/hello');\n\n      expect(mockedAxios).toHaveBeenCalledWith(\n        expect.objectContaining({\n          url: 'http://localhost:8200/v2/secret/data/hello'\n        })\n      );\n    });\n\n    it('should handle endpoint without trailing slash', async () => {\n      const vault = createVaultClient({\n        endpoint: 'http://localhost:8200'\n      });\n\n      mockedAxios.mockResolvedValueOnce({\n        status: 200,\n        data: {}\n      });\n\n      await vault.read('secret/data/hello');\n\n      expect(mockedAxios).toHaveBeenCalledWith(\n        expect.objectContaining({\n          url: 'http://localhost:8200/v1/secret/data/hello'\n        })\n      );\n    });\n  });\n\n  describe('health endpoint handling', () => {\n    it('should not throw error for sys/health even with non-200 status', async () => {\n      const vault = createVaultClient({\n        endpoint: 'http://localhost:8200'\n      });\n\n      const healthResponse = {\n        initialized: true,\n        sealed: true,\n        standby: true\n      };\n\n      mockedAxios.mockResolvedValueOnce({\n        status: 503,\n        data: healthResponse\n      });\n\n      const result = await vault.read('sys/health');\n      expect(result).toEqual(healthResponse);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-requests/src/utils/node-vault.ts",
    "content": "import axios, { AxiosRequestConfig, AxiosError } from 'axios';\nimport * as https from 'node:https';\n\n/**\n * Configuration options for creating a Vault client\n */\nexport interface VaultConfig {\n  apiVersion?: string;\n  endpoint?: string;\n  token?: string;\n  namespace?: string;\n  requestOptions?: VaultRequestOptions;\n  debug?: (...args: any[]) => void;\n}\n\n/**\n * Request options for Vault HTTP requests\n * Compatible with node-vault's requestOptions\n */\nexport interface VaultRequestOptions {\n  strictSSL?: boolean;\n  ca?: string | Buffer | Array<string | Buffer>;\n  proxy?: string;\n  [key: string]: any;\n}\n\n/**\n * AppRole login arguments\n */\nexport interface ApproleLoginArgs {\n  role?: string;\n  role_id: string;\n  secret_id?: string;\n  mount_point?: string;\n}\n\n/**\n * Token renew arguments\n */\nexport interface TokenRenewArgs {\n  increment?: number | string;\n}\n\n/**\n * Vault API response error structure\n * Includes both statusCode (node-vault style) and status (axios style) for compatibility\n */\nexport class VaultError extends Error {\n  response?: {\n    statusCode: number;\n    status: number; // Alias for axios-style error handling\n    body: any;\n  };\n\n  code?: string; // For network errors\n\n  constructor(message: string, response?: { statusCode: number; body: any }) {\n    super(message);\n    this.name = 'VaultError';\n    if (response) {\n      this.response = {\n        statusCode: response.statusCode,\n        status: response.statusCode, // Alias for compatibility\n        body: response.body\n      };\n    }\n  }\n}\n\n/**\n * Vault client interface - matches node-vault API surface\n */\nexport interface VaultClient {\n  endpoint: string;\n  namespace?: string;\n  token?: string;\n  apiVersion: string;\n\n  read(path: string, requestOptions?: VaultRequestOptions): Promise<any>;\n  write(path: string, data: any, requestOptions?: VaultRequestOptions): Promise<any>;\n  approleLogin(args: ApproleLoginArgs): Promise<any>;\n  tokenLookupSelf(args?: any): Promise<any>;\n  tokenRenewSelf(args?: TokenRenewArgs): Promise<any>;\n}\n\n/**\n * Creates an HTTPS agent based on request options\n */\nfunction createHttpsAgent(options: VaultRequestOptions): https.Agent | undefined {\n  const agentOptions: https.AgentOptions = {};\n  let needsAgent = false;\n\n  if (options.strictSSL === false) {\n    agentOptions.rejectUnauthorized = false;\n    needsAgent = true;\n  }\n\n  if (options.ca) {\n    agentOptions.ca = options.ca;\n    needsAgent = true;\n  }\n\n  return needsAgent ? new https.Agent(agentOptions) : undefined;\n}\n\n/**\n * Handles Vault API response, extracting body or throwing error\n */\nfunction handleVaultResponse(statusCode: number, body: any, path: string): any {\n  // Success responses\n  if (statusCode === 200 || statusCode === 204) {\n    return body;\n  }\n\n  // Health endpoint special handling (matches node-vault behavior)\n  if (path.match(/sys\\/health/) !== null) {\n    return body;\n  }\n\n  // Error responses\n  let message: string;\n  if (body && body.errors && body.errors.length > 0) {\n    message = body.errors[0];\n  } else {\n    message = `Status ${statusCode}`;\n  }\n\n  throw new VaultError(message, { statusCode, body });\n}\n\n/**\n * Creates a Vault client instance\n *\n * This is a drop-in replacement for node-vault, implementing only the methods\n * used by bruno-electron and bruno-cli.\n *\n * @param config - Configuration options\n * @returns VaultClient instance with mutable properties\n *\n * @example\n * ```javascript\n * const vault = createVaultClient({ apiVersion: 'v1' });\n * vault.endpoint = 'https://vault.example.com';\n * vault.token = 'my-token';\n * const secret = await vault.read('secret/data/myapp');\n * ```\n */\nfunction createVaultClient(config: VaultConfig = {}): VaultClient {\n  const debug = config.debug || (() => {});\n  const defaultRequestOptions = config.requestOptions || {};\n\n  /**\n   * Makes an HTTP request to the Vault API\n   */\n  async function request(\n    method: string,\n    path: string,\n    data?: any,\n    requestOptions?: VaultRequestOptions\n  ): Promise<any> {\n    // Merge request options: defaults from config + per-request options\n    const mergedOptions: VaultRequestOptions = {\n      ...defaultRequestOptions,\n      ...requestOptions\n    };\n\n    const endpointOrigin = client.endpoint?.endsWith('/') ? client.endpoint : `${client.endpoint}/`;\n\n    // Build URL\n    const uri = `${endpointOrigin}${client.apiVersion}${path}`;\n    debug(method, uri);\n\n    // Build headers\n    const headers: Record<string, string> = {\n      'Content-Type': 'application/json'\n    };\n\n    if (typeof client.token === 'string' && client.token.length) {\n      headers['X-Vault-Token'] = client.token;\n    }\n\n    if (typeof client.namespace === 'string' && client.namespace.length) {\n      headers['X-Vault-Namespace'] = client.namespace;\n    }\n\n    // Build axios config\n    const axiosConfig: AxiosRequestConfig = {\n      method: method as any,\n      url: uri,\n      headers,\n      validateStatus: () => true // Don't throw on non-2xx status\n    };\n\n    // Add request body for POST/PUT\n    if (data && (method === 'POST' || method === 'PUT')) {\n      axiosConfig.data = data;\n      debug('data:', data);\n    }\n\n    // Configure HTTPS agent\n    if (uri.startsWith('https')) {\n      const agent = createHttpsAgent(mergedOptions);\n      if (agent) {\n        axiosConfig.httpsAgent = agent;\n      }\n    }\n\n    // Configure proxy\n    if (mergedOptions.proxy) {\n      // Parse proxy URL into axios proxy config\n      try {\n        const proxyUrl = new URL(mergedOptions.proxy);\n        axiosConfig.proxy = {\n          host: proxyUrl.hostname,\n          port: parseInt(proxyUrl.port, 10) || (proxyUrl.protocol === 'https:' ? 443 : 80),\n          protocol: proxyUrl.protocol.replace(':', '')\n        };\n        if (proxyUrl.username && proxyUrl.password) {\n          axiosConfig.proxy.auth = {\n            username: decodeURIComponent(proxyUrl.username),\n            password: decodeURIComponent(proxyUrl.password)\n          };\n        }\n      } catch (e) {\n        // If proxy URL parsing fails, pass it as-is for backward compatibility\n        debug('Failed to parse proxy URL:', mergedOptions.proxy);\n      }\n    }\n\n    try {\n      const response = await axios(axiosConfig);\n      return handleVaultResponse(response.status, response.data, path);\n    } catch (error) {\n      // Network errors or other axios errors\n      if (axios.isAxiosError(error)) {\n        const axiosError = error as AxiosError;\n        if (axiosError.response) {\n          // Server responded with error status\n          return handleVaultResponse(\n            axiosError.response.status,\n            axiosError.response.data,\n            path\n          );\n        }\n        // Network error - preserve original error structure\n        const vaultError = new VaultError(axiosError.message);\n        (vaultError as any).code = axiosError.code;\n        throw vaultError;\n      }\n      throw error;\n    }\n  }\n\n  // Create client object with mutable properties\n  const client: VaultClient = {\n    // Mutable properties (support direct assignment like node-vault)\n    apiVersion: config.apiVersion || 'v1',\n    endpoint: config.endpoint || process.env.VAULT_ADDR || 'http://127.0.0.1:8200',\n    token: config.token || process.env.VAULT_TOKEN,\n    namespace: config.namespace || process.env.VAULT_NAMESPACE,\n\n    /**\n     * Read data from a Vault path\n     * @param path - The path to read from (e.g., 'secret/data/myapp')\n     * @param requestOptions - Optional request options\n     */\n    async read(path: string, requestOptions?: VaultRequestOptions): Promise<any> {\n      path = path.startsWith('/') ? path : `/${path}`;\n      debug('read', path);\n      return request('GET', path, undefined, requestOptions);\n    },\n\n    /**\n     * Write data to a Vault path\n     * @param path - The path to write to\n     * @param data - The data to write\n     * @param requestOptions - Optional request options\n     */\n    async write(path: string, data: any, requestOptions?: VaultRequestOptions): Promise<any> {\n      path = path.startsWith('/') ? path : `/${path}`;\n      debug('write', path, data);\n      return request('POST', path, data, requestOptions);\n    },\n\n    /**\n     * Authenticate using AppRole\n     * @param args - AppRole login arguments\n     */\n    async approleLogin(args: ApproleLoginArgs): Promise<any> {\n      debug('approleLogin', args.role_id);\n      const mountPoint = args.mount_point || 'approle';\n      const body: Record<string, any> = {\n        role_id: args.role_id\n      };\n      if (args.secret_id) {\n        body.secret_id = args.secret_id;\n      }\n      return request('POST', `/auth/${mountPoint}/login`, body);\n    },\n\n    /**\n     * Look up the current token's properties\n     */\n    async tokenLookupSelf(args?: any): Promise<any> {\n      debug('tokenLookupSelf');\n      return request('GET', '/auth/token/lookup-self');\n    },\n\n    /**\n     * Renew the current token\n     * @param args - Optional arguments including increment\n     */\n    async tokenRenewSelf(args?: TokenRenewArgs): Promise<any> {\n      debug('tokenRenewSelf');\n      const body: Record<string, any> = {};\n      if (args?.increment !== undefined) {\n        body.increment = args.increment;\n      }\n      return request('POST', '/auth/token/renew-self', Object.keys(body).length > 0 ? body : undefined);\n    }\n  };\n\n  return client;\n}\n\nexport default createVaultClient;\n"
  },
  {
    "path": "packages/bruno-requests/src/utils/proxy-util.spec.ts",
    "content": "import { transformProxyConfig } from './proxy-util';\n\ndescribe('transformProxyConfig', () => {\n  describe('Migration from old to new format', () => {\n    describe('Old Format: enabled (true | false | \"global\")', () => {\n      test('should migrate enabled: true to disabled: false, inherit: false', () => {\n        const oldConfig = {\n          enabled: true,\n          protocol: 'http',\n          hostname: 'proxy.example.com',\n          port: 8080,\n          auth: {\n            enabled: true,\n            username: 'user',\n            password: 'pass'\n          },\n          bypassProxy: 'localhost'\n        };\n\n        const result = transformProxyConfig(oldConfig);\n\n        expect(result).toEqual({\n          inherit: false,\n          config: {\n            protocol: 'http',\n            hostname: 'proxy.example.com',\n            port: 8080,\n            auth: {\n              username: 'user',\n              password: 'pass'\n            },\n            bypassProxy: 'localhost'\n          }\n        });\n        expect((result as any).disabled).toBeUndefined(); // disabled: false is omitted\n      });\n\n      test('should migrate enabled: false to disabled: true, inherit: false', () => {\n        const oldConfig = {\n          enabled: false,\n          protocol: 'http',\n          hostname: 'proxy.example.com',\n          port: 8080,\n          auth: {\n            enabled: false,\n            username: '',\n            password: ''\n          },\n          bypassProxy: ''\n        };\n\n        const result = transformProxyConfig(oldConfig);\n\n        expect((result as any).disabled).toBe(true);\n        expect((result as any).inherit).toBe(false);\n      });\n\n      test('should migrate enabled: \"global\" to disabled: false, inherit: true', () => {\n        const oldConfig = {\n          enabled: 'global' as const,\n          protocol: 'http',\n          hostname: '',\n          port: null,\n          auth: {\n            enabled: false,\n            username: '',\n            password: ''\n          },\n          bypassProxy: ''\n        };\n\n        const result = transformProxyConfig(oldConfig);\n\n        expect((result as any).disabled).toBeUndefined(); // disabled: false is omitted\n        expect((result as any).inherit).toBe(true);\n      });\n\n      test('should migrate auth.enabled: false to auth.disabled: true', () => {\n        const oldConfig = {\n          enabled: true,\n          protocol: 'http',\n          hostname: 'proxy.example.com',\n          port: 8080,\n          auth: {\n            enabled: false,\n            username: 'user',\n            password: 'pass'\n          },\n          bypassProxy: ''\n        };\n\n        const result = transformProxyConfig(oldConfig);\n\n        expect((result as any).config.auth.disabled).toBe(true);\n        expect((result as any).config.auth.username).toBe('user');\n        expect((result as any).config.auth.password).toBe('pass');\n      });\n\n      test('should omit auth.disabled when auth.enabled: true', () => {\n        const oldConfig = {\n          enabled: true,\n          protocol: 'http',\n          hostname: 'proxy.example.com',\n          port: 8080,\n          auth: {\n            enabled: true,\n            username: 'user',\n            password: 'pass'\n          },\n          bypassProxy: ''\n        };\n\n        const result = transformProxyConfig(oldConfig);\n\n        expect((result as any).config.auth.disabled).toBeUndefined();\n        expect((result as any).config.auth.username).toBe('user');\n        expect((result as any).config.auth.password).toBe('pass');\n      });\n    });\n\n    describe('New Format (no migration)', () => {\n      test('should not modify new format with inherit: false', () => {\n        const newConfig = {\n          inherit: false,\n          config: {\n            protocol: 'https',\n            hostname: 'proxy.example.com',\n            port: 8443,\n            auth: {\n              username: 'user',\n              password: 'pass'\n            },\n            bypassProxy: '*.local'\n          }\n        };\n\n        const result = transformProxyConfig(newConfig);\n\n        expect(result).toEqual(newConfig);\n      });\n\n      test('should not modify new format with inherit: true', () => {\n        const newConfig = {\n          inherit: true,\n          config: {\n            protocol: 'http',\n            hostname: '',\n            port: null,\n            auth: {\n              username: '',\n              password: ''\n            },\n            bypassProxy: ''\n          }\n        };\n\n        const result = transformProxyConfig(newConfig);\n\n        expect(result).toEqual(newConfig);\n      });\n\n      test('should not modify new format with disabled: true', () => {\n        const newConfig = {\n          disabled: true,\n          inherit: false,\n          config: {\n            protocol: 'http',\n            hostname: '',\n            port: null,\n            auth: {\n              username: '',\n              password: ''\n            },\n            bypassProxy: ''\n          }\n        };\n\n        const result = transformProxyConfig(newConfig);\n\n        expect(result).toEqual(newConfig);\n      });\n\n      test('should not modify new format with auth.disabled: true', () => {\n        const newConfig = {\n          inherit: false,\n          config: {\n            protocol: 'http',\n            hostname: 'proxy.example.com',\n            port: 8080,\n            auth: {\n              disabled: true,\n              username: 'user',\n              password: 'pass'\n            },\n            bypassProxy: ''\n          }\n        };\n\n        const result = transformProxyConfig(newConfig);\n\n        expect(result).toEqual(newConfig);\n      });\n    });\n\n    describe('Edge Cases', () => {\n      test('should handle missing/null/undefined proxy config', () => {\n        expect(transformProxyConfig(null)).toEqual({});\n        expect(transformProxyConfig(undefined)).toEqual({});\n        expect(transformProxyConfig({})).toEqual({});\n      });\n\n      test('should handle null port values', () => {\n        const oldConfig = {\n          enabled: true,\n          protocol: 'http',\n          hostname: 'proxy.example.com',\n          port: null,\n          auth: {\n            enabled: false,\n            username: '',\n            password: ''\n          },\n          bypassProxy: ''\n        };\n\n        const result = transformProxyConfig(oldConfig);\n\n        expect((result as any).config.port).toBeNull();\n      });\n\n      test('should handle SOCKS protocols', () => {\n        const oldConfig = {\n          enabled: true,\n          protocol: 'socks5',\n          hostname: 'socks.example.com',\n          port: 1080,\n          auth: {\n            enabled: true,\n            username: 'socksuser',\n            password: 'sockspass'\n          },\n          bypassProxy: ''\n        };\n\n        const result = transformProxyConfig(oldConfig);\n\n        expect((result as any).config.protocol).toBe('socks5');\n        expect((result as any).config.hostname).toBe('socks.example.com');\n        expect((result as any).config.port).toBe(1080);\n      });\n\n      test('should handle missing auth object', () => {\n        const oldConfig = {\n          enabled: true,\n          protocol: 'http',\n          hostname: 'proxy.example.com',\n          port: 8080,\n          bypassProxy: ''\n        };\n\n        const result = transformProxyConfig(oldConfig);\n\n        expect((result as any).config.auth).toEqual({\n          username: '',\n          password: ''\n        });\n      });\n\n      test('should handle missing protocol (defaults to http)', () => {\n        const oldConfig = {\n          enabled: true,\n          hostname: 'proxy.example.com',\n          port: 8080\n        };\n\n        const result = transformProxyConfig(oldConfig);\n\n        expect((result as any).config.protocol).toBe('http');\n      });\n\n      test('should handle missing hostname (defaults to empty string)', () => {\n        const oldConfig = {\n          enabled: true,\n          protocol: 'http',\n          port: 8080\n        };\n\n        const result = transformProxyConfig(oldConfig);\n\n        expect((result as any).config.hostname).toBe('');\n      });\n\n      test('should handle missing port (defaults to null)', () => {\n        const oldConfig = {\n          enabled: true,\n          protocol: 'http',\n          hostname: 'proxy.example.com'\n        };\n\n        const result = transformProxyConfig(oldConfig);\n\n        expect((result as any).config.port).toBeNull();\n      });\n\n      test('should handle missing bypassProxy (defaults to empty string)', () => {\n        const oldConfig = {\n          enabled: true,\n          protocol: 'http',\n          hostname: 'proxy.example.com',\n          port: 8080\n        };\n\n        const result = transformProxyConfig(oldConfig);\n\n        expect((result as any).config.bypassProxy).toBe('');\n      });\n\n      test('should handle auth with missing username/password', () => {\n        const oldConfig = {\n          enabled: true,\n          protocol: 'http',\n          hostname: 'proxy.example.com',\n          port: 8080,\n          auth: {\n            enabled: true\n          }\n        };\n\n        const result = transformProxyConfig(oldConfig);\n\n        expect((result as any).config.auth.username).toBe('');\n        expect((result as any).config.auth.password).toBe('');\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-requests/src/utils/proxy-util.ts",
    "content": "/**\n * Transform proxy config from old format to new format.\n * Old format: { enabled: true | false | 'global', protocol, hostname, port, auth: { enabled, ... }, ... }\n * New format: { disabled?, inherit, config: { protocol, hostname, port, auth: { disabled?, ... }, ... } }\n */\n\ninterface OldProxyAuth {\n  enabled?: boolean;\n  username?: string;\n  password?: string;\n}\n\ninterface OldProxyConfig {\n  enabled?: true | false | 'global';\n  protocol?: string;\n  hostname?: string;\n  port?: number | null;\n  auth?: OldProxyAuth;\n  bypassProxy?: string;\n}\n\ninterface NewProxyAuth {\n  disabled?: boolean;\n  username?: string;\n  password?: string;\n}\n\ninterface NewProxyConfig {\n  disabled?: boolean;\n  inherit: boolean;\n  config: {\n    protocol: string;\n    hostname: string;\n    port: number | null;\n    auth: NewProxyAuth;\n    bypassProxy: string;\n  };\n}\n\nexport const transformProxyConfig = (proxy: OldProxyConfig | NewProxyConfig | null | undefined): NewProxyConfig | OldProxyConfig => {\n  proxy = proxy || {};\n  // Check if this is an old format (has 'enabled' property)\n  if (proxy.hasOwnProperty('enabled')) {\n    const oldProxy = proxy as OldProxyConfig;\n    const enabled = oldProxy.enabled;\n\n    const newProxy: NewProxyConfig = {\n      inherit: true,\n      config: {\n        protocol: oldProxy.protocol || 'http',\n        hostname: oldProxy.hostname || '',\n        port: oldProxy.port || null,\n        auth: {\n          username: oldProxy.auth?.username || '',\n          password: oldProxy.auth?.password || ''\n        },\n        bypassProxy: oldProxy.bypassProxy || ''\n      }\n    };\n\n    // Handle old format: enabled (true | false | 'global')\n    if (enabled === true) {\n      newProxy.disabled = false;\n      newProxy.inherit = false;\n    } else if (enabled === false) {\n      newProxy.disabled = true;\n      newProxy.inherit = false;\n    } else if (enabled === 'global') {\n      newProxy.disabled = false;\n      newProxy.inherit = true;\n    }\n\n    // Migrate auth.enabled to auth.disabled\n    if (oldProxy.auth?.enabled === false) {\n      newProxy.config.auth.disabled = true;\n    }\n    // If auth.enabled is true or undefined, omit disabled (defaults to false)\n\n    // Omit disabled: false at top level (optional field)\n    if (newProxy.disabled === false) {\n      delete newProxy.disabled;\n    }\n    // Omit auth.disabled: false (optional field)\n    if (newProxy.config.auth.disabled === false) {\n      delete newProxy.config.auth.disabled;\n    }\n\n    return newProxy;\n  }\n\n  return proxy;\n};\n"
  },
  {
    "path": "packages/bruno-requests/src/utils/shell-env.spec.ts",
    "content": "import { initializeShellEnv } from './shell-env';\n\nlet mockShellEnvResult: Record<string, string> = {};\n\njest.mock('shell-env', () => ({\n  shellEnv: () => Promise.resolve(mockShellEnvResult)\n}));\n\nconst originalPlatform = process.platform;\n\nafterEach(() => {\n  Object.defineProperty(process, 'platform', { value: originalPlatform });\n});\n\ndescribe('initializeShellEnv', () => {\n  test('should add shell env vars that are not in process.env', async () => {\n    delete process.env.TEST_SHELL_VAR;\n    mockShellEnvResult = { TEST_SHELL_VAR: 'from_shell_config' };\n\n    await initializeShellEnv();\n\n    expect(process.env.TEST_SHELL_VAR).toBe('from_shell_config');\n    delete process.env.TEST_SHELL_VAR;\n  });\n\n  test('should not overwrite existing process.env values', async () => {\n    process.env.http_proxy = 'updated_value';\n    mockShellEnvResult = { http_proxy: 'config_file_value' };\n\n    await initializeShellEnv();\n\n    expect(process.env.http_proxy).toBe('updated_value');\n    delete process.env.http_proxy;\n  });\n\n  test('should preserve multiple existing env vars while adding new ones', async () => {\n    process.env.EXISTING_VAR = 'existing';\n    delete process.env.NEW_VAR;\n    mockShellEnvResult = { EXISTING_VAR: 'overwritten', NEW_VAR: 'new_value' };\n\n    await initializeShellEnv();\n\n    expect(process.env.EXISTING_VAR).toBe('existing');\n    expect(process.env.NEW_VAR).toBe('new_value');\n    delete process.env.EXISTING_VAR;\n    delete process.env.NEW_VAR;\n  });\n\n  test('should return the shell env vars (not the merged result)', async () => {\n    mockShellEnvResult = { SOME_VAR: 'value' };\n\n    const result = await initializeShellEnv();\n\n    expect(result).toEqual({ SOME_VAR: 'value' });\n    delete process.env.SOME_VAR;\n  });\n\n  test('should return empty object on Windows', async () => {\n    Object.defineProperty(process, 'platform', { value: 'win32' });\n    mockShellEnvResult = { SHOULD_NOT_APPEAR: 'value' };\n\n    const result = await initializeShellEnv();\n\n    expect(result).toEqual({});\n    expect(process.env.SHOULD_NOT_APPEAR).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/bruno-requests/src/utils/shell-env.ts",
    "content": "/**\n * Shell Environment Utility\n *\n * Fetches environment variables from the user's shell configuration files (e.g., .zshenv, .bashrc)\n */\n\nconst fetchShellEnv = async (): Promise<Record<string, string>> => {\n  // Windows handles environment variables differently - skip\n  if (process.platform === 'win32') {\n    return {};\n  }\n\n  try {\n    // shell-env is ESM-only, so we use dynamic import\n    const { shellEnv } = await import('shell-env');\n    const env = await shellEnv();\n    return env;\n  } catch (error) {\n    return {};\n  }\n};\n\n/**\n * Initializes process.env with shell environment variables.\n * Should be called early in the app startup.\n *\n * @returns The fetched shell environment variables\n */\nexport const initializeShellEnv = async (): Promise<Record<string, string>> => {\n  const shellEnvVars = await fetchShellEnv();\n  for (const [key, value] of Object.entries(shellEnvVars)) {\n    if (!(key in process.env)) {\n      process.env[key] = value;\n    }\n  }\n  return shellEnvVars;\n};\n"
  },
  {
    "path": "packages/bruno-requests/src/utils/timeline-agent.ts",
    "content": "import http from 'node:http';\nimport https from 'node:https';\n\ntype TimelineEntry = {\n  timestamp: Date;\n  type: 'info' | 'tls' | 'error';\n  message: string;\n};\n\ntype CaCertificatesCount = {\n  root?: number;\n  system?: number;\n  extra?: number;\n  custom?: number;\n};\n\ntype AgentOptions = {\n  rejectUnauthorized?: boolean;\n  ca?: string | string[] | Buffer | Buffer[];\n  cert?: string | Buffer;\n  key?: string | Buffer;\n  pfx?: string | Buffer;\n  passphrase?: string;\n  minVersion?: string;\n  secureProtocol?: string;\n  keepAlive?: boolean;\n  ALPNProtocols?: string[];\n  caCertificatesCount?: CaCertificatesCount;\n  proxy?: string;\n  secureContext?: any;\n};\n\ntype AgentClass = new (options: AgentOptions, timeline?: TimelineEntry[]) => https.Agent;\ntype ProxyAgentClass = new (proxyUri: string, options?: AgentOptions) => https.Agent;\n\ntype HttpAgentOptions = {\n  keepAlive?: boolean;\n  proxy?: string;\n};\n\ntype HttpAgentClass = new (options: HttpAgentOptions, timeline?: TimelineEntry[]) => http.Agent;\ntype HttpProxyAgentClass = new (proxyUri: string, options?: HttpAgentOptions) => http.Agent;\n\n/**\n * Creates a timeline-aware agent class that logs TLS connection events.\n * The returned class wraps the base agent and adds timeline logging for:\n * - SSL validation status\n * - Proxy usage\n * - ALPN protocol negotiation\n * - CA certificates info\n * - DNS lookups\n * - Connection establishment\n * - TLS handshake details\n * - Server certificate info\n */\nfunction createTimelineAgentClass<T extends ProxyAgentClass | typeof https.Agent>(BaseAgentClass: T): AgentClass {\n  return class TimelineAgent extends (BaseAgentClass as any) {\n    timeline: TimelineEntry[];\n    alpnProtocols: string[];\n    caProvided: boolean;\n    caCertificatesCount: CaCertificatesCount;\n\n    /**\n     * Helper method to log entries to the timeline.\n     */\n    private log(type: 'info' | 'tls' | 'error', message: string): void {\n      this.timeline.push({\n        timestamp: new Date(),\n        type,\n        message\n      });\n    }\n\n    constructor(options: AgentOptions, timeline?: TimelineEntry[]) {\n      const caCertificatesCount = options.caCertificatesCount || {};\n      const optionsCopy = { ...options };\n      delete optionsCopy.caCertificatesCount;\n\n      // For proxy agents, the first argument is the proxy URI and the second is options\n      if (optionsCopy?.proxy) {\n        const { proxy: proxyUri, ...agentOptions } = optionsCopy;\n        // Ensure TLS options are properly set\n        const tlsOptions = {\n          ...agentOptions,\n          rejectUnauthorized: agentOptions.rejectUnauthorized ?? true\n        };\n        super(proxyUri, tlsOptions);\n        this.timeline = Array.isArray(timeline) ? timeline : [];\n        this.alpnProtocols = tlsOptions.ALPNProtocols || ['h2', 'http/1.1'];\n        this.caProvided = !!(tlsOptions.ca || tlsOptions.secureContext);\n\n        // Log TLS verification status and proxy details\n        this.log('info', `SSL validation: ${tlsOptions.rejectUnauthorized ? 'enabled' : 'disabled'}`);\n        this.log('info', `Using proxy: ${proxyUri}`);\n      } else {\n        // This is a regular HTTPS agent case\n        const tlsOptions = {\n          ...optionsCopy,\n          rejectUnauthorized: optionsCopy.rejectUnauthorized ?? true\n        };\n        super(tlsOptions);\n        this.timeline = Array.isArray(timeline) ? timeline : [];\n        this.alpnProtocols = optionsCopy.ALPNProtocols || ['h2', 'http/1.1'];\n        this.caProvided = !!(optionsCopy.ca || optionsCopy.secureContext);\n\n        // Log TLS verification status\n        this.log('info', `SSL validation: ${tlsOptions.rejectUnauthorized ? 'enabled' : 'disabled'}`);\n      }\n\n      this.caCertificatesCount = caCertificatesCount;\n    }\n\n    createConnection(options: any, callback: any) {\n      const { host, port } = options;\n\n      // Capture the current timeline reference to avoid race conditions\n      // when multiple concurrent requests reuse the same cached agent\n      const timeline = this.timeline;\n      const log = (type: 'info' | 'tls' | 'error', message: string): void => {\n        timeline.push({\n          timestamp: new Date(),\n          type,\n          message\n        });\n      };\n\n      // Log ALPN protocols offered\n      if (this.alpnProtocols && this.alpnProtocols.length > 0) {\n        log('tls', `ALPN: offers ${this.alpnProtocols.join(', ')}`);\n      }\n\n      const rootCerts = this.caCertificatesCount.root || 0;\n      const systemCerts = this.caCertificatesCount.system || 0;\n      const extraCerts = this.caCertificatesCount.extra || 0;\n      const customCerts = this.caCertificatesCount.custom || 0;\n\n      log('tls', `CA Certificates: ${rootCerts} root, ${systemCerts} system, ${extraCerts} extra, ${customCerts} custom`);\n\n      // Log \"Trying host:port...\"\n      log('info', `Trying ${host}:${port}...`);\n\n      let socket: any;\n      try {\n        socket = super.createConnection(options, callback);\n      } catch (error: any) {\n        log('error', `Error creating connection: ${error.message}`);\n        error.timeline = timeline;\n        throw error;\n      }\n\n      // Attach event listeners to the socket\n      socket?.on('lookup', (err: Error | null, address: string, family: number, host: string) => {\n        if (err) {\n          log('error', `DNS lookup error for ${host}: ${err.message}`);\n        } else {\n          log('info', `DNS lookup: ${host} -> ${address}`);\n        }\n      });\n\n      socket?.on('connect', () => {\n        const address = socket.remoteAddress || host;\n        const remotePort = socket.remotePort || port;\n\n        log('info', `Connected to ${host} (${address}) port ${remotePort}`);\n      });\n\n      socket?.on('secureConnect', () => {\n        const protocol = socket.getProtocol?.() || 'SSL/TLS';\n        const cipher = socket.getCipher?.();\n        const cipherSuite = cipher ? `${cipher.name} (${cipher.version})` : 'Unknown cipher';\n\n        log('tls', `SSL connection using ${protocol} / ${cipherSuite}`);\n\n        // ALPN protocol\n        const alpnProtocol = socket.alpnProtocol || 'None';\n        log('tls', `ALPN: server accepted ${alpnProtocol}`);\n\n        // Server certificate\n        const cert = socket.getPeerCertificate?.(true);\n        if (cert) {\n          log('tls', `Server certificate:`);\n          if (cert.subject) {\n            log('tls', ` subject: ${Object.entries(cert.subject).map(([k, v]) => `${k}=${v}`).join(', ')}`);\n          }\n          if (cert.valid_from) {\n            log('tls', ` start date: ${cert.valid_from}`);\n          }\n          if (cert.valid_to) {\n            log('tls', ` expire date: ${cert.valid_to}`);\n          }\n          if (cert.subjectaltname) {\n            log('tls', ` subjectAltName: ${cert.subjectaltname}`);\n          }\n          if (cert.issuer) {\n            log('tls', ` issuer: ${Object.entries(cert.issuer).map(([k, v]) => `${k}=${v}`).join(', ')}`);\n          }\n\n          // SSL certificate verification status\n          if (socket.authorized !== false) {\n            log('tls', `SSL certificate verify ok.`);\n          } else {\n            log('tls', `SSL certificate verification skipped (rejectUnauthorized: false).`);\n          }\n        }\n      });\n\n      socket?.on('error', (err: Error) => {\n        log('error', `Socket error: ${err.message}`);\n      });\n\n      return socket;\n    }\n  } as unknown as AgentClass;\n}\n\n/**\n * Creates a timeline-aware HTTP agent class that logs connection events.\n * The returned class wraps the base HTTP agent and adds timeline logging for:\n * - Proxy usage (when applicable)\n * - DNS lookups\n * - Connection establishment\n * - Errors\n *\n * This is a simplified version of createTimelineAgentClass for HTTP (non-TLS) connections.\n */\nfunction createTimelineHttpAgentClass<T extends HttpProxyAgentClass | typeof http.Agent>(BaseAgentClass: T): HttpAgentClass {\n  return class TimelineHttpAgent extends (BaseAgentClass as any) {\n    timeline: TimelineEntry[];\n\n    /**\n     * Helper method to log entries to the timeline.\n     */\n    private log(type: 'info' | 'tls' | 'error', message: string): void {\n      this.timeline.push({\n        timestamp: new Date(),\n        type,\n        message\n      });\n    }\n\n    constructor(options: HttpAgentOptions, timeline?: TimelineEntry[]) {\n      const optionsCopy = { ...options };\n\n      // For proxy agents, the first argument is the proxy URI and the second is options\n      if (optionsCopy?.proxy) {\n        const { proxy: proxyUri, ...agentOptions } = optionsCopy;\n        super(proxyUri, agentOptions);\n        this.timeline = Array.isArray(timeline) ? timeline : [];\n\n        // Log proxy details\n        this.log('info', `Using proxy: ${proxyUri}`);\n      } else {\n        super(optionsCopy);\n        this.timeline = Array.isArray(timeline) ? timeline : [];\n      }\n    }\n\n    createConnection(options: any, callback: any) {\n      const { host, port } = options;\n\n      // Capture the current timeline reference to avoid race conditions\n      // when multiple concurrent requests reuse the same cached agent\n      const timeline = this.timeline;\n      const log = (type: 'info' | 'tls' | 'error', message: string): void => {\n        timeline.push({\n          timestamp: new Date(),\n          type,\n          message\n        });\n      };\n\n      // Log \"Trying host:port...\"\n      log('info', `Trying ${host}:${port}...`);\n\n      let socket: any;\n      try {\n        socket = super.createConnection(options, callback);\n      } catch (error: any) {\n        log('error', `Error creating connection: ${error.message}`);\n        error.timeline = timeline;\n        throw error;\n      }\n\n      // Attach event listeners to the socket\n      socket?.on('lookup', (err: Error | null, address: string, family: number, host: string) => {\n        if (err) {\n          log('error', `DNS lookup error for ${host}: ${err.message}`);\n        } else {\n          log('info', `DNS lookup: ${host} -> ${address}`);\n        }\n      });\n\n      socket?.on('connect', () => {\n        const address = socket.remoteAddress || host;\n        const remotePort = socket.remotePort || port;\n\n        log('info', `Connected to ${host} (${address}) port ${remotePort}`);\n      });\n\n      socket?.on('error', (err: Error) => {\n        log('error', `Socket error: ${err.message}`);\n      });\n\n      return socket;\n    }\n  } as unknown as HttpAgentClass;\n}\n\nexport { createTimelineAgentClass, createTimelineHttpAgentClass, TimelineEntry, AgentOptions, HttpAgentOptions, CaCertificatesCount, AgentClass, HttpAgentClass, ProxyAgentClass, HttpProxyAgentClass };\n"
  },
  {
    "path": "packages/bruno-requests/src/utils/url-validation.spec.ts",
    "content": "import { isPotentiallyTrustworthyOrigin } from './url-validation';\n\ndescribe('isPotentiallyTrustworthyOrigin', () => {\n  describe('secure schemes', () => {\n    it('should return true for HTTPS URLs', () => {\n      expect(isPotentiallyTrustworthyOrigin('https://example.com')).toBe(true);\n      expect(isPotentiallyTrustworthyOrigin('https://api.github.com/v1/users')).toBe(true);\n      expect(isPotentiallyTrustworthyOrigin('https://localhost:3000')).toBe(true);\n    });\n\n    it('should return true for WSS URLs', () => {\n      expect(isPotentiallyTrustworthyOrigin('wss://example.com')).toBe(true);\n      expect(isPotentiallyTrustworthyOrigin('wss://localhost:8080/ws')).toBe(true);\n    });\n\n    it('should return true for file URLs', () => {\n      expect(isPotentiallyTrustworthyOrigin('file:///path/to/file.html')).toBe(true);\n      expect(isPotentiallyTrustworthyOrigin('file://localhost/path/to/file.html')).toBe(true);\n    });\n  });\n\n  describe('insecure schemes', () => {\n    it('should return false for HTTP URLs with non-localhost domains', () => {\n      expect(isPotentiallyTrustworthyOrigin('http://example.com')).toBe(false);\n      expect(isPotentiallyTrustworthyOrigin('http://api.github.com')).toBe(false);\n    });\n\n    it('should return false for WS URLs with non-localhost domains', () => {\n      expect(isPotentiallyTrustworthyOrigin('ws://example.com')).toBe(false);\n      expect(isPotentiallyTrustworthyOrigin('ws://api.github.com')).toBe(false);\n    });\n\n    it('should return false for other schemes', () => {\n      expect(isPotentiallyTrustworthyOrigin('ftp://example.com')).toBe(false);\n      expect(isPotentiallyTrustworthyOrigin('ssh://example.com')).toBe(false);\n    });\n\n    it('should return true for HTTP/WS URLs with localhost (localhost is always trustworthy)', () => {\n      expect(isPotentiallyTrustworthyOrigin('http://localhost')).toBe(true);\n      expect(isPotentiallyTrustworthyOrigin('ws://localhost')).toBe(true);\n    });\n  });\n\n  describe('loopback addresses', () => {\n    describe('IPv4 loopback', () => {\n      it('should return true for 127.0.0.1', () => {\n        expect(isPotentiallyTrustworthyOrigin('http://127.0.0.1')).toBe(true);\n        expect(isPotentiallyTrustworthyOrigin('http://127.0.0.1:3000')).toBe(true);\n      });\n\n      it('should return true for other 127.x.x.x addresses', () => {\n        expect(isPotentiallyTrustworthyOrigin('http://127.0.0.0')).toBe(true);\n        expect(isPotentiallyTrustworthyOrigin('http://127.255.255.255')).toBe(true);\n        expect(isPotentiallyTrustworthyOrigin('http://127.1.2.3')).toBe(true);\n      });\n\n      it('should return false for non-loopback IPv4 addresses', () => {\n        expect(isPotentiallyTrustworthyOrigin('http://192.168.1.1')).toBe(false);\n        expect(isPotentiallyTrustworthyOrigin('http://10.0.0.1')).toBe(false);\n        expect(isPotentiallyTrustworthyOrigin('http://172.16.0.1')).toBe(false);\n        expect(isPotentiallyTrustworthyOrigin('http://8.8.8.8')).toBe(false);\n      });\n    });\n\n    describe('IPv6 loopback', () => {\n      it('should return true for ::1', () => {\n        expect(isPotentiallyTrustworthyOrigin('http://[::1]')).toBe(true);\n        expect(isPotentiallyTrustworthyOrigin('http://[::1]:3000')).toBe(true);\n      });\n\n      it('should return false for non-loopback IPv6 addresses', () => {\n        expect(isPotentiallyTrustworthyOrigin('http://[2001:db8::1]')).toBe(false);\n        expect(isPotentiallyTrustworthyOrigin('http://[fe80::1]')).toBe(false);\n      });\n    });\n  });\n\n  describe('localhost hostnames', () => {\n    it('should return true for localhost and *.localhost domains', () => {\n      expect(isPotentiallyTrustworthyOrigin('http://localhost')).toBe(true);\n      expect(isPotentiallyTrustworthyOrigin('http://localhost:3000')).toBe(true);\n      expect(isPotentiallyTrustworthyOrigin('http://app.localhost')).toBe(true);\n      expect(isPotentiallyTrustworthyOrigin('http://api.localhost')).toBe(true);\n      expect(isPotentiallyTrustworthyOrigin('http://sub.domain.localhost')).toBe(true);\n    });\n\n    it('should handle case insensitive localhost', () => {\n      expect(isPotentiallyTrustworthyOrigin('http://LOCALHOST')).toBe(true);\n      expect(isPotentiallyTrustworthyOrigin('http://LocalHost')).toBe(true);\n      expect(isPotentiallyTrustworthyOrigin('http://APP.LOCALHOST')).toBe(true);\n    });\n\n    it('should return false for non-localhost domains', () => {\n      expect(isPotentiallyTrustworthyOrigin('http://api.example.com')).toBe(false);\n      expect(isPotentiallyTrustworthyOrigin('http://localhost.example.com')).toBe(false);\n    });\n  });\n\n  describe('edge cases', () => {\n    it('should handle trailing dots in hostnames', () => {\n      expect(isPotentiallyTrustworthyOrigin('http://localhost.')).toBe(true);\n      expect(isPotentiallyTrustworthyOrigin('http://app.localhost.')).toBe(true);\n      expect(isPotentiallyTrustworthyOrigin('http://example.com.')).toBe(false);\n    });\n\n    it('should handle URLs with query parameters and fragments', () => {\n      expect(isPotentiallyTrustworthyOrigin('https://example.com/path?query=value#fragment')).toBe(true);\n      expect(isPotentiallyTrustworthyOrigin('http://localhost/path?query=value#fragment')).toBe(true);\n      expect(isPotentiallyTrustworthyOrigin('http://api.example.com/path?query=value#fragment')).toBe(false);\n    });\n\n    it('should handle URLs with authentication', () => {\n      expect(isPotentiallyTrustworthyOrigin('https://user:pass@example.com')).toBe(true);\n      expect(isPotentiallyTrustworthyOrigin('http://user:pass@localhost')).toBe(true);\n      expect(isPotentiallyTrustworthyOrigin('http://user:pass@api.example.com')).toBe(false);\n    });\n  });\n\n  describe('mixed scenarios', () => {\n    it('should prioritize secure schemes over hostname checks', () => {\n      // Even though example.com is not localhost, HTTPS makes it trustworthy\n      expect(isPotentiallyTrustworthyOrigin('https://example.com')).toBe(true);\n\n      // Even though 192.168.1.1 is not loopback, HTTPS makes it trustworthy\n      expect(isPotentiallyTrustworthyOrigin('https://192.168.1.1')).toBe(true);\n    });\n\n    it('should handle localhost with different schemes', () => {\n      expect(isPotentiallyTrustworthyOrigin('https://localhost')).toBe(true);\n      expect(isPotentiallyTrustworthyOrigin('wss://localhost')).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-requests/src/utils/url-validation.ts",
    "content": "import { isIPv4, isIPv6, isIP } from 'is-ip';\n\nconst hostNoBrackets = (host: string): string => {\n  if (host.length >= 2 && host.startsWith('[') && host.endsWith(']')) {\n    return host.substring(1, host.length - 1);\n  }\n  return host;\n};\n\nconst isLoopbackV4 = (address: string): boolean => {\n  const octets = address.split('.');\n  if (octets.length !== 4 || parseInt(octets[0], 10) !== 127) {\n    return false;\n  }\n  return octets.every((octet) => {\n    const n = parseInt(octet, 10);\n    return !Number.isNaN(n) && n >= 0 && n <= 255;\n  });\n};\n\nconst isLoopbackV6 = (address: string): boolean => address === '::1';\n\nconst isIpLoopback = (address: string): boolean => {\n  if (isIPv4(address)) {\n    return isLoopbackV4(address);\n  }\n  if (isIPv6(address)) {\n    return isLoopbackV6(address);\n  }\n  return false;\n};\n\nconst isNormalizedLocalhostTLD = (host: string): boolean => host.toLowerCase().endsWith('.localhost');\n\nconst isLocalHostname = (host: string): boolean => {\n  return host.toLowerCase() === 'localhost' || isNormalizedLocalhostTLD(host);\n};\n\n/**\n * Mirrors Chrome / Secure Contexts spec for \"potentially trustworthy origins\".\n */\nconst isPotentiallyTrustworthyOrigin = (urlString: string): boolean => {\n  let url: URL;\n  try {\n    url = new URL(urlString);\n  } catch {\n    return false; // invalid URL or opaque origin\n  }\n\n  const scheme = url.protocol.replace(':', '').toLowerCase();\n  const hostname = hostNoBrackets(url.hostname).replace(/\\.+$/, '');\n\n  // Secure schemes\n  if (scheme === 'https' || scheme === 'wss' || scheme === 'file') {\n    return true;\n  }\n\n  // IP literals\n  if (isIP(hostname)) {\n    return isIpLoopback(hostname);\n  }\n\n  // localhost / *.localhost\n  return isLocalHostname(hostname);\n};\n\nexport { isPotentiallyTrustworthyOrigin };\n"
  },
  {
    "path": "packages/bruno-requests/src/ws/ws-client.js",
    "content": "import ws from 'ws';\nimport { hexy as hexdump } from 'hexy';\nimport { getParsedWsUrlObject } from './ws-url';\n\n/**\n * Safely parse JSON string with error handling\n * @param {string} jsonString - The JSON string to parse\n * @param {string} context - Context for error messages\n * @returns {Object} Parsed object or throws error with context\n * @throws {Error} If JSON parsing fails\n */\nconst safeParseJSON = (jsonString, context = 'JSON string') => {\n  try {\n    return JSON.parse(jsonString);\n  } catch (error) {\n    const errorMessage = `Failed to parse ${context}: ${error.message}`;\n    console.error(errorMessage, {\n      originalString: jsonString,\n      parseError: error\n    });\n    throw new Error(errorMessage);\n  }\n};\n\nconst normalizeMessageByFormat = (message, format) => {\n  if (!message) {\n    return '';\n  }\n  switch (format) {\n    case 'json':\n      // If it was already stringified, do not double encode\n      if (typeof message === 'string') {\n        return message;\n      }\n      return JSON.stringify(message);\n    case 'raw':\n    case 'xml':\n      return message;\n    default: {\n      if (typeof message === 'string') {\n        return message;\n      }\n      if (typeof message === 'object') {\n        return JSON.stringify(message);\n      }\n      console.warn('Received message of unhandled type.', { type: typeof message });\n      return '';\n    }\n  }\n};\n\nconst createSequencer = () => {\n  const seq = {};\n\n  const nextSeq = (requestId, collectionId) => {\n    seq[requestId] ||= {};\n    seq[requestId][collectionId] ||= 0;\n    return ++seq[requestId][collectionId];\n  };\n\n  /**\n   * @param {string} requestId\n   * @param {string} [collectionId]\n   */\n  const clean = (requestId, collectionId = undefined) => {\n    if (collectionId) {\n      delete seq[requestId][collectionId];\n    }\n    if (!Object.keys(seq[requestId]).length) {\n      delete seq[requestId];\n    }\n  };\n\n  return {\n    next: nextSeq,\n    clean\n  };\n};\n\nconst seq = createSequencer();\n\nclass WsClient {\n  messageQueues = {};\n  activeConnections = new Map();\n  connectionKeepAlive = new Map();\n\n  constructor(eventCallback) {\n    this.eventCallback = eventCallback;\n  }\n\n  /**\n   * Start a WebSocket connection\n   * @param {Object} params - Connection parameters\n   * @param {Object} params.request - The WebSocket request object\n   * @param {Object} params.collection - The collection object\n   * @param {Object} params.options - Additional connection options\n   */\n  async startConnection({ request, collection, options = {} }) {\n    const { url, headers } = request;\n    const { timeout = 30000, keepAlive = false, keepAliveInterval = 10_000, sslOptions = {} } = options;\n\n    const parsedUrl = getParsedWsUrlObject(url);\n    const timeoutAsNumber = Number(timeout);\n    const validTimeout = isNaN(timeoutAsNumber) ? 30000 : timeoutAsNumber;\n\n    const requestId = request.uid;\n    const collectionUid = collection.uid;\n\n    try {\n      // Create WebSocket connection\n      // Note: unlike the standard Websocket constructor the `ws` library doesn't support adding Protocols as a single string\n      // and instead needs it broken down manually, make sure this tested with multiple protocols again.\n      const protocols = []\n        .concat([headers['Sec-WebSocket-Protocol'], headers['sec-websocket-protocol']])\n        .filter(Boolean)\n        .map((d) => d.split(','))\n        .flat()\n        .map((d) => d.trim());\n\n      const protocolVersion = headers['Sec-WebSocket-Version'] || headers['sec-websocket-version'];\n\n      const wsOptions = {\n        headers,\n        handshakeTimeout: validTimeout,\n        followRedirects: true,\n        rejectUnauthorized: sslOptions.rejectUnauthorized,\n        ca: sslOptions.ca,\n        cert: sslOptions.cert,\n        key: sslOptions.key,\n        pfx: sslOptions.pfx,\n        passphrase: sslOptions.passphrase\n      };\n\n      if (protocolVersion) {\n        // Force convert to number since `ws` doesn't do it for you\n        const asNumber = Number(protocolVersion);\n        if (!isNaN(asNumber)) {\n          wsOptions.protocolVersion = asNumber;\n        }\n      }\n\n      const wsConnection = new ws.WebSocket(parsedUrl.fullUrl, protocols, wsOptions);\n\n      // Set up event handlers\n      this.#setupWsEventHandlers(wsConnection, requestId, collectionUid, { keepAlive, keepAliveInterval });\n\n      // Store the connection\n      this.#addConnection(requestId, collectionUid, wsConnection);\n\n      // Emit connecting event\n      this.eventCallback('main:ws:connecting', requestId, collectionUid);\n\n      return wsConnection;\n    } catch (error) {\n      console.error('Error creating WebSocket connection:', error);\n      this.eventCallback('main:ws:error', requestId, collectionUid, {\n        error: error.message\n      });\n      throw error;\n    }\n  }\n\n  #getMessageQueueId(requestId) {\n    return `${requestId}`;\n  }\n\n  queueMessage(requestId, collectionUid, message, format = 'raw') {\n    const connectionMeta = this.activeConnections.get(requestId);\n\n    const mqKey = this.#getMessageQueueId(requestId);\n    this.messageQueues[mqKey] ||= [];\n    this.messageQueues[mqKey].push({\n      message,\n      format\n    });\n\n    if (connectionMeta && connectionMeta.connection && connectionMeta.connection.readyState === WebSocket.OPEN) {\n      this.#flushQueue(requestId, collectionUid);\n      return;\n    }\n  }\n\n  #flushQueue(requestId, collectionUid) {\n    const mqKey = this.#getMessageQueueId(requestId);\n    if (!(mqKey in this.messageQueues)) return;\n    while (this.messageQueues[mqKey].length > 0) {\n      const { message, format } = this.messageQueues[mqKey].shift();\n      this.sendMessage(requestId, collectionUid, message, format);\n    }\n  }\n\n  /**\n   * Send a message to an active WebSocket connection\n   * @param {string} requestId - The request ID of the active connection\n   * @param {string} collectionUid - The collection UID for the request\n   * @param {Object|string} message - The message to send\n   */\n  sendMessage(requestId, collectionUid, message, format = 'raw') {\n    const connectionMeta = this.activeConnections.get(requestId);\n\n    if (connectionMeta.connection && connectionMeta.connection.readyState === WebSocket.OPEN) {\n      const payload = normalizeMessageByFormat(message, format);\n\n      // Send the message\n      connectionMeta.connection.send(payload, (error) => {\n        if (error) {\n          this.eventCallback('main:ws:error', requestId, collectionUid, { error });\n        } else {\n          // Emit message sent event\n          this.eventCallback('main:ws:message', requestId, collectionUid, {\n            message: payload,\n            messageHexdump: hexdump(payload),\n            type: 'outgoing',\n            seq: seq.next(requestId, collectionUid),\n            timestamp: Date.now()\n          });\n        }\n      });\n    } else {\n      const error = new Error('WebSocket connection not available or not open');\n      this.eventCallback('main:ws:error', requestId, collectionUid, {\n        error: error.message\n      });\n    }\n  }\n\n  /**\n   * Close a WebSocket connection\n   * @param {string} requestId - The request ID to close\n   * @param {number} code - Close code (optional)\n   * @param {string} reason - Close reason (optional)\n   */\n  close(requestId, code = 1000, reason = 'Client initiated close') {\n    const connectionMeta = this.activeConnections.get(requestId);\n    if (connectionMeta?.connection) {\n      connectionMeta.connection.close(code, reason);\n      this.#removeConnection(requestId);\n      seq.clean(requestId);\n    }\n  }\n\n  /**\n   * Check if a connection is active\n   * @param {string} requestId - The request ID to check\n   * @returns {boolean} - Whether the connection is active\n   */\n  isConnectionActive(requestId) {\n    const connectionMeta = this.activeConnections.get(requestId);\n    return connectionMeta && connectionMeta.connection.readyState === ws.WebSocket.OPEN;\n  }\n\n  /**\n   * Get all active connection IDs\n   * @returns {string[]} Array of active connection IDs\n   */\n  getActiveConnectionIds() {\n    return Array.from(this.activeConnections.keys());\n  }\n\n  closeForCollection(collectionUid) {\n    [...this.activeConnections.keys()].forEach((k) => {\n      const meta = this.activeConnections.get(k);\n      if (meta.collectionUid === collectionUid) {\n        meta.connection.close();\n        this.activeConnections.delete(k);\n      }\n    });\n  }\n\n  /**\n   * Clear all active connections\n   */\n  clearAllConnections() {\n    const connectionIds = this.getActiveConnectionIds();\n\n    this.activeConnections.forEach((connection) => {\n      if (connection.readyState === WebSocket.OPEN) {\n        connection.close(1000, 'Client clearing all connections');\n      }\n    });\n\n    this.activeConnections.clear();\n\n    // Emit an event with empty active connection IDs\n    if (connectionIds.length > 0) {\n      this.eventCallback('main:ws:connections-changed', {\n        type: 'cleared',\n        activeConnectionIds: []\n      });\n    }\n  }\n\n  /**\n   * Set up WebSocket event handlers\n   * @param {WebSocket} ws - The WebSocket instance\n   * @param {string} requestId - The request ID\n   * @param {string} collectionUid - The collection UID\n   * @param {object} options\n   * @param {boolean} options.keepAlive - keep the connection alive\n   * @param {number} options.keepAliveInterval - What the interval for keeping interval\n   * @private\n   */\n  #setupWsEventHandlers(ws, requestId, collectionUid, options) {\n    ws.on('open', () => {\n      this.#flushQueue(requestId, collectionUid);\n\n      if (options.keepAlive) {\n        const handle = setInterval(() => {\n          ws.isAlive = false;\n          ws.ping();\n        }, options.keepAliveInterval);\n\n        this.connectionKeepAlive.set(requestId, handle);\n      }\n\n      this.eventCallback('main:ws:open', requestId, collectionUid, {\n        timestamp: Date.now(),\n        url: ws.url,\n        seq: seq.next(requestId, collectionUid)\n      });\n    });\n\n    ws.on('redirect', (url, req) => {\n      const headerNames = req.getHeaderNames();\n      const headers = Object.fromEntries(headerNames.map((d) => [d, req.getHeader(d)]));\n      this.eventCallback('main:ws:redirect', requestId, collectionUid, {\n        message: `Redirected to ${url}`,\n        type: 'info',\n        timestamp: Date.now(),\n        headers: headers,\n        seq: seq.next(requestId, collectionUid)\n      });\n    });\n\n    ws.on('upgrade', (response) => {\n      this.eventCallback('main:ws:upgrade', requestId, collectionUid, {\n        type: 'info',\n        timestamp: Date.now(),\n        seq: seq.next(requestId, collectionUid),\n        headers: { ...response.headers }\n      });\n    });\n\n    ws.on('message', (data) => {\n      try {\n        const message = JSON.parse(data.toString());\n        this.eventCallback('main:ws:message', requestId, collectionUid, {\n          message,\n          messageHexdump: hexdump(Buffer.from(data)),\n          type: 'incoming',\n          seq: seq.next(requestId, collectionUid),\n          timestamp: Date.now()\n        });\n      } catch (error) {\n        // If parsing fails, send as raw data\n        this.eventCallback('main:ws:message', requestId, collectionUid, {\n          message: data.toString(),\n          messageHexdump: hexdump(data),\n          type: 'incoming',\n          seq: seq.next(requestId, collectionUid),\n          timestamp: Date.now()\n        });\n      }\n    });\n\n    ws.on('close', (code, reason) => {\n      this.eventCallback('main:ws:close', requestId, collectionUid, {\n        code,\n        reason: Buffer.from(reason).toString(),\n        seq: seq.next(requestId, collectionUid),\n        timestamp: Date.now()\n      });\n      seq.clean(requestId, collectionUid);\n      this.#removeConnection(requestId);\n    });\n\n    ws.on('error', (error) => {\n      this.eventCallback('main:ws:error', requestId, collectionUid, {\n        error: error.message,\n        seq: seq.next(requestId, collectionUid),\n        timestamp: Date.now()\n      });\n    });\n  }\n\n  /**\n   * Add a connection to the active connections map and emit an event\n   * @param {string} requestId - The request ID\n   * @param {WebSocket} connection - The WebSocket connection\n   * @private\n   */\n  #addConnection(requestId, collectionUid, connection) {\n    this.activeConnections.set(requestId, { collectionUid, connection });\n\n    // Emit an event with all active connection IDs\n    this.eventCallback('main:ws:connections-changed', {\n      type: 'added',\n      requestId,\n      seq: seq.next(requestId, collectionUid),\n      activeConnectionIds: this.getActiveConnectionIds()\n    });\n  }\n\n  /**\n   * Remove a connection from the active connections map and emit an event\n   * @param {string} requestId - The request ID\n   * @private\n   */\n  #removeConnection(requestId) {\n    if (this.connectionKeepAlive.has(requestId)) {\n      clearInterval(this.connectionKeepAlive.get(requestId));\n      this.connectionKeepAlive.delete(requestId);\n    }\n\n    const mqId = this.#getMessageQueueId(requestId);\n    if (mqId in this.messageQueues) {\n      this.messageQueues[mqId] = [];\n    }\n\n    if (this.activeConnections.has(requestId)) {\n      this.activeConnections.delete(requestId);\n\n      // Emit an event with all active connection IDs\n      this.eventCallback('main:ws:connections-changed', {\n        type: 'removed',\n        requestId,\n        activeConnectionIds: this.getActiveConnectionIds()\n      });\n    }\n  }\n\n  /**\n   * Get the connection status of a connection\n   * @param {string} requestId - The request ID to get the connection status of\n   * @returns {string} - The connection status\n   */\n  // Returns \"disconnected\", \"connecting\", \"connected\"\n  connectionStatus(requestId) {\n    const connectionMeta = this.activeConnections.get(requestId);\n    if (connectionMeta?.connection?.readyState === ws.WebSocket.CONNECTING) return 'connecting';\n    if (connectionMeta?.connection?.readyState === ws.WebSocket.OPEN) return 'connected';\n    return 'disconnected';\n  }\n}\n\nexport { WsClient };\n"
  },
  {
    "path": "packages/bruno-requests/src/ws/ws-url.js",
    "content": "/**\n * Get parsed WebSocket URL object\n * @param {string} url - The WebSocket URL\n * @returns {Object} Parsed URL object with protocol, host, path\n */\nexport const getParsedWsUrlObject = (url) => {\n  const addProtocolIfMissing = (str) => {\n    if (str.includes('://')) return str;\n\n    // For localhost, default to insecure (grpc://) for local development\n    if (str.includes('localhost') || str.includes('127.0.0.1')) {\n      return `ws://${str}`;\n    }\n\n    // For other hosts, default to secure\n    return `wss://${str}`;\n  };\n\n  const removeTrailingSlash = (str) => (str.endsWith('/') ? str.slice(0, -1) : str);\n\n  if (!url) return { host: '', path: '' };\n\n  try {\n    const urlObj = new URL(addProtocolIfMissing(url));\n    return {\n      protocol: urlObj.protocol,\n      host: urlObj.host,\n      path: removeTrailingSlash(urlObj.pathname),\n      search: urlObj.search,\n      fullUrl: urlObj.href\n    };\n  } catch (err) {\n    console.error({ err });\n    return {\n      host: '',\n      path: ''\n    };\n  }\n};\n"
  },
  {
    "path": "packages/bruno-requests/src/ws/ws-url.spec.ts",
    "content": "import { getParsedWsUrlObject } from './ws-url';\n\ndescribe('getParsedWsUrlObject', () => {\n  it('returns empty host and path for empty input', () => {\n    expect(getParsedWsUrlObject('')).toEqual({ host: '', path: '' });\n  });\n\n  it('defaults to ws:// for localhost without protocol', () => {\n    const parsed: any = getParsedWsUrlObject('localhost:8080/some/path');\n    expect(parsed.protocol).toBe('ws:');\n    expect(parsed.host).toBe('localhost:8080');\n    expect(parsed.path).toBe('/some/path');\n    expect(parsed.fullUrl.startsWith('ws://')).toBe(true);\n  });\n\n  it('defaults to wss:// for external hosts without protocol', () => {\n    const parsed: any = getParsedWsUrlObject('example.com/s');\n    expect(parsed.protocol).toBe('wss:');\n    expect(parsed.host).toBe('example.com');\n    expect(parsed.path).toBe('/s');\n    expect(parsed.fullUrl.startsWith('wss://')).toBe(true);\n  });\n\n  it('preserves provided protocol and parses query/search', () => {\n    const parsed: any = getParsedWsUrlObject('wss://example.com/path/With/cAses/?a=1&b=2');\n    expect(parsed.protocol).toBe('wss:');\n    expect(parsed.host).toBe('example.com');\n    expect(parsed.path).toBe('/path/With/cAses');\n    expect(parsed.search).toBe('?a=1&b=2');\n  });\n\n  it('removes trailing slash from path', () => {\n    const parsed: any = getParsedWsUrlObject('ws://127.0.0.1:9000/endpoint/');\n    expect(parsed.path).toBe('/endpoint');\n  });\n});\n"
  },
  {
    "path": "packages/bruno-requests/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"module\": \"ESNext\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\",\n    \"resolveJsonModule\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"moduleResolution\": \"node\",\n    \"declaration\": true,\n    \"declarationDir\": \"./dist/types\",\n    \"allowJs\": true,\n    \"checkJs\": false\n  },\n  \"include\": [\"src/**/*.ts\", \"src/**/*.tsx\", \"src/**/*.js\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}"
  },
  {
    "path": "packages/bruno-schema/.gitignore",
    "content": "node_modules\nweb\nout\n\npnpm-lock.yaml\npackage-lock.json\nyarn.lock\n"
  },
  {
    "path": "packages/bruno-schema/license.md",
    "content": "\nMIT License\n\nCopyright (c) 2022 Anoop M D, Anusree P S and Contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "packages/bruno-schema/package.json",
    "content": "{\n  \"name\": \"@usebruno/schema\",\n  \"version\": \"0.7.0\",\n  \"license\": \"MIT\",\n  \"main\": \"src/index.js\",\n  \"files\": [\n    \"src\",\n    \"package.json\"\n  ],\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"dependencies\": {\n    \"nanoid\": \"3.3.8\",\n    \"yup\": \"^0.32.11\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-schema/readme.md",
    "content": "# bruno-schema\n\nThe schema definition for collections\n\n### Publish to Npm Registry\n```bash\nnpm publish --access=public\n```\n\n### Collection schema\n```bash\nid                       Unique id (when persisted to a db)\nuid                      Unique id\nname                     collection name\nitems                    Items (folders and requests)\n  |-uid                  A unique id   \n  |-name                 Item name \n  |-type                 Item type  (folder, http-request, graphql-request)\n  |-request              Request object\n    |-url                Request url\n    |-method             Request method\n    |-headers            Request headers (array of key-val)\n    |-params             Request params (array of key-val)\n    |-body               Request body object  \n      |-mode             Request body mode\n      |-json             Request json body\n      |-text             Request text body\n      |-xml              Request xml body\n      |-multipartForm    Request multipartForm body\n      |-formUrlEncoded   Request formUrlEncoded body\n```\n"
  },
  {
    "path": "packages/bruno-schema/src/collections/index.js",
    "content": "const Yup = require('yup');\nconst { uidSchema } = require('../common');\n\nconst environmentVariablesSchema = Yup.object({\n  uid: uidSchema,\n  name: Yup.string().nullable(),\n  // Allow mixed types (string, number, boolean, object) to support setting non-string values via scripts.\n  value: Yup.mixed().nullable(),\n  type: Yup.string().oneOf(['text']).required('type is required'),\n  enabled: Yup.boolean().defined(),\n  secret: Yup.boolean()\n})\n  .noUnknown(true)\n  .strict();\n\nconst environmentSchema = Yup.object({\n  uid: uidSchema,\n  name: Yup.string().min(1).required('name is required'),\n  variables: Yup.array().of(environmentVariablesSchema).required('variables are required'),\n  color: Yup.string().nullable().optional()\n})\n  .noUnknown(true)\n  .strict();\n\nconst environmentsSchema = Yup.array().of(environmentSchema);\n\nconst keyValueSchema = Yup.object({\n  uid: uidSchema,\n  name: Yup.string().nullable(),\n  value: Yup.string().nullable(),\n  description: Yup.string().nullable(),\n  enabled: Yup.boolean()\n})\n  .noUnknown(true)\n  .strict();\n\nconst assertionOperators = [\n  'eq',\n  'neq',\n  'gt',\n  'gte',\n  'lt',\n  'lte',\n  'in',\n  'notIn',\n  'contains',\n  'notContains',\n  'length',\n  'matches',\n  'notMatches',\n  'startsWith',\n  'endsWith',\n  'between',\n  'isEmpty',\n  'isNotEmpty',\n  'isNull',\n  'isUndefined',\n  'isDefined',\n  'isTruthy',\n  'isFalsy',\n  'isJson',\n  'isNumber',\n  'isString',\n  'isBoolean',\n  'isArray'\n];\n\nconst assertionSchema = keyValueSchema.shape({\n  operator: Yup.string()\n    .oneOf(assertionOperators)\n    .nullable()\n    .optional()\n})\n  .noUnknown(true)\n  .strict();\n\nconst varsSchema = Yup.object({\n  uid: uidSchema,\n  name: Yup.string().nullable(),\n  value: Yup.string().nullable(),\n  description: Yup.string().nullable(),\n  enabled: Yup.boolean(),\n\n  // todo\n  // anoop(4 feb 2023) - nobody uses this, and it needs to be removed\n  local: Yup.boolean()\n})\n  .noUnknown(true)\n  .strict();\n\nconst requestUrlSchema = Yup.string().min(0).defined();\nconst requestMethodSchema = Yup.string()\n  .min(1, 'method is required')\n  .required('method is required');\n\nconst graphqlBodySchema = Yup.object({\n  query: Yup.string().nullable(),\n  variables: Yup.string().nullable()\n})\n  .noUnknown(true)\n  .strict();\n\nconst multipartFormSchema = Yup.object({\n  uid: uidSchema,\n  type: Yup.string().oneOf(['file', 'text']).required('type is required'),\n  name: Yup.string().nullable(),\n  value: Yup.mixed().when('type', {\n    is: 'file',\n    then: Yup.array().of(Yup.string().nullable()).nullable(),\n    otherwise: Yup.string().nullable()\n  }),\n  description: Yup.string().nullable(),\n  contentType: Yup.string().nullable(),\n  enabled: Yup.boolean()\n})\n  .noUnknown(true)\n  .strict();\n\n\nconst fileSchema = Yup.object({ \n  uid: uidSchema,\n  filePath: Yup.string().nullable(),\n  contentType: Yup.string().nullable(),\n  selected: Yup.boolean()\n})\n  .noUnknown(true)\n  .strict();\n\nconst requestBodySchema = Yup.object({\n  mode: Yup.string()\n    .oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql', 'sparql', 'file'])\n    .required('mode is required'),\n  json: Yup.string().nullable(),\n  text: Yup.string().nullable(),\n  xml: Yup.string().nullable(),\n  sparql: Yup.string().nullable(),\n  formUrlEncoded: Yup.array().of(keyValueSchema).nullable(),\n  multipartForm: Yup.array().of(multipartFormSchema).nullable(),\n  graphql: graphqlBodySchema.nullable(),\n  file: Yup.array().of(fileSchema).nullable()\n})\n  .noUnknown(true)\n  .strict();\n\nconst authAwsV4Schema = Yup.object({\n  accessKeyId: Yup.string().nullable(),\n  secretAccessKey: Yup.string().nullable(),\n  sessionToken: Yup.string().nullable(),\n  service: Yup.string().nullable(),\n  region: Yup.string().nullable(),\n  profileName: Yup.string().nullable()\n})\n  .noUnknown(true)\n  .strict();\n\nconst authBasicSchema = Yup.object({\n  username: Yup.string().nullable(),\n  password: Yup.string().nullable()\n})\n  .noUnknown(true)\n  .strict();\n\nconst authWsseSchema = Yup.object({\n  username: Yup.string().nullable(),\n  password: Yup.string().nullable()\n})\n  .noUnknown(true)\n  .strict();\n\nconst authBearerSchema = Yup.object({\n  token: Yup.string().nullable()\n})\n  .noUnknown(true)\n  .strict();\n\nconst authDigestSchema = Yup.object({\n  username: Yup.string().nullable(),\n  password: Yup.string().nullable()\n})\n  .noUnknown(true)\n  .strict();\n\n\n\n  const authNTLMSchema = Yup.object({\n    username: Yup.string().nullable(),\n    password: Yup.string().nullable(),\n    domain: Yup.string().nullable()\n\n  })\n    .noUnknown(true)\n    .strict();  \n\nconst authApiKeySchema = Yup.object({\n  key: Yup.string().nullable(),\n  value: Yup.string().nullable(),\n  placement: Yup.string().oneOf(['header', 'queryparams']).nullable()\n})\n  .noUnknown(true)\n  .strict();\n\nconst oauth2AuthorizationAdditionalParametersSchema = Yup.object({\n  name: Yup.string().nullable(),\n  value: Yup.string().nullable(),\n  sendIn: Yup.string()\n    .oneOf(['headers', 'queryparams'])\n    .required('send in property is required'),\n  enabled: Yup.boolean()\n})\n  .noUnknown(true)\n  .strict();\n\nconst oauth2AdditionalParametersSchema = Yup.object({\n    name: Yup.string().nullable(),\n    value: Yup.string().nullable(),\n    sendIn: Yup.string()\n      .oneOf(['headers', 'queryparams', 'body'])\n      .required('send in property is required'),\n    enabled: Yup.boolean()\n  })\n    .noUnknown(true)\n    .strict();\n\nconst oauth2Schema = Yup.object({\n  grantType: Yup.string()\n    .oneOf(['client_credentials', 'password', 'authorization_code', 'implicit'])\n    .required('grantType is required'),\n  username: Yup.string().when('grantType', {\n    is: (val) => ['client_credentials', 'password'].includes(val),\n    then: Yup.string().nullable(),\n    otherwise: Yup.string().nullable().strip()\n  }),\n  password: Yup.string().when('grantType', {\n    is: (val) => ['client_credentials', 'password'].includes(val),\n    then: Yup.string().nullable(),\n    otherwise: Yup.string().nullable().strip()\n  }),\n  callbackUrl: Yup.string().when('grantType', {\n    is: (val) => ['authorization_code', 'implicit'].includes(val),\n    then: Yup.string().nullable(),\n    otherwise: Yup.string().nullable().strip()\n  }),\n  authorizationUrl: Yup.string().when('grantType', {\n    is: (val) => ['authorization_code', 'implicit'].includes(val),\n    then: Yup.string().nullable(),\n    otherwise: Yup.string().nullable().strip()\n  }),\n  accessTokenUrl: Yup.string().when('grantType', {\n    is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val),\n    then: Yup.string().nullable(),\n    otherwise: Yup.string().nullable().strip()\n  }),\n  clientId: Yup.string().when('grantType', {\n    is: (val) => ['client_credentials', 'password', 'authorization_code', 'implicit'].includes(val),\n    then: Yup.string().nullable(),\n    otherwise: Yup.string().nullable().strip()\n  }),\n  clientSecret: Yup.string().when('grantType', {\n    is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val),\n    then: Yup.string().nullable(),\n    otherwise: Yup.string().nullable().strip()\n  }),\n  scope: Yup.string().when('grantType', {\n    is: (val) => ['client_credentials', 'password', 'authorization_code', 'implicit'].includes(val),\n    then: Yup.string().nullable(),\n    otherwise: Yup.string().nullable().strip()\n  }),\n  state: Yup.string().when('grantType', {\n    is: (val) => ['authorization_code', 'implicit'].includes(val),\n    then: Yup.string().nullable(),\n    otherwise: Yup.string().nullable().strip()\n  }),\n  pkce: Yup.boolean().when('grantType', {\n    is: (val) => ['authorization_code'].includes(val),\n    then: Yup.boolean().default(false),\n    otherwise: Yup.boolean()\n  }),\n  credentialsPlacement: Yup.string().when('grantType', {\n    is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val),\n    then: Yup.string().nullable(),\n    otherwise: Yup.string().nullable().strip()\n  }),\n  credentialsId: Yup.string().when('grantType', {\n    is: (val) => ['client_credentials', 'password', 'authorization_code', 'implicit'].includes(val),\n    then: Yup.string().nullable(),\n    otherwise: Yup.string().nullable().strip()\n  }),\n  tokenSource: Yup.string().when('grantType', {\n    is: (val) => ['client_credentials', 'password', 'authorization_code', 'implicit'].includes(val),\n    then: Yup.string().oneOf(['access_token', 'id_token']).optional(),\n    otherwise: Yup.string().optional().strip()\n  }),\n  tokenPlacement: Yup.string().when('grantType', {\n    is: (val) => ['client_credentials', 'password', 'authorization_code', 'implicit'].includes(val),\n    then: Yup.string().nullable(),\n    otherwise: Yup.string().nullable().strip()\n  }),\n  tokenHeaderPrefix: Yup.string().when(['grantType', 'tokenPlacement'], {\n    is: (grantType, tokenPlacement) => \n      ['client_credentials', 'password', 'authorization_code', 'implicit'].includes(grantType) && tokenPlacement === 'header',\n    then: Yup.string().nullable(),\n    otherwise: Yup.string().nullable().strip()\n  }),\n  tokenQueryKey: Yup.string().when(['grantType', 'tokenPlacement'], {\n    is: (grantType, tokenPlacement) => \n      ['client_credentials', 'password', 'authorization_code', 'implicit'].includes(grantType) && tokenPlacement === 'url',\n    then: Yup.string().nullable(),\n    otherwise: Yup.string().nullable().strip()\n  }),\n  refreshTokenUrl: Yup.string().when('grantType', {\n    is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val),\n    then: Yup.string().nullable(),\n    otherwise: Yup.string().nullable().strip()\n  }),\n  autoRefreshToken: Yup.boolean().when('grantType', {\n    is: (val) => ['client_credentials', 'password', 'authorization_code'].includes(val),\n    then: Yup.boolean().default(false),\n    otherwise: Yup.boolean()\n  }),\n  autoFetchToken: Yup.boolean().when('grantType', {\n    is: (val) => ['authorization_code', 'implicit'].includes(val),\n    then: Yup.boolean().default(true),\n    otherwise: Yup.boolean()\n  }),\n  additionalParameters: Yup.object({\n    authorization: Yup.mixed().when('grantType', {\n      is: 'authorization_code',\n      then: Yup.array().of(oauth2AuthorizationAdditionalParametersSchema).required(),\n      otherwise: Yup.mixed().nullable().optional()\n    }),\n    token: Yup.array().of(oauth2AdditionalParametersSchema).optional(),\n    refresh: Yup.array().of(oauth2AdditionalParametersSchema).optional()\n  })\n})\n  .noUnknown(true)\n  .strict();\n\nconst authSchema = Yup.object({\n  mode: Yup.string()\n    .oneOf(['inherit', 'none', 'awsv4', 'basic', 'bearer', 'digest', 'ntlm', 'oauth2', 'wsse', 'apikey'])\n    .required('mode is required'),\n  awsv4: authAwsV4Schema.nullable(),\n  basic: authBasicSchema.nullable(),\n  bearer: authBearerSchema.nullable(),\n  ntlm: authNTLMSchema.nullable(),\n  digest: authDigestSchema.nullable(),\n  oauth2: oauth2Schema.nullable(),\n  wsse: authWsseSchema.nullable(),\n  apikey: authApiKeySchema.nullable()\n})\n  .noUnknown(true)\n  .strict()\n  .nullable();\n\nconst requestParamsSchema = Yup.object({\n  uid: uidSchema,\n  name: Yup.string().nullable(),\n  value: Yup.string().nullable(),\n  description: Yup.string().nullable(),\n  type: Yup.string().oneOf(['query', 'path']).required('type is required'),\n  enabled: Yup.boolean()\n})\n  .noUnknown(true)\n  .strict();\n\nconst exampleSchema = Yup.object({\n  uid: uidSchema,\n  itemUid: uidSchema,\n  name: Yup.string().min(1, 'name must be at least 1 character').required('name is required'),\n  description: Yup.string().nullable(),\n  type: Yup.string().oneOf(['http-request', 'graphql-request', 'grpc-request',]).required('type is required'),\n  request: Yup.object({\n    url: requestUrlSchema,\n    method: requestMethodSchema,\n    headers: Yup.array().of(keyValueSchema).required('headers are required'),\n    params: Yup.array().of(requestParamsSchema).required('params are required'),\n    body: requestBodySchema\n  })\n    .noUnknown(true)\n    .strict()\n    .nullable(),\n  response: Yup.object({\n    status: Yup.number().nullable(),\n    statusText: Yup.string().nullable(),\n    headers: Yup.array().of(keyValueSchema).nullable(),\n    body: Yup.object({\n      type: Yup.string().oneOf(['json', 'text', 'xml', 'html', 'binary']).nullable(),\n      content: Yup.mixed().nullable()\n    }).nullable()\n  })\n    .noUnknown(true)\n    .strict()\n    .nullable()\n})\n  .noUnknown(true)\n  .strict();\n\n// Right now, the request schema is very tightly coupled with http request\n// As we introduce more request types in the future, we will improve the definition to support\n// schema structure based on other request type\nconst requestSchema = Yup.object({\n  url: requestUrlSchema,\n  method: requestMethodSchema,\n  headers: Yup.array().of(keyValueSchema).required('headers are required'),\n  params: Yup.array().of(requestParamsSchema).required('params are required'),\n  auth: authSchema,\n  body: requestBodySchema,\n  script: Yup.object({\n    req: Yup.string().nullable(),\n    res: Yup.string().nullable()\n  })\n    .noUnknown(true)\n    .strict(),\n  vars: Yup.object({\n    req: Yup.array().of(varsSchema).nullable(),\n    res: Yup.array().of(varsSchema).nullable()\n  })\n    .noUnknown(true)\n    .strict()\n    .nullable(),\n  assertions: Yup.array().of(assertionSchema).nullable(),\n  tests: Yup.string().nullable(),\n  docs: Yup.string().nullable()\n})\n  .noUnknown(true)\n  .strict();\n\nconst grpcRequestSchema = Yup.object({\n  url: requestUrlSchema,\n  method: Yup.string().optional(),\n  methodType: Yup.string().oneOf(['unary', 'client-streaming', 'server-streaming', 'bidi-streaming', '']).nullable(),\n  protoPath: Yup.string().nullable(),\n  headers: Yup.array().of(keyValueSchema).required('headers are required'),\n  auth: authSchema,\n  body: Yup.object({\n    mode: Yup.string().oneOf(['grpc']).required('mode is required'),\n    grpc: Yup.array().of(Yup.object({\n      name: Yup.string().nullable(),\n      content: Yup.string().nullable()\n    })).nullable()\n  })\n    .strict()\n    .required('body is required'),\n  script: Yup.object({\n    req: Yup.string().nullable(),\n    res: Yup.string().nullable()\n  })\n    .noUnknown(true)\n    .strict(),\n  vars: Yup.object({\n    req: Yup.array().of(varsSchema).nullable(),\n    res: Yup.array().of(varsSchema).nullable()\n  })\n    .noUnknown(true)\n    .strict()\n    .nullable(),\n  assertions: Yup.array().of(assertionSchema).nullable(),\n  tests: Yup.string().nullable(),\n  docs: Yup.string().nullable(),\n})\n  .noUnknown(true)\n  .strict();\n\nconst wsRequestSchema = Yup.object({\n  url: requestUrlSchema,\n  headers: Yup.array().of(keyValueSchema).required('headers are required'),\n  auth: authSchema,\n  body: Yup.object({\n    mode: Yup.string().oneOf(['ws']).required('mode is required'),\n    ws: Yup.array()\n      .of(\n        Yup.object({\n          name: Yup.string().nullable(),\n          type: Yup.string().nullable(),\n          content: Yup.string().nullable()\n        })\n      )\n      .nullable()\n  })\n    .strict()\n    .required('body is required'),\n  script: Yup.object({\n    req: Yup.string().nullable(),\n    res: Yup.string().nullable()\n  })\n    .noUnknown(true)\n    .strict(),\n  vars: Yup.object({\n    req: Yup.array().of(varsSchema).nullable(),\n    res: Yup.array().of(varsSchema).nullable()\n  })\n    .noUnknown(true)\n    .strict()\n    .nullable(),\n  assertions: Yup.array().of(assertionSchema).nullable(),\n  tests: Yup.string().nullable(),\n  docs: Yup.string().nullable()\n})\n  .noUnknown(true)\n  .strict();\n\nconst wsSettingsSchema = Yup.object({\n  settings: Yup.object({\n    timeout: Yup.number()\n      .default(500),\n    keepAliveInterval: Yup.number()\n      .default(0)\n  }).noUnknown(true)\n    .strict()\n    .nullable()\n});\n\nconst folderRootSchema = Yup.object({\n  request: Yup.object({\n    headers: Yup.array().of(keyValueSchema).nullable(),\n    auth: authSchema,\n    script: Yup.object({\n      req: Yup.string().nullable(),\n      res: Yup.string().nullable()\n    })\n      .noUnknown(true)\n      .strict()\n      .nullable(),\n    vars: Yup.object({\n      req: Yup.array().of(varsSchema).nullable(),\n      res: Yup.array().of(varsSchema).nullable()\n    })\n      .noUnknown(true)\n      .strict()\n      .nullable(),\n    tests: Yup.string().nullable()\n  })\n    .noUnknown(true)\n    .strict()\n    .nullable(),\n  docs: Yup.string().nullable(),\n  meta: Yup.object({\n    name: Yup.string().nullable(),\n    seq: Yup.number().min(1).nullable()\n  })\n    .noUnknown(true)\n    .strict()\n    .nullable()\n})\n  .noUnknown(true)\n  .nullable();\n\nconst itemSchema = Yup.object({\n  uid: uidSchema,\n  type: Yup.string().oneOf(['http-request', 'graphql-request', 'folder', 'js', 'grpc-request', 'ws-request']).required('type is required'),\n  seq: Yup.number().min(1),\n  name: Yup.string().min(1, 'name must be at least 1 character').required('name is required'),\n  tags: Yup.array().of(Yup.string().matches(/^[\\p{L}\\p{N}_-](?:[\\p{L}\\p{N}_\\s-]*[\\p{L}\\p{N}_-])?$/u, 'tag must contain only letters, numbers, spaces, hyphens, or underscores')),\n  request: Yup.mixed().when('type', {\n    is: (type) => type === 'grpc-request',\n    then: grpcRequestSchema.required('request is required when item-type is grpc-request'),\n    otherwise: Yup.mixed().when('type', {\n      is: (type) => type === 'ws-request',\n      then: wsRequestSchema.required('request is required when item-type is ws-request'),\n      otherwise: requestSchema.when('type', {\n        is: (type) => ['http-request', 'graphql-request'].includes(type),\n        then: (schema) => schema.required('request is required when item-type is request')\n      })\n    })\n  }),\n    settings: Yup.mixed()\n    .when('type', {\n      is: (type) => type === 'ws-request',\n      then: wsSettingsSchema,\n      otherwise: Yup.object({\n        encodeUrl: Yup.boolean().nullable(),\n        followRedirects: Yup.boolean().nullable(),\n        maxRedirects: Yup.number().min(0).max(50).nullable(),\n        timeout: Yup.mixed().nullable(),\n      }).noUnknown(true)\n    .strict()\n    .nullable()\n    }),\n  fileContent: Yup.string().when('type', {\n    // If the type is 'js', the fileContent field is expected to be a string.\n    // This can include an empty string, indicating that the JS file may not have any content.\n    is: 'js',\n    then: Yup.string(),\n    // For all other types, the fileContent field is not required and can be null.\n    otherwise: Yup.string().nullable()\n  }),\n  root: Yup.mixed().when('type', {\n    is: 'folder',\n    then: folderRootSchema,\n    otherwise: Yup.mixed().nullable().notRequired()\n  }),\n  items: Yup.lazy(() => Yup.array().of(itemSchema)),\n  examples: Yup.array().of(exampleSchema).when('type', {\n    is: (type) => ['http-request', 'graphql-request', 'grpc-request'].includes(type),\n    then: (schema) => schema.nullable(),\n    otherwise: Yup.array().strip()\n  }),\n  filename: Yup.string().nullable(),\n  pathname: Yup.string().nullable()\n})\n  .noUnknown(true)\n  .strict();\n\nconst collectionSchema = Yup.object({\n  version: Yup.string().oneOf(['1']).required('version is required'),\n  uid: uidSchema,\n  name: Yup.string().min(1, 'name must be at least 1 character').required('name is required'),\n  items: Yup.array().of(itemSchema),\n  activeEnvironmentUid: Yup.string()\n    .length(21, 'activeEnvironmentUid must be 21 characters in length')\n    .matches(/^[a-zA-Z0-9]*$/, 'uid must be alphanumeric')\n    .nullable(),\n  environments: environmentsSchema,\n  pathname: Yup.string().nullable(),\n  runnerResult: Yup.object({\n    items: Yup.array()\n  }),\n  runtimeVariables: Yup.object(),\n  workspaceProcessEnvVariables: Yup.object().default({}),\n  brunoConfig: Yup.object(),\n  root: folderRootSchema\n})\n  .noUnknown(true)\n  .strict();\n\nmodule.exports = {\n  requestSchema,\n  itemSchema,\n  environmentSchema,\n  environmentsSchema,\n  collectionSchema\n};\n"
  },
  {
    "path": "packages/bruno-schema/src/collections/index.spec.js",
    "content": "const { expect } = require('@jest/globals');\nconst { uuid } = require('../utils/testUtils');\nconst { collectionSchema } = require('./index');\n\ndescribe('Collection Schema Validation', () => {\n  it('collection schema must validate successfully - simple collection, no items', async () => {\n    const collection = {\n      version: '1',\n      uid: uuid(),\n      name: 'My Collection'\n    };\n\n    const isValid = await collectionSchema.validate(collection);\n    expect(isValid).toBeTruthy();\n  });\n\n  it('collection schema must validate successfully - simple collection, empty items', async () => {\n    const collection = {\n      version: '1',\n      uid: uuid(),\n      name: 'My Collection',\n      items: []\n    };\n\n    const isValid = await collectionSchema.validate(collection);\n    expect(isValid).toBeTruthy();\n  });\n\n  it('collection schema must validate successfully - simple collection, just a folder item', async () => {\n    const collection = {\n      version: '1',\n      uid: uuid(),\n      name: 'My Collection',\n      items: [\n        {\n          uid: uuid(),\n          name: 'A Folder',\n          type: 'folder'\n        }\n      ]\n    };\n\n    const isValid = await collectionSchema.validate(collection);\n    expect(isValid).toBeTruthy();\n  });\n\n  it('collection schema must validate successfully - simple collection, just a request item', async () => {\n    const collection = {\n      version: '1',\n      uid: uuid(),\n      name: 'My Collection',\n      items: [\n        {\n          uid: uuid(),\n          name: 'Get Countries',\n          type: 'http-request',\n          request: {\n            url: 'https://restcountries.com/v2/alpha/in',\n            method: 'GET',\n            headers: [],\n            params: [],\n            body: {\n              mode: 'none'\n            }\n          }\n        }\n      ]\n    };\n\n    const isValid = await collectionSchema.validate(collection);\n    expect(isValid).toBeTruthy();\n  });\n\n  it('collection schema must validate successfully - simple collection, just a gRPC request item', async () => {\n    const collection = {\n      version: '1',\n      uid: uuid(),\n      name: 'My Collection',\n      items: [\n        {\n          uid: uuid(),\n          name: 'Get User',\n          type: 'grpc-request',\n          request: {\n            url: 'grpc://localhost:50051',\n            method: 'GetUser',\n            methodType: 'unary',\n            protoPath: '/path/to/proto/file.proto',\n            headers: [],\n            body: {\n              mode: 'grpc',\n              grpc: [\n                {\n                  name: 'message 1',\n                  content: '{}'\n                }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    const isValid = await collectionSchema.validate(collection);\n    expect(isValid).toBeTruthy();\n  });\n\n  it('collection schema validation must fail - invalid grpc collection item with params', async () => {\n    const collection = {\n      version: '1',\n      uid: uuid(),\n      name: 'My Collection',\n      items: [\n        {\n          uid: uuid(),\n          name: 'Get User',\n          type: 'grpc-request',\n          request: {\n            url: 'grpc://localhost:50051',\n            method: 'GetUser',\n            methodType: 'unary',\n            protoPath: '/path/to/proto/file.proto',\n            headers: [],\n            params: [],\n            body: {\n              mode: 'grpc',\n              grpc: [\n                {\n                  name: 'message 1',\n                  content: '{}'\n                }\n              ]\n            }\n          }\n        }\n      ]\n    };\n\n    expect(collectionSchema.validate(collection)).rejects.toThrow('items[0].request field has unspecified keys: params');\n  });\n\n\n  it('collection schema must validate successfully - simple collection, folder inside folder', async () => {\n    const collection = {\n      version: '1',\n      uid: uuid(),\n      name: 'My Collection',\n      items: [\n        {\n          uid: uuid(),\n          name: 'First Level Folder',\n          type: 'folder',\n          items: [\n            {\n              uid: uuid(),\n              name: 'Second Level Folder',\n              type: 'folder'\n            }\n          ]\n        }\n      ]\n    };\n\n    const isValid = await collectionSchema.validate(collection);\n    expect(isValid).toBeTruthy();\n  });\n\n  it('collection schema must validate successfully - simple collection, [folder] [request + folder]', async () => {\n    const collection = {\n      version: '1',\n      uid: uuid(),\n      name: 'My Collection',\n      items: [\n        {\n          uid: uuid(),\n          name: 'First Level Folder',\n          type: 'folder',\n          items: [\n            {\n              uid: uuid(),\n              name: 'Get Countries',\n              type: 'http-request',\n              request: {\n                url: 'https://restcountries.com/v2/alpha/in',\n                method: 'GET',\n                headers: [],\n                params: [],\n                body: {\n                  mode: 'none'\n                }\n              }\n            },\n            {\n              uid: uuid(),\n              name: 'Second Level Folder',\n              type: 'folder'\n            }\n          ]\n        }\n      ]\n    };\n\n    const isValid = await collectionSchema.validate(collection);\n    expect(isValid).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "packages/bruno-schema/src/collections/itemSchema.spec.js",
    "content": "const { expect } = require('@jest/globals');\nconst { uuid, validationErrorWithMessages } = require('../utils/testUtils');\nconst { itemSchema } = require('./index');\n\ndescribe('Item Schema Validation', () => {\n  it('item schema must validate successfully - simple items', async () => {\n    const item = {\n      uid: uuid(),\n      name: 'A Folder',\n      type: 'folder',\n      tags: ['smoke-test']\n    };\n\n    const isValid = await itemSchema.validate(item);\n    expect(isValid).toBeTruthy();\n  });\n\n  it('item schema must validate tag regex rules', async () => {\n    const validItem = {\n      uid: uuid(),\n      name: 'A Folder',\n      type: 'folder',\n      tags: ['tag_1', 'Äiti-123 test']\n    };\n\n    const isValid = await itemSchema.validate(validItem);\n    expect(isValid).toBeTruthy();\n\n    let invalidItem = {\n      uid: uuid(),\n      name: 'A Folder',\n      type: 'folder',\n      tags: [' invalid-tag']\n    };\n\n    await expect(itemSchema.validate(invalidItem)).rejects.toThrow(\n      'tag must contain only letters, numbers, spaces, hyphens, or underscores'\n    );\n\n    invalidItem = {\n      uid: uuid(),\n      name: 'A Folder',\n      type: 'folder',\n      tags: ['tag🔥name']\n    };\n\n    await expect(itemSchema.validate(invalidItem)).rejects.toThrow(\n      'tag must contain only letters, numbers, spaces, hyphens, or underscores'\n    );\n  });\n\n  it('item schema must throw an error if name is missing', async () => {\n    const item = {\n      uid: uuid(),\n      type: 'folder'\n    };\n\n    return Promise.all([\n      expect(itemSchema.validate(item)).rejects.toEqual(validationErrorWithMessages('name is required'))\n    ]);\n  });\n\n  it('item schema must throw an error if name is empty', async () => {\n    const item = {\n      uid: uuid(),\n      name: '',\n      type: 'folder'\n    };\n\n    return Promise.all([\n      expect(itemSchema.validate(item)).rejects.toEqual(\n        validationErrorWithMessages('name must be at least 1 character')\n      )\n    ]);\n  });\n\n  it('item schema must throw an error if request is not present when item-type is http-request', async () => {\n    const item = {\n      uid: uuid(),\n      name: 'Get Users',\n      type: 'http-request'\n    };\n\n    return Promise.all([\n      expect(itemSchema.validate(item)).rejects.toEqual(\n        validationErrorWithMessages('request is required when item-type is request')\n      )\n    ]);\n  });\n\n  it('item schema must throw an error if request is not present when item-type is graphql-request', async () => {\n    const item = {\n      uid: uuid(),\n      name: 'Get Users',\n      type: 'graphql-request'\n    };\n\n    return Promise.all([\n      expect(itemSchema.validate(item)).rejects.toEqual(\n        validationErrorWithMessages('request is required when item-type is request')\n      )\n    ]);\n  });\n});\n"
  },
  {
    "path": "packages/bruno-schema/src/collections/requestSchema.spec.js",
    "content": "const { expect } = require('@jest/globals');\nconst { uuid, validationErrorWithMessages } = require('../utils/testUtils');\nconst { requestSchema } = require('./index');\n\ndescribe('Request Schema Validation', () => {\n  it('request schema must validate successfully - simple request', async () => {\n    const request = {\n      url: 'https://restcountries.com/v2/alpha/in',\n      method: 'GET',\n      headers: [],\n      params: [],\n      body: {\n        mode: 'none'\n      }\n    };\n\n    const isValid = await requestSchema.validate(request);\n    expect(isValid).toBeTruthy();\n  });\n\n  it('request schema must validate successfully - custom method', async () => {\n    const request = {\n      url: 'https://restcountries.com/v2/alpha/in',\n      method: 'FOO',\n      headers: [],\n      params: [],\n      body: {\n        mode: 'none'\n      }\n    };\n\n    const isValid = await requestSchema.validate(request);\n    expect(isValid).toBeTruthy();\n  });\n\n  it('request schema must validate successfully - custom method with dash', async () => {\n    const request = {\n      url: 'https://restcountries.com/v2/alpha/in',\n      method: 'X-CUSTOM',\n      headers: [],\n      params: [],\n      body: {\n        mode: 'none'\n      }\n    };\n\n    const isValid = await requestSchema.validate(request);\n    expect(isValid).toBeTruthy();\n  });\n\n  it('request schema must throw an error if method is empty', async () => {\n    const request = {\n      url: 'https://restcountries.com/v2/alpha/in',\n      method: '',\n      headers: [],\n      params: [],\n      body: {\n        mode: 'none'\n      }\n    };\n\n    await expect(requestSchema.validate(request)).rejects.toThrow();\n  });\n\n  it('request schema must validate successfully - method with space is allowed now', async () => {\n    const request = {\n      url: 'https://restcountries.com/v2/alpha/in',\n      method: 'GET JUNK',\n      headers: [],\n      params: [],\n      body: {\n        mode: 'none'\n      }\n    };\n\n    const isValid = await requestSchema.validate(request);\n    expect(isValid).toBeTruthy();\n  });\n});\n"
  },
  {
    "path": "packages/bruno-schema/src/common/index.js",
    "content": "const Yup = require('yup');\n\nconst uidSchema = Yup.string()\n  .length(21, 'uid must be 21 characters in length')\n  .matches(/^[a-zA-Z0-9]*$/, 'uid must be alphanumeric')\n  .required('uid is required')\n  .strict();\n\nmodule.exports = {\n  uidSchema\n};\n"
  },
  {
    "path": "packages/bruno-schema/src/index.js",
    "content": "const { collectionSchema, itemSchema, environmentSchema, environmentsSchema } = require('./collections');\n\nmodule.exports = {\n  itemSchema,\n  environmentSchema,\n  environmentsSchema,\n  collectionSchema\n};\n"
  },
  {
    "path": "packages/bruno-schema/src/utils/testUtils.js",
    "content": "const { customAlphabet } = require('nanoid');\nconst { expect } = require('@jest/globals');\n\n// a customized version of nanoid without using _ and -\nconst uuid = () => {\n  // https://github.com/ai/nanoid/blob/main/url-alphabet/index.js\n  const urlAlphabet = 'useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict';\n  const customNanoId = customAlphabet(urlAlphabet, 21);\n\n  return customNanoId();\n};\n\nconst validationErrorWithMessages = (...errors) => {\n  return expect.objectContaining({\n    errors\n  });\n};\n\nmodule.exports = {\n  uuid,\n  validationErrorWithMessages\n};\n"
  },
  {
    "path": "packages/bruno-schema-types/.gitignore",
    "content": "# Dependencies\nnode_modules/\n\n# Build outputs\ndist/\n*.tsbuildinfo\n\n# IDE\n.vscode/\n.idea/\n*.swp\n*.swo\n*~\n\n# OS\n.DS_Store\nThumbs.db\n\n"
  },
  {
    "path": "packages/bruno-schema-types/package.json",
    "content": "{\n  \"name\": \"@usebruno/schema-types\",\n  \"version\": \"0.0.1\",\n  \"description\": \"TypeScript types for Bruno schema\",\n  \"author\": \"Bruno Software Inc.\",\n  \"main\": \"dist/schema-types.js\",\n  \"types\": \"dist/schema-types.d.ts\",\n  \"files\": [\n    \"dist\",\n    \"src\",\n    \"package.json\"\n  ],\n  \"scripts\": {\n    \"build\": \"tsc -p tsconfig.json\",\n    \"clean\": \"rm -rf dist\",\n    \"prepublishOnly\": \"npm run build\"\n  },\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.js\"\n    },\n    \"./common/*\": {\n      \"types\": \"./dist/common/*.d.ts\",\n      \"default\": \"./dist/common/*.js\"\n    },\n    \"./config/*\": {\n      \"types\": \"./dist/config/*.d.ts\",\n      \"default\": \"./dist/config/*.js\"\n    },\n    \"./collection/*\": {\n      \"types\": \"./dist/collection/*.d.ts\",\n      \"default\": \"./dist/collection/*.js\"\n    },\n    \"./requests/*\": {\n      \"types\": \"./dist/requests/*.d.ts\",\n      \"default\": \"./dist/requests/*.js\"\n    }\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.0.0\"\n  },\n  \"license\": \"MIT\",\n  \"keywords\": [\n    \"bruno\",\n    \"types\",\n    \"typescript\",\n    \"api\",\n    \"http\"\n  ]\n}\n\n"
  },
  {
    "path": "packages/bruno-schema-types/src/collection/collection.ts",
    "content": "import type { UID } from '../common';\nimport type { Item } from './item';\nimport type { Environments } from './environment';\nimport type { FolderRoot } from './folder';\n\nexport interface RunnerResult {\n  items?: unknown[] | null;\n}\n\nexport interface Collection {\n  version: '1';\n  uid: UID;\n  name: string;\n  items: Item[];\n  activeEnvironmentUid?: string | null;\n  environments?: Environments | null;\n  pathname?: string | null;\n  runnerResult?: RunnerResult | null;\n  runtimeVariables?: Record<string, unknown> | null;\n  brunoConfig?: Record<string, unknown> | null;\n  root?: FolderRoot | null;\n}\n"
  },
  {
    "path": "packages/bruno-schema-types/src/collection/environment.ts",
    "content": "import type { UID } from '../common';\n\nexport interface EnvironmentVariable {\n  uid: UID;\n  name?: string | null;\n  value?: string | number | boolean | Record<string, unknown> | null;\n  type: 'text';\n  enabled?: boolean;\n  secret?: boolean;\n}\n\nexport interface Environment {\n  uid: UID;\n  name: string;\n  variables: EnvironmentVariable[];\n  color?: string | null;\n}\n\nexport type Environments = Environment[];\n"
  },
  {
    "path": "packages/bruno-schema-types/src/collection/examples.ts",
    "content": "import type { UID, KeyValue } from '../common';\nimport type { HttpRequestBody, HttpRequestParam } from '../requests';\n\nexport type ExampleType = 'http-request' | 'graphql-request' | 'grpc-request';\n\nexport interface ExampleRequest {\n  url: string;\n  method: string;\n  headers: KeyValue[];\n  params: HttpRequestParam[];\n  body: HttpRequestBody;\n}\n\nexport interface ExampleResponseBody {\n  type?: 'json' | 'text' | 'xml' | 'html' | 'binary' | null;\n  content?: unknown;\n}\n\nexport interface ExampleResponse {\n  status?: number | null;\n  statusText?: string | null;\n  headers?: KeyValue[] | null;\n  body?: ExampleResponseBody | null;\n}\n\nexport interface Example {\n  uid: UID;\n  itemUid: UID;\n  name: string;\n  description?: string | null;\n  type: ExampleType;\n  request?: ExampleRequest | null;\n  response?: ExampleResponse | null;\n}\n"
  },
  {
    "path": "packages/bruno-schema-types/src/collection/folder.ts",
    "content": "import type { KeyValue, Auth, Script, Variables } from '../common';\n\nexport interface FolderRequest {\n  headers?: KeyValue[] | null;\n  auth?: Auth | null;\n  script?: Script | null;\n  vars?: {\n    req?: Variables | null;\n    res?: Variables | null;\n  } | null;\n  tests?: string | null;\n}\n\nexport interface FolderMeta {\n  name?: string | null;\n  seq?: number | null;\n}\n\nexport interface FolderRoot {\n  request?: FolderRequest | null;\n  docs?: string | null;\n  meta?: FolderMeta | null;\n}\n"
  },
  {
    "path": "packages/bruno-schema-types/src/collection/index.ts",
    "content": "export type {\n  EnvironmentVariable,\n  Environment,\n  Environments\n} from './environment';\nexport type { FolderRequest, FolderMeta, FolderRoot } from './folder';\nexport type {\n  Example,\n  ExampleType,\n  ExampleRequest,\n  ExampleResponse,\n  ExampleResponseBody\n} from './examples';\nexport type {\n  Item,\n  ItemType,\n  ItemSettings,\n  HttpItemSettings,\n  WebSocketItemSettings\n} from './item';\nexport type { Collection, RunnerResult } from './collection';\n"
  },
  {
    "path": "packages/bruno-schema-types/src/collection/item.ts",
    "content": "import type { UID } from '../common';\nimport type { Request } from '../requests';\nimport type { Example } from './examples';\nimport type { FolderRoot } from './folder';\n\nexport type ItemType\n  = | 'http-request'\n    | 'graphql-request'\n    | 'folder'\n    | 'js'\n    | 'grpc-request'\n    | 'ws-request';\n\nexport interface HttpItemSettings {\n  encodeUrl?: boolean | null;\n  followRedirects?: boolean | null;\n  maxRedirects?: number | null;\n  timeout?: number | 'inherit' | null;\n}\n\nexport interface WebSocketItemSettings {\n  settings?: {\n    timeout?: number | null;\n    keepAliveInterval?: number | null;\n  } | null;\n}\n\nexport type ItemSettings = HttpItemSettings | WebSocketItemSettings | null;\n\nexport interface Item {\n  uid: UID;\n  type: ItemType;\n  seq?: number | null;\n  name: string;\n  tags?: string[] | null;\n  request?: Request | null;\n  settings?: ItemSettings;\n  fileContent?: string | null;\n  root?: FolderRoot | null;\n  items?: Item[] | null;\n  examples?: Example[] | null;\n  filename?: string | null;\n  pathname?: string | null;\n}\n"
  },
  {
    "path": "packages/bruno-schema-types/src/common/auth.ts",
    "content": "export interface AuthAwsV4 {\n  accessKeyId?: string | null;\n  secretAccessKey?: string | null;\n  sessionToken?: string | null;\n  service?: string | null;\n  region?: string | null;\n  profileName?: string | null;\n}\n\nexport interface AuthBasic {\n  username?: string | null;\n  password?: string | null;\n}\n\nexport interface AuthWsse {\n  username?: string | null;\n  password?: string | null;\n}\n\nexport interface AuthBearer {\n  token?: string | null;\n}\n\nexport interface AuthDigest {\n  username?: string | null;\n  password?: string | null;\n}\n\nexport interface AuthNTLM {\n  username?: string | null;\n  password?: string | null;\n  domain?: string | null;\n}\n\nexport interface AuthApiKey {\n  key?: string | null;\n  value?: string | null;\n  placement?: 'header' | 'queryparams' | null;\n}\n\nexport type OAuthGrantType\n  = | 'client_credentials'\n    | 'password'\n    | 'authorization_code'\n    | 'implicit';\n\nexport interface OAuthAdditionalParameter {\n  name?: string | null;\n  value?: string | null;\n  sendIn: 'headers' | 'queryparams' | 'body';\n  enabled?: boolean;\n}\n\nexport interface OAuthAdditionalParameters {\n  authorization?: OAuthAdditionalParameter[] | null;\n  token?: OAuthAdditionalParameter[] | null;\n  refresh?: OAuthAdditionalParameter[] | null;\n}\n\nexport interface OAuth2 {\n  grantType: OAuthGrantType;\n  username?: string | null;\n  password?: string | null;\n  callbackUrl?: string | null;\n  authorizationUrl?: string | null;\n  accessTokenUrl?: string | null;\n  clientId?: string | null;\n  clientSecret?: string | null;\n  scope?: string | null;\n  state?: string | null;\n  pkce?: boolean | null;\n  credentialsPlacement?: 'body' | 'basic_auth_header' | null;\n  credentialsId?: string | null;\n  tokenPlacement?: string | null;\n  tokenHeaderPrefix?: string | null;\n  tokenQueryKey?: string | null;\n  refreshTokenUrl?: string | null;\n  autoRefreshToken?: boolean | null;\n  autoFetchToken?: boolean | null;\n  tokenSource?: 'access_token' | 'id_token';\n  additionalParameters?: OAuthAdditionalParameters | null;\n}\n\nexport type AuthMode\n  = | 'inherit'\n    | 'none'\n    | 'awsv4'\n    | 'basic'\n    | 'bearer'\n    | 'digest'\n    | 'ntlm'\n    | 'oauth2'\n    | 'wsse'\n    | 'apikey';\n\nexport interface Auth {\n  mode: AuthMode;\n  awsv4?: AuthAwsV4 | null;\n  basic?: AuthBasic | null;\n  bearer?: AuthBearer | null;\n  digest?: AuthDigest | null;\n  ntlm?: AuthNTLM | null;\n  oauth2?: OAuth2 | null;\n  wsse?: AuthWsse | null;\n  apikey?: AuthApiKey | null;\n}\n"
  },
  {
    "path": "packages/bruno-schema-types/src/common/file.ts",
    "content": "import type { UID } from './uid';\n\nexport interface FileEntry {\n  uid: UID;\n  filePath?: string | null;\n  contentType?: string | null;\n  selected: boolean;\n}\n\nexport type FileList = FileEntry[];\n"
  },
  {
    "path": "packages/bruno-schema-types/src/common/graphql.ts",
    "content": "export interface GraphqlBody {\n  query?: string | null;\n  variables?: string | null;\n}\n"
  },
  {
    "path": "packages/bruno-schema-types/src/common/index.ts",
    "content": "export type { UID } from './uid';\nexport type { KeyValue } from './key-value';\nexport type { Variable, Variables } from './variables';\nexport type { MultipartFormEntry, MultipartForm } from './multipart-form';\nexport type { FileEntry, FileList } from './file';\nexport type { GraphqlBody } from './graphql';\nexport type { Script } from './scripts';\nexport type {\n  Auth,\n  AuthMode,\n  AuthAwsV4,\n  AuthBasic,\n  AuthBearer,\n  AuthDigest,\n  AuthNTLM,\n  AuthWsse,\n  AuthApiKey,\n  OAuth2,\n  OAuthGrantType,\n  OAuthAdditionalParameter,\n  OAuthAdditionalParameters\n} from './auth';\n"
  },
  {
    "path": "packages/bruno-schema-types/src/common/key-value.ts",
    "content": "import type { UID } from './uid';\n\n/**\n * Generic key/value structure used for headers, params, assertions, etc.\n */\nexport interface KeyValue {\n  uid: UID;\n  name?: string | null;\n  value?: string | null;\n  description?: string | null;\n  enabled?: boolean;\n}\n"
  },
  {
    "path": "packages/bruno-schema-types/src/common/multipart-form.ts",
    "content": "import type { UID } from './uid';\n\nexport interface MultipartFormEntry {\n  uid: UID;\n  type: 'file' | 'text';\n  name?: string | null;\n  value?: string | string[] | null;\n  description?: string | null;\n  contentType?: string | null;\n  enabled?: boolean;\n}\n\nexport type MultipartForm = MultipartFormEntry[];\n"
  },
  {
    "path": "packages/bruno-schema-types/src/common/scripts.ts",
    "content": "export interface Script {\n  req?: string | null;\n  res?: string | null;\n}\n"
  },
  {
    "path": "packages/bruno-schema-types/src/common/uid.ts",
    "content": "/**\n * Unique identifier used across Bruno collections.\n */\nexport type UID = string;\n"
  },
  {
    "path": "packages/bruno-schema-types/src/common/variables.ts",
    "content": "import type { UID } from './uid';\n\n/**\n * Request-scoped variable entry.\n */\nexport interface Variable {\n  uid: UID;\n  name?: string | null;\n  value?: string | null;\n  description?: string | null;\n  enabled?: boolean;\n  local?: boolean;\n}\n\nexport type Variables = Variable[] | null;\n"
  },
  {
    "path": "packages/bruno-schema-types/src/index.ts",
    "content": "export * as Common from './common';\nexport * as Requests from './requests';\nexport * as Collection from './collection';\n\nexport type {\n  Collection as BrunoCollection,\n  Item as BrunoItem,\n  Environment as BrunoEnvironment,\n  Environments as BrunoEnvironments\n} from './collection';\nexport type { Request as BrunoRequest } from './requests';\n"
  },
  {
    "path": "packages/bruno-schema-types/src/requests/grpc.ts",
    "content": "import type { KeyValue, Script, Variables, Auth } from '../common';\n\nexport type GrpcMethodType\n  = | 'unary'\n    | 'client-streaming'\n    | 'server-streaming'\n    | 'bidi-streaming'\n    | '';\n\nexport interface GrpcMessage {\n  name?: string | null;\n  content?: string | null;\n}\n\nexport interface GrpcRequestBody {\n  mode: 'grpc';\n  grpc?: GrpcMessage[] | null;\n}\n\nexport interface GrpcRequest {\n  url: string;\n  method?: string | null;\n  methodType?: GrpcMethodType | null;\n  protoPath?: string | null;\n  headers: KeyValue[];\n  auth?: Auth | null;\n  body: GrpcRequestBody;\n  script?: Script | null;\n  vars?: {\n    req: Variables;\n    res: Variables;\n  } | null;\n  assertions?: KeyValue[] | null;\n  tests?: string | null;\n  docs?: string | null;\n}\n"
  },
  {
    "path": "packages/bruno-schema-types/src/requests/http.ts",
    "content": "import type {\n  KeyValue,\n  Script,\n  Variables,\n  Auth,\n  MultipartForm,\n  FileList,\n  GraphqlBody\n} from '../common';\n\nexport type HttpRequestParamType = 'query' | 'path';\n\nexport interface HttpRequestParam extends KeyValue {\n  type: HttpRequestParamType;\n}\n\nexport type HttpRequestBodyMode\n  = | 'none'\n    | 'json'\n    | 'text'\n    | 'xml'\n    | 'formUrlEncoded'\n    | 'multipartForm'\n    | 'graphql'\n    | 'sparql'\n    | 'file';\n\nexport interface HttpRequestBody {\n  mode: HttpRequestBodyMode;\n  json?: string | null;\n  text?: string | null;\n  xml?: string | null;\n  sparql?: string | null;\n  formUrlEncoded?: KeyValue[] | null;\n  multipartForm?: MultipartForm | null;\n  graphql?: GraphqlBody | null;\n  file?: FileList | null;\n}\n\nexport interface HttpRequest {\n  url: string;\n  method: string;\n  headers: KeyValue[];\n  params: HttpRequestParam[];\n  auth?: Auth | null;\n  body?: HttpRequestBody | null;\n  script?: Script | null;\n  vars?: {\n    req: Variables;\n    res: Variables;\n  } | null;\n  assertions?: KeyValue[] | null;\n  tests?: string | null;\n  docs?: string | null;\n}\n"
  },
  {
    "path": "packages/bruno-schema-types/src/requests/index.ts",
    "content": "import type { HttpRequest } from './http';\nimport type { GrpcRequest } from './grpc';\nimport type { WebSocketRequest } from './websocket';\n\nexport type {\n  HttpRequest,\n  HttpRequestBody,\n  HttpRequestBodyMode,\n  HttpRequestParam,\n  HttpRequestParamType\n} from './http';\n\nexport type {\n  GrpcRequest,\n  GrpcRequestBody,\n  GrpcMessage,\n  GrpcMethodType\n} from './grpc';\n\nexport type {\n  WebSocketRequest,\n  WebSocketRequestBody,\n  WebSocketMessage\n} from './websocket';\n\nexport type Request = HttpRequest | GrpcRequest | WebSocketRequest;\n"
  },
  {
    "path": "packages/bruno-schema-types/src/requests/websocket.ts",
    "content": "import type { KeyValue, Script, Variables, Auth } from '../common';\n\nexport interface WebSocketMessage {\n  name?: string | null;\n  type?: string | null;\n  content?: string | null;\n}\n\nexport interface WebSocketRequestBody {\n  mode: 'ws';\n  ws?: WebSocketMessage[] | null;\n}\n\nexport interface WebSocketRequest {\n  url: string;\n  headers: KeyValue[];\n  auth?: Auth | null;\n  body: WebSocketRequestBody;\n  script?: Script | null;\n  vars?: {\n    req: Variables;\n    res: Variables;\n  } | null;\n  assertions?: KeyValue[] | null;\n  tests?: string | null;\n  docs?: string | null;\n}\n"
  },
  {
    "path": "packages/bruno-schema-types/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"module\": \"ESNext\",\n    \"lib\": [\"ES2020\"],\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"moduleResolution\": \"bundler\",\n    \"resolveJsonModule\": true\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n\n"
  },
  {
    "path": "packages/bruno-tests/.gitignore",
    "content": "# JUnit\ncollection/junit.xml\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript v1 declaration files\ntypings/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n.env.test\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n\n# Next.js build output\n.next\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and *not* Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n"
  },
  {
    "path": "packages/bruno-tests/.nvmrc",
    "content": "v22.17.0\n"
  },
  {
    "path": "packages/bruno-tests/additional-context-root-lib/index.js",
    "content": "/**\n * Utility module in additionalContextRoot to test:\n * 1. Loading modules from additionalContextRoot\n * 2. npm module resolution (@faker-js/faker) from collection's node_modules\n * 3. Local module resolution (./lib.js) relative to additionalContextRoot\n */\n\nconst { faker } = require('@faker-js/faker');\nconst { formatName, generateGreeting } = require('./lib');\n\n/**\n * Generate a random user with greeting\n * Tests both npm module and local module resolution\n */\nfunction generateUser() {\n  const firstName = faker.person.firstName();\n  const lastName = faker.person.lastName();\n  const fullName = formatName(firstName, lastName);\n  const greeting = generateGreeting(fullName);\n\n  return {\n    firstName,\n    lastName,\n    fullName,\n    greeting,\n    email: faker.internet.email({ firstName, lastName })\n  };\n}\n\n/**\n * Verify that all dependencies resolved correctly\n */\nfunction verifyDependencies() {\n  return {\n    fakerLoaded: typeof faker === 'object' && typeof faker.person === 'object',\n    localModuleLoaded: typeof formatName === 'function' && typeof generateGreeting === 'function'\n  };\n}\n\nmodule.exports = {\n  generateUser,\n  verifyDependencies,\n  formatName,\n  generateGreeting\n};\n"
  },
  {
    "path": "packages/bruno-tests/additional-context-root-lib/lib.js",
    "content": "/**\n * Simple local module to test local module resolution from additionalContextRoot\n */\n\nfunction formatName(firstName, lastName) {\n  return `${firstName} ${lastName}`;\n}\n\nfunction generateGreeting(name) {\n  return `Hello, ${name}!`;\n}\n\nmodule.exports = {\n  formatName,\n  generateGreeting\n};\n"
  },
  {
    "path": "packages/bruno-tests/collection/.env",
    "content": "PROC_ENV_VAR=woof"
  },
  {
    "path": "packages/bruno-tests/collection/.gitignore",
    "content": "!.env"
  },
  {
    "path": "packages/bruno-tests/collection/.nvmrc",
    "content": "v18"
  },
  {
    "path": "packages/bruno-tests/collection/asserts/test-assert-combinations.bru",
    "content": "meta {\n  name: test-assert-combinations\n  type: http\n  seq: 1\n}\n\npost {\n  url: {{httpfaker}}/api/echo/custom\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"type\": \"application/json\",\n    \"contentJSON\": {\n      \"string\": \"foo\",\n      \"stringWithSQuotes\": \"'foo'\",\n      \"stringWithDQuotes\": \"\\\"foo\\\"\",\n      \"number\": 123,\n      \"numberAsString\": \"123\",\n      \"numberAsStringWithSQuotes\": \"'123'\",\n      \"numberAsStringWithDQuotes\": \"\\\"123\\\"\",\n      \"numberAsStringWithLeadingZero\": \"0123\",\n      \"numberBig\": 9007199254740992000,\n      \"numberBigAsString\": \"9007199254740991999\",\n      \"null\": null,\n      \"nullAsString\": \"null\",\n      \"nullAsStringWithSQuotes\": \"'null'\",\n      \"nullAsStringWithDQuotes\": \"\\\"null\\\"\",\n      \"true\": true,\n      \"trueAsString\": \"true\",\n      \"trueAsStringWithSQuotes\": \"'true'\",\n      \"trueAsStringWithDQuotes\": \"\\\"true\\\"\",\n      \"false\": false,\n      \"falseAsString\": \"false\",\n      \"falseAsStringWithSQuotes\": \"'false'\",\n      \"falseAsStringWithDQuotes\": \"\\\"false\\\"\",\n      \"stringWithCurlyBraces\": \"{foo}\",\n      \"stringWithDoubleCurlyBraces\": \"{{foobar}}\"\n    }\n  }\n}\n\nassert {\n  res.body.string: eq foo\n  res.body.string: eq 'foo'\n  res.body.string: eq \"foo\"\n  res.body.stringWithSQuotes: eq \"'foo'\"\n  res.body.stringWithDQuotes: eq '\"foo\"'\n  res.body.number: eq 123\n  res.body.numberAsString: eq '123'\n  res.body.numberAsString: eq \"123\"\n  res.body.numberAsStringWithSQuotes: eq \"'123'\"\n  res.body.numberAsStringWithDQuotes: eq '\"123\"'\n  res.body.numberAsStringWithLeadingZero: eq \"0123\"\n  res.body.numberBig.toString(): eq '9007199254740992000'\n  res.body.numberBigAsString: eq \"9007199254740991999\"\n  res.body.null: eq null\n  res.body.nullAsString: eq \"null\"\n  res.body.nullAsStringWithSQuotes: eq \"'null'\"\n  res.body.nullAsStringWithDQuotes: eq '\"null\"'\n  res.body.true: eq true\n  res.body.trueAsString: eq \"true\"\n  res.body.trueAsStringWithSQuotes: eq \"'true'\"\n  res.body.trueAsStringWithDQuotes: eq '\"true\"'\n  res.body.false: eq false\n  res.body.falseAsString: eq \"false\"\n  res.body.falseAsStringWithSQuotes: eq \"'false'\"\n  res.body.falseAsStringWithDQuotes: eq '\"false\"'\n  res.body.nonexistent: eq undefined\n  res.body.stringWithCurlyBraces: eq \"{foo}\"\n  res.body.stringWithDoubleCurlyBraces: eq \"{{foobar}}\"\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/auth/basic/via auth/Basic Auth 200.bru",
    "content": "meta {\n  name: Basic Auth 200\n  type: http\n  seq: 1\n}\n\npost {\n  url: {{host}}/api/auth/basic/protected\n  body: json\n  auth: basic\n}\n\nauth:basic {\n  username: bruno\n  password: {{basic_auth_password}}\n}\n\nassert {\n  res.status: 200\n  res.body.message: Authentication successful\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/auth/basic/via auth/Basic Auth 401.bru",
    "content": "meta {\n  name: Basic Auth 400\n  type: http\n  seq: 2\n}\n\npost {\n  url: {{host}}/api/auth/basic/protected\n  body: json\n  auth: none\n}\n\nauth:basic {\n  username: bruno\n  password: invalid\n}\n\nassert {\n  res.status: 401\n  res.body: Unauthorized\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/auth/basic/via script/Basic Auth 200.bru",
    "content": "meta {\n  name: Basic Auth 200\n  type: http\n  seq: 1\n}\n\npost {\n  url: {{host}}/api/auth/basic/protected\n  body: json\n  auth: none\n}\n\nassert {\n  res.status: eq 200\n  res.body.message: Authentication successful\n}\n\nscript:pre-request {\n  const username = \"bruno\";\n  const password = \"della\";\n  \n  const authString = `${username}:${password}`;\n  const encodedAuthString = require('btoa')(authString);\n  \n  req.setHeader(\"Authorization\", `Basic ${encodedAuthString}`);\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/auth/basic/via script/Basic Auth 401.bru",
    "content": "meta {\n  name: Basic Auth 401\n  type: http\n  seq: 2\n}\n\npost {\n  url: {{host}}/api/auth/basic/protected\n  body: json\n  auth: none\n}\n\nassert {\n  res.status: 401\n  res.body: Unauthorized\n}\n\nscript:pre-request {\n  const username = \"bruno\";\n  const password = \"invalid\";\n  \n  const authString = `${username}:${password}`;\n  const encodedAuthString = require('btoa')(authString);\n  \n  req.setHeader(\"Authorization\", `Basic ${encodedAuthString}`);\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/auth/bearer/via auth/Bearer Auth 200.bru",
    "content": "meta {\n  name: Bearer Auth 200\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/api/auth/bearer/protected\n  body: none\n  auth: bearer\n}\n\nauth:bearer {\n  token: {{bearer_auth_token}}\n}\n\nassert {\n  res.status: 200\n  res.body.message: Authentication successful\n}\n\nscript:post-response {\n  bru.setEnvVar(\"foo\", \"bar\");\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/auth/bearer/via auth/Bearer Auth undefined.bru",
    "content": "meta {\n  name: Bearer Auth undefined\n  type: http\n  seq: 2\n}\n\nget {\n  url: {{host}}/api/auth/bearer/protected\n  body: none\n  auth: bearer\n}\n\nheaders {\n  Authorization: Bearer {{bearer_auth_token}}\n}\n\nassert {\n  res.body.message: eq Unauthorized\n  res.status: eq 401\n}\n\ntests {\n  test(\"selected auth overrides Authorization header always\", function() {\n    const authHeader =  req.getHeader(\"Authorization\")\n    expect(authHeader).to.eql(\"Bearer \")\n  })\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/auth/bearer/via headers/Bearer Auth 200.bru",
    "content": "meta {\n  name: Bearer Auth 200\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/api/auth/bearer/protected\n  body: json\n  auth: none\n}\n\nheaders {\n  Authorization: Bearer {{bearer_auth_token}}\n}\n\nvars:pre-request {\n  a-c: foo\n}\n\nassert {\n  res.status: 200\n  res.body.message: Authentication successful\n}\n\nscript:post-response {\n  bru.setEnvVar(\"foo\", \"bar\");\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/auth/cookie/Check.bru",
    "content": "meta {\n  name: Check\n  type: http\n  seq: 2\n}\n\nget {\n  url: {{host}}/api/auth/cookie/protected\n  body: none\n  auth: none\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/auth/cookie/Login.bru",
    "content": "meta {\n  name: Login\n  type: http\n  seq: 1\n}\n\npost {\n  url: {{host}}/api/auth/cookie/login\n  body: none\n  auth: none\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/auth/digest/Digest Auth 200.bru",
    "content": "meta {\n  name: Digest Auth 200\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://www.httpfaker.org/api/auth/digest/auth/admin/password\n  body: none\n  auth: digest\n}\n\nauth:digest {\n  username: admin\n  password: password\n}\n\nassert {\n  res.status: eq 200\n  res.body.authenticated: isTruthy\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/auth/digest/Digest Auth 401.bru",
    "content": "meta {\n  name: Digest Auth 401\n  type: http\n  seq: 2\n}\n\nget {\n  url: https://www.httpfaker.org/api/auth/digest/auth/admin/badpassword\n  body: none\n  auth: digest\n}\n\nauth:digest {\n  username: foo\n  password: passwd\n}\n\nassert {\n  res.status: eq 401\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/auth/digest/folder.bru",
    "content": "meta {\n  name: digest\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/auth/inherit auth/inherit Bearer Auth 200.bru",
    "content": "meta {\n  name: inherit Bearer Auth 200\n  type: http\n  seq: 2\n}\n\nget {\n  url: {{host}}/api/auth/bearer/protected\n  body: none\n  auth: inherit\n}\n\nassert {\n  res.status: 200\n  res.body.message: Authentication successful\n}\n\nscript:post-response {\n  bru.setEnvVar(\"foo\", \"bar\");\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"bruno-testbench\",\n  \"type\": \"collection\",\n  \"proxy\": {\n    \"enabled\": false,\n    \"protocol\": \"http\",\n    \"hostname\": \"{{proxyHostname}}\",\n    \"port\": 4000,\n    \"auth\": {\n      \"enabled\": false,\n      \"username\": \"anoop\",\n      \"password\": \"password\"\n    },\n    \"bypassProxy\": \"\"\n  },\n  \"scripts\": {\n    \"moduleWhitelist\": [\"crypto\", \"buffer\", \"form-data\"],\n    \"additionalContextRoots\": [\"../additional-context-root-lib\"]\n  },\n  \"clientCertificates\": {\n    \"enabled\": true,\n    \"certs\": []\n  },\n  \"presets\": {\n    \"requestType\": \"http\",\n    \"requestUrl\": \"http://localhost:6000\"\n  }\n}"
  },
  {
    "path": "packages/bruno-tests/collection/collection.bru",
    "content": "headers {\n  check: again\n  token: {{collection_pre_var_token}}\n  collection-header: collection-header-value\n}\n\nauth {\n  mode: bearer\n}\n\nauth:bearer {\n  token: {{bearer_auth_token}}\n}\n\nvars:pre-request {\n  collection_pre_var: collection_pre_var_value\n  collection_pre_var_token: {{request_pre_var_token}}\n  collection-var: collection-var-value\n}\n\nscript:pre-request {\n  // used by `scripting/js/folder-collection script-tests`\n  const shouldTestCollectionScripts = bru.getVar('should-test-collection-scripts');\n  if(shouldTestCollectionScripts) {\n   bru.setVar('collection-var-set-by-collection-script', 'collection-var-value-set-by-collection-script');\n  }\n}\n\ntests {\n  // used by `scripting/js/folder-collection script-tests`\n  const shouldTestCollectionScripts = bru.getVar('should-test-collection-scripts');\n  const collectionVar = bru.getVar(\"collection-var-set-by-collection-script\");\n  if (shouldTestCollectionScripts && collectionVar) {\n    test(\"collection level test - should get the var that was set by the collection script\", function() {\n      expect(collectionVar).to.equal(\"collection-var-value-set-by-collection-script\");\n    }); \n    bru.setVar('collection-var-set-by-collection-script', null); \n    bru.setVar('should-test-collection-scripts', null);\n  }\n}\n\ndocs {\n  # bruno-testbench 🐶\n  \n  This is a test collection that I am using to test various functionalities around bruno\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/echo/echo bom json.bru",
    "content": "meta {\n  name: echo bom json\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/api/echo/bom-json-test\n  body: none\n  auth: none\n}"
  },
  {
    "path": "packages/bruno-tests/collection/echo/echo form-url-encoded.bru",
    "content": "meta {\n  name: echo form-url-encoded\n  type: http\n  seq: 9\n}\n\npost {\n  url: {{echo-host}}\n  body: formUrlEncoded\n  auth: none\n}\n\nbody:form-urlencoded {\n  form-data-key: {{form-data-key}}\n  form-data-stringified-object: {{form-data-stringified-object}}\n  key_1: value_1\n  key_2: value_2\n  key_1: value_3\n  key_2: value_4\n}\n\nscript:pre-request {\n  let obj = JSON.stringify({foo:123});\n  bru.setVar('form-data-key', 'form-data-value');\n  bru.setVar('form-data-stringified-object', obj);\n}\n\ntests {\n  test(\"form-urlencoded body with variables and duplicate keys\", function() {\n    const expected = [\n      \"form-data-key=form-data-value\",\n      \"form-data-stringified-object=%7B%22foo%22%3A123%7D\", // {\"foo\":123} URL encoded\n      \"key_1=value_1\",\n      \"key_2=value_2\", \n      \"key_1=value_3\", // duplicate key with different value\n      \"key_2=value_4\"  // duplicate key with different value\n    ].join(\"&\");\n    \n    expect(res.getBody()).to.eql(expected);\n  });\n}"
  },
  {
    "path": "packages/bruno-tests/collection/echo/echo headers.bru",
    "content": "meta {\n  name: echo headers\n  type: http\n  seq: 13\n}\n\npost {\n  url: {{echo-host}}\n  body: none\n  auth: inherit\n}\n\nheaders {\n  Custom-Header-String: bruno\n}\n\ntests {\n  test(\"test headers\",function() {\n    expect(res.getHeaders()).to.have.property(\"Custom-Header-String\".toLowerCase())\n    expect(res.getHeaders()).to.have.property(\"Custom-Header-String\".toLowerCase(), \"bruno\")\n  })\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/echo/echo json.bru",
    "content": "meta {\n  name: echo json\n  type: http\n  seq: 2\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nheaders {\n  foo: bar\n}\n\nauth:basic {\n  username: asd\n  password: j\n}\n\nauth:bearer {\n  token: \n}\n\nbody:json {\n  {\n    \"hello\": \"bruno\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\nscript:pre-request {\n  bru.setVar(\"foo\", \"foo-world-2\");\n}\n\ntests {\n  test(\"should return json\", function() {\n    const data = res.getBody();\n    expect(res.getBody()).to.eql({\n      \"hello\": \"bruno\"\n    });\n  });  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/echo/echo multipart scripting.bru",
    "content": "meta {\n  name: echo multipart via scripting\n  type: http\n  seq: 10\n}\n\npost {\n  url: {{echo-host}}\n  body: multipartForm\n  auth: none\n}\n\nassert {\n  res.body: contains form-data-value\n}\n\nscript:pre-request {\n  const FormData = require(\"form-data\");\n  const form = new FormData();\n  form.append('form-data-key', 'form-data-value');\n  req.setBody(form);\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/echo/echo multipart.bru",
    "content": "meta {\n  name: echo multipart\n  type: http\n  seq: 8\n}\n\npost {\n  url: {{echo-host}}\n  body: multipartForm\n  auth: none\n}\n\nbody:multipart-form {\n  foo: {\"bar\":\"baz\"} @contentType(application/json--test)\n  multiline: '''\n    \"multiline-test\"\n  ''' @contentType(application/json--multiline--test)\n  form-data-key: {{form-data-key}}\n  form-data-stringified-object: {{form-data-stringified-object}}\n  file: @file(bruno.png)\n}\n\nassert {\n  res.body: contains form-data-value\n  res.body: contains {\"foo\":123}\n  res.body: contains Content-Type: application/json--test\n  res.body: contains Content-Type: application/json--multiline--test\n}\n\nscript:pre-request {\n  let obj = JSON.stringify({foo:123});\n  bru.setVar('form-data-key', 'form-data-value');\n  bru.setVar('form-data-stringified-object', obj);\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/echo/echo numbers.bru",
    "content": "meta {\n  name: echo numbers\n  type: http\n  seq: 1\n}\n\npost {\n  url: {{echo-host}}\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"integer\": 123,\n    \"negativeInteger\": -99,\n    \"zero\": 0,\n    \"float\": 2.718,\n    \"negativeFloat\": -1.618,\n    \"largeDouble\": 12345.678901234567,\n    \"smallDouble\": 9.876e-12,\n    \"booleanTrue\": true,\n    \"booleanFalse\": false\n  }\n}\n\nassert {\n  res.body.integer: eq 123\n  res.body.integer: isNumber\n  res.body.negativeInteger: eq -99\n  res.body.negativeInteger: isNumber\n  res.body.zero: eq 0\n  res.body.zero: isNumber\n  res.body.float: eq 2.718\n  res.body.float: isNumber\n  res.body.negativeFloat: eq -1.618\n  res.body.negativeFloat: isNumber\n  res.body.largeDouble: eq 12345.678901234567\n  res.body.largeDouble: isNumber\n  res.body.smallDouble: eq 9.876e-12\n  res.body.smallDouble: isNumber\n  res.body.booleanTrue: eq true\n  res.body.booleanFalse: eq false\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/echo/echo plaintext.bru",
    "content": "meta {\n  name: echo plaintext\n  type: http\n  seq: 3\n}\n\npost {\n  url: {{host}}/api/echo/text\n  body: text\n  auth: none\n}\n\nbody:text {\n  hello\n}\n\nassert {\n  res.status: eq 200\n}\n\ntests {\n  test(\"should return plain text\", function() {\n    const data = res.getBody();\n    expect(res.getBody()).to.eql(\"hello\");\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/echo/echo xml parsed(self closing tags).bru",
    "content": "meta {\n  name: echo xml parsed(self closing tags)\n  type: http\n  seq: 6\n}\n\npost {\n  url: {{host}}/api/echo/xml-parsed\n  body: xml\n  auth: none\n}\n\nbody:xml {\n  <hello>\n    <world>bruno</world>\n    <world/>\n  </hello>\n}\n\nassert {\n  res.status: eq 200\n}\n\ntests {\n  test(\"should return parsed xml\", function() {\n    const data = res.getBody();\n    expect(res.getBody()).to.eql({\n      \"hello\": {\n        \"world\": [\n          \"bruno\",\n          \"\"\n        ]\n      }\n    });\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/echo/echo xml parsed.bru",
    "content": "meta {\n  name: echo xml parsed\n  type: http\n  seq: 4\n}\n\npost {\n  url: {{host}}/api/echo/xml-parsed\n  body: xml\n  auth: none\n}\n\nbody:xml {\n  <hello>\n    <world>bruno</world>\n  </hello>\n}\n\nassert {\n  res.status: eq 200\n}\n\ntests {\n  test(\"should return parsed xml\", function() {\n    const data = res.getBody();\n    expect(res.getBody()).to.eql({\n      \"hello\": {\n        \"world\": [\"bruno\"]\n      }\n    });\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/echo/echo xml raw.bru",
    "content": "meta {\n  name: echo xml raw\n  type: http\n  seq: 5\n}\n\npost {\n  url: {{host}}/api/echo/xml-raw\n  body: xml\n  auth: none\n}\n\nbody:xml {\n  <hello><world>bruno</world></hello>\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/echo/multiline/echo binary.bru",
    "content": "meta {\n  name: echo binary\n  type: http\n  seq: 1\n}\n\npost {\n  url: {{echo-host}}\n  body: file\n  auth: none\n}\n\nbody:file {\n  file: @file(bruno.png) @contentType(image/png)\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/echo/test echo any.bru",
    "content": "meta {\n  name: test echo any\n  type: http\n  seq: 11\n}\n\npost {\n  url: {{httpfaker}}/api/echo/custom\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"headers\": { \"content-type\": \"text/plain\" },\n    \"content\": \"hello\"\n  }\n}\n\nassert {\n  res.body: eq hello\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/echo/test echo-any json.bru",
    "content": "meta {\n  name: test echo-any json\n  type: http\n  seq: 12\n}\n\npost {\n  url: {{httpfaker}}/api/echo/custom\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"type\": \"application/json\",\n    \"contentJSON\": {\"x\": 42}\n  }\n}\n\nassert {\n  res.body.x: eq 42\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/environments/Local.bru",
    "content": "vars {\n  host: http://localhost:8080\n  localhost: http://localhost:8081\n  httpfaker: https://www.httpfaker.org\n  bearer_auth_token: your_secret_token\n  basic_auth_password: della\n  env.var1: envVar1\n  env-var2: envVar2\n  bark: {{process.env.PROC_ENV_VAR}}\n  foo: bar\n  testSetEnvVar: bruno-29653\n  echo-host: https://echo.usebruno.com\n  client_id: client_id_1\n  client_secret: client_secret_1\n  auth_url: http://localhost:8080/api/auth/oauth2/authorization_code/authorize\n  callback_url: http://localhost:8080/api/auth/oauth2/authorization_code/callback\n  access_token_url: http://localhost:8080/api/auth/oauth2/authorization_code/token\n  passwordCredentials_username: foo\n  passwordCredentials_password: bar\n  github_authorize_url: https://github.com/login/oauth/authorize\n  github_access_token_url: https://github.com/login/oauth/access_token\n  google_auth_url: https://accounts.google.com/o/oauth2/auth\n  google_access_token_url: https://accounts.google.com/o/oauth2/token\n  google_scope: https://www.googleapis.com/auth/userinfo.email\n}\nvars:secret [\n  github_client_secret,\n  github_client_id,\n  google_client_id,\n  google_client_secret,\n  github_authorization_code,\n  passwordCredentials_access_token,\n  client_credentials_access_token,\n  authorization_code_access_token,\n  github_access_token\n]\n"
  },
  {
    "path": "packages/bruno-tests/collection/environments/Prod.bru",
    "content": "vars {\n  host: https://testbench-sanity.usebruno.com\n  localhost: http://localhost:8081\n  httpfaker: https://www.httpfaker.org\n  bearer_auth_token: your_secret_token\n  basic_auth_password: della\n  env.var1: envVar1\n  env-var2: envVar2\n  bark: {{process.env.PROC_ENV_VAR}}\n  foo: bar\n  testSetEnvVar: bruno-29653\n  echo-host: https://echo.usebruno.com\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/file.json",
    "content": "{\n  \"hello\": \"bruno\"\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/file.txt",
    "content": "file.txt\n\nhello, bruno\n"
  },
  {
    "path": "packages/bruno-tests/collection/graphql/mutation.bru",
    "content": "meta {\n  name: mutation\n  type: graphql\n  seq: 3\n}\n\npost {\n  url: {{localhost}}/api/graphql\n  body: graphql\n  auth: inherit\n}\n\nbody:graphql {\n  mutation create($id: String!) {\n    create(payload: { id: $id }) {\n      success\n    }\n  }\n  \n}\n\nbody:graphql:vars {\n  {\n    \"id\":\"1\"\n  }\n}\n\nassert {\n  res.status: eq 200\n  res.body.data.create.success: eq true\n}\n\nsettings {\n  encodeUrl: true\n  timeout: 0\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/graphql/spacex.bru",
    "content": "meta {\n  name: spacex\n  type: graphql\n  seq: 1\n}\n\npost {\n  url: {{localhost}}/api/graphql\n  body: graphql\n  auth: none\n}\n\nbody:graphql {\n  {\n    company {\n      ceo\n    }\n  }\n  \n}\n\nassert {\n  res.status: eq 200\n  res.body.data.company.ceo: eq Elon Musk\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/graphql/variable-interpolation.bru",
    "content": "meta {\n  name: variables interpolation\n  type: graphql\n  seq: 3\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: graphql\n  auth: none\n}\n\nbody:graphql {\n  query { __typename }\n}\n\nbody:graphql:vars {\n  {\n    \"my_json\": \"{{my_json}}\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\nscript:pre-request {\n  const testData = {\n    a: [1,2,3],\n    b: {\n      c: \"test\",\n      d: \"another value\"\n    }\n  };\n  \n  // Single escaping\n  let cv = JSON.stringify(testData).replace(/\"/g, '\\\\\"');\n  \n  bru.setVar(\"my_json\", cv)\n}\n\nscript:post-response {\n  bru.deleteVar(\"my_json\")\n}\n\ntests {\n  test(\"GraphQL variables with nested object and array are interpolated then sent as parsed object\", function() {\n    const body = res.getBody();\n    expect(body).to.have.property(\"variables\");\n    expect(body.variables).to.be.an(\"object\");\n    expect(body.variables).to.have.property(\"my_json\");\n    expect(body.variables.my_json).to.eql(\"{\\\"a\\\":[1,2,3],\\\"b\\\":{\\\"c\\\":\\\"test\\\",\\\"d\\\":\\\"another value\\\"}}\");\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/lib/constants.js",
    "content": "const PI = 3.14;\n\nmodule.exports = {\n  PI\n};\n"
  },
  {
    "path": "packages/bruno-tests/collection/lib/math.js",
    "content": "const { PI } = require('./constants');\n\nconst sum = (a, b) => a + b;\nconst areaOfCircle = (radius) => PI * radius * radius;\n\nmodule.exports = {\n  sum,\n  areaOfCircle\n};\n"
  },
  {
    "path": "packages/bruno-tests/collection/lib/notes.js",
    "content": "const visualizeNotes = (res) => {\n  let response = res.body;\n\n  let notes = response?.notes || {};\n  let responseRuntime = notes.runtime_sec || 0;\n\n  notes.runtime = new Date(responseRuntime * 1000).toISOString().substr(11, 8);\n\n  if (typeof response?.data === 'undefined' && typeof response?.rows === 'object') {\n    response.data = response?.rows?.map(function (data) {\n      return data?.values;\n    });\n  }\n\n  const templateScript = `\n    <script id=\"template\" type=\"text/x-handlebars-template\">\n      {{#if response.data}}\n          <div>\n              <p>Total rows: {{notes.result_rows}}</p>\n              <p>Query count: {{notes.query_count}}</p>\n              <p>Duration: {{notes.runtime}}</p>\n          </div>\n          <table id=\"data_table\">\n              {{#each response.data}}\n                  {{#if @first}}\n                      <tr>\n                          {{#each this}}\n                              <th>\n                                  {{#with (lookup ../../response.fields @index)~}}\n                                      <small>\n                                          {{name}} ({{type}})<br>\n                                          {{data_type}}<br>\n                                      </small>\n                                  {{/with}}\n                                  {{this}}\n                              </th>\n                          {{/each}}\n                      </tr>\n                  {{else}}\n                      <tr id=\"row_{{@key}}\" class=\"data_row\">\n                          {{#each this}}\n                              <td>{{this}}</td>\n                          {{/each}}\n                      </tr>\n                  {{/if}}\n              {{/each}}\n          </table>\n      {{else if response.results}}\n          <table id=\"data_table\">\n              <tr>\n                  {{#each response.results.[0]}}\n                      <th>{{@key}}</th>\n                  {{/each}}\n              </tr>\n              {{#each response.results}}\n                  <tr id=\"row_{{@key}}\" class=\"data_row\">\n                      {{#each this}}\n                          <td>{{this}}</td>\n                      {{/each}}\n                  </tr>\n              {{/each}}\n          </table>\n      {{else}}\n          <div class=\"error\">\n              <h1>Error</h1>\n              {{#if response.notes}}\n                  {{response.notes.error}}\n              {{else}}\n                  No response\n              {{/if}}\n          </div>\n      {{/if}}\n    </script>\n  `;\n\n  const mainScript = `\n    <script>\n      document.addEventListener(\"DOMContentLoaded\", function() {\n        let data = ${JSON.stringify({\n          response,\n          notes\n        })}\n        let source = document.getElementById(\"template\").innerHTML;\n        let template = Handlebars.compile(source);\n        document.body.innerHTML = template(data);\n        document.getElementById('data_table').addEventListener('click', function(e) {\n          var row = e.target.closest('tr.data_row');\n          if (row) {\n              row.classList.toggle('marked');\n          }\n        });\n      });\n    </script>\n  `;\n\n  const style = `\n    <style type=\"text/css\">\n        div {\n            margin-bottom: 8px;\n        }\n        div p {\n            font-family: courier;\n            font-size: 12px;\n            line-height: 1.2;\n            color: #afafaf;\n            margin: 0;\n        }\n        div.error {\n            padding: 20px;\n            background-color: #ffcece;\n            color: #792626;\n            font-size: 18px;\n        }\n        div.error h1 {\n            color: #dd4545;\n            line-height: 50px;\n            font-size: 28px;\n            font-weight: bold;\n            text-transform: uppercase;\n        }\n        table {\n            background-color: #454545;\n            color: #dedede;\n            font-size: 12px;\n            width: 100%;\n            border: 1px solid #cdcdcd;\n            border-collapse: collapse;\n        }\n        table th, table td {\n            border: 1px solid #797979;\n        }\n        table th {\n            font-size: 14px;\n            font-weight: bold;\n            background-color: #565656;\n            text-align: left;\n            vertical-align: bottom;\n        }\n        table th, table th:first-child, table th:last-child {\n            padding: 4px;\n        }\n        table th small {\n            font-size: 10px;\n            color: #afafaf;\n        }\n        table tr:hover {\n            background-color:#505050;\n        }\n        table tr.marked:nth-child(even) {\n            background-color: #707070;\n        }\n        table tr.marked:nth-child(odd) {\n            background-color: #696969;\n        }\n        table td {\n            padding: 2px;\n        }\n        table td, table td:first-child, table td:last-child {\n            padding: 3px;\n        }\n    </style>\n  `;\n\n  const htmlString = `\n    <html>\n      <head>\n        ${style}\n      </head>\n      <body>\n        <script src=\"https://rawgit.com/components/handlebars.js/master/handlebars.js\"></script>\n        ${templateScript}\n        ${mainScript}\n      </body>\n    </html>\n  `;\n\n  return htmlString;\n};\n\nmodule.exports = visualizeNotes;\n"
  },
  {
    "path": "packages/bruno-tests/collection/package.json",
    "content": "{\n  \"name\": \"@usebruno/test-collection\",\n  \"version\": \"0.0.1\",\n  \"dependencies\": {\n    \"@faker-js/faker\": \"^8.4.0\",\n    \"ajv\": \"~8.17.1\",\n    \"external-lib-with-bru-req-res-objects\": \"file:../external-lib-with-bru-req-res-objects\",\n    \"jose\": \"^5.2.0\",\n    \"jsonwebtoken\": \"^9.0.3\",\n    \"lru-map-cache\": \"^0.1.0\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/ping.bru",
    "content": "meta {\n  name: ping\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  bru.runner.stopExecution();\n}"
  },
  {
    "path": "packages/bruno-tests/collection/preview/html/bruno.bru",
    "content": "meta {\n  name: bruno\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://www.usebruno.com\n  body: none\n  auth: none\n}\n\nassert {\n  res.status: eq 200\n}\n\ntests {\n  test(\"should return parsed xml\", function() {\n    const headers = res.getHeaders();\n    expect(headers['content-type']).to.eql(\"text/html; charset=utf-8\");\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/preview/image/bruno.bru",
    "content": "meta {\n  name: bruno\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://www.usebruno.com/favicon.ico\n  body: none\n  auth: none\n}\n\ntests {\n  test(\"should return parsed xml\", function() {\n    const headers = res.getHeaders();\n    expect(headers['content-type']).to.eql(\"image/x-icon\");\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/readme.md",
    "content": "# bruno-tests collection\n\nAPI Collection to run sanity tests on Bruno CLI.\n"
  },
  {
    "path": "packages/bruno-tests/collection/redirects/Disable Redirect.bru",
    "content": "meta {\n  name: Disable Redirect\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/redirect-to-ping\n  body: none\n  auth: none\n}\n\nassert {\n  res.status: 302\n}\n\nscript:pre-request {\n  req.setMaxRedirects(0);\n}\n\ntests {\n  test(\"should disable redirect to ping\", function() {\n    const data = res.getBody();\n    expect(data).to.equal('Found. Redirecting to /ping');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/redirects/Test Multipart Redirect Consumed FormData.bru",
    "content": "meta {\n  name: Test Multipart Redirect Consumed FormData\n  type: http\n  seq: 7\n}\n\npost {\n  url: {{localhost}}/api/redirect/multipart-redirect-source\n  body: multipartForm\n  auth: none\n}\n\nbody:multipart-form {\n  consumed-field: consumed-value\n}\n\nassert {\n  res.status: 200\n}\n\ntests {\n  test(\"should handle consumed FormData recreation during 308 redirect\", function() {\n    const data = res.getBody();\n    expect(data).to.be.an('object');\n    expect(data.status).to.equal('success');\n    expect(data.method).to.equal('POST');\n  });\n  \n  test(\"should preserve POST method when FormData is consumed and recreated\", function() {\n    const data = res.getBody();\n    expect(data.method).to.equal('POST');\n  });\n  \n  test(\"should receive form data after FormData recreation\", function() {\n    const data = res.getBody();\n    expect(data.body).to.have.property('consumed-field');\n    expect(data.body['consumed-field']).to.equal('consumed-value');\n  });\n  \n  test(\"should maintain proper content-type after FormData recreation\", function() {\n    const data = res.getBody();\n    expect(data.headers).to.have.property('content-type');\n    expect(data.headers['content-type']).to.include('multipart/form-data');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/redirects/Test Multipart Redirect Multiple Fields.bru",
    "content": "meta {\n  name: Test Multipart Redirect Multiple Fields\n  type: http\n  seq: 5\n}\n\npost {\n  url: {{localhost}}/api/redirect/multipart-redirect-source\n  body: multipartForm\n  auth: none\n}\n\nbody:multipart-form {\n  field1: value1\n  field2: value2\n  field3: value3\n}\n\nassert {\n  res.status: 200\n}\n\ntests {\n  test(\"should successfully redirect complex multipart form data with 308\", function() {\n    const data = res.getBody();\n    expect(data).to.be.an('object');\n    expect(data.status).to.equal('success');\n    expect(data.method).to.equal('POST');\n  });\n  \n  test(\"should preserve POST method during redirect\", function() {\n    const data = res.getBody();\n    expect(data.method).to.equal('POST');\n  });\n  \n  test(\"should receive all text fields at target endpoint\", function() {\n    const data = res.getBody();\n    expect(data.body).to.have.property('field1');\n    expect(data.body).to.have.property('field2');\n    expect(data.body).to.have.property('field3');\n  });\n  \n  test(\"should maintain content-type header during redirect\", function() {\n    const data = res.getBody();\n    expect(data.headers).to.have.property('content-type');\n    expect(data.headers['content-type']).to.include('multipart/form-data');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/redirects/Test Multipart Redirect.bru",
    "content": "meta {\n  name: Test Multipart Redirect\n  type: http\n  seq: 3\n}\n\npost {\n  url: {{localhost}}/api/redirect/multipart-redirect-source\n  body: multipartForm\n  auth: none\n}\n\nbody:multipart-form {\n  test-field: test-value\n}\n\nassert {\n  res.status: 200\n}\n\ntests {\n  test(\"should successfully redirect multipart form data with 308\", function() {\n    const data = res.getBody();\n    expect(data).to.be.an('object');\n    expect(data.status).to.equal('success');\n    expect(data.method).to.equal('POST');\n    expect(data.body).to.be.an('object');\n    expect(data.body['test-field']).to.equal('test-value');\n  });\n  \n  test(\"should preserve POST method during redirect\", function() {\n    const data = res.getBody();\n    expect(data.method).to.equal('POST');\n  });\n  \n  test(\"should receive form data at target endpoint\", function() {\n    const data = res.getBody();\n    expect(data.body).to.have.property('test-field');\n    expect(data.body['test-field']).to.equal('test-value');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/redirects/Test Redirect.bru",
    "content": "meta {\n  name: Test Redirect\n  type: http\n  seq: 2\n}\n\nget {\n  url: {{host}}/redirect-to-ping\n  body: none\n  auth: none\n}\n\nassert {\n  res.status: 200\n  res.body: pong\n}\n\ntests {\n  test(\"should redirect to ping\", function() {\n    const data = res.getBody();\n    expect(data).to.equal('pong');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/request-setting/folder.bru",
    "content": "meta {\n  name: request-setting\n  seq: 14\n}\n\nauth {\n  mode: inherit\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/request-setting/follow-redirect.bru",
    "content": "meta {\n  name: follow-redirect\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{localhost}}/api/redirect/3\n  body: none\n  auth: inherit\n}\n\nscript:post-response {\n  test(\"body should include redirecting\", function() {\n    const data = res.getBody();\n    expect(data).to.include(\"Redirecting...\");\n  });\n}\n\nsettings {\n  encodeUrl: true\n  followRedirects: false\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/request-setting/max-redirect.bru",
    "content": "meta {\n  name: max-redirect\n  type: http\n  seq: 2\n}\n\nget {\n  url: {{localhost}}/api/redirect/3\n  body: none\n  auth: inherit\n}\n\nscript:post-response {\n  test(\"body should include redirecting\", function() {\n    const data = res.status;\n    expect(data).to.be.equal(200)\n  });\n}\n\nsettings {\n  encodeUrl: true\n  followRedirects: true\n  maxRedirects: 5\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/response-parsing/test JSON false response.bru",
    "content": "meta {\n  name: test JSON false response\n  type: http\n  seq: 11\n}\n\npost {\n  url: {{httpfaker}}/api/echo/custom\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"headers\": { \"content-type\": \"application/json\" },\n    \"content\": \"false\"\n  }\n}\n\nassert {\n  res.body: eq false\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/response-parsing/test JSON null response.bru",
    "content": "meta {\n  name: test JSON null response\n  type: http\n  seq: 6\n}\n\npost {\n  url: {{httpfaker}}/api/echo/custom\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"headers\": { \"content-type\": \"application/json\" },\n    \"content\": \"null\"\n  }\n}\n\nassert {\n  res.body: eq null\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/response-parsing/test JSON number response.bru",
    "content": "meta {\n  name: test JSON number response\n  type: http\n  seq: 12\n}\n\npost {\n  url: {{httpfaker}}/api/echo/custom\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"headers\": { \"content-type\": \"application/json\" },\n    \"content\": \"3.1\"\n  }\n}\n\nassert {\n  res.body: eq 3.1\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/response-parsing/test JSON response.bru",
    "content": "meta {\n  name: test JSON response\n  type: http\n  seq: 2\n}\n\npost {\n  url: {{httpfaker}}/api/echo/custom\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"headers\": { \"content-type\": \"application/json\" },\n    \"contentJSON\": { \"message\": \"hello\" }\n  }\n}\n\nassert {\n  res.body.message: eq hello\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/response-parsing/test JSON string response.bru",
    "content": "meta {\n  name: test JSON string response\n  type: http\n  seq: 7\n}\n\npost {\n  url: {{httpfaker}}/api/echo/custom\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"headers\": { \"content-type\": \"application/json\" },\n    \"content\": \"\\\"ok\\\"\"\n  }\n}\n\nassert {\n  res.body: eq ok\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/response-parsing/test JSON string with quotes response.bru",
    "content": "meta {\n  name: test JSON string with quotes response\n  type: http\n  seq: 8\n}\n\npost {\n  url: {{httpfaker}}/api/echo/custom\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"headers\": { \"content-type\": \"application/json\" },\n    \"contentJSON\": \"\\\"ok\\\"\"\n  }\n}\n\nassert {\n  res.body: eq '\"ok\"'\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/response-parsing/test JSON true response.bru",
    "content": "meta {\n  name: test JSON true response\n  type: http\n  seq: 10\n}\n\npost {\n  url: {{httpfaker}}/api/echo/custom\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"headers\": { \"content-type\": \"application/json\" },\n    \"content\": \"true\"\n  }\n}\n\nassert {\n  res.body: eq true\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/response-parsing/test JSON unsafe-int response.bru",
    "content": "meta {\n  name: test JSON unsafe-int response\n  type: http\n  seq: 13\n}\n\npost {\n  url: {{httpfaker}}/api/echo/custom\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"headers\": { \"content-type\": \"application/json\" },\n    \"content\": \"90071992547409919876\"\n  }\n}\n\nassert {\n  res.body.toString(): eq 90071992547409920000\n}\n\ndocs {\n  Note: This test is not perfect, we should match the unparsed raw-response with the expected string version of the unsafe-integer\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/response-parsing/test binary response.bru",
    "content": "meta {\n  name: test binary response\n  type: http\n  seq: 4\n}\n\npost {\n  url: {{httpfaker}}/api/echo/custom\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"type\": \"application/octet-stream\",\n    \"contentBase64\": \"+Z1P82iH1wmbILfvnhvjQVbVAktP4TzltpxYD74zNyA=\"\n  }\n}\n\ntests {\n  test(\"response matches the expectation after utf-8 decoding(needs improvement)\", function () {\n    expect(res.getStatus()).to.equal(200);\n    const dataBinary = Buffer.from(\"+Z1P82iH1wmbILfvnhvjQVbVAktP4TzltpxYD74zNyA=\", \"base64\"); \n    expect(res.body).to.equal(dataBinary.toString(\"utf-8\"));\n  });\n}\n\ndocs {\n  Note:\n  \n  This test is not perfect and needs to be improved by direclty matching expected binary data with raw-response.\n  \n  Currently res.body is decoded with `utf-8` by default and looses data in the process. We need some property in `res` which gives access to raw-data/Buffer.\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/response-parsing/test html response.bru",
    "content": "meta {\n  name: test html response\n  type: http\n  seq: 5\n}\n\npost {\n  url: {{httpfaker}}/api/echo/custom\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"headers\": { \"content-type\": \"text/html\" },\n    \"content\": \"<h1>hello</h1>\"\n  }\n}\n\nassert {\n  res.body: eq <h1>hello</h1>\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/response-parsing/test image response.bru",
    "content": "meta {\n  name: test image response\n  type: http\n  seq: 3\n}\n\npost {\n  url: {{httpfaker}}/api/echo/custom\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"type\": \"image/png\",\n    \"contentBase64\": \"iVBORw0KGgoAAAANSUhEUgAAAGQAAABkAQMAAABKLAcXAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAGUExURQCqAP///59OGOoAAAABYktHRAH/Ai3eAAAAB3RJTUUH6QMHCwUNKHvFmgAAABRJREFUOMtjYBgFo2AUjIJRQE8AAAV4AAEpcbn8AAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDI1LTAzLTA3VDExOjA1OjEzKzAwOjAwQkgGWgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyNS0wMy0wN1QxMTowNToxMyswMDowMDMVvuYAAAAodEVYdGRhdGU6dGltZXN0YW1wADIwMjUtMDMtMDdUMTE6MDU6MTMrMDA6MDBkAJ85AAAAAElFTkSuQmCC\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/response-parsing/test invalid JSON response with formatting.bru",
    "content": "meta {\n  name: test invalid JSON response with formatting\n  type: http\n  seq: 19\n}\n\npost {\n  url: {{httpfaker}}/api/echo/custom\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"headers\": { \"content-type\": \"application/json\" },\n    \"content\": \"hello\\n\\tworld\"\n  }\n}\n\nassert {\n  res.body: eq hello\\n\\tworld\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/response-parsing/test plain text response with formatting.bru",
    "content": "meta {\n  name: test plain text response with formatting\n  type: http\n  seq: 18\n}\n\npost {\n  url: {{httpfaker}}/api/echo/custom\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"headers\": { \"content-type\": \"text/plain\" },\n    \"content\": \"hello\\n\\tworld\"\n  }\n}\n\nassert {\n  res.body: eq hello\\n\\tworld\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/response-parsing/test plain text response.bru",
    "content": "meta {\n  name: test plain text response\n  type: http\n  seq: 1\n}\n\npost {\n  url: {{httpfaker}}/api/echo/custom\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"headers\": { \"content-type\": \"text/plain\" },\n    \"content\": \"hello\"\n  }\n}\n\nassert {\n  res.body: eq hello\n}\n\n"
  },
  {
    "path": "packages/bruno-tests/collection/response-parsing/test plain text utf16 response.bru",
    "content": "meta {\n  name: test plain text utf16 response\n  type: http\n  seq: 14\n}\n\npost {\n  url: {{httpfaker}}/api/echo/custom\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"headers\": { \"content-type\": \"text/plain; charset=utf-16\" },\n    \"contentBase64\": \"dABoAGkAcwAgAGkAcwAgAGUAbgBjAG8AZABlAGQAIAB3AGkAdABoACAAdQB0AGYAMQA2AA==\"\n  }\n}\n\nassert {\n  res.body: eq \"this is encoded with utf16\"\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/response-parsing/test plain text utf16-be with BOM response.bru",
    "content": "meta {\n  name: test plain text utf16-be with BOM response\n  type: http\n  seq: 15\n}\n\npost {\n  url: {{httpfaker}}/api/echo/custom\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"headers\": { \"content-type\": \"text/plain; charset=utf-16\" },\n    \"contentBase64\": \"/v8AdABoAGkAcwAgAGkAcwAgAGUAbgBjAG8AZABlAGQAIAB3AGkAdABoACAAdQB0AGYAMQA2AC0AYgBlACAAdwBpAHQAaAAgAEIATwBN\"\n  }\n}\n\nassert {\n  res.body: eq \"this is encoded with utf16-be with BOM\"\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/response-parsing/test plain text utf16-le with BOM response.bru",
    "content": "meta {\n  name: test plain text utf16-le with BOM response\n  type: http\n  seq: 16\n}\n\npost {\n  url: {{httpfaker}}/api/echo/custom\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"headers\": { \"content-type\": \"text/plain; charset=utf-16\" },\n    \"contentBase64\": \"//50AGgAaQBzACAAaQBzACAAZQBuAGMAbwBkAGUAZAAgAHcAaQB0AGgAIAB1AHQAZgAxADYALQBsAGUAIAB3AGkAdABoACAAQgBPAE0A\"\n  }\n}\n\nassert {\n  res.body: eq \"this is encoded with utf16-le with BOM\"\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/response-parsing/test plain text utf8 with BOM response.bru",
    "content": "meta {\n  name: test plain text utf8 with BOM response\n  type: http\n  seq: 17\n}\n\npost {\n  url: {{httpfaker}}/api/echo/custom\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"headers\": { \"content-type\": \"text/plain; charset=utf8\" },\n    \"contentBase64\": \"77u/dGhpcyBpcyB1dGY4IGVuY29kZWQgd2l0aCBCT00sIHdoeSBub3Q/\"\n  }\n}\n\nassert {\n  res.body: eq \"this is utf8 encoded with BOM, why not?\"\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/response-parsing/test xml response.bru",
    "content": "meta {\n  name: test xml response\n  type: http\n  seq: 9\n}\n\npost {\n  url: {{httpfaker}}/api/echo/custom\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"headers\": { \"content-type\": \"application/xml\" },\n    \"content\": \"<message>hello</message>\"\n  }\n}\n\nassert {\n  res.body: eq <message>hello</message>\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/cookies/Redirect Cookie Save.bru",
    "content": "meta {\n  name: Redirect Cookie Save\n  type: http\n  seq: 9\n}\n\nget {\n  url: http://localhost:8081/api/mix?s=302&c=foo:bar&r=http://127.0.0.1:8081/query\n  body: none\n  auth: inherit\n}\n\nparams:query {\n  s: 302\n  c: foo:bar\n  r: http://127.0.0.1:8081/query\n}\n\ntests {\n  const jar = bru.cookies.jar()\n  \n  const cookieData = await jar.getCookie(\n    \"http://localhost:8081\",\n    \"foo\"\n  );\n  \n  test(\"should store redirect cookie under initial request domain\", function () {\n    expect(cookieData).to.not.be.undefined;\n    expect(cookieData.key).to.equal(\"foo\");\n    expect(cookieData.value).to.equal(\"bar\");\n  });\n  \n  jar.clear();\n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/cookies/clear.bru",
    "content": "meta {\n  name: clear\n  type: http\n  seq: 6\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: inherit\n}\n\nscript:pre-request {\n  const jar = bru.cookies.jar()\n  \n  await jar.setCookies('https://testbench-sanity.usebruno.com', [\n    {\n      key: 'test_cookie_1',\n      value: 'value1',\n      path: '/',\n      secure: true\n    },\n    {\n      key: 'test_cookie_2', \n      value: 'value2',\n      path: '/',\n      secure: true\n    }\n  ]);\n  \n  console.log(\"Test cookies set up for clear test\");\n}\n\nscript:post-response {\n  const jar = bru.cookies.jar()\n  \n  const cookiesBeforeClear = await jar.getCookies('https://testbench-sanity.usebruno.com');\n  console.log(`Found ${cookiesBeforeClear.length} cookies before clearing`);\n  \n  test(\"cookies should exist before clearing\", function() {\n    expect(cookiesBeforeClear).to.be.an('array');\n    expect(cookiesBeforeClear.length).to.be.greaterThan(0);\n  });\n  \n  await jar.clear();\n  console.log(\"Cookie jar cleared\");\n}\n\ntests {\n  const jar = bru.cookies.jar()\n  \n  test(\"should have no cookies after clearing\", async function() {\n    const cookiesAfterClear = await jar.getCookies('https://testbench-sanity.usebruno.com');\n    expect(cookiesAfterClear).to.be.an('array');\n    expect(cookiesAfterClear.length).to.equal(0);\n  });\n  \n  jar.clear(function(error) {\n    test(\"should successfully clear with callback\", function() {\n      expect(error).to.be.null;\n    });\n  });\n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/cookies/deleteCookie.bru",
    "content": "meta {\n  name: deleteCookie\n  type: http\n  seq: 5\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: inherit\n}\n\nscript:pre-request {\n  const jar = bru.cookies.jar()\n  \n  await jar.setCookies('https://testbench-sanity.usebruno.com', [\n    {\n      key: 'cookie_to_delete',\n      value: 'will_be_deleted',\n      path: '/',\n      secure: true\n    },\n    {\n      key: 'cookie_to_keep', \n      value: 'should_remain',\n      path: '/',\n      secure: true\n    }\n  ]);\n  \n  console.log(\"Test cookies set up\");\n}\n\nscript:post-response {\n  const jar = bru.cookies.jar()\n  \n  const cookiesBefore = await jar.getCookies('https://testbench-sanity.usebruno.com');\n  console.log(`Found ${cookiesBefore.length} cookies before deletion`);\n  \n  const targetCookie = await jar.getCookie('https://testbench-sanity.usebruno.com', 'cookie_to_delete');\n  test(\"cookie should exist before deletion\", function() {\n    expect(targetCookie).to.not.be.null;\n    expect(targetCookie.key).to.equal('cookie_to_delete');\n  });\n  \n  await jar.deleteCookie('https://testbench-sanity.usebruno.com', 'cookie_to_delete');\n  console.log(\"Cookie deleted\");\n}\n\ntests {\n  const jar = bru.cookies.jar()\n  \n  test(\"should have deleted the target cookie\", async function() {\n    const deletedCookie = await jar.getCookie('https://testbench-sanity.usebruno.com', 'cookie_to_delete');\n    expect(deletedCookie).to.be.null;\n  });\n  \n  test(\"should keep other cookies intact\", async function() {\n    const cookieToKeep = await jar.getCookie('https://testbench-sanity.usebruno.com', 'cookie_to_keep');\n    expect(cookieToKeep).to.not.be.null;\n    expect(cookieToKeep.key).to.equal('cookie_to_keep');\n  });\n  \n  jar.deleteCookie(\"https://testbench-sanity.usebruno.com\", \"cookie_to_keep\", function(error) {\n    test(\"should successfully delete with callback\", function() {\n      expect(error).to.be.null;\n    });\n  });\n  \n  jar.clear()\n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/cookies/deleteCookies.bru",
    "content": "meta {\n  name: deleteCookies\n  type: http\n  seq: 7\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: inherit\n}\n\nscript:pre-request {\n  const jar = bru.cookies.jar()\n  \n  // Set up test cookies before the request\n  try {\n    await jar.setCookies('https://testbench-sanity.usebruno.com', [\n      {\n        key: 'test_cookie_1',\n        value: 'value1',\n        path: '/',\n        httpOnly: false,\n        secure: true\n      },\n      {\n        key: 'test_cookie_2', \n        value: 'value2',\n        path: '/',\n        httpOnly: true,\n        secure: true\n      },\n      {\n        key: 'test_cookie_3',\n        value: 'value3',\n        path: '/api',\n        httpOnly: false,\n        secure: true\n      }\n    ]);\n    \n    console.log(\"Test cookies set up successfully in pre-request script\");\n    \n    // Verify cookies were set\n    const cookies = await jar.getCookies('https://testbench-sanity.usebruno.com');\n    console.log(`${cookies.length} cookies set for domain`);\n    \n  } catch (error) {\n    console.error(\"Failed to set up test cookies:\", error);\n    throw new Error(`Pre-request cookie setup failed: ${error.message || error}`);\n  }\n}\n\nscript:post-response {\n  const jar = bru.cookies.jar()\n  \n  // Verify cookies exist before deletion\n  try {\n    const cookiesBeforeDeletion = await jar.getCookies('https://testbench-sanity.usebruno.com');\n  \n    test(\"cookies should exist before clearing\", function() {\n    expect(cookiesBeforeDeletion).to.be.an('array');\n    expect(cookiesBeforeDeletion.length).to.be.greaterThan(0);\n  });\n    \n    \n    if (cookiesBeforeDeletion.length === 0) {\n      throw new Error(\"No cookies found to delete - setup may have failed\");\n    }\n    \n    // Delete all cookies for the domain\n    await jar.deleteCookies('https://testbench-sanity.usebruno.com');\n    console.log(\"deleteCookies operation completed in post-response\");\n    \n    // Verify deletion worked\n    const cookiesAfterDeletion = await jar.getCookies('https://testbench-sanity.usebruno.com');\n    console.log(`Found ${cookiesAfterDeletion.length} cookies after deletion`);\n    \n  } catch (error) {\n    console.error(\"Delete cookies error in post-response:\", error);\n    throw new Error(`Failed to delete cookies in post-response: ${error.message || error}`);\n  }\n}\n\ntests {\n  const jar = bru.cookies.jar()\n  \n  jar.getCookies(\"https://testbench-sanity.usebruno.com\", function(error, remainingCookies) {\n    if(error) {\n      console.error(\"Error checking remaining cookies:\", error)\n      throw new Error(`Failed to get remaining cookies: ${error.message || error}`)\n    }\n    \n    test(\"should have no cookies remaining after deletion\", function() {\n      expect(remainingCookies).to.be.an('array');\n      expect(remainingCookies.length).to.equal(0);\n      console.log(\"✓ Confirmed: no cookies remain for domain after deleteCookies\");\n    });\n  });\n  \n  jar.clear()\n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/cookies/folder.bru",
    "content": "meta {\n  name: cookies\n  seq: 17\n}\n\nauth {\n  mode: inherit\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/cookies/getCookie.bru",
    "content": "meta {\n  name: getCookie\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: inherit\n}\n\nscript:pre-request {\n  const jar = bru.cookies.jar()\n  \n  jar.setCookie(\"https://testbench-sanity.usebruno.com\", \"name\", \"value\")\n}\n\ntests {\n  const jar = bru.cookies.jar()\n  // Await so the callback runs before jar.clear() below; otherwise the test script can finish\n  // before the callback registers/runs the test, causing a flaky failure (e.g. in CI).\n  await jar.getCookie(\"https://testbench-sanity.usebruno.com\", \"name\", function(error, data) {\n    if(error) {\n      console.error(\"Cookie retrieval error:\", error)\n      throw new Error(`Failed to get cookie: ${error.message || error}`)\n    }\n    \n    test(\"should successfully retrieve cookie data\", function() {\n      expect(data).to.have.property('key');\n      expect(data).to.have.property('value');\n      expect(data.key).to.equal(\"name\");\n      expect(data.value).to.be.a('string');\n      expect(data.value).to.not.be.empty;\n      expect(data.domain).to.include('usebruno.com');\n      console.log(\"Retrieved cookie:\", data);\n    });\n  })\n  \n  jar.clear()\n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/cookies/getCookies.bru",
    "content": "meta {\n  name: getCookies\n  type: http\n  seq: 3\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: inherit\n}\n\ntests {\n  const jar = bru.cookies.jar()\n  \n  jar.getCookies(\"https://testbench-sanity.usebruno.com\", function(error, data) {\n    if(error) {\n      console.error(\"Cookies retrieval error:\", error)\n      throw new Error(`Failed to get cookies: ${error.message || error}`)\n    }\n    \n    test(\"should successfully retrieve cookies array\", function() {\n      expect(error).to.be.null;\n      expect(data).to.not.be.null;\n      expect(data).to.be.an('array');\n      console.log(\"Retrieved cookies count:\", data.length);\n    });\n    \n  test(\"should have valid cookie structure in array\", function() {\n        data.forEach((cookie, index) => {\n          expect(cookie).to.have.property('key');\n          expect(cookie).to.have.property('value');\n          expect(cookie.key).to.be.a('string');\n          expect(cookie.value).to.be.a('string');\n          expect(cookie.domain).to.include('usebruno.com');\n          console.log(`Cookie ${index + 1}:`, cookie);\n        });\n      });\n      \n      test(\"should contain expected cookie properties\", function() {\n        const cookieKeys = data.map(cookie => cookie.key);\n        expect(cookieKeys).to.be.an('array');\n        console.log(\"Found cookie keys:\", cookieKeys);\n      });\n  })\n  \n  jar.clear()\n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/cookies/hasCookie.bru",
    "content": "meta {\n  name: hasCookie\n  type: http\n  seq: 10\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: inherit\n}\n\nscript:pre-request {\n  const jar = bru.cookies.jar()\n\n  jar.setCookie(\"https://testbench-sanity.usebruno.com\", \"existing_cookie\", \"some_value\")\n}\n\ntests {\n  const jar = bru.cookies.jar()\n\n  test(\"should return true for a cookie that exists\", async function() {\n    const exists = await jar.hasCookie('https://testbench-sanity.usebruno.com', 'existing_cookie');\n    expect(exists).to.be.true;\n  });\n\n  test(\"should return false for a cookie that does not exist\", async function() {\n    const exists = await jar.hasCookie('https://testbench-sanity.usebruno.com', 'nonexistent_cookie');\n    expect(exists).to.be.false;\n  });\n\n  jar.hasCookie(\"https://testbench-sanity.usebruno.com\", \"existing_cookie\", function(error, exists) {\n    test(\"should work with callback pattern\", function() {\n      expect(error).to.be.null;\n      expect(exists).to.be.true;\n    });\n  });\n\n  jar.clear()\n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/cookies/setCookie.bru",
    "content": "meta {\n  name: setCookie\n  type: http\n  seq: 2\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: inherit\n}\n\nscript:pre-request {\n  const jar = bru.cookies.jar()\n  \n  // Set cookie before the request\n  try {\n    await jar.setCookie(\"https://testbench-sanity.usebruno.com\", {\n      key: \"auth\",\n      value: \"1234\",\n      path: \"/path\"\n    });\n    \n    console.log(\"Cookie set successfully in pre-request script\");\n    \n  } catch (error) {\n    console.error(\"Cookie setting error in pre-request:\", error);\n    throw new Error(`Pre-request setCookie failed: ${error.message || error}`);\n  }\n}\n\ntests {\n  const jar = bru.cookies.jar()\n  \n  test(\"should have set cookie successfully\", function() {\n    console.log(\"Verifying cookie set in pre-request script\");\n  });\n  \n  // Test: Verify the cookie was set by retrieving it\n  const cookieData = await jar.getCookie(\"https://testbench-sanity.usebruno.com/path\", \"auth\");\n  \n  test(\"should retrieve the set cookie with correct properties\", function() {\n      expect(cookieData.key).to.equal(\"auth\");\n      expect(cookieData.value).to.equal(\"1234\");\n      expect(cookieData.path).to.equal(\"/path\");\n      expect(cookieData.domain).to.include('usebruno.com');\n      console.log(\"Retrieved and verified cookie:\", cookieData);\n  });\n  \n  // Test: Additional verification - check all cookies for the domain\n  const allCookies = await jar.getCookies(\"https://testbench-sanity.usebruno.com/path\");\n  \n  test(\"should find the cookie in domain cookie list\", function() {\n    expect(allCookies).to.be.an('array');\n    expect(allCookies.length).to.be.at.least(1);\n    \n    const authCookie = allCookies.find(c => c.key === 'auth');\n    expect(authCookie).to.not.be.undefined;\n    expect(authCookie.value).to.equal(\"1234\");\n    \n    console.log(\"All cookies for domain:\", allCookies.map(c => ({ key: c.key, value: c.value, path: c.path })));\n  });\n  \n  jar.clear()\n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/cookies/setCookieHeader.bru",
    "content": "meta {\n  name: setCookie header inclusion\n  type: http\n  seq: 6\n}\n\npost {\n  url: {{echo-host}}\n  body: none\n  auth: inherit\n}\n\nscript:pre-request {\n  const jar = bru.cookies.jar();\n  \n  // Set a cookie that should be sent with the upcoming request\n  await jar.setCookie('https://echo.usebruno.com', {\n    key: 'auth',\n    value: 'token123',\n    path: '/',\n    secure: false\n  });\n}\n\ntests {\n  const cookieHeader = res.getHeader('cookie');\n  \n  test('should attach auth cookie in request headers', function () {\n    expect(cookieHeader).to.be.a('string');\n    expect(cookieHeader).to.include('auth=token123');\n  });\n  \n  // Clean up the jar so other tests are not affected\n  const jar = bru.cookies.jar();\n  await jar.clear();\n}\n\nsettings {\n  encodeUrl: false\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/cookies/setCookies.bru",
    "content": "meta {\n  name: setCookies\n  type: http\n  seq: 4\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: inherit\n}\n\nscript:pre-request {\n  const jar = bru.cookies.jar()\n  \n  // Set multiple cookies before the request\n  try {\n    await jar.setCookies('https://example.com', [\n      {\n        key: 'auth',\n        value: 'abc123',\n        path: '/path',          \n        httpOnly: true,\n        secure: true,\n        expires: new Date(Date.now() + 24 * 60 * 60 * 1000)\n      },\n      {\n        key: 'session',\n        value: 'xyz789',\n        path: '/foo',          \n        httpOnly: true,\n        secure: true,\n      }\n    ]);\n    \n    console.log(\"Multiple cookies set successfully in pre-request script\");\n    \n  } catch (error) {\n    console.error(\"setCookies operation failed in pre-request:\", error);\n    throw new Error(`Pre-request setCookies failed: ${error.message || error}`);\n  }\n}\n\ntests {\n  const jar = bru.cookies.jar()\n  \n  test(\"should have set multiple cookies successfully\", function() {\n    console.log(\"Verifying cookies set in pre-request script\");\n  });\n  \n  // Test: Verify first cookie was set correctly\n  const authCookie = await jar.getCookie('https://example.com/path', 'auth');\n  \n  test(\"should retrieve first cookie with correct properties\", function() {\n      expect(authCookie.key).to.equal(\"auth\");\n      expect(authCookie.value).to.equal(\"abc123\");\n      expect(authCookie.path).to.equal(\"/path\");\n      expect(authCookie.httpOnly).to.be.true;\n      expect(authCookie.secure).to.be.true;\n      expect(authCookie.domain).to.include('example.com');\n      console.log(\"Auth cookie verified:\", authCookie);\n  });\n  \n  // Test: Verify second cookie was set correctly\n  const sessionCookie = await jar.getCookie('https://example.com/foo', 'session');\n  \n  test(\"should retrieve second cookie with correct properties\", function() {\n    expect(sessionCookie).to.not.be.null;\n    if (sessionCookie) {\n      expect(sessionCookie.key).to.equal(\"session\");\n      expect(sessionCookie.value).to.equal(\"xyz789\");\n      expect(sessionCookie.path).to.equal(\"/foo\");\n      expect(sessionCookie.httpOnly).to.be.true;\n      expect(sessionCookie.secure).to.be.true;\n      expect(sessionCookie.domain).to.include('example.com');\n      console.log(\"Session cookie verified:\", sessionCookie);\n    }\n  });\n  \n  jar.clear()\n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/deleteAllCollectionVars.bru",
    "content": "meta {\n  name: deleteAllCollectionVars\n  type: http\n  seq: 28\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // TODO: skipped because deleteAllCollectionVars does not update the UI\n  bru.runner.skipRequest();\n  return;\n  bru.setCollectionVar(\"testDelAllCollectionA\", \"a\");\n  bru.setCollectionVar(\"testDelAllCollectionB\", \"b\");\n}\n\ntests {\n  const savedCollectionVars = bru.getAllCollectionVars();\n  bru.deleteAllCollectionVars();\n\n  test(\"should delete all collection vars\", function() {\n    const valA = bru.getCollectionVar(\"testDelAllCollectionA\");\n    const valB = bru.getCollectionVar(\"testDelAllCollectionB\");\n    expect(valA).to.be.undefined;\n    expect(valB).to.be.undefined;\n  });\n\n  // Restore collection vars for subsequent requests\n  for (const [key, value] of Object.entries(savedCollectionVars)) {\n    bru.setCollectionVar(key, value);\n  }\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/deleteAllEnvVars.bru",
    "content": "meta {\n  name: deleteAllEnvVars\n  type: http\n  seq: 23\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  bru.setEnvVar(\"testDelAllEnvVar\", \"to-be-deleted\");\n}\n\ntests {\n  const savedEnvVars = bru.getAllEnvVars();\n  bru.deleteAllEnvVars();\n\n  test(\"should delete all env vars\", function() {\n    const val = bru.getEnvVar(\"testDelAllEnvVar\");\n    expect(val).to.be.undefined;\n  });\n\n  test(\"should preserve env name after deleting all vars\", function() {\n    const envName = bru.getEnvName();\n    expect(envName).to.equal(\"Prod\");\n  });\n\n  // Restore env vars for subsequent requests\n  for (const [key, value] of Object.entries(savedEnvVars)) {\n    bru.setEnvVar(key, value);\n  }\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/deleteAllGlobalEnvVars.bru",
    "content": "meta {\n  name: deleteAllGlobalEnvVars\n  type: http\n  seq: 21\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // TODO: skipped because deleteAllGlobalEnvVars does not update the UI\n  bru.runner.skipRequest();\n  return;\n  bru.setGlobalEnvVar(\"testDelAllGlobalA\", \"a\");\n  bru.setGlobalEnvVar(\"testDelAllGlobalB\", \"b\");\n}\n\ntests {\n  const savedGlobalEnvVars = bru.getAllGlobalEnvVars();\n  bru.deleteAllGlobalEnvVars();\n\n  test(\"should delete all global env vars\", function() {\n    const valA = bru.getGlobalEnvVar(\"testDelAllGlobalA\");\n    const valB = bru.getGlobalEnvVar(\"testDelAllGlobalB\");\n    expect(valA).to.be.undefined;\n    expect(valB).to.be.undefined;\n  });\n\n  // Restore global env vars for subsequent requests\n  for (const [key, value] of Object.entries(savedGlobalEnvVars)) {\n    bru.setGlobalEnvVar(key, value);\n  }\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/deleteCollectionVar.bru",
    "content": "meta {\n  name: deleteCollectionVar\n  type: http\n  seq: 27\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // TODO: skipped because deleteCollectionVar does not update the UI\n  bru.runner.skipRequest();\n  return;\n  bru.setCollectionVar(\"testDeleteCollectionVar\", \"to-be-deleted\");\n  bru.deleteCollectionVar(\"testDeleteCollectionVar\");\n}\n\ntests {\n  test(\"should delete collection var\", function() {\n    const val = bru.getCollectionVar(\"testDeleteCollectionVar\");\n    expect(val).to.be.undefined;\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/deleteGlobalEnvVar.bru",
    "content": "meta {\n  name: deleteGlobalEnvVar\n  type: http\n  seq: 19\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // TODO: skipped because deleteGlobalEnvVar does not update the UI\n  bru.runner.skipRequest();\n  return;\n  bru.setGlobalEnvVar(\"testDeleteGlobalEnvVar\", \"to-be-deleted\");\n  bru.deleteGlobalEnvVar(\"testDeleteGlobalEnvVar\");\n}\n\ntests {\n  test(\"should delete global env var\", function() {\n    const val = bru.getGlobalEnvVar(\"testDeleteGlobalEnvVar\");\n    expect(val).to.be.undefined;\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/folder.bru",
    "content": "meta {\n  name: bru\n}\n\nvars:pre-request {\n  folder-var: folder-var-value\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/getAllCollectionVars.bru",
    "content": "meta {\n  name: getAllCollectionVars\n  type: http\n  seq: 29\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // TODO: skipped because getAllCollectionVars does not update the UI\n  bru.runner.skipRequest();\n  return;\n  bru.setCollectionVar(\"testCollectionA\", \"valueA\");\n  bru.setCollectionVar(\"testCollectionB\", \"valueB\");\n}\n\ntests {\n  test(\"should return all collection vars\", function() {\n    const vars = bru.getAllCollectionVars();\n    expect(vars.testCollectionA).to.equal(\"valueA\");\n    expect(vars.testCollectionB).to.equal(\"valueB\");\n  });\n\n  test(\"should return a shallow copy\", function() {\n    const vars = bru.getAllCollectionVars();\n    vars.testCollectionA = \"mutated\";\n    const vars2 = bru.getAllCollectionVars();\n    expect(vars2.testCollectionA).to.equal(\"valueA\");\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/getAllEnvVars.bru",
    "content": "meta {\n  name: getAllEnvVars\n  type: http\n  seq: 22\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\ntests {\n  test(\"should return all env vars including host\", function() {\n    const vars = bru.getAllEnvVars();\n    expect(vars.host).to.be.a(\"string\");\n  });\n\n  test(\"should not include __name__ in result\", function() {\n    const vars = bru.getAllEnvVars();\n    expect(vars.__name__).to.be.undefined;\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/getAllGlobalEnvVars.bru",
    "content": "meta {\n  name: getAllGlobalEnvVars\n  type: http\n  seq: 20\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  bru.setGlobalEnvVar(\"testGlobalA\", \"valueA\");\n  bru.setGlobalEnvVar(\"testGlobalB\", \"valueB\");\n}\n\ntests {\n  test(\"should return all global env vars\", function() {\n    const vars = bru.getAllGlobalEnvVars();\n    expect(vars.testGlobalA).to.equal(\"valueA\");\n    expect(vars.testGlobalB).to.equal(\"valueB\");\n  });\n\n  test(\"should return a shallow copy\", function() {\n    const vars = bru.getAllGlobalEnvVars();\n    vars.testGlobalA = \"mutated\";\n    const vars2 = bru.getAllGlobalEnvVars();\n    expect(vars2.testGlobalA).to.equal(\"valueA\");\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/getAllVars.bru",
    "content": "meta {\n  name: getAllVars\n  type: http\n  seq: 24\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  bru.setVar(\"testGetAllVarsA\", \"alphaValue\");\n  bru.setVar(\"testGetAllVarsB\", \"betaValue\");\n}\n\ntests {\n  test(\"should return all runtime vars\", function() {\n    const vars = bru.getAllVars();\n    expect(vars.testGetAllVarsA).to.equal(\"alphaValue\");\n    expect(vars.testGetAllVarsB).to.equal(\"betaValue\");\n  });\n\n  test(\"should return a shallow copy\", function() {\n    const vars = bru.getAllVars();\n    vars.testGetAllVarsA = \"mutated\";\n    const vars2 = bru.getAllVars();\n    expect(vars2.testGetAllVarsA).to.equal(\"alphaValue\");\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/getCollectionName.bru",
    "content": "meta {\n  name: getCollectionName\n  type: http\n  seq: 13\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: inherit\n}\n\ntests {\n  test(\"Check if collection name is bruno-testbench\", function () {\n      expect(bru.getCollectionName()).to.eql(\"bruno-testbench\");\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/getCollectionVar.bru",
    "content": "meta {\n  name: getCollectionVar\n  type: http\n  seq: 9\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\ntests {\n  test(\"should get collection var in scripts\", function() {\n    const testVar = bru.getCollectionVar(\"collection-var\");\n    expect(testVar).to.equal(\"collection-var-value\");\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/getEnvName.bru",
    "content": "meta {\n  name: getEnvName\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  const envName = bru.getEnvName();\n  bru.setVar(\"testEnvName\", envName);\n}\n\ntests {\n  test(\"should get env name in scripts\", function() {\n    const testEnvName = bru.getVar(\"testEnvName\");\n    expect(testEnvName).to.equal(\"Prod\");\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/getEnvVar.bru",
    "content": "meta {\n  name: getEnvVar\n  type: http\n  seq: 2\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\n\ntests {\n  test(\"should get env var in scripts\", function() {\n    const host = bru.getEnvVar(\"host\")\n    expect(host).to.equal(\"https://testbench-sanity.usebruno.com\");\n  });\n}"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/getFolderVar.bru",
    "content": "meta {\n  name: getFolderVar\n  type: http\n  seq: 8\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\ntests {\n  test(\"should get folder var in scripts\", function() {\n    const testVar = bru.getFolderVar(\"folder-var\");\n    expect(testVar).to.equal(\"folder-var-value\");\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/getProcessEnv.bru",
    "content": "meta {\n  name: getProcessEnv\n  type: http\n  seq: 6\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\n\ntests {\n  test(\"bru.getProcessEnv()\", function() {\n    const v = bru.getProcessEnv(\"PROC_ENV_VAR\");\n    expect(v).to.equal(\"woof\");\n  });\n}"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/getRequestVar.bru",
    "content": "meta {\n  name: getRequestVar\n  type: http\n  seq: 7\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nvars:pre-request {\n  request-var: request-var-value\n}\n\ntests {\n  test(\"should get request var in scripts\", function() {\n    const testVar = bru.getRequestVar(\"request-var\");\n    expect(testVar).to.equal(\"request-var-value\");\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/getVar.bru",
    "content": "meta {\n  name: getVar\n  type: http\n  seq: 5\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\n\ntests {\n  test(\"should get var in scripts\", function() {\n    const testSetVar = bru.getVar(\"testSetVar\");\n    expect(testSetVar).to.equal(\"bruno-test-87267\");\n  });\n}"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/hasCollectionVar.bru",
    "content": "meta {\n  name: hasCollectionVar\n  type: http\n  seq: 26\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\ntests {\n  test(\"should return true for existing collection var\", function() {\n    const exists = bru.hasCollectionVar(\"collection-var\");\n    expect(exists).to.be.true;\n  });\n\n  test(\"should return false for nonexistent collection var\", function() {\n    const exists = bru.hasCollectionVar(\"nonexistent-collection-var\");\n    expect(exists).to.be.false;\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/interpolate.bru",
    "content": "meta {\n  name: interpolate\n  type: http\n  seq: 13\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\ntests {\n  test(\"should interpolate envs\", function() {\n    const interpolated = bru.interpolate(\"url: {{host}}\")\n    expect(interpolated).to.equal(\"url: https://testbench-sanity.usebruno.com\");\n  });\n  \n  test(\"should interpolate random variables\", function() {\n    const a = bru.interpolate(\"{{$randomInt}}\")\n    const b = bru.interpolate(\"{{$randomInt}}\")\n    expect(a).to.not.equal(b)\n  });\n  \n  const randomObj = {\n    host: \"{{host}}\",\n    int: \"{{$randomInt}}\",\n    timestamp: \"{{$timestamp}}\"\n  }\n  \n  test(\"should interpolate objects with vars, random vars\", function() {\n    const objA = bru.interpolate(randomObj)\n    const objB = bru.interpolate(randomObj)\n    \n    expect(objA).to.be.an(\"object\")\n    expect(objB).to.be.an(\"object\")\n    expect(objA).to.not.deep.eql(objB)\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/isSafeMode.bru",
    "content": "meta {\n  name: isSafeMode\n  type: http\n  seq: 18\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: inherit\n}\n\nscript:pre-request {\n   test(\"bru.isSafeMode() returns true in safe mode\", function() {\n      expect(bru.isSafeMode()).to.be.false;\n  });\n}\n\nsettings {\n  encodeUrl: true\n  timeout: 0\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/runRequest-1.bru",
    "content": "meta {\n  name: runRequest-1\n  type: http\n  seq: 10\n}\n\npost {\n  url: {{echo-host}}\n  body: text\n  auth: none\n}\n\nbody:text {\n  bruno\n}\n\nscript:pre-request {\n  // reset values\n  bru.setVar('run-request-runtime-var', null);\n  bru.setEnvVar('run-request-env-var', null);\n  bru.setGlobalEnvVar('run-request-global-env-var', null);\n  \n  // the above vars will be set in the below request\n  const resp = await bru.runRequest('scripting/api/bru/runRequest-2');\n  \n  bru.setVar('run-request-resp', {\n    data: resp?.data,\n    statusText: resp?.statusText,\n    status: resp?.status\n  });\n}\n\ntests {\n  test(\"should get runtime var set in runRequest-2\", function() {\n    const val = bru.getVar(\"run-request-runtime-var\");\n    expect(val).to.equal(\"run-request-runtime-var-value\");\n  });\n  \n  test(\"should get env var set in runRequest-2\", function() {\n    const val = bru.getEnvVar(\"run-request-env-var\");\n    expect(val).to.equal(\"run-request-env-var-value\");\n  });\n  \n  test(\"should get global env var set in runRequest-2\", function() {\n    const val = bru.getGlobalEnvVar(\"run-request-global-env-var\");\n    const executionMode = req.getExecutionMode();\n    if (executionMode == 'runner') {\n      expect(val).to.equal(\"run-request-global-env-var-value\");\n    }\n  });\n  \n  test(\"should get response of runRequest-2\", function() {\n    const val = bru.getVar('run-request-resp');\n    expect(JSON.stringify(val)).to.equal(JSON.stringify({\n        \"data\": \"bruno\",\n        \"statusText\": \"OK\",\n        \"status\": 200\n      }));\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/runRequest-2.bru",
    "content": "meta {\n  name: runRequest-2\n  type: http\n  seq: 11\n}\n\npost {\n  url: {{echo-host}}\n  body: text\n  auth: none\n}\n\nbody:text {\n  bruno\n}\n\nscript:pre-request {\n  bru.setVar('run-request-runtime-var', 'run-request-runtime-var-value');\n  bru.setEnvVar('run-request-env-var', 'run-request-env-var-value');\n  bru.setGlobalEnvVar('run-request-global-env-var', 'run-request-global-env-var-value');\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/runRequest.bru",
    "content": "meta {\n  name: runRequest\n  type: http\n  seq: 2\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nheaders {\n  foo: bar\n}\n\nauth:basic {\n  username: asd\n  password: j\n}\n\nauth:bearer {\n  token: \n}\n\nbody:json {\n  {\n    \"hello\": \"bruno\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\nscript:pre-request {\n  bru.setVar(\"runRequest-ping-res-1\", null);\n  bru.setVar(\"runRequest-ping-res-2\", null);\n  bru.setVar(\"runRequest-ping-res-3\", null);\n  \n  let pingRes = await bru.runRequest('ping');\n  bru.setVar('runRequest-ping-res-1', {\n    data: pingRes?.data,\n    statusText: pingRes?.statusText,\n    status: pingRes?.status\n  });\n}\n\nscript:post-response {\n  let pingRes = await bru.runRequest('ping');\n  bru.setVar('runRequest-ping-res-2', {\n    data: pingRes?.data,\n    statusText: pingRes?.statusText,\n    status: pingRes?.status\n  });\n}\n\ntests {\n  const pingRes = await bru.runRequest('ping');\n  bru.setVar('runRequest-ping-res-3', {\n    data: pingRes?.data,\n    statusText: pingRes?.statusText,\n    status: pingRes?.status\n  });\n  \n  test(\"should run request and return valid response in pre-request script\", function() {\n    const expectedPingRes = {\n      data: \"pong\",\n      statusText: \"OK\",\n      status: 200\n    };\n    const pingRes = bru.getVar('runRequest-ping-res-1');\n    expect(pingRes).to.eql(expectedPingRes);\n  });\n  \n  test(\"should run request and return valid response in post-response script\", function() {\n    const expectedPingRes = {\n      data: \"pong\",\n      statusText: \"OK\",\n      status: 200\n    };\n    const pingRes = bru.getVar('runRequest-ping-res-2');\n    expect(pingRes).to.eql(expectedPingRes);\n  });\n  \n  test(\"should run request and return valid response in tests script\", function() {\n    const expectedPingRes = {\n      data: \"pong\",\n      statusText: \"OK\",\n      status: 200\n    };\n    const pingRes = bru.getVar('runRequest-ping-res-3');\n    expect(pingRes).to.eql(expectedPingRes);\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/runner/1.bru",
    "content": "meta {\n  name: 1\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://echo.usebruno.com\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  bru.setVar('bru-runner-req', 1);\n}\n\nscript:post-response {\n  bru.setVar('bru.runner.skipRequest', true);\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/runner/2.bru",
    "content": "meta {\n  name: 2\n  type: http\n  seq: 2\n}\n\npost {\n  url: https://echo.usebruno.com\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  bru.runner.skipRequest();\n}\n\nscript:post-response {\n  bru.setVar('bru.runner.skipRequest', false);\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/runner/3.bru",
    "content": "meta {\n  name: 3\n  type: http\n  seq: 3\n}\n\npost {\n  url: https://echo.usebruno.com\n  body: none\n  auth: none\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/send-request/folder.bru",
    "content": "meta {\n  name: send-request\n  seq: 16\n}\n\nauth {\n  mode: inherit\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/send-request/get-url-string.bru",
    "content": "meta {\n  name: get-url-string\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://echo.usebruno.com\n  body: none\n  auth: inherit\n}\n\ntests {\n  await test(\"send request with a get url string\", async () => {\n    const res = await bru.sendRequest(\"https://testbench-sanity.usebruno.com/ping\");\n    expect(res.data).to.eql('pong');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/send-request/usage-patterns.bru",
    "content": "meta {\n  name: usage-patterns\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://echo.usebruno.com\n  body: none\n  auth: inherit\n}\n\ntests {\n  // pattern 1: using async/await\n  await test(\"post request with async/await - success case\", async () => {\n    const res = await bru.sendRequest({\n      url: 'https://echo.usebruno.com',\n      method: 'POST',\n      data: 'ping'\n    });\n    expect(res.data).to.eql('ping');\n  });\n  \n  await test(\"post request with async/await - error case\", async () => {\n    try {\n      await bru.sendRequest({\n        url: 'https://echo.usebruno.com/invalid',\n        method: 'POST',\n        data: 'ping'\n      }); \n    }\n    catch(err) {\n      expect(err.status).to.eql(404);\n    }\n  });\n  \n  // pattern 2: using promise (.then/.catch)\n  await test(\"post request with promise chain - success case\", async () => {\n    await bru.sendRequest({\n      url: 'https://echo.usebruno.com',\n      method: 'POST',\n      data: 'ping'\n    })\n    .then(res => {\n      expect(res.data).to.eql('ping');\n    });\n  });\n  \n  await test(\"post request with promise chain - error case\", async () => {\n    await bru.sendRequest({\n      url: 'https://echo.usebruno.com/invalid',\n      method: 'POST',\n      data: 'ping'\n    })\n    .catch(err => {\n      expect(err.status).to.eql(404);\n    });\n  });\n  \n  // pattern 3: using callbacks\n  await test(\"post request with callback - success case\", async () => {\n    await bru.sendRequest({\n      url: 'https://echo.usebruno.com',\n      method: 'POST',\n      data: 'ping'\n    }, function(error, response) {\n      expect(response.data).to.eql('ping');\n    });\n  });\n  \n  await test(\"post request with callback - error case\", async () => {\n    await bru.sendRequest({\n      url: 'https://echo.usebruno.com/invalid',\n      method: 'POST',\n      data: 'ping'\n    }, function(error, response) {\n      expect(error.status).to.eql(404);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/setCollectionVar.bru",
    "content": "meta {\n  name: setCollectionVar\n  type: http\n  seq: 25\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // TODO: skipped because setCollectionVar does not update the UI\n  bru.runner.skipRequest();\n}\n\nscript:post-response {\n  bru.setCollectionVar(\"testSetCollectionVar\", \"collection-test-value\")\n}\n\ntests {\n  test(\"should set collection var in scripts\", function() {\n    const testSetCollectionVar = bru.getCollectionVar(\"testSetCollectionVar\");\n    expect(testSetCollectionVar).to.equal(\"collection-test-value\");\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/setEnvVar.bru",
    "content": "meta {\n  name: setEnvVar\n  type: http\n  seq: 3\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\n\nscript:post-response {\n  bru.setEnvVar(\"testSetEnvVar\", \"bruno-29653\")\n}\n\ntests {\n  test(\"should set env var in scripts\", function() {\n    const testSetEnvVar = bru.getEnvVar(\"testSetEnvVar\")\n    expect(testSetEnvVar).to.equal(\"bruno-29653\");\n  });\n}"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/bru/setVar.bru",
    "content": "meta {\n  name: setVar\n  type: http\n  seq: 4\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:post-response {\n  bru.setVar(\"testSetVar\", \"bruno-test-87267\")\n}\n\ntests {\n  test(\"should get var in scripts\", function() {\n    const testSetVar = bru.getVar(\"testSetVar\");\n    expect(testSetVar).to.equal(\"bruno-test-87267\");\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/deleteHeader.bru",
    "content": "meta {\n  name: deleteHeader\n  type: http\n  seq: 12\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nheaders {\n  bruno: is-awesome\n}\n\nassert {\n  res.status: eq 200\n  res.body: eq pong\n}\n\nscript:pre-request {\n  req.deleteHeader('bruno');\n}\n\ntests {\n  test(\"req.deleteHeader(name)\", function() {\n    const h = req.getHeader('bruno');\n    expect(h).to.be.undefined;\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/deleteHeaders.bru",
    "content": "meta {\n  name: deleteHeaders\n  type: http\n  seq: 13\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nheaders {\n  X-Frame-Options: 1\n  Content-Type: application/json\n}\n\nassert {\n  res.status: eq 200\n  res.body: eq pong\n}\n\nscript:pre-request {\n  req.deleteHeaders(['X-Frame-Options']);\n}\n\ntests {\n  test(\"req.deleteHeaders(names)\", function() {\n    const h = req.getHeaders();\n    expect(h[\"x-frame-options\"]).to.be.undefined;\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/getBody.bru",
    "content": "meta {\n  name: getBody\n  type: http\n  seq: 9\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nauth:basic {\n  username: asd\n  password: j\n}\n\nauth:bearer {\n  token: \n}\n\nbody:json {\n  {\n    \"hello\": \"bruno\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\ntests {\n  test(\"req.getBody()\", function() {\n    const data = res.getBody();\n    expect(data).to.eql({\n      \"hello\": \"bruno\"\n    });\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/getHeader.bru",
    "content": "meta {\n  name: getHeader\n  type: http\n  seq: 5\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nheaders {\n  bruno: is-awesome\n}\n\n\nassert {\n  res.status: eq 200\n  res.body: eq pong\n}\n\ntests {\n  test(\"req.getHeader(name)\", function() {\n    const h = req.getHeader('bruno');\n    expect(h).to.equal(\"is-awesome\");\n  });\n}"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/getHeaders.bru",
    "content": "meta {\n  name: getHeaders\n  type: http\n  seq: 7\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nheaders {\n  bruno: is-awesome\n  della: is-beautiful\n}\n\n\nassert {\n  res.status: eq 200\n  res.body: eq pong\n}\n\ntests {\n  test(\"req.getHeaders()\", function() {\n    const h = req.getHeaders();\n    expect(h.bruno).to.equal(\"is-awesome\");\n    expect(h.della).to.equal(\"is-beautiful\");\n  });\n}"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/getHost.bru",
    "content": "meta {\n  name: getHost\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nassert {\n  res.status: eq 200\n  res.body: eq pong\n}\n\ntests {\n  test(\"req.getHost()\", function() {\n    const host = req.getHost();\n    expect(host).to.equal(\"testbench-sanity.usebruno.com\");\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/getMethod.bru",
    "content": "meta {\n  name: getMethod\n  type: http\n  seq: 3\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\n\nassert {\n  res.status: eq 200\n  res.body: eq pong\n}\n\ntests {\n  test(\"req.getMethod()()\", function() {\n    const method = req.getMethod();\n    expect(method).to.equal(\"GET\");\n  });\n}"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/getName.bru",
    "content": "meta {\n  name: getName\n  type: http\n  seq: 11\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: inherit\n}\n\ntests {\n  test(\"Check if request name is getName\", function () {\n      expect(req.getName()).to.eql(\"getName\");\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/getPath.bru",
    "content": "meta {\n  name: getPath\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/api/users/123\n  body: none\n  auth: none\n}\n\ntests {\n  test(\"req.getPath()\", function() {\n    const path = req.getPath();\n    expect(path).to.equal(\"/api/users/123\");\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/getPathParams.bru",
    "content": "meta {\n  name: getPathParam\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/:pathParam\n  body: none\n  auth: none\n}\n\nparams:path {\n  pathParam: ping\n}\n\ntests {\n  test(\"req.getPathParams()\", function() {\n    const pathParams = req.getPathParams();\n    expect(pathParams[0].name).to.equal('pathParam');\n    expect(pathParams[0].value).to.equal('ping');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/getQueryString.bru",
    "content": "meta {\n  name: getQueryString\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/ping?page=1&limit=10&sort=desc\n  body: none\n  auth: none\n}\n\nparams:query {\n  page: 1\n  limit: 10\n  sort: desc\n}\n\nassert {\n  res.status: eq 200\n  res.body: eq pong\n}\n\ntests {\n  test(\"req.getQueryString()\", function() {\n    const queryString = req.getQueryString();\n    expect(queryString).to.equal(\"page=1&limit=10&sort=desc\");\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/getTags.bru",
    "content": "meta {\n  name: getTags\n  type: http\n  seq: 11\n  tags: [\n    api\n    test\n    tags\n  ]\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // Test getTags() function\n  const tags = req.getTags();\n  bru.setVar('request-tags', tags);\n}\n\ntests {\n  test(\"req.getTags() should return array of tags\", function() {\n    const tags = bru.getVar('request-tags');\n    expect(tags).to.be.an('array');\n    expect(tags).to.include('api');\n    expect(tags).to.include('test');\n    expect(tags).to.include('tags');\n    expect(tags.length).to.be.equal(3);\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/getUrl.bru",
    "content": "meta {\n  name: getUrl\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nassert {\n  res.status: eq 200\n  res.body: eq pong\n}\n\ntests {\n  test(\"req.getUrl()\", function() {\n    const url = req.getUrl();\n    expect(url).to.equal(\"https://testbench-sanity.usebruno.com/ping\");\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/setBody/form-urlencoded/array body.bru",
    "content": "meta {\n  name: array body\n  type: http\n  seq: 8\n}\n\npost {\n  url: {{echo-host}}\n  body: formUrlEncoded\n  auth: inherit\n}\n\nscript:pre-request {\n  req.setBody([\n    {name: \"empty\", value: \"\"},\n    {name: \"null\", value: null},\n    {name: \"undefined\", value: undefined},\n    {name: \"zero\", value: 0},\n    {name: \"false\", value: false},\n    {name: \"\", value: \"empty_key\"},\n    {name: \"key\", value: \"value1\"},\n    {name: \"name\", value: \"bruno\"},\n    {name: \"key\", value: \"value2\"},\n  ]);\n}\n\ntests {\n  test(\"req.setBody() with edge cases - request body\", function() {\n    const data = req.getBody();\n    const expected = [\n      \"empty=\",\n      \"null=\",\n      \"undefined=\",\n      \"zero=0\",\n      \"false=false\",\n      \"=empty_key\",\n      \"key=value1\",\n      \"name=bruno\",\n      \"key=value2\"\n    ].join(\"&\");\n    \n    expect(data).to.eql(expected);\n  });\n  \n  test(\"req.setBody() with edge cases - response body\", function() {\n    const data = res.getBody();\n    const expected = [\n      \"empty=\",\n      \"null=\",\n      \"undefined=\",\n      \"zero=0\",\n      \"false=false\",\n      \"=empty_key\",\n      \"key=value1\",\n      \"name=bruno\",\n      \"key=value2\"\n    ].join(\"&\");\n    \n    expect(data).to.eql(expected);\n  });\n}\n\nsettings {\n  encodeUrl: true\n  timeout: 0\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/setBody/form-urlencoded/content-type via setHeader.bru",
    "content": "meta {\n  name: content-type via setHeader\n  type: http\n  seq: 7\n}\n\npost {\n  url: {{echo-host}}\n  body: none\n  auth: inherit\n}\n\nscript:pre-request {\n  req.setHeader('content-type', 'application/x-www-form-urlencoded');\n  req.setBody([\n    {name: \"key\", value: \"value\"},\n    {name: \"name\", value: \"bruno\"}\n  ]);\n}\n\ntests {\n  test(\"req.setBody() - request body\", function() {\n    const data = req.getBody();\n    expect(data).to.eql(\"key=value&name=bruno\");\n  });\n  \n  test(\"req.setBody() - response body\", function() {\n    const data = res.getBody();\n    expect(data).to.eql(\"key=value&name=bruno\");\n  });\n  \n  test(\"Content-Type header is set correctly\", function() {\n    const contentType = req.getHeader('content-type');\n    expect(contentType).to.eql('application/x-www-form-urlencoded');\n  });\n}\n\nsettings {\n  encodeUrl: true\n  timeout: 0\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/setBody/form-urlencoded/folder.bru",
    "content": "meta {\n  name: form-urlencoded\n  seq: 1\n}\n\nauth {\n  mode: inherit\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/setBody/form-urlencoded/object body.bru",
    "content": "meta {\n  name: object body\n  type: http\n  seq: 1\n}\n\npost {\n  url: {{echo-host}}\n  body: formUrlEncoded\n  auth: inherit\n}\n\nscript:pre-request {\n  req.setBody({\n    \"key\": \"value with spaces\",\n    \"name\": \"bruno\",\n    \"array\": [\"test\", \"value\"],\n  });\n}\n\ntests {\n  // https://github.com/usebruno/bruno/issues/5813\n  test(\"req.setBody() with object - request body\", function() {\n    const data = req.getBody();\n    expect(data).to.eql(\"key=value%20with%20spaces&name=bruno&array=test&array=value\");\n  });\n  \n  test(\"req.setBody() with object - response body\", function() {\n    const data = res.getBody();\n    expect(data).to.eql(\"key=value%20with%20spaces&name=bruno&array=test&array=value\");\n  });\n}\n\nsettings {\n  encodeUrl: true\n  timeout: 0\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/setBody/form-urlencoded/string body.bru",
    "content": "meta {\n  name: string body\n  type: http\n  seq: 3\n}\n\npost {\n  url: {{echo-host}}\n  body: formUrlEncoded\n  auth: inherit\n}\n\nscript:pre-request {\n  req.setBody(\"key=value&name=bruno\");\n}\n\ntests {\n  test(\"req.setBody() with string format - request body\", function() {\n    const data = req.getBody();\n    expect(data).to.eql(\"key=value&name=bruno\");\n  });\n  \n  test(\"req.setBody() with string format - response body\", function() {\n    const data = res.getBody();\n    expect(data).to.eql(\"key=value&name=bruno\");\n  });\n}\n\nsettings {\n  encodeUrl: true\n  timeout: 0\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/setBody.bru",
    "content": "meta {\n  name: setBody\n  type: http\n  seq: 10\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nauth:basic {\n  username: asd\n  password: j\n}\n\nauth:bearer {\n  token: \n}\n\nbody:json {\n  {\n    \"hello\": \"bruno\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\nscript:pre-request {\n  req.setBody({\n    \"bruno\": \"is awesome\"\n  });\n}\n\ntests {\n  test(\"req.setBody()\", function() {\n    const data = res.getBody();\n    expect(data).to.eql({\n      \"bruno\": \"is awesome\"\n    });\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/setHeader.bru",
    "content": "meta {\n  name: setHeader\n  type: http\n  seq: 6\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nheaders {\n  bruno: is-awesome\n}\n\n\nassert {\n  res.status: eq 200\n  res.body: eq pong\n}\n\nscript:pre-request {\n  req.setHeader('bruno', 'is-the-future');\n}\n\ntests {\n  test(\"req.setHeader(name)\", function() {\n    const h = req.getHeader('bruno');\n    expect(h).to.equal(\"is-the-future\");\n  });\n}"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/setHeaders.bru",
    "content": "meta {\n  name: setHeaders\n  type: http\n  seq: 8\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nheaders {\n  bruno: is-awesome\n  della: is-beautiful\n}\n\n\nassert {\n  res.status: eq 200\n  res.body: eq pong\n}\n\nscript:pre-request {\n  req.setHeaders({\n    \"content-type\": \"application/text\",\n    \"transaction-id\": \"foobar\"\n  });\n}\n\ntests {\n  test(\"req.setHeaders()\", function() {\n    const h = req.getHeaders();\n    expect(h['content-type']).to.equal(\"application/text\");\n    expect(h['transaction-id']).to.equal(\"foobar\");\n  });\n}"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/setMethod.bru",
    "content": "meta {\n  name: setMethod\n  type: http\n  seq: 4\n}\n\npost {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\n\nassert {\n  res.status: eq 200\n  res.body: eq pong\n}\n\nscript:pre-request {\n  req.setMethod(\"GET\");\n}\n\ntests {\n  test(\"req.setMethod()()\", function() {\n    const method = req.getMethod();\n    expect(method).to.equal(\"GET\");\n  });\n}"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/req/setUrl.bru",
    "content": "meta {\n  name: setUrl\n  type: http\n  seq: 2\n}\n\nget {\n  url: {{host}}/ping/invalid\n  body: none\n  auth: none\n}\n\n\nassert {\n  res.status: eq 200\n  res.body: eq pong\n}\n\nscript:pre-request {\n  req.setUrl(\"https://testbench-sanity.usebruno.com/ping\");\n}\n\ntests {\n  test(\"req.setUrl()\", function() {\n    const url = req.getUrl();\n    expect(url).to.equal(\"https://testbench-sanity.usebruno.com/ping\");\n  });\n}"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/res/getBody.bru",
    "content": "meta {\n  name: getBody\n  type: http\n  seq: 4\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nauth:basic {\n  username: asd\n  password: j\n}\n\nauth:bearer {\n  token: \n}\n\nbody:json {\n  {\n    \"hello\": \"bruno\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\ntests {\n  test(\"res.getBody()\", function() {\n    const data = res.getBody();\n    expect(data).to.eql({\n      \"hello\": \"bruno\"\n    });\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/res/getHeader.bru",
    "content": "meta {\n  name: getHeader\n  type: http\n  seq: 2\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nauth:basic {\n  username: asd\n  password: j\n}\n\nauth:bearer {\n  token: \n}\n\nbody:json {\n  {\n    \"hello\": \"bruno\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\ntests {\n  test(\"res.getHeader(name)\", function() {\n    const server = res.getHeader('x-powered-by');\n    expect(server).to.eql('Express');\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/res/getHeaders.bru",
    "content": "meta {\n  name: getHeaders\n  type: http\n  seq: 3\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nauth:basic {\n  username: asd\n  password: j\n}\n\nauth:bearer {\n  token: \n}\n\nbody:json {\n  {\n    \"hello\": \"bruno\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\ntests {\n  test(\"res.getHeaders(name)\", function() {\n    const h = res.getHeaders();\n    expect(h['x-powered-by']).to.eql('Express');\n    expect(h['content-length']).to.eql('17');\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/res/getResponseTime.bru",
    "content": "meta {\n  name: getResponseTime\n  type: http\n  seq: 5\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nauth:basic {\n  username: asd\n  password: j\n}\n\nauth:bearer {\n  token: \n}\n\nbody:json {\n  {\n    \"hello\": \"bruno\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\ntests {\n  test(\"res.getResponseTime()\", function() {\n    const responseTime = res.getResponseTime();\n    expect(typeof responseTime).to.eql(\"number\");\n    expect(responseTime > 0).to.be.true;\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/res/getSize.bru",
    "content": "meta {\n  name: getSize\n  type: http\n  seq: 8\n}\n\nget {\n  url: https://www.httpfaker.org/api/random/json?size=1mb\n  body: none\n  auth: inherit\n}\n\nparams:query {\n  size: 1mb\n}\n\nscript:post-response {\n  console.log(res.getSize())\n}\n\ntests {\n  test(\"test total size\", function() {\n    const sizes = res.getSize();\n    expect(sizes.total).to.equal(sizes.header + sizes.body);\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/res/getStatus.bru",
    "content": "meta {\n  name: getStatus\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\n\nassert {\n  res.status: eq 200\n  res.body: eq pong\n}\n\ntests {\n  test(\"res.getStatus()\", function() {\n    const status = res.getStatus()\n    expect(status).to.equal(200);\n  });\n}"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/res/getStatusText.bru",
    "content": "meta {\n  name: getStatusText\n  type: http\n  seq: 6\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nassert {\n  res.statusText: eq OK\n  res.body: eq pong\n}\n\ntests {\n  test(\"res.getStatusText()\", function() {\n    const statusText = res.getStatusText()\n    expect(statusText).to.equal('OK');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/res/getUrl.bru",
    "content": "meta {\n  name: getUrl\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nassert {\n  res.status: eq 200\n  res.body: eq pong\n}\n\ntests {\n  test(\"res.url\", function() {\n    expect(res.url).to.equal(\"https://testbench-sanity.usebruno.com/ping\");\n  });\n  \n  test(\"res.getUrl()\", function() {\n    const url = res.getUrl();\n    expect(url).to.equal(\"https://testbench-sanity.usebruno.com/ping\");\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/res/setBody/array.bru",
    "content": "meta {\n  name: array\n  type: http\n  seq: 6\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"hello\": \"bruno\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\nscript:post-response {\n  const obj = {\n    hello : \"hello from post-res\"\n  }\n  // Safe mode, Dev mode behaves differently, null is getting converted to undefined, although both have null in the response, tests with undefined fails in safe mode, this needs to be investigated,, undefined is not a valid JSON\n  res.setBody([\"hello\",1, null, undefined, true, obj])\n}\n\ntests {\n  test(\"res.setBody(array)\", function() {\n    const body = res.getBody();\n    expect(body.length).to.eql(6);\n    expect(body[0]).to.eql(\"hello\")\n    expect(body[1]).to.eql(1)\n    expect(body[2]).to.be.null\n  // Safe mode, Dev mode behaves differently, null is getting converted to undefined, although both have null in the response, tests with undefined fails in safe mode, this needs to be investigated,, undefined is not a valid JSON\n    expect(body[3]).to.be.undefined;\n    expect(body[4]).to.eql(true)\n    expect(body[5].hello).to.eql(\"hello from post-res\")\n    \n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/res/setBody/boolean.bru",
    "content": "meta {\n  name: boolean\n  type: http\n  seq: 7\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"hello\": \"bruno\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\nscript:post-response {\n  res.setBody(true)\n}\n\ntests {\n  test(\"res.setBody(boolean)\", function() {\n    const body = res.getBody();\n    expect(body).to.be.true;\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/res/setBody/folder.bru",
    "content": "meta {\n  name: setBody\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/res/setBody/isJson after setBody.bru",
    "content": "meta {\n  name: isJson after setBody\n  type: http\n  seq: 2\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"hello\": \"bruno\"\n  }\n}\n\nassert {\n  res.status: eq 200\n  res.body: isJson\n}\n\nscript:post-response {\n  res.setBody({ id: 1, name: \"updated\", nested: { key: \"value\" } });\n}\n\ntests {\n  test(\"res.body should be json after setBody with object\", function() {\n    const body = res.getBody();\n    expect(body).to.be.json;\n    expect(body.id).to.eql(1);\n    expect(body.name).to.eql(\"updated\");\n    expect(body.nested.key).to.eql(\"value\");\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/res/setBody/null.bru",
    "content": "meta {\n  name: null\n  type: http\n  seq: 6\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"hello\": \"bruno\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\nscript:post-response {\n  res.setBody(null)\n}\n\ntests {\n  test(\"res.setBody(null)\", function() {\n    const body = res.getBody();\n    expect(body).to.be.null;\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/res/setBody/number.bru",
    "content": "meta {\n  name: number\n  type: http\n  seq: 3\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"hello\": \"bruno\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\nscript:post-response {\n  res.setBody(2)\n}\n\ntests {\n  test(\"res.setBody(number)\", function() {\n    const body = res.getBody();\n    expect(body).to.eql(2);\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/res/setBody/object.bru",
    "content": "meta {\n  name: object\n  type: http\n  seq: 1\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"hello\": \"bruno\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\nscript:post-response {\n  res.setBody({\n    hello : \"hello from post-res\"\n  })\n}\n\ntests {\n  test(\"res.setBody(object)\", function() {\n    const body = res.getBody();\n    expect(body.hello).to.eql(\"hello from post-res\");\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/res/setBody/string.bru",
    "content": "meta {\n  name: string\n  type: http\n  seq: 4\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"hello\": \"bruno\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\nscript:post-response {\n  res.setBody(\"hello from post-res\")\n}\n\ntests {\n  test(\"res.setBody(string)\", function() {\n    const body = res.getBody();\n    expect(body).to.eql(\"hello from post-res\");\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/api/res/setBody/undefined.bru",
    "content": "meta {\n  name: undefined\n  type: http\n  seq: 7\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"hello\": \"bruno\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\nscript:post-response {\n  // if undefined  is not passed to res.setBody() the test fails in only safe-mode, needs to check, undefined is not a valid JSON\n  // Safe mode, Dev mode behaves differently, null is getting converted to undefined, although both have null in the response, tests with undefined fails in safe mode, this needs to be investigated, undefined is not a valid JSON\n  res.setBody(undefined)\n}\n\ntests {\n  test(\"res.setBody(undefined)\", function() {\n    const body = res.getBody();\n  // Safe mode, Dev mode behaves differently, null is getting converted to undefined, although both have null in the response, tests with undefined fails in safe mode, this needs to be investigated, undefined is not a valid JSON\n    expect(body).to.be.undefined;\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/inbuilt modules/axios/axios-pre-req-script.bru",
    "content": "meta {\n  name: axios-pre-req-script\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  const axios = require(\"axios\");\n  \n  const url = \"https://testbench-sanity.usebruno.com/api/echo/json\";\n  const response = await axios.post(url, {\n    \"hello\": \"bruno\"\n  });\n  \n  req.setHeader('Content-Type', 'application/json');\n  req.setBody(response.data);\n  req.setMethod(\"POST\");\n  req.setUrl(url);\n}\n\ntests {\n  test(\"req.getBody()\", function() {\n    const data = res.getBody();\n    expect(data).to.eql({\n      \"hello\": \"bruno\"\n    });\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/inbuilt modules/cheerio/cheerio.bru",
    "content": "meta {\n  name: cheerio\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://echo.usebruno.com\n  body: text\n  auth: none\n}\n\nbody:text {\n  <h2 class=\"title\">Hello Bruno!</h2>\n}\n\nscript:pre-request {\n  const cheerio = require('cheerio');\n  \n  const $ = cheerio.load('<h2 class=\"title\">Hello world</h2>');\n  \n  $('h2.title').text('Hello pre-request!');\n  $('h2').addClass('welcome');\n  \n  bru.setVar(\"cheerio-test-pre-request\", $.html());\n}\n\nscript:post-response {\n  const cheerio = require('cheerio');\n\n  const $ = cheerio.load('<h2 class=\"title\">Hello world</h2>');\n\n  $('h2.title').text('Hello post-response!');\n  $('h2').addClass('welcome');\n\n  bru.setVar(\"cheerio-test-post-response\", $.html());\n}\n\ntests {\n  const cheerio = require('cheerio');\n  \n  test(\"cheerio html - from pre request script\", function() {\n    const expected = '<html><head></head><body><h2 class=\"title welcome\">Hello pre-request!</h2></body></html>';\n    const html = bru.getVar('cheerio-test-pre-request');\n    expect(html).to.eql(expected);\n  });\n\n  test(\"cheerio html - from post response script\", function() {\n    const expected = '<html><head></head><body><h2 class=\"title welcome\">Hello post-response!</h2></body></html>';\n    const html = bru.getVar('cheerio-test-post-response');\n    expect(html).to.eql(expected);\n  });\n  \n  test(\"cheerio html - from tests\", function() {\n    const expected = '<html><head></head><body><h2 class=\"title\">Hello Bruno!</h2></body></html>';\n    const $ = cheerio.load(res.body);\n    expect($.html()).to.eql(expected);\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/inbuilt modules/crypto-js/crypto-js-pre-request-script.bru",
    "content": "meta {\n  name: crypto-js-pre-request-script\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\ntests {\n  test(\"crypto message\", function() {\n    var CryptoJS = require(\"crypto-js\");\n\n    // Encrypt\n    var ciphertext = CryptoJS.AES.encrypt('my message', 'secret key 123').toString();\n    \n    // Decrypt\n    var bytes  = CryptoJS.AES.decrypt(ciphertext, 'secret key 123');\n    var originalText = bytes.toString(CryptoJS.enc.Utf8);\n    \n    expect(originalText).to.eql('my message');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/inbuilt modules/crypto-utils/getRandomValues.bru",
    "content": "meta {\n  name: getRandomValues\n  type: http\n  seq: 3\n}\n\npost {\n  url: https://echo.usebruno.com\n  body: none\n  auth: inherit\n}\n\nassert {\n  res.status: eq 200\n}\n\ntests {\n  const { getRandomValuesFunction, isUint8Array } = require('./scripting/inbuilt modules/utils.js');\n  \n  // check if Uint8Array work as expected\n  test(\"should get random values\", function() {\n    const uint8Array = new Uint8Array(32).fill(0);\n    const randomValueUint8Array = getRandomValuesFunction(new Uint8Array(uint8Array));\n    \n    const isValueUint8Array = isUint8Array(randomValueUint8Array);\n    expect(isValueUint8Array).to.be.true;\n    \n    const plainArray = Array.from(randomValueUint8Array);\n    expect(plainArray).to.be.of.length(32);\n    \n    const ogPlainArray = Array.from(uint8Array);\n    expect(ogPlainArray).to.not.deep.eql(plainArray);\n  });\n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/inbuilt modules/crypto-utils/randomBytes.bru",
    "content": "meta {\n  name: randomBytes\n  type: http\n  seq: 4\n}\n\npost {\n  url: https://echo.usebruno.com\n  body: none\n  auth: inherit\n}\n\nassert {\n  res.status: eq 200\n}\n\ntests {\n  const { randomBytesFunction, isUint8Array } = require('./scripting/inbuilt modules/utils.js');\n  \n  test(\"should get random byte values\", function() {\n    const randomValueUint8Array = randomBytesFunction(32);\n    \n    const isValueUint8Array = isUint8Array(randomValueUint8Array);\n    expect(isValueUint8Array).to.be.true;\n    \n    const plainArray = Array.from(randomValueUint8Array);\n    expect(plainArray).to.be.of.length(32);\n  });\n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/inbuilt modules/nanoid/nanoid.bru",
    "content": "meta {\n  name: nanoid\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  const { nanoid } = require(\"nanoid\");\n   \n  bru.setVar(\"nanoid-test-id\", nanoid());\n}\n\ntests {\n  test(\"nanoid var\", function() {\n    const id = bru.getVar('nanoid-test-id');\n    let isValidNanoid = /^[a-zA-Z0-9_-]{21}$/.test(id)\n    bru.setVar('nanoid-test-id', null);\n    expect(isValidNanoid).to.eql(true);\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/inbuilt modules/tv4/folder.bru",
    "content": "meta {\n  name: tv4\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/inbuilt modules/tv4/tv4.bru",
    "content": "meta {\n  name: tv4\n  type: http\n  seq: 1\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: inherit\n}\n\nbody:json {\n  {\n    \"name\": \"John\",\n    \"age\": 30\n  }\n}\n\ntests {\n  const tv4 = require(\"tv4\")\n  \n  const schema = {\n    type: 'object',\n    properties: {\n      name: { type: 'string' },\n      age: { type: 'number' }\n    }\n  };\n  \n  let responseData = res.getBody();\n  \n  let isValid = tv4.validate(responseData, schema);\n  \n  test(\"Response body matches expected schema\", function () {\n      expect(isValid, tv4.error ? tv4.error.message : \"\").to.be.true;\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/inbuilt modules/utils.js",
    "content": "const isUint8Array = (val) => {\n  try {\n    // developer mode [node:vm]\n    const util = require('node:util');\n    return util.types.isUint8Array(val);\n  } catch (err) {\n    // node:util not present in safe mode [quickjs]\n    return val instanceof Uint8Array;\n  }\n};\n\nconst getRandomValuesFunction = (typedArray) => {\n  try {\n    // developer mode [node:vm]\n    const crypto = require('node:crypto');\n    return crypto.getRandomValues(typedArray);\n  } catch (err) {\n    // node:crypto not present in safe mode [quickjs] - uses shim\n    return crypto.getRandomValues(typedArray);\n  }\n};\n\nconst randomBytesFunction = (num) => {\n  try {\n    // developer mode [node:vm]\n    const crypto = require('node:crypto');\n    return crypto.randomBytes(num);\n  } catch (err) {\n    // node:crypto not present in safe mode [quickjs] - uses shim\n    return crypto.randomBytes(num);\n  }\n};\n\nmodule.exports = {\n  isUint8Array,\n  getRandomValuesFunction,\n  randomBytesFunction\n};\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/inbuilt modules/uuid/uuid.bru",
    "content": "meta {\n  name: uuid\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  const { v4 } = require(\"uuid\");\n   \n  bru.setVar(\"uuid-test-id\", v4());\n}\n\ntests {\n  test(\"uuid var\", function() {\n    const id = bru.getVar('uuid-test-id');\n    let isValidUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id);\n    bru.setVar('uuid-test-id', null);\n    expect(isValidUuid).to.eql(true);\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/inbuilt modules/xml2js/xml2js.bru",
    "content": "meta {\n  name: xml2js\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  var parseString = require('xml2js').parseString;\n  var xml = \"<root>Hello xml2js - pre request!</root>\"\n  parseString(xml, function (err, result) {\n     bru.setVar(\"xml2js-test-result-pre-request\", result); \n  });\n}\n\nscript:post-response {\n  var parseString = require('xml2js').parseString;\n  var xml = \"<root>Hello xml2js - post response!</root>\"\n  parseString(xml, function (err, result) {\n     bru.setVar(\"xml2js-test-result-post-response\", result);\n  });\n}\n\ntests {\n  var parseString = require('xml2js').parseString;\n  \n  test(\"xml2js parseString in scripts - pre request\", function() {\n    const expected = {\n      root: 'Hello xml2js - pre request!'\n    };\n    const result = bru.getVar('xml2js-test-result-pre-request');\n    expect(result).to.eql(expected);\n  });\n\n  test(\"xml2js parseString in scripts - post response\", function() {\n    const expected = {\n      root: 'Hello xml2js - post response!'\n    };\n    const result = bru.getVar('xml2js-test-result-post-response');\n    expect(result).to.eql(expected);\n  });\n  \n  test(\"xml2js parseString in tests\", async function() {\n    var xml = \"<root>Hello inside test!</root>\"\n    const expected = {\n      root: 'Hello inside test!'\n    };\n    parseString(xml, function (err, result) {\n      expect(result).to.eql(expected);\n    });\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/js/data types - request vars.bru",
    "content": "meta {\n  name: data types - request vars\n  type: http\n  seq: 3\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"boolean\": false,\n    \"number_1\": 1,\n    \"number_2\": 0,\n    \"number_3\": -1,\n    \"string\": \"bruno\",\n    \"array\": [1, 2, 3, 4, 5],\n    \"object\": {\n      \"hello\": \"bruno\"\n    },\n    \"null\": null\n  }\n}\n\nassert {\n  req.body.boolean: isBoolean false\n  req.body.number_1: isNumber 1\n  req.body.undefined: isUndefined undefined\n  req.body.string: isString bruno\n  req.body.null: isNull null\n  req.body.array: isArray\n  req.body.boolean: eq false\n  req.body.number_1: eq 1\n  req.body.undefined: eq undefined\n  req.body.string: eq bruno\n  req.body.null: eq null\n  req.body.number_2: eq 0\n  req.body.number_3: eq -1\n  req.body.number_2: isNumber\n  req.body.number_3: isNumber\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/js/data types.bru",
    "content": "meta {\n  name: data types\n  type: http\n  seq: 2\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"boolean\": false,\n    \"number\": 1,\n    \"string\": \"bruno\",\n    \"array\": [1, 2, 3, 4, 5],\n    \"object\": {\n      \"hello\": \"bruno\"\n    },\n    \"null\": null\n  }\n}\n\nscript:pre-request {\n  const reqBody = req.getBody();\n  \n  bru.setVar(\"dataTypeVarTest\", {\n    ...reqBody,\n    \"undefined\": undefined\n  });\n}\n\ntests {\n  test(\"data types check via bru var\", function() {\n    let v = bru.getVar(\"dataTypeVarTest\");\n    v = {\n      ...v,\n      \"undefined\": undefined\n    };\n    expect(v).to.eql({\n      \"boolean\": false,\n      \"number\": 1,\n      \"string\": \"bruno\",\n      \"array\": [1, 2, 3, 4, 5],\n      \"object\": {\n        \"hello\": \"bruno\"\n      },\n      \"null\": null,\n      \"undefined\": undefined\n    })\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/js/folder-collection script-tests pre.bru",
    "content": "meta {\n  name: folder-collection script-tests pre\n  type: http\n  seq: 4\n}\n\npost {\n  url: {{echo-host}}\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  bru.setVar('should-test-collection-scripts', true);\n  bru.setVar('should-test-folder-scripts', true);\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/js/folder-collection script-tests.bru",
    "content": "meta {\n  name: folder-collection script-tests\n  type: http\n  seq: 5\n}\n\npost {\n  url: {{echo-host}}\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // do not delete - the collection/folder scripts/tests run during this request execution\n}\n\ntests {\n  const collectionHeader = req.getHeader(\"collection-header\");\n  const folderHeader = req.getHeader(\"folder-header\");\n  \n  test(\"should get the header value set at collection level\", function() {\n    expect(collectionHeader).to.equal(\"collection-header-value\");\n  });\n  \n  test(\"should get the header value set at folder level\", function() {\n    expect(folderHeader).to.equal(\"folder-header-value\");\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/js/folder.bru",
    "content": "meta {\n  name: js\n}\n\nheaders {\n  folder-header: folder-header-value\n}\n\nscript:pre-request {\n  // used by `scripting/js/folder-collection script-tests`\n  const shouldTestFolderScripts = bru.getVar('should-test-folder-scripts');\n  if(shouldTestFolderScripts) {\n   bru.setVar('folder-var-set-by-folder-script', 'folder-var-value-set-by-folder-script');\n  }\n}\n\ntests {\n  // used by `scripting/js/folder-collection script-tests`\n  const shouldTestFolderScripts = bru.getVar('should-test-folder-scripts');\n  const folderVar = bru.getVar(\"folder-var-set-by-folder-script\");\n  if (shouldTestFolderScripts && folderVar) {\n    test(\"folder level test - should get the var that was set by the folder script\", function() {\n      expect(folderVar).to.equal(\"folder-var-value-set-by-folder-script\");\n    }); \n    bru.setVar('folder-var-set-by-folder-script', null); \n    bru.setVar('should-test-folder-scripts', null);\n  }\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/js/setTimeout.bru",
    "content": "meta {\n  name: setTimeout\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  bru.setVar(\"test-js-set-timeout\", \"\");\n  await new Promise((resolve, reject) => {\n    setTimeout(() => {\n      bru.setVar(\"test-js-set-timeout\", \"bruno\");\n      resolve();\n    }, 1000);\n  });\n  \n  const v = bru.getVar(\"test-js-set-timeout\");\n  bru.setVar(\"test-js-set-timeout\", v + \"-is-awesome\");\n  \n}\n\ntests {\n  test(\"setTimeout()\", function() {\n    const v = bru.getVar(\"test-js-set-timeout\")\n    expect(v).to.eql(\"bruno-is-awesome\");\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/local modules/additional context root.bru",
    "content": "meta {\n  name: additional context root\n  type: http\n  seq: 4\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"test\": \"additionalContextRoot\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\nscript:pre-request {\n  // Load module from additionalContextRoot using relative path\n  // This tests that modules outside the collection can be loaded when configured in bruno.json\n  // The path \"../additional-context-root-lib\" is allowed because it's listed in additionalContextRoots\n  const additionalLib = require('../additional-context-root-lib');\n\n  // Verify all dependencies loaded correctly\n  const deps = additionalLib.verifyDependencies();\n  bru.setVar('fakerLoaded', deps.fakerLoaded);\n  bru.setVar('localModuleLoaded', deps.localModuleLoaded);\n\n  // Test the utility functions\n  const user = additionalLib.generateUser();\n  bru.setVar('hasFirstName', typeof user.firstName === 'string' && user.firstName.length > 0);\n  bru.setVar('hasLastName', typeof user.lastName === 'string' && user.lastName.length > 0);\n  bru.setVar('hasFullName', typeof user.fullName === 'string' && user.fullName.includes(' '));\n  bru.setVar('hasGreeting', typeof user.greeting === 'string' && user.greeting.startsWith('Hello, '));\n  bru.setVar('hasEmail', typeof user.email === 'string' && user.email.includes('@'));\n\n  // Test direct functions from local module\n  const formatted = additionalLib.formatName('John', 'Doe');\n  bru.setVar('formatNameResult', formatted);\n\n  const greeting = additionalLib.generateGreeting('Bruno');\n  bru.setVar('greetingResult', greeting);\n\n  // Test direct require of a specific file from additionalContextRoot\n  const libDirect = require('../additional-context-root-lib/lib.js');\n  bru.setVar('directRequireWorks', typeof libDirect.formatName === 'function');\n  bru.setVar('directFormatName', libDirect.formatName('Direct', 'Test'));\n  bru.setVar('directGreeting', libDirect.generateGreeting('World'));\n}\n\ntests {\n  test(\"should load module from additionalContextRoot\", function() {\n    expect(bru.getVar('fakerLoaded')).to.equal(true);\n    expect(bru.getVar('localModuleLoaded')).to.equal(true);\n  });\n\n  test(\"should resolve npm module (@faker-js/faker) from collection node_modules\", function() {\n    expect(bru.getVar('hasFirstName')).to.equal(true);\n    expect(bru.getVar('hasLastName')).to.equal(true);\n    expect(bru.getVar('hasEmail')).to.equal(true);\n  });\n\n  test(\"should resolve local module (./lib.js) relative to additionalContextRoot\", function() {\n    expect(bru.getVar('hasFullName')).to.equal(true);\n    expect(bru.getVar('hasGreeting')).to.equal(true);\n  });\n\n  test(\"should correctly execute local module functions\", function() {\n    expect(bru.getVar('formatNameResult')).to.equal('John Doe');\n    expect(bru.getVar('greetingResult')).to.equal('Hello, Bruno!');\n  });\n\n  test(\"should directly require specific file from additionalContextRoot\", function() {\n    expect(bru.getVar('directRequireWorks')).to.equal(true);\n    expect(bru.getVar('directFormatName')).to.equal('Direct Test');\n    expect(bru.getVar('directGreeting')).to.equal('Hello, World!');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/local modules/invalid and valid module imports.bru",
    "content": "meta {\n  name: invalid and valid module imports\n  type: http\n  seq: 3\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nassert {\n  invalid_module_error_thrown: eq true\n  valid_module_no_error: eq true\n}\n\nscript:pre-request {\n  try {\n    bru.setVar('invalid_module_error_thrown', false);\n    // should throw an error\n    const invalid = require(\"./lib/invalid\");\n  }\n  catch(error) {\n    bru.setVar('invalid_module_error_thrown', true);\n  }\n  \n  \n  try {\n    bru.setVar('valid_module_no_error', true);\n    // should not throw an error\n    const math = require(\"./lib/math\");\n  }\n  catch(error) {\n    bru.setVar('valid_module_no_error', false);\n  }\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/local modules/sum (without js extn).bru",
    "content": "meta {\n  name: sum (without js extn)\n  type: http\n  seq: 2\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"a\": 1,\n    \"b\": 2\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\nscript:pre-request {\n  const math = require(\"./lib/math\");\n  console.log(math, 'math');\n  \n  const body = req.getBody();\n  body.sum = math.sum(body.a, body.b);\n  body.areaOfCircle = math.areaOfCircle(2);\n  \n  req.setBody(body);\n}\n\ntests {\n  test(\"should return json\", function() {\n    const data = res.getBody();\n    expect(res.getBody()).to.eql({\n      \"a\": 1,\n      \"b\": 2,\n      \"sum\": 3,\n      \"areaOfCircle\": 12.56\n    });\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/local modules/sum.bru",
    "content": "meta {\n  name: sum\n  type: http\n  seq: 1\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"a\": 1,\n    \"b\": 2\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\nscript:pre-request {\n  const math = require(\"./lib/math.js\");  \n  const body = req.getBody();\n  body.sum = math.sum(body.a, body.b);\n  \n  req.setBody(body);\n}\n\ntests {\n  test(\"should return json\", function() {\n    const data = res.getBody();\n    expect(res.getBody()).to.eql({\n      \"a\": 1,\n      \"b\": 2,\n      \"sum\": 3\n    });\n  });\n  \n  test(\"should return json\", function() {\n    const data = res.getBody();\n    expect(res.getBody()).to.eql({\n      \"a\": 1,\n      \"b\": 2,\n      \"sum\": 3\n    });\n  });\n  \n  test(\"should return json\", function() {\n    const data = res.getBody();\n    expect(res.getBody()).to.eql({\n      \"a\": 1,\n      \"b\": 2,\n      \"sum\": 3\n    });\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/node-builtins/buffer.bru",
    "content": "meta {\n  name: buffer\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // Skip in safe mode - these tests require developer sandbox\n  if (bru.isSafeMode()) {\n    bru.runner.skipRequest();\n    return;\n  }\n}\n\ntests {\n  test(\"Buffer.from and toString\", function() {\n    const buf = Buffer.from('hello bruno', 'utf8');\n    expect(buf.toString()).to.equal('hello bruno');\n    expect(buf.toString('base64')).to.equal('aGVsbG8gYnJ1bm8=');\n    expect(buf.toString('hex')).to.equal('68656c6c6f206272756e6f');\n    expect(buf.length).to.equal(11);\n  });\n\n  test(\"Buffer.from with base64 and hex\", function() {\n    expect(Buffer.from('aGVsbG8=', 'base64').toString()).to.equal('hello');\n    expect(Buffer.from('68656c6c6f', 'hex').toString()).to.equal('hello');\n  });\n\n  test(\"Buffer.alloc\", function() {\n    const buf = Buffer.alloc(10, 0);\n    expect(buf.length).to.equal(10);\n    expect(buf[0]).to.equal(0);\n  });\n\n  test(\"Buffer.concat\", function() {\n    const result = Buffer.concat([Buffer.from('hello '), Buffer.from('world')]);\n    expect(result.toString()).to.equal('hello world');\n  });\n\n  test(\"Buffer.isBuffer\", function() {\n    expect(Buffer.isBuffer(Buffer.from('test'))).to.equal(true);\n    expect(Buffer.isBuffer('string')).to.equal(false);\n    expect(Buffer.isBuffer(new Uint8Array(4))).to.equal(false);\n  });\n\n  test(\"Buffer.subarray\", function() {\n    const buf = Buffer.from('hello bruno');\n    expect(buf.subarray(0, 5).toString()).to.equal('hello');\n    expect(buf.subarray(6).toString()).to.equal('bruno');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/node-builtins/encoding.bru",
    "content": "meta {\n  name: encoding\n  type: http\n  seq: 3\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // Skip in safe mode - these tests require developer sandbox\n  if (bru.isSafeMode()) {\n    bru.runner.skipRequest();\n    return;\n  }\n}\n\ntests {\n  test(\"TextEncoder\", function() {\n    const encoder = new TextEncoder();\n    const encoded = encoder.encode('hello');\n    expect(encoded).to.be.instanceOf(Uint8Array);\n    expect(encoded.length).to.equal(5);\n    expect(encoded[0]).to.equal(104); // 'h'\n  });\n\n  test(\"TextDecoder\", function() {\n    const decoder = new TextDecoder('utf-8');\n    const decoded = decoder.decode(new Uint8Array([104, 101, 108, 108, 111]));\n    expect(decoded).to.equal('hello');\n  });\n\n  test(\"TextDecoder with utf-16le\", function() {\n    const decoder = new TextDecoder('utf-16le');\n    const decoded = decoder.decode(new Uint8Array([104, 0, 105, 0]));\n    expect(decoded).to.equal('hi');\n  });\n\n  test(\"btoa and atob\", function() {\n    expect(btoa('hello bruno')).to.equal('aGVsbG8gYnJ1bm8=');\n    expect(atob('aGVsbG8gYnJ1bm8=')).to.equal('hello bruno');\n  });\n\n  test(\"base64 roundtrip with binary data\", function() {\n    const binary = String.fromCharCode(0, 1, 255, 254);\n    const encoded = btoa(binary);\n    const decoded = atob(encoded);\n    expect(decoded.charCodeAt(0)).to.equal(0);\n    expect(decoded.charCodeAt(2)).to.equal(255);\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/node-builtins/events.bru",
    "content": "meta {\n  name: events\n  type: http\n  seq: 20\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // Skip in safe mode - these tests require developer sandbox\n  if (bru.isSafeMode()) {\n    bru.runner.skipRequest();\n    return;\n  }\n}\n\ntests {\n  test(\"Event, EventTarget, CustomEvent exist\", function() {\n    expect(Event).to.be.a('function');\n    expect(EventTarget).to.be.a('function');\n    expect(CustomEvent).to.be.a('function');\n  });\n\n  test(\"Event properties\", function() {\n    const event = new Event('click', { bubbles: true, cancelable: true });\n    expect(event.type).to.equal('click');\n    expect(event.bubbles).to.equal(true);\n    expect(event.cancelable).to.equal(true);\n  });\n\n  test(\"CustomEvent with detail\", function() {\n    const event = new CustomEvent('custom', { detail: { foo: 'bar' } });\n    expect(event.type).to.equal('custom');\n    expect(event.detail).to.deep.equal({ foo: 'bar' });\n  });\n\n  test(\"EventTarget addEventListener and dispatchEvent\", function() {\n    let eventFired = false;\n    let eventDetail = null;\n\n    const target = new EventTarget();\n    target.addEventListener('test', (e) => {\n      eventFired = true;\n      eventDetail = e.detail;\n    });\n    target.dispatchEvent(new CustomEvent('test', { detail: 'hello' }));\n\n    expect(eventFired).to.equal(true);\n    expect(eventDetail).to.equal('hello');\n  });\n\n  test(\"Multiple event listeners\", function() {\n    let count = 0;\n    const target = new EventTarget();\n    target.addEventListener('inc', () => count++);\n    target.addEventListener('inc', () => count++);\n    target.dispatchEvent(new Event('inc'));\n\n    expect(count).to.equal(2);\n  });\n\n  test(\"removeEventListener\", function() {\n    let removed = true;\n    const target = new EventTarget();\n    const handler = () => { removed = false; };\n    target.addEventListener('test', handler);\n    target.removeEventListener('test', handler);\n    target.dispatchEvent(new Event('test'));\n\n    expect(removed).to.equal(true);\n  });\n\n  test(\"addEventListener with once option\", function() {\n    let onceCount = 0;\n    const target = new EventTarget();\n    target.addEventListener('test', () => onceCount++, { once: true });\n    target.dispatchEvent(new Event('test'));\n    target.dispatchEvent(new Event('test'));\n\n    expect(onceCount).to.equal(1);\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/node-builtins/fetch-api.bru",
    "content": "meta {\n  name: fetch-api\n  type: http\n  seq: 9\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // Skip in safe mode - these tests require developer sandbox\n  if (bru.isSafeMode()) {\n    bru.runner.skipRequest();\n    return;\n  }\n}\n\ntests {\n  test(\"Fetch API globals exist\", function() {\n    expect(fetch).to.be.a('function');\n    expect(Request).to.be.a('function');\n    expect(Response).to.be.a('function');\n    expect(Headers).to.be.a('function');\n  });\n\n  test(\"Headers\", function() {\n    const headers = new Headers();\n    headers.set('Content-Type', 'application/json');\n    headers.append('X-Custom', 'value');\n    expect(headers.get('Content-Type')).to.equal('application/json');\n    expect(headers.has('X-Custom')).to.equal(true);\n    expect(headers.has('Missing')).to.equal(false);\n  });\n\n  test(\"Request\", function() {\n    const req = new Request('https://example.com/api', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' }\n    });\n    expect(req.url).to.equal('https://example.com/api');\n    expect(req.method).to.equal('POST');\n    expect(req.headers.get('Content-Type')).to.equal('application/json');\n  });\n\n  test(\"Response\", function() {\n    const res = new Response('body', { status: 201, statusText: 'Created' });\n    expect(res.status).to.equal(201);\n    expect(res.statusText).to.equal('Created');\n    expect(res.ok).to.equal(true);\n  });\n\n  test(\"Response body methods exist\", function() {\n    const res = new Response('test');\n    expect(res.json).to.be.a('function');\n    expect(res.text).to.be.a('function');\n    expect(res.arrayBuffer).to.be.a('function');\n    expect(res.blob).to.be.a('function');\n  });\n\n  test(\"AbortController\", function() {\n    const controller = new AbortController();\n    expect(controller.signal.aborted).to.equal(false);\n    controller.abort();\n    expect(controller.signal.aborted).to.equal(true);\n  });\n\n  test(\"FormData\", function() {\n    const fd = new FormData();\n    fd.append('field', 'value');\n    expect(fd.get('field')).to.equal('value');\n    expect(fd.has('field')).to.equal(true);\n  });\n\n  test(\"Blob\", function() {\n    const blob = new Blob(['hello'], { type: 'text/plain' });\n    expect(blob.size).to.equal(5);\n    expect(blob.type).to.equal('text/plain');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/node-builtins/intl.bru",
    "content": "meta {\n  name: intl\n  type: http\n  seq: 10\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // Skip in safe mode - these tests require developer sandbox\n  if (bru.isSafeMode()) {\n    bru.runner.skipRequest();\n    return;\n  }\n}\n\ntests {\n  test(\"Intl.DateTimeFormat\", function() {\n    const formatter = new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long', day: 'numeric' });\n    expect(formatter.format(new Date('2024-06-15'))).to.equal('June 15, 2024');\n  });\n\n  test(\"Intl.NumberFormat\", function() {\n    const currency = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });\n    expect(currency.format(1234.56)).to.equal('$1,234.56');\n\n    const percent = new Intl.NumberFormat('en-US', { style: 'percent', minimumFractionDigits: 1 });\n    expect(percent.format(0.456)).to.equal('45.6%');\n  });\n\n  test(\"Intl.Collator\", function() {\n    const collator = new Intl.Collator('en', { sensitivity: 'base' });\n    expect(collator.compare('a', 'A')).to.equal(0);\n    expect(collator.compare('a', 'b')).to.be.lessThan(0);\n  });\n\n  test(\"Intl.PluralRules\", function() {\n    const rules = new Intl.PluralRules('en-US');\n    expect(rules.select(1)).to.equal('one');\n    expect(rules.select(5)).to.equal('other');\n  });\n\n  test(\"Intl.RelativeTimeFormat\", function() {\n    const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });\n    expect(rtf.format(-1, 'day')).to.equal('yesterday');\n    expect(rtf.format(1, 'day')).to.equal('tomorrow');\n  });\n\n  test(\"Intl.ListFormat\", function() {\n    const formatter = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' });\n    expect(formatter.format(['Apple', 'Banana', 'Cherry'])).to.equal('Apple, Banana, and Cherry');\n  });\n\n  test(\"Intl.DisplayNames\", function() {\n    const regions = new Intl.DisplayNames(['en'], { type: 'region' });\n    expect(regions.of('US')).to.equal('United States');\n\n    const languages = new Intl.DisplayNames(['en'], { type: 'language' });\n    expect(languages.of('fr')).to.equal('French');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/node-builtins/json.bru",
    "content": "meta {\n  name: json\n  type: http\n  seq: 19\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // Skip in safe mode - these tests require developer sandbox\n  if (bru.isSafeMode()) {\n    bru.runner.skipRequest();\n    return;\n  }\n}\n\ntests {\n  test(\"JSON.stringify\", function() {\n    expect(JSON.stringify({ a: 1 })).to.equal('{\"a\":1}');\n    expect(JSON.stringify([1, 2, 3])).to.equal('[1,2,3]');\n    expect(JSON.stringify('hello')).to.equal('\"hello\"');\n    expect(JSON.stringify(null)).to.equal('null');\n  });\n\n  test(\"JSON.stringify with replacer\", function() {\n    const obj = { a: 1, b: 2, c: 3 };\n    expect(JSON.stringify(obj, ['a', 'c'])).to.equal('{\"a\":1,\"c\":3}');\n    expect(JSON.stringify(obj, (k, v) => k === 'b' ? undefined : v)).to.equal('{\"a\":1,\"c\":3}');\n  });\n\n  test(\"JSON.stringify with space\", function() {\n    const obj = { a: 1 };\n    expect(JSON.stringify(obj, null, 2)).to.equal('{\\n  \"a\": 1\\n}');\n  });\n\n  test(\"JSON.parse\", function() {\n    expect(JSON.parse('{\"a\":1}')).to.deep.equal({ a: 1 });\n    expect(JSON.parse('[1,2,3]')).to.deep.equal([1, 2, 3]);\n    expect(JSON.parse('\"hello\"')).to.equal('hello');\n    expect(JSON.parse('null')).to.equal(null);\n  });\n\n  test(\"JSON.parse with reviver\", function() {\n    const result = JSON.parse('{\"a\":1,\"b\":2}', (k, v) => typeof v === 'number' ? v * 2 : v);\n    expect(result).to.deep.equal({ a: 2, b: 4 });\n  });\n\n  test(\"JSON roundtrip with complex object\", function() {\n    const obj = {\n      string: 'hello',\n      number: 42,\n      float: 3.14,\n      boolean: true,\n      null: null,\n      array: [1, 2, 3],\n      nested: { a: { b: { c: 'deep' } } }\n    };\n    expect(JSON.parse(JSON.stringify(obj))).to.deep.equal(obj);\n  });\n\n  test(\"JSON.stringify handles special values\", function() {\n    expect(JSON.stringify({ a: undefined })).to.equal('{}');\n    expect(JSON.stringify([undefined])).to.equal('[null]');\n    expect(JSON.stringify({ a: NaN })).to.equal('{\"a\":null}');\n    expect(JSON.stringify({ a: Infinity })).to.equal('{\"a\":null}');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/node-builtins/node-crypto.bru",
    "content": "meta {\n  name: node-crypto\n  type: http\n  seq: 12\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // Skip in safe mode - these tests require developer sandbox\n  if (bru.isSafeMode()) {\n    bru.runner.skipRequest();\n    return;\n  }\n}\n\ntests {\n  const crypto = require('node:crypto');\n\n  test(\"crypto.randomBytes\", function() {\n    const bytes = crypto.randomBytes(16);\n    expect(Buffer.isBuffer(bytes)).to.equal(true);\n    expect(bytes.length).to.equal(16);\n  });\n\n  test(\"crypto.randomUUID\", function() {\n    const uuid = crypto.randomUUID();\n    expect(uuid).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);\n  });\n\n  test(\"crypto.createHash\", function() {\n    const md5 = crypto.createHash('md5').update('hello').digest('hex');\n    expect(md5).to.have.lengthOf(32);\n\n    const sha256 = crypto.createHash('sha256').update('hello').digest('hex');\n    expect(sha256).to.have.lengthOf(64);\n\n    const sha512 = crypto.createHash('sha512').update('hello').digest('hex');\n    expect(sha512).to.have.lengthOf(128);\n  });\n\n  test(\"crypto.createHmac\", function() {\n    const hmac = crypto.createHmac('sha256', 'secret').update('hello').digest('hex');\n    expect(hmac).to.have.lengthOf(64);\n  });\n\n  test(\"crypto.getHashes and crypto.getCiphers\", function() {\n    const hashes = crypto.getHashes();\n    expect(hashes).to.be.an('array').that.includes('sha256');\n\n    const ciphers = crypto.getCiphers();\n    expect(ciphers).to.be.an('array');\n    expect(ciphers.some(c => c.includes('aes'))).to.equal(true);\n  });\n\n  test(\"crypto.pbkdf2Sync\", function() {\n    const key = crypto.pbkdf2Sync('password', 'salt', 1000, 32, 'sha256');\n    expect(key.length).to.equal(32);\n  });\n\n  test(\"crypto.scryptSync\", function() {\n    const key = crypto.scryptSync('password', 'salt', 32);\n    expect(key.length).to.equal(32);\n  });\n\n  test(\"crypto.timingSafeEqual\", function() {\n    const a = Buffer.from('hello');\n    const b = Buffer.from('hello');\n    const c = Buffer.from('world');\n    expect(crypto.timingSafeEqual(a, b)).to.equal(true);\n    expect(crypto.timingSafeEqual(a, c)).to.equal(false);\n  });\n\n  test(\"AES encryption/decryption\", function() {\n    const key = crypto.randomBytes(32);\n    const iv = crypto.randomBytes(16);\n    const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);\n    let encrypted = cipher.update('secret message', 'utf8', 'hex');\n    encrypted += cipher.final('hex');\n\n    const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);\n    let decrypted = decipher.update(encrypted, 'hex', 'utf8');\n    decrypted += decipher.final('utf8');\n\n    expect(decrypted).to.equal('secret message');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/node-builtins/node-fs.bru",
    "content": "meta {\n  name: node-fs\n  type: http\n  seq: 13\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // Skip in safe mode - these tests require developer sandbox\n  if (bru.isSafeMode()) {\n    bru.runner.skipRequest();\n    return;\n  }\n}\n\ntests {\n  const fs = require('node:fs');\n  const path = require('node:path');\n  const os = require('node:os');\n\n  // Setup - create test directory and file\n  const testDir = path.join(os.tmpdir(), 'bruno-fs-test-' + Date.now());\n  const testFile = path.join(testDir, 'test.txt');\n  fs.mkdirSync(testDir, { recursive: true });\n  fs.writeFileSync(testFile, 'Hello Bruno!');\n\n  test(\"fs.existsSync\", function() {\n    expect(fs.existsSync(testDir)).to.equal(true);\n    expect(fs.existsSync(testFile)).to.equal(true);\n    expect(fs.existsSync('/nonexistent')).to.equal(false);\n  });\n\n  test(\"fs.readFileSync\", function() {\n    expect(fs.readFileSync(testFile, 'utf8')).to.equal('Hello Bruno!');\n    expect(Buffer.isBuffer(fs.readFileSync(testFile))).to.equal(true);\n  });\n\n  test(\"fs.appendFileSync\", function() {\n    fs.appendFileSync(testFile, ' Appended.');\n    expect(fs.readFileSync(testFile, 'utf8')).to.equal('Hello Bruno! Appended.');\n  });\n\n  test(\"fs.statSync\", function() {\n    const fileStat = fs.statSync(testFile);\n    expect(fileStat.isFile()).to.equal(true);\n    expect(fileStat.isDirectory()).to.equal(false);\n\n    const dirStat = fs.statSync(testDir);\n    expect(dirStat.isDirectory()).to.equal(true);\n  });\n\n  test(\"fs.readdirSync\", function() {\n    const files = fs.readdirSync(testDir);\n    expect(files).to.include('test.txt');\n  });\n\n  test(\"fs.copyFileSync and fs.renameSync\", function() {\n    const copyPath = path.join(testDir, 'copy.txt');\n    const renamePath = path.join(testDir, 'renamed.txt');\n\n    fs.copyFileSync(testFile, copyPath);\n    expect(fs.existsSync(copyPath)).to.equal(true);\n\n    fs.renameSync(copyPath, renamePath);\n    expect(fs.existsSync(copyPath)).to.equal(false);\n    expect(fs.existsSync(renamePath)).to.equal(true);\n\n    // Cleanup\n    fs.unlinkSync(renamePath);\n  });\n\n  // Cleanup\n  fs.unlinkSync(testFile);\n  fs.rmdirSync(testDir);\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/node-builtins/node-os.bru",
    "content": "meta {\n  name: node-os\n  type: http\n  seq: 14\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // Skip in safe mode - these tests require developer sandbox\n  if (bru.isSafeMode()) {\n    bru.runner.skipRequest();\n    return;\n  }\n}\n\ntests {\n  const os = require('node:os');\n\n  test(\"os.platform and os.arch\", function() {\n    expect(['darwin', 'linux', 'win32']).to.include(os.platform());\n    expect(['x64', 'arm64', 'arm', 'ia32']).to.include(os.arch());\n  });\n\n  test(\"os.type and os.release\", function() {\n    expect(os.type()).to.be.a('string');\n    expect(os.release()).to.be.a('string');\n  });\n\n  test(\"os.hostname, os.homedir, os.tmpdir\", function() {\n    expect(os.hostname()).to.be.a('string');\n    expect(os.homedir()).to.be.a('string').with.length.greaterThan(0);\n    expect(os.tmpdir()).to.be.a('string').with.length.greaterThan(0);\n  });\n\n  test(\"os.cpus\", function() {\n    const cpus = os.cpus();\n    expect(cpus).to.be.an('array').with.length.greaterThan(0);\n    expect(cpus[0]).to.have.property('model');\n  });\n\n  test(\"os.totalmem and os.freemem\", function() {\n    expect(os.totalmem()).to.be.a('number').greaterThan(0);\n    expect(os.freemem()).to.be.a('number').greaterThan(0);\n  });\n\n  test(\"os.uptime\", function() {\n    expect(os.uptime()).to.be.a('number').greaterThan(0);\n  });\n\n  test(\"os.loadavg\", function() {\n    const load = os.loadavg();\n    expect(load).to.be.an('array').with.lengthOf(3);\n  });\n\n  test(\"os.networkInterfaces\", function() {\n    expect(os.networkInterfaces()).to.be.an('object');\n  });\n\n  test(\"os.userInfo\", function() {\n    const info = os.userInfo();\n    expect(info.username).to.be.a('string');\n    expect(info.homedir).to.be.a('string');\n  });\n\n  test(\"os.EOL and os.constants\", function() {\n    expect(['\\n', '\\r\\n']).to.include(os.EOL);\n    expect(os.constants).to.have.property('signals');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/node-builtins/node-path.bru",
    "content": "meta {\n  name: node-path\n  type: http\n  seq: 11\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // Skip in safe mode - these tests require developer sandbox\n  if (bru.isSafeMode()) {\n    bru.runner.skipRequest();\n    return;\n  }\n}\n\ntests {\n  const path = require('node:path');\n\n  test(\"path.join\", function() {\n    expect(path.join('/foo', 'bar', 'baz')).to.equal('/foo/bar/baz');\n    expect(path.join('foo', 'bar', 'baz')).to.equal('foo/bar/baz');\n  });\n\n  test(\"path.resolve\", function() {\n    const resolved = path.resolve('foo', 'bar');\n    expect(path.isAbsolute(resolved)).to.equal(true);\n  });\n\n  test(\"path.dirname and path.basename\", function() {\n    expect(path.dirname('/foo/bar/baz.txt')).to.equal('/foo/bar');\n    expect(path.basename('/foo/bar/baz.txt')).to.equal('baz.txt');\n    expect(path.basename('/foo/bar/baz.txt', '.txt')).to.equal('baz');\n  });\n\n  test(\"path.extname\", function() {\n    expect(path.extname('file.txt')).to.equal('.txt');\n    expect(path.extname('file')).to.equal('');\n    expect(path.extname('.gitignore')).to.equal('');\n  });\n\n  test(\"path.parse and path.format\", function() {\n    const parsed = path.parse('/foo/bar/baz.txt');\n    expect(parsed.root).to.equal('/');\n    expect(parsed.dir).to.equal('/foo/bar');\n    expect(parsed.base).to.equal('baz.txt');\n    expect(parsed.name).to.equal('baz');\n    expect(parsed.ext).to.equal('.txt');\n\n    expect(path.format(parsed)).to.equal('/foo/bar/baz.txt');\n  });\n\n  test(\"path.normalize\", function() {\n    expect(path.normalize('/foo/bar//baz/../qux')).to.equal('/foo/bar/qux');\n  });\n\n  test(\"path.isAbsolute\", function() {\n    expect(path.isAbsolute('/foo/bar')).to.equal(true);\n    expect(path.isAbsolute('foo/bar')).to.equal(false);\n  });\n\n  test(\"path.relative\", function() {\n    expect(path.relative('/foo/bar', '/foo/baz')).to.equal('../baz');\n  });\n\n  test(\"path.sep and path.delimiter\", function() {\n    expect(path.sep).to.be.a('string');\n    expect(path.delimiter).to.be.a('string');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/node-builtins/node-querystring.bru",
    "content": "meta {\n  name: node-querystring\n  type: http\n  seq: 16\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // Skip in safe mode - these tests require developer sandbox\n  if (bru.isSafeMode()) {\n    bru.runner.skipRequest();\n    return;\n  }\n}\n\ntests {\n  const qs = require('node:querystring');\n\n  test(\"querystring.parse\", function() {\n    const parsed = qs.parse('foo=1&bar=2&foo=3');\n    expect(parsed.bar).to.equal('2');\n    expect(parsed.foo).to.deep.equal(['1', '3']);\n  });\n\n  test(\"querystring.parse with custom separators\", function() {\n    const parsed = qs.parse('foo:1;bar:2', ';', ':');\n    expect(parsed.foo).to.equal('1');\n    expect(parsed.bar).to.equal('2');\n  });\n\n  test(\"querystring.stringify\", function() {\n    expect(qs.stringify({ foo: 'bar', baz: 'qux' })).to.equal('foo=bar&baz=qux');\n    expect(qs.stringify({ foo: ['a', 'b'] })).to.equal('foo=a&foo=b');\n  });\n\n  test(\"querystring.stringify with custom separators\", function() {\n    expect(qs.stringify({ foo: 'bar', baz: 'qux' }, ';', ':')).to.equal('foo:bar;baz:qux');\n  });\n\n  test(\"querystring.escape and unescape\", function() {\n    expect(qs.escape('hello world')).to.equal('hello%20world');\n    expect(qs.unescape('hello%20world')).to.equal('hello world');\n  });\n\n  test(\"querystring roundtrip\", function() {\n    const obj = { name: 'Bruno', version: '1.0' };\n    const encoded = qs.stringify(obj);\n    const decoded = qs.parse(encoded);\n    expect(decoded.name).to.equal('Bruno');\n    expect(decoded.version).to.equal('1.0');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/node-builtins/node-stream.bru",
    "content": "meta {\n  name: node-stream\n  type: http\n  seq: 18\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // Skip in safe mode - these tests require developer sandbox\n  if (bru.isSafeMode()) {\n    bru.runner.skipRequest();\n    return;\n  }\n}\n\ntests {\n  const stream = require('node:stream');\n  const { Readable, Writable, Transform, Duplex, pipeline } = stream;\n\n  test(\"stream module exports\", function() {\n    expect(stream.Readable).to.be.a('function');\n    expect(stream.Writable).to.be.a('function');\n    expect(stream.Transform).to.be.a('function');\n    expect(stream.Duplex).to.be.a('function');\n    expect(stream.pipeline).to.be.a('function');\n  });\n\n  test(\"Readable.from creates readable stream\", function() {\n    const readable = Readable.from(['hello', ' ', 'bruno']);\n    expect(readable).to.be.an('object');\n    expect(readable.read).to.be.a('function');\n    expect(readable.on).to.be.a('function');\n  });\n\n  test(\"Writable stream can be created\", function() {\n    const chunks = [];\n    const writable = new Writable({\n      write(chunk, enc, cb) { chunks.push(chunk); cb(); }\n    });\n    expect(writable).to.be.an('object');\n    expect(writable.write).to.be.a('function');\n    expect(writable.end).to.be.a('function');\n  });\n\n  test(\"Transform stream can be created\", function() {\n    const transform = new Transform({\n      transform(chunk, enc, cb) { cb(null, chunk.toString().toUpperCase()); }\n    });\n    expect(transform).to.be.an('object');\n    expect(transform.write).to.be.a('function');\n    expect(transform.read).to.be.a('function');\n  });\n\n  test(\"Duplex stream can be created\", function() {\n    const duplex = new Duplex({\n      read() {},\n      write(chunk, enc, cb) { cb(); }\n    });\n    expect(duplex).to.be.an('object');\n    expect(duplex.read).to.be.a('function');\n    expect(duplex.write).to.be.a('function');\n  });\n\n  test(\"pipeline is a function\", function() {\n    expect(pipeline).to.be.a('function');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/node-builtins/node-util.bru",
    "content": "meta {\n  name: node-util\n  type: http\n  seq: 15\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // Skip in safe mode - these tests require developer sandbox\n  if (bru.isSafeMode()) {\n    bru.runner.skipRequest();\n    return;\n  }\n}\n\ntests {\n  const util = require('node:util');\n\n  test(\"util.format\", function() {\n    expect(util.format('Hello %s', 'Bruno')).to.equal('Hello Bruno');\n    expect(util.format('Count: %d', 42)).to.equal('Count: 42');\n    expect(util.format('Data: %j', { a: 1 })).to.equal('Data: {\"a\":1}');\n  });\n\n  test(\"util.inspect\", function() {\n    const obj = { name: 'bruno', nested: { value: 42 } };\n    const str = util.inspect(obj);\n    expect(str).to.be.a('string').that.includes('bruno');\n\n    const deep = { a: { b: { c: { d: 'deep' } } } };\n    expect(util.inspect(deep, { depth: 1 })).to.include('[Object]');\n    expect(util.inspect(deep, { depth: null })).to.include('deep');\n  });\n\n  test(\"util.promisify\", function() {\n    const promisified = util.promisify(setTimeout);\n    expect(promisified).to.be.a('function');\n    // Returns a promise when called\n    const result = promisified(1);\n    expect(result).to.be.a('promise');\n  });\n\n  test(\"util.types\", function() {\n    expect(util.types.isDate(new Date())).to.equal(true);\n    expect(util.types.isMap(new Map())).to.equal(true);\n    expect(util.types.isSet(new Set())).to.equal(true);\n    expect(util.types.isRegExp(/test/)).to.equal(true);\n    expect(util.types.isPromise(Promise.resolve())).to.equal(true);\n  });\n\n  test(\"util.isDeepStrictEqual\", function() {\n    expect(util.isDeepStrictEqual({ a: 1 }, { a: 1 })).to.equal(true);\n    expect(util.isDeepStrictEqual({ a: 1 }, { a: 2 })).to.equal(false);\n  });\n\n  test(\"util.deprecate and util.callbackify\", function() {\n    expect(util.deprecate(() => {}, 'deprecated')).to.be.a('function');\n    expect(util.callbackify(async () => {})).to.be.a('function');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/node-builtins/node-zlib.bru",
    "content": "meta {\n  name: node-zlib\n  type: http\n  seq: 17\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // Skip in safe mode - these tests require developer sandbox\n  if (bru.isSafeMode()) {\n    bru.runner.skipRequest();\n    return;\n  }\n}\n\ntests {\n  const zlib = require('node:zlib');\n\n  const testData = Buffer.from('Hello Bruno! '.repeat(100));\n\n  test(\"gzip and gunzip\", function() {\n    const compressed = zlib.gzipSync(testData);\n    expect(compressed.length).to.be.lessThan(testData.length);\n\n    const decompressed = zlib.gunzipSync(compressed);\n    expect(decompressed.toString()).to.equal(testData.toString());\n  });\n\n  test(\"deflate and inflate\", function() {\n    const compressed = zlib.deflateSync(testData);\n    expect(compressed.length).to.be.lessThan(testData.length);\n\n    const decompressed = zlib.inflateSync(compressed);\n    expect(decompressed.toString()).to.equal(testData.toString());\n  });\n\n  test(\"deflateRaw and inflateRaw\", function() {\n    const compressed = zlib.deflateRawSync(testData);\n    const decompressed = zlib.inflateRawSync(compressed);\n    expect(decompressed.toString()).to.equal(testData.toString());\n  });\n\n  test(\"brotli compression\", function() {\n    const compressed = zlib.brotliCompressSync(testData);\n    expect(compressed.length).to.be.lessThan(testData.length);\n\n    const decompressed = zlib.brotliDecompressSync(compressed);\n    expect(decompressed.toString()).to.equal(testData.toString());\n  });\n\n  test(\"compression levels\", function() {\n    const high = zlib.gzipSync(testData, { level: 9 });\n    const low = zlib.gzipSync(testData, { level: 1 });\n    expect(high.length).to.be.at.most(low.length);\n  });\n\n  test(\"zlib.constants\", function() {\n    expect(zlib.constants).to.have.property('Z_BEST_COMPRESSION');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/node-builtins/process.bru",
    "content": "meta {\n  name: process\n  type: http\n  seq: 6\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // Skip in safe mode - these tests require developer sandbox\n  if (bru.isSafeMode()) {\n    bru.runner.skipRequest();\n    return;\n  }\n}\n\ntests {\n  test(\"process exists with expected properties\", function() {\n    expect(typeof process).to.equal('object');\n    expect(process.version).to.match(/^v\\d+\\.\\d+\\.\\d+/);\n    expect(process.versions).to.have.property('node');\n    expect(process.versions).to.have.property('v8');\n  });\n\n  test(\"process.arch and process.platform\", function() {\n    expect(['x64', 'arm64', 'arm', 'ia32']).to.include(process.arch);\n    expect(['darwin', 'linux', 'win32']).to.include(process.platform);\n  });\n\n  test(\"process.pid and process.title\", function() {\n    expect(process.pid).to.be.a('number').and.to.be.greaterThan(0);\n    expect(process.title).to.be.a('string');\n  });\n\n  test(\"process.argv is array\", function() {\n    expect(process.argv).to.be.an('array');\n  });\n\n  test(\"process.env is available\", function() {\n    expect(process.env).to.be.an('object');\n  });\n\n  test(\"process.nextTick is available\", function() {\n    expect(process.nextTick).to.be.a('function');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/node-builtins/timers.bru",
    "content": "meta {\n  name: timers\n  type: http\n  seq: 5\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // Skip in safe mode - these tests require developer sandbox\n  if (bru.isSafeMode()) {\n    bru.runner.skipRequest();\n    return;\n  }\n}\n\ntests {\n  test(\"setTimeout exists and is callable\", function() {\n    expect(setTimeout).to.be.a('function');\n    const id = setTimeout(() => {}, 1000);\n    expect(id).to.not.be.undefined;\n    clearTimeout(id);\n  });\n\n  test(\"clearTimeout exists and works\", function() {\n    expect(clearTimeout).to.be.a('function');\n    let fired = false;\n    const id = setTimeout(() => { fired = true; }, 0);\n    clearTimeout(id);\n    // Can't fully verify without async, but clearTimeout should not throw\n    expect(fired).to.equal(false);\n  });\n\n  test(\"setInterval exists and is callable\", function() {\n    expect(setInterval).to.be.a('function');\n    const id = setInterval(() => {}, 1000);\n    expect(id).to.not.be.undefined;\n    clearInterval(id);\n  });\n\n  test(\"clearInterval exists\", function() {\n    expect(clearInterval).to.be.a('function');\n  });\n\n  test(\"setImmediate exists and is callable\", function() {\n    expect(setImmediate).to.be.a('function');\n    const id = setImmediate(() => {});\n    expect(id).to.not.be.undefined;\n    clearImmediate(id);\n  });\n\n  test(\"clearImmediate exists\", function() {\n    expect(clearImmediate).to.be.a('function');\n  });\n\n  test(\"queueMicrotask exists\", function() {\n    expect(queueMicrotask).to.be.a('function');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/node-builtins/url.bru",
    "content": "meta {\n  name: url\n  type: http\n  seq: 2\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // Skip in safe mode - these tests require developer sandbox\n  if (bru.isSafeMode()) {\n    bru.runner.skipRequest();\n    return;\n  }\n}\n\ntests {\n  test(\"URL parsing\", function() {\n    const url = new URL('https://user:pass@example.com:8080/path?foo=bar#hash');\n    expect(url.protocol).to.equal('https:');\n    expect(url.hostname).to.equal('example.com');\n    expect(url.port).to.equal('8080');\n    expect(url.pathname).to.equal('/path');\n    expect(url.search).to.equal('?foo=bar');\n    expect(url.hash).to.equal('#hash');\n    expect(url.username).to.equal('user');\n    expect(url.password).to.equal('pass');\n    expect(url.origin).to.equal('https://example.com:8080');\n  });\n\n  test(\"URL modification\", function() {\n    const url = new URL('https://example.com');\n    url.pathname = '/api/v1';\n    url.searchParams.set('key', 'value');\n    expect(url.toString()).to.equal('https://example.com/api/v1?key=value');\n  });\n\n  test(\"URLSearchParams\", function() {\n    const params = new URLSearchParams('foo=1&bar=2&foo=3');\n    expect(params.get('foo')).to.equal('1');\n    expect(params.getAll('foo')).to.deep.equal(['1', '3']);\n    expect(params.has('bar')).to.equal(true);\n    expect(params.has('missing')).to.equal(false);\n\n    params.set('bar', 'updated');\n    params.delete('foo');\n    params.append('new', 'value');\n    expect(params.toString()).to.equal('bar=updated&new=value');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/node-builtins/web-crypto.bru",
    "content": "meta {\n  name: web-crypto\n  type: http\n  seq: 4\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  // Skip in safe mode - these tests require developer sandbox\n  if (bru.isSafeMode()) {\n    bru.runner.skipRequest();\n    return;\n  }\n}\n\ntests {\n  test(\"crypto global exists\", function() {\n    expect(typeof crypto).to.equal('object');\n    expect(typeof crypto.subtle).to.equal('object');\n  });\n\n  test(\"crypto.randomUUID\", function() {\n    const uuid = crypto.randomUUID();\n    expect(uuid).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);\n  });\n\n  test(\"crypto.getRandomValues\", function() {\n    const array = new Uint8Array(16);\n    crypto.getRandomValues(array);\n    expect(array.some(b => b !== 0)).to.equal(true);\n  });\n\n  test(\"crypto.subtle methods exist\", function() {\n    expect(crypto.subtle.digest).to.be.a('function');\n    expect(crypto.subtle.generateKey).to.be.a('function');\n    expect(crypto.subtle.sign).to.be.a('function');\n    expect(crypto.subtle.verify).to.be.a('function');\n    expect(crypto.subtle.encrypt).to.be.a('function');\n    expect(crypto.subtle.decrypt).to.be.a('function');\n    expect(crypto.subtle.importKey).to.be.a('function');\n    expect(crypto.subtle.exportKey).to.be.a('function');\n  });\n\n  test(\"crypto.subtle.digest returns promise\", function() {\n    const data = new TextEncoder().encode('hello');\n    const result = crypto.subtle.digest('SHA-256', data);\n    expect(result).to.be.a('promise');\n  });\n\n  test(\"crypto.subtle.generateKey returns promise\", function() {\n    const result = crypto.subtle.generateKey(\n      { name: 'AES-GCM', length: 256 },\n      true,\n      ['encrypt', 'decrypt']\n    );\n    expect(result).to.be.a('promise');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/npm modules/ajv.bru",
    "content": "meta {\n  name: ajv\n  type: http\n  seq: 2\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"hello\": \"bruno\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\nscript:pre-request {\n  const Ajv = require('ajv');\n  const ajv = new Ajv();\n\n  // Define a JSON schema\n  const schema = {\n    type: 'object',\n    properties: {\n      name: { type: 'string', minLength: 1 },\n      age: { type: 'integer', minimum: 0 },\n      email: { type: 'string' }\n    },\n    required: ['name', 'age']\n  };\n\n  // Valid data to validate\n  const validData = {\n    name: 'Bruno User',\n    age: 25,\n    email: 'bruno@example.com'\n  };\n\n  // Compile and validate\n  const validate = ajv.compile(schema);\n  const isValid = validate(validData);\n\n  // Set validation result in request body\n  const data = req.getBody();\n  data.ajvValidation = {\n    isValid: isValid,\n    validatedData: validData\n  };\n\n  req.setBody(data);\n}\n\ntests {\n  test(\"ajv should validate data correctly\", function() {\n    const data = res.getBody();\n\n    expect(data.hello).to.equal(\"bruno\");\n    expect(data.ajvValidation).to.be.an('object');\n    expect(data.ajvValidation.isValid).to.be.true;\n    expect(data.ajvValidation.validatedData.name).to.equal('Bruno User');\n    expect(data.ajvValidation.validatedData.age).to.equal(25);\n  });\n\n  test(\"ajv should be available in tests\", function() {\n    const Ajv = require('ajv');\n    const ajv = new Ajv();\n\n    const schema = { type: 'number' };\n    const validate = ajv.compile(schema);\n\n    expect(validate(42)).to.be.true;\n    expect(validate('not a number')).to.be.false;\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/npm modules/external-lib-bru-req-res.bru",
    "content": "meta {\n  name: external-lib-bru-req-res\n  type: http\n  seq: 5\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"name\": \"Bruno User\",\n    \"age\": 25\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\ntests {\n  const extLib = require('external-lib-with-bru-req-res-objects');\n  \n  test(\"should provide bru object to npm modules\", function() {\n    extLib.setVar('ext-lib-test', 'hello');\n    expect(extLib.getVar('ext-lib-test')).to.equal('hello');\n  });\n  \n  test(\"should provide req object to npm modules\", function() {\n    const method = extLib.getReqMethod();\n    expect(method).to.equal('POST');\n  \n    const headers = extLib.getReqHeaders();\n    // expect(headers).to.be.an('object');\n    expect(headers['content-type']).to.include('json');\n  });\n  \n  test(\"should provide res object to npm modules\", function() {\n    const status = extLib.getResStatus();\n    expect(status).to.equal(200);\n  \n    const body = extLib.getResBody();\n    // expect(body).to.be.an('object');\n    expect(body.name).to.equal('Bruno User');\n    expect(body.age).to.equal(25);\n  \n    const headers = extLib.getResHeaders();\n    // expect(headers).to.be.an('object');\n    expect(headers['content-type']).to.include('json');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/npm modules/fakerjs.bru",
    "content": "meta {\n  name: fakerjs\n  type: http\n  seq: 1\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"hello\": \"bruno\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\nscript:pre-request {\n  const { faker } = require('@faker-js/faker');\n  const uuid = faker.string.uuid();\n  \n  const data = req.getBody();\n  data.uuid = uuid;\n  \n  req.setBody(data);\n}\n\ntests {\n  test(\"should return json\", function() {\n    const data = res.getBody();\n    const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n    const isUUID = (inputString) => {\n      return uuidRegex.test(inputString);\n    };\n    \n    expect(data.hello).to.equal(\"bruno\");\n    expect(isUUID(data.uuid)).to.be.true;\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/scripting/npm modules/jose.bru",
    "content": "meta {\n  name: jose\n  type: http\n  seq: 3\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"hello\": \"bruno\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\nscript:pre-request {\n  const jose = require('jose');\n\n  // Create a symmetric secret for HS256\n  const secret = new TextEncoder().encode('my-super-secret-key-for-testing');\n\n  // Create a JWT with jose\n  const jwt = await new jose.SignJWT({ sub: 'bruno-user', name: 'Bruno' })\n    .setProtectedHeader({ alg: 'HS256' })\n    .setIssuedAt()\n    .setExpirationTime('1h')\n    .sign(secret);\n\n  // Verify the JWT\n  const { payload, protectedHeader } = await jose.jwtVerify(jwt, secret);\n\n  const data = req.getBody();\n  data.jwt = jwt;\n  data.verified = {\n    alg: protectedHeader.alg,\n    sub: payload.sub,\n    name: payload.name\n  };\n\n  req.setBody(data);\n}\n\ntests {\n  test(\"jose should create and verify JWT\", function() {\n    const data = res.getBody();\n\n    expect(data.hello).to.equal(\"bruno\");\n    expect(data.jwt).to.be.a('string');\n\n    // JWT should have 3 parts separated by dots\n    const parts = data.jwt.split('.');\n    expect(parts.length).to.equal(3);\n\n    // Verify the verification worked\n    expect(data.verified.alg).to.equal('HS256');\n    expect(data.verified.sub).to.equal('bruno-user');\n    expect(data.verified.name).to.equal('Bruno');\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/string interpolation/env vars.bru",
    "content": "meta {\n  name: env vars\n  type: http\n  seq: 2\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nauth:basic {\n  username: asd\n  password: j\n}\n\nauth:bearer {\n  token: \n}\n\nbody:json {\n  {\n    \"envVar1\": \"{{env.var1}}\",\n    \"envVar2\": \"{{env-var2}}\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\ntests {\n  test(\"should return json\", function() {\n    expect(res.getBody()).to.eql({\n      \"envVar1\": \"envVar1\",\n      \"envVar2\": \"envVar2\"\n    });\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/string interpolation/folder.bru",
    "content": "meta {\n  name: string interpolation\n}\n\nvars:pre-request {\n  folder_pre_var: folder_pre_var_value\n  folder_pre_var_2: {{env.var1}}\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/string interpolation/missing values.bru",
    "content": "meta {\n  name: missing values\n  type: http\n  seq: 1\n}\n\npost {\n  url: {{host}}/api/echo/json?foo={{undefinedVar}}\n  body: json\n  auth: none\n}\n\nquery {\n  foo: {{undefinedVar}}\n}\n\nauth:basic {\n  username: asd\n  password: j\n}\n\nauth:bearer {\n  token: \n}\n\nbody:json {\n  {\n    \"hello\": \"{{undefinedVar2}}\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\ntests {\n  test(\"should return json\", function() {\n    const url = req.getUrl();\n    const query = url.split(\"?\")[1];\n    expect(query).to.equal(\"foo={{undefinedVar}}\");\n  \n    const data = res.getBody();\n    expect(res.getBody()).to.eql({\n      \"hello\": \"{{undefinedVar2}}\"\n    });\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/string interpolation/objects-arrays interpolation.bru",
    "content": "meta {\n  name: objects/arrays interpolation\n  type: http\n  seq: 5\n}\n\npost {\n  url: https://echo.usebruno.com\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n   \"undefined\": \"{{obj.undefined}}\",\n   \"null\": {{obj.null}},\n   \"number\": {{obj.number}},\n   \"boolean\": {{obj.boolean}},\n   \"array\": {{arr}},\n   \"array[0]\": {{arr[0]}},\n   \"object\": {{obj}},\n   \"object.foo\": {{obj.foo}},\n   \"object.foo.bar\": {{obj.foo.bar}},\n   \"object.foo.bar.baz\": {{obj.foo.bar.baz}}\n  }\n}\n\nscript:pre-request {\n  bru.setVar(\"arr\", [1,2,3,4,5]);\n  \n  bru.setVar(\"obj\", {\n    \"null\": null,\n    \"number\": 1,\n    \"boolean\": true,\n    \"foo\": {\n      \"bar\": {\n        \"baz\": 1\n      }\n    }\n  });\n}\n\ntests {\n  test(\"should interpolate arrays and objects in request payload body\", () => {\n    const resBody = res.getBody();\n    const expectedOutput = {\n      \"undefined\": \"{{obj.undefined}}\",\n      \"null\": null,\n      \"number\": 1,\n      \"boolean\": true,\n      \"array\": [\n        1,\n        2,\n        3,\n        4,\n        5\n      ],\n      \"array[0]\": 1,\n      \"object\": {\n        \"null\": null,\n        \"number\": 1,\n        \"boolean\": true,\n        \"foo\": {\n          \"bar\": {\n            \"baz\": 1\n          }\n        }\n      },\n      \"object.foo\": {\n        \"bar\": {\n          \"baz\": 1\n        }\n      },\n      \"object.foo.bar\": {\n        \"baz\": 1\n      },\n      \"object.foo.bar.baz\": 1\n    };\n    expect(resBody).to.be.eql(expectedOutput);\n  })\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/string interpolation/process env vars.bru",
    "content": "meta {\n  name: process env vars\n  type: http\n  seq: 4\n}\n\npost {\n  url: {{host}}/api/echo/json\n  body: json\n  auth: none\n}\n\nauth:basic {\n  username: asd\n  password: j\n}\n\nauth:bearer {\n  token: \n}\n\nbody:json {\n  {\n    \"bark\": \"{{bark}}\",\n    \"bark2\": \"{{process.env.PROC_ENV_VAR}}\"\n  }\n}\n\nassert {\n  res.status: eq 200\n}\n\ntests {\n  test(\"should return json\", function() {\n    expect(res.getBody()).to.eql({\n      \"bark\": \"woof\",\n      \"bark2\": \"woof\"\n    });\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/string interpolation/runtime vars.bru",
    "content": "meta {\n  name: runtime vars\n  type: http\n  seq: 3\n}\n\npost {\n  url: {{host}}/api/echo/text\n  body: text\n  auth: none\n}\n\nauth:basic {\n  username: asd\n  password: j\n}\n\nauth:bearer {\n  token: \n}\n\nbody:json {\n  {\n    \"envVar1\": \"{{env.var1}}\",\n    \"envVar2\": \"{{env-var2}}\"\n  }\n}\n\nbody:text {\n  Hi, I am {{rUser.full_name}},\n  I am {{rUser.age}} years old.\n  My favorite food is {{rUser.fav-food[0]}} and {{rUser.fav-food[1]}}.\n  I like attention: {{rUser['want.attention']}}\n}\n\nassert {\n  res.status: eq 200\n}\n\nscript:pre-request {\n  const brunoBirthDate = new Date('2019-08-08');\n\n  const calculateAgeFromBirthDate = (birthDate = brunoBirthDate) => {\n    const today = new Date();\n    let age = today.getFullYear() - birthDate.getFullYear();\n\n    const hasBirthdayPassedThisYear =\n      today.getMonth() > birthDate.getMonth() ||\n      (today.getMonth() === birthDate.getMonth() && today.getDate() >= birthDate.getDate());\n\n    if (!hasBirthdayPassedThisYear) {\n      age--;\n    }\n\n    return age;\n  };\n\n  const brunoAge = calculateAgeFromBirthDate(brunoBirthDate);\n\n  bru.setVar(\"rUser\", {\n    full_name: 'Bruno',\n    age: brunoAge,\n    'fav-food': ['egg', 'meat'],\n    'want.attention': true\n  });\n}\n\ntests {\n  test(\"should return json\", function() {\n    const brunoBirthDate = new Date('2019-08-08');\n\n    const calculateAgeFromBirthDate = (birthDate = brunoBirthDate) => {\n      const today = new Date();\n      let age = today.getFullYear() - birthDate.getFullYear();\n\n      const hasBirthdayPassedThisYear =\n        today.getMonth() > birthDate.getMonth() ||\n        (today.getMonth() === birthDate.getMonth() && today.getDate() >= birthDate.getDate());\n\n      if (!hasBirthdayPassedThisYear) {\n        age--;\n      }\n\n      return age;\n    };\n\n    const brunoAge = calculateAgeFromBirthDate(brunoBirthDate);\n\n    const expectedResponse = `Hi, I am Bruno,\n  I am ${brunoAge} years old.\n  My favorite food is egg and meat.\n  I like attention: true`;\n    expect(res.getBody()).to.equal(expectedResponse);\n  });\n  \n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/url-serialization/Duplicate Keys.bru",
    "content": "meta {\n  name: Duplicate Keys\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://echo.usebruno.com\n  body: formUrlEncoded\n  auth: none\n}\n\nheaders {\n  Content-Type: application/x-www-form-urlencoded\n}\n\nbody:form-urlencoded {\n  tags: frontend\n  tags: api\n  user: john\n}\n\nscript:post-response {\n  test('Response body matches expected value', function () {\n      expect(res.getBody()).to.eql(\"tags=frontend&tags=api&user=john\");\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection/url-serialization/folder.bru",
    "content": "meta {\n  name: url-serialization\n  seq: 13\n}\n\nauth {\n  mode: inherit\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection_level_oauth2/.gitignore",
    "content": "!.env"
  },
  {
    "path": "packages/bruno-tests/collection_level_oauth2/.nvmrc",
    "content": "v18"
  },
  {
    "path": "packages/bruno-tests/collection_level_oauth2/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"collection_level_oauth2\",\n  \"type\": \"collection\",\n  \"scripts\": {\n    \"moduleWhitelist\": [\"crypto\"]\n  },\n  \"clientCertificates\": {\n    \"enabled\": true,\n    \"certs\": []\n  },\n  \"presets\": {\n    \"requestType\": \"http\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection_level_oauth2/collection.bru",
    "content": "headers {\n  check: again\n}\n\nauth {\n  mode: oauth2\n}\n\nauth:oauth2 {\n  grant_type: authorization_code\n  callback_url: {{authorization_code_callback_url}}\n  authorization_url: {{authorization_code_authorize_url}}\n  access_token_url: {{authorization_code_access_token_url}}\n  client_id: {{client_id}}\n  client_secret: {{client_secret}}\n  scope: \n  pkce: true\n}\n\nscript:post-response {\n  if(req.getAuthMode() == 'oauth2' && res.body.access_token) {\n   bru.setEnvVar('access_token_set_by_collection',res.body.access_token) \n  }\n}\n\ndocs {\n  # bruno-testbench 🐶\n  \n  This is a test collection that I am using to test various functionalities around bruno\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection_level_oauth2/environments/Local.bru",
    "content": "vars {\n  host: http://localhost:8080\n  bearer_auth_token: your_secret_token\n  basic_auth_password: della\n  client_id: client_id_1\n  client_secret: client_secret_1\n  password_credentials_access_token_url: http://localhost:8080/api/auth/oauth2/password_credentials/token\n  password_credentials_username: foo\n  password_credentials_password: bar\n  password_credentials_scope: \n  authorization_code_authorize_url: http://localhost:8080/api/auth/oauth2/authorization_code/authorize\n  authorization_code_callback_url: http://localhost:8080/api/auth/oauth2/authorization_code/callback\n  authorization_code_access_token_url: http://localhost:8080/api/auth/oauth2/authorization_code/token\n  authorization_code_google_auth_url: https://accounts.google.com/o/oauth2/auth\n  authorization_code_google_access_token_url: https://accounts.google.com/o/oauth2/token\n  authorization_code_google_scope: https://www.googleapis.com/auth/userinfo.email\n  authorization_code_github_authorize_url: https://github.com/login/oauth/authorize\n  authorization_code_github_access_token_url: https://github.com/login/oauth/access_token\n  authorization_code_access_token: null\n  client_credentials_access_token_url: http://localhost:8080/api/auth/oauth2/client_credentials/token\n  client_credentials_client_id: client_id_1\n  client_credentials_client_secret: client_secret_1\n  client_credentials_scope: admin\n  client_credentials_access_token: 9f1b1874f1e79b48a46d65569d830bbb\n  common_access_token: 9f1b1874f1e79b48a46d65569d830bbb\n}\nvars:secret [\n  authorization_code_google_client_id,\n  authorization_code_google_client_secret,\n  authorization_code_github_client_secret,\n  authorization_code_github_client_id,\n  authorization_code_github_authorization_code,\n  authorization_code_github_access_token\n]\n"
  },
  {
    "path": "packages/bruno-tests/collection_level_oauth2/environments/Prod.bru",
    "content": "vars {\n  host: https://testbench-sanity.usebruno.com\n  bearer_auth_token: your_secret_token\n  basic_auth_password: della\n  env.var1: envVar1\n  env-var2: envVar2\n  bark: {{process.env.PROC_ENV_VAR}}\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection_level_oauth2/package.json",
    "content": "{\n  \"name\": \"@usebruno/test-collection\",\n  \"version\": \"0.0.1\",\n  \"dependencies\": {\n    \"@faker-js/faker\": \"^8.4.1\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection_level_oauth2/readme.md",
    "content": "# bruno-tests collection\n\nAPI Collection to run sanity tests on Bruno CLI.\n"
  },
  {
    "path": "packages/bruno-tests/collection_level_oauth2/resource.bru",
    "content": "meta {\n  name: resource\n  type: http\n  seq: 2\n}\n\npost {\n  url: {{host}}/api/auth/oauth2/authorization_code/resource?token={{access_token_set_by_collection}}\n  body: json\n  auth: none\n}\n\nquery {\n  token: {{access_token_set_by_collection}}\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection_oauth2/.env",
    "content": "PROC_ENV_VAR=woof"
  },
  {
    "path": "packages/bruno-tests/collection_oauth2/.gitignore",
    "content": "!.env"
  },
  {
    "path": "packages/bruno-tests/collection_oauth2/.nvmrc",
    "content": "v18"
  },
  {
    "path": "packages/bruno-tests/collection_oauth2/auth/oauth2/authorization_code/github token with authorize.bru",
    "content": "meta {\n  name: github token with authorize\n  type: http\n  seq: 1\n}\n\npost {\n  url: \n  body: none\n  auth: oauth2\n}\n\nauth:oauth2 {\n  grant_type: authorization_code\n  callback_url: {{authorization_code_callback_url}}\n  authorization_url: {{authorization_code_github_authorize_url}}\n  access_token_url: {{authorization_code_github_access_token_url}}\n  client_id: {{authorization_code_github_client_id}}\n  client_secret: {{authorization_code_github_client_secret}}\n  scope: repo,gist\n}\n\nscript:post-response {\n  bru.setEnvVar('github_access_token',res.body.split('access_token=')[1]?.split('&scope')[0]);\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection_oauth2/auth/oauth2/authorization_code/google token with authorize.bru",
    "content": "meta {\n  name: google token with authorize\n  type: http\n  seq: 4\n}\n\npost {\n  url: \n  body: none\n  auth: oauth2\n}\n\nauth:oauth2 {\n  grant_type: authorization_code\n  callback_url: {{authorization_code_callback_url}}\n  authorization_url: {{authorization_code_google_auth_url}}\n  access_token_url: {{authorization_code_google_access_token_url}}\n  client_id: {{authorization_code_google_client_id}}\n  client_secret: {{authorization_code_google_client_secret}}\n  scope: {{authorization_code_google_scope}}\n}\n\nscript:post-response {\n  bru.setEnvVar('authorization_code_access_token', res.body.access_token);\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection_oauth2/auth/oauth2/authorization_code/resource.bru",
    "content": "meta {\n  name: resource\n  type: http\n  seq: 3\n}\n\npost {\n  url: {{host}}/api/auth/oauth2/authorization_code/resource?token={{authorization_code_access_token}}\n  body: json\n  auth: none\n}\n\nquery {\n  token: {{authorization_code_access_token}}\n}"
  },
  {
    "path": "packages/bruno-tests/collection_oauth2/auth/oauth2/authorization_code/token with authorize.bru",
    "content": "meta {\n  name: token with authorize\n  type: http\n  seq: 4\n}\n\npost {\n  url: \n  body: none\n  auth: oauth2\n}\n\nauth:oauth2 {\n  grant_type: authorization_code\n  callback_url: {{authorization_code_callback_url}}\n  authorization_url: {{authorization_code_authorize_url}}\n  access_token_url: {{authorization_code_access_token_url}}\n  client_id: {{client_id}}\n  client_secret: {{client_secret}}\n  scope: \n  pkce: true\n}\n\nscript:post-response {\n  bru.setEnvVar('authorization_code_access_token', res.body.access_token);\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection_oauth2/auth/oauth2/client_credentials/resource.bru",
    "content": "meta {\n  name: resource\n  type: http\n  seq: 2\n}\n\nget {\n  url: {{host}}/api/auth/oauth2/client_credentials/resource?token={{client_credentials_access_token}}\n  body: none\n  auth: none\n}\n\nquery {\n  token: {{client_credentials_access_token}}\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection_oauth2/auth/oauth2/client_credentials/token.bru",
    "content": "meta {\n  name: token\n  type: http\n  seq: 1\n}\n\npost {\n  url: \n  body: none\n  auth: oauth2\n}\n\nauth:oauth2 {\n  grant_type: client_credentials\n  access_token_url: {{client_credentials_access_token_url}}\n  client_id: {{client_credentials_client_id}}\n  client_secret: {{client_credentials_client_secret}}\n  scope: {{client_credentials_scope}}\n}\n\nscript:post-response {\n  bru.setEnvVar('client_credentials_access_token', res.body.access_token);\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection_oauth2/auth/oauth2/password_credentials/resource.bru",
    "content": "meta {\n  name: resource\n  type: http\n  seq: 2\n}\n\npost {\n  url: {{host}}/api/auth/oauth2/password_credentials/resource\n  body: none\n  auth: bearer\n}\n\nauth:bearer {\n  token: {{passwordCredentials_access_token}}\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection_oauth2/auth/oauth2/password_credentials/token.bru",
    "content": "meta {\n  name: token\n  type: http\n  seq: 1\n}\n\npost {\n  url: \n  body: none\n  auth: oauth2\n}\n\nauth:oauth2 {\n  grant_type: password\n  access_token_url: {{password_credentials_access_token_url}}\n  username: {{password_credentials_username}}\n  password: {{password_credentials_password}}\n  scope: \n}\n\nscript:post-response {\n  bru.setEnvVar('passwordCredentials_access_token', res.body.access_token);\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection_oauth2/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"OAuth2 Demo\",\n  \"type\": \"collection\",\n  \"scripts\": {\n    \"moduleWhitelist\": [\n      \"crypto\"\n    ]\n  },\n  \"clientCertificates\": {\n    \"enabled\": true,\n    \"certs\": []\n  },\n  \"presets\": {\n    \"requestType\": \"http\"\n  }\n}"
  },
  {
    "path": "packages/bruno-tests/collection_oauth2/collection.bru",
    "content": "headers {\n  check: again\n}\n\nauth {\n  mode: none\n}\n\ndocs {\n  # bruno-testbench 🐶\n  \n  This is a test collection that I am using to test various functionalities around bruno\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection_oauth2/environments/Local.bru",
    "content": "vars {\n  host: http://localhost:8080\n  bearer_auth_token: your_secret_token\n  basic_auth_password: della\n  client_id: client_id_1\n  client_secret: client_secret_1\n  password_credentials_access_token_url: http://localhost:8080/api/auth/oauth2/password_credentials/token\n  password_credentials_username: foo\n  password_credentials_password: bar\n  password_credentials_scope: \n  authorization_code_authorize_url: http://localhost:8080/api/auth/oauth2/authorization_code/authorize\n  authorization_code_callback_url: http://localhost:8080/api/auth/oauth2/authorization_code/callback\n  authorization_code_access_token_url: http://localhost:8080/api/auth/oauth2/authorization_code/token\n  authorization_code_google_auth_url: https://accounts.google.com/o/oauth2/auth\n  authorization_code_google_access_token_url: https://accounts.google.com/o/oauth2/token\n  authorization_code_google_scope: https://www.googleapis.com/auth/userinfo.email\n  authorization_code_github_authorize_url: https://github.com/login/oauth/authorize\n  authorization_code_github_access_token_url: https://github.com/login/oauth/access_token\n  authorization_code_access_token: null\n  client_credentials_access_token_url: http://localhost:8080/api/auth/oauth2/client_credentials/token\n  client_credentials_client_id: client_id_1\n  client_credentials_client_secret: client_secret_1\n  client_credentials_scope: admin\n}\nvars:secret [\n  authorization_code_google_client_id,\n  authorization_code_google_client_secret,\n  authorization_code_github_client_secret,\n  authorization_code_github_client_id,\n  authorization_code_github_authorization_code,\n  authorization_code_github_access_token\n]\n"
  },
  {
    "path": "packages/bruno-tests/collection_oauth2/environments/Prod.bru",
    "content": "vars {\n  host: https://testbench-sanity.usebruno.com\n  bearer_auth_token: your_secret_token\n  basic_auth_password: della\n  env.var1: envVar1\n  env-var2: envVar2\n  bark: {{process.env.PROC_ENV_VAR}}\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection_oauth2/file.json",
    "content": "{\n  \"hello\": \"bruno\"\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection_oauth2/package.json",
    "content": "{\n  \"name\": \"@usebruno/test-collection\",\n  \"version\": \"0.0.1\",\n  \"dependencies\": {\n    \"@faker-js/faker\": \"^8.4.0\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-tests/collection_oauth2/readme.md",
    "content": "# bruno-tests collection\n\nAPI Collection to run sanity tests on Bruno CLI.\n"
  },
  {
    "path": "packages/bruno-tests/external-lib-with-bru-req-res-objects/index.js",
    "content": "/**\n * External library that accesses bru, req, res objects from the VM context.\n * These are available as globals inside the Node VM sandbox.\n *\n * Used to test that npm modules can access bru, req, res context objects.\n */\n\nmodule.exports = {\n  // bru accessors\n  getVar: function (name) { return bru.getVar(name); },\n  setVar: function (name, value) { bru.setVar(name, value); },\n  getEnvVar: function (name) { return bru.getEnvVar(name); },\n\n  // req accessors\n  getReqBody: function (options) { return req.getBody(options); },\n  getReqHeaders: function () { return req.getHeaders(); },\n  getReqMethod: function () { return req.getMethod(); },\n\n  // res accessors\n  getResBody: function () { return res.getBody(); },\n  getResHeaders: function () { return res.getHeaders(); },\n  getResStatus: function () { return res.getStatus(); }\n};\n"
  },
  {
    "path": "packages/bruno-tests/external-lib-with-bru-req-res-objects/package.json",
    "content": "{\n  \"name\": \"@usebruno/external-lib-with-bru-req-res-objects\",\n  \"version\": \"0.0.1\"\n}\n"
  },
  {
    "path": "packages/bruno-tests/keycloak-authorization_code/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"keycloak-authorization_code\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}"
  },
  {
    "path": "packages/bruno-tests/keycloak-authorization_code/collection.bru",
    "content": "auth {\n  mode: oauth2\n}\n\nauth:oauth2 {\n  grant_type: authorization_code\n  callback_url: {{key-host}}/realms/bruno/account\n  authorization_url: {{key-host}}/realms/bruno/protocol/openid-connect/auth\n  access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token\n  refresh_token_url: \n  client_id: account\n  client_secret: {{client_secret}}\n  scope: openid\n  state: \n  pkce: true\n  credentials_placement: body\n  credentials_id: credentials\n  token_placement: header\n  token_header_prefix: Bearer\n  auto_fetch_token: true\n  auto_refresh_token: false\n}\n"
  },
  {
    "path": "packages/bruno-tests/keycloak-authorization_code/environments/oauth2.bru",
    "content": "vars {\n  key-host: http://localhost:8080\n}\nvars:secret [\n  client_secret\n]\n"
  },
  {
    "path": "packages/bruno-tests/keycloak-authorization_code/user_info_coll-auth.bru",
    "content": "meta {\n  name: user_info_coll-auth\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo\n  body: none\n  auth: inherit\n}\n"
  },
  {
    "path": "packages/bruno-tests/keycloak-authorization_code/user_info_custom.bru",
    "content": "meta {\n  name: user_info_custom\n  type: http\n  seq: 2\n}\n\nget {\n  url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo\n  body: none\n  auth: bearer\n}\n\nauth:bearer {\n  token: {{$oauth2.credentials.access_token}}\n}\n"
  },
  {
    "path": "packages/bruno-tests/keycloak-authorization_code/user_info_request-auth.bru",
    "content": "meta {\n  name: user_info_request-auth\n  type: http\n  seq: 3\n}\n\nget {\n  url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo\n  body: json\n  auth: oauth2\n}\n\nauth:oauth2 {\n  grant_type: authorization_code\n  callback_url: {{key-host}}/realms/bruno/account\n  authorization_url: {{key-host}}/realms/bruno/protocol/openid-connect/auth\n  access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token\n  refresh_token_url: \n  client_id: account\n  client_secret: {{client_secret}}\n  scope: openid\n  state: \n  pkce: true\n  credentials_placement: body\n  credentials_id: credentials\n  token_placement: header\n  token_header_prefix: Bearer\n  auto_fetch_token: true\n  auto_refresh_token: true\n}\n"
  },
  {
    "path": "packages/bruno-tests/keycloak-client-credentials/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"keycloak-client-credentials\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}"
  },
  {
    "path": "packages/bruno-tests/keycloak-client-credentials/collection.bru",
    "content": "auth {\n  mode: oauth2\n}\n\nauth:oauth2 {\n  grant_type: client_credentials\n  access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token\n  refresh_token_url: \n  client_id: account\n  client_secret: {{client_secret}}\n  scope: openid\n  credentials_placement: body\n  credentials_id: credentials\n  token_placement: header\n  token_header_prefix: Bearer\n  auto_fetch_token: true\n  auto_refresh_token: false\n}\n"
  },
  {
    "path": "packages/bruno-tests/keycloak-client-credentials/environments/oauth2.bru",
    "content": "vars {\n  key-host: http://localhost:8080\n}\nvars:secret [\n  client_secret\n]\n"
  },
  {
    "path": "packages/bruno-tests/keycloak-client-credentials/user_info_coll-auth.bru",
    "content": "meta {\n  name: user_info_coll-auth\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo\n  body: none\n  auth: inherit\n}\n"
  },
  {
    "path": "packages/bruno-tests/keycloak-client-credentials/user_info_custom.bru",
    "content": "meta {\n  name: user_info_custom\n  type: http\n  seq: 2\n}\n\nget {\n  url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo\n  body: none\n  auth: bearer\n}\n\nauth:bearer {\n  token: {{$oauth2.credentials.access_token}}\n}\n"
  },
  {
    "path": "packages/bruno-tests/keycloak-client-credentials/user_info_request-auth.bru",
    "content": "meta {\n  name: user_info_request-auth\n  type: http\n  seq: 3\n}\n\nget {\n  url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo\n  body: none\n  auth: oauth2\n}\n\nauth:oauth2 {\n  grant_type: client_credentials\n  access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token\n  refresh_token_url: \n  client_id: account\n  client_secret: {{client_secret}}a\n  scope: openid\n  credentials_placement: body\n  credentials_id: credentials\n  token_placement: header\n  token_header_prefix: Bearer\n  auto_fetch_token: true\n  auto_refresh_token: false\n}\n"
  },
  {
    "path": "packages/bruno-tests/keycloak-password-credentials/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"keycloak-password-credentials\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}"
  },
  {
    "path": "packages/bruno-tests/keycloak-password-credentials/collection.bru",
    "content": "auth {\n  mode: oauth2\n}\n\nauth:oauth2 {\n  grant_type: password\n  access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token\n  refresh_token_url: \n  username: bruno\n  password: bruno\n  client_id: account\n  client_secret: {{client_secret}}\n  scope: openid\n  credentials_placement: body\n  credentials_id: credentials\n  token_placement: header\n  token_header_prefix: Bearer\n  auto_fetch_token: true\n  auto_refresh_token: false\n}\n"
  },
  {
    "path": "packages/bruno-tests/keycloak-password-credentials/environments/oauth2.bru",
    "content": "vars {\n  key-host: http://localhost:8080\n}\nvars:secret [\n  client_secret\n]\n"
  },
  {
    "path": "packages/bruno-tests/keycloak-password-credentials/user_info_coll-auth.bru",
    "content": "meta {\n  name: user_info_coll-auth\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo\n  body: none\n  auth: inherit\n}\n"
  },
  {
    "path": "packages/bruno-tests/keycloak-password-credentials/user_info_custom.bru",
    "content": "meta {\n  name: user_info_custom\n  type: http\n  seq: 2\n}\n\nget {\n  url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo\n  body: none\n  auth: bearer\n}\n\nauth:bearer {\n  token: {{$oauth2.credentials.access_token}}\n}\n"
  },
  {
    "path": "packages/bruno-tests/keycloak-password-credentials/user_info_request-auth.bru",
    "content": "meta {\n  name: user_info_request-auth\n  type: http\n  seq: 3\n}\n\nget {\n  url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo\n  body: none\n  auth: oauth2\n}\n\nauth:oauth2 {\n  grant_type: password\n  access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token\n  refresh_token_url: \n  username: admin\n  password: admin\n  client_id: account\n  client_secret: {{client_secret}}\n  scope: openid\n  credentials_placement: body\n  credentials_id: credentials\n  token_placement: header\n  token_header_prefix: Bearer\n  auto_fetch_token: true\n  auto_refresh_token: false\n}\n"
  },
  {
    "path": "packages/bruno-tests/package.json",
    "content": "{\n  \"name\": \"@usebruno/tests\",\n  \"version\": \"0.0.1\",\n  \"description\": \"\",\n  \"main\": \"src/index.js\",\n  \"scripts\": {\n    \"start\": \"node .\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/usebruno/bruno-testbench.git\"\n  },\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/usebruno/bruno-testbench/issues\"\n  },\n  \"homepage\": \"https://github.com/usebruno/bruno-testbench#readme\",\n  \"dependencies\": {\n    \"axios\": \"^1.8.3\",\n    \"body-parser\": \"2.2.0\",\n    \"cookie-parser\": \"^1.4.6\",\n    \"cors\": \"^2.8.5\",\n    \"express\": \"^4.21.2\",\n    \"graphql\": \"^16.10.0\",\n    \"graphql-yoga\": \"^5.10.6\",\n    \"express-basic-auth\": \"^1.2.1\",\n    \"fast-xml-parser\": \"^5.0.8\",\n    \"http-proxy\": \"^1.18.1\",\n    \"js-yaml\": \"^4.1.1\",\n    \"jsonwebtoken\": \"^9.0.3\",\n    \"lodash\": \"^4.17.21\",\n    \"multer\": \"^1.4.5-lts.1\",\n    \"ws\": \"^8.18.3\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-tests/readme.md",
    "content": "# bruno-tests\n\nThis package is used to test the Bruno CLI.\nWe have a collection that sits in the `collection` directory.\n\n### Test Server\n\nThis will start the server on port 80 which exposes endpoints that the collection will hit.\n\n```bash\n# install node dependencies\nnpm install\n\n# start server\nnpm start\n```\n\n### Run Bru CLI on Collection\n\n```bash\ncd collection\n\n# run collection against local server\nnode ../../bruno-cli/bin/bru.js run --env Local --output junit.xml --format junit\n\n# run collection against prod server hosted at https://testbench.usebruno.com\nnode ../../bruno-cli/bin/bru.js run --env Prod --output junit.xml --format junit\n```\n\n### License\n\n[MIT](LICENSE)\n"
  },
  {
    "path": "packages/bruno-tests/sandwich_exec/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"sandwich_exec\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ],\n  \"scripts\": {\n    \"flow\": \"sandwich\"\n  }\n}"
  },
  {
    "path": "packages/bruno-tests/sandwich_exec/collection.bru",
    "content": "script:pre-request {\n  console.log(\"collection pre\");\n}\n\nscript:post-response {\n  {\n    console.log(\"collection post\");\n    const sequence = bru.getVar('sequence') || [];\n    sequence.push(1);\n    bru.setVar('sequence', sequence);\n    console.log(\"sequence\", bru.getVar('sequence'));\n  }\n}\n"
  },
  {
    "path": "packages/bruno-tests/sandwich_exec/folder/folder.bru",
    "content": "meta {\n  name: folder\n}\n\nscript:pre-request {\n  console.log(\"folder pre\");\n}\n\nscript:post-response {\n  {\n    const sequence = bru.getVar('sequence') || [];\n    sequence.push(2);\n    bru.setVar('sequence', sequence);\n  }\n  console.log(\"folder post\");\n}\n"
  },
  {
    "path": "packages/bruno-tests/sandwich_exec/folder/request.bru",
    "content": "meta {\n  name: request\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://www.example.com\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  console.log(\"request pre\");\n}\n\nscript:post-response {\n  {\n    console.log(\"request post\");\n  \n    const sequence = bru.getVar('sequence') || [];\n    sequence.push(3);\n    bru.setVar('sequence', sequence);\n  }\n}\n\ntests {\n  test(\"sandwich script execution is proper\", function() {\n    const sequence = bru.getVar('sequence');\n    bru.setVar('sequence', null);\n    expect(sequence.toString()).to.equal([3,2,1].toString());\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/sequential_exec/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"sequential_exec\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ],\n  \"scripts\": {\n    \"flow\": \"sequential\"\n  }\n}"
  },
  {
    "path": "packages/bruno-tests/sequential_exec/collection.bru",
    "content": "script:pre-request {\n  console.log(\"collection pre\");\n}\n\nscript:post-response {\n  {\n    console.log(\"collection post\");\n    const sequence = bru.getVar('sequence') || [];\n    sequence.push(1);\n    bru.setVar('sequence', sequence);\n  }\n}\n"
  },
  {
    "path": "packages/bruno-tests/sequential_exec/folder/folder.bru",
    "content": "meta {\n  name: folder\n}\n\nscript:pre-request {\n  console.log(\"folder pre\");\n}\n\nscript:post-response {\n  {\n    console.log(\"folder post\");\n    const sequence = bru.getVar('sequence') || [];\n    sequence.push(2);\n    bru.setVar('sequence', sequence);\n  }\n}\n"
  },
  {
    "path": "packages/bruno-tests/sequential_exec/folder/request.bru",
    "content": "meta {\n  name: request\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://www.example.com\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  console.log(\"request pre\");\n}\n\nscript:post-response {\n  {\n    console.log(\"request post\");\n    const sequence = bru.getVar('sequence') || [];\n    sequence.push(3);\n    bru.setVar('sequence', sequence);\n    \n    console.log(\"sequence\", bru.getVar('sequence'));\n  }\n}\n\ntests {\n  test(\"sequential script execution is proper\", function() {\n    const sequence = bru.getVar('sequence');\n    bru.setVar('sequence', null);\n    expect(sequence.toString()).to.equal([1,2,3].toString());\n  });\n}\n"
  },
  {
    "path": "packages/bruno-tests/src/auth/basic.js",
    "content": "const express = require('express');\nconst router = express.Router();\nconst basicAuth = require('express-basic-auth');\n\nconst users = {\n  bruno: 'della'\n};\n\nconst basicAuthMiddleware = basicAuth({\n  users,\n  challenge: true, // Sends a 401 Unauthorized response when authentication fails\n  unauthorizedResponse: 'Unauthorized'\n});\n\nrouter.post('/protected', basicAuthMiddleware, (req, res) => {\n  res.status(200).json({ message: 'Authentication successful' });\n});\n\nmodule.exports = router;\n"
  },
  {
    "path": "packages/bruno-tests/src/auth/bearer.js",
    "content": "const express = require('express');\nconst router = express.Router();\n\nconst authenticateToken = (req, res, next) => {\n  const token = req.header('Authorization');\n\n  if (!token || token !== `Bearer your_secret_token`) {\n    return res.status(401).json({ message: 'Unauthorized' });\n  }\n\n  next();\n};\n\nrouter.get('/protected', authenticateToken, (req, res) => {\n  res.status(200).json({ message: 'Authentication successful' });\n});\n\nmodule.exports = router;\n"
  },
  {
    "path": "packages/bruno-tests/src/auth/cookie.js",
    "content": "const express = require('express');\nconst cookieParser = require('cookie-parser');\nconst router = express.Router();\n\n// Initialize the cookie-parser middleware\nrouter.use(cookieParser());\n\n// Middleware to check if the user is authenticated\nfunction requireAuth(req, res, next) {\n  const isAuthenticated = req.cookies.isAuthenticated === 'true';\n\n  if (isAuthenticated) {\n    next(); // User is authenticated, continue to the next middleware or route handler\n  } else {\n    res.status(401).json({ message: 'Unauthorized' }); // User is not authenticated, send a 401 Unauthorized response\n  }\n}\n\n// Route to set a cookie when a user logs in\nrouter.post('/login', (req, res) => {\n  // You should perform authentication here, and if successful, set the cookie.\n  // For demonstration purposes, let's assume the user is authenticated.\n  res.cookie('isAuthenticated', 'true');\n  res.status(200).json({ message: 'Logged in successfully' });\n});\n\n// Route to log out and clear the cookie\nrouter.post('/logout', (req, res) => {\n  res.clearCookie('isAuthenticated');\n  res.status(200).json({ message: 'Logged out successfully' });\n});\n\n// Protected route that requires authentication\nrouter.get('/protected', requireAuth, (req, res) => {\n  res.status(200).json({ message: 'Authentication successful' });\n});\n\nmodule.exports = router;\n"
  },
  {
    "path": "packages/bruno-tests/src/auth/index.js",
    "content": "const express = require('express');\nconst router = express.Router();\n\nconst authBearer = require('./bearer');\nconst authBasic = require('./basic');\nconst authWsse = require('./wsse');\nconst authCookie = require('./cookie');\nconst authOAuth2PasswordCredentials = require('./oauth2/passwordCredentials');\nconst authOAuth2AuthorizationCode = require('./oauth2/authorizationCode');\nconst authOAuth2ClientCredentials = require('./oauth2/clientCredentials');\n\nrouter.use('/oauth2/password_credentials', authOAuth2PasswordCredentials);\nrouter.use('/oauth2/authorization_code', authOAuth2AuthorizationCode);\nrouter.use('/oauth2/client_credentials', authOAuth2ClientCredentials);\nrouter.use('/bearer', authBearer);\nrouter.use('/basic', authBasic);\nrouter.use('/wsse', authWsse);\nrouter.use('/cookie', authCookie);\n\nmodule.exports = router;\n"
  },
  {
    "path": "packages/bruno-tests/src/auth/oauth2/authorizationCode.js",
    "content": "const express = require('express');\nconst router = express.Router();\nconst crypto = require('crypto');\nconst clients = [\n  {\n    client_id: 'client_id_1',\n    client_secret: 'client_secret_1',\n    redirect_uri: 'http://localhost:3001/callback'\n  }\n];\n\nconst authCodes = [];\n\nconst tokens = [];\n\nfunction generateUniqueString() {\n  return crypto.randomBytes(16).toString('hex');\n}\n\nconst generateCodeChallenge = (codeVerifier) => {\n  const hash = crypto.createHash('sha256');\n  hash.update(codeVerifier);\n  const base64Hash = hash.digest('base64');\n  return base64Hash.replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=/g, '');\n};\n\nrouter.get('/authorize', (req, res) => {\n  const { response_type, client_id, redirect_uri, code_challenge } = req.query;\n  console.log('authorization code authorize', req.query);\n  if (response_type !== 'code') {\n    return res.status(401).json({ error: 'Invalid Response type, expected \"code\"' });\n  }\n\n  const client = clients.find((c) => c.client_id === client_id);\n\n  if (!client) {\n    return res.status(401).json({ error: 'Invalid client' });\n  }\n\n  if (!redirect_uri) {\n    return res.status(401).json({ error: 'Invalid redirect URI' });\n  }\n\n  const authorization_code = generateUniqueString();\n  authCodes.push({\n    authCode: authorization_code,\n    client_id,\n    redirect_uri,\n    code_challenge\n  });\n\n  const redirectUrl = `${redirect_uri}?code=${authorization_code}`;\n\n  try {\n    // validating redirect URL\n    const url = new URL(redirectUrl);\n  } catch (err) {\n    return res.status(401).json({ error: 'Invalid redirect URI' });\n  }\n\n  const _res = `\n    <html>\n      <script>\n        document.addEventListener(\"DOMContentLoaded\", (event) => {\n          const buttonElement = document.getElementById('authorize');\n          buttonElement.addEventListener('click', e => {\n            e.preventDefault();\n            buttonElement.innerText = 'redirecting...';\n            try {\n              const url = new URL(\"${redirectUrl}\");\n              window.location.href = url;\n            }\n            catch(err) {\n              buttonElement.innerText = 'Invalid Redirect URL';\n              console.log('Invalid Redirect URL')\n            }\n          });\n        });\n      </script>\n      <body>\n        <button id='authorize'>Authorize</button>\n      </body>\n    </html>\n  `;\n\n  res.send(_res);\n});\n\n// Handle the authorization callback\nrouter.get('/callback', (req, res) => {\n  console.log('authorization code callback', req.query);\n  const { code } = req.query;\n\n  // Check if the authCode is valid.\n  const storedAuthCode = authCodes.find((t) => t.authCode === code);\n\n  if (!storedAuthCode) {\n    return res.status(401).json({ error: 'Invalid Authorization Code' });\n  }\n\n  return res.json({ message: 'Authorization successful', storedAuthCode });\n});\n\nrouter.post('/token', (req, res) => {\n  console.log('authorization code token', req.body, req.headers);\n  let grant_type, code, redirect_uri, client_id, client_secret, code_verifier;\n  if (req?.body?.grant_type) {\n    grant_type = req?.body?.grant_type;\n    code = req?.body?.code;\n    redirect_uri = req?.body?.redirect_uri;\n    client_id = req?.body?.client_id;\n    client_secret = req?.body?.client_secret;\n    code_verifier = req?.body?.code_verifier;\n  }\n  if (req?.headers?.grant_type) {\n    grant_type = req?.headers?.grant_type;\n    code = req?.headers?.code;\n    redirect_uri = req?.headers?.redirect_uri;\n    client_id = req?.headers?.client_id;\n    client_secret = req?.headers?.client_secret;\n    code_verifier = req?.headers?.code_verifier;\n  }\n\n  if (grant_type !== 'authorization_code') {\n    return res.status(401).json({ error: 'Invalid Grant Type' });\n  }\n\n  // const client = clients.find((c) => c.client_id === client_id && c.client_secret === client_secret);\n  // if (!client) {\n  //   return res.status(401).json({ error: 'Invalid client credentials' });\n  // }\n\n  const storedAuthCode = authCodes.find((t) => {\n    if (!t?.code_challenge) {\n      return t.authCode === code;\n    } else {\n      return t.authCode === code && t.code_challenge === generateCodeChallenge(code_verifier);\n    }\n  });\n\n  if (!storedAuthCode) {\n    return res.status(401).json({ error: 'Invalid Authorization Code' });\n  }\n\n  const accessToken = generateUniqueString();\n  tokens.push({\n    accessToken: accessToken,\n    client_id\n  });\n\n  res.json({ access_token: accessToken });\n});\n\nrouter.post('/resource', (req, res) => {\n  try {\n    console.log('authorization code resource', req.query, tokens);\n    const { token } = req.query;\n    const storedToken = tokens.find((t) => t.accessToken === token);\n    if (!storedToken) {\n      return res.status(401).json({ error: 'Invalid Access Token' });\n    }\n    return res.json({ resource: { name: 'foo', email: 'foo@bar.com' } });\n  } catch (err) {\n    return res.status(401).json({ error: 'Corrupt Access Token' });\n  }\n});\n\nmodule.exports = router;\n"
  },
  {
    "path": "packages/bruno-tests/src/auth/oauth2/clientCredentials.js",
    "content": "const express = require('express');\nconst router = express.Router();\nconst crypto = require('crypto');\nconst clients = [\n  {\n    client_id: 'client_id_1',\n    client_secret: 'client_secret_1',\n    scope: 'admin'\n  }\n];\n\nconst tokens = [];\n\nfunction generateUniqueString() {\n  return crypto.randomBytes(16).toString('hex');\n}\n\nrouter.post('/token', (req, res) => {\n  let grant_type, client_id, client_secret, scope;\n  if (req?.body?.grant_type) {\n    grant_type = req?.body?.grant_type;\n    client_id = req?.body?.client_id;\n    client_secret = req?.body?.client_secret;\n    scope = req?.body?.scope;\n  } else if (req?.headers?.grant_type) {\n    grant_type = req?.headers?.grant_type;\n    client_id = req?.headers?.client_id;\n    client_secret = req?.headers?.client_secret;\n    scope = req?.headers?.scope;\n  }\n\n  console.log('client_cred', client_id, client_secret, scope);\n  if (grant_type !== 'client_credentials') {\n    return res.status(401).json({ error: 'Invalid Grant Type, expected \"client_credentials\"' });\n  }\n\n  const client = clients.find((c) => c.client_id == client_id && c.client_secret == client_secret && c.scope == scope);\n\n  if (!client) {\n    return res.status(401).json({ error: 'Invalid client details or scope' });\n  }\n\n  const token = generateUniqueString();\n  tokens.push({\n    token,\n    client_id,\n    client_secret,\n    scope\n  });\n\n  return res.json({ message: 'Authenticated successfully', access_token: token, scope });\n});\n\nrouter.get('/resource', (req, res) => {\n  try {\n    const { token } = req.query;\n    const storedToken = tokens.find((t) => t.token === token);\n    if (!storedToken) {\n      return res.status(401).json({ error: 'Invalid Access Token' });\n    }\n    return res.json({ resource: { name: 'foo', email: 'foo@bar.com' } });\n  } catch (err) {\n    return res.status(401).json({ error: 'Corrupt Access Token' });\n  }\n});\n\nmodule.exports = router;\n"
  },
  {
    "path": "packages/bruno-tests/src/auth/oauth2/passwordCredentials.js",
    "content": "const express = require('express');\nconst router = express.Router();\nconst jwt = require('jsonwebtoken');\n\nconst users = [\n  {\n    username: 'foo',\n    password: 'bar'\n  }\n];\n\nrouter.post('/token', (req, res) => {\n  const { grant_type, username, password, scope } = req.body;\n\n  console.log('password_credentials', username, password, scope);\n\n  if (grant_type !== 'password') {\n    return res.status(401).json({ error: 'Invalid Grant Type' });\n  }\n\n  const user = users.find((u) => u.username == username && u.password == password);\n\n  if (!user) {\n    return res.status(401).json({ error: 'Invalid user credentials' });\n  }\n  var token = jwt.sign({ username, password }, 'bruno');\n  return res.json({ message: 'Authorization successful', access_token: token });\n});\n\nrouter.post('/resource', (req, res) => {\n  try {\n    const tokenString = req.header('Authorization');\n    const token = tokenString.split(' ')[1];\n    var decodedJwt = jwt.verify(token, 'bruno');\n    const { username, password } = decodedJwt;\n    const user = users.find((u) => u.username === username && u.password === password);\n    if (!user) {\n      return res.status(401).json({ error: 'Invalid token' });\n    }\n    return res.json({ resource: { name: 'foo', email: 'foo@bar.com' } });\n  } catch (err) {\n    return res.status(401).json({ error: 'Corrupt token' });\n  }\n});\n\nmodule.exports = router;\n"
  },
  {
    "path": "packages/bruno-tests/src/auth/wsse.js",
    "content": "'use strict';\n\nconst express = require('express');\nconst router = express.Router();\nconst crypto = require('crypto');\n\nfunction sha256(data) {\n  return crypto.createHash('sha256').update(data).digest('base64');\n}\n\nfunction validateWSSE(req, res, next) {\n  const wsseHeader = req.headers['x-wsse'];\n  if (!wsseHeader) {\n    return unauthorized(res, 'WSSE header is missing');\n  }\n\n  const regex = /UsernameToken Username=\"(.+?)\", PasswordDigest=\"(.+?)\", (?:Nonce|nonce)=\"(.+?)\", Created=\"(.+?)\"/;\n  const matches = wsseHeader.match(regex);\n\n  if (!matches) {\n    return unauthorized(res, 'Invalid WSSE header format');\n  }\n\n  const [_, username, passwordDigest, nonce, created] = matches;\n  const expectedPassword = 'bruno'; // Ideally store in a config or env variable\n  const expectedDigest = sha256(nonce + created + expectedPassword);\n\n  if (passwordDigest !== expectedDigest) {\n    return unauthorized(res, 'Invalid credentials');\n  }\n\n  next();\n}\n\n// Helper to respond with an unauthorized SOAP fault\nfunction unauthorized(res, message) {\n  const faultResponse = `\n    <soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:web=\"http://webservice/\">\n      <soapenv:Header/>\n      <soapenv:Body>\n        <soapenv:Fault>\n          <faultcode>soapenv:Client</faultcode>\n          <faultstring>${message}</faultstring>\n        </soapenv:Fault>\n      </soapenv:Body>\n    </soapenv:Envelope>\n  `;\n  res.status(401).set('Content-Type', 'text/xml');\n  res.send(faultResponse);\n}\n\nconst responses = {\n  success: `\n    <soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:web=\"http://webservice/\">\n      <soapenv:Header/>\n      <soapenv:Body>\n        <web:response>\n          <web:result>Success</web:result>\n        </web:response>\n      </soapenv:Body>\n    </soapenv:Envelope>\n  `\n};\n\nrouter.post('/protected', validateWSSE, (req, res) => {\n  res.set('Content-Type', 'text/xml');\n  res.send(responses.success);\n});\n\nmodule.exports = router;\n"
  },
  {
    "path": "packages/bruno-tests/src/echo/index.js",
    "content": "const express = require('express');\nconst router = express.Router();\n\nrouter.get('/path/*', (req, res) => {\n  return res.json({ url: req.url });\n});\n\nrouter.post('/json', (req, res) => {\n  return res.json(req.body);\n});\n\nrouter.post('/text', (req, res) => {\n  res.setHeader('Content-Type', 'text/plain');\n  return res.send(req.body);\n});\n\nrouter.post('/xml-parsed', (req, res) => {\n  return res.send(req.body);\n});\n\nrouter.post('/xml-raw', (req, res) => {\n  res.setHeader('Content-Type', 'application/xml');\n  return res.send(req.rawBody);\n});\n\nrouter.post('/bin', (req, res) => {\n  const rawBody = req.body;\n\n  if (!rawBody || rawBody.length === 0) {\n    return res.status(400).send('No data received');\n  }\n\n  res.set('Content-Type', req.headers['content-type'] || 'application/octet-stream');\n  res.send(rawBody);\n});\n\nrouter.get('/bom-json-test', (req, res) => {\n  const jsonData = {\n    message: 'Hello!',\n    success: true\n  };\n  const jsonString = JSON.stringify(jsonData);\n  const bom = '\\uFEFF';\n  const jsonWithBom = bom + jsonString;\n  res.set('Content-Type', 'application/json; charset=utf-8');\n  return res.send(jsonWithBom);\n});\n\nrouter.get('/iso-enc', (req, res) => {\n  res.set('Content-Type', 'text/plain; charset=ISO-8859-1');\n  const responseText = 'éçà';\n  return res.send(Buffer.from(responseText, 'latin1'));\n});\n\nrouter.post('/custom', (req, res) => {\n  const { headers, content, contentBase64, contentJSON, type } = req.body || {};\n\n  res._headers = {};\n\n  if (type) {\n    res.setHeader('Content-Type', type);\n  }\n\n  if (headers && typeof headers === 'object') {\n    Object.entries(headers).forEach(([key, value]) => {\n      res.setHeader(key, value);\n    });\n  }\n\n  if (contentBase64) {\n    res.write(Buffer.from(contentBase64, 'base64'));\n  } else if (contentJSON !== undefined) {\n    res.write(JSON.stringify(contentJSON));\n  } else if (content !== undefined) {\n    res.write(content);\n  }\n\n  return res.end();\n});\n\nmodule.exports = router;\n"
  },
  {
    "path": "packages/bruno-tests/src/graphql/index.js",
    "content": "const setupGraphQL = async (app) => {\n  // Dynamic import because `graphql-yoga` is a pure ESM module.\n  const { createYoga, createSchema } = await import('graphql-yoga');\n\n  const yoga = createYoga({\n    schema: createSchema({\n      typeDefs: /* GraphQL */ `\n        type Company {\n          ceo: String\n          name: String\n          founder: String\n        }\n        \n        input ICreate {\n          id: String!\n        }\n\n        type Message {\n          success: Boolean\n        }\n\n        type Query {\n          company: Company\n        }\n        \n        type Mutation {\n          create(payload: ICreate!): Message\n        }\n      `,\n      resolvers: {\n        Query: {\n          company: () => ({\n            ceo: 'Elon Musk',\n            name: 'SpaceX',\n            founder: 'Elon Musk'\n          })\n        },\n        Mutation: {\n          create: () => ({\n            success: true\n          })\n        }\n      }\n    }),\n    graphqlEndpoint: '/api/graphql'\n  });\n\n  app.use(yoga.graphqlEndpoint, yoga);\n};\n\nmodule.exports = setupGraphQL;\n"
  },
  {
    "path": "packages/bruno-tests/src/index.js",
    "content": "const express = require('express');\nconst bodyParser = require('body-parser');\nconst cors = require('cors');\nconst formDataParser = require('./multipart/form-data-parser');\nconst authRouter = require('./auth');\nconst echoRouter = require('./echo');\nconst xmlParser = require('./utils/xmlParser');\nconst multipartRouter = require('./multipart');\nconst redirectRouter = require('./redirect');\nconst mixRouter = require('./mix');\nconst wsRouter = require('./ws');\nconst setupGraphQL = require('./graphql');\n\nconst app = new express();\nconst port = process.env.PORT || 8081;\n\napp.use(cors());\n\nconst saveRawBody = (req, res, buf) => {\n  req.rawBuffer = Buffer.from(buf);\n  req.rawBody = buf.toString();\n};\n\napp.use(bodyParser.json({ verify: saveRawBody }));\napp.use(bodyParser.urlencoded({ extended: true, verify: saveRawBody }));\napp.use(bodyParser.text({ verify: saveRawBody }));\napp.use(xmlParser());\n// Only parse raw body for content types not already handled by other parsers\napp.use(express.raw({\n  type: (req) => {\n    const contentType = req.headers['content-type'] || '';\n    // Skip if already handled by json, urlencoded, text, or xml parsers\n    if (contentType.includes('application/json')\n      || contentType.includes('application/x-www-form-urlencoded')\n      || contentType.includes('text/')\n      || contentType.includes('application/xml')) {\n      return false;\n    }\n    return true;\n  },\n  limit: '100mb',\n  verify: saveRawBody\n}));\n\nformDataParser.init(app, express);\n\napp.use('/api/auth', authRouter);\napp.use('/api/echo', echoRouter);\napp.use('/api/multipart', multipartRouter);\napp.use('/api/redirect', redirectRouter);\napp.use('/api/mix', mixRouter);\n\napp.get('/ping', function (req, res) {\n  return res.send('pong');\n});\n\napp.get('/headers', function (req, res) {\n  return res.json(req.headers);\n});\n\napp.get('/query', function (req, res) {\n  return res.json(req.query);\n});\n\napp.get('/redirect-to-ping', function (req, res) {\n  return res.redirect('/ping');\n});\n\nconst server = require('http').createServer(app);\n\nserver.on('upgrade', wsRouter);\n\nsetupGraphQL(app).then(() => {\n  server.listen(port, function () {\n    console.log(`Testbench started on port: ${port}`);\n  });\n})\n  .catch((error) => {\n    console.error('Failed to initialize GraphQL', error);\n    process.exit(1);\n  });\n"
  },
  {
    "path": "packages/bruno-tests/src/mix/index.js",
    "content": "const express = require('express');\nconst router = express.Router();\n\nrouter.get('/', function (req, res) {\n  // Parse query parameters similar to http bun's /mix endpoint\n  // s=status code, c=cookie (name:value), r=redirect URL\n  const statusCode = parseInt(req.query.s, 10) || 302;\n  const cookie = req.query.c; // format: name:value\n  const redirectUrl = req.query.r;\n\n  // Set cookie if provided\n  if (cookie) {\n    const [cookieName, cookieValue] = cookie.split(':');\n    if (cookieName && cookieValue) {\n      res.setHeader('Set-Cookie', `${cookieName}=${cookieValue}; Path=/`);\n    }\n  }\n\n  // Redirect to the specified URL, even if it's not on our domain\n  if (redirectUrl) {\n    res.status(statusCode)\n      .set('Location', redirectUrl)\n      .send(`<!doctype html>\n      <title>Redirecting...</title>\n      <h1>Redirecting...</h1>\n      <p>You should be redirected automatically to target URL: <a href=\"${redirectUrl}\">${redirectUrl}</a>. If not click the link.</p>\n    `);\n  } else {\n    res.status(400).json({ error: 'Missing redirect URL parameter (r)' });\n  }\n});\n\nmodule.exports = router;\n"
  },
  {
    "path": "packages/bruno-tests/src/multipart/form-data-parser.js",
    "content": "/**\n * Instead of using multer for example to parse the multipart form data, we build our own parser\n * so that we can verify the content type are set correctly by bruno (for example application/json for json content)\n */\n\nconst extractParam = function (param, str, delimiter, quote, endDelimiter) {\n  let regex = new RegExp(`${param}${delimiter}\\\\s*${quote}(.*?)${quote}${endDelimiter}`);\n  const found = str.match(regex);\n  if (found != null && found.length > 1) {\n    return found[1];\n  } else {\n    return null;\n  }\n};\n\nconst init = function (app, express) {\n  app.use(express.raw({ type: 'multipart/form-data' }));\n};\n\nconst parsePart = function (part) {\n  let result = {};\n  const name = extractParam('name', part, '=', '\"', '');\n  if (name) {\n    result.name = name;\n  }\n  const filename = extractParam('filename', part, '=', '\"', '');\n  if (filename) {\n    result.filename = filename;\n  }\n  const contentType = extractParam('Content-Type', part, ':', '', ';');\n  if (contentType) {\n    result.contentType = contentType;\n  }\n  if (!filename) {\n    result.value = part.substring(part.indexOf('value=') + 'value='.length);\n  }\n  if (contentType === 'application/json') {\n    result.value = JSON.parse(result.value);\n  }\n  return result;\n};\n\nconst parse = function (req) {\n  const BOUNDARY = 'boundary=';\n  const contentType = req.headers['content-type'];\n  const boundary = '--' + contentType.substring(contentType.indexOf(BOUNDARY) + BOUNDARY.length);\n  const rawBody = req.body.toString();\n  let parts = rawBody.split(boundary).filter((part) => part.length > 0);\n  parts = parts.map((part) => part.trim('\\r\\n'));\n  parts = parts.filter((part) => part != '--');\n  parts = parts.map((part) => part.replace('\\r\\n\\r\\n', ';value='));\n  parts = parts.map((part) => part.replace('\\r\\n', ';'));\n  parts = parts.map((part) => parsePart(part));\n  return parts;\n};\n\nmodule.exports.parse = parse;\nmodule.exports.init = init;\n"
  },
  {
    "path": "packages/bruno-tests/src/multipart/index.js",
    "content": "const express = require('express');\nconst router = express.Router();\nconst formDataParser = require('./form-data-parser');\n\nrouter.post('/mixed-content-types', (req, res) => {\n  const parts = formDataParser.parse(req);\n  return res.json(parts);\n});\n\nmodule.exports = router;\n"
  },
  {
    "path": "packages/bruno-tests/src/redirect/index.js",
    "content": "const express = require('express');\nconst formDataParser = require('../multipart/form-data-parser');\nconst router = express.Router();\n\nconst parseMultipartFormData = (req) => {\n  if (req.headers['content-type'] && req.headers['content-type'].includes('multipart/form-data')) {\n    try {\n      const parts = formDataParser.parse(req);\n      const parsedBody = {};\n      const files = [];\n\n      parts.forEach((part) => {\n        if (part.filename) {\n          files.push({\n            fieldname: part.name,\n            originalname: part.filename,\n            mimetype: part.contentType,\n            size: part.value ? part.value.length : 0\n          });\n        } else {\n          parsedBody[part.name] = part.value;\n        }\n      });\n\n      return { body: parsedBody, files };\n    } catch (error) {\n      console.error('Error parsing multipart form data:', error);\n      return { body: {}, files: [] };\n    }\n  }\n  return { body: req.body, files: [] };\n};\n\nrouter.post('/multipart-redirect-source', function (req, res) {\n  console.log('Multipart redirect source endpoint hit');\n  console.log('Method:', req.method);\n  console.log('Headers:', req.headers);\n\n  const { body, files } = parseMultipartFormData(req);\n  console.log('Parsed Body:', body);\n  console.log('Files:', files);\n\n  res.status(308).location('/api/redirect/multipart-redirect-target').send('Permanently moved');\n});\n\nrouter.post('/multipart-redirect-target', function (req, res) {\n  console.log('Multipart redirect target endpoint hit');\n  console.log('Method:', req.method);\n  console.log('Headers:', req.headers);\n\n  const { body, files } = parseMultipartFormData(req);\n  console.log('Parsed Body:', body);\n  console.log('Files:', files);\n\n  res.json({\n    status: 'success',\n    method: req.method,\n    body: body,\n    files: files,\n    headers: req.headers\n  });\n});\n\nrouter.get('/anything', function (req, res) {\n  const { body, files } = parseMultipartFormData(req);\n\n  // Parse query parameters\n  const args = req.query;\n\n  // Parse form data if present\n  const form = {};\n  if (req.headers['content-type'] && req.headers['content-type'].includes('application/x-www-form-urlencoded')) {\n    Object.assign(form, req.body);\n  }\n\n  // Get origin IP\n  const origin = req.ip || req.connection.remoteAddress || req.socket.remoteAddress;\n\n  // Parse JSON body if present\n  let json = null;\n  let data = '';\n  if (req.headers['content-type'] && req.headers['content-type'].includes('application/json')) {\n    try {\n      json = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;\n      data = typeof req.body === 'string' ? req.body : JSON.stringify(req.body);\n    } catch (e) {\n      data = typeof req.body === 'string' ? req.body : JSON.stringify(req.body);\n    }\n  } else if (req.body) {\n    data = typeof req.body === 'string' ? req.body : JSON.stringify(req.body);\n  }\n\n  res.json({\n    method: req.method,\n    args: args,\n    headers: req.headers,\n    origin: origin,\n    url: req.url,\n    form: form,\n    data: data,\n    json: json,\n    files: files\n  });\n});\n\nrouter.get('/:count', function (req, res) {\n  const count = parseInt(req.params.count, 10);\n\n  // Validate that count is a valid number to prevent infinite redirect loops\n  if (isNaN(count)) {\n    return res.status(404).json({ error: 'Invalid redirect count. Must be a number.' });\n  }\n\n  if (count > 1) {\n    // Redirect to the next redirect in the chain\n    const nextCount = count - 1;\n    res.status(302).set('Location', `/api/redirect/${nextCount}`).send(`<!doctype html>\n          <title>Redirecting...</title>\n          <h1>Redirecting...</h1>\n          <p>You should be redirected automatically to target URL: <a href=\"${nextCount}\">${nextCount}</a>.  If not click the link.</p>\n    `);\n  } else {\n    res.status(302)\n      .set('Location', '/api/redirect/anything')\n      .send(`<!doctype html>\n          <title>Redirecting...</title>\n          <h1>Redirecting...</h1>\n          <p>You should be redirected automatically to target URL: <a href=\"../anything\">../anything</a>.  If not click the link.</p>\n    `);\n  }\n});\n\nmodule.exports = router;\n"
  },
  {
    "path": "packages/bruno-tests/src/utils/xmlParser.js",
    "content": "const { XMLParser } = require('fast-xml-parser');\n\nconst xmlParser = () => {\n  const parser = new XMLParser({\n    ignoreAttributes: false,\n    allowBooleanAttributes: true\n  });\n\n  return (req, res, next) => {\n    if (req.is('application/xml') || req.is('text/xml')) {\n      let data = '';\n      req.setEncoding('utf8');\n      req.on('data', (chunk) => {\n        data += chunk;\n      });\n      req.on('end', () => {\n        try {\n          req.body = parser.parse(data);\n          next();\n        } catch (err) {\n          res.status(400).send('Invalid XML');\n        }\n      });\n    } else {\n      next();\n    }\n  };\n};\n\nmodule.exports = xmlParser;\n"
  },
  {
    "path": "packages/bruno-tests/src/ws/index.js",
    "content": "const ws = require('ws');\n\nconst onSocketError = (err) => {\n  console.error(err);\n};\n\nconst wss = new ws.Server({\n  noServer: true,\n  handleProtocols: (protocols, request) => {\n    if (request.url == '/ws/sub-proto') {\n      if (protocols.has('soap')) {\n        return 'soap';\n      }\n      return false;\n    }\n    return false;\n  }\n});\n\nwss.on('connection', function connection(ws, request) {\n  ws.on('message', function message(data) {\n    const msg = Buffer.from(data).toString().trim();\n    let isJSON = false;\n    let obj = {};\n    try {\n      obj = JSON.parse(msg);\n      isJSON = true;\n    } catch (err) {\n      // Not a json value, don't do any modification\n    }\n    if (isJSON) {\n      if ('func' in obj && obj.func === 'headers') {\n        return ws.send(JSON.stringify({\n          headers: request.headers\n        }));\n      } else if ('func' in obj && obj.func === 'query') {\n        const url = new URL(request.url, `http://${request.headers.host}`);\n        const query = Object.fromEntries(url.searchParams.entries());\n        return ws.send(JSON.stringify({\n          query: query\n        }));\n      } else {\n        return ws.send(JSON.stringify({\n          data: JSON.parse(Buffer.from(data).toString())\n        }));\n      }\n    }\n    return ws.send(Buffer.from(data).toString());\n  });\n});\n\nconst ACCEPTED_SUB_PROTOS = ['soap'];\n\nconst wsRouter = (request, socket, head) => {\n  socket.on('error', onSocketError);\n\n  if (!request.url.startsWith('/ws')) {\n    socket.write('HTTP/1.1 404 Not Found\\r\\n\\r\\n');\n    socket.destroy();\n\n    socket.removeListener('error', onSocketError);\n    return;\n  }\n\n  if (request.url == '/ws/sub-proto') {\n    const subproto = request.headers['sec-websocket-protocol'] || request.headers['Sec-WebSocket-Protocol'];\n    const hasAcceptedProtocols = subproto.split(',').some((d) => ACCEPTED_SUB_PROTOS.includes(d));\n    if (!hasAcceptedProtocols) {\n      const message = 'Unsupported WebSocket subprotocol';\n      socket.write('HTTP/1.1 400 Bad Request\\r\\n'\n        + 'Content-Type: text/plain\\r\\n'\n        + `Content-Length: ${Buffer.byteLength(message)}\\r\\n`\n        + 'Connection: close\\r\\n'\n        + '\\r\\n'\n        + message);\n      socket.destroy();\n      socket.removeListener('error', onSocketError);\n      return;\n    }\n  }\n\n  wss.handleUpgrade(request, socket, head, function done(ws) {\n    wss.emit('connection', ws, request);\n  });\n};\n\nmodule.exports = wsRouter;\n"
  },
  {
    "path": "packages/bruno-toml/lib/stringify",
    "content": "/**\n * Copyright (c) 2016, Rebecca Turner <me@re-becca.org>\n * \n * Permission to use, copy, modify, and/or distribute this software for any\n * purpose with or without fee is hereby granted, provided that the above\n * copyright notice and this permission notice appear in all copies.\n * \n * THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\n * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\n * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n * \n * We have made some modifications to this file to support the bruno-toml\n * You can search for \"modified for bruno-toml\" to see the changes\n */\n\n'use strict'\nmodule.exports = stringify\nmodule.exports.value = stringifyInline\n\nfunction stringify (obj) {\n  if (obj === null) throw typeError('null')\n  if (obj === void (0)) throw typeError('undefined')\n  if (typeof obj !== 'object') throw typeError(typeof obj)\n\n  if (typeof obj.toJSON === 'function') obj = obj.toJSON()\n  if (obj == null) return null\n  const type = tomlType(obj)\n  if (type !== 'table') throw typeError(type)\n  return stringifyObject('', '', obj)\n}\n\nfunction typeError (type) {\n  return new Error('Can only stringify objects, not ' + type)\n}\n\nfunction getInlineKeys (obj) {\n  return Object.keys(obj).filter(key => isInline(obj[key]))\n}\nfunction getComplexKeys (obj) {\n  return Object.keys(obj).filter(key => !isInline(obj[key]))\n}\n\nfunction toJSON (obj) {\n  let nobj = Array.isArray(obj) ? [] : Object.prototype.hasOwnProperty.call(obj, '__proto__') ? {['__proto__']: undefined} : {}\n  for (let prop of Object.keys(obj)) {\n    if (obj[prop] && typeof obj[prop].toJSON === 'function' && !('toISOString' in obj[prop])) {\n      nobj[prop] = obj[prop].toJSON()\n    } else {\n      nobj[prop] = obj[prop]\n    }\n  }\n  return nobj\n}\n\nfunction stringifyObject (prefix, indent, obj) {\n  obj = toJSON(obj)\n  let inlineKeys\n  let complexKeys\n  inlineKeys = getInlineKeys(obj)\n  complexKeys = getComplexKeys(obj)\n  const result = []\n  const inlineIndent = indent || ''\n  inlineKeys.forEach(key => {\n    var type = tomlType(obj[key])\n    if (type !== 'undefined' && type !== 'null') {\n      result.push(inlineIndent + stringifyKey(key) + ' = ' + stringifyAnyInline(obj[key], true))\n    }\n  })\n  if (result.length > 0) result.push('')\n\n  // original\n  // const complexIndent = prefix && inlineKeys.length > 0 ? indent + '  ' : ''\n\n  // modified for bruno-toml\n  // we don't want to indent tables\n  const complexIndent = '';\n\n  complexKeys.forEach(key => {\n    result.push(stringifyComplex(prefix, complexIndent, key, obj[key]))\n  })\n  return result.join('\\n')\n}\n\nfunction isInline (value) {\n  switch (tomlType(value)) {\n    case 'undefined':\n    case 'null':\n    case 'integer':\n    case 'nan':\n    case 'float':\n    case 'boolean':\n    case 'string':\n    case 'datetime':\n      return true\n    case 'array':\n      return value.length === 0 || tomlType(value[0]) !== 'table'\n    case 'table':\n      return Object.keys(value).length === 0\n    /* istanbul ignore next */\n    default:\n      return false\n  }\n}\n\nfunction tomlType (value) {\n  if (value === undefined) {\n    return 'undefined'\n  } else if (value === null) {\n    return 'null'\n  } else if (typeof value === 'bigint' || (Number.isInteger(value) && !Object.is(value, -0))) {\n    return 'integer'\n  } else if (typeof value === 'number') {\n    return 'float'\n  } else if (typeof value === 'boolean') {\n    return 'boolean'\n  } else if (typeof value === 'string') {\n    return 'string'\n  } else if ('toISOString' in value) {\n    return isNaN(value) ? 'undefined' : 'datetime'\n  } else if (Array.isArray(value)) {\n    return 'array'\n  } else {\n    return 'table'\n  }\n}\n\nfunction stringifyKey (key) {\n  const keyStr = String(key)\n  if (/^[-A-Za-z0-9_]+$/.test(keyStr)) {\n    return keyStr\n  } else {\n    return stringifyBasicString(keyStr)\n  }\n}\n\nfunction stringifyBasicString (str) {\n  // original\n  // return '\"' + escapeString(str).replace(/\"/g, '\\\\\"') + '\"'\n\n  // modified for bruno-toml\n  return \"'\" + escapeString(str).replace(/'/g, \"\\\\'\") + \"'\"\n}\n\nfunction stringifyLiteralString (str) {\n  return \"'\" + str + \"'\"\n}\n\nfunction numpad (num, str) {\n  while (str.length < num) str = '0' + str\n  return str\n}\n\nfunction escapeString (str) {\n  return str.replace(/\\\\/g, '\\\\\\\\')\n    .replace(/[\\b]/g, '\\\\b')\n    .replace(/\\t/g, '\\\\t')\n    .replace(/\\n/g, '\\\\n')\n    .replace(/\\f/g, '\\\\f')\n    .replace(/\\r/g, '\\\\r')\n    .replace(/([\\u0000-\\u001f\\u007f])/, c => '\\\\u' + numpad(4, c.codePointAt(0).toString(16)))\n}\n\nfunction stringifyMultilineString (str) {\n  // original\n  // let escaped = str.split(/\\n/).map(str => {\n  //   return escapeString(str).replace(/\"(?=\"\")/g, '\\\\\"')\n  // }).join('\\n')\n  // if (escaped.slice(-1) === '\"') escaped += '\\\\\\n'\n  // return '\"\"\"\\n' + escaped + '\"\"\"'\n\n  // modified for bruno-toml\n  let escaped = str.split(/\\n/).map(str => {\n    return escapeString(str).replace(/'(?='')/g, \"\\\\'\")\n  }).join('\\n')\n  if (escaped.slice(-1) === \"'\") escaped += '\\\\\\n'\n  return \"'''\\n\" + escaped + \"'''\"\n}\n\nfunction stringifyAnyInline (value, multilineOk) {\n  let type = tomlType(value)\n  if (type === 'string') {\n    if (multilineOk && /\\n/.test(value)) {\n      type = 'string-multiline'\n    } else if (!/[\\b\\t\\n\\f\\r']/.test(value) && /\"/.test(value)) {\n      type = 'string-literal'\n    }\n  }\n  return stringifyInline(value, type)\n}\n\nfunction stringifyInline (value, type) {\n  if (!type) type = tomlType(value)\n  switch (type) {\n    case 'string-multiline':\n      return stringifyMultilineString(value)\n    case 'string':\n      return stringifyBasicString(value)\n    case 'string-literal':\n      return stringifyLiteralString(value)\n    case 'integer':\n      return stringifyInteger(value)\n    case 'float':\n      return stringifyFloat(value)\n    case 'boolean':\n      return stringifyBoolean(value)\n    case 'datetime':\n      return stringifyDatetime(value)\n    case 'array':\n      return stringifyInlineArray(value.filter(_ => tomlType(_) !== 'null' && tomlType(_) !== 'undefined' && tomlType(_) !== 'nan'))\n    case 'table':\n      return stringifyInlineTable(value)\n    /* istanbul ignore next */\n    default:\n      throw typeError(type)\n  }\n}\n\nfunction stringifyInteger (value) {\n  return String(value).replace(/\\B(?=(\\d{3})+(?!\\d))/g, '_')\n}\n\nfunction stringifyFloat (value) {\n  if (value === Infinity) {\n    return 'inf'\n  } else if (value === -Infinity) {\n    return '-inf'\n  } else if (Object.is(value, NaN)) {\n    return 'nan'\n  } else if (Object.is(value, -0)) {\n    return '-0.0'\n  }\n  const [int, dec] = String(value).split('.')\n  return stringifyInteger(int) + '.' + dec\n}\n\nfunction stringifyBoolean (value) {\n  return String(value)\n}\n\nfunction stringifyDatetime (value) {\n  return value.toISOString()\n}\n\nfunction stringifyInlineArray (values) {\n  values = toJSON(values)\n  let result = '['\n  const stringified = values.map(_ => stringifyInline(_))\n  if (stringified.join(', ').length > 60 || /\\n/.test(stringified)) {\n    result += '\\n  ' + stringified.join(',\\n  ') + '\\n'\n  } else {\n    result += ' ' + stringified.join(', ') + (stringified.length > 0 ? ' ' : '')\n  }\n  return result + ']'\n}\n\nfunction stringifyInlineTable (value) {\n  value = toJSON(value)\n  const result = []\n  Object.keys(value).forEach(key => {\n    result.push(stringifyKey(key) + ' = ' + stringifyAnyInline(value[key], false))\n  })\n  return '{ ' + result.join(', ') + (result.length > 0 ? ' ' : '') + '}'\n}\n\nfunction stringifyComplex (prefix, indent, key, value) {\n  const valueType = tomlType(value)\n  /* istanbul ignore else */\n  if (valueType === 'array') {\n    return stringifyArrayOfTables(prefix, indent, key, value)\n  } else if (valueType === 'table') {\n    return stringifyComplexTable(prefix, indent, key, value)\n  } else {\n    throw typeError(valueType)\n  }\n}\n\nfunction stringifyArrayOfTables (prefix, indent, key, values) {\n  values = toJSON(values)\n  const firstValueType = tomlType(values[0])\n  /* istanbul ignore if */\n  if (firstValueType !== 'table') throw typeError(firstValueType)\n  const fullKey = prefix + stringifyKey(key)\n  let result = ''\n  values.forEach(table => {\n    if (result.length > 0) result += '\\n'\n    result += indent + '[[' + fullKey + ']]\\n'\n    result += stringifyObject(fullKey + '.', indent, table)\n  })\n  return result\n}\n\nfunction stringifyComplexTable (prefix, indent, key, value) {\n  const fullKey = prefix + stringifyKey(key)\n  let result = ''\n  if (getInlineKeys(value).length > 0) {\n    // original\n    // result += indent + '[' + fullKey + ']\\n'\n\n    // modified for bruno-toml\n    // we don't want to indent tables\n    result += '[' + fullKey + ']\\n'\n  }\n  return result + stringifyObject(fullKey + '.', indent, value)\n}"
  },
  {
    "path": "packages/bruno-toml/package.json",
    "content": "{\n  \"name\": \"@usebruno/toml\",\n  \"version\": \"0.1.0\",\n  \"license\": \"MIT\",\n  \"main\": \"src/index.js\",\n  \"files\": [\n    \"lib\",\n    \"src\",\n    \"package.json\"\n  ],\n  \"scripts\": {\n    \"test\": \"jest\"\n  },\n  \"dependencies\": {\n    \"@iarna/toml\": \"^2.2.5\",\n    \"lodash\": \"^4.17.21\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-toml/src/jsonToToml.js",
    "content": "const stringify = require('../lib/stringify');\nconst { get, each, filter } = require('lodash');\n\nconst keyValPairHasDuplicateKeys = (keyValPair) => {\n  if (!keyValPair || !Array.isArray(keyValPair) || !keyValPair.length) {\n    return false;\n  }\n\n  const names = keyValPair.map((pair) => pair.name);\n  const uniqueNames = new Set(names);\n\n  return names.length !== uniqueNames.size;\n};\n\n// these keys are reserved: disabled, description, enum\nconst keyValPairHasReservedKeys = (keyValPair) => {\n  if (!keyValPair || !Array.isArray(keyValPair) || !keyValPair.length) {\n    return false;\n  }\n\n  const reservedKeys = ['disabled', 'description', 'enum', 'bru'];\n  const names = keyValPair.map((pair) => pair.name);\n\n  return names.some((name) => reservedKeys.includes(name));\n};\n\n/**\n * Json to Toml\n *\n * Note: Bruno always append a new line at the end of text blocks\n *       This is to aid readability when viewing the toml representation of the request\n *       The newline is removed when converting back to json\n *\n * @param {object} json\n * @returns string\n */\nconst jsonToToml = (json) => {\n  const formattedJson = {\n    meta: {\n      name: get(json, 'meta.name'),\n      type: get(json, 'meta.type'),\n      seq: get(json, 'meta.seq')\n    },\n    http: {\n      method: get(json, 'http.method'),\n      url: get(json, 'http.url', '')\n    }\n  };\n\n  if (json.tags && json.tags.length) {\n    formattedJson.tags = get(json, 'tags', []);\n  }\n\n  if (json.headers && json.headers.length) {\n    const hasDuplicateHeaders = keyValPairHasDuplicateKeys(json.headers);\n    const hasReservedHeaders = keyValPairHasReservedKeys(json.headers);\n\n    if (!hasDuplicateHeaders && !hasReservedHeaders) {\n      const enabledHeaders = filter(json.headers, (header) => header.enabled);\n      const disabledHeaders = filter(json.headers, (header) => !header.enabled);\n      each(enabledHeaders, (header) => {\n        formattedJson.headers = formattedJson.headers || {};\n        formattedJson.headers[header.name] = header.value;\n      });\n      each(disabledHeaders, (header) => {\n        formattedJson.headers = formattedJson.headers || {};\n        formattedJson.headers.disabled = formattedJson.headers.disabled || {};\n        formattedJson.headers.disabled[header.name] = header.value;\n      });\n    } else {\n      formattedJson.headers = {\n        bru: JSON.stringify(json.headers, null, 2) + '\\n'\n      };\n    }\n  }\n\n  if (json.script) {\n    let preRequestScript = get(json, 'script.req', '');\n    if (preRequestScript.trim().length > 0) {\n      formattedJson.script = formattedJson.script || {};\n      formattedJson.script['pre-request'] = preRequestScript + '\\n';\n    }\n\n    let postResponseScript = get(json, 'script.res', '');\n    if (postResponseScript.trim().length > 0) {\n      formattedJson.script = formattedJson.script || {};\n      formattedJson.script['post-response'] = postResponseScript + '\\n';\n    }\n  }\n\n  if (json.tests) {\n    let testsScript = get(json, 'tests', '');\n    if (testsScript.trim().length > 0) {\n      formattedJson.script = formattedJson.script || {};\n      formattedJson.script['tests'] = testsScript + '\\n';\n    }\n  }\n\n  if (json.settings && Object.keys(json.settings).length > 0) {\n    formattedJson.settings = {\n      encodeUrl: typeof settings.encodeUrl === 'boolean' ? settings.encodeUrl : settings.encodeUrl === 'true'\n    };\n  }\n\n  return stringify(formattedJson);\n};\n\nmodule.exports = jsonToToml;\n"
  },
  {
    "path": "packages/bruno-toml/src/tomlToJson.js",
    "content": "const Toml = require('@iarna/toml');\nconst { has, each, get } = require('lodash');\n\nconst stripNewlineAtEnd = (str) => {\n  if (!str || typeof str !== 'string') {\n    return '';\n  }\n\n  return str.replace(/\\n$/, '');\n};\n\nconst tomlToJson = (toml) => {\n  const json = Toml.parse(toml);\n\n  const formattedJson = {\n    meta: {\n      name: get(json, 'meta.name', ''),\n      type: get(json, 'meta.type', ''),\n      seq: get(json, 'meta.seq', 0)\n    },\n    http: {\n      method: json.http.method,\n      url: json.http.url\n    }\n  };\n\n  if (json.tags && json.tags.length) {\n    formattedJson.tags = get(json, 'tags', []);\n  }\n\n  if (json.headers) {\n    formattedJson.headers = [];\n\n    // headers are stored in plain json format if they contain duplicate keys\n    // the json is stored in a stringified format in the bru key\n    if (has(json.headers, 'bru')) {\n      let parsedHeaders = JSON.parse(json.headers.bru);\n\n      each(parsedHeaders, (header) => {\n        formattedJson.headers.push({\n          name: header.name,\n          value: header.value,\n          enabled: header.enabled\n        });\n      });\n    } else {\n      Object.keys(json.headers).forEach((key) => {\n        if (key === 'disabled') {\n          Object.keys(json.headers['disabled']).forEach((disabledKey) => {\n            formattedJson.headers.push({\n              name: disabledKey,\n              value: json.headers[key][disabledKey],\n              enabled: false\n            });\n          });\n          return;\n        }\n\n        formattedJson.headers.push({\n          name: key,\n          value: json.headers[key],\n          enabled: true\n        });\n      });\n    }\n  }\n\n  if (json.script) {\n    if (json.script['pre-request']) {\n      formattedJson.script = formattedJson.script || {};\n      formattedJson.script.req = stripNewlineAtEnd(json.script['pre-request']);\n    }\n\n    if (json.script['post-response']) {\n      formattedJson.script = formattedJson.script || {};\n      formattedJson.script.res = stripNewlineAtEnd(json.script['post-response']);\n    }\n\n    if (json.script['tests']) {\n      formattedJson.tests = stripNewlineAtEnd(json.script['tests']);\n    }\n  }\n\n  if (json.settings && Object.keys(json.settings).length > 0) {\n    formattedJson.settings = {\n      encodeUrl: typeof settings.encodeUrl === 'boolean' ? settings.encodeUrl : settings.encodeUrl === 'true'\n    };\n  }\n\n  return formattedJson;\n};\n\nmodule.exports = tomlToJson;\n"
  },
  {
    "path": "packages/bruno-toml/tests/headers/disabled-header/request.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Get users\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"http\": {\n    \"method\": \"GET\",\n    \"url\": \"https://reqres.in/api/users\"\n  },\n  \"headers\": [\n    {\n      \"name\": \"Content-Type\",\n      \"value\": \"application/json\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"Cookie\",\n      \"value\": \"foo=bar\",\n      \"enabled\": false\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/bruno-toml/tests/headers/disabled-header/request.toml",
    "content": "[meta]\nname = 'Get users'\ntype = 'http'\nseq = 1\n\n[http]\nmethod = 'GET'\nurl = 'https://reqres.in/api/users'\n\n[headers]\nContent-Type = 'application/json'\n\n[headers.disabled]\nCookie = 'foo=bar'\n"
  },
  {
    "path": "packages/bruno-toml/tests/headers/dotted-header/request.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Get users\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"http\": {\n    \"method\": \"GET\",\n    \"url\": \"https://reqres.in/api/users\"\n  },\n  \"headers\": [\n    {\n      \"name\": \"Content-Type\",\n      \"value\": \"application/json\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"Dots.In.Header.Key\",\n      \"value\": \"Dots.In.Header.Value\",\n      \"enabled\": true\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/bruno-toml/tests/headers/dotted-header/request.toml",
    "content": "[meta]\nname = 'Get users'\ntype = 'http'\nseq = 1\n\n[http]\nmethod = 'GET'\nurl = 'https://reqres.in/api/users'\n\n[headers]\nContent-Type = 'application/json'\n'Dots.In.Header.Key' = 'Dots.In.Header.Value'\n"
  },
  {
    "path": "packages/bruno-toml/tests/headers/duplicate-header/request.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Get users\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"http\": {\n    \"method\": \"GET\",\n    \"url\": \"https://reqres.in/api/users\"\n  },\n  \"headers\": [\n    {\n      \"name\": \"Content-Type\",\n      \"value\": \"application/json\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"Content-Type\",\n      \"value\": \"application/ld+json\",\n      \"enabled\": true\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/bruno-toml/tests/headers/duplicate-header/request.toml",
    "content": "[meta]\nname = 'Get users'\ntype = 'http'\nseq = 1\n\n[http]\nmethod = 'GET'\nurl = 'https://reqres.in/api/users'\n\n[headers]\nbru = '''\n[\n  {\n    \"name\": \"Content-Type\",\n    \"value\": \"application/json\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"Content-Type\",\n    \"value\": \"application/ld+json\",\n    \"enabled\": true\n  }\n]\n'''\n"
  },
  {
    "path": "packages/bruno-toml/tests/headers/empty-header/request.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Get users\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"http\": {\n    \"method\": \"GET\",\n    \"url\": \"https://reqres.in/api/users\"\n  },\n  \"headers\": [\n    {\n      \"name\": \"Content-Type\",\n      \"value\": \"application/json\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"Empty-Header\",\n      \"value\": \"\",\n      \"enabled\": true\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/bruno-toml/tests/headers/empty-header/request.toml",
    "content": "[meta]\nname = 'Get users'\ntype = 'http'\nseq = 1\n\n[http]\nmethod = 'GET'\nurl = 'https://reqres.in/api/users'\n\n[headers]\nContent-Type = 'application/json'\nEmpty-Header = ''\n"
  },
  {
    "path": "packages/bruno-toml/tests/headers/reserved-header/request.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Get users\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"http\": {\n    \"method\": \"GET\",\n    \"url\": \"https://reqres.in/api/users\"\n  },\n  \"headers\": [\n    {\n      \"name\": \"disabled\",\n      \"value\": \"foo\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"disabled-header-name\",\n      \"value\": \"disabled-header-value\",\n      \"enabled\": false\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/bruno-toml/tests/headers/reserved-header/request.toml",
    "content": "[meta]\nname = 'Get users'\ntype = 'http'\nseq = 1\n\n[http]\nmethod = 'GET'\nurl = 'https://reqres.in/api/users'\n\n[headers]\nbru = '''\n[\n  {\n    \"name\": \"disabled\",\n    \"value\": \"foo\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"disabled-header-name\",\n    \"value\": \"disabled-header-value\",\n    \"enabled\": false\n  }\n]\n'''\n"
  },
  {
    "path": "packages/bruno-toml/tests/headers/simple-header/request.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Get users\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"http\": {\n    \"method\": \"GET\",\n    \"url\": \"https://reqres.in/api/users\"\n  },\n  \"headers\": [\n    {\n      \"name\": \"Content-Type\",\n      \"value\": \"application/json\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"Cookie\",\n      \"value\": \"foo=bar\",\n      \"enabled\": true\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/bruno-toml/tests/headers/simple-header/request.toml",
    "content": "[meta]\nname = 'Get users'\ntype = 'http'\nseq = 1\n\n[http]\nmethod = 'GET'\nurl = 'https://reqres.in/api/users'\n\n[headers]\nContent-Type = 'application/json'\nCookie = 'foo=bar'\n"
  },
  {
    "path": "packages/bruno-toml/tests/headers/spaces-in-header/request.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Get users\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"http\": {\n    \"method\": \"GET\",\n    \"url\": \"https://reqres.in/api/users\"\n  },\n  \"headers\": [\n    {\n      \"name\": \"Content-Type\",\n      \"value\": \"application/json\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"Spaces In Header\",\n      \"value\": \"\",\n      \"enabled\": true\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/bruno-toml/tests/headers/spaces-in-header/request.toml",
    "content": "[meta]\nname = 'Get users'\ntype = 'http'\nseq = 1\n\n[http]\nmethod = 'GET'\nurl = 'https://reqres.in/api/users'\n\n[headers]\nContent-Type = 'application/json'\n'Spaces In Header' = ''\n"
  },
  {
    "path": "packages/bruno-toml/tests/headers/unicode-in-header/request.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Get users\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"http\": {\n    \"method\": \"GET\",\n    \"url\": \"https://reqres.in/api/users\"\n  },\n  \"headers\": [\n    {\n      \"name\": \"Content-Type\",\n      \"value\": \"application/json\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"🐶\",\n      \"value\": \"🚀\",\n      \"enabled\": true\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/bruno-toml/tests/headers/unicode-in-header/request.toml",
    "content": "[meta]\nname = 'Get users'\ntype = 'http'\nseq = 1\n\n[http]\nmethod = 'GET'\nurl = 'https://reqres.in/api/users'\n\n[headers]\nContent-Type = 'application/json'\n'🐶' = '🚀'\n"
  },
  {
    "path": "packages/bruno-toml/tests/index.spec.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst jsonToToml = require('../src/jsonToToml');\nconst tomlToJson = require('../src/tomlToJson');\n\nconst fixtures = [\n  'methods/get',\n  'methods/delete',\n  'headers/simple-header',\n  'headers/empty-header',\n  'headers/spaces-in-header',\n  'headers/unicode-in-header',\n  'headers/disabled-header',\n  'headers/dotted-header',\n  'headers/duplicate-header',\n  'headers/reserved-header',\n  'scripts/pre-request',\n  'scripts/post-response',\n  'scripts/tests'\n];\n\ndescribe('bruno toml', () => {\n  fixtures.forEach((fixture) => {\n    describe(fixture, () => {\n      const json = require(`./${fixture}/request.json`);\n      const toml = fs.readFileSync(path.join(__dirname, fixture, 'request.toml'), 'utf8');\n\n      if (process.env.DEBUG === 'true') {\n        console.log(`DEBUG: Running ${fixture} tests`);\n        console.log('json', JSON.stringify(json, null, 2));\n        console.log('toml', toml);\n        console.log('jsonToToml', jsonToToml(json));\n        console.log('tomlToJson', JSON.stringify(tomlToJson(toml), null, 2));\n      }\n\n      it(`should convert json to toml`, () => {\n        expect(toml).toEqual(jsonToToml(json));\n      });\n\n      it(`should convert toml to json`, () => {\n        expect(json).toEqual(tomlToJson(toml));\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/bruno-toml/tests/methods/delete/request.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Delete User\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"http\": {\n    \"method\": \"DELETE\",\n    \"url\": \"https://reqres.in/api/users/2\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-toml/tests/methods/delete/request.toml",
    "content": "[meta]\nname = 'Delete User'\ntype = 'http'\nseq = 1\n\n[http]\nmethod = 'DELETE'\nurl = 'https://reqres.in/api/users/2'\n"
  },
  {
    "path": "packages/bruno-toml/tests/methods/get/request.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Get users\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"tags\": [\"foo\", \"bar\"],\n  \"http\": {\n    \"method\": \"GET\",\n    \"url\": \"https://reqres.in/api/users\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-toml/tests/methods/get/request.toml",
    "content": "tags = [ 'foo', 'bar' ]\n\n[meta]\nname = 'Get users'\ntype = 'http'\nseq = 1\n\n[http]\nmethod = 'GET'\nurl = 'https://reqres.in/api/users'\n"
  },
  {
    "path": "packages/bruno-toml/tests/scripts/post-response/request.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Get users\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"http\": {\n    \"method\": \"GET\",\n    \"url\": \"https://reqres.in/api/users\"\n  },\n  \"script\": {\n    \"res\": \"bru.setVar('token', res.body.token);\\nconsole.log('token: ' + res.body.token);\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-toml/tests/scripts/post-response/request.toml",
    "content": "[meta]\nname = 'Get users'\ntype = 'http'\nseq = 1\n\n[http]\nmethod = 'GET'\nurl = 'https://reqres.in/api/users'\n\n[script]\npost-response = '''\nbru.setVar('token', res.body.token);\nconsole.log('token: ' + res.body.token);\n'''\n"
  },
  {
    "path": "packages/bruno-toml/tests/scripts/pre-request/request.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Get users\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"http\": {\n    \"method\": \"GET\",\n    \"url\": \"https://reqres.in/api/users\"\n  },\n  \"script\": {\n    \"req\": \"req.body.id = uuid();\"\n  }\n}\n"
  },
  {
    "path": "packages/bruno-toml/tests/scripts/pre-request/request.toml",
    "content": "[meta]\nname = 'Get users'\ntype = 'http'\nseq = 1\n\n[http]\nmethod = 'GET'\nurl = 'https://reqres.in/api/users'\n\n[script]\npre-request = '''\nreq.body.id = uuid();\n'''\n"
  },
  {
    "path": "packages/bruno-toml/tests/scripts/tests/request.json",
    "content": "{\n  \"meta\": {\n    \"name\": \"Get users\",\n    \"type\": \"http\",\n    \"seq\": 1\n  },\n  \"http\": {\n    \"method\": \"GET\",\n    \"url\": \"https://reqres.in/api/users\"\n  },\n  \"tests\": \"test('Status code is 200', function () {\\n  expect(res.statusCode).to.eql(200);\\n});\"\n}\n"
  },
  {
    "path": "packages/bruno-toml/tests/scripts/tests/request.toml",
    "content": "[meta]\nname = 'Get users'\ntype = 'http'\nseq = 1\n\n[http]\nmethod = 'GET'\nurl = 'https://reqres.in/api/users'\n\n[script]\ntests = '''\ntest('Status code is 200', function () {\n  expect(res.statusCode).to.eql(200);\n});\n'''\n"
  },
  {
    "path": "playwright/codegen.ts",
    "content": "const path = require('path');\nconst { startApp } = require('./electron.ts');\n\nasync function main() {\n  const { app, context } = await startApp();\n  let outputFile = process.argv[2]?.trim();\n  if (outputFile && !/\\.(ts|js)$/.test(outputFile)) {\n    outputFile = path.join(__dirname, '../tests/', outputFile + '.spec.ts');\n  }\n  await context._enableRecorder({ language: 'playwright-test', mode: 'recording', outputFile });\n}\n\nmain();\n"
  },
  {
    "path": "playwright/electron.ts",
    "content": "const path = require('path');\nconst { _electron: electron } = require('playwright');\n\nconst electronAppPath = path.join(__dirname, '../packages/bruno-electron');\n\nexports.startApp = async () => {\n  const app = await electron.launch({\n    args: [electronAppPath]\n  });\n  const context = await app.context();\n\n  app.process().stdout.on('data', (data) => {\n    process.stdout.write(data.toString().replace(/^(?=.)/gm, '[Electron] |'));\n  });\n  app.process().stderr.on('data', (error) => {\n    process.stderr.write(error.toString().replace(/^(?=.)/gm, '[Electron] |'));\n  });\n  return { app, context };\n};\n"
  },
  {
    "path": "playwright/index.ts",
    "content": "import { test as baseTest, BrowserContext, ElectronApplication, Page, TestInfo } from '@playwright/test';\nimport * as path from 'path';\nimport * as os from 'os';\nimport * as fs from 'fs';\n\nconst electronAppPath = path.join(__dirname, '../packages/bruno-electron');\n\nconst existsAsync = (filepath: string) => fs.promises.access(filepath).then(() => true).catch(() => false);\n\nasync function recursiveCopy(src: string, dest: string) {\n  if (!await existsAsync(src)) {\n    throw new Error(`${src} doesn't exist`);\n  }\n\n  const files = await fs.promises.readdir(src, {\n    recursive: true,\n    withFileTypes: true\n  });\n\n  for (const file of files) {\n    if (!file.isFile()) continue;\n    const fullPath = path.join(src, file.name);\n    const fullDestPath = path.join(dest, file.name);\n    await fs.promises.copyFile(fullPath, fullDestPath);\n  }\n}\n\nconst TRACING_OPTIONS = { screenshots: true, snapshots: true, sources: true };\n\nfunction isTracingEnabled(testInfo: TestInfo): boolean {\n  return !!(testInfo as any)._tracing.traceOptions();\n}\n\nasync function usePageWithTracing(\n  context: BrowserContext,\n  page: Page,\n  testInfo: TestInfo,\n  use: (page: Page) => Promise<void>,\n  options: { initTracing?: boolean; useChunks?: boolean } = {}\n) {\n  const { initTracing = false, useChunks = true } = options;\n\n  if (!isTracingEnabled(testInfo)) {\n    await use(page);\n    return;\n  }\n\n  const tracePath = testInfo.outputPath(`trace-${testInfo.testId}.zip`);\n\n  if (initTracing) {\n    try {\n      await context.tracing.start(TRACING_OPTIONS);\n    } catch (e) { }\n  }\n\n  if (useChunks) {\n    await context.tracing.startChunk();\n    await use(page);\n    try { await context.tracing.stopChunk({ path: tracePath }); } catch { }\n  } else {\n    await use(page);\n    try { await context.tracing.stop({ path: tracePath }); } catch { }\n  }\n\n  try { await testInfo.attach('trace', { path: tracePath }); } catch { }\n}\n\n/**\n * Gracefully close an Electron app by telling it to exit with code 0.\n * This avoids the macOS \"quit unexpectedly\" crash dialog that appears when\n * app.context().close() kills subprocesses (renderer/GPU) abruptly before\n * the main process can shut down cleanly.\n *\n * Emits 'before-quit' first so cleanup handlers run (e.g., saving cookies to disk),\n * since app.exit() bypasses all lifecycle events.\n */\nexport async function closeElectronApp(app: ElectronApplication) {\n  try {\n    await app.evaluate(async ({ app }) => {\n      app.emit('before-quit');\n\n      // Add a delay to ensure the app is fully closed\n      await new Promise((resolve) => setTimeout(resolve, 250));\n      app.exit(0);\n    });\n  } catch {\n    // Expected: process exited before the CDP response was sent\n  }\n\n  try {\n    await app.close();\n  } catch {\n    // Process already exited\n  }\n}\n\nexport const test = baseTest.extend<\n  {\n    context: BrowserContext;\n    page: Page;\n    newPage: Page;\n    pageWithUserData: Page;\n    collectionFixturePath: string | null;\n    restartApp: (options?: { initUserDataPath?: string }) => Promise<ElectronApplication>;\n  },\n  {\n    createTmpDir: (tag?: string) => Promise<string>;\n    launchElectronApp: (options?: { initUserDataPath?: string; userDataPath?: string; dotEnv?: Record<string, string>; templateVars?: Record<string, string> }) => Promise<ElectronApplication>;\n    electronApp: ElectronApplication;\n    reuseOrLaunchElectronApp: (options?: { initUserDataPath?: string; testFile?: string; userDataPath?: string; dotEnv?: Record<string, string>; templateVars?: Record<string, string>; closePrevious?: boolean }) => Promise<ElectronApplication>;\n  }\n>({\n  createTmpDir: [\n    async ({ }, use) => {\n      const dirs: string[] = [];\n      await use(async (tag?: string) => {\n        const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), `pw-${tag || ''}-`));\n        dirs.push(dir);\n        return dir;\n      });\n      await Promise.all(\n        dirs.map((dir) => fs.promises.rm(dir, { recursive: true, force: true, maxRetries: 10 }).catch((e) => e))\n      );\n    },\n    { scope: 'worker' }\n  ],\n\n  collectionFixturePath: async ({ createTmpDir }, use, testInfo) => {\n    const testDir = path.dirname(testInfo.file);\n    const fixturesDir = path.join(testDir, 'fixtures');\n    // fixtures/collections — multiple named collections (subdirs with bruno.json/opencollection.yml)\n    // fixtures/collection — single collection (single dir with bruno.json/opencollection.yml)\n    const srcPath = [path.join(fixturesDir, 'collections'), path.join(fixturesDir, 'collection')]\n      .find((p) => fs.existsSync(p));\n\n    if (srcPath) {\n      const tmpDir = await createTmpDir(path.basename(srcPath));\n      await fs.promises.cp(srcPath, tmpDir, { recursive: true });\n      await use(tmpDir);\n    } else {\n      await use(null);\n    }\n  },\n\n  launchElectronApp: [\n    async ({ playwright, createTmpDir }, use, workerInfo) => {\n      const apps: ElectronApplication[] = [];\n      await use(async ({ initUserDataPath, userDataPath: providedUserDataPath, dotEnv = {}, templateVars = {} } = {}) => {\n        const userDataPath = providedUserDataPath || (await createTmpDir('electron-userdata'));\n\n        // Ensure dir exists when caller supplies their own path\n        if (providedUserDataPath) {\n          await fs.promises.mkdir(userDataPath, { recursive: true });\n        }\n\n        if (initUserDataPath) {\n          const replacements: Record<string, string> = {\n            projectRoot: path.posix.join(__dirname, '..'),\n            ...templateVars\n          };\n\n          for (const file of await fs.promises.readdir(initUserDataPath)) {\n            let content = await fs.promises.readFile(path.join(initUserDataPath, file), 'utf-8');\n            content = content.replace(/{{(\\w+)}}/g, (_, key) => {\n              if (replacements[key]) {\n                return replacements[key];\n              } else {\n                throw new Error(`\\tNo replacement for {{${key}}} in ${path.join(initUserDataPath, file)}`);\n              }\n            });\n            await fs.promises.writeFile(path.join(userDataPath, file), content, 'utf-8');\n          }\n        } else {\n          // No initUserDataPath provided: create default preferences to skip onboarding\n          // BUT only if preferences.json doesn't already exist\n          const prefsPath = path.join(userDataPath, 'preferences.json');\n          const prefsExist = await existsAsync(prefsPath);\n\n          if (!prefsExist) {\n            const defaultPreferences = {\n              preferences: {\n                onboarding: {\n                  hasLaunchedBefore: true,\n                  hasSeenWelcomeModal: true\n                }\n              }\n            };\n            await fs.promises.writeFile(\n              prefsPath,\n              JSON.stringify(defaultPreferences, null, 2),\n              'utf-8'\n            );\n          }\n        }\n\n        const app = await playwright._electron.launch({\n          args: [electronAppPath, '--disable-gpu'],\n          env: {\n            ...process.env,\n            ELECTRON_USER_DATA_PATH: userDataPath,\n            DISABLE_SAMPLE_COLLECTION_IMPORT: 'true',\n            PLAYWRIGHT: 'true',\n            DISABLE_SINGLE_INSTANCE: 'true',\n            ...dotEnv\n          }\n        });\n\n        const { workerIndex } = workerInfo;\n        const electronProcess = app.process();\n        if (electronProcess?.stdout) {\n          electronProcess.stdout.on('data', (data) => {\n            process.stdout.write(data.toString().replace(/^(?=.)/gm, `[Electron #${workerIndex}] |`));\n          });\n        }\n        if (electronProcess?.stderr) {\n          electronProcess.stderr.on('data', (error) => {\n            process.stderr.write(error.toString().replace(/^(?=.)/gm, `[Electron #${workerIndex}] |`));\n          });\n        }\n\n        apps.push(app);\n        return app;\n      });\n      for (const app of apps) {\n        await closeElectronApp(app);\n      }\n    },\n    { scope: 'worker' }\n  ],\n\n  electronApp: [\n    async ({ launchElectronApp }, use) => {\n      const app = await launchElectronApp();\n      await use(app);\n    },\n    { scope: 'worker' }\n  ],\n\n  context: async ({ electronApp }, use, testInfo) => {\n    const context = await electronApp.context();\n    if (isTracingEnabled(testInfo)) {\n      try {\n        await context.tracing.start(TRACING_OPTIONS);\n      } catch (e) { }\n    }\n    await use(context);\n  },\n\n  page: async ({ electronApp, context }, use, testInfo) => {\n    const page = await electronApp.firstWindow();\n    await usePageWithTracing(context, page, testInfo, use);\n  },\n\n  newPage: async ({ launchElectronApp }, use, testInfo) => {\n    const app = await launchElectronApp();\n    const context = await app.context();\n    const page = await app.firstWindow();\n    await usePageWithTracing(context, page, testInfo, use, { initTracing: true, useChunks: false });\n  },\n\n  reuseOrLaunchElectronApp: [\n    async ({ launchElectronApp }, use, testInfo) => {\n      const apps: Record<string, ElectronApplication> = {};\n      await use(async ({ initUserDataPath, testFile, userDataPath, dotEnv = {}, templateVars = {}, closePrevious = false } = {}) => {\n        const key = testFile || userDataPath || initUserDataPath;\n        if (key && apps[key]) {\n          if (closePrevious) {\n            await closeElectronApp(apps[key]);\n            delete apps[key];\n          } else {\n            return apps[key];\n          }\n        }\n\n        // Close other cached apps to prevent resource accumulation across test files\n        for (const existingKey of Object.keys(apps)) {\n          if (existingKey !== key) {\n            await closeElectronApp(apps[existingKey]);\n            delete apps[existingKey];\n          }\n        }\n\n        const app = await launchElectronApp({ initUserDataPath, userDataPath, dotEnv, templateVars });\n        if (key) {\n          apps[key] = app;\n        }\n        return app;\n      });\n    },\n    { scope: 'worker' }\n  ],\n\n  restartApp: async ({ reuseOrLaunchElectronApp, createTmpDir, collectionFixturePath }, use, testInfo) => {\n    await use(async ({ initUserDataPath } = {}) => {\n      const testDir = path.dirname(testInfo.file);\n      const defaultInitUserDataPath = path.join(testDir, 'init-user-data');\n\n      let srcUserDataPath = initUserDataPath;\n      if (!srcUserDataPath) {\n        const hasInitUserData = await fs.promises.stat(defaultInitUserDataPath).catch(() => false);\n        srcUserDataPath = hasInitUserData ? defaultInitUserDataPath : undefined;\n      }\n\n      // Copy init-user-data to a fresh tmp dir (same as pageWithUserData)\n      const tmpAppDataDir = await createTmpDir();\n      if (srcUserDataPath) {\n        await recursiveCopy(srcUserDataPath, tmpAppDataDir);\n      }\n\n      const templateVars: Record<string, string> = {};\n      if (collectionFixturePath) {\n        templateVars.collectionPath = collectionFixturePath;\n      }\n\n      // Close the previous app (from pageWithUserData) before launching a new one\n      return await reuseOrLaunchElectronApp({\n        initUserDataPath: tmpAppDataDir,\n        testFile: testInfo.file,\n        templateVars,\n        closePrevious: true\n      });\n    });\n  },\n\n  pageWithUserData: async ({ reuseOrLaunchElectronApp, createTmpDir, collectionFixturePath }, use, testInfo) => {\n    const testDir = path.dirname(testInfo.file);\n    const initUserDataPath = path.join(testDir, 'init-user-data');\n\n    const tmpAppDataDir = await createTmpDir();\n    try {\n      await recursiveCopy(initUserDataPath, tmpAppDataDir);\n    } catch (err) {\n      if (err instanceof Error && err.message.includes('doesn\\'t exist')) {\n        throw new Error(`${initUserDataPath} doesn't exist, either add one or if you don't need an initial state then use the \\`page\\` fixture instead of \\`pageWithUserData\\`.`);\n      }\n      throw err;\n    }\n\n    const templateVars: Record<string, string> = {};\n    if (collectionFixturePath) {\n      templateVars.collectionPath = collectionFixturePath;\n    }\n\n    const app = await reuseOrLaunchElectronApp({ initUserDataPath: tmpAppDataDir, testFile: testInfo.file, templateVars });\n\n    const context = await app.context();\n    const page = await app.firstWindow();\n\n    // Wait for app to be ready\n    await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n    await usePageWithTracing(context, page, testInfo, use, { initTracing: true });\n  }\n});\n\nexport * from '@playwright/test';\n"
  },
  {
    "path": "playwright.config.ts",
    "content": "import { defineConfig } from '@playwright/test';\n\nconst reporter: any[] = [['list'], ['html'], ['json', { outputFile: 'playwright-report/results.json' }]];\n\nif (process.env.CI) {\n  reporter.push(['github']);\n}\n\nexport default defineConfig({\n  fullyParallel: false,\n  forbidOnly: !!process.env.CI,\n  retries: process.env.CI ? 2 : 0,\n  workers: process.env.CI ? undefined : 1,\n  reporter,\n\n  use: {\n    trace: process.env.CI ? 'on-first-retry' : 'on'\n  },\n\n  projects: [\n    {\n      name: 'default',\n      testDir: './tests',\n      testIgnore: [\n        'ssl/**' // custom CA certificate tests require separate server setup and certificate generation\n      ]\n    },\n    {\n      name: 'ssl',\n      testDir: './tests/ssl'\n    }\n  ],\n\n  webServer: [\n    {\n      command: 'npm run dev:web',\n      url: 'http://localhost:3000',\n      reuseExistingServer: !process.env.CI,\n      timeout: 10 * 60 * 1000\n    },\n    {\n      command: 'npm start --workspace=packages/bruno-tests',\n      url: 'http://localhost:8081/ping',\n      reuseExistingServer: !process.env.CI,\n      timeout: 10 * 60 * 1000\n    }\n  ]\n});\n"
  },
  {
    "path": "publishing.md",
    "content": "**English**\n| [Türkçe](docs/publishing/publishing_tr.md)\n| [Deutsch](docs/publishing/publishing_de.md)\n| [Français](docs/publishing/publishing_fr.md)\n| [Português (BR)](docs/publishing/publishing_pt_br.md)\n| [বাংলা](docs/publishing/publishing_bn.md)\n| [Română](docs/publishing/publishing_ro.md)\n| [Polski](docs/publishing/publishing_pl.md)\n| [简体中文](docs/publishing/publishing_cn.md)\n| [正體中文](docs/publishing/publishing_zhtw.md)\n| [日本語](docs/publishing/publishing_ja.md)\n| [Nederlands](docs/publishing/publishing_nl.md)\n| [فارسی](docs/publishing/publishing_fa.md)\n\n### Publishing Bruno to a new package manager\n\nWhile our code is open source and available for everyone to use, we kindly request that you reach out to us before considering publication on new package managers. As the creator of Bruno, I hold the trademark `Bruno` for this project and would like to manage its distribution. If you'd like to see Bruno on a new package manager, please raise a GitHub issue.\n\nWhile the majority of our features are free and open source (which covers REST and GraphQL Apis),\nwe strive to strike a harmonious balance between open-source principles and sustainability - https://github.com/usebruno/bruno/discussions/269\n"
  },
  {
    "path": "readme.md",
    "content": "<br />\n<img src=\"assets/images/logo-transparent.png\" width=\"80\"/>\n\n### Bruno - Opensource IDE for exploring and testing APIs.\n\n[![GitHub version](https://badge.fury.io/gh/usebruno%2Fbruno.svg)](https://badge.fury.io/gh/usebruno%2Fbruno)\n[![CI](https://github.com/usebruno/bruno/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/usebruno/bruno/actions/workflows/tests.yml)\n[![Commit Activity](https://img.shields.io/github/commit-activity/m/usebruno/bruno)](https://github.com/usebruno/bruno/pulse)\n[![X](https://img.shields.io/twitter/follow/use_bruno?style=social&logo=x)](https://twitter.com/use_bruno)\n[![Website](https://img.shields.io/badge/Website-Visit-blue)](https://www.usebruno.com)\n[![Download](https://img.shields.io/badge/Download-Latest-brightgreen)](https://www.usebruno.com/downloads)\n\n**English**\n| [Українська](docs/readme/readme_ua.md)\n| [Русский](docs/readme/readme_ru.md)\n| [Türkçe](docs/readme/readme_tr.md)\n| [Deutsch](docs/readme/readme_de.md)\n| [Français](docs/readme/readme_fr.md)\n| [Português (BR)](docs/readme/readme_pt_br.md)\n| [한국어](docs/readme/readme_kr.md)\n| [বাংলা](docs/readme/readme_bn.md)\n| [Español](docs/readme/readme_es.md)\n| [Italiano](docs/readme/readme_it.md)\n| [Română](docs/readme/readme_ro.md)\n| [Polski](docs/readme/readme_pl.md)\n| [简体中文](docs/readme/readme_cn.md)\n| [正體中文](docs/readme/readme_zhtw.md)\n| [العربية](docs/readme/readme_ar.md)\n| [日本語](docs/readme/readme_ja.md)\n| [ქართული](docs/readme/readme_ka.md)\n| [Nederlands](docs/readme/readme_nl.md)\n| [فارسی](docs/readme/readme_fa.md)\n\nBruno is a new and innovative API client, aimed at revolutionizing the status quo represented by Postman and similar tools out there.\n\nBruno stores your collections directly in a folder on your filesystem. We use a plain text markup language, Bru, to save information about API requests.\n\nYou can use Git or any version control of your choice to collaborate over your API collections.\n\nBruno is offline-only. There are no plans to add cloud-sync to Bruno, ever. We value your data privacy and believe it should stay on your device. Read our long-term vision [here](https://github.com/usebruno/bruno/discussions/269)\n\n[Download Bruno](https://www.usebruno.com/downloads)\n\n📢 Watch our recent talk at India FOSS 3.0 Conference [here](https://www.youtube.com/watch?v=7bSMFpbcPiY)\n\n![bruno](assets/images/landing-2-dark.png#gh-light-mode-only)\n![bruno](assets/images/landing-2-light.png#gh-dark-mode-only) <br /><br />\n\n## Commercial Versions ✨\n\nMajority of our features are free and open source.\nWe strive to strike a harmonious balance between [open-source principles and sustainability](https://github.com/usebruno/bruno/discussions/269)\n\nYou can explore our [paid versions](https://www.usebruno.com/pricing) to see if there are additional features that you or your team may find useful! <br/>\n\n## Table of Contents\n\n- [Installation](#installation)\n- [Features](#features)\n  - [Run across multiple platforms 🖥️](#run-across-multiple-platforms-%EF%B8%8F)\n  - [Collaborate via Git 👩‍💻🧑‍💻](#collaborate-via-git-%E2%80%8D%E2%80%8D)\n- [Important Links 📌](#important-links-)\n- [Showcase 🎥](#showcase-)\n- [Share Testimonials 📣](#share-testimonials-)\n- [Publishing to New Package Managers](#publishing-to-new-package-managers)\n- [Stay in touch 🌐](#stay-in-touch-)\n- [Trademark](#trademark)\n- [Contribute 👩‍💻🧑‍💻](#contribute-%E2%80%8D%E2%80%8D)\n- [Authors](#authors)\n- [License 📄](#license-)\n\n## Installation\n\nBruno is available as binary download [on our website](https://www.usebruno.com/downloads) for Mac, Windows and Linux.\n\nYou can also install Bruno via package managers like Homebrew, Chocolatey, Scoop, Snap, Flatpak and Apt.\n\n```sh\n# On Mac via Homebrew\nbrew install bruno\n\n# On Windows via Chocolatey\nchoco install bruno\n\n# On Windows via Scoop\nscoop bucket add extras\nscoop install bruno\n\n# On Windows via winget\nwinget install Bruno.Bruno\n\n# On Linux via Snap\nsnap install bruno\n\n# On Linux via Flatpak\nflatpak install com.usebruno.Bruno\n\n# On Arch Linux via AUR\nyay -S bruno\n\n# On Linux via Apt\nsudo mkdir -p /etc/apt/keyrings\nsudo apt update && sudo apt install gpg curl\ncurl -fsSL \"https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x9FA6017ECABE0266\" \\\n  | gpg --dearmor \\\n  | sudo tee /etc/apt/keyrings/bruno.gpg > /dev/null\nsudo chmod 644 /etc/apt/keyrings/bruno.gpg\necho \"deb [arch=amd64 signed-by=/etc/apt/keyrings/bruno.gpg] http://debian.usebruno.com/ bruno stable\" \\\n  | sudo tee /etc/apt/sources.list.d/bruno.list\nsudo apt update && sudo apt install bruno\n```\n\n## Features\n\n### Run across multiple platforms 🖥️\n\n![bruno](assets/images/run-anywhere.png) <br /><br />\n\n### Collaborate via Git 👩‍💻🧑‍💻\n\nOr any version control system of your choice\n\n![bruno](assets/images/version-control.png) <br /><br />\n\n## Important Links 📌\n\n- [Our Long Term Vision](https://github.com/usebruno/bruno/discussions/269)\n- [Roadmap](https://www.usebruno.com/roadmap)\n- [Documentation](https://docs.usebruno.com)\n- [Stack Overflow](https://stackoverflow.com/questions/tagged/bruno)\n- [Website](https://www.usebruno.com)\n- [Pricing](https://www.usebruno.com/pricing)\n- [Download](https://www.usebruno.com/downloads)\n\n## Showcase 🎥\n\n- [Testimonials](https://github.com/usebruno/bruno/discussions/343)\n- [Knowledge Hub](https://github.com/usebruno/bruno/discussions/386)\n- [Scriptmania](https://github.com/usebruno/bruno/discussions/385)\n\n## Share Testimonials 📣\n\nIf Bruno has helped you at work and your teams, please don't forget to share your [testimonials on our GitHub discussion](https://github.com/usebruno/bruno/discussions/343)\n\n## Publishing to New Package Managers\n\nPlease see [here](publishing.md) for more information.\n\n## Stay in touch 🌐\n\n[𝕏 (Twitter)](https://twitter.com/use_bruno) <br />\n[Website](https://www.usebruno.com) <br />\n[Discord](https://discord.com/invite/KgcZUncpjq) <br />\n[LinkedIn](https://www.linkedin.com/company/usebruno)\n\n## Trademark\n\n**Name**\n\n`Bruno` is a trademark held by [Anoop M D](https://www.helloanoop.com/)\n\n**Logo**\n\nThe logo is sourced from [OpenMoji](https://openmoji.org/library/emoji-1F436/). License: CC [BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)\n\n## Contribute 👩‍💻🧑‍💻\n\nI am happy that you are looking to improve bruno. Please check out the [contributing guide](contributing.md)\n\nEven if you are not able to make contributions via code, please don't hesitate to file bugs and feature requests that needs to be implemented to solve your use case.\n\n## Authors\n\n<div align=\"center\">\n    <a href=\"https://github.com/usebruno/bruno/graphs/contributors\">\n        <img src=\"https://contrib.rocks/image?repo=usebruno/bruno\" />\n    </a>\n</div>\n\n## License 📄\n\n[MIT](license.md)\n"
  },
  {
    "path": "scripts/build-electron.js",
    "content": "const os = require('os');\nconst fs = require('fs-extra');\nconst util = require('util');\nconst spawn = util.promisify(require('child_process').spawn);\nconst path = require('path');\n\nasync function deleteFileIfExists(filePath) {\n  try {\n    const exists = await fs.pathExists(filePath);\n    if (exists) {\n      await fs.remove(filePath);\n      console.log(`${filePath} has been successfully deleted.`);\n    } else {\n      console.log(`${filePath} does not exist.`);\n    }\n  } catch (err) {\n    console.error(`Error while checking the existence of ${filePath}: ${err}`);\n  }\n}\n\nasync function copyFolderIfExists(srcPath, destPath) {\n  try {\n    const exists = await fs.pathExists(srcPath);\n    if (exists) {\n      await fs.copy(srcPath, destPath);\n      console.log(`${srcPath} has been successfully copied.`);\n    } else {\n      console.log(`${srcPath} was not copied as it does not exist.`);\n    }\n  } catch (err) {\n    console.error(`Error while checking the existence of ${srcPath}: ${err}`);\n  }\n}\n\nasync function removeSourceMapFiles(directory) {\n  try {\n    const files = await fs.readdir(directory);\n    for (const file of files) {\n      if (file.endsWith('.map')) {\n        const filePath = path.join(directory, file);\n        await fs.remove(filePath);\n        console.log(`${filePath} has been successfully deleted.`);\n      }\n    }\n  } catch (error) {\n    console.error(`Error while deleting .map files: ${error}`);\n  }\n}\n\nasync function execCommandWithOutput(command) {\n  return new Promise(async (resolve, reject) => {\n    const childProcess = await spawn(command, {\n      stdio: 'inherit',\n      shell: true\n    });\n    childProcess.on('error', (error) => {\n      reject(error);\n    });\n    childProcess.on('exit', (code) => {\n      if (code === 0) {\n        resolve();\n      } else {\n        reject(new Error(`Command exited with code ${code}.`));\n      }\n    });\n  });\n}\n\nasync function main() {\n  try {\n    // Remove out directory\n    await deleteFileIfExists('packages/bruno-electron/out');\n\n    // Remove web directory\n    await deleteFileIfExists('packages/bruno-electron/web');\n\n    // Create a new web directory\n    await fs.ensureDir('packages/bruno-electron/web');\n    console.log('The directory has been created successfully!');\n\n    // Copy build\n    await copyFolderIfExists('packages/bruno-app/dist', 'packages/bruno-electron/web');\n\n    // Update static paths\n    const files = await fs.readdir('packages/bruno-electron/web');\n    for (const file of files) {\n      if (file.endsWith('.html')) {\n        let content = await fs.readFile(`packages/bruno-electron/web/${file}`, 'utf8');\n        content = content.replace(/\\/static/g, './static');\n        await fs.writeFile(`packages/bruno-electron/web/${file}`, content);\n      }\n    }\n\n    // update font load paths\n    const cssDir = path.join('packages/bruno-electron/web/static/css');\n    try {\n      const cssFiles = await fs.readdir(cssDir);\n      for (const file of cssFiles) {\n        if (file.endsWith('.css')) {\n          const filePath = path.join(cssDir, file);\n          let content = await fs.readFile(filePath, 'utf8');\n          content = content.replace(/\\/static\\/font/g, '../../static/font');\n          await fs.writeFile(filePath, content);\n        }\n      }\n    } catch (error) {\n      console.error(`Error updating font paths: ${error}`);\n    }\n\n    // Remove sourcemaps\n    await removeSourceMapFiles('packages/bruno-electron/web');\n\n    // Run npm dist command\n    console.log('Building the Electron distribution');\n\n    // Determine the OS and set the appropriate argument\n    let osArg;\n    if (os.platform() === 'win32') {\n      osArg = 'win';\n    } else if (os.platform() === 'darwin') {\n      osArg = 'mac';\n    } else {\n      osArg = 'linux';\n    }\n\n    await execCommandWithOutput(`npm run dist:${osArg} --workspace=packages/bruno-electron`);\n  } catch (error) {\n    console.error('An error occurred:', error);\n  }\n}\n\nmain();\n"
  },
  {
    "path": "scripts/build-electron.sh",
    "content": "#!/bin/bash\n\n# Remove out directory\nrm -rf packages/bruno-electron/out\n\n# Remove web directory\nrm -rf packages/bruno-electron/web\n\n# Create a new web directory\nmkdir packages/bruno-electron/web\n\n# Copy build\ncp -r packages/bruno-app/dist/* packages/bruno-electron/web\n\n\n# Update static paths\nsed -i'' -e 's@/static/@static/@g' packages/bruno-electron/web/**.html\nsed -i'' -e 's@/static/font@../../static/font@g' packages/bruno-electron/web/static/css/**.**.css\n\n# Remove sourcemaps\nfind packages/bruno-electron/web -name '*.map' -type f -delete\n\nif [ \"$1\" == \"snap\" ]; then\n  echo \"Building snap distribution\"\n  npm run dist:snap --workspace=packages/bruno-electron \nelif [ \"$1\" == \"mac\" ]; then\n  echo \"Building mac distribution\"\n  npm run dist:mac --workspace=packages/bruno-electron\nelif [ \"$1\" == \"win\" ]; then\n  echo \"Building windows distribution\"\n  npm run dist:win --workspace=packages/bruno-electron\nelif [ \"$1\" == \"deb\" ]; then\n  echo \"Building debian distribution\"\n  npm run dist:deb --workspace=packages/bruno-electron\nelif [ \"$1\" == \"rpm\" ]; then\n  echo \"Building rpm distribution\"\n  npm run dist:rpm --workspace=packages/bruno-electron\nelif [ \"$1\" == \"linux\" ]; then\n  echo \"Building linux distribution\"\n  npm run dist:linux --workspace=packages/bruno-electron\nelse\n  echo \"Please pass a build distribution type\"\nfi"
  },
  {
    "path": "scripts/changed-packages.js",
    "content": "#!/usr/bin/env node\nconst { execSync } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\n\n/**\n * changed-packages.js\n *\n * Usage:\n *   node scripts/changed-packages.js <ref>\n *\n * Examples:\n *   node scripts/changed-packages.js main\n *   node scripts/changed-packages.js v1.2.3\n *\n * Description:\n *   Prints the top-level package directories under `packages/` that\n *   have changed since <ref>, reads their package names, and prints\n *   both the dependency tree (internal packages it depends on) and\n *   the dependent tree (internal packages that depend on it).\n *\n * Options:\n *   -h, --help    Show this help message\n */\n\nconst USAGE = [\n  'Usage:',\n  '  node scripts/changed-packages.js <ref>',\n  '',\n  'Examples:',\n  '  node scripts/changed-packages.js main',\n  '  node scripts/changed-packages.js v1.2.3',\n  '',\n  'Description:',\n  '  Print package directories under packages/ that have changed since <ref>,',\n  '  and show their internal dependency and dependent trees.',\n  '',\n  'Options:',\n  '  -h, --help    Show this help message'\n].join('\\n');\n\nconst ref = process.argv.slice(2)[0];\n\nif (!ref || ['-h', '--help'].includes(ref)) {\n  console.log(USAGE);\n  process.exit(0);\n}\n\n// Validate ref exists\ntry {\n  const getRefs = execSync(`git show-ref`);\n  const refs = getRefs.toString().split('\\n').filter((d) => d.includes('refs/heads') || d.includes('refs/tags')).map((d) => {\n    const [_, refPath] = d.split(/\\s+/);\n    return refPath.replace('refs/heads/', '').replace('refs/tags/', '');\n  });\n\n  if (!refs.includes(ref)) {\n    console.error('The passed in Ref cannot be found');\n    process.exit(1);\n  }\n} catch (err) {\n  console.error('Error checking git refs:', err.message);\n  process.exit(1);\n}\n\n// Get changed files since ref and map to top-level package directories\nlet changedFiles = [];\ntry {\n  changedFiles = execSync(`git diff --name-only ${ref}`).toString().split('\\n').filter(Boolean);\n} catch (err) {\n  console.error('Error running git diff:', err.message);\n  process.exit(1);\n}\n\nconst changedPackageDirs = Array.from(new Set(changedFiles.map((f) => {\n  const parts = f.split('/');\n  if (parts[0] === 'packages' && parts.length >= 2) {\n    return `packages/${parts[1]}`;\n  }\n  return null;\n}).filter(Boolean))).sort();\n\nif (changedPackageDirs.length === 0) {\n  console.log('No changed packages found since', ref);\n  process.exit(0);\n}\n\n// Build map of all packages in packages/ -> name and their internal dependencies\nconst packagesRoot = path.join(process.cwd(), 'packages');\nconst allPackageDirs = fs.readdirSync(packagesRoot).filter((d) => {\n  try {\n    return fs.statSync(path.join(packagesRoot, d)).isDirectory();\n  } catch (e) {\n    return false;\n  }\n});\n\nconst packageNameByDir = {}; // 'packages/foo' -> '@scope/foo'\nconst packageDirByName = {}; // '@scope/foo' -> 'packages/foo'\nconst rawPackageJsonByName = {}; // name -> package.json contents\n\nallPackageDirs.forEach((d) => {\n  const pkgJsonPath = path.join(packagesRoot, d, 'package.json');\n  try {\n    const raw = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));\n    if (raw && raw.name) {\n      const dir = `packages/${d}`;\n      packageNameByDir[dir] = raw.name;\n      packageDirByName[raw.name] = dir;\n      rawPackageJsonByName[raw.name] = raw;\n    }\n  } catch (e) {\n    // ignore directories without valid package.json\n  }\n});\n\nconst packageNames = Object.keys(packageDirByName);\n\n// Build dependency maps (only internal package deps)\nconst depsMap = {}; // pkgName -> [internal dep names]\nconst dependentsMap = {}; // pkgName -> Set(internal dependent names)\n\npackageNames.forEach((name) => {\n  const pkg = rawPackageJsonByName[name] || {};\n  const allDeps = Object.assign({}, pkg.dependencies || {}, pkg.devDependencies || {}, pkg.peerDependencies || {});\n  const internalDeps = Object.keys(allDeps).filter((depName) => packageNames.includes(depName));\n  depsMap[name] = internalDeps;\n  internalDeps.forEach((dep) => {\n    dependentsMap[dep] = dependentsMap[dep] || new Set();\n    dependentsMap[dep].add(name);\n  });\n});\n\nfunction printTree(rootName, map, seen = new Set(), indent = '') {\n  if (!map[rootName] || map[rootName].length === 0) return ''; // no children\n  let out = '';\n  const children = map[rootName];\n  children.forEach((child) => {\n    if (seen.has(child)) {\n      out += `${indent}- ${child} (cycle)\\n`;\n      return;\n    }\n    out += `${indent}- ${child}\\n`;\n    seen.add(child);\n    // For dependentsMap value is Set, convert to Array\n    const nextChildren = Array.isArray(map[child]) ? map[child] : (map[child] ? Array.from(map[child]) : []);\n    if (nextChildren.length > 0) {\n      out += printTree(child, map, seen, indent + '  ');\n    }\n  });\n  return out;\n}\n\n// For dependentsMap, convert sets to arrays for printing\nconst dependentsMapArr = {};\nObject.keys(dependentsMap).forEach((k) => {\n  dependentsMapArr[k] = Array.from(dependentsMap[k]);\n});\n\n// Build bottom-up tree for changed packages\nconst changedPackageNames = changedPackageDirs.map((d) => packageNameByDir[d]).filter(Boolean);\n\nfunction getTransitiveDependents(pkgName, visited = new Set()) {\n  if (visited.has(pkgName)) return [];\n  visited.add(pkgName);\n\n  const directDependents = Array.from(dependentsMap[pkgName] || []);\n  let result = [];\n\n  directDependents.forEach((dependent) => {\n    if (changedPackageNames.includes(dependent)) {\n      result.push(dependent);\n    }\n    result.push(...getTransitiveDependents(dependent, visited));\n  });\n\n  return result;\n}\n\nfunction buildUpdateOrder(changedPackages) {\n  const levels = [];\n  let remaining = new Set(changedPackages);\n\n  while (remaining.size > 0) {\n    const currentLevel = [];\n    const nextLevel = [];\n\n    remaining.forEach((pkg) => {\n      const deps = depsMap[pkg] || [];\n      const depsInRemaining = deps.filter((d) => remaining.has(d));\n\n      if (depsInRemaining.length === 0) {\n        currentLevel.push(pkg);\n      } else {\n        nextLevel.push(pkg);\n      }\n    });\n\n    if (currentLevel.length === 0) {\n      break;\n    }\n\n    currentLevel.forEach((pkg) => remaining.delete(pkg));\n    levels.push(currentLevel.sort());\n  }\n\n  return levels;\n}\n\nconsole.log('='.repeat(80));\nconsole.log('Changed packages since', ref);\nconsole.log('='.repeat(80));\nconsole.log();\n\nconst updateLevels = buildUpdateOrder(changedPackageNames);\n\nif (updateLevels.length === 0) {\n  console.log('No changed packages found.');\n  process.exit(0);\n}\n\nupdateLevels.forEach((level, idx) => {\n  console.log(`Level ${idx + 1}:`);\n  level.forEach((pkgName) => {\n    const dir = packageDirByName[pkgName];\n    console.log(`  ${dir || pkgName} -> ${pkgName}`);\n    const transitiveDependents = getTransitiveDependents(pkgName);\n    if (transitiveDependents.length > 0) {\n      console.log(`    ├─ Dependent packages: ${transitiveDependents.join(', ')}`);\n    }\n  });\n  console.log();\n});\n\nconsole.log('='.repeat(80));\n"
  },
  {
    "path": "scripts/count-locs.js",
    "content": "#!/usr/bin/env node\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst PACKAGES_DIR = path.join(__dirname, '..', 'packages');\nconst EXCLUDE_DIRS = ['node_modules', 'dist', 'build', '.next', 'coverage', '.git'];\nconst EXCLUDE_PACKAGES = ['bruno-toml', 'bruno-tests', 'bruno-docs'];\nconst CODE_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.css', '.scss', '.json', '.md'];\n\nfunction countLinesInFile(filePath) {\n  try {\n    const content = fs.readFileSync(filePath, 'utf-8');\n    return content.split('\\n').length;\n  } catch (error) {\n    return 0;\n  }\n}\n\nfunction shouldExcludeDir(dirName) {\n  return EXCLUDE_DIRS.includes(dirName) || dirName.startsWith('.');\n}\n\nfunction isCodeFile(fileName) {\n  return CODE_EXTENSIONS.some(ext => fileName.endsWith(ext));\n}\n\nfunction countLinesInDirectory(dirPath) {\n  let totalLines = 0;\n  let fileCount = 0;\n  \n  function walkDir(currentPath) {\n    const items = fs.readdirSync(currentPath);\n    \n    for (const item of items) {\n      const itemPath = path.join(currentPath, item);\n      const stat = fs.statSync(itemPath);\n      \n      if (stat.isDirectory()) {\n        if (!shouldExcludeDir(item)) {\n          walkDir(itemPath);\n        }\n      } else if (stat.isFile() && isCodeFile(item)) {\n        const lines = countLinesInFile(itemPath);\n        totalLines += lines;\n        fileCount++;\n      }\n    }\n  }\n  \n  walkDir(dirPath);\n  return { totalLines, fileCount };\n}\n\nfunction getPackages() {\n  const packages = [];\n  const items = fs.readdirSync(PACKAGES_DIR);\n  \n  for (const item of items) {\n    const itemPath = path.join(PACKAGES_DIR, item);\n    const stat = fs.statSync(itemPath);\n    \n    if (stat.isDirectory() && !shouldExcludeDir(item) && !EXCLUDE_PACKAGES.includes(item)) {\n      packages.push({\n        name: item,\n        path: itemPath\n      });\n    }\n  }\n  \n  return packages;\n}\n\nfunction formatNumber(num) {\n  return num.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',');\n}\n\nfunction printTable(data) {\n  // Calculate column widths\n  const nameWidth = Math.max(20, ...data.map(d => d.name.length));\n  const locWidth = 12;\n  const filesWidth = 12;\n  \n  // Header\n  console.log('\\n┌' + '─'.repeat(nameWidth + 2) + '┬' + '─'.repeat(locWidth + 2) + '┬' + '─'.repeat(filesWidth + 2) + '┐');\n  console.log(`│ ${'Package'.padEnd(nameWidth)} │ ${'LOC'.padStart(locWidth)} │ ${'Files'.padStart(filesWidth)} │`);\n  console.log('├' + '─'.repeat(nameWidth + 2) + '┼' + '─'.repeat(locWidth + 2) + '┼' + '─'.repeat(filesWidth + 2) + '┤');\n  \n  // Data rows\n  let totalLOC = 0;\n  let totalFiles = 0;\n  \n  for (const row of data) {\n    console.log(`│ ${row.name.padEnd(nameWidth)} │ ${formatNumber(row.loc).padStart(locWidth)} │ ${formatNumber(row.files).padStart(filesWidth)} │`);\n    totalLOC += row.loc;\n    totalFiles += row.files;\n  }\n  \n  // Footer\n  console.log('├' + '─'.repeat(nameWidth + 2) + '┼' + '─'.repeat(locWidth + 2) + '┼' + '─'.repeat(filesWidth + 2) + '┤');\n  console.log(`│ ${'TOTAL'.padEnd(nameWidth)} │ ${formatNumber(totalLOC).padStart(locWidth)} │ ${formatNumber(totalFiles).padStart(filesWidth)} │`);\n  console.log('└' + '─'.repeat(nameWidth + 2) + '┴' + '─'.repeat(locWidth + 2) + '┴' + '─'.repeat(filesWidth + 2) + '┘\\n');\n}\n\nfunction main() {\n  console.log('Counting lines of code in Bruno packages...\\n');\n  \n  const packages = getPackages();\n  const results = [];\n  \n  for (const pkg of packages) {\n    process.stdout.write(`Analyzing ${pkg.name}...`);\n    const { totalLines, fileCount } = countLinesInDirectory(pkg.path);\n    results.push({\n      name: pkg.name,\n      loc: totalLines,\n      files: fileCount\n    });\n    process.stdout.write(' Done\\n');\n  }\n  \n  // Sort by LOC descending\n  results.sort((a, b) => b.loc - a.loc);\n  \n  printTable(results);\n}\n\nmain();"
  },
  {
    "path": "scripts/dev-hot-reload.js",
    "content": "#!/usr/bin/env node\n\n/**\n# Bruno Development Script\n#\n# This script sets up and runs the Bruno development environment with hot-reloading.\n# It manages concurrent processes for various packages and provides cleanup on exit.\n#\n# Usage:\n#   From the root of the project, run:\n#       node ./scripts/dev-hot-reload.js [options]\n#   or\n#       npm run dev:watch -- [options]\n*/\n\nconst { execSync } = require('child_process');\nconst { readFileSync } = require('fs');\n\n// Get major version from .nvmrc (e.g. v22.1.0  -> v22)\nconst NODE_VERSION = readFileSync('.nvmrc', 'utf8').trim().split('.')[0];\n\n// Configuration\nconst CONFIG = {\n  NODE_VERSION,\n  ELECTRON_WATCH_PATHS: [\n    'packages/**/dist/',\n    'packages/bruno-electron/src/',\n    'packages/bruno-lang/src/',\n    'packages/bruno-lang/v2/src/',\n    'packages/bruno-js/src/',\n    'packages/bruno-schema/src/'\n  ],\n  ELECTRON_START_DELAY: 10, // seconds\n  NODEMON_WATCH_DELAY: 1000 // milliseconds\n};\n\nconst COLORS = {\n  red: '\\x1b[0;31m',\n  green: '\\x1b[0;32m',\n  yellow: '\\x1b[1;33m',\n  blue: '\\x1b[0;34m',\n  nc: '\\x1b[0m' // No Color\n};\n\nconst LOG_LEVELS = {\n  INFO: 'INFO',\n  WARN: 'WARN',\n  ERROR: 'ERROR',\n  DEBUG: 'DEBUG',\n  SUCCESS: 'SUCCESS'\n};\n\nfunction log(level, msg) {\n  let color = COLORS.nc;\n  switch (level) {\n    case LOG_LEVELS.INFO:\n    case LOG_LEVELS.SUCCESS:  color = COLORS.green; break;\n    case LOG_LEVELS.WARN:     color = COLORS.yellow; break;\n    case LOG_LEVELS.ERROR:    color = COLORS.red; break;\n    case LOG_LEVELS.DEBUG:    color = COLORS.blue; break;\n  }\n\n  const output = `${color}[${level}]${COLORS.nc} ${msg}`;\n  if (level === LOG_LEVELS.ERROR) {\n    console.error(output);\n  } else {\n    console.log(output);\n  }\n}\n\n// Show help documentation\nfunction showHelp() {\n  console.log(`\n  Development Environment Setup for Bruno\n\n  Usage:\n      From the root of the project, run:\n          npm run dev:watch -- [options]\n      or\n          node scripts/dev-hot-reload.js [options]\n\n  Options:\n      -s, --setup    Clean all node_modules folders and re-install dependencies before starting\n      -h, --help     Show this help message\n\n  Examples:\n      # Start development environment\n      npm run dev:watch\n\n      # Start after cleaning node_modules\n      npm run dev:watch -- --setup\n\n      # Show this help\n      npm run dev:watch -- --help\n`);\n}\n\nfunction commandExists(command) {\n  try {\n    execSync(`command -v ${command}`, { stdio: 'ignore' });\n    return true;\n  } catch {\n    return false;\n  }\n}\n\n// Install global NPM package if not present\nfunction ensureGlobalPackage(packageName) {\n  if (!commandExists(packageName)) {\n    log(LOG_LEVELS.INFO, `Installing ${packageName} globally...`);\n    execSync(`npm install -g ${packageName}`, { stdio: 'inherit' });\n  }\n}\n\n// Ensure correct node version\nfunction ensureNodeVersion(requiredVersion) {\n  const currentVersion = process.version;\n  if (!currentVersion.includes(requiredVersion)) {\n    log(LOG_LEVELS.ERROR, `Node ${requiredVersion} is required but currently installed version is ${currentVersion}`);\n    log(LOG_LEVELS.ERROR, `Please install node ${requiredVersion} and try again.`);\n    log(LOG_LEVELS.ERROR, `You can run 'nvm install ${requiredVersion}' to install it, or 'nvm use ${requiredVersion}' if it's already installed.`);\n\n    process.exit(1);\n  }\n}\n\nfunction cleanNodeModules() {\n  log(LOG_LEVELS.INFO, 'Removing all node_modules directories...');\n  execSync('find . -name \"node_modules\" -type d -prune -exec rm -rf {} +', { stdio: 'inherit' });\n  log(LOG_LEVELS.SUCCESS, 'Node modules cleanup completed');\n}\n\nfunction reinstallDependencies() {\n  log(LOG_LEVELS.INFO, 'Re-installing dependencies...');\n  execSync('npm install --legacy-peer-deps', { stdio: 'inherit' });\n  log(LOG_LEVELS.SUCCESS, 'Dependencies re-installation completed');\n}\n\n// Setup development environment\nfunction startDevelopment() {\n  log(LOG_LEVELS.INFO, 'Starting development servers...');\n\n  const concurrently = require('concurrently');\n  const watchPaths = CONFIG.ELECTRON_WATCH_PATHS.map(path => `--watch \"${path}\"`).join(' ');\n\n  // concurrently command objects: { command, name, prefixColor, env, cwd, ipc }\n  const commandObjects = [\n    {\n      command: 'npm run watch --workspace=packages/bruno-common',\n      name: 'common',\n      prefixColor: 'magenta'\n    },\n    {\n      command: 'npm run watch --workspace=packages/bruno-converters',\n      name: 'converters',\n      prefixColor: 'green'\n    },\n    {\n      command: 'npm run watch --workspace=packages/bruno-query',\n      name: 'query',\n      prefixColor: 'blue'\n    },\n    {\n      command: 'npm run watch --workspace=packages/bruno-graphql-docs',\n      name: 'graphql',\n      prefixColor: 'white'\n    },\n    {\n      command: 'npm run watch --workspace=packages/bruno-requests',\n      name: 'requests',\n      prefixColor: 'gray'\n    },\n    {\n      command: 'npm run watch --workspace=packages/bruno-filestore',\n      name: 'filestore',\n      prefixColor: '#FA8072'\n    },\n    {\n      command: 'npm run dev:web',\n      name: 'react',\n      prefixColor: 'cyan'\n    },\n    {\n      command: `sleep ${CONFIG.ELECTRON_START_DELAY} && nodemon ${watchPaths} --ext js,jsx,ts,tsx --delay ${CONFIG.NODEMON_WATCH_DELAY}ms --exec \"npm run dev --workspace=packages/bruno-electron\"`,\n      name: 'electron',\n      prefixColor: 'yellow',\n      delay: CONFIG.ELECTRON_START_DELAY\n    }\n  ];\n\n  const { result } = concurrently(commandObjects, {\n    prefix: '[{name}: {pid}]',\n    killOthers: ['failure', 'success'],\n    restartTries: 3,\n    restartDelay: 1000\n  });\n\n  result\n    .then(() => log(LOG_LEVELS.SUCCESS, 'All processes completed successfully'))\n    .catch(err => {\n      log(LOG_LEVELS.ERROR, 'Development environment failed to start');\n      console.error(err);\n      process.exit(1);\n    });\n}\n\n// Main function\n(async function main() {\n  const args = process.argv.slice(2);\n  let runSetup = false;\n\n  // Parse command line arguments\n  for (const arg of args) {\n    if (arg === '-s' || arg === '--setup') {\n      runSetup = true;\n    } else if (arg === '-h' || arg === '--help') {\n      showHelp();\n      process.exit(0);\n    } else {\n      log(LOG_LEVELS.ERROR, `Unknown parameter: ${arg}`);\n      showHelp();\n      process.exit(1);\n    }\n  }\n\n  log(LOG_LEVELS.INFO, 'Initializing Bruno development environment...');\n\n  // Ensure required global packages and node version\n  ensureNodeVersion(CONFIG.NODE_VERSION);\n  ensureGlobalPackage('nodemon');\n  ensureGlobalPackage('concurrently');\n\n  // Run setup if requested\n  if (runSetup) {\n    cleanNodeModules();\n    reinstallDependencies();\n  }\n\n  // Start development environment\n  startDevelopment();\n})().catch(err => {\n  log(LOG_LEVELS.ERROR, 'An error occurred:');\n  console.error(err);\n  process.exit(1);\n});"
  },
  {
    "path": "scripts/dev.js",
    "content": "const { spawn } = require('child_process');\nconst path = require('path');\n\n// ANSI color codes\nconst colors = {\n  reset: '\\x1b[0m',\n  bright: '\\x1b[1m',\n  dim: '\\x1b[2m',\n  green: '\\x1b[32m',\n  yellow: '\\x1b[33m',\n  blue: '\\x1b[34m',\n  magenta: '\\x1b[35m',\n  cyan: '\\x1b[36m',\n  red: '\\x1b[31m'\n};\n\nconst log = {\n  info: (msg) => console.log(`${colors.cyan}ℹ${colors.reset} ${msg}`),\n  success: (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`),\n  warn: (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`),\n  error: (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`),\n  label: (label, msg) => console.log(`${colors.bright}${colors.magenta}[${label}]${colors.reset} ${msg}`)\n};\n\nconst rootDir = path.join(__dirname, '..');\nconst webDir = path.join(rootDir, 'packages/bruno-app');\nconst electronDir = path.join(rootDir, 'packages/bruno-electron');\n\nlet electronProcess = null;\nlet detectedPort = null;\n\n// Regex to match rsbuild's local URL output (e.g., \"➜ Local:    http://localhost:3000/\")\nconst portRegex = /Local:\\s+http:\\/\\/localhost:(\\d+)/;\n\nconsole.log(`\\n${colors.bright}${colors.yellow}🚀 Starting Bruno development environment...${colors.reset}\\n`);\n\n// Start the rsbuild dev server\nconst webProcess = spawn('npm', ['run', 'dev'], {\n  cwd: webDir,\n  stdio: ['inherit', 'pipe', 'pipe'],\n  shell: true\n});\n\nwebProcess.stdout.on('data', (data) => {\n  const output = data.toString();\n  process.stdout.write(output);\n\n  // Try to detect the port from rsbuild output\n  if (!detectedPort) {\n    const match = output.match(portRegex);\n    if (match) {\n      detectedPort = match[1];\n      log.success(`Detected dev server on port ${colors.bright}${detectedPort}${colors.reset}`);\n      startElectron(detectedPort);\n    }\n  }\n});\n\nwebProcess.stderr.on('data', (data) => {\n  process.stderr.write(data.toString());\n});\n\nwebProcess.on('close', (code) => {\n  log.info(`Web process exited with code ${code}`);\n  cleanup();\n});\n\nfunction startElectron(port) {\n  log.info(`Starting Electron with ${colors.cyan}BRUNO_DEV_PORT=${port}${colors.reset}`);\n\n  electronProcess = spawn('npm', ['run', 'dev'], {\n    cwd: electronDir,\n    stdio: 'inherit',\n    shell: true,\n    env: {\n      ...process.env,\n      BRUNO_DEV_PORT: port\n    }\n  });\n\n  electronProcess.on('close', (code) => {\n    log.info(`Electron process exited with code ${code}`);\n    cleanup();\n  });\n}\n\nfunction cleanup() {\n  if (webProcess && !webProcess.killed) {\n    webProcess.kill();\n  }\n  if (electronProcess && !electronProcess.killed) {\n    electronProcess.kill();\n  }\n  process.exit(0);\n}\n\n// Handle termination signals\nprocess.on('SIGINT', cleanup);\nprocess.on('SIGTERM', cleanup);\n"
  },
  {
    "path": "scripts/pr-checkout.js",
    "content": "#!/usr/bin/env node\n\nconst { execSync } = require('child_process');\nconst path = require('path');\nconst fs = require('fs');\n\nconst prNumber = process.argv[2];\n\nif (!prNumber || !/^\\d+$/.test(prNumber)) {\n  console.error('Usage: node scripts/pr-checkout.js <pr-number>');\n  process.exit(1);\n}\n\nconst repoRoot = path.resolve(__dirname, '..');\nconst repoName = path.basename(repoRoot);\nconst worktreesDir = path.resolve(repoRoot, '..', `${repoName}-worktrees`);\nconst worktreePath = path.join(worktreesDir, `pr-${prNumber}`);\n\nfunction log(...args) {\n  console.error(...args);\n}\n\nfunction run(cmd, options = {}) {\n  log(`$ ${cmd}`);\n  return execSync(cmd, { encoding: 'utf-8', cwd: repoRoot, stdio: 'inherit', ...options });\n}\n\nfunction runCapture(cmd) {\n  return execSync(cmd, { encoding: 'utf-8', cwd: repoRoot }).trim();\n}\n\n// Check if gh CLI is available\ntry {\n  runCapture('gh --version');\n} catch {\n  console.error('Error: GitHub CLI (gh) is not installed. Install it from https://cli.github.com/');\n  process.exit(1);\n}\n\n// Get PR info\nlog(`\\nFetching PR #${prNumber} info...`);\nlet prBranch, prHeadRepo;\ntry {\n  const prInfo = JSON.parse(runCapture(`gh pr view ${prNumber} --json headRefName,headRepository,headRepositoryOwner`));\n  prBranch = prInfo.headRefName;\n  prHeadRepo = `${prInfo.headRepositoryOwner.login}/${prInfo.headRepository.name}`;\n  log(`PR branch: ${prBranch}`);\n  log(`PR repo: ${prHeadRepo}`);\n} catch (error) {\n  console.error(`Error: Could not fetch PR #${prNumber}. Make sure the PR exists and you're authenticated with gh.`);\n  process.exit(1);\n}\n\n// Check if worktree already exists\nif (fs.existsSync(worktreePath)) {\n  log(`\\nWorktree already exists at ${worktreePath}`);\n  log(`To remove it, run: git worktree remove ${worktreePath}`);\n  console.log(worktreePath);\n  process.exit(0);\n}\n\n// Create worktrees directory if needed\nif (!fs.existsSync(worktreesDir)) {\n  log(`\\nCreating worktrees directory: ${worktreesDir}`);\n  fs.mkdirSync(worktreesDir, { recursive: true });\n}\n\n// Fetch the PR\nlog(`\\nFetching PR #${prNumber}...`);\nrun(`gh pr checkout ${prNumber} --detach`, { stdio: 'pipe' });\n\n// Get the current commit after checkout\nconst prCommit = runCapture('git rev-parse HEAD');\n\n// Go back to original branch\nconst originalBranch = runCapture('git rev-parse --abbrev-ref @{-1} 2>/dev/null || git rev-parse --abbrev-ref HEAD');\nrun(`git checkout ${originalBranch}`, { stdio: 'pipe' });\n\n// Create the worktree\nlog(`\\nCreating worktree at ${worktreePath}...`);\nrun(`git worktree add ${worktreePath} ${prCommit}`);\n\nlog(`\\n✓ PR #${prNumber} checked out to: ${worktreePath}`);\nlog(`\\nTo remove the worktree later:`);\nlog(`  git worktree remove ${worktreePath}`);\n\n// Output path to stdout for cd integration\nconsole.log(worktreePath);\n"
  },
  {
    "path": "scripts/setup.js",
    "content": "const { execSync } = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\n\nconst icons = {\n  clean: '🧹',\n  delete: '🗑️',\n  install: '📦',\n  build: '🔨',\n  success: '✅',\n  error: '❌',\n  working: '⚡'\n};\n\nconst execCommand = (command, description) => {\n  try {\n    console.log(`\\n${icons.working} ${description}...`);\n    execSync(command, { stdio: 'inherit' });\n    console.log(`${icons.success} ${description} completed`);\n  } catch (error) {\n    console.error(`${icons.error} ${description} failed`);\n    throw error;\n  }\n};\n\nconst glob = function (startPath, pattern) {\n  let results = [];\n\n  // Ensure start path exists\n  if (!fs.existsSync(startPath)) {\n    return results;\n  }\n\n  const files = fs.readdirSync(startPath);\n  for (const file of files) {\n    const filename = path.join(startPath, file);\n    const stat = fs.lstatSync(filename);\n\n    // If directory, recurse into it\n    if (stat.isDirectory()) {\n      // Skip node_modules recursion to avoid unnecessary deep scanning\n      if (file === 'node_modules') {\n        if (file === pattern) {\n          results.push(filename);\n        }\n        continue;\n      }\n      results = results.concat(glob(filename, pattern));\n    }\n\n    // If file matches pattern, add to results\n    if (file === pattern) {\n      results.push(filename);\n    }\n  }\n\n  return results;\n};\n\nfunction forceInstallPlatformDeps() {\n  // Note: make sure to hard pin deps and only add deps that have been checked\n  // for sec vuln already since the following will be force installed.\n  const deps = {\n    darwin: ['@lydell/node-pty-darwin-arm64@1.1.0', '@lydell/node-pty-darwin-x64@1.1.0'],\n    win32: ['@lydell/node-pty-win32-arm64@1.1.0', '@lydell/node-pty-win32-x64@1.1.0'],\n    linux: ['@lydell/node-pty-linux-arm64@1.1.0', '@lydell/node-pty-linux-x64@1.1.0']\n  };\n\n  // Ignore if no deps need to be installed\n  if (!deps[process.platform] || (Array.isArray(deps[process.platform]) && deps[process.platform].length === 0)) return;\n\n  const toInstall = deps[process.platform];\n  execCommand(\n    `npm i --legacy-peer-deps --no-save --force ${toInstall.join(' ')}`,\n    'Installing platform specific dependencies'\n  );\n}\n\nasync function setup() {\n  try {\n    // Clean up node_modules (if exists)\n    console.log(`\\n${icons.clean} Cleaning up node_modules directories...`);\n    const nodeModulesPaths = glob('.', 'node_modules');\n    for (const dir of nodeModulesPaths) {\n      console.log(`${icons.delete} Removing ${dir}`);\n      fs.rmSync(dir, { recursive: true, force: true });\n    }\n\n    // Install dependencies\n    execCommand('npm i --legacy-peer-deps', 'Installing dependencies');\n    forceInstallPlatformDeps();\n\n    // Build packages\n    execCommand('npm run build:graphql-docs', 'Building graphql-docs');\n    execCommand('npm run build:bruno-query', 'Building bruno-query');\n    execCommand('npm run build:bruno-common', 'Building bruno-common');\n    execCommand('npm run build:bruno-converters', 'Building bruno-converters');\n    execCommand('npm run build:bruno-requests', 'Building bruno-requests');\n    execCommand('npm run build:schema-types', 'Building schema-types');\n    execCommand('npm run build:bruno-filestore', 'Building bruno-filestore');\n\n    // Bundle JS sandbox libraries\n    execCommand('npm run sandbox:bundle-libraries --workspace=packages/bruno-js', 'Bundling JS sandbox libraries');\n\n    console.log(`\\n${icons.success} Setup completed successfully!\\n`);\n  } catch (error) {\n    console.error(`\\n${icons.error} Setup failed:`);\n    console.error(error);\n    process.exit(1);\n  }\n}\n\nsetup().catch((error) => {\n  console.error(error);\n  process.exit(1);\n});\n"
  },
  {
    "path": "security.md",
    "content": "## Security  \n\nAt Bruno, we take security seriously and are committed to providing a safe experience for all users.  \nWe appreciate responsible disclosure and value contributions that help improve Bruno's security.  \n\n\n## Reporting a Vulnerability  \n\nTo report a security issue, please email us at [security@usebruno.com](mailto:security@usebruno.com)\n\nWhen reporting a vulnerability, please include as many details as possible to help us investigate:  \n\n- **Type of issue** (e.g., cross-site scripting, malicious npm package, etc.).  \n- **Full paths of source file(s)** related to the issue.  \n- **Location of affected code** (tag, branch, commit, or direct URL).  \n- **Any special configuration** required to reproduce the issue.  \n- **Step-by-step instructions** to reproduce the issue.  \n- **Proof-of-concept or exploit code** (if available).  \n- **Potential impact**, including how an attacker might exploit the issue.  \n\n\n**Please do not report security vulnerabilities through public GitHub issues.**  \n\n"
  },
  {
    "path": "tests/asserts/add-assertions.spec.ts",
    "content": "import { test, expect } from '../../playwright';\nimport {\n  closeAllCollections,\n  openCollection,\n  openRequest,\n  selectRequestPaneTab,\n  sendRequest,\n  selectEnvironment,\n  addAssertion,\n  editAssertion,\n  deleteAssertion,\n  saveRequest\n} from '../utils/page';\nimport { buildCommonLocators } from '../utils/page/locators';\n\ntest.describe('Assertions - BRU Collection', () => {\n  test.beforeAll(async ({ pageWithUserData: page }) => {\n    await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n    await test.step('Navigate to assertions tab', async () => {\n      await openCollection(page, 'test-assertions-bru');\n      await selectEnvironment(page, 'Local', 'collection');\n      await openRequest(page, 'test-assertions-bru', 'ping');\n      await selectRequestPaneTab(page, 'Assert');\n      await page.waitForTimeout(1000);\n    });\n  });\n\n  test.afterEach(async ({ pageWithUserData: page }) => {\n    const locators = buildCommonLocators(page);\n    const table = locators.assertionsTable();\n\n    // Ensure we're on the Assertions tab\n    await selectRequestPaneTab(page, 'Assert');\n\n    // Wait for table to be visible\n    await expect(table.container()).toBeVisible();\n\n    // Get all rows and delete assertions (skip the empty row at the end)\n    let rowCount = await table.allRows().count();\n\n    // Keep deleting assertions until only the empty row remains\n    // We delete from the end to avoid index shifting issues\n    while (rowCount > 1) {\n      const deleteButton = table.rowDeleteButton(rowCount - 2); // Second to last (skip empty row)\n\n      await expect(deleteButton).toBeVisible({ timeout: 1000 });\n      await deleteButton.click();\n      // Wait for row count to decrease after deletion\n      await expect(table.allRows()).toHaveCount(rowCount - 1);\n      rowCount = await table.allRows().count(); // Re-count rows\n    }\n\n    // Save the request to persist the clean state\n    // saveRequest already waits for the \"Request saved successfully\" toast internally\n    await saveRequest(page);\n  });\n\n  test.afterAll(async ({ pageWithUserData: page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('should add assertion to request, verify toast, and run request successfully', async ({ pageWithUserData: page }) => {\n    const locators = buildCommonLocators(page);\n\n    await test.step('Add assertion to the request', async () => {\n      await addAssertion(page, {\n        expr: 'res.body',\n        value: 'pong'\n      });\n    });\n\n    await test.step('Save request and verify success toast', async () => {\n      await saveRequest(page);\n    });\n\n    await test.step('Send request and verify response', async () => {\n      await sendRequest(page, 200);\n\n      // Verify response status\n      await expect(locators.response.statusCode()).toContainText('200');\n    });\n\n    await test.step('Delete assertion and save', async () => {\n      // Navigate back to Assertions tab\n      await selectRequestPaneTab(page, 'Assert');\n\n      // Delete the assertion at row 0 (first data row)\n      await deleteAssertion(page, 0);\n\n      // Save the request\n      await saveRequest(page);\n    });\n  });\n\n  test('should add multiple assertions', async ({ pageWithUserData: page }) => {\n    const locators = buildCommonLocators(page);\n    const table = locators.assertionsTable();\n\n    await test.step('Add first assertion', async () => {\n      await addAssertion(page, {\n        expr: 'res.status',\n        value: '200'\n      });\n    });\n\n    await test.step('Add second assertion', async () => {\n      await addAssertion(page, {\n        expr: 'res.body',\n        value: 'pong'\n      });\n    });\n\n    await test.step('Add third assertion', async () => {\n      await addAssertion(page, {\n        expr: 'res.responseTime',\n        value: '1000'\n      });\n    });\n\n    await test.step('Verify all assertions are present', async () => {\n      // Check input values instead of cell text content\n      await expect(table.rowExprInput(0)).toHaveValue('res.status');\n      await expect(table.rowExprInput(1)).toHaveValue('res.body');\n      await expect(table.rowExprInput(2)).toHaveValue('res.responseTime');\n    });\n\n    await test.step('Save request', async () => {\n      await saveRequest(page);\n    });\n  });\n\n  test('should edit an existing assertion', async ({ pageWithUserData: page }) => {\n    const locators = buildCommonLocators(page);\n    const table = locators.assertionsTable();\n\n    await test.step('Add initial assertion', async () => {\n      await addAssertion(page, {\n        expr: 'res.body',\n        value: 'ping'\n      });\n    });\n\n    await test.step('Edit the assertion', async () => {\n      await editAssertion(page, 0, {\n        expr: 'res.status',\n        value: '200'\n      });\n    });\n\n    await test.step('Verify assertion was updated', async () => {\n      await expect(table.rowExprInput(0)).toHaveValue('res.status');\n      // The value cell might contain the operator, so we check it contains our value\n      const valueCell = table.rowCell('value', 0);\n      await expect(valueCell).toContainText('200');\n    });\n\n    await test.step('Save request', async () => {\n      await saveRequest(page);\n    });\n  });\n\n  test('should toggle assertion checkbox (enable/disable)', async ({ pageWithUserData: page }) => {\n    const locators = buildCommonLocators(page);\n    const table = locators.assertionsTable();\n\n    await test.step('Add assertion', async () => {\n      await addAssertion(page, {\n        expr: 'res.status',\n        value: '200'\n      });\n    });\n\n    await test.step('Verify checkbox is checked by default', async () => {\n      const checkbox = table.rowCheckbox(0);\n      await expect(checkbox).toBeChecked();\n    });\n\n    await test.step('Uncheck the assertion', async () => {\n      const checkbox = table.rowCheckbox(0);\n      await checkbox.uncheck();\n    });\n\n    await test.step('Verify checkbox is unchecked', async () => {\n      const checkbox = table.rowCheckbox(0);\n      await expect(checkbox).not.toBeChecked();\n    });\n\n    await test.step('Re-check the assertion', async () => {\n      const checkbox = table.rowCheckbox(0);\n      await checkbox.check();\n    });\n\n    await test.step('Verify checkbox is checked again', async () => {\n      const checkbox = table.rowCheckbox(0);\n      await expect(checkbox).toBeChecked();\n    });\n\n    await test.step('Save request', async () => {\n      await saveRequest(page);\n    });\n  });\n\n  test('should delete multiple assertions', async ({ pageWithUserData: page }) => {\n    const locators = buildCommonLocators(page);\n    const table = locators.assertionsTable();\n\n    await test.step('Add multiple assertions', async () => {\n      await addAssertion(page, { expr: 'res.status', value: '200' });\n      await addAssertion(page, { expr: 'res.body', value: 'pong' });\n      await addAssertion(page, { expr: 'res.responseTime', value: '1000' });\n    });\n\n    await test.step('Verify three assertions exist', async () => {\n      const rowCount = await table.allRows().count();\n      expect(rowCount).toBeGreaterThanOrEqual(3);\n    });\n\n    await test.step('Delete first assertion', async () => {\n      await deleteAssertion(page, 0);\n    });\n\n    await test.step('Delete second assertion (now at index 0 after first deletion)', async () => {\n      await deleteAssertion(page, 0);\n    });\n\n    await test.step('Verify only one assertion remains', async () => {\n      const rowCount = await table.allRows().count();\n      // Should have at least 1 assertion row + 1 empty row\n      expect(rowCount).toBeGreaterThanOrEqual(1);\n    });\n\n    await test.step('Save request', async () => {\n      await saveRequest(page);\n    });\n  });\n\n  test('should add assertion with different operators', async ({ pageWithUserData: page }) => {\n    const locators = buildCommonLocators(page);\n    const table = locators.assertionsTable();\n\n    await test.step('Add assertion with contains operator', async () => {\n      await addAssertion(page, {\n        expr: 'res.body',\n        value: 'pong',\n        operator: 'contains'\n      });\n    });\n\n    await test.step('Add assertion with greater than operator', async () => {\n      await addAssertion(page, {\n        expr: 'res.status',\n        value: '199',\n        operator: 'gt'\n      });\n    });\n\n    await test.step('Add assertion with length operator', async () => {\n      await addAssertion(page, {\n        expr: 'res.body',\n        value: '4',\n        operator: 'length'\n      });\n    });\n\n    await test.step('Verify assertions with different operators exist', async () => {\n      await expect(table.rowExprInput(0)).toHaveValue('res.body');\n      await expect(table.rowExprInput(1)).toHaveValue('res.status');\n      await expect(table.rowExprInput(2)).toHaveValue('res.body');\n    });\n\n    await test.step('Save request', async () => {\n      await saveRequest(page);\n    });\n  });\n});\n"
  },
  {
    "path": "tests/asserts/fixtures/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"test-assertions-bru\",\n  \"type\": \"collection\",\n  \"uid\": \"test-assertions-bru-uid\"\n}"
  },
  {
    "path": "tests/asserts/fixtures/collection/environments/Local.bru",
    "content": "vars {\n  host: https://testbench-sanity.usebruno.com\n}"
  },
  {
    "path": "tests/asserts/fixtures/collection/ping.bru",
    "content": "meta {\n  name: ping\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n"
  },
  {
    "path": "tests/asserts/init-user-data/preferences.json",
    "content": "{\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/asserts/fixtures/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/collection/close-all-collections/close-all-collections.spec.ts",
    "content": "import { execSync } from 'child_process';\nimport { test, expect } from '../../../playwright';\nimport { Page, ElectronApplication } from '@playwright/test';\nimport path from 'path';\nimport { openCollection } from '../../utils/page/actions';\nimport { buildCommonLocators } from '../../utils/page/locators';\n\n/**\n * Helper function to restart app and get fresh state with locators\n */\nconst restartAppAndGetLocators = async (restartApp: (options?: { initUserDataPath?: string }) => Promise<ElectronApplication>): Promise<{ app: ElectronApplication; page: Page; locators: ReturnType<typeof buildCommonLocators> }> => {\n  const app = await restartApp();\n  const page = await app.firstWindow();\n  await page.locator('[data-app-state=\"loaded\"]').waitFor();\n  const locators = buildCommonLocators(page);\n  return { app, page, locators };\n};\n\n// TODO: These tests need to be updated for the new workspace UI\n// The CollectionsHeader component (with collections-header-actions-menu-close-all) is not rendered in workspace mode\n// The \"Remove from workspace\" flow is different from the old \"Close collection\" flow\ntest.describe.skip('Close All Collections', () => {\n  test.afterAll(async () => {\n    // Reset the request file to the original state after saving changes\n    execSync(`git checkout -- \"${path.join(__dirname, 'fixtures', 'collections', 'collection 1', 'test-request.bru')}\"`);\n  });\n\n  test('should show/hide close all icon based on hover state', async ({ pageWithUserData: page }) => {\n    const locators = buildCommonLocators(page);\n\n    await test.step('Verify initial state', async () => {\n      await expect(locators.sidebar.collection('collection 1')).toBeVisible();\n      const closeAllButton = locators.sidebar.closeAllCollectionsButton();\n      await expect(closeAllButton).toHaveCSS('opacity', '0');\n    });\n\n    await test.step('Hover to show icon', async () => {\n      const closeAllButton = locators.sidebar.closeAllCollectionsButton();\n      await locators.sidebar.collectionsContainer().hover();\n      await expect(closeAllButton).toHaveCSS('opacity', '1');\n    });\n\n    await test.step('Move mouse away to hide icon', async () => {\n      const closeAllButton = locators.sidebar.closeAllCollectionsButton();\n      await page.mouse.move(0, 0);\n      await expect(closeAllButton).toHaveCSS('opacity', '0');\n    });\n  });\n\n  test('should handle closing all collections without unsaved changes', async ({ restartApp }) => {\n    const { page, locators } = await restartAppAndGetLocators(restartApp);\n\n    await test.step('Verify collections are visible', async () => {\n      await expect(locators.sidebar.collection('collection 1')).toBeVisible();\n      await expect(locators.sidebar.collection('collection 2')).toBeVisible();\n    });\n\n    await test.step('Cancel closing collections', async () => {\n      // Hover and click close all icon\n      await locators.sidebar.collectionsContainer().hover();\n      await locators.sidebar.closeAllCollectionsButton().click();\n\n      // Verify confirmation modal appears\n      const confirmModal = locators.modal.byTitle('Close all collections');\n      await expect(confirmModal).toBeVisible();\n\n      // Click \"Cancel\" to dismiss the modal\n      await locators.modal.closeButton().click();\n\n      // Verify collections are still visible\n      await expect(locators.sidebar.collection('collection 1')).toBeVisible();\n      await expect(locators.sidebar.collection('collection 2')).toBeVisible();\n    });\n\n    await test.step('Confirm closing collections', async () => {\n      // Hover and click close all icon again\n      await locators.sidebar.collectionsContainer().hover();\n      await locators.sidebar.closeAllCollectionsButton().click();\n\n      // Verify confirmation modal appears\n      const confirmModal = locators.modal.byTitle('Close all collections');\n      await expect(confirmModal).toBeVisible();\n\n      // Click \"Close All\" to confirm\n      await locators.modal.button('Close All').click();\n\n      // Verify collections are closed\n      await expect(locators.sidebar.collection('collection 1')).not.toBeVisible();\n      await expect(locators.sidebar.collection('collection 2')).not.toBeVisible();\n    });\n  });\n\n  test('should discard changes and close collections when Discard and Close is clicked', async ({ restartApp }) => {\n    const { page, locators: newLocators } = await restartAppAndGetLocators(restartApp);\n\n    await test.step('Verify collections are visible', async () => {\n      await expect(newLocators.sidebar.collection('collection 1')).toBeVisible();\n      await expect(newLocators.sidebar.collection('collection 2')).toBeVisible();\n    });\n\n    await test.step('Create unsaved changes', async () => {\n      await openCollection(page, 'collection 1');\n      await newLocators.sidebar.request('test-request').click();\n\n      const urlContainer = page.locator('#request-url');\n      await expect(urlContainer).toBeVisible();\n\n      const codeMirrorEditor = urlContainer.locator('.CodeMirror');\n      await codeMirrorEditor.click();\n      await page.keyboard.type('modified');\n    });\n\n    await test.step('Trigger close all and discard changes', async () => {\n      await newLocators.sidebar.collectionsContainer().hover();\n      await newLocators.sidebar.closeAllCollectionsButton().click();\n\n      const unsavedChangesModal = newLocators.modal.byTitle('Close all collections');\n      await expect(unsavedChangesModal).toBeVisible();\n      await expect(unsavedChangesModal.getByText('Do you want to save')).toBeVisible();\n\n      await newLocators.modal.button('Discard and Close').click();\n\n      await expect(page.getByText('Closed all collections')).toBeVisible();\n      await expect(newLocators.sidebar.collection('collection 1')).not.toBeVisible();\n      await expect(newLocators.sidebar.collection('collection 2')).not.toBeVisible();\n    });\n\n    await test.step('Restart app to verify changes were discarded', async () => {\n      const { page: restartedPage, locators: restartedLocators } = await restartAppAndGetLocators(restartApp);\n\n      await expect(restartedLocators.sidebar.collection('collection 1')).toBeVisible();\n      await openCollection(restartedPage, 'collection 1');\n      await restartedLocators.sidebar.request('test-request').click();\n\n      const urlContainerAfterReopen = restartedPage.locator('#request-url');\n      await expect(urlContainerAfterReopen).toBeVisible();\n      const urlAfterReopen = await urlContainerAfterReopen.locator('.CodeMirror').textContent();\n      expect(urlAfterReopen).not.toContain('modified');\n    });\n  });\n\n  test('should save changes and close collections when Save and Close is clicked', async ({ restartApp }) => {\n    const { page, locators: newLocators } = await restartAppAndGetLocators(restartApp);\n\n    await test.step('Verify collections are visible', async () => {\n      await expect(newLocators.sidebar.collection('collection 1')).toBeVisible();\n      await expect(newLocators.sidebar.collection('collection 2')).toBeVisible();\n    });\n\n    await test.step('Create unsaved changes', async () => {\n      await openCollection(page, 'collection 1');\n      await newLocators.sidebar.request('test-request').click();\n\n      const urlContainer = page.locator('#request-url');\n      await expect(urlContainer).toBeVisible();\n\n      const codeMirrorEditor = urlContainer.locator('.CodeMirror');\n      await codeMirrorEditor.click();\n      await page.keyboard.type('modified');\n    });\n\n    await test.step('Trigger close all and save changes', async () => {\n      await newLocators.sidebar.collectionsContainer().hover();\n      await newLocators.sidebar.closeAllCollectionsButton().click();\n\n      const unsavedChangesModal = newLocators.modal.byTitle('Close all collections');\n      await expect(unsavedChangesModal).toBeVisible();\n      await expect(unsavedChangesModal.getByText('Do you want to save')).toBeVisible();\n\n      await newLocators.modal.button('Save and Close').click();\n\n      await expect(newLocators.sidebar.collection('collection 1')).not.toBeVisible();\n      await expect(newLocators.sidebar.collection('collection 2')).not.toBeVisible();\n    });\n\n    await test.step('Restart app to verify changes were saved', async () => {\n      const { page: restartedPage, locators: restartedLocators } = await restartAppAndGetLocators(restartApp);\n\n      await expect(restartedLocators.sidebar.collection('collection 1')).toBeVisible();\n      await openCollection(restartedPage, 'collection 1');\n      await restartedLocators.sidebar.request('test-request').click();\n\n      const urlContainerAfterReopen = restartedPage.locator('#request-url');\n      await expect(urlContainerAfterReopen).toBeVisible();\n      const urlAfterReopen = await urlContainerAfterReopen.locator('.CodeMirror').textContent();\n      expect(urlAfterReopen).toContain('modified');\n    });\n  });\n});\n"
  },
  {
    "path": "tests/collection/close-all-collections/fixtures/collections/collection 1/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"collection 1\",\n  \"type\": \"collection\"\n}\n\n"
  },
  {
    "path": "tests/collection/close-all-collections/fixtures/collections/collection 1/test-request.bru",
    "content": "meta {\n  name: test-request\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://jsonplaceholder.typicode.com/posts/1\n  body: none\n  auth: none\n}\n\n"
  },
  {
    "path": "tests/collection/close-all-collections/fixtures/collections/collection 2/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"collection 2\",\n  \"type\": \"collection\"\n}\n\n"
  },
  {
    "path": "tests/collection/close-all-collections/fixtures/collections/collection 2/test-request.bru",
    "content": "meta {\n  name: test-request\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://jsonplaceholder.typicode.com/users/1\n  body: none\n  auth: none\n}\n\n"
  },
  {
    "path": "tests/collection/close-all-collections/init-user-data/preferences.json",
    "content": "{\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/collection/close-all-collections/fixtures/collections/collection 1\",\n    \"{{projectRoot}}/tests/collection/close-all-collections/fixtures/collections/collection 2\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/collection/create/create-collection.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections, createCollection, createRequest } from '../../utils/page';\n\ntest.describe('Create collection', () => {\n  test.afterEach(async ({ page }) => {\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  test('Create collection and add a simple HTTP request', async ({ page, createTmpDir }) => {\n    const collectionName = 'test-collection';\n    const requestName = 'ping';\n\n    await createCollection(page, collectionName, await createTmpDir(collectionName));\n\n    // Create a new request using the dialog/modal flow\n    await createRequest(page, requestName, collectionName);\n\n    // Set the URL\n    await page.locator('#request-url .CodeMirror').click();\n    await page.locator('#request-url').locator('textarea').fill('http://localhost:8081');\n    await page.locator('#send-request').getByTitle('Save Request').click();\n\n    // Send a request\n    await page.locator('#request-url .CodeMirror').click();\n    await page.locator('#request-url').locator('textarea').fill('/ping');\n    await page.locator('#send-request').getByTitle('Save Request').click();\n    await page.locator('#send-request').getByRole('img').nth(2).click();\n\n    // Verify the response\n    await expect(page.getByRole('main')).toContainText('200 OK');\n  });\n});\n"
  },
  {
    "path": "tests/collection/create-requests/fixtures/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"create-requests\",\n  \"type\": \"collection\"\n}"
  },
  {
    "path": "tests/collection/create-requests/fixtures/collection/folder1/folder.bru",
    "content": "meta {\n  name: folder1\n  seq: 1\n}\n\nauth {\n  mode: inherit\n}\n"
  },
  {
    "path": "tests/collection/create-requests/graphql-requests.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections } from '../../utils/page';\nimport { buildCommonLocators } from '../../utils/page/locators';\n\ntest.describe('Create GraphQL Requests', () => {\n  let locators: ReturnType<typeof buildCommonLocators>;\n\n  test.beforeAll(async ({ pageWithUserData: page }) => {\n    locators = buildCommonLocators(page);\n  });\n\n  test.afterAll(async ({ pageWithUserData: page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('Verifies that GraphQL requests are created at the expected locations', async ({ pageWithUserData: page }) => {\n    await test.step('Navigate to collection and verify it exists', async () => {\n      await expect(locators.sidebar.collection('create-requests')).toBeVisible();\n    });\n\n    await test.step('Create GraphQL request via collection three dots menu', async () => {\n      await locators.sidebar.collection('create-requests').hover();\n      await locators.actions.collectionActions('create-requests').click();\n      await locators.dropdown.item('New Request').click();\n\n      await page.getByTestId('graphql-request').click();\n      await page.getByTestId('request-name').fill('Root GraphQL Request');\n      await page.getByTestId('new-request-url').locator('.CodeMirror').click();\n      await page.keyboard.type('https://api.example.com/graphql');\n      await locators.modal.button('Create').click();\n    });\n\n    await test.step('Verify GraphQL request was created at collection root', async () => {\n      // Open collection and verify request is present in collection root\n      await locators.sidebar.collection('create-requests').click();\n      const requestItem = locators.sidebar.request('Root GraphQL Request');\n      await expect(requestItem).toBeVisible();\n\n      // Open request and verify it is the active request\n      await requestItem.click();\n      await expect(locators.tabs.activeRequestTab()).toContainText('Root GraphQL Request');\n\n      // Open folder1 and verify request is not in folder1\n      await locators.sidebar.folder('folder1').click();\n      const folderRequestItem = locators.sidebar.folderRequest('folder1', 'Root GraphQL Request');\n      await expect(folderRequestItem).not.toBeVisible();\n    });\n\n    await test.step('Create GraphQL request via folder1 three dots menu', async () => {\n      await locators.sidebar.folder('folder1').hover();\n      await locators.actions.collectionItemActions('folder1').click();\n      await locators.dropdown.item('New Request').click();\n\n      await page.getByTestId('graphql-request').click();\n      await page.getByTestId('request-name').fill('Folder GraphQL Request');\n      await page.getByTestId('new-request-url').locator('.CodeMirror').click();\n      await page.keyboard.type('https://api.example.com/graphql/v2');\n      await locators.modal.button('Create').click();\n    });\n\n    await test.step('Verify GraphQL request was created within folder1', async () => {\n      // Open collection and verify request is not in collection root\n      await locators.sidebar.collection('create-requests').click();\n      const folderRequestItem = locators.sidebar.folderRequest('folder1', 'Folder GraphQL Request');\n      await expect(folderRequestItem).toBeVisible();\n\n      // Open request and verify it is the active request\n      await folderRequestItem.click();\n      await expect(locators.tabs.activeRequestTab()).toContainText('Folder GraphQL Request');\n    });\n  });\n});\n"
  },
  {
    "path": "tests/collection/create-requests/grpc-requests.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { buildCommonLocators } from '../../utils/page/locators';\nimport { closeAllCollections } from '../../utils/page';\n\ntest.describe('Create gRPC Requests', () => {\n  let locators: ReturnType<typeof buildCommonLocators>;\n\n  test.beforeAll(async ({ pageWithUserData: page }) => {\n    locators = buildCommonLocators(page);\n  });\n\n  test.afterAll(async ({ pageWithUserData: page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('Verifies that gRPC requests are created at the expected locations', async ({ pageWithUserData: page }) => {\n    await test.step('Navigate to collection and verify it exists', async () => {\n      await expect(locators.sidebar.collection('create-requests')).toContainText('create-requests');\n    });\n\n    await test.step('Create gRPC request via collection three dots menu', async () => {\n      await locators.sidebar.collection('create-requests').hover();\n      await locators.actions.collectionActions('create-requests').click();\n      await locators.dropdown.item('New Request').click();\n\n      await page.getByTestId('grpc-request').click();\n      await page.getByTestId('request-name').fill('Root gRPC Request');\n      await page.getByTestId('new-request-url').locator('.CodeMirror').click();\n      await page.keyboard.type('grpc://localhost:50051');\n      await locators.modal.button('Create').click();\n    });\n\n    await test.step('Verify gRPC request was created at collection root', async () => {\n      // Open collection and verify request is present in collection root\n      await locators.sidebar.collection('create-requests').click();\n      const requestItem = locators.sidebar.request('Root gRPC Request');\n      await expect(requestItem).toBeVisible();\n\n      // Open request and verify it is the active request\n      await requestItem.click();\n      await expect(locators.tabs.activeRequestTab()).toContainText('Root gRPC Request');\n\n      // Open folder1 and verify request is not in folder1\n      await locators.sidebar.folder('folder1').click();\n      const folderRequestItem = locators.sidebar.folderRequest('folder1', 'Root gRPC Request');\n      await expect(folderRequestItem).not.toBeVisible();\n    });\n\n    await test.step('Create gRPC request via folder1 three dots menu', async () => {\n      await locators.sidebar.folder('folder1').hover();\n      await locators.actions.collectionItemActions('folder1').click();\n      await locators.dropdown.item('New Request').click();\n\n      await page.getByTestId('grpc-request').click();\n      await page.getByTestId('request-name').fill('Folder gRPC Request');\n      await page.getByTestId('new-request-url').locator('.CodeMirror').click();\n      await page.keyboard.type('grpc://localhost:50052');\n      await locators.modal.button('Create').click();\n    });\n\n    await test.step('Verify gRPC request was created within folder1', async () => {\n      // Open collection and verify request is not in collection root\n      await locators.sidebar.collection('create-requests').click();\n      const folderRequestItem = locators.sidebar.folderRequest('folder1', 'Folder gRPC Request');\n      await expect(folderRequestItem).toBeVisible();\n\n      // Open request and verify it is the active request\n      await folderRequestItem.click();\n      await expect(locators.tabs.activeRequestTab()).toContainText('Folder gRPC Request');\n    });\n  });\n});\n"
  },
  {
    "path": "tests/collection/create-requests/http-requests.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { buildCommonLocators } from '../../utils/page/locators';\nimport { closeAllCollections } from '../../utils/page';\n\ntest.describe('Create HTTP Requests', () => {\n  let locators: ReturnType<typeof buildCommonLocators>;\n\n  test.beforeAll(async ({ pageWithUserData: page }) => {\n    locators = buildCommonLocators(page);\n  });\n\n  test.afterAll(async ({ pageWithUserData: page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('Verifies that HTTP requests are created at the expected locations', async ({ pageWithUserData: page }) => {\n    await test.step('Navigate to collection and verify it exists', async () => {\n      await expect(locators.sidebar.collection('create-requests')).toContainText('create-requests');\n    });\n\n    await test.step('Create HTTP request via collection three dots menu', async () => {\n      await locators.sidebar.collection('create-requests').hover();\n      await locators.actions.collectionActions('create-requests').click();\n      await locators.dropdown.item('New Request').click();\n\n      await page.getByTestId('request-name').fill('Root HTTP Request');\n      await page.getByTestId('new-request-url').locator('.CodeMirror').click();\n      await page.keyboard.type('https://echo.usebruno.com');\n      await locators.modal.button('Create').click();\n    });\n\n    await test.step('Verify HTTP request was created at collection root', async () => {\n      // Open collection and verify request is present in collection root\n      await locators.sidebar.collection('create-requests').click();\n      const requestItem = locators.sidebar.request('Root HTTP Request');\n      await expect(requestItem).toBeVisible();\n\n      // Open request and verify it is the active request\n      await requestItem.click();\n      await expect(locators.tabs.activeRequestTab()).toContainText('Root HTTP Request');\n\n      // Open folder1 and verify request is not in folder1\n      await locators.sidebar.folder('folder1').click();\n      const folderRequestItem = locators.sidebar.folderRequest('folder1', 'Root HTTP Request');\n      await expect(folderRequestItem).not.toBeVisible();\n    });\n\n    await test.step('Create HTTP request via folder1 three dots menu', async () => {\n      await locators.sidebar.folder('folder1').hover();\n      await locators.actions.collectionItemActions('folder1').click();\n      await locators.dropdown.item('New Request').click();\n\n      await page.getByTestId('request-name').fill('Folder HTTP Request');\n      await page.getByTestId('new-request-url').locator('.CodeMirror').click();\n      await page.keyboard.type('https://echo.usebruno.com');\n      await locators.modal.button('Create').click();\n    });\n\n    await test.step('Verify HTTP request was created within folder1', async () => {\n      // Open collection and verify request is not in collection root\n      await locators.sidebar.collection('create-requests').click();\n      const folderRequestItem = locators.sidebar.folderRequest('folder1', 'Folder HTTP Request');\n      await expect(folderRequestItem).toBeVisible();\n\n      // Open request and verify it is the active request\n      await folderRequestItem.click();\n      await expect(locators.tabs.activeRequestTab()).toContainText('Folder HTTP Request');\n    });\n  });\n});\n"
  },
  {
    "path": "tests/collection/create-requests/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{collectionPath}}\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "tests/collection/create-requests/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{collectionPath}}\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/collection/create-requests/ws-requests.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { buildCommonLocators } from '../../utils/page/locators';\nimport { closeAllCollections } from '../../utils/page';\n\ntest.describe('Create WebSocket Requests', () => {\n  let locators: ReturnType<typeof buildCommonLocators>;\n\n  test.beforeAll(async ({ pageWithUserData: page }) => {\n    locators = buildCommonLocators(page);\n  });\n\n  test.afterAll(async ({ pageWithUserData: page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('Verifies that WebSocket requests are created at the expected locations', async ({ pageWithUserData: page }) => {\n    await test.step('Navigate to collection and verify it exists', async () => {\n      await expect(locators.sidebar.collection('create-requests')).toBeVisible();\n    });\n\n    await test.step('Create WebSocket request via collection three dots menu', async () => {\n      await locators.sidebar.collection('create-requests').hover();\n      await locators.actions.collectionActions('create-requests').click();\n      await locators.dropdown.item('New Request').click();\n\n      await page.getByTestId('ws-request').click();\n      await page.getByTestId('request-name').fill('Root WebSocket Request');\n      await page.getByTestId('new-request-url').locator('.CodeMirror').click();\n      await page.keyboard.type('ws://localhost:8080');\n      await locators.modal.button('Create').click();\n    });\n\n    await test.step('Verify WebSocket request was created at collection root', async () => {\n      // Open collection and verify request is present in collection root\n      await locators.sidebar.collection('create-requests').click();\n      const requestItem = locators.sidebar.request('Root WebSocket Request');\n      await expect(requestItem).toBeVisible();\n\n      // Open request and verify it is the active request\n      await requestItem.click();\n      await expect(locators.tabs.activeRequestTab()).toContainText('Root WebSocket Request');\n\n      // Open folder1 and verify request is not in folder1\n      await locators.sidebar.folder('folder1').click();\n      const folderRequestItem = locators.sidebar.folderRequest('folder1', 'Root WebSocket Request');\n      await expect(folderRequestItem).not.toBeVisible();\n    });\n\n    await test.step('Create WebSocket request via folder1 three dots menu', async () => {\n      await locators.sidebar.folder('folder1').hover();\n      await locators.actions.collectionItemActions('folder1').click();\n      await locators.dropdown.item('New Request').click();\n\n      await page.getByTestId('ws-request').click();\n      await page.getByTestId('request-name').fill('Folder WebSocket Request');\n      await page.getByTestId('new-request-url').locator('.CodeMirror').click();\n      await page.keyboard.type('ws://localhost:8081');\n      await locators.modal.button('Create').click();\n    });\n\n    await test.step('Verify WebSocket request was created within folder1', async () => {\n      // Open collection and verify request is not in collection root\n      await locators.sidebar.collection('create-requests').click();\n      const folderRequestItem = locators.sidebar.folderRequest('folder1', 'Folder WebSocket Request');\n      await expect(folderRequestItem).toBeVisible();\n\n      // Open request and verify it is the active request\n      await folderRequestItem.click();\n      await expect(locators.tabs.activeRequestTab()).toContainText('Folder WebSocket Request');\n    });\n  });\n});\n"
  },
  {
    "path": "tests/collection/default-ignores/default-ignores.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\nimport * as fs from 'fs';\nimport { closeAllCollections, openCollection } from '../../utils/page';\nimport { buildCommonLocators } from '../../utils/page/locators';\n\ntest.describe('Default ignores for node_modules and .git', () => {\n  test.afterEach(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('Should always ignore node_modules even when user has custom ignore config', async ({\n    page,\n    electronApp,\n    createTmpDir\n  }) => {\n    const locators = buildCommonLocators(page);\n    const collectionDir = await createTmpDir('node-modules-ignore-test');\n\n    // Create bruno.json with custom ignore that doesn't include node_modules\n    const brunoConfig = {\n      version: '1',\n      name: 'Node Modules Ignore Test',\n      type: 'collection',\n      ignore: ['custom-folder', 'another-folder'] // Explicitly NOT including node_modules\n    };\n    fs.writeFileSync(path.join(collectionDir, 'bruno.json'), JSON.stringify(brunoConfig, null, 2));\n\n    // Create node_modules directory with .bru files inside\n    const nodeModulesDir = path.join(collectionDir, 'node_modules');\n    fs.mkdirSync(nodeModulesDir);\n    fs.mkdirSync(path.join(nodeModulesDir, 'some-package'));\n\n    // Create a .bru file inside node_modules (should be ignored)\n    fs.writeFileSync(\n      path.join(nodeModulesDir, 'some-package', 'fake-request.bru'),\n      `meta {\n  name: Fake Request In Node Modules\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://fake.com\n  body: none\n  auth: none\n}\n`\n    );\n\n    // Create a real request at the collection root\n    fs.writeFileSync(\n      path.join(collectionDir, 'real-request.bru'),\n      `meta {\n  name: Real Request\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://real.com\n  body: none\n  auth: none\n}\n`\n    );\n\n    // Mock the electron dialog\n    await electronApp.evaluate(\n      ({ dialog }, { collectionDir }) => {\n        dialog.showOpenDialog = async () => ({\n          canceled: false,\n          filePaths: [collectionDir]\n        });\n      },\n      { collectionDir }\n    );\n\n    // Open the collection\n    await locators.plusMenu.button().click();\n    await locators.dropdown.tippyItem('Open collection').click();\n\n    // Wait for collection to load\n    await expect(locators.sidebar.collection('Node Modules Ignore Test')).toBeVisible({ timeout: 30000 });\n\n    // Accept the sandbox mode\n    await openCollection(page, 'Node Modules Ignore Test');\n\n    // Verify only the real request is visible\n    await expect(locators.sidebar.request('Real Request')).toBeVisible({ timeout: 10000 });\n\n    // The fake request inside node_modules should NOT be visible\n    await expect(locators.sidebar.request('Fake Request In Node Modules')).not.toBeVisible();\n\n    // node_modules folder should not appear in the sidebar\n    await expect(locators.sidebar.folder('node_modules')).not.toBeVisible();\n  });\n\n  test('Should always ignore .git even when user has custom ignore config', async ({\n    page,\n    electronApp,\n    createTmpDir\n  }) => {\n    const locators = buildCommonLocators(page);\n    const collectionDir = await createTmpDir('git-ignore-test');\n\n    // Create bruno.json with custom ignore that doesn't include .git\n    const brunoConfig = {\n      version: '1',\n      name: 'Git Ignore Test',\n      type: 'collection',\n      ignore: ['custom-folder'] // Explicitly NOT including .git\n    };\n    fs.writeFileSync(path.join(collectionDir, 'bruno.json'), JSON.stringify(brunoConfig, null, 2));\n\n    // Create .git directory with .bru files inside\n    const gitDir = path.join(collectionDir, '.git');\n    fs.mkdirSync(gitDir);\n    fs.mkdirSync(path.join(gitDir, 'hooks'));\n\n    // Create a .bru file inside .git (should be ignored)\n    fs.writeFileSync(\n      path.join(gitDir, 'hooks', 'fake-git-request.bru'),\n      `meta {\n  name: Fake Request In Git\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://fake-git.com\n  body: none\n  auth: none\n}\n`\n    );\n\n    // Create a real request at the collection root\n    fs.writeFileSync(\n      path.join(collectionDir, 'real-request.bru'),\n      `meta {\n  name: Real Git Request\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://real.com\n  body: none\n  auth: none\n}\n`\n    );\n\n    // Mock the electron dialog\n    await electronApp.evaluate(\n      ({ dialog }, { collectionDir }) => {\n        dialog.showOpenDialog = async () => ({\n          canceled: false,\n          filePaths: [collectionDir]\n        });\n      },\n      { collectionDir }\n    );\n\n    // Open the collection\n    await locators.plusMenu.button().click();\n    await locators.dropdown.tippyItem('Open collection').click();\n\n    // Wait for collection to load\n    await expect(locators.sidebar.collection('Git Ignore Test')).toBeVisible({ timeout: 30000 });\n\n    // Accept the sandbox mode\n    await openCollection(page, 'Git Ignore Test');\n\n    // Verify only the real request is visible\n    await expect(locators.sidebar.request('Real Git Request')).toBeVisible({ timeout: 10000 });\n\n    // The fake request inside .git should NOT be visible\n    await expect(locators.sidebar.request('Fake Request In Git')).not.toBeVisible();\n\n    // .git folder should not appear in the sidebar\n    await expect(locators.sidebar.folder('.git')).not.toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/collection/default-sandbox-mode/default-sandbox-mode.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { createCollection, openCollection } from '../../utils/page/actions';\nimport { buildSandboxLocators } from '../../utils/page/locators';\n\ntest.describe('Default JavaScript Sandbox Mode', () => {\n  test('should set jsSandboxMode to safe by default when creating a new collection', async ({ page, createTmpDir }) => {\n    const collectionName = 'test-sandbox-collection';\n\n    await createCollection(page, collectionName, await createTmpDir());\n    const sandboxLocators = buildSandboxLocators(page);\n\n    // Verify sandbox mode is set to safe by default\n    await expect(sandboxLocators.sandboxModeSelector()).toBeVisible();\n\n    // Click on sandbox mode selector to open security settings\n    await sandboxLocators.sandboxModeSelector().click();\n\n    // Change to developer mode\n    const developerRadio = sandboxLocators.developerModeRadio();\n    await developerRadio.check();\n\n    // For developer mode, check if safe mode is currently selected\n    const safeModeChecked = await sandboxLocators.safeModeRadio().isChecked().catch(() => false);\n    await expect(safeModeChecked).toBe(false);\n\n    await page.keyboard.press('Escape');\n  });\n});\n"
  },
  {
    "path": "tests/collection/delete/delete-collection.spec.ts",
    "content": "import fs from 'fs';\nimport path from 'path';\nimport { test, expect } from '../../../playwright';\nimport { createCollection, createRequest, deleteCollectionFromOverview } from '../../utils/page';\n\ntest.describe('Delete collection', () => {\n  test('Delete collection from workspace overview removes files from disk', async ({ page, createTmpDir }) => {\n    const collectionName = 'delete-test-collection';\n    const tmpDir = await createTmpDir(collectionName);\n    const collectionPath = path.join(tmpDir, collectionName);\n\n    // Create a collection with a request\n    await createCollection(page, collectionName, tmpDir);\n    await createRequest(page, 'ping', collectionName, { url: 'http://localhost:8081/ping' });\n\n    // Verify collection directory exists on disk\n    expect(fs.existsSync(collectionPath)).toBe(true);\n\n    // Capture any uncaught errors during deletion\n    const pageErrors: Error[] = [];\n    page.on('pageerror', (error) => pageErrors.push(error));\n\n    // Navigate to Workspace and delete collection from overview\n    await deleteCollectionFromOverview(page, collectionName);\n\n    // Verify collection is removed from overview\n    await expect(\n      page.locator('.collection-card').filter({ hasText: collectionName })\n    ).not.toBeVisible();\n\n    // Verify collection is removed from sidebar\n    await expect(\n      page.locator('#sidebar-collection-name').filter({ hasText: collectionName })\n    ).not.toBeVisible();\n\n    // Verify collection directory is deleted from disk\n    expect(fs.existsSync(collectionPath)).toBe(false);\n\n    // Verify no uncaught JS errors occurred during deletion\n    expect(pageErrors).toHaveLength(0);\n  });\n});\n"
  },
  {
    "path": "tests/collection/draft/draft-indicator.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections, createCollection } from '../../utils/page';\n\ntest.describe('Draft indicator in collection and folder settings', () => {\n  test.afterAll(async ({ page }) => {\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  test('Verify draft indicator appears when changing collection settings - Headers', async ({ page, createTmpDir }) => {\n    const collectionName = 'test-draft';\n\n    // Create a new collection\n    await createCollection(page, collectionName, await createTmpDir());\n\n    // Verify the collection settings tab is open\n    await expect(page.locator('.request-tab .tab-label').filter({ hasText: 'Collection' })).toBeVisible();\n\n    // Verify initially there is NO draft indicator (close icon is present)\n    const collectionTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) });\n    await expect(collectionTab.locator('.close-icon')).toBeVisible();\n    await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible();\n\n    await page.locator('.tab.headers').click();\n\n    const headerTable = page.locator('table').first();\n    const headerRow = headerTable.locator('tbody tr').first();\n\n    const nameEditor = headerRow.locator('.CodeMirror').first();\n    await nameEditor.click();\n    await page.keyboard.type('X-Custom-Header');\n\n    const valueEditor = headerRow.locator('.CodeMirror').nth(1);\n    await valueEditor.click();\n    await page.keyboard.type('custom-value');\n\n    // Verify draft indicator appears in the tab\n    await expect(collectionTab.locator('.has-changes-icon')).toBeVisible();\n    await expect(collectionTab.locator('.close-icon')).not.toBeVisible();\n\n    // Save the changes\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    // Verify draft indicator is gone after saving\n    await expect(collectionTab.locator('.close-icon')).toBeVisible();\n    await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible();\n  });\n\n  test('Verify draft indicator appears when changing collection settings - Auth', async ({ page }) => {\n    // Verify the collection settings tab is open\n    const collectionTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) });\n    await expect(collectionTab).toBeVisible();\n\n    // Verify initially there is NO draft indicator\n    await expect(collectionTab.locator('.close-icon')).toBeVisible();\n    await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible();\n\n    // Click on Auth tab\n    await page.locator('.tab.auth').click();\n\n    // Change auth mode from 'none' to 'bearer' by clicking the dropdown\n    await page.locator('.auth-mode-label').click();\n    await page.locator('.dropdown-item').filter({ hasText: 'Bearer Token' }).click();\n\n    // Verify draft indicator appears in the tab\n    await expect(collectionTab.locator('.has-changes-icon')).toBeVisible();\n    await expect(collectionTab.locator('.close-icon')).not.toBeVisible();\n\n    // Save the changes\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    // Verify draft indicator is gone after saving\n    await expect(collectionTab.locator('.close-icon')).toBeVisible();\n    await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible();\n  });\n\n  test('Verify draft indicator appears when changing collection settings - Protobuf', async ({ page }) => {\n    const collectionTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) });\n    await expect(collectionTab).toBeVisible();\n\n    // Verify initially there is NO draft indicator\n    await expect(collectionTab.locator('.close-icon')).toBeVisible();\n    await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible();\n\n    // Click on Protobuf tab\n    await page.locator('.tab.protobuf').click();\n\n    // Add a new proto file - handle file picker dialog\n    const fileChooserPromise = page.waitForEvent('filechooser');\n    await page.getByTestId('protobuf-add-file-button').click();\n    const fileChooser = await fileChooserPromise;\n    await fileChooser.setFiles('./tests/collection/draft/fixtures/grpcbin.proto');\n\n    // Wait for the file to be processed and added to the table\n    // The file goes through IPC to get the path, then Redux to update state\n    const protoFilesTable = page.getByTestId('protobuf-proto-file-name');\n    await expect(protoFilesTable.getByText('grpcbin.proto')).toBeVisible();\n\n    // Verify draft indicator appears\n    await expect(collectionTab.locator('.has-changes-icon')).toBeVisible();\n    await expect(collectionTab.locator('.close-icon')).not.toBeVisible();\n\n    // Save the changes\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    // Verify draft indicator is gone after saving\n    await expect(collectionTab.locator('.close-icon')).toBeVisible();\n    await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible();\n  });\n\n  test('Verify draft indicator appears when changing client certificate settings', async ({ page }) => {\n    const collectionTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) });\n    await expect(collectionTab).toBeVisible();\n\n    // Verify initially there is NO draft indicator\n    await expect(collectionTab.locator('.close-icon')).toBeVisible();\n    await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible();\n\n    // Click on Client Certificates tab\n    await page.locator('.tab.clientCert').click();\n\n    // Fill domain\n    await page.locator('#domain').fill('test.com');\n\n    // Select cert file using file picker (using grpcbin.proto as a dummy file)\n    const certFileChooserPromise = page.waitForEvent('filechooser');\n    await page.locator('input#certFilePath[type=\"file\"]').click();\n    const certFileChooser = await certFileChooserPromise;\n    await certFileChooser.setFiles('./tests/collection/draft/fixtures/grpcbin.proto');\n\n    // Select key file using file picker (using grpcbin.proto as a dummy file)\n    const keyFileChooserPromise = page.waitForEvent('filechooser');\n    await page.locator('input#keyFilePath[type=\"file\"]').click();\n    const keyFileChooser = await keyFileChooserPromise;\n    await keyFileChooser.setFiles('./tests/collection/draft/fixtures/grpcbin.proto');\n\n    // Click Add button\n    await page.getByTestId('add-client-cert').click();\n\n    // Verify draft indicator appears\n    await expect(collectionTab.locator('.has-changes-icon')).toBeVisible();\n    await expect(collectionTab.locator('.close-icon')).not.toBeVisible();\n\n    // Save the changes\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    // Verify draft indicator is gone after saving\n    await expect(collectionTab.locator('.close-icon')).toBeVisible();\n    await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible();\n  });\n\n  test('Verify draft indicator appears when changing proxy settings', async ({ page }) => {\n    const collectionTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) });\n    await expect(collectionTab).toBeVisible();\n\n    // Verify initially there is NO draft indicator\n    await expect(collectionTab.locator('.close-icon')).toBeVisible();\n    await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible();\n\n    // Click on Proxy tab\n    await page.locator('.tab.proxy').click();\n\n    // Enable proxy - select \"enabled\" radio button\n    await page.locator('input[name=\"enabled\"][value=\"true\"]').check();\n\n    // Fill in hostname and port\n    await page.locator('#hostname').fill('localhost');\n    await page.locator('#port').fill('8080');\n\n    // Verify draft indicator appears\n    await expect(collectionTab.locator('.has-changes-icon')).toBeVisible();\n    await expect(collectionTab.locator('.close-icon')).not.toBeVisible();\n\n    // Save the changes\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    // Verify draft indicator is gone after saving\n    await expect(collectionTab.locator('.close-icon')).toBeVisible();\n    await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible();\n  });\n\n  test('Verify draft indicator appears when changing collection settings - Vars', async ({ page }) => {\n    const collectionTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) });\n\n    // Verify initially there is NO draft indicator\n    await expect(collectionTab.locator('.close-icon')).toBeVisible();\n\n    await page.locator('.tab.vars').click();\n\n    const varsTable = page.locator('table').first();\n    const varRow = varsTable.locator('tbody tr').first();\n\n    const varNameInput = varRow.locator('input[type=\"text\"]');\n    await varNameInput.click();\n    await varNameInput.fill('testVar');\n\n    const varValueEditor = varRow.locator('.CodeMirror');\n    await varValueEditor.click();\n    await page.keyboard.type('testValue');\n\n    // Verify draft indicator appears\n    await expect(collectionTab.locator('.has-changes-icon')).toBeVisible();\n    await expect(collectionTab.locator('.close-icon')).not.toBeVisible();\n\n    // Save the changes\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    // Verify draft indicator is gone after saving\n    await expect(collectionTab.locator('.close-icon')).toBeVisible();\n    await expect(collectionTab.locator('.has-changes-icon')).not.toBeVisible();\n  });\n\n  test('Verify draft indicator appears when changing folder settings - Headers', async ({ page }) => {\n    const collectionName = 'test-draft';\n\n    // Create a folder in the collection\n    const collection = page.locator('.collection-name').filter({ hasText: collectionName });\n    await collection.hover(); // Hover on collection to reveal action buttons\n    await collection.locator('.collection-actions .icon').click();\n    await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click();\n\n    // Fill folder name\n    await expect(page.locator('#folder-name')).toBeVisible();\n    await page.locator('#folder-name').fill('test-folder');\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    await expect(page.locator('.collection-item-name').filter({ hasText: 'test-folder' })).toBeVisible();\n\n    // Open folder settings by double-clicking the folder\n    await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).dblclick();\n\n    // Verify folder settings tab is open\n    const folderTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'test-folder' }) });\n    await expect(folderTab).toBeVisible();\n\n    await expect(folderTab.locator('.close-icon')).toBeVisible();\n    await expect(folderTab.locator('.has-changes-icon')).not.toBeVisible();\n\n    const headerTable = page.locator('table').first();\n    const headerRow = headerTable.locator('tbody tr').first();\n\n    const nameEditor = headerRow.locator('.CodeMirror').first();\n    await nameEditor.click();\n    await page.keyboard.type('X-Folder-Header');\n\n    const valueEditor = headerRow.locator('.CodeMirror').nth(1);\n    await valueEditor.click();\n    await page.keyboard.type('folder-value');\n\n    // Verify draft indicator appears in the folder tab\n    await expect(folderTab.locator('.has-changes-icon')).toBeVisible();\n    await expect(folderTab.locator('.close-icon')).not.toBeVisible();\n\n    // Save the changes\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    // Verify draft indicator is gone after saving\n    await expect(folderTab.locator('.close-icon')).toBeVisible();\n    await expect(folderTab.locator('.has-changes-icon')).not.toBeVisible();\n  });\n\n  test('Verify draft indicator appears when changing folder settings - Auth', async ({ page }) => {\n    // Open folder settings by double-clicking the folder from previous test\n    await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).dblclick();\n\n    // Verify folder settings tab is open\n    const folderTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'test-folder' }) });\n    await expect(folderTab).toBeVisible();\n\n    // Verify initially no draft indicator\n    await expect(folderTab.locator('.close-icon')).toBeVisible();\n\n    // Click on Auth tab\n    await page.locator('.tab.auth').click();\n\n    // Change auth mode by clicking the dropdown\n    await page.locator('.auth-mode-label').click();\n    await page.locator('.dropdown-item').filter({ hasText: 'Bearer Token' }).click();\n\n    // Verify draft indicator appears\n    await expect(folderTab.locator('.has-changes-icon')).toBeVisible();\n    await expect(folderTab.locator('.close-icon')).not.toBeVisible();\n\n    // Save the changes\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    // Verify draft indicator is gone\n    await expect(folderTab.locator('.close-icon')).toBeVisible();\n    await expect(folderTab.locator('.has-changes-icon')).not.toBeVisible();\n  });\n\n  test('Verify draft indicator appears when changing folder settings - Vars', async ({ page }) => {\n    // Open folder settings by double-clicking the folder from previous test\n    await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).dblclick();\n\n    // Verify folder settings tab is open\n    const folderTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'test-folder' }) });\n    await expect(folderTab).toBeVisible();\n\n    await expect(folderTab.locator('.close-icon')).toBeVisible();\n\n    await page.locator('.tab.vars').click();\n\n    const varsTable = page.locator('table').first();\n    const varRow = varsTable.locator('tbody tr').first();\n\n    const varNameInput = varRow.locator('input[type=\"text\"]');\n    await varNameInput.click();\n    await varNameInput.fill('folderVar');\n\n    const folderVarValueEditor = varRow.locator('.CodeMirror');\n    await folderVarValueEditor.click();\n    await page.keyboard.type('folderValue');\n\n    // Verify draft indicator appears\n    await expect(folderTab.locator('.has-changes-icon')).toBeVisible();\n    await expect(folderTab.locator('.close-icon')).not.toBeVisible();\n\n    // Save the changes\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    // Verify draft indicator is gone\n    await expect(folderTab.locator('.close-icon')).toBeVisible();\n    await expect(folderTab.locator('.has-changes-icon')).not.toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/collection/draft/draft-values-in-requests.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { createCollection, closeAllCollections } from '../../utils/page';\n\ntest.describe('Draft values are used in requests', () => {\n  test.afterEach(async ({ page }) => {\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  test('Verify draft collection headers are used in HTTP requests', async ({ page, createTmpDir }) => {\n    const collectionName = 'test-draft-headers';\n\n    // Create a new collection\n    await createCollection(page, collectionName, await createTmpDir());\n\n    // Verify the collection settings tab is open\n    await expect(page.locator('.request-tab .tab-label').filter({ hasText: 'Collection' })).toBeVisible();\n\n    await page.locator('.tab.headers').click();\n\n    const headerTable = page.locator('table').first();\n    const headerRow = headerTable.locator('tbody tr').first();\n\n    const nameEditor = headerRow.locator('.CodeMirror').first();\n    await nameEditor.click();\n    await headerRow.locator('textarea').first().fill('X-Draft-Header');\n\n    const valueEditor = headerRow.locator('.CodeMirror').nth(1);\n    await valueEditor.click();\n    await headerRow.locator('textarea').nth(1).fill('draft-value-123');\n\n    // Verify draft indicator appears (header is not saved yet)\n    const collectionTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Collection' }) });\n    await expect(collectionTab.locator('.has-changes-icon')).toBeVisible();\n\n    // Create a folder in the collection\n    const collection = page.locator('.collection-name').filter({ hasText: collectionName });\n\n    await collection.hover();\n    await collection.locator('.collection-actions .icon').click();\n    await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click();\n    await page.locator('#folder-name').fill('Test Folder');\n    await page.getByRole('button', { name: 'Create', exact: true }).click();\n    await page.locator('.collection-item-name').filter({ hasText: 'Test Folder' }).click();\n\n    await expect(page.locator('.collection-item-name').filter({ hasText: 'Test Folder' })).toBeVisible();\n    const folder = page.locator('.collection-item-name').filter({ hasText: 'Test Folder' });\n\n    const folderHeaderTable = page.locator('table').first();\n    const folderHeaderRow = folderHeaderTable.locator('tbody tr').first();\n\n    const folderNameEditor = folderHeaderRow.locator('.CodeMirror').first();\n    await folderNameEditor.click();\n    await folderHeaderRow.locator('textarea').first().fill('X-Folder-Draft-Header');\n\n    const folderValueEditor = folderHeaderRow.locator('.CodeMirror').nth(1);\n    await folderValueEditor.click();\n    await folderHeaderRow.locator('textarea').nth(1).fill('folder-draft-value-123');\n\n    // Create a request in the collection\n    // Create a new request via collection menu\n    await folder.hover();\n    await folder.locator('.menu-icon').click();\n    await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();\n\n    // Fill in request details - using httpbin.org which echoes headers back\n    await page.getByTestId('request-name').fill('Test Request');\n    await page.getByTestId('new-request-url').locator('.CodeMirror').click();\n    await page.keyboard.type('https://httpbin.org/headers');\n    await page.getByRole('button', { name: 'Create', exact: true }).click();\n\n    // Send request and verify draft header is included\n    // Wait for the request tab to be active\n    await expect(page.locator('.request-tab .tab-label').filter({ hasText: 'Test Request' })).toBeVisible();\n\n    // Click on Generate Code from the sidebar request item dropdown\n    const requestItem = page.locator('.collection-item-name').filter({ hasText: 'Test Request' });\n    await expect(requestItem).toBeVisible();\n\n    // Right-click on the request item to open context menu\n    await requestItem.click({ button: 'right' });\n\n    // Click on Generate Code option\n    await page.locator('.dropdown-item').filter({ hasText: 'Generate Code' }).click();\n\n    // Wait for the Generate Code modal to open\n    await expect(page.getByTestId('modal-close-button')).toBeVisible();\n\n    // Wait for code generator to be visible\n    const codeGenerator = page.locator('.code-generator');\n    await expect(codeGenerator).toBeVisible();\n\n    // Target the CodeMirror specifically within the code generator modal\n    const generatedCodeEditor = codeGenerator.locator('.editor-container .CodeMirror').first();\n    await expect(generatedCodeEditor).toBeVisible();\n\n    // Wait for code generation to complete by checking for the URL in the generated code\n    await expect(generatedCodeEditor).toContainText('https://httpbin.org/headers');\n\n    // Check that the generated code contains the draft header\n    // The header appears as a --header argument in the generated curl/httpie/wget command\n    await expect(generatedCodeEditor).toContainText('x-draft-header');\n    await expect(generatedCodeEditor).toContainText('draft-value-123');\n    await expect(generatedCodeEditor).toContainText('x-folder-draft-header');\n    await expect(generatedCodeEditor).toContainText('folder-draft-value-123');\n\n    // Close the modal by clicking the X button using the test id\n    await page.getByTestId('modal-close-button').click();\n\n    // Wait for modal to fully close before continuing\n    await page.waitForSelector('.bruno-modal', { state: 'hidden', timeout: 10000 });\n    await page.waitForSelector('.bruno-modal-backdrop', { state: 'hidden', timeout: 10000 });\n  });\n\n  test('Verify draft for proxy settings are used in HTTP requests', async ({ page, createTmpDir }) => {\n    test.skip(true, 'Temporarily skipping this test because of proxy-related problems');\n    const collectionName = 'test-draft-proxy-settings';\n\n    // Create a new collection\n    await createCollection(page, collectionName, await createTmpDir());\n\n    // Create a new request from collection menu\n    const collection = page.locator('.collection-name').filter({ hasText: collectionName });\n    await collection.hover();\n    await collection.locator('.collection-actions .icon').click({ force: true });\n    await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();\n    await page.getByTestId('request-name').fill('Test Request');\n    await page.getByTestId('new-request-url').locator('.CodeMirror').click();\n    await page.keyboard.type('https://testbench-sanity.usebruno.com/ping');\n    await page.getByRole('button', { name: 'Create', exact: true }).click();\n\n    // Verify the request is created\n    await expect(page.locator('.collection-item-name').filter({ hasText: 'Test Request' })).toBeVisible();\n    const request = page.locator('.collection-item-name').filter({ hasText: 'Test Request' });\n\n    // Run the request with inherit timeout\n    await page.getByTestId('send-arrow-icon').click();\n    await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 });\n\n    // Click on collection in sidebar to open collection settings\n    await page.locator('#sidebar-collection-name').filter({ hasText: collectionName }).click();\n\n    // Go to Proxy Settings tab\n    await page.locator('.tab.proxy').click();\n    await page.locator('input[name=\"enabled\"][value=\"true\"]').check();\n    await page.locator('#hostname').fill('localhost');\n    await page.locator('#port').fill('8080');\n\n    await page.locator('.collection-item-name').filter({ hasText: 'Test Request' }).click();\n\n    // Run the request again\n    await page.getByTestId('send-arrow-icon').click();\n    await expect(page.getByText('Error occurred while executing the request!')).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/collection/draft/fixtures/grpcbin.proto",
    "content": "syntax = \"proto3\";\n\npackage grpcbin;\n\nservice GRPCBin {\n  // This endpoint\n  rpc Index(EmptyMessage) returns (IndexReply) {}\n  // Unary endpoint that takes no argument and replies an empty message.\n  rpc Empty(EmptyMessage) returns (EmptyMessage) {}\n  // Unary endpoint that replies a received DummyMessage\n  rpc DummyUnary(DummyMessage) returns (DummyMessage) {}\n  // Stream endpoint that sends back 10 times the received DummyMessage\n  rpc DummyServerStream(DummyMessage) returns (stream DummyMessage) {}\n  // Stream endpoint that receives 10 DummyMessages and replies with the last received one\n  rpc DummyClientStream(stream DummyMessage) returns (DummyMessage) {}\n  // Stream endpoint that sends back a received DummyMessage indefinitely (chat mode)\n  rpc DummyBidirectionalStreamStream(stream DummyMessage) returns (stream DummyMessage) {}\n  // Unary endpoint that raises a specified (by code) gRPC error\n  rpc SpecificError(SpecificErrorRequest) returns (EmptyMessage) {}\n  // Unary endpoint that raises a random gRPC error\n  rpc RandomError(EmptyMessage) returns (EmptyMessage) {}\n  // Unary endpoint that returns headers\n  rpc HeadersUnary(EmptyMessage) returns (HeadersMessage) {}\n  // Unary endpoint that returns no respnose\n  rpc NoResponseUnary(EmptyMessage) returns (EmptyMessage) {}\n}\n\nmessage HeadersMessage {\n  message Values {\n    repeated string values = 1;\n  }\n  map<string, Values> Metadata = 1;\n}\n\nmessage SpecificErrorRequest {\n  uint32 code = 1;\n  string reason = 2;\n}\n\nmessage EmptyMessage {}\n\nmessage DummyMessage {\n  message Sub {\n    string f_string = 1;\n  }\n  enum Enum {\n    ENUM_0 = 0;\n    ENUM_1 = 1;\n    ENUM_2 = 2;\n  }\n  string f_string = 1;\n  repeated string f_strings = 2;\n  int32 f_int32 = 3;\n  repeated int32 f_int32s = 4;\n  Enum f_enum = 5;\n  repeated Enum f_enums = 6;\n  Sub f_sub = 7;\n  repeated Sub f_subs = 8;\n  bool f_bool = 9;\n  repeated bool f_bools = 10;\n  int64 f_int64 = 11;\n  repeated int64 f_int64s= 12;\n  bytes f_bytes = 13;\n  repeated bytes f_bytess = 14;\n  float f_float = 15;\n  repeated float f_floats = 16;\n  // TODO: timestamp, duration, oneof, any, maps, fieldmask, wrapper type, struct, listvalue, value, nullvalue, deprecated\n}\n\nmessage IndexReply {\n  message Endpoint {\n    string path = 1;\n    string description = 2;\n  }\n  string description = 1;\n  repeated Endpoint endpoints = 2;\n}\n"
  },
  {
    "path": "tests/collection/draft/fixtures/mitmproxy-ca-cert.cer",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDNTCCAh2gAwIBAgIUJr/uwo5anPA3YCk1swJPk4z0ZOswDQYJKoZIhvcNAQEL\nBQAwKDESMBAGA1UEAwwJbWl0bXByb3h5MRIwEAYDVQQKDAltaXRtcHJveHkwHhcN\nMjUwNjEwMTgwNTM3WhcNMzUwNjEwMTgwNTM3WjAoMRIwEAYDVQQDDAltaXRtcHJv\neHkxEjAQBgNVBAoMCW1pdG1wcm94eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\nAQoCggEBAL+bc9tuU/bv7+20IhQ7lMeemBsZiiccWk11fUwYlO1oOgTh0YCQk04J\nWu7WvA52OTZp9CtwRCF+iUwHw8iIYlNMc9RBiTdfYA8KBxia3NEJBllPGxawGjzJ\nCyjemGuC5f2pjRa2lVZnFBIfdEzYT9WyjsMovJAhhm88P17JF6jr2UTG5S8gzdyO\n/ArKxDtNnebXOFKtxgiB1QAE3fm8EQC5neD6bUr+UfvHEAzIUhJfco5ckEk50yXR\nheRNMnSOycQcMRwlO7/IGtTru+sM+tnrlXdMmX0j0dRzuZEDItGA78O/mMSdGgJJ\nKwRf9MplHPx+F+7Bl30oz1I/QiDzqPkCAwEAAaNXMFUwDwYDVR0TAQH/BAUwAwEB\n/zATBgNVHSUEDDAKBggrBgEFBQcDATAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE\nFEwsfBQerk5DimWXnYFPvrczmQL1MA0GCSqGSIb3DQEBCwUAA4IBAQCb+S04pxue\nu9xtyGZJ32pxE9erUAB/ONYKSw0+ab2qdySBhNjRalwrm9NHlJoL/0g4p0pCV5sd\n3lro5POrsfBcANdDSQ/e//jJG3gt/6ipgSVgeFW9LGx0INJAByhvkKvNbpWKiS9i\n4iGGIFxzPQLac2lHL6BTgV0mwkHC1YI9zSLpunqiQFRbU497MbZDmLEw57i2C0MB\nRt7Ri9Ah0ajApPCofGFXvnKPf6SL4a0xkd3SUgXtovIdzTYPuhwXlJDoUkQuUs1G\nhq0M++IKXL6DqFp+T+zDrnEWLuzJ0uLo1VDEMBIFbaK2WOh1sMaGz75No6s+kZlG\nLkFX7Z0xl4iK\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/collection/moving-requests/cross-collection-drag-drop-folder.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections, createCollection } from '../../utils/page';\n\ntest.describe('Cross-Collection Drag and Drop for folder', () => {\n  test.afterEach(async ({ page }) => {\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  test('Verify cross-collection folder drag and drop', async ({ page, createTmpDir }) => {\n    // Create first collection - open with sandbox mode\n    await createCollection(page, 'source-collection', await createTmpDir('source-collection'));\n\n    // Create a folder in the first collection\n    // Look for the collection menu button for the source collection specifically\n    const sourceCollectionContainer1 = page.locator('.collection-name').filter({ hasText: 'source-collection' });\n    await sourceCollectionContainer1.hover();\n    await sourceCollectionContainer1.locator('.collection-actions .icon').click();\n    await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click();\n\n    // Fill folder name in the modal\n    await expect(page.locator('#folder-name')).toBeVisible();\n    await page.locator('#folder-name').fill('test-folder');\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    // Wait for the folder to be created and appear in the sidebar\n    await page.waitForTimeout(200);\n    await expect(page.locator('.collection-item-name').filter({ hasText: 'test-folder' })).toBeVisible();\n\n    // Add a request to the folder to make it more realistic\n    await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).hover();\n    await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).locator('.menu-icon').click({ force: true });\n    await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();\n    await page.getByPlaceholder('Request Name').fill('test-request-in-folder');\n    await page.locator('#new-request-url .CodeMirror').click();\n    await page.locator('textarea').fill('https://echo.usebruno.com');\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    // Wait for the request to be created\n    await page.waitForTimeout(200);\n\n    // Expand the folder to see the request inside\n    await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).click();\n    await page.waitForTimeout(200);\n    await expect(page.locator('.collection-item-name').filter({ hasText: 'test-request-in-folder' })).toBeVisible();\n\n    // Create second collection - open with sandbox mode\n    await createCollection(page, 'target-collection', await createTmpDir('target-collection'));\n\n    // Wait for both collections to be visible in sidebar\n    await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' })).toBeVisible();\n    await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' })).toBeVisible();\n\n    // Locate the folder in source collection\n    const sourceFolder = page.locator('.collection-item-name').filter({ hasText: 'test-folder' });\n    await expect(sourceFolder).toBeVisible();\n\n    // Locate the target collection area (the collection name element)\n    const targetCollection = page.locator('.collection-name').filter({ hasText: 'target-collection' });\n    await expect(targetCollection).toBeVisible();\n\n    // Perform drag and drop operation\n    await sourceFolder.dragTo(targetCollection);\n\n    // Wait for the operation to complete\n    await page.waitForTimeout(200);\n\n    // Verify the folder has been moved to the target collection\n    // Check that the folder now appears under target collection\n    const targetCollectionContainer = page\n      .locator('.collection-name')\n      .filter({ hasText: 'target-collection' })\n      .locator('..');\n    await expect(\n      targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-folder' })\n    ).toBeVisible();\n\n    // Expand the moved folder to verify the request inside is also moved\n    await targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-folder' }).click();\n    await page.waitForTimeout(200);\n    await expect(\n      targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-request-in-folder' })\n    ).toBeVisible();\n\n    // Verify the folder is no longer in the source collection\n    const sourceCollectionContainer = page\n      .locator('.collection-name')\n      .filter({ hasText: 'source-collection' })\n      .locator('..');\n    await expect(\n      sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-folder' })\n    ).not.toBeVisible();\n\n    // Verify the request is also no longer in the source collection\n    await expect(\n      sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: 'test-request-in-folder' })\n    ).not.toBeVisible();\n  });\n\n  test('Verify cross-collection folder drag and drop, a duplicate folder exist. expected to throw error toast', async ({\n    page,\n    createTmpDir\n  }) => {\n    // Create first collection (source) - use unique names for this test\n    await createCollection(page, 'source-collection', await createTmpDir('source-collection'));\n\n    // Create a folder in the first collection\n    await page\n      .locator('.collection-name')\n      .filter({ hasText: 'source-collection' })\n      .locator('..')\n      .locator('.collection-actions')\n      .hover();\n    await page\n      .locator('.collection-name')\n      .filter({ hasText: 'source-collection' })\n      .locator('..')\n      .locator('.collection-actions .icon')\n      .click();\n    await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click();\n    await expect(page.locator('#folder-name')).toBeVisible();\n    await page.locator('#folder-name').fill('folder-1');\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    await expect(page.locator('.collection-item-name').filter({ hasText: 'folder-1' })).toBeVisible();\n\n    // Add a request to the folder to make it more realistic\n    await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).hover();\n    await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).locator('.menu-icon').click({ force: true });\n    await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();\n    await page.getByPlaceholder('Request Name').fill('http-request');\n    await page.locator('#new-request-url .CodeMirror').click();\n    await page.locator('textarea').fill('https://echo.usebruno.com');\n    await page.getByRole('button', { name: 'Create' }).click();\n    // Expand the folder to see the request inside\n    await page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).click();\n    await expect(page.locator('.collection-item-name').filter({ hasText: 'http-request' })).toBeVisible();\n\n    // Create second collection (target)\n    await createCollection(page, 'target-collection', await createTmpDir('target-collection'));\n\n    // Create a folder with the same name in the target collection\n    await page\n      .locator('.collection-name')\n      .filter({ hasText: 'target-collection' })\n      .locator('..')\n      .locator('.collection-actions')\n      .hover();\n    await page\n      .locator('.collection-name')\n      .filter({ hasText: 'target-collection' })\n      .locator('..')\n      .locator('.collection-actions .icon')\n      .click();\n    await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click();\n    await expect(page.locator('#folder-name')).toBeVisible();\n    await page.locator('#folder-name').fill('folder-1');\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    // Verify we have the folder to drag in the source collection\n    const sourceFolder = page.locator('.collection-item-name').filter({ hasText: 'folder-1' }).first();\n    await expect(sourceFolder).toBeVisible();\n\n    // Locate the target collection area\n    const targetCollection = page.locator('.collection-name').filter({ hasText: 'target-collection' });\n    await expect(targetCollection).toBeVisible();\n\n    // Perform drag and drop operation\n    await sourceFolder.dragTo(targetCollection);\n\n    // check for error toast notification\n    await expect(page.getByText(/Error: Cannot copy.*already exists/i)).toBeVisible();\n\n    // source and target collection request should remain unchanged\n    const sourceCollectionContainer = page\n      .locator('.collection-name')\n      .filter({ hasText: 'source-collection' })\n      .locator('..');\n    await expect(\n      sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: 'folder-1' })\n    ).toBeVisible();\n    await expect(\n      sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: 'http-request' })\n    ).toBeVisible();\n\n    const targetCollectionContainer = page\n      .locator('.collection-name')\n      .filter({ hasText: 'target-collection' })\n      .locator('..');\n    await expect(\n      targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'folder-1' })\n    ).toBeVisible();\n    await expect(\n      targetCollectionContainer.locator('.collection-item-name').filter({ hasText: 'http-request' })\n    ).not.toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/collection/moving-requests/cross-collection-drag-drop-request.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections, createCollection, createRequest } from '../../utils/page';\n\ntest.describe('Cross-Collection Drag and Drop', () => {\n  test.afterEach(async ({ page }) => {\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  test('Verify request drag and drop', async ({ page, createTmpDir }) => {\n    const requestName = 'drag-drop-request';\n\n    // Create first collection - open with sandbox mode\n    await createCollection(page, 'source-collection', await createTmpDir('source-collection'));\n\n    // Create a request in the first collection using the dialog/modal flow\n    await createRequest(page, requestName, 'source-collection', { url: 'https://echo.usebruno.com' });\n\n    // Create second collection - open with sandbox mode\n    await createCollection(page, 'target-collection', await createTmpDir('target-collection'));\n\n    await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' })).toBeVisible();\n    await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' })).toBeVisible();\n\n    // Locate the request in source collection\n    const sourceCollectionContainer = page\n      .locator('.collection-name')\n      .filter({ hasText: 'source-collection' })\n      .locator('..');\n    const sourceRequest = sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: requestName }).first();\n    await expect(sourceRequest).toBeVisible();\n\n    // Locate the target collection area (the collection name element)\n    const targetCollection = page.locator('.collection-name').filter({ hasText: 'target-collection' });\n    await expect(targetCollection).toBeVisible();\n\n    // Perform drag and drop operation\n    await sourceRequest.dragTo(targetCollection);\n\n    // Verify the request has been moved to the target collection\n    // Check that the request now appears under target collection\n    const targetCollectionContainer = page\n      .locator('.collection-name')\n      .filter({ hasText: 'target-collection' })\n      .locator('..');\n    await expect(targetCollectionContainer.locator('.collection-item-name').filter({ hasText: requestName })).toBeVisible();\n\n    // Verify the request is no longer in the source collection\n    await page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' }).click();\n    await expect(sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: requestName })).toHaveCount(0);\n  });\n\n  test('Expected to show error toast message, when duplicate request found in drop location', async ({\n    page,\n    createTmpDir\n  }) => {\n    const requestName = 'duplicate-request';\n\n    // Create first collection (source-collection)\n    await createCollection(page, 'source-collection', await createTmpDir('source-collection'));\n\n    // Create a request in the first collection using the dialog/modal flow\n    await createRequest(page, requestName, 'source-collection', { url: 'https://echo.usebruno.com' });\n\n    // Create second collection (target-collection)\n    await createCollection(page, 'target-collection', await createTmpDir('target-collection'));\n\n    // Create a request with the same name in the target collection using the dialog/modal flow\n    await createRequest(page, requestName, 'target-collection', { url: 'https://echo.usebruno.com' });\n\n    // Go back to source collection to drag the request\n    await page.locator('#sidebar-collection-name').filter({ hasText: 'source-collection' }).click();\n\n    const sourceCollectionContainer = page\n      .locator('.collection-name')\n      .filter({ hasText: 'source-collection' })\n      .locator('..');\n    const targetCollectionContainer = page\n      .locator('.collection-name')\n      .filter({ hasText: 'target-collection' })\n      .locator('..');\n\n    const sourceRequest = sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: requestName }).first();\n    await expect(sourceRequest).toBeVisible();\n\n    // Locate the target collection area\n    const targetCollection = page.locator('.collection-name').filter({ hasText: 'target-collection' });\n    await expect(targetCollection).toBeVisible();\n\n    // Perform drag and drop operation to target-collection\n    await sourceRequest.dragTo(targetCollection);\n\n    // check for error toast notification\n    await expect(page.getByText(/Error: Cannot copy.*already exists/i)).toBeVisible();\n\n    // source and target collection request should remain unchanged\n    await expect(sourceCollectionContainer.locator('.collection-item-name').filter({ hasText: requestName }).first()).toBeVisible();\n    await page.locator('#sidebar-collection-name').filter({ hasText: 'target-collection' }).click();\n    await expect(targetCollectionContainer.locator('.collection-item-name').filter({ hasText: requestName }).first()).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/collection/moving-requests/tag-persistence.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections, createCollection, createRequest, saveRequest, selectRequestPaneTab } from '../../utils/page';\nimport { buildCommonLocators } from '../../utils/page/locators';\n\ntest.describe('Tag persistence', () => {\n  test.afterEach(async ({ page }) => {\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  test('Verify tag persistence while moving requests within a collection', async ({ page, createTmpDir }) => {\n    const locators = buildCommonLocators(page);\n    const collectionName = 'test-collection';\n    const requestUrl = 'https://httpfaker.org/api/echo';\n    const tagName = 'smoke';\n\n    // Create first collection\n    await createCollection(page, collectionName, await createTmpDir(collectionName));\n    // Create three requests via the dialog/modal flow, then add a tag to each\n    const requestNames = ['request-1', 'request-2', 'request-3'];\n    for (const requestName of requestNames) {\n      await createRequest(page, requestName, collectionName, { url: requestUrl });\n      await locators.sidebar.request(requestName).click();\n      await locators.tabs.requestTab(requestName).waitFor({ state: 'visible' });\n      await selectRequestPaneTab(page, 'Settings');\n      await expect(locators.tags.input()).toBeVisible();\n      await locators.tags.input().fill(tagName);\n      await locators.tags.input().press('Enter');\n      await expect(locators.tags.item(tagName)).toBeVisible();\n      await saveRequest(page);\n    }\n\n    // Move the last request to just above the first request within the same collection\n    const r3Request = locators.sidebar.request('request-3');\n    const r1Request = locators.sidebar.request('request-1');\n\n    await expect(r3Request).toBeVisible();\n    await expect(r1Request).toBeVisible();\n\n    // Perform drag and drop operation to move the last request above the first using source position\n    await r3Request.dragTo(r1Request, {\n      targetPosition: { x: 0, y: 1 }\n    });\n\n    // Verify the requests are still in the collection\n    for (const requestName of requestNames) {\n      await expect(locators.sidebar.request(requestName)).toBeVisible();\n    }\n\n    // Click on the moved request to verify the tag persisted after the move\n    await r3Request.click();\n    await locators.tabs.requestTab('request-3').waitFor({ state: 'visible' });\n    await selectRequestPaneTab(page, 'Settings');\n    // Verify the tag is still present after the move\n    await expect(locators.tags.item(tagName)).toBeVisible();\n  });\n\n  test('verify tag persistence while moving requests between folders', async ({ page, createTmpDir }) => {\n    const locators = buildCommonLocators(page);\n    // Create first collection\n    await createCollection(page, 'test-collection', await createTmpDir('test-collection'));\n\n    // Create a new folder\n    await locators.sidebar.collectionRow('test-collection').hover();\n    await locators.actions.collectionActions('test-collection').click();\n    await page.waitForTimeout(1);\n    await locators.dropdown.item('New Folder').click();\n    await page.locator('#folder-name').fill('folder-1');\n    await locators.modal.button('Create').click();\n    await expect(locators.sidebar.folder('folder-1')).toBeVisible();\n\n    // Create a new request within folder-1 folder\n    await locators.sidebar.folder('folder-1').click();\n\n    await locators.sidebar.folder('folder-1').hover();\n    await locators.actions.collectionItemActions('folder-1').click();\n    await locators.dropdown.item('New Request').click();\n    await locators.request.requestNameInput().fill('request-1');\n    await locators.request.newRequestUrl().click();\n    await page.keyboard.type('https://httpfaker.org/api/echo');\n    await locators.modal.button('Create').click();\n\n    // create another request within folder-1 folder\n    await locators.sidebar.folder('folder-1').hover();\n    await locators.actions.collectionItemActions('folder-1').click();\n    await locators.dropdown.item('New Request').click();\n    await locators.request.requestNameInput().fill('request-2');\n    await locators.request.newRequestUrl().click();\n    await page.keyboard.type('https://httpfaker.org/api/echo');\n    await locators.modal.button('Create').click();\n    await expect(locators.sidebar.folderRequest('folder-1', 'request-2')).toBeVisible();\n    await locators.sidebar.folderRequest('folder-1', 'request-2').click();\n    await expect(locators.tabs.activeRequestTab()).toContainText('request-2');\n\n    // Add a tag to the request\n    await selectRequestPaneTab(page, 'Settings');\n    await expect(locators.tags.input()).toBeVisible();\n\n    await locators.tags.input().fill('smoke');\n    await locators.tags.input().press('Enter');\n    await expect(locators.tags.item('smoke')).toBeVisible();\n    const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';\n    await page.keyboard.press(saveShortcut);\n\n    // Create another folder\n    await locators.sidebar.collectionRow('test-collection').hover();\n    await locators.actions.collectionActions('test-collection').click();\n    await locators.dropdown.item('New Folder').click();\n    await page.locator('#folder-name').fill('folder-2');\n    await locators.modal.button('Create').click();\n\n    // open folder-2 folder\n    await locators.sidebar.folder('folder-2').click();\n    await locators.sidebar.folder('folder-2').hover();\n    await locators.actions.collectionItemActions('folder-2').click();\n    await locators.dropdown.item('New Request').click();\n    await locators.request.requestNameInput().fill('request-3');\n    await locators.request.newRequestUrl().click();\n    await page.keyboard.type('https://httpfaker.org/api/echo');\n    await locators.modal.button('Create').click();\n\n    // Drag and drop request-2 request to folder-2 folder\n    const r2Request = locators.sidebar.request('request-2');\n    const f2Folder = locators.sidebar.folder('folder-2');\n    await r2Request.dragTo(f2Folder);\n\n    const request2 = locators.sidebar.folderRequest('folder-2', 'request-2');\n    await expect(request2).toBeVisible();\n\n    // Click on request-2 to verify the tag persisted after the move\n    await request2.click();\n    await locators.tabs.requestTab('request-2').waitFor({ state: 'visible' });\n\n    await selectRequestPaneTab(page, 'Settings');\n    await expect(locators.tags.item('smoke')).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/collection/moving-tabs/move-tabs.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections, createCollection } from '../../utils/page';\n\ntest.describe('Move tabs', () => {\n  test.afterEach(async ({ page }) => {\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  test('Verify tab move by drag and drop', async ({ page, createTmpDir }) => {\n    // Create a collection\n    await createCollection(page, 'source-collection-drag-drop', await createTmpDir('source-collection-drag-drop'));\n\n    // Create a folder in the collection\n    const sourceCollection = page.locator('.collection-name').filter({ hasText: 'source-collection-drag-drop' });\n    await sourceCollection.hover();\n    await sourceCollection.locator('.collection-actions .icon').click();\n    await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click();\n\n    // Fill folder name in the modal\n    await expect(page.locator('#folder-name')).toBeVisible();\n    await page.locator('#folder-name').fill('test-folder');\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    // Wait for the folder to be created and appear in the sidebar\n    await page.waitForTimeout(2000);\n    await expect(page.locator('.collection-item-name').filter({ hasText: 'test-folder' })).toBeVisible();\n\n    // Open the folder tab\n    await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).dblclick();\n    await page.waitForTimeout(500);\n    await expect(page.locator('.request-tab .tab-label').filter({ hasText: 'test-folder' })).toBeVisible();\n\n    // Add a request to the collection\n    await sourceCollection.hover();\n    await sourceCollection.locator('.collection-actions .icon').click();\n    await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();\n    await page.getByPlaceholder('Request Name').fill('test-request');\n    await page.locator('#new-request-url .CodeMirror').click();\n    await page.locator('#new-request-url textarea').fill('https://echo.usebruno.com');\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    // Wait for the request to be created\n    await page.waitForTimeout(1000);\n    await expect(page.locator('.collection-item-name').filter({ hasText: 'test-request' })).toBeVisible();\n\n    // Open the request tab\n    await page.locator('.collection-item-name').filter({ hasText: 'test-request' }).dblclick();\n    await page.waitForTimeout(500);\n    await expect(page.locator('.request-tab .tab-label').filter({ hasText: 'test-request' })).toBeVisible();\n\n    // Verify order of tabs before move\n    const tabs = page.locator('.request-tab .tab-label');\n    await expect(tabs.nth(0)).toHaveText('test-folder');\n    await expect(tabs.nth(1)).toHaveText('GETtest-request');\n\n    // Drag and drop the request tab before the folder tab\n    let source = page.locator('.request-tab .tab-label').filter({ hasText: 'test-request' });\n    let target = page.locator('.request-tab .tab-label').filter({ hasText: 'test-folder' });\n    let sourceBox = await source.boundingBox();\n    let targetBox = await target.boundingBox();\n\n    if (sourceBox && targetBox) {\n      await page.mouse.move(sourceBox.x + sourceBox.width / 2, sourceBox.y + sourceBox.height / 2);\n      await page.mouse.down();\n      await page.mouse.move(targetBox.x + targetBox.width / 2, targetBox.y + targetBox.height / 2, { steps: 5 });\n      await page.mouse.up();\n    }\n\n    // Verify order of tabs after drag and drop\n    await expect(tabs.nth(0)).toHaveText('GETtest-request');\n    await expect(tabs.nth(1)).toHaveText('test-folder');\n\n    // Drag and drop the request tab back to its original position\n    source = page.locator('.request-tab .tab-label').filter({ hasText: 'test-request' });\n    target = page.locator('.request-tab .tab-label').filter({ hasText: 'test-folder' });\n    sourceBox = await source.boundingBox();\n    targetBox = await target.boundingBox();\n\n    if (sourceBox && targetBox) {\n      await page.mouse.move(sourceBox.x + sourceBox.width / 2, sourceBox.y + sourceBox.height / 2);\n      await page.mouse.down();\n      await page.mouse.move(targetBox.x + targetBox.width / 2, targetBox.y + targetBox.height + 10, { steps: 5 });\n      await page.mouse.up();\n    }\n  });\n\n  test('Verify tab move by keyboard shortcut', async ({ page, createTmpDir }) => {\n    // Create a collection\n    await createCollection(page, 'source-collection-keyboard-shortcut', await createTmpDir('source-collection-keyboard-shortcut'));\n\n    // Create a folder in the collection\n    const sourceCollection = page.locator('.collection-name').filter({ hasText: 'source-collection-keyboard-shortcut' });\n    await sourceCollection.hover();\n    await sourceCollection.locator('.collection-actions .icon').click();\n    await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click();\n\n    // Fill folder name in the modal\n    await expect(page.locator('#folder-name')).toBeVisible();\n    await page.locator('#folder-name').fill('test-folder');\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    // Wait for the folder to be created and appear in the sidebar\n    await page.waitForTimeout(2000);\n    await expect(page.locator('.collection-item-name').filter({ hasText: 'test-folder' })).toBeVisible();\n\n    // Open the folder tab\n    await page.locator('.collection-item-name').filter({ hasText: 'test-folder' }).dblclick();\n    await page.waitForTimeout(500);\n    await expect(page.locator('.request-tab .tab-label').filter({ hasText: 'test-folder' })).toBeVisible();\n\n    // Add a request to the collection\n    await sourceCollection.hover();\n    await sourceCollection.locator('.collection-actions .icon').click();\n    await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();\n    await page.getByPlaceholder('Request Name').fill('test-request');\n    await page.locator('#new-request-url .CodeMirror').click();\n    await page.locator('#new-request-url textarea').fill('https://echo.usebruno.com');\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    // Wait for the request to be created\n    await page.waitForTimeout(1000);\n    await expect(page.locator('.collection-item-name').filter({ hasText: 'test-request' })).toBeVisible();\n\n    // Open the request tab\n    await page.locator('.collection-item-name').filter({ hasText: 'test-request' }).dblclick();\n    await page.waitForTimeout(500);\n    await expect(page.locator('.request-tab .tab-label').filter({ hasText: 'test-request' })).toBeVisible();\n\n    // Verify order of tabs before move\n    const tabs = page.locator('.request-tab .tab-label');\n    await expect(tabs.nth(0)).toHaveText('test-folder');\n    await expect(tabs.nth(1)).toHaveText('GETtest-request');\n\n    // Move the request tab before the folder tab using keyboard shortcut\n    const source = page.locator('.request-tab .tab-label').filter({ hasText: 'test-request' });\n    await source.click();\n    await page.keyboard.press('ControlOrMeta+Shift+PageUp');\n    await page.waitForTimeout(500);\n\n    // Verify order of tabs after move\n    await expect(tabs.nth(0)).toHaveText('GETtest-request');\n    await expect(tabs.nth(1)).toHaveText('test-folder');\n\n    // Move the request tab back to its original position using keyboard shortcut\n    await source.click();\n    await page.keyboard.press('ControlOrMeta+Shift+PageDown');\n    await page.waitForTimeout(500);\n\n    // Verify order of tabs after move\n    await expect(tabs.nth(0)).toHaveText('test-folder');\n    await expect(tabs.nth(1)).toHaveText('GETtest-request');\n  });\n});\n"
  },
  {
    "path": "tests/collection/open/open-multiple-collections.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\nimport * as fs from 'fs';\nimport { closeAllCollections } from '../../utils/page';\n\ntest.describe('Open Multiple Collections', () => {\n  let originalShowOpenDialog;\n\n  test.beforeAll(async ({ electronApp }) => {\n    // save the original showOpenDialog function\n    await electronApp.evaluate(({ dialog }) => {\n      originalShowOpenDialog = dialog.showOpenDialog;\n    });\n  });\n\n  test.afterAll(async ({ electronApp }) => {\n    // restore the original showOpenDialog function\n    await electronApp.evaluate(({ dialog }) => {\n      dialog.showOpenDialog = originalShowOpenDialog;\n    });\n  });\n\n  test('Should open multiple collections using Open Collection feature', async ({\n    page,\n    electronApp,\n    createTmpDir\n  }) => {\n    // Create two test collections with proper bruno.json files\n    const collection1Dir = await createTmpDir('collection-1');\n    const collection2Dir = await createTmpDir('collection-2');\n\n    // Create bruno.json for first collection\n    const collection1Config = {\n      version: '1',\n      name: 'Test Collection 1',\n      type: 'collection'\n    };\n    // Create bruno.json for second collection\n    const collection2Config = {\n      version: '1',\n      name: 'Test Collection 2',\n      type: 'collection'\n    };\n\n    fs.writeFileSync(path.join(collection1Dir, 'bruno.json'), JSON.stringify(collection1Config, null, 2));\n    fs.writeFileSync(path.join(collection2Dir, 'bruno.json'), JSON.stringify(collection2Config, null, 2));\n\n    // Mock the electron dialog to return multiple folder selections\n    await electronApp.evaluate(({ dialog }, { collection1Dir, collection2Dir }) => {\n      dialog.showOpenDialog = async () => ({\n        canceled: false,\n        filePaths: [collection1Dir, collection2Dir]\n      });\n    },\n    { collection1Dir, collection2Dir });\n\n    await expect(page.locator('#sidebar-collection-name').getByText('Test Collection 1')).not.toBeVisible();\n\n    // Click on plus icon button and then \"Open collection\" in the dropdown\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Open collection' }).click();\n\n    // Wait for both collections to appear in the sidebar\n    const collection1Element = page.locator('#sidebar-collection-name').getByText('Test Collection 1');\n    const collection2Element = page.locator('#sidebar-collection-name').getByText('Test Collection 2');\n\n    await expect(collection1Element).toBeVisible();\n    await expect(collection2Element).toBeVisible();\n\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  test('Should handle invalid collection path and display error', async ({\n    page,\n    electronApp,\n    createTmpDir\n  }) => {\n    // Directory without bruno.json file\n    const collection1Dir = await createTmpDir('collection-1');\n    const collection2Dir = 'invalid-collection-path';\n\n    // Count collections before attempting to open invalid ones\n    const collectionCountBefore = await page.locator('#sidebar-collection-name').count();\n\n    // Mock the electron dialog to return multiple folder selections\n    await electronApp.evaluate(({ dialog }, { collection1Dir, collection2Dir }) => {\n      dialog.showOpenDialog = async () => ({\n        canceled: false,\n        filePaths: [collection1Dir, collection2Dir]\n      });\n    },\n    { collection1Dir, collection2Dir });\n\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Open collection' }).click();\n\n    // Wait for error toasts to appear\n    await page.waitForTimeout(1000);\n\n    // Verify no collections were opened\n    await expect(page.locator('#sidebar-collection-name')).toHaveCount(collectionCountBefore);\n\n    // Verify invalid collection error\n    const invalidCollectionError = page.getByText('The collection is not valid').first();\n    await expect(invalidCollectionError).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/cookies/cookie-persistence.spec.ts",
    "content": "import { test, expect, closeElectronApp } from '../../playwright';\n\ntest('should persist cookies across app restarts', async ({ createTmpDir, launchElectronApp }) => {\n  // Create a temporary user-data directory so we control where the cookies store file is written.\n  const userDataPath = await createTmpDir('cookie-persistence');\n\n  const app1 = await launchElectronApp({ userDataPath });\n  const page1 = await app1.firstWindow();\n  await page1.waitForSelector('[data-trigger=\"cookies\"]');\n\n  // Open Cookies modal via the status-bar button.\n  await page1.click('[data-trigger=\"cookies\"]');\n\n  // When no cookies are present the modal shows a centred \"Add Cookie\" button.\n  await page1.getByRole('button', { name: /Add Cookie/i }).click();\n\n  // Fill out the form.\n  await page1.fill('input[name=\"domain\"]', 'example.com');\n  await page1.fill('input[name=\"path\"]', '/');\n  await page1.fill('input[name=\"key\"]', 'session');\n  await page1.fill('input[name=\"value\"]', 'abc123');\n  await page1.check('input[name=\"secure\"]');\n  await page1.check('input[name=\"httpOnly\"]');\n\n  await page1.getByRole('button', { name: 'Save' }).click();\n\n  await expect(page1.getByText('example.com')).toBeVisible();\n\n  await closeElectronApp(app1);\n\n  // Second launch – verify the cookie was persisted and re-loaded\n  const app2 = await launchElectronApp({ userDataPath });\n  const page2 = await app2.firstWindow();\n\n  // Open the Cookies modal again.\n  await page2.waitForSelector('[data-trigger=\"cookies\"]');\n  await page2.click('[data-trigger=\"cookies\"]');\n\n  // The domain we added earlier should still be present.\n  await expect(page2.getByText('example.com')).toBeVisible();\n\n  await closeElectronApp(app2);\n});\n"
  },
  {
    "path": "tests/cookies/corrupted-passkey.spec.ts",
    "content": "import { test, expect, closeElectronApp } from '../../playwright';\nimport * as path from 'path';\nimport * as fs from 'fs/promises';\n\ntest('should handle corrupted passkey and still display saved cookie list', async ({ createTmpDir, launchElectronApp }) => {\n  const userDataPath = await createTmpDir('corrupted-passkey');\n\n  const app1 = await launchElectronApp({ userDataPath });\n  // 1. First run – add a cookie via the UI so `cookies.json` is created.\n  const page1 = await app1.firstWindow();\n\n  await page1.waitForSelector('[data-trigger=\"cookies\"]');\n  await page1.click('[data-trigger=\"cookies\"]');\n  await page1.getByRole('button', { name: /Add Cookie/i }).click();\n\n  await page1.fill('input[name=\"domain\"]', 'example.com');\n  await page1.fill('input[name=\"path\"]', '/');\n  await page1.fill('input[name=\"key\"]', 'session');\n  await page1.fill('input[name=\"value\"]', 'abc123');\n  await page1.check('input[name=\"secure\"]');\n  await page1.check('input[name=\"httpOnly\"]');\n\n  await page1.getByRole('button', { name: 'Save' }).click();\n\n  await expect(page1.getByText('example.com')).toBeVisible();\n\n  await closeElectronApp(app1);\n\n  // 2. Corrupt the encryptedPasskey in cookies.json\n  const cookiesFilePath = path.join(userDataPath, 'cookies.json');\n  const raw = await fs.readFile(cookiesFilePath, 'utf-8');\n  const cookiesJson = JSON.parse(raw);\n  cookiesJson.encryptedPasskey = 'deadbeef'; // clearly invalid value\n  await fs.writeFile(cookiesFilePath, JSON.stringify(cookiesJson, null, 2));\n\n  // 3. Second run – Bruno should recover and still list the cookie domain\n  const app2 = await launchElectronApp({ userDataPath });\n  const page2 = await app2.firstWindow();\n\n  await page2.waitForSelector('[data-trigger=\"cookies\"]');\n  await page2.click('[data-trigger=\"cookies\"]');\n\n  // The domain row should still be visible (even if cookie values are blank).\n  await expect(page2.getByText('example.com')).toBeVisible();\n\n  await closeElectronApp(app2);\n});\n"
  },
  {
    "path": "tests/devtools/performance/performance-tab.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\n\ntest.describe('DevTools Performance Tab', () => {\n  test('should display performance metrics when Performance tab is opened', async ({ page }) => {\n    const devToolsButton = page.locator('button[data-trigger=\"dev-tools\"]');\n    await expect(devToolsButton).toBeVisible();\n    await devToolsButton.click();\n\n    // Wait for DevTools to open\n    await expect(page.locator('.console-header')).toBeVisible();\n\n    // Click on the Performance tab\n    const performanceTab = page.locator('.console-tab').filter({ hasText: 'Performance' });\n    await expect(performanceTab).toBeVisible();\n    await performanceTab.click();\n\n    await expect(performanceTab).toHaveClass(/active/);\n\n    await expect(page.locator('.system-resources h2')).toContainText('System Resources');\n\n    // Verify all resource cards are present\n    const resourceCards = page.locator('.resource-card');\n    await expect(resourceCards).toHaveCount(4);\n\n    // Test CPU Usage card\n    const cpuCard = resourceCards.filter({ has: page.locator('.resource-title', { hasText: 'CPU Usage' }) });\n    await expect(cpuCard).toBeVisible();\n\n    const cpuValue = cpuCard.locator('.resource-value');\n    await expect(cpuValue).toBeVisible();\n    // CPU value should match pattern like \"0.0%\" or \"12.5%\"\n    await expect(cpuValue).toContainText(/%/);\n    const cpuText = await cpuValue.textContent();\n    expect(cpuText).toMatch(/^\\d+\\.\\d+%$/);\n\n    // Test Memory Usage card\n    const memoryCard = resourceCards.filter({ has: page.locator('.resource-title', { hasText: 'Memory Usage' }) });\n    await expect(memoryCard).toBeVisible();\n\n    const memoryValue = memoryCard.locator('.resource-value');\n    await expect(memoryValue).toBeVisible();\n    // Memory value should match pattern like \"123.45 MB\" or \"1.23 GB\"\n    const memoryText = await memoryValue.textContent();\n    expect(memoryText).toMatch(/^\\d+(?:\\.\\d+)?\\s+(Bytes|KB|MB|GB)$/);\n\n    // Test Uptime card\n    const uptimeCard = resourceCards.filter({ has: page.locator('.resource-title', { hasText: 'Uptime' }) });\n    await expect(uptimeCard).toBeVisible();\n\n    const uptimeValue = uptimeCard.locator('.resource-value');\n    await expect(uptimeValue).toBeVisible();\n    // Uptime should match patterns like \"5s\", \"1m 30s\", or \"2h 15m 30s\"\n    const uptimeText = await uptimeValue.textContent();\n    expect(uptimeText).toMatch(/^(\\d+h\\s)?(\\d+m\\s)?\\d+s$/);\n\n    // Test Process ID card\n    const pidCard = resourceCards.filter({ has: page.locator('.resource-title', { hasText: 'Process ID' }) });\n    await expect(pidCard).toBeVisible();\n\n    const pidValue = pidCard.locator('.resource-value');\n    await expect(pidValue).toBeVisible();\n    // PID should be a number\n    const pidText = await pidValue.textContent();\n    expect(pidText).toMatch(/^\\d+$/);\n  });\n\n  test('should update performance metrics over time', async ({ page }) => {\n    await page.locator('button[data-trigger=\"dev-tools\"]').click();\n    await expect(page.locator('.console-header')).toBeVisible();\n\n    await page.locator('.console-tab').filter({ hasText: 'Performance' }).click();\n\n    const uptimeCard = page.locator('.resource-card').filter({\n      has: page.locator('.resource-title', { hasText: 'Uptime' })\n    });\n    const uptimeValue = uptimeCard.locator('.resource-value');\n    const initialUptime = await uptimeValue.textContent();\n\n    // Wait for metrics to update (monitoring interval is 2000ms)\n    await page.waitForTimeout(3000);\n\n    // Get updated uptime value\n    const updatedUptime = await uptimeValue.textContent();\n\n    // Verify uptime has increased\n    expect(updatedUptime).not.toBe(initialUptime);\n\n    // Parse and verify uptime increased\n    const parseUptime = (uptimeStr: string): number => {\n      let seconds = 0;\n      const hourMatch = uptimeStr.match(/(\\d+)h/);\n      const minuteMatch = uptimeStr.match(/(\\d+)m/);\n      const secondMatch = uptimeStr.match(/(\\d+)s/);\n\n      if (hourMatch) seconds += parseInt(hourMatch[1]) * 3600;\n      if (minuteMatch) seconds += parseInt(minuteMatch[1]) * 60;\n      if (secondMatch) seconds += parseInt(secondMatch[1]);\n\n      return seconds;\n    };\n\n    const initialSeconds = parseUptime(initialUptime || '');\n    const updatedSeconds = parseUptime(updatedUptime || '');\n\n    expect(updatedSeconds).toBeGreaterThan(initialSeconds);\n  });\n\n  test('should stop monitoring when switching away from Performance tab', async ({ page }) => {\n    await page.locator('button[data-trigger=\"dev-tools\"]').click();\n    await expect(page.locator('.console-header')).toBeVisible();\n\n    const performanceTab = page.locator('.console-tab').filter({ hasText: 'Performance' });\n    await performanceTab.click();\n    await expect(performanceTab).toHaveClass(/active/);\n\n    await expect(page.locator('.resource-card')).toHaveCount(4);\n\n    const consoleTab = page.locator('.console-tab').filter({ hasText: 'Console' });\n    await consoleTab.click();\n    await expect(consoleTab).toHaveClass(/active/);\n    await expect(performanceTab).not.toHaveClass(/active/);\n\n    // Verify Console tab content is shown\n    await expect(page.locator('.console-empty')).toBeVisible();\n\n    // Switch back to Performance tab\n    await performanceTab.click();\n    await expect(performanceTab).toHaveClass(/active/);\n\n    // Verify metrics are still working\n    const resourceCards = page.locator('.resource-card');\n    await expect(resourceCards).toHaveCount(4);\n\n    // Verify values are being displayed\n    const cpuValue = resourceCards.first().locator('.resource-value');\n    await expect(cpuValue).not.toBeEmpty();\n  });\n});\n"
  },
  {
    "path": "tests/dotenv/special-chars-collection-path/dotenv-special-chars.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { createCollection, createEnvironment, closeAllCollections } from '../../utils/page';\n\ntest.describe('DotEnv file in collection with special characters in path', () => {\n  test.afterEach(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('should detect .env file in collection with brackets in collection name', async ({ page, createTmpDir }) => {\n    const collectionName = 'My API (v2)';\n    const tmpDir = await createTmpDir(collectionName);\n\n    await test.step('Create collection with brackets in name', async () => {\n      await createCollection(page, collectionName, tmpDir);\n    });\n\n    await test.step('Create a collection environment to access env settings', async () => {\n      await createEnvironment(page, 'Test Env', 'collection');\n    });\n\n    await test.step('Open environment config and create .env file', async () => {\n      // Open the environment selector to see the .ENV FILES section\n      await page.getByTestId('environment-selector-trigger').click();\n\n      // The .env Files section is collapsed by default — click to expand it\n      const dotEnvSection = page.locator('.section-header').filter({ hasText: '.env Files' });\n      await dotEnvSection.waitFor({ state: 'visible' });\n      await dotEnvSection.click();\n\n      // Now click the + button to create a new .env file\n      const addButton = dotEnvSection.locator('.section-actions button');\n      await addButton.click();\n\n      // Type the .env file name and press Enter\n      const nameInput = page.locator('.environment-name-input');\n      await nameInput.press('Enter');\n\n      // Wait for success toast\n      await expect(page.getByText('.env file created!')).toBeVisible();\n    });\n\n    await test.step('Verify .env file is detected by watcher and shown in UI', async () => {\n      const dotEnvBadge = page.locator('.section-header').filter({ hasText: '.env Files' }).locator('.section-badge');\n      await expect(dotEnvBadge).toHaveText('1');\n    });\n  });\n});\n"
  },
  {
    "path": "tests/editable-table/editable-table.spec.ts",
    "content": "import { test, expect } from '../../playwright';\nimport { createCollection, closeAllCollections, createRequest, selectRequestPaneTab } from '../utils/page';\n\ntest.describe('EditableTable - Focus and Placeholder', () => {\n  test.afterEach(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('Cursor focus restored after save and placeholder shown for empty value', async ({ page, createTmpDir }) => {\n    const collectionName = 'test-editable-table';\n\n    // Create a new collection\n    await createCollection(page, collectionName, await createTmpDir());\n\n    // Create a request\n    await createRequest(page, 'Test Request', collectionName, {\n      url: 'https://httpbin.org/get'\n    });\n\n    // Navigate to Params tab\n    await selectRequestPaneTab(page, 'Params');\n\n    // Find the Query params table\n    const queryTable = page.locator('table').first();\n    const firstRow = queryTable.locator('tbody tr').first();\n\n    // Get the Name input (regular input)\n    const nameInput = firstRow.locator('input[type=\"text\"]').first();\n    await nameInput.click();\n    await page.keyboard.type('testParam');\n\n    // Verify input has focus before save\n    await expect(nameInput).toBeFocused();\n\n    // Save the request\n    const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';\n    await page.keyboard.press(saveShortcut);\n\n    // Wait for save toast\n    await expect(page.getByText('Request saved successfully').last()).toBeVisible();\n\n    // Verify cursor focus is restored after save\n    await expect(nameInput).toBeFocused();\n\n    // Verify placeholder shows for empty Value field\n    const valueCell = firstRow.locator('[data-testid=\"column-value\"]');\n    const placeholder = valueCell.locator('pre.CodeMirror-placeholder');\n    await expect(placeholder).toHaveText('Value');\n  });\n});\n"
  },
  {
    "path": "tests/environments/api-deleteEnvVar/api-deleteEnvVar.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { sendRequest, openRequest, selectEnvironment, openEnvironmentSelector, closeEnvironmentPanel, closeAllCollections } from '../../utils/page';\n\ntest.describe.serial('bru.deleteEnvVar(name)', () => {\n  test('should remove ephemeral variable from UI after deletion', async ({ pageWithUserData: page }) => {\n    await test.step('Open request and select environment', async () => {\n      await openRequest(page, 'collection', 'api-deleteEnvVar');\n      await selectEnvironment(page, 'Stage');\n    });\n\n    await test.step('Send request to set and delete variable', async () => {\n      await sendRequest(page, 200);\n    });\n\n    await test.step('Verify variable is removed from UI', async () => {\n      await openEnvironmentSelector(page, 'collection');\n      await page.getByText('Configure', { exact: true }).click();\n\n      const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });\n      await expect(envTab).toBeVisible();\n\n      await expect(page.getByRole('row', { name: 'host' })).toBeVisible();\n      await expect(page.getByRole('row', { name: 'tempToken' })).not.toBeVisible();\n    });\n\n    await test.step('Cleanup', async () => {\n      await closeEnvironmentPanel(page, 'collection');\n      await closeAllCollections(page);\n    });\n  });\n});\n"
  },
  {
    "path": "tests/environments/api-deleteEnvVar/fixtures/collection/api-deleteEnvVar.bru",
    "content": "meta {\n  name: api-deleteEnvVar\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  bru.setEnvVar(\"tempToken\", \"abc123\");\n}\n\nscript:post-response {\n  bru.deleteEnvVar(\"tempToken\");\n}\n\n"
  },
  {
    "path": "tests/environments/api-deleteEnvVar/fixtures/collection/bruno.json",
    "content": "{\n    \"version\": \"1\",\n    \"name\": \"collection\",\n    \"type\": \"collection\"\n}\n\n"
  },
  {
    "path": "tests/environments/api-deleteEnvVar/fixtures/collection/environments/Stage.bru",
    "content": "vars {\n  host: https://testbench-sanity.usebruno.com\n}\n\n"
  },
  {
    "path": "tests/environments/api-deleteEnvVar/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{projectRoot}}/tests/environments/api-deleteEnvVar/fixtures/collection\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/environments/api-deleteEnvVar/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/environments/api-deleteEnvVar/fixtures/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/environments/api-setEnvVar/api-setEnvVar-with-persist.spec.ts",
    "content": "import { test, expect, closeElectronApp } from '../../../playwright';\nimport { sendRequest } from '../../utils/page';\n\ntest.describe.serial('bru.setEnvVar(name, value, { persist: true })', () => {\n  test('set env var with persist using script', async ({ pageWithUserData: page, restartApp }) => {\n    // Select the collection and request\n    await page.locator('#sidebar-collection-name').click();\n    await page.getByText('api-setEnvVar-with-persist', { exact: true }).click();\n\n    // open environment dropdown\n    await page.getByTestId('environment-selector-trigger').click();\n\n    // select stage environment\n    await expect(page.locator('.environment-list .dropdown-item', { hasText: 'Stage' })).toBeVisible();\n    await page.locator('.environment-list .dropdown-item', { hasText: 'Stage' }).click();\n    await expect(page.locator('.current-environment', { hasText: 'Stage' })).toBeVisible();\n\n    // Send the request\n    await sendRequest(page, 200);\n\n    // confirm that the environment variable is set\n    await page.getByTestId('environment-selector-trigger').hover();\n    await page.getByTestId('environment-selector-trigger').click();\n    // open environment configuration\n\n    await page.locator('#configure-env').hover();\n    await page.locator('#configure-env').click();\n\n    const envTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Environments' }) });\n    await expect(envTab).toBeVisible();\n\n    await expect(page.getByRole('row', { name: 'token' }).getByRole('cell').nth(1)).toBeVisible();\n    await expect(page.getByRole('row', { name: 'secret' }).getByRole('cell').nth(2)).toBeVisible();\n    await envTab.hover();\n    await envTab.getByTestId('request-tab-close-icon').click({ force: true });\n\n    // we restart the app to confirm that the environment variable is persisted\n    const newApp = await restartApp();\n    const newPage = await newApp.firstWindow();\n\n    // select the collection and request\n    await newPage.locator('#sidebar-collection-name').click();\n    await newPage.getByText('api-setEnvVar-with-persist', { exact: true }).click();\n\n    // open environment dropdown\n    await newPage.getByTestId('environment-selector-trigger').click();\n    await newPage.locator('#configure-env').click();\n\n    const newEnvTab = newPage.locator('.request-tab').filter({ hasText: 'Environments' });\n    await expect(newEnvTab).toBeVisible();\n\n    await expect(newPage.getByRole('row', { name: 'token' }).getByRole('cell').nth(1)).toBeVisible();\n    await expect(newPage.getByRole('row', { name: 'secret' }).getByRole('cell').nth(2)).toBeVisible();\n\n    await newEnvTab.hover();\n    await newEnvTab.getByTestId('request-tab-close-icon').click({ force: true });\n\n    await closeElectronApp(newApp);\n  });\n});\n"
  },
  {
    "path": "tests/environments/api-setEnvVar/api-setEnvVar-without-persist.spec.ts",
    "content": "import { test, expect, closeElectronApp } from '../../../playwright';\nimport { sendRequest } from '../../utils/page';\n\ntest.describe.serial('bru.setEnvVar(name, value)', () => {\n  test('set env var using script', async ({ pageWithUserData: page, restartApp }) => {\n    // Select the collection and request\n    await page.locator('#sidebar-collection-name').click();\n    await page.getByText('api-setEnvVar-without-persist', { exact: true }).click();\n\n    // open environment dropdown\n    await page.getByTestId('environment-selector-trigger').click();\n\n    // select stage environment\n    await expect(page.locator('.environment-list .dropdown-item', { hasText: 'Stage' })).toBeVisible();\n    await page.locator('.environment-list .dropdown-item', { hasText: 'Stage' }).click();\n    await expect(page.locator('.current-environment', { hasText: 'Stage' })).toBeVisible();\n\n    // Send the request\n    await sendRequest(page, 200);\n\n    // confirm that the environment variable is set\n    await page.getByTestId('environment-selector-trigger').click();\n    await page.locator('#configure-env').click();\n\n    const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });\n    await expect(envTab).toBeVisible();\n\n    await expect(page.getByRole('row', { name: 'token' }).getByRole('cell').nth(1)).toBeVisible();\n    await expect(page.getByRole('row', { name: 'secret' }).getByRole('cell').nth(2)).toBeVisible();\n    await envTab.hover();\n    await envTab.getByTestId('request-tab-close-icon').click({ force: true });\n\n    // we restart the app to confirm that the environment variable is not persisted\n    const newApp = await restartApp();\n    const newPage = await newApp.firstWindow();\n\n    // select the collection and request\n    await newPage.locator('#sidebar-collection-name').click();\n    await newPage.getByText('api-setEnvVar-without-persist', { exact: true }).click();\n\n    // open environment dropdown\n    await newPage.getByTestId('environment-selector-trigger').click();\n    await newPage.locator('#configure-env').click();\n\n    const newEnvTab = newPage.locator('.request-tab').filter({ hasText: 'Environments' });\n    await expect(newEnvTab).toBeVisible();\n\n    await expect(newPage.locator('.table-container tbody')).not.toContainText('token');\n\n    await newEnvTab.hover();\n    await newEnvTab.getByTestId('request-tab-close-icon').click({ force: true });\n    await closeElectronApp(newApp);\n  });\n});\n"
  },
  {
    "path": "tests/environments/api-setEnvVar/fixtures/collection/api-setEnvVar-with-persist.bru",
    "content": "meta {\n  name: api-setEnvVar-with-persist\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  bru.setEnvVar(\"token\", \"secret\", { persist: true });\n}"
  },
  {
    "path": "tests/environments/api-setEnvVar/fixtures/collection/api-setEnvVar-without-persist.bru",
    "content": "meta {\n  name: api-setEnvVar-without-persist\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  bru.setEnvVar(\"token\", \"secret\");\n}"
  },
  {
    "path": "tests/environments/api-setEnvVar/fixtures/collection/bruno.json",
    "content": "{\n    \"version\": \"1\",\n    \"name\": \"collection\",\n    \"type\": \"collection\"\n}"
  },
  {
    "path": "tests/environments/api-setEnvVar/fixtures/collection/environments/Stage.bru",
    "content": "vars {\n  host: https://testbench-sanity.usebruno.com\n  token: secret\n  multiple-persist-vars-key1: value1\n  multiple-persist-vars-key2: value2\n}\n"
  },
  {
    "path": "tests/environments/api-setEnvVar/fixtures/collection/multiple-persist-vars-folder/folder.bru",
    "content": "meta {\n  name: multiple-persist-vars-folder\n  type: folder\n}\n"
  },
  {
    "path": "tests/environments/api-setEnvVar/fixtures/collection/multiple-persist-vars-folder/multiple-persist-vars-1.bru",
    "content": "meta {\n  name: multiple-persist-vars-1\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  bru.setEnvVar(\"multiple-persist-vars-key1\", \"value1\", { persist: true });\n}\n"
  },
  {
    "path": "tests/environments/api-setEnvVar/fixtures/collection/multiple-persist-vars-folder/multiple-persist-vars-2.bru",
    "content": "meta {\n  name: multiple-persist-vars-2\n  type: http\n  seq: 2\n}\n\nget {\n  url: {{host}}/ping\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  bru.setEnvVar(\"multiple-persist-vars-key2\", \"value2\", { persist: true });\n}\n"
  },
  {
    "path": "tests/environments/api-setEnvVar/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{collectionPath}}\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "tests/environments/api-setEnvVar/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{collectionPath}}\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/environments/api-setEnvVar/multiple-persist-vars.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport fs from 'fs';\nimport path from 'path';\n\ntest.describe.serial('bru.setEnvVar multiple persistent variables', () => {\n  test.afterEach(async ({ pageWithUserData: page }) => {\n    // Clean up test environment variables after each test\n    try {\n      // Check if the page is still valid before attempting cleanup\n      if (page && !page.isClosed()) {\n        await page.locator('#sidebar-collection-name').click();\n        await page.getByTestId('environment-selector-trigger').click();\n        await page.waitForTimeout(200);\n        await page.locator('#configure-env').click();\n        await page.waitForTimeout(200);\n\n        const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });\n\n        const key1Row = page.getByRole('row', { name: 'multiple-persist-vars-key1' });\n        if (await key1Row.isVisible()) {\n          await key1Row.getByRole('button').click();\n        }\n\n        const key2Row = page.getByRole('row', { name: 'multiple-persist-vars-key2' });\n        if (await key2Row.isVisible()) {\n          await key2Row.getByRole('button').click();\n        }\n\n        await envTab.hover();\n        await envTab.getByTestId('request-tab-close-icon').click({ force: true });\n      }\n    } catch (error) {\n      // Ignore cleanup errors to avoid masking test failures\n      console.log('Cleanup failed:', error);\n    }\n  });\n\n  test('should persist multiple environment variables from different requests', async ({ pageWithUserData: page, collectionFixturePath }) => {\n    await test.step('Select collection', async () => {\n      await page.locator('#sidebar-collection-name').click();\n      // The collection name should be 'collection' based on the test setup\n      await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'collection' })).toBeVisible();\n    });\n\n    await test.step('Select stage environment', async () => {\n      await page.getByTestId('environment-selector-trigger').click();\n      await page.waitForTimeout(200);\n      await expect(page.locator('.environment-list .dropdown-item', { hasText: 'Stage' })).toBeVisible();\n      await page.locator('.environment-list .dropdown-item', { hasText: 'Stage' }).click();\n      await expect(page.locator('.current-environment', { hasText: 'Stage' })).toBeVisible();\n    });\n\n    await test.step('Run the folder containing both requests', async () => {\n      // Ensure we're in the correct collection context before selecting the folder\n      await expect(page.locator('#sidebar-collection-name', { hasText: 'collection' })).toBeVisible();\n\n      // Hover on the folder and open context menu\n      await page.getByText('multiple-persist-vars-folder', { exact: true }).hover();\n      await page.locator('.collection-item-name').filter({ hasText: 'multiple-persist-vars-folder' }).locator('.menu-icon').click();\n\n      // Click on Run option\n      await page.getByText('Run', { exact: true }).click();\n\n      // Click Run button in the modal\n      await page.getByRole('button', { name: 'Run', exact: true }).click();\n\n      // Wait for the folder to finish running\n      await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 30000 });\n    });\n\n    await test.step('Verify both environment variables are set in UI', async () => {\n      // Ensure we're still in the correct collection context\n      await expect(page.locator('#sidebar-collection-name', { hasText: 'collection' })).toBeVisible();\n\n      await page.getByTestId('environment-selector-trigger').click();\n      await page.waitForTimeout(200);\n      await page.locator('#configure-env').click();\n      await page.waitForTimeout(200);\n\n      const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });\n      await expect(envTab).toBeVisible();\n\n      await expect(page.getByRole('row', { name: 'multiple-persist-vars-key1' }).getByRole('cell').nth(1)).toBeVisible();\n      await expect(page.getByRole('row', { name: 'value1' }).getByRole('cell').nth(2)).toBeVisible();\n      await expect(page.getByRole('row', { name: 'multiple-persist-vars-key2' }).getByRole('cell').nth(1)).toBeVisible();\n      await expect(page.getByRole('row', { name: 'value2' }).getByRole('cell').nth(2)).toBeVisible();\n      await envTab.hover();\n      await envTab.getByTestId('request-tab-close-icon').click({ force: true });\n    });\n\n    await test.step('Verify variables are persisted to file', async () => {\n      // Check that the variables are written to the Stage.bru file\n      const stageBruPath = path.join(collectionFixturePath!, 'environments', 'Stage.bru');\n      const stageBruContent = fs.readFileSync(stageBruPath, 'utf8');\n\n      // Both variables should be present in the file\n      expect(stageBruContent).toContain('multiple-persist-vars-key1');\n      expect(stageBruContent).toContain('value1');\n      expect(stageBruContent).toContain('multiple-persist-vars-key2');\n      expect(stageBruContent).toContain('value2');\n    });\n  });\n});\n"
  },
  {
    "path": "tests/environments/collection-env-config-selection/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"collection-env-config-selection\",\n  \"type\": \"collection\"\n}\n"
  },
  {
    "path": "tests/environments/collection-env-config-selection/collection/environments/dev.bru",
    "content": "vars {\n  baseUrl: /api/v2\n  name: staging\n  host: staging.example.com\n}\n"
  },
  {
    "path": "tests/environments/collection-env-config-selection/collection/environments/prod.bru",
    "content": "vars {\n  baseUrl: /api/v1\n  name: development\n  host: localhost:3000\n}\n"
  },
  {
    "path": "tests/environments/collection-env-config-selection/collection/test-request.bru",
    "content": "meta {\n  name: test-request\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{baseUrl}}/echo\n  body: none\n  auth: none\n}\n\ntests {\n  test(\"should get 200 response\", function() {\n    expect(res.getStatus()).to.equal(200);\n  });\n}\n"
  },
  {
    "path": "tests/environments/collection-env-config-selection/collection-env-config-selection.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections } from '../../utils/page';\n\ntest.describe('Collection Environment Configuration Selection Tests', () => {\n  test.afterEach(async ({ pageWithUserData: page }) => {\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  test('should open collection environment config with currently active environment selected', async ({\n    pageWithUserData: page\n  }) => {\n    // Open the collection from sidebar\n    await page.locator('#sidebar-collection-name').filter({ hasText: 'collection-env-config-selection' }).click();\n\n    // First, select an environment (prod - development)\n    await page.getByTestId('environment-selector-trigger').click();\n    await page.getByTestId('env-tab-collection').click();\n\n    // Select prod (development environment)\n    await page.locator('.dropdown-item').filter({ hasText: 'prod' }).click();\n\n    // Verify environment was selected\n    await expect(page.locator('.current-environment')).toContainText('prod');\n\n    // Now open the dropdown again and go to configuration\n    await page.getByTestId('environment-selector-trigger').click();\n    await page.getByTestId('env-tab-collection').click();\n    await page.getByText('Configure', { exact: true }).click();\n\n    const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });\n    await expect(envTab).toBeVisible();\n\n    const activeEnvItem = page.locator('.environment-item.active');\n    await expect(activeEnvItem).toContainText('prod');\n\n    await envTab.hover();\n    await envTab.getByTestId('request-tab-close-icon').click({ force: true });\n  });\n});\n"
  },
  {
    "path": "tests/environments/collection-env-config-selection/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{projectRoot}}/tests/environments/collection-env-config-selection/collection\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/environments/collection-env-config-selection/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/environments/collection-env-config-selection/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/environments/color-picker/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"global-env-config-selection\",\n  \"type\": \"collection\"\n}\n"
  },
  {
    "path": "tests/environments/color-picker/collection/test-request.bru",
    "content": "meta {\n  name: test-request\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/api/echo\n  body: none\n  auth: none\n}\n\ntests {\n  test(\"should get 200 response\", function() {\n    expect(res.getStatus()).to.equal(200);\n  });\n}\n"
  },
  {
    "path": "tests/environments/color-picker/color-picker.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections } from '../../utils/page/actions';\n\nconst PRESET_COLORS = [\n  '#CE4F3B',\n  '#2E8A54',\n  '#346AB2',\n  '#C77A0F',\n  '#B83D7F',\n  '#8D44B2'\n];\n\n// Convert hex color to RGB format used by CSS\nconst hexToRgb = (hex: string): string => {\n  const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex);\n  if (!result) return '';\n  const r = parseInt(result[1], 16);\n  const g = parseInt(result[2], 16);\n  const b = parseInt(result[3], 16);\n  return `rgb(${r}, ${g}, ${b})`;\n};\n\ntest.describe('Color Picker Tests', () => {\n  test.afterAll(async ({ pageWithUserData: page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('should select a preset color for global environment', async ({ pageWithUserData: page }) => {\n    // Open the collection from sidebar\n    await page.locator('#sidebar-collection-name').filter({ hasText: 'global-env-config-selection' }).click();\n\n    // Open global environment configuration\n    await page.getByTestId('environment-selector-trigger').click();\n    await page.getByTestId('env-tab-global').click();\n    await page.getByText('Configure', { exact: true }).click();\n\n    // Wait for the environments tab to be visible\n    const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });\n    await expect(envTab).toBeVisible();\n\n    // Click on the color picker icon (brush icon) next to the environment name\n    const colorPickerTrigger = page.locator('[title=\"Change color\"]').first();\n    await colorPickerTrigger.click();\n\n    // Wait for the color picker dropdown to appear\n    const colorPickerDropdown = page.locator('.tippy-box');\n    await expect(colorPickerDropdown).toBeVisible();\n\n    // Select the first preset color (red) using title attribute\n    const presetColor = PRESET_COLORS[0];\n    const colorOption = colorPickerDropdown.locator(`[title=\"${presetColor}\"]`);\n    await colorOption.click();\n\n    // Verify the color badge in the environment list shows the selected color\n    const activeEnvItem = page.locator('.environment-item.active');\n    const colorBadge = activeEnvItem.locator('.rounded-full').first();\n    await expect(colorBadge).toHaveCSS('background-color', hexToRgb(presetColor));\n  });\n\n  test('should remove color from environment', async ({ pageWithUserData: page }) => {\n    // Open global environment configuration\n    await page.getByTestId('environment-selector-trigger').click();\n    await page.getByTestId('env-tab-global').click();\n    await page.getByText('Configure', { exact: true }).click();\n\n    // Wait for the environments tab to be visible\n    const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });\n    await expect(envTab).toBeVisible();\n\n    // Click on the color picker icon\n    const colorPickerTrigger = page.locator('[title=\"Change color\"]').first();\n    await colorPickerTrigger.click();\n\n    // Wait for the color picker dropdown to appear\n    const colorPickerDropdown = page.locator('.tippy-box');\n    await expect(colorPickerDropdown).toBeVisible();\n\n    // Click the \"No color\" option (ban icon)\n    const noColorOption = colorPickerDropdown.locator('[title=\"No color\"]');\n    await noColorOption.click();\n\n    // Verify the color badge becomes transparent (no color)\n    const activeEnvItem = page.locator('.environment-item.active');\n    const colorBadge = activeEnvItem.locator('.rounded-full').first();\n    await expect(colorBadge).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');\n  });\n\n  test('should select custom color using slider', async ({ pageWithUserData: page }) => {\n    // Open global environment configuration\n    await page.getByTestId('environment-selector-trigger').click();\n    await page.getByTestId('env-tab-global').click();\n    await page.getByText('Configure', { exact: true }).click();\n\n    // Wait for the environments tab to be visible\n    const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });\n    await expect(envTab).toBeVisible();\n\n    // Click on the color picker icon\n    const colorPickerTrigger = page.locator('[title=\"Change color\"]').first();\n    await colorPickerTrigger.click();\n\n    // Wait for the color picker dropdown to appear\n    const colorPickerDropdown = page.locator('.tippy-box');\n    await expect(colorPickerDropdown).toBeVisible();\n\n    // Find the slider and change its value\n    const slider = colorPickerDropdown.locator('input[type=\"range\"]');\n    await expect(slider).toBeVisible();\n\n    // Move slider to middle position (50%)\n    await slider.fill('50');\n\n    // Click the custom color preview to apply it\n    const customColorPreview = colorPickerDropdown.locator('[title=\"Custom color\"]');\n    await customColorPreview.click();\n\n    // Verify the color badge has a color applied (not transparent)\n    const activeEnvItem = page.locator('.environment-item.active');\n    const colorBadge = activeEnvItem.locator('.rounded-full').first();\n    const bgColor = await colorBadge.evaluate((el) => getComputedStyle(el).backgroundColor);\n    expect(bgColor).not.toBe('rgba(0, 0, 0, 0)');\n    expect(bgColor).toMatch(/^rgb\\(\\d+, \\d+, \\d+\\)$/);\n  });\n\n  test('should display color badge in environment list after selecting color', async ({ pageWithUserData: page }) => {\n    // Open global environment configuration\n    await page.getByTestId('environment-selector-trigger').click();\n    await page.getByTestId('env-tab-global').click();\n    await page.getByText('Configure', { exact: true }).click();\n\n    // Wait for the environments tab to be visible\n    const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });\n    await expect(envTab).toBeVisible();\n\n    // Get the currently selected environment name\n    const activeEnvItem = page.locator('.environment-item.active');\n    const envName = await activeEnvItem.locator('.environment-name').textContent();\n\n    // Click on the color picker icon\n    const colorPickerTrigger = page.locator('[title=\"Change color\"]').first();\n    await colorPickerTrigger.click();\n\n    // Wait for the color picker dropdown to appear and select a color\n    const colorPickerDropdown = page.locator('.tippy-box');\n    await expect(colorPickerDropdown).toBeVisible();\n\n    const presetColor = PRESET_COLORS[1]; // green\n    const colorOption = colorPickerDropdown.locator(`[title=\"${presetColor}\"]`);\n    await colorOption.click();\n\n    // Verify the color badge in the environment list shows the selected color\n    const envListItem = page.locator('.environment-item').filter({ hasText: envName as string });\n    const colorBadge = envListItem.locator('.rounded-full').first();\n    await expect(colorBadge).toHaveCSS('background-color', hexToRgb(presetColor));\n  });\n});\n"
  },
  {
    "path": "tests/environments/color-picker/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{projectRoot}}/tests/environments/global-env-config-selection/collection\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/environments/color-picker/init-user-data/global-environments.json",
    "content": "{\n  \"environments\": [\n    {\n      \"uid\": \"FlaexlO7lcH7UtEpWsVyz\",\n      \"name\": \"Development Environment\",\n      \"variables\": [\n        {\n          \"uid\": \"lflBDSYBdHkUedYhBF4Ty\",\n          \"name\": \"env_type\",\n          \"value\": \"development\",\n          \"type\": \"text\",\n          \"secret\": false,\n          \"enabled\": true\n        }\n      ]\n    },\n    {\n      \"uid\": \"MsHcnAIonZ3455OfvpTUT\",\n      \"name\": \"Production Environment\",\n      \"variables\": [\n        {\n          \"uid\": \"TZljXLErzW1nUWoozntZE\",\n          \"name\": \"env_type\",\n          \"value\": \"production\",\n          \"type\": \"text\",\n          \"secret\": false,\n          \"enabled\": true\n        }\n      ]\n    },\n    {\n      \"uid\": \"VdUAdMPcfapMCqjKAeUiI\",\n      \"name\": \"Staging Environment\",\n      \"variables\": [\n        {\n          \"uid\": \"FwoWhHvu9eLhA8H4brG6f\",\n          \"name\": \"env_type\",\n          \"value\": \"staging\",\n          \"type\": \"text\",\n          \"secret\": false,\n          \"enabled\": true\n        }\n      ]\n    }\n  ],\n  \"activeGlobalEnvironmentUid\": \"MsHcnAIonZ3455OfvpTUT\"\n}"
  },
  {
    "path": "tests/environments/color-picker/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/environments/global-env-config-selection/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/environments/create-environment/collection-env-create.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport path from 'path';\nimport {\n  importCollection,\n  createEnvironment,\n  addEnvironmentVariables,\n  saveEnvironment,\n  sendRequest,\n  expectResponseContains,\n  removeCollection\n} from '../../utils/page';\nimport { buildCommonLocators } from '../../utils/page/locators';\n\ntest.describe('Collection Environment Create Tests', () => {\n  test('should import collection and create environment for request usage', async ({\n    page,\n    createTmpDir\n  }) => {\n    const collectionFile = path.join(__dirname, 'fixtures', 'bruno-collection.json');\n    const locators = buildCommonLocators(page);\n\n    await test.step('Import collection', async () => {\n      await importCollection(page, collectionFile, await createTmpDir('env-test'), {\n        expectedCollectionName: 'test_collection'\n      });\n    });\n\n    await test.step('Create environment with variables', async () => {\n      await createEnvironment(page, 'Test Environment', 'collection');\n\n      await addEnvironmentVariables(page, [\n        { name: 'host', value: 'https://echo.usebruno.com' },\n        { name: 'userId', value: '1' },\n        { name: 'postTitle', value: 'Test Post from Environment' },\n        { name: 'postBody', value: 'This is a test post body with environment variables' },\n        { name: 'secretApiToken', value: 'super-secret-token-12345', isSecret: true }\n      ]);\n\n      await saveEnvironment(page);\n      await expect(locators.environment.currentEnvironment()).toContainText('Test Environment');\n    });\n\n    await test.step('Test GET request with environment variables', async () => {\n      await page.locator('.collection-item-name').first().click();\n      await expect(locators.request.urlLine()).toContainText('{{host}}');\n      await sendRequest(page, 200);\n    });\n\n    await test.step('Verify response contains environment variables', async () => {\n      await expectResponseContains(page, [\n        '\"userId\": 1',\n        '\"title\": \"Test Post from Environment\"',\n        '\"body\": \"This is a test post body with environment variables\"',\n        '\"apiToken\": \"super-secret-token-12345\"'\n      ]);\n    });\n\n    await test.step('Cleanup', async () => {\n      await removeCollection(page, 'test_collection');\n    });\n  });\n});\n"
  },
  {
    "path": "tests/environments/create-environment/fixtures/bruno-collection.json",
    "content": "{\n  \"name\": \"test_collection\",\n  \"version\": \"1\",\n  \"items\": [\n    {\n      \"type\": \"http\",\n      \"name\": \"test\",\n      \"filename\": \"test.bru\",\n      \"seq\": 1,\n      \"settings\": {\n        \"encodeUrl\": true\n      },\n      \"tags\": [],\n      \"request\": {\n        \"url\": \"{{host}}\",\n        \"method\": \"POST\",\n        \"headers\": [],\n        \"params\": [],\n        \"body\": {\n          \"mode\": \"json\",\n          \"json\": \"{\\n  \\\"userId\\\": {{userId}},\\n  \\\"title\\\": \\\"{{postTitle}}\\\",\\n  \\\"body\\\": \\\"{{postBody}}\\\",\\n  \\\"apiToken\\\": \\\"{{secretApiToken}}\\\"\\n}\",\n          \"formUrlEncoded\": [],\n          \"multipartForm\": [],\n          \"file\": []\n        },\n        \"script\": {},\n        \"vars\": {},\n        \"assertions\": [],\n        \"tests\": \"\",\n        \"docs\": \"\",\n        \"auth\": {\n          \"mode\": \"inherit\"\n        }\n      }\n    }\n  ],\n  \"environments\": [],\n  \"brunoConfig\": {\n    \"version\": \"1\",\n    \"name\": \"test_collection\",\n    \"type\": \"collection\",\n    \"ignore\": [\n      \"node_modules\",\n      \".git\"\n    ],\n    \"size\": 0.000133514404296875,\n    \"filesCount\": 1,\n    \"proxy\": {\n      \"bypassProxy\": \"\",\n      \"enabled\": false,\n      \"auth\": {\n        \"enabled\": false,\n        \"username\": \"\",\n        \"password\": \"\"\n      },\n      \"port\": null,\n      \"hostname\": \"\",\n      \"protocol\": \"http\"\n    }\n  }\n}"
  },
  {
    "path": "tests/environments/create-environment/global-env-create.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport path from 'path';\nimport {\n  importCollection,\n  createEnvironment,\n  addEnvironmentVariables,\n  saveEnvironment,\n  sendRequest,\n  expectResponseContains,\n  closeAllCollections\n} from '../../utils/page';\nimport { buildCommonLocators } from '../../utils/page/locators';\n\ntest.describe('Global Environment Create Tests', () => {\n  test.setTimeout(60000);\n\n  test('should import collection and create global environment for request usage', async ({\n    page,\n    createTmpDir\n  }) => {\n    const collectionFile = path.join(__dirname, 'fixtures', 'bruno-collection.json');\n    const locators = buildCommonLocators(page);\n\n    await test.step('Import collection', async () => {\n      await importCollection(page, collectionFile, await createTmpDir('global-env-test'), {\n        expectedCollectionName: 'test_collection'\n      });\n    });\n\n    await test.step('Create global environment with variables', async () => {\n      await createEnvironment(page, 'Test Global Environment', 'global');\n\n      await addEnvironmentVariables(page, [\n        { name: 'host', value: 'https://echo.usebruno.com' },\n        { name: 'userId', value: '1' },\n        { name: 'postTitle', value: 'Global Test Post from Environment' },\n        { name: 'postBody', value: 'This is a global test post body with environment variables' },\n        { name: 'secretApiToken', value: 'global-secret-token-12345', isSecret: true }\n      ]);\n\n      await saveEnvironment(page);\n      await expect(locators.environment.currentEnvironment()).toContainText('Test Global Environment');\n    });\n\n    await test.step('Test GET request with environment variables', async () => {\n      await page.locator('.collection-item-name').first().click();\n      await expect(locators.request.urlLine()).toContainText('{{host}}');\n      await sendRequest(page, 200);\n    });\n\n    await test.step('Verify response contains environment variables', async () => {\n      await expectResponseContains(page, [\n        '\"userId\": 1',\n        '\"title\": \"Global Test Post from Environment\"',\n        '\"body\": \"This is a global test post body with environment variables\"',\n        '\"apiToken\": \"global-secret-token-12345\"'\n      ]);\n    });\n\n    await test.step('Cleanup', async () => {\n      await closeAllCollections(page);\n    });\n  });\n});\n"
  },
  {
    "path": "tests/environments/export-environment/collection-env-export/collection-env-export.spec.ts",
    "content": "import { test, expect } from '../../../../playwright';\nimport path from 'path';\nimport fs from 'fs';\n\n// Helper function to load expected fixtures\nfunction loadExpectedFixture(fixturePath: string) {\n  const fullPath = path.join(__dirname, '..', '../fixtures', 'environment-exports', fixturePath);\n  console.log(fullPath);\n  return JSON.parse(fs.readFileSync(fullPath, 'utf8'));\n}\n\n// Helper function to normalize dynamic fields for comparison\nfunction normalizeExportedContent(content: any) {\n  if (content.info) {\n    // Replace dynamic fields with fixed values for comparison\n    content.info.exportedAt = '2024-01-01T00:00:00.000Z';\n    content.info.exportedUsing = 'Bruno/v1.0.0';\n  }\n  return content;\n}\n\ntest.describe.serial('Collection Environment Export Tests', () => {\n  test.describe.serial('folder exports', () => {\n    test('should export single collection environment', async ({\n      pageWithUserData: page,\n      createTmpDir\n    }) => {\n      const exportDir = await createTmpDir('collection-env-export-single');\n\n      await test.step('Open collection and navigate to environment settings', async () => {\n        // Open the collection from sidebar\n        await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();\n\n        // Open environment settings\n        await page.getByTestId('environment-selector-trigger').click();\n        await page.getByTestId('env-tab-collection').click();\n        await page.getByText('Configure', { exact: true }).click();\n\n        const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });\n        await expect(envTab).toBeVisible();\n      });\n\n      await test.step('Open export modal and configure export settings', async () => {\n        // Click export button\n        await page.locator('button[title=\"Export environment\"]').click();\n\n        // Verify export modal opens\n        const exportModal = page.locator('.bruno-modal').filter({ hasText: 'Export Environments' });\n        await expect(exportModal).toBeVisible();\n\n        // Deselect all environments first\n        await page.getByText('Deselect All').click();\n\n        // Select only \"local\" environment\n        const localEnvCheckbox = page.locator('label').filter({ hasText: 'local' }).locator('input[type=\"checkbox\"]');\n        await localEnvCheckbox.check();\n\n        // Set export directory\n        await page.locator('input[id=\"export-location\"]').fill(exportDir);\n      });\n\n      await test.step('Execute export and close modal', async () => {\n        // Export the environment\n        await page.getByRole('button', { name: 'Export 1 Environment' }).click();\n      });\n\n      await test.step('Verify exported file and content', async () => {\n        // Verify exported file exists\n        const exportedFile = path.join(exportDir, 'local.json');\n\n        expect(fs.existsSync(exportedFile)).toBe(true);\n\n        // Verify file content matches expected fixture\n        const exportedContent = JSON.parse(fs.readFileSync(exportedFile, 'utf8'));\n        const expectedContent = loadExpectedFixture('bruno-collection-environments/local.json');\n\n        expect(normalizeExportedContent(exportedContent)).toEqual(expectedContent);\n      });\n    });\n\n    test('should export multiple collection environments', async ({\n      pageWithUserData: page,\n      createTmpDir\n    }) => {\n      const exportDir = await createTmpDir('collection-env-export-multiple');\n\n      await test.step('Open collection and navigate to environment settings', async () => {\n        // Open the collection from sidebar\n        await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();\n\n        // Open environment settings\n        await page.getByTestId('environment-selector-trigger').click();\n        await page.getByTestId('env-tab-collection').click();\n        await page.getByText('Configure', { exact: true }).click();\n\n        const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });\n        await expect(envTab).toBeVisible();\n      });\n\n      await test.step('Configure export settings for multiple environments', async () => {\n        // Click export button\n        await page.locator('button[title=\"Export environment\"]').click();\n\n        // Verify all environments are selected by default\n        await expect(page.getByRole('checkbox', { name: 'Local' })).toBeChecked();\n        await expect(page.getByRole('checkbox', { name: 'Prod' })).toBeChecked();\n\n        // Select folder export format (default might be single JSON file)\n        await page.getByText('Separate files in folder').click();\n\n        // Set export directory\n        await page.locator('input[id=\"export-location\"]').fill(exportDir);\n      });\n\n      await test.step('Execute export and close modal', async () => {\n        // Export all environments\n        await page.getByRole('button', { name: /Export \\d+ Environments?/ }).click();\n      });\n\n      await test.step('Verify exported files and content', async () => {\n        // Verify exported files exist\n        const exportPath = path.join(exportDir, 'bruno-collection-environments');\n        expect(fs.existsSync(exportPath)).toBe(true);\n\n        const expectedFiles = [\n          'local.json',\n          'prod.json'\n        ];\n\n        for (const fileName of expectedFiles) {\n          const filePath = path.join(exportPath, fileName);\n          expect(fs.existsSync(filePath)).toBe(true);\n\n          // Verify file content matches expected fixture\n          const content = JSON.parse(fs.readFileSync(filePath, 'utf8'));\n          const expectedContent = loadExpectedFixture(`bruno-collection-environments/${fileName}`);\n          expect(normalizeExportedContent(content)).toEqual(expectedContent);\n        }\n      });\n    });\n\n    test('should generate unique names when the export directory already contains previously exported contents', async ({\n      pageWithUserData: page,\n      createTmpDir\n    }) => {\n      const exportDir = await createTmpDir('collection-env-export-conflict');\n\n      await test.step('Setup existing export directory to simulate conflict', async () => {\n        // Create existing export directory and file to simulate conflict\n        const existingExportPath = path.join(exportDir, 'bruno-collection-environments');\n        fs.mkdirSync(existingExportPath, { recursive: true });\n        fs.writeFileSync(path.join(existingExportPath, 'local.json'), '{}');\n      });\n\n      await test.step('Open collection and navigate to environment settings', async () => {\n        // Open the collection from sidebar\n        await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();\n\n        // Open environment settings\n        await page.getByTestId('environment-selector-trigger').click();\n        await page.getByTestId('env-tab-collection').click();\n        await page.getByText('Configure', { exact: true }).click();\n\n        const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });\n        await expect(envTab).toBeVisible();\n      });\n\n      await test.step('Configure export settings with folder format', async () => {\n        // Click export button\n        await page.locator('button[title=\"Export environment\"]').click();\n\n        // Select folder export format (default might be single JSON file)\n        await page.getByText('Separate files in folder').click();\n\n        // Set export directory\n        await page.locator('input[id=\"export-location\"]').fill(exportDir);\n      });\n\n      await test.step('Execute export and close modal', async () => {\n        // Export should succeed with unique names\n        await page.getByRole('button', { name: 'Export 2 Environment' }).click();\n      });\n\n      await test.step('Verify unique naming and file content', async () => {\n        // Verify original folder still exists\n        const existingExportPath = path.join(exportDir, 'bruno-collection-environments');\n        expect(fs.existsSync(existingExportPath)).toBe(true);\n        expect(fs.existsSync(path.join(existingExportPath, 'local.json'))).toBe(true);\n\n        // Verify new folder with unique name was created\n        const uniqueExportPath = path.join(exportDir, 'bruno-collection-environments copy');\n        expect(fs.existsSync(uniqueExportPath)).toBe(true);\n\n        // Verify the new file exists in the unique folder\n        const newExportedFile = path.join(uniqueExportPath, 'local.json');\n        expect(fs.existsSync(newExportedFile)).toBe(true);\n\n        // Verify file content matches expected fixture\n        const exportedContent = JSON.parse(fs.readFileSync(newExportedFile, 'utf8'));\n        const expectedContent = loadExpectedFixture('bruno-collection-environments/local.json');\n        expect(normalizeExportedContent(exportedContent)).toEqual(expectedContent);\n      });\n    });\n  });\n\n  test.describe.serial('json file exports', () => {\n    test('should export single collection environment as object', async ({\n      pageWithUserData: page,\n      createTmpDir\n    }) => {\n      const exportDir = await createTmpDir('collection-env-export-single-object');\n\n      await test.step('Open collection and navigate to environment settings', async () => {\n        // Open the collection from sidebar\n        await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();\n\n        // Open collection environment settings\n        await page.getByTestId('environment-selector-trigger').click();\n        await page.getByTestId('env-tab-collection').click();\n        await page.getByText('Configure', { exact: true }).click();\n\n        const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });\n        await expect(envTab).toBeVisible();\n      });\n\n      await test.step('Configure export settings for single JSON file', async () => {\n        await page.locator('button[title=\"Export environment\"]').click();\n\n        // Deselect all environments first\n        await page.getByText('Deselect All').click();\n\n        // Select only \"local\" environment\n        const localEnvCheckbox = page.locator('label').filter({ hasText: 'local' }).locator('input[type=\"checkbox\"]');\n        await localEnvCheckbox.check();\n\n        await page.getByText('Single JSON file').click();\n\n        // Set export directory\n        await page.locator('input[id=\"export-location\"]').fill(exportDir);\n      });\n\n      await test.step('Execute export and verify success', async () => {\n        // Export the environment\n        await page.getByRole('button', { name: 'Export 1 Environment' }).click();\n\n        // Verify success message\n        await expect(page.getByText('Environment(s) exported successfully', { exact: false }).first()).toBeVisible();\n      });\n\n      await test.step('Verify exported file and content', async () => {\n        // Verify exported file exists\n        const exportedFile = path.join(exportDir, 'local.json');\n        expect(fs.existsSync(exportedFile)).toBe(true);\n\n        // Verify file content matches expected fixture\n        const content = JSON.parse(fs.readFileSync(exportedFile, 'utf8'));\n        const expectedContent = loadExpectedFixture('local.json');\n        expect(normalizeExportedContent(content)).toEqual(expectedContent);\n      });\n    });\n\n    test('should export multiple collection environments as single JSON file', async ({\n      pageWithUserData: page,\n      createTmpDir\n    }) => {\n      const exportDir = await createTmpDir('collection-env-export-single-file');\n\n      await test.step('Open collection and navigate to environment settings', async () => {\n        // Open the collection from sidebar\n        await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();\n\n        // Open collection environment settings\n        await page.getByTestId('environment-selector-trigger').click();\n        await page.getByTestId('env-tab-collection').click();\n        await page.getByText('Configure', { exact: true }).click();\n\n        const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });\n        await expect(envTab).toBeVisible();\n      });\n\n      await test.step('Configure export settings for single JSON file', async () => {\n        await page.locator('button[title=\"Export environment\"]').click();\n\n        // Select single JSON file format\n        await page.getByText('Single JSON file').click();\n\n        // Set export directory\n        await page.locator('input[id=\"export-location\"]').fill(exportDir);\n      });\n\n      await test.step('Execute export and verify success', async () => {\n        // Export the environments\n        await page.getByRole('button', { name: 'Export 2 Environments' }).click();\n\n        // Verify success message\n        await expect(page.getByText('Environment(s) exported successfully', { exact: false }).first()).toBeVisible();\n      });\n\n      await test.step('Verify exported file and content', async () => {\n        // Verify exported file exists\n        const exportedFile = path.join(exportDir, 'bruno-collection-environments.json');\n        expect(fs.existsSync(exportedFile)).toBe(true);\n\n        // Verify file content matches expected fixture\n        const content = JSON.parse(fs.readFileSync(exportedFile, 'utf8'));\n        const expectedContent = loadExpectedFixture('bruno-collection-environments.json');\n        expect(normalizeExportedContent(content)).toEqual(expectedContent);\n      });\n    });\n\n    test('should generate unique names when the export directory already contains previously exported contents', async ({\n      pageWithUserData: page,\n      createTmpDir\n    }) => {\n      const exportDir = await createTmpDir('collection-env-export-single-object');\n\n      await test.step('Setup existing export file to simulate conflict', async () => {\n        // Create existing export directory and file to simulate conflict\n        const existingExportJsonPath = path.join(exportDir, 'local.json');\n        fs.writeFileSync(existingExportJsonPath, '{}');\n      });\n\n      await test.step('Open collection and navigate to environment settings', async () => {\n        // Open the collection from sidebar\n        await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();\n\n        // Open collection environment settings\n        await page.getByTestId('environment-selector-trigger').click();\n        await page.getByTestId('env-tab-collection').click();\n        await page.getByText('Configure', { exact: true }).click();\n\n        const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });\n        await expect(envTab).toBeVisible();\n      });\n\n      await test.step('Configure export settings for single JSON file', async () => {\n        await page.locator('button[title=\"Export environment\"]').click();\n\n        // Deselect all environments first\n        await page.getByText('Deselect All').click();\n\n        // Select only \"local\" environment\n        const localEnvCheckbox = page.locator('label').filter({ hasText: 'local' }).locator('input[type=\"checkbox\"]');\n        await localEnvCheckbox.check();\n\n        await page.getByText('Single JSON file').click();\n\n        // Set export directory\n        await page.locator('input[id=\"export-location\"]').fill(exportDir);\n      });\n\n      await test.step('Execute export and close modal', async () => {\n        // Export should succeed with unique names\n        await page.getByRole('button', { name: 'Export 1 Environment' }).click();\n      });\n\n      await test.step('Verify unique naming and file content', async () => {\n        // Verify original file still exists\n        const existingExportJsonPath = path.join(exportDir, 'local.json');\n        expect(fs.existsSync(existingExportJsonPath)).toBe(true);\n\n        // Verify new file with unique name was created\n        const uniqueExportPath = path.join(exportDir, 'local copy.json');\n        expect(fs.existsSync(uniqueExportPath)).toBe(true);\n\n        // Verify file content matches expected fixture\n        const exportedContent = JSON.parse(fs.readFileSync(uniqueExportPath, 'utf8'));\n        const expectedContent = loadExpectedFixture('local.json');\n        expect(normalizeExportedContent(exportedContent)).toEqual(expectedContent);\n      });\n    });\n  });\n\n  // common tests\n  test('should not be able to export, when no environments are selected', async ({\n    pageWithUserData: page,\n    createTmpDir\n  }) => {\n    const exportDir = await createTmpDir('collection-env-export-no-selection');\n\n    await test.step('Open collection and navigate to environment settings', async () => {\n      // Open the collection from sidebar\n      await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();\n\n      // Open environment settings\n      await page.getByTestId('environment-selector-trigger').click();\n      await page.getByTestId('env-tab-collection').click();\n      await page.getByText('Configure', { exact: true }).click();\n\n      const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });\n      await expect(envTab).toBeVisible();\n    });\n\n    await test.step('Open export modal and deselect all environments', async () => {\n      // Click export button\n      await page.getByRole('button', { name: 'Export Environment' }).click();\n\n      // Deselect all environments\n      await page.getByText('Deselect All').click();\n    });\n\n    await test.step('Verify export button is disabled when no environments selected', async () => {\n      // Verify export button is disabled\n      await expect(page.getByRole('button', { name: 'Export Environments' })).toBeDisabled();\n    });\n  });\n});\n"
  },
  {
    "path": "tests/environments/export-environment/collection-env-export/fixtures/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"Environment Export Test Collection\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}\n"
  },
  {
    "path": "tests/environments/export-environment/collection-env-export/fixtures/collection/environments/local.bru",
    "content": "vars {\n  host: http://localhost:3000\n}\n\nvars:secret [\n  secretToken\n]"
  },
  {
    "path": "tests/environments/export-environment/collection-env-export/fixtures/collection/environments/prod.bru",
    "content": "vars {\n  host: https://echo.usebruno.com\n}\n\nvars:secret [\n  secretToken\n]"
  },
  {
    "path": "tests/environments/export-environment/collection-env-export/fixtures/collection/test-request.bru",
    "content": "meta {\n  name: Test Request\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}\n}\n"
  },
  {
    "path": "tests/environments/export-environment/collection-env-export/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{projectRoot}}/tests/environments/export-environment/collection-env-export/fixtures/collection\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/environments/export-environment/collection-env-export/init-user-data/global-environments.json",
    "content": "{\n  \"environments\": [\n    {\n      \"uid\": \"FfmX1qYW2EaOpLWeNPLKM\",\n      \"name\": \"local\",\n      \"variables\": [\n        {\n          \"uid\": \"PYtEmbl0WSSE7NQcYeGx7\",\n          \"name\": \"host\",\n          \"value\": \"http://localhost:3000\",\n          \"type\": \"text\",\n          \"enabled\": true,\n          \"secret\": false\n        },\n        {\n          \"uid\": \"FExjpMlo6zg3egKcgpfop\",\n          \"name\": \"secretToken\",\n          \"value\": \"\",\n          \"type\": \"text\",\n          \"enabled\": true,\n          \"secret\": true\n        }\n      ]\n    },\n    {\n      \"uid\": \"YCuMlcT9KEWf3cSn3lWOq\",\n      \"name\": \"prod\",\n      \"variables\": [\n        {\n          \"uid\": \"cdfGL8VF46DrJtBYkiR8i\",\n          \"name\": \"host\",\n          \"value\": \"https://echo.usebruno.com\",\n          \"type\": \"text\",\n          \"enabled\": true,\n          \"secret\": false\n        },\n        {\n          \"uid\": \"hTqi6z7CLLWLHxjQ3bwtt\",\n          \"name\": \"secretToken\",\n          \"value\": \"\",\n          \"type\": \"text\",\n          \"enabled\": true,\n          \"secret\": true\n        }\n      ]\n    }\n  ],\n  \"activeGlobalEnvironmentUid\": \"FfmX1qYW2EaOpLWeNPLKM\"\n}\n"
  },
  {
    "path": "tests/environments/export-environment/collection-env-export/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/environments/export-environment/collection-env-export/fixtures/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/environments/export-environment/global-env-export/fixtures/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"Environment Export Test Collection\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}\n"
  },
  {
    "path": "tests/environments/export-environment/global-env-export/fixtures/collection/test-request.bru",
    "content": "meta {\n  name: Test Request\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}\n}\n"
  },
  {
    "path": "tests/environments/export-environment/global-env-export/global-env-export.spec.ts",
    "content": "import { test, expect } from '../../../../playwright';\nimport path from 'path';\nimport fs from 'fs';\n\n// Helper function to load expected fixtures\nfunction loadExpectedFixture(fixturePath: string) {\n  const fullPath = path.join(__dirname, '..', '../fixtures', 'environment-exports', fixturePath);\n  return JSON.parse(fs.readFileSync(fullPath, 'utf8'));\n}\n\n// Helper function to normalize dynamic fields for comparison\nfunction normalizeExportedContent(content: any) {\n  if (content.info) {\n    // Replace dynamic fields with fixed values for comparison\n    content.info.exportedAt = '2024-01-01T00:00:00.000Z';\n    content.info.exportedUsing = 'Bruno/v1.0.0';\n  }\n  // Handle individual environment files that have info at the root level\n  if (content.name && content.variables && content.info) {\n    content.info.exportedAt = '2024-01-01T00:00:00.000Z';\n    content.info.exportedUsing = 'Bruno/v1.0.0';\n  }\n  return content;\n}\n\ntest.describe.serial('Global Environment Export Tests', () => {\n  test.describe.serial('folder exports', () => {\n    test('should export single global environment', async ({\n      pageWithUserData: page,\n      createTmpDir\n    }) => {\n      const exportDir = await createTmpDir('global-env-export-single');\n\n      await test.step('Open collection and navigate to global environment settings', async () => {\n        // Open the collection from sidebar\n        await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();\n\n        // Open global environment settings\n        await page.getByTestId('environment-selector-trigger').click();\n        await page.getByTestId('env-tab-global').click();\n        await page.getByText('Configure', { exact: true }).click();\n\n        const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });\n        await expect(envTab).toBeVisible();\n      });\n\n      await test.step('Open export modal and configure export settings', async () => {\n        // Click export button\n        await page.locator('button[title=\"Export environment\"]').click();\n\n        // Verify export modal opens\n        const exportModal = page.locator('.bruno-modal').filter({ hasText: 'Export Environments' });\n        await expect(exportModal).toBeVisible();\n\n        // Deselect all environments first\n        await page.getByText('Deselect All').click();\n\n        // Select only \"local\" environment\n        const localEnvCheckbox = page.locator('label').filter({ hasText: 'local' }).locator('input[type=\"checkbox\"]');\n        await localEnvCheckbox.check();\n\n        // Set export directory\n        await page.locator('input[id=\"export-location\"]').fill(exportDir);\n      });\n\n      await test.step('Execute export and close modal', async () => {\n        // Export the environment\n        await page.getByRole('button', { name: 'Export 1 Environment' }).click();\n      });\n\n      await test.step('Verify exported file and content', async () => {\n        // Verify exported file exists\n        const exportedFile = path.join(exportDir, 'local.json');\n\n        expect(fs.existsSync(exportedFile)).toBe(true);\n\n        // Verify file content matches expected fixture\n        const exportedContent = JSON.parse(fs.readFileSync(exportedFile, 'utf8'));\n        const expectedContent = loadExpectedFixture('bruno-global-environments/local.json');\n\n        expect(normalizeExportedContent(exportedContent)).toEqual(expectedContent);\n      });\n    });\n\n    test('should export multiple global environments', async ({\n      pageWithUserData: page,\n      createTmpDir\n    }) => {\n      const exportDir = await createTmpDir('global-env-export-multiple');\n\n      await test.step('Open collection and navigate to global environment settings', async () => {\n        // Open the collection from sidebar\n        await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();\n\n        // Open global environment settings\n        await page.getByTestId('environment-selector-trigger').click();\n        await page.getByTestId('env-tab-global').click();\n        await page.getByText('Configure', { exact: true }).click();\n\n        const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });\n        await expect(envTab).toBeVisible();\n      });\n\n      await test.step('Configure export settings for multiple environments', async () => {\n        // Click export button\n        await page.locator('button[title=\"Export environment\"]').click();\n\n        // Verify all environments are selected by default\n        await expect(page.getByRole('checkbox', { name: 'Local' })).toBeChecked();\n        await expect(page.getByRole('checkbox', { name: 'Prod' })).toBeChecked();\n\n        // Select folder export format (default might be single JSON file)\n        await page.getByText('Separate files in folder').click();\n\n        // Set export directory\n        await page.locator('input[id=\"export-location\"]').fill(exportDir);\n      });\n\n      await test.step('Execute export and close modal', async () => {\n        // Export all environments\n        await page.getByRole('button', { name: /Export \\d+ Environments?/ }).click();\n      });\n\n      await test.step('Verify exported files and content', async () => {\n        // Verify exported files exist\n        const exportPath = path.join(exportDir, 'bruno-global-environments');\n        expect(fs.existsSync(exportPath)).toBe(true);\n\n        const expectedFiles = [\n          'local.json',\n          'prod.json'\n        ];\n\n        for (const fileName of expectedFiles) {\n          const filePath = path.join(exportPath, fileName);\n          expect(fs.existsSync(filePath)).toBe(true);\n\n          // Verify file content matches expected fixture\n          const content = JSON.parse(fs.readFileSync(filePath, 'utf8'));\n          const expectedContent = loadExpectedFixture(`bruno-global-environments/${fileName}`);\n          expect(normalizeExportedContent(content)).toEqual(expectedContent);\n        }\n      });\n    });\n\n    test('should generate unique names when the export directory already contains previously exported contents', async ({\n      pageWithUserData: page,\n      createTmpDir\n    }) => {\n      const exportDir = await createTmpDir('global-env-export-conflict');\n\n      await test.step('Setup existing export directory to simulate conflict', async () => {\n        // Create existing export directory and file to simulate conflict\n        const existingExportPath = path.join(exportDir, 'bruno-global-environments');\n        fs.mkdirSync(existingExportPath, { recursive: true });\n        fs.writeFileSync(path.join(existingExportPath, 'local.json'), '{}');\n      });\n\n      await test.step('Open collection and navigate to global environment settings', async () => {\n        // Open the collection from sidebar\n        await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();\n\n        // Open global environment settings\n        await page.getByTestId('environment-selector-trigger').click();\n        await page.getByTestId('env-tab-global').click();\n        await page.getByText('Configure', { exact: true }).click();\n\n        const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });\n        await expect(envTab).toBeVisible();\n      });\n\n      await test.step('Configure export settings with folder format', async () => {\n        // Click export button\n        await page.locator('button[title=\"Export environment\"]').click();\n\n        // Set export directory\n        await page.locator('input[id=\"export-location\"]').fill(exportDir);\n\n        // Select folder export format\n        await page.getByText('Separate files in folder').click();\n      });\n\n      await test.step('Execute export and close modal', async () => {\n        // Export should succeed with unique names\n        await page.getByRole('button', { name: 'Export 2 Environment' }).click();\n      });\n\n      await test.step('Verify unique naming and file content', async () => {\n        // Verify original folder still exists\n        const existingExportPath = path.join(exportDir, 'bruno-global-environments');\n        expect(fs.existsSync(existingExportPath)).toBe(true);\n        expect(fs.existsSync(path.join(existingExportPath, 'local.json'))).toBe(true);\n\n        // Verify new folder with unique name was created\n        const uniqueExportPath = path.join(exportDir, 'bruno-global-environments copy');\n        expect(fs.existsSync(uniqueExportPath)).toBe(true);\n\n        // Verify the new file exists in the unique folder\n        const newExportedFile = path.join(uniqueExportPath, 'local.json');\n        expect(fs.existsSync(newExportedFile)).toBe(true);\n\n        // Verify file content matches expected fixture\n        const exportedContent = JSON.parse(fs.readFileSync(newExportedFile, 'utf8'));\n        const expectedContent = loadExpectedFixture('bruno-global-environments/local.json');\n        expect(normalizeExportedContent(exportedContent)).toEqual(expectedContent);\n      });\n    });\n  });\n\n  test.describe.serial('json file exports', () => {\n    test('should export single global environment as object', async ({\n      pageWithUserData: page,\n      createTmpDir\n    }) => {\n      const exportDir = await createTmpDir('global-env-export-single-object');\n\n      await test.step('Open collection and navigate to global environment settings', async () => {\n        // Open the collection from sidebar\n        await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();\n\n        // Open global environment settings\n        await page.getByTestId('environment-selector-trigger').click();\n        await page.getByTestId('env-tab-global').click();\n        await page.getByText('Configure', { exact: true }).click();\n\n        const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });\n        await expect(envTab).toBeVisible();\n      });\n\n      await test.step('Configure export settings for single JSON file', async () => {\n        await page.locator('button[title=\"Export environment\"]').click();\n\n        // Deselect all environments first\n        await page.getByText('Deselect All').click();\n\n        // Select only \"local\" environment\n        const localEnvCheckbox = page.locator('label').filter({ hasText: 'local' }).locator('input[type=\"checkbox\"]');\n        await localEnvCheckbox.check();\n\n        // Single JSON file format is automatically selected for single environment\n        // The backend will automatically use 'single-object' format for single environment\n        await page.getByText('Single JSON file').click();\n\n        // Set export directory\n        await page.locator('input[id=\"export-location\"]').fill(exportDir);\n      });\n\n      await test.step('Execute export and verify success', async () => {\n        // Export the environment\n        await page.getByRole('button', { name: 'Export 1 Environment' }).click();\n\n        // Verify success message\n        await expect(page.getByText('Environment(s) exported successfully', { exact: false }).first()).toBeVisible();\n      });\n\n      await test.step('Verify exported file and content', async () => {\n        // Verify exported file exists\n        const exportedFile = path.join(exportDir, 'local.json');\n        expect(fs.existsSync(exportedFile)).toBe(true);\n\n        // Verify file content matches expected fixture\n        const content = JSON.parse(fs.readFileSync(exportedFile, 'utf8'));\n        const expectedContent = loadExpectedFixture('local.json');\n        expect(normalizeExportedContent(content)).toEqual(expectedContent);\n      });\n    });\n\n    test('should export multiple global environments as single JSON file', async ({\n      pageWithUserData: page,\n      createTmpDir\n    }) => {\n      const exportDir = await createTmpDir('global-env-export-single-file');\n\n      await test.step('Open collection and navigate to global environment settings', async () => {\n        // Open the collection from sidebar\n        await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();\n\n        // Open global environment settings\n        await page.getByTestId('environment-selector-trigger').click();\n        await page.getByTestId('env-tab-global').click();\n        await page.getByText('Configure', { exact: true }).click();\n\n        const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });\n        await expect(envTab).toBeVisible();\n      });\n\n      await test.step('Configure export settings for single JSON file', async () => {\n        await page.locator('button[title=\"Export environment\"]').click();\n\n        // Select single JSON file format\n        await page.getByText('Single JSON file').click();\n\n        // Set export directory\n        await page.locator('input[id=\"export-location\"]').fill(exportDir);\n      });\n\n      await test.step('Execute export and verify success', async () => {\n        // Export the environments\n        await page.getByRole('button', { name: 'Export 2 Environments' }).click();\n        await page.waitForTimeout(200);\n        // Verify success message\n        await expect(page.getByText('Environment(s) exported successfully', { exact: false }).first()).toBeVisible();\n      });\n\n      await test.step('Verify exported file and content', async () => {\n        // Verify exported file exists\n        const exportedFile = path.join(exportDir, 'bruno-global-environments.json');\n        expect(fs.existsSync(exportedFile)).toBe(true);\n\n        // Verify file content matches expected fixture\n        const content = JSON.parse(fs.readFileSync(exportedFile, 'utf8'));\n        const expectedContent = loadExpectedFixture('bruno-global-environments.json');\n        expect(normalizeExportedContent(content)).toEqual(expectedContent);\n      });\n    });\n\n    test('should generate unique names when the export directory already contains previously exported contents', async ({\n      pageWithUserData: page,\n      createTmpDir\n    }) => {\n      const exportDir = await createTmpDir('global-env-export-single-object-conflict');\n\n      await test.step('Setup existing export file to simulate conflict', async () => {\n        // Create existing export directory and file to simulate conflict\n        const existingExportJsonPath = path.join(exportDir, 'local.json');\n        fs.writeFileSync(existingExportJsonPath, '{}');\n      });\n\n      await test.step('Open collection and navigate to global environment settings', async () => {\n        // Open the collection from sidebar\n        await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();\n\n        // Open global environment settings\n        await page.getByTestId('environment-selector-trigger').click();\n        await page.getByTestId('env-tab-global').click();\n        await page.getByText('Configure', { exact: true }).click();\n\n        const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });\n        await expect(envTab).toBeVisible();\n      });\n\n      await test.step('Configure export settings for single JSON file', async () => {\n        await page.locator('button[title=\"Export environment\"]').click();\n\n        // Deselect all environments first\n        await page.getByText('Deselect All').click();\n\n        // Select only \"local\" environment\n        const localEnvCheckbox = page.locator('label').filter({ hasText: 'local' }).locator('input[type=\"checkbox\"]');\n        await localEnvCheckbox.check();\n\n        // Single JSON file format is automatically selected for single environment\n        // The backend will automatically use 'single-object' format for single environment\n        await page.getByText('Single JSON file').click();\n\n        // Set export directory\n        await page.locator('input[id=\"export-location\"]').fill(exportDir);\n      });\n\n      await test.step('Execute export and close modal', async () => {\n        // Export should succeed with unique names\n        await page.getByRole('button', { name: 'Export 1 Environment' }).click();\n      });\n\n      await test.step('Verify unique naming and file content', async () => {\n        // Verify original file still exists\n        const existingExportJsonPath = path.join(exportDir, 'local.json');\n        expect(fs.existsSync(existingExportJsonPath)).toBe(true);\n\n        // Verify new file with unique name was created\n        const uniqueExportPath = path.join(exportDir, 'local copy.json');\n        expect(fs.existsSync(uniqueExportPath)).toBe(true);\n\n        // Verify file content matches expected fixture\n        const exportedContent = JSON.parse(fs.readFileSync(uniqueExportPath, 'utf8'));\n        const expectedContent = loadExpectedFixture('local.json');\n        expect(normalizeExportedContent(exportedContent)).toEqual(expectedContent);\n      });\n    });\n  });\n\n  // common tests\n  test('should not be able to export, when no environments are selected', async ({\n    pageWithUserData: page,\n    createTmpDir\n  }) => {\n    const exportDir = await createTmpDir('global-env-export-no-selection');\n\n    await test.step('Open collection and navigate to global environment settings', async () => {\n      // Open the collection from sidebar\n      await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Export Test Collection' }).click();\n\n      // Open global environment settings\n      await page.getByTestId('environment-selector-trigger').click();\n      await page.getByTestId('env-tab-global').click();\n      await page.getByText('Configure', { exact: true }).click();\n\n      const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });\n      await expect(envTab).toBeVisible();\n    });\n\n    await test.step('Open export modal and deselect all environments', async () => {\n      await page.getByRole('button', { name: 'Export Environment' }).click();\n\n      await page.getByText('Deselect All').click();\n    });\n\n    await test.step('Verify export button is disabled when no environments selected', async () => {\n      await expect(page.getByRole('button', { name: 'Export Environments' })).toBeDisabled();\n    });\n  });\n});\n"
  },
  {
    "path": "tests/environments/export-environment/global-env-export/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{projectRoot}}/tests/environments/export-environment/global-env-export/fixtures/collection\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/environments/export-environment/global-env-export/init-user-data/global-environments.json",
    "content": "{\n  \"environments\": [\n    {\n      \"uid\": \"FfmX1qYW2EaOpLWeNPLKM\",\n      \"name\": \"local\",\n      \"variables\": [\n        {\n          \"uid\": \"rfrIqlfmuyFD4560ciK04\",\n          \"name\": \"host\",\n          \"value\": \"http://localhost:3000\",\n          \"type\": \"text\",\n          \"enabled\": true,\n          \"secret\": false\n        },\n        {\n          \"uid\": \"3WEAWueN0Ov99uOX0uuuM\",\n          \"name\": \"secretToken\",\n          \"value\": \"\",\n          \"type\": \"text\",\n          \"enabled\": true,\n          \"secret\": true\n        }\n      ]\n    },\n    {\n      \"uid\": \"PYtEmbl0WSSE7NQcYeGx7\",\n      \"name\": \"prod\",\n      \"variables\": [\n        {\n          \"uid\": \"tdqX6Yln9DYQYNievEJR1\",\n          \"name\": \"host\",\n          \"value\": \"https://echo.usebruno.com\",\n          \"type\": \"text\",\n          \"enabled\": true,\n          \"secret\": false\n        },\n        {\n          \"uid\": \"fSxTRpngl8fxkhrl3z7hA\",\n          \"name\": \"secretToken\",\n          \"value\": \"\",\n          \"type\": \"text\",\n          \"enabled\": true,\n          \"secret\": true\n        }\n      ]\n    }\n  ],\n  \"activeGlobalEnvironmentUid\": \"FfmX1qYW2EaOpLWeNPLKM\"\n}\n"
  },
  {
    "path": "tests/environments/export-environment/global-env-export/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/environments/export-environment/global-env-export/fixtures/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/environments/fixtures/environment-exports/bruno-collection-environments/local.json",
    "content": "{\n  \"name\": \"local\",\n  \"variables\": [\n    {\n      \"name\": \"host\",\n      \"value\": \"http://localhost:3000\",\n      \"type\": \"text\",\n      \"enabled\": true,\n      \"secret\": false\n    },\n    {\n      \"name\": \"secretToken\",\n      \"value\": \"\",\n      \"type\": \"text\",\n      \"enabled\": true,\n      \"secret\": true\n    }\n  ],\n  \"info\": {\n    \"type\": \"bruno-environment\",\n    \"exportedAt\": \"2024-01-01T00:00:00.000Z\",\n    \"exportedUsing\": \"Bruno/v1.0.0\"\n  }\n}\n"
  },
  {
    "path": "tests/environments/fixtures/environment-exports/bruno-collection-environments/prod.json",
    "content": "{\n  \"name\": \"prod\",\n  \"variables\": [\n    {\n      \"name\": \"host\",\n      \"value\": \"https://echo.usebruno.com\",\n      \"type\": \"text\",\n      \"enabled\": true,\n      \"secret\": false\n    },\n    {\n      \"name\": \"secretToken\",\n      \"value\": \"\",\n      \"type\": \"text\",\n      \"enabled\": true,\n      \"secret\": true\n    }\n  ],\n  \"info\": {\n    \"type\": \"bruno-environment\",\n    \"exportedAt\": \"2024-01-01T00:00:00.000Z\",\n    \"exportedUsing\": \"Bruno/v1.0.0\"\n  }\n}\n"
  },
  {
    "path": "tests/environments/fixtures/environment-exports/bruno-collection-environments.json",
    "content": "{\n  \"info\": {\n    \"type\": \"bruno-environment\",\n    \"exportedAt\": \"2024-01-01T00:00:00.000Z\",\n    \"exportedUsing\": \"Bruno/v1.0.0\"\n  },\n  \"environments\": [\n    {\n      \"name\": \"local\",\n      \"variables\": [\n        {\n          \"name\": \"host\",\n          \"value\": \"http://localhost:3000\",\n          \"type\": \"text\",\n          \"enabled\": true,\n          \"secret\": false\n        },\n        {\n          \"name\": \"secretToken\",\n          \"value\": \"\",\n          \"type\": \"text\",\n          \"enabled\": true,\n          \"secret\": true\n        }\n      ]\n    },\n    {\n      \"name\": \"prod\",\n      \"variables\": [\n        {\n          \"name\": \"host\",\n          \"value\": \"https://echo.usebruno.com\",\n          \"type\": \"text\",\n          \"enabled\": true,\n          \"secret\": false\n        },\n        {\n          \"name\": \"secretToken\",\n          \"value\": \"\",\n          \"type\": \"text\",\n          \"enabled\": true,\n          \"secret\": true\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/environments/fixtures/environment-exports/bruno-global-environments/local.json",
    "content": "{\n  \"name\": \"local\",\n  \"variables\": [\n    {\n      \"name\": \"host\",\n      \"value\": \"http://localhost:3000\",\n      \"type\": \"text\",\n      \"enabled\": true,\n      \"secret\": false\n    },\n    {\n      \"name\": \"secretToken\",\n      \"value\": \"\",\n      \"type\": \"text\",\n      \"enabled\": true,\n      \"secret\": true\n    }\n  ],\n  \"info\": {\n    \"type\": \"bruno-environment\",\n    \"exportedAt\": \"2024-01-01T00:00:00.000Z\",\n    \"exportedUsing\": \"Bruno/v1.0.0\"\n  }\n}\n"
  },
  {
    "path": "tests/environments/fixtures/environment-exports/bruno-global-environments/prod.json",
    "content": "{\n  \"name\": \"prod\",\n  \"variables\": [\n    {\n      \"name\": \"host\",\n      \"value\": \"https://echo.usebruno.com\",\n      \"type\": \"text\",\n      \"enabled\": true,\n      \"secret\": false\n    },\n    {\n      \"name\": \"secretToken\",\n      \"value\": \"\",\n      \"type\": \"text\",\n      \"enabled\": true,\n      \"secret\": true\n    }\n  ],\n  \"info\": {\n    \"type\": \"bruno-environment\",\n    \"exportedAt\": \"2024-01-01T00:00:00.000Z\",\n    \"exportedUsing\": \"Bruno/v1.0.0\"\n  }\n}\n"
  },
  {
    "path": "tests/environments/fixtures/environment-exports/bruno-global-environments.json",
    "content": "{\n  \"info\": {\n    \"type\": \"bruno-environment\",\n    \"exportedAt\": \"2024-01-01T00:00:00.000Z\",\n    \"exportedUsing\": \"Bruno/v1.0.0\"\n  },\n  \"environments\": [\n    {\n      \"name\": \"local\",\n      \"variables\": [\n        {\n          \"name\": \"host\",\n          \"value\": \"http://localhost:3000\",\n          \"type\": \"text\",\n          \"enabled\": true,\n          \"secret\": false\n        },\n        {\n          \"name\": \"secretToken\",\n          \"value\": \"\",\n          \"type\": \"text\",\n          \"enabled\": true,\n          \"secret\": true\n        }\n      ]\n    },\n    {\n      \"name\": \"prod\",\n      \"variables\": [\n        {\n          \"name\": \"host\",\n          \"value\": \"https://echo.usebruno.com\",\n          \"type\": \"text\",\n          \"enabled\": true,\n          \"secret\": false\n        },\n        {\n          \"name\": \"secretToken\",\n          \"value\": \"\",\n          \"type\": \"text\",\n          \"enabled\": true,\n          \"secret\": true\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/environments/fixtures/environment-exports/local.json",
    "content": "{\n  \"name\": \"local\",\n  \"variables\": [\n    {\n      \"name\": \"host\",\n      \"value\": \"http://localhost:3000\",\n      \"type\": \"text\",\n      \"enabled\": true,\n      \"secret\": false\n    },\n    {\n      \"name\": \"secretToken\",\n      \"value\": \"\",\n      \"type\": \"text\",\n      \"enabled\": true,\n      \"secret\": true\n    }\n  ],\n  \"info\": {\n    \"type\": \"bruno-environment\",\n    \"exportedAt\": \"2024-01-01T00:00:00.000Z\",\n    \"exportedUsing\": \"Bruno/v1.0.0\"\n  }\n}\n"
  },
  {
    "path": "tests/environments/focus-retention/environment-focus.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { createCollection, createEnvironment, closeAllCollections } from '../../utils/page';\n\ntest.describe('Environment Variables Focus Retention', () => {\n  test.afterEach(async ({ page }) => {\n    if (!page.isClosed()) {\n      await closeAllCollections(page);\n    }\n  });\n\n  test('should keep focus on name input after save hotkey', async ({ page, createTmpDir }) => {\n    await createCollection(page, 'env-focus', await createTmpDir('env-focus'));\n    await createEnvironment(page, 'Focus Env', 'collection');\n\n    const nameInput = page.locator('input[name=\"0.name\"]');\n    await nameInput.waitFor({ state: 'visible' });\n    await nameInput.click();\n    await page.keyboard.type('apiKey');\n    await expect(nameInput).toBeFocused();\n\n    await page.keyboard.press('Control+s');\n    await expect(page.getByText('Changes saved successfully').last()).toBeVisible({ timeout: 5000 });\n\n    // intentionally wait a few seconds because the focus is lost after a while\n    await page.waitForTimeout(1000);\n    await expect(nameInput).toBeFocused();\n  });\n});\n"
  },
  {
    "path": "tests/environments/global-env-config-selection/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"global-env-config-selection\",\n  \"type\": \"collection\"\n}\n"
  },
  {
    "path": "tests/environments/global-env-config-selection/collection/test-request.bru",
    "content": "meta {\n  name: test-request\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}/api/echo\n  body: none\n  auth: none\n}\n\ntests {\n  test(\"should get 200 response\", function() {\n    expect(res.getStatus()).to.equal(200);\n  });\n}\n"
  },
  {
    "path": "tests/environments/global-env-config-selection/global-env-config-selection.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections } from '../../utils/page';\n\ntest.describe('Global Environment Configuration Selection Tests', () => {\n  test.afterEach(async ({ pageWithUserData: page }) => {\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  test('should open global environment config with currently active environment selected', async ({\n    pageWithUserData: page\n  }) => {\n    // Open the collection from sidebar\n    await page.locator('#sidebar-collection-name').filter({ hasText: 'global-env-config-selection' }).click();\n\n    // Get the currently active environment name\n    const currentEnvName = await page.locator('.current-environment').textContent() as string;\n\n    // Open global environment configuration\n    await page.getByTestId('environment-selector-trigger').click();\n    await page.getByTestId('env-tab-global').click();\n    await page.getByText('Configure', { exact: true }).click();\n\n    const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });\n    await expect(envTab).toBeVisible();\n\n    const activeEnvItem = page.locator('.environment-item.active');\n    await expect(activeEnvItem).toContainText(currentEnvName);\n\n    await envTab.hover();\n    await envTab.getByTestId('request-tab-close-icon').click({ force: true });\n  });\n});\n"
  },
  {
    "path": "tests/environments/global-env-config-selection/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{projectRoot}}/tests/environments/global-env-config-selection/collection\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/environments/global-env-config-selection/init-user-data/global-environments.json",
    "content": "{\n  \"environments\": [\n    {\n      \"uid\": \"FlaexlO7lcH7UtEpWsVyz\",\n      \"name\": \"Development Environment\",\n      \"variables\": [\n        {\n          \"uid\": \"lflBDSYBdHkUedYhBF4Ty\",\n          \"name\": \"env_type\",\n          \"value\": \"development\",\n          \"type\": \"text\",\n          \"secret\": false,\n          \"enabled\": true\n        }\n      ]\n    },\n    {\n      \"uid\": \"MsHcnAIonZ3455OfvpTUT\",\n      \"name\": \"Production Environment\",\n      \"variables\": [\n        {\n          \"uid\": \"TZljXLErzW1nUWoozntZE\",\n          \"name\": \"env_type\",\n          \"value\": \"production\",\n          \"type\": \"text\",\n          \"secret\": false,\n          \"enabled\": true\n        }\n      ]\n    },\n    {\n      \"uid\": \"VdUAdMPcfapMCqjKAeUiI\",\n      \"name\": \"Staging Environment\",\n      \"variables\": [\n        {\n          \"uid\": \"FwoWhHvu9eLhA8H4brG6f\",\n          \"name\": \"env_type\",\n          \"value\": \"staging\",\n          \"type\": \"text\",\n          \"secret\": false,\n          \"enabled\": true\n        }\n      ]\n    }\n  ],\n  \"activeGlobalEnvironmentUid\": \"MsHcnAIonZ3455OfvpTUT\"\n}"
  },
  {
    "path": "tests/environments/global-env-config-selection/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/environments/global-env-config-selection/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/environments/import-environment/bruno-env-import/collection-env-import/collection-env-import.spec.ts",
    "content": "import { test, expect } from '../../../../../playwright';\nimport path from 'path';\nimport fs from 'fs';\n\ntest.describe.serial('Collection Environment Import Tests', () => {\n  test('should import single collection environment', async ({ pageWithUserData: page }) => {\n    const singleEnvFile = path.join(__dirname, '../../../fixtures/environment-exports/local.json');\n    const collectionPath = path.join(__dirname, 'fixtures/collection');\n    const environmentsPath = path.join(collectionPath, 'environments');\n\n    await test.step('Clean up existing environments and open collection', async () => {\n      // Clean up any existing environments folder before test\n      if (fs.existsSync(environmentsPath)) {\n        fs.rmSync(environmentsPath, { recursive: true, force: true });\n      }\n\n      // Open the collection from sidebar\n      await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Import Test Collection' }).click();\n    });\n\n    await test.step('Navigate to collection environment import', async () => {\n      // Open environment import\n      await page.getByTestId('environment-selector-trigger').hover();\n      await page.getByTestId('environment-selector-trigger').click();\n      await page.getByTestId('env-tab-collection').click();\n      await expect(page.getByTestId('env-tab-collection')).toHaveClass(/active/);\n      await page.getByText('Import', { exact: true }).click();\n\n      // Verify import modal opens\n      const importModal = page.locator('[data-testid=\"import-environment-modal\"]');\n      await expect(importModal).toBeVisible();\n    });\n\n    await test.step('Import environment file', async () => {\n      // Import environment file\n      const fileChooserPromise = page.waitForEvent('filechooser');\n      await page.getByTestId('import-environment').click();\n      const fileChooser = await fileChooserPromise;\n      await fileChooser.setFiles(singleEnvFile);\n    });\n\n    await test.step('Verify imported environment and variables', async () => {\n      const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });\n      await expect(envTab).toBeVisible();\n\n      await expect(page.getByRole('row', { name: 'host' }).getByRole('cell').nth(1)).toBeVisible();\n      await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();\n\n      await envTab.hover();\n      await envTab.getByTestId('request-tab-close-icon').click({ force: true });\n    });\n\n    await test.step('Clean up after test', async () => {\n      // Clean up any existing environments folder before test\n      if (fs.existsSync(environmentsPath)) {\n        fs.rmSync(environmentsPath, { recursive: true, force: true });\n      }\n    });\n  });\n\n  test('should import multiple collection environments', async ({ pageWithUserData: page }) => {\n    const multiEnvFile = path.join(__dirname, '../../../fixtures/environment-exports/bruno-collection-environments.json');\n    const collectionPath = path.join(__dirname, 'fixtures/collection');\n    const environmentsPath = path.join(collectionPath, 'environments');\n\n    await test.step('Clean up existing environments and open collection', async () => {\n      // Clean up any existing environments folder before test\n      if (fs.existsSync(environmentsPath)) {\n        fs.rmSync(environmentsPath, { recursive: true, force: true });\n      }\n\n      // Open the collection from sidebar\n      await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Import Test Collection' }).click();\n    });\n\n    await test.step('Navigate to collection environment import', async () => {\n      // Open environment import\n\n      await page.getByTestId('environment-selector-trigger').hover();\n      await page.getByTestId('environment-selector-trigger').click();\n\n      await page.getByTestId('env-tab-collection').click();\n      await expect(page.getByTestId('env-tab-collection')).toHaveClass(/active/);\n      await page.getByText('Import', { exact: true }).click();\n\n      // Verify import modal opens\n      const importModal = page.locator('[data-testid=\"import-environment-modal\"]');\n      await expect(importModal).toBeVisible();\n    });\n\n    await test.step('Import multiple environments file', async () => {\n      // Import environment file\n      const fileChooserPromise = page.waitForEvent('filechooser');\n      await page.getByTestId('import-environment').click();\n      const fileChooser = await fileChooserPromise;\n      await fileChooser.setFiles(multiEnvFile);\n\n      const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });\n      await expect(envTab).toBeVisible();\n    });\n\n    await test.step('Verify both environments are available in selector', async () => {\n      await page.waitForTimeout(500);\n\n      await page.getByTestId('environment-selector-trigger').hover();\n      await page.getByTestId('environment-selector-trigger').click();\n\n      await page.waitForTimeout(300);\n      await expect(page.locator('.dropdown-item').filter({ hasText: /^local$/ })).toBeVisible({ timeout: 10000 });\n      await expect(page.locator('.dropdown-item').filter({ hasText: /^prod$/ })).toBeVisible({ timeout: 10000 });\n    });\n\n    await test.step('Test switching to prod environment and verify variables', async () => {\n      // Test switching to prod environment\n      await page.locator('.dropdown-item').filter({ hasText: 'prod' }).click();\n      await expect(page.locator('.current-environment')).toContainText('prod');\n\n      // Verify prod environment variables by opening settings again\n      await page.getByTestId('environment-selector-trigger').hover();\n      await page.getByTestId('environment-selector-trigger').click();\n      await page.getByText('Configure', { exact: true }).click();\n\n      const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });\n      await expect(envTab).toBeVisible();\n\n      // Verify prod environment variables\n      await expect(page.getByRole('row', { name: 'host' }).getByRole('cell').nth(1)).toBeVisible();\n      await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();\n\n      await envTab.hover();\n      await envTab.getByTestId('request-tab-close-icon').click({ force: true });\n    });\n\n    await test.step('Clean up after test', async () => {\n      // Clean up any existing environments folder before test\n      if (fs.existsSync(environmentsPath)) {\n        fs.rmSync(environmentsPath, { recursive: true, force: true });\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "tests/environments/import-environment/bruno-env-import/collection-env-import/fixtures/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"Environment Import Test Collection\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}\n"
  },
  {
    "path": "tests/environments/import-environment/bruno-env-import/collection-env-import/fixtures/collection/test-request.bru",
    "content": "meta {\n  name: Test Request\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}\n}\n"
  },
  {
    "path": "tests/environments/import-environment/bruno-env-import/collection-env-import/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{projectRoot}}/tests/environments/import-environment/bruno-env-import/collection-env-import/fixtures/collection\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/environments/import-environment/bruno-env-import/collection-env-import/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/environments/import-environment/bruno-env-import/collection-env-import/fixtures/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/environments/import-environment/bruno-env-import/global-env-import/fixtures/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"Environment Import Test Collection\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}\n"
  },
  {
    "path": "tests/environments/import-environment/bruno-env-import/global-env-import/fixtures/collection/test-request.bru",
    "content": "meta {\n  name: Test Request\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{host}}\n}\n"
  },
  {
    "path": "tests/environments/import-environment/bruno-env-import/global-env-import/global-env-import.spec.ts",
    "content": "import { test, expect } from '../../../../../playwright';\nimport path from 'path';\n\ntest.describe.serial('Global Environment Import Tests', () => {\n  test('should import single global environment', async ({ pageWithUserData: page }) => {\n    const singleEnvFile = path.join(__dirname, '../../../fixtures/environment-exports/local.json');\n\n    await test.step('Open collection and clean up existing global environments', async () => {\n      // Open the collection from sidebar\n      await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Import Test Collection' }).click();\n\n      // Clean up any existing global environments before test\n      await page.getByTestId('environment-selector-trigger').click();\n      await page.getByTestId('env-tab-global').click();\n\n      // Check if there are existing environments to delete\n      const existingEnvs = page.locator('.dropdown-item').filter({ hasText: /^(local|prod)$/ });\n      const count = await existingEnvs.count();\n\n      if (count > 0) {\n        // Open global environment settings to delete existing environments\n        await page.getByText('Configure', { exact: true }).click();\n\n        // Delete all existing environments\n        for (let i = 0; i < count; i++) {\n          await page.locator('button[title=\"Delete\"]').first().click();\n          const confirmButton = page.getByRole('button', { name: 'Delete' });\n          if (await confirmButton.isVisible()) {\n            await confirmButton.click();\n          }\n        }\n\n        // Clean up any existing global environments before test\n        await page.getByTestId('environment-selector-trigger').click();\n        await page.getByTestId('env-tab-global').click();\n      }\n    });\n\n    await test.step('Navigate to global environment import', async () => {\n      await expect(page.getByTestId('env-tab-global')).toHaveClass(/active/);\n      await page.getByRole('button', { name: 'Import', exact: true }).click();\n\n      // Verify import modal opens\n      const importModal = page.locator('[data-testid=\"import-global-environment-modal\"]');\n      await expect(importModal).toBeVisible();\n    });\n\n    await test.step('Import global environment file', async () => {\n      // Import environment file\n      const fileChooserPromise = page.waitForEvent('filechooser');\n      await page.locator('[data-testid=\"import-global-environment\"]').click();\n      const fileChooser = await fileChooserPromise;\n      await fileChooser.setFiles(singleEnvFile);\n    });\n\n    await test.step('Verify imported global environment and variables', async () => {\n      const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });\n      await expect(envTab).toBeVisible();\n\n      // Verify imported variables\n      await expect(page.getByRole('row', { name: 'host' }).getByRole('cell').nth(1)).toBeVisible();\n      await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();\n\n      await envTab.hover();\n      await envTab.getByTestId('request-tab-close-icon').click({ force: true });\n    });\n  });\n\n  test('should import multiple global environments', async ({ pageWithUserData: page }) => {\n    const multiEnvFile = path.join(__dirname, '../../../fixtures/environment-exports/bruno-global-environments.json');\n\n    await test.step('Open collection and clean up existing global environments', async () => {\n      // Open the collection from sidebar\n      await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Import Test Collection' }).click();\n\n      // Clean up any existing global environments before test\n      await page.getByTestId('environment-selector-trigger').click();\n      await page.getByTestId('env-tab-global').click();\n\n      // Check if there are existing environments to delete\n      const existingEnvs = page.locator('.dropdown-item').filter({ hasText: /^(local|prod)$/ });\n      const count = await existingEnvs.count();\n\n      if (count > 0) {\n        // Open global environment settings to delete existing environments\n        await page.getByText('Configure', { exact: true }).click();\n\n        // Delete all existing environments\n        for (let i = 0; i < count; i++) {\n          await page.locator('button[title=\"Delete\"]').first().click();\n          const confirmButton = page.getByText('Delete', { exact: true });\n          if (await confirmButton.isVisible()) {\n            await confirmButton.click();\n          }\n        }\n\n        // Clean up any existing global environments before test\n        await page.getByTestId('environment-selector-trigger').click();\n        await page.getByTestId('env-tab-global').click();\n      }\n    });\n\n    await test.step('Navigate to global environment import', async () => {\n      await page.getByText('Import', { exact: true }).click();\n\n      // Verify import modal opens\n      const importModal = page.locator('[data-testid=\"import-global-environment-modal\"]');\n      await expect(importModal).toBeVisible();\n    });\n\n    await test.step('Import multiple global environments file', async () => {\n      // Import environment file\n      const fileChooserPromise = page.waitForEvent('filechooser');\n      await page.locator('[data-testid=\"import-global-environment\"]').click();\n      const fileChooser = await fileChooserPromise;\n      await fileChooser.setFiles(multiEnvFile);\n\n      const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });\n      await expect(envTab).toBeVisible();\n    });\n\n    await test.step('Verify both global environments are available in selector', async () => {\n      await page.getByTestId('environment-selector-trigger').click();\n      await page.getByTestId('env-tab-global').click();\n\n      // Verify both environments are in the dropdown\n      await expect(page.locator('.dropdown-item').filter({ hasText: /^local$/ })).toBeVisible();\n      await expect(page.locator('.dropdown-item').filter({ hasText: /^prod$/ })).toBeVisible();\n    });\n\n    await test.step('Test switching to prod environment and verify variables', async () => {\n      // Test switching to prod environment\n      await page.locator('.dropdown-item').filter({ hasText: 'prod' }).click();\n      await expect(page.locator('.current-environment')).toContainText('prod');\n\n      // Verify prod environment variables by opening settings again\n      await page.getByTestId('environment-selector-trigger').click();\n      await page.getByTestId('env-tab-global').click();\n      await page.getByText('Configure', { exact: true }).click();\n      const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });\n      await expect(envTab).toBeVisible();\n\n      // Verify imported variables\n      await expect(page.getByRole('row', { name: 'host' }).getByRole('cell').nth(1)).toBeVisible();\n      await expect(page.getByRole('row', { name: 'secretToken' }).getByRole('cell').nth(1)).toBeVisible();\n\n      await envTab.hover();\n      await envTab.getByTestId('request-tab-close-icon').click({ force: true });\n    });\n  });\n});\n"
  },
  {
    "path": "tests/environments/import-environment/bruno-env-import/global-env-import/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{projectRoot}}/tests/environments/import-environment/bruno-env-import/global-env-import/fixtures/collection\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/environments/import-environment/bruno-env-import/global-env-import/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/environments/import-environment/bruno-env-import/global-env-import/fixtures/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/environments/import-environment/collection-env-import.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport path from 'path';\nimport { closeAllCollections } from '../../utils/page';\n\ntest.describe('Collection Environment Import Tests', () => {\n  test.afterAll(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('should import collection environment from file', async ({ page, createTmpDir }) => {\n    const openApiFile = path.join(__dirname, 'fixtures', 'collection.json');\n    const envFile = path.join(__dirname, 'fixtures', 'collection-env.json');\n\n    // Import test collection\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    const importModal = page.locator('[data-testid=\"import-collection-modal\"]');\n    await importModal.waitFor({ state: 'visible' });\n\n    await page.setInputFiles('input[type=\"file\"]', openApiFile);\n\n    const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n    await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n    await expect(locationModal.getByText('Environment Test Collection')).toBeVisible();\n\n    // Select a location and import\n    await page.locator('#collection-location').fill(await createTmpDir('collection-env-import-test'));\n    await locationModal.getByRole('button', { name: 'Import' }).click();\n    await locationModal.waitFor({ state: 'hidden' });\n\n    await expect(\n      page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Test Collection' })).toBeVisible({ timeout: 10000 });\n\n    // Configure collection\n    await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Test Collection' }).click();\n\n    // Import collection environment\n    await page.locator('[data-testid=\"environment-selector-trigger\"]').click();\n    await expect(page.locator('[data-testid=\"env-tab-collection\"]')).toHaveClass(/active/);\n    await page.locator('button[id=\"import-env\"]').click();\n    const importEnvModal = page.locator('[data-testid=\"import-environment-modal\"]');\n    await expect(importEnvModal).toBeVisible();\n\n    // Import environment file\n    const fileChooserPromise = page.waitForEvent('filechooser');\n    await page.getByTestId('import-environment').click();\n    const fileChooser = await fileChooserPromise;\n    await fileChooser.setFiles(envFile);\n\n    // Wait for import to complete and environment settings modal to open\n    await expect(page.locator('.current-environment')).toContainText('Test Collection Environment');\n\n    const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });\n    await expect(envTab).toBeVisible();\n\n    await expect(page.locator('input[name=\"0.name\"]')).toHaveValue('host');\n    await expect(page.locator('input[name=\"1.name\"]')).toHaveValue('userId');\n    await expect(page.locator('input[name=\"2.name\"]')).toHaveValue('apiKey');\n    await expect(page.locator('input[name=\"3.name\"]')).toHaveValue('postTitle');\n    await expect(page.locator('input[name=\"4.name\"]')).toHaveValue('postBody');\n    await expect(page.locator('input[name=\"5.name\"]')).toHaveValue('secretApiToken');\n    await expect(page.locator('input[name=\"5.secret\"]')).toBeChecked();\n    await envTab.hover();\n    await envTab.getByTestId('request-tab-close-icon').click({ force: true });\n\n    await page.locator('.collection-item-name').first().click();\n    await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}/posts/{{userId}}');\n    await page.locator('[data-testid=\"send-arrow-icon\"]').click();\n    await page.locator('[data-testid=\"response-status-code\"]').waitFor({ state: 'visible' });\n    await expect(page.locator('[data-testid=\"response-status-code\"]')).toContainText('200');\n\n    // Verify the JSON response contains the interpolated userId\n    const responsePane = page.locator('.response-pane');\n    await expect(responsePane).toContainText('\"userId\": 1');\n\n    // Test POST request\n    await page.locator('.collection-item-name').nth(1).click();\n    await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}/posts');\n    await page.locator('[data-testid=\"send-arrow-icon\"]').click();\n    await page.locator('[data-testid=\"response-status-code\"]').waitFor({ state: 'visible' });\n    await expect(page.locator('[data-testid=\"response-status-code\"]')).toContainText('201');\n  });\n});\n"
  },
  {
    "path": "tests/environments/import-environment/env-color-import/env-color-import.spec.ts",
    "content": "import { test, expect } from '../../../../playwright';\nimport path from 'path';\nimport { closeAllCollections } from '../../../utils/page';\n\ntest.describe.serial('Environment Color Import Tests', () => {\n  test.afterAll(async ({ pageWithUserData: page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('should import global environment with color preserved', async ({ pageWithUserData: page }) => {\n    const envWithColorFile = path.join(__dirname, 'fixtures/env-with-color.json');\n\n    await test.step('Open collection and navigate to global environment import', async () => {\n      // Open the collection from sidebar\n      const collectionName = page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Color Import Test Collection' });\n      await expect(collectionName).toBeVisible();\n      await collectionName.click();\n\n      // Open environment selector dropdown\n      const envSelector = page.getByTestId('environment-selector-trigger');\n      await expect(envSelector).toBeVisible();\n      await envSelector.click();\n\n      // Click global tab\n      const globalTab = page.getByTestId('env-tab-global');\n      await expect(globalTab).toBeVisible();\n      await globalTab.click();\n\n      // Verify global tab is active\n      await expect(globalTab).toHaveClass(/active/);\n\n      // Click Import button\n      await page.getByRole('button', { name: 'Import', exact: true }).click();\n\n      // Verify import modal opens\n      const importModal = page.getByTestId('import-global-environment-modal');\n      await expect(importModal).toBeVisible();\n    });\n\n    await test.step('Import environment with color', async () => {\n      // Import environment file\n      const fileChooserPromise = page.waitForEvent('filechooser');\n      await page.getByTestId('import-global-environment').click();\n      const fileChooser = await fileChooserPromise;\n      await fileChooser.setFiles(envWithColorFile);\n\n      // Wait for the environment tab to appear\n      const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });\n      await expect(envTab).toBeVisible();\n    });\n\n    await test.step('Verify imported environment has the color badge displayed', async () => {\n      // The color badge should be visible in the environment details\n      const colorBadge = page.locator('div.rounded-full[style*=\"background-color: rgb(16, 185, 129)\"]').first();\n      await expect(colorBadge).toBeVisible();\n    });\n  });\n});\n"
  },
  {
    "path": "tests/environments/import-environment/env-color-import/fixtures/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"Environment Color Import Test Collection\",\n  \"type\": \"collection\"\n}\n"
  },
  {
    "path": "tests/environments/import-environment/env-color-import/fixtures/collection/test-request.bru",
    "content": "meta {\n  name: Test Request\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://httpbin.org/get\n  body: none\n  auth: none\n}\n"
  },
  {
    "path": "tests/environments/import-environment/env-color-import/fixtures/env-with-color.json",
    "content": "{\n  \"name\": \"colored-env\",\n  \"variables\": [\n    {\n      \"name\": \"baseUrl\",\n      \"value\": \"https://api.example.com\",\n      \"type\": \"text\",\n      \"enabled\": true,\n      \"secret\": false\n    }\n  ],\n  \"color\": \"#10B981\",\n  \"info\": {\n    \"type\": \"bruno-environment\",\n    \"exportedAt\": \"2024-01-01T00:00:00.000Z\",\n    \"exportedUsing\": \"Bruno/v1.0.0\"\n  }\n}\n"
  },
  {
    "path": "tests/environments/import-environment/env-color-import/fixtures/multiple-envs-with-colors.json",
    "content": "{\n  \"info\": {\n    \"type\": \"bruno-environment\",\n    \"exportedAt\": \"2024-01-01T00:00:00.000Z\",\n    \"exportedUsing\": \"Bruno/v1.0.0\"\n  },\n  \"environments\": [\n    {\n      \"name\": \"dev\",\n      \"variables\": [\n        {\n          \"name\": \"apiUrl\",\n          \"value\": \"https://dev.api.example.com\",\n          \"type\": \"text\",\n          \"enabled\": true,\n          \"secret\": false\n        }\n      ],\n      \"color\": \"#3B82F6\"\n    },\n    {\n      \"name\": \"staging\",\n      \"variables\": [\n        {\n          \"name\": \"apiUrl\",\n          \"value\": \"https://staging.api.example.com\",\n          \"type\": \"text\",\n          \"enabled\": true,\n          \"secret\": false\n        }\n      ],\n      \"color\": \"#F59E0B\"\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/environments/import-environment/env-color-import/init-user-data/collection-security.json",
    "content": "{}\n"
  },
  {
    "path": "tests/environments/import-environment/env-color-import/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{collectionPath}}\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/environments/import-environment/fixtures/collection-env.json",
    "content": "{\n  \"id\": \"test-collection-env-id\",\n  \"name\": \"Test Collection Environment\",\n  \"values\": [\n    {\n      \"key\": \"host\",\n      \"value\": \"https://jsonplaceholder.typicode.com\",\n      \"enabled\": true,\n      \"type\": \"text\"\n    },\n    {\n      \"key\": \"userId\",\n      \"value\": \"1\",\n      \"enabled\": true,\n      \"type\": \"text\"\n    },\n    {\n      \"key\": \"apiKey\",\n      \"value\": \"collection-api-key-12345\",\n      \"enabled\": true,\n      \"type\": \"text\"\n    },\n    {\n      \"key\": \"postTitle\",\n      \"value\": \"Collection Environment Test Post\",\n      \"enabled\": true,\n      \"type\": \"text\"\n    },\n    {\n      \"key\": \"postBody\",\n      \"value\": \"This is a test post created using collection environment variables\",\n      \"enabled\": true,\n      \"type\": \"text\"\n    },\n    {\n      \"key\": \"secretApiToken\",\n      \"value\": \"collection-secret-token-67890\",\n      \"enabled\": true,\n      \"type\": \"secret\"\n    }\n  ],\n  \"_postman_variable_scope\": \"environment\",\n  \"_postman_exported_at\": \"2024-01-01T00:00:00.000Z\",\n  \"_postman_exported_using\": \"Postman/10.0.0\"\n}"
  },
  {
    "path": "tests/environments/import-environment/fixtures/collection.json",
    "content": "{\n  \"info\": {\n    \"name\": \"Environment Test Collection\",\n    \"description\": \"Test collection for environment import and usage tests\",\n    \"schema\": \"https://schema.getpostman.com/json/collection/v2.1.0/collection.json\",\n    \"_postman_id\": \"env-test-collection-id\"\n  },\n  \"item\": [\n    {\n      \"name\": \"Get Posts with Environment Variables\",\n      \"request\": {\n        \"method\": \"GET\",\n        \"header\": [\n          {\n            \"key\": \"Authorization\",\n            \"value\": \"Bearer {{apiKey}}\",\n            \"type\": \"text\"\n          },\n          {\n            \"key\": \"X-Secret-Token\",\n            \"value\": \"{{secretApiToken}}\",\n            \"type\": \"text\"\n          }\n        ],\n        \"url\": {\n          \"raw\": \"{{host}}/posts/{{userId}}\",\n          \"host\": [\"{{host}}\"],\n          \"path\": [\"posts\", \"{{userId}}\"]\n        }\n      },\n      \"response\": []\n    },\n    {\n      \"name\": \"Create Post with Body Variables\",\n      \"request\": {\n        \"method\": \"POST\",\n        \"header\": [\n          {\n            \"key\": \"Authorization\",\n            \"value\": \"Bearer {{apiKey}}\",\n            \"type\": \"text\"\n          },\n          {\n            \"key\": \"Content-Type\",\n            \"value\": \"application/json\",\n            \"type\": \"text\"\n          },\n          {\n            \"key\": \"X-Secret-Token\",\n            \"value\": \"{{secretApiToken}}\",\n            \"type\": \"text\"\n          }\n        ],\n        \"body\": {\n          \"mode\": \"raw\",\n          \"raw\": \"{\\n  \\\"title\\\": \\\"{{postTitle}}\\\",\\n  \\\"body\\\": \\\"{{postBody}}\\\",\\n  \\\"userId\\\": {{userId}}\\n}\"\n        },\n        \"url\": {\n          \"raw\": \"{{host}}/posts\",\n          \"host\": [\"{{host}}\"],\n          \"path\": [\"posts\"]\n        }\n      },\n      \"response\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/environments/import-environment/fixtures/global-env.json",
    "content": "{\n  \"id\": \"test-global-env-id\",\n  \"name\": \"Test Global Environment\",\n  \"values\": [\n    {\n      \"key\": \"host\",\n      \"value\": \"https://jsonplaceholder.typicode.com\",\n      \"enabled\": true,\n      \"type\": \"text\"\n    },\n    {\n      \"key\": \"userId\",\n      \"value\": \"1\",\n      \"enabled\": true,\n      \"type\": \"text\"\n    },\n    {\n      \"key\": \"apiKey\",\n      \"value\": \"global-api-key-12345\",\n      \"enabled\": true,\n      \"type\": \"text\"\n    },\n    {\n      \"key\": \"postTitle\",\n      \"value\": \"Global Test Post from Environment\",\n      \"enabled\": true,\n      \"type\": \"text\"\n    },\n    {\n      \"key\": \"postBody\",\n      \"value\": \"This is a global test post body with environment variables\",\n      \"enabled\": true,\n      \"type\": \"text\"\n    },\n    {\n      \"key\": \"secretApiToken\",\n      \"value\": \"global-secret-token-67890\",\n      \"enabled\": true,\n      \"type\": \"secret\"\n    }\n  ],\n  \"_postman_variable_scope\": \"globals\",\n  \"_postman_exported_at\": \"2024-01-01T00:00:00.000Z\",\n  \"_postman_exported_using\": \"Postman/10.0.0\"\n}"
  },
  {
    "path": "tests/environments/import-environment/global-env-import.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport path from 'path';\nimport { closeAllCollections } from '../../utils/page';\n\ntest.describe('Global Environment Import Tests', () => {\n  test('should import global environment from file', async ({ newPage: page, createTmpDir }) => {\n    const openApiFile = path.join(__dirname, 'fixtures', 'collection.json');\n    const globalEnvFile = path.join(__dirname, 'fixtures', 'global-env.json');\n\n    // Import test collection\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    const importModal = page.locator('[data-testid=\"import-collection-modal\"]');\n    await importModal.waitFor({ state: 'visible' });\n\n    await page.setInputFiles('input[type=\"file\"]', openApiFile);\n\n    const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n    await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n    await expect(locationModal.getByText('Environment Test Collection')).toBeVisible();\n\n    await page.locator('#collection-location').fill(await createTmpDir('global-env-import-test'));\n    await locationModal.getByRole('button', { name: 'Import' }).click();\n\n    await expect(\n      page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Test Collection' })).toBeVisible({ timeout: 10000 });\n\n    // Configure collection\n    await page.locator('#sidebar-collection-name').filter({ hasText: 'Environment Test Collection' }).click();\n\n    // Import global environment\n    await page.getByTestId('environment-selector-trigger').click();\n    await page.getByTestId('env-tab-global').click();\n    await page.getByText('Import', { exact: true }).click();\n    const importGlobalEnvModal = page.locator('[data-testid=\"import-global-environment-modal\"]');\n    await expect(importGlobalEnvModal).toBeVisible();\n\n    // Import environment file\n    const fileChooserPromise = page.waitForEvent('filechooser');\n    await page.locator('[data-testid=\"import-global-environment\"]').click();\n    const fileChooser = await fileChooserPromise;\n    await fileChooser.setFiles(globalEnvFile);\n\n    // Wait for import to complete and global environment settings modal to open\n    await expect(page.locator('.current-environment')).toContainText('Test Global Environment');\n\n    const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });\n    await expect(envTab).toBeVisible();\n\n    const variablesTable = page.locator('.table-container');\n    await expect(variablesTable.locator('input[name=\"0.name\"]')).toHaveValue('host');\n    await expect(variablesTable.locator('input[name=\"1.name\"]')).toHaveValue('userId');\n    await expect(variablesTable.locator('input[name=\"2.name\"]')).toHaveValue('apiKey');\n    await expect(variablesTable.locator('input[name=\"3.name\"]')).toHaveValue('postTitle');\n    await expect(variablesTable.locator('input[name=\"4.name\"]')).toHaveValue('postBody');\n    await expect(variablesTable.locator('input[name=\"5.name\"]')).toHaveValue('secretApiToken');\n    await expect(variablesTable.locator('input[name=\"5.secret\"]')).toBeChecked();\n    await envTab.hover();\n    await envTab.getByTestId('request-tab-close-icon').click({ force: true });\n\n    await page.locator('#collection-environment-test-collection .collection-item-name').first().click();\n    await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}/posts/{{userId}}');\n    await page.locator('[data-testid=\"send-arrow-icon\"]').click();\n    await page.locator('[data-testid=\"response-status-code\"]').waitFor({ state: 'visible' });\n    await expect(page.locator('[data-testid=\"response-status-code\"]')).toContainText('200');\n\n    // Verify the JSON response contains the interpolated userId\n    const responsePane = page.locator('.response-pane');\n    await expect(responsePane).toContainText('\"userId\": 1');\n\n    // Test POST request\n    await page.locator('#collection-environment-test-collection .collection-item-name').nth(1).click();\n    await expect(page.locator('#request-url .CodeMirror-line')).toContainText('{{host}}/posts');\n    await page.locator('[data-testid=\"send-arrow-icon\"]').click();\n    await page.locator('[data-testid=\"response-status-code\"]').waitFor({ state: 'visible' });\n    await expect(page.locator('[data-testid=\"response-status-code\"]')).toContainText('201');\n\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n});\n"
  },
  {
    "path": "tests/environments/multiline-variables/fixtures/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"multiline-variables\",\n  \"type\": \"collection\"\n}\n"
  },
  {
    "path": "tests/environments/multiline-variables/fixtures/collection/collection.bru",
    "content": "meta {\n  name: multiline-variables\n  type: collection\n  version: 1.0.0\n}"
  },
  {
    "path": "tests/environments/multiline-variables/fixtures/collection/environments/Test.bru",
    "content": "vars {\n  host: https://www.httpfaker.org\n  multiline_data: '''\n    line1\n    line2\n    line3\n  '''\n}\n"
  },
  {
    "path": "tests/environments/multiline-variables/fixtures/collection/multiline-test.bru",
    "content": "meta {\n  name: multiline-test\n  type: http\n  seq: 2\n}\n\npost {\n  url: {{host}}/api/echo\n  body: json\n  auth: none\n}\n\nbody:json {\n  {{multiline_data_json}}\n}\n\ntests {\n  test(\"should post multiline data successfully\", function() {\n    expect(res.getStatus()).to.equal(200);\n  });\n\n  test(\"should resolve multiline_data_json variable correctly\", function() {\n    const body = res.getBody();\n    // HTTP Faker echo endpoint returns the request body in body.body\n    // Verify the multiline JSON variable was resolved and parsed correctly\n    expect(body.body.user.name).to.equal(\"John Doe\");\n    expect(body.body.user.email).to.equal(\"john@example.com\");\n    expect(body.body.user.preferences.theme).to.equal(\"dark\");\n    expect(body.body.user.preferences.notifications).to.equal(true);\n  });\n\n  test(\"should preserve JSON structure from multiline variable\", function() {\n    const body = res.getBody();\n    // Verify the complete JSON structure was preserved\n    expect(body.body.metadata.created).to.equal(\"2025-09-03\");\n    expect(body.body.metadata.version).to.equal(\"1.0\");\n  });\n\n  test(\"should resolve host variable in URL\", function() {\n    const body = res.getBody();\n    // Verify the host variable was resolved in the request URL\n    expect(body.url).to.equal(\"https://www.httpfaker.org/api/echo\");\n  });\n}\n"
  },
  {
    "path": "tests/environments/multiline-variables/fixtures/collection/request.bru",
    "content": "meta {\n  name: request\n  type: http\n  seq: 1\n}\n\npost {\n  url: {{host}}/api/echo\n  body: text\n  auth: none\n}\n\nbody:json {\n  Ping Test Request\n  Host: {{host}}\n  \n  Multiline Data:\n  {{multiline_data}}\n  \n  End of multiline content.\n}\n\nbody:text {\n  {{host}}\n  {{multiline_data}}\n}\n\ntests {\n  test(\"should get 200 response\", function() {\n    expect(res.getStatus()).to.equal(200);\n  });\n  \n  test(\"should resolve multiline_data variable correctly\", function() {\n    const body = res.getBody();\n    // Verify the multiline variable was resolved and contains all three lines\n    expect(body.body).to.equal(\"https://www.httpfaker.org\\nline1\\nline2\\nline3\");\n  });\n}\n"
  },
  {
    "path": "tests/environments/multiline-variables/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{collectionPath}}\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"developer\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "tests/environments/multiline-variables/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{collectionPath}}\"\n  ],\n  \"request\": {\n    \"sslVerification\": false,\n    \"customCaCertificate\": {\n      \"enabled\": false,\n      \"filePath\": null\n    }\n  },\n  \"font\": {\n    \"codeFont\": \"default\"\n  },\n  \"proxy\": {\n    \"enabled\": false,\n    \"protocol\": \"http\",\n    \"hostname\": \"\",\n    \"port\": \"\",\n    \"auth\": {\n      \"enabled\": false,\n      \"username\": \"\",\n      \"password\": \"\"\n    },\n    \"bypassProxy\": \"\"\n  },\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/environments/multiline-variables/read-multiline-environment.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\n\ntest.describe('Multiline Variables - Read Environment Test', () => {\n  test('should read existing multiline environment variables', async ({ pageWithUserData: page }) => {\n    test.setTimeout(30 * 1000);\n\n    // open the collection\n    const collection = page.getByTestId('collections').locator('#sidebar-collection-name').filter({ hasText: 'multiline-variables' });\n    await expect(collection).toBeVisible();\n    await collection.click();\n\n    // open request\n    await expect(page.getByTitle('request', { exact: true })).toBeVisible();\n    await page.getByTitle('request', { exact: true }).click();\n\n    // open environment dropdown\n    await page.locator('div.current-environment').click();\n\n    // select test environment\n    await expect(page.locator('.dropdown-item').filter({ hasText: 'Test' })).toBeVisible();\n    await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click();\n    await expect(page.locator('.current-environment').filter({ hasText: /Test/ })).toBeVisible();\n\n    // send request\n    const sendButton = page.locator('#send-request').getByRole('img').nth(2);\n    await expect(sendButton).toBeVisible();\n    await sendButton.click();\n    await expect(page.locator('.response-status-code.text-ok')).toBeVisible();\n    await expect(page.locator('.response-status-code')).toContainText('200');\n\n    // response pane should contain the expected multiline text in JSON body\n    const responsePane = page.locator('.response-pane');\n    await expect(responsePane).toContainText('\"body\": \"https://www.httpfaker.org\\\\nline1\\\\nline2\\\\nline3\"');\n  });\n});\n"
  },
  {
    "path": "tests/environments/multiline-variables/write-multiline-variable.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\n\ntest.describe('Multiline Variables - Write Test', () => {\n  test('should create and use multiline environment variable dynamically', async ({ pageWithUserData: page }) => {\n    // open the collection\n    const collection = page.getByTestId('collections').locator('#sidebar-collection-name').filter({ hasText: 'multiline-variables' });\n    await expect(collection).toBeVisible();\n    await collection.click();\n\n    // open request\n    await expect(page.getByTitle('multiline-test', { exact: true })).toBeVisible();\n    await page.getByTitle('multiline-test', { exact: true }).dblclick();\n\n    // open environment dropdown\n    await page.locator('div.current-environment').click();\n\n    // select test environment\n    await expect(page.locator('.dropdown-item').filter({ hasText: 'Test' })).toBeVisible();\n    await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click();\n    await expect(page.locator('.current-environment').filter({ hasText: /Test/ })).toBeVisible();\n\n    // select configure button from environment dropdown\n    await page.locator('div.current-environment').click();\n\n    // open environment configuration\n    await expect(page.getByText('Configure', { exact: true })).toBeVisible();\n    await page.getByText('Configure', { exact: true }).click();\n\n    const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });\n    await expect(envTab).toBeVisible();\n\n    const emptyRowNameInput = page.locator('tbody tr').last().locator('input[placeholder=\"Name\"]');\n    await expect(emptyRowNameInput).toBeVisible();\n    await emptyRowNameInput.fill('multiline_data_json');\n\n    // After filling the name, the table appends a new empty row causing persistent layout shifts.\n    // Use force:true to bypass Playwright's stability check on the CodeMirror click.\n    const variableRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\"multiline_data_json\"]') });\n    await expect(variableRow).toBeVisible();\n    const codeMirror = variableRow.locator('.CodeMirror');\n\n    const jsonValue = `{\n  \"user\": {\n    \"name\": \"John Doe\",\n    \"email\": \"john@example.com\",\n    \"preferences\": {\n      \"theme\": \"dark\",\n      \"notifications\": true\n    }\n  },\n  \"metadata\": {\n    \"created\": \"2025-09-03\",\n    \"version\": \"1.0\"\n  }\n}`;\n\n    await codeMirror.click({ force: true });\n    await page.keyboard.insertText(jsonValue);\n\n    await page.getByTestId('save-env').click();\n\n    await envTab.hover();\n    await envTab.getByTestId('request-tab-close-icon').click({ force: true });\n\n    await page.getByTestId('send-arrow-icon').click();\n\n    // wait for response status\n    await expect(page.locator('.response-status-code.text-ok')).toBeVisible();\n    await expect(page.locator('.response-status-code')).toContainText('200');\n\n    // verify multiline JSON variable resolution in response\n    const expectedBody\n      = '{\\n  \"user\": {\\n    \"name\": \"John Doe\",\\n    \"email\": \"john@example.com\",\\n    \"preferences\": {\\n      \"theme\": \"dark\",\\n      \"notifications\": true\\n    }\\n  },\\n  \"metadata\": {\\n    \"created\": \"2025-09-03\",\\n    \"version\": \"1.0\"\\n  }\\n}';\n    await expect(page.locator('.response-pane')).toContainText(`\"body\": ${JSON.stringify(expectedBody)}`);\n  });\n});\n"
  },
  {
    "path": "tests/environments/update-global-environment-via-script/fixtures/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"Global Environment Update\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}"
  },
  {
    "path": "tests/environments/update-global-environment-via-script/fixtures/collection/collection.bru",
    "content": "meta {\n  name: Global Environment Update\n}\n\nauth {\n  mode: none\n}\n\nscript:pre-request {\n  //create a new global env variable.\n  bru.setGlobalEnvVar('newEnv', \"newEnvValue\");\n}\n"
  },
  {
    "path": "tests/environments/update-global-environment-via-script/fixtures/collection/test-request.bru",
    "content": "meta {\n  name: Test Request\n  type: http\n  seq: 1\n}\n\nget {\n  url: {{baseUrl}}/users\n  body: json\n  auth: inherit\n}\n\nscript:pre-request {\n  //update already existing enabled env variable\n  bru.setGlobalEnvVar(\"existingEnvEnabled\", \"newExistingEnvEnabledValue\");\n\n  //update already existing disabled env variable\n  bru.setGlobalEnvVar(\"existingEnvDisabled\", \"newExistingEnvDisabledValue\");\n}\n\nsettings {\n  encodeUrl: true\n  timeout: 0\n}\n"
  },
  {
    "path": "tests/environments/update-global-environment-via-script/global-env-update-via-script.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections } from '../../utils/page';\n\ntest.describe('Global Environment Variable Update via Script', () => {\n  test.afterEach(async ({ pageWithUserData: page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('should update global environment values via script and verify the changes', async ({\n    pageWithUserData: page\n  }) => {\n    await test.step('Open the collection from sidebar', async () => {\n      await page.locator('#sidebar-collection-name').filter({ hasText: 'Global Environment Update' }).click();\n    });\n\n    await test.step('Open the test request that has a pre-request script', async () => {\n      await page.locator('.collection-name', { hasText: 'Global Environment Update' }).click();\n      await page.locator('.collection-item-name', { hasText: 'Test Request' }).click();\n    });\n\n    await test.step('Run the request', async () => {\n      await page.getByTestId('send-arrow-icon').click();\n    });\n\n    await test.step('Open the Global Environment Config tab', async () => {\n      await page.getByTestId('environment-selector-trigger').click();\n      await page.getByTestId('env-tab-global').click();\n      await page.getByText('Configure', { exact: true }).click();\n\n      const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });\n      await expect(envTab).toBeVisible();\n    });\n\n    await test.step('Verify that the value of \"existingEnvEnabled\" is updated by the pre-request script', async () => {\n      const row = page.locator('tbody tr').filter({ has: page.locator('input[value=\"existingEnvEnabled\"]') });\n      const value = await row.locator('.CodeMirror-line').first().textContent();\n      await expect(value).toContain('newExistingEnvEnabledValue');\n    });\n\n    await test.step('Verify that the value of \"existingEnvDisabled\" is updated by the pre-request script', async () => {\n      const row = page.locator('tbody tr').filter({ has: page.locator('input[value=\"existingEnvDisabled\"]') });\n      const value = await row.locator('.CodeMirror-line').first().textContent();\n      await expect(value).toContain('newExistingEnvDisabledValue');\n    });\n\n    await test.step('Verify that a new env variable \"newEnv\" is added by the pre-request script to the global environment', async () => {\n      const row = page.locator('tbody tr').filter({ has: page.locator('input[value=\"newEnv\"]') });\n      const value = await row.locator('.CodeMirror-line').first().textContent();\n      await expect(value).toContain('newEnvValue');\n    });\n\n    await test.step('Verify that the value of \"baseUrl\" is unchanged.', async () => {\n      const row = page.locator('tbody tr').filter({ has: page.locator('input[value=\"baseUrl\"]') });\n      const value = await row.locator('.CodeMirror-line').first().textContent();\n      await expect(value).toContain('https://echo.usebruno.com');\n    });\n\n    await test.step('Close the global environment config tab.', async () => {\n      const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });\n      await envTab.hover();\n      await envTab.getByTestId('request-tab-close-icon').click({ force: true });\n    });\n  });\n});\n"
  },
  {
    "path": "tests/environments/update-global-environment-via-script/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{projectRoot}}/tests/environments/update-global-environment-via-script/fixtures/collection\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/environments/update-global-environment-via-script/init-user-data/global-environments.json",
    "content": "{\n\t\"environments\": [\n\t\t{\n\t\t\t\"uid\": \"RrPsTcwRnHMv3yljQO3ex\",\n\t\t\t\"name\": \"global\",\n\t\t\t\"variables\": [\n\t\t\t\t{\n\t\t\t\t\t\"uid\": \"VXKOZdkYw0DyI4mlhn6Wr\",\n\t\t\t\t\t\"name\": \"baseUrl\",\n\t\t\t\t\t\"value\": \"https://echo.usebruno.com\",\n\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\"secret\": false,\n\t\t\t\t\t\"enabled\": true\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"uid\": \"NTwrSscXsaeh4uee6ocJN\",\n\t\t\t\t\t\"name\": \"existingEnvEnabled\",\n\t\t\t\t\t\"value\": \"existingEnvEnabledValue\",\n\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\"secret\": false,\n\t\t\t\t\t\"enabled\": true\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"uid\": \"PCsUccFm4pktVowXEKRvw\",\n\t\t\t\t\t\"name\": \"existingEnvDisabled\",\n\t\t\t\t\t\"value\": \"existingEnvDisabledValue\",\n\t\t\t\t\t\"type\": \"text\",\n\t\t\t\t\t\"secret\": false,\n\t\t\t\t\t\"enabled\": false\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t],\n\t\"activeGlobalEnvironmentUid\": \"RrPsTcwRnHMv3yljQO3ex\"\n}"
  },
  {
    "path": "tests/environments/update-global-environment-via-script/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/environments/update-global-environment-via-script/fixtures/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/footer/notifications/notifications.spec.js",
    "content": "import { test, expect } from '../../../playwright';\n\ntest.describe('Notifications Modal', () => {\n  test('should open notifications modal when clicking bell icon and close with close button', async ({ page }) => {\n    // Get the notification bell icon in the status bar\n    const notificationBell = page.getByLabel('Check all Notifications');\n\n    // Click on the bell icon to open notifications\n    await notificationBell.click();\n\n    // Get modal elements\n    const notificationsModal = page.locator('.bruno-modal');\n    const modalCloseButton = notificationsModal.locator('div.bruno-modal-header div.close');\n\n    // Verify modal is visible and has the correct title\n    await expect(notificationsModal).toBeVisible();\n    await expect(notificationsModal.locator('.bruno-modal-header-title')).toContainText('NOTIFICATIONS');\n\n    // Click the close button\n    await modalCloseButton.click();\n\n    // Verify modal is closed\n    await expect(notificationsModal).not.toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/footer/sidebar-toggle/sidebar-toggle.spec.js",
    "content": "import { test, expect } from '../../../playwright';\n\ntest.describe('Sidebar Toggle', () => {\n  test('should toggle sidebar visibility when clicking the toggle button', async ({ page }) => {\n    // Get the sidebar and toggle button elements\n    const sidebar = page.locator('aside.sidebar');\n    const toggleButton = page.getByTestId('toggle-sidebar-button');\n    const dragHandle = page.locator('.sidebar-drag-handle');\n\n    // Initial state - sidebar and drag handle should be visible\n    await expect(sidebar).toBeVisible();\n    await expect(dragHandle).toBeVisible();\n\n    // Click toggle to hide sidebar\n    await toggleButton.click();\n\n    // Wait for transition to complete and verify sidebar and drag handle are hidden\n    await expect(sidebar).not.toBeVisible();\n    await expect(dragHandle).not.toBeVisible();\n\n    // Verify the sidebar has collapsed width\n    const sidebarBox = await sidebar.boundingBox();\n    expect(sidebarBox?.width).toBe(0);\n\n    // Click toggle again to show sidebar\n    await toggleButton.click();\n\n    // Wait for transition and verify sidebar and drag handle are visible again\n    await expect(sidebar).toBeVisible();\n    await expect(dragHandle).toBeVisible();\n\n    // Verify the sidebar has expanded width\n    const expandedSidebarBox = await sidebar.boundingBox();\n    expect(expandedSidebarBox?.width).toBeGreaterThan(0);\n  });\n});\n"
  },
  {
    "path": "tests/global-environments/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"global-env-non-string\",\n  \"type\": \"collection\"\n}\n\n"
  },
  {
    "path": "tests/global-environments/collection/set-global-nonstring.bru",
    "content": "meta {\n  name: set-global-nonstring\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://example.com\n  body: none\n  auth: none\n}\n\nscript:post-response {\n  bru.setGlobalEnvVar('numericVar', 170001);\n  bru.setGlobalEnvVar('booleanVar', true);\n}\n"
  },
  {
    "path": "tests/global-environments/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/global-environments/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/global-environments/non-string-values.spec.ts",
    "content": "import { test, expect } from '../../playwright';\nimport { openCollection, closeAllCollections, sendRequest, addEnvironmentVariables } from '../utils/page';\nimport { buildCommonLocators } from '../utils/page/locators';\n\ntest.describe('Global Environment Variables - Non-string Values', () => {\n  test.afterEach(async ({ pageWithUserData: page }) => {\n    // Cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  test('should seed non-string globals via request and verify read-only + tooltip', async ({\n    pageWithUserData: page\n  }) => {\n    await openCollection(page, 'global-env-non-string');\n\n    await test.step('Create a new global environment with a string variable', async () => {\n      await page.getByTestId('environment-selector-trigger').click();\n      await page.getByTestId('env-tab-global').click();\n\n      // Create a new global environment\n      await page.getByRole('button', { name: 'Create' }).click();\n      await page.locator('#environment-name').click();\n      await page.locator('#environment-name').fill('Test Env');\n      await page.getByRole('button', { name: 'Create', exact: true }).click();\n\n      const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });\n      await expect(envTab).toBeVisible();\n\n      await addEnvironmentVariables(page, [\n        { name: 'stringVar', value: 'hello world' },\n        { name: 'numericVar', value: '170001' },\n        { name: 'booleanVar', value: 'true' }\n      ]);\n\n      await page.getByTestId('save-env').click();\n\n      await envTab.hover();\n      await envTab.getByTestId('request-tab-close-icon').click({ force: true });\n    });\n\n    // Request contains a script that sets the non-string global variables.\n    await test.step('Run the request to seed non-string global variables via post-script', async () => {\n      const locators = buildCommonLocators(page);\n      await locators.sidebar.request('set-global-nonstring').click();\n      await sendRequest(page, 200);\n    });\n\n    await test.step('Re-open Global Environments to see the seeded variables', async () => {\n      await page.getByTestId('environment-selector-trigger').click();\n      await page.getByTestId('env-tab-global').click();\n      await page.getByRole('button', { name: 'Configure' }).click();\n\n      const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });\n      await expect(envTab).toBeVisible();\n    });\n\n    const numericInput = page.locator('input[value=\"numericVar\"]');\n    const booleanInput = page.locator('input[value=\"booleanVar\"]');\n    await expect(numericInput).toBeVisible();\n    await expect(booleanInput).toBeVisible();\n    const numericRow = numericInput.locator('xpath=ancestor::tr');\n    const booleanRow = booleanInput.locator('xpath=ancestor::tr');\n\n    await test.step('Verify that numericVar is read-only with tooltip', async () => {\n      // This value is set via a post-script (not user input). We verify that attempts to edit the input do not change the value, proving it is read-only in the UI.\n\n      // Verify the script-set value is rendered.\n      await expect(numericRow.locator('.CodeMirror-line').first()).toContainText(/170001/);\n\n      // Verify that typing into the input does not mutate the value.\n      await numericRow.locator('.CodeMirror').click();\n      await page.keyboard.type('999');\n      await expect(numericRow.locator('.CodeMirror-line').first()).toContainText(/170001/);\n\n      const infoIcon = numericRow.locator('[id$=\"-disabled-info-icon\"]').nth(0);\n      await infoIcon.hover();\n\n      // The tooltip explains why the field is locked.\n      const tooltip = page.locator('[role=\"tooltip\"], .react-tooltip');\n      await expect(tooltip.first()).toBeVisible();\n      await expect(tooltip.first()).toContainText('Non-string values set via scripts are read-only and can only be updated through scripts.');\n\n      // Hovering outside the tooltip should hide it.\n      await page.mouse.move(0, 0);\n      await expect(tooltip.first()).not.toBeVisible();\n\n      // Clicking the info icon reveals the tooltip.\n      await infoIcon.click();\n      await expect(tooltip.first()).toBeVisible();\n      await expect(tooltip.first()).toContainText('Non-string values set via scripts are read-only and can only be updated through scripts.');\n    });\n\n    await test.step('Verify that booleanVar is read-only with tooltip', async () => {\n      // This value is also set via post-script.\n      await expect(booleanRow.locator('.CodeMirror-line').first()).toContainText(/true/);\n\n      // Verify that typing into the input does not mutate the value.\n      await booleanRow.locator('.CodeMirror').click();\n      await page.keyboard.type('false');\n      await expect(booleanRow.locator('.CodeMirror-line').first()).toContainText(/true/);\n\n      const infoIcon = booleanRow.locator('[id$=\"-disabled-info-icon\"]').nth(0);\n      await infoIcon.hover();\n\n      // The tooltip explains why the field is locked.\n      const tooltip = page.locator('[role=\"tooltip\"], .react-tooltip');\n      await expect(tooltip.first()).toBeVisible();\n      await expect(tooltip.first()).toContainText('Non-string values set via scripts are read-only and can only be updated through scripts.');\n    });\n\n    await test.step('Verify that stringVar remains editable', async () => {\n      const stringInput = page.locator('input[value=\"stringVar\"]');\n      await expect(stringInput).toBeVisible();\n      const stringRow = stringInput.locator('xpath=ancestor::tr');\n\n      await expect(stringRow.locator('.CodeMirror-line').first()).toContainText('hello world');\n      await stringRow.locator('.CodeMirror').click();\n      await page.keyboard.type(' updated');\n\n      // Verify the user edit persists in the UI.\n      await expect(stringRow.locator('.CodeMirror-line').first()).toContainText('hello world updated');\n\n      await page.getByTestId('save-env').click();\n\n      const envTab = page.locator('.request-tab').filter({ hasText: 'Global Environments' });\n      await envTab.hover();\n      await envTab.getByTestId('request-tab-close-icon').click({ force: true });\n    });\n  });\n});\n"
  },
  {
    "path": "tests/grpc/make-request/fixtures/collection/HelloService/BidiHello.bru",
    "content": "meta {\n  name: BidiHello\n  type: grpc\n  seq: 4\n}\n\ngrpc {\n  url: {{host}}\n  method: /hello.HelloService/BidiHello\n  body: grpc\n  auth: inherit\n  methodType: bidi-streaming\n}\n\nbody:grpc {\n  name: message 1\n  content: '''\n    {\n      \"greeting\": \"cuius\"\n    }\n  '''\n}\n\nbody:grpc {\n  name: message 2\n  content: '''\n    {\n      \"greeting\": \"adfectus\"\n    }\n  '''\n}\n"
  },
  {
    "path": "tests/grpc/make-request/fixtures/collection/HelloService/LotOfGreetings.bru",
    "content": "meta {\n  name: LotOfGreetings\n  type: grpc\n  seq: 3\n}\n\ngrpc {\n  url: {{host}}\n  method: /hello.HelloService/LotsOfGreetings\n  body: grpc\n  auth: inherit\n  methodType: client-streaming\n}\n\nbody:grpc {\n  name: message 1\n  content: '''\n    {\n      \"greeting\": \"sortitus\"\n    }\n  '''\n}\n\nbody:grpc {\n  name: message 2\n  content: '''\n    {\n      \"greeting\": \"porro\"\n    }\n  '''\n}\n"
  },
  {
    "path": "tests/grpc/make-request/fixtures/collection/HelloService/LotOfReplies.bru",
    "content": "meta {\n  name: LotOfReplies\n  type: grpc\n  seq: 2\n}\n\ngrpc {\n  url: {{host}}\n  method: /hello.HelloService/LotsOfReplies\n  body: grpc\n  auth: inherit\n  methodType: server-streaming\n}\n\nbody:grpc {\n  name: message 1\n  content: '''\n    {\n      \"greeting\": \"suadeo\"\n    }\n  '''\n}\n"
  },
  {
    "path": "tests/grpc/make-request/fixtures/collection/HelloService/SayHello.bru",
    "content": "meta {\n  name: SayHello\n  type: grpc\n  seq: 1\n}\n\ngrpc {\n  url: {{host}}\n  method: /hello.HelloService/SayHello\n  body: grpc\n  auth: inherit\n  methodType: unary\n}\n\nbody:grpc {\n  name: message 1\n  content: '''\n    {\n      \"greeting\": \"amoveo\"\n    }\n  '''\n}\n"
  },
  {
    "path": "tests/grpc/make-request/fixtures/collection/HelloService/folder.bru",
    "content": "meta {\n  name: HelloService\n  seq: 2\n}\n\nauth {\n  mode: inherit\n}\n"
  },
  {
    "path": "tests/grpc/make-request/fixtures/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"Grpcbin\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ],\n  \"size\": 0.001827239990234375,\n  \"filesCount\": 10,\n  \"protobuf\": {\n    \"protoFiles\": [\n      {\n        \"path\": \"../protos/services/product.proto\",\n        \"type\": \"file\"\n      },\n      {\n        \"path\": \"../protos/services/order.proto\",\n        \"type\": \"file\"\n      }\n    ],\n    \"importPaths\": [\n      {\n        \"path\": \"../protos/types\",\n        \"enabled\": false\n      },\n      {\n        \"path\": \".\",\n        \"enabled\": true\n      }\n    ]\n  }\n}"
  },
  {
    "path": "tests/grpc/make-request/fixtures/collection/collection.bru",
    "content": ""
  },
  {
    "path": "tests/grpc/make-request/fixtures/collection/environments/Env.bru",
    "content": "vars {\n  host: grpc://grpcb.in:9000\n}\n"
  },
  {
    "path": "tests/grpc/make-request/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{projectRoot}}/tests/grpc/make-request/fixtures/collection\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "tests/grpc/make-request/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/grpc/make-request/fixtures/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    },\n    \"beta\": {\n      \"nodevm\": false\n    }\n  }\n}\n"
  },
  {
    "path": "tests/grpc/make-request/make-request.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { buildGrpcCommonLocators } from '../../utils/page/locators';\n\nconst saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';\n\ntest.describe('make grpc requests', () => {\n  const setupGrpcTest = async (page) => {\n    const locators = buildGrpcCommonLocators(page);\n\n    await test.step('navigate to gRPC collection', async () => {\n      await locators.sidebar.collection('Grpcbin').click();\n      await locators.sidebar.folder('HelloService').click();\n    });\n\n    await test.step('select environment', async () => {\n      await locators.environment.selector().click();\n      await locators.environment.collectionTab().click();\n      await locators.environment.envOption('Env').click();\n    });\n  };\n\n  test('make unary request', async ({ pageWithUserData: page }) => {\n    await setupGrpcTest(page);\n    const locators = buildGrpcCommonLocators(page);\n\n    await test.step('select unary method', async () => {\n      await locators.sidebar.request('SayHello').click();\n      await expect(locators.method.dropdownTrigger()).toContainText('HelloService/SayHello', { timeout: 30000 });\n    });\n\n    await test.step('verify gRPC unary request is opened successfully', async () => {\n      await expect(locators.method.indicator()).toContainText('gRPC');\n      await expect(locators.request.queryUrlContainer().locator('.CodeMirror')).toBeVisible();\n      await expect(locators.request.sendButton()).toBeVisible();\n      await expect(locators.request.messagesContainer()).toBeVisible();\n    });\n\n    await test.step('send request', async () => {\n      await locators.request.sendButton().click();\n      await expect(locators.response.statusCode()).toBeVisible({ timeout: 2000 });\n      await expect(locators.response.statusText()).toBeVisible({ timeout: 2000 });\n      await expect(locators.response.statusCode()).toHaveText(/0/);\n      await expect(locators.response.statusText()).toHaveText(/OK/);\n    });\n\n    await test.step('verify response message count', async () => {\n      await expect(locators.response.tabCount()).toHaveText('1');\n    });\n\n    await test.step('verify response items are rendered', async () => {\n      await expect(locators.response.content()).toBeVisible();\n      await expect(locators.response.container()).toBeVisible();\n      await expect(locators.response.singleResponse()).toBeVisible();\n    });\n\n    /* TODO: Reflection fetching incorrectly marks requests as modified, causing save indicators to appear. This save step prevents test timeouts by clearing the modified state. This is a temporary workaround until the reflection fetching issue is resolved. */\n    await test.step('save request via shortcut', async () => {\n      await page.keyboard.press(saveShortcut);\n    });\n  });\n\n  test('make server streaming request', async ({ pageWithUserData: page }) => {\n    await setupGrpcTest(page);\n    const locators = buildGrpcCommonLocators(page);\n\n    await test.step('select server streaming method', async () => {\n      await locators.sidebar.request('LotOfReplies').click();\n      await expect(locators.method.dropdownTrigger()).toContainText('HelloService/LotsOfReplies');\n    });\n\n    await test.step('verify gRPC server streaming request is opened successfully', async () => {\n      await expect(locators.method.indicator()).toContainText('gRPC');\n      await expect(locators.request.queryUrlContainer().locator('.CodeMirror')).toBeVisible();\n      await expect(locators.request.messagesContainer()).toBeVisible();\n      await expect(locators.request.sendButton()).toBeVisible();\n    });\n\n    await test.step('send request', async () => {\n      await locators.request.sendButton().click();\n      await expect(locators.response.statusCode()).toBeVisible({ timeout: 2000 });\n      await expect(locators.response.statusText()).toBeVisible({ timeout: 2000 });\n      await expect(locators.response.statusCode()).toHaveText(/0/);\n      await expect(locators.response.statusText()).toHaveText(/OK/);\n    });\n\n    await test.step('verify response message count', async () => {\n      await expect(locators.response.tabCount()).toHaveText('10');\n    });\n\n    await test.step('verify response items are rendered', async () => {\n      await expect(locators.response.content()).toBeVisible();\n      await expect(locators.response.container()).toBeVisible();\n      await expect(locators.response.list()).toBeVisible();\n      await expect(locators.response.responseItems()).toHaveCount(10);\n    });\n\n    /* TODO: Reflection fetching incorrectly marks requests as modified, causing save indicators to appear. This save step prevents test timeouts by clearing the modified state. This is a temporary workaround until the reflection fetching issue is resolved. */\n    await test.step('save request via shortcut', async () => {\n      await page.keyboard.press(saveShortcut);\n    });\n  });\n\n  test('make client streaming request', async ({ pageWithUserData: page }) => {\n    await setupGrpcTest(page);\n    const locators = buildGrpcCommonLocators(page);\n\n    await test.step('select client streaming method', async () => {\n      await locators.sidebar.request('LotOfGreetings').click();\n      await expect(locators.method.dropdownTrigger()).toContainText('HelloService/LotsOfGreetings');\n    });\n\n    await test.step('verify gRPC client streaming request is opened successfully', async () => {\n      await expect(locators.request.queryUrlContainer().locator('.CodeMirror')).toBeVisible();\n      await expect(locators.request.messagesContainer()).toBeVisible();\n      await expect(locators.request.addMessageButton()).toBeVisible();\n      await expect(locators.request.sendMessage(0)).toBeVisible();\n      await expect(locators.request.sendButton()).toBeVisible();\n    });\n\n    await test.step('start client streaming connection', async () => {\n      await locators.request.sendButton().click();\n      await expect(locators.request.endConnectionButton()).toBeVisible();\n    });\n\n    await test.step('send individual message', async () => {\n      await locators.request.sendMessage(0).click();\n    });\n\n    await test.step('end client streaming connection', async () => {\n      await locators.request.endConnectionButton().click();\n      await expect(locators.response.statusCode()).toBeVisible({ timeout: 2000 });\n      await expect(locators.response.statusText()).toBeVisible({ timeout: 2000 });\n      await expect(locators.response.statusCode()).toHaveText(/0/);\n      await expect(locators.response.statusText()).toHaveText(/OK/);\n    });\n\n    await test.step('verify response message count', async () => {\n      await expect(locators.response.tabCount()).toHaveText('1');\n    });\n\n    await test.step('verify response items are rendered', async () => {\n      await expect(locators.response.content()).toBeVisible();\n      await expect(locators.response.container()).toBeVisible();\n      await expect(locators.response.singleResponse()).toBeVisible();\n    });\n\n    /* TODO: Reflection fetching incorrectly marks requests as modified, causing save indicators to appear. This save step prevents test timeouts by clearing the modified state. This is a temporary workaround until the reflection fetching issue is resolved. */\n    await test.step('save request via shortcut', async () => {\n      await page.keyboard.press(saveShortcut);\n    });\n  });\n\n  test('make bidi streaming request', async ({ pageWithUserData: page }) => {\n    await setupGrpcTest(page);\n    const locators = buildGrpcCommonLocators(page);\n\n    await test.step('select bidirectional streaming method', async () => {\n      await locators.sidebar.request('BidiHello').click();\n      await expect(locators.method.dropdownTrigger()).toContainText('HelloService/BidiHello');\n    });\n\n    await test.step('verify gRPC bidi streaming request is opened successfully', async () => {\n      await expect(locators.request.queryUrlContainer().locator('.CodeMirror')).toBeVisible();\n      await expect(locators.request.messagesContainer()).toBeVisible();\n      await expect(locators.request.addMessageButton()).toBeVisible();\n      await expect(locators.request.sendMessage(0)).toBeVisible();\n      await expect(locators.request.sendButton()).toBeVisible();\n    });\n\n    await test.step('start bidirectional streaming connection', async () => {\n      await locators.request.sendButton().click();\n      await expect(locators.request.endConnectionButton()).toBeVisible();\n    });\n\n    await test.step('send individual message', async () => {\n      await locators.request.sendMessage(0).click();\n      await locators.request.sendMessage(1).click();\n    });\n\n    await test.step('end bidirectional streaming connection', async () => {\n      await locators.request.endConnectionButton().click();\n      await expect(locators.response.statusCode()).toBeVisible({ timeout: 2000 });\n      await expect(locators.response.statusText()).toBeVisible({ timeout: 2000 });\n      await expect(locators.response.statusCode()).toHaveText(/0/);\n      await expect(locators.response.statusText()).toHaveText(/OK/);\n    });\n\n    await test.step('verify response message count', async () => {\n      await expect(locators.response.tabCount()).toHaveText('2');\n    });\n\n    await test.step('verify response items are rendered', async () => {\n      await expect(locators.response.content()).toBeVisible();\n      await expect(locators.response.container()).toBeVisible();\n      await expect(locators.response.list()).toBeVisible();\n      await expect(locators.response.responseItems()).toHaveCount(2);\n    });\n\n    /* TODO: Reflection fetching incorrectly marks requests as modified, causing save indicators to appear. This save step prevents test timeouts by clearing the modified state. This is a temporary workaround until the reflection fetching issue is resolved. */\n    await test.step('save request via shortcut', async () => {\n      await page.keyboard.press(saveShortcut);\n    });\n  });\n});\n"
  },
  {
    "path": "tests/grpc/metadata/fixtures/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"Grpcbin\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}"
  },
  {
    "path": "tests/grpc/metadata/fixtures/collection/collection.bru",
    "content": ""
  },
  {
    "path": "tests/grpc/metadata/fixtures/collection/sayHello.bru",
    "content": "meta {\n  name: SayHello\n  type: grpc\n  seq: 1\n}\n\ngrpc {\n  url: grpc://grpcb.in:9000\n  method: /hello.HelloService/SayHello\n  body: grpc\n  auth: inherit\n  methodType: unary\n}\n\nmetadata {\n  test-bin: hello\n  test: hello\n}\n\nbody:grpc {\n  name: message 1\n  content: '''\n    {\n      \"greeting\": \"amoveo\"\n    }\n  '''\n}\n"
  },
  {
    "path": "tests/grpc/metadata/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{projectRoot}}/tests/grpc/metadata/fixtures/collection\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "tests/grpc/metadata/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/grpc/metadata/fixtures/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    },\n    \"beta\": {\n      \"nodevm\": false\n    }\n  }\n}\n"
  },
  {
    "path": "tests/grpc/metadata/with-bin-metadata.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections } from '../../utils/page';\n\ntest.describe('grpc metadata', () => {\n  test.afterAll(async ({ pageWithUserData: page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('should handle binary metadata', async ({ pageWithUserData: page }) => {\n    await test.step('Open the request', async () => {\n      const collection = page.locator('#sidebar-collection-name').filter({ hasText: 'Grpcbin' });\n      await collection.click();\n      const request = page.locator('.collection-item-name').filter({ hasText: 'SayHello' });\n      await request.click();\n    });\n\n    await test.step('Verify request sent successfully', async () => {\n      await page.getByTestId('grpc-send-request-button').click();\n      const statusCode = page.getByTestId('grpc-response-status-code');\n      const statusText = page.getByTestId('grpc-response-status-text');\n      await expect(statusCode).toBeVisible({ timeout: 30000 });\n      await expect(statusText).toBeVisible({ timeout: 30000 });\n      await expect(statusCode).toHaveText(/0/);\n      await expect(statusText).toHaveText(/OK/);\n    });\n\n    /* TODO: Reflection fetching incorrectly marks requests as modified, causing save indicators to appear. This save step prevents test timeouts by clearing the modified state. This is a temporary workaround until the reflection fetching issue is resolved. */\n    await test.step('save request via shortcut', async () => {\n      const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';\n      await page.keyboard.press(saveShortcut);\n    });\n  });\n});\n"
  },
  {
    "path": "tests/grpc/method-search/fixtures/grpc-collection/SayHello.bru",
    "content": "meta {\n  name: SayHello\n  type: grpc\n  seq: 1\n}\n\ngrpc {\n  url: {{host}}\n  method: /hello.HelloService/SayHello\n  body: grpc\n  auth: inherit\n  methodType: unary\n}\n\nbody:grpc {\n  name: message 1\n  content: '''\n  {\n    \"greeting\": \"supra\"\n  }\n  '''\n}\n"
  },
  {
    "path": "tests/grpc/method-search/fixtures/grpc-collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"grpc-collection\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ],\n  \"size\": 0,\n  \"filesCount\": 1\n}"
  },
  {
    "path": "tests/grpc/method-search/fixtures/grpc-collection/environments/GrpcEnv.bru",
    "content": "vars {\n  host: grpc://grpcb.in:9000\n}\n"
  },
  {
    "path": "tests/grpc/method-search/grpc-method-search.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\n\ntest.describe('Grpc Collection - Method Search Functionality', () => {\n  test.beforeAll(async ({ pageWithUserData: page }) => {\n    await test.step('Open the grpc-collection in the sidebar', async () => {\n      await page.locator('#sidebar-collection-name').filter({ hasText: 'grpc-collection' }).click();\n    });\n\n    await test.step('Switch to GrpcEnv environment', async () => {\n      await page.locator('div.current-environment').click();\n      await page.getByText('GrpcEnv').click();\n      await expect(page.locator('.current-environment').filter({ hasText: /GrpcEnv/ })).toBeVisible();\n    });\n  });\n\n  test.afterEach(async ({ pageWithUserData: page }) => {\n    await test.step('Close the gRPC sayHello tab without saving changes', async () => {\n      await page.getByRole('tab', { name: 'gRPC sayHello' }).getByTestId('request-tab-close-icon').click({ force: true });\n      await page.getByRole('button', { name: 'Don\\'t Save' }).click();\n    });\n  });\n\n  test('Search for grpc methods using the search input', async ({ pageWithUserData: page }) => {\n    await test.step('Select SayHello request', async () => {\n      await page.getByText('SayHello').click();\n    });\n\n    await test.step('Wait for gRPC query URL container to be visible', async () => {\n      const grpcQueryUrlContainer = page.getByTestId('grpc-query-url-container');\n      await grpcQueryUrlContainer.waitFor({ state: 'visible' });\n    });\n\n    await test.step('Refresh gRPC methods and open methods dropdown', async () => {\n      await page.getByTestId('refresh-methods-icon').click();\n      const grpcMethodsDropdown = page.getByTestId('grpc-methods-dropdown');\n      await grpcMethodsDropdown.click();\n    });\n\n    await test.step('Search the term \"Loojup\" and verify the \"Lookup\" grpc method is visible and select it', async () => {\n      await page.getByTestId('grpc-methods-search-input').fill('loojup');\n      const method = page.getByTestId('grpc-method-item').filter({ hasText: 'Lookup' });\n      await expect(method).toBeVisible();\n      await method.click();\n    });\n\n    await test.step('Verify the \"Lookup\" grpc method is selected', async () => {\n      const method = page.getByTestId('selected-grpc-method-name').filter({ hasText: 'Lookup' });\n      await expect(method).toBeVisible();\n    });\n  });\n\n  test('Search for grpc methods using the keyboard', async ({ pageWithUserData: page }) => {\n    await test.step('Select SayHello request', async () => {\n      await page.getByText('SayHello').click();\n    });\n\n    await test.step('Wait for gRPC query URL container to be visible', async () => {\n      const grpcQueryUrlContainer = page.getByTestId('grpc-query-url-container');\n      await grpcQueryUrlContainer.waitFor({ state: 'visible' });\n    });\n\n    await test.step('Refresh gRPC methods and open methods dropdown', async () => {\n      await page.getByTestId('refresh-methods-icon').click();\n      const grpcMethodsDropdown = page.getByTestId('grpc-methods-dropdown');\n      await grpcMethodsDropdown.click();\n    });\n\n    await test.step('Use keyboard to navigate to \"Sum\" method and select it', async () => {\n      await page.keyboard.press('ArrowDown');\n      await page.keyboard.press('Enter');\n    });\n\n    await test.step('Verify the \"Sum\" grpc method is selected', async () => {\n      const method = page.getByTestId('selected-grpc-method-name').filter({ hasText: 'Add/Sum' });\n      await expect(method).toBeVisible();\n    });\n  });\n});\n"
  },
  {
    "path": "tests/grpc/method-search/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{projectRoot}}/tests/grpc/method-search/fixtures/grpc-collection\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "tests/grpc/method-search/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/grpc/method-search/fixtures/grpc-collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/import/bruno/fixtures/bruno-invalid-corrupted.json",
    "content": "{\n  \"version\": \"1\",\n  \"uid\": \"corrupted_bruno_collection\",\n  \"name\": \"Corrupted Bruno Collection\",\n  \"items\": [\n    {\n      \"uid\": \"corrupted_request\",\n      \"type\": \"invalid-request-type\",\n      \"name\": \"Invalid Request Type\",\n      \"seq\": 1,\n      \"request\": {\n        \"url\": \"https://example.com/api\",\n        \"method\": \"INVALID_METHOD\",\n        \"headers\": \"this should be an array not a string\",\n        \"params\": null,\n        \"body\": {\n          \"mode\": \"invalid-mode\",\n          \"invalidField\": \"this field doesn't exist in schema\"\n        },\n        \"auth\": {\n          \"mode\": \"unknown-auth-type\",\n          \"invalidAuth\": {\n            \"badField\": \"invalid value\"\n          }\n        },\n        \"script\": \"this should be an object not a string\",\n        \"vars\": \"this should be an object not a string\",\n        \"assertions\": \"this should be an array not a string\",\n        \"tests\": 12345,\n        \"docs\": true\n      }\n    },\n    {\n      \"uid\": \"missing_required_fields\",\n      \"type\": \"http-request\",\n      \"name\": \"Missing Required Fields\",\n      \"seq\": 2\n    }\n  ],\n  \"environments\": [\n    {\n      \"uid\": \"invalid_env\",\n      \"name\": \"Invalid Environment\",\n      \"variables\": \"this should be an array not a string\"\n    }\n  ],\n  \"activeEnvironmentUid\": \"non_existent_environment_id\",\n  \"root\": {\n    \"request\": {\n      \"headers\": \"invalid headers format\",\n      \"auth\": {\n        \"mode\": \"completely-unknown-auth\"\n      },\n      \"script\": 42,\n      \"vars\": false,\n      \"tests\": null\n    }\n  },\n  \"invalidTopLevelField\": \"this field doesn't belong here\",\n  \"brunoConfig\": {\n    \"version\": \"999\",\n    \"name\": \"Invalid Config\",\n    \"type\": \"invalid-type\",\n    \"invalidConfigField\": true\n  }\n}\n"
  },
  {
    "path": "tests/import/bruno/fixtures/bruno-malformed.json",
    "content": "{\n  \"version\": \"1\",\n  \"uid\": \"malformed_bruno_collection\",\n  \"name\": \"Malformed Bruno Collection\",\n  \"items\": [\n    {\n      \"uid\": \"malformed_request\",\n      \"type\": \"http-request\",\n      \"name\": \"Malformed Request\",\n      \"seq\": 1,\n      \"request\": {\n        \"url\": \"https://example.com/api\",\n        \"method\": \"GET\",\n        \"headers\": [],\n        \"params\": [],\n        \"body\": {\n          \"mode\": \"none\"\n        },\n        \"auth\": {\n          \"mode\": \"none\"\n        },\n        \"script\": {},\n        \"vars\": {},\n        \"assertions\": [],\n        \"tests\": \"\",\n        \"docs\": \"\"\n      }\n    }\n  ],\n  \"environments\": [],\n  \"activeEnvironmentUid\": null,\n  \"root\": {\n    \"request\": {\n      \"headers\": [],\n      \"auth\": {\n        \"mode\": \"none\"\n      },\n      \"script\": {},\n      \"vars\": {},\n      \"tests\": \"\"\n    }\n  }\n  // Missing comma and closing bracket - this makes it malformed JSON"
  },
  {
    "path": "tests/import/bruno/fixtures/bruno-missing-required-fields.json",
    "content": "{\n  \"name\": \"bruno-testbench\",\n  \"items\": [\n    {\n      \"type\": \"http\",\n      \"name\": \"aaaaa\",\n      \"seq\": 2,\n      \"request\": {\n        \"url\": \"https://reqres.in/api/users/1\",\n        \"method\": \"PUT\",\n        \"headers\": [\n          {\n            \"name\": \"Accept\",\n            \"value\": \"application/json\",\n            \"enabled\": true\n          },\n          {\n            \"name\": \"Cookie\",\n            \"value\": \"session-id=abc123\",\n            \"enabled\": true\n          }\n        ],\n        \"params\": [],\n        \"body\": {\n          \"mode\": \"none\",\n          \"formUrlEncoded\": [],\n          \"multipartForm\": [],\n          \"file\": []\n        },\n        \"script\": {\n          \"req\": \"console.log(req.getCookie());\"\n        },\n        \"vars\": {},\n        \"assertions\": [],\n        \"tests\": \"\",\n        \"docs\": \"\",\n        \"auth\": {\n          \"mode\": \"none\"\n        }\n      }\n    },\n    {\n      \"type\": \"folder\",\n      \"name\": \"auth\",\n      \"root\": {\n        \"meta\": {\n          \"name\": \"auth\"\n        }\n      },\n      \"items\": [\n        {\n          \"type\": \"folder\",\n          \"name\": \"basic\",\n          \"root\": {\n            \"meta\": {\n              \"name\": \"basic\"\n            }\n          },\n          \"items\": [\n            {\n              \"type\": \"folder\",\n              \"name\": \"via auth\",\n              \"root\": {\n                \"meta\": {\n                  \"name\": \"via auth\"\n                }\n              },\n              \"items\": [\n                {\n                  \"type\": \"http\",\n                  \"name\": \"Basic Auth 200\",\n                  \"seq\": 1,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/auth/basic/protected\",\n                    \"method\": \"POST\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"json\",\n                      \"json\": \"\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"200\",\n                        \"enabled\": true,\n                        \"uid\": \"YLpcflD1RLvdkSSvAYimh\"\n                      },\n                      {\n                        \"name\": \"res.body.message\",\n                        \"value\": \"Authentication successful\",\n                        \"enabled\": true,\n                        \"uid\": \"oqRPDS5d7CLIqBQ5OCEko\"\n                      }\n                    ],\n                    \"tests\": \"\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"basic\",\n                      \"basic\": {\n                        \"username\": \"bruno\",\n                        \"password\": \"{{basic_auth_password}}\"\n                      }\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"Basic Auth 400\",\n                  \"seq\": 2,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/auth/basic/protected\",\n                    \"method\": \"POST\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"json\",\n                      \"json\": \"\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"401\",\n                        \"enabled\": true,\n                        \"uid\": \"WsBvjaJuowT05ri9A8Qc5\"\n                      },\n                      {\n                        \"name\": \"res.body\",\n                        \"value\": \"Unauthorized\",\n                        \"enabled\": true,\n                        \"uid\": \"VW1wyd6hu74Yyfzhn0RuQ\"\n                      }\n                    ],\n                    \"tests\": \"\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                }\n              ]\n            },\n            {\n              \"type\": \"folder\",\n              \"name\": \"via script\",\n              \"root\": {\n                \"meta\": {\n                  \"name\": \"via script\"\n                }\n              },\n              \"items\": [\n                {\n                  \"type\": \"http\",\n                  \"name\": \"Basic Auth 200\",\n                  \"seq\": 1,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/auth/basic/protected\",\n                    \"method\": \"POST\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"json\",\n                      \"json\": \"\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"const username = \\\"bruno\\\";\\nconst password = \\\"della\\\";\\n\\nconst authString = `${username}:${password}`;\\nconst encodedAuthString = require('btoa')(authString);\\n\\nreq.setHeader(\\\"Authorization\\\", `Basic ${encodedAuthString}`);\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"5p6vUUMuLxbA7KnYnXrkT\"\n                      },\n                      {\n                        \"name\": \"res.body.message\",\n                        \"value\": \"Authentication successful\",\n                        \"enabled\": true,\n                        \"uid\": \"Tk43KT6Hyf3h8Jfeyk2XD\"\n                      }\n                    ],\n                    \"tests\": \"\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"Basic Auth 401\",\n                  \"seq\": 2,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/auth/basic/protected\",\n                    \"method\": \"POST\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"json\",\n                      \"json\": \"\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"const username = \\\"bruno\\\";\\nconst password = \\\"invalid\\\";\\n\\nconst authString = `${username}:${password}`;\\nconst encodedAuthString = require('btoa')(authString);\\n\\nreq.setHeader(\\\"Authorization\\\", `Basic ${encodedAuthString}`);\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"401\",\n                        \"enabled\": true,\n                        \"uid\": \"dLnctBQFSISlaooCYzp5C\"\n                      },\n                      {\n                        \"name\": \"res.body\",\n                        \"value\": \"Unauthorized\",\n                        \"enabled\": true,\n                        \"uid\": \"iWPqym01ksxrDV1gfuhWv\"\n                      }\n                    ],\n                    \"tests\": \"\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"type\": \"folder\",\n          \"name\": \"bearer\",\n          \"root\": {\n            \"meta\": {\n              \"name\": \"bearer\"\n            }\n          },\n          \"items\": [\n            {\n              \"type\": \"folder\",\n              \"name\": \"via auth\",\n              \"root\": {\n                \"meta\": {\n                  \"name\": \"via auth\"\n                }\n              },\n              \"items\": [\n                {\n                  \"type\": \"http\",\n                  \"name\": \"Bearer Auth 200\",\n                  \"seq\": 1,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/auth/bearer/protected\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"res\": \"bru.setEnvVar(\\\"foo\\\", \\\"bar\\\");\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"200\",\n                        \"enabled\": true,\n                        \"uid\": \"F01gjRjDDQefuLn2Vcyed\"\n                      },\n                      {\n                        \"name\": \"res.body.message\",\n                        \"value\": \"Authentication successful\",\n                        \"enabled\": true,\n                        \"uid\": \"Hmw3BpVyz9tDBEcdA88O0\"\n                      }\n                    ],\n                    \"tests\": \"\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"bearer\",\n                      \"bearer\": {\n                        \"token\": \"{{bearer_auth_token}}\"\n                      }\n                    }\n                  }\n                }\n              ]\n            },\n            {\n              \"type\": \"folder\",\n              \"name\": \"via headers\",\n              \"root\": {\n                \"meta\": {\n                  \"name\": \"via headers\"\n                }\n              },\n              \"items\": [\n                {\n                  \"type\": \"http\",\n                  \"name\": \"Bearer Auth 200\",\n                  \"seq\": 1,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/auth/bearer/protected\",\n                    \"method\": \"GET\",\n                    \"headers\": [\n                      {\n                        \"name\": \"Authorization\",\n                        \"value\": \"Bearer your_secret_token\",\n                        \"enabled\": true\n                      }\n                    ],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"json\",\n                      \"json\": \"\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"res\": \"bru.setEnvVar(\\\"foo\\\", \\\"bar\\\");\"\n                    },\n                    \"vars\": {\n                      \"req\": [\n                        {\n                          \"name\": \"a-c\",\n                          \"value\": \"foo\",\n                          \"enabled\": true,\n                          \"local\": false\n                        }\n                      ]\n                    },\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"200\",\n                        \"enabled\": true,\n                        \"uid\": \"VDQ7l9zN9WfS3lEGsJ0aw\"\n                      },\n                      {\n                        \"name\": \"res.body.message\",\n                        \"value\": \"Authentication successful\",\n                        \"enabled\": true,\n                        \"uid\": \"0n8UMinkhzOuRJCCABVz9\"\n                      }\n                    ],\n                    \"tests\": \"\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"type\": \"folder\",\n          \"name\": \"cookie\",\n          \"root\": {\n            \"meta\": {\n              \"name\": \"cookie\"\n            }\n          },\n          \"items\": [\n            {\n              \"type\": \"http\",\n              \"name\": \"Check\",\n              \"seq\": 2,\n              \"request\": {\n                \"url\": \"{{host}}/api/auth/cookie/protected\",\n                \"method\": \"GET\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"none\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {},\n                \"vars\": {},\n                \"assertions\": [],\n                \"tests\": \"\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"none\"\n                }\n              }\n            },\n            {\n              \"type\": \"http\",\n              \"name\": \"Login\",\n              \"seq\": 1,\n              \"request\": {\n                \"url\": \"{{host}}/api/auth/cookie/login\",\n                \"method\": \"POST\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"none\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {},\n                \"vars\": {},\n                \"assertions\": [],\n                \"tests\": \"\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"none\"\n                }\n              }\n            }\n          ]\n        },\n        {\n          \"type\": \"folder\",\n          \"name\": \"inherit auth\",\n          \"root\": {\n            \"meta\": {\n              \"name\": \"inherit auth\"\n            }\n          },\n          \"items\": [\n            {\n              \"type\": \"http\",\n              \"name\": \"inherit Bearer Auth 200\",\n              \"seq\": 2,\n              \"request\": {\n                \"url\": \"{{host}}/api/auth/bearer/protected\",\n                \"method\": \"GET\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"none\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {\n                  \"res\": \"bru.setEnvVar(\\\"foo\\\", \\\"bar\\\");\"\n                },\n                \"vars\": {},\n                \"assertions\": [\n                  {\n                    \"name\": \"res.status\",\n                    \"value\": \"200\",\n                    \"enabled\": true,\n                    \"uid\": \"G6vVLMAqfvpa4aBtQ3WSG\"\n                  },\n                  {\n                    \"name\": \"res.body.message\",\n                    \"value\": \"Authentication successful\",\n                    \"enabled\": true,\n                    \"uid\": \"QItzfaevVVrycFox5jTlS\"\n                  }\n                ],\n                \"tests\": \"\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"inherit\"\n                }\n              }\n            }\n          ]\n        }\n      ]\n    },\n    {\n      \"type\": \"folder\",\n      \"name\": \"echo\",\n      \"root\": {\n        \"meta\": {\n          \"name\": \"echo\"\n        }\n      },\n      \"items\": [\n        {\n          \"type\": \"http\",\n          \"name\": \"echo bigint\",\n          \"seq\": 6,\n          \"request\": {\n            \"url\": \"{{host}}/api/echo/json\",\n            \"method\": \"POST\",\n            \"headers\": [\n              {\n                \"name\": \"foo\",\n                \"value\": \"bar\",\n                \"enabled\": true\n              }\n            ],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"json\",\n              \"json\": \"{\\n  \\\"hello\\\": 990531470713421825,\\n  \\\"decimal\\\": 1.0,\\n  \\\"decimal2\\\": 1.00,\\n  \\\"decimal3\\\": 1.00200,\\n  \\\"decimal4\\\": 0.00\\n}\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {},\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"eq 200\",\n                \"enabled\": true,\n                \"uid\": \"lCoGpuLOpVmXPfGzJqbTB\"\n              }\n            ],\n            \"tests\": \"// todo: add tests once lossless json echo server is ready\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"echo bom json\",\n          \"seq\": 1,\n          \"request\": {\n            \"url\": \"{{host}}/api/echo/bom-json-test\",\n            \"method\": \"GET\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"none\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {},\n            \"vars\": {},\n            \"assertions\": [],\n            \"tests\": \"\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"echo form-url-encoded\",\n          \"seq\": 9,\n          \"request\": {\n            \"url\": \"{{echo-host}}\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"formUrlEncoded\",\n              \"formUrlEncoded\": [\n                {\n                  \"name\": \"form-data-key\",\n                  \"value\": \"{{form-data-key}}\",\n                  \"enabled\": true\n                },\n                {\n                  \"name\": \"form-data-stringified-object\",\n                  \"value\": \"{{form-data-stringified-object}}\",\n                  \"enabled\": true\n                }\n              ],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {\n              \"req\": \"let obj = JSON.stringify({foo:123});\\nbru.setVar('form-data-key', 'form-data-value');\\nbru.setVar('form-data-stringified-object', obj);\"\n            },\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.body\",\n                \"value\": \"eq form-data-key=form-data-value&form-data-stringified-object=%7B%22foo%22%3A123%7D\",\n                \"enabled\": true,\n                \"uid\": \"V0MSBvq2iFun9gIWfgqtQ\"\n              }\n            ],\n            \"tests\": \"\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"echo json\",\n          \"seq\": 2,\n          \"request\": {\n            \"url\": \"{{host}}/api/echo/json\",\n            \"method\": \"POST\",\n            \"headers\": [\n              {\n                \"name\": \"foo\",\n                \"value\": \"bar\",\n                \"enabled\": true\n              }\n            ],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"json\",\n              \"json\": \"{\\n  \\\"hello\\\": \\\"bruno\\\"\\n}\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {\n              \"req\": \"bru.setVar(\\\"foo\\\", \\\"foo-world-2\\\");\"\n            },\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"eq 200\",\n                \"enabled\": true,\n                \"uid\": \"FFVx1w4MstKeQfQR66Xy8\"\n              }\n            ],\n            \"tests\": \"test(\\\"should return json\\\", function() {\\n  const data = res.getBody();\\n  expect(res.getBody()).to.eql({\\n    \\\"hello\\\": \\\"bruno\\\"\\n  });\\n});\\n\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"echo multipart via scripting\",\n          \"seq\": 10,\n          \"request\": {\n            \"url\": \"{{echo-host}}\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"multipartForm\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {\n              \"req\": \"const FormData = require(\\\"form-data\\\");\\nconst form = new FormData();\\nform.append('form-data-key', 'form-data-value');\\nreq.setBody(form);\"\n            },\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.body\",\n                \"value\": \"contains form-data-value\",\n                \"enabled\": true,\n                \"uid\": \"USCnLx51IlWz6HrLxlR1r\"\n              }\n            ],\n            \"tests\": \"\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"echo multipart\",\n          \"seq\": 8,\n          \"request\": {\n            \"url\": \"{{echo-host}}\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"multipartForm\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [\n                {\n                  \"type\": \"text\",\n                  \"name\": \"form-data-key\",\n                  \"value\": \"{{form-data-key}}\",\n                  \"enabled\": true\n                },\n                {\n                  \"type\": \"text\",\n                  \"name\": \"form-data-stringified-object\",\n                  \"value\": \"{{form-data-stringified-object}}\",\n                  \"enabled\": true\n                },\n                {\n                  \"type\": \"file\",\n                  \"name\": \"file\",\n                  \"value\": [\n                    \"bruno.png\"\n                  ],\n                  \"enabled\": true\n                }\n              ],\n              \"file\": []\n            },\n            \"script\": {\n              \"req\": \"let obj = JSON.stringify({foo:123});\\nbru.setVar('form-data-key', 'form-data-value');\\nbru.setVar('form-data-stringified-object', obj);\"\n            },\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.body\",\n                \"value\": \"contains form-data-value\",\n                \"enabled\": true,\n                \"uid\": \"L5wrs8CJKD7skDazamdTZ\"\n              },\n              {\n                \"name\": \"res.body\",\n                \"value\": \"contains {\\\"foo\\\":123}\",\n                \"enabled\": true,\n                \"uid\": \"2rPiaUFbPuWPq0ew6dqVd\"\n              }\n            ],\n            \"tests\": \"\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"echo plaintext\",\n          \"seq\": 3,\n          \"request\": {\n            \"url\": \"{{host}}/api/echo/text\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"text\",\n              \"text\": \"hello\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {},\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"eq 200\",\n                \"enabled\": true,\n                \"uid\": \"cW2RamEi0zqLmn84SjUoh\"\n              }\n            ],\n            \"tests\": \"test(\\\"should return plain text\\\", function() {\\n  const data = res.getBody();\\n  expect(res.getBody()).to.eql(\\\"hello\\\");\\n});\\n\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"echo xml parsed-self closing tags-\",\n          \"seq\": 6,\n          \"request\": {\n            \"url\": \"{{host}}/api/echo/xml-parsed\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"xml\",\n              \"xml\": \"<hello>\\n  <world>bruno</world>\\n  <world/>\\n</hello>\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {},\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"eq 200\",\n                \"enabled\": true,\n                \"uid\": \"lIbw7OdlPxbNUKdShGNvi\"\n              }\n            ],\n            \"tests\": \"test(\\\"should return parsed xml\\\", function() {\\n  const data = res.getBody();\\n  expect(res.getBody()).to.eql({\\n    \\\"hello\\\": {\\n      \\\"world\\\": [\\n        \\\"bruno\\\",\\n        \\\"\\\"\\n      ]\\n    }\\n  });\\n});\\n\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"echo xml parsed\",\n          \"seq\": 4,\n          \"request\": {\n            \"url\": \"{{host}}/api/echo/xml-parsed\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"xml\",\n              \"xml\": \"<hello>\\n  <world>bruno</world>\\n</hello>\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {},\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"eq 200\",\n                \"enabled\": true,\n                \"uid\": \"5yr5fbjrAre0Cp0C3PY4c\"\n              }\n            ],\n            \"tests\": \"test(\\\"should return parsed xml\\\", function() {\\n  const data = res.getBody();\\n  expect(res.getBody()).to.eql({\\n    \\\"hello\\\": {\\n      \\\"world\\\": [\\\"bruno\\\"]\\n    }\\n  });\\n});\\n\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"echo xml raw\",\n          \"seq\": 5,\n          \"request\": {\n            \"url\": \"{{host}}/api/echo/xml-raw\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"xml\",\n              \"xml\": \"<hello><world>bruno</world></hello>\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {},\n            \"vars\": {},\n            \"assertions\": [],\n            \"tests\": \"\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        }\n      ]\n    },\n    {\n      \"type\": \"folder\",\n      \"name\": \"graphql\",\n      \"root\": {\n        \"meta\": {\n          \"name\": \"graphql\"\n        }\n      },\n      \"items\": [\n        {\n          \"type\": \"graphql\",\n          \"name\": \"spacex\",\n          \"seq\": 1,\n          \"request\": {\n            \"url\": \"https://spacex-production.up.railway.app/\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"graphql\",\n              \"graphql\": {\n                \"query\": \"{\\n  company {\\n    ceo\\n  }\\n}\\n\"\n              },\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {},\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"eq 200\",\n                \"enabled\": true,\n                \"uid\": \"P99wa88sX4L4Zat94bHzz\"\n              }\n            ],\n            \"tests\": \"\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        }\n      ]\n    },\n    {\n      \"type\": \"folder\",\n      \"name\": \"lib\",\n      \"root\": {\n        \"meta\": {\n          \"name\": \"lib\"\n        }\n      }\n    },\n    {\n      \"type\": \"http\",\n      \"name\": \"ping\",\n      \"seq\": 1,\n      \"request\": {\n        \"url\": \"{{host}}/ping\",\n        \"method\": \"GET\",\n        \"headers\": [],\n        \"params\": [],\n        \"body\": {\n          \"mode\": \"none\",\n          \"formUrlEncoded\": [],\n          \"multipartForm\": [],\n          \"file\": []\n        },\n        \"script\": {},\n        \"vars\": {},\n        \"assertions\": [],\n        \"tests\": \"\",\n        \"docs\": \"\",\n        \"auth\": {\n          \"mode\": \"none\"\n        }\n      }\n    },\n    {\n      \"type\": \"folder\",\n      \"name\": \"preview\",\n      \"root\": {\n        \"meta\": {\n          \"name\": \"preview\"\n        }\n      },\n      \"items\": [\n        {\n          \"type\": \"folder\",\n          \"name\": \"html\",\n          \"root\": {\n            \"meta\": {\n              \"name\": \"html\"\n            }\n          },\n          \"items\": [\n            {\n              \"type\": \"http\",\n              \"name\": \"bruno\",\n              \"seq\": 1,\n              \"request\": {\n                \"url\": \"https://www.github.com\",\n                \"method\": \"GET\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"none\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {\n                  \"req\": \"console.log(req.getCookie());\\n\\nconsole.log(req.getHeaders());\"\n                },\n                \"vars\": {},\n                \"assertions\": [\n                  {\n                    \"name\": \"res.status\",\n                    \"value\": \"eq 200\",\n                    \"enabled\": true,\n                    \"uid\": \"2bpXrsR3q5MBpkXbO1vfS\"\n                  }\n                ],\n                \"tests\": \"test(\\\"should return parsed xml\\\", function() {\\n  const headers = res.getHeaders();\\n  expect(headers['content-type']).to.eql(\\\"text/html; charset=utf-8\\\");\\n});\\n\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"none\"\n                }\n              }\n            }\n          ]\n        },\n        {\n          \"type\": \"folder\",\n          \"name\": \"image\",\n          \"root\": {\n            \"meta\": {\n              \"name\": \"image\"\n            }\n          },\n          \"items\": [\n            {\n              \"type\": \"http\",\n              \"name\": \"bruno\",\n              \"seq\": 1,\n              \"request\": {\n                \"url\": \"https://www.usebruno.com/images/landing-2.png\",\n                \"method\": \"GET\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"none\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {},\n                \"vars\": {},\n                \"assertions\": [],\n                \"tests\": \"test(\\\"should return parsed xml\\\", function() {\\n  const headers = res.getHeaders();\\n  expect(headers['content-type']).to.eql(\\\"image/png\\\");\\n});\\n\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"none\"\n                }\n              }\n            }\n          ]\n        }\n      ]\n    },\n    {\n      \"type\": \"folder\",\n      \"name\": \"redirects\",\n      \"root\": {\n        \"meta\": {\n          \"name\": \"redirects\"\n        }\n      },\n      \"items\": [\n        {\n          \"type\": \"http\",\n          \"name\": \"Disable Redirect\",\n          \"seq\": 1,\n          \"request\": {\n            \"url\": \"{{host}}/redirect-to-ping\",\n            \"method\": \"GET\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"none\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {\n              \"req\": \"req.setMaxRedirects(0);\"\n            },\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"302\",\n                \"enabled\": true,\n                \"uid\": \"Bs0jGEFFNAyyRBx6DILEN\"\n              }\n            ],\n            \"tests\": \"test(\\\"should disable redirect to ping\\\", function() {\\n  const data = res.getBody();\\n  expect(data).to.equal('Found. Redirecting to /ping');\\n});\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"Test Redirect\",\n          \"seq\": 2,\n          \"request\": {\n            \"url\": \"{{host}}/redirect-to-ping\",\n            \"method\": \"GET\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"none\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {},\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"200\",\n                \"enabled\": true,\n                \"uid\": \"nNRcxeANwM6VBEXR1qoM0\"\n              },\n              {\n                \"name\": \"res.body\",\n                \"value\": \"pong\",\n                \"enabled\": true,\n                \"uid\": \"3Y5SHtNsQHK0glgikD1IU\"\n              }\n            ],\n            \"tests\": \"test(\\\"should redirect to ping\\\", function() {\\n  const data = res.getBody();\\n  expect(data).to.equal('pong');\\n});\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        }\n      ]\n    },\n    {\n      \"type\": \"folder\",\n      \"name\": \"scripting\",\n      \"root\": {\n        \"meta\": {\n          \"name\": \"scripting\"\n        }\n      },\n      \"items\": [\n        {\n          \"type\": \"folder\",\n          \"name\": \"api\",\n          \"root\": {\n            \"meta\": {\n              \"name\": \"api\"\n            }\n          },\n          \"items\": [\n            {\n              \"type\": \"folder\",\n              \"name\": \"bru\",\n              \"root\": {\n                \"meta\": {\n                  \"name\": \"bru\"\n                }\n              },\n              \"items\": [\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getEnvName\",\n                  \"seq\": 1,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"const envName = bru.getEnvName();\\nbru.setVar(\\\"testEnvName\\\", envName);\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [],\n                    \"tests\": \"test(\\\"should get env name in scripts\\\", function() {\\n  const testEnvName = bru.getVar(\\\"testEnvName\\\");\\n  expect(testEnvName).to.equal(\\\"Prod\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getEnvVar\",\n                  \"seq\": 2,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [],\n                    \"tests\": \"test(\\\"should get env var in scripts\\\", function() {\\n  const host = bru.getEnvVar(\\\"host\\\")\\n  expect(host).to.equal(\\\"https://testbench-sanity.usebruno.com\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getProcessEnv\",\n                  \"seq\": 6,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [],\n                    \"tests\": \"test(\\\"bru.getProcessEnv()\\\", function() {\\n  const v = bru.getProcessEnv(\\\"PROC_ENV_VAR\\\");\\n  expect(v).to.equal(\\\"woof\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getVar\",\n                  \"seq\": 5,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [],\n                    \"tests\": \"test(\\\"should get var in scripts\\\", function() {\\n  const testSetVar = bru.getVar(\\\"testSetVar\\\");\\n  expect(testSetVar).to.equal(\\\"bruno-test-87267\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"setEnvVar\",\n                  \"seq\": 3,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"res\": \"bru.setEnvVar(\\\"testSetEnvVar\\\", \\\"bruno-29653\\\")\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [],\n                    \"tests\": \"test(\\\"should set env var in scripts\\\", function() {\\n  const testSetEnvVar = bru.getEnvVar(\\\"testSetEnvVar\\\")\\n  expect(testSetEnvVar).to.equal(\\\"bruno-29653\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"setVar\",\n                  \"seq\": 4,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"res\": \"bru.setVar(\\\"testSetVar\\\", \\\"bruno-test-87267\\\")\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [],\n                    \"tests\": \"test(\\\"should get var in scripts\\\", function() {\\n  const testSetVar = bru.getVar(\\\"testSetVar\\\");\\n  expect(testSetVar).to.equal(\\\"bruno-test-87267\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                }\n              ]\n            },\n            {\n              \"type\": \"folder\",\n              \"name\": \"req\",\n              \"root\": {\n                \"meta\": {\n                  \"name\": \"req\"\n                }\n              },\n              \"items\": [\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getBody\",\n                  \"seq\": 9,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/echo/json\",\n                    \"method\": \"POST\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"json\",\n                      \"json\": \"{\\n  \\\"hello\\\": \\\"bruno\\\"\\n}\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"7VvXmRWwUdGYbvrQaeDMD\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"req.getBody()\\\", function() {\\n  const data = res.getBody();\\n  expect(data).to.eql({\\n    \\\"hello\\\": \\\"bruno\\\"\\n  });\\n});\\n\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getHeader\",\n                  \"seq\": 5,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [\n                      {\n                        \"name\": \"bruno\",\n                        \"value\": \"is-awesome\",\n                        \"enabled\": true\n                      }\n                    ],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"HssG2g6gUaWaBFFanCozP\"\n                      },\n                      {\n                        \"name\": \"res.body\",\n                        \"value\": \"eq pong\",\n                        \"enabled\": true,\n                        \"uid\": \"50NSDIeXgRr0TcXEdquci\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"req.getHeader(name)\\\", function() {\\n  const h = req.getHeader('bruno');\\n  expect(h).to.equal(\\\"is-awesome\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getHeaders\",\n                  \"seq\": 7,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [\n                      {\n                        \"name\": \"bruno\",\n                        \"value\": \"is-awesome\",\n                        \"enabled\": true\n                      },\n                      {\n                        \"name\": \"della\",\n                        \"value\": \"is-beautiful\",\n                        \"enabled\": true\n                      }\n                    ],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"9NDZWAvBS23WJAZsKl9SS\"\n                      },\n                      {\n                        \"name\": \"res.body\",\n                        \"value\": \"eq pong\",\n                        \"enabled\": true,\n                        \"uid\": \"f9ULUob9jYiABYuEzfmgC\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"req.getHeaders()\\\", function() {\\n  const h = req.getHeaders();\\n  expect(h.bruno).to.equal(\\\"is-awesome\\\");\\n  expect(h.della).to.equal(\\\"is-beautiful\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getMethod\",\n                  \"seq\": 3,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"EpNcKUgCYzdg8KrOLqaCX\"\n                      },\n                      {\n                        \"name\": \"res.body\",\n                        \"value\": \"eq pong\",\n                        \"enabled\": true,\n                        \"uid\": \"qFqcLtNYbZ9nqhiwvS3Qk\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"req.getMethod()()\\\", function() {\\n  const method = req.getMethod();\\n  expect(method).to.equal(\\\"GET\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getUrl\",\n                  \"seq\": 1,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"VaiDs2JU1NM8prTc59GdX\"\n                      },\n                      {\n                        \"name\": \"res.body\",\n                        \"value\": \"eq pong\",\n                        \"enabled\": true,\n                        \"uid\": \"4vC9zp5XBajbYKDYM4oFN\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"req.getUrl()\\\", function() {\\n  const url = req.getUrl();\\n  expect(url).to.equal(\\\"https://testbench-sanity.usebruno.com/ping\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"setBody\",\n                  \"seq\": 10,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/echo/json\",\n                    \"method\": \"POST\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"json\",\n                      \"json\": \"{\\n  \\\"hello\\\": \\\"bruno\\\"\\n}\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"req.setBody({\\n  \\\"bruno\\\": \\\"is awesome\\\"\\n});\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"EGlBavIEZ2j0s2aczQxAP\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"req.setBody()\\\", function() {\\n  const data = res.getBody();\\n  expect(data).to.eql({\\n    \\\"bruno\\\": \\\"is awesome\\\"\\n  });\\n});\\n\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"setHeader\",\n                  \"seq\": 6,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [\n                      {\n                        \"name\": \"bruno\",\n                        \"value\": \"is-awesome\",\n                        \"enabled\": true\n                      }\n                    ],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"req.setHeader('bruno', 'is-the-future');\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"J9AUIh6CbTnIxlCyKqqq7\"\n                      },\n                      {\n                        \"name\": \"res.body\",\n                        \"value\": \"eq pong\",\n                        \"enabled\": true,\n                        \"uid\": \"sb6dpEtw8SYyeXVqEz3OA\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"req.setHeader(name)\\\", function() {\\n  const h = req.getHeader('bruno');\\n  expect(h).to.equal(\\\"is-the-future\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"setHeaders\",\n                  \"seq\": 8,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [\n                      {\n                        \"name\": \"bruno\",\n                        \"value\": \"is-awesome\",\n                        \"enabled\": true\n                      },\n                      {\n                        \"name\": \"della\",\n                        \"value\": \"is-beautiful\",\n                        \"enabled\": true\n                      }\n                    ],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"req.setHeaders({\\n  \\\"content-type\\\": \\\"application/text\\\",\\n  \\\"transaction-id\\\": \\\"foobar\\\"\\n});\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"1djJxGMAwmAHF2ruhdQQO\"\n                      },\n                      {\n                        \"name\": \"res.body\",\n                        \"value\": \"eq pong\",\n                        \"enabled\": true,\n                        \"uid\": \"sqvBwQilTBWnFDoIBAC4T\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"req.setHeaders()\\\", function() {\\n  const h = req.getHeaders();\\n  expect(h['content-type']).to.equal(\\\"application/text\\\");\\n  expect(h['transaction-id']).to.equal(\\\"foobar\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"setMethod\",\n                  \"seq\": 4,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"POST\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"req.setMethod(\\\"GET\\\");\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"3eRRXHvWErUAC2IiBto7B\"\n                      },\n                      {\n                        \"name\": \"res.body\",\n                        \"value\": \"eq pong\",\n                        \"enabled\": true,\n                        \"uid\": \"UAqBwv3S2607RXzgC6S1i\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"req.setMethod()()\\\", function() {\\n  const method = req.getMethod();\\n  expect(method).to.equal(\\\"GET\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"setUrl\",\n                  \"seq\": 2,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping/invalid\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"req.setUrl(\\\"https://testbench-sanity.usebruno.com/ping\\\");\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"sGLaBON85rqipc1VP8R3W\"\n                      },\n                      {\n                        \"name\": \"res.body\",\n                        \"value\": \"eq pong\",\n                        \"enabled\": true,\n                        \"uid\": \"SPVLRHys6RQh1GbRWnPpT\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"req.setUrl()\\\", function() {\\n  const url = req.getUrl();\\n  expect(url).to.equal(\\\"https://testbench-sanity.usebruno.com/ping\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                }\n              ]\n            },\n            {\n              \"type\": \"folder\",\n              \"name\": \"res\",\n              \"root\": {\n                \"meta\": {\n                  \"name\": \"res\"\n                }\n              },\n              \"items\": [\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getBody\",\n                  \"seq\": 4,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/echo/json\",\n                    \"method\": \"POST\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"json\",\n                      \"json\": \"{\\n  \\\"hello\\\": \\\"bruno\\\"\\n}\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"rQe4gitaooPrQkEqD6AvL\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"res.getBody()\\\", function() {\\n  const data = res.getBody();\\n  expect(data).to.eql({\\n    \\\"hello\\\": \\\"bruno\\\"\\n  });\\n});\\n\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getHeader\",\n                  \"seq\": 2,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/echo/json\",\n                    \"method\": \"POST\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"json\",\n                      \"json\": \"{\\n  \\\"hello\\\": \\\"bruno\\\"\\n}\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"LOem8LkfWBML3yI1Kh3ZU\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"res.getHeader(name)\\\", function() {\\n  const server = res.getHeader('x-powered-by');\\n  expect(server).to.eql('Express');\\n});\\n\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getHeaders\",\n                  \"seq\": 3,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/echo/json\",\n                    \"method\": \"POST\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"json\",\n                      \"json\": \"{\\n  \\\"hello\\\": \\\"bruno\\\"\\n}\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"qF8ASpikHJzQLRS0ZqNkA\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"res.getHeaders(name)\\\", function() {\\n  const h = res.getHeaders();\\n  expect(h['x-powered-by']).to.eql('Express');\\n  expect(h['content-length']).to.eql('17');\\n});\\n\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getResponseTime\",\n                  \"seq\": 5,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/echo/json\",\n                    \"method\": \"POST\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"json\",\n                      \"json\": \"{\\n  \\\"hello\\\": \\\"bruno\\\"\\n}\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"pqN8P939S4dfZ1Bmuemi7\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"res.getResponseTime()\\\", function() {\\n  const responseTime = res.getResponseTime();\\n  expect(typeof responseTime).to.eql(\\\"number\\\");\\n  expect(responseTime > 0).to.be.true;\\n});\\n\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getStatus\",\n                  \"seq\": 1,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"0DqeIPuHtcmaULlYG9eWu\"\n                      },\n                      {\n                        \"name\": \"res.body\",\n                        \"value\": \"eq pong\",\n                        \"enabled\": true,\n                        \"uid\": \"WZDxwKadXkDkK3Ea1VMKW\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"res.getStatus()\\\", function() {\\n  const status = res.getStatus()\\n  expect(status).to.equal(200);\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"type\": \"folder\",\n          \"name\": \"inbuilt modules\",\n          \"root\": {\n            \"meta\": {\n              \"name\": \"inbuilt modules\"\n            }\n          },\n          \"items\": [\n            {\n              \"type\": \"folder\",\n              \"name\": \"axios\",\n              \"root\": {\n                \"meta\": {\n                  \"name\": \"axios\"\n                }\n              },\n              \"items\": [\n                {\n                  \"type\": \"http\",\n                  \"name\": \"axios-pre-req-script\",\n                  \"seq\": 1,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"const axios = require(\\\"axios\\\");\\n\\nconst url = \\\"https://testbench-sanity.usebruno.com/api/echo/json\\\";\\nconst response = await axios.post(url, {\\n  \\\"hello\\\": \\\"bruno\\\"\\n});\\n\\nreq.setBody(response.data);\\nreq.setMethod(\\\"POST\\\");\\nreq.setUrl(url);\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [],\n                    \"tests\": \"test(\\\"req.getBody()\\\", function() {\\n  const data = res.getBody();\\n  expect(data).to.eql({\\n    \\\"hello\\\": \\\"bruno\\\"\\n  });\\n});\\n\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                }\n              ]\n            },\n            {\n              \"type\": \"folder\",\n              \"name\": \"crypto-js\",\n              \"root\": {\n                \"meta\": {\n                  \"name\": \"crypto-js\"\n                }\n              },\n              \"items\": [\n                {\n                  \"type\": \"http\",\n                  \"name\": \"crypto-js-pre-request-script\",\n                  \"seq\": 1,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"var CryptoJS = require(\\\"crypto-js\\\");\\n\\n// Encrypt\\nvar ciphertext = CryptoJS.AES.encrypt('my message', 'secret key 123').toString();\\n\\n// Decrypt\\nvar bytes  = CryptoJS.AES.decrypt(ciphertext, 'secret key 123');\\nvar originalText = bytes.toString(CryptoJS.enc.Utf8);\\n\\nbru.setVar('crypto-test-message', originalText);\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [],\n                    \"tests\": \"test(\\\"crypto message\\\", function() {\\n  const data = bru.getVar('crypto-test-message');\\n  bru.setVar('crypto-test-message', null);\\n  expect(data).to.eql('my message');\\n});\\n\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                }\n              ]\n            },\n            {\n              \"type\": \"folder\",\n              \"name\": \"nanoid\",\n              \"root\": {\n                \"meta\": {\n                  \"name\": \"nanoid\"\n                }\n              },\n              \"items\": [\n                {\n                  \"type\": \"http\",\n                  \"name\": \"nanoid\",\n                  \"seq\": 1,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"const { nanoid } = require(\\\"nanoid\\\");\\n \\nbru.setVar(\\\"nanoid-test-id\\\", nanoid());\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [],\n                    \"tests\": \"test(\\\"nanoid var\\\", function() {\\n  const id = bru.getVar('nanoid-test-id');\\n  let isValidNanoid = /^[a-zA-Z0-9_-]{21}$/.test(id)\\n  bru.setVar('nanoid-test-id', null);\\n  expect(isValidNanoid).to.eql(true);\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                }\n              ]\n            },\n            {\n              \"type\": \"folder\",\n              \"name\": \"uuid\",\n              \"root\": {\n                \"meta\": {\n                  \"name\": \"uuid\"\n                }\n              },\n              \"items\": [\n                {\n                  \"type\": \"http\",\n                  \"name\": \"uuid\",\n                  \"seq\": 1,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"const { v4 } = require(\\\"uuid\\\");\\n \\nbru.setVar(\\\"uuid-test-id\\\", v4());\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [],\n                    \"tests\": \"test(\\\"uuid var\\\", function() {\\n  const id = bru.getVar('uuid-test-id');\\n  let isValidUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id);\\n  bru.setVar('uuid-test-id', null);\\n  expect(isValidUuid).to.eql(true);\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"type\": \"folder\",\n          \"name\": \"js\",\n          \"root\": {\n            \"meta\": {\n              \"name\": \"js\"\n            }\n          },\n          \"items\": [\n            {\n              \"type\": \"http\",\n              \"name\": \"data types - request vars\",\n              \"seq\": 3,\n              \"request\": {\n                \"url\": \"{{host}}/api/echo/json\",\n                \"method\": \"POST\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"json\",\n                  \"json\": \"{\\n  \\\"boolean\\\": false,\\n  \\\"number_1\\\": 1,\\n  \\\"number_2\\\": 0,\\n  \\\"number_3\\\": -1,\\n  \\\"string\\\": \\\"bruno\\\",\\n  \\\"array\\\": [1, 2, 3, 4, 5],\\n  \\\"object\\\": {\\n    \\\"hello\\\": \\\"bruno\\\"\\n  },\\n  \\\"null\\\": null\\n}\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {},\n                \"vars\": {},\n                \"assertions\": [\n                  {\n                    \"name\": \"req.body.boolean\",\n                    \"value\": \"isBoolean false\",\n                    \"enabled\": true,\n                    \"uid\": \"SgXkeY8p7ahXIq2kA9FzA\"\n                  },\n                  {\n                    \"name\": \"req.body.number_1\",\n                    \"value\": \"isNumber 1\",\n                    \"enabled\": true,\n                    \"uid\": \"lhS17xvEP5jzHGP2Uqfg9\"\n                  },\n                  {\n                    \"name\": \"req.body.undefined\",\n                    \"value\": \"isUndefined undefined\",\n                    \"enabled\": true,\n                    \"uid\": \"bFTk8cAUAzNnrvUhbNeyC\"\n                  },\n                  {\n                    \"name\": \"req.body.string\",\n                    \"value\": \"isString bruno\",\n                    \"enabled\": true,\n                    \"uid\": \"ohANzzhuM8E8egvoVy20M\"\n                  },\n                  {\n                    \"name\": \"req.body.null\",\n                    \"value\": \"isNull null\",\n                    \"enabled\": true,\n                    \"uid\": \"r6W6I7ATDVswqkAf7Kl1k\"\n                  },\n                  {\n                    \"name\": \"req.body.array\",\n                    \"value\": \"isArray\",\n                    \"enabled\": true,\n                    \"uid\": \"fFUuv0vldfqaAGPjhfmdl\"\n                  },\n                  {\n                    \"name\": \"req.body.boolean\",\n                    \"value\": \"eq false\",\n                    \"enabled\": true,\n                    \"uid\": \"eXPS2R19qWsPGm6usEogu\"\n                  },\n                  {\n                    \"name\": \"req.body.number_1\",\n                    \"value\": \"eq 1\",\n                    \"enabled\": true,\n                    \"uid\": \"WCKmMIqsFPwocy6LZmCcc\"\n                  },\n                  {\n                    \"name\": \"req.body.undefined\",\n                    \"value\": \"eq undefined\",\n                    \"enabled\": true,\n                    \"uid\": \"7fJRYC8ELm68Uc5CaB7B8\"\n                  },\n                  {\n                    \"name\": \"req.body.string\",\n                    \"value\": \"eq bruno\",\n                    \"enabled\": true,\n                    \"uid\": \"fXTl58gxhAUrUM8SZFxLW\"\n                  },\n                  {\n                    \"name\": \"req.body.null\",\n                    \"value\": \"eq null\",\n                    \"enabled\": true,\n                    \"uid\": \"yUhXaWTPJaUYU4zerBABN\"\n                  },\n                  {\n                    \"name\": \"req.body.number_2\",\n                    \"value\": \"eq 0\",\n                    \"enabled\": true,\n                    \"uid\": \"WWCCm6i8GzyNBH6xiQGAP\"\n                  },\n                  {\n                    \"name\": \"req.body.number_3\",\n                    \"value\": \"eq -1\",\n                    \"enabled\": true,\n                    \"uid\": \"G73JIdrxSUDpc33EfAZGW\"\n                  },\n                  {\n                    \"name\": \"req.body.number_2\",\n                    \"value\": \"isNumber\",\n                    \"enabled\": true,\n                    \"uid\": \"Dp5pdDeMEulfPB3ZDdjl4\"\n                  },\n                  {\n                    \"name\": \"req.body.number_3\",\n                    \"value\": \"isNumber\",\n                    \"enabled\": true,\n                    \"uid\": \"fWghLNcBbslVF2OJMSS6x\"\n                  }\n                ],\n                \"tests\": \"\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"none\"\n                }\n              }\n            },\n            {\n              \"type\": \"http\",\n              \"name\": \"data types\",\n              \"seq\": 2,\n              \"request\": {\n                \"url\": \"{{host}}/api/echo/json\",\n                \"method\": \"POST\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"json\",\n                  \"json\": \"{\\n  \\\"boolean\\\": false,\\n  \\\"number\\\": 1,\\n  \\\"string\\\": \\\"bruno\\\",\\n  \\\"array\\\": [1, 2, 3, 4, 5],\\n  \\\"object\\\": {\\n    \\\"hello\\\": \\\"bruno\\\"\\n  },\\n  \\\"null\\\": null\\n}\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {\n                  \"req\": \"const reqBody = req.getBody();\\n\\nbru.setVar(\\\"dataTypeVarTest\\\", {\\n  ...reqBody,\\n  \\\"undefined\\\": undefined\\n});\"\n                },\n                \"vars\": {},\n                \"assertions\": [],\n                \"tests\": \"test(\\\"data types check via bru var\\\", function() {\\n  let v = bru.getVar(\\\"dataTypeVarTest\\\");\\n  v = {\\n    ...v,\\n    \\\"undefined\\\": undefined\\n  };\\n  expect(v).to.eql({\\n    \\\"boolean\\\": false,\\n    \\\"number\\\": 1,\\n    \\\"string\\\": \\\"bruno\\\",\\n    \\\"array\\\": [1, 2, 3, 4, 5],\\n    \\\"object\\\": {\\n      \\\"hello\\\": \\\"bruno\\\"\\n    },\\n    \\\"null\\\": null,\\n    \\\"undefined\\\": undefined\\n  })\\n});\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"none\"\n                }\n              }\n            },\n            {\n              \"type\": \"http\",\n              \"name\": \"setTimeout\",\n              \"seq\": 1,\n              \"request\": {\n                \"url\": \"{{host}}/ping\",\n                \"method\": \"GET\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"none\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {\n                  \"req\": \"bru.setVar(\\\"test-js-set-timeout\\\", \\\"\\\");\\nawait new Promise((resolve, reject) => {\\n  setTimeout(() => {\\n    bru.setVar(\\\"test-js-set-timeout\\\", \\\"bruno\\\");\\n    resolve();\\n  }, 1000);\\n});\\n\\nconst v = bru.getVar(\\\"test-js-set-timeout\\\");\\nbru.setVar(\\\"test-js-set-timeout\\\", v + \\\"-is-awesome\\\");\\n\"\n                },\n                \"vars\": {},\n                \"assertions\": [],\n                \"tests\": \"test(\\\"setTimeout()\\\", function() {\\n  const v = bru.getVar(\\\"test-js-set-timeout\\\")\\n  expect(v).to.eql(\\\"bruno-is-awesome\\\");\\n});\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"none\"\n                }\n              }\n            }\n          ]\n        },\n        {\n          \"type\": \"folder\",\n          \"name\": \"local modules\",\n          \"root\": {\n            \"meta\": {\n              \"name\": \"local modules\"\n            }\n          },\n          \"items\": [\n            {\n              \"type\": \"http\",\n              \"name\": \"invalid and valid module imports\",\n              \"seq\": 3,\n              \"request\": {\n                \"url\": \"{{host}}/ping\",\n                \"method\": \"GET\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"none\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {\n                  \"req\": \"try {\\n  bru.setVar('invalid_module_error_thrown', false);\\n  // should throw an error\\n  const invalid = require(\\\"./lib/invalid\\\");\\n}\\ncatch(error) {\\n  bru.setVar('invalid_module_error_thrown', true);\\n}\\n\\n\\ntry {\\n  bru.setVar('valid_module_no_error', true);\\n  // should not throw an error\\n  const math = require(\\\"./lib/math\\\");\\n}\\ncatch(error) {\\n  bru.setVar('valid_module_no_error', false);\\n}\"\n                },\n                \"vars\": {},\n                \"assertions\": [\n                  {\n                    \"name\": \"invalid_module_error_thrown\",\n                    \"value\": \"eq true\",\n                    \"enabled\": true,\n                    \"uid\": \"wdZ0MsGXmW7tRiX4VtQaT\"\n                  },\n                  {\n                    \"name\": \"valid_module_no_error\",\n                    \"value\": \"eq true\",\n                    \"enabled\": true,\n                    \"uid\": \"A7hwDplpc0qDp5Bk46AMJ\"\n                  }\n                ],\n                \"tests\": \"\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"none\"\n                }\n              }\n            },\n            {\n              \"type\": \"http\",\n              \"name\": \"sum -without js extn-\",\n              \"seq\": 2,\n              \"request\": {\n                \"url\": \"{{host}}/api/echo/json\",\n                \"method\": \"POST\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"json\",\n                  \"json\": \"{\\n  \\\"a\\\": 1,\\n  \\\"b\\\": 2\\n}\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {\n                  \"req\": \"const math = require(\\\"./lib/math\\\");\\nconsole.log(math, 'math');\\n\\nconst body = req.getBody();\\nbody.sum = math.sum(body.a, body.b);\\nbody.areaOfCircle = math.areaOfCircle(2);\\n\\nreq.setBody(body);\"\n                },\n                \"vars\": {},\n                \"assertions\": [\n                  {\n                    \"name\": \"res.status\",\n                    \"value\": \"eq 200\",\n                    \"enabled\": true,\n                    \"uid\": \"R6TRM5HoxKGuC5dHFvYdH\"\n                  }\n                ],\n                \"tests\": \"test(\\\"should return json\\\", function() {\\n  const data = res.getBody();\\n  expect(res.getBody()).to.eql({\\n    \\\"a\\\": 1,\\n    \\\"b\\\": 2,\\n    \\\"sum\\\": 3,\\n    \\\"areaOfCircle\\\": 12.56\\n  });\\n});\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"none\"\n                }\n              }\n            },\n            {\n              \"type\": \"http\",\n              \"name\": \"sum\",\n              \"seq\": 1,\n              \"request\": {\n                \"url\": \"{{host}}/api/echo/json\",\n                \"method\": \"POST\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"json\",\n                  \"json\": \"{\\n  \\\"a\\\": 1,\\n  \\\"b\\\": 2\\n}\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {\n                  \"req\": \"const math = require(\\\"./lib/math.js\\\");  \\nconst body = req.getBody();\\nbody.sum = math.sum(body.a, body.b);\\n\\nreq.setBody(body);\"\n                },\n                \"vars\": {},\n                \"assertions\": [\n                  {\n                    \"name\": \"res.status\",\n                    \"value\": \"eq 200\",\n                    \"enabled\": true,\n                    \"uid\": \"UXDIZRejDajw3j0oYHhij\"\n                  }\n                ],\n                \"tests\": \"test(\\\"should return json\\\", function() {\\n  const data = res.getBody();\\n  expect(res.getBody()).to.eql({\\n    \\\"a\\\": 1,\\n    \\\"b\\\": 2,\\n    \\\"sum\\\": 3\\n  });\\n});\\n\\ntest(\\\"should return json\\\", function() {\\n  const data = res.getBody();\\n  expect(res.getBody()).to.eql({\\n    \\\"a\\\": 1,\\n    \\\"b\\\": 2,\\n    \\\"sum\\\": 3\\n  });\\n});\\n\\ntest(\\\"should return json\\\", function() {\\n  const data = res.getBody();\\n  expect(res.getBody()).to.eql({\\n    \\\"a\\\": 1,\\n    \\\"b\\\": 2,\\n    \\\"sum\\\": 3\\n  });\\n});\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"none\"\n                }\n              }\n            }\n          ]\n        },\n        {\n          \"type\": \"folder\",\n          \"name\": \"npm modules\",\n          \"root\": {\n            \"meta\": {\n              \"name\": \"npm modules\"\n            }\n          },\n          \"items\": [\n            {\n              \"type\": \"http\",\n              \"name\": \"fakerjs\",\n              \"seq\": 1,\n              \"request\": {\n                \"url\": \"{{host}}/api/echo/json\",\n                \"method\": \"POST\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"json\",\n                  \"json\": \"{\\n  \\\"hello\\\": \\\"bruno\\\"\\n}\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {\n                  \"req\": \"const { faker } = require('@faker-js/faker');\\nconst uuid = faker.string.uuid();\\n\\nconst data = req.getBody();\\ndata.uuid = uuid;\\n\\nreq.setBody(data);\"\n                },\n                \"vars\": {},\n                \"assertions\": [\n                  {\n                    \"name\": \"res.status\",\n                    \"value\": \"eq 200\",\n                    \"enabled\": true,\n                    \"uid\": \"ZVuM9BByoo5XFCtUYTwfP\"\n                  }\n                ],\n                \"tests\": \"test(\\\"should return json\\\", function() {\\n  const data = res.getBody();\\n  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\\n  const isUUID = (inputString) => {\\n    return uuidRegex.test(inputString);\\n  };\\n  \\n  expect(data.hello).to.equal(\\\"bruno\\\");\\n  expect(isUUID(data.uuid)).to.be.true;\\n});\\n\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"none\"\n                }\n              }\n            }\n          ]\n        }\n      ]\n    },\n    {\n      \"type\": \"folder\",\n      \"name\": \"string interpolation\",\n      \"root\": {\n        \"request\": {\n          \"vars\": {\n            \"req\": [\n              {\n                \"name\": \"folder_pre_var\",\n                \"value\": \"folder_pre_var_value\",\n                \"enabled\": true,\n                \"local\": false,\n                \"uid\": \"OHd64NVOj1HQV2PLqRzy8\"\n              },\n              {\n                \"name\": \"folder_pre_var_2\",\n                \"value\": \"{{env.var1}}\",\n                \"enabled\": true,\n                \"local\": false,\n                \"uid\": \"J12VEAvPGi3R0wBKXy2jK\"\n              }\n            ]\n          }\n        },\n        \"meta\": {\n          \"name\": \"string interpolation\"\n        }\n      },\n      \"items\": [\n        {\n          \"type\": \"http\",\n          \"name\": \"env vars\",\n          \"seq\": 2,\n          \"request\": {\n            \"url\": \"{{host}}/api/echo/json\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"json\",\n              \"json\": \"{\\n  \\\"envVar1\\\": \\\"{{env.var1}}\\\",\\n  \\\"envVar2\\\": \\\"{{env-var2}}\\\"\\n}\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {},\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"eq 200\",\n                \"enabled\": true,\n                \"uid\": \"mxXvAcLVxfpRpGqh76ugy\"\n              }\n            ],\n            \"tests\": \"test(\\\"should return json\\\", function() {\\n  expect(res.getBody()).to.eql({\\n    \\\"envVar1\\\": \\\"envVar1\\\",\\n    \\\"envVar2\\\": \\\"envVar2\\\"\\n  });\\n});\\n\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"missing values\",\n          \"seq\": 1,\n          \"request\": {\n            \"url\": \"{{host}}/api/echo/json?foo={{undefinedVar}}\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [\n              {\n                \"name\": \"foo\",\n                \"value\": \"{{undefinedVar}}\",\n                \"type\": \"query\",\n                \"enabled\": true\n              }\n            ],\n            \"body\": {\n              \"mode\": \"json\",\n              \"json\": \"{\\n  \\\"hello\\\": \\\"{{undefinedVar2}}\\\"\\n}\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {},\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"eq 200\",\n                \"enabled\": true,\n                \"uid\": \"nzPl7aN2MK9uc3SKEepG0\"\n              }\n            ],\n            \"tests\": \"test(\\\"should return json\\\", function() {\\n  const url = req.getUrl();\\n  const query = url.split(\\\"?\\\")[1];\\n  expect(query).to.equal(\\\"foo={{undefinedVar}}\\\");\\n\\n  const data = res.getBody();\\n  expect(res.getBody()).to.eql({\\n    \\\"hello\\\": \\\"{{undefinedVar2}}\\\"\\n  });\\n});\\n\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"process env vars\",\n          \"seq\": 4,\n          \"request\": {\n            \"url\": \"{{host}}/api/echo/json\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"json\",\n              \"json\": \"{\\n  \\\"bark\\\": \\\"{{bark}}\\\",\\n  \\\"bark2\\\": \\\"{{process.env.PROC_ENV_VAR}}\\\"\\n}\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {},\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"eq 200\",\n                \"enabled\": true,\n                \"uid\": \"T1m34pWveQfE4Aao7Xqlt\"\n              }\n            ],\n            \"tests\": \"test(\\\"should return json\\\", function() {\\n  expect(res.getBody()).to.eql({\\n    \\\"bark\\\": \\\"woof\\\",\\n    \\\"bark2\\\": \\\"woof\\\"\\n  });\\n});\\n\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"runtime vars\",\n          \"seq\": 3,\n          \"request\": {\n            \"url\": \"{{host}}/api/echo/text\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"text\",\n              \"json\": \"{\\n  \\\"envVar1\\\": \\\"{{env.var1}}\\\",\\n  \\\"envVar2\\\": \\\"{{env-var2}}\\\"\\n}\",\n              \"text\": \"Hi, I am {{rUser.full_name}},\\nI am {{rUser.age}} years old.\\nMy favorite food is {{rUser.fav-food[0]}} and {{rUser.fav-food[1]}}.\\nI like attention: {{rUser.want.attention}}\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {\n              \"req\": \"bru.setVar(\\\"rUser\\\", {\\n  full_name: 'Bruno',\\n  age: 4,\\n  'fav-food': ['egg', 'meat'],\\n  'want.attention': true\\n});\"\n            },\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"eq 200\",\n                \"enabled\": true,\n                \"uid\": \"xe4IAIu4EXYOYiXmKU374\"\n              }\n            ],\n            \"tests\": \"test(\\\"should return json\\\", function() {\\n  const expectedResponse = `Hi, I am Bruno,\\nI am 4 years old.\\nMy favorite food is egg and meat.\\nI like attention: true`;\\n  expect(res.getBody()).to.equal(expectedResponse);\\n});\\n\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        }\n      ]\n    }\n  ],\n  \"activeEnvironmentUid\": \"s4jJkWbb9017JXdVqOxLR\",\n  \"environments\": [\n    {\n      \"variables\": [\n        {\n          \"name\": \"host\",\n          \"value\": \"http://localhost:8080\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"bearer_auth_token\",\n          \"value\": \"your_secret_token\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"basic_auth_password\",\n          \"value\": \"della\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"client_id\",\n          \"value\": \"client_id_1\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"client_secret\",\n          \"value\": \"client_secret_1\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"auth_url\",\n          \"value\": \"http://localhost:8080/api/auth/oauth2/authorization_code/authorize\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"callback_url\",\n          \"value\": \"http://localhost:8080/api/auth/oauth2/authorization_code/callback\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"access_token_url\",\n          \"value\": \"http://localhost:8080/api/auth/oauth2/authorization_code/token\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"passwordCredentials_username\",\n          \"value\": \"foo\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"passwordCredentials_password\",\n          \"value\": \"bar\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"github_authorize_url\",\n          \"value\": \"https://github.com/login/oauth/authorize\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"github_access_token_url\",\n          \"value\": \"https://github.com/login/oauth/access_token\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"google_auth_url\",\n          \"value\": \"https://accounts.google.com/o/oauth2/auth\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"google_access_token_url\",\n          \"value\": \"https://accounts.google.com/o/oauth2/token\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"google_scope\",\n          \"value\": \"https://www.googleapis.com/auth/userinfo.email\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"github_client_secret\",\n          \"value\": \"\",\n          \"enabled\": true,\n          \"secret\": true,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"github_client_id\",\n          \"value\": \"\",\n          \"enabled\": true,\n          \"secret\": true,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"google_client_id\",\n          \"value\": \"\",\n          \"enabled\": true,\n          \"secret\": true,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"google_client_secret\",\n          \"value\": \"\",\n          \"enabled\": true,\n          \"secret\": true,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"github_authorization_code\",\n          \"value\": \"\",\n          \"enabled\": true,\n          \"secret\": true,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"passwordCredentials_access_token\",\n          \"value\": \"\",\n          \"enabled\": true,\n          \"secret\": true,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"client_credentials_access_token\",\n          \"value\": \"\",\n          \"enabled\": true,\n          \"secret\": true,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"authorization_code_access_token\",\n          \"value\": \"\",\n          \"enabled\": true,\n          \"secret\": true,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"github_access_token\",\n          \"value\": \"\",\n          \"enabled\": true,\n          \"secret\": true,\n          \"type\": \"text\"\n        }\n      ],\n      \"name\": \"Local\"\n    },\n    {\n      \"variables\": [\n        {\n          \"name\": \"host\",\n          \"value\": \"https://testbench-sanity.usebruno.com\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"bearer_auth_token\",\n          \"value\": \"your_secret_token\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"basic_auth_password\",\n          \"value\": \"della\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"env.var1\",\n          \"value\": \"envVar1\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"env-var2\",\n          \"value\": \"envVar2\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"bark\",\n          \"value\": \"{{process.env.PROC_ENV_VAR}}\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"foo\",\n          \"value\": \"bar\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"testSetEnvVar\",\n          \"value\": \"bruno-29653\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"echo-host\",\n          \"value\": \"https://echo.usebruno.com\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        }\n      ],\n      \"name\": \"Prod\"\n    }\n  ],\n  \"root\": {\n    \"request\": {\n      \"auth\": {\n        \"mode\": \"bearer\",\n        \"bearer\": {\n          \"token\": \"{{bearer_auth_token}}\"\n        }\n      },\n      \"headers\": [\n        {\n          \"name\": \"check\",\n          \"value\": \"again\",\n          \"enabled\": true,\n          \"uid\": \"wbTRFykhPHZwnzVUTd1gr\"\n        },\n        {\n          \"name\": \"token\",\n          \"value\": \"{{collection_pre_var_token}}\",\n          \"enabled\": true,\n          \"uid\": \"YGZ16VXf9NusINngKeXqn\"\n        }\n      ],\n      \"vars\": {\n        \"req\": [\n          {\n            \"name\": \"collection_pre_var\",\n            \"value\": \"collection_pre_var_value\",\n            \"enabled\": true,\n            \"local\": false,\n            \"uid\": \"HI7DgTPA1gBLB6lIl1t3O\"\n          },\n          {\n            \"name\": \"collection_pre_var_token\",\n            \"value\": \"{{request_pre_var_token}}\",\n            \"enabled\": true,\n            \"local\": false,\n            \"uid\": \"FoDj77i1KoZ6Koq9oavPy\"\n          }\n        ]\n      }\n    },\n    \"docs\": \"# bruno-testbench 🐶\\n\\nThis is a test collection that I am using to test various functionalities around bruno\"\n  },\n  \"brunoConfig\": {\n    \"version\": \"1\",\n    \"name\": \"bruno-testbench\",\n    \"type\": \"collection\",\n    \"proxy\": {\n      \"enabled\": false,\n      \"protocol\": \"http\",\n      \"hostname\": \"{{proxyHostname}}\",\n      \"port\": 4000,\n      \"auth\": {\n        \"enabled\": false,\n        \"username\": \"anoop\"\n      },\n      \"bypassProxy\": \"\"\n    },\n    \"scripts\": {\n      \"moduleWhitelist\": [\n        \"crypto\",\n        \"buffer\",\n        \"form-data\"\n      ]\n    },\n    \"clientCertificates\": {\n      \"enabled\": true,\n      \"certs\": []\n    },\n    \"presets\": {\n      \"requestType\": \"http\",\n      \"requestUrl\": \"http://localhost:6000\"\n    },\n    \"ignore\": [\n      \"node_modules\",\n      \".git\"\n    ],\n    \"size\": 0.026633262634277344,\n    \"filesCount\": 96\n  }\n}"
  },
  {
    "path": "tests/import/bruno/fixtures/bruno-testbench.json",
    "content": "{\n  \"name\": \"bruno-testbench\",\n  \"version\": \"1\",\n  \"items\": [\n    {\n      \"type\": \"http\",\n      \"name\": \"aaaaa\",\n      \"seq\": 2,\n      \"request\": {\n        \"url\": \"https://reqres.in/api/users/1\",\n        \"method\": \"PUT\",\n        \"headers\": [\n          {\n            \"name\": \"Accept\",\n            \"value\": \"application/json\",\n            \"enabled\": true\n          },\n          {\n            \"name\": \"Cookie\",\n            \"value\": \"session-id=abc123\",\n            \"enabled\": true\n          }\n        ],\n        \"params\": [],\n        \"body\": {\n          \"mode\": \"none\",\n          \"formUrlEncoded\": [],\n          \"multipartForm\": [],\n          \"file\": []\n        },\n        \"script\": {\n          \"req\": \"console.log(req.getCookie());\"\n        },\n        \"vars\": {},\n        \"assertions\": [],\n        \"tests\": \"\",\n        \"docs\": \"\",\n        \"auth\": {\n          \"mode\": \"none\"\n        }\n      }\n    },\n    {\n      \"type\": \"folder\",\n      \"name\": \"auth\",\n      \"root\": {\n        \"meta\": {\n          \"name\": \"auth\"\n        }\n      },\n      \"items\": [\n        {\n          \"type\": \"folder\",\n          \"name\": \"basic\",\n          \"root\": {\n            \"meta\": {\n              \"name\": \"basic\"\n            }\n          },\n          \"items\": [\n            {\n              \"type\": \"folder\",\n              \"name\": \"via auth\",\n              \"root\": {\n                \"meta\": {\n                  \"name\": \"via auth\"\n                }\n              },\n              \"items\": [\n                {\n                  \"type\": \"http\",\n                  \"name\": \"Basic Auth 200\",\n                  \"seq\": 1,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/auth/basic/protected\",\n                    \"method\": \"POST\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"json\",\n                      \"json\": \"\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"200\",\n                        \"enabled\": true,\n                        \"uid\": \"YLpcflD1RLvdkSSvAYimh\"\n                      },\n                      {\n                        \"name\": \"res.body.message\",\n                        \"value\": \"Authentication successful\",\n                        \"enabled\": true,\n                        \"uid\": \"oqRPDS5d7CLIqBQ5OCEko\"\n                      }\n                    ],\n                    \"tests\": \"\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"basic\",\n                      \"basic\": {\n                        \"username\": \"bruno\",\n                        \"password\": \"{{basic_auth_password}}\"\n                      }\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"Basic Auth 400\",\n                  \"seq\": 2,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/auth/basic/protected\",\n                    \"method\": \"POST\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"json\",\n                      \"json\": \"\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"401\",\n                        \"enabled\": true,\n                        \"uid\": \"WsBvjaJuowT05ri9A8Qc5\"\n                      },\n                      {\n                        \"name\": \"res.body\",\n                        \"value\": \"Unauthorized\",\n                        \"enabled\": true,\n                        \"uid\": \"VW1wyd6hu74Yyfzhn0RuQ\"\n                      }\n                    ],\n                    \"tests\": \"\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                }\n              ]\n            },\n            {\n              \"type\": \"folder\",\n              \"name\": \"via script\",\n              \"root\": {\n                \"meta\": {\n                  \"name\": \"via script\"\n                }\n              },\n              \"items\": [\n                {\n                  \"type\": \"http\",\n                  \"name\": \"Basic Auth 200\",\n                  \"seq\": 1,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/auth/basic/protected\",\n                    \"method\": \"POST\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"json\",\n                      \"json\": \"\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"const username = \\\"bruno\\\";\\nconst password = \\\"della\\\";\\n\\nconst authString = `${username}:${password}`;\\nconst encodedAuthString = require('btoa')(authString);\\n\\nreq.setHeader(\\\"Authorization\\\", `Basic ${encodedAuthString}`);\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"5p6vUUMuLxbA7KnYnXrkT\"\n                      },\n                      {\n                        \"name\": \"res.body.message\",\n                        \"value\": \"Authentication successful\",\n                        \"enabled\": true,\n                        \"uid\": \"Tk43KT6Hyf3h8Jfeyk2XD\"\n                      }\n                    ],\n                    \"tests\": \"\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"Basic Auth 401\",\n                  \"seq\": 2,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/auth/basic/protected\",\n                    \"method\": \"POST\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"json\",\n                      \"json\": \"\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"const username = \\\"bruno\\\";\\nconst password = \\\"invalid\\\";\\n\\nconst authString = `${username}:${password}`;\\nconst encodedAuthString = require('btoa')(authString);\\n\\nreq.setHeader(\\\"Authorization\\\", `Basic ${encodedAuthString}`);\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"401\",\n                        \"enabled\": true,\n                        \"uid\": \"dLnctBQFSISlaooCYzp5C\"\n                      },\n                      {\n                        \"name\": \"res.body\",\n                        \"value\": \"Unauthorized\",\n                        \"enabled\": true,\n                        \"uid\": \"iWPqym01ksxrDV1gfuhWv\"\n                      }\n                    ],\n                    \"tests\": \"\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"type\": \"folder\",\n          \"name\": \"bearer\",\n          \"root\": {\n            \"meta\": {\n              \"name\": \"bearer\"\n            }\n          },\n          \"items\": [\n            {\n              \"type\": \"folder\",\n              \"name\": \"via auth\",\n              \"root\": {\n                \"meta\": {\n                  \"name\": \"via auth\"\n                }\n              },\n              \"items\": [\n                {\n                  \"type\": \"http\",\n                  \"name\": \"Bearer Auth 200\",\n                  \"seq\": 1,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/auth/bearer/protected\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"res\": \"bru.setEnvVar(\\\"foo\\\", \\\"bar\\\");\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"200\",\n                        \"enabled\": true,\n                        \"uid\": \"F01gjRjDDQefuLn2Vcyed\"\n                      },\n                      {\n                        \"name\": \"res.body.message\",\n                        \"value\": \"Authentication successful\",\n                        \"enabled\": true,\n                        \"uid\": \"Hmw3BpVyz9tDBEcdA88O0\"\n                      }\n                    ],\n                    \"tests\": \"\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"bearer\",\n                      \"bearer\": {\n                        \"token\": \"{{bearer_auth_token}}\"\n                      }\n                    }\n                  }\n                }\n              ]\n            },\n            {\n              \"type\": \"folder\",\n              \"name\": \"via headers\",\n              \"root\": {\n                \"meta\": {\n                  \"name\": \"via headers\"\n                }\n              },\n              \"items\": [\n                {\n                  \"type\": \"http\",\n                  \"name\": \"Bearer Auth 200\",\n                  \"seq\": 1,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/auth/bearer/protected\",\n                    \"method\": \"GET\",\n                    \"headers\": [\n                      {\n                        \"name\": \"Authorization\",\n                        \"value\": \"Bearer your_secret_token\",\n                        \"enabled\": true\n                      }\n                    ],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"json\",\n                      \"json\": \"\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"res\": \"bru.setEnvVar(\\\"foo\\\", \\\"bar\\\");\"\n                    },\n                    \"vars\": {\n                      \"req\": [\n                        {\n                          \"name\": \"a-c\",\n                          \"value\": \"foo\",\n                          \"enabled\": true,\n                          \"local\": false\n                        }\n                      ]\n                    },\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"200\",\n                        \"enabled\": true,\n                        \"uid\": \"VDQ7l9zN9WfS3lEGsJ0aw\"\n                      },\n                      {\n                        \"name\": \"res.body.message\",\n                        \"value\": \"Authentication successful\",\n                        \"enabled\": true,\n                        \"uid\": \"0n8UMinkhzOuRJCCABVz9\"\n                      }\n                    ],\n                    \"tests\": \"\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"type\": \"folder\",\n          \"name\": \"cookie\",\n          \"root\": {\n            \"meta\": {\n              \"name\": \"cookie\"\n            }\n          },\n          \"items\": [\n            {\n              \"type\": \"http\",\n              \"name\": \"Check\",\n              \"seq\": 2,\n              \"request\": {\n                \"url\": \"{{host}}/api/auth/cookie/protected\",\n                \"method\": \"GET\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"none\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {},\n                \"vars\": {},\n                \"assertions\": [],\n                \"tests\": \"\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"none\"\n                }\n              }\n            },\n            {\n              \"type\": \"http\",\n              \"name\": \"Login\",\n              \"seq\": 1,\n              \"request\": {\n                \"url\": \"{{host}}/api/auth/cookie/login\",\n                \"method\": \"POST\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"none\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {},\n                \"vars\": {},\n                \"assertions\": [],\n                \"tests\": \"\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"none\"\n                }\n              }\n            }\n          ]\n        },\n        {\n          \"type\": \"folder\",\n          \"name\": \"inherit auth\",\n          \"root\": {\n            \"meta\": {\n              \"name\": \"inherit auth\"\n            }\n          },\n          \"items\": [\n            {\n              \"type\": \"http\",\n              \"name\": \"inherit Bearer Auth 200\",\n              \"seq\": 2,\n              \"request\": {\n                \"url\": \"{{host}}/api/auth/bearer/protected\",\n                \"method\": \"GET\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"none\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {\n                  \"res\": \"bru.setEnvVar(\\\"foo\\\", \\\"bar\\\");\"\n                },\n                \"vars\": {},\n                \"assertions\": [\n                  {\n                    \"name\": \"res.status\",\n                    \"value\": \"200\",\n                    \"enabled\": true,\n                    \"uid\": \"G6vVLMAqfvpa4aBtQ3WSG\"\n                  },\n                  {\n                    \"name\": \"res.body.message\",\n                    \"value\": \"Authentication successful\",\n                    \"enabled\": true,\n                    \"uid\": \"QItzfaevVVrycFox5jTlS\"\n                  }\n                ],\n                \"tests\": \"\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"inherit\"\n                }\n              }\n            }\n          ]\n        }\n      ]\n    },\n    {\n      \"type\": \"folder\",\n      \"name\": \"echo\",\n      \"root\": {\n        \"meta\": {\n          \"name\": \"echo\"\n        }\n      },\n      \"items\": [\n        {\n          \"type\": \"http\",\n          \"name\": \"echo bigint\",\n          \"seq\": 6,\n          \"request\": {\n            \"url\": \"{{host}}/api/echo/json\",\n            \"method\": \"POST\",\n            \"headers\": [\n              {\n                \"name\": \"foo\",\n                \"value\": \"bar\",\n                \"enabled\": true\n              }\n            ],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"json\",\n              \"json\": \"{\\n  \\\"hello\\\": 990531470713421825,\\n  \\\"decimal\\\": 1.0,\\n  \\\"decimal2\\\": 1.00,\\n  \\\"decimal3\\\": 1.00200,\\n  \\\"decimal4\\\": 0.00\\n}\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {},\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"eq 200\",\n                \"enabled\": true,\n                \"uid\": \"lCoGpuLOpVmXPfGzJqbTB\"\n              }\n            ],\n            \"tests\": \"// todo: add tests once lossless json echo server is ready\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"echo bom json\",\n          \"seq\": 1,\n          \"request\": {\n            \"url\": \"{{host}}/api/echo/bom-json-test\",\n            \"method\": \"GET\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"none\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {},\n            \"vars\": {},\n            \"assertions\": [],\n            \"tests\": \"\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"echo form-url-encoded\",\n          \"seq\": 9,\n          \"request\": {\n            \"url\": \"{{echo-host}}\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"formUrlEncoded\",\n              \"formUrlEncoded\": [\n                {\n                  \"name\": \"form-data-key\",\n                  \"value\": \"{{form-data-key}}\",\n                  \"enabled\": true\n                },\n                {\n                  \"name\": \"form-data-stringified-object\",\n                  \"value\": \"{{form-data-stringified-object}}\",\n                  \"enabled\": true\n                }\n              ],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {\n              \"req\": \"let obj = JSON.stringify({foo:123});\\nbru.setVar('form-data-key', 'form-data-value');\\nbru.setVar('form-data-stringified-object', obj);\"\n            },\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.body\",\n                \"value\": \"eq form-data-key=form-data-value&form-data-stringified-object=%7B%22foo%22%3A123%7D\",\n                \"enabled\": true,\n                \"uid\": \"V0MSBvq2iFun9gIWfgqtQ\"\n              }\n            ],\n            \"tests\": \"\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"echo json\",\n          \"seq\": 2,\n          \"request\": {\n            \"url\": \"{{host}}/api/echo/json\",\n            \"method\": \"POST\",\n            \"headers\": [\n              {\n                \"name\": \"foo\",\n                \"value\": \"bar\",\n                \"enabled\": true\n              }\n            ],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"json\",\n              \"json\": \"{\\n  \\\"hello\\\": \\\"bruno\\\"\\n}\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {\n              \"req\": \"bru.setVar(\\\"foo\\\", \\\"foo-world-2\\\");\"\n            },\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"eq 200\",\n                \"enabled\": true,\n                \"uid\": \"FFVx1w4MstKeQfQR66Xy8\"\n              }\n            ],\n            \"tests\": \"test(\\\"should return json\\\", function() {\\n  const data = res.getBody();\\n  expect(res.getBody()).to.eql({\\n    \\\"hello\\\": \\\"bruno\\\"\\n  });\\n});\\n\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"echo multipart via scripting\",\n          \"seq\": 10,\n          \"request\": {\n            \"url\": \"{{echo-host}}\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"multipartForm\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {\n              \"req\": \"const FormData = require(\\\"form-data\\\");\\nconst form = new FormData();\\nform.append('form-data-key', 'form-data-value');\\nreq.setBody(form);\"\n            },\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.body\",\n                \"value\": \"contains form-data-value\",\n                \"enabled\": true,\n                \"uid\": \"USCnLx51IlWz6HrLxlR1r\"\n              }\n            ],\n            \"tests\": \"\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"echo multipart\",\n          \"seq\": 8,\n          \"request\": {\n            \"url\": \"{{echo-host}}\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"multipartForm\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [\n                {\n                  \"type\": \"text\",\n                  \"name\": \"form-data-key\",\n                  \"value\": \"{{form-data-key}}\",\n                  \"enabled\": true\n                },\n                {\n                  \"type\": \"text\",\n                  \"name\": \"form-data-stringified-object\",\n                  \"value\": \"{{form-data-stringified-object}}\",\n                  \"enabled\": true\n                },\n                {\n                  \"type\": \"file\",\n                  \"name\": \"file\",\n                  \"value\": [\n                    \"bruno.png\"\n                  ],\n                  \"enabled\": true\n                }\n              ],\n              \"file\": []\n            },\n            \"script\": {\n              \"req\": \"let obj = JSON.stringify({foo:123});\\nbru.setVar('form-data-key', 'form-data-value');\\nbru.setVar('form-data-stringified-object', obj);\"\n            },\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.body\",\n                \"value\": \"contains form-data-value\",\n                \"enabled\": true,\n                \"uid\": \"L5wrs8CJKD7skDazamdTZ\"\n              },\n              {\n                \"name\": \"res.body\",\n                \"value\": \"contains {\\\"foo\\\":123}\",\n                \"enabled\": true,\n                \"uid\": \"2rPiaUFbPuWPq0ew6dqVd\"\n              }\n            ],\n            \"tests\": \"\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"echo plaintext\",\n          \"seq\": 3,\n          \"request\": {\n            \"url\": \"{{host}}/api/echo/text\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"text\",\n              \"text\": \"hello\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {},\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"eq 200\",\n                \"enabled\": true,\n                \"uid\": \"cW2RamEi0zqLmn84SjUoh\"\n              }\n            ],\n            \"tests\": \"test(\\\"should return plain text\\\", function() {\\n  const data = res.getBody();\\n  expect(res.getBody()).to.eql(\\\"hello\\\");\\n});\\n\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"echo xml parsed-self closing tags-\",\n          \"seq\": 6,\n          \"request\": {\n            \"url\": \"{{host}}/api/echo/xml-parsed\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"xml\",\n              \"xml\": \"<hello>\\n  <world>bruno</world>\\n  <world/>\\n</hello>\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {},\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"eq 200\",\n                \"enabled\": true,\n                \"uid\": \"lIbw7OdlPxbNUKdShGNvi\"\n              }\n            ],\n            \"tests\": \"test(\\\"should return parsed xml\\\", function() {\\n  const data = res.getBody();\\n  expect(res.getBody()).to.eql({\\n    \\\"hello\\\": {\\n      \\\"world\\\": [\\n        \\\"bruno\\\",\\n        \\\"\\\"\\n      ]\\n    }\\n  });\\n});\\n\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"echo xml parsed\",\n          \"seq\": 4,\n          \"request\": {\n            \"url\": \"{{host}}/api/echo/xml-parsed\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"xml\",\n              \"xml\": \"<hello>\\n  <world>bruno</world>\\n</hello>\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {},\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"eq 200\",\n                \"enabled\": true,\n                \"uid\": \"5yr5fbjrAre0Cp0C3PY4c\"\n              }\n            ],\n            \"tests\": \"test(\\\"should return parsed xml\\\", function() {\\n  const data = res.getBody();\\n  expect(res.getBody()).to.eql({\\n    \\\"hello\\\": {\\n      \\\"world\\\": [\\\"bruno\\\"]\\n    }\\n  });\\n});\\n\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"echo xml raw\",\n          \"seq\": 5,\n          \"request\": {\n            \"url\": \"{{host}}/api/echo/xml-raw\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"xml\",\n              \"xml\": \"<hello><world>bruno</world></hello>\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {},\n            \"vars\": {},\n            \"assertions\": [],\n            \"tests\": \"\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        }\n      ]\n    },\n    {\n      \"type\": \"folder\",\n      \"name\": \"graphql\",\n      \"root\": {\n        \"meta\": {\n          \"name\": \"graphql\"\n        }\n      },\n      \"items\": [\n        {\n          \"type\": \"graphql\",\n          \"name\": \"spacex\",\n          \"seq\": 1,\n          \"request\": {\n            \"url\": \"https://spacex-production.up.railway.app/\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"graphql\",\n              \"graphql\": {\n                \"query\": \"{\\n  company {\\n    ceo\\n  }\\n}\\n\"\n              },\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {},\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"eq 200\",\n                \"enabled\": true,\n                \"uid\": \"P99wa88sX4L4Zat94bHzz\"\n              }\n            ],\n            \"tests\": \"\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        }\n      ]\n    },\n    {\n      \"type\": \"folder\",\n      \"name\": \"lib\",\n      \"root\": {\n        \"meta\": {\n          \"name\": \"lib\"\n        }\n      }\n    },\n    {\n      \"type\": \"http\",\n      \"name\": \"ping\",\n      \"seq\": 1,\n      \"request\": {\n        \"url\": \"{{host}}/ping\",\n        \"method\": \"GET\",\n        \"headers\": [],\n        \"params\": [],\n        \"body\": {\n          \"mode\": \"none\",\n          \"formUrlEncoded\": [],\n          \"multipartForm\": [],\n          \"file\": []\n        },\n        \"script\": {},\n        \"vars\": {},\n        \"assertions\": [],\n        \"tests\": \"\",\n        \"docs\": \"\",\n        \"auth\": {\n          \"mode\": \"none\"\n        }\n      }\n    },\n    {\n      \"type\": \"folder\",\n      \"name\": \"preview\",\n      \"root\": {\n        \"meta\": {\n          \"name\": \"preview\"\n        }\n      },\n      \"items\": [\n        {\n          \"type\": \"folder\",\n          \"name\": \"html\",\n          \"root\": {\n            \"meta\": {\n              \"name\": \"html\"\n            }\n          },\n          \"items\": [\n            {\n              \"type\": \"http\",\n              \"name\": \"bruno\",\n              \"seq\": 1,\n              \"request\": {\n                \"url\": \"https://www.github.com\",\n                \"method\": \"GET\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"none\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {\n                  \"req\": \"console.log(req.getCookie());\\n\\nconsole.log(req.getHeaders());\"\n                },\n                \"vars\": {},\n                \"assertions\": [\n                  {\n                    \"name\": \"res.status\",\n                    \"value\": \"eq 200\",\n                    \"enabled\": true,\n                    \"uid\": \"2bpXrsR3q5MBpkXbO1vfS\"\n                  }\n                ],\n                \"tests\": \"test(\\\"should return parsed xml\\\", function() {\\n  const headers = res.getHeaders();\\n  expect(headers['content-type']).to.eql(\\\"text/html; charset=utf-8\\\");\\n});\\n\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"none\"\n                }\n              }\n            }\n          ]\n        },\n        {\n          \"type\": \"folder\",\n          \"name\": \"image\",\n          \"root\": {\n            \"meta\": {\n              \"name\": \"image\"\n            }\n          },\n          \"items\": [\n            {\n              \"type\": \"http\",\n              \"name\": \"bruno\",\n              \"seq\": 1,\n              \"request\": {\n                \"url\": \"https://www.usebruno.com/images/landing-2.png\",\n                \"method\": \"GET\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"none\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {},\n                \"vars\": {},\n                \"assertions\": [],\n                \"tests\": \"test(\\\"should return parsed xml\\\", function() {\\n  const headers = res.getHeaders();\\n  expect(headers['content-type']).to.eql(\\\"image/png\\\");\\n});\\n\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"none\"\n                }\n              }\n            }\n          ]\n        }\n      ]\n    },\n    {\n      \"type\": \"folder\",\n      \"name\": \"redirects\",\n      \"root\": {\n        \"meta\": {\n          \"name\": \"redirects\"\n        }\n      },\n      \"items\": [\n        {\n          \"type\": \"http\",\n          \"name\": \"Disable Redirect\",\n          \"seq\": 1,\n          \"request\": {\n            \"url\": \"{{host}}/redirect-to-ping\",\n            \"method\": \"GET\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"none\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {\n              \"req\": \"req.setMaxRedirects(0);\"\n            },\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"302\",\n                \"enabled\": true,\n                \"uid\": \"Bs0jGEFFNAyyRBx6DILEN\"\n              }\n            ],\n            \"tests\": \"test(\\\"should disable redirect to ping\\\", function() {\\n  const data = res.getBody();\\n  expect(data).to.equal('Found. Redirecting to /ping');\\n});\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"Test Redirect\",\n          \"seq\": 2,\n          \"request\": {\n            \"url\": \"{{host}}/redirect-to-ping\",\n            \"method\": \"GET\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"none\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {},\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"200\",\n                \"enabled\": true,\n                \"uid\": \"nNRcxeANwM6VBEXR1qoM0\"\n              },\n              {\n                \"name\": \"res.body\",\n                \"value\": \"pong\",\n                \"enabled\": true,\n                \"uid\": \"3Y5SHtNsQHK0glgikD1IU\"\n              }\n            ],\n            \"tests\": \"test(\\\"should redirect to ping\\\", function() {\\n  const data = res.getBody();\\n  expect(data).to.equal('pong');\\n});\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        }\n      ]\n    },\n    {\n      \"type\": \"folder\",\n      \"name\": \"scripting\",\n      \"root\": {\n        \"meta\": {\n          \"name\": \"scripting\"\n        }\n      },\n      \"items\": [\n        {\n          \"type\": \"folder\",\n          \"name\": \"api\",\n          \"root\": {\n            \"meta\": {\n              \"name\": \"api\"\n            }\n          },\n          \"items\": [\n            {\n              \"type\": \"folder\",\n              \"name\": \"bru\",\n              \"root\": {\n                \"meta\": {\n                  \"name\": \"bru\"\n                }\n              },\n              \"items\": [\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getEnvName\",\n                  \"seq\": 1,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"const envName = bru.getEnvName();\\nbru.setVar(\\\"testEnvName\\\", envName);\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [],\n                    \"tests\": \"test(\\\"should get env name in scripts\\\", function() {\\n  const testEnvName = bru.getVar(\\\"testEnvName\\\");\\n  expect(testEnvName).to.equal(\\\"Prod\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getEnvVar\",\n                  \"seq\": 2,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [],\n                    \"tests\": \"test(\\\"should get env var in scripts\\\", function() {\\n  const host = bru.getEnvVar(\\\"host\\\")\\n  expect(host).to.equal(\\\"https://testbench-sanity.usebruno.com\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getProcessEnv\",\n                  \"seq\": 6,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [],\n                    \"tests\": \"test(\\\"bru.getProcessEnv()\\\", function() {\\n  const v = bru.getProcessEnv(\\\"PROC_ENV_VAR\\\");\\n  expect(v).to.equal(\\\"woof\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getVar\",\n                  \"seq\": 5,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [],\n                    \"tests\": \"test(\\\"should get var in scripts\\\", function() {\\n  const testSetVar = bru.getVar(\\\"testSetVar\\\");\\n  expect(testSetVar).to.equal(\\\"bruno-test-87267\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"setEnvVar\",\n                  \"seq\": 3,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"res\": \"bru.setEnvVar(\\\"testSetEnvVar\\\", \\\"bruno-29653\\\")\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [],\n                    \"tests\": \"test(\\\"should set env var in scripts\\\", function() {\\n  const testSetEnvVar = bru.getEnvVar(\\\"testSetEnvVar\\\")\\n  expect(testSetEnvVar).to.equal(\\\"bruno-29653\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"setVar\",\n                  \"seq\": 4,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"res\": \"bru.setVar(\\\"testSetVar\\\", \\\"bruno-test-87267\\\")\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [],\n                    \"tests\": \"test(\\\"should get var in scripts\\\", function() {\\n  const testSetVar = bru.getVar(\\\"testSetVar\\\");\\n  expect(testSetVar).to.equal(\\\"bruno-test-87267\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                }\n              ]\n            },\n            {\n              \"type\": \"folder\",\n              \"name\": \"req\",\n              \"root\": {\n                \"meta\": {\n                  \"name\": \"req\"\n                }\n              },\n              \"items\": [\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getBody\",\n                  \"seq\": 9,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/echo/json\",\n                    \"method\": \"POST\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"json\",\n                      \"json\": \"{\\n  \\\"hello\\\": \\\"bruno\\\"\\n}\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"7VvXmRWwUdGYbvrQaeDMD\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"req.getBody()\\\", function() {\\n  const data = res.getBody();\\n  expect(data).to.eql({\\n    \\\"hello\\\": \\\"bruno\\\"\\n  });\\n});\\n\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getHeader\",\n                  \"seq\": 5,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [\n                      {\n                        \"name\": \"bruno\",\n                        \"value\": \"is-awesome\",\n                        \"enabled\": true\n                      }\n                    ],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"HssG2g6gUaWaBFFanCozP\"\n                      },\n                      {\n                        \"name\": \"res.body\",\n                        \"value\": \"eq pong\",\n                        \"enabled\": true,\n                        \"uid\": \"50NSDIeXgRr0TcXEdquci\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"req.getHeader(name)\\\", function() {\\n  const h = req.getHeader('bruno');\\n  expect(h).to.equal(\\\"is-awesome\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getHeaders\",\n                  \"seq\": 7,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [\n                      {\n                        \"name\": \"bruno\",\n                        \"value\": \"is-awesome\",\n                        \"enabled\": true\n                      },\n                      {\n                        \"name\": \"della\",\n                        \"value\": \"is-beautiful\",\n                        \"enabled\": true\n                      }\n                    ],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"9NDZWAvBS23WJAZsKl9SS\"\n                      },\n                      {\n                        \"name\": \"res.body\",\n                        \"value\": \"eq pong\",\n                        \"enabled\": true,\n                        \"uid\": \"f9ULUob9jYiABYuEzfmgC\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"req.getHeaders()\\\", function() {\\n  const h = req.getHeaders();\\n  expect(h.bruno).to.equal(\\\"is-awesome\\\");\\n  expect(h.della).to.equal(\\\"is-beautiful\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getMethod\",\n                  \"seq\": 3,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"EpNcKUgCYzdg8KrOLqaCX\"\n                      },\n                      {\n                        \"name\": \"res.body\",\n                        \"value\": \"eq pong\",\n                        \"enabled\": true,\n                        \"uid\": \"qFqcLtNYbZ9nqhiwvS3Qk\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"req.getMethod()()\\\", function() {\\n  const method = req.getMethod();\\n  expect(method).to.equal(\\\"GET\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getUrl\",\n                  \"seq\": 1,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"VaiDs2JU1NM8prTc59GdX\"\n                      },\n                      {\n                        \"name\": \"res.body\",\n                        \"value\": \"eq pong\",\n                        \"enabled\": true,\n                        \"uid\": \"4vC9zp5XBajbYKDYM4oFN\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"req.getUrl()\\\", function() {\\n  const url = req.getUrl();\\n  expect(url).to.equal(\\\"https://testbench-sanity.usebruno.com/ping\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"setBody\",\n                  \"seq\": 10,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/echo/json\",\n                    \"method\": \"POST\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"json\",\n                      \"json\": \"{\\n  \\\"hello\\\": \\\"bruno\\\"\\n}\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"req.setBody({\\n  \\\"bruno\\\": \\\"is awesome\\\"\\n});\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"EGlBavIEZ2j0s2aczQxAP\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"req.setBody()\\\", function() {\\n  const data = res.getBody();\\n  expect(data).to.eql({\\n    \\\"bruno\\\": \\\"is awesome\\\"\\n  });\\n});\\n\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"setHeader\",\n                  \"seq\": 6,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [\n                      {\n                        \"name\": \"bruno\",\n                        \"value\": \"is-awesome\",\n                        \"enabled\": true\n                      }\n                    ],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"req.setHeader('bruno', 'is-the-future');\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"J9AUIh6CbTnIxlCyKqqq7\"\n                      },\n                      {\n                        \"name\": \"res.body\",\n                        \"value\": \"eq pong\",\n                        \"enabled\": true,\n                        \"uid\": \"sb6dpEtw8SYyeXVqEz3OA\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"req.setHeader(name)\\\", function() {\\n  const h = req.getHeader('bruno');\\n  expect(h).to.equal(\\\"is-the-future\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"setHeaders\",\n                  \"seq\": 8,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [\n                      {\n                        \"name\": \"bruno\",\n                        \"value\": \"is-awesome\",\n                        \"enabled\": true\n                      },\n                      {\n                        \"name\": \"della\",\n                        \"value\": \"is-beautiful\",\n                        \"enabled\": true\n                      }\n                    ],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"req.setHeaders({\\n  \\\"content-type\\\": \\\"application/text\\\",\\n  \\\"transaction-id\\\": \\\"foobar\\\"\\n});\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"1djJxGMAwmAHF2ruhdQQO\"\n                      },\n                      {\n                        \"name\": \"res.body\",\n                        \"value\": \"eq pong\",\n                        \"enabled\": true,\n                        \"uid\": \"sqvBwQilTBWnFDoIBAC4T\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"req.setHeaders()\\\", function() {\\n  const h = req.getHeaders();\\n  expect(h['content-type']).to.equal(\\\"application/text\\\");\\n  expect(h['transaction-id']).to.equal(\\\"foobar\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"setMethod\",\n                  \"seq\": 4,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"POST\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"req.setMethod(\\\"GET\\\");\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"3eRRXHvWErUAC2IiBto7B\"\n                      },\n                      {\n                        \"name\": \"res.body\",\n                        \"value\": \"eq pong\",\n                        \"enabled\": true,\n                        \"uid\": \"UAqBwv3S2607RXzgC6S1i\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"req.setMethod()()\\\", function() {\\n  const method = req.getMethod();\\n  expect(method).to.equal(\\\"GET\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"setUrl\",\n                  \"seq\": 2,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping/invalid\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"req.setUrl(\\\"https://testbench-sanity.usebruno.com/ping\\\");\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"sGLaBON85rqipc1VP8R3W\"\n                      },\n                      {\n                        \"name\": \"res.body\",\n                        \"value\": \"eq pong\",\n                        \"enabled\": true,\n                        \"uid\": \"SPVLRHys6RQh1GbRWnPpT\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"req.setUrl()\\\", function() {\\n  const url = req.getUrl();\\n  expect(url).to.equal(\\\"https://testbench-sanity.usebruno.com/ping\\\");\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                }\n              ]\n            },\n            {\n              \"type\": \"folder\",\n              \"name\": \"res\",\n              \"root\": {\n                \"meta\": {\n                  \"name\": \"res\"\n                }\n              },\n              \"items\": [\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getBody\",\n                  \"seq\": 4,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/echo/json\",\n                    \"method\": \"POST\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"json\",\n                      \"json\": \"{\\n  \\\"hello\\\": \\\"bruno\\\"\\n}\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"rQe4gitaooPrQkEqD6AvL\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"res.getBody()\\\", function() {\\n  const data = res.getBody();\\n  expect(data).to.eql({\\n    \\\"hello\\\": \\\"bruno\\\"\\n  });\\n});\\n\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getHeader\",\n                  \"seq\": 2,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/echo/json\",\n                    \"method\": \"POST\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"json\",\n                      \"json\": \"{\\n  \\\"hello\\\": \\\"bruno\\\"\\n}\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"LOem8LkfWBML3yI1Kh3ZU\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"res.getHeader(name)\\\", function() {\\n  const server = res.getHeader('x-powered-by');\\n  expect(server).to.eql('Express');\\n});\\n\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getHeaders\",\n                  \"seq\": 3,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/echo/json\",\n                    \"method\": \"POST\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"json\",\n                      \"json\": \"{\\n  \\\"hello\\\": \\\"bruno\\\"\\n}\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"qF8ASpikHJzQLRS0ZqNkA\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"res.getHeaders(name)\\\", function() {\\n  const h = res.getHeaders();\\n  expect(h['x-powered-by']).to.eql('Express');\\n  expect(h['content-length']).to.eql('17');\\n});\\n\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getResponseTime\",\n                  \"seq\": 5,\n                  \"request\": {\n                    \"url\": \"{{host}}/api/echo/json\",\n                    \"method\": \"POST\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"json\",\n                      \"json\": \"{\\n  \\\"hello\\\": \\\"bruno\\\"\\n}\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"pqN8P939S4dfZ1Bmuemi7\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"res.getResponseTime()\\\", function() {\\n  const responseTime = res.getResponseTime();\\n  expect(typeof responseTime).to.eql(\\\"number\\\");\\n  expect(responseTime > 0).to.be.true;\\n});\\n\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                },\n                {\n                  \"type\": \"http\",\n                  \"name\": \"getStatus\",\n                  \"seq\": 1,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {},\n                    \"vars\": {},\n                    \"assertions\": [\n                      {\n                        \"name\": \"res.status\",\n                        \"value\": \"eq 200\",\n                        \"enabled\": true,\n                        \"uid\": \"0DqeIPuHtcmaULlYG9eWu\"\n                      },\n                      {\n                        \"name\": \"res.body\",\n                        \"value\": \"eq pong\",\n                        \"enabled\": true,\n                        \"uid\": \"WZDxwKadXkDkK3Ea1VMKW\"\n                      }\n                    ],\n                    \"tests\": \"test(\\\"res.getStatus()\\\", function() {\\n  const status = res.getStatus()\\n  expect(status).to.equal(200);\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"type\": \"folder\",\n          \"name\": \"inbuilt modules\",\n          \"root\": {\n            \"meta\": {\n              \"name\": \"inbuilt modules\"\n            }\n          },\n          \"items\": [\n            {\n              \"type\": \"folder\",\n              \"name\": \"axios\",\n              \"root\": {\n                \"meta\": {\n                  \"name\": \"axios\"\n                }\n              },\n              \"items\": [\n                {\n                  \"type\": \"http\",\n                  \"name\": \"axios-pre-req-script\",\n                  \"seq\": 1,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"const axios = require(\\\"axios\\\");\\n\\nconst url = \\\"https://testbench-sanity.usebruno.com/api/echo/json\\\";\\nconst response = await axios.post(url, {\\n  \\\"hello\\\": \\\"bruno\\\"\\n});\\n\\nreq.setBody(response.data);\\nreq.setMethod(\\\"POST\\\");\\nreq.setUrl(url);\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [],\n                    \"tests\": \"test(\\\"req.getBody()\\\", function() {\\n  const data = res.getBody();\\n  expect(data).to.eql({\\n    \\\"hello\\\": \\\"bruno\\\"\\n  });\\n});\\n\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                }\n              ]\n            },\n            {\n              \"type\": \"folder\",\n              \"name\": \"crypto-js\",\n              \"root\": {\n                \"meta\": {\n                  \"name\": \"crypto-js\"\n                }\n              },\n              \"items\": [\n                {\n                  \"type\": \"http\",\n                  \"name\": \"crypto-js-pre-request-script\",\n                  \"seq\": 1,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"var CryptoJS = require(\\\"crypto-js\\\");\\n\\n// Encrypt\\nvar ciphertext = CryptoJS.AES.encrypt('my message', 'secret key 123').toString();\\n\\n// Decrypt\\nvar bytes  = CryptoJS.AES.decrypt(ciphertext, 'secret key 123');\\nvar originalText = bytes.toString(CryptoJS.enc.Utf8);\\n\\nbru.setVar('crypto-test-message', originalText);\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [],\n                    \"tests\": \"test(\\\"crypto message\\\", function() {\\n  const data = bru.getVar('crypto-test-message');\\n  bru.setVar('crypto-test-message', null);\\n  expect(data).to.eql('my message');\\n});\\n\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                }\n              ]\n            },\n            {\n              \"type\": \"folder\",\n              \"name\": \"nanoid\",\n              \"root\": {\n                \"meta\": {\n                  \"name\": \"nanoid\"\n                }\n              },\n              \"items\": [\n                {\n                  \"type\": \"http\",\n                  \"name\": \"nanoid\",\n                  \"seq\": 1,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"const { nanoid } = require(\\\"nanoid\\\");\\n \\nbru.setVar(\\\"nanoid-test-id\\\", nanoid());\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [],\n                    \"tests\": \"test(\\\"nanoid var\\\", function() {\\n  const id = bru.getVar('nanoid-test-id');\\n  let isValidNanoid = /^[a-zA-Z0-9_-]{21}$/.test(id)\\n  bru.setVar('nanoid-test-id', null);\\n  expect(isValidNanoid).to.eql(true);\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                }\n              ]\n            },\n            {\n              \"type\": \"folder\",\n              \"name\": \"uuid\",\n              \"root\": {\n                \"meta\": {\n                  \"name\": \"uuid\"\n                }\n              },\n              \"items\": [\n                {\n                  \"type\": \"http\",\n                  \"name\": \"uuid\",\n                  \"seq\": 1,\n                  \"request\": {\n                    \"url\": \"{{host}}/ping\",\n                    \"method\": \"GET\",\n                    \"headers\": [],\n                    \"params\": [],\n                    \"body\": {\n                      \"mode\": \"none\",\n                      \"formUrlEncoded\": [],\n                      \"multipartForm\": [],\n                      \"file\": []\n                    },\n                    \"script\": {\n                      \"req\": \"const { v4 } = require(\\\"uuid\\\");\\n \\nbru.setVar(\\\"uuid-test-id\\\", v4());\"\n                    },\n                    \"vars\": {},\n                    \"assertions\": [],\n                    \"tests\": \"test(\\\"uuid var\\\", function() {\\n  const id = bru.getVar('uuid-test-id');\\n  let isValidUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id);\\n  bru.setVar('uuid-test-id', null);\\n  expect(isValidUuid).to.eql(true);\\n});\",\n                    \"docs\": \"\",\n                    \"auth\": {\n                      \"mode\": \"none\"\n                    }\n                  }\n                }\n              ]\n            }\n          ]\n        },\n        {\n          \"type\": \"folder\",\n          \"name\": \"js\",\n          \"root\": {\n            \"meta\": {\n              \"name\": \"js\"\n            }\n          },\n          \"items\": [\n            {\n              \"type\": \"http\",\n              \"name\": \"data types - request vars\",\n              \"seq\": 3,\n              \"request\": {\n                \"url\": \"{{host}}/api/echo/json\",\n                \"method\": \"POST\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"json\",\n                  \"json\": \"{\\n  \\\"boolean\\\": false,\\n  \\\"number_1\\\": 1,\\n  \\\"number_2\\\": 0,\\n  \\\"number_3\\\": -1,\\n  \\\"string\\\": \\\"bruno\\\",\\n  \\\"array\\\": [1, 2, 3, 4, 5],\\n  \\\"object\\\": {\\n    \\\"hello\\\": \\\"bruno\\\"\\n  },\\n  \\\"null\\\": null\\n}\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {},\n                \"vars\": {},\n                \"assertions\": [\n                  {\n                    \"name\": \"req.body.boolean\",\n                    \"value\": \"isBoolean false\",\n                    \"enabled\": true,\n                    \"uid\": \"SgXkeY8p7ahXIq2kA9FzA\"\n                  },\n                  {\n                    \"name\": \"req.body.number_1\",\n                    \"value\": \"isNumber 1\",\n                    \"enabled\": true,\n                    \"uid\": \"lhS17xvEP5jzHGP2Uqfg9\"\n                  },\n                  {\n                    \"name\": \"req.body.undefined\",\n                    \"value\": \"isUndefined undefined\",\n                    \"enabled\": true,\n                    \"uid\": \"bFTk8cAUAzNnrvUhbNeyC\"\n                  },\n                  {\n                    \"name\": \"req.body.string\",\n                    \"value\": \"isString bruno\",\n                    \"enabled\": true,\n                    \"uid\": \"ohANzzhuM8E8egvoVy20M\"\n                  },\n                  {\n                    \"name\": \"req.body.null\",\n                    \"value\": \"isNull null\",\n                    \"enabled\": true,\n                    \"uid\": \"r6W6I7ATDVswqkAf7Kl1k\"\n                  },\n                  {\n                    \"name\": \"req.body.array\",\n                    \"value\": \"isArray\",\n                    \"enabled\": true,\n                    \"uid\": \"fFUuv0vldfqaAGPjhfmdl\"\n                  },\n                  {\n                    \"name\": \"req.body.boolean\",\n                    \"value\": \"eq false\",\n                    \"enabled\": true,\n                    \"uid\": \"eXPS2R19qWsPGm6usEogu\"\n                  },\n                  {\n                    \"name\": \"req.body.number_1\",\n                    \"value\": \"eq 1\",\n                    \"enabled\": true,\n                    \"uid\": \"WCKmMIqsFPwocy6LZmCcc\"\n                  },\n                  {\n                    \"name\": \"req.body.undefined\",\n                    \"value\": \"eq undefined\",\n                    \"enabled\": true,\n                    \"uid\": \"7fJRYC8ELm68Uc5CaB7B8\"\n                  },\n                  {\n                    \"name\": \"req.body.string\",\n                    \"value\": \"eq bruno\",\n                    \"enabled\": true,\n                    \"uid\": \"fXTl58gxhAUrUM8SZFxLW\"\n                  },\n                  {\n                    \"name\": \"req.body.null\",\n                    \"value\": \"eq null\",\n                    \"enabled\": true,\n                    \"uid\": \"yUhXaWTPJaUYU4zerBABN\"\n                  },\n                  {\n                    \"name\": \"req.body.number_2\",\n                    \"value\": \"eq 0\",\n                    \"enabled\": true,\n                    \"uid\": \"WWCCm6i8GzyNBH6xiQGAP\"\n                  },\n                  {\n                    \"name\": \"req.body.number_3\",\n                    \"value\": \"eq -1\",\n                    \"enabled\": true,\n                    \"uid\": \"G73JIdrxSUDpc33EfAZGW\"\n                  },\n                  {\n                    \"name\": \"req.body.number_2\",\n                    \"value\": \"isNumber\",\n                    \"enabled\": true,\n                    \"uid\": \"Dp5pdDeMEulfPB3ZDdjl4\"\n                  },\n                  {\n                    \"name\": \"req.body.number_3\",\n                    \"value\": \"isNumber\",\n                    \"enabled\": true,\n                    \"uid\": \"fWghLNcBbslVF2OJMSS6x\"\n                  }\n                ],\n                \"tests\": \"\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"none\"\n                }\n              }\n            },\n            {\n              \"type\": \"http\",\n              \"name\": \"data types\",\n              \"seq\": 2,\n              \"request\": {\n                \"url\": \"{{host}}/api/echo/json\",\n                \"method\": \"POST\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"json\",\n                  \"json\": \"{\\n  \\\"boolean\\\": false,\\n  \\\"number\\\": 1,\\n  \\\"string\\\": \\\"bruno\\\",\\n  \\\"array\\\": [1, 2, 3, 4, 5],\\n  \\\"object\\\": {\\n    \\\"hello\\\": \\\"bruno\\\"\\n  },\\n  \\\"null\\\": null\\n}\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {\n                  \"req\": \"const reqBody = req.getBody();\\n\\nbru.setVar(\\\"dataTypeVarTest\\\", {\\n  ...reqBody,\\n  \\\"undefined\\\": undefined\\n});\"\n                },\n                \"vars\": {},\n                \"assertions\": [],\n                \"tests\": \"test(\\\"data types check via bru var\\\", function() {\\n  let v = bru.getVar(\\\"dataTypeVarTest\\\");\\n  v = {\\n    ...v,\\n    \\\"undefined\\\": undefined\\n  };\\n  expect(v).to.eql({\\n    \\\"boolean\\\": false,\\n    \\\"number\\\": 1,\\n    \\\"string\\\": \\\"bruno\\\",\\n    \\\"array\\\": [1, 2, 3, 4, 5],\\n    \\\"object\\\": {\\n      \\\"hello\\\": \\\"bruno\\\"\\n    },\\n    \\\"null\\\": null,\\n    \\\"undefined\\\": undefined\\n  })\\n});\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"none\"\n                }\n              }\n            },\n            {\n              \"type\": \"http\",\n              \"name\": \"setTimeout\",\n              \"seq\": 1,\n              \"request\": {\n                \"url\": \"{{host}}/ping\",\n                \"method\": \"GET\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"none\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {\n                  \"req\": \"bru.setVar(\\\"test-js-set-timeout\\\", \\\"\\\");\\nawait new Promise((resolve, reject) => {\\n  setTimeout(() => {\\n    bru.setVar(\\\"test-js-set-timeout\\\", \\\"bruno\\\");\\n    resolve();\\n  }, 1000);\\n});\\n\\nconst v = bru.getVar(\\\"test-js-set-timeout\\\");\\nbru.setVar(\\\"test-js-set-timeout\\\", v + \\\"-is-awesome\\\");\\n\"\n                },\n                \"vars\": {},\n                \"assertions\": [],\n                \"tests\": \"test(\\\"setTimeout()\\\", function() {\\n  const v = bru.getVar(\\\"test-js-set-timeout\\\")\\n  expect(v).to.eql(\\\"bruno-is-awesome\\\");\\n});\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"none\"\n                }\n              }\n            }\n          ]\n        },\n        {\n          \"type\": \"folder\",\n          \"name\": \"local modules\",\n          \"root\": {\n            \"meta\": {\n              \"name\": \"local modules\"\n            }\n          },\n          \"items\": [\n            {\n              \"type\": \"http\",\n              \"name\": \"invalid and valid module imports\",\n              \"seq\": 3,\n              \"request\": {\n                \"url\": \"{{host}}/ping\",\n                \"method\": \"GET\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"none\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {\n                  \"req\": \"try {\\n  bru.setVar('invalid_module_error_thrown', false);\\n  // should throw an error\\n  const invalid = require(\\\"./lib/invalid\\\");\\n}\\ncatch(error) {\\n  bru.setVar('invalid_module_error_thrown', true);\\n}\\n\\n\\ntry {\\n  bru.setVar('valid_module_no_error', true);\\n  // should not throw an error\\n  const math = require(\\\"./lib/math\\\");\\n}\\ncatch(error) {\\n  bru.setVar('valid_module_no_error', false);\\n}\"\n                },\n                \"vars\": {},\n                \"assertions\": [\n                  {\n                    \"name\": \"invalid_module_error_thrown\",\n                    \"value\": \"eq true\",\n                    \"enabled\": true,\n                    \"uid\": \"wdZ0MsGXmW7tRiX4VtQaT\"\n                  },\n                  {\n                    \"name\": \"valid_module_no_error\",\n                    \"value\": \"eq true\",\n                    \"enabled\": true,\n                    \"uid\": \"A7hwDplpc0qDp5Bk46AMJ\"\n                  }\n                ],\n                \"tests\": \"\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"none\"\n                }\n              }\n            },\n            {\n              \"type\": \"http\",\n              \"name\": \"sum -without js extn-\",\n              \"seq\": 2,\n              \"request\": {\n                \"url\": \"{{host}}/api/echo/json\",\n                \"method\": \"POST\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"json\",\n                  \"json\": \"{\\n  \\\"a\\\": 1,\\n  \\\"b\\\": 2\\n}\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {\n                  \"req\": \"const math = require(\\\"./lib/math\\\");\\nconsole.log(math, 'math');\\n\\nconst body = req.getBody();\\nbody.sum = math.sum(body.a, body.b);\\nbody.areaOfCircle = math.areaOfCircle(2);\\n\\nreq.setBody(body);\"\n                },\n                \"vars\": {},\n                \"assertions\": [\n                  {\n                    \"name\": \"res.status\",\n                    \"value\": \"eq 200\",\n                    \"enabled\": true,\n                    \"uid\": \"R6TRM5HoxKGuC5dHFvYdH\"\n                  }\n                ],\n                \"tests\": \"test(\\\"should return json\\\", function() {\\n  const data = res.getBody();\\n  expect(res.getBody()).to.eql({\\n    \\\"a\\\": 1,\\n    \\\"b\\\": 2,\\n    \\\"sum\\\": 3,\\n    \\\"areaOfCircle\\\": 12.56\\n  });\\n});\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"none\"\n                }\n              }\n            },\n            {\n              \"type\": \"http\",\n              \"name\": \"sum\",\n              \"seq\": 1,\n              \"request\": {\n                \"url\": \"{{host}}/api/echo/json\",\n                \"method\": \"POST\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"json\",\n                  \"json\": \"{\\n  \\\"a\\\": 1,\\n  \\\"b\\\": 2\\n}\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {\n                  \"req\": \"const math = require(\\\"./lib/math.js\\\");  \\nconst body = req.getBody();\\nbody.sum = math.sum(body.a, body.b);\\n\\nreq.setBody(body);\"\n                },\n                \"vars\": {},\n                \"assertions\": [\n                  {\n                    \"name\": \"res.status\",\n                    \"value\": \"eq 200\",\n                    \"enabled\": true,\n                    \"uid\": \"UXDIZRejDajw3j0oYHhij\"\n                  }\n                ],\n                \"tests\": \"test(\\\"should return json\\\", function() {\\n  const data = res.getBody();\\n  expect(res.getBody()).to.eql({\\n    \\\"a\\\": 1,\\n    \\\"b\\\": 2,\\n    \\\"sum\\\": 3\\n  });\\n});\\n\\ntest(\\\"should return json\\\", function() {\\n  const data = res.getBody();\\n  expect(res.getBody()).to.eql({\\n    \\\"a\\\": 1,\\n    \\\"b\\\": 2,\\n    \\\"sum\\\": 3\\n  });\\n});\\n\\ntest(\\\"should return json\\\", function() {\\n  const data = res.getBody();\\n  expect(res.getBody()).to.eql({\\n    \\\"a\\\": 1,\\n    \\\"b\\\": 2,\\n    \\\"sum\\\": 3\\n  });\\n});\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"none\"\n                }\n              }\n            }\n          ]\n        },\n        {\n          \"type\": \"folder\",\n          \"name\": \"npm modules\",\n          \"root\": {\n            \"meta\": {\n              \"name\": \"npm modules\"\n            }\n          },\n          \"items\": [\n            {\n              \"type\": \"http\",\n              \"name\": \"fakerjs\",\n              \"seq\": 1,\n              \"request\": {\n                \"url\": \"{{host}}/api/echo/json\",\n                \"method\": \"POST\",\n                \"headers\": [],\n                \"params\": [],\n                \"body\": {\n                  \"mode\": \"json\",\n                  \"json\": \"{\\n  \\\"hello\\\": \\\"bruno\\\"\\n}\",\n                  \"formUrlEncoded\": [],\n                  \"multipartForm\": [],\n                  \"file\": []\n                },\n                \"script\": {\n                  \"req\": \"const { faker } = require('@faker-js/faker');\\nconst uuid = faker.string.uuid();\\n\\nconst data = req.getBody();\\ndata.uuid = uuid;\\n\\nreq.setBody(data);\"\n                },\n                \"vars\": {},\n                \"assertions\": [\n                  {\n                    \"name\": \"res.status\",\n                    \"value\": \"eq 200\",\n                    \"enabled\": true,\n                    \"uid\": \"ZVuM9BByoo5XFCtUYTwfP\"\n                  }\n                ],\n                \"tests\": \"test(\\\"should return json\\\", function() {\\n  const data = res.getBody();\\n  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\\n  const isUUID = (inputString) => {\\n    return uuidRegex.test(inputString);\\n  };\\n  \\n  expect(data.hello).to.equal(\\\"bruno\\\");\\n  expect(isUUID(data.uuid)).to.be.true;\\n});\\n\",\n                \"docs\": \"\",\n                \"auth\": {\n                  \"mode\": \"none\"\n                }\n              }\n            }\n          ]\n        }\n      ]\n    },\n    {\n      \"type\": \"folder\",\n      \"name\": \"string interpolation\",\n      \"root\": {\n        \"request\": {\n          \"vars\": {\n            \"req\": [\n              {\n                \"name\": \"folder_pre_var\",\n                \"value\": \"folder_pre_var_value\",\n                \"enabled\": true,\n                \"local\": false,\n                \"uid\": \"OHd64NVOj1HQV2PLqRzy8\"\n              },\n              {\n                \"name\": \"folder_pre_var_2\",\n                \"value\": \"{{env.var1}}\",\n                \"enabled\": true,\n                \"local\": false,\n                \"uid\": \"J12VEAvPGi3R0wBKXy2jK\"\n              }\n            ]\n          }\n        },\n        \"meta\": {\n          \"name\": \"string interpolation\"\n        }\n      },\n      \"items\": [\n        {\n          \"type\": \"http\",\n          \"name\": \"env vars\",\n          \"seq\": 2,\n          \"request\": {\n            \"url\": \"{{host}}/api/echo/json\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"json\",\n              \"json\": \"{\\n  \\\"envVar1\\\": \\\"{{env.var1}}\\\",\\n  \\\"envVar2\\\": \\\"{{env-var2}}\\\"\\n}\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {},\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"eq 200\",\n                \"enabled\": true,\n                \"uid\": \"mxXvAcLVxfpRpGqh76ugy\"\n              }\n            ],\n            \"tests\": \"test(\\\"should return json\\\", function() {\\n  expect(res.getBody()).to.eql({\\n    \\\"envVar1\\\": \\\"envVar1\\\",\\n    \\\"envVar2\\\": \\\"envVar2\\\"\\n  });\\n});\\n\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"missing values\",\n          \"seq\": 1,\n          \"request\": {\n            \"url\": \"{{host}}/api/echo/json?foo={{undefinedVar}}\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [\n              {\n                \"name\": \"foo\",\n                \"value\": \"{{undefinedVar}}\",\n                \"type\": \"query\",\n                \"enabled\": true\n              }\n            ],\n            \"body\": {\n              \"mode\": \"json\",\n              \"json\": \"{\\n  \\\"hello\\\": \\\"{{undefinedVar2}}\\\"\\n}\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {},\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"eq 200\",\n                \"enabled\": true,\n                \"uid\": \"nzPl7aN2MK9uc3SKEepG0\"\n              }\n            ],\n            \"tests\": \"test(\\\"should return json\\\", function() {\\n  const url = req.getUrl();\\n  const query = url.split(\\\"?\\\")[1];\\n  expect(query).to.equal(\\\"foo={{undefinedVar}}\\\");\\n\\n  const data = res.getBody();\\n  expect(res.getBody()).to.eql({\\n    \\\"hello\\\": \\\"{{undefinedVar2}}\\\"\\n  });\\n});\\n\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"process env vars\",\n          \"seq\": 4,\n          \"request\": {\n            \"url\": \"{{host}}/api/echo/json\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"json\",\n              \"json\": \"{\\n  \\\"bark\\\": \\\"{{bark}}\\\",\\n  \\\"bark2\\\": \\\"{{process.env.PROC_ENV_VAR}}\\\"\\n}\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {},\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"eq 200\",\n                \"enabled\": true,\n                \"uid\": \"T1m34pWveQfE4Aao7Xqlt\"\n              }\n            ],\n            \"tests\": \"test(\\\"should return json\\\", function() {\\n  expect(res.getBody()).to.eql({\\n    \\\"bark\\\": \\\"woof\\\",\\n    \\\"bark2\\\": \\\"woof\\\"\\n  });\\n});\\n\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        },\n        {\n          \"type\": \"http\",\n          \"name\": \"runtime vars\",\n          \"seq\": 3,\n          \"request\": {\n            \"url\": \"{{host}}/api/echo/text\",\n            \"method\": \"POST\",\n            \"headers\": [],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"text\",\n              \"json\": \"{\\n  \\\"envVar1\\\": \\\"{{env.var1}}\\\",\\n  \\\"envVar2\\\": \\\"{{env-var2}}\\\"\\n}\",\n              \"text\": \"Hi, I am {{rUser.full_name}},\\nI am {{rUser.age}} years old.\\nMy favorite food is {{rUser.fav-food[0]}} and {{rUser.fav-food[1]}}.\\nI like attention: {{rUser.want.attention}}\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            },\n            \"script\": {\n              \"req\": \"bru.setVar(\\\"rUser\\\", {\\n  full_name: 'Bruno',\\n  age: 4,\\n  'fav-food': ['egg', 'meat'],\\n  'want.attention': true\\n});\"\n            },\n            \"vars\": {},\n            \"assertions\": [\n              {\n                \"name\": \"res.status\",\n                \"value\": \"eq 200\",\n                \"enabled\": true,\n                \"uid\": \"xe4IAIu4EXYOYiXmKU374\"\n              }\n            ],\n            \"tests\": \"test(\\\"should return json\\\", function() {\\n  const expectedResponse = `Hi, I am Bruno,\\nI am 4 years old.\\nMy favorite food is egg and meat.\\nI like attention: true`;\\n  expect(res.getBody()).to.equal(expectedResponse);\\n});\\n\",\n            \"docs\": \"\",\n            \"auth\": {\n              \"mode\": \"none\"\n            }\n          }\n        }\n      ]\n    }\n  ],\n  \"activeEnvironmentUid\": \"s4jJkWbb9017JXdVqOxLR\",\n  \"environments\": [\n    {\n      \"variables\": [\n        {\n          \"name\": \"host\",\n          \"value\": \"http://localhost:8080\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"bearer_auth_token\",\n          \"value\": \"your_secret_token\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"basic_auth_password\",\n          \"value\": \"della\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"client_id\",\n          \"value\": \"client_id_1\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"client_secret\",\n          \"value\": \"client_secret_1\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"auth_url\",\n          \"value\": \"http://localhost:8080/api/auth/oauth2/authorization_code/authorize\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"callback_url\",\n          \"value\": \"http://localhost:8080/api/auth/oauth2/authorization_code/callback\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"access_token_url\",\n          \"value\": \"http://localhost:8080/api/auth/oauth2/authorization_code/token\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"passwordCredentials_username\",\n          \"value\": \"foo\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"passwordCredentials_password\",\n          \"value\": \"bar\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"github_authorize_url\",\n          \"value\": \"https://github.com/login/oauth/authorize\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"github_access_token_url\",\n          \"value\": \"https://github.com/login/oauth/access_token\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"google_auth_url\",\n          \"value\": \"https://accounts.google.com/o/oauth2/auth\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"google_access_token_url\",\n          \"value\": \"https://accounts.google.com/o/oauth2/token\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"google_scope\",\n          \"value\": \"https://www.googleapis.com/auth/userinfo.email\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"github_client_secret\",\n          \"value\": \"\",\n          \"enabled\": true,\n          \"secret\": true,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"github_client_id\",\n          \"value\": \"\",\n          \"enabled\": true,\n          \"secret\": true,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"google_client_id\",\n          \"value\": \"\",\n          \"enabled\": true,\n          \"secret\": true,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"google_client_secret\",\n          \"value\": \"\",\n          \"enabled\": true,\n          \"secret\": true,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"github_authorization_code\",\n          \"value\": \"\",\n          \"enabled\": true,\n          \"secret\": true,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"passwordCredentials_access_token\",\n          \"value\": \"\",\n          \"enabled\": true,\n          \"secret\": true,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"client_credentials_access_token\",\n          \"value\": \"\",\n          \"enabled\": true,\n          \"secret\": true,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"authorization_code_access_token\",\n          \"value\": \"\",\n          \"enabled\": true,\n          \"secret\": true,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"github_access_token\",\n          \"value\": \"\",\n          \"enabled\": true,\n          \"secret\": true,\n          \"type\": \"text\"\n        }\n      ],\n      \"name\": \"Local\"\n    },\n    {\n      \"variables\": [\n        {\n          \"name\": \"host\",\n          \"value\": \"https://testbench-sanity.usebruno.com\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"bearer_auth_token\",\n          \"value\": \"your_secret_token\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"basic_auth_password\",\n          \"value\": \"della\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"env.var1\",\n          \"value\": \"envVar1\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"env-var2\",\n          \"value\": \"envVar2\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"bark\",\n          \"value\": \"{{process.env.PROC_ENV_VAR}}\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"foo\",\n          \"value\": \"bar\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"testSetEnvVar\",\n          \"value\": \"bruno-29653\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        },\n        {\n          \"name\": \"echo-host\",\n          \"value\": \"https://echo.usebruno.com\",\n          \"enabled\": true,\n          \"secret\": false,\n          \"type\": \"text\"\n        }\n      ],\n      \"name\": \"Prod\"\n    }\n  ],\n  \"root\": {\n    \"request\": {\n      \"auth\": {\n        \"mode\": \"bearer\",\n        \"bearer\": {\n          \"token\": \"{{bearer_auth_token}}\"\n        }\n      },\n      \"headers\": [\n        {\n          \"name\": \"check\",\n          \"value\": \"again\",\n          \"enabled\": true,\n          \"uid\": \"wbTRFykhPHZwnzVUTd1gr\"\n        },\n        {\n          \"name\": \"token\",\n          \"value\": \"{{collection_pre_var_token}}\",\n          \"enabled\": true,\n          \"uid\": \"YGZ16VXf9NusINngKeXqn\"\n        }\n      ],\n      \"vars\": {\n        \"req\": [\n          {\n            \"name\": \"collection_pre_var\",\n            \"value\": \"collection_pre_var_value\",\n            \"enabled\": true,\n            \"local\": false,\n            \"uid\": \"HI7DgTPA1gBLB6lIl1t3O\"\n          },\n          {\n            \"name\": \"collection_pre_var_token\",\n            \"value\": \"{{request_pre_var_token}}\",\n            \"enabled\": true,\n            \"local\": false,\n            \"uid\": \"FoDj77i1KoZ6Koq9oavPy\"\n          }\n        ]\n      }\n    },\n    \"docs\": \"# bruno-testbench 🐶\\n\\nThis is a test collection that I am using to test various functionalities around bruno\"\n  },\n  \"brunoConfig\": {\n    \"version\": \"1\",\n    \"name\": \"bruno-testbench\",\n    \"type\": \"collection\",\n    \"proxy\": {\n      \"enabled\": false,\n      \"protocol\": \"http\",\n      \"hostname\": \"{{proxyHostname}}\",\n      \"port\": 4000,\n      \"auth\": {\n        \"enabled\": false,\n        \"username\": \"anoop\"\n      },\n      \"bypassProxy\": \"\"\n    },\n    \"scripts\": {\n      \"moduleWhitelist\": [\n        \"crypto\",\n        \"buffer\",\n        \"form-data\"\n      ]\n    },\n    \"clientCertificates\": {\n      \"enabled\": true,\n      \"certs\": []\n    },\n    \"presets\": {\n      \"requestType\": \"http\",\n      \"requestUrl\": \"http://localhost:6000\"\n    },\n    \"ignore\": [\n      \"node_modules\",\n      \".git\"\n    ],\n    \"size\": 0.026633262634277344,\n    \"filesCount\": 96\n  }\n}"
  },
  {
    "path": "tests/import/bruno/fixtures/bruno-with-examples.json",
    "content": "{\n  \"name\": \"bruno-with-examples\",\n  \"version\": \"1\",\n  \"items\": [\n    {\n      \"type\": \"http\",\n      \"name\": \"echo-request\",\n      \"seq\": 1,\n      \"request\": {\n        \"url\": \"https://testbench-sanity.usebruno.com/api/echo/json\",\n        \"method\": \"POST\",\n        \"headers\": [\n          {\n            \"name\": \"Content-Type\",\n            \"value\": \"application/json\",\n            \"enabled\": true\n          }\n        ],\n        \"params\": [],\n        \"body\": {\n          \"mode\": \"json\",\n          \"json\": \"{\\n  \\\"message\\\": \\\"Hello World\\\",\\n  \\\"timestamp\\\": \\\"{{$timestamp}}\\\"\\n}\",\n          \"formUrlEncoded\": [],\n          \"multipartForm\": [],\n          \"file\": []\n        },\n        \"script\": {\n          \"req\": \"\",\n          \"res\": \"\"\n        },\n        \"vars\": {\n          \"req\": [],\n          \"res\": []\n        },\n        \"assertions\": [],\n        \"tests\": \"\",\n        \"docs\": \"\",\n        \"auth\": {\n          \"mode\": \"none\"\n        }\n      },\n      \"examples\": [\n        {\n          \"name\": \"example\",\n          \"description\": \"example description\",\n          \"type\": \"http\",\n          \"request\": {\n            \"url\": \"https://testbench-sanity.usebruno.com/api/echo/json\",\n            \"method\": \"POST\",\n            \"headers\": [\n              {\n                \"name\": \"Content-Type\",\n                \"value\": \"application/json\",\n                \"enabled\": true\n              }\n            ],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"json\",\n              \"json\": \"{\\n  \\\"message\\\": \\\"Hello World\\\",\\n  \\\"timestamp\\\": \\\"{{$timestamp}}\\\"\\n}\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": [],\n              \"file\": []\n            }\n          },\n          \"response\": {\n            \"status\": \"200\",\n            \"statusText\": \"OK\",\n            \"headers\": [\n              {\n                \"name\": \"content-type\",\n                \"value\": \"application/json; charset=utf-8\",\n                \"enabled\": true\n              }\n            ],\n            \"body\": {\n              \"type\": \"json\",\n              \"content\": \"{\\n  \\\"message\\\": \\\"Hello World\\\",\\n  \\\"timestamp\\\": \\\"1760611876\\\"\\n}\"\n            }\n          }\n        }\n      ]\n    }\n  ],\n  \"environments\": [],\n  \"activeEnvironmentUid\": null,\n  \"brunoConfig\": {\n    \"version\": \"1\",\n    \"name\": \"bruno-with-examples\",\n    \"type\": \"collection\",\n    \"ignore\": [\n      \"node_modules\",\n      \".git\"\n    ]\n  }\n}"
  },
  {
    "path": "tests/import/bruno/import-bruno-corrupted-fails.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\n\ntest.describe('Import Corrupted Bruno Collection - Should Fail', () => {\n  test('Import Bruno collection with invalid JSON structure should fail', async ({ page }) => {\n    const brunoFile = path.resolve(__dirname, 'fixtures', 'bruno-malformed.json');\n\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // Wait for import collection modal to be ready\n    const importModal = page.getByRole('dialog');\n    await importModal.waitFor({ state: 'visible' });\n    await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n\n    await page.setInputFiles('input[type=\"file\"]', brunoFile);\n\n    const errorLocator = page.getByText(/Failed to parse the file|Unsupported collection format|Invalid|Error/).first();\n    await expect(errorLocator).toBeVisible({ timeout: 10000 });\n\n    // Cleanup: close any open modals\n    await page.getByTestId('modal-close-button').click();\n  });\n});\n"
  },
  {
    "path": "tests/import/bruno/import-bruno-missing-required-schema.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\n\ntest.describe('Import Bruno Collection - Missing Required Schema Fields', () => {\n  test('Import Bruno collection missing required version field should fail', async ({ page }) => {\n    const brunoFile = path.resolve(__dirname, 'fixtures', 'bruno-missing-required-fields.json');\n\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // Wait for import collection modal to be ready\n    const importModal = page.getByRole('dialog');\n    await importModal.waitFor({ state: 'visible' });\n    await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n\n    await page.setInputFiles('input[type=\"file\"]', brunoFile);\n\n    const errorMessage = page.getByText('Unsupported collection format').first();\n    await expect(errorMessage).toBeVisible({ timeout: 10000 });\n\n    // Cleanup: close any open modals\n    await page.getByTestId('modal-close-button').click();\n  });\n});\n"
  },
  {
    "path": "tests/import/bruno/import-bruno-testbench.spec.ts",
    "content": "import { test } from '../../../playwright';\nimport * as path from 'path';\nimport { closeAllCollections, importCollection } from '../../utils/page';\n\ntest.describe('Import Bruno Testbench Collection', () => {\n  test.afterAll(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('Import Bruno Testbench collection successfully', async ({ page, createTmpDir }) => {\n    const brunoFile = path.resolve(__dirname, 'fixtures', 'bruno-testbench.json');\n\n    await importCollection(page, brunoFile, await createTmpDir('bruno-testbench-test'), {\n      expectedCollectionName: 'bruno-testbench'\n    });\n  });\n});\n"
  },
  {
    "path": "tests/import/bruno/import-bruno-with-examples.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\nimport { closeAllCollections } from '../../utils/page';\n\ntest.describe('Import Bruno Collection with Examples', () => {\n  test.afterAll(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('Import Bruno collection with examples successfully', async ({ page }) => {\n    const brunoFile = path.resolve(__dirname, 'fixtures', 'bruno-with-examples.json');\n\n    await test.step('Open import collection modal', async () => {\n      await page.getByTestId('collections-header-add-menu').click();\n      await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n    });\n\n    await test.step('Wait for import modal and verify title', async () => {\n      const importModal = page.getByRole('dialog');\n      await importModal.waitFor({ state: 'visible' });\n      await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n    });\n\n    await test.step('Upload collection file and verify location modal appears', async () => {\n      await page.setInputFiles('input[type=\"file\"]', brunoFile);\n\n      const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n      const errorMessage = page.getByText('Failed to parse the file');\n\n      const result = await Promise.race([\n        locationModal.waitFor({ state: 'visible', timeout: 15000 }).then(() => 'success'),\n        errorMessage.waitFor({ state: 'visible', timeout: 15000 }).then(() => 'error')\n      ]).catch(() => 'timeout');\n\n      if (result === 'error') {\n        throw new Error('Collection import failed with parsing error');\n      }\n      if (result === 'timeout') {\n        throw new Error('Import timed out - neither success nor error state was reached');\n      }\n\n      await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n    });\n\n    await test.step('Verify collection name appears in location modal', async () => {\n      const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n      await expect(locationModal.getByText('bruno-with-examples')).toBeVisible();\n      await page.getByTestId('modal-close-button').click();\n    });\n  });\n});\n"
  },
  {
    "path": "tests/import/bulk-import/001-multiple-files-upload.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\nimport { closeAllCollections } from '../../utils/page';\n\ntest.describe('Multiple Files Upload', () => {\n  const testDataDir = path.join(__dirname, '../test-data');\n\n  test.afterEach(async ({ page }) => {\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  test('Multiple files can be uploaded together', async ({ page, createTmpDir }) => {\n    const postmanFile = path.join(testDataDir, 'sample-postman.json');\n    const insomniaFile = path.join(testDataDir, 'sample-insomnia.json');\n\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // Wait for import collection modal to be ready\n    const importModal = page.getByRole('dialog');\n    await importModal.waitFor({ state: 'visible' });\n    await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n\n    await page.setInputFiles('input[type=\"file\"]', [postmanFile, insomniaFile]);\n\n    // Wait for the loader to disappear\n    await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });\n\n    // Verify that the Bulk Import modal is now displayed\n    const bulkImportModal = page.getByRole('dialog');\n    await expect(bulkImportModal.locator('.bruno-modal-header-title')).toContainText('Bulk Import');\n\n    // Check that the Collections count shows 2 collections in the Bulk Import modal\n    await expect(bulkImportModal.getByText('Collections (2)')).toBeVisible();\n\n    // Verify collection names are displayed\n    await expect(bulkImportModal.getByText('Sample Postman Collection')).toBeVisible();\n    await expect(bulkImportModal.getByText('Sample Insomnia Collection')).toBeVisible();\n\n    // Select a location and import\n    await page.locator('#collection-location').fill(await createTmpDir('multiple-files-test'));\n    await bulkImportModal.getByRole('button', { name: 'Import' }).click();\n\n    // Wait for import to complete (summary modal shows with \"Close\" button)\n    await expect(bulkImportModal.getByRole('button', { name: 'Close' })).toBeVisible();\n\n    // Close the summary modal\n    await bulkImportModal.getByRole('button', { name: 'Close' }).click();\n    await bulkImportModal.waitFor({ state: 'hidden' });\n\n    // Verify collections were imported successfully\n    await expect(page.locator('#sidebar-collection-name').getByText('Sample Postman Collection')).toBeVisible();\n    await expect(page.locator('#sidebar-collection-name').getByText('Sample Insomnia Collection')).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/import/bulk-import/002-all-collection-types.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\nimport { closeAllCollections } from '../../utils/page';\n\ntest.describe('All Collection Types Bulk Import', () => {\n  const testDataDir = path.join(__dirname, '../test-data');\n\n  test.afterEach(async ({ page }) => {\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  test('All 4 collection types appear in bulk import', async ({ page, createTmpDir }) => {\n    const postmanFile = path.join(testDataDir, 'sample-postman.json');\n    const insomniaFile = path.join(testDataDir, 'sample-insomnia.json');\n    const brunoFile = path.join(testDataDir, 'sample-bruno.json');\n    const openapiFile = path.join(testDataDir, 'sample-openapi.yaml');\n\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // Wait for import collection modal to be ready\n    const importModal = page.getByRole('dialog');\n    await importModal.waitFor({ state: 'visible' });\n    await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n\n    await page.setInputFiles('input[type=\"file\"]', [postmanFile, insomniaFile, brunoFile, openapiFile]);\n\n    // Wait for the loader to disappear\n    await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });\n\n    // Verify that the Bulk Import modal is displayed (no separate settings modal anymore)\n    const bulkImportModal = page.getByRole('dialog');\n    await expect(bulkImportModal.locator('.bruno-modal-header-title')).toContainText('Bulk Import');\n\n    // Check that the Collections count shows 4 collections in the Bulk Import modal\n    await expect(bulkImportModal.getByText('Collections (4)')).toBeVisible();\n    await expect(bulkImportModal.getByText('Sample Postman Collection')).toBeVisible();\n    await expect(bulkImportModal.getByText('Sample Insomnia Collection')).toBeVisible();\n    await expect(bulkImportModal.getByText('Sample Bruno Collection')).toBeVisible();\n    await expect(bulkImportModal.getByText('Sample API')).toBeVisible();\n\n    // Verify that OpenAPI settings are visible (since one file is OpenAPI)\n    await expect(bulkImportModal.getByText('Folder arrangement')).toBeVisible();\n    await expect(bulkImportModal.getByTestId('grouping-dropdown')).toBeVisible();\n\n    // Optionally change grouping to path-based\n    await bulkImportModal.getByTestId('grouping-dropdown').click();\n    await bulkImportModal.getByTestId('grouping-option-path').click();\n\n    // Select a location and import\n    await page.locator('#collection-location').fill(await createTmpDir('all-collection-types-test'));\n    await bulkImportModal.getByRole('button', { name: 'Import' }).click();\n\n    // Wait for import to complete (summary modal shows with \"Close\" button)\n    await expect(bulkImportModal.getByRole('button', { name: 'Close' })).toBeVisible();\n\n    // Close the summary modal\n    await bulkImportModal.getByRole('button', { name: 'Close' }).click();\n    await bulkImportModal.waitFor({ state: 'hidden' });\n\n    // Verify all collections were imported successfully\n    await expect(page.locator('#sidebar-collection-name').getByText('Sample Postman Collection')).toBeVisible();\n    await expect(page.locator('#sidebar-collection-name').getByText('Sample Insomnia Collection')).toBeVisible();\n    await expect(page.locator('#sidebar-collection-name').getByText('Sample Bruno Collection')).toBeVisible();\n    await expect(page.locator('#sidebar-collection-name').getByText('Sample API')).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/import/file-types/file-input-acceptance.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\n\ntest.describe('File Input Acceptance', () => {\n  test('File input accepts expected file types', async ({ page }) => {\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // Check that file input exists (even if hidden)\n    const fileInput = page.locator('input[type=\"file\"]');\n    await expect(fileInput).toBeAttached();\n\n    // Verify it accepts the expected file types\n    const acceptValue = await fileInput.getAttribute('accept');\n    expect(acceptValue).toContain('.json');\n    expect(acceptValue).toContain('.yaml');\n    expect(acceptValue).toContain('.yml');\n\n    // Cleanup: close any open modals\n    await page.getByTestId('modal-close-button').click();\n  });\n});\n"
  },
  {
    "path": "tests/import/file-types/fixtures/invalid.txt",
    "content": "This is not a valid collection file\n"
  },
  {
    "path": "tests/import/file-types/invalid-file-handling.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\n\ntest.describe('Invalid File Handling', () => {\n  test('Handle invalid file without crashing', async ({ page }) => {\n    const invalidFile = path.resolve(__dirname, 'fixtures', 'invalid.txt');\n\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // Wait for import collection modal to be ready\n    const importModal = page.getByRole('dialog');\n    await importModal.waitFor({ state: 'visible' });\n    await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n\n    await page.setInputFiles('input[type=\"file\"]', invalidFile);\n\n    // Wait for the loader to disappear\n    await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });\n\n    // Use auto-retrying assertion instead of snapshot isVisible() check\n    await expect(page.getByText('Failed to parse the file – ensure it is valid JSON or YAML').first()).toBeVisible();\n\n    // Cleanup: close any open modals\n    await page.getByTestId('modal-close-button').click();\n  });\n});\n"
  },
  {
    "path": "tests/import/insomnia/fixtures/insomnia-malformed.json",
    "content": "{\n  \"_type\": \"export\",\n  \"__export_format\": 4,\n  \"resources\": [\n    {\n      \"_id\": \"req_123\",\n      \"parentId\": \"wrk_456\",\n      \"url\": \"https://api.example.com/users\",\n      \"name\": \"Get Users\",\n      \"method\": \"GET\",\n      \"_type\": \"request\"\n    },\n    {\n      \"_id\": \"wrk_456\",\n      \"name\": \"Test Collection\",\n      \"_type\": \"workspace\"\n      // Missing comma and closing bracket - malformed JSON\n    }\n  ]\n"
  },
  {
    "path": "tests/import/insomnia/fixtures/insomnia-v4-with-envs.json",
    "content": "{\n  \"_type\": \"export\",\n  \"__export_format\": 4,\n  \"__export_date\": \"2025-01-01T12:00:00.000Z\",\n  \"__export_source\": \"insomnia.desktop.app:v10.3.1\",\n  \"resources\": [\n    {\n      \"_id\": \"req_fdedb34f7d5541d0aa7a917ce37ec067\",\n      \"parentId\": \"wrk_398c634c4fbc4774bcff39cbff44b31b\",\n      \"modified\": 1689952276171,\n      \"created\": 1689951240510,\n      \"url\": \"{{baseUrl}}/api/users\",\n      \"name\": \"Get Users\",\n      \"description\": \"Fetch all users from the API\",\n      \"method\": \"GET\",\n      \"body\": {},\n      \"parameters\": [],\n      \"headers\": [\n        {\n          \"name\": \"Accept\",\n          \"value\": \"application/json\"\n        }\n      ],\n      \"authentication\": {},\n      \"metaSortKey\": -1689951414329,\n      \"isPrivate\": false,\n      \"settingStoreCookies\": true,\n      \"settingSendCookies\": true,\n      \"settingDisableRenderRequestBody\": false,\n      \"settingEncodeUrl\": true,\n      \"settingRebuildPath\": true,\n      \"settingFollowRedirects\": \"global\",\n      \"_type\": \"request\"\n    },\n    {\n      \"_id\": \"wrk_398c634c4fbc4774bcff39cbff44b31b\",\n      \"parentId\": null,\n      \"modified\": 1743678539806,\n      \"created\": 1743678539806,\n      \"name\": \"Test API Collection v4 with Environments\",\n      \"description\": \"Test collection for Insomnia v4 format with environments\",\n      \"scope\": \"collection\",\n      \"_type\": \"workspace\"\n    },\n    {\n      \"_id\": \"env_93781eb62f074459bb67692112b76da0\",\n      \"parentId\": \"wrk_398c634c4fbc4774bcff39cbff44b31b\",\n      \"modified\": 1743681240772,\n      \"created\": 1689951235312,\n      \"name\": \"Base Environment\",\n      \"data\": {\n        \"baseUrl\": \"https://api.example.com\",\n        \"authToken\": \"your_auth_token_here\",\n        \"user\": {\n          \"name\": \"admin\",\n          \"id\": 123,\n          \"roles\": [\"admin\", \"user\"]\n        },\n        \"config\": {\n          \"timeout\": 30000,\n          \"retries\": 3,\n          \"debug\": true\n        }\n      },\n      \"dataPropertyOrder\": null,\n      \"color\": null,\n      \"isPrivate\": false,\n      \"metaSortKey\": 1689951235312,\n      \"_type\": \"environment\"\n    },\n    {\n      \"_id\": \"env_staging_123\",\n      \"parentId\": \"env_93781eb62f074459bb67692112b76da0\",\n      \"modified\": 1743681240772,\n      \"created\": 1689951235312,\n      \"name\": \"Staging\",\n      \"data\": {\n        \"baseUrl\": \"https://staging-api.example.com\",\n        \"user\": {\n          \"name\": \"staging_admin\"\n        },\n        \"config\": {\n          \"timeout\": 60000,\n          \"debug\": false\n        }\n      },\n      \"dataPropertyOrder\": null,\n      \"color\": null,\n      \"isPrivate\": false,\n      \"metaSortKey\": 1689951235312,\n      \"_type\": \"environment\"\n    },\n    {\n      \"_id\": \"env_dev_456\",\n      \"parentId\": \"env_93781eb62f074459bb67692112b76da0\",\n      \"modified\": 1743681240772,\n      \"created\": 1689951235312,\n      \"name\": \"Development\",\n      \"data\": {\n        \"baseUrl\": \"https://dev-api.example.com\",\n        \"authToken\": \"dev_token_123\",\n        \"newFeature\": {\n          \"enabled\": true,\n          \"version\": 2.099123123\n        }\n      },\n      \"dataPropertyOrder\": null,\n      \"color\": null,\n      \"isPrivate\": false,\n      \"metaSortKey\": 1689951235312,\n      \"_type\": \"environment\"\n    },\n    {\n      \"_id\": \"jar_09963a0322c24b698ecd2f866ae9a6ab\",\n      \"parentId\": \"wrk_398c634c4fbc4774bcff39cbff44b31b\",\n      \"modified\": 1689951235313,\n      \"created\": 1689951235313,\n      \"name\": \"Default Jar\",\n      \"cookies\": [],\n      \"_type\": \"cookie_jar\"\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/import/insomnia/fixtures/insomnia-v4.json",
    "content": "{\n  \"_type\": \"export\",\n  \"__export_format\": 4,\n  \"__export_date\": \"2025-01-01T12:00:00.000Z\",\n  \"__export_source\": \"insomnia.desktop.app:v10.3.1\",\n  \"resources\": [\n    {\n      \"_id\": \"req_fdedb34f7d5541d0aa7a917ce37ec067\",\n      \"parentId\": \"wrk_398c634c4fbc4774bcff39cbff44b31b\",\n      \"modified\": 1689952276171,\n      \"created\": 1689951240510,\n      \"url\": \"{{baseUrl}}/api/users\",\n      \"name\": \"Get Users\",\n      \"description\": \"Fetch all users from the API\",\n      \"method\": \"GET\",\n      \"body\": {},\n      \"parameters\": [],\n      \"headers\": [\n        {\n          \"name\": \"Accept\",\n          \"value\": \"application/json\"\n        }\n      ],\n      \"authentication\": {},\n      \"metaSortKey\": -1689951414329,\n      \"isPrivate\": false,\n      \"settingStoreCookies\": true,\n      \"settingSendCookies\": true,\n      \"settingDisableRenderRequestBody\": false,\n      \"settingEncodeUrl\": true,\n      \"settingRebuildPath\": true,\n      \"settingFollowRedirects\": \"global\",\n      \"_type\": \"request\"\n    },\n    {\n      \"_id\": \"req_c920d219404144e8bc6b6bd36f442974\",\n      \"parentId\": \"fld_ab2a1533f2be48c194883bf07d693292\",\n      \"modified\": 1689952281595,\n      \"created\": 1689951281897,\n      \"url\": \"{{baseUrl}}/api/auth/login\",\n      \"name\": \"Login User\",\n      \"description\": \"User authentication endpoint\",\n      \"method\": \"POST\",\n      \"body\": {\n        \"mimeType\": \"application/json\",\n        \"text\": \"{\\n  \\\"username\\\": \\\"admin\\\",\\n  \\\"password\\\": \\\"password123\\\"\\n}\"\n      },\n      \"parameters\": [],\n      \"headers\": [\n        {\n          \"name\": \"Content-Type\",\n          \"value\": \"application/json\"\n        }\n      ],\n      \"authentication\": {},\n      \"metaSortKey\": -1689951404530.5625,\n      \"isPrivate\": false,\n      \"settingStoreCookies\": true,\n      \"settingSendCookies\": true,\n      \"settingDisableRenderRequestBody\": false,\n      \"settingEncodeUrl\": true,\n      \"settingRebuildPath\": true,\n      \"settingFollowRedirects\": \"global\",\n      \"_type\": \"request\"\n    },\n    {\n      \"_id\": \"fld_ab2a1533f2be48c194883bf07d693292\",\n      \"parentId\": \"wrk_398c634c4fbc4774bcff39cbff44b31b\",\n      \"modified\": 1743683080329,\n      \"created\": 1743683080329,\n      \"name\": \"Authentication\",\n      \"description\": \"Authentication related endpoints\",\n      \"environment\": {},\n      \"environmentPropertyOrder\": null,\n      \"metaSortKey\": -1743683080329,\n      \"_type\": \"request_group\"\n    },\n    {\n      \"_id\": \"wrk_398c634c4fbc4774bcff39cbff44b31b\",\n      \"parentId\": null,\n      \"modified\": 1743678539806,\n      \"created\": 1743678539806,\n      \"name\": \"Test API Collection v4\",\n      \"description\": \"Test collection for Insomnia v4 format\",\n      \"scope\": \"collection\",\n      \"_type\": \"workspace\"\n    },\n    {\n      \"_id\": \"env_93781eb62f074459bb67692112b76da0\",\n      \"parentId\": \"wrk_398c634c4fbc4774bcff39cbff44b31b\",\n      \"modified\": 1743681240772,\n      \"created\": 1689951235312,\n      \"name\": \"Base Environment\",\n      \"data\": {\n        \"baseUrl\": \"https://api.example.com\"\n      },\n      \"dataPropertyOrder\": null,\n      \"color\": null,\n      \"isPrivate\": false,\n      \"metaSortKey\": 1689951235312,\n      \"_type\": \"environment\"\n    },\n    {\n      \"_id\": \"jar_09963a0322c24b698ecd2f866ae9a6ab\",\n      \"parentId\": \"wrk_398c634c4fbc4774bcff39cbff44b31b\",\n      \"modified\": 1689951235313,\n      \"created\": 1689951235313,\n      \"name\": \"Default Jar\",\n      \"cookies\": [],\n      \"_type\": \"cookie_jar\"\n    }\n  ]\n}"
  },
  {
    "path": "tests/import/insomnia/fixtures/insomnia-v5-invalid-missing-collection.yaml",
    "content": "type: collection.insomnia.rest/5.0\nname: Invalid v5 Collection\nmeta:\n  id: wrk_7faf891d273e4b7ea82bdbaa641ee17a\n  created: 1743683067888\n  modified: 1743683067888\n# Missing collection array - this should cause import to fail\ncookieJar:\n  name: Default Jar\n  meta:\n    id: jar_25f97142fa796ae37f7f4937c0ebf3a07869d0a8\n    created: 1743683067908\n    modified: 1743683833282\n  cookies: []\nenvironments:\n  name: Base Environment\n  meta:\n    id: env_25f97142fa796ae37f7f4937c0ebf3a07869d0a8\n    created: 1743683067895\n    modified: 1743683476058\n    isPrivate: false\n  data:\n    base_url: https://api.example.com\n"
  },
  {
    "path": "tests/import/insomnia/fixtures/insomnia-v5-with-envs.yaml",
    "content": "type: collection.insomnia.rest/5.0\nname: Test API Collection v5 with Environments\nmeta:\n  id: wrk_7faf891d273e4b7ea82bdbaa641ee17a\n  created: 1743683067888\n  modified: 1743683067888\ncollection:\n  - name: API Tests\n    meta:\n      id: fld_ab2a1533f2be48c194883bf07d693292\n      created: 1743683080329\n      modified: 1743683080329\n      sortKey: -1743683080329\n    children:\n      - url: \"{{ _.base_url }}/api/users\"\n        name: Get Users\n        meta:\n          id: req_0393b8ff4ee1454daddacdda33fd33ea\n          created: 1743683426423\n          modified: 1743683632735\n          isPrivate: false\n          sortKey: -1743683429031\n        method: GET\n        headers:\n          - name: Authorization\n            value: Bearer {{ _.auth_token }}\n        settings:\n          renderRequestBody: true\n          encodeUrl: true\n          followRedirects: global\n          cookies:\n            send: true\n            store: true\n          rebuildPath: true\ncookieJar:\n  name: Default Jar\n  meta:\n    id: jar_25f97142fa796ae37f7f4937c0ebf3a07869d0a8\n    created: 1743683067908\n    modified: 1743683833282\n  cookies: []\nenvironments:\n  name: Base Environment\n  meta:\n    id: env_25f97142fa796ae37f7f4937c0ebf3a07869d0a8\n    created: 1743683067895\n    modified: 1743683476058\n    isPrivate: false\n  data:\n    base_url: https://api.example.com\n    auth_token: your_auth_token_here\n    user:\n      name: admin\n      id: 123\n      roles:\n        - admin\n        - user\n    config:\n      timeout: 30000\n      retries: 3\n      debug: true\n  subEnvironments:\n    - name: Staging\n      meta:\n        id: env_staging_123\n        created: 1743683067895\n        modified: 1743683476058\n        isPrivate: false\n      data:\n        base_url: https://staging-api.example.com\n        user:\n          name: staging_admin\n        config:\n          timeout: 60000\n          debug: false\n    - name: Development\n      meta:\n        id: env_dev_456\n        created: 1743683067895\n        modified: 1743683476058\n        isPrivate: false\n      data:\n        base_url: https://dev-api.example.com\n        auth_token: dev_token_123\n        new_feature:\n          enabled: true\n          version: 2.099123123\n"
  },
  {
    "path": "tests/import/insomnia/fixtures/insomnia-v5.yaml",
    "content": "type: collection.insomnia.rest/5.0\nname: Test API Collection v5\nmeta:\n  id: wrk_7faf891d273e4b7ea82bdbaa641ee17a\n  created: 1743683067888\n  modified: 1743683067888\ncollection:\n  - name: API Tests\n    meta:\n      id: fld_ab2a1533f2be48c194883bf07d693292\n      created: 1743683080329\n      modified: 1743683080329\n      sortKey: -1743683080329\n    children:\n      - name: Authentication\n        meta:\n          id: fld_e7bcaad160254179a9c86e39a58c6893\n          created: 1743683088154\n          modified: 1743683090190\n          sortKey: -1743683090140\n        children:\n          - url: \"{{ _.base_url }}/api/auth/login\"\n            name: Login User\n            meta:\n              id: req_d48ab8553cff4eb486b816e064cf99a4\n              created: 1743683199141\n              modified: 1743683342872\n              isPrivate: false\n              sortKey: -1743683199141\n            method: POST\n            body:\n              mimeType: application/json\n              text: |-\n                {\n                  \"username\": \"testuser\",\n                  \"password\": \"testpass123\"\n                }\n            headers:\n              - name: Content-Type\n                value: application/json\n              - name: User-Agent\n                value: insomnia/10.3.1\n            settings:\n              renderRequestBody: true\n              encodeUrl: true\n              followRedirects: global\n              cookies:\n                send: true\n                store: true\n              rebuildPath: true\n      - url: \"{{ _.base_url }}/api/users\"\n        name: Get Users\n        meta:\n          id: req_0393b8ff4ee1454daddacdda33fd33ea\n          created: 1743683426423\n          modified: 1743683632735\n          isPrivate: false\n          description: Retrieve all users from the system\n          sortKey: -1743683429031\n        method: GET\n        headers:\n          - name: Authorization\n            value: Bearer {{ _.auth_token }}\n          - name: User-Agent\n            value: insomnia/10.3.1\n        settings:\n          renderRequestBody: true\n          encodeUrl: true\n          followRedirects: global\n          cookies:\n            send: true\n            store: true\n          rebuildPath: true\n  - name: Data Management\n    meta:\n      id: fld_4736de73a1634b16960fa9e90d78f868\n      created: 1743683403969\n      modified: 1743683403969\n      sortKey: -1743683403969\n    children:\n      - url: \"{{ _.base_url }}/api/posts\"\n        name: Create Post\n        meta:\n          id: req_10db7ec1332d4444aa551e70a1bfae33\n          created: 1743683795359\n          modified: 1743683832200\n          isPrivate: false\n          sortKey: -1743683429131\n        method: POST\n        body:\n          mimeType: application/json\n          text: |-\n            {\n              \"title\": \"Test Post\",\n              \"content\": \"This is a test post content\",\n              \"author\": \"Test Author\"\n            }\n        headers:\n          - name: Content-Type\n            value: application/json\n          - name: Authorization\n            value: Bearer {{ _.auth_token }}\n          - name: User-Agent\n            value: insomnia/10.3.1\n        settings:\n          renderRequestBody: true\n          encodeUrl: true\n          followRedirects: global\n          cookies:\n            send: true\n            store: true\n          rebuildPath: true\ncookieJar:\n  name: Default Jar\n  meta:\n    id: jar_25f97142fa796ae37f7f4937c0ebf3a07869d0a8\n    created: 1743683067908\n    modified: 1743683833282\n  cookies: []\nenvironments:\n  name: Base Environment\n  meta:\n    id: env_25f97142fa796ae37f7f4937c0ebf3a07869d0a8\n    created: 1743683067895\n    modified: 1743683476058\n    isPrivate: false\n  data:\n    base_url: https://api.example.com\n    auth_token: your_auth_token_here\n"
  },
  {
    "path": "tests/import/insomnia/import-insomnia-v4-environments.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\nimport { openCollection, closeAllCollections } from '../../utils/page/actions';\n\ntest.describe('Import Insomnia v4 Collection - Environment Import', () => {\n  test.afterEach(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n  /**\n   * Tests Insomnia v4 environment import with nested data flattening and environment merging.\n   * Verifies that base and sub-environments are imported correctly with JavaScript-style keys\n   * (e.g., user.name, user.roles[0]) and proper value inheritance/overrides.\n   *\n   * Test Structure:\n   * - Base Environment: Contains nested objects, arrays, and primitive values\n   * - Staging Environment: Overrides some base values, inherits others\n   * - Development Environment: Adds new variables while inheriting base values\n   */\n  test('Import Insomnia v4 collection with nested environments and verify flattening', async ({\n    page,\n    createTmpDir\n  }) => {\n    const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-v4-with-envs.json');\n\n    await test.step('Import Insomnia v4 collection with environments', async () => {\n      await page.getByTestId('collections-header-add-menu').click();\n      await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n      const importModal = page.getByTestId('import-collection-modal');\n      await importModal.waitFor({ state: 'visible' });\n      await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n\n      await page.setInputFiles('input[type=\"file\"]', insomniaFile);\n\n      const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n      await locationModal.waitFor({ state: 'visible', timeout: 10000 });\n\n      await expect(locationModal.getByText('Test API Collection v4 with Environments')).toBeVisible();\n\n      await page.locator('#collection-location').fill(await createTmpDir('insomnia-v4-env-test'));\n      await locationModal.getByRole('button', { name: 'Import' }).click();\n      await locationModal.waitFor({ state: 'hidden' });\n\n      await expect(page.locator('#sidebar-collection-name').getByText('Test API Collection v4 with Environments')).toBeVisible();\n\n      await openCollection(page, 'Test API Collection v4 with Environments');\n    });\n\n    await test.step('Open collection environments panel', async () => {\n      await page.getByTestId('environment-selector-trigger').click();\n      await page.getByTestId('env-tab-collection').click();\n      await page.getByRole('button', { name: 'Configure' }).click();\n    });\n\n    await test.step('Verify all environments are present', async () => {\n      await expect(page\n        .locator('div')\n        .filter({ hasText: /^Base Environment$/ })\n        .first()).toBeVisible();\n      await expect(page\n        .locator('div')\n        .filter({ hasText: /^Staging$/ })\n        .first()).toBeVisible();\n      await expect(page\n        .locator('div')\n        .filter({ hasText: /^Development$/ })\n        .first()).toBeVisible();\n    });\n\n    await test.step('Test Base Environment - verify flattened keys', async () => {\n      await page\n        .locator('div')\n        .filter({ hasText: /^Base Environment$/ })\n        .first()\n        .click();\n\n      // Wait for environment variables to load - use input selector as it's more reliable\n      await expect(page.locator('input[value=\"baseUrl\"]')).toBeVisible({ timeout: 10000 });\n\n      // **Assertion 1: Basic Variables (Top-level keys)**\n      // Verifies that simple key-value pairs from the base environment are imported correctly\n      const v4BaseUrlInput = page.locator('input[value=\"baseUrl\"]');\n      const v4AuthTokenInput = page.locator('input[value=\"authToken\"]');\n      await expect(v4BaseUrlInput).toBeVisible();\n      await expect(v4AuthTokenInput).toBeVisible();\n\n      // Assert: Top-level string values are preserved exactly as in the source\n      const baseUrlRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\"baseUrl\"]') });\n      await expect(baseUrlRow.locator('.CodeMirror-line').first()).toHaveText('https://api.example.com');\n      const authTokenRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\"authToken\"]') });\n      await expect(authTokenRow.locator('.CodeMirror-line').first()).toHaveText('your_auth_token_here');\n\n      // **Assertion 2: Nested Object Flattening**\n      // Verifies that nested objects are flattened to dot-notation keys (e.g., user.name, user.id)\n      const v4UserNameInput = page.locator('input[value=\"user.name\"]');\n      const v4UserIdInput = page.locator('input[value=\"user.id\"]');\n      await expect(v4UserNameInput).toBeVisible();\n      await expect(v4UserIdInput).toBeVisible();\n\n      // Assert: Nested object properties are accessible via dot notation\n      const userNameRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\"user.name\"]') });\n      await expect(userNameRow.locator('.CodeMirror-line').first()).toHaveText('admin');\n      // Assert: Numeric values are converted to strings and preserved\n      const userIdRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\"user.id\"]') });\n      await expect(userIdRow.locator('.CodeMirror-line').first()).toHaveText('123');\n\n      // **Assertion 3: Array Flattening**\n      // Verifies that arrays are flattened using JavaScript-style square bracket notation (e.g., user.roles[0], user.roles[1])\n      const v4UserRoles0Input = page.locator('input[value=\"user.roles[0]\"]');\n      const v4UserRoles1Input = page.locator('input[value=\"user.roles[1]\"]');\n      await expect(v4UserRoles0Input).toBeVisible();\n      await expect(v4UserRoles1Input).toBeVisible();\n\n      // Assert: Array elements are accessible via JavaScript-style square bracket notation\n      const userRoles0Row = page.locator('tbody tr').filter({ has: page.locator('input[value=\"user.roles[0]\"]') });\n      await expect(userRoles0Row.locator('.CodeMirror-line').first()).toHaveText('admin');\n      const userRoles1Row = page.locator('tbody tr').filter({ has: page.locator('input[value=\"user.roles[1]\"]') });\n      await expect(userRoles1Row.locator('.CodeMirror-line').first()).toHaveText('user');\n    });\n\n    await test.step('Test Staging Environment - verify merging with base', async () => {\n      await page\n        .locator('div')\n        .filter({ hasText: /^Staging$/ })\n        .first()\n        .click();\n\n      // **Assertion 1: Top-level Variable Override**\n      // Verifies that staging environment overrides base environment values\n      const v4StagingBaseUrlInput = page.locator('input[value=\"baseUrl\"]');\n      await expect(v4StagingBaseUrlInput).toBeVisible();\n      // Assert: Staging overrides baseUrl with its own value\n      const stagingBaseUrlRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\"baseUrl\"]') });\n      await expect(stagingBaseUrlRow.locator('.CodeMirror-line').first()).toHaveText('https://staging-api.example.com');\n\n      // **Assertion 2: Top-level Variable Inheritance**\n      // Verifies that staging environment inherits base environment values when not overridden\n      const v4StagingAuthTokenInput = page.locator('input[value=\"authToken\"]');\n      await expect(v4StagingAuthTokenInput).toBeVisible();\n      // Assert: Staging inherits authToken from base (not overridden in staging)\n      const stagingAuthTokenRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\"authToken\"]') });\n      await expect(stagingAuthTokenRow.locator('.CodeMirror-line').first()).toHaveText('your_auth_token_here');\n\n      // **Assertion 3: Nested Object Variable Override and Inheritance**\n      // Verifies that nested object properties can be selectively overridden while inheriting others\n      const v4StagingUserNameInput = page.locator('input[value=\"user.name\"]');\n      const v4StagingUserIdInput = page.locator('input[value=\"user.id\"]');\n      const v4StagingUserRoles0Input = page.locator('input[value=\"user.roles[0]\"]');\n      await expect(v4StagingUserNameInput).toBeVisible();\n      await expect(v4StagingUserIdInput).toBeVisible();\n      await expect(v4StagingUserRoles0Input).toBeVisible();\n\n      // Assert: Staging overrides user.name with its own value\n      const stagingUserNameRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\"user.name\"]') });\n      await expect(stagingUserNameRow.locator('.CodeMirror-line').first()).toHaveText('staging_admin');\n      // Assert: Staging inherits user.id from base (not overridden in staging)\n      const stagingUserIdRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\"user.id\"]') });\n      await expect(stagingUserIdRow.locator('.CodeMirror-line').first()).toHaveText('123');\n      // Assert: Staging inherits user.roles[0] from base (not overridden in staging)\n      const stagingUserRoles0Row = page.locator('tbody tr').filter({ has: page.locator('input[value=\"user.roles[0]\"]') });\n      await expect(stagingUserRoles0Row.locator('.CodeMirror-line').first()).toHaveText('admin');\n    });\n\n    await test.step('Test Development Environment - verify new variables', async () => {\n      await page\n        .locator('div')\n        .filter({ hasText: /^Development$/ })\n        .first()\n        .click();\n\n      // **Assertion 1: Multiple Top-level Variable Overrides**\n      // Verifies that development environment can override multiple base environment values\n      const v4DevBaseUrlInput = page.locator('input[value=\"baseUrl\"]');\n      const v4DevAuthTokenInput = page.locator('input[value=\"authToken\"]');\n      await expect(v4DevBaseUrlInput).toBeVisible();\n      await expect(v4DevAuthTokenInput).toBeVisible();\n\n      // Assert: Development overrides baseUrl with its own value\n      const devBaseUrlRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\"baseUrl\"]') });\n      await expect(devBaseUrlRow.locator('.CodeMirror-line').first()).toHaveText('https://dev-api.example.com');\n      // Assert: Development overrides authToken with its own value\n      const devAuthTokenRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\"authToken\"]') });\n      await expect(devAuthTokenRow.locator('.CodeMirror-line').first()).toHaveText('dev_token_123');\n\n      // **Assertion 2: New Nested Variables Addition**\n      // Verifies that development environment can add completely new nested variables not present in base\n      const v4NewFeatureEnabledInput = page.locator('input[value=\"newFeature.enabled\"]');\n      const v4NewFeatureVersionInput = page.locator('input[value=\"newFeature.version\"]');\n      await expect(v4NewFeatureEnabledInput).toBeVisible();\n      await expect(v4NewFeatureVersionInput).toBeVisible();\n\n      // Assert: New boolean variable is added and converted to string\n      const newFeatureEnabledRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\"newFeature.enabled\"]') });\n      await expect(newFeatureEnabledRow.locator('.CodeMirror-line').first()).toHaveText('true');\n      // Assert: New numeric variable is added and converted to string with full precision\n      const newFeatureVersionRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\"newFeature.version\"]') });\n      await expect(newFeatureVersionRow.locator('.CodeMirror-line').first()).toHaveText('2.099123123');\n    });\n\n    await test.step('Close environment tab', async () => {\n      const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });\n      await envTab.hover();\n      await envTab.getByTestId('request-tab-close-icon').click({ force: true });\n    });\n  });\n});\n"
  },
  {
    "path": "tests/import/insomnia/import-insomnia-v4.spec.ts",
    "content": "import { test } from '../../../playwright';\nimport * as path from 'path';\nimport { closeAllCollections, importCollection } from '../../utils/page';\n\ntest.describe('Import Insomnia Collection v4', () => {\n  test.afterEach(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('Import Insomnia Collection v4 successfully', async ({ page, createTmpDir }) => {\n    const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-v4.json');\n\n    await importCollection(page, insomniaFile, await createTmpDir('insomnia-v4-test'), {\n      expectedCollectionName: 'Test API Collection v4'\n    });\n  });\n});\n"
  },
  {
    "path": "tests/import/insomnia/import-insomnia-v5-environments.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\nimport { openCollection, closeAllCollections } from '../../utils/page/actions';\n\ntest.describe('Import Insomnia v5 Collection - Environment Import', () => {\n  test.afterEach(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n  /**\n   * Tests Insomnia v5 environment import with nested data flattening and environment merging.\n   * Verifies that base and sub-environments are imported correctly with JavaScript-style keys\n   * (e.g., user.name, user.roles[0]) and proper value inheritance/overrides.\n   *\n   * Test Structure:\n   * - Base Environment: Contains nested objects, arrays, and primitive values\n   * - Staging Environment: Overrides some base values, inherits others\n   * - Development Environment: Adds new variables while inheriting base values\n   */\n  test('Import Insomnia v5 collection with nested environments and verify flattening', async ({\n    page,\n    createTmpDir\n  }) => {\n    const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-v5-with-envs.yaml');\n\n    await test.step('Import Insomnia v5 collection with environments', async () => {\n      await page.getByTestId('collections-header-add-menu').click();\n      await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n      const importModal = page.getByTestId('import-collection-modal');\n      await importModal.waitFor({ state: 'visible' });\n      await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n\n      await page.setInputFiles('input[type=\"file\"]', insomniaFile);\n\n      // Wait for location modal to appear after file processing\n      const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n      await locationModal.waitFor({ state: 'visible', timeout: 10000 });\n\n      await page.locator('#collection-location').fill(await createTmpDir('insomnia-v5-env-test'));\n      await locationModal.getByRole('button', { name: 'Import' }).click();\n      await locationModal.waitFor({ state: 'hidden' });\n\n      await openCollection(page, 'Test API Collection v5 with Environments');\n    });\n\n    await test.step('Open collection environments panel', async () => {\n      await page.getByTestId('environment-selector-trigger').click();\n      await page.getByTestId('env-tab-collection').click();\n      await page.getByRole('button', { name: 'Configure' }).click();\n    });\n\n    await test.step('Verify all environments are present', async () => {\n      await expect(page\n        .locator('div')\n        .filter({ hasText: /^Base Environment$/ })\n        .first()).toBeVisible();\n      await expect(page\n        .locator('div')\n        .filter({ hasText: /^Staging$/ })\n        .first()).toBeVisible();\n      await expect(page\n        .locator('div')\n        .filter({ hasText: /^Development$/ })\n        .first()).toBeVisible();\n    });\n\n    await test.step('Test Base Environment - verify flattened keys', async () => {\n      await page\n        .locator('div')\n        .filter({ hasText: /^Base Environment$/ })\n        .first()\n        .click();\n\n      // **Assertion 1: Basic Variables (Top-level keys)**\n      // Verifies that simple key-value pairs from the base environment are imported correctly\n      const baseUrlInput = page.locator('input[value=\"base_url\"]');\n      const authTokenInput = page.locator('input[value=\"auth_token\"]');\n      await expect(baseUrlInput).toBeVisible();\n      await expect(authTokenInput).toBeVisible();\n\n      // Assert: Top-level string values are preserved exactly as in the source\n      const baseUrlRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\\\"base_url\\\"]') });\n      await expect(baseUrlRow.locator('.CodeMirror-line').first()).toHaveText('https://api.example.com');\n      const authTokenRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\\\"auth_token\\\"]') });\n      await expect(authTokenRow.locator('.CodeMirror-line').first()).toHaveText('your_auth_token_here');\n\n      // **Assertion 2: Nested Object Flattening**\n      // Verifies that nested objects are flattened to dot-notation keys (e.g., user.name, user.id)\n      const userNameInput = page.locator('input[value=\"user.name\"]');\n      const userIdInput = page.locator('input[value=\"user.id\"]');\n      await expect(userNameInput).toBeVisible();\n      await expect(userIdInput).toBeVisible();\n\n      // Assert: Nested object properties are accessible via dot notation\n      const userNameRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\\\"user.name\\\"]') });\n      await expect(userNameRow.locator('.CodeMirror-line').first()).toHaveText('admin');\n      // Assert: Numeric values are converted to strings and preserved\n      const userIdRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\\\"user.id\\\"]') });\n      await expect(userIdRow.locator('.CodeMirror-line').first()).toHaveText('123');\n\n      // **Assertion 3: Array Flattening**\n      // Verifies that arrays are flattened using JavaScript-style square bracket notation (e.g., user.roles[0], user.roles[1])\n      const userRoles0Input = page.locator('input[value=\"user.roles[0]\"]');\n      const userRoles1Input = page.locator('input[value=\"user.roles[1]\"]');\n      await expect(userRoles0Input).toBeVisible();\n      await expect(userRoles1Input).toBeVisible();\n\n      // Assert: Array elements are accessible via JavaScript-style square bracket notation\n      const userRoles0Row = page.locator('tbody tr').filter({ has: page.locator('input[value=\\\"user.roles[0]\\\"]') });\n      await expect(userRoles0Row.locator('.CodeMirror-line').first()).toHaveText('admin');\n      const userRoles1Row = page.locator('tbody tr').filter({ has: page.locator('input[value=\\\"user.roles[1]\\\"]') });\n      await expect(userRoles1Row.locator('.CodeMirror-line').first()).toHaveText('user');\n\n      // **Assertion 4: Deeply Nested Config Objects**\n      // Verifies that deeply nested objects are properly flattened (e.g., config.timeout, config.debug)\n      const configTimeoutInput = page.locator('input[value=\"config.timeout\"]');\n      const configDebugInput = page.locator('input[value=\"config.debug\"]');\n      await expect(configTimeoutInput).toBeVisible();\n      await expect(configDebugInput).toBeVisible();\n\n      // Assert: Numeric values in nested objects are converted to strings\n      const configTimeoutRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\\\"config.timeout\\\"]') });\n      await expect(configTimeoutRow.locator('.CodeMirror-line').first()).toHaveText('30000');\n      // Assert: Boolean values in nested objects are converted to strings\n      const configDebugRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\\\"config.debug\\\"]') });\n      await expect(configDebugRow.locator('.CodeMirror-line').first()).toHaveText('true');\n    });\n\n    await test.step('Test Staging Environment - verify merging and overrides', async () => {\n      await page\n        .locator('div')\n        .filter({ hasText: /^Staging$/ })\n        .first()\n        .click();\n\n      // **Assertion 1: Top-level Variable Override**\n      // Verifies that staging environment overrides base environment values\n      const stagingBaseUrlInput = page.locator('input[value=\"base_url\"]');\n      await expect(stagingBaseUrlInput).toBeVisible();\n      // Assert: Staging overrides base_url with its own value\n      const baseUrlRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\\\"base_url\\\"]') });\n      await expect(baseUrlRow.locator('.CodeMirror-line').first()).toHaveText('https://staging-api.example.com');\n\n      // **Assertion 2: Top-level Variable Inheritance**\n      // Verifies that staging environment inherits base environment values when not overridden\n      const stagingAuthTokenInput = page.locator('input[value=\"auth_token\"]');\n      await expect(stagingAuthTokenInput).toBeVisible();\n      // Assert: Staging inherits auth_token from base (not overridden in staging)\n      const authTokenRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\\\"auth_token\\\"]') });\n      await expect(authTokenRow.locator('.CodeMirror-line').first()).toHaveText('your_auth_token_here');\n\n      // **Assertion 3: Nested Object Variable Override and Inheritance**\n      // Verifies that nested object properties can be selectively overridden while inheriting others\n      const stagingUserNameInput = page.locator('input[value=\"user.name\"]');\n      const stagingUserIdInput = page.locator('input[value=\"user.id\"]');\n      await expect(stagingUserNameInput).toBeVisible();\n      await expect(stagingUserIdInput).toBeVisible();\n\n      // Assert: Staging overrides user.name with its own value\n      const userNameRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\\\"user.name\\\"]') });\n      await expect(userNameRow.locator('.CodeMirror-line').first()).toHaveText('staging_admin');\n      // Assert: Staging inherits user.id from base (not overridden in staging)\n      const userIdRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\\\"user.id\\\"]') });\n      await expect(userIdRow.locator('.CodeMirror-line').first()).toHaveText('123');\n\n      // **Assertion 4: Deeply Nested Config Override**\n      // Verifies that deeply nested object properties can be overridden\n      const stagingConfigTimeoutInput = page.locator('input[value=\"config.timeout\"]');\n      const stagingConfigDebugInput = page.locator('input[value=\"config.debug\"]');\n      await expect(stagingConfigTimeoutInput).toBeVisible();\n      await expect(stagingConfigDebugInput).toBeVisible();\n\n      // Assert: Staging overrides config.timeout with its own value\n      const configTimeoutRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\\\"config.timeout\\\"]') });\n      await expect(configTimeoutRow.locator('.CodeMirror-line').first()).toHaveText('60000');\n      // Assert: Staging overrides config.debug with its own value\n      const configDebugRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\\\"config.debug\\\"]') });\n      await expect(configDebugRow.locator('.CodeMirror-line').first()).toHaveText('false');\n    });\n\n    await test.step('Test Development Environment - verify new variables', async () => {\n      await page\n        .locator('div')\n        .filter({ hasText: /^Development$/ })\n        .first()\n        .click();\n\n      // **Assertion 1: Multiple Top-level Variable Overrides**\n      // Verifies that development environment can override multiple base environment values\n      const devBaseUrlInput = page.locator('input[value=\"base_url\"]');\n      const devAuthTokenInput = page.locator('input[value=\"auth_token\"]');\n      await expect(devBaseUrlInput).toBeVisible();\n      await expect(devAuthTokenInput).toBeVisible();\n\n      // Assert: Development overrides base_url with its own value\n      const baseUrlRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\\\"base_url\\\"]') });\n      await expect(baseUrlRow.locator('.CodeMirror-line').first()).toHaveText('https://dev-api.example.com');\n      // Assert: Development overrides auth_token with its own value\n      const authTokenRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\\\"auth_token\\\"]') });\n      await expect(authTokenRow.locator('.CodeMirror-line').first()).toHaveText('dev_token_123');\n\n      // **Assertion 2: New Nested Variables Addition**\n      // Verifies that development environment can add completely new nested variables not present in base\n      const newFeatureEnabledInput = page.locator('input[value=\"new_feature.enabled\"]');\n      const newFeatureVersionInput = page.locator('input[value=\"new_feature.version\"]');\n      await expect(newFeatureEnabledInput).toBeVisible();\n      await expect(newFeatureVersionInput).toBeVisible();\n\n      // Assert: New boolean variable is added and converted to string\n      const newFeatureEnabledRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\\\"new_feature.enabled\\\"]') });\n      await expect(newFeatureEnabledRow.locator('.CodeMirror-line').first()).toHaveText('true');\n      // Assert: New numeric variable is added and converted to string with full precision\n      const newFeatureVersionRow = page.locator('tbody tr').filter({ has: page.locator('input[value=\\\"new_feature.version\\\"]') });\n      await expect(newFeatureVersionRow.locator('.CodeMirror-line').first()).toHaveText('2.099123123');\n\n      // **Assertion 3: Base Variable Inheritance**\n      // Verifies that development environment still inherits base variables that are not overridden\n      const devUserRoles0Input = page.locator('input[value=\"user.roles[0]\"]');\n      await expect(devUserRoles0Input).toBeVisible();\n      // Assert: Development inherits user.roles[0] from base (not overridden in development)\n      const userRoles0Row = page.locator('tbody tr').filter({ has: page.locator('input[value=\\\"user.roles[0]\\\"]') });\n      await expect(userRoles0Row.locator('.CodeMirror-line').first()).toHaveText('admin');\n    });\n\n    await test.step('Close environment tab', async () => {\n      const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' });\n      await envTab.hover();\n      await envTab.getByTestId('request-tab-close-icon').click({ force: true });\n    });\n  });\n});\n"
  },
  {
    "path": "tests/import/insomnia/import-insomnia-v5.spec.ts",
    "content": "import { test } from '../../../playwright';\nimport * as path from 'path';\nimport { closeAllCollections, importCollection } from '../../utils/page';\n\ntest.describe('Import Insomnia Collection v5', () => {\n  test.afterEach(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('Import Insomnia Collection v5 successfully', async ({ page, createTmpDir }) => {\n    const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-v5.yaml');\n\n    await importCollection(page, insomniaFile, await createTmpDir('insomnia-v5-test'), {\n      expectedCollectionName: 'Test API Collection v5'\n    });\n  });\n});\n"
  },
  {
    "path": "tests/import/insomnia/invalid-missing-collection.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\n\ntest.describe('Invalid Insomnia Collection - Missing Collection Array', () => {\n  test('Handle Insomnia v5 collection missing collection array', async ({ page }) => {\n    const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-v5-invalid-missing-collection.yaml');\n\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // Wait for import collection modal to be ready\n    const importModal = page.getByRole('dialog');\n    await importModal.waitFor({ state: 'visible' });\n    await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n\n    await page.setInputFiles('input[type=\"file\"]', insomniaFile);\n\n    const errorLocator = page.getByText('Unsupported collection format').first();\n    await expect(errorLocator).toBeVisible({ timeout: 10000 });\n\n    // Cleanup: close any open modals\n    await page.getByTestId('modal-close-button').click();\n  });\n});\n"
  },
  {
    "path": "tests/import/insomnia/malformed-structure.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\n\ntest.describe('Invalid Insomnia Collection - Malformed Structure', () => {\n  test('Handle malformed Insomnia collection structure', async ({ page }) => {\n    const insomniaFile = path.resolve(__dirname, 'fixtures', 'insomnia-malformed.json');\n\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // Wait for import collection modal to be ready\n    const importModal = page.getByRole('dialog');\n    await importModal.waitFor({ state: 'visible' });\n    await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n\n    await page.setInputFiles('input[type=\"file\"]', insomniaFile);\n\n    // Check for error message - this should fail during JSON parsing\n    // Use auto-retrying assertion instead of snapshot isVisible() check\n    await expect(page.getByText('Failed to parse the file').first()).toBeVisible();\n\n    // Cleanup: close any open modals\n    await page.getByTestId('modal-close-button').click();\n  });\n});\n"
  },
  {
    "path": "tests/import/openapi/cli/fixtures/openapi.json",
    "content": "{\n  \"openapi\": \"3.0.0\",\n  \"info\": {\n    \"title\": \"Simple Test API\",\n    \"version\": \"1.0.0\",\n    \"description\": \"A simple API for testing groupBy functionality\"\n  },\n  \"servers\": [\n    {\n      \"url\": \"https://api.example.com\",\n      \"description\": \"Example server\"\n    }\n  ],\n  \"paths\": {\n    \"/users\": {\n      \"get\": {\n        \"summary\": \"Get all users\",\n        \"operationId\": \"getUsers\",\n        \"tags\": [\"users\"],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"List of users\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"id\": { \"type\": \"integer\" },\n                      \"name\": { \"type\": \"string\" }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      },\n      \"post\": {\n        \"summary\": \"Create user\",\n        \"operationId\": \"createUser\",\n        \"tags\": [\"users\"],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"name\": { \"type\": \"string\" }\n                }\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"201\": {\n            \"description\": \"User created\"\n          }\n        }\n      }\n    },\n    \"/users/{id}\": {\n      \"get\": {\n        \"summary\": \"Get user by ID\",\n        \"operationId\": \"getUserById\",\n        \"tags\": [\"users\"],\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"integer\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"User details\"\n          }\n        }\n      },\n      \"put\": {\n        \"summary\": \"Update user\",\n        \"operationId\": \"updateUser\",\n        \"tags\": [\"users\"],\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"integer\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"name\": { \"type\": \"string\" }\n                }\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"User updated\"\n          }\n        }\n      }\n    },\n    \"/products\": {\n      \"get\": {\n        \"summary\": \"Get all products\",\n        \"operationId\": \"getProducts\",\n        \"tags\": [\"products\"],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"List of products\"\n          }\n        }\n      }\n    },\n    \"/products/{id}\": {\n      \"get\": {\n        \"summary\": \"Get product by ID\",\n        \"operationId\": \"getProductById\",\n        \"tags\": [\"products\"],\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"integer\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Product details\"\n          }\n        }\n      }\n    },\n    \"/orders\": {\n      \"get\": {\n        \"summary\": \"Get all orders\",\n        \"operationId\": \"getOrders\",\n        \"tags\": [\"orders\"],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"List of orders\"\n          }\n        }\n      },\n      \"post\": {\n        \"summary\": \"Create order\",\n        \"operationId\": \"createOrder\",\n        \"tags\": [\"orders\"],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"userId\": { \"type\": \"integer\" },\n                  \"productId\": { \"type\": \"integer\" }\n                }\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"201\": {\n            \"description\": \"Order created\"\n          }\n        }\n      }\n    },\n    \"/orders/{id}\": {\n      \"get\": {\n        \"summary\": \"Get order by ID\",\n        \"operationId\": \"getOrderById\",\n        \"tags\": [\"orders\"],\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"integer\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Order details\"\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/import/openapi/cli/group-by-import.spec.ts",
    "content": "import { test, expect } from '../../../../playwright';\nimport { execSync } from 'child_process';\nimport fs from 'fs';\nimport path from 'path';\n\ntest.describe('OpenAPI Import GroupBy Tests', () => {\n  test('CLI: Import OpenAPI with tags grouping', async ({ createTmpDir }) => {\n    const outputDir = await createTmpDir('openapi-tags');\n    const jsonOutputPath = path.join(outputDir, 'petstore-tags.json');\n\n    // Run OpenAPI import with tags grouping using JSON output\n    const cliPath = path.resolve(__dirname, '../../../../packages/bruno-cli/bin/bru.js');\n    const specPath = path.resolve(__dirname, './fixtures/openapi.json');\n    const command = `node \"${cliPath}\" import openapi --source \"${specPath}\" --output-file \"${jsonOutputPath}\" --collection-name \"Simple API (Tags)\" --group-by tags`;\n\n    try {\n      execSync(command, { stdio: 'pipe' });\n    } catch (error) {\n      // Continue with test even if import fails\n    }\n\n    // Verify JSON file was created\n    expect(fs.existsSync(jsonOutputPath)).toBe(true);\n\n    // Read and verify collection structure\n    const jsonCollection = JSON.parse(fs.readFileSync(jsonOutputPath, 'utf8'));\n    expect(jsonCollection.name).toBe('Simple API (Tags)');\n\n    // Verify tags grouping creates folders by OpenAPI tags\n    const folders = jsonCollection.items.filter((item) => item.type === 'folder');\n    expect(folders.length).toBe(3);\n\n    const folderNames = folders.map((folder) => folder.name);\n    expect(folderNames).toContain('users');\n    expect(folderNames).toContain('products');\n    expect(folderNames).toContain('orders');\n\n    // Verify tags grouping doesn't create {id} folders\n    const hasIdFolders = folders.some((folder) => folder.items?.some((item) => item.name === '{id}'));\n    expect(hasIdFolders).toBe(false);\n  });\n\n  test('CLI: Import OpenAPI with path grouping', async ({ createTmpDir }) => {\n    const outputDir = await createTmpDir('openapi-path');\n    const jsonOutputPath = path.join(outputDir, 'petstore-path.json');\n\n    // Run OpenAPI import with path grouping using JSON output\n    const cliPath = path.resolve(__dirname, '../../../../packages/bruno-cli/bin/bru.js');\n    const specPath = path.resolve(__dirname, './fixtures/openapi.json');\n    const command = `node \"${cliPath}\" import openapi --source \"${specPath}\" --output-file \"${jsonOutputPath}\" --collection-name \"Simple API (Path)\" --group-by path`;\n\n    try {\n      execSync(command, { stdio: 'pipe' });\n    } catch (error) {\n      // Continue with test even if import fails\n    }\n\n    // Verify JSON file was created\n    expect(fs.existsSync(jsonOutputPath)).toBe(true);\n\n    // Read and verify collection structure\n    const jsonCollection = JSON.parse(fs.readFileSync(jsonOutputPath, 'utf8'));\n    expect(jsonCollection.name).toBe('Simple API (Path)');\n\n    // Verify path grouping creates folders by URL path structure\n    const folders = jsonCollection.items.filter((item) => item.type === 'folder');\n    expect(folders.length).toBe(3); // users, products, orders\n\n    const folderNames = folders.map((folder) => folder.name);\n    expect(folderNames).toContain('users');\n    expect(folderNames).toContain('products');\n    expect(folderNames).toContain('orders');\n\n    // Verify path grouping creates {id} folders for parameterized paths\n    const hasIdFolders = folders.some((folder) => folder.items?.some((item) => item.name === '{id}'));\n    expect(hasIdFolders).toBe(true);\n  });\n});\n"
  },
  {
    "path": "tests/import/openapi/duplicate-operation-names-fix.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\nimport { closeAllCollections } from '../../utils/page';\n\ntest.describe('OpenAPI Duplicate Names Handling', () => {\n  test.afterEach(async ({ page }) => {\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  test('should handle duplicate operation names', async ({ page, createTmpDir }) => {\n    const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-duplicate-operation-name.yaml');\n\n    // start the import process\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // wait for the import collection modal to appear\n    const importModal = page.getByTestId('import-collection-modal');\n    await importModal.waitFor({ state: 'visible' });\n\n    // upload the OpenAPI file with duplicate operation names\n    await page.setInputFiles('input[type=\"file\"]', openApiFile);\n\n    // Wait for location modal to appear after file processing\n    const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n    await locationModal.waitFor({ state: 'visible', timeout: 10000 });\n\n    // wait for the file processing to complete\n\n    // select a location\n    await page.locator('#collection-location').fill(await createTmpDir('duplicate-test'));\n    await locationModal.getByRole('button', { name: 'Import' }).click();\n    await locationModal.waitFor({ state: 'hidden' });\n\n    // verify the collection was imported successfully\n    await expect(page.locator('#sidebar-collection-name').getByText('Duplicate Test Collection')).toBeVisible();\n\n    // configure the collection settings\n    await page.locator('#sidebar-collection-name').getByText('Duplicate Test Collection').click();\n\n    // verify that all 3 requests were imported correctly despite duplicate operation names\n    await expect(page.locator('#collection-duplicate-test-collection .collection-item-name')).toHaveCount(3);\n  });\n});\n"
  },
  {
    "path": "tests/import/openapi/fixtures/openapi-comprehensive.yaml",
    "content": "openapi: 3.0.3\ninfo:\n  title: Comprehensive API Test Collection\n  description: A comprehensive API for testing OpenAPI v3 imports with various features\n  version: 2.1.0\n  contact:\n    name: API Support\n    email: support@example.com\n  license:\n    name: MIT\n    url: https://opensource.org/licenses/MIT\nservers:\n  - url: https://api.example.com/v1\n    description: Production server\n  - url: https://staging-api.example.com/v1\n    description: Staging server\n  - url: http://localhost:3000/v1\n    description: Development server\nsecurity:\n  - bearerAuth: []\n  - apiKey: []\npaths:\n  /users:\n    get:\n      summary: Get all users\n      description: Retrieve a paginated list of all users\n      tags:\n        - Users\n      parameters:\n        - name: page\n          in: query\n          description: Page number for pagination\n          schema:\n            type: integer\n            minimum: 1\n            default: 1\n        - name: limit\n          in: query\n          description: Number of items per page\n          schema:\n            type: integer\n            minimum: 1\n            maximum: 100\n            default: 20\n        - name: filter\n          in: query\n          description: Filter users by name or email\n          schema:\n            type: string\n      responses:\n        '200':\n          description: List of users retrieved successfully\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  users:\n                    type: array\n                    items:\n                      $ref: '#/components/schemas/User'\n                  pagination:\n                    $ref: '#/components/schemas/Pagination'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '401':\n          $ref: '#/components/responses/Unauthorized'\n    post:\n      summary: Create a new user\n      description: Create a new user account\n      tags:\n        - Users\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/CreateUserRequest'\n      responses:\n        '201':\n          description: User created successfully\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/User'\n        '400':\n          $ref: '#/components/responses/BadRequest'\n        '409':\n          description: User already exists\n  /users/{userId}:\n    get:\n      summary: Get user by ID\n      description: Retrieve a specific user by their ID\n      tags:\n        - Users\n      parameters:\n        - name: userId\n          in: path\n          required: true\n          description: The user ID\n          schema:\n            type: string\n            format: uuid\n      responses:\n        '200':\n          description: User retrieved successfully\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/User'\n        '404':\n          $ref: '#/components/responses/NotFound'\n    put:\n      summary: Update user\n      description: Update an existing user\n      tags:\n        - Users\n      parameters:\n        - name: userId\n          in: path\n          required: true\n          description: The user ID\n          schema:\n            type: string\n            format: uuid\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/UpdateUserRequest'\n      responses:\n        '200':\n          description: User updated successfully\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/User'\n        '404':\n          $ref: '#/components/responses/NotFound'\n    delete:\n      summary: Delete user\n      description: Delete a user account\n      tags:\n        - Users\n      parameters:\n        - name: userId\n          in: path\n          required: true\n          description: The user ID\n          schema:\n            type: string\n            format: uuid\n      responses:\n        '204':\n          description: User deleted successfully\n        '404':\n          $ref: '#/components/responses/NotFound'\n  /auth/login:\n    post:\n      summary: User login\n      description: Authenticate user and get access token\n      tags:\n        - Authentication\n      security: []  # No security required for login\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              type: object\n              required:\n                - email\n                - password\n              properties:\n                email:\n                  type: string\n                  format: email\n                password:\n                  type: string\n                  minLength: 8\n      responses:\n        '200':\n          description: Login successful\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  token:\n                    type: string\n                  expiresIn:\n                    type: integer\n                  user:\n                    $ref: '#/components/schemas/User'\n        '401':\n          description: Invalid credentials\n  /posts:\n    get:\n      summary: Get all posts\n      description: Retrieve all blog posts\n      tags:\n        - Posts\n      parameters:\n        - name: author\n          in: query\n          description: Filter by author ID\n          schema:\n            type: string\n        - name: category\n          in: query\n          description: Filter by category\n          schema:\n            type: string\n      responses:\n        '200':\n          description: List of posts\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  $ref: '#/components/schemas/Post'\n    post:\n      summary: Create a new post\n      description: Create a new blog post\n      tags:\n        - Posts\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/CreatePostRequest'\n      responses:\n        '201':\n          description: Post created successfully\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Post'\ncomponents:\n  securitySchemes:\n    bearerAuth:\n      type: http\n      scheme: bearer\n      bearerFormat: JWT\n    apiKey:\n      type: apiKey\n      in: header\n      name: X-API-Key\n  schemas:\n    User:\n      type: object\n      properties:\n        id:\n          type: string\n          format: uuid\n        email:\n          type: string\n          format: email\n        name:\n          type: string\n        avatar:\n          type: string\n          format: uri\n        createdAt:\n          type: string\n          format: date-time\n        updatedAt:\n          type: string\n          format: date-time\n    CreateUserRequest:\n      type: object\n      required:\n        - email\n        - name\n        - password\n      properties:\n        email:\n          type: string\n          format: email\n        name:\n          type: string\n          minLength: 2\n        password:\n          type: string\n          minLength: 8\n        avatar:\n          type: string\n          format: uri\n    UpdateUserRequest:\n      type: object\n      properties:\n        name:\n          type: string\n          minLength: 2\n        avatar:\n          type: string\n          format: uri\n    Post:\n      type: object\n      properties:\n        id:\n          type: string\n          format: uuid\n        title:\n          type: string\n        content:\n          type: string\n        author:\n          $ref: '#/components/schemas/User'\n        category:\n          type: string\n        publishedAt:\n          type: string\n          format: date-time\n        createdAt:\n          type: string\n          format: date-time\n    CreatePostRequest:\n      type: object\n      required:\n        - title\n        - content\n        - category\n      properties:\n        title:\n          type: string\n          minLength: 5\n        content:\n          type: string\n          minLength: 10\n        category:\n          type: string\n    Pagination:\n      type: object\n      properties:\n        page:\n          type: integer\n        limit:\n          type: integer\n        total:\n          type: integer\n        totalPages:\n          type: integer\n    Error:\n      type: object\n      properties:\n        error:\n          type: string\n        message:\n          type: string\n        code:\n          type: integer\n  responses:\n    BadRequest:\n      description: Bad request\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/Error'\n    Unauthorized:\n      description: Unauthorized\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/Error'\n    NotFound:\n      description: Resource not found\n      content:\n        application/json:\n          schema:\n            $ref: '#/components/schemas/Error'\n"
  },
  {
    "path": "tests/import/openapi/fixtures/openapi-duplicate-operation-name.yaml",
    "content": "openapi: 3.0.0\ninfo:\n  title: Duplicate Test Collection\n  version: 1.0.0\n  description: Test collection for handling duplicate operation names\nservers:\n  - url: https://api.example.com\n    description: Example server\npaths:\n  /users:\n    get:\n      summary: 'Get Users'\n      description: 'Get all users'\n      operationId: getUsers\n      responses:\n        '200':\n          description: Success\n          content:\n            application/json:\n              schema:\n                type: object\n    post:\n      summary: 'Get Users'\n      description: 'Create a new user (same summary as GET)'\n      operationId: createUser\n      responses:\n        '201':\n          description: Created\n          content:\n            application/json:\n              schema:\n                type: object\n  /products:\n    get:\n      summary: 'Get Users'\n      description: 'Get all products (same summary as users GET)'\n      operationId: getProducts\n      responses:\n        '200':\n          description: Success\n          content:\n            application/json:\n              schema:\n                type: object\n"
  },
  {
    "path": "tests/import/openapi/fixtures/openapi-invalid-version.yaml",
    "content": "openapi: 2.0  # Invalid version - only v3 is supported\ninfo:\n  title: Invalid OpenAPI Version\n  description: This uses OpenAPI v2 which is not supported\n  version: 1.0.0\nhost: api.example.com\nbasePath: /v1\nschemes:\n  - https\npaths:\n  /users:\n    get:\n      summary: Get users\n      responses:\n        '200':\n          description: List of users\n"
  },
  {
    "path": "tests/import/openapi/fixtures/openapi-malformed.yaml",
    "content": "openapi: 3.0.0\ninfo:\n  title: Malformed OpenAPI\n  version: 1.0.0\npaths:\n  /test:\n    get:\n      summary: Test endpoint\n      responses:\n        '200':\n          description: Success\n        # Missing closing quotes and malformed YAML\n        '400':\n          description: Bad request\n            malformed: yaml here\n              extra: indentation\n"
  },
  {
    "path": "tests/import/openapi/fixtures/openapi-missing-info.yaml",
    "content": "openapi: 3.0.0\n# Missing required info section\npaths:\n  /test:\n    get:\n      summary: Test endpoint\n      responses:\n        '200':\n          description: Success\n"
  },
  {
    "path": "tests/import/openapi/fixtures/openapi-newline-in-operation-name.yaml",
    "content": "openapi: 3.0.0\ninfo:\n  title: Newline Test Collection\n  version: 1.0.0\n  description: Test collection for operation names with newlines\nservers:\n  - url: https://api.example.com\n    description: Example server\npaths:\n  /users:\n    get:\n      summary: \"Get users\\nwith newline\"\n      description: 'This operation has newlines in the summary'\n      operationId: getUsersWithNewline\n      responses:\n        '200':\n          description: Success\n          content:\n            application/json:\n              schema:\n                type: object\n    post:\n      summary: \"Create user\\n\\nwith multiple\\n\\nnewlines\"\n      description: 'This operation has multiple consecutive newlines'\n      operationId: createUserWithNewlines\n      responses:\n        '201':\n          description: Created\n          content:\n            application/json:\n              schema:\n                type: object\n"
  },
  {
    "path": "tests/import/openapi/fixtures/openapi-path-grouping.json",
    "content": "{\n  \"openapi\": \"3.0.0\",\n  \"info\": {\n    \"title\": \"Path Grouping Test API\",\n    \"description\": \"API for testing path-based folder grouping\",\n    \"version\": \"1.0.0\"\n  },\n  \"servers\": [\n    {\n      \"url\": \"https://api.example.com\",\n      \"description\": \"Test server\"\n    }\n  ],\n  \"paths\": {\n    \"/users\": {\n      \"get\": {\n        \"summary\": \"List users\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success\"\n          }\n        }\n      }\n    },\n    \"/users/{id}\": {\n      \"get\": {\n        \"summary\": \"Get user by ID\",\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success\"\n          }\n        }\n      }\n    },\n    \"/products\": {\n      \"get\": {\n        \"summary\": \"List products\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success\"\n          }\n        }\n      }\n    },\n    \"/products/{id}\": {\n      \"get\": {\n        \"summary\": \"Get product by ID\",\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"schema\": {\n              \"type\": \"string\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Success\"\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/import/openapi/fixtures/openapi-simple.json",
    "content": "{\n  \"openapi\": \"3.0.0\",\n  \"info\": {\n    \"title\": \"Simple Test API\",\n    \"description\": \"A simple API for basic OpenAPI testing\",\n    \"version\": \"1.0.0\"\n  },\n  \"servers\": [\n    {\n      \"url\": \"https://echo.usebruno.com\",\n      \"description\": \"Echo test server\"\n    }\n  ],\n  \"paths\": {\n    \"/get\": {\n      \"get\": {\n        \"summary\": \"HTTP GET test\",\n        \"description\": \"Test HTTP GET request\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"args\": {\n                      \"type\": \"object\"\n                    },\n                    \"headers\": {\n                      \"type\": \"object\"\n                    },\n                    \"origin\": {\n                      \"type\": \"string\"\n                    },\n                    \"url\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/post\": {\n      \"post\": {\n        \"summary\": \"HTTP POST test\",\n        \"description\": \"Test HTTP POST request\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"data\": {\n                    \"type\": \"string\"\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"json\": {\n                      \"type\": \"object\"\n                    },\n                    \"origin\": {\n                      \"type\": \"string\"\n                    },\n                    \"url\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/status/{code}\": {\n      \"get\": {\n        \"summary\": \"Return status code\",\n        \"description\": \"Return a specific HTTP status code\",\n        \"parameters\": [\n          {\n            \"name\": \"code\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"description\": \"HTTP status code to return\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"minimum\": 100,\n              \"maximum\": 599\n            }\n          }\n        ],\n        \"responses\": {\n          \"default\": {\n            \"description\": \"Returns the specified status code\"\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/import/openapi/fixtures/openapi-with-examples.yaml",
    "content": "openapi: '3.0.0'\ninfo:\n  version: '1.0.0'\n  title: 'API with Examples'\n  description: 'A sample API with response examples'\nservers:\n  - url: 'https://api.example.com'\n    description: 'Production server'\npaths:\n  /users:\n    get:\n      summary: 'Get all users'\n      operationId: 'getUsers'\n      responses:\n        '200':\n          description: 'Successful response'\n          content:\n            application/json:\n              examples:\n                success:\n                  summary: 'Success Response'\n                  description: 'A successful response with user data'\n                  value:\n                    users:\n                      - id: 1\n                        name: 'John Doe'\n                        email: 'john@example.com'\n                      - id: 2\n                        name: 'Jane Smith'\n                        email: 'jane@example.com'\n                empty:\n                  summary: 'Empty Response'\n                  description: 'No users found'\n                  value:\n                    users: []\n        '400':\n          description: 'Bad Request'\n          content:\n            application/json:\n              examples:\n                validation_error:\n                  summary: 'Validation Error'\n                  description: 'Invalid request parameters'\n                  value:\n                    error: 'Invalid parameters'\n                    message: 'The request contains invalid data'\n                    code: 'VALIDATION_ERROR'\n        '500':\n          description: 'Internal Server Error'\n          content:\n            application/json:\n              examples:\n                server_error:\n                  summary: 'Server Error'\n                  description: 'Internal server error occurred'\n                  value:\n                    error: 'Internal Server Error'\n                    message: 'Something went wrong on our end'\n                    code: 'INTERNAL_ERROR'\n    post:\n      summary: 'Create a new user'\n      operationId: 'createUser'\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              type: object\n              required:\n                - name\n                - email\n              properties:\n                name:\n                  type: string\n                  example: 'John Doe'\n                email:\n                  type: string\n                  format: email\n                  example: 'john@example.com'\n            examples:\n              valid_user:\n                summary: 'Valid User'\n                description: 'A valid user creation request'\n                value:\n                  name: 'John Doe'\n                  email: 'john@example.com'\n              invalid_user:\n                summary: 'Invalid User'\n                description: 'An invalid user creation request'\n                value:\n                  name: ''\n                  email: 'invalid-email'\n      responses:\n        '201':\n          description: 'User created successfully'\n          content:\n            application/json:\n              examples:\n                created:\n                  summary: 'User Created'\n                  description: 'Successfully created user'\n                  value:\n                    id: 123\n                    name: 'John Doe'\n                    email: 'john@example.com'\n                    created_at: '2023-01-01T00:00:00Z'\n        '400':\n          description: 'Bad Request'\n          content:\n            application/json:\n              examples:\n                validation_error:\n                  summary: 'Validation Error'\n                  description: 'Invalid user data'\n                  value:\n                    error: 'Validation failed'\n                    message: 'Name and email are required'\n                    code: 'VALIDATION_ERROR'\n  /users/{id}:\n    get:\n      summary: 'Get user by ID'\n      operationId: 'getUserById'\n      parameters:\n        - name: id\n          in: path\n          required: true\n          schema:\n            type: integer\n          example: 123\n      responses:\n        '200':\n          description: 'User found'\n          content:\n            application/json:\n              examples:\n                found:\n                  summary: 'User Found'\n                  description: 'User successfully retrieved'\n                  value:\n                    id: 123\n                    name: 'John Doe'\n                    email: 'john@example.com'\n                    created_at: '2023-01-01T00:00:00Z'\n        '404':\n          description: 'User not found'\n          content:\n            application/json:\n              examples:\n                not_found:\n                  summary: 'User Not Found'\n                  description: 'User with the specified ID does not exist'\n                  value:\n                    error: 'Not Found'\n                    message: 'User with ID 123 not found'\n                    code: 'USER_NOT_FOUND'\n"
  },
  {
    "path": "tests/import/openapi/fixtures/openapi-with-security-schemes.json",
    "content": "{\n  \"openapi\": \"3.0.0\",\n  \"info\": {\n    \"title\": \"API with Security Schemes\",\n    \"description\": \"An API that demonstrates various security schemes\",\n    \"version\": \"1.0.0\"\n  },\n  \"servers\": [\n    {\n      \"url\": \"https://api.example.com/v1\",\n      \"description\": \"Production server\"\n    }\n  ],\n  \"security\": [\n    {\n      \"bearerAuth\": []\n    }\n  ],\n  \"components\": {\n    \"securitySchemes\": {\n      \"bearerAuth\": {\n        \"type\": \"http\",\n        \"scheme\": \"bearer\",\n        \"description\": \"Bearer token authentication\"\n      },\n      \"basicAuth\": {\n        \"type\": \"http\",\n        \"scheme\": \"basic\",\n        \"description\": \"Basic authentication\"\n      },\n      \"apiKey\": {\n        \"type\": \"apiKey\",\n        \"in\": \"header\",\n        \"name\": \"X-API-Key\",\n        \"description\": \"API Key authentication\"\n      },\n      \"oauth2\": {\n        \"type\": \"oauth2\",\n        \"flows\": {\n          \"authorizationCode\": {\n            \"authorizationUrl\": \"https://auth.example.com/oauth/authorize\",\n            \"tokenUrl\": \"https://auth.example.com/oauth/token\",\n            \"scopes\": {\n              \"read\": \"Read access\",\n              \"write\": \"Write access\",\n              \"admin\": \"Admin access\"\n            }\n          }\n        },\n        \"description\": \"OAuth 2.0 authentication\"\n      }\n    }\n  },\n  \"paths\": {\n    \"/users\": {\n      \"get\": {\n        \"summary\": \"Get users\",\n        \"description\": \"Retrieve a list of users\",\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"id\": {\n                        \"type\": \"integer\"\n                      },\n                      \"name\": {\n                        \"type\": \"string\"\n                      },\n                      \"email\": {\n                        \"type\": \"string\"\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      },\n      \"post\": {\n        \"summary\": \"Create user\",\n        \"description\": \"Create a new user\",\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"name\": {\n                    \"type\": \"string\"\n                  },\n                  \"email\": {\n                    \"type\": \"string\"\n                  }\n                },\n                \"required\": [\"name\", \"email\"]\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"201\": {\n            \"description\": \"User created\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"integer\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    },\n                    \"email\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/admin/users\": {\n      \"get\": {\n        \"summary\": \"Admin get users\",\n        \"description\": \"Retrieve all users (admin only)\",\n        \"security\": [\n          {\n            \"oauth2\": [\"admin\"]\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"id\": {\n                        \"type\": \"integer\"\n                      },\n                      \"name\": {\n                        \"type\": \"string\"\n                      },\n                      \"email\": {\n                        \"type\": \"string\"\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/public/data\": {\n      \"get\": {\n        \"summary\": \"Get public data\",\n        \"description\": \"Retrieve public data without authentication\",\n        \"security\": [],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"message\": {\n                      \"type\": \"string\"\n                    },\n                    \"timestamp\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/import/openapi/fixtures/openapi-without-security-schemes.json",
    "content": "{\n  \"openapi\": \"3.0.0\",\n  \"info\": {\n    \"title\": \"API without Security Schemes\",\n    \"description\": \"An API that has no security schemes defined - should use the getSecurity fallback logic\",\n    \"version\": \"1.0.0\"\n  },\n  \"servers\": [\n    {\n      \"url\": \"https://api.public-example.com/v1\",\n      \"description\": \"Public API server\"\n    }\n  ],\n  \"paths\": {\n    \"/health\": {\n      \"get\": {\n        \"summary\": \"Health check\",\n        \"description\": \"Check if the API is healthy\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"API is healthy\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"status\": {\n                      \"type\": \"string\",\n                      \"enum\": [\"ok\"]\n                    },\n                    \"timestamp\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/public/info\": {\n      \"get\": {\n        \"summary\": \"Get public information\",\n        \"description\": \"Retrieve public information that doesn't require authentication\",\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"version\": {\n                      \"type\": \"string\"\n                    },\n                    \"features\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"string\"\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/data\": {\n      \"get\": {\n        \"summary\": \"Get data\",\n        \"description\": \"Retrieve data from the API\",\n        \"parameters\": [\n          {\n            \"name\": \"limit\",\n            \"in\": \"query\",\n            \"description\": \"Maximum number of items to return\",\n            \"schema\": {\n              \"type\": \"integer\",\n              \"minimum\": 1,\n              \"maximum\": 100,\n              \"default\": 10\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"array\",\n                  \"items\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"id\": {\n                        \"type\": \"integer\"\n                      },\n                      \"name\": {\n                        \"type\": \"string\"\n                      },\n                      \"description\": {\n                        \"type\": \"string\"\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      },\n      \"post\": {\n        \"summary\": \"Create data\",\n        \"description\": \"Create new data item\",\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"name\": {\n                    \"type\": \"string\"\n                  },\n                  \"description\": {\n                    \"type\": \"string\"\n                  }\n                },\n                \"required\": [\"name\"]\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"201\": {\n            \"description\": \"Data created successfully\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"integer\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    },\n                    \"description\": {\n                      \"type\": \"string\"\n                    },\n                    \"created_at\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/data/{id}\": {\n      \"get\": {\n        \"summary\": \"Get data by ID\",\n        \"description\": \"Retrieve a specific data item by its ID\",\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"description\": \"ID of the data item\",\n            \"schema\": {\n              \"type\": \"integer\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Successful response\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"integer\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    },\n                    \"description\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Data item not found\"\n          }\n        }\n      },\n      \"put\": {\n        \"summary\": \"Update data\",\n        \"description\": \"Update an existing data item\",\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"description\": \"ID of the data item\",\n            \"schema\": {\n              \"type\": \"integer\"\n            }\n          }\n        ],\n        \"requestBody\": {\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"name\": {\n                    \"type\": \"string\"\n                  },\n                  \"description\": {\n                    \"type\": \"string\"\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Data updated successfully\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"integer\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    },\n                    \"description\": {\n                      \"type\": \"string\"\n                    },\n                    \"updated_at\": {\n                      \"type\": \"string\"\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      },\n      \"delete\": {\n        \"summary\": \"Delete data\",\n        \"description\": \"Delete a data item\",\n        \"parameters\": [\n          {\n            \"name\": \"id\",\n            \"in\": \"path\",\n            \"required\": true,\n            \"description\": \"ID of the data item\",\n            \"schema\": {\n              \"type\": \"integer\"\n            }\n          }\n        ],\n        \"responses\": {\n          \"204\": {\n            \"description\": \"Data deleted successfully\"\n          },\n          \"404\": {\n            \"description\": \"Data item not found\"\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/import/openapi/import-openapi-json.spec.ts",
    "content": "import { test } from '../../../playwright';\nimport * as path from 'path';\nimport { closeAllCollections, importCollection } from '../../utils/page';\n\ntest.describe('Import OpenAPI v3 JSON Collection', () => {\n  test.afterEach(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('Import simple OpenAPI v3 JSON successfully', async ({ page, createTmpDir }) => {\n    const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-simple.json');\n\n    await importCollection(page, openApiFile, await createTmpDir('simple-test'), {\n      expectedCollectionName: 'Simple Test API'\n    });\n  });\n});\n"
  },
  {
    "path": "tests/import/openapi/import-openapi-with-examples.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\nimport { closeAllCollections, openCollection } from '../../utils/page';\n\ntest.describe('Import OpenAPI Collection with Examples', () => {\n  let originalShowOpenDialog;\n\n  test.beforeAll(async ({ electronApp }) => {\n    // save the original showOpenDialog function\n    await electronApp.evaluate(({ dialog }) => {\n      originalShowOpenDialog = dialog.showOpenDialog;\n    });\n  });\n\n  test.afterAll(async ({ electronApp, page }) => {\n    await closeAllCollections(page);\n    // restore the original showOpenDialog function\n    await electronApp.evaluate(({ dialog }) => {\n      dialog.showOpenDialog = originalShowOpenDialog;\n    });\n  });\n\n  test('should import OpenAPI collection with examples successfully', async ({ page, electronApp, createTmpDir }) => {\n    const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-with-examples.yaml');\n\n    // Create a temporary directory for the collection to be imported into\n    const importDir = await createTmpDir('imported-openapi-collection');\n\n    // Mock the electron dialog to return the import directory selection\n    await electronApp.evaluate(({ dialog }, { importDir }) => {\n      dialog.showOpenDialog = async () => ({\n        canceled: false,\n        filePaths: [importDir]\n      });\n    }, { importDir });\n\n    await test.step('Open import collection modal', async () => {\n      await page.getByTestId('collections-header-add-menu').click();\n      await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n    });\n\n    await test.step('Wait for import modal and verify title', async () => {\n      const importModal = page.getByRole('dialog');\n      await importModal.waitFor({ state: 'visible' });\n      await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n    });\n\n    await test.step('Upload OpenAPI collection file using hidden file input', async () => {\n      // The \"choose a file\" button triggers a hidden file input, so we can directly set files on it\n      await page.setInputFiles('input[type=\"file\"]', openApiFile);\n\n      // Wait for location modal to appear after file processing\n      const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n      await locationModal.waitFor({ state: 'visible', timeout: 10000 });\n    });\n\n    await test.step('Verify no parsing errors occurred', async () => {\n      const hasError = await page.getByText('Failed to parse the file').isVisible().catch(() => false);\n      if (hasError) {\n        throw new Error('Collection import failed with parsing error');\n      }\n    });\n\n    await test.step('Verify Import Collection location modal appears', async () => {\n      const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n      await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n      await expect(locationModal.getByText('API with Examples')).toBeVisible();\n    });\n\n    await test.step('Click Browse link to select collection folder', async () => {\n      const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n      await locationModal.getByText('Browse').click();\n    });\n\n    await test.step('Complete import by clicking import button', async () => {\n      const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n      await locationModal.getByRole('button', { name: 'Import' }).click();\n      await locationModal.waitFor({ state: 'hidden' });\n    });\n\n    await test.step('Handle sandbox modal', async () => {\n      await openCollection(page, 'API with Examples');\n    });\n\n    await test.step('Verify collection name appears in sidebar', async () => {\n      const collectionName = page.locator('#sidebar-collection-name').getByText('API with Examples');\n      await expect(collectionName).toBeVisible();\n    });\n\n    await test.step('Verify GET /users request exists and has examples', async () => {\n      const getUsersRequest = page.locator('.collection-item-name').getByText('Get all users');\n      await expect(getUsersRequest).toBeVisible();\n\n      // Find the chevron icon specifically for the \"Get all users\" request\n      const chevronIcon = page.getByTestId('request-item-chevron').nth(0);\n      await expect(chevronIcon).toBeVisible();\n\n      // Click the chevron to expand examples\n      await chevronIcon.click();\n\n      // Check if examples are visible\n      const successExample = page.locator('.collection-item-name').getByText('Success Response');\n      const emptyExample = page.locator('.collection-item-name').getByText('Empty Response');\n      const validationErrorExample = page.locator('.collection-item-name').getByText('Validation Error');\n      const serverErrorExample = page.locator('.collection-item-name').getByText('Server Error');\n\n      await expect(successExample).toBeVisible();\n      await expect(emptyExample).toBeVisible();\n      await expect(validationErrorExample).toBeVisible();\n      await expect(serverErrorExample).toBeVisible();\n\n      await chevronIcon.click();\n    });\n\n    await test.step('Verify POST /users request exists and has examples', async () => {\n      // Click on the POST request\n      const createUserRequest = page.locator('.collection-item-name').getByText('Create a new user');\n      await expect(createUserRequest).toBeVisible();\n      await createUserRequest.click();\n\n      // Find the chevron icon specifically for the \"Create a new user\" request\n      const chevronIcon = page.getByTestId('request-item-chevron').nth(1);\n      await expect(chevronIcon).toBeVisible();\n\n      // Click the chevron to expand examples\n      await chevronIcon.click();\n\n      // Check if examples are visible\n      const createdExample = page.locator('.collection-item-name').getByText('User Created (Valid User)');\n      const validationErrorExample = page.locator('.collection-item-name').getByText('Validation Error (Invalid User)');\n\n      await expect(createdExample).toBeVisible();\n      await expect(validationErrorExample).toBeVisible();\n    });\n\n    await test.step('Cleanup - close all collections', async () => {\n      await closeAllCollections(page);\n    });\n  });\n\n  test('should import OpenAPI collection with path-based grouping', async ({ page, electronApp, createTmpDir }) => {\n    const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-with-examples.yaml');\n\n    // Create a temporary directory for the collection to be imported into\n    const importDir = await createTmpDir('imported-openapi-collection-path');\n\n    // Mock the electron dialog to return the import directory selection\n    await electronApp.evaluate(({ dialog }, { importDir }) => {\n      dialog.showOpenDialog = async () => ({\n        canceled: false,\n        filePaths: [importDir]\n      });\n    }, { importDir });\n\n    await test.step('Open import collection modal', async () => {\n      await page.getByTestId('collections-header-add-menu').click();\n      await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n    });\n\n    await test.step('Wait for import modal and verify title', async () => {\n      const importModal = page.getByRole('dialog');\n      await importModal.waitFor({ state: 'visible' });\n      await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n    });\n\n    await test.step('Upload OpenAPI collection file using hidden file input', async () => {\n      await page.setInputFiles('input[type=\"file\"]', openApiFile);\n\n      // Wait for location modal to appear after file processing\n      const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n      await locationModal.waitFor({ state: 'visible', timeout: 10000 });\n    });\n\n    await test.step('Verify no parsing errors occurred', async () => {\n      const hasError = await page.getByText('Failed to parse the file').isVisible().catch(() => false);\n      if (hasError) {\n        throw new Error('Collection import failed with parsing error');\n      }\n    });\n\n    await test.step('Verify Import Collection location modal appears', async () => {\n      const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n      await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n      await expect(locationModal.getByText('API with Examples')).toBeVisible();\n    });\n\n    await test.step('Select path-based grouping option from dropdown', async () => {\n      const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n\n      // Click on the grouping dropdown to open it\n      const groupingDropdown = locationModal.getByTestId('grouping-dropdown');\n      await groupingDropdown.click();\n\n      // Wait for dropdown to open and select \"Paths\" option (note: it's \"Paths\" not \"Path\")\n      const pathOption = page.getByTestId('grouping-option-path');\n      await pathOption.click();\n    });\n\n    await test.step('Click Browse link to select collection folder', async () => {\n      const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n      await locationModal.getByText('Browse').click();\n    });\n\n    await test.step('Complete import by clicking import button', async () => {\n      const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n      await locationModal.getByRole('button', { name: 'Import' }).click();\n      await locationModal.waitFor({ state: 'hidden' });\n    });\n\n    await test.step('Handle sandbox modal', async () => {\n      await openCollection(page, 'API with Examples');\n    });\n\n    await test.step('Verify collection name appears in sidebar', async () => {\n      const collectionName = page.locator('#sidebar-collection-name').getByText('API with Examples');\n      await expect(collectionName).toBeVisible();\n    });\n\n    await test.step('Verify path-based grouping structure', async () => {\n      // With path-based grouping, requests should be organized by their path\n      // users should be a folder containing GET and POST requests\n      const usersFolder = page.locator('.collection-item-name').getByText('users');\n      await expect(usersFolder).toBeVisible();\n\n      // Click on the users folder to expand it\n      await usersFolder.click();\n\n      // Verify that the requests are inside the users folder\n      const getUsersRequest = page.locator('.collection-item-name').getByText('Get all users');\n      const createUserRequest = page.locator('.collection-item-name').getByText('Create a new user');\n\n      await expect(getUsersRequest).toBeVisible();\n      await expect(createUserRequest).toBeVisible();\n    });\n\n    await test.step('Verify examples work with path-based grouping', async () => {\n      // Test GET /users request examples\n      const getUsersRequest = page.locator('.collection-item-name').getByText('Get all users');\n      await expect(getUsersRequest).toBeVisible();\n\n      const chevronIcon = page.getByTestId('request-item-chevron').nth(0);\n      await expect(chevronIcon).toBeVisible();\n      await chevronIcon.click();\n\n      // Check if examples are visible\n      const successExample = page.locator('.collection-item-name').getByText('Success Response');\n      await expect(successExample).toBeVisible();\n    });\n  });\n});\n"
  },
  {
    "path": "tests/import/openapi/import-openapi-yaml.spec.ts",
    "content": "import { test } from '../../../playwright';\nimport * as path from 'path';\nimport { closeAllCollections, importCollection } from '../../utils/page';\n\ntest.describe('Import OpenAPI v3 YAML Collection', () => {\n  test.afterEach(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('Import comprehensive OpenAPI v3 YAML successfully', async ({ page, createTmpDir }) => {\n    const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-comprehensive.yaml');\n\n    await importCollection(page, openApiFile, await createTmpDir('comprehensive-test'), {\n      expectedCollectionName: 'Comprehensive API Test Collection'\n    });\n  });\n});\n"
  },
  {
    "path": "tests/import/openapi/malformed-yaml.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\n\ntest.describe('Invalid OpenAPI - Malformed YAML', () => {\n  test('Handle malformed OpenAPI YAML structure', async ({ page }) => {\n    const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-malformed.yaml');\n\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // Wait for import collection modal to be ready\n    const importModal = page.getByRole('dialog');\n    await importModal.waitFor({ state: 'visible' });\n    await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n\n    await page.setInputFiles('input[type=\"file\"]', openApiFile);\n\n    const parseError = page.getByText('Failed to parse the file');\n    const importError = page.getByText('Import collection failed');\n\n    // Wait for at least one error message to be visible\n    await expect(parseError.or(importError)).toBeVisible({ timeout: 10000 });\n\n    // Cleanup: close any open modals\n    await page.getByTestId('modal-close-button').click();\n  });\n});\n"
  },
  {
    "path": "tests/import/openapi/missing-info.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\n\ntest.describe('Invalid OpenAPI - Missing Info Section', () => {\n  test('Handle OpenAPI specification missing required info section', async ({ page }) => {\n    const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-missing-info.yaml');\n\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // Wait for import collection modal to be ready\n    const importModal = page.getByRole('dialog');\n    await importModal.waitFor({ state: 'visible' });\n    await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n\n    await page.setInputFiles('input[type=\"file\"]', openApiFile);\n\n    const errorMessage = page.getByText('Unsupported collection format').first();\n    await expect(errorMessage).toBeVisible({ timeout: 10000 });\n\n    // Cleanup: close any open modals\n    await page.getByTestId('modal-close-button').click();\n  });\n});\n"
  },
  {
    "path": "tests/import/openapi/operation-name-with-newlines-fix.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\nimport { closeAllCollections } from '../../utils/page';\n\ntest.describe('OpenAPI Newline Handling', () => {\n  test.afterEach(async ({ page }) => {\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  test('should handle operation names with newlines', async ({ page, createTmpDir }) => {\n    const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-newline-in-operation-name.yaml');\n\n    // start the import process\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // wait for the import collection modal to appear\n    const importModal = page.getByTestId('import-collection-modal');\n    await importModal.waitFor({ state: 'visible' });\n\n    // upload the OpenAPI file with problematic operation names\n    await page.setInputFiles('input[type=\"file\"]', openApiFile);\n\n    // Wait for location modal to appear after file processing\n    const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n    await locationModal.waitFor({ state: 'visible', timeout: 10000 });\n    await expect(locationModal.getByText('Newline Test Collection')).toBeVisible();\n\n    // select a location\n    await page.locator('#collection-location').fill(await createTmpDir('newline-test'));\n    await locationModal.getByRole('button', { name: 'Import' }).click();\n    await locationModal.waitFor({ state: 'hidden' });\n\n    // verify the collection was imported successfully\n    await expect(page.locator('#sidebar-collection-name').getByText('Newline Test Collection')).toBeVisible();\n\n    // configure the collection settings\n    await page.locator('#sidebar-collection-name').getByText('Newline Test Collection').click();\n\n    // verify that all requests were imported correctly despite newlines in operation names\n    // the parser should clean up the operation names and create valid request names\n    await expect(page.locator('#collection-newline-test-collection .collection-item-name')).toHaveCount(2);\n  });\n});\n"
  },
  {
    "path": "tests/import/openapi/path-based-grouping.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\nimport { closeAllCollections } from '../../utils/page';\n\ntest.describe('OpenAPI Path-Based Grouping', () => {\n  test.afterEach(async ({ page }) => {\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  test('should import with path-based folder grouping', async ({ page, createTmpDir }) => {\n    const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-path-grouping.json');\n\n    // Start the import process\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // Wait for import collection modal to be ready\n    const importModal = page.getByTestId('import-collection-modal');\n    await importModal.waitFor({ state: 'visible' });\n\n    // Upload the OpenAPI file\n    await page.setInputFiles('input[type=\"file\"]', openApiFile);\n\n    // Wait for location modal to appear after file processing\n    const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n    await locationModal.waitFor({ state: 'visible', timeout: 10000 });\n    await expect(locationModal.getByText('Path Grouping Test API')).toBeVisible();\n\n    // Select path-based grouping from dropdown\n    await page.getByTestId('grouping-dropdown').click();\n    await page.getByTestId('grouping-option-path').click();\n\n    // Select a location and import\n    await page.locator('#collection-location').fill(await createTmpDir('path-grouping-test'));\n    await locationModal.getByRole('button', { name: 'Import' }).click();\n    await locationModal.waitFor({ state: 'hidden' });\n\n    // Verify the collection was imported successfully\n    await expect(page.locator('#sidebar-collection-name').getByText('Path Grouping Test API')).toBeVisible();\n\n    // Configure the collection settings\n    await page.locator('#sidebar-collection-name').getByText('Path Grouping Test API').click();\n\n    // Verify path-based folder structure was created\n    // Should have 'users' and 'products' folders\n    await expect(page.locator('.collection-item-name').getByText('users')).toBeVisible();\n    await expect(page.locator('.collection-item-name').getByText('products')).toBeVisible();\n\n    // Expand the products folder to check for nested structure\n    await page.locator('.collection-item-name').getByText('products').click();\n\n    // Verify that the products folder contains the {id} subfolder\n    await expect(page.locator('.collection-item-name').getByText('{id}')).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/import/openapi/security-schemes-import.spec.ts",
    "content": "import { test } from '../../../playwright';\nimport * as path from 'path';\nimport { closeAllCollections, importCollection } from '../../utils/page';\n\ntest.describe('OpenAPI Security Schemes Import', () => {\n  test.afterEach(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('Import OpenAPI spec with security schemes', async ({ page, createTmpDir }) => {\n    const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-with-security-schemes.json');\n\n    await importCollection(page, openApiFile, await createTmpDir('openapi-with-security'), {\n      expectedCollectionName: 'API with Security Schemes'\n    });\n  });\n\n  test('Import OpenAPI spec without security schemes', async ({ page, createTmpDir }) => {\n    const openApiFile = path.resolve(__dirname, 'fixtures', 'openapi-without-security-schemes.json');\n\n    await importCollection(page, openApiFile, await createTmpDir('openapi-without-security'), {\n      expectedCollectionName: 'API without Security Schemes'\n    });\n  });\n});\n"
  },
  {
    "path": "tests/import/postman/fixtures/postman-invalid-missing-info.json",
    "content": "{\n  \"item\": [\n    {\n      \"name\": \"Request without info\",\n      \"request\": {\n        \"method\": \"GET\",\n        \"url\": \"https://example.com\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/import/postman/fixtures/postman-invalid-schema.json",
    "content": "{\n  \"info\": {\n    \"name\": \"Invalid Schema Collection\",\n    \"schema\": \"https://schema.getpostman.com/json/collection/v999.0.0/collection.json\"\n  },\n  \"item\": []\n}\n"
  },
  {
    "path": "tests/import/postman/fixtures/postman-malformed.json",
    "content": "{\n  \"info\": {\n    \"name\": \"Malformed Collection\"\n  },\n  \"item\": \"this should be an array, not a string\"\n}\n"
  },
  {
    "path": "tests/import/postman/fixtures/postman-v20.json",
    "content": "{\n  \"info\": {\n    \"name\": \"Postman v2.0 Collection\",\n    \"description\": \"Test collection using Postman Collection Format v2.0\",\n    \"schema\": \"https://schema.getpostman.com/json/collection/v2.0.0/collection.json\"\n  },\n  \"item\": [\n    {\n      \"name\": \"Get Posts\",\n      \"request\": {\n        \"method\": \"GET\",\n        \"header\": [],\n        \"url\": \"https://jsonplaceholder.typicode.com/posts\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/import/postman/fixtures/postman-v21.json",
    "content": "{\n  \"info\": {\n    \"name\": \"Postman v2.1 Collection\",\n    \"description\": \"Test collection using Postman Collection Format v2.1\",\n    \"schema\": \"https://schema.getpostman.com/json/collection/v2.1.0/collection.json\",\n    \"_postman_id\": \"12345678-1234-1234-1234-123456789012\"\n  },\n  \"item\": [\n    {\n      \"name\": \"Get Users\",\n      \"request\": {\n        \"method\": \"GET\",\n        \"header\": [\n          {\n            \"key\": \"Authorization\",\n            \"value\": \"Bearer {{token}}\",\n            \"type\": \"text\"\n          }\n        ],\n        \"url\": {\n          \"raw\": \"{{baseUrl}}/users\",\n          \"host\": [\"{{baseUrl}}\"],\n          \"path\": [\"users\"],\n          \"query\": [\n            {\n              \"key\": \"page\",\n              \"value\": \"1\"\n            }\n          ]\n        }\n      },\n      \"response\": []\n    },\n    {\n      \"name\": \"Create User\",\n      \"request\": {\n        \"method\": \"POST\",\n        \"header\": [\n          {\n            \"key\": \"Content-Type\",\n            \"value\": \"application/json\",\n            \"type\": \"text\"\n          }\n        ],\n        \"body\": {\n          \"mode\": \"raw\",\n          \"raw\": \"{\\n  \\\"name\\\": \\\"John Doe\\\",\\n  \\\"email\\\": \\\"john@example.com\\\"\\n}\"\n        },\n        \"url\": {\n          \"raw\": \"{{baseUrl}}/users\",\n          \"host\": [\"{{baseUrl}}\"],\n          \"path\": [\"users\"]\n        }\n      },\n      \"response\": []\n    }\n  ],\n  \"variable\": [\n    {\n      \"key\": \"baseUrl\",\n      \"value\": \"https://api.example.com\"\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/import/postman/fixtures/postman-with-examples.json",
    "content": "{\n  \"info\": {\n    \"_postman_id\": \"d7b47cc4-c3c5-4c9d-99d4-04b6025c9000\",\n    \"name\": \"collection with examples\",\n    \"schema\": \"https://schema.getpostman.com/json/collection/v2.1.0/collection.json\",\n    \"_exporter_id\": \"41238764\"\n  },\n  \"item\": [\n    {\n      \"name\": \"New Request\",\n      \"request\": {\n        \"method\": \"GET\",\n        \"header\": [],\n        \"url\": {\n          \"raw\": \"https://testbench-sanity.usebruno.com/ping\",\n          \"protocol\": \"https\",\n          \"host\": [\n            \"testbench-sanity\",\n            \"usebruno\",\n            \"com\"\n          ],\n          \"path\": [\n            \"ping\"\n          ]\n        }\n      },\n      \"response\": [\n        {\n          \"name\": \"Success Response\",\n          \"originalRequest\": {\n            \"method\": \"GET\",\n            \"header\": [],\n            \"url\": {\n              \"raw\": \"https://testbench-sanity.usebruno.com/ping\",\n              \"protocol\": \"https\",\n              \"host\": [\n                \"testbench-sanity\",\n                \"usebruno\",\n                \"com\"\n              ],\n              \"path\": [\n                \"ping\"\n              ]\n            }\n          },\n          \"status\": \"OK\",\n          \"code\": 200,\n          \"_postman_previewlanguage\": \"json\",\n          \"header\": [\n            {\n              \"key\": \"Content-Type\",\n              \"value\": \"application/json\",\n              \"name\": \"Content-Type\",\n              \"description\": \"\",\n              \"type\": \"text\"\n            },\n            {\n              \"key\": \"x-powered-by\",\n              \"value\": \"Express\"\n            }\n          ],\n          \"cookie\": [],\n          \"body\": \"{\\n    \\\"ping\\\": \\\"pong\\\"\\n}\"\n        },\n        {\n          \"name\": \"Error Response\",\n          \"originalRequest\": {\n            \"method\": \"GET\",\n            \"header\": [\n              {\n                \"key\": \"Content-Type\",\n                \"value\": \"application/json\",\n                \"type\": \"text\"\n              }\n            ],\n            \"body\": {\n              \"mode\": \"raw\",\n              \"raw\": \"{\\n    \\\"ping\\\": \\\"pong\\\"\\n}\",\n              \"options\": {\n                \"raw\": {\n                  \"language\": \"json\"\n                }\n              }\n            },\n            \"url\": {\n              \"raw\": \"https://testbench-sanity.usebruno.com/ping\",\n              \"protocol\": \"https\",\n              \"host\": [\n                \"testbench-sanity\",\n                \"usebruno\",\n                \"com\"\n              ],\n              \"path\": [\n                \"ping\"\n              ]\n            }\n          },\n          \"status\": \"Internal Server Error\",\n          \"code\": 500,\n          \"_postman_previewlanguage\": \"json\",\n          \"header\": [\n            {\n              \"key\": \"Content-Type\",\n              \"value\": \"application/json\",\n              \"name\": \"Content-Type\",\n              \"description\": \"\",\n              \"type\": \"text\"\n            }\n          ],\n          \"cookie\": [],\n          \"body\": \"{\\n    \\\"error\\\": \\\"Internal Server Error\\\"\\n}\"\n        }\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "tests/import/postman/import-postman-v20.spec.ts",
    "content": "import { test } from '../../../playwright';\nimport * as path from 'path';\nimport { closeAllCollections, importCollection } from '../../utils/page';\n\ntest.describe('Import Postman Collection v2.0', () => {\n  test.afterEach(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('Import Postman Collection v2.0 successfully', async ({ page, createTmpDir }) => {\n    const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-v20.json');\n\n    await importCollection(page, postmanFile, await createTmpDir('postman-v20-test'), {\n      expectedCollectionName: 'Postman v2.0 Collection'\n    });\n  });\n});\n"
  },
  {
    "path": "tests/import/postman/import-postman-v21.spec.ts",
    "content": "import { test } from '../../../playwright';\nimport * as path from 'path';\nimport { closeAllCollections, importCollection } from '../../utils/page';\n\ntest.describe('Import Postman Collection v2.1', () => {\n  test.afterEach(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('Import Postman Collection v2.1 successfully', async ({ page, createTmpDir }) => {\n    const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-v21.json');\n\n    await importCollection(page, postmanFile, await createTmpDir('postman-v21-test'), {\n      expectedCollectionName: 'Postman v2.1 Collection'\n    });\n  });\n});\n"
  },
  {
    "path": "tests/import/postman/import-postman-with-examples.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\nimport { closeAllCollections, openCollection } from '../../utils/page';\n\ntest.describe('Import Postman Collection with Examples', () => {\n  let originalShowOpenDialog;\n\n  test.beforeAll(async ({ electronApp }) => {\n    // save the original showOpenDialog function\n    await electronApp.evaluate(({ dialog }) => {\n      originalShowOpenDialog = dialog.showOpenDialog;\n    });\n  });\n\n  test.afterAll(async ({ electronApp, page }) => {\n    await closeAllCollections(page);\n    // restore the original showOpenDialog function\n    await electronApp.evaluate(({ dialog }) => {\n      dialog.showOpenDialog = originalShowOpenDialog;\n    });\n  });\n\n  test('should import Postman collection with examples successfully', async ({ page, electronApp, createTmpDir }) => {\n    const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-with-examples.json');\n\n    // Create a temporary directory for the collection to be imported into\n    const importDir = await createTmpDir('imported-collection');\n\n    // Mock the electron dialog to return the import directory selection\n    await electronApp.evaluate(({ dialog }, { importDir }) => {\n      dialog.showOpenDialog = async () => ({\n        canceled: false,\n        filePaths: [importDir]\n      });\n    }, { importDir });\n\n    await test.step('Open import collection modal', async () => {\n      await page.getByTestId('collections-header-add-menu').click();\n      await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n    });\n\n    await test.step('Wait for import modal and verify title', async () => {\n      const importModal = page.getByRole('dialog');\n      await importModal.waitFor({ state: 'visible' });\n      await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n    });\n\n    await test.step('Upload Postman collection file using hidden file input', async () => {\n      // The \"choose a file\" button triggers a hidden file input, so we can directly set files on it\n      await page.setInputFiles('input[type=\"file\"]', postmanFile);\n\n      // Wait for location modal to appear after file processing\n      const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n      await locationModal.waitFor({ state: 'visible', timeout: 10000 });\n    });\n\n    await test.step('Verify no parsing errors occurred', async () => {\n      const hasError = await page.getByText('Failed to parse the file').isVisible().catch(() => false);\n      if (hasError) {\n        throw new Error('Collection import failed with parsing error');\n      }\n    });\n\n    await test.step('Verify location selection modal appears', async () => {\n      const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n      await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n    });\n\n    await test.step('Verify collection name appears in location modal', async () => {\n      const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n      await expect(locationModal.getByText('collection with examples')).toBeVisible();\n    });\n\n    await test.step('Click Browse link to select collection folder', async () => {\n      const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n      await locationModal.getByText('Browse').click();\n    });\n\n    await test.step('Complete import by clicking import button', async () => {\n      const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n      await locationModal.getByRole('button', { name: 'Import' }).click();\n      await locationModal.waitFor({ state: 'hidden' });\n    });\n\n    await test.step('Open collection', async () => {\n      await openCollection(page, 'collection with examples');\n    });\n\n    await test.step('Verify collection name appears in sidebar', async () => {\n      const collectionName = page.locator('#sidebar-collection-name').getByText('collection with examples');\n      await expect(collectionName).toBeVisible();\n    });\n\n    await test.step('Verify request exists in the collection', async () => {\n      const requestItem = page.locator('.collection-item-name').getByText('New Request');\n      await expect(requestItem).toBeVisible();\n    });\n\n    await test.step('Click chevron to expand examples', async () => {\n      const chevronIcon = page.getByTestId('request-item-chevron');\n      await expect(chevronIcon).toBeVisible();\n      await chevronIcon.click();\n    });\n\n    await test.step('Verify both examples are visible', async () => {\n      const successExample = page.locator('.collection-item-name').getByText('Success Response');\n      const errorExample = page.locator('.collection-item-name').getByText('Error Response');\n\n      await expect(successExample).toBeVisible();\n      await expect(errorExample).toBeVisible();\n    });\n  });\n});\n"
  },
  {
    "path": "tests/import/postman/invalid-json.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\n\ntest.describe('Invalid Postman Collection - Invalid JSON', () => {\n  test('Handle invalid JSON syntax', async ({ page }) => {\n    const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-invalid-schema.json');\n\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // Wait for import collection modal to be ready\n    const importModal = page.getByRole('dialog');\n    await importModal.waitFor({ state: 'visible' });\n    await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n\n    await page.setInputFiles('input[type=\"file\"]', postmanFile);\n\n    // Check for error message\n    await expect(page.getByText('Unsupported collection format').first()).toBeVisible();\n\n    // Cleanup: close any open modals\n    await page.getByTestId('modal-close-button').click();\n  });\n});\n"
  },
  {
    "path": "tests/import/postman/invalid-missing-info.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\n\ntest.describe('Invalid Postman Collection - Missing Info', () => {\n  test('Handle Postman collection missing required info field', async ({ page }) => {\n    const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-invalid-missing-info.json');\n\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // Wait for import collection modal to be ready\n    const importModal = page.getByRole('dialog');\n    await importModal.waitFor({ state: 'visible' });\n    await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n\n    await page.setInputFiles('input[type=\"file\"]', postmanFile);\n\n    // Check for error message\n    await expect(page.getByText('Unsupported collection format').first()).toBeVisible();\n\n    // Cleanup: close any open modals\n    await page.getByTestId('modal-close-button').click();\n  });\n});\n"
  },
  {
    "path": "tests/import/postman/invalid-schema.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\n\ntest.describe('Invalid Postman Collection - Invalid Schema', () => {\n  test('Handle Postman collection with invalid schema version', async ({ page }) => {\n    const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-invalid-schema.json');\n\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // Wait for import collection modal to be ready\n    const importModal = page.getByRole('dialog');\n    await importModal.waitFor({ state: 'visible' });\n    await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n\n    await page.setInputFiles('input[type=\"file\"]', postmanFile);\n\n    // Check for error message\n    await expect(page.getByText('Unsupported collection format').first()).toBeVisible();\n\n    // Cleanup: close any open modals\n    await page.getByTestId('modal-close-button').click();\n  });\n});\n"
  },
  {
    "path": "tests/import/postman/malformed-structure.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\n\ntest.describe('Invalid Postman Collection - Malformed Structure', () => {\n  test('Handle malformed Postman collection structure', async ({ page }) => {\n    const postmanFile = path.resolve(__dirname, 'fixtures', 'postman-malformed.json');\n\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // Wait for import collection modal to be ready\n    const importModal = page.getByRole('dialog');\n    await importModal.waitFor({ state: 'visible' });\n    await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n\n    await page.setInputFiles('input[type=\"file\"]', postmanFile);\n\n    const errorLocator = page.getByText(/Unsupported collection format|Failed to parse|Invalid|Error/).first();\n    await expect(errorLocator).toBeVisible({ timeout: 10000 });\n\n    // Cleanup: close any open modals\n    await page.getByTestId('modal-close-button').click();\n  });\n});\n"
  },
  {
    "path": "tests/import/test-data/sample-bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"uid\": \"bruno_test_collection_1\",\n  \"name\": \"Sample Bruno Collection\",\n  \"items\": [\n    {\n      \"uid\": \"bruno_test_request_1\",\n      \"type\": \"http-request\",\n      \"name\": \"Get Sample Data\",\n      \"seq\": 1,\n      \"request\": {\n        \"url\": \"https://jsonplaceholder.typicode.com/todos/1\",\n        \"method\": \"GET\",\n        \"headers\": [],\n        \"params\": [],\n        \"body\": {\n          \"mode\": \"none\"\n        },\n        \"auth\": {\n          \"mode\": \"none\"\n        },\n        \"script\": {},\n        \"vars\": {},\n        \"assertions\": [],\n        \"tests\": \"\",\n        \"docs\": \"\"\n      }\n    }\n  ],\n  \"environments\": [],\n  \"activeEnvironmentUid\": null,\n  \"root\": {\n    \"request\": {\n      \"headers\": [],\n      \"auth\": {\n        \"mode\": \"none\"\n      },\n      \"script\": {},\n      \"vars\": {},\n      \"tests\": \"\"\n    }\n  }\n}\n"
  },
  {
    "path": "tests/import/test-data/sample-insomnia.json",
    "content": "{\n  \"_type\": \"export\",\n  \"__export_format\": 4,\n  \"__export_date\": \"2023-01-01T00:00:00.000Z\",\n  \"__export_source\": \"insomnia.desktop.app:v2023.1.0\",\n  \"resources\": [\n    {\n      \"_id\": \"req_123\",\n      \"authentication\": {},\n      \"body\": {},\n      \"created\": 1672531200000,\n      \"description\": \"\",\n      \"headers\": [],\n      \"isPrivate\": false,\n      \"metaSortKey\": -1672531200000,\n      \"method\": \"GET\",\n      \"modified\": 1672531200000,\n      \"name\": \"Get Posts\",\n      \"parameters\": [],\n      \"parentId\": \"wrk_456\",\n      \"settingDisableRenderRequestBody\": false,\n      \"settingEncodeUrl\": true,\n      \"settingFollowRedirects\": \"global\",\n      \"settingRebuildPath\": true,\n      \"settingSendCookies\": true,\n      \"settingStoreCookies\": true,\n      \"url\": \"https://jsonplaceholder.typicode.com/posts\",\n      \"_type\": \"request\"\n    },\n    {\n      \"_id\": \"wrk_456\",\n      \"created\": 1672531200000,\n      \"description\": \"Sample Insomnia collection for testing\",\n      \"modified\": 1672531200000,\n      \"name\": \"Sample Insomnia Collection\",\n      \"parentId\": null,\n      \"scope\": \"collection\",\n      \"_type\": \"workspace\"\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/import/test-data/sample-openapi.yaml",
    "content": "openapi: 3.0.0\ninfo:\n  title: Sample API\n  description: A simple API for testing OpenAPI imports\n  version: 1.0.0\nservers:\n  - url: https://jsonplaceholder.typicode.com\npaths:\n  /posts:\n    get:\n      summary: Get all posts\n      description: Retrieve a list of all posts\n      responses:\n        '200':\n          description: List of posts\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: object\n                  properties:\n                    id:\n                      type: integer\n                    title:\n                      type: string\n                    body:\n                      type: string\n                    userId:\n                      type: integer\n    post:\n      summary: Create a new post\n      description: Create a new post\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              type: object\n              properties:\n                title:\n                  type: string\n                body:\n                  type: string\n                userId:\n                  type: integer\n      responses:\n        '201':\n          description: Post created successfully\n  /posts/{id}:\n    get:\n      summary: Get a specific post\n      parameters:\n        - name: id\n          in: path\n          required: true\n          schema:\n            type: integer\n      responses:\n        '200':\n          description: Post details\n"
  },
  {
    "path": "tests/import/test-data/sample-postman.json",
    "content": "{\n  \"info\": {\n    \"name\": \"Sample Postman Collection\",\n    \"description\": \"A simple collection for testing imports\",\n    \"schema\": \"https://schema.getpostman.com/json/collection/v2.1.0/collection.json\"\n  },\n  \"item\": [\n    {\n      \"name\": \"Get Users\",\n      \"request\": {\n        \"method\": \"GET\",\n        \"header\": [],\n        \"url\": {\n          \"raw\": \"https://jsonplaceholder.typicode.com/users\",\n          \"protocol\": \"https\",\n          \"host\": [\"jsonplaceholder\", \"typicode\", \"com\"],\n          \"path\": [\"users\"]\n        }\n      }\n    },\n    {\n      \"name\": \"Create User\",\n      \"request\": {\n        \"method\": \"POST\",\n        \"header\": [\n          {\n            \"key\": \"Content-Type\",\n            \"value\": \"application/json\"\n          }\n        ],\n        \"body\": {\n          \"mode\": \"raw\",\n          \"raw\": \"{\\n  \\\"name\\\": \\\"John Doe\\\",\\n  \\\"email\\\": \\\"john@example.com\\\"\\n}\"\n        },\n        \"url\": {\n          \"raw\": \"https://jsonplaceholder.typicode.com/users\",\n          \"protocol\": \"https\",\n          \"host\": [\"jsonplaceholder\", \"typicode\", \"com\"],\n          \"path\": [\"users\"]\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/import/url-import/github-repository-import.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections } from '../../utils/page';\n\ntest.describe('GitHub Repository URL Import', () => {\n  test.afterEach(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('GitHub repository URL import', async ({ page }) => {\n    const githubUrl = 'https://github.com/usebruno/github-rest-api-collection';\n\n    // Test GitHub repository import\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // Wait for import collection modal to be ready\n    const importModal = page.getByRole('dialog');\n    await importModal.waitFor({ state: 'visible' });\n    await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n\n    // Select the GitHub tab\n    await page.getByTestId('github-tab').click();\n\n    // Fill in the URL input\n    await page.getByTestId('git-url-input').fill(githubUrl);\n    await page.locator('#clone-git-button').click();\n\n    // Wait for the loader to disappear\n    await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });\n\n    // Verify that the Clone Git Repository modal is displayed\n    const cloneModal = page.getByRole('dialog');\n    await expect(cloneModal.locator('.bruno-modal-header-title')).toContainText('Clone Git Repository');\n\n    // Cleanup: close any open modals using Cancel button (avoids form validation)\n    await page.getByRole('button', { name: 'Cancel' }).click();\n  });\n});\n"
  },
  {
    "path": "tests/import/url-import/insomnia-url-import.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections, openCollection } from '../../utils/page';\n\ntest.describe('Insomnia URL Import', () => {\n  test.afterEach(async ({ page }) => {\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  test('Insomnia URL import', async ({ page, createTmpDir }) => {\n    const insomniaUrl = 'https://raw.githubusercontent.com/usebruno/bruno/refs/heads/main/tests/import/insomnia/fixtures/insomnia-v5.yaml';\n\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // Wait for import collection modal to be ready\n    const importModal = page.getByTestId('import-collection-modal');\n    await importModal.waitFor({ state: 'visible' });\n\n    await page.getByTestId('url-tab').click();\n    await page.getByTestId('url-input').waitFor({ state: 'visible' });\n    await page.getByTestId('url-input').fill(insomniaUrl);\n    await page.locator('#import-url-button').click();\n\n    // Wait for the loader to disappear\n    await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });\n\n    // Verify that the collection location modal appears\n    const locationModal = page.getByTestId('import-collection-location-modal');\n    await expect(locationModal.getByText('Test API Collection v5')).toBeVisible();\n\n    // Select a location and import\n    await page.locator('#collection-location').fill(await createTmpDir('test-api-collection-v5'));\n    await locationModal.getByRole('button', { name: 'Import' }).click();\n    await locationModal.waitFor({ state: 'hidden' });\n\n    // Verify the collection was imported successfully and configure it\n    await expect(page.locator('#sidebar-collection-name').getByText('Test API Collection v5')).toBeVisible();\n    await openCollection(page, 'Test API Collection v5');\n\n    // Verify these folder names are present\n    await expect(page.locator('.collection-item-name').getByText('API Tests')).toBeVisible();\n    await expect(page.locator('.collection-item-name').getByText('Data Management')).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/import/url-import/openapi-url-import.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections, openCollection } from '../../utils/page';\n\ntest.describe('OpenAPI URL Import', () => {\n  test.afterEach(async ({ page }) => {\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  test('Swagger/OpenAPI URL import', async ({ page, createTmpDir }) => {\n    const openapiUrl = 'https://petstore.swagger.io/v2/swagger.json';\n\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // Wait for import collection modal to be ready\n    const importModal = page.getByTestId('import-collection-modal');\n    await importModal.waitFor({ state: 'visible' });\n\n    await page.getByTestId('url-tab').click();\n    await page.getByTestId('url-input').waitFor({ state: 'visible' });\n    await page.getByTestId('url-input').fill(openapiUrl);\n    await page.locator('#import-url-button').click();\n\n    // Wait for the loader to disappear\n    await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });\n\n    // Verify that the collection location modal appears with OpenAPI settings\n    const locationModal = page.getByTestId('import-collection-location-modal');\n    await expect(locationModal.getByText('Swagger Petstore')).toBeVisible();\n\n    // Verify OpenAPI settings are available in the location modal\n    await expect(locationModal.getByText('Folder arrangement')).toBeVisible();\n    await expect(locationModal.getByTestId('grouping-dropdown')).toBeVisible();\n\n    // Select a location and import with default grouping (tags)\n    await page.locator('#collection-location').fill(await createTmpDir('swagger-petstore'));\n    await locationModal.getByRole('button', { name: 'Import' }).click();\n    await locationModal.waitFor({ state: 'hidden' });\n\n    // Verify the collection was imported successfully and configure it\n    await expect(page.locator('#sidebar-collection-name').getByText('Swagger Petstore')).toBeVisible();\n    await openCollection(page, 'Swagger Petstore');\n\n    // Verify these folder names are present (tag-based grouping)\n    await expect(page.locator('.collection-item-name').getByText('pet')).toBeVisible();\n    await expect(page.locator('.collection-item-name').getByText('store')).toBeVisible();\n    await expect(page.locator('.collection-item-name').getByText('user')).toBeVisible();\n  });\n\n  test('Swagger/OpenAPI URL import with path-based grouping', async ({ page, createTmpDir }) => {\n    const openapiUrl = 'https://petstore.swagger.io/v2/swagger.json';\n\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // Wait for import collection modal to be ready\n    const importModal = page.getByTestId('import-collection-modal');\n    await importModal.waitFor({ state: 'visible' });\n\n    await page.getByTestId('url-tab').click();\n    await page.getByTestId('url-input').waitFor({ state: 'visible' });\n    await page.getByTestId('url-input').fill(openapiUrl);\n    await page.locator('#import-url-button').click();\n\n    // Wait for the loader to disappear\n    await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });\n\n    // Verify that the collection location modal appears with OpenAPI settings\n    const locationModal = page.getByTestId('import-collection-location-modal');\n    await expect(locationModal.getByText('Swagger Petstore')).toBeVisible();\n\n    // Verify OpenAPI settings are available in the location modal\n    await expect(locationModal.getByText('Folder arrangement')).toBeVisible();\n\n    // Select path-based grouping from the dropdown\n    await locationModal.getByTestId('grouping-dropdown').click();\n\n    // Wait for dropdown options to be visible and select path-based grouping\n    await page.getByTestId('grouping-option-path').waitFor({ state: 'visible' });\n    await page.getByTestId('grouping-option-path').click();\n\n    // Select a location and import with path-based grouping\n    await page.locator('#collection-location').fill(await createTmpDir('swagger-petstore-path'));\n    await locationModal.getByRole('button', { name: 'Import' }).click();\n    await locationModal.waitFor({ state: 'hidden' });\n\n    // Verify the collection was imported successfully and configure it\n    await expect(page.locator('#sidebar-collection-name').getByText('Swagger Petstore')).toBeVisible();\n    await openCollection(page, 'Swagger Petstore');\n\n    // Verify that the collection has been imported with path-based grouping\n    // Should have folders based on URL paths like 'pet', 'store', 'user'\n    await expect(page.locator('.collection-item-name').getByText('pet')).toBeVisible();\n    await expect(page.locator('.collection-item-name').getByText('store')).toBeVisible();\n    await expect(page.locator('.collection-item-name').getByText('user')).toBeVisible();\n\n    // Expand the pet folder to check for nested path structure\n    await page.locator('.collection-item-name').getByText('pet').click();\n\n    // Verify that the pet folder contains path-based subfolders like '{petId}'\n    await expect(page.locator('.collection-item-name').getByText('{petId}')).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/import/url-import/postman-url-import.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections, openCollection } from '../../utils/page';\n\ntest.describe('Postman URL Import', () => {\n  test.afterEach(async ({ page }) => {\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  test('Postman URL import', async ({ page, createTmpDir }) => {\n    const postmanUrl = 'https://raw.githubusercontent.com/usebruno/bruno/refs/heads/main/tests/import/postman/fixtures/postman-v21.json';\n\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // Wait for import collection modal to be ready\n    const importModal = page.getByTestId('import-collection-modal');\n    await importModal.waitFor({ state: 'visible' });\n\n    await page.getByTestId('url-tab').click();\n    await page.getByTestId('url-input').waitFor({ state: 'visible' });\n    await page.getByTestId('url-input').fill(postmanUrl);\n    await page.locator('#import-url-button').click();\n\n    // Wait for the loader to disappear\n    await page.locator('#import-collection-loader').waitFor({ state: 'hidden' });\n\n    // Verify that the collection location modal appears\n    const locationModal = page.getByTestId('import-collection-location-modal');\n    await expect(locationModal.getByText('Postman v2.1 Collection')).toBeVisible();\n\n    // Select a location and import\n    await page.locator('#collection-location').fill(await createTmpDir('postman-v21-collection'));\n    await locationModal.getByRole('button', { name: 'Import' }).click();\n    await locationModal.waitFor({ state: 'hidden' });\n\n    // Verify the collection was imported successfully and configure it\n    await expect(page.locator('#sidebar-collection-name').getByText('Postman v2.1 Collection')).toBeVisible();\n    await openCollection(page, 'Postman v2.1 Collection');\n\n    // Verify these folder names are present\n    await expect(page.locator('.collection-item-name').getByText('Get Users')).toBeVisible();\n    await expect(page.locator('.collection-item-name').getByText('Create User')).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/import/wsdl/fixtures/wsdl-bruno.json",
    "content": "{\n  \"uid\": \"TestServiceCollection\",\n  \"version\": \"1\",\n  \"name\": \"TestWSDLServiceJSON\",\n  \"items\": [\n    {\n      \"uid\": \"UserServiceFolder\",\n      \"name\": \"UserService\",\n      \"type\": \"folder\",\n      \"items\": [\n        {\n          \"uid\": \"GetUserRequest\",\n          \"name\": \"GetUser\",\n          \"type\": \"http-request\",\n          \"seq\": 1,\n          \"request\": {\n            \"url\": \"http://example.com/soap/userservice\",\n            \"method\": \"POST\",\n            \"auth\": {\n              \"mode\": \"none\",\n              \"basic\": null,\n              \"bearer\": null,\n              \"digest\": null\n            },\n            \"headers\": [\n              {\n                \"uid\": \"ContentTypeHeader\",\n                \"name\": \"Content-Type\",\n                \"value\": \"text/xml; charset=utf-8\",\n                \"description\": \"\",\n                \"enabled\": true\n              },\n              {\n                \"uid\": \"SOAPActionHeader\",\n                \"name\": \"SOAPAction\",\n                \"value\": \"http://example.com/testservice/GetUser\",\n                \"description\": \"\",\n                \"enabled\": true\n              }\n            ],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"xml\",\n              \"json\": null,\n              \"text\": null,\n              \"xml\": \"<soap:Envelope xmlns:soap=\\\"http://schemas.xmlsoap.org/soap/envelope/\\\"><soap:Body><GetUserRequest xmlns=\\\"http://example.com/testservice\\\"><userId>string</userId><includeDetails>true</includeDetails></GetUserRequest></soap:Body></soap:Envelope>\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": []\n            },\n            \"script\": {\n              \"res\": null\n            }\n          }\n        },\n        {\n          \"uid\": \"CreateUserRequest\",\n          \"name\": \"CreateUser\",\n          \"type\": \"http-request\",\n          \"seq\": 2,\n          \"request\": {\n            \"url\": \"http://example.com/soap/userservice\",\n            \"method\": \"POST\",\n            \"auth\": {\n              \"mode\": \"none\",\n              \"basic\": null,\n              \"bearer\": null,\n              \"digest\": null\n            },\n            \"headers\": [\n              {\n                \"uid\": \"ContentTypeHeader2\",\n                \"name\": \"Content-Type\",\n                \"value\": \"text/xml; charset=utf-8\",\n                \"description\": \"\",\n                \"enabled\": true\n              },\n              {\n                \"uid\": \"SOAPActionHeader2\",\n                \"name\": \"SOAPAction\",\n                \"value\": \"http://example.com/testservice/CreateUser\",\n                \"description\": \"\",\n                \"enabled\": true\n              }\n            ],\n            \"params\": [],\n            \"body\": {\n              \"mode\": \"xml\",\n              \"json\": null,\n              \"text\": null,\n              \"xml\": \"<soap:Envelope xmlns:soap=\\\"http://schemas.xmlsoap.org/soap/envelope/\\\"><soap:Body><CreateUserRequest xmlns=\\\"http://example.com/testservice\\\"><name>string</name><email>string</email><password>string</password></CreateUserRequest></soap:Body></soap:Envelope>\",\n              \"formUrlEncoded\": [],\n              \"multipartForm\": []\n            },\n            \"script\": {\n              \"res\": null\n            }\n          }\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/import/wsdl/fixtures/wsdl.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<wsdl:definitions \n  name=\"TestWSDLServiceXML\" \n  targetNamespace=\"http://example.com/testservice\" \n  xmlns:wsdl=\"http://schemas.xmlsoap.org/wsdl/\"\n  xmlns:soap=\"http://schemas.xmlsoap.org/wsdl/soap/\"\n  xmlns:tns=\"http://example.com/testservice\"\n  xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n\n  <wsdl:documentation>Test WSDL for Bruno import testing</wsdl:documentation>\n\n  <wsdl:types>\n    <xsd:schema targetNamespace=\"http://example.com/testservice\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">\n      <xsd:element name=\"GetUserRequest\">\n        <xsd:complexType>\n          <xsd:sequence>\n            <xsd:element name=\"userId\" type=\"xsd:string\"/>\n            <xsd:element name=\"includeDetails\" type=\"xsd:boolean\" minOccurs=\"0\"/>\n          </xsd:sequence>\n        </xsd:complexType>\n      </xsd:element>\n      \n      <xsd:element name=\"GetUserResponse\">\n        <xsd:complexType>\n          <xsd:sequence>\n            <xsd:element name=\"user\" type=\"tns:User\"/>\n            <xsd:element name=\"status\" type=\"xsd:string\"/>\n          </xsd:sequence>\n        </xsd:complexType>\n      </xsd:element>\n      \n      <xsd:complexType name=\"User\">\n        <xsd:sequence>\n          <xsd:element name=\"id\" type=\"xsd:string\"/>\n          <xsd:element name=\"name\" type=\"xsd:string\"/>\n          <xsd:element name=\"email\" type=\"xsd:string\"/>\n          <xsd:element name=\"active\" type=\"xsd:boolean\"/>\n        </xsd:sequence>\n      </xsd:complexType>\n      \n      <xsd:element name=\"CreateUserRequest\">\n        <xsd:complexType>\n          <xsd:sequence>\n            <xsd:element name=\"name\" type=\"xsd:string\"/>\n            <xsd:element name=\"email\" type=\"xsd:string\"/>\n            <xsd:element name=\"password\" type=\"xsd:string\"/>\n          </xsd:sequence>\n        </xsd:complexType>\n      </xsd:element>\n      \n      <xsd:element name=\"CreateUserResponse\">\n        <xsd:complexType>\n          <xsd:sequence>\n            <xsd:element name=\"userId\" type=\"xsd:string\"/>\n            <xsd:element name=\"status\" type=\"xsd:string\"/>\n            <xsd:element name=\"message\" type=\"xsd:string\"/>\n          </xsd:sequence>\n        </xsd:complexType>\n      </xsd:element>\n    </xsd:schema>\n  </wsdl:types>\n\n  <wsdl:message name=\"GetUserRequestMessage\">\n    <wsdl:part name=\"parameters\" element=\"tns:GetUserRequest\"/>\n  </wsdl:message>\n  \n  <wsdl:message name=\"GetUserResponseMessage\">\n    <wsdl:part name=\"parameters\" element=\"tns:GetUserResponse\"/>\n  </wsdl:message>\n  \n  <wsdl:message name=\"CreateUserRequestMessage\">\n    <wsdl:part name=\"parameters\" element=\"tns:CreateUserRequest\"/>\n  </wsdl:message>\n  \n  <wsdl:message name=\"CreateUserResponseMessage\">\n    <wsdl:part name=\"parameters\" element=\"tns:CreateUserResponse\"/>\n  </wsdl:message>\n\n  <wsdl:portType name=\"UserServicePortType\">\n    <wsdl:documentation>User management service port type</wsdl:documentation>\n    \n    <wsdl:operation name=\"GetUser\">\n      <wsdl:documentation>Retrieve user information by ID</wsdl:documentation>\n      <wsdl:input message=\"tns:GetUserRequestMessage\"/>\n      <wsdl:output message=\"tns:GetUserResponseMessage\"/>\n    </wsdl:operation>\n    \n    <wsdl:operation name=\"CreateUser\">\n      <wsdl:documentation>Create a new user</wsdl:documentation>\n      <wsdl:input message=\"tns:CreateUserRequestMessage\"/>\n      <wsdl:output message=\"tns:CreateUserResponseMessage\"/>\n    </wsdl:operation>\n  </wsdl:portType>\n\n  <wsdl:binding name=\"UserServiceBinding\" type=\"tns:UserServicePortType\">\n    <soap:binding style=\"document\" transport=\"http://schemas.xmlsoap.org/soap/http\"/>\n    \n    <wsdl:operation name=\"GetUser\">\n      <soap:operation soapAction=\"http://example.com/testservice/GetUser\"/>\n      <wsdl:input>\n        <soap:body use=\"literal\"/>\n      </wsdl:input>\n      <wsdl:output>\n        <soap:body use=\"literal\"/>\n      </wsdl:output>\n    </wsdl:operation>\n    \n    <wsdl:operation name=\"CreateUser\">\n      <soap:operation soapAction=\"http://example.com/testservice/CreateUser\"/>\n      <wsdl:input>\n        <soap:body use=\"literal\"/>\n      </wsdl:input>\n      <wsdl:output>\n        <soap:body use=\"literal\"/>\n      </wsdl:output>\n    </wsdl:operation>\n  </wsdl:binding>\n\n  <wsdl:service name=\"UserService\">\n    <wsdl:documentation>User management web service</wsdl:documentation>\n    <wsdl:port name=\"UserServicePort\" binding=\"tns:UserServiceBinding\">\n      <soap:address location=\"http://example.com/soap/userservice\"/>\n    </wsdl:port>\n  </wsdl:service>\n\n</wsdl:definitions>\n"
  },
  {
    "path": "tests/import/wsdl/import-wsdl.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport * as path from 'path';\nimport { closeAllCollections, openCollection } from '../../utils/page/actions';\n\ntest.describe('Import WSDL Collection', () => {\n  const testDataDir = path.join(__dirname, 'fixtures');\n\n  test.afterEach(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('Import WSDL XML file as Bruno collection', async ({ page, createTmpDir }) => {\n    const wsdlFile = path.join(testDataDir, 'wsdl.xml');\n\n    await test.step('Open import collection modal', async () => {\n      await page.getByTestId('collections-header-add-menu').click();\n      await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n      // Wait for import collection modal to be ready\n      const importModal = page.getByRole('dialog');\n      await importModal.waitFor({ state: 'visible' });\n    });\n\n    await test.step('Choose WSDL XML file', async () => {\n      await page.setInputFiles('input[type=\"file\"]', wsdlFile);\n\n      // Wait for location modal to appear after file processing\n      const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n      await locationModal.waitFor({ state: 'visible', timeout: 10000 });\n    });\n\n    await test.step('Select the location for the collection and submit to import', async () => {\n      // Verify that the location selection modal is displayed to import the collection\n      const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n      await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n\n      // select a location\n      await page.locator('#collection-location').fill(await createTmpDir('wsdl-xml-test'));\n      await locationModal.getByRole('button', { name: 'Import' }).click();\n      await locationModal.waitFor({ state: 'hidden' });\n      await expect(page.locator('#sidebar-collection-name').getByText('TestWSDLServiceXML')).toBeVisible();\n    });\n\n    await test.step('Verify that the collection was imported successfully', async () => {\n      // verify the collection was imported successfully\n      await expect(page.locator('#sidebar-collection-name').getByText('TestWSDLServiceXML')).toBeVisible();\n\n      // open the collection and accept the sandbox modal\n      await openCollection(page, 'TestWSDLServiceXML');\n\n      // verify that all requests were imported correctly\n      await expect(page.locator('#collection-testwsdlservicexml .collection-item-name')).toHaveCount(1);\n    });\n\n    await test.step('Verify that folders and requests were imported correctly', async () => {\n      await expect(page.locator('#collection-testwsdlservicexml .collection-item-name').getByText('UserService')).toBeVisible();\n      // open the user service folder\n      await page.locator('#collection-testwsdlservicexml .collection-item-name').getByText('UserService').click();\n\n      await expect(page.locator('#collection-testwsdlservicexml .collection-item-name').getByText('GetUser')).toBeVisible();\n      await expect(page.locator('#collection-testwsdlservicexml .collection-item-name').getByText('CreateUser')).toBeVisible();\n    });\n\n    await test.step('Verify the GetUser request is imported correctly', async () => {\n      await page.locator('#collection-testwsdlservicexml .collection-item-name').getByText('GetUser').click();\n      await expect(page.locator('.request-tab.active').getByText('GetUser')).toBeVisible();\n      await expect(page.locator('#request-url').getByText('http://example.com/soap/userservice')).toBeVisible();\n    });\n  });\n\n  test('Import WSDL JSON file as Bruno collection', async ({ page, createTmpDir }) => {\n    const wsdlFile = path.join(testDataDir, 'wsdl-bruno.json');\n\n    await test.step('Open import collection modal', async () => {\n      await page.getByTestId('collections-header-add-menu').click();\n      await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n      // Wait for import collection modal to be ready\n      const importModal = page.getByRole('dialog');\n      await importModal.waitFor({ state: 'visible' });\n    });\n\n    await test.step('Choose WSDL JSON file', async () => {\n      await page.setInputFiles('input[type=\"file\"]', wsdlFile);\n\n      // Wait for location modal to appear after file processing\n      const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n      await locationModal.waitFor({ state: 'visible', timeout: 10000 });\n    });\n\n    await test.step('Select the location for the collection and submit to import', async () => {\n      // Verify that the location selection modal is displayed to import the collection\n      const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n      await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n\n      // Wait for collection to appear in the location modal\n      await expect(locationModal.getByText('TestWSDLServiceJSON')).toBeVisible();\n\n      // select a location\n      await page.locator('#collection-location').fill(await createTmpDir('wsdl-json-test'));\n      await locationModal.getByRole('button', { name: 'Import' }).click();\n      await locationModal.waitFor({ state: 'hidden' });\n    });\n\n    await test.step('Verify that the collection was imported successfully', async () => {\n      // verify the collection was imported successfully\n      await expect(page.locator('#sidebar-collection-name').getByText('TestWSDLServiceJSON')).toBeVisible();\n\n      // open the collection and accept the sandbox modal\n      await openCollection(page, 'TestWSDLServiceJSON');\n\n      // verify that all requests were imported correctly\n      await expect(page.locator('#collection-testwsdlservicejson .collection-item-name')).toHaveCount(1);\n    });\n\n    await test.step('Verify that folders and requests were imported correctly', async () => {\n      await expect(page.locator('#collection-testwsdlservicejson .collection-item-name').getByText('UserService')).toBeVisible();\n      // open the user service folder\n      await page.locator('#collection-testwsdlservicejson .collection-item-name').getByText('UserService').click();\n\n      await expect(page.locator('#collection-testwsdlservicejson .collection-item-name').getByText('GetUser')).toBeVisible();\n      await expect(page.locator('#collection-testwsdlservicejson .collection-item-name').getByText('CreateUser')).toBeVisible();\n    });\n\n    await test.step('Verify the CreateUser request is imported correctly', async () => {\n      await page.locator('#collection-testwsdlservicejson .collection-item-name').getByText('CreateUser').click();\n      await expect(page.locator('.request-tab.active').getByText('CreateUser')).toBeVisible();\n      await expect(page.locator('#request-url').getByText('http://example.com/soap/userservice')).toBeVisible();\n    });\n  });\n});\n"
  },
  {
    "path": "tests/interpolation/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"interpolation\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}"
  },
  {
    "path": "tests/interpolation/collection/echo-request-odata.bru",
    "content": "meta {\n  name: echo-request-odata\n  type: http\n  seq: 2\n}\n\nget {\n  url: http://localhost:8081/api/echo/path/Category(':CategoryID')/Item(:ItemId)/:xpath/Tags(\"tag test\")\n  body: none\n  auth: inherit\n}\n\nparams:path {\n  CategoryID: category123\n  ItemId: item456\n  xpath: foobar\n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "tests/interpolation/collection/echo-request-url.bru",
    "content": "meta {\n  name: echo-request-url\n  type: http\n  seq: 1\n}\n\nget {\n  url: http://localhost:8081/api/echo/path/:path\n  auth: inherit\n}\n\nparams:path {\n  path: some-data\n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "tests/interpolation/dynamic-variable/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"dynamic-variable-interpolation\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}\n"
  },
  {
    "path": "tests/interpolation/dynamic-variable/collection/set-var-dynamic-variable.bru",
    "content": "meta {\n  name: set-var-dynamic-variable\n  type: http\n  seq: 1\n}\n\npost {\n  url:  https://echo.usebruno.com\n  body: json\n  auth: none\n}\n\nheaders {\n  Content-Type: application/json\n}\n\nscript:pre-request {\n  bru.setVar(\"title\", \"{{$randomFirstName}}\");\n}\n\nbody:json {\n  {\n    \"title\": \"{{title}}\"\n  }\n}\n"
  },
  {
    "path": "tests/interpolation/dynamic-variable/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/interpolation/dynamic-variable/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/interpolation/dynamic-variable/set-var-dynamic-variable.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections, openCollection, sendRequest } from '../../utils/page';\nimport { buildCommonLocators } from '../../utils/page/locators';\n\ntest.describe.serial('Dynamic Variable Interpolation', () => {\n  test.afterEach(async ({ pageWithUserData: page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('Verifying if the bru.setVar method interpolates random generator functions properly', async ({ pageWithUserData: page }) => {\n    const locators = buildCommonLocators(page);\n\n    // Open collection and accept sandbox mode\n    await openCollection(page, 'dynamic-variable-interpolation');\n\n    // Navigate to the request\n    await locators.sidebar.request('set-var-dynamic-variable').click();\n\n    // Send the request\n    await sendRequest(page, 200);\n\n    // Verify response contains the title field and that it's not the literal interpolation string\n    const responsePane = page.locator('.response-pane');\n\n    // Check that the response contains a title field\n    await expect(responsePane).toContainText('\"title\":');\n\n    // Get the response body text to extract the actual title value\n    const responseBodyText = await responsePane.innerText();\n\n    // Extract the title value from the JSON response\n    const titleMatch = responseBodyText.match(/\"title\":\\s*\"([^\"]+)\"/) ?? [];\n    expect(titleMatch).toBeTruthy();\n\n    const actualTitle = titleMatch[1];\n\n    // Verify that the title is not the literal interpolation string\n    // This ensures that the randomFirstName function was properly interpolated\n    expect(actualTitle).not.toEqual('{{$randomFirstName}}');\n\n    // Additional verification: ensure the title is a string and not empty\n    expect(actualTitle).toBeDefined();\n    expect(typeof actualTitle).toBe('string');\n    expect(actualTitle.length).toBeGreaterThan(0);\n  });\n});\n"
  },
  {
    "path": "tests/interpolation/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{projectRoot}}/tests/interpolation/collection\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "tests/interpolation/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/interpolation/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/interpolation/interpolate-request-url.spec.ts",
    "content": "import { test, expect } from '../../playwright';\nimport { closeAllCollections, sendRequest } from '../utils/page';\nimport { buildCommonLocators } from '../utils/page/locators';\n\ntest.describe.serial('URL Interpolation', () => {\n  test.afterAll(async ({ pageWithUserData: page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('Interpolate basic path params', async ({ pageWithUserData: page }) => {\n    const locators = buildCommonLocators(page);\n    await locators.sidebar.collection('interpolation').click();\n    await locators.sidebar.request('echo-request-url').click();\n    await sendRequest(page, 200);\n\n    const texts = await page.getByTestId('response-preview-container').locator('.CodeMirror-scroll').allInnerTexts();\n    await expect(texts.some((d) => d.includes(`\"url\": \"/path/some-data\"`))).toBe(true);\n  });\n\n  test('Interpolate oData path params', async ({ pageWithUserData: page }) => {\n    const locators = buildCommonLocators(page);\n    await locators.sidebar.request('echo-request-odata').click();\n    await sendRequest(page, 200);\n\n    const texts = await page.getByTestId('response-preview-container').locator('.CodeMirror-scroll').allInnerTexts();\n    await expect(texts.some((d) => d.includes(`\"url\": \"/path/Category('category123')/Item(item456)/foobar/Tags(%22tag%20test%22)\"`))).toBe(true);\n  });\n});\n"
  },
  {
    "path": "tests/interpolation/prompt-variables/fixtures/client.pfx",
    "content": ""
  },
  {
    "path": "tests/interpolation/prompt-variables/fixtures/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"prompt-variables-interpolation\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ],\n  \"size\": 0.0008153915405273438,\n  \"filesCount\": 4,\n  \"clientCertificates\": {\n    \"enabled\": true,\n    \"certs\": [\n      {\n        \"domain\": \"localhost:8081\",\n        \"type\": \"pfx\",\n        \"pfxFilePath\": \"../client.pfx\",\n        \"passphrase\": \"{{?Enter Client CA Password}}\"\n      }\n    ]\n  }\n}"
  },
  {
    "path": "tests/interpolation/prompt-variables/fixtures/collection/collection.bru",
    "content": "auth {\n  mode: basic\n}\n\nauth:basic {\n  username: auth_unsername\n  password: {{?Enter Collection Auth Password}}\n}\n\nvars:pre-request {\n  collectionVar: {{?Enter Collection Variable}}\n  ~collectionVarDisabled: {{?Should Not Prompt collectionVarDisabled}}\n}\n"
  },
  {
    "path": "tests/interpolation/prompt-variables/fixtures/collection/environments/local.bru",
    "content": "vars {\n  collectionEnvVar: {{?Enter Collection Env Variable}}\n  ~collectionEnvVarDisabled: {{?Should Not Prompt collectionEnvVarDisabled}}\n}\n"
  },
  {
    "path": "tests/interpolation/prompt-variables/fixtures/collection/http-folder/folder.bru",
    "content": "meta {\n  name: http-folder\n}\n\nheaders {\n  folderHeaderVar: {{?Enter Folder Header Variable}}\n  ~folderHeaderVarDisabled: {{?Should Not Prompt folderHeaderVarDisabled}}\n}\n\nauth {\n  mode: basic\n}\n\nauth:basic {\n  username: auth_username\n  password: {{?Enter Folder Auth Password}}\n}\n\nvars:pre-request {\n  folderVar: {{?Enter Folder Variable}}\n  ~folderVarDisabled: {{?Should Not Prompt folderVarDisabled}}\n}\n"
  },
  {
    "path": "tests/interpolation/prompt-variables/fixtures/collection/http-folder/http-request-without-ca.bru",
    "content": "meta {\n  name: http-request-without-ca\n  type: http\n  seq: 2\n}\n\npost {\n  url: http://localhost:{{?Enter Port Variable}}/api/echo/json?query={{?Enter Query Variable}}\n  body: json\n  auth: inherit\n}\n\nparams:query {\n  query: {{?Enter Query Variable}}\n}\n\nheaders {\n  Content-Type: application/json\n  ~x-disabled-header: {{?Should Not Prompt request x-disabled-header}}\n}\n\nbody:json {\n  {\n    \"body\": \"{{?Enter Body Variable}}\",\n    \"bodyNumber\": {{?Enter Number Variable}},\n    \"bodyBoolean\": {{?Enter Boolean Variable}},\n    \"repeat-1\": \"{{?Enter Body Variable}}\",\n    \"requestVar\": \"{{requestVar}}\",\n    \"folderVar\": \"{{folderVar}}\",\n    \"collectionVar\": \"{{collectionVar}}\",\n    \"collectionEnvVar\": \"{{collectionEnvVar}}\",\n    \"globalEnvVar\": \"{{globalEnvVar}}\",\n    \"folderHeader\": \"{{folderHeader}}\"\n  }\n}\n\nbody:form-urlencoded {\n  formurlencoded: {{?Should Not Prompt body mode form-urlencoded}}\n}\n\nvars:pre-request {\n  requestVar: {{?Enter Request Variable}}\n}\n"
  },
  {
    "path": "tests/interpolation/prompt-variables/fixtures/collection/http-folder/https-request-with-ca.bru",
    "content": "meta {\n  name: https-request-with-ca\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://localhost:8081/api/echo/json\n  body: json\n  auth: inherit\n}\n\nbody:json {\n  {\n    \"body\": \"test\"\n  }\n}\n"
  },
  {
    "path": "tests/interpolation/prompt-variables/http-request-prompt-variables.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections } from '../../utils/page';\n\ntest.describe('Prompt Variables Interpolation', () => {\n  test.afterAll(async ({ pageWithUserData: page }) => {\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  // without client certificate - no HTTPS\n  test('Verifying if the prompt variables are prompted correctly for the http request - without client certificate', async ({ pageWithUserData: page }) => {\n    let promptVariablesModal;\n    let promptInputs;\n\n    await test.step('Open collection and navigate to the http request with prompt variables', async () => {\n      // Open collection and accept sandbox mode\n      await page.locator('#sidebar-collection-name').filter({ hasText: 'prompt-variables-interpolation' }).click();\n\n      // Navigate to the request\n      await page.locator('.collection-item-name').filter({ hasText: 'http-folder' }).click();\n      await page.locator('.collection-item-name').filter({ hasText: 'http-request-without-ca' }).click();\n    });\n\n    await test.step('Send the request and verify the prompt variables modal is visible', async () => {\n      // Send the request\n      await page.getByTestId('send-arrow-icon').click();\n\n      promptVariablesModal = page.getByRole('dialog').filter({ has: page.locator('.bruno-modal-header-title').getByText('Input Required') });\n      await promptVariablesModal.waitFor({ state: 'visible' });\n    });\n\n    await test.step('Verify duplicate prompt variables are not allowed', async () => {\n      // Enter the prompt variables\n      promptInputs = promptVariablesModal.getByTestId('prompt-variable-input-container');\n      await expect(promptInputs).toHaveCount(12);\n    });\n\n    await test.step('Verify disabled / non selected modes prompt variables are not prompted', async () => {\n      // verify that any prompt added to the inactive fields starting with label \"Should Not Prompt\" are not displayed\n      // eg: 1. Headers - disabled or hierarchical overrides should not be displayed\n      // 2. Vars - respects hierarchical overrides, eg: request var > folder var > collection var > global env var\n      // 3. Body - only prompts from selected body mode should be displayed eg: json\n      // 4. Auth - only prompts from selected mode should be displayed eg: basic, respects hierarchical overrides\n      // 5. Client Cert - only prompts from current domain config should be displayed\n      await expect(promptInputs.filter({ hasText: 'Should Not Prompt', exact: true })).toHaveCount(0);\n    });\n\n    await test.step('Fill the prompt variables and send the request', async () => {\n      await promptInputs.filter({ hasText: 'Enter Port Variable' }).locator('input').fill('8081');\n      await promptInputs.filter({ hasText: 'Enter Query Variable' }).locator('input').fill('queryPromptValue');\n      await promptInputs.filter({ hasText: 'Enter Body Variable' }).locator('input').fill('bodyPromptValue');\n      await promptInputs.filter({ hasText: 'Enter Number Variable' }).locator('input').fill('123');\n      await promptInputs.filter({ hasText: 'Enter Boolean Variable' }).locator('input').fill('true');\n      await promptInputs.filter({ hasText: 'Enter Request Variable' }).locator('input').fill('requestVarPromptValue');\n      await promptInputs.filter({ hasText: 'Enter Folder Variable' }).locator('input').fill('folderVarPromptValue');\n      await promptInputs.filter({ hasText: 'Enter Collection Variable' }).locator('input').fill('collectionVarPromptValue');\n      await promptInputs.filter({ hasText: 'Enter Collection Env Variable' }).locator('input').fill('collectionEnvVarPromptValue');\n      await promptInputs.filter({ hasText: 'Enter Global Env Variable' }).locator('input').fill('globalEnvVarPromptValue');\n      await promptInputs.filter({ hasText: 'Enter Folder Auth Password' }).locator('input').fill('folderAuthPasswordValue');\n      await promptInputs.filter({ hasText: 'Enter Folder Header Variable' }).locator('input').fill('folderHeaderVarPromptValue');\n\n      // Submit the prompt variables\n      await promptVariablesModal.getByRole('button', { name: 'Continue' }).click();\n    });\n\n    await test.step('Verify the request is sent with the correct variables', async () => {\n      // Verify the response status code\n      await expect(page.getByTestId('response-status-code')).toHaveText(/200/);\n      await expect(page.locator('.response-pane').locator('.CodeMirror-line').getByText('\"folderVar\": \"folderVarPromptValue\"').first()).toBeVisible();\n      await expect(page.locator('.response-pane').locator('.CodeMirror-line').getByText('\"collectionVar\": \"collectionVarPromptValue\"').first()).toBeVisible();\n      await expect(page.locator('.response-pane').locator('.CodeMirror-line').getByText('\"collectionEnvVar\": \"collectionEnvVarPromptValue\"').first()).toBeVisible();\n      await expect(page.locator('.response-pane').locator('.CodeMirror-line').getByText('\"globalEnvVar\": \"globalEnvVarPromptValue\"').first()).toBeVisible();\n      await expect(page.locator('.response-pane').locator('.CodeMirror-line').getByText('\"requestVar\": \"requestVarPromptValue\"').first()).toBeVisible();\n      await expect(page.locator('.response-pane').locator('.CodeMirror-line').getByText('\"body\": \"bodyPromptValue\"').first()).toBeVisible();\n      await expect(page.locator('.response-pane').locator('.CodeMirror-line').getByText('\"repeat-1\": \"bodyPromptValue\"').first()).toBeVisible();\n      await expect(page.locator('.response-pane').locator('.CodeMirror-line').getByText('\"bodyNumber\": 123').first()).toBeVisible();\n      await expect(page.locator('.response-pane').locator('.CodeMirror-line').getByText('\"bodyBoolean\": true').first()).toBeVisible();\n    });\n  });\n\n  // with client certificate - HTTPS\n  test('Verifying if the prompt variables are prompted correctly for the http request - with client certificate', async ({ pageWithUserData: page }) => {\n    let promptVariablesModal;\n    let promptInputs;\n\n    await test.step('Open collection and navigate to the http request with prompt variables', async () => {\n      // Open collection and accept sandbox mode\n      await page.locator('#sidebar-collection-name').filter({ hasText: 'prompt-variables-interpolation' }).click();\n\n      // Navigate to the request\n      await page.locator('.collection-item-name').filter({ hasText: 'http-folder' }).click();\n      await page.locator('.collection-item-name').filter({ hasText: 'https-request-with-ca' }).click();\n    });\n\n    await test.step('Send the request and verify the prompt variables modal is visible', async () => {\n      // Send the request\n      await page.getByTestId('send-arrow-icon').click();\n\n      promptVariablesModal = page.getByRole('dialog').filter({ has: page.locator('.bruno-modal-header-title').getByText('Input Required') });\n      await promptVariablesModal.waitFor({ state: 'visible' });\n    });\n\n    await test.step('Verify disabled / non selected modes prompt variables are not prompted', async () => {\n      promptInputs = promptVariablesModal.getByTestId('prompt-variable-input-container');\n      // verify that any prompt added to the inactive fields starting with label \"Should Not Prompt\" are not displayed\n      // eg: 1. Headers - disabled or hierarchical overrides should not be displayed\n      // 2. Vars - respects hierarchical overrides, eg: request var > folder var > collection var > global env var\n      // 3. Body - only prompts from selected body mode should be displayed eg: json\n      // 4. Auth - only prompts from selected mode should be displayed eg: basic, respects hierarchical overrides\n      // 5. Client Cert - only prompts from current domain config should be displayed\n      await expect(promptInputs.filter({ hasText: 'Should Not Prompt', exact: true })).toHaveCount(0);\n      await expect(promptInputs.filter({ hasText: 'Enter Client CA Password', exact: true })).toHaveCount(1);\n    });\n\n    await test.step('Fill the prompt variables and send the request', async () => {\n      await promptInputs.filter({ hasText: 'Enter Client CA Password' }).locator('input').fill('clientCAPasswordValue');\n      // leave the rest of the prompt variables empty\n\n      // Submit the prompt variables\n      await promptVariablesModal.getByRole('button', { name: 'Continue' }).click();\n    });\n\n    // @TODO: setup a valid certificate and server required to verify the request is sent with the correct variables\n  });\n});\n"
  },
  {
    "path": "tests/interpolation/prompt-variables/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{projectRoot}}/tests/interpolation/prompt-variables/fixtures/collection\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "tests/interpolation/prompt-variables/init-user-data/global-environments.json",
    "content": "{\n  \"environments\": [\n    {\n      \"uid\": \"FlaexlO7lcH7UtEpWsVyz\",\n      \"name\": \"E2E_Global\",\n      \"variables\": [\n        {\n          \"uid\": \"lflBDSYBdHkUedYhBF4Ty\",\n          \"name\": \"globalEnvVar\",\n          \"value\": \"{{?Enter Global Env Variable}}\",\n          \"type\": \"text\",\n          \"secret\": false,\n          \"enabled\": true\n        },\n        {\n          \"uid\": \"lflBDSYBdHkUedYhBF4Ty\",\n          \"name\": \"globalEnvVarDisabled\",\n          \"value\": \"{{?Should Not Prompt globalEnvVarDisabled}}\",\n          \"type\": \"text\",\n          \"secret\": false,\n          \"enabled\": false\n        }\n      ]\n    }\n  ],\n  \"activeGlobalEnvironmentUid\": \"FlaexlO7lcH7UtEpWsVyz\"\n}"
  },
  {
    "path": "tests/interpolation/prompt-variables/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/interpolation/prompt-variables/fixtures/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/interpolation/prompt-variables/init-user-data/ui-state-snapshot.json",
    "content": "{\n  \"collections\": [\n    {\n      \"pathname\": \"{{projectRoot}}/tests/interpolation/prompt-variables/fixtures/collection\",\n      \"selectedEnvironment\": \"local\"\n    }\n  ]\n}"
  },
  {
    "path": "tests/onboarding/init-user-data/preferences.json",
    "content": "{\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/packages/bruno-tests/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/onboarding/init-user-data-fresh/preferences.json",
    "content": "{\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": false,\n      \"hasSeenWelcomeModal\": false\n    }\n  }\n}\n"
  },
  {
    "path": "tests/onboarding/sample-collection.spec.ts",
    "content": "import path from 'path';\nimport { test, expect, errors, closeElectronApp } from '../../playwright';\n\nconst initUserDataPath = path.join(__dirname, 'init-user-data-fresh');\n\nconst env = {\n  DISABLE_SAMPLE_COLLECTION_IMPORT: 'false'\n};\n\n// Helper to dismiss welcome modal if visible\nasync function dismissWelcomeModalIfVisible(page: any) {\n  const welcomeModal = page.getByTestId('welcome-modal');\n  const isVisible = await welcomeModal.isVisible().catch(() => false);\n  if (isVisible) {\n    await page.getByRole('button', { name: 'Skip' }).click();\n    await expect(welcomeModal).not.toBeVisible();\n  }\n}\n\ntest.describe('Onboarding', () => {\n  test('should create sample collection on first launch', async ({ launchElectronApp }) => {\n    const app = await launchElectronApp({ initUserDataPath, dotEnv: env });\n    const page = await app.firstWindow();\n\n    // Wait for app to load and dismiss welcome modal\n    await page.locator('[data-app-state=\"loaded\"]').waitFor();\n    await dismissWelcomeModalIfVisible(page);\n\n    // Verify sample collection appears in sidebar\n    const sampleCollection = page.locator('#sidebar-collection-name').getByText('Sample API Collection');\n    await expect(sampleCollection).toBeVisible();\n\n    // Click on the sample collection to open it\n    await sampleCollection.click();\n\n    // Verify the sample request is visible and clickable\n    const request = page.locator('.collection-item-name').getByText('Get Users');\n    await expect(request).toBeVisible();\n    await request.click();\n\n    // Verify the URL is set correctly\n    await expect(page.locator('#request-url')).toContainText('https://jsonplaceholder.typicode.com/users');\n\n    // Clean up\n    await closeElectronApp(app);\n  });\n\n  test('should not create duplicate collections on subsequent launches', async ({ launchElectronApp, createTmpDir }) => {\n    // Use a fresh app instance to avoid contamination from previous tests\n    const userDataPath = await createTmpDir('duplicate-collections');\n    const app = await launchElectronApp({ userDataPath, initUserDataPath, dotEnv: env });\n    const page = await app.firstWindow();\n\n    // Wait for app to load and dismiss welcome modal\n    await page.locator('[data-app-state=\"loaded\"]').waitFor();\n    await dismissWelcomeModalIfVisible(page);\n\n    // First launch - verify sample collection is created\n    const sampleCollection = page.locator('#sidebar-collection-name').getByText('Sample API Collection');\n    await expect(sampleCollection).toBeVisible();\n    await sampleCollection.click();\n\n    // Verify the sample request\n    const request = page.locator('.collection-item-name').getByText('Get Users');\n    await expect(request).toBeVisible();\n    await request.click();\n\n    // Verify the URL is set correctly\n    await expect(page.locator('#request-url')).toContainText('https://jsonplaceholder.typicode.com/users');\n\n    // Close the first app instance\n    await closeElectronApp(app);\n\n    // Restart app - should not create sample collection again\n    const newApp = await launchElectronApp({ userDataPath, dotEnv: env });\n    const newPage = await newApp.firstWindow();\n\n    // Verify only one sample collection exists\n    const sampleCollections = newPage.locator('#sidebar-collection-name').getByText('Sample API Collection');\n    await expect(sampleCollections).toHaveCount(1);\n\n    // Verify the collection still works after restart\n    await sampleCollections.click();\n    const request2 = newPage.locator('.collection-item-name').getByText('Get Users');\n    await expect(request2).toBeVisible();\n    await request2.click();\n\n    // Verify the URL is still correct after restart\n    await expect(newPage.locator('#request-url')).toContainText('https://jsonplaceholder.typicode.com/users');\n\n    // Clean up\n    await closeElectronApp(newApp);\n  });\n\n  test('should not recreate sample collection after user deletes it', async ({ launchElectronApp, reuseOrLaunchElectronApp, createTmpDir }) => {\n    const userDataPath = await createTmpDir('first-launch');\n    const app = await launchElectronApp({ userDataPath, initUserDataPath, dotEnv: env });\n    const page = await app.firstWindow();\n\n    // Wait for app to load and dismiss welcome modal\n    await page.locator('[data-app-state=\"loaded\"]').waitFor();\n    await dismissWelcomeModalIfVisible(page);\n\n    // First launch - sample collection should be created\n    const sampleCollection = page.getByTestId('collections').locator('.collection-name').filter({ hasText: 'Sample API Collection' });\n    await expect(sampleCollection).toBeVisible();\n\n    // User removes the sample collection from workspace (hover on the collection and open context menu)\n    await sampleCollection.hover();\n    await sampleCollection.locator('.collection-actions .icon').click();\n\n    // Remove the sample collection\n    const removeOption = page.locator('.dropdown-item').getByText('Remove');\n    await expect(removeOption).toBeVisible();\n    await removeOption.click();\n\n    // Wait for modal to appear - could be either regular remove or drafts confirmation\n    const removeModal = page.locator('.bruno-modal').filter({ hasText: 'Remove Collection' });\n    await removeModal.waitFor({ state: 'visible', timeout: 5000 });\n\n    // Check if it's the drafts confirmation modal (has \"Discard All and Remove\" button)\n    const hasDiscardButton = await page.getByRole('button', { name: 'Discard All and Remove' }).isVisible().catch(() => false);\n\n    if (hasDiscardButton) {\n      // Drafts modal - click \"Discard All and Remove\"\n      await page.getByRole('button', { name: 'Discard All and Remove' }).click();\n    } else {\n      // Regular modal - click the submit button\n      await page.locator('.bruno-modal-footer .submit').click();\n    }\n\n    // Verify collection is closed (no longer visible in sidebar)\n    await expect(sampleCollection).not.toBeVisible();\n\n    // Restart app - sample collection should NOT be recreated\n    const newApp = await reuseOrLaunchElectronApp({ userDataPath, dotEnv: env });\n    const newPage = await newApp.firstWindow();\n\n    // Wait for the app to be loaded / onboarding to be completed\n    await newPage.locator('[data-app-state=\"loaded\"]').waitFor();\n\n    // Sample collection should not appear since it's no longer first launch\n    const sampleCollections = newPage.locator('#sidebar-collection-name').getByText('Sample API Collection');\n    await expect(sampleCollections).not.toBeVisible();\n  });\n\n  test('should not create sample collection if user has already opened a collection', async ({ pageWithUserData: page }) => {\n    // Wait for the app to be loaded / onboarding to be completed\n    await page.locator('[data-app-state=\"loaded\"]').waitFor();\n\n    // This test simulates old users who already have a collection opened\n    const brunoTestbench = page.locator('#sidebar-collection-name').getByText('bruno-testbench');\n    await expect(brunoTestbench).toBeVisible();\n\n    // Verify no sample collection was created since user already has collections\n    const sampleCollection = page.locator('#sidebar-collection-name').getByText('Sample API Collection');\n    await expect(sampleCollection).not.toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/onboarding/welcome-modal.spec.ts",
    "content": "import path from 'path';\nimport { ElectronApplication } from '@playwright/test';\nimport { test, expect, closeElectronApp } from '../../playwright';\n\nconst initUserDataPath = path.join(__dirname, 'init-user-data-fresh');\n\ntest.describe('Welcome Modal', () => {\n  test('should show welcome modal for new users on first launch', async ({ launchElectronApp }) => {\n    let app: ElectronApplication | undefined;\n\n    try {\n      app = await launchElectronApp({ initUserDataPath });\n      const page = await app.firstWindow();\n\n      // Wait for the app to fully initialize before interacting\n      await page.locator('[data-app-state=\"loaded\"]').waitFor();\n\n      // Welcome modal should be visible for new users\n      const welcomeModal = page.getByTestId('welcome-modal');\n      await expect(welcomeModal).toBeVisible();\n\n      // Verify welcome content is displayed\n      await expect(welcomeModal.getByText('Welcome to Bruno')).toBeVisible();\n      await expect(welcomeModal.getByText('A fast, Git-friendly, and open-source API client.')).toBeVisible();\n    } finally {\n      if (app) {\n        await closeElectronApp(app);\n      }\n    }\n  });\n\n  test('should not show welcome modal for existing users', async ({ pageWithUserData: page }) => {\n    // pageWithUserData uses init-user-data/preferences.json which has hasSeenWelcomeModal: true\n    // Welcome modal should NOT be visible for existing users\n    const welcomeModal = page.getByTestId('welcome-modal');\n    await expect(welcomeModal).not.toBeVisible();\n  });\n\n  test('should dismiss welcome modal and not show again on restart', async ({ launchElectronApp, createTmpDir }) => {\n    const userDataPath = await createTmpDir('welcome-modal-dismiss');\n    let app: ElectronApplication | undefined;\n\n    try {\n      // Launch app for a new user - welcome modal should appear\n      app = await launchElectronApp({ userDataPath, initUserDataPath });\n      let page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor();\n\n      // Welcome modal should be visible for new users\n      const welcomeModal = page.getByTestId('welcome-modal');\n      await expect(welcomeModal).toBeVisible();\n\n      // Dismiss the modal by clicking Skip\n      await page.getByRole('button', { name: 'Skip' }).click();\n      await expect(welcomeModal).not.toBeVisible();\n\n      // Close the app\n      await closeElectronApp(app);\n      app = undefined;\n\n      // Restart the app with the same userDataPath\n      app = await launchElectronApp({ userDataPath });\n      page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor();\n\n      // Welcome modal should NOT appear after restart (hasSeenWelcomeModal persisted)\n      await expect(page.getByTestId('welcome-modal')).not.toBeVisible();\n    } finally {\n      if (app) {\n        await closeElectronApp(app);\n      }\n    }\n  });\n\n  test('should navigate through welcome modal steps', async ({ launchElectronApp }) => {\n    let app: ElectronApplication | undefined;\n\n    try {\n      app = await launchElectronApp({ initUserDataPath });\n      const page = await app.firstWindow();\n\n      // Wait for the app to fully initialize before interacting\n      await page.locator('[data-app-state=\"loaded\"]').waitFor();\n\n      const welcomeModal = page.getByTestId('welcome-modal');\n\n      // Step 1: Welcome\n      await expect(welcomeModal.getByText('Welcome to Bruno')).toBeVisible();\n      await welcomeModal.getByRole('button', { name: 'Get Started' }).click();\n\n      // Step 2: Theme selection\n      await expect(welcomeModal.getByText('Choose your theme')).toBeVisible();\n      await welcomeModal.getByRole('button', { name: 'Next' }).click();\n\n      // Step 3: Collection location\n      await expect(welcomeModal.getByText('Where should we store your collections?')).toBeVisible();\n      await welcomeModal.getByRole('button', { name: 'Next' }).click();\n\n      // Step 4: Actions\n      await expect(welcomeModal.getByText('Ready to go!')).toBeVisible();\n    } finally {\n      if (app) {\n        await closeElectronApp(app);\n      }\n    }\n  });\n\n  test('should open create collection modal from welcome modal', async ({ launchElectronApp }) => {\n    let app: ElectronApplication | undefined;\n\n    try {\n      app = await launchElectronApp({ initUserDataPath });\n      const page = await app.firstWindow();\n\n      // Wait for the app to fully initialize before interacting\n      await page.locator('[data-app-state=\"loaded\"]').waitFor();\n\n      const welcomeModal = page.getByTestId('welcome-modal');\n\n      // Navigate to last step\n      await welcomeModal.getByRole('button', { name: 'Get Started' }).click();\n      await welcomeModal.getByRole('button', { name: 'Next' }).click();\n      await welcomeModal.getByRole('button', { name: 'Next' }).click();\n\n      // Click Create Collection\n      await welcomeModal.locator('.primary-action-card').filter({ hasText: 'Create Collection' }).click();\n\n      // Welcome modal should be dismissed\n      await expect(welcomeModal).not.toBeVisible();\n\n      // Create Collection modal should appear\n      await expect(page.locator('.bruno-modal').filter({ hasText: 'Create Collection' })).toBeVisible();\n    } finally {\n      if (app) {\n        await closeElectronApp(app);\n      }\n    }\n  });\n});\n"
  },
  {
    "path": "tests/preferences/autosave/autosave.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { createCollection, closeAllCollections, createRequest } from '../../utils/page';\n\ntest.describe('Autosave', () => {\n  test.setTimeout(60000);\n\n  test.afterEach(async ({ page }) => {\n    // Only try to cleanup if page is still open\n    if (!page.isClosed()) {\n      await closeAllCollections(page);\n    }\n  });\n\n  test('should automatically save request changes when autosave is enabled', async ({ page, createTmpDir }) => {\n    const collectionName = 'autosave-test';\n\n    await test.step('Create collection and request', async () => {\n      await createCollection(page, collectionName, await createTmpDir('autosave-collection'));\n      await expect(page.locator('#sidebar-collection-name').filter({ hasText: collectionName })).toBeVisible();\n\n      await createRequest(page, 'Test Request', collectionName);\n      await page.locator('.collection-item-name').filter({ hasText: 'Test Request' }).click();\n\n      // Set initial URL and save\n      const urlEditor = page.locator('#request-url .CodeMirror');\n      await urlEditor.click();\n      await page.keyboard.type('https://api.example.com');\n      await page.keyboard.press('Control+s');\n\n      // Verify no draft indicator\n      const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Test Request' }) });\n      await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible();\n    });\n\n    await test.step('Enable autosave in preferences', async () => {\n      // Open preferences tab\n      await page.locator('.status-bar button[data-trigger=\"preferences\"]').click();\n\n      // Wait for preferences tab to be visible\n      await page.waitForTimeout(500);\n\n      // Navigate to General tab (should be default, but ensure it)\n      await page.getByRole('tab', { name: 'General' }).click();\n\n      // Enable autosave checkbox\n      const autoSaveCheckbox = page.locator('#autoSaveEnabled');\n      await autoSaveCheckbox.check();\n\n      // Wait for auto-save to complete (debounce is 500ms)\n      await page.waitForTimeout(1000);\n\n      // Close preferences tab using the close icon\n      const preferencesTab = page.locator('.request-tab').filter({ hasText: 'Preferences' });\n      await preferencesTab.hover();\n      await preferencesTab.locator('.close-icon').click({ force: true });\n\n      // Click on the request to make it active again\n      await page.locator('.collection-item-name').filter({ hasText: 'Test Request' }).click();\n    });\n\n    await test.step('Make changes and verify autosave', async () => {\n      // Make a change to the URL\n      const urlEditor = page.locator('#request-url .CodeMirror');\n      await urlEditor.click();\n      await page.keyboard.press('End');\n      await page.keyboard.type('/users');\n\n      // Wait for draft indicator to appear (change registered)\n      const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Test Request' }) });\n      await expect(requestTab.locator('.has-changes-icon')).toBeVisible();\n\n      // Wait for autosave to complete (draft indicator disappears)\n      await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible({ timeout: 5000 });\n    });\n\n    await test.step('Verify changes persisted', async () => {\n      // Close and reopen the request tab to verify persistence\n      const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Test Request' }) });\n      await requestTab.hover();\n      await requestTab.getByTestId('request-tab-close-icon').click({ force: true });\n\n      // Reopen request\n      await page.locator('.collection-item-name').filter({ hasText: 'Test Request' }).click();\n\n      // Verify URL contains our changes\n      const urlEditor = page.locator('#request-url .CodeMirror');\n      const urlContent = await urlEditor.locator('.CodeMirror-line').first().textContent();\n      expect(urlContent).toContain('api.example.com/users');\n    });\n\n    await test.step('Disable autosave in preferences', async () => {\n      // Open preferences tab\n      await page.locator('.status-bar button[data-trigger=\"preferences\"]').click();\n\n      // Wait for preferences tab to be visible\n      await page.waitForTimeout(500);\n\n      // Navigate to General tab\n      await page.getByRole('tab', { name: 'General' }).click();\n\n      // Disable autosave checkbox\n      const autoSaveCheckbox = page.locator('#autoSaveEnabled');\n      await autoSaveCheckbox.uncheck();\n\n      // Wait for auto-save to complete (debounce is 500ms)\n      await page.waitForTimeout(1000);\n\n      // Close preferences tab using the close icon\n      const preferencesTab = page.locator('.request-tab').filter({ hasText: 'Preferences' });\n      await preferencesTab.hover();\n      await preferencesTab.locator('.close-icon').click({ force: true });\n\n      // Click on the request to make it active again\n      await page.locator('.collection-item-name').filter({ hasText: 'Test Request' }).click();\n    });\n\n    await test.step('Make changes and verify no autosave when disabled', async () => {\n      // Make a change to the URL\n      const urlEditor = page.locator('#request-url .CodeMirror');\n      await urlEditor.click();\n      await page.keyboard.press('End');\n      await page.keyboard.type('/posts');\n\n      // Move mouse away from tab to ensure draft icon is visible (hover shows close icon)\n      await page.mouse.move(0, 0);\n\n      // Verify draft indicator appears\n      const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Test Request' }) });\n      await expect(requestTab.locator('.has-changes-icon')).toBeVisible();\n\n      await expect(requestTab.locator('.has-changes-icon')).toBeVisible({ timeout: 2000 });\n\n      // Save the request\n      await page.keyboard.press('Control+s');\n      await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible();\n    });\n  });\n\n  test('should autosave existing drafts when autosave is enabled', async ({ page, createTmpDir }) => {\n    const collectionName = 'autosave-existing-drafts-test';\n\n    await test.step('Create collection and request with initial URL', async () => {\n      await createCollection(page, collectionName, await createTmpDir('autosave-existing-drafts-collection'));\n      await expect(page.locator('#sidebar-collection-name').filter({ hasText: collectionName })).toBeVisible();\n\n      await createRequest(page, 'Draft Request', collectionName);\n      await page.locator('.collection-item-name').filter({ hasText: 'Draft Request' }).click();\n\n      // Set initial URL and save\n      const urlEditor = page.locator('#request-url .CodeMirror');\n      await urlEditor.click();\n      await page.keyboard.type('https://api.example.com');\n      await page.keyboard.press('Control+s');\n\n      // Verify no draft indicator\n      const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Draft Request' }) });\n      await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible();\n    });\n\n    await test.step('Make changes to create a draft (without saving)', async () => {\n      // Make a change to the URL\n      const urlEditor = page.locator('#request-url .CodeMirror');\n      await urlEditor.click();\n      await page.keyboard.press('End');\n      await page.keyboard.type('/existing-draft');\n\n      // Move mouse away from tab to ensure draft icon is visible (hover shows close icon)\n      await page.mouse.move(0, 0);\n\n      // Verify draft indicator appears\n      const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Draft Request' }) });\n      await expect(requestTab.locator('.has-changes-icon')).toBeVisible();\n    });\n\n    await test.step('Enable autosave and verify existing draft is saved', async () => {\n      // Open preferences tab\n      await page.locator('.status-bar button[data-trigger=\"preferences\"]').click();\n\n      // Wait for preferences tab to be visible\n      await page.waitForTimeout(500);\n\n      // Navigate to General tab\n      await page.getByRole('tab', { name: 'General' }).click();\n\n      // Enable autosave checkbox\n      const autoSaveCheckbox = page.locator('#autoSaveEnabled');\n      await autoSaveCheckbox.check();\n\n      // Wait for auto-save to complete (debounce is 500ms)\n      await page.waitForTimeout(1000);\n\n      // Close preferences tab using the close icon\n      const preferencesTab = page.locator('.request-tab').filter({ hasText: 'Preferences' });\n      await preferencesTab.hover();\n      await preferencesTab.locator('.close-icon').click({ force: true });\n\n      // Click on the request to make it active again\n      await page.locator('.collection-item-name').filter({ hasText: 'Draft Request' }).click();\n\n      await page.waitForTimeout(1000);\n\n      const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Draft Request' }) });\n      await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible({ timeout: 10000 });\n    });\n\n    await test.step('Verify changes persisted', async () => {\n      // Close and reopen the request tab to verify persistence\n      const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Draft Request' }) });\n      await requestTab.hover();\n      await requestTab.getByTestId('request-tab-close-icon').click({ force: true });\n\n      // Reopen request\n      await page.locator('.collection-item-name').filter({ hasText: 'Draft Request' }).click();\n\n      // Verify URL contains our changes\n      const urlEditor = page.locator('#request-url .CodeMirror');\n      const urlContent = await urlEditor.locator('.CodeMirror-line').first().textContent();\n      expect(urlContent).toContain('api.example.com/existing-draft');\n    });\n  });\n});\n"
  },
  {
    "path": "tests/preferences/default-collection-location/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"collection\",\n  \"type\": \"collection\"\n}\n"
  },
  {
    "path": "tests/preferences/default-collection-location/collection/collection.bru",
    "content": "meta {\n  name: collection\n  type: collection\n  version: 1.0.0\n}"
  },
  {
    "path": "tests/preferences/default-collection-location/collection/environments/Test.bru",
    "content": "vars {\n  host: https://www.httpfaker.org\n}\n"
  },
  {
    "path": "tests/preferences/default-collection-location/collection/request.bru",
    "content": "meta {\n  name: request\n  type: http\n  seq: 1\n}\n\npost {\n  url: {{host}}/api/echo\n  body: text\n  auth: none\n}"
  },
  {
    "path": "tests/preferences/default-collection-location/default-collection-location.spec.js",
    "content": "import { test, expect } from '../../../playwright';\n\nconst EXPECTED_PATH_SUFFIX = 'tests/preferences/default-collection-location';\n\ntest.describe('Default Location Feature', () => {\n  test('Should hydrate the default location from preferences', async ({ pageWithUserData: page }) => {\n    // open preferences tab\n    await page.locator('.preferences-button').click();\n\n    // wait for preferences tab to be visible\n    await page.waitForTimeout(500);\n\n    // navigate to General tab\n    await page.getByRole('tab', { name: 'General' }).click();\n\n    // verify the default location is pre-filled with the expected path suffix\n    const defaultLocationInput = page.locator('.default-location-input');\n    const value = await defaultLocationInput.inputValue();\n    expect(value.endsWith(EXPECTED_PATH_SUFFIX)).toBe(true);\n  });\n\n  test('Should save a valid default location', async ({ pageWithUserData: page }) => {\n    // open preferences tab\n    await page.locator('.preferences-button').click();\n\n    // wait for preferences tab to be visible\n    await page.waitForTimeout(500);\n\n    // navigate to General tab\n    await page.getByRole('tab', { name: 'General' }).click();\n\n    // get the current default location and compute a different valid path\n    const defaultLocationInput = page.locator('.default-location-input');\n    const currentValue = await defaultLocationInput.inputValue();\n    // Use parent directory as alternate path (guaranteed to exist and differ)\n    const alternateExistingPath = currentValue.split('/').slice(0, -1).join('/');\n\n    // set a different default location (readonly input, remove readonly then fill)\n    await defaultLocationInput.evaluate((el) => {\n      const input = el;\n      input.removeAttribute('readonly');\n      input.readOnly = false;\n    });\n    await defaultLocationInput.fill(alternateExistingPath);\n\n    // wait for auto-save to complete (debounce is 500ms)\n    await page.waitForTimeout(1000);\n\n    // close preferences tab\n    await page.locator('.preferences-button').click();\n    await page.waitForTimeout(300);\n\n    // reopen preferences and verify persistence\n    await page.locator('.preferences-button').click();\n    await page.waitForTimeout(500);\n    await page.getByRole('tab', { name: 'General' }).click();\n\n    const savedValue = await page.locator('.default-location-input').inputValue();\n    expect(savedValue).toBe(alternateExistingPath);\n  });\n\n  test('Should use default location in Create Collection modal', async ({ pageWithUserData: page }) => {\n    // test Create Collection modal\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();\n\n    // Wait for inline creator to appear, then click the cog button to open advanced modal\n    const inlineCreator = page.locator('.inline-collection-creator');\n    await inlineCreator.waitFor({ state: 'visible', timeout: 5000 });\n    await inlineCreator.locator('.cog-btn').click();\n\n    // Wait for modal to be visible\n    await page.locator('.bruno-modal').waitFor({ state: 'visible' });\n\n    // verify the default location is pre-filled\n    // Scope to the modal to avoid conflict with preferences tab\n    const collectionLocationInput = page.locator('.bruno-modal').getByLabel('Location', { exact: true });\n    await expect(collectionLocationInput).toBeVisible();\n\n    const inputValue = await collectionLocationInput.inputValue();\n    expect(inputValue.endsWith(EXPECTED_PATH_SUFFIX)).toBe(true);\n\n    // cancel the collection creation\n    await page.locator('.bruno-modal').getByRole('button', { name: 'Cancel' }).click();\n  });\n\n  test('Should use default location in Clone Collection modal', async ({ pageWithUserData: page }) => {\n    // open the clone collection modal\n    const collection = page.locator('.collection-name').first();\n    await collection.hover();\n    await collection.locator('.collection-actions .icon').click();\n    await page.locator('.dropdown-item').filter({ hasText: 'Clone' }).click();\n\n    // Wait for modal to be visible\n    await page.locator('.bruno-modal').waitFor({ state: 'visible' });\n\n    // verify the default location is pre-filled\n    // Scope to the modal to avoid conflict with preferences tab\n    const cloneLocationInput = page.locator('.bruno-modal').getByLabel('Location', { exact: true });\n    await expect(cloneLocationInput).toBeVisible();\n    const cloneValue = await cloneLocationInput.inputValue();\n    expect(cloneValue.endsWith(EXPECTED_PATH_SUFFIX)).toBe(true);\n\n    // cancel the clone operation\n    await page.locator('.bruno-modal').getByRole('button', { name: 'Cancel' }).click();\n  });\n\n  test('Should save empty default location', async ({ pageWithUserData: page }) => {\n    // open preferences tab\n    await page.locator('.preferences-button').click();\n\n    // wait for preferences tab to be visible\n    await page.waitForTimeout(500);\n\n    // navigate to General tab\n    await page.getByRole('tab', { name: 'General' }).click();\n\n    // clear the default location field (readonly input, remove readonly then clear)\n    const defaultLocationInput = page.locator('.default-location-input');\n    await defaultLocationInput.evaluate((el) => {\n      const input = el;\n      input.removeAttribute('readonly');\n      input.readOnly = false;\n    });\n    await defaultLocationInput.clear();\n\n    // wait for auto-save to complete (debounce is 500ms)\n    await page.waitForTimeout(1000);\n  });\n});\n"
  },
  {
    "path": "tests/preferences/default-collection-location/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{projectRoot}}/tests/preferences/default-collection-location/collection\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"developer\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "tests/preferences/default-collection-location/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\"{{projectRoot}}/tests/preferences/default-collection-location/collection\"],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    },\n    \"general\": {\n      \"defaultLocation\": \"{{projectRoot}}/tests/preferences/default-collection-location\"\n    }\n  }\n}\n"
  },
  {
    "path": "tests/preferences/support-links.spec.js",
    "content": "import { test, expect } from '../../playwright';\n\ntest('Should verify all support links with correct URL in preference > Support tab', async ({ page }) => {\n  // Open Preferences\n  await page.getByLabel('Open Preferences').click();\n\n  // Go to Support tab\n  await page.getByRole('tab', { name: 'Support' }).click();\n\n  // Verify all support links with correct URL\n  const locator_twitter = page.getByRole('link', { name: 'Twitter' });\n  expect(await locator_twitter.getAttribute('href')).toEqual('https://twitter.com/use_bruno');\n\n  const locator_github = page.getByRole('link', { name: 'GitHub', exact: true });\n  expect(await locator_github.getAttribute('href')).toEqual('https://github.com/usebruno/bruno');\n\n  const locator_discord = page.getByRole('link', { name: 'Discord', exact: true });\n  expect(await locator_discord.getAttribute('href')).toEqual('https://discord.com/invite/KgcZUncpjq');\n\n  const locator_reportissues = page.getByRole('link', { name: 'Report Issues', exact: true });\n  expect(await locator_reportissues.getAttribute('href')).toEqual('https://github.com/usebruno/bruno/issues');\n\n  const locator_documentation = page.getByRole('link', { name: 'Documentation', exact: true });\n  expect(await locator_documentation.getAttribute('href')).toEqual('https://docs.usebruno.com');\n});\n"
  },
  {
    "path": "tests/preferences/tab-switch-persistence/tab-switch-persistence.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\n\ntest.describe('Preferences Tab Switch Persistence', () => {\n  test('should persist General tab SSL setting when immediately switching tabs', async ({ page }) => {\n    // Open preferences\n    await page.locator('.preferences-button').click();\n    await page.getByRole('tab', { name: 'General' }).waitFor({ state: 'visible' });\n\n    // Navigate to General tab\n    await page.getByRole('tab', { name: 'General' }).click();\n    await page.waitForTimeout(300);\n\n    // Get the initial state of SSL verification checkbox\n    const sslCheckbox = page.locator('#sslVerification');\n    await sslCheckbox.waitFor({ state: 'visible' });\n    const initialChecked = await sslCheckbox.isChecked();\n\n    // Toggle the SSL verification checkbox\n    await sslCheckbox.click();\n\n    // Immediately switch to another tab (don't wait for debounce)\n    await page.getByRole('tab', { name: 'Themes' }).click();\n    await page.waitForTimeout(100);\n\n    // Switch back to General tab\n    await page.getByRole('tab', { name: 'General' }).click();\n    await sslCheckbox.waitFor({ state: 'visible' });\n\n    // Verify the setting was persisted (should be opposite of initial state)\n    const newChecked = await sslCheckbox.isChecked();\n    expect(newChecked).toBe(!initialChecked);\n\n    // Restore original state\n    await sslCheckbox.click();\n    await page.waitForTimeout(600);\n  });\n\n  test('should persist Store Cookies setting when immediately switching tabs', async ({ page }) => {\n    // Open preferences\n    await page.locator('.preferences-button').click();\n    await page.getByRole('tab', { name: 'General' }).waitFor({ state: 'visible' });\n\n    // Navigate to General tab\n    await page.getByRole('tab', { name: 'General' }).click();\n    await page.waitForTimeout(300);\n\n    // Get the initial state of Store Cookies checkbox\n    const storeCookiesCheckbox = page.locator('#storeCookies');\n    await storeCookiesCheckbox.waitFor({ state: 'visible' });\n    const initialChecked = await storeCookiesCheckbox.isChecked();\n\n    // Toggle the checkbox\n    await storeCookiesCheckbox.click();\n\n    // Immediately switch to Themes tab (lighter than Proxy)\n    await page.getByRole('tab', { name: 'Themes' }).click();\n    await page.waitForTimeout(100);\n\n    // Switch back to General tab\n    await page.getByRole('tab', { name: 'General' }).click();\n    await storeCookiesCheckbox.waitFor({ state: 'visible' });\n\n    // Verify the setting was persisted\n    const newChecked = await storeCookiesCheckbox.isChecked();\n    expect(newChecked).toBe(!initialChecked);\n\n    // Restore original state\n    await storeCookiesCheckbox.click();\n    await page.waitForTimeout(600);\n  });\n\n  test('should persist Cache settings when immediately switching tabs', async ({ page }) => {\n    // Open preferences\n    await page.locator('.preferences-button').click();\n    await page.getByRole('tab', { name: 'Cache' }).waitFor({ state: 'visible' });\n\n    // Navigate to Cache tab\n    await page.getByRole('tab', { name: 'Cache' }).click();\n    await page.waitForTimeout(300);\n\n    // Get the initial state of SSL session caching checkbox\n    const sslSessionCheckbox = page.locator('#sslSession\\\\.enabled');\n    await sslSessionCheckbox.waitFor({ state: 'visible' });\n    const initialChecked = await sslSessionCheckbox.isChecked();\n\n    // Toggle the checkbox\n    await sslSessionCheckbox.click();\n\n    // Immediately switch to another tab\n    await page.getByRole('tab', { name: 'Themes' }).click();\n    await page.waitForTimeout(100);\n\n    // Switch back to Cache tab\n    await page.getByRole('tab', { name: 'Cache' }).click();\n    await sslSessionCheckbox.waitFor({ state: 'visible' });\n\n    // Verify the setting was persisted\n    const newChecked = await sslSessionCheckbox.isChecked();\n    expect(newChecked).toBe(!initialChecked);\n\n    // Restore original state\n    await sslSessionCheckbox.click();\n    await page.waitForTimeout(600);\n  });\n\n  test('should persist settings after closing and reopening preferences tab', async ({ page }) => {\n    // Open preferences\n    await page.locator('.preferences-button').click();\n    await page.getByRole('tab', { name: 'General' }).waitFor({ state: 'visible' });\n\n    // Navigate to General tab\n    await page.getByRole('tab', { name: 'General' }).click();\n    await page.waitForTimeout(300);\n\n    // Get the initial state of SSL verification checkbox\n    const sslCheckbox = page.locator('#sslVerification');\n    await sslCheckbox.waitFor({ state: 'visible' });\n    const initialChecked = await sslCheckbox.isChecked();\n\n    // Toggle the SSL verification checkbox\n    await sslCheckbox.click();\n\n    // Immediately close the preferences tab\n    const preferencesTab = page.locator('.request-tab').filter({ hasText: 'Preferences' });\n    await preferencesTab.hover();\n    await preferencesTab.locator('.close-icon').click({ force: true });\n\n    // Wait for preferences tab to close\n    await preferencesTab.waitFor({ state: 'hidden' });\n\n    // Reopen preferences\n    await page.locator('.preferences-button').click();\n    await page.getByRole('tab', { name: 'General' }).waitFor({ state: 'visible' });\n\n    // Navigate to General tab\n    await page.getByRole('tab', { name: 'General' }).click();\n    await sslCheckbox.waitFor({ state: 'visible' });\n\n    // Verify the setting was persisted\n    const newChecked = await sslCheckbox.isChecked();\n    expect(newChecked).toBe(!initialChecked);\n\n    // Restore original state\n    await sslCheckbox.click();\n    await page.waitForTimeout(600);\n  });\n\n  test('should persist Cache settings after closing and reopening preferences', async ({ page }) => {\n    // Open preferences\n    await page.locator('.preferences-button').click();\n    await page.getByRole('tab', { name: 'Cache' }).waitFor({ state: 'visible' });\n\n    // Navigate to Cache tab\n    await page.getByRole('tab', { name: 'Cache' }).click();\n    await page.waitForTimeout(300);\n\n    // Get the initial state of SSL session caching checkbox\n    const sslSessionCheckbox = page.locator('#sslSession\\\\.enabled');\n    await sslSessionCheckbox.waitFor({ state: 'visible' });\n    const initialCacheState = await sslSessionCheckbox.isChecked();\n\n    // Toggle the checkbox\n    await sslSessionCheckbox.click();\n\n    // Close preferences tab immediately\n    const preferencesTab = page.locator('.request-tab').filter({ hasText: 'Preferences' });\n    await preferencesTab.hover();\n    await preferencesTab.locator('.close-icon').click({ force: true });\n    await preferencesTab.waitFor({ state: 'hidden' });\n\n    // Wait for save to complete\n    await page.waitForTimeout(300);\n\n    // Reopen preferences\n    await page.locator('.preferences-button').click();\n    await page.getByRole('tab', { name: 'Cache' }).waitFor({ state: 'visible' });\n\n    // Navigate to Cache tab\n    await page.getByRole('tab', { name: 'Cache' }).click();\n    await page.waitForTimeout(300);\n    await sslSessionCheckbox.waitFor({ state: 'visible' });\n\n    // Verify the setting was persisted\n    expect(await sslSessionCheckbox.isChecked()).toBe(!initialCacheState);\n\n    // Restore original state\n    await sslSessionCheckbox.click();\n    await page.waitForTimeout(600);\n  });\n});\n"
  },
  {
    "path": "tests/protobuf/fixtures/collection/HelloService/folder.bru",
    "content": "meta {\n  name: HelloService\n  seq: 1\n}\n\nauth {\n  mode: inherit\n}\n"
  },
  {
    "path": "tests/protobuf/fixtures/collection/HelloService/sayHello.bru",
    "content": "meta {\n  name: sayHello\n  type: grpc\n  seq: 1\n}\n\ngrpc {\n  url: {{host}}\n  body: grpc\n  auth: inherit\n}\n\nbody:grpc {\n  name: message 1\n  content: '''\n    {}\n  '''\n}\n"
  },
  {
    "path": "tests/protobuf/fixtures/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"Grpcbin\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ],\n  \"size\": 0.001827239990234375,\n  \"filesCount\": 10,\n  \"protobuf\": {\n    \"protoFiles\": [\n      {\n        \"path\": \"./protos/services/invalid-file-path.proto\",\n        \"type\": \"file\"\n      },\n      {\n        \"path\": \"./protos/services/product.proto\",\n        \"type\": \"file\"\n      },\n      {\n        \"path\": \"./protos/services/order.proto\",\n        \"type\": \"file\"\n      }\n    ],\n    \"importPaths\": [\n      {\n        \"path\": \"./protos/invalid-import-path\",\n        \"enabled\": true\n      },\n      {\n        \"path\": \"./protos/types\",\n        \"enabled\": false\n      },\n      {\n        \"path\": \".\",\n        \"enabled\": true\n      }\n    ]\n  }\n}"
  },
  {
    "path": "tests/protobuf/fixtures/collection/collection.bru",
    "content": ""
  },
  {
    "path": "tests/protobuf/fixtures/collection/environments/GrpcEnv.bru",
    "content": "vars {\n  host: grpc://grpcb.in:9000\n}\n"
  },
  {
    "path": "tests/protobuf/fixtures/collection/protos/services/order.proto",
    "content": "syntax = \"proto3\";\n\npackage order;\n\nservice Order {\n  rpc CreateOrder (OrderRequest) returns (OrderResponse);\n  rpc GetOrder (OrderId) returns (OrderResponse);\n  rpc ListOrders (ListOrdersRequest) returns (ListOrdersResponse);\n  rpc UpdateOrder (OrderRequest) returns (OrderResponse);\n  rpc DeleteOrder (OrderId) returns (DeleteOrderResponse);\n  \n  // Stream of order status updates\n  rpc TrackOrderStatus (OrderId) returns (stream OrderStatusUpdate);\n}\n\nmessage OrderId {\n  string id = 1;\n}\n\nmessage OrderItem {\n  int32 product_id = 1;\n  int32 quantity = 2;\n  float unit_price = 3;\n}\n\nmessage OrderRequest {\n  string id = 1;\n  string user_id = 2;\n  repeated OrderItem items = 3;\n  string shipping_address = 4;\n  float total_amount = 5;\n  OrderStatus status = 6;\n}\n\nmessage OrderResponse {\n  string id = 1;\n  string user_id = 2;\n  repeated OrderItem items = 3;\n  string shipping_address = 4;\n  float total_amount = 5;\n  OrderStatus status = 6;\n  string created_at = 7;\n  string updated_at = 8;\n}\n\nenum OrderStatus {\n  PENDING = 0;\n  PROCESSING = 1;\n  SHIPPED = 2;\n  DELIVERED = 3;\n  CANCELLED = 4;\n}\n\nmessage ListOrdersRequest {\n  string user_id = 1;\n  int32 page = 2;\n  int32 limit = 3;\n}\n\nmessage ListOrdersResponse {\n  repeated OrderResponse orders = 1;\n  int32 total_count = 2;\n}\n\nmessage DeleteOrderResponse {\n  bool success = 1;\n  string message = 2;\n}\n\nmessage OrderStatusUpdate {\n  string order_id = 1;\n  OrderStatus status = 2;\n  string timestamp = 3;\n  string message = 4;\n} "
  },
  {
    "path": "tests/protobuf/fixtures/collection/protos/services/product.proto",
    "content": "syntax = \"proto3\";\n\npackage product;\n\nimport \"product-message.proto\";\n\nservice Product {\n  rpc CreateProduct (ProductItem) returns (ProductItem);\n  rpc ReadProduct (ProductId) returns (ProductItem);\n  rpc ReadProducts (VoidParam) returns (ProductItems);\n  rpc UpdateProduct(ProductItem) returns (ProductItem);\n  rpc DeleteProduct (ProductId) returns (DeleteProductResponse);\n  rpc CreateExampleProduct (VoidParam) returns (ProductItem);\n  \n  // Server Streaming: Stream product updates to client\n  rpc WatchProductUpdates (ProductId) returns (stream ProductUpdate);\n  \n  // Client Streaming: Batch create products\n  rpc BatchCreateProducts (stream ProductItem) returns (ProductBatchResponse);\n  \n  // Bidirectional Streaming: Real-time price monitoring\n  rpc MonitorProductPrices (stream PriceAlert) returns (stream PriceUpdate);\n}"
  },
  {
    "path": "tests/protobuf/fixtures/collection/protos/types/product-message.proto",
    "content": "syntax = \"proto3\";\n\npackage product;\n\nmessage VoidParam {}\n\nmessage ProductId {\n  int32 id = 1;\n}\n\nenum  Category {\n  SMARTPHONE = 0;\n  CAMERA = 1;\n  LAPTOPS = 2;\n  HEADPHONES = 3;\n  CHARGERS = 4;\n  SPEAKERS = 5;\n  TELEVISIONS = 6;\n  MODEMS = 7;\n  KEYBOARD = 8;\n  MICROPHONES = 9;\n}\n\nmessage ProductItem {\n  int32 id = 1;\n  string name = 2;\n  string description = 3;\n  float price = 4;\n  Category category = 5;\n}\n\nmessage ProductItems {\n  repeated ProductItem products = 1;\n}\n\nmessage DeleteProductResponse {\n  bool deleted = 1;\n}\n\nmessage ProductUpdate {\n  ProductItem product = 1;\n  enum UpdateType {\n    CREATED = 0;\n    MODIFIED = 1;\n    DELETED = 2;\n  }\n  UpdateType type = 2;\n  string timestamp = 3;\n}\n\nmessage ProductBatchResponse {\n  int32 success_count = 1;\n  repeated ProductItem failed_items = 2;\n  string message = 3;\n}\n\nmessage PriceAlert {\n  int32 product_id = 1;\n  float target_price = 2;\n  enum AlertType {\n    PRICE_ABOVE = 0;\n    PRICE_BELOW = 1;\n  }\n  AlertType type = 3;\n}\n\nmessage PriceUpdate {\n  int32 product_id = 1;\n  float current_price = 2;\n  bool alert_triggered = 3;\n  string timestamp = 4;\n}"
  },
  {
    "path": "tests/protobuf/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{collectionPath}}\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "tests/protobuf/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{collectionPath}}\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    },\n    \"beta\": {\n      \"grpc\": true,\n      \"nodevm\": false\n    }\n  }\n}\n"
  },
  {
    "path": "tests/protobuf/manage-protofile.spec.ts",
    "content": "import { test, expect } from '../../playwright';\nimport { closeAllCollections } from '../utils/page';\n\ntest.describe('manage protofile', () => {\n  test.afterAll(async ({ pageWithUserData: page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('protofiles, import paths from bruno.json are visible in the protobuf settings', async ({ pageWithUserData: page }) => {\n    await page.locator('#sidebar-collection-name').filter({ hasText: 'Grpcbin' }).click();\n\n    await page.getByRole('tab', { name: 'Protobuf' }).click();\n\n    // Wait for protobuf settings to load\n    const protobufProtoFilesSection = page.getByTestId('protobuf-proto-files-section');\n    await protobufProtoFilesSection.waitFor({ state: 'visible' });\n\n    // Check proto files table\n    const protoFilesTable = page.getByTestId('protobuf-proto-files-table');\n    await expect(protoFilesTable).toBeVisible();\n\n    // Wait for table data to load by checking for a known cell\n    const file = page.getByRole('cell', { name: 'product.proto', exact: true });\n    await expect(file).toBeVisible();\n\n    const filePath = page.getByRole('cell', { name: './protos/services/product.proto' });\n    await expect(filePath).toBeVisible();\n\n    // Check import paths table\n    const importPathsTable = page.getByTestId('protobuf-import-paths-table');\n    await expect(importPathsTable).toBeVisible();\n\n    // Wait for import paths table data to load\n    const importPath = page.getByRole('cell', { name: './protos/types', exact: true });\n    await expect(importPath).toBeVisible();\n\n    // Wait for invalid file path cell to appear\n    const invalidFilePath = page.getByRole('cell', { name: 'invalid-file-path.proto', exact: true });\n    await expect(invalidFilePath).toBeVisible();\n\n    const invalidImportPath = page.getByRole('cell', { name: './protos/invalid-import-path', exact: true });\n    await expect(invalidImportPath).toBeVisible();\n\n    const collectionPathAsImportPath = page.getByRole('cell', { name: '.', exact: true });\n    const collectionPathName = page.getByRole('cell', { name: /^pw-collection-/ });\n\n    // Invalid messages using test IDs\n    const invalidProtoFilesMessage = page.getByTestId('protobuf-invalid-files-message');\n    const invalidImportPathsMessage = page.getByTestId('protobuf-invalid-import-paths-message');\n\n    await expect(invalidProtoFilesMessage).toBeVisible();\n    await expect(invalidImportPathsMessage).toBeVisible();\n\n    // Wait for collection path cells to appear\n    await expect(collectionPathAsImportPath).toBeVisible();\n    await expect(collectionPathName).toBeVisible();\n\n    await page.getByRole('row', { name: 'invalid-file-path.proto' }).getByTestId('protobuf-remove-file-button').click();\n\n    await expect(page.getByRole('cell', { name: 'invalid-file-path.proto', exact: true })).not.toBeVisible();\n    await expect(invalidProtoFilesMessage).not.toBeVisible();\n\n    await page.getByRole('row', { name: './protos/invalid-import-path' }).getByTestId('protobuf-remove-import-path-button').click();\n\n    await expect(page.getByRole('cell', { name: './protos/invalid-import-path', exact: true })).not.toBeVisible();\n    await expect(invalidImportPathsMessage).not.toBeVisible();\n\n    // Save the changes to persist them to bruno.json\n    await page.getByRole('button', { name: 'Save' }).click();\n  });\n\n  test('order.proto loads methods successfully when selected', async ({ pageWithUserData: page }) => {\n    await page.locator('#sidebar-collection-name').filter({ hasText: 'Grpcbin' }).click();\n    await page.getByText('HelloService').click();\n    await page.getByText('SayHello').click();\n\n    // Wait for gRPC query URL container to load\n    const grpcQueryUrlContainer = page.getByTestId('grpc-query-url-container');\n    await grpcQueryUrlContainer.waitFor({ state: 'visible' });\n\n    await page.getByText('Using Reflection').click();\n    await page.getByText('Proto FileReflection').click();\n\n    // Use more specific selector for proto file selection\n    await page.locator('div').filter({ hasText: /^order\\.proto\\.\\/protos\\/services\\/order\\.proto$/ }).first().click();\n\n    // Use test ID for method selection\n    const grpcMethodsDropdown = page.getByTestId('grpc-methods-dropdown');\n    await grpcMethodsDropdown.click();\n    const method = page.getByTestId('grpc-method-item').filter({ hasText: /^CreateOrderunary$/ }).first();\n    await expect(method).toBeVisible();\n    await method.click();\n    const requestTab = page.getByRole('tab', { name: 'gRPC sayHello' });\n    await requestTab.hover();\n    await requestTab.getByTestId('request-tab-close-icon').click({ force: true });\n    await page.getByRole('button', { name: 'Don\\'t Save' }).click();\n  });\n\n  test('product.proto fails to load methods when selected', async ({ pageWithUserData: page }) => {\n    await page.locator('#sidebar-collection-name').filter({ hasText: 'Grpcbin' }).click();\n    await page.getByText('HelloService').click();\n    await page.getByText('SayHello').click();\n\n    // Wait for gRPC query URL container to load\n    const grpcQueryUrlContainer = page.getByTestId('grpc-query-url-container');\n    await grpcQueryUrlContainer.waitFor({ state: 'visible' });\n\n    await page.getByText('Using Reflection').click();\n    await page.getByText('Proto FileReflection').click();\n\n    // Use more specific selector for proto file selection\n    await page.locator('div').filter({ hasText: /^product\\.proto\\.\\/protos\\/services\\/product\\.proto$/ }).first().click();\n\n    // Verify the error message is visible (auto-retrying)\n    await expect(page.getByText('Failed to load gRPC methods: Unknown error').first()).toBeVisible();\n\n    // Check that methods dropdown is not visible when loading fails\n    const methodsDropdown = page.getByTestId('grpc-methods-dropdown');\n    await expect(methodsDropdown).not.toBeVisible();\n\n    const requestTab = page.getByRole('tab', { name: 'gRPC sayHello' });\n    await requestTab.hover();\n    await requestTab.getByTestId('request-tab-close-icon').click({ force: true });\n    await page.getByRole('button', { name: 'Don\\'t Save' }).click();\n  });\n\n  test('product.proto successfully loads methods once import path is provided', async ({ pageWithUserData: page }) => {\n    await page.locator('#sidebar-collection-name').filter({ hasText: 'Grpcbin' }).click();\n    // add import path within collection setting, protobuf tab\n    await page.getByRole('tab', { name: 'Protobuf' }).click();\n\n    // Wait for protobuf settings to load\n    const protobufImportPathsSection = page.getByTestId('protobuf-import-paths-section');\n    await protobufImportPathsSection.waitFor({ state: 'visible' });\n    const importPathTable = page.getByTestId('protobuf-import-paths-table');\n    await expect(importPathTable).toBeVisible();\n\n    // Use test ID for checkbox\n    const checkbox = page.getByRole('row', { name: 'Enable this import path types' }).getByTestId('protobuf-import-path-checkbox');\n    await checkbox.click();\n\n    // Save the changes to persist them to bruno.json\n    await page.getByRole('button', { name: 'Save' }).click();\n\n    // Now test that product.proto can load methods successfully\n    await page.getByText('HelloService').click();\n    await page.getByText('SayHello').click();\n\n    // Wait for gRPC query URL container to load\n    const grpcQueryUrlContainer = page.getByTestId('grpc-query-url-container');\n    await grpcQueryUrlContainer.waitFor({ state: 'visible' });\n\n    await page.getByText('Using Reflection').click();\n    await page.getByText('Proto FileReflection').click();\n\n    // Use more specific selector for proto file selection\n    await page.locator('div').filter({ hasText: /^product\\.proto\\.\\/protos\\/services\\/product\\.proto$/ }).first().click();\n    const grpcMethodsDropdown = page.getByTestId('grpc-methods-dropdown');\n    await grpcMethodsDropdown.click();\n    const method = page.getByTestId('grpc-methods-list').filter({ hasText: 'CreateProductunary' }).first();\n    await expect(method).toBeVisible();\n    await method.click();\n\n    // Clean up\n    const requestTab = page.getByRole('tab', { name: 'gRPC sayHello' });\n    await requestTab.hover();\n    await requestTab.getByTestId('request-tab-close-icon').click({ force: true });\n    await page.getByRole('button', { name: 'Don\\'t Save' }).click();\n  });\n});\n"
  },
  {
    "path": "tests/request/body-scroll/body-scroll-restoration.spec.ts",
    "content": "import { test, expect, Page } from '../../../playwright';\nimport {\n  closeAllCollections,\n  createCollection,\n  createRequest,\n  selectRequestPaneTab,\n  openRequest\n} from '../../utils/page';\n\n// Generate a large JSON body that requires scrolling\nconst generateLargeJsonBody = () => JSON.stringify(\n  {\n    users: Array.from({ length: 50 }, (_, i) => ({\n      id: i + 1,\n      name: `User ${i + 1}`,\n      email: `user${i + 1}@example.com`,\n      address: {\n        street: `${i + 1} Main Street`,\n        city: 'Test City',\n        zipCode: `${10000 + i}`\n      },\n      metadata: {\n        createdAt: '2024-01-01T00:00:00Z',\n        updatedAt: '2024-01-01T00:00:00Z',\n        tags: ['tag1', 'tag2', 'tag3']\n      }\n    }))\n  },\n  null,\n  2\n);\n\n// Helper to set body content using CodeMirror API\nconst setBodyContent = async (page: Page, content: string) => {\n  const bodyEditor = page.locator('.request-pane .CodeMirror').first();\n  await bodyEditor.evaluate((el, value) => {\n    const cm = (el as any).CodeMirror;\n    if (cm) {\n      cm.setValue(value);\n    }\n  }, content);\n};\n\n// Helper to get scroll position\nconst getScrollPosition = async (page: Page): Promise<number> => {\n  const bodyEditor = page.locator('.request-pane .CodeMirror').first();\n  return await bodyEditor.evaluate((el) => {\n    const cm = (el as any).CodeMirror;\n    if (cm && cm.doc) {\n      return cm.doc.scrollTop || 0;\n    }\n    const scrollElement = el.querySelector('.CodeMirror-scroll');\n    return scrollElement ? scrollElement.scrollTop : 0;\n  });\n};\n\n// Helper to set scroll position\nconst setScrollPosition = async (page: Page, scrollTop: number) => {\n  const bodyEditor = page.locator('.request-pane .CodeMirror').first();\n  await bodyEditor.evaluate((el, top) => {\n    const cm = (el as any).CodeMirror;\n    if (cm) {\n      cm.scrollTo(null, top);\n    }\n  }, scrollTop);\n};\n\n// Helper to select body mode\nconst selectBodyMode = async (page: Page, mode: string) => {\n  await page.locator('.body-mode-selector').click();\n  await page.locator('.dropdown-item').filter({ hasText: mode }).click();\n  await page.waitForTimeout(100);\n};\n\ntest.describe('Request Body Scroll Position Restoration', () => {\n  test.afterEach(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('should restore scroll position when switching tabs and back', async ({ page, createTmpDir }) => {\n    const collectionName = 'body-scroll-test';\n    const largeJsonBody = generateLargeJsonBody();\n\n    await test.step('Create collection and request with JSON body', async () => {\n      await createCollection(page, collectionName, await createTmpDir(collectionName));\n      await createRequest(page, 'scroll-test', collectionName, {\n        url: 'https://testbench-sanity.usebruno.com/api/echo/json'\n      });\n    });\n\n    await test.step('Navigate to Body tab and set JSON body', async () => {\n      await selectRequestPaneTab(page, 'Body');\n      await selectBodyMode(page, 'JSON');\n      await setBodyContent(page, largeJsonBody);\n    });\n\n    let initialScrollTop: number;\n\n    await test.step('Scroll down in the body editor', async () => {\n      await setScrollPosition(page, 500);\n      await page.waitForTimeout(200);\n      initialScrollTop = await getScrollPosition(page);\n      expect(initialScrollTop).toBeGreaterThan(0);\n    });\n\n    await test.step('Switch to Headers tab', async () => {\n      await selectRequestPaneTab(page, 'Headers');\n      await page.waitForTimeout(200);\n    });\n\n    await test.step('Switch back to Body tab and verify scroll position', async () => {\n      await selectRequestPaneTab(page, 'Body');\n      await page.waitForTimeout(300);\n\n      const restoredScrollTop = await getScrollPosition(page);\n\n      // The restored scroll position should be approximately the same\n      // Allow some tolerance for rendering differences\n      expect(restoredScrollTop).toBeGreaterThan(0);\n      expect(Math.abs(restoredScrollTop - initialScrollTop)).toBeLessThan(50);\n    });\n  });\n\n  test('should restore scroll position when switching between requests', async ({ page, createTmpDir }) => {\n    const collectionName = 'body-scroll-multi-request';\n    const largeJsonBody = generateLargeJsonBody();\n\n    await test.step('Create collection with two requests', async () => {\n      await createCollection(page, collectionName, await createTmpDir(collectionName));\n      await createRequest(page, 'request-1', collectionName, {\n        url: 'https://testbench-sanity.usebruno.com/api/echo/json'\n      });\n      await createRequest(page, 'request-2', collectionName, {\n        url: 'https://testbench-sanity.usebruno.com/ping'\n      });\n    });\n\n    let scrollPosition: number;\n\n    await test.step('Open first request, add body, and scroll', async () => {\n      await openRequest(page, collectionName, 'request-1');\n      await selectRequestPaneTab(page, 'Body');\n      await selectBodyMode(page, 'JSON');\n      await setBodyContent(page, largeJsonBody);\n\n      await setScrollPosition(page, 400);\n      await page.waitForTimeout(200);\n\n      scrollPosition = await getScrollPosition(page);\n      expect(scrollPosition).toBeGreaterThan(0);\n    });\n\n    await test.step('Switch to second request', async () => {\n      await openRequest(page, collectionName, 'request-2');\n      await page.waitForTimeout(200);\n    });\n\n    await test.step('Switch back to first request and verify scroll position', async () => {\n      await openRequest(page, collectionName, 'request-1');\n      await selectRequestPaneTab(page, 'Body');\n      await page.waitForTimeout(300);\n\n      const restoredScrollTop = await getScrollPosition(page);\n\n      // Verify scroll position is restored\n      expect(restoredScrollTop).toBeGreaterThan(0);\n      expect(Math.abs(restoredScrollTop - scrollPosition)).toBeLessThan(50);\n    });\n  });\n\n  test('should preserve scroll position for XML body mode', async ({ page, createTmpDir }) => {\n    const collectionName = 'body-scroll-xml';\n\n    // Generate large XML body\n    const largeXmlBody = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<root>\n${Array.from({ length: 50 }, (_, i) => `  <item id=\"${i + 1}\">\n    <name>Item ${i + 1}</name>\n    <description>This is a description for item ${i + 1}</description>\n    <metadata>\n      <created>2024-01-01T00:00:00Z</created>\n      <updated>2024-01-01T00:00:00Z</updated>\n    </metadata>\n  </item>`).join('\\n')}\n</root>`;\n\n    await test.step('Create collection and request', async () => {\n      await createCollection(page, collectionName, await createTmpDir(collectionName));\n      await createRequest(page, 'xml-scroll-test', collectionName, {\n        url: 'https://testbench-sanity.usebruno.com/api/echo/xml'\n      });\n    });\n\n    await test.step('Set XML body mode and add content', async () => {\n      await selectRequestPaneTab(page, 'Body');\n      await selectBodyMode(page, 'XML');\n      await setBodyContent(page, largeXmlBody);\n    });\n\n    let xmlScrollPosition: number;\n\n    await test.step('Scroll in XML body and verify restoration', async () => {\n      await setScrollPosition(page, 350);\n      await page.waitForTimeout(200);\n\n      xmlScrollPosition = await getScrollPosition(page);\n      expect(xmlScrollPosition).toBeGreaterThan(0);\n\n      // Switch tabs\n      await selectRequestPaneTab(page, 'Params');\n      await page.waitForTimeout(200);\n\n      // Switch back\n      await selectRequestPaneTab(page, 'Body');\n      await page.waitForTimeout(300);\n\n      const restoredScrollTop = await getScrollPosition(page);\n\n      expect(restoredScrollTop).toBeGreaterThan(0);\n      expect(Math.abs(restoredScrollTop - xmlScrollPosition)).toBeLessThan(50);\n    });\n  });\n});\n"
  },
  {
    "path": "tests/request/collections/custom-search/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"custom-search\",\n  \"type\": \"collection\",\n  \"ignore\": [\"node_modules\", \".git\"]\n}\n"
  },
  {
    "path": "tests/request/collections/custom-search/package.json",
    "content": "{\n  \"name\": \"custom-search\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A test collection for search functionality\",\n  \"main\": \"index.js\",\n  \"keywords\": [],\n  \"author\": \"\",\n  \"license\": \"ISC\"\n}\n"
  },
  {
    "path": "tests/request/collections/custom-search/search-request.bru",
    "content": "meta {\n  name: search-test-request\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://echo.usebruno.com\n  body: none\n  auth: inherit\n}\n\nscript:pre-request {\n  const testVariable = \"hello world\";\n  const anotherVariable = \"search test\";\n  console.log(\"This is a test log message\");\n  const searchableText = \"find me\";\n  \n  const apiKey = \"test-api-key-123\";\n  const baseUrl = \"https://api.example.com\";\n  console.log(\"Pre-request script executed\");\n  \n  const uniquePreVar = \"only in pre-request\";\n  const commonVar = \"common content\";\n  \n  const searchableText2 = \"find me again\";\n  const searchableText3 = \"find me third time\";\n  console.log(\"More searchableText instances\");\n}\n\nscript:post-response {\n  const responseData = \"response content\";\n  const searchableResponse = \"find this too\";\n  console.log(\"Response processed\");\n  \n  const statusCode = bru.getResponseStatus();\n  const responseTime = bru.getResponseTime();\n  console.log(\"Response status:\", statusCode);\n  \n  const uniquePostVar = \"only in post-response\";\n  const commonVar = \"common content\";\n  \n  const searchableResponse2 = \"find this too again\";\n  const searchableResponse3 = \"find this too third time\";\n  console.log(\"More searchableResponse instances\");\n}\n"
  },
  {
    "path": "tests/request/copy-request/copy-folder.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections, createCollection } from '../../utils/page';\n\ntest.describe('Copy and Paste Folders', () => {\n  test.afterAll(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('should copy and paste a folder within the same collection', async ({ page, createTmpDir }) => {\n    await createCollection(page, 'test-collection', await createTmpDir('test-collection'));\n    const collection = page.locator('.collection-name').filter({ hasText: 'test-collection' });\n\n    // Create a new folder with a request inside\n    await collection.hover();\n    await collection.locator('.collection-actions .icon').click();\n    await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click();\n    await page.locator('#folder-name').fill('folder-to-copy');\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    const folder = page.locator('.collection-item-name').filter({ hasText: 'folder-to-copy' });\n    await expect(folder).toBeVisible();\n\n    // Add a request to the folder\n    await folder.hover();\n    await folder.locator('.menu-icon').click({ force: true });\n    await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();\n    await page.getByPlaceholder('Request Name').fill('request-in-folder');\n    await page.locator('#new-request-url .CodeMirror').click();\n    await page.locator('textarea').fill('https://echo.usebruno.com/test');\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    await folder.click();\n    await expect(page.locator('.collection-item-name').filter({ hasText: 'request-in-folder' })).toBeVisible();\n\n    // Copy the folder\n    await folder.hover();\n    await folder.locator('.menu-icon').click({ force: true });\n    await page.locator('.dropdown-item').filter({ hasText: 'Copy' }).click();\n\n    // Paste into the collection root\n    await collection.hover();\n    await collection.locator('.collection-actions .icon').click();\n    await page.locator('.dropdown-item').filter({ hasText: 'Paste' }).click();\n\n    // Verify the pasted folder appears\n    await expect(page.locator('.collection-item-name').filter({ hasText: 'folder-to-copy' })).toHaveCount(2);\n  });\n\n  test('should copy and paste a folder into a different collection', async ({ page, createTmpDir }) => {\n    // Create second collection\n    await createCollection(page, 'test-collection-2', await createTmpDir('test-collection-2'));\n    const collection2 = page.locator('.collection-name').filter({ hasText: 'test-collection-2' });\n\n    // Paste the folder from clipboard into the new collection\n    await collection2.hover();\n    await collection2.locator('.collection-actions .icon').click();\n    await page.locator('.dropdown-item').filter({ hasText: 'Paste' }).click();\n\n    // Verify the pasted folder appears in the new collection\n    await expect(page.locator('.collection-item-name').filter({ hasText: 'folder-to-copy' })).toHaveCount(3);\n  });\n\n  test('should paste folder into another folder', async ({ page }) => {\n    const collection = page.locator('.collection-name').filter({ hasText: 'test-collection-2' });\n    const folderToCopy = page.locator('.collection-item-name').filter({ hasText: 'folder-to-copy' }).first();\n\n    // Create a target folder\n    await collection.hover();\n    await collection.locator('.collection-actions .icon').click();\n    await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click();\n    await page.locator('#folder-name').fill('target-folder');\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    const targetFolder = page.locator('.collection-item-name').filter({ hasText: 'target-folder' });\n    await expect(targetFolder).toBeVisible();\n    await targetFolder.click();\n\n    // Copy folder-to-copy\n    await folderToCopy.hover();\n    await folderToCopy.locator('.menu-icon').click({ force: true });\n    await page.locator('.dropdown-item').filter({ hasText: 'Copy' }).click();\n    await folderToCopy.click();\n\n    // Paste into target folder\n    await targetFolder.hover();\n    await targetFolder.locator('.menu-icon').click({ force: true });\n    await page.locator('.dropdown-item').filter({ hasText: 'Paste' }).click();\n\n    // Verify folder was pasted inside target folder\n    await expect(page.locator('.collection-item-name').filter({ hasText: 'folder-to-copy' })).toHaveCount(4);\n  });\n});\n"
  },
  {
    "path": "tests/request/copy-request/copy-request.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections, createCollection, createRequest } from '../../utils/page';\n\ntest.describe('Copy and Paste Requests', () => {\n  test.afterAll(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('should copy and paste a request within the same collection', async ({ page, createTmpDir }) => {\n    await createCollection(page, 'test-collection', await createTmpDir('test-collection'));\n\n    // Create a new request\n    const collection = page.locator('.collection-name').filter({ hasText: 'test-collection' });\n    await collection.hover();\n    await collection.locator('.collection-actions .icon').click();\n    await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();\n    await page.getByPlaceholder('Request Name').fill('original-request');\n    await page.locator('#new-request-url .CodeMirror').click();\n    await page.locator('textarea').fill('https://echo.usebruno.com');\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    await expect(page.locator('.collection-item-name').filter({ hasText: 'original-request' })).toBeVisible();\n\n    // Copy the request\n    const requestItem = page.locator('.collection-item-name').filter({ hasText: 'original-request' });\n    await requestItem.hover();\n    await requestItem.locator('.menu-icon').click({ force: true });\n    await page.locator('.dropdown-item').filter({ hasText: 'Copy' }).click();\n\n    // Paste into the collection root\n    await collection.hover();\n    await collection.locator('.collection-actions .icon').click();\n    await page.locator('.dropdown-item').filter({ hasText: 'Paste' }).click();\n\n    // Verify the pasted request appears with the same name\n    await expect(page.locator('.collection-item-name').filter({ hasText: 'original-request' })).toHaveCount(2);\n  });\n\n  test('should paste request into a folder', async ({ page, createTmpDir }) => {\n    const collection = page.locator('.collection-name').filter({ hasText: 'test-collection' });\n    await collection.hover();\n    await collection.locator('.collection-actions .icon').click();\n    await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click();\n    await page.locator('#folder-name').fill('test-folder');\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    // Paste into the folder\n    const folder = page.locator('.collection-item-name').filter({ hasText: 'test-folder' });\n    await folder.click();\n    await folder.hover();\n    await folder.locator('.menu-icon').click({ force: true });\n    await page.locator('.dropdown-item').filter({ hasText: 'Paste' }).click();\n\n    await expect(page.locator('.collection-item-name').filter({ hasText: 'original-request' })).toHaveCount(3);\n  });\n\n  test('should copy and paste a request into a different collection', async ({ page, createTmpDir }) => {\n    await createCollection(page, 'test-collection-2', await createTmpDir('test-collection-2'));\n    const collection = page.locator('.collection-name').filter({ hasText: 'test-collection-2' });\n\n    // Paste into the collection root\n    await collection.hover();\n    await collection.locator('.collection-actions .icon').click();\n    await page.locator('.dropdown-item').filter({ hasText: 'Paste' }).click();\n\n    // Verify the pasted request appears with the same name\n    await expect(page.locator('.collection-item-name').filter({ hasText: 'original-request' })).toHaveCount(4);\n  });\n\n  test('should paste request into parent folder even if request is selected', async ({ page, createTmpDir }) => {\n    // Create a collection and a request\n    await createCollection(page, 'test-collection-3', await createTmpDir('test-collection-3'));\n    await createRequest(page, 'request-to-copy', 'test-collection-3');\n\n    const modifier = process.platform === 'darwin' ? 'Meta' : 'Control';\n\n    // Copy the request\n    await page.locator('.collection-item-name').filter({ hasText: 'request-to-copy' }).click();\n    await page.keyboard.press(`${modifier}+C`);\n    await page.keyboard.press(`${modifier}+V`);\n\n    // Verify the pasted request appears with the same name\n    await expect(page.locator('.collection-item-name').filter({ hasText: 'request-to-copy' })).toHaveCount(2);\n  });\n});\n"
  },
  {
    "path": "tests/request/copy-request/keyboard-shortcuts.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections, createCollection } from '../../utils/page';\n\ntest.describe('Copy and Paste with Keyboard Shortcuts', () => {\n  test.afterAll(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('should copy and paste request using keyboard shortcuts', async ({ page, createTmpDir }) => {\n    await createCollection(page, 'keyboard-test', await createTmpDir('keyboard-test'));\n    const collection = page.locator('.collection-name').filter({ hasText: 'keyboard-test' });\n\n    // Create a request\n    await collection.hover();\n    await collection.locator('.collection-actions .icon').click();\n    await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();\n    await page.getByPlaceholder('Request Name').fill('test-request');\n    await page.locator('#new-request-url .CodeMirror').click();\n    await page.locator('textarea').fill('https://echo.usebruno.com');\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    const requestItem = page.locator('.collection-item-name').filter({ hasText: 'test-request' });\n    await expect(requestItem).toBeVisible();\n\n    // Focus the request item\n    await requestItem.click();\n    await requestItem.focus();\n\n    // Wait for keyboard focus indicator\n    await expect(requestItem).toHaveClass(/item-keyboard-focused/);\n\n    // Use Cmd+C on Mac, Ctrl+C on Windows/Linux\n    const modifier = process.platform === 'darwin' ? 'Meta' : 'Control';\n    await page.keyboard.press(`${modifier}+KeyC`);\n\n    // Verify copy success (toast message)\n    await expect(page.getByText(/Request copied/i).first()).toBeVisible();\n\n    // Focus the collection to paste\n    await collection.click();\n    await collection.focus();\n\n    // Use Cmd+V on Mac, Ctrl+V on Windows/Linux\n    await page.keyboard.press(`${modifier}+KeyV`);\n\n    // Verify paste success\n    await expect(page.getByText(/pasted successfully/i).first()).toBeVisible();\n\n    // Verify the pasted request appears\n    await expect(page.locator('.collection-item-name').filter({ hasText: 'test-request' })).toHaveCount(2);\n  });\n\n  test('should copy and paste folder using keyboard shortcuts', async ({ page }) => {\n    const collection = page.locator('.collection-name').filter({ hasText: 'keyboard-test' });\n\n    // Create a folder\n    await collection.hover();\n    await collection.locator('.collection-actions .icon').click();\n    await page.locator('.dropdown-item').filter({ hasText: 'New Folder' }).click();\n    await page.locator('#folder-name').fill('test-folder');\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    const folder = page.locator('.collection-item-name').filter({ hasText: 'test-folder' });\n    await expect(folder).toBeVisible();\n\n    // Focus the folder\n    await folder.click();\n    await folder.focus();\n\n    // Wait for keyboard focus indicator\n    await expect(folder).toHaveClass(/item-keyboard-focused/);\n\n    // Use keyboard shortcut to copy\n    const modifier = process.platform === 'darwin' ? 'Meta' : 'Control';\n    await page.keyboard.press(`${modifier}+KeyC`);\n\n    // Verify copy success\n    await expect(page.getByText(/Folder copied/i).first()).toBeVisible();\n\n    // Focus the collection to paste\n    await collection.click();\n    await collection.focus();\n\n    // Use keyboard shortcut to paste\n    await page.keyboard.press(`${modifier}+KeyV`);\n\n    // Verify paste success\n    await expect(page.getByText(/pasted successfully/i).first()).toBeVisible();\n\n    // Verify the pasted folder appears\n    await expect(page.locator('.collection-item-name').filter({ hasText: 'test-folder' })).toHaveCount(2);\n  });\n});\n"
  },
  {
    "path": "tests/request/delete-request/delete-request-sequence-updation.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections, createCollection, createRequest, deleteRequest } from '../../utils/page';\n\ntest.describe('Delete Request Sequence Updation', () => {\n  test.afterAll(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('Maintain correct sequence after deleting requests', async ({ page, createTmpDir }) => {\n    const collectionName = 'test-collection';\n\n    // Create a collection\n    await createCollection(page, collectionName, await createTmpDir(collectionName));\n\n    // Create request-a\n    await createRequest(page, 'request-a', collectionName);\n\n    // Create request-b\n    await createRequest(page, 'request-b', collectionName);\n\n    // Create request-c\n    await createRequest(page, 'request-c', collectionName);\n\n    // Create request-d\n    await createRequest(page, 'request-d', collectionName);\n\n    // Verify all requests are created in order\n    const allRequests = page.locator('.collection-item-name');\n    await expect(allRequests.nth(0)).toContainText('request-a');\n    await expect(allRequests.nth(1)).toContainText('request-b');\n    await expect(allRequests.nth(2)).toContainText('request-c');\n    await expect(allRequests.nth(3)).toContainText('request-d');\n\n    // Delete request-b\n    await deleteRequest(page, 'request-b', collectionName);\n\n    // Delete request-c\n    await deleteRequest(page, 'request-c', collectionName);\n\n    // Verify remaining requests are in correct order (a and d)\n    const remainingRequests = page.locator('.collection-item-name');\n    await expect(remainingRequests.nth(0)).toContainText('request-a');\n    await expect(remainingRequests.nth(1)).toContainText('request-d');\n\n    // Create request-e\n    await createRequest(page, 'request-e', collectionName);\n\n    // Verify request-e is created at the last position (3rd position: a, d, e)\n    const finalRequests = page.locator('.collection-item-name');\n    await expect(finalRequests.nth(0)).toContainText('request-a');\n    await expect(finalRequests.nth(1)).toContainText('request-d');\n    await expect(finalRequests.nth(2)).toContainText('request-e');\n    await expect(finalRequests).toHaveCount(3);\n  });\n});\n"
  },
  {
    "path": "tests/request/encoding/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"encoding-test\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}"
  },
  {
    "path": "tests/request/encoding/collection/encode-url-preencoded.bru",
    "content": "meta {\n  name: encode-url-preencoded\n  type: http\n  seq: 2\n}\n\nget {\n  url: http://base.source?name=John%20Doe\n  body: none\n  auth: inherit\n}\n\nsettings {\n  encodeUrl: true\n}"
  },
  {
    "path": "tests/request/encoding/collection/encode-url-unencoded.bru",
    "content": "meta {\n  name: encode-url-unencoded\n  type: http\n  seq: 1\n}\n\nget {\n  url: http://base.source?name=John Doe\n  body: none\n  auth: inherit\n}\n\nsettings {\n  encodeUrl: true\n}"
  },
  {
    "path": "tests/request/encoding/collection/raw-url-preencoded.bru",
    "content": "meta {\n  name: raw-url-preencoded\n  type: http\n  seq: 4\n}\n\nget {\n  url: http://base.source?name=John%20Doe\n  body: none\n  auth: inherit\n}\n\nsettings {\n  encodeUrl: false\n}"
  },
  {
    "path": "tests/request/encoding/collection/raw-url-unencoded.bru",
    "content": "meta {\n  name: raw-url-unencoded\n  type: http\n  seq: 3\n}\n\nget {\n  url: http://base.source?name=John Doe\n  body: none\n  auth: inherit\n}\n\nsettings {\n  encodeUrl: false\n}"
  },
  {
    "path": "tests/request/encoding/curl-encoding.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { openCollection } from '../../utils/page';\nimport { buildCommonLocators } from '../../utils/page/locators';\n\ntest.describe('Code Generation URL Encoding', () => {\n  test.describe('when encodeUrl is true', () => {\n    test('should encode unencoded URL (spaces to %20)', async ({ pageWithUserData: page }) => {\n      const { sidebar, request, modal } = buildCommonLocators(page);\n\n      await openCollection(page, 'encoding-test');\n      await sidebar.request('encode-url-unencoded').click();\n\n      await request.generateCodeButton().click();\n      await expect(page.getByRole('dialog')).toBeVisible();\n\n      const codeEditor = page.locator('.editor-content .CodeMirror').first();\n      await expect(codeEditor).toBeVisible();\n\n      const generatedCode = await codeEditor.textContent();\n      expect(generatedCode).toContain('http://base.source?name=John%20Doe');\n\n      await modal.closeButton().click();\n      await modal.closeButton().waitFor({ state: 'hidden' });\n    });\n\n    test('should double-encode pre-encoded URL (%20 to %2520)', async ({ pageWithUserData: page }) => {\n      const { sidebar, request, modal } = buildCommonLocators(page);\n\n      await openCollection(page, 'encoding-test');\n      await sidebar.request('encode-url-preencoded').click();\n\n      await request.generateCodeButton().click();\n      await expect(page.getByRole('dialog')).toBeVisible();\n\n      const codeEditor = page.locator('.editor-content .CodeMirror').first();\n      await expect(codeEditor).toBeVisible();\n\n      const generatedCode = await codeEditor.textContent();\n      expect(generatedCode).toContain('http://base.source?name=John%2520Doe');\n\n      await modal.closeButton().click();\n      await modal.closeButton().waitFor({ state: 'hidden' });\n    });\n  });\n\n  test.describe('when encodeUrl is false', () => {\n    test('should preserve unencoded URL as-is (spaces kept)', async ({ pageWithUserData: page }) => {\n      const { sidebar, request, modal } = buildCommonLocators(page);\n\n      await openCollection(page, 'encoding-test');\n      await sidebar.request('raw-url-unencoded').click();\n\n      await request.generateCodeButton().click();\n      await expect(page.getByRole('dialog')).toBeVisible();\n\n      const codeEditor = page.locator('.editor-content .CodeMirror').first();\n      await expect(codeEditor).toBeVisible();\n\n      const generatedCode = await codeEditor.textContent();\n      expect(generatedCode).toContain('http://base.source?name=John Doe');\n\n      await modal.closeButton().click();\n      await modal.closeButton().waitFor({ state: 'hidden' });\n    });\n\n    test('should preserve pre-encoded URL as-is', async ({ pageWithUserData: page }) => {\n      const { sidebar, request, modal } = buildCommonLocators(page);\n\n      await openCollection(page, 'encoding-test');\n      await sidebar.request('raw-url-preencoded').click();\n\n      await request.generateCodeButton().click();\n      await expect(page.getByRole('dialog')).toBeVisible();\n\n      const codeEditor = page.locator('.editor-content .CodeMirror').first();\n      await expect(codeEditor).toBeVisible();\n\n      const generatedCode = await codeEditor.textContent();\n      expect(generatedCode).toContain('http://base.source?name=John%20Doe');\n\n      await modal.closeButton().click();\n      await modal.closeButton().waitFor({ state: 'hidden' });\n    });\n  });\n});\n"
  },
  {
    "path": "tests/request/encoding/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{projectRoot}}/tests/request/encoding/collection\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "tests/request/encoding/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/request/encoding/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/request/headers/header-validation.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections, createCollection, createRequest, openCollection, openRequest, saveRequest, selectRequestPaneTab } from '../../utils/page';\nimport { getTableCell } from '../../utils/page/locators';\n\ntest.describe.serial('Header Validation', () => {\n  test.afterAll(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test.beforeAll(async ({ page, createTmpDir }) => {\n    await test.step('Create collection and request', async () => {\n      await createCollection(page, 'header-validation', await createTmpDir('header-validation'));\n      await createRequest(page, 'test-headers', '', {\n        url: 'https://httpbin.org/get',\n        inFolder: false\n      });\n    });\n\n    await test.step('Open the request', async () => {\n      await openCollection(page, 'header-validation');\n      await openRequest(page, 'header-validation', 'test-headers', { persist: true });\n    });\n  });\n\n  test('should show error icon when header name contains spaces', async ({ page, createTmpDir }) => {\n    await test.step('Navigate to Headers tab', async () => {\n      await selectRequestPaneTab(page, 'Headers');\n    });\n\n    await test.step('Enter header name with space and verify error icon', async () => {\n      const headerRow = page.locator('table tbody tr').first();\n      const nameCell = getTableCell(headerRow, 0);\n\n      // Click on the CodeMirror editor and type a header name with space\n      await nameCell.locator('.CodeMirror').click();\n      await nameCell.locator('textarea').fill('invalid header');\n\n      // Verify the error icon is visible\n      const errorIcon = headerRow.locator('.text-red-600');\n      await expect(errorIcon).toBeVisible();\n\n      // Hover over the error icon to show the tooltip\n      await errorIcon.hover();\n\n      // Verify the tooltip message\n      const tooltip = page.locator('.tooltip-mod');\n      await expect(tooltip).toContainText('Header name cannot contain spaces or newlines');\n    });\n\n    await test.step('Enter valid header name and verify no error icon', async () => {\n      const headerRow = page.locator('table tbody tr').first();\n      const nameCell = getTableCell(headerRow, 0);\n\n      // Clear and enter a valid header name - use triple-click to select all (works cross-platform)\n      await nameCell.locator('.CodeMirror').click({ clickCount: 3 });\n      await nameCell.locator('textarea').fill('Valid-Header');\n\n      // Verify the error icon is not visible\n      const errorIcon = headerRow.locator('.text-red-600');\n\n      // wait for formik to revalidate\n      await headerRow.click();\n      await expect(errorIcon).not.toBeVisible({});\n    });\n  });\n\n  test('should show error icon when header value contains newlines', async ({ page }) => {\n    await test.step('Navigate to Headers tab', async () => {\n      await selectRequestPaneTab(page, 'Headers');\n    });\n\n    await test.step('Enter header value with newline and verify error icon', async () => {\n      const headerRow = page.locator('table tbody tr').first();\n      const valueCell = getTableCell(headerRow, 1);\n\n      // Click on the value CodeMirror editor and type a value with newline\n      await valueCell.locator('.CodeMirror').click();\n      await valueCell.locator('textarea').fill('header\\nValue');\n\n      // Verify the error icon is visible\n      const errorIcon = headerRow.locator('.text-red-600');\n      await expect(errorIcon).toBeVisible();\n\n      // Hover over the error icon to show the tooltip\n      await errorIcon.hover();\n\n      // Verify the tooltip message\n      const tooltip = page.locator('.tooltip-mod');\n      await expect(tooltip).toContainText('Header value cannot contain newlines');\n\n      // Save the request\n      await page.keyboard.press('Control+s');\n    });\n  });\n});\n"
  },
  {
    "path": "tests/request/newlines/newlines-persistence.spec.ts",
    "content": "import { test, expect, closeElectronApp } from '../../../playwright';\nimport { createCollection, openCollection, selectRequestPaneTab } from '../../utils/page';\nimport { getTableCell } from '../../utils/page/locators';\n\ntest('should persist request with newlines across app restarts', async ({ createTmpDir, launchElectronApp }) => {\n  const userDataPath = await createTmpDir('newlines-persistence-userdata');\n  const collectionPath = await createTmpDir('newlines-persistence-collection');\n\n  // Create collection and request\n  const app1 = await launchElectronApp({ userDataPath });\n  const page = await app1.firstWindow();\n\n  await createCollection(page, 'newlines-persistence', collectionPath);\n\n  const collection = page.getByTestId('collections').locator('.collection-name').filter({ hasText: 'newlines-persistence' });\n  await collection.hover();\n  await collection.locator('.collection-actions .icon').click();\n  await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();\n  await page.getByPlaceholder('Request Name').fill('persistence-test');\n  await page.locator('#new-request-url').locator('.CodeMirror').click();\n  await page.locator('#new-request-url').locator('textarea').fill('https://httpbin.org/get');\n  await page.locator('.bruno-modal').getByRole('button', { name: 'Create', exact: true }).click();\n\n  await openCollection(page, 'newlines-persistence');\n\n  await page.locator('.collection-item-name').filter({ hasText: 'persistence-test' }).dblclick();\n\n  await selectRequestPaneTab(page, 'Params');\n  const paramRow = page.locator('table tbody tr').first();\n  await getTableCell(paramRow, 0).getByRole('textbox').fill('queryParamKey');\n\n  await selectRequestPaneTab(page, 'Headers');\n  const headerRow = page.locator('table tbody tr').first();\n  await getTableCell(headerRow, 0).locator('.CodeMirror').click();\n  await getTableCell(headerRow, 0).locator('textarea').fill('headerKey');\n  await getTableCell(headerRow, 1).locator('.CodeMirror').click();\n  await getTableCell(headerRow, 1).locator('textarea').fill('header\\nValue');\n\n  await selectRequestPaneTab(page, 'Vars');\n  const preReqRow = page.locator('table').first().locator('tbody tr').first();\n  await getTableCell(preReqRow, 0).getByRole('textbox').fill('preRequestVar');\n  // Wait for table to stabilize after fill (new empty row may be appended)\n  await expect(getTableCell(preReqRow, 0).getByRole('textbox')).toHaveValue('preRequestVar');\n  await getTableCell(preReqRow, 1).locator('.CodeMirror').click();\n  await getTableCell(preReqRow, 1).locator('textarea').fill('pre\\nRequest\\nValue');\n\n  const postResRow = page.locator('table').nth(1).locator('tbody tr').first();\n  await getTableCell(postResRow, 0).getByRole('textbox').fill('postResponseVar');\n  // Wait for table to stabilize after fill (new empty row may be appended)\n  await expect(getTableCell(postResRow, 0).getByRole('textbox')).toHaveValue('postResponseVar');\n  await getTableCell(postResRow, 1).locator('.CodeMirror').click();\n  await getTableCell(postResRow, 1).locator('textarea').fill('post\\nResponse\\nValue');\n\n  const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';\n  await page.keyboard.press(saveShortcut);\n  await expect(page.getByText('Request saved successfully')).toBeVisible();\n  await closeElectronApp(app1);\n\n  // Verify persistence after restart\n  const app2 = await launchElectronApp({ userDataPath });\n  const page2 = await app2.firstWindow();\n\n  await page2.getByTestId('collections').locator('.collection-name').filter({ hasText: 'newlines-persistence' }).click();\n  await page2.locator('.collection-item-name').filter({ hasText: 'persistence-test' }).dblclick();\n\n  // Verify params persisted\n  await selectRequestPaneTab(page2, 'Params');\n  await expect(page2.locator('table tbody tr')).toHaveCount(2);\n\n  // Verify headers persisted\n  await selectRequestPaneTab(page2, 'Headers');\n  await expect(page2.locator('table tbody tr')).toHaveCount(2);\n\n  // Verify vars persisted\n  await selectRequestPaneTab(page2, 'Vars');\n  await expect(page2.locator('table').first().locator('tbody tr')).toHaveCount(2);\n  await expect(page2.locator('table').nth(1).locator('tbody tr')).toHaveCount(2);\n\n  await closeElectronApp(app2);\n});\n"
  },
  {
    "path": "tests/request/response-pane-update-when-focused.spec.ts",
    "content": "import { test, expect } from '../../playwright';\nimport {\n  createCollection,\n  createRequest,\n  sendRequest,\n  openRequest,\n  closeAllCollections,\n  selectRequestPaneTab\n} from '../utils/page';\nimport { buildCommonLocators } from '../utils/page/locators';\n\nconst runShortcut = process.platform === 'darwin' ? 'Meta+Enter' : 'Control+Enter';\nconst selectAllShortcut = process.platform === 'darwin' ? 'Meta+a' : 'Control+a';\nconst echoUrl = 'https://echo.usebruno.com';\n\ntest.describe.serial('Response pane updates when focused and request is re-sent', () => {\n  const collectionName = 'response-pane-update-test';\n  const requestName = 'Echo Request';\n\n  test.beforeAll(async ({ page, createTmpDir }) => {\n    const collectionPath = await createTmpDir('response-pane-collection');\n    await createCollection(page, collectionName, collectionPath);\n    await createRequest(page, requestName, collectionName, { url: echoUrl, method: 'POST' });\n  });\n\n  test.afterAll(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('Response pane shows new response after re-send with Cmd+Enter while focused in response', async ({\n    page\n  }) => {\n    const locators = buildCommonLocators(page);\n\n    await test.step('Open request and set body to {\"run\": 1}', async () => {\n      await openRequest(page, collectionName, requestName);\n\n      await selectRequestPaneTab(page, 'Body');\n      await locators.request.bodyModeSelector().click();\n      await locators.dropdown.item('JSON').click();\n\n      const bodyCodeMirror = locators.request.bodyEditor().locator('.CodeMirror');\n      await bodyCodeMirror.click();\n      await page.keyboard.press(selectAllShortcut);\n      await page.keyboard.type('{\"run\": 1}');\n      await expect(locators.request.bodyEditor()).toContainText('\"run\": 1', { timeout: 5000 });\n    });\n\n    await test.step('Send first request and verify response contains run: 1', async () => {\n      await sendRequest(page, 200, 15000);\n      const runEquals1 = /\"run\":\\s*1/; // \"run\" with value 1, optional space after colon\n      await expect(locators.response.previewContainer()).toContainText(runEquals1);\n    });\n\n    await test.step('Change body to {\"run\": 2}', async () => {\n      await selectRequestPaneTab(page, 'Body');\n      const bodyCodeMirror = locators.request.bodyEditor().locator('.CodeMirror');\n      await bodyCodeMirror.click();\n      await page.keyboard.press(selectAllShortcut);\n      await page.keyboard.type('{\"run\": 2}');\n      await expect(locators.request.bodyEditor()).toContainText('\"run\": 2', { timeout: 5000 });\n    });\n\n    await test.step('Click inside response pane (Raw/JSON editor) to give it focus', async () => {\n      const responseEditor = locators.response.previewContainerCodeMirror();\n      await responseEditor.waitFor({ state: 'visible', timeout: 5000 });\n      await responseEditor.click();\n    });\n\n    await test.step('Press Cmd+Enter / Ctrl+Enter to re-send request', async () => {\n      await page.keyboard.press(runShortcut);\n      await expect(locators.response.statusCode()).toContainText('200', { timeout: 15000 });\n    });\n\n    await test.step('Response pane must show new response (run: 2)', async () => {\n      const responseBody = locators.response.previewContainer();\n      // Must show the new response body: single JSON object with \"run\": 2\n      const runEquals2 = /\"run\":\\s*2/;\n      await expect(responseBody).toContainText(runEquals2, { timeout: 5000 });\n    });\n  });\n});\n"
  },
  {
    "path": "tests/request/save/save.spec.ts",
    "content": "import { test, expect, Locator, Page } from '../../../playwright';\nimport { closeAllCollections, createCollection } from '../../utils/page';\nimport { buildCommonLocators } from '../../utils/page/locators';\nimport { waitForPredicate } from '../../utils/wait';\n\nconst isRequestSaved = async (saveButton: Locator) => {\n  // Saved state uses the className cursor-default; unsaved uses cursor-pointer.\n  return await saveButton.locator('svg').evaluate((node) => (node as HTMLElement).classList.contains('cursor-default'));\n};\n\nconst setup = async (page: Page, createTmpDir: (tag?: string | undefined) => Promise<string>) => {\n  await createCollection(page, 'source-collection', await createTmpDir('source-collection'));\n\n  const sourceCollection = page.locator('.collection-name').filter({ hasText: 'source-collection' });\n  await sourceCollection.hover();\n  await sourceCollection.locator('.collection-actions .icon').click();\n  await page.locator('.dropdown-item').filter({ hasText: 'New Request' }).click();\n  await page.getByPlaceholder('Request Name').fill('test-request');\n  await page.locator('#new-request-url .CodeMirror').click();\n  await page.locator('textarea').fill('https://echo.usebruno.com');\n  await page.getByRole('button', { name: 'Create' }).click();\n  await expect(page.locator('.collection-item-name').filter({ hasText: 'test-request' })).toBeVisible();\n};\n\ntest.describe.serial('save requests', () => {\n  test.beforeAll(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('saves new http request', async ({ page, createTmpDir }) => {\n    // prep the collection by creating a new collection and a new http request\n    await setup(page, createTmpDir);\n\n    const locators = buildCommonLocators(page);\n    const originalUrl = 'https://echo.usebruno.com';\n    const replacementUrl = 'ws://localhost:8082';\n\n    const clearText = async (text: string) => {\n      for (let i = text.length; i > 0; i--) {\n        await page.keyboard.press('Backspace');\n      }\n    };\n\n    // Open the request tab\n    await page.locator('.collection-item-name').filter({ hasText: 'test-request' }).dblclick();\n    await expect(page.locator('.request-tab .tab-label').filter({ hasText: 'test-request' })).toBeVisible();\n\n    // remove the original url from the request\n    await page.locator('.input-container').filter({ hasText: originalUrl }).first().click();\n    await clearText(originalUrl);\n\n    // replace it with an arbitrary url\n    await page.keyboard.insertText(replacementUrl);\n\n    // check if the request is now unsaved\n    await expect(await isRequestSaved(locators.saveButton())).toBe(false);\n\n    // trigger a save\n    locators.saveButton().click();\n\n    // Wait for it to be saved\n    const result = await waitForPredicate(() => isRequestSaved(locators.saveButton()));\n    await expect(result).toBe(true);\n  });\n});\n"
  },
  {
    "path": "tests/request/settings/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"settings-test\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}"
  },
  {
    "path": "tests/request/settings/collection/max-redirects.bru",
    "content": "meta {\n  name: max-redirects-test\n  type: http\n  seq: 1\n}\n\nget {\n  url: http://localhost:8081/api/redirect/2\n  body: none\n  auth: inherit\n}\n\nsettings {\n  followRedirects: true\n  maxRedirects: 1\n  timeout: 0\n}\n"
  },
  {
    "path": "tests/request/settings/collection/no-redirects.bru",
    "content": "meta {\n  name: no-redirects-test\n  type: http\n  seq: 2\n}\n\nget {\n  url: http://localhost:8081/api/redirect/2\n  body: none\n  auth: inherit\n}\n\nsettings {\n  followRedirects: false\n  maxRedirects: 5\n  timeout: 0\n}\n"
  },
  {
    "path": "tests/request/settings/collection/timeout.bru",
    "content": "meta {\n  name: timeout-test\n  type: http\n  seq: 3\n}\n\nget {\n  url: https://testbench-sanity.usebruno.com/redirect-to-ping\n  body: none\n  auth: inherit\n}\n\nsettings {\n  followRedirects: false\n  maxRedirects: 0\n  timeout: 5\n}\n"
  },
  {
    "path": "tests/request/settings/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{projectRoot}}/tests/request/settings/collection\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "tests/request/settings/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/request/settings/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/request/settings/max-redirects.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { selectRequestPaneTab } from '../../utils/page';\n\ntest.describe('Max Redirects Settings Tests', () => {\n  test('should configure and test max redirects settings', async ({\n    pageWithUserData: page\n  }) => {\n    // Navigate to the test collection and request\n    await expect(page.locator('#sidebar-collection-name').getByText('settings-test')).toBeVisible();\n\n    await page.locator('#sidebar-collection-name').getByText('settings-test').click();\n\n    // Navigate to the max-redirects request\n    await page.getByRole('complementary').getByText('max-redirects').click();\n\n    // Go to Settings tab\n    await selectRequestPaneTab(page, 'Settings');\n\n    // Test Max Redirects Settings\n    const maxRedirectsInput = page.locator('input[id=\"maxRedirects\"]');\n    await expect(maxRedirectsInput).toBeVisible();\n\n    // Verify default value from .bru file (1)\n    await expect(maxRedirectsInput).toHaveValue('1');\n\n    // Test Follow Redirects toggle\n    const followRedirectsToggle = page.getByTestId('follow-redirects-toggle');\n    await expect(followRedirectsToggle).toBeVisible();\n    await expect(followRedirectsToggle).toBeChecked();\n\n    // Send the request\n    await page.getByTestId('send-arrow-icon').click();\n\n    await expect(page.getByTestId('response-status-code')).toContainText('302', { timeout: 15000 });\n\n    // change the max redirects to 2\n    await maxRedirectsInput.fill('2');\n    await page.getByTestId('send-arrow-icon').click();\n    await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 });\n\n    // Close without saving to avoid modifying the .bru file\n    await page.locator('.close-icon-container').click({ force: true });\n    await page.locator('button:has-text(\"Don\\'t Save\")').first().click();\n  });\n});\n"
  },
  {
    "path": "tests/request/settings/no-redirects.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { selectRequestPaneTab } from '../../utils/page';\n\ntest.describe('No Redirects Settings Tests', () => {\n  test('should configure and test no redirects settings', async ({\n    pageWithUserData: page\n  }) => {\n    // Navigate to the test collection and request\n    await expect(page.locator('#sidebar-collection-name').getByText('settings-test')).toBeVisible();\n\n    await page.locator('#sidebar-collection-name').getByText('settings-test').click();\n\n    // Navigate to the no-redirects request\n    await page.getByRole('complementary').getByText('no-redirects').click();\n\n    // Go to Settings tab\n    await selectRequestPaneTab(page, 'Settings');\n\n    // Test No Redirects Settings\n    const maxRedirectsInput = page.locator('input[id=\"maxRedirects\"]');\n    await expect(maxRedirectsInput).toBeVisible();\n\n    // Verify default value from .bru file (5)\n    await expect(maxRedirectsInput).toHaveValue('5');\n\n    // Test Follow Redirects toggle - should be unchecked\n    const followRedirectsToggle = page.getByTestId('follow-redirects-toggle');\n    await expect(followRedirectsToggle).toBeVisible();\n    await expect(followRedirectsToggle).not.toBeChecked();\n\n    // Send the request - should stop at first redirect (302) without following\n    await page.getByTestId('send-arrow-icon').click();\n\n    // Should get 302 because redirects are disabled, regardless of maxRedirects value\n    await expect(page.getByTestId('response-status-code')).toContainText('302', { timeout: 15000 });\n\n    // Toggle follow redirects to true\n    await followRedirectsToggle.click();\n    await expect(followRedirectsToggle).toBeChecked();\n\n    // Send request again - now should follow redirects and get 200\n    await page.getByTestId('send-arrow-icon').click();\n    await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 });\n\n    // Close without saving to avoid modifying the .bru file\n    await page.locator('.close-icon-container').click({ force: true });\n    await page.locator('button:has-text(\"Don\\'t Save\")').first().click();\n  });\n});\n"
  },
  {
    "path": "tests/request/settings/timeout.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections, selectRequestPaneTab } from '../../utils/page';\n\ntest.describe('Timeout Settings Tests', () => {\n  test('should configure and test timeout settings', async ({\n    pageWithUserData: page\n  }) => {\n    // Navigate to the test collection and request\n    await expect(page.locator('#sidebar-collection-name').getByText('settings-test')).toBeVisible();\n\n    await page.locator('#sidebar-collection-name').getByText('settings-test').click();\n    // Navigate to thetimeout request\n    await page.getByRole('complementary').getByText('timeout-test').click();\n\n    // Go to Settings tab\n    await selectRequestPaneTab(page, 'Settings');\n\n    // Test Timeout Settings with custom value\n    const timeoutInput = page.locator('input[id=\"timeout\"]');\n    await expect(timeoutInput).toBeVisible();\n\n    // Verify default value from .bru file (5)\n    await expect(timeoutInput).toHaveValue('5');\n\n    await page.getByTestId('send-arrow-icon').click();\n\n    const responsePane = page.locator('.response-pane');\n    await expect(responsePane).toContainText('timeout of 5ms exceeded');\n\n    // Now test inherit functionality\n    // Click the X button to reset to inherit\n    const resetButton = page.locator('button[title=\"Reset to inherit\"]');\n    await expect(resetButton).toBeVisible();\n    await resetButton.click();\n\n    // After reset, should see \"Inherit\" button instead of input\n    const inheritButton = page.locator('button:has-text(\"Inherit\")');\n    await expect(inheritButton).toBeVisible();\n    await expect(timeoutInput).not.toBeVisible();\n\n    // Run the request with inherit timeout\n    await page.getByTestId('send-arrow-icon').click();\n\n    // Verify the request runs successfully with inherited timeout (should not timeout)\n    await expect(responsePane).toContainText('302');\n\n    // Close without saving to avoid modifying the .bru file\n    await page.locator('.close-icon-container').click({ force: true });\n    await page.locator('button:has-text(\"Don\\'t Save\")').first().click();\n  });\n\n  test.afterEach(async ({ pageWithUserData: page }) => {\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n});\n"
  },
  {
    "path": "tests/request/tests/custom-search/custom-search.spec.ts",
    "content": "import { test, expect } from '../../../../playwright';\nimport { selectRequestPaneTab } from '../../../utils/page';\n\nconst findShortcut = process.platform === 'darwin' ? 'Meta+f' : 'Control+f';\n\ntest.describe('Custom Search Functionality in Scripts Tab', () => {\n  test('should open search box when Cmd+F or Ctrl+F is pressed in scripts tab', async ({ pageWithUserData: page }) => {\n    await page.getByTestId('collections').locator('#sidebar-collection-name').filter({ hasText: 'custom-search' }).click();\n\n    await page.getByText('search-test-request').click();\n\n    await selectRequestPaneTab(page, 'Script');\n\n    // Pre Request tab should be active by default\n    await expect(page.getByRole('button', { name: 'Pre Request' })).toBeVisible();\n    await expect(page.getByRole('button', { name: 'Post Response' })).toBeVisible();\n\n    // Click on Pre Request tab to ensure it's active\n    await page.getByRole('button', { name: 'Pre Request' }).click();\n\n    const preRequestEditor = page.getByTestId('pre-request-script-editor').locator('.CodeMirror').first();\n    const preTextarea = preRequestEditor.locator('textarea[tabindex=\"0\"]');\n    await preTextarea.focus();\n\n    const preContent = await preRequestEditor.textContent();\n    console.log('Pre Request content loaded:', preContent?.substring(0, 100));\n\n    await page.keyboard.press(findShortcut);\n\n    // Verify search box appears\n    const preEditorSearchBar = page.getByTestId('pre-request-script-editor');\n    await expect(preEditorSearchBar.locator('.bruno-search-bar input[placeholder=\"Search...\"]')).toBeVisible();\n\n    // Test search functionality\n    const searchInput = preEditorSearchBar.locator('.bruno-search-bar input[placeholder=\"Search...\"]');\n    await searchInput.fill('searchableText');\n    await expect(preEditorSearchBar.locator('.searchbar-result-count')).toContainText('1 / 4');\n\n    // Test search options\n    const regexButton = preEditorSearchBar.locator('.searchbar-icon-btn').filter({ hasText: '' }).first();\n    const caseSensitiveButton = preEditorSearchBar.locator('.searchbar-icon-btn').filter({ hasText: '' }).nth(1);\n    const wholeWordButton = preEditorSearchBar.locator('.searchbar-icon-btn').filter({ hasText: '' }).nth(2);\n\n    // Test regex search\n    await regexButton.click();\n    await searchInput.fill('test\\\\w+');\n    await expect(preEditorSearchBar.locator('.searchbar-result-count')).toContainText('1 / 1');\n\n    // Test case sensitive search\n    await regexButton.click();\n    await caseSensitiveButton.click();\n    await searchInput.fill('Test');\n    await expect(preEditorSearchBar.locator('.searchbar-result-count')).toContainText('0 results');\n\n    // Test whole word search\n    await caseSensitiveButton.click();\n    await wholeWordButton.click();\n    await searchInput.fill('hello');\n    await expect(preEditorSearchBar.locator('.searchbar-result-count')).toContainText('1 / 1');\n\n    // Test close search\n    const closeButton = page.getByTestId('pre-request-script-editor').locator('.searchbar-icon-btn').last();\n    await closeButton.click();\n    await expect(page.getByTestId('pre-request-script-editor').locator('.bruno-search-bar')).not.toBeVisible();\n  });\n\n  test('should handle search in different script editors independently', async ({ pageWithUserData: page }) => {\n    await page.getByTestId('collections').locator('#sidebar-collection-name').filter({ hasText: 'custom-search' }).click();\n\n    await page.getByText('search-test-request').click();\n\n    await selectRequestPaneTab(page, 'Script');\n\n    // Test Pre Request tab\n    await page.getByRole('button', { name: 'Pre Request' }).click();\n\n    const preRequestEditor = page.getByTestId('pre-request-script-editor').locator('.CodeMirror').first();\n    const preTextarea = preRequestEditor.locator('textarea[tabindex=\"0\"]');\n    await preTextarea.focus();\n    await page.keyboard.press(findShortcut);\n\n    const preSearchInput = page.getByTestId('pre-request-script-editor').locator('.bruno-search-bar input[placeholder=\"Search...\"]');\n    await preSearchInput.fill('uniquePreVar');\n    await expect(page.getByTestId('pre-request-script-editor').locator('.searchbar-result-count')).toContainText('1 / 1');\n    await page.keyboard.press('Escape');\n\n    // Switch to Post Response tab\n    await page.getByRole('button', { name: 'Post Response' }).click();\n\n    const postResponseEditor = page.getByTestId('post-response-script-editor').locator('.CodeMirror').first();\n    const postTextarea = postResponseEditor.locator('textarea[tabindex=\"0\"]');\n    await postTextarea.focus();\n    await page.keyboard.press(findShortcut);\n\n    const postSearchInput = page.getByTestId('post-response-script-editor').locator('.bruno-search-bar input[placeholder=\"Search...\"]');\n    await postSearchInput.fill('uniquePostVar');\n    await expect(page.getByTestId('post-response-script-editor').locator('.searchbar-result-count')).toContainText('1 / 1');\n    await page.keyboard.press('Escape');\n  });\n\n  test('should maintain search state when switching between tabs', async ({ pageWithUserData: page }) => {\n    await page.getByTestId('collections').locator('#sidebar-collection-name').filter({ hasText: 'custom-search' }).click();\n\n    await page.getByText('search-test-request').click();\n\n    await selectRequestPaneTab(page, 'Script');\n\n    // Open search in Pre Request editor\n    await page.getByRole('button', { name: 'Pre Request' }).click();\n\n    const preRequestEditor = page.getByTestId('pre-request-script-editor').locator('.CodeMirror').first();\n    const preTextarea = preRequestEditor.locator('textarea[tabindex=\"0\"]');\n    await preTextarea.focus();\n    await page.keyboard.press(findShortcut);\n\n    const preSearchInput = page.getByTestId('pre-request-script-editor').locator('.bruno-search-bar input[placeholder=\"Search...\"]');\n    await preSearchInput.fill('commonVar');\n    await expect(page.getByTestId('pre-request-script-editor').locator('.searchbar-result-count')).toContainText('1 / 1');\n\n    // Switch to Post Response tab while search is open\n    await page.getByRole('button', { name: 'Post Response' }).click();\n\n    // Open search in Post Response editor\n    const postResponseEditor = page.getByTestId('post-response-script-editor').locator('.CodeMirror').first();\n    const postTextarea = postResponseEditor.locator('textarea[tabindex=\"0\"]');\n    await postTextarea.focus();\n    await page.keyboard.press(findShortcut);\n\n    const postSearchInput = page.getByTestId('post-response-script-editor').locator('.bruno-search-bar input[placeholder=\"Search...\"]');\n    await postSearchInput.fill('postVar');\n    await expect(page.getByTestId('post-response-script-editor').locator('.searchbar-result-count')).toContainText('1 / 1');\n\n    // Switch back to Pre Request tab\n    await page.getByRole('button', { name: 'Pre Request' }).click();\n\n    // Search state should be maintained in Pre Request editor\n    await expect(page.getByTestId('pre-request-script-editor').locator('.bruno-search-bar')).toBeVisible();\n    await expect(preSearchInput).toHaveValue('commonVar');\n\n    // Close the search in Pre Request editor\n    const closeButton = page.getByTestId('pre-request-script-editor').locator('.searchbar-icon-btn').last();\n    await closeButton.click();\n    await expect(page.getByTestId('pre-request-script-editor').locator('.bruno-search-bar')).not.toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/request/tests/custom-search/init-user-data/collection-security.json",
    "content": "{\n    \"collections\": [\n      {\n        \"path\": \"{{projectRoot}}/tests/request/collections/custom-search\",\n        \"securityConfig\": {\n          \"jsSandboxMode\": \"safe\"\n        }\n      }\n    ]\n}\n"
  },
  {
    "path": "tests/request/tests/custom-search/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\"{{projectRoot}}/tests/request/collections/custom-search\"],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/response/json-response-formatting/fixtures/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"collection\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}\n"
  },
  {
    "path": "tests/response/json-response-formatting/fixtures/collection/request.bru",
    "content": "meta {\n  name: request\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://echo.usebruno.com\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"bigint\": 1736184243098437392,\n    \"unicode\": [\"\\u4e00\",\"\\u4e8c\",\"\\u4e09\"],\n    \"forwardslashes\": \"\\/url\\/path\\/\"\n  }\n}\n"
  },
  {
    "path": "tests/response/json-response-formatting/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{collectionPath}}\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/response/json-response-formatting/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{collectionPath}}\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/response/json-response-formatting/json-response-formatting.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections } from '../../utils/page';\n\ntest.describe.serial('JSON Response Formatting', () => {\n  test.afterAll(async ({ pageWithUserData: page }) => {\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  test('should handle BigInt values and unicode chars in JSON response formatting', async ({ pageWithUserData: page }) => {\n    await test.step('Navigate to collection and request', async () => {\n      // Navigate to the test collection\n      await expect(page.locator('#sidebar-collection-name').getByText('collection')).toBeVisible();\n      await page.locator('#sidebar-collection-name').getByText('collection').click();\n\n      // Navigate to the request\n      await page.getByRole('complementary').getByText('request').click();\n    });\n\n    await test.step('Send request and verify response', async () => {\n      // Send the request\n      await page.getByTestId('send-arrow-icon').click();\n\n      // Wait for response\n      await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 });\n\n      // Verify the response is properly formatted JSON\n      const responseBody = page.locator('.response-pane');\n      await expect(responseBody).toBeVisible();\n\n      // The response should preserve `bigint` value precision\n      await expect(responseBody).toContainText('1736184243098437392');\n\n      // The response should handle unicode chars\n      await expect(responseBody).toContainText('一');\n      await expect(responseBody).toContainText('二');\n      await expect(responseBody).toContainText('三');\n\n      // The response should handle escaped forward slashes\n      await expect(responseBody).toContainText('/url/path/');\n    });\n  });\n});\n"
  },
  {
    "path": "tests/response/large-response-crash-prevention.spec.ts",
    "content": "import { test, expect } from '../../playwright';\nimport { closeAllCollections, createCollection, createRequest } from '../utils/page/actions';\n\ntest.describe('Large Response Crash/High Memory Usage Prevention', () => {\n  // Increase timeout to 1 minute for all tests in this describe block, default is 30 seconds.\n  // Prevents tests from failing due to timeout while waiting for the response, especially on slower internet connections.\n  test.setTimeout(1 * 60 * 1000); // 1 minute\n\n  test.afterAll(async ({ page }) => {\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  test('Show appropriate warning for responses over 10MB', async ({ page, createTmpDir }) => {\n    const collectionName = 'size-warning-test';\n    const requestName = 'large-response';\n\n    // Create collection (auto-opens the collection)\n    await createCollection(page, collectionName, await createTmpDir(collectionName));\n\n    // Create request using the dialog/modal flow\n    await createRequest(page, requestName, collectionName, {\n      url: 'https://samples.json-format.com/employees/json/employees_50MB.json'\n    });\n\n    // Send request\n    const sendButton = page.getByTestId('send-arrow-icon');\n    await sendButton.click();\n\n    // Verify warning appears\n    await expect(page.getByText('Large Response Warning')).toBeVisible({ timeout: 60000 });\n\n    // Verify warning content\n    await expect(page.getByText('Handling responses over')).toBeVisible();\n    await expect(page.getByText('could degrade performance')).toBeVisible();\n\n    // Verify action button\n    await expect(page.getByRole('button', { name: 'View', exact: true })).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/response/response-actions.spec.ts",
    "content": "import { test, expect } from '../../playwright';\nimport {\n  clickResponseAction,\n  closeAllCollections,\n  createCollection,\n  createRequest,\n  sendRequest,\n  switchResponseFormat,\n  switchToEditorTab\n} from '../utils/page/actions';\n\ntest.describe('Response Pane Actions', () => {\n  test.afterAll(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('should copy response to clipboard', async ({ page, createTmpDir }) => {\n    const collectionName = 'response-copy-test';\n\n    await test.step('Create collection and request', async () => {\n      await createCollection(page, collectionName, await createTmpDir(collectionName));\n      await createRequest(page, 'copy-test', collectionName, { url: 'https://testbench-sanity.usebruno.com/ping' });\n    });\n\n    await test.step('Send request and wait for response', async () => {\n      await sendRequest(page, 200);\n    });\n\n    await test.step('Copy response to clipboard', async () => {\n      await clickResponseAction(page, 'response-copy-btn');\n      await expect(page.getByText('Response copied to clipboard')).toBeVisible();\n    });\n  });\n\n  test('should copy Base64 when editor mode and Base64 format selected', async ({ page, createTmpDir }) => {\n    const collectionName = 'response-copy-base64-test';\n\n    await test.step('Create collection and request', async () => {\n      await createCollection(page, collectionName, await createTmpDir(collectionName));\n      await createRequest(page, 'base64-copy-test', collectionName, {\n        url: 'https://testbench-sanity.usebruno.com/ping'\n      });\n    });\n\n    await test.step('Send request and wait for response', async () => {\n      await sendRequest(page, 200);\n    });\n\n    await test.step('Switch to Base64 format (editor mode - preview OFF)', async () => {\n      await switchToEditorTab(page);\n      await switchResponseFormat(page, 'Base64');\n    });\n\n    await test.step('Copy response and verify clipboard contains Base64', async () => {\n      await clickResponseAction(page, 'response-copy-btn');\n      await expect(page.getByText('Response copied to clipboard')).toBeVisible();\n\n      const clipboardText = await page.evaluate(() => navigator.clipboard.readText());\n      // \"pong\" in Base64 is \"cG9uZw==\"\n      expect(clipboardText).toBe('cG9uZw==');\n    });\n  });\n});\n"
  },
  {
    "path": "tests/response/response-format-select-and-preview/fixtures/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"collection\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}\n"
  },
  {
    "path": "tests/response/response-format-select-and-preview/fixtures/collection/request-html.bru",
    "content": "meta {\n  name: request-html\n  type: http\n  seq: 5\n}\n\npost {\n  url: https://www.httpfaker.org/api/echo/custom\n  body: json\n  auth: inherit\n}\n\nbody:json {\n  {\n    \"headers\": { \"content-type\": \"text/html\" },\n    \"content\": \"<h1>hello</h1>\"\n  }\n}\n\nsettings {\n  encodeUrl: true\n  timeout: 0\n}\n"
  },
  {
    "path": "tests/response/response-format-select-and-preview/fixtures/collection/request-json.bru",
    "content": "meta {\n  name: request-json\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://echo.usebruno.com\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"hello\": \"bruno\"\n  }\n}\n"
  },
  {
    "path": "tests/response/response-format-select-and-preview/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{projectRoot}}/tests/response/response-format-select-and-preview/fixtures/collection\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/response/response-format-select-and-preview/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/response/response-format-select-and-preview/fixtures/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/response/response-format-select-and-preview/response-format-select-and-preview.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { closeAllCollections } from '../../utils/page';\nimport { buildCommonLocators } from '../../utils/page/locators';\nimport {\n  openRequest,\n  sendRequestAndWaitForResponse,\n  switchResponseFormat,\n  switchToPreviewTab,\n  switchToEditorTab\n} from '../../utils/page/actions';\n\ntest.describe.serial('Response Format Select and Preview', () => {\n  test.afterAll(async ({ pageWithUserData: page }) => {\n    // cleanup: close all collections\n    await closeAllCollections(page);\n  });\n\n  test('Verify Response Format Select and Preview features are rendering properly for JSON response', async ({ pageWithUserData: page }) => {\n    await openRequest(page, 'collection', 'request-json');\n    await sendRequestAndWaitForResponse(page);\n\n    const locators = buildCommonLocators(page);\n    const responseBody = locators.response.body();\n    const editorContainer = locators.response.editorContainer();\n    const responseFormatTab = locators.response.formatTab();\n    const codeLine = locators.response.codeLine();\n    const previewContainer = locators.response.previewContainer();\n\n    await test.step('Verify response pane and default JSON editor formatting', async () => {\n      await expect(responseBody).toBeVisible();\n      await expect(responseFormatTab).toHaveText('JSON');\n      await expect(codeLine.nth(1)).toContainText('\"hello\": \"bruno\"');\n    });\n\n    await test.step('Switch to Preview tab and check formatted object', async () => {\n      await switchToPreviewTab(page);\n      const jsonTreeLines = locators.response.jsonTreeLine();\n      await expect(jsonTreeLines.nth(1)).toContainText('\"hello\":\"bruno\"');\n    });\n\n    await test.step('Switch to Editor, select HTML, verify editor and preview', async () => {\n      await switchToEditorTab(page);\n      await switchResponseFormat(page, 'HTML');\n      await expect(codeLine.nth(1)).toContainText('\"hello\": \"bruno\"');\n      await switchToPreviewTab(page);\n      await expect(previewContainer.locator('webview')).toBeVisible();\n    });\n\n    await test.step('Switch to Editor, select XML, verify editor and preview error', async () => {\n      await switchToEditorTab(page);\n      await switchResponseFormat(page, 'XML');\n      await expect(codeLine.nth(1)).toContainText('\"hello\": \"bruno\"');\n      await switchToPreviewTab(page);\n      await expect(previewContainer).toContainText('Cannot preview as XML');\n    });\n\n    await test.step('Switch to Editor, select JavaScript, verify editor and preview fallback', async () => {\n      await switchToEditorTab(page);\n      await switchResponseFormat(page, 'JavaScript');\n      await expect(codeLine.nth(1)).toContainText('\"hello\": \"bruno\"');\n      await switchToPreviewTab(page);\n      await expect(previewContainer.locator('webview')).toBeVisible();\n    });\n\n    await test.step('Switch to Editor, select Raw, verify editor and preview', async () => {\n      await switchToEditorTab(page);\n      await switchResponseFormat(page, 'Raw');\n      await expect(codeLine.nth(1)).toContainText('\"hello\": \"bruno\"');\n      await switchToPreviewTab(page);\n      await expect(previewContainer).toContainText('{\"hello\":\"bruno\"}');\n    });\n\n    await test.step('Switch to Editor, select Hex, verify editor and preview', async () => {\n      await switchToEditorTab(page);\n      await switchResponseFormat(page, 'Hex');\n      await expect(editorContainer).toContainText('00000000: 7B 0A 20 20 22 68 65 6C 6C 6F 22 3A 20 22');\n      await switchToPreviewTab(page);\n      await expect(previewContainer).toContainText('{\"hello\":\"bruno\"}');\n    });\n\n    await test.step('Switch to Editor, select Base64, verify editor and preview', async () => {\n      await switchToEditorTab(page);\n      await switchResponseFormat(page, 'Base64');\n      await expect(editorContainer).toContainText('ewogICJoZWxsbyI6ICJicnVubyIKfQ==');\n      await switchToPreviewTab(page);\n      await expect(previewContainer).toContainText('{\"hello\":\"bruno\"}');\n    });\n  });\n\n  test('Verify Response Format Select and Preview features are rendering properly for HTML response', async ({ pageWithUserData: page }) => {\n    await openRequest(page, 'collection', 'request-html');\n    await sendRequestAndWaitForResponse(page);\n\n    const locators = buildCommonLocators(page);\n    const responseBody = locators.response.body();\n    const editorContainer = locators.response.editorContainer();\n    const responseFormatTab = locators.response.formatTab();\n    const codeLine = locators.response.codeLine();\n    const previewContainer = locators.response.previewContainer();\n\n    await test.step('Verify response pane and default HTML preview', async () => {\n      await expect(responseBody).toBeVisible();\n      await expect(previewContainer.locator('webview')).toBeVisible();\n    });\n\n    await test.step('Switch to Editor tab and check formatted HTML', async () => {\n      await expect(responseFormatTab).toHaveText('HTML');\n      await switchToEditorTab(page);\n      await expect(codeLine.first()).toContainText('<h1>hello</h1>');\n    });\n\n    await test.step('Select JSON, verify editor and preview', async () => {\n      await switchResponseFormat(page, 'JSON');\n      await expect(codeLine.first()).toContainText('<h1>hello</h1>');\n      await switchToPreviewTab(page);\n      await expect(previewContainer).toContainText('Cannot preview as JSON');\n    });\n\n    await test.step('Switch to Editor, select XML, verify editor and preview', async () => {\n      await switchToEditorTab(page);\n      await switchResponseFormat(page, 'XML');\n      await expect(codeLine.first()).toContainText('<h1>hello</h1>');\n      await switchToPreviewTab(page);\n      await expect(previewContainer).toContainText('h1');\n      await expect(previewContainer).toContainText(':');\n      await expect(previewContainer).toContainText('hello');\n    });\n\n    await test.step('Switch to Editor, select JavaScript, verify editor and preview fallback', async () => {\n      await switchToEditorTab(page);\n      await switchResponseFormat(page, 'JavaScript');\n      await expect(codeLine.first()).toContainText('<h1>hello</h1>');\n      await switchToPreviewTab(page);\n      await expect(previewContainer.locator('webview')).toBeVisible();\n    });\n\n    await test.step('Switch to Editor, select Raw, verify editor and preview', async () => {\n      await switchToEditorTab(page);\n      await switchResponseFormat(page, 'Raw');\n      await expect(codeLine.first()).toContainText('<h1>hello</h1>');\n      await switchToPreviewTab(page);\n      await expect(previewContainer).toContainText('<h1>hello</h1>');\n    });\n\n    await test.step('Switch to Editor, select Hex, verify editor and preview', async () => {\n      await switchToEditorTab(page);\n      await switchResponseFormat(page, 'Hex');\n      await expect(editorContainer).toContainText('00000000: 3C 68 31 3E 68 65 6C 6C 6F 3C 2F 68 31 3E');\n      await switchToPreviewTab(page);\n      await expect(previewContainer).toContainText('<h1>hello</h1>');\n    });\n\n    await test.step('Switch to Editor, select Base64, verify editor and preview', async () => {\n      await switchToEditorTab(page);\n      await switchResponseFormat(page, 'Base64');\n      await expect(editorContainer).toContainText('PGgxPmhlbGxvPC9oMT4=');\n      await switchToPreviewTab(page);\n      await expect(previewContainer).toContainText('<h1>hello</h1>');\n    });\n  });\n});\n"
  },
  {
    "path": "tests/response-examples/create-example.spec.ts",
    "content": "import { execSync } from 'child_process';\nimport { test, expect } from '../../playwright';\nimport path from 'path';\nimport { clickResponseAction } from '../utils/page/actions';\n\ntest.describe.serial('Create and Delete Response Examples', () => {\n  test.afterAll(async () => {\n    // Reset the collection request file to the original state\n    execSync(`git checkout -- ${path.join(__dirname, 'fixtures', 'collection', 'create-example.bru')}`);\n  });\n\n  test('should create a response example from response bookmark', async ({ pageWithUserData: page }) => {\n    await test.step('Open collection and request', async () => {\n      await page.locator('#sidebar-collection-name').filter({ hasText: 'collection' }).click();\n      await page.locator('.collection-item-name').filter({ hasText: 'create-example' }).click();\n    });\n\n    await test.step('Send request and validate example creation', async () => {\n      await page.getByTestId('send-arrow-icon').click();\n      // Wait for 30 seconds for the response bookmark button to be visible, on slower internet connections it may take longer to get the response.\n      await clickResponseAction(page, 'response-bookmark-btn');\n\n      await expect(page.getByText('Save Response as Example')).toBeVisible();\n      await expect(page.getByTestId('create-example-name-input')).toBeVisible();\n      await expect(page.getByTestId('create-example-description-input')).toBeVisible();\n\n      await page.getByTestId('create-example-name-input').clear();\n      await page.getByTestId('create-example-name-input').fill('Test Example from Bookmark');\n      await page.getByTestId('create-example-description-input').fill('This is a test example created from response bookmark');\n      await page.getByRole('button', { name: 'Create Example' }).click();\n      await expect(page.getByTestId('response-example-title')).toHaveText('create-example / Test Example from Bookmark');\n    });\n  });\n\n  test('Validate name is required to create example', async ({ pageWithUserData: page }) => {\n    await test.step('Open collection and request', async () => {\n      await page.locator('#sidebar-collection-name').getByText('collection').click();\n      await page.locator('.collection-item-name').getByText('create-example').click();\n    });\n\n    await test.step('Validate error when name is empty', async () => {\n      await page.getByTestId('send-arrow-icon').click();\n      await clickResponseAction(page, 'response-bookmark-btn');\n\n      await expect(page.getByRole('button', { name: 'Create Example' })).toBeEnabled();\n\n      // Clear the pre-filled name to test validation\n      await page.getByTestId('create-example-name-input').clear();\n      await page.getByRole('button', { name: 'Create Example' }).click();\n      await expect(page.getByTestId('name-error')).toBeVisible();\n      await expect(page.getByTestId('name-error')).toHaveText('Example name is required');\n\n      await page.getByTestId('create-example-name-input').clear();\n      await page.getByTestId('create-example-name-input').fill('New Required Name');\n      await expect(page.getByRole('button', { name: 'Create Example' })).toBeEnabled();\n      await page.getByRole('button', { name: 'Create Example' }).click();\n\n      // Modal should close and example should be created\n      await expect(page.getByText('Save Response as Example')).not.toBeVisible();\n    });\n  });\n\n  test('should close modal when cancelled', async ({ pageWithUserData: page }) => {\n    await test.step('Test modal cancellation', async () => {\n      await page.locator('.collection-item-name').getByText('create-example').click();\n      await page.getByTestId('send-arrow-icon').click();\n      await clickResponseAction(page, 'response-bookmark-btn');\n      await page.getByRole('button', { name: 'Cancel' }).click();\n      await expect(page.getByText('Save Response as Example')).not.toBeVisible();\n    });\n  });\n\n  test('should reset form when modal is reopened', async ({ pageWithUserData: page }) => {\n    await test.step('Open collection and request', async () => {\n      await page.locator('#sidebar-collection-name').getByText('collection').click();\n      await page.locator('.collection-item-name').getByText('create-example').click();\n    });\n\n    await test.step('Test form reset', async () => {\n      await page.locator('#send-request').getByRole('img').nth(2).click();\n      await clickResponseAction(page, 'response-bookmark-btn');\n\n      await page.getByTestId('create-example-name-input').fill('Test Name');\n      await page.getByTestId('create-example-description-input').fill('Test Description');\n      await page.getByRole('button', { name: 'Cancel' }).click();\n\n      await clickResponseAction(page, 'response-bookmark-btn');\n      // The name field should have the pre-filled default value\n      await expect(page.getByTestId('create-example-name-input')).toHaveValue('example');\n      // Description should still be empty\n      await expect(page.getByTestId('create-example-description-input')).toHaveValue('');\n      await page.getByRole('button', { name: 'Cancel' }).click();\n    });\n  });\n\n  test('should show created example in sidebar after expanding request', async ({ pageWithUserData: page }) => {\n    await test.step('Open collection and request', async () => {\n      await page.locator('#sidebar-collection-name').getByText('collection').click();\n      await page.locator('.collection-item-name').getByText('create-example').click();\n    });\n\n    await test.step('Create example and verify sidebar visibility', async () => {\n      await page.getByTestId('send-arrow-icon').click();\n      await clickResponseAction(page, 'response-bookmark-btn');\n\n      await page.getByTestId('create-example-name-input').clear();\n      await page.getByTestId('create-example-name-input').fill('Sidebar Test Example');\n      await page.getByTestId('create-example-description-input').fill('This example should appear in the sidebar');\n      await page.getByRole('button', { name: 'Create Example' }).click();\n      // Wait for modal to close\n      await page.waitForSelector('text=Save Response as Example', { state: 'detached' });\n    });\n\n    await test.step('Verify example appears in sidebar', async () => {\n      await page.locator('.collection-item-name', { hasText: 'create-example' }).getByTestId('request-item-chevron').click();\n      const exampleItem = page.locator('.collection-item-name').getByText('Sidebar Test Example', { exact: true });\n      await expect(exampleItem).toBeVisible();\n    });\n  });\n});\n"
  },
  {
    "path": "tests/response-examples/edit-example.spec.ts",
    "content": "import { test, expect } from '../../playwright';\nimport { execSync } from 'child_process';\nimport path from 'path';\nimport { clickResponseAction } from '../utils/page/actions';\n\ntest.describe.serial('Edit Response Examples', () => {\n  test.afterAll(async () => {\n    // Reset the collection request file to the original state\n    execSync(`git checkout -- ${path.join(__dirname, 'fixtures', 'collection', 'edit-example.bru')}`);\n  });\n\n  test('should enter edit mode and show editable fields when edit button is clicked', async ({ pageWithUserData: page }) => {\n    await test.step('Open collection and request', async () => {\n      await page.locator('#sidebar-collection-name').getByText('collection').click();\n      await page.locator('.collection-item-name').getByText('edit-example').click();\n    });\n\n    await test.step('Make a successful request and create an example', async () => {\n      await page.getByTestId('send-arrow-icon').click();\n      await clickResponseAction(page, 'response-bookmark-btn');\n      await page.getByTestId('create-example-name-input').clear();\n      await page.getByTestId('create-example-name-input').fill('Test Example');\n      await page.getByTestId('create-example-description-input').fill('This is a test example');\n      await page.getByRole('button', { name: 'Create Example' }).click();\n      // Wait for modal to close\n      await page.waitForSelector('text=Save Response as Example', { state: 'detached' });\n    });\n\n    await test.step('Open existing example', async () => {\n      await page.locator('.collection-item-name', { hasText: 'edit-example' }).getByTestId('request-item-chevron').click();\n      const exampleItem = page.locator('.collection-item-name').filter({ hasText: 'Test Example' });\n      await expect(exampleItem).toBeVisible();\n      await exampleItem.click();\n    });\n\n    await test.step('Verify edit mode functionality', async () => {\n      await expect(page.getByTestId('response-example-title')).toBeVisible();\n      await expect(page.getByTestId('response-example-edit-btn')).toBeVisible();\n      await page.getByTestId('response-example-edit-btn').click();\n      await expect(page.getByTestId('response-example-name-input')).toBeVisible();\n      await expect(page.getByTestId('response-example-description-input')).toBeVisible();\n      await expect(page.getByTestId('response-example-save-btn')).toBeVisible();\n      await expect(page.getByTestId('response-example-cancel-btn')).toBeVisible();\n    });\n  });\n\n  test('should successfully update example name and persist changes', async ({ pageWithUserData: page }) => {\n    await test.step('Open collection and request', async () => {\n      await page.locator('#sidebar-collection-name').getByText('collection').click();\n      await page.locator('.collection-item-name').getByText('edit-example').click();\n    });\n\n    await test.step('Create example to update', async () => {\n      await page.getByTestId('send-arrow-icon').click();\n      await clickResponseAction(page, 'response-bookmark-btn');\n      await page.getByTestId('create-example-name-input').clear();\n      await page.getByTestId('create-example-name-input').fill('Original Example Name');\n      await page.getByTestId('create-example-description-input').fill('Original description');\n      await page.getByRole('button', { name: 'Create Example' }).click();\n      // Wait for modal to close\n      await page.waitForSelector('text=Save Response as Example', { state: 'detached' });\n      const exampleItem = page.locator('.collection-item-name').getByText('Original Example Name', { exact: true });\n      await expect(exampleItem).toBeVisible();\n    });\n\n    await test.step('Open existing example', async () => {\n      const exampleItem = page.locator('.collection-item-name').getByText('Original Example Name', { exact: true });\n      await expect(exampleItem).toBeVisible();\n      await exampleItem.click();\n    });\n\n    await test.step('Update example name and verify persistence', async () => {\n      await page.getByTestId('response-example-edit-btn').click();\n      await page.getByTestId('response-example-name-input').clear();\n      await page.getByTestId('response-example-name-input').fill('Updated Example Name');\n      await page.getByTestId('response-example-save-btn').click();\n      await expect(page.getByTestId('response-example-title')).toHaveText('edit-example / Updated Example Name');\n    });\n  });\n\n  test('should successfully update example description and persist changes', async ({ pageWithUserData: page }) => {\n    await test.step('Open collection and request', async () => {\n      await page.locator('#sidebar-collection-name').getByText('collection').click();\n      await page.locator('.collection-item-name').getByText('edit-example').click();\n    });\n\n    await test.step('Create example to update description', async () => {\n      await page.getByTestId('send-arrow-icon').click();\n      await clickResponseAction(page, 'response-bookmark-btn');\n      await page.getByTestId('create-example-name-input').clear();\n      await page.getByTestId('create-example-name-input').fill('Description Test Example');\n      await page.getByTestId('create-example-description-input').fill('Original description');\n      await page.getByRole('button', { name: 'Create Example' }).click();\n      // Wait for modal to close\n      await page.waitForSelector('text=Save Response as Example', { state: 'detached' });\n      const exampleItem = page.locator('.collection-item-name').getByText('Description Test Example', { exact: true });\n      await expect(exampleItem).toBeVisible();\n    });\n\n    await test.step('Open existing example', async () => {\n      const exampleItem = page.locator('.collection-item-name').getByText('Description Test Example', { exact: true });\n      await expect(exampleItem).toBeVisible();\n      await exampleItem.click();\n    });\n\n    await test.step('Update example description and verify persistence', async () => {\n      await page.getByTestId('response-example-edit-btn').click();\n      await page.getByTestId('response-example-description-input').clear();\n      await page.getByTestId('response-example-description-input').fill('Updated description for the example');\n      await page.getByTestId('response-example-save-btn').click();\n      await expect(page.getByTestId('response-example-description')).toHaveText('Updated description for the example');\n    });\n  });\n\n  test('should discard changes and revert to original values when cancel is clicked', async ({ pageWithUserData: page }) => {\n    await test.step('Open collection and request', async () => {\n      await page.locator('#sidebar-collection-name').getByText('collection').click();\n      await page.locator('.collection-item-name').getByText('edit-example').click();\n    });\n\n    await test.step('Create example to test cancel functionality', async () => {\n      await page.getByTestId('send-arrow-icon').click();\n      await clickResponseAction(page, 'response-bookmark-btn');\n      await page.getByTestId('create-example-name-input').clear();\n      await page.getByTestId('create-example-name-input').fill('Cancel Test Example');\n      await page.getByTestId('create-example-description-input').fill('Original description for cancel test');\n      await page.getByRole('button', { name: 'Create Example' }).click();\n      // Wait for modal to close\n      await page.waitForSelector('text=Save Response as Example', { state: 'detached' });\n      const exampleItem = page.locator('.collection-item-name').getByText('Cancel Test Example', { exact: true });\n      await expect(exampleItem).toBeVisible();\n    });\n\n    await test.step('Open existing example', async () => {\n      const exampleItem = page.locator('.collection-item-name').getByText('Cancel Test Example', { exact: true });\n      await expect(exampleItem).toBeVisible();\n      await exampleItem.click();\n    });\n\n    await test.step('Test cancel functionality and verify reversion', async () => {\n      const originalName = await page.getByTestId('response-example-title').textContent();\n      await page.getByTestId('response-example-edit-btn').click();\n      await page.getByTestId('response-example-name-input').clear();\n      await page.getByTestId('response-example-name-input').fill('This should not be saved');\n      await page.getByTestId('response-example-cancel-btn').click();\n      await expect(page.getByTestId('response-example-title')).toHaveText(originalName!);\n    });\n  });\n\n  test('should save changes using keyboard shortcut (Cmd+S)', async ({ pageWithUserData: page }) => {\n    await test.step('Open collection and request', async () => {\n      await page.locator('#sidebar-collection-name').getByText('collection').click();\n      await page.locator('.collection-item-name').getByText('edit-example').click();\n    });\n\n    await test.step('Create example to test keyboard shortcut', async () => {\n      await page.getByTestId('send-arrow-icon').click();\n      await clickResponseAction(page, 'response-bookmark-btn');\n      await page.getByTestId('create-example-name-input').clear();\n      await page.getByTestId('create-example-name-input').fill('Keyboard Shortcut Test Example');\n      await page.getByTestId('create-example-description-input').fill('Original description for keyboard test');\n      await page.getByRole('button', { name: 'Create Example' }).click();\n      // Wait for modal to close\n      await page.waitForSelector('text=Save Response as Example', { state: 'detached' });\n      const exampleItem = page.locator('.collection-item-name').getByText('Keyboard Shortcut Test Example', { exact: true });\n      await expect(exampleItem).toBeVisible();\n    });\n\n    await test.step('Open existing example', async () => {\n      const exampleItem = page.locator('.collection-item-name').getByText('Keyboard Shortcut Test Example', { exact: true });\n      await expect(exampleItem).toBeVisible();\n      await exampleItem.click();\n    });\n\n    await test.step('Test keyboard shortcut save functionality', async () => {\n      await page.getByTestId('response-example-edit-btn').click();\n      await page.getByTestId('response-example-name-input').clear();\n      await page.getByTestId('response-example-name-input').fill('Keyboard Shortcut Test');\n      const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';\n      await page.keyboard.press(saveShortcut);\n      await expect(page.getByTestId('response-example-title')).toHaveText('edit-example / Keyboard Shortcut Test');\n    });\n  });\n});\n"
  },
  {
    "path": "tests/response-examples/fixtures/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"collection\",\n  \"type\": \"collection\"\n}\n"
  },
  {
    "path": "tests/response-examples/fixtures/collection/create-example.bru",
    "content": "meta {\n  name: create-example\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://testbench-sanity.usebruno.com/api/echo/json\n  body: json\n  auth: none\n}\n\nheaders {\n  Content-Type: application/json\n}\n\nbody:json {\n  {\n    \"message\": \"Hello World\",\n    \"timestamp\": \"{{$timestamp}}\"\n  }\n}\n"
  },
  {
    "path": "tests/response-examples/fixtures/collection/edit-example.bru",
    "content": "meta {\n  name: edit-example\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://testbench-sanity.usebruno.com/api/echo/json\n  body: json\n  auth: none\n}\n\nheaders {\n  Content-Type: application/json\n}\n\nbody:json {\n  {\n    \"message\": \"Hello World\",\n    \"timestamp\": \"{{$timestamp}}\"\n  }\n}\n"
  },
  {
    "path": "tests/response-examples/fixtures/collection/menu-operations.bru",
    "content": "meta {\n  name: menu-operations\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://testbench-sanity.usebruno.com/api/echo/json\n  body: json\n  auth: none\n}\n\nheaders {\n  Content-Type: application/json\n}\n\nbody:json {\n  {\n    \"message\": \"Hello World\",\n    \"timestamp\": \"{{$timestamp}}\"\n  }\n}\n"
  },
  {
    "path": "tests/response-examples/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{projectRoot}}/tests/response-examples/fixtures/collection\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "tests/response-examples/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/response-examples/fixtures/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/response-examples/menu-operations.spec.ts",
    "content": "import { test, expect } from '../../playwright';\nimport { execSync } from 'child_process';\nimport path from 'path';\nimport { clickResponseAction, sendRequest } from '../utils/page/actions';\n\ntest.describe.serial('Response Example Menu Operations', () => {\n  test.setTimeout(1 * 60 * 1000); // 1 minute for all tests in this describe block, default is 30 seconds.\n  test.afterAll(async () => {\n    // Reset the collection request file to the original state\n    execSync(`git checkout -- ${path.join(__dirname, 'fixtures', 'collection', 'menu-operations.bru')}`);\n  });\n\n  test('should clone a response example via three dots menu', async ({ pageWithUserData: page }) => {\n    await test.step('Open collection and request', async () => {\n      await page.locator('#sidebar-collection-name').getByText('collection').click();\n      await page.locator('.collection-item-name').getByText('menu-operations').click();\n    });\n\n    await test.step('Create example', async () => {\n      await sendRequest(page, 200);\n      await clickResponseAction(page, 'response-bookmark-btn');\n      await page.getByTestId('create-example-name-input').clear();\n      await page.getByTestId('create-example-name-input').fill('Example to Clone');\n      await page.getByRole('button', { name: 'Create Example' }).click();\n      // Wait for modal to close\n      await page.waitForSelector('text=Save Response as Example', { state: 'detached' });\n      await page.locator('.collection-item-name').filter({ hasText: 'menu-operations' }).getByTestId('request-item-chevron').click();\n\n      const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Clone' });\n      await expect(exampleRow).toBeVisible();\n    });\n\n    await test.step('Clone example', async () => {\n      const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Clone' });\n      await exampleRow.hover();\n      await exampleRow.locator('.menu-icon').click({ force: true });\n\n      await page.getByTestId('response-example-menu-clone').click();\n      const clonedExampleItem = page.locator('.collection-item-name').filter({ hasText: 'Example to Clone (Copy)' });\n      await expect(clonedExampleItem).toBeVisible();\n    });\n  });\n\n  test('should delete a response example via three dots menu', async ({ pageWithUserData: page }) => {\n    await test.step('Open collection and request', async () => {\n      await page.locator('#sidebar-collection-name').getByText('collection').click();\n      await page.locator('.collection-item-name').getByText('menu-operations').click();\n    });\n\n    await test.step('Create example to delete', async () => {\n      await sendRequest(page, 200);\n      await clickResponseAction(page, 'response-bookmark-btn');\n      await page.getByTestId('create-example-name-input').clear();\n      await page.getByTestId('create-example-name-input').fill('Example to Delete');\n      await page.getByTestId('create-example-description-input').fill('This example will be deleted');\n      await page.getByRole('button', { name: 'Create Example' }).click();\n      // Wait for modal to close\n      await page.waitForSelector('text=Save Response as Example', { state: 'detached' });\n\n      const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Delete' });\n      await expect(exampleRow).toBeVisible();\n    });\n\n    await test.step('Delete example', async () => {\n      const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Delete' });\n      await expect(exampleRow).toBeVisible();\n      await exampleRow.hover();\n      await exampleRow.locator('.menu-icon').click({ force: true });\n\n      await page.getByTestId('response-example-menu-delete').click();\n      await expect(page.getByText('Delete Example')).toBeVisible();\n      await page.getByRole('button', { name: 'Delete' }).click();\n      await expect(exampleRow).not.toBeVisible();\n    });\n  });\n\n  test('should rename a response example via three dots menu', async ({ pageWithUserData: page }) => {\n    await test.step('Open collection and request', async () => {\n      await page.locator('#sidebar-collection-name').getByText('collection').click();\n      await page.locator('.collection-item-name').getByText('menu-operations').click();\n    });\n\n    await test.step('Create example to rename', async () => {\n      await sendRequest(page, 200);\n      await clickResponseAction(page, 'response-bookmark-btn');\n      await page.getByTestId('create-example-name-input').clear();\n      await page.getByTestId('create-example-name-input').fill('Example to Rename');\n      await page.getByTestId('create-example-description-input').fill('This example will be renamed');\n      await page.getByRole('button', { name: 'Create Example' }).click();\n      // Wait for modal to close\n      await page.waitForSelector('text=Save Response as Example', { state: 'detached' });\n\n      const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Rename' });\n      await expect(exampleRow).toBeVisible();\n    });\n\n    await test.step('Rename example', async () => {\n      const exampleRow = page.locator('.collection-item-name').filter({ hasText: 'Example to Rename' });\n      await expect(exampleRow).toBeVisible();\n      await exampleRow.hover();\n      await exampleRow.locator('.menu-icon').click({ force: true });\n      await page.getByTestId('response-example-menu-rename').click();\n      await expect(page.getByText('Rename Example')).toBeVisible();\n      const renameExampleNameInput = page.getByTestId('rename-example-name-input');\n      await renameExampleNameInput.clear();\n      await renameExampleNameInput.fill('Renamed Example');\n      await page.getByRole('button', { name: 'Rename' }).click();\n      // Wait for modal to close\n      await page.waitForSelector('text=Rename Example', { state: 'detached' });\n      const updatedExampleRow = page.locator('.collection-item-name').filter({ hasText: 'Renamed Example' });\n      await expect(exampleRow).not.toBeVisible();\n      await expect(updatedExampleRow).toBeVisible();\n    });\n  });\n});\n"
  },
  {
    "path": "tests/runner/cli-env-combined/cli-env-combined.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { execSync } from 'child_process';\nimport fs from 'fs';\nimport path from 'path';\n\ntest.describe('CLI Combined Environment Support (--env and --env-file)', () => {\n  const collectionPath = path.resolve(__dirname, 'collection');\n  const BRU = 'node ../../../../packages/bruno-cli/bin/bru.js';\n\n  // Helper: run bru CLI and return exit code\n  const runFrom = (cwd: string, args: string): number => {\n    try {\n      execSync(`cd \"${cwd}\" && ${BRU} ${args}`, { stdio: 'pipe' });\n      return 0;\n    } catch (error: any) {\n      return error?.status ?? 1;\n    }\n  };\n\n  test('CLI: Should allow --env and --env-file to be used together', async () => {\n    const envFilePath = path.join(collectionPath, 'global-env.json');\n    const outputPath = path.join(collectionPath, 'combined-out.json');\n\n    // This should NOT error out anymore - both options should be accepted\n    runFrom(\n      collectionPath,\n      `run request.bru --env CollectionEnv --env-file \"${envFilePath}\" --reporter-json \"${outputPath}\"`\n    );\n\n    // Check that the output file was created (command ran successfully)\n    expect(fs.existsSync(outputPath)).toBe(true);\n\n    try {\n      fs.unlinkSync(outputPath);\n    } catch (_) {}\n  });\n\n  test('CLI: Collection env (--env) should override env-file variables', async () => {\n    const envFilePath = path.join(collectionPath, 'global-env.json');\n    const outputPath = path.join(collectionPath, 'override-out.json');\n\n    runFrom(\n      collectionPath,\n      `run request.bru --env CollectionEnv --env-file \"${envFilePath}\" --reporter-json \"${outputPath}\"`\n    );\n\n    expect(fs.existsSync(outputPath)).toBe(true);\n\n    const report = JSON.parse(fs.readFileSync(outputPath, 'utf8'));\n    const result = report.results[0];\n\n    // baseUrl should be from collection env (https://echo.usebruno.com), not global (https://global.example.com)\n    expect(result.request.url).toBe('https://echo.usebruno.com');\n\n    // overrideVar should be from collection env, not global\n    const body = JSON.parse(result.request.data);\n    expect(body.overrideVar).toBe('collection-value');\n\n    // globalOnly should come from env-file since it's not in collection env\n    expect(body.globalOnly).toBe('from-global');\n\n    // collectionOnly should come from collection env\n    expect(body.collectionOnly).toBe('from-collection');\n\n    try {\n      fs.unlinkSync(outputPath);\n    } catch (_) {}\n  });\n\n  test('CLI: --env-file only should still work', async () => {\n    const envFilePath = path.join(collectionPath, 'global-env.json');\n    const outputPath = path.join(collectionPath, 'envfile-only-out.json');\n\n    runFrom(collectionPath, `run request.bru --env-file \"${envFilePath}\" --reporter-json \"${outputPath}\"`);\n\n    expect(fs.existsSync(outputPath)).toBe(true);\n\n    const report = JSON.parse(fs.readFileSync(outputPath, 'utf8'));\n    const result = report.results[0];\n\n    // Should use env-file values when --env is not provided\n    // baseUrl would be from global-env.json but the request would fail since it's not a real URL\n    // We just verify the interpolation happened\n    expect(result.request.url).toBe('https://global.example.com');\n\n    try {\n      fs.unlinkSync(outputPath);\n    } catch (_) {}\n  });\n\n  test('CLI: --env only should still work', async () => {\n    const outputPath = path.join(collectionPath, 'env-only-out.json');\n\n    runFrom(collectionPath, `run request.bru --env CollectionEnv --reporter-json \"${outputPath}\"`);\n\n    expect(fs.existsSync(outputPath)).toBe(true);\n\n    const report = JSON.parse(fs.readFileSync(outputPath, 'utf8'));\n    const result = report.results[0];\n\n    // Should use collection env values\n    expect(result.request.url).toBe('https://echo.usebruno.com');\n\n    const body = JSON.parse(result.request.data);\n    expect(body.overrideVar).toBe('collection-value');\n    expect(body.collectionOnly).toBe('from-collection');\n    // globalOnly is not in collection env, so it should remain as template\n    expect(body.globalOnly).toBe('{{globalOnly}}');\n\n    try {\n      fs.unlinkSync(outputPath);\n    } catch (_) {}\n  });\n});\n"
  },
  {
    "path": "tests/runner/cli-env-combined/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"cli-env-combined-test\",\n  \"type\": \"collection\"\n}\n"
  },
  {
    "path": "tests/runner/cli-env-combined/collection/environments/CollectionEnv.bru",
    "content": "vars {\n  baseUrl: https://echo.usebruno.com\n  overrideVar: collection-value\n  collectionOnly: from-collection\n}\n"
  },
  {
    "path": "tests/runner/cli-env-combined/collection/global-env.json",
    "content": "{\n  \"name\": \"GlobalEnv\",\n  \"variables\": [\n    { \"name\": \"baseUrl\", \"value\": \"https://global.example.com\", \"enabled\": true },\n    { \"name\": \"overrideVar\", \"value\": \"global-value\", \"enabled\": true },\n    { \"name\": \"globalOnly\", \"value\": \"from-global\", \"enabled\": true }\n  ]\n}\n"
  },
  {
    "path": "tests/runner/cli-env-combined/collection/request.bru",
    "content": "meta {\n  name: combined-env-test\n  type: http\n}\n\nhttp {\n  method: POST\n  url: {{baseUrl}}\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"overrideVar\": \"{{overrideVar}}\",\n    \"globalOnly\": \"{{globalOnly}}\",\n    \"collectionOnly\": \"{{collectionOnly}}\"\n  }\n}\n"
  },
  {
    "path": "tests/runner/cli-json-env-file/cli-json-env-file.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { execSync } from 'child_process';\nimport fs from 'fs';\nimport path from 'path';\nimport constants from '../../../packages/bruno-cli/src/constants.js';\n\ntest.describe('CLI JSON Environment File Support', () => {\n  const collectionPath = path.resolve(__dirname, 'collection');\n  const BRU = 'node ../../../../packages/bruno-cli/bin/bru.js';\n\n  // Helper: emulate `bru run` from a given working directory and\n  // return the process exit code (0 on success). We use execSync so\n  // these tests behave like invoking the CLI directly in a shell.\n  const runFrom = (cwd: string, args: string): number => {\n    try {\n      execSync(`cd \"${cwd}\" && ${BRU} ${args}`, { stdio: 'pipe' });\n      return 0;\n    } catch (error: any) {\n      return error?.status ?? 1;\n    }\n  };\n\n  test('CLI: Run with invalid JSON environment file should fail', async () => {\n    // Create a temporary invalid JSON file\n    const tempDir = '/tmp/bruno-cli-test';\n    const invalidEnvPath = path.join(tempDir, 'invalid-env.json');\n\n    fs.mkdirSync(tempDir, { recursive: true });\n    fs.writeFileSync(invalidEnvPath,\n      JSON.stringify({\n        name: 'Invalid Env'\n        // missing variables array - invalid JSON\n      }));\n\n    const status = runFrom(collectionPath, `run --env-file \"${invalidEnvPath}\"`);\n    expect(status).toBe(constants.EXIT_STATUS.ERROR_INVALID_FILE);\n    try {\n      // Cleanup\n      fs.unlinkSync(invalidEnvPath);\n      fs.rmdirSync(tempDir);\n    } catch (e) {}\n  });\n\n  test('CLI: Run with valid JSON env and interpolates variables', async () => {\n    const envPath = path.join(collectionPath, 'env.json');\n    const outputPath = path.join(collectionPath, 'out.json');\n\n    // Even if exit is non-zero (network warnings), reporter should be written\n    runFrom(collectionPath, `run request.bru --env-file \"${envPath}\" --reporter-json \"${outputPath}\"`);\n\n    expect(fs.existsSync(outputPath)).toBe(true);\n    const report = JSON.parse(fs.readFileSync(outputPath, 'utf8'));\n    const result = report.results[0];\n    expect(result.request.url).toBe('https://echo.usebruno.com');\n    expect(result.response.status).toEqual(200);\n\n    try {\n      fs.unlinkSync(outputPath);\n    } catch (_) {}\n  });\n});\n"
  },
  {
    "path": "tests/runner/cli-json-env-file/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"cli-json-env-fixture\",\n  \"type\": \"collection\"\n}\n\n"
  },
  {
    "path": "tests/runner/cli-json-env-file/collection/env.json",
    "content": "{\n  \"name\": \"CLI JSON Env\",\n  \"variables\": [{ \"name\": \"baseUrl\", \"value\": \"https://echo.usebruno.com\", \"enabled\": true }]\n}\n"
  },
  {
    "path": "tests/runner/cli-json-env-file/collection/request.bru",
    "content": "meta {\n  name: cli-json-env-test\n  type: http\n}\n\nhttp {\n  method: POST\n  url: {{baseUrl}}\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"ping\": \"pong\"\n  }\n}"
  },
  {
    "path": "tests/runner/collection-run-report/collection/api/v1/posts.bru",
    "content": "meta {\n  name: Get UUID\n  type: http\n  seq: 2\n}\n\npost {\n  url: https://echo.usebruno.com\n  body: json\n  auth: none\n}\n\nheaders {\n  Accept: application/json\n}\n\nbody:json {\n  {\n    \"uuid\": \"49889366-72d2-4ba6-a0b7-94de29a04dc4\"\n  }\n}\n\ntests {\n  test(\"This test will fail\", function() {\n    expect(res.getStatus()).to.equal(404); // Intentional failure\n  });\n  \n  test(\"Status code is 200\", function() {\n    expect(res.getStatus()).to.equal(200);\n  });\n  \n  test(\"Response is an object\", function() {\n    expect(res.getBody()).to.be.an('object');\n  });\n  \n  test(\"Response has uuid property\", function() {\n    expect(res.getBody()).to.have.property('uuid');\n  });\n  \n  test(\"UUID is a string\", function() {\n    const body = res.getBody();\n    expect(body.uuid).to.be.a('string');\n  });\n}\n"
  },
  {
    "path": "tests/runner/collection-run-report/collection/api/v1/users.bru",
    "content": "meta {\n  name: Get User Info\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://echo.usebruno.com\n  body: json\n  auth: none\n}\n\nbody:json {\n  {\n    \"slideshow\": {\n      \"author\": \"Yours Truly\",\n      \"date\": \"date of publication\",\n      \"slides\": [\n        {\n          \"title\": \"Wake up to WonderWidgets!\",\n          \"type\": \"all\"\n        },\n        {\n          \"items\": [\n            \"Why <em>WonderWidgets</em> are great\",\n            \"Who <em>buys</em> WonderWidgets\"\n          ],\n          \"title\": \"Overview\",\n          \"type\": \"all\"\n        }\n      ],\n      \"title\": \"Sample Slide Show\"\n    }\n  }\n}\n\nheaders {\n  Accept: application/json\n}\n\ntests {\n  test(\"Status code is 200\", function() {\n    expect(res.getStatus()).to.equal(200);\n  });\n  \n  test(\"Response is an object\", function() {\n    expect(res.getBody()).to.be.an('object');\n  });\n  \n  test(\"Response has slideshow property\", function() {\n    expect(res.getBody()).to.have.property('slideshow');\n  });\n  \n  test(\"Slideshow has title\", function() {\n    const body = res.getBody();\n    expect(body.slideshow).to.have.property('title');\n  });\n}\n"
  },
  {
    "path": "tests/runner/collection-run-report/collection/auth/login.bru",
    "content": "meta {\n  name: Login Request\n  type: http\n  seq: 3\n}\n\npost {\n  url: https://echo.usebruno.com\n  body: json\n  auth: none\n}\n\nheaders {\n  Content-Type: application/json\n}\n\nbody:json {\n  {\n    \"args\": {},\n    \"data\": \"{\\n  \\\"username\\\": \\\"testuser\\\",\\n  \\\"password\\\": \\\"testpass\\\"\\n}\",\n    \"files\": {},\n    \"form\": {},\n    \"headers\": {\n      \"Accept\": \"application/json, text/plain, */*\",\n      \"Accept-Encoding\": \"gzip, compress, deflate, br\",\n      \"Content-Length\": \"54\",\n      \"Content-Type\": \"application/json\",\n      \"Host\": \"echo.usebruno.com\",\n      \"Request-Start-Time\": \"1762260235887\",\n      \"User-Agent\": \"bruno-runtime/1.99.3\",\n      \"X-Amzn-Trace-Id\": \"Root=1-6909f50d-3468da337d4402452b3503f4\"\n    },\n    \"json\": {\n      \"password\": \"testpass\",\n      \"username\": \"testuser\"\n    },\n    \"origin\": \"180.151.198.14\",\n    \"url\": \"https://echo.usebruno.com\"\n  }\n}\n\ntests {\n  test(\"Status code is 200\", function() {\n    expect(res.getStatus()).to.equal(200);\n  });\n  \n  test(\"Response has json field\", function() {\n    const response = res.getBody();\n    expect(response).to.have.property('json');\n  });\n  \n  test(\"Response json has username\", function() {\n    const response = res.getBody();\n    expect(response.json).to.have.property('username');\n  });\n}\n"
  },
  {
    "path": "tests/runner/collection-run-report/collection/auth/logout.bru",
    "content": "meta {\n  name: Logout Request\n  type: http\n  seq: 4\n}\n\npost {\n  url: https://echo.usebruno.com\n  body: json\n  auth: none\n}\n\nheaders {\n  Accept: application/json\n}\n\nbody:json {\n  {\n    \"args\": {},\n    \"data\": \"\",\n    \"files\": {},\n    \"form\": {},\n    \"headers\": {\n      \"Accept\": \"application/json\",\n      \"Accept-Encoding\": \"gzip, compress, deflate, br\",\n      \"Host\": \"echo.usebruno.com\",\n      \"Request-Start-Time\": \"1762260355402\",\n      \"User-Agent\": \"bruno-runtime/1.99.3\",\n      \"X-Amzn-Trace-Id\": \"Root=1-6909f585-57dba07b099d8b143524cc8a\"\n    },\n    \"json\": null,\n    \"origin\": \"180.151.198.14\",\n    \"url\": \"https://echo.usebruno.com\"\n  }\n}\n\ntests {\n  test(\"This test will also fail\", function() {\n    expect(res.getStatus()).to.equal(500); // Intentional failure\n  });\n  \n  test(\"Status code is 200\", function() {\n    expect(res.getStatus()).to.equal(200);\n  });\n}\n"
  },
  {
    "path": "tests/runner/collection-run-report/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"Report Test Collection\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}\n"
  },
  {
    "path": "tests/runner/collection-run-report/collection-run-report.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { execSync } from 'child_process';\nimport fs from 'fs';\nimport path from 'path';\n\nfunction normalizeJunitReport(xmlContent: string): string {\n  return xmlContent\n    // Replace timestamps with fixed value\n    .replace(/timestamp=\"[^\"]*\"/g, 'timestamp=\"2024-01-01T00:00:00.000\"')\n    // Replace hostnames with fixed value\n    .replace(/hostname=\"[^\"]*\"/g, 'hostname=\"test-host\"')\n    // Replace execution times with fixed value\n    .replace(/time=\"[^\"]*\"/g, 'time=\"0.100\"')\n    // Replace file paths with normalized path\n    .replace(/file=\"[^\"]*\\/[^\"]*\"/g, 'file=\"/mock/path/to/file.bru\"')\n    // Replace test paths with normalized path\n    .replace(/classname=\"[^\"]*\\/[^\"]*\"/g, 'classname=\"/test/path/collection\"');\n}\n\ntest.describe('Collection Run Report Tests', () => {\n  const collectionPath = path.join(__dirname, 'collection');\n\n  test('CLI: Run collection and generate JUnit report', async ({ createTmpDir }) => {\n    const outputDir = await createTmpDir('junit-report');\n    const junitOutputPath = path.join(outputDir, 'cli-report.xml');\n\n    // Run collection via CLI with JUnit reporter\n    const command = `cd \"${collectionPath}\" && node ../../../../packages/bruno-cli/bin/bru.js run --reporter-junit \"${junitOutputPath}\"`;\n\n    try {\n      execSync(command, { stdio: 'pipe' });\n    } catch (error) {\n      // CLI may exit with non-zero code if tests fail, which is expected\n      console.log('CLI execution completed with exit code:', error.status);\n    }\n\n    // Verify report was generated\n    expect(fs.existsSync(junitOutputPath)).toBe(true);\n    const junitReportContent = fs.readFileSync(junitOutputPath, 'utf8');\n    // Snapshot the normalized XML\n    const normalizedJunitReport = normalizeJunitReport(junitReportContent);\n    expect(normalizedJunitReport).toMatchSnapshot('cli-junit-report.xml');\n  });\n});\n"
  },
  {
    "path": "tests/runner/collection-run-report/collection-run-report.spec.ts-snapshots/cli-junit-report-default-darwin.xml",
    "content": "<?xml version=\"1.0\"?>\n<testsuites>\n  <testsuite name=\"Get User Info\" file=\"/mock/path/to/file.bru\" errors=\"0\" failures=\"0\" skipped=\"0\" tests=\"4\" timestamp=\"2024-01-01T00:00:00.000\" hostname=\"test-host\" time=\"0.100\">\n    <testcase name=\"Status code is 200\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n    <testcase name=\"Response is an object\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n    <testcase name=\"Response has slideshow property\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n    <testcase name=\"Slideshow has title\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n  </testsuite>\n  <testsuite name=\"Get UUID\" file=\"/mock/path/to/file.bru\" errors=\"0\" failures=\"1\" skipped=\"0\" tests=\"5\" timestamp=\"2024-01-01T00:00:00.000\" hostname=\"test-host\" time=\"0.100\">\n    <testcase name=\"This test will fail\" status=\"fail\" classname=\"/test/path/collection\" time=\"0.100\">\n      <failure type=\"failure\" message=\"expected 200 to equal 404\"/>\n    </testcase>\n    <testcase name=\"Status code is 200\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n    <testcase name=\"Response is an object\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n    <testcase name=\"Response has uuid property\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n    <testcase name=\"UUID is a string\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n  </testsuite>\n  <testsuite name=\"Login Request\" file=\"/mock/path/to/file.bru\" errors=\"0\" failures=\"0\" skipped=\"0\" tests=\"3\" timestamp=\"2024-01-01T00:00:00.000\" hostname=\"test-host\" time=\"0.100\">\n    <testcase name=\"Status code is 200\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n    <testcase name=\"Response has json field\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n    <testcase name=\"Response json has username\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n  </testsuite>\n  <testsuite name=\"Logout Request\" file=\"/mock/path/to/file.bru\" errors=\"0\" failures=\"1\" skipped=\"0\" tests=\"2\" timestamp=\"2024-01-01T00:00:00.000\" hostname=\"test-host\" time=\"0.100\">\n    <testcase name=\"This test will also fail\" status=\"fail\" classname=\"/test/path/collection\" time=\"0.100\">\n      <failure type=\"failure\" message=\"expected 200 to equal 500\"/>\n    </testcase>\n    <testcase name=\"Status code is 200\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n  </testsuite>\n</testsuites>"
  },
  {
    "path": "tests/runner/collection-run-report/collection-run-report.spec.ts-snapshots/cli-junit-report-default-linux.xml",
    "content": "<?xml version=\"1.0\"?>\n<testsuites>\n  <testsuite name=\"Get User Info\" file=\"/mock/path/to/file.bru\" errors=\"0\" failures=\"0\" skipped=\"0\" tests=\"4\" timestamp=\"2024-01-01T00:00:00.000\" hostname=\"test-host\" time=\"0.100\">\n    <testcase name=\"Status code is 200\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n    <testcase name=\"Response is an object\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n    <testcase name=\"Response has slideshow property\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n    <testcase name=\"Slideshow has title\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n  </testsuite>\n  <testsuite name=\"Get UUID\" file=\"/mock/path/to/file.bru\" errors=\"0\" failures=\"1\" skipped=\"0\" tests=\"5\" timestamp=\"2024-01-01T00:00:00.000\" hostname=\"test-host\" time=\"0.100\">\n    <testcase name=\"This test will fail\" status=\"fail\" classname=\"/test/path/collection\" time=\"0.100\">\n      <failure type=\"failure\" message=\"expected 200 to equal 404\"/>\n    </testcase>\n    <testcase name=\"Status code is 200\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n    <testcase name=\"Response is an object\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n    <testcase name=\"Response has uuid property\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n    <testcase name=\"UUID is a string\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n  </testsuite>\n  <testsuite name=\"Login Request\" file=\"/mock/path/to/file.bru\" errors=\"0\" failures=\"0\" skipped=\"0\" tests=\"3\" timestamp=\"2024-01-01T00:00:00.000\" hostname=\"test-host\" time=\"0.100\">\n    <testcase name=\"Status code is 200\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n    <testcase name=\"Response has json field\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n    <testcase name=\"Response json has username\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n  </testsuite>\n  <testsuite name=\"Logout Request\" file=\"/mock/path/to/file.bru\" errors=\"0\" failures=\"1\" skipped=\"0\" tests=\"2\" timestamp=\"2024-01-01T00:00:00.000\" hostname=\"test-host\" time=\"0.100\">\n    <testcase name=\"This test will also fail\" status=\"fail\" classname=\"/test/path/collection\" time=\"0.100\">\n      <failure type=\"failure\" message=\"expected 200 to equal 500\"/>\n    </testcase>\n    <testcase name=\"Status code is 200\" status=\"pass\" classname=\"/test/path/collection\" time=\"0.100\"/>\n  </testsuite>\n</testsuites>"
  },
  {
    "path": "tests/runner/collection-run.ts",
    "content": "import { test, expect } from '../../playwright';\nimport { setSandboxMode, runCollection, validateRunnerResults } from '../utils/page/index';\n\ntest.describe.parallel('Collection Run', () => {\n  test('Run bruno-testbench in Developer Mode', async ({ pageWithUserData: page }) => {\n    test.setTimeout(2 * 60 * 1000);\n\n    // Set up developer mode\n    await setSandboxMode(page, 'bruno-testbench', 'developer');\n\n    // Select environment\n    await page.locator('.environment-selector').nth(1).click();\n    await page.locator('.dropdown-item').getByText('Prod').click();\n\n    // Run the collection\n    await runCollection(page, 'bruno-testbench');\n\n    // Validate test results\n    await validateRunnerResults(page, {\n      failed: 0\n    });\n  });\n\n  test.fixme('Run bruno-testbench in Safe Mode', async ({ pageWithUserData: page }) => {\n    test.setTimeout(2 * 60 * 1000);\n\n    await page.getByText('bruno-testbench').click();\n    await page.locator('.environment-selector').nth(1).click();\n    await page.locator('.dropdown-item').getByText('Prod').click();\n    const collectionContainer = page.getByTestId('collections').locator('.collection-name').filter({ hasText: 'bruno-testbench' });\n    await collectionContainer.hover();\n    await collectionContainer.locator('.collection-actions .icon').waitFor({ state: 'visible' });\n    await collectionContainer.locator('.collection-actions .icon').click();\n    await page.getByText('Run', { exact: true }).click();\n    // Wait for the runner tab to open\n    // If there are existing results, reset first, otherwise wait for Run Collection button\n    const resetButton = page.getByRole('button', { name: 'Reset' });\n    const runCollectionButton = page.getByRole('button', { name: 'Run Collection' });\n\n    // Check if Reset button is visible (means there are existing results)\n    const resetVisible = await resetButton.isVisible().catch(() => false);\n    if (resetVisible) {\n      await resetButton.click();\n      // Wait a bit for the reset to complete\n      await page.waitForTimeout(500);\n    }\n\n    // Now wait for and click Run Collection button\n    await runCollectionButton.waitFor({ state: 'visible', timeout: 10000 });\n    await runCollectionButton.click();\n    await page.getByRole('button', { name: 'Run Again' }).waitFor({ timeout: 2 * 60 * 1000 });\n\n    // Parse and validate test results from filter buttons\n    const allButton = page.locator('button').filter({ hasText: /^All/ });\n    const passedButton = page.locator('button').filter({ hasText: /^Passed/ });\n    const failedButton = page.locator('button').filter({ hasText: /^Failed/ });\n    const skippedButton = page.locator('button').filter({ hasText: /^Skipped/ });\n\n    const totalRequests = parseInt(await allButton.locator('span').innerText());\n    const passed = parseInt(await passedButton.locator('span').innerText());\n    const failed = parseInt(await failedButton.locator('span').innerText());\n    const skipped = parseInt(await skippedButton.locator('span').innerText());\n\n    await expect(failed).toBe(0);\n    await expect(passed).toBe(totalRequests - skipped - failed);\n  });\n});\n"
  },
  {
    "path": "tests/runner/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\"{{projectRoot}}/packages/bruno-tests/collection\"],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/scratch-requests/scratch-requests.spec.ts",
    "content": "import { test, expect, Page } from '../../playwright';\nimport { fillRequestUrl, sendRequest, clickResponseAction, createCollection, closeAllCollections, closeAllTabs } from '../utils/page';\nimport { buildCommonLocators } from '../utils/page/locators';\n\ntest.describe.serial('Scratch Requests', () => {\n  let locators: ReturnType<typeof buildCommonLocators>;\n\n  test.beforeAll(async ({ page }) => {\n    locators = buildCommonLocators(page);\n\n    // Wait for the app to fully load\n    await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n  });\n\n  test.afterAll(async ({ page }) => {\n    // Close all tabs (including scratch requests) to avoid \"unsaved changes\" modal\n    await closeAllTabs(page);\n\n    // Clean up any regular collections\n    await closeAllCollections(page);\n  });\n\n  /**\n   * Helper to create a scratch request when on workspace overview\n   */\n  const createScratchRequest = async (page: Page, requestType: 'HTTP' | 'GraphQL' | 'gRPC' | 'WebSocket' = 'HTTP') => {\n    await test.step(`Create scratch ${requestType} request`, async () => {\n      // Click the + button to create a new request (this is on the workspace overview)\n      const createButton = page.getByRole('button', { name: 'New Transient Request' });\n      await createButton.waitFor({ state: 'visible', timeout: 5000 });\n\n      // Right-click to open the dropdown menu\n      await createButton.click({ button: 'right' });\n\n      // Wait for dropdown to be visible\n      await page.locator('.dropdown-item').first().waitFor({ state: 'visible' });\n\n      // Select the request type from dropdown\n      await page.locator('.dropdown-item').filter({ hasText: requestType }).click();\n\n      // Wait for the request tab to be active\n      await page.locator('.request-tab.active').waitFor({ state: 'visible' });\n      await expect(page.locator('.request-tab.active')).toContainText('Untitled');\n      await page.waitForTimeout(300);\n    });\n  };\n\n  /**\n   * Helper to navigate to workspace overview (home)\n   */\n  const goToWorkspaceOverview = async (page: Page) => {\n    await test.step('Navigate to workspace overview', async () => {\n      // Click the home icon in the title bar to go to workspace overview\n      const homeButton = page.locator('.titlebar-left .home-button');\n      await homeButton.click();\n      await page.waitForTimeout(300);\n    });\n  };\n\n  test('Create scratch HTTP request - should open in workspace tabs', async ({ page }) => {\n    await test.step('Navigate to workspace overview', async () => {\n      await goToWorkspaceOverview(page);\n    });\n\n    await test.step('Create scratch HTTP request', async () => {\n      await createScratchRequest(page, 'HTTP');\n      await fillRequestUrl(page, 'http://localhost:8081/ping');\n    });\n\n    await test.step('Verify HTTP request tab is open', async () => {\n      const activeTab = page.locator('.request-tab.active');\n      await expect(activeTab).toBeVisible();\n      await expect(activeTab).toContainText('Untitled');\n    });\n\n    await test.step('Verify collection header shows for scratch collection', async () => {\n      // Scratch requests should show the collection header with workspace name in the switcher\n      const collectionSwitcher = page.locator('.collection-switcher');\n      await expect(collectionSwitcher).toBeVisible();\n\n      // The switcher should display the workspace name (e.g., \"My Workspace\")\n      const switcherName = page.locator('.switcher-name');\n      await expect(switcherName).toBeVisible();\n    });\n  });\n\n  test('Create scratch GraphQL request', async ({ page }) => {\n    await test.step('Navigate to workspace overview', async () => {\n      await goToWorkspaceOverview(page);\n    });\n\n    await test.step('Create scratch GraphQL request', async () => {\n      await createScratchRequest(page, 'GraphQL');\n      await fillRequestUrl(page, 'https://api.example.com/graphql');\n    });\n\n    await test.step('Verify GraphQL request tab is open', async () => {\n      const activeTab = page.locator('.request-tab.active');\n      await expect(activeTab).toBeVisible();\n      await expect(activeTab).toContainText('Untitled');\n    });\n  });\n\n  test('Create scratch gRPC request', async ({ page }) => {\n    await test.step('Navigate to workspace overview', async () => {\n      await goToWorkspaceOverview(page);\n    });\n\n    await test.step('Create scratch gRPC request', async () => {\n      await createScratchRequest(page, 'gRPC');\n      await fillRequestUrl(page, 'grpc://localhost:50051');\n    });\n\n    await test.step('Verify gRPC request tab is open', async () => {\n      const activeTab = page.locator('.request-tab.active');\n      await expect(activeTab).toBeVisible();\n      await expect(activeTab).toContainText('Untitled');\n    });\n  });\n\n  test('Create scratch WebSocket request', async ({ page }) => {\n    await test.step('Navigate to workspace overview', async () => {\n      await goToWorkspaceOverview(page);\n    });\n\n    await test.step('Create scratch WebSocket request', async () => {\n      await createScratchRequest(page, 'WebSocket');\n      await fillRequestUrl(page, 'ws://localhost:8082');\n    });\n\n    await test.step('Verify WebSocket request tab is open', async () => {\n      const activeTab = page.locator('.request-tab.active');\n      await expect(activeTab).toBeVisible();\n      await expect(activeTab).toContainText('Untitled');\n    });\n  });\n\n  test('Send scratch HTTP request - verify response', async ({ page }) => {\n    await test.step('Navigate to workspace overview', async () => {\n      await goToWorkspaceOverview(page);\n    });\n\n    await test.step('Create scratch HTTP request', async () => {\n      await createScratchRequest(page, 'HTTP');\n      await fillRequestUrl(page, 'http://localhost:8081/ping');\n    });\n\n    await test.step('Send request and verify response', async () => {\n      await sendRequest(page, 200);\n\n      // Copy response to clipboard and verify\n      await clickResponseAction(page, 'response-copy-btn');\n      await expect(page.getByText('Response copied to clipboard')).toBeVisible();\n\n      const clipboardText = await page.evaluate(() => navigator.clipboard.readText());\n      expect(clipboardText).toBe('pong');\n    });\n  });\n\n  test('Save scratch request to a collection', async ({ page, createTmpDir }) => {\n    // Create a collection to save the scratch request to\n    const collectionPath = await createTmpDir('scratch-save-target');\n    await createCollection(page, 'scratch-save-test', collectionPath);\n\n    await test.step('Navigate to workspace overview', async () => {\n      await goToWorkspaceOverview(page);\n    });\n\n    await test.step('Create scratch HTTP request', async () => {\n      await createScratchRequest(page, 'HTTP');\n      await fillRequestUrl(page, 'http://localhost:8081/echo');\n    });\n\n    await test.step('Trigger save action using keyboard shortcut', async () => {\n      const saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';\n      await page.keyboard.press(saveShortcut);\n    });\n\n    await test.step('Fill in save dialog', async () => {\n      // Wait for save modal to appear - scratch requests show \"Select Collection\" first\n      const saveModal = page.locator('.bruno-modal-card');\n      await expect(saveModal).toBeVisible({ timeout: 5000 });\n\n      // Fill in request name\n      const requestNameInput = saveModal.locator('#request-name');\n      await requestNameInput.clear();\n      await requestNameInput.fill('Saved Scratch Request');\n\n      // Select the target collection from the list (this transitions from \"Select Collection\" to \"Save Request\")\n      const collectionSelector = saveModal.locator('.collection-item').filter({ hasText: 'scratch-save-test' });\n      await collectionSelector.click();\n\n      // Wait for the modal to transition to \"Save Request\" state (Save button becomes visible)\n      const saveButton = saveModal.getByRole('button', { name: 'Save' });\n      await expect(saveButton).toBeVisible({ timeout: 5000 });\n\n      // Click Save button\n      await saveButton.click();\n\n      // Wait for success toast\n      await expect(page.getByText('Request saved')).toBeVisible({ timeout: 5000 });\n    });\n\n    await test.step('Verify saved request appears in collection sidebar', async () => {\n      // Click on the collection to ensure it's expanded\n      await locators.sidebar.collection('scratch-save-test').click();\n\n      // Look for the saved request in sidebar\n      const savedRequest = locators.sidebar.request('Saved Scratch Request');\n      await expect(savedRequest).toBeVisible();\n    });\n  });\n\n  test('Multiple scratch requests maintain separate tabs', async ({ page }) => {\n    await test.step('Navigate to workspace overview', async () => {\n      await goToWorkspaceOverview(page);\n    });\n\n    await test.step('Create first scratch HTTP request', async () => {\n      await createScratchRequest(page, 'HTTP');\n      await fillRequestUrl(page, 'http://localhost:8081/ping');\n    });\n\n    await test.step('Create second scratch HTTP request', async () => {\n      await createScratchRequest(page, 'HTTP');\n      await fillRequestUrl(page, 'http://localhost:8081/echo');\n    });\n\n    await test.step('Verify both tabs exist', async () => {\n      const tabs = page.locator('.request-tab');\n      const tabCount = await tabs.count();\n      expect(tabCount).toBeGreaterThanOrEqual(2);\n\n      // Both should contain \"Untitled\" with different numbers\n      await expect(tabs.filter({ hasText: 'Untitled' }).first()).toBeVisible();\n    });\n  });\n});\n"
  },
  {
    "path": "tests/scripting/bru-api/isSafeMode/fixtures/collections/is-safe-mode-test/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"is-safe-mode-test\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}\n"
  },
  {
    "path": "tests/scripting/bru-api/isSafeMode/fixtures/collections/is-safe-mode-test/test-safe-mode-false.bru",
    "content": "meta {\n  name: test-safe-mode-false\n  type: http\n  seq: 2\n}\n\nget {\n  url: https://echo.usebruno.com\n  body: none\n  auth: none\n}\n\ntests {\n  test(\"bru.isSafeMode() returns false in developer mode\", function() {\n    expect(bru.isSafeMode()).to.be.false;\n  });\n}\n"
  },
  {
    "path": "tests/scripting/bru-api/isSafeMode/fixtures/collections/is-safe-mode-test/test-safe-mode-true.bru",
    "content": "meta {\n  name: test-safe-mode-true\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://echo.usebruno.com\n  body: none\n  auth: none\n}\n\ntests {\n  test(\"bru.isSafeMode() returns true in safe mode\", function() {\n    expect(bru.isSafeMode()).to.be.true;\n  });\n}\n"
  },
  {
    "path": "tests/scripting/bru-api/isSafeMode/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{collectionPath}}/is-safe-mode-test\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"developer\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/scripting/bru-api/isSafeMode/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{collectionPath}}/is-safe-mode-test\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/scripting/bru-api/isSafeMode/isSafeMode.spec.ts",
    "content": "import { test } from '../../../../playwright';\nimport { setSandboxMode, runCollection, validateRunnerResults } from '../../../utils/page';\n\ntest.describe.parallel('bru.isSafeMode() API', () => {\n  test('returns false when running in developer mode', async ({ pageWithUserData: page }) => {\n    // Set up developer mode\n    await setSandboxMode(page, 'is-safe-mode-test', 'developer');\n\n    // Run the collection\n    await runCollection(page, 'is-safe-mode-test');\n\n    // Validate test results\n    // In developer mode:\n    // - test-safe-mode-false should PASS (expects false, gets false)\n    // - test-safe-mode-true should FAIL (expects true, gets false)\n    await validateRunnerResults(page, {\n      totalRequests: 2,\n      passed: 1,\n      failed: 1,\n      skipped: 0\n    });\n  });\n\n  test('returns true when running in safe mode', async ({ pageWithUserData: page }) => {\n    // Set up safe mode\n    await setSandboxMode(page, 'is-safe-mode-test', 'safe');\n\n    // Run the collection\n    await runCollection(page, 'is-safe-mode-test');\n\n    // Validate test results\n    // In safe mode:\n    // - test-safe-mode-false should FAIL (expects false, gets true)\n    // - test-safe-mode-true should PASS (expects true, gets true)\n    await validateRunnerResults(page, {\n      totalRequests: 2,\n      passed: 1,\n      failed: 1,\n      skipped: 0\n    });\n  });\n});\n"
  },
  {
    "path": "tests/scripting/inbuilt-libraries/fs/fixtures/collections/should_allow_fs/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"should_allow_fs\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}"
  },
  {
    "path": "tests/scripting/inbuilt-libraries/fs/fixtures/collections/should_allow_fs/request.bru",
    "content": "meta {\n  name: request\n  type: http\n  seq: 1\n}\n\npost {\n  url: https://echo.usebruno.com\n  body: none\n  auth: none\n}\n\nscript:pre-request {\n  const fs = require('fs');\n}"
  },
  {
    "path": "tests/scripting/inbuilt-libraries/fs/fs.spec.ts",
    "content": "import { test } from '../../../../playwright';\nimport { setSandboxMode, runCollection, validateRunnerResults } from '../../../utils/page';\n\ntest.describe.serial('`fs` library', () => {\n  test('developer mode allows fs', async ({ pageWithUserData: page }) => {\n    test.setTimeout(2 * 60 * 1000);\n\n    // Set up developer mode\n    await setSandboxMode(page, 'should_allow_fs', 'developer');\n\n    // Run the collection\n    await runCollection(page, 'should_allow_fs');\n\n    // Validate test results\n    await validateRunnerResults(page, {\n      totalRequests: 1,\n      passed: 1,\n      failed: 0,\n      skipped: 0\n    });\n  });\n\n  test('safe mode blocks fs', async ({ pageWithUserData: page }) => {\n    test.setTimeout(2 * 60 * 1000);\n\n    // Set up safe mode\n    await setSandboxMode(page, 'should_allow_fs', 'safe');\n\n    // Run the collection\n    await runCollection(page, 'should_allow_fs');\n\n    // Validate test results\n    await validateRunnerResults(page, {\n      totalRequests: 1,\n      passed: 0,\n      failed: 1,\n      skipped: 0\n    });\n  });\n});\n"
  },
  {
    "path": "tests/scripting/inbuilt-libraries/fs/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/scripting/inbuilt-libraries/fs/fixtures/collections/should_allow_fs\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/scripting/inbuilt-libraries/jsonwebtoken/fixtures/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"jsonwebtoken\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}"
  },
  {
    "path": "tests/scripting/inbuilt-libraries/jsonwebtoken/fixtures/collection/decode/decode.bru",
    "content": "meta {\n  name: decode\n  type: http\n  seq: 1\n}\n\npost {\n  url: {{host}}/api/echo\n  body: none\n  auth: inherit\n}\n\nscript:pre-request {\n  const jwt = require('jsonwebtoken');\n  \n  const testPayload = {\n    userId: 456,\n    username: 'decodeuser',\n    role: 'user',\n    iat: Math.floor(Date.now() / 1000)\n  };\n  \n  const secret = bru.getEnvVar('secret') || 'test-secret-key';\n  const testToken = jwt.sign(testPayload, secret, { algorithm: 'HS256', expiresIn: '1h' });\n  \n  try {\n    console.log('Testing JWT decoding...');\n    console.log('Test token:', testToken);\n    \n    const decoded = jwt.decode(testToken);\n    \n    bru.setEnvVar('decoded_payload', JSON.stringify(decoded));\n    \n  } catch (error) {\n    console.error('JWT decoding failed:', error.message);\n    throw error; \n  }\n}\n\ntests {\n  test(\"Decoded payload should exist\", function() {\n    const decodedPayload = bru.getEnvVar('decoded_payload');\n    expect(decodedPayload).to.exist;\n  });\n  \n  test(\"Decoded payload should contain correct user data\", function() {\n    const decodedPayload = JSON.parse(bru.getEnvVar('decoded_payload'));\n    \n    expect(decodedPayload.userId).to.equal(456);\n    expect(decodedPayload.username).to.equal('decodeuser');\n    expect(decodedPayload.role).to.equal('user');\n  });\n  \n  test(\"Decoded payload should have timestamp fields\", function() {\n    const decodedPayload = JSON.parse(bru.getEnvVar('decoded_payload'));\n    \n    expect(decodedPayload.iat).to.exist;\n    expect(decodedPayload.exp).to.exist;\n    expect(typeof decodedPayload.iat).to.equal('number');\n    expect(typeof decodedPayload.exp).to.equal('number');\n  });\n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "tests/scripting/inbuilt-libraries/jsonwebtoken/fixtures/collection/decode/folder.bru",
    "content": "meta {\n  name: decode\n  seq: 3\n}\n\nauth {\n  mode: inherit\n}\n"
  },
  {
    "path": "tests/scripting/inbuilt-libraries/jsonwebtoken/fixtures/collection/environments/Prod.bru",
    "content": "vars {\n  host: http://httpfaker.org\n  secret: my-secret-key\n}\n"
  },
  {
    "path": "tests/scripting/inbuilt-libraries/jsonwebtoken/fixtures/collection/sign/folder.bru",
    "content": "meta {\n  name: sign\n  seq: 1\n}\n\nauth {\n  mode: inherit\n}\n"
  },
  {
    "path": "tests/scripting/inbuilt-libraries/jsonwebtoken/fixtures/collection/sign/sign with callback err.bru",
    "content": "meta {\n  name: sign with callback err\n  type: http\n  seq: 2\n}\n\npost {\n  url: {{host}}/api/echo\n  body: none\n  auth: inherit\n}\n\ntests {\n  const jwt = require('jsonwebtoken');\n  \n  const HS_SECRET = 'supersecret';\n  \n  /**\n   * Helper that calls jwt.sign **with a callback** and resolves/rejects\n   * based on the callback's (err, token) — so tests can `await` it.\n   */\n  function signViaCallback(payload, secret, options = {}) {\n    return new Promise((resolve, reject) => {\n      jwt.sign(payload, secret, options, (err, token) => {\n        if (err) return reject(err);       \n        resolve(token);                   \n      });\n    });\n  }\n  \n  /* ============================================================\n     ERROR TESTS — jwt.sign should call callback with `err`\n     ============================================================ */\n  \n  test('ERROR (callback) — missing secret for HS256', async function () {\n    try {\n      await signViaCallback({ sub: 'no_secret' }, undefined, { algorithm: 'HS256' });\n      throw new Error('Expected jwt.sign to error via callback');\n    } catch (err) {\n      expect(err).to.be.instanceOf(Error);\n      expect(String(err.message)).to.match(/secret|private key must have a value/i);\n    }\n  });\n  \n  test('ERROR (callback) — invalid expiresIn format', async function () {\n    try {\n      await signViaCallback({ sub: 'bad_exp' }, HS_SECRET, { expiresIn: 'not-a-time' });\n      throw new Error('Expected jwt.sign to error via callback');\n    } catch (err) {\n      expect(err).to.be.instanceOf(Error);\n      expect(String(err.message)).to.match(/expiresIn/i);\n    }\n  });\n  \n  test('ERROR (callback) — unsupported/invalid algorithm', async function () {\n    try {\n      await signViaCallback({ sub: 'bad_alg' }, HS_SECRET, { algorithm: 'FOO256' });\n      throw new Error('Expected jwt.sign to error via callback');\n    } catch (err) {\n      expect(err).to.be.instanceOf(Error);\n      expect(String(err.message)).to.match(/algorithm/i);\n    }\n  });\n  \n  test('CONTROL (callback) — succeeds when options are valid', async function () {\n    const token = await jwt.sign({ sub: 'ok' }, HS_SECRET, { algorithm: 'HS256', expiresIn: '10m' });\n    expect(token).to.be.a('string');\n  });\n  \n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "tests/scripting/inbuilt-libraries/jsonwebtoken/fixtures/collection/sign/sign with callback token.bru",
    "content": "meta {\n  name: sign with callback token\n  type: http\n  seq: 3\n}\n\npost {\n  url: {{host}}/api/echo\n  body: none\n  auth: inherit\n}\n\ntests {\n  const jwt = require('jsonwebtoken');\n  const HS_SECRET = 'supersecret';\n  \n  const payload = { sub: 'user123' };\n  \n  function once(fn) {\n    let called = false;\n    return (...args) => {\n      if (!called) {\n        called = true;\n        fn(...args);\n      }\n    };\n  }\n  \n  function signAsync(payload, secret, options = {}) {\n    return new Promise((resolve, reject) => {\n      jwt.sign(payload, secret, options, (err, token) => {\n        if (err) reject(err);\n        else resolve(token);\n      });\n    });\n  }\n  \n  // ------------------------------------------------------------\n  // 1. Named Normal Callback\n  // ------------------------------------------------------------\n  test('sign — named normal callback', function () {\n    function signCallback(err, token) {\n      expect(err).to.be.null;\n      expect(token).to.be.a('string');\n  \n      // Verify token to ensure correctness\n      const decoded = jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] });\n      expect(decoded.sub).to.equal('user123');\n  \n      console.log('Named callback signed token:', token);\n    }\n  \n    jwt.sign(payload, HS_SECRET, { algorithm: 'HS256', expiresIn: '15m' }, signCallback);\n  });\n  \n  // ------------------------------------------------------------\n  // 2. Anonymous Callback\n  // ------------------------------------------------------------\n  test('sign — anonymous callback', function () {\n    jwt.sign(payload, HS_SECRET, { algorithm: 'HS256' }, function (err, token) {\n      expect(err).to.be.null;\n      expect(token).to.be.a('string');\n  \n      const decoded = jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] });\n      expect(decoded.sub).to.equal('user123');\n  \n      console.log('Anonymous callback signed token:', token);\n    });\n  });\n  \n  // ------------------------------------------------------------\n  // 3. Arrow Function Callback\n  // ------------------------------------------------------------\n  test('sign — arrow function callback', function () {\n    jwt.sign(payload, HS_SECRET, { algorithm: 'HS256' }, (err, token) => {\n      expect(err).to.be.null;\n      expect(token).to.be.a('string');\n  \n      const decoded = jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] });\n      expect(decoded.sub).to.equal('user123');\n  \n      console.log('Arrow callback signed token:', token);\n    });\n  });\n  \n  // ------------------------------------------------------------\n  // 4. Bound Method Callback\n  // ------------------------------------------------------------\n  test('sign — bound method callback', function () {\n    const signer = {\n      prefix: '[SIGN]',\n      done(err, token) {\n        expect(err).to.be.null;\n        expect(token).to.be.a('string');\n  \n        const decoded = jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] });\n        expect(decoded.sub).to.equal('user123');\n  \n        console.log(this.prefix, 'Bound callback signed token:', token);\n      },\n    };\n  \n    jwt.sign(payload, HS_SECRET, { algorithm: 'HS256' }, signer.done.bind(signer));\n  });\n  \n  // ------------------------------------------------------------\n  // 5. Higher-Order Callback\n  // ------------------------------------------------------------\n  function makeSignCallback(label) {\n    return (err, token) => {\n      expect(err).to.be.null;\n      expect(token).to.be.a('string');\n  \n      const decoded = jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] });\n      expect(decoded.sub).to.equal('user123');\n  \n      console.log(label, 'Higher-order callback signed token:', token);\n    };\n  }\n  \n  test('sign — higher-order callback', function () {\n    const cb = makeSignCallback('[CUSTOM LABEL]');\n    jwt.sign(payload, HS_SECRET, { algorithm: 'HS256' }, cb);\n  });\n  \n  // ------------------------------------------------------------\n  // 6. Once-Wrapped Callback\n  // ------------------------------------------------------------\n  test('sign — once-wrapped callback', function () {\n    const cb = once((err, token) => {\n      expect(err).to.be.null;\n      expect(token).to.be.a('string');\n  \n      const decoded = jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] });\n      expect(decoded.sub).to.equal('user123');\n  \n      console.log('Once callback executed and signed token:', token);\n    });\n  \n    jwt.sign(payload, HS_SECRET, { algorithm: 'HS256' }, cb);\n  });\n  \n  // ------------------------------------------------------------\n  // 7. Promise / Async-Await\n  // ------------------------------------------------------------\n  test('sign — promise wrapper with async/await', async function () {\n    const token = await signAsync(payload, HS_SECRET, { algorithm: 'HS256', expiresIn: '15m' });\n    expect(token).to.be.a('string');\n  \n    const decoded = jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] });\n    expect(decoded.sub).to.equal('user123');\n  \n    console.log('Promise/async signed token:', token);\n  });\n  \n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "tests/scripting/inbuilt-libraries/jsonwebtoken/fixtures/collection/sign/sign.bru",
    "content": "meta {\n  name: sign\n  type: http\n  seq: 1\n}\n\npost {\n  url: {{host}}/api/echo\n  body: none\n  auth: inherit\n}\n\nscript:pre-request {\n  const jwt = require('jsonwebtoken');\n  \n  const payload = {\n    userId: 123,\n    username: 'testuser',\n    role: 'admin',\n    iat: Math.floor(Date.now() / 1000)\n  };\n  \n  const secret = bru.getEnvVar('secret');\n  \n  const options = {\n    algorithm: 'HS256',\n    expiresIn: '1h'\n  };\n  \n  try {\n    console.log('Testing JWT encoding...');\n    const token = jwt.sign(payload, secret, options);\n    \n    console.log('JWT Token encoded successfully:', token);\n    \n    bru.setEnvVar('jwt_token', token);\n    \n    bru.setEnvVar('original_payload', JSON.stringify(payload));\n    \n    console.log('JWT encoding test passed!');\n    \n  } catch (error) {\n    console.error('JWT encoding failed:', error.message);\n    throw error;\n  }\n}\n\ntests {\n  const atob = require('atob')\n  \n  test(\"JWT token should be generated\", function() {\n    const jwtToken = bru.getEnvVar('jwt_token');\n    expect(jwtToken).to.exist;\n  });\n  \n  test(\"JWT token should be a string\", function() {\n    const jwtToken = bru.getEnvVar('jwt_token');\n    expect(typeof jwtToken).to.equal('string');\n  });\n  \n  test(\"JWT token should have 3 parts (header.payload.signature)\", function() {\n    const jwtToken = bru.getEnvVar('jwt_token');\n    const parts = jwtToken.split('.');\n    expect(parts.length).to.equal(3);\n  });\n  \n  test(\"JWT token should be valid base64\", function() {\n    const jwtToken = bru.getEnvVar('jwt_token');\n    const parts = jwtToken.split('.');\n    \n    // Test that each part is valid base64\n    parts.forEach((part, index) => {\n      try {\n        atob(part);\n      } catch (e) {\n        throw new Error(`JWT part ${index + 1} is not valid base64`);\n      }\n    });\n  });\n  \n  test(\"JWT token should contain expected payload data\", function() {\n    const jwtToken = bru.getEnvVar('jwt_token');\n    const originalPayload = JSON.parse(bru.getEnvVar('original_payload'));\n    \n    // Decode the payload part (second part of JWT)\n    const parts = jwtToken.split('.');\n    const payloadPart = parts[1];\n    const decodedPayload = JSON.parse(atob(payloadPart));\n    console.log(decodedPayload)\n    \n    expect(decodedPayload.userId).to.equal(originalPayload.userId);\n    expect(decodedPayload.username).to.equal(originalPayload.username);\n    expect(decodedPayload.role).to.equal(originalPayload.role);\n  });\n  \n  test(\"JWT token should have proper header\", function() {\n    const jwtToken = bru.getEnvVar('jwt_token');\n    const parts = jwtToken.split('.');\n    const headerPart = parts[0];\n    const header = JSON.parse(atob(headerPart));\n    \n    expect(header.alg).to.equal('HS256');\n    expect(header.typ).to.equal('JWT');\n  });\n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "tests/scripting/inbuilt-libraries/jsonwebtoken/fixtures/collection/verify/folder.bru",
    "content": "meta {\n  name: verify\n  seq: 2\n}\n\nauth {\n  mode: inherit\n}\n"
  },
  {
    "path": "tests/scripting/inbuilt-libraries/jsonwebtoken/fixtures/collection/verify/verify with callback err.bru",
    "content": "meta {\n  name: verify with callback err\n  type: http\n  seq: 2\n}\n\npost {\n  url: {{host}}/api/echo\n  body: none\n  auth: inherit\n}\n\ntests {\n  const jwt = require('jsonwebtoken');\n  \n  const HS_SECRET = 'supersecret';\n  \n  function verifyViaCallback(token, secret, options = {}) {\n    return new Promise((resolve, reject) => {\n      jwt.verify(token, secret, options, (err, decoded) => {\n        if (err) return reject(err); \n        resolve(decoded);\n      });\n    });\n  }\n  \n  function createValidToken(payload = { sub: 'user123' }, secret = HS_SECRET) {\n    return jwt.sign(payload, secret, { algorithm: 'HS256', expiresIn: '1h' });\n  }\n  \n  /* ============================================================\n     ERROR TESTS — jwt.verify should call callback with `err`\n     ============================================================ */\n  \n  test('ERROR (callback) — malformed token', async function () {\n    const malformedToken = 'abc.def'; // not a valid JWT\n    try {\n      await verifyViaCallback(malformedToken, HS_SECRET, { algorithms: ['HS256'] });\n      throw new Error('Expected jwt.verify to error via callback');\n    } catch (err) {\n      expect(err).to.be.instanceOf(Error);\n      expect(String(err.message)).to.match(/jwt malformed|invalid token/i);\n    }\n  });\n  \n  test('ERROR (callback) — invalid signature (wrong secret)', async function () {\n    const token = createValidToken(); // signed with HS_SECRET\n    try {\n      await verifyViaCallback(token, 'wrong_secret', { algorithms: ['HS256'] });\n      throw new Error('Expected jwt.verify to error via callback');\n    } catch (err) {\n      expect(err).to.be.instanceOf(Error);\n      expect(String(err.message)).to.match(/invalid signature/i);\n    }\n  });\n  \n  test('ERROR (callback) — invalid algorithm', async function () {\n    const token = createValidToken();\n    try {\n      // Pass unsupported algorithm intentionally\n      await verifyViaCallback(token, HS_SECRET, { algorithms: ['RS256'] });\n      throw new Error('Expected jwt.verify to error due to invalid algorithm');\n    } catch (err) {\n      expect(err).to.be.instanceOf(Error);\n      expect(String(err.message)).to.match(/invalid algorithm/i);\n    }\n  });\n  \n  test('ERROR (callback) — missing secret', async function () {\n    const token = createValidToken();\n    try {\n      await verifyViaCallback(token, undefined, { algorithms: ['HS256'] });\n      throw new Error('Expected jwt.verify to error due to missing secret');\n    } catch (err) {\n      expect(err).to.be.instanceOf(Error);\n      expect(String(err.message)).to.match(/secret|key must be provided/i);\n    }\n  });\n  \n  \n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "tests/scripting/inbuilt-libraries/jsonwebtoken/fixtures/collection/verify/verify with callback token.bru",
    "content": "meta {\n  name: verify with callback token\n  type: http\n  seq: 3\n}\n\npost {\n  url: {{host}}/api/echo\n  body: none\n  auth: inherit\n}\n\ntests {\n  const jwt = require('jsonwebtoken');\n  \n  const HS_SECRET = 'supersecret';\n  \n  const token = jwt.sign({ sub: 'user123' }, HS_SECRET, {\n    algorithm: 'HS256',\n    expiresIn: '15m',\n  });\n  \n  function once(fn) {\n    let called = false;\n    return (...args) => {\n      if (!called) {\n        called = true;\n        fn(...args);\n      }\n    };\n  }\n  \n  function verifyAsync(token, secret, options = {}) {\n    return new Promise((resolve, reject) => {\n      jwt.verify(token, secret, options, (err, decoded) => {\n        if (err) reject(err);\n        else resolve(decoded);\n      });\n    });\n  }\n  \n  test('verify — named normal callback', function () {\n    function verifyCallback(err, decoded) {\n      expect(err).to.be.null;\n      expect(decoded.sub).to.equal('user123');\n      console.log('Named callback verified user:', decoded.sub);\n    }\n  \n    jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] }, verifyCallback);\n  });\n  \n  test('verify — anonymous callback', function () {\n    jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] }, function (err, decoded) {\n      expect(err).to.be.null;\n      expect(decoded.sub).to.equal('user123');\n      console.log('Anonymous callback verified user:', decoded.sub);\n    });\n  });\n  \n  test('verify — arrow function callback', function () {\n    jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] }, (err, decoded) => {\n      expect(err).to.be.null;\n      expect(decoded.sub).to.equal('user123');\n      console.log('Arrow callback verified user:', decoded.sub);\n    });\n  });\n  \n  test('verify — bound method callback', function () {\n    const handler = {\n      prefix: '[VERIFY]',\n      done(err, decoded) {\n        expect(err).to.be.null;\n        expect(decoded.sub).to.equal('user123');\n        console.log(this.prefix, 'Bound callback verified user:', decoded.sub);\n      },\n    };\n  \n    jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] }, handler.done.bind(handler));\n  });\n  \n  function makeVerifyCallback(label) {\n    return (err, decoded) => {\n      expect(err).to.be.null;\n      expect(decoded.sub).to.equal('user123');\n      console.log(label, 'Higher-order callback verified user:', decoded.sub);\n    };\n  }\n  \n  test('verify — higher-order callback', function () {\n    const cb = makeVerifyCallback('[CUSTOM LABEL]');\n    jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] }, cb);\n  });\n  \n  test('verify — once-wrapped callback', function () {\n    const cb = once((err, decoded) => {\n      expect(err).to.be.null;\n      expect(decoded.sub).to.equal('user123');\n      console.log('Once callback executed and verified user:', decoded.sub);\n    });\n  \n    jwt.verify(token, HS_SECRET, { algorithms: ['HS256'] }, cb);\n  });\n  \n  test('verify — promise wrapper with async/await', async function () {\n    const decoded = await verifyAsync(token, HS_SECRET, { algorithms: ['HS256'] });\n    expect(decoded.sub).to.equal('user123');\n    console.log('Promise/async verified user:', decoded.sub);\n  });\n  \n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "tests/scripting/inbuilt-libraries/jsonwebtoken/fixtures/collection/verify/verify.bru",
    "content": "meta {\n  name: verify\n  type: http\n  seq: 1\n}\n\npost {\n  url: {{host}}/api/echo\n  body: none\n  auth: inherit\n}\n\nscript:pre-request {\n  const jwt = require('jsonwebtoken');\n  \n  const validPayload = {\n    userId: 789,\n    username: 'verifyuser',\n    role: 'admin',\n    iat: Math.floor(Date.now() / 1000)\n  };\n  \n  const secret = bru.getEnvVar('secret') || 'test-secret-key';\n  const wrongSecret = 'wrong-secret-key';\n  \n  const validToken = jwt.sign(validPayload, secret, { algorithm: 'HS256', expiresIn: '1h' });\n  const invalidToken = jwt.sign(validPayload, wrongSecret, { algorithm: 'HS256', expiresIn: '1h' });\n   \n  \n  bru.setEnvVar('valid_token', validToken);\n  bru.setEnvVar('invalid_token', invalidToken);\n  \n  try {\n    console.log('Testing JWT verification...');\n    console.log('Valid token:', validToken);\n    \n    const verified = jwt.verify(validToken, secret);\n  \n    const verifiedWithOptions = jwt.verify(validToken, secret, { \n      algorithms: ['HS256'],\n      ignoreExpiration: false \n    });\n    if (!verifiedWithOptions) {\n      throw new Error('Verification with options should work');\n    }\n    \n    console.log('JWT verification test passed!');\n  \n    bru.setEnvVar('verified_payload', JSON.stringify(verified));\n    \n  } catch (error) {\n    console.error('JWT verification failed:', error.message);\n    throw error;\n  }\n}\n\ntests {\n  test(\"Verified payload should exist\", function() {\n    const verifiedPayload = bru.getEnvVar('verified_payload');\n    expect(verifiedPayload).to.exist;\n  });\n  \n  test(\"Verified payload should be valid JSON\", function() {\n    const verifiedPayload = bru.getEnvVar('verified_payload');\n    const parsed = JSON.parse(verifiedPayload);\n    expect(typeof parsed).to.equal('object');\n  });\n  \n  test(\"Verified payload should contain correct user data\", function() {\n    const verifiedPayload = JSON.parse(bru.getEnvVar('verified_payload'));\n    \n    expect(verifiedPayload.userId).to.equal(789);\n    expect(verifiedPayload.username).to.equal('verifyuser');\n    expect(verifiedPayload.role).to.equal('admin');\n  });\n  \n  test(\"Verified payload should have timestamp fields\", function() {\n    const verifiedPayload = JSON.parse(bru.getEnvVar('verified_payload'));\n    \n    expect(verifiedPayload.iat).to.exist;\n    expect(verifiedPayload.exp).to.exist;\n    expect(typeof verifiedPayload.iat).to.equal('number');\n    expect(typeof verifiedPayload.exp).to.equal('number');\n  });\n  \n  test(\"Invalid token with wrong secret should throw error\", function() {\n    const jwt = require('jsonwebtoken');\n    const invalidToken = bru.getEnvVar('invalid_token');\n    const secret = bru.getEnvVar('secret') || 'test-secret-key';\n    \n    try {\n      jwt.verify(invalidToken, secret);\n      expect.fail('Expected JWT verification to throw an error for invalid token');\n    } catch (error) {\n      expect(error).to.exist;\n      expect(error.message).to.equal('invalid signature');\n      console.log('Invalid token correctly threw error:', error.message);\n    }\n  });\n}\n\nsettings {\n  encodeUrl: true\n}\n"
  },
  {
    "path": "tests/scripting/inbuilt-libraries/jsonwebtoken/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\"{{projectRoot}}/tests/scripting/inbuilt-libraries/jsonwebtoken/fixtures/collection\"],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    },\n    \"request\": {\n      \"sslVerification\": true,\n      \"customCaCertificate\": {\n        \"enabled\": false,\n        \"filePath\": \"\"\n      },\n      \"keepDefaultCaCertificates\": {\n        \"enabled\": true\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/scripting/inbuilt-libraries/jsonwebtoken/init-user-data/ui-state-snapshot.json",
    "content": "{\n    \"collections\": [\n        {\n\t\t\t\"pathname\": \"{{projectRoot}}/tests/scripting/inbuilt-libraries/jsonwebtoken/fixtures/collection\",\n\t\t\t\"selectedEnvironment\": \"Prod\"\n\t\t}\n    ]\n}\n"
  },
  {
    "path": "tests/scripting/inbuilt-libraries/jsonwebtoken/jsonwebtoken.spec.ts",
    "content": "import { test } from '../../../../playwright';\nimport { setSandboxMode, runCollection, validateRunnerResults } from '../../../utils/page';\n\ntest.describe.serial('jwt collection success', () => {\n  test('developer mode', async ({ pageWithUserData: page }) => {\n    test.setTimeout(2 * 60 * 1000);\n\n    // Set up developer mode\n    await setSandboxMode(page, 'jsonwebtoken', 'developer');\n\n    // Run the collection\n    await runCollection(page, 'jsonwebtoken');\n\n    // Validate test results\n    await validateRunnerResults(page, {\n      totalRequests: 7,\n      passed: 7,\n      failed: 0,\n      skipped: 0\n    });\n  });\n\n  test('safe mode', async ({ pageWithUserData: page }) => {\n    test.setTimeout(2 * 60 * 1000);\n\n    // Set up safe mode\n    await setSandboxMode(page, 'jsonwebtoken', 'safe');\n\n    // Run the collection\n    await runCollection(page, 'jsonwebtoken');\n\n    // Validate test results\n    await validateRunnerResults(page, {\n      totalRequests: 7,\n      passed: 7,\n      failed: 0,\n      skipped: 0\n    });\n  });\n});\n"
  },
  {
    "path": "tests/scripting/url-helpers/fixtures/collections/url_helpers_test/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"url_helpers_test\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}\n"
  },
  {
    "path": "tests/scripting/url-helpers/fixtures/collections/url_helpers_test/url-helpers-test.bru",
    "content": "meta {\n  name: url-helpers-test\n  type: http\n  seq: 1\n}\n\nget {\n  url: https://echo.usebruno.com/api/users/:userId?name=john&age=30\n  body: none\n  auth: none\n}\n\nparams:path {\n  userId: 123\n}\n\nscript:pre-request {\n  // Test URL helper methods in pre-request script\n  const host = req.getHost();\n  const path = req.getPath();\n  const queryString = req.getQueryString();\n  const pathParams = req.getPathParams();\n  \n  // Store values for verification in tests\n  bru.setVar('preReqHost', host);\n  bru.setVar('preReqPath', path);\n  bru.setVar('preReqQueryString', queryString);\n  bru.setVar('preReqPathParams', JSON.stringify(pathParams));\n}\n\nscript:post-response {\n  // Test URL helper methods in post-response script\n  const host = req.getHost();\n  const path = req.getPath();\n  const queryString = req.getQueryString();\n  const pathParams = req.getPathParams();\n  \n  // Store values for verification in tests\n  bru.setVar('postResHost', host);\n  bru.setVar('postResPath', path);\n  bru.setVar('postResQueryString', queryString);\n  bru.setVar('postResPathParams', JSON.stringify(pathParams));\n}\n\ntests {\n  test(\"getHost() returns correct host\", function() {\n    const preReqHost = bru.getVar('preReqHost');\n    const postResHost = bru.getVar('postResHost');\n  \n    expect(preReqHost).to.equal('echo.usebruno.com');\n    expect(postResHost).to.equal('echo.usebruno.com');\n  });\n  \n  test(\"getPath() returns correct path\", function() {\n    const preReqPath = bru.getVar('preReqPath');\n    const postResPath = bru.getVar('postResPath');\n  \n    expect(preReqPath).to.equal('/api/users/123');\n    expect(postResPath).to.equal('/api/users/123');\n  });\n  \n  test(\"getQueryString() returns correct query string\", function() {\n    const preReqQueryString = bru.getVar('preReqQueryString');\n    const postResQueryString = bru.getVar('postResQueryString');\n  \n    expect(preReqQueryString).to.equal('name=john&age=30');\n    expect(postResQueryString).to.equal('name=john&age=30');\n  });\n  \n  test(\"getPathParams() returns correct path parameters\", function() {\n    const preReqPathParams = JSON.parse(bru.getVar('preReqPathParams'));\n    const postResPathParams = JSON.parse(bru.getVar('postResPathParams'));\n  \n    expect(preReqPathParams).to.be.an('array');\n    expect(preReqPathParams).to.have.lengthOf(1);\n    expect(preReqPathParams[0].name).to.equal('userId');\n    expect(preReqPathParams[0].value).to.equal('123');\n    \n    expect(postResPathParams).to.be.an('array');\n    expect(postResPathParams).to.have.lengthOf(1);\n    expect(postResPathParams[0].name).to.equal('userId');\n    expect(postResPathParams[0].value).to.equal('123');\n  });\n}\n"
  },
  {
    "path": "tests/scripting/url-helpers/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{collectionPath}}/url_helpers_test\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/scripting/url-helpers/url-helpers.spec.ts",
    "content": "import { test } from '../../../playwright';\nimport { setSandboxMode, runCollection, validateRunnerResults } from '../../utils/page';\n\ntest.describe.serial('URL helper methods', () => {\n  test.describe('req.getHost(), req.getPath(), req.getQueryString(), req.getPathParams()', () => {\n    test('should work in developer mode', async ({ pageWithUserData: page }) => {\n      // Set up developer mode\n      await setSandboxMode(page, 'url_helpers_test', 'developer');\n\n      // Run the collection\n      await runCollection(page, 'url_helpers_test');\n\n      // Validate test results - 1 request should pass (with 4 assertions inside)\n      await validateRunnerResults(page, {\n        totalRequests: 1,\n        passed: 1,\n        failed: 0,\n        skipped: 0\n      });\n    });\n\n    test('should work in safe mode', async ({ pageWithUserData: page }) => {\n      // Set up safe mode\n      await setSandboxMode(page, 'url_helpers_test', 'safe');\n\n      // Run the collection\n      await runCollection(page, 'url_helpers_test');\n\n      // Validate test results - 1 request should pass in safe mode too (with 4 assertions inside)\n      await validateRunnerResults(page, {\n        totalRequests: 1,\n        passed: 1,\n        failed: 0,\n        skipped: 0\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "tests/sidebar/rename-collection-item.spec.ts",
    "content": "import { test, expect } from '../../playwright';\nimport { buildCommonLocators, createCollection, createRequest, closeAllCollections } from '../utils/page';\n\ntest.describe('Rename Collection Item - File Extension', () => {\n  test.afterEach(async ({ page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('should show .yml extension for OpenCollection format when renaming a request', async ({ page, createTmpDir }) => {\n    const locators = buildCommonLocators(page);\n    const testDir = await createTmpDir('yml-rename-test');\n\n    // Create a collection with OpenCollection (YAML) format\n    await test.step('Create collection with OpenCollection format', async () => {\n      await createCollection(page, 'YML Rename Test', testDir);\n    });\n\n    // Create a request inside the collection\n    await createRequest(page, 'Test Request', 'YML Rename Test');\n\n    // Open rename dialog via context menu\n    await test.step('Open rename dialog and verify .yml extension', async () => {\n      await locators.sidebar.request('Test Request').hover();\n      await locators.actions.collectionItemActions('Test Request').click();\n      await locators.dropdown.item('Rename').click();\n\n      const renameModal = page.locator('.bruno-modal').filter({ hasText: 'Rename Request' });\n      await renameModal.waitFor({ state: 'visible' });\n\n      // Show filesystem name via Options dropdown\n      await renameModal.locator('.btn-advanced').click();\n      await page.locator('.dropdown-item').filter({ hasText: 'Show Filesystem Name' }).click();\n\n      // Click the IconEdit SVG to enable filename editing\n      await renameModal.getByTestId('rename-request-edit-icon').click();\n\n      // Verify the extension shows .yml, not .bru\n      const extensionLabel = renameModal.locator('.file-extension');\n      await expect(extensionLabel).toHaveText('.yml');\n\n      // Close the rename modal\n      await renameModal.getByRole('button', { name: 'Cancel' }).click();\n    });\n  });\n});\n"
  },
  {
    "path": "tests/sidebar/section-auto-expand.spec.ts",
    "content": "import { test, expect } from '../../playwright';\n\ntest.describe('Sidebar Section Auto-Expand', () => {\n  test('Clicking action button on collapsed section should expand it', async ({ page }) => {\n    // The api-specs section is collapsed by default (only collections is expanded)\n    // Find the api-specs section by its title\n    const apiSpecsSection = page.locator('.sidebar-section').filter({ hasText: 'API Specs' });\n\n    // Verify the api-specs section is initially collapsed (doesn't have 'expanded' class)\n    await expect(apiSpecsSection).not.toHaveClass(/expanded/);\n\n    // Verify section-content is not visible when collapsed\n    const sectionContent = apiSpecsSection.locator('.section-content');\n    await expect(sectionContent).not.toBeVisible();\n\n    // Click on the add button in the section-actions area\n    // This should trigger the auto-expand logic\n    const addButton = page.getByTestId('api-specs-header-add-menu');\n    await addButton.click();\n\n    // Close the dropdown by pressing Escape (we just want to test the expand, not the dropdown action)\n    await page.keyboard.press('Escape');\n\n    // After clicking an action, the section should be expanded\n    await expect(apiSpecsSection).toHaveClass(/expanded/);\n\n    // Verify section-content is now visible\n    await expect(sectionContent).toBeVisible();\n  });\n\n  test('Clicking action button on already expanded section should keep it expanded', async ({ page }) => {\n    // The collections section is expanded by default\n    const collectionsSection = page.locator('.sidebar-section').filter({ hasText: 'Collections' });\n\n    // Verify the collections section is initially expanded\n    await expect(collectionsSection).toHaveClass(/expanded/);\n\n    // Verify section-content is visible\n    const sectionContent = collectionsSection.locator('.section-content');\n    await expect(sectionContent).toBeVisible();\n\n    // Click on the add button in the section-actions area\n    const addButton = page.getByTestId('collections-header-add-menu');\n    await addButton.click();\n\n    // Close the dropdown\n    await page.keyboard.press('Escape');\n\n    // Section should still be expanded\n    await expect(collectionsSection).toHaveClass(/expanded/);\n    await expect(sectionContent).toBeVisible();\n  });\n\n  test('Clicking search action on collapsed collections section should expand it', async ({ page }) => {\n    // First, collapse the collections section by clicking on its header\n    const collectionsSection = page.locator('.sidebar-section').filter({ hasText: 'Collections' });\n    const sectionHeader = collectionsSection.locator('.section-header-left');\n    await sectionHeader.click();\n\n    // Verify the section is now collapsed\n    await expect(collectionsSection).not.toHaveClass(/expanded/);\n    const sectionContent = collectionsSection.locator('.section-content');\n    await expect(sectionContent).not.toBeVisible();\n\n    // Now click on the search action button in the collapsed section\n    // The search button is in section-actions with title \"Search requests\"\n    const searchButton = collectionsSection.locator('.section-actions button[title=\"Search requests\"]');\n    await searchButton.click();\n\n    // The section should now be expanded\n    await expect(collectionsSection).toHaveClass(/expanded/);\n    await expect(sectionContent).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "tests/ssl/basic-ssl/collections/badssl/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"badssl\",\n  \"type\": \"collection\",\n  \"ignore\": [\"node_modules\", \".git\"]\n} "
  },
  {
    "path": "tests/ssl/basic-ssl/collections/badssl/package.json",
    "content": "{\n  \"name\": \"badssl\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Bruno test collection for basic ssl testing\"\n} "
  },
  {
    "path": "tests/ssl/basic-ssl/collections/badssl/request.bru",
    "content": "meta {\n  name: request\n  type: http\n  seq: 6\n}\n\nget {\n  url: https://www.badssl.com\n  body: none\n  auth: inherit\n}\n\nassert {\n  res.status: eq 200\n}\n"
  },
  {
    "path": "tests/ssl/basic-ssl/collections/self-signed-badssl/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"self-signed-badssl\",\n  \"type\": \"collection\",\n  \"ignore\": [\"node_modules\", \".git\"]\n} "
  },
  {
    "path": "tests/ssl/basic-ssl/collections/self-signed-badssl/package.json",
    "content": "{\n  \"name\": \"self-signed-badssl\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Bruno test collection for basic ssl testing\"\n} "
  },
  {
    "path": "tests/ssl/basic-ssl/collections/self-signed-badssl/request.bru",
    "content": "meta {\n  name: request\n  type: http\n  seq: 6\n}\n\nget {\n  url: https://self-signed.badssl.com\n  body: none\n  auth: inherit\n}\n\nassert {\n  res.status: eq 200\n}\n"
  },
  {
    "path": "tests/ssl/basic-ssl/tests/basic-ssl-success/basic-ssl-success.spec.ts",
    "content": "import { test } from '../../../../../playwright';\nimport { setSandboxMode, runCollection, validateRunnerResults } from '../../../../utils/page';\n\ntest.describe.serial('basic ssl success', () => {\n  test('developer mode', async ({ pageWithUserData: page }) => {\n    test.setTimeout(2 * 60 * 1000);\n\n    // Set up developer mode\n    await setSandboxMode(page, 'badssl', 'developer');\n\n    // Run the collection\n    await runCollection(page, 'badssl');\n\n    // Validate test results\n    await validateRunnerResults(page, {\n      totalRequests: 1,\n      passed: 1,\n      failed: 0,\n      skipped: 0\n    });\n  });\n\n  test('safe mode', async ({ pageWithUserData: page }) => {\n    test.setTimeout(2 * 60 * 1000);\n\n    // Set up safe mode\n    await setSandboxMode(page, 'badssl', 'safe');\n\n    // Run the collection\n    await runCollection(page, 'badssl');\n\n    // Validate test results\n    await validateRunnerResults(page, {\n      totalRequests: 1,\n      passed: 1,\n      failed: 0,\n      skipped: 0\n    });\n  });\n});\n"
  },
  {
    "path": "tests/ssl/basic-ssl/tests/basic-ssl-success/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\"{{projectRoot}}/tests/ssl/basic-ssl/collections/badssl\"],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    },\n    \"request\": {\n      \"sslVerification\": true,\n      \"customCaCertificate\": {\n        \"enabled\": false,\n        \"filePath\": \"\"\n      },\n      \"keepDefaultCaCertificates\": {\n        \"enabled\": true\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/ssl/basic-ssl/tests/self-signed-rejected/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\"{{projectRoot}}/tests/ssl/basic-ssl/collections/self-signed-badssl\"],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    },\n    \"request\": {\n      \"sslVerification\": true,\n      \"customCaCertificate\": {\n        \"enabled\": false,\n        \"filePath\": \"\"\n      },\n      \"keepDefaultCaCertificates\": {\n        \"enabled\": true\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/ssl/basic-ssl/tests/self-signed-rejected/self-signed-rejected.spec.ts",
    "content": "import { test } from '../../../../../playwright';\nimport { setSandboxMode, runCollection, validateRunnerResults } from '../../../../utils/page';\n\ntest.describe.serial('self signed rejected', () => {\n  test('developer mode', async ({ pageWithUserData: page }) => {\n    test.setTimeout(2 * 60 * 1000);\n\n    // Set up developer mode\n    await setSandboxMode(page, 'self-signed-badssl', 'developer');\n\n    // Run the collection\n    await runCollection(page, 'self-signed-badssl');\n\n    // Validate test results\n    await validateRunnerResults(page, {\n      totalRequests: 1,\n      passed: 0,\n      failed: 1,\n      skipped: 0\n    });\n  });\n\n  test('safe mode', async ({ pageWithUserData: page }) => {\n    test.setTimeout(2 * 60 * 1000);\n\n    // Set up safe mode\n    await setSandboxMode(page, 'self-signed-badssl', 'safe');\n\n    // Run the collection\n    await runCollection(page, 'self-signed-badssl');\n\n    // Validate test results\n    await validateRunnerResults(page, {\n      totalRequests: 1,\n      passed: 0,\n      failed: 1,\n      skipped: 0\n    });\n  });\n});\n"
  },
  {
    "path": "tests/ssl/basic-ssl/tests/self-signed-success-with-validation-disabled/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\"{{projectRoot}}/tests/ssl/basic-ssl/collections/self-signed-badssl\"],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    },\n    \"request\": {\n      \"sslVerification\": false,\n      \"customCaCertificate\": {\n        \"enabled\": false,\n        \"filePath\": \"\"\n      },\n      \"keepDefaultCaCertificates\": {\n        \"enabled\": true\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/ssl/basic-ssl/tests/self-signed-success-with-validation-disabled/self-signed-success-with-validation-disabled.spec.ts",
    "content": "import { test } from '../../../../../playwright';\nimport { setSandboxMode, runCollection, validateRunnerResults } from '../../../../utils/page';\n\ntest.describe.serial('self signed success with validation disabled', () => {\n  test('developer mode', async ({ pageWithUserData: page }) => {\n    test.setTimeout(2 * 60 * 1000);\n\n    // Set up developer mode\n    await setSandboxMode(page, 'self-signed-badssl', 'developer');\n\n    // Run the collection\n    await runCollection(page, 'self-signed-badssl');\n\n    // Validate test results\n    await validateRunnerResults(page, {\n      totalRequests: 1,\n      passed: 1,\n      failed: 0,\n      skipped: 0\n    });\n  });\n\n  test('safe mode', async ({ pageWithUserData: page }) => {\n    test.setTimeout(2 * 60 * 1000);\n\n    // Set up safe mode\n    await setSandboxMode(page, 'self-signed-badssl', 'safe');\n\n    // Run the collection\n    await runCollection(page, 'self-signed-badssl');\n\n    // Validate test results\n    await validateRunnerResults(page, {\n      totalRequests: 1,\n      passed: 1,\n      failed: 0,\n      skipped: 0\n    });\n  });\n});\n"
  },
  {
    "path": "tests/ssl/custom-ca-certs/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"custom-ca-certs\",\n  \"type\": \"collection\",\n  \"ignore\": [\"node_modules\", \".git\"]\n} "
  },
  {
    "path": "tests/ssl/custom-ca-certs/collection/package.json",
    "content": "{\n  \"name\": \"custom-ca-certs\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Bruno test collection for CA certificates and HTTPS server testing\"\n} "
  },
  {
    "path": "tests/ssl/custom-ca-certs/collection/request.bru",
    "content": "meta {\n  name: request\n  type: http\n  seq: 6\n}\n\nget {\n  url: https://localhost:8090\n  body: none\n  auth: inherit\n}\n\nassert {\n  res.status: eq 200\n  res.body: eq helloworld\n}\n"
  },
  {
    "path": "tests/ssl/custom-ca-certs/server/.gitignore",
    "content": "certs"
  },
  {
    "path": "tests/ssl/custom-ca-certs/server/helpers/certs.js",
    "content": "const { execCommand, execCommandSilent, detectPlatform } = require('./platform');\nconst fs = require('node:fs');\nconst path = require('node:path');\n\nfunction createCertsDir(certsDir) {\n  if (fs.existsSync(certsDir)) {\n    fs.rmSync(certsDir, { recursive: true, force: true });\n  }\n  fs.mkdirSync(certsDir, { recursive: true });\n}\n\nfunction generateCertificates(certsDir) {\n  execCommand('openssl version');\n\n  // Generate CA private key\n  execCommand('openssl genrsa -out ca-key.pem 4096', certsDir);\n\n  // Create CA configuration file with proper CA extensions and subject (LibreSSL/OpenSSL compatible)\n  const caConfigContent = `[req]\ndistinguished_name = req_distinguished_name\nx509_extensions = v3_ca\nprompt = no\n\n[req_distinguished_name]\nC = US\nST = Dev\nL = Local\nO = Local Dev CA\nCN = Local Dev CA\n\n[v3_ca]\nbasicConstraints = critical, CA:TRUE\nkeyUsage = critical, keyCertSign, cRLSign\nsubjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid:always,issuer:always`;\n\n  fs.writeFileSync(path.join(certsDir, 'ca.conf'), caConfigContent);\n\n  // Generate CA certificate with proper CA extensions using config file (no -subj needed)\n  execCommand('openssl req -new -x509 -key ca-key.pem -out ca-cert.pem -days 3650 -config ca.conf', certsDir);\n\n  // Generate server private key and CSR\n  execCommand('openssl genrsa -out localhost-key.pem 4096', certsDir);\n\n  // Create server CSR configuration file\n  const serverCsrConfigContent = `[req]\ndistinguished_name = req_distinguished_name\nprompt = no\n\n[req_distinguished_name]\nC = US\nST = Dev\nL = Local\nO = Local Dev\nCN = localhost`;\n\n  fs.writeFileSync(path.join(certsDir, 'localhost-csr.conf'), serverCsrConfigContent);\n  execCommand('openssl req -new -key localhost-key.pem -out localhost.csr -config localhost-csr.conf', certsDir);\n\n  // Create server certificate configuration file (LibreSSL/OpenSSL compatible)\n  const serverConfigContent = `[req]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\nprompt = no\n\n[req_distinguished_name]\nC = Country Name\nST = State or Province Name\nL = Locality Name\nO = Organization Name\nCN = Common Name\n\n[v3_req]\nkeyUsage = critical, keyEncipherment, dataEncipherment, digitalSignature\nextendedKeyUsage = serverAuth\nsubjectAltName = @alt_names\nbasicConstraints = critical, CA:FALSE\nauthorityKeyIdentifier = keyid:always,issuer:always\n\n[alt_names]\nDNS.1 = localhost\nDNS.2 = localhost.localdomain\nIP.1 = 127.0.0.1\nIP.2 = ::1\nIP.3 = ::ffff:127.0.0.1`;\n\n  fs.writeFileSync(path.join(certsDir, 'localhost.conf'), serverConfigContent);\n  execCommand('openssl x509 -req -in localhost.csr -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out localhost-cert.pem -days 730 -extensions v3_req -extfile localhost.conf', certsDir);\n\n  const platform = detectPlatform();\n  if (platform === 'windows') {\n    execCommand('openssl x509 -in ca-cert.pem -outform DER -out ca-cert.der', certsDir);\n    execCommand('openssl pkcs12 -export -out localhost.p12 -inkey localhost-key.pem -in localhost-cert.pem -certfile ca-cert.pem -password pass:', certsDir);\n    execCommand('openssl x509 -in localhost-cert.pem -outform DER -out localhost-cert.der', certsDir);\n  }\n\n  if (platform !== 'windows') {\n    execCommand('chmod 600 ca-key.pem localhost-key.pem', certsDir);\n    execCommand('chmod 644 ca-cert.pem localhost-cert.pem', certsDir);\n  }\n\n  ['localhost.csr', 'localhost.conf', 'localhost-csr.conf', 'ca.conf', 'ca-cert.srl'].forEach((file) => {\n    const filePath = path.join(certsDir, file);\n    if (fs.existsSync(filePath)) fs.unlinkSync(filePath);\n  });\n\n  // Validate certificate chain\n  validateCertificateChain(certsDir);\n}\n\nfunction validateCertificateChain(certsDir) {\n  try {\n    // Verify CA certificate is valid and has proper CA extensions\n    const caVerifyOutput = execCommandSilent('openssl x509 -in ca-cert.pem -text -noout', certsDir).toString();\n\n    if (!caVerifyOutput.includes('CA:TRUE')) {\n      throw new Error('CA certificate missing basicConstraints=CA:TRUE');\n    }\n\n    if (!caVerifyOutput.includes('Certificate Sign')) {\n      throw new Error('CA certificate missing keyCertSign in keyUsage');\n    }\n\n    // Verify server certificate is valid and signed by CA\n    const serverVerifyOutput = execCommandSilent('openssl x509 -in localhost-cert.pem -text -noout', certsDir).toString();\n\n    if (!serverVerifyOutput.includes('CA:FALSE')) {\n      throw new Error('Server certificate should have basicConstraints=CA:FALSE');\n    }\n\n    if (!serverVerifyOutput.includes('TLS Web Server Authentication')) {\n      throw new Error('Server certificate missing serverAuth in extendedKeyUsage');\n    }\n\n    // Verify certificate chain\n    execCommandSilent('openssl verify -CAfile ca-cert.pem localhost-cert.pem', certsDir);\n\n    console.log('✅ Certificate chain validation passed');\n  } catch (error) {\n    console.error('❌ Certificate validation failed:', error.message);\n    throw new Error(`Certificate validation failed: ${error.message}`);\n  }\n}\n\nfunction addCAToTruststore(certsDir) {\n  const platform = detectPlatform();\n\n  switch (platform) {\n    case 'macos': {\n      const macCertPath = path.join(certsDir, 'ca-cert.pem');\n      execCommand(`sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain \"${macCertPath}\"`);\n      break;\n    }\n\n    case 'linux': {\n      const linuxCertPath = path.join(certsDir, 'ca-cert.pem');\n      execCommand(`sudo cp \"${linuxCertPath}\" /usr/local/share/ca-certificates/bruno-ca.crt`);\n      execCommand('sudo update-ca-certificates');\n      break;\n    }\n\n    case 'windows': {\n      const winCertPath = path.join(certsDir, 'ca-cert.der');\n\n      // Escape backslashes for PowerShell\n      const psPath = winCertPath.replace(/\\\\/g, '\\\\\\\\');\n\n      // PowerShell .NET method (works reliably in CI)\n      const psCommand = [\n        `$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2('${psPath}');`,\n        `$store = New-Object System.Security.Cryptography.X509Certificates.X509Store('Root','LocalMachine');`,\n        `$store.Open('ReadWrite');`,\n        `$store.Add($cert);`,\n        `$store.Close();`,\n        // Verify cert was added by checking if it exists in LocalMachine\\Root\n        `$verifyStore = New-Object System.Security.Cryptography.X509Certificates.X509Store('Root','LocalMachine');`,\n        `$verifyStore.Open('ReadOnly');`,\n        `$found = $verifyStore.Certificates | Where-Object { $_.Thumbprint -eq $cert.Thumbprint };`,\n        `$verifyStore.Close();`,\n        `if (-not $found) { throw 'Certificate was not added to LocalMachine\\Root' };`\n      ].join(' ');\n\n      execCommand(`powershell -Command \"${psCommand}\"`);\n      break;\n    }\n\n    default:\n      throw new Error(`Unsupported platform: ${platform}`);\n  }\n}\n\nfunction verifyCertificates(certsDir) {\n  const platform = detectPlatform();\n  // Core PEM files required for all platforms\n  const requiredFiles = ['ca-cert.pem', 'ca-key.pem', 'localhost-cert.pem', 'localhost-key.pem'];\n\n  // Verify required PEM files exist\n  for (const file of requiredFiles) {\n    const filePath = path.join(certsDir, file);\n    if (!fs.existsSync(filePath)) {\n      throw new Error(`missing certificate file: ${file}`);\n    }\n  }\n\n  // Check Windows-specific files but don't require them (they're optional fallbacks)\n  if (platform === 'windows') {\n    const windowsFiles = ['ca-cert.der', 'localhost.p12', 'localhost-cert.der'];\n    for (const file of windowsFiles) {\n      const filePath = path.join(certsDir, file);\n      if (fs.existsSync(filePath)) {\n        console.log(`✅ Windows certificate file available: ${file}`);\n      } else {\n        console.log(`⚠️ Windows certificate file missing (but not required): ${file}`);\n      }\n    }\n  }\n}\n\nmodule.exports = {\n  createCertsDir,\n  generateCertificates,\n  addCAToTruststore,\n  verifyCertificates\n};\n"
  },
  {
    "path": "tests/ssl/custom-ca-certs/server/helpers/platform.js",
    "content": "const { execSync } = require('node:child_process');\nconst os = require('node:os');\n\nfunction execCommand(command, cwd = process.cwd()) {\n  return execSync(command, {\n    cwd,\n    stdio: 'inherit',\n    timeout: 30000\n  });\n}\n\nfunction execCommandSilent(command, cwd = process.cwd()) {\n  return execSync(command, {\n    cwd,\n    stdio: 'pipe',\n    timeout: 30000\n  });\n}\n\nfunction detectPlatform() {\n  const platform = os.platform();\n  switch (platform) {\n    case 'darwin': return 'macos';\n    case 'linux': return 'linux';\n    case 'win32': return 'windows';\n    default: throw new Error(`Unsupported platform: ${platform}`);\n  }\n}\n\nfunction killProcessOnPort(port) {\n  const platform = detectPlatform();\n\n  try {\n    switch (platform) {\n      case 'macos':\n        execCommand(`lsof -ti :${port} | xargs kill -9`);\n        break;\n      case 'linux':\n        execCommand(`lsof -ti :${port} | xargs kill -9`);\n        break;\n      case 'windows':\n        const result = execCommandSilent(`netstat -ano | findstr :${port}`);\n        const lines = result.toString().split('\\n');\n        for (const line of lines) {\n          const match = line.trim().match(/\\s+(\\d+)$/);\n          if (match) {\n            execCommandSilent(`taskkill /F /PID ${match[1]}`);\n          }\n        }\n        break;\n    }\n  } catch (error) {}\n}\n\nmodule.exports = {\n  execCommand,\n  execCommandSilent,\n  detectPlatform,\n  killProcessOnPort\n};\n"
  },
  {
    "path": "tests/ssl/custom-ca-certs/server/index.js",
    "content": "#!/usr/bin/env node\n\nconst path = require('node:path');\nconst fs = require('node:fs');\nconst https = require('node:https');\nconst WebSocket = require('ws');\nconst { killProcessOnPort } = require('./helpers/platform');\n\nfunction createServer(certsDir, port = 8090) {\n  const serverOptions = {\n    key: fs.readFileSync(path.join(certsDir, 'localhost-key.pem')),\n    cert: fs.readFileSync(path.join(certsDir, 'localhost-cert.pem')),\n    ca: fs.readFileSync(path.join(certsDir, 'ca-cert.pem'))\n  };\n\n  const server = https.createServer(serverOptions, (req, res) => {\n    res.setHeader('Content-Type', 'text/html; charset=UTF-8');\n    res.end('helloworld');\n  });\n\n  // Create WebSocket server for WSS support\n  const wss = new WebSocket.Server({ noServer: true });\n\n  wss.on('connection', function connection(ws, request) {\n    ws.on('error', function error(err) {\n      console.error('WebSocket error:', err.message);\n    });\n\n    ws.on('message', function message(data) {\n      const msg = Buffer.from(data).toString().trim();\n      let isJSON = false;\n      let obj = {};\n      try {\n        obj = JSON.parse(msg);\n        isJSON = true;\n      } catch (err) {\n        // Not a JSON value\n      }\n      if (isJSON) {\n        if ('func' in obj && obj.func === 'headers') {\n          return ws.send(JSON.stringify({\n            headers: request.headers\n          }));\n        } else if ('func' in obj && obj.func === 'query') {\n          const url = new URL(request.url, `https://${request.headers.host}`);\n          const query = Object.fromEntries(url.searchParams.entries());\n          return ws.send(JSON.stringify({\n            query: query\n          }));\n        } else {\n          return ws.send(JSON.stringify({\n            data: obj\n          }));\n        }\n      }\n      return ws.send(Buffer.from(data).toString());\n    });\n  });\n\n  // Handle WebSocket upgrade requests\n  server.on('upgrade', (request, socket, head) => {\n    if (request.url.startsWith('/ws')) {\n      wss.handleUpgrade(request, socket, head, (ws) => {\n        wss.emit('connection', ws, request);\n      });\n    } else {\n      socket.destroy();\n    }\n  });\n\n  return new Promise((resolve, reject) => {\n    server.listen(port, (error) => {\n      if (error) {\n        reject(error);\n      } else {\n        resolve(server);\n      }\n    });\n  });\n}\n\nfunction shutdownServer(server, cleanup) {\n  const shutdown = (signal) => {\n    console.log(`🛑 Received ${signal}, shutting down`);\n\n    if (cleanup) cleanup();\n\n    if (server) {\n      server.close(() => process.exit(0));\n    } else {\n      process.exit(0);\n    }\n  };\n\n  process.on('SIGINT', () => shutdown('SIGINT'));\n  process.on('SIGTERM', () => shutdown('SIGTERM'));\n}\n\nasync function startServer() {\n  const certsDir = path.join(__dirname, 'certs');\n  const port = 8090;\n\n  console.log('🚀 Starting HTTPS test server');\n\n  try {\n    killProcessOnPort(port);\n\n    console.log(`🌐 Creating server on port ${port}`);\n    const server = await createServer(certsDir, port);\n\n    shutdownServer(server, () => {\n      console.log('✨ Server cleanup completed');\n    });\n  } catch (error) {\n    console.error('❌ Server startup failed:', error.message);\n    process.exit(1);\n  }\n}\n\nif (require.main === module) {\n  startServer();\n}\n\nmodule.exports = { startServer };\n"
  },
  {
    "path": "tests/ssl/custom-ca-certs/server/readme.md",
    "content": "# CA Certificates Test Server\n\nA Node.js HTTPS test server with self-signed certificate generation for testing SSL/TLS connections in Bruno.\n\n## Overview\n\nThis server provides two main functionalities:\n1. **Certificate Generation** - Creates a complete CA certificate chain for testing\n2. **HTTPS Server** - Runs a secure server using the generated certificates\n\n## Usage\n\n### 1. Generate Certificates\n\nGenerate the required CA certificates and add them to your system's truststore:\n\n```bash\nnode scripts/generate-certs.js\n```\n\nThis will:\n- Create a `certs/` directory\n- Generate CA certificate, server certificate, and private keys\n- Verify the certificate chain\n- Add the CA certificate to your system's truststore (macOS/Linux/Windows)\n\n**Generated Files:**\n- `certs/ca-cert.pem` - Certificate Authority certificate\n- `certs/ca-key.pem` - CA private key\n- `certs/localhost-cert.pem` - Server certificate for localhost\n- `certs/localhost-key.pem` - Server private key\n\n**Windows-Specific Files (automatically generated on Windows):**\n- `certs/ca-cert.der` - CA certificate in DER format (for Windows certificate store)\n- `certs/localhost.p12` - PKCS#12 bundle containing server certificate and key\n- `certs/localhost-cert.der` - Server certificate in DER format\n\n### Certificate Installation Details\n\nThe certificate generation script automatically adds the CA certificate to your system's truststore:\n\n**macOS:** Uses `security add-trusted-cert` to add the CA to the System keychain\n**Linux:** Copies the CA certificate to `/usr/local/share/ca-certificates/` and runs `update-ca-certificates`\n**Windows:** Uses PowerShell to add the CA certificate to the LocalMachine\\Root certificate store\n\n> **Note:** On Windows, the script requires Administrator privileges to install certificates to the machine-wide certificate store. If you encounter permission issues, run your terminal as Administrator.\n\n### 2. Run HTTPS Server\n\nStart the HTTPS server on port 8090:\n\n```bash\nnode index.js\n```\n\nThe server will:\n- Load certificates from the `certs/` directory\n- Start an HTTPS server on `https://localhost:8090`\n- Serve a simple \"helloworld\" response\n- Handle graceful shutdown on SIGINT/SIGTERM\n\n## Testing\n\nOnce the server is running, you can test SSL connections:\n\n### Unix/Linux/macOS\n```bash\n# Test with curl\ncurl https://localhost:8090\n\n# Test certificate verification\nopenssl s_client -connect localhost:8090 -CAfile certs/ca-cert.pem\n```\n\n### Windows\n```powershell\n# Test with curl (if available)\ncurl https://localhost:8090\n\n# Test with PowerShell Invoke-WebRequest\nInvoke-WebRequest -Uri https://localhost:8090\n\n# Test certificate verification with OpenSSL\nopenssl s_client -connect localhost:8090 -CAfile certs/ca-cert.pem\n\n# Verify certificate is installed in Windows certificate store\nGet-ChildItem -Path Cert:\\LocalMachine\\Root | Where-Object { $_.Subject -like \"*Local Dev CA*\" }\n\n# Test with .NET WebClient (alternative method)\n$client = New-Object System.Net.WebClient\n$client.DownloadString(\"https://localhost:8090\")\n```\n\n## File Structure\n\n```\nserver/\n├── index.js              # Main HTTPS server\n├── scripts/\n│   └── generate-certs.js  # Certificate generation script\n├── helpers/\n│   ├── certs.js          # Certificate management utilities\n│   └── platform.js       # Platform-specific utilities\n├── certs/                # Generated certificates (created by script)\n└── readme.md            # This file\n```\n"
  },
  {
    "path": "tests/ssl/custom-ca-certs/server/scripts/generate-certs.js",
    "content": "#!/usr/bin/env node\n\nconst path = require('node:path');\nconst {\n  createCertsDir,\n  generateCertificates,\n  addCAToTruststore,\n  verifyCertificates\n} = require('../helpers/certs');\n\n/**\n * Setup CA certificates for testing server\n */\nasync function setup() {\n  console.log('🔧 Setting up CA certificates for test server');\n\n  const certsDir = path.join(__dirname, '..', 'certs');\n\n  try {\n    console.log('📁 Creating certificates directory');\n    createCertsDir(certsDir);\n\n    console.log('🔐 Generating certificates');\n    generateCertificates(certsDir);\n\n    console.log('✅ Verifying certificates');\n    verifyCertificates(certsDir);\n\n    console.log('🛡️ Adding CA to truststore');\n    addCAToTruststore(certsDir);\n\n    console.log('🎉 CA certificate setup completed successfully');\n    return true;\n  } catch (error) {\n    console.error('❌ Generate certs failed:', error.message);\n    throw error;\n  }\n}\n\nif (require.main === module) {\n  setup()\n    .then(() => process.exit(0))\n    .catch(() => process.exit(1));\n}\n\nmodule.exports = { setup };\n"
  },
  {
    "path": "tests/ssl/custom-ca-certs/tests/custom-invalid-ca-cert-in-config/custom-invalid-ca-cert-in-config.spec.ts",
    "content": "import { test } from '../../../../../playwright';\nimport { setSandboxMode, runCollection, validateRunnerResults } from '../../../../utils/page';\n\ntest.describe.serial('custom invalid ca cert added to the config and NO default ca certs', () => {\n  test('developer mode', async ({ pageWithUserData: page }) => {\n    test.setTimeout(2 * 60 * 1000);\n\n    // Set up developer mode\n    await setSandboxMode(page, 'custom-ca-certs', 'developer');\n\n    // Run the collection\n    await runCollection(page, 'custom-ca-certs');\n\n    // Validate test results\n    await validateRunnerResults(page, {\n      totalRequests: 1,\n      passed: 0,\n      failed: 1,\n      skipped: 0\n    });\n  });\n\n  test('safe mode', async ({ pageWithUserData: page }) => {\n    test.setTimeout(2 * 60 * 1000);\n\n    // Set up safe mode\n    await setSandboxMode(page, 'custom-ca-certs', 'safe');\n\n    // Run the collection\n    await runCollection(page, 'custom-ca-certs');\n\n    // Validate test results\n    await validateRunnerResults(page, {\n      totalRequests: 1,\n      passed: 0,\n      failed: 1,\n      skipped: 0\n    });\n  });\n});\n"
  },
  {
    "path": "tests/ssl/custom-ca-certs/tests/custom-invalid-ca-cert-in-config/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\"{{projectRoot}}/tests/ssl/custom-ca-certs/collection\"],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    },\n    \"request\": {\n      \"sslVerification\": true,\n      \"customCaCertificate\": {\n        \"enabled\": true,\n        \"filePath\": \"{{projectRoot}}/tests/ssl/custom-ca-certs/server/certs/ca-key.pem\"\n      },\n      \"keepDefaultCaCertificates\": {\n        \"enabled\": false\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/ssl/custom-ca-certs/tests/custom-invalid-ca-cert-in-config-with-defaults/custom-invalid-ca-cert-in-config-with-defaults.spec.ts",
    "content": "import { test } from '../../../../../playwright';\nimport { setSandboxMode, runCollection, validateRunnerResults } from '../../../../utils/page';\n\ntest.describe('custom invalid ca cert added to the config and keep default ca certs', () => {\n  test('developer mode', async ({ pageWithUserData: page }) => {\n    test.setTimeout(2 * 60 * 1000);\n\n    // Set up developer mode\n    await setSandboxMode(page, 'custom-ca-certs', 'developer');\n\n    // Run the collection\n    await runCollection(page, 'custom-ca-certs');\n\n    // Validate test results\n    await validateRunnerResults(page, {\n      totalRequests: 1,\n      passed: 1,\n      failed: 0,\n      skipped: 0\n    });\n  });\n\n  test('safe mode', async ({ pageWithUserData: page }) => {\n    test.setTimeout(2 * 60 * 1000);\n\n    // Set up safe mode\n    await setSandboxMode(page, 'custom-ca-certs', 'safe');\n\n    // Run the collection\n    await runCollection(page, 'custom-ca-certs');\n\n    // Validate test results\n    await validateRunnerResults(page, {\n      totalRequests: 1,\n      passed: 1,\n      failed: 0,\n      skipped: 0\n    });\n  });\n});\n"
  },
  {
    "path": "tests/ssl/custom-ca-certs/tests/custom-invalid-ca-cert-in-config-with-defaults/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\"{{projectRoot}}/tests/ssl/custom-ca-certs/collection\"],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    },\n    \"request\": {\n      \"sslVerification\": true,\n      \"customCaCertificate\": {\n        \"enabled\": true,\n        \"filePath\": \"{{projectRoot}}/tests/ssl/custom-ca-certs/server/certs/ca-key.pem\"\n      },\n      \"keepDefaultCaCertificates\": {\n        \"enabled\": true\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/ssl/custom-ca-certs/tests/custom-valid-ca-cert-in-config/custom-valid-ca-cert-in-config.spec.ts",
    "content": "import { test } from '../../../../../playwright';\nimport { setSandboxMode, runCollection, validateRunnerResults } from '../../../../utils/page';\n\ntest.describe('custom valid ca cert added to the config and NO default ca certs', () => {\n  test('developer mode', async ({ pageWithUserData: page }) => {\n    test.setTimeout(2 * 60 * 1000);\n\n    // Set up developer mode\n    await setSandboxMode(page, 'custom-ca-certs', 'developer');\n\n    // Run the collection\n    await runCollection(page, 'custom-ca-certs');\n\n    // Validate test results\n    await validateRunnerResults(page, {\n      totalRequests: 1,\n      passed: 1,\n      failed: 0,\n      skipped: 0\n    });\n  });\n\n  test('safe mode', async ({ pageWithUserData: page }) => {\n    test.setTimeout(2 * 60 * 1000);\n\n    // Set up safe mode\n    await setSandboxMode(page, 'custom-ca-certs', 'safe');\n\n    // Run the collection\n    await runCollection(page, 'custom-ca-certs');\n\n    // Validate test results\n    await validateRunnerResults(page, {\n      totalRequests: 1,\n      passed: 1,\n      failed: 0,\n      skipped: 0\n    });\n  });\n});\n"
  },
  {
    "path": "tests/ssl/custom-ca-certs/tests/custom-valid-ca-cert-in-config/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\"{{projectRoot}}/tests/ssl/custom-ca-certs/collection\"],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    },\n    \"request\": {\n      \"sslVerification\": true,\n      \"customCaCertificate\": {\n        \"enabled\": true,\n        \"filePath\": \"{{projectRoot}}/tests/ssl/custom-ca-certs/server/certs/ca-cert.pem\"\n      },\n      \"keepDefaultCaCertificates\": {\n        \"enabled\": false\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/ssl/custom-ca-certs/tests/custom-valid-ca-cert-in-config-with-defaults/custom-valid-ca-cert-in-config-with-defaults.spec.ts",
    "content": "import { test } from '../../../../../playwright';\nimport { setSandboxMode, runCollection, validateRunnerResults } from '../../../../utils/page';\n\ntest.describe('custom valid ca cert added to the config and keep default ca certs', () => {\n  test('developer mode', async ({ pageWithUserData: page }) => {\n    test.setTimeout(2 * 60 * 1000);\n\n    // Set up developer mode\n    await setSandboxMode(page, 'custom-ca-certs', 'developer');\n\n    // Run the collection\n    await runCollection(page, 'custom-ca-certs');\n\n    // Validate test results\n    await validateRunnerResults(page, {\n      totalRequests: 1,\n      passed: 1,\n      failed: 0,\n      skipped: 0\n    });\n  });\n\n  test('safe mode', async ({ pageWithUserData: page }) => {\n    test.setTimeout(2 * 60 * 1000);\n\n    // Set up safe mode\n    await setSandboxMode(page, 'custom-ca-certs', 'safe');\n\n    // Run the collection\n    await runCollection(page, 'custom-ca-certs');\n\n    // Validate test results\n    await validateRunnerResults(page, {\n      totalRequests: 1,\n      passed: 1,\n      failed: 0,\n      skipped: 0\n    });\n  });\n});\n"
  },
  {
    "path": "tests/ssl/custom-ca-certs/tests/custom-valid-ca-cert-in-config-with-defaults/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\"{{projectRoot}}/tests/ssl/custom-ca-certs/collection\"],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    },\n    \"request\": {\n      \"sslVerification\": true,\n      \"customCaCertificate\": {\n        \"enabled\": true,\n        \"filePath\": \"{{projectRoot}}/tests/ssl/custom-ca-certs/server/certs/ca-cert.pem\"\n      },\n      \"keepDefaultCaCertificates\": {\n        \"enabled\": true\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/ssl/custom-ca-certs/tests/wss-success/fixtures/wss-collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"wss-custom-ca-certs-test\",\n  \"type\": \"collection\",\n  \"ignore\": [\"node_modules\", \".git\"]\n}\n"
  },
  {
    "path": "tests/ssl/custom-ca-certs/tests/wss-success/fixtures/wss-collection/package.json",
    "content": "{\n  \"name\": \"wss-custom-ca-certs\"\n}\n"
  },
  {
    "path": "tests/ssl/custom-ca-certs/tests/wss-success/fixtures/wss-collection/ws-ssl-request.bru",
    "content": "meta {\n  name: ws-ssl-request\n  type: ws\n  seq: 1\n}\n\nws {\n  url: wss://localhost:8090/ws\n  auth: inherit\n}\n\nbody:ws {\n  name: message 1\n  content: '''\n    {\n      \"func\":\"headers\"\n    }\n  '''\n}\n"
  },
  {
    "path": "tests/ssl/custom-ca-certs/tests/wss-success/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\"{{projectRoot}}/tests/ssl/custom-ca-certs/tests/wss-success/fixtures/wss-collection\"],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    },\n    \"request\": {\n      \"sslVerification\": true,\n      \"customCaCertificate\": {\n        \"enabled\": true,\n        \"filePath\": \"{{projectRoot}}/tests/ssl/custom-ca-certs/server/certs/ca-cert.pem\"\n      },\n      \"keepDefaultCaCertificates\": {\n        \"enabled\": false\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tests/ssl/custom-ca-certs/tests/wss-success/wss-success.spec.ts",
    "content": "import { test, expect } from '../../../../../playwright';\nimport { openCollection } from '../../../../utils/page';\nimport { buildWebsocketCommonLocators } from '../../../../utils/page/locators';\n\nconst BRU_REQ_NAME = /^ws-ssl-request$/;\n\ntest.describe.serial('wss with custom ca cert', () => {\n  test('websocket connects over ssl', async ({ pageWithUserData: page }) => {\n    const locators = buildWebsocketCommonLocators(page);\n\n    // Define reusable locators\n    const requestItem = page.getByTitle(BRU_REQ_NAME);\n\n    await test.step('Open collection', async () => {\n      await openCollection(page, 'wss-custom-ca-certs-test');\n    });\n\n    await test.step('Connect to WSS', async () => {\n      await requestItem.click();\n      await locators.connectionControls.connect().click();\n      await expect(locators.connectionControls.disconnect()).toBeAttached();\n    });\n\n    await test.step('Send message and verify response', async () => {\n      await locators.runner().click();\n      const responseMessage = locators.messages().nth(2).locator('.text-ellipsis');\n      await expect(responseMessage).toHaveText(/\\\"headers\\\"/);\n    });\n  });\n});\n"
  },
  {
    "path": "tests/start/app-open.spec.ts",
    "content": "import { test, expect } from '../../playwright';\n\ntest('Check if the workspace name is visible in the sidebar', async ({ page }) => {\n  // Wait for the app to be loaded\n  await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n  // Wait for the workspace name container to be visible (contains workspace name like \"My Workspace\" or \"Default Workspace\")\n  await expect(page.locator('.workspace-name-container')).toBeVisible();\n});\n"
  },
  {
    "path": "tests/transient-requests/transient-requests.spec.ts",
    "content": "import { test, expect } from '../../playwright';\nimport { createTransientRequest, fillRequestUrl, closeAllCollections, createCollection, sendRequest, clickResponseAction, selectRequestPaneTab } from '../utils/page';\nimport { buildCommonLocators, buildWebsocketCommonLocators } from '../utils/page/locators';\n\nconst saveShortcut = process.platform === 'darwin' ? 'Meta+s' : 'Control+s';\n\ntest.describe.serial('Transient Requests', () => {\n  let locators: ReturnType<typeof buildCommonLocators>;\n\n  test.beforeAll(async ({ page, createTmpDir }) => {\n    locators = buildCommonLocators(page);\n\n    // Create a temporary collection\n    const collectionPath = await createTmpDir('transient-collection');\n    await createCollection(page, 'transient-requests-test', collectionPath);\n\n    // Verify the collection is loaded\n    await test.step('Verify test collection is loaded', async () => {\n      await expect(locators.sidebar.collection('transient-requests-test')).toBeVisible();\n      await locators.sidebar.collection('transient-requests-test').click();\n    });\n  });\n\n  test.afterAll(async ({ page }) => {\n    // Clean up all collections\n    await closeAllCollections(page);\n  });\n\n  test('Create transient HTTP request - should not appear in sidebar', async ({ page }) => {\n    await test.step('Create transient HTTP request', async () => {\n      await createTransientRequest(page, {\n        requestType: 'HTTP'\n      });\n      await fillRequestUrl(page, 'http://localhost:8081/ping');\n    });\n\n    await test.step('Verify HTTP request tab is open', async () => {\n      const activeTab = page.locator('.request-tab.active');\n      await expect(activeTab).toBeVisible();\n      await expect(activeTab).toContainText('Untitled');\n    });\n\n    await test.step('Verify request is NOT in sidebar', async () => {\n      // Click on the collection to ensure it's expanded\n      await locators.sidebar.collection('transient-requests-test').click();\n      await page.waitForTimeout(300);\n\n      // Check that there are no requests in the collection\n      // Transient requests should not appear in the sidebar\n      const collectionItems = page.locator('.collection-item-name');\n      await expect(collectionItems).toHaveCount(0);\n    });\n  });\n\n  test('Create transient GraphQL request - should not appear in sidebar', async ({ page }) => {\n    await test.step('Create transient GraphQL request', async () => {\n      await createTransientRequest(page, {\n        requestType: 'GraphQL'\n      });\n      await fillRequestUrl(page, 'https://api.example.com/graphql');\n    });\n\n    await test.step('Verify GraphQL request tab is open', async () => {\n      const activeTab = page.locator('.request-tab.active');\n      await expect(activeTab).toBeVisible();\n      await expect(activeTab).toContainText('Untitled');\n    });\n\n    await test.step('Verify request is NOT in sidebar', async () => {\n      // Check that there are still no requests in the collection\n      const collectionItems = page.locator('.collection-item-name');\n      await expect(collectionItems).toHaveCount(0);\n    });\n  });\n\n  test('Create transient gRPC request - should not appear in sidebar', async ({ page }) => {\n    await test.step('Create transient gRPC request', async () => {\n      await createTransientRequest(page, {\n        requestType: 'gRPC'\n      });\n      await fillRequestUrl(page, 'grpc://localhost:50051');\n    });\n\n    await test.step('Verify gRPC request tab is open', async () => {\n      const activeTab = page.locator('.request-tab.active');\n      await expect(activeTab).toBeVisible();\n      await expect(activeTab).toContainText('Untitled');\n    });\n\n    await test.step('Verify request is NOT in sidebar', async () => {\n      // Check that there are still no requests in the collection\n      const collectionItems = page.locator('.collection-item-name');\n      await expect(collectionItems).toHaveCount(0);\n    });\n  });\n\n  test('Create transient WebSocket request - should not appear in sidebar', async ({ page }) => {\n    await test.step('Create transient WebSocket request', async () => {\n      await createTransientRequest(page, {\n        requestType: 'WebSocket'\n      });\n      await fillRequestUrl(page, 'ws://localhost:8082');\n    });\n\n    await test.step('Verify WebSocket request tab is open', async () => {\n      const activeTab = page.locator('.request-tab.active');\n      await expect(activeTab).toBeVisible();\n      await expect(activeTab).toContainText('Untitled');\n    });\n\n    await test.step('Verify request is NOT in sidebar', async () => {\n      // Check that there are still no requests in the collection\n      const collectionItems = page.locator('.collection-item-name');\n      await expect(collectionItems).toHaveCount(0);\n    });\n  });\n\n  test('Save transient HTTP request - should appear in sidebar after save', async ({ page }) => {\n    await test.step('Create transient HTTP request', async () => {\n      await createTransientRequest(page, {\n        requestType: 'HTTP'\n      });\n      await fillRequestUrl(page, 'http://localhost:8081/echo');\n    });\n\n    await test.step('Trigger save action using keyboard shortcut', async () => {\n      // Try to save using Cmd+S (Mac) or Ctrl+S (other platforms)\n      await page.keyboard.press(saveShortcut);\n      await page.waitForTimeout(500);\n    });\n\n    await test.step('Fill in save dialog', async () => {\n      // Wait for save modal to appear\n      const saveModal = page.locator('.bruno-modal-card').filter({ hasText: 'Save Request' });\n      await expect(saveModal).toBeVisible({ timeout: 5000 });\n\n      // Fill in request name\n      const requestNameInput = saveModal.locator('#request-name');\n      await requestNameInput.clear();\n      await requestNameInput.fill('Saved HTTP Request');\n\n      // Click Save button\n      await saveModal.getByRole('button', { name: 'Save' }).click();\n\n      // Wait for success toast\n      await page.waitForTimeout(1000);\n    });\n\n    await test.step('Verify saved request appears in sidebar', async () => {\n      // Check collection is expanded\n      await locators.sidebar.collection('transient-requests-test').click();\n\n      // Look for the saved request in sidebar\n      const savedRequest = locators.sidebar.request('Saved HTTP Request');\n      await expect(savedRequest).toBeVisible();\n    });\n\n    await test.step('Cleanup: Delete the saved request', async () => {\n      await locators.sidebar.request('Saved HTTP Request').hover();\n      await locators.actions.collectionItemActions('Saved HTTP Request').click();\n      await locators.dropdown.item('Delete').click();\n      await locators.modal.button('Delete').click();\n      await expect(locators.sidebar.request('Saved HTTP Request')).not.toBeVisible();\n    });\n  });\n\n  test('Save transient GraphQL request - should appear in sidebar after save', async ({ page }) => {\n    await test.step('Create transient GraphQL request', async () => {\n      await createTransientRequest(page, {\n        requestType: 'GraphQL'\n      });\n      await fillRequestUrl(page, 'https://api.example.com/graphql');\n    });\n\n    await test.step('Trigger save action using keyboard shortcut', async () => {\n      await page.keyboard.press(saveShortcut);\n      await page.waitForTimeout(500);\n    });\n\n    await test.step('Fill in save dialog', async () => {\n      const saveModal = page.locator('.bruno-modal-card').filter({ hasText: 'Save Request' });\n      await expect(saveModal).toBeVisible({ timeout: 5000 });\n\n      const requestNameInput = saveModal.locator('#request-name');\n      await requestNameInput.clear();\n      await requestNameInput.fill('Saved GraphQL Request');\n\n      await saveModal.getByRole('button', { name: 'Save' }).click();\n      await page.waitForTimeout(1000);\n    });\n\n    await test.step('Verify saved request appears in sidebar', async () => {\n      await locators.sidebar.collection('transient-requests-test').click();\n      const savedRequest = locators.sidebar.request('Saved GraphQL Request');\n      await expect(savedRequest).toBeVisible();\n    });\n\n    await test.step('Cleanup: Delete the saved request', async () => {\n      await locators.sidebar.request('Saved GraphQL Request').hover();\n      await locators.actions.collectionItemActions('Saved GraphQL Request').click();\n      await locators.dropdown.item('Delete').click();\n      await locators.modal.button('Delete').click();\n      await expect(locators.sidebar.request('Saved GraphQL Request')).not.toBeVisible();\n    });\n  });\n\n  test('Send transient HTTP request - verify response', async ({ page }) => {\n    await test.step('Create transient HTTP request', async () => {\n      await createTransientRequest(page, {\n        requestType: 'HTTP'\n      });\n      await fillRequestUrl(page, 'http://localhost:8081/ping');\n    });\n\n    await test.step('Send request and verify response', async () => {\n      // Send request using the helper function\n      await sendRequest(page, 200);\n\n      // Copy response to clipboard and verify\n      await clickResponseAction(page, 'response-copy-btn');\n      await expect(page.getByText('Response copied to clipboard')).toBeVisible();\n\n      const clipboardText = await page.evaluate(() => navigator.clipboard.readText());\n      expect(clipboardText).toBe('pong');\n    });\n  });\n});\n"
  },
  {
    "path": "tests/utils/page/actions.ts",
    "content": "import { test, expect, Page } from '../../../playwright';\nimport { buildCommonLocators } from './locators';\n\ntype SandboxMode = 'safe' | 'developer';\n\n/**\n * Close all collections\n * @param page - The page object\n * @returns void\n */\nconst closeAllCollections = async (page) => {\n  await test.step('Close all collections', async () => {\n    const numberOfCollections = await page.locator('[data-testid=\"collections\"] .collection-name').count();\n\n    for (let i = 0; i < numberOfCollections; i++) {\n      const firstCollection = page.locator('[data-testid=\"collections\"] .collection-name').first();\n      await firstCollection.hover();\n      await firstCollection.locator('.collection-actions .icon').click();\n      await page.locator('.dropdown-item').getByText('Remove').click();\n\n      // Wait for modal to appear - could be either regular remove or drafts confirmation\n      const removeModal = page.locator('.bruno-modal').filter({ hasText: 'Remove Collection' });\n      await removeModal.waitFor({ state: 'visible', timeout: 5000 });\n\n      // Check if it's the drafts confirmation modal (has \"Discard All and Remove\" button)\n      const hasDiscardButton = await page.getByRole('button', { name: 'Discard All and Remove' }).isVisible().catch(() => false);\n\n      if (hasDiscardButton) {\n        // Drafts modal - click \"Discard All and Remove\"\n        await page.getByRole('button', { name: 'Discard All and Remove' }).click();\n      } else {\n        // Regular modal - click the submit button\n        await page.locator('.bruno-modal-footer .submit').click();\n      }\n\n      // Wait for modal to close\n      await removeModal.waitFor({ state: 'hidden', timeout: 5000 });\n    }\n\n    // Wait until no collections are left open (check sidebar only)\n    await expect(page.getByTestId('collections').locator('.collection-name')).toHaveCount(0);\n  });\n};\n\n/**\n * Open a collection from the sidebar and accept the JavaScript Sandbox modal\n * @param page - The page object\n * @param collectionName - The name of the collection to open\n * @returns void\n */\nconst openCollection = async (page, collectionName: string) => {\n  await test.step(`Open collection \"${collectionName}\"`, async () => {\n    await page.locator('#sidebar-collection-name').filter({ hasText: collectionName }).click();\n  });\n};\n\n/**\n * Create a collection\n * @param page - The page object\n * @param collectionName - The name of the collection to create\n * @param collectionLocation - The location of the collection to create (eg)\n * @param options - The options for creating the collection\n *\n * @returns void\n */\nconst createCollection = async (page, collectionName: string, collectionLocation: string) => {\n  await test.step(`Create collection \"${collectionName}\"`, async () => {\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }).click();\n\n    // Wait for inline creator to appear, then click the cog button to open advanced modal\n    const inlineCreator = page.locator('.inline-collection-creator');\n    await inlineCreator.waitFor({ state: 'visible', timeout: 5000 });\n    await inlineCreator.locator('.cog-btn').click();\n\n    const createCollectionModal = page.locator('.bruno-modal-card').filter({ hasText: 'Create Collection' });\n    await createCollectionModal.waitFor({ state: 'visible', timeout: 5000 });\n\n    await createCollectionModal.getByLabel('Name').fill(collectionName);\n    const locationInput = createCollectionModal.getByLabel('Location');\n    if (await locationInput.isVisible()) {\n      // Location input can be read-only; drop the attribute so fill can type\n      await locationInput.evaluate((el) => {\n        const input = el as HTMLInputElement;\n        input.removeAttribute('readonly');\n        input.readOnly = false;\n      });\n      await locationInput.fill(collectionLocation);\n    }\n    await createCollectionModal.getByRole('button', { name: 'Create', exact: true }).click();\n\n    await createCollectionModal.waitFor({ state: 'detached', timeout: 15000 });\n    await page.waitForTimeout(200);\n    await openCollection(page, collectionName);\n  });\n};\n\nconst STANDARD_HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'TRACE', 'CONNECT'];\n\ntype CreateRequestOptions = {\n  url?: string;\n  method?: string;\n  inFolder?: boolean;\n};\n\ntype CreateUntitledRequestOptions = {\n  requestType?: 'HTTP' | 'GraphQL' | 'WebSocket' | 'gRPC';\n  requestName?: string;\n  url?: string;\n  tag?: string;\n};\n\n/**\n * Create an untitled request using the new dropdown flow (from tabs area)\n * @param page - The page object\n * @param options - Optional settings (requestType, url, tag)\n * @returns void\n */\nconst createUntitledRequest = async (\n  page: Page,\n  options: CreateUntitledRequestOptions = {}\n) => {\n  const { requestType = 'HTTP', url, tag } = options;\n\n  await test.step(`Create untitled ${requestType} request${url ? ' with URL' : ''}${tag ? ' with tag' : ''}`, async () => {\n    // Click the + icon to open the dropdown\n    const createButton = page.locator('.short-tab').locator('svg').first();\n    await createButton.waitFor({ state: 'visible' });\n    await createButton.click();\n\n    // Select the request type from dropdown\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: requestType }).waitFor({ state: 'visible' });\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: requestType }).click();\n\n    // Wait for the request tab to be active\n    await page.locator('.request-tab.active').waitFor({ state: 'visible' });\n    await page.waitForTimeout(300);\n\n    // Fill URL if provided\n    if (url) {\n      await page.locator('#request-url .CodeMirror').click();\n      await page.locator('#request-url textarea').fill(url);\n      await page.locator('#send-request').getByTitle('Save Request').click();\n      await page.waitForTimeout(200);\n    }\n\n    // Add tag if provided\n    if (tag) {\n      await selectRequestPaneTab(page, 'Settings');\n      await page.waitForTimeout(200);\n      const tagInput = await page.getByTestId('tag-input').getByRole('textbox');\n      await tagInput.fill(tag);\n      await tagInput.press('Enter');\n      await page.waitForTimeout(200);\n      await expect(page.locator('.tag-item', { hasText: tag })).toBeVisible();\n      await page.keyboard.press('Meta+s');\n      await page.waitForTimeout(200);\n    }\n\n    // Wait for toast message to ensure request creation is complete\n    // This helps prevent race conditions when creating multiple requests\n    await expect(page.getByText('New request created!')).toBeVisible({ timeout: 2000 }).catch(() => {\n      // Toast might have already disappeared, that's okay\n    });\n  });\n};\n\ntype CreateTransientRequestOptions = {\n  requestType?: 'HTTP' | 'GraphQL' | 'gRPC' | 'WebSocket';\n};\n\n/**\n * Create a transient request using the + icon button in the tabs area\n * Based on the CreateTransientRequest component behavior\n * @param page - The page object\n * @param options - Optional settings (requestType)\n * @returns void\n */\nconst createTransientRequest = async (\n  page: Page,\n  options: CreateTransientRequestOptions = {}\n) => {\n  const { requestType = 'HTTP' } = options;\n\n  await test.step(`Create transient ${requestType} request`, async () => {\n    // Find the + icon button (ActionIcon with aria-label=\"New Transient Request\")\n    const createButton = page.getByRole('button', { name: 'New Transient Request' });\n    await createButton.waitFor({ state: 'visible', timeout: 5000 });\n\n    // Click the + icon to open the dropdown\n    await createButton.click({\n      button: 'right'\n    });\n\n    // Wait for dropdown to be visible\n    await page.locator('.dropdown-item').first().waitFor({ state: 'visible' });\n\n    // Select the request type from dropdown\n    // The dropdown items have both icon and label, we match by the label text\n    await page.locator('.dropdown-item').filter({ hasText: requestType }).click();\n\n    // Wait for the request tab to be active (transient requests show as \"Untitled X\")\n    await page.locator('.request-tab.active').waitFor({ state: 'visible' });\n    await expect(page.locator('.request-tab.active')).toContainText('Untitled');\n    await page.waitForTimeout(300);\n  });\n};\n\n/**\n * Fill the URL field in the currently active request\n * Works with HTTP, GraphQL, gRPC, and WebSocket requests\n * @param page - The page object\n * @param url - The URL to fill\n * @returns void\n */\nconst fillRequestUrl = async (page: Page, url: string) => {\n  await test.step(`Fill request URL: ${url}`, async () => {\n    // HTTP/GraphQL requests use #request-url\n    // gRPC/WebSocket don't have a specific ID, so we need to find the CodeMirror in the active request pane\n    const httpGraphqlUrl = page.locator('#request-url .CodeMirror');\n    const grpcWsUrl = page.locator('.input-container .CodeMirror').first();\n\n    // Try HTTP/GraphQL selector first\n    const isHttpOrGraphql = await httpGraphqlUrl.isVisible().catch(() => false);\n\n    if (isHttpOrGraphql) {\n      await httpGraphqlUrl.click();\n      await page.locator('#request-url textarea').fill(url);\n    } else {\n      // Fall back to generic selector for gRPC/WebSocket\n      await grpcWsUrl.click();\n      await page.locator('.input-container textarea').first().fill(url);\n    }\n\n    await page.waitForTimeout(200);\n  });\n};\n\n/**\n * Create a request in a collection or folder\n * @param page - The page object\n * @param requestName - The name of the request to create\n * @param parentName - The name of the collection or folder\n * @param options - Optional settings (url, inFolder)\n * @returns void\n */\nconst createRequest = async (\n  page: Page,\n  requestName: string,\n  parentName: string,\n  options: CreateRequestOptions = {}\n) => {\n  const { url, method, inFolder = false } = options;\n  const parentType = inFolder ? 'folder' : 'collection';\n\n  await test.step(`Create request \"${requestName}\" in ${parentType} \"${parentName}\"`, async () => {\n    const locators = buildCommonLocators(page);\n\n    if (inFolder) {\n      await locators.sidebar.folder(parentName).hover();\n      await locators.actions.collectionItemActions(parentName).click();\n    } else {\n      await locators.sidebar.collection(parentName).hover();\n      await locators.actions.collectionActions(parentName).click();\n    }\n\n    await locators.dropdown.item('New Request').click();\n    await page.getByPlaceholder('Request Name').fill(requestName);\n\n    if (method) {\n      await page.locator('.bruno-modal .method-selector').click();\n      const isStandardMethod = STANDARD_HTTP_METHODS.includes(method.toUpperCase());\n      if (isStandardMethod) {\n        await locators.modal.newRequestMethodOption(method).click();\n      } else {\n        await locators.modal.newRequestMethodOption('add-custom').click();\n        await page.locator('.bruno-modal .method-selector input').fill(method);\n        await page.keyboard.press('Enter');\n      }\n      await page.waitForTimeout(200);\n    }\n\n    if (url) {\n      await page.locator('#new-request-url .CodeMirror').click();\n      await page.keyboard.type(url);\n    }\n\n    await locators.modal.button('Create').click();\n\n    if (inFolder) {\n      await expect(locators.sidebar.folderRequest(parentName, requestName)).toBeVisible();\n    } else {\n      await expect(locators.sidebar.request(requestName)).toBeVisible();\n    }\n  });\n};\n\n/**\n * Delete a request from a collection\n * @param page - The page object\n * @param requestName - The name of the request to delete\n * @param collectionName - The name of the collection\n * @returns void\n */\nconst deleteRequest = async (page, requestName: string, collectionName: string) => {\n  await test.step(`Delete request \"${requestName}\" from collection \"${collectionName}\"`, async () => {\n    const locators = buildCommonLocators(page);\n\n    // Click on the collection first to open it if it's closed\n    await locators.sidebar.collection(collectionName).click();\n\n    // Find the request within the collection's context\n    // Use the collection container (.collection-name) scoped to sidebar to scope the search\n    const collectionContainer = page.getByTestId('collections').locator('.collection-name').filter({ hasText: collectionName });\n    const collectionWrapper = collectionContainer.locator('..');\n    const request = collectionWrapper.locator('.collection-item-name').filter({ hasText: requestName });\n\n    await request.hover();\n    await request.locator('.menu-icon').click();\n    await locators.dropdown.item('Delete').click();\n    await locators.modal.button('Delete').click();\n    await expect(request).not.toBeVisible();\n  });\n};\n\n/**\n * Delete a collection permanently from disk via the workspace overview page\n * @param page - The page object\n * @param collectionName - The name of the collection to delete\n * @returns void\n */\nconst deleteCollectionFromOverview = async (page: Page, collectionName: string) => {\n  await test.step(`Delete collection \"${collectionName}\" from workspace overview`, async () => {\n    // Navigate to workspace overview\n    await page.locator('.home-button').click();\n    const overviewTab = page.locator('.request-tab').filter({ hasText: 'Overview' });\n    await overviewTab.click();\n\n    // Find the collection card and open its menu\n    const collectionCard = page.locator('.collection-card').filter({ hasText: collectionName });\n    await collectionCard.waitFor({ state: 'visible', timeout: 5000 });\n    await collectionCard.locator('.collection-menu').click();\n\n    // Click Delete from the dropdown\n    await page.locator('.dropdown-item').filter({ hasText: 'Delete' }).click();\n\n    // Wait for delete confirmation modal\n    const deleteModal = page.locator('.bruno-modal').filter({ hasText: 'Delete Collection' });\n    await deleteModal.waitFor({ state: 'visible', timeout: 5000 });\n\n    // Type 'delete' to confirm\n    await deleteModal.locator('#delete-confirm-input').fill('delete');\n\n    // Click the Delete button\n    await deleteModal.getByRole('button', { name: 'Delete', exact: true }).click();\n\n    // Wait for modal to close\n    await deleteModal.waitFor({ state: 'hidden', timeout: 10000 });\n  });\n};\n\n/**\n * Import a collection from a file\n * @param page - The page object\n * @param filePath - The path to the collection file to import\n * @param collectionLocation - The directory where the collection will be saved\n * @param options - Optional settings for import\n * @returns void\n */\ntype ImportCollectionOptions = {\n  expectedCollectionName?: string;\n};\n\nconst importCollection = async (\n  page: Page,\n  filePath: string,\n  collectionLocation: string,\n  options: ImportCollectionOptions = {}\n) => {\n  await test.step(`Import collection from \"${filePath}\"`, async () => {\n    const locators = buildCommonLocators(page);\n\n    await page.getByTestId('collections-header-add-menu').click();\n    await page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' }).click();\n\n    // Wait for import modal\n    const importModal = page.getByRole('dialog');\n    await importModal.waitFor({ state: 'visible' });\n    await expect(importModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n\n    // Set the file\n    await page.setInputFiles('input[type=\"file\"]', filePath);\n\n    // Wait for location modal to appear\n    const locationModal = page.locator('[data-testid=\"import-collection-location-modal\"]');\n    await locationModal.waitFor({ state: 'visible', timeout: 10000 });\n    await expect(locationModal.locator('.bruno-modal-header-title')).toContainText('Import Collection');\n\n    // Verify expected collection name if provided\n    if (options.expectedCollectionName) {\n      await expect(locationModal.getByText(options.expectedCollectionName)).toBeVisible();\n    }\n\n    // Set location and import\n    await page.locator('#collection-location').fill(collectionLocation);\n    await locationModal.getByRole('button', { name: 'Import' }).click();\n\n    // Wait for collection to appear in sidebar\n    if (options.expectedCollectionName) {\n      await expect(\n        page.locator('#sidebar-collection-name').filter({ hasText: options.expectedCollectionName })\n      ).toBeVisible();\n    }\n\n    if (options.expectedCollectionName) {\n      await openCollection(page, options.expectedCollectionName);\n    }\n  });\n};\n\n/**\n * Remove a specific collection from the sidebar\n * @param page - The page object\n * @param collectionName - The name of the collection to remove\n * @returns void\n */\nconst removeCollection = async (page: Page, collectionName: string) => {\n  await test.step(`Remove collection \"${collectionName}\"`, async () => {\n    const locators = buildCommonLocators(page);\n    const collectionRow = page.locator('.collection-name').filter({\n      has: page.locator('#sidebar-collection-name', { hasText: collectionName })\n    });\n\n    await collectionRow.hover();\n    await collectionRow.locator('.collection-actions .icon').click();\n    await locators.dropdown.item('Remove').click();\n\n    // Wait for modal to appear - could be either regular remove or drafts confirmation\n    const removeModal = page.locator('.bruno-modal').filter({ hasText: 'Remove Collection' });\n    await removeModal.waitFor({ state: 'visible', timeout: 5000 });\n\n    // Check if it's the drafts confirmation modal (has \"Discard All and Remove\" button)\n    const hasDiscardButton = await page.getByRole('button', { name: 'Discard All and Remove' }).isVisible().catch(() => false);\n\n    if (hasDiscardButton) {\n      // Drafts modal - click \"Discard All and Remove\"\n      await page.getByRole('button', { name: 'Discard All and Remove' }).click();\n    } else {\n      // Regular modal - click Remove button\n      await locators.modal.button('Remove').click();\n    }\n\n    // Wait for modal to close\n    await removeModal.waitFor({ state: 'hidden', timeout: 5000 });\n\n    // Verify collection is removed\n    await expect(\n      page.locator('#sidebar-collection-name').filter({ hasText: collectionName })\n    ).not.toBeVisible();\n  });\n};\n\n/**\n * Create a folder inside a collection or another folder\n * @param page - The page object\n * @param folderName - The name of the folder to create\n * @param parentName - The name of the parent collection or folder\n * @param isCollection - Whether the parent is a collection (true) or folder (false)\n * @returns void\n */\nconst createFolder = async (\n  page: Page,\n  folderName: string,\n  parentName: string,\n  isCollection: boolean = true\n) => {\n  await test.step(`Create folder \"${folderName}\" in \"${parentName}\"`, async () => {\n    const locators = buildCommonLocators(page);\n\n    if (isCollection) {\n      await locators.sidebar.collection(parentName).hover();\n      await locators.actions.collectionActions(parentName).click();\n    } else {\n      await locators.sidebar.folder(parentName).hover();\n      await locators.actions.collectionItemActions(parentName).click();\n    }\n\n    await locators.dropdown.item('New Folder').click();\n    await page.getByPlaceholder('Folder Name').fill(folderName);\n    await locators.modal.button('Create').click();\n    await expect(locators.sidebar.folder(folderName)).toBeVisible();\n  });\n};\n\ntype EnvironmentType = 'collection' | 'global';\n\n/**\n * Open the environment selector panel\n * @param page - The page object\n * @param type - The type of environment tab to select\n * @returns void\n */\nconst openEnvironmentSelector = async (page: Page, type: EnvironmentType = 'collection') => {\n  await test.step(`Open ${type} environment selector`, async () => {\n    const locators = buildCommonLocators(page);\n\n    await locators.environment.selector().click();\n\n    if (type === 'global') {\n      await locators.environment.globalTab().click();\n      await expect(locators.environment.globalTab()).toHaveClass(/active/);\n    } else {\n      await expect(locators.environment.collectionTab()).toHaveClass(/active/);\n    }\n  });\n};\n\n/**\n * Create a new environment\n * @param page - The page object\n * @param environmentName - The name of the environment\n * @param type - The type of environment (collection or global)\n * @returns void\n */\nconst createEnvironment = async (\n  page: Page,\n  environmentName: string,\n  type: EnvironmentType = 'collection'\n) => {\n  await test.step(`Create ${type} environment \"${environmentName}\"`, async () => {\n    await openEnvironmentSelector(page, type);\n\n    await page.locator('button[id=\"create-env\"]').click();\n\n    const nameInput = type === 'collection'\n      ? page.locator('input[name=\"name\"]')\n      : page.locator('#environment-name');\n    await expect(nameInput).toBeVisible();\n    await nameInput.fill(environmentName);\n    await page.getByRole('button', { name: 'Create' }).click();\n\n    const tabLabel = type === 'collection' ? 'Environments' : 'Global Environments';\n    await expect(page.locator('.request-tab').filter({ hasText: tabLabel })).toBeVisible();\n\n    const locators = buildCommonLocators(page);\n    await page.waitForTimeout(200); // @TODO replace with dynamic waiting logic\n    await locators.environment.selector().click();\n    if (type === 'global') {\n      await locators.environment.globalTab().click();\n    }\n    await locators.environment.envOption(environmentName).click();\n    await expect(page.locator('.current-environment')).toContainText(environmentName);\n  });\n};\n\ntype EnvironmentVariable = {\n  name: string;\n  value: string;\n  isSecret?: boolean;\n};\n\n/**\n * Add an environment variable to the currently open environment\n * @param page - The page object\n * @param variable - The variable to add (name, value, and optional secret flag)\n * @param index - The index of the variable (0-based)\n * @returns void\n */\nconst addEnvironmentVariable = async (\n  page: Page,\n  variable: EnvironmentVariable,\n  index: number\n) => {\n  await test.step(`Add environment variable \"${variable.name}\"`, async () => {\n    const nameInput = page.locator(`input[name=\"${index}.name\"]`);\n    await nameInput.waitFor({ state: 'visible' });\n    await nameInput.fill(variable.name);\n\n    // Wait for the CodeMirror editor in the row to be ready\n    const variableRow = page.locator('tr').filter({ has: page.locator(`input[name=\"${index}.name\"]`) });\n    const codeMirror = variableRow.locator('.CodeMirror');\n    await codeMirror.waitFor({ state: 'visible' });\n    await codeMirror.click();\n    await page.keyboard.type(variable.value);\n\n    if (variable.isSecret) {\n      const secretCheckbox = page.locator(`input[name=\"${index}.secret\"]`);\n      await secretCheckbox.waitFor({ state: 'visible' });\n      await secretCheckbox.check();\n    }\n  });\n};\n\n/**\n * Add multiple environment variables to the currently open environment\n * @param page - The page object\n * @param variables - Array of variables to add\n * @returns void\n */\nconst addEnvironmentVariables = async (page: Page, variables: EnvironmentVariable[]) => {\n  await test.step(`Add ${variables.length} environment variables`, async () => {\n    for (let i = 0; i < variables.length; i++) {\n      await addEnvironmentVariable(page, variables[i], i);\n    }\n  });\n};\n\n/**\n * Save the current environment settings\n * @param page - The page object\n * @returns void\n */\nconst saveEnvironment = async (page: Page) => {\n  await test.step('Save environment', async () => {\n    await page.getByRole('button', { name: 'Save' }).click();\n  });\n};\n\n/**\n * Close the environment tab\n * @param page - The page object\n * @param type - The type of environment tab to close\n * @returns void\n */\nconst closeEnvironmentPanel = async (page: Page, type: EnvironmentType = 'collection') => {\n  await test.step('Close environment tab', async () => {\n    const tabLabel = type === 'collection' ? 'Environments' : 'Global Environments';\n    const envTab = page.locator('.request-tab').filter({ hasText: tabLabel });\n    await envTab.hover();\n    await envTab.getByTestId('request-tab-close-icon').click({ force: true });\n  });\n};\n\n/**\n * Select an environment from the dropdown\n * @param page - The page object\n * @param environmentName - The name of the environment to select\n * @param type - The type of environment (collection or global)\n * @returns void\n */\nconst selectEnvironment = async (\n  page: Page,\n  environmentName: string,\n  type: EnvironmentType = 'collection'\n) => {\n  await test.step(`Select ${type} environment \"${environmentName}\"`, async () => {\n    const locators = buildCommonLocators(page);\n\n    await locators.environment.selector().click();\n\n    if (type === 'global') {\n      await locators.environment.globalTab().click();\n    }\n\n    await locators.environment.envOption(environmentName).click();\n\n    // Verify selection\n    await expect(page.locator('.current-environment')).toContainText(environmentName);\n  });\n};\n\n/**\n * Send the current request and wait for response\n * @param page - The page object\n * @param expectedStatusCode - Optional expected status code to wait for\n * @param timeout - Timeout in milliseconds (default: 30000)\n * @returns void\n */\nconst sendRequest = async (\n  page: Page,\n  expectedStatusCode?: number,\n  timeout: number = 30000\n) => {\n  await test.step('Send request', async () => {\n    await page.getByTestId('send-arrow-icon').click();\n    await page.getByTestId('response-status-code').waitFor({ state: 'visible', timeout });\n\n    if (expectedStatusCode !== undefined) {\n      await expect(page.getByTestId('response-status-code')).toContainText(\n        String(expectedStatusCode),\n        { timeout }\n      );\n    }\n  });\n};\n\n/**\n * Open a request by clicking on it in the sidebar\n * @param page - The page object\n * @param requestName - The name of the request to open\n * @returns void\n */\n// const openRequest = async (page: Page, requestName: string) => {\n//   await test.step(`Open request \"${requestName}\"`, async () => {\n//     const locators = buildCommonLocators(page);\n//     await locators.sidebar.request(requestName).click();\n//     await expect(locators.tabs.activeRequestTab()).toContainText(requestName);\n//   });\n// };\n\n/**\n* Navigate to a collection and open a request\n* @param page - The page object\n* @param collectionName - The name of the collection\n* @param requestName - The name of the request\n*/\nconst openRequest = async (page: Page, collectionName: string, requestName: string, { persist = false } = {}) => {\n  await test.step(`Navigate to collection \"${collectionName}\" and open request \"${requestName}\"`, async () => {\n    const collectionContainer = page.getByTestId('sidebar-collection-row').filter({ hasText: collectionName });\n    await collectionContainer.click();\n    const collectionWrapper = collectionContainer.locator('..');\n    const request = collectionWrapper.getByTestId('sidebar-collection-item-row').filter({ hasText: requestName });\n    if (!persist) {\n      await request.click();\n    } else {\n      await request.dblclick();\n    }\n  });\n};\n/**\n * Open a request within a folder\n * @param page - The page object\n * @param folderName - The name of the folder\n * @param requestName - The name of the request\n * @returns void\n */\nconst openFolderRequest = async (page: Page, folderName: string, requestName: string) => {\n  await test.step(`Open request \"${requestName}\" in folder \"${folderName}\"`, async () => {\n    const locators = buildCommonLocators(page);\n    await locators.sidebar.folderRequest(folderName, requestName).click();\n    await expect(locators.tabs.activeRequestTab()).toContainText(requestName);\n  });\n};\n\n/**\n* Send a request and wait for the response\n * @param page - The page object\n * @param expectedStatusCode - The expected status code (default: 200)\n * @param options - The options for sending the request (default: { timeout: 15000 })\n */\nconst sendRequestAndWaitForResponse = async (page: Page,\n  expectedStatusCode: number = 200,\n  options: {\n    ignoreCase?: boolean;\n    timeout?: number;\n    useInnerText?: boolean;\n  } = { timeout: 15000 }) => {\n  await test.step(`Send request and wait for status code ${expectedStatusCode}`, async () => {\n    await page.getByTestId('send-arrow-icon').click();\n    await expect(page.getByTestId('response-status-code')).toContainText(String(expectedStatusCode), options);\n  });\n};\n\n/**\n * Switch the response format\n * @param page - The page object\n * @param format - The format to switch to (e.g., 'JSON', 'HTML', 'XML', 'JavaScript', 'Raw', 'Hex', 'Base64')\n */\nconst switchResponseFormat = async (page: Page, format: string) => {\n  await test.step(`Switch response format to ${format}`, async () => {\n    const responseFormatTab = page.getByTestId('format-response-tab');\n    await responseFormatTab.click();\n    // Wait for dropdown to be visible before clicking the format option\n    const dropdown = page.getByTestId('format-response-tab-dropdown');\n    await dropdown.waitFor({ state: 'visible' });\n    await dropdown.getByText(format).click();\n  });\n};\n\n/**\n * Switch to the preview tab\n * @param page - The page object\n */\nconst switchToPreviewTab = async (page: Page) => {\n  await test.step('Switch to preview tab', async () => {\n    const responseFormatTab = page.getByTestId('format-response-tab');\n    await responseFormatTab.click();\n    const previewTab = page.getByTestId('preview-response-tab');\n    await previewTab.click();\n  });\n};\n\n/**\n * Switch to the editor tab\n * @param page - The page object\n */\nconst switchToEditorTab = async (page: Page) => {\n  await test.step('Switch to editor tab', async () => {\n    const responseFormatTab = page.getByTestId('format-response-tab');\n    await responseFormatTab.click();\n    const previewTab = page.getByTestId('preview-response-tab');\n    await previewTab.click();\n  });\n};\n\n/**\n * Get the response body text\n * @param page - The page object\n * @returns The response body text\n */\nconst getResponseBody = async (page: Page): Promise<string> => {\n  return await page.locator('.response-pane').innerText();\n};\n\nconst selectRequestPaneTab = async (page: Page, tabName: string) => {\n  await test.step(`Wait for request to open up \"${tabName}\"`, async () => {\n    const requestPane = page.locator('.request-pane > .px-4');\n    await expect(requestPane).toBeVisible();\n    await expect(requestPane.locator('.tabs')).toBeVisible();\n  });\n  await test.step(`Select request pane tab \"${tabName}\"`, async () => {\n    const visibleTab = page.locator('.tabs').getByRole('tab', { name: tabName });\n\n    // Check if tab is directly visible\n    if (await visibleTab.isVisible()) {\n      await visibleTab.click();\n      await expect(visibleTab).toContainClass('active');\n      return;\n    }\n\n    const overflowButton = page.locator('.tabs .more-tabs');\n    // Check if there's an overflow dropdown\n    if (await overflowButton.isVisible()) {\n      await overflowButton.click();\n\n      // Wait for dropdown to appear and click the menu item (overflow tabs are rendered as menuitems)\n      const dropdownItem = page.locator('.tippy-box .dropdown-item').filter({ hasText: tabName });\n      await dropdownItem.click();\n      await expect(visibleTab).toContainClass('active');\n      return;\n    }\n\n    // If neither found, fail with a helpful message\n    throw new Error(`Tab \"${tabName}\" not found in visible tabs or overflow dropdown`);\n  });\n};\n\n/**\n * Verify response contains specific text\n * @param page - The page object\n * @param texts - Array of texts to verify in the response\n * @returns void\n */\nconst expectResponseContains = async (page: Page, texts: string[]) => {\n  await test.step('Verify response content', async () => {\n    const responsePane = page.locator('.response-pane');\n    for (const text of texts) {\n      await expect(responsePane).toContainText(text);\n    }\n  });\n};\n\n// Map button testIds to menu item IDs\nconst buttonToMenuItemMap: Record<string, string> = {\n  'response-copy-btn': 'copy-response',\n  'response-bookmark-btn': 'save-response',\n  'response-download-btn': 'download-response',\n  'response-clear-btn': 'clear-response',\n  'response-layout-toggle-btn': 'change-layout'\n};\n\n// Click a response action - handles both visible buttons and menu items\nconst clickResponseAction = async (page: Page, actionTestId: string) => {\n  const actionButton = page.getByTestId(actionTestId).first();\n  if (await actionButton.isVisible()) {\n    await actionButton.click();\n  } else {\n    // Open the menu dropdown\n    const menu = page.getByTestId('response-actions-menu');\n    await menu.click();\n\n    // Click the corresponding menu item\n    const menuItemId = buttonToMenuItemMap[actionTestId];\n    if (menuItemId) {\n      await page.locator(`[role=\"menuitem\"][data-item-id=\"${menuItemId}\"]`).click();\n    } else {\n      throw new Error(`Unknown action testId: ${actionTestId}. Add mapping to buttonToMenuItemMap.`);\n    }\n  }\n};\n\ntype AssertionInput = {\n  expr: string;\n  value: string;\n  operator?: string;\n};\n\n/**\n * Add an assertion to the current request (adds to the last empty row)\n * @param page - The page object\n * @param assertion - The assertion to add (expr, value, optional operator)\n * @returns The row index where the assertion was added\n */\nconst addAssertion = async (page: Page, assertion: AssertionInput): Promise<number> => {\n  const operator = assertion.operator || 'eq';\n\n  return await test.step(`Add assertion: ${assertion.expr} ${operator} ${assertion.value}`, async () => {\n    const locators = buildCommonLocators(page);\n    const table = locators.assertionsTable();\n\n    // Ensure assertions table is visible\n    await expect(table.container()).toBeVisible();\n\n    // Find the last row (which is the empty row for adding new assertions)\n    const rowCount = await table.allRows().count();\n    const targetRowIndex = rowCount - 1; // Last row is the empty row\n\n    // Wait for the row to exist\n    await expect(table.row(targetRowIndex)).toBeVisible();\n\n    // Fill in the expression\n    const exprInput = table.rowExprInput(targetRowIndex);\n    await expect(exprInput).toBeVisible({ timeout: 2000 });\n    await exprInput.click();\n    await page.keyboard.type(assertion.expr);\n\n    // The component creates a new empty row when the key field is filled\n    await expect(table.allRows()).toHaveCount(rowCount + 1);\n\n    // Fill in the value first (defaults to 'eq value')\n    const valueInput = table.rowValueInput(targetRowIndex);\n    await valueInput.click();\n    await page.keyboard.type(assertion.value);\n\n    // Select the operator from dropdown (if provided and not default 'eq')\n    // This will update the value field to combine operator + value\n    if (assertion.operator && assertion.operator !== 'eq') {\n      const operatorSelect = table.rowOperatorSelect(targetRowIndex);\n      await operatorSelect.selectOption(assertion.operator);\n    }\n\n    // Wait for the assertion to be fully processed\n    // Verify the expression was actually saved by checking the input value\n    const exprInputAfter = table.rowExprInput(targetRowIndex);\n    await expect(exprInputAfter).toHaveValue(assertion.expr, { timeout: 2000 });\n\n    return targetRowIndex;\n  });\n};\n\n/**\n * Edit an assertion at a specific row index\n * @param page - The page object\n * @param rowIndex - The row index of the assertion to edit\n * @param assertion - The assertion data to update (expr, value, optional operator)\n * @returns void\n */\nconst editAssertion = async (page: Page, rowIndex: number, assertion: AssertionInput) => {\n  const operator = assertion.operator || 'eq';\n\n  await test.step(`Edit assertion at row ${rowIndex}: ${assertion.expr} ${operator} ${assertion.value}`, async () => {\n    const locators = buildCommonLocators(page);\n    const table = locators.assertionsTable();\n\n    // Ensure assertions table is visible\n    await expect(table.container()).toBeVisible();\n\n    // Wait for the row to exist\n    await expect(table.row(rowIndex)).toBeVisible();\n\n    // Update the expression\n    const exprInput = table.rowExprInput(rowIndex);\n    await expect(exprInput).toBeVisible({ timeout: 2000 });\n    await exprInput.click();\n    // Clear the input and type new value - use triple-click to select all (works cross-platform)\n    await exprInput.click({ clickCount: 3 });\n    await page.keyboard.press('Backspace'); // Clear selection\n    await page.keyboard.type(assertion.expr);\n\n    // Update the operator from dropdown (if provided)\n    if (assertion.operator) {\n      const operatorSelect = table.rowOperatorSelect(rowIndex);\n      await operatorSelect.selectOption(assertion.operator);\n    }\n\n    // Update the value (just the value, operator is already selected)\n    // The value cell contains a SingleLineEditor, so we need to click and type\n    const valueInput = table.rowValueInput(rowIndex);\n    await valueInput.click({ clickCount: 3 });\n    await page.keyboard.press('Backspace'); // Clear selection\n    await page.keyboard.type(assertion.value);\n  });\n};\n\n/**\n * Delete an assertion from the current request by row index\n * @param page - The page object\n * @param rowIndex - The row index of the assertion to delete\n * @returns void\n */\nconst deleteAssertion = async (page: Page, rowIndex: number) => {\n  await test.step(`Delete assertion at row ${rowIndex}`, async () => {\n    const locators = buildCommonLocators(page);\n    const table = locators.assertionsTable();\n\n    await expect(table.container()).toBeVisible();\n\n    const initialRowCount = await table.allRows().count();\n    const deleteButton = table.rowDeleteButton(rowIndex);\n\n    await deleteButton.click();\n    await expect(table.allRows()).toHaveCount(initialRowCount - 1);\n  });\n};\n\n/**\n * Save the current request and verify success toast\n * @param page - The page object\n * @returns void\n */\nconst saveRequest = async (page: Page) => {\n  await test.step('Save request', async () => {\n    await page.keyboard.press('Meta+s');\n    await expect(page.getByText('Request saved successfully').last()).toBeVisible({ timeout: 3000 });\n    await page.waitForTimeout(200);\n  });\n};\n\n/**\n * Close all open request tabs using the right-click context menu\n * @param page - The page object\n * @returns void\n */\nconst closeAllTabs = async (page: Page) => {\n  await test.step('Close all tabs', async () => {\n    // Find actual request tabs (those with .tab-method, not Overview/Environments)\n    const requestTabLabel = page.locator('.request-tab').filter({ has: page.locator('.tab-method') }).locator('.tab-label').first();\n    if (!(await requestTabLabel.isVisible().catch(() => false))) {\n      return; // No request tabs to close\n    }\n\n    // Right-click on the tab label to open context menu\n    await requestTabLabel.click({ button: 'right' });\n\n    // Wait for the dropdown menu to appear\n    const dropdown = page.locator('.tippy-box.dropdown');\n    await dropdown.waitFor({ state: 'visible', timeout: 5000 });\n\n    // Click \"Close All\" menu item\n    await dropdown.locator('[role=\"menuitem\"][data-item-id=\"close-all\"]').click();\n\n    // Handle \"Unsaved Transient Requests\" modal if it appears\n    const discardAllButton = page.getByRole('button', { name: 'Discard All' });\n    if (await discardAllButton.isVisible({ timeout: 1000 }).catch(() => false)) {\n      await discardAllButton.click();\n    }\n  });\n};\n\nexport {\n  closeAllCollections,\n  openCollection,\n  createCollection,\n  createRequest,\n  createUntitledRequest,\n  createTransientRequest,\n  fillRequestUrl,\n  deleteRequest,\n  deleteCollectionFromOverview,\n  importCollection,\n  removeCollection,\n  createFolder,\n  openEnvironmentSelector,\n  createEnvironment,\n  addEnvironmentVariable,\n  addEnvironmentVariables,\n  saveEnvironment,\n  closeEnvironmentPanel,\n  selectEnvironment,\n  sendRequest,\n  openRequest,\n  openFolderRequest,\n  getResponseBody,\n  expectResponseContains,\n  selectRequestPaneTab,\n  sendRequestAndWaitForResponse,\n  switchResponseFormat,\n  switchToPreviewTab,\n  switchToEditorTab,\n  clickResponseAction,\n  addAssertion,\n  editAssertion,\n  deleteAssertion,\n  saveRequest,\n  closeAllTabs\n};\n\nexport type { SandboxMode, EnvironmentType, EnvironmentVariable, ImportCollectionOptions, CreateRequestOptions, CreateUntitledRequestOptions, CreateTransientRequestOptions, AssertionInput };\n"
  },
  {
    "path": "tests/utils/page/index.ts",
    "content": "export * from './actions';\nexport * from './runner';\nexport * from './locators';\n"
  },
  {
    "path": "tests/utils/page/locators.ts",
    "content": "import { Page } from '../../../playwright';\n\nexport const buildCommonLocators = (page: Page) => ({\n  runner: () => page.getByTestId('run-button'),\n  saveButton: () => page\n    .locator('.infotip')\n    .filter({ hasText: /^Save/ }),\n  sidebar: {\n    collectionsContainer: () => page.getByTestId('collections'),\n    collection: (name: string) => page.locator('#sidebar-collection-name').filter({ hasText: name }),\n    folder: (name: string) => page.locator('.collection-item-name').filter({ hasText: name }),\n    request: (name: string) => page.locator('.collection-item-name').filter({ hasText: name }),\n    folderRequest: (folderName: string, requestName: string) => {\n      // Find the folder's collection-item-name, then navigate to its parent wrapper container (StyledWrapper),\n      // and search for the request within that container's descendants.\n      // Using .locator('..') gets the parent element of the folder's collection-item-name div.\n      const folderWrapper = page.locator('.collection-item-name').filter({ hasText: folderName }).locator('..');\n      return folderWrapper.locator('.collection-item-name').filter({ hasText: requestName });\n    },\n    closeAllCollectionsButton: () => page.getByTestId('collections-header-actions-menu-close-all'),\n    collectionRow: (name: string) => page.locator('.collection-name').filter({\n      has: page.locator('#sidebar-collection-name', { hasText: name })\n    })\n  },\n  actions: {\n    collectionActions: (collectionName: string) =>\n      page.getByTestId('collections').locator('.collection-name')\n        .filter({ hasText: collectionName })\n        .locator('.collection-actions .icon'),\n    collectionItemActions: (itemName: string) =>\n      page.locator('.collection-item-name')\n        .filter({ hasText: itemName })\n        .locator('.menu-icon')\n  },\n  dropdown: {\n    item: (text: string) => page.locator('.dropdown-item').filter({ hasText: text }),\n    tippyItem: (text: string) => page.locator('.tippy-box .dropdown-item').filter({ hasText: text })\n  },\n  tabs: {\n    requestTab: (requestName: string) => page.locator('.request-tab .tab-label').filter({ hasText: requestName }),\n    activeRequestTab: () => page.locator('.request-tab.active'),\n    closeTab: (requestName: string) => page.locator('.request-tab').filter({ hasText: requestName }).getByTestId('request-tab-close-icon')\n  },\n  folder: {\n    chevron: (folderName: string) => page.locator('.collection-item-name').filter({ hasText: folderName }).getByTestId('folder-chevron')\n  },\n  modal: {\n    title: (title: string) => page.locator('.bruno-modal-header-title').filter({ hasText: title }),\n    byTitle: (title: string) => page.locator('.bruno-modal').filter({ has: page.locator('.bruno-modal-header-title').filter({ hasText: title }) }),\n    button: (name: string) => page.locator('.bruno-modal').getByRole('button', { name: name, exact: true }),\n    closeButton: () => page.locator('.bruno-modal').getByTestId('modal-close-button'),\n    card: () => page.locator('.bruno-modal-card'),\n    footer: () => page.locator('.bruno-modal-footer'),\n    submitButton: () => page.locator('.bruno-modal-footer .submit'),\n    newRequestMethodOption: (id: string) => page.getByTestId(`method-selector-${id.toLowerCase()}`)\n  },\n  environment: {\n    selector: () => page.getByTestId('environment-selector-trigger'),\n    collectionTab: () => page.getByTestId('env-tab-collection'),\n    globalTab: () => page.getByTestId('env-tab-global'),\n    envOption: (name: string) => page.locator('.dropdown-item').getByText(name, { exact: true }),\n    currentEnvironment: () => page.locator('.current-environment'),\n    addVariableButton: () => page.locator('button[data-testid=\"add-variable\"]'),\n    variableNameInput: (index: number) => page.locator(`input[name=\"${index}.name\"]`),\n    variableSecretCheckbox: (index: number) => page.locator(`input[name=\"${index}.secret\"]`),\n    variableRow: (index: number) => page.locator('tr').filter({ has: page.locator(`input[name=\"${index}.name\"]`) }),\n    createEnvButton: () => page.locator('button[id=\"create-env\"]'),\n    envNameInput: () => page.locator('input[name=\"name\"]')\n  },\n  request: {\n    urlInput: () => page.locator('#request-url .CodeMirror'),\n    urlLine: () => page.locator('#request-url .CodeMirror-line'),\n    sendButton: () => page.getByTestId('send-arrow-icon'),\n    methodDropdown: () => page.getByTestId('request-method-selector'),\n    newRequestUrl: () => page.locator('#new-request-url .CodeMirror'),\n    requestNameInput: () => page.getByPlaceholder('Request Name'),\n    requestTestId: () => page.getByTestId('request-name'),\n    generateCodeButton: () => page.locator('#send-request .infotip').first(),\n    bodyModeSelector: () => page.getByTestId('request-body-mode-selector'),\n    bodyEditor: () => page.getByTestId('request-body-editor')\n  },\n  tags: {\n    input: () => page.getByTestId('tag-input').getByRole('textbox'),\n    item: (tagName: string) => page.locator('.tag-item', { hasText: tagName })\n  },\n  response: {\n    statusCode: () => page.getByTestId('response-status-code'),\n    pane: () => page.locator('.response-pane'),\n    copyButton: () => page.locator('button[title=\"Copy response to clipboard\"]'),\n    body: () => page.locator('.response-pane'),\n    editorContainer: () => page.locator('.response-pane .editor-container'),\n    formatTab: () => page.getByTestId('format-response-tab'),\n    formatTabDropdown: () => page.getByTestId('format-response-tab-dropdown'),\n    previewContainer: () => page.getByTestId('response-preview-container'),\n    previewContainerCodeMirror: () => page.getByTestId('response-preview-container').locator('.CodeMirror').first(),\n    codeLine: () => page.locator('.response-pane .editor-container .CodeMirror-line'),\n    jsonTreeLine: () => page.locator('.response-pane .object-content')\n  },\n  plusMenu: {\n    button: () => page.getByTestId('collections-header-add-menu'),\n    createCollection: () => page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Create collection' }),\n    importCollection: () => page.locator('.tippy-box .dropdown-item').filter({ hasText: 'Import collection' })\n  },\n  import: {\n    modal: () => page.locator('[data-testid=\"import-collection-modal\"]'),\n    locationModal: () => page.locator('[data-testid=\"import-collection-location-modal\"]'),\n    locationInput: () => page.locator('#collection-location'),\n    fileInput: () => page.locator('input[type=\"file\"]'),\n    envOption: (name: string) => page.locator('.dropdown-item').getByText(name, { exact: true })\n  },\n  /**\n   * Build generic table locators for any table with a testId\n   * @param testId - The testId of the table\n   * @returns Table locators object\n   */\n  table: (testId: string) => {\n    const container = () => page.getByTestId(testId);\n    const getBodyRow = (index?: number) => {\n      const locator = container().locator('tbody tr');\n      return index !== undefined ? locator.nth(index) : locator;\n    };\n\n    return {\n      container,\n      row: (index?: number) => getBodyRow(index),\n      rowCell: (columnKey: string, rowIndex?: number) => {\n        const row = getBodyRow(rowIndex);\n        return row.getByTestId(`column-${columnKey}`);\n      },\n      rowCheckbox: (rowIndex: number) => getBodyRow(rowIndex).getByTestId('column-checkbox'),\n      rowDeleteButton: (rowIndex: number) => getBodyRow(rowIndex).getByTestId('column-delete'),\n      allRows: () => container().locator('tbody tr')\n    };\n  },\n  /**\n   * Assertions table locators (extends generic table with assertion-specific helpers)\n   * @returns Assertions table locators object\n   */\n  assertionsTable: () => {\n    const baseTable = buildCommonLocators(page).table('assertions-table');\n    return {\n      ...baseTable,\n      // Assertion-specific helpers\n      rowExprInput: (rowIndex: number) => {\n        const cell = baseTable.rowCell('name', rowIndex);\n        // Wait for the cell to be visible, then find the textbox\n        return cell.getByRole('textbox').or(cell.locator('input[type=\"text\"]'));\n      },\n      rowOperatorSelect: (rowIndex: number) => {\n        const cell = baseTable.rowCell('operator', rowIndex);\n        return cell.getByTestId('assertion-operator-select').or(cell.locator('select'));\n      },\n      rowValueInput: (rowIndex: number) => baseTable.rowCell('value', rowIndex)\n    };\n  }\n});\n\nexport const buildWebsocketCommonLocators = (page: Page) => ({\n  ...buildCommonLocators(page),\n  connectionControls: {\n    connect: () =>\n      page\n        .locator('div.connection-controls')\n        .locator('.infotip')\n        .filter({ hasText: /^Connect$/ }),\n    disconnect: () =>\n      page\n        .locator('div.connection-controls')\n        .locator('.infotip')\n        .filter({ hasText: /^Close Connection$/ })\n  },\n  messages: () => page.locator('.ws-message'),\n  toolbar: {\n    latestFirst: () => page.getByRole('button', { name: 'Latest First' }),\n    latestLast: () => page.getByRole('button', { name: 'Latest Last' }),\n    clearResponse: () => page.getByTestId('response-clear-btn')\n  }\n});\n\nexport const getTableCell = (row, index) => row.locator('td').nth(index + 1);\n\nexport const buildGrpcCommonLocators = (page: Page) => ({\n  ...buildCommonLocators(page),\n  method: {\n    dropdownTrigger: () => page.getByTestId('grpc-method-dropdown-trigger'),\n    indicator: () => page.getByTestId('grpc-method-indicator')\n  },\n  request: {\n    queryUrlContainer: () => page.getByTestId('grpc-query-url-container'),\n    sendButton: () => page.getByTestId('grpc-send-request-button'),\n    messagesContainer: () => page.getByTestId('grpc-messages-container'),\n    addMessageButton: () => page.getByTestId('grpc-add-message-button'),\n    sendMessage: (index: number) => page.getByTestId(`grpc-send-message-${index}`),\n    endConnectionButton: () => page.getByTestId('grpc-end-connection-button'),\n    cancelConnectionButton: () => page.getByTestId('grpc-cancel-connection-button')\n  },\n  response: {\n    statusCode: () => page.getByTestId('grpc-response-status-code'),\n    statusText: () => page.getByTestId('grpc-response-status-text'),\n    content: () => page.getByTestId('grpc-response-content'),\n    container: () => page.getByTestId('grpc-responses-container'),\n    singleResponse: () => page.getByTestId('grpc-single-response'),\n    list: () => page.getByTestId('grpc-responses-list'),\n    responseItem: (index: number) => page.getByTestId(`grpc-response-item-${index}`),\n    responseItems: () => page.locator('[data-testid^=\"grpc-response-item-\"]'),\n    tabCount: () => page.getByRole('tab', { name: 'Response' }).getByTestId('grpc-tab-response-count')\n  }\n});\n\n/**\n * Builds locators for sandbox mode settings\n * @param page - The Playwright page object\n * @returns Object with locators for sandbox elements\n */\nexport const buildSandboxLocators = (page: Page) => ({\n  sandboxModeSelector: () => page.getByTestId('sandbox-mode-selector'),\n  safeModeRadio: () => page.getByTestId('sandbox-mode-safe'),\n  developerModeRadio: () => page.getByTestId('sandbox-mode-developer'),\n  jsSandboxHeading: () => page.getByText('JavaScript Sandbox'),\n  saveButton: () => page.getByRole('button', { name: 'Save' })\n});\n"
  },
  {
    "path": "tests/utils/page/navigation.ts",
    "content": ""
  },
  {
    "path": "tests/utils/page/runner.ts",
    "content": "import { Page, expect, test } from '../../../playwright';\nimport { buildSandboxLocators } from './locators';\n\n/**\n * Builds locators for the runner results view\n * @param page - The Playwright page object\n * @returns Object with locators for runner elements\n */\nexport const buildRunnerLocators = (page: Page) => ({\n  allButton: () => page.locator('button').filter({ hasText: /^All/ }),\n  passedButton: () => page.locator('button').filter({ hasText: /^Passed/ }),\n  failedButton: () => page.locator('button').filter({ hasText: /^Failed/ }),\n  skippedButton: () => page.locator('button').filter({ hasText: /^Skipped/ }),\n  resetButton: () => page.getByRole('button', { name: 'Reset' }),\n  runCollectionButton: () => page.getByRole('button', { name: 'Run Collection' }),\n  runAgainButton: () => page.getByRole('button', { name: 'Run Again' })\n});\n\n/**\n * Reads test result counts from the filter buttons in the runner results view\n * @param page - The Playwright page object\n * @returns An object with totalRequests, passed, failed, and skipped counts\n */\nexport const getRunnerResultCounts = async (page: Page) => {\n  const locators = buildRunnerLocators(page);\n\n  const totalRequests = parseInt(await locators.allButton().locator('span').innerText());\n  const passed = parseInt(await locators.passedButton().locator('span').innerText());\n  const failed = parseInt(await locators.failedButton().locator('span').innerText());\n  const skipped = parseInt(await locators.skippedButton().locator('span').innerText());\n\n  return { totalRequests, passed, failed, skipped };\n};\n\n/**\n * Runs a collection by clicking the Run menu item and handling the runner tab\n * Includes logic to reset existing results if present\n * @param page - The Playwright page object\n * @param collectionName - The name of the collection to run\n * @returns void\n */\nexport const runCollection = async (page: Page, collectionName: string) => {\n  await test.step(`Run collection \"${collectionName}\"`, async () => {\n    // Ensure collection is visible and loaded (scope to sidebar)\n    const collectionContainer = page.getByTestId('collections').locator('.collection-name').filter({ hasText: collectionName });\n    await collectionContainer.waitFor({ state: 'visible' });\n\n    // Open collection actions menu - hover first to reveal the hidden actions button\n    const actionsContainer = collectionContainer.locator('.collection-actions');\n    await collectionContainer.hover();\n    await actionsContainer.waitFor({ state: 'visible' });\n\n    const icon = actionsContainer.locator('.icon');\n    await icon.waitFor({ state: 'visible', timeout: 5000 });\n    await icon.click();\n\n    // Click Run menu item\n    const runMenuItem = page.getByText('Run', { exact: true });\n    await runMenuItem.waitFor({ state: 'visible' });\n    await runMenuItem.click();\n\n    // Handle runner tab - reset if needed, then run\n    const locators = buildRunnerLocators(page);\n\n    // Check if Reset button is visible (means there are existing results)\n    const resetVisible = await locators.resetButton().isVisible({ timeout: 1000 }).catch(() => false);\n    if (resetVisible) {\n      await locators.resetButton().click();\n      // Wait for the Run Collection button to become visible after reset\n      await locators.runCollectionButton().waitFor({ state: 'visible', timeout: 5000 });\n    }\n\n    // Now wait for and click Run Collection button\n    await locators.runCollectionButton().waitFor({ state: 'visible', timeout: 10000 });\n    await locators.runCollectionButton().click();\n\n    // Wait for the run to complete\n    await locators.runAgainButton().waitFor({ timeout: 2 * 60 * 1000 });\n  });\n};\n\n/**\n * Sets up the JavaScript sandbox mode for a collection\n * @param page - The Playwright page object\n * @param collectionName - The name of the collection (can be title or text)\n * @param mode - 'developer' or 'safe' mode\n * @returns void\n */\nexport const setSandboxMode = async (page: Page, collectionName: string, mode: 'developer' | 'safe') => {\n  await test.step(`Set sandbox mode to \"${mode}\" for \"${collectionName}\"`, async () => {\n    const sandboxLocators = buildSandboxLocators(page);\n\n    // Click on the collection name in the sidebar\n    const sidebarCollection = page.getByTestId('collections').locator('#sidebar-collection-name').filter({ hasText: collectionName }).first();\n    await sidebarCollection.waitFor({ state: 'visible' });\n    await sidebarCollection.click();\n\n    // Check if there's already a mode selected - if so, we need to click the badge to open settings tab\n    const sandboxBadgeVisible = await sandboxLocators.sandboxModeSelector().isVisible().catch(() => false);\n    // If a badge exists, click it to open the security settings tab\n    if (sandboxBadgeVisible) {\n      await sandboxLocators.sandboxModeSelector().click();\n\n      // Wait for the security settings tab to be active\n      await sandboxLocators.jsSandboxHeading().waitFor({ state: 'visible', timeout: 10000 });\n    }\n    // If no badge exists, the modal should have appeared automatically (first time selection)\n\n    // Wait for security settings form to be visible - wait for either radio button\n    await Promise.race([\n      sandboxLocators.safeModeRadio().waitFor({ state: 'visible', timeout: 10000 }).catch(() => {}),\n      sandboxLocators.developerModeRadio().waitFor({ state: 'visible', timeout: 10000 }).catch(() => {})\n    ]);\n\n    if (mode === 'developer') {\n      await sandboxLocators.developerModeRadio().waitFor({ state: 'visible', timeout: 5000 });\n      await sandboxLocators.developerModeRadio().click();\n    } else {\n      await sandboxLocators.safeModeRadio().waitFor({ state: 'visible', timeout: 5000 });\n      await sandboxLocators.safeModeRadio().click();\n    }\n\n    await page.keyboard.press('Escape');\n  });\n};\n\n/**\n * Validates runner results against expected counts\n * @param page - The Playwright page object\n * @param expected - Expected counts\n * @returns void\n */\nexport const validateRunnerResults = async (page: Page,\n  expected: {\n    totalRequests?: number;\n    passed?: number;\n    failed?: number;\n    skipped?: number;\n  }) => {\n  const { totalRequests, passed, failed, skipped } = await getRunnerResultCounts(page);\n\n  if (expected.totalRequests !== undefined) {\n    await expect(totalRequests).toBe(expected.totalRequests);\n  }\n  if (expected.passed !== undefined) {\n    await expect(passed).toBe(expected.passed);\n  }\n  if (expected.failed !== undefined) {\n    await expect(failed).toBe(expected.failed);\n  }\n  if (expected.skipped !== undefined) {\n    await expect(skipped).toBe(expected.skipped);\n  }\n\n  // Validate that passed + failed + skipped = totalRequests\n  await expect(passed).toBe(totalRequests - skipped - failed);\n};\n"
  },
  {
    "path": "tests/utils/wait.ts",
    "content": "import { setTimeout } from 'timers/promises';\n\n// TODO: reaper Might not be necessary, figure out a better way later\nexport const waitForPredicate = async (predicate: () => Promise<boolean>, { tries = 10, interval = 100 } = {}) => {\n  let result;\n  let retries = tries;\n  do {\n    result = await predicate();\n    retries -= 1;\n    await setTimeout(interval);\n  } while (!result && retries > 0);\n  return result;\n};\n"
  },
  {
    "path": "tests/variable-tooltip/variable-tooltip.spec.ts",
    "content": "import { test, expect } from '../../playwright';\nimport {\n  createCollection,\n  closeAllCollections,\n  createRequest,\n  createEnvironment,\n  addEnvironmentVariables,\n  saveEnvironment,\n  selectRequestPaneTab,\n  closeEnvironmentPanel\n} from '../utils/page';\nimport { buildCommonLocators } from '../utils/page/locators';\n\ntest.describe('Variable Tooltip', () => {\n  test.afterEach(async ({ page }) => {\n    if (!page.isClosed()) {\n      await closeAllCollections(page);\n    }\n  });\n\n  test('should test tooltip functionality with environment variables', async ({ page, createTmpDir }) => {\n    const collectionName = 'tooltip-test';\n\n    await test.step('Create collection and add environment variables', async () => {\n      await createCollection(page, collectionName, await createTmpDir('tooltip-collection'));\n\n      await createEnvironment(page, 'Test Env', 'collection');\n\n      await addEnvironmentVariables(page, [\n        { name: 'apiKey', value: 'test-key-123' },\n        { name: 'secretToken', value: 'secret-xyz', isSecret: true }\n      ]);\n\n      await saveEnvironment(page);\n      await closeEnvironmentPanel(page);\n    });\n\n    await test.step('Create request and test tooltip', async () => {\n      // Create request using utility method\n      await createRequest(page, 'Test Request', collectionName);\n\n      // Set the URL\n      await page.locator('.collection-item-name').filter({ hasText: 'Test Request' }).click();\n      const urlEditor = page.locator('#request-url .CodeMirror');\n      await urlEditor.click();\n      await page.keyboard.type('https://api.example.com?key={{apiKey}}');\n      await page.keyboard.press('Control+s');\n    });\n\n    await test.step('Test basic tooltip', async () => {\n      const urlEditor = page.locator('#request-url .CodeMirror');\n      const apiKeyVar = urlEditor.locator('.cm-variable-valid').filter({ hasText: 'apiKey' }).first();\n\n      await apiKeyVar.hover();\n\n      const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();\n      await expect(tooltip).toBeVisible();\n      await expect(tooltip.locator('.var-name')).toContainText('apiKey');\n      await expect(tooltip.locator('.var-scope-badge')).toContainText('Environment');\n      await expect(tooltip.locator('.var-value-editable-display')).toContainText('test-key-123');\n      await expect(tooltip.locator('.copy-button')).toBeVisible();\n    });\n\n    await test.step('Test secret variable with toggle', async () => {\n      await page.mouse.move(0, 0);\n\n      await selectRequestPaneTab(page, 'Headers');\n\n      const headerTable = page.locator('table').first();\n      const headerRow = headerTable.locator('tbody tr').first();\n\n      const headerNameEditor = headerRow.locator('.CodeMirror').first();\n      await headerNameEditor.click();\n      await page.keyboard.type('Authorization');\n\n      const headerValueEditor = headerRow.locator('.CodeMirror').nth(1);\n      await headerValueEditor.click();\n      await page.keyboard.type('Bearer {{secretToken}}');\n      await page.keyboard.press('Control+s');\n\n      // Test tooltip with secret\n      const secretVar = headerValueEditor.locator('.cm-variable-valid').filter({ hasText: 'secretToken' }).first();\n      await secretVar.hover();\n\n      const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();\n      await expect(tooltip).toBeVisible();\n\n      // Verify masked\n      const valueDisplay = tooltip.locator('.var-value-editable-display');\n      const maskedText = await valueDisplay.textContent();\n      // Check that value is masked (contains bullet points and not the actual value)\n      expect(maskedText).not.toContain('secret-xyz');\n      expect(maskedText?.length).toBeGreaterThan(0);\n\n      // Test toggle\n      const toggleButton = tooltip.locator('.secret-toggle-button');\n      await expect(toggleButton).toBeVisible();\n      await toggleButton.click();\n      await expect(valueDisplay).toContainText('secret-xyz');\n\n      // Toggle back\n      await toggleButton.click();\n      const remaskedText = await valueDisplay.textContent();\n      expect(remaskedText).not.toContain('secret-xyz');\n      expect(remaskedText?.length).toBeGreaterThan(0);\n    });\n  });\n\n  test('should test tooltip with variable references', async ({ page, createTmpDir }) => {\n    const collectionName = 'tooltip-reference-test';\n\n    await test.step('Create collection with interdependent variables', async () => {\n      await createCollection(page, collectionName, await createTmpDir('tooltip-ref-collection'));\n\n      await createEnvironment(page, 'Ref Test Env', 'collection');\n\n      await addEnvironmentVariables(page, [\n        { name: 'host', value: 'api.example.com' },\n        { name: 'endpoint', value: 'https://{{host}}/users' }\n      ]);\n\n      await saveEnvironment(page);\n      await closeEnvironmentPanel(page);\n    });\n\n    await test.step('Create request with variable references', async () => {\n      // Create request using utility method\n      await createRequest(page, 'Ref Test Request', collectionName);\n\n      // Set the URL\n      await page.locator('.collection-item-name').filter({ hasText: 'Ref Test Request' }).click();\n      const urlEditor = page.locator('#request-url .CodeMirror');\n      await urlEditor.click();\n      await page.keyboard.type('{{endpoint}}');\n      await page.keyboard.press('Control+s');\n    });\n\n    await test.step('Test variable referencing other variables', async () => {\n      const urlEditor = page.locator('#request-url .CodeMirror');\n      const endpointVar = urlEditor.locator('.cm-variable-valid').filter({ hasText: 'endpoint' }).first();\n\n      await endpointVar.hover();\n\n      const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();\n      await expect(tooltip).toBeVisible();\n      await expect(tooltip.locator('.var-name')).toContainText('endpoint');\n\n      // Should show resolved value\n      await expect(tooltip.locator('.var-value-editable-display')).toContainText('https://api.example.com/users');\n\n      // Should have copy button\n      await expect(tooltip.locator('.copy-button')).toBeVisible();\n    });\n\n    await test.step('Test editing variable with references', async () => {\n      // Move mouse away to dismiss any active tooltip\n      await page.mouse.move(0, 0);\n\n      // URL editor is always visible at the top\n      const urlEditor = page.locator('#request-url .CodeMirror');\n      const endpointVar = urlEditor.locator('.cm-variable-valid').filter({ hasText: 'endpoint' }).first();\n\n      await endpointVar.hover();\n\n      const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();\n      await expect(tooltip).toBeVisible();\n\n      // Click on value to edit\n      const valueDisplay = tooltip.locator('.var-value-editable-display');\n      await valueDisplay.click();\n\n      // Should show editor with raw value (not resolved)\n      const editor = tooltip.locator('.var-value-editor .CodeMirror');\n      await expect(editor).toBeVisible();\n\n      // Verify it shows the raw value with variable references\n      // focus on the editor\n      const editorContent = await editor.locator('.CodeMirror-line').textContent();\n      expect(editorContent).toContain('{{host}}');\n\n      // Edit the value\n      await page.keyboard.press('End');\n      await page.keyboard.type('/posts');\n\n      // Click outside to save\n      await page.locator('body').click();\n\n      // Move mouse away and back to get fresh tooltip\n      await page.mouse.move(0, 0);\n\n      // Hover again to verify the change\n      await endpointVar.hover();\n\n      const newTooltip = page.locator('.CodeMirror-brunoVarInfo').first();\n      await expect(newTooltip).toBeVisible();\n\n      // Should show updated resolved value\n      await expect(newTooltip.locator('.var-value-editable-display')).toContainText('https://api.example.com/users/posts');\n    });\n\n    await test.step('Test copy button', async () => {\n      // Move mouse away to dismiss any active tooltip\n      await page.mouse.move(0, 0);\n\n      const urlEditor = page.locator('#request-url .CodeMirror');\n      const endpointVar = urlEditor.locator('.cm-variable-valid').filter({ hasText: 'endpoint' }).first();\n\n      await endpointVar.hover();\n\n      const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();\n      await expect(tooltip).toBeVisible();\n\n      const copyButton = tooltip.locator('.copy-button');\n      await expect(copyButton).toBeVisible();\n\n      // Click copy button\n      await copyButton.click();\n\n      // Should show success state (checkmark)\n      await expect(copyButton.locator('svg polyline')).toBeVisible({ timeout: 1000 });\n\n      // Wait for it to revert back to copy icon\n      await expect(copyButton.locator('svg rect')).toBeVisible();\n    });\n  });\n\n  test('should handle runtime and process.env variables', async ({ page, createTmpDir }) => {\n    const collectionName = 'tooltip-readonly-test';\n\n    await test.step('Create collection and request', async () => {\n      await createCollection(page, collectionName, await createTmpDir('tooltip-readonly-collection'));\n\n      await createEnvironment(page, 'Readonly Env', 'collection');\n      await saveEnvironment(page);\n      await closeEnvironmentPanel(page);\n\n      // Create request using utility method\n      await createRequest(page, 'Readonly Test', collectionName);\n\n      // Set the URL\n      const locators = buildCommonLocators(page);\n      await locators.sidebar.request('Readonly Test').click();\n      const urlEditor = locators.request.urlInput();\n      await urlEditor.click();\n      await page.keyboard.type('https://example.com');\n      await page.keyboard.press('Control+s');\n    });\n\n    await test.step('Test process.env variable tooltip', async () => {\n      // Move mouse away to dismiss any active tooltip\n      await page.mouse.move(0, 0);\n\n      // Add a process.env variable in URL (URL editor is always visible at the top)\n      const urlEditor = page.locator('#request-url .CodeMirror');\n      await urlEditor.click();\n      await page.keyboard.press('End');\n      await page.keyboard.type('?env={{process.env.HOME}}');\n      await page.keyboard.press('Control+s');\n\n      // Hover over process.env variable\n      const processEnvVar = urlEditor.locator('.cm-variable-valid, .cm-variable-invalid').filter({ hasText: 'process.env.HOME' }).first();\n      await processEnvVar.hover();\n\n      const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();\n      await expect(tooltip).toBeVisible();\n      await expect(tooltip.locator('.var-name')).toContainText('process.env.HOME');\n      await expect(tooltip.locator('.var-scope-badge')).toContainText('Process Env');\n\n      // Should show read-only note\n      await expect(tooltip.locator('.var-readonly-note')).toContainText('read-only');\n\n      // Should have copy button but not be editable\n      await expect(tooltip.locator('.copy-button')).toBeVisible();\n      await expect(tooltip.locator('.var-value-editor')).not.toBeVisible();\n    });\n  });\n\n  test('should auto-save request when creating variable via tooltip', async ({ page, createTmpDir }) => {\n    const collectionName = 'draft-autosave-test';\n\n    await test.step('Setup collection and request', async () => {\n      await createCollection(page, collectionName, await createTmpDir('draft-autosave'));\n\n      // Create request using utility method\n      await createRequest(page, 'Autosave Test', collectionName);\n\n      // Set the URL\n      await page.locator('.collection-item-name').filter({ hasText: 'Autosave Test' }).click();\n      const urlEditor = page.locator('#request-url .CodeMirror');\n      await urlEditor.click();\n      await page.keyboard.type('https://api.example.com');\n      await page.keyboard.press('Control+s');\n    });\n\n    await test.step('Edit URL to create draft with undefined variable', async () => {\n      // Edit the URL to add a variable reference\n      const urlEditor = page.locator('#request-url .CodeMirror');\n      await urlEditor.click();\n      await page.keyboard.press('End');\n      await page.keyboard.type('/users/{{myApiKey}}');\n\n      // Verify draft indicator appears (unsaved changes) in the request tab\n      const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Autosave Test' }) });\n      await expect(requestTab.locator('.has-changes-icon')).toBeVisible();\n    });\n\n    await test.step('Create variable via tooltip - should auto-save entire request', async () => {\n      // Hover over the undefined variable {{myApiKey}}\n      const urlEditor = page.locator('#request-url .CodeMirror');\n      const undefinedVar = urlEditor.locator('.cm-variable-invalid').filter({ hasText: 'myApiKey' }).first();\n      await undefinedVar.hover();\n\n      // Tooltip should appear\n      const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();\n      await expect(tooltip).toBeVisible();\n      await expect(tooltip.locator('.var-name')).toContainText('myApiKey');\n      await expect(tooltip.locator('.var-scope-badge')).toContainText('Request');\n\n      // Click to edit the variable\n      const valueDisplay = tooltip.locator('.var-value-editable-display');\n      await valueDisplay.click();\n\n      // Type value\n      const editor = tooltip.locator('.var-value-editor .CodeMirror');\n      await expect(editor).toBeVisible();\n      await page.keyboard.type('secret-key-123');\n\n      // Click outside to close editor - this will auto-save the entire request\n      await page.locator('body').click();\n    });\n\n    await test.step('Verify request was auto-saved with URL changes and new variable', async () => {\n      // Move mouse away\n      await page.mouse.move(0, 0);\n\n      // Verify variable is now valid (green) in the URL\n      const urlEditor = page.locator('#request-url .CodeMirror');\n      const validVar = urlEditor.locator('.cm-variable-valid').filter({ hasText: 'myApiKey' });\n      await expect(validVar).toBeVisible();\n\n      // Hover to verify value was saved\n      await validVar.first().hover();\n      const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();\n      await expect(tooltip).toBeVisible();\n      await expect(tooltip.locator('.var-value-editable-display')).toContainText('secret-key-123');\n\n      // Move mouse away\n      await page.mouse.move(0, 0);\n\n      // Verify the URL changes were also saved\n      const urlContent = await urlEditor.locator('.CodeMirror-line').first().textContent();\n      expect(urlContent).toContain('api.example.com/users');\n      expect(urlContent).toContain('myApiKey');\n\n      // Verify draft indicator is GONE (everything was auto-saved)\n      const requestTab = page.locator('.request-tab').filter({ has: page.locator('.tab-label', { hasText: 'Autosave Test' }) });\n      await expect(requestTab.locator('.has-changes-icon')).not.toBeVisible();\n      await expect(requestTab.locator('.close-icon')).toBeVisible();\n    });\n\n    await test.step('Verify variable exists in Vars tab', async () => {\n      // Check variable is saved to file - should appear in the Vars tab\n      await selectRequestPaneTab(page, 'Vars');\n\n      // The variable should exist in the saved file\n      const varsTable = page.locator('table').first();\n      await expect(varsTable).toBeVisible();\n\n      const varRow = varsTable.locator('tbody tr').first();\n      await expect(varRow).toBeVisible();\n\n      // Check variable name\n      const varNameInput = varRow.locator('td').nth(1).getByRole('textbox');\n      await expect(varNameInput).toBeVisible();\n      await expect(varNameInput).toHaveValue('myApiKey');\n\n      // Check variable value\n      const varValueTd = varRow.locator('td').nth(2);\n      const varValue = varValueTd.locator('.CodeMirror');\n      await expect(varValue).toBeVisible();\n      const varValueContent = await varValue.locator('.CodeMirror-line').textContent();\n      expect(varValueContent).toContain('secret-key-123');\n    });\n  });\n\n  test('should handle invalid variable names with warning', async ({ page, createTmpDir }) => {\n    const collectionName = 'invalid-var-test';\n\n    await test.step('Setup collection and request', async () => {\n      await createCollection(page, collectionName, await createTmpDir('invalid-var-collection'));\n\n      // Create request using utility method\n      await createRequest(page, 'Invalid Var Test', collectionName);\n\n      // Set the URL\n      await page.locator('.collection-item-name').filter({ hasText: 'Invalid Var Test' }).click();\n      const urlEditor = page.locator('#request-url .CodeMirror');\n      await urlEditor.click();\n      await page.keyboard.type('https://api.example.com');\n      await page.keyboard.press('Control+s');\n    });\n\n    await test.step('Test invalid variable name with space', async () => {\n      await selectRequestPaneTab(page, 'Body');\n\n      // Select JSON body mode\n      await page.locator('.body-mode-selector').click();\n      await page.locator('.dropdown-item').filter({ hasText: 'JSON' }).click();\n\n      const bodyEditor = page.locator('.CodeMirror').last();\n      await bodyEditor.click();\n\n      await bodyEditor.evaluate((el: any) => {\n        const cm = el.CodeMirror;\n        cm.setValue('{\\n  \"userId\": \"{{user id}}\"\\n}');\n      });\n      await page.keyboard.press('Control+s');\n\n      // Hover over the invalid variable\n      await page.mouse.move(0, 0);\n      const invalidVar = bodyEditor.locator('.cm-variable-invalid, .cm-variable-valid').filter({ hasText: 'user id' }).first();\n      await invalidVar.hover();\n\n      // Verify tooltip shows warning and hides input\n      const tooltip = page.locator('.CodeMirror-brunoVarInfo').first();\n      await expect(tooltip).toBeVisible();\n      await expect(tooltip.locator('.var-name')).toContainText('user id');\n      await expect(tooltip.locator('.var-warning-note')).toBeVisible();\n      await expect(tooltip.locator('.var-value-editable-display')).not.toBeVisible();\n    });\n  });\n});\n"
  },
  {
    "path": "tests/websockets/connection.spec.ts",
    "content": "import { expect, test } from '../../playwright';\nimport { buildWebsocketCommonLocators } from '../utils/page/locators';\n\nconst MAX_CONNECTION_TIME = 3000;\nconst BRU_REQ_NAME = /^ws-test-request$/;\n\ntest.describe.serial('websockets', () => {\n  test('websocket requests are visible', async ({ pageWithUserData: page, restartApp }) => {\n    await page.locator('#sidebar-collection-name').click();\n\n    expect(page.locator('span.item-name').filter({ hasText: BRU_REQ_NAME })).toBeVisible();\n  });\n\n  test('websocket connects', async ({ pageWithUserData: page, restartApp }) => {\n    const locators = buildWebsocketCommonLocators(page);\n\n    // Attempt a connection for the specified request\n    await page.getByTitle(BRU_REQ_NAME).click();\n    await locators.connectionControls.connect().click();\n\n    // See if the socket connected by monitoring the opposite state\n    await expect(locators.connectionControls.disconnect()).toBeAttached({\n      timeout: MAX_CONNECTION_TIME\n    });\n  });\n\n  test('websocket closes', async ({ pageWithUserData: page, restartApp }) => {\n    const locators = buildWebsocketCommonLocators(page);\n    await locators.connectionControls.disconnect().click();\n\n    // See if the socket disconnected by monitoring the opposite state\n    await expect(locators.connectionControls.connect()).toBeVisible();\n  });\n\n  test('websocket messages were recorded', async ({ pageWithUserData: page, restartApp }) => {\n    const locators = buildWebsocketCommonLocators(page);\n\n    // Hard validate the recieved messages to confirm the connection state\n    await expect(locators.messages().first().getByText('Connected to ws://')).toBeAttached();\n    await expect(locators.messages().nth(1).getByText('Closed')).toBeAttached();\n  });\n\n  test('websocket request can send messages', async ({ pageWithUserData: page, restartApp }) => {\n    const locators = buildWebsocketCommonLocators(page);\n\n    await locators.toolbar.clearResponse().click();\n    await locators.runner().click();\n\n    // Check if the messages from the request are actually displayed on the messages container\n    await expect(locators.messages().nth(1).locator('.text-ellipsis')).toHaveText('{ \"foo\": \"bar\" }');\n    await expect(locators.messages().nth(2).locator('.text-ellipsis')).toHaveText('{ \"data\": { \"foo\": \"bar\" } }');\n  });\n});\n"
  },
  {
    "path": "tests/websockets/fixtures/collection/base.bru",
    "content": "meta {\n  name: base\n  type: ws\n  seq: 3\n}\n\nws {\n  url: ws://localhost:8082\n  body: ws\n  auth: inherit\n}\n\nbody:ws {\n  name: message 1\n  content: '''\n    {}\n  '''\n}\n"
  },
  {
    "path": "tests/websockets/fixtures/collection/bruno.json",
    "content": "{\n    \"version\": \"1\",\n    \"name\": \"collection\",\n    \"type\": \"collection\"\n}"
  },
  {
    "path": "tests/websockets/fixtures/collection/collection.bru",
    "content": "vars:pre-request {\n  variable: Variable Value\n}"
  },
  {
    "path": "tests/websockets/fixtures/collection/ws-test-request-with-headers.bru",
    "content": "meta {\n  name: ws-test-request-with-headers\n  type: ws\n  seq: 2\n}\n\nws {\n  url: ws://localhost:8081/ws\n  auth: inherit\n}\n\nheaders {\n  Authorization: Dummy\n  X-BRUNO-COLLECTION-VAR: {{variable}}\n}\n\nbody:ws {\n  name: message 1\n  content: '''\n    {\n      \"func\":\"headers\"\n    }\n  '''\n}\n"
  },
  {
    "path": "tests/websockets/fixtures/collection/ws-test-request-with-query.bru",
    "content": "meta {\n  name: ws-test-request-with-query\n  type: ws\n  seq: 3\n}\n\nws {\n  url: ws://localhost:8081/ws?testParam=testValue&anotherParam={{variable}}\n  auth: inherit\n}\n\nbody:ws {\n  name: message 1\n  content: '''\n    {\n      \"func\":\"query\"\n    }\n  '''\n}\n"
  },
  {
    "path": "tests/websockets/fixtures/collection/ws-test-request-with-subproto.bru",
    "content": "meta {\n  name: ws-test-request-with-subproto\n  type: ws\n  seq: 3\n}\n\nws {\n  url: ws://localhost:8081/ws/sub-proto\n  body: ws\n  auth: inherit\n}\n\nheaders {\n  Sec-WebSocket-Protocol: soap\n  Sec-WebSocket-Protocol: mqtt\n  Sec-WebSocket-Version: 13\n}\n\nbody:ws {\n  name: message 1\n  content: '''\n    {}\n  '''\n}\n"
  },
  {
    "path": "tests/websockets/fixtures/collection/ws-test-request.bru",
    "content": "meta {\n  name: ws-test-request\n  type: ws\n  seq: 2\n}\n\nws {\n  url: ws://localhost:8081/ws\n  auth: inherit\n}\n\nbody:ws {\n  name: message 1\n  content: '''\n    {\n      \"foo\":\"bar\"\n    }\n  '''\n}\n"
  },
  {
    "path": "tests/websockets/headers.spec.ts",
    "content": "import { test, expect } from '../../playwright';\nimport { buildWebsocketCommonLocators } from '../utils/page/locators';\n\nconst BRU_REQ_NAME = /^ws-test-request-with-headers$/;\n\ntest.describe.serial('headers', () => {\n  test('headers are returned if passed', async ({ pageWithUserData: page, restartApp }) => {\n    const locators = buildWebsocketCommonLocators(page);\n\n    // Open the most recent collection\n    await page.locator('#sidebar-collection-name').click();\n\n    // Click on the required request\n    await page.getByTitle(BRU_REQ_NAME).click();\n    await locators.runner().click();\n\n    // Check if the message has the authorisation header\n    await expect(locators.messages().nth(2).locator('.text-ellipsis')).toHaveText(/\\\"(authorization)\\\"\\:\\s+\\\"Dummy\\\"/);\n    await expect(locators.messages().nth(2).locator('.text-ellipsis')).toHaveText(/\\\"(x-bruno-collection-var)\\\"\\:\\s+\\\"Variable Value\\\"/);\n  });\n});\n"
  },
  {
    "path": "tests/websockets/init-user-data/collection-security.json",
    "content": "{\n  \"collections\": [\n    {\n      \"path\": \"{{projectRoot}}/tests/websockets/fixtures/collection\",\n      \"securityConfig\": {\n        \"jsSandboxMode\": \"safe\"\n      }\n    }\n  ]\n}"
  },
  {
    "path": "tests/websockets/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/websockets/fixtures/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/websockets/persistence.spec.ts",
    "content": "import { expect, Locator, test } from '../../playwright';\nimport { buildWebsocketCommonLocators } from '../utils/page/locators';\nimport { readFile, writeFile } from 'fs/promises';\nimport { join } from 'path';\n\nconst BRU_REQ_NAME = /^base$/;\nconst BRU_PATH = join(__dirname, 'fixtures/collection/base.bru');\n\n// TODO: reaper move to someplace common\nconst isRequestSaved = async (saveButton: Locator) => {\n  // Saved state uses the className cursor-default; unsaved uses cursor-pointer.\n  return await saveButton.locator('svg').evaluate((node) => (node as HTMLElement).classList.contains('cursor-default'));\n};\n\ntest.describe.serial('persistence', () => {\n  let originalUrl = '';\n  let originalData = '';\n\n  test.beforeAll(async () => {\n    originalData = await readFile(BRU_PATH, 'utf8');\n    const originalUrlMatch = originalData.match(`(url)\\s*\\:\\s*(.+)`);\n    if (!originalUrlMatch) {\n      throw new Error('url not found in bru file for websocket');\n    }\n    // Trim to remove leading/trailing whitespace from the regex capture\n    originalUrl = originalUrlMatch[0].replace(/url\\:/, '').trim();\n  });\n\n  test.afterAll(async () => {\n    // Restore original fixture since pageWithUserData does not isolate collection files\n    await writeFile(BRU_PATH, originalData, 'utf8');\n  });\n\n  test('save new websocket url', async ({ pageWithUserData: page }) => {\n    const replacementUrl = 'ws://localhost:8083';\n    const locators = buildWebsocketCommonLocators(page);\n    const selectAllShortcut = process.platform === 'darwin' ? 'Meta+a' : 'Control+a';\n\n    await page.locator('#sidebar-collection-name').click();\n    await page.getByTitle(BRU_REQ_NAME).click();\n\n    // Select all text in the URL input and replace with new URL\n    await page.locator('.input-container').filter({ hasText: originalUrl }).first().click();\n    await page.keyboard.press(selectAllShortcut);\n    await page.keyboard.insertText(replacementUrl);\n\n    // Use auto-retrying assertion to check if the request is now unsaved\n    await expect.poll(() => isRequestSaved(locators.saveButton())).toBe(false);\n\n    await locators.saveButton().click();\n\n    // Use auto-retrying assertion to verify save completed\n    await expect.poll(() => isRequestSaved(locators.saveButton())).toBe(true);\n\n    // check if the replacementUrl is now visually available\n    await expect(page.locator('.input-container').filter({ hasText: replacementUrl }).first()).toBeAttached();\n  });\n});\n"
  },
  {
    "path": "tests/websockets/query.spec.ts",
    "content": "import { test, expect } from '../../playwright';\nimport { buildWebsocketCommonLocators } from '../utils/page/locators';\n\nconst BRU_REQ_NAME = /^ws-test-request-with-query$/;\n\ntest.describe.serial('query params', () => {\n  test('query params are returned if passed', async ({ pageWithUserData: page }) => {\n    const locators = buildWebsocketCommonLocators(page);\n\n    // Open the most recent collection\n    await page.locator('#sidebar-collection-name').click();\n\n    // Click on the required request\n    await page.getByTitle(BRU_REQ_NAME).click();\n    await locators.runner().click();\n\n    // Check if the message has the query params\n    await expect(locators.messages().nth(2).locator('.text-ellipsis')).toHaveText(/\\\"(testParam)\\\"\\:\\s+\\\"testValue\\\"/);\n  });\n});\n"
  },
  {
    "path": "tests/websockets/subproto.spec.ts",
    "content": "import { test, expect } from '../../playwright';\nimport { buildWebsocketCommonLocators } from '../utils/page/locators';\n\nconst BRU_REQ_NAME = /^ws-test-request-with-subproto$/;\n\ntest.describe.serial('subprotocol tests', () => {\n  test('has multiple sub proto headers', async ({ pageWithUserData: page, restartApp }) => {\n    const originalProtocols = ['soap', 'mqtt'];\n    const locators = buildWebsocketCommonLocators(page);\n    // Open the needed request and keep the headers tab in focus for modifications\n    await page.locator('#sidebar-collection-name').click();\n    await page.getByTitle(BRU_REQ_NAME).click();\n    await page.locator('[role=tab].headers').click();\n\n    // Check if the original / correct protocol is in place and then send a request\n    for (let proto of originalProtocols) {\n      await expect(page.locator('pre').filter({ hasText: proto })).toBeAttached();\n    }\n  });\n\n  test('Only connect if a valid subprotocol is sent with the request', async ({ pageWithUserData: page, restartApp }) => {\n    const locators = buildWebsocketCommonLocators(page);\n    const clearText = async (text: string) => {\n      for (let i = text.length; i > 0; i--) {\n        await page.keyboard.press('Backspace');\n      }\n    };\n\n    const originalProtocol = 'soap';\n    const wrongProtocol = 'wap';\n\n    // Open the needed request and keep the headers tab in focus for modifications\n    await page.locator('#sidebar-collection-name').click();\n    await page.getByTitle(BRU_REQ_NAME).click();\n    await page.locator('[role=tab].headers').click();\n\n    // Check if the original / correct protocol is in place and then send a request\n    await expect(page.locator('pre').filter({ hasText: originalProtocol })).toBeAttached();\n    await locators.runner().click();\n\n    // Check the messages to confirm we ended up connecting\n    await expect(locators.messages().first().locator('.text-ellipsis')).toHaveText(/^(Connected to)/);\n\n    // Disconnect the request\n    await locators.connectionControls.disconnect().click();\n\n    // Make changes to the header and add in an invalid sub protocol\n    await page.locator('pre').filter({ hasText: originalProtocol }).click();\n    await clearText(originalProtocol);\n    await page.keyboard.insertText(wrongProtocol);\n\n    // clear before making another request\n    await locators.toolbar.clearResponse().click();\n\n    // Make another request and check the new set of messages to confirm that we did\n    // get an error on connection\n    await locators.runner().click();\n\n    await expect(locators.messages().nth(0).locator('.text-ellipsis')).toHaveText(/^(Unexpected server response)/);\n\n    // Reset state back to the original\n    await page.locator('pre').filter({ hasText: wrongProtocol }).click();\n    await clearText(wrongProtocol);\n    await page.keyboard.insertText(originalProtocol);\n    await locators.saveButton().click();\n  });\n});\n"
  },
  {
    "path": "tests/websockets/variable-interpolation/fixtures/collection/bruno.json",
    "content": "{\n  \"version\": \"1\",\n  \"name\": \"variable-interpolation\",\n  \"type\": \"collection\",\n  \"ignore\": [\n    \"node_modules\",\n    \".git\"\n  ]\n}\n\n"
  },
  {
    "path": "tests/websockets/variable-interpolation/fixtures/collection/environments/Test.bru",
    "content": "vars {\n  url: websocket\n  data: test-data\n}\n\n"
  },
  {
    "path": "tests/websockets/variable-interpolation/fixtures/collection/ws-interpolation-test.bru",
    "content": "meta {\n  name: ws-interpolation-test\n  type: ws\n  seq: 1\n}\n\nws {\n  url: wss://echo.{{url}}.org\n  auth: inherit\n}\n\nbody:ws {\n  name: message 1\n  content: '''\n    {\n      \"test\": \"{{data}}\"\n    }\n  '''\n}\n\n"
  },
  {
    "path": "tests/websockets/variable-interpolation/init-user-data/preferences.json",
    "content": "{\n  \"maximized\": false,\n  \"lastOpenedCollections\": [\n    \"{{projectRoot}}/tests/websockets/variable-interpolation/fixtures/collection\"\n  ],\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    }\n  }\n}\n"
  },
  {
    "path": "tests/websockets/variable-interpolation/variable-interpolation.spec.ts",
    "content": "import { test, expect } from '../../../playwright';\nimport { buildWebsocketCommonLocators } from '../../utils/page/locators';\nimport { closeAllCollections, openCollection } from '../../utils/page';\n\nconst BRU_REQ_NAME = /^ws-interpolation-test$/;\nconst MAX_CONNECTION_TIME = 10000; // Increased timeout for external server\n\ntest.describe.serial('WebSocket Variable Interpolation', () => {\n  test.afterAll(async ({ pageWithUserData: page }) => {\n    await closeAllCollections(page);\n  });\n\n  test('interpolates variables in WebSocket URL', async ({ pageWithUserData: page, restartApp }) => {\n    const locators = buildWebsocketCommonLocators(page);\n\n    // Open the collection and accept sandbox modal if it appears\n    await openCollection(page, 'variable-interpolation');\n\n    // Open the request\n    await expect(page.getByTitle(BRU_REQ_NAME)).toBeVisible();\n    await page.getByTitle(BRU_REQ_NAME).click();\n\n    // Select the test environment (which has url: websocket)\n    await page.locator('div.current-environment').click();\n    await expect(page.locator('.dropdown-item').filter({ hasText: 'Test' })).toBeVisible();\n    await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click();\n    await expect(page.locator('.current-environment').filter({ hasText: /Test/ })).toBeVisible();\n\n    // Connect WebSocket\n    await locators.connectionControls.connect().click();\n\n    // Wait for connection to establish\n    await expect(locators.connectionControls.disconnect()).toBeAttached({\n      timeout: MAX_CONNECTION_TIME\n    });\n\n    // Verify the connection message shows interpolated URL\n    // The URL should be wss://echo.websocket.org (not wss://echo.{{url}}.org)\n    await expect(locators.messages().first().getByText(/Connected to wss:\\/\\/echo\\.websocket\\.org/)).toBeAttached({\n      timeout: 2000\n    });\n  });\n\n  test('interpolates variables in WebSocket message content', async ({ pageWithUserData: page, restartApp }) => {\n    const locators = buildWebsocketCommonLocators(page);\n\n    // Wait for collection to be visible (it should auto-load from preferences)\n    await expect(page.locator('#sidebar-collection-name').filter({ hasText: 'variable-interpolation' })).toBeVisible({ timeout: 5000 });\n\n    // Click to expand the collection\n    await page.locator('#sidebar-collection-name').filter({ hasText: 'variable-interpolation' }).click();\n\n    // Open the request\n    await expect(page.getByTitle(BRU_REQ_NAME)).toBeVisible();\n    await page.getByTitle(BRU_REQ_NAME).click();\n\n    // Select the test environment (which has data: test-data)\n    await page.locator('div.current-environment').click();\n    await expect(page.locator('.dropdown-item').filter({ hasText: 'Test' })).toBeVisible();\n    await page.locator('.dropdown-item').filter({ hasText: 'Test' }).click();\n    await expect(page.locator('.current-environment').filter({ hasText: /Test/ })).toBeVisible();\n\n    // Clear any previous messages\n    await locators.toolbar.clearResponse().click();\n\n    // Send the request (connect + send messages)\n    await locators.runner().click();\n\n    // Wait for connection\n    await expect(locators.connectionControls.disconnect()).toBeAttached({\n      timeout: MAX_CONNECTION_TIME\n    });\n\n    // Verify the sent message contains interpolated value\n    // Should send {\"test\": \"test-data\"} (not {\"test\": \"{{data}}\"})\n    const messages = locators.messages();\n\n    // Find the outgoing message with interpolated content\n    // The echo server will echo back the same message, so we should see it twice\n    const sentMessage = messages.filter({ hasText: 'test-data' }).first();\n    await expect(sentMessage).toBeAttached({ timeout: MAX_CONNECTION_TIME });\n\n    // Verify the message content shows interpolated value, not literal variable\n    const messageContent = sentMessage.locator('.text-ellipsis');\n    await expect(messageContent).toContainText('test-data');\n    await expect(messageContent).not.toContainText('{{data}}');\n\n    // Verify JSON structure is correct\n    await expect(messageContent).toContainText('\"test\"');\n  });\n});\n"
  },
  {
    "path": "tests/workspace/close-tab-stays-in-workspace.spec.ts",
    "content": "import path from 'path';\nimport fs from 'fs';\nimport { test, expect, closeElectronApp } from '../../playwright';\nimport {\n  createCollection,\n  createRequest,\n  openRequest\n} from '../utils/page';\nimport { buildCommonLocators } from '../utils/page/locators';\n\nconst WORKSPACE_YML_WORKSPACEB = [\n  'opencollection: 1.0.0',\n  'info:',\n  '  name: WorkspaceB',\n  '  type: workspace',\n  'collections:',\n  'specs: []',\n  'docs: \\'\\'',\n  ''\n].join('\\n');\n\ntest.describe('Close tab stays in workspace', () => {\n  test('after closing last request tab in WorkspaceB, active tab is not from WorkspaceA and workspace stays WorkspaceB', async ({\n    launchElectronApp,\n    createTmpDir\n  }) => {\n    const userDataPath = await createTmpDir('close-tab-two-workspace');\n    const colAPath = await createTmpDir('col-a');\n    const colBPath = await createTmpDir('col-b');\n    const workspaceBPath = await createTmpDir('workspace-b');\n    fs.writeFileSync(path.join(workspaceBPath, 'workspace.yml'), WORKSPACE_YML_WORKSPACEB);\n\n    let app;\n    try {\n      app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await test.step('Create ColA/ReqA in default workspace and open ReqA', async () => {\n        await createCollection(page, 'ColA', colAPath);\n        await createRequest(page, 'ReqA', 'ColA', { url: 'https://echo.usebruno.com', method: 'GET' });\n        await openRequest(page, 'ColA', 'ReqA');\n        const locators = buildCommonLocators(page);\n        await expect(locators.tabs.activeRequestTab()).toContainText('ReqA');\n        await locators.request.sendButton().click();\n        await expect(locators.response.statusCode()).toBeVisible({ timeout: 10000 });\n      });\n\n      await test.step('Stub open-dialog and switch to WorkspaceB', async () => {\n        await app.evaluate(\n          ({ dialog }, targetPath: string) => {\n            (dialog as { showOpenDialog: typeof dialog.showOpenDialog }).showOpenDialog = () =>\n              Promise.resolve({ canceled: false, filePaths: [targetPath] });\n          },\n          workspaceBPath\n        );\n        await page.getByTestId('workspace-menu').click();\n        await page.locator('.dropdown-item').filter({ hasText: 'Open workspace' }).click();\n        await expect(page.getByTestId('workspace-name')).toHaveText('WorkspaceB', { timeout: 10000 });\n      });\n\n      await test.step('Create ColB/ReqB in WorkspaceB and open ReqB', async () => {\n        await createCollection(page, 'ColB', colBPath);\n        await createRequest(page, 'ReqB', 'ColB', { url: 'https://echo.usebruno.com', method: 'GET' });\n        await openRequest(page, 'ColB', 'ReqB');\n        const locators = buildCommonLocators(page);\n        await expect(locators.tabs.activeRequestTab()).toContainText('ReqB');\n        await locators.request.sendButton().click();\n        await expect(locators.response.statusCode()).toBeVisible({ timeout: 10000 });\n      });\n\n      await test.step('Close ReqB tab', async () => {\n        const locators = buildCommonLocators(page);\n        await locators.tabs.closeTab('ReqB').click({ force: true });\n      });\n\n      await test.step('Active tab must not show ReqA and workspace must still be WorkspaceB', async () => {\n        const locators = buildCommonLocators(page);\n        const activeTab = locators.tabs.activeRequestTab();\n        await expect(activeTab).toBeVisible({ timeout: 5000 });\n        await expect(activeTab).not.toContainText('ReqA');\n        await expect(page.getByTestId('workspace-name')).toHaveText('WorkspaceB');\n      });\n    } finally {\n      if (app) await closeElectronApp(app);\n    }\n  });\n});\n"
  },
  {
    "path": "tests/workspace/collection-reorder-persistence.spec.ts",
    "content": "import path from 'path';\nimport fs from 'fs';\nimport yaml from 'js-yaml';\nimport { test, expect } from '../../playwright';\nimport { createCollection } from '../utils/page';\n\ntype WorkspaceConfig = { collections?: { name: string }[] };\n\ntest.describe('Collection reorder persistence', () => {\n  test('reordered collection order persists after app restart', async ({ launchElectronApp, createTmpDir }) => {\n    const userDataPath = await createTmpDir('collection-reorder-persistence');\n    const colAPath = await createTmpDir('col-a');\n    const colBPath = await createTmpDir('col-b');\n\n    const app = await launchElectronApp({ userDataPath });\n    const page = await app.firstWindow();\n    await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n    await test.step('Create two collections', async () => {\n      await createCollection(page, 'ColA', colAPath);\n      await createCollection(page, 'ColB', colBPath);\n    });\n\n    await test.step('Verify initial order is ColA then ColB', async () => {\n      const rows = page.getByTestId('sidebar-collection-row');\n      await expect(rows.nth(0)).toContainText('ColA');\n      await expect(rows.nth(1)).toContainText('ColB');\n    });\n\n    await test.step('Drag ColB above ColA', async () => {\n      const rows = page.getByTestId('sidebar-collection-row');\n      await rows.nth(1).dragTo(rows.nth(0), { targetPosition: { x: 5, y: 5 } });\n    });\n\n    await test.step('Verify order is ColB then ColA', async () => {\n      const rows = page.getByTestId('sidebar-collection-row');\n      await expect(rows.nth(0)).toContainText('ColB');\n      await expect(rows.nth(1)).toContainText('ColA');\n    });\n\n    await test.step('Close app', async () => {\n      await app.context().close();\n      await app.close();\n    });\n\n    await test.step('Restart app and verify order persisted', async () => {\n      const app2 = await launchElectronApp({ userDataPath });\n      const page2 = await app2.firstWindow();\n      await page2.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      const rows2 = page2.getByTestId('sidebar-collection-row');\n      await expect(rows2.nth(0)).toContainText('ColB');\n      await expect(rows2.nth(1)).toContainText('ColA');\n\n      await app2.context().close();\n      await app2.close();\n    });\n  });\n\n  test('workspace.yml reflects reordered collection order', async ({ launchElectronApp, createTmpDir }) => {\n    const userDataPath = await createTmpDir('collection-reorder-yml');\n    const colAPath = await createTmpDir('col-a');\n    const colBPath = await createTmpDir('col-b');\n\n    const app = await launchElectronApp({ userDataPath });\n    const page = await app.firstWindow();\n    await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n    await test.step('Create two collections', async () => {\n      await createCollection(page, 'ColA', colAPath);\n      await createCollection(page, 'ColB', colBPath);\n    });\n\n    await test.step('Drag ColB above ColA', async () => {\n      const rows = page.getByTestId('sidebar-collection-row');\n      await rows.nth(1).dragTo(rows.nth(0), { targetPosition: { x: 5, y: 5 } });\n    });\n\n    await test.step('Close app', async () => {\n      await app.context().close();\n      await app.close();\n    });\n\n    await test.step('Verify workspace.yml has ColB before ColA', async () => {\n      const workspacePath = path.join(userDataPath, 'default-workspace');\n      const ymlPath = path.join(workspacePath, 'workspace.yml');\n      expect(fs.existsSync(ymlPath)).toBe(true);\n      const config = yaml.load(fs.readFileSync(ymlPath, 'utf8')) as WorkspaceConfig | undefined;\n      const names = (config?.collections ?? []).map((c) => c.name);\n      expect(names).toEqual(['ColB', 'ColA']);\n    });\n  });\n});\n"
  },
  {
    "path": "tests/workspace/create-workspace/create-workspace.spec.ts",
    "content": "import path from 'path';\nimport fs from 'fs';\nimport yaml from 'js-yaml';\nimport { test, expect, closeElectronApp } from '../../../playwright';\n\ntype WorkspaceConfig = {\n  opencollection?: string;\n  info?: { name: string; type: string };\n  collections?: { name?: string; path?: string }[];\n};\n\nconst initUserDataPath = path.join(__dirname, 'init-user-data');\n\nfunction findCreatedWorkspaceDirs(location: string): string[] {\n  return fs.readdirSync(location).filter((e) => {\n    const fullPath = path.join(location, e);\n    return (\n      fs.statSync(fullPath).isDirectory()\n      && e !== 'default-workspace'\n      && fs.existsSync(path.join(fullPath, 'workspace.yml'))\n    );\n  });\n}\n\ntest.describe('Create Workspace', () => {\n  test.describe('Inline Creation Flow', () => {\n    test('should create workspace via inline rename and press Enter', async ({ launchElectronApp, createTmpDir }) => {\n      const wsLocation = await createTmpDir('ws-location-enter');\n\n      const app = await launchElectronApp({ initUserDataPath, templateVars: { wsLocation } });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await test.step('Click \"Create workspace\" from title bar dropdown', async () => {\n        await page.locator('.workspace-name-container').click();\n        await page.locator('.dropdown-item').filter({ hasText: 'Create workspace' }).click();\n      });\n\n      await test.step('Verify inline rename input appears with default name', async () => {\n        const renameInput = page.locator('.workspace-name-input');\n        await expect(renameInput).toBeVisible({ timeout: 5000 });\n        await expect(renameInput).not.toHaveValue('');\n      });\n\n      await test.step('Verify workspace is NOT yet created on filesystem', async () => {\n        const wsDirs = findCreatedWorkspaceDirs(wsLocation);\n        expect(wsDirs).toHaveLength(0);\n      });\n\n      await test.step('Type workspace name and press Enter to confirm', async () => {\n        const renameInput = page.locator('.workspace-name-input');\n        await renameInput.fill('My Test Workspace');\n        await renameInput.press('Enter');\n      });\n\n      await test.step('Verify workspace created successfully', async () => {\n        await expect(page.getByText('Workspace created!')).toBeVisible({ timeout: 10000 });\n        await expect(page.getByTestId('workspace-name')).toHaveText('My Test Workspace', { timeout: 5000 });\n      });\n\n      await test.step('Verify workspace folder exists on filesystem', async () => {\n        const wsDirs = findCreatedWorkspaceDirs(wsLocation);\n        expect(wsDirs.length).toBe(1);\n\n        const ymlPath = path.join(wsLocation, wsDirs[0], 'workspace.yml');\n        const config = yaml.load(fs.readFileSync(ymlPath, 'utf8')) as WorkspaceConfig;\n        expect(config?.info?.name).toBe('My Test Workspace');\n        expect(config?.info?.type).toBe('workspace');\n      });\n\n      await closeElectronApp(app);\n    });\n\n    test('should create workspace via inline rename and click check icon', async ({ launchElectronApp, createTmpDir }) => {\n      const wsLocation = await createTmpDir('ws-location-check');\n\n      const app = await launchElectronApp({ initUserDataPath, templateVars: { wsLocation } });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await test.step('Click \"Create workspace\" and fill name', async () => {\n        await page.locator('.workspace-name-container').click();\n        await page.locator('.dropdown-item').filter({ hasText: 'Create workspace' }).click();\n\n        const renameInput = page.locator('.workspace-name-input');\n        await expect(renameInput).toBeVisible({ timeout: 5000 });\n        await renameInput.fill('Check Icon Workspace');\n      });\n\n      await test.step('Click the check icon to confirm', async () => {\n        await page.locator('.inline-action-btn.save').click();\n      });\n\n      await test.step('Verify workspace created', async () => {\n        await expect(page.getByText('Workspace created!')).toBeVisible({ timeout: 10000 });\n        await expect(page.getByTestId('workspace-name')).toHaveText('Check Icon Workspace', { timeout: 5000 });\n      });\n\n      await test.step('Verify filesystem', async () => {\n        const wsDirs = findCreatedWorkspaceDirs(wsLocation);\n        expect(wsDirs.length).toBe(1);\n        expect(fs.existsSync(path.join(wsLocation, wsDirs[0], 'workspace.yml'))).toBe(true);\n      });\n\n      await closeElectronApp(app);\n    });\n\n    test('should create workspace via inline rename and click outside', async ({ launchElectronApp, createTmpDir }) => {\n      const wsLocation = await createTmpDir('ws-location-outside');\n\n      const app = await launchElectronApp({ initUserDataPath, templateVars: { wsLocation } });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await test.step('Create workspace and fill name', async () => {\n        await page.locator('.workspace-name-container').click();\n        await page.locator('.dropdown-item').filter({ hasText: 'Create workspace' }).click();\n\n        const renameInput = page.locator('.workspace-name-input');\n        await expect(renameInput).toBeVisible({ timeout: 5000 });\n        await renameInput.fill('Click Outside Workspace');\n      });\n\n      await test.step('Click outside the rename container to confirm', async () => {\n        await page.locator('.app-titlebar').click({ position: { x: 500, y: 10 } });\n      });\n\n      await test.step('Verify workspace created', async () => {\n        await expect(page.getByText('Workspace created!')).toBeVisible({ timeout: 10000 });\n        await expect(page.getByTestId('workspace-name')).toHaveText('Click Outside Workspace', { timeout: 5000 });\n      });\n\n      await closeElectronApp(app);\n    });\n  });\n\n  test.describe('Cancel/Discard Flow', () => {\n    test('should discard temp workspace when pressing Escape', async ({ launchElectronApp, createTmpDir }) => {\n      const wsLocation = await createTmpDir('ws-location-escape');\n\n      const app = await launchElectronApp({ initUserDataPath, templateVars: { wsLocation } });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await test.step('Start workspace creation', async () => {\n        await page.locator('.workspace-name-container').click();\n        await page.locator('.dropdown-item').filter({ hasText: 'Create workspace' }).click();\n        await expect(page.locator('.workspace-name-input')).toBeVisible({ timeout: 5000 });\n      });\n\n      await test.step('Press Escape to cancel', async () => {\n        await page.locator('.workspace-name-input').press('Escape');\n      });\n\n      await test.step('Verify switched back to default workspace', async () => {\n        await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace', { timeout: 5000 });\n      });\n\n      await test.step('Verify no workspace folder created on filesystem', async () => {\n        const wsDirs = findCreatedWorkspaceDirs(wsLocation);\n        expect(wsDirs).toHaveLength(0);\n      });\n\n      await closeElectronApp(app);\n    });\n\n    test('should discard temp workspace when clicking X icon', async ({ launchElectronApp, createTmpDir }) => {\n      const wsLocation = await createTmpDir('ws-location-x');\n\n      const app = await launchElectronApp({ initUserDataPath, templateVars: { wsLocation } });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await test.step('Start workspace creation', async () => {\n        await page.locator('.workspace-name-container').click();\n        await page.locator('.dropdown-item').filter({ hasText: 'Create workspace' }).click();\n        await expect(page.locator('.workspace-name-input')).toBeVisible({ timeout: 5000 });\n      });\n\n      await test.step('Click the X icon to cancel', async () => {\n        await page.locator('.inline-action-btn.cancel').click();\n      });\n\n      await test.step('Verify switched back to default workspace', async () => {\n        await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace', { timeout: 5000 });\n      });\n\n      await closeElectronApp(app);\n    });\n\n    test('should discard temp workspace when clicking outside with empty name', async ({ launchElectronApp, createTmpDir }) => {\n      const wsLocation = await createTmpDir('ws-location-outside-empty');\n\n      const app = await launchElectronApp({ initUserDataPath, templateVars: { wsLocation } });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await test.step('Start workspace creation and clear the name', async () => {\n        await page.locator('.workspace-name-container').click();\n        await page.locator('.dropdown-item').filter({ hasText: 'Create workspace' }).click();\n        const renameInput = page.locator('.workspace-name-input');\n        await expect(renameInput).toBeVisible({ timeout: 5000 });\n        await renameInput.fill('');\n      });\n\n      await test.step('Click outside to trigger cancel with empty name', async () => {\n        await page.locator('.app-titlebar').click({ position: { x: 500, y: 10 } });\n      });\n\n      await test.step('Verify switched back to default workspace', async () => {\n        await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace', { timeout: 5000 });\n      });\n\n      await closeElectronApp(app);\n    });\n  });\n\n  test.describe('Advanced Modal Flow', () => {\n    test('should create workspace via advanced modal with custom location', async ({ launchElectronApp, createTmpDir }) => {\n      const wsLocation = await createTmpDir('ws-location-modal');\n      const customLocation = await createTmpDir('custom-ws-location');\n\n      const app = await launchElectronApp({ initUserDataPath, templateVars: { wsLocation } });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await test.step('Start inline creation and click settings icon to open advanced modal', async () => {\n        await page.locator('.workspace-name-container').click();\n        await page.locator('.dropdown-item').filter({ hasText: 'Create workspace' }).click();\n        await expect(page.locator('.workspace-name-input')).toBeVisible({ timeout: 5000 });\n\n        // Settings gear icon should be visible during creation\n        const cogBtn = page.locator('.cog-btn');\n        await expect(cogBtn).toBeVisible();\n        await cogBtn.click();\n      });\n\n      await test.step('Fill in the advanced modal form with custom location', async () => {\n        const modal = page.locator('.bruno-modal-card').filter({ hasText: 'Create Workspace' });\n        await modal.waitFor({ state: 'visible', timeout: 5000 });\n\n        // Fill workspace name\n        await modal.locator('#workspace-name').fill('Advanced Workspace');\n\n        // Wait for folder name section to appear\n        await page.waitForTimeout(300);\n\n        // The location input is read-only and Formik-controlled — .fill() won't update\n        // Formik state. Stub the dialog so the browse() callback sets the custom location.\n        await app.evaluate(\n          ({ dialog }, targetPath: string) => {\n            (dialog as any).showOpenDialog = () =>\n              Promise.resolve({ canceled: false, filePaths: [targetPath] });\n          },\n          customLocation\n        );\n        // Click the location input to trigger browse() which calls showOpenDialog\n        await modal.locator('#workspace-location').click();\n        // Verify location was set\n        await expect(modal.locator('#workspace-location')).toHaveValue(customLocation, { timeout: 5000 });\n      });\n\n      await test.step('Submit the form', async () => {\n        const modal = page.locator('.bruno-modal-card').filter({ hasText: 'Create Workspace' });\n        await modal.getByRole('button', { name: 'Create Workspace' }).click();\n      });\n\n      await test.step('Verify workspace created', async () => {\n        await expect(page.getByText('Workspace created!')).toBeVisible({ timeout: 10000 });\n        await expect(page.getByTestId('workspace-name')).toHaveText('Advanced Workspace', { timeout: 5000 });\n      });\n\n      await test.step('Verify filesystem at custom location (NOT default location)', async () => {\n        // Workspace should be at customLocation, not wsLocation\n        const customDirs = findCreatedWorkspaceDirs(customLocation);\n        expect(customDirs.length).toBe(1);\n\n        const config = yaml.load(\n          fs.readFileSync(path.join(customLocation, customDirs[0], 'workspace.yml'), 'utf8')\n        ) as WorkspaceConfig;\n        expect(config?.info?.name).toBe('Advanced Workspace');\n\n        // No workspace at the default location\n        const defaultDirs = findCreatedWorkspaceDirs(wsLocation);\n        expect(defaultDirs).toHaveLength(0);\n      });\n\n      await test.step('Verify inline rename input is cleared after modal creation', async () => {\n        await expect(page.locator('.workspace-name-input')).not.toBeVisible();\n      });\n\n      await closeElectronApp(app);\n    });\n\n    test('should create workspace via advanced modal at default location', async ({ launchElectronApp, createTmpDir }) => {\n      const wsLocation = await createTmpDir('ws-location-modal-default');\n\n      const app = await launchElectronApp({ initUserDataPath, templateVars: { wsLocation } });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await test.step('Start inline creation and open advanced modal', async () => {\n        await page.locator('.workspace-name-container').click();\n        await page.locator('.dropdown-item').filter({ hasText: 'Create workspace' }).click();\n        await expect(page.locator('.workspace-name-input')).toBeVisible({ timeout: 5000 });\n        await page.locator('.cog-btn').click();\n      });\n\n      await test.step('Fill name and keep default location', async () => {\n        const modal = page.locator('.bruno-modal-card').filter({ hasText: 'Create Workspace' });\n        await modal.waitFor({ state: 'visible', timeout: 5000 });\n        await modal.locator('#workspace-name').fill('Default Loc Workspace');\n        await page.waitForTimeout(300);\n      });\n\n      await test.step('Submit the form', async () => {\n        const modal = page.locator('.bruno-modal-card').filter({ hasText: 'Create Workspace' });\n        await modal.getByRole('button', { name: 'Create Workspace' }).click();\n      });\n\n      await test.step('Verify workspace created at default location', async () => {\n        await expect(page.getByText('Workspace created!')).toBeVisible({ timeout: 10000 });\n        await expect(page.getByTestId('workspace-name')).toHaveText('Default Loc Workspace', { timeout: 5000 });\n\n        const wsDirs = findCreatedWorkspaceDirs(wsLocation);\n        expect(wsDirs.length).toBe(1);\n\n        const config = yaml.load(\n          fs.readFileSync(path.join(wsLocation, wsDirs[0], 'workspace.yml'), 'utf8')\n        ) as WorkspaceConfig;\n        expect(config?.info?.name).toBe('Default Loc Workspace');\n      });\n\n      await closeElectronApp(app);\n    });\n\n    test('should cancel advanced modal and discard temp workspace', async ({ launchElectronApp, createTmpDir }) => {\n      const wsLocation = await createTmpDir('ws-location-modal-cancel');\n\n      const app = await launchElectronApp({ initUserDataPath, templateVars: { wsLocation } });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await test.step('Start inline creation and open advanced modal', async () => {\n        await page.locator('.workspace-name-container').click();\n        await page.locator('.dropdown-item').filter({ hasText: 'Create workspace' }).click();\n        await expect(page.locator('.workspace-name-input')).toBeVisible({ timeout: 5000 });\n        await page.locator('.cog-btn').click();\n      });\n\n      await test.step('Cancel the advanced modal', async () => {\n        const modal = page.locator('.bruno-modal-card').filter({ hasText: 'Create Workspace' });\n        await modal.waitFor({ state: 'visible', timeout: 5000 });\n        await modal.getByRole('button', { name: 'Cancel' }).click();\n      });\n\n      await test.step('Verify temp workspace discarded and back to default', async () => {\n        await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace', { timeout: 5000 });\n        await expect(page.locator('.workspace-name-input')).not.toBeVisible();\n      });\n\n      await closeElectronApp(app);\n    });\n\n    test('should show validation error for empty name in modal', async ({ launchElectronApp, createTmpDir }) => {\n      const wsLocation = await createTmpDir('ws-location-modal-empty');\n\n      const app = await launchElectronApp({ initUserDataPath, templateVars: { wsLocation } });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await test.step('Start inline creation and open advanced modal', async () => {\n        await page.locator('.workspace-name-container').click();\n        await page.locator('.dropdown-item').filter({ hasText: 'Create workspace' }).click();\n        await expect(page.locator('.workspace-name-input')).toBeVisible({ timeout: 5000 });\n        await page.locator('.cog-btn').click();\n      });\n\n      await test.step('Clear name and try to submit', async () => {\n        const modal = page.locator('.bruno-modal-card').filter({ hasText: 'Create Workspace' });\n        await modal.waitFor({ state: 'visible', timeout: 5000 });\n\n        // Ensure name field is empty\n        await modal.locator('#workspace-name').fill('');\n        await modal.getByRole('button', { name: 'Create Workspace' }).click();\n      });\n\n      await test.step('Verify validation error appears and modal stays open', async () => {\n        const modal = page.locator('.bruno-modal-card').filter({ hasText: 'Create Workspace' });\n        await expect(modal).toBeVisible();\n        const error = modal.locator('.text-red-500');\n        await expect(error.first()).toBeVisible({ timeout: 2000 });\n      });\n\n      await closeElectronApp(app);\n    });\n  });\n\n  test.describe('Workspace Name Display', () => {\n    test('should show correct name in title bar dropdown after creation', async ({ launchElectronApp, createTmpDir }) => {\n      const wsLocation = await createTmpDir('ws-location-display');\n\n      const app = await launchElectronApp({ initUserDataPath, templateVars: { wsLocation } });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await test.step('Create a workspace with specific name', async () => {\n        await page.locator('.workspace-name-container').click();\n        await page.locator('.dropdown-item').filter({ hasText: 'Create workspace' }).click();\n        const renameInput = page.locator('.workspace-name-input');\n        await expect(renameInput).toBeVisible({ timeout: 5000 });\n        await renameInput.fill('Display Test WS');\n        await renameInput.press('Enter');\n        await expect(page.getByText('Workspace created!')).toBeVisible({ timeout: 10000 });\n      });\n\n      await test.step('Verify name in title bar', async () => {\n        await expect(page.getByTestId('workspace-name')).toHaveText('Display Test WS', { timeout: 5000 });\n      });\n\n      await test.step('Verify name in title bar dropdown', async () => {\n        await page.locator('.workspace-name-container').click();\n        const wsItem = page.locator('.workspace-item, .dropdown-item').filter({ hasText: 'Display Test WS' });\n        await expect(wsItem.first()).toBeVisible();\n      });\n\n      await closeElectronApp(app);\n    });\n\n    test('should persist workspace name after app restart', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('create-ws-name-persist');\n      const wsLocation = await createTmpDir('ws-location-persist');\n\n      // First launch: create workspace\n      const app1 = await launchElectronApp({ userDataPath, initUserDataPath, templateVars: { wsLocation } });\n      const page1 = await app1.firstWindow();\n      await page1.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await test.step('Create workspace', async () => {\n        await page1.locator('.workspace-name-container').click();\n        await page1.locator('.dropdown-item').filter({ hasText: 'Create workspace' }).click();\n        const renameInput = page1.locator('.workspace-name-input');\n        await expect(renameInput).toBeVisible({ timeout: 5000 });\n        await renameInput.fill('Persisted WS');\n        await renameInput.press('Enter');\n        await expect(page1.getByText('Workspace created!')).toBeVisible({ timeout: 10000 });\n      });\n\n      await closeElectronApp(app1);\n\n      // Second launch: verify name persists (reuse same userDataPath)\n      const app2 = await launchElectronApp({ userDataPath });\n      const page2 = await app2.firstWindow();\n      await page2.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await test.step('Verify workspace name persisted', async () => {\n        await page2.locator('.workspace-name-container').click();\n        const wsItem = page2.locator('.workspace-item, .dropdown-item').filter({ hasText: 'Persisted WS' });\n        await expect(wsItem.first()).toBeVisible({ timeout: 5000 });\n      });\n\n      await closeElectronApp(app2);\n    });\n  });\n\n  test.describe('Edge Cases', () => {\n    test('should handle creating multiple workspaces sequentially', async ({ launchElectronApp, createTmpDir }) => {\n      const wsLocation = await createTmpDir('ws-location-multiple');\n\n      const app = await launchElectronApp({ initUserDataPath, templateVars: { wsLocation } });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await test.step('Create first workspace', async () => {\n        await page.locator('.workspace-name-container').click();\n        await page.locator('.dropdown-item').filter({ hasText: 'Create workspace' }).click();\n        const renameInput = page.locator('.workspace-name-input');\n        await expect(renameInput).toBeVisible({ timeout: 5000 });\n        await renameInput.fill('Workspace One');\n        await renameInput.press('Enter');\n        await expect(page.getByText('Workspace created!')).toBeVisible({ timeout: 10000 });\n        await expect(page.getByTestId('workspace-name')).toHaveText('Workspace One', { timeout: 5000 });\n      });\n\n      await test.step('Create second workspace', async () => {\n        await page.locator('.workspace-name-container').click();\n        await page.locator('.dropdown-item').filter({ hasText: 'Create workspace' }).click();\n        const renameInput = page.locator('.workspace-name-input');\n        await expect(renameInput).toBeVisible({ timeout: 5000 });\n        await renameInput.fill('Workspace Two');\n        await renameInput.press('Enter');\n        await expect(page.getByText('Workspace created!')).toBeVisible({ timeout: 10000 });\n        await expect(page.getByTestId('workspace-name')).toHaveText('Workspace Two', { timeout: 5000 });\n      });\n\n      await test.step('Verify both workspaces exist in dropdown', async () => {\n        await page.locator('.workspace-name-container').click();\n        const wsOne = page.locator('.workspace-item, .dropdown-item').filter({ hasText: 'Workspace One' });\n        const wsTwo = page.locator('.workspace-item, .dropdown-item').filter({ hasText: 'Workspace Two' });\n        await expect(wsOne.first()).toBeVisible();\n        await expect(wsTwo.first()).toBeVisible();\n      });\n\n      await test.step('Verify both workspace folders on filesystem', async () => {\n        const wsDirs = findCreatedWorkspaceDirs(wsLocation);\n        expect(wsDirs.length).toBe(2);\n      });\n\n      await closeElectronApp(app);\n    });\n\n    test('should handle creating then cancelling then creating again', async ({ launchElectronApp, createTmpDir }) => {\n      const wsLocation = await createTmpDir('ws-location-cancel-retry');\n\n      const app = await launchElectronApp({ initUserDataPath, templateVars: { wsLocation } });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await test.step('Start creation and cancel with Escape', async () => {\n        await page.locator('.workspace-name-container').click();\n        await page.locator('.dropdown-item').filter({ hasText: 'Create workspace' }).click();\n        await expect(page.locator('.workspace-name-input')).toBeVisible({ timeout: 5000 });\n        await page.locator('.workspace-name-input').press('Escape');\n        await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace', { timeout: 5000 });\n      });\n\n      await test.step('Create again successfully', async () => {\n        await page.locator('.workspace-name-container').click();\n        await page.locator('.dropdown-item').filter({ hasText: 'Create workspace' }).click();\n        const renameInput = page.locator('.workspace-name-input');\n        await expect(renameInput).toBeVisible({ timeout: 5000 });\n        await renameInput.fill('Retry Workspace');\n        await renameInput.press('Enter');\n        await expect(page.getByText('Workspace created!')).toBeVisible({ timeout: 10000 });\n        await expect(page.getByTestId('workspace-name')).toHaveText('Retry Workspace', { timeout: 5000 });\n      });\n\n      await closeElectronApp(app);\n    });\n\n    test('should handle workspace name with special characters', async ({ launchElectronApp, createTmpDir }) => {\n      const wsLocation = await createTmpDir('ws-location-special');\n\n      const app = await launchElectronApp({ initUserDataPath, templateVars: { wsLocation } });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await test.step('Create workspace with special characters in name', async () => {\n        await page.locator('.workspace-name-container').click();\n        await page.locator('.dropdown-item').filter({ hasText: 'Create workspace' }).click();\n        const renameInput = page.locator('.workspace-name-input');\n        await expect(renameInput).toBeVisible({ timeout: 5000 });\n        await renameInput.fill('My API & Testing (v2)');\n        await renameInput.press('Enter');\n        await expect(page.getByText('Workspace created!')).toBeVisible({ timeout: 10000 });\n        await expect(page.getByTestId('workspace-name')).toHaveText('My API & Testing (v2)', { timeout: 5000 });\n      });\n\n      await test.step('Verify workspace name stored correctly in workspace.yml', async () => {\n        const wsDirs = findCreatedWorkspaceDirs(wsLocation);\n        expect(wsDirs.length).toBe(1);\n\n        const config = yaml.load(\n          fs.readFileSync(path.join(wsLocation, wsDirs[0], 'workspace.yml'), 'utf8')\n        ) as WorkspaceConfig;\n        expect(config?.info?.name).toBe('My API & Testing (v2)');\n      });\n\n      await closeElectronApp(app);\n    });\n\n    test('should show validation error for empty name inline when pressing Enter', async ({ launchElectronApp, createTmpDir }) => {\n      const wsLocation = await createTmpDir('ws-location-empty');\n\n      const app = await launchElectronApp({ initUserDataPath, templateVars: { wsLocation } });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await test.step('Create workspace and clear name', async () => {\n        await page.locator('.workspace-name-container').click();\n        await page.locator('.dropdown-item').filter({ hasText: 'Create workspace' }).click();\n        const renameInput = page.locator('.workspace-name-input');\n        await expect(renameInput).toBeVisible({ timeout: 5000 });\n        await renameInput.fill('');\n      });\n\n      await test.step('Press Enter with empty name - should show error', async () => {\n        await page.locator('.workspace-name-input').press('Enter');\n        const error = page.locator('.workspace-error');\n        await expect(error).toBeVisible({ timeout: 2000 });\n        await expect(error).toContainText('required');\n      });\n\n      await test.step('Verify still in rename mode (not discarded)', async () => {\n        await expect(page.locator('.workspace-name-input')).toBeVisible();\n      });\n\n      await closeElectronApp(app);\n    });\n\n    test('should not show settings/cog icon when renaming an existing workspace', async ({ launchElectronApp, createTmpDir }) => {\n      const wsLocation = await createTmpDir('ws-location-no-cog');\n\n      const app = await launchElectronApp({ initUserDataPath, templateVars: { wsLocation } });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await test.step('Create a workspace first', async () => {\n        await page.locator('.workspace-name-container').click();\n        await page.locator('.dropdown-item').filter({ hasText: 'Create workspace' }).click();\n        const renameInput = page.locator('.workspace-name-input');\n        await expect(renameInput).toBeVisible({ timeout: 5000 });\n\n        // During creation, the cog button should be visible\n        await expect(page.locator('.cog-btn')).toBeVisible();\n\n        await renameInput.fill('Existing WS');\n        await renameInput.press('Enter');\n        await expect(page.getByText('Workspace created!')).toBeVisible({ timeout: 10000 });\n      });\n\n      await test.step('Rename existing workspace - cog should NOT be visible', async () => {\n        // Use workspace actions dropdown to start rename\n        const actionsIcon = page.locator('.workspace-actions-trigger');\n        await actionsIcon.click();\n        await page.locator('.dropdown-item').filter({ hasText: 'Rename' }).click();\n\n        // Inline rename input should appear\n        await expect(page.locator('.workspace-name-input')).toBeVisible({ timeout: 5000 });\n\n        // Cog button should NOT be visible for existing workspace rename\n        await expect(page.locator('.cog-btn')).not.toBeVisible();\n      });\n\n      await closeElectronApp(app);\n    });\n  });\n\n  test.describe('Workspace Switching After Creation', () => {\n    test('should switch between created workspace and default workspace', async ({ launchElectronApp, createTmpDir }) => {\n      const wsLocation = await createTmpDir('ws-location-switch');\n\n      const app = await launchElectronApp({ initUserDataPath, templateVars: { wsLocation } });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await test.step('Create a new workspace', async () => {\n        await page.locator('.workspace-name-container').click();\n        await page.locator('.dropdown-item').filter({ hasText: 'Create workspace' }).click();\n        const renameInput = page.locator('.workspace-name-input');\n        await expect(renameInput).toBeVisible({ timeout: 5000 });\n        await renameInput.fill('Switchable WS');\n        await renameInput.press('Enter');\n        await expect(page.getByText('Workspace created!')).toBeVisible({ timeout: 10000 });\n        await expect(page.getByTestId('workspace-name')).toHaveText('Switchable WS', { timeout: 5000 });\n      });\n\n      await test.step('Switch to default workspace via dropdown', async () => {\n        await page.locator('.workspace-name-container').click();\n        const defaultWs = page.locator('.workspace-item, .dropdown-item').filter({ hasText: 'My Workspace' });\n        await defaultWs.first().click();\n        await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace', { timeout: 5000 });\n      });\n\n      await test.step('Switch back to created workspace', async () => {\n        await page.locator('.workspace-name-container').click();\n        const createdWs = page.locator('.workspace-item, .dropdown-item').filter({ hasText: 'Switchable WS' });\n        await createdWs.first().click();\n        await expect(page.getByTestId('workspace-name')).toHaveText('Switchable WS', { timeout: 5000 });\n      });\n\n      await closeElectronApp(app);\n    });\n  });\n\n  test.describe('Temp Workspace Isolation', () => {\n    test('should exclude temp workspace from duplicate name validation in advanced modal', async ({ launchElectronApp, createTmpDir }) => {\n      const wsLocation = await createTmpDir('ws-location-no-temp');\n\n      const app = await launchElectronApp({ initUserDataPath, templateVars: { wsLocation } });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await test.step('Start creation but do not confirm', async () => {\n        await page.locator('.workspace-name-container').click();\n        await page.locator('.dropdown-item').filter({ hasText: 'Create workspace' }).click();\n        await expect(page.locator('.workspace-name-input')).toBeVisible({ timeout: 5000 });\n      });\n\n      await test.step('Open advanced modal and verify temp workspace name is not a conflict', async () => {\n        await page.locator('.cog-btn').click();\n\n        const modal = page.locator('.bruno-modal-card').filter({ hasText: 'Create Workspace' });\n        await modal.waitFor({ state: 'visible', timeout: 5000 });\n\n        // Fill the same name as temp workspace — should NOT show \"already exists\" error\n        // since isCreating workspaces are excluded from validation\n        await modal.locator('#workspace-name').fill('Untitled Workspace');\n        await page.waitForTimeout(500);\n\n        const errorText = modal.locator('.text-red-500');\n        const hasError = await errorText.isVisible().catch(() => false);\n        if (hasError) {\n          const errorContent = await errorText.textContent();\n          expect(errorContent).not.toContain('already exists');\n        }\n      });\n\n      await closeElectronApp(app);\n    });\n  });\n});\n"
  },
  {
    "path": "tests/workspace/create-workspace/init-user-data/preferences.json",
    "content": "{\n  \"preferences\": {\n    \"onboarding\": {\n      \"hasLaunchedBefore\": true,\n      \"hasSeenWelcomeModal\": true\n    },\n    \"general\": {\n      \"defaultLocation\": \"{{wsLocation}}\"\n    }\n  }\n}\n"
  },
  {
    "path": "tests/workspace/default-workspace/default-workspace.spec.ts",
    "content": "import path from 'path';\nimport fs from 'fs';\nimport { test, expect, closeElectronApp } from '../../../playwright';\n\ntest.describe('Default Workspace', () => {\n  test.describe('First Launch', () => {\n    test('should create default workspace with \"My Workspace\" name on first launch', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('default-workspace-first-launch');\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      // Verify the workspace name is \"My Workspace\" in the title bar\n      const workspaceName = page.getByTestId('workspace-name');\n      await expect(workspaceName).toHaveText('My Workspace');\n\n      await closeElectronApp(app);\n    });\n  });\n\n  test.describe('Persistence', () => {\n    test('should persist default workspace across app restarts', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('default-workspace-persistence');\n\n      // First launch\n      const app1 = await launchElectronApp({ userDataPath });\n      const page1 = await app1.firstWindow();\n      await page1.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n      await expect(page1.getByTestId('workspace-name')).toHaveText('My Workspace');\n\n      await closeElectronApp(app1);\n\n      // Second launch - same workspace should be loaded\n      const app2 = await launchElectronApp({ userDataPath });\n      const page2 = await app2.firstWindow();\n      await page2.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n      await expect(page2.getByTestId('workspace-name')).toHaveText('My Workspace');\n\n      await closeElectronApp(app2);\n    });\n  });\n\n  test.describe('Recovery - Creates NEW workspace (never modifies existing)', () => {\n    test('should create NEW workspace when existing workspace.yml is deleted', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('default-workspace-recovery-deleted');\n\n      // Create a corrupted default workspace BEFORE launching app\n      const defaultWorkspacePath = path.join(userDataPath, 'default-workspace');\n      fs.mkdirSync(defaultWorkspacePath, { recursive: true });\n      fs.mkdirSync(path.join(defaultWorkspacePath, 'collections'), { recursive: true });\n      // Note: NOT creating workspace.yml - simulating deleted file\n\n      // Create preferences pointing to the corrupted workspace\n      fs.writeFileSync(\n        path.join(userDataPath, 'preferences.json'),\n        JSON.stringify({\n          general: {\n            defaultWorkspacePath: defaultWorkspacePath\n          }\n        })\n      );\n\n      // Launch app - should create NEW workspace\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      // Should show \"My Workspace\"\n      await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace');\n\n      // Old directory should still exist (never deleted)\n      expect(fs.existsSync(defaultWorkspacePath)).toBe(true);\n\n      // New workspace directory should have been created (default-workspace-1 since default-workspace exists)\n      const newWorkspacePath = path.join(userDataPath, 'default-workspace-1');\n      expect(fs.existsSync(newWorkspacePath)).toBe(true);\n      expect(fs.existsSync(path.join(newWorkspacePath, 'workspace.yml'))).toBe(true);\n\n      await closeElectronApp(app);\n    });\n\n    test('should create NEW workspace when workspace.yml has invalid YAML', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('default-workspace-recovery-invalid');\n\n      // Create workspace with invalid YAML BEFORE launching app\n      const defaultWorkspacePath = path.join(userDataPath, 'default-workspace');\n      fs.mkdirSync(defaultWorkspacePath, { recursive: true });\n      fs.writeFileSync(path.join(defaultWorkspacePath, 'workspace.yml'), 'invalid: yaml: [[[');\n\n      // Create preferences pointing to the corrupted workspace\n      fs.writeFileSync(\n        path.join(userDataPath, 'preferences.json'),\n        JSON.stringify({\n          general: {\n            defaultWorkspacePath: defaultWorkspacePath\n          }\n        })\n      );\n\n      // Launch app - should create NEW workspace\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace');\n\n      // Old corrupted file should still exist (never deleted)\n      const oldContent = fs.readFileSync(path.join(defaultWorkspacePath, 'workspace.yml'), 'utf8');\n      expect(oldContent).toContain('invalid: yaml: [[[');\n\n      // New workspace should have been created\n      const newWorkspacePath = path.join(userDataPath, 'default-workspace-1');\n      expect(fs.existsSync(newWorkspacePath)).toBe(true);\n\n      await closeElectronApp(app);\n    });\n\n    test('should create NEW workspace when workspace.yml has wrong type', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('default-workspace-recovery-wrong-type');\n\n      // Create workspace with wrong type BEFORE launching app\n      const defaultWorkspacePath = path.join(userDataPath, 'default-workspace');\n      fs.mkdirSync(defaultWorkspacePath, { recursive: true });\n      fs.writeFileSync(path.join(defaultWorkspacePath, 'workspace.yml'), `opencollection: 1.0.0\ninfo:\n  name: My Workspace\n  type: collection\ncollections:\nspecs:\ndocs: ''\n`);\n\n      // Create preferences pointing to the invalid workspace\n      fs.writeFileSync(\n        path.join(userDataPath, 'preferences.json'),\n        JSON.stringify({\n          general: {\n            defaultWorkspacePath: defaultWorkspacePath\n          }\n        })\n      );\n\n      // Launch app\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace');\n\n      // New workspace should have been created\n      const newWorkspacePath = path.join(userDataPath, 'default-workspace-1');\n      expect(fs.existsSync(newWorkspacePath)).toBe(true);\n\n      await closeElectronApp(app);\n    });\n\n    test('should create NEW workspace when directory does not exist', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('default-workspace-recovery-dir-missing');\n\n      // Create preferences pointing to non-existent directory\n      const nonExistentPath = path.join(userDataPath, 'non-existent-workspace');\n      fs.writeFileSync(\n        path.join(userDataPath, 'preferences.json'),\n        JSON.stringify({\n          general: {\n            defaultWorkspacePath: nonExistentPath\n          }\n        })\n      );\n\n      // Launch app\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace');\n\n      // New workspace should have been created (default-workspace since non-existent doesn't block)\n      const newWorkspacePath = path.join(userDataPath, 'default-workspace');\n      expect(fs.existsSync(newWorkspacePath)).toBe(true);\n      expect(fs.existsSync(path.join(newWorkspacePath, 'workspace.yml'))).toBe(true);\n\n      await closeElectronApp(app);\n    });\n  });\n\n  test.describe('UI Behavior', () => {\n    test('should display default workspace in workspace dropdown', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('default-workspace-ui-dropdown');\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      // Click on workspace name to open dropdown\n      await page.locator('.workspace-name-container').click();\n\n      // Verify default workspace is shown\n      const workspaceItem = page.locator('.workspace-item, .dropdown-item').filter({ hasText: 'My Workspace' });\n      await expect(workspaceItem.first()).toBeVisible();\n\n      await closeElectronApp(app);\n    });\n\n    test('should not show pin button for default workspace', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('default-workspace-ui-no-pin');\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await page.locator('.workspace-name-container').click();\n\n      const workspaceItem = page.locator('.workspace-item').filter({ hasText: 'My Workspace' });\n      // Default workspace should NOT have pin button\n      await expect(workspaceItem.locator('.pin-btn')).not.toBeVisible();\n\n      await closeElectronApp(app);\n    });\n  });\n});\n"
  },
  {
    "path": "tests/workspace/default-workspace/migration.spec.ts",
    "content": "import path from 'path';\nimport fs from 'fs';\nimport { test, expect, closeElectronApp } from '../../../playwright';\n\nconst env = {\n  DISABLE_SAMPLE_COLLECTION_IMPORT: 'false'\n};\n\ntest.describe('Default Workspace Migration', () => {\n  test.describe('Migration from lastOpenedCollections', () => {\n    test('should migrate collections from lastOpenedCollections to new workspace', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('default-workspace-migration');\n\n      await test.step('Setup test collection and preferences', async () => {\n        const testCollectionPath = path.join(userDataPath, 'my-old-collection');\n        fs.mkdirSync(testCollectionPath, { recursive: true });\n        fs.writeFileSync(\n          path.join(testCollectionPath, 'bruno.json'),\n          JSON.stringify({\n            version: '1',\n            name: 'My Old Collection',\n            type: 'collection'\n          })\n        );\n        fs.writeFileSync(\n          path.join(userDataPath, 'preferences.json'),\n          JSON.stringify({\n            lastOpenedCollections: [testCollectionPath]\n          })\n        );\n      });\n\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await test.step('Verify workspace UI', async () => {\n        await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace');\n      });\n\n      await test.step('Verify workspace filesystem artifacts', async () => {\n        const workspacePath = path.join(userDataPath, 'default-workspace');\n        expect(fs.existsSync(workspacePath)).toBe(true);\n\n        const workspaceYmlPath = path.join(workspacePath, 'workspace.yml');\n        expect(fs.existsSync(workspaceYmlPath)).toBe(true);\n        const workspaceYml = fs.readFileSync(workspaceYmlPath, 'utf8');\n        expect(workspaceYml).toContain('collections:');\n        expect(workspaceYml).toContain('my-old-collection');\n      });\n\n      await test.step('Cleanup', async () => {\n        await closeElectronApp(app);\n      });\n    });\n\n    test('should migrate multiple collections from lastOpenedCollections', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('default-workspace-migration-multiple');\n\n      // Create multiple test collections\n      const collection1Path = path.join(userDataPath, 'collection-1');\n      const collection2Path = path.join(userDataPath, 'collection-2');\n\n      for (const collPath of [collection1Path, collection2Path]) {\n        fs.mkdirSync(collPath, { recursive: true });\n        fs.writeFileSync(\n          path.join(collPath, 'bruno.json'),\n          JSON.stringify({\n            version: '1',\n            name: path.basename(collPath),\n            type: 'collection'\n          })\n        );\n      }\n\n      // Create old-style preferences\n      fs.writeFileSync(\n        path.join(userDataPath, 'preferences.json'),\n        JSON.stringify({\n          lastOpenedCollections: [collection1Path, collection2Path]\n        })\n      );\n\n      // Launch app\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace');\n\n      // Verify workspace.yml has both collections\n      const workspacePath = path.join(userDataPath, 'default-workspace');\n      const workspaceYmlPath = path.join(workspacePath, 'workspace.yml');\n      expect(fs.existsSync(workspaceYmlPath)).toBe(true);\n      const workspaceYml = fs.readFileSync(workspaceYmlPath, 'utf8');\n      expect(workspaceYml).toContain('collection-1');\n      expect(workspaceYml).toContain('collection-2');\n\n      await closeElectronApp(app);\n    });\n  });\n\n  test.describe('Migration does not affect existing users', () => {\n    test('should skip sample collection when user has existing collections', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('default-workspace-existing-user');\n\n      // Create a test collection (simulating existing user)\n      const oldCollectionPath = path.join(userDataPath, 'old-user-collection');\n      fs.mkdirSync(oldCollectionPath, { recursive: true });\n      fs.writeFileSync(\n        path.join(oldCollectionPath, 'bruno.json'),\n        JSON.stringify({\n          version: '1',\n          name: 'Old User Collection',\n          type: 'collection'\n        })\n      );\n\n      // Create old-style preferences with lastOpenedCollections\n      fs.writeFileSync(\n        path.join(userDataPath, 'preferences.json'),\n        JSON.stringify({\n          lastOpenedCollections: [oldCollectionPath]\n        })\n      );\n\n      // Launch app - sample collection should NOT be created (existing user)\n      const app = await launchElectronApp({ userDataPath, dotEnv: env });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      // Verify default workspace is created\n      await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace');\n\n      // Sample collection should NOT be created (because user has existing collections)\n      const sampleCollection = page.locator('#sidebar-collection-name').getByText('Sample API Collection');\n      await expect(sampleCollection).not.toBeVisible();\n\n      await closeElectronApp(app);\n    });\n  });\n\n  test.describe('No duplicate workspaces on restart', () => {\n    test('should reuse existing workspace on subsequent launches', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('default-workspace-reuse');\n\n      // First launch - creates workspace\n      const app1 = await launchElectronApp({ userDataPath });\n      const page1 = await app1.firstWindow();\n      await page1.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n      await expect(page1.getByTestId('workspace-name')).toHaveText('My Workspace');\n\n      // Verify initial workspace was created\n      const workspacePath = path.join(userDataPath, 'default-workspace');\n      expect(fs.existsSync(workspacePath)).toBe(true);\n      const originalYmlContent = fs.readFileSync(path.join(workspacePath, 'workspace.yml'), 'utf8');\n\n      await closeElectronApp(app1);\n\n      // Second launch - should reuse existing workspace\n      const app2 = await launchElectronApp({ userDataPath });\n      const page2 = await app2.firstWindow();\n      await page2.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n      await expect(page2.getByTestId('workspace-name')).toHaveText('My Workspace');\n\n      // workspace.yml should NOT have been modified\n      const currentYmlContent = fs.readFileSync(path.join(workspacePath, 'workspace.yml'), 'utf8');\n      expect(currentYmlContent).toBe(originalYmlContent);\n\n      // No new workspace should have been created\n      expect(fs.existsSync(path.join(userDataPath, 'default-workspace-1'))).toBe(false);\n\n      await closeElectronApp(app2);\n    });\n  });\n\n  test.describe('Clean installation', () => {\n    test('should create empty workspace on fresh install without old preferences', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('default-workspace-clean');\n\n      // Launch with completely empty user data (no preferences file)\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace');\n\n      // Verify workspace was created\n      const workspacePath = path.join(userDataPath, 'default-workspace');\n      expect(fs.existsSync(workspacePath)).toBe(true);\n\n      // Verify workspace has empty collections section\n      const workspaceYmlPath = path.join(workspacePath, 'workspace.yml');\n      expect(fs.existsSync(workspaceYmlPath)).toBe(true);\n      const workspaceYml = fs.readFileSync(workspaceYmlPath, 'utf8');\n      // Collections should be empty (just the key)\n      expect(workspaceYml).toMatch(/collections:\\s*\\n/);\n\n      await closeElectronApp(app);\n    });\n  });\n});\n"
  },
  {
    "path": "tests/workspace/default-workspace/recovery-and-backup.spec.ts",
    "content": "import path from 'path';\nimport fs from 'fs';\nimport { test, expect, closeElectronApp } from '../../../playwright';\n\ntest.describe('Default Workspace Recovery and Backup', () => {\n  test.describe('Global Environments Backup', () => {\n    test('should create backup file for global environments during migration', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('global-env-backup');\n\n      // Setup: Create global-environments.json\n      const globalEnvData = {\n        environments: [\n          {\n            uid: 'env1abcdefghijk123456',\n            name: 'Production',\n            variables: [\n              { uid: 'var1abcdefghijk123456', name: 'API_URL', value: 'https://api.prod.com', secret: false, type: 'text', enabled: true }\n            ]\n          },\n          {\n            uid: 'env2abcdefghijk123456',\n            name: 'Staging',\n            variables: [\n              { uid: 'var2abcdefghijk123456', name: 'API_URL', value: 'https://api.staging.com', secret: false, type: 'text', enabled: true }\n            ]\n          }\n        ],\n        activeGlobalEnvironmentUid: 'env1abcdefghijk123456'\n      };\n      fs.writeFileSync(\n        path.join(userDataPath, 'global-environments.json'),\n        JSON.stringify(globalEnvData)\n      );\n\n      // Also add lastOpenedCollections to trigger migration\n      const collectionPath = path.join(userDataPath, 'test-collection');\n      fs.mkdirSync(collectionPath, { recursive: true });\n      fs.writeFileSync(\n        path.join(collectionPath, 'bruno.json'),\n        JSON.stringify({ version: '1', name: 'Test', type: 'collection' })\n      );\n      fs.writeFileSync(\n        path.join(userDataPath, 'preferences.json'),\n        JSON.stringify({ lastOpenedCollections: [collectionPath] })\n      );\n\n      // Launch app - should trigger migration and create backup\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      // Verify backup file was created\n      const backupPath = path.join(userDataPath, 'global-environments-backup.json');\n      expect(fs.existsSync(backupPath)).toBe(true);\n\n      // Verify backup content\n      const backup = JSON.parse(fs.readFileSync(backupPath, 'utf8'));\n      expect(backup.environments).toHaveLength(2);\n      expect(backup.environments[0].name).toBe('Production');\n      expect(backup.environments[1].name).toBe('Staging');\n      expect(backup.activeGlobalEnvironmentUid).toBe('env1abcdefghijk123456');\n      expect(backup.backupDate).toBeDefined();\n\n      await closeElectronApp(app);\n    });\n\n    test('should preserve global environments backup across multiple app restarts', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('global-env-backup-persist');\n\n      // Setup: Create legacy global environments\n      const globalEnvData = {\n        environments: [\n          { uid: 'env1abcdefghijk123456', name: 'Dev', variables: [] }\n        ],\n        activeGlobalEnvironmentUid: 'env1abcdefghijk123456'\n      };\n      fs.writeFileSync(\n        path.join(userDataPath, 'global-environments.json'),\n        JSON.stringify(globalEnvData)\n      );\n\n      // Add collection to trigger migration\n      const collectionPath = path.join(userDataPath, 'test-collection');\n      fs.mkdirSync(collectionPath, { recursive: true });\n      fs.writeFileSync(\n        path.join(collectionPath, 'bruno.json'),\n        JSON.stringify({ version: '1', name: 'Test', type: 'collection' })\n      );\n      fs.writeFileSync(\n        path.join(userDataPath, 'preferences.json'),\n        JSON.stringify({ lastOpenedCollections: [collectionPath] })\n      );\n\n      // First launch\n      const app1 = await launchElectronApp({ userDataPath });\n      const page1 = await app1.firstWindow();\n      await page1.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n      await closeElectronApp(app1);\n\n      // Verify backup exists\n      const backupPath = path.join(userDataPath, 'global-environments-backup.json');\n      expect(fs.existsSync(backupPath)).toBe(true);\n      const backupContentAfterFirst = fs.readFileSync(backupPath, 'utf8');\n\n      // Second launch - backup should still exist\n      const app2 = await launchElectronApp({ userDataPath });\n      const page2 = await app2.firstWindow();\n      await page2.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      // Backup should not be modified on second launch\n      expect(fs.existsSync(backupPath)).toBe(true);\n      const backupContentAfterSecond = fs.readFileSync(backupPath, 'utf8');\n      expect(backupContentAfterSecond).toBe(backupContentAfterFirst);\n\n      await closeElectronApp(app2);\n    });\n  });\n\n  test.describe('lastOpenedCollections Preservation', () => {\n    test('should NOT delete lastOpenedCollections from preferences after migration', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('preserve-last-opened');\n\n      // Setup: Create a valid collection\n      const collectionPath = path.join(userDataPath, 'my-collection');\n      fs.mkdirSync(collectionPath, { recursive: true });\n      fs.writeFileSync(\n        path.join(collectionPath, 'bruno.json'),\n        JSON.stringify({ version: '1', name: 'My Collection', type: 'collection' })\n      );\n\n      // Setup: Create preferences with lastOpenedCollections\n      fs.writeFileSync(\n        path.join(userDataPath, 'preferences.json'),\n        JSON.stringify({ lastOpenedCollections: [collectionPath] })\n      );\n\n      // Launch app - triggers migration\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n      await closeElectronApp(app);\n\n      // Verify lastOpenedCollections is still in preferences\n      const prefsPath = path.join(userDataPath, 'preferences.json');\n      const prefs = JSON.parse(fs.readFileSync(prefsPath, 'utf8'));\n      expect(prefs.lastOpenedCollections).toBeDefined();\n      expect(prefs.lastOpenedCollections).toContain(collectionPath);\n    });\n  });\n\n  test.describe('Workspace Discovery (No Path in Preferences)', () => {\n    test('should find and use existing valid default workspace when path not in preferences', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('discover-existing');\n\n      // Setup: Create a valid default workspace manually (without setting in preferences)\n      const workspacePath = path.join(userDataPath, 'default-workspace');\n      fs.mkdirSync(workspacePath, { recursive: true });\n      fs.mkdirSync(path.join(workspacePath, 'collections'), { recursive: true });\n      fs.mkdirSync(path.join(workspacePath, 'environments'), { recursive: true });\n      fs.writeFileSync(\n        path.join(workspacePath, 'workspace.yml'),\n        `opencollection: 1.0.0\ninfo:\n  name: \"My Workspace\"\n  type: workspace\ncollections:\nspecs:\ndocs: ''\n`\n      );\n\n      // Create empty preferences (no defaultWorkspacePath)\n      fs.writeFileSync(\n        path.join(userDataPath, 'preferences.json'),\n        JSON.stringify({})\n      );\n\n      // Launch app - should discover and use existing workspace\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      // UI always shows \"My Workspace\"\n      await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace');\n\n      // Should NOT create a new workspace\n      expect(fs.existsSync(path.join(userDataPath, 'default-workspace-1'))).toBe(false);\n\n      // Preferences should now have the path set (electron-store saves under 'preferences' key)\n      const prefs = JSON.parse(fs.readFileSync(path.join(userDataPath, 'preferences.json'), 'utf8'));\n      expect(prefs.preferences?.general?.defaultWorkspacePath).toBe(workspacePath);\n\n      await closeElectronApp(app);\n    });\n\n    test('should find latest numbered workspace when multiple exist and path not in preferences', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('discover-numbered');\n\n      // Setup: Create multiple numbered workspaces\n      const workspace0 = path.join(userDataPath, 'default-workspace');\n      const workspace1 = path.join(userDataPath, 'default-workspace-1');\n      const workspace2 = path.join(userDataPath, 'default-workspace-2');\n\n      for (const wsPath of [workspace0, workspace1, workspace2]) {\n        fs.mkdirSync(wsPath, { recursive: true });\n        fs.mkdirSync(path.join(wsPath, 'environments'), { recursive: true });\n        fs.writeFileSync(\n          path.join(wsPath, 'workspace.yml'),\n          `opencollection: 1.0.0\ninfo:\n  name: \"My Workspace\"\n  type: workspace\ncollections:\nspecs:\ndocs: ''\n`\n        );\n      }\n\n      // Create empty preferences\n      fs.writeFileSync(\n        path.join(userDataPath, 'preferences.json'),\n        JSON.stringify({})\n      );\n\n      // Launch app - should use workspace-2 (latest/highest number)\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace');\n\n      // Verify the correct workspace was selected (workspace-2)\n      const prefs = JSON.parse(fs.readFileSync(path.join(userDataPath, 'preferences.json'), 'utf8'));\n      expect(prefs.preferences?.general?.defaultWorkspacePath).toBe(workspace2);\n\n      // No new workspace should be created\n      expect(fs.existsSync(path.join(userDataPath, 'default-workspace-3'))).toBe(false);\n\n      await closeElectronApp(app);\n    });\n\n    test('should skip invalid workspaces and use latest valid one', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('discover-skip-invalid');\n\n      // Setup: Create workspaces where latest is invalid\n      const workspace0 = path.join(userDataPath, 'default-workspace');\n      const workspace1 = path.join(userDataPath, 'default-workspace-1');\n      const workspace2 = path.join(userDataPath, 'default-workspace-2');\n\n      // workspace-0: valid\n      fs.mkdirSync(workspace0, { recursive: true });\n      fs.writeFileSync(\n        path.join(workspace0, 'workspace.yml'),\n        `opencollection: 1.0.0\ninfo:\n  name: \"My Workspace\"\n  type: workspace\ncollections:\nspecs:\ndocs: ''\n`\n      );\n\n      // workspace-1: valid (should be selected as highest valid)\n      fs.mkdirSync(workspace1, { recursive: true });\n      fs.writeFileSync(\n        path.join(workspace1, 'workspace.yml'),\n        `opencollection: 1.0.0\ninfo:\n  name: \"My Workspace\"\n  type: workspace\ncollections:\nspecs:\ndocs: ''\n`\n      );\n\n      // workspace-2: invalid (corrupt YAML)\n      fs.mkdirSync(workspace2, { recursive: true });\n      fs.writeFileSync(path.join(workspace2, 'workspace.yml'), 'invalid: yaml: [[[');\n\n      // Create empty preferences\n      fs.writeFileSync(\n        path.join(userDataPath, 'preferences.json'),\n        JSON.stringify({})\n      );\n\n      // Launch app - should skip workspace-2, use workspace-1\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace');\n\n      // Verify workspace-1 was selected (not workspace-2 which is broken)\n      const prefs = JSON.parse(fs.readFileSync(path.join(userDataPath, 'preferences.json'), 'utf8'));\n      expect(prefs.preferences?.general?.defaultWorkspacePath).toBe(workspace1);\n\n      await closeElectronApp(app);\n    });\n  });\n\n  test.describe('Recovery from Broken Workspace', () => {\n    test('should recover collections from broken workspace to new workspace', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('recover-collections');\n\n      // Setup: Create a valid collection\n      const collectionPath = path.join(userDataPath, 'external-collection');\n      fs.mkdirSync(collectionPath, { recursive: true });\n      fs.writeFileSync(\n        path.join(collectionPath, 'bruno.json'),\n        JSON.stringify({ version: '1', name: 'External Collection', type: 'collection' })\n      );\n\n      // Setup: Create a \"broken\" workspace with valid workspace.yml but invalid internal state\n      const brokenWorkspace = path.join(userDataPath, 'default-workspace');\n      fs.mkdirSync(brokenWorkspace, { recursive: true });\n      fs.mkdirSync(path.join(brokenWorkspace, 'environments'), { recursive: true });\n      // Write a valid workspace.yml that references the collection\n      fs.writeFileSync(\n        path.join(brokenWorkspace, 'workspace.yml'),\n        `opencollection: 1.0.0\ninfo:\n  name: \"Old Workspace\"\n  type: workspace\ncollections:\n  - name: \"External Collection\"\n    path: \"${collectionPath}\"\nspecs:\ndocs: ''\n`\n      );\n\n      // Now corrupt it\n      fs.writeFileSync(path.join(brokenWorkspace, 'workspace.yml'), 'invalid: yaml: [[[');\n\n      // Set preferences to point to broken workspace\n      fs.writeFileSync(\n        path.join(userDataPath, 'preferences.json'),\n        JSON.stringify({\n          general: { defaultWorkspacePath: brokenWorkspace }\n        })\n      );\n\n      // Launch app - should recover collections and create new workspace\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      // New workspace should be created\n      const newWorkspace = path.join(userDataPath, 'default-workspace-1');\n      expect(fs.existsSync(newWorkspace)).toBe(true);\n\n      await closeElectronApp(app);\n    });\n\n    test('should recover environments from broken workspace to new workspace', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('recover-envs');\n\n      // Setup: Create a workspace with environments\n      const brokenWorkspace = path.join(userDataPath, 'default-workspace');\n      fs.mkdirSync(brokenWorkspace, { recursive: true });\n      const envDir = path.join(brokenWorkspace, 'environments');\n      fs.mkdirSync(envDir, { recursive: true });\n\n      // Create environment files\n      fs.writeFileSync(\n        path.join(envDir, 'production.yml'),\n        `name: production\nvariables:\n  - uid: var1\n    name: API_URL\n    value: https://api.prod.com\n    enabled: true\n    secret: false\n    type: text\n`\n      );\n      fs.writeFileSync(\n        path.join(envDir, 'staging.yml'),\n        `name: staging\nvariables:\n  - uid: var2\n    name: API_URL\n    value: https://api.staging.com\n    enabled: true\n    secret: false\n    type: text\n`\n      );\n\n      // Create valid workspace.yml first\n      fs.writeFileSync(\n        path.join(brokenWorkspace, 'workspace.yml'),\n        `opencollection: 1.0.0\ninfo:\n  name: \"Old Workspace\"\n  type: workspace\ncollections:\nspecs:\ndocs: ''\n`\n      );\n\n      // Now corrupt it\n      fs.writeFileSync(path.join(brokenWorkspace, 'workspace.yml'), 'broken: [[[');\n\n      // Set preferences\n      fs.writeFileSync(\n        path.join(userDataPath, 'preferences.json'),\n        JSON.stringify({\n          general: { defaultWorkspacePath: brokenWorkspace }\n        })\n      );\n\n      // Launch app\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      // New workspace should have recovered environments\n      const newWorkspace = path.join(userDataPath, 'default-workspace-1');\n      const newEnvDir = path.join(newWorkspace, 'environments');\n      expect(fs.existsSync(newEnvDir)).toBe(true);\n      expect(fs.existsSync(path.join(newEnvDir, 'production.yml'))).toBe(true);\n      expect(fs.existsSync(path.join(newEnvDir, 'staging.yml'))).toBe(true);\n\n      await closeElectronApp(app);\n    });\n\n    test('should use lastOpenedCollections as fallback when workspace config parsing fails', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('recover-fallback');\n\n      // Setup: Create a valid collection\n      const collectionPath = path.join(userDataPath, 'fallback-collection');\n      fs.mkdirSync(collectionPath, { recursive: true });\n      fs.writeFileSync(\n        path.join(collectionPath, 'bruno.json'),\n        JSON.stringify({ version: '1', name: 'Fallback Collection', type: 'collection' })\n      );\n\n      // Setup: Create broken workspace with NO valid config to recover from\n      const brokenWorkspace = path.join(userDataPath, 'default-workspace');\n      fs.mkdirSync(brokenWorkspace, { recursive: true });\n      fs.writeFileSync(path.join(brokenWorkspace, 'workspace.yml'), 'totally: broken: [[[');\n\n      // Set preferences with lastOpenedCollections AND point to broken workspace\n      fs.writeFileSync(\n        path.join(userDataPath, 'preferences.json'),\n        JSON.stringify({\n          general: { defaultWorkspacePath: brokenWorkspace },\n          lastOpenedCollections: [collectionPath]\n        })\n      );\n\n      // Launch app\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      // New workspace should have the collection from lastOpenedCollections\n      const newWorkspace = path.join(userDataPath, 'default-workspace-1');\n      expect(fs.existsSync(newWorkspace)).toBe(true);\n\n      const workspaceYml = fs.readFileSync(path.join(newWorkspace, 'workspace.yml'), 'utf8');\n      expect(workspaceYml).toContain('fallback-collection');\n\n      await closeElectronApp(app);\n    });\n  });\n\n  test.describe('Recovery from Non-Existent Workspace Path', () => {\n    test('should recover from previously created workspace when path in preferences does not exist', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('recover-from-old');\n\n      // Setup: Create a valid collection\n      const collectionPath = path.join(userDataPath, 'old-collection');\n      fs.mkdirSync(collectionPath, { recursive: true });\n      fs.writeFileSync(\n        path.join(collectionPath, 'bruno.json'),\n        JSON.stringify({ version: '1', name: 'Old Collection', type: 'collection' })\n      );\n\n      // Setup: Create an old default workspace (simulating previously created)\n      const oldWorkspace = path.join(userDataPath, 'default-workspace');\n      fs.mkdirSync(oldWorkspace, { recursive: true });\n      fs.mkdirSync(path.join(oldWorkspace, 'environments'), { recursive: true });\n      fs.writeFileSync(\n        path.join(oldWorkspace, 'workspace.yml'),\n        `opencollection: 1.0.0\ninfo:\n  name: \"My Workspace\"\n  type: workspace\ncollections:\n  - name: \"Old Collection\"\n    path: \"${collectionPath}\"\nspecs:\ndocs: ''\n`\n      );\n\n      // Set preferences to point to non-existent path\n      fs.writeFileSync(\n        path.join(userDataPath, 'preferences.json'),\n        JSON.stringify({\n          general: { defaultWorkspacePath: '/non/existent/path/workspace' }\n        })\n      );\n\n      // Launch app - should find and use the existing valid workspace\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace');\n\n      // Since path doesn't exist but we have a valid workspace, it should use it\n      // OR create a new one recovering from the existing one\n      const prefs = JSON.parse(fs.readFileSync(path.join(userDataPath, 'preferences.json'), 'utf8'));\n      // Either uses the existing workspace or creates workspace-1\n      const usedExisting = prefs.preferences?.general?.defaultWorkspacePath === oldWorkspace;\n      const createdNew = fs.existsSync(path.join(userDataPath, 'default-workspace-1'));\n      expect(usedExisting || createdNew).toBe(true);\n\n      await closeElectronApp(app);\n    });\n\n    test('should recover from latest workspace when path does not exist and multiple workspaces exist', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('recover-from-latest');\n\n      // Create collection\n      const collectionPath = path.join(userDataPath, 'latest-collection');\n      fs.mkdirSync(collectionPath, { recursive: true });\n      fs.writeFileSync(\n        path.join(collectionPath, 'bruno.json'),\n        JSON.stringify({ version: '1', name: 'Latest Collection', type: 'collection' })\n      );\n\n      // Create older collection\n      const oldCollectionPath = path.join(userDataPath, 'old-collection');\n      fs.mkdirSync(oldCollectionPath, { recursive: true });\n      fs.writeFileSync(\n        path.join(oldCollectionPath, 'bruno.json'),\n        JSON.stringify({ version: '1', name: 'Old Collection', type: 'collection' })\n      );\n\n      // Create workspace-0 (older)\n      const workspace0 = path.join(userDataPath, 'default-workspace');\n      fs.mkdirSync(workspace0, { recursive: true });\n      fs.mkdirSync(path.join(workspace0, 'environments'), { recursive: true });\n      fs.writeFileSync(\n        path.join(workspace0, 'workspace.yml'),\n        `opencollection: 1.0.0\ninfo:\n  name: \"My Workspace\"\n  type: workspace\ncollections:\n  - name: \"Old Collection\"\n    path: \"${oldCollectionPath}\"\nspecs:\ndocs: ''\n`\n      );\n\n      // Create workspace-1 (newer - should be used)\n      const workspace1 = path.join(userDataPath, 'default-workspace-1');\n      fs.mkdirSync(workspace1, { recursive: true });\n      fs.mkdirSync(path.join(workspace1, 'environments'), { recursive: true });\n      fs.writeFileSync(\n        path.join(workspace1, 'workspace.yml'),\n        `opencollection: 1.0.0\ninfo:\n  name: \"My Workspace\"\n  type: workspace\ncollections:\n  - name: \"Latest Collection\"\n    path: \"${collectionPath}\"\nspecs:\ndocs: ''\n`\n      );\n\n      // Set preferences to non-existent path\n      fs.writeFileSync(\n        path.join(userDataPath, 'preferences.json'),\n        JSON.stringify({\n          general: { defaultWorkspacePath: '/deleted/workspace/path' }\n        })\n      );\n\n      // Launch app - should use workspace-1 (latest valid)\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace');\n\n      // Verify workspace-1 was used (or workspace-2 was created recovering from workspace-1)\n      const prefs = JSON.parse(fs.readFileSync(path.join(userDataPath, 'preferences.json'), 'utf8'));\n      const usedWorkspace1 = prefs.preferences?.general?.defaultWorkspacePath === workspace1;\n      const createdWorkspace2 = fs.existsSync(path.join(userDataPath, 'default-workspace-2'));\n      expect(usedWorkspace1 || createdWorkspace2).toBe(true);\n\n      await closeElectronApp(app);\n    });\n  });\n\n  test.describe('App Restart After Breaking Workspace', () => {\n    test('should recover data after workspace is corrupted between app restarts', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('restart-after-break');\n\n      // Setup collection\n      const collectionPath = path.join(userDataPath, 'important-collection');\n      fs.mkdirSync(collectionPath, { recursive: true });\n      fs.writeFileSync(\n        path.join(collectionPath, 'bruno.json'),\n        JSON.stringify({ version: '1', name: 'Important Collection', type: 'collection' })\n      );\n\n      // First launch - creates workspace\n      const app1 = await launchElectronApp({ userDataPath });\n      const page1 = await app1.firstWindow();\n      await page1.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      // Verify workspace was created\n      const workspacePath = path.join(userDataPath, 'default-workspace');\n      expect(fs.existsSync(workspacePath)).toBe(true);\n\n      await closeElectronApp(app1);\n\n      // Now add collection to the workspace\n      const workspaceYmlPath = path.join(workspacePath, 'workspace.yml');\n      fs.writeFileSync(\n        workspaceYmlPath,\n        `opencollection: 1.0.0\ninfo:\n  name: \"My Workspace\"\n  type: workspace\ncollections:\n  - name: \"Important Collection\"\n    path: \"${collectionPath}\"\nspecs:\ndocs: ''\n`\n      );\n\n      // Create environment in workspace\n      const envDir = path.join(workspacePath, 'environments');\n      fs.mkdirSync(envDir, { recursive: true });\n      fs.writeFileSync(\n        path.join(envDir, 'myenv.yml'),\n        `name: myenv\nvariables:\n  - uid: v1\n    name: KEY\n    value: secret123\n    enabled: true\n    secret: false\n    type: text\n`\n      );\n\n      // CORRUPT the workspace\n      fs.writeFileSync(workspaceYmlPath, 'corrupted: [[[');\n\n      // Second launch - should recover\n      const app2 = await launchElectronApp({ userDataPath });\n      const page2 = await app2.firstWindow();\n      await page2.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      // New workspace should exist\n      const newWorkspace = path.join(userDataPath, 'default-workspace-1');\n      expect(fs.existsSync(newWorkspace)).toBe(true);\n\n      // Environment should be recovered\n      expect(fs.existsSync(path.join(newWorkspace, 'environments', 'myenv.yml'))).toBe(true);\n\n      await closeElectronApp(app2);\n    });\n\n    test('should handle workspace deleted between app restarts', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('restart-after-delete');\n\n      // First launch - creates workspace\n      const app1 = await launchElectronApp({ userDataPath });\n      const page1 = await app1.firstWindow();\n      await page1.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      const workspacePath = path.join(userDataPath, 'default-workspace');\n      expect(fs.existsSync(workspacePath)).toBe(true);\n\n      await closeElectronApp(app1);\n\n      // DELETE the workspace directory\n      fs.rmSync(workspacePath, { recursive: true, force: true });\n      expect(fs.existsSync(workspacePath)).toBe(false);\n\n      // Second launch - should create new workspace\n      const app2 = await launchElectronApp({ userDataPath });\n      const page2 = await app2.firstWindow();\n      await page2.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      // New workspace should be created at default-workspace (since it was deleted)\n      expect(fs.existsSync(workspacePath)).toBe(true);\n      expect(fs.existsSync(path.join(workspacePath, 'workspace.yml'))).toBe(true);\n\n      await closeElectronApp(app2);\n    });\n\n    test('should preserve all data through multiple corruption and recovery cycles', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('multiple-recovery-cycles');\n\n      // Create collection\n      const collectionPath = path.join(userDataPath, 'persistent-collection');\n      fs.mkdirSync(collectionPath, { recursive: true });\n      fs.writeFileSync(\n        path.join(collectionPath, 'bruno.json'),\n        JSON.stringify({ version: '1', name: 'Persistent Collection', type: 'collection' })\n      );\n\n      // Create preferences with lastOpenedCollections (no global environments for simpler test)\n      fs.writeFileSync(\n        path.join(userDataPath, 'preferences.json'),\n        JSON.stringify({ lastOpenedCollections: [collectionPath] })\n      );\n\n      // First launch\n      const app1 = await launchElectronApp({ userDataPath });\n      const page1 = await app1.firstWindow();\n      await page1.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n      await closeElectronApp(app1);\n\n      // Verify workspace-0 created\n      const ws0 = path.join(userDataPath, 'default-workspace');\n      expect(fs.existsSync(ws0)).toBe(true);\n\n      // Add an environment to workspace-0\n      const envDir0 = path.join(ws0, 'environments');\n      fs.mkdirSync(envDir0, { recursive: true });\n      fs.writeFileSync(\n        path.join(envDir0, 'PersistentEnv.yml'),\n        `name: PersistentEnv\nvariables: []\n`\n      );\n\n      // Corrupt workspace-0\n      fs.writeFileSync(path.join(ws0, 'workspace.yml'), 'broken1: [[[');\n\n      // Second launch - recovery to workspace-1\n      const app2 = await launchElectronApp({ userDataPath });\n      const page2 = await app2.firstWindow();\n      await page2.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n      await closeElectronApp(app2);\n\n      // Verify workspace-1 created with recovered data\n      const ws1 = path.join(userDataPath, 'default-workspace-1');\n      expect(fs.existsSync(ws1)).toBe(true);\n      expect(fs.existsSync(path.join(ws1, 'environments', 'PersistentEnv.yml'))).toBe(true);\n\n      const ws1Yml = fs.readFileSync(path.join(ws1, 'workspace.yml'), 'utf8');\n      expect(ws1Yml).toContain('persistent-collection');\n\n      // Corrupt workspace-1\n      fs.writeFileSync(path.join(ws1, 'workspace.yml'), 'broken2: [[[');\n\n      // Third launch - recovery to workspace-2\n      const app3 = await launchElectronApp({ userDataPath });\n      const page3 = await app3.firstWindow();\n      await page3.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      // Verify workspace-2 created with all data preserved\n      const ws2 = path.join(userDataPath, 'default-workspace-2');\n      expect(fs.existsSync(ws2)).toBe(true);\n      expect(fs.existsSync(path.join(ws2, 'environments', 'PersistentEnv.yml'))).toBe(true);\n\n      const ws2Yml = fs.readFileSync(path.join(ws2, 'workspace.yml'), 'utf8');\n      expect(ws2Yml).toContain('persistent-collection');\n\n      await closeElectronApp(app3);\n    });\n  });\n\n  test.describe('Edge Cases', () => {\n    test('should handle empty environments directory during recovery', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('empty-env-dir');\n\n      // Create workspace with empty environments dir\n      const workspace = path.join(userDataPath, 'default-workspace');\n      fs.mkdirSync(workspace, { recursive: true });\n      fs.mkdirSync(path.join(workspace, 'environments'), { recursive: true });\n      fs.writeFileSync(path.join(workspace, 'workspace.yml'), 'broken: [[[');\n\n      fs.writeFileSync(\n        path.join(userDataPath, 'preferences.json'),\n        JSON.stringify({ general: { defaultWorkspacePath: workspace } })\n      );\n\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      // Should not crash, new workspace created\n      const newWorkspace = path.join(userDataPath, 'default-workspace-1');\n      expect(fs.existsSync(newWorkspace)).toBe(true);\n\n      await closeElectronApp(app);\n    });\n\n    test('should handle missing environments directory during recovery', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('missing-env-dir');\n\n      // Create workspace WITHOUT environments dir\n      const workspace = path.join(userDataPath, 'default-workspace');\n      fs.mkdirSync(workspace, { recursive: true });\n      fs.writeFileSync(path.join(workspace, 'workspace.yml'), 'broken: [[[');\n\n      fs.writeFileSync(\n        path.join(userDataPath, 'preferences.json'),\n        JSON.stringify({ general: { defaultWorkspacePath: workspace } })\n      );\n\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      // Should not crash\n      expect(fs.existsSync(path.join(userDataPath, 'default-workspace-1'))).toBe(true);\n\n      await closeElectronApp(app);\n    });\n\n    test('should deduplicate collections between recovered and preference sources', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('dedup-collections');\n\n      // Create collection\n      const collectionPath = path.join(userDataPath, 'shared-collection');\n      fs.mkdirSync(collectionPath, { recursive: true });\n      fs.writeFileSync(\n        path.join(collectionPath, 'bruno.json'),\n        JSON.stringify({ version: '1', name: 'Shared Collection', type: 'collection' })\n      );\n\n      // Create workspace with the collection (but it will be corrupted)\n      const workspace = path.join(userDataPath, 'default-workspace');\n      fs.mkdirSync(workspace, { recursive: true });\n      fs.mkdirSync(path.join(workspace, 'environments'), { recursive: true });\n      // Workspace is created but immediately corrupted - no valid config to recover collections from\n      fs.writeFileSync(path.join(workspace, 'workspace.yml'), 'broken: [[[');\n\n      // Add same collection to lastOpenedCollections\n      fs.writeFileSync(\n        path.join(userDataPath, 'preferences.json'),\n        JSON.stringify({\n          general: { defaultWorkspacePath: workspace },\n          lastOpenedCollections: [collectionPath]\n        })\n      );\n\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      // New workspace should have collection only ONCE (no duplicates)\n      const newWorkspace = path.join(userDataPath, 'default-workspace-1');\n      const yml = fs.readFileSync(path.join(newWorkspace, 'workspace.yml'), 'utf8');\n\n      // Count collection entries by counting \"- name:\" patterns (each collection has one)\n      const collectionEntries = yml.match(/- name:/g);\n      expect(collectionEntries).toHaveLength(1);\n\n      await closeElectronApp(app);\n    });\n\n    test('should not overwrite recovered environments with global environments of same name', async ({ launchElectronApp, createTmpDir }) => {\n      const userDataPath = await createTmpDir('env-no-overwrite');\n\n      // Create workspace with environment\n      const workspace = path.join(userDataPath, 'default-workspace');\n      fs.mkdirSync(workspace, { recursive: true });\n      const envDir = path.join(workspace, 'environments');\n      fs.mkdirSync(envDir, { recursive: true });\n\n      // Environment in workspace (should be preserved)\n      fs.writeFileSync(\n        path.join(envDir, 'Production.yml'),\n        `name: Production\nvariables:\n  - uid: v1\n    name: URL\n    value: workspace-value\n    enabled: true\n    secret: false\n    type: text\n`\n      );\n\n      // Corrupt workspace.yml\n      fs.writeFileSync(path.join(workspace, 'workspace.yml'), 'broken: [[[');\n\n      // Create global environments with same name but different value\n      fs.writeFileSync(\n        path.join(userDataPath, 'global-environments.json'),\n        JSON.stringify({\n          environments: [{\n            uid: 'env1abcdefghijk123456',\n            name: 'Production',\n            variables: [{ uid: 'var1abcdefghijk123456', name: 'URL', value: 'global-value', secret: false, type: 'text', enabled: true }]\n          }],\n          activeGlobalEnvironmentUid: 'env1abcdefghijk123456'\n        })\n      );\n\n      fs.writeFileSync(\n        path.join(userDataPath, 'preferences.json'),\n        JSON.stringify({ general: { defaultWorkspacePath: workspace } })\n      );\n\n      const app = await launchElectronApp({ userDataPath });\n      const page = await app.firstWindow();\n      await page.locator('[data-app-state=\"loaded\"]').waitFor({ timeout: 30000 });\n\n      // Check new workspace has the recovered environment (not overwritten by global)\n      const newWorkspace = path.join(userDataPath, 'default-workspace-1');\n      const envContent = fs.readFileSync(path.join(newWorkspace, 'environments', 'Production.yml'), 'utf8');\n      expect(envContent).toContain('workspace-value');\n      expect(envContent).not.toContain('global-value');\n\n      await closeElectronApp(app);\n    });\n  });\n});\n"
  }
]